@svgrid/grid 2.6.19 → 2.6.21

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 (65) hide show
  1. package/dist/SvGrid.controller.svelte.d.ts +1 -0
  2. package/dist/SvGrid.controller.svelte.js +64 -2
  3. package/dist/SvGrid.svelte +2 -1
  4. package/dist/SvGrid.types.d.ts +150 -0
  5. package/dist/ai.d.ts +28 -0
  6. package/dist/ai.js +6 -0
  7. package/dist/cdn/{GridMenus-B0F9iBrG.js → GridMenus-BfTAKn84.js} +1 -1
  8. package/dist/cdn/{GridMenus-IHK_l7m6.js → GridMenus-C3bJd7w8.js} +1 -1
  9. package/dist/cdn/{src-Cd0tearp.js → src-BYq-qyrp.js} +1012 -999
  10. package/dist/cdn/{src-B1TdiyS8.js → src-DBel9wRZ.js} +1324 -1311
  11. package/dist/cdn/svgrid.js +1 -1
  12. package/dist/cdn/svgrid.svelte-external.js +1 -1
  13. package/dist/cdn/validate-_CDJzgIo.js +75 -0
  14. package/dist/cell-formatting.d.ts +2 -0
  15. package/dist/cell-formatting.js +2 -0
  16. package/dist/chart-export.d.ts +1 -0
  17. package/dist/chart.d.ts +31 -5
  18. package/dist/chart.js +9 -3
  19. package/dist/core.d.ts +197 -0
  20. package/dist/core.js +72 -0
  21. package/dist/createTree.svelte.d.ts +3 -0
  22. package/dist/createTree.svelte.js +1 -0
  23. package/dist/datetime/date-core.d.ts +2 -0
  24. package/dist/datetime/date-restrict.d.ts +1 -0
  25. package/dist/datetime/timezone.d.ts +1 -0
  26. package/dist/dock-manager-model.d.ts +3 -0
  27. package/dist/dock-manager-model.js +1 -0
  28. package/dist/dock-model.d.ts +6 -0
  29. package/dist/dock-model.js +3 -0
  30. package/dist/editor-contract.d.ts +1 -0
  31. package/dist/list-option.d.ts +1 -0
  32. package/dist/positioning.d.ts +2 -0
  33. package/dist/scheduler-ical.d.ts +1 -0
  34. package/dist/scheduler-model.d.ts +1 -0
  35. package/dist/summaries.js +22 -0
  36. package/dist/svgrid-wrapper.types.d.ts +5 -0
  37. package/dist/toast-store.svelte.d.ts +4 -0
  38. package/dist/validate.d.ts +50 -0
  39. package/dist/validate.js +187 -0
  40. package/package.json +4 -1
  41. package/src/SvGrid.controller.svelte.ts +68 -2
  42. package/src/SvGrid.svelte +2 -1
  43. package/src/SvGrid.types.ts +150 -0
  44. package/src/ai.ts +28 -0
  45. package/src/cell-formatting.ts +2 -0
  46. package/src/chart-export.ts +1 -0
  47. package/src/chart.ts +31 -5
  48. package/src/core.ts +207 -0
  49. package/src/createTree.svelte.ts +3 -0
  50. package/src/datetime/date-core.ts +2 -0
  51. package/src/datetime/date-restrict.ts +1 -0
  52. package/src/datetime/timezone.ts +1 -0
  53. package/src/dock-manager-model.ts +3 -0
  54. package/src/dock-model.ts +6 -0
  55. package/src/editor-contract.ts +1 -0
  56. package/src/list-option.ts +1 -0
  57. package/src/positioning.ts +2 -0
  58. package/src/scheduler-ical.ts +1 -0
  59. package/src/scheduler-model.ts +1 -0
  60. package/src/summaries.ts +21 -0
  61. package/src/svgrid-wrapper.types.ts +5 -0
  62. package/src/svgrid.summaries.test.ts +217 -0
  63. package/src/toast-store.svelte.ts +4 -0
  64. package/src/validate.test.ts +207 -0
  65. package/src/validate.ts +269 -0
@@ -37,6 +37,7 @@ export type Viewport = {
37
37
  width: number;
38
38
  height: number;
39
39
  };
40
+ /** Where to put a floating element: its preferred side, offsets, and collision behaviour. */
40
41
  export type ComputePositionOptions = {
41
42
  /** Preferred placement. Default `'bottom-start'`. */
42
43
  placement?: Placement;
@@ -61,6 +62,7 @@ export type ComputePositionOptions = {
61
62
  /** Floor for the available main-axis size returned by `size`. Default 0. */
62
63
  minMainAxis?: number;
63
64
  };
65
+ /** The resolved position, including the side actually used after collision handling. */
64
66
  export type ComputePositionResult = {
65
67
  /** Floating left, in viewport (fixed-position) coordinates. */
66
68
  x: number;
@@ -6,6 +6,7 @@
6
6
  * LOCATION. No `Date.now()` in the core - `toICS` accepts a stamp.
7
7
  */
8
8
  import type { RecurrenceRule } from './recurrence.js';
9
+ /** One VEVENT: the fields SvGrid reads from and writes to an iCalendar feed. */
9
10
  export type ICalEvent = {
10
11
  uid?: string;
11
12
  title: string;
@@ -171,6 +171,7 @@ export type DayLayout<TData = unknown> = {
171
171
  events: PositionedEvent<TData>[];
172
172
  overflows: OverflowMarker<TData>[];
173
173
  };
174
+ /** Tuning for event layout: how overlapping events share horizontal space. */
174
175
  export type LayoutOptions = {
175
176
  dayStartHour?: number;
176
177
  dayEndHour?: number;
package/dist/summaries.js CHANGED
@@ -1,3 +1,7 @@
1
+ // summaries handlers extracted from the controller. Imperative event handlers
2
+ // reading/writing controller state via the `ctx` handle; the reactive core
3
+ // ($state/$derived/$effect) stays in the controller.
4
+ import { applyGroupAggregate, } from "./index.js";
1
5
  import "./sv-grid-scrollbar.js";
2
6
  import { getCellKey, } from "./SvGrid.helpers.js";
3
7
  import { formatSummaryNumeric, } from "./cell-values.js";
@@ -25,6 +29,24 @@ export function createSummaries(ctx) {
25
29
  const fieldFn = def.fieldFn;
26
30
  const field = def.field;
27
31
  const columnId = column.id;
32
+ // A column that declares its own `summary` opts out of the default
33
+ // sum/count below. Only that column pays for the aggregator dispatch, so
34
+ // a grid that declares none keeps the original hot loop exactly as it was.
35
+ const declared = def.summary;
36
+ if (declared !== undefined) {
37
+ if (declared === false) {
38
+ summary[columnId] = "";
39
+ continue;
40
+ }
41
+ const value = applyGroupAggregate(declared, columnId, rows);
42
+ summary[columnId] =
43
+ typeof value === "number" && Number.isFinite(value)
44
+ ? formatSummaryNumeric(column, value)
45
+ : value == null
46
+ ? ""
47
+ : String(value);
48
+ continue;
49
+ }
28
50
  let numericSum = 0;
29
51
  let numericCount = 0;
30
52
  for (let i = 0; i < rowCount; i += 1) {
@@ -3,6 +3,7 @@ import type { FilterOperator, Props } from './SvGrid.types.js';
3
3
  import type { GridExportOptions, GridClipboardOptions } from './export-format.js';
4
4
  import type { ChartSpec, ChartType } from './chart.js';
5
5
  import type { GridPredicateExpr } from './filtering/predicate-expr.js';
6
+ /** The comparisons a column filter can use, as shown in the filter menu. */
6
7
  export type SvGridFilterOperator = FilterOperator;
7
8
  /**
8
9
  * A serializable snapshot of everything that makes up the current "view":
@@ -80,6 +81,7 @@ export type SvGridTransaction<TData> = {
80
81
  update?: ReadonlyArray<TData>;
81
82
  remove?: ReadonlyArray<TData | string>;
82
83
  };
84
+ /** What a batched `applyTransaction` did: how many rows were added, updated and removed. */
83
85
  export type SvGridTransactionResult = {
84
86
  added: number;
85
87
  updated: number;
@@ -458,6 +460,7 @@ export type SvGridApi<TFeatures extends TableFeatures, TData extends RowData> =
458
460
  /** Force a recompute of the row pipeline + a re-render. */
459
461
  refresh(): void;
460
462
  };
463
+ /** The props `<SvGrid>` accepts. See the SvGrid reference for the full list with defaults. */
461
464
  export type SvGridWrapperProps<TFeatures extends TableFeatures, TData extends RowData> = {
462
465
  data: ReadonlyArray<TData>;
463
466
  columns: Array<ColumnDef<TFeatures, TData>>;
@@ -505,6 +508,8 @@ export type SvGridWrapperProps<TFeatures extends TableFeatures, TData extends Ro
505
508
  enableCellSelection?: boolean;
506
509
  enableInlineEditing?: boolean;
507
510
  enableRowSummaries?: boolean;
511
+ /** Shortcut alias for `enableRowSummaries`; wins when both are set. */
512
+ summary?: boolean;
508
513
  /** Receives the imperative grid API when the component is ready. */
509
514
  onApiReady?: (api: SvGridApi<TFeatures, TData>) => void;
510
515
  };
@@ -14,6 +14,7 @@
14
14
  * Mount a single <SvToaster /> once near the app root to render the queue.
15
15
  */
16
16
  import type { Snippet } from 'svelte';
17
+ /** A toast's severity, which selects its colour and icon. */
17
18
  export type ToastVariant = 'info' | 'success' | 'warning' | 'error';
18
19
  /** A button rendered inside a toast (a primary `action` or a secondary `cancel`). */
19
20
  export type ToastAction = {
@@ -23,6 +24,7 @@ export type ToastAction = {
23
24
  /** Keep the toast open after the click (default: dismiss it). */
24
25
  keepOpen?: boolean;
25
26
  };
27
+ /** Options for one toast: its variant, how long it stays, and any action button. */
26
28
  export type ToastOptions = {
27
29
  variant?: ToastVariant;
28
30
  /** Auto-dismiss after N ms. `0` = sticky (dismiss manually). Default 4000. */
@@ -38,6 +40,7 @@ export type ToastOptions = {
38
40
  /** Render a fully custom toast body instead of the icon + title + message. */
39
41
  render?: Snippet<[Toast]>;
40
42
  };
43
+ /** A live toast: its options plus the id needed to dismiss it. */
41
44
  export type Toast = {
42
45
  id: number;
43
46
  message: string;
@@ -58,6 +61,7 @@ export type PromiseMessages<T> = {
58
61
  error: string | ((error: unknown) => string);
59
62
  };
60
63
  type VariantOptions = Omit<ToastOptions, 'variant'>;
64
+ /** The callable toast API - `toast(msg)` plus `.success` / `.error` / friends. */
61
65
  export type ToastFn = {
62
66
  (message: string, options?: ToastOptions): number;
63
67
  info: (message: string, options?: VariantOptions) => number;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Development-time configuration checks.
3
+ *
4
+ * The grid used to fail silently on the most common mistakes. A misspelled
5
+ * `field` rendered a column of empty cells with nothing on the console; a
6
+ * `pageSize` with no pagination was quietly ignored; a column marked
7
+ * `sortable` with no sorting enabled just did not sort. Each of those costs
8
+ * someone an afternoon, and none of them were detectable from the outside.
9
+ *
10
+ * This module is the check. It is pure and returns messages rather than
11
+ * printing them, so it can be unit-tested; the controller runs it in an effect
12
+ * and prints each message once, in dev builds only.
13
+ *
14
+ * Rules for anything added here:
15
+ * - Never fire on valid config. A false positive teaches people to ignore
16
+ * warnings, which is worse than staying silent.
17
+ * - Say what happened, what it means, and how to fix it - the house style set
18
+ * by the one pre-existing warning in server-data-source.ts.
19
+ * - Stay O(columns), or O(columns x a fixed row sample). This runs on data
20
+ * changes, and must not scale with row count.
21
+ */
22
+ import type { ColumnDef, RowData, TableFeatures } from './core.js';
23
+ export type ValidateInput<TFeatures extends TableFeatures, TData extends RowData> = {
24
+ data: ReadonlyArray<TData>;
25
+ columns: ReadonlyArray<ColumnDef<TFeatures, TData>>;
26
+ /** The resolved feature set, after the boolean shortcuts have injected theirs. */
27
+ features?: Record<string, unknown>;
28
+ sortable?: boolean;
29
+ pageable?: boolean;
30
+ showPagination?: boolean;
31
+ pageSize?: number;
32
+ groupBy?: ReadonlyArray<string>;
33
+ treeData?: {
34
+ parentField?: string;
35
+ idField?: string;
36
+ column?: string;
37
+ };
38
+ initialColumnPinning?: {
39
+ left?: ReadonlyArray<string>;
40
+ right?: ReadonlyArray<string>;
41
+ };
42
+ columnVirtualization?: boolean;
43
+ externalPagination?: boolean;
44
+ rowCount?: number;
45
+ externalSort?: boolean;
46
+ onSortingChange?: unknown;
47
+ externalFilter?: boolean;
48
+ onFiltersChange?: unknown;
49
+ };
50
+ export declare function validateGridConfig<TFeatures extends TableFeatures, TData extends RowData>(input: ValidateInput<TFeatures, TData>): string[];
@@ -0,0 +1,187 @@
1
+ /** How many rows to sample when deciding whether a `field` exists. */
2
+ const FIELD_SAMPLE_ROWS = 10;
3
+ const DOCS = 'https://svgrid.com/docs/getting-started/3-data-and-columns/';
4
+ /** Every leaf column, flattened through group columns. */
5
+ function leaves(columns, out = []) {
6
+ for (const col of columns) {
7
+ if (col.columns?.length)
8
+ leaves(col.columns, out);
9
+ else
10
+ out.push(col);
11
+ }
12
+ return out;
13
+ }
14
+ export function validateGridConfig(input) {
15
+ const messages = [];
16
+ const cols = leaves(input.columns ?? []);
17
+ // ---- 1. A `field` that does not exist on the data -------------------------
18
+ // The single most expensive silent failure: the column renders, and every
19
+ // cell in it is blank. Sampled across several rows so genuinely sparse data
20
+ // (a key absent from row 0 but present later) does not trip it.
21
+ const sample = (input.data ?? []).slice(0, FIELD_SAMPLE_ROWS);
22
+ if (sample.length) {
23
+ const known = new Set();
24
+ for (const row of sample) {
25
+ if (row && typeof row === 'object')
26
+ for (const k of Object.keys(row))
27
+ known.add(k);
28
+ }
29
+ for (const col of cols) {
30
+ const field = col.field;
31
+ // `fieldFn` computes its value, and an id-only column (actions, buttons)
32
+ // never reads the row - neither needs a matching key.
33
+ if (!field || col.fieldFn)
34
+ continue;
35
+ if (known.has(field))
36
+ continue;
37
+ const guess = nearest(field, [...known]);
38
+ messages.push(`[svgrid] Column field "${field}" does not exist on your row data, so that ` +
39
+ `column renders empty.${guess ? ` Did you mean "${guess}"?` : ''} ` +
40
+ `Available keys: ${[...known].slice(0, 12).join(', ')}. See ${DOCS}`);
41
+ }
42
+ }
43
+ // ---- 2. Duplicate column ids ---------------------------------------------
44
+ // Two columns resolving to the same id makes selection, sorting and column
45
+ // state address the wrong one, with no error anywhere.
46
+ const seen = new Map();
47
+ for (const col of cols) {
48
+ const id = (col.id ?? col.field);
49
+ if (!id)
50
+ continue;
51
+ seen.set(id, (seen.get(id) ?? 0) + 1);
52
+ }
53
+ for (const [id, n] of seen) {
54
+ if (n > 1) {
55
+ messages.push(`[svgrid] ${n} columns share the id "${id}". Column state (sorting, ` +
56
+ `filtering, pinning, width) is keyed by id, so they will act as one. ` +
57
+ `Give each an explicit \`id\`. See ${DOCS}`);
58
+ }
59
+ }
60
+ // ---- 3. `pageSize` with pagination never asked for ------------------------
61
+ // Only when BOTH pagination props are absent. `pageable={false}` is a
62
+ // deliberate statement - often a bound toggle that flips on later - whereas
63
+ // an unset prop means the author never considered it. Warning on the former
64
+ // fired on our own shortcut-config demo, which is exactly the kind of noise
65
+ // that teaches people to ignore warnings.
66
+ const paginationUnset = input.pageable === undefined && input.showPagination === undefined;
67
+ if (input.pageSize !== undefined && paginationUnset) {
68
+ messages.push('[svgrid] `pageSize` is set but pagination was never turned on, so it has ' +
69
+ 'no effect and every row renders. Add `pageable` to switch the pager on.');
70
+ }
71
+ // ---- 4. A column asks to sort, but nothing enables sorting ----------------
72
+ // `sortable` on the grid injects rowSortingFeature, so only warn when neither
73
+ // the shortcut nor an explicitly registered feature is present.
74
+ const sortingEnabled = input.sortable === true || Boolean(input.features?.rowSortingFeature);
75
+ if (!sortingEnabled && cols.some((c) => c.sortable === true)) {
76
+ messages.push('[svgrid] A column sets `sortable: true`, but sorting is not enabled on the ' +
77
+ 'grid, so its header does nothing. Add `sortable` to <SvGrid> (it registers ' +
78
+ 'the sorting feature for you).');
79
+ }
80
+ // ---- 5. Column ids referenced by other props ------------------------------
81
+ // `groupBy` and `treeData.column` address columns by id. A name that matches
82
+ // nothing is silently ignored, so grouping or the tree expander just never
83
+ // appears and there is nothing to debug against.
84
+ const columnIds = new Set();
85
+ for (const col of cols) {
86
+ const id = (col.id ?? col.field);
87
+ if (id)
88
+ columnIds.add(id);
89
+ }
90
+ for (const id of input.groupBy ?? []) {
91
+ if (columnIds.has(id))
92
+ continue;
93
+ const guess = nearest(id, [...columnIds]);
94
+ messages.push(`[svgrid] \`groupBy\` refers to column "${id}", which does not exist, so ` +
95
+ `that grouping level is ignored.${guess ? ` Did you mean "${guess}"?` : ''} ` +
96
+ `Column ids: ${[...columnIds].slice(0, 12).join(', ')}`);
97
+ }
98
+ const treeColumn = input.treeData?.column;
99
+ if (treeColumn && !columnIds.has(treeColumn)) {
100
+ messages.push(`[svgrid] \`treeData.column\` refers to column "${treeColumn}", which does ` +
101
+ `not exist, so the expander falls back to the first visible column.`);
102
+ }
103
+ // ---- 6. treeData pointing at fields the data does not have ----------------
104
+ if (sample.length && input.treeData) {
105
+ const known = new Set();
106
+ for (const row of sample) {
107
+ if (row && typeof row === 'object')
108
+ for (const k of Object.keys(row))
109
+ known.add(k);
110
+ }
111
+ const parentField = input.treeData.parentField;
112
+ if (parentField && !known.has(parentField)) {
113
+ const guess = nearest(parentField, [...known]);
114
+ messages.push(`[svgrid] \`treeData.parentField\` is "${parentField}", which is not on ` +
115
+ `your row data, so every row becomes a root and no hierarchy appears.` +
116
+ `${guess ? ` Did you mean "${guess}"?` : ''}`);
117
+ }
118
+ // `idField` defaults to 'id'; only check what was asked for explicitly.
119
+ const idField = input.treeData.idField;
120
+ if (idField && !known.has(idField)) {
121
+ messages.push(`[svgrid] \`treeData.idField\` is "${idField}", which is not on your row ` +
122
+ `data, so parent lookups cannot match and the tree stays flat.`);
123
+ }
124
+ }
125
+ // ---- 7. Pinning that column virtualization will hide ----------------------
126
+ // Documented incompatibility: the virtualizer recycles column DOM nodes, so
127
+ // sticky pinning cannot survive it. `columnVirtualization` defaults to ON,
128
+ // which means the natural way to write this silently does nothing.
129
+ const pinned = (input.initialColumnPinning?.left?.length ?? 0) +
130
+ (input.initialColumnPinning?.right?.length ?? 0);
131
+ if (pinned > 0 && input.columnVirtualization !== false) {
132
+ messages.push('[svgrid] `initialColumnPinning` is set while column virtualization is on ' +
133
+ '(its default), so the pinned columns will not stick - the virtualizer ' +
134
+ 'recycles column nodes. Add `columnVirtualization={false}`.');
135
+ }
136
+ // ---- 8. Server-mode contracts left half-wired -----------------------------
137
+ // Each of these makes the grid hand control to the consumer. Miss the other
138
+ // half and the feature looks broken rather than unconfigured.
139
+ if (input.externalPagination === true && input.rowCount === undefined) {
140
+ messages.push('[svgrid] `externalPagination` is on but `rowCount` is not set, so the pager ' +
141
+ 'cannot know how many pages exist. Pass the server total as `rowCount`.');
142
+ }
143
+ if (input.externalSort === true && !input.onSortingChange) {
144
+ messages.push('[svgrid] `externalSort` is on but there is no `onSortingChange` handler, so ' +
145
+ 'clicking a header changes nothing - the grid stopped sorting and nobody ' +
146
+ 'is listening. Add `onSortingChange` and re-fetch in your handler.');
147
+ }
148
+ if (input.externalFilter === true && !input.onFiltersChange) {
149
+ messages.push('[svgrid] `externalFilter` is on but there is no `onFiltersChange` handler, ' +
150
+ 'so filtering the grid changes nothing. Add `onFiltersChange` and re-fetch ' +
151
+ 'in your handler.');
152
+ }
153
+ return messages;
154
+ }
155
+ /** Closest key by edit distance, for a "did you mean" hint. Undefined if none is close. */
156
+ function nearest(target, candidates) {
157
+ let best;
158
+ let bestScore = Infinity;
159
+ for (const c of candidates) {
160
+ const d = distance(target.toLowerCase(), c.toLowerCase());
161
+ if (d < bestScore) {
162
+ bestScore = d;
163
+ best = c;
164
+ }
165
+ }
166
+ // Only suggest a genuinely near miss - a third of the length, at most 3 edits.
167
+ const limit = Math.min(3, Math.floor(target.length / 3) + 1);
168
+ return bestScore <= limit ? best : undefined;
169
+ }
170
+ /** Levenshtein distance, iterative single-row. Inputs here are identifier-length. */
171
+ function distance(a, b) {
172
+ if (a === b)
173
+ return 0;
174
+ const prev = new Array(b.length + 1);
175
+ for (let j = 0; j <= b.length; j++)
176
+ prev[j] = j;
177
+ for (let i = 1; i <= a.length; i++) {
178
+ let carry = prev[0];
179
+ prev[0] = i;
180
+ for (let j = 1; j <= b.length; j++) {
181
+ const next = Math.min(prev[j] + 1, prev[j - 1] + 1, carry + (a[i - 1] === b[j - 1] ? 0 : 1));
182
+ carry = prev[j];
183
+ prev[j] = next;
184
+ }
185
+ }
186
+ return prev[b.length];
187
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svgrid/grid",
3
- "version": "2.6.19",
3
+ "version": "2.6.21",
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",
@@ -138,5 +138,8 @@
138
138
  },
139
139
  "peerDependencies": {
140
140
  "svelte": "^5.0.0"
141
+ },
142
+ "dependencies": {
143
+ "esm-env": "^1.2.2"
141
144
  }
142
145
  }
@@ -32,6 +32,7 @@ import {
32
32
  resolveMaxDomHeight,
33
33
  } from "./virtualization/scroll-scaling";
34
34
  import { buildPreMeasureItems } from "./virtualization/virtualizer";
35
+ import { DEV } from "esm-env";
35
36
  import "./sv-grid-scrollbar";
36
37
  import {
37
38
  computeColumnStat,
@@ -61,6 +62,7 @@ import {
61
62
  import {
62
63
  createSummaries,
63
64
  } from "./summaries";
65
+
64
66
  import {
65
67
  createMenus,
66
68
  } from "./menus";
@@ -606,6 +608,17 @@ export function createSvGridController<
606
608
  (props.selectionMode ?? "both") === "both"),
607
609
  );
608
610
 
611
+ // The aggregate footer row. `summary` is the shortcut alias, so it wins over
612
+ // the fine-grained prop - the same precedence `selectable` uses.
613
+ //
614
+ // It starts OFF. It used to default ON, which meant a plain
615
+ // `<SvGrid {data} {columns} />` grew a totals row nobody asked for, and every
616
+ // caller that did not want one had to say so: 375 call sites in this repo
617
+ // passed `enableRowSummaries={false}` against 6 that opted in.
618
+ const rowSummariesEnabled = $derived(
619
+ props.summary ?? props.enableRowSummaries ?? false,
620
+ );
621
+
609
622
 
610
623
 
611
624
  // Internal source-of-truth for data and column defs. Seeded from props and
@@ -2583,7 +2596,7 @@ export function createSvGridController<
2583
2596
  */
2584
2597
  const eagerSummaries = $derived.by(() => {
2585
2598
  if (!summarize) return null;
2586
- if (!(props.enableRowSummaries ?? true)) return null;
2599
+ if (!rowSummariesEnabled) return null;
2587
2600
  const rows = allRows;
2588
2601
  const columns = allColumns;
2589
2602
  if (rows.length * columns.length > SUMMARY_DEFER_CELL_LIMIT) return null;
@@ -2607,7 +2620,7 @@ export function createSvGridController<
2607
2620
  void editedCellValues;
2608
2621
  const rows = allRows;
2609
2622
  const columns = allColumns;
2610
- if (!(props.enableRowSummaries ?? true)) {
2623
+ if (!rowSummariesEnabled) {
2611
2624
  deferredSummaries = {};
2612
2625
  return;
2613
2626
  }
@@ -3499,6 +3512,7 @@ export function createSvGridController<
3499
3512
  get showColumnFiltersEffective() { return showColumnFiltersEffective; },
3500
3513
  get showInlineColumnFilterEffective() { return showInlineColumnFilterEffective; },
3501
3514
  get showRowSelectionEffective() { return showRowSelectionEffective; },
3515
+ get rowSummariesEnabled() { return rowSummariesEnabled; },
3502
3516
  get enableCellSelectionEffective() { return enableCellSelectionEffective; },
3503
3517
  get flushScheduledScrollSync() { return flushScheduledScrollSync; },
3504
3518
  get scheduleScrollSync() { return scheduleScrollSync; },
@@ -3945,6 +3959,58 @@ export function createSvGridController<
3945
3959
  const { buildApi } = createGridApi<TFeatures, TData>(ctx);
3946
3960
  const { readCellRaw, writeCellRaw, applyFillPattern, clearSelectedCellValues, startFillDrag, onFillPointerMove, onFillPointerUp, toggleBooleanCell, copySelectionToClipboard, clearSelectedCells, cutSelectionToClipboard } = createClipboard(ctx);
3947
3961
 
3962
+ // Dev-time configuration checks. Silent misconfiguration was the grid's
3963
+ // biggest usability gap - a misspelled `field` rendered a column of blank
3964
+ // cells and printed nothing, and an inert `pageSize` was simply ignored.
3965
+ //
3966
+ // `validateGridConfig` is pure (see validate.ts) so it can be unit-tested;
3967
+ // this only decides when to run it, and prints each distinct message once.
3968
+ //
3969
+ // It is loaded with `import()` rather than a static import so the checks live
3970
+ // in a lazy chunk. The base bundle is size-budgeted and already sits at its
3971
+ // limit, so a static import cost ~1 KB gzip that a production app would carry
3972
+ // and never execute.
3973
+ //
3974
+ // `DEV` comes from `esm-env` because the alternatives do not work here:
3975
+ // `process.env.NODE_ENV` is undefined in Vite's browser runtime (the checks
3976
+ // silently never ran), and Vite's own flag makes svelte-package warn that it
3977
+ // only works for Vite consumers. esm-env resolves through export conditions,
3978
+ // so it is correct in dev and folds to `false` in a production build. It is
3979
+ // already a direct dependency of Svelte, so it adds nothing to an install.
3980
+ if (DEV) {
3981
+ const warnedConfig = new Set<string>();
3982
+ $effect(() => {
3983
+ // Read the reactive values synchronously so the effect tracks them; the
3984
+ // module load below is async and would otherwise register no dependencies.
3985
+ const snapshot = {
3986
+ data: internalData,
3987
+ columns: internalColumns,
3988
+ features: resolveEffectiveFeatures() as Record<string, unknown>,
3989
+ sortable: props.sortable,
3990
+ pageable: props.pageable,
3991
+ showPagination: props.showPagination,
3992
+ pageSize: props.pageSize,
3993
+ groupBy: props.groupBy,
3994
+ treeData: props.treeData,
3995
+ initialColumnPinning: props.initialColumnPinning,
3996
+ columnVirtualization: props.columnVirtualization,
3997
+ externalPagination: props.externalPagination,
3998
+ rowCount: props.rowCount,
3999
+ externalSort: props.externalSort,
4000
+ onSortingChange: props.onSortingChange,
4001
+ externalFilter: props.externalFilter,
4002
+ onFiltersChange: props.onFiltersChange,
4003
+ };
4004
+ void import("./validate").then(({ validateGridConfig }) => {
4005
+ for (const message of validateGridConfig(snapshot as never)) {
4006
+ if (warnedConfig.has(message)) continue;
4007
+ warnedConfig.add(message);
4008
+ console.warn(message);
4009
+ }
4010
+ });
4011
+ });
4012
+ }
4013
+
3948
4014
  // Aligned grids: register in the shared group on mount, and mirror column
3949
4015
  // resizes to peers whenever columnWidths changes. Horizontal-scroll mirroring
3950
4016
  // is driven from onBodyScroll (via ctx.broadcastAlignedScroll).
package/src/SvGrid.svelte CHANGED
@@ -184,6 +184,7 @@
184
184
  ctrl.showInlineColumnFilterEffective,
185
185
  );
186
186
  const showRowSelectionEffective = $derived(ctrl.showRowSelectionEffective);
187
+ const rowSummariesEnabled = $derived(ctrl.rowSummariesEnabled);
187
188
  const grid = $derived(ctrl.grid);
188
189
  const allColumns = $derived(ctrl.allColumns);
189
190
  const headerGroups = $derived(ctrl.headerGroups);
@@ -2519,7 +2520,7 @@
2519
2520
  {/each}
2520
2521
  </tbody>
2521
2522
  {/if}
2522
- {#if opt.enableRowSummaries ?? true}
2523
+ {#if rowSummariesEnabled}
2523
2524
  <!-- svelte-ignore a11y_no_redundant_roles -->
2524
2525
  <tfoot class="sv-grid-foot" role="rowgroup">
2525
2526
  <tr