/** * Isomorphic theme metadata. * * Deliberately framework-free: it is imported from server code (project * theme validation in lib/actions/projects.ts, the no-FOUC script rendered * by the project page) *and* client code (theme provider, pickers), so it * must stay free of React/DOM imports. What *applies* themes to the DOM * lives in components/theme/theme-provider.tsx; the display options * (labels, icons, swatches) live in components/theme/theme-toggle.tsx. */ /* Order is the picker order: all the light looks, then the dark looks. */ export const THEMES = [ "default", "sunset", "meadow", "honey", "rose", "lavender", "slate", "blueprint", "vaporwave", "notebook", "dark", "ocean", "pine", "plum", "midnight", "ember", "rosewood", "cyberpunk", "starfield", ] as const; export type ThemeName = (typeof THEMES)[number]; export type RawTheme = ThemeName | "system"; /** Theme name -> class applied to (see app/globals.css). */ export const THEME_CLASSES: Record = { default: "theme-default", sunset: "theme-sunset", meadow: "theme-meadow", honey: "theme-honey", rose: "theme-rose", lavender: "theme-lavender", slate: "theme-slate", blueprint: "theme-blueprint", vaporwave: "theme-vaporwave", notebook: "theme-notebook", dark: "dark", ocean: "ocean", pine: "theme-pine", plum: "theme-plum", midnight: "theme-midnight", ember: "theme-ember", rosewood: "theme-rosewood", cyberpunk: "theme-cyberpunk", starfield: "theme-starfield", }; /** Themes whose *surfaces* are dark (see app/globals.css). The single source * of truth for every dark-surface check: isDarkTheme, the sonner toaster, * color-scheme, and the `dark:` custom-variant in globals.css. */ export const DARK_SURFACES: ThemeName[] = [ "dark", "ocean", "pine", "plum", "midnight", "ember", "rosewood", "cyberpunk", "starfield", ]; export function themeColorScheme(name: ThemeName): "light" | "dark" { return DARK_SURFACES.includes(name) ? "dark" : "light"; } /** * Source of the root layout's no-FOUC theme script (app/layout.tsx renders * it as plain server HTML so it runs before first paint). * * `serverTheme` is the signed-in user's stored preference (the layout * reads it from the profile row). When present it is applied verbatim and * localStorage is *not* consulted -- that is what lets each account in a * linked set keep its own look in the same browser after an account * toggle. When absent (anonymous visitor) the script keeps the old * behavior: restore the browser's localStorage preference, falling back * to the OS light/dark setting. */ export function themeInitScript(serverTheme?: RawTheme | null): string { const allClasses = JSON.stringify([...Object.values(THEME_CLASSES), "light"]); const classMap = JSON.stringify(THEME_CLASSES); const dark = JSON.stringify(DARK_SURFACES); const system = "(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'default'"; const apply = `r.classList.add(m[v]);r.style.colorScheme=d.indexOf(v)>=0?'dark':'light';`; if (serverTheme != null) { return ( `try{var r=document.documentElement,c=${allClasses};` + `for(var i=0;i over to `theme` immediately * (before first paint), the same way the global no-FOUC script in * app/layout.tsx works, and leaves a `data-project-theme` marker behind so * the ThemeProvider can pick the theme up as its initial scope at * hydration. Render it from a server component with * `dangerouslySetInnerHTML`. */ export function themeSwitchScript(theme: ThemeName): string { const classes = JSON.stringify([ ...Object.values(THEME_CLASSES), "light", ]); return `try{var r=document.documentElement;var c=${classes};for(var i=0;i