LearnNewsExamplesServices
Frontmatter
id17928
titleDock layouts have no maximize — a first-class pane maximize for tabs nodes
stateClosed
labels
enhancementdesignaitestingarchitecturecore
assigneesneo-opus-vega
createdAtAug 31, 2026, 10:50 AM
updatedAtAug 31, 2026, 7:14 PM
githubUrlhttps://github.com/neomjs/neo/issues/17928
authorneo-opus-vega
commentsCount4
parentIssue13158
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[x] 17949 Engine-owned lock header action protects a dock pane, [x] 17948 Engine-owned reload header action with a per-pane delegation contract, [x] 17947 Engine-owned pop-out header action enters the tear-out lifecycle
closedAtAug 31, 2026, 7:14 PM

Dock layouts have no maximize — a first-class pane maximize for tabs nodes

Closed v13.2.0/archive-v13-2-0-chunk-17 enhancementdesignaitestingarchitecturecore
neo-opus-vega
neo-opus-vega commented on Aug 31, 2026, 10:50 AM

The gap

Qt-parity dock UIs ship a maximize affordance on every dock pane; src/dashboard/dock has none — grep -rn maximize src/dashboard/ yields one hit, and it is the assignment-math wording in TopologyReconciler, not a feature. Consumers can hand-roll an overlay through the host header-action seam, but each one re-derives the same design decisions (state, restore, animation, keyboard). The dock should own it, the way it owns close behind enableDockCloseAction.

Design sketch (the pieces already exist)

  • Affordance: an engine-owned header action behind enableDockMaximizeAction, rendered by projection.LayoutAdapter next to close and owned by dashboard.dock.Workspace#onDockHeaderAction rather than re-emitted (the close precedent, including the reserved-name guard the adapter already throws for). Icon pair + toggle semantics as in dialog.Base (maximizeCls/minimizeCls).
  • Mechanism — presentation, not topology: maximize the tabs node in place via a class toggle (neo-dock-maximized) plus a MEASURED rect: position: fixed with top/left/width/height set to the workspace root's viewport rect at toggle time — never inset: 0, which answers to the viewport or an incidental fixed containing block instead of the workspace. The class carries everything static (z-index, transitions); the four rect values are per-instance inline style. The header stays visible carrying the restore button.
  • Hard constraint — no DOM re-parenting: a maximized pane must keep DOM identity. Panes hosting an iframe reload their browsing context on re-parent (losing embedded-app state); canvas and video lose state the same way. This rules out floating-container/DOM-move implementations and rules out expressing maximize as a committed topology mutation the reconciler could satisfy by rebuilding nodes.
  • State: a workspace-local transient (public maximizedNodeId, defined as maximizedNodeId_ in static config), deliberately not part of the committed dock document — maximize is presentation. Perspectives, persistence, and topology diffs never see it. Re-projection is deterministic: after any re-projection, the workspace re-applies the maximize presentation iff maximizedNodeId_ still resolves to a projected tabs node, and clears the transient otherwise (the node was closed/merged away) — never a third outcome, never baked into the model.
  • Motion: FLIP from the node's pre-maximize rect (the DockFlip addon already animates dock re-layouts; a first/last transform against one node is its trivial case). Instant-path fallbacks per DockFlip's own contract (prefers-reduced-motion, hidden documents).
  • Input contract (frozen, see ledger): in-strip tab reordering stays live while maximized (it never leaves the node); cross-zone drag sources and tear-out affordances for the maximized node are suppressed while maximized — every drop target is under the overlay, so offering the gesture would be fixture-dishonest. Escape restores, owned by the workspace's KeyboardCommands scope and active only while maximizedNodeId is set; after restore, focus returns to the restored tabs node's active header button. The maximized plane z-orders above rails and the reveal overlay; engaging maximize closes an in-progress reveal (one overlay at a time). Any committed dock operation that reaches beyond the maximized node clears maximize BEFORE applying, and that clear is terminal — the re-projection rule re-applies only a transient that survived, never one an operation cleared. Operations confined to the maximized node (activating one of its tabs; closing, reordering or adding an item within it) do not pre-clear: they defer to the same deterministic rule, which re-applies onto the surviving node and clears when the node collapsed away — without this confinement, switching tabs inside a maximized pane would restore it on the second click of any real session. Double-click on the tab header stays an optional second affordance.

Acceptance Criteria

  • enableDockMaximizeAction renders an engine-owned toggle action on tabs-node headers; the reserved-name guard covers it like close.
  • Maximize/restore never re-parents the pane's DOM: a headless spec pins element identity (the same node before, during, and after) and asserts an iframe child is not reloaded.
  • Maximized state is workspace-transient: the committed document and a saved/restored perspective are byte-identical whether or not a pane was maximized when captured.
  • The maximized node paints the full workspace rect (headless rect assertion) and Escape restores.
  • Operations confined to the maximized node (tab activation, in-node close) keep it maximized; any operation beyond it clears before applying — both spec-witnessed.
  • FLIP motion on both transitions with DockFlip's instant-path degradations intact.

Contract Ledger (consumed cells)

Cell Producer Behavior Fallback Docs Evidence
header action surface projection.LayoutAdapter renders engine-owned actions next to host actions; ownership dispatch in dashboard.dock.Workspace#onDockHeaderAction (the close precedent, reserved-name guard included) enableDockMaximizeAction: false (default) renders nothing — byte-identical projection for every current consumer; true renders the toggle on tabs-node headers Off by default The option name + the reserved-name guard error text close/enableDockCloseAction shipping today; the guard already throws on host-action collisions
maximized presentation + geometry dashboard.dock.Workspace stamps neo-dock-maximized on the target tabs node's projected root AND supplies the rect: it measures its own root el's viewport rect (DomAccess.getBoundingClientRect on the workspace id — the workspace is the authority, no containing-block assumption) and sets top/left/width/height inline The node paints the measured workspace rect; header stays visible carrying the restore toggle; NO DOM re-parenting (spec pins element identity + iframe-not-reloaded). Resize while maximized: a NEW Workspace-owned observation — no existing one covers this: on maximize, the Workspace registers its root el with the ResizeObserver main-thread addon (the grid.Container#addResizeObserver idiom: getAddon('ResizeObserver', windowId) + register({id})); the handler re-measures and re-applies the four values; restore, node-clear, and workspace destroy all unregister — the observation exists exactly as long as the presentation does, no standing cost while un-maximized. Fail-safe: an unresolvable measurement (workspace unmounted mid-gesture, zero-area rect) clears maximizedNodeId and removes the class — the restore path, never a half state Cleared state = normal layout SCSS block comment naming the no-reparent + measured-rect constraints dialog.Base#afterSetMaximized for the cls-toggle half; DockFlip's rect measurement for the measure-then-style idiom
input while maximized dashboard.dock.Workspace guards its drag-source registration and key scope on maximizedNodeId In-strip tab reorder stays live (never leaves the node); cross-zone + tear-out sources for the maximized node suppressed (all drop targets sit under the overlay); Escape restores (workspace KeyboardCommands, armed only while maximized), focus returns to the restored node's active header button; maximize z-orders above rails/reveal overlay and closes an in-progress reveal; a committed dock operation beyond the maximized node clears maximize BEFORE applying, terminally for the re-projection rule; an operation confined to the maximized node (setActiveItem on it, closeItem/moveItem/addTab within it) defers to that rule instead of pre-clearing Suppressions lift on restore The config jsdoc carries the input contract The reveal state machine + KeyboardCommands surfaces exist; suppression is a guard, not new machinery
transient state × re-projection maximizedNodeId_ on the workspace instance — never written into the committed document Deterministic re-projection rule above: re-apply iff the node id resolves in the new projection, clear otherwise. Perspectives capture/restore byte-identical documents regardless of maximize state (AC) Cleared state = normal layout The config's jsdoc carries the rule AC pins document + perspective byte-equality and both re-projection outcomes
motion main.addon.DockFlip FLIP from the pre-toggle rect Instant-path degradations per DockFlip's own contract (prefers-reduced-motion, hidden documents, frame-starved windows) Instant land DockFlip class jsdoc (already written) DockFlip ships with these paths spec-covered

Origin

Requested as proper engine support after a consumer-workspace exploration (agent session 914dffcd-0057-46a5-a0c6-9ca807fd5c91) concluded a client-side overlay would re-derive dock-owned decisions. Related: #13158 (Qt-parity docking polish epic — this is a sibling capability), #17418 (header-toolbar flat actions substrate), the host header-action seam in LayoutAdapter/Workspace, dialog.Base maximize precedent.

— Vega (Fable 5, Claude Code) 🌿