@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
@@ -4,7 +4,9 @@
4
4
  * the masked display value and the raw (unmasked) value.
5
5
  *
6
6
  * The styled renderer over the headless `createMaskedInput` core; the mask
7
- * formatting + state live in the core, spread here via prop-getters.
7
+ * formatting + state live in the core, spread here via prop-getters. The box,
8
+ * size, invalid state, clear button and adornments are owned by SvField's
9
+ * `frame` chrome.
8
10
  */
9
11
  import type { Snippet } from 'svelte';
10
12
  import { type SvEditorProps } from './editor-contract';
@@ -16,9 +18,22 @@ type Props = SvEditorProps & {
16
18
  placeholder?: string;
17
19
  /** Show a clear (x) button when there is a value. */
18
20
  clearable?: boolean;
19
- /** Leading / trailing icon adornments. */
21
+ /** Leading adornment (icon/button) inside the field. */
22
+ leading?: Snippet;
23
+ /** Trailing adornment (icon/button) inside the field. */
24
+ trailing?: Snippet;
25
+ /** Plain-text affix at the start. */
26
+ prefix?: string;
27
+ /** Plain-text affix at the end. */
28
+ suffix?: string;
29
+ /** @deprecated Use `leading`. */
20
30
  prefixIcon?: Snippet;
31
+ /** @deprecated Use `trailing`. */
21
32
  suffixIcon?: Snippet;
33
+ /** Stretch to the container width. */
34
+ block?: boolean;
35
+ /** Control width in px (ignored when `block`). Default 200. */
36
+ width?: number;
22
37
  };
23
38
  declare const SvMaskedInput: import("svelte").Component<Props, {}, "value">;
24
39
  type SvMaskedInput = ReturnType<typeof SvMaskedInput>;
@@ -15,6 +15,7 @@
15
15
  import { type EditorDir } from './editor-contract'
16
16
  import type { MenuItem } from './menu-item'
17
17
  import { createMenu } from './createMenu.svelte'
18
+ import { computePosition, autoUpdate, type Placement } from './positioning'
18
19
 
19
20
  let {
20
21
  items,
@@ -53,6 +54,32 @@
53
54
  dir: () => dir,
54
55
  focusItem: (i) => queueMicrotask(() => listEl?.querySelector<HTMLElement>(`[data-mi="${i}"]`)?.focus()),
55
56
  })
57
+
58
+ // Position an open submenu flyout with the shared engine: prefer opening to the
59
+ // side away from the reading direction, flip to the opposite side when there is
60
+ // no room, and clamp vertically to stay in view. Kept absolute (relative to the
61
+ // itemwrap) so a nested flyout is never clipped by a scroll container.
62
+ const startSide: Placement = dir === 'rtl' ? 'left-start' : 'right-start'
63
+ function flyout(node: HTMLElement) {
64
+ const wrap = node.parentElement // .sv-menu__itemwrap
65
+ const btn = wrap?.querySelector<HTMLElement>(':scope > .sv-menu__item') ?? null
66
+ const update = () => {
67
+ if (!btn || !wrap) return
68
+ const r = btn.getBoundingClientRect()
69
+ const w = wrap.getBoundingClientRect()
70
+ const f = { width: node.offsetWidth || 200, height: node.offsetHeight || 120 }
71
+ const p = computePosition(
72
+ { x: r.left, y: r.top, width: r.width, height: r.height },
73
+ f,
74
+ { placement: startSide, offset: 2, padding: 8 },
75
+ )
76
+ node.style.left = `${p.x - w.left}px`
77
+ node.style.top = `${p.y - w.top}px`
78
+ }
79
+ if (!btn) { update(); return {} }
80
+ const stop = autoUpdate(btn, node, update)
81
+ return { destroy() { stop() } }
82
+ }
56
83
  </script>
57
84
 
58
85
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
@@ -74,7 +101,7 @@
74
101
  {#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}
75
102
  </button>
76
103
  {#if item.children?.length && menu.isSubOpen(i)}
77
- <div class="sv-menu__flyout">
104
+ <div class="sv-menu__flyout" use:flyout>
78
105
  <Self
79
106
  items={item.children}
80
107
  {onclose}
@@ -109,9 +136,11 @@
109
136
  .sv-menu__shortcut { color: var(--sg-muted, #94a3b8); font-size: 11.5px; margin-inline-start: 12px; }
110
137
  .sv-menu__chev { color: var(--sg-muted, #94a3b8); flex: none; }
111
138
  .sv-menu[dir='rtl'] .sv-menu__chev, .sv-menu__list.is-submenu .sv-menu__chev { transform: scaleX(-1); }
139
+ /* top/left are the pre-JS fallback (open to the right); the `flyout` action
140
+ overrides them with engine-computed offsets (flip + vertical clamp). */
112
141
  .sv-menu__flyout {
113
- position: absolute; top: -5px; inset-inline-start: 100%; z-index: 1;
142
+ position: absolute; top: -5px; left: 100%; z-index: 1;
114
143
  background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0);
115
- border-radius: 10px; box-shadow: 0 16px 40px -12px rgba(15,23,42,0.32); margin-inline-start: 2px;
144
+ border-radius: 10px; box-shadow: 0 16px 40px -12px rgba(15,23,42,0.32);
116
145
  }
117
146
  </style>
@@ -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>
@@ -0,0 +1,18 @@
1
+ export type MenubarMenu = {
2
+ /** Trigger label on the bar. */
3
+ label: string;
4
+ /** The dropdown's items (submenus, separators, icons, shortcuts all work). */
5
+ items: import('./menu-item').MenuItem[];
6
+ disabled?: boolean;
7
+ };
8
+ import { type EditorDir } from './editor-contract';
9
+ import type { MenuItem } from './menu-item';
10
+ type Props = {
11
+ menus: ReadonlyArray<MenubarMenu>;
12
+ onSelect?: (item: MenuItem) => void;
13
+ ariaLabel?: string;
14
+ dir?: EditorDir;
15
+ };
16
+ declare const SvMenubar: import("svelte").Component<Props, {}, "">;
17
+ type SvMenubar = ReturnType<typeof SvMenubar>;
18
+ export default SvMenubar;
@@ -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)}
@@ -9,7 +9,7 @@ export type { MultiSelectOption } from './ui-app.types';
9
9
  */
10
10
  import type { MultiSelectOption } from './ui-app.types';
11
11
  import { type SvEditorProps } from './editor-contract';
12
- type Props = Pick<SvEditorProps, 'disabled' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel'> & {
12
+ type Props = Pick<SvEditorProps, 'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'> & {
13
13
  options: ReadonlyArray<MultiSelectOption>;
14
14
  value?: ReadonlyArray<string | number>;
15
15
  onChange?: (value: Array<string | number>) => void;
@@ -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>
@@ -1,3 +1,16 @@
1
+ /**
2
+ * SvNumberInput - a numeric input with min/max/step, spinner buttons, optional
3
+ * thousands grouping + precision, prefix/suffix. Parity: Smart `smart-number-input`.
4
+ * Emits number | null. The SvGrid number cell editor, standalone too.
5
+ *
6
+ * This is the styled renderer over the headless `createNumberInput` core - the
7
+ * same split as the grid (`createSvGrid` / `<SvGrid>`): parse/format/clamp,
8
+ * spinner + keyboard and ARIA all come from the core via prop-getters. The box,
9
+ * size, invalid state and focus ring are owned by SvField's `frame` chrome; the
10
+ * `prefix`/`suffix` are formatted INTO the value (they travel with the
11
+ * right-aligned number), and the spinner + clear live in the trailing slot.
12
+ */
13
+ import type { Snippet } from 'svelte';
1
14
  import { type SvEditorProps } from './editor-contract';
2
15
  type Props = SvEditorProps & {
3
16
  value?: number | null;
@@ -8,12 +21,24 @@ type Props = SvEditorProps & {
8
21
  precision?: number;
9
22
  /** Group thousands (1,234) on display. */
10
23
  grouping?: boolean;
24
+ /** Text formatted before the number (e.g. `$`); travels with the value. */
11
25
  prefix?: string;
26
+ /** Text formatted after the number (e.g. `%`); travels with the value. */
12
27
  suffix?: string;
13
28
  placeholder?: string;
14
29
  spinButtons?: boolean;
15
30
  /** Show a clear (x) button when there is a value. */
16
31
  clearable?: boolean;
32
+ /** Select the whole value when the field is focused. */
33
+ selectOnFocus?: boolean;
34
+ /** Step the value with the mouse wheel while the field is focused. */
35
+ wheelStep?: boolean;
36
+ /** Leading adornment (icon) inside the field. */
37
+ leading?: Snippet;
38
+ /** Stretch to the container width. */
39
+ block?: boolean;
40
+ /** Control width in px (ignored when `block`). Default 150. */
41
+ width?: number;
17
42
  };
18
43
  declare const SvNumberInput: import("svelte").Component<Props, {}, "value">;
19
44
  type SvNumberInput = ReturnType<typeof SvNumberInput>;
@@ -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