// Void 2.0 SPA bootstrap. Mounts chrome (sidebar/topbar/rightrail) and // delegates the #main panel to a view module based on the current route. // Views are loaded dynamically so a missing view never breaks the shell. import { api } from './api.js'; import { route, current, navigate } from './router.js'; import { renderSidebar } from './components/sidebar.js'; import { renderTopbar } from './components/topbar.js'; import { renderDrossBubble } from './components/dross_bubble.js'; import { emit, state } from './state.js'; import { el, mount } from './dom.js'; import { attachDropzone } from './components/dropzone.js'; import { initChrome } from './components/chrome.js'; import { loadTheme } from './theme.js'; const VIEWS = { home: () => import('./views/home.js'), space: () => import('./views/space.js'), project: () => import('./views/project.js'), page: () => import('./views/page.js'), ref: () => import('./views/reference.js'), resource: () => import('./views/resource.js'), search: () => import('./views/search.js'), inbox: () => import('./views/inbox.js'), 'sacred-valley': () => import('./views/sacred_valley.js'), yerin: () => import('./views/sentinel.js'), 'little-blue': () => import('./views/little_blue.js'), terminal: () => import('./views/terminal.js'), timelapse: () => import('./views/timelapse.js'), 'ai-usage': () => import('./views/aiusage.js'), obd2: () => import('./views/obd2.js'), links: () => import('./views/links.js'), mirror: () => import('./views/mirror.js'), settings: () => import('./views/settings.js'), jobs: () => import('./views/jobs.js'), speedtest: () => import('./views/speedtest.js') }; async function renderView(ctx) { // Update cross-component state so the right rail knows the active Space + view. if (ctx.name === 'space') { state.spaceId = ctx.params.id || null; state.view = null; } else if (ctx.name === 'project' || ctx.name === 'page' || ctx.name === 'ref' || ctx.name === 'resource') { // Keep the last known spaceId; update the focused entity. state.view = { entityType: ctx.name, entityId: ctx.params.id || null }; } else { state.view = null; } // Notify subscribers (right rail) of the active Space. The state bus replays // the last value on subscribe, so this covers both the initial route() call // and every subsequent navigation with one path. emit('space-active', state.spaceId); const main = document.getElementById('main'); const loader = VIEWS[ctx.name] || VIEWS.home; try { const mod = await loader(); await mod.render(main, ctx); } catch (e) { console.error('view render failed', e); mount(main, el('h1', { class: 'view-h1' }, 'Something went wrong'), el('p', { class: 'view-sub' }, 'View failed to load. Check console for details.'), el('pre', {}, String(e && e.stack || e)) ); } } async function pollPending() { try { const rows = await api.get('/api/pending-changes'); emit('pending-count', rows.length); } catch (e) { if (e.status === 403) emit('pending-count', 0); } } async function init() { if (!api.hasToken()) { try { await api.get('/api/spaces'); } catch { /* api wrapper opens the modal on 401 */ } } await loadTheme(); // apply saved palette overrides before rendering chrome renderTopbar(document.getElementById('topbar')); renderSidebar(document.getElementById('sidebar')); renderDrossBubble(); initChrome(); attachDropzone(document.getElementById('main')); route(renderView); pollPending(); setInterval(pollPending, 15000); } window.addEventListener('DOMContentLoaded', init); window.voidNav = navigate; // dev convenience: window.voidNav('/space/abc')