@svgrid/grid 3.0.3 → 4.0.0

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 (318) hide show
  1. package/README.md +5 -4
  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 +845 -207
  9. package/dist/SvGrid.css +178 -2
  10. package/dist/SvGrid.svelte +467 -516
  11. package/dist/SvGrid.types.d.ts +315 -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-CLI93jke.js +635 -0
  30. package/dist/cdn/{GridMenus-DhdPF700.js → GridMenus-CwooL3QA.js} +240 -207
  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-B6HXdfHY.js} +10448 -10049
  68. package/dist/cdn/{src-Bugf5XlI.js → src-C6Iowdvs.js} +6908 -6509
  69. package/dist/cdn/svgrid.js +27 -15
  70. package/dist/cdn/svgrid.svelte-external.js +27 -15
  71. package/dist/cdn/validate-CchXzyrX.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/grid-icons.d.ts +2 -2
  143. package/dist/grid-icons.js +2 -2
  144. package/dist/grid-messages.d.ts +12 -3
  145. package/dist/grid-messages.js +7 -0
  146. package/dist/history.d.ts +83 -0
  147. package/dist/history.js +133 -0
  148. package/dist/index.d.ts +27 -10
  149. package/dist/index.js +22 -5
  150. package/dist/keyboard-handlers.js +88 -31
  151. package/dist/keyboard.d.ts +55 -0
  152. package/dist/keyboard.js +132 -0
  153. package/dist/menus.d.ts +3 -1
  154. package/dist/menus.js +7 -2
  155. package/dist/merges.d.ts +97 -0
  156. package/dist/merges.js +147 -0
  157. package/dist/row-model.d.ts +133 -0
  158. package/dist/row-model.js +16 -0
  159. package/dist/row-resize.d.ts +3 -0
  160. package/dist/row-resize.js +14 -0
  161. package/dist/scroll-sync.d.ts +1 -0
  162. package/dist/scroll-sync.js +24 -0
  163. package/dist/selection.d.ts +18 -1
  164. package/dist/selection.js +138 -18
  165. package/dist/server-block-cache.d.ts +214 -0
  166. package/dist/server-block-cache.js +531 -0
  167. package/dist/server-data-source.d.ts +240 -6
  168. package/dist/server-data-source.js +220 -20
  169. package/dist/server.d.ts +13 -0
  170. package/dist/server.js +13 -0
  171. package/dist/shortcut-registry.d.ts +145 -0
  172. package/dist/shortcut-registry.js +44 -0
  173. package/dist/svgrid-wrapper.types.d.ts +213 -22
  174. package/dist/validate.d.ts +2 -0
  175. package/dist/validate.js +8 -3
  176. package/dist/virtualization/virtualizer.js +38 -17
  177. package/package.json +11 -1
  178. package/src/GridMenus.svelte +44 -2
  179. package/src/SvChartMenu.svelte +69 -0
  180. package/src/SvChartPanes.svelte +113 -0
  181. package/src/SvChartPanes.test.ts +66 -0
  182. package/src/SvGrid.controller.svelte.ts +818 -179
  183. package/src/SvGrid.css +178 -2
  184. package/src/SvGrid.svelte +467 -516
  185. package/src/SvGrid.types.ts +293 -11
  186. package/src/SvGridCellEditor.svelte +17 -0
  187. package/src/SvGridChart.svelte +2257 -329
  188. package/src/SvGridChart.test.ts +1419 -1
  189. package/src/SvGridChart.types.ts +336 -0
  190. package/src/SvGridChartBuilder.svelte +323 -0
  191. package/src/SvGridChartPanel.svelte +221 -275
  192. package/src/SvGridChartPickers.svelte +417 -0
  193. package/src/SvGridChartView.svelte +13 -14
  194. package/src/SvModal.svelte +7 -1
  195. package/src/SvModal.test.ts +9 -0
  196. package/src/ai.test.ts +57 -2
  197. package/src/ai.ts +166 -22
  198. package/src/aria.d.ts +11 -0
  199. package/src/build-api.coverage.test.ts +184 -0
  200. package/src/build-api.ts +118 -22
  201. package/src/chart-axes.test.ts +573 -0
  202. package/src/chart-axes.ts +342 -0
  203. package/src/chart-cartesian.ts +1863 -0
  204. package/src/chart-decimate.test.ts +249 -0
  205. package/src/chart-decimate.ts +186 -0
  206. package/src/chart-export-pdf.test.ts +91 -0
  207. package/src/chart-export-pdf.ts +207 -0
  208. package/src/chart-export.test.ts +14 -0
  209. package/src/chart-export.ts +13 -3
  210. package/src/chart-financial.ts +208 -0
  211. package/src/chart-flow.ts +286 -0
  212. package/src/chart-format.ts +67 -0
  213. package/src/chart-grid.ts +179 -0
  214. package/src/chart-hierarchy.ts +135 -0
  215. package/src/chart-indicators.test.ts +443 -0
  216. package/src/chart-indicators.ts +384 -0
  217. package/src/chart-messages.test.ts +21 -0
  218. package/src/chart-messages.ts +184 -0
  219. package/src/chart-motion.test.ts +71 -0
  220. package/src/chart-motion.ts +148 -0
  221. package/src/chart-panel-messages.test.ts +29 -0
  222. package/src/chart-panel-messages.ts +550 -0
  223. package/src/chart-pivot.test.ts +54 -0
  224. package/src/chart-pivot.ts +225 -0
  225. package/src/chart-polar.ts +674 -0
  226. package/src/chart-samples.ts +106 -0
  227. package/src/chart-scale.ts +394 -0
  228. package/src/chart-series-types.test.ts +714 -0
  229. package/src/chart-stats.ts +409 -0
  230. package/src/chart-stream.test.ts +75 -0
  231. package/src/chart-stream.ts +114 -0
  232. package/src/chart-summary.test.ts +90 -0
  233. package/src/chart-summary.ts +197 -0
  234. package/src/chart-sync.svelte.ts +41 -0
  235. package/src/chart-table.test.ts +76 -0
  236. package/src/chart-table.ts +178 -0
  237. package/src/chart-trend.ts +47 -0
  238. package/src/chart-types.ts +1309 -0
  239. package/src/chart-validate.test.ts +100 -0
  240. package/src/chart-validate.ts +247 -0
  241. package/src/chart-zoom.test.ts +137 -0
  242. package/src/chart-zoom.ts +124 -0
  243. package/src/chart.coverage.test.ts +81 -0
  244. package/src/chart.test.ts +56 -3
  245. package/src/chart.ts +557 -2784
  246. package/src/clipboard.test.ts +151 -0
  247. package/src/clipboard.ts +145 -16
  248. package/src/column-resize.ts +7 -1
  249. package/src/columns.ts +2 -0
  250. package/src/command-context.test.ts +200 -0
  251. package/src/command-context.ts +137 -0
  252. package/src/core.rowmodel-cache.test.ts +51 -0
  253. package/src/core.ts +40 -12
  254. package/src/editing.test.ts +281 -3
  255. package/src/editing.ts +264 -38
  256. package/src/fill-patterns.test.ts +35 -0
  257. package/src/fill-patterns.ts +56 -2
  258. package/src/grid-icons.ts +2 -2
  259. package/src/grid-messages.ts +21 -3
  260. package/src/history.test.ts +196 -0
  261. package/src/history.ts +162 -0
  262. package/src/icon-seam.test.ts +5 -6
  263. package/src/index.ts +169 -12
  264. package/src/keyboard-handlers.coverage.test.ts +126 -6
  265. package/src/keyboard-handlers.ts +91 -30
  266. package/src/keyboard-shortcuts.seam.test.ts +200 -0
  267. package/src/keyboard.test.ts +124 -1
  268. package/src/keyboard.ts +142 -0
  269. package/src/menus.test.ts +26 -0
  270. package/src/menus.ts +10 -3
  271. package/src/merges.test.ts +112 -0
  272. package/src/merges.ts +168 -0
  273. package/src/pivot.grid.test.ts +40 -5
  274. package/src/row-model.ts +146 -0
  275. package/src/row-resize.test.ts +44 -0
  276. package/src/row-resize.ts +17 -0
  277. package/src/scroll-sync.test.ts +42 -1
  278. package/src/scroll-sync.ts +25 -0
  279. package/src/selection.multi-range.test.ts +34 -0
  280. package/src/selection.test.ts +33 -2
  281. package/src/selection.ts +143 -21
  282. package/src/server-block-cache.test.ts +645 -0
  283. package/src/server-block-cache.ts +677 -0
  284. package/src/server-data-source.infinite.test.ts +343 -0
  285. package/src/server-data-source.ts +450 -27
  286. package/src/server.ts +47 -0
  287. package/src/shortcut-registry.test.ts +97 -0
  288. package/src/shortcut-registry.ts +181 -0
  289. package/src/svgrid-wrapper.types.ts +184 -13
  290. package/src/svgrid.behavior.test.ts +135 -0
  291. package/src/svgrid.charting.test.ts +513 -3
  292. package/src/svgrid.context-menu.test.ts +31 -0
  293. package/src/svgrid.new-features.wrapper.test.ts +10 -3
  294. package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
  295. package/src/svgrid.row-model-seam.svelte.test.ts +289 -0
  296. package/src/validate.test.ts +10 -0
  297. package/src/validate.ts +10 -3
  298. package/src/virtualization/virtualizer.ts +39 -17
  299. package/dist/SvGroupCell.svelte +0 -141
  300. package/dist/SvGroupCell.svelte.d.ts +0 -49
  301. package/dist/SvRowGroupPanel.svelte +0 -186
  302. package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
  303. package/dist/cdn/GridMenus-CHOMnW-c.js +0 -602
  304. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
  305. package/dist/cdn/SvGridChart-1PMSp2LZ.js +0 -1481
  306. package/dist/cdn/SvGridChart-D02avhyr.js +0 -1480
  307. package/dist/cdn/SvGridChartPanel-DJnViOv4.js +0 -834
  308. package/dist/cdn/SvGridChartPanel-vPTNerFZ.js +0 -810
  309. package/dist/cdn/SvGridChartView-C--HhdUo.js +0 -56
  310. package/dist/cdn/SvGridChartView-KyyFG-wY.js +0 -55
  311. package/dist/cdn/chart-DLg3_zTQ.js +0 -1652
  312. package/dist/cdn/validate-_CDJzgIo.js +0 -75
  313. package/dist/server-group-model.d.ts +0 -98
  314. package/dist/server-group-model.js +0 -263
  315. package/src/SvGroupCell.svelte +0 -141
  316. package/src/SvRowGroupPanel.svelte +0 -186
  317. package/src/server-group-model.test.ts +0 -294
  318. package/src/server-group-model.ts +0 -370
package/src/editing.ts CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  type TableFeatures,
12
12
  } from "./index";
13
13
  import "./sv-grid-scrollbar";
14
- import { getNextActiveCell } from "./keyboard";
14
+ import { getEntryStep } from "./keyboard";
15
15
  import {
16
16
  getCellKey,
17
17
  toValueArray,
@@ -21,14 +21,10 @@ import {
21
21
  getColumnBaseValue,
22
22
  isGroupRow,
23
23
  } from "./cell-values";
24
+ import { pushHistory, nextGroupId, runHistoryGroup, type HistoryStep } from "./history";
25
+ import { hasGridShortcuts, runGridShortcuts } from "./shortcut-registry";
26
+ import { buildCommandContext } from "./command-context";
24
27
 
25
- type HistoryStep = {
26
- rowId: string;
27
- columnId: string;
28
- field: string;
29
- before: unknown;
30
- after: unknown;
31
- };
32
28
 
33
29
  export function createEditing<
34
30
  TFeatures extends TableFeatures = TableFeatures,
@@ -51,6 +47,14 @@ export function createEditing<
51
47
  function isCellEditable(column: Column<TData>, row?: Row<TData>): boolean {
52
48
  const editable = column.columnDef.editable;
53
49
  if (editable === false) return false;
50
+ if (row) {
51
+ // A server-side group row is a key with aggregates under the leaf
52
+ // columns, and a placeholder row has no data yet: neither takes an
53
+ // edit, whatever the column says.
54
+ const serverGroup = ctx.props.serverGroup;
55
+ if (serverGroup && row.original !== undefined && serverGroup.isGroup(row.original)) return false;
56
+ if (ctx.placeholderStateOf?.(row)) return false;
57
+ }
54
58
  if (typeof editable !== "function") return true;
55
59
  if (!row) return true;
56
60
  const cellCtx: CellContext<TData> = {
@@ -132,11 +136,29 @@ export function createEditing<
132
136
  rowRef: row.original,
133
137
  };
134
138
  ctx.setActiveCell(rowIndex, colIndex);
135
- ctx.setSelection(rowIndex, colIndex);
139
+ selectForEdit(rowIndex, colIndex);
136
140
  return true;
137
141
  }
138
142
 
143
+ /**
144
+ * The selection an edit starts with. A cell already inside the selection
145
+ * leaves it standing: typing into the active cell of a selected block
146
+ * used to collapse the block to that one cell, so Enter could never walk
147
+ * on inside it. A cell outside the selection becomes the selection.
148
+ */
149
+ function selectForEdit(rowIndex: number, colIndex: number) {
150
+ if (ctx.isCellInSelectedRange(rowIndex, colIndex)) return;
151
+ ctx.setSelection(rowIndex, colIndex);
152
+ }
153
+
139
154
  function saveEditingCell() {
155
+ // One group: a consumer's onCellValueChange may record a step of its own
156
+ // (a spreadsheet gives "12%" a percent format as it lands), and that
157
+ // belongs to the same Ctrl+Z as the value.
158
+ runHistoryGroup(ctx, () => commitEditingCell());
159
+ }
160
+
161
+ function commitEditingCell() {
140
162
  if (!ctx.editingCell) return;
141
163
  const editing = ctx.editingCell;
142
164
  // Resolve the row this edit belongs to. `allRows` is only the current page,
@@ -197,19 +219,7 @@ export function createEditing<
197
219
  before: oldValue,
198
220
  after: finalValue,
199
221
  }
200
- const truncated = ctx.history.slice(0, ctx.historyPtr + 1)
201
- truncated.push(step)
202
- // Cap the buffer at UNDO_LIMIT; drop the OLDEST entries so the
203
- // pointer stays valid relative to the newest steps.
204
- if (truncated.length > ctx.UNDO_LIMIT) {
205
- const drop = truncated.length - ctx.UNDO_LIMIT
206
- ctx.history = truncated.slice(drop)
207
- ctx.historyPtr = ctx.history.length - 1
208
- } else {
209
- ctx.history = truncated
210
- ctx.historyPtr = ctx.history.length - 1
211
- }
212
- ctx.historyVersion += 1
222
+ pushHistory(ctx, [step])
213
223
  }
214
224
  // Notify the consumer AFTER the row has been updated so any callback-
215
225
  // driven recompute (cascade totals, server save, undo stack) sees the
@@ -230,6 +240,11 @@ export function createEditing<
230
240
  /** Apply an undo / redo step directly to the underlying row, bypassing
231
241
  * the editor pipeline so we don't accidentally re-push to the stack. */
232
242
  function applyHistoryStep(step: HistoryStep, direction: 'undo' | 'redo') {
243
+ if (step.custom) {
244
+ if (direction === 'undo') step.custom.undo()
245
+ else step.custom.redo()
246
+ return
247
+ }
233
248
  const row = ctx.allRows.find((r: any) => r.id === step.rowId)
234
249
  const col = ctx.allColumns.find((c: any) => c.id === step.columnId)
235
250
  if (!row?.original || !col) return
@@ -261,26 +276,111 @@ export function createEditing<
261
276
  * move, Tab committed the edit and then let the browser walk focus out of the
262
277
  * grid entirely (#48). Shared by every editor type, including the textarea.
263
278
  */
264
- function commitAndMoveByTab(shiftKey: boolean) {
279
+ /**
280
+ * Commit the edit and step the cursor the way data entry expects: Tab a
281
+ * column to the right (wrapping at the row's end), Enter a row down, Shift
282
+ * reversing either. Enter used to commit and stay, so a column of numbers
283
+ * took a click or an arrow between every value; Excel and every other
284
+ * sheet move down, and the accessibility page has always said this grid
285
+ * does too. At the last row or column the cursor stays put.
286
+ */
287
+ function commitAndMove(intent: "tabNext" | "tabPrev" | "moveDown" | "moveUp") {
265
288
  const active = ctx.activeCell;
266
289
  saveEditingCell();
267
290
  ctx.gridRootEl?.focus({ preventScroll: true });
268
291
  if (!active) return;
269
- const next = getNextActiveCell(active, shiftKey ? "tabPrev" : "tabNext", {
292
+ // The same stepping as Enter and Tab on the grid root: a Tab run's Enter
293
+ // goes back to the run's first column, and a selected block keeps the
294
+ // cursor inside it.
295
+ const step = getEntryStep(active, intent, {
270
296
  maxRow: ctx.allRows.length - 1,
271
297
  maxCol: ctx.allColumns.length - 1,
298
+ tabOrigin: ctx.tabRunOrigin,
299
+ range: ctx.activeRangeRect(),
300
+ collapsed: {
301
+ isRowCollapsed: (i: number) => ctx.isRowCollapsed(i) as boolean,
302
+ isColumnCollapsed: (i: number) => !!ctx.collapsedColumns[ctx.allColumns[i]?.id],
303
+ },
272
304
  });
305
+ const next = step.cell;
273
306
  ctx.setActiveCell(next.rowIndex, next.colIndex);
274
- ctx.setSelection(next.rowIndex, next.colIndex);
307
+ ctx.tabRunOrigin = step.tabOrigin;
308
+ if (!step.withinRange) ctx.setSelection(next.rowIndex, next.colIndex);
275
309
  ctx.scrollActiveCellIntoView(next.rowIndex, next.colIndex);
276
310
  }
277
311
 
312
+ function commitAndMoveByTab(shiftKey: boolean) {
313
+ commitAndMove(shiftKey ? "tabPrev" : "tabNext");
314
+ }
315
+
316
+ /**
317
+ * Ctrl+Enter: the entry goes into every cell of the selected block and the
318
+ * cursor stays where it is, as in Excel, where it is how a block is filled
319
+ * with one value in one go. Each cell takes the same raw entry through the
320
+ * ordinary commit, so its column's parser, the history and
321
+ * `onCellValueChange` all see it, and the whole block is one undo. A cell
322
+ * that cannot be edited is left alone. With nothing but the active cell
323
+ * selected it is a commit that does not move.
324
+ */
325
+ function commitToSelection() {
326
+ const editing = ctx.editingCell;
327
+ if (!editing) return;
328
+ const raw = editing.value;
329
+ const active = ctx.activeCell;
330
+ const range = ctx.activeRangeRect();
331
+ runHistoryGroup(ctx, () => {
332
+ commitEditingCell();
333
+ if (!active || !range) return;
334
+ for (let r = range.minRow; r <= range.maxRow; r += 1) {
335
+ for (let c = range.minCol; c <= range.maxCol; c += 1) {
336
+ if (r === active.rowIndex && c === active.colIndex) continue;
337
+ const row = ctx.allRows[r];
338
+ const column = ctx.allColumns[c];
339
+ if (!row || !column || isGroupRow(row) || !isCellEditable(column, row)) continue;
340
+ ctx.editingCell = {
341
+ rowId: row.id,
342
+ columnId: column.id,
343
+ editorType: (column.columnDef.editorType ?? "text") as CellEditorType,
344
+ value: raw,
345
+ rowRef: row.original,
346
+ };
347
+ commitEditingCell();
348
+ }
349
+ }
350
+ });
351
+ ctx.gridRootEl?.focus({ preventScroll: true });
352
+ }
353
+
278
354
  function onEditorKeyDown(event: KeyboardEvent) {
279
355
  event.stopPropagation();
280
- if (event.key === "Enter") {
356
+ // Registered commands get the key first here too, with `editing: true`, so
357
+ // a handler can claim a combination that only means something mid-edit
358
+ // (Alt+Enter for a newline, F4 to cycle a reference between relative and
359
+ // absolute) before the editor's own Enter / Tab / Escape handling runs.
360
+ if (hasGridShortcuts() && runGridShortcuts(event, buildCommandContext(ctx, true))) {
361
+ return;
362
+ }
363
+ if (event.key === "Enter" && event.altKey) {
364
+ // A line break, in an editor that can hold one (`editorMultiline`).
365
+ // In a single-line editor Alt+Enter is just Enter.
366
+ const el = event.currentTarget;
367
+ if (el instanceof HTMLTextAreaElement) {
368
+ event.preventDefault();
369
+ const start = el.selectionStart ?? el.value.length;
370
+ const end = el.selectionEnd ?? start;
371
+ el.value = el.value.slice(0, start) + "\n" + el.value.slice(end);
372
+ el.selectionStart = el.selectionEnd = start + 1;
373
+ updateEditingCellValue(el.value);
374
+ el.dispatchEvent(new Event("input", { bubbles: true }));
375
+ return;
376
+ }
377
+ }
378
+ if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
281
379
  event.preventDefault();
282
- saveEditingCell();
283
- ctx.gridRootEl?.focus({ preventScroll: true });
380
+ commitToSelection();
381
+ } else if (event.key === "Enter") {
382
+ event.preventDefault();
383
+ commitAndMove(event.shiftKey ? "moveUp" : "moveDown");
284
384
  } else if (event.key === "Tab") {
285
385
  event.preventDefault();
286
386
  commitAndMoveByTab(event.shiftKey);
@@ -356,7 +456,7 @@ export function createEditing<
356
456
  rowRef: row.original,
357
457
  };
358
458
  ctx.setActiveCell(rowIndex, colIndex);
359
- ctx.setSelection(rowIndex, colIndex);
459
+ selectForEdit(rowIndex, colIndex);
360
460
  }
361
461
 
362
462
  async function pasteFromClipboard() {
@@ -366,6 +466,28 @@ export function createEditing<
366
466
  // On plain HTTP (a XAMPP/Apache LAN host) `navigator.clipboard` is
367
467
  // undefined - there the Ctrl+V keydown handler skips preventDefault and
368
468
  // lets the browser deliver a native `paste` event to `onGridPaste`.
469
+ if (ctx.props.onPasteClipboard && navigator.clipboard?.read) {
470
+ // Both types, for a handler that reads the HTML.
471
+ try {
472
+ const items = await navigator.clipboard.read();
473
+ let text = "";
474
+ let html: string | null = null;
475
+ for (const item of items) {
476
+ if (item.types.includes("text/html")) html = await (await item.getType("text/html")).text();
477
+ if (item.types.includes("text/plain")) text = await (await item.getType("text/plain")).text();
478
+ }
479
+ if (text || html) applyPastedPayload({ text, html, source: "async" });
480
+ } catch {
481
+ // No permission or nothing readable: the text-only read below may still work.
482
+ if (!navigator.clipboard.readText) return;
483
+ try {
484
+ applyPastedPayload({ text: await navigator.clipboard.readText(), html: null, source: "async" });
485
+ } catch {
486
+ return;
487
+ }
488
+ }
489
+ return;
490
+ }
369
491
  if (!navigator.clipboard?.readText) return;
370
492
  let text: string;
371
493
  try {
@@ -376,6 +498,26 @@ export function createEditing<
376
498
  applyPastedText(text);
377
499
  }
378
500
 
501
+ /**
502
+ * With `onPasteClipboard` set, Ctrl+V leaves the key to the browser so the
503
+ * `paste` event arrives with the HTML. A browser that does not deliver
504
+ * that event to the grid root (Firefox, a non-editable element) would
505
+ * paste nothing, so the async read is armed to run shortly after unless
506
+ * the event lands first and disarms it.
507
+ */
508
+ let pasteFallback: ReturnType<typeof setTimeout> | null = null;
509
+ function armPasteFallback() {
510
+ if (pasteFallback) clearTimeout(pasteFallback);
511
+ pasteFallback = setTimeout(() => {
512
+ pasteFallback = null;
513
+ void pasteFromClipboard();
514
+ }, 80);
515
+ }
516
+ function disarmPasteFallback() {
517
+ if (pasteFallback) clearTimeout(pasteFallback);
518
+ pasteFallback = null;
519
+ }
520
+
379
521
  /**
380
522
  * Native `paste` ClipboardEvent fallback (insecure-context path). A real
381
523
  * Ctrl/Cmd+V on the focused grid root fires this with `clipboardData`
@@ -390,6 +532,17 @@ export function createEditing<
390
532
  // preventDefault the editor's paste and write to the data cell instead,
391
533
  // so the edit "wouldn't update".
392
534
  if (ctx.editingCell) return;
535
+ if (ctx.props.onPasteClipboard) {
536
+ // The event is the preferred path for a handler: it carries the HTML
537
+ // as the source wrote it, needs no permission, and works over HTTP.
538
+ disarmPasteFallback();
539
+ const text = event.clipboardData?.getData("text/plain") ?? "";
540
+ const html = event.clipboardData?.getData("text/html") || null;
541
+ if (!text && !html) return;
542
+ event.preventDefault();
543
+ applyPastedPayload({ text, html, source: "event" });
544
+ return;
545
+ }
393
546
  // When the async API is available the keydown handler already pasted via
394
547
  // pasteFromClipboard(); ignore the (suppressed) native event so we don't
395
548
  // paste twice.
@@ -401,6 +554,22 @@ export function createEditing<
401
554
  }
402
555
 
403
556
  function applyPastedText(text: string) {
557
+ // One history group for the whole paste, so whatever a consumer's
558
+ // processCellFromClipboard records alongside the values (a format, via
559
+ // recordUndo) comes back with them on a single Ctrl+Z.
560
+ runHistoryGroup(ctx, () => applyPastedCells(text));
561
+ }
562
+
563
+ /** A paste with both types: the handler first, the plain text otherwise. */
564
+ function applyPastedPayload(payload: { text: string; html: string | null; source: "event" | "async" }) {
565
+ runHistoryGroup(ctx, () => {
566
+ const handled = ctx.props.onPasteClipboard?.(payload);
567
+ if (handled === true) return;
568
+ if (payload.text) applyPastedCells(payload.text);
569
+ });
570
+ }
571
+
572
+ function applyPastedCells(text: string) {
404
573
  const anchor = ctx.selectionRange.anchor ?? ctx.grid.getState().activeCell;
405
574
  if (!anchor) return;
406
575
  const focus = ctx.selectionRange.focus ?? anchor;
@@ -429,6 +598,15 @@ export function createEditing<
429
598
 
430
599
  const next = ctx.internalData.slice() as Array<TData>;
431
600
  const dataIndexOf = createDataIndexLookup(next);
601
+ // Every cell the paste changes: one grouped history entry for the lot,
602
+ // so Ctrl+Z takes the whole paste back, and one onCellValueChange each,
603
+ // so a consumer keeping its own model behind the grid (a formula engine)
604
+ // learns the values. The loop used to swap the rows in and stop, which
605
+ // left the undo stack blind to a paste and the consumer never told.
606
+ const changed: Array<{
607
+ row: TData; rowId: string; rowIndex: number; columnId: string; field: string;
608
+ before: unknown; after: unknown;
609
+ }> = [];
432
610
  for (let i = 0; i < rowSpan; i += 1) {
433
611
  const targetRowIndex = startRow + i;
434
612
  const row = ctx.allRows[targetRowIndex];
@@ -445,21 +623,67 @@ export function createEditing<
445
623
  const updated: Record<string, unknown> = {
446
624
  ...(originalRow as Record<string, unknown>),
447
625
  };
626
+ const rowChanges: typeof changed = [];
627
+ const record = (column: { id: string; columnDef: { field?: string } }, before: unknown, after: unknown) => {
628
+ if (before === after) return;
629
+ rowChanges.push({
630
+ row: updated as TData, rowId: row.id, rowIndex: dataIndex, columnId: column.id,
631
+ field: column.columnDef.field as string, before, after,
632
+ });
633
+ };
448
634
  for (let j = 0; j < colSpan; j += 1) {
449
635
  const column = ctx.allColumns[startCol + j];
450
636
  if (!column?.columnDef.field) continue;
451
637
  if (!isCellEditableAt(targetRowIndex, startCol + j)) continue;
638
+ const before = updated[column.columnDef.field];
452
639
  const editorType = (column.columnDef.editorType ??
453
640
  "text") as CellEditorType;
454
641
  const raw = fillRange ? lines[0]! : sourceCells?.[j] ?? "";
455
- updated[column.columnDef.field] = parseEditorValue(editorType, raw, {
642
+ const parsedValue = parseEditorValue(editorType, raw, {
456
643
  dateOnly: column.columnDef.cellDataType === "dateString",
457
644
  });
645
+ // The inbound half of the clipboard pair. A consumer (or a feature
646
+ // pack doing paste-special) gets the raw text AND what the grid would
647
+ // have written, and can return either. `undefined` leaves the cell
648
+ // alone, which is how "paste values only" skips a formula column.
649
+ const hook = ctx.props.processCellFromClipboard;
650
+ if (hook) {
651
+ const decided = hook({
652
+ text: raw,
653
+ parsedValue,
654
+ row: originalRow as TData,
655
+ rowIndex: targetRowIndex,
656
+ columnId: column.id as string,
657
+ });
658
+ if (decided === undefined) continue;
659
+ updated[column.columnDef.field] = decided;
660
+ record(column, before, decided);
661
+ continue;
662
+ }
663
+ updated[column.columnDef.field] = parsedValue;
664
+ record(column, before, parsedValue);
458
665
  }
666
+ if (!rowChanges.length) continue;
459
667
  next[dataIndex] = updated as TData;
668
+ changed.push(...rowChanges);
460
669
  }
670
+ if (!changed.length) return;
461
671
  ctx.internalData = next;
462
672
  ctx.grid.store.setState((prev: any) => ({ ...prev }));
673
+ pushHistory(
674
+ ctx,
675
+ changed.map((c) => ({
676
+ rowId: c.rowId, columnId: c.columnId, field: c.field, before: c.before, after: c.after,
677
+ })),
678
+ nextGroupId(),
679
+ );
680
+ if (ctx.props.onCellValueChange) {
681
+ for (const c of changed) {
682
+ ctx.props.onCellValueChange({
683
+ rowIndex: c.rowIndex, columnId: c.columnId, oldValue: c.before, newValue: c.after, row: c.row,
684
+ });
685
+ }
686
+ }
463
687
  }
464
688
 
465
689
  /** Programmatically begin editing a cell (mirrors a double-click). */
@@ -563,14 +787,15 @@ export function createEditing<
563
787
  }
564
788
  // Record the whole-row change as consecutive history steps.
565
789
  if (changed.length) {
566
- let hist = ctx.history.slice(0, ctx.historyPtr + 1);
567
- for (const c of changed) {
568
- hist.push({ rowId: fr.rowId, columnId: c.columnId, field: c.field, before: c.before, after: c.after });
569
- }
570
- if (hist.length > ctx.UNDO_LIMIT) hist = hist.slice(hist.length - ctx.UNDO_LIMIT);
571
- ctx.history = hist;
572
- ctx.historyPtr = ctx.history.length - 1;
573
- ctx.historyVersion += 1;
790
+ // One full-row commit is ONE undo, not one per changed column.
791
+ pushHistory(
792
+ ctx,
793
+ changed.map((c) => ({
794
+ rowId: fr.rowId, columnId: c.columnId, field: c.field,
795
+ before: c.before, after: c.after,
796
+ })),
797
+ nextGroupId(),
798
+ );
574
799
  }
575
800
  ctx.grid.store.setState((prev: any) => ({ ...prev }));
576
801
  if (ctx.props.onCellValueChange && changed.length) {
@@ -614,5 +839,6 @@ export function createEditing<
614
839
  onCellDoubleClick,
615
840
  pasteFromClipboard,
616
841
  onGridPaste,
842
+ armPasteFallback,
617
843
  };
618
844
  }
@@ -8,6 +8,14 @@ describe('buildFillPattern - numeric', () => {
8
8
  expect(buildFillPattern([100, 90, 80], 3)).toEqual([70, 60, 50])
9
9
  })
10
10
 
11
+ it('continues an uneven run of numbers along its linear trend, as Excel does', () => {
12
+ // 1, 2, 4: slope 1.5, intercept -2/3, so the next three are 5.33, 6.83, 8.33.
13
+ const next = buildFillPattern([1, 2, 4], 3) as number[]
14
+ expect(next.map((v) => Number(v.toFixed(4)))).toEqual([5.3333, 6.8333, 8.3333])
15
+ expect((buildFillPattern([10, 20, 40], 2) as number[]).map((v) => Number(v.toFixed(4)))).toEqual([53.3333, 68.3333])
16
+ expect(buildFillPattern([18200, 4200, 1200], 2)).toEqual([-9133.33333333333, -17633.3333333333])
17
+ })
18
+
11
19
  it('repeats a single numeric value (Excel default)', () => {
12
20
  expect(buildFillPattern([42], 4)).toEqual([42, 42, 42, 42])
13
21
  })
@@ -87,7 +95,34 @@ describe('buildFillPattern - fallback cycle', () => {
87
95
  ])
88
96
  })
89
97
 
98
+ it('repeats a single number held as text, the way a numeric one repeats', () => {
99
+ // A sheet stores what was typed, so 5 arrives as '5'. It is a number,
100
+ // not 'Item 5' with an empty prefix: one copy, no 6, 7, 8.
101
+ expect(buildFillPattern(['5'], 3)).toEqual(['5', '5', '5'])
102
+ expect(buildFillPattern(['Item 5'], 2)).toEqual(['Item 6', 'Item 7'])
103
+ })
104
+
90
105
  it('repeats a single non-numeric string', () => {
91
106
  expect(buildFillPattern(['hello'], 3)).toEqual(['hello', 'hello', 'hello'])
92
107
  })
108
+
109
+ it('steps an ISO date by a day, and by the gap two dates set', () => {
110
+ // Used to read 2024-01-15 as the number 2024 with a suffix and fill
111
+ // 2025-01-15, 2026-01-15.
112
+ expect(buildFillPattern(['2024-01-15'], 3)).toEqual(['2024-01-16', '2024-01-17', '2024-01-18'])
113
+ expect(buildFillPattern(['2024-01-01', '2024-01-08'], 2)).toEqual(['2024-01-15', '2024-01-22'])
114
+ expect(buildFillPattern(['2024-02-28'], 2)).toEqual(['2024-02-29', '2024-03-01'])
115
+ expect(buildFillPattern(['2024-03-03', '2024-03-02'], 2)).toEqual(['2024-03-01', '2024-02-29'])
116
+ })
117
+
118
+ it('steps Date objects by the day and keeps them Dates', () => {
119
+ const out = buildFillPattern([new Date(Date.UTC(2024, 0, 31))], 2)
120
+ expect(out.every((d) => d instanceof Date)).toBe(true)
121
+ expect((out as Date[]).map((d) => d.toISOString().slice(0, 10))).toEqual(['2024-02-01', '2024-02-02'])
122
+ })
123
+
124
+ it('still counts a prefixed number that happens to hold digits and dashes', () => {
125
+ expect(buildFillPattern(['Run-001'], 2)).toEqual(['Run-002', 'Run-003'])
126
+ expect(buildFillPattern(['2024-1-5'], 1)).not.toEqual(['2024-01-06'])
127
+ })
93
128
  })
@@ -164,13 +164,54 @@ function arithmeticStep(nums: ReadonlyArray<number>): number | null {
164
164
  * `sourceValues` already in the selection. See module header for the
165
165
  * pattern-detection rules.
166
166
  */
167
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/
168
+
169
+ /**
170
+ * The sources as day stamps when every one is a date: `Date` objects, or
171
+ * `YYYY-MM-DD` text (read as UTC so a fill never crosses a day boundary
172
+ * with the time zone). Anything else means "not a date series".
173
+ */
174
+ function datesOf(values: ReadonlyArray<unknown>): { stamps: number[]; iso: boolean } | null {
175
+ if (values.every((v) => v instanceof Date && Number.isFinite(v.getTime()))) {
176
+ return { stamps: values.map((v) => (v as Date).getTime()), iso: false }
177
+ }
178
+ if (values.every((v) => typeof v === 'string' && ISO_DATE.test(v))) {
179
+ const stamps = values.map((v) => Date.parse(`${v}T00:00:00Z`))
180
+ return stamps.every((t) => Number.isFinite(t)) ? { stamps, iso: true } : null
181
+ }
182
+ return null
183
+ }
184
+
167
185
  export function buildFillPattern(
168
186
  sourceValues: ReadonlyArray<unknown>,
169
187
  targetCount: number,
170
188
  ): unknown[] {
171
189
  if (sourceValues.length === 0 || targetCount <= 0) return []
172
190
 
173
- // -------- 1) Numeric arithmetic progression (2+ values) -----------
191
+ // -------- 0) Dates step by the day ---------------------------------
192
+ // Before the numeric rule, since a Date coerces to a finite number of
193
+ // milliseconds, and before the prefix rule, which read 2024-01-15 as the
194
+ // number 2024 with a suffix and filled 2025-01-15. Two dates set the step
195
+ // (a week apart fills weekly); one date steps by a day, as Excel's does.
196
+ const dates = datesOf(sourceValues)
197
+ if (dates) {
198
+ const DAY = 86_400_000
199
+ const days = dates.stamps.map((t) => Math.round(t / DAY))
200
+ const step = days.length >= 2 ? arithmeticStep(days) : 1
201
+ if (step !== null) {
202
+ const last = days[days.length - 1]!
203
+ return Array.from({ length: targetCount }, (_, i) => {
204
+ const stamp = (last + step * (i + 1)) * DAY
205
+ return dates.iso ? new Date(stamp).toISOString().slice(0, 10) : new Date(stamp)
206
+ })
207
+ }
208
+ }
209
+
210
+ // -------- 1) Numbers: the linear trend (2+ values) -----------------
211
+ // Excel's AutoFill of two or more numbers continues their least-squares
212
+ // line: 1, 2, 3 goes on 4, 5, 6, and 1, 2, 4 goes on 5.33, 6.83, ...
213
+ // rather than repeating. An even step is the same line, so the common
214
+ // case is unchanged.
174
215
  const nums = sourceValues.map((v) => Number(v))
175
216
  const allNumericLike =
176
217
  sourceValues.every((v) => v !== null && v !== '' && v !== undefined) &&
@@ -181,6 +222,17 @@ export function buildFillPattern(
181
222
  const last = nums[nums.length - 1]!
182
223
  return Array.from({ length: targetCount }, (_, i) => last + step * (i + 1))
183
224
  }
225
+ const n = nums.length
226
+ const meanX = (n - 1) / 2
227
+ const meanY = nums.reduce((a, b) => a + b, 0) / n
228
+ let sxy = 0
229
+ let sxx = 0
230
+ for (let i = 0; i < n; i += 1) { sxy += (i - meanX) * (nums[i]! - meanY); sxx += (i - meanX) ** 2 }
231
+ const slope = sxx === 0 ? 0 : sxy / sxx
232
+ const intercept = meanY - slope * meanX
233
+ // Rounded to the precision floating point gives 5.333333333333333, so
234
+ // 10, 20, 40 fills 50, 60 and not 50.00000000000001.
235
+ return Array.from({ length: targetCount }, (_, i) => Number((intercept + slope * (n + i)).toPrecision(15)))
184
236
  }
185
237
 
186
238
  // -------- 2) Known string sequence (days / months / quarters) -----
@@ -200,7 +252,9 @@ export function buildFillPattern(
200
252
  if (allStrings && sourceValues.length >= 1) {
201
253
  const stringSources = sourceValues as string[]
202
254
  const split = findPrefixNumberSuffix(stringSources)
203
- if (split) {
255
+ // A bare number as text ('5') is a number, not 'Item 5': one of them
256
+ // copies, as a numeric 5 does, and two or more were a progression above.
257
+ if (split && (split.prefix !== '' || split.suffix !== '')) {
204
258
  const step = split.numbers.length >= 2
205
259
  ? arithmeticStep(split.numbers) ?? 1
206
260
  : 1
package/src/grid-icons.ts CHANGED
@@ -97,8 +97,8 @@ export type GridIcons = Partial<Record<GridIconName, Snippet>>
97
97
 
98
98
  /**
99
99
  * Icons whose built-in form is a character rather than an SVG path. Kept as
100
- * data so `<SvGrid>`, its footer and the standalone `SvRowGroupPanel` render
101
- * the same defaults instead of three copies drifting apart.
100
+ * data so `<SvGrid>`, its footer and the row-group panel in @svgrid/enterprise
101
+ * render the same defaults instead of three copies drifting apart.
102
102
  *
103
103
  * Each is the exact character that shipped inline before, so a grid that sets
104
104
  * no `icons` renders byte-for-byte what it always did.
@@ -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,6 +106,9 @@ 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
@@ -102,7 +118,7 @@ export type GridMessages = {
102
118
  }
103
119
 
104
120
  /** English defaults - the literal strings the grid shipped before localization. */
105
- export const defaultGridMessages: GridMessages = {
121
+ export const defaultGridMessages: GridChromeMessages = {
106
122
  noRows: 'No rows to display.',
107
123
  loading: 'Loading grid data...',
108
124
  columns: 'Columns',
@@ -172,6 +188,8 @@ export const defaultGridMessages: GridMessages = {
172
188
  opBefore: 'Before',
173
189
  opAfter: 'After',
174
190
  chartRange: 'Chart selected range',
191
+ rowLoadFailed: 'Could not load these rows.',
192
+ rowLoadRetry: 'Retry',
175
193
  pivotUpsellTitle: 'Pivot mode',
176
194
  pivotUpsellBody: 'Pivot mode is an Enterprise feature. Install @svgrid/enterprise and call enablePivot() to use it.',
177
195
  upsellLicense:
@@ -184,6 +202,6 @@ export const defaultGridMessages: GridMessages = {
184
202
  * values fall back to the default (via `resolveMessages`), so a partial map only
185
203
  * replaces the keys it sets.
186
204
  */
187
- export function resolveGridMessages(overrides?: Partial<GridMessages> | null): GridMessages {
205
+ export function resolveGridMessages(overrides?: Partial<GridMessages> | null): GridChromeMessages {
188
206
  return resolveMessages(defaultGridMessages, overrides)
189
207
  }