import type { ThemeName, UserTheme, WikiThemeMode } from "./resolve.ts"; import { type Theme, themes } from "./themes.ts"; const kebab = (s: string): string => s.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`); function themeVars(theme: Theme): string { return Object.entries(theme) .map(([k, v]) => { const value = Array.isArray(v) ? v.join(", ") : (v as string); return `--${kebab(k)}: ${value};`; }) .join(" "); } function themeVarsImportant(theme: Theme): string { return Object.entries(theme) .map(([k, v]) => { const value = Array.isArray(v) ? v.join(", ") : (v as string); return `--${kebab(k)}: ${value} !important;`; }) .join(" "); } // Tailwind's prose palette is re-declared inside `.prose {}`, so we must re-point those vars at our tokens there. const PROSE_OVERRIDES = [ "--tw-prose-body: var(--text-secondary)", "--tw-prose-headings: var(--text)", "--tw-prose-lead: var(--text-secondary)", "--tw-prose-links: var(--accent)", "--tw-prose-bold: var(--text)", "--tw-prose-counters: var(--text-secondary)", "--tw-prose-bullets: var(--text-secondary)", "--tw-prose-hr: var(--border)", "--tw-prose-quotes: var(--text-secondary)", "--tw-prose-quote-borders: var(--accent-soft-border)", "--tw-prose-captions: var(--text-muted)", "--tw-prose-kbd: var(--text)", "--tw-prose-code: var(--accent)", "--tw-prose-pre-code: var(--text)", "--tw-prose-pre-bg: var(--placeholder)", "--tw-prose-th-borders: var(--border)", "--tw-prose-td-borders: var(--border-subtle)", ].join("; "); const PROSE_RULE = `.prose { ${PROSE_OVERRIDES}; }`; const SELECTION_RULE = `::selection { background-color: var(--accent-soft); color: var(--text); }`; const REDUCED_MOTION_RULE = `@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } }`; // !important on `body *` beats inline enforce-theme vars, forcing print to the light palette. const PRINT_RULE = `@media print { body, body * { ${themeVarsImportant(themes.light)}; color-scheme: light !important; } }`; function bodyBlock(theme: Theme, colorScheme: string): string { return `body { ${themeVars(theme)} color-scheme: ${colorScheme}; }`; } // "system" emits light defaults + a prefers-color-scheme:dark override (native resolution, no FOUC/JS). export function themeRootStyle(userTheme: UserTheme): string { let block: string; if (userTheme === "light") { block = bodyBlock(themes.light, "light"); } else if (userTheme === "dark") { block = bodyBlock(themes.dark, "dark"); } else { block = `${bodyBlock(themes.light, "light dark")} @media (prefers-color-scheme: dark) { body { ${themeVars(themes.dark)} } }`; } return `${block} ${PROSE_RULE} ${SELECTION_RULE} ${REDUCED_MOTION_RULE} ${PRINT_RULE}`; } // theme-color tints the browser/OS chrome, so it has to follow the same branch as // themeRootStyle — an explicit choice wins outright, "system" defers to the media // query. A blanket media pair would light the title bar for a visitor who picked dark. export function themeColorMeta(userTheme: UserTheme): string { if (userTheme === "light") { return ``; } if (userTheme === "dark") { return ``; } return ` `; } function themeStyleAttr(theme: Theme): string { return Object.entries(theme) .map(([k, v]) => { const value = Array.isArray(v) ? v.join(", ") : (v as string); return `--${kebab(k)}: ${value}`; }) .join("; "); } // Enforce mode: set theme tokens + the wrapper's own bg/color so it visibly switches palette ("" in reader mode). export function wikiThemeStyleAttr( wikiThemeMode: WikiThemeMode | undefined, wikiTheme: ThemeName | undefined, ): string { if (wikiThemeMode !== "enforce" || !wikiTheme) return ""; const theme = themes[wikiTheme]; if (!theme) return ""; return `${themeStyleAttr(theme)}; background-color: var(--bg); color: var(--text); color-scheme: ${wikiTheme};`; }