diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d9dbf41..2965e30 100755 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -48,6 +48,15 @@ export const metadata: Metadata = { }, }; +// PRD §5.6: suppressHydrationWarning on html + body because we use: +// 1. next-themes (class on changes client-side) +// 2. Zustand persist (view/locale loaded from localStorage client-side) +// 3. Radix UI generates IDs with useId() which differ between server and client +// (this is expected and the warning is cosmetic — Radix components still work correctly) +// The alternative would be to delay rendering until after hydration (mount gate), +// but that causes a flash of unstyled content. suppressHydrationWarning is the +// official next-themes + Radix pattern. + export default function RootLayout({ children, }: Readonly<{ @@ -57,6 +66,7 @@ export default function RootLayout({ {children} diff --git a/src/components/aos/app-shell.tsx b/src/components/aos/app-shell.tsx index ec5d2f3..1568f39 100755 --- a/src/components/aos/app-shell.tsx +++ b/src/components/aos/app-shell.tsx @@ -1,5 +1,6 @@ 'use client' +import { useState, useEffect } from 'react' import { useAos } from '@/store/aos' import { SidebarNav } from './sidebar-nav' import { TopBar } from './top-bar' @@ -36,10 +37,32 @@ import { MobileBottomNav } from './mobile-bottom-nav' export function AppShell() { const { view, sidebarOpen, setSidebarOpen, portalMode } = useAos() + const [mounted, setMounted] = useState(false) + + // Hydration gate: wait for client mount before rendering interactive UI + // This prevents Radix UI useId() mismatches between server and client + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setMounted(true) + }, []) // Portal mode renders a completely separate experience if (portalMode) return + // Before mount: render a minimal skeleton to prevent hydration mismatch + if (!mounted) { + return ( +
+
+ ) + } + return (
{/* Desktop sidebar */}