@crisp-ui-kit/crisp 0.54.6 → 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 +19 -1
  17. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +239 -50
  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 +19 -1
  52. package/dist/esm/components/tabletoolbar/tabletoolbar.js +240 -51
  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 +2 -1
  80. package/dist/recordstable.layered.css +2 -1
  81. package/dist/sidebar.css +6 -1
  82. package/dist/sidebar.layered.css +6 -1
  83. package/dist/styles.css +165 -42
  84. package/dist/styles.layered.css +165 -42
  85. package/dist/tabletoolbar.css +17 -1
  86. package/dist/tabletoolbar.layered.css +17 -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 +47 -5
  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
@@ -9,8 +9,15 @@
9
9
  & .crisp-datatable-count { margin-inline-start: auto; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
10
10
  /* announced, never drawn */
11
11
  & .crisp-datatable-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
12
- /* The header cell has no padding of its own -- the button owns the full cell so the whole header is clickable (its own inset is 12px). Mirrors the summary-row fix. */
13
- & th { padding: 0; }
12
+ /* The header cell has no padding of its own -- the button owns the full cell so the whole header is clickable (its own inset is 12px). Mirrors the summary-row fix. `& .crisp-table th`, not bare `& th` (found while re-checking #691's
13
+ own fix live): DataTable's root nests `Table`'s own wrapper, whose
14
+ `table.recipe.ts` carries `& th, & td { padding: 0 12px }` at the
15
+ SAME specificity (.crisp-table th vs .crisp-datatable th, both one
16
+ class + one element) -- a bare `& th` here only won by source
17
+ order, which flipped the moment table.recipe.ts's own CSS file happened to land after datatable's in the build (alphabetical:
18
+ datatable < table), silently reinstating 12px on every header cell
19
+ in the whole product and making every interactive column's button double it to 24px. Scoping through `.crisp-table` adds a second class to the selector (0,2,1 beats 0,1,1) so this can never lose a specificity tie to source order again. */
20
+ & .crisp-table th { padding: 0; }
14
21
  /* Same wrapper trap as AUTHORING.md Lesson 40 (ColumnCalc's summary trigger):
15
22
  Menu wraps its trigger in a flex div that sizes to its content, so a
16
23
  long header grew past the column instead of filling it -- with nothing
@@ -19,6 +26,15 @@
19
26
  & th .crisp-menu { width: 100%; height: 100%; }
20
27
  & .crisp-datatable-th-inner { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; min-width: 0; height: 100%; }
21
28
  & .crisp-datatable-th-inner > svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
29
+ /* crisp/#605 follow-up: a header with neither a Menu nor a sort button (the primary/entity column's now-inert path, or any column with no
30
+ columnMenu/sortable at all) renders .crisp-datatable-th-inner as a
31
+ DIRECT child of th instead of nested inside a button -- and loses
32
+ the button's own 12px inset, drifting left of the td's 12px base
33
+ inset. Measured live on Attio's own Companies grid: the header label and the cell's own text land at the exact same x (335px from the
34
+ viewport, both 40px from the column's left edge) via two different padding+icon+gap combinations -- Attio does not leave the inert primary header flush left either. The `>` combinator targets only the button-less case: Menu's own wrapper (`.crisp-menu`) and the
35
+ plain sort `button` both put >=1 more level between `th` and this
36
+ span, so their own 12px (already on the button) isn't doubled. */
37
+ & th > .crisp-datatable-th-inner { box-sizing: border-box; padding: 0 var(--crisp-space-3); }
22
38
  /* The summary cell has NO padding of its own -- the calculation trigger owns the whole 246x37 area so a click anywhere in the cell opens the menu. Zero the cell's inset and let ColumnCalc supply the 12px. */
23
39
  & tfoot td { padding: 0; }
24
40
  /* Group heading row -- a view is grouped under a subtle label with the
@@ -89,8 +105,23 @@
89
105
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
90
106
  & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
91
107
  & .crisp-datatable-empty { padding: var(--crisp-space-8) var(--crisp-space-3); text-align: center; color: var(--crisp-fg-muted); font-size: var(--crisp-text-lg); }
108
+ /* #579: click-to-expand overlay for a truncated cell -- a read-only,
109
+ full-value card, settled live as click (not Attio's usual hover-expand) and measured position: absolute, unlike this file's
110
+ floating chrome (Menu/Select popovers) which portals and escapes the
111
+ scroll container. Deliberately NOT the edit-error pattern above
112
+ (grow the row, block-level, never clipped): a virtualized grid
113
+ assumes a fixed row height for its scroll math (owned by the grid
114
+ core, not this cell), so growing a row here would fight it. Anchored
115
+ to the SAME column width the cell itself renders at (measured:
116
+ Attio's own overlay came out only slightly wider than the triggering cell's clientWidth -- the overshoot is this padding),
117
+ wrapping onto as many lines as the value needs; it CAN be clipped by
118
+ `.crisp-table-scroll`'s own `overflow: auto` if it grows past the visible viewport, same as the edit-error comment above already notes for any position:absolute escape -- unmeasured against Attio for this exact case, flagged rather than guessed. */
119
+ & td[data-cell-expanded] { overflow: visible; z-index: 7; }
120
+ & .crisp-datatable-cell-overlay { position: absolute; inset-inline-start: 0; top: 0; width: 100%; box-sizing: border-box; padding: var(--crisp-space-2) var(--crisp-space-3); background: var(--crisp-bg-surface); border: 1px solid var(--crisp-bg-brand-solid); border-radius: 4px; font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); white-space: normal; word-break: break-word; cursor: default; }
92
121
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
93
122
  & table { table-layout: fixed; }
123
+ /* read-only/computed column marker (#596) -- sits just inside the resize handle, same absolute-positioned-against-the-th approach the handle itself uses, since th has zero padding and the button/inner row fills the whole cell with no room left for a trailing sibling. */
124
+ & .crisp-datatable-readonly { position: absolute; top: 50%; inset-inline-end: 11px; transform: translateY(-50%); color: var(--crisp-fg-muted); pointer-events: none; z-index: 5; }
94
125
  /* column resize grip */
95
126
  & .crisp-datatable-resize { position: absolute; top: 0; inset-inline-end: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
96
127
  & .crisp-datatable-resize::after { content: ""; position: absolute; top: 0; inset-inline-start: 3px; width: 2px; height: 100%; background: var(--crisp-bg-brand-solid); opacity: 0; transition: opacity .12s; }
@@ -101,7 +132,12 @@
101
132
  & .crisp-datatable-entity:hover .crisp-datatable-entity-name { text-decoration: underline; }
102
133
  & button.crisp-datatable-entity-name--link { margin: 0; padding: 0; border: none; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-align: start; cursor: pointer; max-width: 100%; }
103
134
  & .crisp-datatable-avatar { width: var(--crisp-size-icon); height: var(--crisp-size-icon); border-radius: var(--crisp-radius-sm); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: var(--crisp-weight-semibold); text-transform: uppercase; color: var(--crisp-fg-on-brand); background: var(--crisp-bg-brand-solid); }
104
- & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); } & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); } & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); } & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); } & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); } & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
135
+ & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
136
+ & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
137
+ & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); }
138
+ & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); }
139
+ & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); }
140
+ & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
105
141
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
106
142
  & .crisp-datatable-tags { display: flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
107
143
  & .crisp-datatable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
@@ -114,14 +150,16 @@
114
150
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
115
151
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
116
152
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
117
- /* url/email cell types (#654) -- same measured values as RecordsTable's own .crisp-recordstable-link, which this mirrors: no underline at rest, underline on hover, own-colour text rather than a browser-blue link. */
153
+ /* url/email cell types (#654) -- same measured values as RecordsTable's
154
+ own .crisp-recordstable-link, which this mirrors: no underline at
155
+ rest, underline on hover, own-colour text rather than a browser-blue
156
+ link. */
118
157
  & .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
119
158
  & .crisp-datatable-link:hover { text-decoration: underline; }
120
- /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
121
- hairline doubling as the drop indicator (it tints toward the brand
122
- colour as the active drop target). The affordance + geometry are
123
- measured; the drag MECHANISM is crisp's existing house idiom -- HTML5 draggable + dataTransfer, the same one column reorder already uses (RecordsTable's reorderColumn) -- not a proprietary
124
- pointer-events implementation. */
159
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
160
+ a leading 20px grip column, a 16px grip icon, and the row's own hairline doubling as the drop indicator (it tints toward the brand colour as the active drop target). The affordance + geometry are measured; the drag MECHANISM is crisp's existing house idiom -- HTML5
161
+ draggable + dataTransfer, the same one column reorder already uses
162
+ (RecordsTable's reorderColumn) -- not a proprietary pointer-events implementation. */
125
163
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
126
164
  & .crisp-datatable-grip { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; margin: 0; border: none; border-radius: 0; background: none; font-family: inherit; color: var(--crisp-fg-muted); cursor: grab; }
127
165
  & .crisp-datatable-grip:active { cursor: grabbing; }
@@ -10,8 +10,15 @@
10
10
  & .crisp-datatable-count { margin-inline-start: auto; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); }
11
11
  /* announced, never drawn */
12
12
  & .crisp-datatable-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
13
- /* The header cell has no padding of its own -- the button owns the full cell so the whole header is clickable (its own inset is 12px). Mirrors the summary-row fix. */
14
- & th { padding: 0; }
13
+ /* The header cell has no padding of its own -- the button owns the full cell so the whole header is clickable (its own inset is 12px). Mirrors the summary-row fix. `& .crisp-table th`, not bare `& th` (found while re-checking #691's
14
+ own fix live): DataTable's root nests `Table`'s own wrapper, whose
15
+ `table.recipe.ts` carries `& th, & td { padding: 0 12px }` at the
16
+ SAME specificity (.crisp-table th vs .crisp-datatable th, both one
17
+ class + one element) -- a bare `& th` here only won by source
18
+ order, which flipped the moment table.recipe.ts's own CSS file happened to land after datatable's in the build (alphabetical:
19
+ datatable < table), silently reinstating 12px on every header cell
20
+ in the whole product and making every interactive column's button double it to 24px. Scoping through `.crisp-table` adds a second class to the selector (0,2,1 beats 0,1,1) so this can never lose a specificity tie to source order again. */
21
+ & .crisp-table th { padding: 0; }
15
22
  /* Same wrapper trap as AUTHORING.md Lesson 40 (ColumnCalc's summary trigger):
16
23
  Menu wraps its trigger in a flex div that sizes to its content, so a
17
24
  long header grew past the column instead of filling it -- with nothing
@@ -20,6 +27,15 @@
20
27
  & th .crisp-menu { width: 100%; height: 100%; }
21
28
  & .crisp-datatable-th-inner { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); width: 100%; min-width: 0; height: 100%; }
22
29
  & .crisp-datatable-th-inner > svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
30
+ /* crisp/#605 follow-up: a header with neither a Menu nor a sort button (the primary/entity column's now-inert path, or any column with no
31
+ columnMenu/sortable at all) renders .crisp-datatable-th-inner as a
32
+ DIRECT child of th instead of nested inside a button -- and loses
33
+ the button's own 12px inset, drifting left of the td's 12px base
34
+ inset. Measured live on Attio's own Companies grid: the header label and the cell's own text land at the exact same x (335px from the
35
+ viewport, both 40px from the column's left edge) via two different padding+icon+gap combinations -- Attio does not leave the inert primary header flush left either. The `>` combinator targets only the button-less case: Menu's own wrapper (`.crisp-menu`) and the
36
+ plain sort `button` both put >=1 more level between `th` and this
37
+ span, so their own 12px (already on the button) isn't doubled. */
38
+ & th > .crisp-datatable-th-inner { box-sizing: border-box; padding: 0 var(--crisp-space-3); }
23
39
  /* The summary cell has NO padding of its own -- the calculation trigger owns the whole 246x37 area so a click anywhere in the cell opens the menu. Zero the cell's inset and let ColumnCalc supply the 12px. */
24
40
  & tfoot td { padding: 0; }
25
41
  /* Group heading row -- a view is grouped under a subtle label with the
@@ -90,8 +106,23 @@
90
106
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
91
107
  & .crisp-datatable-refoption--create { color: var(--crisp-fg-muted); }
92
108
  & .crisp-datatable-empty { padding: var(--crisp-space-8) var(--crisp-space-3); text-align: center; color: var(--crisp-fg-muted); font-size: var(--crisp-text-lg); }
109
+ /* #579: click-to-expand overlay for a truncated cell -- a read-only,
110
+ full-value card, settled live as click (not Attio's usual hover-expand) and measured position: absolute, unlike this file's
111
+ floating chrome (Menu/Select popovers) which portals and escapes the
112
+ scroll container. Deliberately NOT the edit-error pattern above
113
+ (grow the row, block-level, never clipped): a virtualized grid
114
+ assumes a fixed row height for its scroll math (owned by the grid
115
+ core, not this cell), so growing a row here would fight it. Anchored
116
+ to the SAME column width the cell itself renders at (measured:
117
+ Attio's own overlay came out only slightly wider than the triggering cell's clientWidth -- the overshoot is this padding),
118
+ wrapping onto as many lines as the value needs; it CAN be clipped by
119
+ `.crisp-table-scroll`'s own `overflow: auto` if it grows past the visible viewport, same as the edit-error comment above already notes for any position:absolute escape -- unmeasured against Attio for this exact case, flagged rather than guessed. */
120
+ & td[data-cell-expanded] { overflow: visible; z-index: 7; }
121
+ & .crisp-datatable-cell-overlay { position: absolute; inset-inline-start: 0; top: 0; width: 100%; box-sizing: border-box; padding: var(--crisp-space-2) var(--crisp-space-3); background: var(--crisp-bg-surface); border: 1px solid var(--crisp-bg-brand-solid); border-radius: 4px; font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); white-space: normal; word-break: break-word; cursor: default; }
93
122
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
94
123
  & table { table-layout: fixed; }
124
+ /* read-only/computed column marker (#596) -- sits just inside the resize handle, same absolute-positioned-against-the-th approach the handle itself uses, since th has zero padding and the button/inner row fills the whole cell with no room left for a trailing sibling. */
125
+ & .crisp-datatable-readonly { position: absolute; top: 50%; inset-inline-end: 11px; transform: translateY(-50%); color: var(--crisp-fg-muted); pointer-events: none; z-index: 5; }
95
126
  /* column resize grip */
96
127
  & .crisp-datatable-resize { position: absolute; top: 0; inset-inline-end: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
97
128
  & .crisp-datatable-resize::after { content: ""; position: absolute; top: 0; inset-inline-start: 3px; width: 2px; height: 100%; background: var(--crisp-bg-brand-solid); opacity: 0; transition: opacity .12s; }
@@ -102,7 +133,12 @@
102
133
  & .crisp-datatable-entity:hover .crisp-datatable-entity-name { text-decoration: underline; }
103
134
  & button.crisp-datatable-entity-name--link { margin: 0; padding: 0; border: none; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-align: start; cursor: pointer; max-width: 100%; }
104
135
  & .crisp-datatable-avatar { width: var(--crisp-size-icon); height: var(--crisp-size-icon); border-radius: var(--crisp-radius-sm); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: var(--crisp-weight-semibold); text-transform: uppercase; color: var(--crisp-fg-on-brand); background: var(--crisp-bg-brand-solid); }
105
- & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); } & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); } & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); } & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); } & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); } & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
136
+ & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
137
+ & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
138
+ & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); }
139
+ & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); }
140
+ & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); }
141
+ & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
106
142
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
107
143
  & .crisp-datatable-tags { display: flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
108
144
  & .crisp-datatable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
@@ -115,14 +151,16 @@
115
151
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
116
152
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
117
153
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
118
- /* url/email cell types (#654) -- same measured values as RecordsTable's own .crisp-recordstable-link, which this mirrors: no underline at rest, underline on hover, own-colour text rather than a browser-blue link. */
154
+ /* url/email cell types (#654) -- same measured values as RecordsTable's
155
+ own .crisp-recordstable-link, which this mirrors: no underline at
156
+ rest, underline on hover, own-colour text rather than a browser-blue
157
+ link. */
119
158
  & .crisp-datatable-link { color: var(--crisp-fg-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
120
159
  & .crisp-datatable-link:hover { text-decoration: underline; }
121
- /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
122
- hairline doubling as the drop indicator (it tints toward the brand
123
- colour as the active drop target). The affordance + geometry are
124
- measured; the drag MECHANISM is crisp's existing house idiom -- HTML5 draggable + dataTransfer, the same one column reorder already uses (RecordsTable's reorderColumn) -- not a proprietary
125
- pointer-events implementation. */
160
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
161
+ a leading 20px grip column, a 16px grip icon, and the row's own hairline doubling as the drop indicator (it tints toward the brand colour as the active drop target). The affordance + geometry are measured; the drag MECHANISM is crisp's existing house idiom -- HTML5
162
+ draggable + dataTransfer, the same one column reorder already uses
163
+ (RecordsTable's reorderColumn) -- not a proprietary pointer-events implementation. */
126
164
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
127
165
  & .crisp-datatable-grip { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; margin: 0; border: none; border-radius: 0; background: none; font-family: inherit; color: var(--crisp-fg-muted); cursor: grab; }
128
166
  & .crisp-datatable-grip:active { cursor: grabbing; }
package/dist/dialog.css CHANGED
@@ -1,16 +1,28 @@
1
1
  .crisp-dialog {
2
2
  display: contents; font-family: var(--crisp-font-sans);
3
- & .crisp-dialog-backdrop { position: fixed; inset: 0; z-index: 60; background: transparent; }
3
+ & .crisp-dialog-backdrop { position: fixed; inset: 0; z-index: 60; background: var(--crisp-bg-scrim); }
4
4
  & .crisp-dialog-positioner { position: fixed; inset: 0; z-index: 61; display: flex; align-items: flex-start; justify-content: center; padding: 15vh var(--crisp-space-4) var(--crisp-space-4); }
5
+ & .crisp-dialog-positioner[data-variant="alert"] { align-items: center; padding: var(--crisp-space-4); }
5
6
  & .crisp-dialog-content { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: relative; display: flex; flex-direction: column; box-sizing: border-box; width: 90vw; max-height: 74vh; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-2xl); background: color-mix(in srgb, var(--crisp-bg-overlay-frost) 60%, transparent); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); box-shadow: 0 8px 28px -6px var(--crisp-shadow-overlay1), 0 18px 88px -4px var(--crisp-shadow-overlay2), 0 0 1px var(--crisp-shadow-overlay3); opacity: 1; transform: scale(1); transition: opacity 0.2s ease, transform 0.2s ease; @starting-style { opacity: 0; transform: scale(0.97); } }
7
+ & .crisp-dialog-content[data-variant="alert"] { padding: 0; background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; box-shadow: none; }
6
8
  & .crisp-dialog-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); overflow: hidden; }
9
+ & .crisp-dialog-content[data-variant="alert"] .crisp-dialog-card { border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); }
7
10
  & .crisp-dialog-header { display: flex; align-items: center; gap: var(--crisp-space-2); flex-shrink: 0; height: 52px; padding: 0 var(--crisp-space-2) 0 var(--crisp-space-4); border-bottom: 1px solid var(--crisp-border-subtle); }
8
11
  & .crisp-dialog-header svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
9
12
  & .crisp-dialog-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); margin: 0; }
10
13
  & .crisp-dialog-close { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; }
11
14
  & .crisp-dialog-close:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
15
+ /* The discard-confirm's own stacked title + sentence (crisp#677) — no
16
+ border, no close button, unlike the panel header above: measured
17
+ live, both themes, 18px/24px 600-weight title (no scale token
18
+ reaches 18px — the scale tops out at text.xl's 16px, so this is raw px per Golden Rule 3's "genuinely intrinsic size" exception, same as
19
+ this file's own submit-key glyphs) then a 14px/20px 500-weight sentence 4px below it. */
20
+ & .crisp-dialog-alert-header { display: flex; flex-direction: column; gap: var(--crisp-space-1); padding: var(--crisp-space-4) var(--crisp-space-4) 0; }
21
+ & .crisp-dialog-alert-title { margin: 0; font-size: 18px; line-height: 24px; font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-primary); }
22
+ & .crisp-dialog-alert-description { margin: 0; font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-alert-description); }
12
23
  & .crisp-dialog-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--crisp-space-4); padding: var(--crisp-space-5) var(--crisp-space-4); }
13
24
  & .crisp-dialog-footer { display: flex; align-items: center; justify-content: flex-end; gap: var(--crisp-space-3); flex-shrink: 0; min-height: 56px; padding: 0 var(--crisp-space-4); border-top: 1px solid var(--crisp-border-subtle); }
25
+ & .crisp-dialog-footer[data-variant="alert"] { min-height: 45px; padding: var(--crisp-space-2); }
14
26
  & .crisp-dialog-field { display: flex; flex-direction: column; gap: var(--crisp-space-1-5); }
15
27
  & .crisp-dialog-field-label { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 50%, transparent); }
16
28
  & .crisp-dialog-more { display: inline-flex; align-items: center; gap: var(--crisp-space-2); margin-inline-end: auto; border: none; background: none; padding: 0; font-family: inherit; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); cursor: pointer; }
@@ -1,17 +1,29 @@
1
1
  @layer crisp-components {
2
2
  .crisp-dialog {
3
3
  display: contents; font-family: var(--crisp-font-sans);
4
- & .crisp-dialog-backdrop { position: fixed; inset: 0; z-index: 60; background: transparent; }
4
+ & .crisp-dialog-backdrop { position: fixed; inset: 0; z-index: 60; background: var(--crisp-bg-scrim); }
5
5
  & .crisp-dialog-positioner { position: fixed; inset: 0; z-index: 61; display: flex; align-items: flex-start; justify-content: center; padding: 15vh var(--crisp-space-4) var(--crisp-space-4); }
6
+ & .crisp-dialog-positioner[data-variant="alert"] { align-items: center; padding: var(--crisp-space-4); }
6
7
  & .crisp-dialog-content { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: relative; display: flex; flex-direction: column; box-sizing: border-box; width: 90vw; max-height: 74vh; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-2xl); background: color-mix(in srgb, var(--crisp-bg-overlay-frost) 60%, transparent); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); box-shadow: 0 8px 28px -6px var(--crisp-shadow-overlay1), 0 18px 88px -4px var(--crisp-shadow-overlay2), 0 0 1px var(--crisp-shadow-overlay3); opacity: 1; transform: scale(1); transition: opacity 0.2s ease, transform 0.2s ease; @starting-style { opacity: 0; transform: scale(0.97); } }
8
+ & .crisp-dialog-content[data-variant="alert"] { padding: 0; background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; box-shadow: none; }
7
9
  & .crisp-dialog-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); overflow: hidden; }
10
+ & .crisp-dialog-content[data-variant="alert"] .crisp-dialog-card { border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); }
8
11
  & .crisp-dialog-header { display: flex; align-items: center; gap: var(--crisp-space-2); flex-shrink: 0; height: 52px; padding: 0 var(--crisp-space-2) 0 var(--crisp-space-4); border-bottom: 1px solid var(--crisp-border-subtle); }
9
12
  & .crisp-dialog-header svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
10
13
  & .crisp-dialog-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); margin: 0; }
11
14
  & .crisp-dialog-close { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; }
12
15
  & .crisp-dialog-close:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
16
+ /* The discard-confirm's own stacked title + sentence (crisp#677) — no
17
+ border, no close button, unlike the panel header above: measured
18
+ live, both themes, 18px/24px 600-weight title (no scale token
19
+ reaches 18px — the scale tops out at text.xl's 16px, so this is raw px per Golden Rule 3's "genuinely intrinsic size" exception, same as
20
+ this file's own submit-key glyphs) then a 14px/20px 500-weight sentence 4px below it. */
21
+ & .crisp-dialog-alert-header { display: flex; flex-direction: column; gap: var(--crisp-space-1); padding: var(--crisp-space-4) var(--crisp-space-4) 0; }
22
+ & .crisp-dialog-alert-title { margin: 0; font-size: 18px; line-height: 24px; font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-primary); }
23
+ & .crisp-dialog-alert-description { margin: 0; font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-alert-description); }
13
24
  & .crisp-dialog-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--crisp-space-4); padding: var(--crisp-space-5) var(--crisp-space-4); }
14
25
  & .crisp-dialog-footer { display: flex; align-items: center; justify-content: flex-end; gap: var(--crisp-space-3); flex-shrink: 0; min-height: 56px; padding: 0 var(--crisp-space-4); border-top: 1px solid var(--crisp-border-subtle); }
26
+ & .crisp-dialog-footer[data-variant="alert"] { min-height: 45px; padding: var(--crisp-space-2); }
15
27
  & .crisp-dialog-field { display: flex; flex-direction: column; gap: var(--crisp-space-1-5); }
16
28
  & .crisp-dialog-field-label { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 50%, transparent); }
17
29
  & .crisp-dialog-more { display: inline-flex; align-items: center; gap: var(--crisp-space-2); margin-inline-end: auto; border: none; background: none; padding: 0; font-family: inherit; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); cursor: pointer; }
@@ -11,7 +11,11 @@ const spec = defineSpec({
11
11
  gap: c.space["1"],
12
12
  fontFamily: c.font.sans,
13
13
  fontWeight: c.weight.medium,
14
- letterSpacing: "-0.1px",
14
+ // crisp#696: every Attio pill measured this round -- the count pill and
15
+ // the tag pill both -- reads `normal` tracking, not the `-0.1px` this
16
+ // used to carry as a base default. `large` (unmeasured this round) keeps
17
+ // the old `-0.1px` as its own variant override, below.
18
+ letterSpacing: "normal",
15
19
  whiteSpace: "nowrap",
16
20
  },
17
21
  parts: [
@@ -38,7 +42,12 @@ const spec = defineSpec({
38
42
  neutral: {
39
43
  background: c.bgRaised,
40
44
  color: c.fgNeutralSoft,
41
- boxShadow: `inset 0 0 0 1px ${c.borderDefault}`,
45
+ // crisp#696: Attio's neutral pill ring measures rgb(238,239,241) --
46
+ // borderSubtle/line100 (#eeeff1), not borderDefault/line200
47
+ // (#e4e4e7). Background is unchanged: Attio measured
48
+ // rgb(246,247,247) against crisp's bgRaised #f4f4f5, a 2-unit
49
+ // difference the `savedviews` recipe already treats as noise.
50
+ boxShadow: `inset 0 0 0 1px ${c.borderSubtle}`,
42
51
  },
43
52
  brand: {
44
53
  background: soft(c.bgBrandSolid, 14),
@@ -72,22 +81,36 @@ const spec = defineSpec({
72
81
  },
73
82
  },
74
83
  size: {
84
+ // crisp#696: Attio's count pill (sidebar "Tasks 1", the "+1" overflow)
85
+ // measures 16px h / 4px padding / 12px text / 16px line-height -- was
86
+ // 6px padding, 11px text, and no line-height at all.
75
87
  small: {
76
88
  height: "16px",
77
- paddingInline: c.space["1.5"],
78
- fontSize: c.text.xs,
89
+ paddingInline: c.space["1"],
90
+ fontSize: c.text.sm,
91
+ lineHeight: "16px",
79
92
  borderRadius: c.radius.sm,
80
93
  },
94
+ // crisp#696: Attio's tag pill (Standard/System/Enriched) measures
95
+ // 20px h / 4px padding / 12px text / 16px line-height / 3px gap -- was
96
+ // 8px padding, no line-height, and the base's (now normal-tracking)
97
+ // 4px gap.
81
98
  medium: {
82
99
  height: "20px",
83
- paddingInline: c.space["2"],
100
+ paddingInline: c.space["1"],
84
101
  fontSize: c.text.sm,
102
+ lineHeight: "16px",
103
+ gap: "3px",
85
104
  borderRadius: c.radius.sm,
86
105
  },
106
+ // crisp#696: not re-measured this round -- kept exactly as it rendered
107
+ // before the base tracking changed to `normal`, by pinning the old
108
+ // `-0.1px` here instead. Gap is untouched (base's 4px).
87
109
  large: {
88
110
  height: c.size.chip,
89
111
  paddingInline: "9px",
90
112
  fontSize: c.text.lg,
113
+ letterSpacing: "-0.1px",
91
114
  borderRadius: c.radius.chip,
92
115
  },
93
116
  },
@@ -1,4 +1,4 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { Primitive } from "../../core/index.js";
4
4
  import { buttonRecipe } from "./button.recipe.js";
@@ -6,10 +6,41 @@ import { buttonRecipe } from "./button.recipe.js";
6
6
  function Spinner() {
7
7
  return (_jsxs("svg", { width: "1em", height: "1em", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", style: { flexShrink: 0 }, children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5", stroke: "currentColor", strokeOpacity: "0.25", strokeWidth: "2" }), _jsx("path", { d: "M8 1.5a6.5 6.5 0 0 1 6.5 6.5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: _jsx("animateTransform", { attributeName: "transform", type: "rotate", from: "0 8 8", to: "360 8 8", dur: "0.7s", repeatCount: "indefinite" }) })] }));
8
8
  }
9
+ /**
10
+ * crisp#682: an "icon" part is a valid element whose type is the host `svg`
11
+ * tag or isn't a string at all (a lucide icon, `Spinner`, any component) --
12
+ * everything else (strings, numbers, host elements like `span`) is a "text"
13
+ * part.
14
+ */
15
+ function isIconPart(node) {
16
+ return (React.isValidElement(node) &&
17
+ (node.type === "svg" || typeof node.type !== "string"));
18
+ }
9
19
  export const Button = React.forwardRef(({ intent, size, shape, touchTarget, className, asChild, loading, disabled, children, type, ...props }, ref) => {
10
20
  const isDisabled = disabled || loading;
11
21
  // Slot (asChild) requires a single child — never inject the spinner in that mode.
12
- const content = loading && !asChild ? (_jsxs(_Fragment, { children: [_jsx(Spinner, {}), children] })) : (children);
22
+ // An array (not a Fragment) so React.Children.toArray below sees the
23
+ // Spinner as its own top-level part instead of one opaque Fragment node.
24
+ const content = loading && !asChild
25
+ ? [_jsx(Spinner, {}, "crisp-button-spinner"), children]
26
+ : children;
27
+ // crisp#682: md's icon-side padding (6px vs 8px) depends on whether a
28
+ // side ends in an icon or text, which CSS alone can't tell apart
29
+ // (`<svg/>Label` vs `Label<svg/>` both skip text nodes under
30
+ // `:first-child`). Skipped for `asChild`, which renders someone else's
31
+ // single element rather than a list of parts.
32
+ let iconStart;
33
+ let iconEnd;
34
+ if (!asChild) {
35
+ const parts = React.Children.toArray(content);
36
+ const hasText = parts.some((part) => !isIconPart(part));
37
+ if (hasText && parts.length > 0) {
38
+ if (isIconPart(parts[0]))
39
+ iconStart = true;
40
+ if (isIconPart(parts[parts.length - 1]))
41
+ iconEnd = true;
42
+ }
43
+ }
13
44
  return (_jsx(Primitive.button, { ref: ref, asChild: asChild, className: buttonRecipe({
14
45
  intent,
15
46
  size,
@@ -21,6 +52,6 @@ export const Button = React.forwardRef(({ intent, size, shape, touchTarget, clas
21
52
  // a design system should submit a form by accident, so default to
22
53
  // "button" and make submitting explicit. `asChild` renders someone
23
54
  // else's element, which must not be given a button-only attribute.
24
- type: asChild ? type : (type ?? "button"), disabled: isDisabled, "aria-busy": loading || undefined, "data-loading": loading ? "" : undefined, ...props, children: content }));
55
+ type: asChild ? type : (type ?? "button"), disabled: isDisabled, "aria-busy": loading || undefined, "data-loading": loading ? "" : undefined, "data-icon-start": iconStart ? "" : undefined, "data-icon-end": iconEnd ? "" : undefined, ...props, children: content }));
25
56
  });
26
57
  Button.displayName = "Button";
@@ -9,7 +9,11 @@ const spec = defineSpec({
9
9
  gap: c.space["1.5"],
10
10
  fontFamily: c.font.sans,
11
11
  fontWeight: c.weight.medium,
12
- letterSpacing: "-0.14px",
12
+ // crisp#682: every 14px button label on app.attio.com measures -0.07px
13
+ // (17 of 17 on the Companies view, plus 222 sidebar/nav labels).
14
+ // -0.14px is Attio's table-cell tracking, not its button tracking --
15
+ // that's where this value came from.
16
+ letterSpacing: "-0.07px",
13
17
  lineHeight: "20px",
14
18
  whiteSpace: "nowrap",
15
19
  border: "none",
@@ -77,6 +81,9 @@ const spec = defineSpec({
77
81
  },
78
82
  },
79
83
  size: {
84
+ // sm/lg unchanged by crisp#682 -- neither was measured against Attio
85
+ // (Attio's small button is 20px, not sm's 24px; see crisp#682), so
86
+ // only md's icon-side padding is touched here.
80
87
  sm: {
81
88
  height: "24px",
82
89
  paddingInline: c.space["1.5"],
@@ -84,10 +91,29 @@ const spec = defineSpec({
84
91
  borderRadius: c.radius.md,
85
92
  },
86
93
  md: {
87
- height: "28px",
88
- paddingInline: c.space["2"],
89
- fontSize: c.text.lg,
90
- borderRadius: c.radius.md,
94
+ decls: {
95
+ height: "28px",
96
+ paddingInline: c.space["2"],
97
+ fontSize: c.text.lg,
98
+ borderRadius: c.radius.md,
99
+ },
100
+ // crisp#682: a side that ends in an icon gets 6px padding instead of
101
+ // 8px. CSS can't tell `<svg/>Label` apart from `Label<svg/>`
102
+ // (`:first-child` skips text nodes), so `Button` marks the icon
103
+ // sides itself via `data-icon-start`/`data-icon-end` (button.tsx).
104
+ // The attribute selector outranks the plain class above it, so this
105
+ // always wins over the shorthand `paddingInline` regardless of
106
+ // declaration order.
107
+ parts: [
108
+ {
109
+ selector: "&[data-icon-start]",
110
+ decls: { paddingInlineStart: c.space["1.5"] },
111
+ },
112
+ {
113
+ selector: "&[data-icon-end]",
114
+ decls: { paddingInlineEnd: c.space["1.5"] },
115
+ },
116
+ ],
91
117
  },
92
118
  lg: {
93
119
  height: "40px",
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ArrowDown, ArrowUp, Check, ChevronsUpDown, CircleUserRound, GripVertical, Plus, Search, SlidersHorizontal, } from "lucide-react";
2
+ import { ArrowDown, ArrowUp, Check, ChevronsUpDown, CircleUserRound, GripVertical, Lock, Plus, Search, SlidersHorizontal, } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { cellDatePrecision, formatCellDate, isRtlElement, parseCellDate, useCrispMessage, useCrispMessages, } from "../../core/index.js";
5
5
  import { Avatar } from "../avatar/index.js";
@@ -492,6 +492,34 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
492
492
  const showColumn = React.useCallback((key) => applyColumns(allColumns.map((c) => (c.key === key ? { ...c, hidden: false } : c))), [allColumns, applyColumns]);
493
493
  const [selCell, setSelCell] = React.useState(null);
494
494
  const gridRef = React.useRef(null);
495
+ // #579: the click-to-expand overlay for a truncated cell. Only one is ever
496
+ // open (a second click-to-expand elsewhere closes the first, matching "one
497
+ // live session"-style singular UI state already used for `selCell`).
498
+ const [expandedCell, setExpandedCell] = React.useState(null);
499
+ // Closes on any click outside the overlay itself (a click ON it is
500
+ // stopped from propagating, see the overlay's own onClick) and on
501
+ // Escape, kept for accessibility even though Attio's own Escape handling
502
+ // wasn't reliable when this was measured live (crisp#579).
503
+ React.useEffect(() => {
504
+ if (!expandedCell)
505
+ return;
506
+ const close = (e) => {
507
+ const target = e.target;
508
+ if (target?.closest?.(".crisp-datatable-cell-overlay"))
509
+ return;
510
+ setExpandedCell(null);
511
+ };
512
+ const onKey = (e) => {
513
+ if (e.key === "Escape")
514
+ setExpandedCell(null);
515
+ };
516
+ document.addEventListener("click", close, true);
517
+ document.addEventListener("keydown", onKey);
518
+ return () => {
519
+ document.removeEventListener("click", close, true);
520
+ document.removeEventListener("keydown", onKey);
521
+ };
522
+ }, [expandedCell]);
495
523
  const sort = sortProp !== undefined ? sortProp : internalSort;
496
524
  // One shape downstream: [] when unsorted, ordered by precedence otherwise.
497
525
  const sorts = React.useMemo(() => (sort == null ? [] : Array.isArray(sort) ? sort : [sort]), [sort]);
@@ -920,6 +948,10 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
920
948
  function openEditing(id, key) {
921
949
  setEditError(null);
922
950
  setEditing({ id, key });
951
+ // #579: the editing branch pre-empts the plain-cell render (and its
952
+ // overlay) entirely, but `expandedCell` itself would survive a later
953
+ // cancel-back-to-plain-view unless cleared here too.
954
+ setExpandedCell(null);
923
955
  }
924
956
  // Async-aware (#385): a sync `onEdit` (returns undefined) commits exactly
925
957
  // as before -- close the editor immediately. A Promise-returning `onEdit`
@@ -1175,7 +1207,17 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
1175
1207
  // aria-sort is specified as one header per table, and with the
1176
1208
  // multi-sort the `sort` prop allows, a second "ascending" says
1177
1209
  // nothing about which column came first.
1178
- const sortable = col.sortable || !!columnMenu;
1210
+ // crisp/#605: the primary/entity column is inert -- no menu,
1211
+ // no click-to-sort, no aria-sort claim -- matching Attio's
1212
+ // own measured behaviour exactly ("no popover opens, no
1213
+ // right-click menu either... primary column is fixed by
1214
+ // construction, not a menu target", checklist G-023). A
1215
+ // consumer setting `sortable`/`columnMenu` on an entity
1216
+ // column doesn't get a sort affordance there; this is
1217
+ // `!isPrimary` gating every interactive branch below, not
1218
+ // just the menu.
1219
+ const isPrimary = col.type === "entity";
1220
+ const sortable = !isPrimary && (col.sortable || !!columnMenu);
1179
1221
  const ariaSort = !sortable
1180
1222
  ? undefined
1181
1223
  : sorts[0]?.key === col.key
@@ -1196,9 +1238,9 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
1196
1238
  }
1197
1239
  else if (e.key === "Escape")
1198
1240
  setRenaming(null);
1199
- } })) : columnMenu ? (_jsxs(Menu, { align: "start", trigger: _jsx("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), "aria-label": `${typeof col.header === "string"
1241
+ } })) : columnMenu && !isPrimary ? (_jsxs(Menu, { align: "start", trigger: _jsx("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), "aria-label": `${typeof col.header === "string"
1200
1242
  ? col.header
1201
- : col.key} ${crispMessages["column options"] ?? "column options"}`, children: inner }), children: [_jsx(MenuItem, { onSelect: () => setSort({ key: col.key, dir: "asc" }), children: sortAscendingLabel }), _jsx(MenuItem, { onSelect: () => setSort({ key: col.key, dir: "desc" }), children: sortDescendingLabel }), col.managed === false ? null : (_jsxs(_Fragment, { children: [_jsx(MenuItem, { onSelect: () => moveColumn(col.key, -1), children: moveColumnLeftLabel }), _jsx(MenuItem, { onSelect: () => moveColumn(col.key, 1), children: moveColumnRightLabel }), _jsx(MenuItem, { onSelect: () => setRenaming(col.key), children: editColumnLabel }), _jsx(MenuItem, { onSelect: () => hideColumn(col.key), children: hideColumnLabel })] }))] })) : col.sortable ? (_jsx("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), onClick: () => cycleSort(col.key), children: inner })) : (inner), col.resizable !== false && (_jsx("span", { className: "crisp-datatable-resize", "data-resizing": resizingKey === col.key ? "" : undefined, onPointerDown: (e) => startResize(col, e), onPointerMove: moveResize, onPointerUp: endResize }))] }, col.key));
1243
+ : col.key} ${crispMessages["column options"] ?? "column options"}`, children: inner }), children: [_jsx(MenuItem, { onSelect: () => setSort({ key: col.key, dir: "asc" }), children: sortAscendingLabel }), _jsx(MenuItem, { onSelect: () => setSort({ key: col.key, dir: "desc" }), children: sortDescendingLabel }), col.managed === false ? null : (_jsxs(_Fragment, { children: [_jsx(MenuItem, { onSelect: () => moveColumn(col.key, -1), children: moveColumnLeftLabel }), _jsx(MenuItem, { onSelect: () => moveColumn(col.key, 1), children: moveColumnRightLabel }), _jsx(MenuItem, { onSelect: () => setRenaming(col.key), children: editColumnLabel }), _jsx(MenuItem, { onSelect: () => hideColumn(col.key), children: hideColumnLabel })] }))] })) : col.sortable && !isPrimary ? (_jsx("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), onClick: () => cycleSort(col.key), children: inner })) : (inner), col.readOnly && (_jsx(Lock, { className: "crisp-datatable-readonly", size: 12, strokeWidth: 1.75, "aria-hidden": "true" })), col.resizable !== false && (_jsx("span", { className: "crisp-datatable-resize", "data-resizing": resizingKey === col.key ? "" : undefined, onPointerDown: (e) => startResize(col, e), onPointerMove: moveResize, onPointerUp: endResize }))] }, col.key));
1202
1244
  })] }) }), _jsx(TableBody, { children: error ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: totalCols, children: _jsx("div", { className: "crisp-datatable-empty", children: error }) }) })) : rows.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: totalCols, children: _jsx("div", { className: "crisp-datatable-empty", children: empty }) }) })) : (_jsxs(_Fragment, { children: [topSpacerHeight > 0 && (_jsx("tr", { "aria-hidden": "true", children: _jsx("td", { colSpan: totalCols, style: {
1203
1245
  height: topSpacerHeight,
1204
1246
  padding: 0,
@@ -1300,22 +1342,39 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
1300
1342
  commitEdit(row.id, col.key, e.currentTarget.value);
1301
1343
  } }), isPending && (_jsx(Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }, col.key));
1302
1344
  }
1345
+ const rendered = col.render
1346
+ ? col.render(value, row)
1347
+ : renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale);
1348
+ const isExpanded = expandedCell?.id === row.id &&
1349
+ expandedCell?.key === col.key;
1303
1350
  return (_jsxs(TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: col.editable
1304
1351
  ? "crisp-datatable-editable"
1305
1352
  : undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
1306
1353
  ? ""
1307
- : undefined, onClick: () => {
1354
+ : undefined, "data-cell-expanded": isExpanded ? "" : undefined, onClick: (e) => {
1308
1355
  // the keyboard model's anchor -- a single click
1309
1356
  // only selects. A genuine double-click (or Enter
1310
1357
  // on the selected cell) opens the editor (#411),
1311
1358
  // matching RecordsTable's own onDoubleClick.
1312
1359
  setSelCell({ id: row.id, key: col.key });
1313
1360
  gridRef.current?.focus({ preventScroll: true });
1361
+ // #579: a truncated cell also opens a read-only
1362
+ // overlay with its full value -- settled live
1363
+ // that this is click, not Attio's usual
1364
+ // hover-expand (a 1s hover on a truncated
1365
+ // Description cell produced nothing). Measured
1366
+ // the same way as `ThLabel`'s header tooltip
1367
+ // (#534): scrollWidth vs clientWidth on the
1368
+ // element the CSS ellipsis actually applies to,
1369
+ // which for a data cell is the `td` itself
1370
+ // (table.recipe.ts's own `overflow: hidden;
1371
+ // text-overflow: ellipsis`), not a nested span.
1372
+ setExpandedCell(isLabelTruncated(e.currentTarget)
1373
+ ? { id: row.id, key: col.key }
1374
+ : null);
1314
1375
  }, onDoubleClick: col.editable
1315
1376
  ? () => openEditing(row.id, col.key)
1316
- : undefined, children: [col.render
1317
- ? col.render(value, row)
1318
- : renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
1377
+ : undefined, children: [!isExpanded && rendered, errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere })), isExpanded && (_jsx("div", { className: "crisp-datatable-cell-overlay", onClick: (e) => e.stopPropagation(), children: rendered }))] }, col.key));
1319
1378
  })] }, row.id));
1320
1379
  }), bottomSpacerHeight > 0 && (_jsx("tr", { "aria-hidden": "true", children: _jsx("td", { colSpan: totalCols, style: {
1321
1380
  height: bottomSpacerHeight,