@svgrid/grid 2.2.16 → 2.2.18

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 (290) hide show
  1. package/README.md +1 -1
  2. package/dist/GridFooter.svelte +12 -11
  3. package/dist/SvAnchor.svelte +65 -0
  4. package/dist/SvAnchor.svelte.d.ts +24 -0
  5. package/dist/SvAspectRatio.svelte +38 -0
  6. package/dist/SvAspectRatio.svelte.d.ts +16 -0
  7. package/dist/SvAutoComplete.svelte +52 -7
  8. package/dist/SvAutoComplete.svelte.d.ts +4 -1
  9. package/dist/SvBlockquote.svelte +43 -0
  10. package/dist/SvBlockquote.svelte.d.ts +16 -0
  11. package/dist/SvCalendar.svelte +33 -2
  12. package/dist/SvCalendar.svelte.d.ts +8 -1
  13. package/dist/SvCheckBox.svelte +6 -2
  14. package/dist/SvCode.svelte +52 -0
  15. package/dist/SvCode.svelte.d.ts +19 -0
  16. package/dist/SvCollapsible.svelte +75 -0
  17. package/dist/SvCollapsible.svelte.d.ts +27 -0
  18. package/dist/SvColorInput.svelte +11 -17
  19. package/dist/SvColorInput.svelte.d.ts +2 -0
  20. package/dist/SvComboBox.svelte +85 -8
  21. package/dist/SvComboBox.svelte.d.ts +14 -0
  22. package/dist/SvCountryInput.svelte +2 -2
  23. package/dist/SvCountryInput.svelte.d.ts +1 -1
  24. package/dist/SvDropDownList.svelte +154 -51
  25. package/dist/SvDropDownList.svelte.d.ts +17 -3
  26. package/dist/SvDurationInput.svelte +59 -41
  27. package/dist/SvDurationInput.svelte.d.ts +18 -0
  28. package/dist/SvField.svelte +203 -12
  29. package/dist/SvField.svelte.d.ts +45 -8
  30. package/dist/SvForm.svelte +195 -21
  31. package/dist/SvForm.svelte.d.ts +23 -0
  32. package/dist/SvGrid.controller.svelte.d.ts +12 -1
  33. package/dist/SvGrid.controller.svelte.js +87 -7
  34. package/dist/SvGrid.svelte +383 -181
  35. package/dist/SvGrid.types.d.ts +123 -2
  36. package/dist/SvGridChartView.svelte +70 -0
  37. package/dist/SvGridChartView.svelte.d.ts +11 -0
  38. package/dist/SvGridDropdown.svelte +9 -2
  39. package/dist/SvGridSelect.svelte +5 -2
  40. package/dist/SvGridSelect.svelte.d.ts +1 -1
  41. package/dist/SvGroup.svelte +41 -0
  42. package/dist/SvGroup.svelte.d.ts +27 -0
  43. package/dist/SvHoverCard.svelte +70 -0
  44. package/dist/SvHoverCard.svelte.d.ts +41 -0
  45. package/dist/SvKbd.svelte +51 -0
  46. package/dist/SvKbd.svelte.d.ts +18 -0
  47. package/dist/SvList.svelte +55 -0
  48. package/dist/SvList.svelte.d.ts +23 -0
  49. package/dist/SvListBox.svelte +166 -77
  50. package/dist/SvListBox.svelte.d.ts +17 -4
  51. package/dist/SvLoadingOverlay.svelte +53 -0
  52. package/dist/SvLoadingOverlay.svelte.d.ts +28 -0
  53. package/dist/SvMark.svelte +36 -0
  54. package/dist/SvMark.svelte.d.ts +14 -0
  55. package/dist/SvMaskedInput.svelte +50 -35
  56. package/dist/SvMaskedInput.svelte.d.ts +17 -2
  57. package/dist/SvMenuList.svelte +32 -3
  58. package/dist/SvMenubar.svelte +200 -0
  59. package/dist/SvMenubar.svelte.d.ts +18 -0
  60. package/dist/SvMultiSelect.svelte +7 -4
  61. package/dist/SvMultiSelect.svelte.d.ts +1 -1
  62. package/dist/SvNumberInput.svelte +79 -42
  63. package/dist/SvNumberInput.svelte.d.ts +25 -0
  64. package/dist/SvOtpInput.svelte +2 -2
  65. package/dist/SvPasswordInput.svelte +51 -31
  66. package/dist/SvPasswordInput.svelte.d.ts +17 -0
  67. package/dist/SvPhoneInput.svelte +39 -28
  68. package/dist/SvPhoneInput.svelte.d.ts +4 -0
  69. package/dist/SvPopconfirm.svelte +79 -0
  70. package/dist/SvPopconfirm.svelte.d.ts +42 -0
  71. package/dist/SvPopover.svelte +87 -29
  72. package/dist/SvPopover.svelte.d.ts +18 -4
  73. package/dist/SvRadioGroup.svelte +5 -1
  74. package/dist/SvResult.svelte +73 -0
  75. package/dist/SvResult.svelte.d.ts +28 -0
  76. package/dist/SvSegmented.svelte +114 -0
  77. package/dist/SvSegmented.svelte.d.ts +29 -0
  78. package/dist/SvSimpleGrid.svelte +41 -0
  79. package/dist/SvSimpleGrid.svelte.d.ts +25 -0
  80. package/dist/SvSlider.svelte +23 -10
  81. package/dist/SvSpinner.svelte +43 -0
  82. package/dist/SvSpinner.svelte.d.ts +20 -0
  83. package/dist/SvStack.svelte +37 -0
  84. package/dist/SvStack.svelte.d.ts +24 -0
  85. package/dist/SvSwitchButton.svelte +6 -1
  86. package/dist/SvTagsInput.svelte +9 -5
  87. package/dist/SvText.svelte +76 -0
  88. package/dist/SvText.svelte.d.ts +31 -0
  89. package/dist/SvTextInput.svelte +75 -43
  90. package/dist/SvTextInput.svelte.d.ts +28 -1
  91. package/dist/SvTimePicker.svelte +32 -2
  92. package/dist/SvTimePicker.svelte.d.ts +8 -9
  93. package/dist/SvTitle.svelte +57 -0
  94. package/dist/SvTitle.svelte.d.ts +24 -0
  95. package/dist/SvToaster.svelte +41 -6
  96. package/dist/SvToggleButton.svelte +4 -1
  97. package/dist/SvTooltip.svelte +68 -34
  98. package/dist/SvTooltip.svelte.d.ts +14 -2
  99. package/dist/SvTree.svelte +81 -21
  100. package/dist/SvTree.svelte.d.ts +3 -2
  101. package/dist/SvTreeSelect.svelte +5 -2
  102. package/dist/SvTreeSelect.svelte.d.ts +1 -1
  103. package/dist/SvVisuallyHidden.svelte +49 -0
  104. package/dist/SvVisuallyHidden.svelte.d.ts +19 -0
  105. package/dist/ai.d.ts +289 -0
  106. package/dist/ai.js +1013 -0
  107. package/dist/board-view.svelte.d.ts +22 -0
  108. package/dist/board-view.svelte.js +13 -0
  109. package/dist/build-api.js +36 -6
  110. package/dist/cdn/GridMenus-0AK9O7qG.js +420 -0
  111. package/dist/cdn/GridMenus-BjUoCXoD.js +416 -0
  112. package/dist/cdn/SvDateTimePicker-CYC7gRER.js +2176 -0
  113. package/dist/cdn/SvDateTimePicker-DtQKd4Ns.js +2198 -0
  114. package/dist/cdn/SvGridChart-BcLCYvw9.js +1195 -0
  115. package/dist/cdn/SvGridChart-BhnFdLd0.js +1203 -0
  116. package/dist/cdn/SvGridChartPanel-Cmx9DHAS.js +561 -0
  117. package/dist/cdn/SvGridChartPanel-DvjwxLvK.js +577 -0
  118. package/dist/cdn/SvGridChartView-B40DTJSX.js +54 -0
  119. package/dist/cdn/SvGridChartView-PwqI4s02.js +53 -0
  120. package/dist/cdn/SvGridDropdown-DjALc_4f.js +255 -0
  121. package/dist/cdn/SvGridDropdown-hpnHvOGt.js +254 -0
  122. package/dist/cdn/chart-T1usDN0o.js +1212 -0
  123. package/dist/cdn/disclose-version-DoD9AFD_.js +2958 -0
  124. package/dist/cdn/export-format-JT5pWVT8.js +561 -0
  125. package/dist/cdn/popover-P-9P2YYn.js +69 -0
  126. package/dist/cdn/rolldown-runtime-Dy4uBu1J.js +11 -0
  127. package/dist/cdn/src-BLhdLmoN.js +25707 -0
  128. package/dist/cdn/src-BwnUVj3T.js +25623 -0
  129. package/dist/cdn/svgrid.js +9 -32897
  130. package/dist/cdn/svgrid.svelte-external.js +9 -30052
  131. package/dist/cell-formatting.d.ts +2 -2
  132. package/dist/cell-render.js +4 -4
  133. package/dist/chart-view.svelte.d.ts +25 -0
  134. package/dist/chart-view.svelte.js +14 -0
  135. package/dist/createCombobox.svelte.d.ts +5 -0
  136. package/dist/createCombobox.svelte.js +21 -5
  137. package/dist/createDropdownList.svelte.d.ts +3 -0
  138. package/dist/createDropdownList.svelte.js +8 -2
  139. package/dist/createForm.svelte.d.ts +18 -0
  140. package/dist/createForm.svelte.js +51 -6
  141. package/dist/createMenu.svelte.js +27 -0
  142. package/dist/createPopoverSelect.svelte.d.ts +3 -0
  143. package/dist/createPopoverSelect.svelte.js +9 -1
  144. package/dist/createSlider.svelte.d.ts +8 -21
  145. package/dist/createSlider.svelte.js +34 -0
  146. package/dist/createTooltip.svelte.d.ts +12 -0
  147. package/dist/createTooltip.svelte.js +72 -9
  148. package/dist/editor-contract.d.ts +15 -0
  149. package/dist/export-provider.d.ts +27 -0
  150. package/dist/export-provider.js +9 -0
  151. package/dist/filter-operators.d.ts +6 -0
  152. package/dist/filter-operators.js +33 -0
  153. package/dist/form-field.d.ts +45 -2
  154. package/dist/grid-messages.d.ts +61 -0
  155. package/dist/grid-messages.js +72 -0
  156. package/dist/group-display.d.ts +62 -0
  157. package/dist/group-display.js +78 -0
  158. package/dist/index.d.ts +36 -4
  159. package/dist/index.js +43 -2
  160. package/dist/js-scroller.svelte.d.ts +35 -0
  161. package/dist/js-scroller.svelte.js +154 -0
  162. package/dist/list-option.d.ts +46 -0
  163. package/dist/list-option.js +53 -0
  164. package/dist/menus.js +25 -11
  165. package/dist/pivot-view.svelte.d.ts +72 -0
  166. package/dist/pivot-view.svelte.js +13 -0
  167. package/dist/positioning.d.ts +111 -0
  168. package/dist/positioning.js +191 -0
  169. package/dist/scheduler-model.d.ts +3 -0
  170. package/dist/scheduler-model.js +22 -5
  171. package/dist/svgrid-wrapper.types.d.ts +18 -0
  172. package/dist/toast-store.svelte.d.ts +54 -0
  173. package/dist/toast-store.svelte.js +65 -15
  174. package/package.json +9 -1
  175. package/src/GridFooter.svelte +12 -11
  176. package/src/SvAnchor.svelte +65 -0
  177. package/src/SvAspectRatio.svelte +38 -0
  178. package/src/SvAutoComplete.svelte +52 -7
  179. package/src/SvBlockquote.svelte +43 -0
  180. package/src/SvCalendar.svelte +33 -2
  181. package/src/SvCalendar.test.ts +15 -0
  182. package/src/SvCheckBox.svelte +6 -2
  183. package/src/SvCode.svelte +52 -0
  184. package/src/SvCollapsible.svelte +75 -0
  185. package/src/SvColorInput.svelte +11 -17
  186. package/src/SvComboBox.svelte +85 -8
  187. package/src/SvCountryInput.svelte +2 -2
  188. package/src/SvDropDownList.svelte +154 -51
  189. package/src/SvDurationInput.svelte +59 -41
  190. package/src/SvField.svelte +203 -12
  191. package/src/SvForm.svelte +195 -21
  192. package/src/SvForm.test.ts +112 -0
  193. package/src/SvGrid.controller.svelte.ts +88 -7
  194. package/src/SvGrid.svelte +383 -181
  195. package/src/SvGrid.types.ts +125 -1
  196. package/src/SvGridChartView.svelte +70 -0
  197. package/src/SvGridDropdown.svelte +9 -2
  198. package/src/SvGridSelect.svelte +5 -2
  199. package/src/SvGroup.svelte +41 -0
  200. package/src/SvHoverCard.svelte +70 -0
  201. package/src/SvKbd.svelte +51 -0
  202. package/src/SvList.svelte +55 -0
  203. package/src/SvListBox.svelte +166 -77
  204. package/src/SvLoadingOverlay.svelte +53 -0
  205. package/src/SvMark.svelte +36 -0
  206. package/src/SvMaskedInput.svelte +50 -35
  207. package/src/SvMenuList.svelte +32 -3
  208. package/src/SvMenubar.svelte +200 -0
  209. package/src/SvMultiSelect.svelte +7 -4
  210. package/src/SvNumberInput.svelte +79 -42
  211. package/src/SvOtpInput.svelte +2 -2
  212. package/src/SvPasswordInput.svelte +51 -31
  213. package/src/SvPhoneInput.svelte +39 -28
  214. package/src/SvPopconfirm.svelte +79 -0
  215. package/src/SvPopover.svelte +87 -29
  216. package/src/SvRadioGroup.svelte +5 -1
  217. package/src/SvResult.svelte +73 -0
  218. package/src/SvSegmented.svelte +114 -0
  219. package/src/SvSimpleGrid.svelte +41 -0
  220. package/src/SvSlider.svelte +23 -10
  221. package/src/SvSpinner.svelte +43 -0
  222. package/src/SvStack.svelte +37 -0
  223. package/src/SvSwitchButton.svelte +6 -1
  224. package/src/SvTagsInput.svelte +9 -5
  225. package/src/SvText.svelte +76 -0
  226. package/src/SvTextInput.svelte +75 -43
  227. package/src/SvTimePicker.svelte +32 -2
  228. package/src/SvTitle.svelte +57 -0
  229. package/src/SvToaster.svelte +41 -6
  230. package/src/SvToaster.test.ts +32 -0
  231. package/src/SvToggleButton.svelte +4 -1
  232. package/src/SvTooltip.svelte +68 -34
  233. package/src/SvTree.svelte +81 -21
  234. package/src/SvTreeSelect.svelte +5 -2
  235. package/src/SvVisuallyHidden.svelte +49 -0
  236. package/src/ai.test.ts +502 -0
  237. package/src/ai.ts +1391 -0
  238. package/src/board-view.svelte.ts +33 -0
  239. package/src/build-api.coverage.test.ts +101 -0
  240. package/src/build-api.ts +43 -18
  241. package/src/builtin-editors.grid.test.ts +1 -1
  242. package/src/cell-formatting.ts +5 -5
  243. package/src/cell-render.ts +4 -4
  244. package/src/chart-view.svelte.ts +36 -0
  245. package/src/createCombobox.svelte.ts +17 -5
  246. package/src/createDropdownList.svelte.ts +8 -2
  247. package/src/createForm.svelte.ts +59 -6
  248. package/src/createMenu.svelte.ts +23 -0
  249. package/src/createPopoverSelect.svelte.ts +8 -1
  250. package/src/createSlider.svelte.ts +20 -0
  251. package/src/createTooltip.svelte.ts +67 -9
  252. package/src/editor-contract.ts +16 -0
  253. package/src/export-provider.ts +40 -0
  254. package/src/filter-operators.ts +37 -0
  255. package/src/form-field.ts +42 -2
  256. package/src/grid-messages.test.ts +24 -0
  257. package/src/grid-messages.ts +137 -0
  258. package/src/group-display.test.ts +77 -0
  259. package/src/group-display.ts +117 -0
  260. package/src/i18n.grid.test.ts +73 -0
  261. package/src/index.ts +91 -2
  262. package/src/js-scroller.svelte.ts +173 -0
  263. package/src/list-option.test.ts +56 -0
  264. package/src/list-option.ts +81 -0
  265. package/src/menus.test.ts +32 -0
  266. package/src/menus.ts +25 -12
  267. package/src/pivot-view.svelte.ts +90 -0
  268. package/src/pivot-view.test.ts +38 -0
  269. package/src/pivot.grid.test.ts +102 -0
  270. package/src/positioning.test.ts +141 -0
  271. package/src/positioning.ts +266 -0
  272. package/src/scheduler-model.test.ts +16 -0
  273. package/src/scheduler-model.ts +24 -5
  274. package/src/svgrid-wrapper.types.ts +23 -0
  275. package/src/svgrid.charting.test.ts +8 -1
  276. package/src/svgrid.comments-autocomplete.test.ts +7 -2
  277. package/src/svgrid.context-menu.test.ts +8 -3
  278. package/src/toast-store.svelte.ts +98 -0
  279. package/src/toast-store.test.ts +62 -1
  280. package/src/ui-breadth.test.ts +138 -0
  281. package/src/ui-buttons.test.ts +39 -0
  282. package/src/ui-inputs.test.ts +2 -2
  283. package/src/ui-range.test.ts +9 -0
  284. package/src/ui-selection.test.ts +36 -0
  285. package/src/ui-tier1.test.ts +1 -1
  286. package/src/virtual.test.ts +20 -6
  287. package/dist/SvGridBoard.svelte +0 -2339
  288. package/dist/SvGridBoard.svelte.d.ts +0 -31
  289. package/src/SvGridBoard.svelte +0 -2339
  290. package/src/board.test.ts +0 -927
@@ -0,0 +1,200 @@
1
+ <script lang="ts" module>
2
+ export type MenubarMenu = {
3
+ /** Trigger label on the bar. */
4
+ label: string
5
+ /** The dropdown's items (submenus, separators, icons, shortcuts all work). */
6
+ items: import('./menu-item').MenuItem[]
7
+ disabled?: boolean
8
+ }
9
+ </script>
10
+
11
+ <script lang="ts">
12
+ /**
13
+ * SvMenubar - an application menu bar: a horizontal row of menu buttons
14
+ * (File / Edit / View ...) that each open a dropdown built on SvMenuList.
15
+ * Roving focus across the bar, ArrowLeft/Right move between menus, ArrowDown /
16
+ * Enter open the active one, hovering another trigger switches while a menu is
17
+ * open, and Escape closes and returns focus to the trigger. The dropdown is
18
+ * portalled and positioned by the shared engine.
19
+ *
20
+ * ```svelte
21
+ * <SvMenubar menus={[{ label: 'File', items: fileItems }, ...]}
22
+ * onSelect={(i) => run(i)} />
23
+ * ```
24
+ */
25
+ import { portalToBody, popIn } from './popover'
26
+ import { computePosition, autoUpdate, type ComputePositionResult } from './positioning'
27
+ import { createDismissableLayer } from './a11y/dismissable'
28
+ import { type EditorDir } from './editor-contract'
29
+ import SvMenuList from './SvMenuList.svelte'
30
+ import type { MenuItem } from './menu-item'
31
+
32
+ type Props = {
33
+ menus: ReadonlyArray<MenubarMenu>
34
+ onSelect?: (item: MenuItem) => void
35
+ ariaLabel?: string
36
+ dir?: EditorDir
37
+ }
38
+
39
+ let { menus, onSelect, ariaLabel = 'Menu bar', dir }: Props = $props()
40
+
41
+ const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
42
+ const rtl = $derived(resolvedDir === 'rtl')
43
+
44
+ let triggerEls = $state<(HTMLButtonElement | null)[]>([])
45
+ let panelEl = $state<HTMLDivElement | null>(null)
46
+ let openIndex = $state(-1)
47
+ let activeIndex = $state(0)
48
+ let pos = $state<ComputePositionResult>({ x: 0, y: 0, placement: 'bottom-start', side: 'bottom', align: 'start', maxWidth: 0, maxHeight: 0 })
49
+
50
+ const enabled = (i: number) => !menus[i]?.disabled
51
+
52
+ function focusTrigger(i: number) { queueMicrotask(() => triggerEls[i]?.focus()) }
53
+ function focusFirstItem() {
54
+ queueMicrotask(() => panelEl?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])')?.focus())
55
+ }
56
+
57
+ function open(i: number) {
58
+ if (!enabled(i)) return
59
+ openIndex = i
60
+ activeIndex = i
61
+ focusFirstItem()
62
+ }
63
+ function close(refocus = true) {
64
+ const i = openIndex
65
+ openIndex = -1
66
+ if (refocus && i >= 0) focusTrigger(i)
67
+ }
68
+ function toggle(i: number) { if (openIndex === i) close(); else open(i) }
69
+ function onTriggerEnter(i: number) {
70
+ // While a menu is open, hovering another trigger switches to it (menubar UX).
71
+ if (openIndex >= 0 && openIndex !== i && enabled(i)) open(i)
72
+ }
73
+
74
+ function step(from: number, delta: number): number {
75
+ const n = menus.length
76
+ for (let k = 1; k <= n; k++) {
77
+ const i = (from + delta * k + n * k) % n
78
+ if (enabled(i)) return i
79
+ }
80
+ return from
81
+ }
82
+ function moveActive(delta: number) {
83
+ activeIndex = step(activeIndex, delta)
84
+ focusTrigger(activeIndex)
85
+ if (openIndex >= 0) open(activeIndex)
86
+ }
87
+
88
+ function onBarKeydown(e: KeyboardEvent) {
89
+ const fwd = rtl ? 'ArrowLeft' : 'ArrowRight'
90
+ const back = rtl ? 'ArrowRight' : 'ArrowLeft'
91
+ switch (e.key) {
92
+ case fwd: e.preventDefault(); moveActive(1); break
93
+ case back: e.preventDefault(); moveActive(-1); break
94
+ case 'Home': e.preventDefault(); activeIndex = step(-1, 1); focusTrigger(activeIndex); if (openIndex >= 0) open(activeIndex); break
95
+ case 'End': e.preventDefault(); activeIndex = step(menus.length, -1); focusTrigger(activeIndex); if (openIndex >= 0) open(activeIndex); break
96
+ case 'ArrowDown':
97
+ case 'Enter':
98
+ case ' ': e.preventDefault(); open(activeIndex); break
99
+ case 'Escape': if (openIndex >= 0) { e.preventDefault(); close() } break
100
+ }
101
+ }
102
+
103
+ function updatePos() {
104
+ const trigger = triggerEls[openIndex]
105
+ if (!trigger) return
106
+ const r = trigger.getBoundingClientRect()
107
+ const f = { width: panelEl?.offsetWidth || 180, height: panelEl?.offsetHeight || 200 }
108
+ pos = computePosition(
109
+ { x: r.left, y: r.top, width: r.width, height: r.height },
110
+ f,
111
+ { placement: rtl ? 'bottom-end' : 'bottom-start', offset: 4, padding: 8, minMainAxis: 96 },
112
+ )
113
+ }
114
+
115
+ // Position + dismiss the open dropdown, and let ArrowLeft/Right at the root menu
116
+ // level switch to the adjacent menubar menu (WAI-ARIA menubar behavior). An
117
+ // ArrowRight on a submenu parent still opens its submenu; ArrowLeft inside a
118
+ // submenu still collapses - those are left to SvMenuList.
119
+ $effect(() => {
120
+ const trigger = triggerEls[openIndex]
121
+ if (openIndex < 0 || !panelEl || !trigger) return
122
+ const stop = autoUpdate(trigger, panelEl, updatePos)
123
+ const layer = createDismissableLayer({
124
+ element: () => [trigger, panelEl],
125
+ onDismiss: () => close(false),
126
+ closeOnEscape: false, // Escape is owned by SvMenuList -> onclose -> close()
127
+ })
128
+ layer.activate()
129
+
130
+ const onKey = (e: KeyboardEvent) => {
131
+ const el = document.activeElement as HTMLElement | null
132
+ const list = el?.closest('.sv-menu__list')
133
+ const inSubmenu = !!list?.classList.contains('is-submenu')
134
+ const fwd = rtl ? 'ArrowLeft' : 'ArrowRight'
135
+ const back = rtl ? 'ArrowRight' : 'ArrowLeft'
136
+ if (e.key === fwd) {
137
+ if (el?.getAttribute('aria-haspopup') === 'menu') return // opens a submenu
138
+ e.preventDefault(); open(step(openIndex, 1))
139
+ } else if (e.key === back) {
140
+ if (inSubmenu) return // collapse the submenu instead
141
+ e.preventDefault(); open(step(openIndex, -1))
142
+ }
143
+ }
144
+ document.addEventListener('keydown', onKey, true)
145
+ return () => { stop(); layer.release(); document.removeEventListener('keydown', onKey, true) }
146
+ })
147
+ </script>
148
+
149
+ <div class="sv-menubar" role="menubar" aria-label={ariaLabel} dir={resolvedDir} onkeydown={onBarKeydown}>
150
+ {#each menus as m, i (i)}
151
+ <button
152
+ type="button"
153
+ bind:this={triggerEls[i]}
154
+ class="sv-menubar__item"
155
+ class:is-open={openIndex === i}
156
+ role="menuitem"
157
+ aria-haspopup="menu"
158
+ aria-expanded={openIndex === i}
159
+ tabindex={activeIndex === i ? 0 : -1}
160
+ disabled={m.disabled}
161
+ onclick={() => toggle(i)}
162
+ onpointerenter={() => onTriggerEnter(i)}
163
+ >{m.label}</button>
164
+ {/each}
165
+ </div>
166
+
167
+ {#if openIndex >= 0}
168
+ <div
169
+ bind:this={panelEl}
170
+ class="sv-menu"
171
+ use:portalToBody
172
+ use:popIn={{ up: pos.side === 'top' }}
173
+ style:position="fixed"
174
+ style:top={`${pos.y}px`}
175
+ style:left={`${pos.x}px`}
176
+ dir={resolvedDir}
177
+ >
178
+ <SvMenuList
179
+ items={menus[openIndex]?.items ?? []}
180
+ onclose={() => close()}
181
+ onselect={(item) => { onSelect?.(item); close() }}
182
+ dir={resolvedDir}
183
+ />
184
+ </div>
185
+ {/if}
186
+
187
+ <style>
188
+ .sv-menubar {
189
+ display: inline-flex; align-items: center; gap: 2px; padding: 3px;
190
+ background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0); border-radius: 9px;
191
+ }
192
+ .sv-menubar__item {
193
+ font: inherit; font-size: 13px; font-weight: 500; color: var(--sg-fg, #0f172a);
194
+ padding: 5px 11px; background: none; border: 0; border-radius: 6px; cursor: pointer;
195
+ transition: background 0.1s;
196
+ }
197
+ .sv-menubar__item:hover:not([disabled]), .sv-menubar__item.is-open { background: var(--sg-row-hover-bg, #f1f5f9); }
198
+ .sv-menubar__item:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--sg-accent, #2563eb)); outline-offset: -2px; }
199
+ .sv-menubar__item[disabled] { opacity: 0.45; cursor: default; }
200
+ </style>
@@ -20,7 +20,7 @@
20
20
 
21
21
  type Props = Pick<
22
22
  SvEditorProps,
23
- 'disabled' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel'
23
+ 'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'
24
24
  > & {
25
25
  options: ReadonlyArray<MultiSelectOption>
26
26
  value?: ReadonlyArray<string | number>
@@ -53,6 +53,7 @@
53
53
  debounceMs = 250,
54
54
  loadingText = 'Loading…',
55
55
  disabled = false,
56
+ readonly = false,
56
57
  label,
57
58
  hint,
58
59
  error,
@@ -63,6 +64,7 @@
63
64
  name,
64
65
  dir,
65
66
  ariaLabel,
67
+ loading: loadingProp = false,
66
68
  }: Props = $props()
67
69
 
68
70
  const autoId = nextEditorId('sv-ms')
@@ -120,6 +122,7 @@
120
122
  const ms = createPopoverSelect({
121
123
  itemCount: () => filtered.length,
122
124
  disabled: (i) => !!filtered[i]?.disabled,
125
+ readonly: () => readonly,
123
126
  onSelect: (i) => { const o = filtered[i]; if (o) toggleOption(o) },
124
127
  onOpenChange: (o) => { if (o) { search = ''; if (loadOptions) doLoad('') } },
125
128
  getTrigger: () => triggerEl,
@@ -131,7 +134,7 @@
131
134
  })
132
135
  </script>
133
136
 
134
- <SvField id={uid} {label} {hint} {error} {required} {dir}>
137
+ <SvField id={uid} {label} {hint} {error} {required} {dir} loading={loadingProp || loading}>
135
138
  <div class="sv-ms sv-ms--{size}" class:is-disabled={disabled}>
136
139
  <button
137
140
  bind:this={triggerEl}
@@ -147,7 +150,7 @@
147
150
  <span class="sv-ms__placeholder">{placeholder}</span>
148
151
  {:else}
149
152
  <span class="sv-ms__chips">
150
- {#each selectedOptions.slice(0, maxTagCount) as opt (opt.value)}
153
+ {#each selectedOptions.slice(0, maxTagCount) as opt, i (i)}
151
154
  <span class="sv-ms__chip">{opt.label}</span>
152
155
  {/each}
153
156
  {#if selectedOptions.length > maxTagCount}
@@ -206,7 +209,7 @@
206
209
  aria-activedescendant={searchable ? undefined : ms.activeDescendant()}
207
210
  tabindex={searchable ? -1 : 0}
208
211
  >
209
- {#each filtered as opt, i (opt.value)}
212
+ {#each filtered as opt, i (i)}
210
213
  <li
211
214
  class="sv-ms__opt"
212
215
  class:is-active={ms.isActive(i)}
@@ -6,8 +6,12 @@
6
6
  *
7
7
  * This is the styled renderer over the headless `createNumberInput` core - the
8
8
  * same split as the grid (`createSvGrid` / `<SvGrid>`): parse/format/clamp,
9
- * spinner + keyboard and ARIA all come from the core via prop-getters.
9
+ * spinner + keyboard and ARIA all come from the core via prop-getters. The box,
10
+ * size, invalid state and focus ring are owned by SvField's `frame` chrome; the
11
+ * `prefix`/`suffix` are formatted INTO the value (they travel with the
12
+ * right-aligned number), and the spinner + clear live in the trailing slot.
10
13
  */
14
+ import type { Snippet } from 'svelte'
11
15
  import SvRepeatButton from './SvRepeatButton.svelte'
12
16
  import SvField from './SvField.svelte'
13
17
  import { nextEditorId, type SvEditorProps } from './editor-contract'
@@ -22,12 +26,24 @@
22
26
  precision?: number
23
27
  /** Group thousands (1,234) on display. */
24
28
  grouping?: boolean
29
+ /** Text formatted before the number (e.g. `$`); travels with the value. */
25
30
  prefix?: string
31
+ /** Text formatted after the number (e.g. `%`); travels with the value. */
26
32
  suffix?: string
27
33
  placeholder?: string
28
34
  spinButtons?: boolean
29
35
  /** Show a clear (x) button when there is a value. */
30
36
  clearable?: boolean
37
+ /** Select the whole value when the field is focused. */
38
+ selectOnFocus?: boolean
39
+ /** Step the value with the mouse wheel while the field is focused. */
40
+ wheelStep?: boolean
41
+ /** Leading adornment (icon) inside the field. */
42
+ leading?: Snippet
43
+ /** Stretch to the container width. */
44
+ block?: boolean
45
+ /** Control width in px (ignored when `block`). Default 150. */
46
+ width?: number
31
47
  }
32
48
 
33
49
  let {
@@ -45,6 +61,8 @@
45
61
  readonly = false,
46
62
  spinButtons = true,
47
63
  clearable = false,
64
+ selectOnFocus = false,
65
+ wheelStep = false,
48
66
  name,
49
67
  size = 'md',
50
68
  ariaLabel,
@@ -55,6 +73,10 @@
55
73
  hint,
56
74
  dir,
57
75
  id,
76
+ loading = false,
77
+ leading,
78
+ block = false,
79
+ width = 150,
58
80
  }: Props = $props()
59
81
 
60
82
  const autoId = nextEditorId('sv-num')
@@ -81,51 +103,66 @@
81
103
  hint: () => hint,
82
104
  ariaLabel: () => ariaLabel,
83
105
  })
106
+
107
+ // DOM-bound ergonomics the headless core stays out of: select-all on focus and
108
+ // wheel-to-step (only while focused, so scrolling the page never changes it).
109
+ function numExtras(node: HTMLInputElement) {
110
+ const onFocus = () => { if (selectOnFocus) requestAnimationFrame(() => node.select()) }
111
+ const onWheel = (e: WheelEvent) => {
112
+ if (!wheelStep || document.activeElement !== node || !num.isInteractive) return
113
+ e.preventDefault()
114
+ num.bump(e.deltaY < 0 ? 1 : -1)
115
+ }
116
+ node.addEventListener('focus', onFocus)
117
+ node.addEventListener('wheel', onWheel, { passive: false })
118
+ return { destroy() { node.removeEventListener('focus', onFocus); node.removeEventListener('wheel', onWheel) } }
119
+ }
84
120
  </script>
85
121
 
86
- <SvField id={uid} {label} {hint} {error} {required} {dir}>
87
- <div class="sv-num sv-num--{size}" class:is-disabled={disabled} class:is-invalid={invalid}>
88
- <input class="sv-num__input" {...num.inputProps()} />
89
- {#if clearable && value != null && !disabled && !readonly}
90
- <button type="button" class="sv-num__clear" aria-label="Clear" tabindex="-1" onclick={() => { value = null; onChange?.(null) }}>
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>
92
- </button>
93
- {/if}
94
- {#if spinButtons}
95
- <div class="sv-num__spin">
96
- <SvRepeatButton size="sm" variant="ghost" ariaLabel="Increment" onclick={num.increment}>
97
- <svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M6 15l6-6 6 6" /></svg>
98
- </SvRepeatButton>
99
- <SvRepeatButton size="sm" variant="ghost" ariaLabel="Decrement" onclick={num.decrement}>
100
- <svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6" /></svg>
101
- </SvRepeatButton>
102
- </div>
103
- {/if}
104
- {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
105
- </div>
122
+ {#snippet trailing()}
123
+ {#if clearable && value != null && !disabled && !readonly}
124
+ <button type="button" class="sv-num__clear" aria-label="Clear" tabindex="-1" onclick={() => { value = null; onChange?.(null) }}>
125
+ <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>
126
+ </button>
127
+ {/if}
128
+ {#if spinButtons}
129
+ <div class="sv-num__spin">
130
+ <SvRepeatButton size="sm" variant="ghost" ariaLabel="Increment" onclick={num.increment}>
131
+ <svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M6 15l6-6 6 6" /></svg>
132
+ </SvRepeatButton>
133
+ <SvRepeatButton size="sm" variant="ghost" ariaLabel="Decrement" onclick={num.decrement}>
134
+ <svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6" /></svg>
135
+ </SvRepeatButton>
136
+ </div>
137
+ {/if}
138
+ {/snippet}
139
+
140
+ <SvField
141
+ frame
142
+ id={uid}
143
+ {label}
144
+ {hint}
145
+ {error}
146
+ {required}
147
+ {dir}
148
+ {size}
149
+ {invalid}
150
+ {disabled}
151
+ {readonly}
152
+ {loading}
153
+ {block}
154
+ {width}
155
+ {leading}
156
+ {trailing}
157
+ >
158
+ <input class="sv-num__input" use:numExtras {...num.inputProps()} />
159
+ {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
106
160
  </SvField>
107
161
 
108
162
  <style>
109
- .sv-num {
110
- --_accent: var(--sg-accent, #2563eb);
111
- display: inline-flex; align-items: stretch; width: 150px;
112
- background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
113
- border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
114
- overflow: hidden;
115
- }
116
- .sv-num:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
117
- .sv-num.is-invalid { border-color: var(--sg-danger, #dc2626); }
118
- .sv-num.is-invalid:focus-within { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
119
- .sv-num.is-disabled { opacity: 0.6; }
120
- .sv-num__input {
121
- flex: 1; min-width: 0; border: 0; background: none; outline: none; color: inherit; font: inherit;
122
- text-align: end; padding: 0 8px;
123
- }
124
- .sv-num--sm { height: 28px; font-size: 12px; }
125
- .sv-num--md { height: 34px; font-size: 13px; }
126
- .sv-num--lg { height: 40px; font-size: 15px; }
127
- .sv-num__clear { display: grid; place-items: center; width: 22px; align-self: center; flex: none; background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; border-radius: 4px; }
163
+ .sv-num__input { text-align: end; }
164
+ .sv-num__clear { display: grid; place-items: center; width: 20px; align-self: center; flex: none; background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; border-radius: 4px; }
128
165
  .sv-num__clear:hover { color: var(--sg-danger, #dc2626); }
129
- .sv-num__spin { display: flex; flex-direction: column; border-inline-start: 1px solid var(--sg-border, #e2e8f0); }
130
- .sv-num__spin :global(.sv-repeat) { flex: 1; padding: 0 6px; border-radius: 0; }
166
+ .sv-num__spin { display: flex; flex-direction: column; margin-inline-start: 2px; }
167
+ .sv-num__spin :global(.sv-repeat) { flex: 1; min-height: 0; padding: 0 5px; border-radius: 4px; line-height: 1; }
131
168
  </style>
@@ -10,7 +10,7 @@
10
10
  * ```
11
11
  */
12
12
  import SvField from './SvField.svelte'
13
- import { nextEditorId, type SvEditorProps } from './editor-contract'
13
+ import { editorAria, nextEditorId, type SvEditorProps } from './editor-contract'
14
14
  import { sanitizeOtp, otpCells, isOtpComplete } from './otp'
15
15
 
16
16
  type Props = Pick<
@@ -118,7 +118,7 @@
118
118
  class:is-disabled={disabled}
119
119
  class:is-invalid={invalid}
120
120
  role="group"
121
- aria-label={label ?? 'One-time code'}
121
+ {...editorAria({ id: uid, invalid, required, error, hint, ariaLabel: label ?? 'One-time code' })}
122
122
  >
123
123
  {#each cells as cell, i (i)}
124
124
  <input
@@ -5,7 +5,11 @@
5
5
  *
6
6
  * The styled renderer over the headless `createPasswordInput` core: the reveal
7
7
  * toggle + strength heuristic come from the core, spread here via prop-getters.
8
+ * The box, size, invalid state and focus ring are owned by SvField's `frame`
9
+ * chrome; the reveal eye lives in the trailing slot and the strength meter sits
10
+ * below the field.
8
11
  */
12
+ import type { Snippet } from 'svelte'
9
13
  import SvField from './SvField.svelte'
10
14
  import { nextEditorId, resolveMessages, type SvEditorProps } from './editor-contract'
11
15
  import { createPasswordInput } from './createPasswordInput.svelte'
@@ -28,6 +32,12 @@
28
32
  autocomplete?: string
29
33
  /** Override the built-in strings (reveal toggle + strength labels). */
30
34
  messages?: Partial<PasswordMessages>
35
+ /** Leading adornment (icon) inside the field. */
36
+ leading?: Snippet
37
+ /** Stretch to the container width. */
38
+ block?: boolean
39
+ /** Control width in px (ignored when `block`). Default 220. */
40
+ width?: number
31
41
  }
32
42
 
33
43
  let {
@@ -48,8 +58,12 @@
48
58
  hint,
49
59
  dir,
50
60
  id,
61
+ loading = false,
51
62
  autocomplete = 'current-password',
52
63
  messages,
64
+ leading,
65
+ block = false,
66
+ width = 220,
53
67
  }: Props = $props()
54
68
 
55
69
  const autoId = nextEditorId('sv-pass')
@@ -76,20 +90,40 @@
76
90
  })
77
91
  </script>
78
92
 
79
- <SvField id={uid} {label} {hint} {error} {required} {dir}>
80
- <div class="sv-pw sv-pw--{size}" class:is-disabled={disabled}>
81
- <div class="sv-pw__field" class:is-invalid={invalid}>
93
+ {#snippet eye()}
94
+ {#if revealable}
95
+ <button class="sv-pw__eye" {...pw.toggleProps()}>
96
+ {#if pw.revealed}
97
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" /><path d="M1 1l22 22" /></svg>
98
+ {:else}
99
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" /><circle cx="12" cy="12" r="3" /></svg>
100
+ {/if}
101
+ </button>
102
+ {/if}
103
+ {/snippet}
104
+
105
+ <div class="sv-pw" class:sv-pw--block={block}>
106
+ <SvField
107
+ frame
108
+ id={uid}
109
+ {label}
110
+ {hint}
111
+ {error}
112
+ {required}
113
+ {dir}
114
+ {size}
115
+ {invalid}
116
+ {disabled}
117
+ {readonly}
118
+ {loading}
119
+ {block}
120
+ {width}
121
+ {leading}
122
+ trailing={revealable ? eye : undefined}
123
+ >
82
124
  <input class="sv-pw__input" {...pw.inputProps()} />
83
- {#if revealable}
84
- <button class="sv-pw__eye" {...pw.toggleProps()}>
85
- {#if pw.revealed}
86
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" /><path d="M1 1l22 22" /></svg>
87
- {:else}
88
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" /><circle cx="12" cy="12" r="3" /></svg>
89
- {/if}
90
- </button>
91
- {/if}
92
- </div>
125
+ {#if name}<input type="hidden" {name} value={value} />{/if}
126
+ </SvField>
93
127
  {#if showStrength}
94
128
  <div class="sv-pw__meter" aria-hidden="true">
95
129
  {#each Array(4) as _, i (i)}
@@ -98,27 +132,13 @@
98
132
  </div>
99
133
  <span class="sv-pw__strength is-lvl{pw.strength}">{pw.strengthLabel}</span>
100
134
  {/if}
101
- {#if name}<input type="hidden" {name} value={value} />{/if}
102
135
  </div>
103
- </SvField>
104
136
 
105
137
  <style>
106
- .sv-pw { --_accent: var(--sg-accent, #2563eb); display: inline-flex; flex-direction: column; gap: 5px; width: 220px; }
107
- .sv-pw.is-disabled { opacity: 0.6; }
108
- .sv-pw__field {
109
- display: flex; align-items: center;
110
- background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
111
- border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
112
- }
113
- .sv-pw__field:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
114
- .sv-pw__field.is-invalid { border-color: var(--sg-danger, #dc2626); }
115
- .sv-pw__field.is-invalid:focus-within { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
116
- .sv-pw__input { flex: 1; min-width: 0; border: 0; background: none; outline: none; color: inherit; font: inherit; padding: 0 10px; }
117
- .sv-pw--sm .sv-pw__field { height: 28px; font-size: 12px; }
118
- .sv-pw--md .sv-pw__field { height: 34px; font-size: 13px; }
119
- .sv-pw--lg .sv-pw__field { height: 40px; font-size: 15px; }
120
- .sv-pw__eye { display: grid; place-items: center; width: 32px; align-self: stretch; background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; }
121
- .sv-pw__eye:hover { color: var(--_accent); }
138
+ .sv-pw { display: inline-flex; flex-direction: column; gap: 5px; }
139
+ .sv-pw--block { display: flex; width: 100%; }
140
+ .sv-pw__eye { display: grid; place-items: center; width: 26px; align-self: center; background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; padding: 0; }
141
+ .sv-pw__eye:hover { color: var(--sg-accent, #2563eb); }
122
142
  .sv-pw__meter { display: flex; gap: 4px; }
123
143
  .sv-pw__bar { flex: 1; height: 4px; border-radius: 2px; background: var(--sg-border, #e2e8f0); transition: background 0.15s; }
124
144
  .sv-pw__bar.on.is-lvl1 { background: var(--sg-danger, #dc2626); }