@svgrid/grid 2.1.22 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/dist/DockNodeView.svelte +306 -0
- package/dist/DockNodeView.svelte.d.ts +7 -0
- package/dist/DockPopoutHost.svelte +17 -0
- package/dist/DockPopoutHost.svelte.d.ts +14 -0
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +178 -178
- package/dist/GridMenus.svelte +83 -3
- package/dist/SvAutoComplete.svelte +5 -3
- package/dist/SvCarousel.svelte +141 -163
- package/dist/SvColorInput.svelte +187 -186
- package/dist/SvColorInput.svelte.d.ts +1 -1
- package/dist/SvComboBox.svelte +7 -3
- package/dist/SvCommand.svelte +28 -85
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +1 -1
- package/dist/SvDateRangeInput.svelte +1 -1
- package/dist/SvDateTimePicker.svelte +9 -3
- package/dist/SvDateTimePicker.svelte.d.ts +3 -0
- package/dist/SvDockLayout.svelte +244 -0
- package/dist/SvDockLayout.svelte.d.ts +77 -0
- package/dist/SvDockManager.svelte +922 -0
- package/dist/SvDockManager.svelte.d.ts +106 -0
- package/dist/SvDrawer.svelte +250 -228
- package/dist/SvDrawer.svelte.d.ts +4 -0
- package/dist/SvDropDownList.svelte +85 -13
- package/dist/SvDropDownList.svelte.d.ts +3 -0
- package/dist/SvField.svelte +21 -4
- package/dist/SvField.svelte.d.ts +1 -0
- package/dist/SvForm.svelte +201 -91
- package/dist/SvForm.svelte.d.ts +15 -22
- package/dist/SvGrid.controller.svelte.d.ts +32 -1
- package/dist/SvGrid.controller.svelte.js +156 -27
- package/dist/SvGrid.css +193 -5
- package/dist/SvGrid.svelte +3131 -2786
- package/dist/SvGrid.types.d.ts +452 -1
- package/dist/SvGridBoard.svelte +2339 -2339
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridDropdown.svelte +689 -682
- package/dist/SvGridSelect.svelte +1 -0
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvListBox.svelte +36 -14
- package/dist/SvMaskedInput.svelte +4 -3
- package/dist/SvMaskedInput.svelte.d.ts +1 -1
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +117 -165
- package/dist/SvMenuList.svelte.d.ts +2 -13
- package/dist/SvModal.svelte +12 -29
- package/dist/SvMultiSelect.svelte +1 -0
- package/dist/SvNumberInput.svelte +131 -130
- package/dist/SvNumberInput.svelte.d.ts +1 -1
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPagination.svelte +18 -18
- package/dist/SvPasswordInput.svelte +3 -2
- package/dist/SvPasswordInput.svelte.d.ts +1 -1
- package/dist/SvPhoneInput.svelte +3 -2
- package/dist/SvPhoneInput.svelte.d.ts +1 -1
- package/dist/SvPopover.svelte +139 -139
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvStepper.svelte +6 -9
- package/dist/SvTagsInput.svelte +3 -2
- package/dist/SvTagsInput.svelte.d.ts +1 -1
- package/dist/SvTooltip.svelte +127 -138
- package/dist/SvTour.svelte +204 -176
- package/dist/SvTree.svelte +377 -377
- package/dist/SvTreeSelect.svelte +230 -229
- package/dist/build-api.js +76 -54
- package/dist/cdn/svgrid.js +10913 -6450
- package/dist/cdn/svgrid.svelte-external.js +9628 -5481
- package/dist/chart-export.js +8 -8
- package/dist/columns.js +6 -0
- package/dist/conditional-formatting.d.ts +67 -3
- package/dist/conditional-formatting.js +124 -17
- package/dist/createCarousel.svelte.d.ts +72 -0
- package/dist/createCarousel.svelte.js +80 -0
- package/dist/createCommand.svelte.d.ts +62 -0
- package/dist/createCommand.svelte.js +109 -0
- package/dist/createForm.svelte.d.ts +62 -0
- package/dist/createForm.svelte.js +374 -0
- package/dist/createMenu.svelte.d.ts +88 -0
- package/dist/createMenu.svelte.js +153 -0
- package/dist/createOverlay.svelte.d.ts +38 -0
- package/dist/createOverlay.svelte.js +68 -0
- package/dist/createPagination.d.ts +74 -0
- package/dist/createPagination.js +71 -0
- package/dist/createPopoverSelect.svelte.js +6 -12
- package/dist/createStepper.d.ts +44 -0
- package/dist/createStepper.js +26 -0
- package/dist/createTooltip.svelte.d.ts +32 -0
- package/dist/createTooltip.svelte.js +61 -0
- package/dist/datetime/timezone.d.ts +36 -0
- package/dist/datetime/timezone.js +110 -0
- package/dist/dock-context.d.ts +52 -0
- package/dist/dock-context.js +1 -0
- package/dist/dock-manager-model.d.ts +136 -0
- package/dist/dock-manager-model.js +514 -0
- package/dist/dock-model.d.ts +105 -0
- package/dist/dock-model.js +288 -0
- package/dist/dock-popout.d.ts +30 -0
- package/dist/dock-popout.js +81 -0
- package/dist/editor-contract.d.ts +2 -0
- package/dist/filter-operators.d.ts +6 -0
- package/dist/filter-operators.js +35 -2
- package/dist/filtering/excel-filters.d.ts +18 -1
- package/dist/filtering/excel-filters.js +56 -0
- package/dist/form-field.d.ts +73 -0
- package/dist/form-field.js +15 -0
- package/dist/index.d.ts +25 -5
- package/dist/index.js +40 -4
- package/dist/list-nav.d.ts +18 -0
- package/dist/list-nav.js +30 -0
- package/dist/list-option.d.ts +2 -0
- package/dist/menu-item.d.ts +20 -0
- package/dist/menu-item.js +1 -0
- package/dist/menus.d.ts +5 -0
- package/dist/menus.js +68 -16
- package/dist/panel-resize.d.ts +25 -0
- package/dist/panel-resize.js +40 -0
- package/dist/popover.d.ts +29 -3
- package/dist/popover.js +22 -6
- package/dist/recurrence.d.ts +33 -7
- package/dist/recurrence.js +151 -13
- package/dist/row-drag.d.ts +8 -0
- package/dist/row-drag.js +11 -0
- package/dist/scheduler-ical.d.ts +30 -0
- package/dist/scheduler-ical.js +180 -0
- package/dist/scheduler-model.d.ts +269 -0
- package/dist/scheduler-model.js +602 -0
- package/dist/scheduler-view.svelte.d.ts +22 -0
- package/dist/scheduler-view.svelte.js +13 -0
- package/dist/spreadsheet.js +20 -12
- package/dist/ui-app.types.d.ts +3 -0
- package/package.json +14 -10
- package/src/DockNodeView.svelte +306 -0
- package/src/DockPopoutHost.svelte +17 -0
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +178 -178
- package/src/GridMenus.svelte +83 -3
- package/src/SvAutoComplete.svelte +5 -3
- package/src/SvCarousel.svelte +141 -163
- package/src/SvCarousel.test.ts +68 -0
- package/src/SvColorInput.svelte +187 -186
- package/src/SvComboBox.svelte +7 -3
- package/src/SvCommand.svelte +28 -85
- package/src/SvCommand.test.ts +78 -0
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +1 -1
- package/src/SvDateRangeInput.svelte +1 -1
- package/src/SvDateTimePicker.svelte +9 -3
- package/src/SvDockLayout.svelte +244 -0
- package/src/SvDockManager.svelte +922 -0
- package/src/SvDrawer.svelte +250 -228
- package/src/SvDropDownList.svelte +85 -13
- package/src/SvField.svelte +21 -4
- package/src/SvForm.svelte +201 -91
- package/src/SvForm.test.ts +299 -0
- package/src/SvGrid.controller.svelte.ts +3158 -3034
- package/src/SvGrid.css +193 -5
- package/src/SvGrid.svelte +3131 -2786
- package/src/SvGrid.types.ts +1489 -1027
- package/src/SvGridBoard.svelte +2339 -2339
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridDropdown.svelte +689 -682
- package/src/SvGridSelect.svelte +1 -0
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvListBox.svelte +36 -14
- package/src/SvMaskedInput.svelte +4 -3
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -73
- package/src/SvMenuList.svelte +117 -165
- package/src/SvModal.svelte +12 -29
- package/src/SvModal.test.ts +82 -0
- package/src/SvMultiSelect.svelte +1 -0
- package/src/SvNumberInput.svelte +131 -130
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPagination.svelte +18 -18
- package/src/SvPasswordInput.svelte +3 -2
- package/src/SvPhoneInput.svelte +3 -2
- package/src/SvPopover.svelte +139 -139
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvStepper.svelte +6 -9
- package/src/SvTagsInput.svelte +3 -2
- package/src/SvTooltip.svelte +127 -138
- package/src/SvTooltip.test.ts +73 -0
- package/src/SvTour.svelte +204 -176
- package/src/SvTree.svelte +377 -377
- package/src/SvTreeSelect.svelte +230 -229
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/build-api.ts +870 -847
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart.ts +2296 -2296
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/columns.ts +5 -0
- package/src/conditional-formatting.test.ts +104 -0
- package/src/conditional-formatting.ts +176 -19
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1111 -1111
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCarousel.svelte.ts +104 -0
- package/src/createCombobox.svelte.ts +179 -179
- package/src/createCommand.svelte.ts +125 -0
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +162 -162
- package/src/createForm.svelte.ts +333 -0
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createMenu.svelte.ts +202 -0
- package/src/createOverlay.svelte.ts +101 -0
- package/src/createPagination.test.ts +65 -0
- package/src/createPagination.ts +84 -0
- package/src/createPopoverSelect.svelte.ts +206 -210
- package/src/createStepper.test.ts +62 -0
- package/src/createStepper.ts +61 -0
- package/src/createTooltip.svelte.ts +86 -0
- package/src/createTree.svelte.ts +319 -319
- package/src/datetime/timezone.test.ts +69 -0
- package/src/datetime/timezone.ts +134 -0
- package/src/dock-context.ts +47 -0
- package/src/dock-manager-model.test.ts +275 -0
- package/src/dock-manager-model.ts +593 -0
- package/src/dock-model.test.ts +163 -0
- package/src/dock-model.ts +368 -0
- package/src/dock-popout.ts +94 -0
- package/src/editing.test.ts +859 -859
- package/src/editing.ts +675 -675
- package/src/editor-contract.ts +2 -0
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/filter-operators.test.ts +59 -0
- package/src/filter-operators.ts +35 -2
- package/src/filtering/excel-filters.test.ts +86 -1
- package/src/filtering/excel-filters.ts +65 -0
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +87 -0
- package/src/index.ts +623 -469
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -0
- package/src/list-nav.ts +29 -0
- package/src/list-option.ts +2 -0
- package/src/menu-item.ts +21 -0
- package/src/menus.test.ts +48 -0
- package/src/menus.ts +639 -582
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/panel-resize.ts +52 -0
- package/src/popover.bounds.test.ts +93 -0
- package/src/popover.ts +51 -7
- package/src/recurrence.test.ts +88 -1
- package/src/recurrence.ts +166 -15
- package/src/render-component.ts +28 -28
- package/src/row-drag.ts +10 -0
- package/src/scheduler-ical.test.ts +68 -0
- package/src/scheduler-ical.ts +180 -0
- package/src/scheduler-model.test.ts +546 -0
- package/src/scheduler-model.ts +853 -0
- package/src/scheduler-view.svelte.ts +33 -0
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +312 -304
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +501 -501
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.charting.test.ts +527 -527
- package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +159 -159
- package/src/ui-app.types.ts +3 -0
- package/src/virtual.test.ts +6 -4
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dock-manager-model - the pure state behind <SvDockManager>. It wraps the base
|
|
3
|
+
* `dock-model` tree (the docked/tiled area) with the three "pro" docking
|
|
4
|
+
* surfaces a real IDE has:
|
|
5
|
+
*
|
|
6
|
+
* floating panes popped out into movable/resizable windows (a single tabs
|
|
7
|
+
* leaf each - a floating panel).
|
|
8
|
+
* autoHide leaves collapsed to an edge strip; a fly-out reveals them and
|
|
9
|
+
* "pin" re-docks them.
|
|
10
|
+
* main the tiled dock area itself (a `DockNode` tree, or null when
|
|
11
|
+
* everything has been floated / hidden).
|
|
12
|
+
*
|
|
13
|
+
* As with the base model, every operation is a pure transform returning a NEW
|
|
14
|
+
* state, so a whole workspace - tiled + floating + hidden - serializes to JSON
|
|
15
|
+
* and restores exactly. The Svelte view owns the DOM, gestures and z-order
|
|
16
|
+
* painting; all the surgery lives here and is unit-tested without a browser.
|
|
17
|
+
*/
|
|
18
|
+
import {
|
|
19
|
+
tabs,
|
|
20
|
+
dockInto,
|
|
21
|
+
removePane as removePaneFromTree,
|
|
22
|
+
removeLeaf,
|
|
23
|
+
dockLeafToEdge,
|
|
24
|
+
reorderPane,
|
|
25
|
+
findTabsWithPane,
|
|
26
|
+
allPaneIds,
|
|
27
|
+
type DockNode,
|
|
28
|
+
type DockTabs,
|
|
29
|
+
type DockPane,
|
|
30
|
+
type DockZone,
|
|
31
|
+
type IdGen,
|
|
32
|
+
} from './dock-model'
|
|
33
|
+
|
|
34
|
+
export type DockSide = Exclude<DockZone, 'center'>
|
|
35
|
+
|
|
36
|
+
/** A floating window: one tabs leaf shown in a movable/resizable frame. */
|
|
37
|
+
export type FloatWindow = {
|
|
38
|
+
id: string
|
|
39
|
+
leaf: DockTabs
|
|
40
|
+
x: number
|
|
41
|
+
y: number
|
|
42
|
+
width: number
|
|
43
|
+
height: number
|
|
44
|
+
/** Stacking order; higher is on top. */
|
|
45
|
+
z: number
|
|
46
|
+
/** Collapsed to just its title bar. */
|
|
47
|
+
minimized?: boolean
|
|
48
|
+
/** Expanded to fill the manager (its x/y/w/h are kept for restore). */
|
|
49
|
+
maximized?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A leaf collapsed to an edge; a fly-out reveals it, "pin" re-docks it. */
|
|
53
|
+
export type AutoHideEntry = {
|
|
54
|
+
id: string
|
|
55
|
+
side: DockSide
|
|
56
|
+
leaf: DockTabs
|
|
57
|
+
/** Fly-out panel size in px along the reveal axis. */
|
|
58
|
+
size: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type DockManagerState = {
|
|
62
|
+
main: DockNode | null
|
|
63
|
+
floating: FloatWindow[]
|
|
64
|
+
autoHide: AutoHideEntry[]
|
|
65
|
+
/** A tiled leaf shown maximized (filling the docked area), or null. */
|
|
66
|
+
maximizedLeaf?: string | null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- queries --------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/** Where a pane currently lives. */
|
|
72
|
+
export type PaneLocation =
|
|
73
|
+
| { kind: 'main' }
|
|
74
|
+
| { kind: 'floating'; windowId: string }
|
|
75
|
+
| { kind: 'autoHide'; entryId: string }
|
|
76
|
+
|
|
77
|
+
export function locatePane(state: DockManagerState, paneId: string): PaneLocation | null {
|
|
78
|
+
if (state.main && findTabsWithPane(state.main, paneId)) return { kind: 'main' }
|
|
79
|
+
for (const w of state.floating) {
|
|
80
|
+
if (w.leaf.panes.some((p) => p.id === paneId)) return { kind: 'floating', windowId: w.id }
|
|
81
|
+
}
|
|
82
|
+
for (const e of state.autoHide) {
|
|
83
|
+
if (e.leaf.panes.some((p) => p.id === paneId)) return { kind: 'autoHide', entryId: e.id }
|
|
84
|
+
}
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Which surface holds a tabs leaf: 'main', a window id, or null. */
|
|
89
|
+
export function surfaceOfTabs(state: DockManagerState, tabsId: string): 'main' | string | null {
|
|
90
|
+
if (state.main && containsTabs(state.main, tabsId)) return 'main'
|
|
91
|
+
const w = state.floating.find((f) => f.leaf.id === tabsId)
|
|
92
|
+
return w ? w.id : null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function containsTabs(node: DockNode, tabsId: string): boolean {
|
|
96
|
+
if (node.type === 'tabs') return node.id === tabsId
|
|
97
|
+
return node.children.some((c) => containsTabs(c, tabsId))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function findPaneObject(state: DockManagerState, paneId: string): DockPane | null {
|
|
101
|
+
const scan = (node: DockNode): DockPane | null => {
|
|
102
|
+
if (node.type === 'tabs') return node.panes.find((p) => p.id === paneId) ?? null
|
|
103
|
+
for (const c of node.children) { const hit = scan(c); if (hit) return hit }
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
if (state.main) { const m = scan(state.main); if (m) return m }
|
|
107
|
+
for (const w of state.floating) { const f = w.leaf.panes.find((p) => p.id === paneId); if (f) return f }
|
|
108
|
+
for (const e of state.autoHide) { const a = e.leaf.panes.find((p) => p.id === paneId); if (a) return a }
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function maxZ(state: DockManagerState): number {
|
|
113
|
+
return state.floating.reduce((m, w) => Math.max(m, w.z), 0)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- internal: remove a pane from wherever it is --------------------------
|
|
117
|
+
|
|
118
|
+
/** Remove a pane from every surface, dropping windows/entries it empties. */
|
|
119
|
+
function stripPane(state: DockManagerState, paneId: string): DockManagerState {
|
|
120
|
+
let main = state.main
|
|
121
|
+
if (main && findTabsWithPane(main, paneId)) {
|
|
122
|
+
main = removePaneFromTree(main, paneId).root
|
|
123
|
+
}
|
|
124
|
+
const floating = state.floating
|
|
125
|
+
.map((w) => {
|
|
126
|
+
if (!w.leaf.panes.some((p) => p.id === paneId)) return w
|
|
127
|
+
const panes = w.leaf.panes.filter((p) => p.id !== paneId)
|
|
128
|
+
if (panes.length === 0) return null
|
|
129
|
+
const active = Math.max(0, Math.min(w.leaf.active, panes.length - 1))
|
|
130
|
+
return { ...w, leaf: { ...w.leaf, panes, active } }
|
|
131
|
+
})
|
|
132
|
+
.filter((w): w is FloatWindow => w !== null)
|
|
133
|
+
const autoHide = state.autoHide
|
|
134
|
+
.map((e) => {
|
|
135
|
+
if (!e.leaf.panes.some((p) => p.id === paneId)) return e
|
|
136
|
+
const panes = e.leaf.panes.filter((p) => p.id !== paneId)
|
|
137
|
+
if (panes.length === 0) return null
|
|
138
|
+
const active = Math.max(0, Math.min(e.leaf.active, panes.length - 1))
|
|
139
|
+
return { ...e, leaf: { ...e.leaf, panes, active } }
|
|
140
|
+
})
|
|
141
|
+
.filter((e): e is AutoHideEntry => e !== null)
|
|
142
|
+
return { main, floating, autoHide }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---- mutations ------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/** Reorder a tab within its leaf, wherever that leaf lives (main or a float). */
|
|
148
|
+
export function reorderTab(state: DockManagerState, tabsId: string, from: number, to: number): DockManagerState {
|
|
149
|
+
if (state.main && containsTabs(state.main, tabsId)) {
|
|
150
|
+
return { ...state, main: reorderPane(state.main, tabsId, from, to) }
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
...state,
|
|
154
|
+
floating: state.floating.map((w) => {
|
|
155
|
+
if (w.leaf.id !== tabsId) return w
|
|
156
|
+
const reordered = reorderPane(w.leaf, tabsId, from, to)
|
|
157
|
+
return reordered.type === 'tabs' ? { ...w, leaf: reordered } : w
|
|
158
|
+
}),
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Resize a group's children (splitter drag). Groups only exist in `main`
|
|
163
|
+
* (floating windows are single leaves), so this targets the main tree. */
|
|
164
|
+
export function resizeGroup(state: DockManagerState, groupId: string, sizes: number[]): DockManagerState {
|
|
165
|
+
if (!state.main) return state
|
|
166
|
+
return { ...state, main: setSizesInTree(state.main, groupId, sizes) }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function setSizesInTree(node: DockNode, groupId: string, sizes: number[]): DockNode {
|
|
170
|
+
if (node.type === 'tabs') return node
|
|
171
|
+
const children = node.children.map((c) => setSizesInTree(c, groupId, sizes))
|
|
172
|
+
if (node.id === groupId) return { ...node, children, sizes: normalizeManagerSizes(sizes, children.length) }
|
|
173
|
+
return { ...node, children }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function normalizeManagerSizes(sizes: number[], count: number): number[] {
|
|
177
|
+
if (count <= 0) return []
|
|
178
|
+
const src = sizes.length === count ? sizes : new Array(count).fill(1)
|
|
179
|
+
const total = src.reduce((a, b) => a + (b > 0 ? b : 0), 0)
|
|
180
|
+
return total <= 0 ? new Array(count).fill(1 / count) : src.map((s) => (s > 0 ? s : 0) / total)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Set the active tab of a leaf in main or a floating window. */
|
|
184
|
+
export function setManagerActive(state: DockManagerState, tabsId: string, active: number): DockManagerState {
|
|
185
|
+
const w = state.floating.find((f) => f.leaf.id === tabsId)
|
|
186
|
+
if (w) {
|
|
187
|
+
return {
|
|
188
|
+
...state,
|
|
189
|
+
floating: state.floating.map((f) =>
|
|
190
|
+
f.id === w.id ? { ...f, leaf: { ...f.leaf, active: clamp(active, f.leaf.panes.length) } } : f,
|
|
191
|
+
),
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Auto-hidden leaf: activate a pane so its edge tab / flyout opens on it.
|
|
195
|
+
const ah = state.autoHide.find((e) => e.leaf.id === tabsId)
|
|
196
|
+
if (ah) {
|
|
197
|
+
return {
|
|
198
|
+
...state,
|
|
199
|
+
autoHide: state.autoHide.map((e) =>
|
|
200
|
+
e.id === ah.id ? { ...e, leaf: { ...e.leaf, active: clamp(active, e.leaf.panes.length) } } : e,
|
|
201
|
+
),
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { ...state, main: state.main ? setActiveInTree(state.main, tabsId, active) : state.main }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Dock a pane onto a target leaf (`center` = new tab, edge = split). Works
|
|
209
|
+
* across surfaces: a floating pane can dock into main, a main pane into a float
|
|
210
|
+
* (centre only), etc. Edge zones only apply when the target is in `main`.
|
|
211
|
+
*/
|
|
212
|
+
export function dockPaneOnto(
|
|
213
|
+
state: DockManagerState,
|
|
214
|
+
paneId: string,
|
|
215
|
+
targetTabsId: string,
|
|
216
|
+
zone: DockZone,
|
|
217
|
+
genId: IdGen,
|
|
218
|
+
): DockManagerState {
|
|
219
|
+
const moving = findPaneObject(state, paneId)
|
|
220
|
+
if (!moving) return state
|
|
221
|
+
const targetSurface = surfaceOfTabs(state, targetTabsId)
|
|
222
|
+
if (!targetSurface) return state
|
|
223
|
+
|
|
224
|
+
const stripped = stripPane(state, paneId)
|
|
225
|
+
// The target leaf might have dissolved (it was the pane's own single leaf).
|
|
226
|
+
if (surfaceOfTabs(stripped, targetTabsId) === null) return state
|
|
227
|
+
|
|
228
|
+
if (targetSurface === 'main') {
|
|
229
|
+
const base = stripped.main
|
|
230
|
+
if (!base) return state
|
|
231
|
+
return { ...stripped, main: dockInto(base, targetTabsId, moving, zone, genId) }
|
|
232
|
+
}
|
|
233
|
+
// Docking into a floating window: centre only (add as a tab).
|
|
234
|
+
return {
|
|
235
|
+
...stripped,
|
|
236
|
+
floating: stripped.floating.map((w) => {
|
|
237
|
+
if (w.id !== targetSurface) return w
|
|
238
|
+
const panes = [...w.leaf.panes, moving]
|
|
239
|
+
return { ...w, leaf: { ...w.leaf, panes, active: panes.length - 1 } }
|
|
240
|
+
}),
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Insert a NEW pane into the main area - the first leaf (as a tab), or as the
|
|
245
|
+
* whole main when empty. Used to pop a window/pop-out back into the layout. */
|
|
246
|
+
export function addPaneToMain(state: DockManagerState, p: DockPane, genId: IdGen): DockManagerState {
|
|
247
|
+
if (!state.main) return { ...state, main: tabs(genId, [p], 0) }
|
|
248
|
+
let added = false
|
|
249
|
+
const walk = (n: DockNode): DockNode => {
|
|
250
|
+
if (added) return n
|
|
251
|
+
if (n.type === 'tabs') {
|
|
252
|
+
added = true
|
|
253
|
+
const panes = [...n.panes, p]
|
|
254
|
+
return { ...n, panes, active: panes.length - 1 }
|
|
255
|
+
}
|
|
256
|
+
return { ...n, children: n.children.map(walk) }
|
|
257
|
+
}
|
|
258
|
+
return { ...state, main: walk(state.main) }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Dock a pane into an empty main area (main becomes a single leaf holding it). */
|
|
262
|
+
export function dockPaneToEmptyMain(state: DockManagerState, paneId: string, genId: IdGen): DockManagerState {
|
|
263
|
+
const moving = findPaneObject(state, paneId)
|
|
264
|
+
if (!moving) return state
|
|
265
|
+
const stripped = stripPane(state, paneId)
|
|
266
|
+
if (stripped.main) return { ...stripped, main: stripped.main } // main not empty; caller should dock onto a leaf
|
|
267
|
+
return { ...stripped, main: tabs(genId, [moving], 0) }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Pop a pane out into a new floating window at the given rect. */
|
|
271
|
+
export function floatPane(
|
|
272
|
+
state: DockManagerState,
|
|
273
|
+
paneId: string,
|
|
274
|
+
rect: { x: number; y: number; width: number; height: number },
|
|
275
|
+
genId: IdGen,
|
|
276
|
+
): DockManagerState {
|
|
277
|
+
const moving = findPaneObject(state, paneId)
|
|
278
|
+
if (!moving) return state
|
|
279
|
+
const loc = locatePane(state, paneId)
|
|
280
|
+
// Already the sole pane of its own float - nothing to pop out.
|
|
281
|
+
if (loc?.kind === 'floating') {
|
|
282
|
+
const w = state.floating.find((f) => f.id === loc.windowId)
|
|
283
|
+
if (w && w.leaf.panes.length === 1) return state
|
|
284
|
+
}
|
|
285
|
+
const stripped = stripPane(state, paneId)
|
|
286
|
+
const win: FloatWindow = {
|
|
287
|
+
id: genId(),
|
|
288
|
+
leaf: tabs(genId, [moving], 0),
|
|
289
|
+
x: rect.x,
|
|
290
|
+
y: rect.y,
|
|
291
|
+
width: rect.width,
|
|
292
|
+
height: rect.height,
|
|
293
|
+
z: maxZ(state) + 1,
|
|
294
|
+
}
|
|
295
|
+
return { ...stripped, floating: [...stripped.floating, win] }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Dock an entire floating window's panes back into a main leaf. */
|
|
299
|
+
export function dockWindowOnto(
|
|
300
|
+
state: DockManagerState,
|
|
301
|
+
windowId: string,
|
|
302
|
+
targetTabsId: string,
|
|
303
|
+
zone: DockZone,
|
|
304
|
+
genId: IdGen,
|
|
305
|
+
): DockManagerState {
|
|
306
|
+
const win = state.floating.find((w) => w.id === windowId)
|
|
307
|
+
if (!win) return state
|
|
308
|
+
if (surfaceOfTabs(state, targetTabsId) !== 'main' || !state.main) return state
|
|
309
|
+
const rest = state.floating.filter((w) => w.id !== windowId)
|
|
310
|
+
if (zone === 'center') {
|
|
311
|
+
return {
|
|
312
|
+
...state,
|
|
313
|
+
floating: rest,
|
|
314
|
+
main: state.main
|
|
315
|
+
? mergePanesInto(state.main, targetTabsId, win.leaf.panes)
|
|
316
|
+
: state.main,
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Edge: drop the window's whole leaf beside the target.
|
|
320
|
+
const leaf = tabs(genId, win.leaf.panes, win.leaf.active)
|
|
321
|
+
const dir = zone === 'left' || zone === 'right' ? 'row' : 'column'
|
|
322
|
+
const before = zone === 'left' || zone === 'top'
|
|
323
|
+
return {
|
|
324
|
+
...state,
|
|
325
|
+
floating: rest,
|
|
326
|
+
main: insertLeafBeside(state.main, targetTabsId, leaf, dir, before, genId),
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Move a floating window (top-left). */
|
|
331
|
+
export function moveWindow(state: DockManagerState, windowId: string, x: number, y: number): DockManagerState {
|
|
332
|
+
return { ...state, floating: state.floating.map((w) => (w.id === windowId ? { ...w, x, y } : w)) }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Resize a floating window. */
|
|
336
|
+
export function resizeWindow(
|
|
337
|
+
state: DockManagerState,
|
|
338
|
+
windowId: string,
|
|
339
|
+
width: number,
|
|
340
|
+
height: number,
|
|
341
|
+
): DockManagerState {
|
|
342
|
+
return {
|
|
343
|
+
...state,
|
|
344
|
+
floating: state.floating.map((w) =>
|
|
345
|
+
w.id === windowId ? { ...w, width: Math.max(140, width), height: Math.max(80, height) } : w,
|
|
346
|
+
),
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Raise a floating window to the top of the stack. */
|
|
351
|
+
export function bringToFront(state: DockManagerState, windowId: string): DockManagerState {
|
|
352
|
+
const top = maxZ(state)
|
|
353
|
+
return { ...state, floating: state.floating.map((w) => (w.id === windowId ? { ...w, z: top + 1 } : w)) }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Collapse a floating window to just its title bar (or restore it). */
|
|
357
|
+
export function setWindowMinimized(state: DockManagerState, windowId: string, minimized: boolean): DockManagerState {
|
|
358
|
+
return {
|
|
359
|
+
...state,
|
|
360
|
+
floating: state.floating.map((w) =>
|
|
361
|
+
w.id === windowId ? { ...w, minimized, maximized: minimized ? false : w.maximized } : w,
|
|
362
|
+
),
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Toggle a floating window between filling the manager and its own rect. */
|
|
367
|
+
export function toggleWindowMaximized(state: DockManagerState, windowId: string): DockManagerState {
|
|
368
|
+
return {
|
|
369
|
+
...state,
|
|
370
|
+
floating: state.floating.map((w) =>
|
|
371
|
+
w.id === windowId ? { ...w, maximized: !w.maximized, minimized: false } : w,
|
|
372
|
+
),
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Close a floating window and all its panes. */
|
|
377
|
+
export function closeWindow(state: DockManagerState, windowId: string): DockManagerState {
|
|
378
|
+
return { ...state, floating: state.floating.filter((w) => w.id !== windowId) }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Find a tabs leaf anywhere (main or a floating window) by id. */
|
|
382
|
+
export function findLeafById(state: DockManagerState, tabsId: string): DockTabs | null {
|
|
383
|
+
const scan = (n: DockNode): DockTabs | null => {
|
|
384
|
+
if (n.type === 'tabs') return n.id === tabsId ? n : null
|
|
385
|
+
for (const c of n.children) { const hit = scan(c); if (hit) return hit }
|
|
386
|
+
return null
|
|
387
|
+
}
|
|
388
|
+
if (state.main) { const m = scan(state.main); if (m) return m }
|
|
389
|
+
const w = state.floating.find((f) => f.leaf.id === tabsId)
|
|
390
|
+
if (w) return w.leaf
|
|
391
|
+
const e = state.autoHide.find((x) => x.leaf.id === tabsId)
|
|
392
|
+
return e ? e.leaf : null
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Toggle a tiled leaf maximized (filling the docked area). Clears if it is gone. */
|
|
396
|
+
export function toggleMaximizeLeaf(state: DockManagerState, tabsId: string): DockManagerState {
|
|
397
|
+
const on = state.maximizedLeaf === tabsId
|
|
398
|
+
return { ...state, maximizedLeaf: on ? null : tabsId }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Send a floating window's panel to an edge as an auto-hidden entry. */
|
|
402
|
+
export function autoHideWindow(
|
|
403
|
+
state: DockManagerState,
|
|
404
|
+
windowId: string,
|
|
405
|
+
side: DockSide,
|
|
406
|
+
genId: IdGen,
|
|
407
|
+
size = 260,
|
|
408
|
+
): DockManagerState {
|
|
409
|
+
const w = state.floating.find((f) => f.id === windowId)
|
|
410
|
+
if (!w) return state
|
|
411
|
+
const entry: AutoHideEntry = { id: genId(), side, leaf: tabs(genId, w.leaf.panes, w.leaf.active), size }
|
|
412
|
+
return { ...state, floating: state.floating.filter((f) => f.id !== windowId), autoHide: [...state.autoHide, entry] }
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Collapse a MAIN leaf to an edge strip (auto-hide) on the given side. */
|
|
416
|
+
export function autoHideLeaf(
|
|
417
|
+
state: DockManagerState,
|
|
418
|
+
tabsId: string,
|
|
419
|
+
side: DockSide,
|
|
420
|
+
size = 260,
|
|
421
|
+
): DockManagerState {
|
|
422
|
+
if (!state.main) return state
|
|
423
|
+
const { root, leaf } = removeLeaf(state.main, tabsId)
|
|
424
|
+
if (!leaf) return state
|
|
425
|
+
const entry: AutoHideEntry = { id: `ah-${tabsId}`, side, leaf, size }
|
|
426
|
+
return { ...state, main: root, autoHide: [...state.autoHide, entry] }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Auto-hide a single pane to an edge (drag a tab to the manager border). The
|
|
430
|
+
* pane leaves its leaf and becomes its own collapsed entry on that side. */
|
|
431
|
+
export function autoHidePaneToSide(
|
|
432
|
+
state: DockManagerState,
|
|
433
|
+
paneId: string,
|
|
434
|
+
side: DockSide,
|
|
435
|
+
genId: IdGen,
|
|
436
|
+
size = 260,
|
|
437
|
+
): DockManagerState {
|
|
438
|
+
const moving = findPaneObject(state, paneId)
|
|
439
|
+
if (!moving) return state
|
|
440
|
+
const stripped = stripPane(state, paneId)
|
|
441
|
+
const entry: AutoHideEntry = { id: genId(), side, leaf: tabs(genId, [moving], 0), size }
|
|
442
|
+
return { ...stripped, autoHide: [...stripped.autoHide, entry] }
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Pin an auto-hidden entry back into the main dock area on its edge. `fraction`
|
|
447
|
+
* is the share of the axis the re-docked panel should take (default 0.25).
|
|
448
|
+
*/
|
|
449
|
+
export function pinAutoHidden(
|
|
450
|
+
state: DockManagerState,
|
|
451
|
+
entryId: string,
|
|
452
|
+
genId: IdGen,
|
|
453
|
+
fraction = 0.25,
|
|
454
|
+
): DockManagerState {
|
|
455
|
+
const entry = state.autoHide.find((e) => e.id === entryId)
|
|
456
|
+
if (!entry) return state
|
|
457
|
+
const leaf = tabs(genId, entry.leaf.panes, entry.leaf.active)
|
|
458
|
+
return {
|
|
459
|
+
...state,
|
|
460
|
+
autoHide: state.autoHide.filter((e) => e.id !== entryId),
|
|
461
|
+
main: dockLeafToEdge(state.main, leaf, entry.side, genId, fraction),
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Resize an auto-hide fly-out. */
|
|
466
|
+
export function setAutoHideSize(state: DockManagerState, entryId: string, size: number): DockManagerState {
|
|
467
|
+
return {
|
|
468
|
+
...state,
|
|
469
|
+
autoHide: state.autoHide.map((e) => (e.id === entryId ? { ...e, size: Math.max(120, size) } : e)),
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Close a pane wherever it lives. */
|
|
474
|
+
export function closePane(state: DockManagerState, paneId: string): DockManagerState {
|
|
475
|
+
return stripPane(state, paneId)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Every pane id across all surfaces. */
|
|
479
|
+
export function allManagerPaneIds(state: DockManagerState): string[] {
|
|
480
|
+
const ids: string[] = []
|
|
481
|
+
if (state.main) ids.push(...allPaneIds(state.main))
|
|
482
|
+
for (const w of state.floating) ids.push(...w.leaf.panes.map((p) => p.id))
|
|
483
|
+
for (const e of state.autoHide) ids.push(...e.leaf.panes.map((p) => p.id))
|
|
484
|
+
return ids
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Every id used anywhere in the workspace: node ids (groups + tabs leaves),
|
|
488
|
+
* pane ids, floating-window ids and auto-hide entry ids. Used to keep a freshly
|
|
489
|
+
* generated id unique even against a workspace restored from storage - the
|
|
490
|
+
* manager's id counter is module-scoped and resets on reload, so without this
|
|
491
|
+
* the same `dm-N` can be minted twice and collide (a keyed `{#each}` then throws
|
|
492
|
+
* `each_key_duplicate`). */
|
|
493
|
+
export function allManagerIds(state: DockManagerState): string[] {
|
|
494
|
+
const ids: string[] = []
|
|
495
|
+
const walk = (n: DockNode) => {
|
|
496
|
+
ids.push(n.id)
|
|
497
|
+
if (n.type === 'tabs') for (const p of n.panes) ids.push(p.id)
|
|
498
|
+
else for (const c of n.children) walk(c)
|
|
499
|
+
}
|
|
500
|
+
if (state.main) walk(state.main)
|
|
501
|
+
for (const w of state.floating) { ids.push(w.id); walk(w.leaf) }
|
|
502
|
+
for (const e of state.autoHide) { ids.push(e.id); walk(e.leaf) }
|
|
503
|
+
return ids
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Reassign any duplicate group / tabs NODE id (and floating-window / auto-hide
|
|
507
|
+
* entry id) so no keyed sibling list sees a collision. Pane ids are left alone -
|
|
508
|
+
* they key the `pane` snippet to its content, so a rename would blank a panel;
|
|
509
|
+
* a restored workspace's repeats are always on the generated node ids anyway.
|
|
510
|
+
* Idempotent: a clean workspace is returned value-equal. */
|
|
511
|
+
export function dedupeManagerNodeIds(state: DockManagerState): DockManagerState {
|
|
512
|
+
const seen = new Set<string>()
|
|
513
|
+
let n = 1
|
|
514
|
+
const fresh = (): string => { let id = `dm-d${n++}`; while (seen.has(id)) id = `dm-d${n++}`; return id }
|
|
515
|
+
const uniq = (id: string): string => {
|
|
516
|
+
const next = seen.has(id) ? fresh() : id
|
|
517
|
+
seen.add(next)
|
|
518
|
+
return next
|
|
519
|
+
}
|
|
520
|
+
const walk = (node: DockNode): DockNode => {
|
|
521
|
+
const id = uniq(node.id)
|
|
522
|
+
if (node.type === 'tabs') return id === node.id ? node : { ...node, id }
|
|
523
|
+
const children = node.children.map(walk)
|
|
524
|
+
const same = id === node.id && children.every((c, i) => c === node.children[i])
|
|
525
|
+
return same ? node : { ...node, id, children }
|
|
526
|
+
}
|
|
527
|
+
const main = state.main ? walk(state.main) : state.main
|
|
528
|
+
let floatChanged = false
|
|
529
|
+
const floating = state.floating.map((w) => {
|
|
530
|
+
const id = uniq(w.id)
|
|
531
|
+
const leaf = walk(w.leaf) as DockTabs
|
|
532
|
+
if (id === w.id && leaf === w.leaf) return w
|
|
533
|
+
floatChanged = true
|
|
534
|
+
return { ...w, id, leaf }
|
|
535
|
+
})
|
|
536
|
+
let hideChanged = false
|
|
537
|
+
const autoHide = state.autoHide.map((e) => {
|
|
538
|
+
const id = uniq(e.id)
|
|
539
|
+
const leaf = walk(e.leaf) as DockTabs
|
|
540
|
+
if (id === e.id && leaf === e.leaf) return e
|
|
541
|
+
hideChanged = true
|
|
542
|
+
return { ...e, id, leaf }
|
|
543
|
+
})
|
|
544
|
+
// Preserve identity when nothing was duplicated - keeps the effect that seeds
|
|
545
|
+
// the manager from re-running and avoids needless re-renders.
|
|
546
|
+
if (main === state.main && !floatChanged && !hideChanged) return state
|
|
547
|
+
return { ...state, main, floating: floatChanged ? floating : state.floating, autoHide: hideChanged ? autoHide : state.autoHide }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ---- small tree helpers scoped to the manager -----------------------------
|
|
551
|
+
|
|
552
|
+
function clamp(i: number, len: number): number {
|
|
553
|
+
if (len <= 0) return 0
|
|
554
|
+
return Math.max(0, Math.min(i, len - 1))
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function setActiveInTree(node: DockNode, tabsId: string, active: number): DockNode {
|
|
558
|
+
if (node.type === 'tabs') {
|
|
559
|
+
return node.id === tabsId ? { ...node, active: clamp(active, node.panes.length) } : node
|
|
560
|
+
}
|
|
561
|
+
return { ...node, children: node.children.map((c) => setActiveInTree(c, tabsId, active)) }
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function mergePanesInto(node: DockNode, tabsId: string, panes: DockPane[]): DockNode {
|
|
565
|
+
if (node.type === 'tabs') {
|
|
566
|
+
if (node.id !== tabsId) return node
|
|
567
|
+
const merged = [...node.panes, ...panes]
|
|
568
|
+
return { ...node, panes: merged, active: merged.length - 1 }
|
|
569
|
+
}
|
|
570
|
+
return { ...node, children: node.children.map((c) => mergePanesInto(c, tabsId, panes)) }
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function insertLeafBeside(
|
|
574
|
+
node: DockNode,
|
|
575
|
+
targetTabsId: string,
|
|
576
|
+
leaf: DockTabs,
|
|
577
|
+
dir: 'row' | 'column',
|
|
578
|
+
before: boolean,
|
|
579
|
+
genId: IdGen,
|
|
580
|
+
): DockNode {
|
|
581
|
+
// Reuse dockInto by moving the leaf's first pane, then merging the rest - but
|
|
582
|
+
// simpler and exact: rebuild via dockLeafToEdge semantics locally. We wrap the
|
|
583
|
+
// target subtree. Find + replace the target leaf with a group of [leaf,target].
|
|
584
|
+
const replace = (n: DockNode): DockNode => {
|
|
585
|
+
if (n.type === 'tabs') {
|
|
586
|
+
if (n.id !== targetTabsId) return n
|
|
587
|
+
const kids = before ? [leaf, n] : [n, leaf]
|
|
588
|
+
return { type: 'group', id: genId(), direction: dir, children: kids, sizes: [0.5, 0.5] }
|
|
589
|
+
}
|
|
590
|
+
return { ...n, children: n.children.map(replace) }
|
|
591
|
+
}
|
|
592
|
+
return replace(node)
|
|
593
|
+
}
|