From aecc111f3cfeb567d364cd48aa489e99f517dfd6 Mon Sep 17 00:00:00 2001 From: phranck Date: Mon, 2 Feb 2026 13:48:42 +0100 Subject: [PATCH] Feat: Nav icons, theme switcher hydration fix, and pulse animation - Add SF Symbol icons to nav links (book for Docs, for GitHub) - Fix theme button onClick broken by hydration mismatch: start with null on both server and client, sync via useEffect after mount - Active theme dot pulses with 3s ease-in-out glow animation - Stronger glow on active theme indicator (triple-layer box-shadow) --- docs/app/components/Icon.tsx | 4 +++ docs/app/components/ThemeProvider.tsx | 26 +++++++---------- docs/app/components/ThemeSwitcher.tsx | 40 ++++++++++++++------------- docs/app/globals.css | 5 ++++ docs/app/page.tsx | 6 ++-- 5 files changed, 44 insertions(+), 37 deletions(-) diff --git a/docs/app/components/Icon.tsx b/docs/app/components/Icon.tsx index e358a92..feedc07 100644 --- a/docs/app/components/Icon.tsx +++ b/docs/app/components/Icon.tsx @@ -12,6 +12,8 @@ import { SFArrowLeftArrowRight, SFSwift, SFCheckmarkCircleFill, + SFBookFill, + SFChevronLeftForwardslashChevronRight, } from "sf-symbols-lib/hierarchical"; /** Maps of icon names to SF Symbol constants for type-safe usage. */ @@ -26,6 +28,8 @@ const icons = { arrows: SFArrowLeftArrowRight, swift: SFSwift, checkmark: SFCheckmarkCircleFill, + book: SFBookFill, + code: SFChevronLeftForwardslashChevronRight, } as const; export type IconName = keyof typeof icons; diff --git a/docs/app/components/ThemeProvider.tsx b/docs/app/components/ThemeProvider.tsx index c61bd33..69c1f06 100644 --- a/docs/app/components/ThemeProvider.tsx +++ b/docs/app/components/ThemeProvider.tsx @@ -46,27 +46,21 @@ function getInitialTheme(): Theme { /** Provides theme state and applies it to the document root via data-theme attribute. */ export function ThemeProvider({ children }: { children: ReactNode }) { /** - * Initialize theme from DOM attribute that was set by blocking script in layout.tsx. - * This avoids setState in useEffect and prevents hydration mismatches. + * Start with null on both server and client to avoid hydration mismatch. + * The blocking script in layout.tsx already sets data-theme on before + * first paint, so there is no FOUC. React state catches up in useEffect below. */ - const [theme, setThemeState] = useState(() => { - if (typeof window === "undefined") return null; - const attr = document.documentElement.getAttribute("data-theme") as Theme; - return attr || null; - }); + const [theme, setThemeState] = useState(null); /** - * After hydration, ensure theme is set if it wasn't captured in initial state. - * This is a safety net for edge cases. + * After hydration, read the actual theme from DOM/localStorage. + * This runs once on mount and sets the React state to match reality. */ useEffect(() => { - if (theme === null) { - const actual = getInitialTheme(); - // eslint-disable-next-line react-hooks/set-state-in-effect - setThemeState(actual); - document.documentElement.setAttribute("data-theme", actual); - } - }, [theme]); + const actual = getInitialTheme(); + setThemeState(actual); + document.documentElement.setAttribute("data-theme", actual); + }, []); const setTheme = useCallback((next: Theme) => { setThemeState(next); diff --git a/docs/app/components/ThemeSwitcher.tsx b/docs/app/components/ThemeSwitcher.tsx index 0374db2..c43112c 100644 --- a/docs/app/components/ThemeSwitcher.tsx +++ b/docs/app/components/ThemeSwitcher.tsx @@ -28,26 +28,28 @@ export default function ThemeSwitcher() { return (
- {themes.map((themeOption) => ( - - ))} + opacity: isActive ? 1 : 0.35, + }} + /> + + ); + })}
); } diff --git a/docs/app/globals.css b/docs/app/globals.css index ae0c81f..2714272 100644 --- a/docs/app/globals.css +++ b/docs/app/globals.css @@ -298,6 +298,11 @@ code { animation: cursor-blink 1s step-end infinite; } +@keyframes theme-pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.4; transform: scale(0.75); } +} + /* CRT scanline - moving element from top to bottom within container only */ @keyframes crt-scanline-move { 0% { diff --git a/docs/app/page.tsx b/docs/app/page.tsx index 641cdf4..f562c81 100644 --- a/docs/app/page.tsx +++ b/docs/app/page.tsx @@ -44,16 +44,18 @@ export default function Home() {
+ Documentation + GitHub