@crisp-ui-kit/crisp 0.53.5 → 0.54.1

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 (124) hide show
  1. package/dist/activityfeed.css +12 -0
  2. package/dist/activityfeed.d.ts +7 -0
  3. package/dist/activityfeed.layered.css +14 -0
  4. package/dist/badge.css +3 -0
  5. package/dist/badge.d.ts +2 -2
  6. package/dist/badge.layered.css +3 -0
  7. package/dist/cjs/components/badge/badge.recipe.js +10 -0
  8. package/dist/cjs/components/bulkbar/bulkbar.js +5 -1
  9. package/dist/cjs/components/fileupload/fileupload.js +19 -3
  10. package/dist/cjs/components/formfield/formfield.js +8 -1
  11. package/dist/cjs/components/heatmapchart/heatmapchart.js +14 -2
  12. package/dist/cjs/components/skiplink/index.js +17 -0
  13. package/dist/cjs/components/skiplink/skiplink.js +16 -0
  14. package/dist/cjs/components/skiplink/skiplink.recipe.js +33 -0
  15. package/dist/cjs/components/textlink/index.js +7 -0
  16. package/dist/cjs/components/textlink/textlink.js +16 -0
  17. package/dist/cjs/components/textlink/textlink.recipe.js +35 -0
  18. package/dist/cjs/components/visuallyhidden/index.js +17 -0
  19. package/dist/cjs/components/visuallyhidden/visuallyhidden.js +16 -0
  20. package/dist/cjs/components/visuallyhidden/visuallyhidden.recipe.js +20 -0
  21. package/dist/cjs/index.js +5 -0
  22. package/dist/cjs/product/activityfeed/activityfeed.js +38 -0
  23. package/dist/cjs/product/activityfeed/activityfeed.recipe.js +35 -0
  24. package/dist/cjs/product/activityfeed/index.js +10 -0
  25. package/dist/cjs/product/board/board.js +11 -2
  26. package/dist/cjs/product/command/command.js +13 -1
  27. package/dist/cjs/product/flowcanvas/flowcanvas.js +9 -2
  28. package/dist/cjs/product/headerbar/headerbar.js +15 -0
  29. package/dist/cjs/product/headerbar/headerbar.recipe.js +35 -0
  30. package/dist/cjs/product/headerbar/index.js +17 -0
  31. package/dist/cjs/product/inbox/inbox.js +13 -1
  32. package/dist/cjs/product/onboarding/onboarding.js +22 -1
  33. package/dist/cjs/product/recordpage/index.js +2 -1
  34. package/dist/cjs/product/recordpage/recordpage.js +197 -123
  35. package/dist/cjs/product/recordpage/recordpage.recipe.js +5 -0
  36. package/dist/cjs/product/richtext/richtext.js +43 -0
  37. package/dist/cjs/product/tasklist/tasklist.js +8 -1
  38. package/dist/cjs/tokens/palette.js +14 -0
  39. package/dist/cjs/tokens/semantic.js +5 -0
  40. package/dist/esm/components/badge/badge.recipe.js +10 -0
  41. package/dist/esm/components/bulkbar/bulkbar.js +5 -1
  42. package/dist/esm/components/fileupload/fileupload.js +20 -4
  43. package/dist/esm/components/formfield/formfield.js +8 -1
  44. package/dist/esm/components/heatmapchart/heatmapchart.js +14 -2
  45. package/dist/esm/components/skiplink/index.js +1 -0
  46. package/dist/esm/components/skiplink/skiplink.js +13 -0
  47. package/dist/esm/components/skiplink/skiplink.recipe.js +30 -0
  48. package/dist/esm/components/textlink/index.js +2 -0
  49. package/dist/esm/components/textlink/textlink.js +13 -0
  50. package/dist/esm/components/textlink/textlink.recipe.js +32 -0
  51. package/dist/esm/components/visuallyhidden/index.js +1 -0
  52. package/dist/esm/components/visuallyhidden/visuallyhidden.js +13 -0
  53. package/dist/esm/components/visuallyhidden/visuallyhidden.recipe.js +17 -0
  54. package/dist/esm/index.js +5 -0
  55. package/dist/esm/product/activityfeed/activityfeed.js +32 -0
  56. package/dist/esm/product/activityfeed/activityfeed.recipe.js +32 -0
  57. package/dist/esm/product/activityfeed/index.js +2 -0
  58. package/dist/esm/product/board/board.js +12 -3
  59. package/dist/esm/product/command/command.js +14 -2
  60. package/dist/esm/product/flowcanvas/flowcanvas.js +9 -2
  61. package/dist/esm/product/headerbar/headerbar.js +12 -0
  62. package/dist/esm/product/headerbar/headerbar.recipe.js +32 -0
  63. package/dist/esm/product/headerbar/index.js +1 -0
  64. package/dist/esm/product/inbox/inbox.js +13 -1
  65. package/dist/esm/product/onboarding/onboarding.js +22 -1
  66. package/dist/esm/product/recordpage/index.js +1 -1
  67. package/dist/esm/product/recordpage/recordpage.js +197 -124
  68. package/dist/esm/product/recordpage/recordpage.recipe.js +5 -0
  69. package/dist/esm/product/richtext/richtext.js +43 -0
  70. package/dist/esm/product/tasklist/tasklist.js +8 -1
  71. package/dist/esm/tokens/palette.js +14 -0
  72. package/dist/esm/tokens/semantic.js +5 -0
  73. package/dist/headerbar.css +7 -0
  74. package/dist/headerbar.d.ts +7 -0
  75. package/dist/headerbar.layered.css +9 -0
  76. package/dist/native/tokens.d.ts +1 -1
  77. package/dist/native/tokens.js +16 -4
  78. package/dist/recordpage.css +17 -31
  79. package/dist/recordpage.layered.css +17 -31
  80. package/dist/skiplink.css +5 -0
  81. package/dist/skiplink.d.ts +7 -0
  82. package/dist/skiplink.layered.css +7 -0
  83. package/dist/styles.css +69 -31
  84. package/dist/styles.layered.css +69 -31
  85. package/dist/textlink.css +11 -0
  86. package/dist/textlink.d.ts +7 -0
  87. package/dist/textlink.layered.css +13 -0
  88. package/dist/tokens.css +6 -0
  89. package/dist/tokens.json +16 -4
  90. package/dist/types/components/badge/badge.d.ts +2 -0
  91. package/dist/types/components/badge/badge.recipe.d.ts +8 -0
  92. package/dist/types/components/bulkbar/bulkbar.d.ts +1 -1
  93. package/dist/types/components/heatmapchart/heatmapchart.d.ts +1 -1
  94. package/dist/types/components/skiplink/index.d.ts +1 -0
  95. package/dist/types/components/skiplink/skiplink.d.ts +18 -0
  96. package/dist/types/components/skiplink/skiplink.recipe.d.ts +3 -0
  97. package/dist/types/components/textlink/index.d.ts +2 -0
  98. package/dist/types/components/textlink/textlink.d.ts +19 -0
  99. package/dist/types/components/textlink/textlink.recipe.d.ts +13 -0
  100. package/dist/types/components/visuallyhidden/index.d.ts +1 -0
  101. package/dist/types/components/visuallyhidden/visuallyhidden.d.ts +15 -0
  102. package/dist/types/components/visuallyhidden/visuallyhidden.recipe.d.ts +3 -0
  103. package/dist/types/index.d.ts +5 -0
  104. package/dist/types/product/activityfeed/activityfeed.d.ts +57 -0
  105. package/dist/types/product/activityfeed/activityfeed.recipe.d.ts +3 -0
  106. package/dist/types/product/activityfeed/index.d.ts +2 -0
  107. package/dist/types/product/board/board.d.ts +1 -1
  108. package/dist/types/product/command/command.d.ts +1 -1
  109. package/dist/types/product/flowcanvas/flowcanvas.d.ts +1 -1
  110. package/dist/types/product/headerbar/headerbar.d.ts +27 -0
  111. package/dist/types/product/headerbar/headerbar.recipe.d.ts +3 -0
  112. package/dist/types/product/headerbar/index.d.ts +1 -0
  113. package/dist/types/product/inbox/inbox.d.ts +1 -1
  114. package/dist/types/product/onboarding/onboarding.d.ts +1 -1
  115. package/dist/types/product/recordpage/index.d.ts +1 -1
  116. package/dist/types/product/recordpage/recordpage.d.ts +90 -1
  117. package/dist/types/product/tasklist/tasklist.d.ts +1 -1
  118. package/dist/types/tokens/contract.d.ts +3 -0
  119. package/dist/types/tokens/palette.d.ts +12 -0
  120. package/dist/types/tokens/semantic.d.ts +5 -0
  121. package/dist/visuallyhidden.css +3 -0
  122. package/dist/visuallyhidden.d.ts +7 -0
  123. package/dist/visuallyhidden.layered.css +5 -0
  124. package/package.json +26 -1
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HeaderBar = HeaderBar;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const headerbar_recipe_1 = require("./headerbar.recipe");
6
+ /**
7
+ * A page-level header row: an optional leading slot, a flexible title/
8
+ * breadcrumb region, and a trailing action cluster, on the measured 48px
9
+ * bar Attio uses for both its page-header and its view-bar row. See the
10
+ * recipe's own doc comment for the live measurement (crisp#462).
11
+ */
12
+ function HeaderBar({ leading, title, trailing, bordered = true, className, ...rest }) {
13
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, headerbar_recipe_1.headerbarRecipe)({ className }), "data-bordered": bordered ? "" : undefined, ...rest, children: [leading ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-headerbar-leading", children: leading })) : null, title ? (0, jsx_runtime_1.jsx)("div", { className: "crisp-headerbar-title", children: title }) : null, trailing ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-headerbar-trailing", children: trailing })) : null] }));
14
+ }
15
+ HeaderBar.displayName = "HeaderBar";
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.headerbarRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // A page-level header row — measured live at app.attio.com/techpartners/companies
7
+ // against two stacked bars: the top utility bar (breadcrumb + trailing icon
8
+ // actions, e.g. search/notifications) and the view bar directly under it
9
+ // ("All Companies" view name + tabs + "New Company"). Both share the same
10
+ // 48px content height and `0 ${space3}` padding; only the outer (first) bar
11
+ // carries the divider -- measured `border-bottom: 1px solid rgb(238,239,241)`
12
+ // (`borderSubtle`/`line100`, exact match) on the utility bar, no border of
13
+ // its own on the view bar directly beneath it. `bordered` (default true)
14
+ // lets a consumer that stacks two HeaderBars turn it off on the inner one,
15
+ // the way GRC's own AppLayout UtilityBar + Page viewBar do today with two
16
+ // hand-rolled, drifting copies of the same 49px bar (crisp#462 GRC audit,
17
+ // crisp#527).
18
+ //
19
+ // No bell, no breadcrumb, no search box baked in -- those are the `leading`/
20
+ // `title`/`trailing` slot content a consumer supplies. `size.header` (48px)
21
+ // is reused rather than a new token, since it's the same "workspace-switcher
22
+ // header height" measured elsewhere in the sidebar.
23
+ const config = (0, core_1.defineRecipe)({
24
+ name: "headerbar",
25
+ base: `box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: ${tokens_1.contract.space["2"]}; height: ${tokens_1.contract.size.header}; flex-shrink: 0; padding: 0 ${tokens_1.contract.space["3"]}; font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
26
+ &[data-bordered] { border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; }
27
+ & .crisp-headerbar-leading { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; flex-shrink: 0; min-width: 0; }
28
+ & .crisp-headerbar-title { flex: 1; min-width: 0; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; overflow: hidden; }
29
+ & .crisp-headerbar-trailing { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; flex-shrink: 0; }`,
30
+ variants: {},
31
+ defaultVariants: {},
32
+ compoundVariants: [],
33
+ });
34
+ exports.headerbarRecipe = (0, core_1.resolveRecipe)(config);
35
+ exports.default = config;
@@ -0,0 +1,17 @@
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./headerbar"), exports);
@@ -38,6 +38,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const lucide_react_1 = require("lucide-react");
39
39
  const React = __importStar(require("react"));
40
40
  const tabs_1 = require("../../components/tabs");
41
+ const core_1 = require("../../core");
41
42
  const inbox_recipe_1 = require("./inbox.recipe");
42
43
  /**
43
44
  * A notifications inbox panel — measured from a notification flyout. A
@@ -45,8 +46,19 @@ const inbox_recipe_1 = require("./inbox.recipe");
45
46
  * (avatar + message + time + unread dot) or a centred empty state. Put it inside
46
47
  * a Popover for the flyout.
47
48
  */
48
- function Inbox({ notifications = [], title = "Notifications", tabs, activeTab, onTabChange, emptyTitle = "No notifications", emptySubtitle = "Notifications such as mentions and important updates will be displayed here.", emptyAction, tabsLabel = "Notification views", className, ...rest }) {
49
+ function Inbox({ notifications = [], title: titleProp, tabs, activeTab, onTabChange, emptyTitle: emptyTitleProp, emptySubtitle: emptySubtitleProp, emptyAction, tabsLabel: tabsLabelProp, className, ...rest }) {
49
50
  const listRef = React.useRef(null);
51
+ // crisp/#471: `title`/`emptyTitle`/`emptySubtitle` are typed
52
+ // `React.ReactNode`, so they resolve by hand rather than through
53
+ // `useCrispMessage`, which is string-only — same reasoning as DataTable's
54
+ // own `empty`.
55
+ const crispMessages = (0, core_1.useCrispMessages)();
56
+ const title = titleProp ?? crispMessages.Notifications ?? "Notifications";
57
+ const emptyTitle = emptyTitleProp ?? crispMessages["No notifications"] ?? "No notifications";
58
+ const emptySubtitle = emptySubtitleProp ??
59
+ crispMessages["Notifications such as mentions and important updates will be displayed here."] ??
60
+ "Notifications such as mentions and important updates will be displayed here.";
61
+ const tabsLabel = (0, core_1.useCrispMessage)(tabsLabelProp, "Notification views");
50
62
  // crisp's shared tablist model, the one `Tabs` runs (#195). Hand-rolling a
51
63
  // second one is how Inbox ended up with no arrow keys, no roving tabindex
52
64
  // and no name while `Tabs` had all three.
@@ -4,6 +4,7 @@ exports.onboardingLabels = void 0;
4
4
  exports.OnboardingChecklist = OnboardingChecklist;
5
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const lucide_react_1 = require("lucide-react");
7
+ const core_1 = require("../../core");
7
8
  const onboarding_recipe_1 = require("./onboarding.recipe");
8
9
  /**
9
10
  * The English defaults for the two pieces of copy that are formatters rather
@@ -19,9 +20,29 @@ exports.onboardingLabels = {
19
20
  * started" widget. Title + optional menu/minimize, a segmented progress bar,
20
21
  * and step rows (completed rows strike through with a brand check).
21
22
  */
22
- function OnboardingChecklist({ title = "Getting started", steps, encouragement = "Keep it up!", onStepClick, onMenu, onMinimize, className, optionsLabel = "Checklist options", minimizeLabel = "Minimize", progressLabel = "Onboarding progress", progressText = exports.onboardingLabels.progressText, completedStepLabel = exports.onboardingLabels.completedStepLabel, ...rest }) {
23
+ function OnboardingChecklist({ title: titleProp, steps, encouragement: encouragementProp, onStepClick, onMenu, onMinimize, className, optionsLabel: optionsLabelProp, minimizeLabel: minimizeLabelProp, progressLabel: progressLabelProp, progressText: progressTextProp, completedStepLabel: completedStepLabelProp, ...rest }) {
23
24
  const total = steps.length;
24
25
  const done = steps.filter((s) => s.done).length;
26
+ // crisp/#471: same resolution order as every other component's own —
27
+ // explicit prop, then a CrispMessagesProvider's translation of the English
28
+ // default, then that default itself. `title`/`encouragement` are typed
29
+ // `React.ReactNode` (a caller can pass a translated element), so they
30
+ // resolve by hand rather than through `useCrispMessage`, which is
31
+ // string-only — same reasoning as DataTable's own `empty`.
32
+ const crispMessages = (0, core_1.useCrispMessages)();
33
+ const title = titleProp ?? crispMessages["Getting started"] ?? "Getting started";
34
+ const encouragement = encouragementProp ?? crispMessages["Keep it up!"] ?? "Keep it up!";
35
+ const optionsLabel = (0, core_1.useCrispMessage)(optionsLabelProp, "Checklist options");
36
+ const minimizeLabel = (0, core_1.useCrispMessage)(minimizeLabelProp, "Minimize");
37
+ const progressLabel = (0, core_1.useCrispMessage)(progressLabelProp, "Onboarding progress");
38
+ // `progressText`/`completedStepLabel` are formatters, not strings (the
39
+ // count moves within the sentence between languages, #216) — each of
40
+ // their own static fragments gets its own lookup, same as
41
+ // TableToolbar's "Sorted by" (crisp/#217).
42
+ const progressText = progressTextProp ??
43
+ ((doneCount, totalCount) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("b", { children: [doneCount, "/", totalCount] }), " ", crispMessages["steps completed"] ?? "steps completed"] })));
44
+ const completedStepLabel = completedStepLabelProp ??
45
+ ((label) => `${String(label)} ${crispMessages["— completed"] ?? "— completed"}`);
25
46
  return ((0, jsx_runtime_1.jsxs)("div", { className: (0, onboarding_recipe_1.onboardingRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-onboarding-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-onboarding-title", children: title }), onMenu && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-onboarding-iconbtn", "aria-label": optionsLabel, onClick: onMenu, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16, strokeWidth: 1.75 }) })), onMinimize && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-onboarding-iconbtn", "aria-label": minimizeLabel, onClick: onMinimize, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 16, strokeWidth: 1.75 }) }))] }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-onboarding-sub", children: [progressText(done, total), encouragement ? (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [" \u00B7 ", encouragement] }) : null] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-onboarding-progress", role: "progressbar", "aria-valuenow": done, "aria-valuemin": 0, "aria-valuemax": total, "aria-label": progressLabel, children: Array.from({ length: total }, (_, i) => ((0, jsx_runtime_1.jsx)("span", { className: "crisp-onboarding-seg", "data-done": i < done ? "" : undefined }, i))) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-onboarding-steps", children: steps.map((s) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-onboarding-step", "data-done": s.done ? "" : undefined, "aria-label": s.done ? completedStepLabel(s.label) : undefined, disabled: s.done, onClick: s.done ? undefined : () => onStepClick?.(s.id), children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-onboarding-step-icon", children: s.icon }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-onboarding-step-label", children: s.label }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-onboarding-step-check", children: s.done && (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 12, strokeWidth: 3 }) })] }, s.id))) })] }));
26
47
  }
27
48
  OnboardingChecklist.displayName = "OnboardingChecklist";
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.recordpageRecipe = exports.recordPageLabels = exports.RecordPage = void 0;
3
+ exports.recordpageRecipe = exports.recordPageLabels = exports.RecordPage = exports.RecordDetails = void 0;
4
4
  var recordpage_1 = require("./recordpage");
5
+ Object.defineProperty(exports, "RecordDetails", { enumerable: true, get: function () { return recordpage_1.RecordDetails; } });
5
6
  Object.defineProperty(exports, "RecordPage", { enumerable: true, get: function () { return recordpage_1.RecordPage; } });
6
7
  Object.defineProperty(exports, "recordPageLabels", { enumerable: true, get: function () { return recordpage_1.recordPageLabels; } });
7
8
  var recordpage_recipe_1 = require("./recordpage.recipe");
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.recordPageLabels = void 0;
37
+ exports.RecordDetails = RecordDetails;
37
38
  exports.RecordPage = RecordPage;
38
39
  const jsx_runtime_1 = require("react/jsx-runtime");
39
40
  const lucide_react_1 = require("lucide-react");
@@ -124,7 +125,42 @@ exports.recordPageLabels = {
124
125
  taggedCategories: (count) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["Tagged ", (0, jsx_runtime_1.jsx)("b", { children: count }), " categories"] })),
125
126
  recordCreated: (name) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("b", { children: name }), " record was created"] })),
126
127
  noEmailsWith: (name) => (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["No emails logged with ", name, " yet."] }),
128
+ noFieldsMatch: (query) => (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["No fields match \u201C", query, "\u201D."] }),
127
129
  };
130
+ /**
131
+ * crisp/#471: a `CrispMessagesProvider`'s `componentDefaults.recordPage` sits
132
+ * BETWEEN the built-in English defaults and an instance's own explicit
133
+ * `labels` prop -- a provider translates every `RecordPage`/`RecordDetails`
134
+ * on the page at once, and a caller who still passes `labels` for one
135
+ * instance keeps overriding it there. Not `useCrispMessage`: several of
136
+ * these entries (`domainSetTo`, `noFieldsMatch`, …) are formatter FUNCTIONS,
137
+ * which a flat string dictionary can't hold. Shared by `RecordPage` and
138
+ * `RecordDetails` (#528) -- both merge the same three layers the same way,
139
+ * so this is the one place that merge is written, not two copies of it.
140
+ */
141
+ function useRecordPageLabels(labels) {
142
+ const providerLabels = (0, core_1.useCrispComponentDefaults)("recordPage");
143
+ return React.useMemo(() => ({
144
+ ...exports.recordPageLabels,
145
+ ...providerLabels,
146
+ ...labels,
147
+ tabs: {
148
+ ...exports.recordPageLabels.tabs,
149
+ ...providerLabels?.tabs,
150
+ ...labels?.tabs,
151
+ },
152
+ highlightLabels: {
153
+ ...exports.recordPageLabels.highlightLabels,
154
+ ...providerLabels?.highlightLabels,
155
+ ...labels?.highlightLabels,
156
+ },
157
+ highlightEmpty: {
158
+ ...exports.recordPageLabels.highlightEmpty,
159
+ ...providerLabels?.highlightEmpty,
160
+ ...labels?.highlightEmpty,
161
+ },
162
+ }), [labels, providerLabels]);
163
+ }
128
164
  /** `Select` owns its own closed-by-default `open` state with no prop or
129
165
  * imperative way to start it open -- the only thing that opens it is a real
130
166
  * click on its trigger button. Mounting one on click (rather than rendering
@@ -180,6 +216,16 @@ function TagsValue({ values }) {
180
216
  }
181
217
  function FieldValue({ col, row, labels, }) {
182
218
  const v = row[col.key];
219
+ // `render` (#528) -- the same full-custom-content escape hatch
220
+ // `RecordsTable`'s own columns already have (recordstable.tsx's own doc:
221
+ // "an escape hatch for a display crisp can't express via type"), reused
222
+ // here so a value this component has no built-in shape for (a reference
223
+ // field's link to another record, say) doesn't need a new `type`/`editor`
224
+ // of its own. Checked before the empty check -- like `RecordsTable`'s own
225
+ // `render`, it owns the cell's content fully, including what "empty"
226
+ // looks like for that column.
227
+ if (col.render)
228
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: col.render(v, row) });
183
229
  if (valueEmpty(v))
184
230
  return (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-empty", children: labels.setAValue });
185
231
  // `editor: "select"` at rest (#457) -- checked ahead of `col.type` since
@@ -247,6 +293,147 @@ function TasksSection({ tasks, onAdd, onToggle, labels, }) {
247
293
  function EmptyCard({ icon, children, }) {
248
294
  return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-emptycard", children: [icon, children] }));
249
295
  }
296
+ /**
297
+ * The Details column's own field list, on its own (#528) — `RecordPage`'s
298
+ * "Record Details" collapsible + searchable field list, the one piece a
299
+ * consumer with a fixed-width rail of its own (no resize handle, no entity
300
+ * header, no CRM Lists panel — GRC's own persistent 320px column, grc#66)
301
+ * actually needs, rather than rebuilding this same click-to-edit/search/
302
+ * `sections` machinery by hand or pulling in the whole page's chrome for a
303
+ * rail that isn't one. `RecordPage` composes this internally for its own
304
+ * resizable aside (same component either way, not a second copy) — this is
305
+ * that same block, exactly, just not wrapped in `RecordPage`'s own topbar/
306
+ * tabs/resizer.
307
+ *
308
+ * Not wrapped in `recordpageRecipe` itself: doing so would duplicate the
309
+ * recipe's own `display:flex;height:100%` base rule as a nested child
310
+ * inside `RecordPage`'s aside (which already carries that class from
311
+ * `RecordPage`'s own root), breaking that layout. A standalone caller
312
+ * needs exactly one `.crisp-recordpage`-scoped ancestor for this
313
+ * component's classes to resolve — wrap it in one, the same way
314
+ * `RecordPage`'s own root does: `<div className={recordpageRecipe()}>`
315
+ * (also exported from this package).
316
+ */
317
+ function RecordDetails({ record, columns, sections, onEdit, labels, className, ...rest }) {
318
+ const t = useRecordPageLabels(labels);
319
+ const { editingKey, pendingKey, error, openEditing, closeEditing, commit } = (0, recordstable_1.useEditableField)(onEdit);
320
+ const [detailsOpen, setDetailsOpen] = React.useState(true);
321
+ const [searchOpen, setSearchOpen] = React.useState(false);
322
+ const [fieldQuery, setFieldQuery] = React.useState("");
323
+ const [showAllValues, setShowAllValues] = React.useState(false);
324
+ const fields = columns.filter((col) => col.type !== "entity");
325
+ const q = fieldQuery.trim().toLowerCase();
326
+ const matches = (col) => !q || col.label.toLowerCase().includes(q);
327
+ // #528: `sections` groups fields under headings, in the caller's own
328
+ // order, and always shows every match — no 5-field reveal (see
329
+ // `RecordPageSection`'s own doc). Omitted, this stays the flat,
330
+ // Attio-measured list with its 5-field "View all values" reveal,
331
+ // unchanged from before #528.
332
+ const FIELD_LIMIT = 5;
333
+ let canRevealFields = false;
334
+ let groups;
335
+ if (sections) {
336
+ const byKey = new Map(fields.map((col) => [col.key, col]));
337
+ groups = sections
338
+ .map((s) => ({
339
+ title: s.title,
340
+ fields: s.fields
341
+ .map((key) => byKey.get(key))
342
+ .filter((col) => col !== undefined)
343
+ .filter(matches),
344
+ }))
345
+ // A section left with no matching fields (a search that matches
346
+ // nothing in it, or a `fields` key naming no real column) doesn't
347
+ // render at all -- heading included, not an empty group under a
348
+ // heading with nothing below it.
349
+ .filter((g) => g.fields.length > 0);
350
+ }
351
+ else {
352
+ const matched = q ? fields.filter(matches) : fields;
353
+ canRevealFields = !q && matched.length > FIELD_LIMIT;
354
+ const shown = canRevealFields && !showAllValues
355
+ ? matched.slice(0, FIELD_LIMIT)
356
+ : matched;
357
+ groups = [{ title: undefined, fields: shown }];
358
+ }
359
+ const totalShown = groups.reduce((n, g) => n + g.fields.length, 0);
360
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, core_1.cx)("crisp-recordpage-section", className), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-detailhead", children: [searchOpen ? ((0, jsx_runtime_1.jsx)("input", {
361
+ // biome-ignore lint/a11y/noAutofocus: focusing the just-opened field search is expected
362
+ 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) => {
363
+ if (e.key === "Escape") {
364
+ setSearchOpen(false);
365
+ setFieldQuery("");
366
+ }
367
+ } })) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-collapse", "data-collapsed": detailsOpen ? undefined : "", "aria-expanded": detailsOpen, onClick: () => setDetailsOpen((o) => !o), children: [t.recordDetails, (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-recordpage-chev", size: 12, strokeWidth: 2.5 })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-detailsearch", "data-active": searchOpen ? "" : undefined, "aria-label": searchOpen ? t.closeSearch : t.searchFields, "aria-pressed": searchOpen, onClick: () => {
368
+ setSearchOpen((s) => !s);
369
+ setFieldQuery("");
370
+ if (!searchOpen)
371
+ setDetailsOpen(true);
372
+ }, 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 &&
373
+ (totalShown > 0 ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [groups.map((g, i) => ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [sections && g.title != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-section-title crisp-recordpage-fieldsection-title", children: g.title })), g.fields.map((col) => {
374
+ // #431: a field only actually opens when there's an
375
+ // `onEdit` to commit through -- `editable` with nothing
376
+ // to call would be a dead click.
377
+ const canEdit = Boolean(col.editable && onEdit);
378
+ const isPending = pendingKey === col.key;
379
+ const fieldError = error?.key === col.key ? error.message : null;
380
+ 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 })] }));
381
+ // `switch` is always its own live widget -- no
382
+ // click-to-open, no separate edit/view mode: flipping the
383
+ // toggle IS the commit gesture, and there's no
384
+ // meaningfully different "rest" display for a boolean
385
+ // (same exception `DataTable`'s own `editor` makes, #453).
386
+ if (canEdit && col.editor === "switch") {
387
+ const value = record[col.key];
388
+ 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: [(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));
389
+ }
390
+ // `select` opens crisp's own `Select` on a SINGLE click
391
+ // (#457, corrects #431/#385's original always-live copy
392
+ // of the same regression #453 fixed on `DataTable`) --
393
+ // like every other editable type here, at rest it shows
394
+ // its value (see `FieldValue`'s own `col.editor ===
395
+ // "select"` case) via the generic click-to-open branch
396
+ // below, and only mounts `Select` once `editingKey`
397
+ // matches. `DataTable`'s own double-click gate doesn't
398
+ // apply: `RecordPage`'s plain text/date fields already
399
+ // open on a single click (measured live for #431), so
400
+ // `select` matches that, not `DataTable`.
401
+ if (canEdit &&
402
+ editingKey === col.key &&
403
+ col.editor === "select") {
404
+ const value = record[col.key];
405
+ 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)(RecordPageSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commit(record.id, col.key, v), onCancel: closeEditing }), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
406
+ }
407
+ if (canEdit && editingKey === col.key) {
408
+ const value = record[col.key];
409
+ const editor = (0, recordstable_1.editableInputType)(col.type);
410
+ 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",
411
+ // biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
412
+ autoFocus: true, ...editor, disabled: isPending, defaultValue: value == null ? "" : String(value), "aria-label": `Edit ${col.label}`, onKeyDown: (e) => {
413
+ if (isPending)
414
+ return;
415
+ if (e.key === "Enter")
416
+ commit(record.id, col.key, e.currentTarget.value);
417
+ else if (e.key === "Escape")
418
+ closeEditing();
419
+ }, onBlur: (e) => {
420
+ if (isPending)
421
+ return;
422
+ commit(record.id, col.key, e.currentTarget.value);
423
+ } }), 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));
424
+ }
425
+ 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));
426
+ })] }, g.title != null ? String(g.title) : `section-${i}`))), canRevealFields && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-viewall crisp-recordpage-viewall--section", onClick: () => {
427
+ // Revealing all fields also opens the (focused) field
428
+ // search; collapsing closes it.
429
+ const next = !showAllValues;
430
+ setShowAllValues(next);
431
+ setSearchOpen(next);
432
+ if (!next)
433
+ setFieldQuery("");
434
+ }, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-noresults", children: t.noFieldsMatch(fieldQuery) })))] }));
435
+ }
436
+ RecordDetails.displayName = "RecordDetails";
250
437
  /**
251
438
  * The full-page company record view — the page that opens when you click into a
252
439
  * record (as opposed to the peek [RecordPanel]). A top breadcrumb bar with
@@ -255,36 +442,8 @@ function EmptyCard({ icon, children, }) {
255
442
  * Emails sections (Notes & Tasks are live — add and check things off). Drop it
256
443
  * into the app's main content area.
257
444
  */
258
- function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, ...rest }) {
259
- const { editingKey, pendingKey, error, openEditing, closeEditing, commit } = (0, recordstable_1.useEditableField)(onEdit);
260
- // crisp/#471: a CrispMessagesProvider's `componentDefaults.recordPage` sits
261
- // BETWEEN the built-in English defaults and this instance's own explicit
262
- // `labels` prop — a provider translates every RecordPage on the page at
263
- // once, and a caller who still passes `labels` for one instance keeps
264
- // overriding it there, same as before this existed. Not `useCrispMessage`:
265
- // several of these entries (`domainSetTo`, `sortedBy`, …) are formatter
266
- // FUNCTIONS, which a flat string dictionary can't hold.
267
- const providerLabels = (0, core_1.useCrispComponentDefaults)("recordPage");
268
- const t = React.useMemo(() => ({
269
- ...exports.recordPageLabels,
270
- ...providerLabels,
271
- ...labels,
272
- tabs: {
273
- ...exports.recordPageLabels.tabs,
274
- ...providerLabels?.tabs,
275
- ...labels?.tabs,
276
- },
277
- highlightLabels: {
278
- ...exports.recordPageLabels.highlightLabels,
279
- ...providerLabels?.highlightLabels,
280
- ...labels?.highlightLabels,
281
- },
282
- highlightEmpty: {
283
- ...exports.recordPageLabels.highlightEmpty,
284
- ...providerLabels?.highlightEmpty,
285
- ...labels?.highlightEmpty,
286
- },
287
- }), [labels, providerLabels]);
445
+ 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, ...rest }) {
446
+ const t = useRecordPageLabels(labels);
288
447
  const [favorite, setFavorite] = React.useState(false);
289
448
  // Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
290
449
  // `[]`) -- otherwise RecordPage keeps its own self-contained state, as
@@ -296,11 +455,6 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
296
455
  const notes = notesProp ?? internalNotes;
297
456
  const tasks = tasksProp ?? internalTasks;
298
457
  const [showAllActivity, setShowAllActivity] = React.useState(false);
299
- // Collapsible + searchable Record Details.
300
- const [detailsOpen, setDetailsOpen] = React.useState(true);
301
- const [searchOpen, setSearchOpen] = React.useState(false);
302
- const [fieldQuery, setFieldQuery] = React.useState("");
303
- const [showAllValues, setShowAllValues] = React.useState(false);
304
458
  // Drag-resizable left details column.
305
459
  const [detailsW, setDetailsW] = React.useState(320);
306
460
  const [resizing, setResizing] = React.useState(false);
@@ -337,17 +491,6 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
337
491
  ? record[entityCol.key]
338
492
  : undefined;
339
493
  const name = entity?.name ?? "Record";
340
- const fields = columns.filter((col) => col.type !== "entity");
341
- const q = fieldQuery.trim().toLowerCase();
342
- const matchedFields = q
343
- ? fields.filter((col) => col.label.toLowerCase().includes(q))
344
- : fields;
345
- // Shows the first 5 fields; "View all values" reveals the rest.
346
- const FIELD_LIMIT = 5;
347
- const canRevealFields = !q && matchedFields.length > FIELD_LIMIT;
348
- const shownFields = canRevealFields && !showAllValues
349
- ? matchedFields.slice(0, FIELD_LIMIT)
350
- : matchedFields;
351
494
  const byKey = React.useMemo(() => new Map(columns.map((col) => [col.key, col])), [columns]);
352
495
  // The Overview "Highlights" — a fixed set of enrichment stats (measured
353
496
  // from the live app), a couple derived from the record, the rest empty.
@@ -365,7 +508,11 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
365
508
  : n >= 1000
366
509
  ? "1K–10K"
367
510
  : "<1K";
368
- const highlights = [
511
+ // #528: the built-in CRM Highlights set, used when the caller omits
512
+ // `highlights` entirely -- a caller passing `highlights` (an array, or
513
+ // `false` to omit the section) replaces this wholesale, the same
514
+ // "caller's own list, not a merge" choice `tabs` already makes (#379).
515
+ const defaultHighlights = [
369
516
  {
370
517
  label: t.highlightLabels.connectionStrength,
371
518
  icon: lucide_react_1.Zap,
@@ -403,6 +550,7 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
403
550
  empty: t.highlightEmpty.employeeRange,
404
551
  },
405
552
  ];
553
+ const highlights = highlightsProp === undefined ? defaultHighlights : highlightsProp;
406
554
  const ti = { size: 15, strokeWidth: 1.75 };
407
555
  const builtinTabs = [
408
556
  { value: "overview", label: t.tabs.overview, icon: (0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { ...ti }) },
@@ -485,7 +633,7 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
485
633
  const builtinPanel = (active) => {
486
634
  switch (active) {
487
635
  case "overview":
488
- 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 })] }));
636
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-panel", children: [highlights !== false && ((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: [h.icon && ((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 })] }));
489
637
  case "activity":
490
638
  return (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-panel", children: activityFeed });
491
639
  case "notes":
@@ -511,81 +659,7 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
511
659
  const effectivePanel = tabsProp
512
660
  ? (active) => tabsProp.find((tb) => tb.id === active)?.render()
513
661
  : builtinPanel;
514
- 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", {
515
- // biome-ignore lint/a11y/noAutofocus: focusing the just-opened field search is expected
516
- 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) => {
517
- if (e.key === "Escape") {
518
- setSearchOpen(false);
519
- setFieldQuery("");
520
- }
521
- } })) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-collapse", "data-collapsed": detailsOpen ? undefined : "", "aria-expanded": detailsOpen, onClick: () => setDetailsOpen((o) => !o), children: [t.recordDetails, (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-recordpage-chev", size: 12, strokeWidth: 2.5 })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-detailsearch", "data-active": searchOpen ? "" : undefined, "aria-label": searchOpen ? t.closeSearch : t.searchFields, "aria-pressed": searchOpen, onClick: () => {
522
- setSearchOpen((s) => !s);
523
- setFieldQuery("");
524
- if (!searchOpen)
525
- setDetailsOpen(true);
526
- }, 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 &&
527
- (shownFields.length > 0 ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [shownFields.map((col) => {
528
- // #431: a field only actually opens when there's an
529
- // `onEdit` to commit through -- `editable` with nothing
530
- // to call would be a dead click.
531
- const canEdit = Boolean(col.editable && onEdit);
532
- const isPending = pendingKey === col.key;
533
- const fieldError = error?.key === col.key ? error.message : null;
534
- 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 })] }));
535
- // `switch` is always its own live widget -- no
536
- // click-to-open, no separate edit/view mode: flipping the
537
- // toggle IS the commit gesture, and there's no
538
- // meaningfully different "rest" display for a boolean
539
- // (same exception `DataTable`'s own `editor` makes, #453).
540
- if (canEdit && col.editor === "switch") {
541
- const value = record[col.key];
542
- 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: [(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));
543
- }
544
- // `select` opens crisp's own `Select` on a SINGLE click
545
- // (#457, corrects #431/#385's original always-live
546
- // copy of the same regression #453 fixed on `DataTable`)
547
- // -- like every other editable type here, at rest it
548
- // shows its value (see `FieldValue`'s own `col.editor
549
- // === "select"` case) via the generic click-to-open
550
- // branch below, and only mounts `Select` once
551
- // `editingKey` matches. `DataTable`'s own double-click
552
- // gate doesn't apply: `RecordPage`'s plain text/date
553
- // fields already open on a single click (measured live
554
- // for #431), so `select` matches that, not `DataTable`.
555
- if (canEdit &&
556
- editingKey === col.key &&
557
- col.editor === "select") {
558
- const value = record[col.key];
559
- 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)(RecordPageSelectEditor, { col: col, value: value, isPending: isPending, onCommit: (v) => commit(record.id, col.key, v), onCancel: closeEditing }), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
560
- }
561
- if (canEdit && editingKey === col.key) {
562
- const value = record[col.key];
563
- const editor = (0, recordstable_1.editableInputType)(col.type);
564
- 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",
565
- // biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
566
- autoFocus: true, ...editor, disabled: isPending, defaultValue: value == null ? "" : String(value), "aria-label": `Edit ${col.label}`, onKeyDown: (e) => {
567
- if (isPending)
568
- return;
569
- if (e.key === "Enter")
570
- commit(record.id, col.key, e.currentTarget.value);
571
- else if (e.key === "Escape")
572
- closeEditing();
573
- }, onBlur: (e) => {
574
- if (isPending)
575
- return;
576
- commit(record.id, col.key, e.currentTarget.value);
577
- } }), 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));
578
- }
579
- 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));
580
- }), canRevealFields && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-viewall crisp-recordpage-viewall--section", onClick: () => {
581
- // Revealing all fields also opens the (focused)
582
- // field search; collapsing closes it.
583
- const next = !showAllValues;
584
- setShowAllValues(next);
585
- setSearchOpen(next);
586
- if (!next)
587
- setFieldQuery("");
588
- }, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-noresults", children: ["No fields match \u201C", fieldQuery, "\u201D."] })))] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-section", children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.lists, action: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-addbtn", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), (0, jsx_runtime_1.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": 300, "aria-valuemax": 560, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
662
+ 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.jsx)(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-section", children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.lists, action: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-addbtn", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), (0, jsx_runtime_1.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": 300, "aria-valuemax": 560, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
589
663
  if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
590
664
  return;
591
665
  // The pointer drag learned its direction in #212; this did not.
@@ -66,6 +66,11 @@ const config = (0, core_1.defineRecipe)({
66
66
  & .crisp-recordpage-detailsearch { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: ${tokens_1.contract.size.chip}; flex-shrink: 0; padding: 0; margin: 0; border: none; border-radius: ${tokens_1.contract.radius.sm}; background: none; color: ${tokens_1.contract.fgMuted}; font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s; }
67
67
  & .crisp-recordpage-detailsearch:hover, & .crisp-recordpage-detailsearch[data-active] { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
68
68
  & .crisp-recordpage-noresults { padding: ${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["1"]}; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }
69
+ /* A caller-supplied \`sections\` grouping (#528) is not an Attio-measured
70
+ shape (its own Details column is flat) -- this heading reuses
71
+ .crisp-recordpage-section-title's own type treatment verbatim, just
72
+ spaced from the group above it. */
73
+ & .crisp-recordpage-fieldsection-title { margin-top: ${tokens_1.contract.space["3"]}; }
69
74
  & .crisp-recordpage-morevalues { display: inline-flex; align-items: center; padding: 0; margin: 0; border: none; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgMuted}; cursor: pointer; }
70
75
  & .crisp-recordpage-morevalues:hover { color: ${tokens_1.contract.fgPrimary}; }
71
76
  & .crisp-recordpage-viewall { flex-basis: 100%; margin-top: ${tokens_1.contract.space["0.5"]}; border: none; background: none; padding: 0; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 55%, transparent); font-family: inherit; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.regular}; cursor: pointer; text-align: start; }