@handled-ai/design-system 0.20.0 → 0.20.2

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 (33) hide show
  1. package/dist/components/conversation-panel.d.ts +1 -1
  2. package/dist/components/conversation-panel.js +282 -15
  3. package/dist/components/conversation-panel.js.map +1 -1
  4. package/dist/components/owner-chips.d.ts +3 -4
  5. package/dist/components/owner-chips.js +77 -41
  6. package/dist/components/owner-chips.js.map +1 -1
  7. package/dist/components/score-why-chips.d.ts +1 -1
  8. package/dist/components/signal-priority-popover.d.ts +1 -1
  9. package/dist/components/timeline-activity.d.ts +4 -2
  10. package/dist/components/timeline-activity.js +366 -154
  11. package/dist/components/timeline-activity.js.map +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/prototype/index.d.ts +1 -1
  14. package/dist/prototype/prototype-accounts-view.d.ts +1 -1
  15. package/dist/prototype/prototype-admin-view.d.ts +1 -1
  16. package/dist/prototype/prototype-config.d.ts +1 -1
  17. package/dist/prototype/prototype-inbox-view.d.ts +9 -3
  18. package/dist/prototype/prototype-inbox-view.js +94 -47
  19. package/dist/prototype/prototype-inbox-view.js.map +1 -1
  20. package/dist/prototype/prototype-insights-view.d.ts +1 -1
  21. package/dist/prototype/prototype-shell.d.ts +1 -1
  22. package/dist/{signal-priority-popover-Cg9XPJsp.d.ts → signal-priority-popover-BJHd07dU.d.ts} +6 -0
  23. package/package.json +1 -1
  24. package/src/components/__tests__/conversation-panel.test.tsx +276 -0
  25. package/src/components/__tests__/owner-chips.test.tsx +137 -17
  26. package/src/components/__tests__/timeline-activity.test.tsx +92 -1
  27. package/src/components/conversation-panel.tsx +358 -21
  28. package/src/components/owner-chips.tsx +98 -63
  29. package/src/components/timeline-activity.tsx +452 -160
  30. package/src/prototype/__tests__/detail-view-case-panel-v2.test.tsx +181 -0
  31. package/src/prototype/__tests__/detail-view-timeline-system-events.test.tsx +16 -2
  32. package/src/prototype/prototype-config.ts +6 -0
  33. package/src/prototype/prototype-inbox-view.tsx +128 -51
@@ -84,7 +84,7 @@ interface ConversationPanelProps {
84
84
  /** Deployment brand, used in the paused-playbook copy. */
85
85
  tenantName?: string;
86
86
  onSendReply?: (payload: ConversationReplyPayload) => void | Promise<void>;
87
- onCreateGmailDraft?: (payload: ConversationReplyPayload) => void;
87
+ onCreateGmailDraft?: (payload: ConversationReplyPayload) => void | Promise<void>;
88
88
  onOpenInGmail?: (threadId: string) => void;
89
89
  /** Inline-open this thread initially (defaults to the first responded one). */
90
90
  defaultOpenThreadId?: string;
@@ -69,6 +69,248 @@ function escapeHtml(s) {
69
69
  function textToHtml(text) {
70
70
  return text.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean).map((p) => `<p>${escapeHtml(p).replace(/\n/g, "<br>")}</p>`).join("");
71
71
  }
72
+ const SPLITTABLE_BLOCK_TAGS = /* @__PURE__ */ new Set(["p", "div"]);
73
+ const WHOLE_BLOCK_TAGS = /* @__PURE__ */ new Set(["blockquote", "table", "ul", "ol", "hr"]);
74
+ const BLOCK_TAGS = /* @__PURE__ */ new Set([...SPLITTABLE_BLOCK_TAGS, ...WHOLE_BLOCK_TAGS]);
75
+ const BR_TAG_RE = /<br\s*\/?>/gi;
76
+ const HTML_BLOCK_START_RE = /<(p|div|blockquote|table|ul|ol|hr)\b[^>]*>/i;
77
+ const SIGNATURE_DELIMITER_RE = /^--\s*$/;
78
+ const SIGNOFF_RE = /^(?:thanks,|thank you,|best,|regards,|sincerely,)$/i;
79
+ const DETAILS_START_RE = /^(?:confidentiality notice\b|this message and any attachments\b|this email and any attachments\b|the information contained in this message\b|this communication may contain\b|unsubscribe\b|manage your preferences\b)/i;
80
+ const CONTACT_DETAIL_RE = /(?:@|https?:\/\/|www\.|\+?\d[\d\s().-]{6,}\d|\b(?:ceo|cfo|cto|coo|founder|co-founder|director|manager|vp|vice president|president|head of|sales|marketing|operations|account|customer success|success|support|engineer|consultant|partner|principal|advisor|associate)\b|\b(?:inc|llc|ltd|corp|corporation|company|co\.)\b)/i;
81
+ function decodeHtmlText(value) {
82
+ const withoutTags = value.replace(BR_TAG_RE, "\n").replace(/<\/(p|div|blockquote|li|tr|table|ul|ol)>/gi, "\n").replace(/<[^>]*>/g, "");
83
+ if (typeof document !== "undefined") {
84
+ const textarea = document.createElement("textarea");
85
+ textarea.innerHTML = withoutTags;
86
+ return textarea.value;
87
+ }
88
+ return withoutTags.replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;|&apos;/gi, "'");
89
+ }
90
+ function htmlToVisibleText(html) {
91
+ return decodeHtmlText(html).replace(/\u00a0/g, " ").replace(/[ \t]+/g, " ").trim();
92
+ }
93
+ function serializeNode(node) {
94
+ const host = document.createElement("div");
95
+ host.appendChild(node.cloneNode(true));
96
+ return host.innerHTML;
97
+ }
98
+ function wrapHtmlLike(element, innerHtml) {
99
+ const clone = element.cloneNode(false);
100
+ clone.innerHTML = innerHtml;
101
+ return clone.outerHTML;
102
+ }
103
+ function hasDirectBr(nodes) {
104
+ return nodes.some((node) => node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === "br");
105
+ }
106
+ function hasDirectBlockChild(element) {
107
+ return Array.from(element.children).some((child) => {
108
+ const tagName = child.tagName.toLowerCase();
109
+ return tagName !== "br" && BLOCK_TAGS.has(tagName);
110
+ });
111
+ }
112
+ function makeHtmlSegment(html) {
113
+ const visibleText = htmlToVisibleText(html);
114
+ if (!html.trim() || !visibleText && !/<(?:img|hr)\b/i.test(html)) return null;
115
+ return { html, visibleText };
116
+ }
117
+ function splitInlineNodes(nodes, wrapper) {
118
+ const containsBr = hasDirectBr(nodes);
119
+ const chunks = [[]];
120
+ nodes.forEach((node) => {
121
+ var _a;
122
+ if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === "br") {
123
+ chunks.push([]);
124
+ return;
125
+ }
126
+ (_a = chunks[chunks.length - 1]) == null ? void 0 : _a.push(serializeNode(node));
127
+ });
128
+ return chunks.map((chunk) => chunk.join("")).map((innerHtml) => {
129
+ if (wrapper) return wrapHtmlLike(wrapper, innerHtml);
130
+ return containsBr ? `<div>${innerHtml}</div>` : innerHtml;
131
+ }).map(makeHtmlSegment).filter((segment) => Boolean(segment));
132
+ }
133
+ function splitElementSegment(element) {
134
+ const tagName = element.tagName.toLowerCase();
135
+ if (tagName === "div" && hasDirectBlockChild(element)) {
136
+ const childSegments = splitHtmlNodes(Array.from(element.childNodes));
137
+ return childSegments.length ? childSegments : [makeHtmlSegment(element.outerHTML)].filter(Boolean);
138
+ }
139
+ if (SPLITTABLE_BLOCK_TAGS.has(tagName) && hasDirectBr(Array.from(element.childNodes)) && !hasDirectBlockChild(element)) {
140
+ return splitInlineNodes(Array.from(element.childNodes), element);
141
+ }
142
+ const segment = makeHtmlSegment(element.outerHTML);
143
+ return segment ? [segment] : [];
144
+ }
145
+ function splitHtmlNodes(nodes) {
146
+ const segments = [];
147
+ let inlineNodes = [];
148
+ const flushInline = () => {
149
+ if (!inlineNodes.length) return;
150
+ segments.push(...splitInlineNodes(inlineNodes));
151
+ inlineNodes = [];
152
+ };
153
+ nodes.forEach((node) => {
154
+ if (node.nodeType === Node.ELEMENT_NODE) {
155
+ const element = node;
156
+ const tagName = element.tagName.toLowerCase();
157
+ if (BLOCK_TAGS.has(tagName)) {
158
+ flushInline();
159
+ segments.push(...splitElementSegment(element));
160
+ return;
161
+ }
162
+ }
163
+ inlineNodes.push(node);
164
+ });
165
+ flushInline();
166
+ return segments;
167
+ }
168
+ function findMatchingCloseTag(html, tagName, openTagEnd) {
169
+ if (tagName === "hr") return openTagEnd + 1;
170
+ const tagPattern = new RegExp(`</?${tagName}\\b[^>]*>`, "gi");
171
+ tagPattern.lastIndex = openTagEnd + 1;
172
+ let depth = 1;
173
+ let match;
174
+ while ((match = tagPattern.exec(html)) !== null) {
175
+ const rawTag = match[0];
176
+ if (/^<\//.test(rawTag)) depth -= 1;
177
+ else if (!/\/\s*>$/.test(rawTag)) depth += 1;
178
+ if (depth === 0) return tagPattern.lastIndex;
179
+ }
180
+ return html.length;
181
+ }
182
+ function splitHtmlSegmentsFallback(html) {
183
+ const segments = [];
184
+ let cursor = 0;
185
+ const pushInline = (inlineHtml) => {
186
+ const chunks = inlineHtml.split(BR_TAG_RE);
187
+ const hadBr = chunks.length > 1;
188
+ chunks.forEach((chunk) => {
189
+ const segment = makeHtmlSegment(hadBr ? `<div>${chunk}</div>` : chunk);
190
+ if (segment) segments.push(segment);
191
+ });
192
+ };
193
+ while (cursor < html.length) {
194
+ const rest = html.slice(cursor);
195
+ const match = HTML_BLOCK_START_RE.exec(rest);
196
+ if (!match || match.index === void 0) {
197
+ pushInline(rest);
198
+ break;
199
+ }
200
+ if (match.index > 0) pushInline(rest.slice(0, match.index));
201
+ const tagStart = cursor + match.index;
202
+ const rawOpen = match[0];
203
+ const tagName = match[1].toLowerCase();
204
+ const openTagEnd = tagStart + rawOpen.length - 1;
205
+ const segmentEnd = findMatchingCloseTag(html, tagName, openTagEnd);
206
+ const blockHtml = html.slice(tagStart, segmentEnd);
207
+ if (SPLITTABLE_BLOCK_TAGS.has(tagName) && BR_TAG_RE.test(blockHtml)) {
208
+ const openTag = rawOpen;
209
+ const closeTag = `</${tagName}>`;
210
+ const inner = blockHtml.replace(new RegExp(`^${rawOpen.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i"), "").replace(new RegExp(`${closeTag}$`, "i"), "");
211
+ inner.split(BR_TAG_RE).forEach((chunk) => {
212
+ const segment = makeHtmlSegment(`${openTag}${chunk}${closeTag}`);
213
+ if (segment) segments.push(segment);
214
+ });
215
+ } else {
216
+ const segment = makeHtmlSegment(blockHtml);
217
+ if (segment) segments.push(segment);
218
+ }
219
+ cursor = segmentEnd;
220
+ }
221
+ return segments;
222
+ }
223
+ function segmentHtmlMessage(html) {
224
+ if (typeof document === "undefined" || typeof Node === "undefined") {
225
+ return splitHtmlSegmentsFallback(html);
226
+ }
227
+ const template = document.createElement("template");
228
+ template.innerHTML = html;
229
+ return splitHtmlNodes(Array.from(template.content.childNodes));
230
+ }
231
+ function segmentTextMessage(text) {
232
+ var _a;
233
+ const lines = (_a = text.match(/[^\n]*(?:\n|$)/g)) != null ? _a : [];
234
+ return lines.filter((line, index) => line.length > 0 && !(index === lines.length - 1 && line === "")).map((line) => ({ text: line, visibleText: line.replace(/\u00a0/g, " ").trim() }));
235
+ }
236
+ function firstVisibleLine(text) {
237
+ var _a, _b;
238
+ return (_b = (_a = text.replace(/\u00a0/g, " ").trimStart().split(/\r?\n/).find((line) => line.trim())) == null ? void 0 : _a.trim()) != null ? _b : "";
239
+ }
240
+ function isLikelySenderNameLine(line) {
241
+ if (!line || line.length > 60) return false;
242
+ if (/[,@:;!?]|https?:\/\/|www\.|\d/.test(line)) return false;
243
+ const words = line.split(/\s+/).filter(Boolean);
244
+ if (words.length < 1 || words.length > 4) return false;
245
+ return words.every((word) => /^[A-Z][A-Za-z'.-]*$/.test(word));
246
+ }
247
+ function nextVisibleSegmentText(segments, fromIndex) {
248
+ var _a, _b;
249
+ for (let index = fromIndex + 1; index < segments.length; index += 1) {
250
+ const text = firstVisibleLine((_b = (_a = segments[index]) == null ? void 0 : _a.visibleText) != null ? _b : "");
251
+ if (text) return text;
252
+ }
253
+ return "";
254
+ }
255
+ function isFooterBoundary(segments, index) {
256
+ var _a, _b;
257
+ const line = firstVisibleLine((_b = (_a = segments[index]) == null ? void 0 : _a.visibleText) != null ? _b : "");
258
+ if (!line) return false;
259
+ if (SIGNATURE_DELIMITER_RE.test(line) || DETAILS_START_RE.test(line)) return true;
260
+ const nextText = nextVisibleSegmentText(segments, index);
261
+ if (SIGNOFF_RE.test(line)) return Boolean(nextText && (isLikelySenderNameLine(nextText) || CONTACT_DETAIL_RE.test(nextText)));
262
+ return isLikelySenderNameLine(line) && CONTACT_DETAIL_RE.test(nextText);
263
+ }
264
+ function splitFooterSegments(segments) {
265
+ const visibleIndexes = segments.map((segment, index) => segment.visibleText ? index : -1).filter((index) => index >= 0);
266
+ const visibleCount = visibleIndexes.length;
267
+ if (visibleCount < 2) return { bodySegments: segments, detailsSegments: [] };
268
+ const trailingCount = Math.max(Math.ceil(visibleCount * 0.4), 8);
269
+ const firstTrailingOrdinal = Math.max(1, visibleCount - trailingCount);
270
+ for (let ordinal = firstTrailingOrdinal; ordinal < visibleCount; ordinal += 1) {
271
+ const index = visibleIndexes[ordinal];
272
+ if (ordinal > 0 && isFooterBoundary(segments, index)) {
273
+ return { bodySegments: segments.slice(0, index), detailsSegments: segments.slice(index) };
274
+ }
275
+ }
276
+ return { bodySegments: segments, detailsSegments: [] };
277
+ }
278
+ function splitMessageHtml(rawHtml) {
279
+ const sanitizedHtml = sanitizeHtml(rawHtml);
280
+ const { bodySegments, detailsSegments } = splitFooterSegments(segmentHtmlMessage(sanitizedHtml));
281
+ if (!detailsSegments.length) return { bodyHtml: sanitizedHtml, detailsHtml: "" };
282
+ return {
283
+ bodyHtml: bodySegments.map((segment) => {
284
+ var _a;
285
+ return (_a = segment.html) != null ? _a : "";
286
+ }).join(""),
287
+ detailsHtml: detailsSegments.map((segment) => {
288
+ var _a;
289
+ return (_a = segment.html) != null ? _a : "";
290
+ }).join("")
291
+ };
292
+ }
293
+ function splitMessageText(text) {
294
+ const { bodySegments, detailsSegments } = splitFooterSegments(segmentTextMessage(text));
295
+ if (!detailsSegments.length) return { bodyText: text, detailsText: "" };
296
+ return {
297
+ bodyText: bodySegments.map((segment) => {
298
+ var _a;
299
+ return (_a = segment.text) != null ? _a : "";
300
+ }).join(""),
301
+ detailsText: detailsSegments.map((segment) => {
302
+ var _a;
303
+ return (_a = segment.text) != null ? _a : "";
304
+ }).join("")
305
+ };
306
+ }
307
+ function messageBodySnippet(message, maxLength = 140) {
308
+ var _a, _b, _c;
309
+ if (message.bodyHtml) {
310
+ return htmlToTextSnippet(splitMessageHtml(message.bodyHtml).bodyHtml, maxLength);
311
+ }
312
+ return (_c = (_b = splitMessageText((_a = message.body) != null ? _a : "").bodyText.split("\n").find((line) => line.trim())) == null ? void 0 : _b.trim()) != null ? _c : "";
313
+ }
72
314
  function GmailMark({ size = 14 }) {
73
315
  return (
74
316
  // eslint-disable-next-line @next/next/no-img-element
@@ -114,10 +356,11 @@ function MessageView({
114
356
  onToggle,
115
357
  me
116
358
  }) {
117
- var _a, _b, _c;
359
+ var _a, _b;
118
360
  const [quoteOpen, setQuoteOpen] = React.useState(false);
119
- const snippet = (_b = (_a = message.body) == null ? void 0 : _a.split("\n").find(Boolean)) != null ? _b : message.bodyHtml ? htmlToTextSnippet(message.bodyHtml, 140) : "";
361
+ const [detailsOpen, setDetailsOpen] = React.useState(false);
120
362
  if (!expanded) {
363
+ const snippet = messageBodySnippet(message, 140);
121
364
  return /* @__PURE__ */ jsxs(
122
365
  "button",
123
366
  {
@@ -132,13 +375,16 @@ function MessageView({
132
375
  " \xB7 ",
133
376
  snippet
134
377
  ] }),
135
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60 shrink-0 text-xs", children: (_c = message.ago) != null ? _c : message.date }),
378
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60 shrink-0 text-xs", children: (_a = message.ago) != null ? _a : message.date }),
136
379
  /* @__PURE__ */ jsx(ChevronDown, { size: 13, className: "text-muted-foreground shrink-0" })
137
380
  ]
138
381
  }
139
382
  );
140
383
  }
141
384
  const toLabel = me && message.to.email === me.email ? "me" : firstName(message.to.name);
385
+ const htmlParts = message.bodyHtml ? splitMessageHtml(message.bodyHtml) : null;
386
+ const textParts = message.bodyHtml ? null : splitMessageText((_b = message.body) != null ? _b : "");
387
+ const hasDetails = Boolean((htmlParts == null ? void 0 : htmlParts.detailsHtml) || (textParts == null ? void 0 : textParts.detailsText));
142
388
  return /* @__PURE__ */ jsxs("div", { "data-slot": "conv-message", className: "rounded-md border border-border bg-background", children: [
143
389
  /* @__PURE__ */ jsxs(
144
390
  "button",
@@ -173,8 +419,21 @@ function MessageView({
173
419
  ]
174
420
  }
175
421
  ),
176
- /* @__PURE__ */ jsxs("div", { className: "px-3 pb-3", children: [
177
- message.bodyHtml ? /* @__PURE__ */ jsx("div", { className: PROSE, dangerouslySetInnerHTML: { __html: sanitizeHtml(message.bodyHtml) } }) : /* @__PURE__ */ jsx("div", { className: cn(PROSE, "whitespace-pre-line"), children: message.body }),
422
+ /* @__PURE__ */ jsxs("div", { className: "px-3 pb-2.5", children: [
423
+ htmlParts ? /* @__PURE__ */ jsx("div", { "data-slot": "conv-message-body", className: PROSE, dangerouslySetInnerHTML: { __html: htmlParts.bodyHtml } }) : /* @__PURE__ */ jsx("div", { "data-slot": "conv-message-body", className: cn(PROSE, "whitespace-pre-line"), children: textParts == null ? void 0 : textParts.bodyText }),
424
+ hasDetails ? /* @__PURE__ */ jsxs("div", { className: "mt-2", children: [
425
+ /* @__PURE__ */ jsx(
426
+ "button",
427
+ {
428
+ type: "button",
429
+ onClick: () => setDetailsOpen((v) => !v),
430
+ className: "text-muted-foreground hover:text-foreground hover:bg-muted rounded px-1.5 text-xs leading-5",
431
+ "aria-expanded": detailsOpen,
432
+ children: detailsOpen ? "Hide signature/details" : "Show signature/details"
433
+ }
434
+ ),
435
+ detailsOpen ? /* @__PURE__ */ jsx("div", { className: "border-border text-muted-foreground mt-1 border-l-2 pl-3 text-[13px]", children: htmlParts ? /* @__PURE__ */ jsx("div", { "data-slot": "conv-message-details", className: PROSE, dangerouslySetInnerHTML: { __html: htmlParts.detailsHtml } }) : /* @__PURE__ */ jsx("div", { "data-slot": "conv-message-details", className: cn(PROSE, "whitespace-pre-line"), children: textParts == null ? void 0 : textParts.detailsText }) }) : null
436
+ ] }) : null,
178
437
  message.quoted ? /* @__PURE__ */ jsxs("div", { className: "mt-2", children: [
179
438
  /* @__PURE__ */ jsx(
180
439
  "button",
@@ -224,6 +483,18 @@ function ReplyComposer({
224
483
  setSending(false);
225
484
  }
226
485
  };
486
+ const handleDraft = async () => {
487
+ setSending(true);
488
+ setSendError(null);
489
+ try {
490
+ await onDraft(body, sig);
491
+ setPreview(false);
492
+ } catch (error) {
493
+ setSendError(error instanceof Error ? error.message : "Could not create the Gmail draft. Please try again.");
494
+ } finally {
495
+ setSending(false);
496
+ }
497
+ };
227
498
  return /* @__PURE__ */ jsxs("div", { "data-slot": "conv-reply", className: "border-border bg-muted/20 rounded-md border p-3", children: [
228
499
  /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center gap-2", children: [
229
500
  me ? /* @__PURE__ */ jsx(PersonAvatar, { person: me }) : null,
@@ -340,10 +611,7 @@ function ReplyComposer({
340
611
  {
341
612
  type: "button",
342
613
  disabled: sending,
343
- onClick: () => {
344
- setPreview(false);
345
- onDraft(body, sig);
346
- },
614
+ onClick: handleDraft,
347
615
  className: "text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-[13px] disabled:pointer-events-none disabled:opacity-50",
348
616
  children: [
349
617
  /* @__PURE__ */ jsx(GmailMark, { size: 14 }),
@@ -393,7 +661,7 @@ function ThreadBody({
393
661
  return o;
394
662
  });
395
663
  const toggle = (id) => setExpanded((e) => __spreadProps(__spreadValues({}, e), { [id]: !e[id] }));
396
- return /* @__PURE__ */ jsxs("div", { "data-slot": "conv-thread-body", className: "space-y-2", children: [
664
+ return /* @__PURE__ */ jsxs("div", { "data-slot": "conv-thread-body", className: "space-y-1.5", children: [
397
665
  canReply && thread.paused ? /* @__PURE__ */ jsxs("div", { className: "border-status-pending-border bg-status-pending-bg text-status-pending-fg flex items-start gap-2 rounded-md border p-2.5 text-[12px]", children: [
398
666
  /* @__PURE__ */ jsx(Pause, { size: 13, className: "mt-0.5 shrink-0" }),
399
667
  /* @__PURE__ */ jsxs("span", { children: [
@@ -405,7 +673,7 @@ function ThreadBody({
405
673
  " or Gmail."
406
674
  ] })
407
675
  ] }) : null,
408
- /* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: thread.messages.map((m) => /* @__PURE__ */ jsx(MessageView, { message: m, expanded: !!expanded[m.id], onToggle: () => toggle(m.id), me }, m.id)) }),
676
+ /* @__PURE__ */ jsx("div", { className: "space-y-1", children: thread.messages.map((m) => /* @__PURE__ */ jsx(MessageView, { message: m, expanded: !!expanded[m.id], onToggle: () => toggle(m.id), me }, m.id)) }),
409
677
  !canReply ? /* @__PURE__ */ jsxs("div", { className: "border-border bg-muted/30 text-muted-foreground flex items-start gap-2 rounded-md border p-2.5 text-[12px]", children: [
410
678
  /* @__PURE__ */ jsx(Eye, { size: 14, className: "mt-0.5 shrink-0" }),
411
679
  /* @__PURE__ */ jsxs("span", { children: [
@@ -449,8 +717,8 @@ function ThreadBody({
449
717
  await (onSendReply == null ? void 0 : onSendReply({ threadId: thread.threadId, body, includeSignature, replyAll }));
450
718
  setMode("sent");
451
719
  },
452
- onDraft: (body, includeSignature) => {
453
- onCreateGmailDraft == null ? void 0 : onCreateGmailDraft({ threadId: thread.threadId, body, includeSignature, replyAll });
720
+ onDraft: async (body, includeSignature) => {
721
+ await (onCreateGmailDraft == null ? void 0 : onCreateGmailDraft({ threadId: thread.threadId, body, includeSignature, replyAll }));
454
722
  setMode("draft");
455
723
  }
456
724
  }
@@ -497,11 +765,10 @@ function ThreadRow({
497
765
  onCreateGmailDraft,
498
766
  onOpenInGmail
499
767
  }) {
500
- var _a, _b;
501
768
  const status = effectiveStatus(thread);
502
769
  const last = thread.messages[thread.messages.length - 1];
503
770
  const who = (last == null ? void 0 : last.direction) === "inbound" ? firstName(last.from.name) : "You";
504
- const lastSnippet = (_b = (_a = last == null ? void 0 : last.body) == null ? void 0 : _a.split("\n").find(Boolean)) != null ? _b : (last == null ? void 0 : last.bodyHtml) ? htmlToTextSnippet(last.bodyHtml, 120) : "";
771
+ const lastSnippet = last ? messageBodySnippet(last, 120) : "";
505
772
  const pill = STATUS_PILL[status];
506
773
  return /* @__PURE__ */ jsxs("div", { "data-slot": "conv-thread", "data-open": open ? "true" : void 0, className: "border-border border-b last:border-b-0", children: [
507
774
  /* @__PURE__ */ jsxs(