5.5 KiB
Widget system (@polymech/ui-next)
This document describes the scaffold under src/widgets/: registry, plugins, hooks, and extension slots. It implements a subset of the full contract in packages/ui/docs/widgets-api.md (Polymech UI).
1. High-level architecture
Plugins run at bootstrap, receive a capability-limited PluginAPI, and register widgets, hooks, and slot components. The host UI renders ExtensionSlot at fixed slot IDs; injected components appear there without the core editor importing plugin packages directly.
flowchart TB
subgraph bootstrap["Bootstrap"]
Boot([bootWidgetSystem])
Boot --> PM[PluginManager.register]
end
subgraph plugins["Plugins"]
P1[coreWidgetsPlugin]
P2[demoToolbarPlugin]
end
subgraph registries["Registries"]
WR[(WidgetRegistry)]
HR[(HookRegistry)]
SR[(SlotRegistry)]
end
subgraph react["React tree"]
ES[ExtensionSlot slotId]
W[Widget components]
end
PM --> P1
PM --> P2
P1 --> API[PluginAPI]
P2 --> API
API --> WR
API --> HR
API --> SR
WR --> W
SR --> ES
2. Plugin lifecycle
Registration order matters for requires: dependencies must already be registered. Higher priority runs first for ordered hooks (see §3).
sequenceDiagram
participant App
participant PM as PluginManager
participant API as PluginAPI
participant Plugin
App->>PM: register(plugin)
PM->>PM: validate requires deps
PM->>API: createPluginAPI(scoped to plugin id)
PM->>Plugin: setup(api)
Plugin->>API: registerWidget / addHook / injectSlot
API-->>Plugin: void
PM->>PM: plugins.set(id, plugin)
Note over App,Plugin: unregister(pluginId)
App->>PM: unregister(id)
PM->>Plugin: teardown()
PM->>PM: removeHooksForPlugin, clearSlot entries
3. Hook pipeline (conceptual)
Hook handlers are stored with plugin id and priority. When the host calls HookRegistry.runSync, handlers run in descending priority (see widgets-api §13). Not every HookName is wired in the scaffold yet; the registry is ready for editor/layout integration.
flowchart LR
subgraph pipeline["editor:widgetPalette (example)"]
H1["Handler priority 100"]
H2["Handler priority 10"]
H3["Handler priority 0"]
end
InputIn["widgets[] in"] --> H1
H1 --> H2
H2 --> H3
H3 --> OutputOut["widgets[] out"]
4. Extension slots
PluginAPI.injectSlot(slotId, Component) appends a React component to that slot. ExtensionSlot subscribes to SlotRegistry via useSyncExternalStore and re-renders when injections change. Multiple plugins (or multiple calls from one plugin) can add entries; each entry gets a stable entryId.
flowchart TB
subgraph chrome["Editor chrome (conceptual)"]
TBStart["editor:toolbar:start"]
TBEnd["editor:toolbar:end"]
Side["editor:sidebar:top / …"]
end
Plugin["Plugin setup"]
Plugin --> Inject[injectSlot]
Inject --> SR[(SlotRegistry)]
SR --> ES[ExtensionSlot]
ES --> DOM[Rendered React tree]
5. Data flow: from plugin to screen
flowchart LR
subgraph pluginSide["Plugin"]
R["registerWidget(metadata.id)"]
I["injectSlot(editor:toolbar:end)"]
end
subgraph state["State"]
WR[(WidgetRegistry)]
SR[(SlotRegistry)]
end
subgraph ui["UI"]
Palette["Future: palette reads registry"]
Slot[ExtensionSlot]
end
R --> WR
I --> SR
WR -.-> Palette
SR --> Slot
The demo page WidgetSystemDemoPage reads widgetRegistry.getAll() and mounts ExtensionSlot for editor:toolbar:end, plus a direct TextBlockWidget preview with mock BaseWidgetProps.
6. Module map (this package)
| Module | Role |
|---|---|
types.ts |
WidgetDefinition, WidgetPlugin, PluginAPI, HookName, SlotId, … |
WidgetRegistry |
CRUD for widget definitions |
HookRegistry |
Hook lists + runSync |
SlotRegistry |
Slot entries + subscribe |
createPluginAPI |
Facade passed into plugin.setup |
PluginManager |
register / unregister |
system.ts |
Process-wide singletons for the POC |
ExtensionSlot |
Renders slot content |
boot.ts |
bootWidgetSystem() |
7. Relationship to the full widgets API
| Full doc topic | Status in this scaffold |
|---|---|
BaseWidgetProps, metadata, configSchema |
Types + sample text-block |
PluginAPI (widgets, hooks, slots) |
Implemented (minimal getStore / subscribe) |
Container types, registerContainerType |
Not implemented — extend PluginAPI when needed |
Real LayoutStore (Zustand) |
Stub layoutStore object |
| Nested layouts, export, i18n | Not implemented |
For the authoritative interface proposal, keep using packages/ui/docs/widgets-api.md.
8. Try it
- Run the app (
npm run devinpackages/ui-next). - Open
/examples/widgets-system(or Migration examples → widgets + plugins). - Confirm the toolbar slot pill and the
text-blockregistry entry afterbootWidgetSystem()runs.