@crisp-ui-kit/crisp 0.54.10 → 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 (47) hide show
  1. package/dist/cjs/components/datatable/datatable.js +121 -10
  2. package/dist/cjs/components/datatable/datatable.recipe.js +24 -0
  3. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
  4. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +93 -43
  5. package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +28 -9
  6. package/dist/cjs/product/recordpage/column.js +19 -0
  7. package/dist/cjs/product/recordpage/recordpage.js +5 -4
  8. package/dist/cjs/product/recordpage/test-fixtures.js +55 -0
  9. package/dist/cjs/product/recordstable/index.js +1 -1
  10. package/dist/cjs/product/recordstable/recordstable.js +5 -0
  11. package/dist/cjs/product/savedviews/savedviews.recipe.js +13 -11
  12. package/dist/cjs/product/taskstable/taskstable.js +5 -0
  13. package/dist/datatable.css +14 -0
  14. package/dist/datatable.layered.css +14 -0
  15. package/dist/esm/components/datatable/datatable.js +121 -10
  16. package/dist/esm/components/datatable/datatable.recipe.js +24 -0
  17. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
  18. package/dist/esm/components/tabletoolbar/tabletoolbar.js +93 -43
  19. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +28 -9
  20. package/dist/esm/product/recordpage/column.js +16 -0
  21. package/dist/esm/product/recordpage/recordpage.js +2 -1
  22. package/dist/esm/product/recordpage/test-fixtures.js +52 -0
  23. package/dist/esm/product/recordstable/index.js +1 -1
  24. package/dist/esm/product/recordstable/recordstable.js +5 -0
  25. package/dist/esm/product/savedviews/savedviews.recipe.js +13 -11
  26. package/dist/esm/product/taskstable/taskstable.js +5 -0
  27. package/dist/recordstable.css +19 -2
  28. package/dist/recordstable.layered.css +19 -2
  29. package/dist/savedviews.css +6 -18
  30. package/dist/savedviews.layered.css +6 -18
  31. package/dist/styles.css +62 -23
  32. package/dist/styles.layered.css +62 -23
  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/tabletoolbar/tabletoolbar.d.ts +26 -1
  37. package/dist/types/product/recordpage/column.d.ts +112 -0
  38. package/dist/types/product/recordpage/recordpage.d.ts +1 -1
  39. package/dist/types/product/recordpage/test-fixtures.d.ts +3 -0
  40. package/dist/types/product/recordpanel/recordpanel.d.ts +1 -1
  41. package/dist/types/product/recordstable/index.d.ts +1 -1
  42. package/dist/types/product/recordstable/recordstable.d.ts +6 -1
  43. package/dist/types/product/taskstable/taskstable.d.ts +5 -0
  44. package/package.json +1 -1
  45. /package/dist/cjs/product/{recordstable → recordpage}/editable-field.js +0 -0
  46. /package/dist/esm/product/{recordstable → recordpage}/editable-field.js +0 -0
  47. /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);
@@ -1033,8 +1106,9 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1033
1106
  * `editor: "select"`/`"reference"` open their own picker instead of this
1034
1107
  * native-input family, but through the SAME double-click gate (#453) --
1035
1108
  * 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).
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).
1038
1112
  */
1039
1113
  function editorFor(col) {
1040
1114
  switch (col.type) {
@@ -1287,8 +1361,13 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1287
1361
  // already its own live widget (#385); Enter there is a no-op, same
1288
1362
  // as any other non-editable cell that isn't wired to onRowClick.
1289
1363
  // `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") {
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") {
1292
1371
  e.preventDefault();
1293
1372
  openEditing(selCell.id, selCell.key);
1294
1373
  }
@@ -1825,6 +1904,22 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1825
1904
  if (col.editable && col.editor === "switch") {
1826
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));
1827
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
+ }
1828
1923
  if (isEditing &&
1829
1924
  (col.editor === "select" ||
1830
1925
  col.editor === "reference" ||
@@ -1880,9 +1975,25 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1880
1975
  i >= range.c0 &&
1881
1976
  i <= range.c1);
1882
1977
  })();
1883
- const rendered = col.render
1978
+ const rowHoveredForCell = hoveredRowId === row.id;
1979
+ const baseRendered = col.render
1884
1980
  ? col.render(value, row)
1885
- : 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);
1886
1997
  const isExpanded = expandedCell?.id === row.id &&
1887
1998
  expandedCell?.key === col.key;
1888
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 }))] }));
@@ -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
@@ -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; }
@@ -40,11 +40,41 @@ exports.TableToolbar = TableToolbar;
40
40
  const jsx_runtime_1 = require("react/jsx-runtime");
41
41
  const lucide_react_1 = require("lucide-react");
42
42
  const React = __importStar(require("react"));
43
+ const react_dom_1 = require("react-dom");
43
44
  const core_1 = require("../../core");
44
45
  const datepicker_1 = require("../datepicker");
45
46
  const menu_1 = require("../menu");
46
47
  const select_1 = require("../select");
47
48
  const tabletoolbar_recipe_1 = require("./tabletoolbar.recipe");
49
+ // crisp/#766, #772: portals View settings/Import-Export into a
50
+ // consumer-supplied target (e.g. a `<span>` rendered next to `SavedViews`,
51
+ // or another next to the primary action) instead of this toolbar's own row,
52
+ // when one is given -- a plain passthrough otherwise, so the default (no
53
+ // ref given) stays byte-identical to before this existed. Kept outside the
54
+ // main component (no closure over its state) so it's a stable reference
55
+ // across renders.
56
+ //
57
+ // #772 fix: every `.crisp-tabletoolbar-*` chrome rule is nested under this
58
+ // recipe's own root class (`& .crisp-tabletoolbar-tool { ... }`), which
59
+ // compiles to a real CSS descendant combinator -- it needs actual DOM
60
+ // containment under an element carrying that class. A portal target
61
+ // rendered by the CONSUMER, outside this component's own root `<div>`,
62
+ // breaks that containment even though React still considers the portaled
63
+ // content a child for event/context purposes: the classes were still
64
+ // applied, they just matched nothing, so the buttons rendered as unstyled
65
+ // native chrome. Same failure class `select.recipe.ts`'s own doc comment
66
+ // describes for its listbox portal to `document.body`. Fixed the same way
67
+ // `ToolbarTool` (below) already lets a bare `.crisp-tabletoolbar-tool` be
68
+ // used outside an actual `TableToolbar` tree: wrap the portaled content in
69
+ // its own `tabletoolbarRecipe()`-classed span. `display: contents` on that
70
+ // recipe's own base rule means the wrapper contributes no box of its own,
71
+ // so it changes nothing about layout -- only re-establishes the selector
72
+ // match.
73
+ function ViewBarSlot({ target, children, }) {
74
+ return target
75
+ ? (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("span", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)(), children: children }), target)
76
+ : children;
77
+ }
48
78
  // #592: shared by every direction-bearing ChevronRight usage below (the
49
79
  // field picker's own disclosure chevron, a flattened search result's
50
80
  // breadcrumb separator, and a filter chip's own breadcrumb separator) so
@@ -338,7 +368,7 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
338
368
  * this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
339
369
  * Created by), which one shared list can't reproduce.
340
370
  */
341
- function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, showSort = true, showViewSettings = true, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, importExportLabel: importExportLabelProp, onImportCsv, onExportCsv, onExportExcel, importExportHelpHref, importCsvLabel: importCsvLabelProp, exportCsvLabel: exportCsvLabelProp, exportExcelLabel: exportExcelLabelProp, learnImportExportLabel: learnImportExportLabelProp, backLabel: backLabelProp, nestedFieldPlaceholder, ...rest }) {
371
+ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, showSort = true, showViewSettings = true, viewSettingsRef, importExportRef, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, importExportLabel: importExportLabelProp, onImportCsv, onExportCsv, onExportExcel, importExportHelpHref, importCsvLabel: importCsvLabelProp, exportCsvLabel: exportCsvLabelProp, exportExcelLabel: exportExcelLabelProp, learnImportExportLabel: learnImportExportLabelProp, backLabel: backLabelProp, nestedFieldPlaceholder, ...rest }) {
342
372
  // crisp/#471: an explicit prop always wins; missing, it reads the nearest
343
373
  // CrispMessagesProvider's translation of the English default below, or
344
374
  // that default itself with no provider — unchanged from before this
@@ -408,6 +438,19 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
408
438
  setShownKeysState(next);
409
439
  onShownKeysChange?.(next);
410
440
  };
441
+ // crisp/#766: a ref's `.current` is null during THIS render (a ref only
442
+ // attaches during commit, and the consumer's own target element may not
443
+ // even exist in the DOM yet on first paint) -- an effect after mount picks
444
+ // it up and re-renders once, the same one-render-late pattern the floating
445
+ // popovers elsewhere in this file already use for a portal target.
446
+ const [viewSettingsTarget, setViewSettingsTarget] = React.useState(null);
447
+ React.useEffect(() => {
448
+ setViewSettingsTarget(viewSettingsRef?.current ?? null);
449
+ }, [viewSettingsRef]);
450
+ const [importExportTarget, setImportExportTarget] = React.useState(null);
451
+ React.useEffect(() => {
452
+ setImportExportTarget(importExportRef?.current ?? null);
453
+ }, [importExportRef]);
411
454
  const [openMenu, setOpenMenu] = React.useState(null);
412
455
  const [dragSort, setDragSort] = React.useState(null);
413
456
  const [filterQuery, setFilterQuery] = React.useState("");
@@ -641,16 +684,23 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
641
684
  // worse than an imperfect word order); `crispMessages` rather than a
642
685
  // second `useCrispMessage` call, since this only fires on the branch that
643
686
  // needs it.
687
+ // crisp/#788: "Sorted by" and the field name are NOT a font-weight split
688
+ // (both measure 500/medium live) -- the difference is colour/opacity only:
689
+ // "Sorted by" at 63% black, the field name at full-strength near-black
690
+ // (rgb(16,17,18)). The "+N" suffix is smaller (12px vs 14px) and its own
691
+ // mid-grey (rgb(80,81,85)), not muted-63%-black either -- three distinct
692
+ // treatments, not two. `sortField` isolates the resolved label so the
693
+ // default (non-`sortTriggerLabel`) render can wrap each part in its own
694
+ // span; `sortedLabel` stays a single string for the `sortTriggerLabel`
695
+ // escape hatch (a caller-supplied formatter can't be split, by definition)
696
+ // and the empty-state `sortLabel`.
697
+ const sortField = resolveColumnPath(sortColumns, sorts[0]?.key ?? "").at(-1)?.label;
644
698
  const sortedLabel = sortTriggerLabel
645
699
  ? sortTriggerLabel(sorts, sortColumns)
646
700
  : sorts.length
647
- ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${
648
- // #754: sorts[0].key may be a dot-joined nested path
649
- // ("team.jobTitle") — resolve it the same way the chip and sort
650
- // row do, rather than a flat find that fails on a nested key.
651
- resolveColumnPath(sortColumns, sorts[0].key).at(-1)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
701
+ ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortField}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
652
702
  : sortLabel;
653
- return ((0, jsx_runtime_1.jsx)("div", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && ((0, jsx_runtime_1.jsxs)("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
703
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : " crisp-tabletoolbar-tool--dashed"}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortTriggerLabel || !sorts.length ? (sortedLabel) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-sorted-prefix", children: crispMessages["Sorted by"] ?? "Sorted by" }), " ", (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-sorted-field", children: sortField }), sorts.length > 1 && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sorted-count", children: [" ", "+", sorts.length - 1] }))] }))] }), openMenu === "sort" && ((0, jsx_runtime_1.jsxs)("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
654
704
  if (dragSort !== null)
655
705
  e.preventDefault();
656
706
  }, onDrop: () => {
@@ -687,7 +737,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
687
737
  label: ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), descendingLabel] })),
688
738
  keywords: descendingLabel,
689
739
  },
690
- ], "aria-label": sortDirectionLabel }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-foot", children: (0, jsx_runtime_1.jsxs)("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] })), showSort && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost${activeFilters ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => {
740
+ ], "aria-label": sortDirectionLabel }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-foot", children: (0, jsx_runtime_1.jsxs)("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] })), showSort && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool${activeFilters ? " crisp-tabletoolbar-tool--active" : " crisp-tabletoolbar-tool--dashed"}`, onClick: () => {
691
741
  setFieldPickerPath([]);
692
742
  setOpenMenu(openMenu === "filter" ? null : "filter");
693
743
  }, "aria-haspopup": "dialog", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
@@ -786,43 +836,43 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
786
836
  }, children: label }, label))) })] })) : ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", type: col?.type === "number" ? "number" : "text", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
787
837
  // biome-ignore lint/a11y/noAutofocus: focus the value field
788
838
  autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })) }))] }, cond.id));
789
- }), showViewSettings && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), showViewSettings && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost", onClick: () => {
790
- if (openMenu === "viewsettings") {
791
- setOpenMenu(null);
792
- }
793
- else {
794
- setAddingAttribute(false);
795
- setFieldPickerPath([]);
796
- setFilterQuery("");
797
- setOpenMenu("viewsettings");
798
- }
799
- }, "aria-haspopup": "dialog", "aria-expanded": openMenu === "viewsettings", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.SlidersHorizontal, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), viewSettingsLabel] }), openMenu === "viewsettings" &&
800
- (addingAttribute ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": addAttributeToViewLabel, children: nestedFieldPicker(hiddenAttrs, showAttr, searchAttributesPlaceholder, searchAttributesPlaceholder) })) : ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, children: [shownAttrs.map((col, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
801
- if (dragAttr !== null)
802
- e.preventDefault();
803
- }, onDrop: () => {
804
- if (dragAttr !== null)
805
- moveAttr(dragAttr, i);
806
- setDragAttr(null);
807
- }, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderAttributeLabel, draggable: true, onDragStart: () => setDragAttr(i), onDragEnd: () => setDragAttr(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel crisp-tabletoolbar-sort-field", children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : null, col.label] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => hideAttr(col.key), "aria-label": hideAttributeLabel?.(col.label) ??
808
- `${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => {
809
- setAddingAttribute(true);
810
- setFieldPickerPath([]);
811
- setFilterQuery("");
812
- }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] })), (onImportCsv ||
839
+ }), showViewSettings && !viewSettingsTarget && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), showViewSettings && ((0, jsx_runtime_1.jsx)(ViewBarSlot, { target: viewSettingsTarget, children: (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--raised", onClick: () => {
840
+ if (openMenu === "viewsettings") {
841
+ setOpenMenu(null);
842
+ }
843
+ else {
844
+ setAddingAttribute(false);
845
+ setFieldPickerPath([]);
846
+ setFilterQuery("");
847
+ setOpenMenu("viewsettings");
848
+ }
849
+ }, "aria-haspopup": "dialog", "aria-expanded": openMenu === "viewsettings", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.SlidersHorizontal, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), viewSettingsLabel, (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }), openMenu === "viewsettings" &&
850
+ (addingAttribute ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": addAttributeToViewLabel, children: nestedFieldPicker(hiddenAttrs, showAttr, searchAttributesPlaceholder, searchAttributesPlaceholder) })) : ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, children: [shownAttrs.map((col, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
851
+ if (dragAttr !== null)
852
+ e.preventDefault();
853
+ }, onDrop: () => {
854
+ if (dragAttr !== null)
855
+ moveAttr(dragAttr, i);
856
+ setDragAttr(null);
857
+ }, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderAttributeLabel, draggable: true, onDragStart: () => setDragAttr(i), onDragEnd: () => setDragAttr(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel crisp-tabletoolbar-sort-field", children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : null, col.label] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => hideAttr(col.key), "aria-label": hideAttributeLabel?.(col.label) ??
858
+ `${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => {
859
+ setAddingAttribute(true);
860
+ setFieldPickerPath([]);
861
+ setFilterQuery("");
862
+ }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] }) })), (onImportCsv ||
813
863
  onExportCsv ||
814
864
  onExportExcel ||
815
- importExportHelpHref) && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--raised", onClick: () => setOpenMenu(openMenu === "importexport" ? null : "importexport"), "aria-haspopup": "menu", "aria-expanded": openMenu === "importexport", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Upload, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), importExportLabel, (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }), openMenu === "importexport" && ((0, jsx_runtime_1.jsxs)("div", { ref: importExportMenuRef, className: "crisp-tabletoolbar-menu", role: "menu", "aria-label": importExportLabel, onKeyDown: onImportExportKeyDown, children: [onImportCsv && ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
816
- onImportCsv();
817
- setOpenMenu(null);
818
- }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Upload, { size: 15, strokeWidth: 1.75 }), importCsvLabel] })), onImportCsv && (onExportCsv || onExportExcel) && ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-tabletoolbar-menu-sep" })), onExportCsv && ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
819
- onExportCsv();
820
- setOpenMenu(null);
821
- }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), exportCsvLabel] })), onExportExcel && ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
822
- onExportExcel();
823
- setOpenMenu(null);
824
- }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), exportExcelLabel] })), importExportHelpHref &&
825
- (onImportCsv || onExportCsv || onExportExcel) && ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-tabletoolbar-menu-sep" })), importExportHelpHref && ((0, jsx_runtime_1.jsxs)("a", { className: "crisp-tabletoolbar-menu-item", role: "menuitem", href: importExportHelpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 15, strokeWidth: 1.75 }), learnImportExportLabel] }))] }))] })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-spacer" }), count != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
865
+ importExportHelpHref) && ((0, jsx_runtime_1.jsx)(ViewBarSlot, { target: importExportTarget, children: (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--raised", onClick: () => setOpenMenu(openMenu === "importexport" ? null : "importexport"), "aria-haspopup": "menu", "aria-expanded": openMenu === "importexport", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Upload, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), importExportLabel, (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }), openMenu === "importexport" && ((0, jsx_runtime_1.jsxs)("div", { ref: importExportMenuRef, className: "crisp-tabletoolbar-menu", role: "menu", "aria-label": importExportLabel, onKeyDown: onImportExportKeyDown, children: [onImportCsv && ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
866
+ onImportCsv();
867
+ setOpenMenu(null);
868
+ }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Upload, { size: 15, strokeWidth: 1.75 }), importCsvLabel] })), onImportCsv && (onExportCsv || onExportExcel) && ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-tabletoolbar-menu-sep" })), onExportCsv && ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
869
+ onExportCsv();
870
+ setOpenMenu(null);
871
+ }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), exportCsvLabel] })), onExportExcel && ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
872
+ onExportExcel();
873
+ setOpenMenu(null);
874
+ }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), exportExcelLabel] })), importExportHelpHref &&
875
+ (onImportCsv || onExportCsv || onExportExcel) && ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-tabletoolbar-menu-sep" })), importExportHelpHref && ((0, jsx_runtime_1.jsxs)("a", { className: "crisp-tabletoolbar-menu-item", role: "menuitem", href: importExportHelpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 15, strokeWidth: 1.75 }), learnImportExportLabel] }))] }))] }) })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-spacer" }), count != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
826
876
  }
827
877
  /**
828
878
  * The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone
@@ -74,14 +74,21 @@ const filterChipChrome = `
74
74
  & .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: ${tokens_1.contract.space["1"]}; margin-top: ${tokens_1.contract.space["2"]}; padding: 0 ${tokens_1.contract.space["1"]}; }
75
75
  & .crisp-tabletoolbar-datepop-preset { height: ${tokens_1.contract.size.chip}; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.chip}; background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.sm}; cursor: pointer; }
76
76
  & .crisp-tabletoolbar-datepop-preset:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 8%, transparent); }`;
77
- // #595: the Import/Export trigger — measured live, `app.attio.com`'s own
78
- // view bar: `28px` tall, `radius.md` (8px), background `bgSurfaceMuted`
79
- // (`rgb(246,247,247)`, exact match) and `elevation.field`'s exact 3-layer
80
- // shadow (`inset 0 0 0 1px transparent, 0 0 2px rgba(28,40,64,.18), 0 1px 3px
81
- // rgba(0,0,0,.04)`) — a "raised chrome button" distinct from Sort/Filter/View
82
- // settings' plain ringed `.tool` (no background, no elevation shadow), and
83
- // the only trigger of the four with a trailing chevron (2 icons total,
84
- // confirmed live). Overrides `.tool`'s own base background/shadow by cascade
77
+ // #595, corrected #772 (re-measured live 2026-09-25): the view bar's raised
78
+ // trigger chrome — `app.attio.com`'s own view bar: `28px` tall, `radius.md`
79
+ // (8px), background `bgSurface` (white, `rgb(255,255,255)` exact match — the
80
+ // #595 pass originally measured `bgSurfaceMuted`/`rgb(246,247,247)`, which
81
+ // re-measuring live this pass shows was wrong, not a later Attio change: ALL
82
+ // THREE of the switcher, View settings and Import/Export read pure white)
83
+ // and `elevation.field`'s exact 3-layer shadow (`inset 0 0 0 1px transparent,
84
+ // 0 0 2px rgba(28,40,64,.18), 0 1px 3px rgba(0,0,0,.04)`) — distinct from
85
+ // Sort/Filter's plain ringed `.tool` (no elevation shadow; see `--dashed` in
86
+ // `tabletoolbar.chrome.ts` for their own OWN empty-state chrome). Originally
87
+ // built for Import/Export alone ("the only trigger... with a trailing
88
+ // chevron") — that was also wrong: View settings and the view switcher both
89
+ // carry one too, confirmed live (3 children each: icon, label, chevron).
90
+ // Now shared by View settings' own trigger (`tabletoolbar.tsx`) as well as
91
+ // Import/Export's. Overrides `.tool`'s own base background/shadow by cascade
85
92
  // order (this block is appended after `toolbarChromeBody`, which defines
86
93
  // them), the same way `.tool--ghost` already does. `border`/`font-family`/
87
94
  // `cursor` are repeated here too, even though `.tool`'s own base rule (in
@@ -91,7 +98,18 @@ const filterChipChrome = `
91
98
  // behind `chrome.ts`'s function call is invisible to it; harmless repeats,
92
99
  // not a real gap.
93
100
  const importExportChrome = `
94
- & .crisp-tabletoolbar-tool--raised { border: none; background: ${tokens_1.contract.bgSurfaceMuted}; box-shadow: ${tokens_1.elevation.field}; font-family: inherit; cursor: pointer; }`;
101
+ & .crisp-tabletoolbar-tool--raised { border: none; background: ${tokens_1.contract.bgSurface}; box-shadow: ${tokens_1.elevation.field}; font-family: inherit; cursor: pointer; }`;
102
+ // crisp/#788: the Sort trigger's own "Sorted by X (+N)" label, measured live
103
+ // against Attio's own chip -- three parts, not one flat string, and NOT a
104
+ // font-weight split (all three read 500/medium): "Sorted by" is muted (63%
105
+ // black), the resolved field name is full-strength near-black, and a
106
+ // trailing "+N" (when more than one sort is active) is both smaller (12px
107
+ // vs 14px) and its own mid-grey, distinct from the "Sorted by" muted tone.
108
+ // TableToolbar-only, same reasoning as `filterChipChrome` above (RecordsTable
109
+ // keeps its own older Sort label markup, not touched here).
110
+ const sortedLabelChrome = `
111
+ & .crisp-tabletoolbar-sorted-prefix { color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); }
112
+ & .crisp-tabletoolbar-sorted-count { font-size: ${tokens_1.contract.text.sm}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 73%, transparent); }`;
95
113
  // The table toolbar, measured: 28px controls, the Sort/Filter buttons and
96
114
  // the menus they open, sort-condition rows, and the filter chips. The chrome
97
115
  // itself lives in ./tabletoolbar.chrome.ts because RecordsTable renders the
@@ -110,6 +128,7 @@ ${(0, tabletoolbar_chrome_1.toolbarChromeHead)("tabletoolbar")}
110
128
  ${(0, tabletoolbar_chrome_1.toolbarChromeBody)("tabletoolbar")}
111
129
  ${filterChipChrome}
112
130
  ${importExportChrome}
131
+ ${sortedLabelChrome}
113
132
  ${core_1.rtlFlipCss}
114
133
  `,
115
134
  });
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tagTone = tagTone;
4
+ // deterministic pastel colour per tag value (a select option keeps its colour)
5
+ const TAG_TONES = [
6
+ "brand",
7
+ "success",
8
+ "warning",
9
+ "danger",
10
+ "purple",
11
+ "cyan",
12
+ "orange",
13
+ ];
14
+ function tagTone(t) {
15
+ let h = 0;
16
+ for (let i = 0; i < t.length; i++)
17
+ h = (h * 31 + t.charCodeAt(i)) >>> 0;
18
+ return TAG_TONES[h % TAG_TONES.length];
19
+ }