@svgrid/grid 2.1.21 → 2.1.23

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 (219) 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/GridMenus.svelte +83 -3
  7. package/dist/SvAutoComplete.svelte +5 -3
  8. package/dist/SvCarousel.svelte +20 -42
  9. package/dist/SvColorInput.svelte +4 -3
  10. package/dist/SvColorInput.svelte.d.ts +1 -1
  11. package/dist/SvComboBox.svelte +7 -3
  12. package/dist/SvCommand.svelte +28 -85
  13. package/dist/SvCountryInput.svelte +1 -1
  14. package/dist/SvDateRangeInput.svelte +1 -1
  15. package/dist/SvDateTimePicker.svelte +9 -3
  16. package/dist/SvDateTimePicker.svelte.d.ts +3 -0
  17. package/dist/SvDockLayout.svelte +244 -0
  18. package/dist/SvDockLayout.svelte.d.ts +77 -0
  19. package/dist/SvDockManager.svelte +922 -0
  20. package/dist/SvDockManager.svelte.d.ts +106 -0
  21. package/dist/SvDrawer.svelte +69 -47
  22. package/dist/SvDrawer.svelte.d.ts +4 -0
  23. package/dist/SvDropDownList.svelte +85 -13
  24. package/dist/SvDropDownList.svelte.d.ts +3 -0
  25. package/dist/SvField.svelte +21 -4
  26. package/dist/SvField.svelte.d.ts +1 -0
  27. package/dist/SvForm.svelte +201 -91
  28. package/dist/SvForm.svelte.d.ts +15 -22
  29. package/dist/SvGrid.controller.svelte.d.ts +32 -1
  30. package/dist/SvGrid.controller.svelte.js +156 -27
  31. package/dist/SvGrid.css +193 -5
  32. package/dist/SvGrid.svelte +357 -12
  33. package/dist/SvGrid.types.d.ts +452 -1
  34. package/dist/SvGridDropdown.svelte +8 -1
  35. package/dist/SvGridSelect.svelte +1 -0
  36. package/dist/SvListBox.svelte +36 -14
  37. package/dist/SvMaskedInput.svelte +4 -3
  38. package/dist/SvMaskedInput.svelte.d.ts +1 -1
  39. package/dist/SvMenu.svelte +1 -1
  40. package/dist/SvMenuList.svelte +23 -71
  41. package/dist/SvMenuList.svelte.d.ts +2 -13
  42. package/dist/SvModal.svelte +12 -29
  43. package/dist/SvMultiSelect.svelte +1 -0
  44. package/dist/SvNumberInput.svelte +4 -3
  45. package/dist/SvNumberInput.svelte.d.ts +1 -1
  46. package/dist/SvPagination.svelte +18 -18
  47. package/dist/SvPasswordInput.svelte +3 -2
  48. package/dist/SvPasswordInput.svelte.d.ts +1 -1
  49. package/dist/SvPhoneInput.svelte +3 -2
  50. package/dist/SvPhoneInput.svelte.d.ts +1 -1
  51. package/dist/SvPopover.svelte +1 -1
  52. package/dist/SvStepper.svelte +6 -9
  53. package/dist/SvTagsInput.svelte +3 -2
  54. package/dist/SvTagsInput.svelte.d.ts +1 -1
  55. package/dist/SvTooltip.svelte +12 -23
  56. package/dist/SvTour.svelte +32 -4
  57. package/dist/SvTreeSelect.svelte +1 -0
  58. package/dist/build-api.js +76 -54
  59. package/dist/cdn/svgrid.js +10907 -6444
  60. package/dist/cdn/svgrid.svelte-external.js +9622 -5475
  61. package/dist/columns.js +6 -0
  62. package/dist/conditional-formatting.d.ts +67 -3
  63. package/dist/conditional-formatting.js +124 -17
  64. package/dist/createCarousel.svelte.d.ts +72 -0
  65. package/dist/createCarousel.svelte.js +80 -0
  66. package/dist/createCommand.svelte.d.ts +62 -0
  67. package/dist/createCommand.svelte.js +109 -0
  68. package/dist/createForm.svelte.d.ts +62 -0
  69. package/dist/createForm.svelte.js +374 -0
  70. package/dist/createMenu.svelte.d.ts +88 -0
  71. package/dist/createMenu.svelte.js +153 -0
  72. package/dist/createOverlay.svelte.d.ts +38 -0
  73. package/dist/createOverlay.svelte.js +68 -0
  74. package/dist/createPagination.d.ts +74 -0
  75. package/dist/createPagination.js +71 -0
  76. package/dist/createPopoverSelect.svelte.js +6 -12
  77. package/dist/createStepper.d.ts +44 -0
  78. package/dist/createStepper.js +26 -0
  79. package/dist/createTooltip.svelte.d.ts +32 -0
  80. package/dist/createTooltip.svelte.js +61 -0
  81. package/dist/datetime/timezone.d.ts +36 -0
  82. package/dist/datetime/timezone.js +110 -0
  83. package/dist/dock-context.d.ts +52 -0
  84. package/dist/dock-context.js +1 -0
  85. package/dist/dock-manager-model.d.ts +136 -0
  86. package/dist/dock-manager-model.js +514 -0
  87. package/dist/dock-model.d.ts +105 -0
  88. package/dist/dock-model.js +288 -0
  89. package/dist/dock-popout.d.ts +30 -0
  90. package/dist/dock-popout.js +81 -0
  91. package/dist/editor-contract.d.ts +2 -0
  92. package/dist/filter-operators.d.ts +6 -0
  93. package/dist/filter-operators.js +35 -2
  94. package/dist/filtering/excel-filters.d.ts +18 -1
  95. package/dist/filtering/excel-filters.js +56 -0
  96. package/dist/form-field.d.ts +73 -0
  97. package/dist/form-field.js +15 -0
  98. package/dist/index.d.ts +25 -5
  99. package/dist/index.js +40 -4
  100. package/dist/list-nav.d.ts +18 -0
  101. package/dist/list-nav.js +30 -0
  102. package/dist/list-option.d.ts +2 -0
  103. package/dist/menu-item.d.ts +20 -0
  104. package/dist/menu-item.js +1 -0
  105. package/dist/menus.d.ts +5 -0
  106. package/dist/menus.js +68 -16
  107. package/dist/panel-resize.d.ts +25 -0
  108. package/dist/panel-resize.js +40 -0
  109. package/dist/popover.d.ts +29 -3
  110. package/dist/popover.js +22 -6
  111. package/dist/recurrence.d.ts +33 -7
  112. package/dist/recurrence.js +151 -13
  113. package/dist/row-drag.d.ts +8 -0
  114. package/dist/row-drag.js +11 -0
  115. package/dist/scheduler-ical.d.ts +30 -0
  116. package/dist/scheduler-ical.js +180 -0
  117. package/dist/scheduler-model.d.ts +269 -0
  118. package/dist/scheduler-model.js +602 -0
  119. package/dist/scheduler-view.svelte.d.ts +22 -0
  120. package/dist/scheduler-view.svelte.js +13 -0
  121. package/dist/spreadsheet.js +20 -12
  122. package/dist/ui-app.types.d.ts +3 -0
  123. package/package.json +5 -1
  124. package/src/DockNodeView.svelte +306 -0
  125. package/src/DockPopoutHost.svelte +17 -0
  126. package/src/GridMenus.svelte +83 -3
  127. package/src/SvAutoComplete.svelte +5 -3
  128. package/src/SvCarousel.svelte +20 -42
  129. package/src/SvCarousel.test.ts +68 -0
  130. package/src/SvColorInput.svelte +4 -3
  131. package/src/SvComboBox.svelte +7 -3
  132. package/src/SvCommand.svelte +28 -85
  133. package/src/SvCommand.test.ts +78 -0
  134. package/src/SvCountryInput.svelte +1 -1
  135. package/src/SvDateRangeInput.svelte +1 -1
  136. package/src/SvDateTimePicker.svelte +9 -3
  137. package/src/SvDockLayout.svelte +244 -0
  138. package/src/SvDockManager.svelte +922 -0
  139. package/src/SvDrawer.svelte +69 -47
  140. package/src/SvDropDownList.svelte +85 -13
  141. package/src/SvField.svelte +21 -4
  142. package/src/SvForm.svelte +201 -91
  143. package/src/SvForm.test.ts +299 -0
  144. package/src/SvGrid.controller.svelte.ts +152 -28
  145. package/src/SvGrid.css +193 -5
  146. package/src/SvGrid.svelte +357 -12
  147. package/src/SvGrid.types.ts +463 -1
  148. package/src/SvGridDropdown.svelte +8 -1
  149. package/src/SvGridSelect.svelte +1 -0
  150. package/src/SvListBox.svelte +36 -14
  151. package/src/SvMaskedInput.svelte +4 -3
  152. package/src/SvMenu.svelte +1 -1
  153. package/src/SvMenu.test.ts +24 -0
  154. package/src/SvMenuList.svelte +23 -71
  155. package/src/SvModal.svelte +12 -29
  156. package/src/SvModal.test.ts +82 -0
  157. package/src/SvMultiSelect.svelte +1 -0
  158. package/src/SvNumberInput.svelte +4 -3
  159. package/src/SvPagination.svelte +18 -18
  160. package/src/SvPasswordInput.svelte +3 -2
  161. package/src/SvPhoneInput.svelte +3 -2
  162. package/src/SvPopover.svelte +1 -1
  163. package/src/SvStepper.svelte +6 -9
  164. package/src/SvTagsInput.svelte +3 -2
  165. package/src/SvTooltip.svelte +12 -23
  166. package/src/SvTooltip.test.ts +73 -0
  167. package/src/SvTour.svelte +32 -4
  168. package/src/SvTreeSelect.svelte +1 -0
  169. package/src/build-api.ts +74 -51
  170. package/src/columns.ts +5 -0
  171. package/src/conditional-formatting.test.ts +104 -0
  172. package/src/conditional-formatting.ts +176 -19
  173. package/src/createCarousel.svelte.ts +104 -0
  174. package/src/createCommand.svelte.ts +125 -0
  175. package/src/createForm.svelte.ts +333 -0
  176. package/src/createMenu.svelte.ts +202 -0
  177. package/src/createOverlay.svelte.ts +101 -0
  178. package/src/createPagination.test.ts +65 -0
  179. package/src/createPagination.ts +84 -0
  180. package/src/createPopoverSelect.svelte.ts +5 -9
  181. package/src/createStepper.test.ts +62 -0
  182. package/src/createStepper.ts +61 -0
  183. package/src/createTooltip.svelte.ts +86 -0
  184. package/src/datetime/timezone.test.ts +69 -0
  185. package/src/datetime/timezone.ts +134 -0
  186. package/src/dock-context.ts +47 -0
  187. package/src/dock-manager-model.test.ts +275 -0
  188. package/src/dock-manager-model.ts +593 -0
  189. package/src/dock-model.test.ts +163 -0
  190. package/src/dock-model.ts +368 -0
  191. package/src/dock-popout.ts +94 -0
  192. package/src/editor-contract.ts +2 -0
  193. package/src/filter-operators.test.ts +59 -0
  194. package/src/filter-operators.ts +35 -2
  195. package/src/filtering/excel-filters.test.ts +86 -1
  196. package/src/filtering/excel-filters.ts +65 -0
  197. package/src/form-field.ts +87 -0
  198. package/src/index.ts +158 -4
  199. package/src/list-nav.test.ts +49 -0
  200. package/src/list-nav.ts +29 -0
  201. package/src/list-option.ts +2 -0
  202. package/src/menu-item.ts +21 -0
  203. package/src/menus.test.ts +48 -0
  204. package/src/menus.ts +73 -16
  205. package/src/panel-resize.ts +52 -0
  206. package/src/popover.bounds.test.ts +93 -0
  207. package/src/popover.ts +51 -7
  208. package/src/recurrence.test.ts +88 -1
  209. package/src/recurrence.ts +166 -15
  210. package/src/row-drag.ts +10 -0
  211. package/src/scheduler-ical.test.ts +68 -0
  212. package/src/scheduler-ical.ts +180 -0
  213. package/src/scheduler-model.test.ts +546 -0
  214. package/src/scheduler-model.ts +853 -0
  215. package/src/scheduler-view.svelte.ts +33 -0
  216. package/src/spreadsheet.ts +20 -12
  217. package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
  218. package/src/ui-app.types.ts +3 -0
  219. package/src/virtual.test.ts +6 -4
@@ -0,0 +1,922 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvDockManager - a full docking manager built on <SvDockLayout>. It adds the
4
+ * three "pro" features a tiled layout alone does not have:
5
+ *
6
+ * - Floating / pop-out windows: drag a tab out of the dock (or hit the float
7
+ * button) to pop it into a movable, resizable window; drag it back to redock.
8
+ * - Tab reordering: drag a tab along its own strip to reorder it.
9
+ * - Pinning / auto-hide: collapse a panel to an edge strip; a fly-out reveals
10
+ * it on hover, and "pin" docks it back.
11
+ *
12
+ * The docked area is an <SvDockLayout> in "manager mode" (it delegates every
13
+ * gesture here); floating windows are each a small <SvDockLayout> too. The
14
+ * whole workspace is the serializable `DockManagerState` from `dock-manager-model`.
15
+ *
16
+ * Build the initial workspace with the base `dockGroup`/`dockTabs`/`dockPane`
17
+ * helpers:
18
+ *
19
+ * let workspace = $state({
20
+ * main: dockGroup('row', [dockTabs([dockPane('a','A')]), dockTabs([dockPane('b','B')])]),
21
+ * floating: [], autoHide: [],
22
+ * })
23
+ * <SvDockManager bind:workspace>{#snippet pane(p)} ... {/snippet}</SvDockManager>
24
+ */
25
+ import { onDestroy, type Snippet } from 'svelte'
26
+ import { fly } from 'svelte/transition'
27
+ import SvDockLayout from './SvDockLayout.svelte'
28
+ import type { DockPane, DockTabs, DockZone } from './dock-model'
29
+ import {
30
+ reorderTab,
31
+ setManagerActive,
32
+ resizeGroup,
33
+ closePane,
34
+ dockPaneOnto,
35
+ floatPane,
36
+ dockPaneToEmptyMain,
37
+ addPaneToMain,
38
+ dockWindowOnto,
39
+ moveWindow,
40
+ resizeWindow,
41
+ bringToFront,
42
+ setWindowMinimized,
43
+ toggleWindowMaximized,
44
+ closeWindow,
45
+ autoHideWindow,
46
+ autoHideLeaf,
47
+ autoHidePaneToSide,
48
+ pinAutoHidden,
49
+ setAutoHideSize,
50
+ toggleMaximizeLeaf,
51
+ findLeafById,
52
+ allManagerIds,
53
+ type DockManagerState,
54
+ type DockSide,
55
+ type AutoHideEntry,
56
+ } from './dock-manager-model'
57
+ import { openPopout, type Popout } from './dock-popout'
58
+
59
+ /** Imperative handle exposed via `onReady`. */
60
+ export type DockManagerApi = {
61
+ getState: () => DockManagerState
62
+ setState: (s: DockManagerState) => void
63
+ float: (paneId: string) => void
64
+ popout: (paneId: string) => void
65
+ autoHide: (paneId: string) => void
66
+ maximize: (tabsId: string) => void
67
+ close: (paneId: string) => void
68
+ focus: (paneId: string) => void
69
+ }
70
+
71
+ /** Granular lifecycle events (in addition to `onChange`). */
72
+ export type DockEvent =
73
+ | { type: 'activePane'; paneId: string; tabsId: string }
74
+ | { type: 'close'; paneId: string }
75
+ | { type: 'float'; paneId: string }
76
+ | { type: 'popout'; paneId: string }
77
+ | { type: 'autoHide'; paneId: string; side: DockSide }
78
+ | { type: 'pin'; paneId: string }
79
+ | { type: 'maximize'; tabsId: string; maximized: boolean }
80
+ | { type: 'window'; windowId: string; action: 'minimize' | 'maximize' | 'close' | 'autoHide' }
81
+ | { type: 'focus'; paneId: string }
82
+
83
+ type Props = {
84
+ /** The whole workspace: tiled `main` + `floating` windows + `autoHide` edges. Bindable. */
85
+ workspace: DockManagerState
86
+ /** Renders each pane's content, keyed off the `DockPane`. */
87
+ pane: Snippet<[DockPane]>
88
+ /** Notified after any change. */
89
+ onChange?: (workspace: DockManagerState) => void
90
+ /** Granular lifecycle events (activePane / close / float / popout / ...). */
91
+ onEvent?: (event: DockEvent) => void
92
+ /** Minimum pane size in px along a split. Default 80. */
93
+ minSize?: number
94
+ /** Allow dragging tabs to reorder them within a strip. Default `true`. */
95
+ reorderEnabled?: boolean
96
+ /** Which side of each panel its tab strip sits on. Default `'top'`. */
97
+ headerPosition?: 'top' | 'bottom' | 'left' | 'right'
98
+ /** Keep inactive tabs mounted (hidden) so their content state persists.
99
+ * Default `false` (only the active tab renders). */
100
+ keepAlive?: boolean
101
+ /** Hide the tab button on a single-pane leaf (no redundant title bar; the panel
102
+ * actions still show). Applies to the docked area, floating windows, and fly-outs. */
103
+ hideSingleTab?: boolean
104
+ /** Offer the "pop out to a new browser window" panel action. Default `false` -
105
+ * most apps only need in-app floating windows, so the OS pop-out stays hidden
106
+ * unless you opt in. The `popout()` API still works regardless. */
107
+ allowPopout?: boolean
108
+ /** Locked / split mode: a fixed set of resizable panes. Tabs can't be dragged
109
+ * (no reorder / float / dock) and the panel action buttons are hidden - only
110
+ * splitter resize + tab switching remain. Turns the manager into a plain
111
+ * split-view workspace. Default `false`. */
112
+ locked?: boolean
113
+ /** Receive the imperative API once, on mount. */
114
+ onReady?: (api: DockManagerApi) => void
115
+ }
116
+
117
+ let {
118
+ workspace = $bindable(),
119
+ pane,
120
+ onChange,
121
+ onEvent,
122
+ minSize = 80,
123
+ reorderEnabled = true,
124
+ headerPosition = 'top',
125
+ keepAlive = false,
126
+ hideSingleTab = false,
127
+ allowPopout = false,
128
+ locked = false,
129
+ onReady,
130
+ }: Props = $props()
131
+ const emit = (e: DockEvent) => onEvent?.(e)
132
+
133
+ let rootEl = $state<HTMLElement | null>(null)
134
+ // Runtime-only: OS window handles for popped-out panes (not serializable).
135
+ const popouts = new Map<string, Popout>()
136
+ let nextId = mgrBase()
137
+ // Never mint an id that already lives in the workspace. The base counter is
138
+ // module-scoped and resets on reload, so a workspace restored from storage can
139
+ // already hold the `dm-N` we're about to generate - skip past any collision so
140
+ // keyed lists never see a duplicate.
141
+ const genId = (): string => {
142
+ const used = new Set(allManagerIds(workspace))
143
+ let id = `dm-${nextId++}`
144
+ while (used.has(id)) id = `dm-${nextId++}`
145
+ return id
146
+ }
147
+ function commit(next: DockManagerState) { workspace = next; onChange?.(next) }
148
+
149
+ // Focus tracking (which pane the user last engaged) for the active-panel ring.
150
+ let focusedPaneId = $state<string | null>(null)
151
+ function setFocus(paneId: string | null) {
152
+ if (paneId === focusedPaneId) return
153
+ focusedPaneId = paneId
154
+ if (paneId) emit({ type: 'focus', paneId })
155
+ }
156
+ const focusedLeafId = $derived.by(() => {
157
+ if (!focusedPaneId) return null
158
+ const scan = (n: any): string | null => {
159
+ if (n.type === 'tabs') return n.panes.some((p: DockPane) => p.id === focusedPaneId) ? n.id : null
160
+ for (const c of n.children) { const id = scan(c); if (id) return id }
161
+ return null
162
+ }
163
+ if (workspace.main) { const id = scan(workspace.main); if (id) return id }
164
+ const w = workspace.floating.find((f) => f.leaf.panes.some((p) => p.id === focusedPaneId))
165
+ return w ? w.leaf.id : null
166
+ })
167
+
168
+ // Controlled handlers each SvDockLayout routes back to the manager.
169
+ const onActivate = (tabsId: string, i: number) => {
170
+ const paneId = findLeafById(workspace, tabsId)?.panes[i]?.id
171
+ commit(setManagerActive(workspace, tabsId, i))
172
+ if (paneId) { setFocus(paneId); emit({ type: 'activePane', tabsId, paneId }) }
173
+ }
174
+ // Reveal an auto-hidden leaf's flyout on a specific pane (clicked from the
175
+ // edge rail). Toggles closed when the already-open pane is clicked again.
176
+ function revealAutoHidePane(e: AutoHideEntry, pi: number) {
177
+ if (revealed === e.id && e.leaf.active === pi) { revealed = null; return }
178
+ if (e.leaf.active !== pi) onActivate(e.leaf.id, pi)
179
+ revealed = e.id
180
+ }
181
+ const onClose = (paneId: string) => { commit(closePane(workspace, paneId)); emit({ type: 'close', paneId }) }
182
+ const onResize = (groupId: string, sizes: number[]) => commit(resizeGroup(workspace, groupId, sizes))
183
+
184
+ function paneTitle(paneId: string): string {
185
+ const inNode = (n: any): string | null => {
186
+ if (n.type === 'tabs') return n.panes.find((p: DockPane) => p.id === paneId)?.title ?? null
187
+ for (const c of n.children) { const t = inNode(c); if (t) return t }
188
+ return null
189
+ }
190
+ if (workspace.main) { const t = inNode(workspace.main); if (t) return t }
191
+ for (const w of workspace.floating) { const p = w.leaf.panes.find((p) => p.id === paneId); if (p) return p.title }
192
+ for (const e of workspace.autoHide) { const p = e.leaf.panes.find((p) => p.id === paneId); if (p) return p.title }
193
+ return ''
194
+ }
195
+
196
+ // ------------------------------------------------------------------ tab drag
197
+ type Target =
198
+ | { kind: 'reorder'; tabsId: string; index: number }
199
+ | { kind: 'dock'; surface: string; tabsId: string; zone: DockZone }
200
+ | { kind: 'autohide'; side: DockSide }
201
+ | { kind: 'empty-main' }
202
+ | { kind: 'float' }
203
+ let ghost = $state<{ title: string; x: number; y: number } | null>(null)
204
+ let target = $state<Target | null>(null)
205
+ let src: { paneId: string; sourceTabsId: string; sx: number; sy: number } | null = null
206
+ let draggingWindow = $state<string | null>(null)
207
+ // The leaf under the pointer while dragging, for the dock guide. `zone` is the
208
+ // guide chip the pointer is on (drop docks there) or null (off the guide -> the
209
+ // drop floats). Shown whether or not a chip is hit so the user can aim.
210
+ let hoverLeaf = $state<{ tabsId: string; surface: string; zone: DockZone | null } | null>(null)
211
+
212
+ // Drop indicators handed to every surface's SvDockLayout (ids are unique, so
213
+ // only the matching leaf paints it).
214
+ const guideGlobal = $derived.by(() =>
215
+ hoverLeaf ? { tabsId: hoverLeaf.tabsId, zone: hoverLeaf.zone, centerOnly: hoverLeaf.surface !== 'main' } : null,
216
+ )
217
+ const reorderTargetGlobal = $derived.by(() =>
218
+ target && target.kind === 'reorder' ? { tabsId: target.tabsId, index: target.index } : null,
219
+ )
220
+ const autohideSide = $derived.by(() => (target && target.kind === 'autohide' ? target.side : null))
221
+
222
+ /** Which guide chip the pointer is over on a leaf, or null (-> float). The
223
+ * geometry mirrors the guide layout in DockNodeView (26px chips, 30px apart). */
224
+ function zoneFromGuide(r: DOMRect, x: number, y: number, edges: boolean): DockZone | null {
225
+ const dx = x - (r.left + r.right) / 2
226
+ const dy = y - (r.top + r.bottom) / 2
227
+ const H = 15, O = 30
228
+ const box = (bx: number, by: number) => Math.abs(dx - bx) <= H && Math.abs(dy - by) <= H
229
+ if (box(0, 0)) return 'center'
230
+ if (!edges) return null
231
+ if (box(-O, 0)) return 'left'
232
+ if (box(O, 0)) return 'right'
233
+ if (box(0, -O)) return 'top'
234
+ if (box(0, O)) return 'bottom'
235
+ return null
236
+ }
237
+
238
+ function beginTabDrag(e: PointerEvent, paneId: string, tabsId: string) {
239
+ setFocus(paneId)
240
+ src = { paneId, sourceTabsId: tabsId, sx: e.clientX, sy: e.clientY }
241
+ window.addEventListener('pointermove', onTabMove)
242
+ window.addEventListener('pointerup', onTabUp)
243
+ }
244
+
245
+ const verticalHeader = $derived(headerPosition === 'left' || headerPosition === 'right')
246
+
247
+ function insertIndex(strip: Element, x: number, y: number): number {
248
+ const tabsEls = Array.from(strip.querySelectorAll('[data-dock-tab]')) as HTMLElement[]
249
+ for (let i = 0; i < tabsEls.length; i++) {
250
+ const r = tabsEls[i]!.getBoundingClientRect()
251
+ const before = verticalHeader ? y < r.top + r.height / 2 : x < r.left + r.width / 2
252
+ if (before) return i
253
+ }
254
+ return tabsEls.length
255
+ }
256
+
257
+ const EDGE_BAND = 22
258
+
259
+ /** Compute the drop action AND set `hoverLeaf` (which drives the dock guide). */
260
+ function computeTarget(x: number, y: number): Target {
261
+ hoverLeaf = null
262
+ const el = document.elementFromPoint(x, y) as HTMLElement | null
263
+ if (!el || !rootEl?.contains(el)) {
264
+ // Outside the manager entirely -> float.
265
+ return { kind: 'float' }
266
+ }
267
+ const leafEl = el.closest('[data-dock-tabs]') as HTMLElement | null
268
+ const strip = el.closest('.sv-dock__tabstrip')
269
+ // Reorder wins over everything when over the source leaf's own strip.
270
+ if (reorderEnabled && leafEl && strip && leafEl.dataset.dockTabs === src!.sourceTabsId) {
271
+ return { kind: 'reorder', tabsId: src!.sourceTabsId, index: insertIndex(strip, x, y) }
272
+ }
273
+ // Near the manager's outer border -> auto-hide to that side. Checked AFTER
274
+ // the source-strip reorder (above) so reordering still wins, but before the
275
+ // guide - so all four edges work, including the top where tab strips live.
276
+ {
277
+ const rr = rootEl.getBoundingClientRect()
278
+ const dl = x - rr.left, dr = rr.right - x, dt = y - rr.top, db = rr.bottom - y
279
+ const mn = Math.min(dl, dr, dt, db)
280
+ if (mn >= 0 && mn <= EDGE_BAND) {
281
+ return { kind: 'autohide', side: mn === dl ? 'left' : mn === dr ? 'right' : mn === dt ? 'top' : 'bottom' }
282
+ }
283
+ }
284
+ if (leafEl) {
285
+ const surface = (leafEl.closest('[data-dock-surface]') as HTMLElement | null)?.dataset.dockSurface ?? 'main'
286
+ const edges = surface === 'main'
287
+ const zone = zoneFromGuide(leafEl.getBoundingClientRect(), x, y, edges)
288
+ // Always show the guide on the hovered leaf; only a chip hit docks.
289
+ hoverLeaf = { tabsId: leafEl.dataset.dockTabs!, surface, zone }
290
+ return zone
291
+ ? { kind: 'dock', surface, tabsId: leafEl.dataset.dockTabs!, zone }
292
+ : { kind: 'float' } // over the leaf but off the guide -> float
293
+ }
294
+ if (el.closest('[data-dock-empty]')) return { kind: 'empty-main' }
295
+ return { kind: 'float' }
296
+ }
297
+
298
+ function onTabMove(e: PointerEvent) {
299
+ if (!src) return
300
+ if (!ghost) {
301
+ if (Math.hypot(e.clientX - src.sx, e.clientY - src.sy) < 5) return
302
+ ghost = { title: paneTitle(src.paneId), x: e.clientX, y: e.clientY }
303
+ document.body.style.userSelect = 'none'
304
+ document.body.style.cursor = 'grabbing'
305
+ }
306
+ ghost = { ...ghost, x: e.clientX, y: e.clientY }
307
+ target = computeTarget(e.clientX, e.clientY)
308
+ }
309
+
310
+ function onTabUp(e: PointerEvent) {
311
+ window.removeEventListener('pointermove', onTabMove)
312
+ window.removeEventListener('pointerup', onTabUp)
313
+ if (src && ghost && target) {
314
+ const { paneId } = src
315
+ if (target.kind === 'reorder') {
316
+ const from = tabIndexOf(target.tabsId, paneId)
317
+ if (from >= 0) commit(reorderTab(workspace, target.tabsId, from, target.index > from ? target.index - 1 : target.index))
318
+ } else if (target.kind === 'dock') {
319
+ commit(dockPaneOnto(workspace, paneId, target.tabsId, target.zone, genId))
320
+ } else if (target.kind === 'autohide') {
321
+ commit(autoHidePaneToSide(workspace, paneId, target.side, genId))
322
+ } else if (target.kind === 'empty-main') {
323
+ commit(dockPaneToEmptyMain(workspace, paneId, genId))
324
+ } else {
325
+ // Float at the drop point, sized from the manager rect.
326
+ const r = rootEl?.getBoundingClientRect()
327
+ const x = (e.clientX - (r?.left ?? 0)) - 40
328
+ const y = (e.clientY - (r?.top ?? 0)) - 12
329
+ commit(floatPane(workspace, paneId, { x: Math.max(0, x), y: Math.max(0, y), width: 340, height: 240 }, genId))
330
+ }
331
+ }
332
+ src = null; ghost = null; target = null; hoverLeaf = null
333
+ document.body.style.userSelect = ''
334
+ document.body.style.cursor = ''
335
+ }
336
+
337
+ function tabIndexOf(tabsId: string, paneId: string): number {
338
+ const find = (n: any): number => {
339
+ if (n.type === 'tabs') return n.id === tabsId ? n.panes.findIndex((p: DockPane) => p.id === paneId) : -1
340
+ for (const c of n.children) { const i = find(c); if (i >= 0) return i }
341
+ return -1
342
+ }
343
+ if (workspace.main) { const i = find(workspace.main); if (i >= 0) return i }
344
+ const w = workspace.floating.find((w) => w.leaf.id === tabsId)
345
+ return w ? w.leaf.panes.findIndex((p) => p.id === paneId) : -1
346
+ }
347
+
348
+ // -------------------------------------------------- floating window move/resize
349
+ function windowDragStart(e: PointerEvent, windowId: string) {
350
+ if (e.button !== 0) return
351
+ commit(bringToFront(workspace, windowId))
352
+ const w = workspace.floating.find((w) => w.id === windowId)
353
+ if (!w) return
354
+ const ox = e.clientX - w.x
355
+ const oy = e.clientY - w.y
356
+ // Make the dragged window transparent to hit-testing so elementFromPoint
357
+ // sees the main leaf underneath (for redock), not the window itself.
358
+ draggingWindow = windowId
359
+ let redock: { tabsId: string; zone: DockZone } | null = null
360
+ let autoHide: DockSide | null = null
361
+ const move = (ev: PointerEvent) => {
362
+ commit(moveWindow(workspace, windowId, ev.clientX - ox, ev.clientY - oy))
363
+ // Near the manager's outer border -> auto-hide to that edge, mirroring the
364
+ // tab-drag path (computeTarget). Checked BEFORE redock so an edge wins over
365
+ // a guide chip that sits near the same border; `target` drives the edge hint.
366
+ const rr = rootEl?.getBoundingClientRect()
367
+ if (rr) {
368
+ const dl = ev.clientX - rr.left, dr = rr.right - ev.clientX, dt = ev.clientY - rr.top, db = rr.bottom - ev.clientY
369
+ const mn = Math.min(dl, dr, dt, db)
370
+ if (mn >= 0 && mn <= EDGE_BAND) {
371
+ autoHide = mn === dl ? 'left' : mn === dr ? 'right' : mn === dt ? 'top' : 'bottom'
372
+ target = { kind: 'autohide', side: autoHide }
373
+ hoverLeaf = null
374
+ redock = null
375
+ return
376
+ }
377
+ }
378
+ autoHide = null
379
+ target = null
380
+ // Over a main leaf's guide chip -> arm a redock (else the window floats).
381
+ const el = document.elementFromPoint(ev.clientX, ev.clientY) as HTMLElement | null
382
+ const leafEl = el?.closest('[data-dock-tabs]') as HTMLElement | null
383
+ const surface = leafEl ? (leafEl.closest('[data-dock-surface]') as HTMLElement | null)?.dataset.dockSurface : undefined
384
+ if (leafEl && surface === 'main') {
385
+ const zone = zoneFromGuide(leafEl.getBoundingClientRect(), ev.clientX, ev.clientY, true)
386
+ hoverLeaf = { tabsId: leafEl.dataset.dockTabs!, surface: 'main', zone }
387
+ redock = zone ? { tabsId: leafEl.dataset.dockTabs!, zone } : null
388
+ } else {
389
+ hoverLeaf = null
390
+ redock = null
391
+ }
392
+ }
393
+ const up = () => {
394
+ window.removeEventListener('pointermove', move)
395
+ window.removeEventListener('pointerup', up)
396
+ if (autoHide) {
397
+ const size = autoHide === 'left' || autoHide === 'right' ? w.width : w.height
398
+ commit(autoHideWindow(workspace, windowId, autoHide, genId, Math.round(size)))
399
+ } else if (redock) {
400
+ commit(dockWindowOnto(workspace, windowId, redock.tabsId, redock.zone, genId))
401
+ }
402
+ draggingWindow = null
403
+ target = null
404
+ hoverLeaf = null
405
+ document.body.style.userSelect = ''
406
+ }
407
+ document.body.style.userSelect = 'none'
408
+ window.addEventListener('pointermove', move)
409
+ window.addEventListener('pointerup', up)
410
+ }
411
+
412
+ function windowResizeStart(e: PointerEvent, windowId: string) {
413
+ if (e.button !== 0) return
414
+ e.stopPropagation()
415
+ const w = workspace.floating.find((w) => w.id === windowId)
416
+ if (!w) return
417
+ const sx = e.clientX, sy = e.clientY, sw = w.width, sh = w.height
418
+ const move = (ev: PointerEvent) => commit(resizeWindow(workspace, windowId, sw + (ev.clientX - sx), sh + (ev.clientY - sy)))
419
+ const up = () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); document.body.style.userSelect = '' }
420
+ document.body.style.userSelect = 'none'
421
+ window.addEventListener('pointermove', move)
422
+ window.addEventListener('pointerup', up)
423
+ }
424
+
425
+ // ------------------------------------------------------------- auto-hide fly-out
426
+ let revealed = $state<string | null>(null) // entry id currently flown out
427
+ const flyoutStyle = (e: { side: DockSide; size: number }) =>
428
+ (e.side === 'left' || e.side === 'right' ? 'width' : 'height') + `: ${e.size}px`
429
+
430
+ // Dismiss an open fly-out on an outside click or Escape (click a strip button
431
+ // to toggle; the fly-out stays put while you interact inside it).
432
+ $effect(() => {
433
+ if (!revealed) return
434
+ const onDown = (ev: PointerEvent) => {
435
+ const el = ev.target as HTMLElement | null
436
+ if (el?.closest('.sv-dockmgr__flyout') || el?.closest('.sv-dockmgr__strip')) return
437
+ revealed = null
438
+ }
439
+ const onKey = (ev: KeyboardEvent) => { if (ev.key === 'Escape') revealed = null }
440
+ window.addEventListener('pointerdown', onDown, true)
441
+ window.addEventListener('keydown', onKey)
442
+ return () => {
443
+ window.removeEventListener('pointerdown', onDown, true)
444
+ window.removeEventListener('keydown', onKey)
445
+ }
446
+ })
447
+
448
+ // Inset the tiled area so edge strips sit beside content, not over it. Must
449
+ // match the fixed strip thickness in CSS (.sv-dockmgr__strip--*).
450
+ const STRIP = 34
451
+ const mainPad = $derived({
452
+ left: workspace.autoHide.some((e) => e.side === 'left') ? STRIP : 0,
453
+ right: workspace.autoHide.some((e) => e.side === 'right') ? STRIP : 0,
454
+ top: workspace.autoHide.some((e) => e.side === 'top') ? STRIP : 0,
455
+ bottom: workspace.autoHide.some((e) => e.side === 'bottom') ? STRIP : 0,
456
+ })
457
+ // The maximized tiled leaf (shown alone), or null. Guarded so a stale id noops.
458
+ const maxLeaf = $derived(workspace.maximizedLeaf ? findLeafById(workspace, workspace.maximizedLeaf) : null)
459
+
460
+ // -------------------------------------------------------- tab-action helpers
461
+ function floatOne(paneId: string) {
462
+ const r = rootEl?.getBoundingClientRect()
463
+ commit(floatPane(workspace, paneId, { x: (r ? r.width : 400) * 0.3, y: 60, width: 340, height: 240 }, genId))
464
+ emit({ type: 'float', paneId })
465
+ }
466
+
467
+ function findPaneObject(paneId: string): DockPane | null {
468
+ const scan = (n: any): DockPane | null => {
469
+ if (n.type === 'tabs') return n.panes.find((p: DockPane) => p.id === paneId) ?? null
470
+ for (const c of n.children) { const hit = scan(c); if (hit) return hit }
471
+ return null
472
+ }
473
+ if (workspace.main) { const m = scan(workspace.main); if (m) return m }
474
+ for (const w of workspace.floating) { const p = w.leaf.panes.find((x) => x.id === paneId); if (p) return p }
475
+ for (const e of workspace.autoHide) { const p = e.leaf.panes.find((x) => x.id === paneId); if (p) return p }
476
+ return null
477
+ }
478
+
479
+ /** Pop a pane out into a REAL browser window. Falls back to a floating window
480
+ * if the browser blocks the pop-up. */
481
+ function popoutPane(paneId: string) {
482
+ if (popouts.has(paneId) || !rootEl) return
483
+ const paneObj = findPaneObject(paneId)
484
+ if (!paneObj) return
485
+ const removed = closePane(workspace, paneId) // pull it out of the layout
486
+ const handle = openPopout({
487
+ pane: paneObj,
488
+ paneSnippet: pane,
489
+ themeSource: rootEl,
490
+ onClose: (id) => popInPane(id),
491
+ })
492
+ if (!handle) { floatOne(paneId); return } // blocked -> float instead
493
+ popouts.set(paneId, handle)
494
+ commit(removed)
495
+ emit({ type: 'popout', paneId })
496
+ }
497
+
498
+ /** Dock a popped-out pane back into the layout (window closed / API call). */
499
+ function popInPane(paneId: string) {
500
+ const handle = popouts.get(paneId)
501
+ if (!handle) return
502
+ popouts.delete(paneId)
503
+ handle.destroy()
504
+ commit(addPaneToMain(workspace, handle.pane, genId))
505
+ }
506
+ function leafIdOfPane(paneId: string): string | null {
507
+ const findLeaf = (n: any): string | null => {
508
+ if (n.type === 'tabs') return n.panes.some((p: DockPane) => p.id === paneId) ? n.id : null
509
+ for (const c of n.children) { const id = findLeaf(c); if (id) return id }
510
+ return null
511
+ }
512
+ return workspace.main ? findLeaf(workspace.main) : null
513
+ }
514
+ const mainRect = () => (rootEl?.querySelector('.sv-dockmgr__main') as HTMLElement | null)?.getBoundingClientRect()
515
+ const leafRect = (tabsId: string) =>
516
+ (rootEl?.querySelector(`[data-dock-tabs="${CSS.escape(tabsId)}"]`) as HTMLElement | null)?.getBoundingClientRect()
517
+
518
+ /** Which edge a tiled leaf is nearest to - so the auto-hide button sends it to
519
+ * the side it already lives on. */
520
+ function nearestSide(tabsId: string): DockSide {
521
+ const lr = leafRect(tabsId), mr = mainRect()
522
+ if (!lr || !mr || mr.width <= 0 || mr.height <= 0) return 'left'
523
+ const cx = (lr.left + lr.right) / 2, cy = (lr.top + lr.bottom) / 2
524
+ const dl = (cx - mr.left) / mr.width, dr = (mr.right - cx) / mr.width
525
+ const dt = (cy - mr.top) / mr.height, db = (mr.bottom - cy) / mr.height
526
+ const m = Math.min(dl, dr, dt, db)
527
+ return m === dl ? 'left' : m === dr ? 'right' : m === dt ? 'top' : 'bottom'
528
+ }
529
+
530
+ function autoHidePane(paneId: string) {
531
+ const tabsId = leafIdOfPane(paneId)
532
+ if (!tabsId) return
533
+ const side = nearestSide(tabsId)
534
+ const lr = leafRect(tabsId)
535
+ const size = lr ? Math.round(side === 'left' || side === 'right' ? lr.width : lr.height) : 260
536
+ commit(autoHideLeaf(workspace, tabsId, side, size))
537
+ emit({ type: 'autoHide', paneId, side })
538
+ }
539
+ function pinPane(paneId: string) {
540
+ const entry = workspace.autoHide.find((e) => e.leaf.panes.some((p) => p.id === paneId))
541
+ if (!entry) return
542
+ const mr = mainRect()
543
+ const dim = mr ? (entry.side === 'left' || entry.side === 'right' ? mr.width : mr.height) : 1000
544
+ const frac = Math.min(0.5, Math.max(0.12, entry.size / (dim || 1000)))
545
+ commit(pinAutoHidden(workspace, entry.id, genId, frac))
546
+ emit({ type: 'pin', paneId })
547
+ }
548
+
549
+ /** Auto-hide a floating window to the manager edge nearest its position. */
550
+ function autoHideWin(windowId: string) {
551
+ const w = workspace.floating.find((f) => f.id === windowId)
552
+ const mr = mainRect()
553
+ if (!w || !mr) return
554
+ const cx = w.x + w.width / 2, cy = w.y + w.height / 2
555
+ const dl = cx, dr = mr.width - cx, dt = cy, db = mr.height - cy
556
+ const m = Math.min(dl, dr, dt, db)
557
+ const side: DockSide = m === dl ? 'left' : m === dr ? 'right' : m === dt ? 'top' : 'bottom'
558
+ const size = side === 'left' || side === 'right' ? w.width : w.height
559
+ commit(autoHideWindow(workspace, windowId, side, genId, Math.round(size)))
560
+ }
561
+
562
+ // Slide-in params for a fly-out, by which edge it lives on.
563
+ const flyIn = (side: DockSide) => ({
564
+ duration: 160,
565
+ x: side === 'left' ? -20 : side === 'right' ? 20 : 0,
566
+ y: side === 'top' ? -20 : side === 'bottom' ? 20 : 0,
567
+ })
568
+
569
+ // Drag the fly-out's inner edge to resize it; size is stored on the entry.
570
+ function flyoutResizeStart(e: PointerEvent, entry: AutoHideEntry) {
571
+ if (e.button !== 0) return
572
+ e.preventDefault(); e.stopPropagation()
573
+ const start = entry.size
574
+ const sx = e.clientX, sy = e.clientY
575
+ const move = (ev: PointerEvent) => {
576
+ const d =
577
+ entry.side === 'left' ? ev.clientX - sx
578
+ : entry.side === 'right' ? sx - ev.clientX
579
+ : entry.side === 'top' ? ev.clientY - sy
580
+ : sy - ev.clientY
581
+ commit(setAutoHideSize(workspace, entry.id, start + d))
582
+ }
583
+ const up = () => {
584
+ window.removeEventListener('pointermove', move)
585
+ window.removeEventListener('pointerup', up)
586
+ document.body.style.userSelect = ''
587
+ }
588
+ document.body.style.userSelect = 'none'
589
+ window.addEventListener('pointermove', move)
590
+ window.addEventListener('pointerup', up)
591
+ }
592
+
593
+ // Imperative API, handed to the consumer once.
594
+ onReady?.({
595
+ getState: () => workspace,
596
+ setState: (s) => commit(s),
597
+ float: (id) => floatOne(id),
598
+ popout: (id) => popoutPane(id),
599
+ autoHide: (id) => autoHidePane(id),
600
+ maximize: (tabsId) => { if (findLeafById(workspace, tabsId)) commit(toggleMaximizeLeaf(workspace, tabsId)) },
601
+ close: (id) => commit(closePane(workspace, id)),
602
+ focus: (id) => setFocus(id),
603
+ })
604
+
605
+ // Close every pop-out window when the manager itself is torn down.
606
+ onDestroy(() => { for (const h of popouts.values()) h.destroy(); popouts.clear() })
607
+ </script>
608
+
609
+ <div class="sv-dockmgr" bind:this={rootEl}>
610
+ <!-- Auto-hide edge strips. Vertical strips inset past the horizontal ones so
611
+ the corners belong to top/bottom and nothing overlaps. -->
612
+ {#each (['left', 'right', 'top', 'bottom'] as DockSide[]) as side (side)}
613
+ {@const entries = workspace.autoHide.filter((e) => e.side === side)}
614
+ {#if entries.length}
615
+ <div
616
+ class="sv-dockmgr__strip sv-dockmgr__strip--{side}"
617
+ style:top={side === 'left' || side === 'right' ? `${mainPad.top}px` : undefined}
618
+ style:bottom={side === 'left' || side === 'right' ? `${mainPad.bottom}px` : undefined}
619
+ >
620
+ {#each entries as e (e.id)}
621
+ <!-- Auto-hide applies to the whole leaf, so surface one edge tab per
622
+ pane (not one per entry). Clicking a pane's tab reveals the flyout
623
+ with that pane active. -->
624
+ {#each e.leaf.panes as p, pi (p.id)}
625
+ <button
626
+ type="button"
627
+ class="sv-dockmgr__stab"
628
+ class:is-open={revealed === e.id && e.leaf.active === pi}
629
+ title={p.title}
630
+ onclick={() => revealAutoHidePane(e, pi)}
631
+ >{p.title}</button>
632
+ {/each}
633
+ {/each}
634
+ </div>
635
+ {/if}
636
+ {/each}
637
+
638
+ <!-- Docked (tiled) area. When a leaf is maximized, only that leaf is shown. -->
639
+ <div
640
+ class="sv-dockmgr__main"
641
+ style:padding={`${mainPad.top}px ${mainPad.right}px ${mainPad.bottom}px ${mainPad.left}px`}
642
+ >
643
+ {#if maxLeaf}
644
+ <SvDockLayout
645
+ layout={maxLeaf}
646
+ {pane}
647
+ {minSize}
648
+ {headerPosition}
649
+ focusedLeaf={focusedLeafId}
650
+ {keepAlive}
651
+ {hideSingleTab}
652
+ {locked}
653
+ surface="main"
654
+ onBeginDrag={beginTabDrag}
655
+ externalDrop={guideGlobal}
656
+ externalReorder={reorderTargetGlobal}
657
+ {onActivate}
658
+ {onClose}
659
+ {onResize}
660
+ leafActions={mainLeafActions}
661
+ />
662
+ {:else if workspace.main}
663
+ <SvDockLayout
664
+ layout={workspace.main}
665
+ {pane}
666
+ {minSize}
667
+ {headerPosition}
668
+ focusedLeaf={focusedLeafId}
669
+ {keepAlive}
670
+ {hideSingleTab}
671
+ {locked}
672
+ surface="main"
673
+ onBeginDrag={beginTabDrag}
674
+ externalDrop={guideGlobal}
675
+ externalReorder={reorderTargetGlobal}
676
+ {onActivate}
677
+ {onClose}
678
+ {onResize}
679
+ leafActions={mainLeafActions}
680
+ />
681
+ {:else}
682
+ <div class="sv-dockmgr__empty" data-dock-empty>Drag a panel here</div>
683
+ {/if}
684
+ </div>
685
+
686
+ <!-- Auto-hide fly-out -->
687
+ {#each workspace.autoHide as e (e.id)}
688
+ {#if revealed === e.id}
689
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
690
+ <div
691
+ class="sv-dockmgr__flyout sv-dockmgr__flyout--{e.side}"
692
+ style={flyoutStyle(e)}
693
+ transition:fly={flyIn(e.side)}
694
+ >
695
+ <SvDockLayout
696
+ layout={e.leaf}
697
+ {pane}
698
+ {minSize}
699
+ {headerPosition}
700
+ focusedLeaf={focusedLeafId}
701
+ {keepAlive}
702
+ hideSingleTab={true}
703
+ surface={e.id}
704
+ onBeginDrag={beginTabDrag}
705
+ externalDrop={guideGlobal}
706
+ externalReorder={reorderTargetGlobal}
707
+ {onActivate}
708
+ {onClose}
709
+ {onResize}
710
+ leafActions={flyoutLeafActions}
711
+ />
712
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
713
+ <div
714
+ class="sv-dockmgr__flyresize sv-dockmgr__flyresize--{e.side}"
715
+ onpointerdown={(ev) => flyoutResizeStart(ev, e)}
716
+ ></div>
717
+ </div>
718
+ {/if}
719
+ {/each}
720
+
721
+ <!-- Floating windows -->
722
+ {#each workspace.floating as w (w.id)}
723
+ <div
724
+ class="sv-dockmgr__window"
725
+ class:is-minimized={w.minimized}
726
+ class:is-maximized={w.maximized}
727
+ style:left={w.maximized ? undefined : `${w.x}px`}
728
+ style:top={w.maximized ? undefined : `${w.y}px`}
729
+ style:width={w.maximized ? undefined : `${w.width}px`}
730
+ style:height={w.maximized || w.minimized ? undefined : `${w.height}px`}
731
+ style:z-index={100 + w.z}
732
+ style:pointer-events={draggingWindow === w.id ? 'none' : undefined}
733
+ onpointerdown={() => commit(bringToFront(workspace, w.id))}
734
+ >
735
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
736
+ <div
737
+ class="sv-dockmgr__winbar"
738
+ ondblclick={() => commit(toggleWindowMaximized(workspace, w.id))}
739
+ onpointerdown={(e) => { if (!w.maximized) windowDragStart(e, w.id) }}
740
+ >
741
+ <span class="sv-dockmgr__wintitle">{w.leaf.panes[w.leaf.active]?.title ?? 'Window'}</span>
742
+ <div class="sv-dockmgr__winctl">
743
+ <button type="button" title="Auto-hide" aria-label="Auto-hide window"
744
+ onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); autoHideWin(w.id); emit({ type: 'window', windowId: w.id, action: 'autoHide' }) }}>&#9663;</button>
745
+ <button type="button" title={w.minimized ? 'Restore' : 'Minimize'} aria-label="Minimize window"
746
+ onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); commit(setWindowMinimized(workspace, w.id, !w.minimized)); emit({ type: 'window', windowId: w.id, action: 'minimize' }) }}>&#8211;</button>
747
+ <button type="button" title={w.maximized ? 'Restore' : 'Maximize'} aria-label="Maximize window"
748
+ onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); commit(toggleWindowMaximized(workspace, w.id)); emit({ type: 'window', windowId: w.id, action: 'maximize' }) }}>{w.maximized ? '❐' : '□'}</button>
749
+ <button type="button" class="sv-dockmgr__winclose" title="Close" aria-label="Close window"
750
+ onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); commit(closeWindow(workspace, w.id)); emit({ type: 'window', windowId: w.id, action: 'close' }) }}>&times;</button>
751
+ </div>
752
+ </div>
753
+ {#if !w.minimized}
754
+ <div class="sv-dockmgr__winbody">
755
+ <SvDockLayout
756
+ layout={w.leaf}
757
+ {pane}
758
+ {minSize}
759
+ {headerPosition}
760
+ focusedLeaf={focusedLeafId}
761
+ {keepAlive}
762
+ {hideSingleTab}
763
+ surface={w.id}
764
+ onBeginDrag={beginTabDrag}
765
+ externalDrop={guideGlobal}
766
+ externalReorder={reorderTargetGlobal}
767
+ {onActivate}
768
+ {onClose}
769
+ {onResize}
770
+ />
771
+ </div>
772
+ {#if !w.maximized}
773
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
774
+ <div class="sv-dockmgr__winresize" onpointerdown={(e) => windowResizeStart(e, w.id)}></div>
775
+ {/if}
776
+ {/if}
777
+ </div>
778
+ {/each}
779
+
780
+ <!-- Auto-hide edge drop indicator (drag a tab to the manager border). -->
781
+ {#if autohideSide}
782
+ <div class="sv-dockmgr__edgehint sv-dockmgr__edgehint--{autohideSide}" aria-hidden="true"></div>
783
+ {/if}
784
+
785
+ {#if ghost}
786
+ <div class="sv-dockmgr__ghost" style:left={`${ghost.x + 12}px`} style:top={`${ghost.y + 12}px`}>{ghost.title}</div>
787
+ {/if}
788
+ </div>
789
+
790
+ {#snippet mainLeafActions(leaf: DockTabs)}
791
+ {@const active = leaf.panes[leaf.active] ?? leaf.panes[0]}
792
+ <button type="button" class="sv-dockmgr__pact" title={workspace.maximizedLeaf === leaf.id ? 'Restore' : 'Maximize'} aria-label="Maximize panel"
793
+ onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); const on = workspace.maximizedLeaf !== leaf.id; commit(toggleMaximizeLeaf(workspace, leaf.id)); emit({ type: 'maximize', tabsId: leaf.id, maximized: on }) }}>{workspace.maximizedLeaf === leaf.id ? '❐' : '⤢'}</button>
794
+ <button type="button" class="sv-dockmgr__pact" title="Auto-hide" aria-label="Auto-hide panel"
795
+ onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); if (active) autoHidePane(active.id) }}>&#9663;</button>
796
+ {#if active}
797
+ <button type="button" class="sv-dockmgr__pact" title="Float" aria-label="Float {active.title}"
798
+ onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); floatOne(active.id) }}>&#9634;</button>
799
+ {#if allowPopout}
800
+ <button type="button" class="sv-dockmgr__pact" title="Pop out to a new window" aria-label="Pop out {active.title}"
801
+ onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); popoutPane(active.id) }}>&#10697;</button>
802
+ {/if}
803
+ {/if}
804
+ {/snippet}
805
+
806
+ {#snippet flyoutLeafActions(leaf: DockTabs)}
807
+ {@const active = leaf.panes[leaf.active] ?? leaf.panes[0]}
808
+ {#if active}
809
+ <button type="button" class="sv-dockmgr__pact" title="Pin" aria-label="Pin {active.title}"
810
+ onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); pinPane(active.id) }}>&#128204;</button>
811
+ {/if}
812
+ {/snippet}
813
+
814
+ <script lang="ts" module>
815
+ let MGR = 0
816
+ function mgrBase(): number { MGR += 1; return MGR * 1_000_000 }
817
+ </script>
818
+
819
+ <style>
820
+ .sv-dockmgr { position: relative; width: 100%; height: 100%; min-width: 0; min-height: 0; display: flex; }
821
+ .sv-dockmgr__main { position: relative; flex: 1; min-width: 0; min-height: 0; display: flex; }
822
+ .sv-dockmgr__main > :global(*) { flex: 1; min-width: 0; min-height: 0; }
823
+ .sv-dockmgr__empty {
824
+ flex: 1; display: flex; align-items: center; justify-content: center;
825
+ color: var(--sg-muted, #94a3b8); font-size: 13px; border: 2px dashed var(--sg-border, #e2e8f0);
826
+ border-radius: 10px; margin: 8px;
827
+ }
828
+
829
+ /* Auto-hide edge strips. Fixed 34px thickness == the main-area inset, so a
830
+ strip sits BESIDE the tiled content instead of overlapping it. */
831
+ .sv-dockmgr__strip {
832
+ position: absolute; z-index: 40; display: flex; gap: 4px; padding: 4px;
833
+ background: var(--sg-header-bg, #f6f7f9); box-sizing: border-box;
834
+ }
835
+ .sv-dockmgr__strip--left { left: 0; top: 0; bottom: 0; width: 34px; flex-direction: column; align-items: center; border-right: 1px solid var(--sg-border, #e2e8f0); }
836
+ .sv-dockmgr__strip--right { right: 0; top: 0; bottom: 0; width: 34px; flex-direction: column; align-items: center; border-left: 1px solid var(--sg-border, #e2e8f0); }
837
+ .sv-dockmgr__strip--top { top: 0; left: 0; right: 0; height: 34px; align-items: center; border-bottom: 1px solid var(--sg-border, #e2e8f0); }
838
+ .sv-dockmgr__strip--bottom { bottom: 0; left: 0; right: 0; height: 34px; align-items: center; border-top: 1px solid var(--sg-border, #e2e8f0); }
839
+ .sv-dockmgr__stab {
840
+ font: inherit; font-size: 12px; font-weight: 600; padding: 3px 8px; cursor: pointer;
841
+ color: var(--sg-muted, #64748b); background: var(--sg-bg, #fff);
842
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: 6px; white-space: nowrap;
843
+ display: inline-flex; align-items: center; max-width: 100%;
844
+ }
845
+ .sv-dockmgr__strip--left .sv-dockmgr__stab, .sv-dockmgr__strip--right .sv-dockmgr__stab { writing-mode: vertical-rl; padding: 8px 3px; max-height: 60%; overflow: hidden; }
846
+ .sv-dockmgr__strip--top .sv-dockmgr__stab, .sv-dockmgr__strip--bottom .sv-dockmgr__stab { max-width: 40%; overflow: hidden; text-overflow: ellipsis; }
847
+ .sv-dockmgr__stab:hover, .sv-dockmgr__stab.is-open { color: var(--sg-accent, #2563eb); border-color: var(--sg-accent, #2563eb); }
848
+
849
+ /* Auto-hide fly-out. Capped so revealing never covers the whole workspace. */
850
+ .sv-dockmgr__flyout {
851
+ position: absolute; z-index: 60; display: flex;
852
+ background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0);
853
+ border-radius: 8px; box-shadow: 0 12px 40px -8px rgba(15,23,42,0.45); overflow: hidden;
854
+ }
855
+ .sv-dockmgr__flyout > :global(*) { flex: 1; }
856
+ .sv-dockmgr__flyout--left { left: 35px; top: 4px; bottom: 4px; max-width: 62%; }
857
+ .sv-dockmgr__flyout--right { right: 35px; top: 4px; bottom: 4px; max-width: 62%; }
858
+ .sv-dockmgr__flyout--top { top: 35px; left: 4px; right: 4px; max-height: 62%; }
859
+ .sv-dockmgr__flyout--bottom { bottom: 35px; left: 4px; right: 4px; max-height: 62%; }
860
+
861
+ /* Fly-out resize handle on the panel's inner edge. */
862
+ .sv-dockmgr__flyresize { position: absolute; z-index: 2; touch-action: none; }
863
+ .sv-dockmgr__flyresize--left { top: 0; bottom: 0; right: -3px; width: 7px; cursor: ew-resize; }
864
+ .sv-dockmgr__flyresize--right { top: 0; bottom: 0; left: -3px; width: 7px; cursor: ew-resize; }
865
+ .sv-dockmgr__flyresize--top { left: 0; right: 0; bottom: -3px; height: 7px; cursor: ns-resize; }
866
+ .sv-dockmgr__flyresize--bottom { left: 0; right: 0; top: -3px; height: 7px; cursor: ns-resize; }
867
+ .sv-dockmgr__flyresize:hover { background: color-mix(in srgb, var(--sg-accent, #2563eb) 40%, transparent); }
868
+
869
+ /* Floating windows */
870
+ .sv-dockmgr__window {
871
+ position: absolute; display: flex; flex-direction: column; min-width: 140px; min-height: 90px;
872
+ background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
873
+ box-shadow: 0 18px 50px -12px rgba(15,23,42,0.5); overflow: hidden;
874
+ }
875
+ .sv-dockmgr__window.is-maximized { inset: 4px; width: auto; height: auto; border-radius: 8px; }
876
+ .sv-dockmgr__window.is-minimized { min-height: 0; height: auto; width: 220px; }
877
+ .sv-dockmgr__winbar {
878
+ flex: none; height: 28px; display: flex; align-items: center; gap: 8px; padding: 0 4px 0 10px; cursor: grab;
879
+ background: var(--sg-header-bg, #f6f7f9); border-bottom: 1px solid var(--sg-border, #e2e8f0);
880
+ user-select: none; touch-action: none;
881
+ }
882
+ .sv-dockmgr__window.is-maximized .sv-dockmgr__winbar { cursor: default; }
883
+ .sv-dockmgr__wintitle { flex: 1; min-width: 0; font-size: 12px; font-weight: 700; color: var(--sg-fg, #0f172a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
884
+ .sv-dockmgr__winctl { flex: none; display: flex; align-items: center; gap: 1px; }
885
+ .sv-dockmgr__winctl button {
886
+ display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 20px;
887
+ padding: 0; font-size: 13px; line-height: 1; color: var(--sg-muted, #64748b); cursor: pointer;
888
+ background: none; border: none; border-radius: 5px;
889
+ }
890
+ .sv-dockmgr__winctl button:hover { background: color-mix(in srgb, var(--sg-fg, #0f172a) 8%, transparent); color: var(--sg-fg, #0f172a); }
891
+ .sv-dockmgr__winclose:hover { background: var(--sg-danger, #dc2626) !important; color: #fff !important; }
892
+ .sv-dockmgr__winbody { flex: 1; min-height: 0; display: flex; }
893
+ .sv-dockmgr__winbody > :global(*) { flex: 1; min-width: 0; min-height: 0; }
894
+ .sv-dockmgr__winresize {
895
+ position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize; touch-action: none;
896
+ background: linear-gradient(135deg, transparent 50%, var(--sg-border, #cbd5e1) 50%);
897
+ }
898
+
899
+ .sv-dockmgr__pact {
900
+ display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px;
901
+ padding: 0; font-size: 11px; line-height: 1; color: var(--sg-muted, #94a3b8); cursor: pointer;
902
+ background: none; border: none; border-radius: 4px;
903
+ }
904
+ .sv-dockmgr__pact:hover { background: var(--sg-row-hover-bg, #f1f5f9); color: var(--sg-accent, #2563eb); }
905
+
906
+ /* Auto-hide edge drop indicator: a bar along the manager border. */
907
+ .sv-dockmgr__edgehint {
908
+ position: absolute; z-index: 70; pointer-events: none;
909
+ background: color-mix(in srgb, var(--sg-accent, #2563eb) 30%, transparent);
910
+ border: 2px solid var(--sg-accent, #2563eb); border-radius: 6px;
911
+ }
912
+ .sv-dockmgr__edgehint--left { left: 2px; top: 2px; bottom: 2px; width: 46px; }
913
+ .sv-dockmgr__edgehint--right { right: 2px; top: 2px; bottom: 2px; width: 46px; }
914
+ .sv-dockmgr__edgehint--top { top: 2px; left: 2px; right: 2px; height: 40px; }
915
+ .sv-dockmgr__edgehint--bottom { bottom: 2px; left: 2px; right: 2px; height: 40px; }
916
+
917
+ .sv-dockmgr__ghost {
918
+ position: fixed; z-index: 2147483646; pointer-events: none; padding: 5px 10px; font-size: 12.5px;
919
+ font-weight: 600; color: var(--sg-on-accent, #fff); background: var(--sg-accent, #2563eb);
920
+ border-radius: 6px; box-shadow: 0 8px 24px -6px rgba(15,23,42,0.5);
921
+ }
922
+ </style>