@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
@@ -5,13 +5,13 @@
5
5
  export declare function resolveDatePattern(pattern: string | undefined, kind: 'date' | 'datetime'): Intl.DateTimeFormatOptions | undefined;
6
6
  type NumericFormatInput = {
7
7
  type: 'number' | 'currency' | 'percent';
8
- locales?: string | string[];
8
+ locales?: string | readonly string[];
9
9
  currency?: string;
10
10
  /** When type is percent: treat value as 0–100 instead of Intl’s 0–1 fraction */
11
11
  valueIsPercentPoints?: boolean;
12
12
  options?: Intl.NumberFormatOptions;
13
13
  };
14
- export declare function getDateFormatter(locales: string | string[] | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
14
+ export declare function getDateFormatter(locales: string | readonly string[] | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
15
15
  /** Format a numeric value for number / currency / percent column formats. */
16
16
  export declare function formatNumericWithConfig(value: unknown, config: NumericFormatInput): string;
17
17
  export {};
@@ -172,7 +172,7 @@ export function createCellRender(ctx) {
172
172
  formatConfig.type === "percent") {
173
173
  return formatNumericWithConfig(value, {
174
174
  type: formatConfig.type,
175
- locales: formatConfig.locales,
175
+ locales: (formatConfig.locales ?? ctx.props.localization?.locale),
176
176
  currency: formatConfig.type === "currency"
177
177
  ? (formatConfig.currency ?? "USD")
178
178
  : undefined,
@@ -197,7 +197,7 @@ export function createCellRender(ctx) {
197
197
  hour: "2-digit",
198
198
  minute: "2-digit",
199
199
  };
200
- return getDateFormatter(formatConfig.locales, merged).format(parsedDate);
200
+ return getDateFormatter((formatConfig.locales ?? ctx.props.localization?.locale), merged).format(parsedDate);
201
201
  }
202
202
  }
203
203
  return String(value ?? "");
@@ -226,7 +226,7 @@ export function createCellRender(ctx) {
226
226
  formatConfig.type === "percent") {
227
227
  return formatNumericWithConfig(value, {
228
228
  type: formatConfig.type,
229
- locales: formatConfig.locales,
229
+ locales: (formatConfig.locales ?? ctx.props.localization?.locale),
230
230
  currency: formatConfig.type === "currency"
231
231
  ? (formatConfig.currency ?? "USD")
232
232
  : undefined,
@@ -245,7 +245,7 @@ export function createCellRender(ctx) {
245
245
  : formatConfig.type === "date"
246
246
  ? { year: "numeric", month: "2-digit", day: "2-digit" }
247
247
  : { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" };
248
- return getDateFormatter(formatConfig.locales, merged).format(parsedDate);
248
+ return getDateFormatter((formatConfig.locales ?? ctx.props.localization?.locale), merged).format(parsedDate);
249
249
  }
250
250
  }
251
251
  return String(value ?? "");
@@ -0,0 +1,25 @@
1
+ /**
2
+ * chart-view registry - the injection seam for the *chart view of the grid*.
3
+ * Setting the `chart` prop on `<SvGrid>` swaps the table for a chart driven by
4
+ * the grid's filtered + sorted rows. Mirrors the `board-view` / `scheduler-view`
5
+ * seam, with one difference: the chart renderer is NOT a paid feature. The chart
6
+ * primitive (`SvChart` / `SvGridChart`) already ships in the free `@svgrid/grid`,
7
+ * so the grid lazy-loads a built-in default renderer (`SvGridChartView`) out of
8
+ * the box. This seam exists only so a host (or enterprise) can *override* that
9
+ * default with a richer renderer - it is not required for the view to work.
10
+ *
11
+ * ```ts
12
+ * // optional: replace the built-in chart view with your own renderer
13
+ * import { registerChartView } from '@svgrid/grid'
14
+ * import MyChartView from './MyChartView.svelte'
15
+ * registerChartView(MyChartView)
16
+ * ```
17
+ */
18
+ import type { Component } from 'svelte';
19
+ /** Override the built-in chart-view renderer. Optional - the grid ships a
20
+ * free default, so this is only for hosts that want a custom renderer. */
21
+ export declare function registerChartView(component: Component<any>): void;
22
+ /** The registered chart-view override, or null to use the built-in default. */
23
+ export declare function getChartView(): Component<any> | null;
24
+ /** Whether a chart-view override has been registered. */
25
+ export declare function hasChartView(): boolean;
@@ -0,0 +1,14 @@
1
+ let renderer = $state(null);
2
+ /** Override the built-in chart-view renderer. Optional - the grid ships a
3
+ * free default, so this is only for hosts that want a custom renderer. */
4
+ export function registerChartView(component) {
5
+ renderer = component;
6
+ }
7
+ /** The registered chart-view override, or null to use the built-in default. */
8
+ export function getChartView() {
9
+ return renderer;
10
+ }
11
+ /** Whether a chart-view override has been registered. */
12
+ export function hasChartView() {
13
+ return renderer != null;
14
+ }
@@ -27,6 +27,8 @@ export type ComboboxConfig = {
27
27
  value: () => ComboboxValue;
28
28
  onChange?: (value: ComboboxValue) => void;
29
29
  disabled?: () => boolean;
30
+ /** Read-only: value shown, input focusable, but not editable and the panel will not open. */
31
+ readonly?: () => boolean;
30
32
  ariaLabel?: () => string | undefined;
31
33
  /** Filter the options locally by the query. Set false for server-side search. */
32
34
  localFilter?: () => boolean;
@@ -65,13 +67,16 @@ export declare function createCombobox(config: ComboboxConfig): {
65
67
  close: (revert?: boolean) => void;
66
68
  toggle: () => void;
67
69
  pick: (o: ListOption | undefined) => void;
70
+ clear: () => void;
68
71
  onInput: (e: Event) => void;
69
72
  onFocus: () => void;
70
73
  onKeydown: (e: KeyboardEvent) => void;
71
74
  /** Spread onto the editable <input>. */
72
75
  inputProps: () => {
76
+ 'aria-readonly': true | undefined;
73
77
  value: string;
74
78
  disabled: boolean;
79
+ readonly: true | undefined;
75
80
  oninput: (e: Event) => void;
76
81
  onfocus: () => void;
77
82
  onkeydown: (e: KeyboardEvent) => void;
@@ -27,6 +27,7 @@ export function createCombobox(config) {
27
27
  const listId = `${id}-list`;
28
28
  const opts = () => config.options();
29
29
  const disabled = () => config.disabled?.() ?? false;
30
+ const readonly = () => config.readonly?.() ?? false;
30
31
  let open = $state(false);
31
32
  let query = $state('');
32
33
  let active = $state(0);
@@ -47,7 +48,7 @@ export function createCombobox(config) {
47
48
  hint: config.hint?.(),
48
49
  ariaLabel: config.ariaLabel?.(),
49
50
  });
50
- function openPanel() { if (disabled() || open)
51
+ function openPanel() { if (disabled() || readonly() || open)
51
52
  return; open = true; active = 0; }
52
53
  function close(revert = true) {
53
54
  open = false;
@@ -63,7 +64,7 @@ export function createCombobox(config) {
63
64
  openPanel();
64
65
  } }
65
66
  function pick(o) {
66
- if (!o || o.disabled)
67
+ if (readonly() || !o || o.disabled)
67
68
  return;
68
69
  config.onChange?.(o.value);
69
70
  query = o.label;
@@ -71,18 +72,30 @@ export function createCombobox(config) {
71
72
  open = false;
72
73
  config.blurInput?.();
73
74
  }
75
+ /** Clear the selection (value -> null) and reset the shown text. */
76
+ function clear() {
77
+ if (readonly() || disabled())
78
+ return;
79
+ config.onChange?.(null);
80
+ query = '';
81
+ editing = false;
82
+ open = false;
83
+ }
74
84
  function setActive(i) { const o = filtered[i]; if (o && !o.disabled)
75
85
  active = i; }
76
86
  function onInput(e) {
87
+ if (readonly())
88
+ return;
77
89
  query = e.currentTarget.value;
78
90
  editing = true;
79
91
  active = 0;
80
92
  if (!open)
81
93
  openPanel();
82
94
  }
83
- function onFocus() { editing = true; query = selected?.label ?? ''; openPanel(); }
95
+ function onFocus() { if (readonly())
96
+ return; editing = true; query = selected?.label ?? ''; openPanel(); }
84
97
  function onKeydown(e) {
85
- if (disabled())
98
+ if (disabled() || readonly())
86
99
  return;
87
100
  if (!open && e.key === 'ArrowDown') {
88
101
  e.preventDefault();
@@ -133,6 +146,7 @@ export function createCombobox(config) {
133
146
  close,
134
147
  toggle,
135
148
  pick,
149
+ clear,
136
150
  onInput,
137
151
  onFocus,
138
152
  onKeydown,
@@ -145,8 +159,10 @@ export function createCombobox(config) {
145
159
  'aria-autocomplete': 'list',
146
160
  'aria-activedescendant': open && filtered[active] ? optionId(active) : undefined,
147
161
  ...editorAria(ariaState()),
162
+ 'aria-readonly': readonly() || undefined,
148
163
  value: shownText,
149
164
  disabled: disabled(),
165
+ readonly: readonly() || undefined,
150
166
  oninput: onInput,
151
167
  onfocus: onFocus,
152
168
  onkeydown: onKeydown,
@@ -156,7 +172,7 @@ export function createCombobox(config) {
156
172
  type: 'button',
157
173
  tabindex: -1,
158
174
  'aria-label': 'Toggle',
159
- disabled: disabled(),
175
+ disabled: disabled() || readonly(),
160
176
  onclick: toggle,
161
177
  }),
162
178
  /** Spread onto the listbox container. */
@@ -27,6 +27,8 @@ export type DropdownListConfig = {
27
27
  value: () => DropdownValue;
28
28
  onChange?: (value: string | number) => void;
29
29
  disabled?: () => boolean;
30
+ /** Read-only: value shown, trigger focusable, but the panel will not open. */
31
+ readonly?: () => boolean;
30
32
  ariaLabel?: () => string | undefined;
31
33
  /** DOM focus hook provided by the renderer; the core never touches the DOM. */
32
34
  focusTrigger?: () => void;
@@ -56,6 +58,7 @@ export declare function createDropdownList(config: DropdownListConfig): {
56
58
  onKeydown: (e: KeyboardEvent) => void;
57
59
  /** Spread onto the trigger <button>. */
58
60
  triggerProps: () => {
61
+ 'aria-readonly': true | undefined;
59
62
  disabled: boolean;
60
63
  onclick: () => void;
61
64
  onkeydown: (e: KeyboardEvent) => void;
@@ -27,6 +27,7 @@ export function createDropdownList(config) {
27
27
  const optionId = (index) => `${listId}-opt-${index}`;
28
28
  const opts = () => config.options();
29
29
  const disabled = () => config.disabled?.() ?? false;
30
+ const readonly = () => config.readonly?.() ?? false;
30
31
  const ariaState = () => ({
31
32
  id: config.id?.(),
32
33
  invalid: config.invalid?.(),
@@ -40,7 +41,7 @@ export function createDropdownList(config) {
40
41
  const selected = $derived(opts().find((o) => o.value === config.value()) ?? null);
41
42
  const enabledIdx = $derived(opts().map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0));
42
43
  function openPanel() {
43
- if (disabled() || open)
44
+ if (disabled() || readonly() || open)
44
45
  return;
45
46
  open = true;
46
47
  active = Math.max(0, opts().findIndex((o) => o.value === config.value()));
@@ -53,6 +54,8 @@ export function createDropdownList(config) {
53
54
  openPanel();
54
55
  } }
55
56
  function pick(i) {
57
+ if (readonly())
58
+ return;
56
59
  const o = opts()[i];
57
60
  if (!o || o.disabled)
58
61
  return;
@@ -70,6 +73,8 @@ export function createDropdownList(config) {
70
73
  // when the list is closed, matching a native <select>).
71
74
  const typeahead = createTypeaheadBuffer();
72
75
  function onTypeahead(char) {
76
+ if (readonly())
77
+ return;
73
78
  const buffer = typeahead.push(char.toLowerCase());
74
79
  const from = active >= 0 ? active : opts().findIndex((o) => o.value === config.value());
75
80
  const next = nextTypeaheadIndex(opts(), buffer, from);
@@ -80,7 +85,7 @@ export function createDropdownList(config) {
80
85
  config.onChange?.(opts()[next].value);
81
86
  }
82
87
  function onKeydown(e) {
83
- if (disabled())
88
+ if (disabled() || readonly())
84
89
  return;
85
90
  if (isTypeaheadKey(e)) {
86
91
  onTypeahead(e.key);
@@ -144,6 +149,7 @@ export function createDropdownList(config) {
144
149
  'aria-controls': listId,
145
150
  'aria-activedescendant': open && opts()[active] ? optionId(active) : undefined,
146
151
  ...editorAria(ariaState()),
152
+ 'aria-readonly': readonly() || undefined,
147
153
  disabled: disabled(),
148
154
  onclick: toggle,
149
155
  onkeydown: onKeydown,
@@ -20,6 +20,14 @@
20
20
  * ```
21
21
  */
22
22
  import { type FormEntry } from './form-field';
23
+ /** Localizable strings the form generates itself (override via `messages`). */
24
+ export type FormMessages = {
25
+ required: (label: string) => string;
26
+ minItems: (label: string, n: number) => string;
27
+ maxItems: (label: string, n: number) => string;
28
+ /** Shown by SvForm while an async validator runs. */
29
+ checking: string;
30
+ };
23
31
  export type FormConfig = {
24
32
  /** The schema - a mix of flat fields and titled sections. */
25
33
  fields: () => ReadonlyArray<FormEntry>;
@@ -29,6 +37,8 @@ export type FormConfig = {
29
37
  * `submitting` stays true until the returned promise settles. */
30
38
  onSubmit?: (values: Record<string, any>) => void | Promise<void>;
31
39
  onChange?: (values: Record<string, any>) => void;
40
+ /** Override the built-in generated strings (required / min-max items / checking). */
41
+ messages?: Partial<FormMessages>;
32
42
  };
33
43
  export declare function createForm(config: FormConfig): {
34
44
  readonly values: Record<string, any>;
@@ -58,5 +68,13 @@ export declare function createForm(config: FormConfig): {
58
68
  submit: () => Promise<boolean>;
59
69
  reset: (next?: Record<string, any>) => void;
60
70
  setErrors: (map: Record<string, string>) => void;
71
+ errorList: () => {
72
+ name: string;
73
+ label: string;
74
+ message: string;
75
+ }[];
76
+ firstErrorField: () => string | undefined;
77
+ /** The resolved message strings (defaults merged with `config.messages`). */
78
+ messages: FormMessages;
61
79
  };
62
80
  export type Form = ReturnType<typeof createForm>;
@@ -21,6 +21,12 @@
21
21
  */
22
22
  import { flattenFields } from './form-field';
23
23
  import { runRules } from './validators';
24
+ const DEFAULT_FORM_MESSAGES = {
25
+ required: (l) => `${l} is required`,
26
+ minItems: (_l, n) => `Add at least ${n} item(s)`,
27
+ maxItems: (_l, n) => `At most ${n} item(s)`,
28
+ checking: 'Checking…',
29
+ };
24
30
  /** Deep-ish equality for dirty tracking: identity for primitives, structural
25
31
  * (JSON) for arrays/objects. Good enough for form values. */
26
32
  function eq(a, b) {
@@ -43,6 +49,7 @@ function resolveFlag(flag, dflt, values) {
43
49
  return typeof flag === 'function' ? !!flag(values) : !!flag;
44
50
  }
45
51
  export function createForm(config) {
52
+ const M = { ...DEFAULT_FORM_MESSAGES, ...(config.messages ?? {}) };
46
53
  let values = $state({ ...(config.initial ?? {}) });
47
54
  let errors = $state({});
48
55
  let touched = $state({});
@@ -73,7 +80,7 @@ export function createForm(config) {
73
80
  * rows (so within-row cross-field rules work). */
74
81
  function syncError(field, v, ctx = values) {
75
82
  if (field.required && (v == null || v === '' || (Array.isArray(v) && !v.length)))
76
- return `${field.label} is required`;
83
+ return M.required(field.label);
77
84
  if (field.rules) {
78
85
  const e = runRules(v, field.rules, ctx);
79
86
  if (e)
@@ -132,6 +139,11 @@ export function createForm(config) {
132
139
  const field = fieldOf(name);
133
140
  if (!field)
134
141
  return true;
142
+ // Computed fields are derived + read-only - never validated, never block submit.
143
+ if (field.computed) {
144
+ setError(name, null);
145
+ return true;
146
+ }
135
147
  // Hidden fields never block submit; clear any stale error + pending check.
136
148
  if (!isVisible(name)) {
137
149
  setError(name, null);
@@ -213,11 +225,11 @@ export function createForm(config) {
213
225
  const items = arrayItems(name);
214
226
  let arrErr = null;
215
227
  if (field.required && items.length === 0)
216
- arrErr = `${field.label} is required`;
228
+ arrErr = M.required(field.label);
217
229
  else if (field.minItems != null && items.length < field.minItems)
218
- arrErr = `Add at least ${field.minItems} item(s)`;
230
+ arrErr = M.minItems(field.label, field.minItems);
219
231
  else if (field.maxItems != null && items.length > field.maxItems)
220
- arrErr = `At most ${field.maxItems} item(s)`;
232
+ arrErr = M.maxItems(field.label, field.maxItems);
221
233
  setError(name, arrErr);
222
234
  let ok = !arrErr;
223
235
  clearNamespacedErrors(name); // rebuild fresh per-item errors
@@ -227,8 +239,22 @@ export function createForm(config) {
227
239
  ok = false;
228
240
  return ok;
229
241
  }
242
+ /** True when `field` depends on `name` (cascading). */
243
+ function dependsOnName(field, name) {
244
+ const dep = field.dependsOn;
245
+ return dep == null ? false : Array.isArray(dep) ? dep.includes(name) : dep === name;
246
+ }
230
247
  function setValue(name, v) {
231
- values = { ...values, [name]: v };
248
+ let next = { ...values, [name]: v };
249
+ // Cascading: clear any fields that depend on this one so a stale child
250
+ // selection (e.g. a city under the old country) does not linger.
251
+ for (const f of flat()) {
252
+ if (dependsOnName(f, name) && next[f.name] != null) {
253
+ next = { ...next, [f.name]: null };
254
+ setError(f.name, null);
255
+ }
256
+ }
257
+ values = next;
232
258
  config.onChange?.(values);
233
259
  // Re-validate live once the field has been blurred at least once.
234
260
  if (touched[name])
@@ -270,6 +296,10 @@ export function createForm(config) {
270
296
  for (const f of flat())
271
297
  if (!isVisible(f.name))
272
298
  delete payload[f.name];
299
+ // Inject computed (derived) values so the payload carries them.
300
+ for (const f of flat())
301
+ if (f.computed && isVisible(f.name))
302
+ payload[f.name] = f.computed(values);
273
303
  submitting = true;
274
304
  try {
275
305
  await config.onSubmit?.(payload);
@@ -335,6 +365,17 @@ export function createForm(config) {
335
365
  touched = nextTouched;
336
366
  }
337
367
  const isFieldDirty = (name) => !eq(values[name], baseline[name]);
368
+ /** Top-level field errors (visible fields only), for a form-level summary. */
369
+ function errorList() {
370
+ const out = [];
371
+ for (const f of flat()) {
372
+ const m = errors[f.name];
373
+ if (m && isVisible(f.name))
374
+ out.push({ name: f.name, label: f.label, message: m });
375
+ }
376
+ return out;
377
+ }
378
+ const firstErrorField = () => errorList()[0]?.name;
338
379
  return {
339
380
  get values() { return values; },
340
381
  get submitting() { return submitting; },
@@ -345,7 +386,7 @@ export function createForm(config) {
345
386
  return true;
346
387
  return false;
347
388
  },
348
- value: (name) => values[name],
389
+ value: (name) => { const f = fieldOf(name); return f?.computed ? f.computed(values) : values[name]; },
349
390
  error: (name) => errors[name],
350
391
  isTouched: (name) => !!touched[name],
351
392
  hasError: (name) => !!errors[name],
@@ -370,5 +411,9 @@ export function createForm(config) {
370
411
  submit,
371
412
  reset,
372
413
  setErrors,
414
+ errorList,
415
+ firstErrorField,
416
+ /** The resolved message strings (defaults merged with `config.messages`). */
417
+ messages: M,
373
418
  };
374
419
  }
@@ -1,4 +1,5 @@
1
1
  import { enabledIndices, wrapMove } from './list-nav';
2
+ import { createTypeaheadBuffer, isTypeaheadKey } from './list-option';
2
3
  let uid = 0;
3
4
  export function createMenu(config) {
4
5
  const items = () => config.items();
@@ -7,6 +8,23 @@ export function createMenu(config) {
7
8
  // -1 = nothing focused yet (the first enabled item is the tab stop).
8
9
  let active = $state(-1);
9
10
  let openSub = $state(-1);
11
+ // Type-ahead: printable keys jump to the next item whose label starts with the
12
+ // accumulated buffer (WAI-ARIA menu behavior), wrapping past the active item.
13
+ const typeahead = createTypeaheadBuffer();
14
+ function typeaheadMatch(buffer) {
15
+ const b = buffer.trim().toLowerCase();
16
+ if (!b)
17
+ return -1;
18
+ const its = items();
19
+ const n = its.length;
20
+ for (let k = 1; k <= n; k++) {
21
+ const i = (active + k + n) % n;
22
+ const it = its[i];
23
+ if (it && !it.disabled && !it.separator && (it.label ?? '').toLowerCase().startsWith(b))
24
+ return i;
25
+ }
26
+ return -1;
27
+ }
10
28
  // Real (non-separator, non-disabled) item indices, for roving focus.
11
29
  const focusables = $derived(enabledIndices(items().length, (i) => {
12
30
  const it = items()[i];
@@ -108,6 +126,15 @@ export function createMenu(config) {
108
126
  case 'Tab':
109
127
  config.onClose();
110
128
  break;
129
+ default:
130
+ // Type-ahead: a bare printable key jumps to the matching item.
131
+ if (isTypeaheadKey(e)) {
132
+ const idx = typeaheadMatch(typeahead.push(e.key));
133
+ if (idx >= 0) {
134
+ e.preventDefault();
135
+ focus(idx);
136
+ }
137
+ }
111
138
  }
112
139
  }
113
140
  function itemTabIndex(i) {
@@ -18,6 +18,8 @@ export type PopoverSelectConfig = {
18
18
  itemCount: () => number;
19
19
  /** Whether item `i` is disabled (skipped by roving, not selectable). */
20
20
  disabled?: (index: number) => boolean;
21
+ /** Read-only: value shown, trigger focusable, but the panel will not open. */
22
+ readonly?: () => boolean;
21
23
  /** Commit the item at `index` (Enter or a click the component forwards). */
22
24
  onSelect: (index: number) => void;
23
25
  /** Notified whenever the open state changes. */
@@ -69,6 +71,7 @@ export declare function createPopoverSelect(config: PopoverSelectConfig): {
69
71
  'aria-haspopup': "listbox" | "grid" | "tree";
70
72
  'aria-expanded': boolean;
71
73
  'aria-controls': string | undefined;
74
+ 'aria-readonly': true | undefined;
72
75
  onkeydown: (e: KeyboardEvent) => void;
73
76
  };
74
77
  /** Attributes for the element that OWNS focus + activedescendant (the panel,
@@ -25,6 +25,7 @@ export function createPopoverSelect(config) {
25
25
  let rect = $state({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 });
26
26
  const count = () => config.itemCount();
27
27
  const isDisabled = (i) => config.disabled?.(i) ?? false;
28
+ const readonly = () => config.readonly?.() ?? false;
28
29
  function enabledIndices() {
29
30
  return enabledIndicesOf(count(), isDisabled);
30
31
  }
@@ -39,6 +40,8 @@ export function createPopoverSelect(config) {
39
40
  config.onOpenChange?.(v);
40
41
  }
41
42
  function openPanel() {
43
+ if (readonly())
44
+ return;
42
45
  active = clampActive(config.initialActive?.() ?? 0);
43
46
  setOpen(true);
44
47
  }
@@ -47,6 +50,8 @@ export function createPopoverSelect(config) {
47
50
  config.getTrigger()?.focus();
48
51
  }
49
52
  function toggle() {
53
+ if (readonly())
54
+ return;
50
55
  open ? setOpen(false) : openPanel();
51
56
  }
52
57
  function move(delta) {
@@ -57,7 +62,7 @@ export function createPopoverSelect(config) {
57
62
  function first() { active = enabledIndices()[0] ?? 0; }
58
63
  function last() { active = enabledIndices().at(-1) ?? 0; }
59
64
  function selectActive() {
60
- if (count() === 0 || isDisabled(active))
65
+ if (readonly() || count() === 0 || isDisabled(active))
61
66
  return;
62
67
  config.onSelect(active);
63
68
  if (config.closeOnSelect?.() ?? true)
@@ -95,6 +100,8 @@ export function createPopoverSelect(config) {
95
100
  };
96
101
  });
97
102
  function onTriggerKeydown(e) {
103
+ if (readonly())
104
+ return;
98
105
  if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
99
106
  e.preventDefault();
100
107
  openPanel();
@@ -172,6 +179,7 @@ export function createPopoverSelect(config) {
172
179
  'aria-haspopup': haspopup,
173
180
  'aria-expanded': open,
174
181
  'aria-controls': open ? panelId : undefined,
182
+ 'aria-readonly': readonly() || undefined,
175
183
  onkeydown: onTriggerKeydown,
176
184
  }),
177
185
  /** Attributes for the element that OWNS focus + activedescendant (the panel,
@@ -1,24 +1,3 @@
1
- /**
2
- * createSlider - the HEADLESS core behind <SvSlider>, in the same spirit as
3
- * `createSvGrid` for the grid: a runes-based state machine (single + dual-thumb,
4
- * stepping, keyboard, drag state) that owns the value<->position math and the
5
- * ARIA wiring, exposed as **prop-getters** you spread onto YOUR OWN markup.
6
- *
7
- * The core never touches the DOM: the styled renderer measures the track
8
- * (`getBoundingClientRect`) and passes the rect + pointer position IN via
9
- * `pointerDown` / `setFromPointer`. The core turns that into a stepped value.
10
- *
11
- * ```svelte
12
- * <script lang="ts">
13
- * import { createSlider } from '@svgrid/grid'
14
- * let value = $state(50)
15
- * const s = createSlider({ value: () => value, onChange: (v) => (value = v as number) })
16
- * </script>
17
- * <div bind:this={track} onpointerdown={(e) => s.pointerDown({ x: e.clientX, y: e.clientY }, track.getBoundingClientRect())}>
18
- * <div {...s.thumbProps(0)} style:left={s.percentOf(s.hi) + '%'}></div>
19
- * </div>
20
- * ```
21
- */
22
1
  export type SliderValue = number | [number, number];
23
2
  export type SliderOrientation = 'horizontal' | 'vertical';
24
3
  export type SliderThumb = 'single' | 'lo' | 'hi';
@@ -39,6 +18,11 @@ export type SliderConfig = {
39
18
  ariaLabel?: () => string | undefined;
40
19
  /** Right-to-left: mirrors horizontal pointer + Left/Right key direction. */
41
20
  rtl?: () => boolean;
21
+ id?: () => string | undefined;
22
+ invalid?: () => boolean | undefined;
23
+ required?: () => boolean | undefined;
24
+ error?: () => string | undefined;
25
+ hint?: () => string | undefined;
42
26
  };
43
27
  export declare function createSlider(config: SliderConfig): {
44
28
  readonly value: SliderValue;
@@ -67,6 +51,9 @@ export declare function createSlider(config: SliderConfig): {
67
51
  'aria-label': string;
68
52
  'aria-orientation': SliderOrientation;
69
53
  'aria-disabled': true | undefined;
54
+ 'aria-invalid': "true" | undefined;
55
+ 'aria-required': "true" | undefined;
56
+ 'aria-describedby': string | undefined;
70
57
  onkeydown: (e: KeyboardEvent) => void;
71
58
  };
72
59
  };
@@ -1,3 +1,25 @@
1
+ /**
2
+ * createSlider - the HEADLESS core behind <SvSlider>, in the same spirit as
3
+ * `createSvGrid` for the grid: a runes-based state machine (single + dual-thumb,
4
+ * stepping, keyboard, drag state) that owns the value<->position math and the
5
+ * ARIA wiring, exposed as **prop-getters** you spread onto YOUR OWN markup.
6
+ *
7
+ * The core never touches the DOM: the styled renderer measures the track
8
+ * (`getBoundingClientRect`) and passes the rect + pointer position IN via
9
+ * `pointerDown` / `setFromPointer`. The core turns that into a stepped value.
10
+ *
11
+ * ```svelte
12
+ * <script lang="ts">
13
+ * import { createSlider } from '@svgrid/grid'
14
+ * let value = $state(50)
15
+ * const s = createSlider({ value: () => value, onChange: (v) => (value = v as number) })
16
+ * </script>
17
+ * <div bind:this={track} onpointerdown={(e) => s.pointerDown({ x: e.clientX, y: e.clientY }, track.getBoundingClientRect())}>
18
+ * <div {...s.thumbProps(0)} style:left={s.percentOf(s.hi) + '%'}></div>
19
+ * </div>
20
+ * ```
21
+ */
22
+ import { editorAria } from './editor-contract';
1
23
  export function createSlider(config) {
2
24
  const min = () => config.min?.() ?? 0;
3
25
  const max = () => config.max?.() ?? 100;
@@ -118,6 +140,15 @@ export function createSlider(config) {
118
140
  const vmin = which === 'hi' ? lo : min();
119
141
  const vmax = which === 'lo' ? hi : max();
120
142
  const label = which === 'lo' ? 'Minimum' : which === 'hi' ? 'Maximum' : (config.ariaLabel?.() ?? 'Value');
143
+ // Validation ARIA (invalid / required / describedby) from the editor
144
+ // contract; keep the thumb's own value-oriented aria-label.
145
+ const va = editorAria({
146
+ id: config.id?.(),
147
+ invalid: config.invalid?.(),
148
+ required: config.required?.(),
149
+ error: config.error?.(),
150
+ hint: config.hint?.(),
151
+ });
121
152
  return {
122
153
  role: 'slider',
123
154
  tabindex: disabled() ? -1 : 0,
@@ -127,6 +158,9 @@ export function createSlider(config) {
127
158
  'aria-label': label,
128
159
  'aria-orientation': (isVert() ? 'vertical' : 'horizontal'),
129
160
  'aria-disabled': disabled() || undefined,
161
+ 'aria-invalid': va['aria-invalid'],
162
+ 'aria-required': va['aria-required'],
163
+ 'aria-describedby': va['aria-describedby'],
130
164
  onkeydown: (e) => onThumbKey(e, which),
131
165
  };
132
166
  },