@crisp-ui-kit/crisp 0.52.2 → 0.52.4

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.
@@ -7,11 +7,16 @@ const tokens_1 = require("../../tokens");
7
7
  // ~380px rounded panel with a title, "Notifications / Requests" tabs (with
8
8
  // counts), then notification rows (avatar + message + time + unread dot) or a
9
9
  // centred empty state. Drop it in a Popover for the flyout, or use standalone.
10
+ //
11
+ // The shadow is `elevation.floating` (crisp#464) — this is a tier-1
12
+ // floating-and-attached surface, the exact class that token exists for
13
+ // (AUTHORING.md Lesson 31); it used to carry its own hand-typed 3-layer
14
+ // guess instead of the measured 4-layer value.
10
15
  const config = (0, core_1.defineRecipe)({
11
16
  name: "inbox",
12
17
  base: `display: flex; flex-direction: column; box-sizing: border-box; width: 380px; max-height: 588px;
13
18
  border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgSurface}; font-family: ${tokens_1.contract.font.sans}; overflow: hidden;
14
- box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent), 0 8px 28px -6px rgba(28,40,64,0.16), 0 2px 6px -2px rgba(28,40,64,0.10);
19
+ box-shadow: ${tokens_1.elevation.floating};
15
20
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
16
21
 
17
22
  & .crisp-inbox-head { padding: ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["3"]} 0; }
@@ -40,6 +40,8 @@ const React = __importStar(require("react"));
40
40
  const avatar_1 = require("../../components/avatar");
41
41
  const core_1 = require("../../core");
42
42
  const recordpanel_recipe_1 = require("./recordpanel.recipe");
43
+ const RESIZE_MIN_WIDTH = 360;
44
+ const RESIZE_MAX_WIDTH = 720;
43
45
  /**
44
46
  * Right-side record peek panel — Attio's own peek shows a Highlights card
45
47
  * area and an Activity feed (measured live: hover a Companies row, click its
@@ -50,8 +52,65 @@ const recordpanel_recipe_1 = require("./recordpanel.recipe");
50
52
  * place). Render it inside a `position: relative` container (e.g. the app
51
53
  * main area); it slides in from the right and overlays the content.
52
54
  */
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 }) {
55
+ 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, counterLabel, resizable = false, width: controlledWidth, defaultWidth = 420, onWidthChange, minWidth = RESIZE_MIN_WIDTH, maxWidth = RESIZE_MAX_WIDTH, activityLabel = "Activity", viewAllActivityLabel = "View all", onViewAllActivity, activity, sections, footer, ...rest }) {
54
56
  const titleId = React.useId();
57
+ // Controlled/uncontrolled width (#413), the same split every other
58
+ // stateful crisp prop uses (see Accordion's own value/defaultValue). Only
59
+ // read when `resizable` — an immovable panel has no reason to carry width
60
+ // state at all, controlled or not.
61
+ const [internalWidth, setInternalWidth] = React.useState(defaultWidth);
62
+ const width = controlledWidth ?? internalWidth;
63
+ const setWidth = (next) => {
64
+ const clamped = Math.min(maxWidth, Math.max(minWidth, next));
65
+ if (controlledWidth === undefined)
66
+ setInternalWidth(clamped);
67
+ onWidthChange?.(clamped);
68
+ };
69
+ // Dragging reads/writes a ref, not state, mid-drag -- a `pointermove`
70
+ // firing dozens of times a second has no business going through a React
71
+ // re-render for each one; only the final clamped value needs to land in
72
+ // state (or a controlled consumer's own), via `setWidth` above. Mirrors
73
+ // DataTable's own column-resize ref shape (`resizeRef`) for the same
74
+ // reason, `dir` included.
75
+ const resizeRef = React.useRef(null);
76
+ const onResizeStart = (e) => {
77
+ if (e.button !== 0)
78
+ return;
79
+ // The panel sits at `inset-inline-end`, so its resize handle (at the
80
+ // panel's own inline-start edge) is physically on the LEFT in LTR and
81
+ // the RIGHT in RTL. Dragging it away from the panel's pinned edge must
82
+ // grow the panel either way -- physically LEFT in LTR (dir -1, so a
83
+ // negative `clientX` delta adds width), physically RIGHT in RTL (dir
84
+ // +1) -- the mirror image of DataTable's own column-resize grip, which
85
+ // is anchored at inset-inline-end instead (#214's own comment there).
86
+ const dir = (0, core_1.isRtlElement)(e.currentTarget) ? 1 : -1;
87
+ resizeRef.current = { x: e.clientX, width, dir };
88
+ e.target.setPointerCapture(e.pointerId);
89
+ };
90
+ const onResizeMove = (e) => {
91
+ const r = resizeRef.current;
92
+ if (!r)
93
+ return;
94
+ setWidth(r.width + r.dir * (e.clientX - r.x));
95
+ };
96
+ const onResizeEnd = (e) => {
97
+ resizeRef.current = null;
98
+ e.target.releasePointerCapture(e.pointerId);
99
+ };
100
+ // Which sections (#413) are expanded -- uncontrolled and independent
101
+ // (Attio's own Emails/Notes/Tasks open without closing one another, the
102
+ // same as Highlights cards never affecting each other): a plain `Set`,
103
+ // not modelled on Accordion's own single/multiple split, since a
104
+ // consumer here has no reason to want only one open at a time.
105
+ const [openSections, setOpenSections] = React.useState(() => new Set());
106
+ const toggleSection = (key) => setOpenSections((prev) => {
107
+ const next = new Set(prev);
108
+ if (next.has(key))
109
+ next.delete(key);
110
+ else
111
+ next.add(key);
112
+ return next;
113
+ });
55
114
  // close on Escape while open -- but not when a Menu/Select/Popover/
56
115
  // ContextMenu opened from inside the panel (e.g. a footer "More actions"
57
116
  // menu) is itself open (#361): that Escape belongs to the nested layer,
@@ -86,6 +145,10 @@ function RecordPanel({ record, onClose, className, closeLabel = "Close panel", o
86
145
  if (!record)
87
146
  return null;
88
147
  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 }))] }));
148
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpanel_recipe_1.recordpanelRecipe)({ className }), style: resizable ? { width, maxWidth: "calc(92% - 8px)" } : undefined, ...rest, role: "dialog", "aria-labelledby": titleId, children: [resizable && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-resize-handle", onPointerDown: onResizeStart, onPointerMove: onResizeMove, onPointerUp: onResizeEnd })), (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 }) })] })), counterLabel != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-counter", children: counterLabel })), (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 })] })), sections && sections.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-sections", children: sections.map((s) => {
149
+ const isOpen = openSections.has(s.key);
150
+ const panelId = `${titleId}-section-${s.key}`;
151
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-section-row", children: [s.content ? ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpanel-section-trigger", "aria-expanded": isOpen, "aria-controls": panelId, onClick: () => toggleSection(s.key), children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-section-row-label", children: s.label }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-section-row-count", children: s.count }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-recordpanel-section-chevron", size: 14, strokeWidth: 1.75, "data-open": isOpen ? "" : undefined, "aria-hidden": "true" })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-section-trigger crisp-recordpanel-section-trigger-inert", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-section-row-label", children: s.label }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-section-row-count", children: s.count })] })), s.content && isOpen && ((0, jsx_runtime_1.jsx)("div", { id: panelId, className: "crisp-recordpanel-section-content", children: s.content }))] }, s.key));
152
+ }) }))] }), footer != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-footer", children: footer }))] }));
90
153
  }
91
154
  RecordPanel.displayName = "RecordPanel";
@@ -32,12 +32,27 @@ const config = (0, core_1.defineRecipe)({
32
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
33
  & .crisp-recordpanel-toolbar-spacer { flex: 1; }
34
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
+ /* #413: "2 of 37 in All Companies" -- measured live, same toolbar row,
36
+ right after Prev/Next. 12px/500, the same 63%-of-fgPrimary mix every
37
+ other muted heading in this panel already uses. */
38
+ & .crisp-recordpanel-counter { margin-inline-start: ${tokens_1.contract.space["3"]}; font-size: 12px; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
35
39
  & .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
40
  & .crisp-recordpanel-iconbtn:hover { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
37
41
  & .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
42
  & .crisp-recordpanel-navbtn:hover:not(:disabled) { background: ${tokens_1.contract.bgRaised}; }
39
43
  & .crisp-recordpanel-navbtn:disabled { opacity: 0.4; cursor: not-allowed; }
40
44
 
45
+ /* #413: measured live -- a 10px strip at the panel's own left edge,
46
+ cursor: col-resize (the issue's own repro calls it "ew-resize";
47
+ the actual computed value read off the live DOM is col-resize, the
48
+ same cursor family under a different keyword). Kept fully inside the
49
+ panel's own box, not extending past inset-inline-start: 0, so the
50
+ panel's own overflow: hidden (there for its rounded corners) never
51
+ clips it. touch-action: none stops a touchscreen's own pan gesture
52
+ from fighting the drag. */
53
+ & .crisp-recordpanel-resize-handle { position: absolute; top: 0; bottom: 0; inset-inline-start: 0; width: 10px; cursor: col-resize; z-index: 1; touch-action: none; }
54
+ & .crisp-recordpanel-resize-handle:hover, & .crisp-recordpanel-resize-handle:active { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); }
55
+
41
56
  & .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"]}; }
42
57
  & .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}; }
43
58
  & .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; }
@@ -76,6 +91,28 @@ const config = (0, core_1.defineRecipe)({
76
91
  section heading above. */
77
92
  & .crisp-recordpanel-activity { font-size: ${tokens_1.contract.text.lg}; }
78
93
 
94
+ /* #413: collapsed Emails/Notes/Tasks below Activity -- measured live: a
95
+ 473px-wide row (the panel's own content width, 497 minus its 12px/8px
96
+ side insets), 20px tall, 14px/500 solid fgPrimary label (NOT the 63%
97
+ mix Highlights/Activity headings use -- this is body text, not a
98
+ muted heading), a trailing count. padding "0 8px 0 12px" measured on
99
+ the row itself; reproduced here as the trigger's own padding, since
100
+ the trigger IS the row (a plain div, not a button, when there's no
101
+ content to expand into -- matches the empty ("Emails 0") row
102
+ measured live, which has no expand affordance at all). */
103
+ & .crisp-recordpanel-sections { display: flex; flex-direction: column; padding-top: ${tokens_1.contract.space["2"]}; }
104
+ & .crisp-recordpanel-section-trigger { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; width: 100%; height: 20px; padding: 0 ${tokens_1.contract.space["2"]} 0 ${tokens_1.contract.space["3"]}; border: none; background: none; font-family: inherit; text-align: start; cursor: pointer; }
105
+ & .crisp-recordpanel-section-trigger-inert { cursor: default; }
106
+ & .crisp-recordpanel-section-row-label { font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary}; }
107
+ /* The count wasn't captured with its own background/pill chrome in the
108
+ live measurement (only its text: 14px, a "tertiary" internal variant
109
+ name that doesn't map to anything crisp defines) -- muted text next
110
+ to the label, not an invented badge shape. */
111
+ & .crisp-recordpanel-section-row-count { font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgMuted}; }
112
+ & .crisp-recordpanel-section-chevron { margin-inline-start: auto; flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; transition: transform .15s ease; }
113
+ & .crisp-recordpanel-section-chevron[data-open] { transform: rotate(180deg); }
114
+ & .crisp-recordpanel-section-content { padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["3"]}; }
115
+
79
116
  /* #329: an optional slot for record actions (e.g. Edit). Reuses the
80
117
  header's own measured values rather than inventing new ones -- the
81
118
  same horizontal inset (space.3), the same hairline border token
@@ -1,14 +1,19 @@
1
1
  import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
- import { contract as c } from "../../tokens/index.js";
2
+ import { contract as c, elevation as e } from "../../tokens/index.js";
3
3
  // A notifications inbox panel — measured from a notification flyout: a
4
4
  // ~380px rounded panel with a title, "Notifications / Requests" tabs (with
5
5
  // counts), then notification rows (avatar + message + time + unread dot) or a
6
6
  // centred empty state. Drop it in a Popover for the flyout, or use standalone.
7
+ //
8
+ // The shadow is `elevation.floating` (crisp#464) — this is a tier-1
9
+ // floating-and-attached surface, the exact class that token exists for
10
+ // (AUTHORING.md Lesson 31); it used to carry its own hand-typed 3-layer
11
+ // guess instead of the measured 4-layer value.
7
12
  const config = defineRecipe({
8
13
  name: "inbox",
9
14
  base: `display: flex; flex-direction: column; box-sizing: border-box; width: 380px; max-height: 588px;
10
15
  border-radius: ${c.radius.xl}; background: ${c.bgSurface}; font-family: ${c.font.sans}; overflow: hidden;
11
- box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 6%, transparent), 0 8px 28px -6px rgba(28,40,64,0.16), 0 2px 6px -2px rgba(28,40,64,0.10);
16
+ box-shadow: ${e.floating};
12
17
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
13
18
 
14
19
  & .crisp-inbox-head { padding: ${c.space["3"]} ${c.space["3"]} 0; }
@@ -2,8 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ArrowUpRight, ChevronDown, ChevronUp, X } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { Avatar } from "../../components/avatar/index.js";
5
- import { hasOpenFloatingLayer } from "../../core/index.js";
5
+ import { hasOpenFloatingLayer, isRtlElement } from "../../core/index.js";
6
6
  import { recordpanelRecipe } from "./recordpanel.recipe.js";
7
+ const RESIZE_MIN_WIDTH = 360;
8
+ const RESIZE_MAX_WIDTH = 720;
7
9
  /**
8
10
  * Right-side record peek panel — Attio's own peek shows a Highlights card
9
11
  * area and an Activity feed (measured live: hover a Companies row, click its
@@ -14,8 +16,65 @@ import { recordpanelRecipe } from "./recordpanel.recipe.js";
14
16
  * place). Render it inside a `position: relative` container (e.g. the app
15
17
  * main area); it slides in from the right and overlays the content.
16
18
  */
17
- export 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 }) {
19
+ export 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, counterLabel, resizable = false, width: controlledWidth, defaultWidth = 420, onWidthChange, minWidth = RESIZE_MIN_WIDTH, maxWidth = RESIZE_MAX_WIDTH, activityLabel = "Activity", viewAllActivityLabel = "View all", onViewAllActivity, activity, sections, footer, ...rest }) {
18
20
  const titleId = React.useId();
21
+ // Controlled/uncontrolled width (#413), the same split every other
22
+ // stateful crisp prop uses (see Accordion's own value/defaultValue). Only
23
+ // read when `resizable` — an immovable panel has no reason to carry width
24
+ // state at all, controlled or not.
25
+ const [internalWidth, setInternalWidth] = React.useState(defaultWidth);
26
+ const width = controlledWidth ?? internalWidth;
27
+ const setWidth = (next) => {
28
+ const clamped = Math.min(maxWidth, Math.max(minWidth, next));
29
+ if (controlledWidth === undefined)
30
+ setInternalWidth(clamped);
31
+ onWidthChange?.(clamped);
32
+ };
33
+ // Dragging reads/writes a ref, not state, mid-drag -- a `pointermove`
34
+ // firing dozens of times a second has no business going through a React
35
+ // re-render for each one; only the final clamped value needs to land in
36
+ // state (or a controlled consumer's own), via `setWidth` above. Mirrors
37
+ // DataTable's own column-resize ref shape (`resizeRef`) for the same
38
+ // reason, `dir` included.
39
+ const resizeRef = React.useRef(null);
40
+ const onResizeStart = (e) => {
41
+ if (e.button !== 0)
42
+ return;
43
+ // The panel sits at `inset-inline-end`, so its resize handle (at the
44
+ // panel's own inline-start edge) is physically on the LEFT in LTR and
45
+ // the RIGHT in RTL. Dragging it away from the panel's pinned edge must
46
+ // grow the panel either way -- physically LEFT in LTR (dir -1, so a
47
+ // negative `clientX` delta adds width), physically RIGHT in RTL (dir
48
+ // +1) -- the mirror image of DataTable's own column-resize grip, which
49
+ // is anchored at inset-inline-end instead (#214's own comment there).
50
+ const dir = isRtlElement(e.currentTarget) ? 1 : -1;
51
+ resizeRef.current = { x: e.clientX, width, dir };
52
+ e.target.setPointerCapture(e.pointerId);
53
+ };
54
+ const onResizeMove = (e) => {
55
+ const r = resizeRef.current;
56
+ if (!r)
57
+ return;
58
+ setWidth(r.width + r.dir * (e.clientX - r.x));
59
+ };
60
+ const onResizeEnd = (e) => {
61
+ resizeRef.current = null;
62
+ e.target.releasePointerCapture(e.pointerId);
63
+ };
64
+ // Which sections (#413) are expanded -- uncontrolled and independent
65
+ // (Attio's own Emails/Notes/Tasks open without closing one another, the
66
+ // same as Highlights cards never affecting each other): a plain `Set`,
67
+ // not modelled on Accordion's own single/multiple split, since a
68
+ // consumer here has no reason to want only one open at a time.
69
+ const [openSections, setOpenSections] = React.useState(() => new Set());
70
+ const toggleSection = (key) => setOpenSections((prev) => {
71
+ const next = new Set(prev);
72
+ if (next.has(key))
73
+ next.delete(key);
74
+ else
75
+ next.add(key);
76
+ return next;
77
+ });
19
78
  // close on Escape while open -- but not when a Menu/Select/Popover/
20
79
  // ContextMenu opened from inside the panel (e.g. a footer "More actions"
21
80
  // menu) is itself open (#361): that Escape belongs to the nested layer,
@@ -50,6 +109,10 @@ export function RecordPanel({ record, onClose, className, closeLabel = "Close pa
50
109
  if (!record)
51
110
  return null;
52
111
  const heading = title ?? record.name ?? "Record";
53
- return (_jsxs("div", { className: recordpanelRecipe({ className }), ...rest, role: "dialog", "aria-labelledby": titleId, children: [_jsxs("div", { className: "crisp-recordpanel-toolbar", children: [_jsx("button", { type: "button", className: "crisp-recordpanel-iconbtn", "aria-label": closeLabel, onClick: onClose, children: _jsx(X, { size: 16, strokeWidth: 1.75 }) }), (onPrevious || onNext) && (_jsxs("span", { className: "crisp-recordpanel-pager", children: [_jsx("button", { type: "button", className: "crisp-recordpanel-navbtn", "aria-label": previousRecordLabel, disabled: !onPrevious, onClick: onPrevious, children: _jsx(ChevronUp, { size: 16, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpanel-navbtn", "aria-label": nextRecordLabel, disabled: !onNext, onClick: onNext, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75 }) })] })), _jsx("span", { className: "crisp-recordpanel-toolbar-spacer" }), onOpenRecord && (_jsx("button", { type: "button", className: "crisp-recordpanel-navbtn", "aria-label": openRecordLabel, onClick: onOpenRecord, children: _jsx(ArrowUpRight, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75 }) }))] }), _jsxs("div", { className: "crisp-recordpanel-header", children: [avatar ? (_jsx("img", { className: "crisp-recordpanel-fav", src: avatar, alt: "" })) : (_jsx(Avatar, { size: "md", shape: "rounded", tone: avatarTone, children: (record.name ?? "?").charAt(0) })), _jsx("span", { id: titleId, className: "crisp-recordpanel-title", children: heading })] }), _jsxs("div", { className: "crisp-recordpanel-body", children: [highlights && highlights.length > 0 && (_jsxs("div", { className: "crisp-recordpanel-section", children: [_jsx("div", { className: "crisp-recordpanel-section-head", children: _jsx("span", { className: "crisp-recordpanel-section-label", children: highlightsLabel }) }), _jsx("div", { className: "crisp-recordpanel-highlights", children: highlights.map((h) => (_jsxs("div", { className: "crisp-recordpanel-highlight", children: [_jsxs("span", { className: "crisp-recordpanel-highlight-label", children: [_jsx("span", { children: h.label }), h.icon ? _jsx(h.icon, { size: 16, strokeWidth: 1.75 }) : null] }), _jsx("span", { className: "crisp-recordpanel-highlight-value", children: h.value })] }, h.key))) })] })), activity != null && (_jsxs("div", { className: "crisp-recordpanel-section", children: [_jsxs("div", { className: "crisp-recordpanel-section-head", children: [_jsx("span", { className: "crisp-recordpanel-section-label", children: activityLabel }), onViewAllActivity && (_jsx("button", { type: "button", className: "crisp-recordpanel-section-action", onClick: onViewAllActivity, children: viewAllActivityLabel }))] }), _jsx("div", { className: "crisp-recordpanel-activity", children: activity })] }))] }), footer != null && (_jsx("div", { className: "crisp-recordpanel-footer", children: footer }))] }));
112
+ return (_jsxs("div", { className: recordpanelRecipe({ className }), style: resizable ? { width, maxWidth: "calc(92% - 8px)" } : undefined, ...rest, role: "dialog", "aria-labelledby": titleId, children: [resizable && (_jsx("div", { className: "crisp-recordpanel-resize-handle", onPointerDown: onResizeStart, onPointerMove: onResizeMove, onPointerUp: onResizeEnd })), _jsxs("div", { className: "crisp-recordpanel-toolbar", children: [_jsx("button", { type: "button", className: "crisp-recordpanel-iconbtn", "aria-label": closeLabel, onClick: onClose, children: _jsx(X, { size: 16, strokeWidth: 1.75 }) }), (onPrevious || onNext) && (_jsxs("span", { className: "crisp-recordpanel-pager", children: [_jsx("button", { type: "button", className: "crisp-recordpanel-navbtn", "aria-label": previousRecordLabel, disabled: !onPrevious, onClick: onPrevious, children: _jsx(ChevronUp, { size: 16, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpanel-navbtn", "aria-label": nextRecordLabel, disabled: !onNext, onClick: onNext, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75 }) })] })), counterLabel != null && (_jsx("span", { className: "crisp-recordpanel-counter", children: counterLabel })), _jsx("span", { className: "crisp-recordpanel-toolbar-spacer" }), onOpenRecord && (_jsx("button", { type: "button", className: "crisp-recordpanel-navbtn", "aria-label": openRecordLabel, onClick: onOpenRecord, children: _jsx(ArrowUpRight, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75 }) }))] }), _jsxs("div", { className: "crisp-recordpanel-header", children: [avatar ? (_jsx("img", { className: "crisp-recordpanel-fav", src: avatar, alt: "" })) : (_jsx(Avatar, { size: "md", shape: "rounded", tone: avatarTone, children: (record.name ?? "?").charAt(0) })), _jsx("span", { id: titleId, className: "crisp-recordpanel-title", children: heading })] }), _jsxs("div", { className: "crisp-recordpanel-body", children: [highlights && highlights.length > 0 && (_jsxs("div", { className: "crisp-recordpanel-section", children: [_jsx("div", { className: "crisp-recordpanel-section-head", children: _jsx("span", { className: "crisp-recordpanel-section-label", children: highlightsLabel }) }), _jsx("div", { className: "crisp-recordpanel-highlights", children: highlights.map((h) => (_jsxs("div", { className: "crisp-recordpanel-highlight", children: [_jsxs("span", { className: "crisp-recordpanel-highlight-label", children: [_jsx("span", { children: h.label }), h.icon ? _jsx(h.icon, { size: 16, strokeWidth: 1.75 }) : null] }), _jsx("span", { className: "crisp-recordpanel-highlight-value", children: h.value })] }, h.key))) })] })), activity != null && (_jsxs("div", { className: "crisp-recordpanel-section", children: [_jsxs("div", { className: "crisp-recordpanel-section-head", children: [_jsx("span", { className: "crisp-recordpanel-section-label", children: activityLabel }), onViewAllActivity && (_jsx("button", { type: "button", className: "crisp-recordpanel-section-action", onClick: onViewAllActivity, children: viewAllActivityLabel }))] }), _jsx("div", { className: "crisp-recordpanel-activity", children: activity })] })), sections && sections.length > 0 && (_jsx("div", { className: "crisp-recordpanel-sections", children: sections.map((s) => {
113
+ const isOpen = openSections.has(s.key);
114
+ const panelId = `${titleId}-section-${s.key}`;
115
+ return (_jsxs("div", { className: "crisp-recordpanel-section-row", children: [s.content ? (_jsxs("button", { type: "button", className: "crisp-recordpanel-section-trigger", "aria-expanded": isOpen, "aria-controls": panelId, onClick: () => toggleSection(s.key), children: [_jsx("span", { className: "crisp-recordpanel-section-row-label", children: s.label }), _jsx("span", { className: "crisp-recordpanel-section-row-count", children: s.count }), _jsx(ChevronDown, { className: "crisp-recordpanel-section-chevron", size: 14, strokeWidth: 1.75, "data-open": isOpen ? "" : undefined, "aria-hidden": "true" })] })) : (_jsxs("div", { className: "crisp-recordpanel-section-trigger crisp-recordpanel-section-trigger-inert", children: [_jsx("span", { className: "crisp-recordpanel-section-row-label", children: s.label }), _jsx("span", { className: "crisp-recordpanel-section-row-count", children: s.count })] })), s.content && isOpen && (_jsx("div", { id: panelId, className: "crisp-recordpanel-section-content", children: s.content }))] }, s.key));
116
+ }) }))] }), footer != null && (_jsx("div", { className: "crisp-recordpanel-footer", children: footer }))] }));
54
117
  }
55
118
  RecordPanel.displayName = "RecordPanel";
@@ -29,12 +29,27 @@ const config = defineRecipe({
29
29
  & .crisp-recordpanel-toolbar { display: flex; align-items: center; height: 49px; flex-shrink: 0; padding: ${c.space["2"]} ${c.space["2.5"]}; border-bottom: 1px solid ${c.borderSubtle}; }
30
30
  & .crisp-recordpanel-toolbar-spacer { flex: 1; }
31
31
  & .crisp-recordpanel-pager { display: flex; align-items: center; gap: ${c.space["1.5"]}; margin-inline-start: ${c.space["3"]}; }
32
+ /* #413: "2 of 37 in All Companies" -- measured live, same toolbar row,
33
+ right after Prev/Next. 12px/500, the same 63%-of-fgPrimary mix every
34
+ other muted heading in this panel already uses. */
35
+ & .crisp-recordpanel-counter { margin-inline-start: ${c.space["3"]}; font-size: 12px; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
32
36
  & .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: ${c.radius.md}; background: transparent; color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s; }
33
37
  & .crisp-recordpanel-iconbtn:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
34
38
  & .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: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: ${e.field}; color: ${c.fgPrimary}; font-family: inherit; cursor: pointer; }
35
39
  & .crisp-recordpanel-navbtn:hover:not(:disabled) { background: ${c.bgRaised}; }
36
40
  & .crisp-recordpanel-navbtn:disabled { opacity: 0.4; cursor: not-allowed; }
37
41
 
42
+ /* #413: measured live -- a 10px strip at the panel's own left edge,
43
+ cursor: col-resize (the issue's own repro calls it "ew-resize";
44
+ the actual computed value read off the live DOM is col-resize, the
45
+ same cursor family under a different keyword). Kept fully inside the
46
+ panel's own box, not extending past inset-inline-start: 0, so the
47
+ panel's own overflow: hidden (there for its rounded corners) never
48
+ clips it. touch-action: none stops a touchscreen's own pan gesture
49
+ from fighting the drag. */
50
+ & .crisp-recordpanel-resize-handle { position: absolute; top: 0; bottom: 0; inset-inline-start: 0; width: 10px; cursor: col-resize; z-index: 1; touch-action: none; }
51
+ & .crisp-recordpanel-resize-handle:hover, & .crisp-recordpanel-resize-handle:active { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); }
52
+
38
53
  & .crisp-recordpanel-header { display: flex; align-items: center; gap: ${c.space["2"]}; height: 52px; flex-shrink: 0; padding: 0 ${c.space["3"]}; }
39
54
  & .crisp-recordpanel-fav { width: 22px; height: ${c.size.chip}; border-radius: ${c.radius.sm}; flex-shrink: 0; object-fit: cover; background: ${c.bgRaised}; }
40
55
  & .crisp-recordpanel-title { flex: 1; min-width: 0; font-size: ${c.text.xl}; font-weight: ${c.weight.semibold}; letter-spacing: -0.2px; color: ${c.fgPrimary}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -73,6 +88,28 @@ const config = defineRecipe({
73
88
  section heading above. */
74
89
  & .crisp-recordpanel-activity { font-size: ${c.text.lg}; }
75
90
 
91
+ /* #413: collapsed Emails/Notes/Tasks below Activity -- measured live: a
92
+ 473px-wide row (the panel's own content width, 497 minus its 12px/8px
93
+ side insets), 20px tall, 14px/500 solid fgPrimary label (NOT the 63%
94
+ mix Highlights/Activity headings use -- this is body text, not a
95
+ muted heading), a trailing count. padding "0 8px 0 12px" measured on
96
+ the row itself; reproduced here as the trigger's own padding, since
97
+ the trigger IS the row (a plain div, not a button, when there's no
98
+ content to expand into -- matches the empty ("Emails 0") row
99
+ measured live, which has no expand affordance at all). */
100
+ & .crisp-recordpanel-sections { display: flex; flex-direction: column; padding-top: ${c.space["2"]}; }
101
+ & .crisp-recordpanel-section-trigger { display: flex; align-items: center; gap: ${c.space["1"]}; width: 100%; height: 20px; padding: 0 ${c.space["2"]} 0 ${c.space["3"]}; border: none; background: none; font-family: inherit; text-align: start; cursor: pointer; }
102
+ & .crisp-recordpanel-section-trigger-inert { cursor: default; }
103
+ & .crisp-recordpanel-section-row-label { font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }
104
+ /* The count wasn't captured with its own background/pill chrome in the
105
+ live measurement (only its text: 14px, a "tertiary" internal variant
106
+ name that doesn't map to anything crisp defines) -- muted text next
107
+ to the label, not an invented badge shape. */
108
+ & .crisp-recordpanel-section-row-count { font-size: ${c.text.lg}; color: ${c.fgMuted}; }
109
+ & .crisp-recordpanel-section-chevron { margin-inline-start: auto; flex-shrink: 0; color: ${c.fgMuted}; transition: transform .15s ease; }
110
+ & .crisp-recordpanel-section-chevron[data-open] { transform: rotate(180deg); }
111
+ & .crisp-recordpanel-section-content { padding: ${c.space["2"]} ${c.space["2"]} ${c.space["2"]} ${c.space["3"]}; }
112
+
76
113
  /* #329: an optional slot for record actions (e.g. Edit). Reuses the
77
114
  header's own measured values rather than inventing new ones -- the
78
115
  same horizontal inset (space.3), the same hairline border token
package/dist/inbox.css CHANGED
@@ -1,5 +1,5 @@
1
1
  .crisp-inbox {
2
- display: flex; flex-direction: column; box-sizing: border-box; width: 380px; max-height: 588px; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); overflow: hidden; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent), 0 8px 28px -6px rgba(28,40,64,0.16), 0 2px 6px -2px rgba(28,40,64,0.10); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
2
+ display: flex; flex-direction: column; box-sizing: border-box; width: 380px; max-height: 588px; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); overflow: hidden; box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
3
3
  & .crisp-inbox-head { padding: var(--crisp-space-3) var(--crisp-space-3) 0; }
4
4
  & .crisp-inbox-title { font-size: var(--crisp-text-xl); line-height: 28px; font-weight: var(--crisp-weight-semibold); letter-spacing: -0.3px; color: var(--crisp-fg-primary); }
5
5
  & .crisp-inbox-tabs { display: flex; gap: var(--crisp-space-3); margin-top: var(--crisp-space-2); box-shadow: inset 0 -1px 0 var(--crisp-border-subtle); }
@@ -1,6 +1,6 @@
1
1
  @layer crisp-components {
2
2
  .crisp-inbox {
3
- display: flex; flex-direction: column; box-sizing: border-box; width: 380px; max-height: 588px; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); overflow: hidden; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent), 0 8px 28px -6px rgba(28,40,64,0.16), 0 2px 6px -2px rgba(28,40,64,0.10); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
3
+ display: flex; flex-direction: column; box-sizing: border-box; width: 380px; max-height: 588px; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); overflow: hidden; box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
4
4
  & .crisp-inbox-head { padding: var(--crisp-space-3) var(--crisp-space-3) 0; }
5
5
  & .crisp-inbox-title { font-size: var(--crisp-text-xl); line-height: 28px; font-weight: var(--crisp-weight-semibold); letter-spacing: -0.3px; color: var(--crisp-fg-primary); }
6
6
  & .crisp-inbox-tabs { display: flex; gap: var(--crisp-space-3); margin-top: var(--crisp-space-2); box-shadow: inset 0 -1px 0 var(--crisp-border-subtle); }
@@ -12,41 +12,62 @@
12
12
  & .crisp-recordpanel-toolbar { display: flex; align-items: center; height: 49px; flex-shrink: 0; padding: var(--crisp-space-2) var(--crisp-space-2-5); border-bottom: 1px solid var(--crisp-border-subtle); }
13
13
  & .crisp-recordpanel-toolbar-spacer { flex: 1; }
14
14
  & .crisp-recordpanel-pager { display: flex; align-items: center; gap: var(--crisp-space-1-5); margin-inline-start: var(--crisp-space-3); }
15
+ /* #413: "2 of 37 in All Companies" -- measured live, same toolbar row, right after Prev/Next. 12px/500, the same 63%-of-fgPrimary mix every other muted heading in this panel already uses. */
16
+ & .crisp-recordpanel-counter { margin-inline-start: var(--crisp-space-3); font-size: 12px; font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
15
17
  & .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: var(--crisp-radius-md); background: transparent; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s; }
16
18
  & .crisp-recordpanel-iconbtn:hover { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
17
19
  & .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: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); color: var(--crisp-fg-primary); font-family: inherit; cursor: pointer; }
18
20
  & .crisp-recordpanel-navbtn:hover:not(:disabled) { background: var(--crisp-bg-raised); }
19
21
  & .crisp-recordpanel-navbtn:disabled { opacity: 0.4; cursor: not-allowed; }
22
+ /* #413: measured live -- a 10px strip at the panel's own left edge,
23
+ cursor: col-resize (the issue's own repro calls it "ew-resize"; the actual computed value read off the live DOM is col-resize, the same cursor family under a different keyword). Kept fully inside the panel's own box, not extending past inset-inline-start: 0, so the
24
+ panel's own overflow: hidden (there for its rounded corners) never clips it. touch-action: none stops a touchscreen's own pan gesture
25
+ from fighting the drag. */
26
+ & .crisp-recordpanel-resize-handle { position: absolute; top: 0; bottom: 0; inset-inline-start: 0; width: 10px; cursor: col-resize; z-index: 1; touch-action: none; }
27
+ & .crisp-recordpanel-resize-handle:hover, & .crisp-recordpanel-resize-handle:active { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
28
+
20
29
  & .crisp-recordpanel-header { display: flex; align-items: center; gap: var(--crisp-space-2); height: 52px; flex-shrink: 0; padding: 0 var(--crisp-space-3); }
21
30
  & .crisp-recordpanel-fav { width: 22px; height: var(--crisp-size-chip); border-radius: var(--crisp-radius-sm); flex-shrink: 0; object-fit: cover; background: var(--crisp-bg-raised); }
22
31
  & .crisp-recordpanel-title { flex: 1; min-width: 0; font-size: var(--crisp-text-xl); font-weight: var(--crisp-weight-semibold); letter-spacing: -0.2px; color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
32
+
23
33
  & .crisp-recordpanel-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--crisp-space-2) var(--crisp-space-3); }
24
- /* #448: measured live on app.attio.com's peek (hover a Companies row,
25
- click "Open preview"). Section label reads identically for both
26
- Highlights and Activity: 14px/500, the same 63%-of-fgPrimary mix
27
- RecordPage's own field-label already uses (product/recordpage's
28
- .crisp-recordpage-field-label) -- one more surface landing on the
29
- same measured "muted section heading" colour. */
34
+
35
+ /* #448: measured live on app.attio.com's peek (hover a Companies row, click "Open preview"). Section label reads identically for both Highlights and Activity: 14px/500, the same 63%-of-fgPrimary mix RecordPage's own field-label already uses (product/recordpage's .crisp-recordpage-field-label) -- one more surface landing on the same measured "muted section heading" colour. */
30
36
  & .crisp-recordpanel-section { padding: var(--crisp-space-2) var(--crisp-space-1) 0; }
31
37
  & .crisp-recordpanel-section-head { display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-2); height: 20px; margin-bottom: var(--crisp-space-2); }
32
38
  & .crisp-recordpanel-section-label { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
33
- /* "View all" -- measured 10px/400, a lighter mix than the section label
34
- (55%, not 63%) -- its own distinct, honestly-measured value, not a
35
- reuse of a nearby one that happened to be close. */
39
+ /* "View all" -- measured 10px/400, a lighter mix than the section label (55%, not 63%) -- its own distinct, honestly-measured value, not a reuse of a nearby one that happened to be close. */
36
40
  & .crisp-recordpanel-section-action { border: none; background: none; padding: 0; font-family: inherit; font-size: 10px; font-weight: var(--crisp-weight-regular); color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); cursor: pointer; }
37
41
  & .crisp-recordpanel-section-action:hover { color: var(--crisp-fg-primary); }
38
-
39
- /* Highlights: a 2-column grid of label+icon+value cards, no card
40
- chrome at all (no border/background/radius/shadow -- confirmed on
41
- the live DOM: every highlight's computed style is fully transparent). 8px row/column gap; two rows measured "233x86" inside 473px content width is exactly (473 - 8) / 2. */
42
+ /* Highlights: a 2-column grid of label+icon+value cards, no card chrome at all (no border/background/radius/shadow -- confirmed on the live DOM: every highlight's computed style is fully transparent).
43
+ 8px row/column gap; two rows measured "233x86" inside 473px content
44
+ width is exactly (473 - 8) / 2. */
42
45
  & .crisp-recordpanel-highlights { display: grid; grid-template-columns: 1fr 1fr; gap: var(--crisp-space-2); }
43
46
  & .crisp-recordpanel-highlight { display: flex; flex-direction: column; gap: var(--crisp-space-1-5); min-width: 0; }
44
47
  & .crisp-recordpanel-highlight-label { display: flex; align-items: center; gap: var(--crisp-space-1); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
45
48
  & .crisp-recordpanel-highlight-label span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
46
49
  & .crisp-recordpanel-highlight-label svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
47
50
  & .crisp-recordpanel-highlight-value { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; }
48
- /* Activity is a consumer-supplied slot (crisp has no activity-feed model of its own to render) -- this only wraps it with the measured section heading above. */
51
+
52
+ /* Activity is a consumer-supplied slot (crisp has no activity-feed
53
+ model of its own to render) -- this only wraps it with the measured
54
+ section heading above. */
49
55
  & .crisp-recordpanel-activity { font-size: var(--crisp-text-lg); }
56
+
57
+ /* #413: collapsed Emails/Notes/Tasks below Activity -- measured live: a
58
+ 473px-wide row (the panel's own content width, 497 minus its 12px/8px side insets), 20px tall, 14px/500 solid fgPrimary label (NOT the 63% mix Highlights/Activity headings use -- this is body text, not a muted heading), a trailing count. padding "0 8px 0 12px" measured on the row itself; reproduced here as the trigger's own padding, since
59
+ the trigger IS the row (a plain div, not a button, when there's no content to expand into -- matches the empty ("Emails 0") row measured live, which has no expand affordance at all). */
60
+ & .crisp-recordpanel-sections { display: flex; flex-direction: column; padding-top: var(--crisp-space-2); }
61
+ & .crisp-recordpanel-section-trigger { display: flex; align-items: center; gap: var(--crisp-space-1); width: 100%; height: 20px; padding: 0 var(--crisp-space-2) 0 var(--crisp-space-3); border: none; background: none; font-family: inherit; text-align: start; cursor: pointer; }
62
+ & .crisp-recordpanel-section-trigger-inert { cursor: default; }
63
+ & .crisp-recordpanel-section-row-label { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
64
+ /* The count wasn't captured with its own background/pill chrome in the
65
+ live measurement (only its text: 14px, a "tertiary" internal variant
66
+ name that doesn't map to anything crisp defines) -- muted text next to the label, not an invented badge shape. */
67
+ & .crisp-recordpanel-section-row-count { font-size: var(--crisp-text-lg); color: var(--crisp-fg-muted); }
68
+ & .crisp-recordpanel-section-chevron { margin-inline-start: auto; flex-shrink: 0; color: var(--crisp-fg-muted); transition: transform .15s ease; }
69
+ & .crisp-recordpanel-section-chevron[data-open] { transform: rotate(180deg); }
70
+ & .crisp-recordpanel-section-content { padding: var(--crisp-space-2) var(--crisp-space-2) var(--crisp-space-2) var(--crisp-space-3); }
50
71
  /* #329: an optional slot for record actions (e.g. Edit). Reuses the header's own measured values rather than inventing new ones -- the
51
72
  same horizontal inset (space.3), the same hairline border token
52
73
  (borderSubtle, flipped to border-top since this sits at the opposite
@@ -13,41 +13,62 @@
13
13
  & .crisp-recordpanel-toolbar { display: flex; align-items: center; height: 49px; flex-shrink: 0; padding: var(--crisp-space-2) var(--crisp-space-2-5); border-bottom: 1px solid var(--crisp-border-subtle); }
14
14
  & .crisp-recordpanel-toolbar-spacer { flex: 1; }
15
15
  & .crisp-recordpanel-pager { display: flex; align-items: center; gap: var(--crisp-space-1-5); margin-inline-start: var(--crisp-space-3); }
16
+ /* #413: "2 of 37 in All Companies" -- measured live, same toolbar row, right after Prev/Next. 12px/500, the same 63%-of-fgPrimary mix every other muted heading in this panel already uses. */
17
+ & .crisp-recordpanel-counter { margin-inline-start: var(--crisp-space-3); font-size: 12px; font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
16
18
  & .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: var(--crisp-radius-md); background: transparent; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s; }
17
19
  & .crisp-recordpanel-iconbtn:hover { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
18
20
  & .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: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); color: var(--crisp-fg-primary); font-family: inherit; cursor: pointer; }
19
21
  & .crisp-recordpanel-navbtn:hover:not(:disabled) { background: var(--crisp-bg-raised); }
20
22
  & .crisp-recordpanel-navbtn:disabled { opacity: 0.4; cursor: not-allowed; }
23
+ /* #413: measured live -- a 10px strip at the panel's own left edge,
24
+ cursor: col-resize (the issue's own repro calls it "ew-resize"; the actual computed value read off the live DOM is col-resize, the same cursor family under a different keyword). Kept fully inside the panel's own box, not extending past inset-inline-start: 0, so the
25
+ panel's own overflow: hidden (there for its rounded corners) never clips it. touch-action: none stops a touchscreen's own pan gesture
26
+ from fighting the drag. */
27
+ & .crisp-recordpanel-resize-handle { position: absolute; top: 0; bottom: 0; inset-inline-start: 0; width: 10px; cursor: col-resize; z-index: 1; touch-action: none; }
28
+ & .crisp-recordpanel-resize-handle:hover, & .crisp-recordpanel-resize-handle:active { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
29
+
21
30
  & .crisp-recordpanel-header { display: flex; align-items: center; gap: var(--crisp-space-2); height: 52px; flex-shrink: 0; padding: 0 var(--crisp-space-3); }
22
31
  & .crisp-recordpanel-fav { width: 22px; height: var(--crisp-size-chip); border-radius: var(--crisp-radius-sm); flex-shrink: 0; object-fit: cover; background: var(--crisp-bg-raised); }
23
32
  & .crisp-recordpanel-title { flex: 1; min-width: 0; font-size: var(--crisp-text-xl); font-weight: var(--crisp-weight-semibold); letter-spacing: -0.2px; color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
33
+
24
34
  & .crisp-recordpanel-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--crisp-space-2) var(--crisp-space-3); }
25
- /* #448: measured live on app.attio.com's peek (hover a Companies row,
26
- click "Open preview"). Section label reads identically for both
27
- Highlights and Activity: 14px/500, the same 63%-of-fgPrimary mix
28
- RecordPage's own field-label already uses (product/recordpage's
29
- .crisp-recordpage-field-label) -- one more surface landing on the
30
- same measured "muted section heading" colour. */
35
+
36
+ /* #448: measured live on app.attio.com's peek (hover a Companies row, click "Open preview"). Section label reads identically for both Highlights and Activity: 14px/500, the same 63%-of-fgPrimary mix RecordPage's own field-label already uses (product/recordpage's .crisp-recordpage-field-label) -- one more surface landing on the same measured "muted section heading" colour. */
31
37
  & .crisp-recordpanel-section { padding: var(--crisp-space-2) var(--crisp-space-1) 0; }
32
38
  & .crisp-recordpanel-section-head { display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-2); height: 20px; margin-bottom: var(--crisp-space-2); }
33
39
  & .crisp-recordpanel-section-label { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
34
- /* "View all" -- measured 10px/400, a lighter mix than the section label
35
- (55%, not 63%) -- its own distinct, honestly-measured value, not a
36
- reuse of a nearby one that happened to be close. */
40
+ /* "View all" -- measured 10px/400, a lighter mix than the section label (55%, not 63%) -- its own distinct, honestly-measured value, not a reuse of a nearby one that happened to be close. */
37
41
  & .crisp-recordpanel-section-action { border: none; background: none; padding: 0; font-family: inherit; font-size: 10px; font-weight: var(--crisp-weight-regular); color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); cursor: pointer; }
38
42
  & .crisp-recordpanel-section-action:hover { color: var(--crisp-fg-primary); }
39
-
40
- /* Highlights: a 2-column grid of label+icon+value cards, no card
41
- chrome at all (no border/background/radius/shadow -- confirmed on
42
- the live DOM: every highlight's computed style is fully transparent). 8px row/column gap; two rows measured "233x86" inside 473px content width is exactly (473 - 8) / 2. */
43
+ /* Highlights: a 2-column grid of label+icon+value cards, no card chrome at all (no border/background/radius/shadow -- confirmed on the live DOM: every highlight's computed style is fully transparent).
44
+ 8px row/column gap; two rows measured "233x86" inside 473px content
45
+ width is exactly (473 - 8) / 2. */
43
46
  & .crisp-recordpanel-highlights { display: grid; grid-template-columns: 1fr 1fr; gap: var(--crisp-space-2); }
44
47
  & .crisp-recordpanel-highlight { display: flex; flex-direction: column; gap: var(--crisp-space-1-5); min-width: 0; }
45
48
  & .crisp-recordpanel-highlight-label { display: flex; align-items: center; gap: var(--crisp-space-1); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
46
49
  & .crisp-recordpanel-highlight-label span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
47
50
  & .crisp-recordpanel-highlight-label svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
48
51
  & .crisp-recordpanel-highlight-value { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; }
49
- /* Activity is a consumer-supplied slot (crisp has no activity-feed model of its own to render) -- this only wraps it with the measured section heading above. */
52
+
53
+ /* Activity is a consumer-supplied slot (crisp has no activity-feed
54
+ model of its own to render) -- this only wraps it with the measured
55
+ section heading above. */
50
56
  & .crisp-recordpanel-activity { font-size: var(--crisp-text-lg); }
57
+
58
+ /* #413: collapsed Emails/Notes/Tasks below Activity -- measured live: a
59
+ 473px-wide row (the panel's own content width, 497 minus its 12px/8px side insets), 20px tall, 14px/500 solid fgPrimary label (NOT the 63% mix Highlights/Activity headings use -- this is body text, not a muted heading), a trailing count. padding "0 8px 0 12px" measured on the row itself; reproduced here as the trigger's own padding, since
60
+ the trigger IS the row (a plain div, not a button, when there's no content to expand into -- matches the empty ("Emails 0") row measured live, which has no expand affordance at all). */
61
+ & .crisp-recordpanel-sections { display: flex; flex-direction: column; padding-top: var(--crisp-space-2); }
62
+ & .crisp-recordpanel-section-trigger { display: flex; align-items: center; gap: var(--crisp-space-1); width: 100%; height: 20px; padding: 0 var(--crisp-space-2) 0 var(--crisp-space-3); border: none; background: none; font-family: inherit; text-align: start; cursor: pointer; }
63
+ & .crisp-recordpanel-section-trigger-inert { cursor: default; }
64
+ & .crisp-recordpanel-section-row-label { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
65
+ /* The count wasn't captured with its own background/pill chrome in the
66
+ live measurement (only its text: 14px, a "tertiary" internal variant
67
+ name that doesn't map to anything crisp defines) -- muted text next to the label, not an invented badge shape. */
68
+ & .crisp-recordpanel-section-row-count { font-size: var(--crisp-text-lg); color: var(--crisp-fg-muted); }
69
+ & .crisp-recordpanel-section-chevron { margin-inline-start: auto; flex-shrink: 0; color: var(--crisp-fg-muted); transition: transform .15s ease; }
70
+ & .crisp-recordpanel-section-chevron[data-open] { transform: rotate(180deg); }
71
+ & .crisp-recordpanel-section-content { padding: var(--crisp-space-2) var(--crisp-space-2) var(--crisp-space-2) var(--crisp-space-3); }
51
72
  /* #329: an optional slot for record actions (e.g. Edit). Reuses the header's own measured values rather than inventing new ones -- the
52
73
  same horizontal inset (space.3), the same hairline border token
53
74
  (borderSubtle, flipped to border-top since this sits at the opposite
package/dist/styles.css CHANGED
@@ -1968,7 +1968,7 @@
1968
1968
  }
1969
1969
 
1970
1970
  .crisp-inbox {
1971
- display: flex; flex-direction: column; box-sizing: border-box; width: 380px; max-height: 588px; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); overflow: hidden; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent), 0 8px 28px -6px rgba(28,40,64,0.16), 0 2px 6px -2px rgba(28,40,64,0.10); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1971
+ display: flex; flex-direction: column; box-sizing: border-box; width: 380px; max-height: 588px; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); overflow: hidden; box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1972
1972
  & .crisp-inbox-head { padding: var(--crisp-space-3) var(--crisp-space-3) 0; }
1973
1973
  & .crisp-inbox-title { font-size: var(--crisp-text-xl); line-height: 28px; font-weight: var(--crisp-weight-semibold); letter-spacing: -0.3px; color: var(--crisp-fg-primary); }
1974
1974
  & .crisp-inbox-tabs { display: flex; gap: var(--crisp-space-3); margin-top: var(--crisp-space-2); box-shadow: inset 0 -1px 0 var(--crisp-border-subtle); }
@@ -2265,41 +2265,62 @@
2265
2265
  & .crisp-recordpanel-toolbar { display: flex; align-items: center; height: 49px; flex-shrink: 0; padding: var(--crisp-space-2) var(--crisp-space-2-5); border-bottom: 1px solid var(--crisp-border-subtle); }
2266
2266
  & .crisp-recordpanel-toolbar-spacer { flex: 1; }
2267
2267
  & .crisp-recordpanel-pager { display: flex; align-items: center; gap: var(--crisp-space-1-5); margin-inline-start: var(--crisp-space-3); }
2268
+ /* #413: "2 of 37 in All Companies" -- measured live, same toolbar row, right after Prev/Next. 12px/500, the same 63%-of-fgPrimary mix every other muted heading in this panel already uses. */
2269
+ & .crisp-recordpanel-counter { margin-inline-start: var(--crisp-space-3); font-size: 12px; font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
2268
2270
  & .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: var(--crisp-radius-md); background: transparent; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s; }
2269
2271
  & .crisp-recordpanel-iconbtn:hover { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
2270
2272
  & .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: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); color: var(--crisp-fg-primary); font-family: inherit; cursor: pointer; }
2271
2273
  & .crisp-recordpanel-navbtn:hover:not(:disabled) { background: var(--crisp-bg-raised); }
2272
2274
  & .crisp-recordpanel-navbtn:disabled { opacity: 0.4; cursor: not-allowed; }
2275
+ /* #413: measured live -- a 10px strip at the panel's own left edge,
2276
+ cursor: col-resize (the issue's own repro calls it "ew-resize"; the actual computed value read off the live DOM is col-resize, the same cursor family under a different keyword). Kept fully inside the panel's own box, not extending past inset-inline-start: 0, so the
2277
+ panel's own overflow: hidden (there for its rounded corners) never clips it. touch-action: none stops a touchscreen's own pan gesture
2278
+ from fighting the drag. */
2279
+ & .crisp-recordpanel-resize-handle { position: absolute; top: 0; bottom: 0; inset-inline-start: 0; width: 10px; cursor: col-resize; z-index: 1; touch-action: none; }
2280
+ & .crisp-recordpanel-resize-handle:hover, & .crisp-recordpanel-resize-handle:active { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
2281
+
2273
2282
  & .crisp-recordpanel-header { display: flex; align-items: center; gap: var(--crisp-space-2); height: 52px; flex-shrink: 0; padding: 0 var(--crisp-space-3); }
2274
2283
  & .crisp-recordpanel-fav { width: 22px; height: var(--crisp-size-chip); border-radius: var(--crisp-radius-sm); flex-shrink: 0; object-fit: cover; background: var(--crisp-bg-raised); }
2275
2284
  & .crisp-recordpanel-title { flex: 1; min-width: 0; font-size: var(--crisp-text-xl); font-weight: var(--crisp-weight-semibold); letter-spacing: -0.2px; color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2285
+
2276
2286
  & .crisp-recordpanel-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--crisp-space-2) var(--crisp-space-3); }
2277
- /* #448: measured live on app.attio.com's peek (hover a Companies row,
2278
- click "Open preview"). Section label reads identically for both
2279
- Highlights and Activity: 14px/500, the same 63%-of-fgPrimary mix
2280
- RecordPage's own field-label already uses (product/recordpage's
2281
- .crisp-recordpage-field-label) -- one more surface landing on the
2282
- same measured "muted section heading" colour. */
2287
+
2288
+ /* #448: measured live on app.attio.com's peek (hover a Companies row, click "Open preview"). Section label reads identically for both Highlights and Activity: 14px/500, the same 63%-of-fgPrimary mix RecordPage's own field-label already uses (product/recordpage's .crisp-recordpage-field-label) -- one more surface landing on the same measured "muted section heading" colour. */
2283
2289
  & .crisp-recordpanel-section { padding: var(--crisp-space-2) var(--crisp-space-1) 0; }
2284
2290
  & .crisp-recordpanel-section-head { display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-2); height: 20px; margin-bottom: var(--crisp-space-2); }
2285
2291
  & .crisp-recordpanel-section-label { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
2286
- /* "View all" -- measured 10px/400, a lighter mix than the section label
2287
- (55%, not 63%) -- its own distinct, honestly-measured value, not a
2288
- reuse of a nearby one that happened to be close. */
2292
+ /* "View all" -- measured 10px/400, a lighter mix than the section label (55%, not 63%) -- its own distinct, honestly-measured value, not a reuse of a nearby one that happened to be close. */
2289
2293
  & .crisp-recordpanel-section-action { border: none; background: none; padding: 0; font-family: inherit; font-size: 10px; font-weight: var(--crisp-weight-regular); color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); cursor: pointer; }
2290
2294
  & .crisp-recordpanel-section-action:hover { color: var(--crisp-fg-primary); }
2291
-
2292
- /* Highlights: a 2-column grid of label+icon+value cards, no card
2293
- chrome at all (no border/background/radius/shadow -- confirmed on
2294
- the live DOM: every highlight's computed style is fully transparent). 8px row/column gap; two rows measured "233x86" inside 473px content width is exactly (473 - 8) / 2. */
2295
+ /* Highlights: a 2-column grid of label+icon+value cards, no card chrome at all (no border/background/radius/shadow -- confirmed on the live DOM: every highlight's computed style is fully transparent).
2296
+ 8px row/column gap; two rows measured "233x86" inside 473px content
2297
+ width is exactly (473 - 8) / 2. */
2295
2298
  & .crisp-recordpanel-highlights { display: grid; grid-template-columns: 1fr 1fr; gap: var(--crisp-space-2); }
2296
2299
  & .crisp-recordpanel-highlight { display: flex; flex-direction: column; gap: var(--crisp-space-1-5); min-width: 0; }
2297
2300
  & .crisp-recordpanel-highlight-label { display: flex; align-items: center; gap: var(--crisp-space-1); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
2298
2301
  & .crisp-recordpanel-highlight-label span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2299
2302
  & .crisp-recordpanel-highlight-label svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
2300
2303
  & .crisp-recordpanel-highlight-value { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; }
2301
- /* Activity is a consumer-supplied slot (crisp has no activity-feed model of its own to render) -- this only wraps it with the measured section heading above. */
2304
+
2305
+ /* Activity is a consumer-supplied slot (crisp has no activity-feed
2306
+ model of its own to render) -- this only wraps it with the measured
2307
+ section heading above. */
2302
2308
  & .crisp-recordpanel-activity { font-size: var(--crisp-text-lg); }
2309
+
2310
+ /* #413: collapsed Emails/Notes/Tasks below Activity -- measured live: a
2311
+ 473px-wide row (the panel's own content width, 497 minus its 12px/8px side insets), 20px tall, 14px/500 solid fgPrimary label (NOT the 63% mix Highlights/Activity headings use -- this is body text, not a muted heading), a trailing count. padding "0 8px 0 12px" measured on the row itself; reproduced here as the trigger's own padding, since
2312
+ the trigger IS the row (a plain div, not a button, when there's no content to expand into -- matches the empty ("Emails 0") row measured live, which has no expand affordance at all). */
2313
+ & .crisp-recordpanel-sections { display: flex; flex-direction: column; padding-top: var(--crisp-space-2); }
2314
+ & .crisp-recordpanel-section-trigger { display: flex; align-items: center; gap: var(--crisp-space-1); width: 100%; height: 20px; padding: 0 var(--crisp-space-2) 0 var(--crisp-space-3); border: none; background: none; font-family: inherit; text-align: start; cursor: pointer; }
2315
+ & .crisp-recordpanel-section-trigger-inert { cursor: default; }
2316
+ & .crisp-recordpanel-section-row-label { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
2317
+ /* The count wasn't captured with its own background/pill chrome in the
2318
+ live measurement (only its text: 14px, a "tertiary" internal variant
2319
+ name that doesn't map to anything crisp defines) -- muted text next to the label, not an invented badge shape. */
2320
+ & .crisp-recordpanel-section-row-count { font-size: var(--crisp-text-lg); color: var(--crisp-fg-muted); }
2321
+ & .crisp-recordpanel-section-chevron { margin-inline-start: auto; flex-shrink: 0; color: var(--crisp-fg-muted); transition: transform .15s ease; }
2322
+ & .crisp-recordpanel-section-chevron[data-open] { transform: rotate(180deg); }
2323
+ & .crisp-recordpanel-section-content { padding: var(--crisp-space-2) var(--crisp-space-2) var(--crisp-space-2) var(--crisp-space-3); }
2303
2324
  /* #329: an optional slot for record actions (e.g. Edit). Reuses the header's own measured values rather than inventing new ones -- the
2304
2325
  same horizontal inset (space.3), the same hairline border token
2305
2326
  (borderSubtle, flipped to border-top since this sits at the opposite
@@ -1969,7 +1969,7 @@
1969
1969
  }
1970
1970
 
1971
1971
  .crisp-inbox {
1972
- display: flex; flex-direction: column; box-sizing: border-box; width: 380px; max-height: 588px; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); overflow: hidden; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent), 0 8px 28px -6px rgba(28,40,64,0.16), 0 2px 6px -2px rgba(28,40,64,0.10); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1972
+ display: flex; flex-direction: column; box-sizing: border-box; width: 380px; max-height: 588px; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); overflow: hidden; box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1973
1973
  & .crisp-inbox-head { padding: var(--crisp-space-3) var(--crisp-space-3) 0; }
1974
1974
  & .crisp-inbox-title { font-size: var(--crisp-text-xl); line-height: 28px; font-weight: var(--crisp-weight-semibold); letter-spacing: -0.3px; color: var(--crisp-fg-primary); }
1975
1975
  & .crisp-inbox-tabs { display: flex; gap: var(--crisp-space-3); margin-top: var(--crisp-space-2); box-shadow: inset 0 -1px 0 var(--crisp-border-subtle); }
@@ -2266,41 +2266,62 @@
2266
2266
  & .crisp-recordpanel-toolbar { display: flex; align-items: center; height: 49px; flex-shrink: 0; padding: var(--crisp-space-2) var(--crisp-space-2-5); border-bottom: 1px solid var(--crisp-border-subtle); }
2267
2267
  & .crisp-recordpanel-toolbar-spacer { flex: 1; }
2268
2268
  & .crisp-recordpanel-pager { display: flex; align-items: center; gap: var(--crisp-space-1-5); margin-inline-start: var(--crisp-space-3); }
2269
+ /* #413: "2 of 37 in All Companies" -- measured live, same toolbar row, right after Prev/Next. 12px/500, the same 63%-of-fgPrimary mix every other muted heading in this panel already uses. */
2270
+ & .crisp-recordpanel-counter { margin-inline-start: var(--crisp-space-3); font-size: 12px; font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
2269
2271
  & .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: var(--crisp-radius-md); background: transparent; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s; }
2270
2272
  & .crisp-recordpanel-iconbtn:hover { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
2271
2273
  & .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: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); color: var(--crisp-fg-primary); font-family: inherit; cursor: pointer; }
2272
2274
  & .crisp-recordpanel-navbtn:hover:not(:disabled) { background: var(--crisp-bg-raised); }
2273
2275
  & .crisp-recordpanel-navbtn:disabled { opacity: 0.4; cursor: not-allowed; }
2276
+ /* #413: measured live -- a 10px strip at the panel's own left edge,
2277
+ cursor: col-resize (the issue's own repro calls it "ew-resize"; the actual computed value read off the live DOM is col-resize, the same cursor family under a different keyword). Kept fully inside the panel's own box, not extending past inset-inline-start: 0, so the
2278
+ panel's own overflow: hidden (there for its rounded corners) never clips it. touch-action: none stops a touchscreen's own pan gesture
2279
+ from fighting the drag. */
2280
+ & .crisp-recordpanel-resize-handle { position: absolute; top: 0; bottom: 0; inset-inline-start: 0; width: 10px; cursor: col-resize; z-index: 1; touch-action: none; }
2281
+ & .crisp-recordpanel-resize-handle:hover, & .crisp-recordpanel-resize-handle:active { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
2282
+
2274
2283
  & .crisp-recordpanel-header { display: flex; align-items: center; gap: var(--crisp-space-2); height: 52px; flex-shrink: 0; padding: 0 var(--crisp-space-3); }
2275
2284
  & .crisp-recordpanel-fav { width: 22px; height: var(--crisp-size-chip); border-radius: var(--crisp-radius-sm); flex-shrink: 0; object-fit: cover; background: var(--crisp-bg-raised); }
2276
2285
  & .crisp-recordpanel-title { flex: 1; min-width: 0; font-size: var(--crisp-text-xl); font-weight: var(--crisp-weight-semibold); letter-spacing: -0.2px; color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2286
+
2277
2287
  & .crisp-recordpanel-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--crisp-space-2) var(--crisp-space-3); }
2278
- /* #448: measured live on app.attio.com's peek (hover a Companies row,
2279
- click "Open preview"). Section label reads identically for both
2280
- Highlights and Activity: 14px/500, the same 63%-of-fgPrimary mix
2281
- RecordPage's own field-label already uses (product/recordpage's
2282
- .crisp-recordpage-field-label) -- one more surface landing on the
2283
- same measured "muted section heading" colour. */
2288
+
2289
+ /* #448: measured live on app.attio.com's peek (hover a Companies row, click "Open preview"). Section label reads identically for both Highlights and Activity: 14px/500, the same 63%-of-fgPrimary mix RecordPage's own field-label already uses (product/recordpage's .crisp-recordpage-field-label) -- one more surface landing on the same measured "muted section heading" colour. */
2284
2290
  & .crisp-recordpanel-section { padding: var(--crisp-space-2) var(--crisp-space-1) 0; }
2285
2291
  & .crisp-recordpanel-section-head { display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-2); height: 20px; margin-bottom: var(--crisp-space-2); }
2286
2292
  & .crisp-recordpanel-section-label { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
2287
- /* "View all" -- measured 10px/400, a lighter mix than the section label
2288
- (55%, not 63%) -- its own distinct, honestly-measured value, not a
2289
- reuse of a nearby one that happened to be close. */
2293
+ /* "View all" -- measured 10px/400, a lighter mix than the section label (55%, not 63%) -- its own distinct, honestly-measured value, not a reuse of a nearby one that happened to be close. */
2290
2294
  & .crisp-recordpanel-section-action { border: none; background: none; padding: 0; font-family: inherit; font-size: 10px; font-weight: var(--crisp-weight-regular); color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); cursor: pointer; }
2291
2295
  & .crisp-recordpanel-section-action:hover { color: var(--crisp-fg-primary); }
2292
-
2293
- /* Highlights: a 2-column grid of label+icon+value cards, no card
2294
- chrome at all (no border/background/radius/shadow -- confirmed on
2295
- the live DOM: every highlight's computed style is fully transparent). 8px row/column gap; two rows measured "233x86" inside 473px content width is exactly (473 - 8) / 2. */
2296
+ /* Highlights: a 2-column grid of label+icon+value cards, no card chrome at all (no border/background/radius/shadow -- confirmed on the live DOM: every highlight's computed style is fully transparent).
2297
+ 8px row/column gap; two rows measured "233x86" inside 473px content
2298
+ width is exactly (473 - 8) / 2. */
2296
2299
  & .crisp-recordpanel-highlights { display: grid; grid-template-columns: 1fr 1fr; gap: var(--crisp-space-2); }
2297
2300
  & .crisp-recordpanel-highlight { display: flex; flex-direction: column; gap: var(--crisp-space-1-5); min-width: 0; }
2298
2301
  & .crisp-recordpanel-highlight-label { display: flex; align-items: center; gap: var(--crisp-space-1); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
2299
2302
  & .crisp-recordpanel-highlight-label span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2300
2303
  & .crisp-recordpanel-highlight-label svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
2301
2304
  & .crisp-recordpanel-highlight-value { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; }
2302
- /* Activity is a consumer-supplied slot (crisp has no activity-feed model of its own to render) -- this only wraps it with the measured section heading above. */
2305
+
2306
+ /* Activity is a consumer-supplied slot (crisp has no activity-feed
2307
+ model of its own to render) -- this only wraps it with the measured
2308
+ section heading above. */
2303
2309
  & .crisp-recordpanel-activity { font-size: var(--crisp-text-lg); }
2310
+
2311
+ /* #413: collapsed Emails/Notes/Tasks below Activity -- measured live: a
2312
+ 473px-wide row (the panel's own content width, 497 minus its 12px/8px side insets), 20px tall, 14px/500 solid fgPrimary label (NOT the 63% mix Highlights/Activity headings use -- this is body text, not a muted heading), a trailing count. padding "0 8px 0 12px" measured on the row itself; reproduced here as the trigger's own padding, since
2313
+ the trigger IS the row (a plain div, not a button, when there's no content to expand into -- matches the empty ("Emails 0") row measured live, which has no expand affordance at all). */
2314
+ & .crisp-recordpanel-sections { display: flex; flex-direction: column; padding-top: var(--crisp-space-2); }
2315
+ & .crisp-recordpanel-section-trigger { display: flex; align-items: center; gap: var(--crisp-space-1); width: 100%; height: 20px; padding: 0 var(--crisp-space-2) 0 var(--crisp-space-3); border: none; background: none; font-family: inherit; text-align: start; cursor: pointer; }
2316
+ & .crisp-recordpanel-section-trigger-inert { cursor: default; }
2317
+ & .crisp-recordpanel-section-row-label { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
2318
+ /* The count wasn't captured with its own background/pill chrome in the
2319
+ live measurement (only its text: 14px, a "tertiary" internal variant
2320
+ name that doesn't map to anything crisp defines) -- muted text next to the label, not an invented badge shape. */
2321
+ & .crisp-recordpanel-section-row-count { font-size: var(--crisp-text-lg); color: var(--crisp-fg-muted); }
2322
+ & .crisp-recordpanel-section-chevron { margin-inline-start: auto; flex-shrink: 0; color: var(--crisp-fg-muted); transition: transform .15s ease; }
2323
+ & .crisp-recordpanel-section-chevron[data-open] { transform: rotate(180deg); }
2324
+ & .crisp-recordpanel-section-content { padding: var(--crisp-space-2) var(--crisp-space-2) var(--crisp-space-2) var(--crisp-space-3); }
2304
2325
  /* #329: an optional slot for record actions (e.g. Edit). Reuses the header's own measured values rather than inventing new ones -- the
2305
2326
  same horizontal inset (space.3), the same hairline border token
2306
2327
  (borderSubtle, flipped to border-top since this sits at the opposite
@@ -5,6 +5,20 @@ type IconType = React.ComponentType<{
5
5
  size?: number;
6
6
  strokeWidth?: number;
7
7
  }>;
8
+ /** One collapsed section below Activity (#413) — Attio's own peek shows
9
+ * Emails/Notes/Tasks this way, each a label + a count, expanding into the
10
+ * consumer's own content when clicked. crisp has no data model for any of
11
+ * these (no fixed "what is an email/note/task" shape), so both the count
12
+ * and the expanded content are fully consumer-supplied. Omitting `content`
13
+ * renders the row but not as a button — matches the live DOM measured on an
14
+ * empty section (Emails, count 0): no chevron, no click handler, nothing to
15
+ * expand into yet. */
16
+ export type RecordPanelSection = {
17
+ key: string;
18
+ label: string;
19
+ count: number;
20
+ content?: React.ReactNode;
21
+ };
8
22
  /** One Highlights card — label, value and an optional icon, laid out the
9
23
  * way Attio's peek panel lays its own out (measured live, #448): a 2-column
10
24
  * grid, no card chrome (no border/background/shadow), a muted label row
@@ -34,6 +48,31 @@ export type RecordPanelProps = Omit<React.ComponentPropsWithoutRef<"div">, "chil
34
48
  previousRecordLabel?: string;
35
49
  onNext?: () => void;
36
50
  nextRecordLabel?: string;
51
+ /**
52
+ * "2 of 37 in All Companies" (#413) — measured live in the same toolbar
53
+ * row as Prev/Next, right after them. A single pre-formatted string: crisp
54
+ * has no concept of "the current view" a consumer's own table is showing
55
+ * (GRC's own filtered/sorted row order, say), so the index/total/scope-name
56
+ * composition is entirely the consumer's. Omit for no counter at all.
57
+ */
58
+ counterLabel?: React.ReactNode;
59
+ /**
60
+ * Lets a person drag the panel wider or narrower from its left edge
61
+ * (#413 — measured live: a 10px `cursor: col-resize` strip at the panel's
62
+ * own left edge). Uncontrolled by default (the panel keeps its own width
63
+ * in state, starting from `defaultWidth`); pass `width`/`onWidthChange`
64
+ * together to own it instead — same controlled/uncontrolled split as
65
+ * every other stateful crisp prop. `minWidth`/`maxWidth` bound the drag;
66
+ * Attio's own bounds weren't captured (dragging live in a read-only
67
+ * measurement pass isn't something this drag should do to someone else's
68
+ * workspace), so these are a judgement call, not a measured value.
69
+ */
70
+ resizable?: boolean;
71
+ width?: number;
72
+ defaultWidth?: number;
73
+ onWidthChange?: (width: number) => void;
74
+ minWidth?: number;
75
+ maxWidth?: number;
37
76
  /**
38
77
  * The panel's heading, and so its accessible name (via `aria-labelledby`,
39
78
  * not a stringified `aria-label` — it also drives the visible title, which
@@ -77,6 +116,15 @@ export type RecordPanelProps = Omit<React.ComponentPropsWithoutRef<"div">, "chil
77
116
  * for no Activity section at all.
78
117
  */
79
118
  activity?: React.ReactNode;
119
+ /**
120
+ * Collapsed sections below Activity (#413) — Attio's own peek shows
121
+ * Emails/Notes/Tasks this way, each a label + count that expands into its
122
+ * own content on click. crisp has no data model for any of these (an
123
+ * email/note/task has no fixed shape crisp could render generically), so
124
+ * this is fully consumer-supplied — GRC's own equivalent is its Notes and
125
+ * Tasks tabs; it has no Emails. Omit for no sections at all.
126
+ */
127
+ sections?: RecordPanelSection[];
80
128
  /** Rendered below the panel's content, e.g. an Edit button. Omit for no footer. */
81
129
  footer?: React.ReactNode;
82
130
  onClose: () => void;
@@ -92,7 +140,7 @@ export type RecordPanelProps = Omit<React.ComponentPropsWithoutRef<"div">, "chil
92
140
  * place). Render it inside a `position: relative` container (e.g. the app
93
141
  * main area); it slides in from the right and overlays the content.
94
142
  */
95
- export declare function RecordPanel({ record, onClose, className, closeLabel, onOpenRecord, openRecordLabel, onPrevious, previousRecordLabel, onNext, nextRecordLabel, title, avatar, avatarTone, highlightsLabel, highlights, activityLabel, viewAllActivityLabel, onViewAllActivity, activity, footer, ...rest }: RecordPanelProps): React.JSX.Element | null;
143
+ export declare function RecordPanel({ record, onClose, className, closeLabel, onOpenRecord, openRecordLabel, onPrevious, previousRecordLabel, onNext, nextRecordLabel, title, avatar, avatarTone, highlightsLabel, highlights, counterLabel, resizable, width: controlledWidth, defaultWidth, onWidthChange, minWidth, maxWidth, activityLabel, viewAllActivityLabel, onViewAllActivity, activity, sections, footer, ...rest }: RecordPanelProps): React.JSX.Element | null;
96
144
  export declare namespace RecordPanel {
97
145
  var displayName: string;
98
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.52.2",
3
+ "version": "0.52.4",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",