@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
@@ -3,7 +3,11 @@
3
3
  &[data-single-pane] { justify-content: center; }
4
4
  & .crisp-authshell-progress { position: absolute; top: 0; left: 0; width: 100%; height: 2px; background: var(--crisp-border-default); }
5
5
  & .crisp-authshell-progress-fill { height: 2px; background: var(--crisp-fg-primary); border-radius: 4px; transition: width .2s; }
6
- & .crisp-authshell-logo { margin-top: var(--crisp-space-8); height: 24px; display: flex; align-items: center; }
6
+ & .crisp-authshell-logo { margin-top: var(--crisp-space-8); min-height: 24px; display: flex; align-items: center; gap: var(--crisp-space-2-5); }
7
+ & .crisp-authshell-logo-badge { flex-shrink: 0; box-sizing: border-box; width: 32px; height: 32px; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-brand-solid); color: #fff; display: flex; align-items: center; justify-content: center; }
8
+ & .crisp-authshell-logo-text { display: flex; flex-direction: column; line-height: 1.2; }
9
+ & .crisp-authshell-logo-title { font-size: var(--crisp-text-xl); font-weight: var(--crisp-weight-bold); letter-spacing: -0.2px; color: var(--crisp-fg-primary); }
10
+ & .crisp-authshell-logo-subtitle { font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); }
7
11
  & .crisp-authshell-card { margin-top: var(--crisp-space-6); display: flex; height: 692px; border-radius: 20px; border: 1px solid var(--crisp-border-subtle); overflow: hidden; }
8
12
  &[data-single-pane] .crisp-authshell-card { height: auto; border: none; border-radius: 0; overflow: visible; }
9
13
  & .crisp-authshell-pane { flex: 0 0 564px; padding: 72px; display: flex; flex-direction: column; box-sizing: border-box; }
@@ -4,7 +4,11 @@
4
4
  &[data-single-pane] { justify-content: center; }
5
5
  & .crisp-authshell-progress { position: absolute; top: 0; left: 0; width: 100%; height: 2px; background: var(--crisp-border-default); }
6
6
  & .crisp-authshell-progress-fill { height: 2px; background: var(--crisp-fg-primary); border-radius: 4px; transition: width .2s; }
7
- & .crisp-authshell-logo { margin-top: var(--crisp-space-8); height: 24px; display: flex; align-items: center; }
7
+ & .crisp-authshell-logo { margin-top: var(--crisp-space-8); min-height: 24px; display: flex; align-items: center; gap: var(--crisp-space-2-5); }
8
+ & .crisp-authshell-logo-badge { flex-shrink: 0; box-sizing: border-box; width: 32px; height: 32px; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-brand-solid); color: #fff; display: flex; align-items: center; justify-content: center; }
9
+ & .crisp-authshell-logo-text { display: flex; flex-direction: column; line-height: 1.2; }
10
+ & .crisp-authshell-logo-title { font-size: var(--crisp-text-xl); font-weight: var(--crisp-weight-bold); letter-spacing: -0.2px; color: var(--crisp-fg-primary); }
11
+ & .crisp-authshell-logo-subtitle { font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); }
8
12
  & .crisp-authshell-card { margin-top: var(--crisp-space-6); display: flex; height: 692px; border-radius: 20px; border: 1px solid var(--crisp-border-subtle); overflow: hidden; }
9
13
  &[data-single-pane] .crisp-authshell-card { height: auto; border: none; border-radius: 0; overflow: visible; }
10
14
  & .crisp-authshell-pane { flex: 0 0 564px; padding: 72px; display: flex; flex-direction: column; box-sizing: border-box; }
package/dist/badge.css CHANGED
@@ -1,9 +1,9 @@
1
1
  .crisp-badge {
2
- display: inline-flex; align-items: center; justify-content: center; gap: var(--crisp-space-1); font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-medium); letter-spacing: -0.1px; white-space: nowrap;
2
+ display: inline-flex; align-items: center; justify-content: center; gap: var(--crisp-space-1); font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-medium); letter-spacing: normal; white-space: nowrap;
3
3
  & .crisp-badge-dot { width: 6px; height: 6px; flex-shrink: 0; border-radius: var(--crisp-radius-full); background: currentColor; }
4
4
  }
5
5
  .crisp-badge--tone_neutral {
6
- background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-soft); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
6
+ background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-soft); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle);
7
7
  }
8
8
  .crisp-badge--tone_brand {
9
9
  background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: var(--crisp-fg-brand-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-brand-solid) 30%, transparent);
@@ -21,13 +21,13 @@
21
21
  background: var(--crisp-bg-purple-soft); color: var(--crisp-fg-purple-soft); box-shadow: inset 0 0 0 1px var(--crisp-ring-purple-soft);
22
22
  }
23
23
  .crisp-badge--size_small {
24
- height: 16px; padding-inline: var(--crisp-space-1-5); font-size: var(--crisp-text-xs); border-radius: var(--crisp-radius-sm);
24
+ height: 16px; padding-inline: var(--crisp-space-1); font-size: var(--crisp-text-sm); line-height: 16px; border-radius: var(--crisp-radius-sm);
25
25
  }
26
26
  .crisp-badge--size_medium {
27
- height: 20px; padding-inline: var(--crisp-space-2); font-size: var(--crisp-text-sm); border-radius: var(--crisp-radius-sm);
27
+ 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);
28
28
  }
29
29
  .crisp-badge--size_large {
30
- height: var(--crisp-size-chip); padding-inline: 9px; font-size: var(--crisp-text-lg); border-radius: var(--crisp-radius-chip);
30
+ height: var(--crisp-size-chip); padding-inline: 9px; font-size: var(--crisp-text-lg); letter-spacing: -0.1px; border-radius: var(--crisp-radius-chip);
31
31
  }
32
32
  .crisp-badge--variant_soft {
33
33
 
@@ -1,10 +1,10 @@
1
1
  @layer crisp-components {
2
2
  .crisp-badge {
3
- display: inline-flex; align-items: center; justify-content: center; gap: var(--crisp-space-1); font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-medium); letter-spacing: -0.1px; white-space: nowrap;
3
+ display: inline-flex; align-items: center; justify-content: center; gap: var(--crisp-space-1); font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-medium); letter-spacing: normal; white-space: nowrap;
4
4
  & .crisp-badge-dot { width: 6px; height: 6px; flex-shrink: 0; border-radius: var(--crisp-radius-full); background: currentColor; }
5
5
  }
6
6
  .crisp-badge--tone_neutral {
7
- background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-soft); box-shadow: inset 0 0 0 1px var(--crisp-border-default);
7
+ background: var(--crisp-bg-raised); color: var(--crisp-fg-neutral-soft); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle);
8
8
  }
9
9
  .crisp-badge--tone_brand {
10
10
  background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: var(--crisp-fg-brand-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-brand-solid) 30%, transparent);
@@ -22,13 +22,13 @@
22
22
  background: var(--crisp-bg-purple-soft); color: var(--crisp-fg-purple-soft); box-shadow: inset 0 0 0 1px var(--crisp-ring-purple-soft);
23
23
  }
24
24
  .crisp-badge--size_small {
25
- height: 16px; padding-inline: var(--crisp-space-1-5); font-size: var(--crisp-text-xs); border-radius: var(--crisp-radius-sm);
25
+ height: 16px; padding-inline: var(--crisp-space-1); font-size: var(--crisp-text-sm); line-height: 16px; border-radius: var(--crisp-radius-sm);
26
26
  }
27
27
  .crisp-badge--size_medium {
28
- height: 20px; padding-inline: var(--crisp-space-2); font-size: var(--crisp-text-sm); border-radius: var(--crisp-radius-sm);
28
+ 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);
29
29
  }
30
30
  .crisp-badge--size_large {
31
- height: var(--crisp-size-chip); padding-inline: 9px; font-size: var(--crisp-text-lg); border-radius: var(--crisp-radius-chip);
31
+ height: var(--crisp-size-chip); padding-inline: 9px; font-size: var(--crisp-text-lg); letter-spacing: -0.1px; border-radius: var(--crisp-radius-chip);
32
32
  }
33
33
  .crisp-badge--variant_soft {
34
34
 
package/dist/board.css CHANGED
@@ -1,21 +1,47 @@
1
1
  .crisp-board {
2
- display: flex; gap: var(--crisp-space-2); height: 100%; box-sizing: border-box; padding: var(--crisp-space-3); overflow-x: auto; align-items: flex-start; font-family: var(--crisp-font-sans);
3
- & .crisp-board-col { display: flex; flex-direction: column; width: 268px; flex-shrink: 0; max-height: 100%; box-sizing: border-box; }
2
+ display: flex; flex-direction: column; gap: var(--crisp-space-2); height: 100%; box-sizing: border-box; font-family: var(--crisp-font-sans); /* #718: the density toolbar's own row -- everything that used to be on
3
+ the root (padding/overflow/the columns themselves) moves to
4
+ .crisp-board-row below so this can sit above it, unscrolled. */
5
+ & .crisp-board-toolbar { display: flex; align-items: center; justify-content: flex-end; gap: var(--crisp-space-2); padding: 0 var(--crisp-space-3); flex-shrink: 0; }
6
+ /* Measured live (Deals pipeline): a 54×28px group, 24×24px buttons,
7
+ 2px padding/gap -- an 8px gap between the two groups comes from the
8
+ toolbar's own `gap` above. */
9
+ & .crisp-board-density { display: flex; align-items: center; gap: 2px; padding: 2px; border-radius: 10px; background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
10
+ & .crisp-board-density button { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; border: none; border-radius: 8px; background: none; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); font-family: inherit; cursor: pointer; }
11
+ & .crisp-board-density button[data-state="on"] { background: var(--crisp-bg-surface); color: var(--crisp-fg-primary); }
12
+ & .crisp-board-row { display: flex; gap: var(--crisp-space-2); flex: 1; min-height: 0; box-sizing: border-box; padding: var(--crisp-space-3); overflow-x: auto; align-items: flex-start; }
13
+ & .crisp-board-col { display: flex; flex-direction: column; width: 268px; flex-shrink: 0; max-height: 100%; box-sizing: border-box; transition: width 0.15s; }
14
+ /* #718: a collapsed column is a 52px vertical rail -- header only (dot + count stacked above a vertical-rl title), body/footer hidden entirely. Measured live: 52px width, 8px 0 padding on the header, 14px/600 title text. */
15
+ & .crisp-board-col[data-density="collapse"] { width: 52px; }
16
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-header { flex-direction: column; align-items: center; height: auto; padding: var(--crisp-space-2) 0; gap: var(--crisp-space-1); }
17
+ /* Measured stacking order (top to bottom): dot, count, THEN the vertical title -- reordered visually via `order`, not by changing the JSX/DOM order, which stays dot/title/count (reading order, unaffected by density). */
18
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-title { order: 1; writing-mode: vertical-rl; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); }
19
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-count { order: 0; }
20
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-body, & .crisp-board-col[data-density="collapse"] .crisp-board-col-footer { display: none; }
4
21
  & .crisp-board-col-header { display: flex; align-items: center; gap: var(--crisp-space-2); height: 36px; padding: 0 var(--crisp-space-1); flex-shrink: 0; }
5
22
  & .crisp-board-col-dot { width: 12px; height: 12px; border-radius: var(--crisp-radius-full); flex-shrink: 0; }
6
23
  & .crisp-board-col-title { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-primary); }
7
- & .crisp-board-col-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
24
+ /* crisp#697: HANDOFF's #472 note claimed the count pill's color-mix(fgPrimary, 63%, transparent) reproduced the measured rgb(80,81,85) -- it doesn't; 63% of fgPrimary composited on white
25
+ paints rgb(104,105,105), visibly lighter. Text now reads
26
+ fgNeutralSoft directly (the same rgb(80,81,85) token Badge's neutral tone uses, #696), and the ring the pill was also missing (inset 0 0 0 1px borderSubtle, measured rgb(238,239,241)) is added. */
27
+ & .crisp-board-col-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle); font-size: var(--crisp-text-sm); line-height: 16px; font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-neutral-soft); }
8
28
  & .crisp-board-col-add { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin-inline-start: auto; padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; }
9
29
  & .crisp-board-col-add:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
10
30
  & .crisp-board-col-body { display: flex; flex-direction: column; gap: var(--crisp-space-2); flex: 1; min-height: 48px; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-lg); overflow-y: auto; transition: background 0.12s; }
11
31
  & .crisp-board-col[data-over] .crisp-board-col-body { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 7%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-brand-solid) 30%, transparent); }
12
32
  & .crisp-board-newcard { display: flex; align-items: center; justify-content: center; gap: var(--crisp-space-1-5); height: 32px; flex-shrink: 0; border: none; border-radius: var(--crisp-radius-md); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-muted); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; }
13
33
  & .crisp-board-newcard:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); color: var(--crisp-fg-primary); }
14
- & .crisp-board-card { display: flex; flex-direction: column; gap: var(--crisp-space-2); box-sizing: border-box; padding: var(--crisp-space-2-5) var(--crisp-space-3); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle); cursor: grab; }
34
+ & .crisp-board-card { display: flex; flex-direction: column; gap: var(--crisp-space-2); box-sizing: border-box; padding: var(--crisp-space-2-5) var(--crisp-space-3); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle); cursor: grab; touch-action: none; }
35
+ /* #718: a collapsed card is a single 40px row -- measured live and independent of `c.size.tableRow`/`tableHeader` (36/40px) even though it happens to match the latter; a plain literal, not a cross-reference to a token measured for a different surface. `renderCard`'s own second argument is what actually decides what a
36
+ collapsed card SHOWS (Board never sees inside its opaque output);
37
+ this only supplies the row-shaped box that content sits in. */
38
+ & .crisp-board-card[data-density="collapse"] { flex-direction: row; align-items: center; height: 40px; padding: 0 var(--crisp-space-3); }
15
39
  & .crisp-board-card:active { cursor: grabbing; }
16
40
  & .crisp-board-card[data-dragging] { opacity: 0.4; }
17
41
  & .crisp-board-card:focus { outline: none; }
18
42
  & .crisp-board-card:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
43
+ /* #674: the pointer-events drag's own drag image -- the same card surface above, just pinned to the pointer instead of the flex flow. No shadow lift or scale: neither was measured against Attio, and this is the same card, momentarily somewhere else, not a new look. */
44
+ & .crisp-board-ghost { position: fixed; z-index: 1; pointer-events: none; cursor: grabbing; }
19
45
  /* #604: a per-column calc footer, mirroring the table's own (measured
20
46
  live, Deals pipeline). Sits below the scrollable card body, not
21
47
  inside it or pinned to it -- Attio's own is position: static, not sticky, so it's a plain flex-shrink:0 sibling here too. */
@@ -25,11 +51,13 @@
25
51
  & .crisp-board-col-calc[data-empty] { opacity: 0.7; }
26
52
  & .crisp-board-col-calc-label { text-transform: capitalize; }
27
53
  & .crisp-board-col-calc-value { color: var(--crisp-fg-primary); font-variant-numeric: tabular-nums; }
54
+ /* #718: "Add stage" -- measured live: 52×52px, transparent, a 1px
55
+ dashed border (rgb(230,231,234), exactly `borderDropzone`/`line150`
56
+ -- the same dashed-border colour ImportDialog's own dropzone already uses, not borderSubtle/line100, a close but different value), radius.xl (16px), a centered 16px "+". */
57
+ & .crisp-board-addstage { display: inline-flex; align-items: center; justify-content: center; width: 52px; height: 52px; flex-shrink: 0; border: 1px dashed var(--crisp-border-dropzone); border-radius: var(--crisp-radius-xl); background: none; color: var(--crisp-fg-primary); font-family: inherit; cursor: pointer; }
58
+ & .crisp-board-addstage:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
28
59
  /* announced, never drawn */
29
- & .crisp-board-live {
30
- position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
31
- overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
32
- }
60
+ & .crisp-board-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
33
61
  }
34
62
 
35
63
  @media (prefers-reduced-motion: reduce) {
@@ -1,22 +1,48 @@
1
1
  @layer crisp-components {
2
2
  .crisp-board {
3
- display: flex; gap: var(--crisp-space-2); height: 100%; box-sizing: border-box; padding: var(--crisp-space-3); overflow-x: auto; align-items: flex-start; font-family: var(--crisp-font-sans);
4
- & .crisp-board-col { display: flex; flex-direction: column; width: 268px; flex-shrink: 0; max-height: 100%; box-sizing: border-box; }
3
+ display: flex; flex-direction: column; gap: var(--crisp-space-2); height: 100%; box-sizing: border-box; font-family: var(--crisp-font-sans); /* #718: the density toolbar's own row -- everything that used to be on
4
+ the root (padding/overflow/the columns themselves) moves to
5
+ .crisp-board-row below so this can sit above it, unscrolled. */
6
+ & .crisp-board-toolbar { display: flex; align-items: center; justify-content: flex-end; gap: var(--crisp-space-2); padding: 0 var(--crisp-space-3); flex-shrink: 0; }
7
+ /* Measured live (Deals pipeline): a 54×28px group, 24×24px buttons,
8
+ 2px padding/gap -- an 8px gap between the two groups comes from the
9
+ toolbar's own `gap` above. */
10
+ & .crisp-board-density { display: flex; align-items: center; gap: 2px; padding: 2px; border-radius: 10px; background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
11
+ & .crisp-board-density button { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; border: none; border-radius: 8px; background: none; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); font-family: inherit; cursor: pointer; }
12
+ & .crisp-board-density button[data-state="on"] { background: var(--crisp-bg-surface); color: var(--crisp-fg-primary); }
13
+ & .crisp-board-row { display: flex; gap: var(--crisp-space-2); flex: 1; min-height: 0; box-sizing: border-box; padding: var(--crisp-space-3); overflow-x: auto; align-items: flex-start; }
14
+ & .crisp-board-col { display: flex; flex-direction: column; width: 268px; flex-shrink: 0; max-height: 100%; box-sizing: border-box; transition: width 0.15s; }
15
+ /* #718: a collapsed column is a 52px vertical rail -- header only (dot + count stacked above a vertical-rl title), body/footer hidden entirely. Measured live: 52px width, 8px 0 padding on the header, 14px/600 title text. */
16
+ & .crisp-board-col[data-density="collapse"] { width: 52px; }
17
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-header { flex-direction: column; align-items: center; height: auto; padding: var(--crisp-space-2) 0; gap: var(--crisp-space-1); }
18
+ /* Measured stacking order (top to bottom): dot, count, THEN the vertical title -- reordered visually via `order`, not by changing the JSX/DOM order, which stays dot/title/count (reading order, unaffected by density). */
19
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-title { order: 1; writing-mode: vertical-rl; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); }
20
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-count { order: 0; }
21
+ & .crisp-board-col[data-density="collapse"] .crisp-board-col-body, & .crisp-board-col[data-density="collapse"] .crisp-board-col-footer { display: none; }
5
22
  & .crisp-board-col-header { display: flex; align-items: center; gap: var(--crisp-space-2); height: 36px; padding: 0 var(--crisp-space-1); flex-shrink: 0; }
6
23
  & .crisp-board-col-dot { width: 12px; height: 12px; border-radius: var(--crisp-radius-full); flex-shrink: 0; }
7
24
  & .crisp-board-col-title { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-primary); }
8
- & .crisp-board-col-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
25
+ /* crisp#697: HANDOFF's #472 note claimed the count pill's color-mix(fgPrimary, 63%, transparent) reproduced the measured rgb(80,81,85) -- it doesn't; 63% of fgPrimary composited on white
26
+ paints rgb(104,105,105), visibly lighter. Text now reads
27
+ fgNeutralSoft directly (the same rgb(80,81,85) token Badge's neutral tone uses, #696), and the ring the pill was also missing (inset 0 0 0 1px borderSubtle, measured rgb(238,239,241)) is added. */
28
+ & .crisp-board-col-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle); font-size: var(--crisp-text-sm); line-height: 16px; font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-neutral-soft); }
9
29
  & .crisp-board-col-add { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin-inline-start: auto; padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; }
10
30
  & .crisp-board-col-add:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
11
31
  & .crisp-board-col-body { display: flex; flex-direction: column; gap: var(--crisp-space-2); flex: 1; min-height: 48px; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-lg); overflow-y: auto; transition: background 0.12s; }
12
32
  & .crisp-board-col[data-over] .crisp-board-col-body { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 7%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-brand-solid) 30%, transparent); }
13
33
  & .crisp-board-newcard { display: flex; align-items: center; justify-content: center; gap: var(--crisp-space-1-5); height: 32px; flex-shrink: 0; border: none; border-radius: var(--crisp-radius-md); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); color: var(--crisp-fg-muted); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); cursor: pointer; }
14
34
  & .crisp-board-newcard:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 7%, transparent); color: var(--crisp-fg-primary); }
15
- & .crisp-board-card { display: flex; flex-direction: column; gap: var(--crisp-space-2); box-sizing: border-box; padding: var(--crisp-space-2-5) var(--crisp-space-3); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle); cursor: grab; }
35
+ & .crisp-board-card { display: flex; flex-direction: column; gap: var(--crisp-space-2); box-sizing: border-box; padding: var(--crisp-space-2-5) var(--crisp-space-3); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle); cursor: grab; touch-action: none; }
36
+ /* #718: a collapsed card is a single 40px row -- measured live and independent of `c.size.tableRow`/`tableHeader` (36/40px) even though it happens to match the latter; a plain literal, not a cross-reference to a token measured for a different surface. `renderCard`'s own second argument is what actually decides what a
37
+ collapsed card SHOWS (Board never sees inside its opaque output);
38
+ this only supplies the row-shaped box that content sits in. */
39
+ & .crisp-board-card[data-density="collapse"] { flex-direction: row; align-items: center; height: 40px; padding: 0 var(--crisp-space-3); }
16
40
  & .crisp-board-card:active { cursor: grabbing; }
17
41
  & .crisp-board-card[data-dragging] { opacity: 0.4; }
18
42
  & .crisp-board-card:focus { outline: none; }
19
43
  & .crisp-board-card:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
44
+ /* #674: the pointer-events drag's own drag image -- the same card surface above, just pinned to the pointer instead of the flex flow. No shadow lift or scale: neither was measured against Attio, and this is the same card, momentarily somewhere else, not a new look. */
45
+ & .crisp-board-ghost { position: fixed; z-index: 1; pointer-events: none; cursor: grabbing; }
20
46
  /* #604: a per-column calc footer, mirroring the table's own (measured
21
47
  live, Deals pipeline). Sits below the scrollable card body, not
22
48
  inside it or pinned to it -- Attio's own is position: static, not sticky, so it's a plain flex-shrink:0 sibling here too. */
@@ -26,11 +52,13 @@
26
52
  & .crisp-board-col-calc[data-empty] { opacity: 0.7; }
27
53
  & .crisp-board-col-calc-label { text-transform: capitalize; }
28
54
  & .crisp-board-col-calc-value { color: var(--crisp-fg-primary); font-variant-numeric: tabular-nums; }
55
+ /* #718: "Add stage" -- measured live: 52×52px, transparent, a 1px
56
+ dashed border (rgb(230,231,234), exactly `borderDropzone`/`line150`
57
+ -- the same dashed-border colour ImportDialog's own dropzone already uses, not borderSubtle/line100, a close but different value), radius.xl (16px), a centered 16px "+". */
58
+ & .crisp-board-addstage { display: inline-flex; align-items: center; justify-content: center; width: 52px; height: 52px; flex-shrink: 0; border: 1px dashed var(--crisp-border-dropzone); border-radius: var(--crisp-radius-xl); background: none; color: var(--crisp-fg-primary); font-family: inherit; cursor: pointer; }
59
+ & .crisp-board-addstage:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
29
60
  /* announced, never drawn */
30
- & .crisp-board-live {
31
- position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
32
- overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
33
- }
61
+ & .crisp-board-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
34
62
  }
35
63
 
36
64
  @media (prefers-reduced-motion: reduce) {
package/dist/button.css CHANGED
@@ -1,5 +1,5 @@
1
1
  .crisp-button {
2
- display: inline-flex; align-items: center; justify-content: center; gap: var(--crisp-space-1-5); font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; line-height: 20px; white-space: nowrap; border: none; cursor: pointer; transition: background-color .2s, box-shadow .2s, color .2s;
2
+ display: inline-flex; align-items: center; justify-content: center; gap: var(--crisp-space-1-5); font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-medium); letter-spacing: -0.07px; line-height: 20px; white-space: nowrap; border: none; cursor: pointer; transition: background-color .2s, box-shadow .2s, color .2s;
3
3
  &:disabled { opacity: 0.5; cursor: not-allowed; }
4
4
  }
5
5
  .crisp-button--intent_primary {
@@ -25,6 +25,8 @@
25
25
  }
26
26
  .crisp-button--size_md {
27
27
  height: 28px; padding-inline: var(--crisp-space-2); font-size: var(--crisp-text-lg); border-radius: var(--crisp-radius-md);
28
+ &[data-icon-start] { padding-inline-start: var(--crisp-space-1-5); }
29
+ &[data-icon-end] { padding-inline-end: var(--crisp-space-1-5); }
28
30
  }
29
31
  .crisp-button--size_lg {
30
32
  height: 40px; padding-inline: 14px; font-size: var(--crisp-text-lg); border-radius: 10px;
@@ -1,6 +1,6 @@
1
1
  @layer crisp-components {
2
2
  .crisp-button {
3
- display: inline-flex; align-items: center; justify-content: center; gap: var(--crisp-space-1-5); font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-medium); letter-spacing: -0.14px; line-height: 20px; white-space: nowrap; border: none; cursor: pointer; transition: background-color .2s, box-shadow .2s, color .2s;
3
+ display: inline-flex; align-items: center; justify-content: center; gap: var(--crisp-space-1-5); font-family: var(--crisp-font-sans); font-weight: var(--crisp-weight-medium); letter-spacing: -0.07px; line-height: 20px; white-space: nowrap; border: none; cursor: pointer; transition: background-color .2s, box-shadow .2s, color .2s;
4
4
  &:disabled { opacity: 0.5; cursor: not-allowed; }
5
5
  }
6
6
  .crisp-button--intent_primary {
@@ -26,6 +26,8 @@
26
26
  }
27
27
  .crisp-button--size_md {
28
28
  height: 28px; padding-inline: var(--crisp-space-2); font-size: var(--crisp-text-lg); border-radius: var(--crisp-radius-md);
29
+ &[data-icon-start] { padding-inline-start: var(--crisp-space-1-5); }
30
+ &[data-icon-end] { padding-inline-end: var(--crisp-space-1-5); }
29
31
  }
30
32
  .crisp-button--size_lg {
31
33
  height: 40px; padding-inline: 14px; font-size: var(--crisp-text-lg); border-radius: 10px;
@@ -14,7 +14,11 @@ const spec = (0, core_1.defineSpec)({
14
14
  gap: tokens_1.contract.space["1"],
15
15
  fontFamily: tokens_1.contract.font.sans,
16
16
  fontWeight: tokens_1.contract.weight.medium,
17
- letterSpacing: "-0.1px",
17
+ // crisp#696: every Attio pill measured this round -- the count pill and
18
+ // the tag pill both -- reads `normal` tracking, not the `-0.1px` this
19
+ // used to carry as a base default. `large` (unmeasured this round) keeps
20
+ // the old `-0.1px` as its own variant override, below.
21
+ letterSpacing: "normal",
18
22
  whiteSpace: "nowrap",
19
23
  },
20
24
  parts: [
@@ -41,7 +45,12 @@ const spec = (0, core_1.defineSpec)({
41
45
  neutral: {
42
46
  background: tokens_1.contract.bgRaised,
43
47
  color: tokens_1.contract.fgNeutralSoft,
44
- boxShadow: `inset 0 0 0 1px ${tokens_1.contract.borderDefault}`,
48
+ // crisp#696: Attio's neutral pill ring measures rgb(238,239,241) --
49
+ // borderSubtle/line100 (#eeeff1), not borderDefault/line200
50
+ // (#e4e4e7). Background is unchanged: Attio measured
51
+ // rgb(246,247,247) against crisp's bgRaised #f4f4f5, a 2-unit
52
+ // difference the `savedviews` recipe already treats as noise.
53
+ boxShadow: `inset 0 0 0 1px ${tokens_1.contract.borderSubtle}`,
45
54
  },
46
55
  brand: {
47
56
  background: soft(tokens_1.contract.bgBrandSolid, 14),
@@ -75,22 +84,36 @@ const spec = (0, core_1.defineSpec)({
75
84
  },
76
85
  },
77
86
  size: {
87
+ // crisp#696: Attio's count pill (sidebar "Tasks 1", the "+1" overflow)
88
+ // measures 16px h / 4px padding / 12px text / 16px line-height -- was
89
+ // 6px padding, 11px text, and no line-height at all.
78
90
  small: {
79
91
  height: "16px",
80
- paddingInline: tokens_1.contract.space["1.5"],
81
- fontSize: tokens_1.contract.text.xs,
92
+ paddingInline: tokens_1.contract.space["1"],
93
+ fontSize: tokens_1.contract.text.sm,
94
+ lineHeight: "16px",
82
95
  borderRadius: tokens_1.contract.radius.sm,
83
96
  },
97
+ // crisp#696: Attio's tag pill (Standard/System/Enriched) measures
98
+ // 20px h / 4px padding / 12px text / 16px line-height / 3px gap -- was
99
+ // 8px padding, no line-height, and the base's (now normal-tracking)
100
+ // 4px gap.
84
101
  medium: {
85
102
  height: "20px",
86
- paddingInline: tokens_1.contract.space["2"],
103
+ paddingInline: tokens_1.contract.space["1"],
87
104
  fontSize: tokens_1.contract.text.sm,
105
+ lineHeight: "16px",
106
+ gap: "3px",
88
107
  borderRadius: tokens_1.contract.radius.sm,
89
108
  },
109
+ // crisp#696: not re-measured this round -- kept exactly as it rendered
110
+ // before the base tracking changed to `normal`, by pinning the old
111
+ // `-0.1px` here instead. Gap is untouched (base's 4px).
90
112
  large: {
91
113
  height: tokens_1.contract.size.chip,
92
114
  paddingInline: "9px",
93
115
  fontSize: tokens_1.contract.text.lg,
116
+ letterSpacing: "-0.1px",
94
117
  borderRadius: tokens_1.contract.radius.chip,
95
118
  },
96
119
  },
@@ -42,10 +42,41 @@ const button_recipe_1 = require("./button.recipe");
42
42
  function Spinner() {
43
43
  return ((0, jsx_runtime_1.jsxs)("svg", { width: "1em", height: "1em", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", style: { flexShrink: 0 }, children: [(0, jsx_runtime_1.jsx)("circle", { cx: "8", cy: "8", r: "6.5", stroke: "currentColor", strokeOpacity: "0.25", strokeWidth: "2" }), (0, jsx_runtime_1.jsx)("path", { d: "M8 1.5a6.5 6.5 0 0 1 6.5 6.5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: (0, jsx_runtime_1.jsx)("animateTransform", { attributeName: "transform", type: "rotate", from: "0 8 8", to: "360 8 8", dur: "0.7s", repeatCount: "indefinite" }) })] }));
44
44
  }
45
+ /**
46
+ * crisp#682: an "icon" part is a valid element whose type is the host `svg`
47
+ * tag or isn't a string at all (a lucide icon, `Spinner`, any component) --
48
+ * everything else (strings, numbers, host elements like `span`) is a "text"
49
+ * part.
50
+ */
51
+ function isIconPart(node) {
52
+ return (React.isValidElement(node) &&
53
+ (node.type === "svg" || typeof node.type !== "string"));
54
+ }
45
55
  exports.Button = React.forwardRef(({ intent, size, shape, touchTarget, className, asChild, loading, disabled, children, type, ...props }, ref) => {
46
56
  const isDisabled = disabled || loading;
47
57
  // Slot (asChild) requires a single child — never inject the spinner in that mode.
48
- const content = loading && !asChild ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(Spinner, {}), children] })) : (children);
58
+ // An array (not a Fragment) so React.Children.toArray below sees the
59
+ // Spinner as its own top-level part instead of one opaque Fragment node.
60
+ const content = loading && !asChild
61
+ ? [(0, jsx_runtime_1.jsx)(Spinner, {}, "crisp-button-spinner"), children]
62
+ : children;
63
+ // crisp#682: md's icon-side padding (6px vs 8px) depends on whether a
64
+ // side ends in an icon or text, which CSS alone can't tell apart
65
+ // (`<svg/>Label` vs `Label<svg/>` both skip text nodes under
66
+ // `:first-child`). Skipped for `asChild`, which renders someone else's
67
+ // single element rather than a list of parts.
68
+ let iconStart;
69
+ let iconEnd;
70
+ if (!asChild) {
71
+ const parts = React.Children.toArray(content);
72
+ const hasText = parts.some((part) => !isIconPart(part));
73
+ if (hasText && parts.length > 0) {
74
+ if (isIconPart(parts[0]))
75
+ iconStart = true;
76
+ if (isIconPart(parts[parts.length - 1]))
77
+ iconEnd = true;
78
+ }
79
+ }
49
80
  return ((0, jsx_runtime_1.jsx)(core_1.Primitive.button, { ref: ref, asChild: asChild, className: (0, button_recipe_1.buttonRecipe)({
50
81
  intent,
51
82
  size,
@@ -57,6 +88,6 @@ exports.Button = React.forwardRef(({ intent, size, shape, touchTarget, className
57
88
  // a design system should submit a form by accident, so default to
58
89
  // "button" and make submitting explicit. `asChild` renders someone
59
90
  // else's element, which must not be given a button-only attribute.
60
- type: asChild ? type : (type ?? "button"), disabled: isDisabled, "aria-busy": loading || undefined, "data-loading": loading ? "" : undefined, ...props, children: content }));
91
+ 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 }));
61
92
  });
62
93
  exports.Button.displayName = "Button";
@@ -12,7 +12,11 @@ const spec = (0, core_1.defineSpec)({
12
12
  gap: tokens_1.contract.space["1.5"],
13
13
  fontFamily: tokens_1.contract.font.sans,
14
14
  fontWeight: tokens_1.contract.weight.medium,
15
- letterSpacing: "-0.14px",
15
+ // crisp#682: every 14px button label on app.attio.com measures -0.07px
16
+ // (17 of 17 on the Companies view, plus 222 sidebar/nav labels).
17
+ // -0.14px is Attio's table-cell tracking, not its button tracking --
18
+ // that's where this value came from.
19
+ letterSpacing: "-0.07px",
16
20
  lineHeight: "20px",
17
21
  whiteSpace: "nowrap",
18
22
  border: "none",
@@ -80,6 +84,9 @@ const spec = (0, core_1.defineSpec)({
80
84
  },
81
85
  },
82
86
  size: {
87
+ // sm/lg unchanged by crisp#682 -- neither was measured against Attio
88
+ // (Attio's small button is 20px, not sm's 24px; see crisp#682), so
89
+ // only md's icon-side padding is touched here.
83
90
  sm: {
84
91
  height: "24px",
85
92
  paddingInline: tokens_1.contract.space["1.5"],
@@ -87,10 +94,29 @@ const spec = (0, core_1.defineSpec)({
87
94
  borderRadius: tokens_1.contract.radius.md,
88
95
  },
89
96
  md: {
90
- height: "28px",
91
- paddingInline: tokens_1.contract.space["2"],
92
- fontSize: tokens_1.contract.text.lg,
93
- borderRadius: tokens_1.contract.radius.md,
97
+ decls: {
98
+ height: "28px",
99
+ paddingInline: tokens_1.contract.space["2"],
100
+ fontSize: tokens_1.contract.text.lg,
101
+ borderRadius: tokens_1.contract.radius.md,
102
+ },
103
+ // crisp#682: a side that ends in an icon gets 6px padding instead of
104
+ // 8px. CSS can't tell `<svg/>Label` apart from `Label<svg/>`
105
+ // (`:first-child` skips text nodes), so `Button` marks the icon
106
+ // sides itself via `data-icon-start`/`data-icon-end` (button.tsx).
107
+ // The attribute selector outranks the plain class above it, so this
108
+ // always wins over the shorthand `paddingInline` regardless of
109
+ // declaration order.
110
+ parts: [
111
+ {
112
+ selector: "&[data-icon-start]",
113
+ decls: { paddingInlineStart: tokens_1.contract.space["1.5"] },
114
+ },
115
+ {
116
+ selector: "&[data-icon-end]",
117
+ decls: { paddingInlineEnd: tokens_1.contract.space["1.5"] },
118
+ },
119
+ ],
94
120
  },
95
121
  lg: {
96
122
  height: "40px",
@@ -45,7 +45,7 @@ const contextmenu_recipe_1 = require("./contextmenu.recipe");
45
45
  * Wrap the target; right-clicking it opens the menu (flipping at the viewport
46
46
  * edges), closing on select, outside pointer-down, and Escape.
47
47
  */
48
- function ContextMenu({ children, items, menuLabel: menuLabelProp, className, asChild = false, }) {
48
+ function ContextMenu({ children, items, menuLabel: menuLabelProp, className, asChild = false, variant = "light", }) {
49
49
  // crisp/#471: same resolution order as Breadcrumb's own.
50
50
  const menuLabel = (0, core_1.useCrispMessage)(menuLabelProp, "Context menu");
51
51
  const rootRef = React.useRef(null);
@@ -112,7 +112,7 @@ function ContextMenu({ children, items, menuLabel: menuLabelProp, className, asC
112
112
  document.removeEventListener("keydown", onKey);
113
113
  };
114
114
  }, [pos, close]);
115
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [asChild ? ((0, jsx_runtime_1.jsx)(react_slot_1.Slot, { ref: rootRef, className: className, onContextMenu: open, children: children })) : ((0, jsx_runtime_1.jsx)("div", { ref: rootRef, className: (0, contextmenu_recipe_1.contextmenuRecipe)({ className }), children: (0, jsx_runtime_1.jsx)("div", { onContextMenu: open, children: children }) })), pos && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.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 ? ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-contextmenu-sep" }, i)) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
115
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [asChild ? ((0, jsx_runtime_1.jsx)(react_slot_1.Slot, { ref: rootRef, className: className, onContextMenu: open, children: children })) : ((0, jsx_runtime_1.jsx)("div", { ref: rootRef, className: (0, contextmenu_recipe_1.contextmenuRecipe)({ className }), children: (0, jsx_runtime_1.jsx)("div", { onContextMenu: open, children: children }) })), pos && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.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 ? ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-contextmenu-sep" }, i)) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
116
116
  ? "crisp-contextmenu-item crisp-contextmenu-item--danger"
117
117
  : "crisp-contextmenu-item", tabIndex: -1, onClick: () => {
118
118
  item.onSelect?.();
@@ -35,6 +35,30 @@ const config = (0, core_1.defineRecipe)({
35
35
  .crisp-contextmenu-item--danger:hover { background: color-mix(in srgb, ${tokens_1.contract.bgDanger} 10%, transparent); }
36
36
  .crisp-contextmenu-shortcut { margin-inline-start: auto; padding-inline-start: ${tokens_1.contract.space["3"]}; color: ${tokens_1.contract.fgMuted}; font-size: ${tokens_1.contract.text.sm}; }
37
37
  .crisp-contextmenu-sep { height: 1px; margin: ${tokens_1.contract.space["1"]} 0; border: none; background: ${tokens_1.contract.borderSubtle}; }
38
+ /* crisp/#598: Attio's OWN cell-level context menu, measured live on
39
+ app.attio.com/techpartners/companies (right-click a data cell) --
40
+ theme-INDEPENDENT dark chrome, not this app's dark THEME (every
41
+ geometry value above -- 12px/8px radius, 4px/8px padding, 32px item
42
+ height, item gap included below -- already matched what was
43
+ measured here; only the colour pair differs, so only colours are
44
+ overridden). Same "#101112 / #eeeff1" dark-pill ink Tooltip
45
+ already hardcodes for the identical reason (that surface is ALSO
46
+ theme-independent). */
47
+ .crisp-contextmenu-content[data-variant="dark"] { background: #101112; box-shadow: none; }
48
+ .crisp-contextmenu-content[data-variant="dark"] .crisp-contextmenu-item { gap: ${tokens_1.contract.space["1.5"]}; color: #eeeff1; }
49
+ .crisp-contextmenu-content[data-variant="dark"] .crisp-contextmenu-item svg { color: #898a8d; }
50
+ /* Plain-item hover re-measured live on the same cell menu (right-click
51
+ "Description" on app.attio.com/techpartners/companies, hover the
52
+ "Copy" row): getComputedStyle on the row is rgba(255,255,255,0.1),
53
+ not the 0.08 this used to guess at. The danger row's own hover
54
+ (hovering "Clear value") still didn't get re-measured -- that one
55
+ remains the derived red-on-black translation below, flagged on
56
+ crisp#598 for someone to confirm live. */
57
+ .crisp-contextmenu-content[data-variant="dark"] .crisp-contextmenu-item:hover { background: rgba(255,255,255,0.1); }
58
+ .crisp-contextmenu-content[data-variant="dark"] .crisp-contextmenu-item--danger { color: #ff5454; }
59
+ .crisp-contextmenu-content[data-variant="dark"] .crisp-contextmenu-item--danger svg { color: #ff5454; }
60
+ .crisp-contextmenu-content[data-variant="dark"] .crisp-contextmenu-item--danger:hover { background: rgba(255,84,84,0.12); }
61
+ .crisp-contextmenu-content[data-variant="dark"] .crisp-contextmenu-sep { background: rgba(255,255,255,0.1); }
38
62
  `,
39
63
  variants: {},
40
64
  defaultVariants: {},