- Project card expands to show description + status + dates (was only the research stub) - Cards compacted + responsive (actions wrap on narrow) - Sentinel renamed Yerin everywhere (#/yerin, red 'Sage of the Endless Sword' theme + red sidebar dot) - void1 importer now carries research_notes/last_researched_at (was dropped) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
3.4 KiB
JavaScript
89 lines
3.4 KiB
JavaScript
// 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 { renderRightrail } from './components/rightrail.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';
|
|
|
|
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'),
|
|
settings: () => import('./views/settings.js'),
|
|
jobs: () => import('./views/jobs.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 */ }
|
|
}
|
|
renderTopbar(document.getElementById('topbar'));
|
|
renderSidebar(document.getElementById('sidebar'));
|
|
renderRightrail(document.getElementById('rightrail'));
|
|
initChrome();
|
|
attachDropzone(document.getElementById('main'));
|
|
route(renderView);
|
|
pollPending();
|
|
setInterval(pollPending, 15000);
|
|
}
|
|
|
|
window.addEventListener('DOMContentLoaded', init);
|
|
window.voidNav = navigate; // dev convenience: window.voidNav('/space/abc')
|