@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
|
@@ -47,7 +47,8 @@ const spinner_1 = require("../../components/spinner");
|
|
|
47
47
|
const switch_1 = require("../../components/switch");
|
|
48
48
|
const tabs_1 = require("../../components/tabs");
|
|
49
49
|
const core_1 = require("../../core");
|
|
50
|
-
const
|
|
50
|
+
const column_1 = require("./column");
|
|
51
|
+
const editable_field_1 = require("./editable-field");
|
|
51
52
|
const recordpage_recipe_1 = require("./recordpage.recipe");
|
|
52
53
|
function valueEmpty(v) {
|
|
53
54
|
return v == null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
@@ -212,7 +213,7 @@ function TagsValue({ values }) {
|
|
|
212
213
|
const [expanded, setExpanded] = React.useState(false);
|
|
213
214
|
const overflow = values.length - 1;
|
|
214
215
|
const shown = expanded ? values : values.slice(0, 1);
|
|
215
|
-
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [shown.map((t) => ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-tag", "data-tone": (0,
|
|
216
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [shown.map((t) => ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-tag", "data-tone": (0, column_1.tagTone)(t), children: t }, t))), !expanded && overflow > 0 && ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-morevalues", onClick: () => setExpanded(true), children: ["+", overflow] }))] }));
|
|
216
217
|
}
|
|
217
218
|
function FieldValue({ col, row, labels, }) {
|
|
218
219
|
const v = row[col.key];
|
|
@@ -316,7 +317,7 @@ function EmptyCard({ icon, children, }) {
|
|
|
316
317
|
*/
|
|
317
318
|
function RecordDetails({ record, columns, sections, onEdit, labels, className, ...rest }) {
|
|
318
319
|
const t = useRecordPageLabels(labels);
|
|
319
|
-
const { editingKey, pendingKey, error, openEditing, closeEditing, commit } = (0,
|
|
320
|
+
const { editingKey, pendingKey, error, openEditing, closeEditing, commit } = (0, editable_field_1.useEditableField)(onEdit);
|
|
320
321
|
const [detailsOpen, setDetailsOpen] = React.useState(true);
|
|
321
322
|
const [searchOpen, setSearchOpen] = React.useState(false);
|
|
322
323
|
const [fieldQuery, setFieldQuery] = React.useState("");
|
|
@@ -406,7 +407,7 @@ function RecordDetails({ record, columns, sections, onEdit, labels, className, .
|
|
|
406
407
|
}
|
|
407
408
|
if (canEdit && editingKey === col.key) {
|
|
408
409
|
const value = record[col.key];
|
|
409
|
-
const editor = (0,
|
|
410
|
+
const editor = (0, editable_field_1.editableInputType)(col.type);
|
|
410
411
|
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-recordpage-edit",
|
|
411
412
|
// biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
|
|
412
413
|
autoFocus: true, ...editor, disabled: isPending, defaultValue: value == null ? "" : String(value), "aria-label": `Edit ${col.label}`, onKeyDown: (e) => {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.testRows = exports.testColumns = void 0;
|
|
4
|
+
const lucide_react_1 = require("lucide-react");
|
|
5
|
+
// Moved here from product/recordstable/recordstable.tsx's `defaultColumns`/
|
|
6
|
+
// `defaultRows` (crisp#786): RecordsTable is deprecated and its whole
|
|
7
|
+
// directory is being deleted, but RecordPage's own tests (and the shared
|
|
8
|
+
// RTL-keyboard-convention tests that live alongside them) still need a
|
|
9
|
+
// real, 12-column-deep dataset to exercise "a field past the first 5 needs
|
|
10
|
+
// View all values" and similar shape-dependent assertions. Simplified from
|
|
11
|
+
// the original: no baked-in company-logo data URIs (nothing here asserts on
|
|
12
|
+
// avatars), a plain favicon URL instead.
|
|
13
|
+
exports.testColumns = [
|
|
14
|
+
{ key: "company", label: "Company", type: "entity", icon: lucide_react_1.Building2 },
|
|
15
|
+
{ key: "domain", label: "Domain", type: "url", icon: lucide_react_1.Globe },
|
|
16
|
+
{
|
|
17
|
+
key: "categories",
|
|
18
|
+
label: "Categories",
|
|
19
|
+
type: "tags",
|
|
20
|
+
icon: lucide_react_1.Tag,
|
|
21
|
+
width: 240,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
key: "employees",
|
|
25
|
+
label: "Employees",
|
|
26
|
+
type: "number",
|
|
27
|
+
icon: lucide_react_1.Users,
|
|
28
|
+
width: 120,
|
|
29
|
+
},
|
|
30
|
+
{ key: "location", label: "Location", type: "text", icon: lucide_react_1.MapPin },
|
|
31
|
+
{ key: "stage", label: "Stage", type: "tags", icon: lucide_react_1.TrendingUp, width: 150 },
|
|
32
|
+
{ key: "ceo", label: "CEO", type: "text", icon: lucide_react_1.Briefcase, width: 170 },
|
|
33
|
+
{ key: "twitter", label: "Twitter", type: "url", icon: lucide_react_1.AtSign, width: 150 },
|
|
34
|
+
{
|
|
35
|
+
key: "followers",
|
|
36
|
+
label: "Followers",
|
|
37
|
+
type: "number",
|
|
38
|
+
icon: lucide_react_1.Users,
|
|
39
|
+
width: 120,
|
|
40
|
+
},
|
|
41
|
+
{ key: "linkedin", label: "LinkedIn", type: "url", icon: lucide_react_1.Globe, width: 180 },
|
|
42
|
+
{
|
|
43
|
+
key: "founded",
|
|
44
|
+
label: "Founded",
|
|
45
|
+
type: "date",
|
|
46
|
+
icon: lucide_react_1.Calendar,
|
|
47
|
+
width: 110,
|
|
48
|
+
},
|
|
49
|
+
{ key: "arr", label: "Est. ARR", type: "number", icon: lucide_react_1.Hash, width: 130 },
|
|
50
|
+
];
|
|
51
|
+
// biome-ignore format: one row per line keeps the wide demo dataset readable
|
|
52
|
+
exports.testRows = [
|
|
53
|
+
{ id: "google", company: { name: "Google", avatar: "https://www.google.com/s2/favicons?domain=google.com&sz=64" }, domain: "google.com", categories: ["Search", "Ads", "Cloud", "AI", "Android", "Maps", "Hardware", "Workspace", "Chrome"], employees: 182000, location: "Mountain View, CA", stage: ["Public"], ceo: "Sundar Pichai", twitter: "twitter.com/google", followers: 29800000, linkedin: "linkedin.com/company/google", founded: "1998", arr: 307000000000 },
|
|
54
|
+
{ id: "stripe", company: { name: "Stripe", avatar: "https://www.google.com/s2/favicons?domain=stripe.com&sz=64" }, domain: "stripe.com", categories: ["Payments", "Fintech"], employees: 8000, location: "San Francisco, CA", stage: ["Series I"], ceo: "Patrick Collison", twitter: "twitter.com/stripe", followers: 1200000, linkedin: "linkedin.com/company/stripe", founded: "2010", arr: 16000000000 },
|
|
55
|
+
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.recordstableRecipe = exports.tagTone = exports.RecordsTable = exports.defaultRows = exports.defaultColumns = exports.useEditableField = exports.editableInputType = void 0;
|
|
4
|
-
var editable_field_1 = require("
|
|
4
|
+
var editable_field_1 = require("../recordpage/editable-field");
|
|
5
5
|
Object.defineProperty(exports, "editableInputType", { enumerable: true, get: function () { return editable_field_1.editableInputType; } });
|
|
6
6
|
Object.defineProperty(exports, "useEditableField", { enumerable: true, get: function () { return editable_field_1.useEditableField; } });
|
|
7
7
|
var recordstable_1 = require("./recordstable");
|
|
@@ -552,6 +552,11 @@ function CreateRecordDialog({ open, onOpenChange, columns, entityLabel, onCreate
|
|
|
552
552
|
* editing (double-click a text/number/url/date cell), add-row and add-column,
|
|
553
553
|
* and a floating bulk-action bar (delete / deselect) when rows are selected.
|
|
554
554
|
* Selection can be controlled via `selectedIds`/`onSelectionChange`.
|
|
555
|
+
*
|
|
556
|
+
* @deprecated Reach for `DataTable` instead — a single Attio-exact records
|
|
557
|
+
* grid replaces the separate table variants. `RecordsTable` has no
|
|
558
|
+
* consumers left (checked GRC and the fms/Mercury admin) and is slated for
|
|
559
|
+
* removal in a future minor.
|
|
555
560
|
*/
|
|
556
561
|
function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, rows: rowsProp = exports.defaultRows, selectedIds, onSelectionChange, showNew = true, allowAddColumn = true, onOpenRecord, onOpenRecordPage, avatarTone = "brand", emptyStateExtra, className, ...rest }) {
|
|
557
562
|
const [cols, setCols] = React.useState(columnsProp);
|
|
@@ -26,24 +26,26 @@ const config = (0, core_1.defineRecipe)({
|
|
|
26
26
|
position: relative; display: inline-flex; font-family: ${tokens_1.contract.font.sans};
|
|
27
27
|
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
28
28
|
|
|
29
|
-
/*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
/* crisp/#772 follow-up, re-measured live 2026-09-25 in the trigger's
|
|
30
|
+
actual position (Attio's real view bar, next to View settings --
|
|
31
|
+
fixed there in #766/#768/#778): white (bgSurface, rgb(255,255,255)
|
|
32
|
+
exact) with elevation.field's 3-layer shadow, the same raised
|
|
33
|
+
chrome View settings and Import/Export both carry
|
|
34
|
+
(tabletoolbar.recipe.ts's own --raised, not reused here directly
|
|
35
|
+
since this is a separate recipe/component, but the identical measured
|
|
36
|
+
values). Supersedes the original #555 measurement (rgb(246,247,247),
|
|
37
|
+
reused as bgRaised -- a flat grey fill with no shadow at all), which
|
|
38
|
+
was wrong: this trigger was never re-checked once it moved into its
|
|
39
|
+
real view-bar position. */
|
|
38
40
|
& .crisp-savedviews-trigger {
|
|
39
41
|
box-sizing: border-box; display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]};
|
|
40
42
|
height: ${tokens_1.contract.size.row}; margin: 0; padding: ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["1.5"]};
|
|
41
|
-
border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.
|
|
43
|
+
border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgSurface}; box-shadow: ${tokens_1.elevation.field};
|
|
42
44
|
font-family: inherit; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary};
|
|
43
45
|
cursor: pointer; white-space: nowrap; transition: background-color .12s ease;
|
|
44
46
|
}
|
|
45
47
|
& .crisp-savedviews-trigger:hover,
|
|
46
|
-
& .crisp-savedviews-trigger[data-open] { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, ${tokens_1.contract.
|
|
48
|
+
& .crisp-savedviews-trigger[data-open] { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, ${tokens_1.contract.bgSurface}); }
|
|
47
49
|
& .crisp-savedviews-trigger-icon { flex-shrink: 0; display: inline-flex; width: ${tokens_1.contract.size.icon}; height: ${tokens_1.contract.size.icon}; }
|
|
48
50
|
& .crisp-savedviews-trigger-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
49
51
|
& .crisp-savedviews-trigger-chevron { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
|
|
@@ -14,6 +14,11 @@ function Cell({ flex, className, children, }) {
|
|
|
14
14
|
* taskstable.recipe.ts) with a typed column/cell layer: a title+checkbox
|
|
15
15
|
* row grouped into due-date sections, a due-date cell that goes red past
|
|
16
16
|
* due, and consumer-supplied record/assignee chip slots.
|
|
17
|
+
*
|
|
18
|
+
* @deprecated Reach for `DataTable`'s `groupBy` instead — a single
|
|
19
|
+
* Attio-exact records grid replaces the separate table variants.
|
|
20
|
+
* `TasksTable` has no consumers left (checked GRC and the fms/Mercury
|
|
21
|
+
* admin) and is slated for removal in a future minor.
|
|
17
22
|
*/
|
|
18
23
|
function TasksTable({ columns, groups, leading, trailing, emptyLabel: emptyLabelProp, completeLabel: completeLabelProp, incompleteLabel: incompleteLabelProp, className, ...rest }) {
|
|
19
24
|
const crispMessages = (0, core_1.useCrispMessages)();
|
package/dist/datatable.css
CHANGED
|
@@ -152,6 +152,10 @@
|
|
|
152
152
|
& th[data-drop-after]::after { inset-inline-end: -1.5px; }
|
|
153
153
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
154
154
|
& .crisp-datatable-entity { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
|
|
155
|
+
/* crisp/#601 supersession follow-up -- the same flex-row shape .crisp-datatable-entity gives its own avatar+name+actions, but structural only (no entity-specific colour/weight): lets onOpenComments/onOpenPreview compose alongside ANY column's own
|
|
156
|
+
content (a custom render, or a non-entity built-in type), not just
|
|
157
|
+
type: "entity"'s own inline rendering. */
|
|
158
|
+
& .crisp-datatable-cell-actions-row { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; width: 100%; }
|
|
155
159
|
/* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text, plain hover vs rest) that Attio's underline is unconditional, not a
|
|
156
160
|
hover reveal -- identical at rest and on hover: text-decoration-color
|
|
157
161
|
is rgba(0,0,0,0.09), a 9% tint, not the name's own full-contrast text colour. The previous :hover-gated, full-contrast rule here was itself unmeasured and wrong in two ways at once. */
|
|
@@ -183,6 +187,16 @@
|
|
|
183
187
|
/* url/email cell types (#654) -- same measured values as RecordsTable's own .crisp-recordstable-link, which this mirrors: no underline at rest, underline on hover, own-colour text rather than a browser-blue link. */
|
|
184
188
|
& .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
185
189
|
& .crisp-datatable-link:hover { text-decoration: underline; }
|
|
190
|
+
/* rating cell type/editor (#760) -- measured live: 16px stars on an 18px pitch (a 2px gap), an empty star at rgba(0,0,0,0.1), a committed/filled star at brand600 (--crisp-bg-brand-solid, an exact match to the measured #266df0), and a distinct hover-preview colour -- rgb(33,91,196) -- that's close to but NOT brand700/bgBrandSolidPressed
|
|
191
|
+
(#1f5fd6); kept as its own literal rather than forced onto that token,
|
|
192
|
+
since the two were measured stable and different. The read-only svg
|
|
193
|
+
and the editable button's own inner svg both carry the crisp-datatable-rating-star class on the svg itself (not the button), so one set of fill rules covers both -- the button only adds its own reset + cursor via a separate -btn class. */
|
|
194
|
+
& .crisp-datatable-rating { display: inline-flex; align-items: center; gap: 2px; }
|
|
195
|
+
& .crisp-datatable-rating-star { display: block; }
|
|
196
|
+
& .crisp-datatable-rating-star path { fill: rgba(0, 0, 0, 0.1); }
|
|
197
|
+
& .crisp-datatable-rating-star[data-filled] path { fill: var(--crisp-bg-brand-solid); }
|
|
198
|
+
& .crisp-datatable-rating-star[data-hovering] path { fill: #215bc4; }
|
|
199
|
+
& .crisp-datatable-rating-star-btn { display: inline-flex; padding: 0; margin: 0; border: none; background: none; font-family: inherit; cursor: pointer; }
|
|
186
200
|
/* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
|
|
187
201
|
hairline doubling as the drop indicator (it tints toward the brand
|
|
188
202
|
colour as the active drop target). The affordance + geometry are
|
|
@@ -153,6 +153,10 @@
|
|
|
153
153
|
& th[data-drop-after]::after { inset-inline-end: -1.5px; }
|
|
154
154
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
155
155
|
& .crisp-datatable-entity { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
|
|
156
|
+
/* crisp/#601 supersession follow-up -- the same flex-row shape .crisp-datatable-entity gives its own avatar+name+actions, but structural only (no entity-specific colour/weight): lets onOpenComments/onOpenPreview compose alongside ANY column's own
|
|
157
|
+
content (a custom render, or a non-entity built-in type), not just
|
|
158
|
+
type: "entity"'s own inline rendering. */
|
|
159
|
+
& .crisp-datatable-cell-actions-row { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; width: 100%; }
|
|
156
160
|
/* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text, plain hover vs rest) that Attio's underline is unconditional, not a
|
|
157
161
|
hover reveal -- identical at rest and on hover: text-decoration-color
|
|
158
162
|
is rgba(0,0,0,0.09), a 9% tint, not the name's own full-contrast text colour. The previous :hover-gated, full-contrast rule here was itself unmeasured and wrong in two ways at once. */
|
|
@@ -184,6 +188,16 @@
|
|
|
184
188
|
/* url/email cell types (#654) -- same measured values as RecordsTable's own .crisp-recordstable-link, which this mirrors: no underline at rest, underline on hover, own-colour text rather than a browser-blue link. */
|
|
185
189
|
& .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
186
190
|
& .crisp-datatable-link:hover { text-decoration: underline; }
|
|
191
|
+
/* rating cell type/editor (#760) -- measured live: 16px stars on an 18px pitch (a 2px gap), an empty star at rgba(0,0,0,0.1), a committed/filled star at brand600 (--crisp-bg-brand-solid, an exact match to the measured #266df0), and a distinct hover-preview colour -- rgb(33,91,196) -- that's close to but NOT brand700/bgBrandSolidPressed
|
|
192
|
+
(#1f5fd6); kept as its own literal rather than forced onto that token,
|
|
193
|
+
since the two were measured stable and different. The read-only svg
|
|
194
|
+
and the editable button's own inner svg both carry the crisp-datatable-rating-star class on the svg itself (not the button), so one set of fill rules covers both -- the button only adds its own reset + cursor via a separate -btn class. */
|
|
195
|
+
& .crisp-datatable-rating { display: inline-flex; align-items: center; gap: 2px; }
|
|
196
|
+
& .crisp-datatable-rating-star { display: block; }
|
|
197
|
+
& .crisp-datatable-rating-star path { fill: rgba(0, 0, 0, 0.1); }
|
|
198
|
+
& .crisp-datatable-rating-star[data-filled] path { fill: var(--crisp-bg-brand-solid); }
|
|
199
|
+
& .crisp-datatable-rating-star[data-hovering] path { fill: #215bc4; }
|
|
200
|
+
& .crisp-datatable-rating-star-btn { display: inline-flex; padding: 0; margin: 0; border: none; background: none; font-family: inherit; cursor: pointer; }
|
|
187
201
|
/* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
|
|
188
202
|
hairline doubling as the drop indicator (it tints toward the brand
|
|
189
203
|
colour as the active drop target). The affordance + geometry are
|
|
@@ -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);
|
|
@@ -995,8 +1068,9 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
995
1068
|
* `editor: "select"`/`"reference"` open their own picker instead of this
|
|
996
1069
|
* native-input family, but through the SAME double-click gate (#453) --
|
|
997
1070
|
* see the row-render loop's `isEditing` branch for those, this function
|
|
998
|
-
* only covers the native-input-backed types.
|
|
999
|
-
*
|
|
1071
|
+
* only covers the native-input-backed types.
|
|
1072
|
+
* `editor: "switch"`/`"rating"`/`"checkbox"` are the editors with no
|
|
1073
|
+
* double-click gate at all (always live).
|
|
1000
1074
|
*/
|
|
1001
1075
|
function editorFor(col) {
|
|
1002
1076
|
switch (col.type) {
|
|
@@ -1249,8 +1323,13 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1249
1323
|
// already its own live widget (#385); Enter there is a no-op, same
|
|
1250
1324
|
// as any other non-editable cell that isn't wired to onRowClick.
|
|
1251
1325
|
// `select`/`reference` DO have one now (like every other editor),
|
|
1252
|
-
// so Enter opens them too.
|
|
1253
|
-
|
|
1326
|
+
// so Enter opens them too. `rating` (#760) and `checkbox` (#761)
|
|
1327
|
+
// join `switch` in the no-separate-editing-mode exception -- same
|
|
1328
|
+
// always-live shape.
|
|
1329
|
+
if (columns[c]?.editable &&
|
|
1330
|
+
columns[c]?.editor !== "switch" &&
|
|
1331
|
+
columns[c]?.editor !== "rating" &&
|
|
1332
|
+
columns[c]?.editor !== "checkbox") {
|
|
1254
1333
|
e.preventDefault();
|
|
1255
1334
|
openEditing(selCell.id, selCell.key);
|
|
1256
1335
|
}
|
|
@@ -1787,6 +1866,22 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1787
1866
|
if (col.editable && col.editor === "switch") {
|
|
1788
1867
|
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));
|
|
1789
1868
|
}
|
|
1869
|
+
// crisp/#760: `editor: "rating"` is the same always-
|
|
1870
|
+
// live shape as `switch` above -- clicking a star IS
|
|
1871
|
+
// the commit gesture, confirmed live.
|
|
1872
|
+
if (col.editable && col.editor === "rating") {
|
|
1873
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
1874
|
+
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(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));
|
|
1875
|
+
}
|
|
1876
|
+
// crisp/#761: `editor: "checkbox"` is a THIRD always-
|
|
1877
|
+
// live boolean shape, distinct from `switch` --
|
|
1878
|
+
// confirmed live against Attio's own "Checkbox"
|
|
1879
|
+
// attribute type, which renders a plain square, not a
|
|
1880
|
+
// toggle. Reuses crisp's own `Checkbox` (already the
|
|
1881
|
+
// measured 16px/r6/brand-fill grid checkbox).
|
|
1882
|
+
if (col.editable && col.editor === "checkbox") {
|
|
1883
|
+
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(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));
|
|
1884
|
+
}
|
|
1790
1885
|
if (isEditing &&
|
|
1791
1886
|
(col.editor === "select" ||
|
|
1792
1887
|
col.editor === "reference" ||
|
|
@@ -1842,9 +1937,25 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1842
1937
|
i >= range.c0 &&
|
|
1843
1938
|
i <= range.c1);
|
|
1844
1939
|
})();
|
|
1845
|
-
const
|
|
1940
|
+
const rowHoveredForCell = hoveredRowId === row.id;
|
|
1941
|
+
const baseRendered = col.render
|
|
1846
1942
|
? col.render(value, row)
|
|
1847
|
-
: renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale, openPreviewLabel,
|
|
1943
|
+
: renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale, openPreviewLabel, rowHoveredForCell);
|
|
1944
|
+
// crisp/#601 supersession follow-up: `onOpenComments`/
|
|
1945
|
+
// `onOpenPreview`/`commentCount` used to only do
|
|
1946
|
+
// anything inside `renderTyped`'s `"entity"` case,
|
|
1947
|
+
// which already renders its own actions inline (see
|
|
1948
|
+
// `renderCellActions` there) -- silently dropping them
|
|
1949
|
+
// on any OTHER type, and on a custom `render` of any
|
|
1950
|
+
// type, since neither path went through that switch.
|
|
1951
|
+
// This composes the same actions alongside whatever
|
|
1952
|
+
// `rendered` is instead, decoupling the props from
|
|
1953
|
+
// requiring `type: "entity"`.
|
|
1954
|
+
const cellActions = col.type !== "entity" &&
|
|
1955
|
+
(col.onOpenComments || col.onOpenPreview)
|
|
1956
|
+
? renderCellActions(col, row, rowHoveredForCell, openPreviewLabel)
|
|
1957
|
+
: null;
|
|
1958
|
+
const rendered = cellActions ? (_jsxs("span", { className: "crisp-datatable-cell-actions-row", children: [baseRendered, cellActions] })) : (baseRendered);
|
|
1848
1959
|
const isExpanded = expandedCell?.id === row.id &&
|
|
1849
1960
|
expandedCell?.key === col.key;
|
|
1850
1961
|
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 }))] }));
|
|
@@ -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
|
|
@@ -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; }
|