@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
@@ -1,17 +1,8 @@
1
1
  <script lang="ts" module>
2
- import type { Snippet } from 'svelte'
3
- export type MenuItem = {
4
- /** Item text. Omit + set `separator` for a divider. */
5
- label?: string
6
- separator?: boolean
7
- icon?: Snippet
8
- /** Right-aligned hint, e.g. a keyboard shortcut. */
9
- shortcut?: string
10
- disabled?: boolean
11
- /** Nested submenu items (renders a flyout). */
12
- children?: MenuItem[]
13
- onSelect?: () => void
14
- }
2
+ // MenuItem lives in a plain `.ts` module (breaks a Vite dev import cycle); it is
3
+ // re-exported here so existing `import { type MenuItem } from './SvMenuList.svelte'`
4
+ // sites keep working.
5
+ export type { MenuItem } from './menu-item'
15
6
  </script>
16
7
 
17
8
  <script lang="ts">
@@ -22,6 +13,8 @@
22
13
  */
23
14
  import Self from './SvMenuList.svelte'
24
15
  import { type EditorDir } from './editor-contract'
16
+ import type { MenuItem } from './menu-item'
17
+ import { createMenu } from './createMenu.svelte'
25
18
 
26
19
  let {
27
20
  items,
@@ -46,56 +39,24 @@
46
39
  dir?: EditorDir
47
40
  } = $props()
48
41
 
49
- // Indexes of real (non-separator) items, for roving focus.
50
- const focusable = $derived(items.map((it, i) => (it.separator || it.disabled ? -1 : i)).filter((i) => i >= 0))
51
- let active = $state(-1)
52
- let openSub = $state(-1)
53
42
  let listEl = $state<HTMLDivElement | null>(null)
54
43
 
55
- function focusItem(i: number) {
56
- active = i
57
- queueMicrotask(() => listEl?.querySelector<HTMLElement>(`[data-mi="${i}"]`)?.focus())
58
- }
59
- function move(delta: number) {
60
- if (!focusable.length) return
61
- const pos = focusable.indexOf(active)
62
- const next = focusable[(pos + delta + focusable.length) % focusable.length]
63
- if (next != null) { openSub = -1; focusItem(next) }
64
- }
65
- function choose(item: MenuItem) {
66
- if (item.disabled || item.separator) return
67
- if (item.children?.length) { openSub = items.indexOf(item); return }
68
- item.onSelect?.()
69
- onselect(item)
70
- onclose()
71
- }
72
- function onKeydown(e: KeyboardEvent, i: number) {
73
- const item = items[i]
74
- const rtl = dir === 'rtl'
75
- // Open this item's submenu (opposite key under rtl).
76
- const openThis = () => { if (item?.children?.length) { e.preventDefault(); openSub = i } }
77
- // Collapse just THIS list back into its parent item (opposite key under
78
- // rtl) - `oncollapse` is local to this nesting level, unlike `onclose`.
79
- const collapseThis = () => { if (submenu) { e.preventDefault(); oncollapse?.() } }
80
- switch (e.key) {
81
- case 'ArrowDown': e.preventDefault(); move(1); break
82
- case 'ArrowUp': e.preventDefault(); move(-1); break
83
- case 'Home': e.preventDefault(); focusItem(focusable[0] ?? 0); break
84
- case 'End': e.preventDefault(); focusItem(focusable.at(-1) ?? 0); break
85
- case 'ArrowRight': (rtl ? collapseThis : openThis)(); break
86
- case 'ArrowLeft': (rtl ? openThis : collapseThis)(); break
87
- case 'Enter':
88
- case ' ': e.preventDefault(); if (item) choose(item); break
89
- case 'Escape': e.preventDefault(); onclose(); break
90
- // Tab leaves the menu entirely (WAI-ARIA menu button pattern): close the
91
- // whole tree rather than trapping focus inside a portalled panel.
92
- case 'Tab': onclose(); break
93
- }
94
- }
44
+ // Roving focus, submenu open/collapse and the full keyboard contract live in
45
+ // the shared headless core; this component only renders + drives real DOM
46
+ // focus (the core stays DOM-free). See createMenu.svelte.ts.
47
+ const menu = createMenu({
48
+ items: () => items,
49
+ onSelect: (item) => onselect(item),
50
+ onClose: () => onclose(),
51
+ submenu: () => submenu,
52
+ onCollapse: () => oncollapse?.(),
53
+ dir: () => dir,
54
+ focusItem: (i) => queueMicrotask(() => listEl?.querySelector<HTMLElement>(`[data-mi="${i}"]`)?.focus()),
55
+ })
95
56
  </script>
96
57
 
97
58
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
98
- <div bind:this={listEl} class="sv-menu__list" class:is-submenu={submenu} role="menu">
59
+ <div bind:this={listEl} class="sv-menu__list" class:is-submenu={submenu} {...menu.listProps()}>
99
60
  {#each items as item, i (i)}
100
61
  {#if item.separator}
101
62
  <div class="sv-menu__sep" role="separator"></div>
@@ -104,30 +65,21 @@
104
65
  <button
105
66
  type="button"
106
67
  class="sv-menu__item"
107
- class:is-active={active === i}
108
- data-mi={i}
109
- role="menuitem"
110
- aria-haspopup={item.children?.length ? 'menu' : undefined}
111
- aria-expanded={item.children?.length ? openSub === i : undefined}
112
- aria-disabled={item.disabled || undefined}
113
- disabled={item.disabled}
114
- tabindex={active === i || (active === -1 && i === focusable[0]) ? 0 : -1}
115
- onpointerenter={() => { if (!item.disabled) { active = i; openSub = item.children?.length ? i : -1 } }}
116
- onkeydown={(e) => onKeydown(e, i)}
117
- onclick={() => choose(item)}
68
+ class:is-active={menu.isActive(i)}
69
+ {...menu.itemProps(i)}
118
70
  >
119
71
  <span class="sv-menu__icon">{#if item.icon}{@render item.icon()}{/if}</span>
120
72
  <span class="sv-menu__label">{item.label}</span>
121
73
  {#if item.shortcut}<span class="sv-menu__shortcut">{item.shortcut}</span>{/if}
122
74
  {#if item.children?.length}<svg class="sv-menu__chev" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>{/if}
123
75
  </button>
124
- {#if item.children?.length && openSub === i}
76
+ {#if item.children?.length && menu.isSubOpen(i)}
125
77
  <div class="sv-menu__flyout">
126
78
  <Self
127
79
  items={item.children}
128
80
  {onclose}
129
81
  {onselect}
130
- oncollapse={() => { openSub = -1; focusItem(i) }}
82
+ oncollapse={() => menu.focus(i)}
131
83
  {dir}
132
84
  submenu
133
85
  />
@@ -1,17 +1,6 @@
1
- import type { Snippet } from 'svelte';
2
- export type MenuItem = {
3
- /** Item text. Omit + set `separator` for a divider. */
4
- label?: string;
5
- separator?: boolean;
6
- icon?: Snippet;
7
- /** Right-aligned hint, e.g. a keyboard shortcut. */
8
- shortcut?: string;
9
- disabled?: boolean;
10
- /** Nested submenu items (renders a flyout). */
11
- children?: MenuItem[];
12
- onSelect?: () => void;
13
- };
1
+ export type { MenuItem } from './menu-item';
14
2
  import { type EditorDir } from './editor-contract';
3
+ import type { MenuItem } from './menu-item';
15
4
  type $$ComponentProps = {
16
5
  items: ReadonlyArray<MenuItem>;
17
6
  /** Close the WHOLE menu (a leaf was chosen, Escape, or outside-click). */
@@ -15,9 +15,7 @@
15
15
  import type { Snippet } from 'svelte'
16
16
  import { portalToBody } from './popover'
17
17
  import { nextEditorId } from './editor-contract'
18
- import { createFocusTrap } from './a11y/focus-trap'
19
- import { lockScroll } from './a11y/scroll-lock'
20
- import { createDismissableLayer } from './a11y/dismissable'
18
+ import { createOverlay } from './createOverlay.svelte'
21
19
 
22
20
  type Props = {
23
21
  open?: boolean
@@ -55,28 +53,16 @@
55
53
 
56
54
  function close() { open = false; onClose?.() }
57
55
 
58
- // Reset transient drag/resize each time it opens, then hand focus, scroll-lock
59
- // and Escape/backdrop dismissal to the shared a11y primitives (one tested
60
- // implementation, and nested overlays close top-first via the layer stack).
61
- $effect(() => {
62
- if (!open || !dialogEl) return
63
- drag = { x: 0, y: 0 }
64
- box = null
65
- const trap = createFocusTrap(dialogEl)
66
- trap.activate()
67
- const unlockScroll = lockScroll()
68
- const layer = createDismissableLayer({
69
- element: () => dialogEl,
70
- onDismiss: (reason) => {
71
- if (reason === 'escape' ? closeOnEsc : closeOnBackdrop) close()
72
- },
73
- })
74
- layer.activate()
75
- return () => {
76
- layer.release()
77
- unlockScroll()
78
- trap.release()
79
- }
56
+ // Focus trap + scroll lock + Escape/backdrop dismissal come from the shared
57
+ // headless overlay core; this component resets its transient drag/resize on
58
+ // open and renders the panel. See createOverlay.svelte.ts.
59
+ const overlay = createOverlay({
60
+ open: () => open,
61
+ getDialog: () => dialogEl,
62
+ onClose: close,
63
+ closeOnEsc: () => closeOnEsc,
64
+ closeOnBackdrop: () => closeOnBackdrop,
65
+ onOpen: () => { drag = { x: 0, y: 0 }; box = null },
80
66
  })
81
67
 
82
68
  // --- Drag (header) ---------------------------------------------------------
@@ -130,10 +116,7 @@
130
116
  bind:this={dialogEl}
131
117
  class="sv-modal sv-modal--{size}"
132
118
  class:is-resized={!!box}
133
- role="dialog"
134
- aria-modal="true"
135
- aria-labelledby={title ? titleId : undefined}
136
- tabindex="-1"
119
+ {...overlay.dialogProps({ labelledBy: title ? titleId : undefined })}
137
120
  style:transform={`translate(calc(-50% + ${drag.x}px), calc(-50% + ${drag.y}px))`}
138
121
  style:width={box ? `${box.w}px` : undefined}
139
122
  style:height={box ? `${box.h}px` : undefined}
@@ -178,6 +178,7 @@
178
178
  style:top={`${ms.rect.top}px`}
179
179
  style:left={`${ms.rect.left}px`}
180
180
  style:min-width={`${ms.rect.width}px`}
181
+ style:max-height={`${ms.rect.maxHeight}px`}
181
182
  onkeydown={ms.onPanelKeydown}
182
183
  >
183
184
  {#if searchable}
@@ -31,7 +31,7 @@
31
31
  }
32
32
 
33
33
  let {
34
- value = null,
34
+ value = $bindable<number | null>(null),
35
35
  onChange,
36
36
  min = -Infinity,
37
37
  max = Infinity,
@@ -62,7 +62,8 @@
62
62
 
63
63
  const num = createNumberInput({
64
64
  value: () => value,
65
- onChange: (v) => onChange?.(v),
65
+ // Write `value` (so `bind:value` works) AND fire `onChange` (callback users).
66
+ onChange: (v) => { value = v; onChange?.(v) },
66
67
  min: () => min,
67
68
  max: () => max,
68
69
  step: () => step,
@@ -86,7 +87,7 @@
86
87
  <div class="sv-num sv-num--{size}" class:is-disabled={disabled} class:is-invalid={invalid}>
87
88
  <input class="sv-num__input" {...num.inputProps()} />
88
89
  {#if clearable && value != null && !disabled && !readonly}
89
- <button type="button" class="sv-num__clear" aria-label="Clear" tabindex="-1" onclick={() => onChange?.(null)}>
90
+ <button type="button" class="sv-num__clear" aria-label="Clear" tabindex="-1" onclick={() => { value = null; onChange?.(null) }}>
90
91
  <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18" /></svg>
91
92
  </button>
92
93
  {/if}
@@ -15,6 +15,6 @@ type Props = SvEditorProps & {
15
15
  /** Show a clear (x) button when there is a value. */
16
16
  clearable?: boolean;
17
17
  };
18
- declare const SvNumberInput: import("svelte").Component<Props, {}, "">;
18
+ declare const SvNumberInput: import("svelte").Component<Props, {}, "value">;
19
19
  type SvNumberInput = ReturnType<typeof SvNumberInput>;
20
20
  export default SvNumberInput;
@@ -9,7 +9,7 @@
9
9
  * <SvPagination page={p} pageCount={20} onChange={(n) => (p = n)} />
10
10
  * ```
11
11
  */
12
- import { paginationRange } from './paginate'
12
+ import { createPagination } from './createPagination'
13
13
 
14
14
  type Props = {
15
15
  /** 1-based current page. */
@@ -38,23 +38,26 @@
38
38
  ariaLabel = 'Pagination',
39
39
  }: Props = $props()
40
40
 
41
- const items = $derived(paginationRange({ page, pageCount, siblingCount, boundaryCount }))
42
- const go = (p: number) => {
43
- if (disabled) return
44
- const next = Math.min(Math.max(p, 1), pageCount)
45
- if (next !== page) onChange?.(next)
46
- }
41
+ // Page-range math + navigation live in the shared headless core.
42
+ const pg = createPagination({
43
+ page: () => page,
44
+ pageCount: () => pageCount,
45
+ onChange,
46
+ siblingCount: () => siblingCount,
47
+ boundaryCount: () => boundaryCount,
48
+ disabled: () => disabled,
49
+ })
47
50
  </script>
48
51
 
49
- <nav class="sv-pg sv-pg--{size}" aria-label={ariaLabel}>
52
+ <nav class="sv-pg sv-pg--{size}" {...pg.navProps(ariaLabel)}>
50
53
  <ul class="sv-pg__list">
51
54
  {#if showFirstLast}
52
- <li><button type="button" class="sv-pg__btn" aria-label="First page" onclick={() => go(1)} disabled={disabled || page <= 1}>&laquo;</button></li>
55
+ <li><button type="button" class="sv-pg__btn" {...pg.firstProps()}>&laquo;</button></li>
53
56
  {/if}
54
57
  {#if showPrevNext}
55
- <li><button type="button" class="sv-pg__btn" aria-label="Previous page" onclick={() => go(page - 1)} disabled={disabled || page <= 1}>&lsaquo;</button></li>
58
+ <li><button type="button" class="sv-pg__btn" {...pg.prevProps()}>&lsaquo;</button></li>
56
59
  {/if}
57
- {#each items as it, i (typeof it === 'number' ? `p${it}` : `${it}-${i}`)}
60
+ {#each pg.items as it, i (typeof it === 'number' ? `p${it}` : `${it}-${i}`)}
58
61
  {#if it === 'ellipsis-left' || it === 'ellipsis-right'}
59
62
  <li><span class="sv-pg__ellipsis" aria-hidden="true">&hellip;</span></li>
60
63
  {:else}
@@ -62,20 +65,17 @@
62
65
  <button
63
66
  type="button"
64
67
  class="sv-pg__btn"
65
- class:is-active={it === page}
66
- aria-current={it === page ? 'page' : undefined}
67
- aria-label={`Page ${it}`}
68
- {disabled}
69
- onclick={() => go(it)}
68
+ class:is-active={pg.isActive(it)}
69
+ {...pg.pageButtonProps(it)}
70
70
  >{it}</button>
71
71
  </li>
72
72
  {/if}
73
73
  {/each}
74
74
  {#if showPrevNext}
75
- <li><button type="button" class="sv-pg__btn" aria-label="Next page" onclick={() => go(page + 1)} disabled={disabled || page >= pageCount}>&rsaquo;</button></li>
75
+ <li><button type="button" class="sv-pg__btn" {...pg.nextProps()}>&rsaquo;</button></li>
76
76
  {/if}
77
77
  {#if showFirstLast}
78
- <li><button type="button" class="sv-pg__btn" aria-label="Last page" onclick={() => go(pageCount)} disabled={disabled || page >= pageCount}>&raquo;</button></li>
78
+ <li><button type="button" class="sv-pg__btn" {...pg.lastProps()}>&raquo;</button></li>
79
79
  {/if}
80
80
  </ul>
81
81
  </nav>
@@ -31,7 +31,7 @@
31
31
  }
32
32
 
33
33
  let {
34
- value = '',
34
+ value = $bindable(''),
35
35
  onChange,
36
36
  placeholder,
37
37
  disabled = false,
@@ -58,7 +58,8 @@
58
58
 
59
59
  const pw = createPasswordInput({
60
60
  value: () => value,
61
- onChange: (v) => onChange?.(v),
61
+ // Write `value` (so `bind:value` works) AND fire `onChange` (callback users).
62
+ onChange: (v) => { value = v; onChange?.(v) },
62
63
  placeholder: () => placeholder,
63
64
  disabled: () => disabled,
64
65
  readonly: () => readonly,
@@ -20,6 +20,6 @@ type Props = SvEditorProps & {
20
20
  /** Override the built-in strings (reveal toggle + strength labels). */
21
21
  messages?: Partial<PasswordMessages>;
22
22
  };
23
- declare const SvPasswordInput: import("svelte").Component<Props, {}, "">;
23
+ declare const SvPasswordInput: import("svelte").Component<Props, {}, "value">;
24
24
  type SvPasswordInput = ReturnType<typeof SvPasswordInput>;
25
25
  export default SvPasswordInput;
@@ -27,7 +27,7 @@
27
27
  }
28
28
 
29
29
  let {
30
- value = '',
30
+ value = $bindable(''),
31
31
  onChange,
32
32
  country = 'US',
33
33
  disabled = false,
@@ -52,7 +52,8 @@
52
52
 
53
53
  const ph = createPhoneInput({
54
54
  value: () => value,
55
- onChange: (v, parts) => onChange?.(v, parts),
55
+ // Write `value` (so `bind:value` works) AND fire `onChange` (callback users).
56
+ onChange: (v, parts) => { value = v; onChange?.(v, parts) },
56
57
  country: () => country,
57
58
  disabled: () => disabled,
58
59
  readonly: () => readonly,
@@ -13,6 +13,6 @@ type Props = SvEditorProps & {
13
13
  /** Override the built-in strings (e.g. the country selector aria-label). */
14
14
  messages?: Partial<PhoneMessages>;
15
15
  };
16
- declare const SvPhoneInput: import("svelte").Component<Props, {}, "">;
16
+ declare const SvPhoneInput: import("svelte").Component<Props, {}, "value">;
17
17
  type SvPhoneInput = ReturnType<typeof SvPhoneInput>;
18
18
  export default SvPhoneInput;
@@ -49,7 +49,7 @@
49
49
 
50
50
  let anchorEl = $state<HTMLSpanElement | null>(null)
51
51
  let panelEl = $state<HTMLDivElement | null>(null)
52
- let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
52
+ let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
53
53
 
54
54
  function setOpen(v: boolean) { if (v !== open) { open = v; onOpenChange?.(v) } }
55
55
  function toggle() { setOpen(!open) }
@@ -14,6 +14,7 @@
14
14
  * ```
15
15
  */
16
16
  import type { StepItem } from './ui-app.types'
17
+ import { createStepper } from './createStepper'
17
18
 
18
19
  type Props = {
19
20
  steps: ReadonlyArray<StepItem>
@@ -27,22 +28,18 @@
27
28
 
28
29
  let { steps, current, onChange, linear = true, orientation = 'horizontal' }: Props = $props()
29
30
 
30
- const statusOf = (i: number): 'complete' | 'active' | 'upcoming' =>
31
- i < current ? 'complete' : i === current ? 'active' : 'upcoming'
32
- const clickable = (i: number) => (linear ? i <= current : true)
33
- const go = (i: number) => { if (clickable(i) && i !== current) onChange?.(i) }
31
+ // Status, clickability and navigation live in the shared headless core.
32
+ const st = createStepper({ count: () => steps.length, current: () => current, onChange, linear: () => linear })
34
33
  </script>
35
34
 
36
35
  <ol class="sv-step sv-step--{orientation}">
37
36
  {#each steps as step, i (i)}
38
- {@const status = statusOf(i)}
39
- <li class="sv-step__item is-{status}" class:is-clickable={clickable(i)}>
37
+ {@const status = st.statusOf(i)}
38
+ <li class="sv-step__item is-{status}" class:is-clickable={st.clickable(i)}>
40
39
  <button
41
40
  type="button"
42
41
  class="sv-step__btn"
43
- aria-current={status === 'active' ? 'step' : undefined}
44
- disabled={!clickable(i)}
45
- onclick={() => go(i)}
42
+ {...st.stepButtonProps(i)}
46
43
  >
47
44
  <span class="sv-step__marker" aria-hidden="true">
48
45
  {#if status === 'complete'}✓{:else}{i + 1}{/if}
@@ -27,7 +27,7 @@
27
27
  }
28
28
 
29
29
  let {
30
- value = [],
30
+ value = $bindable<string[]>([]),
31
31
  onChange,
32
32
  placeholder = 'Add tag…',
33
33
  disabled = false,
@@ -51,7 +51,8 @@
51
51
 
52
52
  const ti = createTagsInput({
53
53
  value: () => value,
54
- onChange: (t) => onChange?.(t),
54
+ // Write `value` (so `bind:value` works) AND fire `onChange` (callback users).
55
+ onChange: (t) => { value = t; onChange?.(t) },
55
56
  disabled: () => disabled,
56
57
  unique: () => unique,
57
58
  max: () => max,
@@ -14,6 +14,6 @@ type Props = SvEditorProps & {
14
14
  /** Override the built-in strings (draft + remove aria-labels). */
15
15
  messages?: Partial<TagsMessages>;
16
16
  };
17
- declare const SvTagsInput: import("svelte").Component<Props, {}, "">;
17
+ declare const SvTagsInput: import("svelte").Component<Props, {}, "value">;
18
18
  type SvTagsInput = ReturnType<typeof SvTagsInput>;
19
19
  export default SvTagsInput;
@@ -12,8 +12,8 @@
12
12
  */
13
13
  import type { Snippet } from 'svelte'
14
14
  import { portalToBody, popIn } from './popover'
15
- import { nextEditorId } from './editor-contract'
16
15
  import { getFocusable } from './a11y/focus-trap'
16
+ import { createTooltip } from './createTooltip.svelte'
17
17
 
18
18
  type Props = {
19
19
  text: string
@@ -28,10 +28,12 @@
28
28
 
29
29
  let anchorEl = $state<HTMLSpanElement | null>(null)
30
30
  let tipEl = $state<HTMLDivElement | null>(null)
31
- let open = $state(false)
32
31
  let pos = $state({ top: 0, left: 0, arrowX: 12 })
33
- const tid = nextEditorId('sv-tip')
34
- let showTimer: ReturnType<typeof setTimeout> | undefined
32
+
33
+ // Open state, show-delay and Escape-to-hide live in the shared headless core;
34
+ // this component owns only the DOM concerns (positioning + describedby merge).
35
+ const tip = createTooltip({ text: () => text, disabled: () => disabled, delay: () => delay })
36
+ const tid = tip.tipId
35
37
 
36
38
  function place() {
37
39
  if (!anchorEl) return
@@ -46,25 +48,16 @@
46
48
  pos = { top, left, arrowX }
47
49
  }
48
50
 
49
- function show() {
50
- if (disabled || !text) return
51
- clearTimeout(showTimer)
52
- showTimer = setTimeout(() => { open = true; queueMicrotask(place) }, delay)
53
- }
54
- function hide() { clearTimeout(showTimer); open = false }
55
-
51
+ // Reposition while open (and on scroll/resize). Escape-to-hide is the core's.
56
52
  $effect(() => {
57
- if (!open) return
58
- place()
53
+ if (!tip.open) return
54
+ queueMicrotask(place)
59
55
  const rp = () => place()
60
56
  window.addEventListener('scroll', rp, true)
61
57
  window.addEventListener('resize', rp)
62
- const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') hide() }
63
- document.addEventListener('keydown', onKey)
64
58
  return () => {
65
59
  window.removeEventListener('scroll', rp, true)
66
60
  window.removeEventListener('resize', rp)
67
- document.removeEventListener('keydown', onKey)
68
61
  }
69
62
  })
70
63
 
@@ -75,7 +68,7 @@
75
68
  // announced. Mirror the id onto that child too, merging with (and later
76
69
  // restoring) any aria-describedby it already carries.
77
70
  $effect(() => {
78
- if (!open || !anchorEl) return
71
+ if (!tip.open || !anchorEl) return
79
72
  const child = getFocusable(anchorEl)[0]
80
73
  if (!child) return
81
74
  const before = child.getAttribute('aria-describedby')
@@ -96,16 +89,12 @@
96
89
  <span
97
90
  bind:this={anchorEl}
98
91
  class="sv-tip__anchor"
99
- aria-describedby={open ? tid : undefined}
100
- onpointerenter={show}
101
- onpointerleave={hide}
102
- onfocusin={show}
103
- onfocusout={hide}
92
+ {...tip.anchorProps()}
104
93
  >
105
94
  {@render children?.()}
106
95
  </span>
107
96
 
108
- {#if open}
97
+ {#if tip.open}
109
98
  <div
110
99
  bind:this={tipEl}
111
100
  id={tid}
@@ -98,14 +98,34 @@
98
98
  })
99
99
 
100
100
  const POP_W = 300
101
+ // Measured popover height, so placement can keep the WHOLE popover on-screen
102
+ // (long content + tall targets used to push it off the top).
103
+ let popH = $state(200)
101
104
  const pop = $derived.by(() => {
102
105
  if (typeof window === 'undefined' || !rect) {
103
106
  return { top: '50%', left: '50%', transform: 'translate(-50%, -50%)', place: 'center' as const }
104
107
  }
105
- const left = Math.max(12, Math.min(rect.left, window.innerWidth - POP_W - 12))
106
- const below = window.innerHeight - rect.bottom
107
- if (below > 190) return { top: `${rect.bottom + 10}px`, left: `${left}px`, transform: 'none', place: 'bottom' as const }
108
- return { top: `${rect.top - 10}px`, left: `${left}px`, transform: 'translateY(-100%)', place: 'top' as const }
108
+ const vw = window.innerWidth
109
+ const vh = window.innerHeight
110
+ const gap = 12
111
+ const h = popH || 200
112
+ const clampTop = (t: number) => Math.max(gap, Math.min(t, vh - h - gap))
113
+ const clampLeft = (l: number) => Math.max(gap, Math.min(l, vw - POP_W - gap))
114
+ // Prefer directly below the target, then directly above - but only when the
115
+ // target is short enough to leave room for the whole popover.
116
+ if (vh - rect.bottom >= h + gap) {
117
+ return { top: `${rect.bottom + 10}px`, left: `${clampLeft(rect.left)}px`, transform: 'none', place: 'bottom' as const }
118
+ }
119
+ if (rect.top >= h + gap) {
120
+ return { top: `${clampTop(rect.top - 10 - h)}px`, left: `${clampLeft(rect.left)}px`, transform: 'none', place: 'top' as const }
121
+ }
122
+ // Tall target (a full-height rail / canvas / inspector): sit BESIDE it,
123
+ // vertically centred on the target, on whichever side has room.
124
+ const top = clampTop(rect.top + rect.height / 2 - h / 2)
125
+ if (vw - rect.right >= POP_W + gap) return { top: `${top}px`, left: `${rect.right + 10}px`, transform: 'none', place: 'center' as const }
126
+ if (rect.left >= POP_W + gap) return { top: `${top}px`, left: `${rect.left - POP_W - 10}px`, transform: 'none', place: 'center' as const }
127
+ // No side room either - centre horizontally (still fully visible).
128
+ return { top: `${top}px`, left: `${clampLeft((vw - POP_W) / 2)}px`, transform: 'none', place: 'center' as const }
109
129
  })
110
130
  </script>
111
131
 
@@ -125,6 +145,7 @@
125
145
 
126
146
  <div
127
147
  bind:this={popEl}
148
+ bind:clientHeight={popH}
128
149
  class="sv-tour__pop sv-tour__pop--{pop.place}"
129
150
  style:top={pop.top}
130
151
  style:left={pop.left}
@@ -133,6 +154,11 @@
133
154
  aria-modal="true"
134
155
  aria-label={step.title ?? 'Tour step'}
135
156
  >
157
+ {#if step.image}
158
+ <div class="sv-tour__media">
159
+ {#if /^(https?:|data:|\/)/.test(step.image)}<img class="sv-tour__img" src={step.image} alt="" />{:else}{@html step.image}{/if}
160
+ </div>
161
+ {/if}
136
162
  {#if step.title}<div class="sv-tour__title">{step.title}</div>{/if}
137
163
  <div class="sv-tour__body">{step.content}</div>
138
164
  <div class="sv-tour__foot">
@@ -163,6 +189,8 @@
163
189
  }
164
190
  @keyframes sv-tour-in { from { opacity: 0; transform: translateY(4px) } }
165
191
  :global(.sv-tour__pop--top) { animation: none; }
192
+ :global(.sv-tour__media) { margin: -2px -4px 12px; border-radius: 9px; overflow: hidden; line-height: 0; }
193
+ :global(.sv-tour__media svg), :global(.sv-tour__img) { display: block; width: 100%; height: auto; }
166
194
  :global(.sv-tour__title) { font-size: 14.5px; font-weight: 650; margin-bottom: 4px; }
167
195
  :global(.sv-tour__body) { font-size: 13px; line-height: 1.55; color: var(--sg-fg, #334155); }
168
196
  :global(.sv-tour__foot) { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 14px; }
@@ -166,6 +166,7 @@
166
166
  aria-label={label ?? ariaLabel ?? 'Tree'}
167
167
  aria-activedescendant={ts.activeDescendant()}
168
168
  tabindex="0"
169
+ style:max-height={`${ts.rect.maxHeight}px`}
169
170
  onkeydown={ts.onPanelKeydown}
170
171
  >
171
172
  {#each rows as row, i (row.node.value)}