@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.
Files changed (295) hide show
  1. package/README.md +9 -2
  2. package/dist/DockNodeView.svelte +306 -0
  3. package/dist/DockNodeView.svelte.d.ts +7 -0
  4. package/dist/DockPopoutHost.svelte +17 -0
  5. package/dist/DockPopoutHost.svelte.d.ts +14 -0
  6. package/dist/FlexRender.svelte +96 -96
  7. package/dist/GridFooter.svelte +178 -178
  8. package/dist/GridMenus.svelte +83 -3
  9. package/dist/SvAutoComplete.svelte +5 -3
  10. package/dist/SvCarousel.svelte +141 -163
  11. package/dist/SvColorInput.svelte +187 -186
  12. package/dist/SvColorInput.svelte.d.ts +1 -1
  13. package/dist/SvComboBox.svelte +7 -3
  14. package/dist/SvCommand.svelte +28 -85
  15. package/dist/SvContextMenu.svelte +116 -116
  16. package/dist/SvCountryInput.svelte +1 -1
  17. package/dist/SvDateRangeInput.svelte +1 -1
  18. package/dist/SvDateTimePicker.svelte +9 -3
  19. package/dist/SvDateTimePicker.svelte.d.ts +3 -0
  20. package/dist/SvDockLayout.svelte +244 -0
  21. package/dist/SvDockLayout.svelte.d.ts +77 -0
  22. package/dist/SvDockManager.svelte +922 -0
  23. package/dist/SvDockManager.svelte.d.ts +106 -0
  24. package/dist/SvDrawer.svelte +250 -228
  25. package/dist/SvDrawer.svelte.d.ts +4 -0
  26. package/dist/SvDropDownList.svelte +85 -13
  27. package/dist/SvDropDownList.svelte.d.ts +3 -0
  28. package/dist/SvField.svelte +21 -4
  29. package/dist/SvField.svelte.d.ts +1 -0
  30. package/dist/SvForm.svelte +201 -91
  31. package/dist/SvForm.svelte.d.ts +15 -22
  32. package/dist/SvGrid.controller.svelte.d.ts +32 -1
  33. package/dist/SvGrid.controller.svelte.js +156 -27
  34. package/dist/SvGrid.css +193 -5
  35. package/dist/SvGrid.svelte +3131 -2786
  36. package/dist/SvGrid.types.d.ts +452 -1
  37. package/dist/SvGridBoard.svelte +2339 -2339
  38. package/dist/SvGridChart.svelte +1724 -1724
  39. package/dist/SvGridChartPanel.svelte +485 -485
  40. package/dist/SvGridDropdown.svelte +689 -682
  41. package/dist/SvGridSelect.svelte +1 -0
  42. package/dist/SvGroupCell.svelte +109 -109
  43. package/dist/SvListBox.svelte +36 -14
  44. package/dist/SvMaskedInput.svelte +4 -3
  45. package/dist/SvMaskedInput.svelte.d.ts +1 -1
  46. package/dist/SvMenu.svelte +124 -124
  47. package/dist/SvMenuList.svelte +117 -165
  48. package/dist/SvMenuList.svelte.d.ts +2 -13
  49. package/dist/SvModal.svelte +12 -29
  50. package/dist/SvMultiSelect.svelte +1 -0
  51. package/dist/SvNumberInput.svelte +131 -130
  52. package/dist/SvNumberInput.svelte.d.ts +1 -1
  53. package/dist/SvOtpInput.svelte +158 -158
  54. package/dist/SvPagination.svelte +18 -18
  55. package/dist/SvPasswordInput.svelte +3 -2
  56. package/dist/SvPasswordInput.svelte.d.ts +1 -1
  57. package/dist/SvPhoneInput.svelte +3 -2
  58. package/dist/SvPhoneInput.svelte.d.ts +1 -1
  59. package/dist/SvPopover.svelte +139 -139
  60. package/dist/SvRowGroupPanel.svelte +170 -170
  61. package/dist/SvScrollArea.svelte +61 -61
  62. package/dist/SvStepper.svelte +6 -9
  63. package/dist/SvTagsInput.svelte +3 -2
  64. package/dist/SvTagsInput.svelte.d.ts +1 -1
  65. package/dist/SvTooltip.svelte +127 -138
  66. package/dist/SvTour.svelte +204 -176
  67. package/dist/SvTree.svelte +377 -377
  68. package/dist/SvTreeSelect.svelte +230 -229
  69. package/dist/build-api.js +76 -54
  70. package/dist/cdn/svgrid.js +10913 -6450
  71. package/dist/cdn/svgrid.svelte-external.js +9628 -5481
  72. package/dist/chart-export.js +8 -8
  73. package/dist/columns.js +6 -0
  74. package/dist/conditional-formatting.d.ts +67 -3
  75. package/dist/conditional-formatting.js +124 -17
  76. package/dist/createCarousel.svelte.d.ts +72 -0
  77. package/dist/createCarousel.svelte.js +80 -0
  78. package/dist/createCommand.svelte.d.ts +62 -0
  79. package/dist/createCommand.svelte.js +109 -0
  80. package/dist/createForm.svelte.d.ts +62 -0
  81. package/dist/createForm.svelte.js +374 -0
  82. package/dist/createMenu.svelte.d.ts +88 -0
  83. package/dist/createMenu.svelte.js +153 -0
  84. package/dist/createOverlay.svelte.d.ts +38 -0
  85. package/dist/createOverlay.svelte.js +68 -0
  86. package/dist/createPagination.d.ts +74 -0
  87. package/dist/createPagination.js +71 -0
  88. package/dist/createPopoverSelect.svelte.js +6 -12
  89. package/dist/createStepper.d.ts +44 -0
  90. package/dist/createStepper.js +26 -0
  91. package/dist/createTooltip.svelte.d.ts +32 -0
  92. package/dist/createTooltip.svelte.js +61 -0
  93. package/dist/datetime/timezone.d.ts +36 -0
  94. package/dist/datetime/timezone.js +110 -0
  95. package/dist/dock-context.d.ts +52 -0
  96. package/dist/dock-context.js +1 -0
  97. package/dist/dock-manager-model.d.ts +136 -0
  98. package/dist/dock-manager-model.js +514 -0
  99. package/dist/dock-model.d.ts +105 -0
  100. package/dist/dock-model.js +288 -0
  101. package/dist/dock-popout.d.ts +30 -0
  102. package/dist/dock-popout.js +81 -0
  103. package/dist/editor-contract.d.ts +2 -0
  104. package/dist/filter-operators.d.ts +6 -0
  105. package/dist/filter-operators.js +35 -2
  106. package/dist/filtering/excel-filters.d.ts +18 -1
  107. package/dist/filtering/excel-filters.js +56 -0
  108. package/dist/form-field.d.ts +73 -0
  109. package/dist/form-field.js +15 -0
  110. package/dist/index.d.ts +25 -5
  111. package/dist/index.js +40 -4
  112. package/dist/list-nav.d.ts +18 -0
  113. package/dist/list-nav.js +30 -0
  114. package/dist/list-option.d.ts +2 -0
  115. package/dist/menu-item.d.ts +20 -0
  116. package/dist/menu-item.js +1 -0
  117. package/dist/menus.d.ts +5 -0
  118. package/dist/menus.js +68 -16
  119. package/dist/panel-resize.d.ts +25 -0
  120. package/dist/panel-resize.js +40 -0
  121. package/dist/popover.d.ts +29 -3
  122. package/dist/popover.js +22 -6
  123. package/dist/recurrence.d.ts +33 -7
  124. package/dist/recurrence.js +151 -13
  125. package/dist/row-drag.d.ts +8 -0
  126. package/dist/row-drag.js +11 -0
  127. package/dist/scheduler-ical.d.ts +30 -0
  128. package/dist/scheduler-ical.js +180 -0
  129. package/dist/scheduler-model.d.ts +269 -0
  130. package/dist/scheduler-model.js +602 -0
  131. package/dist/scheduler-view.svelte.d.ts +22 -0
  132. package/dist/scheduler-view.svelte.js +13 -0
  133. package/dist/spreadsheet.js +20 -12
  134. package/dist/ui-app.types.d.ts +3 -0
  135. package/package.json +14 -10
  136. package/src/DockNodeView.svelte +306 -0
  137. package/src/DockPopoutHost.svelte +17 -0
  138. package/src/FlexRender.svelte +96 -96
  139. package/src/GridFooter.svelte +178 -178
  140. package/src/GridMenus.svelte +83 -3
  141. package/src/SvAutoComplete.svelte +5 -3
  142. package/src/SvCarousel.svelte +141 -163
  143. package/src/SvCarousel.test.ts +68 -0
  144. package/src/SvColorInput.svelte +187 -186
  145. package/src/SvComboBox.svelte +7 -3
  146. package/src/SvCommand.svelte +28 -85
  147. package/src/SvCommand.test.ts +78 -0
  148. package/src/SvContextMenu.svelte +116 -116
  149. package/src/SvCountryInput.svelte +1 -1
  150. package/src/SvDateRangeInput.svelte +1 -1
  151. package/src/SvDateTimePicker.svelte +9 -3
  152. package/src/SvDockLayout.svelte +244 -0
  153. package/src/SvDockManager.svelte +922 -0
  154. package/src/SvDrawer.svelte +250 -228
  155. package/src/SvDropDownList.svelte +85 -13
  156. package/src/SvField.svelte +21 -4
  157. package/src/SvForm.svelte +201 -91
  158. package/src/SvForm.test.ts +299 -0
  159. package/src/SvGrid.controller.svelte.ts +3158 -3034
  160. package/src/SvGrid.css +193 -5
  161. package/src/SvGrid.svelte +3131 -2786
  162. package/src/SvGrid.types.ts +1489 -1027
  163. package/src/SvGridBoard.svelte +2339 -2339
  164. package/src/SvGridChart.svelte +1724 -1724
  165. package/src/SvGridChartPanel.svelte +485 -485
  166. package/src/SvGridDropdown.svelte +689 -682
  167. package/src/SvGridSelect.svelte +1 -0
  168. package/src/SvGroupCell.svelte +109 -109
  169. package/src/SvListBox.svelte +36 -14
  170. package/src/SvMaskedInput.svelte +4 -3
  171. package/src/SvMenu.svelte +124 -124
  172. package/src/SvMenu.test.ts +97 -73
  173. package/src/SvMenuList.svelte +117 -165
  174. package/src/SvModal.svelte +12 -29
  175. package/src/SvModal.test.ts +82 -0
  176. package/src/SvMultiSelect.svelte +1 -0
  177. package/src/SvNumberInput.svelte +131 -130
  178. package/src/SvOtpInput.svelte +158 -158
  179. package/src/SvPagination.svelte +18 -18
  180. package/src/SvPasswordInput.svelte +3 -2
  181. package/src/SvPhoneInput.svelte +3 -2
  182. package/src/SvPopover.svelte +139 -139
  183. package/src/SvRowGroupPanel.svelte +170 -170
  184. package/src/SvScrollArea.svelte +61 -61
  185. package/src/SvStepper.svelte +6 -9
  186. package/src/SvTagsInput.svelte +3 -2
  187. package/src/SvTooltip.svelte +127 -138
  188. package/src/SvTooltip.test.ts +73 -0
  189. package/src/SvTour.svelte +204 -176
  190. package/src/SvTree.svelte +377 -377
  191. package/src/SvTreeSelect.svelte +230 -229
  192. package/src/a11y.contract.test.ts +49 -49
  193. package/src/a11y.test.ts +59 -59
  194. package/src/a11y.ts +61 -61
  195. package/src/build-api.ts +870 -847
  196. package/src/cell-formatting.ts +169 -169
  197. package/src/cell-render.ts +469 -469
  198. package/src/chart-export.ts +201 -201
  199. package/src/chart.ts +2296 -2296
  200. package/src/collaboration.test.ts +104 -104
  201. package/src/collaboration.ts +167 -167
  202. package/src/columns.ts +5 -0
  203. package/src/conditional-formatting.test.ts +104 -0
  204. package/src/conditional-formatting.ts +176 -19
  205. package/src/core.performance.test.ts +30 -30
  206. package/src/core.ts +1111 -1111
  207. package/src/createAutocomplete.svelte.ts +132 -132
  208. package/src/createCarousel.svelte.ts +104 -0
  209. package/src/createCombobox.svelte.ts +179 -179
  210. package/src/createCommand.svelte.ts +125 -0
  211. package/src/createCountryInput.svelte.ts +157 -157
  212. package/src/createDropdownList.svelte.ts +162 -162
  213. package/src/createForm.svelte.ts +333 -0
  214. package/src/createGrid.svelte.ts +42 -42
  215. package/src/createGrid.test.ts +10 -10
  216. package/src/createGridState.svelte.ts +17 -17
  217. package/src/createMenu.svelte.ts +202 -0
  218. package/src/createOverlay.svelte.ts +101 -0
  219. package/src/createPagination.test.ts +65 -0
  220. package/src/createPagination.ts +84 -0
  221. package/src/createPopoverSelect.svelte.ts +206 -210
  222. package/src/createStepper.test.ts +62 -0
  223. package/src/createStepper.ts +61 -0
  224. package/src/createTooltip.svelte.ts +86 -0
  225. package/src/createTree.svelte.ts +319 -319
  226. package/src/datetime/timezone.test.ts +69 -0
  227. package/src/datetime/timezone.ts +134 -0
  228. package/src/dock-context.ts +47 -0
  229. package/src/dock-manager-model.test.ts +275 -0
  230. package/src/dock-manager-model.ts +593 -0
  231. package/src/dock-model.test.ts +163 -0
  232. package/src/dock-model.ts +368 -0
  233. package/src/dock-popout.ts +94 -0
  234. package/src/editing.test.ts +859 -859
  235. package/src/editing.ts +675 -675
  236. package/src/editor-contract.ts +2 -0
  237. package/src/export-data-api.test.ts +126 -126
  238. package/src/export-format.test.ts +107 -107
  239. package/src/export-format.ts +598 -598
  240. package/src/filter-operators.test.ts +59 -0
  241. package/src/filter-operators.ts +35 -2
  242. package/src/filtering/excel-filters.test.ts +86 -1
  243. package/src/filtering/excel-filters.ts +65 -0
  244. package/src/flex-render.ts +3 -3
  245. package/src/form-field.ts +87 -0
  246. package/src/index.ts +623 -469
  247. package/src/keyboard.test.ts +59 -59
  248. package/src/keyboard.ts +97 -97
  249. package/src/list-nav.test.ts +49 -0
  250. package/src/list-nav.ts +29 -0
  251. package/src/list-option.ts +2 -0
  252. package/src/menu-item.ts +21 -0
  253. package/src/menus.test.ts +48 -0
  254. package/src/menus.ts +639 -582
  255. package/src/merge-objects.ts +48 -48
  256. package/src/overlays.test.ts +90 -90
  257. package/src/panel-resize.ts +52 -0
  258. package/src/popover.bounds.test.ts +93 -0
  259. package/src/popover.ts +51 -7
  260. package/src/recurrence.test.ts +88 -1
  261. package/src/recurrence.ts +166 -15
  262. package/src/render-component.ts +28 -28
  263. package/src/row-drag.ts +10 -0
  264. package/src/scheduler-ical.test.ts +68 -0
  265. package/src/scheduler-ical.ts +180 -0
  266. package/src/scheduler-model.test.ts +546 -0
  267. package/src/scheduler-model.ts +853 -0
  268. package/src/scheduler-view.svelte.ts +33 -0
  269. package/src/selection.test.ts +754 -754
  270. package/src/selection.ts +600 -600
  271. package/src/server-data-source.test.ts +289 -289
  272. package/src/server-data-source.ts +413 -413
  273. package/src/sparkline.test.ts +68 -68
  274. package/src/sparkline.ts +169 -169
  275. package/src/spreadsheet.test.ts +489 -489
  276. package/src/spreadsheet.ts +312 -304
  277. package/src/static-functions.ts +11 -11
  278. package/src/subscribe.ts +38 -38
  279. package/src/svgrid-wrapper.types.ts +501 -501
  280. package/src/svgrid.behavior.test.ts +706 -706
  281. package/src/svgrid.charting.test.ts +527 -527
  282. package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
  283. package/src/svgrid.features.test.ts +157 -157
  284. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  285. package/src/svgrid.wrapper.test.ts +40 -40
  286. package/src/test-setup.ts +62 -62
  287. package/src/themes/index.ts +159 -159
  288. package/src/ui-app.types.ts +3 -0
  289. package/src/virtual.test.ts +6 -4
  290. package/src/virtualization/column-virtualizer.test.ts +27 -27
  291. package/src/virtualization/column-virtualizer.ts +30 -30
  292. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  293. package/src/virtualization/types.ts +30 -30
  294. package/src/virtualization/virtualizer.test.ts +47 -47
  295. 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
+ }