import { useEffect, useState } from 'react' import { createRootRoute, HeadContent, Outlet, Scripts } from '@tanstack/react-router' import { ColorSchemeScript, MantineProvider, mantineHtmlProps, type MantineThemeOverride, } from '@pikku/mantine/core' import '@mantine/core/styles.css' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { PikkuProvider, createPikku } from '@pikku/react' import { PikkuFetch } from '@project/functions-sdk/pikku/pikku-fetch.gen' import { PikkuRPC } from '@project/functions-sdk/pikku/pikku-rpc.gen' import { activeColorScheme, activeId, activeTheme, buildMantineTheme, googleFontsHref, themeColorSchemes, themes, type ColorScheme, type Theme, } from '@project/mantine-themes' import { defaultLocale, localeDir, supportedLocales, setActiveLocale } from '@/i18n/config' import { apiUrl } from '@/lib/env' import { PreferencesContext } from '@/contexts/preferences' const LOCALE_KEY = 'app-locale' const THEME_KEY = 'app-theme' // The active theme id THEME_KEY was saved against — a preference only holds // while that theme is still the workspace's active one, so a builder-side // switch (which changes active.json) always wins over a stale local pick. const THEME_SAVED_ACTIVE_KEY = 'app-theme-saved-active' const fontsHref = googleFontsHref() // Root route owns the full HTML document for SSR ( so the tree // mirrors for RTL locales) and wraps the app in the Mantine + react-query + // Pikku providers. Mantine v8 SSR needs ColorSchemeScript in and // mantineHtmlProps on ; @mantine/core/styles.css ships the static CSS. export const Route = createRootRoute({ head: () => ({ meta: [ { charSet: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { title: 'Pikku App' }, ], }), component: RootDocument, }) function RootDocument() { // Seed from the build-time active theme (active.json) so the applied theme // drives the initial + SSR render; useEffect syncs from localStorage after // hydration. Seeding 'default' here was the "applied theme never shows" bug. const [themeId, setThemeIdRaw] = useState(activeId) const [locale, setLocaleRaw] = useState(defaultLocale) // Transient override from the fabric console live-preview (not persisted) — // the console injects a full theme spec into the iframe so the builder sees // the look instantly. Takes precedence over the persisted theme. const [previewTheme, setPreviewTheme] = useState(null) // The scheme that goes with the live-preview spec (a light style must render // light), tracked alongside previewTheme so a preview flips the scheme too. const [previewScheme, setPreviewScheme] = useState(null) const effectiveTheme = previewTheme ?? themes[themeId] ?? activeTheme // Force the active/previewed theme's natural scheme — a light-first style boots // light, a dark-first style dark — rather than the old hardcoded dark. const colorScheme: ColorScheme = previewScheme ?? themeColorSchemes[themeId] ?? activeColorScheme // Sync preferences from localStorage after hydration. useEffect(() => { const savedTheme = localStorage.getItem(THEME_KEY) const savedAgainst = localStorage.getItem(THEME_SAVED_ACTIVE_KEY) if (savedTheme && themes[savedTheme] && savedAgainst === activeId) { setThemeIdRaw(savedTheme) } else if (savedTheme) { localStorage.removeItem(THEME_KEY) localStorage.removeItem(THEME_SAVED_ACTIVE_KEY) } const savedLocale = localStorage.getItem(LOCALE_KEY) if ( savedLocale && supportedLocales.includes(savedLocale as (typeof supportedLocales)[number]) ) { setLocaleRaw(savedLocale) setActiveLocale(savedLocale as (typeof supportedLocales)[number]) } }, []) // Keep in sync with locale changes after hydration. useEffect(() => { document.documentElement.lang = locale document.documentElement.dir = localeDir(locale) }, [locale]) // Fabric console live-preview: postMessage injects a theme spec to override // the active theme without persisting. Accepts `theme` (a full spec) or the // legacy `palette`; null resets to the persisted theme. useEffect(() => { const onMessage = (event: MessageEvent) => { const data = event.data if (data?.source !== 'fabric-console' || data.type !== 'set-theme') return try { const spec = (data.theme ?? data.palette) as Theme | null setPreviewTheme(spec ? buildMantineTheme(spec) : null) setPreviewScheme(spec?.structure?.defaultColorScheme ?? null) } catch (err) { console.warn('[mantine-themes] ignoring bad theme payload', err) } } window.addEventListener('message', onMessage) return () => window.removeEventListener('message', onMessage) }, []) const setLocale = (next: string) => { localStorage.setItem(LOCALE_KEY, next) setLocaleRaw(next) setActiveLocale(next as (typeof supportedLocales)[number]) } const setThemeId = (next: string) => { localStorage.setItem(THEME_KEY, next) localStorage.setItem(THEME_SAVED_ACTIVE_KEY, activeId) setThemeIdRaw(next) setPreviewTheme(null) setPreviewScheme(null) } // Per-render instances keep server requests from sharing client/cache state. const [queryClient] = useState(() => new QueryClient()) const [pikku] = useState(() => createPikku(PikkuFetch, PikkuRPC, { serverUrl: apiUrl(), credentials: 'include', }), ) return ( {/* Inline data-URI icon so the browser's automatic /favicon.ico request never 404s (no asset to ship, no network round-trip). Gradient spark matches the Wordmark; the build agent rebrands this per project. */} {fontsHref && ( <> )} ) }