Context
#13247 created the standalone examples/dashboard/dock/ example for the dashboard dock-zone layout system. Slice 1 (#13253, merged via #13255) scaffolded the example + the static dock-zone render: a representative neo.harness.dockZone.v1 document projected once through Neo.dashboard.DockLayoutAdapter.project() into a live container of split / tab zones with splitter affordances. Slice 1 deliberately deferred the resize commit loop — the splitters render, but a drag does nothing.
This slice wires the interactive resize commit loop: a splitter drag commits a resizeSplit operation through Neo.dashboard.DockZoneModel and the layout re-projects from the new committed document — the first runtime exercise of the full model → operation → re-projection cycle in a standalone app.
The Problem
The interactive resize capability already shipped — Neo.dashboard.DockSplitter (#13225) converts a drag into a resizeSplit descriptor and commits it through DockZoneModel.applyOperation (#13160), notifying an owning reducer. But nothing wires that loop end-to-end in a standalone, runnable surface:
DockSplitter.commitResizeSplit supports two modes — an owning-reducer callback (applyDockZoneOperation) and a local-document fallback (dockZoneDocument) — plus an onDockZoneDocumentChange notify. Only the local-document path has unit coverage in DockSplitter.spec; the reducer + notify callbacks the example relies on are untested.
- Without a standalone example, the only runtime consumer of the resize loop is the AgentOS PoC app (
#13012, being replaced) — no isolated surface to see or visually verify a drag → commit → re-render.
The Architectural Reality
Neo.dashboard.DockSplitter.commitResizeSplit (src/dashboard/DockSplitter.mjs:218): if applyDockZoneOperation is a function → calls it as the owning reducer (descriptor, splitter) → {document, errors}; else applies DockZoneModel.applyOperation(dockZoneDocument, descriptor) locally. On success it advances dockZoneDocument and calls onDockZoneDocumentChange(document, descriptor, splitter).
Neo.dashboard.DockLayoutAdapter.project(model, {resolveComponentRef, applyDockZoneOperation, onDockZoneDocumentChange}) threads both callbacks onto every projected splitter via createSplitterAffordance (src/dashboard/DockLayoutAdapter.mjs:221).
examples/dashboard/dock/MainContainer.mjs (Neo.container.Viewport, layout: fit) currently holds a one-shot static projection in items.
- The notify fires synchronously from inside the committing splitter's
onDragEnd → a synchronous re-projection that destroys the splitter mid-handler is a use-after-destroy on the trailing me.fire / me.dragStartState; the re-projection must defer one tick.
The Fix
examples/dashboard/dock/MainContainer.mjs — own the committed document on the instance; project with the owning-reducer callbacks:
applyDockZoneOperation(descriptor) → pure DockZoneModel.applyOperation(this.dockModel, descriptor) (the reducer; single source of truth on the example instance).
onDockZoneDocumentChange(document) → store this.dockModel = document and defer one tick (me.timeout(0), isDestroyed-guarded) before removeAll() + re-projecting add(...) — the view-sync, cleanly separated from the reducer.
- Build
items in construct() (not static config) so each projection can carry the instance-bound callbacks.
test/playwright/unit/dashboard/DockSplitter.spec.mjs — cover the two callbacks the example depends on: the applyDockZoneOperation reducer path (descriptor reaches the reducer; its returned document is adopted + notified) + the onDockZoneDocumentChange notify (fires on a successful commit with the committed document; does not fire on a rejected resize).
Acceptance Criteria
Out of Scope
- The advanced QT-parity interactions — auto-hide/pin, named perspectives, grouped tab-drag — are the capability layer of #13158 (the docking-polish epic) and are not yet built in
src/dashboard/. The example will grow to demonstrate them as those capabilities land (future slices, coordinated with that epic). This slice covers only the already-shipped resize loop.
- No change to
src/dashboard/ resize behavior — this wires + tests the existing capability.
Avoided Traps
- Pointer-handler size mutation. The resize must commit through the semantic
resizeSplit operation on the persisted model, never a pointer handler mutating projected flex/sizes directly — the contract's HarnessDockZoneModel.md §Split Projection and DockSplitter's own design require model-first. The example honors this by re-projecting from the committed document.
- Synchronous re-projection inside the drag handler. Deferred one tick (see Architectural Reality) — otherwise
removeAll() destroys the committing splitter mid-onDragEnd.
Related
- Parent: #13247 (standalone docking example)
- Slice 1: #13253 (static render — merged via #13255)
- Capability delivered by: #13225 (splitter drag-wiring), #13160 (resizeSplit operation) — both merged
- Demonstrates-for: #13158 (QT-parity docking-polish epic; its "interactive splitter resize" gap is already delivered by #13225 / #13160)
- Decision Record impact: none (example + test; no ADR)
Release classification: boardless (standalone example enhancement — non-blocking)
Retrieval Hint: "standalone dock example interactive resize commit loop owning reducer"
Context
#13247 created the standalone
examples/dashboard/dock/example for the dashboard dock-zone layout system. Slice 1 (#13253, merged via #13255) scaffolded the example + the static dock-zone render: a representativeneo.harness.dockZone.v1document projected once throughNeo.dashboard.DockLayoutAdapter.project()into a live container of split / tab zones with splitter affordances. Slice 1 deliberately deferred the resize commit loop — the splitters render, but a drag does nothing.This slice wires the interactive resize commit loop: a splitter drag commits a
resizeSplitoperation throughNeo.dashboard.DockZoneModeland the layout re-projects from the new committed document — the first runtime exercise of the full model → operation → re-projection cycle in a standalone app.The Problem
The interactive resize capability already shipped —
Neo.dashboard.DockSplitter(#13225) converts a drag into aresizeSplitdescriptor and commits it throughDockZoneModel.applyOperation(#13160), notifying an owning reducer. But nothing wires that loop end-to-end in a standalone, runnable surface:DockSplitter.commitResizeSplitsupports two modes — an owning-reducer callback (applyDockZoneOperation) and a local-document fallback (dockZoneDocument) — plus anonDockZoneDocumentChangenotify. Only the local-document path has unit coverage inDockSplitter.spec; the reducer + notify callbacks the example relies on are untested.#13012, being replaced) — no isolated surface to see or visually verify a drag → commit → re-render.The Architectural Reality
Neo.dashboard.DockSplitter.commitResizeSplit(src/dashboard/DockSplitter.mjs:218): ifapplyDockZoneOperationis a function → calls it as the owning reducer(descriptor, splitter) → {document, errors}; else appliesDockZoneModel.applyOperation(dockZoneDocument, descriptor)locally. On success it advancesdockZoneDocumentand callsonDockZoneDocumentChange(document, descriptor, splitter).Neo.dashboard.DockLayoutAdapter.project(model, {resolveComponentRef, applyDockZoneOperation, onDockZoneDocumentChange})threads both callbacks onto every projected splitter viacreateSplitterAffordance(src/dashboard/DockLayoutAdapter.mjs:221).examples/dashboard/dock/MainContainer.mjs(Neo.container.Viewport,layout: fit) currently holds a one-shot static projection initems.onDragEnd→ a synchronous re-projection that destroys the splitter mid-handler is a use-after-destroy on the trailingme.fire/me.dragStartState; the re-projection must defer one tick.The Fix
examples/dashboard/dock/MainContainer.mjs— own the committed document on the instance; project with the owning-reducer callbacks:applyDockZoneOperation(descriptor)→ pureDockZoneModel.applyOperation(this.dockModel, descriptor)(the reducer; single source of truth on the example instance).onDockZoneDocumentChange(document)→ storethis.dockModel = documentand defer one tick (me.timeout(0),isDestroyed-guarded) beforeremoveAll()+ re-projectingadd(...)— the view-sync, cleanly separated from the reducer.itemsinconstruct()(not static config) so each projection can carry the instance-bound callbacks.test/playwright/unit/dashboard/DockSplitter.spec.mjs— cover the two callbacks the example depends on: theapplyDockZoneOperationreducer path (descriptor reaches the reducer; its returned document is adopted + notified) + theonDockZoneDocumentChangenotify (fires on a successful commit with the committed document; does not fire on a rejected resize).Acceptance Criteria
resizeSplitand the layout reflects the new sizes (re-projected from the committed document).applyDockZoneOperation= pureDockZoneModel.applyOperation;onDockZoneDocumentChange= deferred re-projection, destroy-guarded).DockSplitter.speccovers the reducer-callback + notify-callback paths (currently only the local-document path is tested).Out of Scope
src/dashboard/. The example will grow to demonstrate them as those capabilities land (future slices, coordinated with that epic). This slice covers only the already-shipped resize loop.src/dashboard/resize behavior — this wires + tests the existing capability.Avoided Traps
resizeSplitoperation on the persisted model, never a pointer handler mutating projectedflex/sizesdirectly — the contract'sHarnessDockZoneModel.md§Split Projection andDockSplitter's own design require model-first. The example honors this by re-projecting from the committed document.removeAll()destroys the committing splitter mid-onDragEnd.Related
Release classification: boardless (standalone example enhancement — non-blocking)
Retrieval Hint: "standalone dock example interactive resize commit loop owning reducer"