From 313973bf1bd5f3c6c1ea4cdad924583ab371ae20 Mon Sep 17 00:00:00 2001 From: Rafael Fernandes Date: Thu, 24 Sep 2026 08:40:15 -0300 Subject: [PATCH 1/2] feat(theme): add a system, light and dark theme switch - dark token set on :root[data-theme='dark']; components already use tokens - inline head script applies the saved or system theme before first paint and follows live system changes while no choice is saved - header toggle (System / Light / Dark) persisted under rafer.theme, synced across tabs, tracked as theme_change - browser theme-color follows manual choices; shadows get their own token - privacy policy discloses the rafer.theme key - Storybook toolbar to preview components in either theme - tests: theme behaviour, axe in both schemes, light and dark screenshots Co-Authored-By: Claude Opus 5.5 --- .storybook/preview.ts | 23 ++++ src/app/_layout/_components/consentBanner.tsx | 2 +- src/app/_layout/_components/siteHeader.tsx | 10 +- src/app/_layout/_components/themeToggle.tsx | 65 ++++++++++ src/app/_layout/global.css | 25 ++++ src/app/_layout/index.tsx | 10 +- src/app/_layout/metadata.tsx | 6 +- src/components/Mockups/window.tsx | 2 +- src/config/content/en.ts | 1 + src/config/content/pt.ts | 1 + src/config/content/types.ts | 1 + src/config/privacy.ts | 4 +- src/config/theme.ts | 42 ++++++ src/lib/analytics.ts | 1 + src/lib/theme.ts | 76 +++++++++++ tests/accessibility.spec.ts | 25 ++-- tests/screenshot.spec.ts | 22 ++-- ...enshot-Full-page-1-Mobile-Chrome-linux.png | Bin 363353 -> 0 bytes .../Screenshot-Full-page-1-chromium-linux.png | Bin 386584 -> 0 bytes tests/theme.spec.ts | 122 ++++++++++++++++++ 20 files changed, 405 insertions(+), 33 deletions(-) create mode 100644 src/app/_layout/_components/themeToggle.tsx create mode 100644 src/config/theme.ts create mode 100644 src/lib/theme.ts delete mode 100644 tests/screenshot.spec.ts-snapshots/Screenshot-Full-page-1-Mobile-Chrome-linux.png delete mode 100644 tests/screenshot.spec.ts-snapshots/Screenshot-Full-page-1-chromium-linux.png create mode 100644 tests/theme.spec.ts diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 1a690d6..022dbb9 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -5,6 +5,29 @@ import './public/css/font.css' import './public/css/bgTransparent.css' const preview: Preview = { + globalTypes: { + theme: { + description: 'Site color theme', + toolbar: { + title: 'Theme', + icon: 'mirror', + items: [ + { value: 'light', icon: 'sun', title: 'Light' }, + { value: 'dark', icon: 'moon', title: 'Dark' }, + ], + dynamicTitle: true, + }, + }, + }, + initialGlobals: { + theme: 'light', + }, + decorators: [ + (Story, { globals }) => { + document.documentElement.setAttribute('data-theme', globals.theme) + return Story() + }, + ], parameters: { actions: { argTypesRegex: '^on[A-Z].*' }, controls: { diff --git a/src/app/_layout/_components/consentBanner.tsx b/src/app/_layout/_components/consentBanner.tsx index 9117dc8..c2c4e9c 100644 --- a/src/app/_layout/_components/consentBanner.tsx +++ b/src/app/_layout/_components/consentBanner.tsx @@ -57,7 +57,7 @@ const ConsentBanner = ({ return (

{text}{' '} diff --git a/src/app/_layout/_components/siteHeader.tsx b/src/app/_layout/_components/siteHeader.tsx index e0fb834..8eb3ea0 100644 --- a/src/app/_layout/_components/siteHeader.tsx +++ b/src/app/_layout/_components/siteHeader.tsx @@ -5,6 +5,7 @@ import { Container } from '@/components/Container' import { Brand } from './brand' import { LanguageToggle } from './languageToggle' import { Navigation } from './navigation' +import { ThemeToggle } from './themeToggle' const SiteHeader = ({ locale }: { locale: Locale }) => { const { ui } = getContent(locale) @@ -19,11 +20,10 @@ const SiteHeader = ({ locale }: { locale: Locale }) => { - +

+ + +
= { + system: Monitor, + light: Sun, + dark: Moon, +} + +type ThemeToggleProps = { + labels: SiteContent['ui']['theme'] + className?: string +} + +const ThemeToggle = ({ labels, className }: ThemeToggleProps) => { + const current = useSyncExternalStore( + subscribeTheme, + readTheme, + () => 'system' + ) + + return ( +
+ {themes.map((theme) => { + const Icon = icons[theme] + const active = theme === current + + return ( + + ) + })} +
+ ) +} + +export { ThemeToggle } diff --git a/src/app/_layout/global.css b/src/app/_layout/global.css index 6ba381e..f13613f 100644 --- a/src/app/_layout/global.css +++ b/src/app/_layout/global.css @@ -14,6 +14,7 @@ --line: 43 12% 83%; --pen: 224 69% 46%; --signal: 146 64% 38%; + --shadow: 220 13% 10%; --background: var(--paper); --foreground: var(--ink); @@ -60,6 +61,30 @@ font-family: var(--font-sans); } + :root { + color-scheme: light; + } + + :root[data-theme='dark'] { + color-scheme: dark; + + --paper: 40 6% 8%; + --paper-card: 40 6% 10.5%; + --ink: 42 24% 91%; + --ink-soft: 40 9% 76%; + --ink-subtle: 40 5% 61%; + --line: 40 5% 20%; + --pen: 222 88% 73%; + --signal: 146 50% 55%; + --shadow: 0 0% 0%; + + --muted: 40 5% 14%; + --muted-foreground: 40 9% 76%; + --primary: 40 5% 13%; + --primary-foreground: 42 24% 91%; + --secondary: 40 4% 36%; + } + html { @apply custom-scroll; scroll-behavior: smooth; diff --git a/src/app/_layout/index.tsx b/src/app/_layout/index.tsx index ce20989..2a8baa0 100644 --- a/src/app/_layout/index.tsx +++ b/src/app/_layout/index.tsx @@ -4,6 +4,7 @@ import { cn } from '@/utils/cn' import { getContent } from '@/config/content' import { htmlLang, languageRedirect, Locale, paths } from '@/config/i18n' +import { themeInit } from '@/config/theme' import { fontHand, fontMono, fontSans } from '@/lib/fonts' import { DevTools } from '@/components/DevTools' @@ -22,11 +23,12 @@ const RootLayout = ({ locale, children }: RootLayoutProps) => { return ( - {locale === 'en' && ( - + + {locale === 'en' && (