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,228 @@
|
||||
import {
|
||||
Activity,
|
||||
Archive,
|
||||
Baby,
|
||||
Bell,
|
||||
BookOpen,
|
||||
BookOpenText,
|
||||
Bookmark,
|
||||
Briefcase,
|
||||
Building,
|
||||
Bus,
|
||||
Calendar,
|
||||
CalendarCheck,
|
||||
CalendarDays,
|
||||
Camera,
|
||||
Car,
|
||||
ChartColumn,
|
||||
ChartPie,
|
||||
Church,
|
||||
ClipboardCheck,
|
||||
ClipboardList,
|
||||
Clock,
|
||||
Coffee,
|
||||
Contact,
|
||||
CreditCard,
|
||||
Database,
|
||||
DoorOpen,
|
||||
FileText,
|
||||
Files,
|
||||
Film,
|
||||
Flag,
|
||||
Folder,
|
||||
Gift,
|
||||
Globe,
|
||||
GraduationCap,
|
||||
HandCoins,
|
||||
HandHeart,
|
||||
Handshake,
|
||||
Headphones,
|
||||
Heart,
|
||||
HeartHandshake,
|
||||
House,
|
||||
Image,
|
||||
Images,
|
||||
Inbox,
|
||||
KeyRound,
|
||||
Landmark,
|
||||
LayoutGrid,
|
||||
Library,
|
||||
ListChecks,
|
||||
ListTodo,
|
||||
Mail,
|
||||
Map as MapIcon,
|
||||
MapPin,
|
||||
Megaphone,
|
||||
MessageCircle,
|
||||
MessageSquare,
|
||||
Mic,
|
||||
Monitor,
|
||||
MonitorPlay,
|
||||
Music,
|
||||
Newspaper,
|
||||
Notebook,
|
||||
Package,
|
||||
Palette,
|
||||
PartyPopper,
|
||||
Phone,
|
||||
Presentation,
|
||||
Printer,
|
||||
Projector,
|
||||
Radio,
|
||||
Receipt,
|
||||
School,
|
||||
Send,
|
||||
Shield,
|
||||
ShoppingBag,
|
||||
Sparkles,
|
||||
Star,
|
||||
Sun,
|
||||
TabletSmartphone,
|
||||
Tag,
|
||||
Tent,
|
||||
Ticket,
|
||||
Trophy,
|
||||
Truck,
|
||||
Tv,
|
||||
UserCheck,
|
||||
UserRound,
|
||||
Users,
|
||||
UsersRound,
|
||||
Utensils,
|
||||
Video,
|
||||
Wallet,
|
||||
Wrench,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import type { ReactElement } from "react";
|
||||
import { type DashboardModuleIconName, isDashboardModuleIconName } from "../module-icons.js";
|
||||
import { ShellIcon } from "./shell-icons.js";
|
||||
|
||||
/**
|
||||
* The Lucide components for the icons a module may declare (manifest v9, ADR 0063), by name.
|
||||
*
|
||||
* Each icon is imported by name, so the Dashboard bundles exactly this curated set and never the
|
||||
* whole library. Typed against `DASHBOARD_MODULE_ICON_NAMES`, so the map can neither miss a name
|
||||
* the platform accepts nor carry one it does not; the shell's tests prove each entry is a real Lucide
|
||||
* component.
|
||||
*/
|
||||
export const MODULE_ICON_COMPONENTS: Readonly<Record<DashboardModuleIconName, LucideIcon>> =
|
||||
Object.freeze({
|
||||
activity: Activity,
|
||||
archive: Archive,
|
||||
baby: Baby,
|
||||
bell: Bell,
|
||||
"book-open": BookOpen,
|
||||
"book-open-text": BookOpenText,
|
||||
bookmark: Bookmark,
|
||||
briefcase: Briefcase,
|
||||
building: Building,
|
||||
bus: Bus,
|
||||
calendar: Calendar,
|
||||
"calendar-check": CalendarCheck,
|
||||
"calendar-days": CalendarDays,
|
||||
camera: Camera,
|
||||
car: Car,
|
||||
"chart-column": ChartColumn,
|
||||
"chart-pie": ChartPie,
|
||||
church: Church,
|
||||
"clipboard-check": ClipboardCheck,
|
||||
"clipboard-list": ClipboardList,
|
||||
clock: Clock,
|
||||
coffee: Coffee,
|
||||
contact: Contact,
|
||||
"credit-card": CreditCard,
|
||||
database: Database,
|
||||
"door-open": DoorOpen,
|
||||
"file-text": FileText,
|
||||
files: Files,
|
||||
film: Film,
|
||||
flag: Flag,
|
||||
folder: Folder,
|
||||
gift: Gift,
|
||||
globe: Globe,
|
||||
"graduation-cap": GraduationCap,
|
||||
"hand-coins": HandCoins,
|
||||
"hand-heart": HandHeart,
|
||||
handshake: Handshake,
|
||||
headphones: Headphones,
|
||||
heart: Heart,
|
||||
"heart-handshake": HeartHandshake,
|
||||
house: House,
|
||||
image: Image,
|
||||
images: Images,
|
||||
inbox: Inbox,
|
||||
"key-round": KeyRound,
|
||||
landmark: Landmark,
|
||||
"layout-grid": LayoutGrid,
|
||||
library: Library,
|
||||
"list-checks": ListChecks,
|
||||
"list-todo": ListTodo,
|
||||
mail: Mail,
|
||||
map: MapIcon,
|
||||
"map-pin": MapPin,
|
||||
megaphone: Megaphone,
|
||||
"message-circle": MessageCircle,
|
||||
"message-square": MessageSquare,
|
||||
mic: Mic,
|
||||
monitor: Monitor,
|
||||
"monitor-play": MonitorPlay,
|
||||
music: Music,
|
||||
newspaper: Newspaper,
|
||||
notebook: Notebook,
|
||||
package: Package,
|
||||
palette: Palette,
|
||||
"party-popper": PartyPopper,
|
||||
phone: Phone,
|
||||
presentation: Presentation,
|
||||
printer: Printer,
|
||||
projector: Projector,
|
||||
radio: Radio,
|
||||
receipt: Receipt,
|
||||
school: School,
|
||||
send: Send,
|
||||
shield: Shield,
|
||||
"shopping-bag": ShoppingBag,
|
||||
sparkles: Sparkles,
|
||||
star: Star,
|
||||
sun: Sun,
|
||||
"tablet-smartphone": TabletSmartphone,
|
||||
tag: Tag,
|
||||
tent: Tent,
|
||||
ticket: Ticket,
|
||||
trophy: Trophy,
|
||||
truck: Truck,
|
||||
tv: Tv,
|
||||
"user-check": UserCheck,
|
||||
"user-round": UserRound,
|
||||
users: Users,
|
||||
"users-round": UsersRound,
|
||||
utensils: Utensils,
|
||||
video: Video,
|
||||
wallet: Wallet,
|
||||
wrench: Wrench,
|
||||
});
|
||||
|
||||
/**
|
||||
* A module's icon: the Lucide icon it declared, or the generic module icon when it declared none or
|
||||
* one this Dashboard cannot draw. Decorative, like every navigation icon (ADR 0058): the module's
|
||||
* name is always beside it or in its control's accessible name.
|
||||
*/
|
||||
export function ModuleIcon(props: {
|
||||
readonly icon: string | undefined;
|
||||
readonly size?: number;
|
||||
}): ReactElement {
|
||||
if (!isDashboardModuleIconName(props.icon)) {
|
||||
return <ShellIcon name="module" {...(props.size === undefined ? {} : { size: props.size })} />;
|
||||
}
|
||||
const Icon = MODULE_ICON_COMPONENTS[props.icon];
|
||||
return (
|
||||
<Icon
|
||||
aria-hidden="true"
|
||||
data-icon={`module:${props.icon}`}
|
||||
focusable="false"
|
||||
size={props.size ?? 20}
|
||||
strokeWidth={1.75}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user