@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,163 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ pane,
4
+ tabs,
5
+ group,
6
+ normalize,
7
+ normalizeSizes,
8
+ findTabsWithPane,
9
+ allPaneIds,
10
+ removePane,
11
+ dockInto,
12
+ movePane,
13
+ setActive,
14
+ setSizes,
15
+ type DockNode,
16
+ type DockTabs,
17
+ } from './dock-model'
18
+
19
+ // Deterministic id generator for tests.
20
+ function counter() {
21
+ let n = 0
22
+ return () => `n${n++}`
23
+ }
24
+
25
+ /** A row of two single-pane leaves: [A | B]. */
26
+ function twoPane() {
27
+ const g = counter()
28
+ const A = tabs(g, [pane('a', 'A')])
29
+ const B = tabs(g, [pane('b', 'B')])
30
+ return { root: group(g, 'row', [A, B]) as DockNode, A, B }
31
+ }
32
+
33
+ describe('normalizeSizes', () => {
34
+ it('splits evenly when absent and normalizes to sum 1', () => {
35
+ expect(normalizeSizes(undefined, 4)).toEqual([0.25, 0.25, 0.25, 0.25])
36
+ const n = normalizeSizes([1, 3], 2)
37
+ expect(n[0]).toBeCloseTo(0.25)
38
+ expect(n[1]).toBeCloseTo(0.75)
39
+ })
40
+ it('recovers from a bad length or zero total', () => {
41
+ expect(normalizeSizes([5], 2)).toEqual([0.5, 0.5])
42
+ expect(normalizeSizes([0, 0], 2)).toEqual([0.5, 0.5])
43
+ })
44
+ })
45
+
46
+ describe('queries', () => {
47
+ it('finds the leaf holding a pane, and lists all panes', () => {
48
+ const { root, A } = twoPane()
49
+ expect(findTabsWithPane(root, 'a')?.id).toBe(A.id)
50
+ expect(findTabsWithPane(root, 'nope')).toBeNull()
51
+ expect(allPaneIds(root)).toEqual(['a', 'b'])
52
+ })
53
+ })
54
+
55
+ describe('dockPane - center adds a tab', () => {
56
+ it('appends the pane to the target leaf and activates it', () => {
57
+ const { root, B } = twoPane()
58
+ const g = counter()
59
+ const next = dockInto(root, B.id, pane('c', 'C'), 'center', g) as DockNode
60
+ const leaf = findTabsWithPane(next, 'c') as DockTabs
61
+ expect(leaf.id).toBe(B.id)
62
+ expect(leaf.panes.map((p) => p.id)).toEqual(['b', 'c'])
63
+ expect(leaf.active).toBe(1)
64
+ })
65
+ })
66
+
67
+ describe('dockPane - edge splits', () => {
68
+ it('inserts a sibling in the same-direction parent (stays flat)', () => {
69
+ const { root, A } = twoPane()
70
+ const g = counter()
71
+ // Dock C to the RIGHT of A: parent row matches -> [A, C, B]
72
+ const next = dockInto(root, A.id, pane('c', 'C'), 'right', g) as DockNode
73
+ expect(next.type).toBe('group')
74
+ expect(allPaneIds(next)).toEqual(['a', 'c', 'b'])
75
+ })
76
+
77
+ it('inserts on the correct side for left/top', () => {
78
+ const { root, A } = twoPane()
79
+ const g = counter()
80
+ const next = dockInto(root, A.id, pane('c', 'C'), 'left', g) as DockNode
81
+ expect(allPaneIds(next)).toEqual(['c', 'a', 'b'])
82
+ })
83
+
84
+ it('wraps in a cross-direction group when the edge differs from the parent', () => {
85
+ const { root, A } = twoPane()
86
+ const g = counter()
87
+ // Dock C BELOW A: parent is a row, so A must become a column [A / C].
88
+ const next = dockInto(root, A.id, pane('c', 'C'), 'bottom', g)
89
+ expect(next.type).toBe('group')
90
+ const rootGroup = next as Extract<DockNode, { type: 'group' }>
91
+ expect(rootGroup.direction).toBe('row')
92
+ // First child is now a column group holding A and C.
93
+ const first = rootGroup.children[0]
94
+ expect(first?.type).toBe('group')
95
+ expect((first as any).direction).toBe('column')
96
+ expect(allPaneIds(next)).toEqual(['a', 'c', 'b'])
97
+ })
98
+ })
99
+
100
+ describe('removePane + normalize', () => {
101
+ it('removes a pane, collapses the emptied leaf and unwraps the single-child group', () => {
102
+ const { root } = twoPane()
103
+ const { root: next, pane: removed } = removePane(root, 'b')
104
+ expect(removed?.id).toBe('b')
105
+ // Only A remains -> the row group collapses to the bare leaf A.
106
+ expect(next?.type).toBe('tabs')
107
+ expect(allPaneIds(next!)).toEqual(['a'])
108
+ })
109
+
110
+ it('keeps other tabs in a multi-pane leaf and fixes the active index', () => {
111
+ const g = counter()
112
+ const leaf = tabs(g, [pane('a', 'A'), pane('b', 'B'), pane('c', 'C')], 2)
113
+ const { root: next } = removePane(leaf, 'a')
114
+ const t = next as DockTabs
115
+ expect(t.panes.map((p) => p.id)).toEqual(['b', 'c'])
116
+ expect(t.active).toBe(1) // was 2 (C), shifted down by the earlier removal
117
+ })
118
+
119
+ it('flattens nested same-direction groups', () => {
120
+ const g = counter()
121
+ const inner = group(g, 'row', [tabs(g, [pane('a', 'A')]), tabs(g, [pane('b', 'B')])])
122
+ const outer = group(g, 'row', [inner, tabs(g, [pane('c', 'C')])])
123
+ const flat = normalize(outer) as Extract<DockNode, { type: 'group' }>
124
+ expect(flat.direction).toBe('row')
125
+ expect(flat.children.every((c) => c.type === 'tabs')).toBe(true)
126
+ expect(allPaneIds(flat)).toEqual(['a', 'b', 'c'])
127
+ expect(flat.sizes).toHaveLength(3)
128
+ })
129
+ })
130
+
131
+ describe('movePane', () => {
132
+ it('relocates a pane from one leaf to another as a tab', () => {
133
+ const { root, B } = twoPane()
134
+ const g = counter()
135
+ const next = movePane(root, 'a', B.id, 'center', g)
136
+ // A moved into B's leaf; the old A leaf collapsed, so root is the B leaf.
137
+ expect(next.type).toBe('tabs')
138
+ expect(allPaneIds(next)).toEqual(['b', 'a'])
139
+ })
140
+
141
+ it('is a no-op when the target leaf dissolves with the moved pane', () => {
142
+ const g = counter()
143
+ const solo = tabs(g, [pane('a', 'A')])
144
+ const next = movePane(solo, 'a', solo.id, 'center', g)
145
+ expect(next).toBe(solo) // unchanged reference - nothing to dock against
146
+ })
147
+ })
148
+
149
+ describe('setActive / setSizes', () => {
150
+ it('updates the active tab', () => {
151
+ const g = counter()
152
+ const leaf = tabs(g, [pane('a', 'A'), pane('b', 'B')], 0)
153
+ const next = setActive(leaf, leaf.id, 1) as DockTabs
154
+ expect(next.active).toBe(1)
155
+ })
156
+ it('updates and normalizes group sizes', () => {
157
+ const { root } = twoPane()
158
+ const gid = (root as Extract<DockNode, { type: 'group' }>).id
159
+ const next = setSizes(root, gid, [3, 1]) as Extract<DockNode, { type: 'group' }>
160
+ expect(next.sizes[0]).toBeCloseTo(0.75)
161
+ expect(next.sizes[1]).toBeCloseTo(0.25)
162
+ })
163
+ })
@@ -0,0 +1,368 @@
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
+ export type DockPane = {
17
+ id: string
18
+ title: string
19
+ /** Show a close (x) affordance on the tab. Default true. */
20
+ closable?: boolean
21
+ /** Minimum size (px) for this pane's leaf along the split axis. */
22
+ minSize?: number
23
+ }
24
+
25
+ /** A leaf: a stack of panes rendered as tabs (`active` is the visible one). */
26
+ export type DockTabs = {
27
+ type: 'tabs'
28
+ id: string
29
+ panes: DockPane[]
30
+ active: number
31
+ }
32
+
33
+ /** A row (side-by-side) or column (stacked) of children with per-child sizes. */
34
+ export type DockGroup = {
35
+ type: 'group'
36
+ id: string
37
+ direction: 'row' | 'column'
38
+ children: DockNode[]
39
+ /** Size weight per child; parallel to `children`, normalized to sum ~1. */
40
+ sizes: number[]
41
+ }
42
+
43
+ export type DockNode = DockGroup | DockTabs
44
+
45
+ /** Where a dragged pane lands relative to a target leaf. */
46
+ export type DockZone = 'left' | 'right' | 'top' | 'bottom' | 'center'
47
+
48
+ export type IdGen = () => string
49
+
50
+ // ---- construction helpers -------------------------------------------------
51
+
52
+ export function pane(id: string, title: string, closable = true): DockPane {
53
+ return { id, title, closable }
54
+ }
55
+
56
+ /** Largest per-pane `minSize` in a leaf (0 if none). Used by splitter clamping. */
57
+ export function leafMinSize(node: DockTabs): number {
58
+ return node.panes.reduce((m, p) => Math.max(m, p.minSize ?? 0), 0)
59
+ }
60
+
61
+ export function tabs(genId: IdGen, panes: DockPane[], active = 0): DockTabs {
62
+ return { type: 'tabs', id: genId(), panes, active: clampIndex(active, panes.length) }
63
+ }
64
+
65
+ export function group(
66
+ genId: IdGen,
67
+ direction: 'row' | 'column',
68
+ children: DockNode[],
69
+ sizes?: number[],
70
+ ): DockGroup {
71
+ return { type: 'group', id: genId(), direction, children, sizes: normalizeSizes(sizes, children.length) }
72
+ }
73
+
74
+ // ---- small utilities ------------------------------------------------------
75
+
76
+ function clampIndex(i: number, len: number): number {
77
+ if (len <= 0) return 0
78
+ return Math.max(0, Math.min(i, len - 1))
79
+ }
80
+
81
+ /** Return sizes summing to 1, padded/trimmed to `count`. Even split when absent. */
82
+ export function normalizeSizes(sizes: number[] | undefined, count: number): number[] {
83
+ if (count <= 0) return []
84
+ const src = sizes && sizes.length === count ? sizes.slice() : new Array(count).fill(1)
85
+ const total = src.reduce((a, b) => a + (b > 0 ? b : 0), 0)
86
+ if (total <= 0) return new Array(count).fill(1 / count)
87
+ return src.map((s) => (s > 0 ? s : 0) / total)
88
+ }
89
+
90
+ // ---- queries --------------------------------------------------------------
91
+
92
+ /** The tabs leaf that holds `paneId`, or null. */
93
+ export function findTabsWithPane(root: DockNode, paneId: string): DockTabs | null {
94
+ if (root.type === 'tabs') return root.panes.some((p) => p.id === paneId) ? root : null
95
+ for (const c of root.children) {
96
+ const hit = findTabsWithPane(c, paneId)
97
+ if (hit) return hit
98
+ }
99
+ return null
100
+ }
101
+
102
+ /** Every pane id in the tree, in document order. */
103
+ export function allPaneIds(root: DockNode): string[] {
104
+ if (root.type === 'tabs') return root.panes.map((p) => p.id)
105
+ return root.children.flatMap(allPaneIds)
106
+ }
107
+
108
+ // ---- normalization --------------------------------------------------------
109
+
110
+ /**
111
+ * Collapse the tree after edits: drop empty tabs and empty groups, unwrap a
112
+ * group that has a single child, and flatten a child group into its parent when
113
+ * they share a direction (so repeated docking never nests infinitely deep).
114
+ * Returns the tidied node, or null when nothing is left.
115
+ */
116
+ export function normalize(node: DockNode): DockNode | null {
117
+ if (node.type === 'tabs') {
118
+ if (node.panes.length === 0) return null
119
+ return { ...node, active: clampIndex(node.active, node.panes.length) }
120
+ }
121
+
122
+ // Normalize children first, dropping the empties.
123
+ const kept: DockNode[] = []
124
+ const keptSizes: number[] = []
125
+ node.children.forEach((child, i) => {
126
+ const n = normalize(child)
127
+ if (n) {
128
+ kept.push(n)
129
+ keptSizes.push(node.sizes[i] ?? 1)
130
+ }
131
+ })
132
+
133
+ if (kept.length === 0) return null
134
+ if (kept.length === 1) return kept[0]! // unwrap single-child group
135
+
136
+ // Flatten same-direction child groups into this one.
137
+ const flatChildren: DockNode[] = []
138
+ const flatSizes: number[] = []
139
+ kept.forEach((child, i) => {
140
+ if (child.type === 'group' && child.direction === node.direction) {
141
+ const share = keptSizes[i]!
142
+ child.children.forEach((gc, j) => {
143
+ flatChildren.push(gc)
144
+ flatSizes.push(share * (child.sizes[j] ?? 1 / child.children.length))
145
+ })
146
+ } else {
147
+ flatChildren.push(child)
148
+ flatSizes.push(keptSizes[i]!)
149
+ }
150
+ })
151
+
152
+ return { ...node, children: flatChildren, sizes: normalizeSizes(flatSizes, flatChildren.length) }
153
+ }
154
+
155
+ // ---- mutations (return a new tree) ----------------------------------------
156
+
157
+ /** Set the active tab index of a tabs leaf. */
158
+ export function setActive(root: DockNode, tabsId: string, active: number): DockNode {
159
+ return mapNodes(root, (n) =>
160
+ n.type === 'tabs' && n.id === tabsId ? { ...n, active: clampIndex(active, n.panes.length) } : n,
161
+ )
162
+ }
163
+
164
+ /** Replace a group's size weights (e.g. after a splitter drag). */
165
+ export function setSizes(root: DockNode, groupId: string, sizes: number[]): DockNode {
166
+ return mapNodes(root, (n) =>
167
+ n.type === 'group' && n.id === groupId ? { ...n, sizes: normalizeSizes(sizes, n.children.length) } : n,
168
+ )
169
+ }
170
+
171
+ /**
172
+ * Reorder a pane WITHIN its tabs leaf (dragging a tab along the strip). `active`
173
+ * follows the pane that was showing. No-op if the indices are out of range.
174
+ */
175
+ export function reorderPane(root: DockNode, tabsId: string, from: number, to: number): DockNode {
176
+ return mapNodes(root, (n) => {
177
+ if (n.type !== 'tabs' || n.id !== tabsId) return n
178
+ if (from < 0 || from >= n.panes.length) return n
179
+ const showing = n.panes[n.active]?.id
180
+ const panes = n.panes.slice()
181
+ const [moved] = panes.splice(from, 1)
182
+ if (!moved) return n
183
+ // `to` is an insertion point in [0, length] (length = append), not a select
184
+ // index - clampIndex would wrongly cap it at length-1.
185
+ panes.splice(Math.max(0, Math.min(to, panes.length)), 0, moved)
186
+ const active = panes.findIndex((p) => p.id === showing)
187
+ return { ...n, panes, active: active < 0 ? 0 : active }
188
+ })
189
+ }
190
+
191
+ /**
192
+ * Remove a pane. Returns the tidied tree (or null if it emptied) plus the pane
193
+ * that was pulled out, so a caller can re-dock it elsewhere.
194
+ */
195
+ export function removePane(root: DockNode, paneId: string): { root: DockNode | null; pane: DockPane | null } {
196
+ let removed: DockPane | null = null
197
+ const strip = (node: DockNode): DockNode => {
198
+ if (node.type === 'tabs') {
199
+ const idx = node.panes.findIndex((p) => p.id === paneId)
200
+ if (idx < 0) return node
201
+ removed = node.panes[idx]!
202
+ const panes = node.panes.filter((_, i) => i !== idx)
203
+ return { ...node, panes, active: clampIndex(node.active > idx ? node.active - 1 : node.active, panes.length) }
204
+ }
205
+ return { ...node, children: node.children.map(strip) }
206
+ }
207
+ const stripped = strip(root)
208
+ return { root: normalize(stripped), pane: removed }
209
+ }
210
+
211
+ /**
212
+ * Dock `movingPane` relative to the leaf `targetTabsId`. `center` adds it as a
213
+ * tab; an edge splits: the target is wrapped in (or extended into) a group whose
214
+ * direction matches the edge, with the new pane on the requested side.
215
+ */
216
+ export function dockInto(
217
+ root: DockNode,
218
+ targetTabsId: string,
219
+ movingPane: DockPane,
220
+ zone: DockZone,
221
+ genId: IdGen,
222
+ ): DockNode {
223
+ const dir: 'row' | 'column' = zone === 'left' || zone === 'right' ? 'row' : 'column'
224
+ const before = zone === 'left' || zone === 'top'
225
+
226
+ const transform = (node: DockNode): DockNode => {
227
+ if (node.type === 'tabs') {
228
+ if (node.id !== targetTabsId) return node
229
+ if (zone === 'center') {
230
+ const panes = [...node.panes, movingPane]
231
+ return { ...node, panes, active: panes.length - 1 }
232
+ }
233
+ // Edge on a bare leaf: wrap it in a new group with the new leaf beside it.
234
+ const leaf = tabs(genId, [movingPane], 0)
235
+ const kids = before ? [leaf, node] : [node, leaf]
236
+ return group(genId, dir, kids)
237
+ }
238
+
239
+ // Group: if it directly contains the target leaf and matches the edge
240
+ // direction, insert a sibling in place (keeps the tree flat).
241
+ const idx = node.children.findIndex((c) => c.type === 'tabs' && c.id === targetTabsId)
242
+ if (idx >= 0 && zone !== 'center' && node.direction === dir) {
243
+ const leaf = tabs(genId, [movingPane], 0)
244
+ const children = node.children.slice()
245
+ const sizes = node.sizes.slice()
246
+ const half = (node.sizes[idx] ?? 1 / node.children.length) / 2
247
+ const insertAt = before ? idx : idx + 1
248
+ children.splice(insertAt, 0, leaf)
249
+ sizes.splice(idx, 1, half, half) // split the target's slot in two
250
+ // splice above replaced 1 with 2 at idx; the new leaf takes `before`/after
251
+ if (before) {
252
+ // children: [..., leaf, target, ...]; sizes already [half(leaf), half(target)]
253
+ } else {
254
+ // we inserted leaf AFTER target but sizes are [half, half] at idx/idx+1 - correct
255
+ }
256
+ return { ...node, children, sizes: normalizeSizes(sizes, children.length) }
257
+ }
258
+
259
+ return { ...node, children: node.children.map(transform) }
260
+ }
261
+
262
+ return normalize(transform(root)) ?? root
263
+ }
264
+
265
+ /** Pull a whole tabs leaf out of the tree (for auto-hide). Returns the tidied
266
+ * tree (or null) plus the extracted leaf. */
267
+ export function removeLeaf(root: DockNode, tabsId: string): { root: DockNode | null; leaf: DockTabs | null } {
268
+ let taken: DockTabs | null = null
269
+ const strip = (node: DockNode): DockNode | null => {
270
+ if (node.type === 'tabs') {
271
+ if (node.id === tabsId) { taken = node; return null }
272
+ return node
273
+ }
274
+ const kids: DockNode[] = []
275
+ const sizes: number[] = []
276
+ node.children.forEach((c, i) => {
277
+ const n = strip(c)
278
+ if (n) { kids.push(n); sizes.push(node.sizes[i] ?? 1) }
279
+ })
280
+ if (kids.length === 0) return null
281
+ return { ...node, children: kids, sizes: normalizeSizes(sizes, kids.length) }
282
+ }
283
+ const stripped = strip(root)
284
+ return { root: stripped ? normalize(stripped) : null, leaf: taken }
285
+ }
286
+
287
+ /** Dock a whole leaf against an EDGE of the entire tree (for pinning an
288
+ * auto-hidden panel back in). Wraps the root in a new group as needed; the
289
+ * pinned leaf takes `fraction` of that axis (clamped 0.1..0.6), the rest keeps
290
+ * the existing content. */
291
+ export function dockLeafToEdge(
292
+ root: DockNode | null,
293
+ leaf: DockTabs,
294
+ side: Exclude<DockZone, 'center'>,
295
+ genId: IdGen,
296
+ fraction = 0.25,
297
+ ): DockNode {
298
+ if (!root) return leaf
299
+ const dir: 'row' | 'column' = side === 'left' || side === 'right' ? 'row' : 'column'
300
+ const before = side === 'left' || side === 'top'
301
+ const f = Math.min(0.6, Math.max(0.1, fraction))
302
+ const kids = before ? [leaf, root] : [root, leaf]
303
+ const sizes = before ? [f, 1 - f] : [1 - f, f]
304
+ return normalize(group(genId, dir, kids, sizes)) ?? leaf
305
+ }
306
+
307
+ /** Move an existing pane to a new dock position (remove then dock). */
308
+ export function movePane(
309
+ root: DockNode,
310
+ paneId: string,
311
+ targetTabsId: string,
312
+ zone: DockZone,
313
+ genId: IdGen,
314
+ ): DockNode {
315
+ const { root: without, pane: moved } = removePane(root, paneId)
316
+ if (!without || !moved) return root
317
+ // If removing the pane dissolved the target leaf (it was the pane's own
318
+ // single-pane leaf), there is nothing to dock against - leave the tree as-is.
319
+ if (!containsTabs(without, targetTabsId)) return root
320
+ return dockInto(without, targetTabsId, moved, zone, genId)
321
+ }
322
+
323
+ function containsTabs(node: DockNode, tabsId: string): boolean {
324
+ if (node.type === 'tabs') return node.id === tabsId
325
+ return node.children.some((c) => containsTabs(c, tabsId))
326
+ }
327
+
328
+ // ---- internal -------------------------------------------------------------
329
+
330
+ /** Map over every node, replacing where `fn` returns a new object. */
331
+ function mapNodes(node: DockNode, fn: (n: DockNode) => DockNode): DockNode {
332
+ const mapped = fn(node)
333
+ if (mapped.type === 'group') {
334
+ return { ...mapped, children: mapped.children.map((c) => mapNodes(c, fn)) }
335
+ }
336
+ return mapped
337
+ }
338
+
339
+ // ---- consumer builders (auto ids) -----------------------------------------
340
+ // Ergonomic constructors for building an initial layout without threading an
341
+ // id generator. Node ids only need to be unique; a running counter suffices.
342
+
343
+ let AUTO = 0
344
+ const autoId: IdGen = () => `d${AUTO++}`
345
+
346
+ /** A pane (tab). Alias of `pane`, named for the public builder set. */
347
+ export function dockPane(
348
+ id: string,
349
+ title: string,
350
+ opts?: boolean | { closable?: boolean; minSize?: number },
351
+ ): DockPane {
352
+ const o = typeof opts === 'boolean' ? { closable: opts } : (opts ?? {})
353
+ return { id, title, closable: o.closable ?? true, ...(o.minSize != null ? { minSize: o.minSize } : {}) }
354
+ }
355
+
356
+ /** A tab-leaf holding one or more panes. */
357
+ export function dockTabs(panes: DockPane[], active = 0): DockTabs {
358
+ return tabs(autoId, panes, active)
359
+ }
360
+
361
+ /** A row (side-by-side) or column (stacked) of children. */
362
+ export function dockGroup(
363
+ direction: 'row' | 'column',
364
+ children: DockNode[],
365
+ sizes?: number[],
366
+ ): DockGroup {
367
+ return group(autoId, direction, children, sizes)
368
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * dock-popout - open a real browser window and render a dock pane's content
3
+ * inside it (Golden-Layout-style pop-out). Runtime-only: an OS window handle is
4
+ * not serializable, so the manager tracks popped panes separately and pops them
5
+ * back into the layout when the window closes.
6
+ *
7
+ * The popped nodes are created by the main document and adopted into the popup,
8
+ * so they stay driven by the main window's reactivity - the pop-out shows the
9
+ * same live content as the docked view. Stylesheets + `--sg-*` theme tokens are
10
+ * copied across so it looks identical.
11
+ */
12
+ import { mount, unmount, type Snippet } from 'svelte'
13
+ import DockPopoutHost from './DockPopoutHost.svelte'
14
+ import { PANEL_THEME_VARS } from './popover'
15
+ import type { DockPane } from './dock-model'
16
+
17
+ export type Popout = {
18
+ pane: DockPane
19
+ win: Window
20
+ destroy: () => void
21
+ }
22
+
23
+ export type OpenPopoutOptions = {
24
+ pane: DockPane
25
+ paneSnippet: Snippet<[DockPane]>
26
+ /** Element whose resolved `--sg-*` tokens theme the popup. */
27
+ themeSource: HTMLElement
28
+ width?: number
29
+ height?: number
30
+ /** Called (once) when the popup closes, to pop the pane back in. */
31
+ onClose: (paneId: string) => void
32
+ }
33
+
34
+ /** Open a pop-out window for a pane. Returns null if the browser blocked it. */
35
+ export function openPopout(opts: OpenPopoutOptions): Popout | null {
36
+ const win = window.open('', '', `width=${opts.width ?? 480},height=${opts.height ?? 360}`)
37
+ if (!win) return null
38
+
39
+ const doc = win.document
40
+ doc.title = opts.pane.title
41
+
42
+ // Copy every stylesheet so the popped content is styled identically.
43
+ for (const sheet of Array.from(document.styleSheets)) {
44
+ try {
45
+ const css = Array.from(sheet.cssRules).map((r) => r.cssText).join('\n')
46
+ const style = doc.createElement('style')
47
+ style.textContent = css
48
+ doc.head.appendChild(style)
49
+ } catch {
50
+ // Cross-origin sheet: re-link it instead of reading its rules.
51
+ if (sheet.href) {
52
+ const link = doc.createElement('link')
53
+ link.rel = 'stylesheet'
54
+ link.href = sheet.href
55
+ doc.head.appendChild(link)
56
+ }
57
+ }
58
+ }
59
+
60
+ // Carry the theme tokens + base look across.
61
+ const cs = getComputedStyle(opts.themeSource)
62
+ for (const v of PANEL_THEME_VARS) {
63
+ const val = cs.getPropertyValue(v).trim()
64
+ if (val) doc.documentElement.style.setProperty(v, val)
65
+ }
66
+ doc.body.style.margin = '0'
67
+ doc.body.style.background = cs.getPropertyValue('--sg-bg').trim() || '#fff'
68
+ doc.body.style.color = cs.getPropertyValue('--sg-fg').trim() || '#0f172a'
69
+ doc.body.style.font = cs.font
70
+
71
+ const app = mount(DockPopoutHost, {
72
+ target: doc.body,
73
+ props: { pane: opts.paneSnippet, paneData: opts.pane },
74
+ })
75
+
76
+ let done = false
77
+ const finish = (popBackIn: boolean) => {
78
+ if (done) return
79
+ done = true
80
+ clearInterval(poll)
81
+ try { unmount(app) } catch { /* window already tearing down */ }
82
+ if (popBackIn) opts.onClose(opts.pane.id)
83
+ }
84
+ // The user closed the OS window -> pop the pane back into the layout.
85
+ win.addEventListener('beforeunload', () => finish(true))
86
+ const poll = setInterval(() => { if (win.closed) finish(true) }, 400)
87
+
88
+ return {
89
+ pane: opts.pane,
90
+ win,
91
+ // Programmatic teardown (dock-back / manager unmount): don't re-pop-in.
92
+ destroy: () => { finish(false); try { win.close() } catch { /* noop */ } },
93
+ }
94
+ }