Files
Ekklesia/surfaces/shell/src/module-icons.ts
T
ekklesiaandClaude Opus 5.5 4671622b9d 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]>
2026-10-15 08:21:50 -05:00

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);
}