@undevy-org/tablewright 0.8.1 → 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,
@@ -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 {
@@ -4868,6 +4901,7 @@ function createTableOrchestrationStore({
4868
4901
  requestOpenFilterId: null,
4869
4902
  currentSort: null,
4870
4903
  columnVisibility: initialColumnVisibility ?? {},
4904
+ columnPinning: { left: [], right: [] },
4871
4905
  page: 1,
4872
4906
  rowsPerPage: initialRowsPerPage,
4873
4907
  density: "normal",
@@ -4948,6 +4982,19 @@ function createTableOrchestrationStore({
4948
4982
  set({ columnVisibility: {} });
4949
4983
  goCustom();
4950
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
+ })),
4951
4998
  setPage: (p) => set({ page: p }),
4952
4999
  setRowsPerPage: (n) => set({ rowsPerPage: n, page: 1 }),
4953
5000
  setDensity: (v) => set({ density: v }),
@@ -5372,6 +5419,25 @@ function useResponsiveColumns(columnPriorities, wideBreakpoint, compactBreakpoin
5372
5419
  }, [isWide, columnPriorities]);
5373
5420
  return { responsiveHiddenColumnIds, isCompact };
5374
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
+ }
5375
5441
  const useIsomorphicLayoutEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
5376
5442
  function useTableOrchestration(config) {
5377
5443
  const {
@@ -5404,6 +5470,9 @@ function useTableOrchestration(config) {
5404
5470
  const requestOpenFilterId = zustand.useStore(store, (s) => s.requestOpenFilterId);
5405
5471
  const currentSort = zustand.useStore(store, (s) => s.currentSort);
5406
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);
5407
5476
  const storedPage = zustand.useStore(store, (s) => s.page);
5408
5477
  const rowsPerPage = zustand.useStore(store, (s) => s.rowsPerPage);
5409
5478
  const density = zustand.useStore(store, (s) => s.density);
@@ -5484,13 +5553,29 @@ function useTableOrchestration(config) {
5484
5553
  );
5485
5554
  const allOnPageSelected = pageIds.size > 0 && selectedOnPage === pageIds.size;
5486
5555
  const headerCheckState = allOnPageSelected ? true : selectedOnPage > 0 ? "indeterminate" : false;
5487
- const { columnGaps, extendedColumnMeta, extendedWidths } = useColumnGaps({
5556
+ const { columnGaps, extendedColumnMeta: gapExtendedColumnMeta, extendedWidths } = useColumnGaps({
5488
5557
  columnGroups,
5489
5558
  toggleableColumnIds,
5490
5559
  columnVisibility,
5491
5560
  columnMeta,
5492
5561
  widths
5493
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
+ );
5494
5579
  const hiddenColumnsCount = React.useMemo(() => {
5495
5580
  return toggleableColumnIds.filter((id) => columnVisibility[id] === false).length;
5496
5581
  }, [toggleableColumnIds, columnVisibility]);
@@ -5549,6 +5634,9 @@ function useTableOrchestration(config) {
5549
5634
  currentSort,
5550
5635
  columnVisibility,
5551
5636
  setColumnVisibility,
5637
+ columnPinning,
5638
+ pinColumn,
5639
+ unpinColumn,
5552
5640
  page,
5553
5641
  rowsPerPage,
5554
5642
  density,
@@ -6089,6 +6177,7 @@ exports.TimestampCell = TimestampCell;
6089
6177
  exports.TopBar = TopBar;
6090
6178
  exports.TwoPanelMenuBase = TwoPanelMenuBase;
6091
6179
  exports.ViewChip = ViewChip;
6180
+ exports.applyColumnPinningToMeta = applyColumnPinningToMeta;
6092
6181
  exports.applyManagedChanges = applyManagedChanges;
6093
6182
  exports.badgeVariants = badgeVariants;
6094
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@undevy-org/tablewright",
3
- "version": "0.8.1",
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": {