fix(theme): polish dark header and toggle

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-09-01 15:42:39 +08:00
parent fa13dd54ee
commit 0d48945fd2
6 changed files with 55 additions and 41 deletions

View file

@ -59,9 +59,13 @@ test.describe('Light and dark theme', () => {
await page.goto('/')
await expect(page.locator('html')).not.toHaveClass(/dark/)
const header = page.locator('header')
const lightHeaderBackground = await header.evaluate((element) => getComputedStyle(element).backgroundColor)
await page.getByRole('button', { name: 'Switch to dark theme' }).click()
await expect(page.locator('html')).toHaveClass(/dark/)
await expect.poll(() => header.evaluate((element) => getComputedStyle(element).backgroundColor))
.not.toBe(lightHeaderBackground)
await expect.poll(() => page.evaluate(() => window.localStorage.getItem('skillhub-theme'))).toBe('dark')
await page.reload()
@ -73,7 +77,7 @@ test.describe('Light and dark theme', () => {
).__themeAtFirstReactContent)).toBe(true)
await page.getByRole('link', { name: 'Search', exact: true }).first().click()
await expect(page).toHaveURL(/\/search$/)
await expect(page).toHaveURL(/\/search(?:\?|$)/)
await expect(page.locator('html')).toHaveClass(/dark/)
await page.screenshot({ path: testInfo.outputPath('dark-desktop.png'), fullPage: true })

View file

@ -3,7 +3,11 @@ import { APP_HEADER_ELEVATED_CLASS_NAME, getAppHeaderClassName } from './layout-
describe('getAppHeaderClassName', () => {
it('keeps the header flat before the page starts scrolling', () => {
expect(getAppHeaderClassName(false)).not.toContain(APP_HEADER_ELEVATED_CLASS_NAME)
const className = getAppHeaderClassName(false)
expect(className).not.toContain(APP_HEADER_ELEVATED_CLASS_NAME)
expect(className).toContain('bg-background/90')
expect(className).not.toContain('bg-white')
})
it('adds a subtle drop shadow after the header becomes sticky', () => {

View file

@ -1,9 +1,10 @@
import { cn } from '@/shared/lib/utils'
export const APP_HEADER_BASE_CLASS_NAME =
'sticky top-0 z-50 flex items-center justify-between border-b bg-white px-6 py-4 transition-shadow duration-200 md:px-12'
'sticky top-0 z-50 flex items-center justify-between border-b border-border/70 bg-background/90 px-6 py-4 backdrop-blur-xl transition-[background-color,border-color,box-shadow] duration-200 supports-[backdrop-filter]:bg-background/80 md:px-12'
export const APP_HEADER_ELEVATED_CLASS_NAME = 'shadow-[0_10px_24px_-20px_rgba(15,23,42,0.32)]'
export const APP_HEADER_ELEVATED_CLASS_NAME =
'shadow-[0_12px_30px_-24px_hsl(var(--foreground)/0.45)]'
export function getAppHeaderClassName(isElevated: boolean): string {
return cn(APP_HEADER_BASE_CLASS_NAME, isElevated && APP_HEADER_ELEVATED_CLASS_NAME)

View file

@ -83,7 +83,7 @@ export function Layout() {
<div
className="absolute top-0 right-0 w-[600px] h-[500px] rounded-full opacity-90"
style={{
background: 'radial-gradient(ellipse at 70% 20%, rgba(184,94,255,0.25) 0%, rgba(106,109,255,0.15) 40%, transparent 70%)',
background: 'radial-gradient(ellipse at 70% 20%, hsl(var(--glow-accent) / 0.12) 0%, hsl(var(--glow-primary) / 0.07) 40%, transparent 70%)',
filter: 'blur(60px)',
}}
/>

View file

@ -51,33 +51,33 @@
}
.dark {
/* Dark theme (preserved, not default) */
--background: 222 47% 6%;
--foreground: 210 40% 96%;
--card: 222 40% 9%;
--card-foreground: 210 40% 96%;
--popover: 222 40% 9%;
--popover-foreground: 210 40% 96%;
--primary: 239 100% 75%;
--primary-foreground: 222 47% 6%;
--secondary: 222 30% 13%;
--secondary-foreground: 210 30% 85%;
--muted: 222 25% 15%;
--muted-foreground: 215 20% 55%;
--accent: 271 100% 72%;
/* Cool ink surfaces: distinct depth without near-black dead zones. */
--background: 222 30% 10%;
--foreground: 216 28% 93%;
--card: 222 25% 14%;
--card-foreground: 216 28% 93%;
--popover: 222 24% 15%;
--popover-foreground: 216 28% 93%;
--primary: 241 92% 74%;
--primary-foreground: 222 30% 10%;
--secondary: 222 20% 18%;
--secondary-foreground: 216 24% 88%;
--muted: 222 18% 19%;
--muted-foreground: 216 14% 66%;
--accent: 272 86% 72%;
--accent-foreground: 0 0% 100%;
--destructive: 0 72% 55%;
--destructive: 0 72% 61%;
--destructive-foreground: 0 0% 100%;
--border: 222 20% 18%;
--input: 222 20% 18%;
--ring: 239 100% 75%;
--surface-glass: 222 35% 11%;
--glow-primary: 239 100% 75%;
--glow-accent: 271 100% 72%;
--text-secondary: 215 20% 72%;
--text-muted: 215 16% 63%;
--text-placeholder: 215 14% 48%;
--border-card: 222 20% 18%;
--border: 222 16% 25%;
--input: 222 16% 25%;
--ring: 241 92% 74%;
--surface-glass: 222 24% 14%;
--glow-primary: 241 92% 70%;
--glow-accent: 272 80% 68%;
--text-secondary: 216 17% 76%;
--text-muted: 216 14% 66%;
--text-placeholder: 216 12% 54%;
--border-card: 222 16% 24%;
}
}

View file

@ -21,24 +21,29 @@ export function ThemeToggle({ className }: ThemeToggleProps) {
title={label}
onClick={toggleTheme}
className={cn(
'group relative inline-flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded-full border border-border/70 bg-card/80 text-muted-foreground shadow-sm transition-colors duration-200 hover:border-primary/35 hover:bg-secondary hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
'group relative inline-flex h-10 w-[4.5rem] shrink-0 items-center rounded-full border border-border bg-muted/70 p-1 text-muted-foreground shadow-sm transition-[background-color,border-color] duration-200 hover:border-primary/40 hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
className,
)}
>
<Sun
<span
aria-hidden="true"
className={cn(
'absolute h-[18px] w-[18px] transition-[opacity,transform] duration-200 motion-reduce:transition-none',
isDark ? 'rotate-90 scale-75 opacity-0' : 'rotate-0 scale-100 opacity-100',
)}
/>
<Moon
aria-hidden="true"
className={cn(
'absolute h-[18px] w-[18px] transition-[opacity,transform] duration-200 motion-reduce:transition-none',
isDark ? 'rotate-0 scale-100 opacity-100' : '-rotate-90 scale-75 opacity-0',
'absolute left-1 top-1 h-8 w-8 rounded-full border border-border/80 bg-card shadow-[0_3px_10px_-4px_hsl(var(--foreground)/0.45)] transition-transform duration-200 ease-out motion-reduce:transition-none',
isDark && 'translate-x-8',
)}
/>
<span className="relative z-10 inline-flex h-8 w-8 items-center justify-center">
<Sun
aria-hidden="true"
className={cn('h-4 w-4 transition-colors duration-200', !isDark && 'text-foreground')}
/>
</span>
<span className="relative z-10 inline-flex h-8 w-8 items-center justify-center">
<Moon
aria-hidden="true"
className={cn('h-4 w-4 transition-colors duration-200', isDark && 'text-foreground')}
/>
</span>
</button>
)
}