@svgrid/grid 2.1.22 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/dist/DockNodeView.svelte +306 -0
- package/dist/DockNodeView.svelte.d.ts +7 -0
- package/dist/DockPopoutHost.svelte +17 -0
- package/dist/DockPopoutHost.svelte.d.ts +14 -0
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +178 -178
- package/dist/GridMenus.svelte +83 -3
- package/dist/SvAutoComplete.svelte +5 -3
- package/dist/SvCarousel.svelte +141 -163
- package/dist/SvColorInput.svelte +187 -186
- package/dist/SvColorInput.svelte.d.ts +1 -1
- package/dist/SvComboBox.svelte +7 -3
- package/dist/SvCommand.svelte +28 -85
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +1 -1
- package/dist/SvDateRangeInput.svelte +1 -1
- package/dist/SvDateTimePicker.svelte +9 -3
- package/dist/SvDateTimePicker.svelte.d.ts +3 -0
- package/dist/SvDockLayout.svelte +244 -0
- package/dist/SvDockLayout.svelte.d.ts +77 -0
- package/dist/SvDockManager.svelte +922 -0
- package/dist/SvDockManager.svelte.d.ts +106 -0
- package/dist/SvDrawer.svelte +250 -228
- package/dist/SvDrawer.svelte.d.ts +4 -0
- package/dist/SvDropDownList.svelte +85 -13
- package/dist/SvDropDownList.svelte.d.ts +3 -0
- package/dist/SvField.svelte +21 -4
- package/dist/SvField.svelte.d.ts +1 -0
- package/dist/SvForm.svelte +201 -91
- package/dist/SvForm.svelte.d.ts +15 -22
- package/dist/SvGrid.controller.svelte.d.ts +32 -1
- package/dist/SvGrid.controller.svelte.js +156 -27
- package/dist/SvGrid.css +193 -5
- package/dist/SvGrid.svelte +3131 -2786
- package/dist/SvGrid.types.d.ts +452 -1
- package/dist/SvGridBoard.svelte +2339 -2339
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridDropdown.svelte +689 -682
- package/dist/SvGridSelect.svelte +1 -0
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvListBox.svelte +36 -14
- package/dist/SvMaskedInput.svelte +4 -3
- package/dist/SvMaskedInput.svelte.d.ts +1 -1
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +117 -165
- package/dist/SvMenuList.svelte.d.ts +2 -13
- package/dist/SvModal.svelte +12 -29
- package/dist/SvMultiSelect.svelte +1 -0
- package/dist/SvNumberInput.svelte +131 -130
- package/dist/SvNumberInput.svelte.d.ts +1 -1
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPagination.svelte +18 -18
- package/dist/SvPasswordInput.svelte +3 -2
- package/dist/SvPasswordInput.svelte.d.ts +1 -1
- package/dist/SvPhoneInput.svelte +3 -2
- package/dist/SvPhoneInput.svelte.d.ts +1 -1
- package/dist/SvPopover.svelte +139 -139
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvStepper.svelte +6 -9
- package/dist/SvTagsInput.svelte +3 -2
- package/dist/SvTagsInput.svelte.d.ts +1 -1
- package/dist/SvTooltip.svelte +127 -138
- package/dist/SvTour.svelte +204 -176
- package/dist/SvTree.svelte +377 -377
- package/dist/SvTreeSelect.svelte +230 -229
- package/dist/build-api.js +76 -54
- package/dist/cdn/svgrid.js +10913 -6450
- package/dist/cdn/svgrid.svelte-external.js +9628 -5481
- package/dist/chart-export.js +8 -8
- package/dist/columns.js +6 -0
- package/dist/conditional-formatting.d.ts +67 -3
- package/dist/conditional-formatting.js +124 -17
- package/dist/createCarousel.svelte.d.ts +72 -0
- package/dist/createCarousel.svelte.js +80 -0
- package/dist/createCommand.svelte.d.ts +62 -0
- package/dist/createCommand.svelte.js +109 -0
- package/dist/createForm.svelte.d.ts +62 -0
- package/dist/createForm.svelte.js +374 -0
- package/dist/createMenu.svelte.d.ts +88 -0
- package/dist/createMenu.svelte.js +153 -0
- package/dist/createOverlay.svelte.d.ts +38 -0
- package/dist/createOverlay.svelte.js +68 -0
- package/dist/createPagination.d.ts +74 -0
- package/dist/createPagination.js +71 -0
- package/dist/createPopoverSelect.svelte.js +6 -12
- package/dist/createStepper.d.ts +44 -0
- package/dist/createStepper.js +26 -0
- package/dist/createTooltip.svelte.d.ts +32 -0
- package/dist/createTooltip.svelte.js +61 -0
- package/dist/datetime/timezone.d.ts +36 -0
- package/dist/datetime/timezone.js +110 -0
- package/dist/dock-context.d.ts +52 -0
- package/dist/dock-context.js +1 -0
- package/dist/dock-manager-model.d.ts +136 -0
- package/dist/dock-manager-model.js +514 -0
- package/dist/dock-model.d.ts +105 -0
- package/dist/dock-model.js +288 -0
- package/dist/dock-popout.d.ts +30 -0
- package/dist/dock-popout.js +81 -0
- package/dist/editor-contract.d.ts +2 -0
- package/dist/filter-operators.d.ts +6 -0
- package/dist/filter-operators.js +35 -2
- package/dist/filtering/excel-filters.d.ts +18 -1
- package/dist/filtering/excel-filters.js +56 -0
- package/dist/form-field.d.ts +73 -0
- package/dist/form-field.js +15 -0
- package/dist/index.d.ts +25 -5
- package/dist/index.js +40 -4
- package/dist/list-nav.d.ts +18 -0
- package/dist/list-nav.js +30 -0
- package/dist/list-option.d.ts +2 -0
- package/dist/menu-item.d.ts +20 -0
- package/dist/menu-item.js +1 -0
- package/dist/menus.d.ts +5 -0
- package/dist/menus.js +68 -16
- package/dist/panel-resize.d.ts +25 -0
- package/dist/panel-resize.js +40 -0
- package/dist/popover.d.ts +29 -3
- package/dist/popover.js +22 -6
- package/dist/recurrence.d.ts +33 -7
- package/dist/recurrence.js +151 -13
- package/dist/row-drag.d.ts +8 -0
- package/dist/row-drag.js +11 -0
- package/dist/scheduler-ical.d.ts +30 -0
- package/dist/scheduler-ical.js +180 -0
- package/dist/scheduler-model.d.ts +269 -0
- package/dist/scheduler-model.js +602 -0
- package/dist/scheduler-view.svelte.d.ts +22 -0
- package/dist/scheduler-view.svelte.js +13 -0
- package/dist/spreadsheet.js +20 -12
- package/dist/ui-app.types.d.ts +3 -0
- package/package.json +14 -10
- package/src/DockNodeView.svelte +306 -0
- package/src/DockPopoutHost.svelte +17 -0
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +178 -178
- package/src/GridMenus.svelte +83 -3
- package/src/SvAutoComplete.svelte +5 -3
- package/src/SvCarousel.svelte +141 -163
- package/src/SvCarousel.test.ts +68 -0
- package/src/SvColorInput.svelte +187 -186
- package/src/SvComboBox.svelte +7 -3
- package/src/SvCommand.svelte +28 -85
- package/src/SvCommand.test.ts +78 -0
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +1 -1
- package/src/SvDateRangeInput.svelte +1 -1
- package/src/SvDateTimePicker.svelte +9 -3
- package/src/SvDockLayout.svelte +244 -0
- package/src/SvDockManager.svelte +922 -0
- package/src/SvDrawer.svelte +250 -228
- package/src/SvDropDownList.svelte +85 -13
- package/src/SvField.svelte +21 -4
- package/src/SvForm.svelte +201 -91
- package/src/SvForm.test.ts +299 -0
- package/src/SvGrid.controller.svelte.ts +3158 -3034
- package/src/SvGrid.css +193 -5
- package/src/SvGrid.svelte +3131 -2786
- package/src/SvGrid.types.ts +1489 -1027
- package/src/SvGridBoard.svelte +2339 -2339
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridDropdown.svelte +689 -682
- package/src/SvGridSelect.svelte +1 -0
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvListBox.svelte +36 -14
- package/src/SvMaskedInput.svelte +4 -3
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -73
- package/src/SvMenuList.svelte +117 -165
- package/src/SvModal.svelte +12 -29
- package/src/SvModal.test.ts +82 -0
- package/src/SvMultiSelect.svelte +1 -0
- package/src/SvNumberInput.svelte +131 -130
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPagination.svelte +18 -18
- package/src/SvPasswordInput.svelte +3 -2
- package/src/SvPhoneInput.svelte +3 -2
- package/src/SvPopover.svelte +139 -139
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvStepper.svelte +6 -9
- package/src/SvTagsInput.svelte +3 -2
- package/src/SvTooltip.svelte +127 -138
- package/src/SvTooltip.test.ts +73 -0
- package/src/SvTour.svelte +204 -176
- package/src/SvTree.svelte +377 -377
- package/src/SvTreeSelect.svelte +230 -229
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/build-api.ts +870 -847
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart.ts +2296 -2296
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/columns.ts +5 -0
- package/src/conditional-formatting.test.ts +104 -0
- package/src/conditional-formatting.ts +176 -19
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1111 -1111
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCarousel.svelte.ts +104 -0
- package/src/createCombobox.svelte.ts +179 -179
- package/src/createCommand.svelte.ts +125 -0
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +162 -162
- package/src/createForm.svelte.ts +333 -0
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createMenu.svelte.ts +202 -0
- package/src/createOverlay.svelte.ts +101 -0
- package/src/createPagination.test.ts +65 -0
- package/src/createPagination.ts +84 -0
- package/src/createPopoverSelect.svelte.ts +206 -210
- package/src/createStepper.test.ts +62 -0
- package/src/createStepper.ts +61 -0
- package/src/createTooltip.svelte.ts +86 -0
- package/src/createTree.svelte.ts +319 -319
- package/src/datetime/timezone.test.ts +69 -0
- package/src/datetime/timezone.ts +134 -0
- package/src/dock-context.ts +47 -0
- package/src/dock-manager-model.test.ts +275 -0
- package/src/dock-manager-model.ts +593 -0
- package/src/dock-model.test.ts +163 -0
- package/src/dock-model.ts +368 -0
- package/src/dock-popout.ts +94 -0
- package/src/editing.test.ts +859 -859
- package/src/editing.ts +675 -675
- package/src/editor-contract.ts +2 -0
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/filter-operators.test.ts +59 -0
- package/src/filter-operators.ts +35 -2
- package/src/filtering/excel-filters.test.ts +86 -1
- package/src/filtering/excel-filters.ts +65 -0
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +87 -0
- package/src/index.ts +623 -469
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -0
- package/src/list-nav.ts +29 -0
- package/src/list-option.ts +2 -0
- package/src/menu-item.ts +21 -0
- package/src/menus.test.ts +48 -0
- package/src/menus.ts +639 -582
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/panel-resize.ts +52 -0
- package/src/popover.bounds.test.ts +93 -0
- package/src/popover.ts +51 -7
- package/src/recurrence.test.ts +88 -1
- package/src/recurrence.ts +166 -15
- package/src/render-component.ts +28 -28
- package/src/row-drag.ts +10 -0
- package/src/scheduler-ical.test.ts +68 -0
- package/src/scheduler-ical.ts +180 -0
- package/src/scheduler-model.test.ts +546 -0
- package/src/scheduler-model.ts +853 -0
- package/src/scheduler-view.svelte.ts +33 -0
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +312 -304
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +501 -501
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.charting.test.ts +527 -527
- package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +159 -159
- package/src/ui-app.types.ts +3 -0
- package/src/virtual.test.ts +6 -4
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
|
@@ -0,0 +1,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
|
+
}
|