export type AlertA11yProps = { role: 'alert'; 'aria-live': 'assertive' | 'polite'; 'aria-label': string; }; export function alertLabel(level: string, description: string): AlertA11yProps { return { role: 'alert', 'aria-live': level === 'danger' ? 'assertive' : 'polite', 'aria-label': description, }; } export type ProgressbarA11yProps = { role: 'progressbar'; 'aria-label': string; 'aria-valuenow': number; 'aria-valuemin': number; 'aria-valuemax': number; 'aria-valuetext': string; }; export function progressLabel( label: string, valuePct: number, description: string, ): ProgressbarA11yProps { return { role: 'progressbar', 'aria-label': label, 'aria-valuenow': valuePct, 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuetext': description, }; } export type AriaLabelOnly = { 'aria-label': string }; export function streakLabel(days: number): AriaLabelOnly { return { 'aria-label': `${days} day streak` }; } export type ButtonA11yProps = { 'aria-label': string; 'aria-roledescription'?: string; }; export function buttonLabel(text: string, hint?: string): ButtonA11yProps { return { 'aria-label': text, ...(hint ? { 'aria-roledescription': hint } : {}), }; } export function achievementLabel(name: string, description: string): AriaLabelOnly { return { 'aria-label': `Achievement: ${name} — ${description}` }; } export type TimerA11yProps = { 'aria-label': string; 'aria-live': 'polite'; }; export function timerLabel( hours: number, minutes: number, seconds: number, status: string, ): TimerA11yProps { return { 'aria-label': `Timer: ${hours}h ${minutes}m ${seconds}s, ${status}`, 'aria-live': 'polite', }; } export type SliderA11yProps = { role: 'slider'; 'aria-label': string; 'aria-valuenow': number; 'aria-valuemin': number; 'aria-valuemax': number; }; export function sliderLabel( label: string, value: number, min: number, max: number, ): SliderA11yProps { return { role: 'slider', 'aria-label': label, 'aria-valuenow': value, 'aria-valuemin': min, 'aria-valuemax': max, }; } function plural(n: number, singular: string, pluralForm: string): string { const word = n === 1 ? singular : pluralForm; return `${n} ${word}`; } /** * Spoken-friendly duration from a fractional hour value, e.g. 12 → "12 hours", 1.5 → "1 hour 30 minutes". */ export function formatDurationForA11y(hours: number): string { const totalMinutes = Math.round(hours * 60); const h = Math.floor(totalMinutes / 60); const m = totalMinutes % 60; if (h === 0 && m === 0) { return '0 minutes'; } if (m === 0) { return plural(h, 'hour', 'hours'); } if (h === 0) { return plural(m, 'minute', 'minutes'); } return `${plural(h, 'hour', 'hours')} ${plural(m, 'minute', 'minutes')}`; }