@crisp-ui-kit/crisp 0.54.9 → 0.54.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/datatable/datatable.js +175 -12
- package/dist/cjs/components/datatable/datatable.recipe.js +30 -0
- package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +159 -63
- package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +41 -9
- package/dist/cjs/components/text/text.recipe.js +17 -7
- package/dist/cjs/product/recordpage/column.js +19 -0
- package/dist/cjs/product/recordpage/recordpage.js +5 -4
- package/dist/cjs/product/recordpage/test-fixtures.js +55 -0
- package/dist/cjs/product/recordstable/index.js +1 -1
- package/dist/cjs/product/recordstable/recordstable.js +5 -0
- package/dist/cjs/product/savedviews/savedviews.recipe.js +13 -11
- package/dist/cjs/product/taskstable/taskstable.js +5 -0
- package/dist/datatable.css +17 -0
- package/dist/datatable.layered.css +17 -0
- package/dist/esm/components/datatable/datatable.js +175 -12
- package/dist/esm/components/datatable/datatable.recipe.js +30 -0
- package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +159 -63
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +41 -9
- package/dist/esm/components/text/text.recipe.js +17 -7
- package/dist/esm/product/recordpage/column.js +16 -0
- package/dist/esm/product/recordpage/recordpage.js +2 -1
- package/dist/esm/product/recordpage/test-fixtures.js +52 -0
- package/dist/esm/product/recordstable/index.js +1 -1
- package/dist/esm/product/recordstable/recordstable.js +5 -0
- package/dist/esm/product/savedviews/savedviews.recipe.js +13 -11
- package/dist/esm/product/taskstable/taskstable.js +5 -0
- package/dist/recordstable.css +19 -2
- package/dist/recordstable.layered.css +19 -2
- package/dist/savedviews.css +6 -18
- package/dist/savedviews.layered.css +6 -18
- package/dist/styles.css +78 -24
- package/dist/styles.layered.css +78 -24
- package/dist/tabletoolbar.css +35 -3
- package/dist/tabletoolbar.layered.css +35 -3
- package/dist/text.css +1 -1
- package/dist/text.layered.css +1 -1
- package/dist/types/components/datatable/datatable.d.ts +38 -3
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +26 -1
- package/dist/types/components/text/text.recipe.d.ts +1 -1
- package/dist/types/product/recordpage/column.d.ts +112 -0
- package/dist/types/product/recordpage/recordpage.d.ts +1 -1
- package/dist/types/product/recordpage/test-fixtures.d.ts +3 -0
- package/dist/types/product/recordpanel/recordpanel.d.ts +1 -1
- package/dist/types/product/recordstable/index.d.ts +1 -1
- package/dist/types/product/recordstable/recordstable.d.ts +6 -1
- package/dist/types/product/taskstable/taskstable.d.ts +5 -0
- package/package.json +1 -1
- /package/dist/cjs/product/{recordstable → recordpage}/editable-field.js +0 -0
- /package/dist/esm/product/{recordstable → recordpage}/editable-field.js +0 -0
- /package/dist/types/product/{recordstable → recordpage}/editable-field.d.ts +0 -0
|
@@ -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
|
@@ -90,6 +90,9 @@
|
|
|
90
90
|
& tbody tr[data-selected] td { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
|
|
91
91
|
& tbody tr[data-selected]:hover td { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface))); }
|
|
92
92
|
& .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
|
|
93
|
+
/* crisp/#758: AI-enriched column marker, ported from RecordsTable's own already-measured value -- a full-bleed #9b69ff (bgPurple) wash at 4%, composited OVER whatever row-state background the cell already has (hover, selected), which is why it's an ::after overlay and not a
|
|
94
|
+
plain background. */
|
|
95
|
+
& td[data-enriched]::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: color-mix(in srgb, var(--crisp-bg-purple) 4%, transparent); }
|
|
93
96
|
& .crisp-datatable-edit { width: 100%; height: 28px; margin: 0; padding: 0 var(--crisp-space-1-5); border: none; border-radius: var(--crisp-radius-sm); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); outline: none; vertical-align: middle; }
|
|
94
97
|
& .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
|
|
95
98
|
/* An async onEdit's in-flight state (#385) -- the field/picker dims and stops accepting input rather than disappearing, so the just-typed or just-chosen value stays visible while its commit is pending. Same treatment Input already gives its own disabled state. */
|
|
@@ -149,6 +152,10 @@
|
|
|
149
152
|
& th[data-drop-after]::after { inset-inline-end: -1.5px; }
|
|
150
153
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
151
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%; }
|
|
152
159
|
/* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text, plain hover vs rest) that Attio's underline is unconditional, not a
|
|
153
160
|
hover reveal -- identical at rest and on hover: text-decoration-color
|
|
154
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. */
|
|
@@ -180,6 +187,16 @@
|
|
|
180
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. */
|
|
181
188
|
& .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
182
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; }
|
|
183
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
|
|
184
201
|
hairline doubling as the drop indicator (it tints toward the brand
|
|
185
202
|
colour as the active drop target). The affordance + geometry are
|
|
@@ -91,6 +91,9 @@
|
|
|
91
91
|
& tbody tr[data-selected] td { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
|
|
92
92
|
& tbody tr[data-selected]:hover td { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface))); }
|
|
93
93
|
& .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
|
|
94
|
+
/* crisp/#758: AI-enriched column marker, ported from RecordsTable's own already-measured value -- a full-bleed #9b69ff (bgPurple) wash at 4%, composited OVER whatever row-state background the cell already has (hover, selected), which is why it's an ::after overlay and not a
|
|
95
|
+
plain background. */
|
|
96
|
+
& td[data-enriched]::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: color-mix(in srgb, var(--crisp-bg-purple) 4%, transparent); }
|
|
94
97
|
& .crisp-datatable-edit { width: 100%; height: 28px; margin: 0; padding: 0 var(--crisp-space-1-5); border: none; border-radius: var(--crisp-radius-sm); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); outline: none; vertical-align: middle; }
|
|
95
98
|
& .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
|
|
96
99
|
/* An async onEdit's in-flight state (#385) -- the field/picker dims and stops accepting input rather than disappearing, so the just-typed or just-chosen value stays visible while its commit is pending. Same treatment Input already gives its own disabled state. */
|
|
@@ -150,6 +153,10 @@
|
|
|
150
153
|
& th[data-drop-after]::after { inset-inline-end: -1.5px; }
|
|
151
154
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
152
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%; }
|
|
153
160
|
/* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text, plain hover vs rest) that Attio's underline is unconditional, not a
|
|
154
161
|
hover reveal -- identical at rest and on hover: text-decoration-color
|
|
155
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. */
|
|
@@ -181,6 +188,16 @@
|
|
|
181
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. */
|
|
182
189
|
& .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
183
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; }
|
|
184
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
|
|
185
202
|
hairline doubling as the drop indicator (it tints toward the brand
|
|
186
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);
|
|
@@ -573,6 +646,19 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
573
646
|
// arrow-key nav, Enter) is unaffected -- a plain click resets `selAnchor`
|
|
574
647
|
// to null, collapsing back to single-cell mode.
|
|
575
648
|
const [selAnchor, setSelAnchor] = React.useState(null);
|
|
649
|
+
// crisp/#749: Shift+ArrowUp/Down extends the ROW (checkbox) selection --
|
|
650
|
+
// confirmed live on Attio's own Companies grid, a DIFFERENT gesture from
|
|
651
|
+
// `selAnchor` above (that's Shift+CLICK extending a cell-value range;
|
|
652
|
+
// this issue's own text is explicit the two are independent, and arrow
|
|
653
|
+
// keys were never measured as a cell-range trigger, G-101). Same
|
|
654
|
+
// fixed-anchor/moving-cell shape as `selAnchor` -- a spreadsheet's own
|
|
655
|
+
// Shift+Arrow both moves the active cell AND grows/shrinks the selection
|
|
656
|
+
// relative to wherever the drag started, which recomputing the anchor→
|
|
657
|
+
// new-row span from scratch on every press gives for free (arrowing back
|
|
658
|
+
// toward the anchor shrinks the same way arrowing away grows it). Reset
|
|
659
|
+
// to null on any plain (non-shift) navigation or click, same as
|
|
660
|
+
// `selAnchor`'s own reset points.
|
|
661
|
+
const [rowSelectAnchor, setRowSelectAnchor] = React.useState(null);
|
|
576
662
|
const gridRef = React.useRef(null);
|
|
577
663
|
// The native-copy-event proxy (crisp/#645): Attio "has no Mousetrap/
|
|
578
664
|
// JS-keydown binding for c/mod+c ... it copies purely via the native
|
|
@@ -982,8 +1068,9 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
982
1068
|
* `editor: "select"`/`"reference"` open their own picker instead of this
|
|
983
1069
|
* native-input family, but through the SAME double-click gate (#453) --
|
|
984
1070
|
* see the row-render loop's `isEditing` branch for those, this function
|
|
985
|
-
* only covers the native-input-backed types.
|
|
986
|
-
*
|
|
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).
|
|
987
1074
|
*/
|
|
988
1075
|
function editorFor(col) {
|
|
989
1076
|
switch (col.type) {
|
|
@@ -1160,6 +1247,36 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1160
1247
|
setSelected(new Set(rows.map((rw) => rw.id)));
|
|
1161
1248
|
return;
|
|
1162
1249
|
}
|
|
1250
|
+
// crisp/#749: Shift+ArrowUp/Down extends the row-checkbox selection --
|
|
1251
|
+
// confirmed live, distinct from Shift+Click's cell-range gesture above
|
|
1252
|
+
// (that one is `selAnchor`; this is `rowSelectAnchor`, and the two
|
|
1253
|
+
// don't interact). Recomputes the whole anchor→destination span on
|
|
1254
|
+
// every press rather than accumulating, so arrowing back toward the
|
|
1255
|
+
// anchor shrinks the selection the same way arrowing away grows it --
|
|
1256
|
+
// the "shrink-from-the-anchor" behavior a spreadsheet's own Shift+Arrow
|
|
1257
|
+
// gives, which a plain add-on-each-press approach can't express.
|
|
1258
|
+
if (selectable &&
|
|
1259
|
+
e.shiftKey &&
|
|
1260
|
+
(e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
|
1261
|
+
const r0 = rows.findIndex((row) => row.id === selCell.id);
|
|
1262
|
+
if (r0 < 0)
|
|
1263
|
+
return;
|
|
1264
|
+
const nr0 = e.key === "ArrowDown"
|
|
1265
|
+
? Math.min(rows.length - 1, r0 + 1)
|
|
1266
|
+
: Math.max(0, r0 - 1);
|
|
1267
|
+
e.preventDefault();
|
|
1268
|
+
const anchorId = rowSelectAnchor ?? selCell.id;
|
|
1269
|
+
const anchorIdx = rows.findIndex((row) => row.id === anchorId);
|
|
1270
|
+
if (anchorIdx < 0)
|
|
1271
|
+
return;
|
|
1272
|
+
setRowSelectAnchor(anchorId);
|
|
1273
|
+
const lo = Math.min(anchorIdx, nr0);
|
|
1274
|
+
const hi = Math.max(anchorIdx, nr0);
|
|
1275
|
+
setSelected(new Set(rows.slice(lo, hi + 1).map((row) => row.id)));
|
|
1276
|
+
setSelCell({ id: rows[nr0].id, key: selCell.key });
|
|
1277
|
+
clipboardProxyRef.current?.select();
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1163
1280
|
const r = rows.findIndex((row) => row.id === selCell.id);
|
|
1164
1281
|
const c = columns.findIndex((col) => col.key === selCell.key);
|
|
1165
1282
|
if (r < 0 || c < 0)
|
|
@@ -1198,6 +1315,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1198
1315
|
else if (e.key === "Escape") {
|
|
1199
1316
|
setSelCell(null);
|
|
1200
1317
|
setSelAnchor(null);
|
|
1318
|
+
setRowSelectAnchor(null);
|
|
1201
1319
|
return;
|
|
1202
1320
|
}
|
|
1203
1321
|
else if (e.key === "Enter") {
|
|
@@ -1205,8 +1323,13 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1205
1323
|
// already its own live widget (#385); Enter there is a no-op, same
|
|
1206
1324
|
// as any other non-editable cell that isn't wired to onRowClick.
|
|
1207
1325
|
// `select`/`reference` DO have one now (like every other editor),
|
|
1208
|
-
// so Enter opens them too.
|
|
1209
|
-
|
|
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") {
|
|
1210
1333
|
e.preventDefault();
|
|
1211
1334
|
openEditing(selCell.id, selCell.key);
|
|
1212
1335
|
}
|
|
@@ -1222,8 +1345,10 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1222
1345
|
// A plain arrow key moves the single-cell selection and drops any active
|
|
1223
1346
|
// range -- Attio's own range trigger is Shift+Click only (G-101), arrow
|
|
1224
1347
|
// keys were never measured as a range-extend gesture, so this doesn't
|
|
1225
|
-
// invent one.
|
|
1348
|
+
// invent one. Also drops an in-progress Shift+Arrow row-selection drag
|
|
1349
|
+
// (crisp/#749) for the same reason -- a plain arrow ends that gesture.
|
|
1226
1350
|
setSelAnchor(null);
|
|
1351
|
+
setRowSelectAnchor(null);
|
|
1227
1352
|
setSelCell({ id: rows[nr].id, key: columns[nc].key });
|
|
1228
1353
|
// keeps the clipboard proxy's selection non-collapsed after a move, so
|
|
1229
1354
|
// a Cmd+C right after an arrow key still has something native `copy`
|
|
@@ -1741,6 +1866,22 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1741
1866
|
if (col.editable && col.editor === "switch") {
|
|
1742
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));
|
|
1743
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
|
+
}
|
|
1744
1885
|
if (isEditing &&
|
|
1745
1886
|
(col.editor === "select" ||
|
|
1746
1887
|
col.editor === "reference" ||
|
|
@@ -1796,9 +1937,25 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1796
1937
|
i >= range.c0 &&
|
|
1797
1938
|
i <= range.c1);
|
|
1798
1939
|
})();
|
|
1799
|
-
const
|
|
1940
|
+
const rowHoveredForCell = hoveredRowId === row.id;
|
|
1941
|
+
const baseRendered = col.render
|
|
1800
1942
|
? col.render(value, row)
|
|
1801
|
-
: 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);
|
|
1802
1959
|
const isExpanded = expandedCell?.id === row.id &&
|
|
1803
1960
|
expandedCell?.key === col.key;
|
|
1804
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 }))] }));
|
|
@@ -1806,7 +1963,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1806
1963
|
? "crisp-datatable-editable"
|
|
1807
1964
|
: undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
|
|
1808
1965
|
? ""
|
|
1809
|
-
: undefined, "data-in-range": inRange ? "" : undefined, "data-cell-expanded": isExpanded ? "" : undefined, onClick: (e) => {
|
|
1966
|
+
: undefined, "data-in-range": inRange ? "" : undefined, "data-cell-expanded": isExpanded ? "" : undefined, "data-enriched": col.enriched ? "" : undefined, onClick: (e) => {
|
|
1810
1967
|
// the keyboard model's anchor -- a single click
|
|
1811
1968
|
// only selects. A genuine double-click (or Enter
|
|
1812
1969
|
// on the selected cell) opens the editor (#411),
|
|
@@ -1821,6 +1978,11 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1821
1978
|
}
|
|
1822
1979
|
else {
|
|
1823
1980
|
setSelAnchor(null);
|
|
1981
|
+
// crisp/#749: a plain click also ends an
|
|
1982
|
+
// in-progress Shift+Arrow row-selection drag
|
|
1983
|
+
// -- Shift+Click itself doesn't touch it,
|
|
1984
|
+
// the two gestures are independent.
|
|
1985
|
+
setRowSelectAnchor(null);
|
|
1824
1986
|
}
|
|
1825
1987
|
setSelCell({ id: row.id, key: col.key });
|
|
1826
1988
|
// Focuses the clipboard proxy, not `gridRef`
|
|
@@ -1861,6 +2023,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
1861
2023
|
// selected (crisp/#598) -- collapses any
|
|
1862
2024
|
// active range, same as a plain left-click.
|
|
1863
2025
|
setSelAnchor(null);
|
|
2026
|
+
setRowSelectAnchor(null);
|
|
1864
2027
|
setSelCell({ id: row.id, key: col.key });
|
|
1865
2028
|
clipboardProxyRef.current?.focus();
|
|
1866
2029
|
clipboardProxyRef.current?.select();
|
|
@@ -135,6 +135,12 @@ const config = defineRecipe({
|
|
|
135
135
|
& tbody tr[data-selected] td { background: color-mix(in srgb, ${c.bgBrandSolid} 10%, ${c.bgSurface}); }
|
|
136
136
|
& tbody tr[data-selected]:hover td { background: color-mix(in srgb, ${c.fgPrimary} 4%, color-mix(in srgb, ${c.bgBrandSolid} 10%, ${c.bgSurface})); }
|
|
137
137
|
& .crisp-datatable-editable:hover { background: color-mix(in srgb, ${c.fgPrimary} 4%, ${c.bgSurface}); cursor: text; }
|
|
138
|
+
/* crisp/#758: AI-enriched column marker, ported from RecordsTable's own
|
|
139
|
+
already-measured value -- a full-bleed #9b69ff (bgPurple) wash at 4%,
|
|
140
|
+
composited OVER whatever row-state background the cell already has
|
|
141
|
+
(hover, selected), which is why it's an ::after overlay and not a
|
|
142
|
+
plain background. */
|
|
143
|
+
& td[data-enriched]::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: color-mix(in srgb, ${c.bgPurple} 4%, transparent); }
|
|
138
144
|
& .crisp-datatable-edit { width: 100%; height: 28px; margin: 0; padding: 0 ${c.space["1.5"]}; border: none; border-radius: ${c.radius.sm}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; outline: none; vertical-align: middle; }
|
|
139
145
|
& .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; }
|
|
140
146
|
/* An async onEdit's in-flight state (#385) -- the field/picker dims and
|
|
@@ -218,6 +224,13 @@ const config = defineRecipe({
|
|
|
218
224
|
& th[data-drop-after]::after { inset-inline-end: -1.5px; }
|
|
219
225
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
220
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%; }
|
|
221
234
|
/* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text,
|
|
222
235
|
plain hover vs rest) that Attio's underline is unconditional, not a
|
|
223
236
|
hover reveal -- identical at rest and on hover: text-decoration-color
|
|
@@ -259,6 +272,23 @@ const config = defineRecipe({
|
|
|
259
272
|
link. */
|
|
260
273
|
& .crisp-datatable-link { color: ${c.fgPrimary}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
261
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; }
|
|
262
292
|
/* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
|
|
263
293
|
a leading 20px grip column, a 16px grip icon, and the row's own
|
|
264
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; }
|