// Shared utility functions export function escHtml(str) { return String(str ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]); } export function formatDate(iso) { if (!iso) return '—'; try { return new Date(iso).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); } catch { return iso.slice(0, 10); } } export function formatDateTime(iso) { if (!iso) return '—'; try { return new Date(iso).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } catch { return iso.slice(0, 19).replace('T', ' '); } } export function formatRelative(iso) { if (!iso) return '—'; const diff = Date.now() - new Date(iso).getTime(); const m = Math.floor(diff / 60000); if (m < 1) return 'just now'; if (m < 60) return `${m}m ago`; const h = Math.floor(m / 60); if (h < 24) return `${h}h ago`; const d = Math.floor(h / 24); if (d < 7) return `${d}d ago`; return formatDate(iso); } export function debounce(fn, ms = 300) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); }; } export function uuid() { return crypto.randomUUID ? crypto.randomUUID() : 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); } // Truncate a string to maxLen chars, appending ellipsis if needed export function truncate(str, maxLen = 40) { if (!str) return ''; return str.length > maxLen ? str.slice(0, maxLen - 1) + '…' : str; } // First letter of a name for avatar initials export function initials(name) { if (!name) return '?'; const parts = name.trim().split(/\s+/); return parts.length >= 2 ? (parts[0][0] + parts[parts.length - 1][0]).toUpperCase() : name.slice(0, 2).toUpperCase(); } // Download a string as a file export function downloadText(filename, content, type = 'text/plain') { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // Convert array of objects to CSV and download export function downloadCsv(filename, rows) { if (!rows.length) return; const headers = Object.keys(rows[0]); const csv = [ headers.join(','), ...rows.map(r => headers.map(h => JSON.stringify(r[h] ?? '')).join(',')) ].join('\n'); downloadText(filename, csv, 'text/csv'); } // Shorten a SHA-256 hash for display export function shortHash(hash, len = 8) { return hash ? hash.slice(0, len) : '—'; }