@crisp-ui-kit/crisp 0.57.1 → 0.57.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/dist/authshell.css +9 -4
  2. package/dist/authshell.layered.css +9 -4
  3. package/dist/badge.css +3 -3
  4. package/dist/badge.layered.css +3 -3
  5. package/dist/board.css +28 -8
  6. package/dist/board.layered.css +28 -8
  7. package/dist/bottomsheet.css +26 -0
  8. package/dist/bottomsheet.d.ts +7 -0
  9. package/dist/bottomsheet.layered.css +28 -0
  10. package/dist/button.css +10 -6
  11. package/dist/button.d.ts +2 -2
  12. package/dist/button.layered.css +10 -6
  13. package/dist/checkbox.css +7 -3
  14. package/dist/checkbox.layered.css +7 -3
  15. package/dist/chip.css +16 -1
  16. package/dist/chip.d.ts +3 -1
  17. package/dist/chip.layered.css +16 -1
  18. package/dist/chipgroup.css +5 -0
  19. package/dist/chipgroup.d.ts +7 -0
  20. package/dist/chipgroup.layered.css +7 -0
  21. package/dist/cjs/components/badge/badge.recipe.js +22 -8
  22. package/dist/cjs/components/bottomsheet/bottomsheet.js +90 -0
  23. package/dist/cjs/components/bottomsheet/bottomsheet.recipe.js +77 -0
  24. package/dist/cjs/components/bottomsheet/index.js +9 -0
  25. package/dist/cjs/components/breadcrumb/breadcrumb.js +1 -1
  26. package/dist/cjs/components/button/button.recipe.js +67 -15
  27. package/dist/cjs/components/checkbox/checkbox.recipe.js +61 -9
  28. package/dist/cjs/components/chip/chip.js +14 -2
  29. package/dist/cjs/components/chip/chip.recipe.js +72 -2
  30. package/dist/cjs/components/chipgroup/chipgroup.js +51 -0
  31. package/dist/cjs/components/chipgroup/chipgroup.recipe.js +36 -0
  32. package/dist/cjs/components/chipgroup/index.js +7 -0
  33. package/dist/cjs/components/datatable/datatable.js +5 -1
  34. package/dist/cjs/components/datatable/datatable.recipe.js +21 -6
  35. package/dist/cjs/components/datepickerfield/datepickerfield.js +2 -2
  36. package/dist/cjs/components/datepickerfield/datepickerfield.recipe.js +16 -3
  37. package/dist/cjs/components/dialog/dialog.recipe.js +21 -10
  38. package/dist/cjs/components/emptystate/emptystate.recipe.js +7 -2
  39. package/dist/cjs/components/fileupload/fileupload.js +21 -4
  40. package/dist/cjs/components/input/index.js +2 -1
  41. package/dist/cjs/components/input/input.js +31 -1
  42. package/dist/cjs/components/input/input.recipe.js +16 -9
  43. package/dist/cjs/components/menu/menu.js +18 -2
  44. package/dist/cjs/components/menu/menu.recipe.js +21 -7
  45. package/dist/cjs/components/menusheet/index.js +9 -0
  46. package/dist/cjs/components/menusheet/menusheet.js +133 -0
  47. package/dist/cjs/components/menusheet/menusheet.recipe.js +103 -0
  48. package/dist/cjs/components/multiselect/multiselect.js +1 -1
  49. package/dist/cjs/components/multiselect/multiselect.recipe.js +33 -9
  50. package/dist/cjs/components/select/select.js +1 -1
  51. package/dist/cjs/components/select/select.recipe.js +38 -12
  52. package/dist/cjs/components/snackbar/index.js +8 -0
  53. package/dist/cjs/components/snackbar/snackbar.js +121 -0
  54. package/dist/cjs/components/snackbar/snackbar.recipe.js +71 -0
  55. package/dist/cjs/components/table/table.recipe.js +7 -1
  56. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +7 -2
  57. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +37 -12
  58. package/dist/cjs/components/tabs/tabs.js +10 -4
  59. package/dist/cjs/components/tabs/tabs.recipe.js +23 -6
  60. package/dist/cjs/components/textarea/textarea.recipe.js +11 -8
  61. package/dist/cjs/core/floating-portal.js +7 -4
  62. package/dist/cjs/index.js +7 -0
  63. package/dist/cjs/product/authshell/authshell.recipe.js +5 -1
  64. package/dist/cjs/product/command/command.recipe.js +4 -1
  65. package/dist/cjs/product/comments/comments.js +7 -2
  66. package/dist/cjs/product/comments/comments.recipe.js +25 -7
  67. package/dist/cjs/product/inbox/inbox.js +11 -3
  68. package/dist/cjs/product/memberssettings/index.js +6 -0
  69. package/dist/cjs/product/memberssettings/memberssettings.js +99 -0
  70. package/dist/cjs/product/memberssettings/memberssettings.recipe.js +83 -0
  71. package/dist/cjs/product/notificationssettings/index.js +5 -0
  72. package/dist/cjs/product/notificationssettings/notificationssettings.js +19 -0
  73. package/dist/cjs/product/notificationssettings/notificationssettings.recipe.js +44 -0
  74. package/dist/cjs/product/recordpage/index.js +2 -1
  75. package/dist/cjs/product/recordpage/recordpage.js +106 -9
  76. package/dist/cjs/product/recordpage/recordpage.recipe.js +68 -6
  77. package/dist/cjs/product/workspacegeneralsettings/index.js +5 -0
  78. package/dist/cjs/product/workspacegeneralsettings/workspacegeneralsettings.js +21 -0
  79. package/dist/cjs/product/workspacegeneralsettings/workspacegeneralsettings.recipe.js +51 -0
  80. package/dist/cjs/tokens/contract.js +1 -0
  81. package/dist/cjs/tokens/elevation.js +8 -5
  82. package/dist/cjs/tokens/palette.js +28 -8
  83. package/dist/cjs/tokens/scale.js +44 -2
  84. package/dist/cjs/tokens/semantic.js +2 -0
  85. package/dist/columncalc.css +15 -7
  86. package/dist/columncalc.layered.css +15 -7
  87. package/dist/command.css +23 -19
  88. package/dist/command.layered.css +23 -19
  89. package/dist/comments.css +28 -19
  90. package/dist/comments.d.ts +2 -2
  91. package/dist/comments.layered.css +28 -19
  92. package/dist/datatable.css +171 -71
  93. package/dist/datatable.layered.css +171 -71
  94. package/dist/datepicker.css +13 -41
  95. package/dist/datepicker.layered.css +13 -41
  96. package/dist/datepickerfield.css +12 -3
  97. package/dist/datepickerfield.d.ts +2 -2
  98. package/dist/datepickerfield.layered.css +12 -3
  99. package/dist/dialog.css +12 -7
  100. package/dist/dialog.layered.css +12 -7
  101. package/dist/emptystate.css +5 -3
  102. package/dist/emptystate.layered.css +5 -3
  103. package/dist/esm/components/badge/badge.recipe.js +22 -8
  104. package/dist/esm/components/bottomsheet/bottomsheet.js +52 -0
  105. package/dist/esm/components/bottomsheet/bottomsheet.recipe.js +74 -0
  106. package/dist/esm/components/bottomsheet/index.js +2 -0
  107. package/dist/esm/components/breadcrumb/breadcrumb.js +1 -1
  108. package/dist/esm/components/button/button.recipe.js +68 -16
  109. package/dist/esm/components/checkbox/checkbox.recipe.js +61 -9
  110. package/dist/esm/components/chip/chip.js +14 -2
  111. package/dist/esm/components/chip/chip.recipe.js +72 -2
  112. package/dist/esm/components/chipgroup/chipgroup.js +15 -0
  113. package/dist/esm/components/chipgroup/chipgroup.recipe.js +33 -0
  114. package/dist/esm/components/chipgroup/index.js +2 -0
  115. package/dist/esm/components/datatable/datatable.js +5 -1
  116. package/dist/esm/components/datatable/datatable.recipe.js +21 -6
  117. package/dist/esm/components/datepickerfield/datepickerfield.js +2 -2
  118. package/dist/esm/components/datepickerfield/datepickerfield.recipe.js +16 -3
  119. package/dist/esm/components/dialog/dialog.recipe.js +22 -11
  120. package/dist/esm/components/emptystate/emptystate.recipe.js +7 -2
  121. package/dist/esm/components/fileupload/fileupload.js +20 -5
  122. package/dist/esm/components/input/index.js +1 -1
  123. package/dist/esm/components/input/input.js +30 -0
  124. package/dist/esm/components/input/input.recipe.js +17 -10
  125. package/dist/esm/components/menu/menu.js +18 -2
  126. package/dist/esm/components/menu/menu.recipe.js +21 -7
  127. package/dist/esm/components/menusheet/index.js +2 -0
  128. package/dist/esm/components/menusheet/menusheet.js +95 -0
  129. package/dist/esm/components/menusheet/menusheet.recipe.js +100 -0
  130. package/dist/esm/components/multiselect/multiselect.js +1 -1
  131. package/dist/esm/components/multiselect/multiselect.recipe.js +33 -9
  132. package/dist/esm/components/select/select.js +1 -1
  133. package/dist/esm/components/select/select.recipe.js +38 -12
  134. package/dist/esm/components/snackbar/index.js +2 -0
  135. package/dist/esm/components/snackbar/snackbar.js +84 -0
  136. package/dist/esm/components/snackbar/snackbar.recipe.js +68 -0
  137. package/dist/esm/components/table/table.recipe.js +7 -1
  138. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +7 -2
  139. package/dist/esm/components/tabletoolbar/tabletoolbar.js +37 -12
  140. package/dist/esm/components/tabs/tabs.js +10 -4
  141. package/dist/esm/components/tabs/tabs.recipe.js +23 -6
  142. package/dist/esm/components/textarea/textarea.recipe.js +12 -9
  143. package/dist/esm/core/floating-portal.js +7 -4
  144. package/dist/esm/index.js +7 -0
  145. package/dist/esm/product/authshell/authshell.recipe.js +5 -1
  146. package/dist/esm/product/command/command.recipe.js +4 -1
  147. package/dist/esm/product/comments/comments.js +7 -2
  148. package/dist/esm/product/comments/comments.recipe.js +25 -7
  149. package/dist/esm/product/inbox/inbox.js +11 -3
  150. package/dist/esm/product/memberssettings/index.js +1 -0
  151. package/dist/esm/product/memberssettings/memberssettings.js +62 -0
  152. package/dist/esm/product/memberssettings/memberssettings.recipe.js +80 -0
  153. package/dist/esm/product/notificationssettings/index.js +1 -0
  154. package/dist/esm/product/notificationssettings/notificationssettings.js +16 -0
  155. package/dist/esm/product/notificationssettings/notificationssettings.recipe.js +41 -0
  156. package/dist/esm/product/recordpage/index.js +1 -1
  157. package/dist/esm/product/recordpage/recordpage.js +106 -10
  158. package/dist/esm/product/recordpage/recordpage.recipe.js +68 -6
  159. package/dist/esm/product/workspacegeneralsettings/index.js +1 -0
  160. package/dist/esm/product/workspacegeneralsettings/workspacegeneralsettings.js +18 -0
  161. package/dist/esm/product/workspacegeneralsettings/workspacegeneralsettings.recipe.js +48 -0
  162. package/dist/esm/tokens/contract.js +2 -1
  163. package/dist/esm/tokens/elevation.js +8 -5
  164. package/dist/esm/tokens/palette.js +28 -8
  165. package/dist/esm/tokens/scale.js +43 -1
  166. package/dist/esm/tokens/semantic.js +2 -0
  167. package/dist/fileupload.css +3 -1
  168. package/dist/fileupload.layered.css +3 -1
  169. package/dist/input.css +6 -4
  170. package/dist/input.layered.css +6 -4
  171. package/dist/listpage.css +3 -1
  172. package/dist/listpage.layered.css +3 -1
  173. package/dist/memberssettings.css +67 -0
  174. package/dist/memberssettings.d.ts +7 -0
  175. package/dist/memberssettings.layered.css +69 -0
  176. package/dist/menu.css +2 -2
  177. package/dist/menu.layered.css +2 -2
  178. package/dist/menusheet.css +36 -0
  179. package/dist/menusheet.d.ts +7 -0
  180. package/dist/menusheet.layered.css +38 -0
  181. package/dist/multiselect.css +13 -6
  182. package/dist/multiselect.layered.css +13 -6
  183. package/dist/native/tokens.d.ts +2 -1
  184. package/dist/native/tokens.js +20 -3
  185. package/dist/notificationssettings.css +22 -0
  186. package/dist/notificationssettings.d.ts +7 -0
  187. package/dist/notificationssettings.layered.css +24 -0
  188. package/dist/objectsettings.css +4 -4
  189. package/dist/objectsettings.layered.css +4 -4
  190. package/dist/recordpage.css +90 -23
  191. package/dist/recordpage.layered.css +90 -23
  192. package/dist/recordpanel.css +35 -16
  193. package/dist/recordpanel.layered.css +35 -16
  194. package/dist/resizablepanel.css +8 -5
  195. package/dist/resizablepanel.layered.css +8 -5
  196. package/dist/richtext.css +24 -8
  197. package/dist/richtext.layered.css +24 -8
  198. package/dist/savedviews.css +9 -2
  199. package/dist/savedviews.layered.css +9 -2
  200. package/dist/select.css +25 -5
  201. package/dist/select.layered.css +25 -5
  202. package/dist/settingsshell.css +2 -3
  203. package/dist/settingsshell.layered.css +2 -3
  204. package/dist/slider.css +6 -27
  205. package/dist/slider.layered.css +6 -27
  206. package/dist/snackbar.css +24 -0
  207. package/dist/snackbar.d.ts +7 -0
  208. package/dist/snackbar.layered.css +26 -0
  209. package/dist/statcard.css +3 -1
  210. package/dist/statcard.layered.css +3 -1
  211. package/dist/styles.css +911 -354
  212. package/dist/styles.layered.css +911 -354
  213. package/dist/table.css +35 -12
  214. package/dist/table.layered.css +35 -12
  215. package/dist/tabletoolbar.css +35 -24
  216. package/dist/tabletoolbar.layered.css +35 -24
  217. package/dist/tabs.css +18 -4
  218. package/dist/tabs.layered.css +18 -4
  219. package/dist/textarea.css +2 -6
  220. package/dist/textarea.layered.css +2 -6
  221. package/dist/themepicker.css +2 -1
  222. package/dist/themepicker.layered.css +2 -1
  223. package/dist/tokens.css +13 -2
  224. package/dist/tokens.json +20 -3
  225. package/dist/types/components/bottomsheet/bottomsheet.d.ts +35 -0
  226. package/dist/types/components/bottomsheet/bottomsheet.recipe.d.ts +3 -0
  227. package/dist/types/components/bottomsheet/index.d.ts +2 -0
  228. package/dist/types/components/breadcrumb/breadcrumb.d.ts +9 -1
  229. package/dist/types/components/button/button.d.ts +2 -0
  230. package/dist/types/components/button/button.recipe.d.ts +24 -6
  231. package/dist/types/components/checkbox/checkbox.recipe.d.ts +28 -5
  232. package/dist/types/components/chip/chip.d.ts +22 -0
  233. package/dist/types/components/chip/chip.recipe.d.ts +49 -1
  234. package/dist/types/components/chipgroup/chipgroup.d.ts +14 -0
  235. package/dist/types/components/chipgroup/chipgroup.recipe.d.ts +5 -0
  236. package/dist/types/components/chipgroup/index.d.ts +2 -0
  237. package/dist/types/components/datatable/datatable.d.ts +19 -0
  238. package/dist/types/components/datepickerfield/datepickerfield.d.ts +4 -1
  239. package/dist/types/components/datepickerfield/datepickerfield.recipe.d.ts +18 -3
  240. package/dist/types/components/fileupload/fileupload.d.ts +26 -0
  241. package/dist/types/components/input/index.d.ts +1 -1
  242. package/dist/types/components/input/input.d.ts +72 -0
  243. package/dist/types/components/menusheet/index.d.ts +2 -0
  244. package/dist/types/components/menusheet/menusheet.d.ts +42 -0
  245. package/dist/types/components/menusheet/menusheet.recipe.d.ts +3 -0
  246. package/dist/types/components/snackbar/index.d.ts +2 -0
  247. package/dist/types/components/snackbar/snackbar.d.ts +36 -0
  248. package/dist/types/components/snackbar/snackbar.recipe.d.ts +5 -0
  249. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +28 -1
  250. package/dist/types/components/tabs/tabs.d.ts +11 -1
  251. package/dist/types/core/floating-portal.d.ts +6 -5
  252. package/dist/types/index.d.ts +7 -0
  253. package/dist/types/product/comments/comments.d.ts +9 -3
  254. package/dist/types/product/comments/comments.recipe.d.ts +12 -2
  255. package/dist/types/product/inbox/inbox.d.ts +13 -1
  256. package/dist/types/product/memberssettings/index.d.ts +1 -0
  257. package/dist/types/product/memberssettings/memberssettings.d.ts +85 -0
  258. package/dist/types/product/memberssettings/memberssettings.recipe.d.ts +3 -0
  259. package/dist/types/product/notificationssettings/index.d.ts +1 -0
  260. package/dist/types/product/notificationssettings/notificationssettings.d.ts +37 -0
  261. package/dist/types/product/notificationssettings/notificationssettings.recipe.d.ts +3 -0
  262. package/dist/types/product/recordpage/index.d.ts +1 -1
  263. package/dist/types/product/recordpage/recordpage.d.ts +99 -2
  264. package/dist/types/product/workspacegeneralsettings/index.d.ts +1 -0
  265. package/dist/types/product/workspacegeneralsettings/workspacegeneralsettings.d.ts +48 -0
  266. package/dist/types/product/workspacegeneralsettings/workspacegeneralsettings.recipe.d.ts +3 -0
  267. package/dist/types/tokens/contract.d.ts +4 -1
  268. package/dist/types/tokens/elevation.d.ts +8 -5
  269. package/dist/types/tokens/palette.d.ts +9 -1
  270. package/dist/types/tokens/scale.d.ts +11 -1
  271. package/dist/types/tokens/semantic.d.ts +2 -0
  272. package/dist/wheelpicker.css +4 -2
  273. package/dist/wheelpicker.layered.css +4 -2
  274. package/dist/workspacegeneralsettings.css +30 -0
  275. package/dist/workspacegeneralsettings.d.ts +7 -0
  276. package/dist/workspacegeneralsettings.layered.css +32 -0
  277. package/package.json +36 -1
@@ -1,7 +1,8 @@
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, Merge, MessageSquare, Minimize2, MoreHorizontal, Pencil, Phone, Plus, Search, Settings, 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, ListFilter, 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
+ import { Button } from "../../components/button/index.js";
5
6
  import { Checkbox } from "../../components/checkbox/index.js";
6
7
  import { Drawer } from "../../components/drawer/index.js";
7
8
  import { Menu, MenuItem, MenuSeparator } from "../../components/menu/index.js";
@@ -62,6 +63,22 @@ export const recordPageLabels = {
62
63
  share: "Share",
63
64
  comments: "Comments",
64
65
  noLists: "This record has not been added to any lists.",
66
+ filters: "Filters",
67
+ addMeeting: "Add meeting",
68
+ noFiles: "No files",
69
+ filesHint: "Drag a file here or choose one from your computer.",
70
+ newFolder: "New folder",
71
+ uploadFile: "Upload file",
72
+ fileMoreActions: "More file actions",
73
+ noDealsFound: "No Deals found",
74
+ noDealsHint: "There are no related records for this relationship.",
75
+ newDeal: "New Deal",
76
+ noEmails: "No emails",
77
+ noEmailsHint: "This record doesn't have any emails, or they may be hidden due to permissions.",
78
+ configureMailbox: "Configure mailbox",
79
+ upgradeToViewCalls: "Upgrade to view calls",
80
+ callsUpgradeHint: "No call recordings yet! Upgrade to Pro to start recording calls.",
81
+ upgrade: "Upgrade",
65
82
  highlights: "Highlights",
66
83
  recordDetails: "Record Details",
67
84
  searchFields: "Search fields",
@@ -273,7 +290,14 @@ function NotesSection({ notes, onAdd, labels, }) {
273
290
  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) => {
274
291
  onAdd(t);
275
292
  setOpen(false);
276
- } })), 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))))] }));
293
+ } })), notes.length === 0 && !open ? (_jsx("div", { className: "crisp-recordpage-emptystate", children: labels.noNotesYet })) : (
294
+ // #871: one line, not a two-row card -- measured live: avatar,
295
+ // then the note's own title and body run together inline, then a
296
+ // relative time at the far end of the SAME row. No author name at
297
+ // all (Attio's real preview never shows one here, only the note's
298
+ // own content) -- `labels.you` stays defined for a consumer that
299
+ // still wants it elsewhere, just unused in this exact spot now.
300
+ 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))))] }));
277
301
  }
278
302
  function TasksSection({ tasks, onAdd, onToggle, labels, }) {
279
303
  const [open, setOpen] = React.useState(false);
@@ -306,13 +330,51 @@ function EmptyCard({ icon, children, }) {
306
330
  * `RecordPage`'s own root does: `<div className={recordpageRecipe()}>`
307
331
  * (also exported from this package).
308
332
  */
309
- export function RecordDetails({ record, columns, sections, onEdit, labels, className, ...rest }) {
333
+ 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 }) {
310
334
  const t = useRecordPageLabels(labels);
311
335
  const { editingKey, pendingKey, error, openEditing, closeEditing, commit } = useEditableField(onEdit);
312
336
  const [detailsOpen, setDetailsOpen] = React.useState(true);
313
337
  const [searchOpen, setSearchOpen] = React.useState(false);
314
338
  const [fieldQuery, setFieldQuery] = React.useState("");
315
339
  const [showAllValues, setShowAllValues] = React.useState(false);
340
+ // #930: controlled/uncontrolled width, same split as RecordPanel's own
341
+ // resizable width -- only read when `resizable`, same reasoning as
342
+ // RecordPanel's own (a non-resizing consumer has no business carrying
343
+ // width state at all).
344
+ const [internalWidth, setInternalWidth] = React.useState(defaultWidth);
345
+ const width = controlledWidth ?? internalWidth;
346
+ const setWidth = (next) => {
347
+ const clamped = Math.max(minWidth, Math.min(maxWidth, next));
348
+ if (controlledWidth === undefined)
349
+ setInternalWidth(clamped);
350
+ onWidthChange?.(clamped);
351
+ };
352
+ const [detailsResizing, setDetailsResizing] = React.useState(false);
353
+ const detailsDragRef = React.useRef(null);
354
+ const onDetailsResizeDown = (e) => {
355
+ // This column is a leading (left-docked) rail, the mirror image of
356
+ // RecordPanel's own trailing (right-docked) one -- a rightward pull
357
+ // grows it in LTR, the same direction RecordPage's own internal aside
358
+ // resize already uses (#212/#255).
359
+ detailsDragRef.current = {
360
+ x: e.clientX,
361
+ w: width,
362
+ dir: isRtlElement(e.currentTarget) ? -1 : 1,
363
+ };
364
+ setDetailsResizing(true);
365
+ e.target.setPointerCapture(e.pointerId);
366
+ };
367
+ const onDetailsResizeMove = (e) => {
368
+ const r = detailsDragRef.current;
369
+ if (!r)
370
+ return;
371
+ setWidth(r.w + r.dir * (e.clientX - r.x));
372
+ };
373
+ const onDetailsResizeUp = (e) => {
374
+ detailsDragRef.current = null;
375
+ setDetailsResizing(false);
376
+ e.target.releasePointerCapture(e.pointerId);
377
+ };
316
378
  const fields = columns.filter((col) => col.type !== "entity");
317
379
  const q = fieldQuery.trim().toLowerCase();
318
380
  const matches = (col) => !q || col.label.toLowerCase().includes(q);
@@ -349,7 +411,7 @@ export function RecordDetails({ record, columns, sections, onEdit, labels, class
349
411
  groups = [{ title: undefined, fields: shown }];
350
412
  }
351
413
  const totalShown = groups.reduce((n, g) => n + g.fields.length, 0);
352
- return (_jsxs("div", { className: cx("crisp-recordpage-section", className), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-detailhead", children: [searchOpen ? (_jsx("input", {
414
+ 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", {
353
415
  // biome-ignore lint/a11y/noAutofocus: focusing the just-opened field search is expected
354
416
  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) => {
355
417
  if (e.key === "Escape") {
@@ -423,9 +485,37 @@ export function RecordDetails({ record, columns, sections, onEdit, labels, class
423
485
  setSearchOpen(next);
424
486
  if (!next)
425
487
  setFieldQuery("");
426
- }, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : (_jsx("div", { className: "crisp-recordpage-noresults", children: t.noFieldsMatch(fieldQuery) })))] }));
488
+ }, 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) => {
489
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
490
+ return;
491
+ const rtl = isRtlElement(e.currentTarget);
492
+ const grow = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
493
+ setWidth(grow ? width + 16 : width - 16);
494
+ } }))] }));
427
495
  }
428
496
  RecordDetails.displayName = "RecordDetails";
497
+ /**
498
+ * The main/tab-content column's own box, on its own (#750) — the same
499
+ * "standalone piece" `RecordDetails` already is for the Details column,
500
+ * mirrored to the other side of the layout. `RecordPage` composes this
501
+ * internally for its own tabbed body (below) — this exact box, not a second
502
+ * copy — so a consumer building a custom record page (its own tab set, its
503
+ * own menu, `RecordDetails` for the left column, but not the full
504
+ * `RecordPage`, which hardcodes the CRM tab set) gets the same
505
+ * Attio-measured inset: `flex: 1; min-width: 0; display: flex;
506
+ * flex-direction: column; min-height: 0; overflow-y: auto; padding: 12px
507
+ * 20px` (`recordpage.recipe.ts`'s own `.crisp-recordpage-main`) — without
508
+ * reaching for that internal class name directly, the only option before
509
+ * this existed.
510
+ *
511
+ * Needs the same single `.crisp-recordpage`-scoped ancestor `RecordDetails`
512
+ * already documents — wrap both in `recordpageRecipe()`, the way
513
+ * `RecordPage`'s own root does.
514
+ */
515
+ export function RecordPageMain({ className, children, ...rest }) {
516
+ return (_jsx("main", { className: cx("crisp-recordpage-main", className), ...rest, children: children }));
517
+ }
518
+ RecordPageMain.displayName = "RecordPageMain";
429
519
  /**
430
520
  * The full-page company record view — the page that opens when you click into a
431
521
  * record (as opposed to the peek [RecordPanel]). A top breadcrumb bar with
@@ -434,7 +524,7 @@ RecordDetails.displayName = "RecordDetails";
434
524
  * Emails sections (Notes & Tasks are live — add and check things off). Drop it
435
525
  * into the app's main content area.
436
526
  */
437
- 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, comments: commentsProp, onPostComment, currentUser, onShare, ...rest }) {
527
+ 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 }) {
438
528
  const t = useRecordPageLabels(labels);
439
529
  const [favorite, setFavorite] = React.useState(false);
440
530
  // Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
@@ -666,13 +756,19 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
666
756
  case "overview":
667
757
  return (_jsxs("div", { className: "crisp-recordpage-panel", children: [highlights !== false && (_jsxs("section", { children: [_jsx("div", { className: "crisp-recordpage-section-title", children: t.highlights }), _jsx("div", { className: "crisp-recordpage-hl-card", children: _jsx("div", { className: "crisp-recordpage-hl-grid", children: highlights.map((h) => (_jsxs("div", { className: "crisp-recordpage-hl-cell", children: [h.icon && (_jsx(h.icon, { className: "crisp-recordpage-hl-icon", size: 15, strokeWidth: 1.75 })), _jsx("div", { className: "crisp-recordpage-hl-label", children: h.label }), _jsx("div", { className: "crisp-recordpage-hl-value", children: h.value ?? (_jsx("span", { className: "crisp-recordpage-hl-value--empty", children: h.empty })) })] }, h.label))) }) })] })), activityFeed, _jsxs("section", { children: [_jsx(SectionHead, { title: t.tabs.emails, count: 0 }), emailsEmpty] }), _jsx(NotesSection, { notes: notes, onAdd: addNote, labels: t }), _jsx(TasksSection, { tasks: tasks, onAdd: addTask, onToggle: toggleTask, labels: t })] }));
668
758
  case "activity":
669
- return _jsx("div", { className: "crisp-recordpage-panel", children: activityFeed });
759
+ return (_jsx("div", { className: "crisp-recordpage-panel", children: _jsxs("section", { children: [_jsx(SectionHead, { title: t.tabs.activity, action: _jsxs("span", { className: "crisp-recordpage-tabtoolbar", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action", children: [_jsx(ListFilter, { size: 14, strokeWidth: 1.75 }), t.filters] }), _jsxs("button", { type: "button", className: "crisp-recordpage-action", children: [_jsx(Plus, { size: 14, strokeWidth: 1.75 }), t.addMeeting] })] }) }), _jsx("div", { className: "crisp-recordpage-feed", children: activity.map((a) => (_jsxs("div", { className: "crisp-recordpage-feed-item", children: [_jsx("span", { className: "crisp-recordpage-feed-dot", children: _jsx(ActivityIcon, { size: 13, strokeWidth: 2 }) }), _jsxs("div", { className: "crisp-recordpage-feed-body", children: [a.text, _jsx("div", { className: "crisp-recordpage-feed-time", children: a.time })] })] }, a.id))) })] }) }));
760
+ case "files":
761
+ return (_jsx("div", { className: "crisp-recordpage-panel crisp-recordpage-panel--center", children: _jsxs("div", { className: "crisp-recordpage-illustrated", children: [_jsx("span", { className: "crisp-recordpage-illustrated-icon", "aria-hidden": "true", children: _jsx(Folder, { size: 40, strokeWidth: 1.25 }) }), _jsx("div", { className: "crisp-recordpage-illustrated-title", children: t.noFiles }), _jsx("div", { className: "crisp-recordpage-illustrated-desc", children: t.filesHint }), _jsxs("div", { className: "crisp-recordpage-illustrated-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action", children: [_jsx(Plus, { size: 14, strokeWidth: 1.75 }), t.newFolder] }), _jsxs("button", { type: "button", className: "crisp-recordpage-action", children: [_jsx(Plus, { size: 14, strokeWidth: 1.75 }), t.uploadFile] }), _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.fileMoreActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) })] })] }) }));
762
+ case "deals":
763
+ return (_jsx("div", { className: "crisp-recordpage-panel crisp-recordpage-panel--center", children: _jsxs("div", { className: "crisp-recordpage-illustrated", children: [_jsx("span", { className: "crisp-recordpage-illustrated-icon", "aria-hidden": "true", children: _jsx(Merge, { size: 40, strokeWidth: 1.25 }) }), _jsx("div", { className: "crisp-recordpage-illustrated-title", children: t.noDealsFound }), _jsx("div", { className: "crisp-recordpage-illustrated-desc", children: t.noDealsHint }), _jsx("div", { className: "crisp-recordpage-illustrated-actions", children: _jsxs(Button, { intent: "primary", size: "md", children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), t.newDeal] }) })] }) }));
670
764
  case "notes":
671
765
  return (_jsx("div", { className: "crisp-recordpage-panel", children: _jsx(NotesSection, { notes: notes, onAdd: addNote, labels: t }) }));
672
766
  case "tasks":
673
767
  return (_jsx("div", { className: "crisp-recordpage-panel", children: _jsx(TasksSection, { tasks: tasks, onAdd: addTask, onToggle: toggleTask, labels: t }) }));
674
768
  case "emails":
675
- return _jsx("div", { className: "crisp-recordpage-panel", children: emailsEmpty });
769
+ return (_jsxs("div", { className: "crisp-recordpage-panel", children: [_jsx(SectionHead, { title: t.tabs.emails }), _jsx("div", { className: "crisp-recordpage-panel crisp-recordpage-panel--center", children: _jsxs("div", { className: "crisp-recordpage-illustrated", children: [_jsx("span", { className: "crisp-recordpage-illustrated-icon", "aria-hidden": "true", children: _jsx(Mail, { size: 40, strokeWidth: 1.25 }) }), _jsx("div", { className: "crisp-recordpage-illustrated-title", children: t.noEmails }), _jsx("div", { className: "crisp-recordpage-illustrated-desc", children: t.noEmailsHint }), _jsx("div", { className: "crisp-recordpage-illustrated-actions", children: _jsx(Button, { intent: "primary", size: "md", children: t.configureMailbox }) })] }) })] }));
770
+ case "calls":
771
+ return (_jsx("div", { className: "crisp-recordpage-panel crisp-recordpage-panel--center", children: _jsxs("div", { className: "crisp-recordpage-illustrated", children: [_jsx("span", { className: "crisp-recordpage-illustrated-icon", "aria-hidden": "true", children: _jsx(Phone, { size: 40, strokeWidth: 1.25 }) }), _jsx("div", { className: "crisp-recordpage-illustrated-title crisp-recordpage-illustrated-title--lg", children: t.upgradeToViewCalls }), _jsx("div", { className: "crisp-recordpage-illustrated-desc", children: t.callsUpgradeHint }), _jsx("div", { className: "crisp-recordpage-illustrated-actions", children: _jsxs(Button, { intent: "primary", size: "md", children: [_jsx(Zap, { size: 15, strokeWidth: 1.75 }), t.upgrade] }) })] }) }));
676
772
  default:
677
773
  return (_jsx("div", { className: "crisp-recordpage-panel", children: _jsx(EmptyCard, { icon: _jsx(Folder, { size: 22, strokeWidth: 1.5 }), children: t.nothingHereYet }) }));
678
774
  }
@@ -690,7 +786,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
690
786
  const effectivePanel = tabsProp
691
787
  ? (active) => tabsProp.find((tb) => tb.id === active)?.render()
692
788
  : builtinPanel;
693
- 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: [_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) => {
789
+ 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) => {
694
790
  if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
695
791
  return;
696
792
  // The pointer drag learned its direction in #212; this did not.
@@ -702,6 +798,6 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
702
798
  setDetailsW((w) => grow
703
799
  ? Math.min(DETAILS_MAX_W, w + 16)
704
800
  : Math.max(DETAILS_MIN_W, w - 16));
705
- } }), _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 }) })] }));
801
+ } }), _jsx(RecordPageMain, { 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 }) })] }));
706
802
  }
707
803
  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; }
@@ -189,6 +226,31 @@ const config = defineRecipe({
189
226
  & .crisp-recordpage-emptycard { display: flex; flex-direction: column; align-items: center; gap: ${c.space["1.5"]}; padding: ${c.space["6"]} ${c.space["3"]}; border: 1px dashed ${c.borderDefault}; border-radius: ${c.radius.xl}; color: ${c.fgMuted}; font-size: ${c.text.md}; text-align: center; }
190
227
  & .crisp-recordpage-emptycard svg { color: ${c.fgMuted}; opacity: 0.7; }
191
228
 
229
+ /* #622: the Activity tab's own toolbar -- "Filters" + "+ Add meeting",
230
+ both the same measured button chrome as the topbar's own action
231
+ buttons (28px, e.field shadow, 14/500 label). */
232
+ & .crisp-recordpage-tabtoolbar { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; }
233
+
234
+ /* #622: Files tab's empty state -- measured live on Attio's own record
235
+ page (a company with no files uploaded). The 138x138 dotted-grid
236
+ backdrop is the same measured tile EmptyState's records variant uses
237
+ (crisp/#603), duplicated here rather than composed through that
238
+ component because this surface's title (16/600) and body (14/500,
239
+ 63%-mixed fgPrimary, not the flat fgMuted EmptyState uses) measure to
240
+ their own values, matching neither of that component's existing
241
+ variants. */
242
+ & .crisp-recordpage-panel--center { flex: 1; align-items: center; justify-content: center; }
243
+ & .crisp-recordpage-illustrated { display: flex; flex-direction: column; align-items: center; gap: ${c.space["1.5"]}; text-align: center; }
244
+ & .crisp-recordpage-illustrated-icon { display: flex; align-items: center; justify-content: center; width: 138px; height: 138px; margin-bottom: ${c.space["2"]}; color: color-mix(in srgb, ${c.fgPrimary} 22%, transparent); background: radial-gradient(circle, color-mix(in srgb, ${c.fgPrimary} 12%, transparent) 1.2px, transparent 1.2px) 0 0 / 16px 16px; }
245
+ & .crisp-recordpage-illustrated-title { font-size: ${c.text.xl}; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; }
246
+ /* #988: the Calls tab's own gated state measures a larger 20/24 title
247
+ than Files/Deals' 16/600 -- Attio's own "empty view" pattern (the
248
+ same 20/600 EmptyState's page/records variants already carry, not a
249
+ one-off). */
250
+ & .crisp-recordpage-illustrated-title--lg { font-size: 20px; line-height: 24px; }
251
+ & .crisp-recordpage-illustrated-desc { max-width: 320px; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
252
+ & .crisp-recordpage-illustrated-actions { display: inline-flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: ${c.space["1.5"]}; margin-top: ${c.space["2"]}; }
253
+
192
254
  & .crisp-recordpage-action[data-active] svg { color: #f5b83d; fill: #f5b83d; }
193
255
 
194
256
  & .crisp-recordstable-tag { display: inline-flex; align-items: center; height: ${c.size.chip}; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.chip}; font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium}; white-space: nowrap; background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: color-mix(in srgb, ${c.fgPrimary} 82%, transparent); }
@@ -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";
@@ -0,0 +1,48 @@
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/workspace
4
+ // (crisp#638, P-128). Logo tile + Name/Slug fields side by side, an
5
+ // "Export workspace data" section (header row + Type/Date table + empty
6
+ // state), and a "Danger zone" section (a red-tinted bordered row).
7
+ const config = defineRecipe({
8
+ name: "workspacegeneralsettings",
9
+ base: `& .crisp-workspacegeneralsettings-page { display: flex; flex-direction: column; gap: ${c.space["6"]}; font-family: ${c.font.sans}; }
10
+ & .crisp-workspacegeneralsettings-heading { display: flex; flex-direction: column; gap: ${c.space["0.5"]}; }
11
+ & .crisp-workspacegeneralsettings-title { font-size: 24px; line-height: 28px; font-weight: ${c.weight.semibold}; letter-spacing: -0.48px; color: ${c.fgPrimary}; margin: 0; }
12
+ & .crisp-workspacegeneralsettings-desc { font-size: ${c.text.lg}; line-height: 20px; color: ${c.fgMuted}; margin: 0; }
13
+
14
+ & .crisp-workspacegeneralsettings-section { display: flex; flex-direction: column; gap: ${c.space["4"]}; }
15
+ & .crisp-workspacegeneralsettings-sectiontitle { font-size: 16px; line-height: 20px; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; margin: 0; }
16
+ & .crisp-workspacegeneralsettings-sectionhead { display: flex; align-items: flex-start; justify-content: space-between; gap: ${c.space["4"]}; }
17
+ & .crisp-workspacegeneralsettings-sectionhead-text { display: flex; flex-direction: column; gap: ${c.space["1"]}; }
18
+
19
+ /* Logo row: a tile + helper text + "Upload logo" button. */
20
+ & .crisp-workspacegeneralsettings-logorow { display: flex; align-items: center; gap: ${c.space["4"]}; }
21
+ & .crisp-workspacegeneralsettings-logohelp { font-size: ${c.text.sm}; color: ${c.fgMuted}; }
22
+ & .crisp-workspacegeneralsettings-logotext { display: flex; flex-direction: column; align-items: flex-start; gap: ${c.space["1"]}; }
23
+
24
+ /* Name + Slug sit side by side, each ~half the content width. */
25
+ & .crisp-workspacegeneralsettings-fieldrow { display: flex; gap: ${c.space["4"]}; }
26
+ & .crisp-workspacegeneralsettings-fieldrow > * { flex: 1; min-width: 0; }
27
+ & .crisp-workspacegeneralsettings-slugfield { display: flex; align-items: center; gap: ${c.space["1.5"]}; }
28
+ & .crisp-workspacegeneralsettings-slugfield > :first-child { flex: 1; min-width: 0; }
29
+ & .crisp-workspacegeneralsettings-copybtn { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 32px; height: 32px; border: none; border-radius: ${c.radius.md}; background: none; font-family: inherit; color: ${c.fgMuted}; cursor: pointer; }
30
+ & .crisp-workspacegeneralsettings-copybtn:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
31
+
32
+ /* Export-data empty state -- measured: 14/500/63%-opacity, centred in a
33
+ 40px row, no border of its own (the Table wrapper already draws it). */
34
+ & .crisp-workspacegeneralsettings-emptyrow { padding: ${c.space["2.5"]} ${c.space["4"]}; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); text-align: center; }
35
+
36
+ /* Danger zone: a red-tinted bordered row, distinct from every other
37
+ card on the page. */
38
+ & .crisp-workspacegeneralsettings-dangerrow { display: flex; align-items: center; justify-content: space-between; gap: ${c.space["6"]}; padding: ${c.space["3"]} ${c.space["4"]}; border: 1px solid color-mix(in srgb, ${c.bgDanger} 30%, transparent); border-radius: 12px; background: color-mix(in srgb, ${c.bgDanger} 6%, transparent); }
39
+ & .crisp-workspacegeneralsettings-dangertext { display: flex; flex-direction: column; gap: ${c.space["1"]}; }
40
+ & .crisp-workspacegeneralsettings-rowtitle { font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }
41
+ & .crisp-workspacegeneralsettings-dangerwarning { font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
42
+ `,
43
+ variants: {},
44
+ defaultVariants: {},
45
+ compoundVariants: [],
46
+ });
47
+ export const workspacegeneralsettingsRecipe = resolveRecipe(config);
48
+ export default config;
@@ -1,5 +1,5 @@
1
1
  import { VAR_PREFIX } from "../core/namespace.js";
2
- import { font as fontScale, radius as radiusScale, size as sizeScale, space as spaceScale, text as textScale, weight as weightScale, } from "./scale.js";
2
+ import { font as fontScale, radius as radiusScale, size as sizeScale, space as spaceScale, text as textScale, tracking as trackingScale, weight as weightScale, } from "./scale.js";
3
3
  import { semantic } from "./semantic.js";
4
4
  const kebab = (s) => s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
5
5
  const toVar = (key) => `var(--${VAR_PREFIX}${kebab(key)})`;
@@ -16,4 +16,5 @@ export const contract = Object.assign(colors, {
16
16
  font: mapScale("font", fontScale),
17
17
  text: mapScale("text", textScale),
18
18
  weight: mapScale("weight", weightScale),
19
+ tracking: mapScale("tracking", trackingScale),
19
20
  });
@@ -12,9 +12,11 @@ import { contract as c } from "./contract.js";
12
12
  */
13
13
  export const elevation = {
14
14
  /**
15
- * Tier 1 — anything floating and *attached*: menus, popovers, dropdowns,
15
+ * Tier 1 — anything floating and *attached*: popovers, dropdowns,
16
16
  * hover-preview cards, the floating sidebar, the record peek, drawers, the
17
17
  * bulk bar. Pairs with a 10–12px radius and, for panels, an 8px gutter.
18
+ * NOT `Menu` (your-moon/crisp#934) -- that moved to
19
+ * `selectFloating`, seed-design's own flat shadow, below.
18
20
  */
19
21
  floating: `inset 0 0 0 1px ${c.ringPopover}, 0 0 0 1px ${c.shadowPopover1}, 0 4px 8px -4px ${c.shadowPopover2}, 0 4px 12px -2px ${c.shadowPopover3}`,
20
22
  /**
@@ -57,10 +59,11 @@ export const elevation = {
57
59
  */
58
60
  fieldNoRing: `0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04)`,
59
61
  /**
60
- * Select's own listbox shadow (crisp/#867) — seed-design's `$shadow.s3`, a
61
- * single layer, NOT `floating` above. This component follows seed, not
62
- * Attio, for the same reason `DatePicker` follows seed's calendar
63
- * structure: an explicit owner request to match seed-design exactly.
62
+ * Seed-design's own `$shadow.s3`, a single flat layer, NOT `floating`
63
+ * above. Originally Select's own listbox shadow (crisp/#867); also `Menu`'s
64
+ * (your-moon/crisp#934) -- the raw seed dump confirms Menu
65
+ * is the one component seed's own spec gives a shadow to at all, and it's
66
+ * this exact value. Despite the name, not exclusive to Select.
64
67
  */
65
68
  selectFloating: `0 4px 16px ${c.shadowSelect}`,
66
69
  };
@@ -63,14 +63,34 @@ export const palette = {
63
63
  ringBrandSoft: { light: "#d6e5ff", dark: "#212224" },
64
64
  tileActive: { light: "#ffffff", dark: "#101112" },
65
65
  tileActiveShadow: { light: "rgba(0,0,0,0.04)", dark: "rgba(0,0,0,0)" },
66
- // Dialog's page-dimming backdrop (crisp#677) — measured live behind the
67
- // Create-attribute FormDialog, both themes: `rgba(0,0,0,.25)` light,
68
- // `rgba(0,0,0,.55)` dark. `Dialog`'s own backdrop had shipped fully
69
- // `transparent` (AUTHORING's Lesson 14 measured the command palette has
70
- // NO dark backdrop and that got generalized to every `Dialog`, which this
71
- // corrects — the palette is a different surface from a plain content
72
- // dialog and really does dim the page).
73
- bgScrim: { light: "rgba(0,0,0,0.25)", dark: "rgba(0,0,0,0.55)" },
66
+ // Dialog's page-dimming backdrop (crisp#677, Attio's own measured value:
67
+ // `rgba(0,0,0,.25)` light, `rgba(0,0,0,.55)` dark — `Dialog`'s own backdrop
68
+ // had shipped fully `transparent`, AUTHORING's Lesson 14 measured the
69
+ // command palette has NO dark backdrop and that got generalized to every
70
+ // `Dialog`, which this corrected).
71
+ //
72
+ // your-moon/crisp#934: light theme moved to seed-design's own scrim
73
+ // (`#00000074` ≈ 45% black, `bg.overlay`) — the owner's flat-dialog
74
+ // directive names "seed's own scrim" specifically, now that elevation is
75
+ // conveyed by the scrim alone (no more frosted-glass shadow/blur). Dark
76
+ // theme's `.55` is UNCHANGED: seed's own token system has no separate
77
+ // dark-theme scrim value to adopt, and `.55` wasn't found wrong -- only
78
+ // light had a sourced replacement value to move to.
79
+ bgScrim: { light: "rgba(0,0,0,0.45)", dark: "rgba(0,0,0,0.55)" },
80
+ // seed-design's `$color.bg.overlay` (BottomSheet/MenuSheet's own backdrop,
81
+ // crisp#911) — kept as its OWN token rather than folded into `bgScrim`
82
+ // above, even though #934 (landed after this one was added) moved
83
+ // `bgScrim`'s LIGHT value to the same `#00000074`: `bgOverlayStrong` is
84
+ // seed's literally theme-CONSTANT value (`color.yaml` gives the same hex
85
+ // for both `theme-light` and `theme-dark`), while `bgScrim` still differs
86
+ // by theme (`.45`/`.55`) -- the two tokens express different design
87
+ // decisions that currently coincide in one theme only.
88
+ bgOverlayStrong: { light: "#00000074", dark: "#00000074" },
89
+ // seed-design's `$color.stroke.neutral-muted` (MenuSheet group divider,
90
+ // crisp#911) -- a faint alpha hairline between grouped rows, theme-flipping
91
+ // (`color.yaml`: `static-black-alpha-300` light / `static-white-alpha-100`
92
+ // dark).
93
+ strokeGroupDivider: { light: "#00000010", dark: "#ffffff17" },
74
94
  // The discard-confirm's description line (crisp#677, `Dialog`'s new
75
95
  // `variant="alert"`) — measured live, both themes, over `popoverBg`: a
76
96
  // theme-relative alpha rather than a flat gray, so it stays legible if
@@ -27,13 +27,22 @@ export const space = {
27
27
  // (inputs, rows, tiles); full is the pill.
28
28
  export const radius = {
29
29
  xs: "4px", // seed-design's $radius.r1 — small badge / checkbox box corner
30
- chip: "7px", // tag / chip corner — the single most used radius
30
+ // your-moon/crisp#919: was `7px` (Attio's own most-measured
31
+ // radius, 70 occurrences) -- now a pill, matching seed-design's own
32
+ // `chip.yaml` (`$radius.full` at every size, no size-based radius step at
33
+ // all, unlike Button/Badge). Same numeric value as `full` below, kept as
34
+ // its own named token because every one of this token's ~9 consumers
35
+ // (Chip, DataTable/RecordPage/RecordsTable/MultiSelect's own tag/mention
36
+ // rules, TableToolbar/DatePickerField's date-preset chips) reasons about
37
+ // "the chip corner" as its own concept, not literally "a generic pill".
38
+ chip: "999px",
31
39
  sm: "6px", // chips, badges
32
40
  md: "8px", // inputs, search box
33
41
  lg: "9px", // rows, tiles, list items
34
42
  xl: "12px", // overlay items (command rows)
35
43
  "2xl": "16px", // overlay panels (command surface)
36
44
  "3xl": "20px", // seed-design's $radius.r5 — Select's listbox corner (crisp/#867)
45
+ "4xl": "24px", // seed-design's $radius.r6 — BottomSheet's top corner radius (crisp/#911)
37
46
  full: "999px", // pills
38
47
  };
39
48
  // Component sizing — recurring measured dimensions that are neither spacing nor
@@ -85,3 +94,36 @@ export const weight = {
85
94
  semibold: "600",
86
95
  bold: "700", // seed-design's font-weight-bold, for the seed-styled calendar
87
96
  };
97
+ // Size-based letter-spacing ramp for DISPLAY/HEADING type (18px and up —
98
+ // `text.*` above tops out at 16px and stays untracked). crisp's five
99
+ // already-shipped headings tighten as they grow — authshell 20px/-0.4px (2%
100
+ // of size), objectsettings 24px/-0.48px (2%), richtext 32px/-0.6px (1.875%)
101
+ // — all Attio-measured (Inter) and left exactly as they are; this token
102
+ // never touches them, or the button (-0.07px) / table-cell (-0.14px)
103
+ // tracking, which are body sizes, not display type. (statcard's 28px/-0.4px,
104
+ // 1.43%, is a looser outlier — it sets tabular numerals, not prose — and is
105
+ // also left alone.)
106
+ //
107
+ // crisp#909: the study at docs/study/korean/ observed that Naver and Kakao
108
+ // both tighten tracking as type gets bigger, and crisp's OWN large type
109
+ // (EmptyState's 20px title, Dialog's 18px alert title, and any future
110
+ // heading) had no tracking at all — it read as default, not set. The study
111
+ // supplied the PATTERN only, never a number: Naver/Kakao's own values are
112
+ // Korean-specific and were not copied. The numbers below come from rendering
113
+ // real Mongolian Cyrillic ("Эрсдэлийн бүртгэл", "Хяналтын самбар") in VK Sans
114
+ // Display — crisp's own font override, see `font.sans` above — at every
115
+ // display size crisp uses today (18/20/24/26/28/32px), screenshotting six
116
+ // candidates per size (0%, 1%, 1.5%, 2%, 2.5%, 3% of the font-size) and
117
+ // checking for glyph collision. None collided even at 3%, so the pre-existing
118
+ // 2%-of-size rule the five Attio headings already happen to follow was kept
119
+ // as the ramp, rounded to the nearest 0.01px — it reads as deliberately set
120
+ // with headroom to spare, and keeps new display type visually consistent
121
+ // with the Attio-measured headings already shipped.
122
+ export const tracking = {
123
+ "18": "-0.36px",
124
+ "20": "-0.4px",
125
+ "24": "-0.48px",
126
+ "26": "-0.52px",
127
+ "28": "-0.56px",
128
+ "32": "-0.64px",
129
+ };
@@ -38,6 +38,8 @@ export const semantic = {
38
38
  bgTileActive: "tileActive",
39
39
  shadowTileActive: "tileActiveShadow",
40
40
  bgScrim: "bgScrim",
41
+ bgOverlayStrong: "bgOverlayStrong",
42
+ strokeGroupDivider: "strokeGroupDivider",
41
43
  fgAlertDescription: "fgAlertDescription",
42
44
  bgOverlayFrost: "overlayFrost",
43
45
  shadowOverlay1: "shadowOverlay1",
@@ -25,7 +25,9 @@
25
25
  & .crisp-fileupload-remove:hover { color: var(--crisp-fg-primary); }
26
26
  & .crisp-fileupload-remove:focus-visible { outline-color: var(--crisp-stroke-focus-ring); }
27
27
  & .crisp-fileupload-errors { display: flex; flex-direction: column; gap: var(--crisp-space-0-5); margin: 0; padding: 0; list-style: none; }
28
- /* out of flow while empty, so the always-mounted live region costs no layout: display:none would take it out of the accessibility tree, which is the one thing it exists to be in */
28
+ /* out of flow while empty, so the always-mounted live region costs no
29
+ layout: display:none would take it out of the accessibility tree, which
30
+ is the one thing it exists to be in */
29
31
  & .crisp-fileupload-errors:empty { position: absolute; }
30
32
  & .crisp-fileupload-error { color: var(--crisp-bg-danger); font-size: var(--crisp-text-md); line-height: 18px; }
31
33
  }