/* Vooice — Live shell. Single-page navigation between screens. Fluid: screens fill the viewport at any size, mobile-friendly. */ const TWEAK_DEFAULTS_LIVE = /*EDITMODE-BEGIN*/{ "signal": "#F04E23", "paper": "#F4EFE6", "ink": "#0E0E0F", "sage": "#7A8A6E", "butter": "#E8C36A", "logoMark":"halfdisc" }/*EDITMODE-END*/; function applyTweaksLive(t) { VC_TOKENS.signal = t.signal; VC_TOKENS.paper = t.paper; VC_TOKENS.paper2 = shadeLive(t.paper, -0.04); VC_TOKENS.paper3 = shadeLive(t.paper, -0.10); VC_TOKENS.ink = t.ink; VC_TOKENS.ink2 = shadeLive(t.ink, 0.10); VC_TOKENS.ink3 = shadeLive(t.ink, 0.42); VC_TOKENS.sage = t.sage; VC_TOKENS.butter = t.butter; VC_TOKENS.signalSoft = mixLive(t.signal, t.paper, 0.84); VC_TOKENS.sageSoft = mixLive(t.sage, t.paper, 0.84); VC_TOKENS.lineSoft = `rgba(0,0,0,0.10)`; VC_TOKENS.line = t.ink; document.body.style.background = t.paper; } function hexLive(h){const x=h.replace('#','');return [0,1,2].map(i=>parseInt(x.substr(i*2,2),16));} function rgbLive(a){return '#'+a.map(v=>Math.max(0,Math.min(255,Math.round(v))).toString(16).padStart(2,'0')).join('');} function shadeLive(h,a){const[r,g,b]=hexLive(h);if(a>=0)return rgbLive([r+(255-r)*a,g+(255-g)*a,b+(255-b)*a]);return rgbLive([r*(1+a),g*(1+a),b*(1+a)]);} function mixLive(c,p,pa){const a=hexLive(c),b=hexLive(p);return rgbLive([0,1,2].map(i=>a[i]*(1-pa)+b[i]*pa));} const ROUTES = { '/': { c: 'MarketingHero', label: 'Landing' }, '/app': { c: 'Dashboard', label: 'Home' }, '/calls/live': { c: 'OutboundCall', label: 'Live call' }, '/calls/c_3F2A8B':{ c: 'CallDetail', label: 'Call detail' }, '/reminders': { c: 'Reminders', label: 'Reminders' }, '/memory': { c: 'ContactsMemory', label: 'Memory' }, '/audit': { c: 'AuditLog', label: 'Audit log' }, '/pricing': { c: 'Pricing', label: 'Pricing' }, }; const NAV_ALIAS = { 'home':'/app', 'outbound-call':'/calls/live', 'call-detail':'/calls/c_3F2A8B', 'reminders':'/reminders', 'contacts-memory':'/memory', 'audit':'/audit', 'pricing':'/pricing', 'marketing-hero':'/', 'about':'/', }; function useRoute() { const get = () => (window.location.hash.replace(/^#/, '') || '/'); const [path, setPath] = React.useState(get); React.useEffect(() => { const onHash = () => setPath(get()); window.addEventListener('hashchange', onHash); return () => window.removeEventListener('hashchange', onHash); }, []); const navigate = React.useCallback((to) => { const p = NAV_ALIAS[to] || to; if (p.startsWith('/')) window.location.hash = p; }, []); return [path, navigate]; } // Track viewport so we can swap layouts at breakpoints. We expose this on // window so any screen can read it without prop-drilling. function useViewport() { const [vw, setVw] = React.useState(window.innerWidth); React.useEffect(() => { const on = () => setVw(window.innerWidth); window.addEventListener('resize', on); return () => window.removeEventListener('resize', on); }, []); React.useEffect(() => { window.__vcVw = vw; }, [vw]); return vw; } function LiveShell() { const [t, setT] = useTweaks(TWEAK_DEFAULTS_LIVE); applyTweaksLive(t); window.__vcLogoMark = t.logoMark || 'halfdisc'; const [path, navigate] = useRoute(); const vw = useViewport(); const route = ROUTES[path] || ROUTES['/']; const ScreenComp = window[route.c] || window.Dashboard; const tweakKey = JSON.stringify(t); return ( <>
setT('signal', v)} /> setT('paper', v)} /> setT('ink', v)} /> setT('sage', v)} /> setT('butter', v)} /> setT('logoMark', v)} options={[ { value: 'halfdisc', label: 'Half-disc' }, { value: 'phone', label: 'Phone' }, { value: 'sound', label: 'Sound' }, { value: 'aperture', label: 'Aperture' }, { value: 'wordmark', label: 'Wordmark' }, ]} />
{Object.entries(ROUTES).map(([p, r]) => ( ))}
); } const loadingLive = document.getElementById('loading'); if (loadingLive) loadingLive.remove(); ReactDOM.createRoot(document.getElementById('root')).render();