Add module loader & registry (task 04)
Implements the extensibility foundation: ModuleManifest/DashboardWidget/EntityTypeRegistration types in _core/module.ts, a plain-Map registry singleton with registerModule/getRegistry/getEntityType/getWidget, and stub manifests for calendar/lists/notes. Root layout imports src/modules/index.ts for side-effect registration; AppNav reads the registry to render nav links. /debug/registry dumps the full registry JSON in dev using zod v4 + z.toJSONSchema(). Adding a fourth module requires one folder + one line in index.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
37977568ed
commit
e237fe8449
@@ -0,0 +1,12 @@
|
||||
export type {
|
||||
ModuleManifest,
|
||||
EntityTypeRegistration,
|
||||
DashboardWidget,
|
||||
QuickAddAction,
|
||||
WidgetContext,
|
||||
ShareCapabilities,
|
||||
ReminderCapabilities,
|
||||
SearchAdapter,
|
||||
SearchResult,
|
||||
} from "./module";
|
||||
export { registerModule, getRegistry, getEntityType, getWidget } from "./registry";
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { ZodType } from "zod";
|
||||
|
||||
export type ShareCapabilities = {
|
||||
canShare: boolean;
|
||||
defaultCapabilities?: string[];
|
||||
};
|
||||
|
||||
export type ReminderCapabilities = {
|
||||
canRemind: boolean;
|
||||
};
|
||||
|
||||
export type SearchResult = {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
excerpt?: string;
|
||||
};
|
||||
|
||||
export type SearchAdapter = {
|
||||
search: (query: string, householdId: string) => Promise<SearchResult[]>;
|
||||
};
|
||||
|
||||
export type WidgetContext = {
|
||||
userId: string;
|
||||
householdId: string;
|
||||
};
|
||||
|
||||
export type DashboardWidget = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category?: string;
|
||||
defaultSize: { w: number; h: number };
|
||||
minSize?: { w: number; h: number };
|
||||
maxSize?: { w: number; h: number };
|
||||
defaultPriority: number;
|
||||
configSchema: ZodType;
|
||||
defaultConfig: unknown;
|
||||
resolveConfigOptions?: (ctx: WidgetContext) => Promise<unknown>;
|
||||
render: (props: { config: unknown; ctx: WidgetContext }) => ReactNode;
|
||||
};
|
||||
|
||||
export type QuickAddAction = {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
action: (ctx: WidgetContext) => void | Promise<void>;
|
||||
};
|
||||
|
||||
export type EntityTypeRegistration = {
|
||||
type: string;
|
||||
label: { singular: string; plural: string };
|
||||
share?: ShareCapabilities;
|
||||
reminder?: ReminderCapabilities;
|
||||
search?: SearchAdapter;
|
||||
resolveUrl: (id: string) => string;
|
||||
loadForShare?: (id: string) => Promise<unknown>;
|
||||
};
|
||||
|
||||
export type ModuleManifest = {
|
||||
id: string;
|
||||
name: string;
|
||||
nav?: { href: string; label: string; icon?: string };
|
||||
entities: EntityTypeRegistration[];
|
||||
dashboardWidgets?: DashboardWidget[];
|
||||
quickAdds?: QuickAddAction[];
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ModuleManifest, EntityTypeRegistration, DashboardWidget } from "./module";
|
||||
|
||||
const modules = new Map<string, ModuleManifest>();
|
||||
const entityTypes = new Map<string, EntityTypeRegistration>();
|
||||
const widgets = new Map<string, DashboardWidget>();
|
||||
|
||||
export function registerModule(manifest: ModuleManifest): void {
|
||||
modules.set(manifest.id, manifest);
|
||||
for (const entity of manifest.entities) {
|
||||
entityTypes.set(entity.type, entity);
|
||||
}
|
||||
for (const widget of manifest.dashboardWidgets ?? []) {
|
||||
widgets.set(widget.id, widget);
|
||||
}
|
||||
}
|
||||
|
||||
export function getRegistry() {
|
||||
return Object.freeze({
|
||||
modules: Object.freeze([...modules.values()]),
|
||||
entityTypes: Object.freeze([...entityTypes.values()]),
|
||||
widgets: Object.freeze([...widgets.values()]),
|
||||
});
|
||||
}
|
||||
|
||||
export function getEntityType(type: string): EntityTypeRegistration | undefined {
|
||||
return entityTypes.get(type);
|
||||
}
|
||||
|
||||
export function getWidget(id: string): DashboardWidget | undefined {
|
||||
return widgets.get(id);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ModuleManifest } from "../_core/module";
|
||||
|
||||
const manifest: ModuleManifest = {
|
||||
id: "calendar",
|
||||
name: "Calendar",
|
||||
nav: { href: "/calendar", label: "Calendar", icon: "calendar" },
|
||||
entities: [
|
||||
{
|
||||
type: "calendar.calendar",
|
||||
label: { singular: "Calendar", plural: "Calendars" },
|
||||
resolveUrl: (id) => `/calendar?id=${id}`,
|
||||
},
|
||||
{
|
||||
type: "calendar.event",
|
||||
label: { singular: "Event", plural: "Events" },
|
||||
resolveUrl: (id) => `/calendar/events/${id}`,
|
||||
},
|
||||
],
|
||||
dashboardWidgets: [],
|
||||
quickAdds: [],
|
||||
};
|
||||
|
||||
export default manifest;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { registerModule } from "./_core/registry";
|
||||
import calendarManifest from "./calendar/manifest";
|
||||
import listsManifest from "./lists/manifest";
|
||||
import notesManifest from "./notes/manifest";
|
||||
|
||||
registerModule(calendarManifest);
|
||||
registerModule(listsManifest);
|
||||
registerModule(notesManifest);
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ModuleManifest } from "../_core/module";
|
||||
|
||||
const manifest: ModuleManifest = {
|
||||
id: "lists",
|
||||
name: "Lists",
|
||||
nav: { href: "/lists", label: "Lists", icon: "list" },
|
||||
entities: [
|
||||
{
|
||||
type: "lists.list",
|
||||
label: { singular: "List", plural: "Lists" },
|
||||
resolveUrl: (id) => `/lists/${id}`,
|
||||
},
|
||||
{
|
||||
type: "lists.item",
|
||||
label: { singular: "List item", plural: "List items" },
|
||||
resolveUrl: (id) => `/lists/items/${id}`,
|
||||
},
|
||||
],
|
||||
dashboardWidgets: [],
|
||||
quickAdds: [],
|
||||
};
|
||||
|
||||
export default manifest;
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { ModuleManifest } from "../_core/module";
|
||||
|
||||
const manifest: ModuleManifest = {
|
||||
id: "notes",
|
||||
name: "Notes",
|
||||
nav: { href: "/notes", label: "Notes", icon: "file-text" },
|
||||
entities: [
|
||||
{
|
||||
type: "notes.note",
|
||||
label: { singular: "Note", plural: "Notes" },
|
||||
resolveUrl: (id) => `/notes/${id}`,
|
||||
},
|
||||
],
|
||||
dashboardWidgets: [],
|
||||
quickAdds: [],
|
||||
};
|
||||
|
||||
export default manifest;
|
||||
Reference in New Issue
Block a user