From 0d48945fd21a0198c22348e823ffbf609472be39 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:42:39 +0800 Subject: [PATCH] fix(theme): polish dark header and toggle Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- web/e2e/theme-toggle.spec.ts | 6 ++- web/src/app/layout-header-style.test.ts | 6 ++- web/src/app/layout-header-style.ts | 5 ++- web/src/app/layout.tsx | 2 +- web/src/index.css | 50 +++++++++++----------- web/src/shared/components/theme-toggle.tsx | 27 +++++++----- 6 files changed, 55 insertions(+), 41 deletions(-) diff --git a/web/e2e/theme-toggle.spec.ts b/web/e2e/theme-toggle.spec.ts index d7a564dd..0983b75a 100644 --- a/web/e2e/theme-toggle.spec.ts +++ b/web/e2e/theme-toggle.spec.ts @@ -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 }) diff --git a/web/src/app/layout-header-style.test.ts b/web/src/app/layout-header-style.test.ts index 25337244..680aa658 100644 --- a/web/src/app/layout-header-style.test.ts +++ b/web/src/app/layout-header-style.test.ts @@ -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', () => { diff --git a/web/src/app/layout-header-style.ts b/web/src/app/layout-header-style.ts index 3ae7e8fc..467dd7a5 100644 --- a/web/src/app/layout-header-style.ts +++ b/web/src/app/layout-header-style.ts @@ -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) diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index af700349..a93f0199 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -83,7 +83,7 @@ export function Layout() {
diff --git a/web/src/index.css b/web/src/index.css index afd4a82c..9b9a58a6 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -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%; } } diff --git a/web/src/shared/components/theme-toggle.tsx b/web/src/shared/components/theme-toggle.tsx index 86a832f9..d913166c 100644 --- a/web/src/shared/components/theme-toggle.tsx +++ b/web/src/shared/components/theme-toggle.tsx @@ -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, )} > - - + + + + + + ) }