@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 CHANGED
@@ -769,21 +769,31 @@ function isFunction$1(value) {
769
769
 
770
770
  const DATA_TABLE_UNIVERSAL_CALC_OPS = [
771
771
  "count",
772
- "countFilled"
772
+ "empty",
773
+ "notEmpty"
773
774
  ];
774
775
  const DATA_TABLE_NUMERIC_CALC_OPS = [
775
776
  "sum",
776
777
  "average",
778
+ "median",
777
779
  "min",
778
- "max"
780
+ "max",
781
+ "range"
782
+ ];
783
+ /** Offered instead of `DATA_TABLE_NUMERIC_CALC_OPS` on a non-numeric column. */ const DATA_TABLE_NON_NUMERIC_CALC_OPS = [
784
+ "countUnique"
779
785
  ];
780
786
  const DATA_TABLE_CALC_LABEL = {
781
787
  count: "Count",
782
- countFilled: "Count filled",
788
+ empty: "Empty",
789
+ notEmpty: "Not empty",
783
790
  sum: "Sum",
784
791
  average: "Average",
792
+ median: "Median",
785
793
  min: "Min",
786
- max: "Max"
794
+ max: "Max",
795
+ range: "Range",
796
+ countUnique: "Count unique"
787
797
  };
788
798
  /** A value counts as empty when it is null/undefined, an empty string, or an empty
789
799
  * array -- crisp's own `isEmptyValue` (columncalc.tsx), reused verbatim. */ function isEmptyValueForCalc(value) {
@@ -792,29 +802,50 @@ const DATA_TABLE_CALC_LABEL = {
792
802
  if (Array.isArray(value)) return value.length === 0;
793
803
  return false;
794
804
  }
805
+ /** The middle value of a SORTED, non-empty numeric array -- the mean of the two
806
+ * middle entries on an even-length array, same convention as every stats library. */ function median(sortedNumbers) {
807
+ const mid = Math.floor(sortedNumbers.length / 2);
808
+ return sortedNumbers.length % 2 === 0 ? (sortedNumbers[mid - 1] + sortedNumbers[mid]) / 2 : sortedNumbers[mid];
809
+ }
795
810
  /**
796
811
  * Pure so the arithmetic is independently (and mutation-) testable without rendering a
797
- * table at all. `sum`/`average`/`min`/`max` skip empty values and non-numeric ones
798
- * (coerced via `Number`, dropping `NaN`); they return `null` -- render nothing, not a
799
- * misleading 0 -- when there is nothing numeric left to reduce over.
812
+ * table at all. The numeric ops (`sum`/`average`/`median`/`min`/`max`/`range`) skip
813
+ * empty values and non-numeric ones (coerced via `Number`, dropping `NaN`); they
814
+ * return `null` -- render nothing, not a misleading 0 -- when there is nothing
815
+ * numeric left to reduce over. `countUnique` counts distinct NON-EMPTY values
816
+ * (`JSON.stringify`-normalized, so two arrays/objects with the same shape count
817
+ * once) -- empties are already their own op (`empty`), so folding them into "one
818
+ * more unique value" here would double-count the same information two different
819
+ * ways in the same menu.
800
820
  */ function computeColumnCalc(values, op) {
801
821
  switch(op){
802
822
  case "count":
803
823
  return values.length;
804
- case "countFilled":
824
+ case "empty":
825
+ return values.filter(isEmptyValueForCalc).length;
826
+ case "notEmpty":
805
827
  return values.filter((value)=>!isEmptyValueForCalc(value)).length;
828
+ case "countUnique":
829
+ {
830
+ const seen = new Set(values.filter((value)=>!isEmptyValueForCalc(value)).map((value)=>JSON.stringify(value)));
831
+ return seen.size;
832
+ }
806
833
  }
807
- const numbers = values.filter((value)=>!isEmptyValueForCalc(value)).map((value)=>typeof value === "number" ? value : Number(value)).filter((value)=>Number.isFinite(value));
834
+ 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);
808
835
  if (numbers.length === 0) return null;
809
836
  switch(op){
810
837
  case "sum":
811
838
  return numbers.reduce((total, value)=>total + value, 0);
812
839
  case "average":
813
840
  return numbers.reduce((total, value)=>total + value, 0) / numbers.length;
841
+ case "median":
842
+ return median(numbers);
814
843
  case "min":
815
- return Math.min(...numbers);
844
+ return numbers[0];
816
845
  case "max":
817
- return Math.max(...numbers);
846
+ return numbers[numbers.length - 1];
847
+ case "range":
848
+ return numbers[numbers.length - 1] - numbers[0];
818
849
  default:
819
850
  return null;
820
851
  }
@@ -1219,31 +1250,48 @@ const EMPTY_RECORD = {};
1219
1250
  ]);
1220
1251
  const endColumnDrag = React.useCallback(()=>setDraggingColumnKey(null), []);
1221
1252
  // -- drag-to-resize ------------------------------------------------------------------
1222
- // `resizeStartRef` holds the ONE thing that must survive across pointermove events
1223
- // without triggering a re-render on every pixel (the drag's origin) -- state
1224
- // (`resizingColumnKey`) is only for what the render actually needs: which column's
1225
- // handle should show its active/dragging look.
1253
+ // `resizeStartRef` holds everything that must survive across pointermove events
1254
+ // WITHOUT triggering a re-render on every pixel: the drag's origin, AND (as of the
1255
+ // live-review fix below) the latest computed width, so `endColumnResize` has
1256
+ // something to commit. State (`resizingColumnKey`) is only for what the render
1257
+ // actually needs: which column's handle should show its active/dragging look.
1258
+ //
1259
+ // `updateColumnResize` used to call `setColumnWidth` on every single pointermove --
1260
+ // a `setColumnWidths` state update (and therefore a full DataTable re-render) per
1261
+ // PIXEL of drag, confirmed live as the stutter a column-border drag produced. It now
1262
+ // only computes+returns the clamped width; the styled layer (`resize.tsx`) writes
1263
+ // that directly to the dragged column's own `<col>` element (`col.style.width`,
1264
+ // outside React), which is all a resize needs frame-to-frame -- table layout already
1265
+ // recalculates from a `<col>`'s own inline style with no re-render involved. React
1266
+ // state (and this hook's `onColumnWidthsChange` contract) is only touched ONCE, in
1267
+ // `endColumnResize`, committing the final width exactly as a controlled consumer
1268
+ // already expects.
1226
1269
  const resizeStartRef = React.useRef(null);
1227
1270
  const [resizingColumnKey, setResizingColumnKey] = React.useState(null);
1228
1271
  const beginColumnResize = React.useCallback((key, clientX, currentWidth)=>{
1229
1272
  resizeStartRef.current = {
1230
1273
  key,
1231
1274
  startX: clientX,
1232
- startWidth: currentWidth
1275
+ startWidth: currentWidth,
1276
+ currentWidth
1233
1277
  };
1234
1278
  setResizingColumnKey(key);
1235
1279
  }, []);
1236
1280
  const updateColumnResize = React.useCallback((clientX)=>{
1237
1281
  const start = resizeStartRef.current;
1238
- if (!start) return;
1239
- setColumnWidth(start.key, start.startWidth + (clientX - start.startX));
1240
- }, [
1241
- setColumnWidth
1242
- ]);
1282
+ if (!start) return undefined;
1283
+ const width = clampColumnWidth(start.startWidth + (clientX - start.startX));
1284
+ start.currentWidth = width;
1285
+ return width;
1286
+ }, []);
1243
1287
  const endColumnResize = React.useCallback(()=>{
1288
+ const start = resizeStartRef.current;
1289
+ if (start) setColumnWidth(start.key, start.currentWidth);
1244
1290
  resizeStartRef.current = null;
1245
1291
  setResizingColumnKey(null);
1246
- }, []);
1292
+ }, [
1293
+ setColumnWidth
1294
+ ]);
1247
1295
  return {
1248
1296
  columnOrder,
1249
1297
  visibleColumnOrder,
@@ -1593,11 +1641,16 @@ function useDataTableSort(props) {
1593
1641
  const [sort, setSort] = useControllableState$1({
1594
1642
  prop: sortProp,
1595
1643
  defaultProp: defaultSort,
1596
- onChange: (nextSort)=>{
1597
- // `toggleSort` never sets `sort` back to `undefined`, but the underlying state is
1598
- // typed as possibly-undefined so an uncontrolled table can start with no sort at all.
1599
- if (nextSort) onSortChange?.(nextSort);
1600
- }
1644
+ // Live-review fix: this used to only fire for a DEFINED next sort ("`toggleSort`
1645
+ // never clears it, so why would this ever see `undefined`") -- true until
1646
+ // `clearSort` (below) existed. Guarding the callback on truthiness meant a
1647
+ // consumer controlling `sort` had no way to ever learn the column sort was
1648
+ // cleared: `setSort(undefined)` would run, but the controlled VALUE never moves
1649
+ // without the consumer's own state updating from this callback, and the guard
1650
+ // swallowed exactly the call that would have told it to. Every other value this
1651
+ // hook contract carries (`onColumnWidthsChange`, `onColumnCalcOpsChange`, ...)
1652
+ // already passes its "cleared" shape through unconditionally; this now matches.
1653
+ onChange: onSortChange
1601
1654
  });
1602
1655
  const toggleSort = React.useCallback((key)=>{
1603
1656
  setSort((current)=>{
@@ -1613,6 +1666,12 @@ function useDataTableSort(props) {
1613
1666
  }, [
1614
1667
  setSort
1615
1668
  ]);
1669
+ /** Attio/GRC's "Remove sort" -- the header click cycle (above) only ever toggles
1670
+ * asc/desc once a column has been clicked once; this is the only way back to no
1671
+ * sort at all. Exposed for a column's own header menu (`column-menu.tsx`'s
1672
+ * `ColumnMenu`) to offer as a "Clear sort" item when that column is the sorted one. */ const clearSort = React.useCallback(()=>setSort(undefined), [
1673
+ setSort
1674
+ ]);
1616
1675
  const getHeaderCellProps = React.useCallback((columnKey, sortable)=>{
1617
1676
  const isSorted = Boolean(columnKey) && sort?.key === columnKey;
1618
1677
  const canSort = Boolean(sortable && columnKey);
@@ -1642,6 +1701,7 @@ function useDataTableSort(props) {
1642
1701
  return {
1643
1702
  sort,
1644
1703
  toggleSort,
1704
+ clearSort,
1645
1705
  getHeaderCellProps
1646
1706
  };
1647
1707
  }
@@ -4163,7 +4223,7 @@ function formatCalcValue(value) {
4163
4223
  ]);
4164
4224
  const ops = [
4165
4225
  ...DATA_TABLE_UNIVERSAL_CALC_OPS,
4166
- ...numeric ? DATA_TABLE_NUMERIC_CALC_OPS : []
4226
+ ...numeric ? DATA_TABLE_NUMERIC_CALC_OPS : DATA_TABLE_NON_NUMERIC_CALC_OPS
4167
4227
  ];
4168
4228
  return /*#__PURE__*/ jsxRuntime.jsxs(MenuRoot, {
4169
4229
  children: [
@@ -4229,8 +4289,32 @@ function formatCalcValue(value) {
4229
4289
  * once captured, this SAME element keeps receiving `pointermove`/`pointerup` even
4230
4290
  * after the cursor leaves it, so the drag math can live entirely in this one
4231
4291
  * handler set with no effect/cleanup to get wrong.
4292
+ *
4293
+ * Live-review fix (stutter): `updateColumnResize` used to also commit the new width
4294
+ * to React state on every `pointermove` -- a full DataTable re-render per pixel of
4295
+ * drag, confirmed as the stutter. During the drag this component now writes the
4296
+ * computed width straight to the dragged column's own `<col>` element (found once,
4297
+ * at pointerdown, via its `data-column-key`, per `column-layout`'s `<colgroup>`
4298
+ * comment: a `<col>`'s own inline `width` is the SINGLE place table layout reads a
4299
+ * column's width from, so writing it directly reflows the table with no React
4300
+ * render at all), coalesced to at most one write per animation frame so a fast
4301
+ * mouse doesn't queue more layout work than the screen can show. React state (and
4302
+ * a controlled consumer's `onColumnWidthsChange`) is only touched ONCE more, in
4303
+ * `endColumnResize`, which is unchanged: it commits whatever `updateColumnResize`
4304
+ * last computed.
4232
4305
  */ function ColumnResizeHandle({ columnKey, width }) {
4233
4306
  const { beginColumnResize, updateColumnResize, endColumnResize, isColumnResizing } = useDataTableContext();
4307
+ // Survive across pointermove events without a re-render: the `<col>` this drag
4308
+ // writes to (resolved once, at pointerdown) and the rAF coalescing the writes.
4309
+ const colElementRef = React.useRef(null);
4310
+ const pendingWidthRef = React.useRef(null);
4311
+ const rafIdRef = React.useRef(null);
4312
+ const flushPendingWidth = ()=>{
4313
+ rafIdRef.current = null;
4314
+ const col = colElementRef.current;
4315
+ const pendingWidth = pendingWidthRef.current;
4316
+ if (col && pendingWidth != null) col.style.width = `${pendingWidth}px`;
4317
+ };
4234
4318
  return /*#__PURE__*/ jsxRuntime.jsx("span", {
4235
4319
  // Mouse/pointer-drag only for now (no keyboard equivalent yet) -- `aria-hidden`
4236
4320
  // rather than `role="separator"` + `aria-label`, which would otherwise fold
@@ -4246,16 +4330,32 @@ function formatCalcValue(value) {
4246
4330
  event.stopPropagation();
4247
4331
  event.preventDefault();
4248
4332
  event.currentTarget.setPointerCapture(event.pointerId);
4249
- const startWidth = width ?? event.currentTarget.closest('[role="columnheader"]')?.getBoundingClientRect().width ?? 150;
4333
+ const headerCell = event.currentTarget.closest('[role="columnheader"]');
4334
+ const startWidth = width ?? headerCell?.getBoundingClientRect().width ?? 150;
4335
+ colElementRef.current = event.currentTarget.closest("table")?.querySelector(`col[data-column-key="${CSS.escape(columnKey)}"]`) ?? null;
4250
4336
  beginColumnResize(columnKey, event.clientX, startWidth);
4251
4337
  },
4252
4338
  onPointerMove: (event)=>{
4253
- if (event.currentTarget.hasPointerCapture(event.pointerId)) {
4254
- updateColumnResize(event.clientX);
4339
+ if (!event.currentTarget.hasPointerCapture(event.pointerId)) return;
4340
+ const nextWidth = updateColumnResize(event.clientX);
4341
+ if (nextWidth == null) return;
4342
+ pendingWidthRef.current = nextWidth;
4343
+ if (rafIdRef.current == null) {
4344
+ rafIdRef.current = requestAnimationFrame(flushPendingWidth);
4255
4345
  }
4256
4346
  },
4257
4347
  onPointerUp: (event)=>{
4258
4348
  event.currentTarget.releasePointerCapture(event.pointerId);
4349
+ if (rafIdRef.current != null) {
4350
+ cancelAnimationFrame(rafIdRef.current);
4351
+ rafIdRef.current = null;
4352
+ }
4353
+ // Apply any not-yet-flushed width immediately so the `<col>` matches the
4354
+ // exact value `endColumnResize` is about to commit to React state -- with
4355
+ // no more pointermoves coming, nothing else would flush it.
4356
+ flushPendingWidth();
4357
+ pendingWidthRef.current = null;
4358
+ colElementRef.current = null;
4259
4359
  endColumnResize();
4260
4360
  },
4261
4361
  children: /*#__PURE__*/ jsxRuntime.jsx("span", {
@@ -4446,68 +4546,100 @@ function ColumnRenameInput({ column, initialLabel, onDone }) {
4446
4546
  * permanently, so this one carries its own marker attribute distinct from that
4447
4547
  * slot's OTHER occupants (the sort chevron, the filter trigger), which stay visible
4448
4548
  * under their own separate rules regardless of hover. */ function ColumnMenu({ column, onRename }) {
4449
- const { moveColumn, hideColumn, isColumnPinned, togglePinColumn } = useDataTableContext();
4549
+ const { moveColumn, hideColumn, isColumnPinned, togglePinColumn, sort, clearSort } = useDataTableContext();
4450
4550
  const columnLabel = typeof column.header === "string" ? column.header : column.key;
4451
4551
  const pinned = isColumnPinned(column.key);
4552
+ // Attio/GRC's "Remove sort" (live-review fix D) -- the header's own click-to-sort
4553
+ // (`SortIndicator`/`toggleSort`, above) only cycles asc/desc once a column has been
4554
+ // clicked; this is the only affordance back to no sort at all, so it only makes
4555
+ // sense to show on the column that IS currently sorted.
4556
+ const isSorted = sort?.key === column.key;
4452
4557
  const stopPropagation = (event)=>event.stopPropagation();
4453
- return /*#__PURE__*/ jsxRuntime.jsxs(MenuRoot, {
4454
- children: [
4455
- /*#__PURE__*/ jsxRuntime.jsx(DataTableHeaderCellIcon, {
4456
- asChild: true,
4457
- "data-column-menu": "",
4458
- onClick: stopPropagation,
4459
- onKeyDown: stopPropagation,
4460
- draggable: false,
4461
- children: /*#__PURE__*/ jsxRuntime.jsx(MenuTrigger, {
4462
- "aria-label": `${columnLabel} column menu`,
4463
- children: /*#__PURE__*/ jsxRuntime.jsx(IconDot3HorizontalLine_default, {
4464
- size: 16
4558
+ return(// Live-review fix (D): a `MenuItem` inside `MenuPositioner` renders through a
4559
+ // `FloatingPortal` -- a REAL React portal (`ReactDOM.createPortal`), which
4560
+ // bubbles a click/keydown up through the REACT TREE (this component, then the
4561
+ // columnheader that renders it), NOT the DOM tree the portal actually appends
4562
+ // to. Confirmed live: clicking ANY item here (not just "Clear sort" -- "Move
4563
+ // left" on a column with no sort at all reproduced it too) also fired the
4564
+ // COLUMNHEADER's own `toggleSort`, because nothing stopped that bubble before
4565
+ // it reached the header's `onClick`/`onKeyDown` (`getHeaderCellProps`). The
4566
+ // trigger's own `stopPropagation` (on `DataTableHeaderCellIcon` below) only
4567
+ // covers clicks that start on the trigger itself -- a SIBLING of
4568
+ // `MenuPositioner` in this tree, not an ancestor of it, so it never sees a
4569
+ // menu-item event at all. This wrapper is a genuine DOM node sitting between
4570
+ // BOTH of them and the header, so it catches the bubble from either one;
4571
+ // `display: contents` keeps it invisible to `DataTableHeaderCellInner`'s own
4572
+ // flex layout.
4573
+ // 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.
4574
+ /*#__PURE__*/ jsxRuntime.jsx("span", {
4575
+ onClick: stopPropagation,
4576
+ onKeyDown: stopPropagation,
4577
+ style: {
4578
+ display: "contents"
4579
+ },
4580
+ children: /*#__PURE__*/ jsxRuntime.jsxs(MenuRoot, {
4581
+ children: [
4582
+ /*#__PURE__*/ jsxRuntime.jsx(DataTableHeaderCellIcon, {
4583
+ asChild: true,
4584
+ "data-column-menu": "",
4585
+ draggable: false,
4586
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuTrigger, {
4587
+ "aria-label": `${columnLabel} column menu`,
4588
+ children: /*#__PURE__*/ jsxRuntime.jsx(IconDot3HorizontalLine_default, {
4589
+ size: 16
4590
+ })
4465
4591
  })
4466
- })
4467
- }),
4468
- /*#__PURE__*/ jsxRuntime.jsx(MenuPositioner, {
4469
- children: /*#__PURE__*/ jsxRuntime.jsx(MenuContent, {
4470
- style: {
4471
- minWidth: 160
4472
- },
4473
- children: /*#__PURE__*/ jsxRuntime.jsxs(MenuScrollArea, {
4474
- children: [
4475
- /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
4476
- onClick: onRename,
4477
- children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
4478
- children: "Rename"
4479
- })
4480
- }),
4481
- /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
4482
- onClick: ()=>moveColumn(column.key, "left"),
4483
- children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
4484
- children: "Move left"
4485
- })
4486
- }),
4487
- /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
4488
- onClick: ()=>moveColumn(column.key, "right"),
4489
- children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
4490
- children: "Move right"
4491
- })
4492
- }),
4493
- /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
4494
- onClick: ()=>togglePinColumn(column.key),
4495
- children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
4496
- children: pinned ? "Unpin column" : "Pin column"
4497
- })
4498
- }),
4499
- /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
4500
- onClick: ()=>hideColumn(column.key),
4501
- children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
4502
- children: "Hide column"
4592
+ }),
4593
+ /*#__PURE__*/ jsxRuntime.jsx(MenuPositioner, {
4594
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuContent, {
4595
+ style: {
4596
+ minWidth: 160
4597
+ },
4598
+ children: /*#__PURE__*/ jsxRuntime.jsxs(MenuScrollArea, {
4599
+ children: [
4600
+ /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
4601
+ onClick: onRename,
4602
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
4603
+ children: "Rename"
4604
+ })
4605
+ }),
4606
+ /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
4607
+ onClick: ()=>moveColumn(column.key, "left"),
4608
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
4609
+ children: "Move left"
4610
+ })
4611
+ }),
4612
+ /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
4613
+ onClick: ()=>moveColumn(column.key, "right"),
4614
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
4615
+ children: "Move right"
4616
+ })
4617
+ }),
4618
+ isSorted && /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
4619
+ onClick: clearSort,
4620
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
4621
+ children: "Clear sort"
4622
+ })
4623
+ }),
4624
+ /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
4625
+ onClick: ()=>togglePinColumn(column.key),
4626
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
4627
+ children: pinned ? "Unpin column" : "Pin column"
4628
+ })
4629
+ }),
4630
+ /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
4631
+ onClick: ()=>hideColumn(column.key),
4632
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
4633
+ children: "Hide column"
4634
+ })
4503
4635
  })
4504
- })
4505
- ]
4636
+ ]
4637
+ })
4506
4638
  })
4507
4639
  })
4508
- })
4509
- ]
4510
- });
4640
+ ]
4641
+ })
4642
+ }));
4511
4643
  }
4512
4644
 
4513
4645
  /**
@@ -10224,6 +10356,25 @@ function DataTableInner(props, ref) {
10224
10356
  // column/direction is active. A consumer that pre-sorts server-side can pass
10225
10357
  // a controlled `sort`; this still sorts the rows it's given, which for
10226
10358
  // already-sorted data is a no-op.
10359
+ //
10360
+ // Live-review fix (D, "Clear sort"): the JSX below passes the RAW `sort` prop
10361
+ // (not `resolvedSort`) on to `DataTablePrimitiveRoot`, plus `defaultSort` --
10362
+ // passing `resolvedSort` used to feed the headless `useDataTableSort`'s own
10363
+ // `useControllableState` a value that starts `undefined` (uncontrolled) and
10364
+ // BECOMES defined the instant a header is first clicked (once `internalSort`
10365
+ // picks it up), flipping that inner hook from uncontrolled to controlled
10366
+ // mid-lifecycle -- `@radix-ui/react-use-controllable-state` warns about
10367
+ // exactly this and, worse, silently misbehaves on it: its own internal
10368
+ // uncontrolled state stops updating the moment it's controlled, so when
10369
+ // `clearSort` later drove `resolvedSort` back to `undefined` (flipping BACK
10370
+ // to uncontrolled), the hook fell through to that stale, never-updated
10371
+ // internal state instead -- still the pre-clear value, confirmed live
10372
+ // (`aria-sort` stayed "ascending" after "Clear sort"). The RAW `sort` prop
10373
+ // never has this problem: for the common uncontrolled case it's simply
10374
+ // always `undefined`, so the headless hook is uncontrolled for its entire
10375
+ // lifetime and owns its state correctly; a consumer that genuinely controls
10376
+ // `sort` behaves exactly as before (`resolvedSort === sort` whenever `sort`
10377
+ // is defined, so nothing changes for that path).
10227
10378
  const [internalSort, setInternalSort] = React__namespace.useState(defaultSort);
10228
10379
  const resolvedSort = sort !== undefined ? sort : internalSort;
10229
10380
  const handleSortChange = React__namespace.useCallback((next)=>{
@@ -10329,7 +10480,8 @@ function DataTableInner(props, ref) {
10329
10480
  selectedIds: selectedIds,
10330
10481
  defaultSelectedIds: defaultSelectedIds,
10331
10482
  onSelectionChange: onSelectionChange,
10332
- sort: resolvedSort,
10483
+ sort: sort,
10484
+ defaultSort: defaultSort,
10333
10485
  onSortChange: handleSortChange,
10334
10486
  query: resolvedQuery,
10335
10487
  onQueryChange: handleQueryChange,
@@ -10592,6 +10744,10 @@ const DataTableTable = withContext$1(DataTableTable$1, "table");
10592
10744
  }
10593
10745
  }),
10594
10746
  effective.map(({ column, width })=>/*#__PURE__*/ jsxRuntime.jsx("col", {
10747
+ // Read by `resize.tsx`'s own pointermove handler (`data-column-key`,
10748
+ // not a ref) to write the dragged width straight to this element
10749
+ // during a drag -- see that file's own comment for why.
10750
+ "data-column-key": column.key,
10595
10751
  style: width === undefined ? undefined : {
10596
10752
  width
10597
10753
  }
@@ -11020,7 +11176,7 @@ DataTableViewsControl.displayName = "DataTableViewsControl";
11020
11176
  });
11021
11177
  }
11022
11178
 
11023
- __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:fixed;top:8px;bottom:8px;border-inline-end:none;border-inline-start:none;border-radius:10px;box-shadow:inset 0 0 0 1px transparent,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;transition:transform .14s,padding .14s}.seed-resizable-panel__panel[data-floating=true][data-side=left]{padding:8px 0 8px 8px}[data-floating=true]>.seed-resizable-panel__inner{transition:opacity .14s}.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}");
11179
+ __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}");
11024
11180
 
11025
11181
  const resizablePanelSlotNames = [
11026
11182
  [
@@ -11224,16 +11380,26 @@ Object.assign(resizablePanel, {
11224
11380
  // selector), so this component and that recipe stay in lockstep without a
11225
11381
  // second source of truth for the string.
11226
11382
  const COLUMN_HANDLE_CLASS = "crisp-seed-resizable-panel-handle--column";
11383
+ // Kept in lock-step with `resizable-panel.ts`'s own `FLOATING_GUTTER` (a
11384
+ // literal there too, for the same reason) -- the floating/inset-card variant
11385
+ // needs an outward-facing gutter that DOCKED never reserves. Read on at
11386
+ // `spacer`'s own render below for why this can't just be one more CSS rule.
11387
+ const FLOATING_GUTTER_PX = 8;
11227
11388
  /**
11228
11389
  * An adjustable-panel primitive: a spacer that reserves layout width plus a
11229
- * panel that overlays it, so the panel can move between docked (in-flow) and
11230
- * floating (`position: fixed`, an overlay) without a reflow glitch. Drag the
11231
- * edge handle to resize; constraints are CSS `min-width`/`max-width`, never
11232
- * re-clamped in JS. Ported from your-moon/crisp's `ResizablePanel`
11233
- * (`components/resizablepanel/`); state/drag behavior lives in
11234
- * `useResizablePanel` (`@seed-design/react-resizable-panel`), this component
11235
- * owns the DOM + recipe classes on top of it.
11236
- */ const ResizablePanel = /*#__PURE__*/ React.forwardRef(({ side = "left", width: sizeProp, defaultSize = 280, onWidthChange, min = 200, max, floating = false, handleVariant = "panel", resizable = true, storageKey, className, children, ...rest }, forwardedRef)=>{
11390
+ * panel that overlays it, so it can resize (or animate open/closed) without
11391
+ * a reflow glitch on the rest of the layout -- the spacer always reserves
11392
+ * the panel's real width, in BOTH the docked and floating visual variants
11393
+ * (floating changes only the panel's own look -- gutter, radius, shadow --
11394
+ * never its `position`, so a consumer laying this out as a row-flex sibling
11395
+ * of e.g. a table sees that table genuinely shrink to make room, matching
11396
+ * Attio's own measured peek). Drag the edge handle to resize; constraints
11397
+ * are CSS `min-width`/`max-width`, never re-clamped in JS. Ported from
11398
+ * your-moon/crisp's `ResizablePanel` (`components/resizablepanel/`);
11399
+ * state/drag behavior lives in `useResizablePanel`
11400
+ * (`@seed-design/react-resizable-panel`), this component owns the DOM +
11401
+ * recipe classes on top of it.
11402
+ */ const ResizablePanel = /*#__PURE__*/ React.forwardRef(({ side = "left", width: sizeProp, defaultSize = 280, onWidthChange, min = 200, max, floating = false, handleVariant = "panel", resizable = true, storageKey, animationState, panelElementRef, className, children, ...rest }, forwardedRef)=>{
11237
11403
  const { width, dragging, containerRef, panelRef, handleHandlers } = useResizablePanel({
11238
11404
  side,
11239
11405
  size: sizeProp,
@@ -11241,9 +11407,29 @@ const COLUMN_HANDLE_CLASS = "crisp-seed-resizable-panel-handle--column";
11241
11407
  onSizeChange: onWidthChange,
11242
11408
  storageKey
11243
11409
  });
11410
+ const composedPanelRef = useComposedRefs(panelRef, panelElementRef);
11244
11411
  const classNames = resizablePanel();
11412
+ // The floating/inset-card variant sits INSET from `root`'s own outward
11413
+ // edge by `FLOATING_GUTTER_PX`, flush against the inward edge (Attio's
11414
+ // own peek: no gap on the side facing the table it shrinks). `width`
11415
+ // (the prop a consumer sets, e.g. `RecordPreview`'s own 497px default)
11416
+ // is the RESERVED FOOTPRINT -- what the table beside it actually gives
11417
+ // up -- so the panel's own rendered card shrinks by the gutter INSIDE
11418
+ // that footprint, rather than the footprint growing past it: reserving
11419
+ // the panel's full width AND adding an outward gutter on top of it
11420
+ // over-claims space the surrounding layout was never asked for --
11421
+ // confirmed live, it pushed the whole page 8px into horizontal
11422
+ // overflow. Docked (no gutter) keeps `width` as both the reservation
11423
+ // and the render, as before.
11424
+ const panelGutter = floating ? FLOATING_GUTTER_PX : 0;
11245
11425
  const maxStyle = max == null ? undefined : typeof max === "number" ? `${max}px` : max;
11426
+ const panelMaxStyle = panelGutter === 0 || maxStyle == null ? maxStyle : `calc(${maxStyle} - ${panelGutter}px)`;
11246
11427
  const panelSizeStyle = {
11428
+ width: width - panelGutter,
11429
+ minWidth: min - panelGutter,
11430
+ maxWidth: panelMaxStyle
11431
+ };
11432
+ const spacerSizeStyle = {
11247
11433
  width,
11248
11434
  minWidth: min,
11249
11435
  maxWidth: maxStyle
@@ -11259,19 +11445,14 @@ const COLUMN_HANDLE_CLASS = "crisp-seed-resizable-panel-handle--column";
11259
11445
  children: [
11260
11446
  /*#__PURE__*/ jsxRuntime.jsx(Primitive.div, {
11261
11447
  className: classNames.spacer,
11262
- style: floating ? {
11263
- width: 0
11264
- } : {
11265
- width,
11266
- minWidth: min,
11267
- maxWidth: maxStyle
11268
- }
11448
+ style: spacerSizeStyle
11269
11449
  }),
11270
11450
  /*#__PURE__*/ jsxRuntime.jsxs(Primitive.div, {
11271
- ref: panelRef,
11451
+ ref: composedPanelRef,
11272
11452
  className: classNames.panel,
11273
11453
  "data-side": side,
11274
11454
  "data-floating": floating ? "true" : "false",
11455
+ "data-state": animationState,
11275
11456
  style: panelSizeStyle,
11276
11457
  children: [
11277
11458
  /*#__PURE__*/ jsxRuntime.jsx(Primitive.div, {
@@ -11296,132 +11477,276 @@ const COLUMN_HANDLE_CLASS = "crisp-seed-resizable-panel-handle--column";
11296
11477
  });
11297
11478
  ResizablePanel.displayName = "ResizablePanel";
11298
11479
 
11299
- __insertCSS(".seed-record-panel__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-panel__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-panel__toolbarSpacer{flex:1}.seed-record-panel__pager{display:flex;align-items:center;gap:6px;margin-inline-start:12px}.seed-record-panel__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-panel__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-panel__iconbtn:hover{background:var(--seed-color-bg-layer-fill);color:var(--seed-color-fg-neutral)}.seed-record-panel__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-panel__navbtn:hover:not(:disabled){background:var(--seed-color-bg-layer-fill)}.seed-record-panel__navbtn:disabled{opacity:.4;cursor:not-allowed}.seed-record-panel__header{display:flex;align-items:center;gap:8px;height:52px;flex-shrink:0;padding:0 12px}.seed-record-panel__fav{width:22px;height:22px;border-radius:4px;flex-shrink:0;object-fit:cover;background:var(--seed-color-bg-layer-fill)}.seed-record-panel__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-panel__body{flex:1;min-height:0;overflow-y:auto;padding:8px 12px}.seed-record-panel__section{padding:8px 4px 0}.seed-record-panel__sectionHead{display:flex;align-items:center;justify-content:space-between;gap:8px;height:20px;margin-bottom:8px}.seed-record-panel__sectionLabel{font-size:14px;font-weight:500;color:var(--seed-color-fg-neutral-subtle)}.seed-record-panel__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-panel__sectionAction:hover{color:var(--seed-color-fg-neutral)}.seed-record-panel__highlights{display:grid;grid-template-columns:1fr 1fr;gap:8px}.seed-record-panel__highlight{display:flex;flex-direction:column;gap:6px;min-width:0}.seed-record-panel__highlightLabel{display:flex;align-items:center;gap:4px;font-size:12px;font-weight:500;color:var(--seed-color-fg-neutral-subtle)}.seed-record-panel__highlightLabel span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.seed-record-panel__highlightLabel svg{flex-shrink:0;color:var(--seed-color-fg-neutral-subtle)}.seed-record-panel__highlightValue{font-size:14px;font-weight:500;color:var(--seed-color-fg-neutral);overflow:hidden;text-overflow:ellipsis}.seed-record-panel__activity{font-size:14px}.seed-record-panel__sections{display:flex;flex-direction:column;padding-top:8px}.seed-record-panel__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-panel__sectionTrigger[data-inert]{cursor:default}.seed-record-panel__sectionRowLabel{font-size:14px;font-weight:500;color:var(--seed-color-fg-neutral)}.seed-record-panel__sectionRowCount{font-size:14px;color:var(--seed-color-fg-neutral-subtle)}.seed-record-panel__sectionChevron{margin-inline-start:auto;flex-shrink:0;color:var(--seed-color-fg-neutral-subtle);transition:transform .15s ease}.seed-record-panel__sectionChevron[data-open]{transform:rotate(180deg)}.seed-record-panel__sectionContent{padding:8px 8px 8px 12px}.seed-record-panel__footer{flex-shrink:0;display:flex;align-items:center;gap:8px;padding:8px 12px;border-top:1px solid var(--seed-color-stroke-neutral-muted)}");
11480
+ __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)}");
11300
11481
 
11301
- const recordPanelSlotNames = [
11482
+ const recordPreviewSlotNames = [
11302
11483
  [
11303
11484
  "root",
11304
- "seed-record-panel__root"
11485
+ "seed-record-preview__root"
11305
11486
  ],
11306
11487
  [
11307
11488
  "toolbar",
11308
- "seed-record-panel__toolbar"
11489
+ "seed-record-preview__toolbar"
11309
11490
  ],
11310
11491
  [
11311
11492
  "toolbarSpacer",
11312
- "seed-record-panel__toolbarSpacer"
11493
+ "seed-record-preview__toolbarSpacer"
11313
11494
  ],
11314
11495
  [
11315
11496
  "pager",
11316
- "seed-record-panel__pager"
11497
+ "seed-record-preview__pager"
11317
11498
  ],
11318
11499
  [
11319
11500
  "counter",
11320
- "seed-record-panel__counter"
11501
+ "seed-record-preview__counter"
11321
11502
  ],
11322
11503
  [
11323
11504
  "iconbtn",
11324
- "seed-record-panel__iconbtn"
11505
+ "seed-record-preview__iconbtn"
11325
11506
  ],
11326
11507
  [
11327
11508
  "navbtn",
11328
- "seed-record-panel__navbtn"
11509
+ "seed-record-preview__navbtn"
11329
11510
  ],
11330
11511
  [
11331
11512
  "header",
11332
- "seed-record-panel__header"
11513
+ "seed-record-preview__header"
11333
11514
  ],
11334
11515
  [
11335
11516
  "fav",
11336
- "seed-record-panel__fav"
11517
+ "seed-record-preview__fav"
11337
11518
  ],
11338
11519
  [
11339
11520
  "title",
11340
- "seed-record-panel__title"
11521
+ "seed-record-preview__title"
11341
11522
  ],
11342
11523
  [
11343
11524
  "body",
11344
- "seed-record-panel__body"
11525
+ "seed-record-preview__body"
11345
11526
  ],
11346
11527
  [
11347
11528
  "section",
11348
- "seed-record-panel__section"
11529
+ "seed-record-preview__section"
11349
11530
  ],
11350
11531
  [
11351
11532
  "sectionHead",
11352
- "seed-record-panel__sectionHead"
11533
+ "seed-record-preview__sectionHead"
11353
11534
  ],
11354
11535
  [
11355
11536
  "sectionLabel",
11356
- "seed-record-panel__sectionLabel"
11537
+ "seed-record-preview__sectionLabel"
11357
11538
  ],
11358
11539
  [
11359
11540
  "sectionAction",
11360
- "seed-record-panel__sectionAction"
11541
+ "seed-record-preview__sectionAction"
11361
11542
  ],
11362
11543
  [
11363
11544
  "highlights",
11364
- "seed-record-panel__highlights"
11545
+ "seed-record-preview__highlights"
11365
11546
  ],
11366
11547
  [
11367
11548
  "highlight",
11368
- "seed-record-panel__highlight"
11549
+ "seed-record-preview__highlight"
11369
11550
  ],
11370
11551
  [
11371
11552
  "highlightLabel",
11372
- "seed-record-panel__highlightLabel"
11553
+ "seed-record-preview__highlightLabel"
11373
11554
  ],
11374
11555
  [
11375
11556
  "highlightValue",
11376
- "seed-record-panel__highlightValue"
11557
+ "seed-record-preview__highlightValue"
11377
11558
  ],
11378
11559
  [
11379
11560
  "activity",
11380
- "seed-record-panel__activity"
11561
+ "seed-record-preview__activity"
11381
11562
  ],
11382
11563
  [
11383
11564
  "sections",
11384
- "seed-record-panel__sections"
11565
+ "seed-record-preview__sections"
11385
11566
  ],
11386
11567
  [
11387
11568
  "sectionTrigger",
11388
- "seed-record-panel__sectionTrigger"
11569
+ "seed-record-preview__sectionTrigger"
11389
11570
  ],
11390
11571
  [
11391
11572
  "sectionRowLabel",
11392
- "seed-record-panel__sectionRowLabel"
11573
+ "seed-record-preview__sectionRowLabel"
11393
11574
  ],
11394
11575
  [
11395
11576
  "sectionRowCount",
11396
- "seed-record-panel__sectionRowCount"
11577
+ "seed-record-preview__sectionRowCount"
11397
11578
  ],
11398
11579
  [
11399
11580
  "sectionChevron",
11400
- "seed-record-panel__sectionChevron"
11581
+ "seed-record-preview__sectionChevron"
11401
11582
  ],
11402
11583
  [
11403
11584
  "sectionContent",
11404
- "seed-record-panel__sectionContent"
11585
+ "seed-record-preview__sectionContent"
11405
11586
  ],
11406
11587
  [
11407
11588
  "footer",
11408
- "seed-record-panel__footer"
11589
+ "seed-record-preview__footer"
11409
11590
  ]
11410
11591
  ];
11411
11592
  const defaultVariant = {};
11412
11593
  const compoundVariants = [];
11413
- const recordPanelVariantMap = {};
11414
- function recordPanel(props) {
11415
- return Object.fromEntries(recordPanelSlotNames.map(([slot, className])=>{
11594
+ const recordPreviewVariantMap = {};
11595
+ function recordPreview(props) {
11596
+ return Object.fromEntries(recordPreviewSlotNames.map(([slot, className])=>{
11416
11597
  return [
11417
11598
  slot,
11418
11599
  createClassName(className, mergeVariants(defaultVariant, props), compoundVariants)
11419
11600
  ];
11420
11601
  }));
11421
11602
  }
11422
- Object.assign(recordPanel, {
11423
- splitVariantProps: (props)=>splitVariantProps(props, recordPanelVariantMap)
11424
- }); // @recipe(seed): record-panel
11603
+ Object.assign(recordPreview, {
11604
+ splitVariantProps: (props)=>splitVariantProps(props, recordPreviewVariantMap)
11605
+ }); // @recipe(seed): record-preview
11606
+
11607
+ // This code includes portions derived from radix-ui/primitives (https://github.com/radix-ui/primitives)
11608
+ // Used under the MIT License: https://opensource.org/licenses/MIT
11609
+ function usePresence(present) {
11610
+ const [node, setNode] = React__namespace.useState();
11611
+ const stylesRef = React__namespace.useRef(null);
11612
+ const prevPresentRef = React__namespace.useRef(present);
11613
+ const prevAnimationNameRef = React__namespace.useRef("none");
11614
+ const initialState = present ? "mounted" : "unmounted";
11615
+ const [state, send] = useStateMachine(initialState, {
11616
+ mounted: {
11617
+ UNMOUNT: "unmounted",
11618
+ ANIMATION_OUT: "unmountSuspended"
11619
+ },
11620
+ unmountSuspended: {
11621
+ MOUNT: "mounted",
11622
+ ANIMATION_END: "unmounted"
11623
+ },
11624
+ unmounted: {
11625
+ MOUNT: "mounted"
11626
+ }
11627
+ });
11628
+ React__namespace.useEffect(()=>{
11629
+ const currentAnimationName = getAnimationName(stylesRef.current);
11630
+ prevAnimationNameRef.current = state === "mounted" ? currentAnimationName : "none";
11631
+ }, [
11632
+ state
11633
+ ]);
11634
+ useLayoutEffect2(()=>{
11635
+ const styles = stylesRef.current;
11636
+ const wasPresent = prevPresentRef.current;
11637
+ const hasPresentChanged = wasPresent !== present;
11638
+ if (hasPresentChanged) {
11639
+ const prevAnimationName = prevAnimationNameRef.current;
11640
+ const currentAnimationName = getAnimationName(styles);
11641
+ if (present) {
11642
+ send("MOUNT");
11643
+ } else if (currentAnimationName === "none" || styles?.display === "none") {
11644
+ // If there is no exit animation or the element is hidden, animations won't run
11645
+ // so we unmount instantly
11646
+ send("UNMOUNT");
11647
+ } else {
11648
+ /**
11649
+ * When `present` changes to `false`, we check changes to animation-name to
11650
+ * determine whether an animation has started. We chose this approach (reading
11651
+ * computed styles) because there is no `animationrun` event and `animationstart`
11652
+ * fires after `animation-delay` has expired which would be too late.
11653
+ */ const isAnimating = prevAnimationName !== currentAnimationName;
11654
+ if (wasPresent && isAnimating) {
11655
+ send("ANIMATION_OUT");
11656
+ } else {
11657
+ send("UNMOUNT");
11658
+ }
11659
+ }
11660
+ prevPresentRef.current = present;
11661
+ }
11662
+ }, [
11663
+ present,
11664
+ send
11665
+ ]);
11666
+ useLayoutEffect2(()=>{
11667
+ if (node) {
11668
+ let timeoutId;
11669
+ const ownerWindow = node.ownerDocument.defaultView ?? window;
11670
+ /**
11671
+ * Triggering an ANIMATION_OUT during an ANIMATION_IN will fire an `animationcancel`
11672
+ * event for ANIMATION_IN after we have entered `unmountSuspended` state. So, we
11673
+ * make sure we only trigger ANIMATION_END for the currently active animation.
11674
+ */ const handleAnimationEnd = (event)=>{
11675
+ const currentAnimationName = getAnimationName(stylesRef.current);
11676
+ const isCurrentAnimation = currentAnimationName.includes(event.animationName);
11677
+ if (event.target === node && isCurrentAnimation) {
11678
+ // With React 18 concurrency this update is applied a frame after the
11679
+ // animation ends, creating a flash of visible content. By setting the
11680
+ // animation fill mode to "forwards", we force the node to keep the
11681
+ // styles of the last keyframe, removing the flash.
11682
+ //
11683
+ // Previously we flushed the update via ReactDom.flushSync, but with
11684
+ // exit animations this resulted in the node being removed from the
11685
+ // DOM before the synthetic animationEnd event was dispatched, meaning
11686
+ // user-provided event handlers would not be called.
11687
+ // https://github.com/radix-ui/primitives/pull/1849
11688
+ send("ANIMATION_END");
11689
+ if (!prevPresentRef.current) {
11690
+ const currentFillMode = node.style.animationFillMode;
11691
+ node.style.animationFillMode = "forwards";
11692
+ // Reset the style after the node had time to unmount (for cases
11693
+ // where the component chooses not to unmount). Doing this any
11694
+ // sooner than `setTimeout` (e.g. with `requestAnimationFrame`)
11695
+ // still causes a flash.
11696
+ timeoutId = ownerWindow.setTimeout(()=>{
11697
+ if (node.style.animationFillMode === "forwards") {
11698
+ node.style.animationFillMode = currentFillMode;
11699
+ }
11700
+ });
11701
+ }
11702
+ }
11703
+ };
11704
+ const handleAnimationStart = (event)=>{
11705
+ if (event.target === node) {
11706
+ // if animation occurred, store its name as the previous animation.
11707
+ prevAnimationNameRef.current = getAnimationName(stylesRef.current);
11708
+ }
11709
+ };
11710
+ node.addEventListener("animationstart", handleAnimationStart);
11711
+ node.addEventListener("animationcancel", handleAnimationEnd);
11712
+ node.addEventListener("animationend", handleAnimationEnd);
11713
+ return ()=>{
11714
+ ownerWindow.clearTimeout(timeoutId);
11715
+ node.removeEventListener("animationstart", handleAnimationStart);
11716
+ node.removeEventListener("animationcancel", handleAnimationEnd);
11717
+ node.removeEventListener("animationend", handleAnimationEnd);
11718
+ };
11719
+ } else {
11720
+ // Transition to the unmounted state if the node is removed prematurely.
11721
+ // We avoid doing so during cleanup as the node may change but still exist.
11722
+ send("ANIMATION_END");
11723
+ }
11724
+ }, [
11725
+ node,
11726
+ send
11727
+ ]);
11728
+ return {
11729
+ isPresent: [
11730
+ "mounted",
11731
+ "unmountSuspended"
11732
+ ].includes(state),
11733
+ ref: React__namespace.useCallback((node)=>{
11734
+ if (node) stylesRef.current = getComputedStyle(node);
11735
+ setNode(node ?? undefined);
11736
+ }, [])
11737
+ };
11738
+ }
11739
+ /* -----------------------------------------------------------------------------------------------*/ function getAnimationName(styles) {
11740
+ return styles?.animationName || "none";
11741
+ }
11742
+ function useStateMachine(initialState, machine) {
11743
+ return React__namespace.useReducer((state, event)=>{
11744
+ const nextState = machine[state][event];
11745
+ return nextState ?? state;
11746
+ }, initialState);
11747
+ }
11748
+ const PresenceContext = /*#__PURE__*/ React.createContext(null);
11749
+ PresenceContext.Provider;
11425
11750
 
11426
11751
  const RESIZE_MIN_WIDTH = 360;
11427
11752
  const RESIZE_MAX_WIDTH = 720;
@@ -11431,12 +11756,35 @@ const RESIZE_MAX_WIDTH = 720;
11431
11756
  * `RecordPanel` (`product/recordpanel/`), built ON `ResizablePanel`
11432
11757
  * (`side="right" floating`) rather than crisp's own duplicate absolute-
11433
11758
  * positioning/slide-in/resize-handle implementation -- the outer shell (the
11434
- * `@starting-style` slide-in, the floating elevation, the drag handle
11759
+ * enter/exit keyframe animation, the floating elevation, the drag handle
11435
11760
  * itself) all come from that primitive; this component only renders the
11436
11761
  * CONTENT inside it.
11437
- */ function RecordPanel({ open, record, onClose, className, 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 = 420, onWidthChange, minWidth = RESIZE_MIN_WIDTH, maxWidth = RESIZE_MAX_WIDTH, activityLabel = "Activity", viewAllActivityLabel = "View all", onViewAllActivity, activity, sections, footer, ...rest }) {
11762
+ *
11763
+ * Enter/exit: closing this panel is a state change (`open` -> false) that
11764
+ * typically clears `record` in the SAME update (see the DataTable story's
11765
+ * own `onClose={() => setPreviewRow(null)}`), so by the time an exit
11766
+ * animation should play, the values this component would render are
11767
+ * already gone. `usePresence` (`@seed-design/react-presence`, the same
11768
+ * idiom Dialog/Drawer already use for their own floating layers) keeps the
11769
+ * tree mounted for exactly the exit animation's duration; the snapshot
11770
+ * below keeps the LAST visible props around for that same window, since
11771
+ * without it the panel would animate out empty.
11772
+ */ function RecordPreview(props) {
11773
+ const shouldRender = props.open && props.record != null;
11774
+ const { isPresent, ref: panelElementRef } = usePresence(shouldRender);
11775
+ const lastVisiblePropsRef = React.useRef(props);
11776
+ if (shouldRender) lastVisiblePropsRef.current = props;
11777
+ if (!isPresent) return null;
11778
+ return /*#__PURE__*/ jsxRuntime.jsx(RecordPreviewPanel, {
11779
+ ...shouldRender ? props : lastVisiblePropsRef.current,
11780
+ animationState: shouldRender ? "open" : "closed",
11781
+ panelElementRef: panelElementRef
11782
+ });
11783
+ }
11784
+ RecordPreview.displayName = "RecordPreview";
11785
+ 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 }) {
11438
11786
  const titleId = React.useId();
11439
- const classNames = recordPanel();
11787
+ const classNames = recordPreview();
11440
11788
  // Which sections (your-moon/crisp#413) are expanded -- uncontrolled and
11441
11789
  // independent (Attio's own Emails/Notes/Tasks open without closing one
11442
11790
  // another): a plain `Set`, reset whenever the record itself changes so a
@@ -11454,28 +11802,31 @@ const RESIZE_MAX_WIDTH = 720;
11454
11802
  else next.add(key);
11455
11803
  return next;
11456
11804
  });
11457
- // Close on Escape while open. Simpler than crisp's own version (crisp
11458
- // additionally checks `hasOpenFloatingLayer()` so an Escape meant for a
11459
- // nested Menu/Select/Popover doesn't also close this panel) -- crispy-seed
11460
- // has no equivalent floating-stack registry to read, so a consumer nesting
11461
- // its own floating layers inside `footer`/`sections` content should stop
11462
- // propagation on its own Escape handler.
11805
+ // Close on Escape. Simpler than crisp's own version (crisp additionally
11806
+ // checks `hasOpenFloatingLayer()` so an Escape meant for a nested Menu/
11807
+ // Select/Popover doesn't also close this panel) -- crispy-seed has no
11808
+ // equivalent floating-stack registry to read, so a consumer nesting its
11809
+ // own floating layers inside `footer`/`sections` content should stop
11810
+ // propagation on its own Escape handler. This component is only ever
11811
+ // mounted (by `RecordPreview`, above) while genuinely open or animating
11812
+ // out, so there's no separate `open` flag to gate on here.
11463
11813
  React.useEffect(()=>{
11464
- if (!open) return;
11465
11814
  const onKey = (event)=>{
11466
11815
  if (event.key === "Escape") onClose();
11467
11816
  };
11468
11817
  document.addEventListener("keydown", onKey);
11469
11818
  return ()=>document.removeEventListener("keydown", onKey);
11470
11819
  }, [
11471
- open,
11472
11820
  onClose
11473
11821
  ]);
11474
- if (!open || !record) return null;
11822
+ // Defensive only -- `RecordPreview` never mounts this without a record.
11823
+ if (!record) return null;
11475
11824
  const heading = title ?? record.name ?? "Record";
11476
11825
  return /*#__PURE__*/ jsxRuntime.jsx(ResizablePanel, {
11477
11826
  side: "right",
11478
11827
  floating: true,
11828
+ animationState: animationState,
11829
+ panelElementRef: panelElementRef,
11479
11830
  resizable: resizable,
11480
11831
  width: width,
11481
11832
  defaultSize: defaultWidth,
@@ -11687,9 +12038,9 @@ const RESIZE_MAX_WIDTH = 720;
11687
12038
  })
11688
12039
  });
11689
12040
  }
11690
- RecordPanel.displayName = "RecordPanel";
12041
+ RecordPreviewPanel.displayName = "RecordPreviewPanel";
11691
12042
 
11692
12043
  exports.DataTable = DataTable;
11693
12044
  exports.DataTableViewsControl = DataTableViewsControl;
11694
- exports.RecordPanel = RecordPanel;
12045
+ exports.RecordPreview = RecordPreview;
11695
12046
  exports.ResizablePanel = ResizablePanel;