Files
mteehan 3de82e3b9b Remove query-string cache-busting from static assets
Drop ?v=N version pins from all JS imports and HTML <link>/<script> tags.
Cache invalidation is now handled solely by server-side cache-control headers.
Update docs and AGENTS.md accordingly.
2026-07-28 13:50:22 +00:00

62 lines
1.9 KiB
JavaScript

/**
* Hoover — router.js
*
* Hash-based SPA router with reactive state (triggers re-render on
* navigation). Link component for client-side navigation.
*/
import { reactive } from './reactivity.js';
import { h } from './vdom.js';
/**
* Hash-based router.
*
* const router = createRouter({
* '/dashboard': () => h('#comp', { component: DashboardPage, key: '/dashboard' }, []),
* '/interfaces': () => h('#comp', { component: InterfacesPage, key: '/interfaces' }, []),
* '*': () => h('#comp', { component: NotFoundPage, key: '*' }, []),
* });
*
* Reactive `router.state.path` updates trigger re-renders automatically.
*/
export function createRouter(routes) {
const initialPath = location.hash.slice(1) || '/dashboard';
if (!location.hash) location.hash = initialPath;
const state = reactive({ path: initialPath });
window.addEventListener('hashchange', () => {
state.path = location.hash.slice(1) || '/dashboard';
});
const component = () => {
const handler = routes[state.path] || routes['*'];
if (!handler) {
return h('div', { class: 'card' },
h('div', { class: 'text-muted' }, `404 — Not found: ${state.path}`));
}
try {
return handler();
} catch (e) {
return h('div', { class: 'card' },
h('div', { class: 'text-muted' }, `Error: ${e.message || String(e)}`));
}
};
return { state, navigate: (p) => { location.hash = p; }, component };
}
/**
* Client-side navigation link component.
* Sets `location.hash` without full page navigation.
*/
export function Link(props) {
const { path, class: cls, children, ...rest } = props || {};
return h('a', {
href: '#' + path,
class: cls || '',
'on:click': (e) => { e.preventDefault(); location.hash = path; },
...rest,
}, children || []);
}