@svgrid/grid 3.0.3 → 3.0.5

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 (325) hide show
  1. package/README.md +6 -5
  2. package/dist/GridMenus.svelte +44 -2
  3. package/dist/SvChartMenu.svelte +69 -0
  4. package/dist/SvChartMenu.svelte.d.ts +11 -0
  5. package/dist/SvChartPanes.svelte +113 -0
  6. package/dist/SvChartPanes.svelte.d.ts +20 -0
  7. package/dist/SvGrid.controller.svelte.d.ts +122 -41
  8. package/dist/SvGrid.controller.svelte.js +846 -207
  9. package/dist/SvGrid.css +178 -2
  10. package/dist/SvGrid.svelte +717 -720
  11. package/dist/SvGrid.types.d.ts +566 -12
  12. package/dist/SvGridCellEditor.svelte +17 -0
  13. package/dist/SvGridChart.svelte +2257 -329
  14. package/dist/SvGridChart.svelte.d.ts +6 -106
  15. package/dist/SvGridChart.types.d.ts +338 -0
  16. package/dist/SvGridChart.types.js +1 -0
  17. package/dist/SvGridChartBuilder.svelte +323 -0
  18. package/dist/SvGridChartBuilder.svelte.d.ts +34 -0
  19. package/dist/SvGridChartPanel.svelte +221 -275
  20. package/dist/SvGridChartPickers.svelte +417 -0
  21. package/dist/SvGridChartPickers.svelte.d.ts +70 -0
  22. package/dist/SvGridChartView.svelte +13 -14
  23. package/dist/SvModal.svelte +7 -1
  24. package/dist/SvModal.svelte.d.ts +5 -0
  25. package/dist/ai.d.ts +40 -8
  26. package/dist/ai.js +135 -24
  27. package/dist/aria.d.ts +11 -0
  28. package/dist/build-api.js +119 -28
  29. package/dist/cdn/GridMenus-CID62rlI.js +636 -0
  30. package/dist/cdn/GridMenus-DLL-qZeu.js +635 -0
  31. package/dist/cdn/SvChartMenu-Bl6PBbkT.js +58 -0
  32. package/dist/cdn/SvChartMenu-FBSMINA6.js +59 -0
  33. package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-BeU_TkbQ.js} +15 -14
  34. package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-yX8vzleW.js} +38 -37
  35. package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-DQwt4UAs.js} +139 -180
  36. package/dist/cdn/SvDateTimePicker-vNU6bZ-q.js +775 -0
  37. package/dist/cdn/{SvGridCellEditor-CjEJMJKc.js → SvGridCellEditor-B1p-vCK5.js} +188 -178
  38. package/dist/cdn/{SvGridCellEditor-BWnTi2N7.js → SvGridCellEditor-D_0q4xAu.js} +98 -88
  39. package/dist/cdn/SvGridChart-C3EWAZaM.js +3574 -0
  40. package/dist/cdn/SvGridChart-DzLkSwCH.js +3572 -0
  41. package/dist/cdn/SvGridChartBuilder-BE2T1ykB.js +783 -0
  42. package/dist/cdn/SvGridChartBuilder-CfII62sZ.js +784 -0
  43. package/dist/cdn/SvGridChartPanel-CCX5_Wgd.js +675 -0
  44. package/dist/cdn/SvGridChartPanel-xf4sWVTo.js +699 -0
  45. package/dist/cdn/SvGridChartView-CfuXmY5I.js +48 -0
  46. package/dist/cdn/SvGridChartView-DYwabQWj.js +47 -0
  47. package/dist/cdn/SvMenuList-CLN8OuIK.js +452 -0
  48. package/dist/cdn/SvMenuList-DkGhNKjf.js +451 -0
  49. package/dist/cdn/SvModal-BAE-pjZX.js +396 -0
  50. package/dist/cdn/SvModal-CIZWCcad.js +397 -0
  51. package/dist/cdn/chart-Dz7SqMXH.js +4341 -0
  52. package/dist/cdn/chart-export-BXqueeWJ.js +135 -0
  53. package/dist/cdn/chart-export-pdf-DONFUnNY.js +110 -0
  54. package/dist/cdn/chart-panel-messages-CUbf2R4i.js +1052 -0
  55. package/dist/cdn/chart-panel-messages-CmNrMdsr.js +1053 -0
  56. package/dist/cdn/chart-summary-BJW_tg_X.js +138 -0
  57. package/dist/cdn/chart-trend-CaN9mDEV.js +50 -0
  58. package/dist/cdn/chart-validate-HcOVFAfJ.js +188 -0
  59. package/dist/cdn/{column-resize-DsfNXMom.js → column-resize-DpmOLfRp.js} +3 -3
  60. package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CtquV-p3.js} +583 -603
  61. package/dist/cdn/{date-format-BNii4zeD.js → date-format-D6KFzU_W.js} +347 -367
  62. package/dist/cdn/dismissable-DAHetSNk.js +44 -0
  63. package/dist/cdn/editor-contract-LXgQJAgd.js +22 -0
  64. package/dist/cdn/focus-trap-BBOQYBma.js +65 -0
  65. package/dist/cdn/{row-resize-BRcimkUT.js → row-resize-niQCp040.js} +44 -36
  66. package/dist/cdn/server-block-cache-CGoWz-87.js +237 -0
  67. package/dist/cdn/{src-D6XvA1Ij.js → src-Bklofsad.js} +10615 -10155
  68. package/dist/cdn/{src-Bugf5XlI.js → src-jcFMkBnN.js} +7111 -6651
  69. package/dist/cdn/svgrid.js +27 -15
  70. package/dist/cdn/svgrid.svelte-external.js +27 -15
  71. package/dist/cdn/validate-C7Rn_Bns.js +76 -0
  72. package/dist/chart-axes.d.ts +53 -0
  73. package/dist/chart-axes.js +351 -0
  74. package/dist/chart-cartesian.d.ts +88 -0
  75. package/dist/chart-cartesian.js +1862 -0
  76. package/dist/chart-decimate.d.ts +51 -0
  77. package/dist/chart-decimate.js +199 -0
  78. package/dist/chart-export-pdf.d.ts +47 -0
  79. package/dist/chart-export-pdf.js +187 -0
  80. package/dist/chart-export.d.ts +2 -1
  81. package/dist/chart-export.js +18 -3
  82. package/dist/chart-financial.d.ts +91 -0
  83. package/dist/chart-financial.js +175 -0
  84. package/dist/chart-flow.d.ts +10 -0
  85. package/dist/chart-flow.js +298 -0
  86. package/dist/chart-format.d.ts +18 -0
  87. package/dist/chart-format.js +65 -0
  88. package/dist/chart-grid.d.ts +8 -0
  89. package/dist/chart-grid.js +190 -0
  90. package/dist/chart-hierarchy.d.ts +17 -0
  91. package/dist/chart-hierarchy.js +146 -0
  92. package/dist/chart-indicators.d.ts +115 -0
  93. package/dist/chart-indicators.js +389 -0
  94. package/dist/chart-messages.d.ts +95 -0
  95. package/dist/chart-messages.js +92 -0
  96. package/dist/chart-motion.d.ts +16 -0
  97. package/dist/chart-motion.js +141 -0
  98. package/dist/chart-panel-messages.d.ts +265 -0
  99. package/dist/chart-panel-messages.js +285 -0
  100. package/dist/chart-pivot.d.ts +87 -0
  101. package/dist/chart-pivot.js +156 -0
  102. package/dist/chart-polar.d.ts +25 -0
  103. package/dist/chart-polar.js +700 -0
  104. package/dist/chart-samples.d.ts +16 -0
  105. package/dist/chart-samples.js +153 -0
  106. package/dist/chart-scale.d.ts +113 -0
  107. package/dist/chart-scale.js +405 -0
  108. package/dist/chart-stats.d.ts +111 -0
  109. package/dist/chart-stats.js +419 -0
  110. package/dist/chart-stream.d.ts +45 -0
  111. package/dist/chart-stream.js +91 -0
  112. package/dist/chart-summary.d.ts +27 -0
  113. package/dist/chart-summary.js +205 -0
  114. package/dist/chart-sync.svelte.d.ts +32 -0
  115. package/dist/chart-sync.svelte.js +23 -0
  116. package/dist/chart-table.d.ts +35 -0
  117. package/dist/chart-table.js +165 -0
  118. package/dist/chart-trend.d.ts +19 -0
  119. package/dist/chart-trend.js +61 -0
  120. package/dist/chart-types.d.ts +1456 -0
  121. package/dist/chart-types.js +6 -0
  122. package/dist/chart-validate.d.ts +43 -0
  123. package/dist/chart-validate.js +272 -0
  124. package/dist/chart-zoom.d.ts +40 -0
  125. package/dist/chart-zoom.js +145 -0
  126. package/dist/chart.d.ts +186 -865
  127. package/dist/chart.js +460 -2204
  128. package/dist/clipboard.d.ts +1 -0
  129. package/dist/clipboard.js +137 -18
  130. package/dist/column-resize.d.ts +3 -0
  131. package/dist/column-resize.js +4 -1
  132. package/dist/columns.js +3 -0
  133. package/dist/command-context.d.ts +19 -0
  134. package/dist/command-context.js +116 -0
  135. package/dist/core.d.ts +7 -0
  136. package/dist/core.js +36 -13
  137. package/dist/createPopoverSelect.svelte.d.ts +2 -2
  138. package/dist/editing.d.ts +2 -8
  139. package/dist/editing.js +272 -33
  140. package/dist/fill-patterns.d.ts +0 -5
  141. package/dist/fill-patterns.js +56 -2
  142. package/dist/gantt-view.svelte.d.ts +24 -0
  143. package/dist/gantt-view.svelte.js +13 -0
  144. package/dist/grid-icons.d.ts +2 -2
  145. package/dist/grid-icons.js +2 -2
  146. package/dist/grid-messages.d.ts +16 -3
  147. package/dist/grid-messages.js +10 -0
  148. package/dist/history.d.ts +83 -0
  149. package/dist/history.js +133 -0
  150. package/dist/index.d.ts +29 -10
  151. package/dist/index.js +23 -5
  152. package/dist/keyboard-handlers.js +88 -31
  153. package/dist/keyboard.d.ts +55 -0
  154. package/dist/keyboard.js +132 -0
  155. package/dist/menus.d.ts +3 -1
  156. package/dist/menus.js +7 -2
  157. package/dist/merges.d.ts +97 -0
  158. package/dist/merges.js +147 -0
  159. package/dist/row-model.d.ts +133 -0
  160. package/dist/row-model.js +16 -0
  161. package/dist/row-resize.d.ts +3 -0
  162. package/dist/row-resize.js +14 -0
  163. package/dist/scroll-sync.d.ts +1 -0
  164. package/dist/scroll-sync.js +24 -0
  165. package/dist/selection.d.ts +18 -1
  166. package/dist/selection.js +138 -18
  167. package/dist/server-block-cache.d.ts +214 -0
  168. package/dist/server-block-cache.js +531 -0
  169. package/dist/server-data-source.d.ts +240 -6
  170. package/dist/server-data-source.js +220 -20
  171. package/dist/server.d.ts +13 -0
  172. package/dist/server.js +13 -0
  173. package/dist/shortcut-registry.d.ts +145 -0
  174. package/dist/shortcut-registry.js +44 -0
  175. package/dist/svgrid-wrapper.types.d.ts +213 -22
  176. package/dist/validate.d.ts +4 -0
  177. package/dist/validate.js +21 -3
  178. package/dist/virtualization/virtualizer.js +38 -17
  179. package/package.json +11 -1
  180. package/src/GridMenus.svelte +44 -2
  181. package/src/SvChartMenu.svelte +69 -0
  182. package/src/SvChartPanes.svelte +113 -0
  183. package/src/SvChartPanes.test.ts +66 -0
  184. package/src/SvGrid.controller.svelte.ts +819 -179
  185. package/src/SvGrid.css +178 -2
  186. package/src/SvGrid.svelte +717 -720
  187. package/src/SvGrid.types.ts +564 -11
  188. package/src/SvGridCellEditor.svelte +17 -0
  189. package/src/SvGridChart.svelte +2257 -329
  190. package/src/SvGridChart.test.ts +1419 -1
  191. package/src/SvGridChart.types.ts +336 -0
  192. package/src/SvGridChartBuilder.svelte +323 -0
  193. package/src/SvGridChartPanel.svelte +221 -275
  194. package/src/SvGridChartPickers.svelte +417 -0
  195. package/src/SvGridChartView.svelte +13 -14
  196. package/src/SvModal.svelte +7 -1
  197. package/src/SvModal.test.ts +9 -0
  198. package/src/ai.test.ts +57 -2
  199. package/src/ai.ts +166 -22
  200. package/src/aria.d.ts +11 -0
  201. package/src/build-api.coverage.test.ts +184 -0
  202. package/src/build-api.ts +118 -22
  203. package/src/chart-axes.test.ts +573 -0
  204. package/src/chart-axes.ts +342 -0
  205. package/src/chart-cartesian.ts +1863 -0
  206. package/src/chart-decimate.test.ts +249 -0
  207. package/src/chart-decimate.ts +186 -0
  208. package/src/chart-export-pdf.test.ts +91 -0
  209. package/src/chart-export-pdf.ts +207 -0
  210. package/src/chart-export.test.ts +14 -0
  211. package/src/chart-export.ts +13 -3
  212. package/src/chart-financial.ts +208 -0
  213. package/src/chart-flow.ts +286 -0
  214. package/src/chart-format.ts +67 -0
  215. package/src/chart-grid.ts +179 -0
  216. package/src/chart-hierarchy.ts +135 -0
  217. package/src/chart-indicators.test.ts +443 -0
  218. package/src/chart-indicators.ts +384 -0
  219. package/src/chart-messages.test.ts +21 -0
  220. package/src/chart-messages.ts +184 -0
  221. package/src/chart-motion.test.ts +71 -0
  222. package/src/chart-motion.ts +148 -0
  223. package/src/chart-panel-messages.test.ts +29 -0
  224. package/src/chart-panel-messages.ts +550 -0
  225. package/src/chart-pivot.test.ts +54 -0
  226. package/src/chart-pivot.ts +225 -0
  227. package/src/chart-polar.ts +674 -0
  228. package/src/chart-samples.ts +106 -0
  229. package/src/chart-scale.ts +394 -0
  230. package/src/chart-series-types.test.ts +714 -0
  231. package/src/chart-stats.ts +409 -0
  232. package/src/chart-stream.test.ts +75 -0
  233. package/src/chart-stream.ts +114 -0
  234. package/src/chart-summary.test.ts +90 -0
  235. package/src/chart-summary.ts +197 -0
  236. package/src/chart-sync.svelte.ts +41 -0
  237. package/src/chart-table.test.ts +76 -0
  238. package/src/chart-table.ts +178 -0
  239. package/src/chart-trend.ts +47 -0
  240. package/src/chart-types.ts +1309 -0
  241. package/src/chart-validate.test.ts +100 -0
  242. package/src/chart-validate.ts +247 -0
  243. package/src/chart-zoom.test.ts +137 -0
  244. package/src/chart-zoom.ts +124 -0
  245. package/src/chart.coverage.test.ts +81 -0
  246. package/src/chart.test.ts +56 -3
  247. package/src/chart.ts +557 -2784
  248. package/src/clipboard.test.ts +151 -0
  249. package/src/clipboard.ts +145 -16
  250. package/src/column-resize.ts +7 -1
  251. package/src/columns.ts +2 -0
  252. package/src/command-context.test.ts +200 -0
  253. package/src/command-context.ts +137 -0
  254. package/src/core.rowmodel-cache.test.ts +51 -0
  255. package/src/core.ts +40 -12
  256. package/src/editing.test.ts +281 -3
  257. package/src/editing.ts +264 -38
  258. package/src/fill-patterns.test.ts +35 -0
  259. package/src/fill-patterns.ts +56 -2
  260. package/src/gantt-stub.test.svelte +38 -0
  261. package/src/gantt-view.svelte.ts +35 -0
  262. package/src/grid-icons.ts +2 -2
  263. package/src/grid-messages.ts +28 -3
  264. package/src/history.test.ts +196 -0
  265. package/src/history.ts +162 -0
  266. package/src/icon-seam.test.ts +5 -6
  267. package/src/index.ts +186 -12
  268. package/src/keyboard-handlers.coverage.test.ts +126 -6
  269. package/src/keyboard-handlers.ts +91 -30
  270. package/src/keyboard-shortcuts.seam.test.ts +200 -0
  271. package/src/keyboard.test.ts +124 -1
  272. package/src/keyboard.ts +142 -0
  273. package/src/menus.test.ts +26 -0
  274. package/src/menus.ts +10 -3
  275. package/src/merges.test.ts +112 -0
  276. package/src/merges.ts +168 -0
  277. package/src/pivot.grid.test.ts +40 -5
  278. package/src/row-model.ts +146 -0
  279. package/src/row-resize.test.ts +44 -0
  280. package/src/row-resize.ts +17 -0
  281. package/src/scroll-sync.test.ts +42 -1
  282. package/src/scroll-sync.ts +25 -0
  283. package/src/selection.multi-range.test.ts +34 -0
  284. package/src/selection.test.ts +33 -2
  285. package/src/selection.ts +143 -21
  286. package/src/server-block-cache.test.ts +645 -0
  287. package/src/server-block-cache.ts +677 -0
  288. package/src/server-data-source.infinite.test.ts +343 -0
  289. package/src/server-data-source.ts +450 -27
  290. package/src/server.ts +47 -0
  291. package/src/shortcut-registry.test.ts +97 -0
  292. package/src/shortcut-registry.ts +181 -0
  293. package/src/svgrid-wrapper.types.ts +184 -13
  294. package/src/svgrid.behavior.test.ts +135 -0
  295. package/src/svgrid.charting.test.ts +513 -3
  296. package/src/svgrid.context-menu.test.ts +31 -0
  297. package/src/svgrid.gantt-seam.test.ts +199 -0
  298. package/src/svgrid.new-features.wrapper.test.ts +10 -3
  299. package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
  300. package/src/svgrid.row-model-seam.svelte.test.ts +289 -0
  301. package/src/svgrid.upsell-license.test.ts +7 -4
  302. package/src/validate.test.ts +29 -0
  303. package/src/validate.ts +28 -3
  304. package/src/virtualization/virtualizer.ts +39 -17
  305. package/dist/SvGroupCell.svelte +0 -141
  306. package/dist/SvGroupCell.svelte.d.ts +0 -49
  307. package/dist/SvRowGroupPanel.svelte +0 -186
  308. package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
  309. package/dist/cdn/GridMenus-CHOMnW-c.js +0 -602
  310. package/dist/cdn/GridMenus-DhdPF700.js +0 -603
  311. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
  312. package/dist/cdn/SvGridChart-1PMSp2LZ.js +0 -1481
  313. package/dist/cdn/SvGridChart-D02avhyr.js +0 -1480
  314. package/dist/cdn/SvGridChartPanel-DJnViOv4.js +0 -834
  315. package/dist/cdn/SvGridChartPanel-vPTNerFZ.js +0 -810
  316. package/dist/cdn/SvGridChartView-C--HhdUo.js +0 -56
  317. package/dist/cdn/SvGridChartView-KyyFG-wY.js +0 -55
  318. package/dist/cdn/chart-DLg3_zTQ.js +0 -1652
  319. package/dist/cdn/validate-_CDJzgIo.js +0 -75
  320. package/dist/server-group-model.d.ts +0 -98
  321. package/dist/server-group-model.js +0 -263
  322. package/src/SvGroupCell.svelte +0 -141
  323. package/src/SvRowGroupPanel.svelte +0 -186
  324. package/src/server-group-model.test.ts +0 -294
  325. package/src/server-group-model.ts +0 -370
@@ -0,0 +1,199 @@
1
+ /**
2
+ * The FREE half of the Gantt view: the `gantt` prop, what the grid hands the
3
+ * renderer, and what shows when no Enterprise renderer is registered.
4
+ *
5
+ * The renderer itself is Pro and lives behind `registerGanttView`, so most of
6
+ * this suite deliberately never registers one - it asserts the upsell path.
7
+ * The last block registers a STUB renderer to pin the seam's contract: which
8
+ * props the grid passes, and that the rows arrive already filtered. The real
9
+ * renderer is covered by `gantt.dom.test.ts` in @svgrid/enterprise.
10
+ */
11
+ import { afterEach, describe, expect, it, vi } from 'vitest'
12
+ import { mount, unmount } from 'svelte'
13
+ import SvGrid from './SvGrid.svelte'
14
+ import {
15
+ createCoreRowModel,
16
+ columnFilteringFeature,
17
+ hasGanttView,
18
+ registerGanttView,
19
+ rowSortingFeature,
20
+ tableFeatures,
21
+ } from './index'
22
+ import { getGanttView } from './gantt-view.svelte'
23
+ import GanttStub from './gantt-stub.test.svelte'
24
+ import type { ColumnDef, GanttConfig, SvGridApi } from './index'
25
+
26
+ type Task = { id: number; name: string; start: string; end: string; parentId: number | null }
27
+ const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
28
+ const rows: Task[] = [
29
+ { id: 1, name: 'Discovery', start: '2026-09-01', end: '2026-09-10', parentId: null },
30
+ { id: 2, name: 'Interviews', start: '2026-09-01', end: '2026-09-04', parentId: 1 },
31
+ { id: 3, name: 'Build', start: '2026-09-10', end: '2026-09-30', parentId: null },
32
+ ]
33
+ const cols: ColumnDef<typeof features, Task>[] = [
34
+ { field: 'name', header: 'Task', width: 180 },
35
+ { field: 'start', header: 'Start', width: 120 },
36
+ { field: 'end', header: 'End', width: 120 },
37
+ ]
38
+ const gantt: GanttConfig<typeof features, Task> = {
39
+ startField: 'start',
40
+ endField: 'end',
41
+ parentField: 'parentId',
42
+ }
43
+
44
+ const tick = () => new Promise<void>((r) => setTimeout(r))
45
+ const UPSELL = '.sv-grid-gantt-upsell'
46
+
47
+ afterEach(() => {
48
+ // The seam is module-level state; a stub left registered would make every
49
+ // later "no renderer" assertion pass for the wrong reason.
50
+ registerGanttView(null as never)
51
+ document.body.innerHTML = ''
52
+ })
53
+
54
+ function mountGrid(extra: Record<string, unknown> = {}) {
55
+ return new Promise<{ api: SvGridApi<typeof features, Task>; target: HTMLElement; destroy: () => void }>(
56
+ (res, rej) => {
57
+ const target = document.createElement('div')
58
+ document.body.appendChild(target)
59
+ const app = mount(SvGrid, {
60
+ target,
61
+ props: {
62
+ data: rows,
63
+ columns: cols,
64
+ features,
65
+ _rowModels: { coreRowModel: createCoreRowModel() },
66
+ getRowId: (r: Task) => String(r.id),
67
+ containerHeight: 260,
68
+ virtualization: false,
69
+ gantt,
70
+ onApiReady(api: SvGridApi<typeof features, Task>) {
71
+ res({ api, target, destroy: () => { unmount(app); target.remove() } })
72
+ },
73
+ ...extra,
74
+ } as any,
75
+ })
76
+ queueMicrotask(() => {
77
+ if (!target.querySelector('.sv-grid-gantt-root')) rej(new Error('no gantt root'))
78
+ })
79
+ },
80
+ )
81
+ }
82
+
83
+ describe('gantt - without the Enterprise renderer', () => {
84
+ it('reports that no renderer is registered', () => {
85
+ // If this is ever true here, the suite is testing the Pro path by accident
86
+ // and every assertion below is meaningless.
87
+ expect(hasGanttView()).toBe(false)
88
+ })
89
+
90
+ it('renders the upsell note in place of the table', async () => {
91
+ const { target, destroy } = await mountGrid()
92
+ await tick()
93
+
94
+ const note = target.querySelector(UPSELL)!
95
+ expect(note).not.toBeNull()
96
+ expect(note.getAttribute('role')).toBe('note')
97
+ expect(note.textContent).toContain('@svgrid/enterprise')
98
+ expect(note.textContent).toContain('enableGanttView()')
99
+ // The table is replaced, not decorated - this is a view of the grid.
100
+ expect(target.querySelector('[role="grid"]')).toBeNull()
101
+ destroy()
102
+ })
103
+
104
+ it('says a license is needed, and links to it', async () => {
105
+ // Naming the package is what unblocks a developer trying it; saying a key
106
+ // is needed is what they have to know before shipping. Enterprise is
107
+ // SOFT-gated, so the wording must not imply the feature is inert without
108
+ // one - it says "watermark", which is what actually happens.
109
+ const { target, destroy } = await mountGrid()
110
+ await tick()
111
+
112
+ const license = target.querySelector('.sv-grid-upsell-license')!
113
+ expect(license).not.toBeNull()
114
+ expect(license.textContent).toContain('license key')
115
+ expect(license.textContent).toContain('watermark')
116
+
117
+ const link = license.querySelector('a')!
118
+ expect(link.getAttribute('href')).toBe('https://svgrid.com/pricing/')
119
+ expect(link.getAttribute('target')).toBe('_blank')
120
+ expect(link.getAttribute('rel')).toContain('noopener')
121
+ destroy()
122
+ })
123
+
124
+ it('shows the search box, and hides it on `searchable: false`', async () => {
125
+ const withBox = await mountGrid()
126
+ await tick()
127
+ const input = withBox.target.querySelector<HTMLInputElement>('.sv-grid-board-search input')!
128
+ expect(input).not.toBeNull()
129
+ expect(input.placeholder).toBe('Search tasks...')
130
+ withBox.destroy()
131
+
132
+ const without = await mountGrid({ gantt: { ...gantt, searchable: false } })
133
+ await tick()
134
+ expect(without.target.querySelector('.sv-grid-board-search')).toBeNull()
135
+ without.destroy()
136
+ })
137
+
138
+ it('is localizable, like the rest of the grid chrome', async () => {
139
+ const { target, destroy } = await mountGrid({
140
+ localization: {
141
+ text: { ganttUpsellTitle: 'Gantt-Ansicht', upsellLicense: 'Lizenz erforderlich.' },
142
+ },
143
+ })
144
+ await vi.waitFor(() => {
145
+ expect(target.querySelector(UPSELL)?.textContent).toContain('Gantt-Ansicht')
146
+ expect(target.querySelector('.sv-grid-upsell-license')?.textContent).toContain(
147
+ 'Lizenz erforderlich.',
148
+ )
149
+ })
150
+ destroy()
151
+ })
152
+ })
153
+
154
+ describe('gantt - the renderer seam', () => {
155
+ // GanttStub stands in for @svgrid/enterprise's SvGridGantt: it writes every
156
+ // prop it receives into the DOM, so the grid's half of the contract is what
157
+ // those attributes say.
158
+ const stub = () => document.querySelector<HTMLElement>('.gantt-stub')!
159
+
160
+ it('mounts the registered renderer instead of the upsell', async () => {
161
+ registerGanttView(GanttStub as never)
162
+ expect(hasGanttView()).toBe(true)
163
+ expect(getGanttView()).not.toBeNull()
164
+
165
+ const { target, destroy } = await mountGrid()
166
+ await tick()
167
+ expect(target.querySelector(UPSELL)).toBeNull()
168
+ expect(target.querySelector('.gantt-stub')).not.toBeNull()
169
+ destroy()
170
+ })
171
+
172
+ it('hands it data, columns, the config and getRowId', async () => {
173
+ registerGanttView(GanttStub as never)
174
+ const { destroy } = await mountGrid()
175
+ await tick()
176
+
177
+ expect(stub().dataset.ids).toBe('1,2,3')
178
+ expect(stub().dataset.cols).toBe(String(cols.length))
179
+ expect(stub().dataset.start).toBe('start')
180
+ expect(stub().dataset.parent).toBe('parentId')
181
+ expect(stub().dataset.hasGetRowId).toBe('true')
182
+ destroy()
183
+ })
184
+
185
+ it("hands it the grid's FILTERED rows, so the search box flows through", async () => {
186
+ // This is what makes the Gantt a view of the grid rather than a second
187
+ // data path: the renderer never filters, it just draws what it is given.
188
+ registerGanttView(GanttStub as never)
189
+ const { target, destroy } = await mountGrid()
190
+ await tick()
191
+
192
+ const search = target.querySelector<HTMLInputElement>('.sv-grid-board-search input')!
193
+ search.value = 'Interviews'
194
+ search.dispatchEvent(new Event('input', { bubbles: true }))
195
+
196
+ await vi.waitFor(() => expect(stub().dataset.ids).toBe('2'))
197
+ destroy()
198
+ })
199
+ })
@@ -75,7 +75,9 @@ describe('SvGrid wrapper - cellClass + rowClass', () => {
75
75
 
76
76
  it('computes the cell-level class per column and threads it onto the <td>', () => {
77
77
  expect(source).toMatch(/function computeCellClass/)
78
- expect(source).toMatch(/userCellClass = computeCellClass\(\s*row,\s*rendered\.column,?\s*\)/)
78
+ // The td that draws a merge shows the origin cell, so the class comes
79
+ // from the cell that draws (cellRow / cellColumn): the row's own outside a merge.
80
+ expect(source).toMatch(/userCellClass = computeCellClass\(\s*cellRow,\s*cellColumn,?\s*\)/)
79
81
  expect(source).toMatch(/class=\{`sv-grid-cell \$\{userCellClass\}`\}/)
80
82
  })
81
83
  })
@@ -93,9 +95,14 @@ describe('SvGrid wrapper - declarative cell validation (validate hook)', () => {
93
95
  expect(source).toMatch(/if \(out == null \|\| out === true\)/)
94
96
  })
95
97
 
96
- it('threads the invalid class + message tooltip onto both <td> render paths', () => {
98
+ it('threads the invalid class + message tooltip onto the one body-row snippet every path renders', () => {
99
+ // The virtualized window, the plain body and the frozen rows all render
100
+ // `bodyRow`, so the class appears exactly once: the two copies the
101
+ // render paths used to carry had drifted apart.
97
102
  const hits = source.match(/class:sv-grid-cell-invalid=\{cellValidity\.invalid\}/g) ?? []
98
- expect(hits.length).toBeGreaterThanOrEqual(2)
103
+ expect(hits.length).toBe(1)
104
+ expect(source).toMatch(/\{#snippet bodyRow\(/)
105
+ expect((source.match(/\{@render bodyRow\(/g) ?? []).length).toBe(3)
99
106
  // Message wins over the plain column tooltip when the cell is invalid.
100
107
  expect(source).toMatch(/cellValidity\.invalid && cellValidity\.message/)
101
108
  })
@@ -0,0 +1,352 @@
1
+ /**
2
+ * `<SvGrid rowModel={ctl} />` - the one-prop integration.
3
+ *
4
+ * The seams below it are tested in `svgrid.row-model-seam.svelte.test.ts`;
5
+ * this suite is about what the prop itself is for: that handing the grid a
6
+ * model wires all of those seams at once, that the model drives re-renders,
7
+ * and that writing a prop explicitly still wins - so adopting it is never
8
+ * all-or-nothing.
9
+ */
10
+ import { describe, expect, it, vi } from 'vitest'
11
+ import { mount, unmount } from 'svelte'
12
+ import SvGrid from './SvGrid.svelte'
13
+ import {
14
+ createCoreRowModel,
15
+ createServerDataSource,
16
+ rowSelectionFeature,
17
+ rowSortingFeature,
18
+ tableFeatures,
19
+ type ColumnDef,
20
+ type GridRowModel,
21
+ type ServerDataSource,
22
+ type SvGridApi,
23
+ } from './index'
24
+
25
+ type Row = { id: number; name: string }
26
+ const features = tableFeatures({ rowSortingFeature, rowSelectionFeature })
27
+ const cols: ColumnDef<typeof features, Row>[] = [{ field: 'name', header: 'Name', width: 160 }]
28
+
29
+ const tick = () => new Promise<void>((r) => setTimeout(r))
30
+ const settle = async () => {
31
+ for (let i = 0; i < 20; i += 1) await Promise.resolve()
32
+ await tick()
33
+ }
34
+
35
+ function mountGrid(props: Record<string, unknown>) {
36
+ return new Promise<{
37
+ api: SvGridApi<typeof features, Row>
38
+ target: HTMLElement
39
+ destroy: () => void
40
+ }>((res, rej) => {
41
+ const target = document.createElement('div')
42
+ document.body.appendChild(target)
43
+ const app = mount(SvGrid, {
44
+ target,
45
+ props: {
46
+ columns: cols,
47
+ features,
48
+ _rowModels: { coreRowModel: createCoreRowModel() },
49
+ containerHeight: 200,
50
+ virtualization: false,
51
+ onApiReady(api: SvGridApi<typeof features, Row>) {
52
+ res({ api, target, destroy: () => { unmount(app); target.remove() } })
53
+ },
54
+ ...props,
55
+ } as any,
56
+ })
57
+ queueMicrotask(() => {
58
+ if (!target.querySelector('[role="grid"]')) rej(new Error('no grid'))
59
+ })
60
+ })
61
+ }
62
+
63
+ /** A hand-written model, to prove the grid depends on the shape not the class. */
64
+ function handWritten(rows: Row[]) {
65
+ const listeners = new Set<() => void>()
66
+ let current = rows
67
+ let loading = false
68
+ const sorts: Array<unknown> = []
69
+ return {
70
+ sorts,
71
+ push(next: Row[]) {
72
+ current = next
73
+ for (const l of listeners) l()
74
+ },
75
+ setLoading(next: boolean) {
76
+ loading = next
77
+ for (const l of listeners) l()
78
+ },
79
+ model: {
80
+ subscribe(onChange: () => void) {
81
+ listeners.add(onChange)
82
+ return () => listeners.delete(onChange)
83
+ },
84
+ getRows: () => current,
85
+ isLoading: () => loading,
86
+ getRowId: (r: Row) => String(r.id),
87
+ setSort: (s: unknown) => sorts.push(s),
88
+ } satisfies GridRowModel<Row>,
89
+ }
90
+ }
91
+
92
+ describe('the rowModel prop', () => {
93
+ it('renders the rows the model reports, with no data prop at all', async () => {
94
+ const source = handWritten([
95
+ { id: 1, name: 'Ada' },
96
+ { id: 2, name: 'Grace' },
97
+ ])
98
+ const { target, destroy } = await mountGrid({ rowModel: source.model })
99
+ await tick()
100
+
101
+ expect(target.textContent).toContain('Ada')
102
+ expect(target.textContent).toContain('Grace')
103
+ destroy()
104
+ })
105
+
106
+ it('re-renders when the model says it changed', async () => {
107
+ const source = handWritten([{ id: 1, name: 'Ada' }])
108
+ const { target, destroy } = await mountGrid({ rowModel: source.model })
109
+ await tick()
110
+ expect(target.textContent).not.toContain('Linus')
111
+
112
+ source.push([{ id: 1, name: 'Ada' }, { id: 9, name: 'Linus' }])
113
+ await vi.waitFor(() => expect(target.textContent).toContain('Linus'))
114
+ destroy()
115
+ })
116
+
117
+ it('unsubscribes on unmount', async () => {
118
+ const source = handWritten([{ id: 1, name: 'Ada' }])
119
+ const { destroy } = await mountGrid({ rowModel: source.model })
120
+ await tick()
121
+ destroy()
122
+ await tick()
123
+
124
+ // A model that outlives the grid must not keep it alive through a
125
+ // listener; pushing after unmount should reach nobody and throw nothing.
126
+ expect(() => source.push([{ id: 2, name: 'Grace' }])).not.toThrow()
127
+ })
128
+
129
+ it('takes over sorting when the model sorts, and says so to the grid', async () => {
130
+ const source = handWritten([
131
+ { id: 1, name: 'Zoe' },
132
+ { id: 2, name: 'Ada' },
133
+ ])
134
+ const { api, target, destroy } = await mountGrid({ rowModel: source.model, sortable: true })
135
+ await tick()
136
+
137
+ api.setSort('name', 'asc')
138
+ await tick()
139
+
140
+ // The model was told; the grid did NOT reorder locally, because a model
141
+ // with `setSort` implies `externalSort`. (The grid also reports the
142
+ // empty initial sort on mount, as it does for any onSortingChange.)
143
+ expect(source.sorts.at(-1)).toEqual([{ id: 'name', desc: false }])
144
+ const firstRow = target.querySelector('tbody .sv-grid-row')!
145
+ expect(firstRow.textContent).toContain('Zoe')
146
+ destroy()
147
+ })
148
+
149
+ it('lets an explicit prop win over the model', async () => {
150
+ const source = handWritten([{ id: 1, name: 'Ada' }])
151
+ const { target, destroy } = await mountGrid({
152
+ rowModel: source.model,
153
+ data: [{ id: 7, name: 'Override' }],
154
+ })
155
+ await tick()
156
+
157
+ expect(target.textContent).toContain('Override')
158
+ expect(target.textContent).not.toContain('Ada')
159
+ destroy()
160
+ })
161
+
162
+ it('drives a real server controller end to end', async () => {
163
+ const source: ServerDataSource<Row> = {
164
+ async getRows(req) {
165
+ const all: Row[] = Array.from({ length: 250 }, (_, i) => ({ id: i, name: `Row ${i}` }))
166
+ const sorted = req.sortModel[0]?.desc ? [...all].reverse() : all
167
+ return { rows: sorted.slice(req.startRow, req.endRow), rowCount: all.length }
168
+ },
169
+ }
170
+ const ctl = createServerDataSource<Row>(source, {
171
+ mode: 'infinite',
172
+ blockSize: 100,
173
+ onChange: () => {},
174
+ })
175
+
176
+ const { target, destroy } = await mountGrid({ rowModel: ctl })
177
+
178
+ // The grid reported its visible range, the controller fetched block 0, and
179
+ // the rows arrived - with no wiring in this test beyond the one prop.
180
+ await vi.waitFor(() => expect(target.textContent).toContain('Row 0'))
181
+ expect(ctl.getCacheState().length).toBeGreaterThan(0)
182
+ expect(ctl.getRows()).toHaveLength(250)
183
+ destroy()
184
+ ctl.dispose()
185
+ }, 20_000)
186
+
187
+ it('wires the pager from a page-mode controller', async () => {
188
+ const source: ServerDataSource<Row> = {
189
+ async getRows(req) {
190
+ const all: Row[] = Array.from({ length: 95 }, (_, i) => ({ id: i, name: `Row ${i}` }))
191
+ return { rows: all.slice(req.startRow, req.endRow), rowCount: all.length }
192
+ },
193
+ }
194
+ const ctl = createServerDataSource<Row>(source, { pageSize: 20, onChange: () => {} })
195
+ ctl.refresh()
196
+ await settle()
197
+
198
+ const { api, destroy } = await mountGrid({ rowModel: ctl, pageable: true })
199
+
200
+ // 95 rows on the server, not the 20 in hand: the count came from the model.
201
+ await vi.waitFor(() => expect(api.getPageInfo().pageCount).toBe(5))
202
+ expect(api.getPageInfo().pageSize).toBe(20)
203
+ destroy()
204
+ ctl.dispose()
205
+ }, 20_000)
206
+
207
+ it('takes the page-size choices from the model and sizes the page to the body on request', async () => {
208
+ const calls: number[] = []
209
+ let pageSize = 10
210
+ const rows: Row[] = Array.from({ length: 50 }, (_, i) => ({ id: i, name: `Row ${i}` }))
211
+ const model: GridRowModel<Row> = {
212
+ subscribe: () => () => {},
213
+ getRows: () => rows.slice(0, pageSize),
214
+ isLoading: () => false,
215
+ get pagination() {
216
+ return {
217
+ pageIndex: 0,
218
+ pageSize,
219
+ rowCount: 50,
220
+ setPage: () => {},
221
+ setPageSize: (n: number) => {
222
+ calls.push(n)
223
+ pageSize = n
224
+ },
225
+ pageSizes: [7, 14, 21],
226
+ autoPageSize: true,
227
+ }
228
+ },
229
+ }
230
+ // jsdom lays nothing out; give every element a 100px box so the grid
231
+ // measures its body on mount (the way the responsive test fakes width).
232
+ const desc = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'clientHeight')
233
+ Object.defineProperty(HTMLElement.prototype, 'clientHeight', { configurable: true, get: () => 100 })
234
+ try {
235
+ const { api, target, destroy } = await mountGrid({ rowModel: model, pageable: true, rowHeight: 20 })
236
+ // The footer offers the model's sizes, not the grid's defaults.
237
+ await vi.waitFor(() => expect(target.querySelector('.sv-grid-pagination')).toBeTruthy())
238
+ expect(api.getOption('pageSizeOptions')).toEqual([7, 14, 21])
239
+ // Auto size: 100px of body over 20px rows is a page of five.
240
+ await vi.waitFor(() => expect(calls.at(-1)).toBe(5))
241
+ destroy()
242
+ } finally {
243
+ if (desc) Object.defineProperty(HTMLElement.prototype, 'clientHeight', desc)
244
+ else delete (HTMLElement.prototype as { clientHeight?: number }).clientHeight
245
+ }
246
+ }, 20_000)
247
+ })
248
+
249
+ describe('editing under a row model', () => {
250
+ type Node = Row & { kind: "group" | "leaf" }
251
+ it('refuses to edit a server group row or a placeholder, and edits a leaf', async () => {
252
+ const nodes: Node[] = [
253
+ { id: 1, name: 'EMEA', kind: 'group' },
254
+ { id: 2, name: 'Ada', kind: 'leaf' },
255
+ ]
256
+ const model: GridRowModel<Node> = {
257
+ subscribe: () => () => {},
258
+ getRows: () => nodes,
259
+ isLoading: () => false,
260
+ getRowId: (r) => String(r.id),
261
+ group: {
262
+ isGroup: (r) => r.kind === 'group',
263
+ level: () => 0,
264
+ expanded: () => false,
265
+ onToggle: () => {},
266
+ },
267
+ }
268
+ const { api, target, destroy } = await mountGrid({
269
+ rowModel: model,
270
+ columns: [{ field: 'name', header: 'Name', width: 160, editorType: 'text' }],
271
+ editable: true,
272
+ })
273
+ try {
274
+ // The group row: nothing opens, whatever the column says.
275
+ api.startEditing(0, 'name')
276
+ await tick()
277
+ expect(target.querySelector('.sv-grid-cell input')).toBeNull()
278
+ // The leaf: an editor.
279
+ api.startEditing(1, 'name')
280
+ await vi.waitFor(() => expect(target.querySelector('.sv-grid-cell input')).not.toBeNull())
281
+ } finally {
282
+ destroy()
283
+ }
284
+ })
285
+ })
286
+
287
+ describe('the rowModel prop with virtualization on', () => {
288
+ /** 100k rows, so only a window is rendered and most rows are placeholders. */
289
+ function bigSource(): ServerDataSource<Row> {
290
+ return {
291
+ async getRows(req) {
292
+ const rows: Row[] = []
293
+ for (let i = req.startRow; i < Math.min(req.endRow, 100_000); i += 1) {
294
+ rows.push({ id: i, name: `Row ${i}` })
295
+ }
296
+ return { rows, rowCount: 100_000 }
297
+ },
298
+ }
299
+ }
300
+
301
+ it('renders skeletons for the rows it has not loaded', async () => {
302
+ const ctl = createServerDataSource<Row>(bigSource(), {
303
+ mode: 'infinite',
304
+ blockSize: 100,
305
+ onChange: () => {},
306
+ })
307
+ const { target, destroy } = await mountGrid({
308
+ rowModel: ctl,
309
+ virtualization: true,
310
+ containerHeight: 300,
311
+ rowHeight: 30,
312
+ })
313
+
314
+ // Block 0 landed, so the window at the top is real data.
315
+ await vi.waitFor(() => expect(target.textContent).toContain('Row 0'))
316
+
317
+ // The grid claims the whole table, and everything outside the loaded
318
+ // block is a placeholder - which is what keeps the scrollbar honest.
319
+ const rows = ctl.getRows()
320
+ expect(rows).toHaveLength(100_000)
321
+ expect(ctl.rowPlaceholder?.(rows[50_000] as Row, 50_000)).toBe('loading')
322
+ destroy()
323
+ ctl.dispose()
324
+ }, 20_000)
325
+
326
+ it('draws skeleton cells for a row the model calls a placeholder', async () => {
327
+ // Straight through the seam, with no fetching involved: the question here
328
+ // is only whether `rowModel.rowPlaceholder` reaches the renderer. Driving
329
+ // it from a real controller cannot answer that - with virtualization off
330
+ // the grid reports every row as visible, so every block loads and no
331
+ // placeholder survives to be rendered.
332
+ const rows: Row[] = [
333
+ { id: 0, name: 'Ada' },
334
+ { id: 1, name: 'Grace' },
335
+ { id: 2, name: 'Linus' },
336
+ ]
337
+ const model: GridRowModel<Row> = {
338
+ subscribe: () => () => {},
339
+ getRows: () => rows,
340
+ isLoading: () => false,
341
+ rowPlaceholder: (row) => (row.id === 2 ? 'loading' : null),
342
+ }
343
+ const { target, destroy } = await mountGrid({ rowModel: model })
344
+ await tick()
345
+
346
+ const placeholder = target.querySelector('.sv-grid-placeholder-row')!
347
+ expect(placeholder).not.toBeNull()
348
+ expect(placeholder.querySelectorAll('.sv-grid-placeholder-skeleton').length).toBe(cols.length)
349
+ expect(target.textContent).toContain('Ada')
350
+ destroy()
351
+ })
352
+ })