Yo folks, I’m wiring up theming in a small dashboard and I wanted one place to own color state so components don’t drift. The failure mode I keep hitting is mutable shared refs: one widget tweaks a palette and suddenly the whole app’s contrast is off.
// single-owner theme state: base HSL -> derived palette + gradient + contrast
export function createThemeStore(initial = { h: 210, s: 70, l: 45 }) {
let base = { ...initial };
const subs = new Set();
const clamp = (n, a, b) => Math.min(b, Math.max(a, n));
const hsl = ({ h, s, l }) => `hsl(${((h % 360) + 360) % 360} ${clamp(s,0,100)}% ${clamp(l,0,100)}%)`;
const relLum = ({ h, s, l }) => {
// quick-ish approximation: use l + a little saturation bias
const L = clamp(l, 0, 100) / 100;
const S = clamp(s, 0, 100) / 100;
return clamp(L * 0.9 + S * 0.1, 0, 1);
};
const derive = (b) => {
const steps = [-18, -10, 0, 10, 18].map((dl) => ({ ...b, l: clamp(b.l + dl, 0, 100) }));
const palette = steps.map(hsl);
const gradient = `linear-gradient(135deg, ${palette[1]}, ${palette[3]})`;
const text = relLum(b) > 0.55 ? "hsl(0 0% 10%)" : "hsl(0 0% 98%)";
return Object.freeze({ base: Object.freeze({ ...b }), palette, gradient, text });
};
let theme = derive(base);
const setBase = (patch) => {
base = { ...base, ...patch }; // no external refs
theme = derive(base);
subs.forEach((fn) => fn(theme));
};
return {
get: () => theme,
set: setBase,
subscribe(fn) { subs.add(fn); fn(theme); return () => subs.delete(fn); }
};
}
Neat part is everything derived stays consistent (palette, gradient, contrast text) and consumers only ever see frozen snapshots, so state ownership is obvious and accidental mutation doesn’t silently wreck the theme.