136 lines
3.9 KiB
TypeScript
136 lines
3.9 KiB
TypeScript
// Type-safe theme initializer that writes CSS variables expected by your Tailwind/index.css.
|
|
import theme from "../theme.json";
|
|
|
|
type ThemeShape = {
|
|
primary?: string;
|
|
background?: string;
|
|
foreground?: string;
|
|
radius?: number | string;
|
|
appearance?: string;
|
|
variant?: string;
|
|
[key: string]: any;
|
|
};
|
|
|
|
const t = theme as ThemeShape | undefined;
|
|
|
|
/**
|
|
* Convert inputs into the token form "H S% L%" that your CSS expects.
|
|
* Accepts:
|
|
* - token form "210 79% 46%"
|
|
* - "hsl(210, 79%, 46%)"
|
|
* - hex: "#aabbcc" or "abc"
|
|
*/
|
|
function hslStringToToken(input?: string | null): string | null {
|
|
if (!input) return null;
|
|
const str = input.trim();
|
|
|
|
// Already tokenized like "210 79% 46%"
|
|
if (/^\d+\s+\d+%?\s+\d+%?$/.test(str)) return str;
|
|
|
|
// hsl(...) form -> extract contents then validate parts
|
|
const hslMatch = str.match(/hsl\(\s*([^)]+)\s*\)/i);
|
|
if (hslMatch && typeof hslMatch[1] === "string") {
|
|
const raw = hslMatch[1];
|
|
const parts = raw.split(",").map((p) => p.trim());
|
|
if (parts.length >= 3) {
|
|
const rawH = parts[0] ?? "";
|
|
const rawS = parts[1] ?? "";
|
|
const rawL = parts[2] ?? "";
|
|
const h = rawH.replace(/deg$/i, "").trim() || "0";
|
|
const s = rawS.endsWith("%") ? rawS : rawS ? `${rawS}%` : "0%";
|
|
const l = rawL.endsWith("%") ? rawL : rawL ? `${rawL}%` : "0%";
|
|
return `${h} ${s} ${l}`;
|
|
}
|
|
}
|
|
|
|
// hex -> convert to hsl token
|
|
const hexMatch = str.match(/^#?([0-9a-f]{6}|[0-9a-f]{3})$/i);
|
|
const hex = hexMatch?.[1];
|
|
if (hex && typeof hex === "string") {
|
|
const normalizedHex =
|
|
hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
|
|
|
|
// parse safely
|
|
const r = parseInt(normalizedHex.substring(0, 2), 16) / 255;
|
|
const g = parseInt(normalizedHex.substring(2, 4), 16) / 255;
|
|
const b = parseInt(normalizedHex.substring(4, 6), 16) / 255;
|
|
|
|
const max = Math.max(r, g, b);
|
|
const min = Math.min(r, g, b);
|
|
let h = 0;
|
|
let s = 0;
|
|
const l = (max + min) / 2;
|
|
|
|
if (max !== min) {
|
|
const d = max - min;
|
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
switch (max) {
|
|
case r:
|
|
h = (g - b) / d + (g < b ? 6 : 0);
|
|
break;
|
|
case g:
|
|
h = (b - r) / d + 2;
|
|
break;
|
|
case b:
|
|
h = (r - g) / d + 4;
|
|
break;
|
|
}
|
|
h = Math.round(h * 60);
|
|
} else {
|
|
h = 0;
|
|
s = 0;
|
|
}
|
|
|
|
const sPct = `${Math.round(s * 100)}%`;
|
|
const lPct = `${Math.round(l * 100)}%`;
|
|
return `${h} ${sPct} ${lPct}`;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function applyThemeObject(themeObj?: ThemeShape) {
|
|
if (!themeObj) return;
|
|
const root = document.documentElement;
|
|
|
|
// primary (maps to --primary used by tailwind.config)
|
|
if (themeObj.primary) {
|
|
const token = hslStringToToken(String(themeObj.primary));
|
|
if (token) root.style.setProperty("--primary", token);
|
|
}
|
|
|
|
// optional background/foreground/card if present
|
|
if (themeObj.background) {
|
|
const token = hslStringToToken(String(themeObj.background));
|
|
if (token) root.style.setProperty("--background", token);
|
|
}
|
|
if (themeObj.foreground) {
|
|
const token = hslStringToToken(String(themeObj.foreground));
|
|
if (token) root.style.setProperty("--foreground", token);
|
|
}
|
|
|
|
// radius (index.css expects --radius)
|
|
if (typeof themeObj.radius !== "undefined") {
|
|
const radiusVal =
|
|
typeof themeObj.radius === "number"
|
|
? `${themeObj.radius}rem`
|
|
: String(themeObj.radius);
|
|
root.style.setProperty("--radius", radiusVal);
|
|
}
|
|
|
|
// data attributes
|
|
if (themeObj.appearance) root.setAttribute("data-appearance", String(themeObj.appearance));
|
|
if (themeObj.variant) root.setAttribute("data-variant", String(themeObj.variant));
|
|
}
|
|
|
|
// apply as early as possible
|
|
try {
|
|
applyThemeObject(t);
|
|
} catch (err) {
|
|
// don't break runtime if theme parsing fails
|
|
// eslint-disable-next-line no-console
|
|
console.warn("theme-init failed to apply theme:", err);
|
|
}
|
|
|
|
export default theme;
|