const ADJECTIVES_FR = [ "Agile", "Brave", "Calme", "Curieux", "Discret", "Elegant", "Fidele", "Gentil", "Habile", "Joyeux", "Leger", "Malin", "Noble", "Paisible", "Rapide", "Sage", "Timide", "Vif", "Zen", "Zeste", ] as const const ANIMALS_FR = [ "Lynx", "Panda", "Faucon", "Loutre", "Renard", "Heron", "Castor", "Koala", "Loup", "Ours", "Tigre", "Aigle", "Dauphin", "Phoque", "Ecureuil", "Chouette", "Cerf", "Lama", "Belette", "Pieuvre", ] as const /** Background colors for caret labels (white text — WCAG AA 4.5:1+). */ const CURSOR_COLORS = [ "#6D28D9", // violet "#BE123C", // rose "#C2410C", // orange "#A16207", // gold (not light yellow) "#1D4ED8", // blue "#0F766E", // teal "#15803D", // green "#A21CAF", // fuchsia "#4338CA", // indigo "#B45309", // amber ] as const const MIN_CURSOR_CONTRAST_WITH_WHITE = 4.5 function hexLuminance(hex: string): number { const raw = hex.replace("#", "") if (raw.length !== 6) return 1 const channel = (i: number) => { const c = parseInt(raw.slice(i, i + 2), 16) / 255 return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4 } return 0.2126 * channel(0) + 0.7152 * channel(2) + 0.0722 * channel(4) } function contrastWithWhite(bgHex: string): number { const bg = hexLuminance(bgHex) return (1 + 0.05) / (bg + 0.05) } export function isReadableCursorColor(hex: string): boolean { return contrastWithWhite(hex) >= MIN_CURSOR_CONTRAST_WITH_WHITE } function resolveCursorColor(id: string, stored?: string): string { if (stored && isReadableCursorColor(stored)) return stored return colorForGuestId(id) } export type GuestEditorIdentity = { guestId: string guestName: string color: string } const STORAGE_PREFIX = "ultidrive-guest-identity:" function pick(items: readonly T[]): T { return items[Math.floor(Math.random() * items.length)]! } function hashString(input: string): number { let hash = 0 for (let i = 0; i < input.length; i++) { hash = (hash << 5) - hash + input.charCodeAt(i) hash |= 0 } return Math.abs(hash) } export function generateGuestDisplayName(): string { return `${pick(ADJECTIVES_FR)} ${pick(ANIMALS_FR)}` } export function colorForGuestId(guestId: string): string { return CURSOR_COLORS[hashString(guestId) % CURSOR_COLORS.length]! } function newGuestId(): string { if (typeof crypto !== "undefined" && "randomUUID" in crypto) { return crypto.randomUUID() } return `guest-${Date.now()}-${Math.random().toString(36).slice(2, 9)}` } export function getGuestEditorIdentity(shareToken: string): GuestEditorIdentity { const key = `${STORAGE_PREFIX}${shareToken}` if (typeof sessionStorage !== "undefined") { const raw = sessionStorage.getItem(key) if (raw) { try { const parsed = JSON.parse(raw) as GuestEditorIdentity if (parsed.guestId && parsed.guestName) { const color = resolveCursorColor(parsed.guestId, parsed.color) return { guestId: parsed.guestId, guestName: parsed.guestName, color, } } } catch { /* regenerate */ } } } const guestId = newGuestId() const identity: GuestEditorIdentity = { guestId, guestName: generateGuestDisplayName(), color: colorForGuestId(guestId), } if (typeof sessionStorage !== "undefined") { sessionStorage.setItem(key, JSON.stringify(identity)) } return identity }