@crispy-seed/data-table 0.1.1 → 0.1.3

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.
Files changed (49) hide show
  1. package/dist/index.cjs +4570 -2
  2. package/dist/index.js +4554 -1
  3. package/package.json +1 -1
  4. package/dist/DataTable-12s-D02v6Qj5.js +0 -1523
  5. package/dist/DataTable-12s-Gpv6I4J8.cjs +0 -1574
  6. package/dist/DataTable-12s-Zml1x9Kr-12s-B-mwMBhn.cjs +0 -161
  7. package/dist/DataTable-12s-Zml1x9Kr-12s-BR-yHfob.js +0 -153
  8. package/dist/Menu-12s-A4lEc__n.cjs +0 -103
  9. package/dist/Menu-12s-v9vnaq8F.js +0 -77
  10. package/dist/Select-12s-CE3SMzFp.js +0 -218
  11. package/dist/Select-12s-CNQC9z8t.cjs +0 -249
  12. package/dist/body-cells-12s-2sTwdliH.cjs +0 -461
  13. package/dist/body-cells-12s-Ba2uF4T-.js +0 -452
  14. package/dist/column-calc-12s-C1-I2KdV.js +0 -129
  15. package/dist/column-calc-12s-CJ26VuDJ.cjs +0 -149
  16. package/dist/column-layout-12s-BhQmNTJG.js +0 -99
  17. package/dist/column-layout-12s-k2fZYeKl.cjs +0 -120
  18. package/dist/column-menu-12s-6Yvu4SZ0.js +0 -406
  19. package/dist/column-menu-12s-BnknuXGh.cjs +0 -426
  20. package/dist/editors-12s-C0RxmTcI.cjs +0 -554
  21. package/dist/editors-12s-COixUnI2.js +0 -524
  22. package/dist/resize-12s-CajyNXFB.js +0 -58
  23. package/dist/resize-12s-ChLpxb4i.cjs +0 -58
  24. package/dist/states-12s-BB0PT_bq.cjs +0 -125
  25. package/dist/states-12s-m5mBR90e.js +0 -122
  26. package/dist/toolbar-12s-B1xiS-Zu.js +0 -68
  27. package/dist/toolbar-12s-DzLbghpK.cjs +0 -70
  28. package/dist/useDataTable-12s-BHiDUubw-12s-CBeCwTe7.js +0 -61
  29. package/dist/useDataTable-12s-BHiDUubw-12s-Dgvt7XDk.cjs +0 -61
  30. package/dist/useDataTableColumnCalc-12s-DTQQTtEJ-12s-CvD14JYo.cjs +0 -207
  31. package/dist/useDataTableColumnCalc-12s-DTQQTtEJ-12s-DsEse3uq.js +0 -183
  32. package/dist/useDataTableColumns-12s-Cn2ZAzFL-12s-CD6goR59.js +0 -289
  33. package/dist/useDataTableColumns-12s-Cn2ZAzFL-12s-CUk_CpYT.cjs +0 -292
  34. package/dist/useDataTableEdit-12s-9Wb_UCrN-12s-BoOHq3GB.js +0 -98
  35. package/dist/useDataTableEdit-12s-9Wb_UCrN-12s-De6YHBba.cjs +0 -98
  36. package/dist/useDataTableFilter-12s-ZzbmWMin-12s-CWyBeFna.js +0 -74
  37. package/dist/useDataTableFilter-12s-ZzbmWMin-12s-lpjzZY3_.cjs +0 -74
  38. package/dist/useDataTableInfiniteScroll-12s-8ZG31URd-12s-BexjP6iO.js +0 -104
  39. package/dist/useDataTableInfiniteScroll-12s-8ZG31URd-12s-Bt8gojr9.cjs +0 -104
  40. package/dist/useDataTableRow-12s-CPYaDa1p-12s-4qkGoz-j.js +0 -30
  41. package/dist/useDataTableRow-12s-CPYaDa1p-12s-DG64uYjn.cjs +0 -30
  42. package/dist/useDataTableScroll-12s-BKDCr0en-12s-C7f2vHeG.js +0 -61
  43. package/dist/useDataTableScroll-12s-BKDCr0en-12s-CUvUYMtt.cjs +0 -61
  44. package/dist/useDataTableSearch-12s-Ck3iWcpX-12s-B-65V6gH.js +0 -37
  45. package/dist/useDataTableSearch-12s-Ck3iWcpX-12s-DhVfN2Hq.cjs +0 -37
  46. package/dist/useDataTableSelection-12s-BIDtiRQq-12s-IS__VAnU.cjs +0 -71
  47. package/dist/useDataTableSelection-12s-BIDtiRQq-12s-O6M_BhrH.js +0 -71
  48. package/dist/useDataTableSort-12s-DvJYtmeS-12s-C5VGnsq4.cjs +0 -78
  49. package/dist/useDataTableSort-12s-DvJYtmeS-12s-DABSNt6U.js +0 -78
@@ -1,207 +0,0 @@
1
- 'use client';
2
- function __insertCSS(code) {
3
- if (!code || typeof document == 'undefined') return
4
- let head = document.head || document.getElementsByTagName('head')[0]
5
- let style = document.createElement('style')
6
- style.type = 'text/css'
7
- head.appendChild(style)
8
- ;style.styleSheet ? (style.styleSheet.cssText = code) : style.appendChild(document.createTextNode(code))
9
- }
10
-
11
- var React = require('react');
12
-
13
- function _interopNamespace(e) {
14
- if (e && e.__esModule) return e;
15
- var n = Object.create(null);
16
- if (e) {
17
- Object.keys(e).forEach(function (k) {
18
- if (k !== 'default') {
19
- var d = Object.getOwnPropertyDescriptor(e, k);
20
- Object.defineProperty(n, k, d.get ? d : {
21
- enumerable: true,
22
- get: function () { return e[k]; }
23
- });
24
- }
25
- });
26
- }
27
- n.default = e;
28
- return n;
29
- }
30
-
31
- var React__namespace = /*#__PURE__*/_interopNamespace(React);
32
-
33
- // packages/react/use-layout-effect/src/use-layout-effect.tsx
34
- var useLayoutEffect2 = globalThis?.document ? React__namespace.useLayoutEffect : ()=>{};
35
-
36
- // src/use-controllable-state.tsx
37
- var useInsertionEffect = React__namespace[" useInsertionEffect ".trim().toString()] || useLayoutEffect2;
38
- function useControllableState({ prop, defaultProp, onChange = ()=>{}, caller }) {
39
- const [uncontrolledProp, setUncontrolledProp, onChangeRef] = useUncontrolledState({
40
- defaultProp,
41
- onChange
42
- });
43
- const isControlled = prop !== void 0;
44
- const value = isControlled ? prop : uncontrolledProp;
45
- {
46
- const isControlledRef = React__namespace.useRef(prop !== void 0);
47
- React__namespace.useEffect(()=>{
48
- const wasControlled = isControlledRef.current;
49
- if (wasControlled !== isControlled) {
50
- const from = wasControlled ? "controlled" : "uncontrolled";
51
- const to = isControlled ? "controlled" : "uncontrolled";
52
- console.warn(`${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`);
53
- }
54
- isControlledRef.current = isControlled;
55
- }, [
56
- isControlled,
57
- caller
58
- ]);
59
- }
60
- const setValue = React__namespace.useCallback((nextValue)=>{
61
- if (isControlled) {
62
- const value2 = isFunction(nextValue) ? nextValue(prop) : nextValue;
63
- if (value2 !== prop) {
64
- onChangeRef.current?.(value2);
65
- }
66
- } else {
67
- setUncontrolledProp(nextValue);
68
- }
69
- }, [
70
- isControlled,
71
- prop,
72
- setUncontrolledProp,
73
- onChangeRef
74
- ]);
75
- return [
76
- value,
77
- setValue
78
- ];
79
- }
80
- function useUncontrolledState({ defaultProp, onChange }) {
81
- const [value, setValue] = React__namespace.useState(defaultProp);
82
- const prevValueRef = React__namespace.useRef(value);
83
- const onChangeRef = React__namespace.useRef(onChange);
84
- useInsertionEffect(()=>{
85
- onChangeRef.current = onChange;
86
- }, [
87
- onChange
88
- ]);
89
- React__namespace.useEffect(()=>{
90
- if (prevValueRef.current !== value) {
91
- onChangeRef.current?.(value);
92
- prevValueRef.current = value;
93
- }
94
- }, [
95
- value,
96
- prevValueRef
97
- ]);
98
- return [
99
- value,
100
- setValue,
101
- onChangeRef
102
- ];
103
- }
104
- function isFunction(value) {
105
- return typeof value === "function";
106
- }
107
-
108
- const DATA_TABLE_UNIVERSAL_CALC_OPS = [
109
- "count",
110
- "countFilled"
111
- ];
112
- const DATA_TABLE_NUMERIC_CALC_OPS = [
113
- "sum",
114
- "average",
115
- "min",
116
- "max"
117
- ];
118
- const DATA_TABLE_CALC_LABEL = {
119
- count: "Count",
120
- countFilled: "Count filled",
121
- sum: "Sum",
122
- average: "Average",
123
- min: "Min",
124
- max: "Max"
125
- };
126
- /** A value counts as empty when it is null/undefined, an empty string, or an empty
127
- * array -- crisp's own `isEmptyValue` (columncalc.tsx), reused verbatim. */ function isEmptyValueForCalc(value) {
128
- if (value == null) return true;
129
- if (typeof value === "string") return value.trim() === "";
130
- if (Array.isArray(value)) return value.length === 0;
131
- return false;
132
- }
133
- /**
134
- * Pure so the arithmetic is independently (and mutation-) testable without rendering a
135
- * table at all. `sum`/`average`/`min`/`max` skip empty values and non-numeric ones
136
- * (coerced via `Number`, dropping `NaN`); they return `null` -- render nothing, not a
137
- * misleading 0 -- when there is nothing numeric left to reduce over.
138
- */ function computeColumnCalc(values, op) {
139
- switch(op){
140
- case "count":
141
- return values.length;
142
- case "countFilled":
143
- return values.filter((value)=>!isEmptyValueForCalc(value)).length;
144
- }
145
- const numbers = values.filter((value)=>!isEmptyValueForCalc(value)).map((value)=>typeof value === "number" ? value : Number(value)).filter((value)=>Number.isFinite(value));
146
- if (numbers.length === 0) return null;
147
- switch(op){
148
- case "sum":
149
- return numbers.reduce((total, value)=>total + value, 0);
150
- case "average":
151
- return numbers.reduce((total, value)=>total + value, 0) / numbers.length;
152
- case "min":
153
- return Math.min(...numbers);
154
- case "max":
155
- return Math.max(...numbers);
156
- default:
157
- return null;
158
- }
159
- }
160
- const EMPTY_OPS = {};
161
- // Owns which aggregation (if any) each column's ColumnCalc footer cell currently
162
- // shows -- the way useDataTableFilter owns one filter per column key. The actual
163
- // REDUCE over row values needs `TRow` (a type this headless package never sees), so
164
- // -- same split as sort/search/filter -- this hook only tracks WHICH op is active per
165
- // column; the styled layer (DataTable.tsx's `ColumnCalcCell`) calls `computeColumnCalc`
166
- // with the column's own resolved values.
167
- function useDataTableColumnCalc(props) {
168
- const { columnCalcOps: opsProp, defaultColumnCalcOps = EMPTY_OPS, onColumnCalcOpsChange } = props;
169
- const [columnCalcOps = EMPTY_OPS, setColumnCalcOps] = useControllableState({
170
- prop: opsProp,
171
- defaultProp: defaultColumnCalcOps,
172
- onChange: onColumnCalcOpsChange
173
- });
174
- const getColumnCalcOp = React.useCallback((columnKey)=>columnCalcOps[columnKey], [
175
- columnCalcOps
176
- ]);
177
- const setColumnCalcOp = React.useCallback((columnKey, op)=>{
178
- setColumnCalcOps((current = EMPTY_OPS)=>{
179
- if (op === undefined) {
180
- if (!(columnKey in current)) return current;
181
- const next = {
182
- ...current
183
- };
184
- delete next[columnKey];
185
- return next;
186
- }
187
- return {
188
- ...current,
189
- [columnKey]: op
190
- };
191
- });
192
- }, [
193
- setColumnCalcOps
194
- ]);
195
- return {
196
- columnCalcOps,
197
- getColumnCalcOp,
198
- setColumnCalcOp
199
- };
200
- }
201
-
202
- exports.DATA_TABLE_CALC_LABEL = DATA_TABLE_CALC_LABEL;
203
- exports.DATA_TABLE_NUMERIC_CALC_OPS = DATA_TABLE_NUMERIC_CALC_OPS;
204
- exports.DATA_TABLE_UNIVERSAL_CALC_OPS = DATA_TABLE_UNIVERSAL_CALC_OPS;
205
- exports.computeColumnCalc = computeColumnCalc;
206
- exports.useControllableState = useControllableState;
207
- exports.useDataTableColumnCalc = useDataTableColumnCalc;
@@ -1,183 +0,0 @@
1
- 'use client';
2
- function __insertCSS(code) {
3
- if (!code || typeof document == 'undefined') return
4
- let head = document.head || document.getElementsByTagName('head')[0]
5
- let style = document.createElement('style')
6
- style.type = 'text/css'
7
- head.appendChild(style)
8
- ;style.styleSheet ? (style.styleSheet.cssText = code) : style.appendChild(document.createTextNode(code))
9
- }
10
-
11
- import * as React from 'react';
12
- import { useCallback } from 'react';
13
-
14
- // packages/react/use-layout-effect/src/use-layout-effect.tsx
15
- var useLayoutEffect2 = globalThis?.document ? React.useLayoutEffect : ()=>{};
16
-
17
- // src/use-controllable-state.tsx
18
- var useInsertionEffect = React[" useInsertionEffect ".trim().toString()] || useLayoutEffect2;
19
- function useControllableState({ prop, defaultProp, onChange = ()=>{}, caller }) {
20
- const [uncontrolledProp, setUncontrolledProp, onChangeRef] = useUncontrolledState({
21
- defaultProp,
22
- onChange
23
- });
24
- const isControlled = prop !== void 0;
25
- const value = isControlled ? prop : uncontrolledProp;
26
- {
27
- const isControlledRef = React.useRef(prop !== void 0);
28
- React.useEffect(()=>{
29
- const wasControlled = isControlledRef.current;
30
- if (wasControlled !== isControlled) {
31
- const from = wasControlled ? "controlled" : "uncontrolled";
32
- const to = isControlled ? "controlled" : "uncontrolled";
33
- console.warn(`${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`);
34
- }
35
- isControlledRef.current = isControlled;
36
- }, [
37
- isControlled,
38
- caller
39
- ]);
40
- }
41
- const setValue = React.useCallback((nextValue)=>{
42
- if (isControlled) {
43
- const value2 = isFunction(nextValue) ? nextValue(prop) : nextValue;
44
- if (value2 !== prop) {
45
- onChangeRef.current?.(value2);
46
- }
47
- } else {
48
- setUncontrolledProp(nextValue);
49
- }
50
- }, [
51
- isControlled,
52
- prop,
53
- setUncontrolledProp,
54
- onChangeRef
55
- ]);
56
- return [
57
- value,
58
- setValue
59
- ];
60
- }
61
- function useUncontrolledState({ defaultProp, onChange }) {
62
- const [value, setValue] = React.useState(defaultProp);
63
- const prevValueRef = React.useRef(value);
64
- const onChangeRef = React.useRef(onChange);
65
- useInsertionEffect(()=>{
66
- onChangeRef.current = onChange;
67
- }, [
68
- onChange
69
- ]);
70
- React.useEffect(()=>{
71
- if (prevValueRef.current !== value) {
72
- onChangeRef.current?.(value);
73
- prevValueRef.current = value;
74
- }
75
- }, [
76
- value,
77
- prevValueRef
78
- ]);
79
- return [
80
- value,
81
- setValue,
82
- onChangeRef
83
- ];
84
- }
85
- function isFunction(value) {
86
- return typeof value === "function";
87
- }
88
-
89
- const DATA_TABLE_UNIVERSAL_CALC_OPS = [
90
- "count",
91
- "countFilled"
92
- ];
93
- const DATA_TABLE_NUMERIC_CALC_OPS = [
94
- "sum",
95
- "average",
96
- "min",
97
- "max"
98
- ];
99
- const DATA_TABLE_CALC_LABEL = {
100
- count: "Count",
101
- countFilled: "Count filled",
102
- sum: "Sum",
103
- average: "Average",
104
- min: "Min",
105
- max: "Max"
106
- };
107
- /** A value counts as empty when it is null/undefined, an empty string, or an empty
108
- * array -- crisp's own `isEmptyValue` (columncalc.tsx), reused verbatim. */ function isEmptyValueForCalc(value) {
109
- if (value == null) return true;
110
- if (typeof value === "string") return value.trim() === "";
111
- if (Array.isArray(value)) return value.length === 0;
112
- return false;
113
- }
114
- /**
115
- * Pure so the arithmetic is independently (and mutation-) testable without rendering a
116
- * table at all. `sum`/`average`/`min`/`max` skip empty values and non-numeric ones
117
- * (coerced via `Number`, dropping `NaN`); they return `null` -- render nothing, not a
118
- * misleading 0 -- when there is nothing numeric left to reduce over.
119
- */ function computeColumnCalc(values, op) {
120
- switch(op){
121
- case "count":
122
- return values.length;
123
- case "countFilled":
124
- return values.filter((value)=>!isEmptyValueForCalc(value)).length;
125
- }
126
- const numbers = values.filter((value)=>!isEmptyValueForCalc(value)).map((value)=>typeof value === "number" ? value : Number(value)).filter((value)=>Number.isFinite(value));
127
- if (numbers.length === 0) return null;
128
- switch(op){
129
- case "sum":
130
- return numbers.reduce((total, value)=>total + value, 0);
131
- case "average":
132
- return numbers.reduce((total, value)=>total + value, 0) / numbers.length;
133
- case "min":
134
- return Math.min(...numbers);
135
- case "max":
136
- return Math.max(...numbers);
137
- default:
138
- return null;
139
- }
140
- }
141
- const EMPTY_OPS = {};
142
- // Owns which aggregation (if any) each column's ColumnCalc footer cell currently
143
- // shows -- the way useDataTableFilter owns one filter per column key. The actual
144
- // REDUCE over row values needs `TRow` (a type this headless package never sees), so
145
- // -- same split as sort/search/filter -- this hook only tracks WHICH op is active per
146
- // column; the styled layer (DataTable.tsx's `ColumnCalcCell`) calls `computeColumnCalc`
147
- // with the column's own resolved values.
148
- function useDataTableColumnCalc(props) {
149
- const { columnCalcOps: opsProp, defaultColumnCalcOps = EMPTY_OPS, onColumnCalcOpsChange } = props;
150
- const [columnCalcOps = EMPTY_OPS, setColumnCalcOps] = useControllableState({
151
- prop: opsProp,
152
- defaultProp: defaultColumnCalcOps,
153
- onChange: onColumnCalcOpsChange
154
- });
155
- const getColumnCalcOp = useCallback((columnKey)=>columnCalcOps[columnKey], [
156
- columnCalcOps
157
- ]);
158
- const setColumnCalcOp = useCallback((columnKey, op)=>{
159
- setColumnCalcOps((current = EMPTY_OPS)=>{
160
- if (op === undefined) {
161
- if (!(columnKey in current)) return current;
162
- const next = {
163
- ...current
164
- };
165
- delete next[columnKey];
166
- return next;
167
- }
168
- return {
169
- ...current,
170
- [columnKey]: op
171
- };
172
- });
173
- }, [
174
- setColumnCalcOps
175
- ]);
176
- return {
177
- columnCalcOps,
178
- getColumnCalcOp,
179
- setColumnCalcOp
180
- };
181
- }
182
-
183
- export { DATA_TABLE_CALC_LABEL as D, DATA_TABLE_UNIVERSAL_CALC_OPS as a, DATA_TABLE_NUMERIC_CALC_OPS as b, computeColumnCalc as c, useControllableState as d, useDataTableColumnCalc as u };
@@ -1,289 +0,0 @@
1
- 'use client';
2
- function __insertCSS(code) {
3
- if (!code || typeof document == 'undefined') return
4
- let head = document.head || document.getElementsByTagName('head')[0]
5
- let style = document.createElement('style')
6
- style.type = 'text/css'
7
- head.appendChild(style)
8
- ;style.styleSheet ? (style.styleSheet.cssText = code) : style.appendChild(document.createTextNode(code))
9
- }
10
-
11
- import { d as useControllableState } from './useDataTableColumnCalc-12s-DTQQTtEJ-12s-DsEse3uq.js';
12
- import { dataAttr } from '@seed-design/dom-utils';
13
- import { useMemo, useCallback, useState, useRef } from 'react';
14
-
15
- // Select's `dom.ts` reads state off REAL rendered elements (aria-disabled, data-value)
16
- // because its options register asynchronously through a floating-ui list and the DOM
17
- // is the one thing always in sync with `elementsRef`. DataTable's selection has no
18
- // such registry -- it is two plain id arrays (`rowIds`, `selectedIds`) the root already
19
- // holds -- so there is nothing to inspect on an element. What's isolated here instead is
20
- // the same KIND of thing Select isolates: small pure functions the state hooks would
21
- // otherwise inline, kept out of the hooks so they can be unit-tested (and mutation-
22
- // tested) on their own.
23
- /**
24
- * Tri-state selection summary of `selectedIds` against the CURRENTLY RENDERED
25
- * `rowIds` -- never against `selectedIds` alone, so a stale id that scrolled out of a
26
- * filtered/sorted view can't quietly count toward "all" or "some".
27
- */ function computeSelectionState(rowIds, selectedIds) {
28
- if (rowIds.length === 0) return "none";
29
- const selectedIdSet = new Set(selectedIds);
30
- const selectedRowCount = rowIds.filter((id)=>selectedIdSet.has(id)).length;
31
- if (selectedRowCount === 0) return "none";
32
- return selectedRowCount === rowIds.length ? "all" : "some";
33
- }
34
- /** Adds `id` if absent, removes it if present. Never mutates `ids`. */ function toggleMembership(ids, id) {
35
- return ids.includes(id) ? ids.filter((existing)=>existing !== id) : [
36
- ...ids,
37
- id
38
- ];
39
- }
40
- /**
41
- * Reconciles a persisted/controlled column ORDER against the table's current column
42
- * KEYS -- used by useDataTableColumns so a consumer's `columnOrder` never silently drops
43
- * a column the table actually renders (a key added to `columns` after the order was
44
- * first captured) nor keeps a ghost entry for one that was removed. Known keys keep
45
- * their position from `order`; any key in `keys` missing from `order` is appended, in
46
- * `keys`' own order; any entry in `order` no longer in `keys` is dropped.
47
- */ function reconcileColumnOrder(order, keys) {
48
- const keySet = new Set(keys);
49
- const known = order.filter((key)=>keySet.has(key));
50
- const knownSet = new Set(known);
51
- const added = keys.filter((key)=>!knownSet.has(key));
52
- return [
53
- ...known,
54
- ...added
55
- ];
56
- }
57
- /**
58
- * Moves `key` one step toward `direction` in `order` -- a no-op (same array, well,
59
- * an equal-valued copy) at the edge it would otherwise walk off. Column-menu's
60
- * "Move left"/"Move right" (DATATABLE-ATTIO-SPEC.md's column menu) is one call of
61
- * this per click; drag-to-reorder is `reorderColumn` below instead, which can jump
62
- * more than one position in a single move.
63
- */ function moveColumnKey(order, key, direction) {
64
- const index = order.indexOf(key);
65
- if (index === -1) return [
66
- ...order
67
- ];
68
- const targetIndex = direction === "left" ? index - 1 : index + 1;
69
- if (targetIndex < 0 || targetIndex >= order.length) return [
70
- ...order
71
- ];
72
- const next = [
73
- ...order
74
- ];
75
- [next[index], next[targetIndex]] = [
76
- next[targetIndex],
77
- next[index]
78
- ];
79
- return next;
80
- }
81
- /**
82
- * Drag-to-reorder: removes `dragKey` from `order` and reinserts it immediately before
83
- * `dropKey`'s CURRENT position (i.e. its position after the removal, so dropping a
84
- * column just after its own old slot doesn't appear to do nothing). A no-op (an
85
- * equal-valued copy) when either key is missing or they're the same key.
86
- */ function reorderColumnKey(order, dragKey, dropKey) {
87
- if (dragKey === dropKey || !order.includes(dragKey) || !order.includes(dropKey)) {
88
- return [
89
- ...order
90
- ];
91
- }
92
- const withoutDrag = order.filter((key)=>key !== dragKey);
93
- const dropIndex = withoutDrag.indexOf(dropKey);
94
- return [
95
- ...withoutDrag.slice(0, dropIndex),
96
- dragKey,
97
- ...withoutDrag.slice(dropIndex)
98
- ];
99
- }
100
- /** Clamps a drag-resized column width to a sane range -- crisp's own datatable.tsx
101
- * resize handler clamps the same way (a column can neither collapse to unreadable
102
- * nor balloon past a sane cap from one fast mouse move). */ function clampColumnWidth(width, min = 80, max = 640) {
103
- return Math.min(max, Math.max(min, width));
104
- }
105
- /**
106
- * Infinite scroll's own gate: fire `onLoadMore` only when the sentinel is ACTUALLY
107
- * intersecting (not e.g. an observer's initial `false` callback, which fires once on
108
- * `observe()` before any real intersection), there IS a next page (`hasMore`), and no
109
- * fetch is already in flight (`loadingMore`) -- either of the latter two alone must
110
- * suppress a trigger, which is why this is a pure `&&` rather than three separate
111
- * call sites that could each drift. Isolated the same way `computeSelectionState`/
112
- * `toggleMembership` are: the real IntersectionObserver callback (useDataTable
113
- * InfiniteScroll.ts) is unit-testable only through a live browser API, but the
114
- * DECISION it makes on every callback is plain data in, boolean out.
115
- */ function shouldTriggerLoadMore(isIntersecting, hasMore, loadingMore) {
116
- return hasMore && !loadingMore;
117
- }
118
- const EMPTY_STRINGS = [];
119
- const EMPTY_RECORD = {};
120
- /**
121
- * Owns everything DATATABLE-ATTIO-SPEC.md's column menu + reorder + resize tranche
122
- * needs -- order, visibility, a per-column label override, and a per-column width --
123
- * the way useDataTableSelection owns the one selected-id SET rather than one hook per
124
- * row. All four are "one value per column key" maps/lists a consumer may want to
125
- * persist (a saved view), so they share this hook instead of splitting into four
126
- * separate ones; drag-to-reorder and drag-to-resize (below) are the two INTERACTIONS
127
- * built on top of the order/width state this hook already owns, not independent state
128
- * of their own.
129
- */ function useDataTableColumns(props) {
130
- const { columnKeys = EMPTY_STRINGS, columnOrder: columnOrderProp, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys: hiddenProp, defaultHiddenColumnKeys = EMPTY_STRINGS, onHiddenColumnKeysChange, columnWidths: widthsProp, defaultColumnWidths = EMPTY_RECORD, onColumnWidthsChange, columnLabels: labelsProp, defaultColumnLabels = EMPTY_RECORD, onColumnLabelsChange } = props;
131
- const [rawOrder, setRawOrder] = useControllableState({
132
- prop: columnOrderProp,
133
- defaultProp: defaultColumnOrder ?? columnKeys,
134
- onChange: onColumnOrderChange
135
- });
136
- // Reconciled EVERY render against the table's current `columnKeys` (dom.ts's
137
- // `reconcileColumnOrder`), not just once at mount -- a controlled `columnOrder` a
138
- // consumer persisted before a new column existed must still show that new column
139
- // (appended), and one holding a column that got removed from `columns` must not
140
- // render a ghost header for it.
141
- const columnOrder = useMemo(()=>reconcileColumnOrder(rawOrder ?? columnKeys, columnKeys), [
142
- rawOrder,
143
- columnKeys
144
- ]);
145
- const [hiddenColumnKeys = EMPTY_STRINGS, setHiddenColumnKeys] = useControllableState({
146
- prop: hiddenProp,
147
- defaultProp: defaultHiddenColumnKeys,
148
- onChange: onHiddenColumnKeysChange
149
- });
150
- const hiddenSet = useMemo(()=>new Set(hiddenColumnKeys), [
151
- hiddenColumnKeys
152
- ]);
153
- const [columnWidths = EMPTY_RECORD, setColumnWidths] = useControllableState({
154
- prop: widthsProp,
155
- defaultProp: defaultColumnWidths,
156
- onChange: onColumnWidthsChange
157
- });
158
- const [columnLabels = EMPTY_RECORD, setColumnLabels] = useControllableState({
159
- prop: labelsProp,
160
- defaultProp: defaultColumnLabels,
161
- onChange: onColumnLabelsChange
162
- });
163
- const visibleColumnOrder = useMemo(()=>columnOrder.filter((key)=>!hiddenSet.has(key)), [
164
- columnOrder,
165
- hiddenSet
166
- ]);
167
- const isColumnHidden = useCallback((key)=>hiddenSet.has(key), [
168
- hiddenSet
169
- ]);
170
- const hideColumn = useCallback((key)=>setHiddenColumnKeys((current = EMPTY_STRINGS)=>[
171
- ...current,
172
- key
173
- ]), [
174
- setHiddenColumnKeys
175
- ]);
176
- const showColumn = useCallback((key)=>setHiddenColumnKeys((current = EMPTY_STRINGS)=>current.filter((existing)=>existing !== key)), [
177
- setHiddenColumnKeys
178
- ]);
179
- const toggleColumnVisibility = useCallback((key)=>isColumnHidden(key) ? showColumn(key) : hideColumn(key), [
180
- isColumnHidden,
181
- showColumn,
182
- hideColumn
183
- ]);
184
- const getColumnLabel = useCallback((key, fallback)=>columnLabels[key] ?? fallback, [
185
- columnLabels
186
- ]);
187
- const renameColumn = useCallback((key, label)=>setColumnLabels((current = EMPTY_RECORD)=>({
188
- ...current,
189
- [key]: label
190
- })), [
191
- setColumnLabels
192
- ]);
193
- const getColumnWidth = useCallback((key)=>columnWidths[key], [
194
- columnWidths
195
- ]);
196
- const setColumnWidth = useCallback((key, width)=>setColumnWidths((current = EMPTY_RECORD)=>({
197
- ...current,
198
- [key]: clampColumnWidth(width)
199
- })), [
200
- setColumnWidths
201
- ]);
202
- const moveColumn = useCallback((key, direction)=>setRawOrder((current)=>moveColumnKey(current ?? columnKeys, key, direction)), [
203
- setRawOrder,
204
- columnKeys
205
- ]);
206
- const reorderColumn = useCallback((dragKey, dropKey)=>setRawOrder((current)=>reorderColumnKey(current ?? columnKeys, dragKey, dropKey)), [
207
- setRawOrder,
208
- columnKeys
209
- ]);
210
- // -- drag-to-reorder ---------------------------------------------------------------
211
- // Live state only (which column is currently being dragged) -- the styled layer
212
- // drives it by calling `dragOverColumn` as the pointer crosses OTHER header cells
213
- // (via `document.elementFromPoint`, since a captured pointer stops native
214
- // `pointerenter` from firing on siblings), which reorders immediately rather than
215
- // waiting for a drop. That is exactly what Attio's own header drag does (the column
216
- // being passed visibly swaps live), and it means `endColumnDrag` never has to also
217
- // commit a move -- there is nothing left to commit.
218
- const [draggingColumnKey, setDraggingColumnKey] = useState(null);
219
- const beginColumnDrag = useCallback((key)=>setDraggingColumnKey(key), []);
220
- const dragOverColumn = useCallback((hoverKey)=>{
221
- setDraggingColumnKey((current)=>{
222
- if (!current || current === hoverKey) return current;
223
- reorderColumn(current, hoverKey);
224
- return current;
225
- });
226
- }, [
227
- reorderColumn
228
- ]);
229
- const endColumnDrag = useCallback(()=>setDraggingColumnKey(null), []);
230
- // -- drag-to-resize ------------------------------------------------------------------
231
- // `resizeStartRef` holds the ONE thing that must survive across pointermove events
232
- // without triggering a re-render on every pixel (the drag's origin) -- state
233
- // (`resizingColumnKey`) is only for what the render actually needs: which column's
234
- // handle should show its active/dragging look.
235
- const resizeStartRef = useRef(null);
236
- const [resizingColumnKey, setResizingColumnKey] = useState(null);
237
- const beginColumnResize = useCallback((key, clientX, currentWidth)=>{
238
- resizeStartRef.current = {
239
- key,
240
- startX: clientX,
241
- startWidth: currentWidth
242
- };
243
- setResizingColumnKey(key);
244
- }, []);
245
- const updateColumnResize = useCallback((clientX)=>{
246
- const start = resizeStartRef.current;
247
- if (!start) return;
248
- setColumnWidth(start.key, start.startWidth + (clientX - start.startX));
249
- }, [
250
- setColumnWidth
251
- ]);
252
- const endColumnResize = useCallback(()=>{
253
- resizeStartRef.current = null;
254
- setResizingColumnKey(null);
255
- }, []);
256
- return {
257
- columnOrder,
258
- visibleColumnOrder,
259
- hiddenColumnKeys,
260
- columnWidths,
261
- columnLabels,
262
- isColumnHidden,
263
- hideColumn,
264
- showColumn,
265
- toggleColumnVisibility,
266
- getColumnLabel,
267
- renameColumn,
268
- getColumnWidth,
269
- setColumnWidth,
270
- moveColumn,
271
- reorderColumn,
272
- draggingColumnKey,
273
- beginColumnDrag,
274
- dragOverColumn,
275
- endColumnDrag,
276
- resizingColumnKey,
277
- beginColumnResize,
278
- updateColumnResize,
279
- endColumnResize,
280
- // Exposed for the styled layer's header cell -- `data-dragging`/`data-resizing`
281
- // read directly off state above already cover most of it; `dataAttr` re-exported
282
- // via this hook's own module keeps every other sub-hook's habit of building its
283
- // own data-attribute values rather than the styled layer re-deriving booleans.
284
- isColumnDragging: (key)=>dataAttr(draggingColumnKey === key),
285
- isColumnResizing: (key)=>dataAttr(resizingColumnKey === key)
286
- };
287
- }
288
-
289
- export { computeSelectionState as c, shouldTriggerLoadMore as s, toggleMembership as t, useDataTableColumns as u };