@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
@@ -7,11 +7,24 @@
7
7
  *
8
8
  * This mirrors the editor kit's `resolveMessages` pattern (editor-contract.ts) so
9
9
  * chrome and editors share one localization idiom.
10
+ *
11
+ * The chart panel's strings are the other half of `GridMessages`
12
+ * (`GridChartPanelMessages` in chart-panel-messages.ts): same map, same
13
+ * `localization.text`, but their English defaults load with the panel, not
14
+ * with every grid.
10
15
  */
11
16
  import { resolveMessages } from './editor-contract'
17
+ import type { GridChartPanelMessages } from './chart-panel-messages'
18
+
19
+ /**
20
+ * Every string `localization.text` accepts: the grid chrome's own
21
+ * ({@link GridChromeMessages}) and the chart panel's
22
+ * ({@link GridChartPanelMessages}).
23
+ */
24
+ export type GridMessages = GridChromeMessages & GridChartPanelMessages
12
25
 
13
26
  /** Every localizable chrome string, grouped by area in the comments. */
14
- export type GridMessages = {
27
+ export type GridChromeMessages = {
15
28
  // Empty / loading state
16
29
  noRows: string
17
30
  loading: string
@@ -93,16 +106,23 @@ export type GridMessages = {
93
106
  bulkEditCancel: string
94
107
  bulkEditNoFields: string
95
108
  chartRange: string
109
+ // Rows whose data has not arrived (server-side / infinite loading)
110
+ rowLoadFailed: string
111
+ rowLoadRetry: string
96
112
  // Upsell notes (enterprise views not installed)
97
113
  pivotUpsellTitle: string
98
114
  pivotUpsellBody: string
115
+ ganttUpsellTitle: string
116
+ ganttUpsellBody: string
117
+ /** Placeholder in the Gantt view's search box. */
118
+ ganttSearchPlaceholder: string
99
119
  /** Shared licensing line under every Enterprise upsell note. */
100
120
  upsellLicense: string
101
121
  upsellLicenseLink: string
102
122
  }
103
123
 
104
124
  /** English defaults - the literal strings the grid shipped before localization. */
105
- export const defaultGridMessages: GridMessages = {
125
+ export const defaultGridMessages: GridChromeMessages = {
106
126
  noRows: 'No rows to display.',
107
127
  loading: 'Loading grid data...',
108
128
  columns: 'Columns',
@@ -172,8 +192,13 @@ export const defaultGridMessages: GridMessages = {
172
192
  opBefore: 'Before',
173
193
  opAfter: 'After',
174
194
  chartRange: 'Chart selected range',
195
+ rowLoadFailed: 'Could not load these rows.',
196
+ rowLoadRetry: 'Retry',
175
197
  pivotUpsellTitle: 'Pivot mode',
176
198
  pivotUpsellBody: 'Pivot mode is an Enterprise feature. Install @svgrid/enterprise and call enablePivot() to use it.',
199
+ ganttUpsellTitle: 'Gantt view',
200
+ ganttUpsellBody: 'The Gantt view is an Enterprise feature. Install @svgrid/enterprise and call enableGanttView() to render it.',
201
+ ganttSearchPlaceholder: 'Search tasks...',
177
202
  upsellLicense:
178
203
  'A license key is required for production use. Without one the feature still works, but the grid renders an unlicensed watermark.',
179
204
  upsellLicenseLink: 'View licensing',
@@ -184,6 +209,6 @@ export const defaultGridMessages: GridMessages = {
184
209
  * values fall back to the default (via `resolveMessages`), so a partial map only
185
210
  * replaces the keys it sets.
186
211
  */
187
- export function resolveGridMessages(overrides?: Partial<GridMessages> | null): GridMessages {
212
+ export function resolveGridMessages(overrides?: Partial<GridMessages> | null): GridChromeMessages {
188
213
  return resolveMessages(defaultGridMessages, overrides)
189
214
  }
@@ -0,0 +1,196 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import {
3
+ undoHistory,
4
+ redoHistory,
5
+ pushHistory,
6
+ runHistoryGroup,
7
+ nextGroupId,
8
+ type HistoryStep,
9
+ } from './history'
10
+
11
+ function step(n: number, over: Partial<HistoryStep> = {}): HistoryStep {
12
+ return { rowId: `r${n}`, columnId: 'c', field: 'c', before: n, after: n * 10, ...over }
13
+ }
14
+
15
+ function makeCtx(history: HistoryStep[] = [], limit = 200) {
16
+ const applied: Array<{ rowId: string; direction: string }> = []
17
+ const ctx = {
18
+ history,
19
+ historyPtr: history.length - 1,
20
+ historyVersion: 0,
21
+ historyGroupId: undefined as string | undefined,
22
+ UNDO_LIMIT: limit,
23
+ applyHistoryStep: vi.fn((s: HistoryStep, direction: 'undo' | 'redo') => {
24
+ applied.push({ rowId: s.rowId, direction })
25
+ }),
26
+ }
27
+ return { ctx, applied }
28
+ }
29
+
30
+ describe('undo / redo without groups', () => {
31
+ it('walks one step per call', () => {
32
+ const { ctx, applied } = makeCtx([step(1), step(2)])
33
+ expect(undoHistory(ctx)).toBe(true)
34
+ expect(applied).toEqual([{ rowId: 'r2', direction: 'undo' }])
35
+ expect(ctx.historyPtr).toBe(0)
36
+ })
37
+
38
+ it('returns false at the bottom and the top', () => {
39
+ const { ctx } = makeCtx([])
40
+ expect(undoHistory(ctx)).toBe(false)
41
+ expect(redoHistory(ctx)).toBe(false)
42
+ })
43
+
44
+ it('round-trips undo then redo', () => {
45
+ const { ctx, applied } = makeCtx([step(1)])
46
+ undoHistory(ctx)
47
+ expect(ctx.historyPtr).toBe(-1)
48
+ redoHistory(ctx)
49
+ expect(ctx.historyPtr).toBe(0)
50
+ expect(applied.map((a) => a.direction)).toEqual(['undo', 'redo'])
51
+ })
52
+ })
53
+
54
+ describe('undo / redo with groups', () => {
55
+ it('undoes a whole group in one call', () => {
56
+ const g = 'g1'
57
+ const { ctx, applied } = makeCtx([
58
+ step(1),
59
+ step(2, { groupId: g }), step(3, { groupId: g }), step(4, { groupId: g }),
60
+ ])
61
+ expect(undoHistory(ctx)).toBe(true)
62
+ expect(applied.map((a) => a.rowId)).toEqual(['r4', 'r3', 'r2'])
63
+ // The pointer lands before the group, so the next undo takes the lone step.
64
+ expect(ctx.historyPtr).toBe(0)
65
+ })
66
+
67
+ it('undoes a group newest-first so repeated writes to one cell unwind right', () => {
68
+ // Both steps touch r1. Applying oldest-first would leave the cell holding
69
+ // the intermediate value instead of the original.
70
+ const g = 'g1'
71
+ const { ctx, applied } = makeCtx([
72
+ { rowId: 'r1', columnId: 'c', field: 'c', before: 'orig', after: 'mid', groupId: g },
73
+ { rowId: 'r1', columnId: 'c', field: 'c', before: 'mid', after: 'final', groupId: g },
74
+ ])
75
+ undoHistory(ctx)
76
+ expect(applied).toEqual([
77
+ { rowId: 'r1', direction: 'undo' },
78
+ { rowId: 'r1', direction: 'undo' },
79
+ ])
80
+ expect(ctx.applyHistoryStep.mock.calls[0]![0].before).toBe('mid')
81
+ expect(ctx.applyHistoryStep.mock.calls[1]![0].before).toBe('orig')
82
+ })
83
+
84
+ it('redoes a whole group in order', () => {
85
+ const g = 'g1'
86
+ const { ctx, applied } = makeCtx([
87
+ step(1, { groupId: g }), step(2, { groupId: g }), step(3, { groupId: g }),
88
+ ])
89
+ ctx.historyPtr = -1
90
+ expect(redoHistory(ctx)).toBe(true)
91
+ expect(applied.map((a) => a.rowId)).toEqual(['r1', 'r2', 'r3'])
92
+ expect(ctx.historyPtr).toBe(2)
93
+ })
94
+
95
+ it('keeps two adjacent groups separate', () => {
96
+ const { ctx, applied } = makeCtx([
97
+ step(1, { groupId: 'g1' }), step(2, { groupId: 'g1' }),
98
+ step(3, { groupId: 'g2' }), step(4, { groupId: 'g2' }),
99
+ ])
100
+ undoHistory(ctx)
101
+ expect(applied.map((a) => a.rowId)).toEqual(['r4', 'r3'])
102
+ expect(ctx.historyPtr).toBe(1)
103
+ undoHistory(ctx)
104
+ expect(applied.map((a) => a.rowId)).toEqual(['r4', 'r3', 'r2', 'r1'])
105
+ expect(ctx.historyPtr).toBe(-1)
106
+ })
107
+ })
108
+
109
+ describe('pushHistory', () => {
110
+ it('truncates forward history so an edit invalidates redo', () => {
111
+ const { ctx } = makeCtx([step(1), step(2), step(3)])
112
+ ctx.historyPtr = 0
113
+ pushHistory(ctx, [step(9)])
114
+ expect(ctx.history.map((s) => s.rowId)).toEqual(['r1', 'r9'])
115
+ expect(ctx.historyPtr).toBe(1)
116
+ })
117
+
118
+ it('drops the OLDEST entries when the buffer is full', () => {
119
+ const { ctx } = makeCtx([step(1), step(2), step(3)], 3)
120
+ pushHistory(ctx, [step(4)])
121
+ expect(ctx.history.map((s) => s.rowId)).toEqual(['r2', 'r3', 'r4'])
122
+ expect(ctx.historyPtr).toBe(2)
123
+ })
124
+
125
+ it('does not tag a group of one', () => {
126
+ const { ctx } = makeCtx()
127
+ pushHistory(ctx, [step(1)], nextGroupId())
128
+ expect(ctx.history[0]!.groupId).toBeUndefined()
129
+ })
130
+
131
+ it('tags every step when several are pushed together', () => {
132
+ const { ctx } = makeCtx()
133
+ pushHistory(ctx, [step(1), step(2)], nextGroupId())
134
+ const ids = ctx.history.map((s) => s.groupId)
135
+ expect(ids[0]).toBeDefined()
136
+ expect(new Set(ids).size).toBe(1)
137
+ })
138
+
139
+ it('ignores an empty push', () => {
140
+ const { ctx } = makeCtx()
141
+ pushHistory(ctx, [])
142
+ expect(ctx.history).toHaveLength(0)
143
+ expect(ctx.historyVersion).toBe(0)
144
+ })
145
+ })
146
+
147
+ describe('runHistoryGroup', () => {
148
+ it('joins separate pushes into one undoable action', () => {
149
+ const { ctx, applied } = makeCtx()
150
+ runHistoryGroup(ctx, () => {
151
+ pushHistory(ctx, [step(1)])
152
+ pushHistory(ctx, [step(2)])
153
+ pushHistory(ctx, [step(3)])
154
+ })
155
+ expect(new Set(ctx.history.map((s) => s.groupId)).size).toBe(1)
156
+ undoHistory(ctx)
157
+ expect(applied.map((a) => a.rowId)).toEqual(['r3', 'r2', 'r1'])
158
+ expect(ctx.historyPtr).toBe(-1)
159
+ })
160
+
161
+ it('survives the buffer overflowing mid-group', () => {
162
+ // This is why the group id is ambient rather than stamped afterwards: the
163
+ // cap drops entries from the front, so any index captured before the call
164
+ // would point at the wrong steps by the end of it.
165
+ const { ctx } = makeCtx([step(0)], 3)
166
+ runHistoryGroup(ctx, () => {
167
+ pushHistory(ctx, [step(1)])
168
+ pushHistory(ctx, [step(2)])
169
+ pushHistory(ctx, [step(3)])
170
+ })
171
+ expect(ctx.history.map((s) => s.rowId)).toEqual(['r1', 'r2', 'r3'])
172
+ expect(new Set(ctx.history.map((s) => s.groupId)).size).toBe(1)
173
+ })
174
+
175
+ it('nests into the outer group so one action is one undo', () => {
176
+ const { ctx } = makeCtx()
177
+ runHistoryGroup(ctx, () => {
178
+ pushHistory(ctx, [step(1)])
179
+ runHistoryGroup(ctx, () => pushHistory(ctx, [step(2)]))
180
+ })
181
+ expect(new Set(ctx.history.map((s) => s.groupId)).size).toBe(1)
182
+ })
183
+
184
+ it('clears the ambient id when the body throws', () => {
185
+ const { ctx } = makeCtx()
186
+ expect(() => runHistoryGroup(ctx, () => { throw new Error('boom') })).toThrow('boom')
187
+ expect(ctx.historyGroupId).toBeUndefined()
188
+ pushHistory(ctx, [step(1)])
189
+ expect(ctx.history[0]!.groupId).toBeUndefined()
190
+ })
191
+
192
+ it('returns the body result', () => {
193
+ const { ctx } = makeCtx()
194
+ expect(runHistoryGroup(ctx, () => 42)).toBe(42)
195
+ })
196
+ })
package/src/history.ts ADDED
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Undo / redo over the grid's history array, group-aware.
3
+ *
4
+ * The history is one ordered array of per-cell steps plus a pointer to the
5
+ * last applied step (the model lives in SvGrid.controller.svelte.ts). A step
6
+ * describes ONE cell, which is right for a single edit and wrong for anything
7
+ * that writes many cells at once: `applyMoveRange` in clipboard.ts pushes one
8
+ * step per moved cell, so undoing a 15x20 range move takes 300 presses of
9
+ * Ctrl+Z, and with UNDO_LIMIT at 200 the same move evicts the entire history
10
+ * behind it.
11
+ *
12
+ * `groupId` fixes that without changing the storage model. Steps written
13
+ * inside one logical action share an id; undo and redo walk the whole run of
14
+ * steps carrying that id and apply them together, so a paste, a fill-down or
15
+ * a range move is one press, the way it is in a spreadsheet. Steps with no
16
+ * groupId behave exactly as before, one step per press.
17
+ *
18
+ * Undo applies a group in REVERSE order. Two steps that touched the same cell
19
+ * inside one action have to be unwound newest-first or the older `before`
20
+ * value is overwritten by the newer one and the cell lands on the wrong value.
21
+ */
22
+
23
+ export type HistoryStep = {
24
+ rowId: string
25
+ columnId: string
26
+ field: string
27
+ before: unknown
28
+ after: unknown
29
+ /** Set when this step is part of a multi-cell action. Steps sharing an id
30
+ * are contiguous in the array and undo / redo as one. */
31
+ groupId?: string
32
+ /**
33
+ * A step that is not a cell write: something outside the grid's data that
34
+ * still belongs in the same Ctrl+Z stream, such as a cell format kept in a
35
+ * consumer's store. The cell fields are empty on such a step and the two
36
+ * functions do the work. Never serialised, since a closure cannot be.
37
+ */
38
+ custom?: { undo: () => void; redo: () => void }
39
+ }
40
+
41
+ /** Ctx surface these helpers touch. The controller satisfies it structurally. */
42
+ type HistoryCtx = {
43
+ history: HistoryStep[]
44
+ historyPtr: number
45
+ historyVersion: number
46
+ UNDO_LIMIT: number
47
+ /** Set while a `runHistoryGroup` call is on the stack. Every step pushed
48
+ * inside it joins that group, so a command does not have to thread a group
49
+ * id down through the writers it calls. */
50
+ historyGroupId: string | undefined
51
+ applyHistoryStep(step: HistoryStep, direction: 'undo' | 'redo'): void
52
+ }
53
+
54
+ /** Index of the first step in the group that ends at `end`, or `end` itself
55
+ * when that step is ungrouped. */
56
+ function groupStart(history: ReadonlyArray<HistoryStep>, end: number): number {
57
+ const id = history[end]?.groupId
58
+ if (!id) return end
59
+ let i = end
60
+ while (i > 0 && history[i - 1]?.groupId === id) i -= 1
61
+ return i
62
+ }
63
+
64
+ /** Index of the last step in the group that begins at `start`, or `start`
65
+ * itself when that step is ungrouped. */
66
+ function groupEnd(history: ReadonlyArray<HistoryStep>, start: number): number {
67
+ const id = history[start]?.groupId
68
+ if (!id) return start
69
+ let i = start
70
+ while (i < history.length - 1 && history[i + 1]?.groupId === id) i += 1
71
+ return i
72
+ }
73
+
74
+ /** Undo one action. Returns false when there is nothing left to undo. */
75
+ export function undoHistory(ctx: HistoryCtx): boolean {
76
+ if (ctx.historyPtr < 0) return false
77
+ const end = ctx.historyPtr
78
+ if (!ctx.history[end]) return false
79
+ const start = groupStart(ctx.history, end)
80
+ // Reverse order: see the note at the top of the file.
81
+ for (let i = end; i >= start; i -= 1) {
82
+ const step = ctx.history[i]
83
+ if (step) ctx.applyHistoryStep(step, 'undo')
84
+ }
85
+ ctx.historyPtr = start - 1
86
+ ctx.historyVersion += 1
87
+ return true
88
+ }
89
+
90
+ /** Redo one action. Returns false when there is nothing left to redo. */
91
+ export function redoHistory(ctx: HistoryCtx): boolean {
92
+ if (ctx.historyPtr >= ctx.history.length - 1) return false
93
+ const start = ctx.historyPtr + 1
94
+ if (!ctx.history[start]) return false
95
+ const end = groupEnd(ctx.history, start)
96
+ for (let i = start; i <= end; i += 1) {
97
+ const step = ctx.history[i]
98
+ if (step) ctx.applyHistoryStep(step, 'redo')
99
+ }
100
+ ctx.historyPtr = end
101
+ ctx.historyVersion += 1
102
+ return true
103
+ }
104
+
105
+ /**
106
+ * Append steps at the pointer, truncating any forward history (the standard
107
+ * "an edit invalidates redo" rule) and capping the buffer at UNDO_LIMIT by
108
+ * dropping the OLDEST entries.
109
+ *
110
+ * Every writer goes through here rather than open-coding the slice / push /
111
+ * cap dance, which editing.ts and clipboard.ts previously did three times
112
+ * with three slightly different shapes.
113
+ */
114
+ export function pushHistory(
115
+ ctx: HistoryCtx,
116
+ steps: ReadonlyArray<HistoryStep>,
117
+ groupId?: string,
118
+ ): void {
119
+ if (!steps.length) return
120
+ // An ambient group wins: inside runHistoryGroup even a single step has to be
121
+ // tagged, because a later push in the same action must join it.
122
+ //
123
+ // Outside one, a group of one is just a step. Tagging it would make undo do
124
+ // the same work through a slower path and would show up in serialized state.
125
+ const id = ctx.historyGroupId ?? (groupId && steps.length > 1 ? groupId : undefined)
126
+ let next = ctx.history.slice(0, ctx.historyPtr + 1)
127
+ for (const step of steps) next.push(id ? { ...step, groupId: id } : step)
128
+ if (next.length > ctx.UNDO_LIMIT) next = next.slice(next.length - ctx.UNDO_LIMIT)
129
+ ctx.history = next
130
+ ctx.historyPtr = ctx.history.length - 1
131
+ ctx.historyVersion += 1
132
+ }
133
+
134
+ let groupCounter = 0
135
+
136
+ /** A fresh group id. Monotonic, not random: ids never leave the session and a
137
+ * counter is comparable in a test snapshot. */
138
+ export function nextGroupId(): string {
139
+ groupCounter += 1
140
+ return `g${groupCounter}`
141
+ }
142
+
143
+ /**
144
+ * Run `fn` with every history step it writes tagged as one undoable action.
145
+ *
146
+ * This is ambient rather than a wrapper that re-tags afterwards, because
147
+ * `pushHistory` can drop the oldest entries when the buffer hits UNDO_LIMIT.
148
+ * A re-tagging implementation would compute its slice bounds from a length
149
+ * that had shifted underneath it and stamp the wrong steps.
150
+ *
151
+ * Nested calls join the outer group: one user action is one undo, however many
152
+ * layers of helper it goes through.
153
+ */
154
+ export function runHistoryGroup<T>(ctx: HistoryCtx, fn: () => T): T {
155
+ const outer = ctx.historyGroupId
156
+ ctx.historyGroupId = outer ?? nextGroupId()
157
+ try {
158
+ return fn()
159
+ } finally {
160
+ ctx.historyGroupId = outer
161
+ }
162
+ }
@@ -20,26 +20,25 @@ const SRC = join(process.cwd(), 'src')
20
20
  const read = (f: string) => readFileSync(join(SRC, f), 'utf8')
21
21
 
22
22
  /** Files that render grid chrome and therefore must go through the seam. */
23
- const CHROME = ['SvGrid.svelte', 'GridMenus.svelte', 'GridFooter.svelte', 'SvRowGroupPanel.svelte']
23
+ const CHROME = ['SvGrid.svelte', 'GridMenus.svelte', 'GridFooter.svelte']
24
24
 
25
25
  /**
26
26
  * Drop the regions allowed to contain raw icon markup: the `icon` snippet's own
27
- * body (which IS the built-in set), the `ic` snippet in SvRowGroupPanel, and
28
- * every `<script>` block.
27
+ * body (which IS the built-in set) and every `<script>` block.
29
28
  */
30
29
  function stripAllowedRegions(src: string): string {
31
30
  let out = src.replace(/<script[\s\S]*?<\/script>/g, '')
32
31
  // The built-in catalogue: from the snippet header to its closing tag.
33
32
  out = out.replace(/\{#snippet icon\(name: GridIconName\)\}[\s\S]*?\n\{\/snippet\}/g, '')
34
- out = out.replace(/\{#snippet ic\(name: GridIconName\)\}[\s\S]*?\{\/snippet\}/g, '')
35
33
  return out
36
34
  }
37
35
 
38
36
  describe('icon seam', () => {
39
37
  it('leaves no inline <svg> outside the built-in catalogue', () => {
40
38
  // Data visualisation is not chrome: these draw values, not affordances, and
41
- // there is nothing for a consumer's icon set to say about them.
42
- const ALLOWED = ['sv-grid-sparkline']
39
+ // there is nothing for a consumer's icon set to say about them. Nor is a
40
+ // context-menu item's own path icon: that IS the consumer's drawing.
41
+ const ALLOWED = ['sv-grid-sparkline', 'sv-grid-menu-icon-own']
43
42
 
44
43
  const offenders: string[] = []
45
44
  for (const file of CHROME) {