@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ArrowDown, ArrowUp, Check, ChevronsUpDown, CircleUserRound, ClipboardPaste, Copy, GripVertical, History, Lock, PanelRight, Plus, Search, SlidersHorizontal, X, } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { cellDatePrecision, formatCellDate, isRtlElement, parseCellDate, useCrispMessage, useCrispMessages, } from "../../core/index.js";
|
|
4
|
+
import { cellDatePrecision, formatCellDate, isRtlElement, parseCellDate, useCrispMessage, useCrispMessages, usePrefersReducedMotion, } from "../../core/index.js";
|
|
5
5
|
// crisp/#601 supersession: a deliberate exception to "DS atoms stay
|
|
6
6
|
// generic; product components compose them" (AUTHORING.md) -- the row's
|
|
7
7
|
// own comment affordance is `CommentCountBadge` (product/comments, #625),
|
|
@@ -15,6 +15,7 @@ import { Checkbox } from "../checkbox/index.js";
|
|
|
15
15
|
import { ColumnCalc } from "../columncalc/index.js";
|
|
16
16
|
import { ContextMenu } from "../contextmenu/index.js";
|
|
17
17
|
import { Menu, MenuItem } from "../menu/index.js";
|
|
18
|
+
import { useRadioGroupKeyboard } from "../radiogroup/index.js";
|
|
18
19
|
import { Select } from "../select/index.js";
|
|
19
20
|
import { Spinner } from "../spinner/index.js";
|
|
20
21
|
import { Switch } from "../switch/index.js";
|
|
@@ -254,6 +255,73 @@ function formatCell(type, v, symbol, locale = "en-US") {
|
|
|
254
255
|
}
|
|
255
256
|
return asText(v);
|
|
256
257
|
}
|
|
258
|
+
// crisp/#760 -- the exact 5-point star Attio's own Rating attribute draws
|
|
259
|
+
// (path traced from the live widget's own SVG, `app.attio.com`, both the
|
|
260
|
+
// attribute-creation default-value picker and the grid cell render the
|
|
261
|
+
// identical path). Kept as one shared `d` string rather than a lucide icon
|
|
262
|
+
// so the star's silhouette matches Attio verbatim rather than an
|
|
263
|
+
// approximation, and so `RatingStars` below can flip its own `fill`
|
|
264
|
+
// per-star instead of drawing a whole new icon per state.
|
|
265
|
+
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";
|
|
266
|
+
/** A star row for `type: "rating"` / `editor: "rating"` (crisp/#760).
|
|
267
|
+
* Read-only (a plain `<span>` row, no interaction) unless `onChange` is
|
|
268
|
+
* given, which renders Attio's own measured accessible shape: a
|
|
269
|
+
* `role="radiogroup"` of `role="radio"` buttons, one per star. Clicking a
|
|
270
|
+
* star sets the rating to its 1-based index; clicking the star that
|
|
271
|
+
* already equals the current value clears it to 0 -- confirmed live
|
|
272
|
+
* (rating a record 5, then clicking the 5th star again cleared it
|
|
273
|
+
* entirely rather than re-committing 5). */
|
|
274
|
+
function RatingStars({ value, max = 5, onChange, "aria-label": ariaLabel, }) {
|
|
275
|
+
const [hoverIdx, setHoverIdx] = React.useState(null);
|
|
276
|
+
// The house radiogroup keyboard model (crisp's own spec check requires it
|
|
277
|
+
// on every role="radiogroup", regardless of Attio parity): arrows move
|
|
278
|
+
// focus AND select, Home/End jump to the ends. Landing on a star always
|
|
279
|
+
// SETS the rating to that star's position -- arrow nav has no "clear on
|
|
280
|
+
// re-land" toggle, that's a click-only gesture (see onClick below).
|
|
281
|
+
const { onKeyDown, groupRef } = useRadioGroupKeyboard({
|
|
282
|
+
select: (v) => onChange?.(Number(v)),
|
|
283
|
+
});
|
|
284
|
+
const stars = Array.from({ length: max }, (_, i) => i);
|
|
285
|
+
const content = stars.map((i) => {
|
|
286
|
+
// Hovering star i previews filling every star up to and including it,
|
|
287
|
+
// whether or not it's already part of the committed value -- measured:
|
|
288
|
+
// hovering the 5th star while 4 were already filled turned all 5 the
|
|
289
|
+
// hover colour, not just the would-be-new one.
|
|
290
|
+
const hovering = onChange != null && hoverIdx != null && i <= hoverIdx;
|
|
291
|
+
const filled = hovering ? true : i < value;
|
|
292
|
+
// A native <input type="radio"> can't be unchecked by re-clicking the
|
|
293
|
+
// already-selected one, which is exactly the measured behaviour
|
|
294
|
+
// (clicking the star that already equals the rating clears it to 0) --
|
|
295
|
+
// Attio's own live widget is a <button role="radio"> for the same
|
|
296
|
+
// reason.
|
|
297
|
+
return onChange ? (_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) => {
|
|
298
|
+
e.stopPropagation();
|
|
299
|
+
onChange(i + 1 === value ? 0 : i + 1);
|
|
300
|
+
}, children: _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: _jsx("path", { d: RATING_STAR_D }) }) }, i)) : (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", className: "crisp-datatable-rating-star", "data-filled": filled ? "" : undefined, children: _jsx("path", { d: RATING_STAR_D }) }, i));
|
|
301
|
+
});
|
|
302
|
+
return onChange ? (_jsx("span", { role: "radiogroup", "aria-label": ariaLabel, className: "crisp-datatable-rating", ref: groupRef, onKeyDown: onKeyDown, children: content })) : (_jsx("span", { className: "crisp-datatable-rating", children: content }));
|
|
303
|
+
}
|
|
304
|
+
/** The preview/comments action icons a column's `onOpenPreview`/
|
|
305
|
+
* `onOpenComments`/`commentCount` produce — the exact markup `type: "entity"`
|
|
306
|
+
* already renders inline, extracted so a column that ISN'T `"entity"` (a
|
|
307
|
+
* custom `render`, or any other built-in type) can compose the same actions
|
|
308
|
+
* alongside its own content instead of the props silently doing nothing
|
|
309
|
+
* (crisp/#601 supersession follow-up: these three props used to only have
|
|
310
|
+
* an effect inside `renderTyped`'s `"entity"` case). `null` when neither
|
|
311
|
+
* prop is set, so a caller can render nothing rather than an empty
|
|
312
|
+
* flex wrapper. */
|
|
313
|
+
function renderCellActions(col, row, rowHovered, openPreviewLabel) {
|
|
314
|
+
if (col.onOpenComments) {
|
|
315
|
+
return (_jsx("span", { className: "crisp-datatable-entity-actions", children: _jsx(CommentCountBadge, { count: col.commentCount?.(row) ?? 0, hovered: rowHovered, onOpen: () => col.onOpenComments?.(row), onOpenPreview: col.onOpenPreview ? () => col.onOpenPreview?.(row) : undefined, previewLabel: openPreviewLabel }) }));
|
|
316
|
+
}
|
|
317
|
+
if (col.onOpenPreview) {
|
|
318
|
+
return (_jsx("span", { className: "crisp-datatable-entity-actions", children: _jsx("button", { type: "button", className: "crisp-datatable-entity-action", "aria-label": openPreviewLabel, onClick: (ev) => {
|
|
319
|
+
ev.stopPropagation();
|
|
320
|
+
col.onOpenPreview?.(row);
|
|
321
|
+
}, children: _jsx(PanelRight, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) }));
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
257
325
|
/** Built-in typed-cell renderer (used when a column has no custom `render`). */
|
|
258
326
|
// `value` is always the RAW cell value and `display` the caller's `format`
|
|
259
327
|
// output, if any. They are separate arguments on purpose: the entity and tags
|
|
@@ -281,10 +349,7 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US", ope
|
|
|
281
349
|
// at a DIFFERENT record than the row itself (#501).
|
|
282
350
|
ev.stopPropagation();
|
|
283
351
|
col.onEntityClick?.(row);
|
|
284
|
-
}, children: e.name })) : (_jsx("span", { className: "crisp-datatable-entity-name", children: e.name })), col
|
|
285
|
-
ev.stopPropagation();
|
|
286
|
-
col.onOpenPreview?.(row);
|
|
287
|
-
}, children: _jsx(PanelRight, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) })))] }));
|
|
352
|
+
}, children: e.name })) : (_jsx("span", { className: "crisp-datatable-entity-name", children: e.name })), renderCellActions(col, row, rowHovered, openPreviewLabel)] }));
|
|
288
353
|
}
|
|
289
354
|
case "tags": {
|
|
290
355
|
// asking for a format on a tag column means "render it your way" -- show
|
|
@@ -304,6 +369,14 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US", ope
|
|
|
304
369
|
// translation) can never silently repaint the chip
|
|
305
370
|
return (_jsx("span", { className: "crisp-datatable-tag", "data-tone": col.tones?.[s] ?? toneFor(s), children: display || s }));
|
|
306
371
|
}
|
|
372
|
+
case "rating": {
|
|
373
|
+
// Read-only display -- `editor: "rating"` (the always-live picker) is
|
|
374
|
+
// handled separately, the same split `switch` gets (#453).
|
|
375
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
376
|
+
if (!n || Number.isNaN(n))
|
|
377
|
+
return empty;
|
|
378
|
+
return _jsx(RatingStars, { value: n, max: col.maxRating ?? 5 });
|
|
379
|
+
}
|
|
307
380
|
case "url":
|
|
308
381
|
case "email": {
|
|
309
382
|
const text = display || asText(value);
|
|
@@ -960,14 +1033,48 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
960
1033
|
const colAriaIndex = (i) => ariaColIndexBase + i + 1;
|
|
961
1034
|
const allSelected = rows.length > 0 && rows.every((r) => selected.has(r.id));
|
|
962
1035
|
const someSelected = selected.size > 0 && !allSelected;
|
|
1036
|
+
// crisp/#782, ported from RecordsTable's own already-shipped logic: the
|
|
1037
|
+
// header checkbox speaks for the rows you can SEE. Rebuilding the whole
|
|
1038
|
+
// set from `rows` (the filtered/visible set) would silently drop any
|
|
1039
|
+
// selection an active filter is hiding -- add/remove only the visible
|
|
1040
|
+
// rows and leave the rest of `selected` alone.
|
|
963
1041
|
function toggleAll() {
|
|
964
|
-
|
|
1042
|
+
const next = new Set(selected);
|
|
1043
|
+
for (const r of rows) {
|
|
1044
|
+
if (allSelected)
|
|
1045
|
+
next.delete(r.id);
|
|
1046
|
+
else
|
|
1047
|
+
next.add(r.id);
|
|
1048
|
+
}
|
|
1049
|
+
setSelected(next);
|
|
965
1050
|
}
|
|
966
1051
|
function toggleRow(id) {
|
|
967
1052
|
const next = new Set(selected);
|
|
968
1053
|
next.has(id) ? next.delete(id) : next.add(id);
|
|
969
1054
|
setSelected(next);
|
|
970
1055
|
}
|
|
1056
|
+
// crisp/#781: Shift+Click on a row checkbox selects the whole contiguous
|
|
1057
|
+
// range from the last plain click (the anchor) to this row -- confirmed
|
|
1058
|
+
// live, the mouse counterpart to #749's Shift+Arrow keyboard gesture,
|
|
1059
|
+
// sharing the same `rowSelectAnchor` state and the same recompute-the-
|
|
1060
|
+
// whole-span shape (not accumulate) so shift-clicking back toward the
|
|
1061
|
+
// anchor shrinks the same way clicking away from it grows. A plain click
|
|
1062
|
+
// both toggles normally AND re-anchors, so the NEXT Shift+Click extends
|
|
1063
|
+
// from wherever the user last plain-clicked, matching Attio.
|
|
1064
|
+
function toggleRowOrExtend(id, shiftKey) {
|
|
1065
|
+
if (shiftKey && rowSelectAnchor != null) {
|
|
1066
|
+
const anchorIdx = rows.findIndex((r) => r.id === rowSelectAnchor);
|
|
1067
|
+
const idx = rows.findIndex((r) => r.id === id);
|
|
1068
|
+
if (anchorIdx >= 0 && idx >= 0) {
|
|
1069
|
+
const lo = Math.min(anchorIdx, idx);
|
|
1070
|
+
const hi = Math.max(anchorIdx, idx);
|
|
1071
|
+
setSelected(new Set(rows.slice(lo, hi + 1).map((r) => r.id)));
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
setRowSelectAnchor(id);
|
|
1076
|
+
toggleRow(id);
|
|
1077
|
+
}
|
|
971
1078
|
function cycleSort(key) {
|
|
972
1079
|
// Clicking a header drives a single sort, as before. Multi-sort is supplied
|
|
973
1080
|
// by the caller through the `sort` prop (built in a sort popover).
|
|
@@ -995,8 +1102,9 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
995
1102
|
* `editor: "select"`/`"reference"` open their own picker instead of this
|
|
996
1103
|
* native-input family, but through the SAME double-click gate (#453) --
|
|
997
1104
|
* see the row-render loop's `isEditing` branch for those, this function
|
|
998
|
-
* only covers the native-input-backed types.
|
|
999
|
-
*
|
|
1105
|
+
* only covers the native-input-backed types.
|
|
1106
|
+
* `editor: "switch"`/`"rating"`/`"checkbox"` are the editors with no
|
|
1107
|
+
* double-click gate at all (always live).
|
|
1000
1108
|
*/
|
|
1001
1109
|
function editorFor(col) {
|
|
1002
1110
|
switch (col.type) {
|
|
@@ -1173,6 +1281,28 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1173
1281
|
setSelected(new Set(rows.map((rw) => rw.id)));
|
|
1174
1282
|
return;
|
|
1175
1283
|
}
|
|
1284
|
+
// crisp/#772: Cmd/Ctrl+ArrowUp jumps the selected cell to the first row,
|
|
1285
|
+
// Cmd/Ctrl+ArrowDown to the last -- confirmed live (Attio's own help
|
|
1286
|
+
// drawer documents "Jump to top"/"Jump to bottom" on these exact
|
|
1287
|
+
// bindings). `!e.shiftKey` guards this from stealing Cmd+Shift+Arrow,
|
|
1288
|
+
// a separate gesture (#773, extend-selection-to-first/last) that falls
|
|
1289
|
+
// through to the Shift+Arrow branch below untouched. Placed before that
|
|
1290
|
+
// branch for the same reason: without the guard, Cmd+Shift+Arrow would
|
|
1291
|
+
// still reach it (that branch doesn't check metaKey/ctrlKey either),
|
|
1292
|
+
// just not do the "jump" part -- the guard here is what keeps the two
|
|
1293
|
+
// gestures from fighting over the same keys.
|
|
1294
|
+
if ((e.metaKey || e.ctrlKey) &&
|
|
1295
|
+
!e.shiftKey &&
|
|
1296
|
+
(e.key === "ArrowUp" || e.key === "ArrowDown")) {
|
|
1297
|
+
const r0 = rows.findIndex((row) => row.id === selCell.id);
|
|
1298
|
+
if (r0 < 0)
|
|
1299
|
+
return;
|
|
1300
|
+
e.preventDefault();
|
|
1301
|
+
const nr0 = e.key === "ArrowUp" ? 0 : rows.length - 1;
|
|
1302
|
+
setSelCell({ id: rows[nr0].id, key: selCell.key });
|
|
1303
|
+
clipboardProxyRef.current?.select();
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1176
1306
|
// crisp/#749: Shift+ArrowUp/Down extends the row-checkbox selection --
|
|
1177
1307
|
// confirmed live, distinct from Shift+Click's cell-range gesture above
|
|
1178
1308
|
// (that one is `selAnchor`; this is `rowSelectAnchor`, and the two
|
|
@@ -1249,8 +1379,13 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1249
1379
|
// already its own live widget (#385); Enter there is a no-op, same
|
|
1250
1380
|
// as any other non-editable cell that isn't wired to onRowClick.
|
|
1251
1381
|
// `select`/`reference` DO have one now (like every other editor),
|
|
1252
|
-
// so Enter opens them too.
|
|
1253
|
-
|
|
1382
|
+
// so Enter opens them too. `rating` (#760) and `checkbox` (#761)
|
|
1383
|
+
// join `switch` in the no-separate-editing-mode exception -- same
|
|
1384
|
+
// always-live shape.
|
|
1385
|
+
if (columns[c]?.editable &&
|
|
1386
|
+
columns[c]?.editor !== "switch" &&
|
|
1387
|
+
columns[c]?.editor !== "rating" &&
|
|
1388
|
+
columns[c]?.editor !== "checkbox") {
|
|
1254
1389
|
e.preventDefault();
|
|
1255
1390
|
openEditing(selCell.id, selCell.key);
|
|
1256
1391
|
}
|
|
@@ -1319,15 +1454,21 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1319
1454
|
}
|
|
1320
1455
|
setEditing(null);
|
|
1321
1456
|
}
|
|
1457
|
+
const reduceMotion = usePrefersReducedMotion();
|
|
1322
1458
|
// crisp/#577 -- keyboard nav to an off-screen (virtualized-out) cell
|
|
1323
|
-
// scrolls it into the rendered window, on both axes.
|
|
1324
|
-
//
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1327
|
-
//
|
|
1328
|
-
//
|
|
1329
|
-
//
|
|
1330
|
-
//
|
|
1459
|
+
// scrolls it into the rendered window, on both axes. `renderItems`/
|
|
1460
|
+
// `rowOffsets` are computed unconditionally (`height == null` just makes
|
|
1461
|
+
// them a no-op elsewhere, see the comment above `renderItems`), so this
|
|
1462
|
+
// runs on both axes regardless of `height` too (crisp/#780 -- it used to
|
|
1463
|
+
// gate the vertical half on `height != null`, on the reasoning that only
|
|
1464
|
+
// `height` gives the grid an internal scroll position to move; that's
|
|
1465
|
+
// wrong when the grid's own container still scrolls without an explicit
|
|
1466
|
+
// height, e.g. inside a shorter ancestor, matching `RecordsTable`'s own
|
|
1467
|
+
// already-shipped #257 fix, which had no such gate). `widths` is read only
|
|
1468
|
+
// indirectly, through `colWidth`'s closure over it -- biome can't see
|
|
1469
|
+
// that, but a column resize while a cell is selected genuinely needs to
|
|
1470
|
+
// re-run this to keep it in view, so the dependency is real even though
|
|
1471
|
+
// the lint rule can't prove it.
|
|
1331
1472
|
// biome-ignore lint/correctness/useExhaustiveDependencies: widths is read via colWidth's closure, see comment above
|
|
1332
1473
|
React.useEffect(() => {
|
|
1333
1474
|
if (!selCell)
|
|
@@ -1335,28 +1476,37 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1335
1476
|
const el = scrollRef.current;
|
|
1336
1477
|
if (!el)
|
|
1337
1478
|
return;
|
|
1338
|
-
//
|
|
1339
|
-
//
|
|
1340
|
-
//
|
|
1341
|
-
//
|
|
1342
|
-
//
|
|
1479
|
+
// `el.scrollTo` rather than a direct `el.scrollTop =`/`el.scrollLeft =`
|
|
1480
|
+
// assignment (crisp/#780, matching RecordsTable's own #208/#257 rule):
|
|
1481
|
+
// property assignment is ALWAYS instant, so it can never honour a
|
|
1482
|
+
// consumer's own `scroll-behavior: smooth` on this container; `scrollTo`
|
|
1483
|
+
// defers to that CSS when no `behavior` is given, and only forces
|
|
1484
|
+
// `"auto"` under `prefers-reduced-motion`, which is the one case that
|
|
1485
|
+
// must win even over a host page's own smooth-scroll opt-in.
|
|
1486
|
+
const scrollTo = (opts) => el.scrollTo({
|
|
1487
|
+
...opts,
|
|
1488
|
+
...(reduceMotion ? { behavior: "auto" } : null),
|
|
1489
|
+
});
|
|
1490
|
+
// Sets `viewport` directly from the SAME value just passed to
|
|
1491
|
+
// `scrollTo`, rather than waiting for a follow-up `scroll` event to
|
|
1492
|
+
// round-trip through the `onScroll` handler above -- a real browser
|
|
1493
|
+
// does fire one when the scroll position changes programmatically, but
|
|
1494
|
+
// relying on that would make this effect's own visible result (the
|
|
1343
1495
|
// newly-selected cell actually rendering) depend on a second render
|
|
1344
1496
|
// triggered from outside React's own update, which is both slower and
|
|
1345
1497
|
// one more thing that has to be true for this to work.
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
setViewport((v) => ({ ...v, top: newTop }));
|
|
1359
|
-
}
|
|
1498
|
+
const itemIndex = rowIdToItemIndex.get(selCell.id);
|
|
1499
|
+
if (itemIndex != null) {
|
|
1500
|
+
const itemTop = rowOffsets[itemIndex];
|
|
1501
|
+
const itemBottom = itemTop + renderItems[itemIndex].height;
|
|
1502
|
+
if (itemTop < el.scrollTop) {
|
|
1503
|
+
scrollTo({ top: itemTop });
|
|
1504
|
+
setViewport((v) => ({ ...v, top: itemTop }));
|
|
1505
|
+
}
|
|
1506
|
+
else if (itemBottom > el.scrollTop + el.clientHeight) {
|
|
1507
|
+
const newTop = itemBottom - el.clientHeight;
|
|
1508
|
+
scrollTo({ top: newTop });
|
|
1509
|
+
setViewport((v) => ({ ...v, top: newTop }));
|
|
1360
1510
|
}
|
|
1361
1511
|
}
|
|
1362
1512
|
const ci = columns.findIndex((c) => c.key === selCell.key);
|
|
@@ -1366,24 +1516,24 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1366
1516
|
left += colWidth(columns[i]);
|
|
1367
1517
|
const right = left + colWidth(columns[ci]);
|
|
1368
1518
|
if (left < el.scrollLeft) {
|
|
1369
|
-
|
|
1519
|
+
scrollTo({ left });
|
|
1370
1520
|
setViewport((v) => ({ ...v, left }));
|
|
1371
1521
|
}
|
|
1372
1522
|
else if (right > el.scrollLeft + el.clientWidth) {
|
|
1373
1523
|
const newLeft = right - el.clientWidth;
|
|
1374
|
-
|
|
1524
|
+
scrollTo({ left: newLeft });
|
|
1375
1525
|
setViewport((v) => ({ ...v, left: newLeft }));
|
|
1376
1526
|
}
|
|
1377
1527
|
}
|
|
1378
1528
|
}, [
|
|
1379
1529
|
selCell,
|
|
1380
|
-
height,
|
|
1381
1530
|
columns,
|
|
1382
1531
|
widths,
|
|
1383
1532
|
selectable,
|
|
1384
1533
|
rowIdToItemIndex,
|
|
1385
1534
|
rowOffsets,
|
|
1386
1535
|
renderItems,
|
|
1536
|
+
reduceMotion,
|
|
1387
1537
|
]);
|
|
1388
1538
|
const totalCols = columns.length + (selectable ? 1 : 0) + (showGrip ? 1 : 0);
|
|
1389
1539
|
const hasSummary = columns.some((c) => c.summary || c.calc);
|
|
@@ -1757,12 +1907,33 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1757
1907
|
}, onDragEnd: () => {
|
|
1758
1908
|
setDragRowId(null);
|
|
1759
1909
|
setDropRowId(null);
|
|
1760
|
-
}, children: _jsx(GripVertical, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) })), selectable && (_jsx(TableCell, { className: "crisp-datatable-col-check", "aria-colindex": showGrip ? 2 : 1, ...stickyCol(-1), onClick: (e) => e.stopPropagation(), children: _jsx("span", { className: "crisp-datatable-check", children: _jsx(Checkbox, { "aria-label": selectRowLabel(row), checked: selected.has(row.id),
|
|
1910
|
+
}, children: _jsx(GripVertical, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) })), selectable && (_jsx(TableCell, { className: "crisp-datatable-col-check", "aria-colindex": showGrip ? 2 : 1, ...stickyCol(-1), onClick: (e) => e.stopPropagation(), children: _jsx("span", { className: "crisp-datatable-check", children: _jsx(Checkbox, { "aria-label": selectRowLabel(row), checked: selected.has(row.id),
|
|
1911
|
+
// preventDefault stops the native check/change
|
|
1912
|
+
// this click would otherwise also fire -- this
|
|
1913
|
+
// handler is the single source of truth so a
|
|
1914
|
+
// Shift+Click's range-select and a plain
|
|
1915
|
+
// click's toggle-and-re-anchor never race each
|
|
1916
|
+
// other (crisp/#781).
|
|
1917
|
+
onClick: (e) => {
|
|
1918
|
+
e.preventDefault();
|
|
1919
|
+
toggleRowOrExtend(row.id, e.shiftKey);
|
|
1920
|
+
}, onChange: () => { } }) }) })), colSlots.map((slot) => {
|
|
1761
1921
|
if (slot.type === "spacer") {
|
|
1762
1922
|
return (_jsx("td", { role: "presentation", "aria-hidden": "true", style: { padding: 0, border: "none" } }, `__spacer_${slot.side}`));
|
|
1763
1923
|
}
|
|
1764
1924
|
const col = columns[slot.i];
|
|
1765
1925
|
const i = slot.i;
|
|
1926
|
+
// crisp/#779, matching RecordsTable's own #231 fix:
|
|
1927
|
+
// column 0 is the row's own identity (its name), not
|
|
1928
|
+
// a value it holds -- a screen reader crossing the
|
|
1929
|
+
// row should read "Google, Employees, 182,000", not a
|
|
1930
|
+
// bare "182,000". Spread onto every TableCell this
|
|
1931
|
+
// column can render as (every editor branch, not just
|
|
1932
|
+
// the default one), since the row-identity semantics
|
|
1933
|
+
// don't depend on which editor is open.
|
|
1934
|
+
const rowHeaderProps = i === 0
|
|
1935
|
+
? { as: "th", scope: "row" }
|
|
1936
|
+
: undefined;
|
|
1766
1937
|
const isEditing = editing?.id === row.id && editing?.key === col.key;
|
|
1767
1938
|
const value = cellValue(col, row);
|
|
1768
1939
|
const sticky = dataStickyCol(i);
|
|
@@ -1785,13 +1956,29 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1785
1956
|
// right). See the `isEditing` block below for their
|
|
1786
1957
|
// actual editor.
|
|
1787
1958
|
if (col.editable && col.editor === "switch") {
|
|
1788
|
-
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [_jsx(Switch, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commitEdit(row.id, col.key, String(checked)) }), isPending && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1959
|
+
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [_jsx(Switch, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commitEdit(row.id, col.key, String(checked)) }), isPending && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1960
|
+
}
|
|
1961
|
+
// crisp/#760: `editor: "rating"` is the same always-
|
|
1962
|
+
// live shape as `switch` above -- clicking a star IS
|
|
1963
|
+
// the commit gesture, confirmed live.
|
|
1964
|
+
if (col.editable && col.editor === "rating") {
|
|
1965
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
1966
|
+
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [_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 && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1967
|
+
}
|
|
1968
|
+
// crisp/#761: `editor: "checkbox"` is a THIRD always-
|
|
1969
|
+
// live boolean shape, distinct from `switch` --
|
|
1970
|
+
// confirmed live against Attio's own "Checkbox"
|
|
1971
|
+
// attribute type, which renders a plain square, not a
|
|
1972
|
+
// toggle. Reuses crisp's own `Checkbox` (already the
|
|
1973
|
+
// measured 16px/r6/brand-fill grid checkbox).
|
|
1974
|
+
if (col.editable && col.editor === "checkbox") {
|
|
1975
|
+
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [_jsx(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 && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1789
1976
|
}
|
|
1790
1977
|
if (isEditing &&
|
|
1791
1978
|
(col.editor === "select" ||
|
|
1792
1979
|
col.editor === "reference" ||
|
|
1793
1980
|
col.editor === "actor")) {
|
|
1794
|
-
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined,
|
|
1981
|
+
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined,
|
|
1795
1982
|
// Same as the plain text editor's own cell:
|
|
1796
1983
|
// opening/using the picker must not also fire
|
|
1797
1984
|
// the row's onRowClick. Escape is handled by
|
|
@@ -1805,7 +1992,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1805
1992
|
onClick: (e) => e.stopPropagation(), children: [_jsxs("span", { className: "crisp-datatable-edit-picker", children: [col.editor === "select" ? (_jsx(DataSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })) : (_jsx(DataReferenceEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commitEdit(row.id, col.key, v), onCancel: () => setEditing(null) })), isPending && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1806
1993
|
}
|
|
1807
1994
|
if (isEditing) {
|
|
1808
|
-
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, onClick: (e) => e.stopPropagation(), children: [_jsx("input", { className: "crisp-datatable-edit",
|
|
1995
|
+
return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, onClick: (e) => e.stopPropagation(), children: [_jsx("input", { className: "crisp-datatable-edit",
|
|
1809
1996
|
// biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
|
|
1810
1997
|
autoFocus: true, ...editorFor(col), "data-editor": editorFor(col).type, disabled: isPending, defaultValue: col.type === "date"
|
|
1811
1998
|
? toDateInputValue(value)
|
|
@@ -1842,13 +2029,29 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1842
2029
|
i >= range.c0 &&
|
|
1843
2030
|
i <= range.c1);
|
|
1844
2031
|
})();
|
|
1845
|
-
const
|
|
2032
|
+
const rowHoveredForCell = hoveredRowId === row.id;
|
|
2033
|
+
const baseRendered = col.render
|
|
1846
2034
|
? col.render(value, row)
|
|
1847
|
-
: renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale, openPreviewLabel,
|
|
2035
|
+
: renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale, openPreviewLabel, rowHoveredForCell);
|
|
2036
|
+
// crisp/#601 supersession follow-up: `onOpenComments`/
|
|
2037
|
+
// `onOpenPreview`/`commentCount` used to only do
|
|
2038
|
+
// anything inside `renderTyped`'s `"entity"` case,
|
|
2039
|
+
// which already renders its own actions inline (see
|
|
2040
|
+
// `renderCellActions` there) -- silently dropping them
|
|
2041
|
+
// on any OTHER type, and on a custom `render` of any
|
|
2042
|
+
// type, since neither path went through that switch.
|
|
2043
|
+
// This composes the same actions alongside whatever
|
|
2044
|
+
// `rendered` is instead, decoupling the props from
|
|
2045
|
+
// requiring `type: "entity"`.
|
|
2046
|
+
const cellActions = col.type !== "entity" &&
|
|
2047
|
+
(col.onOpenComments || col.onOpenPreview)
|
|
2048
|
+
? renderCellActions(col, row, rowHoveredForCell, openPreviewLabel)
|
|
2049
|
+
: null;
|
|
2050
|
+
const rendered = cellActions ? (_jsxs("span", { className: "crisp-datatable-cell-actions-row", children: [baseRendered, cellActions] })) : (baseRendered);
|
|
1848
2051
|
const isExpanded = expandedCell?.id === row.id &&
|
|
1849
2052
|
expandedCell?.key === col.key;
|
|
1850
2053
|
const cellContent = (_jsxs(_Fragment, { children: [!isExpanded && rendered, errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere })), isExpanded && (_jsx("div", { className: "crisp-datatable-cell-overlay", onClick: (e) => e.stopPropagation(), children: rendered }))] }));
|
|
1851
|
-
return (_jsx(ContextMenu, { asChild: true, variant: "dark", menuLabel: cellMenuLabel, items: cellMenuItems(row, col), children: _jsx(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: col.editable
|
|
2054
|
+
return (_jsx(ContextMenu, { asChild: true, variant: "dark", menuLabel: cellMenuLabel, items: cellMenuItems(row, col), children: _jsx(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, ...rowHeaderProps, className: col.editable
|
|
1852
2055
|
? "crisp-datatable-editable"
|
|
1853
2056
|
: undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
|
|
1854
2057
|
? ""
|
|
@@ -224,6 +224,13 @@ const config = defineRecipe({
|
|
|
224
224
|
& th[data-drop-after]::after { inset-inline-end: -1.5px; }
|
|
225
225
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
226
226
|
& .crisp-datatable-entity { display: flex; align-items: center; gap: ${c.space["1.5"]}; min-width: 0; color: ${c.fgPrimary}; font-weight: ${c.weight.medium}; }
|
|
227
|
+
/* crisp/#601 supersession follow-up -- the same flex-row shape
|
|
228
|
+
.crisp-datatable-entity gives its own avatar+name+actions, but
|
|
229
|
+
structural only (no entity-specific colour/weight): lets
|
|
230
|
+
onOpenComments/onOpenPreview compose alongside ANY column's own
|
|
231
|
+
content (a custom render, or a non-entity built-in type), not just
|
|
232
|
+
type: "entity"'s own inline rendering. */
|
|
233
|
+
& .crisp-datatable-cell-actions-row { display: flex; align-items: center; gap: ${c.space["1.5"]}; min-width: 0; width: 100%; }
|
|
227
234
|
/* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text,
|
|
228
235
|
plain hover vs rest) that Attio's underline is unconditional, not a
|
|
229
236
|
hover reveal -- identical at rest and on hover: text-decoration-color
|
|
@@ -265,6 +272,23 @@ const config = defineRecipe({
|
|
|
265
272
|
link. */
|
|
266
273
|
& .crisp-datatable-link { color: ${c.fgPrimary}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
267
274
|
& .crisp-datatable-link:hover { text-decoration: underline; }
|
|
275
|
+
/* rating cell type/editor (#760) -- measured live: 16px stars on an
|
|
276
|
+
18px pitch (a 2px gap), an empty star at rgba(0,0,0,0.1), a
|
|
277
|
+
committed/filled star at brand600 (--crisp-bg-brand-solid, an exact
|
|
278
|
+
match to the measured #266df0), and a distinct hover-preview colour
|
|
279
|
+
-- rgb(33,91,196) -- that's close to but NOT brand700/bgBrandSolidPressed
|
|
280
|
+
(#1f5fd6); kept as its own literal rather than forced onto that token,
|
|
281
|
+
since the two were measured stable and different. The read-only svg
|
|
282
|
+
and the editable button's own inner svg both carry the
|
|
283
|
+
crisp-datatable-rating-star class on the svg itself (not the
|
|
284
|
+
button), so one set of fill rules covers both -- the button only
|
|
285
|
+
adds its own reset + cursor via a separate -btn class. */
|
|
286
|
+
& .crisp-datatable-rating { display: inline-flex; align-items: center; gap: 2px; }
|
|
287
|
+
& .crisp-datatable-rating-star { display: block; }
|
|
288
|
+
& .crisp-datatable-rating-star path { fill: rgba(0, 0, 0, 0.1); }
|
|
289
|
+
& .crisp-datatable-rating-star[data-filled] path { fill: ${c.bgBrandSolid}; }
|
|
290
|
+
& .crisp-datatable-rating-star[data-hovering] path { fill: #215bc4; }
|
|
291
|
+
& .crisp-datatable-rating-star-btn { display: inline-flex; padding: 0; margin: 0; border: none; background: none; font-family: inherit; cursor: pointer; }
|
|
268
292
|
/* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
|
|
269
293
|
a leading 20px grip column, a 16px grip icon, and the row's own
|
|
270
294
|
hairline doubling as the drop indicator (it tints toward the brand
|
|
@@ -122,6 +122,15 @@ export function ResizablePanel({ side = "left", size, defaultSize = 280, onSizeC
|
|
|
122
122
|
const finalWidth = rect ? Math.round(rect.width) : width;
|
|
123
123
|
commitSize(finalWidth);
|
|
124
124
|
}
|
|
125
|
+
// crisp/#795: double-click resets to `defaultSize`, confirmed live on
|
|
126
|
+
// Attio's own record-page divider (dragged 722px -> 924px, double-click
|
|
127
|
+
// snapped it back to exactly 722px -- its own default/initial width, not
|
|
128
|
+
// some other fixed value). Reuses `commitSize`, the same commit path a
|
|
129
|
+
// drag already uses, so persistence (`storageKey`) picks up the reset the
|
|
130
|
+
// same way it picks up a drag -- no separate reset-specific write.
|
|
131
|
+
function onHandleDoubleClick() {
|
|
132
|
+
commitSize(defaultSize);
|
|
133
|
+
}
|
|
125
134
|
const maxStyle = max == null ? undefined : typeof max === "number" ? `${max}px` : max;
|
|
126
135
|
const panelSizeStyle = {
|
|
127
136
|
width,
|
|
@@ -130,6 +139,6 @@ export function ResizablePanel({ side = "left", size, defaultSize = 280, onSizeC
|
|
|
130
139
|
};
|
|
131
140
|
return (_jsxs("div", { ref: containerRef, className: resizablepanelRecipe({ className }), ...rest, children: [_jsx("div", { className: "crisp-resizablepanel-spacer", style: floating ? { width: 0 } : { width, minWidth: min, maxWidth: maxStyle } }), _jsxs("div", { ref: panelRef, className: "crisp-resizablepanel-panel", "data-side": side, "data-floating": floating ? "true" : "false", style: panelSizeStyle, children: [_jsx("div", { className: "crisp-resizablepanel-inner", children: children }), _jsx("div", { className: handleVariant === "column"
|
|
132
141
|
? "crisp-resizablepanel-handle crisp-resizablepanel-handle--column"
|
|
133
|
-
: "crisp-resizablepanel-handle", "data-side": side, "data-dragging": dragging ? "true" : undefined, "data-handle": "", onPointerDown: onHandlePointerDown, onPointerMove: onHandlePointerMove, onPointerUp: onHandlePointerUp, children: handleVariant === "column" && (_jsx("span", { className: "crisp-resizablepanel-handle-inner", "aria-hidden": "true" })) })] })] }));
|
|
142
|
+
: "crisp-resizablepanel-handle", "data-side": side, "data-dragging": dragging ? "true" : undefined, "data-handle": "", onPointerDown: onHandlePointerDown, onPointerMove: onHandlePointerMove, onPointerUp: onHandlePointerUp, onDoubleClick: onHandleDoubleClick, children: handleVariant === "column" && (_jsx("span", { className: "crisp-resizablepanel-handle-inner", "aria-hidden": "true" })) })] })] }));
|
|
134
143
|
}
|
|
135
144
|
ResizablePanel.displayName = "ResizablePanel";
|
|
@@ -28,7 +28,14 @@ export const TableRow = React.forwardRef((props, ref) => _jsx("tr", { ref: ref,
|
|
|
28
28
|
TableRow.displayName = "TableRow";
|
|
29
29
|
export const TableHead = React.forwardRef(({ align, ...props }, ref) => _jsx("th", { ref: ref, "data-align": align, ...props }));
|
|
30
30
|
TableHead.displayName = "TableHead";
|
|
31
|
-
export const TableCell = React.forwardRef(({ align, ...props }, ref) =>
|
|
31
|
+
export const TableCell = React.forwardRef(({ align, as, ...props }, ref) => {
|
|
32
|
+
// Cast to "td", same as RecordsTable's own dynamic-tag precedent --
|
|
33
|
+
// `props` (which may carry `scope`) would otherwise need to satisfy
|
|
34
|
+
// both <td> and <th>'s JSX attribute types at once for a bare union
|
|
35
|
+
// element name.
|
|
36
|
+
const Cell = (as ?? "td");
|
|
37
|
+
return _jsx(Cell, { ref: ref, "data-align": align, ...props });
|
|
38
|
+
});
|
|
32
39
|
TableCell.displayName = "TableCell";
|
|
33
40
|
export const TableCaption = React.forwardRef((props, ref) => _jsx("caption", { ref: ref, ...props }));
|
|
34
41
|
TableCaption.displayName = "TableCaption";
|
|
@@ -50,12 +50,35 @@ export const toolbarChromeHead = (p) => ` & .${CLASS_PREFIX}${p}-toolbar { di
|
|
|
50
50
|
& .${CLASS_PREFIX}${p}-search svg { flex-shrink: 0; }
|
|
51
51
|
& .${CLASS_PREFIX}${p}-search input { border: none; outline: none; background: transparent; font-family: inherit; font-size: ${c.text.md}; color: ${c.fgPrimary}; width: 180px; }
|
|
52
52
|
& .${CLASS_PREFIX}${p}-search input::placeholder { color: ${c.fgMuted}; }
|
|
53
|
-
& .${CLASS_PREFIX}${p}-tool { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.row}; margin: 0; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background:
|
|
53
|
+
& .${CLASS_PREFIX}${p}-tool { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.row}; margin: 0; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.borderDivider}; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
|
|
54
54
|
& .${CLASS_PREFIX}${p}-tool:hover { background: ${c.bgRaised}; }
|
|
55
55
|
& .${CLASS_PREFIX}${p}-tool svg { flex-shrink: 0; color: ${c.fgMuted}; }
|
|
56
|
-
|
|
56
|
+
/* crisp/#772: re-measured live on Sort with an actual sort applied --
|
|
57
|
+
an active trigger's own fill is white (bgSurface), the same as the
|
|
58
|
+
base .tool rule above now already sets; this override existed to
|
|
59
|
+
distinguish active from a base state that used to be transparent.
|
|
60
|
+
Kept explicit (not just deleted) so a future change to the base
|
|
61
|
+
rule's background can't silently change this one too. */
|
|
62
|
+
& .${CLASS_PREFIX}${p}-tool--active { background: ${c.bgSurface}; }
|
|
57
63
|
& .${CLASS_PREFIX}${p}-tool--ghost { box-shadow: none; background: color-mix(in srgb, ${c.fgPrimary} 3%, transparent); color: ${c.fgMuted}; }
|
|
58
64
|
& .${CLASS_PREFIX}${p}-tool--ghost:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); }
|
|
65
|
+
/* crisp/#772 follow-up, re-measured live 2026-09-25: an EMPTY Sort/Filter
|
|
66
|
+
trigger (no sort/condition set) isn't a solid-ring chip at all -- it's
|
|
67
|
+
a dashed ::before (1px dashed, border-radius: inherit, absolutely
|
|
68
|
+
positioned inset: 0) over a barely-there fill, confirmed on Attio's
|
|
69
|
+
own empty Filter chip (its ::before border computes to exactly
|
|
70
|
+
"1px dashed rgba(0, 0, 0, 0.06)"). Once a value IS set, the trigger
|
|
71
|
+
drops the dashed pseudo and picks up the base tool's own solid ring
|
|
72
|
+
instead (the --active modifier, unchanged) -- confirmed on Sort with
|
|
73
|
+
an actual sort applied. color-mix (not the raw rgba Attio computes
|
|
74
|
+
to) so both the border and the fill flip correctly in dark mode, the
|
|
75
|
+
same reasoning --ghost above already uses. Only wired into
|
|
76
|
+
TableToolbar itself (tabletoolbar.tsx) this pass -- RecordsTable's
|
|
77
|
+
own Sort/Filter triggers share this same chrome function but are
|
|
78
|
+
deprecated, not touched here. */
|
|
79
|
+
& .${CLASS_PREFIX}${p}-tool--dashed { position: relative; box-shadow: none; background: color-mix(in srgb, ${c.fgPrimary} 2%, transparent); }
|
|
80
|
+
& .${CLASS_PREFIX}${p}-tool--dashed::before { content: ""; position: absolute; inset: 0; border-radius: inherit; border: 1px dashed color-mix(in srgb, ${c.fgPrimary} 6%, transparent); pointer-events: none; }
|
|
81
|
+
& .${CLASS_PREFIX}${p}-tool--dashed:hover { background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); }
|
|
59
82
|
& .${CLASS_PREFIX}${p}-toolgroup { position: relative; display: inline-flex; }
|
|
60
83
|
& .${CLASS_PREFIX}${p}-divider { flex-shrink: 0; width: 1px; height: 20px; background: ${c.borderSubtle}; }
|
|
61
84
|
& .${CLASS_PREFIX}${p}-menu { position: absolute; top: calc(100% + ${c.space["1"]}); left: 0; z-index: 30; min-width: 232px; padding: ${c.space["1"]}; border-radius: ${c.radius.xl}; background: ${c.bgPopover}; box-shadow: ${e.floating}; display: flex; flex-direction: column; gap: ${c.space.px}; transform-origin: top; opacity: 1; transform: none; transition: opacity .13s ease, transform .13s ease; }
|