chore: starter template

This commit is contained in:
e2e
2026-07-10 19:24:06 +02:00
commit 08a95a050b
157 changed files with 6462 additions and 0 deletions

View File

@@ -0,0 +1,178 @@
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 (<html lang dir> so the tree
// mirrors for RTL locales) and wraps the app in the Mantine + react-query +
// Pikku providers. Mantine v8 SSR needs ColorSchemeScript in <head> and
// mantineHtmlProps on <html>; @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<MantineThemeOverride | null>(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<ColorScheme | null>(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 <html lang dir> 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 (
<html lang={locale} dir={localeDir(locale)} {...mantineHtmlProps}>
<head>
<HeadContent />
<ColorSchemeScript defaultColorScheme={activeColorScheme} />
{/* 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. */}
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%234f46e5'/%3E%3Cstop offset='1' stop-color='%230ea5e9'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='32' height='32' rx='7' fill='url(%23g)'/%3E%3Cpath d='M16 4c1.1 6.4 5.6 10.9 12 12-6.4 1.1-10.9 5.6-12 12-1.1-6.4-5.6-10.9-12-12C10.4 14.9 14.9 10.4 16 4Z' fill='white'/%3E%3C/svg%3E"
/>
{fontsHref && (
<>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link rel="stylesheet" href={fontsHref} />
</>
)}
</head>
<body>
<MantineProvider
theme={effectiveTheme}
forceColorScheme={colorScheme === 'auto' ? undefined : colorScheme}
>
<PreferencesContext.Provider value={{ locale, themeId, setLocale, setThemeId }}>
<QueryClientProvider client={queryClient}>
<PikkuProvider pikku={pikku}>
<Outlet />
</PikkuProvider>
</QueryClientProvider>
</PreferencesContext.Provider>
</MantineProvider>
<Scripts />
</body>
</html>
)
}