@crisp-ui-kit/crisp 0.57.0 → 0.57.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/dist/accentpicker.css +5 -0
  2. package/dist/accentpicker.d.ts +7 -0
  3. package/dist/accentpicker.layered.css +7 -0
  4. package/dist/authshell.css +9 -4
  5. package/dist/authshell.layered.css +9 -4
  6. package/dist/avatar.css +1 -1
  7. package/dist/avatar.layered.css +1 -1
  8. package/dist/badge.css +5 -5
  9. package/dist/badge.layered.css +5 -5
  10. package/dist/board.css +28 -8
  11. package/dist/board.layered.css +28 -8
  12. package/dist/bottomsheet.css +26 -0
  13. package/dist/bottomsheet.d.ts +7 -0
  14. package/dist/bottomsheet.layered.css +28 -0
  15. package/dist/button.css +9 -8
  16. package/dist/button.layered.css +9 -8
  17. package/dist/checkbox.css +7 -3
  18. package/dist/checkbox.layered.css +7 -3
  19. package/dist/chip.css +15 -0
  20. package/dist/chip.d.ts +3 -1
  21. package/dist/chip.layered.css +15 -0
  22. package/dist/chipgroup.css +5 -0
  23. package/dist/chipgroup.d.ts +7 -0
  24. package/dist/chipgroup.layered.css +7 -0
  25. package/dist/cjs/components/accentpicker/accentpicker.js +66 -0
  26. package/dist/cjs/components/accentpicker/accentpicker.recipe.js +25 -0
  27. package/dist/cjs/components/accentpicker/index.js +7 -0
  28. package/dist/cjs/components/avatar/avatar.recipe.js +4 -1
  29. package/dist/cjs/components/badge/badge.recipe.js +38 -10
  30. package/dist/cjs/components/bottomsheet/bottomsheet.js +90 -0
  31. package/dist/cjs/components/bottomsheet/bottomsheet.recipe.js +77 -0
  32. package/dist/cjs/components/bottomsheet/index.js +9 -0
  33. package/dist/cjs/components/button/button.recipe.js +55 -21
  34. package/dist/cjs/components/checkbox/checkbox.recipe.js +61 -9
  35. package/dist/cjs/components/chip/chip.js +14 -2
  36. package/dist/cjs/components/chip/chip.recipe.js +69 -1
  37. package/dist/cjs/components/chipgroup/chipgroup.js +51 -0
  38. package/dist/cjs/components/chipgroup/chipgroup.recipe.js +36 -0
  39. package/dist/cjs/components/chipgroup/index.js +7 -0
  40. package/dist/cjs/components/datatable/datatable.js +5 -1
  41. package/dist/cjs/components/datatable/datatable.recipe.js +11 -2
  42. package/dist/cjs/components/dialog/dialog.recipe.js +21 -10
  43. package/dist/cjs/components/emptystate/emptystate.recipe.js +7 -2
  44. package/dist/cjs/components/fileupload/fileupload.js +21 -4
  45. package/dist/cjs/components/input/index.js +2 -1
  46. package/dist/cjs/components/input/input.js +33 -3
  47. package/dist/cjs/components/input/input.recipe.js +22 -7
  48. package/dist/cjs/components/menu/menu.recipe.js +21 -7
  49. package/dist/cjs/components/menusheet/index.js +9 -0
  50. package/dist/cjs/components/menusheet/menusheet.js +133 -0
  51. package/dist/cjs/components/menusheet/menusheet.recipe.js +103 -0
  52. package/dist/cjs/components/multiselect/multiselect.js +2 -2
  53. package/dist/cjs/components/multiselect/multiselect.recipe.js +45 -12
  54. package/dist/cjs/components/progress/progress.js +21 -4
  55. package/dist/cjs/components/progress/progress.recipe.js +30 -0
  56. package/dist/cjs/components/select/select.js +3 -3
  57. package/dist/cjs/components/select/select.recipe.js +63 -10
  58. package/dist/cjs/components/snackbar/index.js +8 -0
  59. package/dist/cjs/components/snackbar/snackbar.js +121 -0
  60. package/dist/cjs/components/snackbar/snackbar.recipe.js +71 -0
  61. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +7 -2
  62. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +37 -12
  63. package/dist/cjs/components/tabs/tabs.js +10 -4
  64. package/dist/cjs/components/tabs/tabs.recipe.js +23 -6
  65. package/dist/cjs/components/text/text.recipe.js +8 -7
  66. package/dist/cjs/components/textarea/textarea.js +3 -2
  67. package/dist/cjs/components/textarea/textarea.recipe.js +13 -9
  68. package/dist/cjs/components/textlink/textlink.recipe.js +6 -3
  69. package/dist/cjs/index.js +8 -0
  70. package/dist/cjs/product/authshell/authshell.recipe.js +5 -1
  71. package/dist/cjs/product/command/command.recipe.js +4 -1
  72. package/dist/cjs/product/comments/comments.js +7 -2
  73. package/dist/cjs/product/comments/comments.recipe.js +25 -7
  74. package/dist/cjs/product/memberssettings/index.js +6 -0
  75. package/dist/cjs/product/memberssettings/memberssettings.js +99 -0
  76. package/dist/cjs/product/memberssettings/memberssettings.recipe.js +83 -0
  77. package/dist/cjs/product/notificationssettings/index.js +5 -0
  78. package/dist/cjs/product/notificationssettings/notificationssettings.js +19 -0
  79. package/dist/cjs/product/notificationssettings/notificationssettings.recipe.js +44 -0
  80. package/dist/cjs/product/recordpage/recordpage.js +101 -9
  81. package/dist/cjs/product/recordpage/recordpage.recipe.js +43 -6
  82. package/dist/cjs/product/workspacegeneralsettings/index.js +5 -0
  83. package/dist/cjs/product/workspacegeneralsettings/workspacegeneralsettings.js +21 -0
  84. package/dist/cjs/product/workspacegeneralsettings/workspacegeneralsettings.recipe.js +51 -0
  85. package/dist/cjs/tokens/contract.js +1 -0
  86. package/dist/cjs/tokens/elevation.js +8 -5
  87. package/dist/cjs/tokens/palette.js +107 -33
  88. package/dist/cjs/tokens/scale.js +45 -2
  89. package/dist/cjs/tokens/semantic.js +26 -0
  90. package/dist/columncalc.css +15 -7
  91. package/dist/columncalc.layered.css +15 -7
  92. package/dist/command.css +23 -19
  93. package/dist/command.layered.css +23 -19
  94. package/dist/comments.css +28 -19
  95. package/dist/comments.d.ts +2 -2
  96. package/dist/comments.layered.css +28 -19
  97. package/dist/datatable.css +161 -67
  98. package/dist/datatable.layered.css +161 -67
  99. package/dist/datepicker.css +13 -41
  100. package/dist/datepicker.layered.css +13 -41
  101. package/dist/datepickerfield.css +4 -1
  102. package/dist/datepickerfield.layered.css +4 -1
  103. package/dist/dialog.css +12 -7
  104. package/dist/dialog.layered.css +12 -7
  105. package/dist/emptystate.css +5 -3
  106. package/dist/emptystate.layered.css +5 -3
  107. package/dist/esm/components/accentpicker/accentpicker.js +30 -0
  108. package/dist/esm/components/accentpicker/accentpicker.recipe.js +22 -0
  109. package/dist/esm/components/accentpicker/index.js +2 -0
  110. package/dist/esm/components/avatar/avatar.recipe.js +4 -1
  111. package/dist/esm/components/badge/badge.recipe.js +38 -10
  112. package/dist/esm/components/bottomsheet/bottomsheet.js +52 -0
  113. package/dist/esm/components/bottomsheet/bottomsheet.recipe.js +74 -0
  114. package/dist/esm/components/bottomsheet/index.js +2 -0
  115. package/dist/esm/components/button/button.recipe.js +56 -22
  116. package/dist/esm/components/checkbox/checkbox.recipe.js +61 -9
  117. package/dist/esm/components/chip/chip.js +14 -2
  118. package/dist/esm/components/chip/chip.recipe.js +69 -1
  119. package/dist/esm/components/chipgroup/chipgroup.js +15 -0
  120. package/dist/esm/components/chipgroup/chipgroup.recipe.js +33 -0
  121. package/dist/esm/components/chipgroup/index.js +2 -0
  122. package/dist/esm/components/datatable/datatable.js +5 -1
  123. package/dist/esm/components/datatable/datatable.recipe.js +11 -2
  124. package/dist/esm/components/dialog/dialog.recipe.js +22 -11
  125. package/dist/esm/components/emptystate/emptystate.recipe.js +7 -2
  126. package/dist/esm/components/fileupload/fileupload.js +20 -5
  127. package/dist/esm/components/input/index.js +1 -1
  128. package/dist/esm/components/input/input.js +33 -3
  129. package/dist/esm/components/input/input.recipe.js +23 -8
  130. package/dist/esm/components/menu/menu.recipe.js +21 -7
  131. package/dist/esm/components/menusheet/index.js +2 -0
  132. package/dist/esm/components/menusheet/menusheet.js +95 -0
  133. package/dist/esm/components/menusheet/menusheet.recipe.js +100 -0
  134. package/dist/esm/components/multiselect/multiselect.js +2 -2
  135. package/dist/esm/components/multiselect/multiselect.recipe.js +45 -12
  136. package/dist/esm/components/progress/progress.js +22 -5
  137. package/dist/esm/components/progress/progress.recipe.js +30 -0
  138. package/dist/esm/components/select/select.js +3 -3
  139. package/dist/esm/components/select/select.recipe.js +63 -10
  140. package/dist/esm/components/snackbar/index.js +2 -0
  141. package/dist/esm/components/snackbar/snackbar.js +84 -0
  142. package/dist/esm/components/snackbar/snackbar.recipe.js +68 -0
  143. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +7 -2
  144. package/dist/esm/components/tabletoolbar/tabletoolbar.js +37 -12
  145. package/dist/esm/components/tabs/tabs.js +10 -4
  146. package/dist/esm/components/tabs/tabs.recipe.js +23 -6
  147. package/dist/esm/components/text/text.recipe.js +8 -7
  148. package/dist/esm/components/textarea/textarea.js +3 -2
  149. package/dist/esm/components/textarea/textarea.recipe.js +14 -10
  150. package/dist/esm/components/textlink/textlink.recipe.js +6 -3
  151. package/dist/esm/index.js +8 -0
  152. package/dist/esm/product/authshell/authshell.recipe.js +5 -1
  153. package/dist/esm/product/command/command.recipe.js +4 -1
  154. package/dist/esm/product/comments/comments.js +7 -2
  155. package/dist/esm/product/comments/comments.recipe.js +25 -7
  156. package/dist/esm/product/memberssettings/index.js +1 -0
  157. package/dist/esm/product/memberssettings/memberssettings.js +62 -0
  158. package/dist/esm/product/memberssettings/memberssettings.recipe.js +80 -0
  159. package/dist/esm/product/notificationssettings/index.js +1 -0
  160. package/dist/esm/product/notificationssettings/notificationssettings.js +16 -0
  161. package/dist/esm/product/notificationssettings/notificationssettings.recipe.js +41 -0
  162. package/dist/esm/product/recordpage/recordpage.js +102 -10
  163. package/dist/esm/product/recordpage/recordpage.recipe.js +43 -6
  164. package/dist/esm/product/workspacegeneralsettings/index.js +1 -0
  165. package/dist/esm/product/workspacegeneralsettings/workspacegeneralsettings.js +18 -0
  166. package/dist/esm/product/workspacegeneralsettings/workspacegeneralsettings.recipe.js +48 -0
  167. package/dist/esm/tokens/contract.js +2 -1
  168. package/dist/esm/tokens/elevation.js +8 -5
  169. package/dist/esm/tokens/palette.js +107 -33
  170. package/dist/esm/tokens/scale.js +44 -1
  171. package/dist/esm/tokens/semantic.js +26 -0
  172. package/dist/fileupload.css +3 -1
  173. package/dist/fileupload.layered.css +3 -1
  174. package/dist/input.css +11 -2
  175. package/dist/input.layered.css +11 -2
  176. package/dist/listpage.css +3 -1
  177. package/dist/listpage.layered.css +3 -1
  178. package/dist/memberssettings.css +67 -0
  179. package/dist/memberssettings.d.ts +7 -0
  180. package/dist/memberssettings.layered.css +69 -0
  181. package/dist/menu.css +2 -2
  182. package/dist/menu.layered.css +2 -2
  183. package/dist/menusheet.css +36 -0
  184. package/dist/menusheet.d.ts +7 -0
  185. package/dist/menusheet.layered.css +38 -0
  186. package/dist/multiselect.css +17 -6
  187. package/dist/multiselect.d.ts +2 -2
  188. package/dist/multiselect.layered.css +17 -6
  189. package/dist/native/tokens.d.ts +2 -1
  190. package/dist/native/tokens.js +53 -25
  191. package/dist/notificationssettings.css +22 -0
  192. package/dist/notificationssettings.d.ts +7 -0
  193. package/dist/notificationssettings.layered.css +24 -0
  194. package/dist/objectsettings.css +4 -4
  195. package/dist/objectsettings.layered.css +4 -4
  196. package/dist/progress.css +3 -0
  197. package/dist/progress.layered.css +3 -0
  198. package/dist/recordpage.css +67 -23
  199. package/dist/recordpage.layered.css +67 -23
  200. package/dist/recordpanel.css +35 -16
  201. package/dist/recordpanel.layered.css +35 -16
  202. package/dist/resizablepanel.css +8 -5
  203. package/dist/resizablepanel.layered.css +8 -5
  204. package/dist/richtext.css +24 -8
  205. package/dist/richtext.layered.css +24 -8
  206. package/dist/savedviews.css +9 -2
  207. package/dist/savedviews.layered.css +9 -2
  208. package/dist/select.css +47 -7
  209. package/dist/select.d.ts +2 -2
  210. package/dist/select.layered.css +47 -7
  211. package/dist/settingsshell.css +2 -3
  212. package/dist/settingsshell.layered.css +2 -3
  213. package/dist/slider.css +6 -27
  214. package/dist/slider.layered.css +6 -27
  215. package/dist/snackbar.css +24 -0
  216. package/dist/snackbar.d.ts +7 -0
  217. package/dist/snackbar.layered.css +26 -0
  218. package/dist/statcard.css +3 -1
  219. package/dist/statcard.layered.css +3 -1
  220. package/dist/styles.css +927 -365
  221. package/dist/styles.layered.css +927 -365
  222. package/dist/table.css +28 -10
  223. package/dist/table.layered.css +28 -10
  224. package/dist/tabletoolbar.css +35 -24
  225. package/dist/tabletoolbar.layered.css +35 -24
  226. package/dist/tabs.css +18 -4
  227. package/dist/tabs.layered.css +18 -4
  228. package/dist/text.css +1 -1
  229. package/dist/text.layered.css +1 -1
  230. package/dist/textarea.css +4 -7
  231. package/dist/textarea.layered.css +4 -7
  232. package/dist/textlink.css +2 -2
  233. package/dist/textlink.layered.css +2 -2
  234. package/dist/themepicker.css +2 -1
  235. package/dist/themepicker.layered.css +2 -1
  236. package/dist/tokens.css +31 -13
  237. package/dist/tokens.json +53 -25
  238. package/dist/types/components/accentpicker/accentpicker.d.ts +23 -0
  239. package/dist/types/components/accentpicker/accentpicker.recipe.d.ts +3 -0
  240. package/dist/types/components/accentpicker/index.d.ts +2 -0
  241. package/dist/types/components/bottomsheet/bottomsheet.d.ts +35 -0
  242. package/dist/types/components/bottomsheet/bottomsheet.recipe.d.ts +3 -0
  243. package/dist/types/components/bottomsheet/index.d.ts +2 -0
  244. package/dist/types/components/button/button.recipe.d.ts +13 -8
  245. package/dist/types/components/checkbox/checkbox.recipe.d.ts +28 -5
  246. package/dist/types/components/chip/chip.d.ts +22 -0
  247. package/dist/types/components/chip/chip.recipe.d.ts +48 -0
  248. package/dist/types/components/chipgroup/chipgroup.d.ts +14 -0
  249. package/dist/types/components/chipgroup/chipgroup.recipe.d.ts +5 -0
  250. package/dist/types/components/chipgroup/index.d.ts +2 -0
  251. package/dist/types/components/datatable/datatable.d.ts +19 -0
  252. package/dist/types/components/fileupload/fileupload.d.ts +26 -0
  253. package/dist/types/components/input/index.d.ts +1 -1
  254. package/dist/types/components/input/input.d.ts +84 -0
  255. package/dist/types/components/menusheet/index.d.ts +2 -0
  256. package/dist/types/components/menusheet/menusheet.d.ts +42 -0
  257. package/dist/types/components/menusheet/menusheet.recipe.d.ts +3 -0
  258. package/dist/types/components/multiselect/multiselect.d.ts +4 -3
  259. package/dist/types/components/multiselect/multiselect.recipe.d.ts +3 -0
  260. package/dist/types/components/progress/progress.d.ts +16 -4
  261. package/dist/types/components/progress/progress.recipe.d.ts +22 -0
  262. package/dist/types/components/select/select.d.ts +6 -5
  263. package/dist/types/components/select/select.recipe.d.ts +3 -0
  264. package/dist/types/components/snackbar/index.d.ts +2 -0
  265. package/dist/types/components/snackbar/snackbar.d.ts +36 -0
  266. package/dist/types/components/snackbar/snackbar.recipe.d.ts +5 -0
  267. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +28 -1
  268. package/dist/types/components/tabs/tabs.d.ts +11 -1
  269. package/dist/types/components/textarea/textarea.d.ts +16 -0
  270. package/dist/types/index.d.ts +8 -0
  271. package/dist/types/product/comments/comments.d.ts +9 -3
  272. package/dist/types/product/comments/comments.recipe.d.ts +12 -2
  273. package/dist/types/product/memberssettings/index.d.ts +1 -0
  274. package/dist/types/product/memberssettings/memberssettings.d.ts +85 -0
  275. package/dist/types/product/memberssettings/memberssettings.recipe.d.ts +3 -0
  276. package/dist/types/product/notificationssettings/index.d.ts +1 -0
  277. package/dist/types/product/notificationssettings/notificationssettings.d.ts +37 -0
  278. package/dist/types/product/notificationssettings/notificationssettings.recipe.d.ts +3 -0
  279. package/dist/types/product/recordpage/recordpage.d.ts +89 -2
  280. package/dist/types/product/workspacegeneralsettings/index.d.ts +1 -0
  281. package/dist/types/product/workspacegeneralsettings/workspacegeneralsettings.d.ts +48 -0
  282. package/dist/types/product/workspacegeneralsettings/workspacegeneralsettings.recipe.d.ts +3 -0
  283. package/dist/types/tokens/contract.d.ts +7 -1
  284. package/dist/types/tokens/elevation.d.ts +8 -5
  285. package/dist/types/tokens/palette.d.ts +28 -12
  286. package/dist/types/tokens/scale.d.ts +12 -1
  287. package/dist/types/tokens/semantic.d.ts +17 -0
  288. package/dist/wheelpicker.css +4 -2
  289. package/dist/wheelpicker.layered.css +4 -2
  290. package/dist/workspacegeneralsettings.css +30 -0
  291. package/dist/workspacegeneralsettings.d.ts +7 -0
  292. package/dist/workspacegeneralsettings.layered.css +32 -0
  293. package/package.json +41 -1
@@ -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;
@@ -1,14 +1,16 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Activity as ActivityIcon, CalendarClock, ChevronDown, CircleDollarSign, Copy, DollarSign, FileText, Folder, Inbox, Landmark, LayoutGrid, Mail, Maximize2, Minimize2, MoreHorizontal, Pencil, Phone, Plus, Search, Sparkles, SquareCheck, Star, Trash2, Users, UsersRound, Workflow, X, Zap, } from "lucide-react";
2
+ import { Activity as ActivityIcon, CalendarClock, ChevronDown, ChevronUp, CircleDollarSign, Copy, DollarSign, FileText, Folder, Inbox, Landmark, LayoutGrid, Mail, Maximize2, Merge, MessageSquare, Minimize2, MoreHorizontal, Pencil, Phone, Plus, Search, Settings, Sparkles, SquareCheck, Star, Trash2, Users, UsersRound, Workflow, X, Zap, } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { Avatar } from "../../components/avatar/index.js";
5
5
  import { Checkbox } from "../../components/checkbox/index.js";
6
+ import { Drawer } from "../../components/drawer/index.js";
6
7
  import { Menu, MenuItem, MenuSeparator } from "../../components/menu/index.js";
7
8
  import { Select } from "../../components/select/index.js";
8
9
  import { Spinner } from "../../components/spinner/index.js";
9
10
  import { Switch } from "../../components/switch/index.js";
10
11
  import { Tabs } from "../../components/tabs/index.js";
11
12
  import { cx, isRtlElement, useCrispComponentDefaults } from "../../core/index.js";
13
+ import { CommentsPanel, } from "../comments/index.js";
12
14
  import { tagTone } from "./column.js";
13
15
  import { editableInputType, useEditableField } from "./editable-field.js";
14
16
  import { recordpageRecipe } from "./recordpage.recipe.js";
@@ -33,9 +35,16 @@ export const recordPageLabels = {
33
35
  breadcrumb: "Breadcrumb",
34
36
  editRecord: "Edit record",
35
37
  recordActions: "Record actions",
36
- addToFavourites: "Add to favourites",
37
- removeFromFavourites: "Remove from favourites",
38
+ // US spelling, matching Attio's own live text exactly (#621) — this string
39
+ // is shared with the entity header's favourite-star aria-label, so both
40
+ // read "favorites" now, not just the menu.
41
+ addToFavourites: "Add to favorites",
42
+ removeFromFavourites: "Remove from favorites",
38
43
  copyRecordLink: "Copy record link",
44
+ configurePage: "Configure page",
45
+ copyPageUrl: "Copy page URL",
46
+ copyRecordId: "Copy record ID",
47
+ mergeRecords: "Merge records",
39
48
  deleteRecord: "Delete record",
40
49
  linkRecords: "Link records",
41
50
  // Tooltip copy re-measured live for #865 (was "Log a note"/"Create a
@@ -50,6 +59,8 @@ export const recordPageLabels = {
50
59
  composeEmail: "Compose email",
51
60
  askAttio: "Ask AI",
52
61
  addToList: "Add to list",
62
+ share: "Share",
63
+ comments: "Comments",
53
64
  noLists: "This record has not been added to any lists.",
54
65
  highlights: "Highlights",
55
66
  recordDetails: "Record Details",
@@ -262,7 +273,14 @@ function NotesSection({ notes, onAdd, labels, }) {
262
273
  return (_jsxs("section", { children: [_jsx(SectionHead, { title: labels.tabs.notes, count: notes.length, action: _jsxs("button", { type: "button", className: "crisp-recordpage-addbtn", onClick: () => setOpen(true), children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), labels.newNote] }) }), open && (_jsx(Composer, { labels: labels, multiline: true, placeholder: labels.notePlaceholder, submitLabel: labels.saveNote, onCancel: () => setOpen(false), onSubmit: (t) => {
263
274
  onAdd(t);
264
275
  setOpen(false);
265
- } })), notes.length === 0 && !open ? (_jsx("div", { className: "crisp-recordpage-emptystate", children: labels.noNotesYet })) : (notes.map((n) => (_jsxs("div", { className: "crisp-recordpage-note", children: [_jsx(Avatar, { size: "sm", shape: "circle", tone: "neutral", children: "Y" }), _jsxs("div", { className: "crisp-recordpage-note-body", children: [_jsxs("div", { className: "crisp-recordpage-note-head", children: [_jsx("span", { className: "crisp-recordpage-note-author", children: labels.you }), _jsx("span", { className: "crisp-recordpage-note-time", children: n.time })] }), _jsx("div", { className: "crisp-recordpage-note-text", children: n.text })] })] }, n.id))))] }));
276
+ } })), notes.length === 0 && !open ? (_jsx("div", { className: "crisp-recordpage-emptystate", children: labels.noNotesYet })) : (
277
+ // #871: one line, not a two-row card -- measured live: avatar,
278
+ // then the note's own title and body run together inline, then a
279
+ // relative time at the far end of the SAME row. No author name at
280
+ // all (Attio's real preview never shows one here, only the note's
281
+ // own content) -- `labels.you` stays defined for a consumer that
282
+ // still wants it elsewhere, just unused in this exact spot now.
283
+ notes.map((n) => (_jsxs("div", { className: "crisp-recordpage-note", children: [_jsxs("span", { className: "crisp-recordpage-note-left", children: [_jsx(Avatar, { size: "sm", shape: "circle", tone: "neutral", children: "Y" }), n.title && (_jsx("span", { className: "crisp-recordpage-note-title", children: n.title })), _jsx("span", { className: "crisp-recordpage-note-text", children: n.text })] }), _jsx("span", { className: "crisp-recordpage-note-time", children: n.time })] }, n.id))))] }));
266
284
  }
267
285
  function TasksSection({ tasks, onAdd, onToggle, labels, }) {
268
286
  const [open, setOpen] = React.useState(false);
@@ -295,13 +313,51 @@ function EmptyCard({ icon, children, }) {
295
313
  * `RecordPage`'s own root does: `<div className={recordpageRecipe()}>`
296
314
  * (also exported from this package).
297
315
  */
298
- export function RecordDetails({ record, columns, sections, onEdit, labels, className, ...rest }) {
316
+ export function RecordDetails({ record, columns, sections, onEdit, labels, className, resizable = false, width: controlledWidth, defaultWidth = DETAILS_DEFAULT_W, onWidthChange, minWidth = DETAILS_MIN_W, maxWidth = DETAILS_MAX_W, ...rest }) {
299
317
  const t = useRecordPageLabels(labels);
300
318
  const { editingKey, pendingKey, error, openEditing, closeEditing, commit } = useEditableField(onEdit);
301
319
  const [detailsOpen, setDetailsOpen] = React.useState(true);
302
320
  const [searchOpen, setSearchOpen] = React.useState(false);
303
321
  const [fieldQuery, setFieldQuery] = React.useState("");
304
322
  const [showAllValues, setShowAllValues] = React.useState(false);
323
+ // #930: controlled/uncontrolled width, same split as RecordPanel's own
324
+ // resizable width -- only read when `resizable`, same reasoning as
325
+ // RecordPanel's own (a non-resizing consumer has no business carrying
326
+ // width state at all).
327
+ const [internalWidth, setInternalWidth] = React.useState(defaultWidth);
328
+ const width = controlledWidth ?? internalWidth;
329
+ const setWidth = (next) => {
330
+ const clamped = Math.max(minWidth, Math.min(maxWidth, next));
331
+ if (controlledWidth === undefined)
332
+ setInternalWidth(clamped);
333
+ onWidthChange?.(clamped);
334
+ };
335
+ const [detailsResizing, setDetailsResizing] = React.useState(false);
336
+ const detailsDragRef = React.useRef(null);
337
+ const onDetailsResizeDown = (e) => {
338
+ // This column is a leading (left-docked) rail, the mirror image of
339
+ // RecordPanel's own trailing (right-docked) one -- a rightward pull
340
+ // grows it in LTR, the same direction RecordPage's own internal aside
341
+ // resize already uses (#212/#255).
342
+ detailsDragRef.current = {
343
+ x: e.clientX,
344
+ w: width,
345
+ dir: isRtlElement(e.currentTarget) ? -1 : 1,
346
+ };
347
+ setDetailsResizing(true);
348
+ e.target.setPointerCapture(e.pointerId);
349
+ };
350
+ const onDetailsResizeMove = (e) => {
351
+ const r = detailsDragRef.current;
352
+ if (!r)
353
+ return;
354
+ setWidth(r.w + r.dir * (e.clientX - r.x));
355
+ };
356
+ const onDetailsResizeUp = (e) => {
357
+ detailsDragRef.current = null;
358
+ setDetailsResizing(false);
359
+ e.target.releasePointerCapture(e.pointerId);
360
+ };
305
361
  const fields = columns.filter((col) => col.type !== "entity");
306
362
  const q = fieldQuery.trim().toLowerCase();
307
363
  const matches = (col) => !q || col.label.toLowerCase().includes(q);
@@ -338,7 +394,7 @@ export function RecordDetails({ record, columns, sections, onEdit, labels, class
338
394
  groups = [{ title: undefined, fields: shown }];
339
395
  }
340
396
  const totalShown = groups.reduce((n, g) => n + g.fields.length, 0);
341
- return (_jsxs("div", { className: cx("crisp-recordpage-section", className), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-detailhead", children: [searchOpen ? (_jsx("input", {
397
+ return (_jsxs("div", { className: cx("crisp-recordpage-section", className), "data-resizable": resizable ? "" : undefined, style: resizable ? { width } : undefined, ...rest, children: [_jsxs("div", { className: "crisp-recordpage-detailhead", children: [searchOpen ? (_jsx("input", {
342
398
  // biome-ignore lint/a11y/noAutofocus: focusing the just-opened field search is expected
343
399
  autoFocus: true, type: "text", className: "crisp-recordpage-fieldsearch", placeholder: t.searchPlaceholder, "aria-label": t.searchFields, value: fieldQuery, onChange: (e) => setFieldQuery(e.target.value), onKeyDown: (e) => {
344
400
  if (e.key === "Escape") {
@@ -412,7 +468,13 @@ export function RecordDetails({ record, columns, sections, onEdit, labels, class
412
468
  setSearchOpen(next);
413
469
  if (!next)
414
470
  setFieldQuery("");
415
- }, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : (_jsx("div", { className: "crisp-recordpage-noresults", children: t.noFieldsMatch(fieldQuery) })))] }));
471
+ }, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : (_jsx("div", { className: "crisp-recordpage-noresults", children: t.noFieldsMatch(fieldQuery) }))), resizable && (_jsx("div", { className: "crisp-recordpage-detailresize", "data-active": detailsResizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(width), "aria-valuemin": minWidth, "aria-valuemax": maxWidth, tabIndex: 0, onPointerDown: onDetailsResizeDown, onPointerMove: onDetailsResizeMove, onPointerUp: onDetailsResizeUp, onKeyDown: (e) => {
472
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
473
+ return;
474
+ const rtl = isRtlElement(e.currentTarget);
475
+ const grow = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
476
+ setWidth(grow ? width + 16 : width - 16);
477
+ } }))] }));
416
478
  }
417
479
  RecordDetails.displayName = "RecordDetails";
418
480
  /**
@@ -423,7 +485,7 @@ RecordDetails.displayName = "RecordDetails";
423
485
  * Emails sections (Notes & Tasks are live — add and check things off). Drop it
424
486
  * into the app's main content area.
425
487
  */
426
- export function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, sections, highlights: highlightsProp, quickActions: quickActionsProp, ...rest }) {
488
+ export function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, onClose, closeLabel = "Close", onPrevious, previousRecordLabel = "Previous record", onNext, nextRecordLabel = "Next record", counterLabel, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, sections, highlights: highlightsProp, quickActions: quickActionsProp, comments: commentsProp, onPostComment, currentUser, onShare, ...rest }) {
427
489
  const t = useRecordPageLabels(labels);
428
490
  const [favorite, setFavorite] = React.useState(false);
429
491
  // Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
@@ -435,6 +497,11 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
435
497
  const [internalTasks, setInternalTasks] = React.useState([]);
436
498
  const notes = notesProp ?? internalNotes;
437
499
  const tasks = tasksProp ?? internalTasks;
500
+ // Comments follow the same controlled/self-contained duality (#864).
501
+ const [internalComments, setInternalComments] = React.useState([]);
502
+ const comments = commentsProp ?? internalComments;
503
+ const [commentsOpen, setCommentsOpen] = React.useState(false);
504
+ const me = currentUser ?? { id: "me", name: "You" };
438
505
  const [showAllActivity, setShowAllActivity] = React.useState(false);
439
506
  // Drag-resizable left details column.
440
507
  const [detailsW, setDetailsW] = React.useState(DETAILS_DEFAULT_W);
@@ -593,6 +660,31 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
593
660
  }
594
661
  setInternalTasks((n) => n.map((x) => (x.id === id ? { ...x, done: !x.done } : x)));
595
662
  };
663
+ const postComment = (text, threadId) => {
664
+ if (commentsProp) {
665
+ onPostComment?.(text, threadId);
666
+ return;
667
+ }
668
+ if (threadId == null) {
669
+ setInternalComments((prev) => [
670
+ {
671
+ id: nextId(),
672
+ root: { id: nextId(), author: me, time: t.justNow, body: text },
673
+ },
674
+ ...prev,
675
+ ]);
676
+ return;
677
+ }
678
+ setInternalComments((prev) => prev.map((th) => th.id === threadId
679
+ ? {
680
+ ...th,
681
+ replies: [
682
+ ...(th.replies ?? []),
683
+ { id: nextId(), author: me, time: t.justNow, body: text },
684
+ ],
685
+ }
686
+ : th));
687
+ };
596
688
  // A small, plausible activity feed derived from the record's own fields.
597
689
  const domain = byKey.get("domain");
598
690
  const cats = byKey.get("categories");
@@ -649,7 +741,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
649
741
  const effectivePanel = tabsProp
650
742
  ? (active) => tabsProp.find((tb) => tb.id === active)?.render()
651
743
  : builtinPanel;
652
- return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", children: [_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), _jsx(MenuItem, { icon: _jsx(Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), _jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Plus, { size: 16, strokeWidth: 1.75 }), t.addToList] }), quickActions.map((a, i) => (_jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": a.label, onClick: a.onSelect, children: _jsx(a.icon, { size: 15, strokeWidth: 1.75 }) }, i)))] }), _jsx(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "crisp-recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), _jsx("div", { className: "crisp-recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": DETAILS_MIN_W, "aria-valuemax": DETAILS_MAX_W, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
744
+ return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", "data-peek": onClose ? "" : undefined, children: [onClose ? (_jsxs("div", { className: "crisp-recordpage-peekbar", children: [_jsx("button", { type: "button", className: "crisp-recordpage-peek-iconbtn", "aria-label": closeLabel, onClick: onClose, children: _jsx(X, { size: 16, strokeWidth: 1.75 }) }), (onPrevious || onNext) && (_jsxs("span", { className: "crisp-recordpage-peek-pager", children: [_jsx("button", { type: "button", className: "crisp-recordpage-peek-navbtn", "aria-label": previousRecordLabel, disabled: !onPrevious, onClick: onPrevious, children: _jsx(ChevronUp, { size: 16, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-peek-navbtn", "aria-label": nextRecordLabel, disabled: !onNext, onClick: onNext, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75 }) })] })), counterLabel != null && (_jsx("span", { className: "crisp-recordpage-peek-counter", children: counterLabel }))] })) : (_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] })), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", onClick: onShare, children: t.share }), _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: () => setCommentsOpen(true), "aria-label": t.comments, children: _jsx(MessageSquare, { size: 15, strokeWidth: 1.75 }) }), _jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Merge, { size: 15, strokeWidth: 1.75 }), children: t.mergeRecords }), _jsx(MenuItem, { icon: _jsx(Settings, { size: 15, strokeWidth: 1.75 }), children: t.configurePage }), _jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), children: t.copyPageUrl }), _jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), children: t.copyRecordId }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), _jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Plus, { size: 16, strokeWidth: 1.75 }), t.addToList] }), quickActions.map((a, i) => (_jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": a.label, onClick: a.onSelect, children: _jsx(a.icon, { size: 15, strokeWidth: 1.75 }) }, i)))] }), _jsx(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "crisp-recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), _jsx("div", { className: "crisp-recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": DETAILS_MIN_W, "aria-valuemax": DETAILS_MAX_W, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
653
745
  if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
654
746
  return;
655
747
  // The pointer drag learned its direction in #212; this did not.
@@ -661,6 +753,6 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
661
753
  setDetailsW((w) => grow
662
754
  ? Math.min(DETAILS_MAX_W, w + 16)
663
755
  : Math.max(DETAILS_MIN_W, w - 16));
664
- } }), _jsx("main", { className: "crisp-recordpage-main", children: _jsx(Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] })] }));
756
+ } }), _jsx("main", { className: "crisp-recordpage-main", children: _jsx(Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] }), _jsx(Drawer, { open: commentsOpen, onOpenChange: setCommentsOpen, "aria-label": t.comments, style: { width: 320 }, children: _jsx(CommentsPanel, { threads: comments, currentUserId: me.id, onPost: postComment, onClose: () => setCommentsOpen(false), role: undefined }) })] }));
665
757
  }
666
758
  RecordPage.displayName = "RecordPage";
@@ -23,6 +23,21 @@ const config = defineRecipe({
23
23
  & * { box-sizing: border-box; }
24
24
 
25
25
  & .crisp-recordpage-topbar { display: flex; align-items: center; gap: ${c.space["2"]}; height: 44px; flex-shrink: 0; padding: 0 ${c.space["3"]}; border-bottom: 1px solid ${c.borderSubtle}; }
26
+ /* #624: expanding a peek to its full record keeps a hybrid header
27
+ (Close/Previous/Next/counter) instead of reverting to the plain
28
+ breadcrumb above -- measured live on a real expanded record: 49px
29
+ tall (not 44px), 8px/10px padding, same borderSubtle hairline. Same
30
+ tokens as product/recordpanel's own already-shipped toolbar, not
31
+ new values -- this is the same row, reused in a second component. */
32
+ & .crisp-recordpage-topbar[data-peek] { height: 49px; padding: ${c.space["2"]} ${c.space["2.5"]}; }
33
+ & .crisp-recordpage-peekbar { display: flex; align-items: center; }
34
+ & .crisp-recordpage-peek-iconbtn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex-shrink: 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: transparent; color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s; }
35
+ & .crisp-recordpage-peek-iconbtn:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
36
+ & .crisp-recordpage-peek-pager { display: flex; align-items: center; gap: ${c.space["1.5"]}; margin-inline-start: ${c.space["3"]}; }
37
+ & .crisp-recordpage-peek-navbtn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex-shrink: 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: ${e.field}; color: ${c.fgPrimary}; font-family: inherit; cursor: pointer; }
38
+ & .crisp-recordpage-peek-navbtn:hover:not(:disabled) { background: ${c.bgRaised}; }
39
+ & .crisp-recordpage-peek-navbtn:disabled { opacity: 0.4; cursor: not-allowed; }
40
+ & .crisp-recordpage-peek-counter { margin-inline-start: ${c.space["3"]}; font-size: 12px; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
26
41
  & .crisp-recordpage-crumbs { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; min-width: 0; }
27
42
  & .crisp-recordpage-crumb { display: inline-flex; align-items: center; height: 26px; padding: 0 ${c.space["1.5"]}; margin: 0 calc(-1 * ${c.space["1.5"]}) 0 0; border: none; border-radius: ${c.radius.md}; background: none; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; cursor: pointer; }
28
43
  & .crisp-recordpage-crumb:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
@@ -42,6 +57,17 @@ const config = defineRecipe({
42
57
  & .crisp-recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -4px; width: 9px; }
43
58
  & .crisp-recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -1px; width: 2px; background: transparent; transition: background-color .12s ease; }
44
59
  & .crisp-recordpage-resizer:hover::after, & .crisp-recordpage-resizer[data-active]::after { background: color-mix(in srgb, ${c.bgBrandSolid} 55%, transparent); }
60
+ /* #930: RecordDetails' own standalone resize handle -- same visible
61
+ hit-slop/line-highlight shape as .crisp-recordpage-resizer just above,
62
+ just absolutely positioned at the trailing edge instead of a 0-width
63
+ flex sibling, since RecordDetails always returns a single root (the
64
+ same internal-strip placement RecordPanel's own resize handle uses,
65
+ mirrored to the opposite edge -- this column is leading, RecordPanel
66
+ is trailing). */
67
+ & .crisp-recordpage-section[data-resizable] { position: relative; }
68
+ & .crisp-recordpage-detailresize { position: absolute; top: 0; bottom: 0; inset-inline-end: -4px; width: 9px; cursor: col-resize; z-index: 5; touch-action: none; }
69
+ & .crisp-recordpage-detailresize::after { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-end: 3px; width: 2px; background: transparent; transition: background-color .12s ease; }
70
+ & .crisp-recordpage-detailresize:hover::after, & .crisp-recordpage-detailresize[data-active]::after { background: color-mix(in srgb, ${c.bgBrandSolid} 55%, transparent); }
45
71
  & .crisp-recordpage-entity { display: flex; align-items: center; gap: ${c.space["2"]}; padding: ${c.space["1"]} ${c.space["1"]} ${c.space["2"]}; }
46
72
  & .crisp-recordpage-entity-logo { width: 32px; height: 32px; border-radius: 30%; flex-shrink: 0; object-fit: cover; background: ${c.bgRaised}; }
47
73
  & .crisp-recordpage-entity-name { flex: 0 1 auto; min-width: 0; font-size: ${c.text.xl}; font-weight: ${c.weight.semibold}; letter-spacing: -0.3px; color: ${c.fgPrimary}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -175,12 +201,23 @@ const config = defineRecipe({
175
201
  & .crisp-recordpage-btn--primary { box-shadow: none; background: ${c.bgBrandSolid}; color: ${c.fgOnBrand}; }
176
202
  & .crisp-recordpage-btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
177
203
 
178
- & .crisp-recordpage-note { display: flex; gap: ${c.space["2.5"]}; padding: ${c.space["2.5"]} 0; border-top: 1px solid ${c.borderSubtle}; }
179
- & .crisp-recordpage-note-body { flex: 1; min-width: 0; }
180
- & .crisp-recordpage-note-head { display: flex; align-items: center; gap: ${c.space["1.5"]}; margin-bottom: ${c.space["0.5"]}; }
181
- & .crisp-recordpage-note-author { font-size: ${c.text.md}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }
182
- & .crisp-recordpage-note-time { font-size: ${c.text.sm}; color: ${c.fgMuted}; }
183
- & .crisp-recordpage-note-text { font-size: ${c.text.lg}; color: ${c.fgPrimary}; white-space: pre-wrap; word-break: break-word; }
204
+ /* #871: one bordered card per note (border-radius.xl, borderSubtle),
205
+ not a flush divider-list row -- measured live on a real populated
206
+ note: the whole row is flex space-between, avatar+title+body on the
207
+ left (its own flex, gap.2), the relative time alone on the right.
208
+ The real DOM nests an inner clickable button (padding 6/8, radius.md)
209
+ inside an outer card (padding 4px, radius.xl) -- collapsed into this
210
+ one non-interactive div's own combined padding (2.5/3) since crisp's
211
+ NotesSection has no click-to-open behaviour to justify the extra
212
+ layer. The 6px gap between stacked cards is NOT independently
213
+ measured (this workspace only ever had one real note to check) --
214
+ a judgement call off the existing space scale, not a live value. */
215
+ & .crisp-recordpage-note { display: flex; align-items: center; justify-content: space-between; gap: ${c.space["3"]}; padding: ${c.space["2.5"]} ${c.space["3"]}; border: 1px solid ${c.borderSubtle}; border-radius: ${c.radius.xl}; }
216
+ & .crisp-recordpage-note + .crisp-recordpage-note { margin-top: ${c.space["1.5"]}; }
217
+ & .crisp-recordpage-note-left { display: flex; align-items: center; gap: ${c.space["2"]}; min-width: 0; overflow: hidden; }
218
+ & .crisp-recordpage-note-title { flex-shrink: 0; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }
219
+ & .crisp-recordpage-note-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
220
+ & .crisp-recordpage-note-time { flex-shrink: 0; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 50%, transparent); }
184
221
 
185
222
  & .crisp-recordpage-task { display: flex; align-items: flex-start; gap: ${c.space["2"]}; padding: ${c.space["1.5"]} 0; border-top: 1px solid ${c.borderSubtle}; }
186
223
  & .crisp-recordpage-task-text { flex: 1; min-width: 0; font-size: ${c.text.lg}; color: ${c.fgPrimary}; padding-top: 1px; }
@@ -0,0 +1 @@
1
+ export { WorkspaceGeneralSettings, } from "./workspacegeneralsettings.js";
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Copy } from "lucide-react";
3
+ import { Avatar } from "../../components/avatar/index.js";
4
+ import { Button } from "../../components/button/index.js";
5
+ import { FormField } from "../../components/formfield/index.js";
6
+ import { Input } from "../../components/input/index.js";
7
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../../components/table/index.js";
8
+ import { TextLink } from "../../components/textlink/index.js";
9
+ import { workspacegeneralsettingsRecipe } from "./workspacegeneralsettings.recipe.js";
10
+ /**
11
+ * Attio's Settings → Workspace → General page (crisp#638, P-128), measured
12
+ * live: a logo + Name/Slug section, an "Export workspace data" table, and a
13
+ * "Danger zone" delete-workspace row.
14
+ */
15
+ export function WorkspaceGeneralSettings({ logoSrc, workspaceInitial, onUploadLogo, name, onNameChange, slug, onCopySlug, exports: exportRows, onStartExport, onDeleteWorkspace, className, title = "General", description = "Change the settings for your current workspace.", learnMoreLabel = "Learn more", learnMoreHref = "#", logoHelpText = "We only support PNGs, JPEGs and GIFs under 10MB", uploadLogoLabel = "Upload logo", nameFieldLabel = "Name", slugFieldLabel = "Slug", copySlugLabel = "Copy slug", exportSectionTitle = "Export workspace data", exportSectionDescription = "Exports are in CSV format and can be downloaded within 7 days", startExportLabel = "Start new export", typeColumnLabel = "Type", dateColumnLabel = "Date", noExportsLabel = "No exports yet", dangerSectionTitle = "Danger zone", deleteWorkspaceTitle = "Delete workspace", deleteWorkspaceWarning = "Once deleted, your workspace cannot be recovered", deleteWorkspaceLabel = "Delete workspace", ...rest }) {
16
+ return (_jsx("div", { className: workspacegeneralsettingsRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-workspacegeneralsettings-page", children: [_jsxs("div", { className: "crisp-workspacegeneralsettings-heading", children: [_jsx("h1", { className: "crisp-workspacegeneralsettings-title", children: title }), _jsxs("p", { className: "crisp-workspacegeneralsettings-desc", children: [description, " ", _jsx(TextLink, { href: learnMoreHref, children: learnMoreLabel })] })] }), _jsxs("div", { className: "crisp-workspacegeneralsettings-section", children: [_jsxs("div", { className: "crisp-workspacegeneralsettings-logorow", children: [logoSrc ? (_jsx("img", { src: logoSrc, alt: "", width: 48, height: 48, style: { borderRadius: "30%", objectFit: "cover" } })) : (_jsx(Avatar, { size: "xl", shape: "rounded", tone: "brand", children: workspaceInitial })), _jsxs("div", { className: "crisp-workspacegeneralsettings-logotext", children: [_jsx("span", { className: "crisp-workspacegeneralsettings-logohelp", children: logoHelpText }), _jsx(Button, { intent: "neutral", size: "sm", onClick: onUploadLogo, children: uploadLogoLabel })] })] }), _jsxs("div", { className: "crisp-workspacegeneralsettings-fieldrow", children: [_jsx(FormField, { label: nameFieldLabel, children: _jsx(Input, { value: name, onChange: (e) => onNameChange(e.target.value) }) }), _jsx(FormField, { label: slugFieldLabel, children: _jsxs("div", { className: "crisp-workspacegeneralsettings-slugfield", children: [_jsx(Input, { value: slug, readOnly: true }), _jsx("button", { type: "button", className: "crisp-workspacegeneralsettings-copybtn", "aria-label": copySlugLabel, onClick: onCopySlug, children: _jsx(Copy, { size: 16 }) })] }) })] })] }), _jsxs("div", { className: "crisp-workspacegeneralsettings-section", children: [_jsxs("div", { className: "crisp-workspacegeneralsettings-sectionhead", children: [_jsxs("div", { className: "crisp-workspacegeneralsettings-sectionhead-text", children: [_jsx("h2", { className: "crisp-workspacegeneralsettings-sectiontitle", children: exportSectionTitle }), _jsx("p", { className: "crisp-workspacegeneralsettings-desc", children: exportSectionDescription })] }), _jsx(Button, { intent: "neutral", size: "sm", onClick: onStartExport, children: startExportLabel })] }), _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: typeColumnLabel }), _jsx(TableHead, { children: dateColumnLabel })] }) }), _jsx(TableBody, { children: exportRows.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 2, children: _jsx("div", { className: "crisp-workspacegeneralsettings-emptyrow", children: noExportsLabel }) }) })) : (exportRows.map((row) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: row.type }), _jsx(TableCell, { children: row.date })] }, row.id)))) })] })] }), _jsxs("div", { className: "crisp-workspacegeneralsettings-section", children: [_jsx("h2", { className: "crisp-workspacegeneralsettings-sectiontitle", children: dangerSectionTitle }), _jsxs("div", { className: "crisp-workspacegeneralsettings-dangerrow", children: [_jsxs("div", { className: "crisp-workspacegeneralsettings-dangertext", children: [_jsx("span", { className: "crisp-workspacegeneralsettings-rowtitle", children: deleteWorkspaceTitle }), _jsx("span", { className: "crisp-workspacegeneralsettings-dangerwarning", children: deleteWorkspaceWarning })] }), _jsx(Button, { intent: "danger", size: "sm", onClick: onDeleteWorkspace, children: deleteWorkspaceLabel })] })] })] }) }));
17
+ }
18
+ WorkspaceGeneralSettings.displayName = "WorkspaceGeneralSettings";