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.
| Location | Role today |
|---|---|
packages/ts/app-frame/app/composables/useDocumentTabs.ts | One flat list of DocumentTabs per window; no stage field, no move-between-stages operation |
packages/ts/app-frame/app/components/ShellDocumentTabs.vue | Renders the tab strip; no drag-and-drop between stages |
packages/ts/app-frame/app/layouts/default.vue | The main pane holds .stage-stack: one tab row plus one <NuxtPage />; one router, one page, one stage |
packages/ts/panes/src/usePaneGroup.ts | Builds its pane array once from the config passed at construction; not designed to grow a pane at runtime |
packages/ts/panes/src/PaneDivider.vue | Resizes pane widths through the pane-group controller, horizontally only; not reusable inside one pane |
Proposed
Section titled “Proposed”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.
Approach
Section titled “Approach”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.useDocumentTabs.ts: astagefield onDocumentTab, 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.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.StageSplit.vue: the frame, drawing cells and draggable dividers from the flattened tree, and mounting insidelayouts/default.vue’s.stage-stack.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.- Track which stage last held focus and point the
auxpane’s content at that stage’s active tab. - Playwright specs covering the first split, a second split in the other direction, and the collapses; recaptured screenshots for two stages and three.
| Area | Note |
|---|---|
packages/ts/app-frame/app/utils/stageTree.ts | The layout tree and its pure operations |
packages/ts/app-frame/app/composables/useDocumentTabs.ts | stage field, the live tree, split/move/collapse/resize verbs |
packages/ts/app-frame/app/components/ShellDocumentTabs.vue | Drag-and-drop between stages |
packages/ts/app-frame/app/components/StageColumn.vue | One stage, with its edge-drop overlay |
packages/ts/app-frame/app/components/StageSplit.vue | The frame: cells and dividers from the flattened tree |
packages/ts/app-frame/app/layouts/default.vue | Mount StageSplit in .stage-stack |
Acceptance criteria
Section titled “Acceptance criteria”- 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_STAGESstages andMAX_STAGE_DEPTHlevels 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_RATIOof the split’s axis. - AC-7: The
auxpane’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.
Out of scope
Section titled “Out of scope”- 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/panescontroller change; the split lives entirely inside the existingmainpane.
Dependencies
Section titled “Dependencies”- T-PL39-stories-shell-chrome-directions supplies the split-stage design.
- T-RR9V-tabs-carry-entity-icons-and-views supplies the tab-to-view resolver every unrouted stage needs.
Discovery context
Section titled “Discovery context”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.
Depends on
Section titled “Depends on”T-PL39-stories-shell-chrome-directions, T-RR9V-tabs-carry-entity-icons-and-views