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:
70
public/api.js
Normal file
70
public/api.js
Normal file
@@ -0,0 +1,70 @@
|
||||
// Thin fetch wrapper. Owner token in localStorage.void_token. On 401 we
|
||||
// prompt for a token via a modal in modal-root. The wrapper is intentionally
|
||||
// minimal — no retries, no auto-refresh; the owner-only auth model doesn't
|
||||
// need them.
|
||||
|
||||
import { el, mount, clear } from './dom.js';
|
||||
|
||||
const TOKEN_KEY = 'void_token';
|
||||
|
||||
function token() { return localStorage.getItem(TOKEN_KEY) || ''; }
|
||||
|
||||
async function call(method, path, body) {
|
||||
const headers = { 'Authorization': 'Bearer ' + token() };
|
||||
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
||||
const res = await fetch(path, {
|
||||
method,
|
||||
headers,
|
||||
body: body === undefined ? undefined : JSON.stringify(body)
|
||||
});
|
||||
if (res.status === 401) { await promptForToken(); return call(method, path, body); }
|
||||
if (res.status === 204) return null;
|
||||
const ct = res.headers.get('content-type') || '';
|
||||
const data = ct.includes('application/json') ? await res.json() : await res.text();
|
||||
if (!res.ok) {
|
||||
const err = new Error(data?.error?.message || res.statusText);
|
||||
err.status = res.status;
|
||||
err.body = data;
|
||||
throw err;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function promptForToken() {
|
||||
return new Promise((resolve) => {
|
||||
const root = document.getElementById('modal-root');
|
||||
const input = el('input', { type: 'password', style: { width: '100%' }, autofocus: true });
|
||||
const save = () => {
|
||||
const v = input.value.trim();
|
||||
if (!v) return;
|
||||
localStorage.setItem(TOKEN_KEY, v);
|
||||
clear(root);
|
||||
resolve();
|
||||
};
|
||||
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') save(); });
|
||||
mount(root,
|
||||
el('div', { class: 'modal-backdrop' },
|
||||
el('div', { class: 'modal' },
|
||||
el('h2', {}, 'Owner Token'),
|
||||
el('p', { class: 'muted' },
|
||||
'Paste the OWNER_TOKEN configured for the server. It will be stored in localStorage on this device only.'
|
||||
),
|
||||
input,
|
||||
el('div', { class: 'actions' },
|
||||
el('button', { class: 'primary', onclick: save }, 'Save')
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
input.focus();
|
||||
});
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: (p) => call('GET', p),
|
||||
post: (p, body) => call('POST', p, body ?? {}),
|
||||
patch: (p, body) => call('PATCH', p, body ?? {}),
|
||||
del: (p) => call('DELETE', p),
|
||||
setToken: (v) => localStorage.setItem(TOKEN_KEY, v),
|
||||
hasToken: () => !!token()
|
||||
};
|
||||
Reference in New Issue
Block a user