/** * Contraste des pastilles libellé (liste mails) : luminance WCAG + choix texte blanc / foncé. * Les classes `bg-*` sont résolues en hex (palette Tailwind v3 alignée sur les swatches UI). */ /** Luminance relative WCAG 2.1 (sRGB, composantes linéaires 0–1). */ export function relativeLuminanceFromSrgbLinear(r: number, g: number, b: number): number { return 0.2126 * r + 0.7152 * g + 0.0722 * b } function srgbChannelToLinear(c255: number): number { const c = c255 / 255 return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4) } export function hexToRgb(hex: string): [number, number, number] | null { const h = hex.trim().replace(/^#/, "") if (h.length === 3) { const r = parseInt(h[0]! + h[0]!, 16) const g = parseInt(h[1]! + h[1]!, 16) const b = parseInt(h[2]! + h[2]!, 16) if ([r, g, b].some((x) => Number.isNaN(x))) return null return [r, g, b] } if (h.length === 6 || h.length === 8) { const r = parseInt(h.slice(0, 2), 16) const g = parseInt(h.slice(2, 4), 16) const b = parseInt(h.slice(4, 6), 16) if ([r, g, b].some((x) => Number.isNaN(x))) return null return [r, g, b] } return null } export function relativeLuminanceFromHex(hex: string): number { const rgb = hexToRgb(hex) if (!rgb) return 0.5 const [r, g, b] = rgb.map(srgbChannelToLinear) as [number, number, number] return relativeLuminanceFromSrgbLinear(r, g, b) } /** Ratio de contraste WCAG entre deux luminances (symétrique). */ function contrastRatio(L1: number, L2: number): number { const hi = Math.max(L1, L2) + 0.05 const lo = Math.min(L1, L2) + 0.05 return hi / lo } /** « Chroma » simple sur sRGB 8 bits (0 = gris, plus la valeur monte, plus la couleur est vive). */ function rgb255Chroma(rgb: [number, number, number]): number { const [r, g, b] = rgb return (Math.max(r, g, b) - Math.min(r, g, b)) / 255 } /** * Choisit blanc ou quasi-noir sur le fond : biais vers le blanc, prise en compte de la saturation * (les pastelles saturées type Gmail restent en blanc au lieu de basculer en noir par la seule luminance). */ export function labelPillPreferredTextHexOnBackground(bgHex: string): "#ffffff" | "#202124" { const rgb = hexToRgb(bgHex) if (!rgb) return "#202124" const Lbg = relativeLuminanceFromHex(bgHex) const chroma = rgb255Chroma(rgb) const [r255, g255, b255] = rgb // Presque blanc / gris très clair : texte foncé if (Lbg >= 0.88 && chroma < 0.12) return "#202124" // Fond très sombre : blanc if (Lbg <= 0.2) return "#ffffff" // Jaune / ambre très clair (peu de bleu, forte composante R+G) : noir plus lisible if ( Lbg >= 0.62 && b255 < 110 && r255 > 185 && g255 > 165 && chroma >= 0.18 ) { return "#202124" } // Couleurs assez vives et pas trop claires : privilégier le blanc (pastilles type Gmail) if (chroma >= 0.12 && Lbg < 0.78) return "#ffffff" // Gris / couleurs très désaturées au milieu du spectre de luminance : biais blanc sur le score WCAG const rWhite = contrastRatio(Lbg, 1) const rBlack = contrastRatio(Lbg, 0) const WHITE_BIAS = 1.28 return rWhite * WHITE_BIAS >= rBlack ? "#ffffff" : "#202124" } export function labelPillTextClassForBgHex(bgHex: string): string { return labelPillPreferredTextHexOnBackground(bgHex) === "#ffffff" ? "text-white/90" : "text-[#202124]" } /** Palette Tailwind v3 (sRGB) — familles × nuances utilisées par la sidebar / données. */ const TW_BG_HEX = new Map([ // gray ["bg-gray-300", "#d1d5db"], ["bg-gray-400", "#9ca3af"], ["bg-gray-500", "#6b7280"], ["bg-gray-600", "#4b5563"], ["bg-gray-700", "#374151"], // slate ["bg-slate-300", "#cbd5e1"], ["bg-slate-400", "#94a3b8"], ["bg-slate-500", "#64748b"], ["bg-slate-600", "#475569"], ["bg-slate-700", "#334155"], // red ["bg-red-300", "#fca5a5"], ["bg-red-400", "#f87171"], ["bg-red-500", "#ef4444"], ["bg-red-600", "#dc2626"], ["bg-red-700", "#b91c1c"], // orange ["bg-orange-300", "#fdba74"], ["bg-orange-400", "#fb923c"], ["bg-orange-500", "#f97316"], ["bg-orange-600", "#ea580c"], ["bg-orange-700", "#c2410c"], // amber ["bg-amber-300", "#fcd34d"], ["bg-amber-400", "#fbbf24"], ["bg-amber-500", "#f59e0b"], ["bg-amber-600", "#d97706"], ["bg-amber-700", "#b45309"], // yellow ["bg-yellow-300", "#fde047"], ["bg-yellow-400", "#facc15"], ["bg-yellow-500", "#eab308"], ["bg-yellow-600", "#ca8a04"], ["bg-yellow-700", "#a16207"], // lime ["bg-lime-300", "#bef264"], ["bg-lime-400", "#a3e635"], ["bg-lime-500", "#84cc16"], ["bg-lime-600", "#65a30d"], ["bg-lime-700", "#4d7c0f"], // emerald ["bg-emerald-300", "#6ee7b7"], ["bg-emerald-400", "#34d399"], ["bg-emerald-500", "#10b981"], ["bg-emerald-600", "#059669"], ["bg-emerald-700", "#047857"], // teal ["bg-teal-300", "#5eead4"], ["bg-teal-400", "#2dd4bf"], ["bg-teal-500", "#14b8a6"], ["bg-teal-600", "#0d9488"], ["bg-teal-700", "#0f766e"], // cyan ["bg-cyan-400", "#22d3ee"], ["bg-cyan-500", "#06b6d4"], // sky ["bg-sky-300", "#7dd3fc"], ["bg-sky-400", "#38bdf8"], ["bg-sky-500", "#0ea5e9"], ["bg-sky-600", "#0284c7"], ["bg-sky-700", "#0369a1"], // blue ["bg-blue-300", "#93c5fd"], ["bg-blue-400", "#60a5fa"], ["bg-blue-500", "#3b82f6"], ["bg-blue-600", "#2563eb"], ["bg-blue-700", "#1d4ed8"], // indigo ["bg-indigo-300", "#a5b4fc"], ["bg-indigo-400", "#818cf8"], ["bg-indigo-500", "#6366f1"], ["bg-indigo-600", "#4f46e5"], ["bg-indigo-700", "#4338ca"], // violet ["bg-violet-400", "#a78bfa"], ["bg-violet-500", "#8b5cf6"], // purple ["bg-purple-300", "#d8b4fe"], ["bg-purple-400", "#c084fc"], ["bg-purple-500", "#a855f7"], ["bg-purple-600", "#9333ea"], ["bg-purple-700", "#7e22ce"], // fuchsia ["bg-fuchsia-500", "#d946ef"], // pink ["bg-pink-300", "#f9a8d4"], ["bg-pink-400", "#f472b6"], ["bg-pink-500", "#ec4899"], ["bg-pink-600", "#db2777"], ["bg-pink-700", "#be185d"], // green ["bg-green-400", "#4ade80"], ["bg-green-500", "#22c55e"], // rose ["bg-rose-400", "#fb7185"], ["bg-rose-500", "#f43f5e"], ]) export function tailwindBgUtilityToHex(bgClass: string): string | null { return TW_BG_HEX.get(bgClass.trim()) ?? null } /** Classes Tailwind `text-[...]` pour pastille sur fond `bg-*`. */ export function labelPillTextClassForTailwindBgUtility(bgClass: string): string { const hex = tailwindBgUtilityToHex(bgClass) if (!hex) { const c = bgClass.toLowerCase() if (/(^|-)(gray|slate|zinc|stone|neutral)(-|$)/.test(c)) return "text-[#202124]" return "text-white/90" } return labelPillTextClassForBgHex(hex) }