--mf-hero-bg/--mf-spotlight-bg were multi-stop, directional gradients in dark mode (fading from a vivid corner to transparent), unlike light mode which uses the same color at both gradient stops so it reads as flat. Match that: both now use a single tc() tone repeated at both stops, so the hero and spotlight cards render as one solid tint in dark mode too.
89 lines
3.7 KiB
TypeScript
89 lines
3.7 KiB
TypeScript
import { ACCENT_CHROMA, ACCENT_LIGHTNESS } from './color';
|
|
|
|
const L = ACCENT_LIGHTNESS / 100;
|
|
const C = ACCENT_CHROMA;
|
|
|
|
function r(value: number, digits = 4): string {
|
|
return value.toFixed(digits);
|
|
}
|
|
|
|
function tc(alpha: number, l: number, hue: number): string {
|
|
const lightness = 0.37 - l / 380;
|
|
const chroma = Math.max(0, 0.15 - l / 1600);
|
|
return `oklch(${r(lightness)} ${r(chroma)} ${r(hue, 1)} / ${alpha})`;
|
|
}
|
|
|
|
function tcLight(alpha: number, l: number, hue: number): string {
|
|
const depth = (88 - l) / 58;
|
|
const lightness = 0.9 - 0.14 * depth;
|
|
const chroma = 0.05 + 0.13 * depth;
|
|
return `oklch(${r(lightness)} ${r(chroma)} ${r(hue, 1)} / ${alpha})`;
|
|
}
|
|
|
|
export interface ThemeTokens {
|
|
[cssVar: string]: string;
|
|
}
|
|
|
|
export function buildThemeTokens(hue: number, mode: 'dark' | 'light' = 'dark'): ThemeTokens {
|
|
const h = ((hue % 360) + 360) % 360;
|
|
const h2 = (h + 40) % 360;
|
|
const H = r(h, 1);
|
|
const H2 = r(h2, 1);
|
|
const light = mode === 'light';
|
|
|
|
const accentL = light ? 0.52 : L;
|
|
const accentC = light ? 0.16 : C;
|
|
const titleL = light ? 0.4 : 0.87;
|
|
const titleC = light ? 0.13 : 0.07;
|
|
const mutedL = light ? 0.48 : 0.79;
|
|
const mutedC = light ? 0.06 : 0.035;
|
|
|
|
return {
|
|
'--mf-accent': `oklch(${r(accentL)} ${r(accentC)} ${H})`,
|
|
'--mf-accent-title': `oklch(${r(titleL)} ${r(titleC)} ${H})`,
|
|
'--mf-accent-muted': `oklch(${r(mutedL)} ${r(mutedC)} ${H})`,
|
|
'--mf-accent-soft-bg': `oklch(${r(accentL)} ${r(accentC)} ${H} / 0.08)`,
|
|
'--mf-accent-line': `oklch(${r(accentL)} ${r(accentC)} ${H} / 0.2)`,
|
|
'--mf-accent-hair': `oklch(${r(accentL)} ${r(accentC)} ${H} / 0.09)`,
|
|
'--mf-accent-btn-bg': `oklch(${r(accentL)} ${r(accentC)} ${H} / 0.15)`,
|
|
'--mf-accent-btn-bg-hover': `oklch(${r(accentL)} ${r(accentC)} ${H} / 0.24)`,
|
|
'--mf-bg': light ? `oklch(0.97 0.014 ${H})` : `oklch(0.075 0.02 ${H})`,
|
|
'--mf-elevated': light ? `oklch(0.99 0.008 ${H})` : `oklch(0.12 0.025 ${H})`,
|
|
'--mf-panel-bg': light ? `oklch(0.97 0.014 ${H} / 0.7)` : `oklch(0.1 0.022 ${H} / 0.28)`,
|
|
'--mf-bar-bg': light ? `oklch(0.97 0.014 ${H} / 0.9)` : `oklch(0.115 0.025 ${H} / 0.9)`,
|
|
'--mf-hairline': light ? `oklch(0.3 0.035 ${H} / 0.1)` : `oklch(0.62 0.05 ${H} / 0.08)`,
|
|
'--mf-ambient': light
|
|
? 'none'
|
|
: `radial-gradient(95% 85% at 20% 0%, ${tc(0.12, 30, h)}, transparent 88%), radial-gradient(90% 80% at 80% 0%, ${tc(0.09, 40, h)}, transparent 88%)`,
|
|
'--mf-hero-tint': light ? tcLight(1, 55, h) : tc(1, 42, h),
|
|
'--mf-hero-bg': light
|
|
? `linear-gradient(${tcLight(0.08, 55, h)}, ${tcLight(0.08, 55, h)})`
|
|
: `linear-gradient(${tc(0.2, 42, h)}, ${tc(0.2, 42, h)})`,
|
|
'--mf-modal-glow': light
|
|
? `linear-gradient(135deg, ${tcLight(0.34, 42, h)} 0%, ${tcLight(0.2, 55, h)} 30%, ${tcLight(0.1, 70, h)} 55%, ${tcLight(0.04, 85, h)} 80%, ${tcLight(0.02, 88, h)} 100%)`
|
|
: `linear-gradient(135deg, ${tc(0.34, 42, h)} 0%, ${tc(0.11, 70, h)} 28%, ${tc(0.03, 88, h)} 50%, rgba(6,6,9,0) 70%)`,
|
|
'--mf-spotlight-bg': light
|
|
? `linear-gradient(${tcLight(0.08, 55, h)}, ${tcLight(0.08, 55, h)})`
|
|
: `linear-gradient(${tc(0.2, 42, h)}, ${tc(0.2, 42, h)})`,
|
|
'--mf-logo-grad': light
|
|
? `linear-gradient(140deg, oklch(0.74 0.18 ${H}), oklch(0.58 0.16 ${H2}) 92%)`
|
|
: `linear-gradient(140deg, oklch(0.78 0.15 ${H}), oklch(0.62 0.13 ${H2}) 92%)`,
|
|
'--mf-cover-grad': light
|
|
? `linear-gradient(150deg, oklch(0.7 0.16 ${H}), oklch(0.54 0.11 ${H}))`
|
|
: `linear-gradient(150deg, oklch(0.5 0.13 ${H}), oklch(0.16 0.035 ${H}))`
|
|
};
|
|
}
|
|
|
|
export function applyThemeTokens(
|
|
tokens: ThemeTokens,
|
|
target: HTMLElement = document.documentElement
|
|
) {
|
|
for (const [key, value] of Object.entries(tokens)) target.style.setProperty(key, value);
|
|
}
|
|
|
|
export function tokensToCss(tokens: ThemeTokens): string {
|
|
return Object.entries(tokens)
|
|
.map(([key, value]) => `${key}:${value}`)
|
|
.join(';');
|
|
}
|