@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
@@ -14,6 +14,6 @@ type Props = SvEditorProps & {
14
14
  /** Override the built-in popover strings. */
15
15
  messages?: Partial<ColorMessages>;
16
16
  };
17
- declare const SvColorInput: import("svelte").Component<Props, {}, "">;
17
+ declare const SvColorInput: import("svelte").Component<Props, {}, "value">;
18
18
  type SvColorInput = ReturnType<typeof SvColorInput>;
19
19
  export default SvColorInput;
@@ -65,7 +65,7 @@
65
65
  let inputEl = $state<HTMLInputElement | null>(null)
66
66
  let fieldEl = $state<HTMLDivElement | null>(null)
67
67
  let panelEl = $state<HTMLDivElement | null>(null)
68
- let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
68
+ let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
69
69
 
70
70
  // Remote data source: fetch options as the query changes (debounced).
71
71
  let remoteOptions = $state<ListOption[]>([])
@@ -111,7 +111,11 @@
111
111
 
112
112
  function updatePos() {
113
113
  if (!fieldEl) return
114
- rect = anchoredRect(fieldEl.getBoundingClientRect(), { estimatedHeight: Math.min(combo.filtered.length, 8) * 34 + 8 })
114
+ // estimatedHeight only picks the flip direction (up vs down); the panel's
115
+ // real max-height comes from availHeight below, so it takes its natural
116
+ // content height (options + group labels) and scrolls only when it truly
117
+ // overflows the room - never from an estimate that under-counts a row.
118
+ rect = anchoredRect(fieldEl.getBoundingClientRect(), { estimatedHeight: Math.min(combo.filtered.length, 8) * 34 + 12 })
115
119
  }
116
120
 
117
121
  // Position + reposition + outside-click close are render concerns (need the DOM).
@@ -150,7 +154,7 @@
150
154
  </SvField>
151
155
 
152
156
  {#if combo.open}
153
- <div bind:this={panelEl} class="sv-ddl__panel" use:portalToBody use:popIn={{ up: rect.openUpward }} style:position="fixed" style:top={`${rect.top}px`} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} {...combo.listboxProps()}>
157
+ <div bind:this={panelEl} class="sv-ddl__panel" use:portalToBody use:popIn={{ up: rect.openUpward }} style:position="fixed" style:top={rect.openUpward ? undefined : `${rect.top}px`} style:bottom={rect.openUpward ? `${rect.bottom}px` : undefined} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} style:max-height={`${Math.min(rect.availHeight, 288)}px`} {...combo.listboxProps()}>
154
158
  {#if loading}
155
159
  <div class="sv-ddl__empty sv-ddl__loading"><svg class="sv-ddl__spin" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M21 12a9 9 0 1 1-6.2-8.6" /></svg>{M.loading}</div>
156
160
  {:else if loadOptions && !searched && combo.query.trim().length < minLength}
@@ -16,10 +16,8 @@
16
16
  */
17
17
  import type { CommandItem } from './ui-app.types'
18
18
  import { portalToBody, popIn } from './popover'
19
- import { createFocusTrap } from './a11y/focus-trap'
20
- import { lockScroll } from './a11y/scroll-lock'
21
- import { createDismissableLayer } from './a11y/dismissable'
22
- import { fuzzyScore } from './fuzzy'
19
+ import { createOverlay } from './createOverlay.svelte'
20
+ import { createCommand } from './createCommand.svelte'
23
21
 
24
22
  type Props = {
25
23
  open?: boolean
@@ -42,72 +40,29 @@
42
40
  hotkey = 'mod+k',
43
41
  }: Props = $props()
44
42
 
45
- let query = $state('')
46
- let active = $state(0)
47
43
  let panelEl = $state<HTMLDivElement | null>(null)
48
44
  let inputEl = $state<HTMLInputElement | null>(null)
49
45
 
50
- const results = $derived.by(() => {
51
- const q = query.trim()
52
- const list = commands.filter((c) => !c.disabled)
53
- if (!q) return list
54
- return list
55
- .map((c) => ({ c, s: fuzzyScore(`${c.label} ${c.keywords ?? ''} ${c.group ?? ''}`, q) }))
56
- .filter((x) => x.s !== null)
57
- .sort((a, b) => (b.s as number) - (a.s as number))
58
- .map((x) => x.c)
59
- })
60
-
61
46
  function close() { open = false; onClose?.() }
62
- function run(cmd: CommandItem | undefined) {
63
- if (!cmd || cmd.disabled) return
64
- close()
65
- onRun?.(cmd)
66
- cmd.onRun?.()
67
- }
68
- function move(delta: number) {
69
- const n = results.length
70
- if (!n) return
71
- active = (active + delta + n) % n
72
- queueMicrotask(() =>
73
- panelEl?.querySelector(`[data-idx="${active}"]`)?.scrollIntoView({ block: 'nearest' }),
74
- )
75
- }
76
- function onKeydown(e: KeyboardEvent) {
77
- if (e.key === 'ArrowDown') { e.preventDefault(); move(1) }
78
- else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1) }
79
- else if (e.key === 'Enter') { e.preventDefault(); run(results[active]) }
80
- // Escape + outside-click are owned by the dismissable layer.
81
- }
82
-
83
- // Reset active whenever the query changes.
84
- $effect(() => { void query; active = 0 })
85
47
 
86
- // Focus + scroll lock + Escape/outside dismissal while open.
87
- $effect(() => {
88
- if (!open || !panelEl) return
89
- query = ''
90
- active = 0
91
- const trap = createFocusTrap(panelEl, { initialFocus: () => inputEl })
92
- trap.activate()
93
- const unlock = lockScroll()
94
- const layer = createDismissableLayer({ element: () => panelEl, onDismiss: () => close() })
95
- layer.activate()
96
- return () => { layer.release(); unlock(); trap.release() }
48
+ // Fuzzy filter + roving + run + hotkey live in the command core; the overlay
49
+ // lifecycle (focus-trap into the input, scroll-lock, Escape/outside dismissal)
50
+ // is the shared dialog core.
51
+ const cmd = createCommand({
52
+ commands: () => commands,
53
+ open: () => open,
54
+ onRun,
55
+ onClose: close,
56
+ onToggleOpen: () => (open = !open),
57
+ hotkey: () => hotkey,
58
+ scrollActiveIntoView: (i) =>
59
+ queueMicrotask(() => panelEl?.querySelector(`[data-idx="${i}"]`)?.scrollIntoView({ block: 'nearest' })),
97
60
  })
98
-
99
- // Global toggle hotkey.
100
- $effect(() => {
101
- if (!hotkey) return
102
- const key = hotkey === 'mod+p' ? 'p' : 'k'
103
- const onKey = (e: KeyboardEvent) => {
104
- if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === key) {
105
- e.preventDefault()
106
- open = !open
107
- }
108
- }
109
- window.addEventListener('keydown', onKey)
110
- return () => window.removeEventListener('keydown', onKey)
61
+ const overlay = createOverlay({
62
+ open: () => open,
63
+ getDialog: () => panelEl,
64
+ onClose: close,
65
+ initialFocus: () => inputEl,
111
66
  })
112
67
  </script>
113
68
 
@@ -117,11 +72,8 @@
117
72
  bind:this={panelEl}
118
73
  class="sv-cmd"
119
74
  use:popIn={{}}
120
- role="dialog"
121
- aria-modal="true"
122
- aria-label="Command palette"
123
- tabindex="-1"
124
- onkeydown={onKeydown}
75
+ {...overlay.dialogProps({ label: 'Command palette' })}
76
+ onkeydown={cmd.onKeydown}
125
77
  >
126
78
  <div class="sv-cmd__search">
127
79
  <span class="sv-cmd__searchicon" aria-hidden="true">⌕</span>
@@ -129,29 +81,20 @@
129
81
  bind:this={inputEl}
130
82
  class="sv-cmd__input"
131
83
  type="text"
132
- role="combobox"
133
- aria-expanded="true"
134
- aria-controls="sv-cmd-list"
135
- aria-activedescendant={results[active] ? `sv-cmd-opt-${active}` : undefined}
84
+ aria-label="Command palette search"
136
85
  {placeholder}
137
- value={query}
138
- oninput={(e) => (query = e.currentTarget.value)}
86
+ {...cmd.inputProps()}
139
87
  />
140
88
  </div>
141
- <ul class="sv-cmd__list" id="sv-cmd-list" role="listbox" aria-label="Commands">
142
- {#each results as c, i (c.id)}
143
- {#if !query.trim() && c.group && c.group !== results[i - 1]?.group}
89
+ <ul class="sv-cmd__list" aria-label="Commands" {...cmd.listProps()}>
90
+ {#each cmd.results as c, i (c.id)}
91
+ {#if !cmd.query.trim() && c.group && c.group !== cmd.results[i - 1]?.group}
144
92
  <li class="sv-cmd__group" role="presentation">{c.group}</li>
145
93
  {/if}
146
94
  <li
147
- id={`sv-cmd-opt-${i}`}
148
- data-idx={i}
149
95
  class="sv-cmd__opt"
150
- class:is-active={i === active}
151
- role="option"
152
- aria-selected={i === active}
153
- onpointermove={() => (active = i)}
154
- onclick={() => run(c)}
96
+ class:is-active={cmd.isActive(i)}
97
+ {...cmd.optionProps(i)}
155
98
  >
156
99
  {#if c.icon}<span class="sv-cmd__icon" aria-hidden="true">{c.icon}</span>{/if}
157
100
  <span class="sv-cmd__label">{c.label}</span>
@@ -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;
@@ -36,6 +36,9 @@ type Props = SvEditorProps & {
36
36
  stepMinutes?: number;
37
37
  /** Open the dropdown as soon as the field is focused (grid in-cell editing). */
38
38
  autoOpen?: boolean;
39
+ /** Stretch the field to fill its container (100% width) instead of the fixed
40
+ * default width - used when mounted as a grid cell editor. */
41
+ block?: boolean;
39
42
  /** Animate the calendar's month/drill navigation (honors reduced-motion). */
40
43
  animate?: boolean | 'slide' | 'fade';
41
44
  /** Override the built-in strings (tabs, dialog, clear/open aria-labels). */
@@ -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>
@@ -0,0 +1,77 @@
1
+ /**
2
+ * SvDockLayout - a basic docking layout: nested resizable split groups and
3
+ * tabbed leaves, with drag-to-dock. Drag a pane's tab onto another pane and
4
+ * drop it on an edge (split) or the centre (add as a tab); drag the splitters
5
+ * to resize; close panes. The whole layout is a plain `DockNode` tree you pass
6
+ * in and bind - every gesture is a pure transform from `dock-model`, so state
7
+ * is serializable and testable. Inspired by Smart's `smart-layout`.
8
+ *
9
+ * Usage: build a tree with `dockGroup` / `dockTabs` / `dockPane`, bind it, and
10
+ * provide a `pane` snippet that renders each pane's content by id:
11
+ *
12
+ * let layout = $state(dockGroup('row', [
13
+ * dockTabs([dockPane('files', 'Files')]),
14
+ * dockTabs([dockPane('editor', 'Editor')]),
15
+ * ]))
16
+ *
17
+ * <SvDockLayout bind:layout>
18
+ * {#snippet pane(p)} ...render by p.id... {/snippet}
19
+ * </SvDockLayout>
20
+ */
21
+ import { type Snippet } from 'svelte';
22
+ import { type DockNode, type DockPane, type DockZone } from './dock-model';
23
+ type Props = {
24
+ /** The layout tree. Bindable - every dock/resize/close writes a new tree. */
25
+ layout: DockNode;
26
+ /** Renders each pane's content, keyed off the `DockPane`. */
27
+ pane: Snippet<[DockPane]>;
28
+ /** Notified after any structural change (dock / resize / close). */
29
+ onLayoutChange?: (layout: DockNode) => void;
30
+ /** Minimum pane size in px along the split axis. Default 80. */
31
+ minSize?: number;
32
+ /** Surface id stamped on the root (`data-dock-surface`) - used by
33
+ * SvDockManager to hit-test which layout a drag is over. Default 'main'. */
34
+ surface?: string;
35
+ /**
36
+ * Manager mode: when provided, SvDockLayout hands OFF tab-drag to the parent
37
+ * (SvDockManager) instead of running its own drag-to-dock, so a drag can flow
38
+ * across the main area, floating windows and edge strips. Return value is
39
+ * ignored; the manager owns the gesture from here.
40
+ */
41
+ onBeginDrag?: (event: PointerEvent, paneId: string, tabsId: string) => void;
42
+ /** Manager mode: the externally-computed dock guide/highlight to paint. */
43
+ externalDrop?: {
44
+ tabsId: string;
45
+ zone: DockZone | null;
46
+ centerOnly?: boolean;
47
+ } | null;
48
+ /** Manager mode: the externally-computed tab-reorder insertion indicator. */
49
+ externalReorder?: {
50
+ tabsId: string;
51
+ index: number;
52
+ } | null;
53
+ /** Stack-header controls for a leaf (maximize / float / pop-out), rendered
54
+ * by the manager once per leaf at the right of its tab strip. */
55
+ leafActions?: Snippet<[import('./dock-model').DockTabs]>;
56
+ /** Manager mode: route tab activation to the parent (controlled). */
57
+ onActivate?: (tabsId: string, index: number) => void;
58
+ /** Manager mode: route pane close to the parent (controlled). */
59
+ onClose?: (paneId: string) => void;
60
+ /** Manager mode: route splitter resize to the parent (controlled). */
61
+ onResize?: (groupId: string, sizes: number[]) => void;
62
+ /** Which side of each leaf the tab strip sits on. Default `'top'`. */
63
+ headerPosition?: 'top' | 'bottom' | 'left' | 'right';
64
+ /** Manager mode: id of the focused leaf, for the active-panel highlight. */
65
+ focusedLeaf?: string | null;
66
+ /** Keep inactive tabs mounted (hidden) so their content state persists. */
67
+ keepAlive?: boolean;
68
+ /** In a single-pane leaf, hide the redundant tab button (auto-hide fly-out). */
69
+ hideSingleTab?: boolean;
70
+ /** Locked / split mode: tabs cannot be dragged (no reorder / dock / float) and
71
+ * panel action buttons are hidden - only splitter resize + tab switching remain.
72
+ * Turns the layout into a fixed set of resizable panes. */
73
+ locked?: boolean;
74
+ };
75
+ declare const SvDockLayout: import("svelte").Component<Props, {}, "layout">;
76
+ type SvDockLayout = ReturnType<typeof SvDockLayout>;
77
+ export default SvDockLayout;