@crisp-ui-kit/crisp 0.54.9 → 0.54.10

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.
@@ -611,6 +611,19 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
611
611
  // arrow-key nav, Enter) is unaffected -- a plain click resets `selAnchor`
612
612
  // to null, collapsing back to single-cell mode.
613
613
  const [selAnchor, setSelAnchor] = React.useState(null);
614
+ // crisp/#749: Shift+ArrowUp/Down extends the ROW (checkbox) selection --
615
+ // confirmed live on Attio's own Companies grid, a DIFFERENT gesture from
616
+ // `selAnchor` above (that's Shift+CLICK extending a cell-value range;
617
+ // this issue's own text is explicit the two are independent, and arrow
618
+ // keys were never measured as a cell-range trigger, G-101). Same
619
+ // fixed-anchor/moving-cell shape as `selAnchor` -- a spreadsheet's own
620
+ // Shift+Arrow both moves the active cell AND grows/shrinks the selection
621
+ // relative to wherever the drag started, which recomputing the anchor→
622
+ // new-row span from scratch on every press gives for free (arrowing back
623
+ // toward the anchor shrinks the same way arrowing away grows it). Reset
624
+ // to null on any plain (non-shift) navigation or click, same as
625
+ // `selAnchor`'s own reset points.
626
+ const [rowSelectAnchor, setRowSelectAnchor] = React.useState(null);
614
627
  const gridRef = React.useRef(null);
615
628
  // The native-copy-event proxy (crisp/#645): Attio "has no Mousetrap/
616
629
  // JS-keydown binding for c/mod+c ... it copies purely via the native
@@ -1198,6 +1211,36 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1198
1211
  setSelected(new Set(rows.map((rw) => rw.id)));
1199
1212
  return;
1200
1213
  }
1214
+ // crisp/#749: Shift+ArrowUp/Down extends the row-checkbox selection --
1215
+ // confirmed live, distinct from Shift+Click's cell-range gesture above
1216
+ // (that one is `selAnchor`; this is `rowSelectAnchor`, and the two
1217
+ // don't interact). Recomputes the whole anchor→destination span on
1218
+ // every press rather than accumulating, so arrowing back toward the
1219
+ // anchor shrinks the selection the same way arrowing away grows it --
1220
+ // the "shrink-from-the-anchor" behavior a spreadsheet's own Shift+Arrow
1221
+ // gives, which a plain add-on-each-press approach can't express.
1222
+ if (selectable &&
1223
+ e.shiftKey &&
1224
+ (e.key === "ArrowDown" || e.key === "ArrowUp")) {
1225
+ const r0 = rows.findIndex((row) => row.id === selCell.id);
1226
+ if (r0 < 0)
1227
+ return;
1228
+ const nr0 = e.key === "ArrowDown"
1229
+ ? Math.min(rows.length - 1, r0 + 1)
1230
+ : Math.max(0, r0 - 1);
1231
+ e.preventDefault();
1232
+ const anchorId = rowSelectAnchor ?? selCell.id;
1233
+ const anchorIdx = rows.findIndex((row) => row.id === anchorId);
1234
+ if (anchorIdx < 0)
1235
+ return;
1236
+ setRowSelectAnchor(anchorId);
1237
+ const lo = Math.min(anchorIdx, nr0);
1238
+ const hi = Math.max(anchorIdx, nr0);
1239
+ setSelected(new Set(rows.slice(lo, hi + 1).map((row) => row.id)));
1240
+ setSelCell({ id: rows[nr0].id, key: selCell.key });
1241
+ clipboardProxyRef.current?.select();
1242
+ return;
1243
+ }
1201
1244
  const r = rows.findIndex((row) => row.id === selCell.id);
1202
1245
  const c = columns.findIndex((col) => col.key === selCell.key);
1203
1246
  if (r < 0 || c < 0)
@@ -1236,6 +1279,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1236
1279
  else if (e.key === "Escape") {
1237
1280
  setSelCell(null);
1238
1281
  setSelAnchor(null);
1282
+ setRowSelectAnchor(null);
1239
1283
  return;
1240
1284
  }
1241
1285
  else if (e.key === "Enter") {
@@ -1260,8 +1304,10 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1260
1304
  // A plain arrow key moves the single-cell selection and drops any active
1261
1305
  // range -- Attio's own range trigger is Shift+Click only (G-101), arrow
1262
1306
  // keys were never measured as a range-extend gesture, so this doesn't
1263
- // invent one.
1307
+ // invent one. Also drops an in-progress Shift+Arrow row-selection drag
1308
+ // (crisp/#749) for the same reason -- a plain arrow ends that gesture.
1264
1309
  setSelAnchor(null);
1310
+ setRowSelectAnchor(null);
1265
1311
  setSelCell({ id: rows[nr].id, key: columns[nc].key });
1266
1312
  // keeps the clipboard proxy's selection non-collapsed after a move, so
1267
1313
  // a Cmd+C right after an arrow key still has something native `copy`
@@ -1844,7 +1890,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1844
1890
  ? "crisp-datatable-editable"
1845
1891
  : undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
1846
1892
  ? ""
1847
- : undefined, "data-in-range": inRange ? "" : undefined, "data-cell-expanded": isExpanded ? "" : undefined, onClick: (e) => {
1893
+ : undefined, "data-in-range": inRange ? "" : undefined, "data-cell-expanded": isExpanded ? "" : undefined, "data-enriched": col.enriched ? "" : undefined, onClick: (e) => {
1848
1894
  // the keyboard model's anchor -- a single click
1849
1895
  // only selects. A genuine double-click (or Enter
1850
1896
  // on the selected cell) opens the editor (#411),
@@ -1859,6 +1905,11 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1859
1905
  }
1860
1906
  else {
1861
1907
  setSelAnchor(null);
1908
+ // crisp/#749: a plain click also ends an
1909
+ // in-progress Shift+Arrow row-selection drag
1910
+ // -- Shift+Click itself doesn't touch it,
1911
+ // the two gestures are independent.
1912
+ setRowSelectAnchor(null);
1862
1913
  }
1863
1914
  setSelCell({ id: row.id, key: col.key });
1864
1915
  // Focuses the clipboard proxy, not `gridRef`
@@ -1899,6 +1950,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1899
1950
  // selected (crisp/#598) -- collapses any
1900
1951
  // active range, same as a plain left-click.
1901
1952
  setSelAnchor(null);
1953
+ setRowSelectAnchor(null);
1902
1954
  setSelCell({ id: row.id, key: col.key });
1903
1955
  clipboardProxyRef.current?.focus();
1904
1956
  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
@@ -416,6 +416,12 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
416
416
  // [team, company] is Team -> Company, etc. Reset to [] wherever
417
417
  // `filterQuery` already resets (opening the picker fresh), same lifetime.
418
418
  const [fieldPickerPath, setFieldPickerPath] = React.useState([]);
419
+ // #754: which sort row's own field picker is open (its index into
420
+ // `sorts`), or `null` for the normal sort-row list — the same
421
+ // "one sub-screen behind a flag" split View settings' own
422
+ // `addingAttribute` already uses, parameterized by index since more than
423
+ // one sort row exists.
424
+ const [sortFieldPickerIndex, setSortFieldPickerIndex] = React.useState(null);
419
425
  // filters the "isanyof" checkbox list, same lifetime as filterQuery above
420
426
  const [anyOfQuery, setAnyOfQuery] = React.useState("");
421
427
  const [dragAttr, setDragAttr] = React.useState(null);
@@ -429,7 +435,6 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
429
435
  // built here -- hiding an attribute is a trailing control instead, see
430
436
  // the hide button below).
431
437
  const [addingAttribute, setAddingAttribute] = React.useState(false);
432
- const [addAttrQuery, setAddAttrQuery] = React.useState("");
433
438
  // close any open menu on outside click (anything not inside a menu or its
434
439
  // trigger) or Escape
435
440
  React.useEffect(() => {
@@ -512,10 +517,27 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
512
517
  };
513
518
  // #590: shown attributes, in `shownKeys`' order -- a key with no matching
514
519
  // `viewColumns` entry (stale state after a column is removed upstream)
515
- // drops out rather than rendering a blank row.
516
- const shownAttrs = shownKeys
517
- .map((k) => viewColumns.find((c) => c.key === k))
518
- .filter((c) => c != null);
520
+ // drops out rather than rendering a blank row. #754: `shownKeys` can now
521
+ // carry a dot-joined nested path ("team.jobTitle") from the add-attribute
522
+ // screen's own nested picker, so each key resolves via the same
523
+ // `resolveColumnPath` the filter chip uses -- the LEAF column's icon
524
+ // drives the row's own icon, and the label is the full breadcrumb
525
+ // ("Team › Job title"), joined as plain text rather than the chip's own
526
+ // muted/chevron treatment, which was measured specifically for the
527
+ // filter chip and wasn't re-verified for this row shape.
528
+ const shownAttrs = shownKeys.flatMap((k) => {
529
+ const path = resolveColumnPath(viewColumns, k);
530
+ const leaf = path.at(-1);
531
+ if (!leaf)
532
+ return [];
533
+ return [
534
+ {
535
+ key: k,
536
+ icon: leaf.icon,
537
+ label: path.length > 1 ? path.map((c) => c.label).join(" › ") : leaf.label,
538
+ },
539
+ ];
540
+ });
519
541
  const hiddenAttrs = viewColumns.filter((c) => !shownKeys.includes(c.key));
520
542
  const moveAttr = (from, to) => {
521
543
  if (from === to)
@@ -529,7 +551,8 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
529
551
  const showAttr = (key) => {
530
552
  putShownKeys([...shownKeys, key]);
531
553
  setAddingAttribute(false);
532
- setAddAttrQuery("");
554
+ setFieldPickerPath([]);
555
+ setFilterQuery("");
533
556
  };
534
557
  // #594: the filter chip's kebab is a real role="menu" (crisp/#188's shared
535
558
  // model — arrows, Home/End, typeahead, Escape), not a hand-rolled div like
@@ -562,15 +585,20 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
562
585
  close: () => setOpenMenu(null),
563
586
  contentRef: importExportMenuRef,
564
587
  });
565
- // Shared between the "Filter" trigger's own add-condition picker and each
566
- // chip's field segment (#462 follow-up) — both search the same
567
- // `filterColumns` list and differ only in what a pick does. #592: also
568
- // handles drilling into a `nestedColumns` column — `fieldPickerPath`
569
- // tracks the current scope ([] is the top level), reset to [] wherever
570
- // the picker opens fresh (both trigger onClicks, above).
571
- const filterFieldPicker = (onSelect) => {
588
+ // #754: generalized from the Filter-only `filterFieldPicker` (#592) to
589
+ // also drive Sort's own field picker and View settings' add-attribute
590
+ // screen — all three offer the identical nested-drill-down construct on
591
+ // live Attio (confirmed: "Team" with a count badge appears, and drills
592
+ // into the same People-attribute screen, in all three pickers). Shared
593
+ // `fieldPickerPath`/`filterQuery` state across all three call sites is
594
+ // safe: `openMenu` already only ever names ONE open popover at a time,
595
+ // so only one of Filter/Sort/View-settings' own picker can be showing.
596
+ // `fieldPickerPath` tracks the current scope ([] is the top level),
597
+ // reset to [] wherever a picker opens fresh (every trigger's own
598
+ // onClick).
599
+ const nestedFieldPicker = (columns, onSelect, topLevelPlaceholder, topLevelLabel) => {
572
600
  const scope = fieldPickerPath.at(-1);
573
- const scopeColumns = scope ? (scope.nestedColumns ?? []) : filterColumns;
601
+ const scopeColumns = scope ? (scope.nestedColumns ?? []) : columns;
574
602
  const query = filterQuery.trim().toLowerCase();
575
603
  const direct = scopeColumns.filter((c) => c.label.toLowerCase().includes(query));
576
604
  // #592: one level of flattening from wherever the picker is currently
@@ -598,9 +626,9 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
598
626
  setFilterQuery("");
599
627
  }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: nestedFieldPlaceholder
600
628
  ? nestedFieldPlaceholder(scope.nestedObjectLabel ?? scope.label.toLowerCase())
601
- : `Search ${scope.nestedObjectLabel ?? scope.label.toLowerCase()} attributes…`, "aria-label": filterFieldLabel,
629
+ : `Search ${scope.nestedObjectLabel ?? scope.label.toLowerCase()} attributes…`, "aria-label": topLevelLabel,
602
630
  // biome-ignore lint/a11y/noAutofocus: focus the picker on open
603
- autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })] })) : ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterFieldPlaceholder, "aria-label": filterFieldLabel,
631
+ autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })] })) : ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: topLevelPlaceholder, "aria-label": topLevelLabel,
604
632
  // biome-ignore lint/a11y/noAutofocus: focus the picker on open
605
633
  autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })), scope && ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath(fieldPickerPath), children: [scope.icon ? ((0, jsx_runtime_1.jsx)(scope.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), scope.label] })), direct.map((col) => col.nestedColumns?.length ? ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--expand", onClick: () => drillIn(col), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), col.label, (0, jsx_runtime_1.jsx)(RtlChevronRight, { size: 15, className: "crisp-tabletoolbar-menu-item-chevron" })] }, col.key)) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath([...fieldPickerPath, col]), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), col.label] }, col.key))), nested.map(({ parent, col }) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath([...fieldPickerPath, parent, col]), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-menu-item-path", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-menu-item-path-parent", children: parent.label }), (0, jsx_runtime_1.jsx)(RtlChevronRight, { size: 15, className: "crisp-tabletoolbar-menu-item-path-sep" }), (0, jsx_runtime_1.jsx)("span", { children: col.label })] })] }, `${parent.key}.${col.key}`)))] }));
606
634
  };
@@ -616,7 +644,11 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
616
644
  const sortedLabel = sortTriggerLabel
617
645
  ? sortTriggerLabel(sorts, sortColumns)
618
646
  : sorts.length
619
- ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
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}` : ""}`
620
652
  : sortLabel;
621
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) => {
622
654
  if (dragSort !== null)
@@ -625,11 +657,26 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
625
657
  if (dragSort !== null)
626
658
  moveSort(dragSort, i);
627
659
  setDragSort(null);
628
- }, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderSortLabel, draggable: true, onDragStart: () => setDragSort(i), onDragEnd: () => setDragSort(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)(select_1.Select, { className: "crisp-tabletoolbar-sort-field", searchable: true, value: s.key, onValueChange: (k) => setSortField(i, k), options: sortColumns.map((c) => ({
629
- value: c.key,
630
- label: c.icon ? ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [(0, jsx_runtime_1.jsx)(c.icon, { size: 15, strokeWidth: 1.75 }), c.label] })) : (c.label),
631
- keywords: c.label,
632
- })), "aria-label": sortFieldLabel }), (0, jsx_runtime_1.jsx)(select_1.Select, { className: "crisp-tabletoolbar-sort-dir", value: s.dir, onValueChange: (d) => setSortDir(i, d), options: [
660
+ }, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderSortLabel, draggable: true, onDragStart: () => setDragSort(i), onDragEnd: () => setDragSort(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 15, strokeWidth: 1.75 }) }), (() => {
661
+ // #754: the sort row's own field picker -- the
662
+ // same nested-drill-down construct Filter's field
663
+ // picker already has (#592), measured live:
664
+ // clicking a sort row's field (or "Add sort") opens
665
+ // an identical picker, "Team" (a relationship
666
+ // field) drills into the same People-attribute
667
+ // screen Filter's own "Team" does.
668
+ const sortColPath = resolveColumnPath(sortColumns, s.key);
669
+ const sortCol = sortColPath.at(-1);
670
+ const pickerOpen = sortFieldPickerIndex === i;
671
+ return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-field-picker", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-sort-field-trigger", "aria-haspopup": "dialog", "aria-expanded": pickerOpen, "aria-label": sortFieldLabel, onClick: () => {
672
+ setFieldPickerPath([]);
673
+ setFilterQuery("");
674
+ setSortFieldPickerIndex(pickerOpen ? null : i);
675
+ }, children: [sortCol?.icon ? ((0, jsx_runtime_1.jsx)(sortCol.icon, { size: 15, strokeWidth: 1.75 })) : null, sortColPath.length > 1 ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fcond-path", children: sortColPath.map((c, pi) => ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [pi > 0 && ((0, jsx_runtime_1.jsx)(RtlChevronRight, { size: 14, className: "crisp-tabletoolbar-fcond-path-sep" })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fcond-path-seg", children: c.label })] }, c.key))) })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-sort-field-label", children: sortCol?.label }))] }), pickerOpen && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": sortFieldLabel, children: nestedFieldPicker(sortColumns, (key) => {
676
+ setSortField(i, key);
677
+ setSortFieldPickerIndex(null);
678
+ }, searchAttributesPlaceholder, sortFieldLabel) }))] }));
679
+ })(), (0, jsx_runtime_1.jsx)(select_1.Select, { className: "crisp-tabletoolbar-sort-dir", value: s.dir, onValueChange: (d) => setSortDir(i, d), options: [
633
680
  {
634
681
  value: "asc",
635
682
  label: ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpNarrowWide, { size: 15, strokeWidth: 1.75 }), ascendingLabel] })),
@@ -647,7 +694,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
647
694
  ? filterTriggerLabel(activeFilters)
648
695
  : activeFilters
649
696
  ? `${filterLabel} · ${activeFilters}`
650
- : filterLabel] }), openMenu === "filter" && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker(addFilter) }))] }), filters.map((cond) => {
697
+ : filterLabel] }), openMenu === "filter" && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: nestedFieldPicker(filterColumns, addFilter, filterFieldPlaceholder, filterFieldLabel) }))] }), filters.map((cond) => {
651
698
  // #592: cond.key may be a dot-joined nested path
652
699
  // ("team.twitterFollowerCount") — resolve the full path so the
653
700
  // field segment can render its breadcrumb; `col` (the leaf) still
@@ -683,7 +730,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
683
730
  ? col?.type === "date"
684
731
  ? formatFilterDate(cond.value)
685
732
  : cond.value
686
- : filterValuePlaceholder })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--kebab", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && ((0, jsx_runtime_1.jsx)("div", { ref: kebabMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--right crisp-tabletoolbar-menu--hdr", role: "menu", "aria-label": `${col?.label} ${filterOptionsLabel}`, onKeyDown: onKebabKeyDown, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), fieldOpen && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker((key) => changeFilterField(cond.id, key)) })), opOpen && ((0, jsx_runtime_1.jsx)("div", { ref: opMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--op", role: "menu", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, onKeyDown: onOpKeyDown, children: opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => pickOp(cond.id, op), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))) })), valOpen && showValueSeg && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--val", role: "dialog", "aria-label": `${col?.label} filter value`, children: cond.op === "isanyof" && col?.values ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
733
+ : filterValuePlaceholder })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--kebab", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && ((0, jsx_runtime_1.jsx)("div", { ref: kebabMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--right crisp-tabletoolbar-menu--hdr", role: "menu", "aria-label": `${col?.label} ${filterOptionsLabel}`, onKeyDown: onKebabKeyDown, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), fieldOpen && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: nestedFieldPicker(filterColumns, (key) => changeFilterField(cond.id, key), filterFieldPlaceholder, filterFieldLabel) })), opOpen && ((0, jsx_runtime_1.jsx)("div", { ref: opMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--op", role: "menu", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, onKeyDown: onOpKeyDown, children: opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => pickOp(cond.id, op), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))) })), valOpen && showValueSeg && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--val", role: "dialog", "aria-label": `${col?.label} filter value`, children: cond.op === "isanyof" && col?.values ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
687
734
  // biome-ignore lint/a11y/noAutofocus: focus the search field
688
735
  autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
689
736
  .filter((v) => v
@@ -745,17 +792,12 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
745
792
  }
746
793
  else {
747
794
  setAddingAttribute(false);
748
- setAddAttrQuery("");
795
+ setFieldPickerPath([]);
796
+ setFilterQuery("");
749
797
  setOpenMenu("viewsettings");
750
798
  }
751
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" &&
752
- (addingAttribute ? ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": addAttributeToViewLabel, children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: searchAttributesPlaceholder, "aria-label": searchAttributesPlaceholder,
753
- // biome-ignore lint/a11y/noAutofocus: focus the search box on open, same as Filter's field picker
754
- autoFocus: true, value: addAttrQuery, onChange: (e) => setAddAttrQuery(e.target.value) }), hiddenAttrs
755
- .filter((col) => col.label
756
- .toLowerCase()
757
- .includes(addAttrQuery.trim().toLowerCase()))
758
- .map((col) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => showAttr(col.key), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), col.label] }, col.key)))] })) : ((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) => {
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) => {
759
801
  if (dragAttr !== null)
760
802
  e.preventDefault();
761
803
  }, onDrop: () => {
@@ -763,7 +805,11 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
763
805
  moveAttr(dragAttr, i);
764
806
  setDragAttr(null);
765
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) ??
766
- `${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: () => setAddingAttribute(true), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] })), (onImportCsv ||
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 ||
767
813
  onExportCsv ||
768
814
  onExportExcel ||
769
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: () => {
@@ -56,6 +56,19 @@ const filterChipChrome = `
56
56
  & .crisp-tabletoolbar-fcond-seg--val { font-weight: ${tokens_1.contract.weight.medium}; max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
57
57
  & .crisp-tabletoolbar-fcond-seg--kebab { background: ${tokens_1.contract.bgSurface}; justify-content: center; width: 28px; padding: 0; border-radius: 0 ${tokens_1.contract.radius.md} ${tokens_1.contract.radius.md} 0; color: ${tokens_1.contract.fgMuted}; }
58
58
  & .crisp-tabletoolbar-fcond-seg--kebab:hover { color: ${tokens_1.contract.fgPrimary}; }
59
+ /* #754: the sort row's own field trigger, replacing a plain Select
60
+ with the same nested-drill-down popover Filter's field picker
61
+ already has — reuses Select's own already-measured trigger chrome
62
+ verbatim (its class is scoped under Select's own root class, so it
63
+ can't be reused as a bare class name outside an actual Select tree;
64
+ these values are copied, not re-measured). */
65
+ & .crisp-tabletoolbar-sort-field-picker { position: relative; display: inline-flex; flex: 1; min-width: 0; }
66
+ & .crisp-tabletoolbar-sort-field-trigger { box-sizing: border-box; display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: ${tokens_1.contract.size.menuRow}; width: 100%; min-width: 0; margin: 0; padding: 0 ${tokens_1.contract.space["1.5"]} 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 7%, transparent); font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; cursor: pointer; transition: background-color .12s ease, box-shadow .12s ease; }
67
+ & .crisp-tabletoolbar-sort-field-trigger:hover { background: ${tokens_1.contract.bgRaised}; }
68
+ & .crisp-tabletoolbar-sort-field-trigger[aria-expanded="true"],
69
+ & .crisp-tabletoolbar-sort-field-trigger:focus-visible { outline: none; box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; background: ${tokens_1.contract.bgSurface}; }
70
+ & .crisp-tabletoolbar-sort-field-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: start; }
71
+ & .crisp-tabletoolbar-sort-field-picker .crisp-tabletoolbar-menu { min-width: 200px; }
59
72
  & .crisp-tabletoolbar-menu--val { min-width: 134px; }
60
73
  & .crisp-tabletoolbar-datepop { width: 300px; }
61
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"]}; }
@@ -13,13 +13,23 @@ const tokens_1 = require("../../tokens");
13
13
  // the same reason (see its own doc).
14
14
  const spec = (0, core_1.defineSpec)({
15
15
  name: "text",
16
- // `display: inline` -- a span's own default, stated rather than left
17
- // implicit: an EMPTY base compiles to an empty string, and
18
- // `core/resolve.ts`'s `if (base) classes.push(prefixedName)` treats that
19
- // falsy string as "no base rule", which drops the `crisp-text` class
20
- // itself, silently, from every instance (caught by this file's own
21
- // tests going red on that exact class).
22
- base: { display: "inline" },
16
+ // Needs a real, always-present declaration so `resolveRecipe` emits the
17
+ // `crisp-text` class at all -- an EMPTY base compiles to an empty string,
18
+ // and `core/resolve.ts`'s `if (base) classes.push(prefixedName)` treats
19
+ // that falsy string as "no base rule", dropping the class itself,
20
+ // silently, from every instance (this file's own tests catch that).
21
+ // `display: "inline"` filled that role until a real bug: `asChild`
22
+ // (Primitive.span -> Radix Slot) merges this class onto WHATEVER element
23
+ // the caller passes, not a `<span>` -- a `<div>`/`<p>`/`<li>` child's own
24
+ // block default lost to `.crisp-text`'s class-specificity `display:
25
+ // inline`, silently collapsing its layout (internal/grc's own Settings.tsx
26
+ // ran two rows onto one line this way). `vertical-align: baseline` fills
27
+ // the same "always-present, never empty" role without that risk: it's a
28
+ // span's own already-inherited default (so nothing changes for the plain,
29
+ // non-asChild case `Primitive.span` renders), and it has NO effect at all
30
+ // on a block-level box, so an `asChild` div/p/li keeps its own display
31
+ // exactly as its own tag already gives it.
32
+ base: { verticalAlign: "baseline" },
23
33
  variants: {
24
34
  tone: {
25
35
  default: { color: tokens_1.contract.fgPrimary },
@@ -90,6 +90,9 @@
90
90
  & tbody tr[data-selected] td { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
91
91
  & tbody tr[data-selected]:hover td { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface))); }
92
92
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
93
+ /* crisp/#758: AI-enriched column marker, ported from RecordsTable's own already-measured value -- a full-bleed #9b69ff (bgPurple) wash at 4%, composited OVER whatever row-state background the cell already has (hover, selected), which is why it's an ::after overlay and not a
94
+ plain background. */
95
+ & td[data-enriched]::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: color-mix(in srgb, var(--crisp-bg-purple) 4%, transparent); }
93
96
  & .crisp-datatable-edit { width: 100%; height: 28px; margin: 0; padding: 0 var(--crisp-space-1-5); border: none; border-radius: var(--crisp-radius-sm); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); outline: none; vertical-align: middle; }
94
97
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
95
98
  /* An async onEdit's in-flight state (#385) -- the field/picker dims and stops accepting input rather than disappearing, so the just-typed or just-chosen value stays visible while its commit is pending. Same treatment Input already gives its own disabled state. */
@@ -91,6 +91,9 @@
91
91
  & tbody tr[data-selected] td { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
92
92
  & tbody tr[data-selected]:hover td { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface))); }
93
93
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
94
+ /* crisp/#758: AI-enriched column marker, ported from RecordsTable's own already-measured value -- a full-bleed #9b69ff (bgPurple) wash at 4%, composited OVER whatever row-state background the cell already has (hover, selected), which is why it's an ::after overlay and not a
95
+ plain background. */
96
+ & td[data-enriched]::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: color-mix(in srgb, var(--crisp-bg-purple) 4%, transparent); }
94
97
  & .crisp-datatable-edit { width: 100%; height: 28px; margin: 0; padding: 0 var(--crisp-space-1-5); border: none; border-radius: var(--crisp-radius-sm); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); outline: none; vertical-align: middle; }
95
98
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
96
99
  /* An async onEdit's in-flight state (#385) -- the field/picker dims and stops accepting input rather than disappearing, so the just-typed or just-chosen value stays visible while its commit is pending. Same treatment Input already gives its own disabled state. */
@@ -573,6 +573,19 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
573
573
  // arrow-key nav, Enter) is unaffected -- a plain click resets `selAnchor`
574
574
  // to null, collapsing back to single-cell mode.
575
575
  const [selAnchor, setSelAnchor] = React.useState(null);
576
+ // crisp/#749: Shift+ArrowUp/Down extends the ROW (checkbox) selection --
577
+ // confirmed live on Attio's own Companies grid, a DIFFERENT gesture from
578
+ // `selAnchor` above (that's Shift+CLICK extending a cell-value range;
579
+ // this issue's own text is explicit the two are independent, and arrow
580
+ // keys were never measured as a cell-range trigger, G-101). Same
581
+ // fixed-anchor/moving-cell shape as `selAnchor` -- a spreadsheet's own
582
+ // Shift+Arrow both moves the active cell AND grows/shrinks the selection
583
+ // relative to wherever the drag started, which recomputing the anchor→
584
+ // new-row span from scratch on every press gives for free (arrowing back
585
+ // toward the anchor shrinks the same way arrowing away grows it). Reset
586
+ // to null on any plain (non-shift) navigation or click, same as
587
+ // `selAnchor`'s own reset points.
588
+ const [rowSelectAnchor, setRowSelectAnchor] = React.useState(null);
576
589
  const gridRef = React.useRef(null);
577
590
  // The native-copy-event proxy (crisp/#645): Attio "has no Mousetrap/
578
591
  // JS-keydown binding for c/mod+c ... it copies purely via the native
@@ -1160,6 +1173,36 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
1160
1173
  setSelected(new Set(rows.map((rw) => rw.id)));
1161
1174
  return;
1162
1175
  }
1176
+ // crisp/#749: Shift+ArrowUp/Down extends the row-checkbox selection --
1177
+ // confirmed live, distinct from Shift+Click's cell-range gesture above
1178
+ // (that one is `selAnchor`; this is `rowSelectAnchor`, and the two
1179
+ // don't interact). Recomputes the whole anchor→destination span on
1180
+ // every press rather than accumulating, so arrowing back toward the
1181
+ // anchor shrinks the selection the same way arrowing away grows it --
1182
+ // the "shrink-from-the-anchor" behavior a spreadsheet's own Shift+Arrow
1183
+ // gives, which a plain add-on-each-press approach can't express.
1184
+ if (selectable &&
1185
+ e.shiftKey &&
1186
+ (e.key === "ArrowDown" || e.key === "ArrowUp")) {
1187
+ const r0 = rows.findIndex((row) => row.id === selCell.id);
1188
+ if (r0 < 0)
1189
+ return;
1190
+ const nr0 = e.key === "ArrowDown"
1191
+ ? Math.min(rows.length - 1, r0 + 1)
1192
+ : Math.max(0, r0 - 1);
1193
+ e.preventDefault();
1194
+ const anchorId = rowSelectAnchor ?? selCell.id;
1195
+ const anchorIdx = rows.findIndex((row) => row.id === anchorId);
1196
+ if (anchorIdx < 0)
1197
+ return;
1198
+ setRowSelectAnchor(anchorId);
1199
+ const lo = Math.min(anchorIdx, nr0);
1200
+ const hi = Math.max(anchorIdx, nr0);
1201
+ setSelected(new Set(rows.slice(lo, hi + 1).map((row) => row.id)));
1202
+ setSelCell({ id: rows[nr0].id, key: selCell.key });
1203
+ clipboardProxyRef.current?.select();
1204
+ return;
1205
+ }
1163
1206
  const r = rows.findIndex((row) => row.id === selCell.id);
1164
1207
  const c = columns.findIndex((col) => col.key === selCell.key);
1165
1208
  if (r < 0 || c < 0)
@@ -1198,6 +1241,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
1198
1241
  else if (e.key === "Escape") {
1199
1242
  setSelCell(null);
1200
1243
  setSelAnchor(null);
1244
+ setRowSelectAnchor(null);
1201
1245
  return;
1202
1246
  }
1203
1247
  else if (e.key === "Enter") {
@@ -1222,8 +1266,10 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
1222
1266
  // A plain arrow key moves the single-cell selection and drops any active
1223
1267
  // range -- Attio's own range trigger is Shift+Click only (G-101), arrow
1224
1268
  // keys were never measured as a range-extend gesture, so this doesn't
1225
- // invent one.
1269
+ // invent one. Also drops an in-progress Shift+Arrow row-selection drag
1270
+ // (crisp/#749) for the same reason -- a plain arrow ends that gesture.
1226
1271
  setSelAnchor(null);
1272
+ setRowSelectAnchor(null);
1227
1273
  setSelCell({ id: rows[nr].id, key: columns[nc].key });
1228
1274
  // keeps the clipboard proxy's selection non-collapsed after a move, so
1229
1275
  // a Cmd+C right after an arrow key still has something native `copy`
@@ -1806,7 +1852,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
1806
1852
  ? "crisp-datatable-editable"
1807
1853
  : undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
1808
1854
  ? ""
1809
- : undefined, "data-in-range": inRange ? "" : undefined, "data-cell-expanded": isExpanded ? "" : undefined, onClick: (e) => {
1855
+ : undefined, "data-in-range": inRange ? "" : undefined, "data-cell-expanded": isExpanded ? "" : undefined, "data-enriched": col.enriched ? "" : undefined, onClick: (e) => {
1810
1856
  // the keyboard model's anchor -- a single click
1811
1857
  // only selects. A genuine double-click (or Enter
1812
1858
  // on the selected cell) opens the editor (#411),
@@ -1821,6 +1867,11 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
1821
1867
  }
1822
1868
  else {
1823
1869
  setSelAnchor(null);
1870
+ // crisp/#749: a plain click also ends an
1871
+ // in-progress Shift+Arrow row-selection drag
1872
+ // -- Shift+Click itself doesn't touch it,
1873
+ // the two gestures are independent.
1874
+ setRowSelectAnchor(null);
1824
1875
  }
1825
1876
  setSelCell({ id: row.id, key: col.key });
1826
1877
  // Focuses the clipboard proxy, not `gridRef`
@@ -1861,6 +1912,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
1861
1912
  // selected (crisp/#598) -- collapses any
1862
1913
  // active range, same as a plain left-click.
1863
1914
  setSelAnchor(null);
1915
+ setRowSelectAnchor(null);
1864
1916
  setSelCell({ id: row.id, key: col.key });
1865
1917
  clipboardProxyRef.current?.focus();
1866
1918
  clipboardProxyRef.current?.select();
@@ -135,6 +135,12 @@ const config = defineRecipe({
135
135
  & tbody tr[data-selected] td { background: color-mix(in srgb, ${c.bgBrandSolid} 10%, ${c.bgSurface}); }
136
136
  & tbody tr[data-selected]:hover td { background: color-mix(in srgb, ${c.fgPrimary} 4%, color-mix(in srgb, ${c.bgBrandSolid} 10%, ${c.bgSurface})); }
137
137
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, ${c.fgPrimary} 4%, ${c.bgSurface}); cursor: text; }
138
+ /* crisp/#758: AI-enriched column marker, ported from RecordsTable's own
139
+ already-measured value -- a full-bleed #9b69ff (bgPurple) wash at 4%,
140
+ composited OVER whatever row-state background the cell already has
141
+ (hover, selected), which is why it's an ::after overlay and not a
142
+ plain background. */
143
+ & td[data-enriched]::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: color-mix(in srgb, ${c.bgPurple} 4%, transparent); }
138
144
  & .crisp-datatable-edit { width: 100%; height: 28px; margin: 0; padding: 0 ${c.space["1.5"]}; border: none; border-radius: ${c.radius.sm}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; outline: none; vertical-align: middle; }
139
145
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; }
140
146
  /* An async onEdit's in-flight state (#385) -- the field/picker dims and
@@ -377,6 +377,12 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
377
377
  // [team, company] is Team -> Company, etc. Reset to [] wherever
378
378
  // `filterQuery` already resets (opening the picker fresh), same lifetime.
379
379
  const [fieldPickerPath, setFieldPickerPath] = React.useState([]);
380
+ // #754: which sort row's own field picker is open (its index into
381
+ // `sorts`), or `null` for the normal sort-row list — the same
382
+ // "one sub-screen behind a flag" split View settings' own
383
+ // `addingAttribute` already uses, parameterized by index since more than
384
+ // one sort row exists.
385
+ const [sortFieldPickerIndex, setSortFieldPickerIndex] = React.useState(null);
380
386
  // filters the "isanyof" checkbox list, same lifetime as filterQuery above
381
387
  const [anyOfQuery, setAnyOfQuery] = React.useState("");
382
388
  const [dragAttr, setDragAttr] = React.useState(null);
@@ -390,7 +396,6 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
390
396
  // built here -- hiding an attribute is a trailing control instead, see
391
397
  // the hide button below).
392
398
  const [addingAttribute, setAddingAttribute] = React.useState(false);
393
- const [addAttrQuery, setAddAttrQuery] = React.useState("");
394
399
  // close any open menu on outside click (anything not inside a menu or its
395
400
  // trigger) or Escape
396
401
  React.useEffect(() => {
@@ -473,10 +478,27 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
473
478
  };
474
479
  // #590: shown attributes, in `shownKeys`' order -- a key with no matching
475
480
  // `viewColumns` entry (stale state after a column is removed upstream)
476
- // drops out rather than rendering a blank row.
477
- const shownAttrs = shownKeys
478
- .map((k) => viewColumns.find((c) => c.key === k))
479
- .filter((c) => c != null);
481
+ // drops out rather than rendering a blank row. #754: `shownKeys` can now
482
+ // carry a dot-joined nested path ("team.jobTitle") from the add-attribute
483
+ // screen's own nested picker, so each key resolves via the same
484
+ // `resolveColumnPath` the filter chip uses -- the LEAF column's icon
485
+ // drives the row's own icon, and the label is the full breadcrumb
486
+ // ("Team › Job title"), joined as plain text rather than the chip's own
487
+ // muted/chevron treatment, which was measured specifically for the
488
+ // filter chip and wasn't re-verified for this row shape.
489
+ const shownAttrs = shownKeys.flatMap((k) => {
490
+ const path = resolveColumnPath(viewColumns, k);
491
+ const leaf = path.at(-1);
492
+ if (!leaf)
493
+ return [];
494
+ return [
495
+ {
496
+ key: k,
497
+ icon: leaf.icon,
498
+ label: path.length > 1 ? path.map((c) => c.label).join(" › ") : leaf.label,
499
+ },
500
+ ];
501
+ });
480
502
  const hiddenAttrs = viewColumns.filter((c) => !shownKeys.includes(c.key));
481
503
  const moveAttr = (from, to) => {
482
504
  if (from === to)
@@ -490,7 +512,8 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
490
512
  const showAttr = (key) => {
491
513
  putShownKeys([...shownKeys, key]);
492
514
  setAddingAttribute(false);
493
- setAddAttrQuery("");
515
+ setFieldPickerPath([]);
516
+ setFilterQuery("");
494
517
  };
495
518
  // #594: the filter chip's kebab is a real role="menu" (crisp/#188's shared
496
519
  // model — arrows, Home/End, typeahead, Escape), not a hand-rolled div like
@@ -523,15 +546,20 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
523
546
  close: () => setOpenMenu(null),
524
547
  contentRef: importExportMenuRef,
525
548
  });
526
- // Shared between the "Filter" trigger's own add-condition picker and each
527
- // chip's field segment (#462 follow-up) — both search the same
528
- // `filterColumns` list and differ only in what a pick does. #592: also
529
- // handles drilling into a `nestedColumns` column — `fieldPickerPath`
530
- // tracks the current scope ([] is the top level), reset to [] wherever
531
- // the picker opens fresh (both trigger onClicks, above).
532
- const filterFieldPicker = (onSelect) => {
549
+ // #754: generalized from the Filter-only `filterFieldPicker` (#592) to
550
+ // also drive Sort's own field picker and View settings' add-attribute
551
+ // screen — all three offer the identical nested-drill-down construct on
552
+ // live Attio (confirmed: "Team" with a count badge appears, and drills
553
+ // into the same People-attribute screen, in all three pickers). Shared
554
+ // `fieldPickerPath`/`filterQuery` state across all three call sites is
555
+ // safe: `openMenu` already only ever names ONE open popover at a time,
556
+ // so only one of Filter/Sort/View-settings' own picker can be showing.
557
+ // `fieldPickerPath` tracks the current scope ([] is the top level),
558
+ // reset to [] wherever a picker opens fresh (every trigger's own
559
+ // onClick).
560
+ const nestedFieldPicker = (columns, onSelect, topLevelPlaceholder, topLevelLabel) => {
533
561
  const scope = fieldPickerPath.at(-1);
534
- const scopeColumns = scope ? (scope.nestedColumns ?? []) : filterColumns;
562
+ const scopeColumns = scope ? (scope.nestedColumns ?? []) : columns;
535
563
  const query = filterQuery.trim().toLowerCase();
536
564
  const direct = scopeColumns.filter((c) => c.label.toLowerCase().includes(query));
537
565
  // #592: one level of flattening from wherever the picker is currently
@@ -559,9 +587,9 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
559
587
  setFilterQuery("");
560
588
  }, children: _jsx(ChevronLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }) }), _jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: nestedFieldPlaceholder
561
589
  ? nestedFieldPlaceholder(scope.nestedObjectLabel ?? scope.label.toLowerCase())
562
- : `Search ${scope.nestedObjectLabel ?? scope.label.toLowerCase()} attributes…`, "aria-label": filterFieldLabel,
590
+ : `Search ${scope.nestedObjectLabel ?? scope.label.toLowerCase()} attributes…`, "aria-label": topLevelLabel,
563
591
  // biome-ignore lint/a11y/noAutofocus: focus the picker on open
564
- autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })] })) : (_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterFieldPlaceholder, "aria-label": filterFieldLabel,
592
+ autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })] })) : (_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: topLevelPlaceholder, "aria-label": topLevelLabel,
565
593
  // biome-ignore lint/a11y/noAutofocus: focus the picker on open
566
594
  autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })), scope && (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath(fieldPickerPath), children: [scope.icon ? (_jsx(scope.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), scope.label] })), direct.map((col) => col.nestedColumns?.length ? (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--expand", onClick: () => drillIn(col), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), col.label, _jsx(RtlChevronRight, { size: 15, className: "crisp-tabletoolbar-menu-item-chevron" })] }, col.key)) : (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath([...fieldPickerPath, col]), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), col.label] }, col.key))), nested.map(({ parent, col }) => (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath([...fieldPickerPath, parent, col]), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), _jsxs("span", { className: "crisp-tabletoolbar-menu-item-path", children: [_jsx("span", { className: "crisp-tabletoolbar-menu-item-path-parent", children: parent.label }), _jsx(RtlChevronRight, { size: 15, className: "crisp-tabletoolbar-menu-item-path-sep" }), _jsx("span", { children: col.label })] })] }, `${parent.key}.${col.key}`)))] }));
567
595
  };
@@ -577,7 +605,11 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
577
605
  const sortedLabel = sortTriggerLabel
578
606
  ? sortTriggerLabel(sorts, sortColumns)
579
607
  : sorts.length
580
- ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
608
+ ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${
609
+ // #754: sorts[0].key may be a dot-joined nested path
610
+ // ("team.jobTitle") — resolve it the same way the chip and sort
611
+ // row do, rather than a flat find that fails on a nested key.
612
+ resolveColumnPath(sortColumns, sorts[0].key).at(-1)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
581
613
  : sortLabel;
582
614
  return (_jsx("div", { className: tabletoolbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_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: [_jsx(ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && (_jsxs("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? (_jsx("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
583
615
  if (dragSort !== null)
@@ -586,11 +618,26 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
586
618
  if (dragSort !== null)
587
619
  moveSort(dragSort, i);
588
620
  setDragSort(null);
589
- }, children: [_jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderSortLabel, draggable: true, onDragStart: () => setDragSort(i), onDragEnd: () => setDragSort(null), children: _jsx(GripVertical, { size: 15, strokeWidth: 1.75 }) }), _jsx(Select, { className: "crisp-tabletoolbar-sort-field", searchable: true, value: s.key, onValueChange: (k) => setSortField(i, k), options: sortColumns.map((c) => ({
590
- value: c.key,
591
- label: c.icon ? (_jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [_jsx(c.icon, { size: 15, strokeWidth: 1.75 }), c.label] })) : (c.label),
592
- keywords: c.label,
593
- })), "aria-label": sortFieldLabel }), _jsx(Select, { className: "crisp-tabletoolbar-sort-dir", value: s.dir, onValueChange: (d) => setSortDir(i, d), options: [
621
+ }, children: [_jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderSortLabel, draggable: true, onDragStart: () => setDragSort(i), onDragEnd: () => setDragSort(null), children: _jsx(GripVertical, { size: 15, strokeWidth: 1.75 }) }), (() => {
622
+ // #754: the sort row's own field picker -- the
623
+ // same nested-drill-down construct Filter's field
624
+ // picker already has (#592), measured live:
625
+ // clicking a sort row's field (or "Add sort") opens
626
+ // an identical picker, "Team" (a relationship
627
+ // field) drills into the same People-attribute
628
+ // screen Filter's own "Team" does.
629
+ const sortColPath = resolveColumnPath(sortColumns, s.key);
630
+ const sortCol = sortColPath.at(-1);
631
+ const pickerOpen = sortFieldPickerIndex === i;
632
+ return (_jsxs("span", { className: "crisp-tabletoolbar-sort-field-picker", children: [_jsxs("button", { type: "button", className: "crisp-tabletoolbar-sort-field-trigger", "aria-haspopup": "dialog", "aria-expanded": pickerOpen, "aria-label": sortFieldLabel, onClick: () => {
633
+ setFieldPickerPath([]);
634
+ setFilterQuery("");
635
+ setSortFieldPickerIndex(pickerOpen ? null : i);
636
+ }, children: [sortCol?.icon ? (_jsx(sortCol.icon, { size: 15, strokeWidth: 1.75 })) : null, sortColPath.length > 1 ? (_jsx("span", { className: "crisp-tabletoolbar-fcond-path", children: sortColPath.map((c, pi) => (_jsxs(React.Fragment, { children: [pi > 0 && (_jsx(RtlChevronRight, { size: 14, className: "crisp-tabletoolbar-fcond-path-sep" })), _jsx("span", { className: "crisp-tabletoolbar-fcond-path-seg", children: c.label })] }, c.key))) })) : (_jsx("span", { className: "crisp-tabletoolbar-sort-field-label", children: sortCol?.label }))] }), pickerOpen && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": sortFieldLabel, children: nestedFieldPicker(sortColumns, (key) => {
637
+ setSortField(i, key);
638
+ setSortFieldPickerIndex(null);
639
+ }, searchAttributesPlaceholder, sortFieldLabel) }))] }));
640
+ })(), _jsx(Select, { className: "crisp-tabletoolbar-sort-dir", value: s.dir, onValueChange: (d) => setSortDir(i, d), options: [
594
641
  {
595
642
  value: "asc",
596
643
  label: (_jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [_jsx(ArrowUpNarrowWide, { size: 15, strokeWidth: 1.75 }), ascendingLabel] })),
@@ -608,7 +655,7 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
608
655
  ? filterTriggerLabel(activeFilters)
609
656
  : activeFilters
610
657
  ? `${filterLabel} · ${activeFilters}`
611
- : filterLabel] }), openMenu === "filter" && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker(addFilter) }))] }), filters.map((cond) => {
658
+ : filterLabel] }), openMenu === "filter" && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: nestedFieldPicker(filterColumns, addFilter, filterFieldPlaceholder, filterFieldLabel) }))] }), filters.map((cond) => {
612
659
  // #592: cond.key may be a dot-joined nested path
613
660
  // ("team.twitterFollowerCount") — resolve the full path so the
614
661
  // field segment can render its breadcrumb; `col` (the leaf) still
@@ -644,7 +691,7 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
644
691
  ? col?.type === "date"
645
692
  ? formatFilterDate(cond.value)
646
693
  : cond.value
647
- : filterValuePlaceholder })), _jsx("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--kebab", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: _jsx(MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && (_jsx("div", { ref: kebabMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--right crisp-tabletoolbar-menu--hdr", role: "menu", "aria-label": `${col?.label} ${filterOptionsLabel}`, onKeyDown: onKebabKeyDown, children: _jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [_jsx(Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), fieldOpen && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker((key) => changeFilterField(cond.id, key)) })), opOpen && (_jsx("div", { ref: opMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--op", role: "menu", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, onKeyDown: onOpKeyDown, children: opsFor(col).map((op) => (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => pickOp(cond.id, op), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? (_jsx(Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))) })), valOpen && showValueSeg && (_jsx("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--val", role: "dialog", "aria-label": `${col?.label} filter value`, children: cond.op === "isanyof" && col?.values ? (_jsxs(_Fragment, { children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
694
+ : filterValuePlaceholder })), _jsx("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--kebab", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: _jsx(MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && (_jsx("div", { ref: kebabMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--right crisp-tabletoolbar-menu--hdr", role: "menu", "aria-label": `${col?.label} ${filterOptionsLabel}`, onKeyDown: onKebabKeyDown, children: _jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [_jsx(Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), fieldOpen && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: nestedFieldPicker(filterColumns, (key) => changeFilterField(cond.id, key), filterFieldPlaceholder, filterFieldLabel) })), opOpen && (_jsx("div", { ref: opMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--op", role: "menu", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, onKeyDown: onOpKeyDown, children: opsFor(col).map((op) => (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => pickOp(cond.id, op), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? (_jsx(Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))) })), valOpen && showValueSeg && (_jsx("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--val", role: "dialog", "aria-label": `${col?.label} filter value`, children: cond.op === "isanyof" && col?.values ? (_jsxs(_Fragment, { children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
648
695
  // biome-ignore lint/a11y/noAutofocus: focus the search field
649
696
  autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
650
697
  .filter((v) => v
@@ -706,17 +753,12 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
706
753
  }
707
754
  else {
708
755
  setAddingAttribute(false);
709
- setAddAttrQuery("");
756
+ setFieldPickerPath([]);
757
+ setFilterQuery("");
710
758
  setOpenMenu("viewsettings");
711
759
  }
712
760
  }, "aria-haspopup": "dialog", "aria-expanded": openMenu === "viewsettings", "data-menu-trigger": true, children: [_jsx(SlidersHorizontal, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), viewSettingsLabel] }), openMenu === "viewsettings" &&
713
- (addingAttribute ? (_jsxs("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": addAttributeToViewLabel, children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: searchAttributesPlaceholder, "aria-label": searchAttributesPlaceholder,
714
- // biome-ignore lint/a11y/noAutofocus: focus the search box on open, same as Filter's field picker
715
- autoFocus: true, value: addAttrQuery, onChange: (e) => setAddAttrQuery(e.target.value) }), hiddenAttrs
716
- .filter((col) => col.label
717
- .toLowerCase()
718
- .includes(addAttrQuery.trim().toLowerCase()))
719
- .map((col) => (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => showAttr(col.key), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), col.label] }, col.key)))] })) : (_jsxs("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, children: [shownAttrs.map((col, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
761
+ (addingAttribute ? (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": addAttributeToViewLabel, children: nestedFieldPicker(hiddenAttrs, showAttr, searchAttributesPlaceholder, searchAttributesPlaceholder) })) : (_jsxs("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, children: [shownAttrs.map((col, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
720
762
  if (dragAttr !== null)
721
763
  e.preventDefault();
722
764
  }, onDrop: () => {
@@ -724,7 +766,11 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
724
766
  moveAttr(dragAttr, i);
725
767
  setDragAttr(null);
726
768
  }, children: [_jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderAttributeLabel, draggable: true, onDragStart: () => setDragAttr(i), onDragEnd: () => setDragAttr(null), children: _jsx(GripVertical, { size: 15, strokeWidth: 1.75 }) }), _jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel crisp-tabletoolbar-sort-field", children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : null, col.label] }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => hideAttr(col.key), "aria-label": hideAttributeLabel?.(col.label) ??
727
- `${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => setAddingAttribute(true), children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] })), (onImportCsv ||
769
+ `${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => {
770
+ setAddingAttribute(true);
771
+ setFieldPickerPath([]);
772
+ setFilterQuery("");
773
+ }, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] })), (onImportCsv ||
728
774
  onExportCsv ||
729
775
  onExportExcel ||
730
776
  importExportHelpHref) && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_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: [_jsx(Upload, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), importExportLabel, _jsx(ChevronDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }), openMenu === "importexport" && (_jsxs("div", { ref: importExportMenuRef, className: "crisp-tabletoolbar-menu", role: "menu", "aria-label": importExportLabel, onKeyDown: onImportExportKeyDown, children: [onImportCsv && (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
@@ -53,6 +53,19 @@ const filterChipChrome = `
53
53
  & .crisp-tabletoolbar-fcond-seg--val { font-weight: ${c.weight.medium}; max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
54
54
  & .crisp-tabletoolbar-fcond-seg--kebab { background: ${c.bgSurface}; justify-content: center; width: 28px; padding: 0; border-radius: 0 ${c.radius.md} ${c.radius.md} 0; color: ${c.fgMuted}; }
55
55
  & .crisp-tabletoolbar-fcond-seg--kebab:hover { color: ${c.fgPrimary}; }
56
+ /* #754: the sort row's own field trigger, replacing a plain Select
57
+ with the same nested-drill-down popover Filter's field picker
58
+ already has — reuses Select's own already-measured trigger chrome
59
+ verbatim (its class is scoped under Select's own root class, so it
60
+ can't be reused as a bare class name outside an actual Select tree;
61
+ these values are copied, not re-measured). */
62
+ & .crisp-tabletoolbar-sort-field-picker { position: relative; display: inline-flex; flex: 1; min-width: 0; }
63
+ & .crisp-tabletoolbar-sort-field-trigger { box-sizing: border-box; display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: ${c.size.menuRow}; width: 100%; min-width: 0; margin: 0; padding: 0 ${c.space["1.5"]} 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 7%, transparent); font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; cursor: pointer; transition: background-color .12s ease, box-shadow .12s ease; }
64
+ & .crisp-tabletoolbar-sort-field-trigger:hover { background: ${c.bgRaised}; }
65
+ & .crisp-tabletoolbar-sort-field-trigger[aria-expanded="true"],
66
+ & .crisp-tabletoolbar-sort-field-trigger:focus-visible { outline: none; box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; background: ${c.bgSurface}; }
67
+ & .crisp-tabletoolbar-sort-field-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: start; }
68
+ & .crisp-tabletoolbar-sort-field-picker .crisp-tabletoolbar-menu { min-width: 200px; }
56
69
  & .crisp-tabletoolbar-menu--val { min-width: 134px; }
57
70
  & .crisp-tabletoolbar-datepop { width: 300px; }
58
71
  & .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: ${c.space["1"]}; margin-top: ${c.space["2"]}; padding: 0 ${c.space["1"]}; }
@@ -10,13 +10,23 @@ import { contract as c } from "../../tokens/index.js";
10
10
  // the same reason (see its own doc).
11
11
  const spec = defineSpec({
12
12
  name: "text",
13
- // `display: inline` -- a span's own default, stated rather than left
14
- // implicit: an EMPTY base compiles to an empty string, and
15
- // `core/resolve.ts`'s `if (base) classes.push(prefixedName)` treats that
16
- // falsy string as "no base rule", which drops the `crisp-text` class
17
- // itself, silently, from every instance (caught by this file's own
18
- // tests going red on that exact class).
19
- base: { display: "inline" },
13
+ // Needs a real, always-present declaration so `resolveRecipe` emits the
14
+ // `crisp-text` class at all -- an EMPTY base compiles to an empty string,
15
+ // and `core/resolve.ts`'s `if (base) classes.push(prefixedName)` treats
16
+ // that falsy string as "no base rule", dropping the class itself,
17
+ // silently, from every instance (this file's own tests catch that).
18
+ // `display: "inline"` filled that role until a real bug: `asChild`
19
+ // (Primitive.span -> Radix Slot) merges this class onto WHATEVER element
20
+ // the caller passes, not a `<span>` -- a `<div>`/`<p>`/`<li>` child's own
21
+ // block default lost to `.crisp-text`'s class-specificity `display:
22
+ // inline`, silently collapsing its layout (internal/grc's own Settings.tsx
23
+ // ran two rows onto one line this way). `vertical-align: baseline` fills
24
+ // the same "always-present, never empty" role without that risk: it's a
25
+ // span's own already-inherited default (so nothing changes for the plain,
26
+ // non-asChild case `Primitive.span` renders), and it has NO effect at all
27
+ // on a block-level box, so an `asChild` div/p/li keeps its own display
28
+ // exactly as its own tag already gives it.
29
+ base: { verticalAlign: "baseline" },
20
30
  variants: {
21
31
  tone: {
22
32
  default: { color: c.fgPrimary },
package/dist/styles.css CHANGED
@@ -689,6 +689,9 @@
689
689
  & tbody tr[data-selected] td { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
690
690
  & tbody tr[data-selected]:hover td { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface))); }
691
691
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
692
+ /* crisp/#758: AI-enriched column marker, ported from RecordsTable's own already-measured value -- a full-bleed #9b69ff (bgPurple) wash at 4%, composited OVER whatever row-state background the cell already has (hover, selected), which is why it's an ::after overlay and not a
693
+ plain background. */
694
+ & td[data-enriched]::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: color-mix(in srgb, var(--crisp-bg-purple) 4%, transparent); }
692
695
  & .crisp-datatable-edit { width: 100%; height: 28px; margin: 0; padding: 0 var(--crisp-space-1-5); border: none; border-radius: var(--crisp-radius-sm); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); outline: none; vertical-align: middle; }
693
696
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
694
697
  /* An async onEdit's in-flight state (#385) -- the field/picker dims and stops accepting input rather than disappearing, so the just-typed or just-chosen value stays visible while its commit is pending. Same treatment Input already gives its own disabled state. */
@@ -1922,11 +1925,23 @@
1922
1925
  & .crisp-tabletoolbar-fcond-seg--val { font-weight: var(--crisp-weight-medium); max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
1923
1926
  & .crisp-tabletoolbar-fcond-seg--kebab { background: var(--crisp-bg-surface); justify-content: center; width: 28px; padding: 0; border-radius: 0 var(--crisp-radius-md) var(--crisp-radius-md) 0; color: var(--crisp-fg-muted); }
1924
1927
  & .crisp-tabletoolbar-fcond-seg--kebab:hover { color: var(--crisp-fg-primary); }
1928
+ /* #754: the sort row's own field trigger, replacing a plain Select
1929
+ with the same nested-drill-down popover Filter's field picker already has — reuses Select's own already-measured trigger chrome
1930
+ verbatim (its class is scoped under Select's own root class, so it can't be reused as a bare class name outside an actual Select tree;
1931
+ these values are copied, not re-measured). */
1932
+ & .crisp-tabletoolbar-sort-field-picker { position: relative; display: inline-flex; flex: 1; min-width: 0; }
1933
+ & .crisp-tabletoolbar-sort-field-trigger { box-sizing: border-box; display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: var(--crisp-size-menuRow); width: 100%; min-width: 0; margin: 0; padding: 0 var(--crisp-space-1-5) 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); cursor: pointer; transition: background-color .12s ease, box-shadow .12s ease; }
1934
+ & .crisp-tabletoolbar-sort-field-trigger:hover { background: var(--crisp-bg-raised); }
1935
+ & .crisp-tabletoolbar-sort-field-trigger[aria-expanded="true"],
1936
+ & .crisp-tabletoolbar-sort-field-trigger:focus-visible { outline: none; box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); background: var(--crisp-bg-surface); }
1937
+ & .crisp-tabletoolbar-sort-field-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: start; }
1938
+ & .crisp-tabletoolbar-sort-field-picker .crisp-tabletoolbar-menu { min-width: 200px; }
1925
1939
  & .crisp-tabletoolbar-menu--val { min-width: 134px; }
1926
1940
  & .crisp-tabletoolbar-datepop { width: 300px; }
1927
1941
  & .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: var(--crisp-space-1); margin-top: var(--crisp-space-2); padding: 0 var(--crisp-space-1); }
1928
1942
  & .crisp-tabletoolbar-datepop-preset { height: var(--crisp-size-chip); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-chip); background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-sm); cursor: pointer; }
1929
1943
  & .crisp-tabletoolbar-datepop-preset:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); }
1944
+
1930
1945
  & .crisp-tabletoolbar-tool--raised { border: none; background: var(--crisp-bg-surface-muted); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); font-family: inherit; cursor: pointer; }
1931
1946
  [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
1932
1947
  }
@@ -1971,7 +1986,7 @@
1971
1986
  }
1972
1987
 
1973
1988
  .crisp-text {
1974
- display: inline;
1989
+ vertical-align: baseline;
1975
1990
  }
1976
1991
  .crisp-text--tone_default {
1977
1992
  color: var(--crisp-fg-primary);
@@ -690,6 +690,9 @@
690
690
  & tbody tr[data-selected] td { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
691
691
  & tbody tr[data-selected]:hover td { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface))); }
692
692
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
693
+ /* crisp/#758: AI-enriched column marker, ported from RecordsTable's own already-measured value -- a full-bleed #9b69ff (bgPurple) wash at 4%, composited OVER whatever row-state background the cell already has (hover, selected), which is why it's an ::after overlay and not a
694
+ plain background. */
695
+ & td[data-enriched]::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: color-mix(in srgb, var(--crisp-bg-purple) 4%, transparent); }
693
696
  & .crisp-datatable-edit { width: 100%; height: 28px; margin: 0; padding: 0 var(--crisp-space-1-5); border: none; border-radius: var(--crisp-radius-sm); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); outline: none; vertical-align: middle; }
694
697
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
695
698
  /* An async onEdit's in-flight state (#385) -- the field/picker dims and stops accepting input rather than disappearing, so the just-typed or just-chosen value stays visible while its commit is pending. Same treatment Input already gives its own disabled state. */
@@ -1923,11 +1926,23 @@
1923
1926
  & .crisp-tabletoolbar-fcond-seg--val { font-weight: var(--crisp-weight-medium); max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
1924
1927
  & .crisp-tabletoolbar-fcond-seg--kebab { background: var(--crisp-bg-surface); justify-content: center; width: 28px; padding: 0; border-radius: 0 var(--crisp-radius-md) var(--crisp-radius-md) 0; color: var(--crisp-fg-muted); }
1925
1928
  & .crisp-tabletoolbar-fcond-seg--kebab:hover { color: var(--crisp-fg-primary); }
1929
+ /* #754: the sort row's own field trigger, replacing a plain Select
1930
+ with the same nested-drill-down popover Filter's field picker already has — reuses Select's own already-measured trigger chrome
1931
+ verbatim (its class is scoped under Select's own root class, so it can't be reused as a bare class name outside an actual Select tree;
1932
+ these values are copied, not re-measured). */
1933
+ & .crisp-tabletoolbar-sort-field-picker { position: relative; display: inline-flex; flex: 1; min-width: 0; }
1934
+ & .crisp-tabletoolbar-sort-field-trigger { box-sizing: border-box; display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: var(--crisp-size-menuRow); width: 100%; min-width: 0; margin: 0; padding: 0 var(--crisp-space-1-5) 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); cursor: pointer; transition: background-color .12s ease, box-shadow .12s ease; }
1935
+ & .crisp-tabletoolbar-sort-field-trigger:hover { background: var(--crisp-bg-raised); }
1936
+ & .crisp-tabletoolbar-sort-field-trigger[aria-expanded="true"],
1937
+ & .crisp-tabletoolbar-sort-field-trigger:focus-visible { outline: none; box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); background: var(--crisp-bg-surface); }
1938
+ & .crisp-tabletoolbar-sort-field-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: start; }
1939
+ & .crisp-tabletoolbar-sort-field-picker .crisp-tabletoolbar-menu { min-width: 200px; }
1926
1940
  & .crisp-tabletoolbar-menu--val { min-width: 134px; }
1927
1941
  & .crisp-tabletoolbar-datepop { width: 300px; }
1928
1942
  & .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: var(--crisp-space-1); margin-top: var(--crisp-space-2); padding: 0 var(--crisp-space-1); }
1929
1943
  & .crisp-tabletoolbar-datepop-preset { height: var(--crisp-size-chip); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-chip); background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-sm); cursor: pointer; }
1930
1944
  & .crisp-tabletoolbar-datepop-preset:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); }
1945
+
1931
1946
  & .crisp-tabletoolbar-tool--raised { border: none; background: var(--crisp-bg-surface-muted); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); font-family: inherit; cursor: pointer; }
1932
1947
  [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
1933
1948
  }
@@ -1972,7 +1987,7 @@
1972
1987
  }
1973
1988
 
1974
1989
  .crisp-text {
1975
- display: inline;
1990
+ vertical-align: baseline;
1976
1991
  }
1977
1992
  .crisp-text--tone_default {
1978
1993
  color: var(--crisp-fg-primary);
@@ -105,11 +105,23 @@
105
105
  & .crisp-tabletoolbar-fcond-seg--val { font-weight: var(--crisp-weight-medium); max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
106
106
  & .crisp-tabletoolbar-fcond-seg--kebab { background: var(--crisp-bg-surface); justify-content: center; width: 28px; padding: 0; border-radius: 0 var(--crisp-radius-md) var(--crisp-radius-md) 0; color: var(--crisp-fg-muted); }
107
107
  & .crisp-tabletoolbar-fcond-seg--kebab:hover { color: var(--crisp-fg-primary); }
108
+ /* #754: the sort row's own field trigger, replacing a plain Select
109
+ with the same nested-drill-down popover Filter's field picker already has — reuses Select's own already-measured trigger chrome
110
+ verbatim (its class is scoped under Select's own root class, so it can't be reused as a bare class name outside an actual Select tree;
111
+ these values are copied, not re-measured). */
112
+ & .crisp-tabletoolbar-sort-field-picker { position: relative; display: inline-flex; flex: 1; min-width: 0; }
113
+ & .crisp-tabletoolbar-sort-field-trigger { box-sizing: border-box; display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: var(--crisp-size-menuRow); width: 100%; min-width: 0; margin: 0; padding: 0 var(--crisp-space-1-5) 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); cursor: pointer; transition: background-color .12s ease, box-shadow .12s ease; }
114
+ & .crisp-tabletoolbar-sort-field-trigger:hover { background: var(--crisp-bg-raised); }
115
+ & .crisp-tabletoolbar-sort-field-trigger[aria-expanded="true"],
116
+ & .crisp-tabletoolbar-sort-field-trigger:focus-visible { outline: none; box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); background: var(--crisp-bg-surface); }
117
+ & .crisp-tabletoolbar-sort-field-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: start; }
118
+ & .crisp-tabletoolbar-sort-field-picker .crisp-tabletoolbar-menu { min-width: 200px; }
108
119
  & .crisp-tabletoolbar-menu--val { min-width: 134px; }
109
120
  & .crisp-tabletoolbar-datepop { width: 300px; }
110
121
  & .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: var(--crisp-space-1); margin-top: var(--crisp-space-2); padding: 0 var(--crisp-space-1); }
111
122
  & .crisp-tabletoolbar-datepop-preset { height: var(--crisp-size-chip); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-chip); background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-sm); cursor: pointer; }
112
123
  & .crisp-tabletoolbar-datepop-preset:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); }
124
+
113
125
  & .crisp-tabletoolbar-tool--raised { border: none; background: var(--crisp-bg-surface-muted); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); font-family: inherit; cursor: pointer; }
114
126
  [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
115
127
  }
@@ -106,11 +106,23 @@
106
106
  & .crisp-tabletoolbar-fcond-seg--val { font-weight: var(--crisp-weight-medium); max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
107
107
  & .crisp-tabletoolbar-fcond-seg--kebab { background: var(--crisp-bg-surface); justify-content: center; width: 28px; padding: 0; border-radius: 0 var(--crisp-radius-md) var(--crisp-radius-md) 0; color: var(--crisp-fg-muted); }
108
108
  & .crisp-tabletoolbar-fcond-seg--kebab:hover { color: var(--crisp-fg-primary); }
109
+ /* #754: the sort row's own field trigger, replacing a plain Select
110
+ with the same nested-drill-down popover Filter's field picker already has — reuses Select's own already-measured trigger chrome
111
+ verbatim (its class is scoped under Select's own root class, so it can't be reused as a bare class name outside an actual Select tree;
112
+ these values are copied, not re-measured). */
113
+ & .crisp-tabletoolbar-sort-field-picker { position: relative; display: inline-flex; flex: 1; min-width: 0; }
114
+ & .crisp-tabletoolbar-sort-field-trigger { box-sizing: border-box; display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: var(--crisp-size-menuRow); width: 100%; min-width: 0; margin: 0; padding: 0 var(--crisp-space-1-5) 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); cursor: pointer; transition: background-color .12s ease, box-shadow .12s ease; }
115
+ & .crisp-tabletoolbar-sort-field-trigger:hover { background: var(--crisp-bg-raised); }
116
+ & .crisp-tabletoolbar-sort-field-trigger[aria-expanded="true"],
117
+ & .crisp-tabletoolbar-sort-field-trigger:focus-visible { outline: none; box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); background: var(--crisp-bg-surface); }
118
+ & .crisp-tabletoolbar-sort-field-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: start; }
119
+ & .crisp-tabletoolbar-sort-field-picker .crisp-tabletoolbar-menu { min-width: 200px; }
109
120
  & .crisp-tabletoolbar-menu--val { min-width: 134px; }
110
121
  & .crisp-tabletoolbar-datepop { width: 300px; }
111
122
  & .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: var(--crisp-space-1); margin-top: var(--crisp-space-2); padding: 0 var(--crisp-space-1); }
112
123
  & .crisp-tabletoolbar-datepop-preset { height: var(--crisp-size-chip); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-chip); background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-sm); cursor: pointer; }
113
124
  & .crisp-tabletoolbar-datepop-preset:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); }
125
+
114
126
  & .crisp-tabletoolbar-tool--raised { border: none; background: var(--crisp-bg-surface-muted); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); font-family: inherit; cursor: pointer; }
115
127
  [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
116
128
  }
package/dist/text.css CHANGED
@@ -1,5 +1,5 @@
1
1
  .crisp-text {
2
- display: inline;
2
+ vertical-align: baseline;
3
3
  }
4
4
  .crisp-text--tone_default {
5
5
  color: var(--crisp-fg-primary);
@@ -1,6 +1,6 @@
1
1
  @layer crisp-components {
2
2
  .crisp-text {
3
- display: inline;
3
+ vertical-align: baseline;
4
4
  }
5
5
  .crisp-text--tone_default {
6
6
  color: var(--crisp-fg-primary);
@@ -178,6 +178,14 @@ export type DataTableColumn<T> = {
178
178
  * native-copy path writes nothing to the clipboard, and its Clear-value
179
179
  * item is disabled (there is nothing editable to clear). */
180
180
  readOnly?: boolean;
181
+ /** Marks the column's values as AI-enriched (crisp/#758) — every cell
182
+ * gets a full-bleed purple wash, composited OVER whatever row-state
183
+ * background the cell already has (hover, selected), which is why it's
184
+ * an overlay rather than a plain `background`. Ported from
185
+ * `RecordsTable`'s own already-measured value (`#9b69ff` at 4%, same as
186
+ * `bgPurple`) — no new measurement needed, `RecordsTable`'s own
187
+ * `Column.enriched` predates this. */
188
+ enriched?: boolean;
181
189
  /** `type: "entity"` only (#501) — makes the cell's name its own click
182
190
  * target, separate from the row's own `onRowClick`: the name renders as a
183
191
  * `stopPropagation`-guarded button instead of a plain span, the same
@@ -1,7 +1,7 @@
1
1
  declare const spec: {
2
2
  readonly name: "text";
3
3
  readonly base: {
4
- readonly display: "inline";
4
+ readonly verticalAlign: "baseline";
5
5
  };
6
6
  readonly variants: {
7
7
  tone: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.54.9",
3
+ "version": "0.54.10",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",