@undevy-org/tablewright 0.8.0 → 0.9.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.
@@ -1,3 +1,36 @@
1
+ function columnWidthPx(columnId, meta, columnWidths) {
2
+ return columnWidths?.[columnId] ?? meta[columnId]?.minW ?? 0;
3
+ }
4
+ function applyColumnPinningToMeta(baseMeta, pinning, options) {
5
+ const order = options?.columnOrder ?? Object.keys(baseMeta);
6
+ const pinnedLeft = new Set(pinning.left ?? []);
7
+ const pinnedRight = new Set(pinning.right ?? []);
8
+ const result = {};
9
+ for (const [id, meta] of Object.entries(baseMeta)) {
10
+ if (pinnedLeft.has(id)) {
11
+ result[id] = { ...meta, sticky: "left" };
12
+ } else if (pinnedRight.has(id)) {
13
+ result[id] = { ...meta, sticky: "right" };
14
+ } else {
15
+ result[id] = { ...meta };
16
+ }
17
+ }
18
+ let leftOffset = 0;
19
+ for (const id of order) {
20
+ const meta = result[id];
21
+ if (meta?.sticky !== "left") continue;
22
+ result[id] = { ...meta, stickyOffset: leftOffset };
23
+ leftOffset += columnWidthPx(id, baseMeta, options?.columnWidths);
24
+ }
25
+ let rightOffset = 0;
26
+ for (const id of [...order].reverse()) {
27
+ const meta = result[id];
28
+ if (meta?.sticky !== "right") continue;
29
+ result[id] = { ...meta, stickyOffset: rightOffset };
30
+ rightOffset += columnWidthPx(id, baseMeta, options?.columnWidths);
31
+ }
32
+ return result;
33
+ }
1
34
  function stickyStyle(meta, isHeader) {
2
35
  if (isHeader) {
3
36
  return {
@@ -20,6 +53,8 @@ function stickyBorderClass(meta) {
20
53
  return edge === "right" ? "[box-shadow:1px_0_0_0_var(--border-subtle)]" : "[box-shadow:-1px_0_0_0_var(--border-subtle)]";
21
54
  }
22
55
  export {
56
+ applyColumnPinningToMeta,
57
+ columnWidthPx,
23
58
  stickyBorderClass,
24
59
  stickyStyle
25
60
  };
@@ -28,6 +28,7 @@ function createTableOrchestrationStore({
28
28
  requestOpenFilterId: null,
29
29
  currentSort: null,
30
30
  columnVisibility: initialColumnVisibility ?? {},
31
+ columnPinning: { left: [], right: [] },
31
32
  page: 1,
32
33
  rowsPerPage: initialRowsPerPage,
33
34
  density: "normal",
@@ -108,6 +109,19 @@ function createTableOrchestrationStore({
108
109
  set({ columnVisibility: {} });
109
110
  goCustom();
110
111
  },
112
+ pinColumn: (columnId, side) => set((s) => {
113
+ const left = (s.columnPinning.left ?? []).filter((id) => id !== columnId);
114
+ const right = (s.columnPinning.right ?? []).filter((id) => id !== columnId);
115
+ if (side === "left") left.push(columnId);
116
+ else right.push(columnId);
117
+ return { columnPinning: { left, right } };
118
+ }),
119
+ unpinColumn: (columnId) => set((s) => ({
120
+ columnPinning: {
121
+ left: (s.columnPinning.left ?? []).filter((id) => id !== columnId),
122
+ right: (s.columnPinning.right ?? []).filter((id) => id !== columnId)
123
+ }
124
+ })),
111
125
  setPage: (p) => set({ page: p }),
112
126
  setRowsPerPage: (n) => set({ rowsPerPage: n, page: 1 }),
113
127
  setDensity: (v) => set({ density: v }),
@@ -1,6 +1,7 @@
1
1
  import { useState, useRef, useMemo, useCallback, useLayoutEffect, useEffect } from "react";
2
2
  import { useStore } from "zustand";
3
3
  import { useColumnResize } from "../components/data-table/hooks/use-column-resize.js";
4
+ import { applyColumnPinningToMeta } from "../components/data-table/sticky-utils.js";
4
5
  import { getVisiblePageNumbers } from "../components/data-table/hooks/use-table-pagination.js";
5
6
  import { createTableOrchestrationStore } from "./table-orchestration-store.js";
6
7
  import { useColumnFilterStats } from "./table-orchestration/use-column-filter-stats.js";
@@ -8,6 +9,26 @@ import { useColumnGaps } from "./table-orchestration/use-column-gaps.js";
8
9
  import { useFilteredRows } from "./table-orchestration/use-filtered-rows.js";
9
10
  import { useOrchestrationActions } from "./table-orchestration/use-orchestration-actions.js";
10
11
  import { useResponsiveColumns } from "./table-orchestration/use-responsive-columns.js";
12
+ import { gapColumnId } from "../lib/column-gaps.js";
13
+ function computeVisibleColumnOrder(input) {
14
+ const { columnMeta, toggleableColumnIds, columnVisibility, columnGaps } = input;
15
+ const order = [];
16
+ if (columnMeta.rowControl) order.push("rowControl");
17
+ for (const id of toggleableColumnIds) {
18
+ if (columnVisibility[id] !== false) order.push(id);
19
+ }
20
+ for (const gap of columnGaps) {
21
+ const gapId = gapColumnId(gap.afterColumnId);
22
+ if (gap.afterColumnId === null) {
23
+ order.unshift(gapId);
24
+ } else {
25
+ const afterIdx = order.indexOf(gap.afterColumnId);
26
+ if (afterIdx >= 0) order.splice(afterIdx + 1, 0, gapId);
27
+ }
28
+ }
29
+ if (columnMeta.actions) order.push("actions");
30
+ return order;
31
+ }
11
32
  const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
12
33
  function useTableOrchestration(config) {
13
34
  const {
@@ -40,6 +61,9 @@ function useTableOrchestration(config) {
40
61
  const requestOpenFilterId = useStore(store, (s) => s.requestOpenFilterId);
41
62
  const currentSort = useStore(store, (s) => s.currentSort);
42
63
  const columnVisibility = useStore(store, (s) => s.columnVisibility);
64
+ const columnPinning = useStore(store, (s) => s.columnPinning);
65
+ const pinColumn = useStore(store, (s) => s.pinColumn);
66
+ const unpinColumn = useStore(store, (s) => s.unpinColumn);
43
67
  const storedPage = useStore(store, (s) => s.page);
44
68
  const rowsPerPage = useStore(store, (s) => s.rowsPerPage);
45
69
  const density = useStore(store, (s) => s.density);
@@ -120,13 +144,29 @@ function useTableOrchestration(config) {
120
144
  );
121
145
  const allOnPageSelected = pageIds.size > 0 && selectedOnPage === pageIds.size;
122
146
  const headerCheckState = allOnPageSelected ? true : selectedOnPage > 0 ? "indeterminate" : false;
123
- const { columnGaps, extendedColumnMeta, extendedWidths } = useColumnGaps({
147
+ const { columnGaps, extendedColumnMeta: gapExtendedColumnMeta, extendedWidths } = useColumnGaps({
124
148
  columnGroups,
125
149
  toggleableColumnIds,
126
150
  columnVisibility,
127
151
  columnMeta,
128
152
  widths
129
153
  });
154
+ const visibleColumnOrder = useMemo(
155
+ () => computeVisibleColumnOrder({
156
+ columnMeta,
157
+ toggleableColumnIds,
158
+ columnVisibility,
159
+ columnGaps
160
+ }),
161
+ [columnGaps, columnMeta, columnVisibility, toggleableColumnIds]
162
+ );
163
+ const extendedColumnMeta = useMemo(
164
+ () => applyColumnPinningToMeta(gapExtendedColumnMeta, columnPinning, {
165
+ columnOrder: visibleColumnOrder,
166
+ columnWidths: extendedWidths
167
+ }),
168
+ [columnPinning, extendedWidths, gapExtendedColumnMeta, visibleColumnOrder]
169
+ );
130
170
  const hiddenColumnsCount = useMemo(() => {
131
171
  return toggleableColumnIds.filter((id) => columnVisibility[id] === false).length;
132
172
  }, [toggleableColumnIds, columnVisibility]);
@@ -185,6 +225,9 @@ function useTableOrchestration(config) {
185
225
  currentSort,
186
226
  columnVisibility,
187
227
  setColumnVisibility,
228
+ columnPinning,
229
+ pinColumn,
230
+ unpinColumn,
188
231
  page,
189
232
  rowsPerPage,
190
233
  density,
@@ -249,5 +292,6 @@ function useTableOrchestration(config) {
249
292
  };
250
293
  }
251
294
  export {
295
+ computeVisibleColumnOrder,
252
296
  useTableOrchestration
253
297
  };
package/dist/es/index.js CHANGED
@@ -67,6 +67,7 @@ import { useTableOrchestration } from "./hooks/useTableOrchestration.js";
67
67
  import { createTableOrchestrationStore, defaultValueForFilter } from "./hooks/table-orchestration-store.js";
68
68
  import { useRowDragOverride } from "./hooks/useRowDragOverride.js";
69
69
  import { downloadCsvFile, exportTableToCsv } from "./lib/export-table-csv.js";
70
+ import { applyColumnPinningToMeta } from "./components/data-table/sticky-utils.js";
70
71
  import { computeColumnGaps, gapColumnId } from "./lib/column-gaps.js";
71
72
  import { computeColumnStats } from "./lib/filter-stats.js";
72
73
  import { formatEnumLabel } from "./lib/format-enum.js";
@@ -176,6 +177,7 @@ export {
176
177
  TopBar,
177
178
  TwoPanelMenuBase,
178
179
  ViewChip,
180
+ applyColumnPinningToMeta,
179
181
  applyManagedChanges,
180
182
  badgeVariants,
181
183
  buttonVariants,
@@ -8,17 +8,39 @@ function resolveTableExportButtonSize(size) {
8
8
  function resolveTableExportButtonVariant(variant) {
9
9
  return variant ?? "secondary";
10
10
  }
11
+ const PLAIN_DECIMAL_CORE = /^(\d+(\.\d+)?|\.\d+)([eE][+-]?\d+)?$/;
12
+ const FORMULA_TRIGGER_PREFIX = /^[=+\-@\t\r]/;
11
13
  function escapeCsvField(value) {
12
14
  if (/[",\r\n]/.test(value)) {
13
15
  return `"${value.replace(/"/g, '""')}"`;
14
16
  }
15
17
  return value;
16
18
  }
19
+ function isSignedPlainDecimal(value) {
20
+ const first = value[0];
21
+ if (first !== "+" && first !== "-") {
22
+ return false;
23
+ }
24
+ return PLAIN_DECIMAL_CORE.test(value.slice(1));
25
+ }
26
+ function shouldPrefixFormulaEscape(value) {
27
+ if (!FORMULA_TRIGGER_PREFIX.test(value)) {
28
+ return false;
29
+ }
30
+ if (isSignedPlainDecimal(value)) {
31
+ return false;
32
+ }
33
+ return true;
34
+ }
35
+ function formatCsvField(value, escapeFormulas) {
36
+ const prepared = escapeFormulas && shouldPrefixFormulaEscape(value) ? `'${value}` : value;
37
+ return escapeCsvField(prepared);
38
+ }
17
39
  function exportTableToCsv(options) {
18
- const { columns, rows, getCellText } = options;
19
- const headerLine = columns.map((column) => escapeCsvField(column.header)).join(",");
40
+ const { columns, rows, getCellText, escapeFormulas = true } = options;
41
+ const headerLine = columns.map((column) => formatCsvField(column.header, escapeFormulas)).join(",");
20
42
  const bodyLines = rows.map(
21
- (row) => columns.map((column) => escapeCsvField(getCellText(row, column.id))).join(",")
43
+ (row) => columns.map((column) => formatCsvField(getCellText(row, column.id), escapeFormulas)).join(",")
22
44
  );
23
45
  return [headerLine, ...bodyLines].join("\r\n");
24
46
  }
@@ -931,6 +931,39 @@ function ScrollArea({ children, className }) {
931
931
  );
932
932
  }
933
933
  TableLayout.ScrollArea = ScrollArea;
934
+ function columnWidthPx(columnId, meta, columnWidths) {
935
+ return columnWidths?.[columnId] ?? meta[columnId]?.minW ?? 0;
936
+ }
937
+ function applyColumnPinningToMeta(baseMeta, pinning, options) {
938
+ const order = options?.columnOrder ?? Object.keys(baseMeta);
939
+ const pinnedLeft = new Set(pinning.left ?? []);
940
+ const pinnedRight = new Set(pinning.right ?? []);
941
+ const result = {};
942
+ for (const [id, meta] of Object.entries(baseMeta)) {
943
+ if (pinnedLeft.has(id)) {
944
+ result[id] = { ...meta, sticky: "left" };
945
+ } else if (pinnedRight.has(id)) {
946
+ result[id] = { ...meta, sticky: "right" };
947
+ } else {
948
+ result[id] = { ...meta };
949
+ }
950
+ }
951
+ let leftOffset = 0;
952
+ for (const id of order) {
953
+ const meta = result[id];
954
+ if (meta?.sticky !== "left") continue;
955
+ result[id] = { ...meta, stickyOffset: leftOffset };
956
+ leftOffset += columnWidthPx(id, baseMeta, options?.columnWidths);
957
+ }
958
+ let rightOffset = 0;
959
+ for (const id of [...order].reverse()) {
960
+ const meta = result[id];
961
+ if (meta?.sticky !== "right") continue;
962
+ result[id] = { ...meta, stickyOffset: rightOffset };
963
+ rightOffset += columnWidthPx(id, baseMeta, options?.columnWidths);
964
+ }
965
+ return result;
966
+ }
934
967
  function stickyStyle(meta, isHeader) {
935
968
  if (isHeader) {
936
969
  return {
@@ -1824,17 +1857,39 @@ function resolveTableExportButtonSize(size) {
1824
1857
  function resolveTableExportButtonVariant(variant) {
1825
1858
  return variant ?? "secondary";
1826
1859
  }
1860
+ const PLAIN_DECIMAL_CORE = /^(\d+(\.\d+)?|\.\d+)([eE][+-]?\d+)?$/;
1861
+ const FORMULA_TRIGGER_PREFIX = /^[=+\-@\t\r]/;
1827
1862
  function escapeCsvField(value) {
1828
1863
  if (/[",\r\n]/.test(value)) {
1829
1864
  return `"${value.replace(/"/g, '""')}"`;
1830
1865
  }
1831
1866
  return value;
1832
1867
  }
1868
+ function isSignedPlainDecimal(value) {
1869
+ const first = value[0];
1870
+ if (first !== "+" && first !== "-") {
1871
+ return false;
1872
+ }
1873
+ return PLAIN_DECIMAL_CORE.test(value.slice(1));
1874
+ }
1875
+ function shouldPrefixFormulaEscape(value) {
1876
+ if (!FORMULA_TRIGGER_PREFIX.test(value)) {
1877
+ return false;
1878
+ }
1879
+ if (isSignedPlainDecimal(value)) {
1880
+ return false;
1881
+ }
1882
+ return true;
1883
+ }
1884
+ function formatCsvField(value, escapeFormulas) {
1885
+ const prepared = escapeFormulas && shouldPrefixFormulaEscape(value) ? `'${value}` : value;
1886
+ return escapeCsvField(prepared);
1887
+ }
1833
1888
  function exportTableToCsv(options) {
1834
- const { columns, rows, getCellText } = options;
1835
- const headerLine = columns.map((column) => escapeCsvField(column.header)).join(",");
1889
+ const { columns, rows, getCellText, escapeFormulas = true } = options;
1890
+ const headerLine = columns.map((column) => formatCsvField(column.header, escapeFormulas)).join(",");
1836
1891
  const bodyLines = rows.map(
1837
- (row) => columns.map((column) => escapeCsvField(getCellText(row, column.id))).join(",")
1892
+ (row) => columns.map((column) => formatCsvField(getCellText(row, column.id), escapeFormulas)).join(",")
1838
1893
  );
1839
1894
  return [headerLine, ...bodyLines].join("\r\n");
1840
1895
  }
@@ -4846,6 +4901,7 @@ function createTableOrchestrationStore({
4846
4901
  requestOpenFilterId: null,
4847
4902
  currentSort: null,
4848
4903
  columnVisibility: initialColumnVisibility ?? {},
4904
+ columnPinning: { left: [], right: [] },
4849
4905
  page: 1,
4850
4906
  rowsPerPage: initialRowsPerPage,
4851
4907
  density: "normal",
@@ -4926,6 +4982,19 @@ function createTableOrchestrationStore({
4926
4982
  set({ columnVisibility: {} });
4927
4983
  goCustom();
4928
4984
  },
4985
+ pinColumn: (columnId, side) => set((s) => {
4986
+ const left = (s.columnPinning.left ?? []).filter((id) => id !== columnId);
4987
+ const right = (s.columnPinning.right ?? []).filter((id) => id !== columnId);
4988
+ if (side === "left") left.push(columnId);
4989
+ else right.push(columnId);
4990
+ return { columnPinning: { left, right } };
4991
+ }),
4992
+ unpinColumn: (columnId) => set((s) => ({
4993
+ columnPinning: {
4994
+ left: (s.columnPinning.left ?? []).filter((id) => id !== columnId),
4995
+ right: (s.columnPinning.right ?? []).filter((id) => id !== columnId)
4996
+ }
4997
+ })),
4929
4998
  setPage: (p) => set({ page: p }),
4930
4999
  setRowsPerPage: (n) => set({ rowsPerPage: n, page: 1 }),
4931
5000
  setDensity: (v) => set({ density: v }),
@@ -5350,6 +5419,25 @@ function useResponsiveColumns(columnPriorities, wideBreakpoint, compactBreakpoin
5350
5419
  }, [isWide, columnPriorities]);
5351
5420
  return { responsiveHiddenColumnIds, isCompact };
5352
5421
  }
5422
+ function computeVisibleColumnOrder(input) {
5423
+ const { columnMeta, toggleableColumnIds, columnVisibility, columnGaps } = input;
5424
+ const order = [];
5425
+ if (columnMeta.rowControl) order.push("rowControl");
5426
+ for (const id of toggleableColumnIds) {
5427
+ if (columnVisibility[id] !== false) order.push(id);
5428
+ }
5429
+ for (const gap of columnGaps) {
5430
+ const gapId = gapColumnId(gap.afterColumnId);
5431
+ if (gap.afterColumnId === null) {
5432
+ order.unshift(gapId);
5433
+ } else {
5434
+ const afterIdx = order.indexOf(gap.afterColumnId);
5435
+ if (afterIdx >= 0) order.splice(afterIdx + 1, 0, gapId);
5436
+ }
5437
+ }
5438
+ if (columnMeta.actions) order.push("actions");
5439
+ return order;
5440
+ }
5353
5441
  const useIsomorphicLayoutEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
5354
5442
  function useTableOrchestration(config) {
5355
5443
  const {
@@ -5382,6 +5470,9 @@ function useTableOrchestration(config) {
5382
5470
  const requestOpenFilterId = zustand.useStore(store, (s) => s.requestOpenFilterId);
5383
5471
  const currentSort = zustand.useStore(store, (s) => s.currentSort);
5384
5472
  const columnVisibility = zustand.useStore(store, (s) => s.columnVisibility);
5473
+ const columnPinning = zustand.useStore(store, (s) => s.columnPinning);
5474
+ const pinColumn = zustand.useStore(store, (s) => s.pinColumn);
5475
+ const unpinColumn = zustand.useStore(store, (s) => s.unpinColumn);
5385
5476
  const storedPage = zustand.useStore(store, (s) => s.page);
5386
5477
  const rowsPerPage = zustand.useStore(store, (s) => s.rowsPerPage);
5387
5478
  const density = zustand.useStore(store, (s) => s.density);
@@ -5462,13 +5553,29 @@ function useTableOrchestration(config) {
5462
5553
  );
5463
5554
  const allOnPageSelected = pageIds.size > 0 && selectedOnPage === pageIds.size;
5464
5555
  const headerCheckState = allOnPageSelected ? true : selectedOnPage > 0 ? "indeterminate" : false;
5465
- const { columnGaps, extendedColumnMeta, extendedWidths } = useColumnGaps({
5556
+ const { columnGaps, extendedColumnMeta: gapExtendedColumnMeta, extendedWidths } = useColumnGaps({
5466
5557
  columnGroups,
5467
5558
  toggleableColumnIds,
5468
5559
  columnVisibility,
5469
5560
  columnMeta,
5470
5561
  widths
5471
5562
  });
5563
+ const visibleColumnOrder = React.useMemo(
5564
+ () => computeVisibleColumnOrder({
5565
+ columnMeta,
5566
+ toggleableColumnIds,
5567
+ columnVisibility,
5568
+ columnGaps
5569
+ }),
5570
+ [columnGaps, columnMeta, columnVisibility, toggleableColumnIds]
5571
+ );
5572
+ const extendedColumnMeta = React.useMemo(
5573
+ () => applyColumnPinningToMeta(gapExtendedColumnMeta, columnPinning, {
5574
+ columnOrder: visibleColumnOrder,
5575
+ columnWidths: extendedWidths
5576
+ }),
5577
+ [columnPinning, extendedWidths, gapExtendedColumnMeta, visibleColumnOrder]
5578
+ );
5472
5579
  const hiddenColumnsCount = React.useMemo(() => {
5473
5580
  return toggleableColumnIds.filter((id) => columnVisibility[id] === false).length;
5474
5581
  }, [toggleableColumnIds, columnVisibility]);
@@ -5527,6 +5634,9 @@ function useTableOrchestration(config) {
5527
5634
  currentSort,
5528
5635
  columnVisibility,
5529
5636
  setColumnVisibility,
5637
+ columnPinning,
5638
+ pinColumn,
5639
+ unpinColumn,
5530
5640
  page,
5531
5641
  rowsPerPage,
5532
5642
  density,
@@ -6067,6 +6177,7 @@ exports.TimestampCell = TimestampCell;
6067
6177
  exports.TopBar = TopBar;
6068
6178
  exports.TwoPanelMenuBase = TwoPanelMenuBase;
6069
6179
  exports.ViewChip = ViewChip;
6180
+ exports.applyColumnPinningToMeta = applyColumnPinningToMeta;
6070
6181
  exports.applyManagedChanges = applyManagedChanges;
6071
6182
  exports.badgeVariants = badgeVariants;
6072
6183
  exports.buttonVariants = buttonVariants;
@@ -1,4 +1,15 @@
1
+ import type { ColumnPinningState } from "@tanstack/react-table";
1
2
  import type { CSSProperties } from "react";
2
3
  import type { ColumnMetaDef } from "./types.js";
4
+ export interface ApplyColumnPinningToMetaOptions {
5
+ /** Visible columns in left-to-right display order (include gap columns when present). */
6
+ columnOrder: readonly string[];
7
+ /** Resolved widths in px; falls back to `meta.minW`. */
8
+ columnWidths?: Record<string, number>;
9
+ }
10
+ /** @internal Exported for unit tests (not re-exported from package entry). */
11
+ export declare function columnWidthPx(columnId: string, meta: Record<string, ColumnMetaDef>, columnWidths?: Record<string, number>): number;
12
+ /** Merges TanStack `ColumnPinningState` into column meta sticky fields for rendering. */
13
+ export declare function applyColumnPinningToMeta(baseMeta: Record<string, ColumnMetaDef>, pinning: ColumnPinningState, options?: ApplyColumnPinningToMetaOptions): Record<string, ColumnMetaDef>;
3
14
  export declare function stickyStyle(meta: ColumnMetaDef | undefined, isHeader: boolean): CSSProperties | undefined;
4
15
  export declare function stickyBorderClass(meta: ColumnMetaDef | undefined): string;
@@ -1,4 +1,4 @@
1
- import type { VisibilityState } from "@tanstack/react-table";
1
+ import type { ColumnPinningState, VisibilityState } from "@tanstack/react-table";
2
2
  import type { ActiveFilter, ActiveFilterValue, ColumnFilterConfig } from "../components/data-table/filter-types.js";
3
3
  export interface TableOrchestrationState<TViewKey extends string = string> {
4
4
  searchInput: string;
@@ -11,6 +11,7 @@ export interface TableOrchestrationState<TViewKey extends string = string> {
11
11
  direction: "asc" | "desc";
12
12
  } | null;
13
13
  columnVisibility: VisibilityState;
14
+ columnPinning: ColumnPinningState;
14
15
  page: number;
15
16
  rowsPerPage: number;
16
17
  density: "normal" | "dense";
@@ -39,6 +40,8 @@ export interface TableOrchestrationActions<TViewKey extends string = string> {
39
40
  hideColumn: (columnId: string) => void;
40
41
  showColumns: (columnIds: string[]) => void;
41
42
  resetColumns: () => void;
43
+ pinColumn: (columnId: string, side: "left" | "right") => void;
44
+ unpinColumn: (columnId: string) => void;
42
45
  setPage: (p: number) => void;
43
46
  setRowsPerPage: (n: number) => void;
44
47
  setDensity: (v: "normal" | "dense") => void;
@@ -1,7 +1,14 @@
1
- import type { VisibilityState } from "@tanstack/react-table";
1
+ import type { ColumnPinningState, VisibilityState } from "@tanstack/react-table";
2
2
  import type { ColumnMetaDef, SortDirection } from "../components/data-table/types.js";
3
3
  import type { ActiveFilter, ActiveFilterValue, ColumnFilterConfig, ColumnGap, ColumnGroup, FilterStatsSlice, ViewPreset } from "../components/data-table/filter-types.js";
4
4
  import { type TableOrchestrationStoreApi } from "./table-orchestration-store.js";
5
+ /** @internal Exported for unit tests (not re-exported from package entry). */
6
+ export declare function computeVisibleColumnOrder(input: {
7
+ columnMeta: Record<string, ColumnMetaDef>;
8
+ toggleableColumnIds: readonly string[];
9
+ columnVisibility: VisibilityState;
10
+ columnGaps: ColumnGap[];
11
+ }): string[];
5
12
  export interface TableOrchestrationConfig<TRow extends object, TViewKey extends string> {
6
13
  rows: TRow[];
7
14
  getRowId: (row: TRow) => string;
@@ -49,6 +56,9 @@ export interface TableOrchestrationReturn<TRow extends object, TViewKey extends
49
56
  } | null;
50
57
  columnVisibility: VisibilityState;
51
58
  setColumnVisibility: React.Dispatch<React.SetStateAction<VisibilityState>>;
59
+ columnPinning: ColumnPinningState;
60
+ pinColumn: (columnId: string, side: "left" | "right") => void;
61
+ unpinColumn: (columnId: string) => void;
52
62
  page: number;
53
63
  rowsPerPage: number;
54
64
  density: "normal" | "dense";
@@ -104,6 +104,8 @@ export { useRowDragOverride } from "./hooks/useRowDragOverride.js";
104
104
  export type { UseRowDragOverrideResult } from "./hooks/useRowDragOverride.js";
105
105
  export { downloadCsvFile, exportTableToCsv, } from "./lib/export-table-csv.js";
106
106
  export type { DownloadCsvFileOptions, ExportTableToCsvOptions, } from "./lib/export-table-csv.js";
107
+ export { applyColumnPinningToMeta } from "./components/data-table/sticky-utils.js";
108
+ export type { ApplyColumnPinningToMetaOptions } from "./components/data-table/sticky-utils.js";
107
109
  export { computeColumnGaps, gapColumnId } from "./lib/column-gaps.js";
108
110
  export { computeColumnStats } from "./lib/filter-stats.js";
109
111
  export { formatEnumLabel } from "./lib/format-enum.js";
@@ -6,6 +6,11 @@ export type ExportTableToCsvOptions<TRow> = {
6
6
  }[];
7
7
  rows: TRow[];
8
8
  getCellText: (row: TRow, columnId: string) => string;
9
+ /**
10
+ * Prefix formula-triggering cell text with `'` so spreadsheet apps do not evaluate it.
11
+ * Plain decimal numbers (`-5`, `+3.2`) are left unchanged. @default true
12
+ */
13
+ escapeFormulas?: boolean;
9
14
  };
10
15
  export declare const DEFAULT_CSV_EXPORT_LABEL = "Export CSV";
11
16
  export declare function resolveCsvExportLabel(labels?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@undevy-org/tablewright",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "React data-table and application-shell components: filters, drawers, row controls, sortable and resizable columns, built on Radix UI and TanStack Table.",
5
5
  "license": "MIT",
6
6
  "repository": {