gbs-add-block 1.2.6 → 1.2.8

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 (72) hide show
  1. package/README.md +3 -3
  2. package/index.cjs +10 -9
  3. package/package.json +1 -1
  4. package/source/components/button/index.tsx +2 -2
  5. package/source/components/card/index.tsx +1 -1
  6. package/source/components/checkbox/index.tsx +1 -1
  7. package/source/components/datagrid/context/GridContext.tsx +38 -17
  8. package/source/components/datagrid/createTemplate.tsx +2 -2
  9. package/source/components/datagrid/hooks/usePagination.ts +0 -8
  10. package/source/components/datagrid/hooks/useSearch.ts +3 -3
  11. package/source/components/datagrid/index.tsx +2 -1
  12. package/source/components/datagrid/layout/GridBody.tsx +7 -4
  13. package/source/components/datagrid/layout/GridPagination.tsx +3 -2
  14. package/source/components/datagrid/layout/GridToolbar.tsx +2 -1
  15. package/source/components/datagrid/type.ts +18 -2
  16. package/source/components/datagridbeta/README.md +232 -0
  17. package/source/components/datagridbeta/__tests__/core.test.ts +341 -0
  18. package/source/components/datagridbeta/__tests__/export.test.ts +70 -0
  19. package/source/components/datagridbeta/core/columnHelper.ts +21 -0
  20. package/source/components/datagridbeta/core/columns.ts +288 -0
  21. package/source/components/datagridbeta/core/filtering.ts +237 -0
  22. package/source/components/datagridbeta/core/grid.ts +616 -0
  23. package/source/components/datagridbeta/core/index.ts +14 -0
  24. package/source/components/datagridbeta/core/rows.ts +64 -0
  25. package/source/components/datagridbeta/core/sorting.ts +111 -0
  26. package/source/components/datagridbeta/core/state.ts +82 -0
  27. package/source/components/datagridbeta/core/store.ts +52 -0
  28. package/source/components/datagridbeta/core/types.ts +266 -0
  29. package/source/components/datagridbeta/core/values.ts +98 -0
  30. package/source/components/datagridbeta/core/virtual.ts +85 -0
  31. package/source/components/datagridbeta/export/csv.ts +32 -0
  32. package/source/components/datagridbeta/export/download.ts +12 -0
  33. package/source/components/datagridbeta/export/pdf.ts +61 -0
  34. package/source/components/datagridbeta/export/table.ts +73 -0
  35. package/source/components/datagridbeta/export/xlsx.ts +145 -0
  36. package/source/components/datagridbeta/export/zip.ts +87 -0
  37. package/source/components/datagridbeta/index.ts +7 -0
  38. package/source/components/datagridbeta/react/Cell.tsx +282 -0
  39. package/source/components/datagridbeta/react/ColumnMenu.tsx +135 -0
  40. package/source/components/datagridbeta/react/DataGrid.tsx +296 -0
  41. package/source/components/datagridbeta/react/FilterForm.tsx +142 -0
  42. package/source/components/datagridbeta/react/HeaderRow.tsx +306 -0
  43. package/source/components/datagridbeta/react/Pagination.tsx +107 -0
  44. package/source/components/datagridbeta/react/Popover.tsx +79 -0
  45. package/source/components/datagridbeta/react/Row.tsx +85 -0
  46. package/source/components/datagridbeta/react/Toolbar.tsx +266 -0
  47. package/source/components/datagridbeta/react/Viewport.tsx +167 -0
  48. package/source/components/datagridbeta/react/context.ts +57 -0
  49. package/source/components/datagridbeta/react/hooks.ts +80 -0
  50. package/source/components/datagridbeta/react/icons.tsx +72 -0
  51. package/source/components/datagridbeta/react/keyboard.ts +110 -0
  52. package/source/components/datagridbeta/react/locale.ts +128 -0
  53. package/source/components/datagridbeta/styles.css +717 -0
  54. package/source/components/datepicker/index.tsx +7 -7
  55. package/source/components/input/index.tsx +1 -1
  56. package/source/components/multiselect/PortalDropDown.tsx +1 -1
  57. package/source/components/multiselect/index.tsx +6 -6
  58. package/source/components/navbar/AuthSection.tsx +5 -5
  59. package/source/components/navbar/NavItemComponent.tsx +2 -2
  60. package/source/components/select/PortalDropDown.tsx +1 -1
  61. package/source/components/select/index.tsx +5 -5
  62. package/source/components/spinner/index.tsx +2 -2
  63. package/source/components/tabs/index.tsx +6 -6
  64. package/source/components/textarea/index.tsx +9 -9
  65. package/source/components/toast/ToastComponent.tsx +26 -25
  66. package/source/components/toast/ToastIcon.tsx +8 -8
  67. package/source/components/uploader/ProgressAnimation.tsx +2 -2
  68. package/source/components/uploader/UploadedFilePreview.tsx +1 -1
  69. package/source/components/uploader/index.tsx +1 -1
  70. package/source/components/uploader/uploaderIcon.tsx +1 -1
  71. package/source/globalStyle.ts +29 -16
  72. package/source/theme.ts +7 -7
@@ -0,0 +1,616 @@
1
+ import {
2
+ getLayoutVars,
3
+ moveColumn as moveColumnState,
4
+ pinColumn as pinColumnState,
5
+ } from "./columns";
6
+ import type { PageResult } from "./rows";
7
+ import { toggleSorting } from "./sorting";
8
+ import {
9
+ createInitialState,
10
+ mergeState,
11
+ pickQuery,
12
+ queryChanged,
13
+ resetsPage,
14
+ STATE_KEYS,
15
+ } from "./state";
16
+ import { createStore } from "./store";
17
+ import type {
18
+ ColumnFilter,
19
+ ColumnLayout,
20
+ Density,
21
+ ExportOptions,
22
+ ExportScope,
23
+ GridOptions,
24
+ GridRow,
25
+ GridState,
26
+ PdfExportOptions,
27
+ PinSide,
28
+ ResolvedColumn,
29
+ RowId,
30
+ SortItem,
31
+ } from "./types";
32
+ import { formatCellValue, type Formatters } from "./values";
33
+ import type { Viewport } from "./virtual";
34
+
35
+ export interface CellPosition {
36
+ /** -1 is the header row. */
37
+ rowIndex: number;
38
+ /** Index into `layout.columns` (visual order). */
39
+ colIndex: number;
40
+ }
41
+
42
+ export interface UiState {
43
+ activeCell: CellPosition | null;
44
+ editing: { rowId: RowId; columnId: string } | null;
45
+ /** Values shown while an async `onCellEdit` is in flight. */
46
+ pending: Record<RowId, Record<string, unknown>>;
47
+ /** Errors from rejected async edits. */
48
+ errors: Record<RowId, Record<string, string>>;
49
+ }
50
+
51
+ export interface GridSnapshot {
52
+ /** Internal (uncontrolled) state. Controlled props are merged in by the React layer. */
53
+ state: GridState;
54
+ ui: UiState;
55
+ }
56
+
57
+ /** Everything derived during render, handed to the engine after each commit. */
58
+ export interface GridModel<T> {
59
+ columns: ResolvedColumn<T>[];
60
+ coreRows: GridRow<T>[];
61
+ /** Filtered and sorted rows (client mode); the current data in server mode. */
62
+ sortedRows: GridRow<T>[];
63
+ page: PageResult<T>;
64
+ layout: ColumnLayout<T>;
65
+ rowHeight: number;
66
+ headerHeight: number;
67
+ formatters: Formatters;
68
+ }
69
+
70
+ export type MoveDirection = "up" | "down" | "left" | "right";
71
+
72
+ /** Imperative API, available through the grid's `ref`. */
73
+ export interface GridApi<T> {
74
+ getState(): GridState;
75
+ setState(updater: (prev: GridState) => GridState): void;
76
+
77
+ toggleSort(columnId: string, multi?: boolean): void;
78
+ setSorting(sorting: SortItem[]): void;
79
+ setFilter(columnId: string, filter: Omit<ColumnFilter, "columnId"> | null): void;
80
+ clearFilters(): void;
81
+ setGlobalFilter(value: string): void;
82
+ setPageIndex(pageIndex: number): void;
83
+ setPageSize(pageSize: number): void;
84
+ setDensity(density: Density): void;
85
+
86
+ isRowSelectable(row: T): boolean;
87
+ toggleRowSelected(rowId: RowId, options?: { value?: boolean; range?: boolean }): void;
88
+ /** Selects (or deselects) every row matching the current filters. */
89
+ toggleAllRowsSelected(value: boolean): void;
90
+ clearSelection(): void;
91
+ getSelectedRowIds(): RowId[];
92
+ getSelectedRows(): T[];
93
+
94
+ setColumnVisibility(columnId: string, visible: boolean): void;
95
+ setColumnWidth(columnId: string, width: number | null): void;
96
+ pinColumn(columnId: string, side: PinSide | false): void;
97
+ moveColumn(columnId: string, targetId: string, placement: "before" | "after"): void;
98
+ resetColumns(): void;
99
+
100
+ scrollToRow(rowIndex: number): void;
101
+ focusCell(rowIndex: number, columnId: string): void;
102
+ startEditing(rowId: RowId, columnId: string): void;
103
+ cancelEditing(): void;
104
+
105
+ getRows(scope?: ExportScope): T[];
106
+ exportCsv(options?: ExportOptions<T>): Promise<void>;
107
+ exportExcel(options?: ExportOptions<T>): Promise<void>;
108
+ exportPdf(options?: PdfExportOptions<T>): Promise<void>;
109
+ /** Copies selected rows as TSV, or the active cell when nothing is selected. */
110
+ copyToClipboard(): Promise<void>;
111
+ }
112
+
113
+ const EMPTY_UI: UiState = { activeCell: null, editing: null, pending: {}, errors: {} };
114
+
115
+ function setNested<V>(
116
+ record: Record<RowId, Record<string, V>>,
117
+ rowId: RowId,
118
+ columnId: string,
119
+ value: V | undefined,
120
+ ): Record<RowId, Record<string, V>> {
121
+ if (value === undefined && record[rowId]?.[columnId] === undefined) return record;
122
+ const inner = { ...record[rowId] };
123
+ if (value === undefined) delete inner[columnId];
124
+ else inner[columnId] = value;
125
+ const next = { ...record };
126
+ if (Object.keys(inner).length > 0) next[rowId] = inner;
127
+ else delete next[rowId];
128
+ return next;
129
+ }
130
+
131
+ const clamp = (value: number, min: number, max: number) =>
132
+ Math.min(max, Math.max(min, value));
133
+
134
+ export function createGridEngine<T>(initialOptions: GridOptions<T>) {
135
+ let options = initialOptions;
136
+ let model: GridModel<T> | null = null;
137
+ let viewportEl: HTMLElement | null = null;
138
+ let focusRequested = false;
139
+ let anchorRowId: RowId | null = null;
140
+ let draggingColumnId: string | null = null;
141
+ let rowLookup: { rows: GridRow<T>[]; map: Map<RowId, GridRow<T>> } | null = null;
142
+
143
+ const store = createStore<GridSnapshot>({
144
+ state: createInitialState(options),
145
+ ui: EMPTY_UI,
146
+ });
147
+ const viewport = createStore<Viewport>({ scrollTop: 0, scrollLeft: 0, width: 0, height: 0 });
148
+
149
+ const getState = () => mergeState(store.getSnapshot().state, options.state);
150
+
151
+ const setUi = (updater: (ui: UiState) => UiState) =>
152
+ store.setState((snapshot) => {
153
+ const ui = updater(snapshot.ui);
154
+ return ui === snapshot.ui ? snapshot : { ...snapshot, ui };
155
+ });
156
+
157
+ function setState(updater: (prev: GridState) => GridState) {
158
+ const prev = getState();
159
+ let next = updater(prev);
160
+ if (next === prev) return;
161
+
162
+ if (
163
+ resetsPage(prev, next) &&
164
+ next.pagination === prev.pagination &&
165
+ prev.pagination.pageIndex !== 0
166
+ ) {
167
+ next = { ...next, pagination: { ...next.pagination, pageIndex: 0 } };
168
+ }
169
+
170
+ const controlled = options.state ?? {};
171
+ store.setState((snapshot) => {
172
+ let internal = snapshot.state;
173
+ for (const key of STATE_KEYS) {
174
+ if (next[key] !== prev[key] && controlled[key] === undefined) {
175
+ if (internal === snapshot.state) internal = { ...internal };
176
+ (internal as unknown as Record<string, unknown>)[key] = next[key];
177
+ }
178
+ }
179
+ return internal === snapshot.state ? snapshot : { ...snapshot, state: internal };
180
+ });
181
+
182
+ if (next.pagination.pageIndex !== prev.pagination.pageIndex && viewportEl) {
183
+ viewportEl.scrollTop = 0;
184
+ }
185
+
186
+ options.onStateChange?.(next, prev);
187
+ if (queryChanged(prev, next)) options.onQueryChange?.(pickQuery(next));
188
+ }
189
+
190
+ const update = <K extends keyof GridState>(
191
+ key: K,
192
+ compute: (value: GridState[K]) => GridState[K],
193
+ ) =>
194
+ setState((prev) => {
195
+ const value = compute(prev[key]);
196
+ return value === prev[key] ? prev : { ...prev, [key]: value };
197
+ });
198
+
199
+ const getRowById = (rowId: RowId) => {
200
+ const rows = model?.coreRows ?? [];
201
+ if (rowLookup?.rows !== rows) {
202
+ rowLookup = { rows, map: new Map(rows.map((row) => [row.id, row])) };
203
+ }
204
+ return rowLookup.map.get(rowId);
205
+ };
206
+
207
+ const getColumn = (columnId: string) =>
208
+ model?.columns.find((column) => column.id === columnId);
209
+
210
+ // ---------------------------------------------------------------- selection
211
+
212
+ const isRowSelectable = (row: T) => {
213
+ const enabled = options.enableRowSelection;
214
+ return typeof enabled === "function" ? enabled(row) : enabled === true;
215
+ };
216
+
217
+ function toggleRowSelected(
218
+ rowId: RowId,
219
+ { value, range = false }: { value?: boolean; range?: boolean } = {},
220
+ ) {
221
+ const row = getRowById(rowId);
222
+ if (!row || !isRowSelectable(row.original)) return;
223
+
224
+ update("rowSelection", (selection) => {
225
+ const target = value ?? !selection[rowId];
226
+ if (options.selectionMode === "single") {
227
+ return target ? { [rowId]: true } : {};
228
+ }
229
+
230
+ const next = { ...selection };
231
+ const rows = model?.sortedRows ?? [];
232
+ const from = range && anchorRowId !== null ? rows.findIndex((r) => r.id === anchorRowId) : -1;
233
+ const to = from === -1 ? -1 : rows.findIndex((r) => r.id === rowId);
234
+
235
+ if (from !== -1 && to !== -1) {
236
+ for (let i = Math.min(from, to); i <= Math.max(from, to); i++) {
237
+ if (!isRowSelectable(rows[i].original)) continue;
238
+ if (target) next[rows[i].id] = true;
239
+ else delete next[rows[i].id];
240
+ }
241
+ } else if (target) {
242
+ next[rowId] = true;
243
+ } else {
244
+ delete next[rowId];
245
+ }
246
+ return next;
247
+ });
248
+ anchorRowId = rowId;
249
+ }
250
+
251
+ function toggleAllRowsSelected(value: boolean) {
252
+ if (options.selectionMode === "single") return;
253
+ const rows = model?.sortedRows ?? [];
254
+ update("rowSelection", (selection) => {
255
+ const next = { ...selection };
256
+ for (const row of rows) {
257
+ if (!isRowSelectable(row.original)) continue;
258
+ if (value) next[row.id] = true;
259
+ else delete next[row.id];
260
+ }
261
+ return next;
262
+ });
263
+ }
264
+
265
+ const getSelectedRowIds = () =>
266
+ Object.keys(getState().rowSelection).filter((id) => getState().rowSelection[id]);
267
+
268
+ const getSelectedRows = () => {
269
+ const selection = getState().rowSelection;
270
+ return (model?.coreRows ?? []).filter((row) => selection[row.id]).map((row) => row.original);
271
+ };
272
+
273
+ // --------------------------------------------------------------- navigation
274
+
275
+ function ensureVisible(rowIndex: number, colIndex: number) {
276
+ const el = viewportEl;
277
+ if (!el || !model) return;
278
+ const { layout, rowHeight, headerHeight } = model;
279
+
280
+ if (rowIndex >= 0) {
281
+ const top = rowIndex * rowHeight;
282
+ const bodyHeight = el.clientHeight - headerHeight;
283
+ if (top < el.scrollTop) el.scrollTop = top;
284
+ else if (top + rowHeight > el.scrollTop + bodyHeight) {
285
+ el.scrollTop = top + rowHeight - bodyHeight;
286
+ }
287
+ }
288
+
289
+ const item = layout.columns[colIndex];
290
+ if (item && !item.pinned) {
291
+ const rtl = getComputedStyle(el).direction === "rtl";
292
+ const scroll = Math.abs(el.scrollLeft);
293
+ const start = item.offset;
294
+ const end = start + item.width;
295
+ const visibleWidth = el.clientWidth - layout.leftWidth - layout.rightWidth;
296
+ let nextScroll = scroll;
297
+ if (start < scroll) nextScroll = start;
298
+ else if (end > scroll + visibleWidth) nextScroll = end - visibleWidth;
299
+ if (nextScroll !== scroll) el.scrollLeft = rtl ? -nextScroll : nextScroll;
300
+ }
301
+
302
+ // Scroll events are async; update the range now so the target renders this frame.
303
+ viewport.setState((v) =>
304
+ v.scrollTop === el.scrollTop && v.scrollLeft === el.scrollLeft
305
+ ? v
306
+ : { ...v, scrollTop: el.scrollTop, scrollLeft: el.scrollLeft },
307
+ );
308
+ }
309
+
310
+ function setActiveCell(
311
+ rowIndex: number,
312
+ colIndex: number,
313
+ { focus = true, scroll = true }: { focus?: boolean; scroll?: boolean } = {},
314
+ ) {
315
+ if (!model || model.layout.columns.length === 0) return;
316
+ const position = {
317
+ rowIndex: clamp(rowIndex, -1, model.page.rows.length - 1),
318
+ colIndex: clamp(colIndex, 0, model.layout.columns.length - 1),
319
+ };
320
+ if (focus) focusRequested = true;
321
+ setUi((ui) =>
322
+ ui.activeCell?.rowIndex === position.rowIndex &&
323
+ ui.activeCell.colIndex === position.colIndex
324
+ ? ui
325
+ : { ...ui, activeCell: position },
326
+ );
327
+ if (scroll) ensureVisible(position.rowIndex, position.colIndex);
328
+ // Already active: nothing re-renders, so focus the element directly.
329
+ if (focus && viewportEl) {
330
+ const selector = `[data-row-index="${position.rowIndex}"] [data-col-index="${position.colIndex}"]`;
331
+ const cell = viewportEl.querySelector<HTMLElement>(selector);
332
+ if (cell && document.activeElement !== cell) {
333
+ cell.focus({ preventScroll: true });
334
+ focusRequested = false;
335
+ }
336
+ }
337
+ }
338
+
339
+ function moveActiveCell(direction: MoveDirection, by = 1) {
340
+ const current = store.getSnapshot().ui.activeCell ?? { rowIndex: -1, colIndex: 0 };
341
+ const dRow = direction === "up" ? -by : direction === "down" ? by : 0;
342
+ const dCol = direction === "left" ? -by : direction === "right" ? by : 0;
343
+ setActiveCell(current.rowIndex + dRow, current.colIndex + dCol);
344
+ }
345
+
346
+ // ------------------------------------------------------------------ editing
347
+
348
+ const isCellEditable = (row: T, column: ResolvedColumn<T>) => {
349
+ const { editable } = column.def;
350
+ return typeof editable === "function" ? editable(row) : editable === true;
351
+ };
352
+
353
+ function startEditing(rowId: RowId, columnId: string) {
354
+ const row = getRowById(rowId);
355
+ const column = getColumn(columnId);
356
+ if (!row || !column || !isCellEditable(row.original, column)) return;
357
+ setUi((ui) => ({ ...ui, editing: { rowId, columnId } }));
358
+ }
359
+
360
+ function cancelEditing() {
361
+ if (!store.getSnapshot().ui.editing) return;
362
+ focusRequested = true;
363
+ setUi((ui) => ({ ...ui, editing: null }));
364
+ }
365
+
366
+ /** Returns a validation error, or null when the edit was accepted. */
367
+ function commitEdit(
368
+ rowId: RowId,
369
+ columnId: string,
370
+ value: unknown,
371
+ move?: MoveDirection,
372
+ ): string | null {
373
+ const editing = store.getSnapshot().ui.editing;
374
+ if (editing?.rowId !== rowId || editing.columnId !== columnId) return null;
375
+
376
+ const row = getRowById(rowId);
377
+ const column = getColumn(columnId);
378
+ if (!row || !column) return null;
379
+
380
+ const error = column.def.validate?.(value, row.original);
381
+ if (error) return error;
382
+
383
+ focusRequested = true;
384
+ setUi((ui) => ({ ...ui, editing: null, errors: setNested(ui.errors, rowId, columnId, undefined) }));
385
+
386
+ const previousValue = column.getValue(row.original);
387
+ if (!Object.is(previousValue, value) && options.onCellEdit) {
388
+ const result = options.onCellEdit({ row: row.original, rowId, columnId, value, previousValue });
389
+ if (result instanceof Promise) {
390
+ setUi((ui) => ({ ...ui, pending: setNested(ui.pending, rowId, columnId, value) }));
391
+ result.then(
392
+ () => setUi((ui) => ({ ...ui, pending: setNested(ui.pending, rowId, columnId, undefined) })),
393
+ (reason: unknown) =>
394
+ setUi((ui) => ({
395
+ ...ui,
396
+ pending: setNested(ui.pending, rowId, columnId, undefined),
397
+ errors: setNested(
398
+ ui.errors,
399
+ rowId,
400
+ columnId,
401
+ reason instanceof Error ? reason.message : String(reason),
402
+ ),
403
+ })),
404
+ );
405
+ }
406
+ }
407
+
408
+ if (move) moveActiveCell(move);
409
+ return null;
410
+ }
411
+
412
+ // ------------------------------------------------------------------ columns
413
+
414
+ function resetColumns() {
415
+ const initial = createInitialState({ ...options, initialState: undefined });
416
+ setState((prev) => ({
417
+ ...prev,
418
+ columnOrder: initial.columnOrder,
419
+ columnVisibility: initial.columnVisibility,
420
+ columnSizing: initial.columnSizing,
421
+ columnPinning: initial.columnPinning,
422
+ }));
423
+ }
424
+
425
+ /** Live resize: rewrites CSS variables without rendering. */
426
+ function previewColumnWidth(columnId: string, width: number) {
427
+ if (!viewportEl || !model) return;
428
+ const vars = getLayoutVars(model.layout, { [columnId]: width });
429
+ for (const [name, value] of Object.entries(vars)) {
430
+ viewportEl.style.setProperty(name, value);
431
+ }
432
+ }
433
+
434
+ // ------------------------------------------------------------------- export
435
+
436
+ function getRows(scope: ExportScope = "filtered"): T[] {
437
+ if (!model) return [];
438
+ switch (scope) {
439
+ case "all":
440
+ return model.coreRows.map((row) => row.original);
441
+ case "page":
442
+ return model.page.rows.map((row) => row.original);
443
+ case "selected":
444
+ return getSelectedRows();
445
+ default:
446
+ return model.sortedRows.map((row) => row.original);
447
+ }
448
+ }
449
+
450
+ async function buildTable(exportOptions: ExportOptions<T>) {
451
+ const { buildExportTable } = await import("../export/table");
452
+ const rows = exportOptions.rows ?? getRows(exportOptions.scope);
453
+ const items = (model?.layout.columns ?? []).filter(
454
+ (item) => !item.column.internal && item.column.exportable,
455
+ );
456
+ return buildExportTable(rows, items, model!.formatters);
457
+ }
458
+
459
+ const fileName = (exportOptions: ExportOptions<T>) =>
460
+ exportOptions.fileName ?? options.exportFileName ?? "export";
461
+
462
+ const api: GridApi<T> = {
463
+ getState,
464
+ setState,
465
+
466
+ toggleSort: (columnId, multi = false) =>
467
+ update("sorting", (sorting) =>
468
+ toggleSorting(sorting, columnId, multi && options.enableMultiSort !== false),
469
+ ),
470
+ setSorting: (sorting) => update("sorting", () => sorting),
471
+ setFilter: (columnId, filter) =>
472
+ update("filters", (filters) => {
473
+ const rest = filters.filter((f) => f.columnId !== columnId);
474
+ if (!filter) return rest.length === filters.length ? filters : rest;
475
+ return [...rest, { ...filter, columnId }];
476
+ }),
477
+ clearFilters: () =>
478
+ setState((prev) =>
479
+ prev.filters.length === 0 && prev.globalFilter === ""
480
+ ? prev
481
+ : { ...prev, filters: [], globalFilter: "" },
482
+ ),
483
+ setGlobalFilter: (value) => update("globalFilter", () => value),
484
+ setPageIndex: (pageIndex) =>
485
+ update("pagination", (p) =>
486
+ p.pageIndex === pageIndex ? p : { ...p, pageIndex: Math.max(0, pageIndex) },
487
+ ),
488
+ setPageSize: (pageSize) =>
489
+ update("pagination", (p) =>
490
+ p.pageSize === pageSize
491
+ ? p
492
+ : { pageSize, pageIndex: Math.floor((p.pageIndex * p.pageSize) / pageSize) },
493
+ ),
494
+ setDensity: (density) => update("density", () => density),
495
+
496
+ isRowSelectable,
497
+ toggleRowSelected,
498
+ toggleAllRowsSelected,
499
+ clearSelection: () =>
500
+ update("rowSelection", (selection) =>
501
+ Object.keys(selection).length === 0 ? selection : {},
502
+ ),
503
+ getSelectedRowIds,
504
+ getSelectedRows,
505
+
506
+ setColumnVisibility: (columnId, visible) =>
507
+ update("columnVisibility", (visibility) =>
508
+ (visibility[columnId] !== false) === visible
509
+ ? visibility
510
+ : { ...visibility, [columnId]: visible },
511
+ ),
512
+ setColumnWidth: (columnId, width) =>
513
+ update("columnSizing", (sizing) => {
514
+ const next = { ...sizing };
515
+ if (width === null) delete next[columnId];
516
+ else next[columnId] = Math.round(width);
517
+ return next;
518
+ }),
519
+ pinColumn: (columnId, side) =>
520
+ update("columnPinning", (pinning) => pinColumnState(pinning, columnId, side)),
521
+ moveColumn: (columnId, targetId, placement) =>
522
+ setState((prev) => ({
523
+ ...prev,
524
+ ...moveColumnState(model?.columns ?? [], prev, columnId, targetId, placement),
525
+ })),
526
+ resetColumns,
527
+
528
+ scrollToRow: (rowIndex) => ensureVisible(rowIndex, -1),
529
+ focusCell: (rowIndex, columnId) => {
530
+ const colIndex = model?.layout.columns.findIndex((item) => item.column.id === columnId) ?? -1;
531
+ if (colIndex !== -1) setActiveCell(rowIndex, colIndex);
532
+ },
533
+ startEditing,
534
+ cancelEditing,
535
+
536
+ getRows,
537
+ async exportCsv(exportOptions = {}) {
538
+ const [{ createCsvBlob }, { downloadBlob }, table] = await Promise.all([
539
+ import("../export/csv"),
540
+ import("../export/download"),
541
+ buildTable(exportOptions),
542
+ ]);
543
+ downloadBlob(createCsvBlob(table), `${fileName(exportOptions)}.csv`);
544
+ },
545
+ async exportExcel(exportOptions = {}) {
546
+ const [{ createXlsxBlob }, { downloadBlob }, table] = await Promise.all([
547
+ import("../export/xlsx"),
548
+ import("../export/download"),
549
+ buildTable(exportOptions),
550
+ ]);
551
+ downloadBlob(createXlsxBlob(table), `${fileName(exportOptions)}.xlsx`);
552
+ },
553
+ async exportPdf(exportOptions = {}) {
554
+ const [{ printTable }, table] = await Promise.all([
555
+ import("../export/pdf"),
556
+ buildTable(exportOptions),
557
+ ]);
558
+ printTable(table, {
559
+ title: exportOptions.title ?? fileName(exportOptions),
560
+ orientation: exportOptions.orientation ?? "landscape",
561
+ paperSize: exportOptions.paperSize ?? "A4",
562
+ });
563
+ },
564
+ async copyToClipboard() {
565
+ if (!model) return;
566
+ const selected = getSelectedRows();
567
+ let text = "";
568
+ if (selected.length > 0) {
569
+ const { toTsv } = await import("../export/csv");
570
+ text = toTsv(await buildTable({ rows: selected }));
571
+ } else {
572
+ const active = store.getSnapshot().ui.activeCell;
573
+ const row = active && model.page.rows[active.rowIndex];
574
+ const item = active && model.layout.columns[active.colIndex];
575
+ if (row && item && !item.column.internal) {
576
+ const value = item.column.getValue(row.original);
577
+ text = formatCellValue(item.column, value, row.original, model.formatters);
578
+ }
579
+ }
580
+ if (text) await navigator.clipboard.writeText(text);
581
+ },
582
+ };
583
+
584
+ return {
585
+ api,
586
+ store,
587
+ viewport,
588
+ getOptions: () => options,
589
+ getModel: () => model,
590
+ /** Called after every commit with the latest props and derived model. */
591
+ sync(nextOptions: GridOptions<T>, nextModel: GridModel<T>) {
592
+ options = nextOptions;
593
+ model = nextModel;
594
+ },
595
+ attachViewport(el: HTMLElement | null) {
596
+ viewportEl = el;
597
+ },
598
+ setActiveCell,
599
+ moveActiveCell,
600
+ consumeFocusRequest() {
601
+ const requested = focusRequested;
602
+ focusRequested = false;
603
+ return requested;
604
+ },
605
+ isRowSelectable,
606
+ isCellEditable,
607
+ commitEdit,
608
+ previewColumnWidth,
609
+ getDraggingColumn: () => draggingColumnId,
610
+ setDraggingColumn(columnId: string | null) {
611
+ draggingColumnId = columnId;
612
+ },
613
+ };
614
+ }
615
+
616
+ export type GridEngine<T> = ReturnType<typeof createGridEngine<T>>;
@@ -0,0 +1,14 @@
1
+ // Framework-free entry point: safe to import on the server (e.g. to sort,
2
+ // filter and paginate in a Next.js route handler with the same semantics).
3
+ export { createColumnHelper } from "./columnHelper";
4
+ export { computeLayout, resolveColumns } from "./columns";
5
+ export { compileFilter, filterRows, getFilterOperators, isFilterActive } from "./filtering";
6
+ export { createGridEngine } from "./grid";
7
+ export type { GridApi, GridEngine } from "./grid";
8
+ export { buildRows, createRowIdGetter, paginate } from "./rows";
9
+ export type { PageResult } from "./rows";
10
+ export { sortRows, toggleSorting } from "./sorting";
11
+ export { createInitialState } from "./state";
12
+ export { createFormatters, formatCellValue } from "./values";
13
+ export type { Formatters } from "./values";
14
+ export type * from "./types";
@@ -0,0 +1,64 @@
1
+ import type { GridOptions, GridRow, PaginationState, RowId } from "./types";
2
+
3
+ export type RowIdGetter<T> = (row: T, index: number) => RowId;
4
+
5
+ export function createRowIdGetter<T>(
6
+ getRowId: GridOptions<T>["getRowId"],
7
+ ): RowIdGetter<T> {
8
+ if (typeof getRowId === "function") {
9
+ return (row, index) => String(getRowId(row, index));
10
+ }
11
+ if (typeof getRowId === "string") {
12
+ return (row) => String(row[getRowId]);
13
+ }
14
+ return (row, index) => {
15
+ const id = (row as { id?: unknown } | null)?.id;
16
+ return id === null || id === undefined ? String(index) : String(id);
17
+ };
18
+ }
19
+
20
+ export function buildRows<T>(
21
+ data: readonly T[],
22
+ getRowId: RowIdGetter<T>,
23
+ ): GridRow<T>[] {
24
+ return data.map((original, index) => ({
25
+ id: getRowId(original, index),
26
+ index,
27
+ original,
28
+ }));
29
+ }
30
+
31
+ export interface PageResult<T> {
32
+ rows: GridRow<T>[];
33
+ pageIndex: number;
34
+ pageCount: number;
35
+ /** Total rows across all pages. */
36
+ rowCount: number;
37
+ /** Absolute index of the first row on the page. */
38
+ pageOffset: number;
39
+ }
40
+
41
+ export function paginate<T>(
42
+ rows: GridRow<T>[],
43
+ pagination: PaginationState,
44
+ options: { enabled: boolean; server: boolean; rowCount?: number },
45
+ ): PageResult<T> {
46
+ const total = options.server ? (options.rowCount ?? rows.length) : rows.length;
47
+
48
+ if (!options.enabled) {
49
+ return { rows, pageIndex: 0, pageCount: 1, rowCount: total, pageOffset: 0 };
50
+ }
51
+
52
+ const pageSize = Math.max(1, pagination.pageSize);
53
+ const pageCount = Math.max(1, Math.ceil(total / pageSize));
54
+ const pageIndex = Math.min(Math.max(0, pagination.pageIndex), pageCount - 1);
55
+ const pageOffset = pageIndex * pageSize;
56
+
57
+ return {
58
+ rows: options.server ? rows : rows.slice(pageOffset, pageOffset + pageSize),
59
+ pageIndex,
60
+ pageCount,
61
+ rowCount: total,
62
+ pageOffset,
63
+ };
64
+ }