@crisp-ui-kit/crisp 0.57.0 → 0.57.2

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 (293) hide show
  1. package/dist/accentpicker.css +5 -0
  2. package/dist/accentpicker.d.ts +7 -0
  3. package/dist/accentpicker.layered.css +7 -0
  4. package/dist/authshell.css +9 -4
  5. package/dist/authshell.layered.css +9 -4
  6. package/dist/avatar.css +1 -1
  7. package/dist/avatar.layered.css +1 -1
  8. package/dist/badge.css +5 -5
  9. package/dist/badge.layered.css +5 -5
  10. package/dist/board.css +28 -8
  11. package/dist/board.layered.css +28 -8
  12. package/dist/bottomsheet.css +26 -0
  13. package/dist/bottomsheet.d.ts +7 -0
  14. package/dist/bottomsheet.layered.css +28 -0
  15. package/dist/button.css +9 -8
  16. package/dist/button.layered.css +9 -8
  17. package/dist/checkbox.css +7 -3
  18. package/dist/checkbox.layered.css +7 -3
  19. package/dist/chip.css +15 -0
  20. package/dist/chip.d.ts +3 -1
  21. package/dist/chip.layered.css +15 -0
  22. package/dist/chipgroup.css +5 -0
  23. package/dist/chipgroup.d.ts +7 -0
  24. package/dist/chipgroup.layered.css +7 -0
  25. package/dist/cjs/components/accentpicker/accentpicker.js +66 -0
  26. package/dist/cjs/components/accentpicker/accentpicker.recipe.js +25 -0
  27. package/dist/cjs/components/accentpicker/index.js +7 -0
  28. package/dist/cjs/components/avatar/avatar.recipe.js +4 -1
  29. package/dist/cjs/components/badge/badge.recipe.js +38 -10
  30. package/dist/cjs/components/bottomsheet/bottomsheet.js +90 -0
  31. package/dist/cjs/components/bottomsheet/bottomsheet.recipe.js +77 -0
  32. package/dist/cjs/components/bottomsheet/index.js +9 -0
  33. package/dist/cjs/components/button/button.recipe.js +55 -21
  34. package/dist/cjs/components/checkbox/checkbox.recipe.js +61 -9
  35. package/dist/cjs/components/chip/chip.js +14 -2
  36. package/dist/cjs/components/chip/chip.recipe.js +69 -1
  37. package/dist/cjs/components/chipgroup/chipgroup.js +51 -0
  38. package/dist/cjs/components/chipgroup/chipgroup.recipe.js +36 -0
  39. package/dist/cjs/components/chipgroup/index.js +7 -0
  40. package/dist/cjs/components/datatable/datatable.js +5 -1
  41. package/dist/cjs/components/datatable/datatable.recipe.js +11 -2
  42. package/dist/cjs/components/dialog/dialog.recipe.js +21 -10
  43. package/dist/cjs/components/emptystate/emptystate.recipe.js +7 -2
  44. package/dist/cjs/components/fileupload/fileupload.js +21 -4
  45. package/dist/cjs/components/input/index.js +2 -1
  46. package/dist/cjs/components/input/input.js +33 -3
  47. package/dist/cjs/components/input/input.recipe.js +22 -7
  48. package/dist/cjs/components/menu/menu.recipe.js +21 -7
  49. package/dist/cjs/components/menusheet/index.js +9 -0
  50. package/dist/cjs/components/menusheet/menusheet.js +133 -0
  51. package/dist/cjs/components/menusheet/menusheet.recipe.js +103 -0
  52. package/dist/cjs/components/multiselect/multiselect.js +2 -2
  53. package/dist/cjs/components/multiselect/multiselect.recipe.js +45 -12
  54. package/dist/cjs/components/progress/progress.js +21 -4
  55. package/dist/cjs/components/progress/progress.recipe.js +30 -0
  56. package/dist/cjs/components/select/select.js +3 -3
  57. package/dist/cjs/components/select/select.recipe.js +63 -10
  58. package/dist/cjs/components/snackbar/index.js +8 -0
  59. package/dist/cjs/components/snackbar/snackbar.js +121 -0
  60. package/dist/cjs/components/snackbar/snackbar.recipe.js +71 -0
  61. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +7 -2
  62. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +37 -12
  63. package/dist/cjs/components/tabs/tabs.js +10 -4
  64. package/dist/cjs/components/tabs/tabs.recipe.js +23 -6
  65. package/dist/cjs/components/text/text.recipe.js +8 -7
  66. package/dist/cjs/components/textarea/textarea.js +3 -2
  67. package/dist/cjs/components/textarea/textarea.recipe.js +13 -9
  68. package/dist/cjs/components/textlink/textlink.recipe.js +6 -3
  69. package/dist/cjs/index.js +8 -0
  70. package/dist/cjs/product/authshell/authshell.recipe.js +5 -1
  71. package/dist/cjs/product/command/command.recipe.js +4 -1
  72. package/dist/cjs/product/comments/comments.js +7 -2
  73. package/dist/cjs/product/comments/comments.recipe.js +25 -7
  74. package/dist/cjs/product/memberssettings/index.js +6 -0
  75. package/dist/cjs/product/memberssettings/memberssettings.js +99 -0
  76. package/dist/cjs/product/memberssettings/memberssettings.recipe.js +83 -0
  77. package/dist/cjs/product/notificationssettings/index.js +5 -0
  78. package/dist/cjs/product/notificationssettings/notificationssettings.js +19 -0
  79. package/dist/cjs/product/notificationssettings/notificationssettings.recipe.js +44 -0
  80. package/dist/cjs/product/recordpage/recordpage.js +101 -9
  81. package/dist/cjs/product/recordpage/recordpage.recipe.js +43 -6
  82. package/dist/cjs/product/workspacegeneralsettings/index.js +5 -0
  83. package/dist/cjs/product/workspacegeneralsettings/workspacegeneralsettings.js +21 -0
  84. package/dist/cjs/product/workspacegeneralsettings/workspacegeneralsettings.recipe.js +51 -0
  85. package/dist/cjs/tokens/contract.js +1 -0
  86. package/dist/cjs/tokens/elevation.js +8 -5
  87. package/dist/cjs/tokens/palette.js +107 -33
  88. package/dist/cjs/tokens/scale.js +45 -2
  89. package/dist/cjs/tokens/semantic.js +26 -0
  90. package/dist/columncalc.css +15 -7
  91. package/dist/columncalc.layered.css +15 -7
  92. package/dist/command.css +23 -19
  93. package/dist/command.layered.css +23 -19
  94. package/dist/comments.css +28 -19
  95. package/dist/comments.d.ts +2 -2
  96. package/dist/comments.layered.css +28 -19
  97. package/dist/datatable.css +161 -67
  98. package/dist/datatable.layered.css +161 -67
  99. package/dist/datepicker.css +13 -41
  100. package/dist/datepicker.layered.css +13 -41
  101. package/dist/datepickerfield.css +4 -1
  102. package/dist/datepickerfield.layered.css +4 -1
  103. package/dist/dialog.css +12 -7
  104. package/dist/dialog.layered.css +12 -7
  105. package/dist/emptystate.css +5 -3
  106. package/dist/emptystate.layered.css +5 -3
  107. package/dist/esm/components/accentpicker/accentpicker.js +30 -0
  108. package/dist/esm/components/accentpicker/accentpicker.recipe.js +22 -0
  109. package/dist/esm/components/accentpicker/index.js +2 -0
  110. package/dist/esm/components/avatar/avatar.recipe.js +4 -1
  111. package/dist/esm/components/badge/badge.recipe.js +38 -10
  112. package/dist/esm/components/bottomsheet/bottomsheet.js +52 -0
  113. package/dist/esm/components/bottomsheet/bottomsheet.recipe.js +74 -0
  114. package/dist/esm/components/bottomsheet/index.js +2 -0
  115. package/dist/esm/components/button/button.recipe.js +56 -22
  116. package/dist/esm/components/checkbox/checkbox.recipe.js +61 -9
  117. package/dist/esm/components/chip/chip.js +14 -2
  118. package/dist/esm/components/chip/chip.recipe.js +69 -1
  119. package/dist/esm/components/chipgroup/chipgroup.js +15 -0
  120. package/dist/esm/components/chipgroup/chipgroup.recipe.js +33 -0
  121. package/dist/esm/components/chipgroup/index.js +2 -0
  122. package/dist/esm/components/datatable/datatable.js +5 -1
  123. package/dist/esm/components/datatable/datatable.recipe.js +11 -2
  124. package/dist/esm/components/dialog/dialog.recipe.js +22 -11
  125. package/dist/esm/components/emptystate/emptystate.recipe.js +7 -2
  126. package/dist/esm/components/fileupload/fileupload.js +20 -5
  127. package/dist/esm/components/input/index.js +1 -1
  128. package/dist/esm/components/input/input.js +33 -3
  129. package/dist/esm/components/input/input.recipe.js +23 -8
  130. package/dist/esm/components/menu/menu.recipe.js +21 -7
  131. package/dist/esm/components/menusheet/index.js +2 -0
  132. package/dist/esm/components/menusheet/menusheet.js +95 -0
  133. package/dist/esm/components/menusheet/menusheet.recipe.js +100 -0
  134. package/dist/esm/components/multiselect/multiselect.js +2 -2
  135. package/dist/esm/components/multiselect/multiselect.recipe.js +45 -12
  136. package/dist/esm/components/progress/progress.js +22 -5
  137. package/dist/esm/components/progress/progress.recipe.js +30 -0
  138. package/dist/esm/components/select/select.js +3 -3
  139. package/dist/esm/components/select/select.recipe.js +63 -10
  140. package/dist/esm/components/snackbar/index.js +2 -0
  141. package/dist/esm/components/snackbar/snackbar.js +84 -0
  142. package/dist/esm/components/snackbar/snackbar.recipe.js +68 -0
  143. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +7 -2
  144. package/dist/esm/components/tabletoolbar/tabletoolbar.js +37 -12
  145. package/dist/esm/components/tabs/tabs.js +10 -4
  146. package/dist/esm/components/tabs/tabs.recipe.js +23 -6
  147. package/dist/esm/components/text/text.recipe.js +8 -7
  148. package/dist/esm/components/textarea/textarea.js +3 -2
  149. package/dist/esm/components/textarea/textarea.recipe.js +14 -10
  150. package/dist/esm/components/textlink/textlink.recipe.js +6 -3
  151. package/dist/esm/index.js +8 -0
  152. package/dist/esm/product/authshell/authshell.recipe.js +5 -1
  153. package/dist/esm/product/command/command.recipe.js +4 -1
  154. package/dist/esm/product/comments/comments.js +7 -2
  155. package/dist/esm/product/comments/comments.recipe.js +25 -7
  156. package/dist/esm/product/memberssettings/index.js +1 -0
  157. package/dist/esm/product/memberssettings/memberssettings.js +62 -0
  158. package/dist/esm/product/memberssettings/memberssettings.recipe.js +80 -0
  159. package/dist/esm/product/notificationssettings/index.js +1 -0
  160. package/dist/esm/product/notificationssettings/notificationssettings.js +16 -0
  161. package/dist/esm/product/notificationssettings/notificationssettings.recipe.js +41 -0
  162. package/dist/esm/product/recordpage/recordpage.js +102 -10
  163. package/dist/esm/product/recordpage/recordpage.recipe.js +43 -6
  164. package/dist/esm/product/workspacegeneralsettings/index.js +1 -0
  165. package/dist/esm/product/workspacegeneralsettings/workspacegeneralsettings.js +18 -0
  166. package/dist/esm/product/workspacegeneralsettings/workspacegeneralsettings.recipe.js +48 -0
  167. package/dist/esm/tokens/contract.js +2 -1
  168. package/dist/esm/tokens/elevation.js +8 -5
  169. package/dist/esm/tokens/palette.js +107 -33
  170. package/dist/esm/tokens/scale.js +44 -1
  171. package/dist/esm/tokens/semantic.js +26 -0
  172. package/dist/fileupload.css +3 -1
  173. package/dist/fileupload.layered.css +3 -1
  174. package/dist/input.css +11 -2
  175. package/dist/input.layered.css +11 -2
  176. package/dist/listpage.css +3 -1
  177. package/dist/listpage.layered.css +3 -1
  178. package/dist/memberssettings.css +67 -0
  179. package/dist/memberssettings.d.ts +7 -0
  180. package/dist/memberssettings.layered.css +69 -0
  181. package/dist/menu.css +2 -2
  182. package/dist/menu.layered.css +2 -2
  183. package/dist/menusheet.css +36 -0
  184. package/dist/menusheet.d.ts +7 -0
  185. package/dist/menusheet.layered.css +38 -0
  186. package/dist/multiselect.css +17 -6
  187. package/dist/multiselect.d.ts +2 -2
  188. package/dist/multiselect.layered.css +17 -6
  189. package/dist/native/tokens.d.ts +2 -1
  190. package/dist/native/tokens.js +53 -25
  191. package/dist/notificationssettings.css +22 -0
  192. package/dist/notificationssettings.d.ts +7 -0
  193. package/dist/notificationssettings.layered.css +24 -0
  194. package/dist/objectsettings.css +4 -4
  195. package/dist/objectsettings.layered.css +4 -4
  196. package/dist/progress.css +3 -0
  197. package/dist/progress.layered.css +3 -0
  198. package/dist/recordpage.css +67 -23
  199. package/dist/recordpage.layered.css +67 -23
  200. package/dist/recordpanel.css +35 -16
  201. package/dist/recordpanel.layered.css +35 -16
  202. package/dist/resizablepanel.css +8 -5
  203. package/dist/resizablepanel.layered.css +8 -5
  204. package/dist/richtext.css +24 -8
  205. package/dist/richtext.layered.css +24 -8
  206. package/dist/savedviews.css +9 -2
  207. package/dist/savedviews.layered.css +9 -2
  208. package/dist/select.css +47 -7
  209. package/dist/select.d.ts +2 -2
  210. package/dist/select.layered.css +47 -7
  211. package/dist/settingsshell.css +2 -3
  212. package/dist/settingsshell.layered.css +2 -3
  213. package/dist/slider.css +6 -27
  214. package/dist/slider.layered.css +6 -27
  215. package/dist/snackbar.css +24 -0
  216. package/dist/snackbar.d.ts +7 -0
  217. package/dist/snackbar.layered.css +26 -0
  218. package/dist/statcard.css +3 -1
  219. package/dist/statcard.layered.css +3 -1
  220. package/dist/styles.css +927 -365
  221. package/dist/styles.layered.css +927 -365
  222. package/dist/table.css +28 -10
  223. package/dist/table.layered.css +28 -10
  224. package/dist/tabletoolbar.css +35 -24
  225. package/dist/tabletoolbar.layered.css +35 -24
  226. package/dist/tabs.css +18 -4
  227. package/dist/tabs.layered.css +18 -4
  228. package/dist/text.css +1 -1
  229. package/dist/text.layered.css +1 -1
  230. package/dist/textarea.css +4 -7
  231. package/dist/textarea.layered.css +4 -7
  232. package/dist/textlink.css +2 -2
  233. package/dist/textlink.layered.css +2 -2
  234. package/dist/themepicker.css +2 -1
  235. package/dist/themepicker.layered.css +2 -1
  236. package/dist/tokens.css +31 -13
  237. package/dist/tokens.json +53 -25
  238. package/dist/types/components/accentpicker/accentpicker.d.ts +23 -0
  239. package/dist/types/components/accentpicker/accentpicker.recipe.d.ts +3 -0
  240. package/dist/types/components/accentpicker/index.d.ts +2 -0
  241. package/dist/types/components/bottomsheet/bottomsheet.d.ts +35 -0
  242. package/dist/types/components/bottomsheet/bottomsheet.recipe.d.ts +3 -0
  243. package/dist/types/components/bottomsheet/index.d.ts +2 -0
  244. package/dist/types/components/button/button.recipe.d.ts +13 -8
  245. package/dist/types/components/checkbox/checkbox.recipe.d.ts +28 -5
  246. package/dist/types/components/chip/chip.d.ts +22 -0
  247. package/dist/types/components/chip/chip.recipe.d.ts +48 -0
  248. package/dist/types/components/chipgroup/chipgroup.d.ts +14 -0
  249. package/dist/types/components/chipgroup/chipgroup.recipe.d.ts +5 -0
  250. package/dist/types/components/chipgroup/index.d.ts +2 -0
  251. package/dist/types/components/datatable/datatable.d.ts +19 -0
  252. package/dist/types/components/fileupload/fileupload.d.ts +26 -0
  253. package/dist/types/components/input/index.d.ts +1 -1
  254. package/dist/types/components/input/input.d.ts +84 -0
  255. package/dist/types/components/menusheet/index.d.ts +2 -0
  256. package/dist/types/components/menusheet/menusheet.d.ts +42 -0
  257. package/dist/types/components/menusheet/menusheet.recipe.d.ts +3 -0
  258. package/dist/types/components/multiselect/multiselect.d.ts +4 -3
  259. package/dist/types/components/multiselect/multiselect.recipe.d.ts +3 -0
  260. package/dist/types/components/progress/progress.d.ts +16 -4
  261. package/dist/types/components/progress/progress.recipe.d.ts +22 -0
  262. package/dist/types/components/select/select.d.ts +6 -5
  263. package/dist/types/components/select/select.recipe.d.ts +3 -0
  264. package/dist/types/components/snackbar/index.d.ts +2 -0
  265. package/dist/types/components/snackbar/snackbar.d.ts +36 -0
  266. package/dist/types/components/snackbar/snackbar.recipe.d.ts +5 -0
  267. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +28 -1
  268. package/dist/types/components/tabs/tabs.d.ts +11 -1
  269. package/dist/types/components/textarea/textarea.d.ts +16 -0
  270. package/dist/types/index.d.ts +8 -0
  271. package/dist/types/product/comments/comments.d.ts +9 -3
  272. package/dist/types/product/comments/comments.recipe.d.ts +12 -2
  273. package/dist/types/product/memberssettings/index.d.ts +1 -0
  274. package/dist/types/product/memberssettings/memberssettings.d.ts +85 -0
  275. package/dist/types/product/memberssettings/memberssettings.recipe.d.ts +3 -0
  276. package/dist/types/product/notificationssettings/index.d.ts +1 -0
  277. package/dist/types/product/notificationssettings/notificationssettings.d.ts +37 -0
  278. package/dist/types/product/notificationssettings/notificationssettings.recipe.d.ts +3 -0
  279. package/dist/types/product/recordpage/recordpage.d.ts +89 -2
  280. package/dist/types/product/workspacegeneralsettings/index.d.ts +1 -0
  281. package/dist/types/product/workspacegeneralsettings/workspacegeneralsettings.d.ts +48 -0
  282. package/dist/types/product/workspacegeneralsettings/workspacegeneralsettings.recipe.d.ts +3 -0
  283. package/dist/types/tokens/contract.d.ts +7 -1
  284. package/dist/types/tokens/elevation.d.ts +8 -5
  285. package/dist/types/tokens/palette.d.ts +28 -12
  286. package/dist/types/tokens/scale.d.ts +12 -1
  287. package/dist/types/tokens/semantic.d.ts +17 -0
  288. package/dist/wheelpicker.css +4 -2
  289. package/dist/wheelpicker.layered.css +4 -2
  290. package/dist/workspacegeneralsettings.css +30 -0
  291. package/dist/workspacegeneralsettings.d.ts +7 -0
  292. package/dist/workspacegeneralsettings.layered.css +32 -0
  293. package/package.json +41 -1
@@ -1,8 +1,10 @@
1
1
  import { defineSpec, resolveRecipe, specToRecipe } from "../../core/index.js";
2
2
  import { contract as c } from "../../tokens/index.js";
3
- // A 16px square checkbox measured from a record grid: r6, a subtle 1px
4
- // inset ring when empty, brand fill + white check when on. The check is a
5
- // data-URI SVG background so it renders reliably (input pseudo-content doesn't).
3
+ // A 16px square checkbox measured from a record grid: brand fill + white
4
+ // check when on. The check is a data-URI SVG background so it renders
5
+ // reliably (input pseudo-content doesn't). Radius (r4) and the real 1px
6
+ // border when empty are seed-design's own (your-moon/crisp#926),
7
+ // not the original 6px/box-shadow-ring measurement.
6
8
  const CHECK = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 6.5 5 9l4.5-5'/%3E%3C/svg%3E\")";
7
9
  const spec = defineSpec({
8
10
  name: "checkbox",
@@ -14,25 +16,32 @@ const spec = defineSpec({
14
16
  margin: 0,
15
17
  width: "16px",
16
18
  height: "16px",
17
- border: "none",
18
- borderRadius: c.radius.sm,
19
+ // your-moon/crisp#926: was `border: none` + a box-shadow
20
+ // inset ring -- seed-design's own checkmark (`checkmark.yaml`) is a real
21
+ // 1px border (`stroke.neutral-weak`, `borderDefault`'s exact value),
22
+ // removed entirely (not hidden via boxShadow:none) when selected, same
23
+ // technique change already made for Input (your-moon/crisp#910).
24
+ border: `1px solid ${c.borderDefault}`,
25
+ // your-moon/crisp#926: seed's checkmark box corner is
26
+ // `$radius.r1` (4px, `radius.xs`), not the 6px `radius.sm` this shared
27
+ // with Chip's old rounded-rect shape.
28
+ borderRadius: c.radius.xs,
19
29
  background: c.bgSurface,
20
- boxShadow: `inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 9%, transparent)`,
21
30
  cursor: "pointer",
22
- transition: "background-color .12s, box-shadow .12s",
31
+ transition: "background-color .12s, border-color .12s",
23
32
  },
24
33
  parts: [
25
34
  {
26
35
  selector: "&:hover",
27
36
  decls: {
28
- boxShadow: `inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 30%, transparent)`,
37
+ borderColor: `color-mix(in srgb, ${c.fgPrimary} 30%, transparent)`,
29
38
  },
30
39
  },
31
40
  {
32
41
  selector: "&:checked, &:indeterminate",
33
42
  decls: {
34
43
  background: `${c.bgBrandSolid} center / 10px no-repeat`,
35
- boxShadow: "none",
44
+ borderColor: "transparent",
36
45
  },
37
46
  },
38
47
  { selector: "&:checked", decls: { backgroundImage: CHECK } },
@@ -50,6 +59,49 @@ const spec = defineSpec({
50
59
  outlineOffset: "1px",
51
60
  },
52
61
  },
62
+ // your-moon/crisp#963: was no `:disabled` styling at all
63
+ // -- seed-design's own checkmark spec (`checkmark.yaml`) treats disabled
64
+ // as its own recolor, not a blanket opacity dim: fill `bg.disabled`
65
+ // (crisp's already-exact `bgNeutralWeak`, #f3f4f5), border
66
+ // `stroke.neutral-muted` (crisp's already-exact `borderSubtle`, applied
67
+ // here as the disabled border colour rather than its usual ring role).
68
+ // "disabled (any state)" per seed -- checked/indeterminate get the same
69
+ // muted fill/border, not the brand fill, hence the compound selectors
70
+ // below outranking the plain `:checked`/`:indeterminate` rule above.
71
+ {
72
+ selector: "&:disabled",
73
+ decls: {
74
+ background: c.bgNeutralWeak,
75
+ borderColor: c.borderSubtle,
76
+ cursor: "not-allowed",
77
+ },
78
+ },
79
+ {
80
+ // Higher specificity than the plain `&:checked, &:indeterminate` rule
81
+ // above (two pseudo-classes vs one), so this wins regardless of
82
+ // declaration order. Deliberately keeps the existing white check icon
83
+ // rather than recolouring it to seed's `fg.disabled` -- that glyph is
84
+ // baked into a static SVG data-URI with no theme-aware way to retint
85
+ // it without switching techniques entirely (mask + a separate colour
86
+ // layer), and white still reads clearly against the muted grey fill.
87
+ selector: "&:checked:disabled, &:indeterminate:disabled",
88
+ decls: {
89
+ background: `${c.bgNeutralWeak} center / 10px no-repeat`,
90
+ borderColor: c.borderSubtle,
91
+ },
92
+ },
93
+ { selector: "&:checked:disabled", decls: { backgroundImage: CHECK } },
94
+ {
95
+ // The `background` shorthand just above resets `background-image` to
96
+ // `none` on every property it doesn't restate -- without this, a
97
+ // disabled indeterminate checkbox would show the muted fill but lose
98
+ // its dash entirely.
99
+ selector: "&:indeterminate:disabled",
100
+ decls: {
101
+ backgroundImage: `linear-gradient(${c.fgOnBrand}, ${c.fgOnBrand})`,
102
+ backgroundSize: "8px 2px",
103
+ },
104
+ },
53
105
  ],
54
106
  });
55
107
  const config = specToRecipe(spec);
@@ -15,9 +15,21 @@ import { chipRecipe } from "./chip.recipe.js";
15
15
  * an inline ×) — so `Chip` has no `onRemove`/remove button yet. Add one once
16
16
  * that surface can be measured; don't guess the icon, its hit target, or its
17
17
  * spacing from the label.
18
+ *
19
+ * `appearance` (`"filled"` default | `"outline"`) and `selected` separate
20
+ * three visual roles crisp/#907's study found conflated: a filled chip reads
21
+ * as a status/value (not clickable), an outline chip reads as actionable (a
22
+ * tag or filter you can tap), and `selected` overrides either to a dark fill
23
+ * once tapped — see `chip.recipe.ts` for the structural reference.
18
24
  */
19
- export const Chip = React.forwardRef(({ tone, truncate, className, asChild, children, ...props }, ref) => {
25
+ export const Chip = React.forwardRef(({ tone, truncate, appearance, selected, className, asChild, children, ...props }, ref) => {
20
26
  const content = truncate && !asChild && typeof children === "string" ? (_jsx("span", { className: "crisp-chip-label", children: children })) : (children);
21
- return (_jsx(Primitive.span, { ref: ref, asChild: asChild, className: chipRecipe({ tone, truncate, className }), ...props, children: content }));
27
+ return (_jsx(Primitive.span, { ref: ref, asChild: asChild, className: chipRecipe({
28
+ tone,
29
+ truncate,
30
+ appearance,
31
+ selected,
32
+ className,
33
+ }), ...props, children: content }));
22
34
  });
23
35
  Chip.displayName = "Chip";
@@ -9,6 +9,15 @@ import { contract as c } from "../../tokens/index.js";
9
9
  // can't be reused verbatim here — Chip's root IS the styled element, not a
10
10
  // wrapper around one. The values below are transcribed from recordTagCss,
11
11
  // not re-measured; keep the two in sync by hand if either changes.
12
+ //
13
+ // your-moon/crisp#919: `radius.chip` itself (tokens/scale.ts) moved
14
+ // from Attio's measured 7px to a pill (999px), matching seed-design's own
15
+ // `chip.yaml` (always a pill, no size-based radius step at all). Every
16
+ // consumer sharing this one token — DataTable/RecordPage/RecordsTable/
17
+ // MultiSelect's tag & mention chips, TableToolbar/DatePickerField's date
18
+ // presets — goes pill-shaped together from this single token change, not a
19
+ // per-file edit; that's the whole reason those rules keep re-declaring
20
+ // `border-radius: ${c.radius.chip}` instead of a literal.
12
21
  const ring = (v, pct) => `inset 0 0 0 1px color-mix(in srgb, ${v} ${pct}%, transparent)`;
13
22
  const soft = (v, pct) => `color-mix(in srgb, ${v} ${pct}%, transparent)`;
14
23
  const onFg = (v, pct) => `color-mix(in srgb, ${v} ${pct}%, ${c.fgPrimary})`;
@@ -116,8 +125,67 @@ const spec = defineSpec({
116
125
  boxShadow: ring(c.bgOrange, 30),
117
126
  },
118
127
  },
128
+ // your-moon/crisp#907: a three-state read (actionable / informational /
129
+ // selected), the same shape as seed-design's own `chip.yaml` (`variant:
130
+ // solid|outlineStrong|outlineWeak`, each with a `selected` state) --
131
+ // structural reference only, not Naver/Kakao's colours (the study only
132
+ // supplied the pattern: outline reads as tappable, filled reads as
133
+ // status, dark fill reads as selected). `outline` only clears
134
+ // `background`, keeping the `tone`'s own `color`/`boxShadow` ring --
135
+ // SEED's own spec keeps `label.color` identical between `solid` and
136
+ // `outlineWeak`/`outlineStrong`, so the ring is the only thing that
137
+ // changes. Declared after `tone` so its rule wins the cascade at equal
138
+ // specificity (`core/resolve.ts` emits variant blocks in declaration
139
+ // order; see `build/generate-recipes.mjs`).
140
+ appearance: {
141
+ filled: {},
142
+ outline: { background: "transparent" },
143
+ },
144
+ // Overrides `tone`+`appearance` entirely to crisp's own dark-inverted
145
+ // pair (`bgNeutralInverted`/`fgOnNeutralInverted`, already shipped for
146
+ // `Snackbar` #911) -- SEED's `solid`/`outlineStrong` selected state, not
147
+ // Daum's `rgb(44,46,51)` (crisp's own token is `#2a3038`, close but
148
+ // deliberately not copied). Declared last so it always wins regardless
149
+ // of `tone`/`appearance`. Simplified from SEED, which gives
150
+ // `outlineWeak` a DIFFERENT, lighter selected treatment (a stronger
151
+ // stroke + weak fill, not a full inversion) -- one consistent dark-fill
152
+ // "selected" reads clearer across every tone than three different
153
+ // selected looks, and matches what #907 actually asked for.
154
+ selected: {
155
+ true: {
156
+ background: c.bgNeutralInverted,
157
+ color: c.fgOnNeutralInverted,
158
+ boxShadow: "none",
159
+ },
160
+ false: {},
161
+ },
162
+ },
163
+ // Team-lead review of #955's screenshot: the neutral tone's outline ring
164
+ // reuses `borderSubtle` (`#eeeff1`, meant for hairline dividers under a
165
+ // fill, not a standalone edge) -- on outline's transparent background
166
+ // it's nearly invisible, defeating the "outline reads as tappable" point
167
+ // of the appearance itself. Every other tone's ring is already a 30%
168
+ // tint of a saturated colour, visible enough on white; only neutral needs
169
+ // the fix. Matches `Input`'s own border token, per the reviewer's note.
170
+ // `selected: false` keeps this from re-drawing a border under `selected`'s
171
+ // dark fill -- compound rules emit after every plain variant rule
172
+ // (including `selected`'s own), so without this an outline+selected+
173
+ // neutral chip would get its border back on top of `selected`'s
174
+ // `boxShadow: "none"`.
175
+ compoundVariants: [
176
+ {
177
+ tone: "neutral",
178
+ appearance: "outline",
179
+ selected: false,
180
+ decls: { boxShadow: `inset 0 0 0 1px ${c.borderDefault}` },
181
+ },
182
+ ],
183
+ defaultVariants: {
184
+ tone: "neutral",
185
+ truncate: false,
186
+ appearance: "filled",
187
+ selected: false,
119
188
  },
120
- defaultVariants: { tone: "neutral", truncate: false },
121
189
  });
122
190
  const config = specToRecipe(spec);
123
191
  export const chipRecipe = Object.assign(resolveRecipe(config), { config });
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { Primitive } from "../../core/index.js";
4
+ import { chipgroupRecipe } from "./chipgroup.recipe.js";
5
+ /**
6
+ * A single-line, horizontally-scrolling row of `Chip`s (or anything else),
7
+ * with scroll-snap — for a filter/status row that would otherwise wrap onto
8
+ * a second line once it runs out of width (crisp#908). Every child snaps to
9
+ * the row's start edge and never shrinks, so scrolling always lands a chip
10
+ * flush against the left edge rather than mid-chip.
11
+ */
12
+ export const ChipGroup = React.forwardRef(({ className, asChild, ...props }, ref) => {
13
+ return (_jsx(Primitive.div, { ref: ref, asChild: asChild, role: "group", className: chipgroupRecipe({ className }), ...props }));
14
+ });
15
+ ChipGroup.displayName = "ChipGroup";
@@ -0,0 +1,33 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // your-moon/crisp#908: a horizontally-scrolling, single-line row for a set of
4
+ // `Chip`s -- Naver/Kakao both solve "many filter/status chips" this way
5
+ // (docs/study/korean/: Naver home `comp_snap_list`, `scroll-snap-type: x
6
+ // mandatory`; Daum `UL.grid_xscroll`, native overflow with no snap). crisp
7
+ // had no equivalent: a consumer's own flex row wraps chips onto a second
8
+ // line once they run out of horizontal space, which breaks the one-line
9
+ // table-header density Attio parity is built on (GRC's own toolbar filter
10
+ // row and record tabs, per the issue). No new visual values -- the gap
11
+ // between chips reuses `Chip`'s own measured 3px inter-child gap
12
+ // (chip.recipe.ts's `gap: "3px"` comment), and the scrollbar is hidden the
13
+ // same way `WheelPicker`'s already-shipped scroll column does
14
+ // (`scrollbar-width: none` + the `-webkit-scrollbar` pseudo), not a new
15
+ // pattern invented for this component.
16
+ const config = defineRecipe({
17
+ name: "chipgroup",
18
+ base: `
19
+ display: flex; flex-wrap: nowrap; align-items: center; gap: ${c.space["1.5"]};
20
+ overflow-x: auto; overscroll-behavior-x: contain;
21
+ scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
22
+ scrollbar-width: none; -ms-overflow-style: none;
23
+ &::-webkit-scrollbar { display: none; }
24
+ & > * { flex-shrink: 0; scroll-snap-align: start; }
25
+ `,
26
+ variants: {},
27
+ defaultVariants: {},
28
+ compoundVariants: [],
29
+ });
30
+ export const chipgroupRecipe = Object.assign(resolveRecipe(config), {
31
+ config,
32
+ });
33
+ export default config;
@@ -0,0 +1,2 @@
1
+ export { ChipGroup } from "./chipgroup.js";
2
+ export { chipgroupRecipe } from "./chipgroup.recipe.js";
@@ -369,7 +369,11 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US", ope
369
369
  const e = display ? { ...raw, name: display } : raw;
370
370
  if (!e.name)
371
371
  return empty;
372
- return (_jsxs("span", { className: "crisp-datatable-entity", children: [_jsx("span", { className: "crisp-datatable-avatar", "data-tone": avatarTone === "hashed" ? toneFor(raw.name) : undefined, children: e.avatar ? (_jsx("img", { src: e.avatar, alt: "" })) : (initials(e.name).slice(0, 2)) }), col.onEntityClick ? (_jsx("button", { type: "button", className: "crisp-datatable-entity-name crisp-datatable-entity-name--link", onClick: (ev) => {
372
+ return (_jsxs("span", { className: "crisp-datatable-entity", children: [_jsx("span", { className: "crisp-datatable-avatar", "data-tone": avatarTone === "hashed" ? toneFor(raw.name) : undefined, "data-shape": col.avatarShape === "circle" ? "circle" : undefined, children: e.avatar ? (_jsx("img", { src: e.avatar, alt: "" })) : (initials(e.name).slice(0, 2)) }), col.href ? (_jsx("a", { href: col.href(row), className: "crisp-datatable-entity-name crisp-datatable-entity-name--link", onClick: (ev) => {
373
+ // same reasoning as onEntityClick's stopPropagation below --
374
+ // this must not also trigger the row's own onRowClick.
375
+ ev.stopPropagation();
376
+ }, children: e.name })) : col.onEntityClick ? (_jsx("button", { type: "button", className: "crisp-datatable-entity-name crisp-datatable-entity-name--link", onClick: (ev) => {
373
377
  // the cell's own click already just selects (see the
374
378
  // TableCell onClick above) -- this must additionally stop
375
379
  // the row's onRowClick from firing, since this name points
@@ -287,7 +287,10 @@ const config = defineRecipe({
287
287
  primary-column body cell regardless of type. Made explicit here so
288
288
  fixing that leak doesn't also (wrongly) drop entity names to 400. */
289
289
  & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: ${c.weight.medium}; text-decoration: underline; text-decoration-color: color-mix(in srgb, ${c.fgPrimary} 9%, transparent); }
290
- & 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%; }
290
+ /* #893: shared by the button (onEntityClick, a different record) and
291
+ the anchor (href, the row's own record) -- was button-only, which
292
+ silently left a real <a> unstyled the day #893 added one. */
293
+ & .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%; }
291
294
  /* crisp/#601, G-048: two 20px ghost icons, right-aligned in the primary
292
295
  column, revealed on ROW hover (the issue's own title says "row
293
296
  hover", and a live check confirmed hovering well past the name --
@@ -301,7 +304,13 @@ const config = defineRecipe({
301
304
  & tr:hover .crisp-datatable-entity-actions, & .crisp-datatable-entity-actions:focus-within { opacity: 1; }
302
305
  & .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; }
303
306
  & .crisp-datatable-entity-action:hover { background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); color: ${c.fgPrimary}; }
304
- & .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}; }
307
+ /* #892: the default shape's radius was a stray literal (radius.sm, 6px)
308
+ -- Attio's own real company/object avatar measures 30%, which is
309
+ Avatar's own already-shipped shape="rounded" value, not a new number.
310
+ shape="circle" reuses Avatar's own shape="circle" value (radius.full)
311
+ the same way, measured live on a real Attio person avatar. */
312
+ & .crisp-datatable-avatar { width: ${c.size.icon}; height: ${c.size.icon}; border-radius: 30%; 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}; }
313
+ & .crisp-datatable-avatar[data-shape="circle"] { border-radius: ${c.radius.full}; }
305
314
  & .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}; }
306
315
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
307
316
  & .crisp-datatable-tags { display: flex; align-items: center; gap: ${c.space["1"]}; overflow: hidden; }
@@ -1,10 +1,18 @@
1
1
  import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
- import { contract as c, elevation as e } from "../../tokens/index.js";
3
- // A modal dialog surface — measured from a create-record modal: a radius-16
4
- // frosted-glass card (60% tinted background + 15px backdrop blur, both re-measured
5
- // per theme — light uses a blue-tinted 3-layer shadow, dark drops to a plain
6
- // 2-layer black shadow) with a bordered header (title + close), a scrollable
7
- // body, and a bordered footer for the primary action.
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // your-moon/crisp#934: was a radius-16 frosted-glass card (a
4
+ // 60%-tinted, 15px-backdrop-blurred outer shell wrapping a solid radius-12
5
+ // inner card) -- owner-directed: Dialog's identity now follows seed-design
6
+ // (daangn), whose own Dialog spec has no shadow/blur/tint property at all;
7
+ // elevation there is conveyed purely by a darker backdrop scrim plus the
8
+ // existing scale+fade entrance animation (which crisp already had). Flat
9
+ // single surface now: `.crisp-dialog-content` is a bare, invisible sizing
10
+ // wrapper (no background/radius/shadow of its own); `.crisp-dialog-card` is
11
+ // the one real surface, radius bumped from `xl` (12px) to `3xl` (20px,
12
+ // seed's own shared floating-surface radius, `$radius.r5`), no shadow.
13
+ // `bgScrim` itself also moved toward seed's own darker backdrop value
14
+ // (`tokens/palette.ts`). The alert variant no longer needs its own override
15
+ // of either rule -- both variants now render the identical flat surface.
8
16
  //
9
17
  // #414: the footer's submit-key caps (⌘/Enter) used to be one span drawing a
10
18
  // visible pill (18px, 22%-white background). Attio's own are two separate
@@ -18,10 +26,8 @@ const config = defineRecipe({
18
26
  & .crisp-dialog-backdrop { position: fixed; inset: 0; z-index: 60; background: ${c.bgScrim}; }
19
27
  & .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
28
  & .crisp-dialog-positioner[data-variant="alert"] { align-items: center; padding: ${c.space["4"]}; }
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; }
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}; }
29
+ & .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; opacity: 1; transform: scale(1); transition: opacity 0.2s ease, transform 0.2s ease; @starting-style { opacity: 0; transform: scale(0.97); } }
30
+ & .crisp-dialog-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: ${c.radius["3xl"]}; background: ${c.bgSurface}; overflow: hidden; }
25
31
  & .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}; }
26
32
  & .crisp-dialog-header svg { flex-shrink: 0; color: ${c.fgMuted}; }
27
33
  & .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; }
@@ -43,7 +49,12 @@ const config = defineRecipe({
43
49
  & .crisp-dialog-field { display: flex; flex-direction: column; gap: ${c.space["1.5"]}; }
44
50
  & .crisp-dialog-field-label { font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 50%, transparent); }
45
51
  & .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; }
46
- & .crisp-dialog-submit { display: inline-flex; align-items: center; gap: ${c.space["2"]}; height: 28px; padding: 0 ${c.space["1"]} 0 ${c.space["2.5"]}; border: none; border-radius: ${c.radius.md}; background: ${c.bgBrandSolid}; color: ${c.fgOnBrand}; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; cursor: pointer; }
52
+ /* your-moon/crisp#953: bgBrandContrast, not bgBrandSolid --
53
+ the accent (#0298d9) only clears 3.23:1 with white; the darker
54
+ sibling clears 4.5:1+, same fix already applied to Button/Badge/
55
+ Avatar (#897). Scoped to this one fill only -- Dialog's own frosted-
56
+ surface identity (#934) is untouched here. */
57
+ & .crisp-dialog-submit { display: inline-flex; align-items: center; gap: ${c.space["2"]}; height: 28px; padding: 0 ${c.space["1"]} 0 ${c.space["2.5"]}; border: none; border-radius: ${c.radius.md}; background: ${c.bgBrandContrast}; color: ${c.fgOnBrand}; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; cursor: pointer; }
47
58
  & .crisp-dialog-submit-key { display: inline-flex; align-items: center; gap: ${c.space["1"]}; }
48
59
  & .crisp-dialog-submit-key kbd { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0 3px; border-radius: 4px; background: transparent; color: ${c.fgOnBrand}; font-family: inherit; font-style: normal; font-size: 10px; }`,
49
60
  variants: {},
@@ -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";
@@ -1,11 +1,41 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { inputRecipe } from "./input.recipe.js";
4
- export const Input = React.forwardRef(({ size, invalid, className, style, ...props }, ref) => {
4
+ export const Input = React.forwardRef(({ size, invalid, className, style, suffix, ...props }, ref) => {
5
5
  // The wrapper exists only to host the ::after focus ring — an <input> is a
6
6
  // replaced element and cannot have one. Everything the consumer passes
7
7
  // still lands on the <input> itself, so `id`, `name`, `onChange`, form
8
8
  // association and every native attribute behave exactly as before.
9
- return (_jsx("span", { className: inputRecipe({ size, invalid, className }), style: style, children: _jsx("input", { ref: ref, className: "crisp-input-value", "aria-invalid": invalid === true || undefined, ...props }) }));
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 {
@@ -38,6 +40,16 @@ const config = defineRecipe({
38
40
  & .crisp-input-value::placeholder { color: ${c.fgMuted}; }
39
41
  & .crisp-input-value:disabled { opacity: 0.5; cursor: not-allowed; }
40
42
  &:has(.crisp-input-value:disabled) { opacity: 1; }
43
+
44
+ /* crisp#634: measured live on Attio's own Settings > Profile read-only
45
+ primary-email field -- a tinted field, not just muted text, driven off
46
+ the native \`readOnly\` attribute. \`ringField\` (rgba(0,0,0,.06)) is an
47
+ exact match for the measured ring; the 2%-fgPrimary tint reuses the
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}; }
52
+ & .crisp-input-suffix { flex-shrink: 0; margin-inline-start: ${c.space["1"]}; }
41
53
  `,
42
54
  variants: {
43
55
  size: {
@@ -48,7 +60,10 @@ const config = defineRecipe({
48
60
  // padding `medium` already uses, just 4px shorter.
49
61
  sm: `height: ${c.size.row}; border-radius: ${c.radius.md}; & .crisp-input-value { padding-inline: ${c.space["2.5"]}; }`,
50
62
  medium: `height: 32px; border-radius: ${c.radius.md}; & .crisp-input-value { padding-inline: ${c.space["2.5"]}; }`,
51
- 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"]}; }`,
52
67
  },
53
68
  invalid: {
54
69
  // critical outranks focus: a field that is wrong stays visibly wrong
@@ -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";