@svgrid/grid 2.2.30 → 2.2.32

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 (145) hide show
  1. package/README.md +1 -1
  2. package/dist/SvAvatar.svelte +1 -1
  3. package/dist/SvCheckBox.svelte +2 -1
  4. package/dist/SvCircularProgress.svelte +1 -1
  5. package/dist/SvGrid.controller.svelte.d.ts +20 -0
  6. package/dist/SvGrid.controller.svelte.js +358 -18
  7. package/dist/SvGrid.css +103 -3
  8. package/dist/SvGrid.svelte +162 -10
  9. package/dist/SvGrid.types.d.ts +81 -0
  10. package/dist/SvGridDropdown.svelte +38 -6
  11. package/dist/SvGridDropdown.svelte.d.ts +4 -0
  12. package/dist/SvGridSelect.svelte +12 -3
  13. package/dist/SvGridSelect.svelte.d.ts +6 -0
  14. package/dist/SvGroupCell.svelte +9 -2
  15. package/dist/SvMultiSelect.svelte +12 -3
  16. package/dist/SvMultiSelect.svelte.d.ts +6 -0
  17. package/dist/SvPasswordInput.svelte +1 -1
  18. package/dist/SvProgress.svelte +1 -1
  19. package/dist/SvRating.svelte +1 -1
  20. package/dist/SvResult.svelte +1 -1
  21. package/dist/SvRichText.svelte +31 -16
  22. package/dist/SvRichText.svelte.d.ts +9 -0
  23. package/dist/SvSwitchButton.svelte +2 -1
  24. package/dist/SvToaster.svelte +1 -1
  25. package/dist/SvTree.svelte +9 -2
  26. package/dist/SvTree.svelte.d.ts +5 -0
  27. package/dist/SvTreeSelect.svelte +6 -2
  28. package/dist/build-api.js +16 -0
  29. package/dist/cdn/{GridMenus-Ds3OpzZT.js → GridMenus-C6ixSGWW.js} +33 -33
  30. package/dist/cdn/{GridMenus-Da_p7SKS.js → GridMenus-Cyk3qpJ7.js} +33 -33
  31. package/dist/cdn/{SvDateTimePicker-B8GopjhC.js → SvDateTimePicker-CdqaP61H.js} +2 -1
  32. package/dist/cdn/{SvDateTimePicker-DB0aIEk8.js → SvDateTimePicker-ClqtVIj3.js} +2 -1
  33. package/dist/cdn/{SvGridDropdown-DjALc_4f.js → SvGridDropdown-D0VdjeR8.js} +129 -123
  34. package/dist/cdn/{SvGridDropdown-hpnHvOGt.js → SvGridDropdown-Dh-2ej8j.js} +92 -87
  35. package/dist/cdn/{src-BLJfdyL0.js → src-BgHjWF0Q.js} +4193 -3837
  36. package/dist/cdn/{src-DYDCiC0D.js → src-CgD-wiuS.js} +5956 -5602
  37. package/dist/cdn/svgrid.js +8 -8
  38. package/dist/cdn/svgrid.svelte-external.js +8 -8
  39. package/dist/cell-render.d.ts +1 -0
  40. package/dist/cell-render.js +115 -1
  41. package/dist/cell-values.js +5 -0
  42. package/dist/core.d.ts +80 -9
  43. package/dist/core.js +89 -0
  44. package/dist/editor-contract.d.ts +12 -5
  45. package/dist/editor-registry.d.ts +18 -7
  46. package/dist/editor-registry.js +10 -1
  47. package/dist/group-display.d.ts +39 -0
  48. package/dist/group-display.js +52 -0
  49. package/dist/index.d.ts +2 -2
  50. package/dist/index.js +2 -2
  51. package/dist/keyboard-handlers.js +16 -0
  52. package/dist/recurrence.js +3 -2
  53. package/dist/svgrid-wrapper.types.d.ts +8 -2
  54. package/dist/themes/index.d.ts +10 -0
  55. package/dist/themes/index.js +26 -0
  56. package/package.json +1 -1
  57. package/src/SvAccordion.test.ts +5 -5
  58. package/src/SvAvatar.svelte +1 -1
  59. package/src/SvButtonGroup.test.ts +6 -6
  60. package/src/SvCheckBox.svelte +2 -1
  61. package/src/SvCircularProgress.svelte +1 -1
  62. package/src/SvFileUpload.test.ts +1 -1
  63. package/src/SvGrid.controller.svelte.ts +367 -17
  64. package/src/SvGrid.css +103 -3
  65. package/src/SvGrid.svelte +162 -10
  66. package/src/SvGrid.types.ts +81 -0
  67. package/src/SvGridDropdown.svelte +38 -6
  68. package/src/SvGridSelect.svelte +12 -3
  69. package/src/SvGroupCell.svelte +9 -2
  70. package/src/SvMultiSelect.svelte +12 -3
  71. package/src/SvPasswordInput.svelte +1 -1
  72. package/src/SvProgress.svelte +1 -1
  73. package/src/SvRating.svelte +1 -1
  74. package/src/SvResult.svelte +1 -1
  75. package/src/SvRichText.svelte +31 -16
  76. package/src/SvSwitchButton.svelte +2 -1
  77. package/src/SvToaster.svelte +1 -1
  78. package/src/SvTree.svelte +9 -2
  79. package/src/SvTreeSelect.svelte +6 -2
  80. package/src/build-api.ts +15 -0
  81. package/src/builtin-editors.test.ts +3 -1
  82. package/src/cell-render.test.ts +27 -3
  83. package/src/cell-render.ts +122 -1
  84. package/src/cell-values.ts +4 -0
  85. package/src/chart-export.test.ts +2 -2
  86. package/src/clipboard.test.ts +0 -2
  87. package/src/core.ts +157 -8
  88. package/src/editing.test.ts +1 -1
  89. package/src/editor-contract.ts +12 -5
  90. package/src/editor-registry.grid.test.ts +48 -0
  91. package/src/editor-registry.test.ts +3 -1
  92. package/src/editor-registry.ts +23 -7
  93. package/src/facet-buckets.test.ts +33 -33
  94. package/src/group-display.test.ts +91 -1
  95. package/src/group-display.ts +83 -0
  96. package/src/index.ts +9 -0
  97. package/src/keyboard-handlers.ts +17 -0
  98. package/src/list-power.test.ts +2 -2
  99. package/src/menus.test.ts +4 -4
  100. package/src/named-views.coverage.test.ts +3 -3
  101. package/src/recurrence.ts +3 -2
  102. package/src/row-resize.test.ts +40 -40
  103. package/src/scheduler-model.ts +1 -1
  104. package/src/server-data-source.coverage.test.ts +1 -1
  105. package/src/server-group-model.test.ts +6 -6
  106. package/src/spreadsheet.test.ts +0 -1
  107. package/src/svgrid-wrapper.types.ts +11 -9
  108. package/src/svgrid.async-editor-options.test.ts +271 -0
  109. package/src/svgrid.auto-row-height.test.ts +204 -0
  110. package/src/svgrid.charting.test.ts +4 -4
  111. package/src/svgrid.comments-autocomplete.test.ts +8 -2
  112. package/src/svgrid.context-menu.test.ts +25 -9
  113. package/src/svgrid.filter-depth.test.ts +163 -0
  114. package/src/svgrid.filter-menu-listbox.svelte.test.ts +16 -2
  115. package/src/svgrid.filter-menu-scroll.test.ts +10 -2
  116. package/src/svgrid.grand-total.test.ts +188 -0
  117. package/src/svgrid.group-display-mode.test.ts +171 -0
  118. package/src/svgrid.group-footers.test.ts +121 -0
  119. package/src/svgrid.group-pagination.test.ts +153 -0
  120. package/src/svgrid.tree-data.test.ts +186 -0
  121. package/src/svgriddropdown.async-panel.svelte.test.ts +195 -0
  122. package/src/themes/index.ts +42 -0
  123. package/src/tree-edit.test.ts +4 -4
  124. package/src/tree-row-model.test.ts +169 -0
  125. package/src/ui-localization.test.ts +113 -0
  126. package/themes/ag-alpine.css +22 -0
  127. package/themes/antd.css +22 -0
  128. package/themes/atlassian.css +22 -0
  129. package/themes/bootstrap.css +22 -0
  130. package/themes/carbon.css +22 -0
  131. package/themes/catppuccin.css +22 -0
  132. package/themes/dracula.css +22 -0
  133. package/themes/ember.css +22 -0
  134. package/themes/excel.css +22 -0
  135. package/themes/fluent.css +22 -0
  136. package/themes/github.css +22 -0
  137. package/themes/linear.css +22 -0
  138. package/themes/material.css +22 -0
  139. package/themes/nord.css +22 -0
  140. package/themes/notion.css +22 -0
  141. package/themes/salesforce.css +22 -0
  142. package/themes/sap.css +22 -0
  143. package/themes/shadcn.css +22 -0
  144. package/themes/tailwind.css +22 -0
  145. package/themes/vercel.css +22 -0
package/dist/core.d.ts CHANGED
@@ -48,6 +48,12 @@ export type CellSpanParams<TData extends RowData = RowData> = {
48
48
  /** The cell's base value for this column. */
49
49
  value: unknown;
50
50
  };
51
+ /** The raw option list a column's `editorOptions` can supply. */
52
+ export type EditorOptionSource = ReadonlyArray<string | number | {
53
+ value: string | number;
54
+ label?: string;
55
+ color?: string;
56
+ }>;
51
57
  /** Params passed to a column's `valueParser(...)` on edit commit. */
52
58
  export type ValueParserParams<TData extends RowData = RowData> = {
53
59
  /** The value after built-in per-`editorType` coercion. */
@@ -126,6 +132,16 @@ export type ColumnDefTemplate<TContext> = string | ((context: TContext) => unkno
126
132
  export type GroupAggregator<TData = any> = 'sum' | 'avg' | 'min' | 'max' | 'count' | 'countDistinct' | 'extent' | 'first' | ((values: number[], rows: Array<TData>) => unknown);
127
133
  /** Apply a group aggregator over a bucket's leaf rows for one column. */
128
134
  export declare function applyGroupAggregate<TData extends RowData>(agg: GroupAggregator<TData>, columnId: string, rows: ReadonlyArray<Row<TData>>): unknown;
135
+ /**
136
+ * A column definition.
137
+ *
138
+ * `TFeatures` is a phantom parameter - it is threaded through nested
139
+ * `columns` groups but no member depends on it, so `{}`, `TableFeatures` and
140
+ * `typeof features` are all interchangeable here. It is deliberately left
141
+ * WITHOUT a default: `ColumnDef<Row>` would otherwise bind `Row` to this slot
142
+ * and silently type your data as `RowData`, losing every field-name check.
143
+ * Prefer {@link GridColumns} / {@link GridColumnDef} for the common case.
144
+ */
129
145
  export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> = {
130
146
  id?: string;
131
147
  field?: keyof TData & string;
@@ -270,16 +286,17 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
270
286
  *
271
287
  * Pass a function `(row) => options` for row-dependent (cascading)
272
288
  * options - e.g. City options that depend on Country in the same row.
289
+ *
290
+ * Either form may return a **Promise**, for options that come from the
291
+ * server. While it resolves, the editor shows a loading state and the cell
292
+ * renders its raw value.
293
+ *
294
+ * Results are cached so reopening an editor does not refetch: a static source
295
+ * per column, a per-row source per row AND per that row's data - so a cascade
296
+ * reloads by itself when the cell it depends on is edited. Call
297
+ * `api.refreshEditorOptions(columnId?)` when the list changes server-side.
273
298
  */
274
- editorOptions?: ReadonlyArray<string | number | {
275
- value: string | number;
276
- label?: string;
277
- color?: string;
278
- }> | ((row: TData) => ReadonlyArray<string | number | {
279
- value: string | number;
280
- label?: string;
281
- color?: string;
282
- }>);
299
+ editorOptions?: EditorOptionSource | Promise<EditorOptionSource> | ((row: TData) => EditorOptionSource | Promise<EditorOptionSource>);
283
300
  /** When true, list/chips allow multiple selections. Cell value becomes an array. */
284
301
  editorMultiple?: boolean;
285
302
  /** Separator used when joining array values for the readonly cell display. Defaults to ', '. */
@@ -339,6 +356,21 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
339
356
  */
340
357
  cellClass?: string | ReadonlyArray<string> | ((ctx: CellContext<TData>) => string | ReadonlyArray<string> | Record<string, boolean> | undefined | null);
341
358
  };
359
+ /**
360
+ * A column definition keyed only by your row type - the ergonomic form of
361
+ * {@link ColumnDef}, whose first parameter is a phantom feature bag that is
362
+ * almost always `{}`.
363
+ *
364
+ * ```ts
365
+ * const columns: GridColumns<Person> = [{ field: 'firstName', header: 'Name' }]
366
+ * ```
367
+ *
368
+ * Interchangeable with `ColumnDef<{}, TData>` and `ColumnDef<typeof features,
369
+ * TData>` in both directions, so it mixes freely with existing code.
370
+ */
371
+ export type GridColumnDef<TData extends RowData = RowData> = ColumnDef<TableFeatures, TData>;
372
+ /** An array of {@link GridColumnDef} - what you pass to `<SvGrid columns={...}>`. */
373
+ export type GridColumns<TData extends RowData = RowData> = Array<GridColumnDef<TData>>;
342
374
  export type Column<TData extends RowData> = {
343
375
  id: string;
344
376
  columnDef: ColumnDef<any, TData>;
@@ -427,6 +459,45 @@ export declare function createCoreRowModel<TData extends RowData>(): RowModelFac
427
459
  export declare function createFilteredRowModel<TData extends RowData>(): RowModelFactory<TData>;
428
460
  export declare function createPaginatedRowModel<TData extends RowData>(): RowModelFactory<TData>;
429
461
  export declare function createGroupedRowModel<TData extends RowData>(): RowModelFactory<TData>;
462
+ export type TreeRowModelOptions = {
463
+ /** Field holding each row's parent id. Rows with no parent are roots. */
464
+ parentField: string;
465
+ /** Field holding the row's own id. Defaults to `'id'`. */
466
+ idField?: string;
467
+ };
468
+ /**
469
+ * Client-side tree data: nest the grid's own flat rows into a parent/child
470
+ * hierarchy that `createExpandedRowModel` then walks.
471
+ *
472
+ * This works on the rows the grid already built rather than on raw data, so
473
+ * tree rows keep their cells, editing, selection and formatting - they are real
474
+ * data rows that happen to have children, not synthetic banners like grouping's.
475
+ * That is also why the model is parent-id based: nested source arrays never
476
+ * become rows (the grid only builds rows for `data`), so nested input is
477
+ * flattened first with {@link flattenTreeData}. One code path, no duplicated
478
+ * row construction.
479
+ *
480
+ * Rows are tagged `__treeRow` so `isGroupRow` does not mistake an expandable
481
+ * data row for a full-width group banner.
482
+ */
483
+ export declare function createTreeRowModel<TData extends RowData>(options: TreeRowModelOptions): RowModelFactory<TData>;
484
+ export type FlattenTreeOptions = {
485
+ /** Field holding an array of child objects. */
486
+ childrenField: string;
487
+ /** Field holding each object's id. Defaults to `'id'`. */
488
+ idField?: string;
489
+ /** Field to WRITE the resolved parent id onto. Defaults to `'__parentId'`. */
490
+ parentField?: string;
491
+ };
492
+ /**
493
+ * Flatten nested tree data into the flat parent-id shape `createTreeRowModel`
494
+ * consumes, stamping each child with its parent's id.
495
+ *
496
+ * Children are emitted directly after their parent so the natural order already
497
+ * matches the rendered tree. The `childrenField` array is left on the objects
498
+ * (harmless, and callers often still want it); only the parent link is added.
499
+ */
500
+ export declare function flattenTreeData<T extends RowData>(data: ReadonlyArray<T>, options: FlattenTreeOptions): T[];
430
501
  export declare function createExpandedRowModel<TData extends RowData>(): RowModelFactory<TData>;
431
502
  export declare function createSortedRowModel<TData extends RowData>(localSortFns?: typeof sortFns): RowModelFactory<TData>;
432
503
  export type SvGridOptions<TFeatures extends TableFeatures, TData extends RowData> = {
package/dist/core.js CHANGED
@@ -193,6 +193,95 @@ export function createGroupedRowModel() {
193
193
  return buildGroups(rows, 0, 0, 'group');
194
194
  };
195
195
  }
196
+ /**
197
+ * Client-side tree data: nest the grid's own flat rows into a parent/child
198
+ * hierarchy that `createExpandedRowModel` then walks.
199
+ *
200
+ * This works on the rows the grid already built rather than on raw data, so
201
+ * tree rows keep their cells, editing, selection and formatting - they are real
202
+ * data rows that happen to have children, not synthetic banners like grouping's.
203
+ * That is also why the model is parent-id based: nested source arrays never
204
+ * become rows (the grid only builds rows for `data`), so nested input is
205
+ * flattened first with {@link flattenTreeData}. One code path, no duplicated
206
+ * row construction.
207
+ *
208
+ * Rows are tagged `__treeRow` so `isGroupRow` does not mistake an expandable
209
+ * data row for a full-width group banner.
210
+ */
211
+ export function createTreeRowModel(options) {
212
+ const { parentField, idField = 'id' } = options;
213
+ return ({ table, rows }) => {
214
+ if (!rows.length)
215
+ return rows;
216
+ const keyOf = (row) => row.original?.[idField];
217
+ const parentOf = (row) => row.original?.[parentField];
218
+ const present = new Set();
219
+ for (const row of rows)
220
+ present.add(keyOf(row));
221
+ const childrenByParent = new Map();
222
+ const roots = [];
223
+ for (const row of rows) {
224
+ const parent = parentOf(row);
225
+ // A row whose parent is absent (filtered out, or never existed) becomes a
226
+ // root rather than disappearing - silently dropping rows is worse than a
227
+ // shallower tree. Self-parenting is treated the same way.
228
+ if (parent == null || parent === keyOf(row) || !present.has(parent)) {
229
+ roots.push(row);
230
+ continue;
231
+ }
232
+ const list = childrenByParent.get(parent) ?? [];
233
+ list.push(row);
234
+ childrenByParent.set(parent, list);
235
+ }
236
+ // Guards a cycle in the parent chain from recursing forever.
237
+ const seen = new Set();
238
+ const build = (row, depth) => {
239
+ const key = keyOf(row);
240
+ const id = row.id;
241
+ if (seen.has(key)) {
242
+ return { ...row, depth, subRows: [], getCanExpand: () => false };
243
+ }
244
+ seen.add(key);
245
+ const subRows = (childrenByParent.get(key) ?? []).map((child) => build(child, depth + 1));
246
+ return {
247
+ ...row,
248
+ depth,
249
+ subRows,
250
+ leafCount: subRows.reduce((n, sub) => n + 1 + (sub.leafCount ?? 0), 0),
251
+ __treeRow: true,
252
+ getCanExpand: () => subRows.length > 0,
253
+ getIsExpanded: () => Boolean((table.getState().expanded ?? {})[id]),
254
+ toggleExpanded: () => {
255
+ table.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }));
256
+ },
257
+ };
258
+ };
259
+ return roots.map((root) => build(root, 0));
260
+ };
261
+ }
262
+ /**
263
+ * Flatten nested tree data into the flat parent-id shape `createTreeRowModel`
264
+ * consumes, stamping each child with its parent's id.
265
+ *
266
+ * Children are emitted directly after their parent so the natural order already
267
+ * matches the rendered tree. The `childrenField` array is left on the objects
268
+ * (harmless, and callers often still want it); only the parent link is added.
269
+ */
270
+ export function flattenTreeData(data, options) {
271
+ const { childrenField, idField = 'id', parentField = '__parentId' } = options;
272
+ const out = [];
273
+ const walk = (nodes, parentId) => {
274
+ for (const node of nodes) {
275
+ const flat = { ...node, [parentField]: parentId };
276
+ out.push(flat);
277
+ const kids = node[childrenField];
278
+ if (Array.isArray(kids) && kids.length)
279
+ walk(kids, node[idField]);
280
+ }
281
+ };
282
+ walk(data, null);
283
+ return out;
284
+ }
196
285
  export function createExpandedRowModel() {
197
286
  return ({ table, rows }) => {
198
287
  const expanded = table.getState().expanded ?? {};
@@ -63,16 +63,23 @@ export type SvEditorProps = {
63
63
  * The interaction surface an editor exposes when embedded as a grid cell editor.
64
64
  * Standalone usage ignores these; the grid supplies them so every editor shares
65
65
  * one commit / cancel / move contract (Enter commits, Escape cancels, Tab moves)
66
- * instead of each cell-editor re-implementing key handling. Consumed in Phase 2
67
- * (the editor registry).
66
+ * instead of each cell-editor re-implementing key handling.
67
+ *
68
+ * `CellEditorContext` (editor-registry.ts) extends this type, so what the grid
69
+ * hands a registered editor and what this contract promises are the same thing
70
+ * by construction rather than by convention.
68
71
  */
69
72
  export type EditorInteraction<V = unknown> = {
70
- /** Commit the current value and stop editing (Enter, option pick, blur-commit). */
71
- onCommit?: (value: V) => void;
73
+ /**
74
+ * Commit the value and stop editing (Enter, option pick, blur-commit).
75
+ * The argument is optional: an editor that has been reporting through
76
+ * `onChange` can just call `onCommit()` to commit what the grid already has.
77
+ */
78
+ onCommit?: (value?: V) => void;
72
79
  /** Abandon the edit and restore the previous value (Escape). */
73
80
  onCancel?: () => void;
74
81
  /** Commit, then move to the adjacent cell (Tab = 1, Shift+Tab = -1). */
75
- onCommitAndMove?: (value: V, direction: 1 | -1) => void;
82
+ onCommitAndMove?: (value: V | undefined, direction: 1 | -1) => void;
76
83
  /** Ask the surrounding popover/panel to close without committing. */
77
84
  onRequestClose?: () => void;
78
85
  /** True while mounted inside a grid cell (lets an editor tune autofocus etc.). */
@@ -20,8 +20,17 @@
20
20
  * mounting.
21
21
  */
22
22
  import type { Component } from 'svelte';
23
- /** Context the grid hands a cell editor when it mounts one for an edit. */
24
- export type CellEditorContext<T = unknown> = {
23
+ import type { EditorInteraction } from './editor-contract';
24
+ /**
25
+ * Context the grid hands a cell editor when it mounts one for an edit.
26
+ *
27
+ * Extends {@link EditorInteraction} - the shared commit / cancel / move contract
28
+ * the `Sv*` editors are written against - and narrows the parts the grid always
29
+ * supplies, so a registered component and a built-in editor speak the same
30
+ * language. `onCommitAndMove`, `onRequestClose` and `inCell` come from the
31
+ * contract and are always populated by the grid.
32
+ */
33
+ export type CellEditorContext<T = unknown> = Required<Pick<EditorInteraction<T>, 'onCommit' | 'onCancel' | 'onCommitAndMove' | 'onRequestClose' | 'inCell'>> & {
25
34
  /** The value currently being edited. */
26
35
  value: T;
27
36
  /** Id of the row being edited. */
@@ -30,10 +39,6 @@ export type CellEditorContext<T = unknown> = {
30
39
  columnId: string;
31
40
  /** Update the in-progress value WITHOUT ending the edit. */
32
41
  onChange: (value: T) => void;
33
- /** Commit the value and stop editing (optionally pass a final value). */
34
- onCommit: (value?: T) => void;
35
- /** Discard changes and stop editing (returns focus to the grid). */
36
- onCancel: () => void;
37
42
  };
38
43
  /** How a registered component is mounted and wired for a cell edit. */
39
44
  export type CellEditorRegistration = {
@@ -61,7 +66,13 @@ export declare function hasCellEditor(type: string): boolean;
61
66
  export declare function unregisterCellEditor(type: string): void;
62
67
  /** All registered custom editor type names. */
63
68
  export declare function registeredCellEditorTypes(): string[];
64
- /** The default context → props mapping used when a registration has no `props`. */
69
+ /**
70
+ * The default context → props mapping used when a registration has no `props`.
71
+ * Passes the whole {@link EditorInteraction} surface, so an editor written
72
+ * against the shared contract works when registered with no mapping at all.
73
+ * Svelte ignores props a component doesn't declare, so the extra keys are inert
74
+ * for a simple editor that only wants `value` + `onCommit`.
75
+ */
65
76
  export declare function defaultEditorProps(ctx: CellEditorContext): Record<string, unknown>;
66
77
  /** Resolve the props to spread onto a registered editor for a given context. */
67
78
  export declare function resolveEditorProps(registration: CellEditorRegistration, ctx: CellEditorContext): Record<string, unknown>;
@@ -23,13 +23,22 @@ export function unregisterCellEditor(type) {
23
23
  export function registeredCellEditorTypes() {
24
24
  return [...registry.keys()];
25
25
  }
26
- /** The default context → props mapping used when a registration has no `props`. */
26
+ /**
27
+ * The default context → props mapping used when a registration has no `props`.
28
+ * Passes the whole {@link EditorInteraction} surface, so an editor written
29
+ * against the shared contract works when registered with no mapping at all.
30
+ * Svelte ignores props a component doesn't declare, so the extra keys are inert
31
+ * for a simple editor that only wants `value` + `onCommit`.
32
+ */
27
33
  export function defaultEditorProps(ctx) {
28
34
  return {
29
35
  value: ctx.value,
30
36
  onChange: ctx.onChange,
31
37
  onCommit: ctx.onCommit,
32
38
  onCancel: ctx.onCancel,
39
+ onCommitAndMove: ctx.onCommitAndMove,
40
+ onRequestClose: ctx.onRequestClose,
41
+ inCell: ctx.inCell,
33
42
  };
34
43
  }
35
44
  /** Resolve the props to spread onto a registered editor for a given context. */
@@ -36,6 +36,45 @@ export type GroupFooterOptions<R> = {
36
36
  * preserved. No-op when neither footer flag is set.
37
37
  */
38
38
  export declare function insertGroupFooters<R>(rows: ReadonlyArray<R>, opts: GroupFooterOptions<R>): R[];
39
+ export type GroupPaginationOptions<R> = {
40
+ /** Depth of a row (group rows and their descendants). */
41
+ getDepth: (row: R) => number;
42
+ /** True when a row is a group (banner) row. */
43
+ isGroup: (row: R) => boolean;
44
+ /**
45
+ * True when a group row is expanded. Defaults to treating every group as
46
+ * expanded. This matters: an EXPANDED group is a header, reprinted above its
47
+ * children and costing no page budget, while a COLLAPSED group is the visible
48
+ * unit itself and must consume a slot - otherwise a fully collapsed grid has
49
+ * no budget-consuming rows at all and every page comes back empty.
50
+ */
51
+ isExpanded?: (row: R) => boolean;
52
+ pageIndex: number;
53
+ pageSize: number;
54
+ };
55
+ export type GroupPaginationResult<R> = {
56
+ /** The rows to render for this page: the page's budget-consuming rows, each
57
+ * preceded by whichever ancestor banners have not been printed yet. */
58
+ rows: R[];
59
+ /** Budget-consuming rows across every page - what the pager divides by.
60
+ * Data rows plus collapsed group rows; expanded banners do not count. */
61
+ dataRowCount: number;
62
+ };
63
+ /**
64
+ * Paginate a grouped row list by DATA rows, repeating each page's ancestor
65
+ * group headers.
66
+ *
67
+ * Slicing the flat grouped list directly counts group banners against
68
+ * `pageSize`, so `pageSize: 10` over ten groups of two shows almost no data
69
+ * (#73). Here only non-group rows consume the page budget, and a page reprints
70
+ * the group chain its first rows sit under - the same thing a spreadsheet does
71
+ * when a grouped table breaks across pages.
72
+ *
73
+ * Ancestors are tracked with a depth stack rather than parent pointers, so this
74
+ * works on the already-flattened expanded model and needs nothing from the row
75
+ * objects but depth and group-ness.
76
+ */
77
+ export declare function paginateGroupedRows<R>(rows: ReadonlyArray<R>, opts: GroupPaginationOptions<R>): GroupPaginationResult<R>;
39
78
  export type AutoGroupColumnSpec = {
40
79
  /** Synthetic column id (e.g. `__autoGroup` or `__group_<field>`). */
41
80
  id: string;
@@ -51,6 +51,58 @@ export function insertGroupFooters(rows, opts) {
51
51
  }
52
52
  return out;
53
53
  }
54
+ /**
55
+ * Paginate a grouped row list by DATA rows, repeating each page's ancestor
56
+ * group headers.
57
+ *
58
+ * Slicing the flat grouped list directly counts group banners against
59
+ * `pageSize`, so `pageSize: 10` over ten groups of two shows almost no data
60
+ * (#73). Here only non-group rows consume the page budget, and a page reprints
61
+ * the group chain its first rows sit under - the same thing a spreadsheet does
62
+ * when a grouped table breaks across pages.
63
+ *
64
+ * Ancestors are tracked with a depth stack rather than parent pointers, so this
65
+ * works on the already-flattened expanded model and needs nothing from the row
66
+ * objects but depth and group-ness.
67
+ */
68
+ export function paginateGroupedRows(rows, opts) {
69
+ const { getDepth, isGroup, isExpanded, pageIndex, pageSize } = opts;
70
+ const expanded = isExpanded ?? (() => true);
71
+ const start = pageIndex * pageSize;
72
+ const end = start + pageSize;
73
+ const out = [];
74
+ // Expanded banners currently open, outermost first. `printed` marks the ones
75
+ // this page has already emitted, so a group spanning rows 3-8 prints its
76
+ // banner once, not once per row.
77
+ const openGroups = [];
78
+ let unitsSeen = 0;
79
+ for (const row of rows) {
80
+ const depth = getDepth(row);
81
+ // A row at depth <= an open banner's depth closes it.
82
+ while (openGroups.length > 0 && getDepth(openGroups[openGroups.length - 1].row) >= depth) {
83
+ openGroups.pop();
84
+ }
85
+ if (isGroup(row) && expanded(row)) {
86
+ // A header for rows that follow: reprinted per page, costs no budget.
87
+ openGroups.push({ row, printed: false });
88
+ continue;
89
+ }
90
+ // Everything else is a visible unit: a data row, or a collapsed group
91
+ // standing in for its whole subtree.
92
+ const index = unitsSeen;
93
+ unitsSeen += 1;
94
+ if (index < start || index >= end)
95
+ continue;
96
+ for (const g of openGroups) {
97
+ if (g.printed)
98
+ continue;
99
+ g.printed = true;
100
+ out.push(g.row);
101
+ }
102
+ out.push(row);
103
+ }
104
+ return { rows: out, dataRowCount: unitsSeen };
105
+ }
54
106
  /**
55
107
  * Decide the synthetic auto-group columns for a grouping display mode:
56
108
  * - `groupRows`: none (the banner path handles display) - `{ [], {} }`.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { formatNumericWithConfig, resolveDatePattern } from './cell-formatting';
2
2
  export { coerceExportDate, formatValueForExport, projectGridRows, toExcelNumFmt, valueForExcel, serializeDelimited, serializeJson, serializeMarkdown, serializeHtml, serializeXml, downloadTextFile, downloadBlobFile, copyTextToClipboard, type GridExportOptions, type GridExportScope, type GridExportColumn, type GridClipboardOptions, type GridClipboardFormat, type SerializeProgress, type SerializeOptions, type CsvOptions, } from './export-format';
3
- export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, applyGroupAggregate, filterFns, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, type Cell, type CellContext, type CellSpanParams, type ValueParserParams, type EditorContext, type CellData, type ActiveCellState, type CellFormatConfig, type CellFormatter, type Column, type ColumnDef, type ColumnDefTemplate, type GroupAggregator, type Header, type HeaderContext, type HeaderGroup, type Row, type RowData, type SortingState, type SvGrid as SvGridInstance, type SvGridOptions, type TableFeatures, type Updater, } from './core';
3
+ export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, createTreeRowModel, flattenTreeData, applyGroupAggregate, filterFns, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, type Cell, type CellContext, type CellSpanParams, type ValueParserParams, type EditorContext, type CellData, type ActiveCellState, type CellFormatConfig, type CellFormatter, type Column, type ColumnDef, type ColumnDefTemplate, type GridColumnDef, type GridColumns, type GroupAggregator, type Header, type HeaderContext, type HeaderGroup, type Row, type RowData, type SortingState, type SvGrid as SvGridInstance, type SvGridOptions, type TableFeatures, type TreeRowModelOptions, type FlattenTreeOptions, type Updater, } from './core';
4
4
  export { createGrid, createSvGrid, type SvelteGrid } from './createGrid.svelte';
5
5
  export { createGridState, createSvGridState } from './createGridState.svelte';
6
6
  export { subscribeGrid, subscribeSvGrid } from './subscribe';
@@ -206,7 +206,7 @@ export { default as SvRowGroupPanel } from './SvRowGroupPanel.svelte';
206
206
  export { createNamedViews, memoryViews, localStorageViews, attachAutoSavedView, type SavedView, type ViewStorage, type NamedViews, type AutoSavedViewOptions, } from './named-views';
207
207
  export { resolveCellFormat, computeColumnStat, lerpColor, rgba, contrastText, formatNeedsStats, formatsNeedingStats, type ConditionalFormat, type ConditionalFormatSpec, type ColorScaleFormat, type ColorScaleStop, type ScaleBounds, type DataBarFormat, type IconSetFormat, type RuleFormat, type ResolvedCellFormat, } from './conditional-formatting';
208
208
  export { defaultGridMessages, resolveGridMessages, type GridMessages, } from './grid-messages';
209
- export { insertGroupFooters, buildAutoGroupColumns, type GroupDisplayType, type GroupFooterOptions, type AutoGroupColumnSpec, type AutoGroupResult, } from './group-display';
209
+ export { insertGroupFooters, buildAutoGroupColumns, paginateGroupedRows, type GroupDisplayType, type GroupFooterOptions, type AutoGroupColumnSpec, type AutoGroupResult, type GroupPaginationOptions, type GroupPaginationResult, } from './group-display';
210
210
  export { getKeyboardIntent, getNextActiveCell, type GridKeyboardIntent } from './keyboard';
211
211
  export { createVirtualizer } from './virtualization/virtualizer';
212
212
  export { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.svelte';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { formatNumericWithConfig, resolveDatePattern } from './cell-formatting';
2
2
  export { coerceExportDate, formatValueForExport, projectGridRows, toExcelNumFmt, valueForExcel, serializeDelimited, serializeJson, serializeMarkdown, serializeHtml, serializeXml, downloadTextFile, downloadBlobFile, copyTextToClipboard, } from './export-format';
3
- export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, applyGroupAggregate, filterFns, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, } from './core';
3
+ export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, createTreeRowModel, flattenTreeData, applyGroupAggregate, filterFns, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, } from './core';
4
4
  export { createGrid, createSvGrid } from './createGrid.svelte';
5
5
  export { createGridState, createSvGridState } from './createGridState.svelte';
6
6
  export { subscribeGrid, subscribeSvGrid } from './subscribe';
@@ -243,7 +243,7 @@ export { default as SvRowGroupPanel } from './SvRowGroupPanel.svelte';
243
243
  export { createNamedViews, memoryViews, localStorageViews, attachAutoSavedView, } from './named-views';
244
244
  export { resolveCellFormat, computeColumnStat, lerpColor, rgba, contrastText, formatNeedsStats, formatsNeedingStats, } from './conditional-formatting';
245
245
  export { defaultGridMessages, resolveGridMessages, } from './grid-messages';
246
- export { insertGroupFooters, buildAutoGroupColumns, } from './group-display';
246
+ export { insertGroupFooters, buildAutoGroupColumns, paginateGroupedRows, } from './group-display';
247
247
  export { getKeyboardIntent, getNextActiveCell } from './keyboard';
248
248
  export { createVirtualizer } from './virtualization/virtualizer';
249
249
  export { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.svelte';
@@ -107,6 +107,22 @@ export function createKeyboard(ctx) {
107
107
  // expands a collapsed group row, ArrowLeft collapses an expanded one,
108
108
  // instead of moving the active cell (treegrid-style). Leaves fall through
109
109
  // to normal navigation.
110
+ // Client tree data gets the same ArrowRight/ArrowLeft contract, driven off
111
+ // the row model rather than consumer callbacks.
112
+ if (ctx.props.treeData && (intent === "moveRight" || intent === "moveLeft")) {
113
+ const row = ctx.allRows[current.rowIndex];
114
+ if (row?.getCanExpand?.()) {
115
+ const expanded = row.getIsExpanded?.() ?? false;
116
+ if (intent === "moveRight" && !expanded) {
117
+ row.toggleExpanded?.();
118
+ return;
119
+ }
120
+ if (intent === "moveLeft" && expanded) {
121
+ row.toggleExpanded?.();
122
+ return;
123
+ }
124
+ }
125
+ }
110
126
  const serverGroup = ctx.props.serverGroup;
111
127
  if (serverGroup && (intent === "moveRight" || intent === "moveLeft")) {
112
128
  const data = ctx.allRows[current.rowIndex]?.original;
@@ -149,8 +149,9 @@ const MO = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct'
149
149
  const ORD = { 1: 'first', 2: 'second', 3: 'third', 4: 'fourth', 5: 'fifth', [-1]: 'last' };
150
150
  // Where in the month a monthly / yearly rule lands, as a phrase.
151
151
  function whereInMonth(r, wd, ord) {
152
- if (r.weekOfMonth != null && r.weekdays && r.weekdays.length) {
153
- return `the ${ord[r.weekOfMonth] ?? r.weekOfMonth} ${wd[r.weekdays[0]]}`;
152
+ const firstWeekday = r.weekdays?.[0];
153
+ if (r.weekOfMonth != null && firstWeekday != null) {
154
+ return `the ${ord[r.weekOfMonth] ?? r.weekOfMonth} ${wd[firstWeekday]}`;
154
155
  }
155
156
  if (r.day === -1)
156
157
  return 'the last day';
@@ -1,8 +1,8 @@
1
1
  import type { CellFormatConfig, ColumnDef, RowData, SvGridOptions, TableFeatures } from './core';
2
- import type { Props } from './SvGrid.types';
2
+ import type { FilterOperator, Props } from './SvGrid.types';
3
3
  import type { GridExportOptions, GridClipboardOptions } from './export-format';
4
4
  import type { ChartSpec, ChartType } from './chart';
5
- export type SvGridFilterOperator = 'contains' | 'equals' | 'startsWith' | 'greaterThan' | 'lessThan' | 'between' | 'isBlank';
5
+ export type SvGridFilterOperator = FilterOperator;
6
6
  /**
7
7
  * A serializable snapshot of everything that makes up the current "view":
8
8
  * sort, grouping, pagination, column layout (width / pinning / order /
@@ -211,6 +211,12 @@ export type SvGridApi<TFeatures extends TableFeatures, TData extends RowData> =
211
211
  */
212
212
  setFacetFilter(columnId: string, values: ReadonlyArray<string> | null): void;
213
213
  clearFilter(columnId: string): void;
214
+ /**
215
+ * Drop cached async `editorOptions` so the next render refetches them.
216
+ * Omit `columnId` to clear every column. Only affects columns whose
217
+ * `editorOptions` returns a Promise - static lists are never refetched.
218
+ */
219
+ refreshEditorOptions(columnId?: string): void;
214
220
  /**
215
221
  * Clear every active column filter (menu, filter-row, set-list, and global).
216
222
  * Resets the grid to "no filtering" in a single call.
@@ -37,6 +37,16 @@ export type ThemePalette = {
37
37
  pinnedBorder?: string;
38
38
  /** Drop shadow cast by a pinned column into the scroll area. */
39
39
  pinnedShadow?: string;
40
+ /** Errors, destructive actions, invalid cells. */
41
+ danger?: string;
42
+ /** Confirmations, positive deltas. */
43
+ success?: string;
44
+ /** Cautions, pending states, the rating star. */
45
+ warning?: string;
46
+ /** Neutral informational callouts. */
47
+ info?: string;
48
+ /** Focus outline color. Defaults to the preset's accent. */
49
+ focusRing?: string;
40
50
  };
41
51
  export type ThemePreset = {
42
52
  /** Stable id, e.g. used as a CSS file name (`themes/<id>.css`). */
@@ -8,6 +8,12 @@
8
8
  * browser. `@svgrid/enterprise`'s Studio layer builds its `ProjectTheme`-aware
9
9
  * wrapper on top of this module instead of keeping its own copy.
10
10
  */
11
+ /** Status-color defaults per mode - dark mode lifts them so they stay legible
12
+ * on a dark ground. Values match the components' own inline fallbacks. */
13
+ const STATUS_DEFAULTS = {
14
+ light: { danger: '#dc2626', success: '#16a34a', warning: '#d97706', info: '#0891b2' },
15
+ dark: { danger: '#f87171', success: '#4ade80', warning: '#fbbf24', info: '#22d3ee' },
16
+ };
11
17
  /** The built-in theme presets, in gallery order (SvGrid's own theme first). */
12
18
  export const themePresets = [
13
19
  // Ember is SvGrid's signature look - the palette svgrid.com itself runs on, so
@@ -108,6 +114,7 @@ export function resolveThemeTokens(preset, mode, accentOverride) {
108
114
  const resolved = preset ?? defaultThemePreset;
109
115
  const p = resolved[mode];
110
116
  const accent = accentOverride || p.accent;
117
+ const status = STATUS_DEFAULTS[mode];
111
118
  return {
112
119
  '--sg-bg': p.bg,
113
120
  '--sg-fg': p.fg,
@@ -146,6 +153,25 @@ export function resolveThemeTokens(preset, mode, accentOverride) {
146
153
  '--sg-on-accent': onAccent(accent),
147
154
  '--sg-radius': `${resolved.radius}px`,
148
155
  '--sg-font': resolved.font,
156
+ // Semantic status colors. Same reasoning as the scrollbar tokens below:
157
+ // the UI kit consumes these (destructive buttons, alerts, validation,
158
+ // badges, the rating star), so a preset that doesn't define them leaves
159
+ // every status color at a hard-coded red/green/amber that ignores the
160
+ // theme and, on dark presets, sits at the wrong lightness.
161
+ '--sg-danger': p.danger ?? status.danger,
162
+ '--sg-success': p.success ?? status.success,
163
+ '--sg-warning': p.warning ?? status.warning,
164
+ '--sg-info': p.info ?? status.info,
165
+ // A COLOR, not a shadow - consumed as `outline: 2px solid var(...)`.
166
+ '--sg-focus-ring': p.focusRing ?? accent,
167
+ // Derived surfaces + shape the kit reads. Deriving from the palette keeps
168
+ // them following the preset and the light/dark mode.
169
+ '--sg-muted-bg': p.rowHover,
170
+ '--sg-skeleton-bg': p.rowHover,
171
+ '--sg-radius-lg': `${resolved.radius * 2}px`,
172
+ '--sg-rating-on': p.warning ?? status.warning,
173
+ '--sg-rating-empty': p.border,
174
+ '--sg-invalid-fg': p.danger ?? status.danger,
149
175
  // The grid's custom scrollbar is a shadow-DOM web component that reads its
150
176
  // OWN --sg-scrollbar-* tokens (with fixed light-mode fallbacks). Without
151
177
  // these it ignores the theme entirely - a light scrollbar left on a dark
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svgrid/grid",
3
- "version": "2.2.30",
3
+ "version": "2.2.32",
4
4
  "description": "Svelte 5-native data grid and data table. Headless-first engine with virtual scrolling for 100k+ rows, Excel-style filtering, inline editing, grouping, tree data, pivot, and server-side data. Drop-in <SvGrid> component. TypeScript, SSR-ready.",
5
5
  "author": "jQWidgets <boikom@jqwidgets.com>",
6
6
  "license": "MIT",
@@ -32,11 +32,11 @@ describe('SvAccordion', () => {
32
32
  try {
33
33
  const headers = [...target.querySelectorAll<HTMLButtonElement>('.sv-acc__header')]
34
34
  expect(headers).toHaveLength(3)
35
- expect(headers[0].getAttribute('aria-expanded')).toBe('true')
36
- expect(headers[1].getAttribute('aria-expanded')).toBe('false')
35
+ expect(headers[0]!.getAttribute('aria-expanded')).toBe('true')
36
+ expect(headers[1]!.getAttribute('aria-expanded')).toBe('false')
37
37
  const region = target.querySelector<HTMLElement>('.sv-acc__panel')!
38
38
  expect(region.getAttribute('role')).toBe('region')
39
- expect(region.getAttribute('aria-labelledby')).toBe(headers[0].id)
39
+ expect(region.getAttribute('aria-labelledby')).toBe(headers[0]!.id)
40
40
  // Only one panel region is rendered (the open one).
41
41
  expect(target.querySelectorAll('.sv-acc__panel')).toHaveLength(1)
42
42
  } finally { destroy() }
@@ -47,7 +47,7 @@ describe('SvAccordion', () => {
47
47
  const { target, destroy } = mountAcc({ items, expanded: ['a'], expandMode: 'single', onChange: (ids: string[]) => (got = ids) })
48
48
  try {
49
49
  const headers = target.querySelectorAll<HTMLButtonElement>('.sv-acc__header')
50
- headers[1].click()
50
+ headers[1]!.click()
51
51
  flushSync()
52
52
  expect(got).toEqual(['b'])
53
53
  } finally { destroy() }
@@ -58,7 +58,7 @@ describe('SvAccordion', () => {
58
58
  const { target, destroy } = mountAcc({ items, expanded: ['a'], expandMode: 'multiple', onChange: (ids: string[]) => (got = ids) })
59
59
  try {
60
60
  const headers = target.querySelectorAll<HTMLButtonElement>('.sv-acc__header')
61
- headers[2].click()
61
+ headers[2]!.click()
62
62
  flushSync()
63
63
  expect(got).toEqual(['a', 'c'])
64
64
  } finally { destroy() }
@@ -71,5 +71,5 @@
71
71
  .sv-avatar__status.is-online { background: var(--sg-success, #16a34a); }
72
72
  .sv-avatar__status.is-offline { background: var(--sg-muted, #94a3b8); }
73
73
  .sv-avatar__status.is-busy { background: var(--sg-danger, #dc2626); }
74
- .sv-avatar__status.is-away { background: var(--sg-warning, #f59e0b); }
74
+ .sv-avatar__status.is-away { background: var(--sg-warning, #d97706); }
75
75
  </style>