Organize/lib/themes.ts

97 lines
2.8 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";
}
/**
* 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){}`;
}