// public/js/util.js export const fmt = new Intl.NumberFormat(undefined,{maximumFractionDigits:6}); export const currency = (v,cur='USD')=> new Intl.NumberFormat(undefined,{style:'currency',currency:cur,maximumFractionDigits:2}).format(v); export const persist = (key, obj) => localStorage.setItem(key, JSON.stringify(obj)); export const revive = (key, fallback={}) => { try { return JSON.parse(localStorage.getItem(key)) || fallback } catch { return fallback } }; export function el(tag, attrs={}, children=[]){ const x = document.createElement(tag); for (const [k,v] of Object.entries(attrs||{})){ if (k === 'class') x.className = v ?? ''; else if (k === 'html') x.innerHTML = v ?? ''; else if (k === 'on' && v && typeof v === 'object'){ for (const [evt, fn] of Object.entries(v)){ if (typeof fn === 'function') x.addEventListener(evt, fn, false); } } else { if (v !== null && v !== undefined && v !== false){ x.setAttribute(k, v === true ? '' : String(v)); } } } (Array.isArray(children) ? children : [children]).forEach(ch => { if (ch != null) x.append(ch) }); return x; } export function labelInput(labelText, type, name, value, attrs={}){ const w = el('div'); w.append(el('label',{html:labelText})); const input = el('input',{type, name, value: String(value ?? ''), ...attrs}); w.append(input); return w; } export function labelSelect(labelText, name, value, options){ const w = el('div'); w.append(el('label',{html:labelText})); const sel = el('select',{name, 'data-ui':'lite'}); // opt into Select Lite options.forEach(([val, text])=>{ const opt = el('option',{value:val}); opt.textContent = text; if (String(val) === String(value)) opt.selected = true; sel.append(opt); }); w.append(sel); return w; } /* ---- Theme helpers ---- */ export function applyTheme(mode){ if(mode === 'auto'){ document.documentElement.removeAttribute('data-theme'); } else{ document.documentElement.setAttribute('data-theme', mode); } localStorage.setItem('theme', mode); } export function initTheme(toggleBtn){ const saved = localStorage.getItem('theme') || 'auto'; applyTheme(saved); if(toggleBtn){ toggleBtn.textContent = titleForTheme(saved); toggleBtn.addEventListener('click', ()=>{ const next = nextTheme(localStorage.getItem('theme')||'auto'); applyTheme(next); toggleBtn.textContent = titleForTheme(next); }); } } function nextTheme(mode){ return mode==='auto' ? 'light' : mode==='light' ? 'dark' : 'auto' } function titleForTheme(mode){ return mode==='auto' ? 'Auto' : mode==='light' ? 'Light' : 'Dark' } /* ---- Select Lite: progressively enhance