@handled-ai/design-system 0.20.3 → 0.20.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/comment-composer.js +4 -4
- package/dist/components/comment-composer.js.map +1 -1
- package/dist/components/conversation-panel.d.ts +29 -3
- package/dist/components/conversation-panel.js +142 -39
- package/dist/components/conversation-panel.js.map +1 -1
- package/dist/components/owner-chips.js +9 -9
- package/dist/components/owner-chips.js.map +1 -1
- package/dist/components/timeline-activity.js +14 -14
- package/dist/components/timeline-activity.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/comment-composer.test.tsx +28 -0
- package/src/components/__tests__/conversation-panel.test.tsx +217 -0
- package/src/components/__tests__/owner-chips.test.tsx +27 -0
- package/src/components/__tests__/timeline-activity.test.tsx +33 -0
- package/src/components/comment-composer.tsx +4 -4
- package/src/components/conversation-panel.tsx +204 -39
- package/src/components/owner-chips.tsx +12 -10
- package/src/components/timeline-activity.tsx +14 -14
|
@@ -34,12 +34,12 @@ function CommentComposer({
|
|
|
34
34
|
"data-slot": "comment-composer",
|
|
35
35
|
"data-open": open ? "true" : void 0,
|
|
36
36
|
className: cn(
|
|
37
|
-
"border-border bg-background flex items-start gap-2 rounded-lg border
|
|
37
|
+
"border-border bg-background flex items-start gap-2 rounded-lg border px-2 py-1.5 transition-colors",
|
|
38
38
|
open && "ring-ring/30 ring-2",
|
|
39
39
|
className
|
|
40
40
|
),
|
|
41
41
|
children: [
|
|
42
|
-
/* @__PURE__ */ jsxs(Avatar, { size: "sm", className: "mt-
|
|
42
|
+
/* @__PURE__ */ jsxs(Avatar, { size: "sm", className: "mt-px", children: [
|
|
43
43
|
(author == null ? void 0 : author.avatarUrl) ? /* @__PURE__ */ jsx(AvatarImage, { src: author.avatarUrl, alt: (_a = author.name) != null ? _a : "You" }) : null,
|
|
44
44
|
/* @__PURE__ */ jsx(AvatarFallback, { className: "bg-muted text-muted-foreground text-[10px] font-medium uppercase", children: getInitials({ name: author == null ? void 0 : author.name, email: author == null ? void 0 : author.email }) })
|
|
45
45
|
] }),
|
|
@@ -54,7 +54,7 @@ function CommentComposer({
|
|
|
54
54
|
placeholder,
|
|
55
55
|
rows: open ? 3 : 1,
|
|
56
56
|
className: cn(
|
|
57
|
-
"resize-none border-0 bg-transparent
|
|
57
|
+
"resize-none border-0 bg-transparent px-1 py-0.5 text-sm leading-snug shadow-none focus-visible:ring-0",
|
|
58
58
|
!open && "min-h-0"
|
|
59
59
|
),
|
|
60
60
|
onKeyDown: (e) => {
|
|
@@ -65,7 +65,7 @@ function CommentComposer({
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
),
|
|
68
|
-
open ? /* @__PURE__ */ jsxs("div", { className: "mt-
|
|
68
|
+
open ? /* @__PURE__ */ jsxs("div", { className: "mt-0.5 flex items-center justify-between gap-2", children: [
|
|
69
69
|
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground inline-flex items-center gap-1 text-[11px]", children: [
|
|
70
70
|
/* @__PURE__ */ jsx(Lock, { size: 12 }),
|
|
71
71
|
" ",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/comment-composer.tsx"],"sourcesContent":["\"use client\"\n\n/**\n * comment-composer.tsx — an internal-note composer for the case activity\n * timeline. Posting a comment prepends an `operatorNote` event to the log\n * (wired by the consumer). Collapses to a single line; expands on focus or\n * when it has text. ⌘↵ / Ctrl↵ posts.\n *\n * Presentational: `onPost` does the work (the consumer persists the note and\n * adds it to the timeline). Reuses Avatar / Button / Textarea primitives.\n */\n\nimport * as React from \"react\"\nimport { Lock } from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { getInitials } from \"../lib/user-display\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"./avatar\"\nimport { Button } from \"./button\"\nimport { Textarea } from \"./textarea\"\n\nexport interface CommentComposerProps {\n /** Called with the trimmed note text when the operator posts. */\n onPost: (text: string) => void\n /** Current operator (for the avatar). */\n author?: { name?: string; email?: string; avatarUrl?: string | null }\n placeholder?: string\n /** Hint shown in the footer; defaults to the internal-note reassurance. */\n hint?: string\n className?: string\n}\n\nfunction CommentComposer({\n onPost,\n author,\n placeholder = \"Add a comment or internal note…\",\n hint = \"Internal note: only your team sees this\",\n className,\n}: CommentComposerProps) {\n const [text, setText] = React.useState(\"\")\n const [focused, setFocused] = React.useState(false)\n const open = focused || text.length > 0\n const canPost = text.trim().length > 0\n\n const post = () => {\n const value = text.trim()\n if (!value) return\n onPost(value)\n setText(\"\")\n setFocused(false)\n }\n\n return (\n <div\n data-slot=\"comment-composer\"\n data-open={open ? \"true\" : undefined}\n className={cn(\n \"border-border bg-background flex items-start gap-2 rounded-lg border
|
|
1
|
+
{"version":3,"sources":["../../src/components/comment-composer.tsx"],"sourcesContent":["\"use client\"\n\n/**\n * comment-composer.tsx — an internal-note composer for the case activity\n * timeline. Posting a comment prepends an `operatorNote` event to the log\n * (wired by the consumer). Collapses to a single line; expands on focus or\n * when it has text. ⌘↵ / Ctrl↵ posts.\n *\n * Presentational: `onPost` does the work (the consumer persists the note and\n * adds it to the timeline). Reuses Avatar / Button / Textarea primitives.\n */\n\nimport * as React from \"react\"\nimport { Lock } from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { getInitials } from \"../lib/user-display\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"./avatar\"\nimport { Button } from \"./button\"\nimport { Textarea } from \"./textarea\"\n\nexport interface CommentComposerProps {\n /** Called with the trimmed note text when the operator posts. */\n onPost: (text: string) => void\n /** Current operator (for the avatar). */\n author?: { name?: string; email?: string; avatarUrl?: string | null }\n placeholder?: string\n /** Hint shown in the footer; defaults to the internal-note reassurance. */\n hint?: string\n className?: string\n}\n\nfunction CommentComposer({\n onPost,\n author,\n placeholder = \"Add a comment or internal note…\",\n hint = \"Internal note: only your team sees this\",\n className,\n}: CommentComposerProps) {\n const [text, setText] = React.useState(\"\")\n const [focused, setFocused] = React.useState(false)\n const open = focused || text.length > 0\n const canPost = text.trim().length > 0\n\n const post = () => {\n const value = text.trim()\n if (!value) return\n onPost(value)\n setText(\"\")\n setFocused(false)\n }\n\n return (\n <div\n data-slot=\"comment-composer\"\n data-open={open ? \"true\" : undefined}\n className={cn(\n \"border-border bg-background flex items-start gap-2 rounded-lg border px-2 py-1.5 transition-colors\",\n open && \"ring-ring/30 ring-2\",\n className\n )}\n >\n <Avatar size=\"sm\" className=\"mt-px\">\n {author?.avatarUrl ? <AvatarImage src={author.avatarUrl} alt={author.name ?? \"You\"} /> : null}\n <AvatarFallback className=\"bg-muted text-muted-foreground text-[10px] font-medium uppercase\">\n {getInitials({ name: author?.name, email: author?.email })}\n </AvatarFallback>\n </Avatar>\n\n <div className=\"min-w-0 flex-1\">\n <Textarea\n data-slot=\"comment-composer-input\"\n value={text}\n onChange={(e) => setText(e.target.value)}\n onFocus={() => setFocused(true)}\n placeholder={placeholder}\n rows={open ? 3 : 1}\n className={cn(\n \"resize-none border-0 bg-transparent px-1 py-0.5 text-sm leading-snug shadow-none focus-visible:ring-0\",\n !open && \"min-h-0\"\n )}\n onKeyDown={(e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === \"Enter\") {\n e.preventDefault()\n post()\n }\n }}\n />\n\n {open ? (\n <div className=\"mt-0.5 flex items-center justify-between gap-2\">\n <span className=\"text-muted-foreground inline-flex items-center gap-1 text-[11px]\">\n <Lock size={12} /> {hint}\n </span>\n <span className=\"flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => {\n setText(\"\")\n setFocused(false)\n }}\n >\n Cancel\n </Button>\n <Button type=\"button\" size=\"sm\" disabled={!canPost} onClick={post}>\n Comment\n <kbd className=\"bg-primary-foreground/15 ml-1 rounded px-1 text-[10px]\">⌘↵</kbd>\n </Button>\n </span>\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n\nexport { CommentComposer }\n"],"mappings":";AA8DM,SACuB,KADvB;AAlDN,YAAY,WAAW;AACvB,SAAS,YAAY;AAErB,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,QAAQ,gBAAgB,mBAAmB;AACpD,SAAS,cAAc;AACvB,SAAS,gBAAgB;AAazB,SAAS,gBAAgB;AAAA,EACvB;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,OAAO;AAAA,EACP;AACF,GAAyB;AAtCzB;AAuCE,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,EAAE;AACzC,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,OAAO,WAAW,KAAK,SAAS;AACtC,QAAM,UAAU,KAAK,KAAK,EAAE,SAAS;AAErC,QAAM,OAAO,MAAM;AACjB,UAAM,QAAQ,KAAK,KAAK;AACxB,QAAI,CAAC,MAAO;AACZ,WAAO,KAAK;AACZ,YAAQ,EAAE;AACV,eAAW,KAAK;AAAA,EAClB;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,aAAW,OAAO,SAAS;AAAA,MAC3B,WAAW;AAAA,QACT;AAAA,QACA,QAAQ;AAAA,QACR;AAAA,MACF;AAAA,MAEA;AAAA,6BAAC,UAAO,MAAK,MAAK,WAAU,SACzB;AAAA,4CAAQ,aAAY,oBAAC,eAAY,KAAK,OAAO,WAAW,MAAK,YAAO,SAAP,YAAe,OAAO,IAAK;AAAA,UACzF,oBAAC,kBAAe,WAAU,oEACvB,sBAAY,EAAE,MAAM,iCAAQ,MAAM,OAAO,iCAAQ,MAAM,CAAC,GAC3D;AAAA,WACF;AAAA,QAEA,qBAAC,SAAI,WAAU,kBACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,aAAU;AAAA,cACV,OAAO;AAAA,cACP,UAAU,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK;AAAA,cACvC,SAAS,MAAM,WAAW,IAAI;AAAA,cAC9B;AAAA,cACA,MAAM,OAAO,IAAI;AAAA,cACjB,WAAW;AAAA,gBACT;AAAA,gBACA,CAAC,QAAQ;AAAA,cACX;AAAA,cACA,WAAW,CAAC,MAAM;AAChB,qBAAK,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,SAAS;AACjD,oBAAE,eAAe;AACjB,uBAAK;AAAA,gBACP;AAAA,cACF;AAAA;AAAA,UACF;AAAA,UAEC,OACC,qBAAC,SAAI,WAAU,kDACb;AAAA,iCAAC,UAAK,WAAU,oEACd;AAAA,kCAAC,QAAK,MAAM,IAAI;AAAA,cAAE;AAAA,cAAE;AAAA,eACtB;AAAA,YACA,qBAAC,UAAK,WAAU,2BACd;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAQ;AAAA,kBACR,MAAK;AAAA,kBACL,SAAS,MAAM;AACb,4BAAQ,EAAE;AACV,+BAAW,KAAK;AAAA,kBAClB;AAAA,kBACD;AAAA;AAAA,cAED;AAAA,cACA,qBAAC,UAAO,MAAK,UAAS,MAAK,MAAK,UAAU,CAAC,SAAS,SAAS,MAAM;AAAA;AAAA,gBAEjE,oBAAC,SAAI,WAAU,0DAAyD,0BAAE;AAAA,iBAC5E;AAAA,eACF;AAAA,aACF,IACE;AAAA,WACN;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -63,8 +63,16 @@ interface ConversationThread {
|
|
|
63
63
|
paused?: {
|
|
64
64
|
playbook: string;
|
|
65
65
|
} | null;
|
|
66
|
-
/** false => operator
|
|
66
|
+
/** false => operator cannot reply or create drafts from this thread. */
|
|
67
67
|
canReply?: boolean;
|
|
68
|
+
/** Explains why reply and draft creation are disabled. */
|
|
69
|
+
replyDisabledReason?: string;
|
|
70
|
+
/** Existing Gmail draft or thread URL. Rendered as a new-tab link when present. */
|
|
71
|
+
openInGmailUrl?: string | null;
|
|
72
|
+
/** Forces the Open in Gmail action into a disabled state. */
|
|
73
|
+
openInGmailDisabled?: boolean;
|
|
74
|
+
/** Tooltip/read-only copy for a disabled Open in Gmail action. */
|
|
75
|
+
openInGmailDisabledReason?: string | null;
|
|
68
76
|
messages: ConvMessage[];
|
|
69
77
|
/** Prefilled reply draft body. */
|
|
70
78
|
draft?: string;
|
|
@@ -77,6 +85,16 @@ interface ConversationReplyPayload {
|
|
|
77
85
|
includeSignature: boolean;
|
|
78
86
|
replyAll: boolean;
|
|
79
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Result of a server-side reply preview. `htmlBody` is the exact HTML the server
|
|
90
|
+
* prepared for this reply (sanitized by the component before render).
|
|
91
|
+
* `confirmationToken`, when present, identifies the prepared confirmation so a
|
|
92
|
+
* consumer can reuse it for the final send instead of re-preparing the message.
|
|
93
|
+
*/
|
|
94
|
+
interface ConversationReplyPreview {
|
|
95
|
+
htmlBody: string;
|
|
96
|
+
confirmationToken?: string;
|
|
97
|
+
}
|
|
80
98
|
interface ConversationPanelProps {
|
|
81
99
|
threads: ConversationThread[];
|
|
82
100
|
/** Current operator: drives "to me" + the reply avatar. */
|
|
@@ -85,11 +103,19 @@ interface ConversationPanelProps {
|
|
|
85
103
|
tenantName?: string;
|
|
86
104
|
onSendReply?: (payload: ConversationReplyPayload) => void | Promise<void>;
|
|
87
105
|
onCreateGmailDraft?: (payload: ConversationReplyPayload) => void | Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Server-side preview contract. When provided, the reply preview requests the
|
|
108
|
+
* exact send HTML from the server, the component sanitizes it before render,
|
|
109
|
+
* and retains any returned `confirmationToken` in preview state so a consumer
|
|
110
|
+
* can reuse it for the final send. When omitted, the composer falls back to a
|
|
111
|
+
* clearly labeled local draft preview only.
|
|
112
|
+
*/
|
|
113
|
+
onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>;
|
|
88
114
|
onOpenInGmail?: (threadId: string) => void;
|
|
89
115
|
/** Inline-open this thread initially (defaults to the first responded one). */
|
|
90
116
|
defaultOpenThreadId?: string;
|
|
91
117
|
className?: string;
|
|
92
118
|
}
|
|
93
|
-
declare function ConversationPanel({ threads, me, tenantName, onSendReply, onCreateGmailDraft, onOpenInGmail, defaultOpenThreadId, className, }: ConversationPanelProps): React.JSX.Element | null;
|
|
119
|
+
declare function ConversationPanel({ threads, me, tenantName, onSendReply, onCreateGmailDraft, onPreviewReply, onOpenInGmail, defaultOpenThreadId, className, }: ConversationPanelProps): React.JSX.Element | null;
|
|
94
120
|
|
|
95
|
-
export { type ConvMessage, type ConvParticipant, type ConvStatus, ConversationPanel, type ConversationPanelProps, type ConversationReplyPayload, type ConversationThread };
|
|
121
|
+
export { type ConvMessage, type ConvParticipant, type ConvStatus, ConversationPanel, type ConversationPanelProps, type ConversationReplyPayload, type ConversationReplyPreview, type ConversationThread };
|
|
@@ -337,7 +337,7 @@ function firstName(name) {
|
|
|
337
337
|
}
|
|
338
338
|
const STATUS_PILL = {
|
|
339
339
|
responded: { label: "New reply", cls: "bg-status-active-bg text-status-active-fg border-status-active-border" },
|
|
340
|
-
draft: { label: "Draft", cls: "bg-
|
|
340
|
+
draft: { label: "Draft", cls: "bg-background text-foreground/80 border-border" },
|
|
341
341
|
awaiting: { label: "Awaiting", cls: "bg-status-pending-bg text-status-pending-fg border-status-pending-border" },
|
|
342
342
|
viewing: { label: "Viewing", cls: "bg-muted text-muted-foreground border-border" }
|
|
343
343
|
};
|
|
@@ -350,6 +350,47 @@ const STATUS_DOT = {
|
|
|
350
350
|
function effectiveStatus(t) {
|
|
351
351
|
return t.canReply === false ? "viewing" : t.status;
|
|
352
352
|
}
|
|
353
|
+
function disabledOpenInGmailReason(thread) {
|
|
354
|
+
var _a, _b;
|
|
355
|
+
return ((_a = thread.openInGmailDisabledReason) == null ? void 0 : _a.trim()) || ((_b = thread.replyDisabledReason) == null ? void 0 : _b.trim()) || "Gmail access is not available for this thread.";
|
|
356
|
+
}
|
|
357
|
+
function canOpenInGmail(thread, onOpenInGmail) {
|
|
358
|
+
return thread.openInGmailDisabled !== true && Boolean(thread.openInGmailUrl || onOpenInGmail);
|
|
359
|
+
}
|
|
360
|
+
function OpenInGmailButton({
|
|
361
|
+
thread,
|
|
362
|
+
onOpenInGmail,
|
|
363
|
+
label = "Open in Gmail"
|
|
364
|
+
}) {
|
|
365
|
+
const hasConfiguredAction = Boolean(thread.openInGmailUrl || onOpenInGmail || thread.openInGmailDisabled || thread.openInGmailDisabledReason);
|
|
366
|
+
if (!hasConfiguredAction) return null;
|
|
367
|
+
const disabled = !canOpenInGmail(thread, onOpenInGmail);
|
|
368
|
+
const disabledReason = disabled ? disabledOpenInGmailReason(thread) : void 0;
|
|
369
|
+
if (!disabled && thread.openInGmailUrl) {
|
|
370
|
+
return /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", asChild: true, children: /* @__PURE__ */ jsxs("a", { href: thread.openInGmailUrl, target: "_blank", rel: "noopener noreferrer", children: [
|
|
371
|
+
/* @__PURE__ */ jsx(GmailMark, { size: 14 }),
|
|
372
|
+
" ",
|
|
373
|
+
label
|
|
374
|
+
] }) });
|
|
375
|
+
}
|
|
376
|
+
return /* @__PURE__ */ jsx("span", { className: "inline-flex", title: disabledReason, children: /* @__PURE__ */ jsxs(
|
|
377
|
+
Button,
|
|
378
|
+
{
|
|
379
|
+
type: "button",
|
|
380
|
+
variant: "ghost",
|
|
381
|
+
size: "sm",
|
|
382
|
+
disabled,
|
|
383
|
+
"aria-disabled": disabled || void 0,
|
|
384
|
+
"aria-label": disabledReason ? `${label}: ${disabledReason}` : label,
|
|
385
|
+
onClick: disabled ? void 0 : () => onOpenInGmail == null ? void 0 : onOpenInGmail(thread.threadId),
|
|
386
|
+
children: [
|
|
387
|
+
/* @__PURE__ */ jsx(GmailMark, { size: 14 }),
|
|
388
|
+
" ",
|
|
389
|
+
label
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
) });
|
|
393
|
+
}
|
|
353
394
|
function MessageView({
|
|
354
395
|
message,
|
|
355
396
|
expanded,
|
|
@@ -453,6 +494,7 @@ function MessageView({
|
|
|
453
494
|
] })
|
|
454
495
|
] });
|
|
455
496
|
}
|
|
497
|
+
const IDLE_PREVIEW = { loading: false, html: null, confirmationToken: null, error: null, local: false };
|
|
456
498
|
function ReplyComposer({
|
|
457
499
|
thread,
|
|
458
500
|
me,
|
|
@@ -460,23 +502,56 @@ function ReplyComposer({
|
|
|
460
502
|
tenantName,
|
|
461
503
|
onClose,
|
|
462
504
|
onSend,
|
|
463
|
-
onDraft
|
|
505
|
+
onDraft,
|
|
506
|
+
onPreviewReply,
|
|
507
|
+
draftDisabledReason
|
|
464
508
|
}) {
|
|
465
|
-
var _a, _b;
|
|
509
|
+
var _a, _b, _c, _d;
|
|
466
510
|
const [body, setBody] = React.useState((_a = thread.draft) != null ? _a : "");
|
|
467
511
|
const [sig, setSig] = React.useState(true);
|
|
468
512
|
const [preview, setPreview] = React.useState(false);
|
|
513
|
+
const [previewState, setPreviewState] = React.useState(IDLE_PREVIEW);
|
|
469
514
|
const [sending, setSending] = React.useState(false);
|
|
470
515
|
const [sendError, setSendError] = React.useState(null);
|
|
471
516
|
const ccList = replyAll ? (_b = thread.cc) != null ? _b : [] : [];
|
|
472
517
|
const subject = /^re:/i.test(thread.subject) ? thread.subject : `Re: ${thread.subject}`;
|
|
473
|
-
const
|
|
518
|
+
const draftDisabled = Boolean(draftDisabledReason);
|
|
519
|
+
const localPreviewHtml = textToHtml(body) + (sig && thread.signature ? textToHtml(thread.signature) : "");
|
|
520
|
+
const openPreview = async () => {
|
|
521
|
+
var _a2, _b2;
|
|
522
|
+
setPreview(true);
|
|
523
|
+
setSendError(null);
|
|
524
|
+
if (!onPreviewReply) {
|
|
525
|
+
setPreviewState({ loading: false, html: sanitizeHtml(localPreviewHtml), confirmationToken: null, error: null, local: true });
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
setPreviewState({ loading: true, html: null, confirmationToken: null, error: null, local: false });
|
|
529
|
+
try {
|
|
530
|
+
const result = await onPreviewReply({ threadId: thread.threadId, body, includeSignature: sig, replyAll });
|
|
531
|
+
setPreviewState({
|
|
532
|
+
loading: false,
|
|
533
|
+
html: sanitizeHtml((_a2 = result.htmlBody) != null ? _a2 : ""),
|
|
534
|
+
confirmationToken: (_b2 = result.confirmationToken) != null ? _b2 : null,
|
|
535
|
+
error: null,
|
|
536
|
+
local: false
|
|
537
|
+
});
|
|
538
|
+
} catch (error) {
|
|
539
|
+
setPreviewState({
|
|
540
|
+
loading: false,
|
|
541
|
+
html: null,
|
|
542
|
+
confirmationToken: null,
|
|
543
|
+
error: error instanceof Error ? error.message : "Could not load the preview. Please try again.",
|
|
544
|
+
local: false
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
};
|
|
474
548
|
const handleSend = async () => {
|
|
475
549
|
setSending(true);
|
|
476
550
|
setSendError(null);
|
|
477
551
|
try {
|
|
478
552
|
await onSend(body, sig);
|
|
479
553
|
setPreview(false);
|
|
554
|
+
setPreviewState(IDLE_PREVIEW);
|
|
480
555
|
} catch (error) {
|
|
481
556
|
setSendError(error instanceof Error ? error.message : "Could not send this reply. Please try again.");
|
|
482
557
|
} finally {
|
|
@@ -484,11 +559,13 @@ function ReplyComposer({
|
|
|
484
559
|
}
|
|
485
560
|
};
|
|
486
561
|
const handleDraft = async () => {
|
|
562
|
+
if (draftDisabled) return;
|
|
487
563
|
setSending(true);
|
|
488
564
|
setSendError(null);
|
|
489
565
|
try {
|
|
490
566
|
await onDraft(body, sig);
|
|
491
567
|
setPreview(false);
|
|
568
|
+
setPreviewState(IDLE_PREVIEW);
|
|
492
569
|
} catch (error) {
|
|
493
570
|
setSendError(error instanceof Error ? error.message : "Could not create the Gmail draft. Please try again.");
|
|
494
571
|
} finally {
|
|
@@ -545,7 +622,7 @@ function ReplyComposer({
|
|
|
545
622
|
onKeyDown: (e) => {
|
|
546
623
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
547
624
|
e.preventDefault();
|
|
548
|
-
|
|
625
|
+
void openPreview();
|
|
549
626
|
}
|
|
550
627
|
}
|
|
551
628
|
}
|
|
@@ -560,28 +637,32 @@ function ReplyComposer({
|
|
|
560
637
|
/* @__PURE__ */ jsx(Switch, { checked: sig, onCheckedChange: setSig, "aria-label": "Toggle signature" }),
|
|
561
638
|
"Signature"
|
|
562
639
|
] }),
|
|
563
|
-
/* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: sending, onClick: () =>
|
|
640
|
+
/* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: sending, onClick: () => void openPreview(), children: [
|
|
564
641
|
/* @__PURE__ */ jsx(Eye, { size: 14 }),
|
|
565
642
|
" Preview"
|
|
566
643
|
] }),
|
|
567
|
-
/* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", disabled: sending, onClick: () =>
|
|
644
|
+
/* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", disabled: sending, onClick: () => void openPreview(), children: [
|
|
568
645
|
/* @__PURE__ */ jsx(Send, { size: 14 }),
|
|
569
646
|
" Send"
|
|
570
647
|
] })
|
|
571
648
|
] }),
|
|
572
649
|
/* @__PURE__ */ jsx(Dialog, { open: preview, onOpenChange: (open) => {
|
|
573
|
-
if (!sending)
|
|
650
|
+
if (!sending) {
|
|
651
|
+
setPreview(open);
|
|
652
|
+
if (!open) setPreviewState(IDLE_PREVIEW);
|
|
653
|
+
}
|
|
574
654
|
}, children: /* @__PURE__ */ jsxs(DialogContent, { className: "max-w-xl", children: [
|
|
575
655
|
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
576
656
|
/* @__PURE__ */ jsxs(DialogTitle, { className: "flex items-center gap-1.5 text-[15px]", children: [
|
|
577
657
|
/* @__PURE__ */ jsx(Eye, { size: 15 }),
|
|
578
|
-
"
|
|
658
|
+
" ",
|
|
659
|
+
previewState.local ? "Local draft preview" : "Reply preview"
|
|
579
660
|
] }),
|
|
580
|
-
/* @__PURE__ */
|
|
661
|
+
/* @__PURE__ */ jsx(DialogDescription, { children: previewState.local ? "Local draft preview only \u2014 the server prepares the exact message on send." : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
581
662
|
"Stays on the ",
|
|
582
663
|
subject.replace(/^Re:\s*/i, ""),
|
|
583
664
|
" thread. Gmail keeps it threaded."
|
|
584
|
-
] })
|
|
665
|
+
] }) })
|
|
585
666
|
] }),
|
|
586
667
|
/* @__PURE__ */ jsxs("div", { className: "border-border space-y-1 rounded-md border p-3 text-[13px]", children: [
|
|
587
668
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
@@ -603,30 +684,45 @@ function ReplyComposer({
|
|
|
603
684
|
subject
|
|
604
685
|
] })
|
|
605
686
|
] }),
|
|
606
|
-
/* @__PURE__ */
|
|
687
|
+
previewState.loading ? /* @__PURE__ */ jsxs("div", { "data-slot": "conv-preview-loading", role: "status", className: "text-muted-foreground flex items-center gap-2 px-1 py-6 text-[13px]", children: [
|
|
688
|
+
/* @__PURE__ */ jsx("span", { className: "border-muted-foreground/40 border-t-foreground size-4 animate-spin rounded-full border-2", "aria-hidden": true }),
|
|
689
|
+
"Loading preview\u2026"
|
|
690
|
+
] }) : previewState.error ? /* @__PURE__ */ jsx("p", { role: "alert", className: "text-destructive text-sm", children: previewState.error }) : /* @__PURE__ */ jsx(
|
|
691
|
+
"div",
|
|
692
|
+
{
|
|
693
|
+
"data-slot": "conv-preview-body",
|
|
694
|
+
"data-confirmation-token": (_c = previewState.confirmationToken) != null ? _c : void 0,
|
|
695
|
+
className: cn(PROSE, "max-h-72 overflow-auto"),
|
|
696
|
+
dangerouslySetInnerHTML: { __html: (_d = previewState.html) != null ? _d : "" }
|
|
697
|
+
}
|
|
698
|
+
),
|
|
607
699
|
sendError ? /* @__PURE__ */ jsx("p", { role: "alert", className: "text-destructive text-sm", children: sendError }) : null,
|
|
608
700
|
/* @__PURE__ */ jsxs(DialogFooter, { className: "sm:justify-between", children: [
|
|
609
|
-
/* @__PURE__ */ jsxs(
|
|
701
|
+
/* @__PURE__ */ jsx("span", { className: "inline-flex", title: draftDisabledReason != null ? draftDisabledReason : void 0, children: /* @__PURE__ */ jsxs(
|
|
610
702
|
"button",
|
|
611
703
|
{
|
|
612
704
|
type: "button",
|
|
613
|
-
disabled: sending,
|
|
705
|
+
disabled: sending || previewState.loading || draftDisabled,
|
|
614
706
|
onClick: handleDraft,
|
|
707
|
+
"aria-label": draftDisabledReason ? `Open draft in Gmail: ${draftDisabledReason}` : "Open draft in Gmail",
|
|
615
708
|
className: "text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-[13px] disabled:pointer-events-none disabled:opacity-50",
|
|
616
709
|
children: [
|
|
617
710
|
/* @__PURE__ */ jsx(GmailMark, { size: 14 }),
|
|
618
711
|
" Open draft in Gmail"
|
|
619
712
|
]
|
|
620
713
|
}
|
|
621
|
-
),
|
|
714
|
+
) }),
|
|
622
715
|
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
623
|
-
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: sending, onClick: () =>
|
|
716
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: sending, onClick: () => {
|
|
717
|
+
setPreview(false);
|
|
718
|
+
setPreviewState(IDLE_PREVIEW);
|
|
719
|
+
}, children: "Keep editing" }),
|
|
624
720
|
/* @__PURE__ */ jsxs(
|
|
625
721
|
Button,
|
|
626
722
|
{
|
|
627
723
|
type: "button",
|
|
628
724
|
size: "sm",
|
|
629
|
-
disabled: sending,
|
|
725
|
+
disabled: sending || previewState.loading,
|
|
630
726
|
onClick: handleSend,
|
|
631
727
|
children: [
|
|
632
728
|
/* @__PURE__ */ jsx(Send, { size: 14 }),
|
|
@@ -647,9 +743,13 @@ function ThreadBody({
|
|
|
647
743
|
tenantName,
|
|
648
744
|
onSendReply,
|
|
649
745
|
onCreateGmailDraft,
|
|
746
|
+
onPreviewReply,
|
|
650
747
|
onOpenInGmail
|
|
651
748
|
}) {
|
|
749
|
+
var _a;
|
|
652
750
|
const canReply = thread.canReply !== false;
|
|
751
|
+
const replyDisabledReason = ((_a = thread.replyDisabledReason) == null ? void 0 : _a.trim()) || "You are not a participant on this thread, so replying is disabled here.";
|
|
752
|
+
const draftDisabledReason = onCreateGmailDraft ? null : "Gmail draft creation is not available for this thread.";
|
|
653
753
|
const hasCc = !!(thread.cc && thread.cc.length);
|
|
654
754
|
const [mode, setMode] = React.useState("idle");
|
|
655
755
|
const [replyAll, setReplyAll] = React.useState(false);
|
|
@@ -661,7 +761,7 @@ function ThreadBody({
|
|
|
661
761
|
return o;
|
|
662
762
|
});
|
|
663
763
|
const toggle = (id) => setExpanded((e) => __spreadProps(__spreadValues({}, e), { [id]: !e[id] }));
|
|
664
|
-
return /* @__PURE__ */ jsxs("div", { "data-slot": "conv-thread-body", className: "space-y-
|
|
764
|
+
return /* @__PURE__ */ jsxs("div", { "data-slot": "conv-thread-body", className: "space-y-2", children: [
|
|
665
765
|
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: [
|
|
666
766
|
/* @__PURE__ */ jsx(Pause, { size: 13, className: "mt-0.5 shrink-0" }),
|
|
667
767
|
/* @__PURE__ */ jsxs("span", { children: [
|
|
@@ -674,14 +774,16 @@ function ThreadBody({
|
|
|
674
774
|
] })
|
|
675
775
|
] }) : null,
|
|
676
776
|
/* @__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)) }),
|
|
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: [
|
|
777
|
+
!canReply ? /* @__PURE__ */ jsxs("div", { className: "border-border bg-muted/30 text-muted-foreground flex flex-wrap items-start gap-2 rounded-md border p-2.5 text-[12px]", children: [
|
|
678
778
|
/* @__PURE__ */ jsx(Eye, { size: 14, className: "mt-0.5 shrink-0" }),
|
|
679
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
779
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
680
780
|
/* @__PURE__ */ jsx("b", { children: "Viewing only." }),
|
|
681
|
-
"
|
|
682
|
-
|
|
781
|
+
" ",
|
|
782
|
+
replyDisabledReason
|
|
783
|
+
] }),
|
|
784
|
+
/* @__PURE__ */ jsx(OpenInGmailButton, { thread, onOpenInGmail })
|
|
683
785
|
] }) : null,
|
|
684
|
-
canReply && mode === "idle" ? /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
786
|
+
canReply && mode === "idle" ? /* @__PURE__ */ jsxs("div", { "data-slot": "conv-action-row", className: "border-border/70 mt-1 flex flex-wrap items-center gap-x-3 gap-y-2 border-t pt-3", children: [
|
|
685
787
|
/* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", onClick: () => {
|
|
686
788
|
setReplyAll(false);
|
|
687
789
|
setMode("replying");
|
|
@@ -696,12 +798,9 @@ function ThreadBody({
|
|
|
696
798
|
/* @__PURE__ */ jsx(ReplyAll, { size: 14 }),
|
|
697
799
|
" Reply all"
|
|
698
800
|
] }) : null,
|
|
699
|
-
/* @__PURE__ */
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
] }),
|
|
703
|
-
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground/70 ml-auto inline-flex items-center gap-1 text-[11px]", children: [
|
|
704
|
-
/* @__PURE__ */ jsx(GitMerge, { size: 12 }),
|
|
801
|
+
/* @__PURE__ */ jsx(OpenInGmailButton, { thread, onOpenInGmail }),
|
|
802
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground/70 ml-auto inline-flex items-center gap-1.5 text-[12px]", children: [
|
|
803
|
+
/* @__PURE__ */ jsx(GitMerge, { size: 13 }),
|
|
705
804
|
" Stays on this thread"
|
|
706
805
|
] })
|
|
707
806
|
] }) : null,
|
|
@@ -712,15 +811,18 @@ function ThreadBody({
|
|
|
712
811
|
me,
|
|
713
812
|
replyAll,
|
|
714
813
|
tenantName,
|
|
814
|
+
onPreviewReply,
|
|
715
815
|
onClose: () => setMode("idle"),
|
|
716
816
|
onSend: async (body, includeSignature) => {
|
|
717
817
|
await (onSendReply == null ? void 0 : onSendReply({ threadId: thread.threadId, body, includeSignature, replyAll }));
|
|
718
818
|
setMode("sent");
|
|
719
819
|
},
|
|
720
820
|
onDraft: async (body, includeSignature) => {
|
|
721
|
-
|
|
821
|
+
if (!onCreateGmailDraft) return;
|
|
822
|
+
await onCreateGmailDraft({ threadId: thread.threadId, body, includeSignature, replyAll });
|
|
722
823
|
setMode("draft");
|
|
723
|
-
}
|
|
824
|
+
},
|
|
825
|
+
draftDisabledReason
|
|
724
826
|
}
|
|
725
827
|
) : null,
|
|
726
828
|
canReply && mode === "sent" ? /* @__PURE__ */ jsxs("div", { className: "border-status-active-border bg-status-active-bg flex items-center gap-2 rounded-md border p-3 text-[13px]", children: [
|
|
@@ -747,10 +849,7 @@ function ThreadBody({
|
|
|
747
849
|
] }),
|
|
748
850
|
" thread; open it there to finish. Nothing was sent."
|
|
749
851
|
] }),
|
|
750
|
-
/* @__PURE__ */
|
|
751
|
-
/* @__PURE__ */ jsx(GmailMark, { size: 14 }),
|
|
752
|
-
" Open in Gmail"
|
|
753
|
-
] }),
|
|
852
|
+
/* @__PURE__ */ jsx(OpenInGmailButton, { thread, onOpenInGmail }),
|
|
754
853
|
/* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setMode("idle"), children: "Done" })
|
|
755
854
|
] }) : null
|
|
756
855
|
] });
|
|
@@ -763,6 +862,7 @@ function ThreadRow({
|
|
|
763
862
|
tenantName,
|
|
764
863
|
onSendReply,
|
|
765
864
|
onCreateGmailDraft,
|
|
865
|
+
onPreviewReply,
|
|
766
866
|
onOpenInGmail
|
|
767
867
|
}) {
|
|
768
868
|
const status = effectiveStatus(thread);
|
|
@@ -782,8 +882,8 @@ function ThreadRow({
|
|
|
782
882
|
/* @__PURE__ */ jsx("span", { className: cn("size-2 shrink-0 rounded-full", STATUS_DOT[status]), "aria-hidden": true }),
|
|
783
883
|
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
784
884
|
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
785
|
-
/* @__PURE__ */ jsx("span", { className: "truncate text-
|
|
786
|
-
/* @__PURE__ */ jsx("span", { className: cn("shrink-0 rounded border px-1.5 text-[10px] font-medium leading-4", pill.cls), children: pill.label })
|
|
885
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold", children: thread.subject }),
|
|
886
|
+
/* @__PURE__ */ jsx("span", { className: cn("shrink-0 rounded-md border px-1.5 py-px text-[10px] font-medium leading-4", pill.cls), children: pill.label })
|
|
787
887
|
] }),
|
|
788
888
|
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground block truncate text-xs", children: [
|
|
789
889
|
/* @__PURE__ */ jsx("b", { className: "text-foreground/80", children: thread.contact.name }),
|
|
@@ -806,6 +906,7 @@ function ThreadRow({
|
|
|
806
906
|
tenantName,
|
|
807
907
|
onSendReply,
|
|
808
908
|
onCreateGmailDraft,
|
|
909
|
+
onPreviewReply,
|
|
809
910
|
onOpenInGmail
|
|
810
911
|
}
|
|
811
912
|
) }) : null
|
|
@@ -817,6 +918,7 @@ function ConversationPanel({
|
|
|
817
918
|
tenantName,
|
|
818
919
|
onSendReply,
|
|
819
920
|
onCreateGmailDraft,
|
|
921
|
+
onPreviewReply,
|
|
820
922
|
onOpenInGmail,
|
|
821
923
|
defaultOpenThreadId,
|
|
822
924
|
className
|
|
@@ -867,8 +969,8 @@ function ConversationPanel({
|
|
|
867
969
|
}
|
|
868
970
|
),
|
|
869
971
|
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
870
|
-
/* @__PURE__ */ jsx("span", { className: "block truncate text-
|
|
871
|
-
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground block truncate text-xs", children: [
|
|
972
|
+
/* @__PURE__ */ jsx("span", { className: "block truncate text-sm font-semibold leading-tight", children: headTitle }),
|
|
973
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground mt-0.5 block truncate text-xs", children: [
|
|
872
974
|
threads.length,
|
|
873
975
|
" ",
|
|
874
976
|
threads.length === 1 ? "thread" : "threads",
|
|
@@ -893,6 +995,7 @@ function ConversationPanel({
|
|
|
893
995
|
tenantName,
|
|
894
996
|
onSendReply,
|
|
895
997
|
onCreateGmailDraft,
|
|
998
|
+
onPreviewReply,
|
|
896
999
|
onOpenInGmail
|
|
897
1000
|
},
|
|
898
1001
|
t.threadId
|