@crisp-ui-kit/crisp 0.55.0 → 0.55.2

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.
@@ -253,7 +253,7 @@
253
253
  height: 20px; padding-inline: var(--crisp-space-1); font-size: var(--crisp-text-sm); line-height: 16px; gap: 3px; border-radius: var(--crisp-radius-sm);
254
254
  }
255
255
  .crisp-badge--size_large {
256
- height: var(--crisp-size-chip); padding-inline: 9px; font-size: var(--crisp-text-lg); letter-spacing: -0.1px; border-radius: var(--crisp-radius-chip);
256
+ height: var(--crisp-size-chip); padding-block: var(--crisp-space-px); padding-inline-start: var(--crisp-space-1); padding-inline-end: var(--crisp-space-1-5); font-size: var(--crisp-text-lg); letter-spacing: -0.1px; border-radius: var(--crisp-radius-lg);
257
257
  }
258
258
  .crisp-badge--variant_soft {
259
259
 
@@ -599,8 +599,11 @@
599
599
  }
600
600
 
601
601
  .crisp-datatable {
602
- display: flex; flex-direction: column; gap: var(--crisp-space-2-5); font-family: var(--crisp-font-sans);
603
- & .crisp-datatable-toolbar { display: flex; align-items: center; gap: var(--crisp-space-2); min-height: 49px; border-top: 1px solid var(--crisp-border-subtle); }
602
+ display: flex; flex-direction: column; gap: var(--crisp-space-2-5); font-family: var(--crisp-font-sans); /* The toolbar carried no horizontal inset of its own, so its content (search box, sort/filter chips) sat flush at the table's outer edge
603
+ (x=0) while the header row's own th/td padding (table.recipe.ts, #465) starts every column's text at 12px -- one row down, the SAME
604
+ grid's own header cell already begins at that inset. c.space["3"] here just matches the row directly below it, measured live against Attio (the sort/filter row's search chip lines up with the first
605
+ column header's text, not the grid's raw edge). */
606
+ & .crisp-datatable-toolbar { display: flex; align-items: center; gap: var(--crisp-space-2); min-height: 49px; border-top: 1px solid var(--crisp-border-subtle); padding: 0 var(--crisp-space-3); }
604
607
  & .crisp-datatable-search { position: relative; flex: 1; max-width: 280px; display: inline-flex; align-items: center; }
605
608
  & .crisp-datatable-search svg { position: absolute; inset-inline-start: var(--crisp-space-2); color: var(--crisp-fg-muted); pointer-events: none; }
606
609
  & .crisp-datatable-search input { width: 100%; height: 32px; padding: 0 var(--crisp-space-2) 0 var(--crisp-space-6); border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); outline: none; transition: box-shadow .15s; }
@@ -608,72 +611,83 @@
608
611
  & .crisp-datatable-search input:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
609
612
  & .crisp-datatable-count { margin-inline-start: auto; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
610
613
  /* announced, never drawn */
611
- & .crisp-datatable-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
612
- /* crisp/#645: visually-hidden-but-focusable, not visually-hidden-and- inert like .crisp-datatable-live above -- same clip-rect shape, this one just needs to actually hold DOM focus so a native copy/paste event has something to fire on (see its own comment in datatable.tsx). resize: none keeps a stray native textarea handle from ever being drawable, even though it's clipped to 1px anyway. */
613
- & .crisp-datatable-clipboard-proxy {
614
+ & .crisp-datatable-live {
614
615
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
615
616
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
616
- resize: none;
617
617
  }
618
- /* single active cell (crisp/#645 follow-up -- this had no styling of its
619
- own at all before, a gap against Golden Rule 0: RecordsTable's own measured value, .crisp-recordstable-td[data-selected], brought across unchanged now that a visible anchor actually matters for the Shift+Click range below). */
618
+ /* crisp/#645: visually-hidden-but-focusable, not visually-hidden-and-
619
+ inert like .crisp-datatable-live above -- same clip-rect shape,
620
+ this one just needs to actually hold DOM focus so a native
621
+ copy/paste event has something to fire on (see its own comment
622
+ in datatable.tsx). resize: none keeps a stray native textarea handle
623
+ from ever being drawable, even though it's clipped to 1px anyway. */
624
+ & .crisp-datatable-clipboard-proxy { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; resize: none; }
625
+ /* single active cell (crisp/#645 follow-up -- this had no styling of its own at all before, a gap against Golden Rule 0: RecordsTable's own
626
+ measured value, .crisp-recordstable-td[data-selected], brought
627
+ across unchanged now that a visible anchor actually matters for the
628
+ Shift+Click range below). */
620
629
  & td[data-selected] { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: -2px; border-radius: var(--crisp-radius-sm); z-index: 3; }
621
- /* crisp/#645, G-101: a Shift+Click range -- reuses the row-selection tint formula already used twice below (tbody tr[data-selected] td) for the same reason a selected group of rows gets one: a selected GROUP of cells is the same kind of state, just cell-scoped instead of row-scoped. Not independently re-measured against Attio's own range
622
- highlight (out of scope this slice -- flagged on crisp#645). */
623
- & td[data-in-range] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface));
624
- } /* The header cell has no padding of its own -- the button owns the full cell so the whole header is clickable (its own inset is 12px). Mirrors the summary-row fix. `& .crisp-table th`, not bare `& th` (found while re-checking #691's
630
+ /* crisp/#645, G-101: a Shift+Click range -- reuses the row-selection
631
+ tint formula already used twice below (tbody tr[data-selected] td)
632
+ for the same reason a selected group of rows gets one: a selected
633
+ GROUP of cells is the same kind of state, just cell-scoped instead of
634
+ row-scoped. Not independently re-measured against Attio's own range highlight (out of scope this slice -- flagged on crisp#645). */
635
+ & td[data-in-range] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
636
+ /* The header cell has no padding of its own -- the button owns the full cell so the whole header is clickable (its own inset is 12px). Mirrors the summary-row fix. `& .crisp-table th`, not bare `& th` (found while re-checking #691's
625
637
  own fix live): DataTable's root nests `Table`'s own wrapper, whose
626
- `table.recipe.ts` carries `& th, & td { padding: 0 12px }
627
- ` at the SAME specificity (.crisp-table th vs .crisp-datatable th, both one class + one element) -- a bare `& th` here only won by source order, which flipped the moment table.recipe.ts's own CSS file
628
- happened to land after datatable's in the build (alphabetical: datatable < table), silently reinstating 12px on every header cell in the whole product and making every interactive column's button
629
- double it to 24px. Scoping through `.crisp-table` adds a second
630
- class to the selector (0,2,1 beats 0,1,1) so this can never lose a
631
- specificity tie to source order again. */
638
+ `table.recipe.ts` carries `& th, & td { padding: 0 12px }` at the
639
+ SAME specificity (.crisp-table th vs .crisp-datatable th, both one
640
+ class + one element) -- a bare `& th` here only won by source
641
+ order, which flipped the moment table.recipe.ts's own CSS file happened to land after datatable's in the build (alphabetical:
642
+ 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. */
632
644
  & .crisp-table th { padding: 0; }
633
- /* 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
634
- -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
635
- (correct everywhere else) is untouched. */
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. */
636
650
  & th .crisp-menu { width: 100%; height: 100%; }
637
651
  & .crisp-datatable-th-inner { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; min-width: 0; height: 100%; }
638
652
  & .crisp-datatable-th-inner > svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
639
- /* crisp/#605 follow-up: a header with neither a Menu nor a sort button
640
- (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
641
- 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
642
- padding+icon+gap combinations -- Attio does not leave the inert
643
- primary header flush left either. The `>` combinator targets only
644
- 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. */
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. */
645
661
  & th > .crisp-datatable-th-inner { box-sizing: border-box; padding: 0 var(--crisp-space-3); }
646
- /* The summary cell has NO padding of its own -- the calculation trigger
647
- owns the whole 246x37 area so a click anywhere in the cell opens the
648
- menu. Zero the cell's inset and let ColumnCalc supply the 12px. */
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. */
649
663
  & tfoot td { padding: 0; }
650
- /* Group heading row -- a view is grouped under a subtle label with the bucket's count beside it. */
651
- & .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; }
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; }
652
667
  & .crisp-datatable-viewsettings:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
653
668
  & .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); }
654
- & .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
+ & .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); }
655
670
  & .crisp-datatable-group-count { margin-inline-start: var(--crisp-space-1-5); color: var(--crisp-fg-muted); font-variant-numeric: tabular-nums; }
656
671
  & .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; }
657
672
  & .crisp-datatable-th-rename:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
658
673
  & .crisp-datatable-th-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
659
- /* #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. */
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. */
660
677
  & .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; }
661
- /* A right-aligned column's header keeps its field-icon + label packed at
662
- the START, matching its own values only in that the VALUES right-align
663
- -- Attio's own right-aligned header (measured live: "Twitter follower
664
- 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
665
- (#406, reversing #393's own justify-content: flex-end here). The chevron's own margin-inline-start: auto below already pins IT to the
666
- end of a flex-start row with no override needed -- that's what pins the trailing icon on Attio too. */
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. */
667
683
  & .crisp-datatable-chevron { margin-inline-start: auto; flex-shrink: 0; color: var(--crisp-fg-muted); opacity: 0; transition: opacity .12s; }
668
684
  & .crisp-datatable-sort:hover .crisp-datatable-chevron { opacity: 0.6; }
669
685
  & .crisp-datatable-sort[data-sorted] .crisp-datatable-chevron { opacity: 1; color: var(--crisp-fg-primary); }
670
686
  /* sticky first column(s) + horizontal scroll shadow — RecordsTable's feel */
671
687
  & th[data-sticky], & td[data-sticky] { position: sticky; z-index: 1; background: var(--crisp-bg-surface); }
672
- & th[data-sticky] { z-index: 3;
673
- } /* #554: a footer sticky-column cell sits in the bottom-sticky row (table.recipe.ts's
674
- `tfoot td { z-index: 2 }
675
- `), which `td[data-sticky]`'s own higher-specificity
676
- `z-index: 1` would otherwise undercut -- keep it level with the row it's in. */
688
+ & th[data-sticky] { z-index: 3; }
689
+ /* #554: a footer sticky-column cell sits in the bottom-sticky row (table.recipe.ts's
690
+ `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. */
677
691
  & tfoot td[data-sticky] { z-index: 2; }
678
692
  & tbody tr[data-selected] td[data-sticky] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
679
693
  & tbody tr:hover td[data-sticky] { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); }
@@ -690,88 +704,98 @@
690
704
  & tbody tr[data-selected] td { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
691
705
  & 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))); }
692
706
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
693
- /* 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
694
- plain background. */
707
+ /* crisp/#758: AI-enriched column marker, ported from RecordsTable's own
708
+ already-measured value -- a full-bleed #9b69ff (bgPurple) wash at 4%,
709
+ composited OVER whatever row-state background the cell already has
710
+ (hover, selected), which is why it's an ::after overlay and not a plain background. */
695
711
  & td[data-enriched]::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: color-mix(in srgb, var(--crisp-bg-purple) 4%, transparent); }
696
712
  & .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; }
697
713
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
698
- /* 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. */
714
+ /* An async onEdit's in-flight state (#385) -- the field/picker dims and
715
+ stops accepting input rather than disappearing, so the just-typed or
716
+ just-chosen value stays visible while its commit is pending. Same
717
+ treatment Input already gives its own disabled state. */
699
718
  & .crisp-datatable-edit:disabled { opacity: 0.5; cursor: not-allowed; }
700
719
  & .crisp-datatable-edit-pending { flex-shrink: 0; margin-inline-start: var(--crisp-space-1); color: var(--crisp-fg-muted); vertical-align: middle; }
701
- /* 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
702
- 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
703
- guarding against. */
720
+ /* The rejection's message, beside the cell's own content -- a sibling in
721
+ 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
722
+ it. Block-level: the row grows to fit it rather than floating over
723
+ the row below, so it can never end up clipped by the scroll
724
+ container's overflow the way a position:fixed escape would need guarding against. */
704
725
  & .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); }
705
- /* editor: "select"/"switch" -- always live, no separate edit mode, so
706
- the cell IS the picker: same chromeless-fill treatment as
707
- RecordsTable's own --picker cells (measured there already; DataTable shares RecordsTable's grid, Golden Rule 0). */
726
+ /* 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
727
+ shares RecordsTable's grid, Golden Rule 0). */
708
728
  & .crisp-datatable-td--picker { padding: 0; }
709
729
  & .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); }
710
730
  & .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; }
711
731
  & .crisp-datatable-td--picker .crisp-datatable-edit-error { padding-inline-start: var(--crisp-space-2); }
712
- /* reference picker: a leading 16px avatar per option, plus a trailing
713
- "Create X" row when the consumer supplies one (#449) -- same measured
714
- values as RecordsTable's own .crisp-recordstable-refoption*. */
732
+ /* 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*. */
715
733
  & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
716
734
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
717
735
  & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
718
736
  & .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); }
719
- /* #579: click-to-expand overlay for a truncated cell -- a read-only, full-value card, settled live as click (not Attio's usual
720
- 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
721
- 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
722
- visible viewport, same as the edit-error comment above already
723
- notes for any position:absolute escape -- unmeasured against Attio
724
- for this exact case, flagged rather than guessed. */
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. */
725
748
  & td[data-cell-expanded] { overflow: visible; z-index: 7; }
726
- & .crisp-datatable-cell-overlay {
727
- position: absolute; inset-inline-start: 0; top: 0; width: 100%; box-sizing: border-box;
728
- padding: var(--crisp-space-2) var(--crisp-space-3); background: var(--crisp-bg-surface);
729
- border: 1px solid var(--crisp-bg-brand-solid); border-radius: 4px;
730
- font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium);
731
- color: var(--crisp-fg-primary); white-space: normal; word-break: break-word; cursor: default;
732
- }
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; }
733
750
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
734
751
  & table { table-layout: fixed; }
735
- /* read-only/computed column marker (#596) -- sits just inside the
736
- resize handle, same absolute-positioned-against-the-th approach the
737
- handle itself uses, since th has zero padding and the button/inner
738
- row fills the whole cell with no room left for a trailing sibling. */
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. */
739
753
  & .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; }
740
754
  /* column resize grip */
741
755
  & .crisp-datatable-resize { position: absolute; top: 0; inset-inline-end: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
742
756
  & .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; }
743
757
  & .crisp-datatable-resize:hover::after, & .crisp-datatable-resize[data-resizing]::after { opacity: 1; }
744
- /* crisp/#731 -- column drag-to-reorder. Measured live against Attio's own grid (the issue's own comment): the dragged header keeps full
745
- opacity and the plain `pointer` cursor throughout (Attio draws no
746
- ghost and no "grabbing" cursor of its own), so this adds nothing to
747
- `[data-dragging]` itself -- the ONLY new paint is the drop-indicator
748
- 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. */
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. */
749
762
  & th[data-dragging] { cursor: pointer; }
750
- & 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; }
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
+ }
751
767
  & th[data-drop-before]::before { inset-inline-start: -1.5px; }
752
768
  & th[data-drop-after]::after { inset-inline-end: -1.5px; }
753
769
  /* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
754
770
  & .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); }
755
- /* 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
756
- content (a custom render, or a non-entity built-in type), not just
757
- type: "entity"'s own inline rendering. */
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. */
758
775
  & .crisp-datatable-cell-actions-row { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; width: 100%; }
759
- /* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text, plain hover vs rest) that Attio's underline is unconditional, not a
760
- hover reveal -- identical at rest and on hover: text-decoration-color
761
- 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. */
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. */
762
780
  & .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); }
763
781
  & 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%; }
764
- /* 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
765
- hover", and a live check confirmed hovering well past the name --
766
- 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
767
- expressed as color-mix against fgPrimary so they stay correct in
768
- dark mode instead of a literal black that would invert. */
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. */
769
788
  & .crisp-datatable-entity-actions { display: flex; align-items: center; gap: var(--crisp-space-1-5); margin-inline-start: auto; flex-shrink: 0; opacity: 0; }
770
789
  & tr:hover .crisp-datatable-entity-actions, & .crisp-datatable-entity-actions:focus-within { opacity: 1; }
771
790
  & .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; }
772
791
  & .crisp-datatable-entity-action:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
773
792
  & .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); }
774
- & .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); }
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); }
775
799
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
776
800
  & .crisp-datatable-tags { display: flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
777
801
  & .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); }
@@ -784,24 +808,70 @@
784
808
  & .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)); }
785
809
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
786
810
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
787
- /* 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. */
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. */
813
+ & .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. */
788
818
  & .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
789
819
  & .crisp-datatable-link:hover { text-decoration: underline; }
790
- /* 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
791
- (#1f5fd6); kept as its own literal rather than forced onto that token,
792
- since the two were measured stable and different. The read-only svg
793
- 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. */
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. */
823
+ & .crisp-datatable-phone { display: inline-flex; align-items: center; gap: var(--crisp-space-1); }
824
+ & .crisp-datatable-phone-dial { color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
825
+ & .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
+ & .crisp-datatable-phone-editor input { flex: 1; min-width: 0; }
827
+ /* multi-value phone (#816) -- confirmed live (a real custom "Multiple
828
+ values" Phone Number attribute, and the built-in People "Phone
829
+ numbers" field): the collapsed cell is a 4px-gap flex row of real
830
+ tel: links in a measured link blue (~rgb(59,108,232) pixel-sampled;
831
+ reused as the closest already-tokened blue, brand600, rather than
832
+ adding a third blue token for a delta this small), underlined,
833
+ UNLIKE the single-value cell above (plain text, no link, no
834
+ underline) -- multi-value is the one shape Attio makes into a real
835
+ link. The cell hard-clips on overflow (no ellipsis -- confirmed
836
+ live, a second value can end mid-digit with no "..."). */
837
+ & .crisp-datatable-phone-multi { display: inline-flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
838
+ & .crisp-datatable-phone-link { color: var(--crisp-bg-brand-solid); text-decoration: underline; white-space: nowrap; }
839
+ /* multi-value editor (#816) -- confirmed live: a stack of committed-
840
+ value rows (32px = size.menuRow, 5px 8px padding, 8px gap between
841
+ the grip/value/actions, 16px icons) above one ALWAYS-blank trailing
842
+ DataPhoneEditor-shaped row -- that blank row is the only "add
843
+ another" affordance, there's no separate button for it. The copy/delete pair reuses .crisp-datatable-entity-action's own
844
+ hover-reveal chrome (not independently re-measured as its own hit
845
+ target this pass, only the 16px icon glyphs were). */
846
+ & .crisp-datatable-phone-multi-editor { display: flex; flex-direction: column; }
847
+ & .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); }
848
+ & .crisp-datatable-phone-multi-row .crisp-datatable-phone-link { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
849
+ & .crisp-datatable-phone-multi-grip { color: var(--crisp-fg-muted); flex-shrink: 0; cursor: grab; }
850
+ & .crisp-datatable-phone-multi-actions { display: flex; align-items: center; gap: var(--crisp-space-1); margin-inline-start: auto; flex-shrink: 0; opacity: 0; }
851
+ & .crisp-datatable-phone-multi-row:hover .crisp-datatable-phone-multi-actions,
852
+ & .crisp-datatable-phone-multi-actions:focus-within { opacity: 1; }
853
+ & .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; }
854
+ & .crisp-datatable-phone-multi-action:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
855
+ /* datetime cell editor (#762) -- DatePickerField + TimePicker side by side. */
856
+ & .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); }
857
+ /* rating cell type/editor (#760) -- measured live: 16px stars on an
858
+ 18px pitch (a 2px gap), an empty star at rgba(0,0,0,0.1), a
859
+ committed/filled star at brand600 (--crisp-bg-brand-solid, an exact
860
+ match to the measured #266df0), and a distinct hover-preview colour
861
+ -- 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
862
+ crisp-datatable-rating-star class on the svg itself (not the
863
+ button), so one set of fill rules covers both -- the button only
864
+ adds its own reset + cursor via a separate -btn class. */
794
865
  & .crisp-datatable-rating { display: inline-flex; align-items: center; gap: 2px; }
795
866
  & .crisp-datatable-rating-star { display: block; }
796
867
  & .crisp-datatable-rating-star path { fill: rgba(0, 0, 0, 0.1); }
797
868
  & .crisp-datatable-rating-star[data-filled] path { fill: var(--crisp-bg-brand-solid); }
798
869
  & .crisp-datatable-rating-star[data-hovering] path { fill: #215bc4; }
799
870
  & .crisp-datatable-rating-star-btn { display: inline-flex; padding: 0; margin: 0; border: none; background: none; font-family: inherit; cursor: pointer; }
800
- /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
801
- hairline doubling as the drop indicator (it tints toward the brand
802
- colour as the active drop target). The affordance + geometry are
803
- 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
804
- pointer-events implementation. */
871
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
872
+ 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
873
+ draggable + dataTransfer, the same one column reorder already uses
874
+ (RecordsTable's reorderColumn) -- not a proprietary pointer-events implementation. */
805
875
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
806
876
  & .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; }
807
877
  & .crisp-datatable-grip:active { cursor: grabbing; }
@@ -72,7 +72,9 @@ declare const spec: {
72
72
  };
73
73
  readonly large: {
74
74
  readonly height: `var(--${string})`;
75
- readonly paddingInline: "9px";
75
+ readonly paddingBlock: `var(--${string})`;
76
+ readonly paddingInlineStart: `var(--${string})`;
77
+ readonly paddingInlineEnd: `var(--${string})`;
76
78
  readonly fontSize: `var(--${string})`;
77
79
  readonly letterSpacing: "-0.1px";
78
80
  readonly borderRadius: `var(--${string})`;
@@ -0,0 +1,12 @@
1
+ export type Country = {
2
+ /** ISO 3166-1 alpha-2, e.g. "MN". */
3
+ iso2: string;
4
+ /** English short name. */
5
+ name: string;
6
+ /** E.164 calling code, without the leading "+". */
7
+ dialCode: string;
8
+ };
9
+ export declare const COUNTRIES: Country[];
10
+ /** Two Unicode Regional Indicator Symbols spell the flag emoji for `iso2`. */
11
+ export declare function flagEmoji(iso2: string): string;
12
+ export declare function countryByIso2(iso2: string): Country | undefined;
@@ -42,7 +42,18 @@ export type DataCellType = "text" | "entity" | "tags" | "status" | "date" | "dat
42
42
  * confirmed live: 5 filled/empty 5-point stars, `maxRating` stars total
43
43
  * (default 5). Read-only display without `editor: "rating"`; see that
44
44
  * prop for the always-live editable version. */
45
- | "rating";
45
+ | "rating"
46
+ /** Attio's own "Phone Number" attribute type (crisp/#765), confirmed
47
+ * live: a country + national number, e.g. "🇲🇳 +976 88112233". The raw
48
+ * value is a plain string, `"{iso2}|{digits}"` (e.g. `"MN|88112233"`) --
49
+ * a pipe-separated pair rather than a bare E.164 string, so the stored
50
+ * country selection survives even for a national number that's still
51
+ * being typed (an E.164 string alone can't distinguish "no country
52
+ * chosen yet" from "the digits happen to start with a valid dial
53
+ * code"). See `editor: "phone"` for the editable picker. See `multiValue`
54
+ * (crisp#816) for Attio's own "Multiple values" constraint on this
55
+ * type. */
56
+ | "phone";
46
57
  export type DataTableColumn<T> = {
47
58
  key: string;
48
59
  /** Truncates with an ellipsis when it's wider than the column (#534: a
@@ -76,6 +87,26 @@ export type DataTableColumn<T> = {
76
87
  resizable?: boolean;
77
88
  /** Tone overrides for `status`/`tags` values (else a stable colour per value). */
78
89
  tones?: Record<string, string>;
90
+ /** `type: "date"`/`"datetime"` only: render this cell in Attio's own
91
+ * measured overdue/danger red (`fgDanger`, `#eb3b3b` — the same token
92
+ * `TasksTable`'s own overdue due-date already uses, crisp#462) when the
93
+ * predicate returns true. A per-row PREDICATE rather than `tones`'s
94
+ * value-keyed lookup, because a date's danger state is computed over the
95
+ * WHOLE ROW (e.g. a precomputed "overdue" bucket) — `tones` can only key
96
+ * off the formatted string, which a date doesn't have a closed set of
97
+ * (crisp#822). */
98
+ danger?: (value: unknown, row: T) => boolean;
99
+ /** This column holds more than one value per row -- Attio's own
100
+ * "Multiple values" constraint (crisp#816, confirmed live on the Phone
101
+ * Number attribute type: Constraints → Multiple values, a per-attribute
102
+ * toggle, absent for e.g. Text). Built out for `type`/`editor: "phone"`
103
+ * only this pass; the raw value is that type's normal per-value string,
104
+ * several of them joined with `;` (`"MN|88112233;US|4155550123"`) rather
105
+ * than JSON -- the same "a real separator earns its keep" reasoning
106
+ * `phone`'s own single-value `"{iso2}|{digits}"` shape already uses.
107
+ * Structural flag lives on the column (type-agnostic, per the issue)
108
+ * even though only one type renders it today. */
109
+ multiValue?: boolean;
79
110
  /** Enable click-to-sort on this column's header. */
80
111
  sortable?: boolean;
81
112
  /** Exclude this column from the search filter (default: included). */
@@ -121,8 +152,21 @@ export type DataTableColumn<T> = {
121
152
  * field. Always-live like `"switch"`/`"rating"`; renders crisp's own
122
153
  * `Checkbox` component (already measured 16px/r6/brand-fill from the
123
154
  * grid's own row-selection checkbox — this is the SAME visual, not a
124
- * new one). */
125
- editor?: "select" | "switch" | "reference" | "actor" | "rating" | "checkbox";
155
+ * new one).
156
+ *
157
+ * `"phone"` (crisp/#765) opens like `select`/`reference` -- double-click
158
+ * (or Enter on the selected cell), not always-live -- a country picker
159
+ * (crisp's own searchable `Select`, listing `COUNTRIES` from
160
+ * `./countries`) plus a static "+{dialCode}" prefix plus a plain digits
161
+ * field.
162
+ *
163
+ * `"datetime"` (crisp/#762) is an opt-in replacement for `type:
164
+ * "datetime"`'s own DEFAULT editor (a native `datetime-local` input,
165
+ * unchanged unless this is set): confirmed live, Attio's own Timestamp
166
+ * editor is a calendar + time + preset picker, not a native input.
167
+ * Composes crisp's own `DatePickerField` (calendar + presets) with
168
+ * `TimePicker` (hour/minute). */
169
+ editor?: "select" | "switch" | "reference" | "actor" | "rating" | "checkbox" | "phone" | "datetime";
126
170
  /** Options for `editor: "select"` (value/label, an optional `color` a
127
171
  * caller's own `render` can read) — same shape as RecordsTable's
128
172
  * `Column.options` for its `status` picker. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.55.0",
3
+ "version": "0.55.2",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",