Skip to content

T-HU19-split-primary-stage

State: planning/proposed · Impact: medium · Complexity: large

The centre stage today renders exactly one document at a time through a single <NuxtPage>, so comparing two notes side by side means closing one to open the other. This task lets a reader drag a tab onto an edge of the stage to split it in that direction, and split a resulting stage again, up to a bounded number of stages — without touching @sksizer/panes’ pane array, which is fixed at construction.

LocationRole today
packages/ts/app-frame/app/composables/useDocumentTabs.tsOne flat list of DocumentTabs per window; no stage field, no move-between-stages operation
packages/ts/app-frame/app/components/ShellDocumentTabs.vueRenders the tab strip; no drag-and-drop between stages
packages/ts/app-frame/app/layouts/default.vueThe main pane holds .stage-stack: one tab row plus one <NuxtPage />; one router, one page, one stage
packages/ts/panes/src/usePaneGroup.tsBuilds its pane array once from the config passed at construction; not designed to grow a pane at runtime
packages/ts/panes/src/PaneDivider.vueResizes pane widths through the pane-group controller, horizontally only; not reusable inside one pane

A LAYOUT TREE owns the centre stage: every leaf a tabbed stage, every node a horizontal or vertical split with a ratio. The tree lives in utils/stageTree.ts as pure functions, and useDocumentTabs holds the one live instance and keeps it in step with the tab list. A stage is born by splitting another and dies the moment its last document leaves it, so a split with one child never exists.

Dragging a tab over a stage’s body picks one of five landings: the four edges split that stage in the direction the edge names (left/right across, top/bottom down) and open the document in the new half; the middle moves it in beside the documents already there. Dropping on a tab strip still moves or reorders, as it does today.

Two named caps bound the result — MAX_STAGES (4) and MAX_STAGE_DEPTH (3 levels of nesting). Past either, the edges are simply not offered: the overlay reads every position as the middle, so a refusal is visible before the drop rather than silent after it.

StageSplit.vue renders from the FLATTENED tree — one absolutely positioned cell per stage — rather than recursing. A recursive renderer re-parents the routed stage on every split, and re-parenting remounts the <NuxtPage> inside it, losing the page’s scroll position and state.

Since one Nuxt router allows one <NuxtPage>, only the root stage is routed; every other stage renders its active document through the tab-to-view resolver tabs-carry-entity-icons-and-views introduced. The aux pane follows whichever stage last had focus.

  1. utils/stageTree.ts: the tree type and its operations — split, remove-and-promote, ratio, flatten-to-boxes, and the geometry that reads a pointer position as an edge. Pure, with the caps enforced inside the operations rather than only in front of them.
  2. useDocumentTabs.ts: a stage field on DocumentTab, the one live tree, and the verbs that keep the two in step — moveToStage, splitToStage, canSplitFrom, resizeSplit, focusStage, plus collapse on the last document leaving a stage.
  3. StageColumn.vue: one stage — its strip, its document (routed or through a lens), and the drop overlay that draws the landing the pointer is over.
  4. StageSplit.vue: the frame, drawing cells and draggable dividers from the flattened tree, and mounting inside layouts/default.vue’s .stage-stack.
  5. ShellDocumentTabs.vue: native HTML5 drag-and-drop, so a tab dragged between strips moves the document and a tab dragged within its own strip reorders it.
  6. Track which stage last held focus and point the aux pane’s content at that stage’s active tab.
  7. Playwright specs covering the first split, a second split in the other direction, and the collapses; recaptured screenshots for two stages and three.
AreaNote
packages/ts/app-frame/app/utils/stageTree.tsThe layout tree and its pure operations
packages/ts/app-frame/app/composables/useDocumentTabs.tsstage field, the live tree, split/move/collapse/resize verbs
packages/ts/app-frame/app/components/ShellDocumentTabs.vueDrag-and-drop between stages
packages/ts/app-frame/app/components/StageColumn.vueOne stage, with its edge-drop overlay
packages/ts/app-frame/app/components/StageSplit.vueThe frame: cells and dividers from the flattened tree
packages/ts/app-frame/app/layouts/default.vueMount StageSplit in .stage-stack
  • AC-1: Dragging a tab onto the left or right edge of a stage opens that document in a new stage beside it; onto the top or bottom edge, above or below it.
  • AC-2: A stage that came from a split can be split again, in either direction.
  • AC-3: The layout caps at MAX_STAGES stages and MAX_STAGE_DEPTH levels of nesting; past either, the edges are not offered and a drop moves the tab into the stage instead.
  • AC-4: Each stage’s tab strip closes and reorders tabs independently of the others.
  • AC-5: Closing a stage’s last tab removes that stage and promotes its sibling into the place their split held. The routed stage is never removed.
  • AC-6: The divider between two stages drags, and neither side can be dragged below MIN_STAGE_RATIO of the split’s axis.
  • AC-7: The aux pane’s content follows whichever stage most recently had focus.
  • AC-8: Playwright specs exercise the drag-to-split flow, a second split in the other direction, and both collapses, and pass.
  • Persisting the layout or tab-to-stage assignment across a reload.
  • A keyboard route into a split; the gesture is pointer-only until there is a command surface for it.
  • Any @sksizer/panes controller change; the split lives entirely inside the existing main pane.

From the user’s planning request of 2026-09-07. The pane-array-fixed-at-construction constraint is recorded as a risk in the milestone M-BMFD.

T-PL39-stories-shell-chrome-directions, T-RR9V-tabs-carry-entity-icons-and-views


← Back to Tasks