@crisp-ui-kit/crisp 0.54.7 → 0.54.8

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 (113) 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 +13 -9
  6. package/dist/board.layered.css +13 -9
  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/datatable/datatable.js +66 -7
  13. package/dist/cjs/components/datatable/datatable.recipe.js +57 -2
  14. package/dist/cjs/components/dialog/dialog.js +8 -5
  15. package/dist/cjs/components/dialog/dialog.recipe.js +15 -1
  16. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +12 -3
  17. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +231 -48
  18. package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +44 -0
  19. package/dist/cjs/components/tabs/tabs.recipe.js +11 -2
  20. package/dist/cjs/components/tooltip/tooltip.js +23 -9
  21. package/dist/cjs/components/tooltip/tooltip.recipe.js +10 -9
  22. package/dist/cjs/product/authshell/authshell.js +14 -1
  23. package/dist/cjs/product/authshell/authshell.recipe.js +13 -1
  24. package/dist/cjs/product/board/board.js +122 -27
  25. package/dist/cjs/product/board/board.recipe.js +14 -2
  26. package/dist/cjs/product/comments/comments.js +23 -10
  27. package/dist/cjs/product/comments/comments.recipe.js +17 -3
  28. package/dist/cjs/product/formdialog/formdialog.js +10 -4
  29. package/dist/cjs/product/navitem/navitem.js +7 -2
  30. package/dist/cjs/product/navitem/navitem.recipe.js +16 -0
  31. package/dist/cjs/product/objectsettings/index.js +1 -2
  32. package/dist/cjs/product/objectsettings/objectsettings.js +3 -13
  33. package/dist/cjs/product/richtext/richtext.js +76 -1
  34. package/dist/cjs/product/sidebar/sidebar.js +68 -15
  35. package/dist/cjs/product/sidebar/sidebar.recipe.js +6 -1
  36. package/dist/cjs/tokens/palette.js +32 -1
  37. package/dist/cjs/tokens/semantic.js +11 -0
  38. package/dist/comments.css +36 -10
  39. package/dist/comments.layered.css +36 -10
  40. package/dist/datatable.css +47 -9
  41. package/dist/datatable.layered.css +47 -9
  42. package/dist/dialog.css +13 -1
  43. package/dist/dialog.layered.css +13 -1
  44. package/dist/esm/components/badge/badge.recipe.js +28 -5
  45. package/dist/esm/components/button/button.js +34 -3
  46. package/dist/esm/components/button/button.recipe.js +31 -5
  47. package/dist/esm/components/datatable/datatable.js +67 -8
  48. package/dist/esm/components/datatable/datatable.recipe.js +57 -2
  49. package/dist/esm/components/dialog/dialog.js +8 -5
  50. package/dist/esm/components/dialog/dialog.recipe.js +15 -1
  51. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +12 -3
  52. package/dist/esm/components/tabletoolbar/tabletoolbar.js +231 -48
  53. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +44 -0
  54. package/dist/esm/components/tabs/tabs.recipe.js +11 -2
  55. package/dist/esm/components/tooltip/tooltip.js +23 -9
  56. package/dist/esm/components/tooltip/tooltip.recipe.js +10 -9
  57. package/dist/esm/product/authshell/authshell.js +15 -2
  58. package/dist/esm/product/authshell/authshell.recipe.js +13 -1
  59. package/dist/esm/product/authshell/index.js +1 -1
  60. package/dist/esm/product/board/board.js +122 -27
  61. package/dist/esm/product/board/board.recipe.js +14 -2
  62. package/dist/esm/product/comments/comments.js +23 -10
  63. package/dist/esm/product/comments/comments.recipe.js +17 -3
  64. package/dist/esm/product/formdialog/formdialog.js +10 -4
  65. package/dist/esm/product/navitem/index.js +1 -1
  66. package/dist/esm/product/navitem/navitem.js +7 -2
  67. package/dist/esm/product/navitem/navitem.recipe.js +16 -0
  68. package/dist/esm/product/objectsettings/index.js +1 -1
  69. package/dist/esm/product/objectsettings/objectsettings.js +2 -12
  70. package/dist/esm/product/richtext/richtext.js +76 -1
  71. package/dist/esm/product/sidebar/sidebar.js +69 -16
  72. package/dist/esm/product/sidebar/sidebar.recipe.js +6 -1
  73. package/dist/esm/tokens/palette.js +32 -1
  74. package/dist/esm/tokens/semantic.js +11 -0
  75. package/dist/native/tokens.d.ts +1 -1
  76. package/dist/native/tokens.js +16 -2
  77. package/dist/navitem.css +7 -0
  78. package/dist/navitem.layered.css +7 -0
  79. package/dist/recordstable.css +1 -1
  80. package/dist/recordstable.layered.css +1 -1
  81. package/dist/sidebar.css +6 -1
  82. package/dist/sidebar.layered.css +6 -1
  83. package/dist/styles.css +163 -42
  84. package/dist/styles.layered.css +163 -42
  85. package/dist/tabletoolbar.css +16 -1
  86. package/dist/tabletoolbar.layered.css +16 -1
  87. package/dist/tabs.css +1 -1
  88. package/dist/tabs.layered.css +1 -1
  89. package/dist/tokens.css +9 -1
  90. package/dist/tokens.json +16 -2
  91. package/dist/tooltip.css +1 -1
  92. package/dist/tooltip.layered.css +1 -1
  93. package/dist/types/components/badge/badge.recipe.d.ts +5 -1
  94. package/dist/types/components/button/button.recipe.d.ts +18 -5
  95. package/dist/types/components/datatable/datatable.d.ts +9 -0
  96. package/dist/types/components/dialog/dialog.d.ts +25 -2
  97. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +43 -3
  98. package/dist/types/components/tooltip/tooltip.d.ts +12 -4
  99. package/dist/types/components/tooltip/tooltip.recipe.d.ts +1 -1
  100. package/dist/types/product/authshell/authshell.d.ts +26 -2
  101. package/dist/types/product/authshell/index.d.ts +1 -1
  102. package/dist/types/product/board/board.d.ts +5 -4
  103. package/dist/types/product/comments/comments.d.ts +10 -2
  104. package/dist/types/product/formdialog/formdialog.d.ts +2 -1
  105. package/dist/types/product/navitem/index.d.ts +1 -1
  106. package/dist/types/product/navitem/navitem.d.ts +23 -0
  107. package/dist/types/product/objectsettings/index.d.ts +1 -1
  108. package/dist/types/product/objectsettings/objectsettings.d.ts +0 -10
  109. package/dist/types/product/sidebar/sidebar.d.ts +33 -5
  110. package/dist/types/tokens/contract.d.ts +4 -0
  111. package/dist/types/tokens/palette.d.ts +13 -1
  112. package/dist/types/tokens/semantic.d.ts +11 -0
  113. 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
@@ -4,21 +4,28 @@
4
4
  & .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
5
  & .crisp-board-col-dot { width: 12px; height: 12px; border-radius: var(--crisp-radius-full); flex-shrink: 0; }
6
6
  & .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); }
7
+ /* 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
8
+ paints rgb(104,105,105), visibly lighter. Text now reads
9
+ 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. */
10
+ & .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
11
  & .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
12
  & .crisp-board-col-add:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
10
13
  & .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
14
  & .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
15
  & .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
16
  & .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; }
17
+ & .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; }
15
18
  & .crisp-board-card:active { cursor: grabbing; }
16
19
  & .crisp-board-card[data-dragging] { opacity: 0.4; }
17
20
  & .crisp-board-card:focus { outline: none; }
18
21
  & .crisp-board-card:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
19
- /* #604: a per-column calc footer, mirroring the table's own (measured
20
- live, Deals pipeline). Sits below the scrollable card body, not
21
- 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. */
22
+ /* #674: the pointer-events drag's own drag image -- the same card
23
+ surface above, just pinned to the pointer instead of the flex flow.
24
+ No shadow lift or scale: neither was measured against Attio, and this
25
+ is the same card, momentarily somewhere else, not a new look. */
26
+ & .crisp-board-ghost { position: fixed; z-index: 1; pointer-events: none; cursor: grabbing; }
27
+ /* #604: a per-column calc footer, mirroring the table's own (measured live, Deals pipeline). Sits below the scrollable card body, not inside it or pinned to it -- Attio's own is position: static, not
28
+ sticky, so it's a plain flex-shrink:0 sibling here too. */
22
29
  & .crisp-board-col-footer { flex-shrink: 0; padding: var(--crisp-space-1) 0; }
23
30
  & .crisp-board-col-calc { display: flex; align-items: center; justify-content: center; gap: var(--crisp-space-1); width: 100%; height: 28px; padding: var(--crisp-space-1) var(--crisp-space-2); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); cursor: pointer; }
24
31
  & .crisp-board-col-calc:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
@@ -26,10 +33,7 @@
26
33
  & .crisp-board-col-calc-label { text-transform: capitalize; }
27
34
  & .crisp-board-col-calc-value { color: var(--crisp-fg-primary); font-variant-numeric: tabular-nums; }
28
35
  /* 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
- }
36
+ & .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
37
  }
34
38
 
35
39
  @media (prefers-reduced-motion: reduce) {
@@ -5,21 +5,28 @@
5
5
  & .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
6
  & .crisp-board-col-dot { width: 12px; height: 12px; border-radius: var(--crisp-radius-full); flex-shrink: 0; }
7
7
  & .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); }
8
+ /* 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
9
+ paints rgb(104,105,105), visibly lighter. Text now reads
10
+ 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. */
11
+ & .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
12
  & .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
13
  & .crisp-board-col-add:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
11
14
  & .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
15
  & .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
16
  & .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
17
  & .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; }
18
+ & .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; }
16
19
  & .crisp-board-card:active { cursor: grabbing; }
17
20
  & .crisp-board-card[data-dragging] { opacity: 0.4; }
18
21
  & .crisp-board-card:focus { outline: none; }
19
22
  & .crisp-board-card:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
20
- /* #604: a per-column calc footer, mirroring the table's own (measured
21
- live, Deals pipeline). Sits below the scrollable card body, not
22
- 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. */
23
+ /* #674: the pointer-events drag's own drag image -- the same card
24
+ surface above, just pinned to the pointer instead of the flex flow.
25
+ No shadow lift or scale: neither was measured against Attio, and this
26
+ is the same card, momentarily somewhere else, not a new look. */
27
+ & .crisp-board-ghost { position: fixed; z-index: 1; pointer-events: none; cursor: grabbing; }
28
+ /* #604: a per-column calc footer, mirroring the table's own (measured live, Deals pipeline). Sits below the scrollable card body, not inside it or pinned to it -- Attio's own is position: static, not
29
+ sticky, so it's a plain flex-shrink:0 sibling here too. */
23
30
  & .crisp-board-col-footer { flex-shrink: 0; padding: var(--crisp-space-1) 0; }
24
31
  & .crisp-board-col-calc { display: flex; align-items: center; justify-content: center; gap: var(--crisp-space-1); width: 100%; height: 28px; padding: var(--crisp-space-1) var(--crisp-space-2); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); cursor: pointer; }
25
32
  & .crisp-board-col-calc:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
@@ -27,10 +34,7 @@
27
34
  & .crisp-board-col-calc-label { text-transform: capitalize; }
28
35
  & .crisp-board-col-calc-value { color: var(--crisp-fg-primary); font-variant-numeric: tabular-nums; }
29
36
  /* 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
- }
37
+ & .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
38
  }
35
39
 
36
40
  @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",
@@ -529,6 +529,34 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
529
529
  const showColumn = React.useCallback((key) => applyColumns(allColumns.map((c) => (c.key === key ? { ...c, hidden: false } : c))), [allColumns, applyColumns]);
530
530
  const [selCell, setSelCell] = React.useState(null);
531
531
  const gridRef = React.useRef(null);
532
+ // #579: the click-to-expand overlay for a truncated cell. Only one is ever
533
+ // open (a second click-to-expand elsewhere closes the first, matching "one
534
+ // live session"-style singular UI state already used for `selCell`).
535
+ const [expandedCell, setExpandedCell] = React.useState(null);
536
+ // Closes on any click outside the overlay itself (a click ON it is
537
+ // stopped from propagating, see the overlay's own onClick) and on
538
+ // Escape, kept for accessibility even though Attio's own Escape handling
539
+ // wasn't reliable when this was measured live (crisp#579).
540
+ React.useEffect(() => {
541
+ if (!expandedCell)
542
+ return;
543
+ const close = (e) => {
544
+ const target = e.target;
545
+ if (target?.closest?.(".crisp-datatable-cell-overlay"))
546
+ return;
547
+ setExpandedCell(null);
548
+ };
549
+ const onKey = (e) => {
550
+ if (e.key === "Escape")
551
+ setExpandedCell(null);
552
+ };
553
+ document.addEventListener("click", close, true);
554
+ document.addEventListener("keydown", onKey);
555
+ return () => {
556
+ document.removeEventListener("click", close, true);
557
+ document.removeEventListener("keydown", onKey);
558
+ };
559
+ }, [expandedCell]);
532
560
  const sort = sortProp !== undefined ? sortProp : internalSort;
533
561
  // One shape downstream: [] when unsorted, ordered by precedence otherwise.
534
562
  const sorts = React.useMemo(() => (sort == null ? [] : Array.isArray(sort) ? sort : [sort]), [sort]);
@@ -957,6 +985,10 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
957
985
  function openEditing(id, key) {
958
986
  setEditError(null);
959
987
  setEditing({ id, key });
988
+ // #579: the editing branch pre-empts the plain-cell render (and its
989
+ // overlay) entirely, but `expandedCell` itself would survive a later
990
+ // cancel-back-to-plain-view unless cleared here too.
991
+ setExpandedCell(null);
960
992
  }
961
993
  // Async-aware (#385): a sync `onEdit` (returns undefined) commits exactly
962
994
  // as before -- close the editor immediately. A Promise-returning `onEdit`
@@ -1212,7 +1244,17 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1212
1244
  // aria-sort is specified as one header per table, and with the
1213
1245
  // multi-sort the `sort` prop allows, a second "ascending" says
1214
1246
  // nothing about which column came first.
1215
- const sortable = col.sortable || !!columnMenu;
1247
+ // crisp/#605: the primary/entity column is inert -- no menu,
1248
+ // no click-to-sort, no aria-sort claim -- matching Attio's
1249
+ // own measured behaviour exactly ("no popover opens, no
1250
+ // right-click menu either... primary column is fixed by
1251
+ // construction, not a menu target", checklist G-023). A
1252
+ // consumer setting `sortable`/`columnMenu` on an entity
1253
+ // column doesn't get a sort affordance there; this is
1254
+ // `!isPrimary` gating every interactive branch below, not
1255
+ // just the menu.
1256
+ const isPrimary = col.type === "entity";
1257
+ const sortable = !isPrimary && (col.sortable || !!columnMenu);
1216
1258
  const ariaSort = !sortable
1217
1259
  ? undefined
1218
1260
  : sorts[0]?.key === col.key
@@ -1233,9 +1275,9 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1233
1275
  }
1234
1276
  else if (e.key === "Escape")
1235
1277
  setRenaming(null);
1236
- } })) : columnMenu ? ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "start", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), "aria-label": `${typeof col.header === "string"
1278
+ } })) : columnMenu && !isPrimary ? ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "start", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), "aria-label": `${typeof col.header === "string"
1237
1279
  ? col.header
1238
- : col.key} ${crispMessages["column options"] ?? "column options"}`, children: inner }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setSort({ key: col.key, dir: "asc" }), children: sortAscendingLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setSort({ key: col.key, dir: "desc" }), children: sortDescendingLabel }), col.managed === false ? null : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => moveColumn(col.key, -1), children: moveColumnLeftLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => moveColumn(col.key, 1), children: moveColumnRightLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setRenaming(col.key), children: editColumnLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => hideColumn(col.key), children: hideColumnLabel })] }))] })) : col.sortable ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), onClick: () => cycleSort(col.key), children: inner })) : (inner), col.resizable !== false && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-resize", "data-resizing": resizingKey === col.key ? "" : undefined, onPointerDown: (e) => startResize(col, e), onPointerMove: moveResize, onPointerUp: endResize }))] }, col.key));
1280
+ : col.key} ${crispMessages["column options"] ?? "column options"}`, children: inner }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setSort({ key: col.key, dir: "asc" }), children: sortAscendingLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setSort({ key: col.key, dir: "desc" }), children: sortDescendingLabel }), col.managed === false ? null : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => moveColumn(col.key, -1), children: moveColumnLeftLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => moveColumn(col.key, 1), children: moveColumnRightLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setRenaming(col.key), children: editColumnLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => hideColumn(col.key), children: hideColumnLabel })] }))] })) : col.sortable && !isPrimary ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), onClick: () => cycleSort(col.key), children: inner })) : (inner), col.readOnly && ((0, jsx_runtime_1.jsx)(lucide_react_1.Lock, { className: "crisp-datatable-readonly", size: 12, strokeWidth: 1.75, "aria-hidden": "true" })), col.resizable !== false && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-resize", "data-resizing": resizingKey === col.key ? "" : undefined, onPointerDown: (e) => startResize(col, e), onPointerMove: moveResize, onPointerUp: endResize }))] }, col.key));
1239
1281
  })] }) }), (0, jsx_runtime_1.jsx)(table_1.TableBody, { children: error ? ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { colSpan: totalCols, children: (0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-empty", children: error }) }) })) : rows.length === 0 ? ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { colSpan: totalCols, children: (0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-empty", children: empty }) }) })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [topSpacerHeight > 0 && ((0, jsx_runtime_1.jsx)("tr", { "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("td", { colSpan: totalCols, style: {
1240
1282
  height: topSpacerHeight,
1241
1283
  padding: 0,
@@ -1337,22 +1379,39 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
1337
1379
  commitEdit(row.id, col.key, e.currentTarget.value);
1338
1380
  } }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }, col.key));
1339
1381
  }
1382
+ const rendered = col.render
1383
+ ? col.render(value, row)
1384
+ : renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale);
1385
+ const isExpanded = expandedCell?.id === row.id &&
1386
+ expandedCell?.key === col.key;
1340
1387
  return ((0, jsx_runtime_1.jsxs)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: col.editable
1341
1388
  ? "crisp-datatable-editable"
1342
1389
  : undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
1343
1390
  ? ""
1344
- : undefined, onClick: () => {
1391
+ : undefined, "data-cell-expanded": isExpanded ? "" : undefined, onClick: (e) => {
1345
1392
  // the keyboard model's anchor -- a single click
1346
1393
  // only selects. A genuine double-click (or Enter
1347
1394
  // on the selected cell) opens the editor (#411),
1348
1395
  // matching RecordsTable's own onDoubleClick.
1349
1396
  setSelCell({ id: row.id, key: col.key });
1350
1397
  gridRef.current?.focus({ preventScroll: true });
1398
+ // #579: a truncated cell also opens a read-only
1399
+ // overlay with its full value -- settled live
1400
+ // that this is click, not Attio's usual
1401
+ // hover-expand (a 1s hover on a truncated
1402
+ // Description cell produced nothing). Measured
1403
+ // the same way as `ThLabel`'s header tooltip
1404
+ // (#534): scrollWidth vs clientWidth on the
1405
+ // element the CSS ellipsis actually applies to,
1406
+ // which for a data cell is the `td` itself
1407
+ // (table.recipe.ts's own `overflow: hidden;
1408
+ // text-overflow: ellipsis`), not a nested span.
1409
+ setExpandedCell(isLabelTruncated(e.currentTarget)
1410
+ ? { id: row.id, key: col.key }
1411
+ : null);
1351
1412
  }, onDoubleClick: col.editable
1352
1413
  ? () => openEditing(row.id, col.key)
1353
- : undefined, children: [col.render
1354
- ? col.render(value, row)
1355
- : renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
1414
+ : undefined, children: [!isExpanded && rendered, errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere })), isExpanded && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-cell-overlay", onClick: (e) => e.stopPropagation(), children: rendered }))] }, col.key));
1356
1415
  })] }, row.id));
1357
1416
  }), bottomSpacerHeight > 0 && ((0, jsx_runtime_1.jsx)("tr", { "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("td", { colSpan: totalCols, style: {
1358
1417
  height: bottomSpacerHeight,