@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,33 @@
1
+ /**
2
+ * board-view registry - the injection seam for the Kanban *board view of the
3
+ * grid*. Setting the `board` prop on `<SvGrid>` switches the table for lanes of
4
+ * cards, but the renderer itself ships in `@svgrid/enterprise` (a paid feature).
5
+ * This tiny reactive holder lets enterprise register that renderer at import
6
+ * time; the grid looks it up and mounts it, or shows an upsell placeholder when
7
+ * it is absent. Mirrors the `scheduler-view` / `editor-registry` pattern.
8
+ *
9
+ * ```ts
10
+ * // in @svgrid/enterprise, at module load:
11
+ * import { registerBoardView } from '@svgrid/grid'
12
+ * import SvGridBoard from './SvGridBoard.svelte'
13
+ * registerBoardView(SvGridBoard)
14
+ * ```
15
+ */
16
+ import type { Component } from 'svelte'
17
+
18
+ let renderer = $state<Component<any> | null>(null)
19
+
20
+ /** Register the component that renders `board` mode. Enterprise calls this. */
21
+ export function registerBoardView(component: Component<any>): void {
22
+ renderer = component
23
+ }
24
+
25
+ /** The registered board renderer, or null when enterprise is not installed. */
26
+ export function getBoardView(): Component<any> | null {
27
+ return renderer
28
+ }
29
+
30
+ /** Whether a board renderer has been registered. */
31
+ export function hasBoardView(): boolean {
32
+ return renderer != null
33
+ }
@@ -530,3 +530,104 @@ describe("SvGridApi - sort opt-out + refresh", () => {
530
530
  }
531
531
  });
532
532
  });
533
+
534
+ describe("SvGridApi - setOption / getOption (runtime prop overrides)", () => {
535
+ it("getOption returns the incoming prop when no override is set", async () => {
536
+ const { api, destroy } = await mountGrid();
537
+ try {
538
+ expect(api.getOption("rowHeight")).toBe(36);
539
+ expect(api.getOption("zebraRows")).toBeUndefined();
540
+ } finally {
541
+ destroy();
542
+ }
543
+ });
544
+
545
+ it("setOption overrides a prop, getOption reflects it, undefined clears it", async () => {
546
+ const { api, destroy } = await mountGrid();
547
+ try {
548
+ api.setOption("rowHeight", 48);
549
+ await flush();
550
+ expect(api.getOption("rowHeight")).toBe(48);
551
+
552
+ api.setOption("zebraRows", true);
553
+ await flush();
554
+ expect(api.getOption("zebraRows")).toBe(true);
555
+
556
+ // Clearing an override falls back to the incoming prop.
557
+ api.setOption("rowHeight", undefined);
558
+ await flush();
559
+ expect(api.getOption("rowHeight")).toBe(36);
560
+ } finally {
561
+ destroy();
562
+ }
563
+ });
564
+
565
+ it("setOption('sortable', true) turns sorting on at runtime; false turns it off", async () => {
566
+ // Mount with NO features so sorting starts disabled - only the runtime
567
+ // override can inject rowSortingFeature (mirrors resolveEffectiveFeatures).
568
+ const { api, destroy } = await mountGrid({}, tableFeatures({}));
569
+ try {
570
+ // Off by default: setSort is guarded to a no-op.
571
+ api.setSort("salary", "asc");
572
+ await flush();
573
+ expect(api.getState().sorting).toEqual([]);
574
+
575
+ // Turn sorting on via the override, then setSort takes effect.
576
+ api.setOption("sortable", true);
577
+ await flush();
578
+ api.setSort("salary", "asc");
579
+ await flush();
580
+ expect(api.getState().sorting).toEqual([{ id: "salary", desc: false }]);
581
+
582
+ // Turn it back off: setSort is guarded again.
583
+ api.setOption("sortable", false);
584
+ await flush();
585
+ api.clearSort();
586
+ await flush();
587
+ api.setSort("team", "asc");
588
+ await flush();
589
+ expect(api.getState().sorting).toEqual([]);
590
+ } finally {
591
+ destroy();
592
+ }
593
+ });
594
+
595
+ it("a view-direct prop (zebraRows) visually re-renders on override", async () => {
596
+ const { api, target, destroy } = await mountGrid();
597
+ try {
598
+ // No striping to start.
599
+ expect(target.querySelectorAll(".sv-grid-row-alt").length).toBe(0);
600
+
601
+ api.setOption("zebraRows", true);
602
+ await flush();
603
+ await flush();
604
+ // Odd rows now carry the alternate-row class (view reads props.zebraRows,
605
+ // which is the effective proxy).
606
+ expect(target.querySelectorAll(".sv-grid-row-alt").length).toBeGreaterThan(0);
607
+
608
+ api.setOption("zebraRows", undefined);
609
+ await flush();
610
+ await flush();
611
+ expect(target.querySelectorAll(".sv-grid-row-alt").length).toBe(0);
612
+ } finally {
613
+ destroy();
614
+ }
615
+ });
616
+
617
+ it("resetOptions clears every override", async () => {
618
+ const { api, destroy } = await mountGrid();
619
+ try {
620
+ api.setOption("rowHeight", 60);
621
+ api.setOption("zebraRows", true);
622
+ await flush();
623
+ expect(api.getOption("rowHeight")).toBe(60);
624
+
625
+ api.resetOptions();
626
+ await flush();
627
+ expect(api.getOption("rowHeight")).toBe(36);
628
+ expect(api.getOption("zebraRows")).toBeUndefined();
629
+ } finally {
630
+ destroy();
631
+ }
632
+ });
633
+ });
package/src/build-api.ts CHANGED
@@ -48,17 +48,14 @@ import {
48
48
  getDateFormatter,
49
49
  resolveDatePattern,
50
50
  } from "./cell-formatting";
51
- import {
52
- projectGridRows,
53
- serializeDelimited,
54
- serializeJson,
55
- serializeMarkdown,
56
- downloadTextFile,
57
- copyTextToClipboard,
58
- type GridExportColumn,
59
- type GridExportScope,
60
- type GridExportOptions,
61
- type GridClipboardOptions,
51
+ // The export serializers load lazily (their own chunk) so export-format.ts stays
52
+ // out of the base bundle - it is fetched only when an export/copy method is
53
+ // actually invoked (an explicit, already-async user action).
54
+ import type {
55
+ GridExportColumn,
56
+ GridExportScope,
57
+ GridExportOptions,
58
+ GridClipboardOptions,
62
59
  } from "./export-format";
63
60
  import {
64
61
  RenderSnippetConfig,
@@ -166,16 +163,18 @@ export function createGridApi<
166
163
  }
167
164
  return out;
168
165
  };
169
- const projectForExport = (
166
+ const projectForExport = async (
170
167
  scope: GridExportScope,
171
168
  columns: ReadonlyArray<string> | undefined,
172
169
  rawValues: boolean | undefined,
173
- ) =>
174
- projectGridRows(
170
+ ) => {
171
+ const { projectGridRows } = await import("./export-format");
172
+ return projectGridRows(
175
173
  exportScopeRows(scope) as ReadonlyArray<Record<string, unknown>>,
176
174
  exportColumns(),
177
175
  { columns, rawValues },
178
176
  );
177
+ };
179
178
 
180
179
  return {
181
180
  getCellValue(rowIndex, columnId) {
@@ -372,6 +371,28 @@ export function createGridApi<
372
371
  setGroupBy(columnIds) {
373
372
  ctx.grid.setGrouping([...columnIds]);
374
373
  },
374
+ setOption(key, value) {
375
+ // Runtime prop override. Writes into the controller's `optionOverrides`
376
+ // store, which the effective-props proxy merges over the incoming prop, so
377
+ // the grid re-renders exactly as if the parent had changed that prop.
378
+ // `undefined` clears the override (falls back to the prop). Reassign the
379
+ // whole object for reliable reactivity; `untrack` per the `setFilter` note.
380
+ untrack(() => {
381
+ const next: Record<string, unknown> = { ...ctx.optionOverrides };
382
+ if (value === undefined) delete next[key as string];
383
+ else next[key as string] = value;
384
+ ctx.optionOverrides = next;
385
+ });
386
+ },
387
+ getOption(key) {
388
+ // The effective value: the override if set, otherwise the incoming prop.
389
+ return (ctx.props as Record<string, unknown>)[key as string] as never;
390
+ },
391
+ resetOptions() {
392
+ untrack(() => {
393
+ ctx.optionOverrides = {};
394
+ });
395
+ },
375
396
  setFilter(columnId, filter) {
376
397
  // `untrack`: these imperative mutators read the same filter state they
377
398
  // write (read-modify-write on `filterMenuValues`). When a consumer
@@ -511,7 +532,8 @@ export function createGridApi<
511
532
  },
512
533
  // ---- Free data export (CSV / TSV / JSON + clipboard) -----------------
513
534
  async exportCsv(options: GridExportOptions = {}) {
514
- const { records, fields } = projectForExport(
535
+ const { serializeDelimited, downloadTextFile } = await import("./export-format");
536
+ const { records, fields } = await projectForExport(
515
537
  options.rows ?? "displayed",
516
538
  options.columns,
517
539
  options.rawValues,
@@ -526,7 +548,8 @@ export function createGridApi<
526
548
  return text;
527
549
  },
528
550
  async exportTsv(options: GridExportOptions = {}) {
529
- const { records, fields } = projectForExport(
551
+ const { serializeDelimited, downloadTextFile } = await import("./export-format");
552
+ const { records, fields } = await projectForExport(
530
553
  options.rows ?? "displayed",
531
554
  options.columns,
532
555
  options.rawValues,
@@ -541,7 +564,8 @@ export function createGridApi<
541
564
  return text;
542
565
  },
543
566
  async exportJson(options: GridExportOptions = {}) {
544
- const { records, fields } = projectForExport(
567
+ const { serializeJson, downloadTextFile } = await import("./export-format");
568
+ const { records, fields } = await projectForExport(
545
569
  options.rows ?? "displayed",
546
570
  options.columns,
547
571
  options.rawValues,
@@ -555,7 +579,8 @@ export function createGridApi<
555
579
  return text;
556
580
  },
557
581
  async copyToClipboard(options: GridClipboardOptions = {}) {
558
- const { records, fields, align } = projectForExport(
582
+ const { serializeDelimited, serializeMarkdown, copyTextToClipboard } = await import("./export-format");
583
+ const { records, fields, align } = await projectForExport(
559
584
  options.rows ?? "displayed",
560
585
  options.columns,
561
586
  options.rawValues,
@@ -77,7 +77,7 @@ describe('registerBuiltinEditors in SvGrid', () => {
77
77
  api.startEditing(0, 'estimate')
78
78
  await tick()
79
79
  await tick()
80
- expect(target.querySelector('.sv-dur')).not.toBeNull()
80
+ expect(target.querySelector('.sv-dur__input')).not.toBeNull()
81
81
  } finally {
82
82
  destroy()
83
83
  }
@@ -60,7 +60,7 @@ export function resolveDatePattern(
60
60
 
61
61
  type NumericFormatInput = {
62
62
  type: 'number' | 'currency' | 'percent'
63
- locales?: string | string[]
63
+ locales?: string | readonly string[]
64
64
  currency?: string
65
65
  /** When type is percent: treat value as 0–100 instead of Intl’s 0–1 fraction */
66
66
  valueIsPercentPoints?: boolean
@@ -75,7 +75,7 @@ type NumericFormatInput = {
75
75
  */
76
76
  const numberFormatterCache = new Map<string, Intl.NumberFormat>()
77
77
  function getNumberFormatter(
78
- locales: string | string[] | undefined,
78
+ locales: string | readonly string[] | undefined,
79
79
  options: Intl.NumberFormatOptions,
80
80
  ): Intl.NumberFormat {
81
81
  // Stable key - for normal column configs the options object is reused
@@ -98,7 +98,7 @@ function getNumberFormatter(
98
98
  (options.notation ?? '')
99
99
  let fmt = numberFormatterCache.get(key)
100
100
  if (!fmt) {
101
- fmt = new Intl.NumberFormat(locales, options)
101
+ fmt = new Intl.NumberFormat(locales as string | string[] | undefined, options)
102
102
  numberFormatterCache.set(key, fmt)
103
103
  }
104
104
  return fmt
@@ -107,7 +107,7 @@ function getNumberFormatter(
107
107
  /** Cache of `Intl.DateTimeFormat` by `(locale, options)` signature. */
108
108
  const dateFormatterCache = new Map<string, Intl.DateTimeFormat>()
109
109
  export function getDateFormatter(
110
- locales: string | string[] | undefined,
110
+ locales: string | readonly string[] | undefined,
111
111
  options: Intl.DateTimeFormatOptions,
112
112
  ): Intl.DateTimeFormat {
113
113
  const key =
@@ -134,7 +134,7 @@ export function getDateFormatter(
134
134
  (options.hour12 ?? '')
135
135
  let fmt = dateFormatterCache.get(key)
136
136
  if (!fmt) {
137
- fmt = new Intl.DateTimeFormat(locales, options)
137
+ fmt = new Intl.DateTimeFormat(locales as string | string[] | undefined, options)
138
138
  dateFormatterCache.set(key, fmt)
139
139
  }
140
140
  return fmt
@@ -332,7 +332,7 @@ export function createCellRender<
332
332
  ) {
333
333
  return formatNumericWithConfig(value, {
334
334
  type: formatConfig.type,
335
- locales: formatConfig.locales,
335
+ locales: (formatConfig.locales ?? ctx.props.localization?.locale),
336
336
  currency:
337
337
  formatConfig.type === "currency"
338
338
  ? (formatConfig.currency ?? "USD")
@@ -365,7 +365,7 @@ export function createCellRender<
365
365
  minute: "2-digit",
366
366
  };
367
367
 
368
- return getDateFormatter(formatConfig.locales, merged).format(
368
+ return getDateFormatter((formatConfig.locales ?? ctx.props.localization?.locale), merged).format(
369
369
  parsedDate,
370
370
  );
371
371
  }
@@ -406,7 +406,7 @@ export function createCellRender<
406
406
  ) {
407
407
  return formatNumericWithConfig(value, {
408
408
  type: formatConfig.type,
409
- locales: formatConfig.locales,
409
+ locales: (formatConfig.locales ?? ctx.props.localization?.locale),
410
410
  currency:
411
411
  formatConfig.type === "currency"
412
412
  ? (formatConfig.currency ?? "USD")
@@ -428,7 +428,7 @@ export function createCellRender<
428
428
  : formatConfig.type === "date"
429
429
  ? { year: "numeric", month: "2-digit", day: "2-digit" }
430
430
  : { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" };
431
- return getDateFormatter(formatConfig.locales, merged).format(parsedDate);
431
+ return getDateFormatter((formatConfig.locales ?? ctx.props.localization?.locale), merged).format(parsedDate);
432
432
  }
433
433
  }
434
434
  return String(value ?? "");
@@ -0,0 +1,36 @@
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
+
20
+ let renderer = $state<Component<any> | null>(null)
21
+
22
+ /** Override the built-in chart-view renderer. Optional - the grid ships a
23
+ * free default, so this is only for hosts that want a custom renderer. */
24
+ export function registerChartView(component: Component<any>): void {
25
+ renderer = component
26
+ }
27
+
28
+ /** The registered chart-view override, or null to use the built-in default. */
29
+ export function getChartView(): Component<any> | null {
30
+ return renderer
31
+ }
32
+
33
+ /** Whether a chart-view override has been registered. */
34
+ export function hasChartView(): boolean {
35
+ return renderer != null
36
+ }
@@ -30,6 +30,8 @@ export type ComboboxConfig = {
30
30
  value: () => ComboboxValue
31
31
  onChange?: (value: ComboboxValue) => void
32
32
  disabled?: () => boolean
33
+ /** Read-only: value shown, input focusable, but not editable and the panel will not open. */
34
+ readonly?: () => boolean
33
35
  ariaLabel?: () => string | undefined
34
36
  /** Filter the options locally by the query. Set false for server-side search. */
35
37
  localFilter?: () => boolean
@@ -51,6 +53,7 @@ export function createCombobox(config: ComboboxConfig) {
51
53
  const listId = `${id}-list`
52
54
  const opts = () => config.options()
53
55
  const disabled = () => config.disabled?.() ?? false
56
+ const readonly = () => config.readonly?.() ?? false
54
57
 
55
58
  let open = $state(false)
56
59
  let query = $state('')
@@ -76,25 +79,31 @@ export function createCombobox(config: ComboboxConfig) {
76
79
  ariaLabel: config.ariaLabel?.(),
77
80
  })
78
81
 
79
- function openPanel() { if (disabled() || open) return; open = true; active = 0 }
82
+ function openPanel() { if (disabled() || readonly() || open) return; open = true; active = 0 }
80
83
  function close(revert = true) {
81
84
  open = false; editing = false
82
85
  if (revert) query = selected?.label ?? ''
83
86
  }
84
87
  function toggle() { if (open) { close() } else { config.focusInput?.(); openPanel() } }
85
88
  function pick(o: ListOption | undefined) {
86
- if (!o || o.disabled) return
89
+ if (readonly() || !o || o.disabled) return
87
90
  config.onChange?.(o.value); query = o.label; editing = false; open = false; config.blurInput?.()
88
91
  }
92
+ /** Clear the selection (value -> null) and reset the shown text. */
93
+ function clear() {
94
+ if (readonly() || disabled()) return
95
+ config.onChange?.(null); query = ''; editing = false; open = false
96
+ }
89
97
  function setActive(i: number) { const o = filtered[i]; if (o && !o.disabled) active = i }
90
98
  function onInput(e: Event) {
99
+ if (readonly()) return
91
100
  query = (e.currentTarget as HTMLInputElement).value
92
101
  editing = true; active = 0
93
102
  if (!open) openPanel()
94
103
  }
95
- function onFocus() { editing = true; query = selected?.label ?? ''; openPanel() }
104
+ function onFocus() { if (readonly()) return; editing = true; query = selected?.label ?? ''; openPanel() }
96
105
  function onKeydown(e: KeyboardEvent) {
97
- if (disabled()) return
106
+ if (disabled() || readonly()) return
98
107
  if (!open && e.key === 'ArrowDown') { e.preventDefault(); editing = true; openPanel(); return }
99
108
  if (e.key === 'ArrowDown') { e.preventDefault(); active = Math.min(active + 1, filtered.length - 1) }
100
109
  else if (e.key === 'ArrowUp') { e.preventDefault(); active = Math.max(active - 1, 0) }
@@ -128,6 +137,7 @@ export function createCombobox(config: ComboboxConfig) {
128
137
  close,
129
138
  toggle,
130
139
  pick,
140
+ clear,
131
141
  onInput,
132
142
  onFocus,
133
143
  onKeydown,
@@ -140,8 +150,10 @@ export function createCombobox(config: ComboboxConfig) {
140
150
  'aria-autocomplete': 'list' as const,
141
151
  'aria-activedescendant': open && filtered[active] ? optionId(active) : undefined,
142
152
  ...editorAria(ariaState()),
153
+ 'aria-readonly': readonly() || undefined,
143
154
  value: shownText,
144
155
  disabled: disabled(),
156
+ readonly: readonly() || undefined,
145
157
  oninput: onInput,
146
158
  onfocus: onFocus,
147
159
  onkeydown: onKeydown,
@@ -151,7 +163,7 @@ export function createCombobox(config: ComboboxConfig) {
151
163
  type: 'button' as const,
152
164
  tabindex: -1,
153
165
  'aria-label': 'Toggle',
154
- disabled: disabled(),
166
+ disabled: disabled() || readonly(),
155
167
  onclick: toggle,
156
168
  }),
157
169
  /** Spread onto the listbox container. */
@@ -30,6 +30,8 @@ export type DropdownListConfig = {
30
30
  value: () => DropdownValue
31
31
  onChange?: (value: string | number) => void
32
32
  disabled?: () => boolean
33
+ /** Read-only: value shown, trigger focusable, but the panel will not open. */
34
+ readonly?: () => boolean
33
35
  ariaLabel?: () => string | undefined
34
36
  /** DOM focus hook provided by the renderer; the core never touches the DOM. */
35
37
  focusTrigger?: () => void
@@ -48,6 +50,7 @@ export function createDropdownList(config: DropdownListConfig) {
48
50
  const optionId = (index: number) => `${listId}-opt-${index}`
49
51
  const opts = () => config.options()
50
52
  const disabled = () => config.disabled?.() ?? false
53
+ const readonly = () => config.readonly?.() ?? false
51
54
 
52
55
  const ariaState = (): EditorAriaState => ({
53
56
  id: config.id?.(),
@@ -65,13 +68,14 @@ export function createDropdownList(config: DropdownListConfig) {
65
68
  const enabledIdx = $derived(opts().map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0))
66
69
 
67
70
  function openPanel() {
68
- if (disabled() || open) return
71
+ if (disabled() || readonly() || open) return
69
72
  open = true
70
73
  active = Math.max(0, opts().findIndex((o) => o.value === config.value()))
71
74
  }
72
75
  function close() { open = false }
73
76
  function toggle() { if (open) { close() } else { openPanel() } }
74
77
  function pick(i: number) {
78
+ if (readonly()) return
75
79
  const o = opts()[i]
76
80
  if (!o || o.disabled) return
77
81
  config.onChange?.(o.value); close(); config.focusTrigger?.()
@@ -85,6 +89,7 @@ export function createDropdownList(config: DropdownListConfig) {
85
89
  // when the list is closed, matching a native <select>).
86
90
  const typeahead = createTypeaheadBuffer()
87
91
  function onTypeahead(char: string) {
92
+ if (readonly()) return
88
93
  const buffer = typeahead.push(char.toLowerCase())
89
94
  const from = active >= 0 ? active : opts().findIndex((o) => o.value === config.value())
90
95
  const next = nextTypeaheadIndex(opts(), buffer, from)
@@ -93,7 +98,7 @@ export function createDropdownList(config: DropdownListConfig) {
93
98
  if (!open) config.onChange?.(opts()[next]!.value)
94
99
  }
95
100
  function onKeydown(e: KeyboardEvent) {
96
- if (disabled()) return
101
+ if (disabled() || readonly()) return
97
102
  if (isTypeaheadKey(e)) { onTypeahead(e.key); return }
98
103
  if (!open && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); openPanel(); return }
99
104
  if (!open) return
@@ -132,6 +137,7 @@ export function createDropdownList(config: DropdownListConfig) {
132
137
  'aria-controls': listId,
133
138
  'aria-activedescendant': open && opts()[active] ? optionId(active) : undefined,
134
139
  ...editorAria(ariaState()),
140
+ 'aria-readonly': readonly() || undefined,
135
141
  disabled: disabled(),
136
142
  onclick: toggle,
137
143
  onkeydown: onKeydown,
@@ -22,6 +22,22 @@
22
22
  import { flattenFields, type FormField, type FormEntry } from './form-field'
23
23
  import { runRules } from './validators'
24
24
 
25
+ /** Localizable strings the form generates itself (override via `messages`). */
26
+ export type FormMessages = {
27
+ required: (label: string) => string
28
+ minItems: (label: string, n: number) => string
29
+ maxItems: (label: string, n: number) => string
30
+ /** Shown by SvForm while an async validator runs. */
31
+ checking: string
32
+ }
33
+
34
+ const DEFAULT_FORM_MESSAGES: FormMessages = {
35
+ required: (l) => `${l} is required`,
36
+ minItems: (_l, n) => `Add at least ${n} item(s)`,
37
+ maxItems: (_l, n) => `At most ${n} item(s)`,
38
+ checking: 'Checking…',
39
+ }
40
+
25
41
  export type FormConfig = {
26
42
  /** The schema - a mix of flat fields and titled sections. */
27
43
  fields: () => ReadonlyArray<FormEntry>
@@ -31,6 +47,8 @@ export type FormConfig = {
31
47
  * `submitting` stays true until the returned promise settles. */
32
48
  onSubmit?: (values: Record<string, any>) => void | Promise<void>
33
49
  onChange?: (values: Record<string, any>) => void
50
+ /** Override the built-in generated strings (required / min-max items / checking). */
51
+ messages?: Partial<FormMessages>
34
52
  }
35
53
 
36
54
  /** Deep-ish equality for dirty tracking: identity for primitives, structural
@@ -54,6 +72,7 @@ function resolveFlag(
54
72
  }
55
73
 
56
74
  export function createForm(config: FormConfig) {
75
+ const M: FormMessages = { ...DEFAULT_FORM_MESSAGES, ...(config.messages ?? {}) }
57
76
  let values = $state<Record<string, any>>({ ...(config.initial ?? {}) })
58
77
  let errors = $state<Record<string, string>>({})
59
78
  let touched = $state<Record<string, boolean>>({})
@@ -85,7 +104,7 @@ export function createForm(config: FormConfig) {
85
104
  * rows (so within-row cross-field rules work). */
86
105
  function syncError(field: FormField, v: any, ctx: Record<string, any> = values): string | null {
87
106
  if (field.required && (v == null || v === '' || (Array.isArray(v) && !v.length)))
88
- return `${field.label} is required`
107
+ return M.required(field.label)
89
108
  if (field.rules) { const e = runRules(v, field.rules, ctx); if (e) return e }
90
109
  if (field.validate) { const e = field.validate(v, ctx); if (e) return e }
91
110
  return null
@@ -128,6 +147,8 @@ export function createForm(config: FormConfig) {
128
147
  function validateField(name: string): boolean {
129
148
  const field = fieldOf(name)
130
149
  if (!field) return true
150
+ // Computed fields are derived + read-only - never validated, never block submit.
151
+ if (field.computed) { setError(name, null); return true }
131
152
  // Hidden fields never block submit; clear any stale error + pending check.
132
153
  if (!isVisible(name)) { setError(name, null); cancelAsync(name); return true }
133
154
  if (field.type === 'array') return validateArray(field)
@@ -196,9 +217,9 @@ export function createForm(config: FormConfig) {
196
217
  const name = field.name
197
218
  const items = arrayItems(name)
198
219
  let arrErr: string | null = null
199
- if (field.required && items.length === 0) arrErr = `${field.label} is required`
200
- else if (field.minItems != null && items.length < field.minItems) arrErr = `Add at least ${field.minItems} item(s)`
201
- else if (field.maxItems != null && items.length > field.maxItems) arrErr = `At most ${field.maxItems} item(s)`
220
+ if (field.required && items.length === 0) arrErr = M.required(field.label)
221
+ else if (field.minItems != null && items.length < field.minItems) arrErr = M.minItems(field.label, field.minItems)
222
+ else if (field.maxItems != null && items.length > field.maxItems) arrErr = M.maxItems(field.label, field.maxItems)
202
223
  setError(name, arrErr)
203
224
  let ok = !arrErr
204
225
  clearNamespacedErrors(name) // rebuild fresh per-item errors
@@ -208,8 +229,23 @@ export function createForm(config: FormConfig) {
208
229
  return ok
209
230
  }
210
231
 
232
+ /** True when `field` depends on `name` (cascading). */
233
+ function dependsOnName(field: FormField, name: string): boolean {
234
+ const dep = field.dependsOn
235
+ return dep == null ? false : Array.isArray(dep) ? dep.includes(name) : dep === name
236
+ }
237
+
211
238
  function setValue(name: string, v: any) {
212
- values = { ...values, [name]: v }
239
+ let next = { ...values, [name]: v }
240
+ // Cascading: clear any fields that depend on this one so a stale child
241
+ // selection (e.g. a city under the old country) does not linger.
242
+ for (const f of flat()) {
243
+ if (dependsOnName(f, name) && next[f.name] != null) {
244
+ next = { ...next, [f.name]: null }
245
+ setError(f.name, null)
246
+ }
247
+ }
248
+ values = next
213
249
  config.onChange?.(values)
214
250
  // Re-validate live once the field has been blurred at least once.
215
251
  if (touched[name]) validateField(name)
@@ -240,6 +276,8 @@ export function createForm(config: FormConfig) {
240
276
  // Exclude hidden fields from the submitted payload; keep any non-field keys.
241
277
  const payload = { ...values }
242
278
  for (const f of flat()) if (!isVisible(f.name)) delete payload[f.name]
279
+ // Inject computed (derived) values so the payload carries them.
280
+ for (const f of flat()) if (f.computed && isVisible(f.name)) payload[f.name] = f.computed(values)
243
281
  submitting = true
244
282
  try {
245
283
  await config.onSubmit?.(payload)
@@ -294,6 +332,17 @@ export function createForm(config: FormConfig) {
294
332
 
295
333
  const isFieldDirty = (name: string) => !eq(values[name], baseline[name])
296
334
 
335
+ /** Top-level field errors (visible fields only), for a form-level summary. */
336
+ function errorList(): { name: string; label: string; message: string }[] {
337
+ const out: { name: string; label: string; message: string }[] = []
338
+ for (const f of flat()) {
339
+ const m = errors[f.name]
340
+ if (m && isVisible(f.name)) out.push({ name: f.name, label: f.label, message: m })
341
+ }
342
+ return out
343
+ }
344
+ const firstErrorField = (): string | undefined => errorList()[0]?.name
345
+
297
346
  return {
298
347
  get values() { return values },
299
348
  get submitting() { return submitting },
@@ -302,7 +351,7 @@ export function createForm(config: FormConfig) {
302
351
  for (const k of keys) if (!eq(values[k], baseline[k])) return true
303
352
  return false
304
353
  },
305
- value: (name: string) => values[name],
354
+ value: (name: string) => { const f = fieldOf(name); return f?.computed ? f.computed(values) : values[name] },
306
355
  error: (name: string): string | undefined => errors[name],
307
356
  isTouched: (name: string) => !!touched[name],
308
357
  hasError: (name: string) => !!errors[name],
@@ -327,6 +376,10 @@ export function createForm(config: FormConfig) {
327
376
  submit,
328
377
  reset,
329
378
  setErrors,
379
+ errorList,
380
+ firstErrorField,
381
+ /** The resolved message strings (defaults merged with `config.messages`). */
382
+ messages: M,
330
383
  }
331
384
  }
332
385