@svgrid/grid 2.2.16 → 2.2.17

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,23 @@
1
+ /**
2
+ * SvList - a themed ordered / unordered / unstyled list. Pass `items` for the
3
+ * simple case, or a `children` snippet for full control over each `<li>`.
4
+ *
5
+ * <SvList items={['One', 'Two', 'Three']} />
6
+ * <SvList type="ordered" spacing="lg" items={steps} />
7
+ */
8
+ import type { Snippet } from 'svelte';
9
+ type Props = {
10
+ /** Marker style. `none` drops markers + indentation. */
11
+ type?: 'unordered' | 'ordered' | 'none';
12
+ /** Simple string items (skip the snippet). */
13
+ items?: string[];
14
+ /** Vertical gap between items. */
15
+ spacing?: 'sm' | 'md' | 'lg';
16
+ /** Font size step. */
17
+ size?: 'sm' | 'md' | 'lg';
18
+ /** Custom list content (`<li>` elements). Overrides `items`. */
19
+ children?: Snippet;
20
+ };
21
+ declare const SvList: import("svelte").Component<Props, {}, "">;
22
+ type SvList = ReturnType<typeof SvList>;
23
+ export default SvList;
@@ -3,11 +3,20 @@
3
3
  * SvListBox - an inline single/multi-select list (WAI-ARIA listbox) with
4
4
  * roving highlight + full keyboard. Parity: Smart `smart-list-box`. Controlled
5
5
  * via `value` (scalar or array) + `onChange`. Scales to huge option sets with
6
- * `virtual` (fixed-row windowing), and each row can be an `itemTemplate`.
6
+ * `virtual` - windowing over the shared `createSvelteVirtualizer` (slot-keyed
7
+ * DOM recycling), which also windows GROUPED lists and mixes row heights.
8
+ *
9
+ * Virtual mode uses a JS-DRIVEN scroller (like Smart): the option list does
10
+ * NOT use native overflow scroll - a single `scrollOffset` state positions the
11
+ * rows (`translateY`) AND drives the virtualizer window in the same reactive
12
+ * flush. Native scroll is composited off the main thread, so it paints the
13
+ * empty scrolled position a frame before JS can re-window (the "blank on a
14
+ * fast thumb-drag" flash); driving the offset in JS removes that async gap.
7
15
  */
8
- import { type Snippet } from 'svelte'
9
- import { groupOptions, hasGroups, type ListOption } from './list-option'
10
- import { virtualRange, scrollToIndex } from './virtual'
16
+ import { untrack, type Snippet } from 'svelte'
17
+ import { groupOptions, normalizeOptions, flattenForVirtual, type ListOption, type IndexedOption, type RowHeight } from './list-option'
18
+ import { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.svelte'
19
+ import { createJsScroller } from './js-scroller.svelte'
11
20
  import { createListbox } from './createListbox.svelte'
12
21
  import SvField from './SvField.svelte'
13
22
  import { nextEditorId, type SvEditorProps } from './editor-contract'
@@ -21,9 +30,14 @@
21
30
  rows?: number
22
31
  /** Window the list (render only visible rows) for large option sets. */
23
32
  virtual?: boolean
24
- /** Fixed row height in px (must match the CSS row height). Default 32. */
25
- rowHeight?: number
33
+ /** Row height in px - a number, or a per-option function for variable
34
+ * heights. Must match the rendered row height. Default 32. */
35
+ rowHeight?: RowHeight
36
+ /** Height (px) of a group heading row when virtualized + grouped. Default 28. */
37
+ groupHeaderHeight?: number
26
38
  /** Custom per-option content. Receives the option. */
39
+ item?: Snippet<[ListOption]>
40
+ /** @deprecated Use `item`. */
27
41
  itemTemplate?: Snippet<[ListOption]>
28
42
  }
29
43
 
@@ -33,9 +47,12 @@
33
47
  onChange,
34
48
  multiple = false,
35
49
  disabled = false,
50
+ size = 'md',
36
51
  rows = 7,
37
52
  virtual = false,
38
53
  rowHeight = 32,
54
+ groupHeaderHeight = 28,
55
+ item,
39
56
  itemTemplate,
40
57
  ariaLabel,
41
58
  name,
@@ -52,9 +69,13 @@
52
69
  const autoId = nextEditorId('sv-lb')
53
70
  const uid = $derived(id ?? autoId)
54
71
 
72
+ // Tolerate primitive / partial options (e.g. setOptions([1, 2, 3])): normalize
73
+ // to { value, label } so labels render and keys are never undefined.
74
+ const norm = $derived(normalizeOptions(options))
75
+
55
76
  // The styled listbox is just a renderer over the headless core.
56
77
  const lb = createListbox({
57
- options: () => options,
78
+ options: () => norm,
58
79
  value: () => value,
59
80
  onChange: (v) => onChange?.(v),
60
81
  multiple: () => multiple,
@@ -68,26 +89,63 @@
68
89
  })
69
90
  const isSel = (o: ListOption) => lb.isSelected(o.value)
70
91
 
71
- // Virtualization only applies to a flat (ungrouped) list.
72
- const useVirtual = $derived(virtual && !hasGroups(options))
73
- let listEl = $state<HTMLUListElement | null>(null)
74
- let scrollTop = $state(0)
92
+ const rowHeightBase = $derived(typeof rowHeight === 'number' ? rowHeight : 32)
93
+ const optHeight = (o: IndexedOption) => (typeof rowHeight === 'function' ? rowHeight(o, o.index) : rowHeight)
94
+
95
+ // --- Virtualization ---------------------------------------------------------
96
+ const useVirtual = $derived(virtual)
97
+ let listEl = $state<HTMLDivElement | null>(null)
75
98
  let viewportH = $state(0)
76
- const vr = $derived(
77
- virtualRange({ scrollTop, viewportHeight: viewportH, rowHeight, count: options.length, overscan: 10 }),
78
- )
79
- const windowed = $derived(
80
- useVirtual ? options.slice(vr.start, vr.end).map((o, i) => ({ ...o, index: vr.start + i })) : [],
99
+
100
+ const flat = $derived(flattenForVirtual(norm, { rowHeight, groupHeaderHeight }))
101
+ const uniform = $derived(!flat.hasGroups && typeof rowHeight === 'number')
102
+ const estimateSize = $derived(uniform ? rowHeightBase : (i: number) => flat.sizeAt(i))
103
+
104
+ const OVERSCAN = 6
105
+ // Effective viewport height: measured, or the computed `rows` height until the
106
+ // viewport element is measured. Never 0.
107
+ const effViewport = $derived(viewportH > 0 ? viewportH : Math.max(1, rows * rowHeightBase))
108
+
109
+ const virtualizer = createSvelteVirtualizer(
110
+ untrack(() => ({ count: flat.entries.length, estimateSize, overscan: OVERSCAN, viewportHeight: effViewport })),
81
111
  )
82
112
 
83
- // Keep the active option in view. Virtual mode drives scrollTop directly (the
84
- // active row may not be in the DOM); otherwise use scrollIntoView.
113
+ $effect(() => {
114
+ const count = flat.entries.length
115
+ const es = estimateSize
116
+ untrack(() => virtualizer.setOptions({ count, estimateSize: es }))
117
+ })
118
+ $effect(() => {
119
+ const vh = effViewport
120
+ untrack(() => virtualizer.setViewportHeight(vh))
121
+ })
122
+
123
+ const vItems = $derived.by(() => {
124
+ virtualizer.version
125
+ return virtualizer.getVirtualItems()
126
+ })
127
+ const totalSize = $derived.by(() => {
128
+ virtualizer.version
129
+ return virtualizer.getTotalSize()
130
+ })
131
+
132
+ // JS-driven scroll: one offset positions the rows AND drives the virtualizer
133
+ // window in the same flush - no native scroll, so no async "blank flash".
134
+ const scroller = createJsScroller({
135
+ virtualizer,
136
+ totalSize: () => totalSize,
137
+ viewport: () => effViewport,
138
+ lineStep: () => rowHeightBase,
139
+ })
140
+
141
+ // Keep the active option in view - via the scroller (virtual) or scrollIntoView
142
+ // (native non-virtual). Driven by roving keyboard nav.
85
143
  $effect(() => {
86
144
  const i = lb.activeIndex
87
145
  if (useVirtual) {
88
- if (!listEl) return
89
- const next = scrollToIndex(i, listEl.scrollTop, listEl.clientHeight, rowHeight)
90
- if (next !== listEl.scrollTop) listEl.scrollTop = next
146
+ const flatIdx = flat.optionFlatIndex[i]
147
+ if (flatIdx == null) return
148
+ untrack(() => scroller.ensureIndexVisible(flatIdx))
91
149
  } else {
92
150
  queueMicrotask(() => listEl?.querySelector<HTMLElement>(`[data-idx="${i}"]`)?.scrollIntoView({ block: 'nearest' }))
93
151
  }
@@ -95,103 +153,134 @@
95
153
  </script>
96
154
 
97
155
  <SvField id={uid} {label} {hint} {error} {required} {dir}>
98
- <ul
156
+ <!-- Div-based listbox (role comes from `lb.rootProps()`). -->
157
+ <div
99
158
  bind:this={listEl}
100
- class="sv-listbox"
159
+ class="sv-listbox sv-listbox--{size}"
101
160
  class:is-invalid={invalid}
102
161
  class:is-virtual={useVirtual}
103
162
  style:--sv-rows={rows}
104
- style:--sv-row-h={`${rowHeight}px`}
105
- onscroll={(e) => (scrollTop = e.currentTarget.scrollTop)}
106
- bind:clientHeight={viewportH}
163
+ style:--sv-row-h={`${rowHeightBase}px`}
164
+ onwheel={useVirtual ? scroller.onWheel : undefined}
107
165
  {...lb.rootProps()}
108
166
  >
109
167
  {#if useVirtual}
110
- <!-- Windowing by top/bottom spacers with the visible rows IN FLOW between
111
- them (padTop + rows + padBottom always sum to the full height). Plain
112
- flow rows (no absolute positioning, no will-change) keep the scroller
113
- off its own compositor layer, so a fast jump repaints in step with the
114
- scroll instead of showing the blank frame a promoted layer would. The
115
- window updates from `scrollTop` state, which Svelte flushes before the
116
- next paint - no flushSync (it over-flushes app-wide and, on a fling,
117
- starves the very handler that has to keep up with the scroll). -->
118
- <li class="sv-listbox__spacer" aria-hidden="true" style:height={`${vr.padTop}px`}></li>
119
- {#each windowed as opt (opt.value)}
120
- {@render optionLi(opt, opt.index)}
121
- {/each}
122
- <li class="sv-listbox__spacer" aria-hidden="true" style:height={`${vr.padBottom}px`}></li>
168
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
169
+ <div
170
+ class="sv-listbox__vp"
171
+ bind:clientHeight={viewportH}
172
+ onpointerdown={scroller.onContentDown}
173
+ onpointermove={scroller.onContentMove}
174
+ onpointerup={scroller.onContentUp}
175
+ onpointercancel={scroller.onContentUp}
176
+ >
177
+ {#each vItems as vi (vi.key)}
178
+ {@const entry = flat.entries[vi.index]}
179
+ {#if entry?.type === 'group'}
180
+ <div class="sv-listbox__group" role="presentation" style:height={`${vi.size}px`} style:transform={`translateY(${vi.start - scroller.scrollOffset}px)`}>{entry.label}</div>
181
+ {:else if entry}
182
+ {@render optionRow(entry.opt, entry.opt.index, vi.size, vi.start - scroller.scrollOffset)}
183
+ {/if}
184
+ {/each}
185
+ </div>
186
+ {#if scroller.maxOffset > 0}
187
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
188
+ <div class="sv-listbox__sb" onpointerdown={scroller.onTrackDown} aria-hidden="true">
189
+ <div
190
+ class="sv-listbox__thumb"
191
+ class:is-dragging={scroller.dragging}
192
+ style:height={`${scroller.thumbH}px`}
193
+ style:transform={`translateY(${scroller.thumbTop}px)`}
194
+ onpointerdown={scroller.onThumbDown}
195
+ onpointermove={scroller.onThumbMove}
196
+ onpointerup={scroller.onThumbUp}
197
+ onpointercancel={scroller.onThumbUp}
198
+ ></div>
199
+ </div>
200
+ {/if}
123
201
  {:else}
124
- {#each groupOptions(options) as g (g.group ?? ' ')}
125
- {#if g.group != null}<li class="sv-listbox__group" role="presentation">{g.group}</li>{/if}
126
- {#each g.options as opt (opt.value)}
127
- {@render optionLi(opt, opt.index)}
202
+ {#each groupOptions(norm) as g (g.group ?? ' ')}
203
+ {#if g.group != null}<div class="sv-listbox__group" role="presentation">{g.group}</div>{/if}
204
+ {#each g.options as opt (opt.index)}
205
+ {@render optionRow(opt, opt.index)}
128
206
  {/each}
129
207
  {/each}
130
208
  {/if}
131
- </ul>
209
+ </div>
132
210
  {#if name}{#each lb.selectedValues as v (v)}<input type="hidden" {name} value={v} />{/each}{/if}
133
211
  </SvField>
134
212
 
135
- {#snippet optionLi(opt, index)}
213
+ {#snippet optionInner(opt: ListOption)}
214
+ {#if multiple}<span class="sv-listbox__check" aria-hidden="true">{isSel(opt) ? '✓' : ''}</span>{/if}
215
+ {#if item ?? itemTemplate}{@render (item ?? itemTemplate)!(opt)}{:else}<span class="sv-listbox__label">{opt.label}</span>{/if}
216
+ {/snippet}
217
+
218
+ {#snippet optionRow(opt: IndexedOption, index: number, h?: number, top?: number)}
136
219
  <!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
137
- <li
220
+ <div
138
221
  class="sv-listbox__opt"
139
222
  class:is-selected={isSel(opt)}
140
223
  class:is-active={lb.isActive(index)}
141
224
  class:is-disabled={opt.disabled}
142
- style:height={`${rowHeight}px`}
225
+ style:height={`${h ?? optHeight(opt)}px`}
226
+ style:transform={top == null ? undefined : `translateY(${top}px)`}
143
227
  {...lb.optionProps(index)}
144
228
  >
145
- {#if multiple}<span class="sv-listbox__check" aria-hidden="true">{isSel(opt) ? '✓' : ''}</span>{/if}
146
- {#if itemTemplate}{@render itemTemplate(opt)}{:else}<span class="sv-listbox__label">{opt.label}</span>{/if}
147
- </li>
229
+ {@render optionInner(opt)}
230
+ </div>
148
231
  {/snippet}
149
232
 
150
233
  <style>
151
234
  .sv-listbox {
152
235
  --_accent: var(--sg-accent, #2563eb);
153
- margin: 0; padding: 4px; list-style: none;
154
- max-height: calc(var(--sv-rows, 7) * var(--sv-row-h, 32px) + 8px); overflow-y: auto;
236
+ margin: 0; list-style: none;
155
237
  background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
156
238
  border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
157
- width: 220px; outline: none;
239
+ width: 220px; outline: none; --_fs: 13px;
240
+ }
241
+ /* Native scroll for the (fully rendered) non-virtual list. */
242
+ .sv-listbox:not(.is-virtual) {
243
+ padding: 4px; overflow-y: auto;
244
+ max-height: calc(var(--sv-rows, 7) * var(--sv-row-h, 32px) + 8px);
245
+ }
246
+ /* JS-driven scroller for the virtual list: fixed height, no native scroll -
247
+ the rows are absolutely positioned and moved by `scrollOffset`. */
248
+ .sv-listbox.is-virtual {
249
+ padding: 0; overflow: hidden; position: relative;
250
+ height: calc(var(--sv-rows, 7) * var(--sv-row-h, 32px) + 8px);
158
251
  }
252
+ .sv-listbox--sm { --_fs: 12px; }
253
+ .sv-listbox--lg { --_fs: 15px; }
159
254
  .sv-listbox:focus-visible { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
160
255
  .sv-listbox.is-invalid { border-color: var(--sg-danger, #dc2626); }
161
256
  .sv-listbox.is-invalid:focus-visible { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
162
- /* Spacers reserve the off-screen height above/below the windowed rows. They
163
- also paint faint per-row skeleton bars: on a fast scrollbar-thumb drag the
164
- compositor can reveal this padding before JS re-windows (a drag jumps many
165
- rows in one frame - overscan can't cover it), so the user would see blank.
166
- The skeleton fills that gap with placeholder rows that the real rows paint
167
- over a frame later. Bars are one per `--sv-row-h` band, aligned to the row
168
- grid (padTop/padBottom are exact multiples of the row height). */
169
- .sv-listbox__spacer {
170
- padding: 0; margin: 0; flex: none; pointer-events: none; list-style: none;
171
- background-image: linear-gradient(
172
- to bottom,
173
- transparent 0,
174
- transparent calc((var(--sv-row-h, 32px) - 10px) / 2),
175
- var(--sg-skeleton, color-mix(in srgb, currentColor 9%, transparent)) calc((var(--sv-row-h, 32px) - 10px) / 2),
176
- var(--sg-skeleton, color-mix(in srgb, currentColor 9%, transparent)) calc((var(--sv-row-h, 32px) + 10px) / 2),
177
- transparent calc((var(--sv-row-h, 32px) + 10px) / 2),
178
- transparent var(--sv-row-h, 32px)
179
- );
180
- background-size: 62% var(--sv-row-h, 32px);
181
- background-repeat: repeat-y;
182
- background-position: 12px 0;
257
+
258
+ /* The clipped viewport that holds the translated rows. */
259
+ .sv-listbox__vp { position: absolute; inset: 4px; overflow: hidden; touch-action: none; }
260
+ .sv-listbox.is-virtual .sv-listbox__opt,
261
+ .sv-listbox.is-virtual .sv-listbox__group { position: absolute; left: 0; right: 0; top: 0; }
262
+
263
+ /* Custom scrollbar (mouse affordance; keyboard nav handles a11y scrolling). */
264
+ .sv-listbox__sb { position: absolute; top: 4px; right: 2px; bottom: 4px; width: 10px; z-index: 2; }
265
+ .sv-listbox__thumb {
266
+ position: absolute; left: 2px; right: 2px; top: 0; min-height: 24px;
267
+ border-radius: 4px; background: color-mix(in srgb, currentColor 26%, transparent);
268
+ cursor: default; touch-action: none;
183
269
  }
270
+ .sv-listbox__thumb:hover, .sv-listbox__thumb.is-dragging { background: color-mix(in srgb, currentColor 42%, transparent); }
271
+
184
272
  .sv-listbox__opt {
185
273
  display: flex; align-items: center; gap: 8px; box-sizing: border-box; flex: none; padding: 0 10px;
186
- border-radius: 6px; cursor: pointer; font-size: 13px;
274
+ border-radius: 6px; cursor: pointer; font-size: var(--_fs, 13px);
187
275
  }
188
276
  .sv-listbox__opt.is-active { background: var(--sg-row-hover-bg, #f1f5f9); }
189
277
  .sv-listbox__opt.is-selected { background: color-mix(in srgb, var(--_accent) 14%, transparent); color: var(--_accent); font-weight: 600; }
190
278
  .sv-listbox__opt.is-selected.is-active { background: color-mix(in srgb, var(--_accent) 22%, transparent); }
191
279
  .sv-listbox__opt.is-disabled { opacity: 0.4; cursor: not-allowed; }
192
280
  .sv-listbox__group {
193
- padding: 8px 10px 3px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
194
- color: var(--sg-muted, #94a3b8);
281
+ box-sizing: border-box; padding: 8px 10px 3px; font-size: 10.5px; font-weight: 700;
282
+ text-transform: uppercase; letter-spacing: 0.04em; color: var(--sg-muted, #94a3b8);
283
+ display: flex; align-items: flex-end;
195
284
  }
196
285
  .sv-listbox__check { width: 14px; text-align: center; color: var(--_accent); }
197
286
  .sv-listbox__label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -2,10 +2,18 @@
2
2
  * SvListBox - an inline single/multi-select list (WAI-ARIA listbox) with
3
3
  * roving highlight + full keyboard. Parity: Smart `smart-list-box`. Controlled
4
4
  * via `value` (scalar or array) + `onChange`. Scales to huge option sets with
5
- * `virtual` (fixed-row windowing), and each row can be an `itemTemplate`.
5
+ * `virtual` - windowing over the shared `createSvelteVirtualizer` (slot-keyed
6
+ * DOM recycling), which also windows GROUPED lists and mixes row heights.
7
+ *
8
+ * Virtual mode uses a JS-DRIVEN scroller (like Smart): the option list does
9
+ * NOT use native overflow scroll - a single `scrollOffset` state positions the
10
+ * rows (`translateY`) AND drives the virtualizer window in the same reactive
11
+ * flush. Native scroll is composited off the main thread, so it paints the
12
+ * empty scrolled position a frame before JS can re-window (the "blank on a
13
+ * fast thumb-drag" flash); driving the offset in JS removes that async gap.
6
14
  */
7
15
  import { type Snippet } from 'svelte';
8
- import { type ListOption } from './list-option';
16
+ import { type ListOption, type RowHeight } from './list-option';
9
17
  import { type SvEditorProps } from './editor-contract';
10
18
  type Props = SvEditorProps & {
11
19
  options: ReadonlyArray<ListOption>;
@@ -16,9 +24,14 @@ type Props = SvEditorProps & {
16
24
  rows?: number;
17
25
  /** Window the list (render only visible rows) for large option sets. */
18
26
  virtual?: boolean;
19
- /** Fixed row height in px (must match the CSS row height). Default 32. */
20
- rowHeight?: number;
27
+ /** Row height in px - a number, or a per-option function for variable
28
+ * heights. Must match the rendered row height. Default 32. */
29
+ rowHeight?: RowHeight;
30
+ /** Height (px) of a group heading row when virtualized + grouped. Default 28. */
31
+ groupHeaderHeight?: number;
21
32
  /** Custom per-option content. Receives the option. */
33
+ item?: Snippet<[ListOption]>;
34
+ /** @deprecated Use `item`. */
22
35
  itemTemplate?: Snippet<[ListOption]>;
23
36
  };
24
37
  declare const SvListBox: import("svelte").Component<Props, {}, "">;
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvLoadingOverlay - covers its container with a translucent scrim + spinner
4
+ * while `visible`, blocking interaction underneath. Wrap any positioned region
5
+ * (a card, a panel, a form) and toggle it during an async load.
6
+ *
7
+ * ```svelte
8
+ * <div style="position:relative">
9
+ * <SvLoadingOverlay visible={loading} label="Saving" />
10
+ * ...content...
11
+ * </div>
12
+ * ```
13
+ */
14
+ import type { Snippet } from 'svelte'
15
+ import SvSpinner from './SvSpinner.svelte'
16
+
17
+ type Props = {
18
+ /** Show the overlay. */
19
+ visible?: boolean
20
+ /** Text shown under the spinner (also the a11y label). */
21
+ label?: string
22
+ /** Spinner size. Default `lg`. */
23
+ spinnerSize?: 'sm' | 'md' | 'lg' | number
24
+ /** Blur the content behind the scrim. */
25
+ blur?: boolean
26
+ /** Custom overlay content instead of the default spinner + label. */
27
+ children?: Snippet
28
+ }
29
+
30
+ let { visible = false, label, spinnerSize = 'lg', blur = false, children }: Props = $props()
31
+ </script>
32
+
33
+ {#if visible}
34
+ <div class="sv-loading" class:is-blur={blur} role="status" aria-label={label ?? 'Loading'} aria-live="polite">
35
+ {#if children}
36
+ {@render children()}
37
+ {:else}
38
+ <SvSpinner size={spinnerSize} />
39
+ {#if label}<span class="sv-loading__label">{label}</span>{/if}
40
+ {/if}
41
+ </div>
42
+ {/if}
43
+
44
+ <style>
45
+ .sv-loading {
46
+ position: absolute; inset: 0; z-index: 20;
47
+ display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;
48
+ background: color-mix(in srgb, var(--sg-bg, #fff) 68%, transparent);
49
+ border-radius: inherit; cursor: progress;
50
+ }
51
+ .sv-loading.is-blur { backdrop-filter: blur(2px); }
52
+ .sv-loading__label { font-size: 13px; font-weight: 550; color: var(--sg-muted, #64748b); }
53
+ </style>
@@ -0,0 +1,28 @@
1
+ /**
2
+ * SvLoadingOverlay - covers its container with a translucent scrim + spinner
3
+ * while `visible`, blocking interaction underneath. Wrap any positioned region
4
+ * (a card, a panel, a form) and toggle it during an async load.
5
+ *
6
+ * ```svelte
7
+ * <div style="position:relative">
8
+ * <SvLoadingOverlay visible={loading} label="Saving" />
9
+ * ...content...
10
+ * </div>
11
+ * ```
12
+ */
13
+ import type { Snippet } from 'svelte';
14
+ type Props = {
15
+ /** Show the overlay. */
16
+ visible?: boolean;
17
+ /** Text shown under the spinner (also the a11y label). */
18
+ label?: string;
19
+ /** Spinner size. Default `lg`. */
20
+ spinnerSize?: 'sm' | 'md' | 'lg' | number;
21
+ /** Blur the content behind the scrim. */
22
+ blur?: boolean;
23
+ /** Custom overlay content instead of the default spinner + label. */
24
+ children?: Snippet;
25
+ };
26
+ declare const SvLoadingOverlay: import("svelte").Component<Props, {}, "">;
27
+ type SvLoadingOverlay = ReturnType<typeof SvLoadingOverlay>;
28
+ export default SvLoadingOverlay;
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvMark - highlighted inline text (a themed `<mark>`), for search-hit
4
+ * highlighting and call-outs. Colour by `tone`.
5
+ *
6
+ * <SvMark>matched</SvMark> · <SvMark tone="success">passed</SvMark>
7
+ */
8
+ import type { Snippet } from 'svelte'
9
+
10
+ type Props = {
11
+ tone?: 'yellow' | 'accent' | 'success' | 'warning' | 'error'
12
+ children?: Snippet
13
+ }
14
+
15
+ let { tone = 'yellow', children }: Props = $props()
16
+
17
+ // Each tone is a soft tint + a readable foreground.
18
+ const bg = {
19
+ yellow: '#fef08a',
20
+ accent: 'color-mix(in srgb, var(--sg-accent, #2563eb) 22%, transparent)',
21
+ success: 'color-mix(in srgb, var(--sg-success, #16a34a) 22%, transparent)',
22
+ warning: 'color-mix(in srgb, var(--sg-warning, #d97706) 24%, transparent)',
23
+ error: 'color-mix(in srgb, var(--sg-danger, #dc2626) 20%, transparent)',
24
+ } as const
25
+ </script>
26
+
27
+ <mark class="sv-mark" style:background={bg[tone]}>{@render children?.()}</mark>
28
+
29
+ <style>
30
+ .sv-mark {
31
+ color: inherit;
32
+ padding: 0 2px;
33
+ border-radius: 3px;
34
+ font-family: inherit;
35
+ }
36
+ </style>
@@ -0,0 +1,14 @@
1
+ /**
2
+ * SvMark - highlighted inline text (a themed `<mark>`), for search-hit
3
+ * highlighting and call-outs. Colour by `tone`.
4
+ *
5
+ * <SvMark>matched</SvMark> · <SvMark tone="success">passed</SvMark>
6
+ */
7
+ import type { Snippet } from 'svelte';
8
+ type Props = {
9
+ tone?: 'yellow' | 'accent' | 'success' | 'warning' | 'error';
10
+ children?: Snippet;
11
+ };
12
+ declare const SvMark: import("svelte").Component<Props, {}, "">;
13
+ type SvMark = ReturnType<typeof SvMark>;
14
+ export default SvMark;
@@ -5,7 +5,9 @@
5
5
  * the masked display value and the raw (unmasked) value.
6
6
  *
7
7
  * The styled renderer over the headless `createMaskedInput` core; the mask
8
- * formatting + state live in the core, spread here via prop-getters.
8
+ * formatting + state live in the core, spread here via prop-getters. The box,
9
+ * size, invalid state, clear button and adornments are owned by SvField's
10
+ * `frame` chrome.
9
11
  */
10
12
  import type { Snippet } from 'svelte'
11
13
  import { unmask } from './datetime/mask'
@@ -21,9 +23,22 @@
21
23
  placeholder?: string
22
24
  /** Show a clear (x) button when there is a value. */
23
25
  clearable?: boolean
24
- /** Leading / trailing icon adornments. */
26
+ /** Leading adornment (icon/button) inside the field. */
27
+ leading?: Snippet
28
+ /** Trailing adornment (icon/button) inside the field. */
29
+ trailing?: Snippet
30
+ /** Plain-text affix at the start. */
31
+ prefix?: string
32
+ /** Plain-text affix at the end. */
33
+ suffix?: string
34
+ /** @deprecated Use `leading`. */
25
35
  prefixIcon?: Snippet
36
+ /** @deprecated Use `trailing`. */
26
37
  suffixIcon?: Snippet
38
+ /** Stretch to the container width. */
39
+ block?: boolean
40
+ /** Control width in px (ignored when `block`). Default 200. */
41
+ width?: number
27
42
  }
28
43
 
29
44
  let {
@@ -43,9 +58,16 @@
43
58
  hint,
44
59
  dir,
45
60
  id,
61
+ loading = false,
46
62
  clearable = false,
63
+ leading,
64
+ trailing,
65
+ prefix,
66
+ suffix,
47
67
  prefixIcon,
48
68
  suffixIcon,
69
+ block = false,
70
+ width = 200,
49
71
  }: Props = $props()
50
72
 
51
73
  const autoId = nextEditorId('sv-mask')
@@ -68,40 +90,33 @@
68
90
  })
69
91
  </script>
70
92
 
71
- <SvField id={uid} {label} {hint} {error} {required} {dir}>
72
- <div class="sv-masked sv-masked--{size}" class:is-invalid={invalid} class:is-disabled={disabled}>
73
- {#if prefixIcon}<span class="sv-masked__adorn">{@render prefixIcon()}</span>{/if}
74
- <input class="sv-masked__input" {...mi.inputProps()} />
75
- {#if clearable && mi.masked && !disabled && !readonly}
76
- <button type="button" class="sv-masked__clear" aria-label="Clear" tabindex="-1" onclick={() => { value = ''; onChange?.('', '', false) }}>
77
- <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>
78
- </button>
79
- {/if}
80
- {#if suffixIcon}<span class="sv-masked__adorn">{@render suffixIcon()}</span>{/if}
81
- {#if name}<input type="hidden" {name} value={mask ? unmask(mi.masked, mask) : mi.masked} />{/if}
82
- </div>
93
+ <SvField
94
+ frame
95
+ id={uid}
96
+ {label}
97
+ {hint}
98
+ {error}
99
+ {required}
100
+ {dir}
101
+ {size}
102
+ {invalid}
103
+ {disabled}
104
+ {readonly}
105
+ {loading}
106
+ {block}
107
+ {width}
108
+ leading={leading ?? prefixIcon}
109
+ trailing={trailing ?? suffixIcon}
110
+ {prefix}
111
+ {suffix}
112
+ clearable={clearable}
113
+ showClear={!!mi.masked}
114
+ onclear={() => { value = ''; onChange?.('', '', false) }}
115
+ >
116
+ <input class="sv-masked__input" {...mi.inputProps()} />
117
+ {#if name}<input type="hidden" {name} value={mask ? unmask(mi.masked, mask) : mi.masked} />{/if}
83
118
  </SvField>
84
119
 
85
120
  <style>
86
- .sv-masked {
87
- --_accent: var(--sg-accent, #2563eb);
88
- box-sizing: border-box; width: 200px; display: inline-flex; align-items: center; gap: 6px;
89
- background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
90
- border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
91
- font: inherit; padding: 0 8px 0 10px;
92
- }
93
- .sv-masked--sm { height: 28px; font-size: 12px; }
94
- .sv-masked--md { height: 34px; font-size: 13px; }
95
- .sv-masked--lg { height: 40px; font-size: 15px; }
96
- .sv-masked:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
97
- .sv-masked.is-invalid { border-color: var(--sg-danger, #dc2626); }
98
- .sv-masked.is-invalid:focus-within { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
99
- .sv-masked.is-disabled { opacity: 0.6; }
100
- .sv-masked__input {
101
- flex: 1; min-width: 0; border: 0; background: none; outline: none; color: inherit; font: inherit;
102
- font-variant-numeric: tabular-nums; letter-spacing: 0.02em;
103
- }
104
- .sv-masked__adorn { display: inline-flex; flex: none; color: var(--sg-muted, #64748b); }
105
- .sv-masked__clear { display: grid; place-items: center; width: 20px; flex: none; background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; border-radius: 4px; }
106
- .sv-masked__clear:hover { color: var(--sg-danger, #dc2626); }
121
+ .sv-masked__input { font-variant-numeric: tabular-nums; letter-spacing: 0.02em; }
107
122
  </style>