From ebac94a043f3d7a0ff1e1119327131eb9f3a5340 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:20:37 +0800 Subject: [PATCH 01/12] feat(theme): add browser-local light and dark mode Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- web/e2e/theme-toggle.spec.ts | 48 ++++++++++++++++ web/src/app/layout.tsx | 6 +- web/src/bootstrap.ts | 4 ++ .../notification/notification-dropdown.tsx | 2 +- .../notification-sse-coordinator.ts | 5 +- web/src/features/skill/skill-card.tsx | 2 +- web/src/features/skill/skill-label-panel.tsx | 4 +- web/src/i18n/locales/en.json | 4 ++ web/src/i18n/locales/ru.json | 4 ++ web/src/i18n/locales/zh.json | 4 ++ web/src/index.css | 32 +++++++++-- web/src/pages/landing.tsx | 4 +- web/src/pages/register.tsx | 2 +- web/src/pages/reset-password.tsx | 2 +- web/src/pages/skill-detail.tsx | 8 +-- .../shared/components/landing-quick-start.tsx | 16 +++--- .../shared/components/language-switcher.tsx | 4 +- .../shared/components/theme-toggle.test.tsx | 34 +++++++++++ web/src/shared/components/theme-toggle.tsx | 44 +++++++++++++++ web/src/shared/components/user-menu.tsx | 2 +- web/src/shared/hooks/use-theme.ts | 24 ++++++++ web/src/shared/lib/theme.test.ts | 53 ++++++++++++++++++ web/src/shared/lib/theme.ts | 56 +++++++++++++++++++ web/src/shared/ui/input.tsx | 2 +- 24 files changed, 332 insertions(+), 34 deletions(-) create mode 100644 web/e2e/theme-toggle.spec.ts create mode 100644 web/src/shared/components/theme-toggle.test.tsx create mode 100644 web/src/shared/components/theme-toggle.tsx create mode 100644 web/src/shared/hooks/use-theme.ts create mode 100644 web/src/shared/lib/theme.test.ts create mode 100644 web/src/shared/lib/theme.ts diff --git a/web/e2e/theme-toggle.spec.ts b/web/e2e/theme-toggle.spec.ts new file mode 100644 index 00000000..4f4cfff7 --- /dev/null +++ b/web/e2e/theme-toggle.spec.ts @@ -0,0 +1,48 @@ +import { expect, test } from '@playwright/test' +import { setEnglishLocale } from './helpers/auth-fixtures' + +test.describe('Light and dark theme', () => { + test.beforeEach(async ({ page }) => { + await setEnglishLocale(page) + await page.context().setExtraHTTPHeaders({ 'X-Mock-User-Id': 'local-user' }) + await page.addInitScript(() => { + if (!window.sessionStorage.getItem('theme-test-initialized')) { + window.localStorage.removeItem('skillhub-theme') + window.sessionStorage.setItem('theme-test-initialized', 'true') + } + }) + }) + + test('switches themes and restores only the browser-local selection', async ({ page }, testInfo) => { + const consoleErrors: string[] = [] + const pageErrors: string[] = [] + page.on('console', (message) => { + if (message.type() === 'error') consoleErrors.push(message.text()) + }) + page.on('pageerror', (error) => pageErrors.push(error.stack ?? error.message)) + + await page.goto('/') + await expect(page.locator('html')).not.toHaveClass(/dark/) + + await page.getByRole('button', { name: 'Switch to dark theme' }).click() + await expect(page.locator('html')).toHaveClass(/dark/) + await expect.poll(() => page.evaluate(() => window.localStorage.getItem('skillhub-theme'))).toBe('dark') + + await page.reload() + await expect(page.locator('html')).toHaveClass(/dark/) + await expect(page.getByRole('button', { name: 'Switch to light theme' })).toBeVisible() + await expect(page.getByRole('heading', { name: 'SkillHub', exact: true })).toBeVisible() + await page.screenshot({ path: testInfo.outputPath('dark-desktop.png'), fullPage: true }) + + await page.setViewportSize({ width: 390, height: 844 }) + await expect(page.getByRole('button', { name: 'Switch to light theme' })).toBeVisible() + await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true) + await page.screenshot({ path: testInfo.outputPath('dark-mobile.png'), fullPage: true }) + + const unexpectedConsoleErrors = consoleErrors.filter((message) => ( + !message.includes("frame-ancestors' is ignored when delivered via a element") + )) + expect(unexpectedConsoleErrors).toEqual([]) + expect(pageErrors).toEqual([]) + }) +}) diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 7a52720b..af700349 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Outlet, Link, useRouterState } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { useAuth } from '@/features/auth/use-auth' import { LanguageSwitcher } from '@/shared/components/language-switcher' +import { ThemeToggle } from '@/shared/components/theme-toggle' import { UserMenu } from '@/shared/components/user-menu' import { NotificationBell } from '@/features/notification/notification-bell' import { dismissOpenOverlays } from '@/shared/lib/dismiss-open-overlays' @@ -115,7 +116,8 @@ export function Layout() { })} -