/** * Brand-aligned Lottie tinting (Tailwind theme tokens → normalized RGBA). * Primary: #5750F1 — slightly lifted in dark mode for contrast on `dark` bg. */ export type LottieThemeMode = "light" | "dark"; /** #5750F1 */ const PRIMARY_LIGHT = [87 / 255, 80 / 255, 241 / 255, 1] as const; /** Brighter violet for fills on #111928 */ const PRIMARY_ON_DARK = [125 / 255, 118 / 255, 252 / 255, 1] as const; function deepClone(value: T): T { return JSON.parse(JSON.stringify(value)) as T; } function colorsMatch(a: unknown, b: readonly number[]): boolean { if (!Array.isArray(a) || a.length !== 4) return false; return a.every( (v, i) => typeof v === "number" && Math.abs(v - b[i]!) < 0.0001, ); } /** * Replace exact RGBA quadruples anywhere in the JSON tree (Lottie uses nested `c.k`). */ function replaceRgbaEverywhere( node: unknown, from: readonly number[], to: readonly number[], ): unknown { if (node === null || typeof node !== "object") { return node; } if (Array.isArray(node)) { if (colorsMatch(node, from)) { return [...to]; } return node.map((item) => replaceRgbaEverywhere(item, from, to)); } const out: Record = {}; for (const [k, v] of Object.entries(node)) { out[k] = replaceRgbaEverywhere(v, from, to); } return out; } /** Pure black fills/strokes → brand primary (loading spinner dots, etc.). */ export function applyLoadingLottieTheme( data: T, mode: LottieThemeMode, ): T { const clone = deepClone(data); const to = mode === "dark" ? PRIMARY_ON_DARK : PRIMARY_LIGHT; return replaceRgbaEverywhere(clone, [0, 0, 0, 1], to) as T; } /** * Full illustrations (e.g. 404): clone only until we add palette maps per asset. */ export function applyIllustrationLottieTheme( data: T, _mode: LottieThemeMode, ): T { return deepClone(data); }