@crisp-ui-kit/crisp 0.57.1 → 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 (252) 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 +7 -6
  11. package/dist/button.layered.css +7 -6
  12. package/dist/checkbox.css +7 -3
  13. package/dist/checkbox.layered.css +7 -3
  14. package/dist/chip.css +15 -0
  15. package/dist/chip.d.ts +3 -1
  16. package/dist/chip.layered.css +15 -0
  17. package/dist/chipgroup.css +5 -0
  18. package/dist/chipgroup.d.ts +7 -0
  19. package/dist/chipgroup.layered.css +7 -0
  20. package/dist/cjs/components/badge/badge.recipe.js +22 -8
  21. package/dist/cjs/components/bottomsheet/bottomsheet.js +90 -0
  22. package/dist/cjs/components/bottomsheet/bottomsheet.recipe.js +77 -0
  23. package/dist/cjs/components/bottomsheet/index.js +9 -0
  24. package/dist/cjs/components/button/button.recipe.js +39 -15
  25. package/dist/cjs/components/checkbox/checkbox.recipe.js +61 -9
  26. package/dist/cjs/components/chip/chip.js +14 -2
  27. package/dist/cjs/components/chip/chip.recipe.js +69 -1
  28. package/dist/cjs/components/chipgroup/chipgroup.js +51 -0
  29. package/dist/cjs/components/chipgroup/chipgroup.recipe.js +36 -0
  30. package/dist/cjs/components/chipgroup/index.js +7 -0
  31. package/dist/cjs/components/datatable/datatable.js +5 -1
  32. package/dist/cjs/components/datatable/datatable.recipe.js +11 -2
  33. package/dist/cjs/components/dialog/dialog.recipe.js +21 -10
  34. package/dist/cjs/components/emptystate/emptystate.recipe.js +7 -2
  35. package/dist/cjs/components/fileupload/fileupload.js +21 -4
  36. package/dist/cjs/components/input/index.js +2 -1
  37. package/dist/cjs/components/input/input.js +31 -1
  38. package/dist/cjs/components/input/input.recipe.js +16 -9
  39. package/dist/cjs/components/menu/menu.recipe.js +21 -7
  40. package/dist/cjs/components/menusheet/index.js +9 -0
  41. package/dist/cjs/components/menusheet/menusheet.js +133 -0
  42. package/dist/cjs/components/menusheet/menusheet.recipe.js +103 -0
  43. package/dist/cjs/components/multiselect/multiselect.js +1 -1
  44. package/dist/cjs/components/multiselect/multiselect.recipe.js +33 -9
  45. package/dist/cjs/components/select/select.js +1 -1
  46. package/dist/cjs/components/select/select.recipe.js +38 -12
  47. package/dist/cjs/components/snackbar/index.js +8 -0
  48. package/dist/cjs/components/snackbar/snackbar.js +121 -0
  49. package/dist/cjs/components/snackbar/snackbar.recipe.js +71 -0
  50. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +7 -2
  51. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +37 -12
  52. package/dist/cjs/components/tabs/tabs.js +10 -4
  53. package/dist/cjs/components/tabs/tabs.recipe.js +23 -6
  54. package/dist/cjs/components/textarea/textarea.recipe.js +11 -8
  55. package/dist/cjs/index.js +7 -0
  56. package/dist/cjs/product/authshell/authshell.recipe.js +5 -1
  57. package/dist/cjs/product/command/command.recipe.js +4 -1
  58. package/dist/cjs/product/comments/comments.js +7 -2
  59. package/dist/cjs/product/comments/comments.recipe.js +25 -7
  60. package/dist/cjs/product/memberssettings/index.js +6 -0
  61. package/dist/cjs/product/memberssettings/memberssettings.js +99 -0
  62. package/dist/cjs/product/memberssettings/memberssettings.recipe.js +83 -0
  63. package/dist/cjs/product/notificationssettings/index.js +5 -0
  64. package/dist/cjs/product/notificationssettings/notificationssettings.js +19 -0
  65. package/dist/cjs/product/notificationssettings/notificationssettings.recipe.js +44 -0
  66. package/dist/cjs/product/recordpage/recordpage.js +57 -6
  67. package/dist/cjs/product/recordpage/recordpage.recipe.js +43 -6
  68. package/dist/cjs/product/workspacegeneralsettings/index.js +5 -0
  69. package/dist/cjs/product/workspacegeneralsettings/workspacegeneralsettings.js +21 -0
  70. package/dist/cjs/product/workspacegeneralsettings/workspacegeneralsettings.recipe.js +51 -0
  71. package/dist/cjs/tokens/contract.js +1 -0
  72. package/dist/cjs/tokens/elevation.js +8 -5
  73. package/dist/cjs/tokens/palette.js +28 -8
  74. package/dist/cjs/tokens/scale.js +44 -2
  75. package/dist/cjs/tokens/semantic.js +2 -0
  76. package/dist/columncalc.css +15 -7
  77. package/dist/columncalc.layered.css +15 -7
  78. package/dist/command.css +23 -19
  79. package/dist/command.layered.css +23 -19
  80. package/dist/comments.css +28 -19
  81. package/dist/comments.d.ts +2 -2
  82. package/dist/comments.layered.css +28 -19
  83. package/dist/datatable.css +161 -67
  84. package/dist/datatable.layered.css +161 -67
  85. package/dist/datepicker.css +13 -41
  86. package/dist/datepicker.layered.css +13 -41
  87. package/dist/datepickerfield.css +4 -1
  88. package/dist/datepickerfield.layered.css +4 -1
  89. package/dist/dialog.css +12 -7
  90. package/dist/dialog.layered.css +12 -7
  91. package/dist/emptystate.css +5 -3
  92. package/dist/emptystate.layered.css +5 -3
  93. package/dist/esm/components/badge/badge.recipe.js +22 -8
  94. package/dist/esm/components/bottomsheet/bottomsheet.js +52 -0
  95. package/dist/esm/components/bottomsheet/bottomsheet.recipe.js +74 -0
  96. package/dist/esm/components/bottomsheet/index.js +2 -0
  97. package/dist/esm/components/button/button.recipe.js +40 -16
  98. package/dist/esm/components/checkbox/checkbox.recipe.js +61 -9
  99. package/dist/esm/components/chip/chip.js +14 -2
  100. package/dist/esm/components/chip/chip.recipe.js +69 -1
  101. package/dist/esm/components/chipgroup/chipgroup.js +15 -0
  102. package/dist/esm/components/chipgroup/chipgroup.recipe.js +33 -0
  103. package/dist/esm/components/chipgroup/index.js +2 -0
  104. package/dist/esm/components/datatable/datatable.js +5 -1
  105. package/dist/esm/components/datatable/datatable.recipe.js +11 -2
  106. package/dist/esm/components/dialog/dialog.recipe.js +22 -11
  107. package/dist/esm/components/emptystate/emptystate.recipe.js +7 -2
  108. package/dist/esm/components/fileupload/fileupload.js +20 -5
  109. package/dist/esm/components/input/index.js +1 -1
  110. package/dist/esm/components/input/input.js +30 -0
  111. package/dist/esm/components/input/input.recipe.js +17 -10
  112. package/dist/esm/components/menu/menu.recipe.js +21 -7
  113. package/dist/esm/components/menusheet/index.js +2 -0
  114. package/dist/esm/components/menusheet/menusheet.js +95 -0
  115. package/dist/esm/components/menusheet/menusheet.recipe.js +100 -0
  116. package/dist/esm/components/multiselect/multiselect.js +1 -1
  117. package/dist/esm/components/multiselect/multiselect.recipe.js +33 -9
  118. package/dist/esm/components/select/select.js +1 -1
  119. package/dist/esm/components/select/select.recipe.js +38 -12
  120. package/dist/esm/components/snackbar/index.js +2 -0
  121. package/dist/esm/components/snackbar/snackbar.js +84 -0
  122. package/dist/esm/components/snackbar/snackbar.recipe.js +68 -0
  123. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +7 -2
  124. package/dist/esm/components/tabletoolbar/tabletoolbar.js +37 -12
  125. package/dist/esm/components/tabs/tabs.js +10 -4
  126. package/dist/esm/components/tabs/tabs.recipe.js +23 -6
  127. package/dist/esm/components/textarea/textarea.recipe.js +12 -9
  128. package/dist/esm/index.js +7 -0
  129. package/dist/esm/product/authshell/authshell.recipe.js +5 -1
  130. package/dist/esm/product/command/command.recipe.js +4 -1
  131. package/dist/esm/product/comments/comments.js +7 -2
  132. package/dist/esm/product/comments/comments.recipe.js +25 -7
  133. package/dist/esm/product/memberssettings/index.js +1 -0
  134. package/dist/esm/product/memberssettings/memberssettings.js +62 -0
  135. package/dist/esm/product/memberssettings/memberssettings.recipe.js +80 -0
  136. package/dist/esm/product/notificationssettings/index.js +1 -0
  137. package/dist/esm/product/notificationssettings/notificationssettings.js +16 -0
  138. package/dist/esm/product/notificationssettings/notificationssettings.recipe.js +41 -0
  139. package/dist/esm/product/recordpage/recordpage.js +58 -7
  140. package/dist/esm/product/recordpage/recordpage.recipe.js +43 -6
  141. package/dist/esm/product/workspacegeneralsettings/index.js +1 -0
  142. package/dist/esm/product/workspacegeneralsettings/workspacegeneralsettings.js +18 -0
  143. package/dist/esm/product/workspacegeneralsettings/workspacegeneralsettings.recipe.js +48 -0
  144. package/dist/esm/tokens/contract.js +2 -1
  145. package/dist/esm/tokens/elevation.js +8 -5
  146. package/dist/esm/tokens/palette.js +28 -8
  147. package/dist/esm/tokens/scale.js +43 -1
  148. package/dist/esm/tokens/semantic.js +2 -0
  149. package/dist/fileupload.css +3 -1
  150. package/dist/fileupload.layered.css +3 -1
  151. package/dist/input.css +6 -4
  152. package/dist/input.layered.css +6 -4
  153. package/dist/listpage.css +3 -1
  154. package/dist/listpage.layered.css +3 -1
  155. package/dist/memberssettings.css +67 -0
  156. package/dist/memberssettings.d.ts +7 -0
  157. package/dist/memberssettings.layered.css +69 -0
  158. package/dist/menu.css +2 -2
  159. package/dist/menu.layered.css +2 -2
  160. package/dist/menusheet.css +36 -0
  161. package/dist/menusheet.d.ts +7 -0
  162. package/dist/menusheet.layered.css +38 -0
  163. package/dist/multiselect.css +13 -6
  164. package/dist/multiselect.layered.css +13 -6
  165. package/dist/native/tokens.d.ts +2 -1
  166. package/dist/native/tokens.js +20 -3
  167. package/dist/notificationssettings.css +22 -0
  168. package/dist/notificationssettings.d.ts +7 -0
  169. package/dist/notificationssettings.layered.css +24 -0
  170. package/dist/objectsettings.css +4 -4
  171. package/dist/objectsettings.layered.css +4 -4
  172. package/dist/recordpage.css +67 -23
  173. package/dist/recordpage.layered.css +67 -23
  174. package/dist/recordpanel.css +35 -16
  175. package/dist/recordpanel.layered.css +35 -16
  176. package/dist/resizablepanel.css +8 -5
  177. package/dist/resizablepanel.layered.css +8 -5
  178. package/dist/richtext.css +24 -8
  179. package/dist/richtext.layered.css +24 -8
  180. package/dist/savedviews.css +9 -2
  181. package/dist/savedviews.layered.css +9 -2
  182. package/dist/select.css +25 -5
  183. package/dist/select.layered.css +25 -5
  184. package/dist/settingsshell.css +2 -3
  185. package/dist/settingsshell.layered.css +2 -3
  186. package/dist/slider.css +6 -27
  187. package/dist/slider.layered.css +6 -27
  188. package/dist/snackbar.css +24 -0
  189. package/dist/snackbar.d.ts +7 -0
  190. package/dist/snackbar.layered.css +26 -0
  191. package/dist/statcard.css +3 -1
  192. package/dist/statcard.layered.css +3 -1
  193. package/dist/styles.css +859 -345
  194. package/dist/styles.layered.css +859 -345
  195. package/dist/table.css +28 -10
  196. package/dist/table.layered.css +28 -10
  197. package/dist/tabletoolbar.css +35 -24
  198. package/dist/tabletoolbar.layered.css +35 -24
  199. package/dist/tabs.css +18 -4
  200. package/dist/tabs.layered.css +18 -4
  201. package/dist/textarea.css +2 -6
  202. package/dist/textarea.layered.css +2 -6
  203. package/dist/themepicker.css +2 -1
  204. package/dist/themepicker.layered.css +2 -1
  205. package/dist/tokens.css +13 -2
  206. package/dist/tokens.json +20 -3
  207. package/dist/types/components/bottomsheet/bottomsheet.d.ts +35 -0
  208. package/dist/types/components/bottomsheet/bottomsheet.recipe.d.ts +3 -0
  209. package/dist/types/components/bottomsheet/index.d.ts +2 -0
  210. package/dist/types/components/button/button.recipe.d.ts +11 -6
  211. package/dist/types/components/checkbox/checkbox.recipe.d.ts +28 -5
  212. package/dist/types/components/chip/chip.d.ts +22 -0
  213. package/dist/types/components/chip/chip.recipe.d.ts +48 -0
  214. package/dist/types/components/chipgroup/chipgroup.d.ts +14 -0
  215. package/dist/types/components/chipgroup/chipgroup.recipe.d.ts +5 -0
  216. package/dist/types/components/chipgroup/index.d.ts +2 -0
  217. package/dist/types/components/datatable/datatable.d.ts +19 -0
  218. package/dist/types/components/fileupload/fileupload.d.ts +26 -0
  219. package/dist/types/components/input/index.d.ts +1 -1
  220. package/dist/types/components/input/input.d.ts +72 -0
  221. package/dist/types/components/menusheet/index.d.ts +2 -0
  222. package/dist/types/components/menusheet/menusheet.d.ts +42 -0
  223. package/dist/types/components/menusheet/menusheet.recipe.d.ts +3 -0
  224. package/dist/types/components/snackbar/index.d.ts +2 -0
  225. package/dist/types/components/snackbar/snackbar.d.ts +36 -0
  226. package/dist/types/components/snackbar/snackbar.recipe.d.ts +5 -0
  227. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +28 -1
  228. package/dist/types/components/tabs/tabs.d.ts +11 -1
  229. package/dist/types/index.d.ts +7 -0
  230. package/dist/types/product/comments/comments.d.ts +9 -3
  231. package/dist/types/product/comments/comments.recipe.d.ts +12 -2
  232. package/dist/types/product/memberssettings/index.d.ts +1 -0
  233. package/dist/types/product/memberssettings/memberssettings.d.ts +85 -0
  234. package/dist/types/product/memberssettings/memberssettings.recipe.d.ts +3 -0
  235. package/dist/types/product/notificationssettings/index.d.ts +1 -0
  236. package/dist/types/product/notificationssettings/notificationssettings.d.ts +37 -0
  237. package/dist/types/product/notificationssettings/notificationssettings.recipe.d.ts +3 -0
  238. package/dist/types/product/recordpage/recordpage.d.ts +53 -2
  239. package/dist/types/product/workspacegeneralsettings/index.d.ts +1 -0
  240. package/dist/types/product/workspacegeneralsettings/workspacegeneralsettings.d.ts +48 -0
  241. package/dist/types/product/workspacegeneralsettings/workspacegeneralsettings.recipe.d.ts +3 -0
  242. package/dist/types/tokens/contract.d.ts +4 -1
  243. package/dist/types/tokens/elevation.d.ts +8 -5
  244. package/dist/types/tokens/palette.d.ts +9 -1
  245. package/dist/types/tokens/scale.d.ts +11 -1
  246. package/dist/types/tokens/semantic.d.ts +2 -0
  247. package/dist/wheelpicker.css +4 -2
  248. package/dist/wheelpicker.layered.css +4 -2
  249. package/dist/workspacegeneralsettings.css +30 -0
  250. package/dist/workspacegeneralsettings.d.ts +7 -0
  251. package/dist/workspacegeneralsettings.layered.css +32 -0
  252. package/package.json +36 -1
@@ -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}; }
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}; }
@@ -29,7 +29,10 @@ const config = defineRecipe({
29
29
  & .crisp-command-list > .crisp-command-group-label:first-child { padding-top: ${c.space["1"]}; }
30
30
  & .crisp-command-footer { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; height: 44px; padding: 0 ${c.space["2.5"]}; border-top: 1px solid ${c.borderSubtle}; }
31
31
  & .crisp-command-footer-nav { display: flex; align-items: center; gap: ${c.space["1.5"]}; font-size: ${c.text.sm}; color: ${c.fgMuted}; }
32
- & .crisp-command-footer-action { display: flex; align-items: center; gap: ${c.space["2"]}; height: 28px; max-width: 60%; 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; }
32
+ /* your-moon/crisp#953: bgBrandContrast, not bgBrandSolid --
33
+ the accent (#0298d9) only clears 3.23:1 with white; the darker sibling
34
+ clears 4.5:1+, same fix already applied to Button/Badge/Avatar (#897). */
35
+ & .crisp-command-footer-action { display: flex; align-items: center; gap: ${c.space["2"]}; height: 28px; max-width: 60%; 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; }
33
36
  & .crisp-command-footer-action:disabled { opacity: 0.5; cursor: default; }
34
37
  & .crisp-command-footer-action-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
35
38
  & .crisp-command-footer-key { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; background: rgba(255,255,255,0.22); font-size: ${c.text.xs}; }
@@ -277,8 +277,13 @@ function CommentRow({ comment, replyLabel, reactLabel, resolveLabel, moreLabel,
277
277
  * nothing on its own beyond that data. See `CommentCountBadge` for the
278
278
  * separate row affordance (P-051) — deliberately NOT part of this
279
279
  * component, since it lives in a table cell this component doesn't own.
280
+ *
281
+ * `variant="drawer"` (default) is today's measured 320px + left-border
282
+ * look; `variant="pane"` (crisp#960) fills its container's width instead,
283
+ * for a consumer embedding this in a full-width tab pane rather than a
284
+ * true slide-over/drawer.
280
285
  */
281
- export function CommentsPanel({ threads, currentUserId, subject, subjectIcon, showResolved = false, onShowResolvedChange, onlyMine = false, onOnlyMineChange, activeThreadId: activeThreadIdProp, onActiveThreadChange, onPost, onReact, onResolve, onDelete, onCopyLink, emojiOptions = DEFAULT_EMOJI, mentionTrigger = "@", renderMentionPicker, onClose, closeLabel: closeLabelProp, backLabel: backLabelProp, title: titleProp, threadTitle: threadTitleProp, breadcrumbPrefix: breadcrumbPrefixProp, composerPlaceholder: composerPlaceholderProp, replyPlaceholder: replyPlaceholderProp, postLabel: postLabelProp, replyPostLabel: replyPostLabelProp, emptyLabel, filterLabel: filterLabelProp, showResolvedLabel: showResolvedLabelProp, onlyMineLabel: onlyMineLabelProp, replyActionLabel: replyActionLabelProp, reactActionLabel: reactActionLabelProp, resolveActionLabel: resolveActionLabelProp, resolvedByYouLabel: resolvedByYouLabelProp, resolvedByLabel, moreActionsLabel: moreActionsLabelProp, copyLinkLabel: copyLinkLabelProp, deleteLabel: deleteLabelProp, addReactionLabel: addReactionLabelProp, reactionSearchLabel: reactionSearchLabelProp, deleteConfirmTitle: deleteConfirmTitleProp, deleteConfirmBody: deleteConfirmBodyProp, deleteConfirmActionLabel: deleteConfirmActionLabelProp, cancelLabel: cancelLabelProp, composerMentionToolLabel: composerMentionToolLabelProp, className, ...rest }) {
286
+ export function CommentsPanel({ threads, currentUserId, subject, subjectIcon, showResolved = false, onShowResolvedChange, onlyMine = false, onOnlyMineChange, activeThreadId: activeThreadIdProp, onActiveThreadChange, onPost, onReact, onResolve, onDelete, onCopyLink, emojiOptions = DEFAULT_EMOJI, mentionTrigger = "@", renderMentionPicker, onClose, closeLabel: closeLabelProp, backLabel: backLabelProp, title: titleProp, threadTitle: threadTitleProp, breadcrumbPrefix: breadcrumbPrefixProp, composerPlaceholder: composerPlaceholderProp, replyPlaceholder: replyPlaceholderProp, postLabel: postLabelProp, replyPostLabel: replyPostLabelProp, emptyLabel, filterLabel: filterLabelProp, showResolvedLabel: showResolvedLabelProp, onlyMineLabel: onlyMineLabelProp, replyActionLabel: replyActionLabelProp, reactActionLabel: reactActionLabelProp, resolveActionLabel: resolveActionLabelProp, resolvedByYouLabel: resolvedByYouLabelProp, resolvedByLabel, moreActionsLabel: moreActionsLabelProp, copyLinkLabel: copyLinkLabelProp, deleteLabel: deleteLabelProp, addReactionLabel: addReactionLabelProp, reactionSearchLabel: reactionSearchLabelProp, deleteConfirmTitle: deleteConfirmTitleProp, deleteConfirmBody: deleteConfirmBodyProp, deleteConfirmActionLabel: deleteConfirmActionLabelProp, cancelLabel: cancelLabelProp, composerMentionToolLabel: composerMentionToolLabelProp, variant, className, ...rest }) {
282
287
  const closeLabel = useCrispMessage(closeLabelProp, "Close");
283
288
  const backLabel = useCrispMessage(backLabelProp, "Back");
284
289
  const title = useCrispMessage(titleProp, "All comments");
@@ -331,7 +336,7 @@ export function CommentsPanel({ threads, currentUserId, subject, subjectIcon, sh
331
336
  const activeThread = activeThreadId
332
337
  ? threads.find((t) => t.id === activeThreadId)
333
338
  : null;
334
- return (_jsxs("div", { className: commentsRecipe({ className }), role: "dialog", "aria-label": title, ...rest, children: [_jsxs("div", { className: "crisp-comments-head", children: [activeThread ? (_jsx("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": backLabel, onClick: () => setActiveThreadId(null), children: _jsx(ChevronLeft, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })) : null, _jsxs("div", { className: "crisp-comments-title-wrap", children: [_jsx("span", { className: "crisp-comments-title", children: activeThread ? threadTitle : title }), activeThread && (_jsxs("span", { className: "crisp-comments-breadcrumb", children: [breadcrumbPrefix, " ", subjectIcon, " ", subject] }))] }), !activeThread && (_jsxs(Menu, { align: "end", trigger: _jsx("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": filterLabel, children: _jsx(ListFilter, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: [_jsx(MenuCheckboxItem, { checked: showResolved, onCheckedChange: onShowResolvedChange, children: showResolvedLabel }), _jsx(MenuCheckboxItem, { checked: onlyMine, onCheckedChange: onOnlyMineChange, children: onlyMineLabel })] })), onClose && (_jsx("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": closeLabel, onClick: onClose, children: _jsx(X, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }))] }), _jsx("div", { className: "crisp-comments-body crisp-comments-body--pad-top", children: activeThread ? (_jsxs("div", { children: [_jsx(CommentRow, { comment: activeThread.root, currentUserId: currentUserId, replyLabel: replyActionLabel, reactLabel: reactActionLabel, resolveLabel: resolveActionLabel, moreLabel: moreActionsLabel, copyLinkLabel: copyLinkLabel, deleteLabel: deleteLabel, addReactionLabel: addReactionLabel, reactionSearchLabel: reactionSearchLabel, emojiOptions: emojiOptions, resolvable: !!onResolve, resolved: activeThread.resolved, resolvedLabel: activeThread.resolved
339
+ return (_jsxs("div", { className: commentsRecipe({ variant, className }), role: "dialog", "aria-label": title, ...rest, children: [_jsxs("div", { className: "crisp-comments-head", children: [activeThread ? (_jsx("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": backLabel, onClick: () => setActiveThreadId(null), children: _jsx(ChevronLeft, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })) : null, _jsxs("div", { className: "crisp-comments-title-wrap", children: [_jsx("span", { className: "crisp-comments-title", children: activeThread ? threadTitle : title }), activeThread && (_jsxs("span", { className: "crisp-comments-breadcrumb", children: [breadcrumbPrefix, " ", subjectIcon, " ", subject] }))] }), !activeThread && (_jsxs(Menu, { align: "end", trigger: _jsx("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": filterLabel, children: _jsx(ListFilter, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: [_jsx(MenuCheckboxItem, { checked: showResolved, onCheckedChange: onShowResolvedChange, children: showResolvedLabel }), _jsx(MenuCheckboxItem, { checked: onlyMine, onCheckedChange: onOnlyMineChange, children: onlyMineLabel })] })), onClose && (_jsx("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": closeLabel, onClick: onClose, children: _jsx(X, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }))] }), _jsx("div", { className: "crisp-comments-body crisp-comments-body--pad-top", children: activeThread ? (_jsxs("div", { children: [_jsx(CommentRow, { comment: activeThread.root, currentUserId: currentUserId, replyLabel: replyActionLabel, reactLabel: reactActionLabel, resolveLabel: resolveActionLabel, moreLabel: moreActionsLabel, copyLinkLabel: copyLinkLabel, deleteLabel: deleteLabel, addReactionLabel: addReactionLabel, reactionSearchLabel: reactionSearchLabel, emojiOptions: emojiOptions, resolvable: !!onResolve, resolved: activeThread.resolved, resolvedLabel: activeThread.resolved
335
340
  ? activeThread.resolvedBy?.id === currentUserId ||
336
341
  !activeThread.resolvedBy
337
342
  ? resolvedByYouLabel
@@ -15,8 +15,8 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
15
15
  // width and still gets an odd number).
16
16
  const config = defineRecipe({
17
17
  name: "comments",
18
- base: `display: flex; flex-direction: column; box-sizing: border-box; width: 320px; height: 100%;
19
- background: ${c.bgSurface}; border-inline-start: 1px solid ${c.borderSubtle};
18
+ base: `display: flex; flex-direction: column; box-sizing: border-box; height: 100%;
19
+ background: ${c.bgSurface};
20
20
  font-family: ${c.font.sans};
21
21
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
22
22
  & * { box-sizing: border-box; }
@@ -96,9 +96,13 @@ const config = defineRecipe({
96
96
  box-shadow, so this reuses that literal rather than re-deriving it.
97
97
  32px/radius.lg/6px-10px padding has no matching Button \`size\` yet
98
98
  (24/28/40 are the three that exist), so this stays hand-rolled instead
99
- of stretching one of those. */
100
- & .crisp-comments-post { display: inline-flex; align-items: center; justify-content: center; height: 32px; padding: 6px ${c.space["2.5"]}; border: none; border-radius: ${c.radius.lg}; background: ${c.bgBrandSolid}; box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08); font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.semibold}; color: ${c.fgOnBrand}; cursor: pointer; opacity: 1; transition: opacity .12s, background-color .12s; }
101
- & .crisp-comments-post:hover:not(:disabled) { background: ${c.bgBrandSolidPressed}; }
99
+ of stretching one of those. your-moon/crisp#953:
100
+ bgBrandContrast, not bgBrandSolid -- the accent (#0298d9) only clears
101
+ 3.23:1 with white; same fix already applied to Button/Badge/Avatar
102
+ (#897). Hover is a brightness filter on the new fill, matching that
103
+ PR's own Button primary treatment, not bgBrandSolidPressed. */
104
+ & .crisp-comments-post { display: inline-flex; align-items: center; justify-content: center; height: 32px; padding: 6px ${c.space["2.5"]}; border: none; border-radius: ${c.radius.lg}; background: ${c.bgBrandContrast}; box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08); font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.semibold}; color: ${c.fgOnBrand}; cursor: pointer; opacity: 1; transition: opacity .12s, filter .12s; }
105
+ & .crisp-comments-post:hover:not(:disabled) { filter: brightness(0.96); }
102
106
  & .crisp-comments-post:disabled { opacity: .4; cursor: default; }
103
107
 
104
108
  /* Filter menu -- measured: 220px, radius 12 (radius.xl), 4px padding,
@@ -110,8 +114,22 @@ const config = defineRecipe({
110
114
  rather than re-derived since it's the same atomic-chip affordance. */
111
115
  & .crisp-comments-mention { display: inline; padding: 0 2px; border-radius: 4px; background: color-mix(in srgb, ${c.bgBrandSolid} 12%, transparent); color: ${c.bgBrandSolid}; font-weight: ${c.weight.medium}; user-select: none; }
112
116
  ${rtlFlipCss}`,
113
- variants: {},
114
- defaultVariants: {},
117
+ variants: {
118
+ // your-moon/crisp#960: the 320px width + left-border hairline is a
119
+ // drawer-consumer look (RecordPage's own comments drawer, #864), not
120
+ // an intrinsic property of the panel itself. A consumer embedding this
121
+ // in a full-width tab pane (internal/grc's own record-page Comments
122
+ // tab, sibling to Related/Activity/Notes/Tasks/Lists, all of which
123
+ // fill the pane) needs `width: 100%` and no border -- a tab pane
124
+ // doesn't need a seam separating it from itself. `drawer` (default)
125
+ // is today's exact, already-measured look; nothing changes for an
126
+ // existing consumer that doesn't pass `variant`.
127
+ variant: {
128
+ drawer: `width: 320px; border-inline-start: 1px solid ${c.borderSubtle};`,
129
+ pane: `width: 100%;`,
130
+ },
131
+ },
132
+ defaultVariants: { variant: "drawer" },
115
133
  compoundVariants: [],
116
134
  // Two pieces below intentionally live OUTSIDE the nested \`base\` above,
117
135
  // the same reason Menu's own \`.crisp-menu-content\` does (menu.recipe.ts's
@@ -0,0 +1 @@
1
+ export { InviteMembersDialog, MembersSettings, } from "./memberssettings.js";
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Filter, MoreHorizontal, Search, User, UsersRound } from "lucide-react";
3
+ import * as React from "react";
4
+ import { Avatar } from "../../components/avatar/index.js";
5
+ import { Badge } from "../../components/badge/index.js";
6
+ import { Button } from "../../components/button/index.js";
7
+ import { Dialog, DialogBody, DialogFooter } from "../../components/dialog/index.js";
8
+ import { FormField } from "../../components/formfield/index.js";
9
+ import { Select } from "../../components/select/index.js";
10
+ import { useTablistKeyboard } from "../../components/tabs/tabs-keyboard.js";
11
+ import { Textarea } from "../../components/textarea/index.js";
12
+ import { TextLink } from "../../components/textlink/index.js";
13
+ import { memberssettingsRecipe } from "./memberssettings.recipe.js";
14
+ /**
15
+ * Attio's "Members and teams" settings page (crisp#636, P-119/P-120):
16
+ * title + description, a Members/Teams segmented switcher, a
17
+ * search+Filter+Invite toolbar, and the members table (User/Role/Teams).
18
+ * Compose `InviteMembersDialog` alongside for the "Invite members" flow.
19
+ */
20
+ export function MembersSettings({ members, searchValue, onSearchChange, onFilterClick, onInviteClick, onRowMenuClick, tab: tabProp, onTabChange, teamsContent, className, title = "Members and teams", description = "Manage workspace members and teams, set access levels, and invite new users.", learnMoreLabel = "Learn more", learnMoreHref = "#", membersTabLabel = "Members", teamsTabLabel = "Teams", tablistLabel = "Members and teams", searchPlaceholder = "Search name or email", filterLabel = "Filter", inviteLabel = "Invite members", userColumnLabel = "User", roleColumnLabel = "Role", teamsColumnLabel = "Teams", memberActionsLabel = "Member actions", noTeamsLabel = "No teams yet", ...rest }) {
21
+ const [internalTab, setInternalTab] = React.useState("members");
22
+ const tab = tabProp ?? internalTab;
23
+ const setTab = (next) => {
24
+ if (tabProp === undefined)
25
+ setInternalTab(next);
26
+ onTabChange?.(next);
27
+ };
28
+ const listRef = React.useRef(null);
29
+ const tabEntries = [
30
+ {
31
+ value: "members",
32
+ label: membersTabLabel,
33
+ icon: _jsx(User, { size: 16 }),
34
+ },
35
+ {
36
+ value: "teams",
37
+ label: teamsTabLabel,
38
+ icon: _jsx(UsersRound, { size: 16 }),
39
+ },
40
+ ];
41
+ const { onKeyDown, tabIndexFor } = useTablistKeyboard({
42
+ tabs: tabEntries,
43
+ active: tab,
44
+ select: (value) => setTab(value),
45
+ listRef,
46
+ });
47
+ return (_jsx("div", { className: memberssettingsRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-memberssettings-page", children: [_jsxs("div", { className: "crisp-memberssettings-heading", children: [_jsx("h1", { className: "crisp-memberssettings-title", children: title }), _jsxs("p", { className: "crisp-memberssettings-desc", children: [description, " ", _jsx(TextLink, { href: learnMoreHref, children: learnMoreLabel })] })] }), _jsx("div", { ref: listRef, className: "crisp-memberssettings-tabpair", role: "tablist", "aria-label": tablistLabel, onKeyDown: onKeyDown, children: tabEntries.map((t) => (_jsxs("button", { type: "button", role: "tab", "data-value": t.value, "aria-selected": tab === t.value, "data-selected": tab === t.value ? "" : undefined, tabIndex: tabIndexFor(t.value), className: "crisp-memberssettings-tab", onClick: () => setTab(t.value), children: [t.icon, t.label] }, t.value))) }), tab === "members" ? (_jsxs("div", { role: "tabpanel", "aria-label": membersTabLabel, children: [_jsxs("div", { className: "crisp-memberssettings-toolbar", children: [_jsxs("div", { className: "crisp-memberssettings-search", children: [_jsx(Search, { size: 16 }), _jsx("input", { value: searchValue, onChange: (e) => onSearchChange?.(e.target.value), placeholder: searchPlaceholder })] }), _jsxs("button", { type: "button", className: "crisp-memberssettings-filterbtn", onClick: onFilterClick, children: [_jsx(Filter, { size: 14 }), filterLabel] }), _jsx("button", { type: "button", className: "crisp-memberssettings-invitebtn", onClick: onInviteClick, children: inviteLabel })] }), _jsx("div", { className: "crisp-memberssettings-table-wrap", children: _jsxs("table", { className: "crisp-memberssettings-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: userColumnLabel }), _jsx("th", { children: roleColumnLabel }), _jsx("th", { children: teamsColumnLabel })] }) }), _jsx("tbody", { children: members.map((m) => (_jsxs("tr", { children: [_jsx("td", { children: _jsxs("div", { className: "crisp-memberssettings-user", children: [m.avatarSrc ? (_jsx("img", { className: "crisp-memberssettings-avatarimg", src: m.avatarSrc, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "circle", tone: "brand", children: String(m.name).charAt(0) })), _jsxs("div", { className: "crisp-memberssettings-usermeta", children: [_jsxs("span", { children: [m.name, m.isYou ? " (You)" : ""] }), _jsx("span", { className: "crisp-memberssettings-useremail", children: m.email })] })] }) }), _jsx("td", { children: m.role === "Admin" ? (_jsx(Badge, { tone: "purple", children: m.role })) : (m.role) }), _jsxs("td", { children: [m.teams ?? "—", _jsx("button", { type: "button", className: "crisp-memberssettings-kebab", "aria-label": memberActionsLabel, onClick: () => onRowMenuClick?.(m), children: _jsx(MoreHorizontal, { size: 16 }) })] })] }, m.id))) })] }) })] })) : (_jsx("div", { role: "tabpanel", "aria-label": teamsTabLabel, children: teamsContent ?? (_jsx("div", { className: "crisp-memberssettings-teamspanel", children: noTeamsLabel })) }))] }) }));
48
+ }
49
+ MembersSettings.displayName = "MembersSettings";
50
+ /**
51
+ * Attio's "Invite members" modal (crisp#636, P-120), measured live: 708px
52
+ * wide, a multi-email composer, an "Invite as" select (default Member) and
53
+ * an optional "Add to team" search-or-create select, footer "Send Invites".
54
+ */
55
+ export function InviteMembersDialog({ open, onOpenChange, emails, onEmailsChange, inviteAs, onInviteAsChange, team, onTeamChange, teamOptions = [], onSend, title = "Invite members", emailsPlaceholder = "Send Invite to ...", inviteAsFieldLabel = "Invite as", memberOptionLabel = "Member — can edit", adminOptionLabel = "Admin — full access", addToTeamFieldLabel = "Add to team", addToTeamPlaceholder = "Search or create a team", sendLabel = "Send Invites ⌘⏎", ...rest }) {
56
+ const inviteAsOptions = [
57
+ { value: "member", label: memberOptionLabel },
58
+ { value: "admin", label: adminOptionLabel },
59
+ ];
60
+ return (_jsxs(Dialog, { open: open, onOpenChange: onOpenChange, title: title, width: 708, ...rest, children: [_jsxs(DialogBody, { children: [_jsx("div", { className: "crisp-memberssettings-invitefield", children: _jsx(Textarea, { value: emails, onChange: (e) => onEmailsChange(e.target.value), placeholder: emailsPlaceholder, rows: 3 }) }), _jsx(FormField, { label: inviteAsFieldLabel, children: _jsx(Select, { options: inviteAsOptions, value: inviteAs, onValueChange: (v) => onInviteAsChange(v) }) }), _jsx(FormField, { label: addToTeamFieldLabel, optional: true, children: _jsx(Select, { options: teamOptions, value: team, onValueChange: onTeamChange, placeholder: addToTeamPlaceholder, searchable: true }) })] }), _jsx(DialogFooter, { children: _jsx(Button, { intent: "primary", onClick: onSend, children: sendLabel }) })] }));
61
+ }
62
+ InviteMembersDialog.displayName = "InviteMembersDialog";
@@ -0,0 +1,80 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // Measured live against app.attio.com/<workspace>/settings/members-and-teams
4
+ // (crisp#636, P-119/P-120). The Members/Teams switcher is a plain segmented
5
+ // pill toggle -- NOT the shared `Tabs` primitive, which always reserves an
6
+ // 8px baseline strip and draws an ink underline (crisp#749's own tabs
7
+ // recipe comment): this page's tab container measured a clean 28px, no
8
+ // baseline, no underline, bg transparent. Reusing `Tabs` here would paint a
9
+ // baseline Attio's own switcher doesn't have.
10
+ const config = defineRecipe({
11
+ name: "memberssettings",
12
+ base: `& .crisp-memberssettings-page { display: flex; flex-direction: column; gap: ${c.space["6"]}; font-family: ${c.font.sans}; }
13
+ & .crisp-memberssettings-heading { display: flex; flex-direction: column; gap: ${c.space["0.5"]}; }
14
+ & .crisp-memberssettings-title { font-size: 24px; line-height: 28px; font-weight: ${c.weight.semibold}; letter-spacing: -0.48px; color: ${c.fgPrimary}; margin: 0; }
15
+ & .crisp-memberssettings-desc { font-size: ${c.text.lg}; line-height: 20px; color: ${c.fgMuted}; margin: 0; }
16
+
17
+ /* Segmented Members/Teams switcher -- measured: 28px pills, radius 8,
18
+ 4px gap, 6px icon-label gap, 14/500 label, selected bg bgSubtle +
19
+ inset borderSubtle ring, unselected transparent + a flat 63%-opacity
20
+ fgPrimary mix (measured rgba(0,0,0,.63) on the live page, not the
21
+ solid fgMuted token — same alpha technique this file's table header
22
+ already uses). */
23
+ & .crisp-memberssettings-tabpair { display: inline-flex; align-items: center; gap: 4px; }
24
+ & .crisp-memberssettings-tab { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 28px; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: none; box-shadow: inset 0 0 0 1px transparent; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); cursor: pointer; white-space: nowrap; transition: color .12s ease, background-color .12s ease, box-shadow .12s ease; }
25
+ & .crisp-memberssettings-tab svg { flex-shrink: 0; }
26
+ & .crisp-memberssettings-tab:hover:not([data-selected]) { color: ${c.fgPrimary}; }
27
+ & .crisp-memberssettings-tab[data-selected] { color: ${c.fgPrimary}; background: ${c.bgSubtle}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; }
28
+ & .crisp-memberssettings-tab:focus-visible { outline: 2px solid ${c.bgBrandSolid}; outline-offset: 1px; }
29
+
30
+ /* Toolbar row: search grows, Filter + Invite sit trailing -- measured
31
+ 32px height on all three, search radius 9 with a 6% ink ring,
32
+ Filter/Invite radius 9 with the button drop-shadow pairing. */
33
+ & .crisp-memberssettings-toolbar { display: flex; align-items: center; gap: ${c.space["2"]}; }
34
+ & .crisp-memberssettings-search { display: flex; align-items: center; flex: 1; min-width: 0; gap: ${c.space["1.5"]}; height: 32px; padding: 0 ${c.space["2.5"]} 0 ${c.space["2"]}; border-radius: 9px; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgMuted}; }
35
+ & .crisp-memberssettings-search:focus-within { box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; }
36
+ & .crisp-memberssettings-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }
37
+ & .crisp-memberssettings-search input::placeholder { color: ${c.fgMuted}; }
38
+ & .crisp-memberssettings-filterbtn { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 32px; padding: 6px 10px 6px 8px; border: none; border-radius: 9px; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px transparent, 0 0 2px 0 rgba(28,40,64,.18), 0 1px 3px 0 rgba(0,0,0,.04); color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; cursor: pointer; white-space: nowrap; }
39
+ & .crisp-memberssettings-filterbtn:hover { background: ${c.bgRaised}; }
40
+ /* your-moon/crisp#979: bgBrandContrast, not bgBrandSolid -- the accent
41
+ (#0298d9) only clears 3.23:1 with white; the darker sibling clears
42
+ 4.5:1+, same fix already applied to Button/Badge/Avatar (#897) and
43
+ TableToolbar/Dialog/Command/Comments/AuthShell (#953/#961), which
44
+ explicitly flagged this button as found-not-fixed. */
45
+ & .crisp-memberssettings-invitebtn { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 32px; padding: 6px 10px 6px 8px; border: none; border-radius: 9px; background: ${c.bgBrandContrast}; box-shadow: inset 0 0 0 1px rgba(0,0,0,.06), 0 2px 4px -2px color-mix(in srgb, ${c.bgBrandContrast} 12%, transparent), 0 3px 6px -2px color-mix(in srgb, ${c.bgBrandContrast} 8%, transparent); color: ${c.fgOnBrand}; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; cursor: pointer; white-space: nowrap; }
46
+ & .crisp-memberssettings-invitebtn:hover { filter: brightness(0.95); }
47
+
48
+ /* Table wrapper -- measured card: radius 12, 1px borderSubtle, 40px
49
+ rows, header label 12/500/63%-opacity fgPrimary. */
50
+ & .crisp-memberssettings-table-wrap { border-radius: 12px; border: 1px solid ${c.borderSubtle}; overflow: hidden; }
51
+ & .crisp-memberssettings-table { width: 100%; border-collapse: collapse; font-family: inherit; }
52
+ & .crisp-memberssettings-table th { height: 32px; padding: 0 ${c.space["4"]}; text-align: start; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); border-bottom: 1px solid ${c.borderSubtle}; background: ${c.bgSurface}; }
53
+ & .crisp-memberssettings-table td { height: 40px; padding: 0 ${c.space["4"]}; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; border-bottom: 1px solid ${c.borderSubtle}; }
54
+ & .crisp-memberssettings-table tr:last-child td { border-bottom: none; }
55
+ & .crisp-memberssettings-user { display: flex; align-items: center; gap: ${c.space["2"]}; }
56
+ & .crisp-memberssettings-avatarimg { width: 24px; height: 24px; border-radius: ${c.radius.full}; object-fit: cover; }
57
+ /* Name + email sit on ONE line, not stacked -- confirmed live: both spans
58
+ share the same y and the same 14px size, only the email's colour
59
+ differs (a flat 63%-opacity fgPrimary mix, not a smaller muted font). */
60
+ & .crisp-memberssettings-usermeta { display: flex; align-items: baseline; gap: ${c.space["2"]}; min-width: 0; }
61
+ & .crisp-memberssettings-usermeta > span:first-child { flex-shrink: 0; }
62
+ & .crisp-memberssettings-useremail { font-size: ${c.text.lg}; font-weight: ${c.weight.regular}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
63
+ & .crisp-memberssettings-kebab { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; margin-inline-start: auto; border: none; border-radius: ${c.radius.sm}; background: none; font-family: inherit; color: ${c.fgMuted}; cursor: pointer; }
64
+ & .crisp-memberssettings-kebab:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
65
+
66
+ & .crisp-memberssettings-teamspanel { display: flex; align-items: center; justify-content: center; padding: ${c.space["8"]} 0; color: ${c.fgMuted}; font-size: ${c.text.lg}; }
67
+
68
+ /* Invite dialog fields -- measured 708x386 dialog, radius 16; the
69
+ "Invite as"/"Add to team" selects are 32/34px with a flat 1px
70
+ 6%-ink border and 9/10px radius respectively (Attio's own two
71
+ selects don't share one radius). */
72
+ & .crisp-memberssettings-invitefield { display: flex; flex-direction: column; gap: ${c.space["1.5"]}; }
73
+ & .crisp-memberssettings-invitelabel { font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
74
+ `,
75
+ variants: {},
76
+ defaultVariants: {},
77
+ compoundVariants: [],
78
+ });
79
+ export const memberssettingsRecipe = resolveRecipe(config);
80
+ export default config;
@@ -0,0 +1 @@
1
+ export { NotificationsSettings, } from "./notificationssettings.js";
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Bell, Mail, MonitorSmartphone } from "lucide-react";
3
+ import { Checkbox } from "../../components/checkbox/index.js";
4
+ import { Switch } from "../../components/switch/index.js";
5
+ import { TextLink } from "../../components/textlink/index.js";
6
+ import { notificationssettingsRecipe } from "./notificationssettings.recipe.js";
7
+ /**
8
+ * Attio's "Notifications" settings page (crisp#630, P-083): title +
9
+ * description, a "Daily digest" card (a single toggle row), and a
10
+ * "Collaboration notifications" matrix — a header row plus one row per
11
+ * event, each with an Email and an App checkbox. Measured live.
12
+ */
13
+ export function NotificationsSettings({ dailyDigestEnabled, onDailyDigestChange, rows, onEmailChange, onAppChange, className, title = "Notifications", description = "Customize your notification settings to stay informed without being overwhelmed.", learnMoreLabel = "Learn more", learnMoreHref = "#", dailyDigestSectionTitle = "Daily digest", dailyDigestRowTitle = "Enable daily digest", dailyDigestRowDescription = "Includes tasks overdue and due today. Sent every morning if any tasks are due or overdue.", collaborationSectionTitle = "Collaboration notifications", notifyMeAboutLabel = "Notify me about", emailColumnLabel = "Email", appColumnLabel = "App", ...rest }) {
14
+ return (_jsx("div", { className: notificationssettingsRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-notificationssettings-page", children: [_jsxs("div", { className: "crisp-notificationssettings-heading", children: [_jsx("h1", { className: "crisp-notificationssettings-title", children: title }), _jsxs("p", { className: "crisp-notificationssettings-desc", children: [description, " ", _jsx(TextLink, { href: learnMoreHref, children: learnMoreLabel })] })] }), _jsxs("div", { className: "crisp-notificationssettings-section", children: [_jsx("h2", { className: "crisp-notificationssettings-sectiontitle", children: dailyDigestSectionTitle }), _jsxs("div", { className: "crisp-notificationssettings-digestcard", children: [_jsxs("div", { className: "crisp-notificationssettings-digesttext", children: [_jsx("span", { className: "crisp-notificationssettings-rowtitle", children: dailyDigestRowTitle }), _jsx("span", { className: "crisp-notificationssettings-rowdesc", children: dailyDigestRowDescription })] }), _jsx(Switch, { checked: dailyDigestEnabled, onCheckedChange: onDailyDigestChange, "aria-label": dailyDigestRowTitle })] })] }), _jsxs("div", { className: "crisp-notificationssettings-section", children: [_jsx("h2", { className: "crisp-notificationssettings-sectiontitle", children: collaborationSectionTitle }), _jsxs("div", { className: "crisp-notificationssettings-matrix", children: [_jsxs("div", { className: "crisp-notificationssettings-matrixhead", children: [_jsxs("span", { className: "crisp-notificationssettings-matrixhead-about", children: [_jsx(Bell, { size: 14 }), notifyMeAboutLabel] }), _jsxs("span", { className: "crisp-notificationssettings-matrixhead-col", children: [_jsx(Mail, { size: 14 }), emailColumnLabel] }), _jsxs("span", { className: "crisp-notificationssettings-matrixhead-col", children: [_jsx(MonitorSmartphone, { size: 14 }), appColumnLabel] })] }), rows.map((row) => (_jsxs("div", { className: "crisp-notificationssettings-matrixrow", children: [_jsxs("div", { className: "crisp-notificationssettings-matrixrow-about", children: [_jsx("span", { className: "crisp-notificationssettings-rowtitle", children: row.title }), _jsx("span", { className: "crisp-notificationssettings-rowdesc", children: row.description })] }), _jsx("div", { className: "crisp-notificationssettings-matrixrow-col", children: _jsx(Checkbox, { checked: row.email, onChange: (e) => onEmailChange(row.id, e.target.checked), "aria-label": `${emailColumnLabel}: ${typeof row.title === "string" ? row.title : row.id}` }) }), _jsx("div", { className: "crisp-notificationssettings-matrixrow-col", children: _jsx(Checkbox, { checked: row.app, onChange: (e) => onAppChange(row.id, e.target.checked), "aria-label": `${appColumnLabel}: ${typeof row.title === "string" ? row.title : row.id}` }) })] }, row.id)))] })] })] }) }));
15
+ }
16
+ NotificationsSettings.displayName = "NotificationsSettings";
@@ -0,0 +1,41 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // Measured live against app.attio.com/<workspace>/settings/notifications
4
+ // (crisp#630, P-083). Two sections, each a 16/600 heading over a bordered
5
+ // 12px-radius card: "Daily digest" (a single 56px row, label 14/500 +
6
+ // description 12/500/63%-opacity, a Switch trailing) and "Collaboration
7
+ // notifications" (a header row -- 12/500/63%-opacity labels -- then 7 rows,
8
+ // each 52px, a title/description pair on the left and two Checkboxes,
9
+ // right-aligned under Email/App, per row).
10
+ const config = defineRecipe({
11
+ name: "notificationssettings",
12
+ base: `& .crisp-notificationssettings-page { display: flex; flex-direction: column; gap: ${c.space["6"]}; font-family: ${c.font.sans}; }
13
+ & .crisp-notificationssettings-heading { display: flex; flex-direction: column; gap: ${c.space["0.5"]}; }
14
+ & .crisp-notificationssettings-title { font-size: 24px; line-height: 28px; font-weight: ${c.weight.semibold}; letter-spacing: -0.48px; color: ${c.fgPrimary}; margin: 0; }
15
+ & .crisp-notificationssettings-desc { font-size: ${c.text.lg}; line-height: 20px; color: ${c.fgMuted}; margin: 0; }
16
+
17
+ & .crisp-notificationssettings-section { display: flex; flex-direction: column; gap: ${c.space["4"]}; }
18
+ & .crisp-notificationssettings-sectiontitle { font-size: 16px; line-height: 20px; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; margin: 0; }
19
+
20
+ /* Daily digest card: one bordered 12px-radius row. */
21
+ & .crisp-notificationssettings-digestcard { display: flex; align-items: center; justify-content: space-between; gap: ${c.space["6"]}; padding: ${c.space["2"]} ${c.space["3"]}; border: 1px solid ${c.borderSubtle}; border-radius: 12px; background: ${c.bgSurface}; }
22
+ & .crisp-notificationssettings-digesttext { display: flex; flex-direction: column; gap: ${c.space["1"]}; }
23
+ & .crisp-notificationssettings-rowtitle { font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }
24
+ & .crisp-notificationssettings-rowdesc { font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
25
+
26
+ /* Collaboration matrix: a bordered card, header row + N body rows. */
27
+ & .crisp-notificationssettings-matrix { border: 1px solid ${c.borderSubtle}; border-radius: 12px; overflow: hidden; background: ${c.bgSurface}; }
28
+ & .crisp-notificationssettings-matrixhead { display: flex; align-items: center; gap: ${c.space["3"]}; padding: 0 ${c.space["4"]}; height: 40px; border-bottom: 1px solid ${c.borderSubtle}; }
29
+ & .crisp-notificationssettings-matrixhead-about { display: flex; align-items: center; gap: ${c.space["1.5"]}; flex: 1; min-width: 0; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
30
+ & .crisp-notificationssettings-matrixhead-col { display: flex; align-items: center; justify-content: center; gap: ${c.space["1.5"]}; width: 64px; flex-shrink: 0; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
31
+ & .crisp-notificationssettings-matrixrow { display: flex; align-items: center; gap: ${c.space["3"]}; padding: ${c.space["2"]} ${c.space["4"]}; min-height: 52px; }
32
+ & .crisp-notificationssettings-matrixrow + .crisp-notificationssettings-matrixrow { border-top: 1px solid ${c.borderSubtle}; }
33
+ & .crisp-notificationssettings-matrixrow-about { display: flex; flex-direction: column; gap: ${c.space["1"]}; flex: 1; min-width: 0; }
34
+ & .crisp-notificationssettings-matrixrow-col { display: flex; align-items: center; justify-content: center; width: 64px; flex-shrink: 0; }
35
+ `,
36
+ variants: {},
37
+ defaultVariants: {},
38
+ compoundVariants: [],
39
+ });
40
+ export const notificationssettingsRecipe = resolveRecipe(config);
41
+ export default config;