diff --git a/app/layout.tsx b/app/layout.tsx index f2fdee7..7ebcf06 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -7,6 +7,7 @@ import { CommandPalette } from "@/components/navigation/command-palette"; import { TaglineFooter } from "@/components/branding/tagline-footer"; import { Toaster } from "sonner"; import { AuthProvider } from "@/components/auth/auth-provider"; +import { ThemeSessionBridge } from "@/components/mobile/profile/ThemeSessionBridge"; // IBM Plex Sans replaces the brand-mandated Helvetica/Arial. The 2013 // standards guide called for Helvetica Bold for headers and Helvetica @@ -69,6 +70,7 @@ export default function RootLayout({ disableTransitionOnChange > +
{children}
diff --git a/components/mobile/profile/ThemeSessionBridge.tsx b/components/mobile/profile/ThemeSessionBridge.tsx new file mode 100644 index 0000000..3c5ce88 --- /dev/null +++ b/components/mobile/profile/ThemeSessionBridge.tsx @@ -0,0 +1,38 @@ +'use client'; + +/** + * ThemeSessionBridge (THEME-03 / D-16, D-17). + * + * On session load and after sign-in, compares session.user.theme to the + * next-themes useTheme() value and calls setTheme(session.user.theme) if + * different. Server is canonical; this is the bridge that enforces it. + * + * Renders nothing. + */ + +import { useEffect } from 'react'; +import { useTheme } from 'next-themes'; +import { useSession } from '@/lib/auth-client'; + +type SessionUserWithTheme = { theme?: 'light' | 'dark' | 'system' | string }; + +export function ThemeSessionBridge() { + const { data: session } = useSession(); + const { theme, setTheme } = useTheme(); + + useEffect(() => { + if (!session?.user) return; + const serverTheme = (session.user as SessionUserWithTheme).theme; + if ( + serverTheme === 'light' || + serverTheme === 'dark' || + serverTheme === 'system' + ) { + if (serverTheme !== theme) { + setTheme(serverTheme); + } + } + }, [session?.user, theme, setTheme]); + + return null; +} diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx index 4c0afd4..2385cc3 100644 --- a/components/theme-toggle.tsx +++ b/components/theme-toggle.tsx @@ -15,6 +15,21 @@ import { export function ThemeToggle() { const { setTheme, theme } = useTheme() + // Write the new theme choice through to the server so session.user.theme + // stays canonical. Fire-and-forget — ThemeSessionBridge re-syncs from + // session on next session refresh if the request fails. + const writeTheme = (next: 'light' | 'dark' | 'system') => { + setTheme(next) + fetch('/api/me/theme', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ theme: next }), + }).catch(() => { + // Silent fail on network error — the desktop affordance is best-effort. + // The mobile profile Theme section is the explicit-error UX. + }) + } + return ( @@ -25,15 +40,15 @@ export function ThemeToggle() { - setTheme("light")}> + writeTheme("light")}> Light - setTheme("dark")}> + writeTheme("dark")}> Dark - setTheme("system")}> + writeTheme("system")}> System