134 lines
4.5 KiB
TypeScript
134 lines
4.5 KiB
TypeScript
/**
|
|
* 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 <html> (see app/globals.css). */
|
|
export const THEME_CLASSES: Record<ThemeName, string> = {
|
|
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<c.length;i++){r.classList.remove(c[i]);}` +
|
|
`var v=${JSON.stringify(serverTheme)};if(v==='system'){v=${system};}` +
|
|
`var m=${classMap},d=${dark};if(m[v]){${apply}}catch(e){}`
|
|
);
|
|
}
|
|
return (
|
|
`try{var r=document.documentElement,c=${allClasses};` +
|
|
`for(var i=0;i<c.length;i++){r.classList.remove(c[i]);}` +
|
|
`var v=null;try{v=localStorage.getItem('theme');}catch(e){}` +
|
|
`if(v==='system'||!v){v=${system};}` +
|
|
`var m=${classMap},d=${dark};if(m[v]){${apply}}catch(e){}`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Inline-script source that switches <html> 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<c.length;i++){r.classList.remove(c[i]);}r.classList.add(${JSON.stringify(
|
|
THEME_CLASSES[theme]
|
|
)});r.style.colorScheme=${JSON.stringify(
|
|
themeColorScheme(theme)
|
|
)};r.dataset.projectTheme=${JSON.stringify(theme)};}catch(e){}`;
|
|
}
|