diff options
| author | Adam Malczewski <[email protected]> | 2026-06-10 16:29:01 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-10 16:29:01 +0900 |
| commit | 871957b930203c019e631c4606cfdf8266d222fa (patch) | |
| tree | 50c522018c3ce4127ffa76f4b3b6c7843e90db43 /src/features/surface-host/logic/plan.ts | |
| parent | 7b345f132763fa6405ae858b74e46229629c19d9 (diff) | |
| download | dispatch-web-871957b930203c019e631c4606cfdf8266d222fa.tar.gz dispatch-web-871957b930203c019e631c4606cfdf8266d222fa.zip | |
feat(views,surface-host): Extensions sidebar view — auto-expanded surfaces + tables
views (new feature):
- pure panel-stack reducer + thin generic ViewSidebar (dropdown picker + add/remove),
switches on view KIND, never a surface id
Extensions view (composition root):
- folds frontend modules + backend surfaces into one "Extensions" view
- frontend module list AGGREGATED from each feature's public `manifest` export
(can't drift); no per-module version (FE features are internal to dispatch-web)
- surfaces are AUTO-SUBSCRIBED on catalog + rendered expanded (no catalog buttons)
surface-host:
- consecutive `stat` fields coalesce into one aligned label/value table (StatTable)
- generic custom-field renderer: dispatch on rendererId === "table" → SurfaceTable
(pure parseTablePayload), so a backend `custom`/table field renders generically
- shared presentational components/Table.svelte (used by both, neither feature
depends on the other)
store:
- auto-subscribe every catalog entry, unsubscribe vanished ones, re-subscribe all
on reconnect; expose all received specs via `surfaces` (drops single-selection)
backend-handoff: CR-1 — emit Loaded Extensions as a custom/table field; notes
what's already covered FE-side (renderer shipped, stat-table fallback works).
Diffstat (limited to 'src/features/surface-host/logic/plan.ts')
| -rw-r--r-- | src/features/surface-host/logic/plan.ts | 43 |
1 files changed, 39 insertions, 4 deletions
diff --git a/src/features/surface-host/logic/plan.ts b/src/features/surface-host/logic/plan.ts index 5b4530b..769f9f9 100644 --- a/src/features/surface-host/logic/plan.ts +++ b/src/features/surface-host/logic/plan.ts @@ -1,12 +1,14 @@ import type { InvokeMessage, SurfaceSpec } from "@dispatch/ui-contract"; -import type { FieldView, SurfaceRenderPlan } from "./types"; +import type { FieldView, RenderGroup, StatFieldView, SurfaceRenderPlan } from "./types"; -const KNOWN_KINDS = new Set(["toggle", "progress", "selector", "stat", "button"]); +const KNOWN_KINDS = new Set(["toggle", "progress", "selector", "stat", "button", "custom"]); /** * Validate and normalise a SurfaceSpec into a renderable plan. - * Keeps known field kinds in order; drops unknown kinds and `custom` fields - * (no renderer registry yet — graceful skip, never throw). + * Keeps known field kinds in order (including `custom`, carried through verbatim + * for the renderer to dispatch on `rendererId`); drops unknown kinds — graceful + * skip, never throw. Whether a `custom` field actually renders is a RENDER-time + * decision (unknown `rendererId` → skipped there), not a planning one. */ export function planSurface(spec: SurfaceSpec): SurfaceRenderPlan { const fields: FieldView[] = []; @@ -51,12 +53,45 @@ export function planSurface(spec: SurfaceSpec): SurfaceRenderPlan { action: field.action, }); break; + case "custom": + fields.push({ + kind: "custom", + rendererId: field.rendererId, + payload: field.payload, + }); + break; } } return { fields }; } /** + * Coalesce a field list into render groups: maximal runs of consecutive `stat` + * fields become one `stats` group (rendered as a single aligned table), every + * other field stays a standalone `field` group. Order is preserved. Pure. + */ +export function groupRenderFields(fields: readonly FieldView[]): RenderGroup[] { + const groups: RenderGroup[] = []; + let run: StatFieldView[] = []; + const flush = (): void => { + if (run.length > 0) { + groups.push({ type: "stats", stats: run }); + run = []; + } + }; + for (const field of fields) { + if (field.kind === "stat") { + run.push(field); + } else { + flush(); + groups.push({ type: "field", field }); + } + } + flush(); + return groups; +} + +/** * Construct a typed `invoke` client message for an actionable field. * For toggle the payload is the new boolean; for selector the chosen value; * for button the payload is omitted. |
