@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.
- package/dist/cjs/product/inbox/inbox.recipe.js +6 -1
- package/dist/cjs/product/recordpanel/recordpanel.js +65 -2
- package/dist/cjs/product/recordpanel/recordpanel.recipe.js +37 -0
- package/dist/esm/product/inbox/inbox.recipe.js +7 -2
- package/dist/esm/product/recordpanel/recordpanel.js +66 -3
- package/dist/esm/product/recordpanel/recordpanel.recipe.js +37 -0
- package/dist/inbox.css +1 -1
- package/dist/inbox.layered.css +1 -1
- package/dist/recordpanel.css +35 -14
- package/dist/recordpanel.layered.css +35 -14
- package/dist/styles.css +36 -15
- package/dist/styles.layered.css +36 -15
- package/dist/types/product/recordpanel/recordpanel.d.ts +49 -1
- package/package.json +1 -1
|
@@ -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:
|
|
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 })] }))
|
|
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:
|
|
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 })] }))
|
|
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
|
|
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); }
|
package/dist/inbox.layered.css
CHANGED
|
@@ -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
|
|
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); }
|
package/dist/recordpanel.css
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2278
|
-
|
|
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
|
-
|
|
2293
|
-
|
|
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
|
-
|
|
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
|
package/dist/styles.layered.css
CHANGED
|
@@ -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
|
|
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
|
-
|
|
2279
|
-
|
|
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
|
-
|
|
2294
|
-
|
|
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
|
-
|
|
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
|
}
|