@crispy-seed/data-table 0.1.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.
Files changed (56) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +30 -0
  3. package/README.md +103 -0
  4. package/dist/DataTable-12s-8LMBtS2u.cjs +1532 -0
  5. package/dist/DataTable-12s-Bd20OuMs.js +1482 -0
  6. package/dist/DataTable-12s-Zml1x9Kr-12s-CTRzgvMT.cjs +161 -0
  7. package/dist/DataTable-12s-Zml1x9Kr-12s-Coi-3lUa.js +153 -0
  8. package/dist/Menu-12s-Db_zr1AP.cjs +104 -0
  9. package/dist/Menu-12s-Di5JADW4.js +78 -0
  10. package/dist/Select-12s-1Uh7XbkG.js +219 -0
  11. package/dist/Select-12s-D8DdKT-U.cjs +250 -0
  12. package/dist/body-cells-12s-BnOVUTKN.js +453 -0
  13. package/dist/body-cells-12s-BoOTcg-l.cjs +462 -0
  14. package/dist/column-calc-12s-C6IPSOaF.js +129 -0
  15. package/dist/column-calc-12s-CKuSpe-l.cjs +149 -0
  16. package/dist/column-layout-12s-CTu5yT_D.cjs +120 -0
  17. package/dist/column-layout-12s-s7wnxkMN.js +99 -0
  18. package/dist/column-menu-12s-BPE9N1gs.cjs +427 -0
  19. package/dist/column-menu-12s-mRBk-4oa.js +407 -0
  20. package/dist/data-table-recipe.css +442 -0
  21. package/dist/data-table-recipe.layered.css +505 -0
  22. package/dist/editors-12s-BO9Mp034.js +525 -0
  23. package/dist/editors-12s-DO8TZkYV.cjs +555 -0
  24. package/dist/index.cjs +14 -0
  25. package/dist/index.d.ts +323 -0
  26. package/dist/index.js +10 -0
  27. package/dist/resize-12s-jQY-PX67.cjs +58 -0
  28. package/dist/resize-12s-nsMvrmLt.js +58 -0
  29. package/dist/states-12s-CLO_uVue.cjs +125 -0
  30. package/dist/states-12s-DEFSxJCO.js +122 -0
  31. package/dist/styles.css +14660 -0
  32. package/dist/toolbar-12s-AVU-sbDW.js +69 -0
  33. package/dist/toolbar-12s-CaLgJrDo.cjs +71 -0
  34. package/dist/useDataTable-12s-BHiDUubw-12s-CBeCwTe7.js +61 -0
  35. package/dist/useDataTable-12s-BHiDUubw-12s-Dgvt7XDk.cjs +61 -0
  36. package/dist/useDataTableColumnCalc-12s-DTQQTtEJ-12s-CvD14JYo.cjs +207 -0
  37. package/dist/useDataTableColumnCalc-12s-DTQQTtEJ-12s-DsEse3uq.js +183 -0
  38. package/dist/useDataTableColumns-12s-Cn2ZAzFL-12s-CD6goR59.js +289 -0
  39. package/dist/useDataTableColumns-12s-Cn2ZAzFL-12s-CUk_CpYT.cjs +292 -0
  40. package/dist/useDataTableEdit-12s-9Wb_UCrN-12s-BoOHq3GB.js +98 -0
  41. package/dist/useDataTableEdit-12s-9Wb_UCrN-12s-De6YHBba.cjs +98 -0
  42. package/dist/useDataTableFilter-12s-ZzbmWMin-12s-CWyBeFna.js +74 -0
  43. package/dist/useDataTableFilter-12s-ZzbmWMin-12s-lpjzZY3_.cjs +74 -0
  44. package/dist/useDataTableInfiniteScroll-12s-8ZG31URd-12s-BexjP6iO.js +104 -0
  45. package/dist/useDataTableInfiniteScroll-12s-8ZG31URd-12s-Bt8gojr9.cjs +104 -0
  46. package/dist/useDataTableRow-12s-CPYaDa1p-12s-BmUSAnPe.js +30 -0
  47. package/dist/useDataTableRow-12s-CPYaDa1p-12s-D-DRjwAe.cjs +30 -0
  48. package/dist/useDataTableScroll-12s-BKDCr0en-12s-C7f2vHeG.js +61 -0
  49. package/dist/useDataTableScroll-12s-BKDCr0en-12s-CUvUYMtt.cjs +61 -0
  50. package/dist/useDataTableSearch-12s-Ck3iWcpX-12s-B-65V6gH.js +37 -0
  51. package/dist/useDataTableSearch-12s-Ck3iWcpX-12s-DhVfN2Hq.cjs +37 -0
  52. package/dist/useDataTableSelection-12s-BIDtiRQq-12s-IS__VAnU.cjs +71 -0
  53. package/dist/useDataTableSelection-12s-BIDtiRQq-12s-O6M_BhrH.js +71 -0
  54. package/dist/useDataTableSort-12s-DvJYtmeS-12s-C5VGnsq4.cjs +78 -0
  55. package/dist/useDataTableSort-12s-DvJYtmeS-12s-DABSNt6U.js +78 -0
  56. package/package.json +78 -0
@@ -0,0 +1,289 @@
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 };
@@ -0,0 +1,292 @@
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 useDataTableColumnCalc12sDTQQTtEJ12s = require('./useDataTableColumnCalc-12s-DTQQTtEJ-12s-CvD14JYo.cjs');
12
+ var domUtils = require('@seed-design/dom-utils');
13
+ var React = require('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] = useDataTableColumnCalc12sDTQQTtEJ12s.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 = React.useMemo(()=>reconcileColumnOrder(rawOrder ?? columnKeys, columnKeys), [
142
+ rawOrder,
143
+ columnKeys
144
+ ]);
145
+ const [hiddenColumnKeys = EMPTY_STRINGS, setHiddenColumnKeys] = useDataTableColumnCalc12sDTQQTtEJ12s.useControllableState({
146
+ prop: hiddenProp,
147
+ defaultProp: defaultHiddenColumnKeys,
148
+ onChange: onHiddenColumnKeysChange
149
+ });
150
+ const hiddenSet = React.useMemo(()=>new Set(hiddenColumnKeys), [
151
+ hiddenColumnKeys
152
+ ]);
153
+ const [columnWidths = EMPTY_RECORD, setColumnWidths] = useDataTableColumnCalc12sDTQQTtEJ12s.useControllableState({
154
+ prop: widthsProp,
155
+ defaultProp: defaultColumnWidths,
156
+ onChange: onColumnWidthsChange
157
+ });
158
+ const [columnLabels = EMPTY_RECORD, setColumnLabels] = useDataTableColumnCalc12sDTQQTtEJ12s.useControllableState({
159
+ prop: labelsProp,
160
+ defaultProp: defaultColumnLabels,
161
+ onChange: onColumnLabelsChange
162
+ });
163
+ const visibleColumnOrder = React.useMemo(()=>columnOrder.filter((key)=>!hiddenSet.has(key)), [
164
+ columnOrder,
165
+ hiddenSet
166
+ ]);
167
+ const isColumnHidden = React.useCallback((key)=>hiddenSet.has(key), [
168
+ hiddenSet
169
+ ]);
170
+ const hideColumn = React.useCallback((key)=>setHiddenColumnKeys((current = EMPTY_STRINGS)=>[
171
+ ...current,
172
+ key
173
+ ]), [
174
+ setHiddenColumnKeys
175
+ ]);
176
+ const showColumn = React.useCallback((key)=>setHiddenColumnKeys((current = EMPTY_STRINGS)=>current.filter((existing)=>existing !== key)), [
177
+ setHiddenColumnKeys
178
+ ]);
179
+ const toggleColumnVisibility = React.useCallback((key)=>isColumnHidden(key) ? showColumn(key) : hideColumn(key), [
180
+ isColumnHidden,
181
+ showColumn,
182
+ hideColumn
183
+ ]);
184
+ const getColumnLabel = React.useCallback((key, fallback)=>columnLabels[key] ?? fallback, [
185
+ columnLabels
186
+ ]);
187
+ const renameColumn = React.useCallback((key, label)=>setColumnLabels((current = EMPTY_RECORD)=>({
188
+ ...current,
189
+ [key]: label
190
+ })), [
191
+ setColumnLabels
192
+ ]);
193
+ const getColumnWidth = React.useCallback((key)=>columnWidths[key], [
194
+ columnWidths
195
+ ]);
196
+ const setColumnWidth = React.useCallback((key, width)=>setColumnWidths((current = EMPTY_RECORD)=>({
197
+ ...current,
198
+ [key]: clampColumnWidth(width)
199
+ })), [
200
+ setColumnWidths
201
+ ]);
202
+ const moveColumn = React.useCallback((key, direction)=>setRawOrder((current)=>moveColumnKey(current ?? columnKeys, key, direction)), [
203
+ setRawOrder,
204
+ columnKeys
205
+ ]);
206
+ const reorderColumn = React.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] = React.useState(null);
219
+ const beginColumnDrag = React.useCallback((key)=>setDraggingColumnKey(key), []);
220
+ const dragOverColumn = React.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 = React.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 = React.useRef(null);
236
+ const [resizingColumnKey, setResizingColumnKey] = React.useState(null);
237
+ const beginColumnResize = React.useCallback((key, clientX, currentWidth)=>{
238
+ resizeStartRef.current = {
239
+ key,
240
+ startX: clientX,
241
+ startWidth: currentWidth
242
+ };
243
+ setResizingColumnKey(key);
244
+ }, []);
245
+ const updateColumnResize = React.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 = React.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)=>domUtils.dataAttr(draggingColumnKey === key),
285
+ isColumnResizing: (key)=>domUtils.dataAttr(resizingColumnKey === key)
286
+ };
287
+ }
288
+
289
+ exports.computeSelectionState = computeSelectionState;
290
+ exports.shouldTriggerLoadMore = shouldTriggerLoadMore;
291
+ exports.toggleMembership = toggleMembership;
292
+ exports.useDataTableColumns = useDataTableColumns;
@@ -0,0 +1,98 @@
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 { useState, useCallback } from 'react';
12
+
13
+ function sameCell(a, id, key) {
14
+ return a !== null && a.id === id && a.key === key;
15
+ }
16
+ /**
17
+ * Owns which single cell is mid-edit, which one (if any) has a commit in flight, and
18
+ * which one last failed -- the way useDataTableColumnCalc owns which op is active per
19
+ * column. This hook never touches `TRow` or renders an editor itself; it only tracks
20
+ * WHICH cell is in which of those three states and orchestrates the async commit
21
+ * (crisp's own `commitEdit`, `datatable.tsx`) -- the styled layer (react/DataTable's
22
+ * `editors.tsx`) reads `isEditing`/`isPending`/`getEditError` to decide what to render
23
+ * for a given `(id, key)` and calls `startEdit`/`cancelEdit`/`commitEdit` in response
24
+ * to a double-click, Escape, Enter/blur, or an always-live widget's own change event.
25
+ *
26
+ * Only one cell can be mid-edit at a time (`editingCell`, not a set) -- matching
27
+ * crisp's own single `editing` state and this table's spec (no multi-cell/range
28
+ * editing is in scope here).
29
+ */ function useDataTableEdit(props) {
30
+ const { onEdit } = props;
31
+ const [editingCell, setEditingCell] = useState(null);
32
+ const [pendingCell, setPendingCell] = useState(null);
33
+ const [editError, setEditError] = useState(null);
34
+ const isEditing = useCallback((id, key)=>sameCell(editingCell, id, key), [
35
+ editingCell
36
+ ]);
37
+ const isPending = useCallback((id, key)=>sameCell(pendingCell, id, key), [
38
+ pendingCell
39
+ ]);
40
+ const getEditError = useCallback((id, key)=>editError && editError.id === id && editError.key === key ? editError.message : null, [
41
+ editError
42
+ ]);
43
+ const startEdit = useCallback((id, key)=>{
44
+ // Opening a cell's editor also clears any stale error left over from a PREVIOUS
45
+ // failed commit on it (crisp/#385 comment) -- otherwise a rejection from three
46
+ // edits ago would still read under a freshly-reopened editor.
47
+ setEditError(null);
48
+ setEditingCell({
49
+ id,
50
+ key
51
+ });
52
+ }, []);
53
+ const cancelEdit = useCallback(()=>setEditingCell(null), []);
54
+ // Async-aware (crisp/#385): a sync `onEdit` (returns undefined) commits exactly as
55
+ // before -- close the editor immediately. A Promise-returning `onEdit` keeps the
56
+ // editor open with a pending state while it settles, then either closes it
57
+ // (resolve) or closes it AND reverts to the last-known-good value while showing the
58
+ // rejection's message beside the cell (reject). The revert needs no extra code:
59
+ // neither this hook nor the styled layer ever mutates the caller's own row data, so
60
+ // once `editingCell` clears, the cell simply shows that data again.
61
+ const commitEdit = useCallback((id, key, value)=>{
62
+ setEditError(null);
63
+ const result = onEdit?.(id, key, value);
64
+ if (result && typeof result.then === "function") {
65
+ setPendingCell({
66
+ id,
67
+ key
68
+ });
69
+ result.then(()=>{
70
+ setPendingCell(null);
71
+ setEditingCell(null);
72
+ }, (err)=>{
73
+ setPendingCell(null);
74
+ setEditingCell(null);
75
+ setEditError({
76
+ id,
77
+ key,
78
+ message: err instanceof Error ? err.message : typeof err === "string" ? err : "Could not save"
79
+ });
80
+ });
81
+ return;
82
+ }
83
+ setEditingCell(null);
84
+ }, [
85
+ onEdit
86
+ ]);
87
+ return {
88
+ editingCell,
89
+ isEditing,
90
+ isPending,
91
+ getEditError,
92
+ startEdit,
93
+ cancelEdit,
94
+ commitEdit
95
+ };
96
+ }
97
+
98
+ export { useDataTableEdit as u };