feat(ui): static shell + router + api wrapper
Three-column grid (sidebar / main / right rail) with Cradle aesthetic: blackflame accent on Cinzel display headings + Cormorant Garamond body in cards, system UI for chrome. Hash-based router covers all entity routes plus search, inbox, sacred-valley. api.js stores OWNER_TOKEN in localStorage and prompts via a modal on 401. dom.js provides safe el() + mount() builders so no component ever assigns innerHTML from API data (the only exception is an explicit, scary-named html: opt-in for sanitizer output, used later by the markdown editor). state.js is a tiny event bus for shared chrome state (pending count). Components and views are loaded as ES modules — sidebar / topbar / rightrail + 9 view stubs that the later Phase E tasks fill in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
64
public/app.js
Normal file
64
public/app.js
Normal file
@@ -0,0 +1,64 @@
|
||||
// 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 } from './state.js';
|
||||
import { el, mount } from './dom.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')
|
||||
};
|
||||
|
||||
async function renderView(ctx) {
|
||||
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'));
|
||||
route(renderView);
|
||||
pollPending();
|
||||
setInterval(pollPending, 15000);
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', init);
|
||||
window.voidNav = navigate; // dev convenience: window.voidNav('/space/abc')
|
||||
Reference in New Issue
Block a user