feat: generate preset-aware dark theme variables
This commit is contained in:
1 parent
48c596cf41
commit
3fd2a27719
4 files changed
+77
-14
No files matched your search
+1
-12
@@ -86,22 +86,11 @@
|
||||
--border-subtle: color-mix(in srgb, var(--color-text-muted) 18%, transparent);
|
||||
}
|
||||
|
||||
/* Dark mode (toggled by html.dark; wins over :root + ThemeVars via specificity). */
|
||||
/* Dark mode colors are generated by ThemeVars from the selected preset. */
|
||||
html.dark {
|
||||
--color-background: #0b0d14;
|
||||
--color-surface: #141820;
|
||||
--color-dropdown: #1d222b;
|
||||
--color-navbar: #141820;
|
||||
--color-navbar-text: #e5e7eb;
|
||||
--color-text: #e5e7eb;
|
||||
--color-text-muted: #94a3b8;
|
||||
--color-text-readable: var(--color-text);
|
||||
--color-text-muted-readable: var(--color-text-muted);
|
||||
--color-primary-readable: var(--color-text);
|
||||
--color-accent-readable: var(--color-text);
|
||||
--color-navbar-text-readable: var(--color-navbar-text);
|
||||
--link-color-readable: var(--color-text);
|
||||
--link-hover-color-readable: var(--color-text);
|
||||
}
|
||||
html.dark input,
|
||||
html.dark select,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { themePaletteCss } from "@/lib/theme-css";
|
||||
import { PRESETS } from "@/lib/theme-presets";
|
||||
|
||||
describe("themePaletteCss", () => {
|
||||
it("emits separate preset-specific light and dark scopes", () => {
|
||||
const ocean = PRESETS.Ocean;
|
||||
const css = `${themePaletteCss(":root", ocean.light)}${themePaletteCss("html.dark", ocean.dark)}`;
|
||||
|
||||
expect(css).toContain(":root{");
|
||||
expect(css).toContain("html.dark{");
|
||||
expect(css).toContain(`--color-primary:${ocean.light.color_primary}`);
|
||||
expect(css).toContain(`--color-background:${ocean.light.color_background}`);
|
||||
expect(css).toContain(`--color-background:${ocean.dark.color_background}`);
|
||||
expect(ocean.dark.color_primary).toBe(ocean.light.color_primary);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import { FONTS } from "@/lib/theme-presets";
|
||||
import { themePaletteCss } from "@/lib/theme-css";
|
||||
import { FONTS, PRESETS, THEME_COLOR_KEYS, type ThemePalette } from "@/lib/theme-presets";
|
||||
import { derivePublicForegrounds, readableColor } from "@/lib/theme-contrast";
|
||||
|
||||
// Injects the DB-driven CSS custom properties into :root, exactly like
|
||||
@@ -124,6 +125,18 @@ export async function ThemeVars() {
|
||||
|
||||
const font = FONTS[fontKey ?? "nunito"] ?? FONTS.nunito;
|
||||
|
||||
const presetName = (await g("theme_preset", "Atom (golden)")) ?? "Atom (golden)";
|
||||
const preset = PRESETS[presetName] ?? PRESETS["Atom (golden)"];
|
||||
const darkPalette = Object.fromEntries(
|
||||
await Promise.all(
|
||||
THEME_COLOR_KEYS.map(async (key) => [
|
||||
key,
|
||||
safe(await g(`${key}_dark`, preset.dark[key]), preset.dark[key]),
|
||||
] as const),
|
||||
),
|
||||
) as ThemePalette;
|
||||
const darkCss = themePaletteCss("html.dark", darkPalette);
|
||||
|
||||
const css = `:root{
|
||||
--color-primary:${safe(primary, "#f59e0b")};
|
||||
--color-background:${safeBackground};
|
||||
@@ -174,7 +187,7 @@ export async function ThemeVars() {
|
||||
--size-heading-h1:${px(h1, "30")}px;
|
||||
--size-heading-h2:${px(h2, "22")}px;
|
||||
--size-heading-h3:${px(h3, "18")}px;
|
||||
}`;
|
||||
}${darkCss}`;
|
||||
|
||||
const googleHref = font.google
|
||||
? `https://fonts.googleapis.com/css2?family=${font.google}&display=swap`
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { derivePublicForegrounds, readableColor } from "@/lib/theme-contrast";
|
||||
import type { ThemePalette } from "@/lib/theme-presets";
|
||||
|
||||
const CSS_VARIABLES: Record<keyof ThemePalette, string> = {
|
||||
color_primary: "color-primary", color_background: "color-background",
|
||||
color_surface: "color-surface", color_dropdown: "color-dropdown",
|
||||
color_navbar: "color-navbar", color_navbar_text: "color-navbar-text",
|
||||
color_text: "color-text", color_text_muted: "color-text-muted",
|
||||
color_accent: "color-accent", color_success: "color-success",
|
||||
color_warning: "color-warning", color_error: "color-error", color_info: "color-info",
|
||||
button_primary_color: "button-color", button_text_color: "button-text-color",
|
||||
button_secondary_color: "button-secondary-color",
|
||||
button_secondary_text_color: "button-secondary-text-color",
|
||||
button_danger_color: "button-danger-color", button_danger_text_color: "button-danger-text-color",
|
||||
button_outline_color: "button-outline-color", button_outline_text_color: "button-outline-text-color",
|
||||
link_color: "link-color", link_hover_color: "link-hover-color",
|
||||
border_color: "border-color", gradient_from: "gradient-from", gradient_to: "gradient-to",
|
||||
};
|
||||
|
||||
export function themePaletteCss(selector: string, palette: ThemePalette): string {
|
||||
const semantic = derivePublicForegrounds(palette);
|
||||
const declarations = Object.entries(CSS_VARIABLES).map(
|
||||
([key, cssName]) => `--${cssName}:${palette[key as keyof ThemePalette]};`,
|
||||
);
|
||||
declarations.push(
|
||||
`--color-text-readable:${readableColor(palette.color_text, [palette.color_background, palette.color_surface])};`,
|
||||
`--color-text-muted-readable:${readableColor(palette.color_text_muted, [palette.color_background, palette.color_surface])};`,
|
||||
`--color-primary-readable:${readableColor(palette.color_primary, [palette.color_background, palette.color_surface])};`,
|
||||
`--color-accent-readable:${readableColor(palette.color_accent, [palette.color_background, palette.color_surface])};`,
|
||||
`--color-navbar-text-readable:${semantic.byName.navbar};`,
|
||||
`--button-text-color-readable:${semantic.byName["button-primary"]};`,
|
||||
`--button-secondary-text-color-readable:${semantic.byName["button-secondary"]};`,
|
||||
`--button-danger-text-color-readable:${semantic.byName["button-danger"]};`,
|
||||
`--button-outline-text-color-readable:${semantic.byName["button-outline"]};`,
|
||||
`--link-color-readable:${semantic.byName.link};`,
|
||||
`--link-hover-color-readable:${semantic.byName["link-hover"]};`,
|
||||
`--color-surface-text-readable:${semantic.byName.surface};`,
|
||||
`--color-dropdown-text-readable:${semantic.byName.dropdown};`,
|
||||
`--color-pill-text-readable:${semantic.byName.pill};`,
|
||||
`--color-badge-text-readable:${semantic.byName.badge};`,
|
||||
`--color-overlay-text-readable:${semantic.byName.overlay};`,
|
||||
);
|
||||
return `${selector}{${declarations.join("")}}`;
|
||||
}
|
||||
Reference in new issue
Block a user