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]>
116 lines
2.3 KiB
TypeScript
116 lines
2.3 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|