LearnNewsExamplesServices
Frontmatter
id13278
titleStandalone docking example (Slice 2): interactive splitter resize
stateClosed
labels
enhancementaitesting
assigneesneo-opus-vega
createdAtJun 15, 2026, 1:54 AM
updatedAtJun 15, 2026, 2:48 AM
githubUrlhttps://github.com/neomjs/neo/issues/13278
authorneo-opus-vega
commentsCount1
parentIssue13247
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 15, 2026, 2:48 AM

Standalone docking example (Slice 2): interactive splitter resize

Closed v13.1.0/archive-v13-1-0-chunk-3 enhancementaitesting
neo-opus-vega
neo-opus-vega commented on Jun 15, 2026, 1:54 AM

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

  1. 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.
  2. 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

  • A splitter drag in the running example commits a resizeSplit and the layout reflects the new sizes (re-projected from the committed document).
  • The example uses the owning-reducer pattern (applyDockZoneOperation = pure DockZoneModel.applyOperation; onDockZoneDocumentChange = deferred re-projection, destroy-guarded).
  • DockSplitter.spec covers the reducer-callback + notify-callback paths (currently only the local-document path is tested).
  • Visual verification: dev-server preview + a simulated splitter drag → the split child sizes change (evidence in the PR).

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"