@helix-x/datagrid-ui 0.1.0

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.
package/dist/index.cjs ADDED
@@ -0,0 +1,2351 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/components/DataGrid.tsx
7
+
8
+ // src/core/values.ts
9
+ var DEFAULT_WIDTH = 150;
10
+ var DEFAULT_MIN_WIDTH = 60;
11
+ function readPath(row, path) {
12
+ if (row == null) return void 0;
13
+ if (!path.includes(".")) return row[path];
14
+ let current = row;
15
+ for (const segment of path.split(".")) {
16
+ if (current == null || typeof current !== "object") return void 0;
17
+ current = current[segment];
18
+ }
19
+ return current;
20
+ }
21
+ function resolveValue(row, column) {
22
+ if (column.valueGetter) return column.valueGetter(row);
23
+ const { field } = column;
24
+ if (!field) return void 0;
25
+ const nested = readPath(row, field);
26
+ if (nested !== void 0) return nested;
27
+ const flat = row?.[field];
28
+ if (flat !== void 0) return flat;
29
+ for (const alias of column.fieldAliases ?? []) {
30
+ const value = readPath(row, alias) ?? row?.[alias];
31
+ if (value !== void 0) return value;
32
+ }
33
+ return void 0;
34
+ }
35
+ function formatValue(value, row, column) {
36
+ if (column.valueFormatter) return column.valueFormatter(value, row);
37
+ if (value == null) return "";
38
+ if (value instanceof Date) return value.toISOString().slice(0, 10);
39
+ if (typeof value === "boolean") return value ? "Yes" : "No";
40
+ return String(value);
41
+ }
42
+ function exportValue(row, column) {
43
+ if (column.exportValue) return column.exportValue(row);
44
+ return formatValue(resolveValue(row, column), row, column);
45
+ }
46
+ function headerText(column) {
47
+ if (column.headerName) return column.headerName;
48
+ if (typeof column.header === "string") return column.header;
49
+ if (typeof column.header === "number") return String(column.header);
50
+ return column.colId ?? column.field ?? "";
51
+ }
52
+ function columnId(column) {
53
+ const id = column.colId ?? column.field;
54
+ if (!id) {
55
+ throw new Error("@helix-x/datagrid-ui: every column needs a `colId` or a `field`.");
56
+ }
57
+ return id;
58
+ }
59
+ function resolveColumn(column) {
60
+ const minWidth = column.minWidth ?? DEFAULT_MIN_WIDTH;
61
+ return {
62
+ ...column,
63
+ colId: columnId(column),
64
+ width: Math.max(column.width ?? DEFAULT_WIDTH, minWidth),
65
+ minWidth,
66
+ sortable: column.sortable ?? true,
67
+ resizable: column.resizable ?? true
68
+ };
69
+ }
70
+ function isEditable(column, row) {
71
+ if (typeof column.editable === "function") return column.editable(row);
72
+ return column.editable === true;
73
+ }
74
+
75
+ // src/core/useColumnState.ts
76
+ function defaultsFrom(columns) {
77
+ const order = [];
78
+ const hidden = [];
79
+ const widths = {};
80
+ const pinned = {};
81
+ for (const column of columns) {
82
+ const id = columnId(column);
83
+ order.push(id);
84
+ if (column.hide) hidden.push(id);
85
+ if (column.width != null) widths[id] = column.width;
86
+ if (column.pinned) pinned[id] = column.pinned;
87
+ }
88
+ return { order, hidden, widths, pinned };
89
+ }
90
+ function mergePersisted(defaults, persisted, knownIds) {
91
+ if (!persisted) return defaults;
92
+ const savedOrder = persisted.order.filter((id) => knownIds.has(id));
93
+ const savedSet = new Set(savedOrder);
94
+ const order = [...savedOrder];
95
+ defaults.order.forEach((id, index) => {
96
+ if (!savedSet.has(id)) order.splice(Math.min(index, order.length), 0, id);
97
+ });
98
+ return {
99
+ order,
100
+ hidden: (persisted.hidden ?? defaults.hidden).filter((id) => knownIds.has(id)),
101
+ widths: { ...defaults.widths, ...persisted.widths ?? {} },
102
+ pinned: { ...defaults.pinned, ...persisted.pinned ?? {} }
103
+ };
104
+ }
105
+ function useColumnState(columns, persisted, onChange, availableWidth) {
106
+ const resolved = react.useMemo(() => columns.map((c) => resolveColumn(c)), [columns]);
107
+ const byId = react.useMemo(() => {
108
+ const map = /* @__PURE__ */ new Map();
109
+ for (const column of resolved) map.set(column.colId, column);
110
+ return map;
111
+ }, [resolved]);
112
+ const defaults = react.useMemo(() => defaultsFrom(columns), [columns]);
113
+ const [state, setState] = react.useState(
114
+ () => mergePersisted(defaults, persisted, new Set(defaults.order))
115
+ );
116
+ const update = react.useCallback(
117
+ (mutate) => {
118
+ setState((current) => {
119
+ const next = mutate(current);
120
+ onChange(next);
121
+ return next;
122
+ });
123
+ },
124
+ [onChange]
125
+ );
126
+ const hiddenSet = react.useMemo(() => new Set(state.hidden), [state.hidden]);
127
+ const allColumns = react.useMemo(
128
+ () => state.order.map((id) => byId.get(id)).filter((c) => c != null),
129
+ [state.order, byId]
130
+ );
131
+ const visibleColumns = react.useMemo(
132
+ () => allColumns.filter((column) => !hiddenSet.has(column.colId)).map(
133
+ (column) => state.widths[column.colId] != null || state.pinned[column.colId] != null ? {
134
+ ...column,
135
+ width: Math.max(
136
+ state.widths[column.colId] ?? column.width,
137
+ column.minWidth
138
+ ),
139
+ pinned: state.pinned[column.colId] ?? column.pinned
140
+ } : column
141
+ ),
142
+ [allColumns, hiddenSet, state.widths, state.pinned]
143
+ );
144
+ const layout = react.useMemo(() => {
145
+ const flexTotal = visibleColumns.reduce((sum, c) => sum + (c.flex ?? 0), 0);
146
+ const fixedWidth = visibleColumns.reduce(
147
+ (sum, c) => sum + (c.flex ? 0 : c.width),
148
+ 0
149
+ );
150
+ const spare = Math.max(0, availableWidth - fixedWidth);
151
+ const widthOf = (column) => {
152
+ if (!column.flex || flexTotal === 0) return column.width;
153
+ const share = Math.floor(spare * column.flex / flexTotal);
154
+ const capped = column.maxWidth ? Math.min(share, column.maxWidth) : share;
155
+ return Math.max(capped, column.minWidth);
156
+ };
157
+ const left = visibleColumns.filter((c) => c.pinned === "left");
158
+ const middle = visibleColumns.filter((c) => !c.pinned);
159
+ const right = visibleColumns.filter((c) => c.pinned === "right");
160
+ const items = [];
161
+ let offset = 0;
162
+ let leftSticky = 0;
163
+ for (const column of left) {
164
+ const width = widthOf(column);
165
+ items.push({
166
+ column,
167
+ colId: column.colId,
168
+ width,
169
+ left: offset,
170
+ pinned: "left",
171
+ stickyOffset: leftSticky
172
+ });
173
+ offset += width;
174
+ leftSticky += width;
175
+ }
176
+ for (const column of middle) {
177
+ const width = widthOf(column);
178
+ items.push({ column, colId: column.colId, width, left: offset, stickyOffset: 0 });
179
+ offset += width;
180
+ }
181
+ const rightWidths = right.map(widthOf);
182
+ let rightSticky = 0;
183
+ const rightOffsets = [];
184
+ for (let i = right.length - 1; i >= 0; i--) {
185
+ rightOffsets[i] = rightSticky;
186
+ rightSticky += rightWidths[i];
187
+ }
188
+ right.forEach((column, index) => {
189
+ const width = rightWidths[index];
190
+ items.push({
191
+ column,
192
+ colId: column.colId,
193
+ width,
194
+ left: offset,
195
+ pinned: "right",
196
+ stickyOffset: rightOffsets[index]
197
+ });
198
+ offset += width;
199
+ });
200
+ return {
201
+ items,
202
+ totalWidth: offset,
203
+ leftPinnedWidth: leftSticky,
204
+ rightPinnedWidth: rightSticky
205
+ };
206
+ }, [visibleColumns, availableWidth]);
207
+ const isHidden = react.useCallback((colId) => hiddenSet.has(colId), [hiddenSet]);
208
+ const setHidden = react.useCallback(
209
+ (colId, hidden) => {
210
+ update((current) => ({
211
+ ...current,
212
+ hidden: hidden ? current.hidden.includes(colId) ? current.hidden : [...current.hidden, colId] : current.hidden.filter((id) => id !== colId)
213
+ }));
214
+ },
215
+ [update]
216
+ );
217
+ const setWidth = react.useCallback(
218
+ (colId, width) => {
219
+ update((current) => ({
220
+ ...current,
221
+ widths: { ...current.widths, [colId]: Math.round(width) }
222
+ }));
223
+ },
224
+ [update]
225
+ );
226
+ const setPinned = react.useCallback(
227
+ (colId, pinned) => {
228
+ update((current) => {
229
+ const next = { ...current.pinned };
230
+ if (pinned) next[colId] = pinned;
231
+ else delete next[colId];
232
+ return { ...current, pinned: next };
233
+ });
234
+ },
235
+ [update]
236
+ );
237
+ const moveColumn = react.useCallback(
238
+ (colId, toIndex) => {
239
+ update((current) => {
240
+ const from = current.order.indexOf(colId);
241
+ if (from === -1 || from === toIndex) return current;
242
+ const order = [...current.order];
243
+ order.splice(from, 1);
244
+ order.splice(Math.max(0, Math.min(toIndex, order.length)), 0, colId);
245
+ return { ...current, order };
246
+ });
247
+ },
248
+ [update]
249
+ );
250
+ const reset = react.useCallback(() => {
251
+ update(() => defaults);
252
+ }, [update, defaults]);
253
+ return {
254
+ allColumns,
255
+ visibleColumns,
256
+ layout,
257
+ state,
258
+ isHidden,
259
+ setHidden,
260
+ setWidth,
261
+ setPinned,
262
+ moveColumn,
263
+ reset
264
+ };
265
+ }
266
+ function setPath(row, path, value) {
267
+ if (!path.includes(".")) {
268
+ return { ...row, [path]: value };
269
+ }
270
+ const [head, ...rest] = path.split(".");
271
+ const current = row[head];
272
+ return {
273
+ ...row,
274
+ [head]: setPath(
275
+ current ?? {},
276
+ rest.join("."),
277
+ value
278
+ )
279
+ };
280
+ }
281
+ function useEditModel(onCommit) {
282
+ const [edit, setEdit] = react.useState(null);
283
+ const editRef = react.useRef(null);
284
+ const apply = react.useCallback(
285
+ (next) => {
286
+ editRef.current = next;
287
+ setEdit(next);
288
+ },
289
+ []
290
+ );
291
+ const patch = react.useCallback((mutate) => {
292
+ const current = editRef.current;
293
+ if (!current) return;
294
+ const next = mutate(current);
295
+ editRef.current = next;
296
+ setEdit(next);
297
+ }, []);
298
+ const isEditing = react.useCallback(
299
+ (rowId) => edit?.rowId === rowId,
300
+ [edit]
301
+ );
302
+ const start = react.useCallback(
303
+ (rowId, row) => {
304
+ apply({ rowId, draft: row, original: row, errors: {}, isSaving: false });
305
+ },
306
+ [apply]
307
+ );
308
+ const setField = react.useCallback(
309
+ (field, value) => {
310
+ patch((current) => {
311
+ const errors = { ...current.errors };
312
+ delete errors[field];
313
+ return { ...current, draft: setPath(current.draft, field, value), errors };
314
+ });
315
+ },
316
+ [patch]
317
+ );
318
+ const cancel = react.useCallback(() => apply(null), [apply]);
319
+ const commit = react.useCallback(async () => {
320
+ const state = editRef.current;
321
+ if (!state || state.isSaving) return;
322
+ apply({ ...state, isSaving: true });
323
+ try {
324
+ const result = await onCommit(state.draft, state.original);
325
+ if (result.ok) {
326
+ apply(null);
327
+ return;
328
+ }
329
+ patch((current) => ({ ...current, errors: result.errors, isSaving: false }));
330
+ } catch (error) {
331
+ patch((current) => ({
332
+ ...current,
333
+ isSaving: false,
334
+ errors: {
335
+ ...current.errors,
336
+ __row__: error instanceof Error ? error.message : "Failed to save the row."
337
+ }
338
+ }));
339
+ }
340
+ }, [onCommit, apply, patch]);
341
+ return { edit, isEditing, start, setField, cancel, commit };
342
+ }
343
+
344
+ // src/types.ts
345
+ var GRID_STATE_VERSION = 1;
346
+
347
+ // src/core/useGridState.ts
348
+ var PREFIX = "hxg:";
349
+ function read(storageKey) {
350
+ if (typeof window === "undefined") return void 0;
351
+ try {
352
+ const raw = window.localStorage.getItem(PREFIX + storageKey);
353
+ if (!raw) return void 0;
354
+ const parsed = JSON.parse(raw);
355
+ if (!parsed || parsed.v !== GRID_STATE_VERSION) return void 0;
356
+ return parsed;
357
+ } catch {
358
+ return void 0;
359
+ }
360
+ }
361
+ function write(storageKey, state) {
362
+ if (typeof window === "undefined") return;
363
+ try {
364
+ window.localStorage.setItem(PREFIX + storageKey, JSON.stringify(state));
365
+ } catch {
366
+ }
367
+ }
368
+ function useGridState(storageKey) {
369
+ const initialRef = react.useRef(
370
+ storageKey ? read(storageKey) : void 0
371
+ );
372
+ const [hasSavedState, setHasSavedState] = react.useState(() => initialRef.current != null);
373
+ const currentRef = react.useRef(
374
+ initialRef.current ?? {
375
+ v: GRID_STATE_VERSION,
376
+ columns: { order: [], hidden: [], widths: {}, pinned: {} },
377
+ sort: [],
378
+ filters: {},
379
+ pagination: { pageSize: 20 }
380
+ }
381
+ );
382
+ const persist = react.useCallback(
383
+ (patch) => {
384
+ if (!storageKey) return;
385
+ currentRef.current = { ...currentRef.current, ...patch, v: GRID_STATE_VERSION };
386
+ write(storageKey, currentRef.current);
387
+ setHasSavedState(true);
388
+ },
389
+ [storageKey]
390
+ );
391
+ const saveColumns = react.useCallback(
392
+ (columns) => persist({ columns }),
393
+ [persist]
394
+ );
395
+ const saveSort = react.useCallback((sort) => persist({ sort }), [persist]);
396
+ const saveFilters = react.useCallback(
397
+ (filters) => persist({ filters }),
398
+ [persist]
399
+ );
400
+ const savePageSize = react.useCallback(
401
+ (pageSize) => persist({ pagination: { pageSize } }),
402
+ [persist]
403
+ );
404
+ const clear = react.useCallback(() => {
405
+ if (!storageKey || typeof window === "undefined") return;
406
+ try {
407
+ window.localStorage.removeItem(PREFIX + storageKey);
408
+ } catch {
409
+ }
410
+ setHasSavedState(false);
411
+ }, [storageKey]);
412
+ return {
413
+ initial: initialRef.current,
414
+ saveColumns,
415
+ saveSort,
416
+ saveFilters,
417
+ savePageSize,
418
+ clear,
419
+ hasSavedState
420
+ };
421
+ }
422
+ function useSelectionModel(rows, getRowId, onSelectionChanged) {
423
+ const [selectedIds, setSelectedIds] = react.useState(() => /* @__PURE__ */ new Set());
424
+ const lastToggledIndexRef = react.useRef(null);
425
+ const rowsRef = react.useRef(rows);
426
+ rowsRef.current = rows;
427
+ const getRowIdRef = react.useRef(getRowId);
428
+ getRowIdRef.current = getRowId;
429
+ const commit = react.useCallback(
430
+ (next) => {
431
+ setSelectedIds(next);
432
+ onSelectionChanged?.([...next]);
433
+ },
434
+ [onSelectionChanged]
435
+ );
436
+ const isSelected = react.useCallback((id) => selectedIds.has(id), [selectedIds]);
437
+ const visibleIds = react.useMemo(() => rows.map(getRowId), [rows, getRowId]);
438
+ const allVisibleSelected = visibleIds.length > 0 && visibleIds.every((id) => selectedIds.has(id));
439
+ const someVisibleSelected = !allVisibleSelected && visibleIds.some((id) => selectedIds.has(id));
440
+ const toggleRow = react.useCallback(
441
+ (id, index, shiftKey) => {
442
+ const next = new Set(selectedIds);
443
+ const anchor = lastToggledIndexRef.current;
444
+ if (shiftKey && anchor != null && anchor !== index) {
445
+ const [from, to] = anchor < index ? [anchor, index] : [index, anchor];
446
+ const shouldSelect = !next.has(id);
447
+ for (let i = from; i <= to; i++) {
448
+ const rowId = getRowIdRef.current(rowsRef.current[i]);
449
+ if (rowId == null) continue;
450
+ if (shouldSelect) next.add(rowId);
451
+ else next.delete(rowId);
452
+ }
453
+ } else if (next.has(id)) {
454
+ next.delete(id);
455
+ } else {
456
+ next.add(id);
457
+ }
458
+ lastToggledIndexRef.current = index;
459
+ commit(next);
460
+ },
461
+ [selectedIds, commit]
462
+ );
463
+ const toggleAllVisible = react.useCallback(() => {
464
+ const next = new Set(selectedIds);
465
+ if (allVisibleSelected) {
466
+ for (const id of visibleIds) next.delete(id);
467
+ } else {
468
+ for (const id of visibleIds) next.add(id);
469
+ }
470
+ lastToggledIndexRef.current = null;
471
+ commit(next);
472
+ }, [selectedIds, allVisibleSelected, visibleIds, commit]);
473
+ const selectAllVisible = react.useCallback(() => {
474
+ const next = new Set(selectedIds);
475
+ for (const id of visibleIds) next.add(id);
476
+ commit(next);
477
+ }, [selectedIds, visibleIds, commit]);
478
+ const clear = react.useCallback(() => {
479
+ lastToggledIndexRef.current = null;
480
+ commit(/* @__PURE__ */ new Set());
481
+ }, [commit]);
482
+ const getSelectedRows = react.useCallback(
483
+ () => rowsRef.current.filter((row) => selectedIds.has(getRowIdRef.current(row))),
484
+ [selectedIds]
485
+ );
486
+ return {
487
+ selectedIds,
488
+ isSelected,
489
+ allVisibleSelected,
490
+ someVisibleSelected,
491
+ toggleRow,
492
+ toggleAllVisible,
493
+ clear,
494
+ selectAllVisible,
495
+ getSelectedRows
496
+ };
497
+ }
498
+ function buildRequest(page, pageSize, sortModel, filterModel) {
499
+ return {
500
+ startRow: page * pageSize,
501
+ endRow: page * pageSize + pageSize,
502
+ sortModel,
503
+ filterModel,
504
+ // Sent empty for wire compatibility with grid endpoints that expect them.
505
+ rowGroupCols: [],
506
+ valueCols: [],
507
+ pivotCols: [],
508
+ pivotMode: false,
509
+ groupKeys: []
510
+ };
511
+ }
512
+ function useServerDataSource({
513
+ dataSource,
514
+ pageSize,
515
+ page,
516
+ sortModel,
517
+ filterModel,
518
+ maxCachedBlocks = 3,
519
+ onError
520
+ }) {
521
+ const [rows, setRows] = react.useState([]);
522
+ const [totalRows, setTotalRows] = react.useState(0);
523
+ const [isLoading, setIsLoading] = react.useState(false);
524
+ const [error, setError] = react.useState(null);
525
+ const [refreshToken, setRefreshToken] = react.useState(0);
526
+ const cacheRef = react.useRef(/* @__PURE__ */ new Map());
527
+ const requestIdRef = react.useRef(0);
528
+ const abortRef = react.useRef(null);
529
+ const queryKey = react.useMemo(
530
+ () => JSON.stringify({ pageSize, sortModel, filterModel }),
531
+ [pageSize, sortModel, filterModel]
532
+ );
533
+ react.useEffect(() => {
534
+ cacheRef.current.clear();
535
+ }, [queryKey]);
536
+ const onErrorRef = react.useRef(onError);
537
+ react.useEffect(() => {
538
+ onErrorRef.current = onError;
539
+ }, [onError]);
540
+ const dataSourceRef = react.useRef(dataSource);
541
+ react.useEffect(() => {
542
+ dataSourceRef.current = dataSource;
543
+ }, [dataSource]);
544
+ react.useEffect(() => {
545
+ const blockKey = `${queryKey}::${page}`;
546
+ const cached = cacheRef.current.get(blockKey);
547
+ if (cached) {
548
+ setRows(cached.rows);
549
+ setTotalRows(cached.lastRow);
550
+ setError(null);
551
+ return;
552
+ }
553
+ abortRef.current?.abort();
554
+ const controller = new AbortController();
555
+ abortRef.current = controller;
556
+ const requestId = ++requestIdRef.current;
557
+ setIsLoading(true);
558
+ setError(null);
559
+ const request = buildRequest(page, pageSize, sortModel, filterModel);
560
+ dataSourceRef.current.getRows(request, controller.signal).then((response) => {
561
+ if (requestId !== requestIdRef.current) return;
562
+ const lastRow = response.lastRow ?? response.rows.length;
563
+ cacheRef.current.set(blockKey, { rows: response.rows, lastRow });
564
+ while (cacheRef.current.size > maxCachedBlocks) {
565
+ const oldest = cacheRef.current.keys().next().value;
566
+ if (oldest === void 0) break;
567
+ cacheRef.current.delete(oldest);
568
+ }
569
+ setRows(response.rows);
570
+ setTotalRows(lastRow);
571
+ setIsLoading(false);
572
+ }).catch((err) => {
573
+ if (controller.signal.aborted) return;
574
+ if (requestId !== requestIdRef.current) return;
575
+ setError(err);
576
+ setIsLoading(false);
577
+ onErrorRef.current?.(err);
578
+ });
579
+ return () => {
580
+ controller.abort();
581
+ };
582
+ }, [queryKey, page, pageSize, sortModel, filterModel, refreshToken, maxCachedBlocks]);
583
+ const refresh = react.useCallback((options) => {
584
+ if (options?.purge !== false) cacheRef.current.clear();
585
+ setRefreshToken((token) => token + 1);
586
+ }, []);
587
+ const patchRows = react.useCallback(
588
+ (updated, getRowId) => {
589
+ if (updated.length === 0) return;
590
+ const byId = new Map(updated.map((row) => [getRowId(row), row]));
591
+ const merge = (list) => {
592
+ let changed = false;
593
+ const next = list.map((row) => {
594
+ const replacement = byId.get(getRowId(row));
595
+ if (!replacement) return row;
596
+ changed = true;
597
+ return replacement;
598
+ });
599
+ return changed ? next : list;
600
+ };
601
+ setRows(merge);
602
+ for (const [key, block] of cacheRef.current) {
603
+ cacheRef.current.set(key, { ...block, rows: merge(block.rows) });
604
+ }
605
+ },
606
+ []
607
+ );
608
+ return { rows, totalRows, isLoading, error, refresh, patchRows };
609
+ }
610
+ function useVirtualRows({
611
+ rowCount,
612
+ rowHeight,
613
+ overscan = 6
614
+ }) {
615
+ const [viewportHeight, setViewportHeightState] = react.useState(0);
616
+ const [window2, setWindow] = react.useState({ startIndex: 0, endIndex: 0 });
617
+ const scrollTopRef = react.useRef(0);
618
+ const frameRef = react.useRef(null);
619
+ const compute = react.useCallback(
620
+ (scrollTop, height) => {
621
+ if (height <= 0 || rowCount === 0) {
622
+ return { startIndex: 0, endIndex: 0 };
623
+ }
624
+ const first = Math.floor(scrollTop / rowHeight);
625
+ const visible = Math.ceil(height / rowHeight);
626
+ return {
627
+ startIndex: Math.max(0, first - overscan),
628
+ endIndex: Math.min(rowCount, first + visible + overscan)
629
+ };
630
+ },
631
+ [rowCount, rowHeight, overscan]
632
+ );
633
+ const apply = react.useCallback(
634
+ (scrollTop, height) => {
635
+ const next = compute(scrollTop, height);
636
+ setWindow(
637
+ (current) => current.startIndex === next.startIndex && current.endIndex === next.endIndex ? current : next
638
+ );
639
+ },
640
+ [compute]
641
+ );
642
+ const onScroll = react.useCallback(
643
+ (event) => {
644
+ scrollTopRef.current = event.currentTarget.scrollTop;
645
+ if (frameRef.current != null) return;
646
+ frameRef.current = requestAnimationFrame(() => {
647
+ frameRef.current = null;
648
+ apply(scrollTopRef.current, viewportHeight);
649
+ });
650
+ },
651
+ [apply, viewportHeight]
652
+ );
653
+ const setViewportHeight = react.useCallback((height) => {
654
+ setViewportHeightState((current) => current === height ? current : height);
655
+ }, []);
656
+ react.useEffect(() => {
657
+ apply(scrollTopRef.current, viewportHeight);
658
+ }, [apply, viewportHeight]);
659
+ react.useEffect(
660
+ () => () => {
661
+ if (frameRef.current != null) cancelAnimationFrame(frameRef.current);
662
+ },
663
+ []
664
+ );
665
+ return {
666
+ window: window2,
667
+ totalHeight: rowCount * rowHeight,
668
+ onScroll,
669
+ setViewportHeight,
670
+ scrollTopRef
671
+ };
672
+ }
673
+
674
+ // src/core/exporters.ts
675
+ function escapeCsv(value, separator) {
676
+ if (value === "") return "";
677
+ const needsQuotes = value.includes(separator) || value.includes('"') || value.includes("\n") || value.includes("\r");
678
+ return needsQuotes ? `"${value.replace(/"/g, '""')}"` : value;
679
+ }
680
+ function toDelimited(rows, columns, separator) {
681
+ const exportable = columns.filter((column) => !column.suppressExport);
682
+ const lines = [
683
+ exportable.map((c) => escapeCsv(headerText(c), separator)).join(separator)
684
+ ];
685
+ for (const row of rows) {
686
+ lines.push(
687
+ exportable.map((column) => escapeCsv(exportValue(row, column), separator)).join(separator)
688
+ );
689
+ }
690
+ return lines.join("\r\n");
691
+ }
692
+ function toCsv(rows, columns, separator = ",") {
693
+ return toDelimited(rows, columns, separator);
694
+ }
695
+ function toTsv(rows, columns) {
696
+ return toDelimited(rows, columns, " ");
697
+ }
698
+ function downloadCsv(content, fileName) {
699
+ const blob = new Blob(["\uFEFF" + content], {
700
+ type: "text/csv;charset=utf-8;"
701
+ });
702
+ const url = URL.createObjectURL(blob);
703
+ const anchor = document.createElement("a");
704
+ anchor.href = url;
705
+ anchor.download = fileName.endsWith(".csv") ? fileName : `${fileName}.csv`;
706
+ document.body.appendChild(anchor);
707
+ anchor.click();
708
+ document.body.removeChild(anchor);
709
+ URL.revokeObjectURL(url);
710
+ }
711
+ async function copyToClipboard(text) {
712
+ if (navigator?.clipboard?.writeText) {
713
+ await navigator.clipboard.writeText(text);
714
+ return;
715
+ }
716
+ const textarea = document.createElement("textarea");
717
+ textarea.value = text;
718
+ textarea.style.position = "fixed";
719
+ textarea.style.opacity = "0";
720
+ document.body.appendChild(textarea);
721
+ textarea.select();
722
+ document.execCommand("copy");
723
+ document.body.removeChild(textarea);
724
+ }
725
+
726
+ // src/core/filterModel.ts
727
+ var TEXT_FILTER_TYPES = [
728
+ "contains",
729
+ "notContains",
730
+ "equals",
731
+ "notEqual",
732
+ "startsWith",
733
+ "endsWith",
734
+ "blank",
735
+ "notBlank"
736
+ ];
737
+ var NUMBER_FILTER_TYPES = [
738
+ "equals",
739
+ "notEqual",
740
+ "lessThan",
741
+ "lessThanOrEqual",
742
+ "greaterThan",
743
+ "greaterThanOrEqual",
744
+ "inRange",
745
+ "blank",
746
+ "notBlank"
747
+ ];
748
+ var DATE_FILTER_TYPES = [
749
+ "equals",
750
+ "notEqual",
751
+ "before",
752
+ "after",
753
+ "inRange",
754
+ "blank",
755
+ "notBlank"
756
+ ];
757
+ var FILTER_TYPE_LABELS = {
758
+ contains: "Contains",
759
+ notContains: "Does not contain",
760
+ equals: "Equals",
761
+ notEqual: "Not equal",
762
+ startsWith: "Starts with",
763
+ endsWith: "Ends with",
764
+ blank: "Is empty",
765
+ notBlank: "Is not empty",
766
+ lessThan: "Less than",
767
+ lessThanOrEqual: "Less than or equal",
768
+ greaterThan: "Greater than",
769
+ greaterThanOrEqual: "Greater than or equal",
770
+ inRange: "Between",
771
+ before: "Before",
772
+ after: "After"
773
+ };
774
+ function isUnaryFilter(type) {
775
+ return type === "blank" || type === "notBlank";
776
+ }
777
+ function isRangeFilter(type) {
778
+ return type === "inRange";
779
+ }
780
+ function defaultFilterType(kind) {
781
+ switch (kind) {
782
+ case "text":
783
+ return "contains";
784
+ case "number":
785
+ return "equals";
786
+ case "date":
787
+ return "equals";
788
+ case "set":
789
+ return "set";
790
+ }
791
+ }
792
+ function buildTextFilter(type, filter) {
793
+ if (!isUnaryFilter(type) && filter.trim() === "") return null;
794
+ return isUnaryFilter(type) ? { filterType: "text", type } : { filterType: "text", type, filter };
795
+ }
796
+ function buildNumberFilter(type, filter, filterTo) {
797
+ if (isUnaryFilter(type)) return { filterType: "number", type };
798
+ const from = Number(filter);
799
+ if (filter.trim() === "" || Number.isNaN(from)) return null;
800
+ if (isRangeFilter(type)) {
801
+ const to = Number(filterTo);
802
+ if (!filterTo || filterTo.trim() === "" || Number.isNaN(to)) return null;
803
+ return { filterType: "number", type, filter: from, filterTo: to };
804
+ }
805
+ return { filterType: "number", type, filter: from };
806
+ }
807
+ function buildDateFilter(type, dateFrom, dateTo) {
808
+ if (isUnaryFilter(type)) return { filterType: "date", type };
809
+ if (!dateFrom) return null;
810
+ if (isRangeFilter(type)) {
811
+ if (!dateTo) return null;
812
+ return { filterType: "date", type, dateFrom, dateTo };
813
+ }
814
+ return { filterType: "date", type, dateFrom };
815
+ }
816
+ function buildSetFilter(values) {
817
+ if (values.length === 0) return null;
818
+ return { filterType: "set", values };
819
+ }
820
+ function withFilter(model, colId, filter) {
821
+ const next = { ...model };
822
+ if (filter === null) {
823
+ delete next[colId];
824
+ } else {
825
+ next[colId] = filter;
826
+ }
827
+ return next;
828
+ }
829
+ function describeFilter(filter) {
830
+ switch (filter.filterType) {
831
+ case "set":
832
+ return filter.values.length === 1 ? filter.values[0] : `${filter.values.length} selected`;
833
+ case "text":
834
+ return isUnaryFilter(filter.type) ? FILTER_TYPE_LABELS[filter.type] : String(filter.filter ?? "");
835
+ case "number":
836
+ if (isUnaryFilter(filter.type)) return FILTER_TYPE_LABELS[filter.type];
837
+ return isRangeFilter(filter.type) ? `${filter.filter} - ${filter.filterTo}` : `${FILTER_TYPE_LABELS[filter.type]} ${filter.filter}`;
838
+ case "date":
839
+ if (isUnaryFilter(filter.type)) return FILTER_TYPE_LABELS[filter.type];
840
+ return isRangeFilter(filter.type) ? `${filter.dateFrom} - ${filter.dateTo}` : `${FILTER_TYPE_LABELS[filter.type]} ${filter.dateFrom}`;
841
+ }
842
+ }
843
+ function ColumnsPanel({
844
+ columns,
845
+ isHidden,
846
+ onToggle,
847
+ onMove,
848
+ onPin,
849
+ onReset,
850
+ onClose
851
+ }) {
852
+ const [search, setSearch] = react.useState("");
853
+ const filtered = react.useMemo(() => {
854
+ const needle = search.trim().toLowerCase();
855
+ if (!needle) return columns;
856
+ return columns.filter(
857
+ (column) => headerText(column).toLowerCase().includes(needle)
858
+ );
859
+ }, [columns, search]);
860
+ const visibleCount = columns.filter((c) => !isHidden(c.colId)).length;
861
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full w-60 shrink-0 flex-col gap-2 border-r border-gray-200 bg-white p-2 dark:border-gray-700 dark:bg-gray-900", children: [
862
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
863
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-semibold uppercase tracking-wide text-gray-600 dark:text-gray-300", children: "Columns" }),
864
+ /* @__PURE__ */ jsxRuntime.jsx(
865
+ "button",
866
+ {
867
+ type: "button",
868
+ "aria-label": "Close columns panel",
869
+ className: "rounded px-1.5 text-sm text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/5",
870
+ onClick: onClose,
871
+ children: "\xD7"
872
+ }
873
+ )
874
+ ] }),
875
+ /* @__PURE__ */ jsxRuntime.jsx(
876
+ "input",
877
+ {
878
+ type: "search",
879
+ className: "h-7 w-full rounded border border-gray-300 bg-white px-2 text-xs text-gray-800 outline-none focus:border-brand-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100",
880
+ placeholder: "Search columns...",
881
+ value: search,
882
+ onChange: (event) => setSearch(event.target.value)
883
+ }
884
+ ),
885
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-[11px] text-gray-500 dark:text-gray-400", children: [
886
+ visibleCount,
887
+ " of ",
888
+ columns.length,
889
+ " shown"
890
+ ] }),
891
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: filtered.map((column) => {
892
+ const hidden = isHidden(column.colId);
893
+ const index = columns.findIndex((c) => c.colId === column.colId);
894
+ return /* @__PURE__ */ jsxRuntime.jsxs(
895
+ "div",
896
+ {
897
+ className: "group flex items-center gap-1 rounded px-1 py-1 hover:bg-gray-100 dark:hover:bg-white/5",
898
+ draggable: true,
899
+ onDragStart: (event) => event.dataTransfer.setData("text/plain", column.colId),
900
+ onDragOver: (event) => event.preventDefault(),
901
+ onDrop: (event) => {
902
+ event.preventDefault();
903
+ const dragged = event.dataTransfer.getData("text/plain");
904
+ if (dragged && dragged !== column.colId) onMove(dragged, index);
905
+ },
906
+ children: [
907
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cursor-grab text-[10px] text-gray-400", "aria-hidden": true, children: "\u283F" }),
908
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex min-w-0 flex-1 cursor-pointer items-center gap-1.5 text-xs text-gray-700 dark:text-gray-200", children: [
909
+ /* @__PURE__ */ jsxRuntime.jsx(
910
+ "input",
911
+ {
912
+ type: "checkbox",
913
+ className: "h-3.5 w-3.5 accent-brand-500",
914
+ checked: !hidden,
915
+ onChange: () => onToggle(column.colId, !hidden)
916
+ }
917
+ ),
918
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: headerText(column) })
919
+ ] }),
920
+ /* @__PURE__ */ jsxRuntime.jsx(
921
+ "button",
922
+ {
923
+ type: "button",
924
+ className: `shrink-0 rounded px-1 text-[10px] ${column.pinned ? "text-brand-500 dark:text-brand-400" : "text-gray-400 opacity-0 group-hover:opacity-100"}`,
925
+ title: column.pinned ? `Unpin (${column.pinned})` : "Pin left",
926
+ onClick: () => onPin(column.colId, column.pinned ? void 0 : "left"),
927
+ children: "\u{1F4CC}"
928
+ }
929
+ )
930
+ ]
931
+ },
932
+ column.colId
933
+ );
934
+ }) }),
935
+ /* @__PURE__ */ jsxRuntime.jsx(
936
+ "button",
937
+ {
938
+ type: "button",
939
+ className: "rounded border border-error-300 px-2 py-1 text-xs font-medium text-error-600 hover:bg-error-50 dark:border-error-700 dark:text-error-400 dark:hover:bg-error-500/10",
940
+ onClick: onReset,
941
+ children: "Reset to defaults"
942
+ }
943
+ )
944
+ ] });
945
+ }
946
+ var SELECTION_COLUMN_WIDTH = 40;
947
+ function SelectionCell({
948
+ checked,
949
+ indeterminate,
950
+ onToggle,
951
+ isHeader,
952
+ label
953
+ }) {
954
+ return /* @__PURE__ */ jsxRuntime.jsx(
955
+ "div",
956
+ {
957
+ className: `sticky left-0 z-[3] flex h-full items-center justify-center border-r border-gray-200 dark:border-gray-700 ${isHeader ? "bg-gray-100 dark:bg-gray-800" : "bg-inherit"}`,
958
+ style: { width: SELECTION_COLUMN_WIDTH, minWidth: SELECTION_COLUMN_WIDTH },
959
+ onClick: (event) => event.stopPropagation(),
960
+ onDoubleClick: (event) => event.stopPropagation(),
961
+ children: /* @__PURE__ */ jsxRuntime.jsx(
962
+ "input",
963
+ {
964
+ type: "checkbox",
965
+ className: "h-3.5 w-3.5 cursor-pointer accent-brand-500",
966
+ checked,
967
+ "aria-label": label ?? (isHeader ? "Select all rows" : "Select row"),
968
+ ref: (node) => {
969
+ if (node) node.indeterminate = indeterminate === true;
970
+ },
971
+ onChange: () => void 0,
972
+ onClick: (event) => onToggle(event.shiftKey)
973
+ }
974
+ )
975
+ }
976
+ );
977
+ }
978
+ var CELL_INPUT_CLASS = "h-6 w-full rounded border border-gray-300 bg-white px-1.5 text-[11px] text-gray-800 outline-none focus:border-brand-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100";
979
+ function FloatingCell({
980
+ colId,
981
+ kind,
982
+ filter,
983
+ onFilterChange
984
+ }) {
985
+ const isFreeText = kind === "text";
986
+ const [draft, setDraft] = react.useState(
987
+ filter?.filterType === "text" ? filter.filter ?? "" : ""
988
+ );
989
+ react.useEffect(() => {
990
+ if (!isFreeText) return;
991
+ setDraft(filter?.filterType === "text" ? filter.filter ?? "" : "");
992
+ }, [filter, isFreeText]);
993
+ if (!isFreeText) {
994
+ const summary = filter ? describeFilter(filter) : "";
995
+ return /* @__PURE__ */ jsxRuntime.jsx(
996
+ "div",
997
+ {
998
+ className: "flex h-6 items-center truncate rounded border border-dashed border-gray-300 px-1.5 text-[11px] text-gray-500 dark:border-gray-600 dark:text-gray-400",
999
+ title: summary,
1000
+ children: summary || /* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-50", children: "--" })
1001
+ }
1002
+ );
1003
+ }
1004
+ return /* @__PURE__ */ jsxRuntime.jsx(
1005
+ "input",
1006
+ {
1007
+ type: "search",
1008
+ className: CELL_INPUT_CLASS,
1009
+ value: draft,
1010
+ "aria-label": `Filter ${colId}`,
1011
+ onChange: (event) => {
1012
+ const next = event.target.value;
1013
+ setDraft(next);
1014
+ onFilterChange(colId, buildTextFilter("contains", next));
1015
+ }
1016
+ }
1017
+ );
1018
+ }
1019
+ function FloatingFilterRow({
1020
+ layout,
1021
+ filterModel,
1022
+ height,
1023
+ selectable,
1024
+ onFilterChange
1025
+ }) {
1026
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1027
+ "div",
1028
+ {
1029
+ role: "row",
1030
+ className: "sticky z-10 flex border-b border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800/60",
1031
+ style: {
1032
+ height,
1033
+ width: layout.totalWidth + (selectable ? SELECTION_COLUMN_WIDTH : 0)
1034
+ },
1035
+ children: [
1036
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(
1037
+ "div",
1038
+ {
1039
+ className: "sticky left-0 z-[3] h-full border-r border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800",
1040
+ style: { width: SELECTION_COLUMN_WIDTH, minWidth: SELECTION_COLUMN_WIDTH }
1041
+ }
1042
+ ),
1043
+ layout.items.map(({ column, width, pinned, stickyOffset }) => {
1044
+ const style = {
1045
+ width,
1046
+ minWidth: width,
1047
+ maxWidth: width,
1048
+ ...pinned === "left" ? { position: "sticky", left: stickyOffset, zIndex: 3 } : pinned === "right" ? { position: "sticky", right: stickyOffset, zIndex: 3 } : null
1049
+ };
1050
+ const showInput = column.filter && !column.filterParams?.suppressFloatingFilter;
1051
+ return /* @__PURE__ */ jsxRuntime.jsx(
1052
+ "div",
1053
+ {
1054
+ className: `flex h-full items-center border-r border-gray-200 px-1 dark:border-gray-700 ${pinned ? "bg-gray-50 dark:bg-gray-800" : ""}`,
1055
+ style,
1056
+ children: showInput && column.filter && /* @__PURE__ */ jsxRuntime.jsx(
1057
+ FloatingCell,
1058
+ {
1059
+ colId: column.colId,
1060
+ kind: column.filter,
1061
+ filter: filterModel[column.colId],
1062
+ onFilterChange
1063
+ }
1064
+ )
1065
+ },
1066
+ column.colId
1067
+ );
1068
+ })
1069
+ ]
1070
+ }
1071
+ );
1072
+ }
1073
+ var FIELD_CLASS = "h-8 w-full rounded border border-gray-300 bg-white px-2 text-xs text-gray-800 outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100";
1074
+ var BUTTON_CLASS = "rounded px-2.5 py-1 text-xs font-medium transition-colors disabled:opacity-50";
1075
+ function SetFilterBody({
1076
+ value,
1077
+ setValues,
1078
+ onApply
1079
+ }) {
1080
+ const [options, setOptions] = react.useState(
1081
+ Array.isArray(setValues) ? setValues : []
1082
+ );
1083
+ const [isLoading, setIsLoading] = react.useState(typeof setValues === "function");
1084
+ const [search, setSearch] = react.useState("");
1085
+ const [selected, setSelected] = react.useState(
1086
+ () => new Set(value?.filterType === "set" ? value.values : [])
1087
+ );
1088
+ react.useEffect(() => {
1089
+ if (typeof setValues !== "function") {
1090
+ setOptions(Array.isArray(setValues) ? setValues : []);
1091
+ return;
1092
+ }
1093
+ let cancelled = false;
1094
+ setIsLoading(true);
1095
+ setValues().then((loaded) => {
1096
+ if (!cancelled) setOptions(loaded);
1097
+ }).catch(() => {
1098
+ if (!cancelled) setOptions([]);
1099
+ }).finally(() => {
1100
+ if (!cancelled) setIsLoading(false);
1101
+ });
1102
+ return () => {
1103
+ cancelled = true;
1104
+ };
1105
+ }, [setValues]);
1106
+ const filtered = react.useMemo(() => {
1107
+ const needle = search.trim().toLowerCase();
1108
+ return needle ? options.filter((o) => o.toLowerCase().includes(needle)) : options;
1109
+ }, [options, search]);
1110
+ const toggle = (option) => {
1111
+ setSelected((current) => {
1112
+ const next = new Set(current);
1113
+ if (next.has(option)) next.delete(option);
1114
+ else next.add(option);
1115
+ return next;
1116
+ });
1117
+ };
1118
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1119
+ /* @__PURE__ */ jsxRuntime.jsx(
1120
+ "input",
1121
+ {
1122
+ type: "search",
1123
+ className: FIELD_CLASS,
1124
+ placeholder: "Search values...",
1125
+ value: search,
1126
+ onChange: (event) => setSearch(event.target.value)
1127
+ }
1128
+ ),
1129
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-h-52 overflow-y-auto rounded border border-gray-200 dark:border-gray-700", children: [
1130
+ isLoading && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "p-2 text-xs text-gray-500 dark:text-gray-400", children: "Loading..." }),
1131
+ !isLoading && filtered.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "p-2 text-xs text-gray-500 dark:text-gray-400", children: "No values." }),
1132
+ filtered.map((option) => /* @__PURE__ */ jsxRuntime.jsxs(
1133
+ "label",
1134
+ {
1135
+ className: "flex cursor-pointer items-center gap-2 px-2 py-1 text-xs text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-white/5",
1136
+ children: [
1137
+ /* @__PURE__ */ jsxRuntime.jsx(
1138
+ "input",
1139
+ {
1140
+ type: "checkbox",
1141
+ className: "h-3.5 w-3.5 accent-brand-500",
1142
+ checked: selected.has(option),
1143
+ onChange: () => toggle(option)
1144
+ }
1145
+ ),
1146
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: option })
1147
+ ]
1148
+ },
1149
+ option
1150
+ ))
1151
+ ] }),
1152
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between gap-2", children: [
1153
+ /* @__PURE__ */ jsxRuntime.jsx(
1154
+ "button",
1155
+ {
1156
+ type: "button",
1157
+ className: `${BUTTON_CLASS} text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5`,
1158
+ onClick: () => {
1159
+ setSelected(/* @__PURE__ */ new Set());
1160
+ onApply(null);
1161
+ },
1162
+ children: "Clear"
1163
+ }
1164
+ ),
1165
+ /* @__PURE__ */ jsxRuntime.jsx(
1166
+ "button",
1167
+ {
1168
+ type: "button",
1169
+ className: `${BUTTON_CLASS} bg-brand-500 text-white hover:bg-brand-600`,
1170
+ onClick: () => onApply(buildSetFilter([...selected])),
1171
+ children: "Apply"
1172
+ }
1173
+ )
1174
+ ] })
1175
+ ] });
1176
+ }
1177
+ function FilterPopover({
1178
+ kind,
1179
+ value,
1180
+ setValues,
1181
+ onApply,
1182
+ onClose
1183
+ }) {
1184
+ const containerRef = react.useRef(null);
1185
+ const types = kind === "text" ? TEXT_FILTER_TYPES : kind === "number" ? NUMBER_FILTER_TYPES : DATE_FILTER_TYPES;
1186
+ const [type, setType] = react.useState(() => {
1187
+ if (value && value.filterType !== "set") return value.type;
1188
+ return types[0];
1189
+ });
1190
+ const [operand, setOperand] = react.useState(() => {
1191
+ if (!value) return "";
1192
+ if (value.filterType === "text") return value.filter ?? "";
1193
+ if (value.filterType === "number") return value.filter?.toString() ?? "";
1194
+ if (value.filterType === "date") return value.dateFrom ?? "";
1195
+ return "";
1196
+ });
1197
+ const [operandTo, setOperandTo] = react.useState(() => {
1198
+ if (!value) return "";
1199
+ if (value.filterType === "number") return value.filterTo?.toString() ?? "";
1200
+ if (value.filterType === "date") return value.dateTo ?? "";
1201
+ return "";
1202
+ });
1203
+ react.useEffect(() => {
1204
+ const onPointerDown = (event) => {
1205
+ if (!containerRef.current?.contains(event.target)) onClose();
1206
+ };
1207
+ const onKeyDown = (event) => {
1208
+ if (event.key === "Escape") onClose();
1209
+ };
1210
+ document.addEventListener("mousedown", onPointerDown);
1211
+ document.addEventListener("keydown", onKeyDown);
1212
+ return () => {
1213
+ document.removeEventListener("mousedown", onPointerDown);
1214
+ document.removeEventListener("keydown", onKeyDown);
1215
+ };
1216
+ }, [onClose]);
1217
+ const apply = () => {
1218
+ if (kind === "text") {
1219
+ onApply(buildTextFilter(type, operand));
1220
+ } else if (kind === "number") {
1221
+ onApply(buildNumberFilter(type, operand, operandTo));
1222
+ } else {
1223
+ onApply(buildDateFilter(type, operand, operandTo));
1224
+ }
1225
+ };
1226
+ const inputType = kind === "date" ? "date" : kind === "number" ? "number" : "text";
1227
+ return /* @__PURE__ */ jsxRuntime.jsx(
1228
+ "div",
1229
+ {
1230
+ ref: containerRef,
1231
+ className: "absolute right-0 top-full z-30 mt-1 flex w-60 flex-col gap-2 rounded-md border border-gray-200 bg-white p-2 shadow-theme-lg dark:border-gray-700 dark:bg-gray-900",
1232
+ onClick: (event) => event.stopPropagation(),
1233
+ children: kind === "set" ? /* @__PURE__ */ jsxRuntime.jsx(SetFilterBody, { value, setValues, onApply }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1234
+ /* @__PURE__ */ jsxRuntime.jsx(
1235
+ "select",
1236
+ {
1237
+ className: FIELD_CLASS,
1238
+ value: type,
1239
+ onChange: (event) => setType(event.target.value),
1240
+ children: types.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option, children: FILTER_TYPE_LABELS[option] ?? option }, option))
1241
+ }
1242
+ ),
1243
+ !isUnaryFilter(type) && /* @__PURE__ */ jsxRuntime.jsx(
1244
+ "input",
1245
+ {
1246
+ type: inputType,
1247
+ className: FIELD_CLASS,
1248
+ value: operand,
1249
+ autoFocus: true,
1250
+ placeholder: "Value",
1251
+ onChange: (event) => setOperand(event.target.value),
1252
+ onKeyDown: (event) => {
1253
+ if (event.key === "Enter") apply();
1254
+ }
1255
+ }
1256
+ ),
1257
+ isRangeFilter(type) && /* @__PURE__ */ jsxRuntime.jsx(
1258
+ "input",
1259
+ {
1260
+ type: inputType,
1261
+ className: FIELD_CLASS,
1262
+ value: operandTo,
1263
+ placeholder: "To",
1264
+ onChange: (event) => setOperandTo(event.target.value),
1265
+ onKeyDown: (event) => {
1266
+ if (event.key === "Enter") apply();
1267
+ }
1268
+ }
1269
+ ),
1270
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between gap-2", children: [
1271
+ /* @__PURE__ */ jsxRuntime.jsx(
1272
+ "button",
1273
+ {
1274
+ type: "button",
1275
+ className: `${BUTTON_CLASS} text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5`,
1276
+ onClick: () => {
1277
+ setOperand("");
1278
+ setOperandTo("");
1279
+ onApply(null);
1280
+ },
1281
+ children: "Clear"
1282
+ }
1283
+ ),
1284
+ /* @__PURE__ */ jsxRuntime.jsx(
1285
+ "button",
1286
+ {
1287
+ type: "button",
1288
+ className: `${BUTTON_CLASS} bg-brand-500 text-white hover:bg-brand-600`,
1289
+ onClick: apply,
1290
+ children: "Apply"
1291
+ }
1292
+ )
1293
+ ] })
1294
+ ] })
1295
+ }
1296
+ );
1297
+ }
1298
+ function SortIndicator({
1299
+ direction,
1300
+ index,
1301
+ showIndex
1302
+ }) {
1303
+ if (!direction) {
1304
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-0 transition-opacity group-hover:opacity-40", "aria-hidden": true, children: "\u2191" });
1305
+ }
1306
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center text-brand-500 dark:text-brand-400", "aria-hidden": true, children: [
1307
+ direction === "asc" ? "\u2191" : "\u2193",
1308
+ showIndex && /* @__PURE__ */ jsxRuntime.jsx("sub", { className: "ml-0.5 text-[9px]", children: index + 1 })
1309
+ ] });
1310
+ }
1311
+ function GridHeader({
1312
+ layout,
1313
+ sortModel,
1314
+ filterModel,
1315
+ headerHeight,
1316
+ selectable,
1317
+ allSelected,
1318
+ someSelected,
1319
+ onToggleAll,
1320
+ onSort,
1321
+ onFilterChange,
1322
+ onResize,
1323
+ onMove,
1324
+ onPin
1325
+ }) {
1326
+ const [openFilter, setOpenFilter] = react.useState(null);
1327
+ const [openMenu, setOpenMenu] = react.useState(null);
1328
+ const [dragColId, setDragColId] = react.useState(null);
1329
+ const resizeRef = react.useRef(
1330
+ null
1331
+ );
1332
+ const startResize = react.useCallback(
1333
+ (event, item) => {
1334
+ event.preventDefault();
1335
+ event.stopPropagation();
1336
+ resizeRef.current = {
1337
+ colId: item.colId,
1338
+ startX: event.clientX,
1339
+ startWidth: item.width
1340
+ };
1341
+ event.target.setPointerCapture(event.pointerId);
1342
+ },
1343
+ []
1344
+ );
1345
+ const onResizeMove = react.useCallback(
1346
+ (event) => {
1347
+ const active = resizeRef.current;
1348
+ if (!active) return;
1349
+ const delta = event.clientX - active.startX;
1350
+ onResize(active.colId, Math.max(40, active.startWidth + delta));
1351
+ },
1352
+ [onResize]
1353
+ );
1354
+ const endResize = react.useCallback((event) => {
1355
+ if (!resizeRef.current) return;
1356
+ resizeRef.current = null;
1357
+ event.target.releasePointerCapture(event.pointerId);
1358
+ }, []);
1359
+ const sortIndexOf = (colId) => sortModel.findIndex((entry) => entry.colId === colId);
1360
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1361
+ "div",
1362
+ {
1363
+ role: "row",
1364
+ className: "sticky top-0 z-10 flex border-b border-gray-300 bg-gray-100 dark:border-gray-600 dark:bg-gray-800",
1365
+ style: {
1366
+ height: headerHeight,
1367
+ width: layout.totalWidth + (selectable ? SELECTION_COLUMN_WIDTH : 0)
1368
+ },
1369
+ children: [
1370
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(
1371
+ SelectionCell,
1372
+ {
1373
+ isHeader: true,
1374
+ checked: allSelected,
1375
+ indeterminate: someSelected,
1376
+ onToggle: onToggleAll,
1377
+ label: "Select all rows on this page"
1378
+ }
1379
+ ),
1380
+ layout.items.map((item, index) => {
1381
+ const { column, width, pinned, stickyOffset } = item;
1382
+ const sortIndex = sortIndexOf(column.colId);
1383
+ const direction = sortIndex === -1 ? void 0 : sortModel[sortIndex].sort;
1384
+ const activeFilter = filterModel[column.colId];
1385
+ const filterKind = column.filter;
1386
+ const style = {
1387
+ width,
1388
+ minWidth: width,
1389
+ maxWidth: width,
1390
+ ...pinned === "left" ? { position: "sticky", left: stickyOffset, zIndex: 3 } : pinned === "right" ? { position: "sticky", right: stickyOffset, zIndex: 3 } : null
1391
+ };
1392
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1393
+ "div",
1394
+ {
1395
+ role: "columnheader",
1396
+ "aria-sort": direction === "asc" ? "ascending" : direction === "desc" ? "descending" : "none",
1397
+ "aria-colindex": index + 1,
1398
+ className: `group relative flex h-full items-center border-r border-gray-300 bg-gray-100 px-2 dark:border-gray-600 dark:bg-gray-800 ${dragColId === column.colId ? "opacity-40" : ""} ${column.headerClassName ?? ""}`,
1399
+ style,
1400
+ draggable: !column.lockPosition,
1401
+ onDragStart: (event) => {
1402
+ setDragColId(column.colId);
1403
+ event.dataTransfer.effectAllowed = "move";
1404
+ },
1405
+ onDragOver: (event) => {
1406
+ if (dragColId && dragColId !== column.colId) event.preventDefault();
1407
+ },
1408
+ onDrop: (event) => {
1409
+ event.preventDefault();
1410
+ if (dragColId && dragColId !== column.colId) onMove(dragColId, index);
1411
+ setDragColId(null);
1412
+ },
1413
+ onDragEnd: () => setDragColId(null),
1414
+ children: [
1415
+ /* @__PURE__ */ jsxRuntime.jsxs(
1416
+ "button",
1417
+ {
1418
+ type: "button",
1419
+ className: "flex min-w-0 flex-1 cursor-pointer items-center gap-1 text-left text-xs font-semibold uppercase tracking-wide text-gray-600 dark:text-gray-300",
1420
+ disabled: !column.sortable,
1421
+ onClick: (event) => column.sortable && onSort(column.colId, event.shiftKey),
1422
+ title: column.sortable ? "Sort (shift-click to add)" : void 0,
1423
+ children: [
1424
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: column.header }),
1425
+ column.sortable && /* @__PURE__ */ jsxRuntime.jsx(
1426
+ SortIndicator,
1427
+ {
1428
+ direction,
1429
+ index: sortIndex,
1430
+ showIndex: sortModel.length > 1
1431
+ }
1432
+ )
1433
+ ]
1434
+ }
1435
+ ),
1436
+ filterKind && /* @__PURE__ */ jsxRuntime.jsx(
1437
+ "button",
1438
+ {
1439
+ type: "button",
1440
+ "aria-label": `Filter ${column.colId}`,
1441
+ className: `ml-1 shrink-0 rounded px-1 text-[10px] leading-none ${activeFilter ? "text-brand-500 dark:text-brand-400" : "text-gray-400 opacity-0 group-hover:opacity-100"}`,
1442
+ title: activeFilter ? describeFilter(activeFilter) : "Filter",
1443
+ onClick: (event) => {
1444
+ event.stopPropagation();
1445
+ setOpenMenu(null);
1446
+ setOpenFilter(
1447
+ (current) => current === column.colId ? null : column.colId
1448
+ );
1449
+ },
1450
+ children: "\u25BC"
1451
+ }
1452
+ ),
1453
+ /* @__PURE__ */ jsxRuntime.jsx(
1454
+ "button",
1455
+ {
1456
+ type: "button",
1457
+ "aria-label": `Options for ${column.colId}`,
1458
+ className: "ml-0.5 shrink-0 rounded px-1 text-[10px] leading-none text-gray-400 opacity-0 group-hover:opacity-100",
1459
+ onClick: (event) => {
1460
+ event.stopPropagation();
1461
+ setOpenFilter(null);
1462
+ setOpenMenu((current) => current === column.colId ? null : column.colId);
1463
+ },
1464
+ children: "\u22EE"
1465
+ }
1466
+ ),
1467
+ openFilter === column.colId && filterKind && /* @__PURE__ */ jsxRuntime.jsx(
1468
+ FilterPopover,
1469
+ {
1470
+ kind: filterKind,
1471
+ value: activeFilter,
1472
+ setValues: column.filterParams?.values,
1473
+ onApply: (filter) => {
1474
+ onFilterChange(column.colId, filter);
1475
+ setOpenFilter(null);
1476
+ },
1477
+ onClose: () => setOpenFilter(null)
1478
+ }
1479
+ ),
1480
+ openMenu === column.colId && /* @__PURE__ */ jsxRuntime.jsx(
1481
+ "div",
1482
+ {
1483
+ className: "absolute right-0 top-full z-30 mt-1 w-36 rounded-md border border-gray-200 bg-white py-1 shadow-theme-lg dark:border-gray-700 dark:bg-gray-900",
1484
+ onMouseLeave: () => setOpenMenu(null),
1485
+ children: ["left", "right"].map((side) => /* @__PURE__ */ jsxRuntime.jsx(
1486
+ "button",
1487
+ {
1488
+ type: "button",
1489
+ className: "block w-full px-3 py-1 text-left text-xs text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-white/5",
1490
+ onClick: () => {
1491
+ onPin(column.colId, pinned === side ? void 0 : side);
1492
+ setOpenMenu(null);
1493
+ },
1494
+ children: pinned === side ? `Unpin ${side}` : `Pin ${side}`
1495
+ },
1496
+ side
1497
+ ))
1498
+ }
1499
+ ),
1500
+ column.resizable && /* @__PURE__ */ jsxRuntime.jsx(
1501
+ "div",
1502
+ {
1503
+ role: "separator",
1504
+ "aria-orientation": "vertical",
1505
+ className: "absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-brand-400",
1506
+ onPointerDown: (event) => startResize(event, item),
1507
+ onPointerMove: onResizeMove,
1508
+ onPointerUp: endResize,
1509
+ onPointerCancel: endResize,
1510
+ onClick: (event) => event.stopPropagation()
1511
+ }
1512
+ )
1513
+ ]
1514
+ },
1515
+ column.colId
1516
+ );
1517
+ })
1518
+ ]
1519
+ }
1520
+ );
1521
+ }
1522
+ function GridOverlay({ kind, message }) {
1523
+ if (kind === "loading") {
1524
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-none absolute inset-0 z-[5] flex items-start justify-center bg-white/60 pt-10 dark:bg-gray-900/60", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 rounded-md border border-gray-200 bg-white px-3 py-1.5 text-xs text-gray-600 shadow-theme-sm dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300", children: [
1525
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-3 w-3 animate-spin rounded-full border-2 border-brand-400 border-t-transparent" }),
1526
+ message ?? "Loading..."
1527
+ ] }) });
1528
+ }
1529
+ if (kind === "error") {
1530
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 z-[5] flex items-start justify-center bg-white/80 pt-10 dark:bg-gray-900/80", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-w-md rounded-md border border-error-300 bg-error-50 px-3 py-2 text-xs text-error-700 dark:border-error-700 dark:bg-error-500/10 dark:text-error-400", children: message ?? "Something went wrong loading this grid." }) });
1531
+ }
1532
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 z-[5] flex items-start justify-center pt-10", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: message ?? "No records found" }) });
1533
+ }
1534
+ var BUTTON_CLASS2 = "rounded border border-gray-300 px-2 py-0.5 text-xs text-gray-700 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-white/5";
1535
+ function GridPagination({
1536
+ page,
1537
+ pageSize,
1538
+ totalRows,
1539
+ pageSizeOptions,
1540
+ isLoading,
1541
+ onPageChange,
1542
+ onPageSizeChange
1543
+ }) {
1544
+ const pageCount = Math.max(1, Math.ceil(totalRows / pageSize));
1545
+ const first = totalRows === 0 ? 0 : page * pageSize + 1;
1546
+ const last = Math.min(totalRows, (page + 1) * pageSize);
1547
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 border-t border-gray-200 bg-gray-50 px-2 py-1.5 dark:border-gray-700 dark:bg-gray-800/60", children: [
1548
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1549
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "text-xs text-gray-600 dark:text-gray-300", children: [
1550
+ "Rows",
1551
+ /* @__PURE__ */ jsxRuntime.jsx(
1552
+ "select",
1553
+ {
1554
+ className: "ml-1 rounded border border-gray-300 bg-white px-1 py-0.5 text-xs dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100",
1555
+ value: pageSize,
1556
+ onChange: (event) => onPageSizeChange(Number(event.target.value)),
1557
+ children: pageSizeOptions.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option, children: option }, option))
1558
+ }
1559
+ )
1560
+ ] }),
1561
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-600 dark:text-gray-300", children: isLoading ? "Loading..." : `${first}-${last} of ${totalRows}` })
1562
+ ] }),
1563
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
1564
+ /* @__PURE__ */ jsxRuntime.jsx(
1565
+ "button",
1566
+ {
1567
+ type: "button",
1568
+ className: BUTTON_CLASS2,
1569
+ disabled: page === 0,
1570
+ onClick: () => onPageChange(0),
1571
+ children: "\xAB First"
1572
+ }
1573
+ ),
1574
+ /* @__PURE__ */ jsxRuntime.jsx(
1575
+ "button",
1576
+ {
1577
+ type: "button",
1578
+ className: BUTTON_CLASS2,
1579
+ disabled: page === 0,
1580
+ onClick: () => onPageChange(page - 1),
1581
+ children: "\u2039 Prev"
1582
+ }
1583
+ ),
1584
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "px-1 text-xs text-gray-600 dark:text-gray-300", children: [
1585
+ "Page ",
1586
+ page + 1,
1587
+ " of ",
1588
+ pageCount
1589
+ ] }),
1590
+ /* @__PURE__ */ jsxRuntime.jsx(
1591
+ "button",
1592
+ {
1593
+ type: "button",
1594
+ className: BUTTON_CLASS2,
1595
+ disabled: page + 1 >= pageCount,
1596
+ onClick: () => onPageChange(page + 1),
1597
+ children: "Next \u203A"
1598
+ }
1599
+ ),
1600
+ /* @__PURE__ */ jsxRuntime.jsx(
1601
+ "button",
1602
+ {
1603
+ type: "button",
1604
+ className: BUTTON_CLASS2,
1605
+ disabled: page + 1 >= pageCount,
1606
+ onClick: () => onPageChange(pageCount - 1),
1607
+ children: "Last \xBB"
1608
+ }
1609
+ )
1610
+ ] })
1611
+ ] });
1612
+ }
1613
+ var INPUT_CLASS = "h-7 w-full rounded border px-1.5 text-xs outline-none border-gray-300 bg-white text-gray-800 focus:border-brand-500 focus:ring-1 focus:ring-brand-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100";
1614
+ var ERROR_CLASS = "!border-error-500 focus:!border-error-500 focus:!ring-error-500";
1615
+ function className(error) {
1616
+ return error ? `${INPUT_CLASS} ${ERROR_CLASS}` : INPUT_CLASS;
1617
+ }
1618
+ function keyHandler(params) {
1619
+ return (event) => {
1620
+ if (event.key === "Enter") {
1621
+ event.preventDefault();
1622
+ event.stopPropagation();
1623
+ params.onCommit();
1624
+ } else if (event.key === "Escape") {
1625
+ event.preventDefault();
1626
+ event.stopPropagation();
1627
+ params.onCancel();
1628
+ }
1629
+ };
1630
+ }
1631
+ function TextEditor({
1632
+ value,
1633
+ onChange,
1634
+ onCommit,
1635
+ onCancel,
1636
+ error,
1637
+ autoFocus,
1638
+ column
1639
+ }) {
1640
+ return /* @__PURE__ */ jsxRuntime.jsx(
1641
+ "input",
1642
+ {
1643
+ type: "text",
1644
+ className: className(error),
1645
+ value: value == null ? "" : String(value),
1646
+ autoFocus,
1647
+ placeholder: column.editorParams?.placeholder,
1648
+ title: error,
1649
+ "aria-invalid": error ? true : void 0,
1650
+ onChange: (event) => onChange(event.target.value),
1651
+ onKeyDown: keyHandler({ onCommit, onCancel })
1652
+ }
1653
+ );
1654
+ }
1655
+ function NumberEditor({
1656
+ value,
1657
+ onChange,
1658
+ onCommit,
1659
+ onCancel,
1660
+ error,
1661
+ autoFocus
1662
+ }) {
1663
+ return /* @__PURE__ */ jsxRuntime.jsx(
1664
+ "input",
1665
+ {
1666
+ type: "number",
1667
+ className: className(error),
1668
+ value: value == null ? "" : String(value),
1669
+ autoFocus,
1670
+ title: error,
1671
+ "aria-invalid": error ? true : void 0,
1672
+ onChange: (event) => onChange(event.target.value === "" ? null : Number(event.target.value)),
1673
+ onKeyDown: keyHandler({ onCommit, onCancel })
1674
+ }
1675
+ );
1676
+ }
1677
+ function DateEditor({
1678
+ value,
1679
+ onChange,
1680
+ onCommit,
1681
+ onCancel,
1682
+ error,
1683
+ autoFocus
1684
+ }) {
1685
+ const asInputValue = (() => {
1686
+ if (value == null || value === "") return "";
1687
+ const date = value instanceof Date ? value : new Date(String(value));
1688
+ return Number.isNaN(date.getTime()) ? "" : date.toISOString().slice(0, 10);
1689
+ })();
1690
+ return /* @__PURE__ */ jsxRuntime.jsx(
1691
+ "input",
1692
+ {
1693
+ type: "date",
1694
+ className: className(error),
1695
+ value: asInputValue,
1696
+ autoFocus,
1697
+ title: error,
1698
+ "aria-invalid": error ? true : void 0,
1699
+ onChange: (event) => onChange(event.target.value === "" ? null : event.target.value),
1700
+ onKeyDown: keyHandler({ onCommit, onCancel })
1701
+ }
1702
+ );
1703
+ }
1704
+ function SelectEditor({
1705
+ value,
1706
+ onChange,
1707
+ onCommit,
1708
+ onCancel,
1709
+ error,
1710
+ autoFocus,
1711
+ column
1712
+ }) {
1713
+ const options = column.editorParams?.options ?? [];
1714
+ const selectedIndex = options.findIndex((option) => option.value === value);
1715
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1716
+ "select",
1717
+ {
1718
+ className: className(error),
1719
+ value: selectedIndex === -1 ? "" : String(selectedIndex),
1720
+ autoFocus,
1721
+ title: error,
1722
+ "aria-invalid": error ? true : void 0,
1723
+ onChange: (event) => {
1724
+ const index = Number(event.target.value);
1725
+ onChange(Number.isNaN(index) ? null : options[index]?.value ?? null);
1726
+ },
1727
+ onKeyDown: keyHandler({ onCommit, onCancel }),
1728
+ children: [
1729
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "--" }),
1730
+ options.map((option, index) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: index, children: option.label }, `${String(option.value)}-${index}`))
1731
+ ]
1732
+ }
1733
+ );
1734
+ }
1735
+ function CheckboxEditor({
1736
+ value,
1737
+ onChange,
1738
+ onCommit,
1739
+ onCancel,
1740
+ autoFocus
1741
+ }) {
1742
+ return /* @__PURE__ */ jsxRuntime.jsx(
1743
+ "input",
1744
+ {
1745
+ type: "checkbox",
1746
+ className: "h-4 w-4 accent-brand-500",
1747
+ checked: value === true,
1748
+ autoFocus,
1749
+ onChange: (event) => onChange(event.target.checked),
1750
+ onKeyDown: keyHandler({ onCommit, onCancel })
1751
+ }
1752
+ );
1753
+ }
1754
+
1755
+ // src/components/editors/registry.ts
1756
+ var BUILTIN_EDITORS = {
1757
+ text: TextEditor,
1758
+ number: NumberEditor,
1759
+ date: DateEditor,
1760
+ select: SelectEditor,
1761
+ checkbox: CheckboxEditor
1762
+ };
1763
+ var ALIGN_CLASS = {
1764
+ left: "justify-start text-left",
1765
+ center: "justify-center text-center",
1766
+ right: "justify-end text-right"
1767
+ };
1768
+ function GridCellInner({
1769
+ item,
1770
+ row,
1771
+ rowIndex,
1772
+ context,
1773
+ api,
1774
+ isRowEditing,
1775
+ draft,
1776
+ errors,
1777
+ isFirstEditable,
1778
+ onFieldChange,
1779
+ onCommit,
1780
+ onCancel
1781
+ }) {
1782
+ const { column, width, pinned, stickyOffset } = item;
1783
+ const style = {
1784
+ width,
1785
+ minWidth: width,
1786
+ maxWidth: width,
1787
+ ...pinned === "left" ? { position: "sticky", left: stickyOffset, zIndex: 2 } : pinned === "right" ? { position: "sticky", right: stickyOffset, zIndex: 2 } : null
1788
+ };
1789
+ const custom = typeof column.cellClassName === "function" ? column.cellClassName(row) : column.cellClassName;
1790
+ const base = "flex h-full items-center gap-1 border-r border-gray-200 px-2 text-xs text-gray-700 dark:border-gray-700 dark:text-gray-200 " + (pinned ? "bg-white dark:bg-gray-900 " : "");
1791
+ const alignment = ALIGN_CLASS[column.align ?? "left"];
1792
+ const editing = isRowEditing && draft != null && isEditable(column, row);
1793
+ if (editing) {
1794
+ const field = column.field ?? column.colId;
1795
+ const error = errors[field];
1796
+ const editorValue = resolveValue(draft, column);
1797
+ const Editor = typeof column.editor === "function" ? column.editor : BUILTIN_EDITORS[column.editor ?? "text"];
1798
+ return /* @__PURE__ */ jsxRuntime.jsx(
1799
+ "div",
1800
+ {
1801
+ role: "gridcell",
1802
+ className: `${base} ${alignment} ${custom ?? ""}`,
1803
+ style,
1804
+ "data-col-id": column.colId,
1805
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1806
+ Editor,
1807
+ {
1808
+ value: editorValue,
1809
+ row: draft,
1810
+ column,
1811
+ context,
1812
+ error,
1813
+ autoFocus: isFirstEditable,
1814
+ onChange: (value2) => onFieldChange(field, value2),
1815
+ onCommit,
1816
+ onCancel
1817
+ }
1818
+ )
1819
+ }
1820
+ );
1821
+ }
1822
+ const value = resolveValue(row, column);
1823
+ const formatted = formatValue(value, row, column);
1824
+ const content = column.cellRenderer ? column.cellRenderer({
1825
+ row,
1826
+ rowIndex,
1827
+ value,
1828
+ formatted,
1829
+ column,
1830
+ context,
1831
+ api
1832
+ }) : formatted;
1833
+ return /* @__PURE__ */ jsxRuntime.jsx(
1834
+ "div",
1835
+ {
1836
+ role: "gridcell",
1837
+ className: `${base} ${alignment} ${custom ?? ""}`,
1838
+ style,
1839
+ "data-col-id": column.colId,
1840
+ title: column.cellRenderer ? void 0 : formatted || void 0,
1841
+ children: column.cellRenderer ? content : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: formatted })
1842
+ }
1843
+ );
1844
+ }
1845
+ var GridCell = react.memo(GridCellInner);
1846
+ function GridRowInner({
1847
+ row,
1848
+ rowId,
1849
+ rowIndex,
1850
+ top,
1851
+ height,
1852
+ layout,
1853
+ context,
1854
+ api,
1855
+ selectable,
1856
+ isSelected,
1857
+ isRowEditing,
1858
+ draft,
1859
+ errors,
1860
+ isSaving,
1861
+ onToggleSelect,
1862
+ onFieldChange,
1863
+ onCommit,
1864
+ onCancel,
1865
+ onRowDoubleClick,
1866
+ onFilesDropped
1867
+ }) {
1868
+ const [isDropTarget, setIsDropTarget] = react.useState(false);
1869
+ const rowError = errors.__row__;
1870
+ const firstEditableColId = isRowEditing ? layout.items.find((item) => isEditable(item.column, row))?.colId : void 0;
1871
+ const background = isRowEditing ? "bg-brand-25 dark:bg-brand-500/10" : isSelected ? "bg-brand-50 dark:bg-brand-500/15" : rowIndex % 2 === 1 ? "bg-gray-50/60 dark:bg-white/[0.02]" : "bg-white dark:bg-gray-900";
1872
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1873
+ "div",
1874
+ {
1875
+ role: "row",
1876
+ "aria-rowindex": rowIndex + 1,
1877
+ "aria-selected": selectable ? isSelected : void 0,
1878
+ className: `absolute left-0 flex border-b border-gray-200 dark:border-gray-700 ${background} hover:bg-brand-25 dark:hover:bg-white/[0.04] ${isSaving ? "opacity-60" : ""} ${isDropTarget ? "outline-2 -outline-offset-2 outline-dashed outline-brand-500" : ""}`,
1879
+ style: { top, height, width: layout.totalWidth + (selectable ? 40 : 0) },
1880
+ onDoubleClick: () => onRowDoubleClick(rowId, row),
1881
+ title: rowError,
1882
+ onDragOver: onFilesDropped ? (event) => {
1883
+ if (!event.dataTransfer.types.includes("Files")) return;
1884
+ event.preventDefault();
1885
+ event.dataTransfer.dropEffect = "copy";
1886
+ if (!isDropTarget) setIsDropTarget(true);
1887
+ } : void 0,
1888
+ onDragLeave: onFilesDropped ? () => setIsDropTarget(false) : void 0,
1889
+ onDrop: onFilesDropped ? (event) => {
1890
+ if (!event.dataTransfer.types.includes("Files")) return;
1891
+ event.preventDefault();
1892
+ setIsDropTarget(false);
1893
+ const files = Array.from(event.dataTransfer.files);
1894
+ if (files.length > 0) onFilesDropped(row, files);
1895
+ } : void 0,
1896
+ children: [
1897
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(
1898
+ SelectionCell,
1899
+ {
1900
+ checked: isSelected,
1901
+ onToggle: (shiftKey) => onToggleSelect(rowId, rowIndex, shiftKey)
1902
+ }
1903
+ ),
1904
+ layout.items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
1905
+ GridCell,
1906
+ {
1907
+ item,
1908
+ row,
1909
+ rowIndex,
1910
+ context,
1911
+ api,
1912
+ isRowEditing,
1913
+ draft,
1914
+ errors,
1915
+ isFirstEditable: item.colId === firstEditableColId,
1916
+ onFieldChange,
1917
+ onCommit,
1918
+ onCancel
1919
+ },
1920
+ item.colId
1921
+ ))
1922
+ ]
1923
+ }
1924
+ );
1925
+ }
1926
+ var GridRow = react.memo(GridRowInner);
1927
+ var DEFAULT_PAGE_SIZES = [10, 20, 50, 100];
1928
+ function DataGrid({
1929
+ columns,
1930
+ dataSource,
1931
+ getRowId,
1932
+ context,
1933
+ storageKey,
1934
+ rowHeight = 36,
1935
+ headerHeight = 36,
1936
+ floatingFilter = true,
1937
+ selectable = true,
1938
+ defaultPageSize = 20,
1939
+ pageSizeOptions = DEFAULT_PAGE_SIZES,
1940
+ onRowCommit,
1941
+ onSelectionChanged,
1942
+ onRowFilesDropped,
1943
+ onError,
1944
+ toolbar,
1945
+ emptyMessage,
1946
+ exportFileName = "export",
1947
+ className: className2 = "",
1948
+ height = "70vh",
1949
+ apiRef
1950
+ }) {
1951
+ const persisted = useGridState(storageKey);
1952
+ const [sortModel, setSortModel] = react.useState(
1953
+ () => persisted.initial?.sort ?? []
1954
+ );
1955
+ const [filterModel, setFilterModel] = react.useState(
1956
+ () => persisted.initial?.filters ?? {}
1957
+ );
1958
+ const [pageSize, setPageSize] = react.useState(
1959
+ () => persisted.initial?.pagination.pageSize ?? defaultPageSize
1960
+ );
1961
+ const [page, setPage] = react.useState(0);
1962
+ const [showColumnsPanel, setShowColumnsPanel] = react.useState(false);
1963
+ const viewportRef = react.useRef(null);
1964
+ const [viewportWidth, setViewportWidth] = react.useState(0);
1965
+ const floatingFilterHeight = floatingFilter ? 28 : 0;
1966
+ react.useLayoutEffect(() => {
1967
+ const element = viewportRef.current;
1968
+ if (!element) return;
1969
+ const observer = new ResizeObserver((entries) => {
1970
+ const rect = entries[0]?.contentRect;
1971
+ if (!rect) return;
1972
+ setViewportWidth(rect.width);
1973
+ setViewportHeightRef.current(
1974
+ Math.max(0, rect.height - headerHeight - floatingFilterHeight)
1975
+ );
1976
+ });
1977
+ observer.observe(element);
1978
+ return () => observer.disconnect();
1979
+ }, [headerHeight, floatingFilterHeight]);
1980
+ const columnState = useColumnState(
1981
+ columns,
1982
+ persisted.initial?.columns,
1983
+ persisted.saveColumns,
1984
+ viewportWidth - (selectable ? 40 : 0)
1985
+ );
1986
+ const { rows, totalRows, isLoading, error, refresh, patchRows } = useServerDataSource(
1987
+ { dataSource, pageSize, page, sortModel, filterModel, onError }
1988
+ );
1989
+ react.useEffect(() => {
1990
+ if (isLoading || totalRows === 0) return;
1991
+ const pageCount = Math.max(1, Math.ceil(totalRows / pageSize));
1992
+ if (page > pageCount - 1) setPage(pageCount - 1);
1993
+ }, [totalRows, pageSize, page, isLoading]);
1994
+ const virtual = useVirtualRows({ rowCount: rows.length, rowHeight });
1995
+ const setViewportHeightRef = react.useRef(virtual.setViewportHeight);
1996
+ setViewportHeightRef.current = virtual.setViewportHeight;
1997
+ const selection = useSelectionModel(rows, getRowId, onSelectionChanged);
1998
+ const noopCommit = react.useCallback(() => ({ ok: true }), []);
1999
+ const editing = useEditModel(onRowCommit ?? noopCommit);
2000
+ const applySort = react.useCallback(
2001
+ (next) => {
2002
+ setSortModel(next);
2003
+ persisted.saveSort(next);
2004
+ setPage(0);
2005
+ },
2006
+ [persisted]
2007
+ );
2008
+ const applyFilters = react.useCallback(
2009
+ (next) => {
2010
+ setFilterModel(next);
2011
+ persisted.saveFilters(next);
2012
+ setPage(0);
2013
+ },
2014
+ [persisted]
2015
+ );
2016
+ const onSort = react.useCallback(
2017
+ (colId, additive) => {
2018
+ const existing = sortModel.find((entry) => entry.colId === colId);
2019
+ const others = sortModel.filter((entry) => entry.colId !== colId);
2020
+ const next = !existing ? [...additive ? sortModel : [], { colId, sort: "asc" }] : existing.sort === "asc" ? [...additive ? others : [], { colId, sort: "desc" }] : additive ? others : [];
2021
+ applySort(next);
2022
+ },
2023
+ [sortModel, applySort]
2024
+ );
2025
+ const onFilterChange = react.useCallback(
2026
+ (colId, filter) => {
2027
+ applyFilters(withFilter(filterModel, colId, filter));
2028
+ },
2029
+ [filterModel, applyFilters]
2030
+ );
2031
+ const onPageSizeChange = react.useCallback(
2032
+ (next) => {
2033
+ setPageSize(next);
2034
+ persisted.savePageSize(next);
2035
+ setPage(0);
2036
+ },
2037
+ [persisted]
2038
+ );
2039
+ const latest = react.useRef({
2040
+ rows,
2041
+ selection,
2042
+ columnState,
2043
+ filterModel,
2044
+ sortModel,
2045
+ editing,
2046
+ getRowId
2047
+ });
2048
+ latest.current = {
2049
+ rows,
2050
+ selection,
2051
+ columnState,
2052
+ filterModel,
2053
+ sortModel,
2054
+ editing,
2055
+ getRowId
2056
+ };
2057
+ const api = react.useMemo(
2058
+ () => ({
2059
+ refresh: (options) => refresh(options),
2060
+ getDisplayedRows: () => latest.current.rows,
2061
+ getSelectedRows: () => latest.current.selection.getSelectedRows(),
2062
+ getSelectedIds: () => [...latest.current.selection.selectedIds],
2063
+ clearSelection: () => latest.current.selection.clear(),
2064
+ selectAll: () => latest.current.selection.selectAllVisible(),
2065
+ exportCsv: (options) => {
2066
+ const source = options?.onlySelected ? latest.current.selection.getSelectedRows() : latest.current.rows;
2067
+ const csv = toCsv(
2068
+ source,
2069
+ latest.current.columnState.visibleColumns,
2070
+ options?.separator ?? ","
2071
+ );
2072
+ downloadCsv(csv, options?.fileName ?? exportFileName);
2073
+ },
2074
+ copySelectionToClipboard: async () => {
2075
+ const selected = latest.current.selection.getSelectedRows();
2076
+ const source = selected.length > 0 ? selected : latest.current.rows;
2077
+ await copyToClipboard(
2078
+ toTsv(source, latest.current.columnState.visibleColumns)
2079
+ );
2080
+ },
2081
+ getFilterModel: () => latest.current.filterModel,
2082
+ setFilterModel: applyFilters,
2083
+ getSortModel: () => latest.current.sortModel,
2084
+ setSortModel: applySort,
2085
+ resetColumns: () => {
2086
+ latest.current.columnState.reset();
2087
+ persisted.clear();
2088
+ },
2089
+ updateRows: (updated) => patchRows(updated, latest.current.getRowId),
2090
+ startEditing: (rowId) => {
2091
+ const row = latest.current.rows.find(
2092
+ (candidate) => latest.current.getRowId(candidate) === rowId
2093
+ );
2094
+ if (row) latest.current.editing.start(rowId, row);
2095
+ },
2096
+ stopEditing: () => latest.current.editing.cancel()
2097
+ }),
2098
+ [refresh, applyFilters, applySort, patchRows, persisted, exportFileName]
2099
+ );
2100
+ react.useImperativeHandle(apiRef, () => api, [api]);
2101
+ const onRowDoubleClick = react.useCallback(
2102
+ (rowId, row) => {
2103
+ if (onRowCommit) editing.start(rowId, row);
2104
+ },
2105
+ [onRowCommit, editing]
2106
+ );
2107
+ const editingRowId = editing.edit?.rowId ?? null;
2108
+ const cancelEditRef = react.useRef(editing.cancel);
2109
+ cancelEditRef.current = editing.cancel;
2110
+ react.useEffect(() => {
2111
+ if (editingRowId == null) return;
2112
+ const onPointerDown = (event) => {
2113
+ const target = event.target;
2114
+ if (!target.closest('[data-hxg-editing-row="true"]')) cancelEditRef.current();
2115
+ };
2116
+ document.addEventListener("mousedown", onPointerDown);
2117
+ return () => document.removeEventListener("mousedown", onPointerDown);
2118
+ }, [editingRowId]);
2119
+ const emptyContext = react.useMemo(() => ({}), []);
2120
+ const cellContext = context ?? emptyContext;
2121
+ const { startIndex, endIndex } = virtual.window;
2122
+ const visibleRows = rows.slice(startIndex, endIndex);
2123
+ const showEmpty = !isLoading && !error && rows.length === 0;
2124
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2125
+ "div",
2126
+ {
2127
+ className: `flex flex-col overflow-hidden rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900 ${className2}`,
2128
+ children: [
2129
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2 border-b border-gray-200 bg-gray-50 px-2 py-1.5 dark:border-gray-700 dark:bg-gray-800/60", children: [
2130
+ toolbar?.(api),
2131
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
2132
+ /* @__PURE__ */ jsxRuntime.jsx(
2133
+ "button",
2134
+ {
2135
+ type: "button",
2136
+ className: "rounded border border-gray-300 px-2 py-0.5 text-xs text-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-white/5",
2137
+ onClick: () => setShowColumnsPanel((current) => !current),
2138
+ children: "Columns"
2139
+ }
2140
+ ),
2141
+ /* @__PURE__ */ jsxRuntime.jsx(
2142
+ "button",
2143
+ {
2144
+ type: "button",
2145
+ className: "rounded border border-gray-300 px-2 py-0.5 text-xs text-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-white/5",
2146
+ onClick: () => api.exportCsv(),
2147
+ children: "Export CSV"
2148
+ }
2149
+ )
2150
+ ] })
2151
+ ] }),
2152
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1", children: [
2153
+ showColumnsPanel && /* @__PURE__ */ jsxRuntime.jsx(
2154
+ ColumnsPanel,
2155
+ {
2156
+ columns: columnState.allColumns,
2157
+ isHidden: columnState.isHidden,
2158
+ onToggle: columnState.setHidden,
2159
+ onMove: columnState.moveColumn,
2160
+ onPin: columnState.setPinned,
2161
+ onReset: () => {
2162
+ columnState.reset();
2163
+ persisted.clear();
2164
+ },
2165
+ onClose: () => setShowColumnsPanel(false)
2166
+ }
2167
+ ),
2168
+ /* @__PURE__ */ jsxRuntime.jsxs(
2169
+ "div",
2170
+ {
2171
+ ref: viewportRef,
2172
+ role: "grid",
2173
+ "aria-rowcount": totalRows,
2174
+ "aria-colcount": columnState.layout.items.length,
2175
+ "aria-busy": isLoading,
2176
+ className: "relative min-w-0 flex-1 overflow-auto",
2177
+ style: { height },
2178
+ onScroll: virtual.onScroll,
2179
+ children: [
2180
+ /* @__PURE__ */ jsxRuntime.jsx(
2181
+ GridHeader,
2182
+ {
2183
+ layout: columnState.layout,
2184
+ sortModel,
2185
+ filterModel,
2186
+ headerHeight,
2187
+ selectable,
2188
+ allSelected: selection.allVisibleSelected,
2189
+ someSelected: selection.someVisibleSelected,
2190
+ onToggleAll: selection.toggleAllVisible,
2191
+ onSort,
2192
+ onFilterChange,
2193
+ onResize: columnState.setWidth,
2194
+ onMove: columnState.moveColumn,
2195
+ onPin: columnState.setPinned
2196
+ }
2197
+ ),
2198
+ floatingFilter && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { top: headerHeight, position: "sticky", zIndex: 9 }, children: /* @__PURE__ */ jsxRuntime.jsx(
2199
+ FloatingFilterRow,
2200
+ {
2201
+ layout: columnState.layout,
2202
+ filterModel,
2203
+ height: floatingFilterHeight,
2204
+ selectable,
2205
+ onFilterChange
2206
+ }
2207
+ ) }),
2208
+ /* @__PURE__ */ jsxRuntime.jsx(
2209
+ "div",
2210
+ {
2211
+ className: "relative",
2212
+ style: {
2213
+ height: virtual.totalHeight,
2214
+ width: columnState.layout.totalWidth + (selectable ? 40 : 0)
2215
+ },
2216
+ children: visibleRows.map((row, offset) => {
2217
+ const rowIndex = startIndex + offset;
2218
+ const rowId = getRowId(row);
2219
+ const isRowEditing = editing.edit?.rowId === rowId;
2220
+ return /* @__PURE__ */ jsxRuntime.jsx(
2221
+ "div",
2222
+ {
2223
+ "data-hxg-editing-row": isRowEditing ? "true" : void 0,
2224
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2225
+ GridRow,
2226
+ {
2227
+ row,
2228
+ rowId,
2229
+ rowIndex,
2230
+ top: rowIndex * rowHeight,
2231
+ height: rowHeight,
2232
+ layout: columnState.layout,
2233
+ context: cellContext,
2234
+ api,
2235
+ selectable,
2236
+ isSelected: selection.isSelected(rowId),
2237
+ isRowEditing,
2238
+ draft: isRowEditing ? editing.edit?.draft ?? null : null,
2239
+ errors: isRowEditing ? editing.edit?.errors ?? {} : {},
2240
+ isSaving: isRowEditing && editing.edit?.isSaving === true,
2241
+ onToggleSelect: selection.toggleRow,
2242
+ onFieldChange: editing.setField,
2243
+ onCommit: editing.commit,
2244
+ onCancel: editing.cancel,
2245
+ onRowDoubleClick,
2246
+ onFilesDropped: onRowFilesDropped
2247
+ }
2248
+ )
2249
+ },
2250
+ rowId
2251
+ );
2252
+ })
2253
+ }
2254
+ ),
2255
+ isLoading && /* @__PURE__ */ jsxRuntime.jsx(GridOverlay, { kind: "loading" }),
2256
+ error != null && /* @__PURE__ */ jsxRuntime.jsx(
2257
+ GridOverlay,
2258
+ {
2259
+ kind: "error",
2260
+ message: error instanceof Error ? error.message : String(error)
2261
+ }
2262
+ ),
2263
+ showEmpty && /* @__PURE__ */ jsxRuntime.jsx(GridOverlay, { kind: "empty", message: emptyMessage })
2264
+ ]
2265
+ }
2266
+ )
2267
+ ] }),
2268
+ editing.edit && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-2 border-t border-brand-200 bg-brand-25 px-2 py-1.5 dark:border-brand-500/30 dark:bg-brand-500/10", children: [
2269
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mr-auto text-xs text-gray-600 dark:text-gray-300", children: editing.edit.errors.__row__ ?? "Editing row \u2014 Enter to save, Escape to cancel." }),
2270
+ /* @__PURE__ */ jsxRuntime.jsx(
2271
+ "button",
2272
+ {
2273
+ type: "button",
2274
+ className: "rounded border border-gray-300 px-2 py-0.5 text-xs text-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-200",
2275
+ onClick: editing.cancel,
2276
+ children: "Cancel"
2277
+ }
2278
+ ),
2279
+ /* @__PURE__ */ jsxRuntime.jsx(
2280
+ "button",
2281
+ {
2282
+ type: "button",
2283
+ className: "rounded bg-brand-500 px-2.5 py-0.5 text-xs font-medium text-white hover:bg-brand-600 disabled:opacity-50",
2284
+ disabled: editing.edit.isSaving,
2285
+ onClick: editing.commit,
2286
+ children: editing.edit.isSaving ? "Saving..." : "Save"
2287
+ }
2288
+ )
2289
+ ] }),
2290
+ /* @__PURE__ */ jsxRuntime.jsx(
2291
+ GridPagination,
2292
+ {
2293
+ page,
2294
+ pageSize,
2295
+ totalRows,
2296
+ pageSizeOptions,
2297
+ isLoading,
2298
+ onPageChange: setPage,
2299
+ onPageSizeChange
2300
+ }
2301
+ )
2302
+ ]
2303
+ }
2304
+ );
2305
+ }
2306
+
2307
+ exports.BUILTIN_EDITORS = BUILTIN_EDITORS;
2308
+ exports.CheckboxEditor = CheckboxEditor;
2309
+ exports.ColumnsPanel = ColumnsPanel;
2310
+ exports.DATE_FILTER_TYPES = DATE_FILTER_TYPES;
2311
+ exports.DataGrid = DataGrid;
2312
+ exports.DateEditor = DateEditor;
2313
+ exports.FILTER_TYPE_LABELS = FILTER_TYPE_LABELS;
2314
+ exports.FilterPopover = FilterPopover;
2315
+ exports.GRID_STATE_VERSION = GRID_STATE_VERSION;
2316
+ exports.GridOverlay = GridOverlay;
2317
+ exports.GridPagination = GridPagination;
2318
+ exports.NUMBER_FILTER_TYPES = NUMBER_FILTER_TYPES;
2319
+ exports.NumberEditor = NumberEditor;
2320
+ exports.SelectEditor = SelectEditor;
2321
+ exports.TEXT_FILTER_TYPES = TEXT_FILTER_TYPES;
2322
+ exports.TextEditor = TextEditor;
2323
+ exports.buildDateFilter = buildDateFilter;
2324
+ exports.buildNumberFilter = buildNumberFilter;
2325
+ exports.buildSetFilter = buildSetFilter;
2326
+ exports.buildTextFilter = buildTextFilter;
2327
+ exports.columnId = columnId;
2328
+ exports.copyToClipboard = copyToClipboard;
2329
+ exports.defaultFilterType = defaultFilterType;
2330
+ exports.describeFilter = describeFilter;
2331
+ exports.downloadCsv = downloadCsv;
2332
+ exports.exportValue = exportValue;
2333
+ exports.formatValue = formatValue;
2334
+ exports.headerText = headerText;
2335
+ exports.isEditable = isEditable;
2336
+ exports.isRangeFilter = isRangeFilter;
2337
+ exports.isUnaryFilter = isUnaryFilter;
2338
+ exports.resolveColumn = resolveColumn;
2339
+ exports.resolveValue = resolveValue;
2340
+ exports.toCsv = toCsv;
2341
+ exports.toDelimited = toDelimited;
2342
+ exports.toTsv = toTsv;
2343
+ exports.useColumnState = useColumnState;
2344
+ exports.useEditModel = useEditModel;
2345
+ exports.useGridState = useGridState;
2346
+ exports.useSelectionModel = useSelectionModel;
2347
+ exports.useServerDataSource = useServerDataSource;
2348
+ exports.useVirtualRows = useVirtualRows;
2349
+ exports.withFilter = withFilter;
2350
+ //# sourceMappingURL=index.cjs.map
2351
+ //# sourceMappingURL=index.cjs.map