@crisp-ui-kit/crisp 0.54.7 → 0.54.9

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.
Files changed (130) hide show
  1. package/dist/authshell.css +5 -1
  2. package/dist/authshell.layered.css +5 -1
  3. package/dist/badge.css +5 -5
  4. package/dist/badge.layered.css +5 -5
  5. package/dist/board.css +36 -8
  6. package/dist/board.layered.css +36 -8
  7. package/dist/button.css +3 -1
  8. package/dist/button.layered.css +3 -1
  9. package/dist/cjs/components/badge/badge.recipe.js +28 -5
  10. package/dist/cjs/components/button/button.js +33 -2
  11. package/dist/cjs/components/button/button.recipe.js +31 -5
  12. package/dist/cjs/components/contextmenu/contextmenu.js +2 -2
  13. package/dist/cjs/components/contextmenu/contextmenu.recipe.js +24 -0
  14. package/dist/cjs/components/datatable/datatable.js +590 -41
  15. package/dist/cjs/components/datatable/datatable.recipe.js +118 -5
  16. package/dist/cjs/components/dialog/dialog.js +72 -5
  17. package/dist/cjs/components/dialog/dialog.recipe.js +15 -1
  18. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +49 -5
  19. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +365 -49
  20. package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +78 -0
  21. package/dist/cjs/components/tabs/tabs.recipe.js +11 -2
  22. package/dist/cjs/components/tooltip/tooltip.js +23 -9
  23. package/dist/cjs/components/tooltip/tooltip.recipe.js +10 -9
  24. package/dist/cjs/product/authshell/authshell.js +14 -1
  25. package/dist/cjs/product/authshell/authshell.recipe.js +13 -1
  26. package/dist/cjs/product/board/board.js +175 -46
  27. package/dist/cjs/product/board/board.recipe.js +56 -4
  28. package/dist/cjs/product/comments/comments.js +34 -12
  29. package/dist/cjs/product/comments/comments.recipe.js +31 -7
  30. package/dist/cjs/product/formdialog/formdialog.js +10 -4
  31. package/dist/cjs/product/navitem/navitem.js +7 -2
  32. package/dist/cjs/product/navitem/navitem.recipe.js +16 -0
  33. package/dist/cjs/product/objectsettings/index.js +1 -2
  34. package/dist/cjs/product/objectsettings/objectsettings.js +3 -13
  35. package/dist/cjs/product/recordstable/recordstable.js +1 -17
  36. package/dist/cjs/product/richtext/richtext.js +76 -1
  37. package/dist/cjs/product/savedviews/savedviews.js +55 -24
  38. package/dist/cjs/product/savedviews/savedviews.recipe.js +27 -0
  39. package/dist/cjs/product/sidebar/sidebar.js +68 -15
  40. package/dist/cjs/product/sidebar/sidebar.recipe.js +6 -1
  41. package/dist/cjs/tokens/palette.js +32 -1
  42. package/dist/cjs/tokens/semantic.js +11 -0
  43. package/dist/comments.css +50 -14
  44. package/dist/comments.layered.css +50 -14
  45. package/dist/contextmenu.css +24 -0
  46. package/dist/contextmenu.layered.css +24 -0
  47. package/dist/datatable.css +101 -37
  48. package/dist/datatable.layered.css +101 -37
  49. package/dist/dialog.css +13 -1
  50. package/dist/dialog.layered.css +13 -1
  51. package/dist/esm/components/badge/badge.recipe.js +28 -5
  52. package/dist/esm/components/button/button.js +34 -3
  53. package/dist/esm/components/button/button.recipe.js +31 -5
  54. package/dist/esm/components/contextmenu/contextmenu.js +2 -2
  55. package/dist/esm/components/contextmenu/contextmenu.recipe.js +24 -0
  56. package/dist/esm/components/datatable/datatable.js +590 -42
  57. package/dist/esm/components/datatable/datatable.recipe.js +119 -6
  58. package/dist/esm/components/dialog/dialog.js +72 -5
  59. package/dist/esm/components/dialog/dialog.recipe.js +15 -1
  60. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +49 -5
  61. package/dist/esm/components/tabletoolbar/tabletoolbar.js +366 -50
  62. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +80 -2
  63. package/dist/esm/components/tabs/tabs.recipe.js +11 -2
  64. package/dist/esm/components/tooltip/tooltip.js +23 -9
  65. package/dist/esm/components/tooltip/tooltip.recipe.js +10 -9
  66. package/dist/esm/product/authshell/authshell.js +15 -2
  67. package/dist/esm/product/authshell/authshell.recipe.js +13 -1
  68. package/dist/esm/product/authshell/index.js +1 -1
  69. package/dist/esm/product/board/board.js +176 -47
  70. package/dist/esm/product/board/board.recipe.js +56 -4
  71. package/dist/esm/product/comments/comments.js +35 -13
  72. package/dist/esm/product/comments/comments.recipe.js +31 -7
  73. package/dist/esm/product/formdialog/formdialog.js +10 -4
  74. package/dist/esm/product/navitem/index.js +1 -1
  75. package/dist/esm/product/navitem/navitem.js +7 -2
  76. package/dist/esm/product/navitem/navitem.recipe.js +16 -0
  77. package/dist/esm/product/objectsettings/index.js +1 -1
  78. package/dist/esm/product/objectsettings/objectsettings.js +2 -12
  79. package/dist/esm/product/recordstable/recordstable.js +2 -18
  80. package/dist/esm/product/richtext/richtext.js +76 -1
  81. package/dist/esm/product/savedviews/savedviews.js +57 -26
  82. package/dist/esm/product/savedviews/savedviews.recipe.js +27 -0
  83. package/dist/esm/product/sidebar/sidebar.js +69 -16
  84. package/dist/esm/product/sidebar/sidebar.recipe.js +6 -1
  85. package/dist/esm/tokens/palette.js +32 -1
  86. package/dist/esm/tokens/semantic.js +11 -0
  87. package/dist/native/tokens.d.ts +1 -1
  88. package/dist/native/tokens.js +16 -2
  89. package/dist/navitem.css +7 -0
  90. package/dist/navitem.layered.css +7 -0
  91. package/dist/recordstable.css +34 -32
  92. package/dist/recordstable.layered.css +34 -32
  93. package/dist/savedviews.css +27 -0
  94. package/dist/savedviews.layered.css +27 -0
  95. package/dist/sidebar.css +6 -1
  96. package/dist/sidebar.layered.css +6 -1
  97. package/dist/styles.css +369 -106
  98. package/dist/styles.layered.css +369 -106
  99. package/dist/tabletoolbar.css +47 -3
  100. package/dist/tabletoolbar.layered.css +47 -3
  101. package/dist/tabs.css +1 -1
  102. package/dist/tabs.layered.css +1 -1
  103. package/dist/tokens.css +9 -1
  104. package/dist/tokens.json +16 -2
  105. package/dist/tooltip.css +1 -1
  106. package/dist/tooltip.layered.css +1 -1
  107. package/dist/types/components/badge/badge.recipe.d.ts +5 -1
  108. package/dist/types/components/button/button.recipe.d.ts +18 -5
  109. package/dist/types/components/contextmenu/contextmenu.d.ts +8 -1
  110. package/dist/types/components/datatable/datatable.d.ts +98 -1
  111. package/dist/types/components/dialog/dialog.d.ts +25 -2
  112. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +113 -3
  113. package/dist/types/components/tooltip/tooltip.d.ts +12 -4
  114. package/dist/types/components/tooltip/tooltip.recipe.d.ts +1 -1
  115. package/dist/types/product/authshell/authshell.d.ts +26 -2
  116. package/dist/types/product/authshell/index.d.ts +1 -1
  117. package/dist/types/product/board/board.d.ts +53 -6
  118. package/dist/types/product/comments/comments.d.ts +16 -3
  119. package/dist/types/product/formdialog/formdialog.d.ts +2 -1
  120. package/dist/types/product/navitem/index.d.ts +1 -1
  121. package/dist/types/product/navitem/navitem.d.ts +23 -0
  122. package/dist/types/product/objectsettings/index.d.ts +1 -1
  123. package/dist/types/product/objectsettings/objectsettings.d.ts +0 -10
  124. package/dist/types/product/recordstable/recordstable.d.ts +0 -1
  125. package/dist/types/product/savedviews/savedviews.d.ts +45 -8
  126. package/dist/types/product/sidebar/sidebar.d.ts +33 -5
  127. package/dist/types/tokens/contract.d.ts +4 -0
  128. package/dist/types/tokens/palette.d.ts +13 -1
  129. package/dist/types/tokens/semantic.d.ts +11 -0
  130. package/package.json +1 -1
@@ -10,45 +10,71 @@
10
10
  & .crisp-datatable-count { margin-inline-start: auto; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
11
11
  /* announced, never drawn */
12
12
  & .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; }
13
- /* 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. */
14
- & th { padding: 0; }
15
- /* Same wrapper trap as AUTHORING.md Lesson 40 (ColumnCalc's summary trigger):
16
- Menu wraps its trigger in a flex div that sizes to its content, so a
17
- long header grew past the column instead of filling it -- with nothing
18
- 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
19
- it with no "...". Fill the wrapper, scoped to th so Menu's default (correct everywhere else) is untouched. */
13
+ /* 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. */
14
+ & .crisp-datatable-clipboard-proxy {
15
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
16
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
17
+ resize: none;
18
+ }
19
+ /* single active cell (crisp/#645 follow-up -- this had no styling of its
20
+ 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). */
21
+ & td[data-selected] { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: -2px; border-radius: var(--crisp-radius-sm); z-index: 3; }
22
+ /* 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
23
+ highlight (out of scope this slice -- flagged on crisp#645). */
24
+ & td[data-in-range] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface));
25
+ } /* 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
26
+ own fix live): DataTable's root nests `Table`'s own wrapper, whose
27
+ `table.recipe.ts` carries `& th, & td { padding: 0 12px }
28
+ ` 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
29
+ 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
30
+ double it to 24px. Scoping through `.crisp-table` adds a second
31
+ class to the selector (0,2,1 beats 0,1,1) so this can never lose a
32
+ specificity tie to source order again. */
33
+ & .crisp-table th { padding: 0; }
34
+ /* 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
35
+ -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
36
+ (correct everywhere else) is untouched. */
20
37
  & th .crisp-menu { width: 100%; height: 100%; }
21
38
  & .crisp-datatable-th-inner { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; min-width: 0; height: 100%; }
22
39
  & .crisp-datatable-th-inner > svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
23
- /* 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. */
40
+ /* crisp/#605 follow-up: a header with neither a Menu nor a sort button
41
+ (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
42
+ 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
43
+ padding+icon+gap combinations -- Attio does not leave the inert
44
+ primary header flush left either. The `>` combinator targets only
45
+ 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. */
46
+ & th > .crisp-datatable-th-inner { box-sizing: border-box; padding: 0 var(--crisp-space-3); }
47
+ /* The summary cell has NO padding of its own -- the calculation trigger
48
+ owns the whole 246x37 area so a click anywhere in the cell opens the
49
+ menu. Zero the cell's inset and let ColumnCalc supply the 12px. */
24
50
  & tfoot td { padding: 0; }
25
- /* Group heading row -- a view is grouped under a subtle label with the
26
- bucket's count beside it. */
27
- & .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; }
51
+ /* Group heading row -- a view is grouped under a subtle label with the bucket's count beside it. */
52
+ & .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; }
28
53
  & .crisp-datatable-viewsettings:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
29
54
  & .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); }
30
- & .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); }
55
+ & .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); }
31
56
  & .crisp-datatable-group-count { margin-inline-start: var(--crisp-space-1-5); color: var(--crisp-fg-muted); font-variant-numeric: tabular-nums; }
32
57
  & .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; }
33
58
  & .crisp-datatable-th-rename:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
34
59
  & .crisp-datatable-th-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
35
- /* #465: 12px per the comment above (and Attio's measured header cell
36
- padding: 0 12px) -- was still c.space["2"] (8px), drifted from its
37
- own documented value. */
60
+ /* #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. */
38
61
  & .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; }
39
- /* 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
40
- count") keeps the icon+label left and only the trailing column-menu
41
- icon sits pinned to the right edge, independent of the label's width (#406, reversing #393's own justify-content: flex-end here). The
42
- 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
43
- the trailing icon on Attio too. */
62
+ /* A right-aligned column's header keeps its field-icon + label packed at
63
+ the START, matching its own values only in that the VALUES right-align
64
+ -- Attio's own right-aligned header (measured live: "Twitter follower
65
+ 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
66
+ (#406, reversing #393's own justify-content: flex-end here). The chevron's own margin-inline-start: auto below already pins IT to the
67
+ end of a flex-start row with no override needed -- that's what pins the trailing icon on Attio too. */
44
68
  & .crisp-datatable-chevron { margin-inline-start: auto; flex-shrink: 0; color: var(--crisp-fg-muted); opacity: 0; transition: opacity .12s; }
45
69
  & .crisp-datatable-sort:hover .crisp-datatable-chevron { opacity: 0.6; }
46
70
  & .crisp-datatable-sort[data-sorted] .crisp-datatable-chevron { opacity: 1; color: var(--crisp-fg-primary); }
47
71
  /* sticky first column(s) + horizontal scroll shadow — RecordsTable's feel */
48
72
  & th[data-sticky], & td[data-sticky] { position: sticky; z-index: 1; background: var(--crisp-bg-surface); }
49
- & th[data-sticky] { z-index: 3; }
50
- /* #554: a footer sticky-column cell sits in the bottom-sticky row (table.recipe.ts's
51
- `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. */
73
+ & th[data-sticky] { z-index: 3;
74
+ } /* #554: a footer sticky-column cell sits in the bottom-sticky row (table.recipe.ts's
75
+ `tfoot td { z-index: 2 }
76
+ `), which `td[data-sticky]`'s own higher-specificity
77
+ `z-index: 1` would otherwise undercut -- keep it level with the row it's in. */
52
78
  & tfoot td[data-sticky] { z-index: 2; }
53
79
  & tbody tr[data-selected] td[data-sticky] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
54
80
  & tbody tr:hover td[data-sticky] { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); }
@@ -67,40 +93,77 @@
67
93
  & .crisp-datatable-editable:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); cursor: text; }
68
94
  & .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; }
69
95
  & .crisp-datatable-edit:focus { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
70
- /* An async onEdit's in-flight state (#385) -- the field/picker dims and
71
- stops accepting input rather than disappearing, so the just-typed or
72
- just-chosen value stays visible while its commit is pending. Same
73
- treatment Input already gives its own disabled state. */
96
+ /* 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. */
74
97
  & .crisp-datatable-edit:disabled { opacity: 0.5; cursor: not-allowed; }
75
98
  & .crisp-datatable-edit-pending { flex-shrink: 0; margin-inline-start: var(--crisp-space-1); color: var(--crisp-fg-muted); vertical-align: middle; }
76
- /* The rejection's message, beside the cell's own content -- a sibling in
77
- 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
78
- it. Block-level: the row grows to fit it rather than floating over
79
- the row below, so it can never end up clipped by the scroll
80
- container's overflow the way a position:fixed escape would need guarding against. */
99
+ /* 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
100
+ 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
101
+ guarding against. */
81
102
  & .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); }
82
- /* 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
83
- shares RecordsTable's grid, Golden Rule 0). */
103
+ /* editor: "select"/"switch" -- always live, no separate edit mode, so
104
+ the cell IS the picker: same chromeless-fill treatment as
105
+ RecordsTable's own --picker cells (measured there already; DataTable shares RecordsTable's grid, Golden Rule 0). */
84
106
  & .crisp-datatable-td--picker { padding: 0; }
85
107
  & .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); }
86
108
  & .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; }
87
109
  & .crisp-datatable-td--picker .crisp-datatable-edit-error { padding-inline-start: var(--crisp-space-2); }
88
- /* 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*. */
110
+ /* reference picker: a leading 16px avatar per option, plus a trailing
111
+ "Create X" row when the consumer supplies one (#449) -- same measured
112
+ values as RecordsTable's own .crisp-recordstable-refoption*. */
89
113
  & .crisp-datatable-refoption { display: flex; align-items: center; gap: var(--crisp-space-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
90
114
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
91
115
  & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
92
116
  & .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); }
117
+ /* #579: click-to-expand overlay for a truncated cell -- a read-only, full-value card, settled live as click (not Attio's usual
118
+ 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
119
+ 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
120
+ visible viewport, same as the edit-error comment above already
121
+ notes for any position:absolute escape -- unmeasured against Attio
122
+ for this exact case, flagged rather than guessed. */
123
+ & td[data-cell-expanded] { overflow: visible; z-index: 7; }
124
+ & .crisp-datatable-cell-overlay {
125
+ position: absolute; inset-inline-start: 0; top: 0; width: 100%; box-sizing: border-box;
126
+ padding: var(--crisp-space-2) var(--crisp-space-3); background: var(--crisp-bg-surface);
127
+ border: 1px solid var(--crisp-bg-brand-solid); border-radius: 4px;
128
+ font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium);
129
+ color: var(--crisp-fg-primary); white-space: normal; word-break: break-word; cursor: default;
130
+ }
93
131
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
94
132
  & table { table-layout: fixed; }
133
+ /* read-only/computed column marker (#596) -- sits just inside the
134
+ resize handle, same absolute-positioned-against-the-th approach the
135
+ handle itself uses, since th has zero padding and the button/inner
136
+ row fills the whole cell with no room left for a trailing sibling. */
137
+ & .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; }
95
138
  /* column resize grip */
96
139
  & .crisp-datatable-resize { position: absolute; top: 0; inset-inline-end: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
97
140
  & .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; }
98
141
  & .crisp-datatable-resize:hover::after, & .crisp-datatable-resize[data-resizing]::after { opacity: 1; }
142
+ /* crisp/#731 -- column drag-to-reorder. Measured live against Attio's own grid (the issue's own comment): the dragged header keeps full
143
+ opacity and the plain `pointer` cursor throughout (Attio draws no
144
+ ghost and no "grabbing" cursor of its own), so this adds nothing to
145
+ `[data-dragging]` itself -- the ONLY new paint is the drop-indicator
146
+ 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. */
147
+ & th[data-dragging] { cursor: pointer; }
148
+ & 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; }
149
+ & th[data-drop-before]::before { inset-inline-start: -1.5px; }
150
+ & th[data-drop-after]::after { inset-inline-end: -1.5px; }
99
151
  /* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
100
152
  & .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); }
101
- & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
102
- & .crisp-datatable-entity:hover .crisp-datatable-entity-name { text-decoration: underline; }
153
+ /* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text, plain hover vs rest) that Attio's underline is unconditional, not a
154
+ hover reveal -- identical at rest and on hover: text-decoration-color
155
+ 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. */
156
+ & .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); }
103
157
  & 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%; }
158
+ /* 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
159
+ hover", and a live check confirmed hovering well past the name --
160
+ 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
161
+ expressed as color-mix against fgPrimary so they stay correct in
162
+ dark mode instead of a literal black that would invert. */
163
+ & .crisp-datatable-entity-actions { display: flex; align-items: center; gap: var(--crisp-space-1-5); margin-inline-start: auto; flex-shrink: 0; opacity: 0; }
164
+ & tr:hover .crisp-datatable-entity-actions, & .crisp-datatable-entity-actions:focus-within { opacity: 1; }
165
+ & .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; }
166
+ & .crisp-datatable-entity-action:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-primary); }
104
167
  & .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); }
105
168
  & .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); }
106
169
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
@@ -127,6 +190,7 @@
127
190
  & .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; }
128
191
  & .crisp-datatable-grip:active { cursor: grabbing; }
129
192
  & tbody tr[data-drop-target] td, & tbody tr[data-drop-target] th { box-shadow: inset 0 2px 0 0 var(--crisp-bg-brand-solid); transition: box-shadow .12s; }
193
+ [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
130
194
  }
131
195
 
132
196
  @media (prefers-reduced-motion: reduce) {
package/dist/dialog.css CHANGED
@@ -1,16 +1,28 @@
1
1
  .crisp-dialog {
2
2
  display: contents; font-family: var(--crisp-font-sans);
3
- & .crisp-dialog-backdrop { position: fixed; inset: 0; z-index: 60; background: transparent; }
3
+ & .crisp-dialog-backdrop { position: fixed; inset: 0; z-index: 60; background: var(--crisp-bg-scrim); }
4
4
  & .crisp-dialog-positioner { position: fixed; inset: 0; z-index: 61; display: flex; align-items: flex-start; justify-content: center; padding: 15vh var(--crisp-space-4) var(--crisp-space-4); }
5
+ & .crisp-dialog-positioner[data-variant="alert"] { align-items: center; padding: var(--crisp-space-4); }
5
6
  & .crisp-dialog-content { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: relative; display: flex; flex-direction: column; box-sizing: border-box; width: 90vw; max-height: 74vh; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-2xl); background: color-mix(in srgb, var(--crisp-bg-overlay-frost) 60%, transparent); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); box-shadow: 0 8px 28px -6px var(--crisp-shadow-overlay1), 0 18px 88px -4px var(--crisp-shadow-overlay2), 0 0 1px var(--crisp-shadow-overlay3); opacity: 1; transform: scale(1); transition: opacity 0.2s ease, transform 0.2s ease; @starting-style { opacity: 0; transform: scale(0.97); } }
7
+ & .crisp-dialog-content[data-variant="alert"] { padding: 0; background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; box-shadow: none; }
6
8
  & .crisp-dialog-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); overflow: hidden; }
9
+ & .crisp-dialog-content[data-variant="alert"] .crisp-dialog-card { border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); }
7
10
  & .crisp-dialog-header { display: flex; align-items: center; gap: var(--crisp-space-2); flex-shrink: 0; height: 52px; padding: 0 var(--crisp-space-2) 0 var(--crisp-space-4); border-bottom: 1px solid var(--crisp-border-subtle); }
8
11
  & .crisp-dialog-header svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
9
12
  & .crisp-dialog-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); margin: 0; }
10
13
  & .crisp-dialog-close { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; }
11
14
  & .crisp-dialog-close:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
15
+ /* The discard-confirm's own stacked title + sentence (crisp#677) — no
16
+ border, no close button, unlike the panel header above: measured
17
+ live, both themes, 18px/24px 600-weight title (no scale token
18
+ reaches 18px — the scale tops out at text.xl's 16px, so this is raw px per Golden Rule 3's "genuinely intrinsic size" exception, same as
19
+ this file's own submit-key glyphs) then a 14px/20px 500-weight sentence 4px below it. */
20
+ & .crisp-dialog-alert-header { display: flex; flex-direction: column; gap: var(--crisp-space-1); padding: var(--crisp-space-4) var(--crisp-space-4) 0; }
21
+ & .crisp-dialog-alert-title { margin: 0; font-size: 18px; line-height: 24px; font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-primary); }
22
+ & .crisp-dialog-alert-description { margin: 0; font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-alert-description); }
12
23
  & .crisp-dialog-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--crisp-space-4); padding: var(--crisp-space-5) var(--crisp-space-4); }
13
24
  & .crisp-dialog-footer { display: flex; align-items: center; justify-content: flex-end; gap: var(--crisp-space-3); flex-shrink: 0; min-height: 56px; padding: 0 var(--crisp-space-4); border-top: 1px solid var(--crisp-border-subtle); }
25
+ & .crisp-dialog-footer[data-variant="alert"] { min-height: 45px; padding: var(--crisp-space-2); }
14
26
  & .crisp-dialog-field { display: flex; flex-direction: column; gap: var(--crisp-space-1-5); }
15
27
  & .crisp-dialog-field-label { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 50%, transparent); }
16
28
  & .crisp-dialog-more { display: inline-flex; align-items: center; gap: var(--crisp-space-2); margin-inline-end: auto; border: none; background: none; padding: 0; font-family: inherit; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); cursor: pointer; }
@@ -1,17 +1,29 @@
1
1
  @layer crisp-components {
2
2
  .crisp-dialog {
3
3
  display: contents; font-family: var(--crisp-font-sans);
4
- & .crisp-dialog-backdrop { position: fixed; inset: 0; z-index: 60; background: transparent; }
4
+ & .crisp-dialog-backdrop { position: fixed; inset: 0; z-index: 60; background: var(--crisp-bg-scrim); }
5
5
  & .crisp-dialog-positioner { position: fixed; inset: 0; z-index: 61; display: flex; align-items: flex-start; justify-content: center; padding: 15vh var(--crisp-space-4) var(--crisp-space-4); }
6
+ & .crisp-dialog-positioner[data-variant="alert"] { align-items: center; padding: var(--crisp-space-4); }
6
7
  & .crisp-dialog-content { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: relative; display: flex; flex-direction: column; box-sizing: border-box; width: 90vw; max-height: 74vh; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-2xl); background: color-mix(in srgb, var(--crisp-bg-overlay-frost) 60%, transparent); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); box-shadow: 0 8px 28px -6px var(--crisp-shadow-overlay1), 0 18px 88px -4px var(--crisp-shadow-overlay2), 0 0 1px var(--crisp-shadow-overlay3); opacity: 1; transform: scale(1); transition: opacity 0.2s ease, transform 0.2s ease; @starting-style { opacity: 0; transform: scale(0.97); } }
8
+ & .crisp-dialog-content[data-variant="alert"] { padding: 0; background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; box-shadow: none; }
7
9
  & .crisp-dialog-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); overflow: hidden; }
10
+ & .crisp-dialog-content[data-variant="alert"] .crisp-dialog-card { border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); }
8
11
  & .crisp-dialog-header { display: flex; align-items: center; gap: var(--crisp-space-2); flex-shrink: 0; height: 52px; padding: 0 var(--crisp-space-2) 0 var(--crisp-space-4); border-bottom: 1px solid var(--crisp-border-subtle); }
9
12
  & .crisp-dialog-header svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
10
13
  & .crisp-dialog-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); margin: 0; }
11
14
  & .crisp-dialog-close { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; }
12
15
  & .crisp-dialog-close:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
16
+ /* The discard-confirm's own stacked title + sentence (crisp#677) — no
17
+ border, no close button, unlike the panel header above: measured
18
+ live, both themes, 18px/24px 600-weight title (no scale token
19
+ reaches 18px — the scale tops out at text.xl's 16px, so this is raw px per Golden Rule 3's "genuinely intrinsic size" exception, same as
20
+ this file's own submit-key glyphs) then a 14px/20px 500-weight sentence 4px below it. */
21
+ & .crisp-dialog-alert-header { display: flex; flex-direction: column; gap: var(--crisp-space-1); padding: var(--crisp-space-4) var(--crisp-space-4) 0; }
22
+ & .crisp-dialog-alert-title { margin: 0; font-size: 18px; line-height: 24px; font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-primary); }
23
+ & .crisp-dialog-alert-description { margin: 0; font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-alert-description); }
13
24
  & .crisp-dialog-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--crisp-space-4); padding: var(--crisp-space-5) var(--crisp-space-4); }
14
25
  & .crisp-dialog-footer { display: flex; align-items: center; justify-content: flex-end; gap: var(--crisp-space-3); flex-shrink: 0; min-height: 56px; padding: 0 var(--crisp-space-4); border-top: 1px solid var(--crisp-border-subtle); }
26
+ & .crisp-dialog-footer[data-variant="alert"] { min-height: 45px; padding: var(--crisp-space-2); }
15
27
  & .crisp-dialog-field { display: flex; flex-direction: column; gap: var(--crisp-space-1-5); }
16
28
  & .crisp-dialog-field-label { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 50%, transparent); }
17
29
  & .crisp-dialog-more { display: inline-flex; align-items: center; gap: var(--crisp-space-2); margin-inline-end: auto; border: none; background: none; padding: 0; font-family: inherit; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); cursor: pointer; }
@@ -11,7 +11,11 @@ const spec = defineSpec({
11
11
  gap: c.space["1"],
12
12
  fontFamily: c.font.sans,
13
13
  fontWeight: c.weight.medium,
14
- letterSpacing: "-0.1px",
14
+ // crisp#696: every Attio pill measured this round -- the count pill and
15
+ // the tag pill both -- reads `normal` tracking, not the `-0.1px` this
16
+ // used to carry as a base default. `large` (unmeasured this round) keeps
17
+ // the old `-0.1px` as its own variant override, below.
18
+ letterSpacing: "normal",
15
19
  whiteSpace: "nowrap",
16
20
  },
17
21
  parts: [
@@ -38,7 +42,12 @@ const spec = defineSpec({
38
42
  neutral: {
39
43
  background: c.bgRaised,
40
44
  color: c.fgNeutralSoft,
41
- boxShadow: `inset 0 0 0 1px ${c.borderDefault}`,
45
+ // crisp#696: Attio's neutral pill ring measures rgb(238,239,241) --
46
+ // borderSubtle/line100 (#eeeff1), not borderDefault/line200
47
+ // (#e4e4e7). Background is unchanged: Attio measured
48
+ // rgb(246,247,247) against crisp's bgRaised #f4f4f5, a 2-unit
49
+ // difference the `savedviews` recipe already treats as noise.
50
+ boxShadow: `inset 0 0 0 1px ${c.borderSubtle}`,
42
51
  },
43
52
  brand: {
44
53
  background: soft(c.bgBrandSolid, 14),
@@ -72,22 +81,36 @@ const spec = defineSpec({
72
81
  },
73
82
  },
74
83
  size: {
84
+ // crisp#696: Attio's count pill (sidebar "Tasks 1", the "+1" overflow)
85
+ // measures 16px h / 4px padding / 12px text / 16px line-height -- was
86
+ // 6px padding, 11px text, and no line-height at all.
75
87
  small: {
76
88
  height: "16px",
77
- paddingInline: c.space["1.5"],
78
- fontSize: c.text.xs,
89
+ paddingInline: c.space["1"],
90
+ fontSize: c.text.sm,
91
+ lineHeight: "16px",
79
92
  borderRadius: c.radius.sm,
80
93
  },
94
+ // crisp#696: Attio's tag pill (Standard/System/Enriched) measures
95
+ // 20px h / 4px padding / 12px text / 16px line-height / 3px gap -- was
96
+ // 8px padding, no line-height, and the base's (now normal-tracking)
97
+ // 4px gap.
81
98
  medium: {
82
99
  height: "20px",
83
- paddingInline: c.space["2"],
100
+ paddingInline: c.space["1"],
84
101
  fontSize: c.text.sm,
102
+ lineHeight: "16px",
103
+ gap: "3px",
85
104
  borderRadius: c.radius.sm,
86
105
  },
106
+ // crisp#696: not re-measured this round -- kept exactly as it rendered
107
+ // before the base tracking changed to `normal`, by pinning the old
108
+ // `-0.1px` here instead. Gap is untouched (base's 4px).
87
109
  large: {
88
110
  height: c.size.chip,
89
111
  paddingInline: "9px",
90
112
  fontSize: c.text.lg,
113
+ letterSpacing: "-0.1px",
91
114
  borderRadius: c.radius.chip,
92
115
  },
93
116
  },
@@ -1,4 +1,4 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { Primitive } from "../../core/index.js";
4
4
  import { buttonRecipe } from "./button.recipe.js";
@@ -6,10 +6,41 @@ import { buttonRecipe } from "./button.recipe.js";
6
6
  function Spinner() {
7
7
  return (_jsxs("svg", { width: "1em", height: "1em", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", style: { flexShrink: 0 }, children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5", stroke: "currentColor", strokeOpacity: "0.25", strokeWidth: "2" }), _jsx("path", { d: "M8 1.5a6.5 6.5 0 0 1 6.5 6.5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: _jsx("animateTransform", { attributeName: "transform", type: "rotate", from: "0 8 8", to: "360 8 8", dur: "0.7s", repeatCount: "indefinite" }) })] }));
8
8
  }
9
+ /**
10
+ * crisp#682: an "icon" part is a valid element whose type is the host `svg`
11
+ * tag or isn't a string at all (a lucide icon, `Spinner`, any component) --
12
+ * everything else (strings, numbers, host elements like `span`) is a "text"
13
+ * part.
14
+ */
15
+ function isIconPart(node) {
16
+ return (React.isValidElement(node) &&
17
+ (node.type === "svg" || typeof node.type !== "string"));
18
+ }
9
19
  export const Button = React.forwardRef(({ intent, size, shape, touchTarget, className, asChild, loading, disabled, children, type, ...props }, ref) => {
10
20
  const isDisabled = disabled || loading;
11
21
  // Slot (asChild) requires a single child — never inject the spinner in that mode.
12
- const content = loading && !asChild ? (_jsxs(_Fragment, { children: [_jsx(Spinner, {}), children] })) : (children);
22
+ // An array (not a Fragment) so React.Children.toArray below sees the
23
+ // Spinner as its own top-level part instead of one opaque Fragment node.
24
+ const content = loading && !asChild
25
+ ? [_jsx(Spinner, {}, "crisp-button-spinner"), children]
26
+ : children;
27
+ // crisp#682: md's icon-side padding (6px vs 8px) depends on whether a
28
+ // side ends in an icon or text, which CSS alone can't tell apart
29
+ // (`<svg/>Label` vs `Label<svg/>` both skip text nodes under
30
+ // `:first-child`). Skipped for `asChild`, which renders someone else's
31
+ // single element rather than a list of parts.
32
+ let iconStart;
33
+ let iconEnd;
34
+ if (!asChild) {
35
+ const parts = React.Children.toArray(content);
36
+ const hasText = parts.some((part) => !isIconPart(part));
37
+ if (hasText && parts.length > 0) {
38
+ if (isIconPart(parts[0]))
39
+ iconStart = true;
40
+ if (isIconPart(parts[parts.length - 1]))
41
+ iconEnd = true;
42
+ }
43
+ }
13
44
  return (_jsx(Primitive.button, { ref: ref, asChild: asChild, className: buttonRecipe({
14
45
  intent,
15
46
  size,
@@ -21,6 +52,6 @@ export const Button = React.forwardRef(({ intent, size, shape, touchTarget, clas
21
52
  // a design system should submit a form by accident, so default to
22
53
  // "button" and make submitting explicit. `asChild` renders someone
23
54
  // else's element, which must not be given a button-only attribute.
24
- type: asChild ? type : (type ?? "button"), disabled: isDisabled, "aria-busy": loading || undefined, "data-loading": loading ? "" : undefined, ...props, children: content }));
55
+ type: asChild ? type : (type ?? "button"), disabled: isDisabled, "aria-busy": loading || undefined, "data-loading": loading ? "" : undefined, "data-icon-start": iconStart ? "" : undefined, "data-icon-end": iconEnd ? "" : undefined, ...props, children: content }));
25
56
  });
26
57
  Button.displayName = "Button";
@@ -9,7 +9,11 @@ const spec = defineSpec({
9
9
  gap: c.space["1.5"],
10
10
  fontFamily: c.font.sans,
11
11
  fontWeight: c.weight.medium,
12
- letterSpacing: "-0.14px",
12
+ // crisp#682: every 14px button label on app.attio.com measures -0.07px
13
+ // (17 of 17 on the Companies view, plus 222 sidebar/nav labels).
14
+ // -0.14px is Attio's table-cell tracking, not its button tracking --
15
+ // that's where this value came from.
16
+ letterSpacing: "-0.07px",
13
17
  lineHeight: "20px",
14
18
  whiteSpace: "nowrap",
15
19
  border: "none",
@@ -77,6 +81,9 @@ const spec = defineSpec({
77
81
  },
78
82
  },
79
83
  size: {
84
+ // sm/lg unchanged by crisp#682 -- neither was measured against Attio
85
+ // (Attio's small button is 20px, not sm's 24px; see crisp#682), so
86
+ // only md's icon-side padding is touched here.
80
87
  sm: {
81
88
  height: "24px",
82
89
  paddingInline: c.space["1.5"],
@@ -84,10 +91,29 @@ const spec = defineSpec({
84
91
  borderRadius: c.radius.md,
85
92
  },
86
93
  md: {
87
- height: "28px",
88
- paddingInline: c.space["2"],
89
- fontSize: c.text.lg,
90
- borderRadius: c.radius.md,
94
+ decls: {
95
+ height: "28px",
96
+ paddingInline: c.space["2"],
97
+ fontSize: c.text.lg,
98
+ borderRadius: c.radius.md,
99
+ },
100
+ // crisp#682: a side that ends in an icon gets 6px padding instead of
101
+ // 8px. CSS can't tell `<svg/>Label` apart from `Label<svg/>`
102
+ // (`:first-child` skips text nodes), so `Button` marks the icon
103
+ // sides itself via `data-icon-start`/`data-icon-end` (button.tsx).
104
+ // The attribute selector outranks the plain class above it, so this
105
+ // always wins over the shorthand `paddingInline` regardless of
106
+ // declaration order.
107
+ parts: [
108
+ {
109
+ selector: "&[data-icon-start]",
110
+ decls: { paddingInlineStart: c.space["1.5"] },
111
+ },
112
+ {
113
+ selector: "&[data-icon-end]",
114
+ decls: { paddingInlineEnd: c.space["1.5"] },
115
+ },
116
+ ],
91
117
  },
92
118
  lg: {
93
119
  height: "40px",
@@ -9,7 +9,7 @@ import { contextmenuRecipe } from "./contextmenu.recipe.js";
9
9
  * Wrap the target; right-clicking it opens the menu (flipping at the viewport
10
10
  * edges), closing on select, outside pointer-down, and Escape.
11
11
  */
12
- export function ContextMenu({ children, items, menuLabel: menuLabelProp, className, asChild = false, }) {
12
+ export function ContextMenu({ children, items, menuLabel: menuLabelProp, className, asChild = false, variant = "light", }) {
13
13
  // crisp/#471: same resolution order as Breadcrumb's own.
14
14
  const menuLabel = useCrispMessage(menuLabelProp, "Context menu");
15
15
  const rootRef = React.useRef(null);
@@ -76,7 +76,7 @@ export function ContextMenu({ children, items, menuLabel: menuLabelProp, classNa
76
76
  document.removeEventListener("keydown", onKey);
77
77
  };
78
78
  }, [pos, close]);
79
- return (_jsxs(_Fragment, { children: [asChild ? (_jsx(Slot, { ref: rootRef, className: className, onContextMenu: open, children: children })) : (_jsx("div", { ref: rootRef, className: contextmenuRecipe({ className }), children: _jsx("div", { onContextMenu: open, children: children }) })), pos && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsx("div", { ref: menuRef, className: "crisp-contextmenu-content", role: "menu", "aria-label": menuLabel, style: { left: pos.x, top: pos.y }, onKeyDown: onMenuKeyDown, children: items.map((item, i) => item.separator ? (_jsx("hr", { className: "crisp-contextmenu-sep" }, i)) : (_jsxs("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
79
+ return (_jsxs(_Fragment, { children: [asChild ? (_jsx(Slot, { ref: rootRef, className: className, onContextMenu: open, children: children })) : (_jsx("div", { ref: rootRef, className: contextmenuRecipe({ className }), children: _jsx("div", { onContextMenu: open, children: children }) })), pos && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsx("div", { ref: menuRef, className: "crisp-contextmenu-content", "data-variant": variant === "dark" ? "dark" : undefined, role: "menu", "aria-label": menuLabel, style: { left: pos.x, top: pos.y }, onKeyDown: onMenuKeyDown, children: items.map((item, i) => item.separator ? (_jsx("hr", { className: "crisp-contextmenu-sep" }, i)) : (_jsxs("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
80
80
  ? "crisp-contextmenu-item crisp-contextmenu-item--danger"
81
81
  : "crisp-contextmenu-item", tabIndex: -1, onClick: () => {
82
82
  item.onSelect?.();
@@ -32,6 +32,30 @@ const config = defineRecipe({
32
32
  .crisp-contextmenu-item--danger:hover { background: color-mix(in srgb, ${c.bgDanger} 10%, transparent); }
33
33
  .crisp-contextmenu-shortcut { margin-inline-start: auto; padding-inline-start: ${c.space["3"]}; color: ${c.fgMuted}; font-size: ${c.text.sm}; }
34
34
  .crisp-contextmenu-sep { height: 1px; margin: ${c.space["1"]} 0; border: none; background: ${c.borderSubtle}; }
35
+ /* crisp/#598: Attio's OWN cell-level context menu, measured live on
36
+ app.attio.com/techpartners/companies (right-click a data cell) --
37
+ theme-INDEPENDENT dark chrome, not this app's dark THEME (every
38
+ geometry value above -- 12px/8px radius, 4px/8px padding, 32px item
39
+ height, item gap included below -- already matched what was
40
+ measured here; only the colour pair differs, so only colours are
41
+ overridden). Same "#101112 / #eeeff1" dark-pill ink Tooltip
42
+ already hardcodes for the identical reason (that surface is ALSO
43
+ theme-independent). */
44
+ .crisp-contextmenu-content[data-variant="dark"] { background: #101112; box-shadow: none; }
45
+ .crisp-contextmenu-content[data-variant="dark"] .crisp-contextmenu-item { gap: ${c.space["1.5"]}; color: #eeeff1; }
46
+ .crisp-contextmenu-content[data-variant="dark"] .crisp-contextmenu-item svg { color: #898a8d; }
47
+ /* Plain-item hover re-measured live on the same cell menu (right-click
48
+ "Description" on app.attio.com/techpartners/companies, hover the
49
+ "Copy" row): getComputedStyle on the row is rgba(255,255,255,0.1),
50
+ not the 0.08 this used to guess at. The danger row's own hover
51
+ (hovering "Clear value") still didn't get re-measured -- that one
52
+ remains the derived red-on-black translation below, flagged on
53
+ crisp#598 for someone to confirm live. */
54
+ .crisp-contextmenu-content[data-variant="dark"] .crisp-contextmenu-item:hover { background: rgba(255,255,255,0.1); }
55
+ .crisp-contextmenu-content[data-variant="dark"] .crisp-contextmenu-item--danger { color: #ff5454; }
56
+ .crisp-contextmenu-content[data-variant="dark"] .crisp-contextmenu-item--danger svg { color: #ff5454; }
57
+ .crisp-contextmenu-content[data-variant="dark"] .crisp-contextmenu-item--danger:hover { background: rgba(255,84,84,0.12); }
58
+ .crisp-contextmenu-content[data-variant="dark"] .crisp-contextmenu-sep { background: rgba(255,255,255,0.1); }
35
59
  `,
36
60
  variants: {},
37
61
  defaultVariants: {},