@handled-ai/design-system 0.20.3 → 0.20.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/comment-composer.js +4 -4
- package/dist/components/comment-composer.js.map +1 -1
- package/dist/components/conversation-panel.d.ts +20 -2
- package/dist/components/conversation-panel.js +80 -23
- 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 +169 -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 +114 -21
- 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":[]}
|
|
@@ -77,6 +77,16 @@ interface ConversationReplyPayload {
|
|
|
77
77
|
includeSignature: boolean;
|
|
78
78
|
replyAll: boolean;
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Result of a server-side reply preview. `htmlBody` is the exact HTML the server
|
|
82
|
+
* prepared for this reply (sanitized by the component before render).
|
|
83
|
+
* `confirmationToken`, when present, identifies the prepared confirmation so a
|
|
84
|
+
* consumer can reuse it for the final send instead of re-preparing the message.
|
|
85
|
+
*/
|
|
86
|
+
interface ConversationReplyPreview {
|
|
87
|
+
htmlBody: string;
|
|
88
|
+
confirmationToken?: string;
|
|
89
|
+
}
|
|
80
90
|
interface ConversationPanelProps {
|
|
81
91
|
threads: ConversationThread[];
|
|
82
92
|
/** Current operator: drives "to me" + the reply avatar. */
|
|
@@ -85,11 +95,19 @@ interface ConversationPanelProps {
|
|
|
85
95
|
tenantName?: string;
|
|
86
96
|
onSendReply?: (payload: ConversationReplyPayload) => void | Promise<void>;
|
|
87
97
|
onCreateGmailDraft?: (payload: ConversationReplyPayload) => void | Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Server-side preview contract. When provided, the reply preview requests the
|
|
100
|
+
* exact send HTML from the server, the component sanitizes it before render,
|
|
101
|
+
* and retains any returned `confirmationToken` in preview state so a consumer
|
|
102
|
+
* can reuse it for the final send. When omitted, the composer falls back to a
|
|
103
|
+
* clearly labeled local draft preview only.
|
|
104
|
+
*/
|
|
105
|
+
onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>;
|
|
88
106
|
onOpenInGmail?: (threadId: string) => void;
|
|
89
107
|
/** Inline-open this thread initially (defaults to the first responded one). */
|
|
90
108
|
defaultOpenThreadId?: string;
|
|
91
109
|
className?: string;
|
|
92
110
|
}
|
|
93
|
-
declare function ConversationPanel({ threads, me, tenantName, onSendReply, onCreateGmailDraft, onOpenInGmail, defaultOpenThreadId, className, }: ConversationPanelProps): React.JSX.Element | null;
|
|
111
|
+
declare function ConversationPanel({ threads, me, tenantName, onSendReply, onCreateGmailDraft, onPreviewReply, onOpenInGmail, defaultOpenThreadId, className, }: ConversationPanelProps): React.JSX.Element | null;
|
|
94
112
|
|
|
95
|
-
export { type ConvMessage, type ConvParticipant, type ConvStatus, ConversationPanel, type ConversationPanelProps, type ConversationReplyPayload, type ConversationThread };
|
|
113
|
+
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
|
};
|
|
@@ -453,6 +453,7 @@ function MessageView({
|
|
|
453
453
|
] })
|
|
454
454
|
] });
|
|
455
455
|
}
|
|
456
|
+
const IDLE_PREVIEW = { loading: false, html: null, confirmationToken: null, error: null, local: false };
|
|
456
457
|
function ReplyComposer({
|
|
457
458
|
thread,
|
|
458
459
|
me,
|
|
@@ -460,23 +461,54 @@ function ReplyComposer({
|
|
|
460
461
|
tenantName,
|
|
461
462
|
onClose,
|
|
462
463
|
onSend,
|
|
463
|
-
onDraft
|
|
464
|
+
onDraft,
|
|
465
|
+
onPreviewReply
|
|
464
466
|
}) {
|
|
465
|
-
var _a, _b;
|
|
467
|
+
var _a, _b, _c, _d;
|
|
466
468
|
const [body, setBody] = React.useState((_a = thread.draft) != null ? _a : "");
|
|
467
469
|
const [sig, setSig] = React.useState(true);
|
|
468
470
|
const [preview, setPreview] = React.useState(false);
|
|
471
|
+
const [previewState, setPreviewState] = React.useState(IDLE_PREVIEW);
|
|
469
472
|
const [sending, setSending] = React.useState(false);
|
|
470
473
|
const [sendError, setSendError] = React.useState(null);
|
|
471
474
|
const ccList = replyAll ? (_b = thread.cc) != null ? _b : [] : [];
|
|
472
475
|
const subject = /^re:/i.test(thread.subject) ? thread.subject : `Re: ${thread.subject}`;
|
|
473
|
-
const
|
|
476
|
+
const localPreviewHtml = textToHtml(body) + (sig && thread.signature ? textToHtml(thread.signature) : "");
|
|
477
|
+
const openPreview = async () => {
|
|
478
|
+
var _a2, _b2;
|
|
479
|
+
setPreview(true);
|
|
480
|
+
setSendError(null);
|
|
481
|
+
if (!onPreviewReply) {
|
|
482
|
+
setPreviewState({ loading: false, html: sanitizeHtml(localPreviewHtml), confirmationToken: null, error: null, local: true });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
setPreviewState({ loading: true, html: null, confirmationToken: null, error: null, local: false });
|
|
486
|
+
try {
|
|
487
|
+
const result = await onPreviewReply({ threadId: thread.threadId, body, includeSignature: sig, replyAll });
|
|
488
|
+
setPreviewState({
|
|
489
|
+
loading: false,
|
|
490
|
+
html: sanitizeHtml((_a2 = result.htmlBody) != null ? _a2 : ""),
|
|
491
|
+
confirmationToken: (_b2 = result.confirmationToken) != null ? _b2 : null,
|
|
492
|
+
error: null,
|
|
493
|
+
local: false
|
|
494
|
+
});
|
|
495
|
+
} catch (error) {
|
|
496
|
+
setPreviewState({
|
|
497
|
+
loading: false,
|
|
498
|
+
html: null,
|
|
499
|
+
confirmationToken: null,
|
|
500
|
+
error: error instanceof Error ? error.message : "Could not load the preview. Please try again.",
|
|
501
|
+
local: false
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
};
|
|
474
505
|
const handleSend = async () => {
|
|
475
506
|
setSending(true);
|
|
476
507
|
setSendError(null);
|
|
477
508
|
try {
|
|
478
509
|
await onSend(body, sig);
|
|
479
510
|
setPreview(false);
|
|
511
|
+
setPreviewState(IDLE_PREVIEW);
|
|
480
512
|
} catch (error) {
|
|
481
513
|
setSendError(error instanceof Error ? error.message : "Could not send this reply. Please try again.");
|
|
482
514
|
} finally {
|
|
@@ -489,6 +521,7 @@ function ReplyComposer({
|
|
|
489
521
|
try {
|
|
490
522
|
await onDraft(body, sig);
|
|
491
523
|
setPreview(false);
|
|
524
|
+
setPreviewState(IDLE_PREVIEW);
|
|
492
525
|
} catch (error) {
|
|
493
526
|
setSendError(error instanceof Error ? error.message : "Could not create the Gmail draft. Please try again.");
|
|
494
527
|
} finally {
|
|
@@ -545,7 +578,7 @@ function ReplyComposer({
|
|
|
545
578
|
onKeyDown: (e) => {
|
|
546
579
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
547
580
|
e.preventDefault();
|
|
548
|
-
|
|
581
|
+
void openPreview();
|
|
549
582
|
}
|
|
550
583
|
}
|
|
551
584
|
}
|
|
@@ -560,28 +593,32 @@ function ReplyComposer({
|
|
|
560
593
|
/* @__PURE__ */ jsx(Switch, { checked: sig, onCheckedChange: setSig, "aria-label": "Toggle signature" }),
|
|
561
594
|
"Signature"
|
|
562
595
|
] }),
|
|
563
|
-
/* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: sending, onClick: () =>
|
|
596
|
+
/* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: sending, onClick: () => void openPreview(), children: [
|
|
564
597
|
/* @__PURE__ */ jsx(Eye, { size: 14 }),
|
|
565
598
|
" Preview"
|
|
566
599
|
] }),
|
|
567
|
-
/* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", disabled: sending, onClick: () =>
|
|
600
|
+
/* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", disabled: sending, onClick: () => void openPreview(), children: [
|
|
568
601
|
/* @__PURE__ */ jsx(Send, { size: 14 }),
|
|
569
602
|
" Send"
|
|
570
603
|
] })
|
|
571
604
|
] }),
|
|
572
605
|
/* @__PURE__ */ jsx(Dialog, { open: preview, onOpenChange: (open) => {
|
|
573
|
-
if (!sending)
|
|
606
|
+
if (!sending) {
|
|
607
|
+
setPreview(open);
|
|
608
|
+
if (!open) setPreviewState(IDLE_PREVIEW);
|
|
609
|
+
}
|
|
574
610
|
}, children: /* @__PURE__ */ jsxs(DialogContent, { className: "max-w-xl", children: [
|
|
575
611
|
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
576
612
|
/* @__PURE__ */ jsxs(DialogTitle, { className: "flex items-center gap-1.5 text-[15px]", children: [
|
|
577
613
|
/* @__PURE__ */ jsx(Eye, { size: 15 }),
|
|
578
|
-
"
|
|
614
|
+
" ",
|
|
615
|
+
previewState.local ? "Local draft preview" : "Reply preview"
|
|
579
616
|
] }),
|
|
580
|
-
/* @__PURE__ */
|
|
617
|
+
/* @__PURE__ */ jsx(DialogDescription, { children: previewState.local ? "Local draft preview only \u2014 the server prepares the exact message on send." : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
581
618
|
"Stays on the ",
|
|
582
619
|
subject.replace(/^Re:\s*/i, ""),
|
|
583
620
|
" thread. Gmail keeps it threaded."
|
|
584
|
-
] })
|
|
621
|
+
] }) })
|
|
585
622
|
] }),
|
|
586
623
|
/* @__PURE__ */ jsxs("div", { className: "border-border space-y-1 rounded-md border p-3 text-[13px]", children: [
|
|
587
624
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
@@ -603,14 +640,25 @@ function ReplyComposer({
|
|
|
603
640
|
subject
|
|
604
641
|
] })
|
|
605
642
|
] }),
|
|
606
|
-
/* @__PURE__ */
|
|
643
|
+
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: [
|
|
644
|
+
/* @__PURE__ */ jsx("span", { className: "border-muted-foreground/40 border-t-foreground size-4 animate-spin rounded-full border-2", "aria-hidden": true }),
|
|
645
|
+
"Loading preview\u2026"
|
|
646
|
+
] }) : previewState.error ? /* @__PURE__ */ jsx("p", { role: "alert", className: "text-destructive text-sm", children: previewState.error }) : /* @__PURE__ */ jsx(
|
|
647
|
+
"div",
|
|
648
|
+
{
|
|
649
|
+
"data-slot": "conv-preview-body",
|
|
650
|
+
"data-confirmation-token": (_c = previewState.confirmationToken) != null ? _c : void 0,
|
|
651
|
+
className: cn(PROSE, "max-h-72 overflow-auto"),
|
|
652
|
+
dangerouslySetInnerHTML: { __html: (_d = previewState.html) != null ? _d : "" }
|
|
653
|
+
}
|
|
654
|
+
),
|
|
607
655
|
sendError ? /* @__PURE__ */ jsx("p", { role: "alert", className: "text-destructive text-sm", children: sendError }) : null,
|
|
608
656
|
/* @__PURE__ */ jsxs(DialogFooter, { className: "sm:justify-between", children: [
|
|
609
657
|
/* @__PURE__ */ jsxs(
|
|
610
658
|
"button",
|
|
611
659
|
{
|
|
612
660
|
type: "button",
|
|
613
|
-
disabled: sending,
|
|
661
|
+
disabled: sending || previewState.loading,
|
|
614
662
|
onClick: handleDraft,
|
|
615
663
|
className: "text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-[13px] disabled:pointer-events-none disabled:opacity-50",
|
|
616
664
|
children: [
|
|
@@ -620,13 +668,16 @@ function ReplyComposer({
|
|
|
620
668
|
}
|
|
621
669
|
),
|
|
622
670
|
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
623
|
-
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: sending, onClick: () =>
|
|
671
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: sending, onClick: () => {
|
|
672
|
+
setPreview(false);
|
|
673
|
+
setPreviewState(IDLE_PREVIEW);
|
|
674
|
+
}, children: "Keep editing" }),
|
|
624
675
|
/* @__PURE__ */ jsxs(
|
|
625
676
|
Button,
|
|
626
677
|
{
|
|
627
678
|
type: "button",
|
|
628
679
|
size: "sm",
|
|
629
|
-
disabled: sending,
|
|
680
|
+
disabled: sending || previewState.loading,
|
|
630
681
|
onClick: handleSend,
|
|
631
682
|
children: [
|
|
632
683
|
/* @__PURE__ */ jsx(Send, { size: 14 }),
|
|
@@ -647,6 +698,7 @@ function ThreadBody({
|
|
|
647
698
|
tenantName,
|
|
648
699
|
onSendReply,
|
|
649
700
|
onCreateGmailDraft,
|
|
701
|
+
onPreviewReply,
|
|
650
702
|
onOpenInGmail
|
|
651
703
|
}) {
|
|
652
704
|
const canReply = thread.canReply !== false;
|
|
@@ -661,7 +713,7 @@ function ThreadBody({
|
|
|
661
713
|
return o;
|
|
662
714
|
});
|
|
663
715
|
const toggle = (id) => setExpanded((e) => __spreadProps(__spreadValues({}, e), { [id]: !e[id] }));
|
|
664
|
-
return /* @__PURE__ */ jsxs("div", { "data-slot": "conv-thread-body", className: "space-y-
|
|
716
|
+
return /* @__PURE__ */ jsxs("div", { "data-slot": "conv-thread-body", className: "space-y-2", children: [
|
|
665
717
|
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
718
|
/* @__PURE__ */ jsx(Pause, { size: 13, className: "mt-0.5 shrink-0" }),
|
|
667
719
|
/* @__PURE__ */ jsxs("span", { children: [
|
|
@@ -681,7 +733,7 @@ function ThreadBody({
|
|
|
681
733
|
" You\u2019re not a participant on this thread, so replying is disabled here."
|
|
682
734
|
] })
|
|
683
735
|
] }) : null,
|
|
684
|
-
canReply && mode === "idle" ? /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
736
|
+
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
737
|
/* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", onClick: () => {
|
|
686
738
|
setReplyAll(false);
|
|
687
739
|
setMode("replying");
|
|
@@ -700,8 +752,8 @@ function ThreadBody({
|
|
|
700
752
|
/* @__PURE__ */ jsx(GmailMark, { size: 14 }),
|
|
701
753
|
" Open in Gmail"
|
|
702
754
|
] }),
|
|
703
|
-
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground/70 ml-auto inline-flex items-center gap-1 text-[
|
|
704
|
-
/* @__PURE__ */ jsx(GitMerge, { size:
|
|
755
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground/70 ml-auto inline-flex items-center gap-1.5 text-[12px]", children: [
|
|
756
|
+
/* @__PURE__ */ jsx(GitMerge, { size: 13 }),
|
|
705
757
|
" Stays on this thread"
|
|
706
758
|
] })
|
|
707
759
|
] }) : null,
|
|
@@ -712,6 +764,7 @@ function ThreadBody({
|
|
|
712
764
|
me,
|
|
713
765
|
replyAll,
|
|
714
766
|
tenantName,
|
|
767
|
+
onPreviewReply,
|
|
715
768
|
onClose: () => setMode("idle"),
|
|
716
769
|
onSend: async (body, includeSignature) => {
|
|
717
770
|
await (onSendReply == null ? void 0 : onSendReply({ threadId: thread.threadId, body, includeSignature, replyAll }));
|
|
@@ -763,6 +816,7 @@ function ThreadRow({
|
|
|
763
816
|
tenantName,
|
|
764
817
|
onSendReply,
|
|
765
818
|
onCreateGmailDraft,
|
|
819
|
+
onPreviewReply,
|
|
766
820
|
onOpenInGmail
|
|
767
821
|
}) {
|
|
768
822
|
const status = effectiveStatus(thread);
|
|
@@ -782,8 +836,8 @@ function ThreadRow({
|
|
|
782
836
|
/* @__PURE__ */ jsx("span", { className: cn("size-2 shrink-0 rounded-full", STATUS_DOT[status]), "aria-hidden": true }),
|
|
783
837
|
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
784
838
|
/* @__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 })
|
|
839
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold", children: thread.subject }),
|
|
840
|
+
/* @__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
841
|
] }),
|
|
788
842
|
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground block truncate text-xs", children: [
|
|
789
843
|
/* @__PURE__ */ jsx("b", { className: "text-foreground/80", children: thread.contact.name }),
|
|
@@ -806,6 +860,7 @@ function ThreadRow({
|
|
|
806
860
|
tenantName,
|
|
807
861
|
onSendReply,
|
|
808
862
|
onCreateGmailDraft,
|
|
863
|
+
onPreviewReply,
|
|
809
864
|
onOpenInGmail
|
|
810
865
|
}
|
|
811
866
|
) }) : null
|
|
@@ -817,6 +872,7 @@ function ConversationPanel({
|
|
|
817
872
|
tenantName,
|
|
818
873
|
onSendReply,
|
|
819
874
|
onCreateGmailDraft,
|
|
875
|
+
onPreviewReply,
|
|
820
876
|
onOpenInGmail,
|
|
821
877
|
defaultOpenThreadId,
|
|
822
878
|
className
|
|
@@ -867,8 +923,8 @@ function ConversationPanel({
|
|
|
867
923
|
}
|
|
868
924
|
),
|
|
869
925
|
/* @__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: [
|
|
926
|
+
/* @__PURE__ */ jsx("span", { className: "block truncate text-sm font-semibold leading-tight", children: headTitle }),
|
|
927
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground mt-0.5 block truncate text-xs", children: [
|
|
872
928
|
threads.length,
|
|
873
929
|
" ",
|
|
874
930
|
threads.length === 1 ? "thread" : "threads",
|
|
@@ -893,6 +949,7 @@ function ConversationPanel({
|
|
|
893
949
|
tenantName,
|
|
894
950
|
onSendReply,
|
|
895
951
|
onCreateGmailDraft,
|
|
952
|
+
onPreviewReply,
|
|
896
953
|
onOpenInGmail
|
|
897
954
|
},
|
|
898
955
|
t.threadId
|