@svgrid/grid 2.6.20 → 2.6.22

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 (110) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +22 -0
  3. package/dist/GridMenus.svelte +17 -12
  4. package/dist/SvGrid.controller.svelte.d.ts +14 -8
  5. package/dist/SvGrid.controller.svelte.js +214 -74
  6. package/dist/SvGrid.css +1 -1
  7. package/dist/SvGrid.svelte +112 -57
  8. package/dist/SvGrid.types.d.ts +191 -1
  9. package/dist/ai.d.ts +28 -0
  10. package/dist/ai.js +6 -0
  11. package/dist/cdn/{GridMenus-B0F9iBrG.js → GridMenus-BuoBPqxx.js} +137 -132
  12. package/dist/cdn/GridMenus-n4llxoOI.js +494 -0
  13. package/dist/cdn/column-resize-DsfNXMom.js +102 -0
  14. package/dist/cdn/row-resize-BRcimkUT.js +95 -0
  15. package/dist/cdn/{src-Cd0tearp.js → src-C9Hihx1W.js} +3497 -3487
  16. package/dist/cdn/{src-B1TdiyS8.js → src-D1lXwq1l.js} +8258 -8248
  17. package/dist/cdn/svgrid.js +10 -8
  18. package/dist/cdn/svgrid.svelte-external.js +10 -8
  19. package/dist/cdn/validate-_CDJzgIo.js +75 -0
  20. package/dist/cell-formatting.d.ts +2 -0
  21. package/dist/cell-formatting.js +2 -0
  22. package/dist/chart-export.d.ts +1 -0
  23. package/dist/chart.d.ts +31 -5
  24. package/dist/chart.js +9 -3
  25. package/dist/column-groups.js +1 -1
  26. package/dist/column-resize.d.ts +46 -0
  27. package/dist/column-resize.js +205 -0
  28. package/dist/columns.d.ts +0 -3
  29. package/dist/columns.js +0 -57
  30. package/dist/core.d.ts +216 -4
  31. package/dist/core.js +532 -119
  32. package/dist/createTree.svelte.d.ts +3 -0
  33. package/dist/createTree.svelte.js +1 -0
  34. package/dist/datetime/date-core.d.ts +2 -0
  35. package/dist/datetime/date-restrict.d.ts +1 -0
  36. package/dist/datetime/timezone.d.ts +1 -0
  37. package/dist/dock-manager-model.d.ts +3 -0
  38. package/dist/dock-manager-model.js +1 -0
  39. package/dist/dock-model.d.ts +6 -0
  40. package/dist/dock-model.js +3 -0
  41. package/dist/editor-contract.d.ts +1 -0
  42. package/dist/filtering/excel-filters.js +28 -0
  43. package/dist/group-display.d.ts +1 -1
  44. package/dist/index.d.ts +2 -1
  45. package/dist/index.js +6 -0
  46. package/dist/list-option.d.ts +1 -0
  47. package/dist/menus.js +1 -1
  48. package/dist/positioning.d.ts +2 -0
  49. package/dist/row-resize.d.ts +11 -0
  50. package/dist/row-resize.js +7 -1
  51. package/dist/scheduler-ical.d.ts +1 -0
  52. package/dist/scheduler-model.d.ts +1 -0
  53. package/dist/selection.js +9 -0
  54. package/dist/spreadsheet.d.ts +1 -1
  55. package/dist/spreadsheet.js +1 -1
  56. package/dist/summaries.js +22 -0
  57. package/dist/svgrid-wrapper.types.d.ts +5 -0
  58. package/dist/toast-store.svelte.d.ts +4 -0
  59. package/dist/validate.d.ts +50 -0
  60. package/dist/validate.js +187 -0
  61. package/package.json +4 -1
  62. package/src/GridMenus.svelte +17 -12
  63. package/src/SvGrid.controller.svelte.ts +223 -76
  64. package/src/SvGrid.css +1 -1
  65. package/src/SvGrid.svelte +112 -57
  66. package/src/SvGrid.types.ts +191 -1
  67. package/src/ai.ts +28 -0
  68. package/src/cell-formatting.ts +2 -0
  69. package/src/chart-export.ts +1 -0
  70. package/src/chart.ts +31 -5
  71. package/src/column-groups.ts +1 -1
  72. package/src/column-resize.test.ts +381 -0
  73. package/src/column-resize.ts +227 -0
  74. package/src/columns.test.ts +0 -103
  75. package/src/columns.ts +0 -58
  76. package/src/core.aggregate.test.ts +134 -0
  77. package/src/core.filter.test.ts +156 -0
  78. package/src/core.grouping.test.ts +146 -0
  79. package/src/core.row-shape.test.ts +119 -0
  80. package/src/core.rowmodel-cache.test.ts +121 -0
  81. package/src/core.sort.test.ts +293 -0
  82. package/src/core.ts +723 -119
  83. package/src/createTree.svelte.ts +3 -0
  84. package/src/datetime/date-core.ts +2 -0
  85. package/src/datetime/date-restrict.ts +1 -0
  86. package/src/datetime/timezone.ts +1 -0
  87. package/src/dock-manager-model.ts +3 -0
  88. package/src/dock-model.ts +6 -0
  89. package/src/editor-contract.ts +1 -0
  90. package/src/filtering/excel-filters.ts +30 -0
  91. package/src/filtering/normalize-fast-path.test.ts +104 -0
  92. package/src/group-display.ts +1 -1
  93. package/src/index.ts +12 -1
  94. package/src/list-option.ts +1 -0
  95. package/src/menus.ts +1 -1
  96. package/src/positioning.ts +2 -0
  97. package/src/resize-props.test.ts +361 -0
  98. package/src/row-resize.test.ts +31 -0
  99. package/src/row-resize.ts +21 -3
  100. package/src/scheduler-ical.ts +1 -0
  101. package/src/scheduler-model.ts +1 -0
  102. package/src/selection.ts +9 -0
  103. package/src/spreadsheet.ts +1 -1
  104. package/src/summaries.ts +21 -0
  105. package/src/svgrid-wrapper.types.ts +5 -0
  106. package/src/svgrid.summaries.test.ts +217 -0
  107. package/src/toast-store.svelte.ts +4 -0
  108. package/src/validate.test.ts +207 -0
  109. package/src/validate.ts +269 -0
  110. package/dist/cdn/GridMenus-IHK_l7m6.js +0 -489
@@ -0,0 +1,361 @@
1
+ /**
2
+ * The `columnResize` / `rowResize` props, against a REAL mounted `<SvGrid>`.
3
+ *
4
+ * Both default to OFF and both are opt-in, so they are tested together and the
5
+ * last block asserts they behave identically.
6
+ *
7
+ * Two things make "the prop is on" and "the affordance exists" separate claims
8
+ * here, and both are why these tests double-`settle()`:
9
+ *
10
+ * - Neither affordance is rendered by the component. Each is injected by an
11
+ * action through a MutationObserver.
12
+ * - Neither action is imported statically. Both arrive via `import()`, so the
13
+ * handles land a microtask after the first paint.
14
+ *
15
+ * `columnResize` off-by-default is a deliberate behaviour CHANGE - column
16
+ * resizing used to be unconditional - so the assertion pinning that is load
17
+ * bearing, not a restatement of the obvious.
18
+ *
19
+ * jsdom does no layout, so the drag arithmetic is only sanity-checked here; it
20
+ * is covered properly against synthetic DOM in row-resize.test.ts and
21
+ * column-resize.test.ts. What this file checks is the wiring.
22
+ */
23
+ import { describe, expect, it, beforeEach } from 'vitest'
24
+ import { mount, unmount } from 'svelte'
25
+ import SvGrid from './SvGrid.svelte'
26
+ import { tableFeatures, createCoreRowModel } from './index'
27
+ import type { ColumnDef } from './index'
28
+
29
+ type Row = { id: string; name: string; team: string }
30
+
31
+ const features = tableFeatures({})
32
+
33
+ const rows: Row[] = [
34
+ { id: 'r1', name: 'Ada Lovelace', team: 'Platform' },
35
+ { id: 'r2', name: 'Grace Hopper', team: 'Compilers' },
36
+ { id: 'r3', name: 'Barbara Liskov', team: 'Platform' },
37
+ ]
38
+
39
+ const cols: ColumnDef<typeof features, Row>[] = [
40
+ { field: 'name', header: 'Name', width: 200 },
41
+ { field: 'team', header: 'Team', width: 140 },
42
+ ]
43
+
44
+ const mounted: Array<() => void> = []
45
+
46
+ function mountGrid(extraProps: Record<string, unknown> = {}) {
47
+ const target = document.createElement('div')
48
+ document.body.appendChild(target)
49
+ const app = mount(SvGrid, {
50
+ target,
51
+ props: {
52
+ data: rows,
53
+ columns: cols,
54
+ features,
55
+ _rowModels: { coreRowModel: createCoreRowModel() },
56
+ containerHeight: 300,
57
+ // Virtualization off so every row is in the DOM to be counted.
58
+ virtualization: false,
59
+ columnVirtualization: false,
60
+ ...extraProps,
61
+ } as never,
62
+ })
63
+ mounted.push(() => {
64
+ unmount(app)
65
+ target.remove()
66
+ })
67
+ return target
68
+ }
69
+
70
+ /** Let the row model + effects settle before reading the DOM. */
71
+ const settle = () => new Promise((r) => setTimeout(r, 0))
72
+
73
+ beforeEach(() => {
74
+ while (mounted.length) mounted.pop()!()
75
+ document.body.innerHTML = ''
76
+ if (!HTMLElement.prototype.setPointerCapture) {
77
+ HTMLElement.prototype.setPointerCapture = function () {}
78
+ }
79
+ if (!HTMLElement.prototype.releasePointerCapture) {
80
+ HTMLElement.prototype.releasePointerCapture = function () {}
81
+ }
82
+ })
83
+
84
+ const handles = (t: HTMLElement) => t.querySelectorAll('.sv-grid-resize-handle').length
85
+ const strips = (t: HTMLElement) => t.querySelectorAll('.sv-grid-row-resize-handle').length
86
+
87
+ describe('columnResize prop', () => {
88
+ it('injects no handles when the prop is absent', async () => {
89
+ const target = mountGrid()
90
+ await settle()
91
+ await settle()
92
+ // Off by default. Column resizing used to be unconditional, so this is the
93
+ // assertion that pins the deliberate behaviour change.
94
+ expect(handles(target)).toBe(0)
95
+ })
96
+
97
+ it('injects a handle per column when true', async () => {
98
+ const target = mountGrid({ columnResize: true })
99
+ await settle()
100
+ // The action is fetched through import(), so the handles land a tick later
101
+ // than the first paint - a second settle, not a rendering bug.
102
+ await settle()
103
+ expect(handles(target)).toBe(cols.length)
104
+ })
105
+
106
+ it('injects no handles when explicitly false', async () => {
107
+ const target = mountGrid({ columnResize: false })
108
+ await settle()
109
+ await settle()
110
+ expect(handles(target)).toBe(0)
111
+ })
112
+
113
+ it('leaves the headers themselves intact when off', async () => {
114
+ // Only the drag affordance is absent - the column still renders, still
115
+ // sorts, still has its accessible name.
116
+ const target = mountGrid({ columnResize: false })
117
+ await settle()
118
+ expect(target.querySelectorAll('.sv-grid-column').length).toBeGreaterThanOrEqual(cols.length)
119
+ expect(target.textContent).toContain('Name')
120
+ expect(target.textContent).toContain('Team')
121
+ })
122
+
123
+ it('applies a dragged width to the column', async () => {
124
+ const target = mountGrid({ columnResize: true })
125
+ await settle()
126
+ await settle()
127
+
128
+ const handle = target.querySelector<HTMLElement>('.sv-grid-resize-handle')!
129
+ const th = handle.closest<HTMLElement>('th.sv-grid-column')!
130
+ const opts = { bubbles: true, cancelable: true, pointerId: 1 }
131
+ handle.dispatchEvent(new PointerEvent('pointerdown', { ...opts, clientX: 0 } as PointerEventInit))
132
+ window.dispatchEvent(new PointerEvent('pointerup', { ...opts, clientX: 60 } as PointerEventInit))
133
+ await settle()
134
+
135
+ // Name starts at 200 (see `cols`), so a +60 drag lands at 260.
136
+ expect(th.style.width).toContain('260')
137
+ })
138
+ })
139
+
140
+ describe('rowResize prop', () => {
141
+ it('injects no strips when the prop is absent', async () => {
142
+ const target = mountGrid()
143
+ await settle()
144
+ expect(strips(target)).toBe(0)
145
+ })
146
+
147
+ it('injects a strip per body row when on', async () => {
148
+ const target = mountGrid({ rowResize: true })
149
+ await settle()
150
+ // The MutationObserver decorates asynchronously; give it a turn.
151
+ await settle()
152
+ expect(strips(target)).toBe(rows.length)
153
+ })
154
+
155
+ it('works without a gutter column, via the row anchor', async () => {
156
+ // No showRowNumbers, no `sv-row-gutter` cellClass. Before the anchor
157
+ // fallback this rendered zero strips and the prop did nothing at all.
158
+ const target = mountGrid({ rowResize: true })
159
+ await settle()
160
+ await settle()
161
+ const first = target.querySelector<HTMLElement>('.sv-grid-row-resize-handle')
162
+ expect(first).not.toBeNull()
163
+ expect(first!.closest('td')).not.toBeNull()
164
+ })
165
+
166
+ it('gives each strip a keyboard-reachable separator role', async () => {
167
+ const target = mountGrid({ rowResize: true })
168
+ await settle()
169
+ await settle()
170
+ const strip = target.querySelector<HTMLElement>('.sv-grid-row-resize-handle')!
171
+ expect(strip.getAttribute('role')).toBe('separator')
172
+ expect(strip.getAttribute('aria-orientation')).toBe('horizontal')
173
+ expect(strip.tabIndex).toBe(0)
174
+ })
175
+
176
+ it('is suppressed by autoRowHeight, which owns the height itself', async () => {
177
+ // Both want to set the row height. Letting them both run means the next
178
+ // measurement pass silently discards whatever the user dragged.
179
+ const target = mountGrid({ rowResize: true, autoRowHeight: true })
180
+ await settle()
181
+ await settle()
182
+ expect(strips(target)).toBe(0)
183
+ })
184
+
185
+ it('applies a dragged height to the row', async () => {
186
+ const target = mountGrid({ rowResize: true, rowHeight: 30 })
187
+ await settle()
188
+ await settle()
189
+
190
+ const strip = target.querySelector<HTMLElement>('.sv-grid-row-resize-handle')!
191
+ const tr = strip.closest<HTMLTableRowElement>('tr.sv-grid-row')!
192
+ // jsdom reports a zero rect, so the drag needs a real starting height.
193
+ tr.getBoundingClientRect = () =>
194
+ ({ height: 30, width: 300, top: 0, left: 0, right: 300, bottom: 30, x: 0, y: 0, toJSON() {} }) as DOMRect
195
+
196
+ const opts = { bubbles: true, cancelable: true, pointerId: 1 }
197
+ strip.dispatchEvent(new PointerEvent('pointerdown', { ...opts, clientY: 0 } as PointerEventInit))
198
+ window.dispatchEvent(new PointerEvent('pointerup', { ...opts, clientY: 44 } as PointerEventInit))
199
+ await settle()
200
+
201
+ expect(tr.style.height).toBe('74px')
202
+ })
203
+ })
204
+
205
+ /**
206
+ * The two props are meant to be interchangeable in every way a consumer can
207
+ * touch them: same import, same shape, same opt-in, same runtime override.
208
+ * Asserted rather than assumed, because they were built at different times and
209
+ * `rowResize` reads through the controller while `columnResize` reads in the
210
+ * view - two paths that could easily drift apart.
211
+ */
212
+ describe('rowResize / columnResize symmetry', () => {
213
+ it('are exported from the package the same way', async () => {
214
+ const pkg = (await import('./index')) as Record<string, unknown>
215
+ expect(typeof pkg.rowResize).toBe('function')
216
+ expect(typeof pkg.columnResize).toBe('function')
217
+ })
218
+
219
+ it('both default to off', async () => {
220
+ const target = mountGrid()
221
+ await settle()
222
+ await settle()
223
+ expect(handles(target)).toBe(0)
224
+ expect(strips(target)).toBe(0)
225
+ })
226
+
227
+ it('both turn on with an explicit true', async () => {
228
+ const target = mountGrid({ columnResize: true, rowResize: true })
229
+ await settle()
230
+ await settle()
231
+ expect(handles(target)).toBe(cols.length)
232
+ expect(strips(target)).toBe(rows.length)
233
+ })
234
+
235
+ it('both turn on at runtime through api.setOption', async () => {
236
+ // `columnResize` is read in the view and `rowResize` in the controller, but
237
+ // both go through the same override proxy - so both must react here.
238
+ let api: { setOption: (k: string, v: unknown) => void } | null = null
239
+ const target = mountGrid({ onApiReady: (next: unknown) => (api = next as never) })
240
+ await settle()
241
+ expect(handles(target)).toBe(0)
242
+ expect(strips(target)).toBe(0)
243
+
244
+ api!.setOption('columnResize', true)
245
+ api!.setOption('rowResize', true)
246
+ await settle()
247
+ await settle()
248
+ expect(handles(target)).toBe(cols.length)
249
+ expect(strips(target)).toBe(rows.length)
250
+ })
251
+
252
+ it('both turn back off through api.setOption', async () => {
253
+ let api: { setOption: (k: string, v: unknown) => void } | null = null
254
+ const target = mountGrid({
255
+ columnResize: true,
256
+ rowResize: true,
257
+ onApiReady: (next: unknown) => (api = next as never),
258
+ })
259
+ await settle()
260
+ await settle()
261
+ expect(handles(target)).toBe(cols.length)
262
+ expect(strips(target)).toBe(rows.length)
263
+
264
+ api!.setOption('columnResize', false)
265
+ api!.setOption('rowResize', false)
266
+ await settle()
267
+ await settle()
268
+ expect(handles(target)).toBe(0)
269
+ expect(strips(target)).toBe(0)
270
+ })
271
+ })
272
+
273
+ describe('ColumnDef.resizable', () => {
274
+ const mixed = [
275
+ { field: 'name', header: 'Name', width: 200 },
276
+ { field: 'team', header: 'Team', width: 140, resizable: false },
277
+ ] as ColumnDef<typeof features, Row>[]
278
+
279
+ it('drops the handle for the column that opted out', async () => {
280
+ const target = mountGrid({ columnResize: true, columns: mixed })
281
+ await settle()
282
+ await settle()
283
+ expect(handles(target)).toBe(1)
284
+ const th = target
285
+ .querySelector<HTMLElement>('.sv-grid-resize-handle')!
286
+ .closest<HTMLElement>('th')!
287
+ expect(th.dataset.svgridHeaderCol).toBe('name')
288
+ })
289
+
290
+ it('narrows columnResize rather than enabling anything', async () => {
291
+ // With the grid-wide prop off, `resizable: true` must not bring a handle
292
+ // back - the column option only ever subtracts.
293
+ const target = mountGrid({
294
+ columns: [
295
+ { field: 'name', header: 'Name', width: 200, resizable: true },
296
+ { field: 'team', header: 'Team', width: 140 },
297
+ ] as ColumnDef<typeof features, Row>[],
298
+ })
299
+ await settle()
300
+ await settle()
301
+ expect(handles(target)).toBe(0)
302
+ })
303
+
304
+ it('leaves every column resizable when none opts out', async () => {
305
+ const target = mountGrid({ columnResize: true })
306
+ await settle()
307
+ await settle()
308
+ expect(handles(target)).toBe(cols.length)
309
+ })
310
+ })
311
+
312
+ describe('rowResize brings in the row header column', () => {
313
+ const gutters = (t: HTMLElement) => t.querySelectorAll('.sv-grid-row-number-cell').length
314
+
315
+ it('shows the row-number gutter when rowResize is on', async () => {
316
+ // The strip lives in the row header, which is where a spreadsheet puts it.
317
+ // Without a gutter there is nothing that reads as a grab target.
318
+ const target = mountGrid({ rowResize: true })
319
+ await settle()
320
+ await settle()
321
+ expect(gutters(target)).toBe(rows.length)
322
+ const strip = target.querySelector<HTMLElement>('.sv-grid-row-resize-handle')!
323
+ expect(strip.closest('.sv-grid-row-number-cell')).not.toBeNull()
324
+ })
325
+
326
+ it('shows no gutter when rowResize is off', async () => {
327
+ const target = mountGrid()
328
+ await settle()
329
+ await settle()
330
+ expect(gutters(target)).toBe(0)
331
+ })
332
+
333
+ it('lets an explicit showRowNumbers={false} win', async () => {
334
+ // Turning the gutter on is a default, not a takeover. The strip falls back
335
+ // to the row's first cell so the prop still does something.
336
+ const target = mountGrid({ rowResize: true, showRowNumbers: false })
337
+ await settle()
338
+ await settle()
339
+ expect(gutters(target)).toBe(0)
340
+ expect(strips(target)).toBe(rows.length)
341
+ const strip = target.querySelector<HTMLElement>('.sv-grid-row-resize-handle')!
342
+ expect(strip.closest('td')).not.toBeNull()
343
+ })
344
+
345
+ it('does not disturb a grid that already asked for the gutter', async () => {
346
+ const target = mountGrid({ rowResize: true, showRowNumbers: true })
347
+ await settle()
348
+ await settle()
349
+ expect(gutters(target)).toBe(rows.length)
350
+ })
351
+
352
+ it('brings no gutter when autoRowHeight has suppressed rowResize', async () => {
353
+ // rowResizeOn is false there, so the gutter would be a column with no
354
+ // purpose - the gate has to follow the resolved state, not the raw prop.
355
+ const target = mountGrid({ rowResize: true, autoRowHeight: true })
356
+ await settle()
357
+ await settle()
358
+ expect(gutters(target)).toBe(0)
359
+ expect(strips(target)).toBe(0)
360
+ })
361
+ })
@@ -130,6 +130,37 @@ describe('rowResize - strip injection', () => {
130
130
  action.destroy()
131
131
  })
132
132
 
133
+ it("anchor 'row' falls back to the first body cell when there is no gutter", () => {
134
+ // What `<SvGrid rowResize>` relies on: most grids have no row-number
135
+ // column, and without the fallback the prop would be inert on them.
136
+ const { host, rows } = buildGrid([{ gutter: false }, { gutter: false }])
137
+ track(host)
138
+ const action = rowResize(host, { onResize: vi.fn(), anchor: 'row' })
139
+ expect(stripOf(rows[0]!)).not.toBeNull()
140
+ expect(stripOf(rows[1]!)).not.toBeNull()
141
+ // The strip hangs off the row's first cell, since that is all there is.
142
+ expect(stripOf(rows[0]!)!.parentElement).toBe(rows[0]!.querySelector('.sv-grid-cell'))
143
+ action.destroy()
144
+ })
145
+
146
+ it("anchor 'row' still prefers a real gutter when the row has one", () => {
147
+ const { host, rows } = buildGrid([{ gutter: true }])
148
+ track(host)
149
+ const action = rowResize(host, { onResize: vi.fn(), anchor: 'row' })
150
+ expect(stripOf(rows[0]!)!.parentElement).toBe(gutterOf(rows[0]!))
151
+ action.destroy()
152
+ })
153
+
154
+ it("defaults to anchor 'gutter', leaving gutterless rows alone", () => {
155
+ // The exported action's existing contract. Adding the fallback must not
156
+ // start decorating rows for consumers who never asked for it.
157
+ const { host, rows } = buildGrid([{ gutter: false }])
158
+ track(host)
159
+ const action = rowResize(host, { onResize: vi.fn() })
160
+ expect(stripOf(rows[0]!)).toBeNull()
161
+ action.destroy()
162
+ })
163
+
133
164
  it('also injects a strip into the built-in row-number gutter (showRowNumbers)', () => {
134
165
  // The native gutter cell is `.sv-grid-cell.sv-grid-row-number-cell`, NOT
135
166
  // `.sv-row-gutter`; the action must recognise it so `showRowNumbers` grids
package/src/row-resize.ts CHANGED
@@ -39,6 +39,17 @@ export type RowResizeOptions = {
39
39
  max?: number
40
40
  /** When true, the action removes its strips and ignores events. */
41
41
  disabled?: boolean
42
+ /**
43
+ * Where the drag strip is anchored.
44
+ * - `'gutter'` (default) - only rows that have a row-header gutter cell get
45
+ * a strip: the built-in row-number column (`showRowNumbers`) or a column
46
+ * tagged `cellClass: 'sv-row-gutter'`. Rows without one are skipped, so on
47
+ * a plain grid the action does nothing.
48
+ * - `'row'` - use the gutter when there is one, otherwise fall back to the
49
+ * row's first body cell. This is what `<SvGrid rowResize>` passes, so the
50
+ * prop works without also asking for a gutter column.
51
+ */
52
+ anchor?: 'gutter' | 'row'
42
53
  }
43
54
 
44
55
  type ActiveDrag = {
@@ -150,9 +161,16 @@ export function rowResize(node: HTMLElement, opts: RowResizeOptions) {
150
161
  // A row-header gutter cell: either a user column tagged
151
162
  // `sv-row-gutter`, or the grid's built-in row-number gutter
152
163
  // (`showRowNumbers`), whose cell is `.sv-grid-row-number-cell`.
153
- const gutter = tr.querySelector<HTMLTableCellElement>(
154
- '.sv-grid-cell.sv-row-gutter, .sv-grid-cell.sv-grid-row-number-cell',
155
- )
164
+ const gutter =
165
+ tr.querySelector<HTMLTableCellElement>(
166
+ '.sv-grid-cell.sv-row-gutter, .sv-grid-cell.sv-grid-row-number-cell',
167
+ ) ??
168
+ // `anchor: 'row'` - no gutter on this grid, so hang the strip off the
169
+ // first body cell instead. Without this a grid with no row-number
170
+ // column would silently not resize.
171
+ (current.anchor === 'row'
172
+ ? tr.querySelector<HTMLTableCellElement>('.sv-grid-cell')
173
+ : null)
156
174
  if (!gutter) continue
157
175
  if (gutter.querySelector(`:scope > .${STRIP_CLASS}`)) continue
158
176
  // Anchor the strip to the gutter cell + allow it to overflow
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import type { RecurrenceRule } from './recurrence'
9
9
 
10
+ /** One VEVENT: the fields SvGrid reads from and writes to an iCalendar feed. */
10
11
  export type ICalEvent = {
11
12
  uid?: string
12
13
  title: string
@@ -444,6 +444,7 @@ export type DayLayout<TData = unknown> = {
444
444
  overflows: OverflowMarker<TData>[]
445
445
  }
446
446
 
447
+ /** Tuning for event layout: how overlapping events share horizontal space. */
447
448
  export type LayoutOptions = {
448
449
  dayStartHour?: number
449
450
  dayEndHour?: number
package/src/selection.ts CHANGED
@@ -243,6 +243,15 @@ export function createSelection<
243
243
  * outline wins on overlap). Returns null when the cell is in no range.
244
244
  */
245
245
  function getCellRangeEdges(rowIndex: number, colIndex: number) {
246
+ // Bail before `getSelectionRects()`, which allocates an array and resolves
247
+ // every range. This runs once per rendered CELL on every render - about 250
248
+ // times per frame on a default grid - and with nothing selected it did all
249
+ // that work to return null every time. An anchored active range or a
250
+ // committed one is the only way any rect can exist.
251
+ const active = ctx.selectionRange as SelectionRange | undefined;
252
+ const committed = ctx.selectionRanges as SelectionRange[] | undefined;
253
+ if (!committed?.length && !(active?.anchor && active?.focus)) return null;
254
+
246
255
  for (const rect of getSelectionRects()) {
247
256
  if (
248
257
  rowIndex < rect.minRow ||
@@ -57,7 +57,7 @@ export type SpanColumn<TData = Record<string, unknown>> = {
57
57
  /**
58
58
  * Turn declarative per-column `colSpan` / `rowSpan` callbacks into a
59
59
  * `MergeSpec[]` you can hand to `spreadsheetLayout` - so value-driven,
60
- * AG-Grid-style spanning runs on the SAME real colspan/rowspan merge engine
60
+ * Declarative spanning runs on the SAME real colspan/rowspan merge engine
61
61
  * instead of a second code path. Recompute after sort/filter (indexes are
62
62
  * display-row indexes). A common pattern is "merge runs of equal values":
63
63
  *
package/src/summaries.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // reading/writing controller state via the `ctx` handle; the reactive core
3
3
  // ($state/$derived/$effect) stays in the controller.
4
4
  import {
5
+ applyGroupAggregate,
5
6
  type Column,
6
7
  type Row,
7
8
  type RowData,
@@ -45,6 +46,26 @@ export function createSummaries<
45
46
  const fieldFn = def.fieldFn;
46
47
  const field = def.field;
47
48
  const columnId = column.id;
49
+
50
+ // A column that declares its own `summary` opts out of the default
51
+ // sum/count below. Only that column pays for the aggregator dispatch, so
52
+ // a grid that declares none keeps the original hot loop exactly as it was.
53
+ const declared = def.summary;
54
+ if (declared !== undefined) {
55
+ if (declared === false) {
56
+ summary[columnId] = "";
57
+ continue;
58
+ }
59
+ const value = applyGroupAggregate(declared, columnId, rows);
60
+ summary[columnId] =
61
+ typeof value === "number" && Number.isFinite(value)
62
+ ? formatSummaryNumeric(column, value)
63
+ : value == null
64
+ ? ""
65
+ : String(value);
66
+ continue;
67
+ }
68
+
48
69
  let numericSum = 0;
49
70
  let numericCount = 0;
50
71
  for (let i = 0; i < rowCount; i += 1) {
@@ -9,6 +9,7 @@ import type { GridPredicateExpr } from './filtering/predicate-expr'
9
9
  // Aliased to the core union rather than restated: the API surfaces below hand
10
10
  // back whatever the grid actually filtered with, so a hand-maintained subset
11
11
  // here silently mistypes operators like 'endsWith' that reach callers at runtime.
12
+ /** The comparisons a column filter can use, as shown in the filter menu. */
12
13
  export type SvGridFilterOperator = FilterOperator
13
14
 
14
15
  /**
@@ -79,6 +80,7 @@ export type SvGridTransaction<TData> = {
79
80
  remove?: ReadonlyArray<TData | string>
80
81
  }
81
82
 
83
+ /** What a batched `applyTransaction` did: how many rows were added, updated and removed. */
82
84
  export type SvGridTransactionResult = {
83
85
  added: number
84
86
  updated: number
@@ -503,6 +505,7 @@ export type SvGridApi<
503
505
  refresh(): void
504
506
  }
505
507
 
508
+ /** The props `<SvGrid>` accepts. See the SvGrid reference for the full list with defaults. */
506
509
  export type SvGridWrapperProps<
507
510
  TFeatures extends TableFeatures,
508
511
  TData extends RowData,
@@ -553,6 +556,8 @@ export type SvGridWrapperProps<
553
556
  enableCellSelection?: boolean
554
557
  enableInlineEditing?: boolean
555
558
  enableRowSummaries?: boolean
559
+ /** Shortcut alias for `enableRowSummaries`; wins when both are set. */
560
+ summary?: boolean
556
561
  /** Receives the imperative grid API when the component is ready. */
557
562
  onApiReady?: (api: SvGridApi<TFeatures, TData>) => void
558
563
  }