@svgrid/grid 2.1.22 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/README.md +9 -2
  2. package/dist/DockNodeView.svelte +306 -0
  3. package/dist/DockNodeView.svelte.d.ts +7 -0
  4. package/dist/DockPopoutHost.svelte +17 -0
  5. package/dist/DockPopoutHost.svelte.d.ts +14 -0
  6. package/dist/FlexRender.svelte +96 -96
  7. package/dist/GridFooter.svelte +178 -178
  8. package/dist/GridMenus.svelte +83 -3
  9. package/dist/SvAutoComplete.svelte +5 -3
  10. package/dist/SvCarousel.svelte +141 -163
  11. package/dist/SvColorInput.svelte +187 -186
  12. package/dist/SvColorInput.svelte.d.ts +1 -1
  13. package/dist/SvComboBox.svelte +7 -3
  14. package/dist/SvCommand.svelte +28 -85
  15. package/dist/SvContextMenu.svelte +116 -116
  16. package/dist/SvCountryInput.svelte +1 -1
  17. package/dist/SvDateRangeInput.svelte +1 -1
  18. package/dist/SvDateTimePicker.svelte +9 -3
  19. package/dist/SvDateTimePicker.svelte.d.ts +3 -0
  20. package/dist/SvDockLayout.svelte +244 -0
  21. package/dist/SvDockLayout.svelte.d.ts +77 -0
  22. package/dist/SvDockManager.svelte +922 -0
  23. package/dist/SvDockManager.svelte.d.ts +106 -0
  24. package/dist/SvDrawer.svelte +250 -228
  25. package/dist/SvDrawer.svelte.d.ts +4 -0
  26. package/dist/SvDropDownList.svelte +85 -13
  27. package/dist/SvDropDownList.svelte.d.ts +3 -0
  28. package/dist/SvField.svelte +21 -4
  29. package/dist/SvField.svelte.d.ts +1 -0
  30. package/dist/SvForm.svelte +201 -91
  31. package/dist/SvForm.svelte.d.ts +15 -22
  32. package/dist/SvGrid.controller.svelte.d.ts +32 -1
  33. package/dist/SvGrid.controller.svelte.js +156 -27
  34. package/dist/SvGrid.css +193 -5
  35. package/dist/SvGrid.svelte +3131 -2786
  36. package/dist/SvGrid.types.d.ts +452 -1
  37. package/dist/SvGridBoard.svelte +2339 -2339
  38. package/dist/SvGridChart.svelte +1724 -1724
  39. package/dist/SvGridChartPanel.svelte +485 -485
  40. package/dist/SvGridDropdown.svelte +689 -682
  41. package/dist/SvGridSelect.svelte +1 -0
  42. package/dist/SvGroupCell.svelte +109 -109
  43. package/dist/SvListBox.svelte +36 -14
  44. package/dist/SvMaskedInput.svelte +4 -3
  45. package/dist/SvMaskedInput.svelte.d.ts +1 -1
  46. package/dist/SvMenu.svelte +124 -124
  47. package/dist/SvMenuList.svelte +117 -165
  48. package/dist/SvMenuList.svelte.d.ts +2 -13
  49. package/dist/SvModal.svelte +12 -29
  50. package/dist/SvMultiSelect.svelte +1 -0
  51. package/dist/SvNumberInput.svelte +131 -130
  52. package/dist/SvNumberInput.svelte.d.ts +1 -1
  53. package/dist/SvOtpInput.svelte +158 -158
  54. package/dist/SvPagination.svelte +18 -18
  55. package/dist/SvPasswordInput.svelte +3 -2
  56. package/dist/SvPasswordInput.svelte.d.ts +1 -1
  57. package/dist/SvPhoneInput.svelte +3 -2
  58. package/dist/SvPhoneInput.svelte.d.ts +1 -1
  59. package/dist/SvPopover.svelte +139 -139
  60. package/dist/SvRowGroupPanel.svelte +170 -170
  61. package/dist/SvScrollArea.svelte +61 -61
  62. package/dist/SvStepper.svelte +6 -9
  63. package/dist/SvTagsInput.svelte +3 -2
  64. package/dist/SvTagsInput.svelte.d.ts +1 -1
  65. package/dist/SvTooltip.svelte +127 -138
  66. package/dist/SvTour.svelte +204 -176
  67. package/dist/SvTree.svelte +377 -377
  68. package/dist/SvTreeSelect.svelte +230 -229
  69. package/dist/build-api.js +76 -54
  70. package/dist/cdn/svgrid.js +10913 -6450
  71. package/dist/cdn/svgrid.svelte-external.js +9628 -5481
  72. package/dist/chart-export.js +8 -8
  73. package/dist/columns.js +6 -0
  74. package/dist/conditional-formatting.d.ts +67 -3
  75. package/dist/conditional-formatting.js +124 -17
  76. package/dist/createCarousel.svelte.d.ts +72 -0
  77. package/dist/createCarousel.svelte.js +80 -0
  78. package/dist/createCommand.svelte.d.ts +62 -0
  79. package/dist/createCommand.svelte.js +109 -0
  80. package/dist/createForm.svelte.d.ts +62 -0
  81. package/dist/createForm.svelte.js +374 -0
  82. package/dist/createMenu.svelte.d.ts +88 -0
  83. package/dist/createMenu.svelte.js +153 -0
  84. package/dist/createOverlay.svelte.d.ts +38 -0
  85. package/dist/createOverlay.svelte.js +68 -0
  86. package/dist/createPagination.d.ts +74 -0
  87. package/dist/createPagination.js +71 -0
  88. package/dist/createPopoverSelect.svelte.js +6 -12
  89. package/dist/createStepper.d.ts +44 -0
  90. package/dist/createStepper.js +26 -0
  91. package/dist/createTooltip.svelte.d.ts +32 -0
  92. package/dist/createTooltip.svelte.js +61 -0
  93. package/dist/datetime/timezone.d.ts +36 -0
  94. package/dist/datetime/timezone.js +110 -0
  95. package/dist/dock-context.d.ts +52 -0
  96. package/dist/dock-context.js +1 -0
  97. package/dist/dock-manager-model.d.ts +136 -0
  98. package/dist/dock-manager-model.js +514 -0
  99. package/dist/dock-model.d.ts +105 -0
  100. package/dist/dock-model.js +288 -0
  101. package/dist/dock-popout.d.ts +30 -0
  102. package/dist/dock-popout.js +81 -0
  103. package/dist/editor-contract.d.ts +2 -0
  104. package/dist/filter-operators.d.ts +6 -0
  105. package/dist/filter-operators.js +35 -2
  106. package/dist/filtering/excel-filters.d.ts +18 -1
  107. package/dist/filtering/excel-filters.js +56 -0
  108. package/dist/form-field.d.ts +73 -0
  109. package/dist/form-field.js +15 -0
  110. package/dist/index.d.ts +25 -5
  111. package/dist/index.js +40 -4
  112. package/dist/list-nav.d.ts +18 -0
  113. package/dist/list-nav.js +30 -0
  114. package/dist/list-option.d.ts +2 -0
  115. package/dist/menu-item.d.ts +20 -0
  116. package/dist/menu-item.js +1 -0
  117. package/dist/menus.d.ts +5 -0
  118. package/dist/menus.js +68 -16
  119. package/dist/panel-resize.d.ts +25 -0
  120. package/dist/panel-resize.js +40 -0
  121. package/dist/popover.d.ts +29 -3
  122. package/dist/popover.js +22 -6
  123. package/dist/recurrence.d.ts +33 -7
  124. package/dist/recurrence.js +151 -13
  125. package/dist/row-drag.d.ts +8 -0
  126. package/dist/row-drag.js +11 -0
  127. package/dist/scheduler-ical.d.ts +30 -0
  128. package/dist/scheduler-ical.js +180 -0
  129. package/dist/scheduler-model.d.ts +269 -0
  130. package/dist/scheduler-model.js +602 -0
  131. package/dist/scheduler-view.svelte.d.ts +22 -0
  132. package/dist/scheduler-view.svelte.js +13 -0
  133. package/dist/spreadsheet.js +20 -12
  134. package/dist/ui-app.types.d.ts +3 -0
  135. package/package.json +14 -10
  136. package/src/DockNodeView.svelte +306 -0
  137. package/src/DockPopoutHost.svelte +17 -0
  138. package/src/FlexRender.svelte +96 -96
  139. package/src/GridFooter.svelte +178 -178
  140. package/src/GridMenus.svelte +83 -3
  141. package/src/SvAutoComplete.svelte +5 -3
  142. package/src/SvCarousel.svelte +141 -163
  143. package/src/SvCarousel.test.ts +68 -0
  144. package/src/SvColorInput.svelte +187 -186
  145. package/src/SvComboBox.svelte +7 -3
  146. package/src/SvCommand.svelte +28 -85
  147. package/src/SvCommand.test.ts +78 -0
  148. package/src/SvContextMenu.svelte +116 -116
  149. package/src/SvCountryInput.svelte +1 -1
  150. package/src/SvDateRangeInput.svelte +1 -1
  151. package/src/SvDateTimePicker.svelte +9 -3
  152. package/src/SvDockLayout.svelte +244 -0
  153. package/src/SvDockManager.svelte +922 -0
  154. package/src/SvDrawer.svelte +250 -228
  155. package/src/SvDropDownList.svelte +85 -13
  156. package/src/SvField.svelte +21 -4
  157. package/src/SvForm.svelte +201 -91
  158. package/src/SvForm.test.ts +299 -0
  159. package/src/SvGrid.controller.svelte.ts +3158 -3034
  160. package/src/SvGrid.css +193 -5
  161. package/src/SvGrid.svelte +3131 -2786
  162. package/src/SvGrid.types.ts +1489 -1027
  163. package/src/SvGridBoard.svelte +2339 -2339
  164. package/src/SvGridChart.svelte +1724 -1724
  165. package/src/SvGridChartPanel.svelte +485 -485
  166. package/src/SvGridDropdown.svelte +689 -682
  167. package/src/SvGridSelect.svelte +1 -0
  168. package/src/SvGroupCell.svelte +109 -109
  169. package/src/SvListBox.svelte +36 -14
  170. package/src/SvMaskedInput.svelte +4 -3
  171. package/src/SvMenu.svelte +124 -124
  172. package/src/SvMenu.test.ts +97 -73
  173. package/src/SvMenuList.svelte +117 -165
  174. package/src/SvModal.svelte +12 -29
  175. package/src/SvModal.test.ts +82 -0
  176. package/src/SvMultiSelect.svelte +1 -0
  177. package/src/SvNumberInput.svelte +131 -130
  178. package/src/SvOtpInput.svelte +158 -158
  179. package/src/SvPagination.svelte +18 -18
  180. package/src/SvPasswordInput.svelte +3 -2
  181. package/src/SvPhoneInput.svelte +3 -2
  182. package/src/SvPopover.svelte +139 -139
  183. package/src/SvRowGroupPanel.svelte +170 -170
  184. package/src/SvScrollArea.svelte +61 -61
  185. package/src/SvStepper.svelte +6 -9
  186. package/src/SvTagsInput.svelte +3 -2
  187. package/src/SvTooltip.svelte +127 -138
  188. package/src/SvTooltip.test.ts +73 -0
  189. package/src/SvTour.svelte +204 -176
  190. package/src/SvTree.svelte +377 -377
  191. package/src/SvTreeSelect.svelte +230 -229
  192. package/src/a11y.contract.test.ts +49 -49
  193. package/src/a11y.test.ts +59 -59
  194. package/src/a11y.ts +61 -61
  195. package/src/build-api.ts +870 -847
  196. package/src/cell-formatting.ts +169 -169
  197. package/src/cell-render.ts +469 -469
  198. package/src/chart-export.ts +201 -201
  199. package/src/chart.ts +2296 -2296
  200. package/src/collaboration.test.ts +104 -104
  201. package/src/collaboration.ts +167 -167
  202. package/src/columns.ts +5 -0
  203. package/src/conditional-formatting.test.ts +104 -0
  204. package/src/conditional-formatting.ts +176 -19
  205. package/src/core.performance.test.ts +30 -30
  206. package/src/core.ts +1111 -1111
  207. package/src/createAutocomplete.svelte.ts +132 -132
  208. package/src/createCarousel.svelte.ts +104 -0
  209. package/src/createCombobox.svelte.ts +179 -179
  210. package/src/createCommand.svelte.ts +125 -0
  211. package/src/createCountryInput.svelte.ts +157 -157
  212. package/src/createDropdownList.svelte.ts +162 -162
  213. package/src/createForm.svelte.ts +333 -0
  214. package/src/createGrid.svelte.ts +42 -42
  215. package/src/createGrid.test.ts +10 -10
  216. package/src/createGridState.svelte.ts +17 -17
  217. package/src/createMenu.svelte.ts +202 -0
  218. package/src/createOverlay.svelte.ts +101 -0
  219. package/src/createPagination.test.ts +65 -0
  220. package/src/createPagination.ts +84 -0
  221. package/src/createPopoverSelect.svelte.ts +206 -210
  222. package/src/createStepper.test.ts +62 -0
  223. package/src/createStepper.ts +61 -0
  224. package/src/createTooltip.svelte.ts +86 -0
  225. package/src/createTree.svelte.ts +319 -319
  226. package/src/datetime/timezone.test.ts +69 -0
  227. package/src/datetime/timezone.ts +134 -0
  228. package/src/dock-context.ts +47 -0
  229. package/src/dock-manager-model.test.ts +275 -0
  230. package/src/dock-manager-model.ts +593 -0
  231. package/src/dock-model.test.ts +163 -0
  232. package/src/dock-model.ts +368 -0
  233. package/src/dock-popout.ts +94 -0
  234. package/src/editing.test.ts +859 -859
  235. package/src/editing.ts +675 -675
  236. package/src/editor-contract.ts +2 -0
  237. package/src/export-data-api.test.ts +126 -126
  238. package/src/export-format.test.ts +107 -107
  239. package/src/export-format.ts +598 -598
  240. package/src/filter-operators.test.ts +59 -0
  241. package/src/filter-operators.ts +35 -2
  242. package/src/filtering/excel-filters.test.ts +86 -1
  243. package/src/filtering/excel-filters.ts +65 -0
  244. package/src/flex-render.ts +3 -3
  245. package/src/form-field.ts +87 -0
  246. package/src/index.ts +623 -469
  247. package/src/keyboard.test.ts +59 -59
  248. package/src/keyboard.ts +97 -97
  249. package/src/list-nav.test.ts +49 -0
  250. package/src/list-nav.ts +29 -0
  251. package/src/list-option.ts +2 -0
  252. package/src/menu-item.ts +21 -0
  253. package/src/menus.test.ts +48 -0
  254. package/src/menus.ts +639 -582
  255. package/src/merge-objects.ts +48 -48
  256. package/src/overlays.test.ts +90 -90
  257. package/src/panel-resize.ts +52 -0
  258. package/src/popover.bounds.test.ts +93 -0
  259. package/src/popover.ts +51 -7
  260. package/src/recurrence.test.ts +88 -1
  261. package/src/recurrence.ts +166 -15
  262. package/src/render-component.ts +28 -28
  263. package/src/row-drag.ts +10 -0
  264. package/src/scheduler-ical.test.ts +68 -0
  265. package/src/scheduler-ical.ts +180 -0
  266. package/src/scheduler-model.test.ts +546 -0
  267. package/src/scheduler-model.ts +853 -0
  268. package/src/scheduler-view.svelte.ts +33 -0
  269. package/src/selection.test.ts +754 -754
  270. package/src/selection.ts +600 -600
  271. package/src/server-data-source.test.ts +289 -289
  272. package/src/server-data-source.ts +413 -413
  273. package/src/sparkline.test.ts +68 -68
  274. package/src/sparkline.ts +169 -169
  275. package/src/spreadsheet.test.ts +489 -489
  276. package/src/spreadsheet.ts +312 -304
  277. package/src/static-functions.ts +11 -11
  278. package/src/subscribe.ts +38 -38
  279. package/src/svgrid-wrapper.types.ts +501 -501
  280. package/src/svgrid.behavior.test.ts +706 -706
  281. package/src/svgrid.charting.test.ts +527 -527
  282. package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
  283. package/src/svgrid.features.test.ts +157 -157
  284. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  285. package/src/svgrid.wrapper.test.ts +40 -40
  286. package/src/test-setup.ts +62 -62
  287. package/src/themes/index.ts +159 -159
  288. package/src/ui-app.types.ts +3 -0
  289. package/src/virtual.test.ts +6 -4
  290. package/src/virtualization/column-virtualizer.test.ts +27 -27
  291. package/src/virtualization/column-virtualizer.ts +30 -30
  292. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  293. package/src/virtualization/types.ts +30 -30
  294. package/src/virtualization/virtualizer.test.ts +47 -47
  295. package/src/virtualization/virtualizer.ts +296 -296
@@ -1,116 +1,116 @@
1
- <script lang="ts" module>
2
- export type { MenuItem } from './SvMenuList.svelte'
3
- </script>
4
-
5
- <script lang="ts">
6
- /**
7
- * SvContextMenu - wraps a region and opens a menu at the pointer on right-click
8
- * (or long-press). Reuses SvMenuList (submenus, keyboard, icons, shortcuts),
9
- * portals to <body>, and closes via the shared dismissable layer stack.
10
- * Parity: Smart context-menu.
11
- *
12
- * ```svelte
13
- * <SvContextMenu items={items} onSelect={(i) => run(i)}>
14
- * <div class="drop-zone">Right-click me</div>
15
- * </SvContextMenu>
16
- * ```
17
- */
18
- import type { Snippet } from 'svelte'
19
- import { portalToBody, popIn } from './popover'
20
- import { createDismissableLayer } from './a11y/dismissable'
21
- import { createFocusTrap } from './a11y/focus-trap'
22
- import SvMenuList, { type MenuItem } from './SvMenuList.svelte'
23
-
24
- type Props = {
25
- items: ReadonlyArray<MenuItem>
26
- onSelect?: (item: MenuItem) => void
27
- disabled?: boolean
28
- ariaLabel?: string
29
- children?: Snippet
30
- }
31
-
32
- let { items, onSelect, disabled = false, ariaLabel, children }: Props = $props()
33
-
34
- let open = $state(false)
35
- let pos = $state({ x: 0, y: 0 })
36
- let panelEl = $state<HTMLDivElement | null>(null)
37
- let zoneEl = $state<HTMLDivElement | null>(null)
38
-
39
- const MENU_W = 200
40
- function openAt(clientX: number, clientY: number) {
41
- // Clamp so the menu stays on-screen (estimate height from item count).
42
- const estH = Math.min(items.length, 12) * 34 + 8
43
- const x = Math.min(clientX, window.innerWidth - MENU_W - 6)
44
- const y = Math.min(clientY, window.innerHeight - estH - 6)
45
- pos = { x: Math.max(6, x), y: Math.max(6, y) }
46
- open = true
47
- }
48
-
49
- function onContextMenu(e: MouseEvent) {
50
- if (disabled) return
51
- e.preventDefault()
52
- openAt(e.clientX, e.clientY)
53
- }
54
-
55
- // Keyboard equivalent of a right-click: the ContextMenu key, or Shift+F10.
56
- // Anchors at the focused zone's own position instead of the (nonexistent)
57
- // pointer coordinates.
58
- function onZoneKeydown(e: KeyboardEvent) {
59
- if (disabled) return
60
- const isMenuKey = e.key === 'ContextMenu' || (e.key === 'F10' && e.shiftKey)
61
- if (!isMenuKey) return
62
- e.preventDefault()
63
- const rect = zoneEl?.getBoundingClientRect()
64
- openAt(rect ? rect.left : 0, rect ? rect.bottom : 0)
65
- }
66
-
67
- $effect(() => {
68
- if (!open || !panelEl) return
69
- const trap = createFocusTrap(panelEl, {
70
- initialFocus: () => panelEl?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])') ?? null,
71
- })
72
- trap.activate()
73
- const layer = createDismissableLayer({
74
- element: () => panelEl,
75
- onDismiss: () => (open = false),
76
- })
77
- layer.activate()
78
- // Reposition-close on scroll (a moved anchor would leave the menu stranded).
79
- const onScroll = () => (open = false)
80
- window.addEventListener('scroll', onScroll, true)
81
- return () => { layer.release(); trap.release(); window.removeEventListener('scroll', onScroll, true) }
82
- })
83
- </script>
84
-
85
- <!-- svelte-ignore a11y_no_static_element_interactions, a11y_no_noninteractive_tabindex -->
86
- <div class="sv-ctx__zone" bind:this={zoneEl} tabindex={disabled ? -1 : 0} oncontextmenu={onContextMenu} onkeydown={onZoneKeydown}>
87
- {@render children?.()}
88
- </div>
89
-
90
- {#if open}
91
- <div
92
- bind:this={panelEl}
93
- class="sv-ctx"
94
- use:portalToBody
95
- use:popIn={{}}
96
- style:position="fixed"
97
- style:top={`${pos.y}px`}
98
- style:left={`${pos.x}px`}
99
- aria-label={ariaLabel}
100
- >
101
- <SvMenuList {items} onclose={() => (open = false)} onselect={(i) => onSelect?.(i)} />
102
- </div>
103
- {/if}
104
-
105
- <style>
106
- /* display:contents generates no box and per spec cannot receive focus - it
107
- must be a real box for the keyboard-open path (tabindex) to work at all. */
108
- .sv-ctx__zone { display: block; }
109
- .sv-ctx__zone:focus-visible { outline: 2px solid var(--sg-accent, #6366f1); outline-offset: 1px; }
110
- :global(.sv-ctx) {
111
- z-index: 2147483646; min-width: 200px;
112
- background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
113
- border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
114
- box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
115
- }
116
- </style>
1
+ <script lang="ts" module>
2
+ export type { MenuItem } from './SvMenuList.svelte'
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvContextMenu - wraps a region and opens a menu at the pointer on right-click
8
+ * (or long-press). Reuses SvMenuList (submenus, keyboard, icons, shortcuts),
9
+ * portals to <body>, and closes via the shared dismissable layer stack.
10
+ * Parity: Smart context-menu.
11
+ *
12
+ * ```svelte
13
+ * <SvContextMenu items={items} onSelect={(i) => run(i)}>
14
+ * <div class="drop-zone">Right-click me</div>
15
+ * </SvContextMenu>
16
+ * ```
17
+ */
18
+ import type { Snippet } from 'svelte'
19
+ import { portalToBody, popIn } from './popover'
20
+ import { createDismissableLayer } from './a11y/dismissable'
21
+ import { createFocusTrap } from './a11y/focus-trap'
22
+ import SvMenuList, { type MenuItem } from './SvMenuList.svelte'
23
+
24
+ type Props = {
25
+ items: ReadonlyArray<MenuItem>
26
+ onSelect?: (item: MenuItem) => void
27
+ disabled?: boolean
28
+ ariaLabel?: string
29
+ children?: Snippet
30
+ }
31
+
32
+ let { items, onSelect, disabled = false, ariaLabel, children }: Props = $props()
33
+
34
+ let open = $state(false)
35
+ let pos = $state({ x: 0, y: 0 })
36
+ let panelEl = $state<HTMLDivElement | null>(null)
37
+ let zoneEl = $state<HTMLDivElement | null>(null)
38
+
39
+ const MENU_W = 200
40
+ function openAt(clientX: number, clientY: number) {
41
+ // Clamp so the menu stays on-screen (estimate height from item count).
42
+ const estH = Math.min(items.length, 12) * 34 + 8
43
+ const x = Math.min(clientX, window.innerWidth - MENU_W - 6)
44
+ const y = Math.min(clientY, window.innerHeight - estH - 6)
45
+ pos = { x: Math.max(6, x), y: Math.max(6, y) }
46
+ open = true
47
+ }
48
+
49
+ function onContextMenu(e: MouseEvent) {
50
+ if (disabled) return
51
+ e.preventDefault()
52
+ openAt(e.clientX, e.clientY)
53
+ }
54
+
55
+ // Keyboard equivalent of a right-click: the ContextMenu key, or Shift+F10.
56
+ // Anchors at the focused zone's own position instead of the (nonexistent)
57
+ // pointer coordinates.
58
+ function onZoneKeydown(e: KeyboardEvent) {
59
+ if (disabled) return
60
+ const isMenuKey = e.key === 'ContextMenu' || (e.key === 'F10' && e.shiftKey)
61
+ if (!isMenuKey) return
62
+ e.preventDefault()
63
+ const rect = zoneEl?.getBoundingClientRect()
64
+ openAt(rect ? rect.left : 0, rect ? rect.bottom : 0)
65
+ }
66
+
67
+ $effect(() => {
68
+ if (!open || !panelEl) return
69
+ const trap = createFocusTrap(panelEl, {
70
+ initialFocus: () => panelEl?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])') ?? null,
71
+ })
72
+ trap.activate()
73
+ const layer = createDismissableLayer({
74
+ element: () => panelEl,
75
+ onDismiss: () => (open = false),
76
+ })
77
+ layer.activate()
78
+ // Reposition-close on scroll (a moved anchor would leave the menu stranded).
79
+ const onScroll = () => (open = false)
80
+ window.addEventListener('scroll', onScroll, true)
81
+ return () => { layer.release(); trap.release(); window.removeEventListener('scroll', onScroll, true) }
82
+ })
83
+ </script>
84
+
85
+ <!-- svelte-ignore a11y_no_static_element_interactions, a11y_no_noninteractive_tabindex -->
86
+ <div class="sv-ctx__zone" bind:this={zoneEl} tabindex={disabled ? -1 : 0} oncontextmenu={onContextMenu} onkeydown={onZoneKeydown}>
87
+ {@render children?.()}
88
+ </div>
89
+
90
+ {#if open}
91
+ <div
92
+ bind:this={panelEl}
93
+ class="sv-ctx"
94
+ use:portalToBody
95
+ use:popIn={{}}
96
+ style:position="fixed"
97
+ style:top={`${pos.y}px`}
98
+ style:left={`${pos.x}px`}
99
+ aria-label={ariaLabel}
100
+ >
101
+ <SvMenuList {items} onclose={() => (open = false)} onselect={(i) => onSelect?.(i)} />
102
+ </div>
103
+ {/if}
104
+
105
+ <style>
106
+ /* display:contents generates no box and per spec cannot receive focus - it
107
+ must be a real box for the keyboard-open path (tabindex) to work at all. */
108
+ .sv-ctx__zone { display: block; }
109
+ .sv-ctx__zone:focus-visible { outline: 2px solid var(--sg-accent, #6366f1); outline-offset: 1px; }
110
+ :global(.sv-ctx) {
111
+ z-index: 2147483646; min-width: 200px;
112
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
113
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
114
+ box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
115
+ }
116
+ </style>
@@ -54,7 +54,7 @@
54
54
  let triggerEl = $state<HTMLButtonElement | null>(null)
55
55
  let searchEl = $state<HTMLInputElement | null>(null)
56
56
  let panelEl = $state<HTMLDivElement | null>(null)
57
- let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
57
+ let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
58
58
 
59
59
  const ci = createCountryInput({
60
60
  value: () => value,
@@ -118,7 +118,7 @@
118
118
  // --- Portalled popover positioning (DOM-bound; stays in the component) -------
119
119
  let triggerEl = $state<HTMLDivElement | null>(null)
120
120
  let panelEl = $state<HTMLDivElement | null>(null)
121
- let panelRect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
121
+ let panelRect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
122
122
 
123
123
  function updatePos() {
124
124
  if (!triggerEl) return
@@ -54,6 +54,9 @@
54
54
  stepMinutes?: number
55
55
  /** Open the dropdown as soon as the field is focused (grid in-cell editing). */
56
56
  autoOpen?: boolean
57
+ /** Stretch the field to fill its container (100% width) instead of the fixed
58
+ * default width - used when mounted as a grid cell editor. */
59
+ block?: boolean
57
60
  /** Animate the calendar's month/drill navigation (honors reduced-motion). */
58
61
  animate?: boolean | 'slide' | 'fade'
59
62
  /** Override the built-in strings (tabs, dialog, clear/open aria-labels). */
@@ -82,6 +85,7 @@
82
85
  spinButtons = false,
83
86
  stepMinutes = 1,
84
87
  autoOpen = false,
88
+ block = false,
85
89
  animate = false,
86
90
  ariaLabel,
87
91
  invalid = false,
@@ -130,7 +134,7 @@
130
134
  // --- Portalled dropdown positioning (DOM-bound; stays in the component) -----
131
135
  let triggerEl = $state<HTMLDivElement | null>(null)
132
136
  let panelEl = $state<HTMLDivElement | null>(null)
133
- let panelRect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
137
+ let panelRect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
134
138
 
135
139
  function updatePos() {
136
140
  if (!triggerEl) return
@@ -171,8 +175,8 @@
171
175
  }
172
176
  </script>
173
177
 
174
- <SvField id={uid} {label} {hint} {error} {required} {dir}>
175
- <div class="sv-dtp" class:sv-dtp--disabled={disabled} dir={resolvedDir} onfocusout={onRootFocusOut}>
178
+ <SvField id={uid} {label} {hint} {error} {required} {dir} {block}>
179
+ <div class="sv-dtp" class:sv-dtp--disabled={disabled} class:sv-dtp--block={block} dir={resolvedDir} onfocusout={onRootFocusOut}>
176
180
  <div class="sv-dtp__field" class:is-invalid={invalid} bind:this={triggerEl}>
177
181
  {#if spinButtons}
178
182
  <div class="sv-dtp__spin">
@@ -256,6 +260,8 @@
256
260
  --_radius: var(--sg-radius, 8px);
257
261
  display: inline-block; position: relative; width: 240px; color: var(--_fg);
258
262
  }
263
+ /* Fill the container (grid cell editor) instead of the fixed default width. */
264
+ .sv-dtp--block { display: block; width: 100%; }
259
265
  .sv-dtp--disabled { opacity: 0.6; }
260
266
  .sv-dtp__field {
261
267
  display: flex; align-items: center; gap: 2px;
@@ -0,0 +1,244 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvDockLayout - a basic docking layout: nested resizable split groups and
4
+ * tabbed leaves, with drag-to-dock. Drag a pane's tab onto another pane and
5
+ * drop it on an edge (split) or the centre (add as a tab); drag the splitters
6
+ * to resize; close panes. The whole layout is a plain `DockNode` tree you pass
7
+ * in and bind - every gesture is a pure transform from `dock-model`, so state
8
+ * is serializable and testable. Inspired by Smart's `smart-layout`.
9
+ *
10
+ * Usage: build a tree with `dockGroup` / `dockTabs` / `dockPane`, bind it, and
11
+ * provide a `pane` snippet that renders each pane's content by id:
12
+ *
13
+ * let layout = $state(dockGroup('row', [
14
+ * dockTabs([dockPane('files', 'Files')]),
15
+ * dockTabs([dockPane('editor', 'Editor')]),
16
+ * ]))
17
+ *
18
+ * <SvDockLayout bind:layout>
19
+ * {#snippet pane(p)} ...render by p.id... {/snippet}
20
+ * </SvDockLayout>
21
+ */
22
+ import { setContext, type Snippet } from 'svelte'
23
+ import { DOCK_CONTEXT, type DockContext } from './dock-context'
24
+ import DockNodeView from './DockNodeView.svelte'
25
+ import {
26
+ setActive,
27
+ setSizes,
28
+ removePane,
29
+ movePane,
30
+ type DockNode,
31
+ type DockPane,
32
+ type DockZone,
33
+ } from './dock-model'
34
+
35
+ type Props = {
36
+ /** The layout tree. Bindable - every dock/resize/close writes a new tree. */
37
+ layout: DockNode
38
+ /** Renders each pane's content, keyed off the `DockPane`. */
39
+ pane: Snippet<[DockPane]>
40
+ /** Notified after any structural change (dock / resize / close). */
41
+ onLayoutChange?: (layout: DockNode) => void
42
+ /** Minimum pane size in px along the split axis. Default 80. */
43
+ minSize?: number
44
+ /** Surface id stamped on the root (`data-dock-surface`) - used by
45
+ * SvDockManager to hit-test which layout a drag is over. Default 'main'. */
46
+ surface?: string
47
+ /**
48
+ * Manager mode: when provided, SvDockLayout hands OFF tab-drag to the parent
49
+ * (SvDockManager) instead of running its own drag-to-dock, so a drag can flow
50
+ * across the main area, floating windows and edge strips. Return value is
51
+ * ignored; the manager owns the gesture from here.
52
+ */
53
+ onBeginDrag?: (event: PointerEvent, paneId: string, tabsId: string) => void
54
+ /** Manager mode: the externally-computed dock guide/highlight to paint. */
55
+ externalDrop?: { tabsId: string; zone: DockZone | null; centerOnly?: boolean } | null
56
+ /** Manager mode: the externally-computed tab-reorder insertion indicator. */
57
+ externalReorder?: { tabsId: string; index: number } | null
58
+ /** Stack-header controls for a leaf (maximize / float / pop-out), rendered
59
+ * by the manager once per leaf at the right of its tab strip. */
60
+ leafActions?: Snippet<[import('./dock-model').DockTabs]>
61
+ /** Manager mode: route tab activation to the parent (controlled). */
62
+ onActivate?: (tabsId: string, index: number) => void
63
+ /** Manager mode: route pane close to the parent (controlled). */
64
+ onClose?: (paneId: string) => void
65
+ /** Manager mode: route splitter resize to the parent (controlled). */
66
+ onResize?: (groupId: string, sizes: number[]) => void
67
+ /** Which side of each leaf the tab strip sits on. Default `'top'`. */
68
+ headerPosition?: 'top' | 'bottom' | 'left' | 'right'
69
+ /** Manager mode: id of the focused leaf, for the active-panel highlight. */
70
+ focusedLeaf?: string | null
71
+ /** Keep inactive tabs mounted (hidden) so their content state persists. */
72
+ keepAlive?: boolean
73
+ /** In a single-pane leaf, hide the redundant tab button (auto-hide fly-out). */
74
+ hideSingleTab?: boolean
75
+ /** Locked / split mode: tabs cannot be dragged (no reorder / dock / float) and
76
+ * panel action buttons are hidden - only splitter resize + tab switching remain.
77
+ * Turns the layout into a fixed set of resizable panes. */
78
+ locked?: boolean
79
+ }
80
+
81
+ let {
82
+ layout = $bindable(),
83
+ pane,
84
+ onLayoutChange,
85
+ minSize = 80,
86
+ surface = 'main',
87
+ onBeginDrag,
88
+ externalDrop = null,
89
+ externalReorder = null,
90
+ leafActions,
91
+ onActivate,
92
+ onClose,
93
+ onResize,
94
+ headerPosition = 'top',
95
+ focusedLeaf = null,
96
+ keepAlive = false,
97
+ hideSingleTab = false,
98
+ locked = false,
99
+ }: Props = $props()
100
+
101
+ const managed = $derived(!!onBeginDrag)
102
+
103
+ // Ids for nodes created by docking. The per-instance base (from a module
104
+ // counter) keeps them unique across instances and clear of caller ids.
105
+ let nextId = instanceBase()
106
+ const genId = () => `dk-${nextId++}`
107
+
108
+ function commit(next: DockNode) {
109
+ layout = next
110
+ onLayoutChange?.(next)
111
+ }
112
+
113
+ const activate = (tabsId: string, index: number) =>
114
+ onActivate ? onActivate(tabsId, index) : commit(setActive(layout, tabsId, index))
115
+ const resize = (groupId: string, sizes: number[]) =>
116
+ onResize ? onResize(groupId, sizes) : commit(setSizes(layout, groupId, sizes))
117
+ const close = (paneId: string) => {
118
+ if (onClose) { onClose(paneId); return }
119
+ const { root } = removePane(layout, paneId)
120
+ if (root) commit(root)
121
+ }
122
+
123
+ // ---- drag-to-dock ------------------------------------------------------
124
+ type Drag = { paneId: string; sourceTabsId: string; title: string; x: number; y: number }
125
+ let drag = $state<Drag | null>(null)
126
+ let dropTarget = $state<{ tabsId: string; zone: DockZone } | null>(null)
127
+ let candidate: { paneId: string; sourceTabsId: string; title: string; sx: number; sy: number } | null = null
128
+
129
+ function paneTitle(paneId: string): string {
130
+ const walk = (n: DockNode): string | null => {
131
+ if (n.type === 'tabs') return n.panes.find((p) => p.id === paneId)?.title ?? null
132
+ for (const c of n.children) { const t = walk(c); if (t) return t }
133
+ return null
134
+ }
135
+ return walk(layout) ?? ''
136
+ }
137
+
138
+ function beginDrag(e: PointerEvent, paneId: string, tabsId: string) {
139
+ if (e.button !== 0) return
140
+ // Locked / split mode: tabs are fixed - a pointerdown still activates the tab
141
+ // (handled separately), but starts no drag.
142
+ if (locked) return
143
+ // Manager mode: delegate the whole gesture to the parent.
144
+ if (onBeginDrag) { onBeginDrag(e, paneId, tabsId); return }
145
+ candidate = { paneId, sourceTabsId: tabsId, title: paneTitle(paneId), sx: e.clientX, sy: e.clientY }
146
+ window.addEventListener('pointermove', onPointerMove)
147
+ window.addEventListener('pointerup', onPointerUp)
148
+ }
149
+
150
+ /** Which leaf is under the pointer, and which of its 5 zones. */
151
+ function hitTest(x: number, y: number): { tabsId: string; zone: DockZone } | null {
152
+ const el = document.elementFromPoint(x, y)?.closest('[data-dock-tabs]') as HTMLElement | null
153
+ if (!el) return null
154
+ const tabsId = el.dataset.dockTabs!
155
+ const r = el.getBoundingClientRect()
156
+ if (r.width <= 0 || r.height <= 0) return null
157
+ const rx = (x - r.left) / r.width
158
+ const ry = (y - r.top) / r.height
159
+ const edge = 0.25
160
+ const m = Math.min(rx, 1 - rx, ry, 1 - ry)
161
+ let zone: DockZone = 'center'
162
+ if (m <= edge) {
163
+ const nearest = Math.min(rx, 1 - rx, ry, 1 - ry)
164
+ zone = nearest === rx ? 'left' : nearest === 1 - rx ? 'right' : nearest === ry ? 'top' : 'bottom'
165
+ }
166
+ return { tabsId, zone }
167
+ }
168
+
169
+ function onPointerMove(e: PointerEvent) {
170
+ if (!candidate) return
171
+ if (!drag) {
172
+ // Small threshold so a click-to-activate does not start a drag.
173
+ if (Math.hypot(e.clientX - candidate.sx, e.clientY - candidate.sy) < 5) return
174
+ drag = { paneId: candidate.paneId, sourceTabsId: candidate.sourceTabsId, title: candidate.title, x: e.clientX, y: e.clientY }
175
+ document.body.style.userSelect = 'none'
176
+ document.body.style.cursor = 'grabbing'
177
+ }
178
+ drag = { ...drag, x: e.clientX, y: e.clientY }
179
+ dropTarget = hitTest(e.clientX, e.clientY)
180
+ }
181
+
182
+ function onPointerUp() {
183
+ window.removeEventListener('pointermove', onPointerMove)
184
+ window.removeEventListener('pointerup', onPointerUp)
185
+ if (drag && dropTarget) {
186
+ commit(movePane(layout, drag.paneId, dropTarget.tabsId, dropTarget.zone, genId))
187
+ }
188
+ drag = null
189
+ dropTarget = null
190
+ candidate = null
191
+ document.body.style.userSelect = ''
192
+ document.body.style.cursor = ''
193
+ }
194
+
195
+ setContext<DockContext>(DOCK_CONTEXT, {
196
+ get pane() { return pane },
197
+ get leafActions() { return locked ? undefined : leafActions },
198
+ activate,
199
+ close,
200
+ beginDrag,
201
+ resize,
202
+ minSize: () => minSize,
203
+ // In manager mode the highlight is driven from outside.
204
+ dropTarget: () => (managed ? externalDrop : dropTarget),
205
+ reorderTarget: () => (managed ? externalReorder : null),
206
+ headerPosition: () => headerPosition,
207
+ focusedLeaf: () => focusedLeaf,
208
+ keepAlive: () => keepAlive,
209
+ hideSingleTab: () => hideSingleTab,
210
+ })
211
+ </script>
212
+
213
+ <div class="sv-dock" class:is-dragging={!!drag} data-dock-surface={surface}>
214
+ <DockNodeView node={layout} />
215
+ </div>
216
+
217
+ {#if drag}
218
+ <div class="sv-dock__ghost" style:left={`${drag.x + 12}px`} style:top={`${drag.y + 12}px`}>
219
+ {drag.title}
220
+ </div>
221
+ {/if}
222
+
223
+ <script lang="ts" module>
224
+ // Per-instance id base so concurrent layouts never mint the same node id.
225
+ let SEQ = 0
226
+ function instanceBase(): number {
227
+ SEQ += 1
228
+ return SEQ * 1_000_000
229
+ }
230
+ </script>
231
+
232
+ <style>
233
+ .sv-dock { width: 100%; height: 100%; min-width: 0; min-height: 0; display: flex; }
234
+ .sv-dock > :global(*) { flex: 1; min-width: 0; min-height: 0; }
235
+ .sv-dock.is-dragging { cursor: grabbing; }
236
+ .sv-dock.is-dragging :global(.sv-dock__content) { pointer-events: none; }
237
+
238
+ .sv-dock__ghost {
239
+ position: fixed; z-index: 2147483646; pointer-events: none;
240
+ padding: 5px 10px; font-size: 12.5px; font-weight: 600;
241
+ color: var(--sg-on-accent, #fff); background: var(--sg-accent, #2563eb);
242
+ border-radius: 6px; box-shadow: 0 8px 24px -6px rgba(15,23,42,0.5); opacity: 0.95;
243
+ }
244
+ </style>