@crisp-ui-kit/crisp 0.54.7 → 0.54.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/authshell.css +5 -1
  2. package/dist/authshell.layered.css +5 -1
  3. package/dist/badge.css +5 -5
  4. package/dist/badge.layered.css +5 -5
  5. package/dist/board.css +36 -8
  6. package/dist/board.layered.css +36 -8
  7. package/dist/button.css +3 -1
  8. package/dist/button.layered.css +3 -1
  9. package/dist/cjs/components/badge/badge.recipe.js +28 -5
  10. package/dist/cjs/components/button/button.js +33 -2
  11. package/dist/cjs/components/button/button.recipe.js +31 -5
  12. package/dist/cjs/components/contextmenu/contextmenu.js +2 -2
  13. package/dist/cjs/components/contextmenu/contextmenu.recipe.js +24 -0
  14. package/dist/cjs/components/datatable/datatable.js +590 -41
  15. package/dist/cjs/components/datatable/datatable.recipe.js +118 -5
  16. package/dist/cjs/components/dialog/dialog.js +72 -5
  17. package/dist/cjs/components/dialog/dialog.recipe.js +15 -1
  18. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +49 -5
  19. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +365 -49
  20. package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +78 -0
  21. package/dist/cjs/components/tabs/tabs.recipe.js +11 -2
  22. package/dist/cjs/components/tooltip/tooltip.js +23 -9
  23. package/dist/cjs/components/tooltip/tooltip.recipe.js +10 -9
  24. package/dist/cjs/product/authshell/authshell.js +14 -1
  25. package/dist/cjs/product/authshell/authshell.recipe.js +13 -1
  26. package/dist/cjs/product/board/board.js +175 -46
  27. package/dist/cjs/product/board/board.recipe.js +56 -4
  28. package/dist/cjs/product/comments/comments.js +34 -12
  29. package/dist/cjs/product/comments/comments.recipe.js +31 -7
  30. package/dist/cjs/product/formdialog/formdialog.js +10 -4
  31. package/dist/cjs/product/navitem/navitem.js +7 -2
  32. package/dist/cjs/product/navitem/navitem.recipe.js +16 -0
  33. package/dist/cjs/product/objectsettings/index.js +1 -2
  34. package/dist/cjs/product/objectsettings/objectsettings.js +3 -13
  35. package/dist/cjs/product/recordstable/recordstable.js +1 -17
  36. package/dist/cjs/product/richtext/richtext.js +76 -1
  37. package/dist/cjs/product/savedviews/savedviews.js +55 -24
  38. package/dist/cjs/product/savedviews/savedviews.recipe.js +27 -0
  39. package/dist/cjs/product/sidebar/sidebar.js +68 -15
  40. package/dist/cjs/product/sidebar/sidebar.recipe.js +6 -1
  41. package/dist/cjs/tokens/palette.js +32 -1
  42. package/dist/cjs/tokens/semantic.js +11 -0
  43. package/dist/comments.css +50 -14
  44. package/dist/comments.layered.css +50 -14
  45. package/dist/contextmenu.css +24 -0
  46. package/dist/contextmenu.layered.css +24 -0
  47. package/dist/datatable.css +101 -37
  48. package/dist/datatable.layered.css +101 -37
  49. package/dist/dialog.css +13 -1
  50. package/dist/dialog.layered.css +13 -1
  51. package/dist/esm/components/badge/badge.recipe.js +28 -5
  52. package/dist/esm/components/button/button.js +34 -3
  53. package/dist/esm/components/button/button.recipe.js +31 -5
  54. package/dist/esm/components/contextmenu/contextmenu.js +2 -2
  55. package/dist/esm/components/contextmenu/contextmenu.recipe.js +24 -0
  56. package/dist/esm/components/datatable/datatable.js +590 -42
  57. package/dist/esm/components/datatable/datatable.recipe.js +119 -6
  58. package/dist/esm/components/dialog/dialog.js +72 -5
  59. package/dist/esm/components/dialog/dialog.recipe.js +15 -1
  60. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +49 -5
  61. package/dist/esm/components/tabletoolbar/tabletoolbar.js +366 -50
  62. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +80 -2
  63. package/dist/esm/components/tabs/tabs.recipe.js +11 -2
  64. package/dist/esm/components/tooltip/tooltip.js +23 -9
  65. package/dist/esm/components/tooltip/tooltip.recipe.js +10 -9
  66. package/dist/esm/product/authshell/authshell.js +15 -2
  67. package/dist/esm/product/authshell/authshell.recipe.js +13 -1
  68. package/dist/esm/product/authshell/index.js +1 -1
  69. package/dist/esm/product/board/board.js +176 -47
  70. package/dist/esm/product/board/board.recipe.js +56 -4
  71. package/dist/esm/product/comments/comments.js +35 -13
  72. package/dist/esm/product/comments/comments.recipe.js +31 -7
  73. package/dist/esm/product/formdialog/formdialog.js +10 -4
  74. package/dist/esm/product/navitem/index.js +1 -1
  75. package/dist/esm/product/navitem/navitem.js +7 -2
  76. package/dist/esm/product/navitem/navitem.recipe.js +16 -0
  77. package/dist/esm/product/objectsettings/index.js +1 -1
  78. package/dist/esm/product/objectsettings/objectsettings.js +2 -12
  79. package/dist/esm/product/recordstable/recordstable.js +2 -18
  80. package/dist/esm/product/richtext/richtext.js +76 -1
  81. package/dist/esm/product/savedviews/savedviews.js +57 -26
  82. package/dist/esm/product/savedviews/savedviews.recipe.js +27 -0
  83. package/dist/esm/product/sidebar/sidebar.js +69 -16
  84. package/dist/esm/product/sidebar/sidebar.recipe.js +6 -1
  85. package/dist/esm/tokens/palette.js +32 -1
  86. package/dist/esm/tokens/semantic.js +11 -0
  87. package/dist/native/tokens.d.ts +1 -1
  88. package/dist/native/tokens.js +16 -2
  89. package/dist/navitem.css +7 -0
  90. package/dist/navitem.layered.css +7 -0
  91. package/dist/recordstable.css +34 -32
  92. package/dist/recordstable.layered.css +34 -32
  93. package/dist/savedviews.css +27 -0
  94. package/dist/savedviews.layered.css +27 -0
  95. package/dist/sidebar.css +6 -1
  96. package/dist/sidebar.layered.css +6 -1
  97. package/dist/styles.css +369 -106
  98. package/dist/styles.layered.css +369 -106
  99. package/dist/tabletoolbar.css +47 -3
  100. package/dist/tabletoolbar.layered.css +47 -3
  101. package/dist/tabs.css +1 -1
  102. package/dist/tabs.layered.css +1 -1
  103. package/dist/tokens.css +9 -1
  104. package/dist/tokens.json +16 -2
  105. package/dist/tooltip.css +1 -1
  106. package/dist/tooltip.layered.css +1 -1
  107. package/dist/types/components/badge/badge.recipe.d.ts +5 -1
  108. package/dist/types/components/button/button.recipe.d.ts +18 -5
  109. package/dist/types/components/contextmenu/contextmenu.d.ts +8 -1
  110. package/dist/types/components/datatable/datatable.d.ts +98 -1
  111. package/dist/types/components/dialog/dialog.d.ts +25 -2
  112. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +113 -3
  113. package/dist/types/components/tooltip/tooltip.d.ts +12 -4
  114. package/dist/types/components/tooltip/tooltip.recipe.d.ts +1 -1
  115. package/dist/types/product/authshell/authshell.d.ts +26 -2
  116. package/dist/types/product/authshell/index.d.ts +1 -1
  117. package/dist/types/product/board/board.d.ts +53 -6
  118. package/dist/types/product/comments/comments.d.ts +16 -3
  119. package/dist/types/product/formdialog/formdialog.d.ts +2 -1
  120. package/dist/types/product/navitem/index.d.ts +1 -1
  121. package/dist/types/product/navitem/navitem.d.ts +23 -0
  122. package/dist/types/product/objectsettings/index.d.ts +1 -1
  123. package/dist/types/product/objectsettings/objectsettings.d.ts +0 -10
  124. package/dist/types/product/recordstable/recordstable.d.ts +0 -1
  125. package/dist/types/product/savedviews/savedviews.d.ts +45 -8
  126. package/dist/types/product/sidebar/sidebar.d.ts +33 -5
  127. package/dist/types/tokens/contract.d.ts +4 -0
  128. package/dist/types/tokens/palette.d.ts +13 -1
  129. package/dist/types/tokens/semantic.d.ts +11 -0
  130. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { defineRecipe, resolveRecipe } from "../../core/index.js";
1
+ import { defineRecipe, resolveRecipe, rtlFlipCss } from "../../core/index.js";
2
2
  import { contract as c, elevation as e } from "../../tokens/index.js";
3
3
  // The feature-rich, config-driven data grid (sort / filter / selection /
4
4
  // inline edit) built on the composable `Table` primitives. This recipe adds the
@@ -19,10 +19,46 @@ const config = defineRecipe({
19
19
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
20
20
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
21
21
  }
22
+ /* crisp/#645: visually-hidden-but-focusable, not visually-hidden-and-
23
+ inert like .crisp-datatable-live above -- same clip-rect shape,
24
+ this one just needs to actually hold DOM focus so a native
25
+ copy/paste event has something to fire on (see its own comment
26
+ in datatable.tsx). resize: none keeps a stray native textarea handle
27
+ from ever being drawable, even though it's clipped to 1px anyway. */
28
+ & .crisp-datatable-clipboard-proxy {
29
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
30
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
31
+ resize: none;
32
+ }
33
+ /* single active cell (crisp/#645 follow-up -- this had no styling of its
34
+ own at all before, a gap against Golden Rule 0: RecordsTable's own
35
+ measured value, .crisp-recordstable-td[data-selected], brought
36
+ across unchanged now that a visible anchor actually matters for the
37
+ Shift+Click range below). */
38
+ & td[data-selected] { outline: 2px solid ${c.bgBrandSolid}; outline-offset: -2px; border-radius: ${c.radius.sm}; z-index: 3; }
39
+ /* crisp/#645, G-101: a Shift+Click range -- reuses the row-selection
40
+ tint formula already used twice below (tbody tr[data-selected] td)
41
+ for the same reason a selected group of rows gets one: a selected
42
+ GROUP of cells is the same kind of state, just cell-scoped instead of
43
+ row-scoped. Not independently re-measured against Attio's own range
44
+ highlight (out of scope this slice -- flagged on crisp#645). */
45
+ & td[data-in-range] { background: color-mix(in srgb, ${c.bgBrandSolid} 10%, ${c.bgSurface}); }
22
46
  /* The header cell has no padding of its own -- the button owns the
23
47
  full cell so the whole header is clickable (its own inset is 12px).
24
- Mirrors the summary-row fix. */
25
- & th { padding: 0; }
48
+ Mirrors the summary-row fix.
49
+ \`& .crisp-table th\`, not bare \`& th\` (found while re-checking #691's
50
+ own fix live): DataTable's root nests \`Table\`'s own wrapper, whose
51
+ \`table.recipe.ts\` carries \`& th, & td { padding: 0 12px }\` at the
52
+ SAME specificity (.crisp-table th vs .crisp-datatable th, both one
53
+ class + one element) -- a bare \`& th\` here only won by source
54
+ order, which flipped the moment table.recipe.ts's own CSS file
55
+ happened to land after datatable's in the build (alphabetical:
56
+ datatable < table), silently reinstating 12px on every header cell
57
+ in the whole product and making every interactive column's button
58
+ double it to 24px. Scoping through \`.crisp-table\` adds a second
59
+ class to the selector (0,2,1 beats 0,1,1) so this can never lose a
60
+ specificity tie to source order again. */
61
+ & .crisp-table th { padding: 0; }
26
62
  /* Same wrapper trap as AUTHORING.md Lesson 40 (ColumnCalc's summary trigger):
27
63
  Menu wraps its trigger in a flex div that sizes to its content, so a
28
64
  long header grew past the column instead of filling it -- with nothing
@@ -33,6 +69,20 @@ const config = defineRecipe({
33
69
  & th .crisp-menu { width: 100%; height: 100%; }
34
70
  & .crisp-datatable-th-inner { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; width: 100%; min-width: 0; height: 100%; }
35
71
  & .crisp-datatable-th-inner > svg { flex-shrink: 0; color: ${c.fgMuted}; }
72
+ /* crisp/#605 follow-up: a header with neither a Menu nor a sort button
73
+ (the primary/entity column's now-inert path, or any column with no
74
+ columnMenu/sortable at all) renders .crisp-datatable-th-inner as a
75
+ DIRECT child of th instead of nested inside a button -- and loses
76
+ the button's own 12px inset, drifting left of the td's 12px base
77
+ inset. Measured live on Attio's own Companies grid: the header label
78
+ and the cell's own text land at the exact same x (335px from the
79
+ viewport, both 40px from the column's left edge) via two different
80
+ padding+icon+gap combinations -- Attio does not leave the inert
81
+ primary header flush left either. The \`>\` combinator targets only
82
+ the button-less case: Menu's own wrapper (\`.crisp-menu\`) and the
83
+ plain sort \`button\` both put >=1 more level between \`th\` and this
84
+ span, so their own 12px (already on the button) isn't doubled. */
85
+ & th > .crisp-datatable-th-inner { box-sizing: border-box; padding: 0 ${c.space["3"]}; }
36
86
  /* The summary cell has NO padding of its own -- the calculation trigger
37
87
  owns the whole 246x37 area so a click anywhere in the cell opens the
38
88
  menu. Zero the cell's inset and let ColumnCalc supply the 12px. */
@@ -116,17 +166,79 @@ const config = defineRecipe({
116
166
  & .crisp-datatable-refoption-avatar { flex-shrink: 0; }
117
167
  & .crisp-datatable-refoption--create { color: ${c.fgMuted}; }
118
168
  & .crisp-datatable-empty { padding: ${c.space["8"]} ${c.space["3"]}; text-align: center; color: ${c.fgMuted}; font-size: ${c.text.lg}; }
169
+ /* #579: click-to-expand overlay for a truncated cell -- a read-only,
170
+ full-value card, settled live as click (not Attio's usual
171
+ hover-expand) and measured position: absolute, unlike this file's
172
+ floating chrome (Menu/Select popovers) which portals and escapes the
173
+ scroll container. Deliberately NOT the edit-error pattern above
174
+ (grow the row, block-level, never clipped): a virtualized grid
175
+ assumes a fixed row height for its scroll math (owned by the grid
176
+ core, not this cell), so growing a row here would fight it. Anchored
177
+ to the SAME column width the cell itself renders at (measured:
178
+ Attio's own overlay came out only slightly wider than the
179
+ triggering cell's clientWidth -- the overshoot is this padding),
180
+ wrapping onto as many lines as the value needs; it CAN be clipped by
181
+ \`.crisp-table-scroll\`'s own \`overflow: auto\` if it grows past the
182
+ visible viewport, same as the edit-error comment above already
183
+ notes for any position:absolute escape -- unmeasured against Attio
184
+ for this exact case, flagged rather than guessed. */
185
+ & td[data-cell-expanded] { overflow: visible; z-index: 7; }
186
+ & .crisp-datatable-cell-overlay {
187
+ position: absolute; inset-inline-start: 0; top: 0; width: 100%; box-sizing: border-box;
188
+ padding: ${c.space["2"]} ${c.space["3"]}; background: ${c.bgSurface};
189
+ border: 1px solid ${c.bgBrandSolid}; border-radius: 4px;
190
+ font-family: ${c.font.sans}; font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium};
191
+ color: ${c.fgPrimary}; white-space: normal; word-break: break-word; cursor: default;
192
+ }
119
193
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
120
194
  & table { table-layout: fixed; }
195
+ /* read-only/computed column marker (#596) -- sits just inside the
196
+ resize handle, same absolute-positioned-against-the-th approach the
197
+ handle itself uses, since th has zero padding and the button/inner
198
+ row fills the whole cell with no room left for a trailing sibling. */
199
+ & .crisp-datatable-readonly { position: absolute; top: 50%; inset-inline-end: 11px; transform: translateY(-50%); color: ${c.fgMuted}; pointer-events: none; z-index: 5; }
121
200
  /* column resize grip */
122
201
  & .crisp-datatable-resize { position: absolute; top: 0; inset-inline-end: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
123
202
  & .crisp-datatable-resize::after { content: ""; position: absolute; top: 0; inset-inline-start: 3px; width: 2px; height: 100%; background: ${c.bgBrandSolid}; opacity: 0; transition: opacity .12s; }
124
203
  & .crisp-datatable-resize:hover::after, & .crisp-datatable-resize[data-resizing]::after { opacity: 1; }
204
+ /* crisp/#731 -- column drag-to-reorder. Measured live against Attio's
205
+ own grid (the issue's own comment): the dragged header keeps full
206
+ opacity and the plain \`pointer\` cursor throughout (Attio draws no
207
+ ghost and no "grabbing" cursor of its own), so this adds nothing to
208
+ \`[data-dragging]\` itself -- the ONLY new paint is the drop-indicator
209
+ line, a 3px bar in the same measured brand blue the resize handle's
210
+ own hover state (above) already uses, positioned on whichever edge of
211
+ the hovered header the pointer is closer to. */
212
+ & th[data-dragging] { cursor: pointer; }
213
+ & th[data-drop-before]::before, & th[data-drop-after]::after {
214
+ content: ""; position: absolute; top: 0; width: 3px; height: 100%;
215
+ background: ${c.bgBrandSolid}; z-index: 6; pointer-events: none;
216
+ }
217
+ & th[data-drop-before]::before { inset-inline-start: -1.5px; }
218
+ & th[data-drop-after]::after { inset-inline-end: -1.5px; }
125
219
  /* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
126
220
  & .crisp-datatable-entity { display: flex; align-items: center; gap: ${c.space["1.5"]}; min-width: 0; color: ${c.fgPrimary}; font-weight: ${c.weight.medium}; }
127
- & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
128
- & .crisp-datatable-entity:hover .crisp-datatable-entity-name { text-decoration: underline; }
221
+ /* crisp/#601, G-050: measured LIVE (getComputedStyle on the name text,
222
+ plain hover vs rest) that Attio's underline is unconditional, not a
223
+ hover reveal -- identical at rest and on hover: text-decoration-color
224
+ is rgba(0,0,0,0.09), a 9% tint, not the name's own full-contrast text
225
+ colour. The previous :hover-gated, full-contrast rule here was itself
226
+ unmeasured and wrong in two ways at once. */
227
+ & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-decoration: underline; text-decoration-color: color-mix(in srgb, ${c.fgPrimary} 9%, transparent); }
129
228
  & 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%; }
229
+ /* crisp/#601, G-048: two 20px ghost icons, right-aligned in the primary
230
+ column, revealed on ROW hover (the issue's own title says "row
231
+ hover", and a live check confirmed hovering well past the name --
232
+ not just the icons' own hit target -- still reveals them) or
233
+ keyboard focus. Measured live: border-radius 6px, icon colour
234
+ rgba(0,0,0,0.55) at rest, darkening to the full text colour with a
235
+ rgba(0,0,0,0.04) background on the icon's OWN hover -- both
236
+ expressed as color-mix against fgPrimary so they stay correct in
237
+ dark mode instead of a literal black that would invert. */
238
+ & .crisp-datatable-entity-actions { display: flex; align-items: center; gap: ${c.space["1.5"]}; margin-inline-start: auto; flex-shrink: 0; opacity: 0; }
239
+ & tr:hover .crisp-datatable-entity-actions, & .crisp-datatable-entity-actions:focus-within { opacity: 1; }
240
+ & .crisp-datatable-entity-action { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.sm}; background: none; font-family: inherit; color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); cursor: pointer; flex-shrink: 0; }
241
+ & .crisp-datatable-entity-action:hover { background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); color: ${c.fgPrimary}; }
130
242
  & .crisp-datatable-avatar { width: ${c.size.icon}; height: ${c.size.icon}; border-radius: ${c.radius.sm}; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: ${c.weight.semibold}; text-transform: uppercase; color: ${c.fgOnBrand}; background: ${c.bgBrandSolid}; }
131
243
  & .crisp-datatable-avatar[data-tone="success"] { background: ${c.bgSuccess}; } & .crisp-datatable-avatar[data-tone="warning"] { background: ${c.bgWarning}; } & .crisp-datatable-avatar[data-tone="danger"] { background: ${c.bgDanger}; } & .crisp-datatable-avatar[data-tone="purple"] { background: ${c.bgPurple}; } & .crisp-datatable-avatar[data-tone="cyan"] { background: ${c.bgCyan}; } & .crisp-datatable-avatar[data-tone="orange"] { background: ${c.bgOrange}; }
132
244
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
@@ -158,7 +270,8 @@ const config = defineRecipe({
158
270
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
159
271
  & .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: ${c.fgMuted}; cursor: grab; }
160
272
  & .crisp-datatable-grip:active { cursor: grabbing; }
161
- & tbody tr[data-drop-target] td, & tbody tr[data-drop-target] th { box-shadow: inset 0 2px 0 0 ${c.bgBrandSolid}; transition: box-shadow .12s; }`,
273
+ & tbody tr[data-drop-target] td, & tbody tr[data-drop-target] th { box-shadow: inset 0 2px 0 0 ${c.bgBrandSolid}; transition: box-shadow .12s; }
274
+ ${rtlFlipCss}`,
162
275
  variants: {},
163
276
  defaultVariants: {},
164
277
  compoundVariants: [],
@@ -22,12 +22,77 @@ function useDialogInstanceId() {
22
22
  * Escape / outside-click dismissal) with the measured modal surface.
23
23
  * Compose the body with `DialogBody` and actions with `DialogFooter`.
24
24
  */
25
- export function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ariaLabel, closeLabel: closeLabelProp, children, className, style, "aria-describedby": describedBy, ...rest }) {
25
+ export function Dialog({ open, onOpenChange, title, icon, width = 640, variant = "panel", description, "aria-label": ariaLabel, closeLabel: closeLabelProp, children, className, style, "aria-describedby": describedBy, ...rest }) {
26
26
  const closeLabel = useCrispMessage(closeLabelProp, "Close");
27
27
  const id = useDialogInstanceId();
28
+ const isAlert = variant === "alert";
28
29
  const service = useMachine(dialog.machine, {
29
30
  id,
30
31
  open,
32
+ role: isAlert ? "alertdialog" : "dialog",
33
+ // #704: a stacked (second) Dialog can get marked `aria-hidden="true"`
34
+ // on its own outer wrapper a frame after it opens. Traced into
35
+ // `@zag-js/dialog`'s `hideContentBelow` effect and `@zag-js/aria-hidden`'s
36
+ // own source: a modal Dialog's `hideContentBelow` calls
37
+ // `hideOthers([itself])` (deferred one `requestAnimationFrame`), which
38
+ // walks `document.body`'s children and `aria-hidden`s every one that
39
+ // ISN'T an ancestor of its own content. Each open `Dialog` schedules
40
+ // this independently, with no shared registry of who else is open —
41
+ // when the effect re-runs for the FIRST (panel) Dialog (Zag re-runs an
42
+ // effect whenever any prop it reads is considered changed, which a
43
+ // parent re-render can trigger even when the boolean VALUE didn't),
44
+ // its walk runs again with the SECOND (alert) Dialog now mounted as a
45
+ // sibling body child it has never seen before, and marks that whole
46
+ // sibling subtree — including the alert's own wrapper, the thing
47
+ // actually on top and interactive — as hidden. A real upstream
48
+ // limitation (no per-instance awareness of other open Dialogs), not
49
+ // something crisp can fix by calling the library differently.
50
+ //
51
+ // `hideContentBelow` is gated purely on `modal` (no separate flag), so
52
+ // `modal: false` for the alert variant skips it — `trapFocus`/
53
+ // `preventScroll` are passed explicitly right below so they stay on
54
+ // regardless (their own defaults are ALSO `= modal`, so leaving them
55
+ // out here would silently turn them off too).
56
+ //
57
+ // A DOM-level fix was tried first — correcting the wrongly-set
58
+ // attribute after the fact instead of touching `modal` at all, so
59
+ // every other zag-managed behavior would stay untouched by
60
+ // construction. It broke `formdialog.test.tsx`'s own
61
+ // "the confirm's own Cancel keeps the dirtied form open" test (a real
62
+ // regression, reproduced with the correction reduced to literally one
63
+ // `removeAttribute` call, and reproduced again unmodified on the prior
64
+ // WIP commit — that version was never actually run against the full
65
+ // suite): `waitFor(() => expect(queryByRole("alertdialog")).toBeNull())`
66
+ // hung for the full test timeout rather than the confirm's real,
67
+ // sub-100ms unmount, in a way that traces to `aria-hidden` specifically
68
+ // (swapping the correction for a harmless, differently-named attribute
69
+ // did not reproduce it) — a fragile interaction this DOM-mutation
70
+ // approach could not be made to reliably avoid.
71
+ //
72
+ // `modal: false` also turns off `@zag-js/dismissable`'s own
73
+ // `pointerBlocking` for the alert. Verified live (a real
74
+ // headed-then-headless Chromium session against a built `FormDialog`,
75
+ // not just reasoning from the source) that this does NOT regress
76
+ // outside-click safety in practice — but for a DIFFERENT reason than
77
+ // `pointerBlocking` itself: the thing actually keeping a background
78
+ // click from reaching anything is `@zag-js/remove-scroll`'s
79
+ // `preventBodyScroll`, gated on `preventScroll` (kept `true` above,
80
+ // independent of `modal`) — it sets `pointer-events: none` directly on
81
+ // `<body>` while any modal-ish Dialog is open, and `getContentProps()`
82
+ // re-enables `pointer-events: auto` only on each Dialog's OWN content
83
+ // element. Confirmed live: the alert's content had an explicit
84
+ // `pointer-events: auto`; its OWN backdrop/positioner did not, and
85
+ // inherited body's `none`; clicking anywhere in that dead zone (the
86
+ // alert's backdrop, or the panel's, now both non-interactive) dismissed
87
+ // neither dialog, because the click lands on nothing that listens for
88
+ // it — a stronger guarantee than "click closes the wrong layer", not
89
+ // just an absence of that one failure mode. `pointerBlocking` is a
90
+ // second, redundant layer over the same problem `preventScroll` already
91
+ // solves here; the alert's own `closeOnInteractOutside` is already
92
+ // `false` for any `alertdialog` role regardless of `modal`.
93
+ modal: !isAlert,
94
+ trapFocus: true,
95
+ preventScroll: true,
31
96
  onOpenChange(details) {
32
97
  onOpenChange(details.open);
33
98
  },
@@ -51,15 +116,17 @@ export function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-lab
51
116
  const api = dialog.connect(service, normalizeProps);
52
117
  if (!api.open)
53
118
  return null;
54
- return (_jsx(Portal, { children: _jsxs("div", { className: dialogRecipe(), children: [_jsx("div", { ...api.getBackdropProps(), className: "crisp-dialog-backdrop" }), _jsx("div", { ...api.getPositionerProps(), className: "crisp-dialog-positioner", children: _jsx("div", { ...rest, ...withDescribedBy(api.getContentProps(), describedBy), className: cx("crisp-dialog-content", className), "data-crisp-dialog": id,
119
+ return (_jsx(Portal, { children: _jsxs("div", { className: dialogRecipe(), children: [_jsx("div", { ...api.getBackdropProps(), className: "crisp-dialog-backdrop" }), _jsx("div", { ...api.getPositionerProps(), className: "crisp-dialog-positioner", "data-variant": variant, children: _jsx("div", { ...rest, ...withDescribedBy(api.getContentProps(), describedBy), className: cx("crisp-dialog-content", className), "data-crisp-dialog": id, "data-variant": variant,
55
120
  // `width` is the component's own knob, so it wins over a passed
56
121
  // maxWidth; everything else a consumer sets survives
57
- style: { ...style, maxWidth: `min(${width}px, 90vw)` }, children: _jsxs("div", { className: "crisp-dialog-card", children: [title != null && (_jsxs("div", { className: "crisp-dialog-header", children: [icon, _jsx("h2", { ...api.getTitleProps(), className: "crisp-dialog-title", children: title }), _jsx("button", { type: "button", ...api.getCloseTriggerProps(), className: "crisp-dialog-close", "aria-label": closeLabel, children: _jsx(X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] })), children] }) }) })] }) }));
122
+ style: { ...style, maxWidth: `min(${width}px, 90vw)` }, children: _jsxs("div", { className: "crisp-dialog-card", children: [variant === "alert"
123
+ ? (title != null || description != null) && (_jsxs("div", { className: "crisp-dialog-alert-header", children: [title != null && (_jsx("h2", { ...api.getTitleProps(), className: "crisp-dialog-alert-title", children: title })), description != null && (_jsx("p", { ...api.getDescriptionProps(), className: "crisp-dialog-alert-description", children: description }))] }))
124
+ : title != null && (_jsxs("div", { className: "crisp-dialog-header", children: [icon, _jsx("h2", { ...api.getTitleProps(), className: "crisp-dialog-title", children: title }), _jsx("button", { type: "button", ...api.getCloseTriggerProps(), className: "crisp-dialog-close", "aria-label": closeLabel, children: _jsx(X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] })), children] }) }) })] }) }));
58
125
  }
59
126
  Dialog.displayName = "Dialog";
60
127
  export function DialogBody({ children, className, }) {
61
128
  return (_jsx("div", { className: className ? `crisp-dialog-body ${className}` : "crisp-dialog-body", children: children }));
62
129
  }
63
- export function DialogFooter({ children }) {
64
- return _jsx("div", { className: "crisp-dialog-footer", children: children });
130
+ export function DialogFooter({ children, variant = "panel", }) {
131
+ return (_jsx("div", { className: "crisp-dialog-footer", "data-variant": variant, children: children }));
65
132
  }
@@ -15,17 +15,31 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
15
15
  const config = defineRecipe({
16
16
  name: "dialog",
17
17
  base: `display: contents; font-family: ${c.font.sans};
18
- & .crisp-dialog-backdrop { position: fixed; inset: 0; z-index: 60; background: transparent; }
18
+ & .crisp-dialog-backdrop { position: fixed; inset: 0; z-index: 60; background: ${c.bgScrim}; }
19
19
  & .crisp-dialog-positioner { position: fixed; inset: 0; z-index: 61; display: flex; align-items: flex-start; justify-content: center; padding: 15vh ${c.space["4"]} ${c.space["4"]}; }
20
+ & .crisp-dialog-positioner[data-variant="alert"] { align-items: center; padding: ${c.space["4"]}; }
20
21
  & .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: ${c.space["1"]}; border-radius: ${c.radius["2xl"]}; background: color-mix(in srgb, ${c.bgOverlayFrost} 60%, transparent); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); box-shadow: ${e.modal}; opacity: 1; transform: scale(1); transition: opacity 0.2s ease, transform 0.2s ease; @starting-style { opacity: 0; transform: scale(0.97); } }
22
+ & .crisp-dialog-content[data-variant="alert"] { padding: 0; background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; box-shadow: none; }
21
23
  & .crisp-dialog-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: ${c.radius.xl}; background: ${c.bgSurface}; box-shadow: 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 8%, transparent); overflow: hidden; }
24
+ & .crisp-dialog-content[data-variant="alert"] .crisp-dialog-card { border-radius: ${c.radius.xl}; background: ${c.bgPopover}; box-shadow: ${e.floating}; }
22
25
  & .crisp-dialog-header { display: flex; align-items: center; gap: ${c.space["2"]}; flex-shrink: 0; height: 52px; padding: 0 ${c.space["2"]} 0 ${c.space["4"]}; border-bottom: 1px solid ${c.borderSubtle}; }
23
26
  & .crisp-dialog-header svg { flex-shrink: 0; color: ${c.fgMuted}; }
24
27
  & .crisp-dialog-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; margin: 0; }
25
28
  & .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: ${c.radius.md}; background: none; color: ${c.fgMuted}; font-family: inherit; cursor: pointer; }
26
29
  & .crisp-dialog-close:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
30
+ /* The discard-confirm's own stacked title + sentence (crisp#677) — no
31
+ border, no close button, unlike the panel header above: measured
32
+ live, both themes, 18px/24px 600-weight title (no scale token
33
+ reaches 18px — the scale tops out at text.xl's 16px, so this is raw
34
+ px per Golden Rule 3's "genuinely intrinsic size" exception, same as
35
+ this file's own submit-key glyphs) then a 14px/20px 500-weight
36
+ sentence 4px below it. */
37
+ & .crisp-dialog-alert-header { display: flex; flex-direction: column; gap: ${c.space["1"]}; padding: ${c.space["4"]} ${c.space["4"]} 0; }
38
+ & .crisp-dialog-alert-title { margin: 0; font-size: 18px; line-height: 24px; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; }
39
+ & .crisp-dialog-alert-description { margin: 0; font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium}; color: ${c.fgAlertDescription}; }
27
40
  & .crisp-dialog-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: ${c.space["4"]}; padding: ${c.space["5"]} ${c.space["4"]}; }
28
41
  & .crisp-dialog-footer { display: flex; align-items: center; justify-content: flex-end; gap: ${c.space["3"]}; flex-shrink: 0; min-height: 56px; padding: 0 ${c.space["4"]}; border-top: 1px solid ${c.borderSubtle}; }
42
+ & .crisp-dialog-footer[data-variant="alert"] { min-height: 45px; padding: ${c.space["2"]}; }
29
43
  & .crisp-dialog-field { display: flex; flex-direction: column; gap: ${c.space["1.5"]}; }
30
44
  & .crisp-dialog-field-label { font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 50%, transparent); }
31
45
  & .crisp-dialog-more { display: inline-flex; align-items: center; gap: ${c.space["2"]}; margin-inline-end: auto; border: none; background: none; padding: 0; font-family: inherit; font-size: ${c.text.md}; color: ${c.fgMuted}; cursor: pointer; }
@@ -16,12 +16,34 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
16
16
  // stay byte-for-byte identical to what shipped before the extraction.
17
17
  //
18
18
  // `.menu--op` (#594 follow-up) is the filter condition's own operator list —
19
- // measured live against Attio's own popover at 135px (rounds to 134px), not
20
- // the default 232px every other menu here uses. `.menu-item` also picked up
19
+ // no fixed min-width of its own (#593 correction below), unlike the default
20
+ // 232px every other menu here uses. `.menu-item` also picked up
21
21
  // `line-height: 20px` and `font-weight: medium` from the same measurement
22
22
  // (its 8 rows read 14px/20px/500) — shared by every menu that renders one,
23
23
  // not just the operator list, since crisp's menu-item typography was
24
24
  // previously unset rather than wrong.
25
+ //
26
+ // #593 correction: `.menu--op` originally carried a fixed `min-width: 134px`
27
+ // (rounded from one live measurement, the 8-op text set at 135px). Adding
28
+ // number/date columns' own shorter operator sets and re-measuring them live
29
+ // (104.86px for 6 number ops, 91.16px for 6 date ops — both narrower than
30
+ // 134px) showed that number wasn't a real Attio constant, just one set's own
31
+ // content width: a floor sized for the text set overshoots the other two.
32
+ // Dropped to `min-width: 0` so every op set sizes to its own content, the
33
+ // same as any other menu here without an explicit floor.
34
+ //
35
+ // `.menu-item` also picked up `text-decoration: none` for #595's Import/
36
+ // Export menu — Attio's own "Import CSV" and "Learn about imports and
37
+ // exports" rows are real `<a>` tags (the middle two, pure JS actions, are
38
+ // plain `<div role="menuitem">`), and an anchor needs the underline reset
39
+ // every button-based `.menu-item` never needed. No-op for every existing
40
+ // button consumer of this class. Same menu's 2 dividers use `.menu-sep` on
41
+ // a real `<hr>` (implicit `role="separator"`, no extra ARIA plumbing needed)
42
+ // rather than a `<div role="separator">` — biome's a11y rules want a
43
+ // separator either genuinely interactive (`tabIndex`, `aria-valuenow`, a
44
+ // resizable-splitter kind of thing this isn't) or the semantic element
45
+ // instead; `.menu-sep` picked up `border: none` for it, a no-op for the
46
+ // existing plain-`<div>` consumers of this class.
25
47
  export const toolbarChromeHead = (p) => ` & .${CLASS_PREFIX}${p}-toolbar { display: flex; align-items: center; gap: ${c.space["2"]}; padding: 0 ${c.space["3"]} ${c.space["2"]}; }
26
48
  & .${CLASS_PREFIX}${p}-toolbar[data-inline] { padding-bottom: 0; }
27
49
  & .${CLASS_PREFIX}${p}-search { display: flex; align-items: center; gap: ${c.space["1.5"]}; height: ${c.size.row}; padding: 0 ${c.space["2.5"]}; border: none; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; color: ${c.fgMuted}; }
@@ -60,7 +82,7 @@ export const toolbarChromeBody = (p) => ` & .${CLASS_PREFIX}${p}-menu-head {
60
82
  & .${CLASS_PREFIX}${p}-sort-learn { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; font-size: ${c.text.lg}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); text-decoration: none; }
61
83
  & .${CLASS_PREFIX}${p}-sort-learn svg { flex-shrink: 0; }
62
84
  & .${CLASS_PREFIX}${p}-sort-learn:hover { color: ${c.fgPrimary}; }
63
- & .${CLASS_PREFIX}${p}-menu-item { display: flex; align-items: center; gap: ${c.space["1.5"]}; width: 100%; height: ${c.size.menuRow}; margin: 0; padding: 0 ${c.space["2"]}; border: none; background: none; border-radius: ${c.radius.md}; font-family: inherit; font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; text-align: left; cursor: pointer; }
85
+ & .${CLASS_PREFIX}${p}-menu-item { display: flex; align-items: center; gap: ${c.space["1.5"]}; width: 100%; height: ${c.size.menuRow}; margin: 0; padding: 0 ${c.space["2"]}; border: none; background: none; border-radius: ${c.radius.md}; font-family: inherit; font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; text-align: left; text-decoration: none; cursor: pointer; }
64
86
  & .${CLASS_PREFIX}${p}-menu-item:hover { background: ${c.bgRaised}; }
65
87
  & .${CLASS_PREFIX}${p}-menu-item:disabled { opacity: 0.4; cursor: default; }
66
88
  & .${CLASS_PREFIX}${p}-menu-item:disabled:hover { background: none; }
@@ -68,12 +90,34 @@ export const toolbarChromeBody = (p) => ` & .${CLASS_PREFIX}${p}-menu-head {
68
90
  & .${CLASS_PREFIX}${p}-menu-item[data-on] svg { color: ${c.bgBrandSolid}; }
69
91
  & .${CLASS_PREFIX}${p}-menu-item .${CLASS_PREFIX}${p}-menu-check { margin-left: auto; }
70
92
  & .${CLASS_PREFIX}${p}-menu-input { height: ${c.size.row}; margin: ${c.space["0.5"]}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; background: ${c.bgSurface}; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; outline: none; }
71
- & .${CLASS_PREFIX}${p}-menu-sep { height: 1px; background: ${c.borderSubtle}; margin: ${c.space["0.5"]} 0; }
93
+ & .${CLASS_PREFIX}${p}-menu-sep { height: 1px; border: none; background: ${c.borderSubtle}; margin: ${c.space["0.5"]} 0; }
94
+ /* #592: the Filter field picker's nested-scope header (back control +
95
+ search, once drilled into a nestedColumns column) and a search
96
+ result's own breadcrumb row ("Team › Twitter follower count",
97
+ surfaced by the SAME query matching a nested column). menu-back
98
+ reuses the established 28px icon-only control pattern this file
99
+ already uses twice (sort-remove, the kebab segment) rather than
100
+ an independent measurement of Attio's own back arrow. The breadcrumb
101
+ colours ARE measured live: the parent segment muted at the same
102
+ exact color-mix 63% sort-learn above already uses, the
103
+ separator chevron and leaf segment both full fgPrimary (the
104
+ generic menu-item svg rule below defaults every icon to
105
+ fgMuted, so the separator needs its own override to read
106
+ correctly here). */
107
+ & .${CLASS_PREFIX}${p}-menu-nesthead { display: flex; align-items: center; gap: ${c.space["1"]}; margin: ${c.space["0.5"]}; }
108
+ & .${CLASS_PREFIX}${p}-menu-nesthead .${CLASS_PREFIX}${p}-menu-input { flex: 1; margin: 0; }
109
+ & .${CLASS_PREFIX}${p}-menu-back { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex-shrink: 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: none; color: ${c.fgMuted}; cursor: pointer; }
110
+ & .${CLASS_PREFIX}${p}-menu-back:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
111
+ & .${CLASS_PREFIX}${p}-menu-item--expand { justify-content: space-between; }
112
+ & .${CLASS_PREFIX}${p}-menu-item-path { display: inline-flex; align-items: center; gap: 2px; min-width: 0; overflow: hidden; }
113
+ & .${CLASS_PREFIX}${p}-menu-item-path-parent { flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; max-width: 100px; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
114
+ & .${CLASS_PREFIX}${p}-menu-item-path-sep { flex-shrink: 0; color: ${c.fgPrimary}; }
115
+ & .${CLASS_PREFIX}${p}-menu-item-path > span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
72
116
  & .${CLASS_PREFIX}${p}-menu--up { top: auto; bottom: calc(100% + ${c.space["1"]}); transform-origin: bottom; }
73
117
  & .${CLASS_PREFIX}${p}-menu--right { left: auto; right: 0; }
74
118
  & .${CLASS_PREFIX}${p}-menu--fixed { position: fixed; top: auto; left: auto; z-index: 40; }
75
119
  & .${CLASS_PREFIX}${p}-menu--hdr { min-width: 200px; }
76
- & .${CLASS_PREFIX}${p}-menu--op { min-width: 134px; }
120
+ & .${CLASS_PREFIX}${p}-menu--op { min-width: 0; }
77
121
  & .${CLASS_PREFIX}${p}-menu-item--danger { color: ${c.bgDanger}; }
78
122
  & .${CLASS_PREFIX}${p}-menu-item--danger svg { color: ${c.bgDanger}; }
79
123
  & .${CLASS_PREFIX}${p}-menu-item--danger:hover { background: color-mix(in srgb, ${c.bgDanger} 10%, transparent); }