@crisp-ui-kit/crisp 0.54.2 → 0.54.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.
Files changed (116) hide show
  1. package/dist/board.css +13 -1
  2. package/dist/board.layered.css +13 -1
  3. package/dist/chip.css +7 -1
  4. package/dist/chip.d.ts +2 -1
  5. package/dist/chip.layered.css +7 -1
  6. package/dist/cjs/components/chip/chip.js +3 -2
  7. package/dist/cjs/components/chip/chip.recipe.js +37 -1
  8. package/dist/cjs/components/datatable/datatable.js +489 -111
  9. package/dist/cjs/components/emptystate/emptystate.js +3 -2
  10. package/dist/cjs/components/emptystate/emptystate.recipe.js +26 -1
  11. package/dist/cjs/components/personchip/index.js +17 -0
  12. package/dist/cjs/components/personchip/personchip.js +50 -0
  13. package/dist/cjs/components/table/table.js +2 -2
  14. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +41 -4
  15. package/dist/cjs/components/tabs/tabs.js +2 -2
  16. package/dist/cjs/components/tabs/tabs.recipe.js +30 -3
  17. package/dist/cjs/components/text/index.js +7 -0
  18. package/dist/cjs/components/text/text.js +55 -0
  19. package/dist/cjs/components/text/text.recipe.js +34 -0
  20. package/dist/cjs/index.js +4 -0
  21. package/dist/cjs/product/board/board.js +60 -2
  22. package/dist/cjs/product/board/board.recipe.js +10 -0
  23. package/dist/cjs/product/comments/comments.js +426 -0
  24. package/dist/cjs/product/comments/comments.recipe.js +126 -0
  25. package/dist/cjs/product/comments/index.js +8 -0
  26. package/dist/cjs/product/headerbar/headerbar.recipe.js +9 -1
  27. package/dist/cjs/product/listpage/listpage.recipe.js +10 -1
  28. package/dist/cjs/product/objectsettings/index.js +16 -0
  29. package/dist/cjs/product/objectsettings/objectsettings.js +365 -0
  30. package/dist/cjs/product/objectsettings/objectsettings.recipe.js +65 -0
  31. package/dist/cjs/product/recordpage/recordpage.recipe.js +10 -1
  32. package/dist/cjs/product/recordstable/recordstable.js +89 -6
  33. package/dist/cjs/product/richtext/richtext.recipe.js +26 -1
  34. package/dist/cjs/product/taskstable/taskstable.recipe.js +10 -1
  35. package/dist/comments.css +80 -0
  36. package/dist/comments.d.ts +7 -0
  37. package/dist/comments.layered.css +82 -0
  38. package/dist/emptystate.css +15 -1
  39. package/dist/emptystate.d.ts +2 -2
  40. package/dist/emptystate.layered.css +15 -1
  41. package/dist/esm/components/chip/chip.js +3 -2
  42. package/dist/esm/components/chip/chip.recipe.js +37 -1
  43. package/dist/esm/components/datatable/datatable.js +488 -111
  44. package/dist/esm/components/emptystate/emptystate.js +4 -3
  45. package/dist/esm/components/emptystate/emptystate.recipe.js +26 -1
  46. package/dist/esm/components/emptystate/index.js +1 -1
  47. package/dist/esm/components/personchip/index.js +1 -0
  48. package/dist/esm/components/personchip/personchip.js +47 -0
  49. package/dist/esm/components/table/table.js +2 -2
  50. package/dist/esm/components/tabletoolbar/tabletoolbar.js +43 -6
  51. package/dist/esm/components/tabs/tabs.js +2 -2
  52. package/dist/esm/components/tabs/tabs.recipe.js +30 -3
  53. package/dist/esm/components/text/index.js +2 -0
  54. package/dist/esm/components/text/text.js +19 -0
  55. package/dist/esm/components/text/text.recipe.js +31 -0
  56. package/dist/esm/index.js +4 -0
  57. package/dist/esm/product/board/board.js +61 -3
  58. package/dist/esm/product/board/board.recipe.js +10 -0
  59. package/dist/esm/product/comments/comments.js +388 -0
  60. package/dist/esm/product/comments/comments.recipe.js +123 -0
  61. package/dist/esm/product/comments/index.js +2 -0
  62. package/dist/esm/product/headerbar/headerbar.recipe.js +9 -1
  63. package/dist/esm/product/listpage/listpage.recipe.js +10 -1
  64. package/dist/esm/product/objectsettings/index.js +2 -0
  65. package/dist/esm/product/objectsettings/objectsettings.js +322 -0
  66. package/dist/esm/product/objectsettings/objectsettings.recipe.js +62 -0
  67. package/dist/esm/product/recordpage/recordpage.recipe.js +10 -1
  68. package/dist/esm/product/recordstable/recordstable.js +90 -8
  69. package/dist/esm/product/richtext/richtext.recipe.js +26 -1
  70. package/dist/esm/product/taskstable/taskstable.recipe.js +10 -1
  71. package/dist/headerbar.css +1 -1
  72. package/dist/headerbar.layered.css +1 -1
  73. package/dist/listpage.css +8 -1
  74. package/dist/listpage.layered.css +8 -1
  75. package/dist/objectsettings.css +49 -0
  76. package/dist/objectsettings.d.ts +7 -0
  77. package/dist/objectsettings.layered.css +51 -0
  78. package/dist/recordpage.css +13 -2
  79. package/dist/recordpage.layered.css +13 -2
  80. package/dist/richtext.css +11 -1
  81. package/dist/richtext.layered.css +11 -1
  82. package/dist/styles.css +224 -10
  83. package/dist/styles.layered.css +224 -10
  84. package/dist/tabs.css +7 -1
  85. package/dist/tabs.d.ts +2 -2
  86. package/dist/tabs.layered.css +7 -1
  87. package/dist/taskstable.css +8 -1
  88. package/dist/taskstable.layered.css +8 -1
  89. package/dist/text.css +9 -0
  90. package/dist/text.d.ts +7 -0
  91. package/dist/text.layered.css +11 -0
  92. package/dist/types/components/chip/chip.d.ts +32 -0
  93. package/dist/types/components/chip/chip.recipe.d.ts +31 -0
  94. package/dist/types/components/datatable/datatable.d.ts +28 -4
  95. package/dist/types/components/emptystate/emptystate.d.ts +29 -4
  96. package/dist/types/components/emptystate/emptystate.recipe.d.ts +2 -0
  97. package/dist/types/components/emptystate/index.d.ts +1 -1
  98. package/dist/types/components/personchip/index.d.ts +1 -0
  99. package/dist/types/components/personchip/personchip.d.ts +37 -0
  100. package/dist/types/components/table/table.d.ts +36 -0
  101. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +8 -2
  102. package/dist/types/components/tabs/tabs.d.ts +4 -2
  103. package/dist/types/components/tabs/tabs.recipe.d.ts +18 -3
  104. package/dist/types/components/text/index.d.ts +2 -0
  105. package/dist/types/components/text/text.d.ts +32 -0
  106. package/dist/types/components/text/text.recipe.d.ts +40 -0
  107. package/dist/types/index.d.ts +4 -0
  108. package/dist/types/product/board/board.d.ts +20 -1
  109. package/dist/types/product/comments/comments.d.ts +192 -0
  110. package/dist/types/product/comments/comments.recipe.d.ts +3 -0
  111. package/dist/types/product/comments/index.d.ts +2 -0
  112. package/dist/types/product/objectsettings/index.d.ts +2 -0
  113. package/dist/types/product/objectsettings/objectsettings.d.ts +261 -0
  114. package/dist/types/product/objectsettings/objectsettings.recipe.d.ts +3 -0
  115. package/dist/types/product/recordstable/recordstable.d.ts +24 -2
  116. package/package.json +21 -1
@@ -0,0 +1,426 @@
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.serializeComposerField = serializeComposerField;
37
+ exports.CommentsPanel = CommentsPanel;
38
+ exports.CommentCountBadge = CommentCountBadge;
39
+ const jsx_runtime_1 = require("react/jsx-runtime");
40
+ const lucide_react_1 = require("lucide-react");
41
+ const React = __importStar(require("react"));
42
+ const avatar_1 = require("../../components/avatar");
43
+ const button_1 = require("../../components/button");
44
+ const dialog_1 = require("../../components/dialog");
45
+ const menu_1 = require("../../components/menu");
46
+ const popover_1 = require("../../components/popover");
47
+ const core_1 = require("../../core");
48
+ const comments_recipe_1 = require("./comments.recipe");
49
+ /** A single literal JSX occurrence for the reply glyph, used at two sizes --
50
+ * so the RTL-mirroring sweep (which counts the flip-marker attribute against
51
+ * source-text occurrences, not runtime renders) sees one mark for one
52
+ * direction-bearing icon, not two. */
53
+ function ReplyGlyph({ size }) {
54
+ return ((0, jsx_runtime_1.jsx)(lucide_react_1.CornerUpLeft, { "data-rtl-flip": "", size: size, strokeWidth: 2, "aria-hidden": "true" }));
55
+ }
56
+ const DEFAULT_EMOJI = [
57
+ "👍",
58
+ "👎",
59
+ "😀",
60
+ "😂",
61
+ "😍",
62
+ "🎉",
63
+ "🙏",
64
+ "👏",
65
+ "🔥",
66
+ "❤️",
67
+ "✅",
68
+ "👀",
69
+ "🚀",
70
+ "💯",
71
+ "😢",
72
+ "🤔",
73
+ ];
74
+ function initial(name) {
75
+ return (name.trim().charAt(0) || "?").toUpperCase();
76
+ }
77
+ function AuthorAvatar({ author }) {
78
+ if (author.avatar != null)
79
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: author.avatar });
80
+ return ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "sm", tone: "cyan", children: initial(author.name) }));
81
+ }
82
+ /**
83
+ * A minimal reaction picker — search box + a curated emoji grid. Attio's own
84
+ * picker (`Search Emojis`, categorised sections) wasn't independently
85
+ * pixel-measured this pass: the live CDP session's `Page.captureScreenshot`
86
+ * started timing out after the panel/composer/row measurements were already
87
+ * captured, and the popover itself closed on every retry before a shadow/
88
+ * radius read landed. The chrome below reuses crisp's own already-measured
89
+ * floating-popover shell (`radius.xl`, `elevation.floating`-family shadow —
90
+ * see `Menu`) rather than inventing new values, and the search placeholder
91
+ * ("Search Emojis") IS the live-read copy. The emoji SET is crisp's own
92
+ * curated 16, not Attio's full category grid — a full emoji library is out
93
+ * of scope for a dependency-free kit; swap `emojiOptions` for a real one.
94
+ */
95
+ function ReactionPicker({ options, searchLabel, onPick, }) {
96
+ const [query, setQuery] = React.useState("");
97
+ const filtered = query.trim()
98
+ ? options.filter((e) => e.includes(query.trim()))
99
+ : options;
100
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-reactionpicker", children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-comments-reactionpicker-search", placeholder: searchLabel, "aria-label": searchLabel, value: query, onChange: (e) => setQuery(e.target.value) }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-comments-reactionpicker-grid", children: filtered.map((emoji) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-reactionpicker-item", onClick: () => onPick(emoji), children: emoji }, emoji))) })] }));
101
+ }
102
+ /**
103
+ * Reads the composer field's DOM into plain text, serializing each mention
104
+ * chip as `@[label](id)` — a fixed, parseable token a consumer (GRC) can
105
+ * pull the mentioned record's id back out of.
106
+ *
107
+ * This is the actual fix for a real bug (GRC's own composer, a plain
108
+ * `<textarea>` holding `@[name](actor:id)` as its literal editable value:
109
+ * a caret placed inside that markup can edit it like any other character,
110
+ * so a stray keystroke corrupts the token and the broken markup posts
111
+ * verbatim — seen live, `/private/tmp/grc-land138/mentions-6-posted.png`).
112
+ * A mention here is NEVER the editable representation: it's a real DOM node
113
+ * (`contentEditable=false`, so a browser won't place a caret inside it to
114
+ * begin with) carrying its id/label in two `data-` attributes set once, at
115
+ * insertion (`insertMention`), and never touched again. This function reads
116
+ * those attributes — not the chip's own `.textContent` — specifically so
117
+ * that even if something DID manage to mutate the chip's interior (a
118
+ * hostile paste, an IME composition, a direct DOM call bypassing the
119
+ * `contentEditable=false` guard entirely — exactly the class of thing a
120
+ * real browser mostly prevents but a test, or a buggy extension, would
121
+ * not), the token this function emits still can't be corrupted: it was
122
+ * never read from the part that changed.
123
+ */
124
+ function serializeComposerField(field) {
125
+ let out = "";
126
+ for (const node of Array.from(field.childNodes)) {
127
+ if (node.nodeType === Node.TEXT_NODE) {
128
+ out += node.textContent ?? "";
129
+ continue;
130
+ }
131
+ if (node.nodeType !== Node.ELEMENT_NODE)
132
+ continue;
133
+ const el = node;
134
+ if (el.classList.contains("crisp-comments-mention")) {
135
+ const id = el.dataset.mentionId ?? "";
136
+ const label = el.dataset.mentionLabel ?? "";
137
+ out += `@[${label}](${id})`;
138
+ continue;
139
+ }
140
+ if (el.tagName === "BR") {
141
+ out += "\n";
142
+ continue;
143
+ }
144
+ // A wrapper the browser itself inserted (Enter's own new-line block,
145
+ // Shift+Enter's, a pasted `<span>`) -- recurse so a mention nested
146
+ // inside one still serializes atomically instead of falling through to
147
+ // a flattened `.textContent` read that would re-expose its raw text.
148
+ out += serializeComposerField(el);
149
+ if (el.tagName === "DIV" || el.tagName === "P")
150
+ out += "\n";
151
+ }
152
+ return out;
153
+ }
154
+ /**
155
+ * The bottom composer — a plain `contentEditable` field (no rich-text
156
+ * toolbar; Attio's own comment composer has none, unlike its note editor),
157
+ * with an "@" mention trigger mirroring `RichTextEditor`'s own detection
158
+ * (`matchMentionTrigger` + a live `Range`), and a `Comment`/`Reply` button
159
+ * disabled until the field holds text. Uncontrolled — reads its own DOM on
160
+ * submit, the same shape `RichTextEditor` uses for its body.
161
+ */
162
+ const Composer = React.forwardRef(function Composer({ placeholder, postLabel, mentionTrigger, renderMentionPicker, replyToolLabel, mentionToolLabel, onSubmit, }, ref) {
163
+ const fieldRef = React.useRef(null);
164
+ const [empty, setEmpty] = React.useState(true);
165
+ const [mention, setMention] = React.useState(null);
166
+ const mentionRangeRef = React.useRef(null);
167
+ const syncEmpty = () => setEmpty((fieldRef.current?.textContent ?? "") === "");
168
+ const closeMention = React.useCallback(() => {
169
+ mentionRangeRef.current = null;
170
+ setMention(null);
171
+ }, []);
172
+ const updateMention = React.useCallback(() => {
173
+ const field = fieldRef.current;
174
+ const sel = window.getSelection();
175
+ if (!field ||
176
+ !sel?.isCollapsed ||
177
+ sel.rangeCount === 0 ||
178
+ !field.contains(sel.anchorNode)) {
179
+ closeMention();
180
+ return;
181
+ }
182
+ const range = sel.getRangeAt(0);
183
+ const node = range.startContainer;
184
+ if (node.nodeType !== Node.TEXT_NODE) {
185
+ closeMention();
186
+ return;
187
+ }
188
+ const textBefore = (node.textContent ?? "").slice(0, range.startOffset);
189
+ const match = (0, core_1.matchMentionTrigger)(textBefore, mentionTrigger);
190
+ if (!match) {
191
+ closeMention();
192
+ return;
193
+ }
194
+ const triggerRange = document.createRange();
195
+ triggerRange.setStart(node, match.start);
196
+ triggerRange.setEnd(node, range.startOffset);
197
+ mentionRangeRef.current = triggerRange;
198
+ const rect = triggerRange.getBoundingClientRect();
199
+ setMention({
200
+ query: match.query,
201
+ anchor: { x: rect.left, y: rect.bottom },
202
+ });
203
+ }, [mentionTrigger, closeMention]);
204
+ const insertMention = (item) => {
205
+ const field = fieldRef.current;
206
+ const sel = window.getSelection();
207
+ if (!field || !sel)
208
+ return;
209
+ field.focus();
210
+ const range = mentionRangeRef.current ??
211
+ (sel.rangeCount > 0 ? sel.getRangeAt(0) : null);
212
+ if (!range)
213
+ return;
214
+ const chip = document.createElement("span");
215
+ chip.className = "crisp-comments-mention";
216
+ chip.contentEditable = "false";
217
+ // An immutable snapshot, read back only from these two attributes at
218
+ // submit time -- never from the chip's own live text (see `serialize`'s
219
+ // own doc comment for why).
220
+ chip.dataset.mentionId = item.id;
221
+ chip.dataset.mentionLabel = item.label;
222
+ chip.textContent = `${mentionTrigger}${item.label}`;
223
+ range.deleteContents();
224
+ range.insertNode(chip);
225
+ const space = document.createTextNode(" ");
226
+ chip.after(space);
227
+ const after = document.createRange();
228
+ after.setStartAfter(space);
229
+ after.collapse(true);
230
+ sel.removeAllRanges();
231
+ sel.addRange(after);
232
+ closeMention();
233
+ syncEmpty();
234
+ };
235
+ /**
236
+ * Delete a mention chip as one unit, the same "Backspace removes it whole"
237
+ * behaviour a real browser already gives a `contentEditable=false` node --
238
+ * made explicit rather than relied on, since that native behaviour isn't
239
+ * consistent enough across engines to trust alone. Only fires when the
240
+ * caret sits immediately after a chip with nothing selected; a caret
241
+ * anywhere else (including genuinely inside a chip, which a real
242
+ * `contentEditable=false` node refuses to place one in) is untouched.
243
+ */
244
+ const backspaceMention = (e) => {
245
+ const field = fieldRef.current;
246
+ const sel = window.getSelection();
247
+ if (!field || !sel?.isCollapsed || sel.rangeCount === 0)
248
+ return false;
249
+ const range = sel.getRangeAt(0);
250
+ let mention = null;
251
+ if (range.startContainer.nodeType === Node.TEXT_NODE) {
252
+ if (range.startOffset === 0) {
253
+ const prev = range.startContainer.previousSibling;
254
+ if (prev instanceof HTMLElement)
255
+ mention = prev;
256
+ }
257
+ }
258
+ else if (range.startContainer === field) {
259
+ const prev = field.childNodes[range.startOffset - 1];
260
+ if (prev instanceof HTMLElement)
261
+ mention = prev;
262
+ }
263
+ if (!mention?.classList.contains("crisp-comments-mention"))
264
+ return false;
265
+ e.preventDefault();
266
+ mention.remove();
267
+ syncEmpty();
268
+ return true;
269
+ };
270
+ const clear = () => {
271
+ if (fieldRef.current)
272
+ fieldRef.current.innerHTML = "";
273
+ setEmpty(true);
274
+ closeMention();
275
+ };
276
+ React.useImperativeHandle(ref, () => ({
277
+ clear,
278
+ focus: () => fieldRef.current?.focus(),
279
+ }));
280
+ const submit = () => {
281
+ const field = fieldRef.current;
282
+ const text = field ? serializeComposerField(field).trim() : "";
283
+ if (!text)
284
+ return;
285
+ onSubmit(text);
286
+ clear();
287
+ };
288
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-composer", children: [(0, jsx_runtime_1.jsx)("div", { ref: fieldRef, className: "crisp-comments-field", contentEditable: true, suppressContentEditableWarning: true, role: "textbox", tabIndex: 0, "aria-multiline": "true", "aria-label": placeholder, "data-placeholder": placeholder, onInput: () => {
289
+ syncEmpty();
290
+ updateMention();
291
+ }, onKeyUp: updateMention, onMouseUp: updateMention, onKeyDown: (e) => {
292
+ if (e.key === "Backspace" && backspaceMention(e))
293
+ return;
294
+ if (e.key === "Enter" && !e.shiftKey && !mention) {
295
+ e.preventDefault();
296
+ submit();
297
+ }
298
+ } }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-composer-foot", children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-composer-tools", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-tool", "aria-label": replyToolLabel, children: (0, jsx_runtime_1.jsx)(ReplyGlyph, { size: 14 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-tool", "aria-label": mentionToolLabel, onClick: () => {
299
+ const field = fieldRef.current;
300
+ if (!field)
301
+ return;
302
+ field.focus();
303
+ document.execCommand("insertText", false, mentionTrigger);
304
+ syncEmpty();
305
+ updateMention();
306
+ }, children: "@" })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-post", disabled: empty, onClick: submit, children: postLabel })] }), mention &&
307
+ renderMentionPicker?.({
308
+ query: mention.query,
309
+ anchor: mention.anchor,
310
+ select: insertMention,
311
+ close: closeMention,
312
+ })] }));
313
+ });
314
+ function CommentRow({ comment, replyLabel, reactLabel, resolveLabel, moreLabel, copyLinkLabel, deleteLabel, addReactionLabel, reactionSearchLabel, emojiOptions, resolvable, resolved, resolvedLabel, onReply, onReact, onResolve, onCopyLink, onDeleteRequest, }) {
315
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-row", children: [(0, jsx_runtime_1.jsx)(AuthorAvatar, { author: comment.author }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-row-main", children: [resolvedLabel != null && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-resolved-label", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 12, strokeWidth: 2.5, "aria-hidden": "true" }), resolvedLabel] })), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-row-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-comments-author", children: comment.author.name }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-comments-time", children: comment.time })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-comments-body-text", children: comment.body }), comment.reactions && comment.reactions.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-reactions", children: [comment.reactions.map((r) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-comments-reaction", "data-reacted": r.reactedByMe ? "" : undefined, onClick: () => onReact(r.emoji), children: [(0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", children: r.emoji }), r.count] }, r.emoji))), (0, jsx_runtime_1.jsx)(popover_1.Popover, { align: "start", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-reaction-add", "aria-label": addReactionLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.SmilePlus, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: (0, jsx_runtime_1.jsx)(ReactionPicker, { options: emojiOptions, searchLabel: reactionSearchLabel, onPick: onReact }) })] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-row-actions", children: [onReply && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-action", "aria-label": replyLabel, onClick: onReply, children: (0, jsx_runtime_1.jsx)(ReplyGlyph, { size: 16 }) })), (0, jsx_runtime_1.jsx)(popover_1.Popover, { align: "end", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-action", "aria-label": reactLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.SmilePlus, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: (0, jsx_runtime_1.jsx)(ReactionPicker, { options: emojiOptions, searchLabel: reactionSearchLabel, onPick: onReact }) }), resolvable && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-action", "aria-label": resolveLabel, "aria-pressed": !!resolved, "data-active": resolved ? "" : undefined, onClick: onResolve, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-action", "aria-label": moreLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: onCopyLink, children: copyLinkLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { danger: true, onSelect: onDeleteRequest, children: deleteLabel })] })] })] }));
316
+ }
317
+ /**
318
+ * Per-record comment threads (crisp#625) — measured live on app.attio.com's
319
+ * own right-hand comments panel: a header (title, filter menu), a scrolling
320
+ * thread list or a single narrowed thread, and a persistent bottom composer.
321
+ * Controlled: pass `threads` and wire the callbacks; the panel renders
322
+ * nothing on its own beyond that data. See `CommentCountBadge` for the
323
+ * separate row affordance (P-051) — deliberately NOT part of this
324
+ * component, since it lives in a table cell this component doesn't own.
325
+ */
326
+ function CommentsPanel({ threads, currentUserId, subject, subjectIcon, showResolved = false, onShowResolvedChange, onlyMine = false, onOnlyMineChange, activeThreadId: activeThreadIdProp, onActiveThreadChange, onPost, onReact, onResolve, onDelete, onCopyLink, emojiOptions = DEFAULT_EMOJI, mentionTrigger = "@", renderMentionPicker, onClose, closeLabel: closeLabelProp, backLabel: backLabelProp, title: titleProp, threadTitle: threadTitleProp, breadcrumbPrefix: breadcrumbPrefixProp, composerPlaceholder: composerPlaceholderProp, replyPlaceholder: replyPlaceholderProp, postLabel: postLabelProp, replyPostLabel: replyPostLabelProp, emptyLabel, filterLabel: filterLabelProp, showResolvedLabel: showResolvedLabelProp, onlyMineLabel: onlyMineLabelProp, replyActionLabel: replyActionLabelProp, reactActionLabel: reactActionLabelProp, resolveActionLabel: resolveActionLabelProp, resolvedByYouLabel: resolvedByYouLabelProp, resolvedByLabel, moreActionsLabel: moreActionsLabelProp, copyLinkLabel: copyLinkLabelProp, deleteLabel: deleteLabelProp, addReactionLabel: addReactionLabelProp, reactionSearchLabel: reactionSearchLabelProp, deleteConfirmTitle: deleteConfirmTitleProp, deleteConfirmBody: deleteConfirmBodyProp, deleteConfirmActionLabel: deleteConfirmActionLabelProp, cancelLabel: cancelLabelProp, composerReplyToolLabel: composerReplyToolLabelProp, composerMentionToolLabel: composerMentionToolLabelProp, className, ...rest }) {
327
+ const closeLabel = (0, core_1.useCrispMessage)(closeLabelProp, "Close");
328
+ const backLabel = (0, core_1.useCrispMessage)(backLabelProp, "Back");
329
+ const title = (0, core_1.useCrispMessage)(titleProp, "All comments");
330
+ const threadTitle = (0, core_1.useCrispMessage)(threadTitleProp, "Comments");
331
+ const breadcrumbPrefix = (0, core_1.useCrispMessage)(breadcrumbPrefixProp, "Comment on");
332
+ const composerPlaceholder = (0, core_1.useCrispMessage)(composerPlaceholderProp, "Add comment...");
333
+ const replyPlaceholder = (0, core_1.useCrispMessage)(replyPlaceholderProp, "Reply...");
334
+ const postLabel = (0, core_1.useCrispMessage)(postLabelProp, "Comment");
335
+ const replyPostLabel = (0, core_1.useCrispMessage)(replyPostLabelProp, "Reply");
336
+ const filterLabel = (0, core_1.useCrispMessage)(filterLabelProp, "Filter comments");
337
+ const showResolvedLabel = (0, core_1.useCrispMessage)(showResolvedLabelProp, "Show resolved comments");
338
+ const onlyMineLabel = (0, core_1.useCrispMessage)(onlyMineLabelProp, "Only my threads");
339
+ const replyActionLabel = (0, core_1.useCrispMessage)(replyActionLabelProp, "Reply to comment");
340
+ const reactActionLabel = (0, core_1.useCrispMessage)(reactActionLabelProp, "Pick emoji");
341
+ const resolveActionLabel = (0, core_1.useCrispMessage)(resolveActionLabelProp, "Resolve comment");
342
+ const resolvedByYouLabel = (0, core_1.useCrispMessage)(resolvedByYouLabelProp, "Resolved by you");
343
+ const moreActionsLabel = (0, core_1.useCrispMessage)(moreActionsLabelProp, "More actions");
344
+ const copyLinkLabel = (0, core_1.useCrispMessage)(copyLinkLabelProp, "Copy link to comment");
345
+ const deleteLabel = (0, core_1.useCrispMessage)(deleteLabelProp, "Delete comment");
346
+ const addReactionLabel = (0, core_1.useCrispMessage)(addReactionLabelProp, "Add reaction");
347
+ const reactionSearchLabel = (0, core_1.useCrispMessage)(reactionSearchLabelProp, "Search Emojis");
348
+ const deleteConfirmTitle = (0, core_1.useCrispMessage)(deleteConfirmTitleProp, "Delete comment");
349
+ const deleteConfirmBody = (0, core_1.useCrispMessage)(deleteConfirmBodyProp, "You're about to delete this comment. Once deleted it cannot be restored.");
350
+ const deleteConfirmActionLabel = (0, core_1.useCrispMessage)(deleteConfirmActionLabelProp, "Delete");
351
+ const cancelLabel = (0, core_1.useCrispMessage)(cancelLabelProp, "Cancel");
352
+ const composerReplyToolLabel = (0, core_1.useCrispMessage)(composerReplyToolLabelProp, "Reply");
353
+ const composerMentionToolLabel = (0, core_1.useCrispMessage)(composerMentionToolLabelProp, "Mention someone");
354
+ const [internalActiveId, setInternalActiveId] = React.useState(null);
355
+ const activeThreadId = activeThreadIdProp ?? internalActiveId;
356
+ const setActiveThreadId = (id) => {
357
+ if (activeThreadIdProp === undefined)
358
+ setInternalActiveId(id);
359
+ onActiveThreadChange?.(id);
360
+ };
361
+ const [deleteTarget, setDeleteTarget] = React.useState(null);
362
+ const copyLink = (threadId, commentId) => {
363
+ if (onCopyLink) {
364
+ onCopyLink(threadId, commentId);
365
+ return;
366
+ }
367
+ const url = `${typeof window !== "undefined" ? window.location.href.split("#")[0] : ""}#comment-${commentId}`;
368
+ navigator.clipboard?.writeText(url).catch(() => { });
369
+ };
370
+ const visibleThreads = threads.filter((t) => {
371
+ if (!showResolved && t.resolved)
372
+ return false;
373
+ if (onlyMine && !t.mine)
374
+ return false;
375
+ return true;
376
+ });
377
+ const activeThread = activeThreadId
378
+ ? threads.find((t) => t.id === activeThreadId)
379
+ : null;
380
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, comments_recipe_1.commentsRecipe)({ className }), role: "dialog", "aria-label": title, ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-head", children: [activeThread ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": backLabel, onClick: () => setActiveThreadId(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronLeft, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "crisp-comments-title-wrap", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-comments-title", children: activeThread ? threadTitle : title }), activeThread && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-comments-breadcrumb", children: [breadcrumbPrefix, " ", subjectIcon, " ", subject] }))] }), !activeThread && ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": filterLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuCheckboxItem, { checked: showResolved, onCheckedChange: onShowResolvedChange, children: showResolvedLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuCheckboxItem, { checked: onlyMine, onCheckedChange: onOnlyMineChange, children: onlyMineLabel })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-headbtn", "aria-label": closeLabel, onClick: onClose, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-comments-body crisp-comments-body--pad-top", children: activeThread ? ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(CommentRow, { comment: activeThread.root, currentUserId: currentUserId, replyLabel: replyActionLabel, reactLabel: reactActionLabel, resolveLabel: resolveActionLabel, moreLabel: moreActionsLabel, copyLinkLabel: copyLinkLabel, deleteLabel: deleteLabel, addReactionLabel: addReactionLabel, reactionSearchLabel: reactionSearchLabel, emojiOptions: emojiOptions, resolvable: !!onResolve, resolved: activeThread.resolved, resolvedLabel: activeThread.resolved
381
+ ? activeThread.resolvedBy?.id === currentUserId ||
382
+ !activeThread.resolvedBy
383
+ ? resolvedByYouLabel
384
+ : (resolvedByLabel?.(activeThread.resolvedBy.name) ??
385
+ `Resolved by ${activeThread.resolvedBy.name}`)
386
+ : undefined, onReact: (emoji) => onReact(activeThread.id, activeThread.root.id, emoji), onResolve: onResolve ? () => onResolve(activeThread.id) : undefined, onCopyLink: () => copyLink(activeThread.id, activeThread.root.id), onDeleteRequest: () => setDeleteTarget({
387
+ threadId: activeThread.id,
388
+ commentId: activeThread.root.id,
389
+ }) }), activeThread.replies && activeThread.replies.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-comments-replies", children: activeThread.replies.map((reply) => ((0, jsx_runtime_1.jsx)(CommentRow, { comment: reply, currentUserId: currentUserId, replyLabel: replyActionLabel, reactLabel: reactActionLabel, resolveLabel: resolveActionLabel, moreLabel: moreActionsLabel, copyLinkLabel: copyLinkLabel, deleteLabel: deleteLabel, addReactionLabel: addReactionLabel, reactionSearchLabel: reactionSearchLabel, emojiOptions: emojiOptions, onReact: (emoji) => onReact(activeThread.id, reply.id, emoji), onCopyLink: () => copyLink(activeThread.id, reply.id), onDeleteRequest: () => setDeleteTarget({
390
+ threadId: activeThread.id,
391
+ commentId: reply.id,
392
+ }) }, reply.id))) }))] })) : visibleThreads.length === 0 && emptyLabel != null ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-comments-empty", children: emptyLabel })) : (visibleThreads.map((t) => ((0, jsx_runtime_1.jsx)(CommentRow, { comment: t.root, currentUserId: currentUserId, replyLabel: replyActionLabel, reactLabel: reactActionLabel, resolveLabel: resolveActionLabel, moreLabel: moreActionsLabel, copyLinkLabel: copyLinkLabel, deleteLabel: deleteLabel, addReactionLabel: addReactionLabel, reactionSearchLabel: reactionSearchLabel, emojiOptions: emojiOptions, resolvable: !!onResolve, resolved: t.resolved, resolvedLabel: t.resolved
393
+ ? t.resolvedBy?.id === currentUserId || !t.resolvedBy
394
+ ? resolvedByYouLabel
395
+ : (resolvedByLabel?.(t.resolvedBy.name) ??
396
+ `Resolved by ${t.resolvedBy.name}`)
397
+ : undefined, onReply: () => setActiveThreadId(t.id), onReact: (emoji) => onReact(t.id, t.root.id, emoji), onResolve: onResolve ? () => onResolve(t.id) : undefined, onCopyLink: () => copyLink(t.id, t.root.id), onDeleteRequest: () => setDeleteTarget({ threadId: t.id, commentId: t.root.id }) }, t.id)))) }), (0, jsx_runtime_1.jsx)(Composer, { placeholder: activeThread ? replyPlaceholder : composerPlaceholder, postLabel: activeThread ? replyPostLabel : postLabel, mentionTrigger: mentionTrigger, renderMentionPicker: renderMentionPicker, replyToolLabel: composerReplyToolLabel, mentionToolLabel: composerMentionToolLabel, onSubmit: (text) => onPost(text, activeThread ? activeThread.id : null) }, activeThread ? activeThread.id : "root"), (0, jsx_runtime_1.jsxs)(dialog_1.Dialog, { open: deleteTarget != null, onOpenChange: (open) => {
398
+ if (!open)
399
+ setDeleteTarget(null);
400
+ }, title: deleteConfirmTitle, children: [(0, jsx_runtime_1.jsx)(dialog_1.DialogBody, { children: deleteConfirmBody }), (0, jsx_runtime_1.jsxs)(dialog_1.DialogFooter, { children: [(0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", intent: "neutral", size: "md", onClick: () => setDeleteTarget(null), children: cancelLabel }), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", intent: "danger", size: "md", onClick: () => {
401
+ if (deleteTarget)
402
+ onDelete(deleteTarget.threadId, deleteTarget.commentId);
403
+ setDeleteTarget(null);
404
+ }, children: deleteConfirmActionLabel })] })] })] }));
405
+ }
406
+ CommentsPanel.displayName = "CommentsPanel";
407
+ /**
408
+ * The row's own comment affordance (P-051) — a persistent 6px dot once a
409
+ * record has ≥1 comment, replaced on hover by a comment-count icon (P-035's
410
+ * "icon 1") plus, when `onOpenPreview` is given, the row's second hover icon
411
+ * ("Open preview"). A standalone piece by design (see the module doc
412
+ * comment) — the grid lane wires it into a cell.
413
+ */
414
+ function CommentCountBadge({ count, hovered, onOpen, onOpenPreview, label: labelProp, previewLabel: previewLabelProp, className, }) {
415
+ const label = (0, core_1.useCrispMessage)(labelProp, count === 1 ? "1 comment" : `${count} comments`);
416
+ const previewLabel = (0, core_1.useCrispMessage)(previewLabelProp, "Open preview");
417
+ if (count === 0 && !hovered)
418
+ return null;
419
+ if (!hovered) {
420
+ return ((0, jsx_runtime_1.jsx)("span", { className: className
421
+ ? `crisp-comments-badge ${className}`
422
+ : "crisp-comments-badge", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("span", { className: "crisp-comments-badge-dot" }) }));
423
+ }
424
+ return ((0, jsx_runtime_1.jsxs)("span", { className: className ? `crisp-comments-badge ${className}` : "crisp-comments-badge", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-comments-badge", "aria-label": label, onClick: onOpen, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.MessageSquare, { size: 12, strokeWidth: 2, "aria-hidden": "true" }), count > 0 && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-comments-badge-count", children: count }))] }), onOpenPreview && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-comments-badge", "aria-label": previewLabel, onClick: onOpenPreview, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpRight, { "data-rtl-flip": "", size: 12, strokeWidth: 2, "aria-hidden": "true" }) }))] }));
425
+ }
426
+ CommentCountBadge.displayName = "CommentCountBadge";
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.commentsRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // Per-record comment threads (crisp#625) — measured live on app.attio.com's
7
+ // own per-record comments panel (open the "zz-parity-crisp-comments" test
8
+ // company's Overview tab, click the speech-bubble icon top-right of the
9
+ // record header). Unlike `RecordPanel`'s peek, this panel is laid out
10
+ // IN-FLOW beside the record view, not floated over it: `getComputedStyle`
11
+ // on the panel wrapper read a shadow of `none` and no left-edge border at all
12
+ // (elevation.floating was tried first and contradicted the live read), so
13
+ // this recipe carries no shadow of its own — only a hairline
14
+ // `border-inline-start` for the seam visible in every screenshot. Width read
15
+ // 319px in situ; used here as 320px (one hairline off, almost certainly a
16
+ // scrollbar/subpixel artifact of the sampled window, not a real 319px
17
+ // design value — flag this if a future pass measures a *different* window
18
+ // width and still gets an odd number).
19
+ const config = (0, core_1.defineRecipe)({
20
+ name: "comments",
21
+ base: `display: flex; flex-direction: column; box-sizing: border-box; width: 320px; height: 100%;
22
+ background: ${tokens_1.contract.bgSurface}; border-inline-start: 1px solid ${tokens_1.contract.borderSubtle};
23
+ font-family: ${tokens_1.contract.font.sans};
24
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
25
+ & * { box-sizing: border-box; }
26
+
27
+ & .crisp-comments-head { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; flex-shrink: 0; height: 49px; padding: 0 ${tokens_1.contract.space["2"]} 0 ${tokens_1.contract.space["4"]}; }
28
+ & .crisp-comments-headbtn { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: none; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s; }
29
+ & .crisp-comments-headbtn:hover { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
30
+ & .crisp-comments-title-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; }
31
+ & .crisp-comments-title { font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
32
+ & .crisp-comments-breadcrumb { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; font-size: 12px; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
33
+
34
+ & .crisp-comments-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 ${tokens_1.contract.space["4"]}; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["4"]}; }
35
+ & .crisp-comments-body--pad-top { padding-top: ${tokens_1.contract.space["3"]}; }
36
+
37
+ /* One comment row -- avatar 16px (Avatar size="sm"), 6px gap to the
38
+ name/time line, 14px/600 name + 12px/500 63%-muted time, 14px/400 body
39
+ two lines below. Hover actions fade in flush with the name line. */
40
+ & .crisp-comments-row { position: relative; display: flex; gap: ${tokens_1.contract.space["1.5"]}; }
41
+ & .crisp-comments-row-main { flex: 1; min-width: 0; }
42
+ & .crisp-comments-row-head { display: flex; align-items: baseline; gap: ${tokens_1.contract.space["1.5"]}; height: 20px; }
43
+ & .crisp-comments-author { font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.semibold}; color: ${tokens_1.contract.fgPrimary}; }
44
+ & .crisp-comments-time { font-size: 12px; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); white-space: nowrap; }
45
+ & .crisp-comments-resolved-label { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; margin-bottom: ${tokens_1.contract.space["1"]}; font-size: 12px; font-weight: ${tokens_1.contract.weight.medium}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); }
46
+ & .crisp-comments-body-text { font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.regular}; color: ${tokens_1.contract.fgPrimary}; white-space: pre-wrap; word-break: break-word; }
47
+
48
+ /* Hover actions -- measured: 4 buttons, 28x28, radius 8, flush
49
+ side-by-side (0 gap), transparent by default, 16px icon at 55% ink. */
50
+ & .crisp-comments-row-actions { position: absolute; top: 0; inset-inline-end: 0; display: flex; opacity: 0; pointer-events: none; background: ${tokens_1.contract.bgSurface}; }
51
+ & .crisp-comments-row:hover .crisp-comments-row-actions,
52
+ & .crisp-comments-row-actions:focus-within { opacity: 1; pointer-events: auto; }
53
+ & .crisp-comments-action { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; margin: 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, color .12s; }
54
+ & .crisp-comments-action:hover { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
55
+ & .crisp-comments-action[data-active] { color: ${tokens_1.contract.bgBrandSolid}; }
56
+
57
+ /* Reactions -- measured: 28px pill, radius 8, subtle 4% ink fill,
58
+ padding 4px 8px 4px 6px, gap 4px between the emoji glyph and count. */
59
+ & .crisp-comments-reactions { display: flex; flex-wrap: wrap; gap: ${tokens_1.contract.space["1"]}; margin-top: ${tokens_1.contract.space["1.5"]}; }
60
+ & .crisp-comments-reaction { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: 28px; padding: 4px 8px 4px 6px; border: none; border-radius: ${tokens_1.contract.radius.md}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); font-family: inherit; font-size: 12px; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary}; cursor: pointer; }
61
+ & .crisp-comments-reaction[data-reacted] { background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 12%, transparent); color: ${tokens_1.contract.bgBrandSolid}; }
62
+ & .crisp-comments-reaction-add { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; font-family: inherit; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 55%, transparent); cursor: pointer; opacity: 0; transition: opacity .12s, background-color .12s; }
63
+ & .crisp-comments-row:hover .crisp-comments-reaction-add { opacity: 1; }
64
+ & .crisp-comments-reaction-add:hover { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
65
+
66
+ /* Reply-in-thread view -- an indented block of replies below the root. */
67
+ & .crisp-comments-replies { display: flex; flex-direction: column; gap: ${tokens_1.contract.space["4"]}; margin-top: ${tokens_1.contract.space["3"]}; padding-inline-start: ${tokens_1.contract.space["4"]}; border-inline-start: 1px solid ${tokens_1.contract.borderSubtle}; }
68
+
69
+ & .crisp-comments-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: ${tokens_1.contract.space["1"]}; padding: ${tokens_1.contract.space["6"]} ${tokens_1.contract.space["4"]}; text-align: center; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 55%, transparent); font-size: ${tokens_1.contract.text.lg}; }
70
+
71
+ /* Composer -- measured: 14px/20px body text, 16px gap above the
72
+ reply/mention icon row, Comment button 84x32 padding 6px 10px radius
73
+ 9 (= radius.lg) brand-solid fill, 14px/500 white label, opacity .4
74
+ disabled / 1 enabled (no color/background change between the two). */
75
+ & .crisp-comments-composer { flex-shrink: 0; padding: ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["4"]} ${tokens_1.contract.space["4"]}; border-top: 1px solid ${tokens_1.contract.borderSubtle}; }
76
+ & .crisp-comments-field { min-height: 20px; max-height: 120px; overflow-y: auto; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.regular}; color: ${tokens_1.contract.fgPrimary}; outline: none; white-space: pre-wrap; word-break: break-word; }
77
+ & .crisp-comments-field:focus-visible { outline: 2px solid ${tokens_1.contract.strokeFocusRing}; outline-offset: 2px; }
78
+ & .crisp-comments-field:empty::before { content: attr(data-placeholder); color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 40%, transparent); }
79
+ & .crisp-comments-composer-foot { display: flex; align-items: center; justify-content: space-between; margin-top: ${tokens_1.contract.space["4"]}; }
80
+ & .crisp-comments-composer-tools { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; }
81
+ & .crisp-comments-tool { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.sm}; background: none; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 45%, transparent); font-family: inherit; cursor: pointer; }
82
+ & .crisp-comments-tool:hover { color: ${tokens_1.contract.fgPrimary}; }
83
+ /* Same fill/shadow as Button's own intent="primary" (crisp/button.recipe.ts) --
84
+ confirmed byte-identical against the live "Comment" button's computed
85
+ box-shadow, so this reuses that literal rather than re-deriving it.
86
+ 32px/radius.lg/6px-10px padding has no matching Button \`size\` yet
87
+ (24/28/40 are the three that exist), so this stays hand-rolled instead
88
+ of stretching one of those. */
89
+ & .crisp-comments-post { display: inline-flex; align-items: center; justify-content: center; height: 32px; padding: 6px ${tokens_1.contract.space["2.5"]}; border: none; border-radius: ${tokens_1.contract.radius.lg}; background: ${tokens_1.contract.bgBrandSolid}; box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08); font-family: inherit; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.semibold}; color: ${tokens_1.contract.fgOnBrand}; cursor: pointer; opacity: 1; transition: opacity .12s, background-color .12s; }
90
+ & .crisp-comments-post:hover:not(:disabled) { background: ${tokens_1.contract.bgBrandSolidPressed}; }
91
+ & .crisp-comments-post:disabled { opacity: .4; cursor: default; }
92
+
93
+ /* Filter menu -- measured: 220px, radius 12 (radius.xl), 4px padding,
94
+ elevation.floating -- a plain reuse of crisp's own Menu, so no rules
95
+ live here; kept as a comment for the record. */
96
+
97
+ /* An inserted "@mention" chip in the composer -- same shape as
98
+ RichTextEditor's own \`.crisp-rte-mention\` (crisp#428), reused here
99
+ rather than re-derived since it's the same atomic-chip affordance. */
100
+ & .crisp-comments-mention { display: inline; padding: 0 2px; border-radius: 4px; background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 12%, transparent); color: ${tokens_1.contract.bgBrandSolid}; font-weight: ${tokens_1.contract.weight.medium}; user-select: none; }
101
+
102
+ /* Reaction picker -- see \`ReactionPicker\`'s own doc comment: the exact
103
+ chrome wasn't independently pixel-measured this pass (screenshot
104
+ capture broke mid-session), so this reuses Menu's already-measured
105
+ floating-popover shell rather than a new value. */
106
+ & .crisp-comments-reactionpicker { width: 240px; padding: ${tokens_1.contract.space["2"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgSurface}; box-shadow: ${tokens_1.elevation.floating}; }
107
+ & .crisp-comments-reactionpicker-search { box-sizing: border-box; width: 100%; height: 28px; margin-bottom: ${tokens_1.contract.space["2"]}; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgRaised}; font-family: inherit; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgPrimary}; }
108
+ & .crisp-comments-reactionpicker-search::placeholder { color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 40%, transparent); }
109
+ & .crisp-comments-reactionpicker-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: ${tokens_1.contract.space["0.5"]}; max-height: 160px; overflow-y: auto; }
110
+ & .crisp-comments-reactionpicker-item { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: none; border-radius: ${tokens_1.contract.radius.sm}; background: none; font-family: inherit; font-size: 16px; line-height: 1; cursor: pointer; }
111
+ & .crisp-comments-reactionpicker-item:hover { background: ${tokens_1.contract.bgRaised}; }
112
+
113
+ /* Row badge (CommentCountBadge) -- lives in its own class family so the
114
+ grid lanes can drop it into a cell without pulling in the panel's
115
+ CSS. Persistent state: a 6px grey dot. Hover state: a 12px comment
116
+ icon + 12px count text, both at 55% ink, replacing the dot. */
117
+ & .crisp-comments-badge { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; border: none; background: none; padding: 0; margin: 0; font-family: inherit; cursor: pointer; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 55%, transparent); }
118
+ & .crisp-comments-badge-dot { width: 6px; height: 6px; border-radius: ${tokens_1.contract.radius.full}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 38%, transparent); }
119
+ & .crisp-comments-badge-count { font-size: 12px; line-height: 16px; }
120
+ ${core_1.rtlFlipCss}`,
121
+ variants: {},
122
+ defaultVariants: {},
123
+ compoundVariants: [],
124
+ });
125
+ exports.commentsRecipe = (0, core_1.resolveRecipe)(config);
126
+ exports.default = config;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.commentsRecipe = exports.CommentsPanel = exports.CommentCountBadge = void 0;
4
+ var comments_1 = require("./comments");
5
+ Object.defineProperty(exports, "CommentCountBadge", { enumerable: true, get: function () { return comments_1.CommentCountBadge; } });
6
+ Object.defineProperty(exports, "CommentsPanel", { enumerable: true, get: function () { return comments_1.CommentsPanel; } });
7
+ var comments_recipe_1 = require("./comments.recipe");
8
+ Object.defineProperty(exports, "commentsRecipe", { enumerable: true, get: function () { return comments_recipe_1.commentsRecipe; } });
@@ -20,10 +20,18 @@ const tokens_1 = require("../../tokens");
20
20
  // `title`/`trailing` slot content a consumer supplies. `size.header` (48px)
21
21
  // is reused rather than a new token, since it's the same "workspace-switcher
22
22
  // header height" measured elsewhere in the sidebar.
23
+ //
24
+ // crisp#576: `border-box` plus a plain `height: ${size.header}` meant the
25
+ // bordered variant's own 1px border was eaten OUT OF the 48px box, leaving
26
+ // only 47px of content -- caught in review of internal/grc's own shell
27
+ // shifting up 1px end-to-end after adopting this component. Re-measured
28
+ // live: Attio's own `<header>` is 49px total (border-box) with the 1px
29
+ // border OUTSIDE the 48px content row, not inside it. `[data-bordered]` now
30
+ // grows the box by the border's own width so content stays 48px either way.
23
31
  const config = (0, core_1.defineRecipe)({
24
32
  name: "headerbar",
25
33
  base: `box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: ${tokens_1.contract.space["2"]}; height: ${tokens_1.contract.size.header}; flex-shrink: 0; padding: 0 ${tokens_1.contract.space["3"]}; font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
26
- &[data-bordered] { border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; }
34
+ &[data-bordered] { height: calc(${tokens_1.contract.size.header} + 1px); border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; }
27
35
  & .crisp-headerbar-leading { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; flex-shrink: 0; min-width: 0; }
28
36
  & .crisp-headerbar-title { flex: 1; min-width: 0; display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; overflow: hidden; }
29
37
  & .crisp-headerbar-trailing { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; flex-shrink: 0; }`,
@@ -15,7 +15,16 @@ const config = (0, core_1.defineRecipe)({
15
15
  & .crisp-listpage-toolbar { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: ${tokens_1.contract.size.header}; flex-shrink: 0; padding: 0 ${tokens_1.contract.space["3"]}; }
16
16
  & .crisp-listpage-toolbar-lead { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; }
17
17
  & .crisp-listpage-count { margin-inline-start: auto; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; white-space: nowrap; }
18
- & .crisp-listpage-toolbar-trail { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; }
18
+ /* #641: trailing (View settings / New) has to reach the toolbar's own
19
+ inline-end edge on its own -- the only thing pushing it there used
20
+ to be .crisp-listpage-count's margin-inline-start: auto, which a
21
+ consumer with no count badge (TasksTable, measured live: trailing
22
+ sat right after leading, nowhere near the row's own right edge)
23
+ never renders. A bare margin-inline-start: auto here is safely
24
+ overridden by the more specific rule right below whenever count IS
25
+ present, so the two cases don't fight -- count's own auto margin
26
+ still does the pushing there, this one only fires when it's absent. */
27
+ & .crisp-listpage-toolbar-trail { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; margin-inline-start: auto; }
19
28
  & .crisp-listpage-count + .crisp-listpage-toolbar-trail { margin-inline-start: ${tokens_1.contract.space["3"]}; }
20
29
 
21
30
  & .crisp-listpage-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["3"]}; }