215 lines
6.5 KiB
TypeScript
215 lines
6.5 KiB
TypeScript
/**
|
||
* 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<string, string>([
|
||
// 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)
|
||
}
|