@crisp-ui-kit/crisp 0.54.9 → 0.54.11

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 (52) hide show
  1. package/dist/cjs/components/datatable/datatable.js +175 -12
  2. package/dist/cjs/components/datatable/datatable.recipe.js +30 -0
  3. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
  4. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +159 -63
  5. package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +41 -9
  6. package/dist/cjs/components/text/text.recipe.js +17 -7
  7. package/dist/cjs/product/recordpage/column.js +19 -0
  8. package/dist/cjs/product/recordpage/recordpage.js +5 -4
  9. package/dist/cjs/product/recordpage/test-fixtures.js +55 -0
  10. package/dist/cjs/product/recordstable/index.js +1 -1
  11. package/dist/cjs/product/recordstable/recordstable.js +5 -0
  12. package/dist/cjs/product/savedviews/savedviews.recipe.js +13 -11
  13. package/dist/cjs/product/taskstable/taskstable.js +5 -0
  14. package/dist/datatable.css +17 -0
  15. package/dist/datatable.layered.css +17 -0
  16. package/dist/esm/components/datatable/datatable.js +175 -12
  17. package/dist/esm/components/datatable/datatable.recipe.js +30 -0
  18. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
  19. package/dist/esm/components/tabletoolbar/tabletoolbar.js +159 -63
  20. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +41 -9
  21. package/dist/esm/components/text/text.recipe.js +17 -7
  22. package/dist/esm/product/recordpage/column.js +16 -0
  23. package/dist/esm/product/recordpage/recordpage.js +2 -1
  24. package/dist/esm/product/recordpage/test-fixtures.js +52 -0
  25. package/dist/esm/product/recordstable/index.js +1 -1
  26. package/dist/esm/product/recordstable/recordstable.js +5 -0
  27. package/dist/esm/product/savedviews/savedviews.recipe.js +13 -11
  28. package/dist/esm/product/taskstable/taskstable.js +5 -0
  29. package/dist/recordstable.css +19 -2
  30. package/dist/recordstable.layered.css +19 -2
  31. package/dist/savedviews.css +6 -18
  32. package/dist/savedviews.layered.css +6 -18
  33. package/dist/styles.css +78 -24
  34. package/dist/styles.layered.css +78 -24
  35. package/dist/tabletoolbar.css +35 -3
  36. package/dist/tabletoolbar.layered.css +35 -3
  37. package/dist/text.css +1 -1
  38. package/dist/text.layered.css +1 -1
  39. package/dist/types/components/datatable/datatable.d.ts +38 -3
  40. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +26 -1
  41. package/dist/types/components/text/text.recipe.d.ts +1 -1
  42. package/dist/types/product/recordpage/column.d.ts +112 -0
  43. package/dist/types/product/recordpage/recordpage.d.ts +1 -1
  44. package/dist/types/product/recordpage/test-fixtures.d.ts +3 -0
  45. package/dist/types/product/recordpanel/recordpanel.d.ts +1 -1
  46. package/dist/types/product/recordstable/index.d.ts +1 -1
  47. package/dist/types/product/recordstable/recordstable.d.ts +6 -1
  48. package/dist/types/product/taskstable/taskstable.d.ts +5 -0
  49. package/package.json +1 -1
  50. /package/dist/cjs/product/{recordstable → recordpage}/editable-field.js +0 -0
  51. /package/dist/esm/product/{recordstable → recordpage}/editable-field.js +0 -0
  52. /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);
@@ -611,6 +684,19 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
611
684
  // arrow-key nav, Enter) is unaffected -- a plain click resets `selAnchor`
612
685
  // to null, collapsing back to single-cell mode.
613
686
  const [selAnchor, setSelAnchor] = React.useState(null);
687
+ // crisp/#749: Shift+ArrowUp/Down extends the ROW (checkbox) selection --
688
+ // confirmed live on Attio's own Companies grid, a DIFFERENT gesture from
689
+ // `selAnchor` above (that's Shift+CLICK extending a cell-value range;
690
+ // this issue's own text is explicit the two are independent, and arrow
691
+ // keys were never measured as a cell-range trigger, G-101). Same
692
+ // fixed-anchor/moving-cell shape as `selAnchor` -- a spreadsheet's own
693
+ // Shift+Arrow both moves the active cell AND grows/shrinks the selection
694
+ // relative to wherever the drag started, which recomputing the anchor→
695
+ // new-row span from scratch on every press gives for free (arrowing back
696
+ // toward the anchor shrinks the same way arrowing away grows it). Reset
697
+ // to null on any plain (non-shift) navigation or click, same as
698
+ // `selAnchor`'s own reset points.
699
+ const [rowSelectAnchor, setRowSelectAnchor] = React.useState(null);
614
700
  const gridRef = React.useRef(null);
615
701
  // The native-copy-event proxy (crisp/#645): Attio "has no Mousetrap/
616
702
  // JS-keydown binding for c/mod+c ... it copies purely via the native
@@ -1020,8 +1106,9 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1020
1106
  * `editor: "select"`/`"reference"` open their own picker instead of this
1021
1107
  * native-input family, but through the SAME double-click gate (#453) --
1022
1108
  * see the row-render loop's `isEditing` branch for those, this function
1023
- * only covers the native-input-backed types. `editor: "switch"` is the
1024
- * one editor with no double-click gate at all (always live).
1109
+ * only covers the native-input-backed types.
1110
+ * `editor: "switch"`/`"rating"`/`"checkbox"` are the editors with no
1111
+ * double-click gate at all (always live).
1025
1112
  */
1026
1113
  function editorFor(col) {
1027
1114
  switch (col.type) {
@@ -1198,6 +1285,36 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1198
1285
  setSelected(new Set(rows.map((rw) => rw.id)));
1199
1286
  return;
1200
1287
  }
1288
+ // crisp/#749: Shift+ArrowUp/Down extends the row-checkbox selection --
1289
+ // confirmed live, distinct from Shift+Click's cell-range gesture above
1290
+ // (that one is `selAnchor`; this is `rowSelectAnchor`, and the two
1291
+ // don't interact). Recomputes the whole anchor→destination span on
1292
+ // every press rather than accumulating, so arrowing back toward the
1293
+ // anchor shrinks the selection the same way arrowing away grows it --
1294
+ // the "shrink-from-the-anchor" behavior a spreadsheet's own Shift+Arrow
1295
+ // gives, which a plain add-on-each-press approach can't express.
1296
+ if (selectable &&
1297
+ e.shiftKey &&
1298
+ (e.key === "ArrowDown" || e.key === "ArrowUp")) {
1299
+ const r0 = rows.findIndex((row) => row.id === selCell.id);
1300
+ if (r0 < 0)
1301
+ return;
1302
+ const nr0 = e.key === "ArrowDown"
1303
+ ? Math.min(rows.length - 1, r0 + 1)
1304
+ : Math.max(0, r0 - 1);
1305
+ e.preventDefault();
1306
+ const anchorId = rowSelectAnchor ?? selCell.id;
1307
+ const anchorIdx = rows.findIndex((row) => row.id === anchorId);
1308
+ if (anchorIdx < 0)
1309
+ return;
1310
+ setRowSelectAnchor(anchorId);
1311
+ const lo = Math.min(anchorIdx, nr0);
1312
+ const hi = Math.max(anchorIdx, nr0);
1313
+ setSelected(new Set(rows.slice(lo, hi + 1).map((row) => row.id)));
1314
+ setSelCell({ id: rows[nr0].id, key: selCell.key });
1315
+ clipboardProxyRef.current?.select();
1316
+ return;
1317
+ }
1201
1318
  const r = rows.findIndex((row) => row.id === selCell.id);
1202
1319
  const c = columns.findIndex((col) => col.key === selCell.key);
1203
1320
  if (r < 0 || c < 0)
@@ -1236,6 +1353,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1236
1353
  else if (e.key === "Escape") {
1237
1354
  setSelCell(null);
1238
1355
  setSelAnchor(null);
1356
+ setRowSelectAnchor(null);
1239
1357
  return;
1240
1358
  }
1241
1359
  else if (e.key === "Enter") {
@@ -1243,8 +1361,13 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1243
1361
  // already its own live widget (#385); Enter there is a no-op, same
1244
1362
  // as any other non-editable cell that isn't wired to onRowClick.
1245
1363
  // `select`/`reference` DO have one now (like every other editor),
1246
- // so Enter opens them too.
1247
- if (columns[c]?.editable && columns[c]?.editor !== "switch") {
1364
+ // so Enter opens them too. `rating` (#760) and `checkbox` (#761)
1365
+ // join `switch` in the no-separate-editing-mode exception -- same
1366
+ // always-live shape.
1367
+ if (columns[c]?.editable &&
1368
+ columns[c]?.editor !== "switch" &&
1369
+ columns[c]?.editor !== "rating" &&
1370
+ columns[c]?.editor !== "checkbox") {
1248
1371
  e.preventDefault();
1249
1372
  openEditing(selCell.id, selCell.key);
1250
1373
  }
@@ -1260,8 +1383,10 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1260
1383
  // A plain arrow key moves the single-cell selection and drops any active
1261
1384
  // range -- Attio's own range trigger is Shift+Click only (G-101), arrow
1262
1385
  // keys were never measured as a range-extend gesture, so this doesn't
1263
- // invent one.
1386
+ // invent one. Also drops an in-progress Shift+Arrow row-selection drag
1387
+ // (crisp/#749) for the same reason -- a plain arrow ends that gesture.
1264
1388
  setSelAnchor(null);
1389
+ setRowSelectAnchor(null);
1265
1390
  setSelCell({ id: rows[nr].id, key: columns[nc].key });
1266
1391
  // keeps the clipboard proxy's selection non-collapsed after a move, so
1267
1392
  // a Cmd+C right after an arrow key still has something native `copy`
@@ -1779,6 +1904,22 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1779
1904
  if (col.editable && col.editor === "switch") {
1780
1905
  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));
1781
1906
  }
1907
+ // crisp/#760: `editor: "rating"` is the same always-
1908
+ // live shape as `switch` above -- clicking a star IS
1909
+ // the commit gesture, confirmed live.
1910
+ if (col.editable && col.editor === "rating") {
1911
+ const n = typeof value === "number" ? value : Number(value);
1912
+ 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)(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));
1913
+ }
1914
+ // crisp/#761: `editor: "checkbox"` is a THIRD always-
1915
+ // live boolean shape, distinct from `switch` --
1916
+ // confirmed live against Attio's own "Checkbox"
1917
+ // attribute type, which renders a plain square, not a
1918
+ // toggle. Reuses crisp's own `Checkbox` (already the
1919
+ // measured 16px/r6/brand-fill grid checkbox).
1920
+ if (col.editable && col.editor === "checkbox") {
1921
+ 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)(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));
1922
+ }
1782
1923
  if (isEditing &&
1783
1924
  (col.editor === "select" ||
1784
1925
  col.editor === "reference" ||
@@ -1834,9 +1975,25 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1834
1975
  i >= range.c0 &&
1835
1976
  i <= range.c1);
1836
1977
  })();
1837
- const rendered = col.render
1978
+ const rowHoveredForCell = hoveredRowId === row.id;
1979
+ const baseRendered = col.render
1838
1980
  ? col.render(value, row)
1839
- : renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale, openPreviewLabel, hoveredRowId === row.id);
1981
+ : renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale, openPreviewLabel, rowHoveredForCell);
1982
+ // crisp/#601 supersession follow-up: `onOpenComments`/
1983
+ // `onOpenPreview`/`commentCount` used to only do
1984
+ // anything inside `renderTyped`'s `"entity"` case,
1985
+ // which already renders its own actions inline (see
1986
+ // `renderCellActions` there) -- silently dropping them
1987
+ // on any OTHER type, and on a custom `render` of any
1988
+ // type, since neither path went through that switch.
1989
+ // This composes the same actions alongside whatever
1990
+ // `rendered` is instead, decoupling the props from
1991
+ // requiring `type: "entity"`.
1992
+ const cellActions = col.type !== "entity" &&
1993
+ (col.onOpenComments || col.onOpenPreview)
1994
+ ? renderCellActions(col, row, rowHoveredForCell, openPreviewLabel)
1995
+ : null;
1996
+ const rendered = cellActions ? ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-cell-actions-row", children: [baseRendered, cellActions] })) : (baseRendered);
1840
1997
  const isExpanded = expandedCell?.id === row.id &&
1841
1998
  expandedCell?.key === col.key;
1842
1999
  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 }))] }));
@@ -1844,7 +2001,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1844
2001
  ? "crisp-datatable-editable"
1845
2002
  : undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
1846
2003
  ? ""
1847
- : undefined, "data-in-range": inRange ? "" : undefined, "data-cell-expanded": isExpanded ? "" : undefined, onClick: (e) => {
2004
+ : undefined, "data-in-range": inRange ? "" : undefined, "data-cell-expanded": isExpanded ? "" : undefined, "data-enriched": col.enriched ? "" : undefined, onClick: (e) => {
1848
2005
  // the keyboard model's anchor -- a single click
1849
2006
  // only selects. A genuine double-click (or Enter
1850
2007
  // on the selected cell) opens the editor (#411),
@@ -1859,6 +2016,11 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1859
2016
  }
1860
2017
  else {
1861
2018
  setSelAnchor(null);
2019
+ // crisp/#749: a plain click also ends an
2020
+ // in-progress Shift+Arrow row-selection drag
2021
+ // -- Shift+Click itself doesn't touch it,
2022
+ // the two gestures are independent.
2023
+ setRowSelectAnchor(null);
1862
2024
  }
1863
2025
  setSelCell({ id: row.id, key: col.key });
1864
2026
  // Focuses the clipboard proxy, not `gridRef`
@@ -1899,6 +2061,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1899
2061
  // selected (crisp/#598) -- collapses any
1900
2062
  // active range, same as a plain left-click.
1901
2063
  setSelAnchor(null);
2064
+ setRowSelectAnchor(null);
1902
2065
  setSelCell({ id: row.id, key: col.key });
1903
2066
  clipboardProxyRef.current?.focus();
1904
2067
  clipboardProxyRef.current?.select();
@@ -138,6 +138,12 @@ const config = (0, core_1.defineRecipe)({
138
138
  & tbody tr[data-selected] td { background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 10%, ${tokens_1.contract.bgSurface}); }
139
139
  & tbody tr[data-selected]:hover td { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 10%, ${tokens_1.contract.bgSurface})); }
140
140
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, ${tokens_1.contract.bgSurface}); cursor: text; }
141
+ /* crisp/#758: AI-enriched column marker, ported from RecordsTable's own
142
+ already-measured value -- a full-bleed #9b69ff (bgPurple) wash at 4%,
143
+ composited OVER whatever row-state background the cell already has
144
+ (hover, selected), which is why it's an ::after overlay and not a
145
+ plain background. */
146
+ & td[data-enriched]::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: color-mix(in srgb, ${tokens_1.contract.bgPurple} 4%, transparent); }
141
147
  & .crisp-datatable-edit { width: 100%; height: 28px; margin: 0; padding: 0 ${tokens_1.contract.space["1.5"]}; border: none; border-radius: ${tokens_1.contract.radius.sm}; background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; outline: none; vertical-align: middle; }
142
148
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; }
143
149
  /* An async onEdit's in-flight state (#385) -- the field/picker dims and
@@ -221,6 +227,13 @@ const config = (0, core_1.defineRecipe)({
221
227
  & th[data-drop-after]::after { inset-inline-end: -1.5px; }
222
228
  /* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
223
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%; }
224
237
  /* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text,
225
238
  plain hover vs rest) that Attio's underline is unconditional, not a
226
239
  hover reveal -- identical at rest and on hover: text-decoration-color
@@ -262,6 +275,23 @@ const config = (0, core_1.defineRecipe)({
262
275
  link. */
263
276
  & .crisp-datatable-link { color: ${tokens_1.contract.fgPrimary}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
264
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; }
265
295
  /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
266
296
  a leading 20px grip column, a 16px grip icon, and the row's own
267
297
  hairline doubling as the drop indicator (it tints toward the brand
@@ -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; }