@crisp-ui-kit/crisp 0.51.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/cjs/core/caret-rect.js +73 -0
  2. package/dist/cjs/core/index.js +7 -1
  3. package/dist/cjs/core/mention.js +32 -0
  4. package/dist/cjs/core/use-mention-trigger.js +124 -0
  5. package/dist/cjs/product/recordpanel/recordpanel.js +13 -81
  6. package/dist/cjs/product/recordpanel/recordpanel.recipe.js +53 -44
  7. package/dist/cjs/product/recordstable/editable-field.js +11 -8
  8. package/dist/cjs/product/recordstable/recordstable.tag.js +8 -3
  9. package/dist/cjs/product/richtext/index.js +2 -1
  10. package/dist/cjs/product/richtext/richtext.js +440 -23
  11. package/dist/cjs/product/richtext/richtext.recipe.js +59 -9
  12. package/dist/esm/core/caret-rect.js +70 -0
  13. package/dist/esm/core/index.js +3 -0
  14. package/dist/esm/core/mention.js +29 -0
  15. package/dist/esm/core/use-mention-trigger.js +88 -0
  16. package/dist/esm/product/recordpanel/index.js +1 -1
  17. package/dist/esm/product/recordpanel/recordpanel.js +15 -83
  18. package/dist/esm/product/recordpanel/recordpanel.recipe.js +54 -45
  19. package/dist/esm/product/recordstable/editable-field.js +11 -8
  20. package/dist/esm/product/recordstable/recordstable.tag.js +8 -3
  21. package/dist/esm/product/richtext/index.js +1 -1
  22. package/dist/esm/product/richtext/richtext.js +439 -22
  23. package/dist/esm/product/richtext/richtext.recipe.js +60 -10
  24. package/dist/recordpanel.css +47 -41
  25. package/dist/recordpanel.layered.css +47 -41
  26. package/dist/richtext.css +31 -5
  27. package/dist/richtext.layered.css +31 -5
  28. package/dist/styles.css +79 -47
  29. package/dist/styles.layered.css +79 -47
  30. package/dist/types/core/caret-rect.d.ts +16 -0
  31. package/dist/types/core/index.d.ts +3 -0
  32. package/dist/types/core/mention.d.ts +27 -0
  33. package/dist/types/core/use-mention-trigger.d.ts +41 -0
  34. package/dist/types/product/recordpanel/index.d.ts +1 -1
  35. package/dist/types/product/recordpanel/recordpanel.d.ts +81 -27
  36. package/dist/types/product/recordstable/editable-field.d.ts +11 -8
  37. package/dist/types/product/recordstable/recordstable.tag.d.ts +8 -3
  38. package/dist/types/product/richtext/index.d.ts +1 -1
  39. package/dist/types/product/richtext/richtext.d.ts +168 -7
  40. package/package.json +1 -1
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.caretRectInField = caretRectInField;
4
+ /**
5
+ * The pixel position of a caret index inside a `<textarea>`/`<input>`, in
6
+ * viewport coordinates — where a mention picker anchors.
7
+ *
8
+ * Neither element exposes caret geometry directly, so this uses the standard
9
+ * mirror-div technique: an off-screen div replicating every computed style
10
+ * that affects text layout (font, padding, border, wrapping), holding the
11
+ * same text up to the caret plus a marker span. The marker's own
12
+ * `getBoundingClientRect()`, translated back onto the real field's box, is
13
+ * the caret's actual rendered position — nothing here guesses at line
14
+ * height or character width itself.
15
+ */
16
+ function caretRectInField(field, index) {
17
+ const div = document.createElement("div");
18
+ const style = window.getComputedStyle(field);
19
+ const mirrored = [
20
+ "boxSizing",
21
+ "width",
22
+ "paddingTop",
23
+ "paddingRight",
24
+ "paddingBottom",
25
+ "paddingLeft",
26
+ "borderTopWidth",
27
+ "borderRightWidth",
28
+ "borderBottomWidth",
29
+ "borderLeftWidth",
30
+ "fontFamily",
31
+ "fontSize",
32
+ "fontWeight",
33
+ "fontStyle",
34
+ "letterSpacing",
35
+ "lineHeight",
36
+ "textTransform",
37
+ "textIndent",
38
+ "textAlign",
39
+ "wordSpacing",
40
+ "tabSize",
41
+ ];
42
+ for (const prop of mirrored)
43
+ div.style.setProperty(prop, style[prop]);
44
+ div.style.position = "absolute";
45
+ div.style.visibility = "hidden";
46
+ div.style.top = "0";
47
+ div.style.left = "-9999px";
48
+ div.style.whiteSpace =
49
+ field instanceof HTMLTextAreaElement ? "pre-wrap" : "pre";
50
+ div.style.wordWrap = "break-word";
51
+ div.style.overflow = "hidden";
52
+ div.style.height = "auto";
53
+ const value = field.value;
54
+ div.textContent = value.slice(0, index);
55
+ const marker = document.createElement("span");
56
+ // a trailing empty span collapses to zero width and reports the wrong x —
57
+ // give it a hair of content so its box sits exactly at the caret
58
+ marker.textContent = value.slice(index, index + 1) || ".";
59
+ div.appendChild(marker);
60
+ document.body.appendChild(div);
61
+ div.scrollTop = field.scrollTop;
62
+ div.scrollLeft = field.scrollLeft;
63
+ const fieldRect = field.getBoundingClientRect();
64
+ const divRect = div.getBoundingClientRect();
65
+ const markerRect = marker.getBoundingClientRect();
66
+ const x = fieldRect.left + (markerRect.left - divRect.left) - div.scrollLeft;
67
+ const y = fieldRect.top +
68
+ (markerRect.top - divRect.top) -
69
+ div.scrollTop +
70
+ markerRect.height;
71
+ document.body.removeChild(div);
72
+ return { x, y };
73
+ }
@@ -14,7 +14,9 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.usePrefersReducedMotion = exports.useLabelAssociation = exports.useFormReset = exports.useFocusAfterRemoval = exports.specToRecipe = exports.defineSpec = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.formValueStyle = exports.registerFloatingLayer = exports.isInsideLaterFloatingLayer = exports.hasOpenFloatingLayer = exports.FloatingPortal = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = exports.parseCellDate = exports.formatCellDate = exports.cellDatePrecision = void 0;
17
+ exports.usePrefersReducedMotion = exports.useMentionTrigger = exports.useLabelAssociation = exports.useFormReset = exports.useFocusAfterRemoval = exports.specToRecipe = exports.defineSpec = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.matchMentionTrigger = exports.formValueStyle = exports.registerFloatingLayer = exports.isInsideLaterFloatingLayer = exports.hasOpenFloatingLayer = exports.FloatingPortal = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = exports.parseCellDate = exports.formatCellDate = exports.cellDatePrecision = exports.caretRectInField = void 0;
18
+ var caret_rect_1 = require("./caret-rect");
19
+ Object.defineProperty(exports, "caretRectInField", { enumerable: true, get: function () { return caret_rect_1.caretRectInField; } });
18
20
  var cell_date_1 = require("./cell-date");
19
21
  Object.defineProperty(exports, "cellDatePrecision", { enumerable: true, get: function () { return cell_date_1.cellDatePrecision; } });
20
22
  Object.defineProperty(exports, "formatCellDate", { enumerable: true, get: function () { return cell_date_1.formatCellDate; } });
@@ -35,6 +37,8 @@ Object.defineProperty(exports, "isInsideLaterFloatingLayer", { enumerable: true,
35
37
  Object.defineProperty(exports, "registerFloatingLayer", { enumerable: true, get: function () { return floating_stack_1.registerFloatingLayer; } });
36
38
  var form_value_1 = require("./form-value");
37
39
  Object.defineProperty(exports, "formValueStyle", { enumerable: true, get: function () { return form_value_1.formValueStyle; } });
40
+ var mention_1 = require("./mention");
41
+ Object.defineProperty(exports, "matchMentionTrigger", { enumerable: true, get: function () { return mention_1.matchMentionTrigger; } });
38
42
  var place_floating_1 = require("./place-floating");
39
43
  Object.defineProperty(exports, "placeFloating", { enumerable: true, get: function () { return place_floating_1.placeFloating; } });
40
44
  var primitive_1 = require("./primitive");
@@ -51,5 +55,7 @@ var use_form_reset_1 = require("./use-form-reset");
51
55
  Object.defineProperty(exports, "useFormReset", { enumerable: true, get: function () { return use_form_reset_1.useFormReset; } });
52
56
  var use_label_association_1 = require("./use-label-association");
53
57
  Object.defineProperty(exports, "useLabelAssociation", { enumerable: true, get: function () { return use_label_association_1.useLabelAssociation; } });
58
+ var use_mention_trigger_1 = require("./use-mention-trigger");
59
+ Object.defineProperty(exports, "useMentionTrigger", { enumerable: true, get: function () { return use_mention_trigger_1.useMentionTrigger; } });
54
60
  var use_prefers_reduced_motion_1 = require("./use-prefers-reduced-motion");
55
61
  Object.defineProperty(exports, "usePrefersReducedMotion", { enumerable: true, get: function () { return use_prefers_reduced_motion_1.usePrefersReducedMotion; } });
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.matchMentionTrigger = matchMentionTrigger;
4
+ /**
5
+ * Is there an active "@query" mention trigger ending at the caret?
6
+ *
7
+ * Matches the rule every "@mention" implementation (GitHub, Slack, Linear)
8
+ * actually uses: the trigger character must start a word — the very first
9
+ * character, or preceded by whitespace — and nothing between it and the
10
+ * caret may contain whitespace. `foo@bar` is an email/handle stuck to a word,
11
+ * not a trigger; `@jo next` has already moved past the mention it typed.
12
+ *
13
+ * Takes plain text rather than a live DOM node so it is one pure function
14
+ * usable against a `<textarea>`'s `value` and a `contentEditable` text
15
+ * node's content alike, and is the one thing here worth unit-testing on its
16
+ * own — everything downstream of it (caret geometry, DOM insertion) needs a
17
+ * real browser to verify.
18
+ */
19
+ function matchMentionTrigger(textBeforeCaret, trigger = "@") {
20
+ if (!trigger)
21
+ return null;
22
+ const start = textBeforeCaret.lastIndexOf(trigger);
23
+ if (start === -1)
24
+ return null;
25
+ const before = start > 0 ? textBeforeCaret[start - 1] : undefined;
26
+ if (before !== undefined && !/\s/.test(before))
27
+ return null;
28
+ const query = textBeforeCaret.slice(start + trigger.length);
29
+ if (/\s/.test(query))
30
+ return null;
31
+ return { query, start };
32
+ }
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.useMentionTrigger = useMentionTrigger;
37
+ const React = __importStar(require("react"));
38
+ const caret_rect_1 = require("./caret-rect");
39
+ const mention_1 = require("./mention");
40
+ /**
41
+ * Watches a `<textarea>`/`<input>` for an "@query" mention trigger and
42
+ * reports where to anchor a consumer-supplied picker (#372).
43
+ *
44
+ * crisp draws no picker of its own here — GitHub/Slack/Linear-style @mention
45
+ * UI varies too much by app (avatars, groups, recent-first ordering, a
46
+ * "create new person" row) to have one measured shape, and this repo's rule
47
+ * is measure-first, never invented polish. What is shared and worth owning
48
+ * centrally is the fiddly, easy-to-get-wrong part: deciding when "@" is
49
+ * actually a trigger rather than part of a word or an email address (see
50
+ * `matchMentionTrigger`), and where its picker anchors on screen (see
51
+ * `caretRectInField`, since neither element exposes caret geometry itself).
52
+ *
53
+ * Does not close on blur: a picker built from this needs to survive the
54
+ * field losing focus to a `mousedown` on one of its own rows, the same
55
+ * problem every floating surface in this repo solves for itself (see
56
+ * `RichTextEditor`'s toolbar `keepSel`, or `Popover`'s own outside-pointer-
57
+ * down listener) — so closing on outside interaction is left to the
58
+ * consumer's picker, which already knows what counts as "outside" for its
59
+ * own markup.
60
+ */
61
+ function useMentionTrigger(ref, { trigger = "@" } = {}) {
62
+ const [state, setState] = React.useState({
63
+ open: false,
64
+ query: "",
65
+ anchor: null,
66
+ });
67
+ const matchRef = React.useRef(null);
68
+ const recompute = React.useCallback(() => {
69
+ const el = ref.current;
70
+ if (!el || el.selectionStart == null) {
71
+ matchRef.current = null;
72
+ setState({ open: false, query: "", anchor: null });
73
+ return;
74
+ }
75
+ const caret = el.selectionStart;
76
+ const match = (0, mention_1.matchMentionTrigger)(el.value.slice(0, caret), trigger);
77
+ matchRef.current = match;
78
+ if (!match) {
79
+ setState({ open: false, query: "", anchor: null });
80
+ return;
81
+ }
82
+ setState({
83
+ open: true,
84
+ query: match.query,
85
+ anchor: (0, caret_rect_1.caretRectInField)(el, caret),
86
+ });
87
+ }, [ref, trigger]);
88
+ React.useEffect(() => {
89
+ const el = ref.current;
90
+ if (!el)
91
+ return;
92
+ // "input" covers typing; "click"/"keyup" cover the caret moving without
93
+ // the text changing (arrow keys, clicking elsewhere on the same line).
94
+ el.addEventListener("input", recompute);
95
+ el.addEventListener("click", recompute);
96
+ el.addEventListener("keyup", recompute);
97
+ return () => {
98
+ el.removeEventListener("input", recompute);
99
+ el.removeEventListener("click", recompute);
100
+ el.removeEventListener("keyup", recompute);
101
+ };
102
+ }, [ref, recompute]);
103
+ const close = React.useCallback(() => {
104
+ matchRef.current = null;
105
+ setState({ open: false, query: "", anchor: null });
106
+ }, []);
107
+ const insert = React.useCallback((text) => {
108
+ const el = ref.current;
109
+ const match = matchRef.current;
110
+ if (!el || !match)
111
+ return;
112
+ const caret = el.selectionStart ?? el.value.length;
113
+ const before = el.value.slice(0, match.start);
114
+ const after = el.value.slice(caret);
115
+ el.value = before + text + after;
116
+ const cursor = before.length + text.length;
117
+ el.focus();
118
+ el.setSelectionRange(cursor, cursor);
119
+ // a controlled consumer listens on "input", the same event typing fires
120
+ el.dispatchEvent(new Event("input", { bubbles: true }));
121
+ close();
122
+ }, [ref, close]);
123
+ return { ...state, close, insert };
124
+ }
@@ -38,37 +38,20 @@ const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const lucide_react_1 = require("lucide-react");
39
39
  const React = __importStar(require("react"));
40
40
  const avatar_1 = require("../../components/avatar");
41
- const select_1 = require("../../components/select");
42
- const spinner_1 = require("../../components/spinner");
43
- const switch_1 = require("../../components/switch");
44
41
  const core_1 = require("../../core");
45
- const recordstable_1 = require("../recordstable");
46
42
  const recordpanel_recipe_1 = require("./recordpanel.recipe");
47
- function FieldValue({ col, row, emptyLabel, }) {
48
- const v = row[col.key];
49
- if (v == null || v === "" || (Array.isArray(v) && !v.length)) {
50
- return (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-empty", children: emptyLabel });
51
- }
52
- switch (col.type) {
53
- case "tags":
54
- return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: v.map((t) => ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordstable-tag", "data-tone": (0, recordstable_1.tagTone)(t), children: t }, t))) }));
55
- case "number":
56
- return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: typeof v === "number" ? v.toLocaleString() : v });
57
- case "url":
58
- return ((0, jsx_runtime_1.jsx)("a", { className: "crisp-recordpanel-link", href: `https://${v}`, target: "_blank", rel: "noreferrer", children: v }));
59
- default:
60
- return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: String(v) });
61
- }
62
- }
63
43
  /**
64
- * Right-side record peek panel — shows one record's fields, opened from a
65
- * RecordsTable row (via `onOpenRecord`). Render it inside a `position:
66
- * relative` container (e.g. the app main area); it slides in from the right and
67
- * overlays the content.
44
+ * Right-side record peek panel — Attio's own peek shows a Highlights card
45
+ * area and an Activity feed (measured live: hover a Companies row, click its
46
+ * "Open preview" trigger), never an editable field list (#448 — a "Details"
47
+ * list was this component's own invention before, and #431's editable API
48
+ * had nothing real in this panel to attach to; that API stays on
49
+ * `RecordPage`'s own Details column, where Attio does let you edit in
50
+ * place). Render it inside a `position: relative` container (e.g. the app
51
+ * main area); it slides in from the right and overlays the content.
68
52
  */
69
- function RecordPanel({ record, columns, onClose, className, closeLabel = "Close panel", title, detailsLabel = "Details", emptyLabel = "Empty", footer, onEdit, ...rest }) {
53
+ function RecordPanel({ record, onClose, className, closeLabel = "Close panel", onOpenRecord, openRecordLabel = "Open record", onPrevious, previousRecordLabel = "Previous record", onNext, nextRecordLabel = "Next record", title, avatar, avatarTone = "brand", highlightsLabel = "Highlights", highlights, activityLabel = "Activity", viewAllActivityLabel = "View all", onViewAllActivity, activity, footer, ...rest }) {
70
54
  const titleId = React.useId();
71
- const { editingKey, pendingKey, error, openEditing, closeEditing, commit } = (0, recordstable_1.useEditableField)(onEdit);
72
55
  // close on Escape while open -- but not when a Menu/Select/Popover/
73
56
  // ContextMenu opened from inside the panel (e.g. a footer "More actions"
74
57
  // menu) is itself open (#361): that Escape belongs to the nested layer,
@@ -93,67 +76,16 @@ function RecordPanel({ record, columns, onClose, className, closeLabel = "Close
93
76
  if (!record)
94
77
  return;
95
78
  const onKey = (e) => {
96
- if (e.key !== "Escape")
97
- return;
98
- // #431: Escape while a field is open cancels JUST that field, the
99
- // same way DataTable's own editable cells treat it -- not the whole
100
- // panel. Checked ahead of `hasOpenFloatingLayer()` (capture phase, so
101
- // this runs before the field's own bubble-phase onKeyDown would ever
102
- // get a chance to stopPropagation -- same reasoning as the
103
- // floating-layer check below).
104
- if (editingKey) {
105
- closeEditing();
106
- return;
107
- }
108
- if ((0, core_1.hasOpenFloatingLayer)())
79
+ if (e.key !== "Escape" || (0, core_1.hasOpenFloatingLayer)())
109
80
  return;
110
81
  onClose();
111
82
  };
112
83
  document.addEventListener("keydown", onKey, true);
113
84
  return () => document.removeEventListener("keydown", onKey, true);
114
- }, [record, onClose, editingKey, closeEditing]);
85
+ }, [record, onClose]);
115
86
  if (!record)
116
87
  return null;
117
- const entityCol = columns.find((c) => c.type === "entity");
118
- const entity = entityCol
119
- ? record[entityCol.key]
120
- : undefined;
121
- const fields = columns.filter((c) => c.type !== "entity");
122
- const heading = title ?? entity?.name ?? "Record";
123
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpanel_recipe_1.recordpanelRecipe)({ className }), ...rest, role: "dialog", "aria-labelledby": titleId, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-header", children: [entity?.avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpanel-fav", src: entity.avatar, alt: "" })) : entity ? ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "md", shape: "rounded", tone: "brand", children: entity.name.charAt(0) })) : null, (0, jsx_runtime_1.jsx)("span", { id: titleId, className: "crisp-recordpanel-title", children: heading }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-iconbtn", "aria-label": closeLabel, onClick: onClose, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 18, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-body", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-sectionlabel", children: detailsLabel }), fields.map((col) => {
124
- // #431: a field only actually opens when there's an `onEdit` to
125
- // commit through -- `editable` with nothing to call would be a
126
- // dead click.
127
- const canEdit = Boolean(col.editable && onEdit);
128
- const isPending = pendingKey === col.key;
129
- const fieldError = error?.key === col.key ? error.message : null;
130
- // select/switch are always their own live widget -- no click-to-
131
- // open, no separate edit/view mode, the same convention
132
- // DataTable's identical `editor` columns use (#385): choosing a
133
- // value or flipping a toggle IS the commit gesture.
134
- if (canEdit && col.editor) {
135
- const value = record[col.key];
136
- return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-field", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-field-label", children: [col.icon ? (0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 }) : null, col.label] }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-field-value", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-edit-picker", children: [col.editor === "select" ? ((0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", "aria-label": `Edit ${col.label}`, value: value != null ? String(value) : undefined, disabled: isPending, options: col.editOptions ?? [], onValueChange: (v) => commit(record.id, col.key, v) })) : ((0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": `Edit ${col.label}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commit(record.id, col.key, String(checked)) })), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-recordpanel-edit-pending" }))] }), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-edit-error", children: fieldError }))] })] }, col.key));
137
- }
138
- if (canEdit && editingKey === col.key) {
139
- const value = record[col.key];
140
- const editor = (0, recordstable_1.editableInputType)(col.type);
141
- return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-field", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-field-label", children: [col.icon ? (0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 }) : null, col.label] }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-field-value", children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-recordpanel-edit",
142
- // biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
143
- autoFocus: true, ...editor, disabled: isPending, defaultValue: value == null ? "" : String(value), "aria-label": `Edit ${col.label}`, onKeyDown: (e) => {
144
- if (isPending)
145
- return;
146
- if (e.key === "Enter")
147
- commit(record.id, col.key, e.currentTarget.value);
148
- else if (e.key === "Escape")
149
- closeEditing();
150
- }, onBlur: (e) => {
151
- if (isPending)
152
- return;
153
- commit(record.id, col.key, e.currentTarget.value);
154
- } }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-recordpanel-edit-pending" })), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-edit-error", children: fieldError }))] })] }, col.key));
155
- }
156
- return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-field", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-field-label", children: [col.icon ? (0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 }) : null, col.label] }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-field-value", "data-editable": canEdit ? "" : undefined, children: [canEdit ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-edit-trigger", onClick: () => openEditing(col.key), children: (0, jsx_runtime_1.jsx)(FieldValue, { col: col, row: record, emptyLabel: emptyLabel }) })) : ((0, jsx_runtime_1.jsx)(FieldValue, { col: col, row: record, emptyLabel: emptyLabel })), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-edit-error", children: fieldError }))] })] }, col.key));
157
- })] }), footer != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-footer", children: footer }))] }));
88
+ const heading = title ?? record.name ?? "Record";
89
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpanel_recipe_1.recordpanelRecipe)({ className }), ...rest, role: "dialog", "aria-labelledby": titleId, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-toolbar", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-iconbtn", "aria-label": closeLabel, onClick: onClose, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 16, strokeWidth: 1.75 }) }), (onPrevious || onNext) && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-pager", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-navbtn", "aria-label": previousRecordLabel, disabled: !onPrevious, onClick: onPrevious, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronUp, { size: 16, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-navbtn", "aria-label": nextRecordLabel, disabled: !onNext, onClick: onNext, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 16, strokeWidth: 1.75 }) })] })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-toolbar-spacer" }), onOpenRecord && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-navbtn", "aria-label": openRecordLabel, onClick: onOpenRecord, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpRight, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75 }) }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-header", children: [avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpanel-fav", src: avatar, alt: "" })) : ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "md", shape: "rounded", tone: avatarTone, children: (record.name ?? "?").charAt(0) })), (0, jsx_runtime_1.jsx)("span", { id: titleId, className: "crisp-recordpanel-title", children: heading })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-body", children: [highlights && highlights.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-section", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-section-head", children: (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-section-label", children: highlightsLabel }) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-highlights", children: highlights.map((h) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-highlight", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-highlight-label", children: [(0, jsx_runtime_1.jsx)("span", { children: h.label }), h.icon ? (0, jsx_runtime_1.jsx)(h.icon, { size: 16, strokeWidth: 1.75 }) : null] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-highlight-value", children: h.value })] }, h.key))) })] })), activity != null && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-section", children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-section-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-section-label", children: activityLabel }), onViewAllActivity && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-section-action", onClick: onViewAllActivity, children: viewAllActivityLabel }))] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-activity", children: activity })] }))] }), footer != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-footer", children: footer }))] }));
158
90
  }
159
91
  RecordPanel.displayName = "RecordPanel";
@@ -3,10 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.recordpanelRecipe = void 0;
4
4
  const core_1 = require("../../core");
5
5
  const tokens_1 = require("../../tokens");
6
- const recordstable_tag_1 = require("../recordstable/recordstable.tag");
7
6
  // A right-side record peek panel — slides in over the main content to show one
8
- // record's fields, the way a row peek panel opens. Positioned absolutely inside a
9
- // relative container (the app main area); slides in via @starting-style.
7
+ // record's Highlights and Activity, the way Attio's own peek does. Positioned
8
+ // absolutely inside a relative container (the app main area); slides in via
9
+ // @starting-style.
10
10
  const config = (0, core_1.defineRecipe)({
11
11
  name: "recordpanel",
12
12
  base: `
@@ -21,53 +21,60 @@ const config = (0, core_1.defineRecipe)({
21
21
  @starting-style { transform: translateX(calc(100% + ${tokens_1.contract.space["2"]})); }
22
22
  & * { box-sizing: border-box; }
23
23
 
24
- & .crisp-recordpanel-header { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: 52px; flex-shrink: 0; padding: 0 ${tokens_1.contract.space["3"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; }
24
+ /* #448: measured live on app.attio.com's peek -- a toolbar row above
25
+ the identity header: Close, then Previous/Next, on the left; "Open
26
+ record" on the right. 49px tall, 8px/10px padding, a hairline border
27
+ beneath (the same borderSubtle every other panel/row edge here
28
+ already uses). Close keeps the existing transparent ghost button;
29
+ Previous/Next/Open record are a distinct field-elevation style
30
+ (white fill, e.field's own ring+shadow, solid icon) -- measured as
31
+ genuinely different chrome from Close, not a reuse. */
32
+ & .crisp-recordpanel-toolbar { display: flex; align-items: center; height: 49px; flex-shrink: 0; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["2.5"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; }
33
+ & .crisp-recordpanel-toolbar-spacer { flex: 1; }
34
+ & .crisp-recordpanel-pager { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; margin-inline-start: ${tokens_1.contract.space["3"]}; }
35
+ & .crisp-recordpanel-iconbtn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex-shrink: 0; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s; }
36
+ & .crisp-recordpanel-iconbtn:hover { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
37
+ & .crisp-recordpanel-navbtn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex-shrink: 0; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgSurface}; box-shadow: ${tokens_1.elevation.field}; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; cursor: pointer; }
38
+ & .crisp-recordpanel-navbtn:hover:not(:disabled) { background: ${tokens_1.contract.bgRaised}; }
39
+ & .crisp-recordpanel-navbtn:disabled { opacity: 0.4; cursor: not-allowed; }
40
+
41
+ & .crisp-recordpanel-header { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: 52px; flex-shrink: 0; padding: 0 ${tokens_1.contract.space["3"]}; }
25
42
  & .crisp-recordpanel-fav { width: 22px; height: ${tokens_1.contract.size.chip}; border-radius: ${tokens_1.contract.radius.sm}; flex-shrink: 0; object-fit: cover; background: ${tokens_1.contract.bgRaised}; }
26
43
  & .crisp-recordpanel-title { flex: 1; min-width: 0; font-size: ${tokens_1.contract.text.xl}; font-weight: ${tokens_1.contract.weight.semibold}; letter-spacing: -0.2px; color: ${tokens_1.contract.fgPrimary}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
27
- & .crisp-recordpanel-iconbtn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex-shrink: 0; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; color: ${tokens_1.contract.fgMuted}; font-family: inherit; cursor: pointer; transition: background-color .12s; }
28
- & .crisp-recordpanel-iconbtn:hover { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
29
44
 
30
45
  & .crisp-recordpanel-body { flex: 1; min-height: 0; overflow-y: auto; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["3"]}; }
31
- & .crisp-recordpanel-sectionlabel { font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["1"]}; }
32
- & .crisp-recordpanel-field { display: flex; align-items: flex-start; gap: ${tokens_1.contract.space["2"]}; min-height: 32px; padding: ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.md}; }
33
- & .crisp-recordpanel-field:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 3%, transparent); }
34
- & .crisp-recordpanel-field-label { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; width: 132px; flex-shrink: 0; padding-top: 3px; color: ${tokens_1.contract.fgMuted}; font-size: ${tokens_1.contract.text.md}; }
35
- & .crisp-recordpanel-field-label svg { flex-shrink: 0; }
36
- & .crisp-recordpanel-field-value { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: ${tokens_1.contract.space["1"]}; padding-top: 3px; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; }
37
- & .crisp-recordpanel-field-value[data-editable] { cursor: pointer; }
38
- /* the click-to-edit trigger is a real <button> (keyboard-operable),
39
- reset back to plain inline text and made a flex-wrap container of its
40
- own -- FieldValue can render several chips (tags), which still need
41
- to wrap the way they did before this had a button around them at
42
- all. It wraps only FieldValue, never the error slot beside it (see
43
- the component's own comment) -- NOT \`display: contents\`, which
44
- drops a button's accessible semantics in some engines. */
45
- & .crisp-recordpanel-edit-trigger { display: inline-flex; flex-wrap: wrap; align-items: center; gap: ${tokens_1.contract.space["1"]}; margin: 0; padding: 0; border: none; background: none; text-align: start; font: inherit; color: inherit; cursor: pointer; }
46
- & .crisp-recordpanel-empty { color: ${tokens_1.contract.fgMuted}; }
47
46
 
48
- /* #431: measured live on app.attio.com's record side panel -- clicking a
49
- field's value opens a plain, borderless field exactly filling the
50
- value's own box (no border, no background, no box-shadow), same font
51
- as the value it replaces (hence \`font: inherit\`, not a restated
52
- size/weight). A field row's own hover tint above already carries the
53
- "this is interactive" affordance; the pending/error treatment isn't
54
- Attio-measured -- it reuses the same tokens DataTable's identical
55
- states already use (opacity 0.5, \`bgDanger\`), per #385/#430. */
56
- & .crisp-recordpanel-edit { display: block; width: 100%; margin: 0; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.sm}; background: none; box-shadow: none; outline: none; font: inherit; color: inherit; }
57
- /* not Attio-measured (its own field shows no focus ring either, caret
58
- only) -- kept for real keyboard users, the same brand ring every
59
- other crisp field already shows on focus (DataTable's identical
60
- .crisp-datatable-edit:focus, e.g.). */
61
- & .crisp-recordpanel-edit:focus { box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; }
62
- & .crisp-recordpanel-edit:disabled { opacity: 0.5; cursor: not-allowed; }
63
- & .crisp-recordpanel-edit-pending { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
64
- & .crisp-recordpanel-edit-error { flex-basis: 100%; margin-top: ${tokens_1.contract.space["0.5"]}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.bgDanger}; }
65
- & .crisp-recordpanel-edit-picker { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; width: 100%; }
66
- & .crisp-recordpanel-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; }
47
+ /* #448: measured live on app.attio.com's peek (hover a Companies row,
48
+ click "Open preview"). Section label reads identically for both
49
+ Highlights and Activity: 14px/500, the same 63%-of-fgPrimary mix
50
+ RecordPage's own field-label already uses (product/recordpage's
51
+ .crisp-recordpage-field-label) -- one more surface landing on the
52
+ same measured "muted section heading" colour. */
53
+ & .crisp-recordpanel-section { padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["1"]} 0; }
54
+ & .crisp-recordpanel-section-head { display: flex; align-items: center; justify-content: space-between; gap: ${tokens_1.contract.space["2"]}; height: 20px; margin-bottom: ${tokens_1.contract.space["2"]}; }
55
+ & .crisp-recordpanel-section-label { font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); }
56
+ /* "View all" -- measured 10px/400, a lighter mix than the section label
57
+ (55%, not 63%) -- its own distinct, honestly-measured value, not a
58
+ reuse of a nearby one that happened to be close. */
59
+ & .crisp-recordpanel-section-action { border: none; background: none; padding: 0; font-family: inherit; font-size: 10px; font-weight: ${tokens_1.contract.weight.regular}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 55%, transparent); cursor: pointer; }
60
+ & .crisp-recordpanel-section-action:hover { color: ${tokens_1.contract.fgPrimary}; }
67
61
 
68
- ${recordstable_tag_1.recordTagCss}
69
- & .crisp-recordpanel-link { color: ${tokens_1.contract.bgBrandSolid}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
70
- & .crisp-recordpanel-link:hover { text-decoration: underline; }
62
+ /* Highlights: a 2-column grid of label+icon+value cards, no card
63
+ chrome at all (no border/background/radius/shadow -- confirmed on
64
+ the live DOM: every highlight's computed style is fully transparent).
65
+ 8px row/column gap; two rows measured "233x86" inside 473px content
66
+ width is exactly (473 - 8) / 2. */
67
+ & .crisp-recordpanel-highlights { display: grid; grid-template-columns: 1fr 1fr; gap: ${tokens_1.contract.space["2"]}; }
68
+ & .crisp-recordpanel-highlight { display: flex; flex-direction: column; gap: ${tokens_1.contract.space["1.5"]}; min-width: 0; }
69
+ & .crisp-recordpanel-highlight-label { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); }
70
+ & .crisp-recordpanel-highlight-label span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
71
+ & .crisp-recordpanel-highlight-label svg { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
72
+ & .crisp-recordpanel-highlight-value { font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary}; overflow: hidden; text-overflow: ellipsis; }
73
+
74
+ /* Activity is a consumer-supplied slot (crisp has no activity-feed
75
+ model of its own to render) -- this only wraps it with the measured
76
+ section heading above. */
77
+ & .crisp-recordpanel-activity { font-size: ${tokens_1.contract.text.lg}; }
71
78
 
72
79
  /* #329: an optional slot for record actions (e.g. Edit). Reuses the
73
80
  header's own measured values rather than inventing new ones -- the
@@ -76,6 +83,8 @@ const config = (0, core_1.defineRecipe)({
76
83
  edge), and the header/body gap (space.2) for its own vertical padding
77
84
  and for spacing between whatever it's given. */
78
85
  & .crisp-recordpanel-footer { flex-shrink: 0; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["3"]}; border-top: 1px solid ${tokens_1.contract.borderSubtle}; }
86
+
87
+ ${core_1.rtlFlipCss}
79
88
  `,
80
89
  variants: {},
81
90
  defaultVariants: {},
@@ -37,18 +37,21 @@ exports.useEditableField = useEditableField;
37
37
  exports.editableInputType = editableInputType;
38
38
  const React = __importStar(require("react"));
39
39
  /**
40
- * The async-commit state machine behind `RecordPanel`/`RecordPage`'s
41
- * editable fields (#431) — the same shape `DataTable`'s own `commitEdit`
42
- * uses (#385/#430): a sync `onEdit` (returns `undefined`) commits and closes
40
+ * The async-commit state machine behind `RecordPage`'s editable Details
41
+ * column (#431) — the same shape `DataTable`'s own `commitEdit` uses
42
+ * (#385/#430): a sync `onEdit` (returns `undefined`) commits and closes
43
43
  * immediately; a Promise-returning `onEdit` keeps the field open and pending
44
44
  * while it settles, then either closes it (resolve) or closes it AND
45
45
  * reverts while showing the rejection's message (reject). The revert needs
46
- * no extra code here either: neither `RecordPanel` nor `RecordPage` mutates
47
- * `record` itself, so once `editingKey` clears, the field simply renders
48
- * `record[key]` again — whatever it still says.
46
+ * no extra code here either: `RecordPage` never mutates `record` itself, so
47
+ * once `editingKey` clears, the field simply renders `record[key]` again —
48
+ * whatever it still says.
49
49
  *
50
- * One hook shared by both consumers rather than two copies of the same
51
- * three `useState`s and the same `.then` branching.
50
+ * `RecordPanel` (the peek panel) used to share this hook too, before #448
51
+ * found that Attio's own peek has no editable field at all to attach it to
52
+ * — only `RecordPage`'s Details column does. Kept as its own module rather
53
+ * than folded back into `recordpage.tsx`: a second consumer turning up
54
+ * again is a smaller change than re-extracting it would be.
52
55
  */
53
56
  function useEditableField(onEdit) {
54
57
  const [editingKey, setEditingKey] = React.useState(null);
@@ -7,9 +7,14 @@ const tokens_1 = require("../../tokens");
7
7
  * `border-radius: ${c.radius.chip}`, 14px/20px at weight 500, and a 1px inset ring in the
8
8
  * tone's colour.
9
9
  *
10
- * Shared because RecordsTable and RecordPanel render the *same* chip. It used
11
- * to be pasted into both recipes, and the two copies had already started to
12
- * drift — adding the measured inset ring meant patching the same rules twice.
10
+ * Its own module (not pasted into `recordstable.recipe.ts`) because it used
11
+ * to be shared with `RecordPanel`'s own Details field list too, and the two
12
+ * copies had already started to drift before that sharing existed — adding
13
+ * the measured inset ring meant patching the same rules twice. `RecordPanel`
14
+ * no longer renders a Details list (#448, Attio's own peek doesn't have
15
+ * one), so `RecordsTable` is this module's only consumer now; kept as its
16
+ * own file rather than folded back in, since a second consumer showing up
17
+ * again is exactly how the drift happened the first time.
13
18
  */
14
19
  exports.recordTagCss = `& .crisp-recordstable-tag { display: inline-flex; align-items: center; height: ${tokens_1.contract.size.chip}; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.chip}; font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.medium}; white-space: nowrap; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 82%, transparent); box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; }
15
20
  & .crisp-recordstable-tag[data-tone="brand"] { background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 14%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 82%, ${tokens_1.contract.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 30%, transparent); }
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.richtextRecipe = exports.RichTextEditor = void 0;
3
+ exports.richtextRecipe = exports.RichTextEditor = exports.RICHTEXT_COLOR_TONES = void 0;
4
4
  var richtext_1 = require("./richtext");
5
+ Object.defineProperty(exports, "RICHTEXT_COLOR_TONES", { enumerable: true, get: function () { return richtext_1.RICHTEXT_COLOR_TONES; } });
5
6
  Object.defineProperty(exports, "RichTextEditor", { enumerable: true, get: function () { return richtext_1.RichTextEditor; } });
6
7
  var richtext_recipe_1 = require("./richtext.recipe");
7
8
  Object.defineProperty(exports, "richtextRecipe", { enumerable: true, get: function () { return richtext_recipe_1.richtextRecipe; } });