import { DEFAULT_SITE_PRIMARY_COLOR, normalizeSitePrimaryColor, } from "./site-settings"; type RgbColor = { r: number; g: number; b: number; }; type HslColor = { h: number; s: number; l: number; }; function clamp(value: number, min: number, max: number) { return Math.min(Math.max(value, min), max); } function hexToRgb(hex: string): RgbColor { const normalized = normalizeSitePrimaryColor(hex).slice(1); return { r: Number.parseInt(normalized.slice(0, 2), 16), g: Number.parseInt(normalized.slice(2, 4), 16), b: Number.parseInt(normalized.slice(4, 6), 16), }; } function rgbToHsl({ r, g, b }: RgbColor): HslColor { const red = r / 255; const green = g / 255; const blue = b / 255; const max = Math.max(red, green, blue); const min = Math.min(red, green, blue); const delta = max - min; const lightness = (max + min) / 2; if (delta === 0) { return { h: 0, s: 0, l: Math.round(lightness * 100), }; } const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min); let hue = 0; switch (max) { case red: hue = (green - blue) / delta + (green < blue ? 6 : 0); break; case green: hue = (blue - red) / delta + 2; break; default: hue = (red - green) / delta + 4; break; } return { h: Math.round(hue * 60), s: Math.round(saturation * 100), l: Math.round(lightness * 100), }; } function toChannels(color: HslColor): string { return `${Math.round(color.h)} ${Math.round(color.s)}% ${Math.round(color.l)}%`; } function deriveSecondaryColor(color: HslColor, dark = false): HslColor { return { h: (color.h + 10) % 360, s: clamp(color.s - (dark ? 2 : 8), 35, 95), l: clamp(color.l + (dark ? 10 : 8), 20, 80), }; } function deriveDarkPrimary(color: HslColor): HslColor { return { h: color.h, s: clamp(color.s, 40, 95), l: clamp(color.l + 6, 30, 78), }; } export function buildSiteThemeTokens(primaryColor: string) { const safePrimary = normalizeSitePrimaryColor(primaryColor || DEFAULT_SITE_PRIMARY_COLOR); const basePrimary = rgbToHsl(hexToRgb(safePrimary)); const darkPrimary = deriveDarkPrimary(basePrimary); return { light: { primary: toChannels(basePrimary), secondary: toChannels(deriveSecondaryColor(basePrimary)), }, dark: { primary: toChannels(darkPrimary), secondary: toChannels(deriveSecondaryColor(darkPrimary, true)), }, }; } export function buildSiteThemeStyleText(primaryColor: string) { const tokens = buildSiteThemeTokens(primaryColor); return ` :root { --primary: ${tokens.light.primary}; --ring: ${tokens.light.primary}; --brand-primary: ${tokens.light.primary}; --brand-secondary: ${tokens.light.secondary}; --sidebar-primary: ${tokens.light.primary}; --sidebar-ring: ${tokens.light.primary}; } .dark { --primary: ${tokens.dark.primary}; --ring: ${tokens.dark.primary}; --brand-primary: ${tokens.dark.primary}; --brand-secondary: ${tokens.dark.secondary}; --sidebar-primary: ${tokens.dark.primary}; --sidebar-ring: ${tokens.dark.primary}; } `.trim(); }