@svgrid/grid 3.0.4 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +4 -4
  2. package/dist/SvGrid.controller.svelte.d.ts +12 -2
  3. package/dist/SvGrid.controller.svelte.js +192 -8
  4. package/dist/SvGrid.css +73 -0
  5. package/dist/SvGrid.svelte +128 -3
  6. package/dist/SvGrid.types.d.ts +356 -3
  7. package/dist/build-api.js +11 -6
  8. package/dist/cdn/{GridMenus-BL7ZgQvU.js → GridMenus-CID62rlI.js} +11 -11
  9. package/dist/cdn/{GridMenus-7kbpnnBW.js → GridMenus-DLL-qZeu.js} +11 -11
  10. package/dist/cdn/server-block-cache-CGoWz-87.js +237 -0
  11. package/dist/cdn/{src-BKhZ6eXd.js → src-Bklofsad.js} +8818 -8798
  12. package/dist/cdn/{src-V1uu8iE9.js → src-jcFMkBnN.js} +4282 -4262
  13. package/dist/cdn/svgrid.js +27 -26
  14. package/dist/cdn/svgrid.svelte-external.js +27 -26
  15. package/dist/cdn/validate-C7Rn_Bns.js +76 -0
  16. package/dist/core.js +36 -13
  17. package/dist/editing.js +10 -0
  18. package/dist/gantt-view.svelte.d.ts +24 -0
  19. package/dist/gantt-view.svelte.js +13 -0
  20. package/dist/grid-icons.d.ts +2 -2
  21. package/dist/grid-icons.js +2 -2
  22. package/dist/grid-messages.d.ts +6 -0
  23. package/dist/grid-messages.js +5 -0
  24. package/dist/index.d.ts +5 -4
  25. package/dist/index.js +3 -3
  26. package/dist/row-model.d.ts +133 -0
  27. package/dist/row-model.js +16 -0
  28. package/dist/selection.d.ts +2 -1
  29. package/dist/selection.js +30 -2
  30. package/dist/server-block-cache.d.ts +214 -0
  31. package/dist/server-block-cache.js +531 -0
  32. package/dist/server-data-source.d.ts +240 -6
  33. package/dist/server-data-source.js +220 -20
  34. package/dist/server.d.ts +13 -0
  35. package/dist/server.js +13 -0
  36. package/dist/validate.d.ts +4 -0
  37. package/dist/validate.js +21 -3
  38. package/package.json +6 -1
  39. package/src/SvGrid.controller.svelte.ts +191 -8
  40. package/src/SvGrid.css +73 -0
  41. package/src/SvGrid.svelte +128 -3
  42. package/src/SvGrid.types.ts +376 -3
  43. package/src/build-api.ts +11 -4
  44. package/src/core.rowmodel-cache.test.ts +51 -0
  45. package/src/core.ts +33 -12
  46. package/src/editing.ts +8 -0
  47. package/src/gantt-stub.test.svelte +38 -0
  48. package/src/gantt-view.svelte.ts +35 -0
  49. package/src/grid-icons.ts +2 -2
  50. package/src/grid-messages.ts +12 -0
  51. package/src/icon-seam.test.ts +2 -4
  52. package/src/index.ts +36 -10
  53. package/src/row-model.ts +146 -0
  54. package/src/selection.test.ts +3 -0
  55. package/src/selection.ts +32 -2
  56. package/src/server-block-cache.test.ts +645 -0
  57. package/src/server-block-cache.ts +677 -0
  58. package/src/server-data-source.infinite.test.ts +343 -0
  59. package/src/server-data-source.ts +450 -27
  60. package/src/server.ts +47 -0
  61. package/src/svgrid.gantt-seam.test.ts +199 -0
  62. package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
  63. package/src/svgrid.row-model-seam.svelte.test.ts +289 -0
  64. package/src/svgrid.upsell-license.test.ts +7 -4
  65. package/src/validate.test.ts +29 -0
  66. package/src/validate.ts +28 -3
  67. package/dist/SvGroupCell.svelte +0 -141
  68. package/dist/SvGroupCell.svelte.d.ts +0 -49
  69. package/dist/SvRowGroupPanel.svelte +0 -186
  70. package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
  71. package/dist/cdn/validate-_CDJzgIo.js +0 -75
  72. package/dist/server-group-model.d.ts +0 -98
  73. package/dist/server-group-model.js +0 -263
  74. package/src/SvGroupCell.svelte +0 -141
  75. package/src/SvRowGroupPanel.svelte +0 -186
  76. package/src/server-group-model.test.ts +0 -294
  77. package/src/server-group-model.ts +0 -370
@@ -1,6 +1,7 @@
1
1
  // Type definitions extracted from SvGrid.svelte. These are compile-time
2
2
  // only - moving them out keeps the component's <script> focused on logic.
3
3
  import type { Snippet } from "svelte";
4
+ import type { GridRowModel } from "./row-model";
4
5
  import type {
5
6
  CellEditorType,
6
7
  ColumnDef,
@@ -1112,8 +1113,276 @@ export type SchedulerConfig<
1112
1113
  searchPlaceholder?: string;
1113
1114
  };
1114
1115
 
1116
+ /**
1117
+ * The Gantt axis presets: which unit is a tick and which is the coarser row
1118
+ * grouping them. `week` (day ticks under month majors) is the default.
1119
+ */
1120
+ export type GanttZoom = "day" | "week" | "month" | "quarter" | "year";
1121
+
1122
+ /**
1123
+ * The four classic dependency kinds. `FS` (finish-to-start) is the default:
1124
+ * the successor may not start before the predecessor finishes.
1125
+ */
1126
+ export type GanttDependencyType = "FS" | "SS" | "FF" | "SF";
1127
+
1128
+ /**
1129
+ * A link from a predecessor task (`from`) to a successor (`to`). Both are row
1130
+ * ids (the grid's `getRowId`). `lag` is in DAYS - negative for a lead, so
1131
+ * `{ type: 'FS', lag: -1 }` lets the successor start a day before the
1132
+ * predecessor finishes.
1133
+ */
1134
+ export type GanttDependency = {
1135
+ id: string;
1136
+ from: string;
1137
+ to: string;
1138
+ type?: GanttDependencyType;
1139
+ lag?: number;
1140
+ };
1141
+
1142
+ /** Fired when a task bar is dragged to new dates. {@link GanttConfig.onTaskMove}. */
1143
+ export type GanttTaskMoveEvent<TData extends RowData = RowData> = {
1144
+ row: TData;
1145
+ start: Date;
1146
+ end: Date;
1147
+ /**
1148
+ * The moved row's descendants, shifted by the same delta - present only when
1149
+ * a summary (parent) bar was dragged. Cascaded successors are NOT here; they
1150
+ * arrive through {@link GanttConfig.onDependenciesChange}.
1151
+ */
1152
+ subtree?: Array<{ row: TData; start: Date; end: Date }>;
1153
+ };
1154
+
1155
+ /** Fired when a task bar's edge is dragged. {@link GanttConfig.onTaskResize}. */
1156
+ export type GanttTaskResizeEvent<TData extends RowData = RowData> = {
1157
+ row: TData;
1158
+ start: Date;
1159
+ end: Date;
1160
+ /** Which edge the user dragged. */
1161
+ edge: "start" | "end";
1162
+ };
1163
+
1164
+ /** Fired when a bar's progress grip is dragged. {@link GanttConfig.onProgressChange}. */
1165
+ export type GanttProgressChangeEvent<TData extends RowData = RowData> = {
1166
+ row: TData;
1167
+ /** 0-100, in whole percent. */
1168
+ progress: number;
1169
+ };
1170
+
1171
+ /** Fired when the Gantt's detail drawer is saved. {@link GanttConfig.onTaskCommit}. */
1172
+ export type GanttTaskCommitEvent<TData extends RowData = RowData> = {
1173
+ row: TData;
1174
+ values: Partial<TData>;
1175
+ };
1176
+
1177
+ /**
1178
+ * The Gantt's built-in task drawer. Same shape as the scheduler's - a field
1179
+ * list, a title and a width - so one drawer config type covers both views.
1180
+ */
1181
+ export type GanttDrawerConfig<TData extends RowData = RowData> =
1182
+ SchedulerDrawerConfig<TData>;
1183
+
1184
+ /**
1185
+ * Turns the grid into a Gantt chart. Set `gantt` and the grid renders its rows
1186
+ * as a task table beside a time chart: one bar per row positioned by start /
1187
+ * end, nested into a work-breakdown tree by `parentField`, with dependency
1188
+ * arrows between linked tasks.
1189
+ *
1190
+ * Like the Kanban board and the scheduler it is a pure *view of the grid*: it
1191
+ * renders the grid's filtered + sorted rows and writes back only through
1192
+ * callbacks, never mutating your data. Dragging a bar fires
1193
+ * {@link GanttConfig.onTaskMove} where you reassign the dates on your own rows.
1194
+ *
1195
+ * Dates are local calendar days and `end` is EXCLUSIVE, except that a date-only
1196
+ * string (`'2026-09-14'`, no time part) is read as the end OF that day - the
1197
+ * inclusive convention planning tools use. So
1198
+ * `{ start: '2026-09-14', end: '2026-09-16' }` draws a three-day bar.
1199
+ */
1200
+ export type GanttConfig<
1201
+ TFeatures extends TableFeatures = TableFeatures,
1202
+ TData extends RowData = RowData,
1203
+ > = {
1204
+ /** Field holding each task's start (`Date` | epoch-ms | ISO string). Required. */
1205
+ startField: keyof TData & string;
1206
+ /** Field holding the end. Omit to use `durationField`, else the task is a milestone. */
1207
+ endField?: keyof TData & string;
1208
+ /** Field holding the length in WORKING days, used when `endField` is absent. */
1209
+ durationField?: keyof TData & string;
1210
+ /** Field for the task name. Defaults to the first column's field. */
1211
+ titleField?: keyof TData & string;
1212
+ /** Field holding percent complete (0-100). Drives the bar's inner fill. */
1213
+ progressField?: keyof TData & string;
1214
+ /**
1215
+ * Field holding each row's PARENT task id, nesting the flat rows into a
1216
+ * work-breakdown tree. Parents render a rolled-up summary bar and a collapse
1217
+ * chevron. Rows whose parent is missing become roots rather than vanishing.
1218
+ *
1219
+ * This is the Gantt's own tree - do NOT also set the grid's `treeData` prop.
1220
+ * `treeData` hides collapsed children from the view entirely, which would
1221
+ * drop them out of their phase's summary bar.
1222
+ */
1223
+ parentField?: keyof TData & string;
1224
+ /**
1225
+ * Boolean field marking a task as a milestone (a diamond, no length). A task
1226
+ * whose end equals its start renders as one regardless.
1227
+ */
1228
+ milestoneField?: keyof TData & string;
1229
+ /** Field holding a per-task accent color (any CSS color). Else `color`. */
1230
+ colorField?: keyof TData & string;
1231
+ /** Fallback accent color for every bar. */
1232
+ color?: string;
1233
+
1234
+ // --- dependencies ---------------------------------------------------------
1235
+ /**
1236
+ * Predecessor -> successor links, drawn as arrows between bars. `from` / `to`
1237
+ * are row ids (your `getRowId`). See {@link GanttDependency}.
1238
+ */
1239
+ dependencies?: ReadonlyArray<GanttDependency>;
1240
+ /**
1241
+ * Per-row field holding that row's links (as `GanttDependency[]` or a plain
1242
+ * list of successor ids). An alternative to the flat `dependencies` array.
1243
+ */
1244
+ dependencyField?: keyof TData & string;
1245
+ /**
1246
+ * Cascade successors forward on move / resize so every link stays legal,
1247
+ * preserving each task's duration. Defaults to `true` when any dependency is
1248
+ * present. Cascading never pulls a task earlier.
1249
+ */
1250
+ autoReschedule?: boolean;
1251
+ /**
1252
+ * Schedule in working time (see `nonWorkingDays` / `holidays`): a moved
1253
+ * or cascaded task lands on a working day and keeps its WORKING length, so
1254
+ * a five-day task dragged over a weekend stays five days of work, and the
1255
+ * critical path measures slack in working days. `false` schedules in
1256
+ * calendar time. Default `true`.
1257
+ */
1258
+ respectWorkingTime?: boolean;
1259
+ /** Fired with the cascaded shifts after a move / resize (never mutates rows). */
1260
+ onDependenciesChange?: (
1261
+ moves: Array<{ id: string; start: Date; end: Date }>,
1262
+ ) => void;
1263
+ /** Fired when the user draws a new link (drag from one bar's edge to another). */
1264
+ onDependencyAdd?: (dep: GanttDependency) => void;
1265
+ /** Fired when the user removes a link (arrow context menu). */
1266
+ onDependencyRemove?: (id: string) => void;
1267
+
1268
+ // --- axis -----------------------------------------------------------------
1269
+ /** The axis preset the chart opens on. Default `'week'`. */
1270
+ zoom?: GanttZoom;
1271
+ /**
1272
+ * The presets the toolbar's zoom stepper offers. Default all five; a single
1273
+ * entry hides the stepper.
1274
+ */
1275
+ zoomLevels?: ReadonlyArray<GanttZoom>;
1276
+ /** Fired when the zoom preset changes (stepper or Ctrl+wheel). */
1277
+ onZoomChange?: (zoom: GanttZoom) => void;
1278
+ /** First day of the week, 0-6 (0 = Sunday). Default 0. */
1279
+ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
1280
+ /**
1281
+ * Weekday numbers (0 = Sun ... 6 = Sat) that are non-working: shaded in the
1282
+ * chart and skipped by duration + cascade arithmetic. Default `[0, 6]`.
1283
+ */
1284
+ nonWorkingDays?: ReadonlyArray<number>;
1285
+ /** Specific dates that are non-working (holidays, closures). */
1286
+ holidays?: ReadonlyArray<Date | number | string>;
1287
+ /** Shade the non-working columns. Default `true`. */
1288
+ showNonWorking?: boolean;
1289
+ /** Draw the dashed "today" line across the chart. Default `true`. */
1290
+ todayLine?: boolean;
1291
+ /** Earliest date the axis shows and a drag may reach. */
1292
+ minDate?: Date | number | string;
1293
+ /** Latest date the axis shows and a drag may reach. */
1294
+ maxDate?: Date | number | string;
1295
+ /** Calendar days of slack around the first start / last end. Default 7. */
1296
+ rangePaddingDays?: number;
1297
+
1298
+ // --- layout ---------------------------------------------------------------
1299
+ /**
1300
+ * Column ids shown in the task table, in order. Defaults to every leaf
1301
+ * column. Two built-ins need no column definition: `'__duration'` (working
1302
+ * days) and `'__progress'` (a small bar).
1303
+ */
1304
+ tableColumns?: ReadonlyArray<string>;
1305
+ /** Width (px) of the task table pane. Default 360; a splitter resizes it. */
1306
+ tableWidth?: number;
1307
+ /** Height (px) of one task row. Default 32. */
1308
+ rowHeight?: number;
1309
+ /** Parents draw a rolled-up summary bar spanning their children. Default `true`. */
1310
+ summaryBars?: boolean;
1311
+ /**
1312
+ * Where a bar's label sits: `'inside'` (default, falling back to the right
1313
+ * when the bar is too narrow), always `'right'`, or `'none'`.
1314
+ */
1315
+ labelPosition?: "inside" | "right" | "none";
1316
+
1317
+ // --- work-breakdown collapse ------------------------------------------------
1318
+ /**
1319
+ * Controlled collapse: the ids of the parent rows whose children are hidden.
1320
+ * Omit to let the Gantt own its own collapse state.
1321
+ */
1322
+ collapsed?: ReadonlyArray<string>;
1323
+ /** Fired when a chevron (or Expand / Collapse all) changes the collapsed set. */
1324
+ onCollapseChange?: (collapsed: string[]) => void;
1325
+
1326
+ // --- editing ----------------------------------------------------------------
1327
+ /**
1328
+ * Enable drag-to-move, edge resize, the progress grip and link drawing.
1329
+ * Without it the chart is read-only.
1330
+ */
1331
+ editable?: boolean;
1332
+ /**
1333
+ * Enable undo / redo of move, resize and progress edits with `Ctrl/Cmd+Z` and
1334
+ * `Ctrl/Cmd+Shift+Z` (or `Ctrl+Y`). The callbacks re-fire with the reversed
1335
+ * values, so your data follows.
1336
+ */
1337
+ history?: boolean;
1338
+ /** Fired when a bar is dragged to new dates. */
1339
+ onTaskMove?: (event: GanttTaskMoveEvent<TData>) => void;
1340
+ /** Fired when a bar's edge is dragged. */
1341
+ onTaskResize?: (event: GanttTaskResizeEvent<TData>) => void;
1342
+ /** Fired when the progress grip is dragged. */
1343
+ onProgressChange?: (event: GanttProgressChangeEvent<TData>) => void;
1344
+ /** Fired when empty chart space is double-clicked - create a task there. */
1345
+ onTaskAdd?: (start: Date, end: Date, parentId?: string) => void;
1346
+ /**
1347
+ * Fired from the bar's Delete action. Setting this shows Delete in the
1348
+ * drawer and the context menu. Remove the row from your data in the handler.
1349
+ */
1350
+ onTaskDelete?: (row: TData) => void;
1351
+
1352
+ // --- chrome -----------------------------------------------------------------
1353
+ /**
1354
+ * Custom body for a TASK bar. Receives the row. Omit for the built-in label.
1355
+ * A phase's summary bar is a thin spine with no room for a body, so it keeps
1356
+ * its plain label; a milestone has no body at all.
1357
+ */
1358
+ task?: Snippet<[TData]>;
1359
+ /**
1360
+ * Hover tooltip for a bar. A `Snippet<[TData]>` for custom content, or `true`
1361
+ * for the built-in one (title, dates, duration, percent). Omit to disable.
1362
+ */
1363
+ tooltip?: boolean | Snippet<[TData]>;
1364
+ /** Delay (ms) before the hover tooltip opens. Default 400. */
1365
+ tooltipDelay?: number;
1366
+ /** Built-in task drawer: `true` for all fields, or a config object. */
1367
+ drawer?: boolean | GanttDrawerConfig<TData>;
1368
+ /** Fired when the drawer is saved. */
1369
+ onTaskCommit?: (event: GanttTaskCommitEvent<TData>) => void;
1370
+ /** Right-click menu for a bar. Return items or `undefined` to suppress. */
1371
+ taskMenu?: (row: TData) => MenuItem[] | undefined;
1372
+
1373
+ /** Show the search box (binds to the grid's global filter). Default `true`. */
1374
+ searchable?: boolean;
1375
+ searchPlaceholder?: string;
1376
+ };
1377
+
1115
1378
  export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData> = {
1116
- data: ReadonlyArray<TData>;
1379
+ /**
1380
+ * The rows to render.
1381
+ *
1382
+ * Optional only because `rowModel` can supply them instead; a grid with
1383
+ * neither renders empty. When both are present `data` wins.
1384
+ */
1385
+ data?: ReadonlyArray<TData>;
1117
1386
  columns: Array<ColumnDef<TFeatures, TData>>;
1118
1387
  /**
1119
1388
  * Kanban board mode. When set, the grid renders its rows as cards in
@@ -1127,6 +1396,15 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
1127
1396
  * {@link SchedulerConfig}.
1128
1397
  */
1129
1398
  scheduler?: SchedulerConfig<TFeatures, TData>;
1399
+ /**
1400
+ * Gantt mode. When set, the grid renders its rows as a task table beside a
1401
+ * time chart: one bar per row by start / end, a work-breakdown tree from
1402
+ * `parentField`, and dependency arrows. See {@link GanttConfig}.
1403
+ *
1404
+ * A view of the grid like the board and scheduler; the renderer ships in
1405
+ * `@svgrid/enterprise` (call `enableGanttView()`).
1406
+ */
1407
+ gantt?: GanttConfig<TFeatures, TData>;
1130
1408
  /**
1131
1409
  * Chart view. When set, the grid renders its FILTERED + SORTED rows as a chart
1132
1410
  * instead of a table (search / filters / sort flow through). Unlike board and
@@ -2099,6 +2377,99 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
2099
2377
  scrollHeight: number;
2100
2378
  clientHeight: number;
2101
2379
  }) => void;
2380
+ /**
2381
+ * Fires when the range of rows on screen changes, with the first and last
2382
+ * row INDEX (not pixels). Coalesced to one call per frame, so it is cheap
2383
+ * to wire to something that fetches.
2384
+ *
2385
+ * This is the hook a block-loading data source needs and
2386
+ * `onScrollBottomReached` cannot give it: "the user is looking at rows
2387
+ * 4,000-4,020" answers which block to fetch and which to keep, while
2388
+ * "they hit the bottom" only ever means "append more". Both are free; use
2389
+ * this one with `createServerDataSource({ mode: 'infinite' })`, or let
2390
+ * `rowModel` wire it for you.
2391
+ *
2392
+ * Reports `0, data.length - 1` when virtualization is off, since every row
2393
+ * really is rendered.
2394
+ */
2395
+ onVisibleRangeChange?: (range: {
2396
+ startIndex: number;
2397
+ endIndex: number;
2398
+ }) => void;
2399
+ /**
2400
+ * Marks a row as one whose data has not arrived: `"loading"` draws a
2401
+ * shimmer in every cell, `"failed"` draws a full-width "could not load"
2402
+ * row with a Retry button, and `null` (the default for every row) renders
2403
+ * normally.
2404
+ *
2405
+ * Placeholder rows are inert - not selectable, not editable, and skipped
2406
+ * by cell navigation - because there is nothing there to act on yet.
2407
+ *
2408
+ * `createServerDataSource` in `infinite` mode fills the gaps with rows this
2409
+ * recognises, so the usual wiring is `rowPlaceholder={rowPlaceholderState}`
2410
+ * (or nothing at all, via `rowModel`).
2411
+ */
2412
+ rowPlaceholder?: (row: TData, rowIndex: number) => "loading" | "failed" | null;
2413
+ /** Called by the Retry button on a `"failed"` placeholder row. */
2414
+ onRetryRow?: (row: TData, rowIndex: number) => void;
2415
+ /**
2416
+ * Hand selection over to an external model.
2417
+ *
2418
+ * The grid normally tracks selection as a record of the row ids it has
2419
+ * seen, which is right until the rows it has seen are a window onto a
2420
+ * million on a server: "select all" then means 20 ticked checkboxes rather
2421
+ * than a million, and the header checkbox cannot honestly say `all`. A
2422
+ * model that stores the RULE ("everything except these three") can answer
2423
+ * both, so when this is set the header checkbox, the row checkboxes and
2424
+ * `api.selectAllRows()` all route through it instead.
2425
+ *
2426
+ * `@svgrid/enterprise` ships one for the server-side row model; this is the
2427
+ * seam it plugs into.
2428
+ */
2429
+ /**
2430
+ * Drive the grid from a row model instead of wiring a dozen props.
2431
+ *
2432
+ * `createServerDataSource` returns one, and so does the Enterprise
2433
+ * server-side row model, so server-backed grids become:
2434
+ *
2435
+ * ```svelte
2436
+ * <SvGrid rowModel={ctl} {columns} />
2437
+ * ```
2438
+ *
2439
+ * The model supplies `data`, `loading`, `getRowId`, the external sort
2440
+ * and filter wiring, the visible range, placeholder rows, group
2441
+ * accessors, selection and paging - each one only if it implements that
2442
+ * part. A prop written explicitly on the grid always wins, so you can
2443
+ * adopt it and still override one piece.
2444
+ */
2445
+ rowModel?: GridRowModel<TData>;
2446
+ /**
2447
+ * Columns that REPLACE `columns` while set. The server-side row model
2448
+ * supplies them in pivot mode - one column per pivoted value, grouped
2449
+ * under a header per pivot key - and clears them when pivot mode ends,
2450
+ * so `columns` stays the app's own list. Rarely set by hand.
2451
+ */
2452
+ pivotResultColumns?: Array<ColumnDef<TFeatures, TData>> | null;
2453
+ rowSelectionModel?: {
2454
+ isSelected: (rowId: string, row: TData) => boolean;
2455
+ /** Whether the header checkbox shows empty, indeterminate, or ticked. */
2456
+ headerState: () => "none" | "some" | "all";
2457
+ toggle: (rowId: string, row: TData, next: boolean) => void;
2458
+ /** The header checkbox: select or clear everything, loaded or not. */
2459
+ toggleAll: (next: boolean) => void;
2460
+ /**
2461
+ * How many rows the rule selects, counting the ones the grid never
2462
+ * loaded; `null` when it cannot say. The selection bar shows this
2463
+ * instead of counting ticked rows on screen.
2464
+ */
2465
+ selectedCount?: () => number | null;
2466
+ /**
2467
+ * Apply one patch to every selected row, loaded or not. When present,
2468
+ * the bulk-edit drawer sends its edits here instead of writing the
2469
+ * loaded cells. Resolves with how many rows changed.
2470
+ */
2471
+ bulkUpdate?: (patch: Record<string, unknown>) => Promise<number>;
2472
+ };
2102
2473
  /**
2103
2474
  * Marks a row as an expandable "detail row". When this returns true the
2104
2475
  * grid renders that row as a SINGLE full-width cell (colspan across every
@@ -2119,8 +2490,10 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
2119
2490
  * Server-side group / tree keyboard + accessibility, built into the grid. When
2120
2491
  * set, the grid uses the treegrid role and marks matching rows with
2121
2492
  * `aria-level` / `aria-expanded`, and ArrowRight / ArrowLeft expand / collapse
2122
- * the focused group row (no app-level key handling). Pair with `serverGroupRows`
2123
- * + `SvGroupCell` for the visual expander. Every accessor receives the row data.
2493
+ * the focused group row (no app-level key handling). Pair with
2494
+ * `serverGroupRows` + `SvGroupCell` from `@svgrid/enterprise` for the visual
2495
+ * expander, or drive it from your own tree state. Every accessor receives the
2496
+ * row data.
2124
2497
  */
2125
2498
  serverGroup?: {
2126
2499
  /** Whether a row is an expandable group / branch. */
package/src/build-api.ts CHANGED
@@ -735,9 +735,10 @@ export function createGridApi<
735
735
  });
736
736
  },
737
737
  selectAllRows() {
738
- const next: Record<string, boolean> = {};
739
- for (const row of ctx.allRows) if (!isGroupRow(row)) next[row.id] = true;
740
- ctx.grid.setRowSelection(() => next);
738
+ // One implementation of "select everything", shared with the header
739
+ // checkbox, so it knows about an external selection model and about
740
+ // rows whose data has not arrived.
741
+ ctx.setSelectAllRows(true);
741
742
  },
742
743
  toggleRowSelected(id) {
743
744
  ctx.toggleRowSelectionById(id);
@@ -745,7 +746,13 @@ export function createGridApi<
745
746
  // ---- Pagination
746
747
  getPageInfo() {
747
748
  const { pageIndex, pageSize } = ctx.paginationState;
748
- const total = ctx.allRowsBeforePagination.length;
749
+ // Under external pagination the grid holds ONE page, so counting the
750
+ // rows in hand would report a 95-row table as a 20-row one. The
751
+ // consumer tells us the real total through rowCount, which is what
752
+ // the footer has always shown.
753
+ const total = ctx.externalPaginationEnabled
754
+ ? (ctx.props.rowCount ?? ctx.allRowsBeforePagination.length)
755
+ : ctx.allRowsBeforePagination.length;
749
756
  const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
750
757
  return { pageIndex, pageSize, pageCount, total };
751
758
  },
@@ -119,3 +119,54 @@ describe('row-model cache: selection', () => {
119
119
  expect(target.getIsSelected()).toBe(false)
120
120
  })
121
121
  })
122
+
123
+ describe('base rows: reuse across a data swap', () => {
124
+ // The core reads `options.data` and `options.columns` off the object it was
125
+ // given (the component hands it a reactive one), so a swap is a write there.
126
+ const grid = () => {
127
+ const options = {
128
+ _features: tableFeatures({ rowSelectionFeature }),
129
+ _rowModels: { coreRowModel: createCoreRowModel<Row>() },
130
+ columns: COLUMNS as unknown as Array<ColumnDef<ReturnType<typeof tableFeatures>, Row>>,
131
+ data: makeRows(50),
132
+ getRowId: (r: Row) => String(r.id),
133
+ }
134
+ return { g: createSvGridCore<ReturnType<typeof tableFeatures>, Row>(options), options }
135
+ }
136
+
137
+ it('keeps the row object for a data object that stayed at its index, and rebuilds the rest', () => {
138
+ const { g, options } = grid()
139
+ const before = g.getRowModel().rows
140
+ const data = options.data.slice()
141
+ // A block landing: forty of fifty entries are the same objects, ten are new.
142
+ for (let i = 20; i < 30; i += 1) data[i] = { ...data[i]!, name: 'fresh' }
143
+ options.data = data
144
+ const after = g.getRowModel().rows
145
+ expect(after).toHaveLength(50)
146
+ for (let i = 0; i < 50; i += 1) {
147
+ if (i >= 20 && i < 30) expect(after[i]).not.toBe(before[i])
148
+ else expect(after[i]).toBe(before[i])
149
+ }
150
+ expect(after[25]!.getCellValueByColumnId('name')).toBe('fresh')
151
+ })
152
+
153
+ it('does not serve a memoised value after the object was changed in place', () => {
154
+ const { g, options } = grid()
155
+ const rows = g.getRowModel().rows
156
+ expect(rows[3]!.getCellValueByColumnId('score')).toBe((3 * 31) % 1000)
157
+ options.data[3]!.score = 4242
158
+ options.data = options.data.slice()
159
+ const again = g.getRowModel().rows
160
+ expect(again[3]).toBe(rows[3])
161
+ expect(again[3]!.getCellValueByColumnId('score')).toBe(4242)
162
+ })
163
+
164
+ it('rebuilds every row when the columns change', () => {
165
+ const { g, options } = grid()
166
+ const before = g.getRowModel().rows
167
+ options.columns = [...COLUMNS, { field: 'extra' }] as unknown as typeof options.columns
168
+ const after = g.getRowModel().rows
169
+ expect(after[0]).not.toBe(before[0])
170
+ expect(after[0]!.getAllCells()).toHaveLength(4)
171
+ })
172
+ })
package/src/core.ts CHANGED
@@ -1545,6 +1545,7 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
1545
1545
  let cachedBaseRowsInput: ReadonlyArray<TData> | null = null
1546
1546
  let cachedBaseRowsColumns: Array<Column<TData>> | null = null
1547
1547
  let cachedBaseRows: Array<Row<TData>> = []
1548
+ let cachedRowCtx: BaseRowCtx<TData> | null = null
1548
1549
  let cachedRowModel: RowModel<TData> | null = null
1549
1550
  let cachedRowModelBaseRows: Array<Row<TData>> | null = null
1550
1551
  let cachedPipeline = options._rowModels
@@ -1754,22 +1755,35 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
1754
1755
  getRowModel() {
1755
1756
  const columns = grid.getAllColumns()
1756
1757
  if (cachedBaseRowsInput !== options.data || cachedBaseRowsColumns !== columns) {
1758
+ // Same columns as last time: the shared context still describes them,
1759
+ // and a row whose data object sits at the same index can keep its
1760
+ // row object. A row model that streams blocks hands the grid a new
1761
+ // array of 60k entries per block where 100 changed; building 60k
1762
+ // fresh row objects each time was most of the cost of a block
1763
+ // landing. The reused row drops its memoised values and cells, since
1764
+ // an app may have changed the object in place before passing a new
1765
+ // array - that is the case the old rebuild covered by accident.
1766
+ const previous = cachedBaseRowsColumns === columns && cachedRowCtx ? cachedBaseRows : null
1757
1767
  cachedBaseRowsInput = options.data
1758
1768
  cachedBaseRowsColumns = columns
1759
1769
  // O(1) column-id → index lookup so getCellValueByColumnId doesn't do
1760
1770
  // a linear `findIndex` on every cell read (was O(rows × cells × cols)).
1761
- const columnIndexById = new Map<string, number>()
1762
- for (let i = 0; i < columns.length; i++) columnIndexById.set(columns[i]!.id, i)
1763
- const columnCount = columns.length
1764
-
1765
- // One shared context for every row in this table, so a row carries a
1766
- // pointer rather than a closure scope. See BASE_ROW_METHODS.
1767
- const rowCtx: BaseRowCtx<TData> = {
1768
- grid: grid as SvGrid<TData>,
1769
- store,
1770
- columns,
1771
- columnCount,
1772
- columnIndexById,
1771
+ let rowCtx: BaseRowCtx<TData>
1772
+ if (previous && cachedRowCtx) {
1773
+ rowCtx = cachedRowCtx
1774
+ } else {
1775
+ const columnIndexById = new Map<string, number>()
1776
+ for (let i = 0; i < columns.length; i++) columnIndexById.set(columns[i]!.id, i)
1777
+ // One shared context for every row in this table, so a row carries a
1778
+ // pointer rather than a closure scope. See BASE_ROW_METHODS.
1779
+ rowCtx = {
1780
+ grid: grid as SvGrid<TData>,
1781
+ store,
1782
+ columns,
1783
+ columnCount: columns.length,
1784
+ columnIndexById,
1785
+ }
1786
+ cachedRowCtx = rowCtx
1773
1787
  }
1774
1788
 
1775
1789
  cachedBaseRows = new Array(options.data.length)
@@ -1785,6 +1799,13 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
1785
1799
  }
1786
1800
  for (let index = 0; index < options.data.length; index++) {
1787
1801
  const original = options.data[index]!
1802
+ const kept = previous?.[index] as BaseRowState<TData> | undefined
1803
+ if (kept && kept.original === original) {
1804
+ kept[ROW_VALUES] = null
1805
+ kept[ROW_CELLS] = null
1806
+ cachedBaseRows[index] = kept
1807
+ continue
1808
+ }
1788
1809
  // `_values` and `_cells` stay null until something reads them - a
1789
1810
  // 100k-row grid showing twenty rows must not materialise every row's
1790
1811
  // values or cell objects to paint.
package/src/editing.ts CHANGED
@@ -47,6 +47,14 @@ export function createEditing<
47
47
  function isCellEditable(column: Column<TData>, row?: Row<TData>): boolean {
48
48
  const editable = column.columnDef.editable;
49
49
  if (editable === false) return false;
50
+ if (row) {
51
+ // A server-side group row is a key with aggregates under the leaf
52
+ // columns, and a placeholder row has no data yet: neither takes an
53
+ // edit, whatever the column says.
54
+ const serverGroup = ctx.props.serverGroup;
55
+ if (serverGroup && row.original !== undefined && serverGroup.isGroup(row.original)) return false;
56
+ if (ctx.placeholderStateOf?.(row)) return false;
57
+ }
50
58
  if (typeof editable !== "function") return true;
51
59
  if (!row) return true;
52
60
  const cellCtx: CellContext<TData> = {
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ /**
3
+ * A stand-in for @svgrid/enterprise's SvGridGantt, for `svgrid.gantt-seam.test.ts`.
4
+ *
5
+ * The seam's contract is which props the grid hands the renderer, so this
6
+ * writes each of them into the DOM where a test can read them back. It is
7
+ * named `.test.svelte` so `tools/strip-dist-tests.mjs` keeps it out of the
8
+ * published tarball, and the grid's vitest only collects `*.test.ts`, so it
9
+ * is never mistaken for a suite of its own.
10
+ */
11
+ import type { ColumnDef, GanttConfig, RowData, TableFeatures } from './index'
12
+
13
+ let {
14
+ data = [],
15
+ columns = [],
16
+ gantt,
17
+ getRowId,
18
+ }: {
19
+ data: ReadonlyArray<RowData>
20
+ columns: Array<ColumnDef<TableFeatures, any>>
21
+ gantt: GanttConfig<TableFeatures, any>
22
+ getRowId?: (row: any, index: number) => string
23
+ } = $props()
24
+
25
+ const ids = $derived(
26
+ data.map((r, i) => (getRowId ? getRowId(r, i) : String(i))).join(','),
27
+ )
28
+ </script>
29
+
30
+ <div
31
+ class="gantt-stub"
32
+ data-ids={ids}
33
+ data-rows={data.length}
34
+ data-cols={columns.length}
35
+ data-start={gantt?.startField ?? ''}
36
+ data-parent={gantt?.parentField ?? ''}
37
+ data-has-get-row-id={typeof getRowId === 'function'}
38
+ ></div>
@@ -0,0 +1,35 @@
1
+ /**
2
+ * gantt-view registry - the injection seam for the *Gantt view of the grid*.
3
+ * Setting the `gantt` prop on `<SvGrid>` switches the table for a task table
4
+ * beside a time chart (bars by start / end, a work-breakdown tree, dependency
5
+ * arrows), but the renderer itself ships in `@svgrid/enterprise` (a paid
6
+ * feature). This tiny reactive holder lets enterprise register that renderer at
7
+ * import time; the grid looks it up and mounts it, or shows an upsell
8
+ * placeholder when it is absent. Mirrors the `scheduler-view` / `board-view`
9
+ * pattern.
10
+ *
11
+ * ```ts
12
+ * // in @svgrid/enterprise, at module load:
13
+ * import { registerGanttView } from '@svgrid/grid'
14
+ * import SvGridGantt from './gantt/SvGridGantt.svelte'
15
+ * registerGanttView(SvGridGantt)
16
+ * ```
17
+ */
18
+ import type { Component } from 'svelte'
19
+
20
+ let renderer = $state<Component<any> | null>(null)
21
+
22
+ /** Register the component that renders `gantt` mode. Enterprise calls this. */
23
+ export function registerGanttView(component: Component<any>): void {
24
+ renderer = component
25
+ }
26
+
27
+ /** The registered Gantt renderer, or null when enterprise is not installed. */
28
+ export function getGanttView(): Component<any> | null {
29
+ return renderer
30
+ }
31
+
32
+ /** Whether a Gantt renderer has been registered. */
33
+ export function hasGanttView(): boolean {
34
+ return renderer != null
35
+ }
package/src/grid-icons.ts CHANGED
@@ -97,8 +97,8 @@ export type GridIcons = Partial<Record<GridIconName, Snippet>>
97
97
 
98
98
  /**
99
99
  * Icons whose built-in form is a character rather than an SVG path. Kept as
100
- * data so `<SvGrid>`, its footer and the standalone `SvRowGroupPanel` render
101
- * the same defaults instead of three copies drifting apart.
100
+ * data so `<SvGrid>`, its footer and the row-group panel in @svgrid/enterprise
101
+ * render the same defaults instead of three copies drifting apart.
102
102
  *
103
103
  * Each is the exact character that shipped inline before, so a grid that sets
104
104
  * no `icons` renders byte-for-byte what it always did.