Add the module navigation contract: manifest v9, module routes, return-to-origin
Stage 1 PR 7 (ADR 0063). A module becomes a first-class part of the Dashboard. - Manifest v9 = v8 plus optional `navigation`: a Lucide icon from the curated MODULE_NAVIGATION_ICONS set, and up to 12 primary pages (stable id, safe label, kebab-case path relative to the module root; no traversal, schemes, queries, duplicates, or nesting). Validated before activation. - Version 8 stays accepted beside version 9, each read strictly as itself (discriminated union); no released manifest is rewritten. The template moves to v9; the released Signage 0.1.5 manifest is a test fixture. - Real module routes: /<id> is the landing view, /<id>/<page> a declared page, anything beneath a page a module-owned detail route; anything else is not-found and mounts nothing. api/assets/display are never pages. - Sidebar: declared icons, pages nested under an open module, one aria-current, collapsed and drawer behaviour; v8 modules unchanged. - module-ui-sdk: ModuleUiHostContext.navigation (location, subscribe, navigate, link, settings), refusing anything outside the module's routes; undefined on the display host. Mount ABI unchanged. - Return-to-origin: one `?return=` carrier on Settings URLs, validated by parseReturnTarget and against running modules' declared routes; Settings Back returns to exactly the originating module page, across refreshes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7447381c8f
commit
4671622b9d
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* The Lucide icons the Dashboard can draw for a module (manifest v9, ADR 0063), by Lucide name.
|
||||
*
|
||||
* The same curated set `@ekklesia/contracts` validates manifests against (`MODULE_NAVIGATION_ICONS`).
|
||||
* A surface may not import that package — its type graph is the server's (ADR 0024) — so the list is
|
||||
* repeated here, and `apps/api` tests hold the two equal: the platform cannot accept an icon the
|
||||
* Dashboard cannot draw. The browser's map from these names to Lucide components is
|
||||
* `browser/module-icon.tsx`, which imports exactly these icons and no others.
|
||||
*
|
||||
* Framework-free and exported on its own subpath, so the server's parity test reads the list without
|
||||
* importing React.
|
||||
*/
|
||||
export const DASHBOARD_MODULE_ICON_NAMES = Object.freeze([
|
||||
"activity",
|
||||
"archive",
|
||||
"baby",
|
||||
"bell",
|
||||
"book-open",
|
||||
"book-open-text",
|
||||
"bookmark",
|
||||
"briefcase",
|
||||
"building",
|
||||
"bus",
|
||||
"calendar",
|
||||
"calendar-check",
|
||||
"calendar-days",
|
||||
"camera",
|
||||
"car",
|
||||
"chart-column",
|
||||
"chart-pie",
|
||||
"church",
|
||||
"clipboard-check",
|
||||
"clipboard-list",
|
||||
"clock",
|
||||
"coffee",
|
||||
"contact",
|
||||
"credit-card",
|
||||
"database",
|
||||
"door-open",
|
||||
"file-text",
|
||||
"files",
|
||||
"film",
|
||||
"flag",
|
||||
"folder",
|
||||
"gift",
|
||||
"globe",
|
||||
"graduation-cap",
|
||||
"hand-coins",
|
||||
"hand-heart",
|
||||
"handshake",
|
||||
"headphones",
|
||||
"heart",
|
||||
"heart-handshake",
|
||||
"house",
|
||||
"image",
|
||||
"images",
|
||||
"inbox",
|
||||
"key-round",
|
||||
"landmark",
|
||||
"layout-grid",
|
||||
"library",
|
||||
"list-checks",
|
||||
"list-todo",
|
||||
"mail",
|
||||
"map",
|
||||
"map-pin",
|
||||
"megaphone",
|
||||
"message-circle",
|
||||
"message-square",
|
||||
"mic",
|
||||
"monitor",
|
||||
"monitor-play",
|
||||
"music",
|
||||
"newspaper",
|
||||
"notebook",
|
||||
"package",
|
||||
"palette",
|
||||
"party-popper",
|
||||
"phone",
|
||||
"presentation",
|
||||
"printer",
|
||||
"projector",
|
||||
"radio",
|
||||
"receipt",
|
||||
"school",
|
||||
"send",
|
||||
"shield",
|
||||
"shopping-bag",
|
||||
"sparkles",
|
||||
"star",
|
||||
"sun",
|
||||
"tablet-smartphone",
|
||||
"tag",
|
||||
"tent",
|
||||
"ticket",
|
||||
"trophy",
|
||||
"truck",
|
||||
"tv",
|
||||
"user-check",
|
||||
"user-round",
|
||||
"users",
|
||||
"users-round",
|
||||
"utensils",
|
||||
"video",
|
||||
"wallet",
|
||||
"wrench",
|
||||
] as const);
|
||||
|
||||
export type DashboardModuleIconName = (typeof DASHBOARD_MODULE_ICON_NAMES)[number];
|
||||
|
||||
const iconNames: ReadonlySet<string> = new Set(DASHBOARD_MODULE_ICON_NAMES);
|
||||
|
||||
export function isDashboardModuleIconName(value: unknown): value is DashboardModuleIconName {
|
||||
return typeof value === "string" && iconNames.has(value);
|
||||
}
|
||||
Reference in New Issue
Block a user