@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
@@ -31,6 +31,7 @@
31
31
  import type { MenuItem } from './menu-item'
32
32
  import type { EditorDir } from './editor-contract'
33
33
  import { enabledIndices, wrapMove } from './list-nav'
34
+ import { createTypeaheadBuffer, isTypeaheadKey } from './list-option'
34
35
 
35
36
  /** Reactive inputs are getters so the core tracks live prop changes (the same
36
37
  * controlled pattern as `createListbox`). */
@@ -76,6 +77,22 @@ export function createMenu(config: MenuConfig) {
76
77
  let active = $state(-1)
77
78
  let openSub = $state(-1)
78
79
 
80
+ // Type-ahead: printable keys jump to the next item whose label starts with the
81
+ // accumulated buffer (WAI-ARIA menu behavior), wrapping past the active item.
82
+ const typeahead = createTypeaheadBuffer()
83
+ function typeaheadMatch(buffer: string): number {
84
+ const b = buffer.trim().toLowerCase()
85
+ if (!b) return -1
86
+ const its = items()
87
+ const n = its.length
88
+ for (let k = 1; k <= n; k++) {
89
+ const i = (active + k + n) % n
90
+ const it = its[i]
91
+ if (it && !it.disabled && !it.separator && (it.label ?? '').toLowerCase().startsWith(b)) return i
92
+ }
93
+ return -1
94
+ }
95
+
79
96
  // Real (non-separator, non-disabled) item indices, for roving focus.
80
97
  const focusables = $derived(
81
98
  enabledIndices(items().length, (i) => {
@@ -153,6 +170,12 @@ export function createMenu(config: MenuConfig) {
153
170
  // Tab leaves the menu entirely (WAI-ARIA menu-button pattern): close the
154
171
  // whole tree rather than trapping focus inside a portalled panel.
155
172
  case 'Tab': config.onClose(); break
173
+ default:
174
+ // Type-ahead: a bare printable key jumps to the matching item.
175
+ if (isTypeaheadKey(e)) {
176
+ const idx = typeaheadMatch(typeahead.push(e.key))
177
+ if (idx >= 0) { e.preventDefault(); focus(idx) }
178
+ }
156
179
  }
157
180
  }
158
181
 
@@ -23,6 +23,8 @@ export type PopoverSelectConfig = {
23
23
  itemCount: () => number
24
24
  /** Whether item `i` is disabled (skipped by roving, not selectable). */
25
25
  disabled?: (index: number) => boolean
26
+ /** Read-only: value shown, trigger focusable, but the panel will not open. */
27
+ readonly?: () => boolean
26
28
  /** Commit the item at `index` (Enter or a click the component forwards). */
27
29
  onSelect: (index: number) => void
28
30
  /** Notified whenever the open state changes. */
@@ -59,6 +61,7 @@ export function createPopoverSelect(config: PopoverSelectConfig) {
59
61
 
60
62
  const count = () => config.itemCount()
61
63
  const isDisabled = (i: number) => config.disabled?.(i) ?? false
64
+ const readonly = () => config.readonly?.() ?? false
62
65
 
63
66
  function enabledIndices(): number[] {
64
67
  return enabledIndicesOf(count(), isDisabled)
@@ -74,6 +77,7 @@ export function createPopoverSelect(config: PopoverSelectConfig) {
74
77
  config.onOpenChange?.(v)
75
78
  }
76
79
  function openPanel() {
80
+ if (readonly()) return
77
81
  active = clampActive(config.initialActive?.() ?? 0)
78
82
  setOpen(true)
79
83
  }
@@ -82,6 +86,7 @@ export function createPopoverSelect(config: PopoverSelectConfig) {
82
86
  config.getTrigger()?.focus()
83
87
  }
84
88
  function toggle() {
89
+ if (readonly()) return
85
90
  open ? setOpen(false) : openPanel()
86
91
  }
87
92
 
@@ -92,7 +97,7 @@ export function createPopoverSelect(config: PopoverSelectConfig) {
92
97
  function first() { active = enabledIndices()[0] ?? 0 }
93
98
  function last() { active = enabledIndices().at(-1) ?? 0 }
94
99
  function selectActive() {
95
- if (count() === 0 || isDisabled(active)) return
100
+ if (readonly() || count() === 0 || isDisabled(active)) return
96
101
  config.onSelect(active)
97
102
  if (config.closeOnSelect?.() ?? true) closePanel()
98
103
  }
@@ -129,6 +134,7 @@ export function createPopoverSelect(config: PopoverSelectConfig) {
129
134
  })
130
135
 
131
136
  function onTriggerKeydown(e: KeyboardEvent) {
137
+ if (readonly()) return
132
138
  if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
133
139
  e.preventDefault()
134
140
  openPanel()
@@ -185,6 +191,7 @@ export function createPopoverSelect(config: PopoverSelectConfig) {
185
191
  'aria-haspopup': haspopup,
186
192
  'aria-expanded': open,
187
193
  'aria-controls': open ? panelId : undefined,
194
+ 'aria-readonly': readonly() || undefined,
188
195
  onkeydown: onTriggerKeydown,
189
196
  }),
190
197
  /** Attributes for the element that OWNS focus + activedescendant (the panel,
@@ -19,6 +19,8 @@
19
19
  * </div>
20
20
  * ```
21
21
  */
22
+ import { editorAria } from './editor-contract'
23
+
22
24
  export type SliderValue = number | [number, number]
23
25
  export type SliderOrientation = 'horizontal' | 'vertical'
24
26
  export type SliderThumb = 'single' | 'lo' | 'hi'
@@ -37,6 +39,12 @@ export type SliderConfig = {
37
39
  ariaLabel?: () => string | undefined
38
40
  /** Right-to-left: mirrors horizontal pointer + Left/Right key direction. */
39
41
  rtl?: () => boolean
42
+ // Editor contract (validation ARIA) - folded into thumbProps.
43
+ id?: () => string | undefined
44
+ invalid?: () => boolean | undefined
45
+ required?: () => boolean | undefined
46
+ error?: () => string | undefined
47
+ hint?: () => string | undefined
40
48
  }
41
49
 
42
50
  export function createSlider(config: SliderConfig) {
@@ -153,6 +161,15 @@ export function createSlider(config: SliderConfig) {
153
161
  const vmin = which === 'hi' ? lo : min()
154
162
  const vmax = which === 'lo' ? hi : max()
155
163
  const label = which === 'lo' ? 'Minimum' : which === 'hi' ? 'Maximum' : (config.ariaLabel?.() ?? 'Value')
164
+ // Validation ARIA (invalid / required / describedby) from the editor
165
+ // contract; keep the thumb's own value-oriented aria-label.
166
+ const va = editorAria({
167
+ id: config.id?.(),
168
+ invalid: config.invalid?.(),
169
+ required: config.required?.(),
170
+ error: config.error?.(),
171
+ hint: config.hint?.(),
172
+ })
156
173
  return {
157
174
  role: 'slider' as const,
158
175
  tabindex: disabled() ? -1 : 0,
@@ -162,6 +179,9 @@ export function createSlider(config: SliderConfig) {
162
179
  'aria-label': label,
163
180
  'aria-orientation': (isVert() ? 'vertical' : 'horizontal') as SliderOrientation,
164
181
  'aria-disabled': disabled() || undefined,
182
+ 'aria-invalid': va['aria-invalid'],
183
+ 'aria-required': va['aria-required'],
184
+ 'aria-describedby': va['aria-describedby'],
165
185
  onkeydown: (e: KeyboardEvent) => onThumbKey(e, which),
166
186
  }
167
187
  },
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * createTooltip - the HEADLESS core behind <SvTooltip>: the hover/focus open
3
- * state machine with a show delay, Escape-to-hide, and the `aria-describedby` /
4
- * `role="tooltip"` wiring - as prop-getters you spread onto your own anchor and
5
- * tooltip elements. No styles, no positioning (that is a DOM concern the renderer
6
- * owns), like `createListbox`.
3
+ * state machine with a show delay, an optional close delay, a shared delay-group
4
+ * (so moving between grouped tooltips skips the re-delay), Escape-to-hide, and the
5
+ * `aria-describedby` / `role="tooltip"` wiring - as prop-getters you spread onto
6
+ * your own anchor and tooltip elements. No styles, no positioning (that is a DOM
7
+ * concern the renderer owns), like `createListbox`.
7
8
  *
8
9
  * ```svelte
9
10
  * <script lang="ts">
@@ -25,6 +26,14 @@ export type TooltipConfig = {
25
26
  disabled?: () => boolean
26
27
  /** Show delay in ms after hover/focus. Default 300. */
27
28
  delay?: () => number
29
+ /** Hide delay in ms after leave/blur. Default 0. Give it room for interactive tips. */
30
+ closeDelay?: () => number
31
+ /**
32
+ * Delay-group id. Tooltips sharing a group open INSTANTLY while any group
33
+ * member is open or was open within a short grace window - so scanning a
34
+ * toolbar of icon buttons does not re-wait the show delay each time.
35
+ */
36
+ group?: () => string | undefined
28
37
  }
29
38
 
30
39
  export type TooltipAnchorProps = {
@@ -40,20 +49,65 @@ export type TooltipProps = {
40
49
  role: 'tooltip'
41
50
  }
42
51
 
52
+ /** Grace window (ms) after a group tooltip closes during which the next opens instantly. */
53
+ const GROUP_SKIP_WINDOW = 300
54
+ type GroupEntry = { openCount: number; lastCloseAt: number }
55
+ const groupRegistry = new Map<string, GroupEntry>()
56
+ function groupEntry(id: string): GroupEntry {
57
+ let e = groupRegistry.get(id)
58
+ if (!e) { e = { openCount: 0, lastCloseAt: 0 }; groupRegistry.set(id, e) }
59
+ return e
60
+ }
61
+ const now = () => (typeof Date !== 'undefined' ? Date.now() : 0)
62
+
43
63
  export function createTooltip(config: TooltipConfig = {}) {
44
64
  const id = nextEditorId('sv-tip')
45
65
  let open = $state(false)
46
66
  let showTimer: ReturnType<typeof setTimeout> | undefined
67
+ let hideTimer: ReturnType<typeof setTimeout> | undefined
68
+ let counted = false // whether this instance is currently counted in its group
69
+
70
+ function groupId() { return config.group?.() }
71
+
72
+ /** Effective show delay: 0 when the group is warm (a member open / just closed). */
73
+ function effectiveDelay(): number {
74
+ const base = config.delay?.() ?? 300
75
+ const g = groupId()
76
+ if (!g) return base
77
+ const e = groupEntry(g)
78
+ if (e.openCount > 0 || now() - e.lastCloseAt < GROUP_SKIP_WINDOW) return 0
79
+ return base
80
+ }
81
+
82
+ function markOpen() {
83
+ open = true
84
+ const g = groupId()
85
+ if (g && !counted) { counted = true; groupEntry(g).openCount++ }
86
+ }
87
+ function reallyHide() {
88
+ open = false
89
+ clearTimeout(showTimer)
90
+ clearTimeout(hideTimer)
91
+ const g = groupId()
92
+ if (g && counted) { counted = false; const e = groupEntry(g); e.openCount--; e.lastCloseAt = now() }
93
+ }
47
94
 
48
95
  function show() {
49
96
  if ((config.disabled?.() ?? false) || !(config.text?.() ?? '')) return
97
+ clearTimeout(hideTimer)
98
+ if (open) return
50
99
  clearTimeout(showTimer)
51
- showTimer = setTimeout(() => { open = true }, config.delay?.() ?? 300)
100
+ showTimer = setTimeout(markOpen, effectiveDelay())
52
101
  }
53
- function hide() {
102
+ /** Delayed hide (honors `closeDelay`) - used by pointer/focus leave. */
103
+ function scheduleHide() {
54
104
  clearTimeout(showTimer)
55
- open = false
105
+ const cd = config.closeDelay?.() ?? 0
106
+ if (cd > 0 && open) { clearTimeout(hideTimer); hideTimer = setTimeout(reallyHide, cd) }
107
+ else reallyHide()
56
108
  }
109
+ /** Immediate hide - used by Escape and imperative callers. */
110
+ function hide() { reallyHide() }
57
111
 
58
112
  // Escape hides the tooltip while it is open (WAI-ARIA tooltip pattern).
59
113
  $effect(() => {
@@ -69,14 +123,18 @@ export function createTooltip(config: TooltipConfig = {}) {
69
123
  tipId: id,
70
124
  show,
71
125
  hide,
126
+ /** Keep an interactive tip open while the pointer is over it. */
127
+ keepOpen: show,
128
+ /** Delayed hide honoring `closeDelay` (pointer/focus leave). */
129
+ scheduleHide,
72
130
  /** Spread onto the anchor/trigger wrapper. Also mirror `tipId` onto the real
73
131
  * focusable child's `aria-describedby` so AT announces it on focus. */
74
132
  anchorProps: (): TooltipAnchorProps => ({
75
133
  'aria-describedby': open ? id : undefined,
76
134
  onpointerenter: show,
77
- onpointerleave: hide,
135
+ onpointerleave: scheduleHide,
78
136
  onfocusin: show,
79
- onfocusout: hide,
137
+ onfocusout: scheduleHide,
80
138
  }),
81
139
  /** Spread onto the tooltip element. */
82
140
  tooltipProps: (): TooltipProps => ({ id, role: 'tooltip' }),
@@ -12,6 +12,22 @@
12
12
 
13
13
  export type EditorSize = 'sm' | 'md' | 'lg'
14
14
 
15
+ /**
16
+ * A small action button rendered inside a framed field (via `SvField`'s
17
+ * `actions`) - the generalized form of the clear / reveal buttons, for a lookup,
18
+ * a generate, a copy, etc. `icon` is a Svelte snippet; when absent the `label`
19
+ * text is shown. (Type-only Svelte import, so this module stays runtime-free.)
20
+ */
21
+ export type EditorAction = {
22
+ /** Accessible name + tooltip for the button. */
23
+ label: string
24
+ /** Invoked when the button is pressed. */
25
+ onClick: () => void
26
+ /** Optional icon snippet; falls back to the `label` text. */
27
+ icon?: import('svelte').Snippet
28
+ disabled?: boolean
29
+ }
30
+
15
31
  /** Text direction. `'auto'` defers to the surrounding document/CSS. */
16
32
  export type EditorDir = 'ltr' | 'rtl' | 'auto'
17
33
 
@@ -0,0 +1,40 @@
1
+ /**
2
+ * export-provider seam. The free grid ships built-in text export (CSV / TSV /
3
+ * JSON via the api) but the rich Excel / PDF / HTML export engine lives in
4
+ * `@svgrid/enterprise`. `aiExport` (a free, built-in AI feature) plans an export
5
+ * and, when it needs to actually WRITE an enterprise format, delegates to the
6
+ * engine registered here. Enterprise registers `exportGrid` at install time; if
7
+ * it is absent, `aiExport` returns the plan without running the download.
8
+ *
9
+ * ```ts
10
+ * // in @svgrid/enterprise, at install:
11
+ * import { registerExportProvider } from '@svgrid/grid'
12
+ * import { exportGrid } from './export'
13
+ * registerExportProvider(exportGrid)
14
+ * ```
15
+ */
16
+ import type { SvGridApi } from './index'
17
+
18
+ export type GridExportRunOptions = {
19
+ format: string
20
+ filename?: string
21
+ rows?: ReadonlyArray<Record<string, unknown>>
22
+ groupBy?: string[]
23
+ }
24
+
25
+ export type GridExportProvider = (
26
+ api: SvGridApi<any, any>,
27
+ opts: GridExportRunOptions,
28
+ ) => Promise<unknown>
29
+
30
+ let provider: GridExportProvider | null = null
31
+
32
+ /** Register the export engine that runs AI-planned exports. Enterprise calls this. */
33
+ export function registerExportProvider(fn: GridExportProvider | null): void {
34
+ provider = fn
35
+ }
36
+
37
+ /** The registered export engine, or null when @svgrid/enterprise is not installed. */
38
+ export function getExportProvider(): GridExportProvider | null {
39
+ return provider
40
+ }
@@ -118,3 +118,40 @@ export function operatorLabelFor<TData extends RowData>(
118
118
  }
119
119
  return option.label;
120
120
  }
121
+
122
+ /** GridMessages key for each operator's label (see grid-messages.ts). */
123
+ const OPERATOR_MESSAGE_KEY: Record<FilterOperator, string> = {
124
+ contains: "opContains",
125
+ notContains: "opNotContains",
126
+ equals: "opEquals",
127
+ notEquals: "opNotEquals",
128
+ startsWith: "opStartsWith",
129
+ endsWith: "opEndsWith",
130
+ regex: "opRegex",
131
+ in: "opIn",
132
+ notIn: "opNotIn",
133
+ greaterThan: "opGreaterThan",
134
+ lessThan: "opLessThan",
135
+ between: "opBetween",
136
+ isBlank: "opIsBlank",
137
+ isNotBlank: "opIsNotBlank",
138
+ };
139
+
140
+ /**
141
+ * Localized operator label: honors date `Before`/`After` relabels, then the
142
+ * `localeText` override for the operator, falling back to the English
143
+ * `option.label`. `messages` is a loose string map to avoid a type cycle.
144
+ */
145
+ export function localizeOperatorLabel<TData extends RowData>(
146
+ option: FilterOption,
147
+ column: Column<TData> | undefined,
148
+ messages?: Record<string, string> | null,
149
+ ): string {
150
+ const editorType = column?.columnDef.editorType;
151
+ if (editorType === "date" || editorType === "datetime") {
152
+ if (option.value === "lessThan") return messages?.opBefore || "Before";
153
+ if (option.value === "greaterThan") return messages?.opAfter || "After";
154
+ }
155
+ const key = OPERATOR_MESSAGE_KEY[option.value];
156
+ return (key && messages?.[key]) || option.label;
157
+ }
package/src/form-field.ts CHANGED
@@ -8,7 +8,9 @@ import type { Validator } from './validators'
8
8
  */
9
9
  export type FormFieldType =
10
10
  | 'text' | 'email' | 'tel' | 'textarea' | 'number' | 'password'
11
- | 'select' | 'checkbox' | 'switch' | 'date' | 'color' | 'rating'
11
+ | 'select' | 'multiselect' | 'checkbox' | 'switch' | 'date' | 'color' | 'rating'
12
+ | 'radio' | 'slider' | 'tags' | 'phone' | 'country' | 'mask'
13
+ | 'datetime' | 'time' | 'daterange' | 'combobox' | 'file'
12
14
  | 'array'
13
15
 
14
16
  export type FormField = {
@@ -16,8 +18,44 @@ export type FormField = {
16
18
  label: string
17
19
  type?: FormFieldType
18
20
  required?: boolean
21
+ /** Show the field but block edits (still submitted, unlike hidden fields). */
22
+ readonly?: boolean
23
+ /** Helper text shown under the control (distinct from a validation error). */
24
+ help?: string
19
25
  placeholder?: string
20
- options?: Array<{ value: string | number; label: string; color?: string }>
26
+ /**
27
+ * Options for select/combobox/radio/multiselect. A function makes them CASCADE:
28
+ * the list is derived from the current form values (e.g. cities for the picked
29
+ * country). Pair with `dependsOn` so the child clears when the parent changes.
30
+ */
31
+ options?:
32
+ | Array<{ value: string | number; label: string; color?: string }>
33
+ | ((values: Record<string, any>) => Array<{ value: string | number; label: string; color?: string }>)
34
+ /** Field name(s) this one depends on; when a parent changes, this field's value
35
+ * is cleared (cascading selects). */
36
+ dependsOn?: string | string[]
37
+ /** Mask pattern for `type: 'mask'` (`#`=digit, `A`=letter, `*`=alnum). */
38
+ mask?: string
39
+ /** Numeric bounds for `type: 'number'` / `'slider'`. */
40
+ min?: number
41
+ max?: number
42
+ step?: number
43
+ precision?: number
44
+ /** Text affixes for `type: 'number'` (e.g. `$` / `%`). */
45
+ prefix?: string
46
+ suffix?: string
47
+ /** Remote option loader for `type: 'combobox'` (debounced typeahead). The
48
+ * current form `values` are passed so it can depend on other fields. */
49
+ loadOptions?: (query: string, values: Record<string, any>) => Promise<Array<{ value: string | number; label: string }>>
50
+ /** File input hints for `type: 'file'`. */
51
+ accept?: string
52
+ multiple?: boolean
53
+ /**
54
+ * Derive this field's value from the other form values (e.g. a total). A
55
+ * computed field is read-only, recomputes reactively, is not user-validated,
56
+ * and its derived value is included in the submitted payload.
57
+ */
58
+ computed?: (values: Record<string, any>) => any
21
59
  /** Declarative validation rules (see `rules` - email/pattern/min/compare...). */
22
60
  rules?: ReadonlyArray<Validator>
23
61
  /** Return an error message, or null/undefined when valid. */
@@ -33,6 +71,8 @@ export type FormField = {
33
71
  asyncDebounce?: number
34
72
  /** Span full width in the grid. */
35
73
  full?: boolean
74
+ /** Columns to span (generalizes `full`); `2`+ widens the field in a grid. */
75
+ span?: number
36
76
  /**
37
77
  * Show the field only when this is `true` / returns `true` (default: always).
38
78
  * A field derived from other values, e.g. `visible: (v) => v.hasAddress`.
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { defaultGridMessages, resolveGridMessages } from './grid-messages'
3
+
4
+ describe('resolveGridMessages', () => {
5
+ it('returns the full English defaults when no overrides are given', () => {
6
+ expect(resolveGridMessages()).toEqual(defaultGridMessages)
7
+ expect(resolveGridMessages(null)).toEqual(defaultGridMessages)
8
+ expect(resolveGridMessages({})).toEqual(defaultGridMessages)
9
+ })
10
+
11
+ it('replaces only the provided keys, keeping English for the rest', () => {
12
+ const m = resolveGridMessages({ noRows: 'Aucune ligne', pageSize: 'Taille :' })
13
+ expect(m.noRows).toBe('Aucune ligne')
14
+ expect(m.pageSize).toBe('Taille :')
15
+ // untouched keys stay English
16
+ expect(m.columns).toBe('Columns')
17
+ expect(m.opContains).toBe('Contains')
18
+ })
19
+
20
+ it('falls back to the default for empty-string overrides', () => {
21
+ const m = resolveGridMessages({ noRows: '' })
22
+ expect(m.noRows).toBe(defaultGridMessages.noRows)
23
+ })
24
+ })
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Grid chrome localization. Every user-visible UI string the grid renders itself
3
+ * (empty state, tool panel, pager, status bar, group labels, filter operators,
4
+ * context-menu items, upsell notes) lives here as a flat message map with English
5
+ * defaults. Pass `localeText` on `<SvGrid>` to override any subset; unset keys
6
+ * fall back to English, so omitting `localeText` is a no-op.
7
+ *
8
+ * This mirrors the editor kit's `resolveMessages` pattern (editor-contract.ts) so
9
+ * chrome and editors share one localization idiom.
10
+ */
11
+ import { resolveMessages } from './editor-contract'
12
+
13
+ /** Every localizable chrome string, grouped by area in the comments. */
14
+ export type GridMessages = {
15
+ // Empty / loading state
16
+ noRows: string
17
+ loading: string
18
+ // Tool panel
19
+ columns: string
20
+ filters: string
21
+ clearFilter: string
22
+ // Pagination
23
+ pageSize: string
24
+ page: string
25
+ of: string
26
+ firstPage: string
27
+ prevPage: string
28
+ nextPage: string
29
+ lastPage: string
30
+ // Status bar aggregate labels
31
+ statCount: string
32
+ statSum: string
33
+ statAvg: string
34
+ statMin: string
35
+ statMax: string
36
+ // Grouping chrome
37
+ group: string
38
+ grandTotal: string
39
+ total: string
40
+ rowsSuffix: string
41
+ rowSuffix: string
42
+ // Filter operator labels (mirror filterOperatorOptions ids)
43
+ opContains: string
44
+ opNotContains: string
45
+ opEquals: string
46
+ opNotEquals: string
47
+ opStartsWith: string
48
+ opEndsWith: string
49
+ opRegex: string
50
+ opIn: string
51
+ opNotIn: string
52
+ opGreaterThan: string
53
+ opLessThan: string
54
+ opBetween: string
55
+ opIsBlank: string
56
+ opIsNotBlank: string
57
+ // Date-column relabels for less/greater than
58
+ opBefore: string
59
+ opAfter: string
60
+ // Context menu
61
+ menuCopy: string
62
+ menuCut: string
63
+ menuPaste: string
64
+ menuClear: string
65
+ menuInsertRowAbove: string
66
+ menuInsertRowBelow: string
67
+ menuRemoveRow: string
68
+ menuRemoveColumn: string
69
+ menuEditComment: string
70
+ chartRange: string
71
+ // Upsell notes (enterprise views not installed)
72
+ pivotUpsellTitle: string
73
+ pivotUpsellBody: string
74
+ }
75
+
76
+ /** English defaults - the literal strings the grid shipped before localization. */
77
+ export const defaultGridMessages: GridMessages = {
78
+ noRows: 'No rows to display.',
79
+ loading: 'Loading grid data...',
80
+ columns: 'Columns',
81
+ filters: 'Filters',
82
+ clearFilter: 'Clear filter',
83
+ pageSize: 'Page Size:',
84
+ page: 'Page',
85
+ of: 'of',
86
+ firstPage: 'First page',
87
+ prevPage: 'Previous page',
88
+ nextPage: 'Next page',
89
+ lastPage: 'Last page',
90
+ statCount: 'Count',
91
+ statSum: 'Sum',
92
+ statAvg: 'Avg',
93
+ statMin: 'Min',
94
+ statMax: 'Max',
95
+ group: 'Group',
96
+ grandTotal: 'Grand total',
97
+ total: 'Total',
98
+ rowsSuffix: 'rows',
99
+ rowSuffix: 'row',
100
+ menuCopy: 'Copy',
101
+ menuCut: 'Cut',
102
+ menuPaste: 'Paste',
103
+ menuClear: 'Clear',
104
+ menuInsertRowAbove: 'Insert row above',
105
+ menuInsertRowBelow: 'Insert row below',
106
+ menuRemoveRow: 'Remove row',
107
+ menuRemoveColumn: 'Remove column',
108
+ menuEditComment: 'Edit comment',
109
+ opContains: 'Contains',
110
+ opNotContains: 'Not contains',
111
+ opEquals: 'Equals',
112
+ opNotEquals: 'Not equals',
113
+ opStartsWith: 'Starts with',
114
+ opEndsWith: 'Ends with',
115
+ opRegex: 'Regex',
116
+ opIn: 'In',
117
+ opNotIn: 'Not in',
118
+ opGreaterThan: 'Greater than',
119
+ opLessThan: 'Less than',
120
+ opBetween: 'Between',
121
+ opIsBlank: 'Blank',
122
+ opIsNotBlank: 'Not blank',
123
+ opBefore: 'Before',
124
+ opAfter: 'After',
125
+ chartRange: 'Chart selected range',
126
+ pivotUpsellTitle: 'Pivot mode',
127
+ pivotUpsellBody: 'Pivot mode is an Enterprise feature. Install @svgrid/enterprise and call enablePivot() to use it.',
128
+ }
129
+
130
+ /**
131
+ * Merge a consumer's `localeText` over the English defaults. Undefined/empty
132
+ * values fall back to the default (via `resolveMessages`), so a partial map only
133
+ * replaces the keys it sets.
134
+ */
135
+ export function resolveGridMessages(overrides?: Partial<GridMessages> | null): GridMessages {
136
+ return resolveMessages(defaultGridMessages, overrides)
137
+ }
@@ -0,0 +1,77 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { buildAutoGroupColumns, insertGroupFooters } from './group-display'
3
+
4
+ type R = { id: string; depth: number; group?: boolean; total?: number; footer?: boolean }
5
+
6
+ const rows: R[] = [
7
+ { id: 'gA', depth: 0, group: true, total: 30 },
8
+ { id: 'a1', depth: 1 },
9
+ { id: 'a2', depth: 1 },
10
+ { id: 'gB', depth: 0, group: true, total: 20 },
11
+ { id: 'b1', depth: 1 },
12
+ ]
13
+
14
+ const opts = {
15
+ getDepth: (r: R) => r.depth,
16
+ isGroup: (r: R) => !!r.group,
17
+ makeFooter: (g: R): R => ({ id: `${g.id}__footer`, depth: g.depth, footer: true, total: g.total }),
18
+ }
19
+
20
+ describe('insertGroupFooters', () => {
21
+ it('is a no-op when no footer flags are set', () => {
22
+ expect(insertGroupFooters(rows, opts)).toEqual(rows)
23
+ })
24
+
25
+ it('inserts one footer after each group, preserving order', () => {
26
+ const out = insertGroupFooters(rows, { ...opts, includeGroupFooter: true })
27
+ expect(out.map((r) => r.id)).toEqual([
28
+ 'gA', 'a1', 'a2', 'gA__footer',
29
+ 'gB', 'b1', 'gB__footer',
30
+ ])
31
+ // footers carry the group's aggregate
32
+ expect(out.find((r) => r.id === 'gA__footer')!.total).toBe(30)
33
+ })
34
+
35
+ it('closes nested groups deepest-first', () => {
36
+ const nested: R[] = [
37
+ { id: 'g0', depth: 0, group: true, total: 100 },
38
+ { id: 'g0a', depth: 1, group: true, total: 60 },
39
+ { id: 'leaf1', depth: 2 },
40
+ { id: 'g0b', depth: 1, group: true, total: 40 },
41
+ { id: 'leaf2', depth: 2 },
42
+ ]
43
+ const out = insertGroupFooters(nested, { ...opts, includeGroupFooter: true })
44
+ expect(out.map((r) => r.id)).toEqual([
45
+ 'g0', 'g0a', 'leaf1', 'g0a__footer',
46
+ 'g0b', 'leaf2', 'g0b__footer', 'g0__footer',
47
+ ])
48
+ })
49
+
50
+ it('appends a grand-total footer at the end', () => {
51
+ const out = insertGroupFooters(rows, {
52
+ ...opts,
53
+ includeGrandTotalFooter: true,
54
+ makeGrandTotal: () => ({ id: '__grand', depth: 0, footer: true, total: 50 }),
55
+ })
56
+ expect(out[out.length - 1]!.id).toBe('__grand')
57
+ })
58
+ })
59
+
60
+ describe('buildAutoGroupColumns', () => {
61
+ it('returns nothing for groupRows / no grouping', () => {
62
+ expect(buildAutoGroupColumns(['region'], 'groupRows')).toEqual({ autoColumns: [], hiddenSourceIds: new Set() })
63
+ expect(buildAutoGroupColumns([], 'singleColumn').autoColumns).toHaveLength(0)
64
+ })
65
+
66
+ it('singleColumn: one combined auto column, all grouped columns hidden', () => {
67
+ const r = buildAutoGroupColumns(['region', 'category'], 'singleColumn')
68
+ expect(r.autoColumns).toEqual([{ id: '__autoGroup', field: null, level: 0 }])
69
+ expect([...r.hiddenSourceIds].sort()).toEqual(['category', 'region'])
70
+ })
71
+
72
+ it('multipleColumns: one auto column per grouped field', () => {
73
+ const r = buildAutoGroupColumns(['region', 'category'], 'multipleColumns')
74
+ expect(r.autoColumns.map((c) => c.id)).toEqual(['__group_region', '__group_category'])
75
+ expect(r.autoColumns.map((c) => c.field)).toEqual(['region', 'category'])
76
+ })
77
+ })