@svgrid/grid 2.6.23 → 3.0.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.
Files changed (263) hide show
  1. package/CHANGELOG.md +88 -88
  2. package/README.md +199 -199
  3. package/dist/FlexRender.svelte +96 -96
  4. package/dist/GridFooter.svelte +181 -181
  5. package/dist/SvAutoComplete.svelte +169 -169
  6. package/dist/SvAvatar.svelte +75 -75
  7. package/dist/SvCalendar.svelte +503 -503
  8. package/dist/SvCarousel.svelte +141 -141
  9. package/dist/SvCheckBox.svelte +102 -102
  10. package/dist/SvCircularProgress.svelte +109 -109
  11. package/dist/SvColorInput.svelte +181 -181
  12. package/dist/SvComboBox.svelte +279 -279
  13. package/dist/SvContextMenu.svelte +116 -116
  14. package/dist/SvCountryInput.svelte +163 -163
  15. package/dist/SvDrawer.svelte +254 -254
  16. package/dist/SvDropDownList.svelte +378 -378
  17. package/dist/SvDurationInput.svelte +126 -126
  18. package/dist/SvField.svelte +293 -293
  19. package/dist/SvForm.svelte +437 -437
  20. package/dist/SvGridCellEditor.svelte +744 -744
  21. package/dist/SvGridChart.svelte +1716 -1716
  22. package/dist/SvGridChartPanel.svelte +485 -485
  23. package/dist/SvGridChartView.svelte +70 -70
  24. package/dist/SvGridDropdown.svelte +728 -728
  25. package/dist/SvGridSelect.svelte +270 -270
  26. package/dist/SvGroupCell.svelte +116 -116
  27. package/dist/SvListBox.svelte +334 -334
  28. package/dist/SvMaskedInput.svelte +122 -122
  29. package/dist/SvMenu.svelte +124 -124
  30. package/dist/SvMenuList.svelte +146 -146
  31. package/dist/SvMultiSelect.svelte +293 -293
  32. package/dist/SvNumberInput.svelte +172 -172
  33. package/dist/SvOtpInput.svelte +158 -158
  34. package/dist/SvPasswordInput.svelte +151 -151
  35. package/dist/SvPhoneInput.svelte +133 -133
  36. package/dist/SvPopover.svelte +197 -197
  37. package/dist/SvProgress.svelte +116 -116
  38. package/dist/SvRadioGroup.svelte +107 -107
  39. package/dist/SvRating.svelte +112 -112
  40. package/dist/SvResult.svelte +73 -73
  41. package/dist/SvRichText.svelte +211 -211
  42. package/dist/SvRowGroupPanel.svelte +170 -170
  43. package/dist/SvScrollArea.svelte +61 -61
  44. package/dist/SvSlider.svelte +203 -203
  45. package/dist/SvSwitchButton.svelte +108 -108
  46. package/dist/SvTagsInput.svelte +115 -115
  47. package/dist/SvTextInput.svelte +147 -147
  48. package/dist/SvTimePicker.svelte +245 -245
  49. package/dist/SvToaster.svelte +159 -159
  50. package/dist/SvToggleButton.svelte +85 -85
  51. package/dist/SvTooltip.svelte +161 -161
  52. package/dist/SvTour.svelte +208 -208
  53. package/dist/SvTree.svelte +444 -444
  54. package/dist/SvTreeSelect.svelte +239 -239
  55. package/dist/cdn/{GridMenus-BzWXQjv3.js → GridMenus-DMihUtma.js} +3 -3
  56. package/dist/cdn/{GridMenus-xr_rHx8F.js → GridMenus-nmNDj1a3.js} +3 -3
  57. package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-Bh0A0JkF.js} +1 -1
  58. package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-DflbiP7N.js} +1 -1
  59. package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-BWwpfB_o.js} +1 -1
  60. package/dist/cdn/{SvDateTimePicker-CCbDZNZB.js → SvDateTimePicker-Bivn8dAP.js} +1 -1
  61. package/dist/cdn/{SvGridCellEditor-BAPSC7EL.js → SvGridCellEditor-AXL8cHGO.js} +1 -1
  62. package/dist/cdn/{SvGridCellEditor-BwzmUWtL.js → SvGridCellEditor-Ba7rY3eu.js} +1 -1
  63. package/dist/cdn/{SvGridChart-BwVos976.js → SvGridChart-Bs2GIR2Q.js} +1 -1
  64. package/dist/cdn/{SvGridChart-BEJmNNx9.js → SvGridChart-CwhFz7GV.js} +1 -1
  65. package/dist/cdn/{SvGridChartPanel-BOknOkrP.js → SvGridChartPanel-1WXuStzM.js} +1 -1
  66. package/dist/cdn/{SvGridChartPanel-D2PjII4O.js → SvGridChartPanel-e5_nqNP9.js} +1 -1
  67. package/dist/cdn/{SvGridChartView-00LPUHL1.js → SvGridChartView-SmPW10dI.js} +1 -1
  68. package/dist/cdn/{SvGridChartView-BhUEJ5ki.js → SvGridChartView-eSPuJE6g.js} +1 -1
  69. package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-B1ZcLpgs.js} +1 -1
  70. package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CtdbHcIS.js} +1 -1
  71. package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CcP1tafP.js} +2 -2
  72. package/dist/cdn/{date-format-BNii4zeD.js → date-format-DZT7T1wf.js} +2 -2
  73. package/dist/cdn/{row-drag-touch-Ddaa-QeB.js → row-drag-touch-CycRR65n.js} +12 -10
  74. package/dist/cdn/{src-skQN5eqh.js → src-B_YS5AOc.js} +45 -45
  75. package/dist/cdn/{src-DGo7IHug.js → src-DYXXpuSk.js} +44 -44
  76. package/dist/cdn/svgrid.js +7 -7
  77. package/dist/cdn/svgrid.svelte-external.js +7 -7
  78. package/dist/chart-export.js +8 -8
  79. package/dist/row-drag-touch.d.ts +5 -1
  80. package/dist/row-drag-touch.js +15 -4
  81. package/dist/row-drag.js +7 -3
  82. package/package.json +11 -11
  83. package/src/FlexRender.svelte +96 -96
  84. package/src/GridFooter.svelte +181 -181
  85. package/src/SvAutoComplete.svelte +169 -169
  86. package/src/SvAvatar.svelte +75 -75
  87. package/src/SvCalendar.svelte +503 -503
  88. package/src/SvCalendar.test.ts +226 -226
  89. package/src/SvCarousel.svelte +141 -141
  90. package/src/SvCheckBox.svelte +102 -102
  91. package/src/SvCircularProgress.svelte +109 -109
  92. package/src/SvColorInput.svelte +181 -181
  93. package/src/SvComboBox.svelte +279 -279
  94. package/src/SvContextMenu.svelte +116 -116
  95. package/src/SvCountryInput.svelte +163 -163
  96. package/src/SvDrawer.svelte +254 -254
  97. package/src/SvDropDownList.svelte +378 -378
  98. package/src/SvDurationInput.svelte +126 -126
  99. package/src/SvField.svelte +293 -293
  100. package/src/SvForm.svelte +437 -437
  101. package/src/SvForm.test.ts +411 -411
  102. package/src/SvGrid.types.ts +2092 -2092
  103. package/src/SvGridCellEditor.svelte +744 -744
  104. package/src/SvGridChart.svelte +1716 -1716
  105. package/src/SvGridChartPanel.svelte +485 -485
  106. package/src/SvGridChartView.svelte +70 -70
  107. package/src/SvGridDropdown.svelte +728 -728
  108. package/src/SvGridSelect.svelte +270 -270
  109. package/src/SvGroupCell.svelte +116 -116
  110. package/src/SvListBox.svelte +334 -334
  111. package/src/SvMaskedInput.svelte +122 -122
  112. package/src/SvMenu.svelte +124 -124
  113. package/src/SvMenu.test.ts +97 -97
  114. package/src/SvMenuList.svelte +146 -146
  115. package/src/SvMultiSelect.svelte +293 -293
  116. package/src/SvNumberInput.svelte +172 -172
  117. package/src/SvOtpInput.svelte +158 -158
  118. package/src/SvPasswordInput.svelte +151 -151
  119. package/src/SvPhoneInput.svelte +133 -133
  120. package/src/SvPopover.svelte +197 -197
  121. package/src/SvProgress.svelte +116 -116
  122. package/src/SvRadioGroup.svelte +107 -107
  123. package/src/SvRating.svelte +112 -112
  124. package/src/SvResult.svelte +73 -73
  125. package/src/SvRichText.svelte +211 -211
  126. package/src/SvRowGroupPanel.svelte +170 -170
  127. package/src/SvScrollArea.svelte +61 -61
  128. package/src/SvSlider.svelte +203 -203
  129. package/src/SvSwitchButton.svelte +108 -108
  130. package/src/SvTagsInput.svelte +115 -115
  131. package/src/SvTextInput.svelte +147 -147
  132. package/src/SvTimePicker.svelte +245 -245
  133. package/src/SvToaster.svelte +159 -159
  134. package/src/SvToaster.test.ts +95 -95
  135. package/src/SvToggleButton.svelte +85 -85
  136. package/src/SvTooltip.svelte +161 -161
  137. package/src/SvTour.svelte +208 -208
  138. package/src/SvTree.svelte +444 -444
  139. package/src/SvTreeSelect.svelte +239 -239
  140. package/src/a11y/dismissable.test.ts +119 -119
  141. package/src/a11y/dismissable.ts +114 -114
  142. package/src/a11y.contract.test.ts +49 -49
  143. package/src/a11y.test.ts +59 -59
  144. package/src/a11y.ts +61 -61
  145. package/src/ai.test.ts +502 -502
  146. package/src/ai.ts +1419 -1419
  147. package/src/build-api.coverage.test.ts +633 -633
  148. package/src/build-api.ts +846 -846
  149. package/src/builtin-editors.grid.test.ts +83 -83
  150. package/src/cell-formatting.ts +171 -171
  151. package/src/cell-render.test.ts +513 -513
  152. package/src/cell-render.ts +496 -496
  153. package/src/cell-values.ts +148 -148
  154. package/src/chart-export.ts +202 -202
  155. package/src/chart-view.svelte.ts +36 -36
  156. package/src/chart.ts +2321 -2321
  157. package/src/collaboration.test.ts +104 -104
  158. package/src/collaboration.ts +167 -167
  159. package/src/column-groups.ts +78 -78
  160. package/src/core.performance.test.ts +30 -30
  161. package/src/core.ts +1865 -1865
  162. package/src/createAutocomplete.svelte.ts +132 -132
  163. package/src/createCombobox.svelte.ts +191 -191
  164. package/src/createCountryInput.svelte.ts +157 -157
  165. package/src/createDropdownList.svelte.ts +168 -168
  166. package/src/createForm.svelte.ts +386 -386
  167. package/src/createGrid.svelte.ts +42 -42
  168. package/src/createGrid.test.ts +10 -10
  169. package/src/createGridState.svelte.ts +17 -17
  170. package/src/createListbox.svelte.ts +250 -250
  171. package/src/createMenu.svelte.ts +224 -224
  172. package/src/createPopoverSelect.svelte.ts +213 -213
  173. package/src/createSlider.svelte.ts +191 -191
  174. package/src/createTooltip.svelte.ts +144 -144
  175. package/src/createTree.svelte.ts +322 -322
  176. package/src/datetime/date-core.ts +206 -206
  177. package/src/datetime/date-restrict.ts +61 -61
  178. package/src/datetime/timezone.ts +135 -135
  179. package/src/dock-manager-model.ts +596 -596
  180. package/src/dock-model.ts +374 -374
  181. package/src/editing.test.ts +974 -974
  182. package/src/editing.ts +609 -609
  183. package/src/editor-contract.ts +171 -171
  184. package/src/editor-registry.grid.test.ts +144 -144
  185. package/src/editor-registry.ts +122 -122
  186. package/src/export-data-api.test.ts +126 -126
  187. package/src/export-format.test.ts +107 -107
  188. package/src/export-format.ts +601 -601
  189. package/src/filter-operators.ts +160 -160
  190. package/src/filtering/excel-filters.ts +325 -325
  191. package/src/flex-render.ts +3 -3
  192. package/src/form-field.ts +127 -127
  193. package/src/group-display.test.ts +167 -167
  194. package/src/group-display.ts +200 -200
  195. package/src/headless.ts +87 -87
  196. package/src/js-scroller.svelte.ts +173 -173
  197. package/src/keyboard-handlers.ts +270 -270
  198. package/src/keyboard.test.ts +59 -59
  199. package/src/keyboard.ts +97 -97
  200. package/src/list-nav.test.ts +49 -49
  201. package/src/list-nav.ts +29 -29
  202. package/src/list-option.test.ts +56 -56
  203. package/src/list-option.ts +179 -179
  204. package/src/menus.ts +597 -597
  205. package/src/merge-objects.ts +48 -48
  206. package/src/overlays.test.ts +90 -90
  207. package/src/positioning.ts +268 -268
  208. package/src/render-component.ts +28 -28
  209. package/src/row-drag-touch.ts +20 -5
  210. package/src/row-drag.test.ts +401 -353
  211. package/src/row-drag.ts +419 -415
  212. package/src/row-resize.test.ts +524 -524
  213. package/src/row-resize.ts +228 -228
  214. package/src/scheduler-ical.ts +181 -181
  215. package/src/scheduler-model.test.ts +562 -562
  216. package/src/scheduler-model.ts +873 -873
  217. package/src/selection.test.ts +885 -885
  218. package/src/server-data-source.test.ts +383 -383
  219. package/src/server-data-source.ts +469 -469
  220. package/src/sparkline.test.ts +68 -68
  221. package/src/sparkline.ts +169 -169
  222. package/src/spreadsheet.test.ts +488 -488
  223. package/src/spreadsheet.ts +312 -312
  224. package/src/static-functions.ts +11 -11
  225. package/src/subscribe.ts +38 -38
  226. package/src/summaries.ts +113 -113
  227. package/src/svgrid-wrapper.types.ts +563 -563
  228. package/src/svgrid.async-editor-options.test.ts +273 -273
  229. package/src/svgrid.auto-row-height.test.ts +204 -204
  230. package/src/svgrid.behavior.test.ts +910 -910
  231. package/src/svgrid.charting.test.ts +534 -534
  232. package/src/svgrid.comments-autocomplete.test.ts +127 -127
  233. package/src/svgrid.context-menu.test.ts +147 -147
  234. package/src/svgrid.features.test.ts +157 -157
  235. package/src/svgrid.filter-depth.test.ts +163 -163
  236. package/src/svgrid.filter-menu-listbox.svelte.test.ts +377 -377
  237. package/src/svgrid.filter-menu-scroll.test.ts +112 -112
  238. package/src/svgrid.grand-total.test.ts +188 -188
  239. package/src/svgrid.group-display-mode.test.ts +171 -171
  240. package/src/svgrid.group-footers.test.ts +121 -121
  241. package/src/svgrid.group-pagination.test.ts +153 -153
  242. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  243. package/src/svgrid.tree-data.test.ts +186 -186
  244. package/src/svgrid.wrapper.test.ts +63 -63
  245. package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
  246. package/src/test-setup.ts +62 -62
  247. package/src/themes/index.ts +288 -288
  248. package/src/toast-store.svelte.ts +250 -250
  249. package/src/toast-store.test.ts +147 -147
  250. package/src/tree-row-model.test.ts +168 -168
  251. package/src/ui-buttons.test.ts +144 -144
  252. package/src/ui-inputs.test.ts +118 -118
  253. package/src/ui-localization.test.ts +113 -113
  254. package/src/ui-range.test.ts +70 -70
  255. package/src/ui-selection.test.ts +155 -155
  256. package/src/ui-tier1.test.ts +142 -142
  257. package/src/virtual.test.ts +88 -88
  258. package/src/virtualization/column-virtualizer.test.ts +27 -27
  259. package/src/virtualization/column-virtualizer.ts +30 -30
  260. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  261. package/src/virtualization/types.ts +30 -30
  262. package/src/virtualization/virtualizer.test.ts +47 -47
  263. package/src/virtualization/virtualizer.ts +322 -322
package/src/dock-model.ts CHANGED
@@ -1,374 +1,374 @@
1
- /**
2
- * dock-model - the pure, immutable tree behind <SvDockLayout>. A docking layout
3
- * is a tree of two node kinds:
4
- *
5
- * group a row/column of children (nested groups or tab-leaves), each holding
6
- * a normalized `size` weight, with resizable splitters between them.
7
- * tabs a leaf: one or more panes shown as tabs (only `active` is visible).
8
- *
9
- * Every restructuring a user does by dragging - dock a pane to a neighbour's
10
- * edge (split) or centre (add as a tab), close a pane, drag a splitter - is a
11
- * pure transform here that returns a NEW tree. The Svelte view owns the DOM and
12
- * the drag gestures; all the tree surgery lives here so it is unit-testable
13
- * without a browser. Modelled on Smart's `smart-layout` group/item structure.
14
- */
15
-
16
- /** One dockable pane: its id, title, and the state a tab needs to render. */
17
- export type DockPane = {
18
- id: string
19
- title: string
20
- /** Show a close (x) affordance on the tab. Default true. */
21
- closable?: boolean
22
- /** Minimum size (px) for this pane's leaf along the split axis. */
23
- minSize?: number
24
- }
25
-
26
- /** A leaf: a stack of panes rendered as tabs (`active` is the visible one). */
27
- export type DockTabs = {
28
- type: 'tabs'
29
- id: string
30
- panes: DockPane[]
31
- active: number
32
- }
33
-
34
- /** A row (side-by-side) or column (stacked) of children with per-child sizes. */
35
- export type DockGroup = {
36
- type: 'group'
37
- id: string
38
- direction: 'row' | 'column'
39
- children: DockNode[]
40
- /** Size weight per child; parallel to `children`, normalized to sum ~1. */
41
- sizes: number[]
42
- }
43
-
44
- /** A node in the layout tree - either a split group or a tabbed leaf. */
45
- export type DockNode = DockGroup | DockTabs
46
-
47
- /** Where a dragged pane lands relative to a target leaf. */
48
- export type DockZone = 'left' | 'right' | 'top' | 'bottom' | 'center'
49
-
50
- /** Supplies ids for newly created nodes, so layouts stay deterministic in tests. */
51
- export type IdGen = () => string
52
-
53
- // ---- construction helpers -------------------------------------------------
54
-
55
- /** Build a {@link DockPane}. */
56
- export function pane(id: string, title: string, closable = true): DockPane {
57
- return { id, title, closable }
58
- }
59
-
60
- /** Largest per-pane `minSize` in a leaf (0 if none). Used by splitter clamping. */
61
- export function leafMinSize(node: DockTabs): number {
62
- return node.panes.reduce((m, p) => Math.max(m, p.minSize ?? 0), 0)
63
- }
64
-
65
- /** Build a tabbed leaf holding the given panes. */
66
- export function tabs(genId: IdGen, panes: DockPane[], active = 0): DockTabs {
67
- return { type: 'tabs', id: genId(), panes, active: clampIndex(active, panes.length) }
68
- }
69
-
70
- /** Build a split group: panes or nested groups laid out in a row or column. */
71
- export function group(
72
- genId: IdGen,
73
- direction: 'row' | 'column',
74
- children: DockNode[],
75
- sizes?: number[],
76
- ): DockGroup {
77
- return { type: 'group', id: genId(), direction, children, sizes: normalizeSizes(sizes, children.length) }
78
- }
79
-
80
- // ---- small utilities ------------------------------------------------------
81
-
82
- function clampIndex(i: number, len: number): number {
83
- if (len <= 0) return 0
84
- return Math.max(0, Math.min(i, len - 1))
85
- }
86
-
87
- /** Return sizes summing to 1, padded/trimmed to `count`. Even split when absent. */
88
- export function normalizeSizes(sizes: number[] | undefined, count: number): number[] {
89
- if (count <= 0) return []
90
- const src = sizes && sizes.length === count ? sizes.slice() : new Array(count).fill(1)
91
- const total = src.reduce((a, b) => a + (b > 0 ? b : 0), 0)
92
- if (total <= 0) return new Array(count).fill(1 / count)
93
- return src.map((s) => (s > 0 ? s : 0) / total)
94
- }
95
-
96
- // ---- queries --------------------------------------------------------------
97
-
98
- /** The tabs leaf that holds `paneId`, or null. */
99
- export function findTabsWithPane(root: DockNode, paneId: string): DockTabs | null {
100
- if (root.type === 'tabs') return root.panes.some((p) => p.id === paneId) ? root : null
101
- for (const c of root.children) {
102
- const hit = findTabsWithPane(c, paneId)
103
- if (hit) return hit
104
- }
105
- return null
106
- }
107
-
108
- /** Every pane id in the tree, in document order. */
109
- export function allPaneIds(root: DockNode): string[] {
110
- if (root.type === 'tabs') return root.panes.map((p) => p.id)
111
- return root.children.flatMap(allPaneIds)
112
- }
113
-
114
- // ---- normalization --------------------------------------------------------
115
-
116
- /**
117
- * Collapse the tree after edits: drop empty tabs and empty groups, unwrap a
118
- * group that has a single child, and flatten a child group into its parent when
119
- * they share a direction (so repeated docking never nests infinitely deep).
120
- * Returns the tidied node, or null when nothing is left.
121
- */
122
- export function normalize(node: DockNode): DockNode | null {
123
- if (node.type === 'tabs') {
124
- if (node.panes.length === 0) return null
125
- return { ...node, active: clampIndex(node.active, node.panes.length) }
126
- }
127
-
128
- // Normalize children first, dropping the empties.
129
- const kept: DockNode[] = []
130
- const keptSizes: number[] = []
131
- node.children.forEach((child, i) => {
132
- const n = normalize(child)
133
- if (n) {
134
- kept.push(n)
135
- keptSizes.push(node.sizes[i] ?? 1)
136
- }
137
- })
138
-
139
- if (kept.length === 0) return null
140
- if (kept.length === 1) return kept[0]! // unwrap single-child group
141
-
142
- // Flatten same-direction child groups into this one.
143
- const flatChildren: DockNode[] = []
144
- const flatSizes: number[] = []
145
- kept.forEach((child, i) => {
146
- if (child.type === 'group' && child.direction === node.direction) {
147
- const share = keptSizes[i]!
148
- child.children.forEach((gc, j) => {
149
- flatChildren.push(gc)
150
- flatSizes.push(share * (child.sizes[j] ?? 1 / child.children.length))
151
- })
152
- } else {
153
- flatChildren.push(child)
154
- flatSizes.push(keptSizes[i]!)
155
- }
156
- })
157
-
158
- return { ...node, children: flatChildren, sizes: normalizeSizes(flatSizes, flatChildren.length) }
159
- }
160
-
161
- // ---- mutations (return a new tree) ----------------------------------------
162
-
163
- /** Set the active tab index of a tabs leaf. */
164
- export function setActive(root: DockNode, tabsId: string, active: number): DockNode {
165
- return mapNodes(root, (n) =>
166
- n.type === 'tabs' && n.id === tabsId ? { ...n, active: clampIndex(active, n.panes.length) } : n,
167
- )
168
- }
169
-
170
- /** Replace a group's size weights (e.g. after a splitter drag). */
171
- export function setSizes(root: DockNode, groupId: string, sizes: number[]): DockNode {
172
- return mapNodes(root, (n) =>
173
- n.type === 'group' && n.id === groupId ? { ...n, sizes: normalizeSizes(sizes, n.children.length) } : n,
174
- )
175
- }
176
-
177
- /**
178
- * Reorder a pane WITHIN its tabs leaf (dragging a tab along the strip). `active`
179
- * follows the pane that was showing. No-op if the indices are out of range.
180
- */
181
- export function reorderPane(root: DockNode, tabsId: string, from: number, to: number): DockNode {
182
- return mapNodes(root, (n) => {
183
- if (n.type !== 'tabs' || n.id !== tabsId) return n
184
- if (from < 0 || from >= n.panes.length) return n
185
- const showing = n.panes[n.active]?.id
186
- const panes = n.panes.slice()
187
- const [moved] = panes.splice(from, 1)
188
- if (!moved) return n
189
- // `to` is an insertion point in [0, length] (length = append), not a select
190
- // index - clampIndex would wrongly cap it at length-1.
191
- panes.splice(Math.max(0, Math.min(to, panes.length)), 0, moved)
192
- const active = panes.findIndex((p) => p.id === showing)
193
- return { ...n, panes, active: active < 0 ? 0 : active }
194
- })
195
- }
196
-
197
- /**
198
- * Remove a pane. Returns the tidied tree (or null if it emptied) plus the pane
199
- * that was pulled out, so a caller can re-dock it elsewhere.
200
- */
201
- export function removePane(root: DockNode, paneId: string): { root: DockNode | null; pane: DockPane | null } {
202
- let removed: DockPane | null = null
203
- const strip = (node: DockNode): DockNode => {
204
- if (node.type === 'tabs') {
205
- const idx = node.panes.findIndex((p) => p.id === paneId)
206
- if (idx < 0) return node
207
- removed = node.panes[idx]!
208
- const panes = node.panes.filter((_, i) => i !== idx)
209
- return { ...node, panes, active: clampIndex(node.active > idx ? node.active - 1 : node.active, panes.length) }
210
- }
211
- return { ...node, children: node.children.map(strip) }
212
- }
213
- const stripped = strip(root)
214
- return { root: normalize(stripped), pane: removed }
215
- }
216
-
217
- /**
218
- * Dock `movingPane` relative to the leaf `targetTabsId`. `center` adds it as a
219
- * tab; an edge splits: the target is wrapped in (or extended into) a group whose
220
- * direction matches the edge, with the new pane on the requested side.
221
- */
222
- export function dockInto(
223
- root: DockNode,
224
- targetTabsId: string,
225
- movingPane: DockPane,
226
- zone: DockZone,
227
- genId: IdGen,
228
- ): DockNode {
229
- const dir: 'row' | 'column' = zone === 'left' || zone === 'right' ? 'row' : 'column'
230
- const before = zone === 'left' || zone === 'top'
231
-
232
- const transform = (node: DockNode): DockNode => {
233
- if (node.type === 'tabs') {
234
- if (node.id !== targetTabsId) return node
235
- if (zone === 'center') {
236
- const panes = [...node.panes, movingPane]
237
- return { ...node, panes, active: panes.length - 1 }
238
- }
239
- // Edge on a bare leaf: wrap it in a new group with the new leaf beside it.
240
- const leaf = tabs(genId, [movingPane], 0)
241
- const kids = before ? [leaf, node] : [node, leaf]
242
- return group(genId, dir, kids)
243
- }
244
-
245
- // Group: if it directly contains the target leaf and matches the edge
246
- // direction, insert a sibling in place (keeps the tree flat).
247
- const idx = node.children.findIndex((c) => c.type === 'tabs' && c.id === targetTabsId)
248
- if (idx >= 0 && zone !== 'center' && node.direction === dir) {
249
- const leaf = tabs(genId, [movingPane], 0)
250
- const children = node.children.slice()
251
- const sizes = node.sizes.slice()
252
- const half = (node.sizes[idx] ?? 1 / node.children.length) / 2
253
- const insertAt = before ? idx : idx + 1
254
- children.splice(insertAt, 0, leaf)
255
- sizes.splice(idx, 1, half, half) // split the target's slot in two
256
- // splice above replaced 1 with 2 at idx; the new leaf takes `before`/after
257
- if (before) {
258
- // children: [..., leaf, target, ...]; sizes already [half(leaf), half(target)]
259
- } else {
260
- // we inserted leaf AFTER target but sizes are [half, half] at idx/idx+1 - correct
261
- }
262
- return { ...node, children, sizes: normalizeSizes(sizes, children.length) }
263
- }
264
-
265
- return { ...node, children: node.children.map(transform) }
266
- }
267
-
268
- return normalize(transform(root)) ?? root
269
- }
270
-
271
- /** Pull a whole tabs leaf out of the tree (for auto-hide). Returns the tidied
272
- * tree (or null) plus the extracted leaf. */
273
- export function removeLeaf(root: DockNode, tabsId: string): { root: DockNode | null; leaf: DockTabs | null } {
274
- let taken: DockTabs | null = null
275
- const strip = (node: DockNode): DockNode | null => {
276
- if (node.type === 'tabs') {
277
- if (node.id === tabsId) { taken = node; return null }
278
- return node
279
- }
280
- const kids: DockNode[] = []
281
- const sizes: number[] = []
282
- node.children.forEach((c, i) => {
283
- const n = strip(c)
284
- if (n) { kids.push(n); sizes.push(node.sizes[i] ?? 1) }
285
- })
286
- if (kids.length === 0) return null
287
- return { ...node, children: kids, sizes: normalizeSizes(sizes, kids.length) }
288
- }
289
- const stripped = strip(root)
290
- return { root: stripped ? normalize(stripped) : null, leaf: taken }
291
- }
292
-
293
- /** Dock a whole leaf against an EDGE of the entire tree (for pinning an
294
- * auto-hidden panel back in). Wraps the root in a new group as needed; the
295
- * pinned leaf takes `fraction` of that axis (clamped 0.1..0.6), the rest keeps
296
- * the existing content. */
297
- export function dockLeafToEdge(
298
- root: DockNode | null,
299
- leaf: DockTabs,
300
- side: Exclude<DockZone, 'center'>,
301
- genId: IdGen,
302
- fraction = 0.25,
303
- ): DockNode {
304
- if (!root) return leaf
305
- const dir: 'row' | 'column' = side === 'left' || side === 'right' ? 'row' : 'column'
306
- const before = side === 'left' || side === 'top'
307
- const f = Math.min(0.6, Math.max(0.1, fraction))
308
- const kids = before ? [leaf, root] : [root, leaf]
309
- const sizes = before ? [f, 1 - f] : [1 - f, f]
310
- return normalize(group(genId, dir, kids, sizes)) ?? leaf
311
- }
312
-
313
- /** Move an existing pane to a new dock position (remove then dock). */
314
- export function movePane(
315
- root: DockNode,
316
- paneId: string,
317
- targetTabsId: string,
318
- zone: DockZone,
319
- genId: IdGen,
320
- ): DockNode {
321
- const { root: without, pane: moved } = removePane(root, paneId)
322
- if (!without || !moved) return root
323
- // If removing the pane dissolved the target leaf (it was the pane's own
324
- // single-pane leaf), there is nothing to dock against - leave the tree as-is.
325
- if (!containsTabs(without, targetTabsId)) return root
326
- return dockInto(without, targetTabsId, moved, zone, genId)
327
- }
328
-
329
- function containsTabs(node: DockNode, tabsId: string): boolean {
330
- if (node.type === 'tabs') return node.id === tabsId
331
- return node.children.some((c) => containsTabs(c, tabsId))
332
- }
333
-
334
- // ---- internal -------------------------------------------------------------
335
-
336
- /** Map over every node, replacing where `fn` returns a new object. */
337
- function mapNodes(node: DockNode, fn: (n: DockNode) => DockNode): DockNode {
338
- const mapped = fn(node)
339
- if (mapped.type === 'group') {
340
- return { ...mapped, children: mapped.children.map((c) => mapNodes(c, fn)) }
341
- }
342
- return mapped
343
- }
344
-
345
- // ---- consumer builders (auto ids) -----------------------------------------
346
- // Ergonomic constructors for building an initial layout without threading an
347
- // id generator. Node ids only need to be unique; a running counter suffices.
348
-
349
- let AUTO = 0
350
- const autoId: IdGen = () => `d${AUTO++}`
351
-
352
- /** A pane (tab). Alias of `pane`, named for the public builder set. */
353
- export function dockPane(
354
- id: string,
355
- title: string,
356
- opts?: boolean | { closable?: boolean; minSize?: number },
357
- ): DockPane {
358
- const o = typeof opts === 'boolean' ? { closable: opts } : (opts ?? {})
359
- return { id, title, closable: o.closable ?? true, ...(o.minSize != null ? { minSize: o.minSize } : {}) }
360
- }
361
-
362
- /** A tab-leaf holding one or more panes. */
363
- export function dockTabs(panes: DockPane[], active = 0): DockTabs {
364
- return tabs(autoId, panes, active)
365
- }
366
-
367
- /** A row (side-by-side) or column (stacked) of children. */
368
- export function dockGroup(
369
- direction: 'row' | 'column',
370
- children: DockNode[],
371
- sizes?: number[],
372
- ): DockGroup {
373
- return group(autoId, direction, children, sizes)
374
- }
1
+ /**
2
+ * dock-model - the pure, immutable tree behind <SvDockLayout>. A docking layout
3
+ * is a tree of two node kinds:
4
+ *
5
+ * group a row/column of children (nested groups or tab-leaves), each holding
6
+ * a normalized `size` weight, with resizable splitters between them.
7
+ * tabs a leaf: one or more panes shown as tabs (only `active` is visible).
8
+ *
9
+ * Every restructuring a user does by dragging - dock a pane to a neighbour's
10
+ * edge (split) or centre (add as a tab), close a pane, drag a splitter - is a
11
+ * pure transform here that returns a NEW tree. The Svelte view owns the DOM and
12
+ * the drag gestures; all the tree surgery lives here so it is unit-testable
13
+ * without a browser. Modelled on Smart's `smart-layout` group/item structure.
14
+ */
15
+
16
+ /** One dockable pane: its id, title, and the state a tab needs to render. */
17
+ export type DockPane = {
18
+ id: string
19
+ title: string
20
+ /** Show a close (x) affordance on the tab. Default true. */
21
+ closable?: boolean
22
+ /** Minimum size (px) for this pane's leaf along the split axis. */
23
+ minSize?: number
24
+ }
25
+
26
+ /** A leaf: a stack of panes rendered as tabs (`active` is the visible one). */
27
+ export type DockTabs = {
28
+ type: 'tabs'
29
+ id: string
30
+ panes: DockPane[]
31
+ active: number
32
+ }
33
+
34
+ /** A row (side-by-side) or column (stacked) of children with per-child sizes. */
35
+ export type DockGroup = {
36
+ type: 'group'
37
+ id: string
38
+ direction: 'row' | 'column'
39
+ children: DockNode[]
40
+ /** Size weight per child; parallel to `children`, normalized to sum ~1. */
41
+ sizes: number[]
42
+ }
43
+
44
+ /** A node in the layout tree - either a split group or a tabbed leaf. */
45
+ export type DockNode = DockGroup | DockTabs
46
+
47
+ /** Where a dragged pane lands relative to a target leaf. */
48
+ export type DockZone = 'left' | 'right' | 'top' | 'bottom' | 'center'
49
+
50
+ /** Supplies ids for newly created nodes, so layouts stay deterministic in tests. */
51
+ export type IdGen = () => string
52
+
53
+ // ---- construction helpers -------------------------------------------------
54
+
55
+ /** Build a {@link DockPane}. */
56
+ export function pane(id: string, title: string, closable = true): DockPane {
57
+ return { id, title, closable }
58
+ }
59
+
60
+ /** Largest per-pane `minSize` in a leaf (0 if none). Used by splitter clamping. */
61
+ export function leafMinSize(node: DockTabs): number {
62
+ return node.panes.reduce((m, p) => Math.max(m, p.minSize ?? 0), 0)
63
+ }
64
+
65
+ /** Build a tabbed leaf holding the given panes. */
66
+ export function tabs(genId: IdGen, panes: DockPane[], active = 0): DockTabs {
67
+ return { type: 'tabs', id: genId(), panes, active: clampIndex(active, panes.length) }
68
+ }
69
+
70
+ /** Build a split group: panes or nested groups laid out in a row or column. */
71
+ export function group(
72
+ genId: IdGen,
73
+ direction: 'row' | 'column',
74
+ children: DockNode[],
75
+ sizes?: number[],
76
+ ): DockGroup {
77
+ return { type: 'group', id: genId(), direction, children, sizes: normalizeSizes(sizes, children.length) }
78
+ }
79
+
80
+ // ---- small utilities ------------------------------------------------------
81
+
82
+ function clampIndex(i: number, len: number): number {
83
+ if (len <= 0) return 0
84
+ return Math.max(0, Math.min(i, len - 1))
85
+ }
86
+
87
+ /** Return sizes summing to 1, padded/trimmed to `count`. Even split when absent. */
88
+ export function normalizeSizes(sizes: number[] | undefined, count: number): number[] {
89
+ if (count <= 0) return []
90
+ const src = sizes && sizes.length === count ? sizes.slice() : new Array(count).fill(1)
91
+ const total = src.reduce((a, b) => a + (b > 0 ? b : 0), 0)
92
+ if (total <= 0) return new Array(count).fill(1 / count)
93
+ return src.map((s) => (s > 0 ? s : 0) / total)
94
+ }
95
+
96
+ // ---- queries --------------------------------------------------------------
97
+
98
+ /** The tabs leaf that holds `paneId`, or null. */
99
+ export function findTabsWithPane(root: DockNode, paneId: string): DockTabs | null {
100
+ if (root.type === 'tabs') return root.panes.some((p) => p.id === paneId) ? root : null
101
+ for (const c of root.children) {
102
+ const hit = findTabsWithPane(c, paneId)
103
+ if (hit) return hit
104
+ }
105
+ return null
106
+ }
107
+
108
+ /** Every pane id in the tree, in document order. */
109
+ export function allPaneIds(root: DockNode): string[] {
110
+ if (root.type === 'tabs') return root.panes.map((p) => p.id)
111
+ return root.children.flatMap(allPaneIds)
112
+ }
113
+
114
+ // ---- normalization --------------------------------------------------------
115
+
116
+ /**
117
+ * Collapse the tree after edits: drop empty tabs and empty groups, unwrap a
118
+ * group that has a single child, and flatten a child group into its parent when
119
+ * they share a direction (so repeated docking never nests infinitely deep).
120
+ * Returns the tidied node, or null when nothing is left.
121
+ */
122
+ export function normalize(node: DockNode): DockNode | null {
123
+ if (node.type === 'tabs') {
124
+ if (node.panes.length === 0) return null
125
+ return { ...node, active: clampIndex(node.active, node.panes.length) }
126
+ }
127
+
128
+ // Normalize children first, dropping the empties.
129
+ const kept: DockNode[] = []
130
+ const keptSizes: number[] = []
131
+ node.children.forEach((child, i) => {
132
+ const n = normalize(child)
133
+ if (n) {
134
+ kept.push(n)
135
+ keptSizes.push(node.sizes[i] ?? 1)
136
+ }
137
+ })
138
+
139
+ if (kept.length === 0) return null
140
+ if (kept.length === 1) return kept[0]! // unwrap single-child group
141
+
142
+ // Flatten same-direction child groups into this one.
143
+ const flatChildren: DockNode[] = []
144
+ const flatSizes: number[] = []
145
+ kept.forEach((child, i) => {
146
+ if (child.type === 'group' && child.direction === node.direction) {
147
+ const share = keptSizes[i]!
148
+ child.children.forEach((gc, j) => {
149
+ flatChildren.push(gc)
150
+ flatSizes.push(share * (child.sizes[j] ?? 1 / child.children.length))
151
+ })
152
+ } else {
153
+ flatChildren.push(child)
154
+ flatSizes.push(keptSizes[i]!)
155
+ }
156
+ })
157
+
158
+ return { ...node, children: flatChildren, sizes: normalizeSizes(flatSizes, flatChildren.length) }
159
+ }
160
+
161
+ // ---- mutations (return a new tree) ----------------------------------------
162
+
163
+ /** Set the active tab index of a tabs leaf. */
164
+ export function setActive(root: DockNode, tabsId: string, active: number): DockNode {
165
+ return mapNodes(root, (n) =>
166
+ n.type === 'tabs' && n.id === tabsId ? { ...n, active: clampIndex(active, n.panes.length) } : n,
167
+ )
168
+ }
169
+
170
+ /** Replace a group's size weights (e.g. after a splitter drag). */
171
+ export function setSizes(root: DockNode, groupId: string, sizes: number[]): DockNode {
172
+ return mapNodes(root, (n) =>
173
+ n.type === 'group' && n.id === groupId ? { ...n, sizes: normalizeSizes(sizes, n.children.length) } : n,
174
+ )
175
+ }
176
+
177
+ /**
178
+ * Reorder a pane WITHIN its tabs leaf (dragging a tab along the strip). `active`
179
+ * follows the pane that was showing. No-op if the indices are out of range.
180
+ */
181
+ export function reorderPane(root: DockNode, tabsId: string, from: number, to: number): DockNode {
182
+ return mapNodes(root, (n) => {
183
+ if (n.type !== 'tabs' || n.id !== tabsId) return n
184
+ if (from < 0 || from >= n.panes.length) return n
185
+ const showing = n.panes[n.active]?.id
186
+ const panes = n.panes.slice()
187
+ const [moved] = panes.splice(from, 1)
188
+ if (!moved) return n
189
+ // `to` is an insertion point in [0, length] (length = append), not a select
190
+ // index - clampIndex would wrongly cap it at length-1.
191
+ panes.splice(Math.max(0, Math.min(to, panes.length)), 0, moved)
192
+ const active = panes.findIndex((p) => p.id === showing)
193
+ return { ...n, panes, active: active < 0 ? 0 : active }
194
+ })
195
+ }
196
+
197
+ /**
198
+ * Remove a pane. Returns the tidied tree (or null if it emptied) plus the pane
199
+ * that was pulled out, so a caller can re-dock it elsewhere.
200
+ */
201
+ export function removePane(root: DockNode, paneId: string): { root: DockNode | null; pane: DockPane | null } {
202
+ let removed: DockPane | null = null
203
+ const strip = (node: DockNode): DockNode => {
204
+ if (node.type === 'tabs') {
205
+ const idx = node.panes.findIndex((p) => p.id === paneId)
206
+ if (idx < 0) return node
207
+ removed = node.panes[idx]!
208
+ const panes = node.panes.filter((_, i) => i !== idx)
209
+ return { ...node, panes, active: clampIndex(node.active > idx ? node.active - 1 : node.active, panes.length) }
210
+ }
211
+ return { ...node, children: node.children.map(strip) }
212
+ }
213
+ const stripped = strip(root)
214
+ return { root: normalize(stripped), pane: removed }
215
+ }
216
+
217
+ /**
218
+ * Dock `movingPane` relative to the leaf `targetTabsId`. `center` adds it as a
219
+ * tab; an edge splits: the target is wrapped in (or extended into) a group whose
220
+ * direction matches the edge, with the new pane on the requested side.
221
+ */
222
+ export function dockInto(
223
+ root: DockNode,
224
+ targetTabsId: string,
225
+ movingPane: DockPane,
226
+ zone: DockZone,
227
+ genId: IdGen,
228
+ ): DockNode {
229
+ const dir: 'row' | 'column' = zone === 'left' || zone === 'right' ? 'row' : 'column'
230
+ const before = zone === 'left' || zone === 'top'
231
+
232
+ const transform = (node: DockNode): DockNode => {
233
+ if (node.type === 'tabs') {
234
+ if (node.id !== targetTabsId) return node
235
+ if (zone === 'center') {
236
+ const panes = [...node.panes, movingPane]
237
+ return { ...node, panes, active: panes.length - 1 }
238
+ }
239
+ // Edge on a bare leaf: wrap it in a new group with the new leaf beside it.
240
+ const leaf = tabs(genId, [movingPane], 0)
241
+ const kids = before ? [leaf, node] : [node, leaf]
242
+ return group(genId, dir, kids)
243
+ }
244
+
245
+ // Group: if it directly contains the target leaf and matches the edge
246
+ // direction, insert a sibling in place (keeps the tree flat).
247
+ const idx = node.children.findIndex((c) => c.type === 'tabs' && c.id === targetTabsId)
248
+ if (idx >= 0 && zone !== 'center' && node.direction === dir) {
249
+ const leaf = tabs(genId, [movingPane], 0)
250
+ const children = node.children.slice()
251
+ const sizes = node.sizes.slice()
252
+ const half = (node.sizes[idx] ?? 1 / node.children.length) / 2
253
+ const insertAt = before ? idx : idx + 1
254
+ children.splice(insertAt, 0, leaf)
255
+ sizes.splice(idx, 1, half, half) // split the target's slot in two
256
+ // splice above replaced 1 with 2 at idx; the new leaf takes `before`/after
257
+ if (before) {
258
+ // children: [..., leaf, target, ...]; sizes already [half(leaf), half(target)]
259
+ } else {
260
+ // we inserted leaf AFTER target but sizes are [half, half] at idx/idx+1 - correct
261
+ }
262
+ return { ...node, children, sizes: normalizeSizes(sizes, children.length) }
263
+ }
264
+
265
+ return { ...node, children: node.children.map(transform) }
266
+ }
267
+
268
+ return normalize(transform(root)) ?? root
269
+ }
270
+
271
+ /** Pull a whole tabs leaf out of the tree (for auto-hide). Returns the tidied
272
+ * tree (or null) plus the extracted leaf. */
273
+ export function removeLeaf(root: DockNode, tabsId: string): { root: DockNode | null; leaf: DockTabs | null } {
274
+ let taken: DockTabs | null = null
275
+ const strip = (node: DockNode): DockNode | null => {
276
+ if (node.type === 'tabs') {
277
+ if (node.id === tabsId) { taken = node; return null }
278
+ return node
279
+ }
280
+ const kids: DockNode[] = []
281
+ const sizes: number[] = []
282
+ node.children.forEach((c, i) => {
283
+ const n = strip(c)
284
+ if (n) { kids.push(n); sizes.push(node.sizes[i] ?? 1) }
285
+ })
286
+ if (kids.length === 0) return null
287
+ return { ...node, children: kids, sizes: normalizeSizes(sizes, kids.length) }
288
+ }
289
+ const stripped = strip(root)
290
+ return { root: stripped ? normalize(stripped) : null, leaf: taken }
291
+ }
292
+
293
+ /** Dock a whole leaf against an EDGE of the entire tree (for pinning an
294
+ * auto-hidden panel back in). Wraps the root in a new group as needed; the
295
+ * pinned leaf takes `fraction` of that axis (clamped 0.1..0.6), the rest keeps
296
+ * the existing content. */
297
+ export function dockLeafToEdge(
298
+ root: DockNode | null,
299
+ leaf: DockTabs,
300
+ side: Exclude<DockZone, 'center'>,
301
+ genId: IdGen,
302
+ fraction = 0.25,
303
+ ): DockNode {
304
+ if (!root) return leaf
305
+ const dir: 'row' | 'column' = side === 'left' || side === 'right' ? 'row' : 'column'
306
+ const before = side === 'left' || side === 'top'
307
+ const f = Math.min(0.6, Math.max(0.1, fraction))
308
+ const kids = before ? [leaf, root] : [root, leaf]
309
+ const sizes = before ? [f, 1 - f] : [1 - f, f]
310
+ return normalize(group(genId, dir, kids, sizes)) ?? leaf
311
+ }
312
+
313
+ /** Move an existing pane to a new dock position (remove then dock). */
314
+ export function movePane(
315
+ root: DockNode,
316
+ paneId: string,
317
+ targetTabsId: string,
318
+ zone: DockZone,
319
+ genId: IdGen,
320
+ ): DockNode {
321
+ const { root: without, pane: moved } = removePane(root, paneId)
322
+ if (!without || !moved) return root
323
+ // If removing the pane dissolved the target leaf (it was the pane's own
324
+ // single-pane leaf), there is nothing to dock against - leave the tree as-is.
325
+ if (!containsTabs(without, targetTabsId)) return root
326
+ return dockInto(without, targetTabsId, moved, zone, genId)
327
+ }
328
+
329
+ function containsTabs(node: DockNode, tabsId: string): boolean {
330
+ if (node.type === 'tabs') return node.id === tabsId
331
+ return node.children.some((c) => containsTabs(c, tabsId))
332
+ }
333
+
334
+ // ---- internal -------------------------------------------------------------
335
+
336
+ /** Map over every node, replacing where `fn` returns a new object. */
337
+ function mapNodes(node: DockNode, fn: (n: DockNode) => DockNode): DockNode {
338
+ const mapped = fn(node)
339
+ if (mapped.type === 'group') {
340
+ return { ...mapped, children: mapped.children.map((c) => mapNodes(c, fn)) }
341
+ }
342
+ return mapped
343
+ }
344
+
345
+ // ---- consumer builders (auto ids) -----------------------------------------
346
+ // Ergonomic constructors for building an initial layout without threading an
347
+ // id generator. Node ids only need to be unique; a running counter suffices.
348
+
349
+ let AUTO = 0
350
+ const autoId: IdGen = () => `d${AUTO++}`
351
+
352
+ /** A pane (tab). Alias of `pane`, named for the public builder set. */
353
+ export function dockPane(
354
+ id: string,
355
+ title: string,
356
+ opts?: boolean | { closable?: boolean; minSize?: number },
357
+ ): DockPane {
358
+ const o = typeof opts === 'boolean' ? { closable: opts } : (opts ?? {})
359
+ return { id, title, closable: o.closable ?? true, ...(o.minSize != null ? { minSize: o.minSize } : {}) }
360
+ }
361
+
362
+ /** A tab-leaf holding one or more panes. */
363
+ export function dockTabs(panes: DockPane[], active = 0): DockTabs {
364
+ return tabs(autoId, panes, active)
365
+ }
366
+
367
+ /** A row (side-by-side) or column (stacked) of children. */
368
+ export function dockGroup(
369
+ direction: 'row' | 'column',
370
+ children: DockNode[],
371
+ sizes?: number[],
372
+ ): DockGroup {
373
+ return group(autoId, direction, children, sizes)
374
+ }