@crisp-ui-kit/crisp 0.55.2 → 0.55.4

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,52 +40,64 @@
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; }
90
+ /* `thead th[data-sticky]`, not a bare `th[data-sticky]` (crisp#848, same
91
+ bare-`th` family as #829/#833/#835): this z-index:3 was written for
92
+ the HEADER's own sticky corner cell (sticky column x sticky header), meant to win over everything else. Unscoped, it also matched the primary column's BODY cell -- a real `<th scope="row" data-sticky>`
93
+ since #779 -- giving every scrolled body row's own sticky cell a HIGHER z-index than the sticky FOOTER's (2), so it painted over the
94
+ footer instead of the footer showing through. Confirmed live on a
95
+ real bounded-height, 25-row table (GRC's Risk register): scrolled partway, `elementFromPoint` at the footer's own on-screen cell
96
+ resolved to a body row's `<th>`, not the footer's `<td>`, and that
97
+ `<th>`'s computed z-index read 3. Scoped to `thead`, a body row's
98
+ own sticky `<th>` now falls back to the base rule above (z-index 1,
99
+ correct -- the same as a plain sticky `<td>`), and the footer's own explicit z-index below is unaffected either way. */
100
+ & thead th[data-sticky] { z-index: 3; }
89
101
  /* #554: a footer sticky-column cell sits in the bottom-sticky row (table.recipe.ts's
90
102
  `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
103
  & tfoot td[data-sticky] { z-index: 2; }
@@ -134,68 +146,66 @@
134
146
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
135
147
  & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
136
148
  & .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
- /* #579: click-to-expand overlay for a truncated cell -- a read-only,
138
- full-value card, settled live as click (not Attio's usual hover-expand) and measured position: absolute, unlike this file's
139
- floating chrome (Menu/Select popovers) which portals and escapes the
140
- scroll container. Deliberately NOT the edit-error pattern above
141
- (grow the row, block-level, never clipped): a virtualized grid
142
- assumes a fixed row height for its scroll math (owned by the grid
143
- core, not this cell), so growing a row here would fight it. Anchored
144
- to the SAME column width the cell itself renders at (measured:
145
- Attio's own overlay came out only slightly wider than the triggering cell's clientWidth -- the overshoot is this padding),
146
- wrapping onto as many lines as the value needs; it CAN be clipped by
147
- `.crisp-table-scroll`'s own `overflow: auto` if it grows past the visible viewport, same as the edit-error comment above already notes for any position:absolute escape -- unmeasured against Attio for this exact case, flagged rather than guessed. */
149
+ /* #826 -- the full-body loading state (a filter/sort re-query, or a
150
+ fresh page boot), measured live on Attio's Companies grid on both triggers: icon+text centred in the row-body area, 10px gap, 14px/500 weight/20px line-height, both icon and text in the SAME muted rgba(0,0,0,0.63) -- the same color-mix figure this file already uses for other muted text (e.g. -phone-dial). Distinct from the cold in-place row load (blank, no indicator) and the warm-navigation per-cell shimmer (#599) -- neither of those swaps the whole body. */
151
+ & .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; }
152
+ & .crisp-datatable-loading .crisp-spinner { border-top-color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
153
+ /* #579: click-to-expand overlay for a truncated cell -- a read-only, full-value card, settled live as click (not Attio's usual
154
+ hover-expand) and measured position: absolute, unlike this file's floating chrome (Menu/Select popovers) which portals and escapes the scroll container. Deliberately NOT the edit-error pattern above (grow the row, block-level, never clipped): a virtualized grid assumes a fixed row height for its scroll math (owned by the grid core, not this cell), so growing a row here would fight it. Anchored to the SAME column width the cell itself renders at (measured: Attio's own overlay came out only slightly wider than the
155
+ triggering cell's clientWidth -- the overshoot is this padding), wrapping onto as many lines as the value needs; it CAN be clipped by `.crisp-table-scroll`'s own `overflow: auto` if it grows past the
156
+ visible viewport, same as the edit-error comment above already
157
+ notes for any position:absolute escape -- unmeasured against Attio
158
+ for this exact case, flagged rather than guessed. */
148
159
  & td[data-cell-expanded] { overflow: visible; z-index: 7; }
149
- & .crisp-datatable-cell-overlay { position: absolute; inset-inline-start: 0; top: 0; width: 100%; box-sizing: border-box; padding: var(--crisp-space-2) var(--crisp-space-3); background: var(--crisp-bg-surface); border: 1px solid var(--crisp-bg-brand-solid); border-radius: 4px; font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); white-space: normal; word-break: break-word; cursor: default; }
160
+ & .crisp-datatable-cell-overlay {
161
+ position: absolute; inset-inline-start: 0; top: 0; width: 100%; box-sizing: border-box;
162
+ padding: var(--crisp-space-2) var(--crisp-space-3); background: var(--crisp-bg-surface);
163
+ border: 1px solid var(--crisp-bg-brand-solid); border-radius: 4px;
164
+ font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium);
165
+ color: var(--crisp-fg-primary); white-space: normal; word-break: break-word; cursor: default;
166
+ }
150
167
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
151
168
  & table { table-layout: fixed; }
152
- /* read-only/computed column marker (#596) -- sits just inside the resize handle, same absolute-positioned-against-the-th approach the handle itself uses, since th has zero padding and the button/inner row fills the whole cell with no room left for a trailing sibling. */
169
+ /* read-only/computed column marker (#596) -- sits just inside the
170
+ resize handle, same absolute-positioned-against-the-th approach the
171
+ handle itself uses, since th has zero padding and the button/inner
172
+ row fills the whole cell with no room left for a trailing sibling. */
153
173
  & .crisp-datatable-readonly { position: absolute; top: 50%; inset-inline-end: 11px; transform: translateY(-50%); color: var(--crisp-fg-muted); pointer-events: none; z-index: 5; }
154
174
  /* column resize grip */
155
175
  & .crisp-datatable-resize { position: absolute; top: 0; inset-inline-end: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
156
176
  & .crisp-datatable-resize::after { content: ""; position: absolute; top: 0; inset-inline-start: 3px; width: 2px; height: 100%; background: var(--crisp-bg-brand-solid); opacity: 0; transition: opacity .12s; }
157
177
  & .crisp-datatable-resize:hover::after, & .crisp-datatable-resize[data-resizing]::after { opacity: 1; }
158
- /* crisp/#731 -- column drag-to-reorder. Measured live against Attio's
159
- own grid (the issue's own comment): the dragged header keeps full opacity and the plain `pointer` cursor throughout (Attio draws no ghost and no "grabbing" cursor of its own), so this adds nothing to `[data-dragging]` itself -- the ONLY new paint is the drop-indicator line, a 3px bar in the same measured brand blue the resize handle's
160
- own hover state (above) already uses, positioned on whichever edge of
161
- the hovered header the pointer is closer to. */
178
+ /* crisp/#731 -- column drag-to-reorder. Measured live against Attio's own grid (the issue's own comment): the dragged header keeps full
179
+ opacity and the plain `pointer` cursor throughout (Attio draws no
180
+ ghost and no "grabbing" cursor of its own), so this adds nothing to
181
+ `[data-dragging]` itself -- the ONLY new paint is the drop-indicator
182
+ line, a 3px bar in the same measured brand blue the resize handle's own hover state (above) already uses, positioned on whichever edge of the hovered header the pointer is closer to. */
162
183
  & th[data-dragging] { cursor: pointer; }
163
- & th[data-drop-before]::before, & th[data-drop-after]::after {
164
- content: ""; position: absolute; top: 0; width: 3px; height: 100%;
165
- background: var(--crisp-bg-brand-solid); z-index: 6; pointer-events: none;
166
- }
184
+ & th[data-drop-before]::before, & th[data-drop-after]::after { content: ""; position: absolute; top: 0; width: 3px; height: 100%; background: var(--crisp-bg-brand-solid); z-index: 6; pointer-events: none; }
167
185
  & th[data-drop-before]::before { inset-inline-start: -1.5px; }
168
186
  & th[data-drop-after]::after { inset-inline-end: -1.5px; }
169
187
  /* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
170
188
  & .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); }
171
- /* crisp/#601 supersession follow-up -- the same flex-row shape
172
- .crisp-datatable-entity gives its own avatar+name+actions, but
173
- structural only (no entity-specific colour/weight): lets
174
- onOpenComments/onOpenPreview compose alongside ANY column's own content (a custom render, or a non-entity built-in type), not just type: "entity"'s own inline rendering. */
189
+ /* 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
190
+ content (a custom render, or a non-entity built-in type), not just
191
+ type: "entity"'s own inline rendering. */
175
192
  & .crisp-datatable-cell-actions-row { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; width: 100%; }
176
- /* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text,
177
- plain hover vs rest) that Attio's underline is unconditional, not a hover reveal -- identical at rest and on hover: text-decoration-color is rgba(0,0,0,0.09), a 9% tint, not the name's own full-contrast text
178
- colour. The previous :hover-gated, full-contrast rule here was itself
179
- unmeasured and wrong in two ways at once. */
193
+ /* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text, plain hover vs rest) that Attio's underline is unconditional, not a
194
+ hover reveal -- identical at rest and on hover: text-decoration-color
195
+ 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
196
  & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--crisp-fg-primary) 9%, transparent); }
181
197
  & button.crisp-datatable-entity-name--link { margin: 0; padding: 0; border: none; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-align: start; cursor: pointer; max-width: 100%; }
182
- /* crisp/#601, G-048: two 20px ghost icons, right-aligned in the primary
183
- column, revealed on ROW hover (the issue's own title says "row
184
- hover", and a live check confirmed hovering well past the name -- not just the icons' own hit target -- still reveals them) or
185
- keyboard focus. Measured live: border-radius 6px, icon colour
186
- rgba(0,0,0,0.55) at rest, darkening to the full text colour with a
187
- rgba(0,0,0,0.04) background on the icon's OWN hover -- both expressed as color-mix against fgPrimary so they stay correct in dark mode instead of a literal black that would invert. */
198
+ /* crisp/#601, G-048: two 20px ghost icons, right-aligned in the primary column, revealed on ROW hover (the issue's own title says "row
199
+ hover", and a live check confirmed hovering well past the name --
200
+ not just the icons' own hit target -- still reveals them) or keyboard focus. Measured live: border-radius 6px, icon colour rgba(0,0,0,0.55) at rest, darkening to the full text colour with a rgba(0,0,0,0.04) background on the icon's OWN hover -- both
201
+ expressed as color-mix against fgPrimary so they stay correct in
202
+ dark mode instead of a literal black that would invert. */
188
203
  & .crisp-datatable-entity-actions { display: flex; align-items: center; gap: var(--crisp-space-1-5); margin-inline-start: auto; flex-shrink: 0; opacity: 0; }
189
204
  & tr:hover .crisp-datatable-entity-actions, & .crisp-datatable-entity-actions:focus-within { opacity: 1; }
190
205
  & .crisp-datatable-entity-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; }
191
206
  & .crisp-datatable-entity-action:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
192
207
  & .crisp-datatable-avatar { width: var(--crisp-size-icon); height: var(--crisp-size-icon); border-radius: var(--crisp-radius-sm); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: var(--crisp-weight-semibold); text-transform: uppercase; color: var(--crisp-fg-on-brand); background: var(--crisp-bg-brand-solid); }
193
- & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
194
- & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
195
- & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); }
196
- & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); }
197
- & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); }
198
- & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
208
+ & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); } & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); } & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); } & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); } & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); } & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
199
209
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
200
210
  & .crisp-datatable-tags { display: flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
201
211
  & .crisp-datatable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
@@ -208,22 +218,26 @@
208
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)); }
209
219
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
210
220
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
211
- /* crisp#822 -- a date/datetime cell's danger/overdue tone: same token as
212
- TasksTable's own overdue due-date (crisp#462), plain text colour, no fill/weight change. */
221
+ /* crisp#822 -- a date/datetime cell's danger/overdue tone: same token as TasksTable's own overdue due-date (crisp#462), plain text colour, no
222
+ fill/weight change. */
213
223
  & .crisp-datatable-danger { color: var(--crisp-fg-danger); }
214
- /* url/email cell types (#654) -- same measured values as RecordsTable's
215
- own .crisp-recordstable-link, which this mirrors: no underline at
216
- rest, underline on hover, own-colour text rather than a browser-blue
217
- link. */
224
+ /* 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. */
218
225
  & .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
219
226
  & .crisp-datatable-link:hover { text-decoration: underline; }
220
- /* phone cell type/editor (#765) -- measured live: the dial-code prefix
221
- is a muted rgba(0,0,0,0.63) at 14px, matching every other muted-text
222
- value already measured elsewhere in this file. */
227
+ /* phone cell type/editor (#765) -- measured live: the dial-code prefix is a muted rgba(0,0,0,0.63) at 14px, matching every other muted-text value already measured elsewhere in this file. */
223
228
  & .crisp-datatable-phone { display: inline-flex; align-items: center; gap: var(--crisp-space-1); }
224
229
  & .crisp-datatable-phone-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
225
230
  & .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); }
226
231
  & .crisp-datatable-phone-editor input { flex: 1; min-width: 0; }
232
+ /* country-picker option row (#842) -- confirmed live: Attio's own popover
233
+ is plain text, no flag anywhere (not the option row, not the closed
234
+ trigger), name opaque + dial code muted rgba(0,0,0,0.63) -- the SAME
235
+ color-mix figure as -phone-dial above. The internal name/dial gap
236
+ wasn't independently measured this pass (Attio's own two <div>s gave
237
+ no exact number); reused space["1"], the same gap -phone already uses
238
+ for its own flag/dial/digits row. */
239
+ & .crisp-datatable-country-option { display: flex; align-items: center; gap: var(--crisp-space-1); }
240
+ & .crisp-datatable-country-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
227
241
  /* multi-value phone (#816) -- confirmed live (a real custom "Multiple
228
242
  values" Phone Number attribute, and the built-in People "Phone
229
243
  numbers" field): the collapsed cell is a 4px-gap flex row of real
@@ -41,52 +41,64 @@
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; }
91
+ /* `thead th[data-sticky]`, not a bare `th[data-sticky]` (crisp#848, same
92
+ bare-`th` family as #829/#833/#835): this z-index:3 was written for
93
+ the HEADER's own sticky corner cell (sticky column x sticky header), meant to win over everything else. Unscoped, it also matched the primary column's BODY cell -- a real `<th scope="row" data-sticky>`
94
+ since #779 -- giving every scrolled body row's own sticky cell a HIGHER z-index than the sticky FOOTER's (2), so it painted over the
95
+ footer instead of the footer showing through. Confirmed live on a
96
+ real bounded-height, 25-row table (GRC's Risk register): scrolled partway, `elementFromPoint` at the footer's own on-screen cell
97
+ resolved to a body row's `<th>`, not the footer's `<td>`, and that
98
+ `<th>`'s computed z-index read 3. Scoped to `thead`, a body row's
99
+ own sticky `<th>` now falls back to the base rule above (z-index 1,
100
+ correct -- the same as a plain sticky `<td>`), and the footer's own explicit z-index below is unaffected either way. */
101
+ & thead th[data-sticky] { z-index: 3; }
90
102
  /* #554: a footer sticky-column cell sits in the bottom-sticky row (table.recipe.ts's
91
103
  `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. */
92
104
  & tfoot td[data-sticky] { z-index: 2; }
@@ -135,68 +147,66 @@
135
147
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
136
148
  & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
137
149
  & .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
- /* #579: click-to-expand overlay for a truncated cell -- a read-only,
139
- full-value card, settled live as click (not Attio's usual hover-expand) and measured position: absolute, unlike this file's
140
- floating chrome (Menu/Select popovers) which portals and escapes the
141
- scroll container. Deliberately NOT the edit-error pattern above
142
- (grow the row, block-level, never clipped): a virtualized grid
143
- assumes a fixed row height for its scroll math (owned by the grid
144
- core, not this cell), so growing a row here would fight it. Anchored
145
- to the SAME column width the cell itself renders at (measured:
146
- Attio's own overlay came out only slightly wider than the triggering cell's clientWidth -- the overshoot is this padding),
147
- wrapping onto as many lines as the value needs; it CAN be clipped by
148
- `.crisp-table-scroll`'s own `overflow: auto` if it grows past the visible viewport, same as the edit-error comment above already notes for any position:absolute escape -- unmeasured against Attio for this exact case, flagged rather than guessed. */
150
+ /* #826 -- the full-body loading state (a filter/sort re-query, or a
151
+ fresh page boot), measured live on Attio's Companies grid on both triggers: icon+text centred in the row-body area, 10px gap, 14px/500 weight/20px line-height, both icon and text in the SAME muted rgba(0,0,0,0.63) -- the same color-mix figure this file already uses for other muted text (e.g. -phone-dial). Distinct from the cold in-place row load (blank, no indicator) and the warm-navigation per-cell shimmer (#599) -- neither of those swaps the whole body. */
152
+ & .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; }
153
+ & .crisp-datatable-loading .crisp-spinner { border-top-color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
154
+ /* #579: click-to-expand overlay for a truncated cell -- a read-only, full-value card, settled live as click (not Attio's usual
155
+ hover-expand) and measured position: absolute, unlike this file's floating chrome (Menu/Select popovers) which portals and escapes the scroll container. Deliberately NOT the edit-error pattern above (grow the row, block-level, never clipped): a virtualized grid assumes a fixed row height for its scroll math (owned by the grid core, not this cell), so growing a row here would fight it. Anchored to the SAME column width the cell itself renders at (measured: Attio's own overlay came out only slightly wider than the
156
+ triggering cell's clientWidth -- the overshoot is this padding), wrapping onto as many lines as the value needs; it CAN be clipped by `.crisp-table-scroll`'s own `overflow: auto` if it grows past the
157
+ visible viewport, same as the edit-error comment above already
158
+ notes for any position:absolute escape -- unmeasured against Attio
159
+ for this exact case, flagged rather than guessed. */
149
160
  & td[data-cell-expanded] { overflow: visible; z-index: 7; }
150
- & .crisp-datatable-cell-overlay { position: absolute; inset-inline-start: 0; top: 0; width: 100%; box-sizing: border-box; padding: var(--crisp-space-2) var(--crisp-space-3); background: var(--crisp-bg-surface); border: 1px solid var(--crisp-bg-brand-solid); border-radius: 4px; font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); white-space: normal; word-break: break-word; cursor: default; }
161
+ & .crisp-datatable-cell-overlay {
162
+ position: absolute; inset-inline-start: 0; top: 0; width: 100%; box-sizing: border-box;
163
+ padding: var(--crisp-space-2) var(--crisp-space-3); background: var(--crisp-bg-surface);
164
+ border: 1px solid var(--crisp-bg-brand-solid); border-radius: 4px;
165
+ font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium);
166
+ color: var(--crisp-fg-primary); white-space: normal; word-break: break-word; cursor: default;
167
+ }
151
168
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
152
169
  & table { table-layout: fixed; }
153
- /* read-only/computed column marker (#596) -- sits just inside the resize handle, same absolute-positioned-against-the-th approach the handle itself uses, since th has zero padding and the button/inner row fills the whole cell with no room left for a trailing sibling. */
170
+ /* read-only/computed column marker (#596) -- sits just inside the
171
+ resize handle, same absolute-positioned-against-the-th approach the
172
+ handle itself uses, since th has zero padding and the button/inner
173
+ row fills the whole cell with no room left for a trailing sibling. */
154
174
  & .crisp-datatable-readonly { position: absolute; top: 50%; inset-inline-end: 11px; transform: translateY(-50%); color: var(--crisp-fg-muted); pointer-events: none; z-index: 5; }
155
175
  /* column resize grip */
156
176
  & .crisp-datatable-resize { position: absolute; top: 0; inset-inline-end: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
157
177
  & .crisp-datatable-resize::after { content: ""; position: absolute; top: 0; inset-inline-start: 3px; width: 2px; height: 100%; background: var(--crisp-bg-brand-solid); opacity: 0; transition: opacity .12s; }
158
178
  & .crisp-datatable-resize:hover::after, & .crisp-datatable-resize[data-resizing]::after { opacity: 1; }
159
- /* crisp/#731 -- column drag-to-reorder. Measured live against Attio's
160
- own grid (the issue's own comment): the dragged header keeps full opacity and the plain `pointer` cursor throughout (Attio draws no ghost and no "grabbing" cursor of its own), so this adds nothing to `[data-dragging]` itself -- the ONLY new paint is the drop-indicator line, a 3px bar in the same measured brand blue the resize handle's
161
- own hover state (above) already uses, positioned on whichever edge of
162
- the hovered header the pointer is closer to. */
179
+ /* crisp/#731 -- column drag-to-reorder. Measured live against Attio's own grid (the issue's own comment): the dragged header keeps full
180
+ opacity and the plain `pointer` cursor throughout (Attio draws no
181
+ ghost and no "grabbing" cursor of its own), so this adds nothing to
182
+ `[data-dragging]` itself -- the ONLY new paint is the drop-indicator
183
+ line, a 3px bar in the same measured brand blue the resize handle's own hover state (above) already uses, positioned on whichever edge of the hovered header the pointer is closer to. */
163
184
  & th[data-dragging] { cursor: pointer; }
164
- & th[data-drop-before]::before, & th[data-drop-after]::after {
165
- content: ""; position: absolute; top: 0; width: 3px; height: 100%;
166
- background: var(--crisp-bg-brand-solid); z-index: 6; pointer-events: none;
167
- }
185
+ & th[data-drop-before]::before, & th[data-drop-after]::after { content: ""; position: absolute; top: 0; width: 3px; height: 100%; background: var(--crisp-bg-brand-solid); z-index: 6; pointer-events: none; }
168
186
  & th[data-drop-before]::before { inset-inline-start: -1.5px; }
169
187
  & th[data-drop-after]::after { inset-inline-end: -1.5px; }
170
188
  /* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
171
189
  & .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); }
172
- /* crisp/#601 supersession follow-up -- the same flex-row shape
173
- .crisp-datatable-entity gives its own avatar+name+actions, but
174
- structural only (no entity-specific colour/weight): lets
175
- onOpenComments/onOpenPreview compose alongside ANY column's own content (a custom render, or a non-entity built-in type), not just type: "entity"'s own inline rendering. */
190
+ /* 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
191
+ content (a custom render, or a non-entity built-in type), not just
192
+ type: "entity"'s own inline rendering. */
176
193
  & .crisp-datatable-cell-actions-row { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; width: 100%; }
177
- /* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text,
178
- plain hover vs rest) that Attio's underline is unconditional, not a hover reveal -- identical at rest and on hover: text-decoration-color is rgba(0,0,0,0.09), a 9% tint, not the name's own full-contrast text
179
- colour. The previous :hover-gated, full-contrast rule here was itself
180
- unmeasured and wrong in two ways at once. */
194
+ /* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text, plain hover vs rest) that Attio's underline is unconditional, not a
195
+ hover reveal -- identical at rest and on hover: text-decoration-color
196
+ 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
197
  & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--crisp-fg-primary) 9%, transparent); }
182
198
  & button.crisp-datatable-entity-name--link { margin: 0; padding: 0; border: none; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-align: start; cursor: pointer; max-width: 100%; }
183
- /* crisp/#601, G-048: two 20px ghost icons, right-aligned in the primary
184
- column, revealed on ROW hover (the issue's own title says "row
185
- hover", and a live check confirmed hovering well past the name -- not just the icons' own hit target -- still reveals them) or
186
- keyboard focus. Measured live: border-radius 6px, icon colour
187
- rgba(0,0,0,0.55) at rest, darkening to the full text colour with a
188
- rgba(0,0,0,0.04) background on the icon's OWN hover -- both expressed as color-mix against fgPrimary so they stay correct in dark mode instead of a literal black that would invert. */
199
+ /* crisp/#601, G-048: two 20px ghost icons, right-aligned in the primary column, revealed on ROW hover (the issue's own title says "row
200
+ hover", and a live check confirmed hovering well past the name --
201
+ not just the icons' own hit target -- still reveals them) or keyboard focus. Measured live: border-radius 6px, icon colour rgba(0,0,0,0.55) at rest, darkening to the full text colour with a rgba(0,0,0,0.04) background on the icon's OWN hover -- both
202
+ expressed as color-mix against fgPrimary so they stay correct in
203
+ dark mode instead of a literal black that would invert. */
189
204
  & .crisp-datatable-entity-actions { display: flex; align-items: center; gap: var(--crisp-space-1-5); margin-inline-start: auto; flex-shrink: 0; opacity: 0; }
190
205
  & tr:hover .crisp-datatable-entity-actions, & .crisp-datatable-entity-actions:focus-within { opacity: 1; }
191
206
  & .crisp-datatable-entity-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; }
192
207
  & .crisp-datatable-entity-action:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
193
208
  & .crisp-datatable-avatar { width: var(--crisp-size-icon); height: var(--crisp-size-icon); border-radius: var(--crisp-radius-sm); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: var(--crisp-weight-semibold); text-transform: uppercase; color: var(--crisp-fg-on-brand); background: var(--crisp-bg-brand-solid); }
194
- & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
195
- & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
196
- & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); }
197
- & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); }
198
- & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); }
199
- & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
209
+ & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); } & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); } & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); } & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); } & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); } & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
200
210
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
201
211
  & .crisp-datatable-tags { display: flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
202
212
  & .crisp-datatable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
@@ -209,22 +219,26 @@
209
219
  & .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
220
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
211
221
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
212
- /* crisp#822 -- a date/datetime cell's danger/overdue tone: same token as
213
- TasksTable's own overdue due-date (crisp#462), plain text colour, no fill/weight change. */
222
+ /* crisp#822 -- a date/datetime cell's danger/overdue tone: same token as TasksTable's own overdue due-date (crisp#462), plain text colour, no
223
+ fill/weight change. */
214
224
  & .crisp-datatable-danger { color: var(--crisp-fg-danger); }
215
- /* url/email cell types (#654) -- same measured values as RecordsTable's
216
- own .crisp-recordstable-link, which this mirrors: no underline at
217
- rest, underline on hover, own-colour text rather than a browser-blue
218
- link. */
225
+ /* 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. */
219
226
  & .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
220
227
  & .crisp-datatable-link:hover { text-decoration: underline; }
221
- /* phone cell type/editor (#765) -- measured live: the dial-code prefix
222
- is a muted rgba(0,0,0,0.63) at 14px, matching every other muted-text
223
- value already measured elsewhere in this file. */
228
+ /* phone cell type/editor (#765) -- measured live: the dial-code prefix is a muted rgba(0,0,0,0.63) at 14px, matching every other muted-text value already measured elsewhere in this file. */
224
229
  & .crisp-datatable-phone { display: inline-flex; align-items: center; gap: var(--crisp-space-1); }
225
230
  & .crisp-datatable-phone-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
226
231
  & .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); }
227
232
  & .crisp-datatable-phone-editor input { flex: 1; min-width: 0; }
233
+ /* country-picker option row (#842) -- confirmed live: Attio's own popover
234
+ is plain text, no flag anywhere (not the option row, not the closed
235
+ trigger), name opaque + dial code muted rgba(0,0,0,0.63) -- the SAME
236
+ color-mix figure as -phone-dial above. The internal name/dial gap
237
+ wasn't independently measured this pass (Attio's own two <div>s gave
238
+ no exact number); reused space["1"], the same gap -phone already uses
239
+ for its own flag/dial/digits row. */
240
+ & .crisp-datatable-country-option { display: flex; align-items: center; gap: var(--crisp-space-1); }
241
+ & .crisp-datatable-country-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
228
242
  /* multi-value phone (#816) -- confirmed live (a real custom "Multiple
229
243
  values" Phone Number attribute, and the built-in People "Phone
230
244
  numbers" field): the collapsed cell is a 4px-gap flex row of real
@@ -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: {},