LearnNewsExamplesServices
Frontmatter
id17819
titleSplitter supports proxy-free live sibling resizing
stateClosed
labels
enhancementaitestingarchitectureperformancecore
assigneesneo-gpt-emmy
createdAtAug 27, 2026, 9:50 PM
updatedAtAug 28, 2026, 9:27 AM
githubUrlhttps://github.com/neomjs/neo/issues/17819
authorneo-gpt-emmy
commentsCount0
parentIssue13158
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[x] 17820 DockSplitter previews geometry live before semantic commit
closedAtAug 28, 2026, 9:27 AM

Splitter supports proxy-free live sibling resizing

Closed Backlog/active-chunk-18 enhancementaitestingarchitectureperformancecore
neo-gpt-emmy
neo-gpt-emmy commented on Aug 27, 2026, 9:50 PM

Context

Neo.component.Splitter still uses the proxy-and-commit interaction introduced with the original component in 2021: the source splitter fades, a cloned proxy moves, and the chosen sibling receives its new size only after drag:end. The underlying drag and rendering layers have since gained eager zone registration, gesture-wide selection suppression, focused VDOM updates, and buffered heavy widgets.

This leaf introduces an opt-in live-resize contract for the generic component primitive. It does not assume that modern browsers alone make every heavy surface fast; the real Grid/TreeGrid witness decides whether the mode is ready beyond opt-in use.

Live latest-open sweep: checked the latest 20 open issues at 2026-08-27T19:50:45.126Z; no equivalent found. The latest 30 A2A messages contained no overlapping claim. Knowledge Base, Memory Core, live search, and local active/archive/discussion sweeps found the original Splitter tickets and the docking epic, but no proxy-free live-resize leaf.

The Problem

The current implementation has no live-resize path. Neo.draggable.DragZone#dragStart always creates a proxy, and Neo.main.addon.DragDrop#onDragMove moves that proxy in the main thread. Splitter#onDragEnd then performs an asynchronous parent measurement and mutates sibling.style once.

The first implementation exploration proved that a no-proxy DragZone can forward every drag:move to the App Worker, but that is the wrong hot path: it crosses the worker boundary and performs a reactive VDOM update for every pointer frame even though DragDrop already owns main-thread gesture motion.

The old shape also predates newer drag invariants:

  • the DragZone is created inside the first drag:start, after the eager registry needed for exact first-gesture cancellation;
  • layout-item sizing belongs on wrapperStyle, while style can address an inner component root;
  • Escape suppresses the later drag:end, so exact transient-style restoration needs a first-class cancel path;
  • the Splitter does not explicitly destroy its DragZone.

The Architectural Reality

  • src/main/addon/DragDrop.mjs already owns physical pointer-frame work for DragZones.
  • src/main/draggable/Resize.mjs can keep one cohesive gesture-local resize state machine beneath that addon; it is not a second addon or gesture system.
  • Transient DOM geometry belongs to the main thread. Durable layout authority remains the App Worker's wrapperStyle.
  • The main thread must return one frozen {axis, size, targetId} terminal tuple; reactive Splitter config changes during a gesture must not redirect the commit.
  • Merged PR #16797 established that DragZones must register before the first gesture; delayed DragZone construction would reopen that race.
  • Neo.manager.VDomUpdate coalesces updates, but avoiding worker traffic entirely is the stronger live-resize hot path.

The Fix

  1. Add public reactive Neo.component.Splitter#liveResize, defaulting to false for compatibility.
  2. Add Neo.draggable.DragZone#useProxy, defaulting to true, plus an internal main-thread resize descriptor carried by the existing eager zone registration.
  3. Create the Splitter's DragZone eagerly and refresh direction, boundary, cursor, mode, target wrapper, and bounds before each gesture.
  4. Let Neo.main.addon.DragDrop delegate registered live-resize frames to one Neo.main.draggable.Resize controller. Capture transform-immune layout geometry once, update the real outer wrapper synchronously, and send no App-Worker move frames.
  5. On successful end, return one frozen {axis, size, targetId} tuple and commit it once through the target component's wrapperStyle. Deferred mode uses the same terminal math while retaining its proxy presentation.
  6. On Escape, unregister, reset, or destruction, restore the exact inline flex and axis style captured at start. Destroying the Splitter destroys/unregisters its DragZone.
  7. Expose the mode in the existing Splitter example and add real-browser gesture coverage.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback / Edge Case Docs Evidence
New Neo.component.Splitter#liveResize This leaf under parent #13158 true resizes the selected sibling throughout a real drag while the actual splitter follows Flexbox Default false retains proxy movement and one final size commit JSDoc + existing Splitter example Real-browser horizontal/vertical and previous/next gesture matrix
New Neo.draggable.DragZone#useProxy Existing createDragProxy() ownership plus main-thread DragDrop false skips proxy construction without creating a second gesture system Default true; existing DragZone consumers are unchanged JSDoc Unit contract plus rendered proxy-count controls
Main-thread transient resize DragDrop physical gesture ownership + App-Worker wrapperStyle durability Pointer frames mutate only the captured outer DOM wrapper; release emits one frozen terminal tuple Missing/stale target fails closed; Escape restores exact inline style JSDoc on the resize controller Zero App-Worker move messages + rendered held-button geometry
Splitter cancel/destruction lifecycle Eager registry contract from #16797 and current core.Base#destroy semantics First gesture is zone-addressed; Escape restores exact pre-drag sizing; destruction unregisters the zone Cancel commits no resize and leaves no proxy/disabled/dragging residue JSDoc First-gesture Escape and destroy/remount witnesses

Decision Record impact

none — additive behavior within existing generic Splitter, DragZone, and main-thread DragDrop ownership boundaries.

Acceptance Criteria

  • liveResize: false preserves the current proxy lifecycle and commit-on-end behavior.
  • liveResize: true creates no drag proxy, keeps the real splitter visible, and updates the selected sibling continuously through real pointer movement.
  • Live pointer frames remain main-thread-only; the App Worker receives one frozen terminal size/axis/target tuple and performs one durable wrapperStyle update.
  • Horizontal/vertical directions and previous/next resize targets produce correct final geometry.
  • Layout sizing is applied at the target's wrapperStyle boundary, including a component with a distinct wrapper/root.
  • The first drag of a fresh boot carries its DragZone identity; Escape restores the exact captured target style and clears every gesture residue.
  • Destroying the Splitter destroys/unregisters its DragZone.
  • A real-browser heavy Grid and TreeGrid sibling witness proves repeated live resizing causes no VDOM wedge, geometry split-brain, or lost final update.
  • The existing Splitter example exposes the mode and its JSDoc documents compatibility/default semantics.

Out of Scope

  • Changing the default to live resize.
  • Neo.dashboard.DockSplitter and persisted dock documents; that is a dependent leaf.
  • A parallel main-thread addon or gesture system.
  • Reopening the historical sibling min/max request from #4723.
  • Keyboard resizing or broader accessibility redesign.

Avoided Traps

  • Worker-per-frame resizing: it is functionally viable but pays cross-worker and VDOM costs on every pointer frame.
  • Lazy DragZone construction: both modes need first-gesture routing, and delayed construction conflicts with eager zone registration.
  • Moving the source splitter with coordinates: the real splitter should move because Flexbox recomputes after sibling sizing.
  • Mutating style durably: wrapped components require the layout-owned wrapperStyle; direct DOM styles are transient main-thread preview only.
  • Claiming performance by architecture: the buffered-widget browser witness remains the proof.

Related

Parent: #13158
Related: #16758
Related: #16797

Origin Session ID: 38d9509c-f70b-4ece-bab9-d810e86175ea

Retrieval Hint: query_raw_memories("component Splitter main-thread live resize DragDrop useProxy eager registry wrapperStyle")