@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.
- package/dist/cjs/components/datatable/datatable.js +54 -2
- package/dist/cjs/components/datatable/datatable.recipe.js +6 -0
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +79 -33
- package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +13 -0
- package/dist/cjs/components/text/text.recipe.js +17 -7
- package/dist/datatable.css +3 -0
- package/dist/datatable.layered.css +3 -0
- package/dist/esm/components/datatable/datatable.js +54 -2
- package/dist/esm/components/datatable/datatable.recipe.js +6 -0
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +79 -33
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +13 -0
- package/dist/esm/components/text/text.recipe.js +17 -7
- package/dist/styles.css +16 -1
- package/dist/styles.layered.css +16 -1
- package/dist/tabletoolbar.css +12 -0
- package/dist/tabletoolbar.layered.css +12 -0
- package/dist/text.css +1 -1
- package/dist/text.layered.css +1 -1
- package/dist/types/components/datatable/datatable.d.ts +8 -0
- package/dist/types/components/text/text.recipe.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
|
|
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 ?? []) :
|
|
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":
|
|
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:
|
|
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"} ${
|
|
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 }) }), (
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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: () =>
|
|
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
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// `core/resolve.ts`'s `if (base) classes.push(prefixedName)` treats
|
|
19
|
-
// falsy string as "no base rule",
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
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 },
|
package/dist/datatable.css
CHANGED
|
@@ -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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
|
|
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 ?? []) :
|
|
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":
|
|
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:
|
|
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"} ${
|
|
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 }) }),
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 ? (
|
|
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: () =>
|
|
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
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// `core/resolve.ts`'s `if (base) classes.push(prefixedName)` treats
|
|
16
|
-
// falsy string as "no base rule",
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
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
|
-
|
|
1989
|
+
vertical-align: baseline;
|
|
1975
1990
|
}
|
|
1976
1991
|
.crisp-text--tone_default {
|
|
1977
1992
|
color: var(--crisp-fg-primary);
|
package/dist/styles.layered.css
CHANGED
|
@@ -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
|
-
|
|
1990
|
+
vertical-align: baseline;
|
|
1976
1991
|
}
|
|
1977
1992
|
.crisp-text--tone_default {
|
|
1978
1993
|
color: var(--crisp-fg-primary);
|
package/dist/tabletoolbar.css
CHANGED
|
@@ -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
package/dist/text.layered.css
CHANGED
|
@@ -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
|