@crispy-seed/data-table 0.6.0 → 0.8.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.
- package/dist/index.cjs +504 -153
- package/dist/index.d.ts +98 -39
- package/dist/index.js +504 -153
- package/dist/{record-panel-recipe.css → record-preview-recipe.css} +35 -35
- package/dist/{record-panel-recipe.layered.css → record-preview-recipe.layered.css} +35 -35
- package/dist/resizable-panel-recipe.css +62 -22
- package/dist/resizable-panel-recipe.layered.css +55 -21
- package/dist/styles.css +90 -56
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -750,21 +750,31 @@ function isFunction$1(value) {
|
|
|
750
750
|
|
|
751
751
|
const DATA_TABLE_UNIVERSAL_CALC_OPS = [
|
|
752
752
|
"count",
|
|
753
|
-
"
|
|
753
|
+
"empty",
|
|
754
|
+
"notEmpty"
|
|
754
755
|
];
|
|
755
756
|
const DATA_TABLE_NUMERIC_CALC_OPS = [
|
|
756
757
|
"sum",
|
|
757
758
|
"average",
|
|
759
|
+
"median",
|
|
758
760
|
"min",
|
|
759
|
-
"max"
|
|
761
|
+
"max",
|
|
762
|
+
"range"
|
|
763
|
+
];
|
|
764
|
+
/** Offered instead of `DATA_TABLE_NUMERIC_CALC_OPS` on a non-numeric column. */ const DATA_TABLE_NON_NUMERIC_CALC_OPS = [
|
|
765
|
+
"countUnique"
|
|
760
766
|
];
|
|
761
767
|
const DATA_TABLE_CALC_LABEL = {
|
|
762
768
|
count: "Count",
|
|
763
|
-
|
|
769
|
+
empty: "Empty",
|
|
770
|
+
notEmpty: "Not empty",
|
|
764
771
|
sum: "Sum",
|
|
765
772
|
average: "Average",
|
|
773
|
+
median: "Median",
|
|
766
774
|
min: "Min",
|
|
767
|
-
max: "Max"
|
|
775
|
+
max: "Max",
|
|
776
|
+
range: "Range",
|
|
777
|
+
countUnique: "Count unique"
|
|
768
778
|
};
|
|
769
779
|
/** A value counts as empty when it is null/undefined, an empty string, or an empty
|
|
770
780
|
* array -- crisp's own `isEmptyValue` (columncalc.tsx), reused verbatim. */ function isEmptyValueForCalc(value) {
|
|
@@ -773,29 +783,50 @@ const DATA_TABLE_CALC_LABEL = {
|
|
|
773
783
|
if (Array.isArray(value)) return value.length === 0;
|
|
774
784
|
return false;
|
|
775
785
|
}
|
|
786
|
+
/** The middle value of a SORTED, non-empty numeric array -- the mean of the two
|
|
787
|
+
* middle entries on an even-length array, same convention as every stats library. */ function median(sortedNumbers) {
|
|
788
|
+
const mid = Math.floor(sortedNumbers.length / 2);
|
|
789
|
+
return sortedNumbers.length % 2 === 0 ? (sortedNumbers[mid - 1] + sortedNumbers[mid]) / 2 : sortedNumbers[mid];
|
|
790
|
+
}
|
|
776
791
|
/**
|
|
777
792
|
* Pure so the arithmetic is independently (and mutation-) testable without rendering a
|
|
778
|
-
* table at all. `sum`/`average`/`min`/`max` skip
|
|
779
|
-
* (coerced via `Number`, dropping `NaN`); they
|
|
780
|
-
* misleading 0 -- when there is nothing
|
|
793
|
+
* table at all. The numeric ops (`sum`/`average`/`median`/`min`/`max`/`range`) skip
|
|
794
|
+
* empty values and non-numeric ones (coerced via `Number`, dropping `NaN`); they
|
|
795
|
+
* return `null` -- render nothing, not a misleading 0 -- when there is nothing
|
|
796
|
+
* numeric left to reduce over. `countUnique` counts distinct NON-EMPTY values
|
|
797
|
+
* (`JSON.stringify`-normalized, so two arrays/objects with the same shape count
|
|
798
|
+
* once) -- empties are already their own op (`empty`), so folding them into "one
|
|
799
|
+
* more unique value" here would double-count the same information two different
|
|
800
|
+
* ways in the same menu.
|
|
781
801
|
*/ function computeColumnCalc(values, op) {
|
|
782
802
|
switch(op){
|
|
783
803
|
case "count":
|
|
784
804
|
return values.length;
|
|
785
|
-
case "
|
|
805
|
+
case "empty":
|
|
806
|
+
return values.filter(isEmptyValueForCalc).length;
|
|
807
|
+
case "notEmpty":
|
|
786
808
|
return values.filter((value)=>!isEmptyValueForCalc(value)).length;
|
|
809
|
+
case "countUnique":
|
|
810
|
+
{
|
|
811
|
+
const seen = new Set(values.filter((value)=>!isEmptyValueForCalc(value)).map((value)=>JSON.stringify(value)));
|
|
812
|
+
return seen.size;
|
|
813
|
+
}
|
|
787
814
|
}
|
|
788
|
-
const numbers = values.filter((value)=>!isEmptyValueForCalc(value)).map((value)=>typeof value === "number" ? value : Number(value)).filter((value)=>Number.isFinite(value));
|
|
815
|
+
const numbers = values.filter((value)=>!isEmptyValueForCalc(value)).map((value)=>typeof value === "number" ? value : Number(value)).filter((value)=>Number.isFinite(value)).sort((a, b)=>a - b);
|
|
789
816
|
if (numbers.length === 0) return null;
|
|
790
817
|
switch(op){
|
|
791
818
|
case "sum":
|
|
792
819
|
return numbers.reduce((total, value)=>total + value, 0);
|
|
793
820
|
case "average":
|
|
794
821
|
return numbers.reduce((total, value)=>total + value, 0) / numbers.length;
|
|
822
|
+
case "median":
|
|
823
|
+
return median(numbers);
|
|
795
824
|
case "min":
|
|
796
|
-
return
|
|
825
|
+
return numbers[0];
|
|
797
826
|
case "max":
|
|
798
|
-
return
|
|
827
|
+
return numbers[numbers.length - 1];
|
|
828
|
+
case "range":
|
|
829
|
+
return numbers[numbers.length - 1] - numbers[0];
|
|
799
830
|
default:
|
|
800
831
|
return null;
|
|
801
832
|
}
|
|
@@ -1200,31 +1231,48 @@ const EMPTY_RECORD = {};
|
|
|
1200
1231
|
]);
|
|
1201
1232
|
const endColumnDrag = useCallback(()=>setDraggingColumnKey(null), []);
|
|
1202
1233
|
// -- drag-to-resize ------------------------------------------------------------------
|
|
1203
|
-
// `resizeStartRef` holds
|
|
1204
|
-
//
|
|
1205
|
-
//
|
|
1206
|
-
//
|
|
1234
|
+
// `resizeStartRef` holds everything that must survive across pointermove events
|
|
1235
|
+
// WITHOUT triggering a re-render on every pixel: the drag's origin, AND (as of the
|
|
1236
|
+
// live-review fix below) the latest computed width, so `endColumnResize` has
|
|
1237
|
+
// something to commit. State (`resizingColumnKey`) is only for what the render
|
|
1238
|
+
// actually needs: which column's handle should show its active/dragging look.
|
|
1239
|
+
//
|
|
1240
|
+
// `updateColumnResize` used to call `setColumnWidth` on every single pointermove --
|
|
1241
|
+
// a `setColumnWidths` state update (and therefore a full DataTable re-render) per
|
|
1242
|
+
// PIXEL of drag, confirmed live as the stutter a column-border drag produced. It now
|
|
1243
|
+
// only computes+returns the clamped width; the styled layer (`resize.tsx`) writes
|
|
1244
|
+
// that directly to the dragged column's own `<col>` element (`col.style.width`,
|
|
1245
|
+
// outside React), which is all a resize needs frame-to-frame -- table layout already
|
|
1246
|
+
// recalculates from a `<col>`'s own inline style with no re-render involved. React
|
|
1247
|
+
// state (and this hook's `onColumnWidthsChange` contract) is only touched ONCE, in
|
|
1248
|
+
// `endColumnResize`, committing the final width exactly as a controlled consumer
|
|
1249
|
+
// already expects.
|
|
1207
1250
|
const resizeStartRef = useRef(null);
|
|
1208
1251
|
const [resizingColumnKey, setResizingColumnKey] = useState(null);
|
|
1209
1252
|
const beginColumnResize = useCallback((key, clientX, currentWidth)=>{
|
|
1210
1253
|
resizeStartRef.current = {
|
|
1211
1254
|
key,
|
|
1212
1255
|
startX: clientX,
|
|
1213
|
-
startWidth: currentWidth
|
|
1256
|
+
startWidth: currentWidth,
|
|
1257
|
+
currentWidth
|
|
1214
1258
|
};
|
|
1215
1259
|
setResizingColumnKey(key);
|
|
1216
1260
|
}, []);
|
|
1217
1261
|
const updateColumnResize = useCallback((clientX)=>{
|
|
1218
1262
|
const start = resizeStartRef.current;
|
|
1219
|
-
if (!start) return;
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
]);
|
|
1263
|
+
if (!start) return undefined;
|
|
1264
|
+
const width = clampColumnWidth(start.startWidth + (clientX - start.startX));
|
|
1265
|
+
start.currentWidth = width;
|
|
1266
|
+
return width;
|
|
1267
|
+
}, []);
|
|
1224
1268
|
const endColumnResize = useCallback(()=>{
|
|
1269
|
+
const start = resizeStartRef.current;
|
|
1270
|
+
if (start) setColumnWidth(start.key, start.currentWidth);
|
|
1225
1271
|
resizeStartRef.current = null;
|
|
1226
1272
|
setResizingColumnKey(null);
|
|
1227
|
-
}, [
|
|
1273
|
+
}, [
|
|
1274
|
+
setColumnWidth
|
|
1275
|
+
]);
|
|
1228
1276
|
return {
|
|
1229
1277
|
columnOrder,
|
|
1230
1278
|
visibleColumnOrder,
|
|
@@ -1574,11 +1622,16 @@ function useDataTableSort(props) {
|
|
|
1574
1622
|
const [sort, setSort] = useControllableState$1({
|
|
1575
1623
|
prop: sortProp,
|
|
1576
1624
|
defaultProp: defaultSort,
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1625
|
+
// Live-review fix: this used to only fire for a DEFINED next sort ("`toggleSort`
|
|
1626
|
+
// never clears it, so why would this ever see `undefined`") -- true until
|
|
1627
|
+
// `clearSort` (below) existed. Guarding the callback on truthiness meant a
|
|
1628
|
+
// consumer controlling `sort` had no way to ever learn the column sort was
|
|
1629
|
+
// cleared: `setSort(undefined)` would run, but the controlled VALUE never moves
|
|
1630
|
+
// without the consumer's own state updating from this callback, and the guard
|
|
1631
|
+
// swallowed exactly the call that would have told it to. Every other value this
|
|
1632
|
+
// hook contract carries (`onColumnWidthsChange`, `onColumnCalcOpsChange`, ...)
|
|
1633
|
+
// already passes its "cleared" shape through unconditionally; this now matches.
|
|
1634
|
+
onChange: onSortChange
|
|
1582
1635
|
});
|
|
1583
1636
|
const toggleSort = useCallback((key)=>{
|
|
1584
1637
|
setSort((current)=>{
|
|
@@ -1594,6 +1647,12 @@ function useDataTableSort(props) {
|
|
|
1594
1647
|
}, [
|
|
1595
1648
|
setSort
|
|
1596
1649
|
]);
|
|
1650
|
+
/** Attio/GRC's "Remove sort" -- the header click cycle (above) only ever toggles
|
|
1651
|
+
* asc/desc once a column has been clicked once; this is the only way back to no
|
|
1652
|
+
* sort at all. Exposed for a column's own header menu (`column-menu.tsx`'s
|
|
1653
|
+
* `ColumnMenu`) to offer as a "Clear sort" item when that column is the sorted one. */ const clearSort = useCallback(()=>setSort(undefined), [
|
|
1654
|
+
setSort
|
|
1655
|
+
]);
|
|
1597
1656
|
const getHeaderCellProps = useCallback((columnKey, sortable)=>{
|
|
1598
1657
|
const isSorted = Boolean(columnKey) && sort?.key === columnKey;
|
|
1599
1658
|
const canSort = Boolean(sortable && columnKey);
|
|
@@ -1623,6 +1682,7 @@ function useDataTableSort(props) {
|
|
|
1623
1682
|
return {
|
|
1624
1683
|
sort,
|
|
1625
1684
|
toggleSort,
|
|
1685
|
+
clearSort,
|
|
1626
1686
|
getHeaderCellProps
|
|
1627
1687
|
};
|
|
1628
1688
|
}
|
|
@@ -4144,7 +4204,7 @@ function formatCalcValue(value) {
|
|
|
4144
4204
|
]);
|
|
4145
4205
|
const ops = [
|
|
4146
4206
|
...DATA_TABLE_UNIVERSAL_CALC_OPS,
|
|
4147
|
-
...numeric ? DATA_TABLE_NUMERIC_CALC_OPS :
|
|
4207
|
+
...numeric ? DATA_TABLE_NUMERIC_CALC_OPS : DATA_TABLE_NON_NUMERIC_CALC_OPS
|
|
4148
4208
|
];
|
|
4149
4209
|
return /*#__PURE__*/ jsxs(MenuRoot, {
|
|
4150
4210
|
children: [
|
|
@@ -4210,8 +4270,32 @@ function formatCalcValue(value) {
|
|
|
4210
4270
|
* once captured, this SAME element keeps receiving `pointermove`/`pointerup` even
|
|
4211
4271
|
* after the cursor leaves it, so the drag math can live entirely in this one
|
|
4212
4272
|
* handler set with no effect/cleanup to get wrong.
|
|
4273
|
+
*
|
|
4274
|
+
* Live-review fix (stutter): `updateColumnResize` used to also commit the new width
|
|
4275
|
+
* to React state on every `pointermove` -- a full DataTable re-render per pixel of
|
|
4276
|
+
* drag, confirmed as the stutter. During the drag this component now writes the
|
|
4277
|
+
* computed width straight to the dragged column's own `<col>` element (found once,
|
|
4278
|
+
* at pointerdown, via its `data-column-key`, per `column-layout`'s `<colgroup>`
|
|
4279
|
+
* comment: a `<col>`'s own inline `width` is the SINGLE place table layout reads a
|
|
4280
|
+
* column's width from, so writing it directly reflows the table with no React
|
|
4281
|
+
* render at all), coalesced to at most one write per animation frame so a fast
|
|
4282
|
+
* mouse doesn't queue more layout work than the screen can show. React state (and
|
|
4283
|
+
* a controlled consumer's `onColumnWidthsChange`) is only touched ONCE more, in
|
|
4284
|
+
* `endColumnResize`, which is unchanged: it commits whatever `updateColumnResize`
|
|
4285
|
+
* last computed.
|
|
4213
4286
|
*/ function ColumnResizeHandle({ columnKey, width }) {
|
|
4214
4287
|
const { beginColumnResize, updateColumnResize, endColumnResize, isColumnResizing } = useDataTableContext();
|
|
4288
|
+
// Survive across pointermove events without a re-render: the `<col>` this drag
|
|
4289
|
+
// writes to (resolved once, at pointerdown) and the rAF coalescing the writes.
|
|
4290
|
+
const colElementRef = useRef(null);
|
|
4291
|
+
const pendingWidthRef = useRef(null);
|
|
4292
|
+
const rafIdRef = useRef(null);
|
|
4293
|
+
const flushPendingWidth = ()=>{
|
|
4294
|
+
rafIdRef.current = null;
|
|
4295
|
+
const col = colElementRef.current;
|
|
4296
|
+
const pendingWidth = pendingWidthRef.current;
|
|
4297
|
+
if (col && pendingWidth != null) col.style.width = `${pendingWidth}px`;
|
|
4298
|
+
};
|
|
4215
4299
|
return /*#__PURE__*/ jsx("span", {
|
|
4216
4300
|
// Mouse/pointer-drag only for now (no keyboard equivalent yet) -- `aria-hidden`
|
|
4217
4301
|
// rather than `role="separator"` + `aria-label`, which would otherwise fold
|
|
@@ -4227,16 +4311,32 @@ function formatCalcValue(value) {
|
|
|
4227
4311
|
event.stopPropagation();
|
|
4228
4312
|
event.preventDefault();
|
|
4229
4313
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
4230
|
-
const
|
|
4314
|
+
const headerCell = event.currentTarget.closest('[role="columnheader"]');
|
|
4315
|
+
const startWidth = width ?? headerCell?.getBoundingClientRect().width ?? 150;
|
|
4316
|
+
colElementRef.current = event.currentTarget.closest("table")?.querySelector(`col[data-column-key="${CSS.escape(columnKey)}"]`) ?? null;
|
|
4231
4317
|
beginColumnResize(columnKey, event.clientX, startWidth);
|
|
4232
4318
|
},
|
|
4233
4319
|
onPointerMove: (event)=>{
|
|
4234
|
-
if (event.currentTarget.hasPointerCapture(event.pointerId))
|
|
4235
|
-
|
|
4320
|
+
if (!event.currentTarget.hasPointerCapture(event.pointerId)) return;
|
|
4321
|
+
const nextWidth = updateColumnResize(event.clientX);
|
|
4322
|
+
if (nextWidth == null) return;
|
|
4323
|
+
pendingWidthRef.current = nextWidth;
|
|
4324
|
+
if (rafIdRef.current == null) {
|
|
4325
|
+
rafIdRef.current = requestAnimationFrame(flushPendingWidth);
|
|
4236
4326
|
}
|
|
4237
4327
|
},
|
|
4238
4328
|
onPointerUp: (event)=>{
|
|
4239
4329
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
4330
|
+
if (rafIdRef.current != null) {
|
|
4331
|
+
cancelAnimationFrame(rafIdRef.current);
|
|
4332
|
+
rafIdRef.current = null;
|
|
4333
|
+
}
|
|
4334
|
+
// Apply any not-yet-flushed width immediately so the `<col>` matches the
|
|
4335
|
+
// exact value `endColumnResize` is about to commit to React state -- with
|
|
4336
|
+
// no more pointermoves coming, nothing else would flush it.
|
|
4337
|
+
flushPendingWidth();
|
|
4338
|
+
pendingWidthRef.current = null;
|
|
4339
|
+
colElementRef.current = null;
|
|
4240
4340
|
endColumnResize();
|
|
4241
4341
|
},
|
|
4242
4342
|
children: /*#__PURE__*/ jsx("span", {
|
|
@@ -4427,68 +4527,100 @@ function ColumnRenameInput({ column, initialLabel, onDone }) {
|
|
|
4427
4527
|
* permanently, so this one carries its own marker attribute distinct from that
|
|
4428
4528
|
* slot's OTHER occupants (the sort chevron, the filter trigger), which stay visible
|
|
4429
4529
|
* under their own separate rules regardless of hover. */ function ColumnMenu({ column, onRename }) {
|
|
4430
|
-
const { moveColumn, hideColumn, isColumnPinned, togglePinColumn } = useDataTableContext();
|
|
4530
|
+
const { moveColumn, hideColumn, isColumnPinned, togglePinColumn, sort, clearSort } = useDataTableContext();
|
|
4431
4531
|
const columnLabel = typeof column.header === "string" ? column.header : column.key;
|
|
4432
4532
|
const pinned = isColumnPinned(column.key);
|
|
4533
|
+
// Attio/GRC's "Remove sort" (live-review fix D) -- the header's own click-to-sort
|
|
4534
|
+
// (`SortIndicator`/`toggleSort`, above) only cycles asc/desc once a column has been
|
|
4535
|
+
// clicked; this is the only affordance back to no sort at all, so it only makes
|
|
4536
|
+
// sense to show on the column that IS currently sorted.
|
|
4537
|
+
const isSorted = sort?.key === column.key;
|
|
4433
4538
|
const stopPropagation = (event)=>event.stopPropagation();
|
|
4434
|
-
return
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4539
|
+
return(// Live-review fix (D): a `MenuItem` inside `MenuPositioner` renders through a
|
|
4540
|
+
// `FloatingPortal` -- a REAL React portal (`ReactDOM.createPortal`), which
|
|
4541
|
+
// bubbles a click/keydown up through the REACT TREE (this component, then the
|
|
4542
|
+
// columnheader that renders it), NOT the DOM tree the portal actually appends
|
|
4543
|
+
// to. Confirmed live: clicking ANY item here (not just "Clear sort" -- "Move
|
|
4544
|
+
// left" on a column with no sort at all reproduced it too) also fired the
|
|
4545
|
+
// COLUMNHEADER's own `toggleSort`, because nothing stopped that bubble before
|
|
4546
|
+
// it reached the header's `onClick`/`onKeyDown` (`getHeaderCellProps`). The
|
|
4547
|
+
// trigger's own `stopPropagation` (on `DataTableHeaderCellIcon` below) only
|
|
4548
|
+
// covers clicks that start on the trigger itself -- a SIBLING of
|
|
4549
|
+
// `MenuPositioner` in this tree, not an ancestor of it, so it never sees a
|
|
4550
|
+
// menu-item event at all. This wrapper is a genuine DOM node sitting between
|
|
4551
|
+
// BOTH of them and the header, so it catches the bubble from either one;
|
|
4552
|
+
// `display: contents` keeps it invisible to `DataTableHeaderCellInner`'s own
|
|
4553
|
+
// flex layout.
|
|
4554
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: not a widget -- this span never responds to interaction on ITSELF, only stops propagation of events that already occurred on a real interactive descendant (the trigger button, the portaled menu items). No role fits it because it isn't a control.
|
|
4555
|
+
/*#__PURE__*/ jsx("span", {
|
|
4556
|
+
onClick: stopPropagation,
|
|
4557
|
+
onKeyDown: stopPropagation,
|
|
4558
|
+
style: {
|
|
4559
|
+
display: "contents"
|
|
4560
|
+
},
|
|
4561
|
+
children: /*#__PURE__*/ jsxs(MenuRoot, {
|
|
4562
|
+
children: [
|
|
4563
|
+
/*#__PURE__*/ jsx(DataTableHeaderCellIcon, {
|
|
4564
|
+
asChild: true,
|
|
4565
|
+
"data-column-menu": "",
|
|
4566
|
+
draggable: false,
|
|
4567
|
+
children: /*#__PURE__*/ jsx(MenuTrigger, {
|
|
4568
|
+
"aria-label": `${columnLabel} column menu`,
|
|
4569
|
+
children: /*#__PURE__*/ jsx(IconDot3HorizontalLine_default, {
|
|
4570
|
+
size: 16
|
|
4571
|
+
})
|
|
4446
4572
|
})
|
|
4447
|
-
})
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
})
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
})
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
})
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
})
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4573
|
+
}),
|
|
4574
|
+
/*#__PURE__*/ jsx(MenuPositioner, {
|
|
4575
|
+
children: /*#__PURE__*/ jsx(MenuContent, {
|
|
4576
|
+
style: {
|
|
4577
|
+
minWidth: 160
|
|
4578
|
+
},
|
|
4579
|
+
children: /*#__PURE__*/ jsxs(MenuScrollArea, {
|
|
4580
|
+
children: [
|
|
4581
|
+
/*#__PURE__*/ jsx(MenuItem, {
|
|
4582
|
+
onClick: onRename,
|
|
4583
|
+
children: /*#__PURE__*/ jsx(MenuItemLabel, {
|
|
4584
|
+
children: "Rename"
|
|
4585
|
+
})
|
|
4586
|
+
}),
|
|
4587
|
+
/*#__PURE__*/ jsx(MenuItem, {
|
|
4588
|
+
onClick: ()=>moveColumn(column.key, "left"),
|
|
4589
|
+
children: /*#__PURE__*/ jsx(MenuItemLabel, {
|
|
4590
|
+
children: "Move left"
|
|
4591
|
+
})
|
|
4592
|
+
}),
|
|
4593
|
+
/*#__PURE__*/ jsx(MenuItem, {
|
|
4594
|
+
onClick: ()=>moveColumn(column.key, "right"),
|
|
4595
|
+
children: /*#__PURE__*/ jsx(MenuItemLabel, {
|
|
4596
|
+
children: "Move right"
|
|
4597
|
+
})
|
|
4598
|
+
}),
|
|
4599
|
+
isSorted && /*#__PURE__*/ jsx(MenuItem, {
|
|
4600
|
+
onClick: clearSort,
|
|
4601
|
+
children: /*#__PURE__*/ jsx(MenuItemLabel, {
|
|
4602
|
+
children: "Clear sort"
|
|
4603
|
+
})
|
|
4604
|
+
}),
|
|
4605
|
+
/*#__PURE__*/ jsx(MenuItem, {
|
|
4606
|
+
onClick: ()=>togglePinColumn(column.key),
|
|
4607
|
+
children: /*#__PURE__*/ jsx(MenuItemLabel, {
|
|
4608
|
+
children: pinned ? "Unpin column" : "Pin column"
|
|
4609
|
+
})
|
|
4610
|
+
}),
|
|
4611
|
+
/*#__PURE__*/ jsx(MenuItem, {
|
|
4612
|
+
onClick: ()=>hideColumn(column.key),
|
|
4613
|
+
children: /*#__PURE__*/ jsx(MenuItemLabel, {
|
|
4614
|
+
children: "Hide column"
|
|
4615
|
+
})
|
|
4484
4616
|
})
|
|
4485
|
-
|
|
4486
|
-
|
|
4617
|
+
]
|
|
4618
|
+
})
|
|
4487
4619
|
})
|
|
4488
4620
|
})
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
});
|
|
4621
|
+
]
|
|
4622
|
+
})
|
|
4623
|
+
}));
|
|
4492
4624
|
}
|
|
4493
4625
|
|
|
4494
4626
|
/**
|
|
@@ -10205,6 +10337,25 @@ function DataTableInner(props, ref) {
|
|
|
10205
10337
|
// column/direction is active. A consumer that pre-sorts server-side can pass
|
|
10206
10338
|
// a controlled `sort`; this still sorts the rows it's given, which for
|
|
10207
10339
|
// already-sorted data is a no-op.
|
|
10340
|
+
//
|
|
10341
|
+
// Live-review fix (D, "Clear sort"): the JSX below passes the RAW `sort` prop
|
|
10342
|
+
// (not `resolvedSort`) on to `DataTablePrimitiveRoot`, plus `defaultSort` --
|
|
10343
|
+
// passing `resolvedSort` used to feed the headless `useDataTableSort`'s own
|
|
10344
|
+
// `useControllableState` a value that starts `undefined` (uncontrolled) and
|
|
10345
|
+
// BECOMES defined the instant a header is first clicked (once `internalSort`
|
|
10346
|
+
// picks it up), flipping that inner hook from uncontrolled to controlled
|
|
10347
|
+
// mid-lifecycle -- `@radix-ui/react-use-controllable-state` warns about
|
|
10348
|
+
// exactly this and, worse, silently misbehaves on it: its own internal
|
|
10349
|
+
// uncontrolled state stops updating the moment it's controlled, so when
|
|
10350
|
+
// `clearSort` later drove `resolvedSort` back to `undefined` (flipping BACK
|
|
10351
|
+
// to uncontrolled), the hook fell through to that stale, never-updated
|
|
10352
|
+
// internal state instead -- still the pre-clear value, confirmed live
|
|
10353
|
+
// (`aria-sort` stayed "ascending" after "Clear sort"). The RAW `sort` prop
|
|
10354
|
+
// never has this problem: for the common uncontrolled case it's simply
|
|
10355
|
+
// always `undefined`, so the headless hook is uncontrolled for its entire
|
|
10356
|
+
// lifetime and owns its state correctly; a consumer that genuinely controls
|
|
10357
|
+
// `sort` behaves exactly as before (`resolvedSort === sort` whenever `sort`
|
|
10358
|
+
// is defined, so nothing changes for that path).
|
|
10208
10359
|
const [internalSort, setInternalSort] = React.useState(defaultSort);
|
|
10209
10360
|
const resolvedSort = sort !== undefined ? sort : internalSort;
|
|
10210
10361
|
const handleSortChange = React.useCallback((next)=>{
|
|
@@ -10310,7 +10461,8 @@ function DataTableInner(props, ref) {
|
|
|
10310
10461
|
selectedIds: selectedIds,
|
|
10311
10462
|
defaultSelectedIds: defaultSelectedIds,
|
|
10312
10463
|
onSelectionChange: onSelectionChange,
|
|
10313
|
-
sort:
|
|
10464
|
+
sort: sort,
|
|
10465
|
+
defaultSort: defaultSort,
|
|
10314
10466
|
onSortChange: handleSortChange,
|
|
10315
10467
|
query: resolvedQuery,
|
|
10316
10468
|
onQueryChange: handleQueryChange,
|
|
@@ -10573,6 +10725,10 @@ const DataTableTable = withContext$1(DataTableTable$1, "table");
|
|
|
10573
10725
|
}
|
|
10574
10726
|
}),
|
|
10575
10727
|
effective.map(({ column, width })=>/*#__PURE__*/ jsx("col", {
|
|
10728
|
+
// Read by `resize.tsx`'s own pointermove handler (`data-column-key`,
|
|
10729
|
+
// not a ref) to write the dragged width straight to this element
|
|
10730
|
+
// during a drag -- see that file's own comment for why.
|
|
10731
|
+
"data-column-key": column.key,
|
|
10576
10732
|
style: width === undefined ? undefined : {
|
|
10577
10733
|
width
|
|
10578
10734
|
}
|
|
@@ -11001,7 +11157,7 @@ DataTableViewsControl.displayName = "DataTableViewsControl";
|
|
|
11001
11157
|
});
|
|
11002
11158
|
}
|
|
11003
11159
|
|
|
11004
|
-
__insertCSS(".seed-resizable-panel__root{position:relative;display:flex;height:100%}.seed-resizable-panel__spacer{flex:0 0 auto;height:100%;transition:width .14s}.seed-resizable-panel__panel{position:absolute;top:0;bottom:0;box-sizing:border-box;overflow:hidden;background:var(--seed-color-bg-layer-default)}.seed-resizable-panel__panel[data-side=left]{left:0}.seed-resizable-panel__panel[data-side=right]{right:0}.seed-resizable-panel__panel[data-floating=false]{border-radius:0;box-shadow:none}.seed-resizable-panel__panel[data-floating=false][data-side=left]{border-inline-end:1px solid var(--seed-color-stroke-neutral-muted)}.seed-resizable-panel__panel[data-floating=false][data-side=right]{border-inline-start:1px solid var(--seed-color-stroke-neutral-muted)}.seed-resizable-panel__panel[data-floating=true]{position:
|
|
11160
|
+
__insertCSS(".seed-resizable-panel__root{position:relative;display:flex;height:100%}.seed-resizable-panel__spacer{flex:0 0 auto;height:100%;transition:width .14s}.seed-resizable-panel__panel{position:absolute;top:0;bottom:0;box-sizing:border-box;overflow:hidden;background:var(--seed-color-bg-layer-default)}.seed-resizable-panel__panel[data-side=left]{left:0}.seed-resizable-panel__panel[data-side=right]{right:0}.seed-resizable-panel__panel[data-floating=false]{border-radius:0;box-shadow:none}.seed-resizable-panel__panel[data-floating=false][data-side=left]{border-inline-end:1px solid var(--seed-color-stroke-neutral-muted)}.seed-resizable-panel__panel[data-floating=false][data-side=right]{border-inline-start:1px solid var(--seed-color-stroke-neutral-muted)}.seed-resizable-panel__panel[data-floating=true]{position:absolute;top:8px;bottom:8px;z-index:20;border-inline-end:none;border-inline-start:none;border-radius:12px;box-shadow:0 0 0 1px rgba(28,40,64,.04),0 4px 8px -4px rgba(28,40,64,.12),0 4px 12px -2px rgba(24,41,75,.16);transform:none;opacity:1}.seed-resizable-panel__panel[data-floating=true][data-side=left]{left:8px}.seed-resizable-panel__panel[data-floating=true][data-side=right]{right:8px}.seed-resizable-panel__panel[data-floating=true][data-side=left][data-state=open]{animation:seed-enter;animation-timing-function:cubic-bezier(0.16,1,0.3,1);animation-duration:.18s;--seed-enter-translate-x:calc(-100% - 8px);--seed-enter-translate-y:0;--seed-enter-opacity:0;--seed-enter-scale:1}@media (prefers-reduced-motion:reduce){.seed-resizable-panel__panel[data-floating=true][data-side=left][data-state=open]{--seed-enter-translate-x:0}}.seed-resizable-panel__panel[data-floating=true][data-side=left][data-state=closed]{animation:seed-exit;animation-timing-function:cubic-bezier(0.4,0,1,1);animation-duration:.14s;animation-fill-mode:forwards;--seed-exit-translate-x:calc(-100% - 8px);--seed-exit-translate-y:0;--seed-exit-opacity:0;--seed-exit-scale:1}@media (prefers-reduced-motion:reduce){.seed-resizable-panel__panel[data-floating=true][data-side=left][data-state=closed]{--seed-exit-translate-x:0}}.seed-resizable-panel__panel[data-floating=true][data-side=right][data-state=open]{animation:seed-enter;animation-timing-function:cubic-bezier(0.16,1,0.3,1);animation-duration:.18s;--seed-enter-translate-x:calc(100% + 8px);--seed-enter-translate-y:0;--seed-enter-opacity:0;--seed-enter-scale:1}@media (prefers-reduced-motion:reduce){.seed-resizable-panel__panel[data-floating=true][data-side=right][data-state=open]{--seed-enter-translate-x:0}}.seed-resizable-panel__panel[data-floating=true][data-side=right][data-state=closed]{animation:seed-exit;animation-timing-function:cubic-bezier(0.4,0,1,1);animation-duration:.14s;animation-fill-mode:forwards;--seed-exit-translate-x:calc(100% + 8px);--seed-exit-translate-y:0;--seed-exit-opacity:0;--seed-exit-scale:1}@media (prefers-reduced-motion:reduce){.seed-resizable-panel__panel[data-floating=true][data-side=right][data-state=closed]{--seed-exit-translate-x:0}}.seed-resizable-panel__inner{height:100%}.seed-resizable-panel__handle{position:absolute;top:0;width:10px;height:100%;z-index:2;cursor:col-resize;background:0 0;touch-action:none}.seed-resizable-panel__handle[data-side=left]{right:0}.seed-resizable-panel__handle[data-side=right]{left:0}.seed-resizable-panel__handle::before{content:\"\";position:absolute;top:0;width:1px;height:100%;background:var(--seed-color-stroke-neutral-muted);transition:background-color .16s,height .16s}.seed-resizable-panel__handle[data-side=left]::before{right:0}.seed-resizable-panel__handle[data-side=right]::before{left:0}.seed-resizable-panel__handle:hover::before,.seed-resizable-panel__handle[data-dragging]::before{width:2px;background:#0298d9}.seed-resizable-panel__handle.crisp-seed-resizable-panel-handle--column{display:flex;align-items:center;justify-content:center;width:7px;left:-3px;right:-3px;z-index:1;cursor:col-resize}.seed-resizable-panel__handle.crisp-seed-resizable-panel-handle--column::before{content:none}.seed-resizable-panel__handleInner{width:3px;height:100%;border-radius:3px;background:#0298d9;opacity:0;transition:opacity .1s .1s}.seed-resizable-panel__handle:hover .seed-resizable-panel__handleInner,.seed-resizable-panel__handle[data-dragging] .seed-resizable-panel__handleInner{opacity:1}");
|
|
11005
11161
|
|
|
11006
11162
|
const resizablePanelSlotNames = [
|
|
11007
11163
|
[
|
|
@@ -11205,16 +11361,26 @@ Object.assign(resizablePanel, {
|
|
|
11205
11361
|
// selector), so this component and that recipe stay in lockstep without a
|
|
11206
11362
|
// second source of truth for the string.
|
|
11207
11363
|
const COLUMN_HANDLE_CLASS = "crisp-seed-resizable-panel-handle--column";
|
|
11364
|
+
// Kept in lock-step with `resizable-panel.ts`'s own `FLOATING_GUTTER` (a
|
|
11365
|
+
// literal there too, for the same reason) -- the floating/inset-card variant
|
|
11366
|
+
// needs an outward-facing gutter that DOCKED never reserves. Read on at
|
|
11367
|
+
// `spacer`'s own render below for why this can't just be one more CSS rule.
|
|
11368
|
+
const FLOATING_GUTTER_PX = 8;
|
|
11208
11369
|
/**
|
|
11209
11370
|
* An adjustable-panel primitive: a spacer that reserves layout width plus a
|
|
11210
|
-
* panel that overlays it, so
|
|
11211
|
-
*
|
|
11212
|
-
*
|
|
11213
|
-
*
|
|
11214
|
-
*
|
|
11215
|
-
*
|
|
11216
|
-
*
|
|
11217
|
-
|
|
11371
|
+
* panel that overlays it, so it can resize (or animate open/closed) without
|
|
11372
|
+
* a reflow glitch on the rest of the layout -- the spacer always reserves
|
|
11373
|
+
* the panel's real width, in BOTH the docked and floating visual variants
|
|
11374
|
+
* (floating changes only the panel's own look -- gutter, radius, shadow --
|
|
11375
|
+
* never its `position`, so a consumer laying this out as a row-flex sibling
|
|
11376
|
+
* of e.g. a table sees that table genuinely shrink to make room, matching
|
|
11377
|
+
* Attio's own measured peek). Drag the edge handle to resize; constraints
|
|
11378
|
+
* are CSS `min-width`/`max-width`, never re-clamped in JS. Ported from
|
|
11379
|
+
* your-moon/crisp's `ResizablePanel` (`components/resizablepanel/`);
|
|
11380
|
+
* state/drag behavior lives in `useResizablePanel`
|
|
11381
|
+
* (`@seed-design/react-resizable-panel`), this component owns the DOM +
|
|
11382
|
+
* recipe classes on top of it.
|
|
11383
|
+
*/ const ResizablePanel = /*#__PURE__*/ forwardRef(({ side = "left", width: sizeProp, defaultSize = 280, onWidthChange, min = 200, max, floating = false, handleVariant = "panel", resizable = true, storageKey, animationState, panelElementRef, className, children, ...rest }, forwardedRef)=>{
|
|
11218
11384
|
const { width, dragging, containerRef, panelRef, handleHandlers } = useResizablePanel({
|
|
11219
11385
|
side,
|
|
11220
11386
|
size: sizeProp,
|
|
@@ -11222,9 +11388,29 @@ const COLUMN_HANDLE_CLASS = "crisp-seed-resizable-panel-handle--column";
|
|
|
11222
11388
|
onSizeChange: onWidthChange,
|
|
11223
11389
|
storageKey
|
|
11224
11390
|
});
|
|
11391
|
+
const composedPanelRef = useComposedRefs(panelRef, panelElementRef);
|
|
11225
11392
|
const classNames = resizablePanel();
|
|
11393
|
+
// The floating/inset-card variant sits INSET from `root`'s own outward
|
|
11394
|
+
// edge by `FLOATING_GUTTER_PX`, flush against the inward edge (Attio's
|
|
11395
|
+
// own peek: no gap on the side facing the table it shrinks). `width`
|
|
11396
|
+
// (the prop a consumer sets, e.g. `RecordPreview`'s own 497px default)
|
|
11397
|
+
// is the RESERVED FOOTPRINT -- what the table beside it actually gives
|
|
11398
|
+
// up -- so the panel's own rendered card shrinks by the gutter INSIDE
|
|
11399
|
+
// that footprint, rather than the footprint growing past it: reserving
|
|
11400
|
+
// the panel's full width AND adding an outward gutter on top of it
|
|
11401
|
+
// over-claims space the surrounding layout was never asked for --
|
|
11402
|
+
// confirmed live, it pushed the whole page 8px into horizontal
|
|
11403
|
+
// overflow. Docked (no gutter) keeps `width` as both the reservation
|
|
11404
|
+
// and the render, as before.
|
|
11405
|
+
const panelGutter = floating ? FLOATING_GUTTER_PX : 0;
|
|
11226
11406
|
const maxStyle = max == null ? undefined : typeof max === "number" ? `${max}px` : max;
|
|
11407
|
+
const panelMaxStyle = panelGutter === 0 || maxStyle == null ? maxStyle : `calc(${maxStyle} - ${panelGutter}px)`;
|
|
11227
11408
|
const panelSizeStyle = {
|
|
11409
|
+
width: width - panelGutter,
|
|
11410
|
+
minWidth: min - panelGutter,
|
|
11411
|
+
maxWidth: panelMaxStyle
|
|
11412
|
+
};
|
|
11413
|
+
const spacerSizeStyle = {
|
|
11228
11414
|
width,
|
|
11229
11415
|
minWidth: min,
|
|
11230
11416
|
maxWidth: maxStyle
|
|
@@ -11240,19 +11426,14 @@ const COLUMN_HANDLE_CLASS = "crisp-seed-resizable-panel-handle--column";
|
|
|
11240
11426
|
children: [
|
|
11241
11427
|
/*#__PURE__*/ jsx(Primitive.div, {
|
|
11242
11428
|
className: classNames.spacer,
|
|
11243
|
-
style:
|
|
11244
|
-
width: 0
|
|
11245
|
-
} : {
|
|
11246
|
-
width,
|
|
11247
|
-
minWidth: min,
|
|
11248
|
-
maxWidth: maxStyle
|
|
11249
|
-
}
|
|
11429
|
+
style: spacerSizeStyle
|
|
11250
11430
|
}),
|
|
11251
11431
|
/*#__PURE__*/ jsxs(Primitive.div, {
|
|
11252
|
-
ref:
|
|
11432
|
+
ref: composedPanelRef,
|
|
11253
11433
|
className: classNames.panel,
|
|
11254
11434
|
"data-side": side,
|
|
11255
11435
|
"data-floating": floating ? "true" : "false",
|
|
11436
|
+
"data-state": animationState,
|
|
11256
11437
|
style: panelSizeStyle,
|
|
11257
11438
|
children: [
|
|
11258
11439
|
/*#__PURE__*/ jsx(Primitive.div, {
|
|
@@ -11277,132 +11458,276 @@ const COLUMN_HANDLE_CLASS = "crisp-seed-resizable-panel-handle--column";
|
|
|
11277
11458
|
});
|
|
11278
11459
|
ResizablePanel.displayName = "ResizablePanel";
|
|
11279
11460
|
|
|
11280
|
-
__insertCSS(".seed-record-
|
|
11461
|
+
__insertCSS(".seed-record-preview__root{display:flex;flex-direction:column;height:100%;overflow:hidden;background:var(--seed-color-bg-layer-default);font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}.seed-record-preview__toolbar{display:flex;align-items:center;height:49px;flex-shrink:0;padding:8px 10px;border-bottom:1px solid var(--seed-color-stroke-neutral-muted)}.seed-record-preview__toolbarSpacer{flex:1}.seed-record-preview__pager{display:flex;align-items:center;gap:6px;margin-inline-start:12px}.seed-record-preview__counter{margin-inline-start:12px;font-size:12px;font-weight:500;color:var(--seed-color-fg-neutral-subtle);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.seed-record-preview__iconbtn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;flex-shrink:0;padding:0;border:none;border-radius:6px;background:0 0;color:var(--seed-color-fg-neutral-subtle);font-family:inherit;cursor:pointer;transition:background-color .12s}.seed-record-preview__iconbtn:hover{background:var(--seed-color-bg-layer-fill);color:var(--seed-color-fg-neutral)}.seed-record-preview__navbtn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;flex-shrink:0;padding:0;border:none;border-radius:6px;background:var(--seed-color-bg-layer-default);box-shadow:inset 0 0 0 1px rgba(0,0,0,.06),0 0 2px rgba(28,40,64,.18),0 1px 3px rgba(0,0,0,.04);color:var(--seed-color-fg-neutral);font-family:inherit;cursor:pointer}.seed-record-preview__navbtn:hover:not(:disabled){background:var(--seed-color-bg-layer-fill)}.seed-record-preview__navbtn:disabled{opacity:.4;cursor:not-allowed}.seed-record-preview__header{display:flex;align-items:center;gap:8px;height:52px;flex-shrink:0;padding:0 12px}.seed-record-preview__fav{width:22px;height:22px;border-radius:4px;flex-shrink:0;object-fit:cover;background:var(--seed-color-bg-layer-fill)}.seed-record-preview__title{flex:1;min-width:0;font-size:16px;font-weight:600;letter-spacing:-.2px;color:var(--seed-color-fg-neutral);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.seed-record-preview__body{flex:1;min-height:0;overflow-y:auto;padding:8px 12px}.seed-record-preview__section{padding:8px 4px 0}.seed-record-preview__sectionHead{display:flex;align-items:center;justify-content:space-between;gap:8px;height:20px;margin-bottom:8px}.seed-record-preview__sectionLabel{font-size:14px;font-weight:500;color:var(--seed-color-fg-neutral-subtle)}.seed-record-preview__sectionAction{border:none;background:0 0;padding:0;font-family:inherit;font-size:10px;font-weight:400;color:color-mix(in srgb,var(--seed-color-fg-neutral) 55%,transparent);cursor:pointer}.seed-record-preview__sectionAction:hover{color:var(--seed-color-fg-neutral)}.seed-record-preview__highlights{display:grid;grid-template-columns:1fr 1fr;gap:8px}.seed-record-preview__highlight{display:flex;flex-direction:column;gap:6px;min-width:0}.seed-record-preview__highlightLabel{display:flex;align-items:center;gap:4px;font-size:12px;font-weight:500;color:var(--seed-color-fg-neutral-subtle)}.seed-record-preview__highlightLabel span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.seed-record-preview__highlightLabel svg{flex-shrink:0;color:var(--seed-color-fg-neutral-subtle)}.seed-record-preview__highlightValue{font-size:14px;font-weight:500;color:var(--seed-color-fg-neutral);overflow:hidden;text-overflow:ellipsis}.seed-record-preview__activity{font-size:14px}.seed-record-preview__sections{display:flex;flex-direction:column;padding-top:8px}.seed-record-preview__sectionTrigger{display:flex;align-items:center;gap:4px;width:100%;height:20px;padding:0 8px 0 12px;border:none;background:0 0;font-family:inherit;text-align:start;cursor:pointer}.seed-record-preview__sectionTrigger[data-inert]{cursor:default}.seed-record-preview__sectionRowLabel{font-size:14px;font-weight:500;color:var(--seed-color-fg-neutral)}.seed-record-preview__sectionRowCount{font-size:14px;color:var(--seed-color-fg-neutral-subtle)}.seed-record-preview__sectionChevron{margin-inline-start:auto;flex-shrink:0;color:var(--seed-color-fg-neutral-subtle);transition:transform .15s ease}.seed-record-preview__sectionChevron[data-open]{transform:rotate(180deg)}.seed-record-preview__sectionContent{padding:8px 8px 8px 12px}.seed-record-preview__footer{flex-shrink:0;display:flex;align-items:center;gap:8px;padding:8px 12px;border-top:1px solid var(--seed-color-stroke-neutral-muted)}");
|
|
11281
11462
|
|
|
11282
|
-
const
|
|
11463
|
+
const recordPreviewSlotNames = [
|
|
11283
11464
|
[
|
|
11284
11465
|
"root",
|
|
11285
|
-
"seed-record-
|
|
11466
|
+
"seed-record-preview__root"
|
|
11286
11467
|
],
|
|
11287
11468
|
[
|
|
11288
11469
|
"toolbar",
|
|
11289
|
-
"seed-record-
|
|
11470
|
+
"seed-record-preview__toolbar"
|
|
11290
11471
|
],
|
|
11291
11472
|
[
|
|
11292
11473
|
"toolbarSpacer",
|
|
11293
|
-
"seed-record-
|
|
11474
|
+
"seed-record-preview__toolbarSpacer"
|
|
11294
11475
|
],
|
|
11295
11476
|
[
|
|
11296
11477
|
"pager",
|
|
11297
|
-
"seed-record-
|
|
11478
|
+
"seed-record-preview__pager"
|
|
11298
11479
|
],
|
|
11299
11480
|
[
|
|
11300
11481
|
"counter",
|
|
11301
|
-
"seed-record-
|
|
11482
|
+
"seed-record-preview__counter"
|
|
11302
11483
|
],
|
|
11303
11484
|
[
|
|
11304
11485
|
"iconbtn",
|
|
11305
|
-
"seed-record-
|
|
11486
|
+
"seed-record-preview__iconbtn"
|
|
11306
11487
|
],
|
|
11307
11488
|
[
|
|
11308
11489
|
"navbtn",
|
|
11309
|
-
"seed-record-
|
|
11490
|
+
"seed-record-preview__navbtn"
|
|
11310
11491
|
],
|
|
11311
11492
|
[
|
|
11312
11493
|
"header",
|
|
11313
|
-
"seed-record-
|
|
11494
|
+
"seed-record-preview__header"
|
|
11314
11495
|
],
|
|
11315
11496
|
[
|
|
11316
11497
|
"fav",
|
|
11317
|
-
"seed-record-
|
|
11498
|
+
"seed-record-preview__fav"
|
|
11318
11499
|
],
|
|
11319
11500
|
[
|
|
11320
11501
|
"title",
|
|
11321
|
-
"seed-record-
|
|
11502
|
+
"seed-record-preview__title"
|
|
11322
11503
|
],
|
|
11323
11504
|
[
|
|
11324
11505
|
"body",
|
|
11325
|
-
"seed-record-
|
|
11506
|
+
"seed-record-preview__body"
|
|
11326
11507
|
],
|
|
11327
11508
|
[
|
|
11328
11509
|
"section",
|
|
11329
|
-
"seed-record-
|
|
11510
|
+
"seed-record-preview__section"
|
|
11330
11511
|
],
|
|
11331
11512
|
[
|
|
11332
11513
|
"sectionHead",
|
|
11333
|
-
"seed-record-
|
|
11514
|
+
"seed-record-preview__sectionHead"
|
|
11334
11515
|
],
|
|
11335
11516
|
[
|
|
11336
11517
|
"sectionLabel",
|
|
11337
|
-
"seed-record-
|
|
11518
|
+
"seed-record-preview__sectionLabel"
|
|
11338
11519
|
],
|
|
11339
11520
|
[
|
|
11340
11521
|
"sectionAction",
|
|
11341
|
-
"seed-record-
|
|
11522
|
+
"seed-record-preview__sectionAction"
|
|
11342
11523
|
],
|
|
11343
11524
|
[
|
|
11344
11525
|
"highlights",
|
|
11345
|
-
"seed-record-
|
|
11526
|
+
"seed-record-preview__highlights"
|
|
11346
11527
|
],
|
|
11347
11528
|
[
|
|
11348
11529
|
"highlight",
|
|
11349
|
-
"seed-record-
|
|
11530
|
+
"seed-record-preview__highlight"
|
|
11350
11531
|
],
|
|
11351
11532
|
[
|
|
11352
11533
|
"highlightLabel",
|
|
11353
|
-
"seed-record-
|
|
11534
|
+
"seed-record-preview__highlightLabel"
|
|
11354
11535
|
],
|
|
11355
11536
|
[
|
|
11356
11537
|
"highlightValue",
|
|
11357
|
-
"seed-record-
|
|
11538
|
+
"seed-record-preview__highlightValue"
|
|
11358
11539
|
],
|
|
11359
11540
|
[
|
|
11360
11541
|
"activity",
|
|
11361
|
-
"seed-record-
|
|
11542
|
+
"seed-record-preview__activity"
|
|
11362
11543
|
],
|
|
11363
11544
|
[
|
|
11364
11545
|
"sections",
|
|
11365
|
-
"seed-record-
|
|
11546
|
+
"seed-record-preview__sections"
|
|
11366
11547
|
],
|
|
11367
11548
|
[
|
|
11368
11549
|
"sectionTrigger",
|
|
11369
|
-
"seed-record-
|
|
11550
|
+
"seed-record-preview__sectionTrigger"
|
|
11370
11551
|
],
|
|
11371
11552
|
[
|
|
11372
11553
|
"sectionRowLabel",
|
|
11373
|
-
"seed-record-
|
|
11554
|
+
"seed-record-preview__sectionRowLabel"
|
|
11374
11555
|
],
|
|
11375
11556
|
[
|
|
11376
11557
|
"sectionRowCount",
|
|
11377
|
-
"seed-record-
|
|
11558
|
+
"seed-record-preview__sectionRowCount"
|
|
11378
11559
|
],
|
|
11379
11560
|
[
|
|
11380
11561
|
"sectionChevron",
|
|
11381
|
-
"seed-record-
|
|
11562
|
+
"seed-record-preview__sectionChevron"
|
|
11382
11563
|
],
|
|
11383
11564
|
[
|
|
11384
11565
|
"sectionContent",
|
|
11385
|
-
"seed-record-
|
|
11566
|
+
"seed-record-preview__sectionContent"
|
|
11386
11567
|
],
|
|
11387
11568
|
[
|
|
11388
11569
|
"footer",
|
|
11389
|
-
"seed-record-
|
|
11570
|
+
"seed-record-preview__footer"
|
|
11390
11571
|
]
|
|
11391
11572
|
];
|
|
11392
11573
|
const defaultVariant = {};
|
|
11393
11574
|
const compoundVariants = [];
|
|
11394
|
-
const
|
|
11395
|
-
function
|
|
11396
|
-
return Object.fromEntries(
|
|
11575
|
+
const recordPreviewVariantMap = {};
|
|
11576
|
+
function recordPreview(props) {
|
|
11577
|
+
return Object.fromEntries(recordPreviewSlotNames.map(([slot, className])=>{
|
|
11397
11578
|
return [
|
|
11398
11579
|
slot,
|
|
11399
11580
|
createClassName(className, mergeVariants(defaultVariant, props), compoundVariants)
|
|
11400
11581
|
];
|
|
11401
11582
|
}));
|
|
11402
11583
|
}
|
|
11403
|
-
Object.assign(
|
|
11404
|
-
splitVariantProps: (props)=>splitVariantProps(props,
|
|
11405
|
-
}); // @recipe(seed): record-
|
|
11584
|
+
Object.assign(recordPreview, {
|
|
11585
|
+
splitVariantProps: (props)=>splitVariantProps(props, recordPreviewVariantMap)
|
|
11586
|
+
}); // @recipe(seed): record-preview
|
|
11587
|
+
|
|
11588
|
+
// This code includes portions derived from radix-ui/primitives (https://github.com/radix-ui/primitives)
|
|
11589
|
+
// Used under the MIT License: https://opensource.org/licenses/MIT
|
|
11590
|
+
function usePresence(present) {
|
|
11591
|
+
const [node, setNode] = React.useState();
|
|
11592
|
+
const stylesRef = React.useRef(null);
|
|
11593
|
+
const prevPresentRef = React.useRef(present);
|
|
11594
|
+
const prevAnimationNameRef = React.useRef("none");
|
|
11595
|
+
const initialState = present ? "mounted" : "unmounted";
|
|
11596
|
+
const [state, send] = useStateMachine(initialState, {
|
|
11597
|
+
mounted: {
|
|
11598
|
+
UNMOUNT: "unmounted",
|
|
11599
|
+
ANIMATION_OUT: "unmountSuspended"
|
|
11600
|
+
},
|
|
11601
|
+
unmountSuspended: {
|
|
11602
|
+
MOUNT: "mounted",
|
|
11603
|
+
ANIMATION_END: "unmounted"
|
|
11604
|
+
},
|
|
11605
|
+
unmounted: {
|
|
11606
|
+
MOUNT: "mounted"
|
|
11607
|
+
}
|
|
11608
|
+
});
|
|
11609
|
+
React.useEffect(()=>{
|
|
11610
|
+
const currentAnimationName = getAnimationName(stylesRef.current);
|
|
11611
|
+
prevAnimationNameRef.current = state === "mounted" ? currentAnimationName : "none";
|
|
11612
|
+
}, [
|
|
11613
|
+
state
|
|
11614
|
+
]);
|
|
11615
|
+
useLayoutEffect2(()=>{
|
|
11616
|
+
const styles = stylesRef.current;
|
|
11617
|
+
const wasPresent = prevPresentRef.current;
|
|
11618
|
+
const hasPresentChanged = wasPresent !== present;
|
|
11619
|
+
if (hasPresentChanged) {
|
|
11620
|
+
const prevAnimationName = prevAnimationNameRef.current;
|
|
11621
|
+
const currentAnimationName = getAnimationName(styles);
|
|
11622
|
+
if (present) {
|
|
11623
|
+
send("MOUNT");
|
|
11624
|
+
} else if (currentAnimationName === "none" || styles?.display === "none") {
|
|
11625
|
+
// If there is no exit animation or the element is hidden, animations won't run
|
|
11626
|
+
// so we unmount instantly
|
|
11627
|
+
send("UNMOUNT");
|
|
11628
|
+
} else {
|
|
11629
|
+
/**
|
|
11630
|
+
* When `present` changes to `false`, we check changes to animation-name to
|
|
11631
|
+
* determine whether an animation has started. We chose this approach (reading
|
|
11632
|
+
* computed styles) because there is no `animationrun` event and `animationstart`
|
|
11633
|
+
* fires after `animation-delay` has expired which would be too late.
|
|
11634
|
+
*/ const isAnimating = prevAnimationName !== currentAnimationName;
|
|
11635
|
+
if (wasPresent && isAnimating) {
|
|
11636
|
+
send("ANIMATION_OUT");
|
|
11637
|
+
} else {
|
|
11638
|
+
send("UNMOUNT");
|
|
11639
|
+
}
|
|
11640
|
+
}
|
|
11641
|
+
prevPresentRef.current = present;
|
|
11642
|
+
}
|
|
11643
|
+
}, [
|
|
11644
|
+
present,
|
|
11645
|
+
send
|
|
11646
|
+
]);
|
|
11647
|
+
useLayoutEffect2(()=>{
|
|
11648
|
+
if (node) {
|
|
11649
|
+
let timeoutId;
|
|
11650
|
+
const ownerWindow = node.ownerDocument.defaultView ?? window;
|
|
11651
|
+
/**
|
|
11652
|
+
* Triggering an ANIMATION_OUT during an ANIMATION_IN will fire an `animationcancel`
|
|
11653
|
+
* event for ANIMATION_IN after we have entered `unmountSuspended` state. So, we
|
|
11654
|
+
* make sure we only trigger ANIMATION_END for the currently active animation.
|
|
11655
|
+
*/ const handleAnimationEnd = (event)=>{
|
|
11656
|
+
const currentAnimationName = getAnimationName(stylesRef.current);
|
|
11657
|
+
const isCurrentAnimation = currentAnimationName.includes(event.animationName);
|
|
11658
|
+
if (event.target === node && isCurrentAnimation) {
|
|
11659
|
+
// With React 18 concurrency this update is applied a frame after the
|
|
11660
|
+
// animation ends, creating a flash of visible content. By setting the
|
|
11661
|
+
// animation fill mode to "forwards", we force the node to keep the
|
|
11662
|
+
// styles of the last keyframe, removing the flash.
|
|
11663
|
+
//
|
|
11664
|
+
// Previously we flushed the update via ReactDom.flushSync, but with
|
|
11665
|
+
// exit animations this resulted in the node being removed from the
|
|
11666
|
+
// DOM before the synthetic animationEnd event was dispatched, meaning
|
|
11667
|
+
// user-provided event handlers would not be called.
|
|
11668
|
+
// https://github.com/radix-ui/primitives/pull/1849
|
|
11669
|
+
send("ANIMATION_END");
|
|
11670
|
+
if (!prevPresentRef.current) {
|
|
11671
|
+
const currentFillMode = node.style.animationFillMode;
|
|
11672
|
+
node.style.animationFillMode = "forwards";
|
|
11673
|
+
// Reset the style after the node had time to unmount (for cases
|
|
11674
|
+
// where the component chooses not to unmount). Doing this any
|
|
11675
|
+
// sooner than `setTimeout` (e.g. with `requestAnimationFrame`)
|
|
11676
|
+
// still causes a flash.
|
|
11677
|
+
timeoutId = ownerWindow.setTimeout(()=>{
|
|
11678
|
+
if (node.style.animationFillMode === "forwards") {
|
|
11679
|
+
node.style.animationFillMode = currentFillMode;
|
|
11680
|
+
}
|
|
11681
|
+
});
|
|
11682
|
+
}
|
|
11683
|
+
}
|
|
11684
|
+
};
|
|
11685
|
+
const handleAnimationStart = (event)=>{
|
|
11686
|
+
if (event.target === node) {
|
|
11687
|
+
// if animation occurred, store its name as the previous animation.
|
|
11688
|
+
prevAnimationNameRef.current = getAnimationName(stylesRef.current);
|
|
11689
|
+
}
|
|
11690
|
+
};
|
|
11691
|
+
node.addEventListener("animationstart", handleAnimationStart);
|
|
11692
|
+
node.addEventListener("animationcancel", handleAnimationEnd);
|
|
11693
|
+
node.addEventListener("animationend", handleAnimationEnd);
|
|
11694
|
+
return ()=>{
|
|
11695
|
+
ownerWindow.clearTimeout(timeoutId);
|
|
11696
|
+
node.removeEventListener("animationstart", handleAnimationStart);
|
|
11697
|
+
node.removeEventListener("animationcancel", handleAnimationEnd);
|
|
11698
|
+
node.removeEventListener("animationend", handleAnimationEnd);
|
|
11699
|
+
};
|
|
11700
|
+
} else {
|
|
11701
|
+
// Transition to the unmounted state if the node is removed prematurely.
|
|
11702
|
+
// We avoid doing so during cleanup as the node may change but still exist.
|
|
11703
|
+
send("ANIMATION_END");
|
|
11704
|
+
}
|
|
11705
|
+
}, [
|
|
11706
|
+
node,
|
|
11707
|
+
send
|
|
11708
|
+
]);
|
|
11709
|
+
return {
|
|
11710
|
+
isPresent: [
|
|
11711
|
+
"mounted",
|
|
11712
|
+
"unmountSuspended"
|
|
11713
|
+
].includes(state),
|
|
11714
|
+
ref: React.useCallback((node)=>{
|
|
11715
|
+
if (node) stylesRef.current = getComputedStyle(node);
|
|
11716
|
+
setNode(node ?? undefined);
|
|
11717
|
+
}, [])
|
|
11718
|
+
};
|
|
11719
|
+
}
|
|
11720
|
+
/* -----------------------------------------------------------------------------------------------*/ function getAnimationName(styles) {
|
|
11721
|
+
return styles?.animationName || "none";
|
|
11722
|
+
}
|
|
11723
|
+
function useStateMachine(initialState, machine) {
|
|
11724
|
+
return React.useReducer((state, event)=>{
|
|
11725
|
+
const nextState = machine[state][event];
|
|
11726
|
+
return nextState ?? state;
|
|
11727
|
+
}, initialState);
|
|
11728
|
+
}
|
|
11729
|
+
const PresenceContext = /*#__PURE__*/ createContext(null);
|
|
11730
|
+
PresenceContext.Provider;
|
|
11406
11731
|
|
|
11407
11732
|
const RESIZE_MIN_WIDTH = 360;
|
|
11408
11733
|
const RESIZE_MAX_WIDTH = 720;
|
|
@@ -11412,12 +11737,35 @@ const RESIZE_MAX_WIDTH = 720;
|
|
|
11412
11737
|
* `RecordPanel` (`product/recordpanel/`), built ON `ResizablePanel`
|
|
11413
11738
|
* (`side="right" floating`) rather than crisp's own duplicate absolute-
|
|
11414
11739
|
* positioning/slide-in/resize-handle implementation -- the outer shell (the
|
|
11415
|
-
*
|
|
11740
|
+
* enter/exit keyframe animation, the floating elevation, the drag handle
|
|
11416
11741
|
* itself) all come from that primitive; this component only renders the
|
|
11417
11742
|
* CONTENT inside it.
|
|
11418
|
-
|
|
11743
|
+
*
|
|
11744
|
+
* Enter/exit: closing this panel is a state change (`open` -> false) that
|
|
11745
|
+
* typically clears `record` in the SAME update (see the DataTable story's
|
|
11746
|
+
* own `onClose={() => setPreviewRow(null)}`), so by the time an exit
|
|
11747
|
+
* animation should play, the values this component would render are
|
|
11748
|
+
* already gone. `usePresence` (`@seed-design/react-presence`, the same
|
|
11749
|
+
* idiom Dialog/Drawer already use for their own floating layers) keeps the
|
|
11750
|
+
* tree mounted for exactly the exit animation's duration; the snapshot
|
|
11751
|
+
* below keeps the LAST visible props around for that same window, since
|
|
11752
|
+
* without it the panel would animate out empty.
|
|
11753
|
+
*/ function RecordPreview(props) {
|
|
11754
|
+
const shouldRender = props.open && props.record != null;
|
|
11755
|
+
const { isPresent, ref: panelElementRef } = usePresence(shouldRender);
|
|
11756
|
+
const lastVisiblePropsRef = useRef(props);
|
|
11757
|
+
if (shouldRender) lastVisiblePropsRef.current = props;
|
|
11758
|
+
if (!isPresent) return null;
|
|
11759
|
+
return /*#__PURE__*/ jsx(RecordPreviewPanel, {
|
|
11760
|
+
...shouldRender ? props : lastVisiblePropsRef.current,
|
|
11761
|
+
animationState: shouldRender ? "open" : "closed",
|
|
11762
|
+
panelElementRef: panelElementRef
|
|
11763
|
+
});
|
|
11764
|
+
}
|
|
11765
|
+
RecordPreview.displayName = "RecordPreview";
|
|
11766
|
+
function RecordPreviewPanel({ open: _open, record, onClose, className, animationState, panelElementRef, closeLabel = "Close panel", onOpenRecord, openRecordLabel = "Open record", onPrevious, previousRecordLabel = "Previous record", onNext, nextRecordLabel = "Next record", title, avatar, highlightsLabel = "Highlights", highlights, counterLabel, resizable = false, width, defaultWidth = 497, onWidthChange, minWidth = RESIZE_MIN_WIDTH, maxWidth = RESIZE_MAX_WIDTH, activityLabel = "Activity", viewAllActivityLabel = "View all", onViewAllActivity, activity, sections, footer, ...rest }) {
|
|
11419
11767
|
const titleId = useId();
|
|
11420
|
-
const classNames =
|
|
11768
|
+
const classNames = recordPreview();
|
|
11421
11769
|
// Which sections (your-moon/crisp#413) are expanded -- uncontrolled and
|
|
11422
11770
|
// independent (Attio's own Emails/Notes/Tasks open without closing one
|
|
11423
11771
|
// another): a plain `Set`, reset whenever the record itself changes so a
|
|
@@ -11435,28 +11783,31 @@ const RESIZE_MAX_WIDTH = 720;
|
|
|
11435
11783
|
else next.add(key);
|
|
11436
11784
|
return next;
|
|
11437
11785
|
});
|
|
11438
|
-
// Close on Escape
|
|
11439
|
-
//
|
|
11440
|
-
//
|
|
11441
|
-
//
|
|
11442
|
-
//
|
|
11443
|
-
// propagation on its own Escape handler.
|
|
11786
|
+
// Close on Escape. Simpler than crisp's own version (crisp additionally
|
|
11787
|
+
// checks `hasOpenFloatingLayer()` so an Escape meant for a nested Menu/
|
|
11788
|
+
// Select/Popover doesn't also close this panel) -- crispy-seed has no
|
|
11789
|
+
// equivalent floating-stack registry to read, so a consumer nesting its
|
|
11790
|
+
// own floating layers inside `footer`/`sections` content should stop
|
|
11791
|
+
// propagation on its own Escape handler. This component is only ever
|
|
11792
|
+
// mounted (by `RecordPreview`, above) while genuinely open or animating
|
|
11793
|
+
// out, so there's no separate `open` flag to gate on here.
|
|
11444
11794
|
useEffect(()=>{
|
|
11445
|
-
if (!open) return;
|
|
11446
11795
|
const onKey = (event)=>{
|
|
11447
11796
|
if (event.key === "Escape") onClose();
|
|
11448
11797
|
};
|
|
11449
11798
|
document.addEventListener("keydown", onKey);
|
|
11450
11799
|
return ()=>document.removeEventListener("keydown", onKey);
|
|
11451
11800
|
}, [
|
|
11452
|
-
open,
|
|
11453
11801
|
onClose
|
|
11454
11802
|
]);
|
|
11455
|
-
|
|
11803
|
+
// Defensive only -- `RecordPreview` never mounts this without a record.
|
|
11804
|
+
if (!record) return null;
|
|
11456
11805
|
const heading = title ?? record.name ?? "Record";
|
|
11457
11806
|
return /*#__PURE__*/ jsx(ResizablePanel, {
|
|
11458
11807
|
side: "right",
|
|
11459
11808
|
floating: true,
|
|
11809
|
+
animationState: animationState,
|
|
11810
|
+
panelElementRef: panelElementRef,
|
|
11460
11811
|
resizable: resizable,
|
|
11461
11812
|
width: width,
|
|
11462
11813
|
defaultSize: defaultWidth,
|
|
@@ -11668,6 +12019,6 @@ const RESIZE_MAX_WIDTH = 720;
|
|
|
11668
12019
|
})
|
|
11669
12020
|
});
|
|
11670
12021
|
}
|
|
11671
|
-
|
|
12022
|
+
RecordPreviewPanel.displayName = "RecordPreviewPanel";
|
|
11672
12023
|
|
|
11673
|
-
export { DataTable, DataTableViewsControl,
|
|
12024
|
+
export { DataTable, DataTableViewsControl, RecordPreview, ResizablePanel };
|