// Generic display formatters (isomorphic). No domain logic — pure presentation. /** Seconds → "M:SS" (clock). */ export const clockMMSS = (secs: number): string => { const s = Math.max(0, Math.floor(secs)); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; }; /** 0..1 (or 0..100) → "NN%". Pass `of100` when the input is already a percentage. */ export const pct = (v: number, digits = 0, of100 = false): string => `${(of100 ? v : v * 100).toFixed(digits)}%`; /** Last token of a full name ("Vinícius Júnior" → "Júnior"). */ export const surname = (name?: string | null): string => { const p = (name || "").trim().split(/\s+/); return p.length > 1 ? p[p.length - 1] : name || ""; }; /** Ordinal suffix ("1st", "2nd", "3rd", "11th"). */ export const ordinal = (n: number): string => { const s = ["th", "st", "nd", "rd"]; const v = n % 100; return n + (s[(v - 20) % 10] || s[v] || s[0]); }; /** HTML-escape for values interpolated into raw SVG/markup strings. */ export const esc = (s?: string | null): string => (s || "").replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); /** Relative luminance of a hex colour (0..1). */ export const luminance = (hex?: string | null): number => { if (!hex) return 0; const h = hex.replace("#", ""); if (h.length < 6) return 0; return ( (0.299 * parseInt(h.slice(0, 2), 16) + 0.587 * parseInt(h.slice(2, 4), 16) + 0.114 * parseInt(h.slice(4, 6), 16)) / 255 ); }; /** Readable ink (#08080A / #fff) over a given background hex. */ export const inkOn = (hex?: string | null): string => (luminance(hex) > 0.6 ? "#08080A" : "#FFFFFF"); /** hex → "rgba(r,g,b,a)". */ export const rgba = (hex: string, a: number): string => { const h = (hex || "").replace("#", ""); return `rgba(${parseInt(h.slice(0, 2), 16)},${parseInt(h.slice(2, 4), 16)},${parseInt(h.slice(4, 6), 16)},${a})`; }; /** Normalize a colour to a leading-# hex, or null. */ export const hex = (c?: string | null): string | null => c ? (c.startsWith("#") ? c : "#" + c) : null;