import type { Metadata, Viewport } from "next"; import "./globals.css"; import { Inter, Source_Serif_4, Newsreader, Fraunces, JetBrains_Mono } from "next/font/google"; import { cn } from "@/lib/utils"; import "@/modules"; // registers all module manifests import { auth } from "@/lib/auth"; import { db } from "@/lib/db"; import { asc, eq } from "drizzle-orm"; import { dashboards, users } from "@/modules/_core/schema"; import type { DashboardMeta } from "@/app/d/actions"; import { getQuickAdds } from "@/modules/_core"; import { QuickAddProvider } from "@/components/quick-add-provider"; import { QuickAddSheet } from "@/components/quick-add-sheet"; import { CommandPalette } from "@/components/command-palette"; import { PwaRegister } from "@/components/pwa-register"; import { InstallPrompt } from "@/components/install-prompt"; import { AppShell } from "@/components/app-shell"; import { DEFAULT_THEME, navStyleToDataNav } from "@/modules/_core/themes"; import type { Palette, ThemeMode, FontPair, Density, NavStyle } from "@/modules/_core/themes"; const inter = Inter({ subsets: ["latin"], variable: "--sans-inter", display: "swap" }); const sourceSerif = Source_Serif_4({ subsets: ["latin"], variable: "--serif-source", display: "swap", }); const newsreader = Newsreader({ subsets: ["latin"], variable: "--serif-newsreader", display: "swap", }); const fraunces = Fraunces({ subsets: ["latin"], variable: "--serif-fraunces", display: "swap", }); const jetbrains = JetBrains_Mono({ subsets: ["latin"], variable: "--mono-jb", display: "swap", }); export const viewport: Viewport = { themeColor: "#1F1B16", }; export const metadata: Metadata = { title: "famapp", description: "Family coordination app", manifest: "/manifest.webmanifest", appleWebApp: { capable: true, statusBarStyle: "default", title: "famapp", }, icons: { apple: "/icon-180.png", }, }; // Pre-paint: read user's theme prefs from localStorage and apply data-* + .dark. // Falls back to clay/serif-sans/regular/sidebar/system if nothing is stored. const prePaintScript = `(function(){ try { var palette = localStorage.getItem('themePalette') || localStorage.getItem('theme') || 'clay'; var mode = localStorage.getItem('themeMode') || 'system'; var fontPair = localStorage.getItem('themeFontPair') || 'serif-sans'; var density = localStorage.getItem('themeDensity') || 'regular'; var navStyle = localStorage.getItem('themeNavStyle') || 'rail-desktop'; var dataNav = navStyle === 'compact-rail' ? 'rail' : navStyle === 'top-nav' ? 'top' : 'sidebar'; if (window.matchMedia && window.matchMedia('(max-width: 759px)').matches) { dataNav = navStyle === 'fab-only' ? 'fab' : 'bottom'; } var dark = mode === 'dark' || (mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); var html = document.documentElement; html.setAttribute('data-theme', palette); html.setAttribute('data-font-pair', fontPair); html.setAttribute('data-density', density); html.setAttribute('data-nav', dataNav); if (dark) html.classList.add('dark'); else html.classList.remove('dark'); } catch(e) {} })();`; export default async function RootLayout({ children }: { children: React.ReactNode }) { let palette: Palette = DEFAULT_THEME.palette; let mode: ThemeMode = DEFAULT_THEME.mode; let fontPair: FontPair = DEFAULT_THEME.fontPair; let density: Density = DEFAULT_THEME.density; let navStyle: NavStyle = DEFAULT_THEME.navStyle; let userDashboards: DashboardMeta[] = []; let signedIn = false; const session = await auth(); if (session?.user?.id) { signedIn = true; const [row] = await db .select({ themePalette: users.themePalette, themeMode: users.themeMode, themeFontPair: users.themeFontPair, themeDensity: users.themeDensity, themeNavStyle: users.themeNavStyle, }) .from(users) .where(eq(users.id, session.user.id)) .limit(1); if (row) { palette = row.themePalette as Palette; mode = row.themeMode as ThemeMode; fontPair = row.themeFontPair as FontPair; density = row.themeDensity as Density; navStyle = row.themeNavStyle as NavStyle; } userDashboards = await db .select({ id: dashboards.id, name: dashboards.name, slug: dashboards.slug, isDefault: dashboards.isDefault, position: dashboards.position, }) .from(dashboards) .where(eq(dashboards.userId, session.user.id)) .orderBy(asc(dashboards.position), asc(dashboards.createdAt)); } // Server-side initial dark guess: only for `dark` mode (system mode is corrected // before paint by the inline script). Avoids a flash on signed-in users. const isDark = mode === "dark"; const initialDataNav = navStyleToDataNav(navStyle); const quickAdds = getQuickAdds(); const fontVars = cn( inter.variable, sourceSerif.variable, newsreader.variable, fraunces.variable, jetbrains.variable, ); return (