@crisp-ui-kit/crisp 0.54.10 → 0.54.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/datatable/datatable.js +121 -10
- package/dist/cjs/components/datatable/datatable.recipe.js +24 -0
- package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +93 -43
- package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +28 -9
- package/dist/cjs/product/recordpage/column.js +19 -0
- package/dist/cjs/product/recordpage/recordpage.js +5 -4
- package/dist/cjs/product/recordpage/test-fixtures.js +55 -0
- package/dist/cjs/product/recordstable/index.js +1 -1
- package/dist/cjs/product/recordstable/recordstable.js +5 -0
- package/dist/cjs/product/savedviews/savedviews.recipe.js +13 -11
- package/dist/cjs/product/taskstable/taskstable.js +5 -0
- package/dist/datatable.css +14 -0
- package/dist/datatable.layered.css +14 -0
- package/dist/esm/components/datatable/datatable.js +121 -10
- package/dist/esm/components/datatable/datatable.recipe.js +24 -0
- package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +93 -43
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +28 -9
- package/dist/esm/product/recordpage/column.js +16 -0
- package/dist/esm/product/recordpage/recordpage.js +2 -1
- package/dist/esm/product/recordpage/test-fixtures.js +52 -0
- package/dist/esm/product/recordstable/index.js +1 -1
- package/dist/esm/product/recordstable/recordstable.js +5 -0
- package/dist/esm/product/savedviews/savedviews.recipe.js +13 -11
- package/dist/esm/product/taskstable/taskstable.js +5 -0
- package/dist/recordstable.css +19 -2
- package/dist/recordstable.layered.css +19 -2
- package/dist/savedviews.css +6 -18
- package/dist/savedviews.layered.css +6 -18
- package/dist/styles.css +62 -23
- package/dist/styles.layered.css +62 -23
- 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/tabletoolbar/tabletoolbar.d.ts +26 -1
- package/dist/types/product/recordpage/column.d.ts +112 -0
- package/dist/types/product/recordpage/recordpage.d.ts +1 -1
- package/dist/types/product/recordpage/test-fixtures.d.ts +3 -0
- package/dist/types/product/recordpanel/recordpanel.d.ts +1 -1
- package/dist/types/product/recordstable/index.d.ts +1 -1
- package/dist/types/product/recordstable/recordstable.d.ts +6 -1
- package/dist/types/product/taskstable/taskstable.d.ts +5 -0
- package/package.json +1 -1
- /package/dist/cjs/product/{recordstable → recordpage}/editable-field.js +0 -0
- /package/dist/esm/product/{recordstable → recordpage}/editable-field.js +0 -0
- /package/dist/types/product/{recordstable → recordpage}/editable-field.d.ts +0 -0
|
@@ -53,6 +53,7 @@ const checkbox_1 = require("../checkbox");
|
|
|
53
53
|
const columncalc_1 = require("../columncalc");
|
|
54
54
|
const contextmenu_1 = require("../contextmenu");
|
|
55
55
|
const menu_1 = require("../menu");
|
|
56
|
+
const radiogroup_1 = require("../radiogroup");
|
|
56
57
|
const select_1 = require("../select");
|
|
57
58
|
const spinner_1 = require("../spinner");
|
|
58
59
|
const switch_1 = require("../switch");
|
|
@@ -292,6 +293,73 @@ function formatCell(type, v, symbol, locale = "en-US") {
|
|
|
292
293
|
}
|
|
293
294
|
return asText(v);
|
|
294
295
|
}
|
|
296
|
+
// crisp/#760 -- the exact 5-point star Attio's own Rating attribute draws
|
|
297
|
+
// (path traced from the live widget's own SVG, `app.attio.com`, both the
|
|
298
|
+
// attribute-creation default-value picker and the grid cell render the
|
|
299
|
+
// identical path). Kept as one shared `d` string rather than a lucide icon
|
|
300
|
+
// so the star's silhouette matches Attio verbatim rather than an
|
|
301
|
+
// approximation, and so `RatingStars` below can flip its own `fill`
|
|
302
|
+
// per-star instead of drawing a whole new icon per state.
|
|
303
|
+
const RATING_STAR_D = "M7.32203 2.81085C7.65793 1.97438 8.84207 1.97438 9.17797 2.81085L10.0851 5.06967C10.2281 5.42587 10.5624 5.66876 10.9454 5.69473L13.3739 5.8594C14.2733 5.92038 14.6392 7.04656 13.9475 7.62451L12.0795 9.18521C11.7849 9.43132 11.6572 9.82433 11.7509 10.1966L12.3447 12.5572C12.5647 13.4313 11.6067 14.1273 10.8433 13.6481L8.78171 12.3538C8.45661 12.1497 8.04339 12.1497 7.71829 12.3538L5.65675 13.6481C4.89333 14.1273 3.93534 13.4313 4.15526 12.5572L4.74911 10.1966C4.84276 9.82433 4.71507 9.43132 4.4205 9.18521L2.55254 7.62451C1.86081 7.04656 2.22673 5.92038 3.12606 5.8594L5.55463 5.69473C5.9376 5.66876 6.27191 5.42587 6.41495 5.06967L7.32203 2.81085Z";
|
|
304
|
+
/** A star row for `type: "rating"` / `editor: "rating"` (crisp/#760).
|
|
305
|
+
* Read-only (a plain `<span>` row, no interaction) unless `onChange` is
|
|
306
|
+
* given, which renders Attio's own measured accessible shape: a
|
|
307
|
+
* `role="radiogroup"` of `role="radio"` buttons, one per star. Clicking a
|
|
308
|
+
* star sets the rating to its 1-based index; clicking the star that
|
|
309
|
+
* already equals the current value clears it to 0 -- confirmed live
|
|
310
|
+
* (rating a record 5, then clicking the 5th star again cleared it
|
|
311
|
+
* entirely rather than re-committing 5). */
|
|
312
|
+
function RatingStars({ value, max = 5, onChange, "aria-label": ariaLabel, }) {
|
|
313
|
+
const [hoverIdx, setHoverIdx] = React.useState(null);
|
|
314
|
+
// The house radiogroup keyboard model (crisp's own spec check requires it
|
|
315
|
+
// on every role="radiogroup", regardless of Attio parity): arrows move
|
|
316
|
+
// focus AND select, Home/End jump to the ends. Landing on a star always
|
|
317
|
+
// SETS the rating to that star's position -- arrow nav has no "clear on
|
|
318
|
+
// re-land" toggle, that's a click-only gesture (see onClick below).
|
|
319
|
+
const { onKeyDown, groupRef } = (0, radiogroup_1.useRadioGroupKeyboard)({
|
|
320
|
+
select: (v) => onChange?.(Number(v)),
|
|
321
|
+
});
|
|
322
|
+
const stars = Array.from({ length: max }, (_, i) => i);
|
|
323
|
+
const content = stars.map((i) => {
|
|
324
|
+
// Hovering star i previews filling every star up to and including it,
|
|
325
|
+
// whether or not it's already part of the committed value -- measured:
|
|
326
|
+
// hovering the 5th star while 4 were already filled turned all 5 the
|
|
327
|
+
// hover colour, not just the would-be-new one.
|
|
328
|
+
const hovering = onChange != null && hoverIdx != null && i <= hoverIdx;
|
|
329
|
+
const filled = hovering ? true : i < value;
|
|
330
|
+
// A native <input type="radio"> can't be unchecked by re-clicking the
|
|
331
|
+
// already-selected one, which is exactly the measured behaviour
|
|
332
|
+
// (clicking the star that already equals the rating clears it to 0) --
|
|
333
|
+
// Attio's own live widget is a <button role="radio"> for the same
|
|
334
|
+
// reason.
|
|
335
|
+
return onChange ? ((0, jsx_runtime_1.jsx)("button", { type: "button", role: "radio", "aria-checked": i < value, "aria-label": `${i + 1} star${i === 0 ? "" : "s"}`, "data-value": String(i + 1), className: "crisp-datatable-rating-star-btn", onMouseEnter: () => setHoverIdx(i), onMouseLeave: () => setHoverIdx(null), onClick: (e) => {
|
|
336
|
+
e.stopPropagation();
|
|
337
|
+
onChange(i + 1 === value ? 0 : i + 1);
|
|
338
|
+
}, children: (0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", className: "crisp-datatable-rating-star", "data-filled": filled ? "" : undefined, "data-hovering": hovering ? "" : undefined, children: (0, jsx_runtime_1.jsx)("path", { d: RATING_STAR_D }) }) }, i)) : ((0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", className: "crisp-datatable-rating-star", "data-filled": filled ? "" : undefined, children: (0, jsx_runtime_1.jsx)("path", { d: RATING_STAR_D }) }, i));
|
|
339
|
+
});
|
|
340
|
+
return onChange ? ((0, jsx_runtime_1.jsx)("span", { role: "radiogroup", "aria-label": ariaLabel, className: "crisp-datatable-rating", ref: groupRef, onKeyDown: onKeyDown, children: content })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-rating", children: content }));
|
|
341
|
+
}
|
|
342
|
+
/** The preview/comments action icons a column's `onOpenPreview`/
|
|
343
|
+
* `onOpenComments`/`commentCount` produce — the exact markup `type: "entity"`
|
|
344
|
+
* already renders inline, extracted so a column that ISN'T `"entity"` (a
|
|
345
|
+
* custom `render`, or any other built-in type) can compose the same actions
|
|
346
|
+
* alongside its own content instead of the props silently doing nothing
|
|
347
|
+
* (crisp/#601 supersession follow-up: these three props used to only have
|
|
348
|
+
* an effect inside `renderTyped`'s `"entity"` case). `null` when neither
|
|
349
|
+
* prop is set, so a caller can render nothing rather than an empty
|
|
350
|
+
* flex wrapper. */
|
|
351
|
+
function renderCellActions(col, row, rowHovered, openPreviewLabel) {
|
|
352
|
+
if (col.onOpenComments) {
|
|
353
|
+
return ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-actions", children: (0, jsx_runtime_1.jsx)(comments_1.CommentCountBadge, { count: col.commentCount?.(row) ?? 0, hovered: rowHovered, onOpen: () => col.onOpenComments?.(row), onOpenPreview: col.onOpenPreview ? () => col.onOpenPreview?.(row) : undefined, previewLabel: openPreviewLabel }) }));
|
|
354
|
+
}
|
|
355
|
+
if (col.onOpenPreview) {
|
|
356
|
+
return ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-actions", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-entity-action", "aria-label": openPreviewLabel, onClick: (ev) => {
|
|
357
|
+
ev.stopPropagation();
|
|
358
|
+
col.onOpenPreview?.(row);
|
|
359
|
+
}, children: (0, jsx_runtime_1.jsx)(lucide_react_1.PanelRight, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) }));
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
295
363
|
/** Built-in typed-cell renderer (used when a column has no custom `render`). */
|
|
296
364
|
// `value` is always the RAW cell value and `display` the caller's `format`
|
|
297
365
|
// output, if any. They are separate arguments on purpose: the entity and tags
|
|
@@ -319,10 +387,7 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US", ope
|
|
|
319
387
|
// at a DIFFERENT record than the row itself (#501).
|
|
320
388
|
ev.stopPropagation();
|
|
321
389
|
col.onEntityClick?.(row);
|
|
322
|
-
}, children: e.name })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-name", children: e.name })), col
|
|
323
|
-
ev.stopPropagation();
|
|
324
|
-
col.onOpenPreview?.(row);
|
|
325
|
-
}, children: (0, jsx_runtime_1.jsx)(lucide_react_1.PanelRight, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) })))] }));
|
|
390
|
+
}, children: e.name })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-name", children: e.name })), renderCellActions(col, row, rowHovered, openPreviewLabel)] }));
|
|
326
391
|
}
|
|
327
392
|
case "tags": {
|
|
328
393
|
// asking for a format on a tag column means "render it your way" -- show
|
|
@@ -342,6 +407,14 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US", ope
|
|
|
342
407
|
// translation) can never silently repaint the chip
|
|
343
408
|
return ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-tag", "data-tone": col.tones?.[s] ?? toneFor(s), children: display || s }));
|
|
344
409
|
}
|
|
410
|
+
case "rating": {
|
|
411
|
+
// Read-only display -- `editor: "rating"` (the always-live picker) is
|
|
412
|
+
// handled separately, the same split `switch` gets (#453).
|
|
413
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
414
|
+
if (!n || Number.isNaN(n))
|
|
415
|
+
return empty;
|
|
416
|
+
return (0, jsx_runtime_1.jsx)(RatingStars, { value: n, max: col.maxRating ?? 5 });
|
|
417
|
+
}
|
|
345
418
|
case "url":
|
|
346
419
|
case "email": {
|
|
347
420
|
const text = display || asText(value);
|
|
@@ -1033,8 +1106,9 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1033
1106
|
* `editor: "select"`/`"reference"` open their own picker instead of this
|
|
1034
1107
|
* native-input family, but through the SAME double-click gate (#453) --
|
|
1035
1108
|
* see the row-render loop's `isEditing` branch for those, this function
|
|
1036
|
-
* only covers the native-input-backed types.
|
|
1037
|
-
*
|
|
1109
|
+
* only covers the native-input-backed types.
|
|
1110
|
+
* `editor: "switch"`/`"rating"`/`"checkbox"` are the editors with no
|
|
1111
|
+
* double-click gate at all (always live).
|
|
1038
1112
|
*/
|
|
1039
1113
|
function editorFor(col) {
|
|
1040
1114
|
switch (col.type) {
|
|
@@ -1287,8 +1361,13 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1287
1361
|
// already its own live widget (#385); Enter there is a no-op, same
|
|
1288
1362
|
// as any other non-editable cell that isn't wired to onRowClick.
|
|
1289
1363
|
// `select`/`reference` DO have one now (like every other editor),
|
|
1290
|
-
// so Enter opens them too.
|
|
1291
|
-
|
|
1364
|
+
// so Enter opens them too. `rating` (#760) and `checkbox` (#761)
|
|
1365
|
+
// join `switch` in the no-separate-editing-mode exception -- same
|
|
1366
|
+
// always-live shape.
|
|
1367
|
+
if (columns[c]?.editable &&
|
|
1368
|
+
columns[c]?.editor !== "switch" &&
|
|
1369
|
+
columns[c]?.editor !== "rating" &&
|
|
1370
|
+
columns[c]?.editor !== "checkbox") {
|
|
1292
1371
|
e.preventDefault();
|
|
1293
1372
|
openEditing(selCell.id, selCell.key);
|
|
1294
1373
|
}
|
|
@@ -1825,6 +1904,22 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1825
1904
|
if (col.editable && col.editor === "switch") {
|
|
1826
1905
|
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commitEdit(row.id, col.key, String(checked)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1827
1906
|
}
|
|
1907
|
+
// crisp/#760: `editor: "rating"` is the same always-
|
|
1908
|
+
// live shape as `switch` above -- clicking a star IS
|
|
1909
|
+
// the commit gesture, confirmed live.
|
|
1910
|
+
if (col.editable && col.editor === "rating") {
|
|
1911
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
1912
|
+
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(RatingStars, { "aria-label": `Edit ${col.key}`, value: Number.isNaN(n) ? 0 : n, max: col.maxRating ?? 5, onChange: (next) => commitEdit(row.id, col.key, String(next)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1913
|
+
}
|
|
1914
|
+
// crisp/#761: `editor: "checkbox"` is a THIRD always-
|
|
1915
|
+
// live boolean shape, distinct from `switch` --
|
|
1916
|
+
// confirmed live against Attio's own "Checkbox"
|
|
1917
|
+
// attribute type, which renders a plain square, not a
|
|
1918
|
+
// toggle. Reuses crisp's own `Checkbox` (already the
|
|
1919
|
+
// measured 16px/r6/brand-fill grid checkbox).
|
|
1920
|
+
if (col.editable && col.editor === "checkbox") {
|
|
1921
|
+
return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: "crisp-datatable-td--picker", "data-pending": isPending ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-edit-picker", children: [(0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { "aria-label": `Edit ${col.key}`, checked: value === true || value === "true", disabled: isPending, onChange: (e) => commitEdit(row.id, col.key, String(e.currentTarget.checked)) }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
1922
|
+
}
|
|
1828
1923
|
if (isEditing &&
|
|
1829
1924
|
(col.editor === "select" ||
|
|
1830
1925
|
col.editor === "reference" ||
|
|
@@ -1880,9 +1975,25 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1880
1975
|
i >= range.c0 &&
|
|
1881
1976
|
i <= range.c1);
|
|
1882
1977
|
})();
|
|
1883
|
-
const
|
|
1978
|
+
const rowHoveredForCell = hoveredRowId === row.id;
|
|
1979
|
+
const baseRendered = col.render
|
|
1884
1980
|
? col.render(value, row)
|
|
1885
|
-
: renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale, openPreviewLabel,
|
|
1981
|
+
: renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale, openPreviewLabel, rowHoveredForCell);
|
|
1982
|
+
// crisp/#601 supersession follow-up: `onOpenComments`/
|
|
1983
|
+
// `onOpenPreview`/`commentCount` used to only do
|
|
1984
|
+
// anything inside `renderTyped`'s `"entity"` case,
|
|
1985
|
+
// which already renders its own actions inline (see
|
|
1986
|
+
// `renderCellActions` there) -- silently dropping them
|
|
1987
|
+
// on any OTHER type, and on a custom `render` of any
|
|
1988
|
+
// type, since neither path went through that switch.
|
|
1989
|
+
// This composes the same actions alongside whatever
|
|
1990
|
+
// `rendered` is instead, decoupling the props from
|
|
1991
|
+
// requiring `type: "entity"`.
|
|
1992
|
+
const cellActions = col.type !== "entity" &&
|
|
1993
|
+
(col.onOpenComments || col.onOpenPreview)
|
|
1994
|
+
? renderCellActions(col, row, rowHoveredForCell, openPreviewLabel)
|
|
1995
|
+
: null;
|
|
1996
|
+
const rendered = cellActions ? ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-cell-actions-row", children: [baseRendered, cellActions] })) : (baseRendered);
|
|
1886
1997
|
const isExpanded = expandedCell?.id === row.id &&
|
|
1887
1998
|
expandedCell?.key === col.key;
|
|
1888
1999
|
const cellContent = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!isExpanded && rendered, errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere })), isExpanded && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-cell-overlay", onClick: (e) => e.stopPropagation(), children: rendered }))] }));
|
|
@@ -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
|
|
@@ -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; }
|
|
@@ -40,11 +40,41 @@ exports.TableToolbar = TableToolbar;
|
|
|
40
40
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
41
41
|
const lucide_react_1 = require("lucide-react");
|
|
42
42
|
const React = __importStar(require("react"));
|
|
43
|
+
const react_dom_1 = require("react-dom");
|
|
43
44
|
const core_1 = require("../../core");
|
|
44
45
|
const datepicker_1 = require("../datepicker");
|
|
45
46
|
const menu_1 = require("../menu");
|
|
46
47
|
const select_1 = require("../select");
|
|
47
48
|
const tabletoolbar_recipe_1 = require("./tabletoolbar.recipe");
|
|
49
|
+
// crisp/#766, #772: portals View settings/Import-Export into a
|
|
50
|
+
// consumer-supplied target (e.g. a `<span>` rendered next to `SavedViews`,
|
|
51
|
+
// or another next to the primary action) instead of this toolbar's own row,
|
|
52
|
+
// when one is given -- a plain passthrough otherwise, so the default (no
|
|
53
|
+
// ref given) stays byte-identical to before this existed. Kept outside the
|
|
54
|
+
// main component (no closure over its state) so it's a stable reference
|
|
55
|
+
// across renders.
|
|
56
|
+
//
|
|
57
|
+
// #772 fix: every `.crisp-tabletoolbar-*` chrome rule is nested under this
|
|
58
|
+
// recipe's own root class (`& .crisp-tabletoolbar-tool { ... }`), which
|
|
59
|
+
// compiles to a real CSS descendant combinator -- it needs actual DOM
|
|
60
|
+
// containment under an element carrying that class. A portal target
|
|
61
|
+
// rendered by the CONSUMER, outside this component's own root `<div>`,
|
|
62
|
+
// breaks that containment even though React still considers the portaled
|
|
63
|
+
// content a child for event/context purposes: the classes were still
|
|
64
|
+
// applied, they just matched nothing, so the buttons rendered as unstyled
|
|
65
|
+
// native chrome. Same failure class `select.recipe.ts`'s own doc comment
|
|
66
|
+
// describes for its listbox portal to `document.body`. Fixed the same way
|
|
67
|
+
// `ToolbarTool` (below) already lets a bare `.crisp-tabletoolbar-tool` be
|
|
68
|
+
// used outside an actual `TableToolbar` tree: wrap the portaled content in
|
|
69
|
+
// its own `tabletoolbarRecipe()`-classed span. `display: contents` on that
|
|
70
|
+
// recipe's own base rule means the wrapper contributes no box of its own,
|
|
71
|
+
// so it changes nothing about layout -- only re-establishes the selector
|
|
72
|
+
// match.
|
|
73
|
+
function ViewBarSlot({ target, children, }) {
|
|
74
|
+
return target
|
|
75
|
+
? (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("span", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)(), children: children }), target)
|
|
76
|
+
: children;
|
|
77
|
+
}
|
|
48
78
|
// #592: shared by every direction-bearing ChevronRight usage below (the
|
|
49
79
|
// field picker's own disclosure chevron, a flattened search result's
|
|
50
80
|
// breadcrumb separator, and a filter chip's own breadcrumb separator) so
|
|
@@ -338,7 +368,7 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
|
|
|
338
368
|
* this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
|
|
339
369
|
* Created by), which one shared list can't reproduce.
|
|
340
370
|
*/
|
|
341
|
-
function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, showSort = true, showViewSettings = true, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, importExportLabel: importExportLabelProp, onImportCsv, onExportCsv, onExportExcel, importExportHelpHref, importCsvLabel: importCsvLabelProp, exportCsvLabel: exportCsvLabelProp, exportExcelLabel: exportExcelLabelProp, learnImportExportLabel: learnImportExportLabelProp, backLabel: backLabelProp, nestedFieldPlaceholder, ...rest }) {
|
|
371
|
+
function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, showSort = true, showViewSettings = true, viewSettingsRef, importExportRef, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, importExportLabel: importExportLabelProp, onImportCsv, onExportCsv, onExportExcel, importExportHelpHref, importCsvLabel: importCsvLabelProp, exportCsvLabel: exportCsvLabelProp, exportExcelLabel: exportExcelLabelProp, learnImportExportLabel: learnImportExportLabelProp, backLabel: backLabelProp, nestedFieldPlaceholder, ...rest }) {
|
|
342
372
|
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
343
373
|
// CrispMessagesProvider's translation of the English default below, or
|
|
344
374
|
// that default itself with no provider — unchanged from before this
|
|
@@ -408,6 +438,19 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
408
438
|
setShownKeysState(next);
|
|
409
439
|
onShownKeysChange?.(next);
|
|
410
440
|
};
|
|
441
|
+
// crisp/#766: a ref's `.current` is null during THIS render (a ref only
|
|
442
|
+
// attaches during commit, and the consumer's own target element may not
|
|
443
|
+
// even exist in the DOM yet on first paint) -- an effect after mount picks
|
|
444
|
+
// it up and re-renders once, the same one-render-late pattern the floating
|
|
445
|
+
// popovers elsewhere in this file already use for a portal target.
|
|
446
|
+
const [viewSettingsTarget, setViewSettingsTarget] = React.useState(null);
|
|
447
|
+
React.useEffect(() => {
|
|
448
|
+
setViewSettingsTarget(viewSettingsRef?.current ?? null);
|
|
449
|
+
}, [viewSettingsRef]);
|
|
450
|
+
const [importExportTarget, setImportExportTarget] = React.useState(null);
|
|
451
|
+
React.useEffect(() => {
|
|
452
|
+
setImportExportTarget(importExportRef?.current ?? null);
|
|
453
|
+
}, [importExportRef]);
|
|
411
454
|
const [openMenu, setOpenMenu] = React.useState(null);
|
|
412
455
|
const [dragSort, setDragSort] = React.useState(null);
|
|
413
456
|
const [filterQuery, setFilterQuery] = React.useState("");
|
|
@@ -641,16 +684,23 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
641
684
|
// worse than an imperfect word order); `crispMessages` rather than a
|
|
642
685
|
// second `useCrispMessage` call, since this only fires on the branch that
|
|
643
686
|
// needs it.
|
|
687
|
+
// crisp/#788: "Sorted by" and the field name are NOT a font-weight split
|
|
688
|
+
// (both measure 500/medium live) -- the difference is colour/opacity only:
|
|
689
|
+
// "Sorted by" at 63% black, the field name at full-strength near-black
|
|
690
|
+
// (rgb(16,17,18)). The "+N" suffix is smaller (12px vs 14px) and its own
|
|
691
|
+
// mid-grey (rgb(80,81,85)), not muted-63%-black either -- three distinct
|
|
692
|
+
// treatments, not two. `sortField` isolates the resolved label so the
|
|
693
|
+
// default (non-`sortTriggerLabel`) render can wrap each part in its own
|
|
694
|
+
// span; `sortedLabel` stays a single string for the `sortTriggerLabel`
|
|
695
|
+
// escape hatch (a caller-supplied formatter can't be split, by definition)
|
|
696
|
+
// and the empty-state `sortLabel`.
|
|
697
|
+
const sortField = resolveColumnPath(sortColumns, sorts[0]?.key ?? "").at(-1)?.label;
|
|
644
698
|
const sortedLabel = sortTriggerLabel
|
|
645
699
|
? sortTriggerLabel(sorts, sortColumns)
|
|
646
700
|
: sorts.length
|
|
647
|
-
? `${crispMessages["Sorted by"] ?? "Sorted by"} ${
|
|
648
|
-
// #754: sorts[0].key may be a dot-joined nested path
|
|
649
|
-
// ("team.jobTitle") — resolve it the same way the chip and sort
|
|
650
|
-
// row do, rather than a flat find that fails on a nested key.
|
|
651
|
-
resolveColumnPath(sortColumns, sorts[0].key).at(-1)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
|
|
701
|
+
? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortField}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
|
|
652
702
|
: sortLabel;
|
|
653
|
-
return ((0, jsx_runtime_1.jsx)("div", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && ((0, jsx_runtime_1.jsxs)("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
703
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : " crisp-tabletoolbar-tool--dashed"}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortTriggerLabel || !sorts.length ? (sortedLabel) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-sorted-prefix", children: crispMessages["Sorted by"] ?? "Sorted by" }), " ", (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-sorted-field", children: sortField }), sorts.length > 1 && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sorted-count", children: [" ", "+", sorts.length - 1] }))] }))] }), openMenu === "sort" && ((0, jsx_runtime_1.jsxs)("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
654
704
|
if (dragSort !== null)
|
|
655
705
|
e.preventDefault();
|
|
656
706
|
}, onDrop: () => {
|
|
@@ -687,7 +737,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
687
737
|
label: ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), descendingLabel] })),
|
|
688
738
|
keywords: descendingLabel,
|
|
689
739
|
},
|
|
690
|
-
], "aria-label": sortDirectionLabel }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-foot", children: (0, jsx_runtime_1.jsxs)("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] })), showSort && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool
|
|
740
|
+
], "aria-label": sortDirectionLabel }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-foot", children: (0, jsx_runtime_1.jsxs)("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] })), showSort && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool${activeFilters ? " crisp-tabletoolbar-tool--active" : " crisp-tabletoolbar-tool--dashed"}`, onClick: () => {
|
|
691
741
|
setFieldPickerPath([]);
|
|
692
742
|
setOpenMenu(openMenu === "filter" ? null : "filter");
|
|
693
743
|
}, "aria-haspopup": "dialog", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
|
|
@@ -786,43 +836,43 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
786
836
|
}, children: label }, label))) })] })) : ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", type: col?.type === "number" ? "number" : "text", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
|
|
787
837
|
// biome-ignore lint/a11y/noAutofocus: focus the value field
|
|
788
838
|
autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })) }))] }, cond.id));
|
|
789
|
-
}), showViewSettings && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), showViewSettings && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
839
|
+
}), showViewSettings && !viewSettingsTarget && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), showViewSettings && ((0, jsx_runtime_1.jsx)(ViewBarSlot, { target: viewSettingsTarget, children: (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--raised", onClick: () => {
|
|
840
|
+
if (openMenu === "viewsettings") {
|
|
841
|
+
setOpenMenu(null);
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
setAddingAttribute(false);
|
|
845
|
+
setFieldPickerPath([]);
|
|
846
|
+
setFilterQuery("");
|
|
847
|
+
setOpenMenu("viewsettings");
|
|
848
|
+
}
|
|
849
|
+
}, "aria-haspopup": "dialog", "aria-expanded": openMenu === "viewsettings", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.SlidersHorizontal, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), viewSettingsLabel, (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }), openMenu === "viewsettings" &&
|
|
850
|
+
(addingAttribute ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": addAttributeToViewLabel, children: nestedFieldPicker(hiddenAttrs, showAttr, searchAttributesPlaceholder, searchAttributesPlaceholder) })) : ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, children: [shownAttrs.map((col, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
851
|
+
if (dragAttr !== null)
|
|
852
|
+
e.preventDefault();
|
|
853
|
+
}, onDrop: () => {
|
|
854
|
+
if (dragAttr !== null)
|
|
855
|
+
moveAttr(dragAttr, i);
|
|
856
|
+
setDragAttr(null);
|
|
857
|
+
}, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderAttributeLabel, draggable: true, onDragStart: () => setDragAttr(i), onDragEnd: () => setDragAttr(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel crisp-tabletoolbar-sort-field", children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : null, col.label] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => hideAttr(col.key), "aria-label": hideAttributeLabel?.(col.label) ??
|
|
858
|
+
`${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => {
|
|
859
|
+
setAddingAttribute(true);
|
|
860
|
+
setFieldPickerPath([]);
|
|
861
|
+
setFilterQuery("");
|
|
862
|
+
}, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] }) })), (onImportCsv ||
|
|
813
863
|
onExportCsv ||
|
|
814
864
|
onExportExcel ||
|
|
815
|
-
importExportHelpHref) && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--raised", onClick: () => setOpenMenu(openMenu === "importexport" ? null : "importexport"), "aria-haspopup": "menu", "aria-expanded": openMenu === "importexport", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Upload, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), importExportLabel, (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }), openMenu === "importexport" && ((0, jsx_runtime_1.jsxs)("div", { ref: importExportMenuRef, className: "crisp-tabletoolbar-menu", role: "menu", "aria-label": importExportLabel, onKeyDown: onImportExportKeyDown, children: [onImportCsv && ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
865
|
+
importExportHelpHref) && ((0, jsx_runtime_1.jsx)(ViewBarSlot, { target: importExportTarget, children: (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--raised", onClick: () => setOpenMenu(openMenu === "importexport" ? null : "importexport"), "aria-haspopup": "menu", "aria-expanded": openMenu === "importexport", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Upload, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), importExportLabel, (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }), openMenu === "importexport" && ((0, jsx_runtime_1.jsxs)("div", { ref: importExportMenuRef, className: "crisp-tabletoolbar-menu", role: "menu", "aria-label": importExportLabel, onKeyDown: onImportExportKeyDown, children: [onImportCsv && ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
|
|
866
|
+
onImportCsv();
|
|
867
|
+
setOpenMenu(null);
|
|
868
|
+
}, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Upload, { size: 15, strokeWidth: 1.75 }), importCsvLabel] })), onImportCsv && (onExportCsv || onExportExcel) && ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-tabletoolbar-menu-sep" })), onExportCsv && ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
|
|
869
|
+
onExportCsv();
|
|
870
|
+
setOpenMenu(null);
|
|
871
|
+
}, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), exportCsvLabel] })), onExportExcel && ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
|
|
872
|
+
onExportExcel();
|
|
873
|
+
setOpenMenu(null);
|
|
874
|
+
}, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), exportExcelLabel] })), importExportHelpHref &&
|
|
875
|
+
(onImportCsv || onExportCsv || onExportExcel) && ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-tabletoolbar-menu-sep" })), importExportHelpHref && ((0, jsx_runtime_1.jsxs)("a", { className: "crisp-tabletoolbar-menu-item", role: "menuitem", href: importExportHelpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 15, strokeWidth: 1.75 }), learnImportExportLabel] }))] }))] }) })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-spacer" }), count != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
|
|
826
876
|
}
|
|
827
877
|
/**
|
|
828
878
|
* The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone
|
|
@@ -74,14 +74,21 @@ const filterChipChrome = `
|
|
|
74
74
|
& .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: ${tokens_1.contract.space["1"]}; margin-top: ${tokens_1.contract.space["2"]}; padding: 0 ${tokens_1.contract.space["1"]}; }
|
|
75
75
|
& .crisp-tabletoolbar-datepop-preset { height: ${tokens_1.contract.size.chip}; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.chip}; background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.sm}; cursor: pointer; }
|
|
76
76
|
& .crisp-tabletoolbar-datepop-preset:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 8%, transparent); }`;
|
|
77
|
-
// #595
|
|
78
|
-
// view bar: `28px` tall, `radius.md`
|
|
79
|
-
// (`rgb(
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
77
|
+
// #595, corrected #772 (re-measured live 2026-09-25): the view bar's raised
|
|
78
|
+
// trigger chrome — `app.attio.com`'s own view bar: `28px` tall, `radius.md`
|
|
79
|
+
// (8px), background `bgSurface` (white, `rgb(255,255,255)` exact match — the
|
|
80
|
+
// #595 pass originally measured `bgSurfaceMuted`/`rgb(246,247,247)`, which
|
|
81
|
+
// re-measuring live this pass shows was wrong, not a later Attio change: ALL
|
|
82
|
+
// THREE of the switcher, View settings and Import/Export read pure white)
|
|
83
|
+
// and `elevation.field`'s exact 3-layer shadow (`inset 0 0 0 1px transparent,
|
|
84
|
+
// 0 0 2px rgba(28,40,64,.18), 0 1px 3px rgba(0,0,0,.04)`) — distinct from
|
|
85
|
+
// Sort/Filter's plain ringed `.tool` (no elevation shadow; see `--dashed` in
|
|
86
|
+
// `tabletoolbar.chrome.ts` for their own OWN empty-state chrome). Originally
|
|
87
|
+
// built for Import/Export alone ("the only trigger... with a trailing
|
|
88
|
+
// chevron") — that was also wrong: View settings and the view switcher both
|
|
89
|
+
// carry one too, confirmed live (3 children each: icon, label, chevron).
|
|
90
|
+
// Now shared by View settings' own trigger (`tabletoolbar.tsx`) as well as
|
|
91
|
+
// Import/Export's. Overrides `.tool`'s own base background/shadow by cascade
|
|
85
92
|
// order (this block is appended after `toolbarChromeBody`, which defines
|
|
86
93
|
// them), the same way `.tool--ghost` already does. `border`/`font-family`/
|
|
87
94
|
// `cursor` are repeated here too, even though `.tool`'s own base rule (in
|
|
@@ -91,7 +98,18 @@ const filterChipChrome = `
|
|
|
91
98
|
// behind `chrome.ts`'s function call is invisible to it; harmless repeats,
|
|
92
99
|
// not a real gap.
|
|
93
100
|
const importExportChrome = `
|
|
94
|
-
& .crisp-tabletoolbar-tool--raised { border: none; background: ${tokens_1.contract.
|
|
101
|
+
& .crisp-tabletoolbar-tool--raised { border: none; background: ${tokens_1.contract.bgSurface}; box-shadow: ${tokens_1.elevation.field}; font-family: inherit; cursor: pointer; }`;
|
|
102
|
+
// crisp/#788: the Sort trigger's own "Sorted by X (+N)" label, measured live
|
|
103
|
+
// against Attio's own chip -- three parts, not one flat string, and NOT a
|
|
104
|
+
// font-weight split (all three read 500/medium): "Sorted by" is muted (63%
|
|
105
|
+
// black), the resolved field name is full-strength near-black, and a
|
|
106
|
+
// trailing "+N" (when more than one sort is active) is both smaller (12px
|
|
107
|
+
// vs 14px) and its own mid-grey, distinct from the "Sorted by" muted tone.
|
|
108
|
+
// TableToolbar-only, same reasoning as `filterChipChrome` above (RecordsTable
|
|
109
|
+
// keeps its own older Sort label markup, not touched here).
|
|
110
|
+
const sortedLabelChrome = `
|
|
111
|
+
& .crisp-tabletoolbar-sorted-prefix { color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); }
|
|
112
|
+
& .crisp-tabletoolbar-sorted-count { font-size: ${tokens_1.contract.text.sm}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 73%, transparent); }`;
|
|
95
113
|
// The table toolbar, measured: 28px controls, the Sort/Filter buttons and
|
|
96
114
|
// the menus they open, sort-condition rows, and the filter chips. The chrome
|
|
97
115
|
// itself lives in ./tabletoolbar.chrome.ts because RecordsTable renders the
|
|
@@ -110,6 +128,7 @@ ${(0, tabletoolbar_chrome_1.toolbarChromeHead)("tabletoolbar")}
|
|
|
110
128
|
${(0, tabletoolbar_chrome_1.toolbarChromeBody)("tabletoolbar")}
|
|
111
129
|
${filterChipChrome}
|
|
112
130
|
${importExportChrome}
|
|
131
|
+
${sortedLabelChrome}
|
|
113
132
|
${core_1.rtlFlipCss}
|
|
114
133
|
`,
|
|
115
134
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tagTone = tagTone;
|
|
4
|
+
// deterministic pastel colour per tag value (a select option keeps its colour)
|
|
5
|
+
const TAG_TONES = [
|
|
6
|
+
"brand",
|
|
7
|
+
"success",
|
|
8
|
+
"warning",
|
|
9
|
+
"danger",
|
|
10
|
+
"purple",
|
|
11
|
+
"cyan",
|
|
12
|
+
"orange",
|
|
13
|
+
];
|
|
14
|
+
function tagTone(t) {
|
|
15
|
+
let h = 0;
|
|
16
|
+
for (let i = 0; i < t.length; i++)
|
|
17
|
+
h = (h * 31 + t.charCodeAt(i)) >>> 0;
|
|
18
|
+
return TAG_TONES[h % TAG_TONES.length];
|
|
19
|
+
}
|