import type { SikshyaReactConfig } from '../types'; /** Tailwind `brand-*` stops used across the React admin. */ const BRAND_SHADES = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const; type Rgb = { r: number; g: number; b: number }; const DEFAULT_ACCENT_HEX = '#2c5ba8'; function clamp255(n: number): number { return Math.max(0, Math.min(255, Math.round(n))); } function normalizeHex(input?: string | null): string | null { if (!input) { return null; } let h = String(input).trim(); if (h === '') { return null; } if (h.startsWith('#')) { h = h.slice(1); } if (h.length === 3) { h = h .split('') .map((c) => c + c) .join(''); } if (h.length !== 6 || !/^[0-9a-fA-F]{6}$/.test(h)) { return null; } return `#${h.toLowerCase()}`; } function parseRgb(hex: string): Rgb { const h = hex.startsWith('#') ? hex.slice(1) : hex; return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16), }; } function mixRgb(a: Rgb, b: Rgb, t: number): Rgb { const u = Math.max(0, Math.min(1, t)); return { r: clamp255(a.r + (b.r - a.r) * u), g: clamp255(a.g + (b.g - a.g) * u), b: clamp255(a.b + (b.b - a.b) * u), }; } function relLuminance(hex: string): number { const { r, g, b } = parseRgb(hex); const lin = (c: number) => { const x = c / 255; return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); }; const R = lin(r); const G = lin(g); const B = lin(b); return 0.2126 * R + 0.7152 * G + 0.0722 * B; } function rgbTriplet(rgb: Rgb): string { return `${rgb.r} ${rgb.g} ${rgb.b}`; } /** * Build a Tailwind-like brand ramp from a single accent (sidebar or saved brand hex). * Returns space-separated RGB triplets for `rgb(var(--token) / )` usage. */ export function buildBrandRgbScaleFromAccent(accentHex: string): Record<(typeof BRAND_SHADES)[number], string> { const base = parseRgb(accentHex); const white: Rgb = { r: 255, g: 255, b: 255 }; const black: Rgb = { r: 0, g: 0, b: 0 }; const out = {} as Record<(typeof BRAND_SHADES)[number], string>; out[50] = rgbTriplet(mixRgb(base, white, 0.92)); out[100] = rgbTriplet(mixRgb(base, white, 0.78)); out[200] = rgbTriplet(mixRgb(base, white, 0.62)); out[300] = rgbTriplet(mixRgb(base, white, 0.45)); out[400] = rgbTriplet(mixRgb(base, white, 0.28)); out[500] = rgbTriplet(base); out[600] = rgbTriplet(mixRgb(base, black, 0.14)); out[700] = rgbTriplet(mixRgb(base, black, 0.26)); out[800] = rgbTriplet(mixRgb(base, black, 0.38)); out[900] = rgbTriplet(mixRgb(base, black, 0.5)); out[950] = rgbTriplet(mixRgb(base, black, 0.62)); return out; } /** * Pick one hex to drive admin `brand-*` tokens. Prefer sidebar (school chrome), then the * saved “top header / accent” colour from white-label settings — not used to paint the top bar. */ export function pickAdminAccentHex(branding?: SikshyaReactConfig['branding']): string { const top = normalizeHex(branding?.topbarBg); const side = normalizeHex(branding?.sidebarBg); const tryPick = (hex: string | null): string | null => { if (!hex) { return null; } const lum = relLuminance(hex); if (lum < 0.93) { return hex; } return null; } const fromSide = tryPick(side); if (fromSide) { return fromSide; } const fromTop = tryPick(top); if (fromTop) { return fromTop; } if (side) { return side; } if (top) { return top; } return DEFAULT_ACCENT_HEX; } export function applyAdminBrandThemeToRoot(root: HTMLElement, branding?: SikshyaReactConfig['branding']): void { const accent = pickAdminAccentHex(branding); const scale = buildBrandRgbScaleFromAccent(accent); for (const shade of BRAND_SHADES) { root.style.setProperty(`--sikshya-brand-${shade}-rgb`, scale[shade]); } } export function clearAdminBrandThemeFromRoot(root: HTMLElement): void { for (const shade of BRAND_SHADES) { root.style.removeProperty(`--sikshya-brand-${shade}-rgb`); } } /** `rgba(r,g,b,a)` from `#rrggbb` (or `rrggbb`). */ export function rgbaFromHex(hex: string, alpha: number): string | null { const n = normalizeHex(hex); if (!n) { return null; } const { r, g, b } = parseRgb(n); const a = Math.max(0, Math.min(1, alpha)); return `rgba(${r}, ${g}, ${b}, ${a})`; }