@pretable/react 0.0.1 → 0.0.2

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.mjs CHANGED
@@ -1,6 +1,7 @@
1
- import { useMemo, useSyncExternalStore, useLayoutEffect, useRef, useCallback, useState, createElement } from 'react';
1
+ import { memo, useMemo, useRef, useLayoutEffect, useSyncExternalStore, useCallback, useState, useEffect, createElement } from 'react';
2
2
  import { createGrid } from '@pretable/core';
3
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
+ import { getDensityHeights } from '@pretable/ui';
4
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
4
5
 
5
6
  // src/pretable-surface.tsx
6
7
 
@@ -483,23 +484,12 @@ function getEstimatedRowHeightSignature(row, columns) {
483
484
  }).join("|");
484
485
  }
485
486
  function readCellValue(row, column) {
486
- return column.getValue ? column.getValue(row) : row[column.id];
487
+ return column.value ? column.value(row) : row[column.id];
487
488
  }
488
489
  function getColumnWidth(column) {
489
490
  return column.widthPx ?? (column.wrap ? WRAPPED_COLUMN_WIDTH : FIXED_COLUMN_WIDTH);
490
491
  }
491
492
  function usePretable({
492
- autosize,
493
- columns,
494
- rows,
495
- getRowId
496
- }) {
497
- return useMemo(
498
- () => createGrid({ columns, rows, getRowId, autosize }),
499
- [autosize, columns, getRowId, rows]
500
- );
501
- }
502
- function usePretableModel({
503
493
  autosize,
504
494
  columns,
505
495
  rows,
@@ -507,25 +497,79 @@ function usePretableModel({
507
497
  viewportHeight,
508
498
  viewportWidth,
509
499
  overscan = 6,
510
- interactionOverrides,
511
- measuredHeights
500
+ state,
501
+ measuredHeights,
502
+ onSelectionChange,
503
+ onFocusChange
512
504
  }) {
513
- const grid = usePretable({ autosize, columns, rows, getRowId });
514
- if (interactionOverrides) {
515
- grid.setSort(
516
- interactionOverrides.sort?.columnId ?? null,
517
- interactionOverrides.sort?.direction ?? null
518
- );
519
- grid.replaceFilters(interactionOverrides.filters ?? {});
520
- if (interactionOverrides.focusedRowId !== void 0) {
521
- const firstColumnId = columns[0]?.id ?? null;
522
- grid.setFocus(
523
- interactionOverrides.focusedRowId,
524
- interactionOverrides.focusedRowId ? firstColumnId : null
525
- );
505
+ const grid = useMemo(
506
+ () => createGrid({ columns, rows, getRowId, autosize }),
507
+ [autosize, columns, getRowId, rows]
508
+ );
509
+ const lastColumnIdsRef = useRef(null);
510
+ useLayoutEffect(() => {
511
+ const currentIds = columns.map((c) => c.id);
512
+ const prevIds = lastColumnIdsRef.current;
513
+ if (prevIds === null || prevIds.length !== currentIds.length || prevIds.some((id, i) => id !== currentIds[i])) {
514
+ if (prevIds !== null) {
515
+ grid.mergeColumnsFromProps(columns);
516
+ }
517
+ lastColumnIdsRef.current = currentIds;
526
518
  }
527
- if (interactionOverrides.selectedRowId !== void 0) {
528
- grid.selectRow(interactionOverrides.selectedRowId);
519
+ }, [columns, grid]);
520
+ if (state) {
521
+ if (state.sort !== void 0) {
522
+ grid.setSort(state.sort?.columnId ?? null, state.sort?.direction ?? null);
523
+ }
524
+ if (state.filters !== void 0) {
525
+ grid.replaceFilters(state.filters);
526
+ }
527
+ if (state.columnWidths !== void 0) {
528
+ const widths = state.columnWidths;
529
+ for (const column of grid.options.columns) {
530
+ const next = widths[column.id];
531
+ if (next !== void 0 && next !== column.widthPx) {
532
+ grid.setColumnWidth(column.id, next);
533
+ }
534
+ }
535
+ }
536
+ if (state.columnOrder !== void 0) {
537
+ const targetOrder = state.columnOrder;
538
+ const currentIds = grid.options.columns.map((c) => c.id);
539
+ const targetIds = [
540
+ ...targetOrder.filter((id) => currentIds.includes(id)),
541
+ ...currentIds.filter((id) => !targetOrder.includes(id))
542
+ ];
543
+ for (let i = 0; i < targetIds.length; i += 1) {
544
+ const id = targetIds[i];
545
+ const currentIdx = grid.options.columns.findIndex((c) => c.id === id);
546
+ if (currentIdx !== i && id !== "__pretable_row_select__") {
547
+ grid.moveColumn(id, i);
548
+ }
549
+ }
550
+ }
551
+ if (state.columnPinned !== void 0) {
552
+ const pinned = state.columnPinned;
553
+ for (const [id, value] of Object.entries(pinned)) {
554
+ const column = grid.options.columns.find((c) => c.id === id);
555
+ if (!column) continue;
556
+ const targetPinned = value === "left" ? "left" : null;
557
+ const currentPinned = column.pinned ?? null;
558
+ if (currentPinned !== targetPinned) {
559
+ grid.setColumnPinned(id, targetPinned);
560
+ }
561
+ }
562
+ }
563
+ if (state.selection !== void 0) {
564
+ grid.setSelection(state.selection);
565
+ }
566
+ if (state.focus !== void 0) {
567
+ const focus = state.focus;
568
+ if (focus.rowId !== null && focus.columnId !== null) {
569
+ grid.setFocus({ rowId: focus.rowId, columnId: focus.columnId });
570
+ } else {
571
+ grid.setFocus(null);
572
+ }
529
573
  }
530
574
  }
531
575
  const snapshot = useSyncExternalStore(
@@ -584,7 +628,7 @@ function usePretableModel({
584
628
  focusedRowId: snapshot.focus.rowId,
585
629
  rowModelRowCount: snapshot.visibleRows.length,
586
630
  renderedRowCount: renderSnapshot.rows.length,
587
- selectedRowId: snapshot.selection.rowIds[0] ?? null,
631
+ selectedRowId: snapshot.selection.ranges[0]?.startRowId ?? null,
588
632
  totalRowCount: snapshot.totalRowCount,
589
633
  totalHeight: renderSnapshot.totalHeight,
590
634
  visibleRowCount: viewportRows.length,
@@ -601,7 +645,7 @@ function usePretableModel({
601
645
  renderSnapshot.totalHeight,
602
646
  snapshot.focus.rowId,
603
647
  snapshot.visibleRows.length,
604
- snapshot.selection.rowIds,
648
+ snapshot.selection.ranges,
605
649
  snapshot.totalRowCount,
606
650
  snapshot.viewport.height,
607
651
  snapshot.viewport.scrollTop,
@@ -614,6 +658,45 @@ function usePretableModel({
614
658
  telemetry
615
659
  };
616
660
  }
661
+ function subscribe(callback) {
662
+ if (typeof document === "undefined") return () => {
663
+ };
664
+ const observer = new MutationObserver(callback);
665
+ observer.observe(document.documentElement, {
666
+ attributes: true,
667
+ attributeFilter: ["data-density", "data-theme", "class", "style"]
668
+ });
669
+ return () => observer.disconnect();
670
+ }
671
+ function useResolvedHeights(rowHeightProp, headerHeightProp) {
672
+ const cachedClient = useRef(null);
673
+ const cachedServer = useRef(null);
674
+ const getSnapshot = useCallback(() => {
675
+ const css = getDensityHeights();
676
+ const rowHeight = rowHeightProp ?? css.rowHeight;
677
+ const headerHeight = headerHeightProp ?? css.headerHeight;
678
+ const prev = cachedClient.current;
679
+ if (prev !== null && prev.rowHeight === rowHeight && prev.headerHeight === headerHeight) {
680
+ return prev;
681
+ }
682
+ const next = { rowHeight, headerHeight };
683
+ cachedClient.current = next;
684
+ return next;
685
+ }, [rowHeightProp, headerHeightProp]);
686
+ const getServerSnapshot = useCallback(() => {
687
+ const css = getDensityHeights();
688
+ const rowHeight = rowHeightProp ?? css.rowHeight;
689
+ const headerHeight = headerHeightProp ?? css.headerHeight;
690
+ const prev = cachedServer.current;
691
+ if (prev !== null && prev.rowHeight === rowHeight && prev.headerHeight === headerHeight) {
692
+ return prev;
693
+ }
694
+ const next = { rowHeight, headerHeight };
695
+ cachedServer.current = next;
696
+ return next;
697
+ }, [rowHeightProp, headerHeightProp]);
698
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
699
+ }
617
700
 
618
701
  // src/rendering.ts
619
702
  var DEFAULT_ROW_HEIGHT2 = 44;
@@ -645,7 +728,7 @@ function getNextSortDirection(current) {
645
728
  return null;
646
729
  }
647
730
  function resolveCellValue(row, column) {
648
- return column.getValue ? column.getValue(row) : row[column.id];
731
+ return column.value ? column.value(row) : row[column.id];
649
732
  }
650
733
  function formatCellValue(value) {
651
734
  if (Array.isArray(value)) {
@@ -654,69 +737,6 @@ function formatCellValue(value) {
654
737
  return String(value ?? "");
655
738
  }
656
739
 
657
- // src/density.ts
658
- var FALLBACK_ROW_HEIGHT = 32;
659
- var FALLBACK_HEADER_HEIGHT = HEADER_HEIGHT;
660
- function parsePx(value) {
661
- const match = value.trim().match(/^([\d.]+)px$/);
662
- return match ? parseFloat(match[1]) : null;
663
- }
664
- function getDensityHeights() {
665
- if (typeof document === "undefined") {
666
- return {
667
- rowHeight: FALLBACK_ROW_HEIGHT,
668
- headerHeight: FALLBACK_HEADER_HEIGHT
669
- };
670
- }
671
- const styles = getComputedStyle(document.documentElement);
672
- const read = (name) => {
673
- if (typeof styles?.getPropertyValue !== "function") return "";
674
- return styles.getPropertyValue(name);
675
- };
676
- return {
677
- rowHeight: parsePx(read("--pretable-row-height")) ?? FALLBACK_ROW_HEIGHT,
678
- headerHeight: parsePx(read("--pretable-header-height")) ?? FALLBACK_HEADER_HEIGHT
679
- };
680
- }
681
- function subscribe(callback) {
682
- if (typeof document === "undefined") return () => {
683
- };
684
- const observer = new MutationObserver(callback);
685
- observer.observe(document.documentElement, {
686
- attributes: true,
687
- attributeFilter: ["data-density", "data-theme", "class", "style"]
688
- });
689
- return () => observer.disconnect();
690
- }
691
- function useResolvedHeights(rowHeightProp, headerHeightProp) {
692
- const cachedClient = useRef(null);
693
- const cachedServer = useRef(null);
694
- const getSnapshot = useCallback(() => {
695
- const css = getDensityHeights();
696
- const rowHeight = rowHeightProp ?? css.rowHeight;
697
- const headerHeight = headerHeightProp ?? css.headerHeight;
698
- const prev = cachedClient.current;
699
- if (prev !== null && prev.rowHeight === rowHeight && prev.headerHeight === headerHeight) {
700
- return prev;
701
- }
702
- const next = { rowHeight, headerHeight };
703
- cachedClient.current = next;
704
- return next;
705
- }, [rowHeightProp, headerHeightProp]);
706
- const getServerSnapshot = useCallback(() => {
707
- const rowHeight = rowHeightProp ?? FALLBACK_ROW_HEIGHT;
708
- const headerHeight = headerHeightProp ?? FALLBACK_HEADER_HEIGHT;
709
- const prev = cachedServer.current;
710
- if (prev !== null && prev.rowHeight === rowHeight && prev.headerHeight === headerHeight) {
711
- return prev;
712
- }
713
- const next = { rowHeight, headerHeight };
714
- cachedServer.current = next;
715
- return next;
716
- }, [rowHeightProp, headerHeightProp]);
717
- return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
718
- }
719
-
720
740
  // src/styles.ts
721
741
  function getViewportStyle(height) {
722
742
  return {
@@ -786,6 +806,162 @@ function getPinnedCellStyle(left) {
786
806
  zIndex: 1
787
807
  };
788
808
  }
809
+
810
+ // src/constants.ts
811
+ var ROW_SELECT_COLUMN_ID = "__pretable_row_select__";
812
+
813
+ // src/copy.ts
814
+ function defaultCoerceForCopy(value) {
815
+ if (value === null || value === void 0) return "";
816
+ if (value instanceof Date) return value.toISOString();
817
+ const t = typeof value;
818
+ if (t === "string" || t === "number" || t === "boolean" || t === "bigint") {
819
+ return String(value);
820
+ }
821
+ if (t === "object") {
822
+ try {
823
+ return JSON.stringify(value);
824
+ } catch {
825
+ return String(value);
826
+ }
827
+ }
828
+ return String(value);
829
+ }
830
+ function serializeRangesAsTsv(args) {
831
+ const dataColumns = args.columns.filter((c) => c.id !== ROW_SELECT_COLUMN_ID);
832
+ if (dataColumns.length === 0) return null;
833
+ const colIndex = /* @__PURE__ */ new Map();
834
+ dataColumns.forEach((c, i) => colIndex.set(c.id, i));
835
+ const rowIndex = /* @__PURE__ */ new Map();
836
+ args.visibleRows.forEach((r, i) => rowIndex.set(r.id, i));
837
+ const blocks = [];
838
+ for (const range of args.ranges) {
839
+ const startRow = rowIndex.get(range.startRowId);
840
+ const endRow = rowIndex.get(range.endRowId);
841
+ const startIsSynth = range.startColumnId === ROW_SELECT_COLUMN_ID;
842
+ const endIsSynth = range.endColumnId === ROW_SELECT_COLUMN_ID;
843
+ const startCol = colIndex.get(range.startColumnId);
844
+ const endCol = colIndex.get(range.endColumnId);
845
+ const haveRows = startRow !== void 0 && endRow !== void 0;
846
+ const rowLo = haveRows ? Math.min(startRow, endRow) : -1;
847
+ const rowHi = haveRows ? Math.max(startRow, endRow) : -1;
848
+ let colLo;
849
+ let colHi;
850
+ if (startIsSynth && endIsSynth) {
851
+ continue;
852
+ } else if (startIsSynth && endCol !== void 0) {
853
+ colLo = 0;
854
+ colHi = endCol;
855
+ } else if (endIsSynth && startCol !== void 0) {
856
+ colLo = startCol;
857
+ colHi = 0;
858
+ } else if (startCol !== void 0 && endCol !== void 0) {
859
+ colLo = Math.min(startCol, endCol);
860
+ colHi = Math.max(startCol, endCol);
861
+ } else if (startCol !== void 0) {
862
+ colLo = colHi = startCol;
863
+ } else if (endCol !== void 0) {
864
+ colLo = colHi = endCol;
865
+ } else {
866
+ continue;
867
+ }
868
+ if (colLo > colHi) {
869
+ [colLo, colHi] = [colHi, colLo];
870
+ }
871
+ colLo = Math.max(colLo, 0);
872
+ colHi = Math.min(colHi, dataColumns.length - 1);
873
+ if (colLo > colHi) continue;
874
+ if (!haveRows || rowLo > rowHi) continue;
875
+ const lines = [];
876
+ if (args.copyWithHeaders) {
877
+ const headerCells = [];
878
+ for (let c = colLo; c <= colHi; c += 1) {
879
+ const col = dataColumns[c];
880
+ headerCells.push(col.header ?? col.id);
881
+ }
882
+ lines.push(headerCells.join(" "));
883
+ lines.push("");
884
+ }
885
+ for (let r = rowLo; r <= rowHi; r += 1) {
886
+ const row = args.visibleRows[r];
887
+ const cells = [];
888
+ for (let c = colLo; c <= colHi; c += 1) {
889
+ const col = dataColumns[c];
890
+ const raw = col.value ? col.value(row.row) : row.row[col.id];
891
+ const text = col.format ? col.format({ value: raw, row: row.row, column: col }) : defaultCoerceForCopy(raw);
892
+ cells.push(text);
893
+ }
894
+ lines.push(cells.join(" "));
895
+ }
896
+ blocks.push(lines.join("\n"));
897
+ }
898
+ if (blocks.length === 0) return null;
899
+ return { text: blocks.join("\n\n") };
900
+ }
901
+ async function defaultCopyToClipboard(payload) {
902
+ if (typeof navigator === "undefined" || !navigator.clipboard) return;
903
+ if (payload.html && typeof globalThis.ClipboardItem !== "undefined" && typeof navigator.clipboard.write === "function") {
904
+ await navigator.clipboard.write([
905
+ new globalThis.ClipboardItem({
906
+ "text/plain": new Blob([payload.text], { type: "text/plain" }),
907
+ "text/html": new Blob([payload.html], { type: "text/html" })
908
+ })
909
+ ]);
910
+ } else {
911
+ await navigator.clipboard.writeText(payload.text);
912
+ }
913
+ }
914
+ var defaultMessages = {
915
+ selectAllAnnouncement: ({ rowCount, columnCount, isAll }) => isAll ? "All rows selected" : `${rowCount} rows \xD7 ${columnCount} columns selected`,
916
+ copyAnnouncement: ({ rowCount, columnCount }) => `${rowCount} rows \xD7 ${columnCount} columns copied`,
917
+ copyFailedAnnouncement: () => "Copy failed"
918
+ };
919
+ var ANNOUNCE_DEBOUNCE_MS = 500;
920
+ var REORDER_THRESHOLD_PX = 5;
921
+ function CellContentImpl({
922
+ formattedValue,
923
+ renderRef,
924
+ fallbackRenderRef,
925
+ cellRenderInput
926
+ }) {
927
+ if (renderRef) {
928
+ return /* @__PURE__ */ jsx(Fragment, { children: renderRef(cellRenderInput) });
929
+ }
930
+ if (fallbackRenderRef) {
931
+ return /* @__PURE__ */ jsx(Fragment, { children: fallbackRenderRef(cellRenderInput) });
932
+ }
933
+ return /* @__PURE__ */ jsx(Fragment, { children: formattedValue });
934
+ }
935
+ function cellContentPropsEqual(prev, next) {
936
+ return prev.rowId === next.rowId && prev.columnId === next.columnId && prev.value === next.value && prev.formattedValue === next.formattedValue && prev.isFocused === next.isFocused && prev.isSelected === next.isSelected && prev.renderRef === next.renderRef && prev.fallbackRenderRef === next.fallbackRenderRef;
937
+ }
938
+ var MemoizedCellContent = memo(CellContentImpl, cellContentPropsEqual);
939
+ function HeaderContentImpl({
940
+ label,
941
+ sortDirection,
942
+ renderHeaderRef,
943
+ fallbackRenderHeaderRef,
944
+ headerRenderInput
945
+ }) {
946
+ if (renderHeaderRef) {
947
+ return /* @__PURE__ */ jsx(Fragment, { children: renderHeaderRef(headerRenderInput) });
948
+ }
949
+ if (fallbackRenderHeaderRef) {
950
+ return /* @__PURE__ */ jsx(Fragment, { children: fallbackRenderHeaderRef({
951
+ column: headerRenderInput.column,
952
+ label,
953
+ sortDirection
954
+ }) });
955
+ }
956
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
957
+ /* @__PURE__ */ jsx("span", { children: label }),
958
+ /* @__PURE__ */ jsx("strong", { children: sortDirection === "desc" ? "Newest" : sortDirection === "asc" ? "Oldest" : "Sort" })
959
+ ] });
960
+ }
961
+ function headerContentPropsEqual(prev, next) {
962
+ return prev.columnId === next.columnId && prev.label === next.label && prev.sortDirection === next.sortDirection && prev.isSorted === next.isSorted && prev.width === next.width && prev.isSortable === next.isSortable && prev.renderHeaderRef === next.renderHeaderRef && prev.fallbackRenderHeaderRef === next.fallbackRenderHeaderRef;
963
+ }
964
+ var MemoizedHeaderContent = memo(HeaderContentImpl, headerContentPropsEqual);
789
965
  function PretableSurface({
790
966
  ariaLabel,
791
967
  autosize,
@@ -797,45 +973,292 @@ function PretableSurface({
797
973
  getRowClassName,
798
974
  getRowId,
799
975
  getRowProps,
800
- interactionState,
976
+ state,
801
977
  overscan = 6,
802
978
  onGridReady,
803
979
  onSelectedRowIdChange,
980
+ onSelectionChange,
981
+ onFocusChange,
804
982
  onSortChange,
983
+ onColumnWidthsChange,
984
+ onColumnOrderChange,
985
+ onColumnPinnedChange,
805
986
  onTelemetryChange,
806
987
  renderBodyCell,
807
988
  renderHeaderCell,
808
989
  rows,
990
+ rowSelectionColumn,
809
991
  selectFocusedRowOnArrowKey = false,
992
+ tabBehavior = "wrap-rows",
810
993
  viewportStyle,
811
- viewportHeight
994
+ viewportHeight,
995
+ copyWithHeaders,
996
+ onCopy,
997
+ copyToClipboard,
998
+ messages
812
999
  }) {
813
1000
  const [measuredHeights, setMeasuredHeights] = useState({});
1001
+ const [dragLiveWidth, setDragLiveWidth] = useState(null);
1002
+ const resizeStateRef = useRef(null);
1003
+ const wasResizingRef = useRef(false);
1004
+ const wasReorderingRef = useRef(false);
1005
+ const reorderStateRef = useRef(null);
1006
+ const [reorderDrag, setReorderDrag] = useState(null);
814
1007
  const [viewportWidth, setViewportWidth] = useState(0);
1008
+ const [liveMessage, setLiveMessage] = useState("");
1009
+ const announceTimerRef = useRef(null);
1010
+ const pendingAnnouncementRef = useRef(null);
1011
+ const scheduleAnnouncement = useCallback((message) => {
1012
+ pendingAnnouncementRef.current = message;
1013
+ if (announceTimerRef.current !== null) {
1014
+ clearTimeout(announceTimerRef.current);
1015
+ }
1016
+ announceTimerRef.current = setTimeout(() => {
1017
+ if (pendingAnnouncementRef.current !== null) {
1018
+ setLiveMessage(pendingAnnouncementRef.current);
1019
+ pendingAnnouncementRef.current = null;
1020
+ }
1021
+ announceTimerRef.current = null;
1022
+ }, ANNOUNCE_DEBOUNCE_MS);
1023
+ }, []);
1024
+ useEffect(() => {
1025
+ return () => {
1026
+ if (announceTimerRef.current !== null) {
1027
+ clearTimeout(announceTimerRef.current);
1028
+ }
1029
+ };
1030
+ }, []);
1031
+ const effectiveMessages = useMemo(
1032
+ () => ({
1033
+ selectAllAnnouncement: messages?.selectAllAnnouncement ?? defaultMessages.selectAllAnnouncement,
1034
+ copyAnnouncement: messages?.copyAnnouncement ?? defaultMessages.copyAnnouncement,
1035
+ copyFailedAnnouncement: messages?.copyFailedAnnouncement ?? defaultMessages.copyFailedAnnouncement
1036
+ }),
1037
+ [messages]
1038
+ );
815
1039
  const measuredHeightsRef = useRef({});
816
1040
  const measuredRowKeysRef = useRef({});
817
1041
  const rowNodesRef = useRef(/* @__PURE__ */ new Map());
1042
+ const cellNodesRef = useRef(/* @__PURE__ */ new Map());
818
1043
  const viewportRef = useRef(null);
1044
+ const dragAnchorRef = useRef(null);
1045
+ const dragStartSelectionRef = useRef(null);
1046
+ const lastCheckedRowAnchorRef = useRef(null);
819
1047
  const { headerHeight } = useResolvedHeights();
820
1048
  const bodyViewportHeight = Math.max(viewportHeight - headerHeight, 0);
821
- const { grid, snapshot, renderSnapshot, telemetry } = usePretableModel({
1049
+ const effectiveColumns = useMemo(() => {
1050
+ if (!rowSelectionColumn?.enabled) return columns;
1051
+ const synth = {
1052
+ id: ROW_SELECT_COLUMN_ID,
1053
+ header: "",
1054
+ widthPx: rowSelectionColumn.width ?? 36,
1055
+ sortable: false,
1056
+ filterable: false,
1057
+ ...rowSelectionColumn.pinned ?? true ? { pinned: "left" } : {}
1058
+ };
1059
+ return [synth, ...columns];
1060
+ }, [columns, rowSelectionColumn]);
1061
+ const { grid, snapshot, renderSnapshot, telemetry } = usePretable({
822
1062
  autosize,
823
- columns,
1063
+ columns: effectiveColumns,
824
1064
  getRowId,
825
- interactionOverrides: interactionState ?? void 0,
1065
+ state: state ?? void 0,
826
1066
  measuredHeights,
827
1067
  overscan,
828
1068
  rows,
829
1069
  viewportHeight: bodyViewportHeight,
830
- viewportWidth: viewportWidth || void 0
1070
+ viewportWidth: viewportWidth || void 0,
1071
+ onSelectionChange,
1072
+ onFocusChange
831
1073
  });
832
- const pinnedOffsets = useMemo(() => getPinnedLeftOffsets(columns), [columns]);
1074
+ const focusedRowId = snapshot.focus.rowId;
1075
+ const focusedColumnId = snapshot.focus.columnId;
1076
+ const pinnedOffsets = useMemo(
1077
+ () => getPinnedLeftOffsets(effectiveColumns),
1078
+ [effectiveColumns]
1079
+ );
1080
+ const columnsById = useMemo(() => {
1081
+ const map = /* @__PURE__ */ new Map();
1082
+ for (const col of effectiveColumns) {
1083
+ map.set(col.id, col);
1084
+ }
1085
+ return map;
1086
+ }, [effectiveColumns]);
1087
+ const { columnLefts, columnWidths } = useMemo(() => {
1088
+ const engineColumns = grid.options.columns;
1089
+ const lefts = new Array(engineColumns.length).fill(0);
1090
+ const widths = new Array(engineColumns.length).fill(0);
1091
+ for (const planned of renderSnapshot.columns) {
1092
+ lefts[planned.index] = planned.left;
1093
+ widths[planned.index] = planned.width;
1094
+ }
1095
+ let acc = 0;
1096
+ for (let i = 0; i < engineColumns.length; i += 1) {
1097
+ const col = engineColumns[i];
1098
+ if (widths[i] === 0) {
1099
+ widths[i] = col.widthPx ?? 0;
1100
+ lefts[i] = acc;
1101
+ }
1102
+ acc = lefts[i] + widths[i];
1103
+ }
1104
+ return { columnLefts: lefts, columnWidths: widths };
1105
+ }, [renderSnapshot.columns, grid.options.columns]);
1106
+ const visibleRowIndexById = useMemo(() => {
1107
+ const map = /* @__PURE__ */ new Map();
1108
+ for (let i = 0; i < snapshot.visibleRows.length; i += 1) {
1109
+ const row = snapshot.visibleRows[i];
1110
+ if (row) {
1111
+ map.set(row.id, i);
1112
+ }
1113
+ }
1114
+ return map;
1115
+ }, [snapshot.visibleRows]);
1116
+ const dataColumnIndex = useMemo(() => {
1117
+ const dataColumns = effectiveColumns.filter(
1118
+ (c) => c.id !== ROW_SELECT_COLUMN_ID
1119
+ );
1120
+ const idxById = /* @__PURE__ */ new Map();
1121
+ for (let i = 0; i < dataColumns.length; i += 1) {
1122
+ idxById.set(dataColumns[i].id, i);
1123
+ }
1124
+ return { dataColumns, idxById };
1125
+ }, [effectiveColumns]);
1126
+ const { fullySelectedRowIds, indeterminateRowIds } = useMemo(() => {
1127
+ const fullyRows = /* @__PURE__ */ new Set();
1128
+ const indeterminateRows = /* @__PURE__ */ new Set();
1129
+ const ranges = snapshot.selection.ranges;
1130
+ const { dataColumns, idxById: dataColIdxByColId } = dataColumnIndex;
1131
+ if (ranges.length === 0 || dataColumns.length === 0) {
1132
+ return {
1133
+ fullySelectedRowIds: fullyRows,
1134
+ indeterminateRowIds: indeterminateRows
1135
+ };
1136
+ }
1137
+ const visibleRows = snapshot.visibleRows;
1138
+ const colCount = dataColumns.length;
1139
+ if (colCount <= 30) {
1140
+ const rowMask = /* @__PURE__ */ new Map();
1141
+ for (const range of ranges) {
1142
+ const r1 = visibleRowIndexById.get(range.startRowId);
1143
+ const r2 = visibleRowIndexById.get(range.endRowId);
1144
+ if (r1 === void 0 || r2 === void 0) continue;
1145
+ const rowLo = Math.min(r1, r2);
1146
+ const rowHi = Math.max(r1, r2);
1147
+ const startSynth = range.startColumnId === ROW_SELECT_COLUMN_ID;
1148
+ const endSynth = range.endColumnId === ROW_SELECT_COLUMN_ID;
1149
+ let dataColLo;
1150
+ let dataColHi;
1151
+ if (startSynth && endSynth) {
1152
+ continue;
1153
+ }
1154
+ if (startSynth || endSynth) {
1155
+ dataColLo = 0;
1156
+ dataColHi = colCount - 1;
1157
+ } else {
1158
+ const a = dataColIdxByColId.get(range.startColumnId);
1159
+ const b = dataColIdxByColId.get(range.endColumnId);
1160
+ if (a === void 0 || b === void 0) continue;
1161
+ dataColLo = Math.min(a, b);
1162
+ dataColHi = Math.max(a, b);
1163
+ }
1164
+ const spanWidth = dataColHi - dataColLo + 1;
1165
+ const spanMask = (spanWidth >= 30 ? 1073741823 : (1 << spanWidth) - 1) << dataColLo >>> 0;
1166
+ for (let rowIdx = rowLo; rowIdx <= rowHi; rowIdx += 1) {
1167
+ rowMask.set(rowIdx, (rowMask.get(rowIdx) ?? 0) | spanMask);
1168
+ }
1169
+ }
1170
+ const fullMask = colCount >= 30 ? 1073741823 : (1 << colCount) - 1 >>> 0;
1171
+ for (const [rowIdx, mask] of rowMask) {
1172
+ if (mask === 0) continue;
1173
+ const row = visibleRows[rowIdx];
1174
+ if (!row) continue;
1175
+ if (mask === fullMask) fullyRows.add(row.id);
1176
+ else indeterminateRows.add(row.id);
1177
+ }
1178
+ } else {
1179
+ const rowCoverage = /* @__PURE__ */ new Map();
1180
+ for (const range of ranges) {
1181
+ const r1 = visibleRowIndexById.get(range.startRowId);
1182
+ const r2 = visibleRowIndexById.get(range.endRowId);
1183
+ if (r1 === void 0 || r2 === void 0) continue;
1184
+ const rowLo = Math.min(r1, r2);
1185
+ const rowHi = Math.max(r1, r2);
1186
+ const startSynth = range.startColumnId === ROW_SELECT_COLUMN_ID;
1187
+ const endSynth = range.endColumnId === ROW_SELECT_COLUMN_ID;
1188
+ let dataColLo;
1189
+ let dataColHi;
1190
+ if (startSynth && endSynth) continue;
1191
+ if (startSynth || endSynth) {
1192
+ dataColLo = 0;
1193
+ dataColHi = colCount - 1;
1194
+ } else {
1195
+ const a = dataColIdxByColId.get(range.startColumnId);
1196
+ const b = dataColIdxByColId.get(range.endColumnId);
1197
+ if (a === void 0 || b === void 0) continue;
1198
+ dataColLo = Math.min(a, b);
1199
+ dataColHi = Math.max(a, b);
1200
+ }
1201
+ for (let rowIdx = rowLo; rowIdx <= rowHi; rowIdx += 1) {
1202
+ let cov = rowCoverage.get(rowIdx);
1203
+ if (!cov) {
1204
+ cov = /* @__PURE__ */ new Set();
1205
+ rowCoverage.set(rowIdx, cov);
1206
+ }
1207
+ for (let colIdx = dataColLo; colIdx <= dataColHi; colIdx += 1) {
1208
+ cov.add(colIdx);
1209
+ }
1210
+ }
1211
+ }
1212
+ for (const [rowIdx, cov] of rowCoverage) {
1213
+ const row = visibleRows[rowIdx];
1214
+ if (!row) continue;
1215
+ if (cov.size === 0) continue;
1216
+ if (cov.size === colCount) fullyRows.add(row.id);
1217
+ else indeterminateRows.add(row.id);
1218
+ }
1219
+ }
1220
+ return {
1221
+ fullySelectedRowIds: fullyRows,
1222
+ indeterminateRowIds: indeterminateRows
1223
+ };
1224
+ }, [
1225
+ snapshot.selection.ranges,
1226
+ snapshot.visibleRows,
1227
+ dataColumnIndex,
1228
+ visibleRowIndexById
1229
+ ]);
1230
+ const isCellSelected = useCallback(
1231
+ (rowId, columnId) => {
1232
+ const ranges = snapshot.selection.ranges;
1233
+ if (ranges.length === 0) return false;
1234
+ const rIdx = visibleRowIndexById.get(rowId);
1235
+ if (rIdx === void 0) return false;
1236
+ const cIdx = dataColumnIndex.idxById.get(columnId);
1237
+ if (cIdx === void 0) return false;
1238
+ for (const range of ranges) {
1239
+ const r1 = visibleRowIndexById.get(range.startRowId);
1240
+ const r2 = visibleRowIndexById.get(range.endRowId);
1241
+ if (r1 === void 0 || r2 === void 0) continue;
1242
+ if (rIdx < Math.min(r1, r2) || rIdx > Math.max(r1, r2)) continue;
1243
+ const startSynth = range.startColumnId === ROW_SELECT_COLUMN_ID;
1244
+ const endSynth = range.endColumnId === ROW_SELECT_COLUMN_ID;
1245
+ if (startSynth && endSynth) continue;
1246
+ if (startSynth || endSynth) return true;
1247
+ const a = dataColumnIndex.idxById.get(range.startColumnId);
1248
+ const b = dataColumnIndex.idxById.get(range.endColumnId);
1249
+ if (a === void 0 || b === void 0) continue;
1250
+ if (cIdx >= Math.min(a, b) && cIdx <= Math.max(a, b)) return true;
1251
+ }
1252
+ return false;
1253
+ },
1254
+ [snapshot.selection.ranges, visibleRowIndexById, dataColumnIndex]
1255
+ );
833
1256
  useLayoutEffect(() => {
834
1257
  const el = viewportRef.current;
835
1258
  if (el && viewportWidth === 0) {
836
1259
  setViewportWidth(el.clientWidth);
837
1260
  }
838
- });
1261
+ }, [viewportWidth]);
839
1262
  useLayoutEffect(() => {
840
1263
  onTelemetryChange?.(telemetry);
841
1264
  }, [onTelemetryChange, telemetry]);
@@ -843,14 +1266,26 @@ function PretableSurface({
843
1266
  onGridReady?.(grid);
844
1267
  }, [grid, onGridReady]);
845
1268
  useLayoutEffect(() => {
846
- if (!interactionState?.selectedRowId) {
1269
+ if (!focusedRowId || !focusedColumnId) {
847
1270
  return;
848
1271
  }
849
- const currentSelectedRowId = snapshot.selection.rowIds[0] ?? null;
850
- if (currentSelectedRowId !== interactionState.selectedRowId) {
851
- onSelectedRowIdChange?.(interactionState.selectedRowId);
1272
+ const cellNode = cellNodesRef.current.get(
1273
+ `${focusedRowId}::${focusedColumnId}`
1274
+ );
1275
+ if (cellNode && document.activeElement !== cellNode) {
1276
+ cellNode.focus({ preventScroll: true });
852
1277
  }
853
- }, [interactionState, onSelectedRowIdChange, snapshot.selection.rowIds]);
1278
+ }, [focusedRowId, focusedColumnId]);
1279
+ useLayoutEffect(() => {
1280
+ const injectedSelectedRowId = state?.selection?.ranges[0]?.startRowId ?? null;
1281
+ if (!injectedSelectedRowId) {
1282
+ return;
1283
+ }
1284
+ const currentSelectedRowId = snapshot.selection.ranges[0]?.startRowId ?? null;
1285
+ if (currentSelectedRowId !== injectedSelectedRowId) {
1286
+ onSelectedRowIdChange?.(injectedSelectedRowId);
1287
+ }
1288
+ }, [state, onSelectedRowIdChange, snapshot.selection.ranges]);
854
1289
  useLayoutEffect(() => {
855
1290
  let nextHeights = measuredHeightsRef.current;
856
1291
  let nextKeys = measuredRowKeysRef.current;
@@ -891,35 +1326,106 @@ function PretableSurface({
891
1326
  if (changed) {
892
1327
  setMeasuredHeights(nextHeights);
893
1328
  }
894
- }, [snapshot.visibleRows, columns, viewportWidth]);
1329
+ }, [snapshot.visibleRows, effectiveColumns, viewportWidth]);
895
1330
  return /* @__PURE__ */ jsxs(
896
1331
  "div",
897
1332
  {
1333
+ "aria-colcount": effectiveColumns.length,
898
1334
  "aria-label": ariaLabel,
1335
+ "aria-multiselectable": "true",
1336
+ "aria-rowcount": snapshot.totalRowCount + 1,
899
1337
  "data-pretable-scroll-viewport": "",
900
1338
  ref: viewportRef,
901
1339
  role: "grid",
902
- tabIndex: 0,
1340
+ tabIndex: -1,
903
1341
  onKeyDown: (event) => {
904
- if (event.key === "ArrowDown" || event.key === "ArrowUp") {
905
- grid.moveFocus(event.key === "ArrowDown" ? 1 : -1);
906
- const nextFocus = grid.getSnapshot().focus;
907
- if (nextFocus.rowId && nextFocus.columnId === null && columns[0]) {
908
- grid.setFocus(nextFocus.rowId, columns[0].id);
909
- }
910
- if (selectFocusedRowOnArrowKey && nextFocus.rowId) {
911
- grid.selectRow(nextFocus.rowId);
912
- onSelectedRowIdChange?.(nextFocus.rowId);
1342
+ if ((event.key === "Escape" || event.key === "Esc") && reorderStateRef.current?.dragging) {
1343
+ reorderStateRef.current = null;
1344
+ setReorderDrag(null);
1345
+ event.preventDefault();
1346
+ return;
1347
+ }
1348
+ if ((event.key === "Escape" || event.key === "Esc") && dragAnchorRef.current !== null && dragStartSelectionRef.current !== null) {
1349
+ const before2 = grid.getSnapshot();
1350
+ grid.setSelection(dragStartSelectionRef.current);
1351
+ dragAnchorRef.current = null;
1352
+ dragStartSelectionRef.current = null;
1353
+ const after = grid.getSnapshot();
1354
+ if (JSON.stringify(before2.selection) !== JSON.stringify(after.selection)) {
1355
+ onSelectionChange?.(after.selection);
913
1356
  }
914
1357
  event.preventDefault();
915
1358
  return;
916
1359
  }
917
- if (event.key === "Enter" || event.key === " " || event.key === "Space") {
918
- const focusedRowId = grid.getSnapshot().focus.rowId;
919
- if (focusedRowId) {
920
- grid.selectRow(focusedRowId);
921
- onSelectedRowIdChange?.(focusedRowId);
922
- event.preventDefault();
1360
+ if ((event.key === "c" || event.key === "C") && (event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && !(event.target instanceof HTMLInputElement) && !(event.target instanceof HTMLTextAreaElement)) {
1361
+ event.preventDefault();
1362
+ const snap = grid.getSnapshot();
1363
+ if (snap.selection.ranges.length === 0) {
1364
+ return;
1365
+ }
1366
+ const args = {
1367
+ ranges: snap.selection.ranges,
1368
+ visibleRows: snap.visibleRows,
1369
+ columns: effectiveColumns,
1370
+ copyWithHeaders: copyWithHeaders ?? false
1371
+ };
1372
+ const payload = onCopy ? onCopy(args) : serializeRangesAsTsv(args);
1373
+ if (payload) {
1374
+ const extent = computeSelectionExtent(
1375
+ snap.selection.ranges,
1376
+ snap,
1377
+ effectiveColumns
1378
+ );
1379
+ Promise.resolve(
1380
+ (copyToClipboard ?? defaultCopyToClipboard)(payload)
1381
+ ).then(() => {
1382
+ scheduleAnnouncement(
1383
+ effectiveMessages.copyAnnouncement({
1384
+ rowCount: extent.rowCount,
1385
+ columnCount: extent.columnCount
1386
+ })
1387
+ );
1388
+ }).catch((err) => {
1389
+ console.warn("[pretable] clipboard copy failed", err);
1390
+ scheduleAnnouncement(
1391
+ effectiveMessages.copyFailedAnnouncement()
1392
+ );
1393
+ });
1394
+ }
1395
+ return;
1396
+ }
1397
+ const isSelectAll = (event.metaKey || event.ctrlKey) && (event.key === "a" || event.key === "A") && !event.shiftKey && !event.altKey;
1398
+ const before = grid.getSnapshot();
1399
+ const handled = handleSurfaceKeyDown(event, {
1400
+ bodyViewportHeight,
1401
+ columns: effectiveColumns,
1402
+ grid,
1403
+ onSelectedRowIdChange,
1404
+ selectFocusedRowOnArrowKey,
1405
+ tabBehavior
1406
+ });
1407
+ if (handled) {
1408
+ event.preventDefault();
1409
+ const after = grid.getSnapshot();
1410
+ if (isSelectAll) {
1411
+ const extent = computeSelectionExtent(
1412
+ after.selection.ranges,
1413
+ after,
1414
+ effectiveColumns
1415
+ );
1416
+ scheduleAnnouncement(
1417
+ effectiveMessages.selectAllAnnouncement({
1418
+ rowCount: extent.rowCount,
1419
+ columnCount: extent.columnCount,
1420
+ isAll: extent.isAll
1421
+ })
1422
+ );
1423
+ }
1424
+ if (before.focus.rowId !== after.focus.rowId || before.focus.columnId !== after.focus.columnId) {
1425
+ onFocusChange?.(after.focus);
1426
+ }
1427
+ if (JSON.stringify(before.selection) !== JSON.stringify(after.selection)) {
1428
+ onSelectionChange?.(after.selection);
923
1429
  }
924
1430
  }
925
1431
  },
@@ -943,12 +1449,93 @@ function PretableSurface({
943
1449
  /* @__PURE__ */ jsx(
944
1450
  "div",
945
1451
  {
1452
+ "aria-atomic": "true",
1453
+ "aria-live": "polite",
1454
+ className: "pt-sr-only",
1455
+ "data-pretable-live-region": "",
1456
+ role: "status",
1457
+ children: liveMessage
1458
+ }
1459
+ ),
1460
+ /* @__PURE__ */ jsx(
1461
+ "div",
1462
+ {
1463
+ "aria-rowindex": 1,
946
1464
  "data-pretable-header-row": "",
1465
+ role: "row",
947
1466
  style: getHeaderRowStyle(renderSnapshot.totalWidth, headerHeight),
948
- children: renderSnapshot.columns.map((plannedCol) => {
949
- const column = columns[plannedCol.index];
1467
+ children: renderSnapshot.columns.flatMap((plannedCol) => {
1468
+ const column = columnsById.get(plannedCol.id);
950
1469
  if (!column) {
951
- return null;
1470
+ return [];
1471
+ }
1472
+ const effWidth = dragLiveWidth?.columnId === column.id ? dragLiveWidth.width : plannedCol.width;
1473
+ if (column.id === ROW_SELECT_COLUMN_ID) {
1474
+ const pinnedOffset2 = pinnedOffsets[column.id];
1475
+ const positionStyle2 = plannedCol.pinned === "left" && pinnedOffset2 !== void 0 ? {
1476
+ ...getHeaderCellStyle(plannedCol.left, plannedCol.width),
1477
+ ...getPinnedCellStyle(pinnedOffset2)
1478
+ } : getHeaderCellStyle(plannedCol.left, plannedCol.width);
1479
+ const visibleRows = snapshot.visibleRows;
1480
+ const allFullySelected = visibleRows.length > 0 && visibleRows.every((r) => fullySelectedRowIds.has(r.id));
1481
+ const anySelected = visibleRows.some(
1482
+ (r) => fullySelectedRowIds.has(r.id) || indeterminateRowIds.has(r.id)
1483
+ );
1484
+ const headerCheckState = allFullySelected ? "true" : anySelected ? "mixed" : "false";
1485
+ const showHeaderCheckbox = rowSelectionColumn?.headerCheckbox !== false;
1486
+ return /* @__PURE__ */ jsx(
1487
+ "div",
1488
+ {
1489
+ "aria-colindex": plannedCol.index + 1,
1490
+ "data-pretable-header-cell": "",
1491
+ "data-pretable-row-select-header": "",
1492
+ "data-pinned": plannedCol.pinned === "left" ? "left" : void 0,
1493
+ role: "columnheader",
1494
+ style: {
1495
+ alignItems: "center",
1496
+ display: "flex",
1497
+ justifyContent: "center",
1498
+ padding: 0,
1499
+ ...positionStyle2
1500
+ },
1501
+ children: showHeaderCheckbox ? /* @__PURE__ */ jsx(
1502
+ "button",
1503
+ {
1504
+ "aria-checked": headerCheckState,
1505
+ "aria-label": "Select all rows",
1506
+ "data-pretable-row-select-all": "true",
1507
+ onClick: (event) => {
1508
+ event.stopPropagation();
1509
+ const before = grid.getSnapshot();
1510
+ const setting = !allFullySelected;
1511
+ grid.setSelectAllVisible(setting);
1512
+ const after = grid.getSnapshot();
1513
+ if (JSON.stringify(before.selection) !== JSON.stringify(after.selection)) {
1514
+ onSelectionChange?.(after.selection);
1515
+ }
1516
+ if (setting) {
1517
+ const extent = computeSelectionExtent(
1518
+ after.selection.ranges,
1519
+ after,
1520
+ effectiveColumns
1521
+ );
1522
+ scheduleAnnouncement(
1523
+ effectiveMessages.selectAllAnnouncement({
1524
+ rowCount: extent.rowCount,
1525
+ columnCount: extent.columnCount,
1526
+ isAll: extent.isAll
1527
+ })
1528
+ );
1529
+ }
1530
+ },
1531
+ role: "checkbox",
1532
+ type: "button",
1533
+ children: headerCheckState === "true" ? "\u2713" : headerCheckState === "mixed" ? "\u2013" : ""
1534
+ }
1535
+ ) : null
1536
+ },
1537
+ column.id
1538
+ );
952
1539
  }
953
1540
  const label = column.header ?? column.id;
954
1541
  const sortDirection = snapshot.sort.columnId === column.id ? snapshot.sort.direction : null;
@@ -958,54 +1545,261 @@ function PretableSurface({
958
1545
  }) ?? {};
959
1546
  const pinnedOffset = pinnedOffsets[column.id];
960
1547
  const positionStyle = plannedCol.pinned === "left" && pinnedOffset !== void 0 ? {
961
- ...getHeaderCellStyle(plannedCol.left, plannedCol.width),
1548
+ ...getHeaderCellStyle(plannedCol.left, effWidth),
962
1549
  ...getPinnedCellStyle(pinnedOffset)
963
- } : getHeaderCellStyle(plannedCol.left, plannedCol.width);
964
- return /* @__PURE__ */ createElement(
965
- "button",
966
- {
967
- ...headerProps,
968
- "aria-label": `Sort ${label}`,
969
- className: getHeaderCellClassName?.({
970
- column,
971
- sortDirection
972
- }),
973
- "data-pretable-header-cell": "",
974
- "data-pinned": plannedCol.pinned === "left" ? "left" : void 0,
975
- key: column.id,
976
- onClick: () => {
977
- const nextDirection = getNextSortDirection(sortDirection);
978
- grid.setSort(column.id, nextDirection);
979
- if (nextDirection) {
980
- onSortChange?.({
1550
+ } : getHeaderCellStyle(plannedCol.left, effWidth);
1551
+ const ariaSort = sortDirection === "asc" ? "ascending" : sortDirection === "desc" ? "descending" : "none";
1552
+ const showResizeHandle = column.resizable !== false;
1553
+ const isDragging = dragLiveWidth?.columnId === column.id;
1554
+ const handleLeft = plannedCol.left + effWidth - 4;
1555
+ const handlePinnedStyle = plannedCol.pinned === "left" && pinnedOffset !== void 0 ? {
1556
+ position: "sticky",
1557
+ zIndex: 3,
1558
+ left: pinnedOffset + effWidth - 4
1559
+ } : null;
1560
+ return [
1561
+ /* @__PURE__ */ createElement(
1562
+ "button",
1563
+ {
1564
+ ...headerProps,
1565
+ "aria-colindex": plannedCol.index + 1,
1566
+ "aria-label": `Sort ${label}`,
1567
+ "aria-sort": ariaSort,
1568
+ className: getHeaderCellClassName?.({
1569
+ column,
1570
+ sortDirection
1571
+ }),
1572
+ "data-pretable-header-cell": "",
1573
+ "data-pinned": plannedCol.pinned === "left" ? "left" : void 0,
1574
+ key: column.id,
1575
+ role: "columnheader",
1576
+ onClick: (event) => {
1577
+ if (wasReorderingRef.current) {
1578
+ event.preventDefault();
1579
+ wasReorderingRef.current = false;
1580
+ return;
1581
+ }
1582
+ const nextDirection = getNextSortDirection(sortDirection);
1583
+ grid.setSort(column.id, nextDirection);
1584
+ if (nextDirection) {
1585
+ onSortChange?.({
1586
+ columnId: column.id,
1587
+ direction: nextDirection
1588
+ });
1589
+ } else {
1590
+ onSortChange?.(null);
1591
+ }
1592
+ },
1593
+ ...column.id !== ROW_SELECT_COLUMN_ID && column.reorderable !== false ? {
1594
+ onPointerDown: (event) => {
1595
+ if (event.button !== 0) return;
1596
+ if (event.shiftKey || event.metaKey || event.ctrlKey)
1597
+ return;
1598
+ reorderStateRef.current = {
1599
+ columnId: column.id,
1600
+ pointerId: event.pointerId,
1601
+ startX: event.clientX,
1602
+ startY: event.clientY,
1603
+ dragging: false
1604
+ };
1605
+ },
1606
+ onPointerMove: (event) => {
1607
+ const drag = reorderStateRef.current;
1608
+ if (!drag || drag.columnId !== column.id) return;
1609
+ if (event.pointerId !== drag.pointerId) return;
1610
+ const dx = event.clientX - drag.startX;
1611
+ const dy = event.clientY - drag.startY;
1612
+ const dist = Math.hypot(dx, dy);
1613
+ const surfaceRect = viewportRef.current?.getBoundingClientRect();
1614
+ const surfaceLeft = surfaceRect?.left ?? 0;
1615
+ if (!drag.dragging) {
1616
+ if (dist < REORDER_THRESHOLD_PX) return;
1617
+ drag.dragging = true;
1618
+ try {
1619
+ event.currentTarget.setPointerCapture(
1620
+ event.pointerId
1621
+ );
1622
+ } catch {
1623
+ }
1624
+ const headerEl = event.currentTarget;
1625
+ const rect = headerEl.getBoundingClientRect();
1626
+ setReorderDrag({
1627
+ columnId: column.id,
1628
+ cursorX: event.clientX,
1629
+ cursorY: event.clientY,
1630
+ dropIndex: computeDropIndex(
1631
+ event.clientX,
1632
+ effectiveColumns.length,
1633
+ columnLefts,
1634
+ columnWidths,
1635
+ surfaceLeft
1636
+ ),
1637
+ ghostWidth: rect.width || effWidth,
1638
+ ghostHeight: rect.height || headerHeight,
1639
+ ghostHeader: String(column.header ?? column.id)
1640
+ });
1641
+ return;
1642
+ }
1643
+ setReorderDrag(
1644
+ (prev) => prev ? {
1645
+ ...prev,
1646
+ cursorX: event.clientX,
1647
+ cursorY: event.clientY,
1648
+ dropIndex: computeDropIndex(
1649
+ event.clientX,
1650
+ effectiveColumns.length,
1651
+ columnLefts,
1652
+ columnWidths,
1653
+ surfaceLeft
1654
+ )
1655
+ } : null
1656
+ );
1657
+ },
1658
+ onPointerUp: (event) => {
1659
+ const drag = reorderStateRef.current;
1660
+ if (!drag || drag.columnId !== column.id) return;
1661
+ if (event.pointerId !== drag.pointerId) return;
1662
+ const current = reorderDrag;
1663
+ if (drag.dragging && current) {
1664
+ wasReorderingRef.current = true;
1665
+ const beforePinned = buildPinnedMap(grid);
1666
+ grid.moveColumn(column.id, current.dropIndex);
1667
+ const afterOrder = grid.options.columns.map((c) => c.id).filter((id) => id !== ROW_SELECT_COLUMN_ID);
1668
+ onColumnOrderChange?.(afterOrder);
1669
+ const afterPinned = buildPinnedMap(grid);
1670
+ if (!pinnedMapsEqual(beforePinned, afterPinned)) {
1671
+ onColumnPinnedChange?.(afterPinned);
1672
+ }
1673
+ }
1674
+ try {
1675
+ event.currentTarget.releasePointerCapture(
1676
+ event.pointerId
1677
+ );
1678
+ } catch {
1679
+ }
1680
+ reorderStateRef.current = null;
1681
+ setReorderDrag(null);
1682
+ },
1683
+ onPointerCancel: () => {
1684
+ reorderStateRef.current = null;
1685
+ setReorderDrag(null);
1686
+ }
1687
+ } : {},
1688
+ style: {
1689
+ alignItems: "start",
1690
+ border: 0,
1691
+ borderRight: "1px solid rgba(255, 255, 255, 0.06)",
1692
+ color: "inherit",
1693
+ display: "grid",
1694
+ gap: 4,
1695
+ textAlign: "left",
1696
+ ...positionStyle
1697
+ },
1698
+ type: "button"
1699
+ },
1700
+ /* @__PURE__ */ jsx(
1701
+ MemoizedHeaderContent,
1702
+ {
1703
+ columnId: column.id,
1704
+ label,
1705
+ sortDirection,
1706
+ isSorted: sortDirection !== null,
1707
+ width: effWidth,
1708
+ isSortable: column.sortable !== false,
1709
+ renderHeaderRef: column.renderHeader ?? null,
1710
+ fallbackRenderHeaderRef: renderHeaderCell ?? null,
1711
+ headerRenderInput: {
1712
+ column,
1713
+ label,
1714
+ sortDirection,
1715
+ isSorted: sortDirection !== null
1716
+ }
1717
+ }
1718
+ )
1719
+ ),
1720
+ showResizeHandle ? /* @__PURE__ */ jsx(
1721
+ "div",
1722
+ {
1723
+ "data-pretable-resize-handle": "",
1724
+ "data-column-id": column.id,
1725
+ "data-dragging": isDragging ? "true" : "false",
1726
+ style: {
1727
+ position: "absolute",
1728
+ top: 0,
1729
+ height: "100%",
1730
+ width: 4,
1731
+ left: handleLeft,
1732
+ cursor: "col-resize",
1733
+ zIndex: 4,
1734
+ touchAction: "none",
1735
+ userSelect: "none",
1736
+ ...handlePinnedStyle ?? {}
1737
+ },
1738
+ onPointerDown: (event) => {
1739
+ if (event.button !== 0) return;
1740
+ event.stopPropagation();
1741
+ const startWidth = column.widthPx ?? plannedCol.width ?? Math.max(column.minWidthPx ?? 40, 80);
1742
+ resizeStateRef.current = {
981
1743
  columnId: column.id,
982
- direction: nextDirection
983
- });
984
- } else {
985
- onSortChange?.(null);
1744
+ startX: event.clientX,
1745
+ startWidth,
1746
+ pointerId: event.pointerId
1747
+ };
1748
+ wasResizingRef.current = false;
1749
+ try {
1750
+ event.currentTarget.setPointerCapture(event.pointerId);
1751
+ } catch {
1752
+ }
1753
+ setDragLiveWidth({ columnId: column.id, width: startWidth });
1754
+ },
1755
+ onPointerMove: (event) => {
1756
+ const drag = resizeStateRef.current;
1757
+ if (!drag || drag.columnId !== column.id) return;
1758
+ const min = column.minWidthPx ?? 40;
1759
+ const max = column.maxWidthPx ?? Infinity;
1760
+ const next = Math.max(
1761
+ min,
1762
+ Math.min(
1763
+ max,
1764
+ drag.startWidth + (event.clientX - drag.startX)
1765
+ )
1766
+ );
1767
+ if (Math.abs(next - drag.startWidth) > 0) {
1768
+ wasResizingRef.current = true;
1769
+ }
1770
+ setDragLiveWidth({ columnId: column.id, width: next });
1771
+ },
1772
+ onPointerUp: (event) => {
1773
+ const drag = resizeStateRef.current;
1774
+ if (!drag || drag.columnId !== column.id) return;
1775
+ const finalWidth = dragLiveWidth?.width ?? drag.startWidth;
1776
+ try {
1777
+ event.currentTarget.releasePointerCapture(drag.pointerId);
1778
+ } catch {
1779
+ }
1780
+ grid.setColumnWidth(column.id, finalWidth);
1781
+ onColumnWidthsChange?.(buildWidthsMap(grid));
1782
+ resizeStateRef.current = null;
1783
+ setDragLiveWidth(null);
1784
+ },
1785
+ onPointerCancel: () => {
1786
+ resizeStateRef.current = null;
1787
+ setDragLiveWidth(null);
1788
+ wasResizingRef.current = false;
1789
+ },
1790
+ onDoubleClick: (event) => {
1791
+ if (wasResizingRef.current) {
1792
+ event.preventDefault();
1793
+ wasResizingRef.current = false;
1794
+ return;
1795
+ }
1796
+ grid.autosizeColumn(column.id);
1797
+ onColumnWidthsChange?.(buildWidthsMap(grid));
986
1798
  }
987
1799
  },
988
- style: {
989
- alignItems: "start",
990
- border: 0,
991
- borderRight: "1px solid rgba(255, 255, 255, 0.06)",
992
- color: "inherit",
993
- display: "grid",
994
- gap: 4,
995
- textAlign: "left",
996
- ...positionStyle
997
- },
998
- type: "button"
999
- },
1000
- renderHeaderCell ? renderHeaderCell({
1001
- column,
1002
- label,
1003
- sortDirection
1004
- }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1005
- /* @__PURE__ */ jsx("span", { children: label }),
1006
- /* @__PURE__ */ jsx("strong", { children: sortDirection === "desc" ? "Newest" : sortDirection === "asc" ? "Oldest" : "Sort" })
1007
- ] })
1008
- );
1800
+ `${column.id}::resize-handle`
1801
+ ) : null
1802
+ ];
1009
1803
  })
1010
1804
  }
1011
1805
  ),
@@ -1019,7 +1813,7 @@ function PretableSurface({
1019
1813
  ),
1020
1814
  children: renderSnapshot.rows.map(({ height, id, row, rowIndex, top }) => {
1021
1815
  const isFocused = snapshot.focus.rowId === id;
1022
- const isSelected = snapshot.selection.rowIds.includes(id);
1816
+ const isSelected = fullySelectedRowIds.has(id);
1023
1817
  const rowProps = getRowProps?.({
1024
1818
  isFocused,
1025
1819
  isSelected,
@@ -1031,8 +1825,8 @@ function PretableSurface({
1031
1825
  "div",
1032
1826
  {
1033
1827
  ...rowProps,
1034
- "aria-rowindex": rowIndex + 1,
1035
- "aria-selected": isSelected,
1828
+ "aria-rowindex": rowIndex + 2,
1829
+ "aria-selected": isSelected ? "true" : void 0,
1036
1830
  className: getRowClassName?.({
1037
1831
  isFocused,
1038
1832
  isSelected,
@@ -1048,11 +1842,7 @@ function PretableSurface({
1048
1842
  "data-selected": isSelected ? "true" : "false",
1049
1843
  "data-testid": "pretable-row",
1050
1844
  key: id,
1051
- onClick: () => {
1052
- grid.setFocus(id, columns[0]?.id ?? null);
1053
- grid.selectRow(id);
1054
- onSelectedRowIdChange?.(id);
1055
- },
1845
+ role: "row",
1056
1846
  ref: (node) => {
1057
1847
  if (node) {
1058
1848
  rowNodesRef.current.set(id, node);
@@ -1063,15 +1853,20 @@ function PretableSurface({
1063
1853
  style: getRowStyle(top, height)
1064
1854
  },
1065
1855
  renderSnapshot.columns.map((plannedCol) => {
1066
- const column = columns[plannedCol.index];
1856
+ const column = columnsById.get(plannedCol.id);
1067
1857
  if (!column) {
1068
1858
  return null;
1069
1859
  }
1070
1860
  const value = resolveCellValue(row, column);
1861
+ const cellKey = `${id}::${column.id}`;
1862
+ const cellIsFocused = isFocused && snapshot.focus.columnId === column.id;
1863
+ const cellIsSelected = isCellSelected(id, column.id);
1864
+ const formattedValue = column.format ? column.format({ value, row, column }) : formatCellValue(value);
1071
1865
  const bodyInput = {
1072
1866
  column,
1073
- isFocused,
1074
- isSelected,
1867
+ formattedValue,
1868
+ isFocused: cellIsFocused,
1869
+ isSelected: cellIsSelected,
1075
1870
  row,
1076
1871
  rowId: id,
1077
1872
  rowIndex,
@@ -1079,39 +1874,310 @@ function PretableSurface({
1079
1874
  };
1080
1875
  const bodyProps = getBodyCellProps?.(bodyInput) ?? {};
1081
1876
  const pinnedOffset = pinnedOffsets[column.id];
1877
+ const cellEffWidth = dragLiveWidth?.columnId === column.id ? dragLiveWidth.width : plannedCol.width;
1082
1878
  const positionStyle = plannedCol.pinned === "left" && pinnedOffset !== void 0 ? {
1083
- ...getCellStyle(plannedCol.left, plannedCol.width),
1879
+ ...getCellStyle(plannedCol.left, cellEffWidth),
1084
1880
  ...getPinnedCellStyle(pinnedOffset)
1085
- } : getCellStyle(plannedCol.left, plannedCol.width);
1881
+ } : getCellStyle(plannedCol.left, cellEffWidth);
1882
+ const isRowSelectCell = column.id === ROW_SELECT_COLUMN_ID;
1883
+ const rowCheckState = fullySelectedRowIds.has(id) ? "true" : indeterminateRowIds.has(id) ? "mixed" : "false";
1086
1884
  return /* @__PURE__ */ createElement(
1087
1885
  "div",
1088
1886
  {
1089
1887
  ...bodyProps,
1888
+ "aria-colindex": plannedCol.index + 1,
1889
+ "aria-selected": cellIsSelected ? "true" : void 0,
1090
1890
  className: getBodyCellClassName?.(bodyInput),
1091
1891
  "data-column-id": column.id,
1092
- "data-focused": isFocused ? "true" : "false",
1892
+ "data-focused": cellIsFocused ? "true" : "false",
1093
1893
  "data-pinned": column.pinned === "left" ? "left" : void 0,
1094
1894
  "data-pretable-cell": "",
1095
1895
  "data-pretable-wrap": column.wrap ? "true" : void 0,
1096
- "data-selected": isSelected ? "true" : "false",
1896
+ "data-row-select-cell": isRowSelectCell ? "true" : void 0,
1897
+ "data-selected": cellIsSelected ? "true" : "false",
1097
1898
  key: `${id}:${column.id}`,
1899
+ onClick: (event) => {
1900
+ if (column.id === ROW_SELECT_COLUMN_ID) return;
1901
+ handleCellClick({
1902
+ cmd: event.metaKey || event.ctrlKey,
1903
+ columnId: column.id,
1904
+ columns: effectiveColumns,
1905
+ grid,
1906
+ onFocusChange,
1907
+ onSelectedRowIdChange,
1908
+ onSelectionChange,
1909
+ rowId: id,
1910
+ shift: event.shiftKey
1911
+ });
1912
+ },
1913
+ onPointerDown: (event) => {
1914
+ if (event.button !== 0) return;
1915
+ if (column.id === ROW_SELECT_COLUMN_ID) return;
1916
+ const cmd = event.metaKey || event.ctrlKey;
1917
+ if (event.shiftKey || cmd) return;
1918
+ dragStartSelectionRef.current = grid.getSnapshot().selection;
1919
+ dragAnchorRef.current = {
1920
+ rowId: id,
1921
+ columnId: column.id
1922
+ };
1923
+ handleCellClick({
1924
+ cmd: false,
1925
+ columnId: column.id,
1926
+ columns: effectiveColumns,
1927
+ grid,
1928
+ onFocusChange,
1929
+ onSelectedRowIdChange,
1930
+ onSelectionChange,
1931
+ rowId: id,
1932
+ shift: false
1933
+ });
1934
+ try {
1935
+ event.currentTarget.setPointerCapture(event.pointerId);
1936
+ } catch {
1937
+ }
1938
+ },
1939
+ onPointerEnter: () => {
1940
+ if (!dragAnchorRef.current) return;
1941
+ if (column.id === ROW_SELECT_COLUMN_ID) return;
1942
+ const before = grid.getSnapshot();
1943
+ const addr = {
1944
+ rowId: id,
1945
+ columnId: column.id
1946
+ };
1947
+ grid.extendRangeFromAnchor(addr);
1948
+ grid.setFocus(addr);
1949
+ const after = grid.getSnapshot();
1950
+ if (before.focus.rowId !== after.focus.rowId || before.focus.columnId !== after.focus.columnId) {
1951
+ onFocusChange?.(after.focus);
1952
+ }
1953
+ if (JSON.stringify(before.selection) !== JSON.stringify(after.selection)) {
1954
+ onSelectionChange?.(after.selection);
1955
+ const beforeFullRow = singleFullRowSelection(
1956
+ before.selection,
1957
+ effectiveColumns.filter(
1958
+ (c) => c.id !== ROW_SELECT_COLUMN_ID
1959
+ )
1960
+ );
1961
+ const afterFullRow = singleFullRowSelection(
1962
+ after.selection,
1963
+ effectiveColumns.filter(
1964
+ (c) => c.id !== ROW_SELECT_COLUMN_ID
1965
+ )
1966
+ );
1967
+ if (beforeFullRow !== afterFullRow) {
1968
+ onSelectedRowIdChange?.(afterFullRow);
1969
+ }
1970
+ }
1971
+ },
1972
+ onPointerUp: () => {
1973
+ dragAnchorRef.current = null;
1974
+ },
1975
+ onPointerCancel: () => {
1976
+ dragAnchorRef.current = null;
1977
+ },
1978
+ ref: (node) => {
1979
+ if (node) {
1980
+ cellNodesRef.current.set(cellKey, node);
1981
+ } else {
1982
+ cellNodesRef.current.delete(cellKey);
1983
+ }
1984
+ },
1985
+ role: "gridcell",
1098
1986
  style: {
1987
+ outline: "none",
1099
1988
  overflowWrap: column.wrap ? "anywhere" : "normal",
1100
1989
  whiteSpace: column.wrap ? "pre-wrap" : "nowrap",
1101
1990
  ...positionStyle
1102
- }
1991
+ },
1992
+ tabIndex: cellIsFocused ? 0 : -1
1103
1993
  },
1104
- renderBodyCell ? renderBodyCell(bodyInput) : formatCellValue(value)
1994
+ isRowSelectCell ? /* @__PURE__ */ jsx(
1995
+ "button",
1996
+ {
1997
+ "aria-checked": rowCheckState,
1998
+ "aria-label": "Select row",
1999
+ "data-pretable-row-select": "true",
2000
+ onClick: (event) => {
2001
+ event.stopPropagation();
2002
+ event.preventDefault();
2003
+ const before = grid.getSnapshot();
2004
+ const visible = before.visibleRows;
2005
+ if (event.shiftKey && lastCheckedRowAnchorRef.current) {
2006
+ const anchorId = lastCheckedRowAnchorRef.current;
2007
+ const anchorIdx = visible.findIndex(
2008
+ (r) => r.id === anchorId
2009
+ );
2010
+ const clickedIdx = visible.findIndex(
2011
+ (r) => r.id === id
2012
+ );
2013
+ if (anchorIdx >= 0 && clickedIdx >= 0) {
2014
+ const [lo, hi] = anchorIdx <= clickedIdx ? [anchorIdx, clickedIdx] : [clickedIdx, anchorIdx];
2015
+ for (let i = lo; i <= hi; i += 1) {
2016
+ const r = visible[i];
2017
+ if (r && !fullySelectedRowIds.has(r.id)) {
2018
+ grid.toggleRowSelection(r.id);
2019
+ }
2020
+ }
2021
+ }
2022
+ } else {
2023
+ grid.toggleRowSelection(id);
2024
+ }
2025
+ lastCheckedRowAnchorRef.current = id;
2026
+ const after = grid.getSnapshot();
2027
+ if (JSON.stringify(before.selection) !== JSON.stringify(after.selection)) {
2028
+ onSelectionChange?.(after.selection);
2029
+ }
2030
+ },
2031
+ role: "checkbox",
2032
+ type: "button",
2033
+ children: rowCheckState === "true" ? "\u2713" : rowCheckState === "mixed" ? "\u2013" : ""
2034
+ }
2035
+ ) : /* @__PURE__ */ jsx(
2036
+ MemoizedCellContent,
2037
+ {
2038
+ rowId: id,
2039
+ columnId: column.id,
2040
+ value,
2041
+ formattedValue,
2042
+ isFocused: cellIsFocused,
2043
+ isSelected: cellIsSelected,
2044
+ renderRef: column.render ?? null,
2045
+ fallbackRenderRef: renderBodyCell ?? null,
2046
+ cellRenderInput: bodyInput
2047
+ }
2048
+ )
1105
2049
  );
1106
2050
  })
1107
2051
  );
1108
2052
  })
1109
2053
  }
1110
- )
2054
+ ),
2055
+ reorderDrag ? /* @__PURE__ */ jsxs(Fragment, { children: [
2056
+ /* @__PURE__ */ jsx(
2057
+ "div",
2058
+ {
2059
+ "data-pretable-reorder-ghost": "",
2060
+ style: {
2061
+ left: reorderDrag.cursorX + 8,
2062
+ top: reorderDrag.cursorY + 8,
2063
+ width: reorderDrag.ghostWidth,
2064
+ height: reorderDrag.ghostHeight,
2065
+ display: "flex",
2066
+ alignItems: "center",
2067
+ paddingLeft: 12
2068
+ },
2069
+ children: reorderDrag.ghostHeader
2070
+ }
2071
+ ),
2072
+ /* @__PURE__ */ jsx(
2073
+ "div",
2074
+ {
2075
+ "data-pretable-reorder-drop-indicator": "",
2076
+ style: {
2077
+ left: computeDropIndicatorLeft(
2078
+ reorderDrag.dropIndex,
2079
+ columnLefts,
2080
+ columnWidths
2081
+ ),
2082
+ height: reorderDrag.ghostHeight + bodyViewportHeight
2083
+ }
2084
+ }
2085
+ )
2086
+ ] }) : null
1111
2087
  ]
1112
2088
  }
1113
2089
  );
1114
2090
  }
2091
+ function replaceSelectionWithFullRow(grid, rowId, columns) {
2092
+ const firstColumn = columns[0];
2093
+ const lastColumn = columns[columns.length - 1];
2094
+ if (!firstColumn || !lastColumn) {
2095
+ grid.setSelection({ ranges: [], anchor: null });
2096
+ return;
2097
+ }
2098
+ grid.setSelection({
2099
+ ranges: [
2100
+ {
2101
+ startRowId: rowId,
2102
+ endRowId: rowId,
2103
+ startColumnId: firstColumn.id,
2104
+ endColumnId: lastColumn.id
2105
+ }
2106
+ ],
2107
+ anchor: { rowId, columnId: firstColumn.id }
2108
+ });
2109
+ }
2110
+ function handleCellClick(args) {
2111
+ const {
2112
+ cmd,
2113
+ columnId,
2114
+ columns,
2115
+ grid,
2116
+ onFocusChange,
2117
+ onSelectedRowIdChange,
2118
+ onSelectionChange,
2119
+ rowId,
2120
+ shift
2121
+ } = args;
2122
+ const before = grid.getSnapshot();
2123
+ const addr = { rowId, columnId };
2124
+ if (shift && !cmd && before.selection.anchor) {
2125
+ grid.extendRangeFromAnchor(addr);
2126
+ grid.setFocus(addr);
2127
+ } else if (cmd) {
2128
+ grid.addRange({
2129
+ startRowId: rowId,
2130
+ endRowId: rowId,
2131
+ startColumnId: columnId,
2132
+ endColumnId: columnId
2133
+ });
2134
+ grid.setFocus(addr);
2135
+ } else {
2136
+ grid.setFocus(addr);
2137
+ grid.setSelection({
2138
+ ranges: [
2139
+ {
2140
+ startRowId: rowId,
2141
+ endRowId: rowId,
2142
+ startColumnId: columnId,
2143
+ endColumnId: columnId
2144
+ }
2145
+ ],
2146
+ anchor: addr
2147
+ });
2148
+ }
2149
+ const after = grid.getSnapshot();
2150
+ if (before.focus.rowId !== after.focus.rowId || before.focus.columnId !== after.focus.columnId) {
2151
+ onFocusChange?.(after.focus);
2152
+ }
2153
+ const selectionChanged = JSON.stringify(before.selection) !== JSON.stringify(after.selection);
2154
+ if (selectionChanged) {
2155
+ onSelectionChange?.(after.selection);
2156
+ const dataColumns = columns.filter((c) => c.id !== ROW_SELECT_COLUMN_ID);
2157
+ const beforeFullRow = singleFullRowSelection(before.selection, dataColumns);
2158
+ const afterFullRow = singleFullRowSelection(after.selection, dataColumns);
2159
+ if (beforeFullRow !== afterFullRow) {
2160
+ onSelectedRowIdChange?.(afterFullRow);
2161
+ }
2162
+ }
2163
+ }
2164
+ function singleFullRowSelection(selection, columns) {
2165
+ if (selection.ranges.length !== 1 || columns.length === 0) {
2166
+ return null;
2167
+ }
2168
+ const range = selection.ranges[0];
2169
+ if (!range) return null;
2170
+ if (range.startRowId !== range.endRowId) return null;
2171
+ const firstColumn = columns[0];
2172
+ const lastColumn = columns[columns.length - 1];
2173
+ if (!firstColumn || !lastColumn) return null;
2174
+ const startMatchesFirst = range.startColumnId === firstColumn.id;
2175
+ const endMatchesLast = range.endColumnId === lastColumn.id;
2176
+ const startMatchesLast = range.startColumnId === lastColumn.id;
2177
+ const endMatchesFirst = range.endColumnId === firstColumn.id;
2178
+ const coversAllColumns = startMatchesFirst && endMatchesLast || startMatchesLast && endMatchesFirst;
2179
+ return coversAllColumns ? range.startRowId : null;
2180
+ }
1115
2181
  function getRowMeasurementKey(rowNode) {
1116
2182
  const rowParts = [
1117
2183
  rowNode.getAttribute("class") ?? "",
@@ -1138,6 +2204,281 @@ function getRowMeasurementKey(rowNode) {
1138
2204
  function normalizeStyleSignature(styleValue) {
1139
2205
  return styleValue.split(";").map((declaration) => declaration.trim()).filter(Boolean).filter((declaration) => !/^top\s*:/i.test(declaration)).join(";");
1140
2206
  }
2207
+ function computeSelectionExtent(ranges, snapshot, effectiveColumns) {
2208
+ const visibleRows = snapshot.visibleRows;
2209
+ const dataColumns = effectiveColumns.filter(
2210
+ (c) => c.id !== ROW_SELECT_COLUMN_ID
2211
+ );
2212
+ if (ranges.length === 0 || visibleRows.length === 0 || dataColumns.length === 0) {
2213
+ return { rowCount: 0, columnCount: 0, isAll: false };
2214
+ }
2215
+ const rowOrder = /* @__PURE__ */ new Map();
2216
+ for (let i = 0; i < visibleRows.length; i += 1) {
2217
+ const r = visibleRows[i];
2218
+ if (r) rowOrder.set(r.id, i);
2219
+ }
2220
+ const columnOrder = /* @__PURE__ */ new Map();
2221
+ for (let i = 0; i < effectiveColumns.length; i += 1) {
2222
+ const c = effectiveColumns[i];
2223
+ if (c) columnOrder.set(c.id, i);
2224
+ }
2225
+ const coveredRows = /* @__PURE__ */ new Set();
2226
+ const coveredCols = /* @__PURE__ */ new Set();
2227
+ for (const range of ranges) {
2228
+ const r1 = rowOrder.get(range.startRowId);
2229
+ const r2 = rowOrder.get(range.endRowId);
2230
+ if (r1 === void 0 || r2 === void 0) continue;
2231
+ const rowLo = Math.min(r1, r2);
2232
+ const rowHi = Math.max(r1, r2);
2233
+ const startSynth = range.startColumnId === ROW_SELECT_COLUMN_ID;
2234
+ const endSynth = range.endColumnId === ROW_SELECT_COLUMN_ID;
2235
+ let colsForRange;
2236
+ if (startSynth && endSynth) {
2237
+ continue;
2238
+ }
2239
+ if (startSynth || endSynth) {
2240
+ colsForRange = dataColumns.slice();
2241
+ } else {
2242
+ const c1 = columnOrder.get(range.startColumnId);
2243
+ const c2 = columnOrder.get(range.endColumnId);
2244
+ if (c1 === void 0 || c2 === void 0) continue;
2245
+ const colLo = Math.min(c1, c2);
2246
+ const colHi = Math.max(c1, c2);
2247
+ colsForRange = [];
2248
+ for (let i = colLo; i <= colHi; i += 1) {
2249
+ const col = effectiveColumns[i];
2250
+ if (col && col.id !== ROW_SELECT_COLUMN_ID) {
2251
+ colsForRange.push(col);
2252
+ }
2253
+ }
2254
+ }
2255
+ if (colsForRange.length === 0) continue;
2256
+ for (let i = rowLo; i <= rowHi; i += 1) {
2257
+ const row = visibleRows[i];
2258
+ if (row) coveredRows.add(row.id);
2259
+ }
2260
+ for (const col of colsForRange) {
2261
+ coveredCols.add(col.id);
2262
+ }
2263
+ }
2264
+ const rowCount = coveredRows.size;
2265
+ const columnCount = coveredCols.size;
2266
+ const isAll = rowCount === visibleRows.length && columnCount === dataColumns.length;
2267
+ return { rowCount, columnCount, isAll };
2268
+ }
2269
+ var ARROW_DIRECTIONS = {
2270
+ ArrowUp: "up",
2271
+ ArrowDown: "down",
2272
+ ArrowLeft: "left",
2273
+ ArrowRight: "right"
2274
+ };
2275
+ function handleSurfaceKeyDown(event, ctx) {
2276
+ const {
2277
+ bodyViewportHeight,
2278
+ columns: allColumns,
2279
+ grid,
2280
+ onSelectedRowIdChange,
2281
+ selectFocusedRowOnArrowKey,
2282
+ tabBehavior
2283
+ } = ctx;
2284
+ const columns = allColumns.filter((c) => c.id !== ROW_SELECT_COLUMN_ID);
2285
+ const { key } = event;
2286
+ const cmd = event.metaKey || event.ctrlKey;
2287
+ const shift = event.shiftKey;
2288
+ const snapshot = grid.getSnapshot();
2289
+ const focus = snapshot.focus;
2290
+ const visibleRows = snapshot.visibleRows;
2291
+ const firstColumn = columns[0];
2292
+ const lastColumn = columns[columns.length - 1];
2293
+ const direction = ARROW_DIRECTIONS[key];
2294
+ if (direction) {
2295
+ grid.moveFocus(direction, {
2296
+ extend: shift,
2297
+ jumpToEdge: cmd
2298
+ });
2299
+ const after = grid.getSnapshot();
2300
+ if (after.focus.columnId === ROW_SELECT_COLUMN_ID && firstColumn) {
2301
+ const rowId = after.focus.rowId;
2302
+ if (rowId) {
2303
+ grid.setFocus({ rowId, columnId: firstColumn.id });
2304
+ }
2305
+ }
2306
+ if (selectFocusedRowOnArrowKey) {
2307
+ const nextFocus = grid.getSnapshot().focus;
2308
+ if (nextFocus.rowId) {
2309
+ replaceSelectionWithFullRow(grid, nextFocus.rowId, columns);
2310
+ onSelectedRowIdChange?.(nextFocus.rowId);
2311
+ }
2312
+ }
2313
+ return true;
2314
+ }
2315
+ if (key === "Home") {
2316
+ if (!firstColumn) return false;
2317
+ if (cmd) {
2318
+ const firstRow = visibleRows[0];
2319
+ if (!firstRow) return false;
2320
+ grid.setFocus({ rowId: firstRow.id, columnId: firstColumn.id });
2321
+ } else if (focus.rowId) {
2322
+ grid.setFocus({ rowId: focus.rowId, columnId: firstColumn.id });
2323
+ } else {
2324
+ const firstRow = visibleRows[0];
2325
+ if (!firstRow) return false;
2326
+ grid.setFocus({ rowId: firstRow.id, columnId: firstColumn.id });
2327
+ }
2328
+ return true;
2329
+ }
2330
+ if (key === "End") {
2331
+ if (!lastColumn) return false;
2332
+ if (cmd) {
2333
+ const lastRow = visibleRows[visibleRows.length - 1];
2334
+ if (!lastRow) return false;
2335
+ grid.setFocus({ rowId: lastRow.id, columnId: lastColumn.id });
2336
+ } else if (focus.rowId) {
2337
+ grid.setFocus({ rowId: focus.rowId, columnId: lastColumn.id });
2338
+ } else {
2339
+ const firstRow = visibleRows[0];
2340
+ if (!firstRow) return false;
2341
+ grid.setFocus({ rowId: firstRow.id, columnId: lastColumn.id });
2342
+ }
2343
+ return true;
2344
+ }
2345
+ if (key === "PageUp" || key === "PageDown") {
2346
+ if (visibleRows.length === 0 || !firstColumn) return false;
2347
+ const pageRowCount = Math.max(1, Math.floor(bodyViewportHeight / 32));
2348
+ const currentRowIdx = focus.rowId ? visibleRows.findIndex((r) => r.id === focus.rowId) : -1;
2349
+ const baseRowIdx = currentRowIdx === -1 ? 0 : currentRowIdx;
2350
+ const nextRowIdx = key === "PageUp" ? Math.max(0, baseRowIdx - pageRowCount) : Math.min(visibleRows.length - 1, baseRowIdx + pageRowCount);
2351
+ const nextRow = visibleRows[nextRowIdx];
2352
+ if (!nextRow) return false;
2353
+ const columnId = focus.columnId ?? firstColumn.id;
2354
+ const addr = { rowId: nextRow.id, columnId };
2355
+ if (shift) {
2356
+ if (!snapshot.selection.anchor && focus.rowId && focus.columnId) {
2357
+ grid.setSelection({
2358
+ ranges: [
2359
+ {
2360
+ startRowId: focus.rowId,
2361
+ endRowId: focus.rowId,
2362
+ startColumnId: focus.columnId,
2363
+ endColumnId: focus.columnId
2364
+ }
2365
+ ],
2366
+ anchor: { rowId: focus.rowId, columnId: focus.columnId }
2367
+ });
2368
+ }
2369
+ grid.setFocus(addr);
2370
+ grid.extendRangeFromAnchor(addr);
2371
+ } else {
2372
+ grid.setFocus(addr);
2373
+ }
2374
+ return true;
2375
+ }
2376
+ if (key === "Tab") {
2377
+ if (tabBehavior === "exit") {
2378
+ return false;
2379
+ }
2380
+ if (visibleRows.length === 0 || columns.length === 0) return false;
2381
+ const currentRowIdx = focus.rowId ? visibleRows.findIndex((r) => r.id === focus.rowId) : -1;
2382
+ const currentColIdx = focus.columnId ? columns.findIndex((c) => c.id === focus.columnId) : -1;
2383
+ const baseRowIdx = currentRowIdx === -1 ? 0 : currentRowIdx;
2384
+ const baseColIdx = currentColIdx === -1 ? 0 : currentColIdx;
2385
+ let nextRowIdx = baseRowIdx;
2386
+ let nextColIdx = baseColIdx;
2387
+ if (shift) {
2388
+ if (baseColIdx === 0) {
2389
+ nextColIdx = columns.length - 1;
2390
+ nextRowIdx = Math.max(0, baseRowIdx - 1);
2391
+ if (baseRowIdx === 0) {
2392
+ nextColIdx = 0;
2393
+ nextRowIdx = 0;
2394
+ }
2395
+ } else {
2396
+ nextColIdx = baseColIdx - 1;
2397
+ }
2398
+ } else {
2399
+ if (baseColIdx === columns.length - 1) {
2400
+ nextColIdx = 0;
2401
+ nextRowIdx = Math.min(visibleRows.length - 1, baseRowIdx + 1);
2402
+ if (baseRowIdx === visibleRows.length - 1) {
2403
+ nextColIdx = columns.length - 1;
2404
+ nextRowIdx = visibleRows.length - 1;
2405
+ }
2406
+ } else {
2407
+ nextColIdx = baseColIdx + 1;
2408
+ }
2409
+ }
2410
+ const nextRow = visibleRows[nextRowIdx];
2411
+ const nextCol = columns[nextColIdx];
2412
+ if (!nextRow || !nextCol) return false;
2413
+ grid.setFocus({ rowId: nextRow.id, columnId: nextCol.id });
2414
+ return true;
2415
+ }
2416
+ if (cmd && (key === "a" || key === "A")) {
2417
+ grid.selectAll();
2418
+ return true;
2419
+ }
2420
+ if (key === "Escape" || key === "Esc") {
2421
+ grid.clearSelection();
2422
+ return true;
2423
+ }
2424
+ if (key === "Enter" || key === " " || key === "Space") {
2425
+ const focusedRowId = focus.rowId;
2426
+ if (focusedRowId) {
2427
+ replaceSelectionWithFullRow(grid, focusedRowId, columns);
2428
+ onSelectedRowIdChange?.(focusedRowId);
2429
+ return true;
2430
+ }
2431
+ return false;
2432
+ }
2433
+ return false;
2434
+ }
2435
+ function buildWidthsMap(grid) {
2436
+ const result = {};
2437
+ for (const col of grid.options.columns) {
2438
+ if (col.id === ROW_SELECT_COLUMN_ID) continue;
2439
+ if (typeof col.widthPx === "number") {
2440
+ result[col.id] = col.widthPx;
2441
+ }
2442
+ }
2443
+ return result;
2444
+ }
2445
+ function buildPinnedMap(grid) {
2446
+ const result = {};
2447
+ for (const col of grid.options.columns) {
2448
+ if (col.id === ROW_SELECT_COLUMN_ID) continue;
2449
+ result[col.id] = col.pinned === "left" ? "left" : null;
2450
+ }
2451
+ return result;
2452
+ }
2453
+ function pinnedMapsEqual(a, b) {
2454
+ const aKeys = Object.keys(a);
2455
+ const bKeys = Object.keys(b);
2456
+ if (aKeys.length !== bKeys.length) return false;
2457
+ for (const k of aKeys) {
2458
+ if (a[k] !== b[k]) return false;
2459
+ }
2460
+ return true;
2461
+ }
2462
+ function computeDropIndex(cursorX, columnCount, columnLefts, columnWidths, surfaceLeft) {
2463
+ const x = cursorX - surfaceLeft;
2464
+ for (let i = 0; i < columnCount; i += 1) {
2465
+ const left = columnLefts[i] ?? 0;
2466
+ const width = columnWidths[i] ?? 0;
2467
+ const mid = left + width / 2;
2468
+ if (x < mid) {
2469
+ return i;
2470
+ }
2471
+ }
2472
+ return Math.max(0, columnCount - 1);
2473
+ }
2474
+ function computeDropIndicatorLeft(dropIndex, columnLefts, columnWidths) {
2475
+ if (dropIndex >= columnLefts.length) {
2476
+ const lastIdx = columnLefts.length - 1;
2477
+ if (lastIdx < 0) return 0;
2478
+ return (columnLefts[lastIdx] ?? 0) + (columnWidths[lastIdx] ?? 0);
2479
+ }
2480
+ return columnLefts[dropIndex] ?? 0;
2481
+ }
1141
2482
  var VIEWPORT_HEIGHT = 320;
1142
2483
  var BENCHMARK_VIEWPORT_STYLE = {
1143
2484
  contain: "none",
@@ -1149,7 +2490,16 @@ var BENCHMARK_VIEWPORT_STYLE = {
1149
2490
  function Pretable({
1150
2491
  columns,
1151
2492
  getRowId,
1152
- rows
2493
+ rows,
2494
+ rowSelectionColumn,
2495
+ tabBehavior,
2496
+ copyWithHeaders,
2497
+ onCopy,
2498
+ copyToClipboard,
2499
+ messages,
2500
+ onColumnWidthsChange,
2501
+ onColumnOrderChange,
2502
+ onColumnPinnedChange
1153
2503
  }) {
1154
2504
  const resolvedGetRowId = getRowId ?? ((row, index) => {
1155
2505
  const candidate = row.id;
@@ -1233,6 +2583,15 @@ function Pretable({
1233
2583
  )
1234
2584
  ] }),
1235
2585
  rows,
2586
+ rowSelectionColumn,
2587
+ tabBehavior,
2588
+ copyWithHeaders,
2589
+ onCopy,
2590
+ copyToClipboard,
2591
+ messages,
2592
+ onColumnWidthsChange,
2593
+ onColumnOrderChange,
2594
+ onColumnPinnedChange,
1236
2595
  viewportStyle: BENCHMARK_VIEWPORT_STYLE,
1237
2596
  viewportHeight: VIEWPORT_HEIGHT
1238
2597
  }
@@ -1252,7 +2611,7 @@ var inspectionColumns = [
1252
2611
  id: "tags",
1253
2612
  header: "Tags",
1254
2613
  widthPx: 200,
1255
- getValue: (row) => row.tags.join(", ")
2614
+ value: (row) => row.tags.join(", ")
1256
2615
  },
1257
2616
  { id: "message", header: "Message", wrap: true, widthPx: 480 }
1258
2617
  ];
@@ -1265,22 +2624,33 @@ function LabeledGridSurface({
1265
2624
  getHeaderCellProps,
1266
2625
  getRowId,
1267
2626
  headerCellClassName,
1268
- interactionState,
2627
+ state,
1269
2628
  labelClassName,
1270
2629
  overscan,
1271
2630
  onSelectedRowIdChange,
2631
+ onSelectionChange,
2632
+ onFocusChange,
1272
2633
  onSortChange,
2634
+ onColumnWidthsChange,
2635
+ onColumnOrderChange,
2636
+ onColumnPinnedChange,
1273
2637
  onTelemetryChange,
1274
2638
  pinnedClassName,
1275
2639
  rowClassName,
1276
2640
  rows,
2641
+ rowSelectionColumn,
1277
2642
  selectFocusedRowOnArrowKey,
2643
+ tabBehavior,
2644
+ copyWithHeaders,
2645
+ onCopy,
2646
+ copyToClipboard,
2647
+ messages,
1278
2648
  valueClassName,
1279
2649
  viewportHeight
1280
2650
  }) {
1281
2651
  const getPinnedClassName = (column) => column.pinned === "left" && pinnedClassName ? pinnedClassName : void 0;
1282
2652
  const activeFilterColumns = new Set(
1283
- Object.entries(interactionState?.filters ?? {}).filter(([, value]) => value.trim() !== "").map(([columnId]) => columnId)
2653
+ Object.entries(state?.filters ?? {}).filter(([, value]) => value.trim() !== "").map(([columnId]) => columnId)
1284
2654
  );
1285
2655
  const getFormattedValue = ({
1286
2656
  column,
@@ -1312,10 +2682,15 @@ function LabeledGridSurface({
1312
2682
  ),
1313
2683
  getRowClassName: () => rowClassName,
1314
2684
  getRowId,
1315
- interactionState,
2685
+ state,
1316
2686
  overscan,
1317
2687
  onSelectedRowIdChange,
2688
+ onSelectionChange,
2689
+ onFocusChange,
1318
2690
  onSortChange,
2691
+ onColumnWidthsChange,
2692
+ onColumnOrderChange,
2693
+ onColumnPinnedChange,
1319
2694
  onTelemetryChange,
1320
2695
  renderBodyCell: ({ column, row, value }) => /* @__PURE__ */ jsxs(Fragment, { children: [
1321
2696
  /* @__PURE__ */ jsx("span", { className: labelClassName, children: column.header ?? column.id }),
@@ -1330,7 +2705,13 @@ function LabeledGridSurface({
1330
2705
  sortDirection ? /* @__PURE__ */ jsx("span", { className: "sort-indicator", children: sortDirection === "desc" ? "\u25BC" : "\u25B2" }) : null
1331
2706
  ] }),
1332
2707
  rows,
2708
+ rowSelectionColumn,
1333
2709
  selectFocusedRowOnArrowKey,
2710
+ tabBehavior,
2711
+ copyWithHeaders,
2712
+ onCopy,
2713
+ copyToClipboard,
2714
+ messages,
1334
2715
  viewportHeight
1335
2716
  }
1336
2717
  );
@@ -1367,12 +2748,23 @@ var filterableHeaderProps = {
1367
2748
  function InspectionGrid({
1368
2749
  ariaLabel,
1369
2750
  filterableColumnIds,
1370
- interactionState,
2751
+ state,
1371
2752
  onSelectedRowIdChange,
2753
+ onSelectionChange,
2754
+ onFocusChange,
1372
2755
  onSortChange,
2756
+ onColumnWidthsChange,
2757
+ onColumnOrderChange,
2758
+ onColumnPinnedChange,
1373
2759
  onTelemetryChange,
1374
2760
  overscan,
1375
2761
  rows,
2762
+ rowSelectionColumn,
2763
+ tabBehavior,
2764
+ copyWithHeaders,
2765
+ onCopy,
2766
+ copyToClipboard,
2767
+ messages,
1376
2768
  viewportHeight
1377
2769
  }) {
1378
2770
  const filterableColumns = new Set(filterableColumnIds);
@@ -1387,16 +2779,27 @@ function InspectionGrid({
1387
2779
  getHeaderCellProps: ({ column }) => filterableColumns.has(column.id) ? filterableHeaderProps : void 0,
1388
2780
  getRowId: getInspectionRowId,
1389
2781
  headerCellClassName: "inspection-header-cell",
1390
- interactionState,
2782
+ state,
1391
2783
  labelClassName: "inspection-cell-label",
1392
2784
  overscan,
1393
2785
  onSelectedRowIdChange,
2786
+ onSelectionChange,
2787
+ onFocusChange,
1394
2788
  onSortChange,
2789
+ onColumnWidthsChange,
2790
+ onColumnOrderChange,
2791
+ onColumnPinnedChange,
1395
2792
  onTelemetryChange,
1396
2793
  pinnedClassName: "is-pinned",
1397
2794
  rowClassName: "inspection-row",
1398
2795
  rows,
2796
+ rowSelectionColumn,
1399
2797
  selectFocusedRowOnArrowKey: true,
2798
+ tabBehavior,
2799
+ copyWithHeaders,
2800
+ onCopy,
2801
+ copyToClipboard,
2802
+ messages,
1400
2803
  valueClassName: "inspection-cell-value",
1401
2804
  viewportHeight
1402
2805
  }
@@ -1409,4 +2812,4 @@ function formatInspectionValue(value) {
1409
2812
  return String(value ?? "");
1410
2813
  }
1411
2814
 
1412
- export { InspectionGrid, LabeledGridSurface, Pretable, PretableSurface, measureRenderedRowHeight, usePretable, usePretableModel, useResolvedHeights };
2815
+ export { InspectionGrid, LabeledGridSurface, Pretable, PretableSurface, defaultCoerceForCopy, serializeRangesAsTsv, usePretable, ROW_SELECT_COLUMN_ID as ɵROW_SELECT_COLUMN_ID, measureRenderedRowHeight as ɵmeasureRenderedRowHeight, useResolvedHeights as ɵuseResolvedHeights };