@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
package/src/menus.test.ts CHANGED
@@ -666,3 +666,35 @@ describe('createMenus - comments', () => {
666
666
  expect(ctx.commentEditFor).toBeNull()
667
667
  })
668
668
  })
669
+
670
+ describe('createMenus - chart context-menu item', () => {
671
+ const target = { rowIndex: 0, colIndex: 0, columnId: 'a', rowId: '0', row: {} }
672
+
673
+ it('appends "Chart selected range" to the default menu when charting is enabled', () => {
674
+ const ctx = makeCtx({ props: { contextMenu: true }, chartingEnabled: true, contextMenuFor: target })
675
+ const items = createMenus(ctx).contextMenuItems()
676
+ expect(items.map((i) => i.label)).toContain('Chart selected range')
677
+ })
678
+
679
+ it('omits the chart item when charting is disabled', () => {
680
+ const ctx = makeCtx({ props: { contextMenu: true }, chartingEnabled: false, contextMenuFor: target })
681
+ const items = createMenus(ctx).contextMenuItems()
682
+ expect(items.map((i) => i.label)).not.toContain('Chart selected range')
683
+ })
684
+
685
+ it('clicking the chart item opens the chart panel', () => {
686
+ const ctx = makeCtx({ props: { contextMenu: true }, chartingEnabled: true, chartPanelOpen: false, contextMenuFor: target })
687
+ const item = createMenus(ctx).contextMenuItems().find((i) => i.label === 'Chart selected range')
688
+ item?.run?.()
689
+ expect(ctx.chartPanelOpen).toBe(true)
690
+ })
691
+
692
+ it('localizes the chart item label via ctx.messages', () => {
693
+ const ctx = makeCtx({
694
+ props: { contextMenu: true }, chartingEnabled: true, contextMenuFor: target,
695
+ messages: { chartRange: 'Graphique de la selection' },
696
+ })
697
+ const items = createMenus(ctx).contextMenuItems()
698
+ expect(items.map((i) => i.label)).toContain('Graphique de la selection')
699
+ })
700
+ })
package/src/menus.ts CHANGED
@@ -491,16 +491,21 @@ export function createMenus<
491
491
  }
492
492
 
493
493
  // Built-in items, keyed. `run` reads the live target passed at click time.
494
- const builtins: Record<string, { label: string; run: (t: CtxTarget) => void }> = {
495
- copy: { label: "Copy", run: () => ctx.copySelectionToClipboard() },
496
- cut: { label: "Cut", run: () => ctx.cutSelectionToClipboard() },
497
- paste: { label: "Paste", run: () => void ctx.pasteFromClipboard() },
498
- clear: { label: "Clear", run: () => ctx.clearSelectedCells() },
499
- row_above: { label: "Insert row above", run: (t) => insertRowAt(t, 0) },
500
- row_below: { label: "Insert row below", run: (t) => insertRowAt(t, 1) },
501
- remove_row: { label: "Remove row", run: removeRowAt },
502
- remove_col: { label: "Remove column", run: removeColAt },
503
- comment: { label: "Edit comment", run: (t) => openCommentEditor(t) },
494
+ // `msg` names the GridMessages key so the label localizes (falls back to the
495
+ // English `label` when no `localeText` is set).
496
+ const builtins: Record<string, { label: string; msg?: string; run: (t: CtxTarget) => void }> = {
497
+ copy: { label: "Copy", msg: "menuCopy", run: () => ctx.copySelectionToClipboard() },
498
+ cut: { label: "Cut", msg: "menuCut", run: () => ctx.cutSelectionToClipboard() },
499
+ paste: { label: "Paste", msg: "menuPaste", run: () => void ctx.pasteFromClipboard() },
500
+ clear: { label: "Clear", msg: "menuClear", run: () => ctx.clearSelectedCells() },
501
+ row_above: { label: "Insert row above", msg: "menuInsertRowAbove", run: (t) => insertRowAt(t, 0) },
502
+ row_below: { label: "Insert row below", msg: "menuInsertRowBelow", run: (t) => insertRowAt(t, 1) },
503
+ remove_row: { label: "Remove row", msg: "menuRemoveRow", run: removeRowAt },
504
+ remove_col: { label: "Remove column", msg: "menuRemoveColumn", run: removeColAt },
505
+ comment: { label: "Edit comment", msg: "menuEditComment", run: (t) => openCommentEditor(t) },
506
+ // Charting: open the chart panel scoped to the current cell selection. Gated
507
+ // on `chartingEnabled` in the resolver below.
508
+ chart: { label: "Chart selected range", msg: "chartRange", run: () => { ctx.chartPanelOpen = true; } },
504
509
  };
505
510
  const DEFAULT_ITEMS = [
506
511
  "copy", "cut", "paste", "clear", "separator",
@@ -574,16 +579,24 @@ export function createMenus<
574
579
  const target = ctx.contextMenuFor as CtxTarget | null;
575
580
  if (!target) return [];
576
581
  const cfg = ctx.props.contextMenu;
582
+ const messages = ctx.messages;
577
583
  const entries: any[] = Array.isArray(cfg)
578
584
  ? cfg
579
- : [...DEFAULT_ITEMS, ...(ctx.props.editableComments ? ["separator", "comment"] : [])];
585
+ : [
586
+ ...DEFAULT_ITEMS,
587
+ ...(ctx.chartingEnabled ? ["separator", "chart"] : []),
588
+ ...(ctx.props.editableComments ? ["separator", "comment"] : []),
589
+ ];
580
590
  const out: ResolvedItem[] = [];
581
591
  for (const entry of entries) {
582
592
  if (entry === "separator") { out.push({ key: "sep" + out.length, label: "", separator: true }); continue; }
583
593
  if (typeof entry === "string") {
594
+ // The chart item only makes sense when charting is enabled.
595
+ if (entry === "chart" && !ctx.chartingEnabled) continue;
584
596
  const b = builtins[entry];
585
597
  if (!b) continue;
586
- out.push({ key: entry, label: b.label, run: () => b.run(target) });
598
+ const label = (b.msg && messages?.[b.msg]) || b.label;
599
+ out.push({ key: entry, label, run: () => b.run(target) });
587
600
  continue;
588
601
  }
589
602
  if (entry.hidden?.(target)) continue;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * pivot-engine registry - the injection seam for in-grid pivot mode. Setting the
3
+ * `pivot` prop on `<SvGrid>` turns the live grid into a pivot table in place, but
4
+ * the pivot ENGINE (which aggregates rows into a pivot result with nested column
5
+ * headers) ships in `@svgrid/enterprise`. This tiny reactive holder lets
6
+ * enterprise register that engine at import time; the grid looks it up and runs
7
+ * it over its filtered + sorted rows, or shows an upsell placeholder when the
8
+ * engine is absent. Mirrors the `scheduler-view` / `editor-registry` pattern.
9
+ *
10
+ * Unlike the scheduler (which swaps in a whole renderer component), the pivot
11
+ * engine is a PURE function - the pivot result renders through the grid's normal
12
+ * table + grouped-column-header path.
13
+ *
14
+ * ONE ENGINE, two entry points: the engine registered here is enterprise's
15
+ * `createPivotModel`. The `pivot` prop on `<SvGrid>` (declarative, no UI) reaches
16
+ * it through this seam; the visual `<SvPivotDesigner>` component calls the same
17
+ * `createPivotModel` directly and adds a drag-drop field designer, heat-tinting,
18
+ * expand/collapse and a chart view on top. Pick the prop for code-defined
19
+ * pivots, the designer for an end-user field picker - both aggregate identically.
20
+ *
21
+ * ```ts
22
+ * // in @svgrid/enterprise, at module load (via enablePivot()):
23
+ * import { registerPivotEngine } from '@svgrid/grid'
24
+ * import { createPivotModel } from './pivot'
25
+ * registerPivotEngine((data, config) => createPivotModel(data, config))
26
+ * ```
27
+ */
28
+ import type { ColumnDef, RowData, TableFeatures } from './index'
29
+
30
+ /** One value measure in a pivot: which field, how to aggregate, how to label. */
31
+ export type GridPivotValue<TData = RowData> = {
32
+ field: keyof TData & string
33
+ agg?: 'sum' | 'avg' | 'min' | 'max' | 'count' | 'countDistinct' | 'first' | 'last'
34
+ label?: string
35
+ format?: unknown
36
+ }
37
+
38
+ /**
39
+ * Free-grid pivot config. Structurally compatible with enterprise `PivotConfig`
40
+ * so the free package carries zero dependency on enterprise; the enterprise
41
+ * config remains assignable to this.
42
+ */
43
+ export type GridPivotConfig<TData = RowData> = {
44
+ /** Row-axis dimensions, outer-most first. */
45
+ rows: ReadonlyArray<keyof TData & string>
46
+ /** Column-axis dimensions, outer-most first. */
47
+ cols: ReadonlyArray<keyof TData & string>
48
+ /** One or more measures aggregated under each column-axis leaf. */
49
+ values: ReadonlyArray<GridPivotValue<TData>>
50
+ grandTotalRow?: boolean
51
+ grandTotalCol?: boolean
52
+ rowSubtotals?: boolean
53
+ colSort?: (a: unknown, b: unknown, level: number) => number
54
+ rowSort?: (a: unknown, b: unknown, level: number) => number
55
+ }
56
+
57
+ /** A pivot row as emitted by the engine (plain object, keyed by leaf column id). */
58
+ export type GridPivotRow = Record<string, unknown> & {
59
+ __pivotId: string
60
+ __pivotDepth: number
61
+ __pivotLabel: string
62
+ __pivotParentId: string | null
63
+ __pivotExpandable: boolean
64
+ }
65
+
66
+ /** The registered pivot engine: pure `(data, config) => { rows, columns }`. */
67
+ export type PivotEngine = <
68
+ TFeatures extends TableFeatures = TableFeatures,
69
+ TData extends RowData = RowData,
70
+ >(
71
+ data: ReadonlyArray<TData>,
72
+ config: GridPivotConfig<TData>,
73
+ ) => { rows: GridPivotRow[]; columns: Array<ColumnDef<TFeatures, GridPivotRow>> }
74
+
75
+ let engine = $state<PivotEngine | null>(null)
76
+
77
+ /** Register the pivot engine. Enterprise calls this from `enablePivot()`. */
78
+ export function registerPivotEngine(fn: PivotEngine): void {
79
+ engine = fn
80
+ }
81
+
82
+ /** The registered pivot engine, or null when enterprise is not installed. */
83
+ export function getPivotEngine(): PivotEngine | null {
84
+ return engine
85
+ }
86
+
87
+ /** Whether a pivot engine has been registered. */
88
+ export function hasPivotEngine(): boolean {
89
+ return engine != null
90
+ }
@@ -0,0 +1,38 @@
1
+ import { afterEach, describe, expect, it } from 'vitest'
2
+ import {
3
+ getPivotEngine,
4
+ hasPivotEngine,
5
+ registerPivotEngine,
6
+ type GridPivotRow,
7
+ } from './pivot-view.svelte'
8
+
9
+ // A tiny stand-in engine so the seam can be exercised without @svgrid/enterprise.
10
+ const fakeEngine = (data: ReadonlyArray<Record<string, unknown>>) => {
11
+ const rows: GridPivotRow[] = [
12
+ { __pivotId: 'g1', __pivotDepth: 0, __pivotLabel: 'All', __pivotParentId: null, __pivotExpandable: true, total: data.length },
13
+ ]
14
+ return { rows, columns: [{ id: 'total', header: 'Total', field: 'total' }] as never }
15
+ }
16
+
17
+ afterEach(() => {
18
+ // reset the module singleton between tests
19
+ registerPivotEngine(null as never)
20
+ })
21
+
22
+ describe('pivot-view seam', () => {
23
+ it('reports no engine until one is registered', () => {
24
+ expect(hasPivotEngine()).toBe(false)
25
+ expect(getPivotEngine()).toBeNull()
26
+ })
27
+
28
+ it('round-trips a registered engine', () => {
29
+ registerPivotEngine(fakeEngine as never)
30
+ expect(hasPivotEngine()).toBe(true)
31
+ const engine = getPivotEngine()
32
+ expect(engine).not.toBeNull()
33
+ const result = engine!([{ a: 1 }, { a: 2 }], { rows: [], cols: [], values: [] })
34
+ expect(result.rows).toHaveLength(1)
35
+ expect(result.rows[0]!.total).toBe(2)
36
+ expect(result.columns[0]!.id).toBe('total')
37
+ })
38
+ })
@@ -0,0 +1,102 @@
1
+ /**
2
+ * DOM: in-grid pivot mode. Without a registered engine the grid shows an upsell
3
+ * note; after `registerPivotEngine(...)` the pivot result renders in place (as a
4
+ * nested grid) and the toolbar toggle flips back to the flat table.
5
+ */
6
+ import { afterEach, describe, expect, it } from 'vitest'
7
+ import { mount, unmount } from 'svelte'
8
+ import SvGrid from './SvGrid.svelte'
9
+ import {
10
+ createCoreRowModel,
11
+ createFilteredRowModel,
12
+ createPaginatedRowModel,
13
+ createSortedRowModel,
14
+ registerPivotEngine,
15
+ sortFns,
16
+ tableFeatures,
17
+ rowSortingFeature,
18
+ type ColumnDef,
19
+ type GridPivotRow,
20
+ } from './index'
21
+
22
+ type Row = { id: number; region: string; sales: number }
23
+ const features = tableFeatures({ rowSortingFeature })
24
+ const cols: ColumnDef<typeof features, Row>[] = [
25
+ { field: 'region', header: 'Region', width: 140 },
26
+ { field: 'sales', header: 'Sales', width: 120, editorType: 'number' },
27
+ ]
28
+ const data: Row[] = [
29
+ { id: 1, region: 'EU', sales: 100 },
30
+ { id: 2, region: 'EU', sales: 40 },
31
+ { id: 3, region: 'US', sales: 70 },
32
+ ]
33
+
34
+ // A minimal pivot engine: one row per region with a summed `total` column.
35
+ const engine = (rows: ReadonlyArray<Record<string, unknown>>) => {
36
+ const byRegion = new Map<string, number>()
37
+ for (const r of rows) byRegion.set(String(r.region), (byRegion.get(String(r.region)) ?? 0) + Number(r.sales))
38
+ const pivotRows: GridPivotRow[] = [...byRegion].map(([region, total], i) => ({
39
+ __pivotId: `p${i}`, __pivotDepth: 0, __pivotLabel: region, __pivotParentId: null, __pivotExpandable: false,
40
+ region, total,
41
+ }))
42
+ return {
43
+ rows: pivotRows,
44
+ columns: [
45
+ { id: 'region', header: 'Region', field: 'region', width: 140 },
46
+ { id: 'total', header: 'Total sales', field: 'total', width: 140 },
47
+ ] as never,
48
+ }
49
+ }
50
+
51
+ const tick = () => new Promise<void>((r) => queueMicrotask(r))
52
+
53
+ function mountGrid() {
54
+ const target = document.createElement('div')
55
+ document.body.appendChild(target)
56
+ const app = mount(SvGrid, {
57
+ target,
58
+ props: {
59
+ data,
60
+ columns: cols,
61
+ features,
62
+ _rowModels: {
63
+ coreRowModel: createCoreRowModel(),
64
+ filteredRowModel: createFilteredRowModel(),
65
+ sortedRowModel: createSortedRowModel(sortFns),
66
+ paginatedRowModel: createPaginatedRowModel(),
67
+ },
68
+ containerHeight: 300,
69
+ virtualization: false,
70
+ pivot: { rows: ['region'], cols: [], values: [{ field: 'sales', agg: 'sum' }] },
71
+ pivotMode: true,
72
+ } as never,
73
+ })
74
+ return { target, destroy: () => { unmount(app); target.remove() } }
75
+ }
76
+
77
+ let cleanup: (() => void) | null = null
78
+ afterEach(() => {
79
+ registerPivotEngine(null as never)
80
+ cleanup?.(); cleanup = null
81
+ })
82
+
83
+ describe('in-grid pivot mode', () => {
84
+ it('shows an upsell note when no pivot engine is registered', async () => {
85
+ const { target, destroy } = mountGrid()
86
+ cleanup = destroy
87
+ await tick()
88
+ expect(target.querySelector('.sv-grid-pivot-upsell')).toBeTruthy()
89
+ })
90
+
91
+ it('renders the pivot result in place once an engine is registered', async () => {
92
+ registerPivotEngine(engine as never)
93
+ const { target, destroy } = mountGrid()
94
+ cleanup = destroy
95
+ await tick()
96
+ await tick()
97
+ expect(target.querySelector('.sv-grid-pivot-root')).toBeTruthy()
98
+ // The pivot's aggregated column header + a summed value render.
99
+ expect(target.textContent).toContain('Total sales')
100
+ expect(target.textContent).toContain('140') // EU 100 + 40
101
+ })
102
+ })
@@ -0,0 +1,141 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { computePosition, parsePlacement, type Rect, type Viewport } from './positioning'
3
+
4
+ const VP: Viewport = { width: 1000, height: 800 }
5
+ // A reference in the middle of the viewport, 100x40.
6
+ const mid: Rect = { x: 450, y: 380, width: 100, height: 40 }
7
+ const floating: Viewport = { width: 200, height: 120 }
8
+
9
+ describe('parsePlacement', () => {
10
+ it('defaults the bare side to center alignment', () => {
11
+ expect(parsePlacement('bottom')).toEqual({ side: 'bottom', align: 'center' })
12
+ expect(parsePlacement('top-start')).toEqual({ side: 'top', align: 'start' })
13
+ expect(parsePlacement('right-end')).toEqual({ side: 'right', align: 'end' })
14
+ })
15
+ })
16
+
17
+ describe('computePosition - placements', () => {
18
+ it('bottom-start sits below, left-aligned to the reference', () => {
19
+ const r = computePosition(mid, floating, { placement: 'bottom-start', offset: 6, viewport: VP })
20
+ expect(r.side).toBe('bottom')
21
+ expect(r.y).toBe(mid.y + mid.height + 6) // 426
22
+ expect(r.x).toBe(mid.x) // 450
23
+ })
24
+
25
+ it('bottom (center) centers the floating over the reference', () => {
26
+ const r = computePosition(mid, floating, { placement: 'bottom', offset: 6, viewport: VP, shift: false })
27
+ expect(r.x).toBe(mid.x + mid.width / 2 - floating.width / 2) // 500 - 100 = 400
28
+ })
29
+
30
+ it('bottom-end right-aligns the floating to the reference', () => {
31
+ const r = computePosition(mid, floating, { placement: 'bottom-end', offset: 6, viewport: VP })
32
+ expect(r.x).toBe(mid.x + mid.width - floating.width) // 550 - 200 = 350
33
+ })
34
+
35
+ it('top places above the reference', () => {
36
+ const r = computePosition(mid, floating, { placement: 'top-start', offset: 6, viewport: VP })
37
+ expect(r.side).toBe('top')
38
+ expect(r.y).toBe(mid.y - floating.height - 6) // 380 - 126 = 254
39
+ })
40
+
41
+ it('right places to the right, vertically centered', () => {
42
+ const r = computePosition(mid, floating, { placement: 'right', offset: 6, viewport: VP, shift: false })
43
+ expect(r.side).toBe('right')
44
+ expect(r.x).toBe(mid.x + mid.width + 6) // 556
45
+ expect(r.y).toBe(mid.y + mid.height / 2 - floating.height / 2) // 400 - 60 = 340
46
+ })
47
+
48
+ it('left places to the left of the reference', () => {
49
+ const r = computePosition(mid, floating, { placement: 'left', offset: 6, viewport: VP, shift: false })
50
+ expect(r.x).toBe(mid.x - floating.width - 6) // 450 - 206 = 244
51
+ })
52
+ })
53
+
54
+ describe('computePosition - flip', () => {
55
+ it('flips bottom -> top when there is no room below', () => {
56
+ const nearBottom: Rect = { x: 450, y: 760, width: 100, height: 30 } // 30px below to viewport edge
57
+ const r = computePosition(nearBottom, floating, { placement: 'bottom-start', viewport: VP })
58
+ expect(r.side).toBe('top')
59
+ expect(r.y).toBe(nearBottom.y - floating.height - 6)
60
+ })
61
+
62
+ it('does NOT flip when the preferred side fits', () => {
63
+ const r = computePosition(mid, floating, { placement: 'bottom-start', viewport: VP })
64
+ expect(r.side).toBe('bottom')
65
+ })
66
+
67
+ it('keeps the preferred side when flip is disabled even if it overflows', () => {
68
+ const nearBottom: Rect = { x: 450, y: 760, width: 100, height: 30 }
69
+ const r = computePosition(nearBottom, floating, { placement: 'bottom-start', flip: false, viewport: VP })
70
+ expect(r.side).toBe('bottom')
71
+ })
72
+
73
+ it('flips right -> left when there is no room on the right', () => {
74
+ const nearRight: Rect = { x: 940, y: 380, width: 50, height: 40 } // 10px to right edge
75
+ const r = computePosition(nearRight, floating, { placement: 'right', viewport: VP })
76
+ expect(r.side).toBe('left')
77
+ })
78
+ })
79
+
80
+ describe('computePosition - shift', () => {
81
+ it('clamps a start-aligned floating back inside the right edge', () => {
82
+ const nearRight: Rect = { x: 950, y: 380, width: 40, height: 40 }
83
+ const r = computePosition(nearRight, floating, { placement: 'bottom-start', padding: 8, viewport: VP })
84
+ // Would be x=950, but must fit: max x = 1000 - 8 - 200 = 792
85
+ expect(r.x).toBe(792)
86
+ })
87
+
88
+ it('clamps to the left padding when the floating is wider than the room', () => {
89
+ const nearLeft: Rect = { x: 4, y: 380, width: 40, height: 40 }
90
+ const r = computePosition(nearLeft, floating, { placement: 'bottom-start', padding: 8, viewport: VP })
91
+ expect(r.x).toBe(8) // left padding
92
+ })
93
+
94
+ it('does not shift when disabled', () => {
95
+ const nearRight: Rect = { x: 950, y: 380, width: 40, height: 40 }
96
+ const r = computePosition(nearRight, floating, { placement: 'bottom-start', shift: false, viewport: VP })
97
+ expect(r.x).toBe(950)
98
+ })
99
+ })
100
+
101
+ describe('computePosition - size', () => {
102
+ it('reports the room below for a bottom placement', () => {
103
+ const r = computePosition(mid, floating, { placement: 'bottom-start', offset: 6, padding: 8, viewport: VP })
104
+ // 800 - 8 - (380 + 40 + 6) = 366
105
+ expect(r.maxHeight).toBe(366)
106
+ expect(r.maxWidth).toBe(VP.width - 16)
107
+ })
108
+
109
+ it('reports the room above for a top placement', () => {
110
+ const r = computePosition(mid, floating, { placement: 'top-start', offset: 6, padding: 8, viewport: VP })
111
+ // 380 - 6 - 8 = 366
112
+ expect(r.maxHeight).toBe(366)
113
+ })
114
+
115
+ it('honors the minMainAxis floor when the side is cramped', () => {
116
+ const nearBottom: Rect = { x: 450, y: 790, width: 100, height: 8 }
117
+ const r = computePosition(nearBottom, floating, { placement: 'bottom-start', flip: false, minMainAxis: 96, viewport: VP })
118
+ expect(r.maxHeight).toBe(96)
119
+ })
120
+ })
121
+
122
+ describe('computePosition - arrow', () => {
123
+ it('points the arrow at the reference center for a bottom placement', () => {
124
+ const r = computePosition(mid, floating, { placement: 'bottom-start', arrow: { size: 10 }, viewport: VP })
125
+ // reference center x = 500; floating x = 450; arrow = 500 - 450 - 5 = 45
126
+ expect(r.arrow?.x).toBe(45)
127
+ })
128
+
129
+ it('clamps the arrow inside the floating (minus padding)', () => {
130
+ // end-aligned so the reference center is near the floating's leading edge
131
+ const r = computePosition(mid, floating, { placement: 'bottom-end', arrow: { size: 10 }, padding: 8, viewport: VP })
132
+ // floating x = 350; center 500; raw arrow = 500 - 350 - 5 = 145; max = 200 - 10 - 8 = 182 -> stays 145
133
+ expect(r.arrow?.x).toBe(145)
134
+ })
135
+
136
+ it('produces a y arrow offset for side placements', () => {
137
+ const r = computePosition(mid, floating, { placement: 'right', arrow: { size: 10 }, shift: false, viewport: VP })
138
+ // reference center y = 400; floating y = 340; arrow y = 400 - 340 - 5 = 55
139
+ expect(r.arrow?.y).toBe(55)
140
+ })
141
+ })