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