@crisp-ui-kit/crisp 0.54.10 → 0.55.0
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 +249 -46
- package/dist/cjs/components/datatable/datatable.recipe.js +24 -0
- package/dist/cjs/components/resizablepanel/resizablepanel.js +10 -1
- package/dist/cjs/components/table/table.js +8 -1
- package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +176 -43
- package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +28 -9
- package/dist/cjs/index.js +0 -2
- package/dist/cjs/product/recordpage/column.js +19 -0
- package/dist/cjs/product/recordpage/index.js +6 -1
- package/dist/cjs/product/recordpage/recordpage.js +25 -8
- package/dist/cjs/product/recordpage/test-fixtures.js +55 -0
- package/dist/cjs/product/savedviews/savedviews.recipe.js +13 -11
- package/dist/datatable.css +14 -0
- package/dist/datatable.layered.css +14 -0
- package/dist/esm/components/datatable/datatable.js +250 -47
- package/dist/esm/components/datatable/datatable.recipe.js +24 -0
- package/dist/esm/components/resizablepanel/resizablepanel.js +10 -1
- package/dist/esm/components/table/table.js +8 -1
- package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +177 -44
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +28 -9
- package/dist/esm/index.js +0 -2
- package/dist/esm/product/recordpage/column.js +16 -0
- package/dist/esm/product/recordpage/index.js +2 -0
- package/dist/esm/product/recordpage/recordpage.js +22 -5
- package/dist/esm/product/recordpage/test-fixtures.js +52 -0
- package/dist/esm/product/savedviews/savedviews.recipe.js +13 -11
- package/dist/savedviews.css +6 -18
- package/dist/savedviews.layered.css +6 -18
- package/dist/styles.css +43 -276
- package/dist/styles.layered.css +43 -276
- package/dist/tabletoolbar.css +23 -3
- package/dist/tabletoolbar.layered.css +23 -3
- package/dist/types/components/datatable/datatable.d.ts +30 -3
- package/dist/types/components/table/table.d.ts +14 -0
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +26 -1
- package/dist/types/index.d.ts +0 -2
- package/dist/types/product/recordpage/column.d.ts +112 -0
- package/dist/types/product/recordpage/index.d.ts +2 -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/package.json +2 -12
- package/dist/cjs/product/recordstable/company-logos.js +0 -29
- package/dist/cjs/product/recordstable/index.js +0 -13
- package/dist/cjs/product/recordstable/recordstable.js +0 -1357
- package/dist/cjs/product/recordstable/recordstable.recipe.js +0 -175
- package/dist/cjs/product/recordstable/recordstable.tag.js +0 -26
- package/dist/cjs/product/taskstable/index.js +0 -7
- package/dist/cjs/product/taskstable/taskstable.js +0 -27
- package/dist/cjs/product/taskstable/taskstable.recipe.js +0 -56
- package/dist/esm/product/recordstable/company-logos.js +0 -26
- package/dist/esm/product/recordstable/index.js +0 -3
- package/dist/esm/product/recordstable/recordstable.js +0 -1317
- package/dist/esm/product/recordstable/recordstable.recipe.js +0 -172
- package/dist/esm/product/recordstable/recordstable.tag.js +0 -23
- package/dist/esm/product/taskstable/index.js +0 -2
- package/dist/esm/product/taskstable/taskstable.js +0 -24
- package/dist/esm/product/taskstable/taskstable.recipe.js +0 -53
- package/dist/recordstable.css +0 -223
- package/dist/recordstable.d.ts +0 -7
- package/dist/recordstable.layered.css +0 -225
- package/dist/taskstable.css +0 -30
- package/dist/taskstable.d.ts +0 -7
- package/dist/taskstable.layered.css +0 -32
- package/dist/types/product/recordstable/company-logos.d.ts +0 -1
- package/dist/types/product/recordstable/index.d.ts +0 -3
- package/dist/types/product/recordstable/recordstable.d.ts +0 -289
- package/dist/types/product/recordstable/recordstable.recipe.d.ts +0 -5
- package/dist/types/product/recordstable/recordstable.tag.d.ts +0 -15
- package/dist/types/product/taskstable/index.d.ts +0 -2
- package/dist/types/product/taskstable/taskstable.d.ts +0 -54
- package/dist/types/product/taskstable/taskstable.recipe.d.ts +0 -3
- /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);
|
|
@@ -998,14 +1071,48 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
998
1071
|
const colAriaIndex = (i) => ariaColIndexBase + i + 1;
|
|
999
1072
|
const allSelected = rows.length > 0 && rows.every((r) => selected.has(r.id));
|
|
1000
1073
|
const someSelected = selected.size > 0 && !allSelected;
|
|
1074
|
+
// crisp/#782, ported from RecordsTable's own already-shipped logic: the
|
|
1075
|
+
// header checkbox speaks for the rows you can SEE. Rebuilding the whole
|
|
1076
|
+
// set from `rows` (the filtered/visible set) would silently drop any
|
|
1077
|
+
// selection an active filter is hiding -- add/remove only the visible
|
|
1078
|
+
// rows and leave the rest of `selected` alone.
|
|
1001
1079
|
function toggleAll() {
|
|
1002
|
-
|
|
1080
|
+
const next = new Set(selected);
|
|
1081
|
+
for (const r of rows) {
|
|
1082
|
+
if (allSelected)
|
|
1083
|
+
next.delete(r.id);
|
|
1084
|
+
else
|
|
1085
|
+
next.add(r.id);
|
|
1086
|
+
}
|
|
1087
|
+
setSelected(next);
|
|
1003
1088
|
}
|
|
1004
1089
|
function toggleRow(id) {
|
|
1005
1090
|
const next = new Set(selected);
|
|
1006
1091
|
next.has(id) ? next.delete(id) : next.add(id);
|
|
1007
1092
|
setSelected(next);
|
|
1008
1093
|
}
|
|
1094
|
+
// crisp/#781: Shift+Click on a row checkbox selects the whole contiguous
|
|
1095
|
+
// range from the last plain click (the anchor) to this row -- confirmed
|
|
1096
|
+
// live, the mouse counterpart to #749's Shift+Arrow keyboard gesture,
|
|
1097
|
+
// sharing the same `rowSelectAnchor` state and the same recompute-the-
|
|
1098
|
+
// whole-span shape (not accumulate) so shift-clicking back toward the
|
|
1099
|
+
// anchor shrinks the same way clicking away from it grows. A plain click
|
|
1100
|
+
// both toggles normally AND re-anchors, so the NEXT Shift+Click extends
|
|
1101
|
+
// from wherever the user last plain-clicked, matching Attio.
|
|
1102
|
+
function toggleRowOrExtend(id, shiftKey) {
|
|
1103
|
+
if (shiftKey && rowSelectAnchor != null) {
|
|
1104
|
+
const anchorIdx = rows.findIndex((r) => r.id === rowSelectAnchor);
|
|
1105
|
+
const idx = rows.findIndex((r) => r.id === id);
|
|
1106
|
+
if (anchorIdx >= 0 && idx >= 0) {
|
|
1107
|
+
const lo = Math.min(anchorIdx, idx);
|
|
1108
|
+
const hi = Math.max(anchorIdx, idx);
|
|
1109
|
+
setSelected(new Set(rows.slice(lo, hi + 1).map((r) => r.id)));
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
setRowSelectAnchor(id);
|
|
1114
|
+
toggleRow(id);
|
|
1115
|
+
}
|
|
1009
1116
|
function cycleSort(key) {
|
|
1010
1117
|
// Clicking a header drives a single sort, as before. Multi-sort is supplied
|
|
1011
1118
|
// by the caller through the `sort` prop (built in a sort popover).
|
|
@@ -1033,8 +1140,9 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1033
1140
|
* `editor: "select"`/`"reference"` open their own picker instead of this
|
|
1034
1141
|
* native-input family, but through the SAME double-click gate (#453) --
|
|
1035
1142
|
* see the row-render loop's `isEditing` branch for those, this function
|
|
1036
|
-
* only covers the native-input-backed types.
|
|
1037
|
-
*
|
|
1143
|
+
* only covers the native-input-backed types.
|
|
1144
|
+
* `editor: "switch"`/`"rating"`/`"checkbox"` are the editors with no
|
|
1145
|
+
* double-click gate at all (always live).
|
|
1038
1146
|
*/
|
|
1039
1147
|
function editorFor(col) {
|
|
1040
1148
|
switch (col.type) {
|
|
@@ -1211,6 +1319,28 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1211
1319
|
setSelected(new Set(rows.map((rw) => rw.id)));
|
|
1212
1320
|
return;
|
|
1213
1321
|
}
|
|
1322
|
+
// crisp/#772: Cmd/Ctrl+ArrowUp jumps the selected cell to the first row,
|
|
1323
|
+
// Cmd/Ctrl+ArrowDown to the last -- confirmed live (Attio's own help
|
|
1324
|
+
// drawer documents "Jump to top"/"Jump to bottom" on these exact
|
|
1325
|
+
// bindings). `!e.shiftKey` guards this from stealing Cmd+Shift+Arrow,
|
|
1326
|
+
// a separate gesture (#773, extend-selection-to-first/last) that falls
|
|
1327
|
+
// through to the Shift+Arrow branch below untouched. Placed before that
|
|
1328
|
+
// branch for the same reason: without the guard, Cmd+Shift+Arrow would
|
|
1329
|
+
// still reach it (that branch doesn't check metaKey/ctrlKey either),
|
|
1330
|
+
// just not do the "jump" part -- the guard here is what keeps the two
|
|
1331
|
+
// gestures from fighting over the same keys.
|
|
1332
|
+
if ((e.metaKey || e.ctrlKey) &&
|
|
1333
|
+
!e.shiftKey &&
|
|
1334
|
+
(e.key === "ArrowUp" || e.key === "ArrowDown")) {
|
|
1335
|
+
const r0 = rows.findIndex((row) => row.id === selCell.id);
|
|
1336
|
+
if (r0 < 0)
|
|
1337
|
+
return;
|
|
1338
|
+
e.preventDefault();
|
|
1339
|
+
const nr0 = e.key === "ArrowUp" ? 0 : rows.length - 1;
|
|
1340
|
+
setSelCell({ id: rows[nr0].id, key: selCell.key });
|
|
1341
|
+
clipboardProxyRef.current?.select();
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1214
1344
|
// crisp/#749: Shift+ArrowUp/Down extends the row-checkbox selection --
|
|
1215
1345
|
// confirmed live, distinct from Shift+Click's cell-range gesture above
|
|
1216
1346
|
// (that one is `selAnchor`; this is `rowSelectAnchor`, and the two
|
|
@@ -1287,8 +1417,13 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1287
1417
|
// already its own live widget (#385); Enter there is a no-op, same
|
|
1288
1418
|
// as any other non-editable cell that isn't wired to onRowClick.
|
|
1289
1419
|
// `select`/`reference` DO have one now (like every other editor),
|
|
1290
|
-
// so Enter opens them too.
|
|
1291
|
-
|
|
1420
|
+
// so Enter opens them too. `rating` (#760) and `checkbox` (#761)
|
|
1421
|
+
// join `switch` in the no-separate-editing-mode exception -- same
|
|
1422
|
+
// always-live shape.
|
|
1423
|
+
if (columns[c]?.editable &&
|
|
1424
|
+
columns[c]?.editor !== "switch" &&
|
|
1425
|
+
columns[c]?.editor !== "rating" &&
|
|
1426
|
+
columns[c]?.editor !== "checkbox") {
|
|
1292
1427
|
e.preventDefault();
|
|
1293
1428
|
openEditing(selCell.id, selCell.key);
|
|
1294
1429
|
}
|
|
@@ -1357,15 +1492,21 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1357
1492
|
}
|
|
1358
1493
|
setEditing(null);
|
|
1359
1494
|
}
|
|
1495
|
+
const reduceMotion = (0, core_1.usePrefersReducedMotion)();
|
|
1360
1496
|
// crisp/#577 -- keyboard nav to an off-screen (virtualized-out) cell
|
|
1361
|
-
// scrolls it into the rendered window, on both axes.
|
|
1362
|
-
//
|
|
1363
|
-
//
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1497
|
+
// scrolls it into the rendered window, on both axes. `renderItems`/
|
|
1498
|
+
// `rowOffsets` are computed unconditionally (`height == null` just makes
|
|
1499
|
+
// them a no-op elsewhere, see the comment above `renderItems`), so this
|
|
1500
|
+
// runs on both axes regardless of `height` too (crisp/#780 -- it used to
|
|
1501
|
+
// gate the vertical half on `height != null`, on the reasoning that only
|
|
1502
|
+
// `height` gives the grid an internal scroll position to move; that's
|
|
1503
|
+
// wrong when the grid's own container still scrolls without an explicit
|
|
1504
|
+
// height, e.g. inside a shorter ancestor, matching `RecordsTable`'s own
|
|
1505
|
+
// already-shipped #257 fix, which had no such gate). `widths` is read only
|
|
1506
|
+
// indirectly, through `colWidth`'s closure over it -- biome can't see
|
|
1507
|
+
// that, but a column resize while a cell is selected genuinely needs to
|
|
1508
|
+
// re-run this to keep it in view, so the dependency is real even though
|
|
1509
|
+
// the lint rule can't prove it.
|
|
1369
1510
|
// biome-ignore lint/correctness/useExhaustiveDependencies: widths is read via colWidth's closure, see comment above
|
|
1370
1511
|
React.useEffect(() => {
|
|
1371
1512
|
if (!selCell)
|
|
@@ -1373,28 +1514,37 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1373
1514
|
const el = scrollRef.current;
|
|
1374
1515
|
if (!el)
|
|
1375
1516
|
return;
|
|
1376
|
-
//
|
|
1377
|
-
//
|
|
1378
|
-
//
|
|
1379
|
-
//
|
|
1380
|
-
//
|
|
1517
|
+
// `el.scrollTo` rather than a direct `el.scrollTop =`/`el.scrollLeft =`
|
|
1518
|
+
// assignment (crisp/#780, matching RecordsTable's own #208/#257 rule):
|
|
1519
|
+
// property assignment is ALWAYS instant, so it can never honour a
|
|
1520
|
+
// consumer's own `scroll-behavior: smooth` on this container; `scrollTo`
|
|
1521
|
+
// defers to that CSS when no `behavior` is given, and only forces
|
|
1522
|
+
// `"auto"` under `prefers-reduced-motion`, which is the one case that
|
|
1523
|
+
// must win even over a host page's own smooth-scroll opt-in.
|
|
1524
|
+
const scrollTo = (opts) => el.scrollTo({
|
|
1525
|
+
...opts,
|
|
1526
|
+
...(reduceMotion ? { behavior: "auto" } : null),
|
|
1527
|
+
});
|
|
1528
|
+
// Sets `viewport` directly from the SAME value just passed to
|
|
1529
|
+
// `scrollTo`, rather than waiting for a follow-up `scroll` event to
|
|
1530
|
+
// round-trip through the `onScroll` handler above -- a real browser
|
|
1531
|
+
// does fire one when the scroll position changes programmatically, but
|
|
1532
|
+
// relying on that would make this effect's own visible result (the
|
|
1381
1533
|
// newly-selected cell actually rendering) depend on a second render
|
|
1382
1534
|
// triggered from outside React's own update, which is both slower and
|
|
1383
1535
|
// one more thing that has to be true for this to work.
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
setViewport((v) => ({ ...v, top: newTop }));
|
|
1397
|
-
}
|
|
1536
|
+
const itemIndex = rowIdToItemIndex.get(selCell.id);
|
|
1537
|
+
if (itemIndex != null) {
|
|
1538
|
+
const itemTop = rowOffsets[itemIndex];
|
|
1539
|
+
const itemBottom = itemTop + renderItems[itemIndex].height;
|
|
1540
|
+
if (itemTop < el.scrollTop) {
|
|
1541
|
+
scrollTo({ top: itemTop });
|
|
1542
|
+
setViewport((v) => ({ ...v, top: itemTop }));
|
|
1543
|
+
}
|
|
1544
|
+
else if (itemBottom > el.scrollTop + el.clientHeight) {
|
|
1545
|
+
const newTop = itemBottom - el.clientHeight;
|
|
1546
|
+
scrollTo({ top: newTop });
|
|
1547
|
+
setViewport((v) => ({ ...v, top: newTop }));
|
|
1398
1548
|
}
|
|
1399
1549
|
}
|
|
1400
1550
|
const ci = columns.findIndex((c) => c.key === selCell.key);
|
|
@@ -1404,24 +1554,24 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1404
1554
|
left += colWidth(columns[i]);
|
|
1405
1555
|
const right = left + colWidth(columns[ci]);
|
|
1406
1556
|
if (left < el.scrollLeft) {
|
|
1407
|
-
|
|
1557
|
+
scrollTo({ left });
|
|
1408
1558
|
setViewport((v) => ({ ...v, left }));
|
|
1409
1559
|
}
|
|
1410
1560
|
else if (right > el.scrollLeft + el.clientWidth) {
|
|
1411
1561
|
const newLeft = right - el.clientWidth;
|
|
1412
|
-
|
|
1562
|
+
scrollTo({ left: newLeft });
|
|
1413
1563
|
setViewport((v) => ({ ...v, left: newLeft }));
|
|
1414
1564
|
}
|
|
1415
1565
|
}
|
|
1416
1566
|
}, [
|
|
1417
1567
|
selCell,
|
|
1418
|
-
height,
|
|
1419
1568
|
columns,
|
|
1420
1569
|
widths,
|
|
1421
1570
|
selectable,
|
|
1422
1571
|
rowIdToItemIndex,
|
|
1423
1572
|
rowOffsets,
|
|
1424
1573
|
renderItems,
|
|
1574
|
+
reduceMotion,
|
|
1425
1575
|
]);
|
|
1426
1576
|
const totalCols = columns.length + (selectable ? 1 : 0) + (showGrip ? 1 : 0);
|
|
1427
1577
|
const hasSummary = columns.some((c) => c.summary || c.calc);
|
|
@@ -1795,12 +1945,33 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1795
1945
|
}, onDragEnd: () => {
|
|
1796
1946
|
setDragRowId(null);
|
|
1797
1947
|
setDropRowId(null);
|
|
1798
|
-
}, children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) })), selectable && ((0, jsx_runtime_1.jsx)(table_1.TableCell, { className: "crisp-datatable-col-check", "aria-colindex": showGrip ? 2 : 1, ...stickyCol(-1), onClick: (e) => e.stopPropagation(), children: (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-check", children: (0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { "aria-label": selectRowLabel(row), checked: selected.has(row.id),
|
|
1948
|
+
}, children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) })), selectable && ((0, jsx_runtime_1.jsx)(table_1.TableCell, { className: "crisp-datatable-col-check", "aria-colindex": showGrip ? 2 : 1, ...stickyCol(-1), onClick: (e) => e.stopPropagation(), children: (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-check", children: (0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { "aria-label": selectRowLabel(row), checked: selected.has(row.id),
|
|
1949
|
+
// preventDefault stops the native check/change
|
|
1950
|
+
// this click would otherwise also fire -- this
|
|
1951
|
+
// handler is the single source of truth so a
|
|
1952
|
+
// Shift+Click's range-select and a plain
|
|
1953
|
+
// click's toggle-and-re-anchor never race each
|
|
1954
|
+
// other (crisp/#781).
|
|
1955
|
+
onClick: (e) => {
|
|
1956
|
+
e.preventDefault();
|
|
1957
|
+
toggleRowOrExtend(row.id, e.shiftKey);
|
|
1958
|
+
}, onChange: () => { } }) }) })), colSlots.map((slot) => {
|
|
1799
1959
|
if (slot.type === "spacer") {
|
|
1800
1960
|
return ((0, jsx_runtime_1.jsx)("td", { role: "presentation", "aria-hidden": "true", style: { padding: 0, border: "none" } }, `__spacer_${slot.side}`));
|
|
1801
1961
|
}
|
|
1802
1962
|
const col = columns[slot.i];
|
|
1803
1963
|
const i = slot.i;
|
|
1964
|
+
// crisp/#779, matching RecordsTable's own #231 fix:
|
|
1965
|
+
// column 0 is the row's own identity (its name), not
|
|
1966
|
+
// a value it holds -- a screen reader crossing the
|
|
1967
|
+
// row should read "Google, Employees, 182,000", not a
|
|
1968
|
+
// bare "182,000". Spread onto every TableCell this
|
|
1969
|
+
// column can render as (every editor branch, not just
|
|
1970
|
+
// the default one), since the row-identity semantics
|
|
1971
|
+
// don't depend on which editor is open.
|
|
1972
|
+
const rowHeaderProps = i === 0
|
|
1973
|
+
? { as: "th", scope: "row" }
|
|
1974
|
+
: undefined;
|
|
1804
1975
|
const isEditing = editing?.id === row.id && editing?.key === col.key;
|
|
1805
1976
|
const value = cellValue(col, row);
|
|
1806
1977
|
const sticky = dataStickyCol(i);
|
|
@@ -1823,13 +1994,29 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1823
1994
|
// right). See the `isEditing` block below for their
|
|
1824
1995
|
// actual editor.
|
|
1825
1996
|
if (col.editable && col.editor === "switch") {
|
|
1826
|
-
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));
|
|
1997
|
+
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, 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));
|
|
1998
|
+
}
|
|
1999
|
+
// crisp/#760: `editor: "rating"` is the same always-
|
|
2000
|
+
// live shape as `switch` above -- clicking a star IS
|
|
2001
|
+
// the commit gesture, confirmed live.
|
|
2002
|
+
if (col.editable && col.editor === "rating") {
|
|
2003
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
2004
|
+
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, 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));
|
|
2005
|
+
}
|
|
2006
|
+
// crisp/#761: `editor: "checkbox"` is a THIRD always-
|
|
2007
|
+
// live boolean shape, distinct from `switch` --
|
|
2008
|
+
// confirmed live against Attio's own "Checkbox"
|
|
2009
|
+
// attribute type, which renders a plain square, not a
|
|
2010
|
+
// toggle. Reuses crisp's own `Checkbox` (already the
|
|
2011
|
+
// measured 16px/r6/brand-fill grid checkbox).
|
|
2012
|
+
if (col.editable && col.editor === "checkbox") {
|
|
2013
|
+
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, 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));
|
|
1827
2014
|
}
|
|
1828
2015
|
if (isEditing &&
|
|
1829
2016
|
(col.editor === "select" ||
|
|
1830
2017
|
col.editor === "reference" ||
|
|
1831
2018
|
col.editor === "actor")) {
|
|
1832
|
-
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,
|
|
2019
|
+
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined,
|
|
1833
2020
|
// Same as the plain text editor's own cell:
|
|
1834
2021
|
// opening/using the picker must not also fire
|
|
1835
2022
|
// the row's onRowClick. Escape is handled by
|
|
@@ -1843,7 +2030,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1843
2030
|
onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [col.editor === "select" ? ((0, jsx_runtime_1.jsx)(DataSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })) : ((0, jsx_runtime_1.jsx)(DataReferenceEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })), 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));
|
|
1844
2031
|
}
|
|
1845
2032
|
if (isEditing) {
|
|
1846
|
-
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-datatable-edit",
|
|
2033
|
+
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-datatable-edit",
|
|
1847
2034
|
// biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
|
|
1848
2035
|
autoFocus: true, ...editorFor(col), "data-editor": editorFor(col).type, disabled: isPending, defaultValue: col.type === "date"
|
|
1849
2036
|
? toDateInputValue(value)
|
|
@@ -1880,13 +2067,29 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1880
2067
|
i >= range.c0 &&
|
|
1881
2068
|
i <= range.c1);
|
|
1882
2069
|
})();
|
|
1883
|
-
const
|
|
2070
|
+
const rowHoveredForCell = hoveredRowId === row.id;
|
|
2071
|
+
const baseRendered = col.render
|
|
1884
2072
|
? col.render(value, row)
|
|
1885
|
-
: renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale, openPreviewLabel,
|
|
2073
|
+
: renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale, openPreviewLabel, rowHoveredForCell);
|
|
2074
|
+
// crisp/#601 supersession follow-up: `onOpenComments`/
|
|
2075
|
+
// `onOpenPreview`/`commentCount` used to only do
|
|
2076
|
+
// anything inside `renderTyped`'s `"entity"` case,
|
|
2077
|
+
// which already renders its own actions inline (see
|
|
2078
|
+
// `renderCellActions` there) -- silently dropping them
|
|
2079
|
+
// on any OTHER type, and on a custom `render` of any
|
|
2080
|
+
// type, since neither path went through that switch.
|
|
2081
|
+
// This composes the same actions alongside whatever
|
|
2082
|
+
// `rendered` is instead, decoupling the props from
|
|
2083
|
+
// requiring `type: "entity"`.
|
|
2084
|
+
const cellActions = col.type !== "entity" &&
|
|
2085
|
+
(col.onOpenComments || col.onOpenPreview)
|
|
2086
|
+
? renderCellActions(col, row, rowHoveredForCell, openPreviewLabel)
|
|
2087
|
+
: null;
|
|
2088
|
+
const rendered = cellActions ? ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-cell-actions-row", children: [baseRendered, cellActions] })) : (baseRendered);
|
|
1886
2089
|
const isExpanded = expandedCell?.id === row.id &&
|
|
1887
2090
|
expandedCell?.key === col.key;
|
|
1888
2091
|
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 }))] }));
|
|
1889
|
-
return ((0, jsx_runtime_1.jsx)(contextmenu_1.ContextMenu, { asChild: true, variant: "dark", menuLabel: cellMenuLabel, items: cellMenuItems(row, col), children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: col.editable
|
|
2092
|
+
return ((0, jsx_runtime_1.jsx)(contextmenu_1.ContextMenu, { asChild: true, variant: "dark", menuLabel: cellMenuLabel, items: cellMenuItems(row, col), children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: col.editable
|
|
1890
2093
|
? "crisp-datatable-editable"
|
|
1891
2094
|
: undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
|
|
1892
2095
|
? ""
|
|
@@ -227,6 +227,13 @@ const config = (0, core_1.defineRecipe)({
|
|
|
227
227
|
& th[data-drop-after]::after { inset-inline-end: -1.5px; }
|
|
228
228
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
229
229
|
& .crisp-datatable-entity { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; min-width: 0; color: ${tokens_1.contract.fgPrimary}; font-weight: ${tokens_1.contract.weight.medium}; }
|
|
230
|
+
/* crisp/#601 supersession follow-up -- the same flex-row shape
|
|
231
|
+
.crisp-datatable-entity gives its own avatar+name+actions, but
|
|
232
|
+
structural only (no entity-specific colour/weight): lets
|
|
233
|
+
onOpenComments/onOpenPreview compose alongside ANY column's own
|
|
234
|
+
content (a custom render, or a non-entity built-in type), not just
|
|
235
|
+
type: "entity"'s own inline rendering. */
|
|
236
|
+
& .crisp-datatable-cell-actions-row { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; min-width: 0; width: 100%; }
|
|
230
237
|
/* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text,
|
|
231
238
|
plain hover vs rest) that Attio's underline is unconditional, not a
|
|
232
239
|
hover reveal -- identical at rest and on hover: text-decoration-color
|
|
@@ -268,6 +275,23 @@ const config = (0, core_1.defineRecipe)({
|
|
|
268
275
|
link. */
|
|
269
276
|
& .crisp-datatable-link { color: ${tokens_1.contract.fgPrimary}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
270
277
|
& .crisp-datatable-link:hover { text-decoration: underline; }
|
|
278
|
+
/* rating cell type/editor (#760) -- measured live: 16px stars on an
|
|
279
|
+
18px pitch (a 2px gap), an empty star at rgba(0,0,0,0.1), a
|
|
280
|
+
committed/filled star at brand600 (--crisp-bg-brand-solid, an exact
|
|
281
|
+
match to the measured #266df0), and a distinct hover-preview colour
|
|
282
|
+
-- rgb(33,91,196) -- that's close to but NOT brand700/bgBrandSolidPressed
|
|
283
|
+
(#1f5fd6); kept as its own literal rather than forced onto that token,
|
|
284
|
+
since the two were measured stable and different. The read-only svg
|
|
285
|
+
and the editable button's own inner svg both carry the
|
|
286
|
+
crisp-datatable-rating-star class on the svg itself (not the
|
|
287
|
+
button), so one set of fill rules covers both -- the button only
|
|
288
|
+
adds its own reset + cursor via a separate -btn class. */
|
|
289
|
+
& .crisp-datatable-rating { display: inline-flex; align-items: center; gap: 2px; }
|
|
290
|
+
& .crisp-datatable-rating-star { display: block; }
|
|
291
|
+
& .crisp-datatable-rating-star path { fill: rgba(0, 0, 0, 0.1); }
|
|
292
|
+
& .crisp-datatable-rating-star[data-filled] path { fill: ${tokens_1.contract.bgBrandSolid}; }
|
|
293
|
+
& .crisp-datatable-rating-star[data-hovering] path { fill: #215bc4; }
|
|
294
|
+
& .crisp-datatable-rating-star-btn { display: inline-flex; padding: 0; margin: 0; border: none; background: none; font-family: inherit; cursor: pointer; }
|
|
271
295
|
/* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
|
|
272
296
|
a leading 20px grip column, a 16px grip icon, and the row's own
|
|
273
297
|
hairline doubling as the drop indicator (it tints toward the brand
|
|
@@ -163,6 +163,15 @@ function ResizablePanel({ side = "left", size, defaultSize = 280, onSizeChange,
|
|
|
163
163
|
const finalWidth = rect ? Math.round(rect.width) : width;
|
|
164
164
|
commitSize(finalWidth);
|
|
165
165
|
}
|
|
166
|
+
// crisp/#795: double-click resets to `defaultSize`, confirmed live on
|
|
167
|
+
// Attio's own record-page divider (dragged 722px -> 924px, double-click
|
|
168
|
+
// snapped it back to exactly 722px -- its own default/initial width, not
|
|
169
|
+
// some other fixed value). Reuses `commitSize`, the same commit path a
|
|
170
|
+
// drag already uses, so persistence (`storageKey`) picks up the reset the
|
|
171
|
+
// same way it picks up a drag -- no separate reset-specific write.
|
|
172
|
+
function onHandleDoubleClick() {
|
|
173
|
+
commitSize(defaultSize);
|
|
174
|
+
}
|
|
166
175
|
const maxStyle = max == null ? undefined : typeof max === "number" ? `${max}px` : max;
|
|
167
176
|
const panelSizeStyle = {
|
|
168
177
|
width,
|
|
@@ -171,6 +180,6 @@ function ResizablePanel({ side = "left", size, defaultSize = 280, onSizeChange,
|
|
|
171
180
|
};
|
|
172
181
|
return ((0, jsx_runtime_1.jsxs)("div", { ref: containerRef, className: (0, resizablepanel_recipe_1.resizablepanelRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-resizablepanel-spacer", style: floating ? { width: 0 } : { width, minWidth: min, maxWidth: maxStyle } }), (0, jsx_runtime_1.jsxs)("div", { ref: panelRef, className: "crisp-resizablepanel-panel", "data-side": side, "data-floating": floating ? "true" : "false", style: panelSizeStyle, children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-resizablepanel-inner", children: children }), (0, jsx_runtime_1.jsx)("div", { className: handleVariant === "column"
|
|
173
182
|
? "crisp-resizablepanel-handle crisp-resizablepanel-handle--column"
|
|
174
|
-
: "crisp-resizablepanel-handle", "data-side": side, "data-dragging": dragging ? "true" : undefined, "data-handle": "", onPointerDown: onHandlePointerDown, onPointerMove: onHandlePointerMove, onPointerUp: onHandlePointerUp, children: handleVariant === "column" && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-resizablepanel-handle-inner", "aria-hidden": "true" })) })] })] }));
|
|
183
|
+
: "crisp-resizablepanel-handle", "data-side": side, "data-dragging": dragging ? "true" : undefined, "data-handle": "", onPointerDown: onHandlePointerDown, onPointerMove: onHandlePointerMove, onPointerUp: onHandlePointerUp, onDoubleClick: onHandleDoubleClick, children: handleVariant === "column" && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-resizablepanel-handle-inner", "aria-hidden": "true" })) })] })] }));
|
|
175
184
|
}
|
|
176
185
|
ResizablePanel.displayName = "ResizablePanel";
|
|
@@ -64,7 +64,14 @@ exports.TableRow = React.forwardRef((props, ref) => (0, jsx_runtime_1.jsx)("tr",
|
|
|
64
64
|
exports.TableRow.displayName = "TableRow";
|
|
65
65
|
exports.TableHead = React.forwardRef(({ align, ...props }, ref) => (0, jsx_runtime_1.jsx)("th", { ref: ref, "data-align": align, ...props }));
|
|
66
66
|
exports.TableHead.displayName = "TableHead";
|
|
67
|
-
exports.TableCell = React.forwardRef(({ align, ...props }, ref) =>
|
|
67
|
+
exports.TableCell = React.forwardRef(({ align, as, ...props }, ref) => {
|
|
68
|
+
// Cast to "td", same as RecordsTable's own dynamic-tag precedent --
|
|
69
|
+
// `props` (which may carry `scope`) would otherwise need to satisfy
|
|
70
|
+
// both <td> and <th>'s JSX attribute types at once for a bare union
|
|
71
|
+
// element name.
|
|
72
|
+
const Cell = (as ?? "td");
|
|
73
|
+
return (0, jsx_runtime_1.jsx)(Cell, { ref: ref, "data-align": align, ...props });
|
|
74
|
+
});
|
|
68
75
|
exports.TableCell.displayName = "TableCell";
|
|
69
76
|
exports.TableCaption = React.forwardRef((props, ref) => (0, jsx_runtime_1.jsx)("caption", { ref: ref, ...props }));
|
|
70
77
|
exports.TableCaption.displayName = "TableCaption";
|
|
@@ -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; }
|