@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
@@ -12,18 +12,30 @@
12
12
  * a flat `FormField[]` or mix in titled `FormSection`s; with `stepper` the
13
13
  * sections become a validated wizard. Emits `onSubmit(values)` only when valid.
14
14
  */
15
+ import { untrack } from 'svelte'
15
16
  import SvNumberInput from './SvNumberInput.svelte'
16
17
  import SvPasswordInput from './SvPasswordInput.svelte'
17
18
  import SvDropDownList from './SvDropDownList.svelte'
19
+ import SvMultiSelect from './SvMultiSelect.svelte'
18
20
  import SvCheckBox from './SvCheckBox.svelte'
19
21
  import SvSwitchButton from './SvSwitchButton.svelte'
20
22
  import SvDateTimePicker from './SvDateTimePicker.svelte'
21
23
  import SvColorInput from './SvColorInput.svelte'
22
24
  import SvRating from './SvRating.svelte'
25
+ import SvRadioGroup from './SvRadioGroup.svelte'
26
+ import SvSlider from './SvSlider.svelte'
27
+ import SvTagsInput from './SvTagsInput.svelte'
28
+ import SvPhoneInput from './SvPhoneInput.svelte'
29
+ import SvCountryInput from './SvCountryInput.svelte'
30
+ import SvMaskedInput from './SvMaskedInput.svelte'
31
+ import SvComboBox from './SvComboBox.svelte'
32
+ import SvFileUpload from './SvFileUpload.svelte'
23
33
  import SvButton from './SvButton.svelte'
24
34
  import SvStepper from './SvStepper.svelte'
35
+ import SvTabs from './SvTabs.svelte'
25
36
  import { isFormSection, type FormField, type FormEntry, type FormSection } from './form-field'
26
- import { createForm } from './createForm.svelte'
37
+ import { createForm, type FormMessages } from './createForm.svelte'
38
+ import type { EditorDir } from './editor-contract'
27
39
 
28
40
  type Props = {
29
41
  fields: ReadonlyArray<FormEntry>
@@ -41,25 +53,97 @@
41
53
  resetLabel?: string
42
54
  /** Render titled sections as a validated multi-step wizard (requires sections). */
43
55
  stepper?: boolean
56
+ /** Render titled sections as tabs (requires sections; ignored with `stepper`). */
57
+ tabs?: boolean
44
58
  /** Columns in the responsive field grid (a section can override its own). */
45
59
  columns?: number
46
60
  disabled?: boolean
61
+ /** Inject server-side errors (`{ field: message }`); applied reactively. */
62
+ serverErrors?: Record<string, string>
63
+ /** A form-level error message shown in a banner above the fields. */
64
+ formError?: string
65
+ /** Show a summary of field errors (with focus links) above the form after a
66
+ * failed submit. */
67
+ errorSummary?: boolean
68
+ /** Heading for the error summary. Default "Please fix the following:". */
69
+ errorSummaryTitle?: string
70
+ /**
71
+ * Re-seed the form when the `initial` object identity changes (e.g. editing a
72
+ * different record). `off` (default) seeds once; `always` re-seeds on every
73
+ * change; `ifPristine` re-seeds only when the user has no unsaved edits.
74
+ */
75
+ reinitialize?: 'off' | 'always' | 'ifPristine'
76
+ /** Text direction; `rtl` mirrors the form layout. */
77
+ dir?: EditorDir
78
+ /** Override the built-in generated strings (required / min-max items / checking). */
79
+ messages?: Partial<FormMessages>
47
80
  }
48
81
 
49
82
  let {
50
83
  fields, initial = {}, onSubmit, onChange, onCancel,
51
84
  submitLabel = 'Submit', cancelLabel = 'Cancel', showReset = false, resetLabel = 'Reset',
52
- stepper = false, columns = 1, disabled = false,
85
+ stepper = false, tabs = false, columns = 1, disabled = false,
86
+ serverErrors, formError, errorSummary = false, errorSummaryTitle = 'Please fix the following:',
87
+ reinitialize = 'off', dir, messages,
53
88
  }: Props = $props()
54
89
 
55
- const form = createForm({ fields: () => fields, initial, onSubmit, onChange })
56
- const submit = (e: Event) => { e.preventDefault(); form.submit() }
90
+ const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
91
+ const form = createForm({ fields: () => fields, initial, onSubmit, onChange, messages })
92
+ async function submit(e: Event) {
93
+ e.preventDefault()
94
+ const ok = await form.submit()
95
+ // In tabs mode, surface the first tab that has an error so it is not hidden.
96
+ if (!ok && tabbed) {
97
+ const first = form.firstErrorField()
98
+ const s = first ? sections.find((sec) => sec.fields.some((f) => f.name === first)) : undefined
99
+ if (s) activeTab = s.section
100
+ }
101
+ }
102
+
103
+ let formEl = $state<HTMLFormElement | null>(null)
104
+ // Apply server-side errors reactively (e.g. after a failed API submit). The
105
+ // write is untracked so reading `errors` inside setErrors does not make this
106
+ // effect depend on (and re-run from) its own write.
107
+ $effect(() => {
108
+ const se = serverErrors
109
+ if (se) untrack(() => form.setErrors(se))
110
+ })
111
+
112
+ // Opt-in re-seed when the `initial` identity changes. Only `initial` is tracked
113
+ // (read outside `untrack`); the isDirty check + reset run untracked so this
114
+ // never re-fires from its own writes.
115
+ let seeded = false
116
+ $effect(() => {
117
+ const init = initial
118
+ untrack(() => {
119
+ if (!seeded) { seeded = true; return } // the seed-once mount pass
120
+ if (reinitialize === 'off') return
121
+ if (reinitialize === 'ifPristine' && form.isDirty) return
122
+ form.reset(init)
123
+ })
124
+ })
125
+
126
+ const summaryItems = $derived(errorSummary ? form.errorList() : [])
127
+ function focusField(name: string) {
128
+ const wrap = formEl?.querySelector<HTMLElement>(`[data-field="${name}"]`)
129
+ wrap?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
130
+ wrap?.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]')?.focus()
131
+ }
57
132
  const dateVal = (v: any): Date | null => (v instanceof Date ? v : v ? new Date(v) : null)
133
+ // Resolve a field's options, running the function form (cascading) against the
134
+ // live values so a child list updates when its parent changes.
135
+ const resolveOptions = (field: FormField) =>
136
+ typeof field.options === 'function' ? field.options(form.values) : (field.options ?? [])
58
137
 
59
138
  const sections = $derived(fields.filter(isFormSection) as FormSection[])
60
139
  const wizard = $derived(stepper && sections.length > 0)
140
+ const tabbed = $derived(tabs && !stepper && sections.length > 0)
61
141
  const stepItems = $derived(sections.map((s) => ({ label: s.section, description: s.description })))
62
142
 
143
+ let activeTab = $state<string | undefined>(undefined)
144
+ const sectionHasError = (s: FormSection) => s.fields.some((f) => form.error(f.name))
145
+ const tabItems = $derived(sections.map((s) => ({ id: s.section, label: sectionHasError(s) ? `${s.section} ⚠` : s.section })))
146
+
63
147
  let step = $state(0)
64
148
  async function next() {
65
149
  const s = sections[step]
@@ -79,32 +163,57 @@
79
163
  onChange: (v: any) => void
80
164
  onBlur?: () => void
81
165
  disabled: boolean
166
+ readonly?: boolean
167
+ invalid?: boolean
168
+ describedby?: string
82
169
  placeholder?: string
83
170
  options?: ReadonlyArray<{ value: string | number; label: string; color?: string }>
171
+ /** The source field, for type-specific extras (mask, min/max, loadOptions...). */
172
+ field?: FormField
84
173
  }
85
174
  </script>
86
175
 
87
176
  {#snippet control(o: ControlOpts)}
88
177
  {#if o.type === 'textarea'}
89
- <textarea id={o.id} class="sv-form__control sv-form__textarea" rows="3" disabled={o.disabled} placeholder={o.placeholder} value={o.value ?? ''} oninput={(e) => o.onChange(e.currentTarget.value)} onblur={o.onBlur}></textarea>
178
+ <textarea id={o.id} class="sv-form__control sv-form__textarea" rows="3" disabled={o.disabled} readonly={o.readonly} placeholder={o.placeholder} value={o.value ?? ''} aria-invalid={o.invalid ? 'true' : undefined} aria-describedby={o.describedby} oninput={(e) => o.onChange(e.currentTarget.value)} onblur={o.onBlur}></textarea>
90
179
  {:else if o.type === 'number'}
91
- <SvNumberInput value={o.value ?? null} disabled={o.disabled} onChange={o.onChange} />
180
+ <SvNumberInput id={o.id} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block min={o.field?.min ?? -Infinity} max={o.field?.max ?? Infinity} step={o.field?.step ?? 1} precision={o.field?.precision} prefix={o.field?.prefix ?? ''} suffix={o.field?.suffix ?? ''} onChange={o.onChange} />
92
181
  {:else if o.type === 'password'}
93
- <SvPasswordInput value={o.value ?? ''} disabled={o.disabled} onChange={o.onChange} />
182
+ <SvPasswordInput id={o.id} value={o.value ?? ''} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block onChange={o.onChange} />
183
+ {:else if o.type === 'mask'}
184
+ <SvMaskedInput id={o.id} value={o.value ?? ''} mask={o.field?.mask ?? ''} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block placeholder={o.placeholder} onChange={(m) => o.onChange(m)} />
185
+ {:else if o.type === 'phone'}
186
+ <SvPhoneInput id={o.id} value={o.value ?? ''} disabled={o.disabled} invalid={o.invalid} block placeholder={o.placeholder} onChange={(v) => o.onChange(v)} />
187
+ {:else if o.type === 'country'}
188
+ <SvCountryInput id={o.id} value={o.value ?? null} disabled={o.disabled} invalid={o.invalid} onChange={o.onChange} />
94
189
  {:else if o.type === 'select'}
95
- <SvDropDownList options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} placeholder={o.placeholder} onChange={o.onChange} />
190
+ <SvDropDownList id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
191
+ {:else if o.type === 'combobox'}
192
+ <SvComboBox id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} clearable placeholder={o.placeholder} loadOptions={o.field?.loadOptions ? (q) => o.field!.loadOptions!(q, form.values) : undefined} onChange={o.onChange} />
193
+ {:else if o.type === 'multiselect'}
194
+ <SvMultiSelect id={o.id} options={o.options ?? []} value={Array.isArray(o.value) ? o.value : []} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
195
+ {:else if o.type === 'radio'}
196
+ <SvRadioGroup id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} orientation="horizontal" onChange={o.onChange} />
197
+ {:else if o.type === 'tags'}
198
+ <SvTagsInput id={o.id} value={Array.isArray(o.value) ? o.value : []} disabled={o.disabled} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
199
+ {:else if o.type === 'slider'}
200
+ <SvSlider id={o.id} value={o.value ?? o.field?.min ?? 0} min={o.field?.min ?? 0} max={o.field?.max ?? 100} step={o.field?.step ?? 1} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} showValue onChange={o.onChange} />
96
201
  {:else if o.type === 'checkbox'}
97
- <SvCheckBox checked={!!o.value} disabled={o.disabled} onChange={o.onChange}>{o.label}</SvCheckBox>
202
+ <SvCheckBox checked={!!o.value} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} onChange={o.onChange}>{o.label}</SvCheckBox>
98
203
  {:else if o.type === 'switch'}
99
- <div class="sv-form__inline"><SvSwitchButton checked={!!o.value} disabled={o.disabled} onChange={o.onChange} ariaLabel={o.label} /><span>{o.label}</span></div>
204
+ <div class="sv-form__inline"><SvSwitchButton checked={!!o.value} disabled={o.disabled} readonly={o.readonly} onChange={o.onChange} ariaLabel={o.label} /><span>{o.label}</span></div>
100
205
  {:else if o.type === 'date'}
101
206
  <SvDateTimePicker value={dateVal(o.value)} dropDownDisplayMode="calendar" formatString="yyyy-MM-dd" disabled={o.disabled} onChange={o.onChange} />
207
+ {:else if o.type === 'datetime'}
208
+ <SvDateTimePicker value={dateVal(o.value)} dropDownDisplayMode="both" disabled={o.disabled} onChange={o.onChange} />
102
209
  {:else if o.type === 'color'}
103
- <SvColorInput value={o.value ?? '#3b82f6'} disabled={o.disabled} onChange={o.onChange} />
210
+ <SvColorInput id={o.id} value={o.value ?? '#3b82f6'} disabled={o.disabled} invalid={o.invalid} onChange={o.onChange} />
211
+ {:else if o.type === 'file'}
212
+ <SvFileUpload accept={o.field?.accept} multiple={o.field?.multiple ?? false} onChange={o.onChange} />
104
213
  {:else if o.type === 'rating'}
105
- <SvRating value={o.value ?? 0} disabled={o.disabled} onChange={o.onChange} />
214
+ <SvRating id={o.id} value={o.value ?? 0} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} onChange={o.onChange} />
106
215
  {:else}
107
- <input id={o.id} class="sv-form__control" type={o.type} disabled={o.disabled} placeholder={o.placeholder} value={o.value ?? ''} oninput={(e) => o.onChange(e.currentTarget.value)} onblur={o.onBlur} />
216
+ <input id={o.id} class="sv-form__control" type={o.type} disabled={o.disabled} readonly={o.readonly} placeholder={o.placeholder} value={o.value ?? ''} aria-invalid={o.invalid ? 'true' : undefined} aria-describedby={o.describedby} oninput={(e) => o.onChange(e.currentTarget.value)} onblur={o.onBlur} />
108
217
  {/if}
109
218
  {/snippet}
110
219
 
@@ -116,12 +225,13 @@
116
225
  {#if type !== 'checkbox' && type !== 'switch'}
117
226
  <label class="sv-form__label" for={id}>{itf.label}{#if itf.required}<span class="sv-form__req" aria-hidden="true"> *</span>{/if}</label>
118
227
  {/if}
119
- {@render control({ id, type, label: itf.label, value: form.itemValue(arrayName, i, itf.name), onChange: (v) => form.setItemValue(arrayName, i, itf.name, v), onBlur: () => form.handleItemBlur(arrayName, i, itf.name), disabled, placeholder: itf.placeholder, options: itf.options })}
120
- {#if err}<span class="sv-form__error" role="alert">{err}</span>{/if}
228
+ {@render control({ id, type, label: itf.label, value: form.itemValue(arrayName, i, itf.name), onChange: (v) => form.setItemValue(arrayName, i, itf.name, v), onBlur: () => form.handleItemBlur(arrayName, i, itf.name), disabled, readonly: itf.readonly, invalid: !!err, describedby: err ? `${id}__error` : undefined, placeholder: itf.placeholder, options: resolveOptions(itf), field: itf })}
229
+ {#if err}<span class="sv-form__error" id={`${id}__error`} role="alert">{err}</span>{/if}
121
230
  </div>
122
231
  {/snippet}
123
232
 
124
233
  {#snippet renderArray(field: FormField)}
234
+ {@const count = form.arrayItems(field.name).length}
125
235
  <div class="sv-form__field is-full sv-form__array" class:has-error={form.error(field.name)}>
126
236
  <label class="sv-form__label">{field.label}{#if field.required}<span class="sv-form__req" aria-hidden="true"> *</span>{/if}</label>
127
237
  {#each form.arrayItems(field.name) as _item, i (i)}
@@ -129,7 +239,15 @@
129
239
  <div class="sv-form__grid" style:--cols={field.itemFields?.length ?? 1}>
130
240
  {#each field.itemFields ?? [] as itf (itf.name)}{@render renderItemField(field.name, i, itf)}{/each}
131
241
  </div>
132
- <button type="button" class="sv-form__aremove" disabled={disabled} onclick={() => form.removeItem(field.name, i)} aria-label="Remove item">&times;</button>
242
+ <div class="sv-form__arow-tools">
243
+ {#if count > 1}
244
+ <div class="sv-form__amove">
245
+ <button type="button" class="sv-form__amove-btn" disabled={disabled || i === 0} onclick={() => form.moveItem(field.name, i, i - 1)} aria-label="Move up" title="Move up">&#8593;</button>
246
+ <button type="button" class="sv-form__amove-btn" disabled={disabled || i === count - 1} onclick={() => form.moveItem(field.name, i, i + 1)} aria-label="Move down" title="Move down">&#8595;</button>
247
+ </div>
248
+ {/if}
249
+ <button type="button" class="sv-form__aremove" disabled={disabled} onclick={() => form.removeItem(field.name, i)} aria-label="Remove item">&times;</button>
250
+ </div>
133
251
  </div>
134
252
  {/each}
135
253
  <div><SvButton type="button" variant="outline" size="sm" disabled={disabled} onclick={() => form.addItem(field.name)}>{field.addLabel ?? '+ Add'}</SvButton></div>
@@ -144,17 +262,19 @@
144
262
  {@render renderArray(field)}
145
263
  {:else}
146
264
  {@const fieldDisabled = disabled || form.isDisabled(field.name)}
147
- <div class="sv-form__field" class:is-full={field.full || cols === 1} class:has-error={form.error(field.name)}>
265
+ <div class="sv-form__field" data-field={field.name} class:is-full={(field.full || (field.span ?? 1) >= cols) || cols === 1} class:has-error={form.error(field.name)} style:grid-column={field.span && field.span > 1 && field.span < cols ? `span ${field.span}` : undefined}>
148
266
  {#if type !== 'checkbox' && type !== 'switch'}
149
267
  <label class="sv-form__label" for={`f-${field.name}`}>
150
268
  {field.label}{#if field.required}<span class="sv-form__req" aria-hidden="true"> *</span>{/if}
151
269
  </label>
152
270
  {/if}
153
- {@render control({ id: `f-${field.name}`, type, label: field.label, value: form.value(field.name), onChange: (v) => form.setValue(field.name, v), onBlur: () => form.handleBlur(field.name), disabled: fieldDisabled, placeholder: field.placeholder, options: field.options })}
271
+ {@render control({ id: `f-${field.name}`, type, label: field.label, value: form.value(field.name), onChange: (v) => form.setValue(field.name, v), onBlur: () => form.handleBlur(field.name), disabled: fieldDisabled, readonly: field.readonly || !!field.computed, invalid: !!form.error(field.name), describedby: form.error(field.name) ? `f-${field.name}__error` : undefined, placeholder: field.placeholder, options: resolveOptions(field), field })}
154
272
  {#if form.error(field.name)}
155
- <span class="sv-form__error" role="alert">{form.error(field.name)}</span>
273
+ <span class="sv-form__error" id={`f-${field.name}__error`} role="alert">{form.error(field.name)}</span>
156
274
  {:else if form.isValidating(field.name)}
157
- <span class="sv-form__checking" role="status"><span class="sv-form__spinner" aria-hidden="true"></span>Checking…</span>
275
+ <span class="sv-form__checking" role="status"><span class="sv-form__spinner" aria-hidden="true"></span>{form.messages.checking}</span>
276
+ {:else if field.help}
277
+ <span class="sv-form__help">{field.help}</span>
158
278
  {/if}
159
279
  </div>
160
280
  {/if}
@@ -179,7 +299,20 @@
179
299
  </div>
180
300
  {/snippet}
181
301
 
182
- <form class="sv-form" onsubmit={submit} novalidate>
302
+ <form class="sv-form" bind:this={formEl} dir={resolvedDir} onsubmit={submit} novalidate>
303
+ {#if formError}
304
+ <div class="sv-form__banner" role="alert">{formError}</div>
305
+ {/if}
306
+ {#if errorSummary && summaryItems.length}
307
+ <div class="sv-form__summary" role="alert">
308
+ <p class="sv-form__summary-title">{errorSummaryTitle}</p>
309
+ <ul>
310
+ {#each summaryItems as it (it.name)}
311
+ <li><button type="button" class="sv-form__summary-link" onclick={() => focusField(it.name)}>{it.label}: {it.message}</button></li>
312
+ {/each}
313
+ </ul>
314
+ </div>
315
+ {/if}
183
316
  {#if wizard}
184
317
  {@const s = sections[Math.min(step, sections.length - 1)]!}
185
318
  <SvStepper steps={stepItems} current={step} onChange={(n) => (step = n)} linear />
@@ -201,6 +334,17 @@
201
334
  <SvButton type="submit" loading={form.submitting} disabled={disabled || form.submitting}>{submitLabel}</SvButton>
202
335
  {/if}
203
336
  </div>
337
+ {:else if tabbed}
338
+ <SvTabs tabs={tabItems} bind:value={activeTab} ariaLabel="Form sections">
339
+ {#snippet panel(id)}
340
+ {@const s = sections.find((sec) => sec.section === id)}
341
+ {#if s}
342
+ {#if s.description}<p class="sv-form__section-desc">{s.description}</p>{/if}
343
+ {@render grid(s.fields, s.columns ?? columns)}
344
+ {/if}
345
+ {/snippet}
346
+ </SvTabs>
347
+ {@render actions()}
204
348
  {:else if sections.length > 0}
205
349
  {#each fields as entry (isFormSection(entry) ? entry.section : entry.name)}
206
350
  {#if isFormSection(entry)}
@@ -240,6 +384,24 @@
240
384
  .sv-form__field.has-error .sv-form__control { border-color: var(--sg-danger, #dc2626); }
241
385
  .sv-form__inline { display: flex; align-items: center; gap: 8px; font-size: 13px; }
242
386
  .sv-form__error { font-size: 11.5px; color: var(--sg-danger, #dc2626); }
387
+ .sv-form__help { font-size: 11.5px; color: var(--sg-muted, #64748b); }
388
+ .sv-form__banner {
389
+ padding: 10px 12px; border-radius: 8px; font-size: 13px;
390
+ background: color-mix(in srgb, var(--sg-danger, #dc2626) 10%, transparent);
391
+ color: var(--sg-danger, #dc2626); border: 1px solid color-mix(in srgb, var(--sg-danger, #dc2626) 35%, transparent);
392
+ }
393
+ .sv-form__summary {
394
+ padding: 12px 14px; border-radius: 8px;
395
+ background: color-mix(in srgb, var(--sg-danger, #dc2626) 7%, transparent);
396
+ border: 1px solid color-mix(in srgb, var(--sg-danger, #dc2626) 30%, transparent);
397
+ }
398
+ .sv-form__summary-title { margin: 0 0 6px; font-size: 13px; font-weight: 700; color: var(--sg-danger, #dc2626); }
399
+ .sv-form__summary ul { margin: 0; padding-inline-start: 18px; display: flex; flex-direction: column; gap: 3px; }
400
+ .sv-form__summary-link {
401
+ background: none; border: 0; padding: 0; font: inherit; font-size: 12.5px; text-align: start;
402
+ color: var(--sg-danger, #dc2626); cursor: pointer; text-decoration: underline;
403
+ }
404
+ .sv-form__summary-link:hover { color: var(--sg-fg, #0f172a); }
243
405
  .sv-form__checking { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--sg-muted, #64748b); }
244
406
  .sv-form__spinner {
245
407
  width: 11px; height: 11px; border-radius: 50%;
@@ -260,4 +422,16 @@
260
422
  }
261
423
  .sv-form__aremove:hover:not(:disabled) { color: var(--sg-danger, #dc2626); border-color: var(--sg-danger, #dc2626); }
262
424
  .sv-form__aremove:disabled { opacity: 0.5; cursor: default; }
425
+ .sv-form__arow-tools { display: flex; align-items: center; gap: 4px; margin-top: 10px; }
426
+ .sv-form__amove { display: inline-flex; flex-direction: column; }
427
+ .sv-form__amove-btn {
428
+ width: 22px; height: 14px; display: grid; place-items: center; padding: 0; cursor: pointer;
429
+ border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff); color: var(--sg-muted, #64748b);
430
+ font-size: 10px; line-height: 1;
431
+ }
432
+ .sv-form__amove-btn:first-child { border-radius: 6px 6px 0 0; border-bottom: 0; }
433
+ .sv-form__amove-btn:last-child { border-radius: 0 0 6px 6px; }
434
+ .sv-form__amove-btn:hover:not(:disabled) { color: var(--sg-accent, #2563eb); }
435
+ .sv-form__amove-btn:disabled { opacity: 0.4; cursor: default; }
436
+ .sv-form__arow-tools .sv-form__aremove { margin-top: 0; }
263
437
  </style>
@@ -1,5 +1,7 @@
1
1
  export type { FormField, FormFieldType, FormSection, FormEntry } from './form-field';
2
2
  import { type FormEntry } from './form-field';
3
+ import { type FormMessages } from './createForm.svelte';
4
+ import type { EditorDir } from './editor-contract';
3
5
  type Props = {
4
6
  fields: ReadonlyArray<FormEntry>;
5
7
  initial?: Record<string, any>;
@@ -16,9 +18,30 @@ type Props = {
16
18
  resetLabel?: string;
17
19
  /** Render titled sections as a validated multi-step wizard (requires sections). */
18
20
  stepper?: boolean;
21
+ /** Render titled sections as tabs (requires sections; ignored with `stepper`). */
22
+ tabs?: boolean;
19
23
  /** Columns in the responsive field grid (a section can override its own). */
20
24
  columns?: number;
21
25
  disabled?: boolean;
26
+ /** Inject server-side errors (`{ field: message }`); applied reactively. */
27
+ serverErrors?: Record<string, string>;
28
+ /** A form-level error message shown in a banner above the fields. */
29
+ formError?: string;
30
+ /** Show a summary of field errors (with focus links) above the form after a
31
+ * failed submit. */
32
+ errorSummary?: boolean;
33
+ /** Heading for the error summary. Default "Please fix the following:". */
34
+ errorSummaryTitle?: string;
35
+ /**
36
+ * Re-seed the form when the `initial` object identity changes (e.g. editing a
37
+ * different record). `off` (default) seeds once; `always` re-seeds on every
38
+ * change; `ifPristine` re-seeds only when the user has no unsaved edits.
39
+ */
40
+ reinitialize?: 'off' | 'always' | 'ifPristine';
41
+ /** Text direction; `rtl` mirrors the form layout. */
42
+ dir?: EditorDir;
43
+ /** Override the built-in generated strings (required / min-max items / checking). */
44
+ messages?: Partial<FormMessages>;
22
45
  };
23
46
  declare const SvForm: import("svelte").Component<Props, {}, "">;
24
47
  type SvForm = ReturnType<typeof SvForm>;
@@ -12,8 +12,9 @@ import { getColumnBaseValue, isGroupRow, toolPanelHeaderLabel, formatSummaryNume
12
12
  * the component lifecycle) and consumed through the returned getters.
13
13
  */
14
14
  export type SvGridController<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData> = ReturnType<typeof createSvGridController<TFeatures, TData>>;
15
- export declare function createSvGridController<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData>(props: Props<TFeatures, TData>): {
15
+ export declare function createSvGridController<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData>(rawProps: Props<TFeatures, TData>): {
16
16
  readonly props: Props<TFeatures, TData>;
17
+ optionOverrides: Record<string, unknown>;
17
18
  readonly editingEnabled: boolean;
18
19
  readonly paginationEnabled: boolean;
19
20
  readonly groupingControlsEnabled: boolean;
@@ -283,6 +284,16 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
283
284
  readonly toolPanelHeaderLabel: typeof toolPanelHeaderLabel;
284
285
  readonly toggleColumnVisibleInPanel: (columnId: string) => void;
285
286
  readonly moveColumnInPanel: (columnId: string, dir: -1 | 1) => void;
287
+ readonly messages: import("./grid-messages").GridMessages;
288
+ readonly pivotConfig: import("./pivot-view.svelte").GridPivotConfig<TData> | null;
289
+ readonly pivotActive: boolean;
290
+ readonly pivotResult: {
291
+ rows: import("./pivot-view.svelte").GridPivotRow[];
292
+ columns: ColumnDef<TableFeatures, import("./pivot-view.svelte").GridPivotRow>[];
293
+ } | null;
294
+ readonly pivotModeOn: boolean;
295
+ readonly hasPivotEngine: boolean;
296
+ togglePivotMode: () => void;
286
297
  readonly chartingEnabled: boolean;
287
298
  readonly chartingConfig: {
288
299
  position: "right" | "bottom";
@@ -1,4 +1,4 @@
1
- import { applyExcelFilter, normalizeForFilter, createColumnVirtualizer, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createSvelteVirtualizer, createSortedRowModel, createSvGrid, getGridCellDomId, rowsToChartSpec, sortFns, } from "./index";
1
+ import { applyExcelFilter, normalizeForFilter, createColumnVirtualizer, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createSvelteVirtualizer, createSortedRowModel, createSvGrid, getGridCellDomId, sortFns, } from "./index";
2
2
  import { createRowScrollScaling, resolveMaxDomHeight, } from "./virtualization/scroll-scaling";
3
3
  import "./sv-grid-scrollbar";
4
4
  import { computeColumnStat, formatsNeedingStats, formatNeedsStats, } from "./conditional-formatting";
@@ -18,6 +18,8 @@ import { resolveColumnTypes, } from "./column-types";
18
18
  import { computeColumnGroupMeta, hiddenLeavesForCollapse, } from "./column-groups";
19
19
  import { createGridApi, } from "./build-api";
20
20
  import { createClipboard, } from "./clipboard";
21
+ import { resolveGridMessages } from "./grid-messages";
22
+ import { getPivotEngine, hasPivotEngine } from "./pivot-view.svelte";
21
23
  import { filterOperatorOptions, fallbackOperatorOption, TEXT_OPERATORS, NUMBER_OPERATORS, DATE_OPERATORS, CHECKBOX_OPERATORS, operatorOption, operatorsForColumn, defaultOperatorFor, operatorLabelFor, } from "./filter-operators";
22
24
  import { isBucketableColumn, buildBuckets, isInBucket, } from "./facet-buckets";
23
25
  import { getColumnBaseValue, isGroupRow, toolPanelHeaderLabel, formatSummaryNumeric, getColumnAlign, getPinnedCellValue, getColumnAccessorValue, columnDefMatchesId, } from "./cell-values";
@@ -143,7 +145,26 @@ function observeSizeRaf(el, cb) {
143
145
  observer.disconnect();
144
146
  };
145
147
  }
146
- export function createSvGridController(props) {
148
+ export function createSvGridController(rawProps) {
149
+ // Runtime option overrides set via the imperative api (`api.setOption(key, value)`).
150
+ // Every controller read of a prop goes through the `props` proxy below (and the view
151
+ // reads controller-resolved gates like `_features` / `editingEnabled`, not the raw
152
+ // prop), so an override wins over the incoming prop and re-renders reactively -
153
+ // identical to the parent flipping that prop. A get-only Proxy (not a fresh spread)
154
+ // preserves reference identity for non-overridden keys, which the columns-identity
155
+ // column cache and the `data`/`columns` sync effects rely on.
156
+ let optionOverrides = $state({});
157
+ const props = new Proxy(rawProps, {
158
+ // Read the override as a PROPERTY (tracked by `$state`), not via `in` (the `has`
159
+ // trap is not tracked) - so a reactive read of `props.X` re-runs when an override
160
+ // is set. `undefined` means "no override" and falls back to the incoming prop;
161
+ // `setOption(key, undefined)` clears by removing the key, which lands here as
162
+ // undefined too. Whole-object reassignment on write makes the signal fire.
163
+ get: (t, k) => {
164
+ const v = typeof k === "string" ? optionOverrides[k] : undefined;
165
+ return v !== undefined ? v : Reflect.get(t, k);
166
+ },
167
+ });
147
168
  // Resolved capability gates. Capabilities are OFF by default - a bare
148
169
  // grid is a plain read-only table, and each power feature is opted into
149
170
  // via its shortcut (`editable` / `pageable` / `groupable`) or the matching
@@ -466,7 +487,8 @@ export function createSvGridController(props) {
466
487
  state: {
467
488
  columnFilters: [],
468
489
  grouping: [],
469
- sorting: [],
490
+ // svelte-ignore state_referenced_locally
491
+ sorting: props.initialSorting ?? [],
470
492
  // svelte-ignore state_referenced_locally
471
493
  pagination: { pageIndex: 0, pageSize: props.pageSize ?? 10 },
472
494
  rowSelection: {},
@@ -808,10 +830,10 @@ export function createSvGridController(props) {
808
830
  return rawRows;
809
831
  let rows = rawRows;
810
832
  if (globalFilter.trim()) {
811
- const needle = normalizeForFilter(globalFilter, props.filterLocale);
833
+ const needle = normalizeForFilter(globalFilter, (props.filterLocale ?? props.localization?.locale));
812
834
  rows = rows.filter((row) => row
813
835
  .getAllCells()
814
- .some((cell) => normalizeForFilter(String(cell.getValue() ?? ""), props.filterLocale)
836
+ .some((cell) => normalizeForFilter(String(cell.getValue() ?? ""), (props.filterLocale ?? props.localization?.locale))
815
837
  .includes(needle)));
816
838
  }
817
839
  // A single condition is "active" if it has the value(s) it needs.
@@ -822,7 +844,7 @@ export function createSvGridController(props) {
822
844
  return value.trim().length > 0 && (valueTo ?? "").trim().length > 0;
823
845
  return value.trim().length > 0;
824
846
  };
825
- const evalCond = (cellValue, columnId, op, value, valueTo) => applyExcelFilter(cellValue, { id: columnId, operator: op, value, valueTo: op === "between" ? valueTo : undefined }, { locale: props.filterLocale });
847
+ const evalCond = (cellValue, columnId, op, value, valueTo) => applyExcelFilter(cellValue, { id: columnId, operator: op, value, valueTo: op === "between" ? valueTo : undefined }, { locale: (props.filterLocale ?? props.localization?.locale) });
826
848
  // A column filter is active if either of its (up to two) conditions is.
827
849
  const menuFilters = Object.entries(filterMenuValues).filter(([_, f]) => {
828
850
  const a = condActive(f.operator, f.value, f.valueTo);
@@ -1048,6 +1070,39 @@ export function createSvGridController(props) {
1048
1070
  return c === true ? {} : c;
1049
1071
  });
1050
1072
  const chartingEnabled = $derived(!!chartCfg);
1073
+ // Resolved chrome messages (English defaults merged with `localeText`). One
1074
+ // map every consumer (SvGrid.svelte, GridFooter, menus, filter labels) reads.
1075
+ const gridMessages = $derived(resolveGridMessages(props.localization?.text));
1076
+ // ---- In-grid pivot mode ----
1077
+ // The pivot ENGINE ships in @svgrid/enterprise (registered via the pivot-view
1078
+ // seam). When `pivot` is set and the engine is present, the grid runs it over
1079
+ // the already filtered + sorted leaf rows and renders the pivot result in
1080
+ // place. `pivotModeOn` is uncontrolled (seeded from the prop) so a toolbar
1081
+ // toggle can flip between the pivot and the flat table over the same data.
1082
+ const pivotConfig = $derived(props.pivot ?? null);
1083
+ // Controlled when `pivotMode` prop is provided; otherwise internal state
1084
+ // (seeded to on when `pivot` is set) drives it, flipped by the toolbar toggle.
1085
+ let pivotModeInternal = $state(props.pivotMode ?? (props.pivot != null));
1086
+ const pivotModeOn = $derived(props.pivotMode ?? pivotModeInternal);
1087
+ function togglePivotMode() {
1088
+ const next = !pivotModeOn;
1089
+ pivotModeInternal = next;
1090
+ props.onPivotModeChange?.(next);
1091
+ }
1092
+ const pivotActive = $derived(!!pivotConfig && pivotModeOn && hasPivotEngine());
1093
+ const pivotResult = $derived.by(() => {
1094
+ if (!pivotActive || !pivotConfig)
1095
+ return null;
1096
+ const engine = getPivotEngine();
1097
+ if (!engine)
1098
+ return null;
1099
+ // Feed the pivot the filtered + sorted LEAF rows (drop any group rows); its
1100
+ // output must not be re-run through the column-filter pipeline.
1101
+ const leaves = allRowsBeforePagination
1102
+ .filter((r) => !r.subRows)
1103
+ .map((r) => r.original);
1104
+ return engine(leaves, pivotConfig);
1105
+ });
1051
1106
  const chartIsCustom = $derived(typeof chartCfg?.buildSpec === "function");
1052
1107
  const chartingConfig = $derived(chartCfg
1053
1108
  ? {
@@ -1300,6 +1355,17 @@ export function createSvGridController(props) {
1300
1355
  })),
1301
1356
  };
1302
1357
  };
1358
+ // The chart derivation engine (rowsToChartSpec) is loaded lazily the first time
1359
+ // charting is enabled, keeping chart.ts (~11 KB gzip) out of the base <SvGrid>
1360
+ // bundle - it rides in the same lazy chunk as the chart panel. `chartSpec` stays
1361
+ // null until it resolves, a tick after the panel itself starts loading (which it
1362
+ // already does). The `getAggregate` server path uses the local bucketsToSpec and
1363
+ // does not need the engine.
1364
+ let chartEngine = $state(null);
1365
+ $effect(() => {
1366
+ if (chartingEnabled && !chartEngine)
1367
+ import("./chart").then((m) => (chartEngine = m.rowsToChartSpec));
1368
+ });
1303
1369
  const chartSpec = $derived.by(() => {
1304
1370
  if (!chartingEnabled || !chartCfg)
1305
1371
  return null;
@@ -1327,8 +1393,11 @@ export function createSvGridController(props) {
1327
1393
  const values = effectiveChartMeasureIds.map((id) => fieldOf(id)).filter((f) => !!f);
1328
1394
  if (!category || !values.length)
1329
1395
  return null;
1396
+ // Engine not loaded yet (lazy): render nothing until it resolves.
1397
+ if (!chartEngine)
1398
+ return null;
1330
1399
  const seriesField = fieldOf(effectiveChartSeriesId);
1331
- const spec = rowsToChartSpec(chartRows, {
1400
+ const spec = chartEngine(chartRows, {
1332
1401
  type: chartType,
1333
1402
  category: category,
1334
1403
  value: values.length === 1 ? values[0] : values,
@@ -2142,6 +2211,8 @@ export function createSvGridController(props) {
2142
2211
  });
2143
2212
  const ctx = {
2144
2213
  get props() { return props; },
2214
+ get optionOverrides() { return optionOverrides; },
2215
+ set optionOverrides(v) { optionOverrides = v; },
2145
2216
  get editingEnabled() { return editingEnabled; },
2146
2217
  get paginationEnabled() { return paginationEnabled; },
2147
2218
  get groupingControlsEnabled() { return groupingControlsEnabled; },
@@ -2379,6 +2450,15 @@ export function createSvGridController(props) {
2379
2450
  get toolPanelHeaderLabel() { return toolPanelHeaderLabel; },
2380
2451
  get toggleColumnVisibleInPanel() { return toggleColumnVisibleInPanel; },
2381
2452
  get moveColumnInPanel() { return moveColumnInPanel; },
2453
+ // ---- Localized chrome strings ----
2454
+ get messages() { return gridMessages; },
2455
+ // ---- In-grid pivot mode ----
2456
+ get pivotConfig() { return pivotConfig; },
2457
+ get pivotActive() { return pivotActive; },
2458
+ get pivotResult() { return pivotResult; },
2459
+ get pivotModeOn() { return pivotModeOn; },
2460
+ get hasPivotEngine() { return hasPivotEngine(); },
2461
+ togglePivotMode,
2382
2462
  // ---- Integrated charting ----
2383
2463
  get chartingEnabled() { return chartingEnabled; },
2384
2464
  get chartingConfig() { return chartingConfig; },