Organize/lib/themes.ts

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){}`;
}