@crisp-ui-kit/crisp 0.54.9 → 0.54.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/datatable/datatable.js +175 -12
- package/dist/cjs/components/datatable/datatable.recipe.js +30 -0
- package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +159 -63
- package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +41 -9
- package/dist/cjs/components/text/text.recipe.js +17 -7
- package/dist/cjs/product/recordpage/column.js +19 -0
- package/dist/cjs/product/recordpage/recordpage.js +5 -4
- package/dist/cjs/product/recordpage/test-fixtures.js +55 -0
- package/dist/cjs/product/recordstable/index.js +1 -1
- package/dist/cjs/product/recordstable/recordstable.js +5 -0
- package/dist/cjs/product/savedviews/savedviews.recipe.js +13 -11
- package/dist/cjs/product/taskstable/taskstable.js +5 -0
- package/dist/datatable.css +17 -0
- package/dist/datatable.layered.css +17 -0
- package/dist/esm/components/datatable/datatable.js +175 -12
- package/dist/esm/components/datatable/datatable.recipe.js +30 -0
- package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +159 -63
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +41 -9
- package/dist/esm/components/text/text.recipe.js +17 -7
- package/dist/esm/product/recordpage/column.js +16 -0
- package/dist/esm/product/recordpage/recordpage.js +2 -1
- package/dist/esm/product/recordpage/test-fixtures.js +52 -0
- package/dist/esm/product/recordstable/index.js +1 -1
- package/dist/esm/product/recordstable/recordstable.js +5 -0
- package/dist/esm/product/savedviews/savedviews.recipe.js +13 -11
- package/dist/esm/product/taskstable/taskstable.js +5 -0
- package/dist/recordstable.css +19 -2
- package/dist/recordstable.layered.css +19 -2
- package/dist/savedviews.css +6 -18
- package/dist/savedviews.layered.css +6 -18
- package/dist/styles.css +78 -24
- package/dist/styles.layered.css +78 -24
- package/dist/tabletoolbar.css +35 -3
- package/dist/tabletoolbar.layered.css +35 -3
- package/dist/text.css +1 -1
- package/dist/text.layered.css +1 -1
- package/dist/types/components/datatable/datatable.d.ts +38 -3
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +26 -1
- package/dist/types/components/text/text.recipe.d.ts +1 -1
- package/dist/types/product/recordpage/column.d.ts +112 -0
- package/dist/types/product/recordpage/recordpage.d.ts +1 -1
- package/dist/types/product/recordpage/test-fixtures.d.ts +3 -0
- package/dist/types/product/recordpanel/recordpanel.d.ts +1 -1
- package/dist/types/product/recordstable/index.d.ts +1 -1
- package/dist/types/product/recordstable/recordstable.d.ts +6 -1
- package/dist/types/product/taskstable/taskstable.d.ts +5 -0
- package/package.json +1 -1
- /package/dist/cjs/product/{recordstable → recordpage}/editable-field.js +0 -0
- /package/dist/esm/product/{recordstable → recordpage}/editable-field.js +0 -0
- /package/dist/types/product/{recordstable → recordpage}/editable-field.d.ts +0 -0
|
@@ -53,6 +53,7 @@ const checkbox_1 = require("../checkbox");
|
|
|
53
53
|
const columncalc_1 = require("../columncalc");
|
|
54
54
|
const contextmenu_1 = require("../contextmenu");
|
|
55
55
|
const menu_1 = require("../menu");
|
|
56
|
+
const radiogroup_1 = require("../radiogroup");
|
|
56
57
|
const select_1 = require("../select");
|
|
57
58
|
const spinner_1 = require("../spinner");
|
|
58
59
|
const switch_1 = require("../switch");
|
|
@@ -292,6 +293,73 @@ function formatCell(type, v, symbol, locale = "en-US") {
|
|
|
292
293
|
}
|
|
293
294
|
return asText(v);
|
|
294
295
|
}
|
|
296
|
+
// crisp/#760 -- the exact 5-point star Attio's own Rating attribute draws
|
|
297
|
+
// (path traced from the live widget's own SVG, `app.attio.com`, both the
|
|
298
|
+
// attribute-creation default-value picker and the grid cell render the
|
|
299
|
+
// identical path). Kept as one shared `d` string rather than a lucide icon
|
|
300
|
+
// so the star's silhouette matches Attio verbatim rather than an
|
|
301
|
+
// approximation, and so `RatingStars` below can flip its own `fill`
|
|
302
|
+
// per-star instead of drawing a whole new icon per state.
|
|
303
|
+
const RATING_STAR_D = "M7.32203 2.81085C7.65793 1.97438 8.84207 1.97438 9.17797 2.81085L10.0851 5.06967C10.2281 5.42587 10.5624 5.66876 10.9454 5.69473L13.3739 5.8594C14.2733 5.92038 14.6392 7.04656 13.9475 7.62451L12.0795 9.18521C11.7849 9.43132 11.6572 9.82433 11.7509 10.1966L12.3447 12.5572C12.5647 13.4313 11.6067 14.1273 10.8433 13.6481L8.78171 12.3538C8.45661 12.1497 8.04339 12.1497 7.71829 12.3538L5.65675 13.6481C4.89333 14.1273 3.93534 13.4313 4.15526 12.5572L4.74911 10.1966C4.84276 9.82433 4.71507 9.43132 4.4205 9.18521L2.55254 7.62451C1.86081 7.04656 2.22673 5.92038 3.12606 5.8594L5.55463 5.69473C5.9376 5.66876 6.27191 5.42587 6.41495 5.06967L7.32203 2.81085Z";
|
|
304
|
+
/** A star row for `type: "rating"` / `editor: "rating"` (crisp/#760).
|
|
305
|
+
* Read-only (a plain `<span>` row, no interaction) unless `onChange` is
|
|
306
|
+
* given, which renders Attio's own measured accessible shape: a
|
|
307
|
+
* `role="radiogroup"` of `role="radio"` buttons, one per star. Clicking a
|
|
308
|
+
* star sets the rating to its 1-based index; clicking the star that
|
|
309
|
+
* already equals the current value clears it to 0 -- confirmed live
|
|
310
|
+
* (rating a record 5, then clicking the 5th star again cleared it
|
|
311
|
+
* entirely rather than re-committing 5). */
|
|
312
|
+
function RatingStars({ value, max = 5, onChange, "aria-label": ariaLabel, }) {
|
|
313
|
+
const [hoverIdx, setHoverIdx] = React.useState(null);
|
|
314
|
+
// The house radiogroup keyboard model (crisp's own spec check requires it
|
|
315
|
+
// on every role="radiogroup", regardless of Attio parity): arrows move
|
|
316
|
+
// focus AND select, Home/End jump to the ends. Landing on a star always
|
|
317
|
+
// SETS the rating to that star's position -- arrow nav has no "clear on
|
|
318
|
+
// re-land" toggle, that's a click-only gesture (see onClick below).
|
|
319
|
+
const { onKeyDown, groupRef } = (0, radiogroup_1.useRadioGroupKeyboard)({
|
|
320
|
+
select: (v) => onChange?.(Number(v)),
|
|
321
|
+
});
|
|
322
|
+
const stars = Array.from({ length: max }, (_, i) => i);
|
|
323
|
+
const content = stars.map((i) => {
|
|
324
|
+
// Hovering star i previews filling every star up to and including it,
|
|
325
|
+
// whether or not it's already part of the committed value -- measured:
|
|
326
|
+
// hovering the 5th star while 4 were already filled turned all 5 the
|
|
327
|
+
// hover colour, not just the would-be-new one.
|
|
328
|
+
const hovering = onChange != null && hoverIdx != null && i <= hoverIdx;
|
|
329
|
+
const filled = hovering ? true : i < value;
|
|
330
|
+
// A native <input type="radio"> can't be unchecked by re-clicking the
|
|
331
|
+
// already-selected one, which is exactly the measured behaviour
|
|
332
|
+
// (clicking the star that already equals the rating clears it to 0) --
|
|
333
|
+
// Attio's own live widget is a <button role="radio"> for the same
|
|
334
|
+
// reason.
|
|
335
|
+
return onChange ? ((0, jsx_runtime_1.jsx)("button", { type: "button", role: "radio", "aria-checked": i < value, "aria-label": `${i + 1} star${i === 0 ? "" : "s"}`, "data-value": String(i + 1), className: "crisp-datatable-rating-star-btn", onMouseEnter: () => setHoverIdx(i), onMouseLeave: () => setHoverIdx(null), onClick: (e) => {
|
|
336
|
+
e.stopPropagation();
|
|
337
|
+
onChange(i + 1 === value ? 0 : i + 1);
|
|
338
|
+
}, children: (0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", className: "crisp-datatable-rating-star", "data-filled": filled ? "" : undefined, "data-hovering": hovering ? "" : undefined, children: (0, jsx_runtime_1.jsx)("path", { d: RATING_STAR_D }) }) }, i)) : ((0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", className: "crisp-datatable-rating-star", "data-filled": filled ? "" : undefined, children: (0, jsx_runtime_1.jsx)("path", { d: RATING_STAR_D }) }, i));
|
|
339
|
+
});
|
|
340
|
+
return onChange ? ((0, jsx_runtime_1.jsx)("span", { role: "radiogroup", "aria-label": ariaLabel, className: "crisp-datatable-rating", ref: groupRef, onKeyDown: onKeyDown, children: content })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-rating", children: content }));
|
|
341
|
+
}
|
|
342
|
+
/** The preview/comments action icons a column's `onOpenPreview`/
|
|
343
|
+
* `onOpenComments`/`commentCount` produce — the exact markup `type: "entity"`
|
|
344
|
+
* already renders inline, extracted so a column that ISN'T `"entity"` (a
|
|
345
|
+
* custom `render`, or any other built-in type) can compose the same actions
|
|
346
|
+
* alongside its own content instead of the props silently doing nothing
|
|
347
|
+
* (crisp/#601 supersession follow-up: these three props used to only have
|
|
348
|
+
* an effect inside `renderTyped`'s `"entity"` case). `null` when neither
|
|
349
|
+
* prop is set, so a caller can render nothing rather than an empty
|
|
350
|
+
* flex wrapper. */
|
|
351
|
+
function renderCellActions(col, row, rowHovered, openPreviewLabel) {
|
|
352
|
+
if (col.onOpenComments) {
|
|
353
|
+
return ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-actions", children: (0, jsx_runtime_1.jsx)(comments_1.CommentCountBadge, { count: col.commentCount?.(row) ?? 0, hovered: rowHovered, onOpen: () => col.onOpenComments?.(row), onOpenPreview: col.onOpenPreview ? () => col.onOpenPreview?.(row) : undefined, previewLabel: openPreviewLabel }) }));
|
|
354
|
+
}
|
|
355
|
+
if (col.onOpenPreview) {
|
|
356
|
+
return ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-actions", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-entity-action", "aria-label": openPreviewLabel, onClick: (ev) => {
|
|
357
|
+
ev.stopPropagation();
|
|
358
|
+
col.onOpenPreview?.(row);
|
|
359
|
+
}, children: (0, jsx_runtime_1.jsx)(lucide_react_1.PanelRight, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) }));
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
295
363
|
/** Built-in typed-cell renderer (used when a column has no custom `render`). */
|
|
296
364
|
// `value` is always the RAW cell value and `display` the caller's `format`
|
|
297
365
|
// output, if any. They are separate arguments on purpose: the entity and tags
|
|
@@ -319,10 +387,7 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US", ope
|
|
|
319
387
|
// at a DIFFERENT record than the row itself (#501).
|
|
320
388
|
ev.stopPropagation();
|
|
321
389
|
col.onEntityClick?.(row);
|
|
322
|
-
}, children: e.name })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-name", children: e.name })), col
|
|
323
|
-
ev.stopPropagation();
|
|
324
|
-
col.onOpenPreview?.(row);
|
|
325
|
-
}, children: (0, jsx_runtime_1.jsx)(lucide_react_1.PanelRight, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) })))] }));
|
|
390
|
+
}, children: e.name })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-name", children: e.name })), renderCellActions(col, row, rowHovered, openPreviewLabel)] }));
|
|
326
391
|
}
|
|
327
392
|
case "tags": {
|
|
328
393
|
// asking for a format on a tag column means "render it your way" -- show
|
|
@@ -342,6 +407,14 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US", ope
|
|
|
342
407
|
// translation) can never silently repaint the chip
|
|
343
408
|
return ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-tag", "data-tone": col.tones?.[s] ?? toneFor(s), children: display || s }));
|
|
344
409
|
}
|
|
410
|
+
case "rating": {
|
|
411
|
+
// Read-only display -- `editor: "rating"` (the always-live picker) is
|
|
412
|
+
// handled separately, the same split `switch` gets (#453).
|
|
413
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
414
|
+
if (!n || Number.isNaN(n))
|
|
415
|
+
return empty;
|
|
416
|
+
return (0, jsx_runtime_1.jsx)(RatingStars, { value: n, max: col.maxRating ?? 5 });
|
|
417
|
+
}
|
|
345
418
|
case "url":
|
|
346
419
|
case "email": {
|
|
347
420
|
const text = display || asText(value);
|
|
@@ -611,6 +684,19 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
611
684
|
// arrow-key nav, Enter) is unaffected -- a plain click resets `selAnchor`
|
|
612
685
|
// to null, collapsing back to single-cell mode.
|
|
613
686
|
const [selAnchor, setSelAnchor] = React.useState(null);
|
|
687
|
+
// crisp/#749: Shift+ArrowUp/Down extends the ROW (checkbox) selection --
|
|
688
|
+
// confirmed live on Attio's own Companies grid, a DIFFERENT gesture from
|
|
689
|
+
// `selAnchor` above (that's Shift+CLICK extending a cell-value range;
|
|
690
|
+
// this issue's own text is explicit the two are independent, and arrow
|
|
691
|
+
// keys were never measured as a cell-range trigger, G-101). Same
|
|
692
|
+
// fixed-anchor/moving-cell shape as `selAnchor` -- a spreadsheet's own
|
|
693
|
+
// Shift+Arrow both moves the active cell AND grows/shrinks the selection
|
|
694
|
+
// relative to wherever the drag started, which recomputing the anchor→
|
|
695
|
+
// new-row span from scratch on every press gives for free (arrowing back
|
|
696
|
+
// toward the anchor shrinks the same way arrowing away grows it). Reset
|
|
697
|
+
// to null on any plain (non-shift) navigation or click, same as
|
|
698
|
+
// `selAnchor`'s own reset points.
|
|
699
|
+
const [rowSelectAnchor, setRowSelectAnchor] = React.useState(null);
|
|
614
700
|
const gridRef = React.useRef(null);
|
|
615
701
|
// The native-copy-event proxy (crisp/#645): Attio "has no Mousetrap/
|
|
616
702
|
// JS-keydown binding for c/mod+c ... it copies purely via the native
|
|
@@ -1020,8 +1106,9 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1020
1106
|
* `editor: "select"`/`"reference"` open their own picker instead of this
|
|
1021
1107
|
* native-input family, but through the SAME double-click gate (#453) --
|
|
1022
1108
|
* see the row-render loop's `isEditing` branch for those, this function
|
|
1023
|
-
* only covers the native-input-backed types.
|
|
1024
|
-
*
|
|
1109
|
+
* only covers the native-input-backed types.
|
|
1110
|
+
* `editor: "switch"`/`"rating"`/`"checkbox"` are the editors with no
|
|
1111
|
+
* double-click gate at all (always live).
|
|
1025
1112
|
*/
|
|
1026
1113
|
function editorFor(col) {
|
|
1027
1114
|
switch (col.type) {
|
|
@@ -1198,6 +1285,36 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1198
1285
|
setSelected(new Set(rows.map((rw) => rw.id)));
|
|
1199
1286
|
return;
|
|
1200
1287
|
}
|
|
1288
|
+
// crisp/#749: Shift+ArrowUp/Down extends the row-checkbox selection --
|
|
1289
|
+
// confirmed live, distinct from Shift+Click's cell-range gesture above
|
|
1290
|
+
// (that one is `selAnchor`; this is `rowSelectAnchor`, and the two
|
|
1291
|
+
// don't interact). Recomputes the whole anchor→destination span on
|
|
1292
|
+
// every press rather than accumulating, so arrowing back toward the
|
|
1293
|
+
// anchor shrinks the selection the same way arrowing away grows it --
|
|
1294
|
+
// the "shrink-from-the-anchor" behavior a spreadsheet's own Shift+Arrow
|
|
1295
|
+
// gives, which a plain add-on-each-press approach can't express.
|
|
1296
|
+
if (selectable &&
|
|
1297
|
+
e.shiftKey &&
|
|
1298
|
+
(e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
|
1299
|
+
const r0 = rows.findIndex((row) => row.id === selCell.id);
|
|
1300
|
+
if (r0 < 0)
|
|
1301
|
+
return;
|
|
1302
|
+
const nr0 = e.key === "ArrowDown"
|
|
1303
|
+
? Math.min(rows.length - 1, r0 + 1)
|
|
1304
|
+
: Math.max(0, r0 - 1);
|
|
1305
|
+
e.preventDefault();
|
|
1306
|
+
const anchorId = rowSelectAnchor ?? selCell.id;
|
|
1307
|
+
const anchorIdx = rows.findIndex((row) => row.id === anchorId);
|
|
1308
|
+
if (anchorIdx < 0)
|
|
1309
|
+
return;
|
|
1310
|
+
setRowSelectAnchor(anchorId);
|
|
1311
|
+
const lo = Math.min(anchorIdx, nr0);
|
|
1312
|
+
const hi = Math.max(anchorIdx, nr0);
|
|
1313
|
+
setSelected(new Set(rows.slice(lo, hi + 1).map((row) => row.id)));
|
|
1314
|
+
setSelCell({ id: rows[nr0].id, key: selCell.key });
|
|
1315
|
+
clipboardProxyRef.current?.select();
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1201
1318
|
const r = rows.findIndex((row) => row.id === selCell.id);
|
|
1202
1319
|
const c = columns.findIndex((col) => col.key === selCell.key);
|
|
1203
1320
|
if (r < 0 || c < 0)
|
|
@@ -1236,6 +1353,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1236
1353
|
else if (e.key === "Escape") {
|
|
1237
1354
|
setSelCell(null);
|
|
1238
1355
|
setSelAnchor(null);
|
|
1356
|
+
setRowSelectAnchor(null);
|
|
1239
1357
|
return;
|
|
1240
1358
|
}
|
|
1241
1359
|
else if (e.key === "Enter") {
|
|
@@ -1243,8 +1361,13 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1243
1361
|
// already its own live widget (#385); Enter there is a no-op, same
|
|
1244
1362
|
// as any other non-editable cell that isn't wired to onRowClick.
|
|
1245
1363
|
// `select`/`reference` DO have one now (like every other editor),
|
|
1246
|
-
// so Enter opens them too.
|
|
1247
|
-
|
|
1364
|
+
// so Enter opens them too. `rating` (#760) and `checkbox` (#761)
|
|
1365
|
+
// join `switch` in the no-separate-editing-mode exception -- same
|
|
1366
|
+
// always-live shape.
|
|
1367
|
+
if (columns[c]?.editable &&
|
|
1368
|
+
columns[c]?.editor !== "switch" &&
|
|
1369
|
+
columns[c]?.editor !== "rating" &&
|
|
1370
|
+
columns[c]?.editor !== "checkbox") {
|
|
1248
1371
|
e.preventDefault();
|
|
1249
1372
|
openEditing(selCell.id, selCell.key);
|
|
1250
1373
|
}
|
|
@@ -1260,8 +1383,10 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1260
1383
|
// A plain arrow key moves the single-cell selection and drops any active
|
|
1261
1384
|
// range -- Attio's own range trigger is Shift+Click only (G-101), arrow
|
|
1262
1385
|
// keys were never measured as a range-extend gesture, so this doesn't
|
|
1263
|
-
// invent one.
|
|
1386
|
+
// invent one. Also drops an in-progress Shift+Arrow row-selection drag
|
|
1387
|
+
// (crisp/#749) for the same reason -- a plain arrow ends that gesture.
|
|
1264
1388
|
setSelAnchor(null);
|
|
1389
|
+
setRowSelectAnchor(null);
|
|
1265
1390
|
setSelCell({ id: rows[nr].id, key: columns[nc].key });
|
|
1266
1391
|
// keeps the clipboard proxy's selection non-collapsed after a move, so
|
|
1267
1392
|
// a Cmd+C right after an arrow key still has something native `copy`
|
|
@@ -1779,6 +1904,22 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1779
1904
|
if (col.editable && col.editor === "switch") {
|
|
1780
1905
|
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commitEdit(row.id, col.key, String(checked)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1781
1906
|
}
|
|
1907
|
+
// crisp/#760: `editor: "rating"` is the same always-
|
|
1908
|
+
// live shape as `switch` above -- clicking a star IS
|
|
1909
|
+
// the commit gesture, confirmed live.
|
|
1910
|
+
if (col.editable && col.editor === "rating") {
|
|
1911
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
1912
|
+
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(RatingStars, { "aria-label": `Edit ${col.key}`, value: Number.isNaN(n) ? 0 : n, max: col.maxRating ?? 5, onChange: (next) => commitEdit(row.id, col.key, String(next)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1913
|
+
}
|
|
1914
|
+
// crisp/#761: `editor: "checkbox"` is a THIRD always-
|
|
1915
|
+
// live boolean shape, distinct from `switch` --
|
|
1916
|
+
// confirmed live against Attio's own "Checkbox"
|
|
1917
|
+
// attribute type, which renders a plain square, not a
|
|
1918
|
+
// toggle. Reuses crisp's own `Checkbox` (already the
|
|
1919
|
+
// measured 16px/r6/brand-fill grid checkbox).
|
|
1920
|
+
if (col.editable && col.editor === "checkbox") {
|
|
1921
|
+
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onChange: (e) => commitEdit(row.id, col.key, String(e.currentTarget.checked)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1922
|
+
}
|
|
1782
1923
|
if (isEditing &&
|
|
1783
1924
|
(col.editor === "select" ||
|
|
1784
1925
|
col.editor === "reference" ||
|
|
@@ -1834,9 +1975,25 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1834
1975
|
i >= range.c0 &&
|
|
1835
1976
|
i <= range.c1);
|
|
1836
1977
|
})();
|
|
1837
|
-
const
|
|
1978
|
+
const rowHoveredForCell = hoveredRowId === row.id;
|
|
1979
|
+
const baseRendered = col.render
|
|
1838
1980
|
? col.render(value, row)
|
|
1839
|
-
: renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale, openPreviewLabel,
|
|
1981
|
+
: renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale, openPreviewLabel, rowHoveredForCell);
|
|
1982
|
+
// crisp/#601 supersession follow-up: `onOpenComments`/
|
|
1983
|
+
// `onOpenPreview`/`commentCount` used to only do
|
|
1984
|
+
// anything inside `renderTyped`'s `"entity"` case,
|
|
1985
|
+
// which already renders its own actions inline (see
|
|
1986
|
+
// `renderCellActions` there) -- silently dropping them
|
|
1987
|
+
// on any OTHER type, and on a custom `render` of any
|
|
1988
|
+
// type, since neither path went through that switch.
|
|
1989
|
+
// This composes the same actions alongside whatever
|
|
1990
|
+
// `rendered` is instead, decoupling the props from
|
|
1991
|
+
// requiring `type: "entity"`.
|
|
1992
|
+
const cellActions = col.type !== "entity" &&
|
|
1993
|
+
(col.onOpenComments || col.onOpenPreview)
|
|
1994
|
+
? renderCellActions(col, row, rowHoveredForCell, openPreviewLabel)
|
|
1995
|
+
: null;
|
|
1996
|
+
const rendered = cellActions ? ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-cell-actions-row", children: [baseRendered, cellActions] })) : (baseRendered);
|
|
1840
1997
|
const isExpanded = expandedCell?.id === row.id &&
|
|
1841
1998
|
expandedCell?.key === col.key;
|
|
1842
1999
|
const cellContent = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!isExpanded && rendered, errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere })), isExpanded && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-cell-overlay", onClick: (e) => e.stopPropagation(), children: rendered }))] }));
|
|
@@ -1844,7 +2001,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1844
2001
|
? "crisp-datatable-editable"
|
|
1845
2002
|
: undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
|
|
1846
2003
|
? ""
|
|
1847
|
-
: undefined, "data-in-range": inRange ? "" : undefined, "data-cell-expanded": isExpanded ? "" : undefined, onClick: (e) => {
|
|
2004
|
+
: undefined, "data-in-range": inRange ? "" : undefined, "data-cell-expanded": isExpanded ? "" : undefined, "data-enriched": col.enriched ? "" : undefined, onClick: (e) => {
|
|
1848
2005
|
// the keyboard model's anchor -- a single click
|
|
1849
2006
|
// only selects. A genuine double-click (or Enter
|
|
1850
2007
|
// on the selected cell) opens the editor (#411),
|
|
@@ -1859,6 +2016,11 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1859
2016
|
}
|
|
1860
2017
|
else {
|
|
1861
2018
|
setSelAnchor(null);
|
|
2019
|
+
// crisp/#749: a plain click also ends an
|
|
2020
|
+
// in-progress Shift+Arrow row-selection drag
|
|
2021
|
+
// -- Shift+Click itself doesn't touch it,
|
|
2022
|
+
// the two gestures are independent.
|
|
2023
|
+
setRowSelectAnchor(null);
|
|
1862
2024
|
}
|
|
1863
2025
|
setSelCell({ id: row.id, key: col.key });
|
|
1864
2026
|
// Focuses the clipboard proxy, not `gridRef`
|
|
@@ -1899,6 +2061,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1899
2061
|
// selected (crisp/#598) -- collapses any
|
|
1900
2062
|
// active range, same as a plain left-click.
|
|
1901
2063
|
setSelAnchor(null);
|
|
2064
|
+
setRowSelectAnchor(null);
|
|
1902
2065
|
setSelCell({ id: row.id, key: col.key });
|
|
1903
2066
|
clipboardProxyRef.current?.focus();
|
|
1904
2067
|
clipboardProxyRef.current?.select();
|
|
@@ -138,6 +138,12 @@ const config = (0, core_1.defineRecipe)({
|
|
|
138
138
|
& tbody tr[data-selected] td { background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 10%, ${tokens_1.contract.bgSurface}); }
|
|
139
139
|
& tbody tr[data-selected]:hover td { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 10%, ${tokens_1.contract.bgSurface})); }
|
|
140
140
|
& .crisp-datatable-editable:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, ${tokens_1.contract.bgSurface}); cursor: text; }
|
|
141
|
+
/* crisp/#758: AI-enriched column marker, ported from RecordsTable's own
|
|
142
|
+
already-measured value -- a full-bleed #9b69ff (bgPurple) wash at 4%,
|
|
143
|
+
composited OVER whatever row-state background the cell already has
|
|
144
|
+
(hover, selected), which is why it's an ::after overlay and not a
|
|
145
|
+
plain background. */
|
|
146
|
+
& td[data-enriched]::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: color-mix(in srgb, ${tokens_1.contract.bgPurple} 4%, transparent); }
|
|
141
147
|
& .crisp-datatable-edit { width: 100%; height: 28px; margin: 0; padding: 0 ${tokens_1.contract.space["1.5"]}; border: none; border-radius: ${tokens_1.contract.radius.sm}; background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; outline: none; vertical-align: middle; }
|
|
142
148
|
& .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; }
|
|
143
149
|
/* An async onEdit's in-flight state (#385) -- the field/picker dims and
|
|
@@ -221,6 +227,13 @@ const config = (0, core_1.defineRecipe)({
|
|
|
221
227
|
& th[data-drop-after]::after { inset-inline-end: -1.5px; }
|
|
222
228
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
223
229
|
& .crisp-datatable-entity { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; min-width: 0; color: ${tokens_1.contract.fgPrimary}; font-weight: ${tokens_1.contract.weight.medium}; }
|
|
230
|
+
/* crisp/#601 supersession follow-up -- the same flex-row shape
|
|
231
|
+
.crisp-datatable-entity gives its own avatar+name+actions, but
|
|
232
|
+
structural only (no entity-specific colour/weight): lets
|
|
233
|
+
onOpenComments/onOpenPreview compose alongside ANY column's own
|
|
234
|
+
content (a custom render, or a non-entity built-in type), not just
|
|
235
|
+
type: "entity"'s own inline rendering. */
|
|
236
|
+
& .crisp-datatable-cell-actions-row { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; min-width: 0; width: 100%; }
|
|
224
237
|
/* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text,
|
|
225
238
|
plain hover vs rest) that Attio's underline is unconditional, not a
|
|
226
239
|
hover reveal -- identical at rest and on hover: text-decoration-color
|
|
@@ -262,6 +275,23 @@ const config = (0, core_1.defineRecipe)({
|
|
|
262
275
|
link. */
|
|
263
276
|
& .crisp-datatable-link { color: ${tokens_1.contract.fgPrimary}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
264
277
|
& .crisp-datatable-link:hover { text-decoration: underline; }
|
|
278
|
+
/* rating cell type/editor (#760) -- measured live: 16px stars on an
|
|
279
|
+
18px pitch (a 2px gap), an empty star at rgba(0,0,0,0.1), a
|
|
280
|
+
committed/filled star at brand600 (--crisp-bg-brand-solid, an exact
|
|
281
|
+
match to the measured #266df0), and a distinct hover-preview colour
|
|
282
|
+
-- rgb(33,91,196) -- that's close to but NOT brand700/bgBrandSolidPressed
|
|
283
|
+
(#1f5fd6); kept as its own literal rather than forced onto that token,
|
|
284
|
+
since the two were measured stable and different. The read-only svg
|
|
285
|
+
and the editable button's own inner svg both carry the
|
|
286
|
+
crisp-datatable-rating-star class on the svg itself (not the
|
|
287
|
+
button), so one set of fill rules covers both -- the button only
|
|
288
|
+
adds its own reset + cursor via a separate -btn class. */
|
|
289
|
+
& .crisp-datatable-rating { display: inline-flex; align-items: center; gap: 2px; }
|
|
290
|
+
& .crisp-datatable-rating-star { display: block; }
|
|
291
|
+
& .crisp-datatable-rating-star path { fill: rgba(0, 0, 0, 0.1); }
|
|
292
|
+
& .crisp-datatable-rating-star[data-filled] path { fill: ${tokens_1.contract.bgBrandSolid}; }
|
|
293
|
+
& .crisp-datatable-rating-star[data-hovering] path { fill: #215bc4; }
|
|
294
|
+
& .crisp-datatable-rating-star-btn { display: inline-flex; padding: 0; margin: 0; border: none; background: none; font-family: inherit; cursor: pointer; }
|
|
265
295
|
/* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
|
|
266
296
|
a leading 20px grip column, a 16px grip icon, and the row's own
|
|
267
297
|
hairline doubling as the drop indicator (it tints toward the brand
|
|
@@ -53,12 +53,35 @@ const toolbarChromeHead = (p) => ` & .${namespace_1.CLASS_PREFIX}${p}-toolbar
|
|
|
53
53
|
& .${namespace_1.CLASS_PREFIX}${p}-search svg { flex-shrink: 0; }
|
|
54
54
|
& .${namespace_1.CLASS_PREFIX}${p}-search input { border: none; outline: none; background: transparent; font-family: inherit; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgPrimary}; width: 180px; }
|
|
55
55
|
& .${namespace_1.CLASS_PREFIX}${p}-search input::placeholder { color: ${tokens_1.contract.fgMuted}; }
|
|
56
|
-
& .${namespace_1.CLASS_PREFIX}${p}-tool { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.row}; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background:
|
|
56
|
+
& .${namespace_1.CLASS_PREFIX}${p}-tool { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.row}; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderDivider}; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
|
|
57
57
|
& .${namespace_1.CLASS_PREFIX}${p}-tool:hover { background: ${tokens_1.contract.bgRaised}; }
|
|
58
58
|
& .${namespace_1.CLASS_PREFIX}${p}-tool svg { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
|
|
59
|
-
|
|
59
|
+
/* crisp/#772: re-measured live on Sort with an actual sort applied --
|
|
60
|
+
an active trigger's own fill is white (bgSurface), the same as the
|
|
61
|
+
base .tool rule above now already sets; this override existed to
|
|
62
|
+
distinguish active from a base state that used to be transparent.
|
|
63
|
+
Kept explicit (not just deleted) so a future change to the base
|
|
64
|
+
rule's background can't silently change this one too. */
|
|
65
|
+
& .${namespace_1.CLASS_PREFIX}${p}-tool--active { background: ${tokens_1.contract.bgSurface}; }
|
|
60
66
|
& .${namespace_1.CLASS_PREFIX}${p}-tool--ghost { box-shadow: none; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 3%, transparent); color: ${tokens_1.contract.fgMuted}; }
|
|
61
67
|
& .${namespace_1.CLASS_PREFIX}${p}-tool--ghost:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); }
|
|
68
|
+
/* crisp/#772 follow-up, re-measured live 2026-09-25: an EMPTY Sort/Filter
|
|
69
|
+
trigger (no sort/condition set) isn't a solid-ring chip at all -- it's
|
|
70
|
+
a dashed ::before (1px dashed, border-radius: inherit, absolutely
|
|
71
|
+
positioned inset: 0) over a barely-there fill, confirmed on Attio's
|
|
72
|
+
own empty Filter chip (its ::before border computes to exactly
|
|
73
|
+
"1px dashed rgba(0, 0, 0, 0.06)"). Once a value IS set, the trigger
|
|
74
|
+
drops the dashed pseudo and picks up the base tool's own solid ring
|
|
75
|
+
instead (the --active modifier, unchanged) -- confirmed on Sort with
|
|
76
|
+
an actual sort applied. color-mix (not the raw rgba Attio computes
|
|
77
|
+
to) so both the border and the fill flip correctly in dark mode, the
|
|
78
|
+
same reasoning --ghost above already uses. Only wired into
|
|
79
|
+
TableToolbar itself (tabletoolbar.tsx) this pass -- RecordsTable's
|
|
80
|
+
own Sort/Filter triggers share this same chrome function but are
|
|
81
|
+
deprecated, not touched here. */
|
|
82
|
+
& .${namespace_1.CLASS_PREFIX}${p}-tool--dashed { position: relative; box-shadow: none; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 2%, transparent); }
|
|
83
|
+
& .${namespace_1.CLASS_PREFIX}${p}-tool--dashed::before { content: ""; position: absolute; inset: 0; border-radius: inherit; border: 1px dashed color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); pointer-events: none; }
|
|
84
|
+
& .${namespace_1.CLASS_PREFIX}${p}-tool--dashed:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); }
|
|
62
85
|
& .${namespace_1.CLASS_PREFIX}${p}-toolgroup { position: relative; display: inline-flex; }
|
|
63
86
|
& .${namespace_1.CLASS_PREFIX}${p}-divider { flex-shrink: 0; width: 1px; height: 20px; background: ${tokens_1.contract.borderSubtle}; }
|
|
64
87
|
& .${namespace_1.CLASS_PREFIX}${p}-menu { position: absolute; top: calc(100% + ${tokens_1.contract.space["1"]}); left: 0; z-index: 30; min-width: 232px; padding: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover}; box-shadow: ${tokens_1.elevation.floating}; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px}; transform-origin: top; opacity: 1; transform: none; transition: opacity .13s ease, transform .13s ease; }
|