@crisp-ui-kit/crisp 0.55.1 → 0.55.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/styles.css CHANGED
@@ -499,16 +499,26 @@
499
499
  radius, and datatable-grip and the embedded pickers zero theirs. An 8px
500
500
  radius survived the chip-to-full-cell conversion and shipped rounded
501
501
  until #282, so the spec check now guards it. */
502
- /* The hit target is the WHOLE summary cell -- measured 246x37 with
502
+ /* The hit target is the WHOLE summary cell -- measured 246x36 with
503
503
  padding 0 12px, not a content-sized chip. Clicking anywhere in the cell
504
- opens the aggregation menu. */
505
- & .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; }
504
+ opens the aggregation menu.
505
+ No `min-height` (crisp#834, found reproducing the footer-row-38px-
506
+ 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,
507
+ border-box) leaves 34px of content box once its 2px of top+bottom
508
+ border are counted, and `min-height: tableRow` here demanded 36px
509
+ of CONTENT height regardless -- overflowing the cell by 2px, which
510
+ auto-height table layout resolves by growing the whole row to fit,
511
+ landing at exactly the reported 38. `height: 100%` alone already
512
+ fills whatever the cell's real content box is (the doc comment above this rule's own stated intent), with nothing left demanding more. */
513
+ & .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; }
506
514
  & .crisp-columncalc-trigger:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
507
515
  & .crisp-columncalc-trigger svg { flex-shrink: 0; opacity: 0.7; }
508
516
  & .crisp-columncalc-trigger[data-align="right"] { justify-content: flex-end; }
509
517
  & .crisp-columncalc-placeholder { overflow: hidden; text-overflow: ellipsis; }
510
518
  & .crisp-columncalc-value { color: var(--crisp-fg-primary); font-variant-numeric: tabular-nums; }
511
- & .crisp-columncalc-op { color: var(--crisp-fg-muted); }
519
+ /* 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
520
+ `fgMuted` token (a close but not measured match). */
521
+ & .crisp-columncalc-op { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
512
522
  & .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); }
513
523
  }
514
524
 
@@ -639,54 +649,58 @@
639
649
  class + one element) -- a bare `& th` here only won by source
640
650
  order, which flipped the moment table.recipe.ts's own CSS file happened to land after datatable's in the build (alphabetical:
641
651
  datatable < table), silently reinstating 12px on every header cell
642
- 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. */
643
- & .crisp-table th { padding: 0; }
644
- /* Same wrapper trap as AUTHORING.md Lesson 40 (ColumnCalc's summary trigger):
645
- Menu wraps its trigger in a flex div that sizes to its content, so a
646
- long header grew past the column instead of filling it -- with nothing
647
- 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
648
- it with no "...". Fill the wrapper, scoped to th so Menu's default (correct everywhere else) is untouched. */
652
+ 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
653
+ `th` here matches that one too, zeroing an inset nothing else
654
+ supplies -- the primary column's text sat flush left, one column's
655
+ cell no longer matching its own header or any sibling `<td>`. Only
656
+ 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,
657
+ and needs the SAME base 12px (table.recipe.ts, #465) every other
658
+ cell already gets. */
659
+ & .crisp-table thead th { padding: 0; }
660
+ /* 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
661
+ -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
662
+ (correct everywhere else) is untouched. */
649
663
  & th .crisp-menu { width: 100%; height: 100%; }
650
664
  & .crisp-datatable-th-inner { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; min-width: 0; height: 100%; }
651
665
  & .crisp-datatable-th-inner > svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
652
- /* 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
653
- columnMenu/sortable at all) renders .crisp-datatable-th-inner as a
654
- DIRECT child of th instead of nested inside a button -- and loses
655
- the button's own 12px inset, drifting left of the td's 12px base
656
- 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
657
- 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
658
- plain sort `button` both put >=1 more level between `th` and this
659
- span, so their own 12px (already on the button) isn't doubled. */
666
+ /* crisp/#605 follow-up: a header with neither a Menu nor a sort button
667
+ (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
668
+ 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
669
+ padding+icon+gap combinations -- Attio does not leave the inert
670
+ primary header flush left either. The `>` combinator targets only
671
+ 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. */
660
672
  & th > .crisp-datatable-th-inner { box-sizing: border-box; padding: 0 var(--crisp-space-3); }
661
- /* 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. */
673
+ /* The summary cell has NO padding of its own -- the calculation trigger
674
+ owns the whole 246x37 area so a click anywhere in the cell opens the
675
+ menu. Zero the cell's inset and let ColumnCalc supply the 12px. */
662
676
  & tfoot td { padding: 0; }
663
- /* Group heading row -- a view is grouped under a subtle label with the
664
- bucket's count beside it. */
665
- & .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; }
677
+ /* Group heading row -- a view is grouped under a subtle label with the bucket's count beside it. */
678
+ & .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; }
666
679
  & .crisp-datatable-viewsettings:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
667
680
  & .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); }
668
- & .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); }
681
+ & .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); }
669
682
  & .crisp-datatable-group-count { margin-inline-start: var(--crisp-space-1-5); color: var(--crisp-fg-muted); font-variant-numeric: tabular-nums; }
670
683
  & .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; }
671
684
  & .crisp-datatable-th-rename:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
672
685
  & .crisp-datatable-th-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
673
- /* #465: 12px per the comment above (and Attio's measured header cell
674
- padding: 0 12px) -- was still c.space["2"] (8px), drifted from its
675
- own documented value. */
686
+ /* #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. */
676
687
  & .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; }
677
- /* 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
678
- count") keeps the icon+label left and only the trailing column-menu
679
- icon sits pinned to the right edge, independent of the label's width (#406, reversing #393's own justify-content: flex-end here). The
680
- 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
681
- the trailing icon on Attio too. */
688
+ /* A right-aligned column's header keeps its field-icon + label packed at
689
+ the START, matching its own values only in that the VALUES right-align
690
+ -- Attio's own right-aligned header (measured live: "Twitter follower
691
+ 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
692
+ (#406, reversing #393's own justify-content: flex-end here). The chevron's own margin-inline-start: auto below already pins IT to the
693
+ end of a flex-start row with no override needed -- that's what pins the trailing icon on Attio too. */
682
694
  & .crisp-datatable-chevron { margin-inline-start: auto; flex-shrink: 0; color: var(--crisp-fg-muted); opacity: 0; transition: opacity .12s; }
683
695
  & .crisp-datatable-sort:hover .crisp-datatable-chevron { opacity: 0.6; }
684
696
  & .crisp-datatable-sort[data-sorted] .crisp-datatable-chevron { opacity: 1; color: var(--crisp-fg-primary); }
685
697
  /* sticky first column(s) + horizontal scroll shadow — RecordsTable's feel */
686
698
  & th[data-sticky], & td[data-sticky] { position: sticky; z-index: 1; background: var(--crisp-bg-surface); }
687
- & th[data-sticky] { z-index: 3; }
688
- /* #554: a footer sticky-column cell sits in the bottom-sticky row (table.recipe.ts's
689
- `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. */
699
+ & th[data-sticky] { z-index: 3;
700
+ } /* #554: a footer sticky-column cell sits in the bottom-sticky row (table.recipe.ts's
701
+ `tfoot td { z-index: 2 }
702
+ `), which `td[data-sticky]`'s own higher-specificity
703
+ `z-index: 1` would otherwise undercut -- keep it level with the row it's in. */
690
704
  & tfoot td[data-sticky] { z-index: 2; }
691
705
  & tbody tr[data-selected] td[data-sticky] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
692
706
  & tbody tr:hover td[data-sticky] { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); }
@@ -703,36 +717,41 @@
703
717
  & tbody tr[data-selected] td { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
704
718
  & tbody tr[data-selected]:hover td { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface))); }
705
719
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
706
- /* crisp/#758: AI-enriched column marker, ported from RecordsTable's own
707
- already-measured value -- a full-bleed #9b69ff (bgPurple) wash at 4%,
708
- composited OVER whatever row-state background the cell already has
709
- (hover, selected), which is why it's an ::after overlay and not a plain background. */
720
+ /* crisp/#758: AI-enriched column marker, ported from RecordsTable's own already-measured value -- a full-bleed #9b69ff (bgPurple) wash at 4%, composited OVER whatever row-state background the cell already has (hover, selected), which is why it's an ::after overlay and not a
721
+ plain background. */
710
722
  & td[data-enriched]::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: color-mix(in srgb, var(--crisp-bg-purple) 4%, transparent); }
711
723
  & .crisp-datatable-edit { width: 100%; height: 28px; margin: 0; padding: 0 var(--crisp-space-1-5); border: none; border-radius: var(--crisp-radius-sm); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); outline: none; vertical-align: middle; }
712
724
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
713
- /* An async onEdit's in-flight state (#385) -- the field/picker dims and
714
- stops accepting input rather than disappearing, so the just-typed or
715
- just-chosen value stays visible while its commit is pending. Same
716
- treatment Input already gives its own disabled state. */
725
+ /* An async onEdit's in-flight state (#385) -- the field/picker dims and stops accepting input rather than disappearing, so the just-typed or just-chosen value stays visible while its commit is pending. Same treatment Input already gives its own disabled state. */
717
726
  & .crisp-datatable-edit:disabled { opacity: 0.5; cursor: not-allowed; }
718
727
  & .crisp-datatable-edit-pending { flex-shrink: 0; margin-inline-start: var(--crisp-space-1); color: var(--crisp-fg-muted); vertical-align: middle; }
719
- /* The rejection's message, beside the cell's own content -- a sibling in
720
- the TableCell, never nested inside a column's own render output, so a hover-only wrapper (a Tooltip, say) around a custom render can't hide
721
- it. Block-level: the row grows to fit it rather than floating over
722
- the row below, so it can never end up clipped by the scroll
723
- container's overflow the way a position:fixed escape would need guarding against. */
728
+ /* The rejection's message, beside the cell's own content -- a sibling in the TableCell, never nested inside a column's own render output, so a
729
+ hover-only wrapper (a Tooltip, say) around a custom render can't hide it. Block-level: the row grows to fit it rather than floating over the row below, so it can never end up clipped by the scroll container's overflow the way a position:fixed escape would need
730
+ guarding against. */
724
731
  & .crisp-datatable-edit-error { display: block; padding: var(--crisp-space-0-5) 0 0; font-size: var(--crisp-text-sm); color: var(--crisp-bg-danger); }
725
- /* editor: "select"/"switch" -- always live, no separate edit mode, so the cell IS the picker: same chromeless-fill treatment as RecordsTable's own --picker cells (measured there already; DataTable
726
- shares RecordsTable's grid, Golden Rule 0). */
732
+ /* editor: "select"/"switch" -- always live, no separate edit mode, so
733
+ the cell IS the picker: same chromeless-fill treatment as
734
+ RecordsTable's own --picker cells (measured there already; DataTable shares RecordsTable's grid, Golden Rule 0). */
727
735
  & .crisp-datatable-td--picker { padding: 0; }
728
736
  & .crisp-datatable-edit-picker { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
729
737
  & .crisp-datatable-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; height: 100%; min-height: 0; border-radius: 0; box-shadow: none; padding: 0; }
730
738
  & .crisp-datatable-td--picker .crisp-datatable-edit-error { padding-inline-start: var(--crisp-space-2); }
731
- /* reference picker: a leading 16px avatar per option, plus a trailing "Create X" row when the consumer supplies one (#449) -- same measured values as RecordsTable's own .crisp-recordstable-refoption*. */
739
+ /* reference picker: a leading 16px avatar per option, plus a trailing
740
+ "Create X" row when the consumer supplies one (#449) -- same measured
741
+ values as RecordsTable's own .crisp-recordstable-refoption*. */
732
742
  & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
733
743
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
734
744
  & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
735
745
  & .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); }
746
+ /* #826 -- the full-body loading state (a filter/sort re-query, or a fresh page boot), measured live on Attio's Companies grid on both
747
+ triggers: icon+text centred in the row-body area, 10px gap, 14px/500
748
+ weight/20px line-height, both icon and text in the SAME muted
749
+ rgba(0,0,0,0.63) -- the same color-mix figure this file already uses
750
+ for other muted text (e.g. -phone-dial). Distinct from the cold
751
+ in-place row load (blank, no indicator) and the warm-navigation
752
+ per-cell shimmer (#599) -- neither of those swaps the whole body. */
753
+ & .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; }
754
+ & .crisp-datatable-loading .crisp-spinner { border-top-color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
736
755
  /* #579: click-to-expand overlay for a truncated cell -- a read-only,
737
756
  full-value card, settled live as click (not Attio's usual hover-expand) and measured position: absolute, unlike this file's
738
757
  floating chrome (Menu/Select popovers) which portals and escapes the
@@ -807,6 +826,9 @@
807
826
  & .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)); }
808
827
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
809
828
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
829
+ /* crisp#822 -- a date/datetime cell's danger/overdue tone: same token as
830
+ TasksTable's own overdue due-date (crisp#462), plain text colour, no fill/weight change. */
831
+ & .crisp-datatable-danger { color: var(--crisp-fg-danger); }
810
832
  /* url/email cell types (#654) -- same measured values as RecordsTable's
811
833
  own .crisp-recordstable-link, which this mirrors: no underline at
812
834
  rest, underline on hover, own-colour text rather than a browser-blue
@@ -820,26 +842,42 @@
820
842
  & .crisp-datatable-phone-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
821
843
  & .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); }
822
844
  & .crisp-datatable-phone-editor input { flex: 1; min-width: 0; }
845
+ /* country-picker option row (#842) -- confirmed live: Attio's own popover is plain text, no flag anywhere (not the option row, not the closed trigger), name opaque + dial code muted rgba(0,0,0,0.63) -- the SAME color-mix figure as -phone-dial above. The internal name/dial gap wasn't independently measured this pass (Attio's own two <div>s gave no exact number); reused space["1"], the same gap -phone already uses for its own flag/dial/digits row. */
846
+ & .crisp-datatable-country-option { display: flex; align-items: center; gap: var(--crisp-space-1); }
847
+ & .crisp-datatable-country-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
848
+ /* multi-value phone (#816) -- confirmed live (a real custom "Multiple
849
+ values" Phone Number attribute, and the built-in People "Phone
850
+ numbers" field): the collapsed cell is a 4px-gap flex row of real tel: links in a measured link blue (~rgb(59,108,232) pixel-sampled; reused as the closest already-tokened blue, brand600, rather than adding a third blue token for a delta this small), underlined, UNLIKE the single-value cell above (plain text, no link, no underline) -- multi-value is the one shape Attio makes into a real link. The cell hard-clips on overflow (no ellipsis -- confirmed live, a second value can end mid-digit with no "..."). */
851
+ & .crisp-datatable-phone-multi { display: inline-flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
852
+ & .crisp-datatable-phone-link { color: var(--crisp-bg-brand-solid); text-decoration: underline; white-space: nowrap; }
853
+ /* multi-value editor (#816) -- confirmed live: a stack of committed- value rows (32px = size.menuRow, 5px 8px padding, 8px gap between the grip/value/actions, 16px icons) above one ALWAYS-blank trailing DataPhoneEditor-shaped row -- that blank row is the only "add
854
+ another" affordance, there's no separate button for it. The
855
+ copy/delete pair reuses .crisp-datatable-entity-action's own hover-reveal chrome (not independently re-measured as its own hit target this pass, only the 16px icon glyphs were). */
856
+ & .crisp-datatable-phone-multi-editor { display: flex; flex-direction: column; }
857
+ & .crisp-datatable-phone-multi-row { display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); padding: 5px var(--crisp-space-2); }
858
+ & .crisp-datatable-phone-multi-row .crisp-datatable-phone-link { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
859
+ & .crisp-datatable-phone-multi-grip { color: var(--crisp-fg-muted); flex-shrink: 0; cursor: grab; }
860
+ & .crisp-datatable-phone-multi-actions { display: flex; align-items: center; gap: var(--crisp-space-1); margin-inline-start: auto; flex-shrink: 0; opacity: 0; }
861
+ & .crisp-datatable-phone-multi-row:hover .crisp-datatable-phone-multi-actions, & .crisp-datatable-phone-multi-actions:focus-within { opacity: 1; }
862
+ & .crisp-datatable-phone-multi-action { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; font-family: inherit; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); cursor: pointer; flex-shrink: 0; }
863
+ & .crisp-datatable-phone-multi-action:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
823
864
  /* datetime cell editor (#762) -- DatePickerField + TimePicker side by side. */
824
865
  & .crisp-datatable-datetime-editor { display: flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); }
825
- /* rating cell type/editor (#760) -- measured live: 16px stars on an
826
- 18px pitch (a 2px gap), an empty star at rgba(0,0,0,0.1), a
827
- committed/filled star at brand600 (--crisp-bg-brand-solid, an exact
828
- match to the measured #266df0), and a distinct hover-preview colour
829
- -- rgb(33,91,196) -- that's close to but NOT brand700/bgBrandSolidPressed (#1f5fd6); kept as its own literal rather than forced onto that token, since the two were measured stable and different. The read-only svg and the editable button's own inner svg both carry the
830
- crisp-datatable-rating-star class on the svg itself (not the
831
- button), so one set of fill rules covers both -- the button only
832
- adds its own reset + cursor via a separate -btn class. */
866
+ /* rating cell type/editor (#760) -- measured live: 16px stars on an 18px pitch (a 2px gap), an empty star at rgba(0,0,0,0.1), a committed/filled star at brand600 (--crisp-bg-brand-solid, an exact match to the measured #266df0), and a distinct hover-preview colour -- rgb(33,91,196) -- that's close to but NOT brand700/bgBrandSolidPressed
867
+ (#1f5fd6); kept as its own literal rather than forced onto that token,
868
+ since the two were measured stable and different. The read-only svg
869
+ and the editable button's own inner svg both carry the crisp-datatable-rating-star class on the svg itself (not the button), so one set of fill rules covers both -- the button only adds its own reset + cursor via a separate -btn class. */
833
870
  & .crisp-datatable-rating { display: inline-flex; align-items: center; gap: 2px; }
834
871
  & .crisp-datatable-rating-star { display: block; }
835
872
  & .crisp-datatable-rating-star path { fill: rgba(0, 0, 0, 0.1); }
836
873
  & .crisp-datatable-rating-star[data-filled] path { fill: var(--crisp-bg-brand-solid); }
837
874
  & .crisp-datatable-rating-star[data-hovering] path { fill: #215bc4; }
838
875
  & .crisp-datatable-rating-star-btn { display: inline-flex; padding: 0; margin: 0; border: none; background: none; font-family: inherit; cursor: pointer; }
839
- /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
840
- a leading 20px grip column, a 16px grip icon, and the row's own hairline doubling as the drop indicator (it tints toward the brand colour as the active drop target). The affordance + geometry are measured; the drag MECHANISM is crisp's existing house idiom -- HTML5
841
- draggable + dataTransfer, the same one column reorder already uses
842
- (RecordsTable's reorderColumn) -- not a proprietary pointer-events implementation. */
876
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
877
+ hairline doubling as the drop indicator (it tints toward the brand
878
+ colour as the active drop target). The affordance + geometry are
879
+ measured; the drag MECHANISM is crisp's existing house idiom -- HTML5 draggable + dataTransfer, the same one column reorder already uses (RecordsTable's reorderColumn) -- not a proprietary
880
+ pointer-events implementation. */
843
881
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
844
882
  & .crisp-datatable-grip { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; margin: 0; border: none; border-radius: 0; background: none; font-family: inherit; color: var(--crisp-fg-muted); cursor: grab; }
845
883
  & .crisp-datatable-grip:active { cursor: grabbing; }
@@ -1836,8 +1874,16 @@
1836
1874
  & 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; }
1837
1875
  /* #554: sticky on the vertical axis, mirroring RecordsTable's own
1838
1876
  `.crisp-recordstable-th`. z-index 2 sits above a plain body cell (auto)
1839
- and below a sticky-column corner cell (DataTable's `th[data-sticky]`, z-index 3), so the corner always wins. */
1840
- & 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; }
1877
+ 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
1878
+ column renders its BODY cell as a real `<th scope="row">` (#779, for
1879
+ a11y), and a bare `th` here matched that one too -- every property in
1880
+ this rule leaked onto it: `height: tableHeader` (40px) stretched the
1881
+ whole body row to 40px instead of 36 (#833, confirmed live on the
1882
+ showcase, every td in the row equalized to the one th); `color:
1883
+ color-mix(...63%...)` muted the primary column's own text below every sibling `<td>`'s full-opacity color (#835, re-measured live against
1884
+ 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
1885
+ 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. */
1886
+ & 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; }
1841
1887
  & td { color: var(--crisp-fg-primary); background: var(--crisp-bg-surface); }
1842
1888
  & th:last-child, & td:last-child { border-inline-end: none; }
1843
1889
  & tbody tr:last-child td { border-bottom: none; }
@@ -1856,7 +1902,7 @@
1856
1902
  }
1857
1903
  .crisp-table--surface_card {
1858
1904
  border: 1px solid var(--crisp-border-subtle); border-radius: var(--crisp-radius-xl); overflow: hidden;
1859
- & th { border-top: none; }
1905
+ & thead th { border-top: none; }
1860
1906
  & tfoot td { border-bottom: none; }
1861
1907
  }
1862
1908
  .crisp-table--surface_bleed {
@@ -1880,7 +1926,9 @@
1880
1926
  & .crisp-tabletoolbar-search svg { flex-shrink: 0; }
1881
1927
  & .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; }
1882
1928
  & .crisp-tabletoolbar-search input::placeholder { color: var(--crisp-fg-muted); }
1883
- & .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; }
1929
+ /* 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
1930
+ (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. */
1931
+ & .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; }
1884
1932
  & .crisp-tabletoolbar-tool:hover { background: var(--crisp-bg-raised); }
1885
1933
  & .crisp-tabletoolbar-tool svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
1886
1934
  /* 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
@@ -1899,8 +1947,17 @@
1899
1947
  an actual sort applied. color-mix (not the raw rgba Attio computes
1900
1948
  to) so both the border and the fill flip correctly in dark mode, the
1901
1949
  same reasoning --ghost above already uses. Only wired into
1902
- TableToolbar itself (tabletoolbar.tsx) this pass -- RecordsTable's own Sort/Filter triggers share this same chrome function but are deprecated, not touched here. */
1903
- & .crisp-tabletoolbar-tool--dashed { position: relative; box-shadow: none; background: color-mix(in srgb, var(--crisp-fg-primary) 2%, transparent); }
1950
+ 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.
1951
+ 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
1952
+ "Sort's colour" in an earlier pass is a different thing entirely: the
1953
+ "Sorted by" PREFIX word inside an ACTIVE sort chip, #788 -- a
1954
+ compound-label treatment that only exists once a sort is applied, not
1955
+ a resting-state colour.) --dashed is currently only ever applied to
1956
+ Sort's and Filter's own empty triggers (never View settings/
1957
+ Import-Export, which stay full-strength always, confirmed live), so
1958
+ the mute belongs on --dashed itself, not gated behind a Filter-only
1959
+ modifier. */
1960
+ & .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); }
1904
1961
  & .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; }
1905
1962
  & .crisp-tabletoolbar-tool--dashed:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
1906
1963
  & .crisp-tabletoolbar-toolgroup { position: relative; display: inline-flex; }
@@ -1940,7 +1997,11 @@
1940
1997
  & .crisp-tabletoolbar-menu-sep { height: 1px; border: none; background: var(--crisp-border-subtle); margin: var(--crisp-space-0-5) 0; }
1941
1998
  /* #592: the Filter field picker's nested-scope header (back control +
1942
1999
  search, once drilled into a nestedColumns column) and a search
1943
- 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
2000
+ result's own breadcrumb row ("Team › Twitter follower count",
2001
+ surfaced by the SAME query matching a nested column). menu-back
2002
+ reuses the established 28px icon-only control pattern this file
2003
+ already uses twice (sort-remove, the kebab segment) rather than
2004
+ an independent measurement of Attio's own back arrow. The breadcrumb
1944
2005
  colours ARE measured live: the parent segment muted at the same
1945
2006
  exact color-mix 63% sort-learn above already uses, the
1946
2007
  separator chevron and leaf segment both full fgPrimary (the
@@ -1984,9 +2045,15 @@
1984
2045
  & .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--val { background: var(--crisp-bg-surface); }
1985
2046
  & .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; }
1986
2047
  & .crisp-tabletoolbar-fcond-seg--field svg { color: var(--crisp-fg-muted); }
1987
- /* #592: a nested pick's field segment ("Team › Twitter follower
1988
- count") — measured live: BOTH path segments the same colour (chip context only; the picker's own menu rows keep the parent muted, see
1989
- 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. */
2048
+ /* #592: a nested pick's field segment ("Team › Twitter follower count") — measured live: BOTH path segments the same colour (chip
2049
+ context only; the picker's own menu rows keep the parent muted, see
2050
+ tabletoolbar.chrome.ts), each independently truncated. Attio's own
2051
+ layout gives the leaf segment priority (it stays fully readable
2052
+ longer, the parent truncates first) via a two-part min-content
2053
+ measurement this reproduces qualitatively with a flex-shrink
2054
+ differential rather than byte-for-byte (the exact px split Attio
2055
+ computes is content-length-dependent, not a fixed value) — not
2056
+ independently re-verified at other path-segment lengths. */
1990
2057
  & .crisp-tabletoolbar-fcond-path { display: inline-flex; align-items: center; gap: 2px; min-width: 0; max-width: 178px; }
1991
2058
  & .crisp-tabletoolbar-fcond-path-seg { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1 1 auto; }
1992
2059
  & .crisp-tabletoolbar-fcond-path-seg:first-child { flex: 0 1 auto; }
@@ -1996,8 +2063,10 @@
1996
2063
  & .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); }
1997
2064
  & .crisp-tabletoolbar-fcond-seg--kebab:hover { color: var(--crisp-fg-primary); }
1998
2065
  /* #754: the sort row's own field trigger, replacing a plain Select
1999
- with the same nested-drill-down popover Filter's field picker already has — reuses Select's own already-measured trigger chrome
2000
- 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;
2066
+ with the same nested-drill-down popover Filter's field picker
2067
+ already has — reuses Select's own already-measured trigger chrome
2068
+ verbatim (its class is scoped under Select's own root class, so it
2069
+ can't be reused as a bare class name outside an actual Select tree;
2001
2070
  these values are copied, not re-measured). */
2002
2071
  & .crisp-tabletoolbar-sort-field-picker { position: relative; display: inline-flex; flex: 1; min-width: 0; }
2003
2072
  & .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; }