@zvndev/yable-react 0.2.0 → 0.4.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,41 @@
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
5
  import { getInitialRowDragState, moveRow } from '@zvndev/yable-core/features/rowDragging';
6
6
 
7
7
  // src/index.ts
8
+ var YableContext = createContext({});
9
+ function useYableDefaults() {
10
+ return useContext(YableContext);
11
+ }
12
+ function YableProvider({
13
+ children,
14
+ defaultColumnDef,
15
+ striped,
16
+ stickyHeader,
17
+ bordered,
18
+ compact,
19
+ theme,
20
+ direction,
21
+ ariaLabel
22
+ }) {
23
+ const tableProps = {};
24
+ if (striped !== void 0) tableProps.striped = striped;
25
+ if (stickyHeader !== void 0) tableProps.stickyHeader = stickyHeader;
26
+ if (bordered !== void 0) tableProps.bordered = bordered;
27
+ if (compact !== void 0) tableProps.compact = compact;
28
+ if (theme !== void 0) tableProps.theme = theme;
29
+ if (direction !== void 0) tableProps.direction = direction;
30
+ if (ariaLabel !== void 0) tableProps.ariaLabel = ariaLabel;
31
+ const value = {
32
+ tableProps: Object.keys(tableProps).length > 0 ? tableProps : void 0,
33
+ defaultColumnDef
34
+ };
35
+ return /* @__PURE__ */ jsx(YableContext.Provider, { value, children });
36
+ }
37
+
38
+ // src/useTable.ts
8
39
  function shallowEqual(a, b) {
9
40
  if (a === b) return true;
10
41
  const keysA = Object.keys(a);
@@ -17,6 +48,17 @@ function shallowEqual(a, b) {
17
48
  return true;
18
49
  }
19
50
  function useTable(options) {
51
+ const providerDefaults = useYableDefaults();
52
+ const optionsWithDefaults = useMemo(() => {
53
+ if (!providerDefaults.defaultColumnDef) return options;
54
+ return {
55
+ ...options,
56
+ defaultColumnDef: {
57
+ ...providerDefaults.defaultColumnDef,
58
+ ...options.defaultColumnDef
59
+ }
60
+ };
61
+ }, [options, providerDefaults.defaultColumnDef]);
20
62
  const [state, setState] = useState(() => ({
21
63
  sorting: [],
22
64
  columnFilters: [],
@@ -56,14 +98,14 @@ function useTable(options) {
56
98
  }));
57
99
  const stateRef = useRef(state);
58
100
  stateRef.current = state;
59
- const prevOptionsRef = useRef(options);
101
+ const prevOptionsRef = useRef(optionsWithDefaults);
60
102
  const stableOptions = useMemo(() => {
61
- if (shallowEqual(prevOptionsRef.current, options)) {
103
+ if (shallowEqual(prevOptionsRef.current, optionsWithDefaults)) {
62
104
  return prevOptionsRef.current;
63
105
  }
64
- prevOptionsRef.current = options;
65
- return options;
66
- }, [options]);
106
+ prevOptionsRef.current = optionsWithDefaults;
107
+ return optionsWithDefaults;
108
+ }, [optionsWithDefaults]);
67
109
  const onStateChangeRef = useRef(options.onStateChange);
68
110
  onStateChangeRef.current = options.onStateChange;
69
111
  const resolvedState = useMemo(
@@ -73,17 +115,14 @@ function useTable(options) {
73
115
  }),
74
116
  [state, stableOptions.state]
75
117
  );
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
- );
118
+ const onStateChange = useCallback((updater) => {
119
+ const latest = onStateChangeRef.current;
120
+ if (latest) {
121
+ latest(updater);
122
+ } else {
123
+ setState((prev) => functionalUpdate(updater, prev));
124
+ }
125
+ }, []);
87
126
  const resolvedOptions = useMemo(
88
127
  () => ({
89
128
  ...stableOptions,
@@ -96,12 +135,14 @@ function useTable(options) {
96
135
  if (!tableRef.current) {
97
136
  tableRef.current = createTable(resolvedOptions);
98
137
  } else {
99
- tableRef.current.setOptions((prev) => ({
100
- ...prev,
101
- ...resolvedOptions,
102
- state: resolvedState,
103
- onStateChange
104
- }));
138
+ tableRef.current.setOptions(
139
+ (prev) => ({
140
+ ...prev,
141
+ ...resolvedOptions,
142
+ state: resolvedState,
143
+ onStateChange
144
+ })
145
+ );
105
146
  }
106
147
  useEffect(() => {
107
148
  return () => {
@@ -112,6 +153,122 @@ function useTable(options) {
112
153
  }, []);
113
154
  return tableRef.current;
114
155
  }
156
+ var DEFAULT_PERSISTED_KEYS = [
157
+ "columnVisibility",
158
+ "columnOrder",
159
+ "columnSizing",
160
+ "columnPinning"
161
+ ];
162
+ function resolveStorage(custom) {
163
+ if (custom) return custom;
164
+ if (typeof window !== "undefined") {
165
+ try {
166
+ return window.localStorage;
167
+ } catch {
168
+ return void 0;
169
+ }
170
+ }
171
+ return void 0;
172
+ }
173
+ function pick(obj, keys) {
174
+ const result = {};
175
+ for (const k of keys) {
176
+ if (k in obj) {
177
+ result[k] = obj[k];
178
+ }
179
+ }
180
+ return result;
181
+ }
182
+ function readState(storage, key, version, persistedKeys) {
183
+ if (!storage) return {};
184
+ try {
185
+ const raw = storage.getItem(key);
186
+ if (!raw) return {};
187
+ const envelope = JSON.parse(raw);
188
+ if (envelope.version !== version) {
189
+ storage.removeItem(key);
190
+ return {};
191
+ }
192
+ return pick(envelope.state, persistedKeys);
193
+ } catch {
194
+ if (typeof console !== "undefined") {
195
+ console.warn(`[yable] Failed to read persisted state for key "${key}"`);
196
+ }
197
+ return {};
198
+ }
199
+ }
200
+ function writeState(storage, key, version, state) {
201
+ if (!storage) return;
202
+ try {
203
+ const envelope = { version, state };
204
+ storage.setItem(key, JSON.stringify(envelope));
205
+ } catch {
206
+ if (typeof console !== "undefined") {
207
+ console.warn(`[yable] Failed to persist state for key "${key}" (storage may be full)`);
208
+ }
209
+ }
210
+ }
211
+ function useTablePersistence(options) {
212
+ const {
213
+ key,
214
+ persistedKeys = DEFAULT_PERSISTED_KEYS,
215
+ debounce: debounceMs = 100,
216
+ version = 0,
217
+ storage: customStorage
218
+ } = options;
219
+ const storage = useMemo(() => resolveStorage(customStorage), [customStorage]);
220
+ const initialState = useMemo(
221
+ () => readState(storage, key, version, persistedKeys),
222
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally read only on mount
223
+ []
224
+ );
225
+ const [state, setState] = useState(initialState);
226
+ const timerRef = useRef(null);
227
+ const keyRef = useRef(key);
228
+ keyRef.current = key;
229
+ const versionRef = useRef(version);
230
+ versionRef.current = version;
231
+ const persistedKeysRef = useRef(persistedKeys);
232
+ persistedKeysRef.current = persistedKeys;
233
+ const debounceRef = useRef(debounceMs);
234
+ debounceRef.current = debounceMs;
235
+ const storageRef = useRef(storage);
236
+ storageRef.current = storage;
237
+ const onStateChange = useCallback((updater) => {
238
+ setState((prev) => {
239
+ const next = functionalUpdate(updater, prev);
240
+ if (timerRef.current !== null) {
241
+ clearTimeout(timerRef.current);
242
+ }
243
+ timerRef.current = setTimeout(() => {
244
+ const sliced = pick(
245
+ next,
246
+ persistedKeysRef.current
247
+ );
248
+ writeState(storageRef.current, keyRef.current, versionRef.current, sliced);
249
+ timerRef.current = null;
250
+ }, debounceRef.current);
251
+ return next;
252
+ });
253
+ }, []);
254
+ useEffect(() => {
255
+ return () => {
256
+ if (timerRef.current !== null) {
257
+ clearTimeout(timerRef.current);
258
+ }
259
+ };
260
+ }, []);
261
+ const clearPersistedState = useCallback(() => {
262
+ const s = storageRef.current;
263
+ if (s) {
264
+ try {
265
+ s.removeItem(keyRef.current);
266
+ } catch {
267
+ }
268
+ }
269
+ }, []);
270
+ return { initialState, state, onStateChange, clearPersistedState };
271
+ }
115
272
  var EMPTY_RESULT = {
116
273
  virtualRows: [],
117
274
  totalHeight: 0,
@@ -125,11 +282,14 @@ function useVirtualization({
125
282
  overscan = 5,
126
283
  estimateRowHeight: _estimateRowHeight,
127
284
  pretextHeights,
128
- pretextPrefixSums
285
+ pretextPrefixSums,
286
+ columnSizingHash
129
287
  }) {
130
288
  const hasPretextHeights = !!(pretextHeights && pretextPrefixSums && pretextHeights.length >= totalRows);
131
289
  const isFixedHeight = typeof rowHeight === "number" && !hasPretextHeights;
132
290
  const heightCacheRef = useRef(/* @__PURE__ */ new Map());
291
+ const heightCacheVersionRef = useRef(0);
292
+ const [heightCacheVersion, setHeightCacheVersion] = useState(0);
133
293
  const getRowHeight = useCallback(
134
294
  (index) => {
135
295
  if (hasPretextHeights) return pretextHeights[index];
@@ -140,7 +300,9 @@ function useVirtualization({
140
300
  heightCacheRef.current.set(index, height);
141
301
  return height;
142
302
  },
143
- [rowHeight, isFixedHeight, hasPretextHeights, pretextHeights]
303
+ // heightCacheVersion forces a new callback identity after cache invalidation
304
+ // eslint-disable-next-line react-hooks/exhaustive-deps
305
+ [rowHeight, isFixedHeight, hasPretextHeights, pretextHeights, heightCacheVersion]
144
306
  );
145
307
  const [scrollState, setScrollState] = useState({ scrollTop: 0, containerHeight: 0 });
146
308
  const rafRef = useRef(null);
@@ -193,6 +355,18 @@ function useVirtualization({
193
355
  heightCacheRef.current.clear();
194
356
  }
195
357
  }, [totalRows, isFixedHeight]);
358
+ useEffect(() => {
359
+ if (!isFixedHeight && columnSizingHash !== void 0) {
360
+ heightCacheRef.current.clear();
361
+ heightCacheVersionRef.current += 1;
362
+ setHeightCacheVersion(heightCacheVersionRef.current);
363
+ }
364
+ }, [columnSizingHash]);
365
+ const invalidateRowHeights = useCallback(() => {
366
+ heightCacheRef.current.clear();
367
+ heightCacheVersionRef.current += 1;
368
+ setHeightCacheVersion(heightCacheVersionRef.current);
369
+ }, []);
196
370
  const scrollTo = useCallback(
197
371
  (index) => {
198
372
  const container = containerRef.current;
@@ -210,7 +384,15 @@ function useVirtualization({
210
384
  container.scrollTop = offset;
211
385
  }
212
386
  },
213
- [containerRef, totalRows, rowHeight, isFixedHeight, getRowHeight, hasPretextHeights, pretextPrefixSums]
387
+ [
388
+ containerRef,
389
+ totalRows,
390
+ rowHeight,
391
+ isFixedHeight,
392
+ getRowHeight,
393
+ hasPretextHeights,
394
+ pretextPrefixSums
395
+ ]
214
396
  );
215
397
  const totalHeight = useMemo(() => {
216
398
  if (totalRows === 0) return 0;
@@ -221,10 +403,18 @@ function useVirtualization({
221
403
  total += getRowHeight(i);
222
404
  }
223
405
  return total;
224
- }, [totalRows, rowHeight, isFixedHeight, getRowHeight, hasPretextHeights, pretextPrefixSums]);
406
+ }, [
407
+ totalRows,
408
+ rowHeight,
409
+ isFixedHeight,
410
+ getRowHeight,
411
+ hasPretextHeights,
412
+ pretextPrefixSums,
413
+ heightCacheVersion
414
+ ]);
225
415
  const { scrollTop, containerHeight } = scrollState;
226
416
  if (totalRows === 0) {
227
- return { ...EMPTY_RESULT, scrollTo };
417
+ return { ...EMPTY_RESULT, scrollTo, invalidateRowHeights };
228
418
  }
229
419
  if (containerHeight === 0) {
230
420
  return {
@@ -232,7 +422,8 @@ function useVirtualization({
232
422
  totalHeight,
233
423
  startIndex: 0,
234
424
  endIndex: 0,
235
- scrollTo
425
+ scrollTo,
426
+ invalidateRowHeights
236
427
  };
237
428
  }
238
429
  let startIndex = 0;
@@ -264,10 +455,7 @@ function useVirtualization({
264
455
  } else if (isFixedHeight) {
265
456
  const fixedH = rowHeight;
266
457
  startIndex = Math.floor(scrollTop / fixedH);
267
- endIndex = Math.min(
268
- totalRows - 1,
269
- Math.ceil((scrollTop + containerHeight) / fixedH) - 1
270
- );
458
+ endIndex = Math.min(totalRows - 1, Math.ceil((scrollTop + containerHeight) / fixedH) - 1);
271
459
  } else {
272
460
  let accum = 0;
273
461
  let foundStart = false;
@@ -331,7 +519,164 @@ function useVirtualization({
331
519
  totalHeight,
332
520
  startIndex: overscanStart,
333
521
  endIndex: overscanEnd,
334
- scrollTo
522
+ scrollTo,
523
+ invalidateRowHeights
524
+ };
525
+ }
526
+ function binarySearchOffsets(offsets, target) {
527
+ let low = 0;
528
+ let high = offsets.length - 1;
529
+ while (low < high) {
530
+ const mid = low + high >>> 1;
531
+ if (offsets[mid + 1] <= target) {
532
+ low = mid + 1;
533
+ } else {
534
+ high = mid;
535
+ }
536
+ }
537
+ return low;
538
+ }
539
+ function useColumnVirtualization({
540
+ containerRef,
541
+ columns,
542
+ overscan = 2,
543
+ enabled = true
544
+ }) {
545
+ const [scrollState, setScrollState] = useState({
546
+ scrollLeft: 0,
547
+ containerWidth: 0
548
+ });
549
+ const rafRef = useRef(null);
550
+ const sizes = useMemo(() => columns.map((column) => column.getSize()), [columns]);
551
+ const offsets = useMemo(() => {
552
+ const next = new Array(columns.length + 1);
553
+ next[0] = 0;
554
+ for (let i = 0; i < columns.length; i++) {
555
+ next[i + 1] = next[i] + (sizes[i] ?? 0);
556
+ }
557
+ return next;
558
+ }, [columns.length, sizes]);
559
+ const totalWidth = offsets[offsets.length - 1] ?? 0;
560
+ useEffect(() => {
561
+ const container = containerRef.current;
562
+ if (!container) return;
563
+ setScrollState({
564
+ scrollLeft: container.scrollLeft,
565
+ containerWidth: container.clientWidth
566
+ });
567
+ const handleScroll = () => {
568
+ if (rafRef.current !== null) return;
569
+ rafRef.current = requestAnimationFrame(() => {
570
+ rafRef.current = null;
571
+ const el = containerRef.current;
572
+ if (!el) return;
573
+ setScrollState({
574
+ scrollLeft: el.scrollLeft,
575
+ containerWidth: el.clientWidth
576
+ });
577
+ });
578
+ };
579
+ container.addEventListener("scroll", handleScroll, { passive: true });
580
+ let resizeObserver;
581
+ if (typeof ResizeObserver !== "undefined") {
582
+ resizeObserver = new ResizeObserver(() => {
583
+ const el = containerRef.current;
584
+ if (!el) return;
585
+ setScrollState((prev) => {
586
+ const nextWidth = el.clientWidth;
587
+ if (prev.containerWidth === nextWidth && prev.scrollLeft === el.scrollLeft) {
588
+ return prev;
589
+ }
590
+ return {
591
+ scrollLeft: el.scrollLeft,
592
+ containerWidth: nextWidth
593
+ };
594
+ });
595
+ });
596
+ resizeObserver.observe(container);
597
+ }
598
+ return () => {
599
+ container.removeEventListener("scroll", handleScroll);
600
+ if (rafRef.current !== null) {
601
+ cancelAnimationFrame(rafRef.current);
602
+ rafRef.current = null;
603
+ }
604
+ resizeObserver?.disconnect();
605
+ };
606
+ }, [containerRef]);
607
+ const scrollToIndex = useCallback(
608
+ (index) => {
609
+ const container = containerRef.current;
610
+ if (!container || columns.length === 0) return;
611
+ const clampedIndex = Math.max(0, Math.min(index, columns.length - 1));
612
+ container.scrollLeft = offsets[clampedIndex] ?? 0;
613
+ },
614
+ [columns.length, containerRef, offsets]
615
+ );
616
+ if (!enabled || columns.length === 0) {
617
+ return {
618
+ virtualColumns: columns.map((column, index) => ({
619
+ column,
620
+ index,
621
+ start: offsets[index] ?? 0,
622
+ size: sizes[index] ?? 0
623
+ })),
624
+ startOffset: 0,
625
+ endOffset: 0,
626
+ totalWidth,
627
+ visibleWidth: totalWidth,
628
+ startIndex: 0,
629
+ endIndex: Math.max(columns.length - 1, 0),
630
+ isVirtualized: false,
631
+ scrollToIndex
632
+ };
633
+ }
634
+ const { scrollLeft, containerWidth } = scrollState;
635
+ if (containerWidth <= 0 || totalWidth <= containerWidth) {
636
+ return {
637
+ virtualColumns: columns.map((column, index) => ({
638
+ column,
639
+ index,
640
+ start: offsets[index] ?? 0,
641
+ size: sizes[index] ?? 0
642
+ })),
643
+ startOffset: 0,
644
+ endOffset: 0,
645
+ totalWidth,
646
+ visibleWidth: totalWidth,
647
+ startIndex: 0,
648
+ endIndex: Math.max(columns.length - 1, 0),
649
+ isVirtualized: false,
650
+ scrollToIndex
651
+ };
652
+ }
653
+ const startIndex = binarySearchOffsets(offsets, scrollLeft);
654
+ const endBoundary = scrollLeft + containerWidth;
655
+ const endIndex = binarySearchOffsets(offsets, Math.max(scrollLeft, endBoundary - 1));
656
+ const overscanStart = Math.max(0, startIndex - overscan);
657
+ const overscanEnd = Math.min(columns.length - 1, endIndex + overscan);
658
+ const virtualColumns = [];
659
+ for (let i = overscanStart; i <= overscanEnd; i++) {
660
+ virtualColumns.push({
661
+ column: columns[i],
662
+ index: i,
663
+ start: offsets[i] ?? 0,
664
+ size: sizes[i] ?? 0
665
+ });
666
+ }
667
+ const startOffset = offsets[overscanStart] ?? 0;
668
+ const visibleWidth = (offsets[overscanEnd + 1] ?? totalWidth) - startOffset;
669
+ const endOffset = totalWidth - (offsets[overscanEnd + 1] ?? totalWidth);
670
+ return {
671
+ virtualColumns,
672
+ startOffset,
673
+ endOffset,
674
+ totalWidth,
675
+ visibleWidth,
676
+ startIndex: overscanStart,
677
+ endIndex: overscanEnd,
678
+ isVirtualized: true,
679
+ scrollToIndex
335
680
  };
336
681
  }
337
682
  var pretextPromise = null;
@@ -384,7 +729,12 @@ function usePretextMeasurement({
384
729
  }
385
730
  prepareTimeRef.current = performance.now() - start;
386
731
  return result;
387
- }, [pretext, enabled, data, columns.map((c) => `${c.columnId}:${c.font}:${c.fixedHeight ? "F" : "T"}`).join("|")]);
732
+ }, [
733
+ pretext,
734
+ enabled,
735
+ data,
736
+ columns.map((c) => `${c.columnId}:${c.font}:${c.fixedHeight ? "F" : "T"}`).join("|")
737
+ ]);
388
738
  const measurement = useMemo(() => {
389
739
  if (!pretext || !preparedCells) {
390
740
  return { rowHeights: null, prefixSums: null, totalHeight: 0 };
@@ -422,6 +772,7 @@ function usePretextMeasurement({
422
772
  return {
423
773
  rowHeights,
424
774
  prefixSums,
775
+ // Safe: prefixSums has length data.length + 1, so [data.length] always exists
425
776
  totalHeight: prefixSums[data.length]
426
777
  };
427
778
  }, [pretext, preparedCells, columnWidthsKey, minRowHeight, data.length]);
@@ -686,18 +1037,10 @@ function CellDate({
686
1037
  if (raw == null) return null;
687
1038
  const date = raw instanceof Date ? raw : new Date(String(raw));
688
1039
  if (isNaN(date.getTime())) return /* @__PURE__ */ jsx("span", { className: "yable-cell-date", children: String(raw) });
689
- const formatter = useMemo(() => {
690
- if (typeof format === "string" && format !== "relative") {
691
- return new Intl.DateTimeFormat(locale, {
692
- ...PRESETS[format],
693
- timeZone: "UTC"
694
- });
695
- }
696
- if (typeof format === "object") {
697
- return new Intl.DateTimeFormat(locale, { ...format, timeZone: "UTC" });
698
- }
699
- return null;
700
- }, [format, locale]);
1040
+ const formatter = typeof format === "string" && format !== "relative" ? new Intl.DateTimeFormat(locale, {
1041
+ ...PRESETS[format],
1042
+ timeZone: "UTC"
1043
+ }) : typeof format === "object" ? new Intl.DateTimeFormat(locale, { ...format, timeZone: "UTC" }) : null;
701
1044
  const formatted = format === "relative" ? formatRelative(date) : formatter ? formatter.format(date) : date.toLocaleDateString(locale);
702
1045
  return /* @__PURE__ */ jsx("span", { className: `yable-cell-date ${className ?? ""}`, title: date.toISOString(), children: formatted });
703
1046
  }
@@ -708,8 +1051,14 @@ var measureRecipe9 = {
708
1051
  padding: 20
709
1052
  };
710
1053
  function isSafeUrl(url) {
711
- const normalized = String(url).toLowerCase().trim();
712
- return !normalized.startsWith("javascript:") && !normalized.startsWith("data:text/html") && !normalized.startsWith("vbscript:");
1054
+ const normalized = String(url).trim();
1055
+ if (/^[/#?]/.test(normalized)) return true;
1056
+ try {
1057
+ const parsed = new URL(normalized, "https://placeholder.invalid");
1058
+ return ["http:", "https:", "mailto:", "tel:"].includes(parsed.protocol);
1059
+ } catch {
1060
+ return false;
1061
+ }
713
1062
  }
714
1063
  function CellLink({
715
1064
  context,
@@ -886,7 +1235,7 @@ function useTableContext() {
886
1235
  const ctx = useContext(TableContext);
887
1236
  if (!ctx) {
888
1237
  throw new Error(
889
- "[yable] useTableContext must be used within a <Table> component. Did you forget to pass the `table` prop?"
1238
+ "[yable E001] useTableContext must be used within a <Table> component. Did you forget to pass the `table` prop?"
890
1239
  );
891
1240
  }
892
1241
  return ctx;
@@ -936,12 +1285,234 @@ function SortIndicator({ direction, index }) {
936
1285
  }
937
1286
  );
938
1287
  }
1288
+ function formatValue(value) {
1289
+ if (value == null || value === "") return "(empty)";
1290
+ if (typeof value === "string") return value;
1291
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1292
+ if (value instanceof Date) return value.toISOString();
1293
+ return JSON.stringify(value);
1294
+ }
1295
+ function SetFilter({ column, className }) {
1296
+ const [open, setOpen] = useState(false);
1297
+ const filterValue = column.getFilterValue();
1298
+ const selectedValues = Array.isArray(filterValue) ? filterValue : filterValue == null || filterValue === "" ? [] : [filterValue];
1299
+ const facetedUniqueValues = column.getFacetedUniqueValues();
1300
+ const options = Array.from(facetedUniqueValues.entries()).map(([value, count]) => ({
1301
+ value,
1302
+ count,
1303
+ label: formatValue(value)
1304
+ })).sort((a, b) => a.label.localeCompare(b.label));
1305
+ const selectedSet = new Set(selectedValues);
1306
+ const toggleValue = (value) => {
1307
+ const next = new Set(selectedSet);
1308
+ if (next.has(value)) {
1309
+ next.delete(value);
1310
+ } else {
1311
+ next.add(value);
1312
+ }
1313
+ const nextValues = Array.from(next);
1314
+ column.setFilterValue(nextValues.length > 0 ? nextValues : void 0);
1315
+ };
1316
+ const headerLabel = typeof column.columnDef.header === "string" ? column.columnDef.header : column.id;
1317
+ return /* @__PURE__ */ jsxs(
1318
+ "div",
1319
+ {
1320
+ className: ["yable-set-filter", className].filter(Boolean).join(" "),
1321
+ style: { position: "relative" },
1322
+ children: [
1323
+ /* @__PURE__ */ jsx(
1324
+ "button",
1325
+ {
1326
+ type: "button",
1327
+ className: "yable-set-filter-trigger",
1328
+ "aria-haspopup": "dialog",
1329
+ "aria-expanded": open,
1330
+ onClick: () => setOpen((prev) => !prev),
1331
+ style: {
1332
+ width: "100%",
1333
+ minHeight: 28,
1334
+ padding: "4px 8px",
1335
+ border: "1px solid rgba(127, 127, 127, 0.35)",
1336
+ borderRadius: 6,
1337
+ background: "transparent",
1338
+ font: "inherit",
1339
+ textAlign: "left",
1340
+ cursor: "pointer"
1341
+ },
1342
+ children: selectedValues.length > 0 ? `${selectedValues.length} selected` : `Filter ${headerLabel}`
1343
+ }
1344
+ ),
1345
+ open && /* @__PURE__ */ jsxs(
1346
+ "div",
1347
+ {
1348
+ role: "dialog",
1349
+ "aria-label": `${headerLabel} set filter`,
1350
+ className: "yable-set-filter-popover",
1351
+ style: {
1352
+ position: "absolute",
1353
+ insetInlineStart: 0,
1354
+ top: "calc(100% + 4px)",
1355
+ zIndex: 20,
1356
+ width: 220,
1357
+ maxHeight: 240,
1358
+ overflow: "auto",
1359
+ padding: 8,
1360
+ border: "1px solid rgba(127, 127, 127, 0.35)",
1361
+ borderRadius: 8,
1362
+ background: "var(--yable-color-bg, #fff)",
1363
+ boxShadow: "0 10px 30px rgba(0, 0, 0, 0.12)"
1364
+ },
1365
+ children: [
1366
+ /* @__PURE__ */ jsxs(
1367
+ "div",
1368
+ {
1369
+ style: {
1370
+ display: "flex",
1371
+ alignItems: "center",
1372
+ justifyContent: "space-between",
1373
+ gap: 8,
1374
+ marginBottom: 8
1375
+ },
1376
+ children: [
1377
+ /* @__PURE__ */ jsx("strong", { style: { fontSize: 12 }, children: headerLabel }),
1378
+ selectedValues.length > 0 && /* @__PURE__ */ jsx(
1379
+ "button",
1380
+ {
1381
+ type: "button",
1382
+ onClick: () => column.setFilterValue(void 0),
1383
+ style: {
1384
+ border: "none",
1385
+ background: "transparent",
1386
+ padding: 0,
1387
+ cursor: "pointer",
1388
+ fontSize: 12
1389
+ },
1390
+ children: "Clear"
1391
+ }
1392
+ )
1393
+ ]
1394
+ }
1395
+ ),
1396
+ /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": `${headerLabel} options`, children: [
1397
+ options.length === 0 && /* @__PURE__ */ jsx("div", { style: { fontSize: 12, opacity: 0.75 }, children: "No values" }),
1398
+ options.map((option) => {
1399
+ const checked = selectedSet.has(option.value);
1400
+ return /* @__PURE__ */ jsxs(
1401
+ "label",
1402
+ {
1403
+ style: {
1404
+ display: "flex",
1405
+ alignItems: "center",
1406
+ gap: 8,
1407
+ padding: "4px 0",
1408
+ fontSize: 12
1409
+ },
1410
+ children: [
1411
+ /* @__PURE__ */ jsx(
1412
+ "input",
1413
+ {
1414
+ type: "checkbox",
1415
+ checked,
1416
+ onChange: () => toggleValue(option.value)
1417
+ }
1418
+ ),
1419
+ /* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: option.label }),
1420
+ /* @__PURE__ */ jsx("span", { style: { opacity: 0.6 }, children: option.count })
1421
+ ]
1422
+ },
1423
+ `${option.label}-${option.count}`
1424
+ );
1425
+ })
1426
+ ] })
1427
+ ]
1428
+ }
1429
+ )
1430
+ ]
1431
+ }
1432
+ );
1433
+ }
1434
+ function isSetCompatibleValue(value) {
1435
+ return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
1436
+ }
1437
+ function inferFilterVariant(column) {
1438
+ const meta = column.columnDef.meta ?? {};
1439
+ if (meta.filterVariant) return meta.filterVariant;
1440
+ const uniqueValues = column.getFacetedUniqueValues();
1441
+ if (uniqueValues.size > 0 && uniqueValues.size <= 12) {
1442
+ const allValuesSupported = Array.from(uniqueValues.keys()).every(isSetCompatibleValue);
1443
+ if (allValuesSupported) return "set";
1444
+ }
1445
+ return "text";
1446
+ }
1447
+ function FloatingFilter({ column }) {
1448
+ const variant = inferFilterVariant(column);
1449
+ if (!column.getCanFilter()) {
1450
+ return /* @__PURE__ */ jsx("div", { style: { minHeight: 28 }, "aria-hidden": "true" });
1451
+ }
1452
+ if (variant === "set") {
1453
+ return /* @__PURE__ */ jsx(SetFilter, { column });
1454
+ }
1455
+ const filterValue = column.getFilterValue();
1456
+ const normalizedValue = typeof filterValue === "string" || typeof filterValue === "number" ? String(filterValue) : "";
1457
+ const label = typeof column.columnDef.header === "string" ? column.columnDef.header : column.id;
1458
+ return /* @__PURE__ */ jsx("label", { style: { display: "block" }, children: /* @__PURE__ */ jsx(
1459
+ "input",
1460
+ {
1461
+ "aria-label": `Filter ${label}`,
1462
+ className: "yable-floating-filter-input",
1463
+ value: normalizedValue,
1464
+ onChange: (event) => {
1465
+ const nextValue = event.target.value;
1466
+ column.setFilterValue(nextValue === "" ? void 0 : nextValue);
1467
+ },
1468
+ placeholder: `Filter ${label}`,
1469
+ style: {
1470
+ width: "100%",
1471
+ minHeight: 28,
1472
+ padding: "4px 8px",
1473
+ border: "1px solid rgba(127, 127, 127, 0.35)",
1474
+ borderRadius: 6,
1475
+ background: "transparent",
1476
+ font: "inherit"
1477
+ }
1478
+ }
1479
+ ) });
1480
+ }
939
1481
  var DRAG_MIME = "application/yable-column";
940
1482
  function TableHeader({
941
- table
1483
+ table,
1484
+ floatingFilters = false
942
1485
  }) {
943
1486
  const headerGroups = table.getHeaderGroups();
944
- return /* @__PURE__ */ jsx("thead", { className: "yable-thead", children: headerGroups.map((headerGroup) => /* @__PURE__ */ jsx("tr", { className: "yable-header-row", children: headerGroup.headers.map((header) => /* @__PURE__ */ jsx(HeaderCell, { header, table }, header.id)) }, headerGroup.id)) });
1487
+ const visibleColumns = table.getVisibleLeafColumns();
1488
+ return /* @__PURE__ */ jsxs("thead", { className: "yable-thead", children: [
1489
+ headerGroups.map((headerGroup) => /* @__PURE__ */ jsx("tr", { className: "yable-header-row", children: headerGroup.headers.map((header) => /* @__PURE__ */ jsx(HeaderCell, { header, table }, header.id)) }, headerGroup.id)),
1490
+ 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`)) })
1491
+ ] });
1492
+ }
1493
+ function FloatingFilterCell({
1494
+ column
1495
+ }) {
1496
+ const style = useMemo(() => {
1497
+ const next = {
1498
+ width: column.getSize(),
1499
+ minWidth: column.columnDef.minSize,
1500
+ maxWidth: column.columnDef.maxSize,
1501
+ padding: 6,
1502
+ verticalAlign: "top"
1503
+ };
1504
+ const pinned = column.getIsPinned();
1505
+ if (pinned) {
1506
+ next.position = "sticky";
1507
+ if (pinned === "left") {
1508
+ next.left = column.getStart("left");
1509
+ } else {
1510
+ next.right = column.getStart("right");
1511
+ }
1512
+ }
1513
+ return next;
1514
+ }, [column]);
1515
+ return /* @__PURE__ */ jsx("th", { className: "yable-th yable-th--filter", role: "columnheader", style, children: /* @__PURE__ */ jsx(FloatingFilter, { column }) });
945
1516
  }
946
1517
  function HeaderCell({
947
1518
  header,
@@ -995,11 +1566,6 @@ function HeaderCell({
995
1566
  const handleDragStart = useCallback(
996
1567
  (e) => {
997
1568
  if (!canReorder) return;
998
- const target = e.target;
999
- if (target && target.closest(".yable-resize-handle")) {
1000
- e.preventDefault();
1001
- return;
1002
- }
1003
1569
  e.stopPropagation();
1004
1570
  e.dataTransfer.effectAllowed = "move";
1005
1571
  try {
@@ -1007,8 +1573,9 @@ function HeaderCell({
1007
1573
  e.dataTransfer.setData("text/plain", column.id);
1008
1574
  } catch {
1009
1575
  }
1576
+ table.setColumnDragActive(true);
1010
1577
  },
1011
- [canReorder, column.id]
1578
+ [canReorder, column.id, table]
1012
1579
  );
1013
1580
  const handleDragOver = useCallback(
1014
1581
  (e) => {
@@ -1030,17 +1597,15 @@ function HeaderCell({
1030
1597
  },
1031
1598
  [canReorder]
1032
1599
  );
1033
- const handleDragLeave = useCallback(
1034
- (e) => {
1035
- const next = e.relatedTarget;
1036
- if (next && e.currentTarget.contains(next)) return;
1037
- setDragOver(null);
1038
- },
1039
- []
1040
- );
1041
- const handleDragEnd = useCallback(() => {
1600
+ const handleDragLeave = useCallback((e) => {
1601
+ const next = e.relatedTarget;
1602
+ if (next && e.currentTarget.contains(next)) return;
1042
1603
  setDragOver(null);
1043
1604
  }, []);
1605
+ const handleDragEnd = useCallback(() => {
1606
+ setDragOver(null);
1607
+ table.setColumnDragActive(false);
1608
+ }, [table]);
1044
1609
  const handleDrop = useCallback(
1045
1610
  (e) => {
1046
1611
  if (!canReorder) return;
@@ -1050,6 +1615,7 @@ function HeaderCell({
1050
1615
  const rect = e.currentTarget.getBoundingClientRect();
1051
1616
  const insertAfter = e.clientX >= rect.left + rect.width / 2;
1052
1617
  setDragOver(null);
1618
+ table.setColumnDragActive(false);
1053
1619
  if (!sourceId || sourceId === column.id) return;
1054
1620
  const state = table.getState();
1055
1621
  const allLeafs = table.getAllLeafColumns();
@@ -1100,24 +1666,24 @@ function HeaderCell({
1100
1666
  "aria-sort": sortDirection === "asc" ? "ascending" : sortDirection === "desc" ? "descending" : canSort ? "none" : void 0,
1101
1667
  role: "columnheader",
1102
1668
  colSpan: header.colSpan,
1103
- draggable: canReorder || void 0,
1104
1669
  onClick: handleHeaderClick,
1105
- onDragStart: canReorder ? handleDragStart : void 0,
1106
1670
  onDragOver: canReorder ? handleDragOver : void 0,
1107
1671
  onDragLeave: canReorder ? handleDragLeave : void 0,
1108
- onDragEnd: canReorder ? handleDragEnd : void 0,
1109
1672
  onDrop: canReorder ? handleDrop : void 0,
1110
1673
  children: [
1111
- /* @__PURE__ */ jsxs("div", { className: "yable-th-content", children: [
1112
- /* @__PURE__ */ jsx("span", { children: headerContent }),
1113
- canSort && /* @__PURE__ */ jsx(
1114
- SortIndicator,
1115
- {
1116
- direction: sortDirection,
1117
- index: sortIndex > 0 ? sortIndex : void 0
1118
- }
1119
- )
1120
- ] }),
1674
+ /* @__PURE__ */ jsxs(
1675
+ "div",
1676
+ {
1677
+ className: "yable-th-content",
1678
+ draggable: canReorder || void 0,
1679
+ onDragStart: canReorder ? handleDragStart : void 0,
1680
+ onDragEnd: canReorder ? handleDragEnd : void 0,
1681
+ children: [
1682
+ /* @__PURE__ */ jsx("span", { children: headerContent }),
1683
+ canSort && /* @__PURE__ */ jsx(SortIndicator, { direction: sortDirection, index: sortIndex > 0 ? sortIndex : void 0 })
1684
+ ]
1685
+ }
1686
+ ),
1121
1687
  canResize && /* @__PURE__ */ jsx(
1122
1688
  "div",
1123
1689
  {
@@ -1125,9 +1691,7 @@ function HeaderCell({
1125
1691
  "data-resizing": column.getIsResizing() || void 0,
1126
1692
  onMouseDown: startResize,
1127
1693
  onTouchStart: startResize,
1128
- onClick: handleResizeClick,
1129
- draggable: false,
1130
- onDragStart: (e) => e.preventDefault()
1694
+ onClick: handleResizeClick
1131
1695
  }
1132
1696
  )
1133
1697
  ]
@@ -1241,6 +1805,10 @@ function TableCell({
1241
1805
  const cellStatus = table.getCellStatus(cell.row.id, column.id);
1242
1806
  const cellErrorMessage = table.getCellErrorMessage(cell.row.id, column.id);
1243
1807
  const cellConflictWith = table.getCellConflictWith(cell.row.id, column.id);
1808
+ const selectionRange = table.getCellSelectionRange();
1809
+ const selectionEdges = table.getCellSelectionEdges(rowIndex, columnIndex);
1810
+ const isCellSelected = selectionEdges !== null;
1811
+ const isMultiCellSelection = selectionRange !== null && (selectionRange.start.rowIndex !== selectionRange.end.rowIndex || selectionRange.start.columnIndex !== selectionRange.end.columnIndex);
1244
1812
  const overrideValue = cellStatus !== "idle" ? table.getCellRenderValue(cell.row.id, column.id) : void 0;
1245
1813
  let content;
1246
1814
  const cellDef = column.columnDef.cell;
@@ -1264,23 +1832,17 @@ function TableCell({
1264
1832
  }
1265
1833
  const handleClick = useCallback(
1266
1834
  (e) => {
1267
- table.setFocusedCell({ rowIndex, columnIndex });
1268
- const clickTarget = e.target;
1269
- if (!isInteractiveClickTarget(clickTarget)) {
1270
- const currentTarget = e.currentTarget;
1271
- currentTarget.focus({ preventScroll: true });
1272
- }
1273
1835
  table.events.emit("cell:click", {
1274
1836
  cell,
1275
1837
  row: cell.row,
1276
1838
  column: cell.column,
1277
1839
  event: e.nativeEvent
1278
1840
  });
1279
- if (canCellEnterEditMode(table, cell.row, column) && !isAlwaysEditable && !isEditing) {
1841
+ if (canCellEnterEditMode(table, cell.row, column) && !isAlwaysEditable && !isEditing && !isMultiCellSelection && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
1280
1842
  table.startEditing(cell.row.id, column.id);
1281
1843
  }
1282
1844
  },
1283
- [cell, column, columnIndex, isAlwaysEditable, isEditing, rowIndex, table]
1845
+ [cell, column, isAlwaysEditable, isEditing, isMultiCellSelection, table]
1284
1846
  );
1285
1847
  const handleDoubleClick = useCallback(
1286
1848
  (e) => {
@@ -1308,15 +1870,46 @@ function TableCell({
1308
1870
  if (!keyboardNavigationEnabled) return;
1309
1871
  table.setFocusedCell({ rowIndex, columnIndex });
1310
1872
  }, [columnIndex, keyboardNavigationEnabled, rowIndex, table]);
1873
+ const handleMouseDown = useCallback(
1874
+ (e) => {
1875
+ if (e.button !== 0) return;
1876
+ const clickTarget = e.target;
1877
+ if (isInteractiveClickTarget(clickTarget)) return;
1878
+ e.preventDefault();
1879
+ table.setFocusedCell({ rowIndex, columnIndex });
1880
+ table.startCellRangeSelection({ rowIndex, columnIndex }, { extend: e.shiftKey });
1881
+ e.currentTarget.focus({ preventScroll: true });
1882
+ },
1883
+ [columnIndex, rowIndex, table]
1884
+ );
1885
+ const handleMouseEnter = useCallback(() => {
1886
+ if (!table.getState().cellSelection?.isDragging) return;
1887
+ table.updateCellRangeSelection({ rowIndex, columnIndex });
1888
+ }, [columnIndex, rowIndex, table]);
1889
+ const handleMouseUp = useCallback(() => {
1890
+ if (!table.getState().cellSelection?.isDragging) return;
1891
+ table.endCellRangeSelection();
1892
+ }, [table]);
1893
+ const cellClassNameDef = column.columnDef.cellClassName;
1894
+ const userClassName = typeof cellClassNameDef === "function" ? cellClassNameDef(cell.getContext()) : cellClassNameDef;
1895
+ const cellStyleDef = column.columnDef.cellStyle;
1896
+ const userStyle = typeof cellStyleDef === "function" ? cellStyleDef(cell.getContext()) : cellStyleDef;
1897
+ const mergedStyle = userStyle ? { ...style, ...userStyle } : style;
1311
1898
  const classNames = [
1312
1899
  "yable-td",
1313
- isFocused && "yable-cell--focused"
1900
+ isFocused && "yable-cell--focused",
1901
+ isCellSelected && "yable-cell--selected",
1902
+ selectionEdges?.top && "yable-cell--selection-top",
1903
+ selectionEdges?.right && "yable-cell--selection-right",
1904
+ selectionEdges?.bottom && "yable-cell--selection-bottom",
1905
+ selectionEdges?.left && "yable-cell--selection-left",
1906
+ userClassName
1314
1907
  ].filter(Boolean).join(" ");
1315
1908
  return /* @__PURE__ */ jsxs(
1316
1909
  "td",
1317
1910
  {
1318
1911
  className: classNames,
1319
- style,
1912
+ style: mergedStyle,
1320
1913
  "data-editing": isEditing || void 0,
1321
1914
  "data-focused": isFocused || void 0,
1322
1915
  "data-pinned": pinned || void 0,
@@ -1324,10 +1917,14 @@ function TableCell({
1324
1917
  "data-column-id": column.id,
1325
1918
  "data-row-index": rowIndex,
1326
1919
  "data-column-index": columnIndex,
1920
+ "data-cell-selected": isCellSelected || void 0,
1327
1921
  "aria-rowindex": rowIndex + 1,
1328
1922
  "aria-colindex": columnIndex + 1,
1329
1923
  role: "gridcell",
1330
1924
  tabIndex: keyboardNavigationEnabled ? isTabStop ? 0 : -1 : void 0,
1925
+ onMouseDown: handleMouseDown,
1926
+ onMouseEnter: handleMouseEnter,
1927
+ onMouseUp: handleMouseUp,
1331
1928
  onClick: handleClick,
1332
1929
  onDoubleClick: handleDoubleClick,
1333
1930
  onContextMenu: handleContextMenu,
@@ -1454,12 +2051,17 @@ var CellErrorBoundary = class extends React3.Component {
1454
2051
  };
1455
2052
  function TableBody({
1456
2053
  table,
1457
- clickableRows
2054
+ clickableRows,
2055
+ colgroup
1458
2056
  }) {
1459
2057
  const rows = table.getRowModel().rows;
1460
2058
  const visibleColumns = table.getVisibleLeafColumns();
1461
2059
  const activeCell = table.getState().editing.activeCell;
1462
2060
  const focusedCell = table.getFocusedCell();
2061
+ const cellSelection = table.getState().cellSelection ?? {
2062
+ range: null,
2063
+ isDragging: false
2064
+ };
1463
2065
  const options = table.options;
1464
2066
  const enableVirtualization = options.enableVirtualization ?? false;
1465
2067
  const scrollContainerRef = useRef(null);
@@ -1468,6 +2070,19 @@ function TableBody({
1468
2070
  const estimateRowHeight = options.estimateRowHeight;
1469
2071
  const pretextHeights = options.pretextHeights ?? null;
1470
2072
  const pretextPrefixSums = options.pretextPrefixSums ?? null;
2073
+ const columnSizing = table.getState().columnSizing;
2074
+ const columnSizingHash = useMemo(() => {
2075
+ const entries = Object.entries(columnSizing);
2076
+ if (entries.length === 0) return 0;
2077
+ let h = 0;
2078
+ for (const [key, value] of entries) {
2079
+ for (let i = 0; i < key.length; i++) {
2080
+ h = h * 31 + key.charCodeAt(i) | 0;
2081
+ }
2082
+ h = h * 31 + (value | 0) | 0;
2083
+ }
2084
+ return h;
2085
+ }, [columnSizing]);
1471
2086
  const { virtualRows, totalHeight } = useVirtualization({
1472
2087
  containerRef: scrollContainerRef,
1473
2088
  totalRows: rows.length,
@@ -1475,8 +2090,21 @@ function TableBody({
1475
2090
  overscan,
1476
2091
  estimateRowHeight,
1477
2092
  pretextHeights,
1478
- pretextPrefixSums
2093
+ pretextPrefixSums,
2094
+ columnSizingHash
1479
2095
  });
2096
+ 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"}`;
2097
+ useEffect(() => {
2098
+ const handleWindowMouseUp = () => {
2099
+ if (table.getState().cellSelection?.isDragging) {
2100
+ table.endCellRangeSelection();
2101
+ }
2102
+ };
2103
+ window.addEventListener("mouseup", handleWindowMouseUp);
2104
+ return () => {
2105
+ window.removeEventListener("mouseup", handleWindowMouseUp);
2106
+ };
2107
+ }, [table]);
1480
2108
  if (!enableVirtualization) {
1481
2109
  return /* @__PURE__ */ jsx("tbody", { className: "yable-tbody", children: rows.map((row, rowIndex) => /* @__PURE__ */ jsx(
1482
2110
  MemoizedTableRow,
@@ -1490,6 +2118,7 @@ function TableBody({
1490
2118
  activeColumnId: activeCell?.rowId === row.id ? activeCell.columnId : void 0,
1491
2119
  focusedColumnIndex: focusedCell?.rowIndex === rowIndex ? focusedCell.columnIndex : null,
1492
2120
  hasFocusedCell: focusedCell !== null,
2121
+ cellSelectionKey,
1493
2122
  clickable: clickableRows
1494
2123
  },
1495
2124
  row.id
@@ -1498,73 +2127,70 @@ function TableBody({
1498
2127
  const hasPretextData = !!(pretextHeights && pretextPrefixSums);
1499
2128
  const fixedRowHeight = typeof rowHeight === "number" && !hasPretextData ? rowHeight : void 0;
1500
2129
  const containerHeight = hasPretextData ? Math.min(totalHeight, 800) : fixedRowHeight ? Math.min(totalHeight, fixedRowHeight * 20) : 600;
1501
- return /* @__PURE__ */ jsx("tbody", { className: "yable-tbody", children: /* @__PURE__ */ jsx("tr", { style: { height: 0, padding: 0, border: "none" }, children: /* @__PURE__ */ jsx(
1502
- "td",
2130
+ return /* @__PURE__ */ jsx("tbody", { className: "yable-tbody", children: /* @__PURE__ */ jsx("tr", { style: { height: 0, padding: 0, border: "none" }, children: /* @__PURE__ */ jsx("td", { style: { height: 0, padding: 0, border: "none" }, colSpan: visibleColumns.length, children: /* @__PURE__ */ jsx(
2131
+ "div",
1503
2132
  {
1504
- style: { height: 0, padding: 0, border: "none" },
1505
- colSpan: visibleColumns.length,
2133
+ ref: scrollContainerRef,
2134
+ className: "yable-virtual-scroll-container",
2135
+ style: {
2136
+ overflowY: "auto",
2137
+ height: containerHeight,
2138
+ position: "relative"
2139
+ },
1506
2140
  children: /* @__PURE__ */ jsx(
1507
2141
  "div",
1508
2142
  {
1509
- ref: scrollContainerRef,
1510
- className: "yable-virtual-scroll-container",
1511
- style: {
1512
- overflowY: "auto",
1513
- height: containerHeight,
1514
- position: "relative"
1515
- },
1516
- children: /* @__PURE__ */ jsx(
1517
- "div",
2143
+ className: "yable-virtual-spacer",
2144
+ style: { height: totalHeight, position: "relative" },
2145
+ children: /* @__PURE__ */ jsxs(
2146
+ "table",
1518
2147
  {
1519
- className: "yable-virtual-spacer",
1520
- style: { height: totalHeight, position: "relative" },
1521
- children: /* @__PURE__ */ jsx(
1522
- "table",
1523
- {
1524
- style: {
1525
- position: "absolute",
1526
- top: 0,
1527
- left: 0,
1528
- width: "100%",
1529
- tableLayout: "fixed",
1530
- borderCollapse: "collapse"
1531
- },
1532
- children: /* @__PURE__ */ jsx("tbody", { children: virtualRows.map((vRow) => {
1533
- const row = rows[vRow.index];
1534
- if (!row) return null;
1535
- return /* @__PURE__ */ jsx(
1536
- MemoizedTableRow,
1537
- {
1538
- row,
1539
- table,
1540
- rowIndex: vRow.index,
1541
- visibleColumns,
1542
- isSelected: row.getIsSelected(),
1543
- isExpanded: row.getIsExpanded(),
1544
- activeColumnId: activeCell?.rowId === row.id ? activeCell.columnId : void 0,
1545
- focusedColumnIndex: focusedCell?.rowIndex === vRow.index ? focusedCell.columnIndex : null,
1546
- hasFocusedCell: focusedCell !== null,
1547
- clickable: clickableRows,
1548
- virtualStyle: {
1549
- position: "absolute",
1550
- top: 0,
1551
- left: 0,
1552
- width: "100%",
1553
- height: vRow.size,
1554
- transform: `translateY(${vRow.start}px)`
1555
- }
1556
- },
1557
- row.id
1558
- );
1559
- }) })
1560
- }
1561
- )
2148
+ style: {
2149
+ position: "absolute",
2150
+ top: 0,
2151
+ left: 0,
2152
+ width: "100%",
2153
+ tableLayout: "fixed",
2154
+ borderCollapse: "collapse"
2155
+ },
2156
+ children: [
2157
+ colgroup,
2158
+ /* @__PURE__ */ jsx("tbody", { children: virtualRows.map((vRow) => {
2159
+ const row = rows[vRow.index];
2160
+ if (!row) return null;
2161
+ return /* @__PURE__ */ jsx(
2162
+ MemoizedTableRow,
2163
+ {
2164
+ row,
2165
+ table,
2166
+ rowIndex: vRow.index,
2167
+ visibleColumns,
2168
+ isSelected: row.getIsSelected(),
2169
+ isExpanded: row.getIsExpanded(),
2170
+ activeColumnId: activeCell?.rowId === row.id ? activeCell.columnId : void 0,
2171
+ focusedColumnIndex: focusedCell?.rowIndex === vRow.index ? focusedCell.columnIndex : null,
2172
+ hasFocusedCell: focusedCell !== null,
2173
+ cellSelectionKey,
2174
+ clickable: clickableRows,
2175
+ virtualStyle: {
2176
+ position: "absolute",
2177
+ top: 0,
2178
+ left: 0,
2179
+ width: "100%",
2180
+ height: vRow.size,
2181
+ transform: `translateY(${vRow.start}px)`
2182
+ }
2183
+ },
2184
+ row.id
2185
+ );
2186
+ }) })
2187
+ ]
1562
2188
  }
1563
2189
  )
1564
2190
  }
1565
2191
  )
1566
2192
  }
1567
- ) }) });
2193
+ ) }) }) });
1568
2194
  }
1569
2195
  function TableRowInner({
1570
2196
  row,
@@ -1576,10 +2202,12 @@ function TableRowInner({
1576
2202
  activeColumnId,
1577
2203
  focusedColumnIndex,
1578
2204
  hasFocusedCell,
2205
+ cellSelectionKey: _cellSelectionKey,
1579
2206
  clickable,
1580
2207
  virtualStyle
1581
2208
  }) {
1582
- const visibleCells = row.getVisibleCells();
2209
+ const allCells = row.getAllCells();
2210
+ const visibleCells = visibleColumns.map((column) => allCells.find((cell) => cell.column.id === column.id)).filter((cell) => cell != null);
1583
2211
  const handleClick = useCallback(
1584
2212
  (e) => {
1585
2213
  if (clickable) {
@@ -1609,6 +2237,8 @@ function TableRowInner({
1609
2237
  },
1610
2238
  [table.events, row]
1611
2239
  );
2240
+ const selectionEnabled = Boolean(table.options.enableRowSelection);
2241
+ const expansionEnabled = Boolean(table.options.enableExpanding);
1612
2242
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1613
2243
  /* @__PURE__ */ jsx(
1614
2244
  "tr",
@@ -1620,6 +2250,8 @@ function TableRowInner({
1620
2250
  "data-clickable": clickable || void 0,
1621
2251
  "data-row-id": row.id,
1622
2252
  "data-row-index": rowIndex,
2253
+ "aria-selected": selectionEnabled ? isSelected : void 0,
2254
+ "aria-expanded": expansionEnabled ? isExpanded : void 0,
1623
2255
  onClick: handleClick,
1624
2256
  onDoubleClick: handleDoubleClick,
1625
2257
  onContextMenu: handleContextMenu,
@@ -1661,6 +2293,7 @@ function areRowPropsEqual(prev, next) {
1661
2293
  if (prev.activeColumnId !== next.activeColumnId) return false;
1662
2294
  if (prev.focusedColumnIndex !== next.focusedColumnIndex) return false;
1663
2295
  if (prev.hasFocusedCell !== next.hasFocusedCell) return false;
2296
+ if (prev.cellSelectionKey !== next.cellSelectionKey) return false;
1664
2297
  if (prev.virtualStyle !== next.virtualStyle) {
1665
2298
  if (!prev.virtualStyle || !next.virtualStyle) return false;
1666
2299
  if (prev.virtualStyle.transform !== next.virtualStyle.transform) return false;
@@ -1670,9 +2303,7 @@ function areRowPropsEqual(prev, next) {
1670
2303
  return true;
1671
2304
  }
1672
2305
  var MemoizedTableRow = React3.memo(TableRowInner, areRowPropsEqual);
1673
- function TableFooter({
1674
- table
1675
- }) {
2306
+ function TableFooter({ table }) {
1676
2307
  const footerGroups = table.getFooterGroups();
1677
2308
  if (!footerGroups.length) return null;
1678
2309
  return /* @__PURE__ */ jsx("tfoot", { className: "yable-tfoot", children: footerGroups.map((footerGroup) => /* @__PURE__ */ jsx("tr", { className: "yable-tr", children: footerGroup.headers.map((header) => {
@@ -2435,11 +3066,11 @@ function ContextMenu({
2435
3066
  }
2436
3067
  if (e.key === "ArrowDown" || e.key === "ArrowUp") {
2437
3068
  e.preventDefault();
2438
- const items2 = menuRef.current?.querySelectorAll('[role="menuitem"]:not([aria-disabled="true"])');
2439
- if (!items2 || items2.length === 0) return;
2440
- const currentIndex = Array.from(items2).findIndex(
2441
- (el) => el === document.activeElement
3069
+ const items2 = menuRef.current?.querySelectorAll(
3070
+ '[role="menuitem"]:not([aria-disabled="true"])'
2442
3071
  );
3072
+ if (!items2 || items2.length === 0) return;
3073
+ const currentIndex = Array.from(items2).findIndex((el) => el === document.activeElement);
2443
3074
  let nextIndex;
2444
3075
  if (e.key === "ArrowDown") {
2445
3076
  nextIndex = currentIndex < items2.length - 1 ? currentIndex + 1 : 0;
@@ -2484,11 +3115,7 @@ function ContextMenu({
2484
3115
  navigator.clipboard?.readText().then((text) => {
2485
3116
  const editing = table.getState().editing;
2486
3117
  if (editing?.activeCell) {
2487
- table.pasteFromClipboard(
2488
- text,
2489
- editing.activeCell.rowId,
2490
- editing.activeCell.columnId
2491
- );
3118
+ table.pasteFromClipboard(text, editing.activeCell.rowId, editing.activeCell.columnId);
2492
3119
  }
2493
3120
  }).catch(() => {
2494
3121
  });
@@ -2828,13 +3455,19 @@ function isEditableTarget(element) {
2828
3455
  }
2829
3456
  return element.isContentEditable;
2830
3457
  }
3458
+ function filterHeaderGroups(groups, visibleColumnIds) {
3459
+ return groups.map((group) => ({
3460
+ ...group,
3461
+ headers: group.headers.filter((header) => visibleColumnIds.has(header.column.id))
3462
+ })).filter((group) => group.headers.length > 0);
3463
+ }
2831
3464
  function Table({
2832
3465
  table,
2833
- stickyHeader,
2834
- striped,
2835
- bordered,
2836
- compact,
2837
- theme,
3466
+ stickyHeader: stickyHeaderProp,
3467
+ striped: stripedProp,
3468
+ bordered: borderedProp,
3469
+ compact: compactProp,
3470
+ theme: themeProp,
2838
3471
  clickableRows,
2839
3472
  footer,
2840
3473
  loading,
@@ -2848,19 +3481,32 @@ function Table({
2848
3481
  renderLoading,
2849
3482
  children,
2850
3483
  className,
2851
- direction,
3484
+ direction: directionProp,
2852
3485
  statusBar,
2853
3486
  statusBarPanels,
2854
3487
  sidebar,
2855
3488
  sidebarPanels = ["columns", "filters"],
2856
3489
  defaultSidebarPanel,
3490
+ floatingFilters,
3491
+ columnVirtualization,
3492
+ columnVirtualizationOverscan,
3493
+ ariaLabel: ariaLabelProp,
2857
3494
  ...rest
2858
3495
  }) {
3496
+ const { tableProps: providerTableProps } = useYableDefaults();
3497
+ const stickyHeader = stickyHeaderProp ?? providerTableProps?.stickyHeader;
3498
+ const striped = stripedProp ?? providerTableProps?.striped;
3499
+ const bordered = borderedProp ?? providerTableProps?.bordered;
3500
+ const compact = compactProp ?? providerTableProps?.compact;
3501
+ const theme = themeProp ?? providerTableProps?.theme;
3502
+ const direction = directionProp ?? providerTableProps?.direction;
3503
+ const ariaLabel = ariaLabelProp ?? providerTableProps?.ariaLabel;
2859
3504
  const [sidebarOpen, setSidebarOpen] = useState(false);
2860
3505
  const [sidebarPanel, setSidebarPanel] = useState(
2861
3506
  defaultSidebarPanel ?? "columns"
2862
3507
  );
2863
3508
  const containerRef = useRef(null);
3509
+ const horizontalScrollRef = useRef(null);
2864
3510
  const isRtl = direction === "rtl";
2865
3511
  const classNames = [
2866
3512
  "yable",
@@ -2878,8 +3524,86 @@ function Table({
2878
3524
  const hasGlobalFilter = Boolean(table.getState().globalFilter);
2879
3525
  const hasColumnFilters = table.getState().columnFilters.length > 0;
2880
3526
  const isFiltered = hasGlobalFilter || hasColumnFilters;
3527
+ const allVisibleColumns = table.getVisibleLeafColumns();
3528
+ const hasPinnedColumns = table.getLeftVisibleLeafColumns().length > 0 || table.getRightVisibleLeafColumns().length > 0;
3529
+ const hasGroupedHeaders = table.getHeaderGroups().length > 1;
3530
+ const canVirtualizeColumns = Boolean(columnVirtualization) && !hasPinnedColumns && !hasGroupedHeaders && allVisibleColumns.length > 0;
3531
+ const columnVirtualState = useColumnVirtualization({
3532
+ containerRef: horizontalScrollRef,
3533
+ columns: allVisibleColumns,
3534
+ overscan: columnVirtualizationOverscan ?? 2,
3535
+ enabled: canVirtualizeColumns
3536
+ });
3537
+ const renderTable = useMemo(() => {
3538
+ if (!canVirtualizeColumns || !columnVirtualState.isVirtualized) {
3539
+ return table;
3540
+ }
3541
+ const virtualColumns = columnVirtualState.virtualColumns.map((entry) => entry.column);
3542
+ const visibleColumnIds = new Set(virtualColumns.map((column) => column.id));
3543
+ const next = Object.create(table);
3544
+ next.getVisibleFlatColumns = () => virtualColumns;
3545
+ next.getVisibleLeafColumns = () => virtualColumns;
3546
+ next.getCenterVisibleLeafColumns = () => virtualColumns;
3547
+ next.getLeftVisibleLeafColumns = () => [];
3548
+ next.getRightVisibleLeafColumns = () => [];
3549
+ next.getHeaderGroups = () => filterHeaderGroups(table.getHeaderGroups(), visibleColumnIds);
3550
+ next.getCenterHeaderGroups = () => filterHeaderGroups(table.getCenterHeaderGroups(), visibleColumnIds);
3551
+ next.getFooterGroups = () => filterHeaderGroups(table.getFooterGroups(), visibleColumnIds);
3552
+ next.getCenterFooterGroups = () => filterHeaderGroups(table.getCenterFooterGroups(), visibleColumnIds);
3553
+ return next;
3554
+ }, [
3555
+ canVirtualizeColumns,
3556
+ columnVirtualState.isVirtualized,
3557
+ columnVirtualState.virtualColumns,
3558
+ table
3559
+ ]);
3560
+ const showColumnVirtualizationShell = canVirtualizeColumns;
2881
3561
  const contextMenu = useContextMenu();
2882
3562
  useKeyboardNavigation(table, { containerRef });
3563
+ const [announcement, setAnnouncement] = useState("");
3564
+ const prevSortingRef = useRef(table.getState().sorting);
3565
+ const prevFilterCountRef = useRef(rows.length);
3566
+ const prevHasFiltersRef = useRef(isFiltered);
3567
+ const prevPaginationRef = useRef(table.getState().pagination);
3568
+ const isFirstRenderRef = useRef(true);
3569
+ useEffect(() => {
3570
+ if (isFirstRenderRef.current) {
3571
+ isFirstRenderRef.current = false;
3572
+ return;
3573
+ }
3574
+ const currentSorting = table.getState().sorting;
3575
+ const currentPagination = table.getState().pagination;
3576
+ const currentRowCount = rows.length;
3577
+ const currentIsFiltered = isFiltered;
3578
+ if (JSON.stringify(currentSorting) !== JSON.stringify(prevSortingRef.current)) {
3579
+ prevSortingRef.current = currentSorting;
3580
+ const firstSort = currentSorting[0];
3581
+ if (firstSort) {
3582
+ const column = table.getColumn(firstSort.id);
3583
+ const headerDef = column?.columnDef.header;
3584
+ const columnName = typeof headerDef === "string" ? headerDef : firstSort.id;
3585
+ const direction2 = firstSort.desc ? "descending" : "ascending";
3586
+ setAnnouncement(`Sorted by ${columnName} ${direction2}`);
3587
+ return;
3588
+ }
3589
+ }
3590
+ if (currentRowCount !== prevFilterCountRef.current || currentIsFiltered !== prevHasFiltersRef.current) {
3591
+ prevFilterCountRef.current = currentRowCount;
3592
+ prevHasFiltersRef.current = currentIsFiltered;
3593
+ if (currentIsFiltered) {
3594
+ setAnnouncement(`${currentRowCount} rows after filtering`);
3595
+ return;
3596
+ }
3597
+ }
3598
+ if (JSON.stringify(currentPagination) !== JSON.stringify(prevPaginationRef.current)) {
3599
+ prevPaginationRef.current = currentPagination;
3600
+ const pageCount = table.getPageCount();
3601
+ if (pageCount > 0) {
3602
+ setAnnouncement(`Page ${currentPagination.pageIndex + 1} of ${pageCount}`);
3603
+ return;
3604
+ }
3605
+ }
3606
+ });
2883
3607
  const handleContextMenu = useCallback(
2884
3608
  (e) => {
2885
3609
  e.preventDefault();
@@ -2887,6 +3611,46 @@ function Table({
2887
3611
  },
2888
3612
  [contextMenu, table]
2889
3613
  );
3614
+ const enableRowVirtualization = renderTable.options.enableVirtualization ?? false;
3615
+ const visibleLeafColumns = renderTable.getVisibleLeafColumns();
3616
+ const columnSizing = renderTable.getState().columnSizing;
3617
+ const colgroup = useMemo(() => {
3618
+ if (visibleLeafColumns.length === 0) return null;
3619
+ return /* @__PURE__ */ jsx("colgroup", { children: visibleLeafColumns.map((col) => /* @__PURE__ */ jsx("col", { style: { width: col.getSize() } }, col.id)) });
3620
+ }, [visibleLeafColumns, columnSizing]);
3621
+ const outerTableStyle = useMemo(() => {
3622
+ if (columnVirtualState.isVirtualized) {
3623
+ return {
3624
+ width: columnVirtualState.visibleWidth,
3625
+ minWidth: columnVirtualState.visibleWidth,
3626
+ marginLeft: columnVirtualState.startOffset,
3627
+ tableLayout: "fixed"
3628
+ };
3629
+ }
3630
+ if (enableRowVirtualization) {
3631
+ return { tableLayout: "fixed" };
3632
+ }
3633
+ return void 0;
3634
+ }, [
3635
+ columnVirtualState.isVirtualized,
3636
+ columnVirtualState.visibleWidth,
3637
+ columnVirtualState.startOffset,
3638
+ enableRowVirtualization
3639
+ ]);
3640
+ const tableNode = /* @__PURE__ */ jsxs(
3641
+ "table",
3642
+ {
3643
+ className: "yable-table",
3644
+ style: outerTableStyle,
3645
+ "data-column-virtualized": columnVirtualState.isVirtualized || void 0,
3646
+ children: [
3647
+ enableRowVirtualization && colgroup,
3648
+ /* @__PURE__ */ jsx(TableHeader, { table: renderTable, floatingFilters }),
3649
+ /* @__PURE__ */ jsx(TableBody, { table: renderTable, clickableRows, colgroup }),
3650
+ footer && /* @__PURE__ */ jsx(TableFooter, { table: renderTable })
3651
+ ]
3652
+ }
3653
+ );
2890
3654
  return /* @__PURE__ */ jsx(TableProvider, { value: table, children: /* @__PURE__ */ jsxs(
2891
3655
  "div",
2892
3656
  {
@@ -2895,23 +3659,40 @@ function Table({
2895
3659
  "data-theme": theme,
2896
3660
  dir: direction,
2897
3661
  role: "grid",
3662
+ "aria-label": ariaLabel ?? "Data table",
2898
3663
  "aria-rowcount": table.getRowModel().rows.length,
2899
3664
  "aria-colcount": table.getVisibleLeafColumns().length,
2900
3665
  onContextMenu: handleContextMenu,
2901
3666
  ...rest,
2902
3667
  children: [
2903
3668
  /* @__PURE__ */ jsxs("div", { className: "yable-main", children: [
2904
- /* @__PURE__ */ jsxs("table", { className: "yable-table", children: [
2905
- /* @__PURE__ */ jsx(TableHeader, { table }),
2906
- /* @__PURE__ */ jsx(
2907
- TableBody,
2908
- {
2909
- table,
2910
- clickableRows
2911
- }
2912
- ),
2913
- footer && /* @__PURE__ */ jsx(TableFooter, { table })
2914
- ] }),
3669
+ showColumnVirtualizationShell ? /* @__PURE__ */ jsx(
3670
+ "div",
3671
+ {
3672
+ ref: horizontalScrollRef,
3673
+ className: "yable-horizontal-scroll-container",
3674
+ style: {
3675
+ overflowX: "auto",
3676
+ overflowY: "visible",
3677
+ maxWidth: "100%",
3678
+ position: "relative"
3679
+ },
3680
+ children: /* @__PURE__ */ jsx(
3681
+ "div",
3682
+ {
3683
+ className: "yable-horizontal-scroll-inner",
3684
+ style: {
3685
+ width: Math.max(columnVirtualState.totalWidth, columnVirtualState.visibleWidth),
3686
+ minWidth: Math.max(
3687
+ columnVirtualState.totalWidth,
3688
+ columnVirtualState.visibleWidth
3689
+ )
3690
+ },
3691
+ children: tableNode
3692
+ }
3693
+ )
3694
+ }
3695
+ ) : tableNode,
2915
3696
  /* @__PURE__ */ jsx(
2916
3697
  LoadingOverlay,
2917
3698
  {
@@ -2952,6 +3733,24 @@ function Table({
2952
3733
  onClose: contextMenu.close,
2953
3734
  table
2954
3735
  }
3736
+ ),
3737
+ /* @__PURE__ */ jsx(
3738
+ "div",
3739
+ {
3740
+ role: "status",
3741
+ "aria-live": "polite",
3742
+ "aria-atomic": "true",
3743
+ style: {
3744
+ position: "absolute",
3745
+ width: "1px",
3746
+ height: "1px",
3747
+ overflow: "hidden",
3748
+ clip: "rect(0,0,0,0)",
3749
+ whiteSpace: "nowrap",
3750
+ border: 0
3751
+ },
3752
+ children: announcement
3753
+ }
2955
3754
  )
2956
3755
  ]
2957
3756
  }
@@ -3417,14 +4216,7 @@ function formatDateValue(value, type) {
3417
4216
  return String(value);
3418
4217
  }
3419
4218
  function useClipboard(table, options = {}) {
3420
- const {
3421
- enabled = true,
3422
- containerRef,
3423
- onCopy,
3424
- onCut,
3425
- onPaste,
3426
- ...clipboardOptions
3427
- } = options;
4219
+ const { enabled = true, containerRef, onCopy, onCut, onPaste, ...clipboardOptions } = options;
3428
4220
  const handleCopy = useCallback(
3429
4221
  (e) => {
3430
4222
  if (isEditableTarget2(e.target)) return;
@@ -3462,20 +4254,29 @@ function useClipboard(table, options = {}) {
3462
4254
  targetRowId = state.editing.activeCell.rowId;
3463
4255
  targetColumnId = state.editing.activeCell.columnId;
3464
4256
  } else {
3465
- const selectedRowIds = Object.keys(state.rowSelection || {}).filter(
3466
- (id) => state.rowSelection[id]
3467
- );
3468
- if (selectedRowIds.length > 0) {
3469
- targetRowId = selectedRowIds[0];
4257
+ const selectedRange = table.getCellSelectionRange();
4258
+ const rows = table.getRowModel().rows;
4259
+ const columns = table.getVisibleLeafColumns();
4260
+ if (selectedRange) {
4261
+ const startRowIndex = Math.min(selectedRange.start.rowIndex, selectedRange.end.rowIndex);
4262
+ const startColumnIndex = Math.min(
4263
+ selectedRange.start.columnIndex,
4264
+ selectedRange.end.columnIndex
4265
+ );
4266
+ targetRowId = rows[startRowIndex]?.id;
4267
+ targetColumnId = columns[startColumnIndex]?.id;
3470
4268
  } else {
3471
- const rows = table.getRowModel().rows;
3472
- if (rows.length > 0) {
4269
+ const selectedRowIds = Object.keys(state.rowSelection || {}).filter(
4270
+ (id) => state.rowSelection[id]
4271
+ );
4272
+ if (selectedRowIds.length > 0) {
4273
+ targetRowId = selectedRowIds[0];
4274
+ } else if (rows.length > 0) {
3473
4275
  targetRowId = rows[0].id;
3474
4276
  }
3475
- }
3476
- const columns = table.getVisibleLeafColumns();
3477
- if (columns.length > 0) {
3478
- targetColumnId = columns[0].id;
4277
+ if (columns.length > 0) {
4278
+ targetColumnId = columns[0].id;
4279
+ }
3479
4280
  }
3480
4281
  }
3481
4282
  if (targetRowId && targetColumnId) {
@@ -4765,7 +5566,7 @@ function sanitizeCSS(css) {
4765
5566
  sanitized = sanitized.replace(/javascript\s*:/gi, "/* javascript: removed */");
4766
5567
  return sanitized;
4767
5568
  }
4768
- function usePrintLayout(table, options = {}) {
5569
+ function usePrintLayout(_table, options = {}) {
4769
5570
  const { title, additionalCSS } = options;
4770
5571
  const isPrintingRef = useRef(false);
4771
5572
  const preparePrint = useCallback(() => {
@@ -4802,7 +5603,7 @@ function usePrintLayout(table, options = {}) {
4802
5603
  requestAnimationFrame(() => {
4803
5604
  window.print();
4804
5605
  });
4805
- }, [table, title, additionalCSS]);
5606
+ }, [title, additionalCSS]);
4806
5607
  return {
4807
5608
  preparePrint,
4808
5609
  isPrinting: isPrintingRef.current
@@ -4853,7 +5654,234 @@ function useTheme(options = {}) {
4853
5654
  containerRef
4854
5655
  };
4855
5656
  }
5657
+ function selectColumn(options = {}) {
5658
+ const { id = "_select", size = 40, headerAriaLabel = "Select all rows" } = options;
5659
+ return {
5660
+ id,
5661
+ header: ({ table }) => /* @__PURE__ */ jsx(
5662
+ "input",
5663
+ {
5664
+ type: "checkbox",
5665
+ checked: table.getIsAllPageRowsSelected(),
5666
+ ref: (el) => {
5667
+ if (el)
5668
+ el.indeterminate = table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected();
5669
+ },
5670
+ onChange: () => table.toggleAllPageRowsSelected(),
5671
+ "aria-label": headerAriaLabel
5672
+ }
5673
+ ),
5674
+ cell: ({ row }) => /* @__PURE__ */ jsx(
5675
+ "input",
5676
+ {
5677
+ type: "checkbox",
5678
+ checked: row.getIsSelected(),
5679
+ disabled: !row.getCanSelect(),
5680
+ onChange: row.getToggleSelectedHandler(),
5681
+ "aria-label": `Select row`
5682
+ }
5683
+ ),
5684
+ size,
5685
+ enableSorting: false,
5686
+ enableColumnFilter: false,
5687
+ enableResizing: false,
5688
+ enableReorder: false,
5689
+ enableHiding: false,
5690
+ lockVisible: true
5691
+ };
5692
+ }
5693
+
5694
+ // src/presets/rowNumberColumn.tsx
5695
+ function rowNumberColumn(options = {}) {
5696
+ const { id = "_rowNumber", header = "#", size = 50, startFrom = 1 } = options;
5697
+ return {
5698
+ id,
5699
+ header,
5700
+ cell: ({ row }) => row.index + startFrom,
5701
+ size,
5702
+ enableSorting: false,
5703
+ enableColumnFilter: false,
5704
+ enableResizing: false,
5705
+ enableReorder: false,
5706
+ lockVisible: true
5707
+ };
5708
+ }
5709
+ function actionsColumn(options) {
5710
+ const { id = "_actions", header = "", size = 100, actions } = options;
5711
+ return {
5712
+ id,
5713
+ header,
5714
+ cell: (ctx) => {
5715
+ const items = typeof actions === "function" ? actions(ctx.row) : actions;
5716
+ return /* @__PURE__ */ jsx("div", { className: "yable-cell-actions", children: items.filter((a) => !a.hidden || !a.hidden(ctx.row)).map((action, i) => /* @__PURE__ */ jsx(
5717
+ "button",
5718
+ {
5719
+ type: "button",
5720
+ className: "yable-action-btn",
5721
+ disabled: action.disabled?.(ctx.row),
5722
+ onClick: (e) => {
5723
+ e.stopPropagation();
5724
+ action.onClick(ctx.row);
5725
+ },
5726
+ title: action.label,
5727
+ children: action.icon ?? action.label
5728
+ },
5729
+ i
5730
+ )) });
5731
+ },
5732
+ size,
5733
+ enableSorting: false,
5734
+ enableColumnFilter: false,
5735
+ enableResizing: false,
5736
+ enableReorder: false
5737
+ };
5738
+ }
5739
+ function expandColumn(options = {}) {
5740
+ const { id = "_expand", size = 40 } = options;
5741
+ return {
5742
+ id,
5743
+ header: () => null,
5744
+ cell: ({ row }) => {
5745
+ if (!row.getCanExpand()) return null;
5746
+ return /* @__PURE__ */ jsx(
5747
+ "button",
5748
+ {
5749
+ type: "button",
5750
+ className: "yable-expand-btn",
5751
+ onClick: (e) => {
5752
+ e.stopPropagation();
5753
+ row.toggleExpanded();
5754
+ },
5755
+ "aria-expanded": row.getIsExpanded(),
5756
+ "aria-label": row.getIsExpanded() ? "Collapse row" : "Expand row",
5757
+ children: /* @__PURE__ */ jsx(
5758
+ "span",
5759
+ {
5760
+ className: "yable-expand-icon",
5761
+ style: {
5762
+ display: "inline-block",
5763
+ transform: row.getIsExpanded() ? "rotate(90deg)" : "rotate(0deg)",
5764
+ transition: "transform 150ms ease"
5765
+ },
5766
+ children: "\u25B6"
5767
+ }
5768
+ )
5769
+ }
5770
+ );
5771
+ },
5772
+ size,
5773
+ enableSorting: false,
5774
+ enableColumnFilter: false,
5775
+ enableResizing: false,
5776
+ enableReorder: false,
5777
+ lockVisible: true
5778
+ };
5779
+ }
5780
+ function CellStack({ children, gap = 2 }) {
5781
+ return /* @__PURE__ */ jsx(
5782
+ "div",
5783
+ {
5784
+ className: "yable-cell-stack",
5785
+ style: {
5786
+ display: "flex",
5787
+ flexDirection: "column",
5788
+ gap
5789
+ },
5790
+ children
5791
+ }
5792
+ );
5793
+ }
5794
+ function CellRow({ children, gap = 6, align = "center", justify = "start" }) {
5795
+ const justifyMap = {
5796
+ start: "flex-start",
5797
+ center: "center",
5798
+ end: "flex-end",
5799
+ between: "space-between"
5800
+ };
5801
+ return /* @__PURE__ */ jsx(
5802
+ "div",
5803
+ {
5804
+ className: "yable-cell-row",
5805
+ style: {
5806
+ display: "flex",
5807
+ flexDirection: "row",
5808
+ alignItems: align === "baseline" ? "baseline" : align === "start" ? "flex-start" : align === "end" ? "flex-end" : "center",
5809
+ justifyContent: justifyMap[justify] || "flex-start",
5810
+ gap
5811
+ },
5812
+ children
5813
+ }
5814
+ );
5815
+ }
5816
+ function CellWithIcon({ icon, children, gap = 6, iconSize }) {
5817
+ return /* @__PURE__ */ jsxs(
5818
+ "div",
5819
+ {
5820
+ className: "yable-cell-with-icon",
5821
+ style: {
5822
+ display: "flex",
5823
+ flexDirection: "row",
5824
+ alignItems: "center",
5825
+ gap
5826
+ },
5827
+ children: [
5828
+ /* @__PURE__ */ jsx(
5829
+ "span",
5830
+ {
5831
+ className: "yable-cell-icon",
5832
+ style: {
5833
+ display: "inline-flex",
5834
+ alignItems: "center",
5835
+ justifyContent: "center",
5836
+ flexShrink: 0,
5837
+ ...iconSize ? { width: iconSize, height: iconSize } : {}
5838
+ },
5839
+ children: icon
5840
+ }
5841
+ ),
5842
+ /* @__PURE__ */ jsx("span", { className: "yable-cell-icon-content", style: { minWidth: 0 }, children })
5843
+ ]
5844
+ }
5845
+ );
5846
+ }
5847
+ function CellText({
5848
+ children,
5849
+ variant = "primary",
5850
+ bold,
5851
+ truncate,
5852
+ size = "md"
5853
+ }) {
5854
+ const fontSizeMap = { sm: "0.75rem", md: "0.875rem", lg: "1rem" };
5855
+ return /* @__PURE__ */ jsx(
5856
+ "span",
5857
+ {
5858
+ className: `yable-cell-text yable-cell-text--${variant}`,
5859
+ style: {
5860
+ fontSize: fontSizeMap[size],
5861
+ fontWeight: bold ? 600 : void 0,
5862
+ color: variant === "secondary" ? "var(--yable-text-secondary, #6b7280)" : variant === "muted" ? "var(--yable-text-muted, #9ca3af)" : void 0,
5863
+ ...truncate ? {
5864
+ overflow: "hidden",
5865
+ textOverflow: "ellipsis",
5866
+ whiteSpace: "nowrap"
5867
+ } : {}
5868
+ },
5869
+ children
5870
+ }
5871
+ );
5872
+ }
5873
+
5874
+ // src/utils/mergeEditChanges.ts
5875
+ function mergeEditChanges(data, changes, getRowId = (_, i) => String(i)) {
5876
+ const changeKeys = Object.keys(changes);
5877
+ if (changeKeys.length === 0) return data;
5878
+ return data.map((row, i) => {
5879
+ const id = getRowId(row, i);
5880
+ const patch = changes[id];
5881
+ return patch ? { ...row, ...patch } : row;
5882
+ });
5883
+ }
4856
5884
 
4857
- 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, GlobalFilter, LoadingOverlay, MasterDetail, NoRowsOverlay, Pagination, PivotConfigPanel, PrintLayout, Sidebar, SortIndicator, StatusBar, StatusBarPanelComponent, Table, TableBody, TableCell, TableFooter, TableHeader, TableProvider, Tooltip, TreeToggle, getMeasureRecipeForCellType, getRegisteredCellTypes, resolveMeasureRecipe, useAutoMeasurements, useCellFlash, useClipboard, useContextMenu, useFillHandle, useKeyboardNavigation, usePretextMeasurement, usePrintLayout, useRowAnimation, useRowDrag, useTable, useTableContext, useTableRowHeights, useTheme, useTooltip, useVirtualization };
5885
+ 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, expandColumn, getMeasureRecipeForCellType, getRegisteredCellTypes, mergeEditChanges, resolveMeasureRecipe, rowNumberColumn, selectColumn, useAutoMeasurements, useCellFlash, useClipboard, useColumnVirtualization, useContextMenu, useFillHandle, useKeyboardNavigation, usePretextMeasurement, usePrintLayout, useRowAnimation, useRowDrag, useTable, useTableContext, useTablePersistence, useTableRowHeights, useTheme, useTooltip, useVirtualization, useYableDefaults };
4858
5886
  //# sourceMappingURL=index.js.map
4859
5887
  //# sourceMappingURL=index.js.map