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:
25
public/components/rightrail.js
Normal file
25
public/components/rightrail.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// T17 stub — collapsible rail with localStorage persistence.
|
||||
// Chat lands in Plan 5.
|
||||
import { el, mount } from '../dom.js';
|
||||
|
||||
const KEY = 'void_rail_collapsed';
|
||||
|
||||
export function renderRightrail(root) {
|
||||
const shell = document.getElementById('shell');
|
||||
let collapsed = localStorage.getItem(KEY) === 'true';
|
||||
if (collapsed) shell.classList.add('rail-collapsed');
|
||||
|
||||
function toggle() {
|
||||
collapsed = !collapsed;
|
||||
localStorage.setItem(KEY, String(collapsed));
|
||||
shell.classList.toggle('rail-collapsed', collapsed);
|
||||
}
|
||||
|
||||
mount(root,
|
||||
el('div', { class: 'rail-toggle', onclick: toggle, title: 'Toggle right rail' }, 'CRADLE'),
|
||||
el('div', { class: 'rail-body' },
|
||||
el('h3', { style: { marginTop: 0 } }, 'Companion'),
|
||||
el('p', { class: 'muted' }, 'Chat lands in Plan 5. The rail is here so the layout is honest about the empty space it will take.')
|
||||
)
|
||||
);
|
||||
}
|
||||
12
public/components/sidebar.js
Normal file
12
public/components/sidebar.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// T17 stub — full implementation lands in T18.
|
||||
// For now: brand only, so the shell renders without errors.
|
||||
import { el, mount } from '../dom.js';
|
||||
|
||||
export function renderSidebar(root) {
|
||||
mount(root,
|
||||
el('div', { class: 'sb-section' },
|
||||
el('div', { class: 'sb-title' }, 'Void'),
|
||||
el('div', { class: 'sb-item muted' }, 'Sidebar loads in T18')
|
||||
)
|
||||
);
|
||||
}
|
||||
20
public/components/topbar.js
Normal file
20
public/components/topbar.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// T17 stub — full implementation lands in T18.
|
||||
import { el, mount } from '../dom.js';
|
||||
import { navigate } from '../router.js';
|
||||
|
||||
export function renderTopbar(root) {
|
||||
const search = el('input', {
|
||||
type: 'text',
|
||||
placeholder: 'Search …',
|
||||
onkeydown: (e) => {
|
||||
if (e.key === 'Enter' && e.target.value.trim()) {
|
||||
navigate('/search?q=' + encodeURIComponent(e.target.value.trim()));
|
||||
}
|
||||
}
|
||||
});
|
||||
mount(root,
|
||||
el('div', { class: 'brand' }, 'VOID'),
|
||||
el('div', { class: 'topbar-search' }, search),
|
||||
el('div', { class: 'topbar-spacer' })
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user