ultisuite-client/lib/label-pill-contrast.ts

225 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 01). */
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
}
/** Couleur dicône dossier sidebar : `bg-*` Tailwind ou `bg-[#hex]`. */
export function navFolderIconColorFromBgClass(bgClass: string): string {
const trimmed = bgClass.trim()
const mapped = tailwindBgUtilityToHex(trimmed)
if (mapped) return mapped
const arbitrary = trimmed.match(/^bg-\[(#[\da-fA-F]{3,8})\]$/)
if (arbitrary?.[1]) return arbitrary[1]
return "#9ca3af"
}
/** 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)
}