ui:next widgets1/2
This commit is contained in:
parent
5f75ee5bb1
commit
56164fdb31
@ -1,12 +1,19 @@
|
||||
# Widget system (`@polymech/ui-next`)
|
||||
|
||||
This document describes the **scaffold** under [`src/widgets/`](../src/widgets/index.ts): registry, plugins, hooks, and extension slots. It implements a subset of the full contract in [`packages/ui/docs/widgets-api.md`](../../ui/docs/widgets-api.md) (Polymech UI).
|
||||
This document tracks the **implementation status** of the scaffold under
|
||||
[`src/widgets/`](../src/widgets/index.ts) and [`src/widgets-editor/`](../src/widgets-editor/index.ts).
|
||||
It maps every section of the master contract
|
||||
[`packages/ui/docs/widgets-api.md`](../../ui/docs/widgets-api.md) to either
|
||||
**Done**, **Partial**, or a **TODO** item.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
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**. In edit mode the lazy
|
||||
**`EditorShell`** chunk loads on first activation.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
@ -26,9 +33,15 @@ flowchart TB
|
||||
SR[(SlotRegistry)]
|
||||
end
|
||||
|
||||
subgraph store["Zustand store"]
|
||||
LS[(useLayoutStore\npages / tree ops)]
|
||||
end
|
||||
|
||||
subgraph react["React tree"]
|
||||
ES[ExtensionSlot slotId]
|
||||
W[Widget components]
|
||||
NR[NodeRenderer\nview mode]
|
||||
EC[EditorContext\nisEditMode / selectedNodeId]
|
||||
ES2[EditorShell chunk\nEditableNodeRenderer\nNodePropertiesPanel\nEditToggle]
|
||||
end
|
||||
|
||||
PM --> P1
|
||||
@ -38,15 +51,20 @@ flowchart TB
|
||||
API --> WR
|
||||
API --> HR
|
||||
API --> SR
|
||||
WR --> W
|
||||
WR --> NR
|
||||
WR --> ES2
|
||||
SR --> ES
|
||||
LS --> NR
|
||||
LS --> ES2
|
||||
EC -.lazy.-> ES2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Plugin lifecycle
|
||||
|
||||
Registration order matters for **`requires`**: dependencies must already be registered. Higher **`priority`** runs first for ordered hooks (see §3).
|
||||
Registration order matters for **`requires`**: dependencies must already be
|
||||
registered. Higher **`priority`** runs first for ordered hooks.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@ -71,9 +89,10 @@ sequenceDiagram
|
||||
|
||||
---
|
||||
|
||||
## 3. Hook pipeline (conceptual)
|
||||
## 3. Hook pipeline
|
||||
|
||||
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.
|
||||
Hook handlers are stored with **plugin id** and **priority**. When the host
|
||||
calls **`HookRegistry.runSync`**, handlers run in **descending priority**.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
@ -93,86 +112,257 @@ flowchart LR
|
||||
|
||||
## 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`**.
|
||||
|
||||
```mermaid
|
||||
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]
|
||||
```
|
||||
**`PluginAPI.injectSlot(slotId, Component)`** appends a React component to
|
||||
that slot. **`ExtensionSlot`** subscribes to **`SlotRegistry`** via
|
||||
**`useSyncExternalStore`**.
|
||||
|
||||
---
|
||||
|
||||
## 5. Data flow: from plugin to screen
|
||||
## 5. Unified Node model — what's built
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph pluginSide["Plugin"]
|
||||
R["registerWidget(metadata.id)"]
|
||||
I["injectSlot(editor:toolbar:end)"]
|
||||
end
|
||||
The scaffold implements **§14** of the master plan in full. Every layout
|
||||
element is a `LayoutNode`; containers are just nodes with `canHaveChildren:
|
||||
true`.
|
||||
|
||||
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
|
||||
```
|
||||
PageLayout
|
||||
└── root: LayoutNode type: 'flex'
|
||||
├── LayoutNode type: 'flex-row' (layout.columns = 1)
|
||||
│ └── LayoutNode type: 'text-block'
|
||||
└── LayoutNode type: 'flex-row' (layout.columns = 3)
|
||||
├── LayoutNode type: 'text-block'
|
||||
├── LayoutNode type: 'text-block'
|
||||
└── LayoutNode type: 'text-block'
|
||||
```
|
||||
|
||||
The demo page [`WidgetSystemDemoPage`](../src/examples/widgets/WidgetSystemDemoPage.tsx) reads **`widgetRegistry.getAll()`** and mounts **`ExtensionSlot`** for **`editor:toolbar:end`**, plus a direct **`TextBlockWidget`** preview with mock **`BaseWidgetProps`**.
|
||||
Store tree operations: `initPage`, `loadPage`, `savePage`, `removePage`,
|
||||
`addChild`, `removeNode`, `moveNode`, `updateNodeProps`, `updateNodeLayout`.
|
||||
|
||||
`findNode` is exported for external consumers (e.g. `NodePropertiesPanel`).
|
||||
|
||||
---
|
||||
|
||||
## 6. Module map (this package)
|
||||
## 6. Edit-mode architecture (widgets-editor)
|
||||
|
||||
The **`EditorShell`** is the single dynamic-import chunk boundary. Everything
|
||||
in it — DnD, overlays, properties panel, toggle bar — is absent from the
|
||||
view-only bundle.
|
||||
|
||||
```
|
||||
main bundle editor chunk (lazy)
|
||||
────────────────────────── ──────────────────────────────
|
||||
EditorContext EditableNodeRenderer
|
||||
isEditMode @dnd-kit/core + @dnd-kit/sortable
|
||||
selectedNodeId SortableContainer + SortableItem
|
||||
setSelectedNodeId NodeEditorOverlay (drag grip, delete, badge)
|
||||
lazy.tsx NodePropertiesPanel
|
||||
LazyEditableNodeRenderer NodePropertiesForm (field renderer)
|
||||
LazyNodePropertiesPanel EditToggle (mode switch + save)
|
||||
LazyEditToggle
|
||||
|
||||
view path (NodeRenderer) ← never touches editor chunk
|
||||
```
|
||||
|
||||
Build output (vite/rolldown):
|
||||
|
||||
| Chunk | Size gz | Contents |
|
||||
|---|---|---|
|
||||
| `index.js` | ~49 KB | app + widget system + view renderer |
|
||||
| `EditorShell.js` | ~19 KB | all edit code + @dnd-kit |
|
||||
| `TextBlockWidgetEdit.js` | ~0.7 KB | inline edit component per widget |
|
||||
|
||||
---
|
||||
|
||||
## 7. Module map
|
||||
|
||||
| Module | Role |
|
||||
|--------|------|
|
||||
| [`types.ts`](../src/widgets/types.ts) | `WidgetDefinition`, `WidgetPlugin`, `PluginAPI`, `HookName`, `SlotId`, … |
|
||||
| [`types.ts`](../src/widgets/types.ts) | `WidgetDefinition`, `LayoutNode`, `PageLayout`, `NodeLayout`, `ConfigSchema`, … |
|
||||
| [`WidgetRegistry`](../src/widgets/registry/WidgetRegistry.ts) | CRUD for widget definitions |
|
||||
| [`HookRegistry`](../src/widgets/plugins/HookRegistry.ts) | Hook lists + `runSync` |
|
||||
| [`SlotRegistry`](../src/widgets/plugins/SlotRegistry.ts) | Slot entries + `subscribe` |
|
||||
| [`createPluginAPI`](../src/widgets/plugins/createPluginAPI.ts) | Facade passed into `plugin.setup` |
|
||||
| [`PluginManager`](../src/widgets/plugins/PluginManager.ts) | `register` / `unregister` |
|
||||
| [`system.ts`](../src/widgets/system.ts) | Process-wide singletons for the POC |
|
||||
| [`system.ts`](../src/widgets/system.ts) | Process-wide singletons |
|
||||
| [`ExtensionSlot`](../src/widgets/slots/ExtensionSlot.tsx) | Renders slot content |
|
||||
| [`NodeRenderer`](../src/widgets/renderer/NodeRenderer.tsx) | Recursive view renderer |
|
||||
| [`boot.ts`](../src/widgets/bootstrap/boot.ts) | `bootWidgetSystem()` |
|
||||
| [`corePlugin.tsx`](../src/widgets/bootstrap/corePlugin.tsx) | `flex`, `flex-row`, `text-block` registrations |
|
||||
| [`useLayoutStore`](../src/store/useLayoutStore.ts) | Zustand store — pages + tree ops |
|
||||
| [`PagePersistence`](../src/store/persistence/PagePersistence.ts) | Persistence interface |
|
||||
| [`LocalStoragePagePersistence`](../src/store/persistence/LocalStoragePagePersistence.ts) | localStorage impl |
|
||||
| [`EditorContext`](../src/widgets-editor/context/EditorContext.tsx) | `isEditMode`, `selectedNodeId` (main bundle) |
|
||||
| [`EditorShell`](../src/widgets-editor/EditorShell.tsx) | Lazy chunk boundary |
|
||||
| [`lazy.tsx`](../src/widgets-editor/lazy.tsx) | Thin wrappers — `LazyEditableNodeRenderer` etc. |
|
||||
| [`EditableNodeRenderer`](../src/widgets-editor/EditableNodeRenderer.tsx) | Edit canvas — DnD + overlays |
|
||||
| [`NodePropertiesPanel`](../src/widgets-editor/components/NodePropertiesPanel.tsx) | Side panel |
|
||||
| [`NodePropertiesForm`](../src/widgets-editor/components/NodePropertiesForm.tsx) | configSchema field renderer |
|
||||
| [`NodeEditorOverlay`](../src/widgets-editor/components/NodeEditorOverlay.tsx) | Per-node editing chrome |
|
||||
| [`EditToggle`](../src/widgets-editor/components/EditToggle.tsx) | Mode toggle + save |
|
||||
| [`TextBlockWidgetEdit`](../src/widgets/widgets/sample/TextBlockWidgetEdit.tsx) | Lazy inline edit for text-block |
|
||||
|
||||
---
|
||||
|
||||
## 7. Relationship to the full widgets API
|
||||
## 8. Status vs master plan
|
||||
|
||||
| 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`**.
|
||||
| Master plan section | Status | Notes |
|
||||
|---------------------|--------|-------|
|
||||
| §1 `WidgetDefinition` — `component`, `editComponent`, `metadata`, `validate` | ✅ Done | `editComponent` lazy-loaded via `React.lazy` |
|
||||
| §1 `onInit` / `onBeforeSave` / `onDestroy` lifecycle hooks | ❌ TODO | |
|
||||
| §1 `translatableProps` / `extractTranslatableContent` | ❌ TODO | |
|
||||
| §1 `renderStatic` (HTML/email/PDF export) | ❌ TODO | |
|
||||
| §1 `getNestedLayouts` / `nestedLayoutStrategy` | 🚫 N/A | Superseded by §14 unified tree |
|
||||
| §2 `BaseWidgetProps` full contract | ✅ Done | All fields defined in `types.ts` |
|
||||
| §3 `ConfigSchema` — text, number, boolean, select, color, markdown | ✅ Done | `NodePropertiesForm` renders these |
|
||||
| §3 `ConfigField.showWhen` / `disableWhen` conditional visibility | ❌ TODO | |
|
||||
| §3 `ConfigField.required` / `pattern` / `customValidate` validation | ❌ TODO | |
|
||||
| §3 field types: `range`, `classname`, `json`, `component` (custom editor) | ❌ TODO | |
|
||||
| §3 field type: `selectWithText` | ❌ TODO | |
|
||||
| §4 `WidgetInstance` | ✅ Done | Replaced by `LayoutNode` in Node model |
|
||||
| §5 Container types | ✅ Done | Replaced by `flex` / `flex-row` Node types |
|
||||
| §5 `ContainerSettings.visibleWhen` | ❌ TODO | |
|
||||
| §5 `ContainerSettings.background` / `padding` / `maxWidth` | ❌ TODO | |
|
||||
| §6 `PageLayout` with `root: Node` | ✅ Done | |
|
||||
| §6 `PageLayout.meta` (description, thumbnail, locked) | ❌ TODO | |
|
||||
| §7 `NestedLayoutManager` | 🚫 N/A | Not needed — tree is the layout |
|
||||
| §8 `WidgetLifecycleContext` | ❌ TODO | Needed for `onInit` / `onDestroy` |
|
||||
| §9 Container rendering contract | ✅ Done | Replaced by `NodeRenderer` |
|
||||
| §10 Layout context — page ops, widget ops | ✅ Done | Zustand store covers these |
|
||||
| §10 `nestedLayouts` on context | 🚫 N/A | |
|
||||
| §11 `ExportContext` / `renderStatic` | ❌ TODO | |
|
||||
| §12 Zustand store for layout state | ✅ Done | `useLayoutStore` — pages, tree ops |
|
||||
| §12 `isEditMode` / `selectedWidgetId` in Zustand (not React context) | ❌ TODO | Currently in `EditorContext` |
|
||||
| §13 Plugin system — `registerWidget`, `addHook`, `injectSlot` | ✅ Done | |
|
||||
| §13 `modifyWidget`, `wrapWidget`, `extendConfig` | ⚠️ Partial | Declared in `PluginAPI`; impl needs verification |
|
||||
| §13 `registerContainerType`, `extendContainerSettings` | ❌ TODO | Not in current `PluginAPI` |
|
||||
| §13 Hook pipeline wired for real hooks (not just `editor:widgetPalette`) | ❌ TODO | Registry exists; callers not wired |
|
||||
| §14 Unified Node model — `LayoutNode`, tree ops, `NodeRenderer` | ✅ Done | Core of the scaffold |
|
||||
| §14 `NodeLayout.rows` (RowDef[] for per-row column control) | ❌ TODO | Currently `columns` only |
|
||||
| §15 Level 1 — pre-registered widgets | ✅ Done | |
|
||||
| §15 Level 2 — lazy-imported (`editComponent`) | ✅ Done | |
|
||||
| §15 Level 3 — plugin-registered (npm) | ✅ Done | |
|
||||
| §15 Level 4 — remote components (`__remote` node type) | ❌ TODO | |
|
||||
|
||||
---
|
||||
|
||||
## 8. Try it
|
||||
## 9. TODO list
|
||||
|
||||
1. Run the app (`npm run dev` in `packages/ui-next`).
|
||||
2. Open **`/examples/widgets-system`** (or **Migration examples → widgets + plugins**).
|
||||
3. Confirm the toolbar slot pill and the **`text-block`** registry entry after **`bootWidgetSystem()`** runs.
|
||||
TODOs are grouped by theme. Items marked **[editor]** require `EditorShell`
|
||||
changes; items marked **[core]** live in the main bundle.
|
||||
|
||||
### 9.1 Add-widget / palette
|
||||
|
||||
- **[editor]** Widget palette UI — list all `widgetRegistry.getAll()` entries by
|
||||
category; drag from palette → drop onto a container node.
|
||||
- **[editor]** `editor:widgetPalette` hook wired — filter palette list via plugin
|
||||
hook before rendering it.
|
||||
- **[editor]** `addChild` action exposed in `EditableNodeRenderer` — clicking a
|
||||
palette item calls `useLayoutStore().addChild(parentId, newNode)`.
|
||||
- **[editor]** Empty-container drop zone — render `container:empty` slot content
|
||||
when a `flex-row` or `flex` has no children (shows the palette or a prompt).
|
||||
|
||||
### 9.2 Editor UX
|
||||
|
||||
- **[editor]** Undo / redo — snapshot the page tree on every mutation; expose
|
||||
`undo()` / `redo()` in the store or editor context.
|
||||
- **[editor]** Keyboard shortcuts — `Delete` to remove selected node, `Escape` to
|
||||
deselect, `Ctrl+Z` / `Ctrl+Shift+Z` for undo/redo.
|
||||
- **[editor]** Right-click context menu on `NodeEditorOverlay` — hook
|
||||
`editor:contextMenu` to allow plugins to inject items.
|
||||
- **[editor]** Multi-select — shift-click to select multiple nodes; bulk delete /
|
||||
move.
|
||||
- **[editor]** Node breadcrumb trail — show the ancestor path of the selected node
|
||||
(`flex > flex-row > text-block`).
|
||||
|
||||
### 9.3 ConfigSchema / NodePropertiesForm
|
||||
|
||||
- **[editor]** `showWhen` / `disableWhen` — evaluate condition before rendering a
|
||||
field (see `ConfigField.showWhen` in master plan §3).
|
||||
- **[editor]** Field validation — `required`, `pattern`, `customValidate`; show
|
||||
error messages inline below each field.
|
||||
- **[editor]** New field types:
|
||||
- `range` — slider with min/max/step
|
||||
- `selectWithText` — preset select + freeform fallback input
|
||||
- `classname` — Tailwind class picker (port from `ui`)
|
||||
- `json` — code editor (Monaco or textarea) for raw JSON props
|
||||
- `component` — escape hatch: register a custom React component as the editor for
|
||||
a field
|
||||
|
||||
### 9.4 Widget lifecycle hooks
|
||||
|
||||
- **[core]** Define `WidgetLifecycleContext` type (§8 of master plan).
|
||||
- **[core]** Call `def.onInit(ctx)` in the store's `addChild` action when a node
|
||||
is first inserted; merge returned props into `node.props`.
|
||||
- **[core]** Call `def.onBeforeSave(props, ctx)` in `savePage` before persisting.
|
||||
- **[core]** Call `def.onDestroy(props, ctx)` in `removeNode`.
|
||||
|
||||
### 9.5 Plugin API gaps
|
||||
|
||||
- **[core]** `registerContainerType(type, renderer)` — allow plugins to define new
|
||||
structural node types with their own view + edit renderers (§13 Plugin API).
|
||||
- **[core]** `extendContainerSettings(type, fields)` — add `configSchema` fields
|
||||
to an existing node type from a plugin.
|
||||
- **[core]** Wire the full hook pipeline: `widget:beforeRender`,
|
||||
`widget:afterRender`, `widget:beforeAdd`, `widget:afterAdd`,
|
||||
`widget:beforeRemove`, `layout:beforeSave`, `layout:afterSave` — add
|
||||
`runSync` / `runAsync` call-sites in the store and renderer.
|
||||
- **[core]** Verify / fix `modifyWidget` and `wrapWidget` — write a unit test that
|
||||
registers a wrapper HOC and confirms the wrapped component renders.
|
||||
|
||||
### 9.6 State management consolidation
|
||||
|
||||
- **[core]** Move `isEditMode` and `selectedNodeId` from `EditorContext` into
|
||||
`useLayoutStore` (§12 recommendation). `EditorContext` becomes a thin shim
|
||||
that reads from the store, or is removed entirely.
|
||||
- **[core]** Add `subscribeWithSelector` middleware to `useLayoutStore` so
|
||||
consumers can subscribe to narrow slices without re-rendering on unrelated
|
||||
mutations.
|
||||
|
||||
### 9.7 Node layout model
|
||||
|
||||
- **[core]** Add `NodeLayout.rows: RowDef[]` support alongside `columns` —
|
||||
enables per-row independent column widths (`1fr 2fr`) rather than the
|
||||
current uniform `repeat(N, 1fr)`.
|
||||
- **[core]** `ContainerSettings.visibleWhen` — evaluate a `VisibilityCondition`
|
||||
rule (e.g. `'role:admin'`) and hide the node in view mode when not met.
|
||||
- **[core]** `ContainerSettings.background` / `padding` / `maxWidth` — surface
|
||||
these as `configSchema` fields on `flex` and `flex-row` node types.
|
||||
|
||||
### 9.8 Persistence
|
||||
|
||||
- **[core]** Backend persistence adapter — replace `LocalStoragePagePersistence`
|
||||
with a `RestApiPagePersistence` that calls a real API; the `PagePersistence`
|
||||
interface is already defined, only the implementation changes.
|
||||
- **[core]** Conflict resolution / optimistic locking — compare `updatedAt` on
|
||||
save; show a merge conflict UI if the server version is newer.
|
||||
- **[core]** `PageLayout.meta` — add `description`, `thumbnail`, `locked` to the
|
||||
stored layout and expose `locked` in the editor (disable edit mode when
|
||||
`locked: true`).
|
||||
|
||||
### 9.9 Remote components (§15 Level 4)
|
||||
|
||||
- **[core]** Register the `__remote` meta-widget that dynamic-imports a component
|
||||
from a URL at runtime.
|
||||
- **[core]** `ErrorBoundary` wrapper inside `RemoteComponent` so a crashing remote
|
||||
doesn't take down the page.
|
||||
- **[core]** Permission gate — hook `editor:widgetPalette` to hide `__remote` from
|
||||
non-admin users; hook `widget:beforeRender` to validate the URL against an
|
||||
allowlist.
|
||||
|
||||
### 9.10 Export / i18n
|
||||
|
||||
- **[core]** `renderStatic(props, ExportContext): string` — add to
|
||||
`WidgetDefinition`; implement on `text-block` as a baseline; call from a
|
||||
new `exportPage(pageId, format)` utility.
|
||||
- **[core]** `translatableProps` on `WidgetDefinition` — used by an export pipeline
|
||||
or i18n plugin to extract strings for translation.
|
||||
|
||||
---
|
||||
|
||||
## 10. Try it
|
||||
|
||||
1. `npm run dev` in `packages/ui-next`.
|
||||
2. **`/examples/widgets-system`** — plugin bootstrap, slot injection, registry
|
||||
inspection.
|
||||
3. **`/examples/layout`** — unified Node model, `EditableNodeRenderer`,
|
||||
drag-reorder, properties panel, lazy `EditorShell` chunk.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user