187ef6ae24
Appearance is a radio group inside the avatar menu rather than a loose button, so arrow keys reach it and the current choice is announced. Picking one keeps the menu open, so the change is visible where it was made. "跟随系统" removes data-theme instead of writing a third value — the media query has nothing to match otherwise. A synchronous script at the top of <head> re-applies a pinned choice before the first paint; going through next/script with any strategy would defer it and bring the flash straight back, so the test asserts a plain script tag. Blocked storage degrades to following the OS instead of throwing. The stored value is browser state, so it is read through useSyncExternalStore rather than synced into React state in an effect, which also trips the cascading-render lint rule. localStorage only fires `storage` in other tabs, so a same-tab write notifies its own listeners and every open tab stays in step. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
59 lines
1.8 KiB
TypeScript
59 lines
1.8 KiB
TypeScript
import type { Metadata, Viewport } from "next";
|
|
import { Inter } from "next/font/google";
|
|
import Script from "next/script";
|
|
import { Toaster } from "@/components/ui/sonner";
|
|
import { StaleClientRecovery } from "@/components/stale-client-recovery";
|
|
import { themePreferenceBootScript } from "@/lib/theme-preference";
|
|
|
|
const inter = Inter({
|
|
subsets: ["latin"],
|
|
display: "swap",
|
|
variable: "--font-inter",
|
|
});
|
|
|
|
export const metadata: Metadata = {
|
|
title: "Jyotisha · 印度占星",
|
|
description: "与 Mastra Agent 对话,基于星盘证据讨论事业、关系与时间窗口。",
|
|
};
|
|
|
|
export const viewport: Viewport = {
|
|
width: "device-width",
|
|
initialScale: 1,
|
|
viewportFit: "cover",
|
|
interactiveWidget: "resizes-content",
|
|
};
|
|
|
|
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
const enableReactDevTools = process.env.NODE_ENV === "development"
|
|
&& process.env.NEXT_PUBLIC_ENABLE_REACT_DEVTOOLS === "1";
|
|
|
|
return (
|
|
<html lang="zh-CN" className={inter.variable} suppressHydrationWarning>
|
|
<head>
|
|
{/* Synchronous on purpose: a pinned theme must be on <html> before the
|
|
first paint, or the page flashes the other theme on every load. */}
|
|
<script dangerouslySetInnerHTML={{ __html: themePreferenceBootScript }} />
|
|
{enableReactDevTools && (
|
|
<>
|
|
<Script
|
|
crossOrigin="anonymous"
|
|
src="https://unpkg.com/react-grab/dist/index.global.js"
|
|
strategy="lazyOnload"
|
|
/>
|
|
<Script
|
|
crossOrigin="anonymous"
|
|
src="https://unpkg.com/react-scan/dist/auto.global.js"
|
|
strategy="lazyOnload"
|
|
/>
|
|
</>
|
|
)}
|
|
</head>
|
|
<body>
|
|
<StaleClientRecovery />
|
|
{children}
|
|
<Toaster />
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|