@crisp-ui-kit/crisp 0.54.10 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/cjs/components/datatable/datatable.js +249 -46
  2. package/dist/cjs/components/datatable/datatable.recipe.js +24 -0
  3. package/dist/cjs/components/resizablepanel/resizablepanel.js +10 -1
  4. package/dist/cjs/components/table/table.js +8 -1
  5. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
  6. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +176 -43
  7. package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +28 -9
  8. package/dist/cjs/index.js +0 -2
  9. package/dist/cjs/product/recordpage/column.js +19 -0
  10. package/dist/cjs/product/recordpage/index.js +6 -1
  11. package/dist/cjs/product/recordpage/recordpage.js +25 -8
  12. package/dist/cjs/product/recordpage/test-fixtures.js +55 -0
  13. package/dist/cjs/product/savedviews/savedviews.recipe.js +13 -11
  14. package/dist/datatable.css +14 -0
  15. package/dist/datatable.layered.css +14 -0
  16. package/dist/esm/components/datatable/datatable.js +250 -47
  17. package/dist/esm/components/datatable/datatable.recipe.js +24 -0
  18. package/dist/esm/components/resizablepanel/resizablepanel.js +10 -1
  19. package/dist/esm/components/table/table.js +8 -1
  20. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
  21. package/dist/esm/components/tabletoolbar/tabletoolbar.js +177 -44
  22. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +28 -9
  23. package/dist/esm/index.js +0 -2
  24. package/dist/esm/product/recordpage/column.js +16 -0
  25. package/dist/esm/product/recordpage/index.js +2 -0
  26. package/dist/esm/product/recordpage/recordpage.js +22 -5
  27. package/dist/esm/product/recordpage/test-fixtures.js +52 -0
  28. package/dist/esm/product/savedviews/savedviews.recipe.js +13 -11
  29. package/dist/savedviews.css +6 -18
  30. package/dist/savedviews.layered.css +6 -18
  31. package/dist/styles.css +43 -276
  32. package/dist/styles.layered.css +43 -276
  33. package/dist/tabletoolbar.css +23 -3
  34. package/dist/tabletoolbar.layered.css +23 -3
  35. package/dist/types/components/datatable/datatable.d.ts +30 -3
  36. package/dist/types/components/table/table.d.ts +14 -0
  37. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +26 -1
  38. package/dist/types/index.d.ts +0 -2
  39. package/dist/types/product/recordpage/column.d.ts +112 -0
  40. package/dist/types/product/recordpage/index.d.ts +2 -0
  41. package/dist/types/product/recordpage/recordpage.d.ts +1 -1
  42. package/dist/types/product/recordpage/test-fixtures.d.ts +3 -0
  43. package/dist/types/product/recordpanel/recordpanel.d.ts +1 -1
  44. package/package.json +2 -12
  45. package/dist/cjs/product/recordstable/company-logos.js +0 -29
  46. package/dist/cjs/product/recordstable/index.js +0 -13
  47. package/dist/cjs/product/recordstable/recordstable.js +0 -1357
  48. package/dist/cjs/product/recordstable/recordstable.recipe.js +0 -175
  49. package/dist/cjs/product/recordstable/recordstable.tag.js +0 -26
  50. package/dist/cjs/product/taskstable/index.js +0 -7
  51. package/dist/cjs/product/taskstable/taskstable.js +0 -27
  52. package/dist/cjs/product/taskstable/taskstable.recipe.js +0 -56
  53. package/dist/esm/product/recordstable/company-logos.js +0 -26
  54. package/dist/esm/product/recordstable/index.js +0 -3
  55. package/dist/esm/product/recordstable/recordstable.js +0 -1317
  56. package/dist/esm/product/recordstable/recordstable.recipe.js +0 -172
  57. package/dist/esm/product/recordstable/recordstable.tag.js +0 -23
  58. package/dist/esm/product/taskstable/index.js +0 -2
  59. package/dist/esm/product/taskstable/taskstable.js +0 -24
  60. package/dist/esm/product/taskstable/taskstable.recipe.js +0 -53
  61. package/dist/recordstable.css +0 -223
  62. package/dist/recordstable.d.ts +0 -7
  63. package/dist/recordstable.layered.css +0 -225
  64. package/dist/taskstable.css +0 -30
  65. package/dist/taskstable.d.ts +0 -7
  66. package/dist/taskstable.layered.css +0 -32
  67. package/dist/types/product/recordstable/company-logos.d.ts +0 -1
  68. package/dist/types/product/recordstable/index.d.ts +0 -3
  69. package/dist/types/product/recordstable/recordstable.d.ts +0 -289
  70. package/dist/types/product/recordstable/recordstable.recipe.d.ts +0 -5
  71. package/dist/types/product/recordstable/recordstable.tag.d.ts +0 -15
  72. package/dist/types/product/taskstable/index.d.ts +0 -2
  73. package/dist/types/product/taskstable/taskstable.d.ts +0 -54
  74. package/dist/types/product/taskstable/taskstable.recipe.d.ts +0 -3
  75. /package/dist/cjs/product/{recordstable → recordpage}/editable-field.js +0 -0
  76. /package/dist/esm/product/{recordstable → recordpage}/editable-field.js +0 -0
  77. /package/dist/types/product/{recordstable → recordpage}/editable-field.d.ts +0 -0
@@ -53,6 +53,7 @@ const checkbox_1 = require("../checkbox");
53
53
  const columncalc_1 = require("../columncalc");
54
54
  const contextmenu_1 = require("../contextmenu");
55
55
  const menu_1 = require("../menu");
56
+ const radiogroup_1 = require("../radiogroup");
56
57
  const select_1 = require("../select");
57
58
  const spinner_1 = require("../spinner");
58
59
  const switch_1 = require("../switch");
@@ -292,6 +293,73 @@ function formatCell(type, v, symbol, locale = "en-US") {
292
293
  }
293
294
  return asText(v);
294
295
  }
296
+ // crisp/#760 -- the exact 5-point star Attio's own Rating attribute draws
297
+ // (path traced from the live widget's own SVG, `app.attio.com`, both the
298
+ // attribute-creation default-value picker and the grid cell render the
299
+ // identical path). Kept as one shared `d` string rather than a lucide icon
300
+ // so the star's silhouette matches Attio verbatim rather than an
301
+ // approximation, and so `RatingStars` below can flip its own `fill`
302
+ // per-star instead of drawing a whole new icon per state.
303
+ const RATING_STAR_D = "M7.32203 2.81085C7.65793 1.97438 8.84207 1.97438 9.17797 2.81085L10.0851 5.06967C10.2281 5.42587 10.5624 5.66876 10.9454 5.69473L13.3739 5.8594C14.2733 5.92038 14.6392 7.04656 13.9475 7.62451L12.0795 9.18521C11.7849 9.43132 11.6572 9.82433 11.7509 10.1966L12.3447 12.5572C12.5647 13.4313 11.6067 14.1273 10.8433 13.6481L8.78171 12.3538C8.45661 12.1497 8.04339 12.1497 7.71829 12.3538L5.65675 13.6481C4.89333 14.1273 3.93534 13.4313 4.15526 12.5572L4.74911 10.1966C4.84276 9.82433 4.71507 9.43132 4.4205 9.18521L2.55254 7.62451C1.86081 7.04656 2.22673 5.92038 3.12606 5.8594L5.55463 5.69473C5.9376 5.66876 6.27191 5.42587 6.41495 5.06967L7.32203 2.81085Z";
304
+ /** A star row for `type: "rating"` / `editor: "rating"` (crisp/#760).
305
+ * Read-only (a plain `<span>` row, no interaction) unless `onChange` is
306
+ * given, which renders Attio's own measured accessible shape: a
307
+ * `role="radiogroup"` of `role="radio"` buttons, one per star. Clicking a
308
+ * star sets the rating to its 1-based index; clicking the star that
309
+ * already equals the current value clears it to 0 -- confirmed live
310
+ * (rating a record 5, then clicking the 5th star again cleared it
311
+ * entirely rather than re-committing 5). */
312
+ function RatingStars({ value, max = 5, onChange, "aria-label": ariaLabel, }) {
313
+ const [hoverIdx, setHoverIdx] = React.useState(null);
314
+ // The house radiogroup keyboard model (crisp's own spec check requires it
315
+ // on every role="radiogroup", regardless of Attio parity): arrows move
316
+ // focus AND select, Home/End jump to the ends. Landing on a star always
317
+ // SETS the rating to that star's position -- arrow nav has no "clear on
318
+ // re-land" toggle, that's a click-only gesture (see onClick below).
319
+ const { onKeyDown, groupRef } = (0, radiogroup_1.useRadioGroupKeyboard)({
320
+ select: (v) => onChange?.(Number(v)),
321
+ });
322
+ const stars = Array.from({ length: max }, (_, i) => i);
323
+ const content = stars.map((i) => {
324
+ // Hovering star i previews filling every star up to and including it,
325
+ // whether or not it's already part of the committed value -- measured:
326
+ // hovering the 5th star while 4 were already filled turned all 5 the
327
+ // hover colour, not just the would-be-new one.
328
+ const hovering = onChange != null && hoverIdx != null && i <= hoverIdx;
329
+ const filled = hovering ? true : i < value;
330
+ // A native <input type="radio"> can't be unchecked by re-clicking the
331
+ // already-selected one, which is exactly the measured behaviour
332
+ // (clicking the star that already equals the rating clears it to 0) --
333
+ // Attio's own live widget is a <button role="radio"> for the same
334
+ // reason.
335
+ return onChange ? ((0, jsx_runtime_1.jsx)("button", { type: "button", role: "radio", "aria-checked": i < value, "aria-label": `${i + 1} star${i === 0 ? "" : "s"}`, "data-value": String(i + 1), className: "crisp-datatable-rating-star-btn", onMouseEnter: () => setHoverIdx(i), onMouseLeave: () => setHoverIdx(null), onClick: (e) => {
336
+ e.stopPropagation();
337
+ onChange(i + 1 === value ? 0 : i + 1);
338
+ }, children: (0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", className: "crisp-datatable-rating-star", "data-filled": filled ? "" : undefined, "data-hovering": hovering ? "" : undefined, children: (0, jsx_runtime_1.jsx)("path", { d: RATING_STAR_D }) }) }, i)) : ((0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", className: "crisp-datatable-rating-star", "data-filled": filled ? "" : undefined, children: (0, jsx_runtime_1.jsx)("path", { d: RATING_STAR_D }) }, i));
339
+ });
340
+ return onChange ? ((0, jsx_runtime_1.jsx)("span", { role: "radiogroup", "aria-label": ariaLabel, className: "crisp-datatable-rating", ref: groupRef, onKeyDown: onKeyDown, children: content })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-rating", children: content }));
341
+ }
342
+ /** The preview/comments action icons a column's `onOpenPreview`/
343
+ * `onOpenComments`/`commentCount` produce — the exact markup `type: "entity"`
344
+ * already renders inline, extracted so a column that ISN'T `"entity"` (a
345
+ * custom `render`, or any other built-in type) can compose the same actions
346
+ * alongside its own content instead of the props silently doing nothing
347
+ * (crisp/#601 supersession follow-up: these three props used to only have
348
+ * an effect inside `renderTyped`'s `"entity"` case). `null` when neither
349
+ * prop is set, so a caller can render nothing rather than an empty
350
+ * flex wrapper. */
351
+ function renderCellActions(col, row, rowHovered, openPreviewLabel) {
352
+ if (col.onOpenComments) {
353
+ return ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-actions", children: (0, jsx_runtime_1.jsx)(comments_1.CommentCountBadge, { count: col.commentCount?.(row) ?? 0, hovered: rowHovered, onOpen: () => col.onOpenComments?.(row), onOpenPreview: col.onOpenPreview ? () => col.onOpenPreview?.(row) : undefined, previewLabel: openPreviewLabel }) }));
354
+ }
355
+ if (col.onOpenPreview) {
356
+ return ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-actions", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-entity-action", "aria-label": openPreviewLabel, onClick: (ev) => {
357
+ ev.stopPropagation();
358
+ col.onOpenPreview?.(row);
359
+ }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.PanelRight, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) }));
360
+ }
361
+ return null;
362
+ }
295
363
  /** Built-in typed-cell renderer (used when a column has no custom `render`). */
296
364
  // `value` is always the RAW cell value and `display` the caller's `format`
297
365
  // output, if any. They are separate arguments on purpose: the entity and tags
@@ -319,10 +387,7 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US", ope
319
387
  // at a DIFFERENT record than the row itself (#501).
320
388
  ev.stopPropagation();
321
389
  col.onEntityClick?.(row);
322
- }, children: e.name })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-name", children: e.name })), col.onOpenComments ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-actions", children: (0, jsx_runtime_1.jsx)(comments_1.CommentCountBadge, { count: col.commentCount?.(row) ?? 0, hovered: rowHovered, onOpen: () => col.onOpenComments?.(row), onOpenPreview: col.onOpenPreview ? () => col.onOpenPreview?.(row) : undefined, previewLabel: openPreviewLabel }) })) : (col.onOpenPreview && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-actions", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-entity-action", "aria-label": openPreviewLabel, onClick: (ev) => {
323
- ev.stopPropagation();
324
- col.onOpenPreview?.(row);
325
- }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.PanelRight, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) })))] }));
390
+ }, children: e.name })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-name", children: e.name })), renderCellActions(col, row, rowHovered, openPreviewLabel)] }));
326
391
  }
327
392
  case "tags": {
328
393
  // asking for a format on a tag column means "render it your way" -- show
@@ -342,6 +407,14 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US", ope
342
407
  // translation) can never silently repaint the chip
343
408
  return ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-tag", "data-tone": col.tones?.[s] ?? toneFor(s), children: display || s }));
344
409
  }
410
+ case "rating": {
411
+ // Read-only display -- `editor: "rating"` (the always-live picker) is
412
+ // handled separately, the same split `switch` gets (#453).
413
+ const n = typeof value === "number" ? value : Number(value);
414
+ if (!n || Number.isNaN(n))
415
+ return empty;
416
+ return (0, jsx_runtime_1.jsx)(RatingStars, { value: n, max: col.maxRating ?? 5 });
417
+ }
345
418
  case "url":
346
419
  case "email": {
347
420
  const text = display || asText(value);
@@ -998,14 +1071,48 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
998
1071
  const colAriaIndex = (i) => ariaColIndexBase + i + 1;
999
1072
  const allSelected = rows.length > 0 && rows.every((r) => selected.has(r.id));
1000
1073
  const someSelected = selected.size > 0 && !allSelected;
1074
+ // crisp/#782, ported from RecordsTable's own already-shipped logic: the
1075
+ // header checkbox speaks for the rows you can SEE. Rebuilding the whole
1076
+ // set from `rows` (the filtered/visible set) would silently drop any
1077
+ // selection an active filter is hiding -- add/remove only the visible
1078
+ // rows and leave the rest of `selected` alone.
1001
1079
  function toggleAll() {
1002
- setSelected(allSelected ? new Set() : new Set(rows.map((r) => r.id)));
1080
+ const next = new Set(selected);
1081
+ for (const r of rows) {
1082
+ if (allSelected)
1083
+ next.delete(r.id);
1084
+ else
1085
+ next.add(r.id);
1086
+ }
1087
+ setSelected(next);
1003
1088
  }
1004
1089
  function toggleRow(id) {
1005
1090
  const next = new Set(selected);
1006
1091
  next.has(id) ? next.delete(id) : next.add(id);
1007
1092
  setSelected(next);
1008
1093
  }
1094
+ // crisp/#781: Shift+Click on a row checkbox selects the whole contiguous
1095
+ // range from the last plain click (the anchor) to this row -- confirmed
1096
+ // live, the mouse counterpart to #749's Shift+Arrow keyboard gesture,
1097
+ // sharing the same `rowSelectAnchor` state and the same recompute-the-
1098
+ // whole-span shape (not accumulate) so shift-clicking back toward the
1099
+ // anchor shrinks the same way clicking away from it grows. A plain click
1100
+ // both toggles normally AND re-anchors, so the NEXT Shift+Click extends
1101
+ // from wherever the user last plain-clicked, matching Attio.
1102
+ function toggleRowOrExtend(id, shiftKey) {
1103
+ if (shiftKey && rowSelectAnchor != null) {
1104
+ const anchorIdx = rows.findIndex((r) => r.id === rowSelectAnchor);
1105
+ const idx = rows.findIndex((r) => r.id === id);
1106
+ if (anchorIdx >= 0 && idx >= 0) {
1107
+ const lo = Math.min(anchorIdx, idx);
1108
+ const hi = Math.max(anchorIdx, idx);
1109
+ setSelected(new Set(rows.slice(lo, hi + 1).map((r) => r.id)));
1110
+ return;
1111
+ }
1112
+ }
1113
+ setRowSelectAnchor(id);
1114
+ toggleRow(id);
1115
+ }
1009
1116
  function cycleSort(key) {
1010
1117
  // Clicking a header drives a single sort, as before. Multi-sort is supplied
1011
1118
  // by the caller through the `sort` prop (built in a sort popover).
@@ -1033,8 +1140,9 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1033
1140
  * `editor: "select"`/`"reference"` open their own picker instead of this
1034
1141
  * native-input family, but through the SAME double-click gate (#453) --
1035
1142
  * see the row-render loop's `isEditing` branch for those, this function
1036
- * only covers the native-input-backed types. `editor: "switch"` is the
1037
- * one editor with no double-click gate at all (always live).
1143
+ * only covers the native-input-backed types.
1144
+ * `editor: "switch"`/`"rating"`/`"checkbox"` are the editors with no
1145
+ * double-click gate at all (always live).
1038
1146
  */
1039
1147
  function editorFor(col) {
1040
1148
  switch (col.type) {
@@ -1211,6 +1319,28 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1211
1319
  setSelected(new Set(rows.map((rw) => rw.id)));
1212
1320
  return;
1213
1321
  }
1322
+ // crisp/#772: Cmd/Ctrl+ArrowUp jumps the selected cell to the first row,
1323
+ // Cmd/Ctrl+ArrowDown to the last -- confirmed live (Attio's own help
1324
+ // drawer documents "Jump to top"/"Jump to bottom" on these exact
1325
+ // bindings). `!e.shiftKey` guards this from stealing Cmd+Shift+Arrow,
1326
+ // a separate gesture (#773, extend-selection-to-first/last) that falls
1327
+ // through to the Shift+Arrow branch below untouched. Placed before that
1328
+ // branch for the same reason: without the guard, Cmd+Shift+Arrow would
1329
+ // still reach it (that branch doesn't check metaKey/ctrlKey either),
1330
+ // just not do the "jump" part -- the guard here is what keeps the two
1331
+ // gestures from fighting over the same keys.
1332
+ if ((e.metaKey || e.ctrlKey) &&
1333
+ !e.shiftKey &&
1334
+ (e.key === "ArrowUp" || e.key === "ArrowDown")) {
1335
+ const r0 = rows.findIndex((row) => row.id === selCell.id);
1336
+ if (r0 < 0)
1337
+ return;
1338
+ e.preventDefault();
1339
+ const nr0 = e.key === "ArrowUp" ? 0 : rows.length - 1;
1340
+ setSelCell({ id: rows[nr0].id, key: selCell.key });
1341
+ clipboardProxyRef.current?.select();
1342
+ return;
1343
+ }
1214
1344
  // crisp/#749: Shift+ArrowUp/Down extends the row-checkbox selection --
1215
1345
  // confirmed live, distinct from Shift+Click's cell-range gesture above
1216
1346
  // (that one is `selAnchor`; this is `rowSelectAnchor`, and the two
@@ -1287,8 +1417,13 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1287
1417
  // already its own live widget (#385); Enter there is a no-op, same
1288
1418
  // as any other non-editable cell that isn't wired to onRowClick.
1289
1419
  // `select`/`reference` DO have one now (like every other editor),
1290
- // so Enter opens them too.
1291
- if (columns[c]?.editable && columns[c]?.editor !== "switch") {
1420
+ // so Enter opens them too. `rating` (#760) and `checkbox` (#761)
1421
+ // join `switch` in the no-separate-editing-mode exception -- same
1422
+ // always-live shape.
1423
+ if (columns[c]?.editable &&
1424
+ columns[c]?.editor !== "switch" &&
1425
+ columns[c]?.editor !== "rating" &&
1426
+ columns[c]?.editor !== "checkbox") {
1292
1427
  e.preventDefault();
1293
1428
  openEditing(selCell.id, selCell.key);
1294
1429
  }
@@ -1357,15 +1492,21 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1357
1492
  }
1358
1493
  setEditing(null);
1359
1494
  }
1495
+ const reduceMotion = (0, core_1.usePrefersReducedMotion)();
1360
1496
  // crisp/#577 -- keyboard nav to an off-screen (virtualized-out) cell
1361
- // scrolls it into the rendered window, on both axes. Column scroll-into-
1362
- // view runs regardless of `height` -- the horizontal scroll container (and
1363
- // its own virtualization) exists either way; row scroll-into-view only
1364
- // makes sense once `height` gives the grid an internal vertical scroll
1365
- // position to move. `widths` is read only indirectly, through `colWidth`'s
1366
- // closure over it -- biome can't see that, but a column resize while a
1367
- // cell is selected genuinely needs to re-run this to keep it in view, so
1368
- // the dependency is real even though the lint rule can't prove it.
1497
+ // scrolls it into the rendered window, on both axes. `renderItems`/
1498
+ // `rowOffsets` are computed unconditionally (`height == null` just makes
1499
+ // them a no-op elsewhere, see the comment above `renderItems`), so this
1500
+ // runs on both axes regardless of `height` too (crisp/#780 -- it used to
1501
+ // gate the vertical half on `height != null`, on the reasoning that only
1502
+ // `height` gives the grid an internal scroll position to move; that's
1503
+ // wrong when the grid's own container still scrolls without an explicit
1504
+ // height, e.g. inside a shorter ancestor, matching `RecordsTable`'s own
1505
+ // already-shipped #257 fix, which had no such gate). `widths` is read only
1506
+ // indirectly, through `colWidth`'s closure over it -- biome can't see
1507
+ // that, but a column resize while a cell is selected genuinely needs to
1508
+ // re-run this to keep it in view, so the dependency is real even though
1509
+ // the lint rule can't prove it.
1369
1510
  // biome-ignore lint/correctness/useExhaustiveDependencies: widths is read via colWidth's closure, see comment above
1370
1511
  React.useEffect(() => {
1371
1512
  if (!selCell)
@@ -1373,28 +1514,37 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1373
1514
  const el = scrollRef.current;
1374
1515
  if (!el)
1375
1516
  return;
1376
- // Sets `viewport` directly from the SAME value just assigned to the
1377
- // real DOM property, rather than waiting for a follow-up `scroll` event
1378
- // to round-trip through the `onScroll` handler above -- a real browser
1379
- // does fire one when `scrollTop`/`scrollLeft` is set programmatically,
1380
- // but relying on that would make this effect's own visible result (the
1517
+ // `el.scrollTo` rather than a direct `el.scrollTop =`/`el.scrollLeft =`
1518
+ // assignment (crisp/#780, matching RecordsTable's own #208/#257 rule):
1519
+ // property assignment is ALWAYS instant, so it can never honour a
1520
+ // consumer's own `scroll-behavior: smooth` on this container; `scrollTo`
1521
+ // defers to that CSS when no `behavior` is given, and only forces
1522
+ // `"auto"` under `prefers-reduced-motion`, which is the one case that
1523
+ // must win even over a host page's own smooth-scroll opt-in.
1524
+ const scrollTo = (opts) => el.scrollTo({
1525
+ ...opts,
1526
+ ...(reduceMotion ? { behavior: "auto" } : null),
1527
+ });
1528
+ // Sets `viewport` directly from the SAME value just passed to
1529
+ // `scrollTo`, rather than waiting for a follow-up `scroll` event to
1530
+ // round-trip through the `onScroll` handler above -- a real browser
1531
+ // does fire one when the scroll position changes programmatically, but
1532
+ // relying on that would make this effect's own visible result (the
1381
1533
  // newly-selected cell actually rendering) depend on a second render
1382
1534
  // triggered from outside React's own update, which is both slower and
1383
1535
  // one more thing that has to be true for this to work.
1384
- if (height != null) {
1385
- const itemIndex = rowIdToItemIndex.get(selCell.id);
1386
- if (itemIndex != null) {
1387
- const itemTop = rowOffsets[itemIndex];
1388
- const itemBottom = itemTop + renderItems[itemIndex].height;
1389
- if (itemTop < el.scrollTop) {
1390
- el.scrollTop = itemTop;
1391
- setViewport((v) => ({ ...v, top: itemTop }));
1392
- }
1393
- else if (itemBottom > el.scrollTop + el.clientHeight) {
1394
- const newTop = itemBottom - el.clientHeight;
1395
- el.scrollTop = newTop;
1396
- setViewport((v) => ({ ...v, top: newTop }));
1397
- }
1536
+ const itemIndex = rowIdToItemIndex.get(selCell.id);
1537
+ if (itemIndex != null) {
1538
+ const itemTop = rowOffsets[itemIndex];
1539
+ const itemBottom = itemTop + renderItems[itemIndex].height;
1540
+ if (itemTop < el.scrollTop) {
1541
+ scrollTo({ top: itemTop });
1542
+ setViewport((v) => ({ ...v, top: itemTop }));
1543
+ }
1544
+ else if (itemBottom > el.scrollTop + el.clientHeight) {
1545
+ const newTop = itemBottom - el.clientHeight;
1546
+ scrollTo({ top: newTop });
1547
+ setViewport((v) => ({ ...v, top: newTop }));
1398
1548
  }
1399
1549
  }
1400
1550
  const ci = columns.findIndex((c) => c.key === selCell.key);
@@ -1404,24 +1554,24 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1404
1554
  left += colWidth(columns[i]);
1405
1555
  const right = left + colWidth(columns[ci]);
1406
1556
  if (left < el.scrollLeft) {
1407
- el.scrollLeft = left;
1557
+ scrollTo({ left });
1408
1558
  setViewport((v) => ({ ...v, left }));
1409
1559
  }
1410
1560
  else if (right > el.scrollLeft + el.clientWidth) {
1411
1561
  const newLeft = right - el.clientWidth;
1412
- el.scrollLeft = newLeft;
1562
+ scrollTo({ left: newLeft });
1413
1563
  setViewport((v) => ({ ...v, left: newLeft }));
1414
1564
  }
1415
1565
  }
1416
1566
  }, [
1417
1567
  selCell,
1418
- height,
1419
1568
  columns,
1420
1569
  widths,
1421
1570
  selectable,
1422
1571
  rowIdToItemIndex,
1423
1572
  rowOffsets,
1424
1573
  renderItems,
1574
+ reduceMotion,
1425
1575
  ]);
1426
1576
  const totalCols = columns.length + (selectable ? 1 : 0) + (showGrip ? 1 : 0);
1427
1577
  const hasSummary = columns.some((c) => c.summary || c.calc);
@@ -1795,12 +1945,33 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1795
1945
  }, onDragEnd: () => {
1796
1946
  setDragRowId(null);
1797
1947
  setDropRowId(null);
1798
- }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) })), selectable && ((0, jsx_runtime_1.jsx)(table_1.TableCell, { className: "crisp-datatable-col-check", "aria-colindex": showGrip ? 2 : 1, ...stickyCol(-1), onClick: (e) => e.stopPropagation(), children: (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-check", children: (0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { "aria-label": selectRowLabel(row), checked: selected.has(row.id), onChange: () => toggleRow(row.id) }) }) })), colSlots.map((slot) => {
1948
+ }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) })), selectable && ((0, jsx_runtime_1.jsx)(table_1.TableCell, { className: "crisp-datatable-col-check", "aria-colindex": showGrip ? 2 : 1, ...stickyCol(-1), onClick: (e) => e.stopPropagation(), children: (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-check", children: (0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { "aria-label": selectRowLabel(row), checked: selected.has(row.id),
1949
+ // preventDefault stops the native check/change
1950
+ // this click would otherwise also fire -- this
1951
+ // handler is the single source of truth so a
1952
+ // Shift+Click's range-select and a plain
1953
+ // click's toggle-and-re-anchor never race each
1954
+ // other (crisp/#781).
1955
+ onClick: (e) => {
1956
+ e.preventDefault();
1957
+ toggleRowOrExtend(row.id, e.shiftKey);
1958
+ }, onChange: () => { } }) }) })), colSlots.map((slot) => {
1799
1959
  if (slot.type === "spacer") {
1800
1960
  return ((0, jsx_runtime_1.jsx)("td", { role: "presentation", "aria-hidden": "true", style: { padding: 0, border: "none" } }, `__spacer_${slot.side}`));
1801
1961
  }
1802
1962
  const col = columns[slot.i];
1803
1963
  const i = slot.i;
1964
+ // crisp/#779, matching RecordsTable's own #231 fix:
1965
+ // column 0 is the row's own identity (its name), not
1966
+ // a value it holds -- a screen reader crossing the
1967
+ // row should read "Google, Employees, 182,000", not a
1968
+ // bare "182,000". Spread onto every TableCell this
1969
+ // column can render as (every editor branch, not just
1970
+ // the default one), since the row-identity semantics
1971
+ // don't depend on which editor is open.
1972
+ const rowHeaderProps = i === 0
1973
+ ? { as: "th", scope: "row" }
1974
+ : undefined;
1804
1975
  const isEditing = editing?.id === row.id && editing?.key === col.key;
1805
1976
  const value = cellValue(col, row);
1806
1977
  const sticky = dataStickyCol(i);
@@ -1823,13 +1994,29 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1823
1994
  // right). See the `isEditing` block below for their
1824
1995
  // actual editor.
1825
1996
  if (col.editable && col.editor === "switch") {
1826
- return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commitEdit(row.id, col.key, String(checked)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
1997
+ return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commitEdit(row.id, col.key, String(checked)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
1998
+ }
1999
+ // crisp/#760: `editor: "rating"` is the same always-
2000
+ // live shape as `switch` above -- clicking a star IS
2001
+ // the commit gesture, confirmed live.
2002
+ if (col.editable && col.editor === "rating") {
2003
+ const n = typeof value === "number" ? value : Number(value);
2004
+ return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(RatingStars, { "aria-label": `Edit ${col.key}`, value: Number.isNaN(n) ? 0 : n, max: col.maxRating ?? 5, onChange: (next) => commitEdit(row.id, col.key, String(next)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
2005
+ }
2006
+ // crisp/#761: `editor: "checkbox"` is a THIRD always-
2007
+ // live boolean shape, distinct from `switch` --
2008
+ // confirmed live against Attio's own "Checkbox"
2009
+ // attribute type, which renders a plain square, not a
2010
+ // toggle. Reuses crisp's own `Checkbox` (already the
2011
+ // measured 16px/r6/brand-fill grid checkbox).
2012
+ if (col.editable && col.editor === "checkbox") {
2013
+ return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onChange: (e) => commitEdit(row.id, col.key, String(e.currentTarget.checked)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
1827
2014
  }
1828
2015
  if (isEditing &&
1829
2016
  (col.editor === "select" ||
1830
2017
  col.editor === "reference" ||
1831
2018
  col.editor === "actor")) {
1832
- return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined,
2019
+ return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined,
1833
2020
  // Same as the plain text editor's own cell:
1834
2021
  // opening/using the picker must not also fire
1835
2022
  // the row's onRowClick. Escape is handled by
@@ -1843,7 +2030,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1843
2030
  onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [col.editor === "select" ? ((0, jsx_runtime_1.jsx)(DataSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })) : ((0, jsx_runtime_1.jsx)(DataReferenceEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
1844
2031
  }
1845
2032
  if (isEditing) {
1846
- return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-datatable-edit",
2033
+ return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-datatable-edit",
1847
2034
  // biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
1848
2035
  autoFocus: true, ...editorFor(col), "data-editor": editorFor(col).type, disabled: isPending, defaultValue: col.type === "date"
1849
2036
  ? toDateInputValue(value)
@@ -1880,13 +2067,29 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1880
2067
  i >= range.c0 &&
1881
2068
  i <= range.c1);
1882
2069
  })();
1883
- const rendered = col.render
2070
+ const rowHoveredForCell = hoveredRowId === row.id;
2071
+ const baseRendered = col.render
1884
2072
  ? col.render(value, row)
1885
- : renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale, openPreviewLabel, hoveredRowId === row.id);
2073
+ : renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale, openPreviewLabel, rowHoveredForCell);
2074
+ // crisp/#601 supersession follow-up: `onOpenComments`/
2075
+ // `onOpenPreview`/`commentCount` used to only do
2076
+ // anything inside `renderTyped`'s `"entity"` case,
2077
+ // which already renders its own actions inline (see
2078
+ // `renderCellActions` there) -- silently dropping them
2079
+ // on any OTHER type, and on a custom `render` of any
2080
+ // type, since neither path went through that switch.
2081
+ // This composes the same actions alongside whatever
2082
+ // `rendered` is instead, decoupling the props from
2083
+ // requiring `type: "entity"`.
2084
+ const cellActions = col.type !== "entity" &&
2085
+ (col.onOpenComments || col.onOpenPreview)
2086
+ ? renderCellActions(col, row, rowHoveredForCell, openPreviewLabel)
2087
+ : null;
2088
+ const rendered = cellActions ? ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-cell-actions-row", children: [baseRendered, cellActions] })) : (baseRendered);
1886
2089
  const isExpanded = expandedCell?.id === row.id &&
1887
2090
  expandedCell?.key === col.key;
1888
2091
  const cellContent = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!isExpanded && rendered, errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere })), isExpanded && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-cell-overlay", onClick: (e) => e.stopPropagation(), children: rendered }))] }));
1889
- return ((0, jsx_runtime_1.jsx)(contextmenu_1.ContextMenu, { asChild: true, variant: "dark", menuLabel: cellMenuLabel, items: cellMenuItems(row, col), children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: col.editable
2092
+ return ((0, jsx_runtime_1.jsx)(contextmenu_1.ContextMenu, { asChild: true, variant: "dark", menuLabel: cellMenuLabel, items: cellMenuItems(row, col), children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: col.editable
1890
2093
  ? "crisp-datatable-editable"
1891
2094
  : undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
1892
2095
  ? ""
@@ -227,6 +227,13 @@ const config = (0, core_1.defineRecipe)({
227
227
  & th[data-drop-after]::after { inset-inline-end: -1.5px; }
228
228
  /* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
229
229
  & .crisp-datatable-entity { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; min-width: 0; color: ${tokens_1.contract.fgPrimary}; font-weight: ${tokens_1.contract.weight.medium}; }
230
+ /* crisp/#601 supersession follow-up -- the same flex-row shape
231
+ .crisp-datatable-entity gives its own avatar+name+actions, but
232
+ structural only (no entity-specific colour/weight): lets
233
+ onOpenComments/onOpenPreview compose alongside ANY column's own
234
+ content (a custom render, or a non-entity built-in type), not just
235
+ type: "entity"'s own inline rendering. */
236
+ & .crisp-datatable-cell-actions-row { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; min-width: 0; width: 100%; }
230
237
  /* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text,
231
238
  plain hover vs rest) that Attio's underline is unconditional, not a
232
239
  hover reveal -- identical at rest and on hover: text-decoration-color
@@ -268,6 +275,23 @@ const config = (0, core_1.defineRecipe)({
268
275
  link. */
269
276
  & .crisp-datatable-link { color: ${tokens_1.contract.fgPrimary}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
270
277
  & .crisp-datatable-link:hover { text-decoration: underline; }
278
+ /* rating cell type/editor (#760) -- measured live: 16px stars on an
279
+ 18px pitch (a 2px gap), an empty star at rgba(0,0,0,0.1), a
280
+ committed/filled star at brand600 (--crisp-bg-brand-solid, an exact
281
+ match to the measured #266df0), and a distinct hover-preview colour
282
+ -- rgb(33,91,196) -- that's close to but NOT brand700/bgBrandSolidPressed
283
+ (#1f5fd6); kept as its own literal rather than forced onto that token,
284
+ since the two were measured stable and different. The read-only svg
285
+ and the editable button's own inner svg both carry the
286
+ crisp-datatable-rating-star class on the svg itself (not the
287
+ button), so one set of fill rules covers both -- the button only
288
+ adds its own reset + cursor via a separate -btn class. */
289
+ & .crisp-datatable-rating { display: inline-flex; align-items: center; gap: 2px; }
290
+ & .crisp-datatable-rating-star { display: block; }
291
+ & .crisp-datatable-rating-star path { fill: rgba(0, 0, 0, 0.1); }
292
+ & .crisp-datatable-rating-star[data-filled] path { fill: ${tokens_1.contract.bgBrandSolid}; }
293
+ & .crisp-datatable-rating-star[data-hovering] path { fill: #215bc4; }
294
+ & .crisp-datatable-rating-star-btn { display: inline-flex; padding: 0; margin: 0; border: none; background: none; font-family: inherit; cursor: pointer; }
271
295
  /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
272
296
  a leading 20px grip column, a 16px grip icon, and the row's own
273
297
  hairline doubling as the drop indicator (it tints toward the brand
@@ -163,6 +163,15 @@ function ResizablePanel({ side = "left", size, defaultSize = 280, onSizeChange,
163
163
  const finalWidth = rect ? Math.round(rect.width) : width;
164
164
  commitSize(finalWidth);
165
165
  }
166
+ // crisp/#795: double-click resets to `defaultSize`, confirmed live on
167
+ // Attio's own record-page divider (dragged 722px -> 924px, double-click
168
+ // snapped it back to exactly 722px -- its own default/initial width, not
169
+ // some other fixed value). Reuses `commitSize`, the same commit path a
170
+ // drag already uses, so persistence (`storageKey`) picks up the reset the
171
+ // same way it picks up a drag -- no separate reset-specific write.
172
+ function onHandleDoubleClick() {
173
+ commitSize(defaultSize);
174
+ }
166
175
  const maxStyle = max == null ? undefined : typeof max === "number" ? `${max}px` : max;
167
176
  const panelSizeStyle = {
168
177
  width,
@@ -171,6 +180,6 @@ function ResizablePanel({ side = "left", size, defaultSize = 280, onSizeChange,
171
180
  };
172
181
  return ((0, jsx_runtime_1.jsxs)("div", { ref: containerRef, className: (0, resizablepanel_recipe_1.resizablepanelRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-resizablepanel-spacer", style: floating ? { width: 0 } : { width, minWidth: min, maxWidth: maxStyle } }), (0, jsx_runtime_1.jsxs)("div", { ref: panelRef, className: "crisp-resizablepanel-panel", "data-side": side, "data-floating": floating ? "true" : "false", style: panelSizeStyle, children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-resizablepanel-inner", children: children }), (0, jsx_runtime_1.jsx)("div", { className: handleVariant === "column"
173
182
  ? "crisp-resizablepanel-handle crisp-resizablepanel-handle--column"
174
- : "crisp-resizablepanel-handle", "data-side": side, "data-dragging": dragging ? "true" : undefined, "data-handle": "", onPointerDown: onHandlePointerDown, onPointerMove: onHandlePointerMove, onPointerUp: onHandlePointerUp, children: handleVariant === "column" && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-resizablepanel-handle-inner", "aria-hidden": "true" })) })] })] }));
183
+ : "crisp-resizablepanel-handle", "data-side": side, "data-dragging": dragging ? "true" : undefined, "data-handle": "", onPointerDown: onHandlePointerDown, onPointerMove: onHandlePointerMove, onPointerUp: onHandlePointerUp, onDoubleClick: onHandleDoubleClick, children: handleVariant === "column" && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-resizablepanel-handle-inner", "aria-hidden": "true" })) })] })] }));
175
184
  }
176
185
  ResizablePanel.displayName = "ResizablePanel";
@@ -64,7 +64,14 @@ exports.TableRow = React.forwardRef((props, ref) => (0, jsx_runtime_1.jsx)("tr",
64
64
  exports.TableRow.displayName = "TableRow";
65
65
  exports.TableHead = React.forwardRef(({ align, ...props }, ref) => (0, jsx_runtime_1.jsx)("th", { ref: ref, "data-align": align, ...props }));
66
66
  exports.TableHead.displayName = "TableHead";
67
- exports.TableCell = React.forwardRef(({ align, ...props }, ref) => (0, jsx_runtime_1.jsx)("td", { ref: ref, "data-align": align, ...props }));
67
+ exports.TableCell = React.forwardRef(({ align, as, ...props }, ref) => {
68
+ // Cast to "td", same as RecordsTable's own dynamic-tag precedent --
69
+ // `props` (which may carry `scope`) would otherwise need to satisfy
70
+ // both <td> and <th>'s JSX attribute types at once for a bare union
71
+ // element name.
72
+ const Cell = (as ?? "td");
73
+ return (0, jsx_runtime_1.jsx)(Cell, { ref: ref, "data-align": align, ...props });
74
+ });
68
75
  exports.TableCell.displayName = "TableCell";
69
76
  exports.TableCaption = React.forwardRef((props, ref) => (0, jsx_runtime_1.jsx)("caption", { ref: ref, ...props }));
70
77
  exports.TableCaption.displayName = "TableCaption";
@@ -53,12 +53,35 @@ const toolbarChromeHead = (p) => ` & .${namespace_1.CLASS_PREFIX}${p}-toolbar
53
53
  & .${namespace_1.CLASS_PREFIX}${p}-search svg { flex-shrink: 0; }
54
54
  & .${namespace_1.CLASS_PREFIX}${p}-search input { border: none; outline: none; background: transparent; font-family: inherit; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgPrimary}; width: 180px; }
55
55
  & .${namespace_1.CLASS_PREFIX}${p}-search input::placeholder { color: ${tokens_1.contract.fgMuted}; }
56
- & .${namespace_1.CLASS_PREFIX}${p}-tool { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.row}; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderDefault}; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
56
+ & .${namespace_1.CLASS_PREFIX}${p}-tool { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.row}; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderDivider}; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
57
57
  & .${namespace_1.CLASS_PREFIX}${p}-tool:hover { background: ${tokens_1.contract.bgRaised}; }
58
58
  & .${namespace_1.CLASS_PREFIX}${p}-tool svg { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
59
- & .${namespace_1.CLASS_PREFIX}${p}-tool--active { background: ${tokens_1.contract.bgRaised}; }
59
+ /* crisp/#772: re-measured live on Sort with an actual sort applied --
60
+ an active trigger's own fill is white (bgSurface), the same as the
61
+ base .tool rule above now already sets; this override existed to
62
+ distinguish active from a base state that used to be transparent.
63
+ Kept explicit (not just deleted) so a future change to the base
64
+ rule's background can't silently change this one too. */
65
+ & .${namespace_1.CLASS_PREFIX}${p}-tool--active { background: ${tokens_1.contract.bgSurface}; }
60
66
  & .${namespace_1.CLASS_PREFIX}${p}-tool--ghost { box-shadow: none; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 3%, transparent); color: ${tokens_1.contract.fgMuted}; }
61
67
  & .${namespace_1.CLASS_PREFIX}${p}-tool--ghost:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); }
68
+ /* crisp/#772 follow-up, re-measured live 2026-09-25: an EMPTY Sort/Filter
69
+ trigger (no sort/condition set) isn't a solid-ring chip at all -- it's
70
+ a dashed ::before (1px dashed, border-radius: inherit, absolutely
71
+ positioned inset: 0) over a barely-there fill, confirmed on Attio's
72
+ own empty Filter chip (its ::before border computes to exactly
73
+ "1px dashed rgba(0, 0, 0, 0.06)"). Once a value IS set, the trigger
74
+ drops the dashed pseudo and picks up the base tool's own solid ring
75
+ instead (the --active modifier, unchanged) -- confirmed on Sort with
76
+ an actual sort applied. color-mix (not the raw rgba Attio computes
77
+ to) so both the border and the fill flip correctly in dark mode, the
78
+ same reasoning --ghost above already uses. Only wired into
79
+ TableToolbar itself (tabletoolbar.tsx) this pass -- RecordsTable's
80
+ own Sort/Filter triggers share this same chrome function but are
81
+ deprecated, not touched here. */
82
+ & .${namespace_1.CLASS_PREFIX}${p}-tool--dashed { position: relative; box-shadow: none; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 2%, transparent); }
83
+ & .${namespace_1.CLASS_PREFIX}${p}-tool--dashed::before { content: ""; position: absolute; inset: 0; border-radius: inherit; border: 1px dashed color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); pointer-events: none; }
84
+ & .${namespace_1.CLASS_PREFIX}${p}-tool--dashed:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); }
62
85
  & .${namespace_1.CLASS_PREFIX}${p}-toolgroup { position: relative; display: inline-flex; }
63
86
  & .${namespace_1.CLASS_PREFIX}${p}-divider { flex-shrink: 0; width: 1px; height: 20px; background: ${tokens_1.contract.borderSubtle}; }
64
87
  & .${namespace_1.CLASS_PREFIX}${p}-menu { position: absolute; top: calc(100% + ${tokens_1.contract.space["1"]}); left: 0; z-index: 30; min-width: 232px; padding: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover}; box-shadow: ${tokens_1.elevation.floating}; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px}; transform-origin: top; opacity: 1; transform: none; transition: opacity .13s ease, transform .13s ease; }