@crisp-ui-kit/crisp 0.55.1 → 0.55.3

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.
@@ -40,54 +40,58 @@
40
40
  class + one element) -- a bare `& th` here only won by source
41
41
  order, which flipped the moment table.recipe.ts's own CSS file happened to land after datatable's in the build (alphabetical:
42
42
  datatable < table), silently reinstating 12px on every header cell
43
- in the whole product and making every interactive column's button double it to 24px. Scoping through `.crisp-table` adds a second class to the selector (0,2,1 beats 0,1,1) so this can never lose a specificity tie to source order again. */
44
- & .crisp-table th { padding: 0; }
45
- /* Same wrapper trap as AUTHORING.md Lesson 40 (ColumnCalc's summary trigger):
46
- Menu wraps its trigger in a flex div that sizes to its content, so a
47
- long header grew past the column instead of filling it -- with nothing
48
- left to squeeze, the label's own ellipsis CSS (below) had no properly -sized box to truncate against, and the th's own overflow hard-clipped
49
- it with no "...". Fill the wrapper, scoped to th so Menu's default (correct everywhere else) is untouched. */
43
+ in the whole product and making every interactive column's button double it to 24px. Scoping through `.crisp-table` adds a second class to the selector (0,2,1 beats 0,1,1) so this can never lose a specificity tie to source order again. `thead th`, not a bare `th` (crisp#829): #779 made the primary column's BODY cell a real `<th scope="row">` for a11y, and a bare
44
+ `th` here matches that one too, zeroing an inset nothing else
45
+ supplies -- the primary column's text sat flush left, one column's
46
+ cell no longer matching its own header or any sibling `<td>`. Only
47
+ the header row's own `<th>` needs zero padding (its sort BUTTON owns the inset); the body row's `<th scope="row">` has no button,
48
+ and needs the SAME base 12px (table.recipe.ts, #465) every other
49
+ cell already gets. */
50
+ & .crisp-table thead th { padding: 0; }
51
+ /* Same wrapper trap as AUTHORING.md Lesson 40 (ColumnCalc's summary trigger): Menu wraps its trigger in a flex div that sizes to its content, so a long header grew past the column instead of filling it -- with nothing left to squeeze, the label's own ellipsis CSS (below) had no properly
52
+ -sized box to truncate against, and the th's own overflow hard-clipped it with no "...". Fill the wrapper, scoped to th so Menu's default
53
+ (correct everywhere else) is untouched. */
50
54
  & th .crisp-menu { width: 100%; height: 100%; }
51
55
  & .crisp-datatable-th-inner { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; min-width: 0; height: 100%; }
52
56
  & .crisp-datatable-th-inner > svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
53
- /* crisp/#605 follow-up: a header with neither a Menu nor a sort button (the primary/entity column's now-inert path, or any column with no
54
- columnMenu/sortable at all) renders .crisp-datatable-th-inner as a
55
- DIRECT child of th instead of nested inside a button -- and loses
56
- the button's own 12px inset, drifting left of the td's 12px base
57
- inset. Measured live on Attio's own Companies grid: the header label and the cell's own text land at the exact same x (335px from the
58
- viewport, both 40px from the column's left edge) via two different padding+icon+gap combinations -- Attio does not leave the inert primary header flush left either. The `>` combinator targets only the button-less case: Menu's own wrapper (`.crisp-menu`) and the
59
- plain sort `button` both put >=1 more level between `th` and this
60
- span, so their own 12px (already on the button) isn't doubled. */
57
+ /* crisp/#605 follow-up: a header with neither a Menu nor a sort button
58
+ (the primary/entity column's now-inert path, or any column with no columnMenu/sortable at all) renders .crisp-datatable-th-inner as a DIRECT child of th instead of nested inside a button -- and loses the button's own 12px inset, drifting left of the td's 12px base inset. Measured live on Attio's own Companies grid: the header label
59
+ and the cell's own text land at the exact same x (335px from the viewport, both 40px from the column's left edge) via two different
60
+ padding+icon+gap combinations -- Attio does not leave the inert
61
+ primary header flush left either. The `>` combinator targets only
62
+ the button-less case: Menu's own wrapper (`.crisp-menu`) and the plain sort `button` both put >=1 more level between `th` and this span, so their own 12px (already on the button) isn't doubled. */
61
63
  & th > .crisp-datatable-th-inner { box-sizing: border-box; padding: 0 var(--crisp-space-3); }
62
- /* The summary cell has NO padding of its own -- the calculation trigger owns the whole 246x37 area so a click anywhere in the cell opens the menu. Zero the cell's inset and let ColumnCalc supply the 12px. */
64
+ /* The summary cell has NO padding of its own -- the calculation trigger
65
+ owns the whole 246x37 area so a click anywhere in the cell opens the
66
+ menu. Zero the cell's inset and let ColumnCalc supply the 12px. */
63
67
  & tfoot td { padding: 0; }
64
- /* Group heading row -- a view is grouped under a subtle label with the
65
- bucket's count beside it. */
66
- & .crisp-datatable-viewsettings { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; color: var(--crisp-fg-muted); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s, color .12s; }
68
+ /* Group heading row -- a view is grouped under a subtle label with the bucket's count beside it. */
69
+ & .crisp-datatable-viewsettings { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; color: var(--crisp-fg-muted); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s, color .12s; }
67
70
  & .crisp-datatable-viewsettings:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
68
71
  & .crisp-datatable-group-label { padding: var(--crisp-space-1-5) var(--crisp-space-2) var(--crisp-space-1); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
69
- & .crisp-datatable-group { height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-3); background: var(--crisp-bg-subtle); border-bottom: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
72
+ & .crisp-datatable-group { height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-3); background: var(--crisp-bg-subtle); border-bottom: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
70
73
  & .crisp-datatable-group-count { margin-inline-start: var(--crisp-space-1-5); color: var(--crisp-fg-muted); font-variant-numeric: tabular-nums; }
71
74
  & .crisp-datatable-th-rename { width: 100%; min-width: 0; height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-1); border: none; border-radius: var(--crisp-radius-md); 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); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); outline: none; }
72
75
  & .crisp-datatable-th-rename:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
73
76
  & .crisp-datatable-th-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
74
- /* #465: 12px per the comment above (and Attio's measured header cell
75
- padding: 0 12px) -- was still c.space["2"] (8px), drifted from its
76
- own documented value. */
77
+ /* #465: 12px per the comment above (and Attio's measured header cell padding: 0 12px) -- was still c.space["2"] (8px), drifted from its own documented value. */
77
78
  & .crisp-datatable-sort { display: flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; min-width: 0; height: 100%; margin: 0; padding: 0 var(--crisp-space-3); border: none; background: none; font: inherit; letter-spacing: inherit; color: inherit; text-align: start; cursor: pointer; }
78
- /* A right-aligned column's header keeps its field-icon + label packed at the START, matching its own values only in that the VALUES right-align -- Attio's own right-aligned header (measured live: "Twitter follower
79
- count") keeps the icon+label left and only the trailing column-menu
80
- icon sits pinned to the right edge, independent of the label's width (#406, reversing #393's own justify-content: flex-end here). The
81
- chevron's own margin-inline-start: auto below already pins IT to the end of a flex-start row with no override needed -- that's what pins
82
- the trailing icon on Attio too. */
79
+ /* A right-aligned column's header keeps its field-icon + label packed at
80
+ the START, matching its own values only in that the VALUES right-align
81
+ -- Attio's own right-aligned header (measured live: "Twitter follower
82
+ count") keeps the icon+label left and only the trailing column-menu icon sits pinned to the right edge, independent of the label's width
83
+ (#406, reversing #393's own justify-content: flex-end here). The chevron's own margin-inline-start: auto below already pins IT to the
84
+ end of a flex-start row with no override needed -- that's what pins the trailing icon on Attio too. */
83
85
  & .crisp-datatable-chevron { margin-inline-start: auto; flex-shrink: 0; color: var(--crisp-fg-muted); opacity: 0; transition: opacity .12s; }
84
86
  & .crisp-datatable-sort:hover .crisp-datatable-chevron { opacity: 0.6; }
85
87
  & .crisp-datatable-sort[data-sorted] .crisp-datatable-chevron { opacity: 1; color: var(--crisp-fg-primary); }
86
88
  /* sticky first column(s) + horizontal scroll shadow — RecordsTable's feel */
87
89
  & th[data-sticky], & td[data-sticky] { position: sticky; z-index: 1; background: var(--crisp-bg-surface); }
88
- & th[data-sticky] { z-index: 3; }
89
- /* #554: a footer sticky-column cell sits in the bottom-sticky row (table.recipe.ts's
90
- `tfoot td { z-index: 2 }`), which `td[data-sticky]`'s own higher-specificity `z-index: 1` would otherwise undercut -- keep it level with the row it's in. */
90
+ & th[data-sticky] { z-index: 3;
91
+ } /* #554: a footer sticky-column cell sits in the bottom-sticky row (table.recipe.ts's
92
+ `tfoot td { z-index: 2 }
93
+ `), which `td[data-sticky]`'s own higher-specificity
94
+ `z-index: 1` would otherwise undercut -- keep it level with the row it's in. */
91
95
  & tfoot td[data-sticky] { z-index: 2; }
92
96
  & tbody tr[data-selected] td[data-sticky] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
93
97
  & tbody tr:hover td[data-sticky] { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); }
@@ -104,36 +108,41 @@
104
108
  & tbody tr[data-selected] td { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
105
109
  & 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))); }
106
110
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
107
- /* crisp/#758: AI-enriched column marker, ported from RecordsTable's own
108
- already-measured value -- a full-bleed #9b69ff (bgPurple) wash at 4%,
109
- composited OVER whatever row-state background the cell already has
110
- (hover, selected), which is why it's an ::after overlay and not a plain background. */
111
+ /* 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
112
+ plain background. */
111
113
  & td[data-enriched]::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: color-mix(in srgb, var(--crisp-bg-purple) 4%, transparent); }
112
114
  & .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; }
113
115
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
114
- /* An async onEdit's in-flight state (#385) -- the field/picker dims and
115
- stops accepting input rather than disappearing, so the just-typed or
116
- just-chosen value stays visible while its commit is pending. Same
117
- treatment Input already gives its own disabled state. */
116
+ /* 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. */
118
117
  & .crisp-datatable-edit:disabled { opacity: 0.5; cursor: not-allowed; }
119
118
  & .crisp-datatable-edit-pending { flex-shrink: 0; margin-inline-start: var(--crisp-space-1); color: var(--crisp-fg-muted); vertical-align: middle; }
120
- /* The rejection's message, beside the cell's own content -- a sibling in
121
- the TableCell, never nested inside a column's own render output, so a hover-only wrapper (a Tooltip, say) around a custom render can't hide
122
- it. Block-level: the row grows to fit it rather than floating over
123
- the row below, so it can never end up clipped by the scroll
124
- container's overflow the way a position:fixed escape would need guarding against. */
119
+ /* The rejection's message, beside the cell's own content -- a sibling in the TableCell, never nested inside a column's own render output, so a
120
+ hover-only wrapper (a Tooltip, say) around a custom render can't hide it. Block-level: the row grows to fit it rather than floating over the row below, so it can never end up clipped by the scroll container's overflow the way a position:fixed escape would need
121
+ guarding against. */
125
122
  & .crisp-datatable-edit-error { display: block; padding: var(--crisp-space-0-5) 0 0; font-size: var(--crisp-text-sm); color: var(--crisp-bg-danger); }
126
- /* editor: "select"/"switch" -- always live, no separate edit mode, so the cell IS the picker: same chromeless-fill treatment as RecordsTable's own --picker cells (measured there already; DataTable
127
- shares RecordsTable's grid, Golden Rule 0). */
123
+ /* editor: "select"/"switch" -- always live, no separate edit mode, so
124
+ the cell IS the picker: same chromeless-fill treatment as
125
+ RecordsTable's own --picker cells (measured there already; DataTable shares RecordsTable's grid, Golden Rule 0). */
128
126
  & .crisp-datatable-td--picker { padding: 0; }
129
127
  & .crisp-datatable-edit-picker { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
130
128
  & .crisp-datatable-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; height: 100%; min-height: 0; border-radius: 0; box-shadow: none; padding: 0; }
131
129
  & .crisp-datatable-td--picker .crisp-datatable-edit-error { padding-inline-start: var(--crisp-space-2); }
132
- /* reference picker: a leading 16px avatar per option, plus a trailing "Create X" row when the consumer supplies one (#449) -- same measured values as RecordsTable's own .crisp-recordstable-refoption*. */
130
+ /* reference picker: a leading 16px avatar per option, plus a trailing
131
+ "Create X" row when the consumer supplies one (#449) -- same measured
132
+ values as RecordsTable's own .crisp-recordstable-refoption*. */
133
133
  & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
134
134
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
135
135
  & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
136
136
  & .crisp-datatable-empty { padding: var(--crisp-space-8) var(--crisp-space-3); text-align: center; color: var(--crisp-fg-muted); font-size: var(--crisp-text-lg); }
137
+ /* #826 -- the full-body loading state (a filter/sort re-query, or a fresh page boot), measured live on Attio's Companies grid on both
138
+ triggers: icon+text centred in the row-body area, 10px gap, 14px/500
139
+ weight/20px line-height, both icon and text in the SAME muted
140
+ rgba(0,0,0,0.63) -- the same color-mix figure this file already uses
141
+ for other muted text (e.g. -phone-dial). Distinct from the cold
142
+ in-place row load (blank, no indicator) and the warm-navigation
143
+ per-cell shimmer (#599) -- neither of those swaps the whole body. */
144
+ & .crisp-datatable-loading { display: flex; align-items: center; justify-content: center; gap: 10px; padding: var(--crisp-space-8) var(--crisp-space-3); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); line-height: 20px; }
145
+ & .crisp-datatable-loading .crisp-spinner { border-top-color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
137
146
  /* #579: click-to-expand overlay for a truncated cell -- a read-only,
138
147
  full-value card, settled live as click (not Attio's usual hover-expand) and measured position: absolute, unlike this file's
139
148
  floating chrome (Menu/Select popovers) which portals and escapes the
@@ -208,6 +217,9 @@
208
217
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
209
218
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
210
219
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
220
+ /* crisp#822 -- a date/datetime cell's danger/overdue tone: same token as
221
+ TasksTable's own overdue due-date (crisp#462), plain text colour, no fill/weight change. */
222
+ & .crisp-datatable-danger { color: var(--crisp-fg-danger); }
211
223
  /* url/email cell types (#654) -- same measured values as RecordsTable's
212
224
  own .crisp-recordstable-link, which this mirrors: no underline at
213
225
  rest, underline on hover, own-colour text rather than a browser-blue
@@ -221,26 +233,42 @@
221
233
  & .crisp-datatable-phone-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
222
234
  & .crisp-datatable-phone-editor { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
223
235
  & .crisp-datatable-phone-editor input { flex: 1; min-width: 0; }
236
+ /* country-picker option row (#842) -- confirmed live: Attio's own popover is plain text, no flag anywhere (not the option row, not the closed trigger), name opaque + dial code muted rgba(0,0,0,0.63) -- the SAME color-mix figure as -phone-dial above. The internal name/dial gap wasn't independently measured this pass (Attio's own two <div>s gave no exact number); reused space["1"], the same gap -phone already uses for its own flag/dial/digits row. */
237
+ & .crisp-datatable-country-option { display: flex; align-items: center; gap: var(--crisp-space-1); }
238
+ & .crisp-datatable-country-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
239
+ /* multi-value phone (#816) -- confirmed live (a real custom "Multiple
240
+ values" Phone Number attribute, and the built-in People "Phone
241
+ numbers" field): the collapsed cell is a 4px-gap flex row of real tel: links in a measured link blue (~rgb(59,108,232) pixel-sampled; reused as the closest already-tokened blue, brand600, rather than adding a third blue token for a delta this small), underlined, UNLIKE the single-value cell above (plain text, no link, no underline) -- multi-value is the one shape Attio makes into a real link. The cell hard-clips on overflow (no ellipsis -- confirmed live, a second value can end mid-digit with no "..."). */
242
+ & .crisp-datatable-phone-multi { display: inline-flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
243
+ & .crisp-datatable-phone-link { color: var(--crisp-bg-brand-solid); text-decoration: underline; white-space: nowrap; }
244
+ /* multi-value editor (#816) -- confirmed live: a stack of committed- value rows (32px = size.menuRow, 5px 8px padding, 8px gap between the grip/value/actions, 16px icons) above one ALWAYS-blank trailing DataPhoneEditor-shaped row -- that blank row is the only "add
245
+ another" affordance, there's no separate button for it. The
246
+ copy/delete pair reuses .crisp-datatable-entity-action's own hover-reveal chrome (not independently re-measured as its own hit target this pass, only the 16px icon glyphs were). */
247
+ & .crisp-datatable-phone-multi-editor { display: flex; flex-direction: column; }
248
+ & .crisp-datatable-phone-multi-row { display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); padding: 5px var(--crisp-space-2); }
249
+ & .crisp-datatable-phone-multi-row .crisp-datatable-phone-link { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
250
+ & .crisp-datatable-phone-multi-grip { color: var(--crisp-fg-muted); flex-shrink: 0; cursor: grab; }
251
+ & .crisp-datatable-phone-multi-actions { display: flex; align-items: center; gap: var(--crisp-space-1); margin-inline-start: auto; flex-shrink: 0; opacity: 0; }
252
+ & .crisp-datatable-phone-multi-row:hover .crisp-datatable-phone-multi-actions, & .crisp-datatable-phone-multi-actions:focus-within { opacity: 1; }
253
+ & .crisp-datatable-phone-multi-action { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; font-family: inherit; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); cursor: pointer; flex-shrink: 0; }
254
+ & .crisp-datatable-phone-multi-action:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
224
255
  /* datetime cell editor (#762) -- DatePickerField + TimePicker side by side. */
225
256
  & .crisp-datatable-datetime-editor { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
226
- /* rating cell type/editor (#760) -- measured live: 16px stars on an
227
- 18px pitch (a 2px gap), an empty star at rgba(0,0,0,0.1), a
228
- committed/filled star at brand600 (--crisp-bg-brand-solid, an exact
229
- match to the measured #266df0), and a distinct hover-preview colour
230
- -- rgb(33,91,196) -- that's close to but NOT brand700/bgBrandSolidPressed (#1f5fd6); kept as its own literal rather than forced onto that token, since the two were measured stable and different. The read-only svg and the editable button's own inner svg both carry the
231
- crisp-datatable-rating-star class on the svg itself (not the
232
- button), so one set of fill rules covers both -- the button only
233
- adds its own reset + cursor via a separate -btn class. */
257
+ /* 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
258
+ (#1f5fd6); kept as its own literal rather than forced onto that token,
259
+ since the two were measured stable and different. The read-only svg
260
+ 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. */
234
261
  & .crisp-datatable-rating { display: inline-flex; align-items: center; gap: 2px; }
235
262
  & .crisp-datatable-rating-star { display: block; }
236
263
  & .crisp-datatable-rating-star path { fill: rgba(0, 0, 0, 0.1); }
237
264
  & .crisp-datatable-rating-star[data-filled] path { fill: var(--crisp-bg-brand-solid); }
238
265
  & .crisp-datatable-rating-star[data-hovering] path { fill: #215bc4; }
239
266
  & .crisp-datatable-rating-star-btn { display: inline-flex; padding: 0; margin: 0; border: none; background: none; font-family: inherit; cursor: pointer; }
240
- /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
241
- a leading 20px grip column, a 16px grip icon, and the row's own hairline doubling as the drop indicator (it tints toward the brand colour as the active drop target). The affordance + geometry are measured; the drag MECHANISM is crisp's existing house idiom -- HTML5
242
- draggable + dataTransfer, the same one column reorder already uses
243
- (RecordsTable's reorderColumn) -- not a proprietary pointer-events implementation. */
267
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
268
+ hairline doubling as the drop indicator (it tints toward the brand
269
+ colour as the active drop target). The affordance + geometry are
270
+ measured; the drag MECHANISM is crisp's existing house idiom -- HTML5 draggable + dataTransfer, the same one column reorder already uses (RecordsTable's reorderColumn) -- not a proprietary
271
+ pointer-events implementation. */
244
272
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
245
273
  & .crisp-datatable-grip { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; margin: 0; border: none; border-radius: 0; background: none; font-family: inherit; color: var(--crisp-fg-muted); cursor: grab; }
246
274
  & .crisp-datatable-grip:active { cursor: grabbing; }
@@ -41,54 +41,58 @@
41
41
  class + one element) -- a bare `& th` here only won by source
42
42
  order, which flipped the moment table.recipe.ts's own CSS file happened to land after datatable's in the build (alphabetical:
43
43
  datatable < table), silently reinstating 12px on every header cell
44
- in the whole product and making every interactive column's button double it to 24px. Scoping through `.crisp-table` adds a second class to the selector (0,2,1 beats 0,1,1) so this can never lose a specificity tie to source order again. */
45
- & .crisp-table th { padding: 0; }
46
- /* Same wrapper trap as AUTHORING.md Lesson 40 (ColumnCalc's summary trigger):
47
- Menu wraps its trigger in a flex div that sizes to its content, so a
48
- long header grew past the column instead of filling it -- with nothing
49
- left to squeeze, the label's own ellipsis CSS (below) had no properly -sized box to truncate against, and the th's own overflow hard-clipped
50
- it with no "...". Fill the wrapper, scoped to th so Menu's default (correct everywhere else) is untouched. */
44
+ in the whole product and making every interactive column's button double it to 24px. Scoping through `.crisp-table` adds a second class to the selector (0,2,1 beats 0,1,1) so this can never lose a specificity tie to source order again. `thead th`, not a bare `th` (crisp#829): #779 made the primary column's BODY cell a real `<th scope="row">` for a11y, and a bare
45
+ `th` here matches that one too, zeroing an inset nothing else
46
+ supplies -- the primary column's text sat flush left, one column's
47
+ cell no longer matching its own header or any sibling `<td>`. Only
48
+ the header row's own `<th>` needs zero padding (its sort BUTTON owns the inset); the body row's `<th scope="row">` has no button,
49
+ and needs the SAME base 12px (table.recipe.ts, #465) every other
50
+ cell already gets. */
51
+ & .crisp-table thead th { padding: 0; }
52
+ /* Same wrapper trap as AUTHORING.md Lesson 40 (ColumnCalc's summary trigger): Menu wraps its trigger in a flex div that sizes to its content, so a long header grew past the column instead of filling it -- with nothing left to squeeze, the label's own ellipsis CSS (below) had no properly
53
+ -sized box to truncate against, and the th's own overflow hard-clipped it with no "...". Fill the wrapper, scoped to th so Menu's default
54
+ (correct everywhere else) is untouched. */
51
55
  & th .crisp-menu { width: 100%; height: 100%; }
52
56
  & .crisp-datatable-th-inner { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; min-width: 0; height: 100%; }
53
57
  & .crisp-datatable-th-inner > svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
54
- /* crisp/#605 follow-up: a header with neither a Menu nor a sort button (the primary/entity column's now-inert path, or any column with no
55
- columnMenu/sortable at all) renders .crisp-datatable-th-inner as a
56
- DIRECT child of th instead of nested inside a button -- and loses
57
- the button's own 12px inset, drifting left of the td's 12px base
58
- inset. Measured live on Attio's own Companies grid: the header label and the cell's own text land at the exact same x (335px from the
59
- viewport, both 40px from the column's left edge) via two different padding+icon+gap combinations -- Attio does not leave the inert primary header flush left either. The `>` combinator targets only the button-less case: Menu's own wrapper (`.crisp-menu`) and the
60
- plain sort `button` both put >=1 more level between `th` and this
61
- span, so their own 12px (already on the button) isn't doubled. */
58
+ /* crisp/#605 follow-up: a header with neither a Menu nor a sort button
59
+ (the primary/entity column's now-inert path, or any column with no columnMenu/sortable at all) renders .crisp-datatable-th-inner as a DIRECT child of th instead of nested inside a button -- and loses the button's own 12px inset, drifting left of the td's 12px base inset. Measured live on Attio's own Companies grid: the header label
60
+ and the cell's own text land at the exact same x (335px from the viewport, both 40px from the column's left edge) via two different
61
+ padding+icon+gap combinations -- Attio does not leave the inert
62
+ primary header flush left either. The `>` combinator targets only
63
+ the button-less case: Menu's own wrapper (`.crisp-menu`) and the plain sort `button` both put >=1 more level between `th` and this span, so their own 12px (already on the button) isn't doubled. */
62
64
  & th > .crisp-datatable-th-inner { box-sizing: border-box; padding: 0 var(--crisp-space-3); }
63
- /* The summary cell has NO padding of its own -- the calculation trigger owns the whole 246x37 area so a click anywhere in the cell opens the menu. Zero the cell's inset and let ColumnCalc supply the 12px. */
65
+ /* The summary cell has NO padding of its own -- the calculation trigger
66
+ owns the whole 246x37 area so a click anywhere in the cell opens the
67
+ menu. Zero the cell's inset and let ColumnCalc supply the 12px. */
64
68
  & tfoot td { padding: 0; }
65
- /* Group heading row -- a view is grouped under a subtle label with the
66
- bucket's count beside it. */
67
- & .crisp-datatable-viewsettings { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; color: var(--crisp-fg-muted); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s, color .12s; }
69
+ /* Group heading row -- a view is grouped under a subtle label with the bucket's count beside it. */
70
+ & .crisp-datatable-viewsettings { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; color: var(--crisp-fg-muted); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s, color .12s; }
68
71
  & .crisp-datatable-viewsettings:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
69
72
  & .crisp-datatable-group-label { padding: var(--crisp-space-1-5) var(--crisp-space-2) var(--crisp-space-1); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
70
- & .crisp-datatable-group { height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-3); background: var(--crisp-bg-subtle); border-bottom: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
73
+ & .crisp-datatable-group { height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-3); background: var(--crisp-bg-subtle); border-bottom: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
71
74
  & .crisp-datatable-group-count { margin-inline-start: var(--crisp-space-1-5); color: var(--crisp-fg-muted); font-variant-numeric: tabular-nums; }
72
75
  & .crisp-datatable-th-rename { width: 100%; min-width: 0; height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-1); border: none; border-radius: var(--crisp-radius-md); 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); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); outline: none; }
73
76
  & .crisp-datatable-th-rename:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
74
77
  & .crisp-datatable-th-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
75
- /* #465: 12px per the comment above (and Attio's measured header cell
76
- padding: 0 12px) -- was still c.space["2"] (8px), drifted from its
77
- own documented value. */
78
+ /* #465: 12px per the comment above (and Attio's measured header cell padding: 0 12px) -- was still c.space["2"] (8px), drifted from its own documented value. */
78
79
  & .crisp-datatable-sort { display: flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; min-width: 0; height: 100%; margin: 0; padding: 0 var(--crisp-space-3); border: none; background: none; font: inherit; letter-spacing: inherit; color: inherit; text-align: start; cursor: pointer; }
79
- /* A right-aligned column's header keeps its field-icon + label packed at the START, matching its own values only in that the VALUES right-align -- Attio's own right-aligned header (measured live: "Twitter follower
80
- count") keeps the icon+label left and only the trailing column-menu
81
- icon sits pinned to the right edge, independent of the label's width (#406, reversing #393's own justify-content: flex-end here). The
82
- chevron's own margin-inline-start: auto below already pins IT to the end of a flex-start row with no override needed -- that's what pins
83
- the trailing icon on Attio too. */
80
+ /* A right-aligned column's header keeps its field-icon + label packed at
81
+ the START, matching its own values only in that the VALUES right-align
82
+ -- Attio's own right-aligned header (measured live: "Twitter follower
83
+ count") keeps the icon+label left and only the trailing column-menu icon sits pinned to the right edge, independent of the label's width
84
+ (#406, reversing #393's own justify-content: flex-end here). The chevron's own margin-inline-start: auto below already pins IT to the
85
+ end of a flex-start row with no override needed -- that's what pins the trailing icon on Attio too. */
84
86
  & .crisp-datatable-chevron { margin-inline-start: auto; flex-shrink: 0; color: var(--crisp-fg-muted); opacity: 0; transition: opacity .12s; }
85
87
  & .crisp-datatable-sort:hover .crisp-datatable-chevron { opacity: 0.6; }
86
88
  & .crisp-datatable-sort[data-sorted] .crisp-datatable-chevron { opacity: 1; color: var(--crisp-fg-primary); }
87
89
  /* sticky first column(s) + horizontal scroll shadow — RecordsTable's feel */
88
90
  & th[data-sticky], & td[data-sticky] { position: sticky; z-index: 1; background: var(--crisp-bg-surface); }
89
- & th[data-sticky] { z-index: 3; }
90
- /* #554: a footer sticky-column cell sits in the bottom-sticky row (table.recipe.ts's
91
- `tfoot td { z-index: 2 }`), which `td[data-sticky]`'s own higher-specificity `z-index: 1` would otherwise undercut -- keep it level with the row it's in. */
91
+ & th[data-sticky] { z-index: 3;
92
+ } /* #554: a footer sticky-column cell sits in the bottom-sticky row (table.recipe.ts's
93
+ `tfoot td { z-index: 2 }
94
+ `), which `td[data-sticky]`'s own higher-specificity
95
+ `z-index: 1` would otherwise undercut -- keep it level with the row it's in. */
92
96
  & tfoot td[data-sticky] { z-index: 2; }
93
97
  & tbody tr[data-selected] td[data-sticky] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
94
98
  & tbody tr:hover td[data-sticky] { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); }
@@ -105,36 +109,41 @@
105
109
  & tbody tr[data-selected] td { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
106
110
  & 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))); }
107
111
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
108
- /* crisp/#758: AI-enriched column marker, ported from RecordsTable's own
109
- already-measured value -- a full-bleed #9b69ff (bgPurple) wash at 4%,
110
- composited OVER whatever row-state background the cell already has
111
- (hover, selected), which is why it's an ::after overlay and not a plain background. */
112
+ /* 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
113
+ plain background. */
112
114
  & td[data-enriched]::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: color-mix(in srgb, var(--crisp-bg-purple) 4%, transparent); }
113
115
  & .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; }
114
116
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
115
- /* An async onEdit's in-flight state (#385) -- the field/picker dims and
116
- stops accepting input rather than disappearing, so the just-typed or
117
- just-chosen value stays visible while its commit is pending. Same
118
- treatment Input already gives its own disabled state. */
117
+ /* 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. */
119
118
  & .crisp-datatable-edit:disabled { opacity: 0.5; cursor: not-allowed; }
120
119
  & .crisp-datatable-edit-pending { flex-shrink: 0; margin-inline-start: var(--crisp-space-1); color: var(--crisp-fg-muted); vertical-align: middle; }
121
- /* The rejection's message, beside the cell's own content -- a sibling in
122
- the TableCell, never nested inside a column's own render output, so a hover-only wrapper (a Tooltip, say) around a custom render can't hide
123
- it. Block-level: the row grows to fit it rather than floating over
124
- the row below, so it can never end up clipped by the scroll
125
- container's overflow the way a position:fixed escape would need guarding against. */
120
+ /* The rejection's message, beside the cell's own content -- a sibling in the TableCell, never nested inside a column's own render output, so a
121
+ hover-only wrapper (a Tooltip, say) around a custom render can't hide it. Block-level: the row grows to fit it rather than floating over the row below, so it can never end up clipped by the scroll container's overflow the way a position:fixed escape would need
122
+ guarding against. */
126
123
  & .crisp-datatable-edit-error { display: block; padding: var(--crisp-space-0-5) 0 0; font-size: var(--crisp-text-sm); color: var(--crisp-bg-danger); }
127
- /* editor: "select"/"switch" -- always live, no separate edit mode, so the cell IS the picker: same chromeless-fill treatment as RecordsTable's own --picker cells (measured there already; DataTable
128
- shares RecordsTable's grid, Golden Rule 0). */
124
+ /* editor: "select"/"switch" -- always live, no separate edit mode, so
125
+ the cell IS the picker: same chromeless-fill treatment as
126
+ RecordsTable's own --picker cells (measured there already; DataTable shares RecordsTable's grid, Golden Rule 0). */
129
127
  & .crisp-datatable-td--picker { padding: 0; }
130
128
  & .crisp-datatable-edit-picker { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
131
129
  & .crisp-datatable-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; height: 100%; min-height: 0; border-radius: 0; box-shadow: none; padding: 0; }
132
130
  & .crisp-datatable-td--picker .crisp-datatable-edit-error { padding-inline-start: var(--crisp-space-2); }
133
- /* reference picker: a leading 16px avatar per option, plus a trailing "Create X" row when the consumer supplies one (#449) -- same measured values as RecordsTable's own .crisp-recordstable-refoption*. */
131
+ /* reference picker: a leading 16px avatar per option, plus a trailing
132
+ "Create X" row when the consumer supplies one (#449) -- same measured
133
+ values as RecordsTable's own .crisp-recordstable-refoption*. */
134
134
  & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
135
135
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
136
136
  & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
137
137
  & .crisp-datatable-empty { padding: var(--crisp-space-8) var(--crisp-space-3); text-align: center; color: var(--crisp-fg-muted); font-size: var(--crisp-text-lg); }
138
+ /* #826 -- the full-body loading state (a filter/sort re-query, or a fresh page boot), measured live on Attio's Companies grid on both
139
+ triggers: icon+text centred in the row-body area, 10px gap, 14px/500
140
+ weight/20px line-height, both icon and text in the SAME muted
141
+ rgba(0,0,0,0.63) -- the same color-mix figure this file already uses
142
+ for other muted text (e.g. -phone-dial). Distinct from the cold
143
+ in-place row load (blank, no indicator) and the warm-navigation
144
+ per-cell shimmer (#599) -- neither of those swaps the whole body. */
145
+ & .crisp-datatable-loading { display: flex; align-items: center; justify-content: center; gap: 10px; padding: var(--crisp-space-8) var(--crisp-space-3); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); line-height: 20px; }
146
+ & .crisp-datatable-loading .crisp-spinner { border-top-color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
138
147
  /* #579: click-to-expand overlay for a truncated cell -- a read-only,
139
148
  full-value card, settled live as click (not Attio's usual hover-expand) and measured position: absolute, unlike this file's
140
149
  floating chrome (Menu/Select popovers) which portals and escapes the
@@ -209,6 +218,9 @@
209
218
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
210
219
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
211
220
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
221
+ /* crisp#822 -- a date/datetime cell's danger/overdue tone: same token as
222
+ TasksTable's own overdue due-date (crisp#462), plain text colour, no fill/weight change. */
223
+ & .crisp-datatable-danger { color: var(--crisp-fg-danger); }
212
224
  /* url/email cell types (#654) -- same measured values as RecordsTable's
213
225
  own .crisp-recordstable-link, which this mirrors: no underline at
214
226
  rest, underline on hover, own-colour text rather than a browser-blue
@@ -222,26 +234,42 @@
222
234
  & .crisp-datatable-phone-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
223
235
  & .crisp-datatable-phone-editor { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
224
236
  & .crisp-datatable-phone-editor input { flex: 1; min-width: 0; }
237
+ /* country-picker option row (#842) -- confirmed live: Attio's own popover is plain text, no flag anywhere (not the option row, not the closed trigger), name opaque + dial code muted rgba(0,0,0,0.63) -- the SAME color-mix figure as -phone-dial above. The internal name/dial gap wasn't independently measured this pass (Attio's own two <div>s gave no exact number); reused space["1"], the same gap -phone already uses for its own flag/dial/digits row. */
238
+ & .crisp-datatable-country-option { display: flex; align-items: center; gap: var(--crisp-space-1); }
239
+ & .crisp-datatable-country-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
240
+ /* multi-value phone (#816) -- confirmed live (a real custom "Multiple
241
+ values" Phone Number attribute, and the built-in People "Phone
242
+ numbers" field): the collapsed cell is a 4px-gap flex row of real tel: links in a measured link blue (~rgb(59,108,232) pixel-sampled; reused as the closest already-tokened blue, brand600, rather than adding a third blue token for a delta this small), underlined, UNLIKE the single-value cell above (plain text, no link, no underline) -- multi-value is the one shape Attio makes into a real link. The cell hard-clips on overflow (no ellipsis -- confirmed live, a second value can end mid-digit with no "..."). */
243
+ & .crisp-datatable-phone-multi { display: inline-flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
244
+ & .crisp-datatable-phone-link { color: var(--crisp-bg-brand-solid); text-decoration: underline; white-space: nowrap; }
245
+ /* multi-value editor (#816) -- confirmed live: a stack of committed- value rows (32px = size.menuRow, 5px 8px padding, 8px gap between the grip/value/actions, 16px icons) above one ALWAYS-blank trailing DataPhoneEditor-shaped row -- that blank row is the only "add
246
+ another" affordance, there's no separate button for it. The
247
+ copy/delete pair reuses .crisp-datatable-entity-action's own hover-reveal chrome (not independently re-measured as its own hit target this pass, only the 16px icon glyphs were). */
248
+ & .crisp-datatable-phone-multi-editor { display: flex; flex-direction: column; }
249
+ & .crisp-datatable-phone-multi-row { display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); padding: 5px var(--crisp-space-2); }
250
+ & .crisp-datatable-phone-multi-row .crisp-datatable-phone-link { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
251
+ & .crisp-datatable-phone-multi-grip { color: var(--crisp-fg-muted); flex-shrink: 0; cursor: grab; }
252
+ & .crisp-datatable-phone-multi-actions { display: flex; align-items: center; gap: var(--crisp-space-1); margin-inline-start: auto; flex-shrink: 0; opacity: 0; }
253
+ & .crisp-datatable-phone-multi-row:hover .crisp-datatable-phone-multi-actions, & .crisp-datatable-phone-multi-actions:focus-within { opacity: 1; }
254
+ & .crisp-datatable-phone-multi-action { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; font-family: inherit; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); cursor: pointer; flex-shrink: 0; }
255
+ & .crisp-datatable-phone-multi-action:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
225
256
  /* datetime cell editor (#762) -- DatePickerField + TimePicker side by side. */
226
257
  & .crisp-datatable-datetime-editor { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
227
- /* rating cell type/editor (#760) -- measured live: 16px stars on an
228
- 18px pitch (a 2px gap), an empty star at rgba(0,0,0,0.1), a
229
- committed/filled star at brand600 (--crisp-bg-brand-solid, an exact
230
- match to the measured #266df0), and a distinct hover-preview colour
231
- -- rgb(33,91,196) -- that's close to but NOT brand700/bgBrandSolidPressed (#1f5fd6); kept as its own literal rather than forced onto that token, since the two were measured stable and different. The read-only svg and the editable button's own inner svg both carry the
232
- crisp-datatable-rating-star class on the svg itself (not the
233
- button), so one set of fill rules covers both -- the button only
234
- adds its own reset + cursor via a separate -btn class. */
258
+ /* 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
259
+ (#1f5fd6); kept as its own literal rather than forced onto that token,
260
+ since the two were measured stable and different. The read-only svg
261
+ 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. */
235
262
  & .crisp-datatable-rating { display: inline-flex; align-items: center; gap: 2px; }
236
263
  & .crisp-datatable-rating-star { display: block; }
237
264
  & .crisp-datatable-rating-star path { fill: rgba(0, 0, 0, 0.1); }
238
265
  & .crisp-datatable-rating-star[data-filled] path { fill: var(--crisp-bg-brand-solid); }
239
266
  & .crisp-datatable-rating-star[data-hovering] path { fill: #215bc4; }
240
267
  & .crisp-datatable-rating-star-btn { display: inline-flex; padding: 0; margin: 0; border: none; background: none; font-family: inherit; cursor: pointer; }
241
- /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
242
- a leading 20px grip column, a 16px grip icon, and the row's own hairline doubling as the drop indicator (it tints toward the brand colour as the active drop target). The affordance + geometry are measured; the drag MECHANISM is crisp's existing house idiom -- HTML5
243
- draggable + dataTransfer, the same one column reorder already uses
244
- (RecordsTable's reorderColumn) -- not a proprietary pointer-events implementation. */
268
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
269
+ hairline doubling as the drop indicator (it tints toward the brand
270
+ colour as the active drop target). The affordance + geometry are
271
+ measured; the drag MECHANISM is crisp's existing house idiom -- HTML5 draggable + dataTransfer, the same one column reorder already uses (RecordsTable's reorderColumn) -- not a proprietary
272
+ pointer-events implementation. */
245
273
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
246
274
  & .crisp-datatable-grip { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; margin: 0; border: none; border-radius: 0; background: none; font-family: inherit; color: var(--crisp-fg-muted); cursor: grab; }
247
275
  & .crisp-datatable-grip:active { cursor: grabbing; }
@@ -18,16 +18,32 @@ const config = defineRecipe({
18
18
  radius, and datatable-grip and the embedded pickers zero theirs. An 8px
19
19
  radius survived the chip-to-full-cell conversion and shipped rounded
20
20
  until #282, so the spec check now guards it. */
21
- /* The hit target is the WHOLE summary cell -- measured 246x37 with
21
+ /* The hit target is the WHOLE summary cell -- measured 246x36 with
22
22
  padding 0 12px, not a content-sized chip. Clicking anywhere in the cell
23
- opens the aggregation menu. */
24
- & .crisp-columncalc-trigger { display: flex; align-items: center; gap: ${c.space["1"]}; width: 100%; height: 100%; min-height: ${c.size.tableRow}; margin: 0; padding: 0 ${c.space["3"]}; border: none; background: transparent; color: ${c.fgMuted}; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; white-space: nowrap; cursor: pointer; transition: background-color .12s, color .12s; }
23
+ opens the aggregation menu.
24
+ No \`min-height\` (crisp#834, found reproducing the footer-row-38px-
25
+ not-36px bug in an isolated browser test, not table.recipe.ts's own
26
+ border/box-sizing, the hypothesis the issue itself flagged as
27
+ unconfirmed): the table cell's OWN \`height: tableRow\` (36px,
28
+ border-box) leaves 34px of content box once its 2px of top+bottom
29
+ border are counted, and \`min-height: tableRow\` here demanded 36px
30
+ of CONTENT height regardless -- overflowing the cell by 2px, which
31
+ auto-height table layout resolves by growing the whole row to fit,
32
+ landing at exactly the reported 38. \`height: 100%\` alone already
33
+ fills whatever the cell's real content box is (the doc comment above
34
+ this rule's own stated intent), with nothing left demanding more. */
35
+ & .crisp-columncalc-trigger { display: flex; align-items: center; gap: ${c.space["1"]}; width: 100%; height: 100%; margin: 0; padding: 0 ${c.space["3"]}; border: none; background: transparent; color: ${c.fgMuted}; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; white-space: nowrap; cursor: pointer; transition: background-color .12s, color .12s; }
25
36
  & .crisp-columncalc-trigger:hover { background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); color: ${c.fgPrimary}; }
26
37
  & .crisp-columncalc-trigger svg { flex-shrink: 0; opacity: 0.7; }
27
38
  & .crisp-columncalc-trigger[data-align="right"] { justify-content: flex-end; }
28
39
  & .crisp-columncalc-placeholder { overflow: hidden; text-overflow: ellipsis; }
29
40
  & .crisp-columncalc-value { color: ${c.fgPrimary}; font-variant-numeric: tabular-nums; }
30
- & .crisp-columncalc-op { color: ${c.fgMuted}; }
41
+ /* crisp#837: re-measured live against Attio's real footer calc label --
42
+ 14px (was 13px, \`c.text.md\`, fixed on the shared trigger rule above)
43
+ and rgba(0,0,0,.63), the same color-mix(fgPrimary, 63%) figure this
44
+ file's own -op label already should have used instead of the flatter
45
+ \`fgMuted\` token (a close but not measured match). */
46
+ & .crisp-columncalc-op { color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
31
47
  & .crisp-columncalc-section { padding: ${c.space["1.5"]} ${c.space["2"]} ${c.space["1"]}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }`,
32
48
  variants: {},
33
49
  defaultVariants: {},