@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.
@@ -500,16 +500,26 @@
500
500
  radius, and datatable-grip and the embedded pickers zero theirs. An 8px
501
501
  radius survived the chip-to-full-cell conversion and shipped rounded
502
502
  until #282, so the spec check now guards it. */
503
- /* The hit target is the WHOLE summary cell -- measured 246x37 with
503
+ /* The hit target is the WHOLE summary cell -- measured 246x36 with
504
504
  padding 0 12px, not a content-sized chip. Clicking anywhere in the cell
505
- opens the aggregation menu. */
506
- & .crisp-columncalc-trigger { display: flex; align-items: center; gap: var(--crisp-space-1); width: 100%; height: 100%; min-height: var(--crisp-size-tableRow); margin: 0; padding: 0 var(--crisp-space-3); border: none; 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; }
505
+ opens the aggregation menu.
506
+ No `min-height` (crisp#834, found reproducing the footer-row-38px-
507
+ not-36px bug in an isolated browser test, not table.recipe.ts's own border/box-sizing, the hypothesis the issue itself flagged as unconfirmed): the table cell's OWN `height: tableRow` (36px,
508
+ border-box) leaves 34px of content box once its 2px of top+bottom
509
+ border are counted, and `min-height: tableRow` here demanded 36px
510
+ of CONTENT height regardless -- overflowing the cell by 2px, which
511
+ auto-height table layout resolves by growing the whole row to fit,
512
+ landing at exactly the reported 38. `height: 100%` alone already
513
+ fills whatever the cell's real content box is (the doc comment above this rule's own stated intent), with nothing left demanding more. */
514
+ & .crisp-columncalc-trigger { display: flex; align-items: center; gap: var(--crisp-space-1); width: 100%; height: 100%; margin: 0; padding: 0 var(--crisp-space-3); border: none; background: transparent; color: var(--crisp-fg-muted); font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s, color .12s; }
507
515
  & .crisp-columncalc-trigger:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
508
516
  & .crisp-columncalc-trigger svg { flex-shrink: 0; opacity: 0.7; }
509
517
  & .crisp-columncalc-trigger[data-align="right"] { justify-content: flex-end; }
510
518
  & .crisp-columncalc-placeholder { overflow: hidden; text-overflow: ellipsis; }
511
519
  & .crisp-columncalc-value { color: var(--crisp-fg-primary); font-variant-numeric: tabular-nums; }
512
- & .crisp-columncalc-op { color: var(--crisp-fg-muted); }
520
+ /* crisp#837: re-measured live against Attio's real footer calc label -- 14px (was 13px, `c.text.md`, fixed on the shared trigger rule above) and rgba(0,0,0,.63), the same color-mix(fgPrimary, 63%) figure this file's own -op label already should have used instead of the flatter
521
+ `fgMuted` token (a close but not measured match). */
522
+ & .crisp-columncalc-op { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
513
523
  & .crisp-columncalc-section { 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); }
514
524
  }
515
525
 
@@ -640,52 +650,64 @@
640
650
  class + one element) -- a bare `& th` here only won by source
641
651
  order, which flipped the moment table.recipe.ts's own CSS file happened to land after datatable's in the build (alphabetical:
642
652
  datatable < table), silently reinstating 12px on every header cell
643
- 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. */
644
- & .crisp-table th { padding: 0; }
645
- /* Same wrapper trap as AUTHORING.md Lesson 40 (ColumnCalc's summary trigger):
646
- Menu wraps its trigger in a flex div that sizes to its content, so a
647
- long header grew past the column instead of filling it -- with nothing
648
- 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
649
- it with no "...". Fill the wrapper, scoped to th so Menu's default (correct everywhere else) is untouched. */
653
+ 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
654
+ `th` here matches that one too, zeroing an inset nothing else
655
+ supplies -- the primary column's text sat flush left, one column's
656
+ cell no longer matching its own header or any sibling `<td>`. Only
657
+ 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,
658
+ and needs the SAME base 12px (table.recipe.ts, #465) every other
659
+ cell already gets. */
660
+ & .crisp-table thead th { padding: 0; }
661
+ /* 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
662
+ -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
663
+ (correct everywhere else) is untouched. */
650
664
  & th .crisp-menu { width: 100%; height: 100%; }
651
665
  & .crisp-datatable-th-inner { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; min-width: 0; height: 100%; }
652
666
  & .crisp-datatable-th-inner > svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
653
- /* 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
654
- columnMenu/sortable at all) renders .crisp-datatable-th-inner as a
655
- DIRECT child of th instead of nested inside a button -- and loses
656
- the button's own 12px inset, drifting left of the td's 12px base
657
- 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
658
- 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
659
- plain sort `button` both put >=1 more level between `th` and this
660
- span, so their own 12px (already on the button) isn't doubled. */
667
+ /* crisp/#605 follow-up: a header with neither a Menu nor a sort button
668
+ (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
669
+ 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
670
+ padding+icon+gap combinations -- Attio does not leave the inert
671
+ primary header flush left either. The `>` combinator targets only
672
+ 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. */
661
673
  & th > .crisp-datatable-th-inner { box-sizing: border-box; padding: 0 var(--crisp-space-3); }
662
- /* 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. */
674
+ /* The summary cell has NO padding of its own -- the calculation trigger
675
+ owns the whole 246x37 area so a click anywhere in the cell opens the
676
+ menu. Zero the cell's inset and let ColumnCalc supply the 12px. */
663
677
  & tfoot td { padding: 0; }
664
- /* Group heading row -- a view is grouped under a subtle label with the
665
- bucket's count beside it. */
666
- & .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; }
678
+ /* Group heading row -- a view is grouped under a subtle label with the bucket's count beside it. */
679
+ & .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; }
667
680
  & .crisp-datatable-viewsettings:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
668
681
  & .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); }
669
- & .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); }
682
+ & .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); }
670
683
  & .crisp-datatable-group-count { margin-inline-start: var(--crisp-space-1-5); color: var(--crisp-fg-muted); font-variant-numeric: tabular-nums; }
671
684
  & .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; }
672
685
  & .crisp-datatable-th-rename:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
673
686
  & .crisp-datatable-th-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
674
- /* #465: 12px per the comment above (and Attio's measured header cell
675
- padding: 0 12px) -- was still c.space["2"] (8px), drifted from its
676
- own documented value. */
687
+ /* #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. */
677
688
  & .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; }
678
- /* 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
679
- count") keeps the icon+label left and only the trailing column-menu
680
- icon sits pinned to the right edge, independent of the label's width (#406, reversing #393's own justify-content: flex-end here). The
681
- 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
682
- the trailing icon on Attio too. */
689
+ /* A right-aligned column's header keeps its field-icon + label packed at
690
+ the START, matching its own values only in that the VALUES right-align
691
+ -- Attio's own right-aligned header (measured live: "Twitter follower
692
+ 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
693
+ (#406, reversing #393's own justify-content: flex-end here). The chevron's own margin-inline-start: auto below already pins IT to the
694
+ end of a flex-start row with no override needed -- that's what pins the trailing icon on Attio too. */
683
695
  & .crisp-datatable-chevron { margin-inline-start: auto; flex-shrink: 0; color: var(--crisp-fg-muted); opacity: 0; transition: opacity .12s; }
684
696
  & .crisp-datatable-sort:hover .crisp-datatable-chevron { opacity: 0.6; }
685
697
  & .crisp-datatable-sort[data-sorted] .crisp-datatable-chevron { opacity: 1; color: var(--crisp-fg-primary); }
686
698
  /* sticky first column(s) + horizontal scroll shadow — RecordsTable's feel */
687
699
  & th[data-sticky], & td[data-sticky] { position: sticky; z-index: 1; background: var(--crisp-bg-surface); }
688
- & th[data-sticky] { z-index: 3; }
700
+ /* `thead th[data-sticky]`, not a bare `th[data-sticky]` (crisp#848, same
701
+ bare-`th` family as #829/#833/#835): this z-index:3 was written for
702
+ 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>`
703
+ 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
704
+ footer instead of the footer showing through. Confirmed live on a
705
+ real bounded-height, 25-row table (GRC's Risk register): scrolled partway, `elementFromPoint` at the footer's own on-screen cell
706
+ resolved to a body row's `<th>`, not the footer's `<td>`, and that
707
+ `<th>`'s computed z-index read 3. Scoped to `thead`, a body row's
708
+ own sticky `<th>` now falls back to the base rule above (z-index 1,
709
+ correct -- the same as a plain sticky `<td>`), and the footer's own explicit z-index below is unaffected either way. */
710
+ & thead th[data-sticky] { z-index: 3; }
689
711
  /* #554: a footer sticky-column cell sits in the bottom-sticky row (table.recipe.ts's
690
712
  `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. */
691
713
  & tfoot td[data-sticky] { z-index: 2; }
@@ -734,68 +756,66 @@
734
756
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
735
757
  & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
736
758
  & .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); }
737
- /* #579: click-to-expand overlay for a truncated cell -- a read-only,
738
- full-value card, settled live as click (not Attio's usual hover-expand) and measured position: absolute, unlike this file's
739
- floating chrome (Menu/Select popovers) which portals and escapes the
740
- scroll container. Deliberately NOT the edit-error pattern above
741
- (grow the row, block-level, never clipped): a virtualized grid
742
- assumes a fixed row height for its scroll math (owned by the grid
743
- core, not this cell), so growing a row here would fight it. Anchored
744
- to the SAME column width the cell itself renders at (measured:
745
- Attio's own overlay came out only slightly wider than the triggering cell's clientWidth -- the overshoot is this padding),
746
- wrapping onto as many lines as the value needs; it CAN be clipped by
747
- `.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. */
759
+ /* #826 -- the full-body loading state (a filter/sort re-query, or a
760
+ 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. */
761
+ & .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; }
762
+ & .crisp-datatable-loading .crisp-spinner { border-top-color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
763
+ /* #579: click-to-expand overlay for a truncated cell -- a read-only, full-value card, settled live as click (not Attio's usual
764
+ 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
765
+ 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
766
+ visible viewport, same as the edit-error comment above already
767
+ notes for any position:absolute escape -- unmeasured against Attio
768
+ for this exact case, flagged rather than guessed. */
748
769
  & td[data-cell-expanded] { overflow: visible; z-index: 7; }
749
- & .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; }
770
+ & .crisp-datatable-cell-overlay {
771
+ position: absolute; inset-inline-start: 0; top: 0; width: 100%; box-sizing: border-box;
772
+ padding: var(--crisp-space-2) var(--crisp-space-3); background: var(--crisp-bg-surface);
773
+ border: 1px solid var(--crisp-bg-brand-solid); border-radius: 4px;
774
+ font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium);
775
+ color: var(--crisp-fg-primary); white-space: normal; word-break: break-word; cursor: default;
776
+ }
750
777
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
751
778
  & table { table-layout: fixed; }
752
- /* 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. */
779
+ /* read-only/computed column marker (#596) -- sits just inside the
780
+ resize handle, same absolute-positioned-against-the-th approach the
781
+ handle itself uses, since th has zero padding and the button/inner
782
+ row fills the whole cell with no room left for a trailing sibling. */
753
783
  & .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; }
754
784
  /* column resize grip */
755
785
  & .crisp-datatable-resize { position: absolute; top: 0; inset-inline-end: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
756
786
  & .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; }
757
787
  & .crisp-datatable-resize:hover::after, & .crisp-datatable-resize[data-resizing]::after { opacity: 1; }
758
- /* crisp/#731 -- column drag-to-reorder. Measured live against Attio's
759
- 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
760
- own hover state (above) already uses, positioned on whichever edge of
761
- the hovered header the pointer is closer to. */
788
+ /* crisp/#731 -- column drag-to-reorder. Measured live against Attio's own grid (the issue's own comment): the dragged header keeps full
789
+ opacity and the plain `pointer` cursor throughout (Attio draws no
790
+ ghost and no "grabbing" cursor of its own), so this adds nothing to
791
+ `[data-dragging]` itself -- the ONLY new paint is the drop-indicator
792
+ 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. */
762
793
  & th[data-dragging] { cursor: pointer; }
763
- & th[data-drop-before]::before, & th[data-drop-after]::after {
764
- content: ""; position: absolute; top: 0; width: 3px; height: 100%;
765
- background: var(--crisp-bg-brand-solid); z-index: 6; pointer-events: none;
766
- }
794
+ & 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; }
767
795
  & th[data-drop-before]::before { inset-inline-start: -1.5px; }
768
796
  & th[data-drop-after]::after { inset-inline-end: -1.5px; }
769
797
  /* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
770
798
  & .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); }
771
- /* crisp/#601 supersession follow-up -- the same flex-row shape
772
- .crisp-datatable-entity gives its own avatar+name+actions, but
773
- structural only (no entity-specific colour/weight): lets
774
- 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. */
799
+ /* 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
800
+ content (a custom render, or a non-entity built-in type), not just
801
+ type: "entity"'s own inline rendering. */
775
802
  & .crisp-datatable-cell-actions-row { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; width: 100%; }
776
- /* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text,
777
- 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
778
- colour. The previous :hover-gated, full-contrast rule here was itself
779
- unmeasured and wrong in two ways at once. */
803
+ /* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text, plain hover vs rest) that Attio's underline is unconditional, not a
804
+ hover reveal -- identical at rest and on hover: text-decoration-color
805
+ 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. */
780
806
  & .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); }
781
807
  & 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%; }
782
- /* crisp/#601, G-048: two 20px ghost icons, right-aligned in the primary
783
- column, revealed on ROW hover (the issue's own title says "row
784
- hover", and a live check confirmed hovering well past the name -- not just the icons' own hit target -- still reveals them) or
785
- keyboard focus. Measured live: border-radius 6px, icon colour
786
- rgba(0,0,0,0.55) at rest, darkening to the full text colour with a
787
- 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. */
808
+ /* 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
809
+ hover", and a live check confirmed hovering well past the name --
810
+ 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
811
+ expressed as color-mix against fgPrimary so they stay correct in
812
+ dark mode instead of a literal black that would invert. */
788
813
  & .crisp-datatable-entity-actions { display: flex; align-items: center; gap: var(--crisp-space-1-5); margin-inline-start: auto; flex-shrink: 0; opacity: 0; }
789
814
  & tr:hover .crisp-datatable-entity-actions, & .crisp-datatable-entity-actions:focus-within { opacity: 1; }
790
815
  & .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; }
791
816
  & .crisp-datatable-entity-action:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
792
817
  & .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); }
793
- & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
794
- & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
795
- & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); }
796
- & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); }
797
- & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); }
798
- & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
818
+ & .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); }
799
819
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
800
820
  & .crisp-datatable-tags { display: flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
801
821
  & .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); }
@@ -808,22 +828,26 @@
808
828
  & .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)); }
809
829
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
810
830
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
811
- /* crisp#822 -- a date/datetime cell's danger/overdue tone: same token as
812
- TasksTable's own overdue due-date (crisp#462), plain text colour, no fill/weight change. */
831
+ /* 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
832
+ fill/weight change. */
813
833
  & .crisp-datatable-danger { color: var(--crisp-fg-danger); }
814
- /* url/email cell types (#654) -- same measured values as RecordsTable's
815
- own .crisp-recordstable-link, which this mirrors: no underline at
816
- rest, underline on hover, own-colour text rather than a browser-blue
817
- link. */
834
+ /* 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. */
818
835
  & .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
819
836
  & .crisp-datatable-link:hover { text-decoration: underline; }
820
- /* phone cell type/editor (#765) -- measured live: the dial-code prefix
821
- is a muted rgba(0,0,0,0.63) at 14px, matching every other muted-text
822
- value already measured elsewhere in this file. */
837
+ /* 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. */
823
838
  & .crisp-datatable-phone { display: inline-flex; align-items: center; gap: var(--crisp-space-1); }
824
839
  & .crisp-datatable-phone-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
825
840
  & .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); }
826
841
  & .crisp-datatable-phone-editor input { flex: 1; min-width: 0; }
842
+ /* country-picker option row (#842) -- confirmed live: Attio's own popover
843
+ is plain text, no flag anywhere (not the option row, not the closed
844
+ trigger), name opaque + dial code muted rgba(0,0,0,0.63) -- the SAME
845
+ color-mix figure as -phone-dial above. The internal name/dial gap
846
+ wasn't independently measured this pass (Attio's own two <div>s gave
847
+ no exact number); reused space["1"], the same gap -phone already uses
848
+ for its own flag/dial/digits row. */
849
+ & .crisp-datatable-country-option { display: flex; align-items: center; gap: var(--crisp-space-1); }
850
+ & .crisp-datatable-country-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
827
851
  /* multi-value phone (#816) -- confirmed live (a real custom "Multiple
828
852
  values" Phone Number attribute, and the built-in People "Phone
829
853
  numbers" field): the collapsed cell is a 4px-gap flex row of real
@@ -1868,8 +1892,16 @@
1868
1892
  & th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-3); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1869
1893
  /* #554: sticky on the vertical axis, mirroring RecordsTable's own
1870
1894
  `.crisp-recordstable-th`. z-index 2 sits above a plain body cell (auto)
1871
- and below a sticky-column corner cell (DataTable's `th[data-sticky]`, z-index 3), so the corner always wins. */
1872
- & th { position: sticky; top: 0; z-index: 2; height: var(--crisp-size-tableHeader); background: var(--crisp-bg-surface); border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); user-select: none; }
1895
+ and below a sticky-column corner cell (DataTable's `th[data-sticky]`, z-index 3), so the corner always wins. `thead th`, not a bare `th` (crisp#833/#835): DataTable's primary
1896
+ column renders its BODY cell as a real `<th scope="row">` (#779, for
1897
+ a11y), and a bare `th` here matched that one too -- every property in
1898
+ this rule leaked onto it: `height: tableHeader` (40px) stretched the
1899
+ whole body row to 40px instead of 36 (#833, confirmed live on the
1900
+ showcase, every td in the row equalized to the one th); `color:
1901
+ color-mix(...63%...)` muted the primary column's own text below every sibling `<td>`'s full-opacity color (#835, re-measured live against
1902
+ Attio's own header labels -- including a genuinely computed one, "Twitter follower count" -- all fully opaque `rgb(16,17,18)`, no muting at all, so this drops the color-mix entirely rather than narrowing it); `position:sticky; top:0` tried to stick a BODY row to the scroll container's top; the extra `border-top` doubled that
1903
+ row's own grid line. `thead th` simply never matches `tbody th[scope=row]` at all, so none of this reaches it; the base `& th, & td` rule above (36px, one border-bottom, full opacity via `& table`'s own color) applies to it like every other cell. */
1904
+ & thead th { position: sticky; top: 0; z-index: 2; height: var(--crisp-size-tableHeader); background: var(--crisp-bg-surface); border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: var(--crisp-fg-primary); user-select: none; }
1873
1905
  & td { color: var(--crisp-fg-primary); background: var(--crisp-bg-surface); }
1874
1906
  & th:last-child, & td:last-child { border-inline-end: none; }
1875
1907
  & tbody tr:last-child td { border-bottom: none; }
@@ -1888,7 +1920,7 @@
1888
1920
  }
1889
1921
  .crisp-table--surface_card {
1890
1922
  border: 1px solid var(--crisp-border-subtle); border-radius: var(--crisp-radius-xl); overflow: hidden;
1891
- & th { border-top: none; }
1923
+ & thead th { border-top: none; }
1892
1924
  & tfoot td { border-bottom: none; }
1893
1925
  }
1894
1926
  .crisp-table--surface_bleed {
@@ -1912,7 +1944,9 @@
1912
1944
  & .crisp-tabletoolbar-search svg { flex-shrink: 0; }
1913
1945
  & .crisp-tabletoolbar-search input { border: none; outline: none; background: transparent; font-family: inherit; font-size: var(--crisp-text-md); color: var(--crisp-fg-primary); width: 180px; }
1914
1946
  & .crisp-tabletoolbar-search input::placeholder { color: var(--crisp-fg-muted); }
1915
- & .crisp-tabletoolbar-tool { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-divider); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
1947
+ /* crisp/#830: every trigger label here (Sort, Filter, View settings, Import/Export -- all compose this one shared .tool rule) read the "md" text size, 13px. Re-measured on Attio's own trigger LEAF node
1948
+ (the button root itself inherits Attio's own 10px rem base and is a red herring, the same trap the original "10px font" layout-audit finding fell into): every label leaf is 14px/500/lh20, the "lg" text size. */
1949
+ & .crisp-tabletoolbar-tool { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-divider); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
1916
1950
  & .crisp-tabletoolbar-tool:hover { background: var(--crisp-bg-raised); }
1917
1951
  & .crisp-tabletoolbar-tool svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
1918
1952
  /* crisp/#772: re-measured live on Sort with an actual sort applied -- an active trigger's own fill is white (bgSurface), the same as the
@@ -1931,8 +1965,17 @@
1931
1965
  an actual sort applied. color-mix (not the raw rgba Attio computes
1932
1966
  to) so both the border and the fill flip correctly in dark mode, the
1933
1967
  same reasoning --ghost above already uses. Only wired into
1934
- TableToolbar itself (tabletoolbar.tsx) this pass -- RecordsTable's own Sort/Filter triggers share this same chrome function but are deprecated, not touched here. */
1935
- & .crisp-tabletoolbar-tool--dashed { position: relative; box-shadow: none; background: color-mix(in srgb, var(--crisp-fg-primary) 2%, transparent); }
1968
+ TableToolbar itself (tabletoolbar.tsx) this pass -- RecordsTable's own Sort/Filter triggers share this same chrome function but are deprecated, not touched here. crisp/#844, correcting #831: the label colour was added here later, muting only the FILTER trigger (via a "--filter" modifier), on the assumption that was a Filter-specific tone distinct from Sort's.
1969
+ Re-measured live and that premise was wrong -- Sort's own resting label ("Sort", no condition set) is the exact SAME muted rgba(0,0,0,.5) as Filter's, not full-strength. (The rgba(0,0,0,.63) that looked like
1970
+ "Sort's colour" in an earlier pass is a different thing entirely: the
1971
+ "Sorted by" PREFIX word inside an ACTIVE sort chip, #788 -- a
1972
+ compound-label treatment that only exists once a sort is applied, not
1973
+ a resting-state colour.) --dashed is currently only ever applied to
1974
+ Sort's and Filter's own empty triggers (never View settings/
1975
+ Import-Export, which stay full-strength always, confirmed live), so
1976
+ the mute belongs on --dashed itself, not gated behind a Filter-only
1977
+ modifier. */
1978
+ & .crisp-tabletoolbar-tool--dashed { position: relative; box-shadow: none; background: color-mix(in srgb, var(--crisp-fg-primary) 2%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 50%, transparent); }
1936
1979
  & .crisp-tabletoolbar-tool--dashed::before { content: ""; position: absolute; inset: 0; border-radius: inherit; border: 1px dashed color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); pointer-events: none; }
1937
1980
  & .crisp-tabletoolbar-tool--dashed:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
1938
1981
  & .crisp-tabletoolbar-toolgroup { position: relative; display: inline-flex; }
@@ -1972,7 +2015,11 @@
1972
2015
  & .crisp-tabletoolbar-menu-sep { height: 1px; border: none; background: var(--crisp-border-subtle); margin: var(--crisp-space-0-5) 0; }
1973
2016
  /* #592: the Filter field picker's nested-scope header (back control +
1974
2017
  search, once drilled into a nestedColumns column) and a search
1975
- result's own breadcrumb row ("Team › Twitter follower count", surfaced by the SAME query matching a nested column). menu-back reuses the established 28px icon-only control pattern this file already uses twice (sort-remove, the kebab segment) rather than an independent measurement of Attio's own back arrow. The breadcrumb
2018
+ result's own breadcrumb row ("Team › Twitter follower count",
2019
+ surfaced by the SAME query matching a nested column). menu-back
2020
+ reuses the established 28px icon-only control pattern this file
2021
+ already uses twice (sort-remove, the kebab segment) rather than
2022
+ an independent measurement of Attio's own back arrow. The breadcrumb
1976
2023
  colours ARE measured live: the parent segment muted at the same
1977
2024
  exact color-mix 63% sort-learn above already uses, the
1978
2025
  separator chevron and leaf segment both full fgPrimary (the
@@ -2016,9 +2063,15 @@
2016
2063
  & .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--val { background: var(--crisp-bg-surface); }
2017
2064
  & .crisp-tabletoolbar-fcond-seg--field { border-radius: var(--crisp-radius-md) 0 0 var(--crisp-radius-md); font-weight: var(--crisp-weight-medium); max-width: 200px; }
2018
2065
  & .crisp-tabletoolbar-fcond-seg--field svg { color: var(--crisp-fg-muted); }
2019
- /* #592: a nested pick's field segment ("Team › Twitter follower
2020
- count") — measured live: BOTH path segments the same colour (chip context only; the picker's own menu rows keep the parent muted, see
2021
- tabletoolbar.chrome.ts), each independently truncated. Attio's own layout gives the leaf segment priority (it stays fully readable longer, the parent truncates first) via a two-part min-content measurement this reproduces qualitatively with a flex-shrink differential rather than byte-for-byte (the exact px split Attio computes is content-length-dependent, not a fixed value) — not independently re-verified at other path-segment lengths. */
2066
+ /* #592: a nested pick's field segment ("Team › Twitter follower count") — measured live: BOTH path segments the same colour (chip
2067
+ context only; the picker's own menu rows keep the parent muted, see
2068
+ tabletoolbar.chrome.ts), each independently truncated. Attio's own
2069
+ layout gives the leaf segment priority (it stays fully readable
2070
+ longer, the parent truncates first) via a two-part min-content
2071
+ measurement this reproduces qualitatively with a flex-shrink
2072
+ differential rather than byte-for-byte (the exact px split Attio
2073
+ computes is content-length-dependent, not a fixed value) — not
2074
+ independently re-verified at other path-segment lengths. */
2022
2075
  & .crisp-tabletoolbar-fcond-path { display: inline-flex; align-items: center; gap: 2px; min-width: 0; max-width: 178px; }
2023
2076
  & .crisp-tabletoolbar-fcond-path-seg { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1 1 auto; }
2024
2077
  & .crisp-tabletoolbar-fcond-path-seg:first-child { flex: 0 1 auto; }
@@ -2028,8 +2081,10 @@
2028
2081
  & .crisp-tabletoolbar-fcond-seg--kebab { background: var(--crisp-bg-surface); justify-content: center; width: 28px; padding: 0; border-radius: 0 var(--crisp-radius-md) var(--crisp-radius-md) 0; color: var(--crisp-fg-muted); }
2029
2082
  & .crisp-tabletoolbar-fcond-seg--kebab:hover { color: var(--crisp-fg-primary); }
2030
2083
  /* #754: the sort row's own field trigger, replacing a plain Select
2031
- with the same nested-drill-down popover Filter's field picker already has — reuses Select's own already-measured trigger chrome
2032
- verbatim (its class is scoped under Select's own root class, so it can't be reused as a bare class name outside an actual Select tree;
2084
+ with the same nested-drill-down popover Filter's field picker
2085
+ already has — reuses Select's own already-measured trigger chrome
2086
+ verbatim (its class is scoped under Select's own root class, so it
2087
+ can't be reused as a bare class name outside an actual Select tree;
2033
2088
  these values are copied, not re-measured). */
2034
2089
  & .crisp-tabletoolbar-sort-field-picker { position: relative; display: inline-flex; flex: 1; min-width: 0; }
2035
2090
  & .crisp-tabletoolbar-sort-field-trigger { box-sizing: border-box; display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: var(--crisp-size-menuRow); width: 100%; min-width: 0; margin: 0; padding: 0 var(--crisp-space-1-5) 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); cursor: pointer; transition: background-color .12s ease, box-shadow .12s ease; }
package/dist/table.css CHANGED
@@ -12,8 +12,16 @@
12
12
  & th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-3); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
13
13
  /* #554: sticky on the vertical axis, mirroring RecordsTable's own
14
14
  `.crisp-recordstable-th`. z-index 2 sits above a plain body cell (auto)
15
- and below a sticky-column corner cell (DataTable's `th[data-sticky]`, z-index 3), so the corner always wins. */
16
- & th { position: sticky; top: 0; z-index: 2; height: var(--crisp-size-tableHeader); background: var(--crisp-bg-surface); border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); user-select: none; }
15
+ and below a sticky-column corner cell (DataTable's `th[data-sticky]`, z-index 3), so the corner always wins. `thead th`, not a bare `th` (crisp#833/#835): DataTable's primary
16
+ column renders its BODY cell as a real `<th scope="row">` (#779, for
17
+ a11y), and a bare `th` here matched that one too -- every property in
18
+ this rule leaked onto it: `height: tableHeader` (40px) stretched the
19
+ whole body row to 40px instead of 36 (#833, confirmed live on the
20
+ showcase, every td in the row equalized to the one th); `color:
21
+ color-mix(...63%...)` muted the primary column's own text below every sibling `<td>`'s full-opacity color (#835, re-measured live against
22
+ Attio's own header labels -- including a genuinely computed one, "Twitter follower count" -- all fully opaque `rgb(16,17,18)`, no muting at all, so this drops the color-mix entirely rather than narrowing it); `position:sticky; top:0` tried to stick a BODY row to the scroll container's top; the extra `border-top` doubled that
23
+ row's own grid line. `thead th` simply never matches `tbody th[scope=row]` at all, so none of this reaches it; the base `& th, & td` rule above (36px, one border-bottom, full opacity via `& table`'s own color) applies to it like every other cell. */
24
+ & thead th { position: sticky; top: 0; z-index: 2; height: var(--crisp-size-tableHeader); background: var(--crisp-bg-surface); border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: var(--crisp-fg-primary); user-select: none; }
17
25
  & td { color: var(--crisp-fg-primary); background: var(--crisp-bg-surface); }
18
26
  & th:last-child, & td:last-child { border-inline-end: none; }
19
27
  & tbody tr:last-child td { border-bottom: none; }
@@ -32,7 +40,7 @@
32
40
  }
33
41
  .crisp-table--surface_card {
34
42
  border: 1px solid var(--crisp-border-subtle); border-radius: var(--crisp-radius-xl); overflow: hidden;
35
- & th { border-top: none; }
43
+ & thead th { border-top: none; }
36
44
  & tfoot td { border-bottom: none; }
37
45
  }
38
46
  .crisp-table--surface_bleed {
@@ -13,8 +13,16 @@
13
13
  & th, & td { position: relative; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-3); border-bottom: 1px solid var(--crisp-border-subtle); border-inline-end: 1px solid var(--crisp-border-subtle); text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
14
14
  /* #554: sticky on the vertical axis, mirroring RecordsTable's own
15
15
  `.crisp-recordstable-th`. z-index 2 sits above a plain body cell (auto)
16
- and below a sticky-column corner cell (DataTable's `th[data-sticky]`, z-index 3), so the corner always wins. */
17
- & th { position: sticky; top: 0; z-index: 2; height: var(--crisp-size-tableHeader); background: var(--crisp-bg-surface); border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); user-select: none; }
16
+ and below a sticky-column corner cell (DataTable's `th[data-sticky]`, z-index 3), so the corner always wins. `thead th`, not a bare `th` (crisp#833/#835): DataTable's primary
17
+ column renders its BODY cell as a real `<th scope="row">` (#779, for
18
+ a11y), and a bare `th` here matched that one too -- every property in
19
+ this rule leaked onto it: `height: tableHeader` (40px) stretched the
20
+ whole body row to 40px instead of 36 (#833, confirmed live on the
21
+ showcase, every td in the row equalized to the one th); `color:
22
+ color-mix(...63%...)` muted the primary column's own text below every sibling `<td>`'s full-opacity color (#835, re-measured live against
23
+ Attio's own header labels -- including a genuinely computed one, "Twitter follower count" -- all fully opaque `rgb(16,17,18)`, no muting at all, so this drops the color-mix entirely rather than narrowing it); `position:sticky; top:0` tried to stick a BODY row to the scroll container's top; the extra `border-top` doubled that
24
+ row's own grid line. `thead th` simply never matches `tbody th[scope=row]` at all, so none of this reaches it; the base `& th, & td` rule above (36px, one border-bottom, full opacity via `& table`'s own color) applies to it like every other cell. */
25
+ & thead th { position: sticky; top: 0; z-index: 2; height: var(--crisp-size-tableHeader); background: var(--crisp-bg-surface); border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; color: var(--crisp-fg-primary); user-select: none; }
18
26
  & td { color: var(--crisp-fg-primary); background: var(--crisp-bg-surface); }
19
27
  & th:last-child, & td:last-child { border-inline-end: none; }
20
28
  & tbody tr:last-child td { border-bottom: none; }
@@ -33,7 +41,7 @@
33
41
  }
34
42
  .crisp-table--surface_card {
35
43
  border: 1px solid var(--crisp-border-subtle); border-radius: var(--crisp-radius-xl); overflow: hidden;
36
- & th { border-top: none; }
44
+ & thead th { border-top: none; }
37
45
  & tfoot td { border-bottom: none; }
38
46
  }
39
47
  .crisp-table--surface_bleed {
@@ -7,7 +7,9 @@
7
7
  & .crisp-tabletoolbar-search svg { flex-shrink: 0; }
8
8
  & .crisp-tabletoolbar-search input { border: none; outline: none; background: transparent; font-family: inherit; font-size: var(--crisp-text-md); color: var(--crisp-fg-primary); width: 180px; }
9
9
  & .crisp-tabletoolbar-search input::placeholder { color: var(--crisp-fg-muted); }
10
- & .crisp-tabletoolbar-tool { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-divider); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
10
+ /* crisp/#830: every trigger label here (Sort, Filter, View settings, Import/Export -- all compose this one shared .tool rule) read the "md" text size, 13px. Re-measured on Attio's own trigger LEAF node
11
+ (the button root itself inherits Attio's own 10px rem base and is a red herring, the same trap the original "10px font" layout-audit finding fell into): every label leaf is 14px/500/lh20, the "lg" text size. */
12
+ & .crisp-tabletoolbar-tool { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-divider); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
11
13
  & .crisp-tabletoolbar-tool:hover { background: var(--crisp-bg-raised); }
12
14
  & .crisp-tabletoolbar-tool svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
13
15
  /* crisp/#772: re-measured live on Sort with an actual sort applied -- an active trigger's own fill is white (bgSurface), the same as the
@@ -26,8 +28,17 @@
26
28
  an actual sort applied. color-mix (not the raw rgba Attio computes
27
29
  to) so both the border and the fill flip correctly in dark mode, the
28
30
  same reasoning --ghost above already uses. Only wired into
29
- TableToolbar itself (tabletoolbar.tsx) this pass -- RecordsTable's own Sort/Filter triggers share this same chrome function but are deprecated, not touched here. */
30
- & .crisp-tabletoolbar-tool--dashed { position: relative; box-shadow: none; background: color-mix(in srgb, var(--crisp-fg-primary) 2%, transparent); }
31
+ TableToolbar itself (tabletoolbar.tsx) this pass -- RecordsTable's own Sort/Filter triggers share this same chrome function but are deprecated, not touched here. crisp/#844, correcting #831: the label colour was added here later, muting only the FILTER trigger (via a "--filter" modifier), on the assumption that was a Filter-specific tone distinct from Sort's.
32
+ Re-measured live and that premise was wrong -- Sort's own resting label ("Sort", no condition set) is the exact SAME muted rgba(0,0,0,.5) as Filter's, not full-strength. (The rgba(0,0,0,.63) that looked like
33
+ "Sort's colour" in an earlier pass is a different thing entirely: the
34
+ "Sorted by" PREFIX word inside an ACTIVE sort chip, #788 -- a
35
+ compound-label treatment that only exists once a sort is applied, not
36
+ a resting-state colour.) --dashed is currently only ever applied to
37
+ Sort's and Filter's own empty triggers (never View settings/
38
+ Import-Export, which stay full-strength always, confirmed live), so
39
+ the mute belongs on --dashed itself, not gated behind a Filter-only
40
+ modifier. */
41
+ & .crisp-tabletoolbar-tool--dashed { position: relative; box-shadow: none; background: color-mix(in srgb, var(--crisp-fg-primary) 2%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 50%, transparent); }
31
42
  & .crisp-tabletoolbar-tool--dashed::before { content: ""; position: absolute; inset: 0; border-radius: inherit; border: 1px dashed color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); pointer-events: none; }
32
43
  & .crisp-tabletoolbar-tool--dashed:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
33
44
  & .crisp-tabletoolbar-toolgroup { position: relative; display: inline-flex; }
@@ -67,7 +78,11 @@
67
78
  & .crisp-tabletoolbar-menu-sep { height: 1px; border: none; background: var(--crisp-border-subtle); margin: var(--crisp-space-0-5) 0; }
68
79
  /* #592: the Filter field picker's nested-scope header (back control +
69
80
  search, once drilled into a nestedColumns column) and a search
70
- result's own breadcrumb row ("Team › Twitter follower count", surfaced by the SAME query matching a nested column). menu-back reuses the established 28px icon-only control pattern this file already uses twice (sort-remove, the kebab segment) rather than an independent measurement of Attio's own back arrow. The breadcrumb
81
+ result's own breadcrumb row ("Team › Twitter follower count",
82
+ surfaced by the SAME query matching a nested column). menu-back
83
+ reuses the established 28px icon-only control pattern this file
84
+ already uses twice (sort-remove, the kebab segment) rather than
85
+ an independent measurement of Attio's own back arrow. The breadcrumb
71
86
  colours ARE measured live: the parent segment muted at the same
72
87
  exact color-mix 63% sort-learn above already uses, the
73
88
  separator chevron and leaf segment both full fgPrimary (the
@@ -111,9 +126,15 @@
111
126
  & .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--val { background: var(--crisp-bg-surface); }
112
127
  & .crisp-tabletoolbar-fcond-seg--field { border-radius: var(--crisp-radius-md) 0 0 var(--crisp-radius-md); font-weight: var(--crisp-weight-medium); max-width: 200px; }
113
128
  & .crisp-tabletoolbar-fcond-seg--field svg { color: var(--crisp-fg-muted); }
114
- /* #592: a nested pick's field segment ("Team › Twitter follower
115
- count") — measured live: BOTH path segments the same colour (chip context only; the picker's own menu rows keep the parent muted, see
116
- tabletoolbar.chrome.ts), each independently truncated. Attio's own layout gives the leaf segment priority (it stays fully readable longer, the parent truncates first) via a two-part min-content measurement this reproduces qualitatively with a flex-shrink differential rather than byte-for-byte (the exact px split Attio computes is content-length-dependent, not a fixed value) — not independently re-verified at other path-segment lengths. */
129
+ /* #592: a nested pick's field segment ("Team › Twitter follower count") — measured live: BOTH path segments the same colour (chip
130
+ context only; the picker's own menu rows keep the parent muted, see
131
+ tabletoolbar.chrome.ts), each independently truncated. Attio's own
132
+ layout gives the leaf segment priority (it stays fully readable
133
+ longer, the parent truncates first) via a two-part min-content
134
+ measurement this reproduces qualitatively with a flex-shrink
135
+ differential rather than byte-for-byte (the exact px split Attio
136
+ computes is content-length-dependent, not a fixed value) — not
137
+ independently re-verified at other path-segment lengths. */
117
138
  & .crisp-tabletoolbar-fcond-path { display: inline-flex; align-items: center; gap: 2px; min-width: 0; max-width: 178px; }
118
139
  & .crisp-tabletoolbar-fcond-path-seg { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1 1 auto; }
119
140
  & .crisp-tabletoolbar-fcond-path-seg:first-child { flex: 0 1 auto; }
@@ -123,8 +144,10 @@
123
144
  & .crisp-tabletoolbar-fcond-seg--kebab { background: var(--crisp-bg-surface); justify-content: center; width: 28px; padding: 0; border-radius: 0 var(--crisp-radius-md) var(--crisp-radius-md) 0; color: var(--crisp-fg-muted); }
124
145
  & .crisp-tabletoolbar-fcond-seg--kebab:hover { color: var(--crisp-fg-primary); }
125
146
  /* #754: the sort row's own field trigger, replacing a plain Select
126
- with the same nested-drill-down popover Filter's field picker already has — reuses Select's own already-measured trigger chrome
127
- verbatim (its class is scoped under Select's own root class, so it can't be reused as a bare class name outside an actual Select tree;
147
+ with the same nested-drill-down popover Filter's field picker
148
+ already has — reuses Select's own already-measured trigger chrome
149
+ verbatim (its class is scoped under Select's own root class, so it
150
+ can't be reused as a bare class name outside an actual Select tree;
128
151
  these values are copied, not re-measured). */
129
152
  & .crisp-tabletoolbar-sort-field-picker { position: relative; display: inline-flex; flex: 1; min-width: 0; }
130
153
  & .crisp-tabletoolbar-sort-field-trigger { box-sizing: border-box; display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: var(--crisp-size-menuRow); width: 100%; min-width: 0; margin: 0; padding: 0 var(--crisp-space-1-5) 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); cursor: pointer; transition: background-color .12s ease, box-shadow .12s ease; }