@zvndev/yable-react 0.3.0 → 0.5.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.js CHANGED
@@ -1,10 +1,118 @@
1
1
  import { canCellEnterEditMode, functionalUpdate, createTable, getDefaultLocale, getFirstKeyboardCell, getResolvedFocusedCell, detectCellChanges } from '@zvndev/yable-core';
2
2
  export { CommitError, FormulaEngine, FormulaError, PivotEngine, UndoStack, aggregationFns, createColumnHelper, createLocale, createUndoRedoIntegration, en, filterFns, formulaFunctions, functionalUpdate, generatePivotColumnDefs, getDefaultLocale, getInitialPivotState, getPivotRowModel, resetLocale, setDefaultLocale, sortingFns } from '@zvndev/yable-core';
3
- import React3, { createContext, useCallback, useMemo, useState, useRef, useEffect, useContext } from 'react';
3
+ import React3, { createContext, useCallback, useMemo, useContext, useState, useRef, useEffect } from 'react';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
+ import { createPortal } from 'react-dom';
5
6
  import { getInitialRowDragState, moveRow } from '@zvndev/yable-core/features/rowDragging';
6
7
 
7
8
  // src/index.ts
9
+ var YableContext = createContext({});
10
+ function useYableDefaults() {
11
+ return useContext(YableContext);
12
+ }
13
+ function YableProvider({
14
+ children,
15
+ config,
16
+ tableProfile,
17
+ defaultColumnDef,
18
+ striped,
19
+ stickyHeader,
20
+ bordered,
21
+ compact,
22
+ theme,
23
+ direction,
24
+ ariaLabel
25
+ }) {
26
+ const tableProps = {};
27
+ if (striped !== void 0) tableProps.striped = striped;
28
+ if (stickyHeader !== void 0) tableProps.stickyHeader = stickyHeader;
29
+ if (bordered !== void 0) tableProps.bordered = bordered;
30
+ if (compact !== void 0) tableProps.compact = compact;
31
+ if (theme !== void 0) tableProps.theme = theme;
32
+ if (direction !== void 0) tableProps.direction = direction;
33
+ if (ariaLabel !== void 0) tableProps.ariaLabel = ariaLabel;
34
+ const value = {
35
+ config,
36
+ tableProfile,
37
+ tableProps: Object.keys(tableProps).length > 0 ? tableProps : void 0,
38
+ defaultColumnDef
39
+ };
40
+ return /* @__PURE__ */ jsx(YableContext.Provider, { value, children });
41
+ }
42
+
43
+ // src/config.ts
44
+ function createYableConfig(config) {
45
+ return config;
46
+ }
47
+ function resolveYableProfile(config, profileName = "default") {
48
+ const base = pickProfileFields(config);
49
+ const named = profileName === "default" ? void 0 : config?.profiles?.[profileName];
50
+ return {
51
+ name: profileName,
52
+ table: { ...base.table, ...named?.table },
53
+ columns: {
54
+ default: { ...base.columns?.default, ...named?.columns?.default },
55
+ byId: { ...base.columns?.byId, ...named?.columns?.byId }
56
+ },
57
+ rows: { ...base.rows, ...named?.rows },
58
+ cells: {
59
+ default: { ...base.cells?.default, ...named?.cells?.default },
60
+ named: { ...base.cells?.named, ...named?.cells?.named },
61
+ byColumn: { ...base.cells?.byColumn, ...named?.cells?.byColumn }
62
+ }
63
+ };
64
+ }
65
+ function getYableDefaultColumnDef(profile) {
66
+ const next = {
67
+ ...profile?.columns?.default
68
+ };
69
+ return Object.keys(next).length > 0 ? next : void 0;
70
+ }
71
+ function applyYableConfigToColumns(columns, profile) {
72
+ if (!profile) return columns;
73
+ return columns.map((columnDef) => applyColumnConfig(columnDef, profile));
74
+ }
75
+ function applyColumnConfig(columnDef, profile) {
76
+ const columnId = getColumnId(columnDef);
77
+ const explicit = columnDef;
78
+ const cellNames = normalizeCellConfigNames(explicit.cellConfig);
79
+ const namedCellConfig = cellNames.reduce(
80
+ (acc, name) => ({ ...acc, ...profile.cells?.named?.[name] }),
81
+ {}
82
+ );
83
+ const defaultCell = profile.cells?.default;
84
+ const columnCell = columnId ? profile.cells?.byColumn?.[columnId] : void 0;
85
+ const columnConfig = columnId ? profile.columns?.byId?.[columnId] : void 0;
86
+ const children = "columns" in columnDef && columnDef.columns ? { columns: columnDef.columns.map((child) => applyColumnConfig(child, profile)) } : void 0;
87
+ return {
88
+ ...defaultCell,
89
+ ...namedCellConfig,
90
+ ...columnCell,
91
+ ...columnConfig,
92
+ ...columnDef,
93
+ ...children
94
+ };
95
+ }
96
+ function pickProfileFields(config) {
97
+ if (!config) return {};
98
+ return {
99
+ table: config.table,
100
+ columns: config.columns,
101
+ rows: config.rows,
102
+ cells: config.cells
103
+ };
104
+ }
105
+ function getColumnId(columnDef) {
106
+ if ("id" in columnDef && columnDef.id) return String(columnDef.id);
107
+ if ("accessorKey" in columnDef && columnDef.accessorKey) return String(columnDef.accessorKey);
108
+ return void 0;
109
+ }
110
+ function normalizeCellConfigNames(value) {
111
+ if (!value) return [];
112
+ return Array.isArray(value) ? value : [value];
113
+ }
114
+
115
+ // src/useTable.ts
8
116
  function shallowEqual(a, b) {
9
117
  if (a === b) return true;
10
118
  const keysA = Object.keys(a);
@@ -17,6 +125,32 @@ function shallowEqual(a, b) {
17
125
  return true;
18
126
  }
19
127
  function useTable(options) {
128
+ const providerDefaults = useYableDefaults();
129
+ const optionsWithDefaults = useMemo(() => {
130
+ const profile = resolveYableProfile(
131
+ options.config ?? providerDefaults.config,
132
+ options.configProfile ?? providerDefaults.tableProfile
133
+ );
134
+ const profileDefaultColumnDef = getYableDefaultColumnDef(profile);
135
+ const configuredColumns = applyYableConfigToColumns(options.columns, profile);
136
+ const defaultColumnDef = {
137
+ ...profileDefaultColumnDef,
138
+ ...providerDefaults.defaultColumnDef,
139
+ ...options.defaultColumnDef
140
+ };
141
+ return {
142
+ ...options,
143
+ columns: configuredColumns,
144
+ rowClassName: options.rowClassName ?? profile.rows?.className,
145
+ rowStyle: options.rowStyle ?? profile.rows?.style,
146
+ defaultColumnDef: Object.keys(defaultColumnDef).length > 0 ? defaultColumnDef : void 0
147
+ };
148
+ }, [
149
+ options,
150
+ providerDefaults.config,
151
+ providerDefaults.defaultColumnDef,
152
+ providerDefaults.tableProfile
153
+ ]);
20
154
  const [state, setState] = useState(() => ({
21
155
  sorting: [],
22
156
  columnFilters: [],
@@ -56,14 +190,14 @@ function useTable(options) {
56
190
  }));
57
191
  const stateRef = useRef(state);
58
192
  stateRef.current = state;
59
- const prevOptionsRef = useRef(options);
193
+ const prevOptionsRef = useRef(optionsWithDefaults);
60
194
  const stableOptions = useMemo(() => {
61
- if (shallowEqual(prevOptionsRef.current, options)) {
195
+ if (shallowEqual(prevOptionsRef.current, optionsWithDefaults)) {
62
196
  return prevOptionsRef.current;
63
197
  }
64
- prevOptionsRef.current = options;
65
- return options;
66
- }, [options]);
198
+ prevOptionsRef.current = optionsWithDefaults;
199
+ return optionsWithDefaults;
200
+ }, [optionsWithDefaults]);
67
201
  const onStateChangeRef = useRef(options.onStateChange);
68
202
  onStateChangeRef.current = options.onStateChange;
69
203
  const resolvedState = useMemo(
@@ -73,17 +207,14 @@ function useTable(options) {
73
207
  }),
74
208
  [state, stableOptions.state]
75
209
  );
76
- const onStateChange = useCallback(
77
- (updater) => {
78
- const latest = onStateChangeRef.current;
79
- if (latest) {
80
- latest(updater);
81
- } else {
82
- setState((prev) => functionalUpdate(updater, prev));
83
- }
84
- },
85
- []
86
- );
210
+ const onStateChange = useCallback((updater) => {
211
+ const latest = onStateChangeRef.current;
212
+ if (latest) {
213
+ latest(updater);
214
+ } else {
215
+ setState((prev) => functionalUpdate(updater, prev));
216
+ }
217
+ }, []);
87
218
  const resolvedOptions = useMemo(
88
219
  () => ({
89
220
  ...stableOptions,
@@ -96,13 +227,41 @@ function useTable(options) {
96
227
  if (!tableRef.current) {
97
228
  tableRef.current = createTable(resolvedOptions);
98
229
  } else {
99
- tableRef.current.setOptions((prev) => ({
100
- ...prev,
101
- ...resolvedOptions,
102
- state: resolvedState,
103
- onStateChange
104
- }));
230
+ tableRef.current.setOptions(
231
+ (prev) => ({
232
+ ...prev,
233
+ ...resolvedOptions,
234
+ state: resolvedState,
235
+ onStateChange
236
+ })
237
+ );
105
238
  }
239
+ useEffect(() => {
240
+ const table = tableRef.current;
241
+ if (!table) return;
242
+ const unsubscribers = [
243
+ options.onCellClick && table.events.on("cell:click", options.onCellClick),
244
+ options.onCellDoubleClick && table.events.on("cell:dblclick", options.onCellDoubleClick),
245
+ options.onCellContextMenu && table.events.on("cell:contextmenu", options.onCellContextMenu),
246
+ options.onRowClick && table.events.on("row:click", options.onRowClick),
247
+ options.onRowDoubleClick && table.events.on("row:dblclick", options.onRowDoubleClick),
248
+ options.onRowContextMenu && table.events.on("row:contextmenu", options.onRowContextMenu),
249
+ options.onHeaderClick && table.events.on("header:click", options.onHeaderClick),
250
+ options.onHeaderContextMenu && table.events.on("header:contextmenu", options.onHeaderContextMenu)
251
+ ].filter((unsubscribe) => Boolean(unsubscribe));
252
+ return () => {
253
+ unsubscribers.forEach((unsubscribe) => unsubscribe());
254
+ };
255
+ }, [
256
+ options.onCellClick,
257
+ options.onCellContextMenu,
258
+ options.onCellDoubleClick,
259
+ options.onHeaderClick,
260
+ options.onHeaderContextMenu,
261
+ options.onRowClick,
262
+ options.onRowContextMenu,
263
+ options.onRowDoubleClick
264
+ ]);
106
265
  useEffect(() => {
107
266
  return () => {
108
267
  if (tableRef.current) {
@@ -112,6 +271,311 @@ function useTable(options) {
112
271
  }, []);
113
272
  return tableRef.current;
114
273
  }
274
+ function useServerTable({
275
+ fetchData,
276
+ updateRow,
277
+ initialRows = [],
278
+ initialCursor = null,
279
+ initialHasMore = true,
280
+ initialRowCount,
281
+ initialPageCount,
282
+ initialSorting = [],
283
+ initialColumnFilters = [],
284
+ initialGlobalFilter = "",
285
+ initialPagination = { pageIndex: 0, pageSize: 50 },
286
+ autoLoad = true,
287
+ getRowId,
288
+ ...tableOptions
289
+ }) {
290
+ const [rows, setRows] = useState(initialRows);
291
+ const [cursor, setCursor] = useState(initialCursor);
292
+ const [hasMore, setHasMore] = useState(initialHasMore);
293
+ const [rowCount, setRowCount] = useState(initialRowCount);
294
+ const [pageCount, setPageCount] = useState(initialPageCount);
295
+ const [sorting, setSorting] = useState(initialSorting);
296
+ const [columnFilters, setColumnFilters] = useState(initialColumnFilters);
297
+ const [globalFilter, setGlobalFilter] = useState(initialGlobalFilter);
298
+ const [pagination, setPagination] = useState(initialPagination);
299
+ const [loading, setLoading] = useState(false);
300
+ const [error, setError] = useState(null);
301
+ const abortRef = useRef(null);
302
+ const cursorRef = useRef(cursor);
303
+ const fetchDataRef = useRef(fetchData);
304
+ const updateRowRef = useRef(updateRow);
305
+ const resolveRowId = useCallback(
306
+ (row, index) => getRowId?.(row, index) ?? String(row.id ?? index),
307
+ [getRowId]
308
+ );
309
+ const runFetch = useCallback(
310
+ async (mode) => {
311
+ abortRef.current?.abort();
312
+ const abort = new AbortController();
313
+ abortRef.current = abort;
314
+ setLoading(true);
315
+ setError(null);
316
+ try {
317
+ const result = await fetchDataRef.current({
318
+ sorting,
319
+ columnFilters,
320
+ globalFilter,
321
+ pagination,
322
+ cursor: mode === "append" ? cursorRef.current : null,
323
+ signal: abort.signal
324
+ });
325
+ if (abort.signal.aborted) return;
326
+ setRows((prev) => mode === "append" ? [...prev, ...result.rows] : result.rows);
327
+ setCursor(result.cursor ?? null);
328
+ setHasMore((prev) => result.hasMore ?? prev);
329
+ setRowCount((prev) => result.rowCount ?? prev);
330
+ setPageCount((prev) => result.pageCount ?? prev);
331
+ } catch (nextError) {
332
+ if (!abort.signal.aborted) setError(nextError);
333
+ } finally {
334
+ if (!abort.signal.aborted) setLoading(false);
335
+ }
336
+ },
337
+ [columnFilters, globalFilter, pagination, sorting]
338
+ );
339
+ const refresh = useCallback(() => runFetch("replace"), [runFetch]);
340
+ const loadMore = useCallback(async () => {
341
+ if (!hasMore || loading) return;
342
+ await runFetch("append");
343
+ }, [hasMore, loading, runFetch]);
344
+ const patchRow = useCallback(
345
+ async (rowId, patch) => {
346
+ const previousRow = rows.find((row, index) => resolveRowId(row, index) === rowId);
347
+ setRows(
348
+ (prev) => prev.map((row, index) => resolveRowId(row, index) === rowId ? { ...row, ...patch } : row)
349
+ );
350
+ if (!updateRowRef.current) return;
351
+ const abort = new AbortController();
352
+ try {
353
+ const result = await updateRowRef.current({
354
+ rowId,
355
+ patch,
356
+ previousRow,
357
+ signal: abort.signal
358
+ });
359
+ if (!result) return;
360
+ setRows(
361
+ (prev) => prev.map(
362
+ (row, index) => resolveRowId(row, index) === rowId ? { ...row, ...result } : row
363
+ )
364
+ );
365
+ } catch (nextError) {
366
+ setError(nextError);
367
+ if (previousRow) {
368
+ setRows(
369
+ (prev) => prev.map((row, index) => resolveRowId(row, index) === rowId ? previousRow : row)
370
+ );
371
+ }
372
+ }
373
+ },
374
+ [resolveRowId, rows]
375
+ );
376
+ useEffect(() => {
377
+ fetchDataRef.current = fetchData;
378
+ }, [fetchData]);
379
+ useEffect(() => {
380
+ updateRowRef.current = updateRow;
381
+ }, [updateRow]);
382
+ useEffect(() => {
383
+ if (!autoLoad) return;
384
+ void refresh();
385
+ }, [autoLoad, refresh]);
386
+ useEffect(() => {
387
+ cursorRef.current = cursor;
388
+ }, [cursor]);
389
+ useEffect(() => () => abortRef.current?.abort(), []);
390
+ const table = useTable({
391
+ ...tableOptions,
392
+ data: rows,
393
+ getRowId: resolveRowId,
394
+ manualSorting: true,
395
+ manualFiltering: true,
396
+ manualPagination: true,
397
+ rowCount,
398
+ pageCount,
399
+ state: {
400
+ sorting,
401
+ columnFilters,
402
+ globalFilter,
403
+ pagination
404
+ },
405
+ onSortingChange: (updater) => {
406
+ setSorting((prev) => functionalUpdate(updater, prev));
407
+ setCursor(null);
408
+ setHasMore(true);
409
+ },
410
+ onColumnFiltersChange: (updater) => {
411
+ setColumnFilters((prev) => functionalUpdate(updater, prev));
412
+ setCursor(null);
413
+ setHasMore(true);
414
+ },
415
+ onGlobalFilterChange: (updater) => {
416
+ setGlobalFilter((prev) => functionalUpdate(updater, prev));
417
+ setCursor(null);
418
+ setHasMore(true);
419
+ },
420
+ onPaginationChange: (updater) => {
421
+ setPagination((prev) => functionalUpdate(updater, prev));
422
+ setCursor(null);
423
+ setHasMore(true);
424
+ }
425
+ });
426
+ return useMemo(
427
+ () => ({
428
+ table,
429
+ rows,
430
+ loading,
431
+ error,
432
+ cursor,
433
+ hasMore,
434
+ rowCount,
435
+ pageCount,
436
+ sorting,
437
+ columnFilters,
438
+ globalFilter,
439
+ pagination,
440
+ refresh,
441
+ loadMore,
442
+ updateRow: patchRow
443
+ }),
444
+ [
445
+ table,
446
+ rows,
447
+ loading,
448
+ error,
449
+ cursor,
450
+ hasMore,
451
+ rowCount,
452
+ pageCount,
453
+ sorting,
454
+ columnFilters,
455
+ globalFilter,
456
+ pagination,
457
+ refresh,
458
+ loadMore,
459
+ patchRow
460
+ ]
461
+ );
462
+ }
463
+ var DEFAULT_PERSISTED_KEYS = [
464
+ "columnVisibility",
465
+ "columnOrder",
466
+ "columnSizing",
467
+ "columnPinning"
468
+ ];
469
+ function resolveStorage(custom) {
470
+ if (custom) return custom;
471
+ if (typeof window !== "undefined") {
472
+ try {
473
+ return window.localStorage;
474
+ } catch {
475
+ return void 0;
476
+ }
477
+ }
478
+ return void 0;
479
+ }
480
+ function pick(obj, keys) {
481
+ const result = {};
482
+ for (const k of keys) {
483
+ if (k in obj) {
484
+ result[k] = obj[k];
485
+ }
486
+ }
487
+ return result;
488
+ }
489
+ function readState(storage, key, version, persistedKeys) {
490
+ if (!storage) return {};
491
+ try {
492
+ const raw = storage.getItem(key);
493
+ if (!raw) return {};
494
+ const envelope = JSON.parse(raw);
495
+ if (envelope.version !== version) {
496
+ storage.removeItem(key);
497
+ return {};
498
+ }
499
+ return pick(envelope.state, persistedKeys);
500
+ } catch {
501
+ if (typeof console !== "undefined") {
502
+ console.warn(`[yable] Failed to read persisted state for key "${key}"`);
503
+ }
504
+ return {};
505
+ }
506
+ }
507
+ function writeState(storage, key, version, state) {
508
+ if (!storage) return;
509
+ try {
510
+ const envelope = { version, state };
511
+ storage.setItem(key, JSON.stringify(envelope));
512
+ } catch {
513
+ if (typeof console !== "undefined") {
514
+ console.warn(`[yable] Failed to persist state for key "${key}" (storage may be full)`);
515
+ }
516
+ }
517
+ }
518
+ function useTablePersistence(options) {
519
+ const {
520
+ key,
521
+ persistedKeys = DEFAULT_PERSISTED_KEYS,
522
+ debounce: debounceMs = 100,
523
+ version = 0,
524
+ storage: customStorage
525
+ } = options;
526
+ const storage = useMemo(() => resolveStorage(customStorage), [customStorage]);
527
+ const initialState = useMemo(
528
+ () => readState(storage, key, version, persistedKeys),
529
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally read only on mount
530
+ []
531
+ );
532
+ const [state, setState] = useState(initialState);
533
+ const timerRef = useRef(null);
534
+ const keyRef = useRef(key);
535
+ keyRef.current = key;
536
+ const versionRef = useRef(version);
537
+ versionRef.current = version;
538
+ const persistedKeysRef = useRef(persistedKeys);
539
+ persistedKeysRef.current = persistedKeys;
540
+ const debounceRef = useRef(debounceMs);
541
+ debounceRef.current = debounceMs;
542
+ const storageRef = useRef(storage);
543
+ storageRef.current = storage;
544
+ const onStateChange = useCallback((updater) => {
545
+ setState((prev) => {
546
+ const next = functionalUpdate(updater, prev);
547
+ if (timerRef.current !== null) {
548
+ clearTimeout(timerRef.current);
549
+ }
550
+ timerRef.current = setTimeout(() => {
551
+ const sliced = pick(
552
+ next,
553
+ persistedKeysRef.current
554
+ );
555
+ writeState(storageRef.current, keyRef.current, versionRef.current, sliced);
556
+ timerRef.current = null;
557
+ }, debounceRef.current);
558
+ return next;
559
+ });
560
+ }, []);
561
+ useEffect(() => {
562
+ return () => {
563
+ if (timerRef.current !== null) {
564
+ clearTimeout(timerRef.current);
565
+ }
566
+ };
567
+ }, []);
568
+ const clearPersistedState = useCallback(() => {
569
+ const s = storageRef.current;
570
+ if (s) {
571
+ try {
572
+ s.removeItem(keyRef.current);
573
+ } catch {
574
+ }
575
+ }
576
+ }, []);
577
+ return { initialState, state, onStateChange, clearPersistedState };
578
+ }
115
579
  var EMPTY_RESULT = {
116
580
  virtualRows: [],
117
581
  totalHeight: 0,
@@ -125,11 +589,14 @@ function useVirtualization({
125
589
  overscan = 5,
126
590
  estimateRowHeight: _estimateRowHeight,
127
591
  pretextHeights,
128
- pretextPrefixSums
592
+ pretextPrefixSums,
593
+ columnSizingHash
129
594
  }) {
130
595
  const hasPretextHeights = !!(pretextHeights && pretextPrefixSums && pretextHeights.length >= totalRows);
131
596
  const isFixedHeight = typeof rowHeight === "number" && !hasPretextHeights;
132
597
  const heightCacheRef = useRef(/* @__PURE__ */ new Map());
598
+ const heightCacheVersionRef = useRef(0);
599
+ const [heightCacheVersion, setHeightCacheVersion] = useState(0);
133
600
  const getRowHeight = useCallback(
134
601
  (index) => {
135
602
  if (hasPretextHeights) return pretextHeights[index];
@@ -140,7 +607,9 @@ function useVirtualization({
140
607
  heightCacheRef.current.set(index, height);
141
608
  return height;
142
609
  },
143
- [rowHeight, isFixedHeight, hasPretextHeights, pretextHeights]
610
+ // heightCacheVersion forces a new callback identity after cache invalidation
611
+ // eslint-disable-next-line react-hooks/exhaustive-deps
612
+ [rowHeight, isFixedHeight, hasPretextHeights, pretextHeights, heightCacheVersion]
144
613
  );
145
614
  const [scrollState, setScrollState] = useState({ scrollTop: 0, containerHeight: 0 });
146
615
  const rafRef = useRef(null);
@@ -187,12 +656,24 @@ function useVirtualization({
187
656
  resizeObserver.disconnect();
188
657
  }
189
658
  };
190
- }, [containerRef]);
659
+ }, [containerRef, totalRows]);
191
660
  useEffect(() => {
192
661
  if (!isFixedHeight) {
193
662
  heightCacheRef.current.clear();
194
663
  }
195
664
  }, [totalRows, isFixedHeight]);
665
+ useEffect(() => {
666
+ if (!isFixedHeight && columnSizingHash !== void 0) {
667
+ heightCacheRef.current.clear();
668
+ heightCacheVersionRef.current += 1;
669
+ setHeightCacheVersion(heightCacheVersionRef.current);
670
+ }
671
+ }, [columnSizingHash]);
672
+ const invalidateRowHeights = useCallback(() => {
673
+ heightCacheRef.current.clear();
674
+ heightCacheVersionRef.current += 1;
675
+ setHeightCacheVersion(heightCacheVersionRef.current);
676
+ }, []);
196
677
  const scrollTo = useCallback(
197
678
  (index) => {
198
679
  const container = containerRef.current;
@@ -229,10 +710,18 @@ function useVirtualization({
229
710
  total += getRowHeight(i);
230
711
  }
231
712
  return total;
232
- }, [totalRows, rowHeight, isFixedHeight, getRowHeight, hasPretextHeights, pretextPrefixSums]);
713
+ }, [
714
+ totalRows,
715
+ rowHeight,
716
+ isFixedHeight,
717
+ getRowHeight,
718
+ hasPretextHeights,
719
+ pretextPrefixSums,
720
+ heightCacheVersion
721
+ ]);
233
722
  const { scrollTop, containerHeight } = scrollState;
234
723
  if (totalRows === 0) {
235
- return { ...EMPTY_RESULT, scrollTo };
724
+ return { ...EMPTY_RESULT, scrollTo, invalidateRowHeights };
236
725
  }
237
726
  if (containerHeight === 0) {
238
727
  return {
@@ -240,7 +729,8 @@ function useVirtualization({
240
729
  totalHeight,
241
730
  startIndex: 0,
242
731
  endIndex: 0,
243
- scrollTo
732
+ scrollTo,
733
+ invalidateRowHeights
244
734
  };
245
735
  }
246
736
  let startIndex = 0;
@@ -336,7 +826,8 @@ function useVirtualization({
336
826
  totalHeight,
337
827
  startIndex: overscanStart,
338
828
  endIndex: overscanEnd,
339
- scrollTo
829
+ scrollTo,
830
+ invalidateRowHeights
340
831
  };
341
832
  }
342
833
  function binarySearchOffsets(offsets, target) {
@@ -356,14 +847,20 @@ function useColumnVirtualization({
356
847
  containerRef,
357
848
  columns,
358
849
  overscan = 2,
359
- enabled = true
850
+ enabled = true,
851
+ sizingKey
360
852
  }) {
361
853
  const [scrollState, setScrollState] = useState({
362
854
  scrollLeft: 0,
363
855
  containerWidth: 0
364
856
  });
365
857
  const rafRef = useRef(null);
366
- const sizes = useMemo(() => columns.map((column) => column.getSize()), [columns]);
858
+ const sizes = useMemo(
859
+ () => columns.map((column) => column.getSize()),
860
+ // `sizingKey` is an explicit invalidation hook for stable Column objects whose getSize value changed.
861
+ // eslint-disable-next-line react-hooks/exhaustive-deps
862
+ [columns, sizingKey]
863
+ );
367
864
  const offsets = useMemo(() => {
368
865
  const next = new Array(columns.length + 1);
369
866
  next[0] = 0;
@@ -941,12 +1438,12 @@ function getRegisteredCellTypes() {
941
1438
 
942
1439
  // src/hooks/useAutoMeasurements.ts
943
1440
  var NON_DATA_COLUMN_IDS = /* @__PURE__ */ new Set(["select", "expand", "drag", "actions"]);
944
- function getColumnId(col) {
1441
+ function getColumnId2(col) {
945
1442
  const id = col.id ?? col.accessorKey;
946
1443
  return typeof id === "string" ? id : void 0;
947
1444
  }
948
1445
  function defaultShouldMeasure(col) {
949
- const id = getColumnId(col);
1446
+ const id = getColumnId2(col);
950
1447
  if (!id) return false;
951
1448
  if (id.startsWith("_")) return false;
952
1449
  if (NON_DATA_COLUMN_IDS.has(id)) return false;
@@ -971,11 +1468,11 @@ function useAutoMeasurements({
971
1468
  getColumnWidth = defaultGetColumnWidth,
972
1469
  shouldMeasureColumn = defaultShouldMeasure
973
1470
  }) {
974
- const widthKey = columns.map((c) => `${getColumnId(c) ?? "?"}:${getColumnWidth(c)}`).join("|");
1471
+ const widthKey = columns.map((c) => `${getColumnId2(c) ?? "?"}:${getColumnWidth(c)}`).join("|");
975
1472
  return useMemo(() => {
976
1473
  const result = [];
977
1474
  for (const col of columns) {
978
- const id = getColumnId(col);
1475
+ const id = getColumnId2(col);
979
1476
  if (!id) continue;
980
1477
  if (!shouldMeasureColumn(col)) continue;
981
1478
  const recipe = resolveMeasureRecipe(col, defaultRecipe);
@@ -1294,16 +1791,170 @@ function FloatingFilter({ column }) {
1294
1791
  }
1295
1792
  ) });
1296
1793
  }
1297
- var DRAG_MIME = "application/yable-column";
1794
+ var REORDER_TRANSITION = "transform 180ms cubic-bezier(0.2, 0, 0, 1)";
1795
+ function transformAt(i, d) {
1796
+ if (i === d.fromIndex) return 0;
1797
+ if (d.toIndex > d.fromIndex) return i > d.fromIndex && i < d.toIndex ? -d.width : 0;
1798
+ return i >= d.toIndex && i < d.fromIndex ? d.width : 0;
1799
+ }
1298
1800
  function TableHeader({
1299
1801
  table,
1300
1802
  floatingFilters = false
1301
1803
  }) {
1302
1804
  const headerGroups = table.getHeaderGroups();
1303
1805
  const visibleColumns = table.getVisibleLeafColumns();
1304
- return /* @__PURE__ */ jsxs("thead", { className: "yable-thead", children: [
1305
- headerGroups.map((headerGroup) => /* @__PURE__ */ jsx("tr", { className: "yable-header-row", children: headerGroup.headers.map((header) => /* @__PURE__ */ jsx(HeaderCell, { header, table }, header.id)) }, headerGroup.id)),
1306
- floatingFilters && visibleColumns.length > 0 && /* @__PURE__ */ jsx("tr", { className: "yable-header-row yable-header-row--filters", children: visibleColumns.map((column) => /* @__PURE__ */ jsx(FloatingFilterCell, { column }, `${column.id}-filter`)) })
1806
+ const theadRef = useRef(null);
1807
+ const reorderEndRef = useRef(0);
1808
+ const [drag, setDrag] = useState(null);
1809
+ const commitReorder = useCallback(
1810
+ (d) => {
1811
+ if (d.toIndex === d.fromIndex || d.toIndex === d.fromIndex + 1) return;
1812
+ const order = table.getState().columnOrder;
1813
+ const base = order && order.length > 0 ? [...order] : d.layout.map((l) => l.id);
1814
+ const targetId = d.toIndex < d.layout.length ? d.layout[d.toIndex].id : null;
1815
+ const next = base.filter((id) => id !== d.columnId);
1816
+ let insertAt = targetId ? next.indexOf(targetId) : next.length;
1817
+ if (insertAt === -1) insertAt = next.length;
1818
+ next.splice(insertAt, 0, d.columnId);
1819
+ table.setColumnOrder(next);
1820
+ },
1821
+ [table]
1822
+ );
1823
+ const beginReorder = useCallback(
1824
+ (e, columnId) => {
1825
+ if (e.button !== 0) return;
1826
+ const thead = theadRef.current;
1827
+ if (!thead) return;
1828
+ const startX = e.clientX;
1829
+ const startY = e.clientY;
1830
+ const layout = [];
1831
+ let top = 0;
1832
+ let height = 0;
1833
+ for (const c of visibleColumns) {
1834
+ const th = thead.querySelector(`th[data-column-id="${CSS.escape(c.id)}"]`);
1835
+ if (!th) return;
1836
+ const r = th.getBoundingClientRect();
1837
+ layout.push({ id: c.id, left: r.left, width: r.width });
1838
+ if (c.id === columnId) {
1839
+ top = r.top;
1840
+ height = r.height;
1841
+ }
1842
+ }
1843
+ const fromIndex = layout.findIndex((l) => l.id === columnId);
1844
+ if (fromIndex < 0) return;
1845
+ const src = layout[fromIndex];
1846
+ const bodyRoot = thead.closest("table");
1847
+ const applyBody = (d) => {
1848
+ if (!bodyRoot) return;
1849
+ visibleColumns.forEach((col, i) => {
1850
+ if (col.getIsPinned()) return;
1851
+ const tx = transformAt(i, d);
1852
+ bodyRoot.querySelectorAll(`td[data-column-id="${CSS.escape(col.id)}"]`).forEach((td) => {
1853
+ td.style.transition = REORDER_TRANSITION;
1854
+ td.style.opacity = i === d.fromIndex ? "0" : "";
1855
+ td.style.transform = i !== d.fromIndex && tx ? `translateX(${tx}px)` : "";
1856
+ });
1857
+ });
1858
+ };
1859
+ const clearBody = () => {
1860
+ bodyRoot?.querySelectorAll("td[data-column-id]").forEach((td) => {
1861
+ td.style.transform = "";
1862
+ td.style.transition = "";
1863
+ td.style.opacity = "";
1864
+ });
1865
+ };
1866
+ let started = false;
1867
+ let latest = {
1868
+ columnId,
1869
+ fromIndex,
1870
+ toIndex: fromIndex,
1871
+ pointerX: startX,
1872
+ grabOffsetX: startX - src.left,
1873
+ width: src.width,
1874
+ top,
1875
+ height,
1876
+ layout
1877
+ };
1878
+ const computeToIndex = (x) => {
1879
+ let t = layout.findIndex((l) => x < l.left + l.width / 2);
1880
+ if (t === -1) t = layout.length;
1881
+ return t;
1882
+ };
1883
+ const onMove = (ev) => {
1884
+ if (!started) {
1885
+ if (Math.abs(ev.clientX - startX) < 4 && Math.abs(ev.clientY - startY) < 4) return;
1886
+ started = true;
1887
+ table.setColumnDragActive(true);
1888
+ document.body.style.userSelect = "none";
1889
+ document.body.style.cursor = "grabbing";
1890
+ }
1891
+ latest = { ...latest, pointerX: ev.clientX, toIndex: computeToIndex(ev.clientX) };
1892
+ setDrag(latest);
1893
+ applyBody(latest);
1894
+ };
1895
+ const finish = () => {
1896
+ window.removeEventListener("pointermove", onMove);
1897
+ window.removeEventListener("pointerup", finish);
1898
+ window.removeEventListener("pointercancel", finish);
1899
+ if (started) {
1900
+ commitReorder(latest);
1901
+ reorderEndRef.current = Date.now();
1902
+ table.setColumnDragActive(false);
1903
+ document.body.style.userSelect = "";
1904
+ document.body.style.cursor = "";
1905
+ clearBody();
1906
+ }
1907
+ setDrag(null);
1908
+ };
1909
+ window.addEventListener("pointermove", onMove);
1910
+ window.addEventListener("pointerup", finish);
1911
+ window.addEventListener("pointercancel", finish);
1912
+ },
1913
+ [visibleColumns, table, commitReorder]
1914
+ );
1915
+ const transformFor = useCallback(
1916
+ (columnId) => {
1917
+ if (!drag) return 0;
1918
+ const i = visibleColumns.findIndex((c) => c.id === columnId);
1919
+ if (i < 0) return 0;
1920
+ return transformAt(i, drag);
1921
+ },
1922
+ [drag, visibleColumns]
1923
+ );
1924
+ const dragColumn = drag ? visibleColumns.find((c) => c.id === drag.columnId) : null;
1925
+ return /* @__PURE__ */ jsxs("thead", { className: "yable-thead", ref: theadRef, children: [
1926
+ headerGroups.map((headerGroup) => /* @__PURE__ */ jsx("tr", { className: "yable-header-row", children: headerGroup.headers.map((header) => /* @__PURE__ */ jsx(
1927
+ HeaderCell,
1928
+ {
1929
+ header,
1930
+ table,
1931
+ onReorderPointerDown: beginReorder,
1932
+ dragTransform: transformFor(header.column.id),
1933
+ isDragSource: drag?.columnId === header.column.id,
1934
+ dragActive: drag !== null,
1935
+ reorderEndRef
1936
+ },
1937
+ header.id
1938
+ )) }, headerGroup.id)),
1939
+ floatingFilters && visibleColumns.length > 0 && /* @__PURE__ */ jsx("tr", { className: "yable-header-row yable-header-row--filters", children: visibleColumns.map((column) => /* @__PURE__ */ jsx(FloatingFilterCell, { column }, `${column.id}-filter`)) }),
1940
+ drag && dragColumn && typeof document !== "undefined" && createPortal(
1941
+ /* @__PURE__ */ jsx(
1942
+ "div",
1943
+ {
1944
+ className: "yable-col-drag-ghost",
1945
+ "aria-hidden": "true",
1946
+ style: {
1947
+ position: "fixed",
1948
+ top: drag.top,
1949
+ left: drag.pointerX - drag.grabOffsetX,
1950
+ width: drag.width,
1951
+ height: drag.height
1952
+ },
1953
+ children: typeof dragColumn.columnDef.header === "string" ? dragColumn.columnDef.header : dragColumn.id
1954
+ }
1955
+ ),
1956
+ document.body
1957
+ )
1307
1958
  ] });
1308
1959
  }
1309
1960
  function FloatingFilterCell({
@@ -1332,7 +1983,12 @@ function FloatingFilterCell({
1332
1983
  }
1333
1984
  function HeaderCell({
1334
1985
  header,
1335
- table
1986
+ table,
1987
+ onReorderPointerDown,
1988
+ dragTransform,
1989
+ isDragSource,
1990
+ dragActive,
1991
+ reorderEndRef
1336
1992
  }) {
1337
1993
  const column = header.column;
1338
1994
  const canSort = column.getCanSort();
@@ -1340,6 +1996,7 @@ function HeaderCell({
1340
1996
  const sortIndex = column.getSortIndex();
1341
1997
  const canResize = column.getCanResize();
1342
1998
  const canReorder = column.getCanReorder() && !header.isPlaceholder;
1999
+ const pinned = column.getIsPinned();
1343
2000
  const headerContent = header.isPlaceholder ? null : typeof column.columnDef.header === "function" ? column.columnDef.header(header.getContext()) : column.columnDef.header ?? header.id;
1344
2001
  const style = useMemo(() => {
1345
2002
  const s = {
@@ -1347,18 +2004,19 @@ function HeaderCell({
1347
2004
  minWidth: column.columnDef.minSize,
1348
2005
  maxWidth: column.columnDef.maxSize
1349
2006
  };
1350
- const pinned2 = column.getIsPinned();
1351
- if (pinned2) {
2007
+ if (pinned) {
1352
2008
  s.position = "sticky";
1353
- if (pinned2 === "left") {
2009
+ if (pinned === "left") {
1354
2010
  s.left = header.getStart("left");
1355
2011
  } else {
1356
2012
  s.right = header.getStart("right");
1357
2013
  }
1358
2014
  }
2015
+ if (!pinned && !isDragSource && dragTransform !== 0) {
2016
+ s.transform = `translateX(${dragTransform}px)`;
2017
+ }
1359
2018
  return s;
1360
- }, [header, column]);
1361
- const pinned = column.getIsPinned();
2019
+ }, [header, column, pinned, isDragSource, dragTransform]);
1362
2020
  const lastResizeEndRef = useRef(0);
1363
2021
  const startResize = useCallback(
1364
2022
  (e) => {
@@ -1378,124 +2036,66 @@ function HeaderCell({
1378
2036
  const handleResizeClick = useCallback((e) => {
1379
2037
  e.stopPropagation();
1380
2038
  }, []);
1381
- const [dragOver, setDragOver] = useState(null);
1382
- const handleDragStart = useCallback(
2039
+ const handleContentPointerDown = useCallback(
1383
2040
  (e) => {
1384
- if (!canReorder) return;
1385
- const target = e.target;
1386
- if (target && target.closest(".yable-resize-handle")) {
1387
- e.preventDefault();
1388
- return;
1389
- }
1390
- e.stopPropagation();
1391
- e.dataTransfer.effectAllowed = "move";
1392
- try {
1393
- e.dataTransfer.setData(DRAG_MIME, column.id);
1394
- e.dataTransfer.setData("text/plain", column.id);
1395
- } catch {
1396
- }
1397
- },
1398
- [canReorder, column.id]
1399
- );
1400
- const handleDragOver = useCallback(
1401
- (e) => {
1402
- if (!canReorder) return;
1403
- const types = e.dataTransfer.types;
1404
- let isYableDrag = false;
1405
- for (let i = 0; i < types.length; i++) {
1406
- if (types[i] === DRAG_MIME) {
1407
- isYableDrag = true;
1408
- break;
1409
- }
1410
- }
1411
- if (!isYableDrag) return;
1412
- e.preventDefault();
1413
- e.dataTransfer.dropEffect = "move";
1414
- const rect = e.currentTarget.getBoundingClientRect();
1415
- const midpoint = rect.left + rect.width / 2;
1416
- setDragOver(e.clientX < midpoint ? "left" : "right");
1417
- },
1418
- [canReorder]
1419
- );
1420
- const handleDragLeave = useCallback((e) => {
1421
- const next = e.relatedTarget;
1422
- if (next && e.currentTarget.contains(next)) return;
1423
- setDragOver(null);
1424
- }, []);
1425
- const handleDragEnd = useCallback(() => {
1426
- setDragOver(null);
1427
- }, []);
1428
- const handleDrop = useCallback(
1429
- (e) => {
1430
- if (!canReorder) return;
1431
- e.preventDefault();
1432
- e.stopPropagation();
1433
- const sourceId = e.dataTransfer.getData(DRAG_MIME);
1434
- const rect = e.currentTarget.getBoundingClientRect();
1435
- const insertAfter = e.clientX >= rect.left + rect.width / 2;
1436
- setDragOver(null);
1437
- if (!sourceId || sourceId === column.id) return;
1438
- const state = table.getState();
1439
- const allLeafs = table.getAllLeafColumns();
1440
- const baseOrder = state.columnOrder && state.columnOrder.length > 0 ? state.columnOrder : allLeafs.map((c) => c.id);
1441
- const next = [];
1442
- const seen = /* @__PURE__ */ new Set();
1443
- for (const id of baseOrder) {
1444
- if (allLeafs.some((c) => c.id === id)) {
1445
- next.push(id);
1446
- seen.add(id);
1447
- }
1448
- }
1449
- for (const c of allLeafs) {
1450
- if (!seen.has(c.id)) {
1451
- next.push(c.id);
1452
- seen.add(c.id);
1453
- }
1454
- }
1455
- const fromIdx = next.indexOf(sourceId);
1456
- if (fromIdx === -1) return;
1457
- next.splice(fromIdx, 1);
1458
- let toIdx = next.indexOf(column.id);
1459
- if (toIdx === -1) return;
1460
- if (insertAfter) toIdx += 1;
1461
- next.splice(toIdx, 0, sourceId);
1462
- table.setColumnOrder(next);
2041
+ if (!canReorder || pinned) return;
2042
+ onReorderPointerDown(e, column.id);
1463
2043
  },
1464
- [canReorder, column.id, table]
2044
+ [canReorder, pinned, onReorderPointerDown, column.id]
1465
2045
  );
1466
2046
  const handleHeaderClick = useCallback(
1467
2047
  (e) => {
2048
+ table.events.emit("header:click", {
2049
+ column,
2050
+ header,
2051
+ originalEvent: e
2052
+ });
1468
2053
  if (!canSort) return;
1469
2054
  if (Date.now() - lastResizeEndRef.current < 250) return;
2055
+ if (Date.now() - reorderEndRef.current < 250) return;
1470
2056
  const handler = column.getToggleSortingHandler();
1471
2057
  if (handler) handler(e);
1472
2058
  },
1473
- [canSort, column]
2059
+ [canSort, column, header, table.events, reorderEndRef]
2060
+ );
2061
+ const handleHeaderContextMenu = useCallback(
2062
+ (e) => {
2063
+ table.events.emit("header:contextmenu", {
2064
+ column,
2065
+ header,
2066
+ originalEvent: e
2067
+ });
2068
+ },
2069
+ [column, header, table.events]
1474
2070
  );
1475
2071
  return /* @__PURE__ */ jsxs(
1476
2072
  "th",
1477
2073
  {
1478
2074
  className: "yable-th",
1479
2075
  style,
2076
+ "data-column-id": column.id,
1480
2077
  "data-sortable": canSort || void 0,
1481
2078
  "data-pinned": pinned || void 0,
1482
- "data-reorderable": canReorder || void 0,
1483
- "data-drag-over": dragOver || void 0,
2079
+ "data-reorderable": canReorder && !pinned || void 0,
2080
+ "data-reordering": dragActive && !pinned || void 0,
2081
+ "data-drag-source": isDragSource || void 0,
1484
2082
  "aria-sort": sortDirection === "asc" ? "ascending" : sortDirection === "desc" ? "descending" : canSort ? "none" : void 0,
1485
2083
  role: "columnheader",
1486
2084
  colSpan: header.colSpan,
1487
- draggable: canReorder || void 0,
1488
2085
  onClick: handleHeaderClick,
1489
- onDragStart: canReorder ? handleDragStart : void 0,
1490
- onDragOver: canReorder ? handleDragOver : void 0,
1491
- onDragLeave: canReorder ? handleDragLeave : void 0,
1492
- onDragEnd: canReorder ? handleDragEnd : void 0,
1493
- onDrop: canReorder ? handleDrop : void 0,
2086
+ onContextMenu: handleHeaderContextMenu,
1494
2087
  children: [
1495
- /* @__PURE__ */ jsxs("div", { className: "yable-th-content", children: [
1496
- /* @__PURE__ */ jsx("span", { children: headerContent }),
1497
- canSort && /* @__PURE__ */ jsx(SortIndicator, { direction: sortDirection, index: sortIndex > 0 ? sortIndex : void 0 })
1498
- ] }),
2088
+ /* @__PURE__ */ jsxs(
2089
+ "div",
2090
+ {
2091
+ className: "yable-th-content",
2092
+ onPointerDown: canReorder && !pinned ? handleContentPointerDown : void 0,
2093
+ children: [
2094
+ /* @__PURE__ */ jsx("span", { children: headerContent }),
2095
+ canSort && /* @__PURE__ */ jsx(SortIndicator, { direction: sortDirection, index: sortIndex > 0 ? sortIndex : void 0 })
2096
+ ]
2097
+ }
2098
+ ),
1499
2099
  canResize && /* @__PURE__ */ jsx(
1500
2100
  "div",
1501
2101
  {
@@ -1503,9 +2103,7 @@ function HeaderCell({
1503
2103
  "data-resizing": column.getIsResizing() || void 0,
1504
2104
  onMouseDown: startResize,
1505
2105
  onTouchStart: startResize,
1506
- onClick: handleResizeClick,
1507
- draggable: false,
1508
- onDragStart: (e) => e.preventDefault()
2106
+ onClick: handleResizeClick
1509
2107
  }
1510
2108
  )
1511
2109
  ]
@@ -1603,6 +2201,7 @@ function TableCell({
1603
2201
  const isAlwaysEditable = cell.getIsAlwaysEditable();
1604
2202
  const pinned = column.getIsPinned();
1605
2203
  const keyboardNavigationEnabled = table.options.enableKeyboardNavigation !== false;
2204
+ const cellSelectionEnabled = table.options.enableCellSelection !== false && column.columnDef.enableCellSelection !== false;
1606
2205
  const style = {
1607
2206
  width: column.getSize(),
1608
2207
  minWidth: column.columnDef.minSize,
@@ -1687,6 +2286,7 @@ function TableCell({
1687
2286
  const handleMouseDown = useCallback(
1688
2287
  (e) => {
1689
2288
  if (e.button !== 0) return;
2289
+ if (!cellSelectionEnabled) return;
1690
2290
  const clickTarget = e.target;
1691
2291
  if (isInteractiveClickTarget(clickTarget)) return;
1692
2292
  e.preventDefault();
@@ -1694,16 +2294,22 @@ function TableCell({
1694
2294
  table.startCellRangeSelection({ rowIndex, columnIndex }, { extend: e.shiftKey });
1695
2295
  e.currentTarget.focus({ preventScroll: true });
1696
2296
  },
1697
- [columnIndex, rowIndex, table]
2297
+ [cellSelectionEnabled, columnIndex, rowIndex, table]
1698
2298
  );
1699
2299
  const handleMouseEnter = useCallback(() => {
2300
+ if (!cellSelectionEnabled) return;
1700
2301
  if (!table.getState().cellSelection?.isDragging) return;
1701
2302
  table.updateCellRangeSelection({ rowIndex, columnIndex });
1702
- }, [columnIndex, rowIndex, table]);
2303
+ }, [cellSelectionEnabled, columnIndex, rowIndex, table]);
1703
2304
  const handleMouseUp = useCallback(() => {
1704
2305
  if (!table.getState().cellSelection?.isDragging) return;
1705
2306
  table.endCellRangeSelection();
1706
2307
  }, [table]);
2308
+ const cellClassNameDef = column.columnDef.cellClassName;
2309
+ const userClassName = typeof cellClassNameDef === "function" ? cellClassNameDef(cell.getContext()) : cellClassNameDef;
2310
+ const cellStyleDef = column.columnDef.cellStyle;
2311
+ const userStyle = typeof cellStyleDef === "function" ? cellStyleDef(cell.getContext()) : cellStyleDef;
2312
+ const mergedStyle = userStyle ? { ...style, ...userStyle } : style;
1707
2313
  const classNames = [
1708
2314
  "yable-td",
1709
2315
  isFocused && "yable-cell--focused",
@@ -1711,13 +2317,14 @@ function TableCell({
1711
2317
  selectionEdges?.top && "yable-cell--selection-top",
1712
2318
  selectionEdges?.right && "yable-cell--selection-right",
1713
2319
  selectionEdges?.bottom && "yable-cell--selection-bottom",
1714
- selectionEdges?.left && "yable-cell--selection-left"
2320
+ selectionEdges?.left && "yable-cell--selection-left",
2321
+ userClassName
1715
2322
  ].filter(Boolean).join(" ");
1716
2323
  return /* @__PURE__ */ jsxs(
1717
2324
  "td",
1718
2325
  {
1719
2326
  className: classNames,
1720
- style,
2327
+ style: mergedStyle,
1721
2328
  "data-editing": isEditing || void 0,
1722
2329
  "data-focused": isFocused || void 0,
1723
2330
  "data-pinned": pinned || void 0,
@@ -1857,7 +2464,38 @@ var CellErrorBoundary = class extends React3.Component {
1857
2464
  return this.props.children;
1858
2465
  }
1859
2466
  };
1860
- function TableBody({ table, clickableRows }) {
2467
+ function MasterDetail({
2468
+ row,
2469
+ table,
2470
+ colSpan,
2471
+ renderDetailPanel,
2472
+ animationClass
2473
+ }) {
2474
+ const renderer = renderDetailPanel ?? table.options.renderDetailPanel;
2475
+ if (!renderer) return null;
2476
+ const content = renderer(row);
2477
+ if (content == null) return null;
2478
+ const classes = [
2479
+ "yable-detail-row",
2480
+ "yable-detail-row--animated",
2481
+ animationClass
2482
+ ].filter(Boolean).join(" ");
2483
+ return /* @__PURE__ */ jsx(
2484
+ "tr",
2485
+ {
2486
+ className: classes,
2487
+ "data-detail-for": row.id,
2488
+ role: "row",
2489
+ "aria-label": `Details for row ${row.id}`,
2490
+ children: /* @__PURE__ */ jsx("td", { className: "yable-detail-cell", colSpan, role: "cell", children: /* @__PURE__ */ jsx("div", { className: "yable-detail-panel", children: /* @__PURE__ */ jsx("div", { className: "yable-detail-panel-inner", children: content }) }) })
2491
+ }
2492
+ );
2493
+ }
2494
+ function TableBody({
2495
+ table,
2496
+ clickableRows,
2497
+ colgroup
2498
+ }) {
1861
2499
  const rows = table.getRowModel().rows;
1862
2500
  const visibleColumns = table.getVisibleLeafColumns();
1863
2501
  const activeCell = table.getState().editing.activeCell;
@@ -1866,6 +2504,7 @@ function TableBody({ table, clickableRows }) {
1866
2504
  range: null,
1867
2505
  isDragging: false
1868
2506
  };
2507
+ const pendingValues = table.getState().editing.pendingValues ?? {};
1869
2508
  const options = table.options;
1870
2509
  const enableVirtualization = options.enableVirtualization ?? false;
1871
2510
  const scrollContainerRef = useRef(null);
@@ -1874,6 +2513,19 @@ function TableBody({ table, clickableRows }) {
1874
2513
  const estimateRowHeight = options.estimateRowHeight;
1875
2514
  const pretextHeights = options.pretextHeights ?? null;
1876
2515
  const pretextPrefixSums = options.pretextPrefixSums ?? null;
2516
+ const columnSizing = table.getState().columnSizing;
2517
+ const columnSizingHash = useMemo(() => {
2518
+ const entries = Object.entries(columnSizing);
2519
+ if (entries.length === 0) return 0;
2520
+ let h = 0;
2521
+ for (const [key, value] of entries) {
2522
+ for (let i = 0; i < key.length; i++) {
2523
+ h = h * 31 + key.charCodeAt(i) | 0;
2524
+ }
2525
+ h = h * 31 + (value | 0) | 0;
2526
+ }
2527
+ return h;
2528
+ }, [columnSizing]);
1877
2529
  const { virtualRows, totalHeight } = useVirtualization({
1878
2530
  containerRef: scrollContainerRef,
1879
2531
  totalRows: rows.length,
@@ -1881,7 +2533,8 @@ function TableBody({ table, clickableRows }) {
1881
2533
  overscan,
1882
2534
  estimateRowHeight,
1883
2535
  pretextHeights,
1884
- pretextPrefixSums
2536
+ pretextPrefixSums,
2537
+ columnSizingHash
1885
2538
  });
1886
2539
  const cellSelectionKey = cellSelection.range ? `${cellSelection.range.start.rowIndex}:${cellSelection.range.start.columnIndex}:${cellSelection.range.end.rowIndex}:${cellSelection.range.end.columnIndex}:${cellSelection.isDragging ? "dragging" : "idle"}` : `none:${cellSelection.isDragging ? "dragging" : "idle"}`;
1887
2540
  useEffect(() => {
@@ -1895,24 +2548,40 @@ function TableBody({ table, clickableRows }) {
1895
2548
  window.removeEventListener("mouseup", handleWindowMouseUp);
1896
2549
  };
1897
2550
  }, [table]);
2551
+ const renderRow = (row, rowIndex, pinnedPosition) => /* @__PURE__ */ jsx(
2552
+ MemoizedTableRow,
2553
+ {
2554
+ row,
2555
+ table,
2556
+ rowIndex,
2557
+ visibleColumns,
2558
+ isSelected: row.getIsSelected(),
2559
+ isExpanded: row.getIsExpanded(),
2560
+ activeColumnId: activeCell?.rowId === row.id ? activeCell.columnId : void 0,
2561
+ focusedColumnIndex: focusedCell?.rowIndex === rowIndex ? focusedCell.columnIndex : null,
2562
+ hasFocusedCell: focusedCell !== null,
2563
+ cellSelectionKey,
2564
+ pendingValuesKey: getPendingValuesKey(pendingValues[row.id]),
2565
+ clickable: clickableRows,
2566
+ pinnedPosition
2567
+ },
2568
+ row.id
2569
+ );
1898
2570
  if (!enableVirtualization) {
1899
- return /* @__PURE__ */ jsx("tbody", { className: "yable-tbody", children: rows.map((row, rowIndex) => /* @__PURE__ */ jsx(
1900
- MemoizedTableRow,
1901
- {
1902
- row,
1903
- table,
1904
- rowIndex,
1905
- visibleColumns,
1906
- isSelected: row.getIsSelected(),
1907
- isExpanded: row.getIsExpanded(),
1908
- activeColumnId: activeCell?.rowId === row.id ? activeCell.columnId : void 0,
1909
- focusedColumnIndex: focusedCell?.rowIndex === rowIndex ? focusedCell.columnIndex : null,
1910
- hasFocusedCell: focusedCell !== null,
1911
- cellSelectionKey,
1912
- clickable: clickableRows
1913
- },
1914
- row.id
1915
- )) });
2571
+ const rowPinning = table.getState().rowPinning;
2572
+ const hasPinnedRows = (rowPinning.top?.length ?? 0) > 0 || (rowPinning.bottom?.length ?? 0) > 0;
2573
+ if (hasPinnedRows) {
2574
+ const topRows = table.getTopRows();
2575
+ const centerRows = table.getCenterRows();
2576
+ const bottomRows = table.getBottomRows();
2577
+ let visualIndex = 0;
2578
+ return /* @__PURE__ */ jsxs("tbody", { className: "yable-tbody", children: [
2579
+ topRows.map((row) => renderRow(row, visualIndex++, "top")),
2580
+ centerRows.map((row) => renderRow(row, visualIndex++)),
2581
+ bottomRows.map((row) => renderRow(row, visualIndex++, "bottom"))
2582
+ ] });
2583
+ }
2584
+ return /* @__PURE__ */ jsx("tbody", { className: "yable-tbody", children: rows.map((row, rowIndex) => renderRow(row, rowIndex)) });
1916
2585
  }
1917
2586
  const hasPretextData = !!(pretextHeights && pretextPrefixSums);
1918
2587
  const fixedRowHeight = typeof rowHeight === "number" && !hasPretextData ? rowHeight : void 0;
@@ -1932,7 +2601,7 @@ function TableBody({ table, clickableRows }) {
1932
2601
  {
1933
2602
  className: "yable-virtual-spacer",
1934
2603
  style: { height: totalHeight, position: "relative" },
1935
- children: /* @__PURE__ */ jsx(
2604
+ children: /* @__PURE__ */ jsxs(
1936
2605
  "table",
1937
2606
  {
1938
2607
  style: {
@@ -1943,35 +2612,39 @@ function TableBody({ table, clickableRows }) {
1943
2612
  tableLayout: "fixed",
1944
2613
  borderCollapse: "collapse"
1945
2614
  },
1946
- children: /* @__PURE__ */ jsx("tbody", { children: virtualRows.map((vRow) => {
1947
- const row = rows[vRow.index];
1948
- if (!row) return null;
1949
- return /* @__PURE__ */ jsx(
1950
- MemoizedTableRow,
1951
- {
1952
- row,
1953
- table,
1954
- rowIndex: vRow.index,
1955
- visibleColumns,
1956
- isSelected: row.getIsSelected(),
1957
- isExpanded: row.getIsExpanded(),
1958
- activeColumnId: activeCell?.rowId === row.id ? activeCell.columnId : void 0,
1959
- focusedColumnIndex: focusedCell?.rowIndex === vRow.index ? focusedCell.columnIndex : null,
1960
- hasFocusedCell: focusedCell !== null,
1961
- cellSelectionKey,
1962
- clickable: clickableRows,
1963
- virtualStyle: {
1964
- position: "absolute",
1965
- top: 0,
1966
- left: 0,
1967
- width: "100%",
1968
- height: vRow.size,
1969
- transform: `translateY(${vRow.start}px)`
1970
- }
1971
- },
1972
- row.id
1973
- );
1974
- }) })
2615
+ children: [
2616
+ colgroup,
2617
+ /* @__PURE__ */ jsx("tbody", { children: virtualRows.map((vRow) => {
2618
+ const row = rows[vRow.index];
2619
+ if (!row) return null;
2620
+ return /* @__PURE__ */ jsx(
2621
+ MemoizedTableRow,
2622
+ {
2623
+ row,
2624
+ table,
2625
+ rowIndex: vRow.index,
2626
+ visibleColumns,
2627
+ isSelected: row.getIsSelected(),
2628
+ isExpanded: row.getIsExpanded(),
2629
+ activeColumnId: activeCell?.rowId === row.id ? activeCell.columnId : void 0,
2630
+ focusedColumnIndex: focusedCell?.rowIndex === vRow.index ? focusedCell.columnIndex : null,
2631
+ hasFocusedCell: focusedCell !== null,
2632
+ cellSelectionKey,
2633
+ pendingValuesKey: getPendingValuesKey(pendingValues[row.id]),
2634
+ clickable: clickableRows,
2635
+ virtualStyle: {
2636
+ position: "absolute",
2637
+ top: 0,
2638
+ left: 0,
2639
+ width: "100%",
2640
+ height: vRow.size,
2641
+ transform: `translateY(${vRow.start}px)`
2642
+ }
2643
+ },
2644
+ row.id
2645
+ );
2646
+ }) })
2647
+ ]
1975
2648
  }
1976
2649
  )
1977
2650
  }
@@ -1990,13 +2663,18 @@ function TableRowInner({
1990
2663
  focusedColumnIndex,
1991
2664
  hasFocusedCell,
1992
2665
  cellSelectionKey: _cellSelectionKey,
2666
+ pendingValuesKey: _pendingValuesKey,
1993
2667
  clickable,
2668
+ pinnedPosition,
1994
2669
  virtualStyle
1995
2670
  }) {
1996
2671
  const allCells = row.getAllCells();
1997
2672
  const visibleCells = visibleColumns.map((column) => allCells.find((cell) => cell.column.id === column.id)).filter((cell) => cell != null);
1998
2673
  const handleClick = useCallback(
1999
2674
  (e) => {
2675
+ if (table.options.enableRowClickSelection && row.getCanSelect() && !isInteractiveClickTarget2(e.target)) {
2676
+ row.toggleSelected();
2677
+ }
2000
2678
  if (clickable) {
2001
2679
  table.events.emit("row:click", {
2002
2680
  row,
@@ -2004,7 +2682,7 @@ function TableRowInner({
2004
2682
  });
2005
2683
  }
2006
2684
  },
2007
- [clickable, table.events, row]
2685
+ [clickable, table, row]
2008
2686
  );
2009
2687
  const handleDoubleClick = useCallback(
2010
2688
  (e) => {
@@ -2026,15 +2704,26 @@ function TableRowInner({
2026
2704
  );
2027
2705
  const selectionEnabled = Boolean(table.options.enableRowSelection);
2028
2706
  const expansionEnabled = Boolean(table.options.enableExpanding);
2707
+ const rowClassNameDef = table.options.rowClassName;
2708
+ const userRowClassName = typeof rowClassNameDef === "function" ? rowClassNameDef(row) : rowClassNameDef;
2709
+ const rowStyleDef = table.options.rowStyle;
2710
+ const userRowStyle = typeof rowStyleDef === "function" ? rowStyleDef(row) : rowStyleDef;
2711
+ const mergedRowStyle = userRowStyle ? { ...virtualStyle, ...userRowStyle } : virtualStyle;
2712
+ const rowClassName = [
2713
+ "yable-tr",
2714
+ pinnedPosition && `yable-tr--pinned-${pinnedPosition}`,
2715
+ userRowClassName
2716
+ ].filter(Boolean).join(" ");
2029
2717
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2030
2718
  /* @__PURE__ */ jsx(
2031
2719
  "tr",
2032
2720
  {
2033
- className: "yable-tr",
2034
- style: virtualStyle,
2721
+ className: rowClassName,
2722
+ style: mergedRowStyle,
2035
2723
  "data-selected": isSelected || void 0,
2036
2724
  "data-expanded": isExpanded || void 0,
2037
2725
  "data-clickable": clickable || void 0,
2726
+ "data-pinned-row": pinnedPosition,
2038
2727
  "data-row-id": row.id,
2039
2728
  "data-row-index": rowIndex,
2040
2729
  "aria-selected": selectionEnabled ? isSelected : void 0,
@@ -2066,7 +2755,7 @@ function TableRowInner({
2066
2755
  })
2067
2756
  }
2068
2757
  ),
2069
- isExpanded && /* @__PURE__ */ jsx("tr", { className: "yable-expand-row", children: /* @__PURE__ */ jsx("td", { className: "yable-td", colSpan: visibleColumns.length, children: typeof row._renderExpanded === "function" ? row._renderExpanded() : null }) })
2758
+ isExpanded && /* @__PURE__ */ jsx(MasterDetail, { row, table, colSpan: visibleColumns.length })
2070
2759
  ] });
2071
2760
  }
2072
2761
  function areRowPropsEqual(prev, next) {
@@ -2077,10 +2766,12 @@ function areRowPropsEqual(prev, next) {
2077
2766
  if (prev.isSelected !== next.isSelected) return false;
2078
2767
  if (prev.isExpanded !== next.isExpanded) return false;
2079
2768
  if (prev.clickable !== next.clickable) return false;
2769
+ if (prev.pinnedPosition !== next.pinnedPosition) return false;
2080
2770
  if (prev.activeColumnId !== next.activeColumnId) return false;
2081
2771
  if (prev.focusedColumnIndex !== next.focusedColumnIndex) return false;
2082
2772
  if (prev.hasFocusedCell !== next.hasFocusedCell) return false;
2083
2773
  if (prev.cellSelectionKey !== next.cellSelectionKey) return false;
2774
+ if (prev.pendingValuesKey !== next.pendingValuesKey) return false;
2084
2775
  if (prev.virtualStyle !== next.virtualStyle) {
2085
2776
  if (!prev.virtualStyle || !next.virtualStyle) return false;
2086
2777
  if (prev.virtualStyle.transform !== next.virtualStyle.transform) return false;
@@ -2090,6 +2781,16 @@ function areRowPropsEqual(prev, next) {
2090
2781
  return true;
2091
2782
  }
2092
2783
  var MemoizedTableRow = React3.memo(TableRowInner, areRowPropsEqual);
2784
+ function getPendingValuesKey(values) {
2785
+ if (!values) return "";
2786
+ return Object.keys(values).sort().map((key) => `${key}:${String(values[key])}`).join("|");
2787
+ }
2788
+ function isInteractiveClickTarget2(target) {
2789
+ if (!(target instanceof HTMLElement)) return false;
2790
+ return Boolean(
2791
+ target.closest('input, textarea, select, button, a[href], [contenteditable="true"]')
2792
+ );
2793
+ }
2093
2794
  function TableFooter({ table }) {
2094
2795
  const footerGroups = table.getFooterGroups();
2095
2796
  if (!footerGroups.length) return null;
@@ -2377,29 +3078,73 @@ function StatusBar({
2377
3078
  ] });
2378
3079
  }
2379
3080
  function SearchIcon() {
2380
- return /* @__PURE__ */ jsxs("svg", { className: "yable-sidebar-search-icon", width: "13", height: "13", viewBox: "0 0 14 14", fill: "none", "aria-hidden": "true", children: [
2381
- /* @__PURE__ */ jsx("circle", { cx: "6.25", cy: "6.25", r: "4.25", stroke: "currentColor", strokeWidth: "1.5" }),
2382
- /* @__PURE__ */ jsx("path", { d: "M9.5 9.5L12.5 12.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })
2383
- ] });
3081
+ return /* @__PURE__ */ jsxs(
3082
+ "svg",
3083
+ {
3084
+ className: "yable-sidebar-search-icon",
3085
+ width: "13",
3086
+ height: "13",
3087
+ viewBox: "0 0 14 14",
3088
+ fill: "none",
3089
+ "aria-hidden": "true",
3090
+ children: [
3091
+ /* @__PURE__ */ jsx("circle", { cx: "6.25", cy: "6.25", r: "4.25", stroke: "currentColor", strokeWidth: "1.5" }),
3092
+ /* @__PURE__ */ jsx("path", { d: "M9.5 9.5L12.5 12.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })
3093
+ ]
3094
+ }
3095
+ );
2384
3096
  }
2385
3097
  function VisibilityIcon({ visible }) {
2386
3098
  if (visible) {
2387
3099
  return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", "aria-hidden": "true", children: [
2388
- /* @__PURE__ */ jsx("path", { d: "M1 7s2.5-4 6-4 6 4 6 4-2.5 4-6 4-6-4-6-4z", stroke: "currentColor", strokeWidth: "1.2", strokeLinejoin: "round" }),
3100
+ /* @__PURE__ */ jsx(
3101
+ "path",
3102
+ {
3103
+ d: "M1 7s2.5-4 6-4 6 4 6 4-2.5 4-6 4-6-4-6-4z",
3104
+ stroke: "currentColor",
3105
+ strokeWidth: "1.2",
3106
+ strokeLinejoin: "round"
3107
+ }
3108
+ ),
2389
3109
  /* @__PURE__ */ jsx("circle", { cx: "7", cy: "7", r: "2", stroke: "currentColor", strokeWidth: "1.2" })
2390
3110
  ] });
2391
3111
  }
2392
3112
  return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", "aria-hidden": "true", children: [
2393
- /* @__PURE__ */ jsx("path", { d: "M1 7s2.5-4 6-4 6 4 6 4-2.5 4-6 4-6-4-6-4z", stroke: "currentColor", strokeWidth: "1.2", strokeLinejoin: "round", opacity: "0.3" }),
2394
- /* @__PURE__ */ jsx("line", { x1: "2", y1: "2", x2: "12", y2: "12", stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round", opacity: "0.5" })
3113
+ /* @__PURE__ */ jsx(
3114
+ "path",
3115
+ {
3116
+ d: "M1 7s2.5-4 6-4 6 4 6 4-2.5 4-6 4-6-4-6-4z",
3117
+ stroke: "currentColor",
3118
+ strokeWidth: "1.2",
3119
+ strokeLinejoin: "round",
3120
+ opacity: "0.3"
3121
+ }
3122
+ ),
3123
+ /* @__PURE__ */ jsx(
3124
+ "line",
3125
+ {
3126
+ x1: "2",
3127
+ y1: "2",
3128
+ x2: "12",
3129
+ y2: "12",
3130
+ stroke: "currentColor",
3131
+ strokeWidth: "1.2",
3132
+ strokeLinecap: "round",
3133
+ opacity: "0.5"
3134
+ }
3135
+ )
2395
3136
  ] });
2396
3137
  }
2397
- function ColumnsPanel({
2398
- table
2399
- }) {
3138
+ function ColumnsPanel({ table }) {
2400
3139
  const [search, setSearch] = useState("");
2401
3140
  const [draggedId, setDraggedId] = useState(null);
2402
- const columns = table.getAllLeafColumns();
3141
+ const allColumns = table.getAllLeafColumns();
3142
+ const columnOrder = table.getState().columnOrder;
3143
+ const columns = columnOrder && columnOrder.length > 0 ? [...allColumns].sort((a, b) => {
3144
+ const ia = columnOrder.indexOf(a.id);
3145
+ const ib = columnOrder.indexOf(b.id);
3146
+ return (ia === -1 ? Number.MAX_SAFE_INTEGER : ia) - (ib === -1 ? Number.MAX_SAFE_INTEGER : ib);
3147
+ }) : allColumns;
2403
3148
  const visibleCount = columns.filter((c) => c.getIsVisible()).length;
2404
3149
  const filteredColumns = search ? columns.filter((col) => {
2405
3150
  const header = typeof col.columnDef.header === "string" ? col.columnDef.header : col.id;
@@ -2826,6 +3571,7 @@ function ContextMenu({
2826
3571
  y,
2827
3572
  onClose,
2828
3573
  table,
3574
+ targetColumnId,
2829
3575
  customItems
2830
3576
  }) {
2831
3577
  const menuRef = useRef(null);
@@ -2869,6 +3615,17 @@ function ContextMenu({
2869
3615
  },
2870
3616
  [onClose]
2871
3617
  );
3618
+ const resolveSortColumn = () => {
3619
+ if (targetColumnId) {
3620
+ const column = table.getColumn(targetColumnId);
3621
+ if (column) return column;
3622
+ }
3623
+ const focused = table.getFocusedCell();
3624
+ if (focused) {
3625
+ return table.getVisibleLeafColumns()[focused.columnIndex];
3626
+ }
3627
+ return void 0;
3628
+ };
2872
3629
  const defaultItems = [
2873
3630
  {
2874
3631
  id: "copy",
@@ -2917,14 +3674,14 @@ function ContextMenu({
2917
3674
  id: "sort-asc",
2918
3675
  label: "Sort Ascending",
2919
3676
  action: () => {
2920
- table.setSorting([]);
3677
+ resolveSortColumn()?.toggleSorting(false);
2921
3678
  }
2922
3679
  },
2923
3680
  {
2924
3681
  id: "sort-desc",
2925
3682
  label: "Sort Descending",
2926
3683
  action: () => {
2927
- table.setSorting([]);
3684
+ resolveSortColumn()?.toggleSorting(true);
2928
3685
  }
2929
3686
  },
2930
3687
  {
@@ -3016,11 +3773,13 @@ function useContextMenu() {
3016
3773
  const [x, setX] = useState(0);
3017
3774
  const [y, setY] = useState(0);
3018
3775
  const [targetTable, setTargetTable] = useState(null);
3776
+ const [targetColumnId, setTargetColumnId] = useState(void 0);
3019
3777
  const open = useCallback(
3020
- (clientX, clientY, table) => {
3778
+ (clientX, clientY, table, columnId) => {
3021
3779
  setX(clientX);
3022
3780
  setY(clientY);
3023
3781
  setTargetTable(table);
3782
+ setTargetColumnId(columnId);
3024
3783
  setIsOpen(true);
3025
3784
  },
3026
3785
  []
@@ -3028,6 +3787,7 @@ function useContextMenu() {
3028
3787
  const close = useCallback(() => {
3029
3788
  setIsOpen(false);
3030
3789
  setTargetTable(null);
3790
+ setTargetColumnId(void 0);
3031
3791
  }, []);
3032
3792
  useEffect(() => {
3033
3793
  if (!isOpen) return;
@@ -3042,7 +3802,7 @@ function useContextMenu() {
3042
3802
  document.removeEventListener("click", handleClick);
3043
3803
  };
3044
3804
  }, [isOpen, close]);
3045
- return { isOpen, x, y, targetTable, open, close };
3805
+ return { isOpen, x, y, targetTable, targetColumnId, open, close };
3046
3806
  }
3047
3807
  function useKeyboardNavigation(table, options = {}) {
3048
3808
  const {
@@ -3250,11 +4010,13 @@ function filterHeaderGroups(groups, visibleColumnIds) {
3250
4010
  }
3251
4011
  function Table({
3252
4012
  table,
3253
- stickyHeader,
3254
- striped,
3255
- bordered,
3256
- compact,
3257
- theme,
4013
+ stickyHeader: stickyHeaderProp,
4014
+ striped: stripedProp,
4015
+ bordered: borderedProp,
4016
+ compact: compactProp,
4017
+ theme: themeProp,
4018
+ config,
4019
+ configProfile,
3258
4020
  clickableRows,
3259
4021
  footer,
3260
4022
  loading,
@@ -3268,21 +4030,43 @@ function Table({
3268
4030
  renderLoading,
3269
4031
  children,
3270
4032
  className,
3271
- direction,
4033
+ direction: directionProp,
3272
4034
  statusBar,
3273
4035
  statusBarPanels,
3274
4036
  sidebar,
3275
- sidebarPanels = ["columns", "filters"],
4037
+ sidebarPanels,
3276
4038
  defaultSidebarPanel,
3277
4039
  floatingFilters,
3278
4040
  columnVirtualization,
3279
4041
  columnVirtualizationOverscan,
3280
- ariaLabel,
4042
+ ariaLabel: ariaLabelProp,
3281
4043
  ...rest
3282
4044
  }) {
4045
+ const providerDefaults = useYableDefaults();
4046
+ const { tableProps: providerTableProps } = providerDefaults;
4047
+ const profile = resolveYableProfile(
4048
+ config ?? providerDefaults.config,
4049
+ configProfile ?? providerDefaults.tableProfile
4050
+ );
4051
+ const profileTableProps = profile.table;
4052
+ const stickyHeader = stickyHeaderProp ?? profileTableProps?.stickyHeader ?? providerTableProps?.stickyHeader;
4053
+ const striped = stripedProp ?? profileTableProps?.striped ?? providerTableProps?.striped;
4054
+ const bordered = borderedProp ?? profileTableProps?.bordered ?? providerTableProps?.bordered;
4055
+ const compact = compactProp ?? profileTableProps?.compact ?? providerTableProps?.compact;
4056
+ const theme = themeProp ?? profileTableProps?.theme ?? providerTableProps?.theme;
4057
+ const direction = directionProp ?? profileTableProps?.direction ?? providerTableProps?.direction;
4058
+ const ariaLabel = ariaLabelProp ?? profileTableProps?.ariaLabel ?? providerTableProps?.ariaLabel;
4059
+ const resolvedClickableRows = clickableRows ?? profileTableProps?.clickableRows;
4060
+ const resolvedStatusBar = statusBar ?? profileTableProps?.statusBar;
4061
+ const resolvedSidebar = sidebar ?? profileTableProps?.sidebar;
4062
+ const resolvedSidebarPanels = sidebarPanels ?? profileTableProps?.sidebarPanels ?? ["columns", "filters"];
4063
+ const resolvedDefaultSidebarPanel = defaultSidebarPanel ?? profileTableProps?.defaultSidebarPanel;
4064
+ const resolvedFloatingFilters = floatingFilters ?? profileTableProps?.floatingFilters;
4065
+ const resolvedColumnVirtualization = columnVirtualization ?? profileTableProps?.columnVirtualization;
4066
+ const resolvedColumnVirtualizationOverscan = columnVirtualizationOverscan ?? profileTableProps?.columnVirtualizationOverscan;
3283
4067
  const [sidebarOpen, setSidebarOpen] = useState(false);
3284
4068
  const [sidebarPanel, setSidebarPanel] = useState(
3285
- defaultSidebarPanel ?? "columns"
4069
+ resolvedDefaultSidebarPanel ?? "columns"
3286
4070
  );
3287
4071
  const containerRef = useRef(null);
3288
4072
  const horizontalScrollRef = useRef(null);
@@ -3306,12 +4090,14 @@ function Table({
3306
4090
  const allVisibleColumns = table.getVisibleLeafColumns();
3307
4091
  const hasPinnedColumns = table.getLeftVisibleLeafColumns().length > 0 || table.getRightVisibleLeafColumns().length > 0;
3308
4092
  const hasGroupedHeaders = table.getHeaderGroups().length > 1;
3309
- const canVirtualizeColumns = Boolean(columnVirtualization) && !hasPinnedColumns && !hasGroupedHeaders && allVisibleColumns.length > 0;
4093
+ const canVirtualizeColumns = Boolean(resolvedColumnVirtualization) && !hasPinnedColumns && !hasGroupedHeaders && allVisibleColumns.length > 0;
4094
+ const allVisibleColumnSizeSignature = allVisibleColumns.map((column) => `${column.id}:${column.getSize()}`).join("|");
3310
4095
  const columnVirtualState = useColumnVirtualization({
3311
4096
  containerRef: horizontalScrollRef,
3312
4097
  columns: allVisibleColumns,
3313
- overscan: columnVirtualizationOverscan ?? 2,
3314
- enabled: canVirtualizeColumns
4098
+ overscan: resolvedColumnVirtualizationOverscan ?? 2,
4099
+ enabled: canVirtualizeColumns,
4100
+ sizingKey: allVisibleColumnSizeSignature
3315
4101
  });
3316
4102
  const renderTable = useMemo(() => {
3317
4103
  if (!canVirtualizeColumns || !columnVirtualState.isVirtualized) {
@@ -3386,24 +4172,47 @@ function Table({
3386
4172
  const handleContextMenu = useCallback(
3387
4173
  (e) => {
3388
4174
  e.preventDefault();
3389
- contextMenu.open(e.clientX, e.clientY, table);
4175
+ const targetEl = e.target?.closest?.("[data-column-id]");
4176
+ const targetColumnId = targetEl?.getAttribute("data-column-id") ?? void 0;
4177
+ contextMenu.open(e.clientX, e.clientY, table, targetColumnId);
3390
4178
  },
3391
4179
  [contextMenu, table]
3392
4180
  );
3393
- const tableNode = /* @__PURE__ */ jsxs(
3394
- "table",
3395
- {
3396
- className: "yable-table",
3397
- style: columnVirtualState.isVirtualized ? {
4181
+ const visibleLeafColumns = renderTable.getVisibleLeafColumns();
4182
+ const visibleColumnTotalSize = visibleLeafColumns.reduce(
4183
+ (sum, column) => sum + column.getSize(),
4184
+ 0
4185
+ );
4186
+ const colgroup = visibleLeafColumns.length === 0 ? null : /* @__PURE__ */ jsx("colgroup", { children: visibleLeafColumns.map((col) => /* @__PURE__ */ jsx("col", { style: { width: col.getSize() } }, col.id)) });
4187
+ const outerTableStyle = useMemo(() => {
4188
+ if (columnVirtualState.isVirtualized) {
4189
+ return {
3398
4190
  width: columnVirtualState.visibleWidth,
3399
4191
  minWidth: columnVirtualState.visibleWidth,
3400
4192
  marginLeft: columnVirtualState.startOffset,
3401
4193
  tableLayout: "fixed"
3402
- } : void 0,
4194
+ };
4195
+ }
4196
+ return {
4197
+ minWidth: visibleColumnTotalSize || void 0,
4198
+ tableLayout: "fixed"
4199
+ };
4200
+ }, [
4201
+ columnVirtualState.isVirtualized,
4202
+ columnVirtualState.visibleWidth,
4203
+ columnVirtualState.startOffset,
4204
+ visibleColumnTotalSize
4205
+ ]);
4206
+ const tableNode = /* @__PURE__ */ jsxs(
4207
+ "table",
4208
+ {
4209
+ className: "yable-table",
4210
+ style: outerTableStyle,
3403
4211
  "data-column-virtualized": columnVirtualState.isVirtualized || void 0,
3404
4212
  children: [
3405
- /* @__PURE__ */ jsx(TableHeader, { table: renderTable, floatingFilters }),
3406
- /* @__PURE__ */ jsx(TableBody, { table: renderTable, clickableRows }),
4213
+ colgroup,
4214
+ /* @__PURE__ */ jsx(TableHeader, { table: renderTable, floatingFilters: resolvedFloatingFilters }),
4215
+ /* @__PURE__ */ jsx(TableBody, { table: renderTable, clickableRows: resolvedClickableRows, colgroup }),
3407
4216
  footer && /* @__PURE__ */ jsx(TableFooter, { table: renderTable })
3408
4217
  ]
3409
4218
  }
@@ -3469,18 +4278,43 @@ function Table({
3469
4278
  }
3470
4279
  ))
3471
4280
  ] }),
3472
- sidebar && /* @__PURE__ */ jsx(
4281
+ resolvedSidebar && !sidebarOpen && /* @__PURE__ */ jsx(
4282
+ "button",
4283
+ {
4284
+ type: "button",
4285
+ className: "yable-sidebar-trigger",
4286
+ "aria-label": "Open tool panel",
4287
+ title: "Open tool panel",
4288
+ onClick: () => setSidebarOpen(true),
4289
+ children: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
4290
+ /* @__PURE__ */ jsx(
4291
+ "rect",
4292
+ {
4293
+ x: "3",
4294
+ y: "4",
4295
+ width: "18",
4296
+ height: "16",
4297
+ rx: "2",
4298
+ stroke: "currentColor",
4299
+ strokeWidth: "2"
4300
+ }
4301
+ ),
4302
+ /* @__PURE__ */ jsx("line", { x1: "14", y1: "4", x2: "14", y2: "20", stroke: "currentColor", strokeWidth: "2" })
4303
+ ] })
4304
+ }
4305
+ ),
4306
+ resolvedSidebar && /* @__PURE__ */ jsx(
3473
4307
  Sidebar,
3474
4308
  {
3475
4309
  table,
3476
4310
  open: sidebarOpen,
3477
4311
  onClose: () => setSidebarOpen(false),
3478
- panels: sidebarPanels,
4312
+ panels: resolvedSidebarPanels,
3479
4313
  activePanel: sidebarPanel,
3480
4314
  onPanelChange: setSidebarPanel
3481
4315
  }
3482
4316
  ),
3483
- statusBar && /* @__PURE__ */ jsx(StatusBar, { table, panels: statusBarPanels }),
4317
+ resolvedStatusBar && /* @__PURE__ */ jsx(StatusBar, { table, panels: statusBarPanels }),
3484
4318
  children,
3485
4319
  contextMenu.isOpen && /* @__PURE__ */ jsx(
3486
4320
  ContextMenu,
@@ -3488,7 +4322,8 @@ function Table({
3488
4322
  x: contextMenu.x,
3489
4323
  y: contextMenu.y,
3490
4324
  onClose: contextMenu.close,
3491
- table
4325
+ table,
4326
+ targetColumnId: contextMenu.targetColumnId
3492
4327
  }
3493
4328
  ),
3494
4329
  /* @__PURE__ */ jsx(
@@ -4416,33 +5251,6 @@ function TreeToggle({
4416
5251
  }
4417
5252
  );
4418
5253
  }
4419
- function MasterDetail({
4420
- row,
4421
- table,
4422
- colSpan,
4423
- renderDetailPanel,
4424
- animationClass
4425
- }) {
4426
- const renderer = renderDetailPanel ?? table.options.renderDetailPanel;
4427
- if (!renderer) return null;
4428
- const content = renderer(row);
4429
- if (content == null) return null;
4430
- const classes = [
4431
- "yable-detail-row",
4432
- "yable-detail-row--animated",
4433
- animationClass
4434
- ].filter(Boolean).join(" ");
4435
- return /* @__PURE__ */ jsx(
4436
- "tr",
4437
- {
4438
- className: classes,
4439
- "data-detail-for": row.id,
4440
- role: "row",
4441
- "aria-label": `Details for row ${row.id}`,
4442
- children: /* @__PURE__ */ jsx("td", { className: "yable-detail-cell", colSpan, role: "cell", children: /* @__PURE__ */ jsx("div", { className: "yable-detail-panel", children: /* @__PURE__ */ jsx("div", { className: "yable-detail-panel-inner", children: content }) }) })
4443
- }
4444
- );
4445
- }
4446
5254
  function ExpandIcon({
4447
5255
  isExpanded,
4448
5256
  onClick,
@@ -5411,7 +6219,237 @@ function useTheme(options = {}) {
5411
6219
  containerRef
5412
6220
  };
5413
6221
  }
6222
+ function selectColumn(options = {}) {
6223
+ const { id = "_select", size = 40, headerAriaLabel = "Select all rows" } = options;
6224
+ return {
6225
+ id,
6226
+ header: ({ table }) => /* @__PURE__ */ jsx("label", { className: "yable-checkbox-hitbox", onClick: (event) => event.stopPropagation(), children: /* @__PURE__ */ jsx(
6227
+ "input",
6228
+ {
6229
+ type: "checkbox",
6230
+ className: "yable-checkbox",
6231
+ checked: table.getIsAllPageRowsSelected(),
6232
+ ref: (el) => {
6233
+ if (el)
6234
+ el.indeterminate = table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected();
6235
+ },
6236
+ onChange: () => table.toggleAllPageRowsSelected(),
6237
+ "aria-label": headerAriaLabel
6238
+ }
6239
+ ) }),
6240
+ cell: ({ row }) => /* @__PURE__ */ jsx("label", { className: "yable-checkbox-hitbox", onClick: (event) => event.stopPropagation(), children: /* @__PURE__ */ jsx(
6241
+ "input",
6242
+ {
6243
+ type: "checkbox",
6244
+ className: "yable-checkbox",
6245
+ checked: row.getIsSelected(),
6246
+ disabled: !row.getCanSelect(),
6247
+ onChange: row.getToggleSelectedHandler(),
6248
+ "aria-label": "Select row"
6249
+ }
6250
+ ) }),
6251
+ size,
6252
+ enableSorting: true,
6253
+ sortingFn: (rowA, rowB) => Number(rowA.getIsSelected()) - Number(rowB.getIsSelected()),
6254
+ enableColumnFilter: false,
6255
+ enableResizing: false,
6256
+ enableReorder: false,
6257
+ enableHiding: false,
6258
+ lockVisible: true
6259
+ };
6260
+ }
6261
+
6262
+ // src/presets/rowNumberColumn.tsx
6263
+ function rowNumberColumn(options = {}) {
6264
+ const { id = "_rowNumber", header = "#", size = 50, startFrom = 1 } = options;
6265
+ return {
6266
+ id,
6267
+ header,
6268
+ cell: ({ row }) => row.index + startFrom,
6269
+ size,
6270
+ enableSorting: false,
6271
+ enableColumnFilter: false,
6272
+ enableResizing: false,
6273
+ enableReorder: false,
6274
+ lockVisible: true
6275
+ };
6276
+ }
6277
+ function actionsColumn(options) {
6278
+ const { id = "_actions", header = "", size = 100, actions } = options;
6279
+ return {
6280
+ id,
6281
+ header,
6282
+ cell: (ctx) => {
6283
+ const items = typeof actions === "function" ? actions(ctx.row) : actions;
6284
+ return /* @__PURE__ */ jsx("div", { className: "yable-cell-actions", children: items.filter((a) => !a.hidden || !a.hidden(ctx.row)).map((action, i) => /* @__PURE__ */ jsx(
6285
+ "button",
6286
+ {
6287
+ type: "button",
6288
+ className: "yable-action-btn",
6289
+ disabled: action.disabled?.(ctx.row),
6290
+ onClick: (e) => {
6291
+ e.stopPropagation();
6292
+ action.onClick(ctx.row);
6293
+ },
6294
+ title: action.label,
6295
+ children: action.icon ?? action.label
6296
+ },
6297
+ i
6298
+ )) });
6299
+ },
6300
+ size,
6301
+ enableSorting: false,
6302
+ enableColumnFilter: false,
6303
+ enableResizing: false,
6304
+ enableReorder: false
6305
+ };
6306
+ }
6307
+ function expandColumn(options = {}) {
6308
+ const { id = "_expand", size = 40 } = options;
6309
+ return {
6310
+ id,
6311
+ header: () => null,
6312
+ cell: ({ row }) => {
6313
+ if (!row.getCanExpand()) return null;
6314
+ return /* @__PURE__ */ jsx(
6315
+ "button",
6316
+ {
6317
+ type: "button",
6318
+ className: "yable-expand-btn",
6319
+ onClick: (e) => {
6320
+ e.stopPropagation();
6321
+ row.toggleExpanded();
6322
+ },
6323
+ "aria-expanded": row.getIsExpanded(),
6324
+ "aria-label": row.getIsExpanded() ? "Collapse row" : "Expand row",
6325
+ children: /* @__PURE__ */ jsx(
6326
+ "span",
6327
+ {
6328
+ className: "yable-expand-icon",
6329
+ style: {
6330
+ display: "inline-block",
6331
+ transform: row.getIsExpanded() ? "rotate(90deg)" : "rotate(0deg)",
6332
+ transition: "transform 150ms ease"
6333
+ },
6334
+ children: "\u25B6"
6335
+ }
6336
+ )
6337
+ }
6338
+ );
6339
+ },
6340
+ size,
6341
+ enableSorting: false,
6342
+ enableColumnFilter: false,
6343
+ enableResizing: false,
6344
+ enableReorder: false,
6345
+ lockVisible: true
6346
+ };
6347
+ }
6348
+ function CellStack({ children, gap = 2 }) {
6349
+ return /* @__PURE__ */ jsx(
6350
+ "div",
6351
+ {
6352
+ className: "yable-cell-stack",
6353
+ style: {
6354
+ display: "flex",
6355
+ flexDirection: "column",
6356
+ gap
6357
+ },
6358
+ children
6359
+ }
6360
+ );
6361
+ }
6362
+ function CellRow({ children, gap = 6, align = "center", justify = "start" }) {
6363
+ const justifyMap = {
6364
+ start: "flex-start",
6365
+ center: "center",
6366
+ end: "flex-end",
6367
+ between: "space-between"
6368
+ };
6369
+ return /* @__PURE__ */ jsx(
6370
+ "div",
6371
+ {
6372
+ className: "yable-cell-row",
6373
+ style: {
6374
+ display: "flex",
6375
+ flexDirection: "row",
6376
+ alignItems: align === "baseline" ? "baseline" : align === "start" ? "flex-start" : align === "end" ? "flex-end" : "center",
6377
+ justifyContent: justifyMap[justify] || "flex-start",
6378
+ gap
6379
+ },
6380
+ children
6381
+ }
6382
+ );
6383
+ }
6384
+ function CellWithIcon({ icon, children, gap = 6, iconSize }) {
6385
+ return /* @__PURE__ */ jsxs(
6386
+ "div",
6387
+ {
6388
+ className: "yable-cell-with-icon",
6389
+ style: {
6390
+ display: "flex",
6391
+ flexDirection: "row",
6392
+ alignItems: "center",
6393
+ gap
6394
+ },
6395
+ children: [
6396
+ /* @__PURE__ */ jsx(
6397
+ "span",
6398
+ {
6399
+ className: "yable-cell-icon",
6400
+ style: {
6401
+ display: "inline-flex",
6402
+ alignItems: "center",
6403
+ justifyContent: "center",
6404
+ flexShrink: 0,
6405
+ ...iconSize ? { width: iconSize, height: iconSize } : {}
6406
+ },
6407
+ children: icon
6408
+ }
6409
+ ),
6410
+ /* @__PURE__ */ jsx("span", { className: "yable-cell-icon-content", style: { minWidth: 0 }, children })
6411
+ ]
6412
+ }
6413
+ );
6414
+ }
6415
+ function CellText({
6416
+ children,
6417
+ variant = "primary",
6418
+ bold,
6419
+ truncate,
6420
+ size = "md"
6421
+ }) {
6422
+ const fontSizeMap = { sm: "0.75rem", md: "0.875rem", lg: "1rem" };
6423
+ return /* @__PURE__ */ jsx(
6424
+ "span",
6425
+ {
6426
+ className: `yable-cell-text yable-cell-text--${variant}`,
6427
+ style: {
6428
+ fontSize: fontSizeMap[size],
6429
+ fontWeight: bold ? 600 : void 0,
6430
+ color: variant === "secondary" ? "var(--yable-text-secondary, #6b7280)" : variant === "muted" ? "var(--yable-text-muted, #9ca3af)" : void 0,
6431
+ ...truncate ? {
6432
+ overflow: "hidden",
6433
+ textOverflow: "ellipsis",
6434
+ whiteSpace: "nowrap"
6435
+ } : {}
6436
+ },
6437
+ children
6438
+ }
6439
+ );
6440
+ }
6441
+
6442
+ // src/utils/mergeEditChanges.ts
6443
+ function mergeEditChanges(data, changes, getRowId = (_, i) => String(i)) {
6444
+ const changeKeys = Object.keys(changes);
6445
+ if (changeKeys.length === 0) return data;
6446
+ return data.map((row, i) => {
6447
+ const id = getRowId(row, i);
6448
+ const patch = changes[id];
6449
+ return patch ? { ...row, ...patch } : row;
6450
+ });
6451
+ }
5414
6452
 
5415
- export { CellBadge, CellBoolean, CellCheckbox, CellCurrency, CellDate, CellDatePicker, CellErrorBoundary, CellInput, CellLink, CellNumeric, CellProgress, CellRating, CellSelect, CellStatus, CellStatusBadge, CellToggle, ColumnsPanel, ContextMenu, ContextMenuItem, DEFAULT_TEXT_RECIPE, DragHandle, ErrorBoundary, ExpandIcon, FillHandle, FiltersPanel, FlashCell, FloatingFilter, GlobalFilter, LoadingOverlay, MasterDetail, NoRowsOverlay, Pagination, PivotConfigPanel, PrintLayout, SetFilter, Sidebar, SortIndicator, StatusBar, StatusBarPanelComponent, Table, TableBody, TableCell, TableFooter, TableHeader, TableProvider, Tooltip, TreeToggle, getMeasureRecipeForCellType, getRegisteredCellTypes, resolveMeasureRecipe, useAutoMeasurements, useCellFlash, useClipboard, useColumnVirtualization, useContextMenu, useFillHandle, useKeyboardNavigation, usePretextMeasurement, usePrintLayout, useRowAnimation, useRowDrag, useTable, useTableContext, useTableRowHeights, useTheme, useTooltip, useVirtualization };
6453
+ export { CellBadge, CellBoolean, CellCheckbox, CellCurrency, CellDate, CellDatePicker, CellErrorBoundary, CellInput, CellLink, CellNumeric, CellProgress, CellRating, CellRow, CellSelect, CellStack, CellStatus, CellStatusBadge, CellText, CellToggle, CellWithIcon, ColumnsPanel, ContextMenu, ContextMenuItem, DEFAULT_TEXT_RECIPE, DragHandle, ErrorBoundary, ExpandIcon, FillHandle, FiltersPanel, FlashCell, FloatingFilter, GlobalFilter, LoadingOverlay, MasterDetail, NoRowsOverlay, Pagination, PivotConfigPanel, PrintLayout, SetFilter, Sidebar, SortIndicator, StatusBar, StatusBarPanelComponent, Table, TableBody, TableCell, TableFooter, TableHeader, TableProvider, Tooltip, TreeToggle, YableProvider, actionsColumn, applyYableConfigToColumns, createYableConfig, expandColumn, getMeasureRecipeForCellType, getRegisteredCellTypes, getYableDefaultColumnDef, mergeEditChanges, resolveMeasureRecipe, resolveYableProfile, rowNumberColumn, selectColumn, useAutoMeasurements, useCellFlash, useClipboard, useColumnVirtualization, useContextMenu, useFillHandle, useKeyboardNavigation, usePretextMeasurement, usePrintLayout, useRowAnimation, useRowDrag, useServerTable, useTable, useTableContext, useTablePersistence, useTableRowHeights, useTheme, useTooltip, useVirtualization, useYableDefaults };
5416
6454
  //# sourceMappingURL=index.js.map
5417
6455
  //# sourceMappingURL=index.js.map