@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
@@ -26,11 +26,11 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
26
26
  // pattern with no precedent anywhere else in the repo, for one component.
27
27
  // The enter animation (scale 0.95→1, opacity 0→1, seed's own curve) IS
28
28
  // reproduced below.
29
- // - A visible `divider` slot: `select.yaml` defines one, but nothing in
30
- // crisp or GRC groups options with an explicit separator today, and
31
- // AUTHORING's rule against building what nothing consumes applies to CSS
32
- // too. The resolved value is in `~/grc-parity/seed-select-spec.md` for
33
- // whoever adds it.
29
+ // - A general-purpose `divider` slot between arbitrary option groups:
30
+ // nothing in crisp or GRC groups options with an explicit separator
31
+ // today, and AUTHORING's rule against building what nothing consumes
32
+ // applies to CSS too. (`select.yaml`'s resolved value IS used below, just
33
+ // for one specific seam — see `.crisp-select-divider`'s own comment.)
34
34
  const config = defineRecipe({
35
35
  name: "select",
36
36
  base: `
@@ -99,6 +99,20 @@ const config = defineRecipe({
99
99
  .crisp-select-search svg { flex-shrink: 0; }
100
100
  .crisp-select-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}; }
101
101
  .crisp-select-search input::placeholder { color: ${c.fgSelectPlaceholder}; }
102
+ /* select.yaml's own divider slot (marginX x4, height 1px, colour
103
+ stroke.neutral-muted -- described upstream as "the line marking a
104
+ boundary between semantic units: between sections, between content
105
+ and an action area, between a header and a body" -- exactly the
106
+ search-field/option-list boundary crisp#904's review flagged as
107
+ reading too tight). Not the general-purpose separator #867 declined
108
+ to build (nothing groups options with one) -- scoped to this one
109
+ specific header/body seam, which now exists because searchable
110
+ does. Sits as an ordinary flex child, so the listbox's own 8px gap
111
+ still applies on both sides of it -- total visual seam is
112
+ gap+1px+gap (17px), deliberately more than the 8px between two
113
+ plain option rows, so it reads as a section break, not a wider list
114
+ gap. */
115
+ .crisp-select-divider { flex-shrink: 0; height: 1px; margin-inline: ${c.space["4"]}; background: ${c.strokeSelectDivider}; }
102
116
  .crisp-select-empty { padding: ${c.space["3"]} ${c.space["4"]}; text-align: center; font-size: ${c.text.md}; color: ${c.fgNeutralSubtle}; }
103
117
  /* select.yaml's groupLabel -- font-size/line-height/weight are the same
104
118
  across sizes, only paddingY differs (medium 8px, large 10px). */
@@ -133,13 +147,25 @@ const config = defineRecipe({
133
147
  },
134
148
  // Owner's follow-up after #867/#879 (crisp/#889): the LOOK stays seed's
135
149
  // (border, listbox, radius, shadow, items, states, all unchanged below),
136
- // but the SIZE is Attio's again -- a 40px trigger next to a 32px `Input`
137
- // in the same form visibly mismatched (measured live in GRC's own "New
138
- // employee" dialog). `small` is the new default: Attio's original
139
- // 32px `c.size.menuRow`, `Input`'s own radius/padding/line-height so the
140
- // two line up exactly, not just approximately. `medium`/`large` (seed's
141
- // own 40/52px) stay, opt-in. Item rows are IDENTICAL across all three --
142
- // only the trigger differs; only it was ever the mismatch.
150
+ // but the HEIGHT is Attio's again -- a 40px trigger next to a 32px
151
+ // `Input` in the same form visibly mismatched (measured live in GRC's
152
+ // own "New employee" dialog). `small` is the new default: Attio's
153
+ // original 32px `c.size.menuRow`. `medium`/`large` (seed's own 40/52px)
154
+ // stay, opt-in. Item rows are IDENTICAL across all three -- only the
155
+ // trigger differs; only it was ever the mismatch.
156
+ //
157
+ // crisp/#904 (follow-up, then reverted): `small`'s gap/padding-inline
158
+ // briefly moved to seed's literal `select-trigger.yaml` `size=medium`
159
+ // values (8px/14px), on the theory that even the 32px trigger should
160
+ // read seed's numbers verbatim. Team-lead ruling on that PR's review:
161
+ // padding and gap are SPACING, and the owner's actual direction is
162
+ // "sizes/spacing from Attio, look from seed" -- Select and Input sit
163
+ // side by side in every form, so spacing follows Input's measured
164
+ // values here, same as `medium`/`large` below follow seed's. Reverted
165
+ // to `space["1.5"]`/`space["2.5"]` (6px/10px, `Input`'s own gap/padding
166
+ // at this row height) -- everything else `small` already got from seed
167
+ // (fill/stroke/border/radius/chevron/colours/states, see this file's
168
+ // other comments) is unaffected and stays seed's.
143
169
  size: {
144
170
  small: `
145
171
  & .crisp-select-trigger { height: ${c.size.menuRow}; border-radius: ${c.radius.md}; gap: ${c.space["1.5"]}; padding-inline: ${c.space["2.5"]}; }
@@ -0,0 +1,2 @@
1
+ export { SnackbarProvider, useSnackbar, } from "./snackbar.js";
2
+ export { snackbarRecipe } from "./snackbar.recipe.js";
@@ -0,0 +1,84 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CircleAlert, CircleCheck } from "lucide-react";
3
+ import * as React from "react";
4
+ import { useCrispMessage, useFocusAfterRemoval } from "../../core/index.js";
5
+ import { snackbarRecipe } from "./snackbar.recipe.js";
6
+ const SnackbarContext = React.createContext(null);
7
+ const ICONS = {
8
+ positive: CircleCheck,
9
+ critical: CircleAlert,
10
+ };
11
+ function SnackbarPill({ item, onClose, }) {
12
+ const Icon = ICONS[item.variant ?? "default"];
13
+ return (_jsxs("div", { className: "crisp-snackbar", "data-variant": item.variant ?? "default", children: [_jsxs("div", { className: "crisp-snackbar-content", children: [Icon != null && (_jsx("span", { className: "crisp-snackbar-icon", children: _jsx(Icon, { size: 24, strokeWidth: 1.75, "aria-hidden": "true" }) })), _jsx("span", { className: "crisp-snackbar-message", children: item.message })] }), item.action && (_jsx("button", { type: "button", className: "crisp-snackbar-action", onClick: () => {
14
+ item.action?.onClick();
15
+ onClose();
16
+ }, children: item.action.label }))] }));
17
+ }
18
+ /**
19
+ * Wrap your app in a `SnackbarProvider`, then call `useSnackbar().show({...})`
20
+ * from anywhere to queue a bottom-anchored snackbar. Only one shows at a time
21
+ * (seed-design's mobile pattern, crisp#911) -- a second call queues behind the
22
+ * first and shows once it dismisses. Auto-dismisses after `duration` (default
23
+ * 4s; 0 keeps it until `dismiss()` or its action fires -- there is no close
24
+ * button, matching seed's own anatomy, which has none).
25
+ */
26
+ export function SnackbarProvider({ children, duration = 4000, regionLabel: regionLabelProp, }) {
27
+ const regionLabel = useCrispMessage(regionLabelProp, "Notifications");
28
+ const [queue, setQueue] = React.useState([]);
29
+ const idRef = React.useRef(0);
30
+ const timer = React.useRef(null);
31
+ const regionRef = React.useRef(null);
32
+ const cameFrom = React.useRef(null);
33
+ const focusAfterDismiss = useFocusAfterRemoval(regionRef, ".crisp-snackbar-action", cameFrom);
34
+ React.useEffect(() => {
35
+ const onFocusIn = (e) => {
36
+ const target = e.target;
37
+ if (!target || regionRef.current?.contains(target))
38
+ return;
39
+ cameFrom.current = target;
40
+ };
41
+ document.addEventListener("focusin", onFocusIn);
42
+ return () => document.removeEventListener("focusin", onFocusIn);
43
+ }, []);
44
+ const dismiss = React.useCallback((id) => {
45
+ setQueue((list) => list.filter((s) => s.id !== id));
46
+ if (timer.current) {
47
+ clearTimeout(timer.current);
48
+ timer.current = null;
49
+ }
50
+ }, []);
51
+ const show = React.useCallback((o) => {
52
+ idRef.current += 1;
53
+ const id = idRef.current;
54
+ setQueue((list) => [...list, { ...o, id }]);
55
+ return id;
56
+ }, []);
57
+ // Only the FRONT of the queue is ever mounted, so its own auto-dismiss
58
+ // timer starts when it becomes visible, not when `show` was called.
59
+ const current = queue[0];
60
+ React.useEffect(() => {
61
+ if (!current)
62
+ return;
63
+ const d = current.duration ?? duration;
64
+ if (d <= 0)
65
+ return;
66
+ timer.current = setTimeout(() => dismiss(current.id), d);
67
+ return () => {
68
+ if (timer.current)
69
+ clearTimeout(timer.current);
70
+ };
71
+ }, [current, duration, dismiss]);
72
+ const api = React.useMemo(() => ({ show, dismiss }), [show, dismiss]);
73
+ return (_jsxs(SnackbarContext.Provider, { value: api, children: [children, _jsx("div", { className: snackbarRecipe(), children: _jsx("div", { ref: regionRef, className: "crisp-snackbar-region", role: "region", "aria-label": regionLabel, "aria-live": "polite", "aria-atomic": "false", children: current && (_jsx(SnackbarPill, { item: current, onClose: () => {
74
+ focusAfterDismiss(0);
75
+ dismiss(current.id);
76
+ } }, current.id)) }) })] }));
77
+ }
78
+ SnackbarProvider.displayName = "SnackbarProvider";
79
+ export function useSnackbar() {
80
+ const ctx = React.useContext(SnackbarContext);
81
+ if (!ctx)
82
+ throw new Error("useSnackbar must be used within a SnackbarProvider");
83
+ return ctx;
84
+ }
@@ -0,0 +1,68 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // A mobile-pattern snackbar — daangn/seed-design's
4
+ // packages/rootage/components/snackbar.yaml (dev@e1c486b904297f7b6a8f17b8523f732c37015d60),
5
+ // not Attio (no such component lives there; crisp#911). A single dark
6
+ // `bgNeutralInverted` pill, bottom-anchored, holding an optional tone-coloured
7
+ // prefix icon, a message, and an optional bold action. Every numeric value
8
+ // below cites its yaml line; sizes/radii already equal existing crisp tokens
9
+ // (crisp's space/radius scale was itself derived 2px-granular, same as
10
+ // seed's `dimension`/`radius` scales, so no new tokens were needed).
11
+ //
12
+ // Deviation from the spec's literal `actionButton.color: $color.fg.brand`
13
+ // (snackbar.yaml:128, seed's carrot-600 `#ff6600`): crisp's OWN brand token
14
+ // is TechPartners' verified blue (`bgBrandSolid`, `#0298d9`), not seed's
15
+ // orange (the identity migration takes seed's STRUCTURE, crisp's own colour
16
+ // -- `~/grc-parity/seed-identity-spec.md`). Computed contrast of
17
+ // `bgBrandSolid` on `bgNeutralInverted` (light theme: `#0298d9` on `#2a3038`)
18
+ // is 4.11:1 -- below AA's 4.5:1 for 14px text (bold 14px does not clear the
19
+ // large-text 3:1 threshold, which needs 18.66px). crisp has no brand step
20
+ // bright enough on this dark surface yet, so the action reads in
21
+ // `fgOnNeutralInverted` (the same token the message already uses) at bold
22
+ // weight instead -- distinguished by weight, not colour, until a dedicated
23
+ // token is measured.
24
+ const config = defineRecipe({
25
+ name: "snackbar",
26
+ base: `
27
+ & .crisp-snackbar-region {
28
+ position: fixed; z-index: 80; inset-inline: 0; bottom: 0;
29
+ display: flex; justify-content: center;
30
+ padding: ${c.space["2"]}; /* snackbar.yaml:95-96 region.paddingX/paddingY = $dimension.x2 (8px) */
31
+ pointer-events: none;
32
+ }
33
+ & .crisp-snackbar {
34
+ pointer-events: auto; box-sizing: border-box;
35
+ display: flex; align-items: center; gap: ${c.space["2.5"]}; /* snackbar.yaml:116 content.gap = $dimension.x2_5 (10px), reused as the icon+message-to-action gap */
36
+ min-height: 44px; max-width: 464px; /* snackbar.yaml:102-103 */
37
+ width: max-content;
38
+ padding: ${c.space["2.5"]}; /* snackbar.yaml:104-105 root.paddingX/paddingY = $dimension.x2_5 (10px) */
39
+ border-radius: ${c.radius.md}; /* snackbar.yaml:101 root.cornerRadius = $radius.r2 (8px) */
40
+ background: ${c.bgNeutralInverted}; /* snackbar.yaml:100 root.color = $color.bg.neutral-inverted */
41
+ font-family: ${c.font.sans};
42
+ opacity: 1; transform: scale(1);
43
+ transition: opacity 150ms cubic-bezier(0,0,.15,1), transform 150ms cubic-bezier(0,0,.15,1); /* snackbar.yaml:108-109 enterDuration/enterTimingFunction = $duration.d3 (150ms) / $timing-function.enter */
44
+ @starting-style { opacity: 0; transform: scale(0.8); } /* snackbar.yaml:106-107 enterOpacity/enterScale */
45
+ }
46
+ & .crisp-snackbar-content { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: ${c.space["0.5"]}; } /* snackbar.yaml:124 prefixIcon.paddingRight = $dimension.x0_5 (2px), used as the icon-to-message gap */
47
+ & .crisp-snackbar-icon { flex-shrink: 0; display: inline-flex; }
48
+ & .crisp-snackbar[data-variant="positive"] .crisp-snackbar-icon { color: ${c.bgSuccess}; } /* snackbar.yaml:138-139 variant=positive prefixIcon.color = $color.fg.positive */
49
+ & .crisp-snackbar[data-variant="critical"] .crisp-snackbar-icon { color: ${c.bgDanger}; } /* snackbar.yaml:142-143 variant=critical prefixIcon.color = $color.fg.critical */
50
+ & .crisp-snackbar-message { flex: 1 1 auto; min-width: 0; color: ${c.fgOnNeutralInverted}; font-size: ${c.text.lg}; line-height: 19px; font-weight: ${c.weight.regular}; } /* snackbar.yaml:117-121: message.color = $color.fg.neutral-inverted, fontSize/lineHeight = $font-size.t4/$line-height.t4 (14px/19px), fontWeight = regular */
51
+ & .crisp-snackbar-action {
52
+ flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
53
+ min-height: 44px; padding: 0 ${c.space["2"]}; margin: -12px calc(-1 * ${c.space["2"]}); /* snackbar.yaml:126-127 targetPaddingX/targetMinHeight = $dimension.x2 (8px, doubled here as horizontal padding)/44px -- a larger invisible tap target than the pill's own 44px content row, via negative margin so it doesn't grow the pill visually */
54
+ border: none; border-radius: ${c.radius.md}; background: none;
55
+ color: ${c.fgOnNeutralInverted}; font-family: inherit; font-size: ${c.text.lg}; line-height: 19px; font-weight: ${c.weight.bold}; /* fontSize/lineHeight = t4/line-height.t4; fontWeight = bold (snackbar.yaml:129-131); colour deviation documented above */
56
+ cursor: pointer;
57
+ transition: transform 150ms cubic-bezier(0,0,.15,1);
58
+ }
59
+ & .crisp-snackbar-action:active { transform: scale(0.98); } /* snackbar.yaml:133-134 pressed.actionButton.scaleScope = self */
60
+ `,
61
+ variants: {},
62
+ defaultVariants: {},
63
+ compoundVariants: [],
64
+ });
65
+ export const snackbarRecipe = Object.assign(resolveRecipe(config), {
66
+ config,
67
+ });
68
+ export default config;
@@ -21,7 +21,13 @@ const config = defineRecipe({
21
21
  (padding: 0 12px) and independently on a body cell's text inset
22
22
  (getBoundingClientRect delta from the cell edge). Matches
23
23
  ColumnCalc's own already-correct 12px trigger inset. */
24
- & th, & td { position: relative; height: ${c.size.tableRow}; padding: 0 ${c.space["3"]}; border-bottom: 1px solid ${c.borderSubtle}; border-inline-end: 1px solid ${c.borderSubtle}; text-align: start; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
24
+ /* #719: overflow: clip, not hidden -- both render the ellipsis
25
+ identically, but hidden still honours a programmatic scrollLeft
26
+ assignment (a screenshot tool, a browser extension) even though it
27
+ already blocks user-driven wheel/scrollbar input; clip leaves no
28
+ scroll position for anything to move, so the truncated text can
29
+ never be panned out of view. */
30
+ & th, & td { position: relative; height: ${c.size.tableRow}; padding: 0 ${c.space["3"]}; border-bottom: 1px solid ${c.borderSubtle}; border-inline-end: 1px solid ${c.borderSubtle}; text-align: start; vertical-align: middle; white-space: nowrap; overflow: clip; text-overflow: ellipsis; }
25
31
  /* #554: sticky on the vertical axis, mirroring RecordsTable's own
26
32
  \`.crisp-recordstable-th\`. z-index 2 sits above a plain body cell (auto)
27
33
  and below a sticky-column corner cell (DataTable's \`th[data-sticky]\`,
@@ -175,5 +175,10 @@ export const toolbarChromeBody = (p) => ` & .${CLASS_PREFIX}${p}-menu-head {
175
175
  & .${CLASS_PREFIX}${p}-fchip-x:hover { background: color-mix(in srgb, ${c.fgPrimary} 10%, transparent); color: ${c.fgPrimary}; }
176
176
  & .${CLASS_PREFIX}${p}-spacer { flex: 1; }
177
177
  & .${CLASS_PREFIX}${p}-count { font-size: ${c.text.md}; color: ${c.fgMuted}; }
178
- & .${CLASS_PREFIX}${p}-new { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.row}; padding: 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; transition: background-color .12s; }
179
- & .${CLASS_PREFIX}${p}-new:hover { background: ${c.bgBrandSolidPressed}; }`;
178
+ /* your-moon/crisp#953: bgBrandContrast, not bgBrandSolid --
179
+ the accent (#0298d9) only clears 3.23:1 with white; the darker sibling
180
+ clears 4.5:1+, same fix already applied to Button/Badge/Avatar (#897).
181
+ Hover is a brightness filter on the new fill, not bgBrandSolidPressed
182
+ (that token stays tuned for the lighter accent's own consumers). */
183
+ & .${CLASS_PREFIX}${p}-new { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.row}; padding: 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; transition: filter .12s; }
184
+ & .${CLASS_PREFIX}${p}-new:hover { filter: brightness(0.96); }`;
@@ -2,7 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { ArrowDownWideNarrow, ArrowUpDown, ArrowUpNarrowWide, Check, ChevronDown, ChevronLeft, ChevronRight, CircleHelp, Download, GripVertical, ListFilter, MoreVertical, Plus, SlidersHorizontal, Trash2, Upload, X, } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { createPortal } from "react-dom";
5
- import { FloatingPortal, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, useCrispMessages, useFocusAfterRemoval, } from "../../core/index.js";
5
+ import { FloatingPortal, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispComponentDefaults, useCrispMessage, useCrispMessages, useFocusAfterRemoval, } from "../../core/index.js";
6
6
  import { DatePicker, } from "../datepicker/index.js";
7
7
  import { MenuCheckboxItem, useMenuKeyboard } from "../menu/index.js";
8
8
  import { Select } from "../select/index.js";
@@ -44,7 +44,7 @@ function ViewBarSlot({ target, children, }) {
44
44
  // file, not per call site, and three independently attributed tags for
45
45
  // the same name over-counts against that (it expects one per name).
46
46
  const RtlChevronRight = ({ size, className, }) => (_jsx(ChevronRight, { "data-rtl-flip": "", size: size, strokeWidth: 1.75, className: className }));
47
- const OP_LABELS = {
47
+ export const toolbarOperatorLabels = {
48
48
  contains: "contains",
49
49
  doesnotcontain: "does not contain",
50
50
  startswith: "starts with",
@@ -65,6 +65,33 @@ const OP_LABELS = {
65
65
  before: "before",
66
66
  after: "after",
67
67
  };
68
+ /**
69
+ * crisp/#962: resolves each operator word through four layers, explicit
70
+ * prop winning, same "prop beats provider beats built-in" order as every
71
+ * other label in this file — `operatorLabels` prop, then a
72
+ * `CrispMessagesProvider`'s `componentDefaults.tableToolbar` (the new,
73
+ * discoverable, typed override), then the OLD whole-`CrispMessages`-
74
+ * dictionary-keyed-by-English-string lookup this file already had (kept
75
+ * so a consumer who already added entries there keeps working, unchanged),
76
+ * then the hardcoded English word. Not `useCrispMessage` in a loop over 13
77
+ * keys — that breaks the rules of hooks the same reason `useCrispMessages`
78
+ * itself exists (see that hook's own doc comment).
79
+ */
80
+ function useToolbarOperatorLabels(operatorLabels, crispMessages) {
81
+ const providerLabels = useCrispComponentDefaults("tableToolbar");
82
+ return React.useMemo(() => {
83
+ const resolved = {};
84
+ for (const op of Object.keys(toolbarOperatorLabels)) {
85
+ const builtIn = toolbarOperatorLabels[op];
86
+ resolved[op] =
87
+ operatorLabels?.[op] ??
88
+ providerLabels?.[op] ??
89
+ crispMessages[builtIn] ??
90
+ builtIn;
91
+ }
92
+ return resolved;
93
+ }, [operatorLabels, providerLabels, crispMessages]);
94
+ }
68
95
  // #619: order and label set match Attio's own text-filter operator picker
69
96
  // verbatim (measured live, per RecordsTable's own #578 port of the same
70
97
  // set: contains · does not contain · starts with · ends with · is · is not ·
@@ -329,7 +356,7 @@ export function applySorts(rows, sorts, get = (row, key) => row[key]) {
329
356
  * this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
330
357
  * Created by), which one shared list can't reproduce.
331
358
  */
332
- export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, showSort = true, showViewSettings = true, viewSettingsRef, importExportRef, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, importExportLabel: importExportLabelProp, onImportCsv, onExportCsv, onExportExcel, importExportHelpHref, importCsvLabel: importCsvLabelProp, exportCsvLabel: exportCsvLabelProp, exportExcelLabel: exportExcelLabelProp, learnImportExportLabel: learnImportExportLabelProp, backLabel: backLabelProp, nestedFieldPlaceholder, ...rest }) {
359
+ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, operatorLabels, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, showSort = true, showViewSettings = true, viewSettingsRef, importExportRef, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, importExportLabel: importExportLabelProp, onImportCsv, onExportCsv, onExportExcel, importExportHelpHref, importCsvLabel: importCsvLabelProp, exportCsvLabel: exportCsvLabelProp, exportExcelLabel: exportExcelLabelProp, learnImportExportLabel: learnImportExportLabelProp, backLabel: backLabelProp, nestedFieldPlaceholder, ...rest }) {
333
360
  // crisp/#471: an explicit prop always wins; missing, it reads the nearest
334
361
  // CrispMessagesProvider's translation of the English default below, or
335
362
  // that default itself with no provider — unchanged from before this
@@ -362,12 +389,12 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
362
389
  const exportExcelLabel = useCrispMessage(exportExcelLabelProp, "Export view as Excel");
363
390
  const learnImportExportLabel = useCrispMessage(learnImportExportLabelProp, "Learn about imports and exports");
364
391
  const backLabel = useCrispMessage(backLabelProp, "Back");
365
- // OP_LABELS' own words ("contains", "is", "is any of", …) have no prop of
366
- // their own — a whole lookup table, not one string — so they translate
367
- // straight from the provider's dictionary at the two spots they render
368
- // (#628, #673 below), same fallback order as every resolved constant
369
- // above.
370
392
  const crispMessages = useCrispMessages();
393
+ // crisp/#962: `operatorLabels` prop + `componentDefaults.tableToolbar`,
394
+ // layered over the old whole-dictionary lookup — see
395
+ // `useToolbarOperatorLabels`'s own doc comment for the full resolution
396
+ // order.
397
+ const opLabels = useToolbarOperatorLabels(operatorLabels, crispMessages);
371
398
  // Each side falls back to the shared `columns` independently — a caller
372
399
  // that only overrides one (Filter's own Record/Created by, say) still
373
400
  // gets `columns` on the other, unchanged from before either prop existed.
@@ -803,9 +830,7 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
803
830
  return (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("span", { className: "crisp-tabletoolbar-fcond", "data-complete": complete ? "" : undefined, children: [_jsxs("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--field", "data-menu-trigger": true, "aria-haspopup": "dialog", "aria-expanded": fieldOpen, onClick: () => {
804
831
  setFieldPickerPath([]);
805
832
  setOpenMenu(fieldOpen ? null : `ffield:${cond.id}`);
806
- }, children: [col?.icon ? _jsx(col.icon, { size: 14, strokeWidth: 1.75 }) : null, colPath.length > 1 ? (_jsx("span", { className: "crisp-tabletoolbar-fcond-path", children: colPath.map((c, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx(RtlChevronRight, { size: 14, className: "crisp-tabletoolbar-fcond-path-sep" })), _jsx("span", { className: "crisp-tabletoolbar-fcond-path-seg", children: c.label })] }, c.key))) })) : (col?.label)] }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--op", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": opOpen, onClick: () => setOpenMenu(opOpen ? null : `fop:${cond.id}`), children: cond.op
807
- ? (crispMessages[OP_LABELS[cond.op]] ?? OP_LABELS[cond.op])
808
- : selectConditionLabel }), showValueSeg && (_jsx("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--val", "data-menu-trigger": true, "aria-haspopup": "dialog", "aria-expanded": valOpen, onClick: () => setOpenMenu(valOpen ? null : `fval:${cond.id}`), children: cond.op === "isanyof"
833
+ }, children: [col?.icon ? _jsx(col.icon, { size: 14, strokeWidth: 1.75 }) : null, colPath.length > 1 ? (_jsx("span", { className: "crisp-tabletoolbar-fcond-path", children: colPath.map((c, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx(RtlChevronRight, { size: 14, className: "crisp-tabletoolbar-fcond-path-sep" })), _jsx("span", { className: "crisp-tabletoolbar-fcond-path-seg", children: c.label })] }, c.key))) })) : (col?.label)] }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--op", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": opOpen, onClick: () => setOpenMenu(opOpen ? null : `fop:${cond.id}`), children: cond.op ? opLabels[cond.op] : selectConditionLabel }), showValueSeg && (_jsx("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--val", "data-menu-trigger": true, "aria-haspopup": "dialog", "aria-expanded": valOpen, onClick: () => setOpenMenu(valOpen ? null : `fval:${cond.id}`), children: cond.op === "isanyof"
809
834
  ? cond.values?.length
810
835
  ? isAnyOfSummary(cond.values)
811
836
  : filterValuePlaceholder
@@ -813,7 +838,7 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
813
838
  ? col?.type === "date"
814
839
  ? formatFilterDate(cond.value)
815
840
  : cond.value
816
- : filterValuePlaceholder })), _jsx("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--kebab", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: _jsx(MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && (_jsx("div", { ref: kebabMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--right crisp-tabletoolbar-menu--hdr", role: "menu", "aria-label": `${col?.label} ${filterOptionsLabel}`, onKeyDown: onKebabKeyDown, children: _jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [_jsx(Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), fieldOpen && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: nestedFieldPicker(filterColumns, (key) => changeFilterField(cond.id, key), filterFieldPlaceholder, filterFieldLabel) })), opOpen && (_jsx("div", { ref: opMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--op", role: "menu", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, onKeyDown: onOpKeyDown, children: opsFor(col).map((op) => (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => pickOp(cond.id, op), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? (_jsx(Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))) })), valOpen && showValueSeg && (_jsx("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--val", role: "dialog", "aria-label": `${col?.label} filter value`, children: cond.op === "isanyof" && col?.values ? (_jsxs(_Fragment, { children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
841
+ : filterValuePlaceholder })), _jsx("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--kebab", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: _jsx(MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && (_jsx("div", { ref: kebabMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--right crisp-tabletoolbar-menu--hdr", role: "menu", "aria-label": `${col?.label} ${filterOptionsLabel}`, onKeyDown: onKebabKeyDown, children: _jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [_jsx(Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), fieldOpen && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: nestedFieldPicker(filterColumns, (key) => changeFilterField(cond.id, key), filterFieldPlaceholder, filterFieldLabel) })), opOpen && (_jsx("div", { ref: opMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--op", role: "menu", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, onKeyDown: onOpKeyDown, children: opsFor(col).map((op) => (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => pickOp(cond.id, op), children: [opLabels[op], cond.op === op ? (_jsx(Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))) })), valOpen && showValueSeg && (_jsx("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--val", role: "dialog", "aria-label": `${col?.label} filter value`, children: cond.op === "isanyof" && col?.values ? (_jsxs(_Fragment, { children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
817
842
  // biome-ignore lint/a11y/noAutofocus: focus the search field
818
843
  autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
819
844
  .filter((v) => v
@@ -10,7 +10,7 @@ import { useTablistKeyboard } from "./tabs-keyboard.js";
10
10
  * Home/End jump); activation follows focus. Controlled via
11
11
  * `value`/`onValueChange`, or uncontrolled from `defaultValue`.
12
12
  */
13
- export function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, moreLabel = (n) => `+${n} more`, moreMenuLabel = "More tabs", className, fill, "aria-label": ariaLabel, ...rest }) {
13
+ export function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, moreLabel = (n) => `+${n} more`, moreMenuLabel = "More tabs", overflow = "collapse", className, fill, "aria-label": ariaLabel, ...rest }) {
14
14
  // Scoped to this instance: `tab-${value}` and `panel-${value}` are global,
15
15
  // so two tab bars sharing a value mint the same id twice and every
16
16
  // aria-labelledby resolves to the first one (#227).
@@ -57,6 +57,12 @@ export function Tabs({ tabs, value, defaultValue, onValueChange, leading, childr
57
57
  const [hiddenFrom, setHiddenFrom] = React.useState(null);
58
58
  const moreRef = React.useRef(null);
59
59
  React.useLayoutEffect(() => {
60
+ // crisp#908: "scroll" never collapses anything -- the row scrolls
61
+ // instead, so there is nothing to measure or hide.
62
+ if (overflow !== "collapse") {
63
+ setHiddenFrom(null);
64
+ return;
65
+ }
60
66
  const list = listRef.current;
61
67
  if (!list || tabs.length === 0)
62
68
  return;
@@ -97,7 +103,7 @@ export function Tabs({ tabs, value, defaultValue, onValueChange, leading, childr
97
103
  const ro = new ResizeObserver(recompute);
98
104
  ro.observe(list);
99
105
  return () => ro.disconnect();
100
- }, [tabs, active]);
106
+ }, [tabs, active, overflow]);
101
107
  const hiddenTabs = hiddenFrom == null ? [] : tabs.slice(hiddenFrom);
102
108
  const { onKeyDown, tabIndexFor } = useTablistKeyboard({
103
109
  tabs,
@@ -105,11 +111,11 @@ export function Tabs({ tabs, value, defaultValue, onValueChange, leading, childr
105
111
  select,
106
112
  listRef,
107
113
  });
108
- return (_jsxs("div", { className: tabsRecipe({ fill, className }), ...rest, children: [_jsxs("div", { ref: listRef, className: "crisp-tabs-list", role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && _jsx("span", { className: "crisp-tabs-leading", children: leading }), tabs.map((t, i) => {
114
+ return (_jsxs("div", { className: tabsRecipe({ fill, className }), ...rest, children: [_jsxs("div", { ref: listRef, className: "crisp-tabs-list", "data-overflow": overflow, role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && _jsx("span", { className: "crisp-tabs-leading", children: leading }), tabs.map((t, i) => {
109
115
  const selected = t.value === active;
110
116
  const overflowHidden = hiddenFrom != null && i >= hiddenFrom;
111
117
  return (_jsxs("button", { type: "button", role: "tab", id: tabId(t.value), "aria-selected": selected, "aria-controls": children && selected ? panelId(t.value) : undefined, tabIndex: overflowHidden ? -1 : tabIndexFor(t.value), "data-value": t.value, "data-selected": selected ? "" : undefined, "data-tabs-overflow-hidden": overflowHidden ? "" : undefined, disabled: t.disabled, className: "crisp-tabs-tab", onClick: () => select(t.value), children: [t.icon, _jsx("span", { className: "tabs-tab-label", children: t.label }), t.badge != null && (_jsx("span", { className: "crisp-tabs-tab-badge", children: t.badge }))] }, t.value));
112
- }), _jsx("span", { ref: moreRef, className: "crisp-tabs-more", "data-tabs-overflow-hidden": hiddenTabs.length === 0 ? "" : undefined, children: _jsx(Menu, { "aria-label": moreMenuLabel, trigger: _jsxs("button", { type: "button", className: "crisp-tabs-tab", children: [_jsx("span", { className: "tabs-tab-label", children: moreLabel(hiddenTabs.length) }), _jsx(ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" })] }), children: hiddenTabs.map((t) => (_jsxs(MenuItem, { icon: t.icon, onSelect: () => select(t.value), children: [t.label, t.badge != null && (_jsx("span", { className: "crisp-tabs-tab-badge", children: t.badge }))] }, t.value))) }) }), indicator && (_jsx("span", { className: "crisp-tabs-indicator", style: {
118
+ }), overflow === "collapse" && (_jsx("span", { ref: moreRef, className: "crisp-tabs-more", "data-tabs-overflow-hidden": hiddenTabs.length === 0 ? "" : undefined, children: _jsx(Menu, { "aria-label": moreMenuLabel, trigger: _jsxs("button", { type: "button", className: "crisp-tabs-tab", children: [_jsx("span", { className: "tabs-tab-label", children: moreLabel(hiddenTabs.length) }), _jsx(ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" })] }), children: hiddenTabs.map((t) => (_jsxs(MenuItem, { icon: t.icon, onSelect: () => select(t.value), children: [t.label, t.badge != null && (_jsx("span", { className: "crisp-tabs-tab-badge", children: t.badge }))] }, t.value))) }) })), indicator && (_jsx("span", { className: "crisp-tabs-indicator", style: {
113
119
  width: indicator.width,
114
120
  transform: `translateX(${indicator.left}px)`,
115
121
  } }))] }), children && (_jsx("div", { className: "crisp-tabs-panel", role: "tabpanel", id: panelId(active), "aria-labelledby": tabId(active),
@@ -5,10 +5,20 @@ import { contract as c } from "../../tokens/index.js";
5
5
  // baseline needs, reserved as bottom padding so it's real box height and not
6
6
  // paint that trailing content can sit on top of. The bar carries 12px inline
7
7
  // padding too, and draws its own full-width `::after` baseline in
8
- // borderSubtle at its bottom edge, spanning that padding. The active tab is
9
- // a light pill — bg #fbfbfb + inset ring borderSubtle, r8 — with a 1px ink
10
- // underline that sits on the baseline (painted above it) and slides along it;
11
- // inactive tabs are muted and fill to a subtle pill on hover. Each tab carries
8
+ // borderSubtle at its bottom edge, spanning that padding.
9
+ //
10
+ // your-moon/crisp#934: the selected tab used to be a light
11
+ // filled pill (bg #fbfbfb + inset ring borderSubtle, r8) with a 1px ink
12
+ // underline on top of it. Owner-directed: underline tabs, no fill pill --
13
+ // seed-design's own `tab.yaml` has no `root.color` property at all, at any
14
+ // state (selected/hover/pressed); selection is carried purely by the label
15
+ // darkening (muted -> ink, unchanged, already matched seed) plus the
16
+ // existing underline (already `fgPrimary`, "same colour as selected text,
17
+ // not brand" -- also already matched seed). Only the selected pill's own
18
+ // fill/ring is removed here; unselected tabs keep their existing subtle
19
+ // hover tint (a desktop hover affordance seed's spec doesn't need to
20
+ // consider one way or the other, and the owner didn't ask to remove).
21
+ // Inactive tabs are muted and fill to a subtle pill on hover. Each tab carries
12
22
  // a 16px leading icon and an optional count badge. Measured from the live
13
23
  // app: bar `padding: 0 12px 8px`, 28px pills, 4/8 padding, 6px icon-label
14
24
  // gap, 14/500 labels. The count badge (crisp#697, re-measured against a
@@ -45,11 +55,18 @@ const config = defineRecipe({
45
55
 
46
56
  & .crisp-tabs-list { position: relative; display: flex; align-items: center; gap: ${c.space["1"]}; box-sizing: content-box; height: ${c.size.row}; padding: 0 ${c.space["3"]} ${c.space["2"]}; flex-shrink: 0; }
47
57
  & .crisp-tabs-list::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: ${c.borderSubtle}; }
58
+ /* crisp#908: an alternative to the "+N more" collapse above (crisp#863) --
59
+ a consumer opts into a single scrolling row instead (Naver/Kakao's own
60
+ pattern, docs/study/korean/), never both at once. No new visual value:
61
+ reuses the same list gap/tab geometry, only adds overflow-x + snap. */
62
+ & .crisp-tabs-list[data-overflow="scroll"] { overflow-x: auto; flex-wrap: nowrap; scroll-snap-type: x mandatory; scrollbar-width: none; }
63
+ & .crisp-tabs-list[data-overflow="scroll"]::-webkit-scrollbar { display: none; }
64
+ & .crisp-tabs-list[data-overflow="scroll"] .crisp-tabs-tab { flex-shrink: 0; scroll-snap-align: start; }
48
65
  & .crisp-tabs-leading { display: inline-flex; align-items: center; margin-inline-end: ${c.space["1"]}; }
49
- & .crisp-tabs-tab { position: relative; display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 28px; margin: 0; padding: 0 ${c.space["2"]}; border: none; background: none; box-shadow: inset 0 0 0 1px transparent; border-radius: ${c.radius.md}; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.14px; color: ${c.fgMuted}; cursor: pointer; white-space: nowrap; transition: color .12s ease, background-color .12s ease, box-shadow .12s ease; }
66
+ & .crisp-tabs-tab { position: relative; display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 28px; margin: 0; padding: 0 ${c.space["2"]}; border: none; background: none; border-radius: ${c.radius.md}; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.14px; color: ${c.fgMuted}; cursor: pointer; white-space: nowrap; transition: color .12s ease, background-color .12s ease; }
50
67
  & .crisp-tabs-tab svg { flex-shrink: 0; opacity: 0.85; }
51
68
  & .crisp-tabs-tab:hover:not([data-selected]) { background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); color: ${c.fgPrimary}; }
52
- & .crisp-tabs-tab[data-selected] { color: ${c.fgPrimary}; background: ${c.bgSubtle}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; }
69
+ & .crisp-tabs-tab[data-selected] { color: ${c.fgPrimary}; }
53
70
  & .crisp-tabs-tab:focus-visible { outline: 2px solid ${c.bgBrandSolid}; outline-offset: 1px; }
54
71
  & .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 14px; height: 14px; padding: 0 ${c.space["1"]}; border-radius: 4px; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); box-shadow: inset 0 0 0 1px ${c.borderSubtle}; font-size: 10px; line-height: 14px; font-weight: ${c.weight.medium}; color: ${c.fgNeutralSoft}; }
55
72
  & .crisp-tabs-tab[data-selected] .crisp-tabs-tab-badge { color: ${c.fgPrimary}; }
@@ -1,13 +1,16 @@
1
1
  import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
- import { contract as c, elevation as e } from "../../tokens/index.js";
3
- // A multiline text field: an inset 1px ring (radius 8), 14px text, and an
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // A multiline text field: a real 1px border (radius 8), 14px text, and an
4
4
  // optional character counter pinned bottom-right (12px, muted). Focus rings
5
5
  // the field brand.
6
6
  //
7
- // The ring is `elevation.textField` (#466), not `elevation.field` — this
8
- // comment already correctly described "an inset 1px ring", but the code
9
- // pulled in `field`'s own button-chrome shadow (a ring PLUS a glow and a
10
- // drop-shadow layer), which live Attio never carries on a real typed field.
7
+ // your-moon/crisp#920: the rest-state edge used to be `elevation.textField`,
8
+ // a `box-shadow` ring (#466's fix for a real typed field's 1px ring, not
9
+ // `field`'s button-chrome glow/drop-shadow layers) -- now a real `border`,
10
+ // matching Input's own #914/#910 technique and seed-design's `text-input.yaml`
11
+ // (an actual border that THICKENS on focus/invalid via the ::after overlay
12
+ // below, not a fixed-width ring that only recolors). `borderDefault` is
13
+ // seed's own `stroke.neutral-weak`, already the crisp token's exact value.
11
14
  //
12
15
  // The inset is seed-design's, not Attio's -- the second deliberate divergence
13
16
  // after the focus treatment. seed's text-input sets its inline inset to
@@ -24,10 +27,10 @@ const config = defineRecipe({
24
27
 
25
28
  & .crisp-textarea-field {
26
29
  box-sizing: border-box; display: block; width: 100%; min-height: 80px; resize: vertical;
27
- margin: 0; padding: ${c.space["2.5"]} 14px; border: none; border-radius: ${c.radius.md};
28
- background: ${c.bgSurface}; box-shadow: ${e.textField};
30
+ margin: 0; padding: ${c.space["2.5"]} 14px; border-radius: ${c.radius.md};
31
+ background: ${c.bgSurface}; border: 1px solid ${c.borderDefault};
29
32
  font-family: inherit; font-size: ${c.text.lg}; line-height: 20px; color: ${c.fgPrimary};
30
- outline: none; transition: box-shadow .15s ease;
33
+ outline: none;
31
34
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
32
35
  }
33
36
  & .crisp-textarea-field::placeholder { color: ${c.fgMuted}; }
@@ -16,10 +16,11 @@ import { createPortal } from "react-dom";
16
16
  * `Dialog` was rendering offset by the dialog's own box instead of anchored
17
17
  * to its trigger.
18
18
  *
19
- * Every caller renders this conditionally on its own `open` state, which
20
- * starts `false` and is driven only by client interaction — never a prop a
21
- * fixture sets true at mount (unlike Dialog/Drawer/Command) — so this never
22
- * touches `document` during SSR, the same safety Tooltip already had.
19
+ * Every caller renders this conditionally on its own `open` state. A fixture
20
+ * CAN set that true at mount (crisp#964 needs exactly that, to give Menu's
21
+ * roving-tabindex guard something to inspect), so this checks for `window`
22
+ * itself rather than assuming the caller never does — the same guard
23
+ * `@zag-js/react`'s own `Portal` uses for the identical reason.
23
24
  *
24
25
  * `sourceRef` is the caller's own root — still in its original DOM position,
25
26
  * never portaled — used to carry a `data-theme` set on some ancestor of it
@@ -35,6 +36,8 @@ import { createPortal } from "react-dom";
35
36
  * element carrying that attribute.
36
37
  */
37
38
  export function FloatingPortal({ children, sourceRef, }) {
39
+ if (typeof window === "undefined")
40
+ return children;
38
41
  const theme = sourceRef.current
39
42
  ?.closest("[data-theme]")
40
43
  ?.getAttribute("data-theme");
package/dist/esm/index.js CHANGED
@@ -8,6 +8,7 @@ export * from "./components/avatar/index.js";
8
8
  export * from "./components/avatargroup/index.js";
9
9
  export * from "./components/badge/index.js";
10
10
  export * from "./components/barchart/index.js";
11
+ export * from "./components/bottomsheet/index.js";
11
12
  export * from "./components/breadcrumb/index.js";
12
13
  export * from "./components/bulkbar/index.js";
13
14
  export * from "./components/button/index.js";
@@ -15,6 +16,7 @@ export * from "./components/callout/index.js";
15
16
  export * from "./components/card/index.js";
16
17
  export * from "./components/checkbox/index.js";
17
18
  export * from "./components/chip/index.js";
19
+ export * from "./components/chipgroup/index.js";
18
20
  export * from "./components/columncalc/index.js";
19
21
  export * from "./components/composewindow/index.js";
20
22
  export * from "./components/contextmenu/index.js";
@@ -36,6 +38,7 @@ export * from "./components/kbd/index.js";
36
38
  export * from "./components/linechart/index.js";
37
39
  export * from "./components/mentionchip/index.js";
38
40
  export * from "./components/menu/index.js";
41
+ export * from "./components/menusheet/index.js";
39
42
  export * from "./components/middletruncate/index.js";
40
43
  export * from "./components/multiselect/index.js";
41
44
  export * from "./components/personchip/index.js";
@@ -49,6 +52,7 @@ export * from "./components/select/index.js";
49
52
  export * from "./components/skeleton/index.js";
50
53
  export * from "./components/skiplink/index.js";
51
54
  export * from "./components/slider/index.js";
55
+ export * from "./components/snackbar/index.js";
52
56
  export * from "./components/spinner/index.js";
53
57
  export * from "./components/stagebar/index.js";
54
58
  export * from "./components/statcard/index.js";
@@ -83,7 +87,9 @@ export * from "./product/headerbar/index.js";
83
87
  export * from "./product/importdialog/index.js";
84
88
  export * from "./product/inbox/index.js";
85
89
  export * from "./product/listpage/index.js";
90
+ export * from "./product/memberssettings/index.js";
86
91
  export * from "./product/navitem/index.js";
92
+ export * from "./product/notificationssettings/index.js";
87
93
  export * from "./product/objectsettings/index.js";
88
94
  export * from "./product/onboarding/index.js";
89
95
  export * from "./product/recordpage/index.js";
@@ -98,5 +104,6 @@ export * from "./product/tasklist/index.js";
98
104
  export * from "./product/themepicker/index.js";
99
105
  export * from "./product/trialbanner/index.js";
100
106
  export * from "./product/viewtoggle/index.js";
107
+ export * from "./product/workspacegeneralsettings/index.js";
101
108
  // design tokens + recipe core (for building custom components)
102
109
  export * from "./tokens/index.js";
@@ -40,7 +40,11 @@ const config = defineRecipe({
40
40
  & .crisp-authshell-progress-fill { height: 2px; background: ${c.fgPrimary}; border-radius: 4px; transition: width .2s; }
41
41
 
42
42
  & .crisp-authshell-logo { margin-top: ${c.space["8"]}; min-height: 24px; display: flex; align-items: center; gap: ${c.space["2.5"]}; }
43
- & .crisp-authshell-logo-badge { flex-shrink: 0; box-sizing: border-box; width: 32px; height: 32px; border-radius: ${c.radius.md}; background: ${c.bgBrandSolid}; color: #fff; display: flex; align-items: center; justify-content: center; }
43
+ /* your-moon/crisp#953: bgBrandContrast, not bgBrandSolid --
44
+ the accent (#0298d9) only clears 3.23:1 with white; the darker
45
+ sibling clears 4.5:1+, same fix already applied to Button/Badge/
46
+ Avatar (#897). */
47
+ & .crisp-authshell-logo-badge { flex-shrink: 0; box-sizing: border-box; width: 32px; height: 32px; border-radius: ${c.radius.md}; background: ${c.bgBrandContrast}; color: #fff; display: flex; align-items: center; justify-content: center; }
44
48
  & .crisp-authshell-logo-text { display: flex; flex-direction: column; line-height: 1.2; }
45
49
  & .crisp-authshell-logo-title { font-size: ${c.text.xl}; font-weight: ${c.weight.bold}; letter-spacing: -0.2px; color: ${c.fgPrimary}; }
46
50
  & .crisp-authshell-logo-subtitle { font-size: ${c.text.sm}; color: ${c.fgMuted}; }