@crisp-ui-kit/crisp 0.50.0 → 0.52.0

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 (128) hide show
  1. package/dist/cjs/components/composewindow/composewindow.js +144 -0
  2. package/dist/cjs/components/composewindow/composewindow.recipe.js +45 -0
  3. package/dist/cjs/components/composewindow/index.js +8 -0
  4. package/dist/cjs/components/datatable/datatable.js +224 -43
  5. package/dist/cjs/components/datatable/datatable.recipe.js +37 -3
  6. package/dist/cjs/components/datatable/index.js +6 -0
  7. package/dist/cjs/components/datepickerfield/datepickerfield.js +25 -15
  8. package/dist/cjs/components/datepickerfield/datepickerfield.recipe.js +25 -8
  9. package/dist/cjs/components/dialog/dialog.recipe.js +9 -1
  10. package/dist/cjs/components/menu/menu.js +4 -2
  11. package/dist/cjs/components/multiselect/multiselect.recipe.js +14 -8
  12. package/dist/cjs/components/select/select.js +2 -1
  13. package/dist/cjs/components/table/table.recipe.js +12 -3
  14. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +1 -1
  15. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +18 -7
  16. package/dist/cjs/core/caret-rect.js +73 -0
  17. package/dist/cjs/core/cell-date.js +11 -0
  18. package/dist/cjs/core/index.js +8 -1
  19. package/dist/cjs/core/mention.js +32 -0
  20. package/dist/cjs/core/use-mention-trigger.js +124 -0
  21. package/dist/cjs/index.js +1 -0
  22. package/dist/cjs/product/recordpage/recordpage.js +88 -10
  23. package/dist/cjs/product/recordpage/recordpage.recipe.js +30 -0
  24. package/dist/cjs/product/recordpanel/recordpanel.js +11 -29
  25. package/dist/cjs/product/recordpanel/recordpanel.recipe.js +55 -17
  26. package/dist/cjs/product/recordstable/editable-field.js +113 -0
  27. package/dist/cjs/product/recordstable/index.js +4 -1
  28. package/dist/cjs/product/recordstable/recordstable.js +67 -11
  29. package/dist/cjs/product/recordstable/recordstable.recipe.js +9 -1
  30. package/dist/cjs/product/recordstable/recordstable.tag.js +8 -3
  31. package/dist/cjs/product/richtext/index.js +2 -1
  32. package/dist/cjs/product/richtext/richtext.js +440 -23
  33. package/dist/cjs/product/richtext/richtext.recipe.js +59 -9
  34. package/dist/cjs/product/settingsshell/settingsshell.recipe.js +13 -4
  35. package/dist/cjs/product/sidebar/sidebar.js +15 -3
  36. package/dist/cjs/product/sidebar/sidebar.recipe.js +21 -2
  37. package/dist/composewindow.css +24 -0
  38. package/dist/composewindow.d.ts +7 -0
  39. package/dist/composewindow.layered.css +26 -0
  40. package/dist/datatable.css +33 -14
  41. package/dist/datatable.layered.css +33 -14
  42. package/dist/datepickerfield.css +9 -4
  43. package/dist/datepickerfield.layered.css +9 -4
  44. package/dist/dialog.css +2 -1
  45. package/dist/dialog.layered.css +2 -1
  46. package/dist/esm/components/composewindow/composewindow.js +107 -0
  47. package/dist/esm/components/composewindow/composewindow.recipe.js +42 -0
  48. package/dist/esm/components/composewindow/index.js +2 -0
  49. package/dist/esm/components/datatable/datatable.js +226 -45
  50. package/dist/esm/components/datatable/datatable.recipe.js +37 -3
  51. package/dist/esm/components/datatable/index.js +6 -0
  52. package/dist/esm/components/datepickerfield/datepickerfield.js +26 -16
  53. package/dist/esm/components/datepickerfield/datepickerfield.recipe.js +25 -8
  54. package/dist/esm/components/dialog/dialog.recipe.js +9 -1
  55. package/dist/esm/components/menu/menu.js +4 -2
  56. package/dist/esm/components/multiselect/multiselect.recipe.js +14 -8
  57. package/dist/esm/components/select/select.js +2 -1
  58. package/dist/esm/components/table/table.recipe.js +12 -3
  59. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +1 -1
  60. package/dist/esm/components/tabletoolbar/tabletoolbar.js +18 -7
  61. package/dist/esm/core/caret-rect.js +70 -0
  62. package/dist/esm/core/cell-date.js +10 -0
  63. package/dist/esm/core/index.js +4 -1
  64. package/dist/esm/core/mention.js +29 -0
  65. package/dist/esm/core/use-mention-trigger.js +88 -0
  66. package/dist/esm/index.js +1 -0
  67. package/dist/esm/product/recordpage/recordpage.js +89 -11
  68. package/dist/esm/product/recordpage/recordpage.recipe.js +30 -0
  69. package/dist/esm/product/recordpanel/index.js +1 -1
  70. package/dist/esm/product/recordpanel/recordpanel.js +13 -31
  71. package/dist/esm/product/recordpanel/recordpanel.recipe.js +56 -18
  72. package/dist/esm/product/recordstable/editable-field.js +76 -0
  73. package/dist/esm/product/recordstable/index.js +1 -0
  74. package/dist/esm/product/recordstable/recordstable.js +69 -13
  75. package/dist/esm/product/recordstable/recordstable.recipe.js +9 -1
  76. package/dist/esm/product/recordstable/recordstable.tag.js +8 -3
  77. package/dist/esm/product/richtext/index.js +1 -1
  78. package/dist/esm/product/richtext/richtext.js +439 -22
  79. package/dist/esm/product/richtext/richtext.recipe.js +60 -10
  80. package/dist/esm/product/settingsshell/settingsshell.recipe.js +13 -4
  81. package/dist/esm/product/sidebar/sidebar.js +15 -3
  82. package/dist/esm/product/sidebar/sidebar.recipe.js +21 -2
  83. package/dist/multiselect.css +8 -8
  84. package/dist/multiselect.layered.css +8 -8
  85. package/dist/recordpage.css +25 -13
  86. package/dist/recordpage.layered.css +25 -13
  87. package/dist/recordpanel.css +43 -20
  88. package/dist/recordpanel.layered.css +43 -20
  89. package/dist/recordstable.css +9 -2
  90. package/dist/recordstable.layered.css +9 -2
  91. package/dist/richtext.css +31 -5
  92. package/dist/richtext.layered.css +31 -5
  93. package/dist/settingsshell.css +2 -2
  94. package/dist/settingsshell.layered.css +2 -2
  95. package/dist/sidebar.css +7 -2
  96. package/dist/sidebar.layered.css +7 -2
  97. package/dist/styles.css +199 -74
  98. package/dist/styles.layered.css +199 -74
  99. package/dist/table.css +4 -2
  100. package/dist/table.layered.css +4 -2
  101. package/dist/tabletoolbar.css +1 -1
  102. package/dist/tabletoolbar.layered.css +1 -1
  103. package/dist/types/components/composewindow/composewindow.d.ts +80 -0
  104. package/dist/types/components/composewindow/composewindow.recipe.d.ts +3 -0
  105. package/dist/types/components/composewindow/index.d.ts +2 -0
  106. package/dist/types/components/datatable/datatable.d.ts +58 -4
  107. package/dist/types/components/datatable/index.d.ts +1 -1
  108. package/dist/types/components/datepickerfield/datepickerfield.d.ts +18 -4
  109. package/dist/types/components/menu/menu.d.ts +10 -1
  110. package/dist/types/components/select/select.d.ts +6 -1
  111. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +29 -5
  112. package/dist/types/core/caret-rect.d.ts +16 -0
  113. package/dist/types/core/cell-date.d.ts +17 -0
  114. package/dist/types/core/index.d.ts +4 -1
  115. package/dist/types/core/mention.d.ts +27 -0
  116. package/dist/types/core/use-mention-trigger.d.ts +41 -0
  117. package/dist/types/index.d.ts +1 -0
  118. package/dist/types/product/recordpage/recordpage.d.ts +59 -1
  119. package/dist/types/product/recordpanel/index.d.ts +1 -1
  120. package/dist/types/product/recordpanel/recordpanel.d.ts +81 -17
  121. package/dist/types/product/recordstable/editable-field.d.ts +48 -0
  122. package/dist/types/product/recordstable/index.d.ts +1 -0
  123. package/dist/types/product/recordstable/recordstable.d.ts +29 -0
  124. package/dist/types/product/recordstable/recordstable.tag.d.ts +8 -3
  125. package/dist/types/product/richtext/index.d.ts +1 -1
  126. package/dist/types/product/richtext/richtext.d.ts +168 -7
  127. package/dist/types/product/sidebar/sidebar.d.ts +60 -4
  128. package/package.json +6 -1
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.useMentionTrigger = useMentionTrigger;
37
+ const React = __importStar(require("react"));
38
+ const caret_rect_1 = require("./caret-rect");
39
+ const mention_1 = require("./mention");
40
+ /**
41
+ * Watches a `<textarea>`/`<input>` for an "@query" mention trigger and
42
+ * reports where to anchor a consumer-supplied picker (#372).
43
+ *
44
+ * crisp draws no picker of its own here — GitHub/Slack/Linear-style @mention
45
+ * UI varies too much by app (avatars, groups, recent-first ordering, a
46
+ * "create new person" row) to have one measured shape, and this repo's rule
47
+ * is measure-first, never invented polish. What is shared and worth owning
48
+ * centrally is the fiddly, easy-to-get-wrong part: deciding when "@" is
49
+ * actually a trigger rather than part of a word or an email address (see
50
+ * `matchMentionTrigger`), and where its picker anchors on screen (see
51
+ * `caretRectInField`, since neither element exposes caret geometry itself).
52
+ *
53
+ * Does not close on blur: a picker built from this needs to survive the
54
+ * field losing focus to a `mousedown` on one of its own rows, the same
55
+ * problem every floating surface in this repo solves for itself (see
56
+ * `RichTextEditor`'s toolbar `keepSel`, or `Popover`'s own outside-pointer-
57
+ * down listener) — so closing on outside interaction is left to the
58
+ * consumer's picker, which already knows what counts as "outside" for its
59
+ * own markup.
60
+ */
61
+ function useMentionTrigger(ref, { trigger = "@" } = {}) {
62
+ const [state, setState] = React.useState({
63
+ open: false,
64
+ query: "",
65
+ anchor: null,
66
+ });
67
+ const matchRef = React.useRef(null);
68
+ const recompute = React.useCallback(() => {
69
+ const el = ref.current;
70
+ if (!el || el.selectionStart == null) {
71
+ matchRef.current = null;
72
+ setState({ open: false, query: "", anchor: null });
73
+ return;
74
+ }
75
+ const caret = el.selectionStart;
76
+ const match = (0, mention_1.matchMentionTrigger)(el.value.slice(0, caret), trigger);
77
+ matchRef.current = match;
78
+ if (!match) {
79
+ setState({ open: false, query: "", anchor: null });
80
+ return;
81
+ }
82
+ setState({
83
+ open: true,
84
+ query: match.query,
85
+ anchor: (0, caret_rect_1.caretRectInField)(el, caret),
86
+ });
87
+ }, [ref, trigger]);
88
+ React.useEffect(() => {
89
+ const el = ref.current;
90
+ if (!el)
91
+ return;
92
+ // "input" covers typing; "click"/"keyup" cover the caret moving without
93
+ // the text changing (arrow keys, clicking elsewhere on the same line).
94
+ el.addEventListener("input", recompute);
95
+ el.addEventListener("click", recompute);
96
+ el.addEventListener("keyup", recompute);
97
+ return () => {
98
+ el.removeEventListener("input", recompute);
99
+ el.removeEventListener("click", recompute);
100
+ el.removeEventListener("keyup", recompute);
101
+ };
102
+ }, [ref, recompute]);
103
+ const close = React.useCallback(() => {
104
+ matchRef.current = null;
105
+ setState({ open: false, query: "", anchor: null });
106
+ }, []);
107
+ const insert = React.useCallback((text) => {
108
+ const el = ref.current;
109
+ const match = matchRef.current;
110
+ if (!el || !match)
111
+ return;
112
+ const caret = el.selectionStart ?? el.value.length;
113
+ const before = el.value.slice(0, match.start);
114
+ const after = el.value.slice(caret);
115
+ el.value = before + text + after;
116
+ const cursor = before.length + text.length;
117
+ el.focus();
118
+ el.setSelectionRange(cursor, cursor);
119
+ // a controlled consumer listens on "input", the same event typing fires
120
+ el.dispatchEvent(new Event("input", { bubbles: true }));
121
+ close();
122
+ }, [ref, close]);
123
+ return { ...state, close, insert };
124
+ }
package/dist/cjs/index.js CHANGED
@@ -31,6 +31,7 @@ __exportStar(require("./components/card"), exports);
31
31
  __exportStar(require("./components/checkbox"), exports);
32
32
  __exportStar(require("./components/chip"), exports);
33
33
  __exportStar(require("./components/columncalc"), exports);
34
+ __exportStar(require("./components/composewindow"), exports);
34
35
  __exportStar(require("./components/contextmenu"), exports);
35
36
  __exportStar(require("./components/datatable"), exports);
36
37
  __exportStar(require("./components/datepicker"), exports);
@@ -41,6 +41,9 @@ const React = __importStar(require("react"));
41
41
  const avatar_1 = require("../../components/avatar");
42
42
  const checkbox_1 = require("../../components/checkbox");
43
43
  const menu_1 = require("../../components/menu");
44
+ const select_1 = require("../../components/select");
45
+ const spinner_1 = require("../../components/spinner");
46
+ const switch_1 = require("../../components/switch");
44
47
  const tabs_1 = require("../../components/tabs");
45
48
  const core_1 = require("../../core");
46
49
  const recordstable_1 = require("../recordstable");
@@ -190,7 +193,8 @@ function EmptyCard({ icon, children, }) {
190
193
  * Emails sections (Notes & Tasks are live — add and check things off). Drop it
191
194
  * into the app's main content area.
192
195
  */
193
- function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, className, ...rest }) {
196
+ function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, ...rest }) {
197
+ const { editingKey, pendingKey, error, openEditing, closeEditing, commit } = (0, recordstable_1.useEditableField)(onEdit);
194
198
  const t = React.useMemo(() => ({
195
199
  ...exports.recordPageLabels,
196
200
  ...labels,
@@ -205,8 +209,15 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
205
209
  },
206
210
  }), [labels]);
207
211
  const [favorite, setFavorite] = React.useState(false);
208
- const [notes, setNotes] = React.useState([]);
209
- const [tasks, setTasks] = React.useState([]);
212
+ // Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
213
+ // `[]`) -- otherwise RecordPage keeps its own self-contained state, as
214
+ // before (#379). "Controlled" here means the array prop alone decides the
215
+ // mode; there's no meaningful "default seed" worth a separate
216
+ // defaultNotes/defaultTasks prop the way Tabs' defaultValue is.
217
+ const [internalNotes, setInternalNotes] = React.useState([]);
218
+ const [internalTasks, setInternalTasks] = React.useState([]);
219
+ const notes = notesProp ?? internalNotes;
220
+ const tasks = tasksProp ?? internalTasks;
210
221
  const [showAllActivity, setShowAllActivity] = React.useState(false);
211
222
  // Collapsible + searchable Record Details.
212
223
  const [detailsOpen, setDetailsOpen] = React.useState(true);
@@ -316,7 +327,7 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
316
327
  },
317
328
  ];
318
329
  const ti = { size: 15, strokeWidth: 1.75 };
319
- const tabs = [
330
+ const builtinTabs = [
320
331
  { value: "overview", label: t.tabs.overview, icon: (0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { ...ti }) },
321
332
  {
322
333
  value: "activity",
@@ -346,9 +357,27 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
346
357
  badge: 0,
347
358
  },
348
359
  ];
349
- const addNote = (text) => setNotes((n) => [{ id: nextId(), text, time: t.justNow }, ...n]);
350
- const addTask = (text) => setTasks((t) => [{ id: nextId(), text, done: false }, ...t]);
351
- const toggleTask = (id) => setTasks((t) => t.map((x) => (x.id === id ? { ...x, done: !x.done } : x)));
360
+ const addNote = (text) => {
361
+ if (notesProp) {
362
+ onAddNote?.(text);
363
+ return;
364
+ }
365
+ setInternalNotes((n) => [{ id: nextId(), text, time: t.justNow }, ...n]);
366
+ };
367
+ const addTask = (text) => {
368
+ if (tasksProp) {
369
+ onAddTask?.(text);
370
+ return;
371
+ }
372
+ setInternalTasks((n) => [{ id: nextId(), text, done: false }, ...n]);
373
+ };
374
+ const toggleTask = (id) => {
375
+ if (tasksProp) {
376
+ onToggleTask?.(id);
377
+ return;
378
+ }
379
+ setInternalTasks((n) => n.map((x) => (x.id === id ? { ...x, done: !x.done } : x)));
380
+ };
352
381
  // A small, plausible activity feed derived from the record's own fields.
353
382
  const domain = byKey.get("domain");
354
383
  const cats = byKey.get("categories");
@@ -376,7 +405,7 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
376
405
  const shownActivity = showAllActivity ? activity : activity.slice(0, 2);
377
406
  const activityFeed = ((0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.activity, action: activity.length > 2 ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-viewall crisp-recordpage-viewall--inline", onClick: () => setShowAllActivity((s) => !s), children: showAllActivity ? t.showLess : t.viewAll })) : undefined }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-feed", children: shownActivity.map((a) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-feed-item", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-feed-dot", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Activity, { size: 13, strokeWidth: 2 }) }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-feed-body", children: [a.text, (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-feed-time", children: a.time })] })] }, a.id))) })] }));
378
407
  const emailsEmpty = ((0, jsx_runtime_1.jsx)(EmptyCard, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Inbox, { size: 22, strokeWidth: 1.5 }), children: t.noEmailsWith(name) }));
379
- const panel = (active) => {
408
+ const builtinPanel = (active) => {
380
409
  switch (active) {
381
410
  case "overview":
382
411
  return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-panel", children: [(0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-section-title", children: t.highlights }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-card", children: (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-grid", children: highlights.map((h) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-hl-cell", children: [(0, jsx_runtime_1.jsx)(h.icon, { className: "crisp-recordpage-hl-icon", size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-label", children: h.label }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-value", children: h.value ?? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-hl-value--empty", children: h.empty })) })] }, h.label))) }) })] }), activityFeed, (0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.emails, count: 0 }), emailsEmpty] }), (0, jsx_runtime_1.jsx)(NotesSection, { notes: notes, onAdd: addNote, labels: t }), (0, jsx_runtime_1.jsx)(TasksSection, { tasks: tasks, onAdd: addTask, onToggle: toggleTask, labels: t })] }));
@@ -392,6 +421,19 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
392
421
  return ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-panel", children: (0, jsx_runtime_1.jsx)(EmptyCard, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Folder, { size: 22, strokeWidth: 1.5 }), children: t.nothingHereYet }) }));
393
422
  }
394
423
  };
424
+ // #379: `tabs` (consumer-supplied) replaces the CRM set wholesale, rather
425
+ // than merging with it — a mixed set would leave a caller unable to ever
426
+ // drop a built-in tab it doesn't want. `count` maps to Tabs' own `badge`.
427
+ const effectiveTabs = tabsProp
428
+ ? tabsProp.map((tb) => ({
429
+ value: tb.id,
430
+ label: tb.label,
431
+ badge: tb.count,
432
+ }))
433
+ : builtinTabs;
434
+ const effectivePanel = tabsProp
435
+ ? (active) => tabsProp.find((tb) => tb.id === active)?.render()
436
+ : builtinPanel;
395
437
  return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpage_recipe_1.recordpageRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-topbar", children: [(0, jsx_runtime_1.jsxs)("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-name", children: name })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-actions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Minimize2, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Maximize2, { size: 15, strokeWidth: 1.75 })) })), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.recordActions, trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), (0, jsx_runtime_1.jsx)(menu_1.MenuSeparator, {}), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-body", children: [(0, jsx_runtime_1.jsxs)("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-entity-name", children: name }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Pencil, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.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: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-quickactions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-compose", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.copyRecordLink, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.logANote, children: (0, jsx_runtime_1.jsx)(lucide_react_1.FileText, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.linkRecords, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Share2, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.createATask, children: (0, jsx_runtime_1.jsx)(lucide_react_1.SquareCheck, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-section", children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-detailhead", children: [searchOpen ? ((0, jsx_runtime_1.jsx)("input", {
396
438
  // biome-ignore lint/a11y/noAutofocus: focusing the just-opened field search is expected
397
439
  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) => {
@@ -405,7 +447,43 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
405
447
  if (!searchOpen)
406
448
  setDetailsOpen(true);
407
449
  }, children: searchOpen ? ((0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 14, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 14, strokeWidth: 1.75 })) })] }), detailsOpen &&
408
- (shownFields.length > 0 ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [shownFields.map((col) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-label", children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : null, (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-field-label-text", children: col.label })] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-field-value", children: (0, jsx_runtime_1.jsx)(FieldValue, { col: col, row: record, labels: t }) })] }, col.key))), canRevealFields && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-viewall crisp-recordpage-viewall--section", onClick: () => {
450
+ (shownFields.length > 0 ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [shownFields.map((col) => {
451
+ // #431: a field only actually opens when there's an
452
+ // `onEdit` to commit through -- `editable` with nothing
453
+ // to call would be a dead click.
454
+ const canEdit = Boolean(col.editable && onEdit);
455
+ const isPending = pendingKey === col.key;
456
+ const fieldError = error?.key === col.key ? error.message : null;
457
+ const label = ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-label", children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : null, (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-field-label-text", children: col.label })] }));
458
+ // select/switch are always their own live widget -- no
459
+ // click-to-open, no separate edit/view mode, the same
460
+ // convention DataTable's identical `editor` columns use
461
+ // (#385): choosing a value or flipping a toggle IS the
462
+ // commit gesture.
463
+ if (canEdit && col.editor) {
464
+ const value = record[col.key];
465
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-edit-picker", children: [col.editor === "select" ? ((0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", "aria-label": `Edit ${col.label}`, value: value != null ? String(value) : undefined, disabled: isPending, options: col.editOptions ?? [], onValueChange: (v) => commit(record.id, col.key, v) })) : ((0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": `Edit ${col.label}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commit(record.id, col.key, String(checked)) })), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-recordpage-edit-pending" }))] }), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
466
+ }
467
+ if (canEdit && editingKey === col.key) {
468
+ const value = record[col.key];
469
+ const editor = (0, recordstable_1.editableInputType)(col.type);
470
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-recordpage-edit",
471
+ // biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
472
+ autoFocus: true, ...editor, disabled: isPending, defaultValue: value == null ? "" : String(value), "aria-label": `Edit ${col.label}`, onKeyDown: (e) => {
473
+ if (isPending)
474
+ return;
475
+ if (e.key === "Enter")
476
+ commit(record.id, col.key, e.currentTarget.value);
477
+ else if (e.key === "Escape")
478
+ closeEditing();
479
+ }, onBlur: (e) => {
480
+ if (isPending)
481
+ return;
482
+ commit(record.id, col.key, e.currentTarget.value);
483
+ } }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-recordpage-edit-pending" })), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
484
+ }
485
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", "data-editable": canEdit ? "" : undefined, children: [canEdit ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-edit-trigger", onClick: () => openEditing(col.key), children: (0, jsx_runtime_1.jsx)(FieldValue, { col: col, row: record, labels: t }) })) : ((0, jsx_runtime_1.jsx)(FieldValue, { col: col, row: record, labels: t })), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
486
+ }), canRevealFields && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-viewall crisp-recordpage-viewall--section", onClick: () => {
409
487
  // Revealing all fields also opens the (focused)
410
488
  // field search; collapsing closes it.
411
489
  const next = !showAllValues;
@@ -423,6 +501,6 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
423
501
  const rtl = (0, core_1.isRtlElement)(e.currentTarget);
424
502
  const grow = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
425
503
  setDetailsW((w) => grow ? Math.min(560, w + 16) : Math.max(300, w - 16));
426
- } }), (0, jsx_runtime_1.jsx)("main", { className: "crisp-recordpage-main", children: (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { tabs: tabs, defaultValue: "overview", "aria-label": `${name} record`, children: panel }) })] })] }));
504
+ } }), (0, jsx_runtime_1.jsx)("main", { className: "crisp-recordpage-main", children: (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] })] }));
427
505
  }
428
506
  RecordPage.displayName = "RecordPage";
@@ -79,7 +79,37 @@ const config = (0, core_1.defineRecipe)({
79
79
  & .crisp-recordpage-field-label svg { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
80
80
  & .crisp-recordpage-field-label-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
81
81
  & .crisp-recordpage-field-value { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: ${tokens_1.contract.space["1"]}; padding-top: 2px; line-height: 20px; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary}; }
82
+ & .crisp-recordpage-field-value[data-editable] { cursor: pointer; }
83
+ /* the click-to-edit trigger is a real <button> (keyboard-operable),
84
+ reset back to plain inline text and made a flex-wrap container of its
85
+ own -- FieldValue can render several chips (tags), which still need
86
+ to wrap the way they did before this had a button around them at
87
+ all. It wraps only FieldValue, never the error slot beside it (see
88
+ the component's own comment) -- NOT \`display: contents\`, which
89
+ drops a button's accessible semantics in some engines. */
90
+ & .crisp-recordpage-edit-trigger { display: inline-flex; flex-wrap: wrap; align-items: center; gap: ${tokens_1.contract.space["1"]}; margin: 0; padding: 0; border: none; background: none; text-align: start; font: inherit; color: inherit; cursor: pointer; }
82
91
  & .crisp-recordpage-empty { color: ${tokens_1.contract.fgMuted}; }
92
+
93
+ /* #431: measured live on app.attio.com's record page Details column --
94
+ clicking a field's value opens a plain, borderless field exactly
95
+ filling the value's own box (no border, no background, no
96
+ box-shadow), same font as the value it replaces (hence \`font:
97
+ inherit\`, not a restated size/weight). A field row's own hover tint
98
+ above already carries the "this is interactive" affordance; the
99
+ pending/error treatment isn't Attio-measured -- it reuses the same
100
+ tokens DataTable's identical states already use (opacity 0.5,
101
+ \`bgDanger\`), per #385/#430. */
102
+ & .crisp-recordpage-edit { display: block; width: 100%; margin: 0; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.sm}; background: none; box-shadow: none; outline: none; font: inherit; color: inherit; }
103
+ /* not Attio-measured (its own field shows no focus ring either, caret
104
+ only) -- kept for real keyboard users, the same brand ring every
105
+ other crisp field already shows on focus (DataTable's identical
106
+ .crisp-datatable-edit:focus, e.g.). */
107
+ & .crisp-recordpage-edit:focus { box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; }
108
+ & .crisp-recordpage-edit:disabled { opacity: 0.5; cursor: not-allowed; }
109
+ & .crisp-recordpage-edit-pending { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
110
+ & .crisp-recordpage-edit-error { flex-basis: 100%; margin-top: ${tokens_1.contract.space["0.5"]}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.bgDanger}; }
111
+ & .crisp-recordpage-edit-picker { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; width: 100%; }
112
+ & .crisp-recordpage-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; }
83
113
  & .crisp-recordpage-link { color: ${tokens_1.contract.fgPrimary}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
84
114
  & .crisp-recordpage-link:hover { text-decoration: underline; }
85
115
  & .crisp-recordpage-emptystate { padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["1"]}; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }
@@ -39,31 +39,18 @@ const lucide_react_1 = require("lucide-react");
39
39
  const React = __importStar(require("react"));
40
40
  const avatar_1 = require("../../components/avatar");
41
41
  const core_1 = require("../../core");
42
- const recordstable_1 = require("../recordstable");
43
42
  const recordpanel_recipe_1 = require("./recordpanel.recipe");
44
- function FieldValue({ col, row, emptyLabel, }) {
45
- const v = row[col.key];
46
- if (v == null || v === "" || (Array.isArray(v) && !v.length)) {
47
- return (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-empty", children: emptyLabel });
48
- }
49
- switch (col.type) {
50
- case "tags":
51
- return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: v.map((t) => ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-tag", "data-tone": (0, recordstable_1.tagTone)(t), children: t }, t))) }));
52
- case "number":
53
- return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: typeof v === "number" ? v.toLocaleString() : v });
54
- case "url":
55
- return ((0, jsx_runtime_1.jsx)("a", { className: "crisp-recordpanel-link", href: `https://${v}`, target: "_blank", rel: "noreferrer", children: v }));
56
- default:
57
- return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: String(v) });
58
- }
59
- }
60
43
  /**
61
- * Right-side record peek panel — shows one record's fields, opened from a
62
- * RecordsTable row (via `onOpenRecord`). Render it inside a `position:
63
- * relative` container (e.g. the app main area); it slides in from the right and
64
- * overlays the content.
44
+ * Right-side record peek panel — Attio's own peek shows a Highlights card
45
+ * area and an Activity feed (measured live: hover a Companies row, click its
46
+ * "Open preview" trigger), never an editable field list (#448 — a "Details"
47
+ * list was this component's own invention before, and #431's editable API
48
+ * had nothing real in this panel to attach to; that API stays on
49
+ * `RecordPage`'s own Details column, where Attio does let you edit in
50
+ * place). Render it inside a `position: relative` container (e.g. the app
51
+ * main area); it slides in from the right and overlays the content.
65
52
  */
66
- function RecordPanel({ record, columns, onClose, className, closeLabel = "Close panel", title, detailsLabel = "Details", emptyLabel = "Empty", footer, ...rest }) {
53
+ function RecordPanel({ record, onClose, className, closeLabel = "Close panel", onOpenRecord, openRecordLabel = "Open record", onPrevious, previousRecordLabel = "Previous record", onNext, nextRecordLabel = "Next record", title, avatar, avatarTone = "brand", highlightsLabel = "Highlights", highlights, activityLabel = "Activity", viewAllActivityLabel = "View all", onViewAllActivity, activity, footer, ...rest }) {
67
54
  const titleId = React.useId();
68
55
  // close on Escape while open -- but not when a Menu/Select/Popover/
69
56
  // ContextMenu opened from inside the panel (e.g. a footer "More actions"
@@ -98,12 +85,7 @@ function RecordPanel({ record, columns, onClose, className, closeLabel = "Close
98
85
  }, [record, onClose]);
99
86
  if (!record)
100
87
  return null;
101
- const entityCol = columns.find((c) => c.type === "entity");
102
- const entity = entityCol
103
- ? record[entityCol.key]
104
- : undefined;
105
- const fields = columns.filter((c) => c.type !== "entity");
106
- const heading = title ?? entity?.name ?? "Record";
107
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpanel_recipe_1.recordpanelRecipe)({ className }), ...rest, role: "dialog", "aria-labelledby": titleId, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-header", children: [entity?.avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpanel-fav", src: entity.avatar, alt: "" })) : entity ? ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "md", shape: "rounded", tone: "brand", children: entity.name.charAt(0) })) : null, (0, jsx_runtime_1.jsx)("span", { id: titleId, className: "crisp-recordpanel-title", children: heading }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-iconbtn", "aria-label": closeLabel, onClick: onClose, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 18, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-body", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-sectionlabel", children: detailsLabel }), fields.map((col) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-field", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-field-label", children: [col.icon ? (0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 }) : null, col.label] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-field-value", children: (0, jsx_runtime_1.jsx)(FieldValue, { col: col, row: record, emptyLabel: emptyLabel }) })] }, col.key)))] }), footer != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-footer", children: footer }))] }));
88
+ const heading = title ?? record.name ?? "Record";
89
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpanel_recipe_1.recordpanelRecipe)({ className }), ...rest, role: "dialog", "aria-labelledby": titleId, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-toolbar", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-iconbtn", "aria-label": closeLabel, onClick: onClose, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 16, strokeWidth: 1.75 }) }), (onPrevious || onNext) && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-pager", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-navbtn", "aria-label": previousRecordLabel, disabled: !onPrevious, onClick: onPrevious, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronUp, { size: 16, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-navbtn", "aria-label": nextRecordLabel, disabled: !onNext, onClick: onNext, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 16, strokeWidth: 1.75 }) })] })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-toolbar-spacer" }), onOpenRecord && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-navbtn", "aria-label": openRecordLabel, onClick: onOpenRecord, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpRight, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75 }) }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-header", children: [avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpanel-fav", src: avatar, alt: "" })) : ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "md", shape: "rounded", tone: avatarTone, children: (record.name ?? "?").charAt(0) })), (0, jsx_runtime_1.jsx)("span", { id: titleId, className: "crisp-recordpanel-title", children: heading })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-body", children: [highlights && highlights.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-section", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-section-head", children: (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-section-label", children: highlightsLabel }) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-highlights", children: highlights.map((h) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-highlight", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-highlight-label", children: [(0, jsx_runtime_1.jsx)("span", { children: h.label }), h.icon ? (0, jsx_runtime_1.jsx)(h.icon, { size: 16, strokeWidth: 1.75 }) : null] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-highlight-value", children: h.value })] }, h.key))) })] })), activity != null && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-section", children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-section-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-section-label", children: activityLabel }), onViewAllActivity && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-section-action", onClick: onViewAllActivity, children: viewAllActivityLabel }))] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-activity", children: activity })] }))] }), footer != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-footer", children: footer }))] }));
108
90
  }
109
91
  RecordPanel.displayName = "RecordPanel";
@@ -3,10 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.recordpanelRecipe = void 0;
4
4
  const core_1 = require("../../core");
5
5
  const tokens_1 = require("../../tokens");
6
- const recordstable_tag_1 = require("../recordstable/recordstable.tag");
7
6
  // A right-side record peek panel — slides in over the main content to show one
8
- // record's fields, the way a row peek panel opens. Positioned absolutely inside a
9
- // relative container (the app main area); slides in via @starting-style.
7
+ // record's Highlights and Activity, the way Attio's own peek does. Positioned
8
+ // absolutely inside a relative container (the app main area); slides in via
9
+ // @starting-style.
10
10
  const config = (0, core_1.defineRecipe)({
11
11
  name: "recordpanel",
12
12
  base: `
@@ -21,24 +21,60 @@ const config = (0, core_1.defineRecipe)({
21
21
  @starting-style { transform: translateX(calc(100% + ${tokens_1.contract.space["2"]})); }
22
22
  & * { box-sizing: border-box; }
23
23
 
24
- & .crisp-recordpanel-header { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: 52px; flex-shrink: 0; padding: 0 ${tokens_1.contract.space["3"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; }
24
+ /* #448: measured live on app.attio.com's peek -- a toolbar row above
25
+ the identity header: Close, then Previous/Next, on the left; "Open
26
+ record" on the right. 49px tall, 8px/10px padding, a hairline border
27
+ beneath (the same borderSubtle every other panel/row edge here
28
+ already uses). Close keeps the existing transparent ghost button;
29
+ Previous/Next/Open record are a distinct field-elevation style
30
+ (white fill, e.field's own ring+shadow, solid icon) -- measured as
31
+ genuinely different chrome from Close, not a reuse. */
32
+ & .crisp-recordpanel-toolbar { display: flex; align-items: center; height: 49px; flex-shrink: 0; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["2.5"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; }
33
+ & .crisp-recordpanel-toolbar-spacer { flex: 1; }
34
+ & .crisp-recordpanel-pager { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; margin-inline-start: ${tokens_1.contract.space["3"]}; }
35
+ & .crisp-recordpanel-iconbtn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex-shrink: 0; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s; }
36
+ & .crisp-recordpanel-iconbtn:hover { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
37
+ & .crisp-recordpanel-navbtn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex-shrink: 0; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgSurface}; box-shadow: ${tokens_1.elevation.field}; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; cursor: pointer; }
38
+ & .crisp-recordpanel-navbtn:hover:not(:disabled) { background: ${tokens_1.contract.bgRaised}; }
39
+ & .crisp-recordpanel-navbtn:disabled { opacity: 0.4; cursor: not-allowed; }
40
+
41
+ & .crisp-recordpanel-header { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: 52px; flex-shrink: 0; padding: 0 ${tokens_1.contract.space["3"]}; }
25
42
  & .crisp-recordpanel-fav { width: 22px; height: ${tokens_1.contract.size.chip}; border-radius: ${tokens_1.contract.radius.sm}; flex-shrink: 0; object-fit: cover; background: ${tokens_1.contract.bgRaised}; }
26
43
  & .crisp-recordpanel-title { flex: 1; min-width: 0; font-size: ${tokens_1.contract.text.xl}; font-weight: ${tokens_1.contract.weight.semibold}; letter-spacing: -0.2px; color: ${tokens_1.contract.fgPrimary}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
27
- & .crisp-recordpanel-iconbtn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex-shrink: 0; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; color: ${tokens_1.contract.fgMuted}; font-family: inherit; cursor: pointer; transition: background-color .12s; }
28
- & .crisp-recordpanel-iconbtn:hover { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
29
44
 
30
45
  & .crisp-recordpanel-body { flex: 1; min-height: 0; overflow-y: auto; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["3"]}; }
31
- & .crisp-recordpanel-sectionlabel { font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["1"]}; }
32
- & .crisp-recordpanel-field { display: flex; align-items: flex-start; gap: ${tokens_1.contract.space["2"]}; min-height: 32px; padding: ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.md}; }
33
- & .crisp-recordpanel-field:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 3%, transparent); }
34
- & .crisp-recordpanel-field-label { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; width: 132px; flex-shrink: 0; padding-top: 3px; color: ${tokens_1.contract.fgMuted}; font-size: ${tokens_1.contract.text.md}; }
35
- & .crisp-recordpanel-field-label svg { flex-shrink: 0; }
36
- & .crisp-recordpanel-field-value { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: ${tokens_1.contract.space["1"]}; padding-top: 3px; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; }
37
- & .crisp-recordpanel-empty { color: ${tokens_1.contract.fgMuted}; }
38
-
39
- ${recordstable_tag_1.recordTagCss}
40
- & .crisp-recordpanel-link { color: ${tokens_1.contract.bgBrandSolid}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
41
- & .crisp-recordpanel-link:hover { text-decoration: underline; }
46
+
47
+ /* #448: measured live on app.attio.com's peek (hover a Companies row,
48
+ click "Open preview"). Section label reads identically for both
49
+ Highlights and Activity: 14px/500, the same 63%-of-fgPrimary mix
50
+ RecordPage's own field-label already uses (product/recordpage's
51
+ .crisp-recordpage-field-label) -- one more surface landing on the
52
+ same measured "muted section heading" colour. */
53
+ & .crisp-recordpanel-section { padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["1"]} 0; }
54
+ & .crisp-recordpanel-section-head { display: flex; align-items: center; justify-content: space-between; gap: ${tokens_1.contract.space["2"]}; height: 20px; margin-bottom: ${tokens_1.contract.space["2"]}; }
55
+ & .crisp-recordpanel-section-label { font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); }
56
+ /* "View all" -- measured 10px/400, a lighter mix than the section label
57
+ (55%, not 63%) -- its own distinct, honestly-measured value, not a
58
+ reuse of a nearby one that happened to be close. */
59
+ & .crisp-recordpanel-section-action { border: none; background: none; padding: 0; font-family: inherit; font-size: 10px; font-weight: ${tokens_1.contract.weight.regular}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 55%, transparent); cursor: pointer; }
60
+ & .crisp-recordpanel-section-action:hover { color: ${tokens_1.contract.fgPrimary}; }
61
+
62
+ /* Highlights: a 2-column grid of label+icon+value cards, no card
63
+ chrome at all (no border/background/radius/shadow -- confirmed on
64
+ the live DOM: every highlight's computed style is fully transparent).
65
+ 8px row/column gap; two rows measured "233x86" inside 473px content
66
+ width is exactly (473 - 8) / 2. */
67
+ & .crisp-recordpanel-highlights { display: grid; grid-template-columns: 1fr 1fr; gap: ${tokens_1.contract.space["2"]}; }
68
+ & .crisp-recordpanel-highlight { display: flex; flex-direction: column; gap: ${tokens_1.contract.space["1.5"]}; min-width: 0; }
69
+ & .crisp-recordpanel-highlight-label { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); }
70
+ & .crisp-recordpanel-highlight-label span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
71
+ & .crisp-recordpanel-highlight-label svg { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
72
+ & .crisp-recordpanel-highlight-value { font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary}; overflow: hidden; text-overflow: ellipsis; }
73
+
74
+ /* Activity is a consumer-supplied slot (crisp has no activity-feed
75
+ model of its own to render) -- this only wraps it with the measured
76
+ section heading above. */
77
+ & .crisp-recordpanel-activity { font-size: ${tokens_1.contract.text.lg}; }
42
78
 
43
79
  /* #329: an optional slot for record actions (e.g. Edit). Reuses the
44
80
  header's own measured values rather than inventing new ones -- the
@@ -47,6 +83,8 @@ const config = (0, core_1.defineRecipe)({
47
83
  edge), and the header/body gap (space.2) for its own vertical padding
48
84
  and for spacing between whatever it's given. */
49
85
  & .crisp-recordpanel-footer { flex-shrink: 0; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["3"]}; border-top: 1px solid ${tokens_1.contract.borderSubtle}; }
86
+
87
+ ${core_1.rtlFlipCss}
50
88
  `,
51
89
  variants: {},
52
90
  defaultVariants: {},