135 lines
3.1 KiB
TypeScript
135 lines
3.1 KiB
TypeScript
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();
|
|
}
|