@crisp-ui-kit/crisp 0.57.1 → 0.57.3

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 (277) hide show
  1. package/dist/authshell.css +9 -4
  2. package/dist/authshell.layered.css +9 -4
  3. package/dist/badge.css +3 -3
  4. package/dist/badge.layered.css +3 -3
  5. package/dist/board.css +28 -8
  6. package/dist/board.layered.css +28 -8
  7. package/dist/bottomsheet.css +26 -0
  8. package/dist/bottomsheet.d.ts +7 -0
  9. package/dist/bottomsheet.layered.css +28 -0
  10. package/dist/button.css +10 -6
  11. package/dist/button.d.ts +2 -2
  12. package/dist/button.layered.css +10 -6
  13. package/dist/checkbox.css +7 -3
  14. package/dist/checkbox.layered.css +7 -3
  15. package/dist/chip.css +16 -1
  16. package/dist/chip.d.ts +3 -1
  17. package/dist/chip.layered.css +16 -1
  18. package/dist/chipgroup.css +5 -0
  19. package/dist/chipgroup.d.ts +7 -0
  20. package/dist/chipgroup.layered.css +7 -0
  21. package/dist/cjs/components/badge/badge.recipe.js +22 -8
  22. package/dist/cjs/components/bottomsheet/bottomsheet.js +90 -0
  23. package/dist/cjs/components/bottomsheet/bottomsheet.recipe.js +77 -0
  24. package/dist/cjs/components/bottomsheet/index.js +9 -0
  25. package/dist/cjs/components/breadcrumb/breadcrumb.js +1 -1
  26. package/dist/cjs/components/button/button.recipe.js +67 -15
  27. package/dist/cjs/components/checkbox/checkbox.recipe.js +61 -9
  28. package/dist/cjs/components/chip/chip.js +14 -2
  29. package/dist/cjs/components/chip/chip.recipe.js +72 -2
  30. package/dist/cjs/components/chipgroup/chipgroup.js +51 -0
  31. package/dist/cjs/components/chipgroup/chipgroup.recipe.js +36 -0
  32. package/dist/cjs/components/chipgroup/index.js +7 -0
  33. package/dist/cjs/components/datatable/datatable.js +5 -1
  34. package/dist/cjs/components/datatable/datatable.recipe.js +21 -6
  35. package/dist/cjs/components/datepickerfield/datepickerfield.js +2 -2
  36. package/dist/cjs/components/datepickerfield/datepickerfield.recipe.js +16 -3
  37. package/dist/cjs/components/dialog/dialog.recipe.js +21 -10
  38. package/dist/cjs/components/emptystate/emptystate.recipe.js +7 -2
  39. package/dist/cjs/components/fileupload/fileupload.js +21 -4
  40. package/dist/cjs/components/input/index.js +2 -1
  41. package/dist/cjs/components/input/input.js +31 -1
  42. package/dist/cjs/components/input/input.recipe.js +16 -9
  43. package/dist/cjs/components/menu/menu.js +18 -2
  44. package/dist/cjs/components/menu/menu.recipe.js +21 -7
  45. package/dist/cjs/components/menusheet/index.js +9 -0
  46. package/dist/cjs/components/menusheet/menusheet.js +133 -0
  47. package/dist/cjs/components/menusheet/menusheet.recipe.js +103 -0
  48. package/dist/cjs/components/multiselect/multiselect.js +1 -1
  49. package/dist/cjs/components/multiselect/multiselect.recipe.js +33 -9
  50. package/dist/cjs/components/select/select.js +1 -1
  51. package/dist/cjs/components/select/select.recipe.js +38 -12
  52. package/dist/cjs/components/snackbar/index.js +8 -0
  53. package/dist/cjs/components/snackbar/snackbar.js +121 -0
  54. package/dist/cjs/components/snackbar/snackbar.recipe.js +71 -0
  55. package/dist/cjs/components/table/table.recipe.js +7 -1
  56. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +7 -2
  57. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +37 -12
  58. package/dist/cjs/components/tabs/tabs.js +10 -4
  59. package/dist/cjs/components/tabs/tabs.recipe.js +23 -6
  60. package/dist/cjs/components/textarea/textarea.recipe.js +11 -8
  61. package/dist/cjs/core/floating-portal.js +7 -4
  62. package/dist/cjs/index.js +7 -0
  63. package/dist/cjs/product/authshell/authshell.recipe.js +5 -1
  64. package/dist/cjs/product/command/command.recipe.js +4 -1
  65. package/dist/cjs/product/comments/comments.js +7 -2
  66. package/dist/cjs/product/comments/comments.recipe.js +25 -7
  67. package/dist/cjs/product/inbox/inbox.js +11 -3
  68. package/dist/cjs/product/memberssettings/index.js +6 -0
  69. package/dist/cjs/product/memberssettings/memberssettings.js +99 -0
  70. package/dist/cjs/product/memberssettings/memberssettings.recipe.js +83 -0
  71. package/dist/cjs/product/notificationssettings/index.js +5 -0
  72. package/dist/cjs/product/notificationssettings/notificationssettings.js +19 -0
  73. package/dist/cjs/product/notificationssettings/notificationssettings.recipe.js +44 -0
  74. package/dist/cjs/product/recordpage/index.js +2 -1
  75. package/dist/cjs/product/recordpage/recordpage.js +106 -9
  76. package/dist/cjs/product/recordpage/recordpage.recipe.js +68 -6
  77. package/dist/cjs/product/workspacegeneralsettings/index.js +5 -0
  78. package/dist/cjs/product/workspacegeneralsettings/workspacegeneralsettings.js +21 -0
  79. package/dist/cjs/product/workspacegeneralsettings/workspacegeneralsettings.recipe.js +51 -0
  80. package/dist/cjs/tokens/contract.js +1 -0
  81. package/dist/cjs/tokens/elevation.js +8 -5
  82. package/dist/cjs/tokens/palette.js +28 -8
  83. package/dist/cjs/tokens/scale.js +44 -2
  84. package/dist/cjs/tokens/semantic.js +2 -0
  85. package/dist/columncalc.css +15 -7
  86. package/dist/columncalc.layered.css +15 -7
  87. package/dist/command.css +23 -19
  88. package/dist/command.layered.css +23 -19
  89. package/dist/comments.css +28 -19
  90. package/dist/comments.d.ts +2 -2
  91. package/dist/comments.layered.css +28 -19
  92. package/dist/datatable.css +171 -71
  93. package/dist/datatable.layered.css +171 -71
  94. package/dist/datepicker.css +13 -41
  95. package/dist/datepicker.layered.css +13 -41
  96. package/dist/datepickerfield.css +12 -3
  97. package/dist/datepickerfield.d.ts +2 -2
  98. package/dist/datepickerfield.layered.css +12 -3
  99. package/dist/dialog.css +12 -7
  100. package/dist/dialog.layered.css +12 -7
  101. package/dist/emptystate.css +5 -3
  102. package/dist/emptystate.layered.css +5 -3
  103. package/dist/esm/components/badge/badge.recipe.js +22 -8
  104. package/dist/esm/components/bottomsheet/bottomsheet.js +52 -0
  105. package/dist/esm/components/bottomsheet/bottomsheet.recipe.js +74 -0
  106. package/dist/esm/components/bottomsheet/index.js +2 -0
  107. package/dist/esm/components/breadcrumb/breadcrumb.js +1 -1
  108. package/dist/esm/components/button/button.recipe.js +68 -16
  109. package/dist/esm/components/checkbox/checkbox.recipe.js +61 -9
  110. package/dist/esm/components/chip/chip.js +14 -2
  111. package/dist/esm/components/chip/chip.recipe.js +72 -2
  112. package/dist/esm/components/chipgroup/chipgroup.js +15 -0
  113. package/dist/esm/components/chipgroup/chipgroup.recipe.js +33 -0
  114. package/dist/esm/components/chipgroup/index.js +2 -0
  115. package/dist/esm/components/datatable/datatable.js +5 -1
  116. package/dist/esm/components/datatable/datatable.recipe.js +21 -6
  117. package/dist/esm/components/datepickerfield/datepickerfield.js +2 -2
  118. package/dist/esm/components/datepickerfield/datepickerfield.recipe.js +16 -3
  119. package/dist/esm/components/dialog/dialog.recipe.js +22 -11
  120. package/dist/esm/components/emptystate/emptystate.recipe.js +7 -2
  121. package/dist/esm/components/fileupload/fileupload.js +20 -5
  122. package/dist/esm/components/input/index.js +1 -1
  123. package/dist/esm/components/input/input.js +30 -0
  124. package/dist/esm/components/input/input.recipe.js +17 -10
  125. package/dist/esm/components/menu/menu.js +18 -2
  126. package/dist/esm/components/menu/menu.recipe.js +21 -7
  127. package/dist/esm/components/menusheet/index.js +2 -0
  128. package/dist/esm/components/menusheet/menusheet.js +95 -0
  129. package/dist/esm/components/menusheet/menusheet.recipe.js +100 -0
  130. package/dist/esm/components/multiselect/multiselect.js +1 -1
  131. package/dist/esm/components/multiselect/multiselect.recipe.js +33 -9
  132. package/dist/esm/components/select/select.js +1 -1
  133. package/dist/esm/components/select/select.recipe.js +38 -12
  134. package/dist/esm/components/snackbar/index.js +2 -0
  135. package/dist/esm/components/snackbar/snackbar.js +84 -0
  136. package/dist/esm/components/snackbar/snackbar.recipe.js +68 -0
  137. package/dist/esm/components/table/table.recipe.js +7 -1
  138. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +7 -2
  139. package/dist/esm/components/tabletoolbar/tabletoolbar.js +37 -12
  140. package/dist/esm/components/tabs/tabs.js +10 -4
  141. package/dist/esm/components/tabs/tabs.recipe.js +23 -6
  142. package/dist/esm/components/textarea/textarea.recipe.js +12 -9
  143. package/dist/esm/core/floating-portal.js +7 -4
  144. package/dist/esm/index.js +7 -0
  145. package/dist/esm/product/authshell/authshell.recipe.js +5 -1
  146. package/dist/esm/product/command/command.recipe.js +4 -1
  147. package/dist/esm/product/comments/comments.js +7 -2
  148. package/dist/esm/product/comments/comments.recipe.js +25 -7
  149. package/dist/esm/product/inbox/inbox.js +11 -3
  150. package/dist/esm/product/memberssettings/index.js +1 -0
  151. package/dist/esm/product/memberssettings/memberssettings.js +62 -0
  152. package/dist/esm/product/memberssettings/memberssettings.recipe.js +80 -0
  153. package/dist/esm/product/notificationssettings/index.js +1 -0
  154. package/dist/esm/product/notificationssettings/notificationssettings.js +16 -0
  155. package/dist/esm/product/notificationssettings/notificationssettings.recipe.js +41 -0
  156. package/dist/esm/product/recordpage/index.js +1 -1
  157. package/dist/esm/product/recordpage/recordpage.js +106 -10
  158. package/dist/esm/product/recordpage/recordpage.recipe.js +68 -6
  159. package/dist/esm/product/workspacegeneralsettings/index.js +1 -0
  160. package/dist/esm/product/workspacegeneralsettings/workspacegeneralsettings.js +18 -0
  161. package/dist/esm/product/workspacegeneralsettings/workspacegeneralsettings.recipe.js +48 -0
  162. package/dist/esm/tokens/contract.js +2 -1
  163. package/dist/esm/tokens/elevation.js +8 -5
  164. package/dist/esm/tokens/palette.js +28 -8
  165. package/dist/esm/tokens/scale.js +43 -1
  166. package/dist/esm/tokens/semantic.js +2 -0
  167. package/dist/fileupload.css +3 -1
  168. package/dist/fileupload.layered.css +3 -1
  169. package/dist/input.css +6 -4
  170. package/dist/input.layered.css +6 -4
  171. package/dist/listpage.css +3 -1
  172. package/dist/listpage.layered.css +3 -1
  173. package/dist/memberssettings.css +67 -0
  174. package/dist/memberssettings.d.ts +7 -0
  175. package/dist/memberssettings.layered.css +69 -0
  176. package/dist/menu.css +2 -2
  177. package/dist/menu.layered.css +2 -2
  178. package/dist/menusheet.css +36 -0
  179. package/dist/menusheet.d.ts +7 -0
  180. package/dist/menusheet.layered.css +38 -0
  181. package/dist/multiselect.css +13 -6
  182. package/dist/multiselect.layered.css +13 -6
  183. package/dist/native/tokens.d.ts +2 -1
  184. package/dist/native/tokens.js +20 -3
  185. package/dist/notificationssettings.css +22 -0
  186. package/dist/notificationssettings.d.ts +7 -0
  187. package/dist/notificationssettings.layered.css +24 -0
  188. package/dist/objectsettings.css +4 -4
  189. package/dist/objectsettings.layered.css +4 -4
  190. package/dist/recordpage.css +90 -23
  191. package/dist/recordpage.layered.css +90 -23
  192. package/dist/recordpanel.css +35 -16
  193. package/dist/recordpanel.layered.css +35 -16
  194. package/dist/resizablepanel.css +8 -5
  195. package/dist/resizablepanel.layered.css +8 -5
  196. package/dist/richtext.css +24 -8
  197. package/dist/richtext.layered.css +24 -8
  198. package/dist/savedviews.css +9 -2
  199. package/dist/savedviews.layered.css +9 -2
  200. package/dist/select.css +25 -5
  201. package/dist/select.layered.css +25 -5
  202. package/dist/settingsshell.css +2 -3
  203. package/dist/settingsshell.layered.css +2 -3
  204. package/dist/slider.css +6 -27
  205. package/dist/slider.layered.css +6 -27
  206. package/dist/snackbar.css +24 -0
  207. package/dist/snackbar.d.ts +7 -0
  208. package/dist/snackbar.layered.css +26 -0
  209. package/dist/statcard.css +3 -1
  210. package/dist/statcard.layered.css +3 -1
  211. package/dist/styles.css +911 -354
  212. package/dist/styles.layered.css +911 -354
  213. package/dist/table.css +35 -12
  214. package/dist/table.layered.css +35 -12
  215. package/dist/tabletoolbar.css +35 -24
  216. package/dist/tabletoolbar.layered.css +35 -24
  217. package/dist/tabs.css +18 -4
  218. package/dist/tabs.layered.css +18 -4
  219. package/dist/textarea.css +2 -6
  220. package/dist/textarea.layered.css +2 -6
  221. package/dist/themepicker.css +2 -1
  222. package/dist/themepicker.layered.css +2 -1
  223. package/dist/tokens.css +13 -2
  224. package/dist/tokens.json +20 -3
  225. package/dist/types/components/bottomsheet/bottomsheet.d.ts +35 -0
  226. package/dist/types/components/bottomsheet/bottomsheet.recipe.d.ts +3 -0
  227. package/dist/types/components/bottomsheet/index.d.ts +2 -0
  228. package/dist/types/components/breadcrumb/breadcrumb.d.ts +9 -1
  229. package/dist/types/components/button/button.d.ts +2 -0
  230. package/dist/types/components/button/button.recipe.d.ts +24 -6
  231. package/dist/types/components/checkbox/checkbox.recipe.d.ts +28 -5
  232. package/dist/types/components/chip/chip.d.ts +22 -0
  233. package/dist/types/components/chip/chip.recipe.d.ts +49 -1
  234. package/dist/types/components/chipgroup/chipgroup.d.ts +14 -0
  235. package/dist/types/components/chipgroup/chipgroup.recipe.d.ts +5 -0
  236. package/dist/types/components/chipgroup/index.d.ts +2 -0
  237. package/dist/types/components/datatable/datatable.d.ts +19 -0
  238. package/dist/types/components/datepickerfield/datepickerfield.d.ts +4 -1
  239. package/dist/types/components/datepickerfield/datepickerfield.recipe.d.ts +18 -3
  240. package/dist/types/components/fileupload/fileupload.d.ts +26 -0
  241. package/dist/types/components/input/index.d.ts +1 -1
  242. package/dist/types/components/input/input.d.ts +72 -0
  243. package/dist/types/components/menusheet/index.d.ts +2 -0
  244. package/dist/types/components/menusheet/menusheet.d.ts +42 -0
  245. package/dist/types/components/menusheet/menusheet.recipe.d.ts +3 -0
  246. package/dist/types/components/snackbar/index.d.ts +2 -0
  247. package/dist/types/components/snackbar/snackbar.d.ts +36 -0
  248. package/dist/types/components/snackbar/snackbar.recipe.d.ts +5 -0
  249. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +28 -1
  250. package/dist/types/components/tabs/tabs.d.ts +11 -1
  251. package/dist/types/core/floating-portal.d.ts +6 -5
  252. package/dist/types/index.d.ts +7 -0
  253. package/dist/types/product/comments/comments.d.ts +9 -3
  254. package/dist/types/product/comments/comments.recipe.d.ts +12 -2
  255. package/dist/types/product/inbox/inbox.d.ts +13 -1
  256. package/dist/types/product/memberssettings/index.d.ts +1 -0
  257. package/dist/types/product/memberssettings/memberssettings.d.ts +85 -0
  258. package/dist/types/product/memberssettings/memberssettings.recipe.d.ts +3 -0
  259. package/dist/types/product/notificationssettings/index.d.ts +1 -0
  260. package/dist/types/product/notificationssettings/notificationssettings.d.ts +37 -0
  261. package/dist/types/product/notificationssettings/notificationssettings.recipe.d.ts +3 -0
  262. package/dist/types/product/recordpage/index.d.ts +1 -1
  263. package/dist/types/product/recordpage/recordpage.d.ts +99 -2
  264. package/dist/types/product/workspacegeneralsettings/index.d.ts +1 -0
  265. package/dist/types/product/workspacegeneralsettings/workspacegeneralsettings.d.ts +48 -0
  266. package/dist/types/product/workspacegeneralsettings/workspacegeneralsettings.recipe.d.ts +3 -0
  267. package/dist/types/tokens/contract.d.ts +4 -1
  268. package/dist/types/tokens/elevation.d.ts +8 -5
  269. package/dist/types/tokens/palette.d.ts +9 -1
  270. package/dist/types/tokens/scale.d.ts +11 -1
  271. package/dist/types/tokens/semantic.d.ts +2 -0
  272. package/dist/wheelpicker.css +4 -2
  273. package/dist/wheelpicker.layered.css +4 -2
  274. package/dist/workspacegeneralsettings.css +30 -0
  275. package/dist/workspacegeneralsettings.d.ts +7 -0
  276. package/dist/workspacegeneralsettings.layered.css +32 -0
  277. package/package.json +36 -1
@@ -28,9 +28,14 @@ const config = defineRecipe({
28
28
  // A full-view empty state (measured from a filtered-to-zero records table):
29
29
  // a 45px ringed circle icon, a 20/24 semibold heading, a 14/20 medium muted body,
30
30
  // then a primary action. Gaps are 16 / 8 / 16 (icon→title→desc→action).
31
+ // *Correction (crisp#933):* the original pass above didn't check letter-spacing.
32
+ // `records` below is the exact same title component (confirmed live at
33
+ // /techpartners/users and /techpartners/workspaces, both "No X records" at
34
+ // 20px/600/24px) and measures `-0.4px` -- exactly `c.tracking["20"]` (#909),
35
+ // not a coincidence, just a value nobody read off this element until now.
31
36
  page: `gap: ${c.space["2"]};
32
37
  & .crisp-emptystate-icon { width: 45px; height: 45px; margin-bottom: ${c.space["2"]}; border-radius: ${c.radius.full}; border: 1px solid ${c.borderSubtle}; background: none; color: ${c.fgMuted}; }
33
- & .crisp-emptystate-title { font-size: 20px; line-height: 24px; }
38
+ & .crisp-emptystate-title { font-size: 20px; line-height: 24px; letter-spacing: ${c.tracking["20"]}; }
34
39
  & .crisp-emptystate-desc { max-width: 266px; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; line-height: 20px; }
35
40
  & .crisp-emptystate-action { margin-top: ${c.space["2"]}; }`,
36
41
  // #603: Attio's zero-*records* table state (Users/Workspaces with
@@ -45,7 +50,7 @@ const config = defineRecipe({
45
50
  // by the consumer via the existing `icon` prop, same as every
46
51
  // other variant.
47
52
  records: `& .crisp-emptystate-icon { width: 138px; height: 138px; margin-bottom: ${c.space["2"]}; border-radius: 0; color: color-mix(in srgb, ${c.fgPrimary} 22%, transparent); background: radial-gradient(circle, color-mix(in srgb, ${c.fgPrimary} 12%, transparent) 1.2px, transparent 1.2px) 0 0 / 16px 16px; }
48
- & .crisp-emptystate-title { font-size: 20px; line-height: 24px; }
53
+ & .crisp-emptystate-title { font-size: 20px; line-height: 24px; letter-spacing: ${c.tracking["20"]}; }
49
54
  & .crisp-emptystate-desc { max-width: 266px; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; line-height: 20px; }`,
50
55
  },
51
56
  },
@@ -17,8 +17,23 @@ export const fileUploadLabels = {
17
17
  INVALID_TYPE: "That file type is not accepted.",
18
18
  },
19
19
  };
20
+ /**
21
+ * One row of `FileUpload`'s own accepted-file list (crisp#965) — icon-free
22
+ * name + trailing size + remove, `min-height: size.tableRow` on `bgRaised`
23
+ * (measured from a data-grid row, see `fileupload.recipe.ts`). Exported on
24
+ * its own because `FileUpload` internally only lists `File` objects from the
25
+ * current browser session; a caller with already-persisted attachments
26
+ * (fetched from a server, never a real `File`) has no `acceptedFileEntries`
27
+ * to hand it and needs the identical row for its own list instead of
28
+ * hand-rolling one.
29
+ */
30
+ export function FileUploadItem({ name, size, status, progress, onRemove, removeLabel, className, }) {
31
+ return (_jsxs("li", { className: className
32
+ ? `crisp-fileupload-item ${className}`
33
+ : "crisp-fileupload-item", "data-status": status, children: [_jsx("span", { className: "crisp-fileupload-item-name", children: name }), status === "uploading" && progress != null && (_jsx("span", { className: "crisp-fileupload-progress", role: "progressbar", "aria-valuenow": Math.round(progress), "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": name, children: _jsx("span", { className: "crisp-fileupload-progress-fill", style: { width: `${progress}%` } }) })), size != null && (_jsx("span", { className: "crisp-fileupload-item-size", children: formatSize(size) })), onRemove && (_jsx("button", { type: "button", className: "crisp-fileupload-remove", "aria-label": removeLabel, onClick: onRemove, children: _jsx(X, { size: 13, strokeWidth: 2 }) }))] }));
34
+ }
20
35
  /** 1000-based, matching how every file manager reports size. */
21
- function formatSize(bytes) {
36
+ export function formatSize(bytes) {
22
37
  if (bytes < 1000)
23
38
  return `${bytes} B`;
24
39
  const units = ["kB", "MB", "GB", "TB"];
@@ -103,8 +118,8 @@ export function FileUpload({ className, labels, "aria-label": ariaLabel, id, acc
103
118
  // focus should land next.
104
119
  const rootRef = React.useRef(null);
105
120
  const focusAfterRemoval = useFocusAfterRemoval(rootRef, ".crisp-fileupload-remove", dropzoneRef);
106
- return (_jsxs("div", { ref: rootRef, ...rest, className: fileuploadRecipe({ className }), children: [_jsxs("button", { ...dropzoneProps, ref: dropzoneRef, type: "button", className: "crisp-fileupload-zone", tabIndex: disabled ? -1 : 0, "aria-label": ariaLabel ?? t.hint, disabled: disabled, "data-dragging": dragging ? "" : undefined, "data-disabled": disabled ? "" : undefined, "data-invalid": invalid ? "" : undefined, onClick: () => openFilePicker(), children: [_jsx("span", { className: "crisp-fileupload-title", children: t.title }), _jsx("span", { className: "crisp-fileupload-hint", children: t.hint })] }), _jsx("input", { ...hiddenInputProps, id: id ?? hiddenInputProps.id, ref: inputRef, className: "crisp-fileupload-input" }), acceptedFileEntries.length > 0 && (_jsx("ul", { className: "crisp-fileupload-list", children: acceptedFileEntries.map((entry, i) => (_jsxs("li", { className: "crisp-fileupload-item", "data-status": entry.status, children: [_jsx("span", { className: "crisp-fileupload-item-name", children: entry.file.name }), entry.status === "uploading" && entry.progress != null && (_jsx("span", { className: "crisp-fileupload-progress", role: "progressbar", "aria-valuenow": Math.round(entry.progress), "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": entry.file.name, children: _jsx("span", { className: "crisp-fileupload-progress-fill", style: { width: `${entry.progress}%` } }) })), _jsx("span", { className: "crisp-fileupload-item-size", children: formatSize(entry.file.size) }), _jsx("button", { type: "button", className: "crisp-fileupload-remove", "aria-label": `${t.remove}: ${entry.file.name}`, onClick: () => {
107
- focusAfterRemoval(i);
108
- removeFileEntry(entry.id);
109
- }, children: _jsx(X, { size: 13, strokeWidth: 2 }) })] }, entry.id))) })), _jsx("ul", { className: "crisp-fileupload-errors", "aria-live": "polite", children: rejections.map((r) => (_jsx("li", { className: "crisp-fileupload-error", children: `${r.file.name}: ${r.errors.map((e) => t.errors[e] ?? e).join(" ")}` }, r.file.name))) })] }));
121
+ return (_jsxs("div", { ref: rootRef, ...rest, className: fileuploadRecipe({ className }), children: [_jsxs("button", { ...dropzoneProps, ref: dropzoneRef, type: "button", className: "crisp-fileupload-zone", tabIndex: disabled ? -1 : 0, "aria-label": ariaLabel ?? t.hint, disabled: disabled, "data-dragging": dragging ? "" : undefined, "data-disabled": disabled ? "" : undefined, "data-invalid": invalid ? "" : undefined, onClick: () => openFilePicker(), children: [_jsx("span", { className: "crisp-fileupload-title", children: t.title }), _jsx("span", { className: "crisp-fileupload-hint", children: t.hint })] }), _jsx("input", { ...hiddenInputProps, id: id ?? hiddenInputProps.id, ref: inputRef, className: "crisp-fileupload-input" }), acceptedFileEntries.length > 0 && (_jsx("ul", { className: "crisp-fileupload-list", children: acceptedFileEntries.map((entry, i) => (_jsx(FileUploadItem, { name: entry.file.name, size: entry.file.size, status: entry.status, progress: entry.status === "uploading" ? entry.progress : undefined, removeLabel: `${t.remove}: ${entry.file.name}`, onRemove: () => {
122
+ focusAfterRemoval(i);
123
+ removeFileEntry(entry.id);
124
+ } }, entry.id))) })), _jsx("ul", { className: "crisp-fileupload-errors", "aria-live": "polite", children: rejections.map((r) => (_jsx("li", { className: "crisp-fileupload-error", children: `${r.file.name}: ${r.errors.map((e) => t.errors[e] ?? e).join(" ")}` }, r.file.name))) })] }));
110
125
  }
@@ -1,2 +1,2 @@
1
- export { Input } from "./input.js";
1
+ export { Input, InputShell, } from "./input.js";
2
2
  export { inputRecipe } from "./input.recipe.js";
@@ -9,3 +9,33 @@ export const Input = React.forwardRef(({ size, invalid, className, style, suffix
9
9
  return (_jsxs("span", { className: inputRecipe({ size, invalid, className }), style: style, "data-readonly": props.readOnly ? "" : undefined, children: [_jsx("input", { ref: ref, className: "crisp-input-value", "aria-invalid": invalid === true || undefined, ...props }), suffix != null && _jsx("span", { className: "crisp-input-suffix", children: suffix })] }));
10
10
  });
11
11
  Input.displayName = "Input";
12
+ /**
13
+ * crisp#917: the same field chrome `Input` draws (border, focus ring,
14
+ * radius, size, invalid/read-only states) around arbitrary children instead
15
+ * of a hardcoded `<input>` — for a third-party editor (CodeMirror, a
16
+ * rich-text widget, a masked-input library) that needs to mount into a
17
+ * plain element rather than a native `<input>`.
18
+ *
19
+ * Deliberately NOT `asChild` (crisp's usual `Primitive`/`Slot` pattern, used
20
+ * by `Button`/`Chip`/`Avatar`/etc. elsewhere): those merge the shell's own
21
+ * props onto ONE child, replacing it outright — correct when the shell
22
+ * and the interactive surface are the same element. `Input`'s real shape is
23
+ * TWO parts, an outer shell hosting the `::after` focus ring plus an inner
24
+ * value that carries the actual text/interaction; a rich editor mounts its
25
+ * own DOM subtree inside a container div, which is exactly what `children`
26
+ * gives it, not what replacing a single node would.
27
+ *
28
+ * Only draws the OUTER shell (`inputRecipe`'s own class — border, ring,
29
+ * radius, size-driven height, background, `data-readonly` tint) around
30
+ * `children` unchanged, the same granularity GRC's own pre-existing
31
+ * workaround (internal/grc#159, reaching into `.crisp-input`/
32
+ * `.crisp-input--size_medium` by hand) already used — NOT `.crisp-input-
33
+ * value`'s own text-field resets (`border: none`, `padding-inline`, `font:
34
+ * inherit`, …), which assume a real `<input>` and would fight a rich
35
+ * editor's own internal layout. A child that wants to fill the shell the
36
+ * way `Input`'s own `<input>` does should set its own sizing (e.g. `flex: 1
37
+ * 1 auto; min-width: 0; width: 100%; height: 100%`), same as any block
38
+ * content dropped into an `inline-flex` container.
39
+ */
40
+ export const InputShell = React.forwardRef(({ size, invalid, className, style, suffix, readOnly, children, ...props }, ref) => (_jsxs("div", { ref: ref, className: inputRecipe({ size, invalid, className }), style: style, "data-readonly": readOnly ? "" : undefined, ...props, children: [children, suffix != null && _jsx("span", { className: "crisp-input-suffix", children: suffix })] })));
41
+ InputShell.displayName = "InputShell";
@@ -1,11 +1,13 @@
1
1
  import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
- import { contract as c, elevation as e } from "../../tokens/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
3
  // Geometry is measured: 32/36px tall, r8/r10, 10/12px side padding.
4
- // The shadow is `elevation.textField` (#466), not `elevation.field` — a real
5
- // typed field on Attio's own Settings page carries one 1px ring, not
6
- // `field`'s button-chrome glow/drop-shadow layers too; `field`'s own doc
7
- // comment had claimed "inputs, textareas" among its consumers, which this
8
- // corrects.
4
+ // your-moon/crisp#910: the rest-state edge used to be `elevation.textField`,
5
+ // a `box-shadow` ring (#466's fix for a real typed field's 1px ring, not
6
+ // `field`'s button-chrome glow/drop-shadow layers) -- now a real `border`,
7
+ // matching seed-design's own `text-input.yaml` (an actual border that
8
+ // THICKENS on focus/invalid, not a fixed-width ring that only recolors).
9
+ // `borderDefault` (#dcdee3) is seed's own `stroke.neutral-weak`, already the
10
+ // crisp token's exact value.
9
11
  // The FOCUS TREATMENT is seed-design's, measured from @seed-design/css: a 2px
10
12
  // ring on an ::after overlay that is present-but-transparent at rest, so
11
13
  // focusing cannot shift layout the way a width change would, and animates
@@ -20,7 +22,7 @@ const config = defineRecipe({
20
22
  name: "input",
21
23
  base: `
22
24
  position: relative; box-sizing: border-box; display: inline-flex; align-items: center;
23
- width: 100%; background: ${c.bgSurface}; box-shadow: ${e.textField};
25
+ width: 100%; background: ${c.bgSurface}; border: 1px solid ${c.borderDefault};
24
26
  font-family: ${c.font.sans}; font-size: ${c.text.lg}; line-height: 20px;
25
27
 
26
28
  &::after {
@@ -43,8 +45,10 @@ const config = defineRecipe({
43
45
  primary-email field -- a tinted field, not just muted text, driven off
44
46
  the native \`readOnly\` attribute. \`ringField\` (rgba(0,0,0,.06)) is an
45
47
  exact match for the measured ring; the 2%-fgPrimary tint reuses the
46
- same color-mix idiom every other soft crisp surface already uses. */
47
- &[data-readonly] { background: color-mix(in srgb, ${c.fgPrimary} 2%, transparent); box-shadow: inset 0 0 0 1px ${c.ringField}; }
48
+ same color-mix idiom every other soft crisp surface already uses.
49
+ \`border-color: transparent\` neutralises #910's new real rest border
50
+ so this measured ring isn't drawn twice. */
51
+ &[data-readonly] { background: color-mix(in srgb, ${c.fgPrimary} 2%, transparent); border-color: transparent; box-shadow: inset 0 0 0 1px ${c.ringField}; }
48
52
  & .crisp-input-suffix { flex-shrink: 0; margin-inline-start: ${c.space["1"]}; }
49
53
  `,
50
54
  variants: {
@@ -56,7 +60,10 @@ const config = defineRecipe({
56
60
  // padding `medium` already uses, just 4px shorter.
57
61
  sm: `height: ${c.size.row}; border-radius: ${c.radius.md}; & .crisp-input-value { padding-inline: ${c.space["2.5"]}; }`,
58
62
  medium: `height: 32px; border-radius: ${c.radius.md}; & .crisp-input-value { padding-inline: ${c.space["2.5"]}; }`,
59
- large: `height: 36px; border-radius: 10px; & .crisp-input-value { padding-inline: ${c.space["3"]}; }`,
63
+ // your-moon/crisp#910: was a stray literal \`10px\` -- seed's own
64
+ // large-size field radius is \`$radius.r3\` = 12px (\`text-input.yaml\`),
65
+ // matching Button's own \`lg\` fix in the same PR.
66
+ large: `height: 36px; border-radius: ${c.radius.xl}; & .crisp-input-value { padding-inline: ${c.space["3"]}; }`,
60
67
  },
61
68
  invalid: {
62
69
  // critical outranks focus: a field that is wrong stays visibly wrong
@@ -104,12 +104,28 @@ export function Menu({ trigger, children, align = "start", open: openProp, defau
104
104
  document.addEventListener("keydown", onKey);
105
105
  return () => document.removeEventListener("keydown", onKey);
106
106
  }, [open, close]);
107
- const { onKeyDown: onContentKeyDown } = useMenuKeyboard({
107
+ const { onKeyDown: onContentKeyDown, items } = useMenuKeyboard({
108
108
  open,
109
109
  close,
110
110
  dismiss: () => setOpen(false),
111
111
  contentRef,
112
112
  });
113
+ // Roving tabindex (crisp#964): every item hardcodes tabIndex={-1} for the
114
+ // arrow-key model above, which means none of them is ever a real Tab stop
115
+ // on its own -- kept in sync with wherever focus actually lands, same as
116
+ // MenuSheet's identical fix (#966).
117
+ const onContentFocus = React.useCallback((e) => {
118
+ for (const el of items())
119
+ el.tabIndex = -1;
120
+ e.target.tabIndex = 0;
121
+ }, [items]);
122
+ React.useEffect(() => {
123
+ if (!open)
124
+ return;
125
+ const [first] = items();
126
+ if (first)
127
+ first.tabIndex = 0;
128
+ }, [open, items]);
113
129
  const triggerEl = React.cloneElement(trigger, {
114
130
  ref: triggerRef,
115
131
  id: triggerId,
@@ -125,7 +141,7 @@ export function Menu({ trigger, children, align = "start", open: openProp, defau
125
141
  });
126
142
  return (_jsxs("div", { ref: rootRef, className: menuRecipe({ className }), children: [triggerEl, open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsx("div", { ref: contentRef, className: contentClassName
127
143
  ? `crisp-menu-content ${contentClassName}`
128
- : "crisp-menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, children: _jsx(MenuContext.Provider, { value: { close }, children: children }) }) }))] }));
144
+ : "crisp-menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, onFocus: onContentFocus, children: _jsx(MenuContext.Provider, { value: { close }, children: children }) }) }))] }));
129
145
  }
130
146
  Menu.displayName = "Menu";
131
147
  export function MenuItem({ children, icon, shortcut, count, danger, disabled, onSelect, closeOnSelect = true, }) {
@@ -1,11 +1,25 @@
1
1
  import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
2
  import { contract as c, elevation as e } from "../../tokens/index.js";
3
3
  // A dropdown menu — same popover surface used for ⋯ / sort / filter
4
- // menus (measured from listboxes): radius 12, 4px padding, a layered
5
- // soft shadow, over 32px items (radius 8, icon + label + optional shortcut /
6
- // check). The content portals to document.body (#339) so no ancestor's
7
- // transform/backdrop-filter can capture its position: fixed box; z-index 70
8
- // keeps it above Dialog/Drawer's 60/61.
4
+ // menus (measured from listboxes): 4px padding, over 32px items (radius 8,
5
+ // icon + label + optional shortcut / check). The content portals to
6
+ // document.body (#339) so no ancestor's transform/backdrop-filter can
7
+ // capture its position: fixed box; z-index 70 keeps it above Dialog/
8
+ // Drawer's 60/61.
9
+ //
10
+ // your-moon/crisp#934: radius and shadow are seed-design's,
11
+ // not Attio's -- Menu is the ONE component the seed raw dump confirms
12
+ // actually uses a shadow token at all (every other floating surface in
13
+ // seed's own spec has none). Radius moved from `radius.xl` (12px, this
14
+ // component's own private value) to `radius["3xl"]` (20px) -- seed treats
15
+ // "floating surface radius" as one shared value across Dialog/Menu/Select,
16
+ // not a per-component one; `radius["3xl"]` is already that shared token
17
+ // (Select's listbox, crisp/#867; Dialog, this same PR). Shadow moved from
18
+ // `elevation.floating` (crisp's own 4-layer blue-tinted glow, still used by
19
+ // Popover/ContextMenu/Toast/etc -- NOT touched here, out of scope) to
20
+ // `elevation.selectFloating` -- already seed's exact flat `$shadow.s3`
21
+ // value, already in this file (Select's own listbox, #867), reused rather
22
+ // than duplicated.
9
23
  //
10
24
  // The menu's floating content and everything inside it live in `globalCss`
11
25
  // below, not nested under `&` (#357): nesting compiles to a real descendant
@@ -32,8 +46,8 @@ const config = defineRecipe({
32
46
  font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
33
47
  position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
34
48
  min-width: 200px; max-height: 360px; overflow-y: auto; padding: ${c.space["1"]};
35
- border-radius: ${c.radius.xl}; background: ${c.bgPopover};
36
- box-shadow: ${e.floating};
49
+ border-radius: ${c.radius["3xl"]}; background: ${c.bgPopover};
50
+ box-shadow: ${e.selectFloating};
37
51
  transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease;
38
52
  }
39
53
  .crisp-menu-content[data-align="end"] { transform-origin: top right; }
@@ -0,0 +1,2 @@
1
+ export { MenuSheet, MenuSheetGroup, MenuSheetItem, } from "./menusheet.js";
2
+ export { menusheetRecipe } from "./menusheet.recipe.js";
@@ -0,0 +1,95 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as dialog from "@zag-js/dialog";
3
+ import { normalizeProps, Portal, useMachine } from "@zag-js/react";
4
+ import * as React from "react";
5
+ import { hasOpenFloatingLayer, withDescribedBy } from "../../core/index.js";
6
+ import { MENU_ITEM_SELECTOR, useMenuKeyboard } from "../menu/menu-keyboard.js";
7
+ import { menusheetRecipe } from "./menusheet.recipe.js";
8
+ const MenuSheetContext = React.createContext(null);
9
+ let instanceCounter = 0;
10
+ function useMenuSheetInstanceId() {
11
+ const idRef = React.useRef(null);
12
+ if (idRef.current == null) {
13
+ instanceCounter += 1;
14
+ idRef.current = `crisp-menusheet-${instanceCounter}`;
15
+ }
16
+ return idRef.current;
17
+ }
18
+ /**
19
+ * A mobile menu sheet -- a focus-trapped bottom sheet (same Zag dialog
20
+ * machine as BottomSheet) whose body is a role=menu list, with the identical
21
+ * keyboard model Menu already uses: arrow keys move, Home/End jump,
22
+ * typeahead, Enter/Space select, Escape closes. Compose items with
23
+ * MenuSheetItem, and group a run of them in one rounded card with
24
+ * MenuSheetGroup.
25
+ */
26
+ export function MenuSheet({ open, onOpenChange, title, description, footer, "aria-label": ariaLabel, children, className, "aria-describedby": describedBy, ...rest }) {
27
+ const id = useMenuSheetInstanceId();
28
+ const listRef = React.useRef(null);
29
+ const service = useMachine(dialog.machine, {
30
+ id,
31
+ open,
32
+ onOpenChange(details) {
33
+ onOpenChange(details.open);
34
+ },
35
+ "aria-label": typeof title === "string" ? title : ariaLabel,
36
+ onEscapeKeyDown(event) {
37
+ if (hasOpenFloatingLayer())
38
+ event.preventDefault();
39
+ },
40
+ // Without this, Zag's own focus trap parks initial focus on the dialog
41
+ // surface itself, which wins the race against useMenuKeyboard's rAF-
42
+ // scheduled "focus the first item" effect below -- the two compete for
43
+ // the same job and Zag's own dialog-level default was landing last.
44
+ // Pointing it at the first menu item directly makes Zag do that job
45
+ // itself instead of fighting useMenuKeyboard for it.
46
+ initialFocusEl() {
47
+ return (listRef.current?.querySelector(MENU_ITEM_SELECTOR) ?? null);
48
+ },
49
+ });
50
+ const api = dialog.connect(service, normalizeProps);
51
+ const close = React.useCallback(() => onOpenChange(false), [onOpenChange]);
52
+ const { onKeyDown: menuKeyDown, items } = useMenuKeyboard({
53
+ open: api.open,
54
+ close,
55
+ contentRef: listRef,
56
+ });
57
+ // Roving tabindex: exactly one item is a real Tab stop at a time (#237 --
58
+ // "selected, or first when none" -- is right in every state but the one
59
+ // that matters, so it is enforced imperatively here rather than trusted to
60
+ // a per-render default). useMenuKeyboard moves FOCUS with arrow keys; this
61
+ // keeps the tabindex in sync with wherever that focus actually lands,
62
+ // including the initial auto-focus Zag's own initialFocusEl performs.
63
+ const onFocus = React.useCallback((e) => {
64
+ for (const el of items())
65
+ el.tabIndex = -1;
66
+ e.target.tabIndex = 0;
67
+ }, [items]);
68
+ React.useEffect(() => {
69
+ if (!api.open)
70
+ return;
71
+ const [first] = items();
72
+ if (first)
73
+ first.tabIndex = 0;
74
+ }, [api.open, items]);
75
+ const ctx = React.useMemo(() => ({ close }), [close]);
76
+ if (!api.open)
77
+ return null;
78
+ return (_jsx(Portal, { children: _jsxs("div", { className: menusheetRecipe(), children: [_jsx("div", { ...api.getBackdropProps(), className: "crisp-menusheet-backdrop" }), _jsx("div", { ...api.getPositionerProps(), className: "crisp-menusheet-positioner", children: _jsxs("div", { ...rest, ...withDescribedBy(api.getContentProps(), describedBy), className: className
79
+ ? `crisp-menusheet-content ${className}`
80
+ : "crisp-menusheet-content", children: [(title != null || description != null) && (_jsxs("div", { className: "crisp-menusheet-header", children: [title != null && (_jsx("span", { ...api.getTitleProps(), className: "crisp-menusheet-title", children: title })), description != null && (_jsx("span", { className: "crisp-menusheet-description", children: description }))] })), _jsx(MenuSheetContext.Provider, { value: ctx, children: _jsx("div", { ref: listRef, role: "menu", className: "crisp-menusheet-list", onKeyDown: menuKeyDown, onFocus: onFocus, children: children }) }), footer != null && (_jsx("div", { className: "crisp-menusheet-footer", children: footer }))] }) })] }) }));
81
+ }
82
+ MenuSheet.displayName = "MenuSheet";
83
+ export function MenuSheetGroup({ children }) {
84
+ return _jsx("div", { className: "crisp-menusheet-group", children: children });
85
+ }
86
+ MenuSheetGroup.displayName = "MenuSheetGroup";
87
+ export function MenuSheetItem({ children, icon, description, tone = "neutral", disabled, onSelect, closeOnSelect = true, }) {
88
+ const ctx = React.useContext(MenuSheetContext);
89
+ return (_jsxs("button", { type: "button", role: "menuitem", tabIndex: -1, disabled: disabled, "data-disabled": disabled ? "" : undefined, "data-tone": tone, className: "crisp-menusheet-item", onClick: () => {
90
+ onSelect?.();
91
+ if (closeOnSelect)
92
+ ctx?.close();
93
+ }, children: [icon != null && (_jsx("span", { className: "crisp-menusheet-item-icon", children: icon })), _jsxs("span", { className: "crisp-menusheet-item-content", children: [_jsx("span", { className: "crisp-menusheet-item-label", children: children }), description != null && (_jsx("span", { className: "crisp-menusheet-item-description", children: description }))] })] }));
94
+ }
95
+ MenuSheetItem.displayName = "MenuSheetItem";
@@ -0,0 +1,100 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // A mobile menu sheet -- daangn/seed-design components menu-sheet.yaml and
4
+ // menu-sheet-item.yaml (dev at e1c486b904297f7b6a8f17b8523f732c37015d60), not
5
+ // Attio (no such component there; crisp#911). Same @zag-js/dialog machine as
6
+ // BottomSheet, and the identical role=menu keyboard model Menu already uses
7
+ // (useMenuKeyboard, shared rather than reimplemented). Differs from
8
+ // BottomSheet in every slot value: r5 radius (not r6), content-level padding
9
+ // (not per-slot), no header alignment / close-button matrix at all -- menu
10
+ // sheet has neither in its own schema. Every numeric value cites its yaml
11
+ // line. NOTE: no apostrophes in this file -- one broke the recipe compiler
12
+ // silently on BottomSheet, see AUTHORING.md Lesson 64.
13
+ const config = defineRecipe({
14
+ name: "menusheet",
15
+ base: `display: contents; font-family: ${c.font.sans};
16
+ & .crisp-menusheet-backdrop {
17
+ position: fixed; inset: 0; z-index: 60;
18
+ background: ${c.bgOverlayStrong}; /* menu-sheet.yaml:95 backdrop.color = $color.bg.overlay */
19
+ opacity: 1; transition: opacity 100ms cubic-bezier(0,0,.15,1); /* menu-sheet.yaml:96-98 enterDuration/enterTimingFunction = $duration.d2/$timing-function.enter */
20
+ @starting-style { opacity: 0; }
21
+ }
22
+ & .crisp-menusheet-positioner {
23
+ position: fixed; inset: 0; z-index: 61;
24
+ display: flex; align-items: flex-end; justify-content: center;
25
+ }
26
+ & .crisp-menusheet-content {
27
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
28
+ position: relative; box-sizing: border-box; display: flex; flex-direction: column;
29
+ overflow: hidden; width: 100%; max-width: 480px; /* menu-sheet.yaml:104 content.maxWidth */
30
+ max-height: 85vh;
31
+ background: ${c.bgSurface}; /* menu-sheet.yaml:103 content.color = $color.bg.layer-floating */
32
+ padding: ${c.space["6"]} ${c.space["4"]} ${c.space["4"]}; /* menu-sheet.yaml:105-107 content.paddingX/paddingTop/paddingBottom = global-gutter/x6/x4 (16px/24px/16px) */
33
+ border-radius: ${c.radius["3xl"]} ${c.radius["3xl"]} 0 0; /* menu-sheet.yaml:108 content.topCornerRadius = $radius.r5 (20px) */
34
+ transform: translateY(0);
35
+ transition: transform 300ms cubic-bezier(.03,.4,.1,1); /* menu-sheet.yaml:109-110 content.enterDuration/enterTimingFunction (enter-expressive) */
36
+ @starting-style { transform: translateY(100%); }
37
+ }
38
+ & .crisp-menusheet-header {
39
+ display: flex; flex-direction: column;
40
+ gap: ${c.space["1"]}; /* menu-sheet.yaml:115 header.gap = $dimension.x1 (4px) */
41
+ padding-bottom: ${c.space["4"]}; /* menu-sheet.yaml:116 header.paddingBottom = $dimension.x4 (16px) */
42
+ flex-shrink: 0;
43
+ }
44
+ & .crisp-menusheet-title {
45
+ color: ${c.fgPrimary}; /* menu-sheet.yaml:121 title.color = $color.fg.neutral */
46
+ font-size: 18px; line-height: 24px; font-weight: ${c.weight.bold}; /* menu-sheet.yaml:118-120 title.fontSize/lineHeight/fontWeight = $font-size.t6/$line-height.t6/bold */
47
+ }
48
+ & .crisp-menusheet-description {
49
+ color: ${c.fgMuted}; /* menu-sheet.yaml:126 description.color = $color.fg.neutral-muted */
50
+ font-size: ${c.text.lg}; line-height: 19px; font-weight: ${c.weight.regular}; /* menu-sheet.yaml:123-125 description.fontSize/lineHeight/fontWeight = $font-size.t4/$line-height.t4/regular */
51
+ }
52
+ & .crisp-menusheet-list {
53
+ display: flex; flex-direction: column;
54
+ gap: ${c.space["2.5"]}; /* menu-sheet.yaml:129 list.gap = $dimension.x2_5 (10px) */
55
+ overflow-y: auto; min-height: 0;
56
+ }
57
+ & .crisp-menusheet-group {
58
+ display: flex; flex-direction: column;
59
+ border-radius: ${c.radius["2xl"]}; overflow: hidden; /* menu-sheet.yaml:131 group.cornerRadius = $radius.r4 (16px) */
60
+ }
61
+ & .crisp-menusheet-group > * + * { border-top: 1px solid ${c.strokeGroupDivider}; } /* menu-sheet.yaml:132-134 divider.strokeBottomWidth/strokeColor = 1px/$color.stroke.neutral-muted */
62
+ & .crisp-menusheet-footer {
63
+ flex-shrink: 0;
64
+ padding-top: ${c.space["2.5"]}; /* menu-sheet.yaml:136 footer.paddingTop = $dimension.x2_5 (10px) */
65
+ }
66
+ & .crisp-menusheet-item {
67
+ position: relative; box-sizing: border-box; display: flex; align-items: center;
68
+ width: 100%; min-height: 52px; /* menu-sheet-item.yaml:72 root.minHeight */
69
+ padding: ${c.space["3.5"]} ${c.space["4"]}; gap: ${c.space["3.5"]}; /* menu-sheet-item.yaml:73-75 root.paddingX/paddingY/gap = x4/x3_5/x3_5 (16px/14px/14px) */
70
+ border: none; margin: 0; cursor: pointer; text-align: start;
71
+ background: ${c.bgNeutralWeak}; /* menu-sheet-item.yaml:71 root.color = $color.bg.neutral-weak */
72
+ font-family: inherit;
73
+ }
74
+ & .crisp-menusheet-item:hover,
75
+ & .crisp-menusheet-item:active { background: color-mix(in srgb, ${c.fgPrimary} 6%, ${c.bgNeutralWeak}); } /* menu-sheet-item.yaml:90-91 pressed.root.color = $color.bg.neutral-weak-pressed, approximated as a tint over the base fill */
76
+ & .crisp-menusheet-item:active .crisp-menusheet-item-content { transform: scale(0.98); } /* menu-sheet-item.yaml:92 pressed.root.scaleScope = content */
77
+ & .crisp-menusheet-item-icon { flex-shrink: 0; display: inline-flex; width: 22px; height: 22px; } /* menu-sheet-item.yaml:77 prefixIcon.size */
78
+ & .crisp-menusheet-item-content {
79
+ flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column;
80
+ gap: ${c.space["0.5"]}; /* menu-sheet-item.yaml:79 content.gap = $dimension.x0_5 (2px) */
81
+ transition: transform 150ms cubic-bezier(0,0,.15,1);
82
+ }
83
+ & .crisp-menusheet-item-label {
84
+ font-size: ${c.text.xl}; line-height: 22px; font-weight: ${c.weight.regular}; /* menu-sheet-item.yaml:81-83 label.fontSize/lineHeight/fontWeight = $font-size.t5/$line-height.t5/regular */
85
+ }
86
+ & .crisp-menusheet-item-description {
87
+ color: ${c.fgNeutralSubtle}; /* menu-sheet-item.yaml:88 description.color = $color.fg.neutral-subtle */
88
+ font-size: ${c.text.md}; line-height: 18px; font-weight: ${c.weight.medium}; /* menu-sheet-item.yaml:85-87 description.fontSize/lineHeight/fontWeight = $font-size.t3/$line-height.t3/medium */
89
+ }
90
+ & .crisp-menusheet-item[data-tone="neutral"] .crisp-menusheet-item-icon,
91
+ & .crisp-menusheet-item[data-tone="neutral"] .crisp-menusheet-item-label { color: ${c.fgPrimary}; } /* menu-sheet-item.yaml:93-98 tone=neutral */
92
+ & .crisp-menusheet-item[data-tone="critical"] .crisp-menusheet-item-icon,
93
+ & .crisp-menusheet-item[data-tone="critical"] .crisp-menusheet-item-label { color: ${c.fgDanger}; } /* menu-sheet-item.yaml:99-104 tone=critical */
94
+ `,
95
+ variants: {},
96
+ defaultVariants: {},
97
+ compoundVariants: [],
98
+ });
99
+ export const menusheetRecipe = resolveRecipe(config);
100
+ export default config;
@@ -186,7 +186,7 @@ id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPl
186
186
  }, children: [_jsx("span", { className: "crisp-multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "crisp-multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "crisp-multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-multiselect-search", children: [_jsx(Search, { size: 16, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
187
187
  setQuery(e.target.value);
188
188
  setHighlight(0);
189
- }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "crisp-multiselect-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
189
+ }, onKeyDown: onNavKeys })] })), searchable && _jsx("div", { className: "crisp-multiselect-divider" }), filtered.length === 0 ? (_jsx("div", { className: "crisp-multiselect-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
190
190
  const sel = selectedSet.has(opt.value);
191
191
  // #343: a header renders once per run of same-`group`
192
192
  // options, so consecutive rows sharing a group render one
@@ -13,10 +13,14 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
13
13
  // styling (shared with RecordsTable/DataTable elsewhere), the checkbox-style
14
14
  // selected indicator, and the toggle-without-closing interaction.
15
15
  //
16
- // The search row's geometry (36px height, 6px gap, no divider) is an
17
- // Attio measurement (#481) `build/spec-check.mjs` guards directly -- it is
18
- // NOT part of the seed-parity change and must not move. Only its text/icon
19
- // colours follow the rest of this file onto the shared tokens.
16
+ // The search row now follows seed too (owner decision, 2026-09-26: Select
17
+ // AND MultiSelect both follow seed-design, superseding #481's old Attio
18
+ // lock on this row's geometry -- crisp/#904's MultiSelect follow-up). It
19
+ // reads the exact same composed @seed-design/rootage `text-input.yaml`
20
+ // (variant=outline,size=medium) values #913 resolved for Select's own
21
+ // search row -- see that recipe's matching comment for the full citation.
22
+ // `build/spec-check.mjs`'s guard was rewritten to assert these seed values
23
+ // instead of the old 36px Attio measurement.
20
24
  //
21
25
  // The listbox portals to `document.body` (#339), so its rules live in
22
26
  // `globalCss`, not nested under `&` -- see Select's own recipe for the full
@@ -61,13 +65,19 @@ const config = defineRecipe({
61
65
  }
62
66
  .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
63
67
  .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.95); } }
64
- /* LOCKED geometry (#481, guarded by build/spec-check.mjs): height 36px,
65
- gap space["1.5"] (6px), no border-bottom -- an Attio measurement, not
66
- part of this seed-parity pass. Only colours below follow Select. */
67
- .crisp-multiselect-search { display: flex; align-items: center; gap: ${c.space["1.5"]}; height: 36px; flex-shrink: 0; margin: calc(-1 * ${c.space["2"]}) calc(-1 * ${c.space["2"]}) ${c.space["2"]}; padding: ${c.space["2"]} ${c.space["3"]}; color: ${c.fgNeutralMuted}; }
68
+ /* Seed geometry (build/spec-check.mjs guards these four properties
69
+ directly), superseding #481's old Attio lock -- see this file's
70
+ header comment and Select's matching .crisp-select-search comment
71
+ for the full citation. An ordinary flex child of the listbox, so its
72
+ own 8px gap provides the inset/seam on every side, same as Select. */
73
+ .crisp-multiselect-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: ${c.size.selectTriggerMd}; flex-shrink: 0; padding: 0 ${c.space["3.5"]}; border: 1px solid ${c.strokeSelectTrigger}; border-radius: ${c.radius.md}; color: ${c.fgNeutralMuted}; }
68
74
  .crisp-multiselect-search svg { flex-shrink: 0; }
69
- .crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgSelectNeutral}; }
75
+ .crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; line-height: 19px; color: ${c.fgSelectNeutral}; }
70
76
  .crisp-multiselect-search input::placeholder { color: ${c.fgSelectPlaceholder}; }
77
+ /* Same header/body seam as Select's own .crisp-select-divider -- see
78
+ that recipe's comment for the seed source (select.yaml's divider
79
+ slot, stroke.neutral-muted). */
80
+ .crisp-multiselect-divider { flex-shrink: 0; height: 1px; margin-inline: ${c.space["4"]}; background: ${c.strokeSelectDivider}; }
71
81
  /* Same groupLabel treatment as Select's (13px/regular/neutral-subtle). */
72
82
  .crisp-multiselect-label { padding: ${c.space["2"]} ${c.space["4"]} 0; font-size: ${c.text.md}; line-height: 18px; font-weight: ${c.weight.regular}; color: ${c.fgNeutralSubtle}; }
73
83
  .crisp-multiselect-option {
@@ -103,6 +113,20 @@ const config = defineRecipe({
103
113
  // default: `c.size.menuRow` (32px) and the original, pre-#882
104
114
  // Attio-measured asymmetric padding for a chip-wrapping trigger.
105
115
  // `medium`/`large` (seed's own 40/52px) stay, opt-in.
116
+ //
117
+ // crisp/#904 (MultiSelect follow-up, briefly moved then reverted, same
118
+ // ruling as Select's own): `small`'s `gap` moved from `space["1"]`
119
+ // (4px) to `space["2"]` (8px), matching `medium`/`large`'s seed gap --
120
+ // team-lead's review ruling on Select's matching change reversed it:
121
+ // padding AND gap are SPACING, and the owner's direction is "sizes/
122
+ // spacing from Attio, look from seed", not seed's literal gap number.
123
+ // Back to `space["1"]` (4px), the original pre-#882 Attio-measured
124
+ // value. `padding` was never touched either way: the pre-#882,
125
+ // Attio-measured asymmetric padding a chip-wrapping trigger needs
126
+ // (chips sit close to the start edge, the chevron gets more room at
127
+ // the end) -- seed has no multi-value combobox of its own to source a
128
+ // replacement number from (see this file's header comment) even if
129
+ // the ruling had gone the other way.
106
130
  size: {
107
131
  small: `
108
132
  & .crisp-multiselect-trigger { min-height: ${c.size.menuRow}; border-radius: ${c.radius.md}; gap: ${c.space["1"]}; padding: ${c.space["0.5"]} ${c.space["1.5"]} ${c.space["0.5"]} ${c.space["2"]}; }
@@ -185,7 +185,7 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
185
185
  setQuery(e.target.value);
186
186
  setHighlight(0);
187
187
  onSearchChange?.(e.target.value);
188
- }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "crisp-select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
188
+ }, onKeyDown: onNavKeys })] })), searchable && _jsx("div", { className: "crisp-select-divider" }), filtered.length === 0 ? (_jsx("div", { className: "crisp-select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
189
189
  const isSel = opt.value === selected;
190
190
  // #343: a header renders once per run of same-`group`
191
191
  // options, so consecutive rows sharing a group render one