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]>
229 lines
4.7 KiB
TypeScript
229 lines
4.7 KiB
TypeScript
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}
|
|
/>
|
|
);
|
|
}
|