@handled-ai/design-system 0.20.22 → 0.20.23

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.
@@ -61,6 +61,15 @@ interface ConvMessage {
61
61
  };
62
62
  /** HTML body (preferred). Sanitized by the component before rendering. */
63
63
  bodyHtml?: string;
64
+ /**
65
+ * Pre-split signature HTML for this message, sanitized server-side by the
66
+ * sender's signature pipeline (which preserves the authored Gmail look —
67
+ * fonts, colors, logos). When present it renders inside the collapsed
68
+ * details section ("•••" toggle) instead of relying on the heuristic footer
69
+ * split of `bodyHtml`, so long signatures never expand the thread by
70
+ * default.
71
+ */
72
+ signatureHtml?: string | null;
64
73
  /** Plain-text fallback when `bodyHtml` is absent. */
65
74
  body?: string;
66
75
  /** Quoted prior message, collapsed behind a toggle. Sanitized before rendering. */
@@ -219,7 +219,7 @@ function MessageView({
219
219
  onToggle,
220
220
  me
221
221
  }) {
222
- var _a;
222
+ var _a, _b;
223
223
  const [quoteOpen, setQuoteOpen] = React.useState(false);
224
224
  const fromDisplay = displayParticipant(message.from);
225
225
  const toDisplay = displayParticipant(message.to);
@@ -287,6 +287,7 @@ function MessageView({
287
287
  {
288
288
  html: message.bodyHtml,
289
289
  text: message.body,
290
+ detailsHtml: (_b = message.signatureHtml) != null ? _b : void 0,
290
291
  variant: "history",
291
292
  collapseDetails: true,
292
293
  className: "text-sm"
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/conversation-panel.tsx"],"sourcesContent":["\"use client\"\n\n/**\n * conversation-panel.tsx — in-case email-thread reader + reply, for the case\n * panel (\"Email response detected\" hub).\n *\n * v1 scope (WIT-853 / WIT-802): INLINE thread reading + reply only.\n * - A collapsible hub header with a pulse badge whose color reflects state:\n * responded (a reply needs you) / awaiting (sent, waiting) / viewing (read-only).\n * - A list of thread rows; clicking one opens a Gmail-style reader inline,\n * right under the row, with collapsible messages + quoted-history toggle.\n * - Reply / Reply-all composer with a signature toggle and two send paths:\n * Preview→Send (in-app) and \"Open draft in Gmail\" (deep link).\n * - A \"playbook stopped\" banner when the customer's reply halted the sequence,\n * and a read-only notice when the operator is not a thread participant.\n *\n * The bottom-right floating dock / side-by-side compare is intentionally OUT of\n * scope here and tracked separately (WIT-855).\n *\n * Presentational: all data + side effects come from the consumer. Email bodies\n * (`bodyHtml`, `quoted.html`) are sanitized before rendering.\n */\n\nimport * as React from \"react\"\nimport {\n ChevronDown,\n ChevronUp,\n CornerUpLeft,\n CheckCheck,\n MailOpen,\n FilePenLine,\n Reply,\n ReplyAll,\n Eye,\n Send,\n Lock,\n Pause,\n GitMerge,\n Check,\n X,\n} from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { getInitials } from \"../lib/user-display\"\nimport { BRAND_ICONS } from \"../lib/icons\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"./avatar\"\nimport { Button } from \"./button\"\nimport { Switch } from \"./switch\"\nimport { Textarea } from \"./textarea\"\nimport { RichTextToolbar } from \"./rich-text-toolbar\"\nimport { EmailBody } from \"./email-body\"\nimport { decodeEmailDisplayText, emailBodySnippet, formatAddressList, normalizeEmailSender } from \"./email-display-helpers\"\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogDescription,\n DialogFooter,\n} from \"./dialog\"\n\n/* ── Types ───────────────────────────────────────────────────────────────── */\n\nexport interface ConvParticipant {\n name: string\n email: string\n avatarUrl?: string | null\n role?: string\n}\n\nexport interface ConvMessage {\n id: string\n direction: \"inbound\" | \"outbound\"\n from: ConvParticipant\n to: ConvParticipant\n /** Absolute timestamp label, e.g. \"Jun 1, 2026, 9:12 AM\". */\n date: string\n /**\n * Raw chronological timestamp for deterministic thread ordering. Prefer\n * `sentAt` for outbound messages and `receivedAt` for inbound messages.\n * Accepts ISO/RFC822 strings, Date objects, epoch milliseconds, or Gmail\n * internalDate values as strings/numbers. Display-only `date` / `ago` labels\n * are never parsed for ordering.\n */\n timestamp?: string | number | Date | null\n rawTimestamp?: string | number | Date | null\n sentAt?: string | number | Date | null\n receivedAt?: string | number | Date | null\n /** Compatibility with data contracts that pass through source field names. */\n sent_at?: string | number | Date | null\n received_at?: string | number | Date | null\n internalDate?: string | number | Date | null\n gmailInternalDate?: string | number | Date | null\n internal_date?: string | number | Date | null\n rfc822Date?: string | number | Date | null\n dateHeader?: string | number | Date | null\n /** Relative label, e.g. \"2 days ago\". */\n ago?: string\n receipt?: { kind: \"new\" | \"read\" | \"opened\" | \"sent\" | \"draft\"; label: string }\n /** HTML body (preferred). Sanitized by the component before rendering. */\n bodyHtml?: string\n /** Plain-text fallback when `bodyHtml` is absent. */\n body?: string\n /** Quoted prior message, collapsed behind a toggle. Sanitized before rendering. */\n quoted?: { attr: string; html: string }\n}\n\nexport type ConvStatus = \"responded\" | \"awaiting\" | \"viewing\" | \"draft\"\n\nexport interface ConversationThread {\n threadId: string\n subject: string\n status: ConvStatus\n /** Relative label for the most recent activity. */\n lastWhen?: string\n contact: ConvParticipant\n cc?: ConvParticipant[]\n /** Set when this thread's reply halted a playbook (terminal). */\n paused?: { playbook: string } | null\n /** false => operator cannot reply or create drafts from this thread. */\n canReply?: boolean\n /** Explains why reply and draft creation are disabled. */\n replyDisabledReason?: string\n /** Existing Gmail draft or thread URL. Rendered as a new-tab link when present. */\n openInGmailUrl?: string | null\n /** Forces the Open in Gmail action into a disabled state. */\n openInGmailDisabled?: boolean\n /** Tooltip/read-only copy for a disabled Open in Gmail action. */\n openInGmailDisabledReason?: string | null\n messages: ConvMessage[]\n /** Prefilled reply draft body. */\n draft?: string\n /** Signature text appended to replies (plain text). */\n signature?: string\n}\n\nexport interface ConversationReplyPayload {\n threadId: string\n body: string\n includeSignature: boolean\n replyAll: boolean\n}\n\n/**\n * Result of a server-side reply preview. `htmlBody` is the exact HTML the server\n * prepared for this reply (sanitized by the component before render).\n * `confirmationToken`, when present, identifies the prepared confirmation so a\n * consumer can reuse it for the final send instead of re-preparing the message.\n */\nexport interface ConversationReplyPreview {\n htmlBody: string\n confirmationToken?: string\n}\n\nexport interface ConversationPanelProps {\n threads: ConversationThread[]\n /** Current operator: drives \"to me\" + the reply avatar. */\n me?: ConvParticipant\n /** Deployment brand, used in the paused-playbook copy. */\n tenantName?: string\n onSendReply?: (payload: ConversationReplyPayload) => void | Promise<void>\n onCreateGmailDraft?: (payload: ConversationReplyPayload) => void | Promise<void>\n /**\n * Server-side preview contract. When provided, the reply preview requests the\n * exact send HTML from the server, the component sanitizes it before render,\n * and retains any returned `confirmationToken` in preview state so a consumer\n * can reuse it for the final send. When omitted, the composer falls back to a\n * clearly labeled local draft preview only.\n */\n onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>\n onOpenInGmail?: (threadId: string) => void\n /** Inline-open this thread initially (defaults to the first responded one). */\n defaultOpenThreadId?: string\n className?: string\n}\n\n/* ── Shared helpers ──────────────────────────────────────────────────────── */\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\")\n}\n\n/** Plain-text -> simple paragraph HTML for the Preview / sent-message body. */\nfunction textToHtml(text: string): string {\n return decodeEmailDisplayText(text)\n .split(/\\n{2,}/)\n .map((p) => p.trim())\n .filter(Boolean)\n .map((p) => `<p>${escapeHtml(p).replace(/\\n/g, \"<br>\")}</p>`)\n .join(\"\")\n}\n\nfunction displayParticipant(person: ConvParticipant) {\n return normalizeEmailSender({ name: person.name, email: person.email, fallbackName: person.email || person.name })\n}\n\nfunction firstName(name: string): string {\n return decodeEmailDisplayText(name).split(\" \")[0] || decodeEmailDisplayText(name)\n}\n\nfunction sameEmail(a?: string | null, b?: string | null): boolean {\n return Boolean(a && b && a.trim().toLowerCase() === b.trim().toLowerCase())\n}\n\nfunction messageBodySnippet(message: Pick<ConvMessage, \"body\" | \"bodyHtml\">, maxLength = 140): string {\n return emailBodySnippet({ bodyHtml: message.bodyHtml, body: message.body }, maxLength)\n}\n\n\ntype MessageTimestampValue = string | number | Date | null | undefined\n\nfunction parseMessageTimestampValue(value: MessageTimestampValue): number | null {\n if (value == null || value === \"\") return null\n\n if (value instanceof Date) {\n const time = value.getTime()\n return Number.isNaN(time) ? null : time\n }\n\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return null\n return value < 10_000_000_000 ? value * 1000 : value\n }\n\n const trimmed = value.trim()\n if (!trimmed) return null\n\n if (/^\\d+$/.test(trimmed)) {\n const numeric = Number(trimmed)\n if (!Number.isFinite(numeric)) return null\n return numeric < 10_000_000_000 ? numeric * 1000 : numeric\n }\n\n const parsed = Date.parse(trimmed)\n return Number.isNaN(parsed) ? null : parsed\n}\n\nfunction messageTimestamp(message: ConvMessage): number | null {\n const directional = message.direction === \"outbound\"\n ? [message.sentAt, message.sent_at]\n : [message.receivedAt, message.received_at]\n\n const candidates: MessageTimestampValue[] = [\n ...directional,\n message.rawTimestamp,\n message.timestamp,\n message.gmailInternalDate,\n message.internalDate,\n message.internal_date,\n message.rfc822Date,\n message.dateHeader,\n ]\n\n for (const candidate of candidates) {\n const parsed = parseMessageTimestampValue(candidate)\n if (parsed !== null) return parsed\n }\n\n return null\n}\n\nfunction sortMessagesChronologically(messages: ConvMessage[]): ConvMessage[] {\n return messages\n .map((message, index) => ({ message, index, timestamp: messageTimestamp(message) }))\n .sort((a, b) => {\n if (a.timestamp !== null && b.timestamp !== null && a.timestamp !== b.timestamp) {\n return a.timestamp - b.timestamp\n }\n return a.index - b.index\n })\n .map((entry) => entry.message)\n}\n\nfunction GmailMark({ size = 14 }: { size?: number }) {\n return (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={BRAND_ICONS.gmail.icon}\n alt=\"Gmail\"\n width={size}\n height={size}\n style={{ width: size, height: size, objectFit: \"contain\", display: \"block\" }}\n />\n )\n}\n\nfunction PersonAvatar({ person, size = \"sm\" }: { person: ConvParticipant; size?: \"sm\" | \"default\" }) {\n const display = displayParticipant(person)\n\n return (\n <Avatar size={size}>\n {person.avatarUrl ? <AvatarImage src={person.avatarUrl} alt={display.name} /> : null}\n <AvatarFallback className=\"bg-muted text-muted-foreground text-[10px] font-medium uppercase\">\n {getInitials({ name: display.name, email: display.email ?? person.email })}\n </AvatarFallback>\n </Avatar>\n )\n}\n\nconst STATUS_PILL: Record<ConvStatus, { label: string; cls: string }> = {\n responded: { label: \"NEW REPLY\", cls: \"bg-status-warning-bg text-status-warning-fg border-status-warning-border\" },\n draft: { label: \"Draft\", cls: \"bg-background text-foreground/80 border-border\" },\n awaiting: { label: \"SENT\", cls: \"bg-status-info-bg text-status-info-fg border-status-info-border\" },\n viewing: { label: \"Viewing\", cls: \"bg-muted text-muted-foreground border-border\" },\n}\n\nconst STATUS_DOT: Record<ConvStatus, string> = {\n responded: \"bg-status-warning-fg\",\n draft: \"bg-status-pending-fg\",\n awaiting: \"bg-status-info-fg\",\n viewing: \"bg-muted-foreground/50\",\n}\n\nconst THREAD_ROW_ACCENT: Record<ConvStatus, string> = {\n responded: \"border-l-4 border-l-status-warning-border bg-status-warning-bg/25\",\n awaiting: \"border-l-4 border-l-status-info-border bg-status-info-bg/25\",\n draft: \"border-l-4 border-l-status-pending-border bg-status-pending-bg/20\",\n viewing: \"\",\n}\n\nconst RECEIPT_CHIP: Record<NonNullable<ConvMessage[\"receipt\"]>[\"kind\"], string> = {\n new: \"border-status-warning-border bg-status-warning-bg text-status-warning-fg\",\n read: \"border-status-info-border bg-status-info-bg text-status-info-fg\",\n opened: \"border-status-info-border bg-status-info-bg text-status-info-fg\",\n sent: \"border-status-info-border bg-status-info-bg text-status-info-fg\",\n draft: \"border-status-pending-border bg-status-pending-bg text-status-pending-fg\",\n}\n\nfunction effectiveStatus(t: ConversationThread): ConvStatus {\n return t.canReply === false ? \"viewing\" : t.status\n}\n\nfunction disabledOpenInGmailReason(thread: ConversationThread): string {\n return (\n thread.openInGmailDisabledReason?.trim() ||\n thread.replyDisabledReason?.trim() ||\n \"Gmail access is not available for this thread.\"\n )\n}\n\nfunction canOpenInGmail(thread: ConversationThread, onOpenInGmail?: (threadId: string) => void): boolean {\n return thread.openInGmailDisabled !== true && Boolean(thread.openInGmailUrl || onOpenInGmail)\n}\n\nfunction OpenInGmailButton({\n thread,\n onOpenInGmail,\n label = \"Open in Gmail\",\n}: {\n thread: ConversationThread\n onOpenInGmail?: (threadId: string) => void\n label?: string\n}) {\n const hasConfiguredAction = Boolean(thread.openInGmailUrl || onOpenInGmail || thread.openInGmailDisabled || thread.openInGmailDisabledReason)\n if (!hasConfiguredAction) return null\n\n const disabled = !canOpenInGmail(thread, onOpenInGmail)\n const disabledReason = disabled\n ? disabledOpenInGmailReason(thread)\n : undefined\n\n if (!disabled && thread.openInGmailUrl) {\n return (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" asChild>\n <a href={thread.openInGmailUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n <GmailMark size={14} /> {label}\n </a>\n </Button>\n )\n }\n\n return (\n <span className=\"inline-flex\" title={disabledReason}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={disabled}\n aria-disabled={disabled || undefined}\n aria-label={disabledReason ? `${label}: ${disabledReason}` : label}\n onClick={disabled ? undefined : () => onOpenInGmail?.(thread.threadId)}\n >\n <GmailMark size={14} /> {label}\n </Button>\n </span>\n )\n}\n\n/* ── One message (collapsible) ──────────────────────────────────────────── */\n\nfunction MessageView({\n message,\n expanded,\n onToggle,\n me,\n}: {\n message: ConvMessage\n expanded: boolean\n onToggle: () => void\n me?: ConvParticipant\n}) {\n const [quoteOpen, setQuoteOpen] = React.useState(false)\n const fromDisplay = displayParticipant(message.from)\n const toDisplay = displayParticipant(message.to)\n\n if (!expanded) {\n const snippet = messageBodySnippet(message, 140)\n\n return (\n <button\n type=\"button\"\n data-slot=\"conv-message-collapsed\"\n onClick={onToggle}\n className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left hover:bg-muted/40\"\n >\n <PersonAvatar person={message.from} />\n <span className=\"text-muted-foreground min-w-0 flex-1 truncate text-[13px]\">\n <b className=\"text-foreground\">{firstName(fromDisplay.name)}</b> · {snippet}\n </span>\n <span className=\"text-muted-foreground/60 shrink-0 text-xs\">{message.ago ?? message.date}</span>\n <ChevronDown size={13} className=\"text-muted-foreground shrink-0\" />\n </button>\n )\n }\n\n const meDisplay = me ? displayParticipant(me) : null\n const toLabel = meDisplay && sameEmail(toDisplay.email, meDisplay.email) ? \"me\" : firstName(toDisplay.name)\n\n return (\n <div data-slot=\"conv-message\" className=\"rounded-md border border-border bg-background\">\n <button\n type=\"button\"\n onClick={onToggle}\n className=\"flex w-full items-start gap-2 px-3 py-2 text-left\"\n >\n <PersonAvatar person={message.from} size=\"default\" />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex flex-wrap items-baseline gap-x-1.5\">\n <span className=\"text-[13px] font-semibold\">{fromDisplay.name}</span>\n {fromDisplay.email ? (\n <span className=\"text-muted-foreground/60 truncate text-xs\">&lt;{fromDisplay.email}&gt;</span>\n ) : null}\n </span>\n <span className=\"text-muted-foreground block text-xs\">\n to <b>{toLabel}</b>\n </span>\n </span>\n <span className=\"flex shrink-0 items-center gap-2\">\n {message.receipt ? (\n <span className={cn(\"inline-flex items-center gap-1 rounded-md border px-1.5 py-px text-[10px] font-semibold leading-4\", RECEIPT_CHIP[message.receipt.kind])}>\n {message.receipt.kind === \"new\" ? (\n <CornerUpLeft size={11} />\n ) : message.receipt.kind === \"read\" || message.receipt.kind === \"sent\" ? (\n <CheckCheck size={11} />\n ) : message.receipt.kind === \"draft\" ? (\n <FilePenLine size={11} />\n ) : (\n <MailOpen size={11} />\n )}\n {message.receipt.label}\n </span>\n ) : null}\n <span className=\"text-muted-foreground/60 text-xs\">{message.date}</span>\n <ChevronUp size={13} className=\"text-muted-foreground\" />\n </span>\n </button>\n\n <div className=\"px-3 pb-2.5\">\n <div data-slot=\"conv-message-body\">\n <EmailBody\n html={message.bodyHtml}\n text={message.body}\n variant=\"history\"\n collapseDetails={true}\n className=\"text-sm\"\n />\n </div>\n\n {message.quoted ? (\n <div className=\"mt-2\">\n <button\n type=\"button\"\n onClick={() => setQuoteOpen((v) => !v)}\n className=\"text-muted-foreground hover:bg-muted rounded px-1.5 text-xs leading-5\"\n title={quoteOpen ? \"Hide quoted text\" : \"Show quoted text\"}\n >\n •••\n </button>\n {quoteOpen ? (\n <div className=\"border-border text-muted-foreground mt-1 border-l-2 pl-3 text-[13px]\">\n <p className=\"mb-1\">{decodeEmailDisplayText(message.quoted.attr)}</p>\n <div data-slot=\"conv-quoted-body\">\n <EmailBody html={message.quoted.html} variant=\"history\" collapseDetails={false} />\n </div>\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n\n/* ── Reply composer ─────────────────────────────────────────────────────── */\n\ntype PreviewState = {\n loading: boolean\n /** Sanitized HTML ready to render. */\n html: string | null\n /** Confirmation token returned by the server preview, retained for reuse on send. */\n confirmationToken: string | null\n error: string | null\n /** True when `html` came from the local fallback rather than the server. */\n local: boolean\n}\n\nconst IDLE_PREVIEW: PreviewState = { loading: false, html: null, confirmationToken: null, error: null, local: false }\n\nfunction ReplyComposer({\n thread,\n me,\n replyAll,\n tenantName,\n onClose,\n onSend,\n onDraft,\n onPreviewReply,\n draftDisabledReason,\n}: {\n thread: ConversationThread\n me?: ConvParticipant\n replyAll: boolean\n tenantName?: string\n onClose: () => void\n onSend: (body: string, includeSignature: boolean) => void | Promise<void>\n onDraft: (body: string, includeSignature: boolean) => void | Promise<void>\n onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>\n draftDisabledReason?: string | null\n}) {\n const [body, setBody] = React.useState(thread.draft ?? \"\")\n const [sig, setSig] = React.useState(true)\n const [preview, setPreview] = React.useState(false)\n const [previewState, setPreviewState] = React.useState<PreviewState>(IDLE_PREVIEW)\n const [sending, setSending] = React.useState(false)\n const [sendError, setSendError] = React.useState<string | null>(null)\n const ccList = replyAll ? thread.cc ?? [] : []\n const subject = /^re:/i.test(thread.subject) ? thread.subject : `Re: ${thread.subject}`\n const draftDisabled = Boolean(draftDisabledReason)\n\n const localPreviewHtml = textToHtml(body) + (sig && thread.signature ? textToHtml(thread.signature) : \"\")\n\n const openPreview = async () => {\n setPreview(true)\n setSendError(null)\n\n if (!onPreviewReply) {\n // No server preview contract: render a sanitized local draft preview only.\n setPreviewState({ loading: false, html: localPreviewHtml, confirmationToken: null, error: null, local: true })\n return\n }\n\n setPreviewState({ loading: true, html: null, confirmationToken: null, error: null, local: false })\n try {\n const result = await onPreviewReply({ threadId: thread.threadId, body, includeSignature: sig, replyAll })\n setPreviewState({\n loading: false,\n html: result.htmlBody ?? \"\",\n confirmationToken: result.confirmationToken ?? null,\n error: null,\n local: false,\n })\n } catch (error) {\n setPreviewState({\n loading: false,\n html: null,\n confirmationToken: null,\n error: error instanceof Error ? error.message : \"Could not load the preview. Please try again.\",\n local: false,\n })\n }\n }\n\n const handleSend = async () => {\n setSending(true)\n setSendError(null)\n try {\n await onSend(body, sig)\n setPreview(false)\n setPreviewState(IDLE_PREVIEW)\n } catch (error) {\n setSendError(error instanceof Error ? error.message : \"Could not send this reply. Please try again.\")\n } finally {\n setSending(false)\n }\n }\n\n const handleDraft = async () => {\n if (draftDisabled) return\n setSending(true)\n setSendError(null)\n try {\n await onDraft(body, sig)\n setPreview(false)\n setPreviewState(IDLE_PREVIEW)\n } catch (error) {\n setSendError(error instanceof Error ? error.message : \"Could not create the Gmail draft. Please try again.\")\n } finally {\n setSending(false)\n }\n }\n\n return (\n <div data-slot=\"conv-reply\" className=\"border-border bg-muted/20 rounded-md border p-3\">\n <div className=\"mb-2 flex items-center gap-2\">\n {me ? <PersonAvatar person={me} /> : null}\n <span className=\"flex-1 text-[13px] font-medium\">\n {replyAll ? (\n <>\n Reply all{\" \"}\n <span className=\"text-muted-foreground font-normal\">· {1 + ccList.length} recipients</span>\n </>\n ) : (\n <>\n Reply to <b>{firstName(displayParticipant(thread.contact).name)}</b>\n </>\n )}\n </span>\n <span className=\"text-muted-foreground inline-flex items-center gap-1 text-[11px]\">\n <GitMerge size={11} /> Same thread\n </span>\n <button type=\"button\" onClick={onClose} title=\"Discard reply\" className=\"text-muted-foreground hover:text-foreground\">\n <X size={15} />\n </button>\n </div>\n\n <div className=\"border-border mb-2 space-y-1 border-b pb-2 text-[13px]\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">To</span>\n <span className=\"font-medium\">{displayParticipant(thread.contact).name}</span>\n <span className=\"text-muted-foreground/60 truncate text-xs\">{displayParticipant(thread.contact).email ?? thread.contact.email}</span>\n </div>\n {replyAll && ccList.length ? (\n <div className=\"flex items-start gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">Cc</span>\n <span className=\"text-muted-foreground text-xs\">\n {formatAddressList(ccList.map((c) => `${displayParticipant(c).name} <${displayParticipant(c).email ?? c.email}>`))}\n </span>\n </div>\n ) : null}\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">Subject</span>\n <span className=\"truncate\">{subject}</span>\n <span className=\"text-muted-foreground/60 ml-auto inline-flex items-center gap-1 text-[11px]\">\n <Lock size={10} /> on thread\n </span>\n </div>\n </div>\n\n <Textarea\n value={body}\n onChange={(e) => setBody(e.target.value)}\n placeholder=\"Write your reply…\"\n className=\"min-h-28 resize-y text-sm\"\n onKeyDown={(e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === \"Enter\") {\n e.preventDefault()\n void openPreview()\n }\n }}\n />\n\n {sig && thread.signature ? (\n <div className=\"text-muted-foreground mt-2 whitespace-pre-line border-l-2 border-border pl-3 text-[13px]\">\n <span className=\"text-muted-foreground/60 mr-1\">--</span>\n {thread.signature}\n </div>\n ) : null}\n\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <RichTextToolbar />\n <label className=\"text-muted-foreground ml-auto inline-flex cursor-pointer items-center gap-1.5 text-[12px]\">\n <Switch checked={sig} onCheckedChange={setSig} aria-label=\"Toggle signature\" />\n Signature\n </label>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={sending} onClick={() => void openPreview()}>\n <Eye size={14} /> Preview\n </Button>\n <Button type=\"button\" size=\"sm\" disabled={sending} onClick={() => void openPreview()}>\n <Send size={14} /> Send\n </Button>\n </div>\n\n <Dialog open={preview} onOpenChange={(open) => { if (!sending) { setPreview(open); if (!open) setPreviewState(IDLE_PREVIEW) } }}>\n <DialogContent className=\"max-w-xl\">\n <DialogHeader>\n <DialogTitle className=\"flex items-center gap-1.5 text-[15px]\">\n <Eye size={15} /> {previewState.local ? \"Local draft preview\" : \"Reply preview\"}\n </DialogTitle>\n <DialogDescription>\n {previewState.local\n ? \"Local draft preview only — the server prepares the exact message on send.\"\n : <>Stays on the {subject.replace(/^Re:\\s*/i, \"\")} thread. Gmail keeps it threaded.</>}\n </DialogDescription>\n </DialogHeader>\n <div className=\"border-border space-y-1 rounded-md border p-3 text-[13px]\">\n <div>\n <span className=\"text-muted-foreground\">To </span>\n <b>{displayParticipant(thread.contact).name}</b>{\" \"}\n <span className=\"text-muted-foreground/60\">&lt;{displayParticipant(thread.contact).email ?? thread.contact.email}&gt;</span>\n </div>\n {replyAll && ccList.length ? (\n <div className=\"text-muted-foreground\">Cc {formatAddressList(ccList.map((c) => `${displayParticipant(c).name} <${displayParticipant(c).email ?? c.email}>`))}</div>\n ) : null}\n <div>\n <span className=\"text-muted-foreground\">Subject </span>\n {subject}\n </div>\n </div>\n {previewState.loading ? (\n <div data-slot=\"conv-preview-loading\" role=\"status\" className=\"text-muted-foreground flex items-center gap-2 px-1 py-6 text-[13px]\">\n <span className=\"border-muted-foreground/40 border-t-foreground size-4 animate-spin rounded-full border-2\" aria-hidden />\n Loading preview…\n </div>\n ) : previewState.error ? (\n <p role=\"alert\" className=\"text-destructive text-sm\">\n {previewState.error}\n </p>\n ) : (\n <div\n data-slot=\"conv-preview-body\"\n data-confirmation-token={previewState.confirmationToken ?? undefined}\n className=\"max-h-72 overflow-auto\"\n >\n <EmailBody html={previewState.html ?? \"\"} variant=\"preview\" collapseDetails={false} defaultDetailsOpen />\n </div>\n )}\n {sendError ? (\n <p role=\"alert\" className=\"text-destructive text-sm\">\n {sendError}\n </p>\n ) : null}\n <DialogFooter className=\"sm:justify-between\">\n <span className=\"inline-flex\" title={draftDisabledReason ?? undefined}>\n <button\n type=\"button\"\n disabled={sending || previewState.loading || draftDisabled}\n onClick={handleDraft}\n aria-label={draftDisabledReason ? `Open draft in Gmail: ${draftDisabledReason}` : \"Open draft in Gmail\"}\n className=\"text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-[13px] disabled:pointer-events-none disabled:opacity-50\"\n >\n <GmailMark size={14} /> Open draft in Gmail\n </button>\n </span>\n <span className=\"flex items-center gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={sending} onClick={() => { setPreview(false); setPreviewState(IDLE_PREVIEW) }}>\n Keep editing\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n disabled={sending || previewState.loading}\n onClick={handleSend}\n >\n <Send size={14} /> {sending ? \"Sending...\" : \"Send now\"}\n </Button>\n </span>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n {tenantName ? (\n <p className=\"text-muted-foreground/70 mt-2 text-[11px]\">Sends via Gmail · playbooks stay stopped.</p>\n ) : null}\n </div>\n )\n}\n\n/* ── Thread body (messages + footer/composer/done states) ───────────────── */\n\ntype ThreadMode = \"idle\" | \"replying\" | \"sent\" | \"draft\"\n\nfunction ThreadBody({\n thread,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onPreviewReply,\n onOpenInGmail,\n}: {\n thread: ConversationThread\n me?: ConvParticipant\n tenantName?: string\n onSendReply?: (p: ConversationReplyPayload) => void | Promise<void>\n onCreateGmailDraft?: (p: ConversationReplyPayload) => void | Promise<void>\n onPreviewReply?: (p: ConversationReplyPayload) => Promise<ConversationReplyPreview>\n onOpenInGmail?: (threadId: string) => void\n}) {\n const canReply = thread.canReply !== false\n const replyDisabledReason = thread.replyDisabledReason?.trim() || \"You are not a participant on this thread, so replying is disabled here.\"\n const draftDisabledReason = onCreateGmailDraft ? null : \"Gmail draft creation is not available for this thread.\"\n const hasCc = !!(thread.cc && thread.cc.length)\n const sortedMessages = React.useMemo(() => sortMessagesChronologically(thread.messages), [thread.messages])\n const [mode, setMode] = React.useState<ThreadMode>(\"idle\")\n const [replyAll, setReplyAll] = React.useState(false)\n const [expanded, setExpanded] = React.useState<Record<string, boolean>>(() => {\n const o: Record<string, boolean> = {}\n sortedMessages.forEach((m, i) => {\n o[m.id] = i === sortedMessages.length - 1\n })\n return o\n })\n\n React.useEffect(() => {\n setExpanded((current) => {\n const next = { ...current }\n sortedMessages.forEach((m, i) => {\n if (next[m.id] === undefined) next[m.id] = i === sortedMessages.length - 1\n })\n return next\n })\n }, [sortedMessages])\n\n const toggle = (id: string) => setExpanded((e) => ({ ...e, [id]: !e[id] }))\n\n return (\n <div data-slot=\"conv-thread-body\" className=\"space-y-2\">\n {canReply && thread.paused ? (\n <div className=\"border-status-warning-border bg-status-warning-bg text-status-warning-fg flex items-start gap-2 rounded-md border border-l-4 p-2.5 text-[12px]\">\n <Pause size={13} className=\"mt-0.5 shrink-0\" />\n <span>\n <b>Playbook stopped.</b> Follow-up actions for {thread.paused.playbook} won’t send\n automatically while this conversation is live. Continue it in {tenantName ?? \"the app\"} or Gmail.\n </span>\n </div>\n ) : null}\n\n <div className=\"space-y-1\">\n {sortedMessages.map((m) => (\n <MessageView key={m.id} message={m} expanded={!!expanded[m.id]} onToggle={() => toggle(m.id)} me={me} />\n ))}\n </div>\n\n {!canReply ? (\n <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]\">\n <Eye size={14} className=\"mt-0.5 shrink-0\" />\n <span className=\"min-w-0 flex-1\">\n <b>Viewing only.</b> {replyDisabledReason}\n </span>\n <OpenInGmailButton thread={thread} onOpenInGmail={onOpenInGmail} />\n </div>\n ) : null}\n\n {canReply && mode === \"idle\" ? (\n <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\">\n <Button type=\"button\" size=\"sm\" onClick={() => { setReplyAll(false); setMode(\"replying\") }}>\n <Reply size={15} /> Reply\n </Button>\n {hasCc ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => { setReplyAll(true); setMode(\"replying\") }}>\n <ReplyAll size={14} /> Reply all\n </Button>\n ) : null}\n <OpenInGmailButton thread={thread} onOpenInGmail={onOpenInGmail} />\n <span className=\"text-muted-foreground/70 ml-auto inline-flex items-center gap-1.5 text-[12px]\">\n <GitMerge size={13} /> Stays on this thread\n </span>\n </div>\n ) : null}\n\n {canReply && mode === \"replying\" ? (\n <ReplyComposer\n thread={thread}\n me={me}\n replyAll={replyAll}\n tenantName={tenantName}\n onPreviewReply={onPreviewReply}\n onClose={() => setMode(\"idle\")}\n onSend={async (body, includeSignature) => {\n await onSendReply?.({ threadId: thread.threadId, body, includeSignature, replyAll })\n setMode(\"sent\")\n }}\n onDraft={async (body, includeSignature) => {\n if (!onCreateGmailDraft) return\n await onCreateGmailDraft({ threadId: thread.threadId, body, includeSignature, replyAll })\n setMode(\"draft\")\n }}\n draftDisabledReason={draftDisabledReason}\n />\n ) : null}\n\n {canReply && mode === \"sent\" ? (\n <div className=\"border-status-active-border bg-status-active-bg flex items-center gap-2 rounded-md border p-3 text-[13px]\">\n <Check size={16} className=\"text-status-active-fg shrink-0\" />\n <span className=\"flex-1\">\n <b>{replyAll ? \"Reply all sent\" : \"Reply sent\"}</b> · added to the thread. Delivered to{\" \"}\n <b>{displayParticipant(thread.contact).name}</b>. This action stays <b>Pending</b>; playbooks remain stopped.\n </span>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setMode(\"idle\")}>\n Done\n </Button>\n </div>\n ) : null}\n\n {canReply && mode === \"draft\" ? (\n <div className=\"border-border bg-muted/30 flex items-center gap-2 rounded-md border p-3 text-[13px]\">\n <GmailMark size={16} />\n <span className=\"flex-1\">\n <b>Draft saved to Gmail.</b> Waiting on the <b>Re: {thread.subject}</b> thread; open it there to finish. Nothing was sent.\n </span>\n <OpenInGmailButton thread={thread} onOpenInGmail={onOpenInGmail} />\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setMode(\"idle\")}>\n Done\n </Button>\n </div>\n ) : null}\n </div>\n )\n}\n\n/* ── A thread row + its inline reader ───────────────────────────────────── */\n\nfunction ThreadRow({\n thread,\n open,\n onToggleOpen,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onPreviewReply,\n onOpenInGmail,\n}: {\n thread: ConversationThread\n open: boolean\n onToggleOpen: () => void\n} & Pick<ConversationPanelProps, \"me\" | \"tenantName\" | \"onSendReply\" | \"onCreateGmailDraft\" | \"onPreviewReply\" | \"onOpenInGmail\">) {\n const status = effectiveStatus(thread)\n const sortedMessages = React.useMemo(() => sortMessagesChronologically(thread.messages), [thread.messages])\n const last = sortedMessages[sortedMessages.length - 1]\n const lastSender = last ? displayParticipant(last.from) : null\n const meDisplay = me ? displayParticipant(me) : null\n const who = last?.direction === \"outbound\" && sameEmail(lastSender?.email, meDisplay?.email) ? \"You\" : firstName(lastSender?.name ?? \"\")\n const lastSnippet = last ? messageBodySnippet(last, 120) : \"\"\n const pill = STATUS_PILL[status]\n\n return (\n <div\n data-slot=\"conv-thread\"\n data-status={status}\n data-open={open ? \"true\" : undefined}\n className={cn(\"border-border border-b last:border-b-0\", THREAD_ROW_ACCENT[status])}\n >\n <button\n type=\"button\"\n onClick={onToggleOpen}\n aria-expanded={open}\n className=\"flex w-full items-center gap-2.5 px-3 py-2.5 text-left hover:bg-muted/30\"\n >\n <span className={cn(\"size-2 shrink-0 rounded-full\", STATUS_DOT[status])} aria-hidden />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex items-center gap-2\">\n <span className=\"truncate text-sm font-semibold\">{thread.subject}</span>\n <span className={cn(\"shrink-0 rounded-md border px-1.5 py-px text-[10px] font-medium leading-4\", pill.cls)}>\n {pill.label}\n </span>\n </span>\n <span className=\"text-muted-foreground block truncate text-xs\">\n <b className=\"text-foreground/80\">{displayParticipant(thread.contact).name}</b> · {who}: {lastSnippet}\n </span>\n </span>\n <span className=\"text-muted-foreground/60 shrink-0 text-xs\">{thread.lastWhen}</span>\n {open ? (\n <ChevronUp size={15} className=\"text-muted-foreground shrink-0\" />\n ) : (\n <ChevronDown size={15} className=\"text-muted-foreground shrink-0\" />\n )}\n </button>\n\n {open ? (\n <div className=\"px-3 pb-3\">\n <ThreadBody\n thread={thread}\n me={me}\n tenantName={tenantName}\n onSendReply={onSendReply}\n onCreateGmailDraft={onCreateGmailDraft}\n onPreviewReply={onPreviewReply}\n onOpenInGmail={onOpenInGmail}\n />\n </div>\n ) : null}\n </div>\n )\n}\n\n/* ── The hub ─────────────────────────────────────────────────────────────── */\n\nfunction ConversationPanel({\n threads,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onPreviewReply,\n onOpenInGmail,\n defaultOpenThreadId,\n className,\n}: ConversationPanelProps) {\n const responded = threads.filter((t) => t.status === \"responded\" && t.canReply !== false).length\n const draft = threads.filter((t) => effectiveStatus(t) === \"draft\").length\n const awaiting = threads.filter((t) => effectiveStatus(t) === \"awaiting\").length\n const anyPaused = threads.some((t) => t.paused)\n const prioritizedThread =\n threads.find((t) => t.status === \"responded\" && t.canReply !== false) ??\n threads.find((t) => effectiveStatus(t) === \"draft\") ??\n threads.find((t) => effectiveStatus(t) === \"awaiting\")\n const hubGmailThread =\n threads.find((t) => t.status === \"responded\" && t.canReply !== false && canOpenInGmail(t, onOpenInGmail)) ??\n threads.find((t) => effectiveStatus(t) === \"draft\" && canOpenInGmail(t, onOpenInGmail)) ??\n threads.find((t) => effectiveStatus(t) === \"awaiting\" && canOpenInGmail(t, onOpenInGmail)) ??\n threads.find((t) => canOpenInGmail(t, onOpenInGmail))\n const firstAwaiting = threads.find((t) => effectiveStatus(t) === \"awaiting\")\n\n const [hubOpen, setHubOpen] = React.useState(true)\n const [openId, setOpenId] = React.useState<string | null>(() => {\n if (defaultOpenThreadId) return defaultOpenThreadId\n return prioritizedThread ? prioritizedThread.threadId : null\n })\n\n if (!threads.length) return null\n\n // Header badge state: a responded reply leads, then drafts to finish, then sent mail awaiting a reply.\n const badge =\n responded > 0\n ? { label: \"Email response detected\", dot: \"bg-status-warning-fg\", ring: \"bg-status-warning-fg/30\" }\n : draft > 0\n ? { label: \"Draft ready\", dot: \"bg-status-pending-fg\", ring: \"bg-status-pending-fg/30\" }\n : awaiting > 0\n ? { label: \"Email sent · awaiting reply\", dot: \"bg-status-info-fg\", ring: \"bg-status-info-fg/30\" }\n : { label: \"Conversations\", dot: \"bg-muted-foreground/50\", ring: \"bg-muted-foreground/20\" }\n\n const headTitle =\n responded > 0\n ? `${responded} ${responded === 1 ? \"reply needs\" : \"replies need\"} your response`\n : draft > 0\n ? `Draft ready on ${draft} ${draft === 1 ? \"thread\" : \"threads\"}`\n : awaiting > 0\n ? awaiting === 1 && firstAwaiting\n ? `Awaiting a response from ${firstName(displayParticipant(firstAwaiting.contact).name)}`\n : `Awaiting responses on ${awaiting} threads`\n : `${threads.length} email ${threads.length === 1 ? \"thread\" : \"threads\"}`\n\n const panelState = responded > 0 ? \"responded\" : draft > 0 ? \"draft\" : awaiting > 0 ? \"awaiting\" : \"viewing\"\n\n return (\n <section\n data-slot=\"conversation-panel\"\n data-responded={responded > 0 ? \"true\" : undefined}\n data-state={panelState}\n className={cn(\n \"bg-background overflow-hidden rounded-xl border\",\n panelState === \"responded\"\n ? \"border-status-warning-border\"\n : panelState === \"awaiting\"\n ? \"border-status-info-border\"\n : \"border-border\",\n className,\n )}\n >\n <div\n data-slot=\"conversation-panel-header\"\n className={cn(\n \"flex w-full items-center gap-2 px-3 py-2.5\",\n panelState === \"responded\"\n ? \"bg-status-warning-bg/45\"\n : panelState === \"awaiting\"\n ? \"bg-status-info-bg/55\"\n : \"bg-background\",\n )}\n >\n <button\n type=\"button\"\n onClick={() => setHubOpen((v) => !v)}\n aria-expanded={hubOpen}\n className=\"flex min-w-0 flex-1 items-center gap-3 text-left\"\n >\n <span\n data-slot=\"conversation-badge\"\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-semibold\",\n responded > 0\n ? \"bg-status-warning-bg text-status-warning-fg\"\n : draft > 0\n ? \"bg-status-pending-bg text-status-pending-fg\"\n : awaiting > 0\n ? \"bg-status-info-bg text-status-info-fg\"\n : \"bg-muted text-muted-foreground\"\n )}\n >\n <span className=\"relative inline-flex size-2\">\n <span className={cn(\"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75\", badge.ring)} />\n <span className={cn(\"relative inline-flex size-2 rounded-full\", badge.dot)} />\n </span>\n {badge.label}\n </span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-semibold leading-tight\">{headTitle}</span>\n <span className=\"text-muted-foreground mt-0.5 block truncate text-xs\">\n {threads.length} {threads.length === 1 ? \"thread\" : \"threads\"} on this action\n {anyPaused ? <> · <b>playbook stopped</b></> : null}\n </span>\n </span>\n {hubOpen ? (\n <ChevronUp size={16} className=\"text-muted-foreground shrink-0\" />\n ) : (\n <ChevronDown size={16} className=\"text-muted-foreground shrink-0\" />\n )}\n </button>\n {hubGmailThread ? (\n <div className=\"shrink-0\" onClick={(event) => event.stopPropagation()}>\n <OpenInGmailButton thread={hubGmailThread} onOpenInGmail={onOpenInGmail} />\n </div>\n ) : null}\n </div>\n\n {hubOpen ? (\n <div className=\"border-border border-t\">\n {threads.map((t) => (\n <ThreadRow\n key={t.threadId}\n thread={t}\n open={openId === t.threadId}\n onToggleOpen={() => setOpenId((cur) => (cur === t.threadId ? null : t.threadId))}\n me={me}\n tenantName={tenantName}\n onSendReply={onSendReply}\n onCreateGmailDraft={onCreateGmailDraft}\n onPreviewReply={onPreviewReply}\n onOpenInGmail={onOpenInGmail}\n />\n ))}\n </div>\n ) : null}\n </section>\n )\n}\n\nexport { ConversationPanel }\n"],"mappings":"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","names":["_a","_b"]}
1
+ {"version":3,"sources":["../../src/components/conversation-panel.tsx"],"sourcesContent":["\"use client\"\n\n/**\n * conversation-panel.tsx — in-case email-thread reader + reply, for the case\n * panel (\"Email response detected\" hub).\n *\n * v1 scope (WIT-853 / WIT-802): INLINE thread reading + reply only.\n * - A collapsible hub header with a pulse badge whose color reflects state:\n * responded (a reply needs you) / awaiting (sent, waiting) / viewing (read-only).\n * - A list of thread rows; clicking one opens a Gmail-style reader inline,\n * right under the row, with collapsible messages + quoted-history toggle.\n * - Reply / Reply-all composer with a signature toggle and two send paths:\n * Preview→Send (in-app) and \"Open draft in Gmail\" (deep link).\n * - A \"playbook stopped\" banner when the customer's reply halted the sequence,\n * and a read-only notice when the operator is not a thread participant.\n *\n * The bottom-right floating dock / side-by-side compare is intentionally OUT of\n * scope here and tracked separately (WIT-855).\n *\n * Presentational: all data + side effects come from the consumer. Email bodies\n * (`bodyHtml`, `quoted.html`) are sanitized before rendering.\n */\n\nimport * as React from \"react\"\nimport {\n ChevronDown,\n ChevronUp,\n CornerUpLeft,\n CheckCheck,\n MailOpen,\n FilePenLine,\n Reply,\n ReplyAll,\n Eye,\n Send,\n Lock,\n Pause,\n GitMerge,\n Check,\n X,\n} from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { getInitials } from \"../lib/user-display\"\nimport { BRAND_ICONS } from \"../lib/icons\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"./avatar\"\nimport { Button } from \"./button\"\nimport { Switch } from \"./switch\"\nimport { Textarea } from \"./textarea\"\nimport { RichTextToolbar } from \"./rich-text-toolbar\"\nimport { EmailBody } from \"./email-body\"\nimport { decodeEmailDisplayText, emailBodySnippet, formatAddressList, normalizeEmailSender } from \"./email-display-helpers\"\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogDescription,\n DialogFooter,\n} from \"./dialog\"\n\n/* ── Types ───────────────────────────────────────────────────────────────── */\n\nexport interface ConvParticipant {\n name: string\n email: string\n avatarUrl?: string | null\n role?: string\n}\n\nexport interface ConvMessage {\n id: string\n direction: \"inbound\" | \"outbound\"\n from: ConvParticipant\n to: ConvParticipant\n /** Absolute timestamp label, e.g. \"Jun 1, 2026, 9:12 AM\". */\n date: string\n /**\n * Raw chronological timestamp for deterministic thread ordering. Prefer\n * `sentAt` for outbound messages and `receivedAt` for inbound messages.\n * Accepts ISO/RFC822 strings, Date objects, epoch milliseconds, or Gmail\n * internalDate values as strings/numbers. Display-only `date` / `ago` labels\n * are never parsed for ordering.\n */\n timestamp?: string | number | Date | null\n rawTimestamp?: string | number | Date | null\n sentAt?: string | number | Date | null\n receivedAt?: string | number | Date | null\n /** Compatibility with data contracts that pass through source field names. */\n sent_at?: string | number | Date | null\n received_at?: string | number | Date | null\n internalDate?: string | number | Date | null\n gmailInternalDate?: string | number | Date | null\n internal_date?: string | number | Date | null\n rfc822Date?: string | number | Date | null\n dateHeader?: string | number | Date | null\n /** Relative label, e.g. \"2 days ago\". */\n ago?: string\n receipt?: { kind: \"new\" | \"read\" | \"opened\" | \"sent\" | \"draft\"; label: string }\n /** HTML body (preferred). Sanitized by the component before rendering. */\n bodyHtml?: string\n /**\n * Pre-split signature HTML for this message, sanitized server-side by the\n * sender's signature pipeline (which preserves the authored Gmail look —\n * fonts, colors, logos). When present it renders inside the collapsed\n * details section (\"•••\" toggle) instead of relying on the heuristic footer\n * split of `bodyHtml`, so long signatures never expand the thread by\n * default.\n */\n signatureHtml?: string | null\n /** Plain-text fallback when `bodyHtml` is absent. */\n body?: string\n /** Quoted prior message, collapsed behind a toggle. Sanitized before rendering. */\n quoted?: { attr: string; html: string }\n}\n\nexport type ConvStatus = \"responded\" | \"awaiting\" | \"viewing\" | \"draft\"\n\nexport interface ConversationThread {\n threadId: string\n subject: string\n status: ConvStatus\n /** Relative label for the most recent activity. */\n lastWhen?: string\n contact: ConvParticipant\n cc?: ConvParticipant[]\n /** Set when this thread's reply halted a playbook (terminal). */\n paused?: { playbook: string } | null\n /** false => operator cannot reply or create drafts from this thread. */\n canReply?: boolean\n /** Explains why reply and draft creation are disabled. */\n replyDisabledReason?: string\n /** Existing Gmail draft or thread URL. Rendered as a new-tab link when present. */\n openInGmailUrl?: string | null\n /** Forces the Open in Gmail action into a disabled state. */\n openInGmailDisabled?: boolean\n /** Tooltip/read-only copy for a disabled Open in Gmail action. */\n openInGmailDisabledReason?: string | null\n messages: ConvMessage[]\n /** Prefilled reply draft body. */\n draft?: string\n /** Signature text appended to replies (plain text). */\n signature?: string\n}\n\nexport interface ConversationReplyPayload {\n threadId: string\n body: string\n includeSignature: boolean\n replyAll: boolean\n}\n\n/**\n * Result of a server-side reply preview. `htmlBody` is the exact HTML the server\n * prepared for this reply (sanitized by the component before render).\n * `confirmationToken`, when present, identifies the prepared confirmation so a\n * consumer can reuse it for the final send instead of re-preparing the message.\n */\nexport interface ConversationReplyPreview {\n htmlBody: string\n confirmationToken?: string\n}\n\nexport interface ConversationPanelProps {\n threads: ConversationThread[]\n /** Current operator: drives \"to me\" + the reply avatar. */\n me?: ConvParticipant\n /** Deployment brand, used in the paused-playbook copy. */\n tenantName?: string\n onSendReply?: (payload: ConversationReplyPayload) => void | Promise<void>\n onCreateGmailDraft?: (payload: ConversationReplyPayload) => void | Promise<void>\n /**\n * Server-side preview contract. When provided, the reply preview requests the\n * exact send HTML from the server, the component sanitizes it before render,\n * and retains any returned `confirmationToken` in preview state so a consumer\n * can reuse it for the final send. When omitted, the composer falls back to a\n * clearly labeled local draft preview only.\n */\n onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>\n onOpenInGmail?: (threadId: string) => void\n /** Inline-open this thread initially (defaults to the first responded one). */\n defaultOpenThreadId?: string\n className?: string\n}\n\n/* ── Shared helpers ──────────────────────────────────────────────────────── */\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\")\n}\n\n/** Plain-text -> simple paragraph HTML for the Preview / sent-message body. */\nfunction textToHtml(text: string): string {\n return decodeEmailDisplayText(text)\n .split(/\\n{2,}/)\n .map((p) => p.trim())\n .filter(Boolean)\n .map((p) => `<p>${escapeHtml(p).replace(/\\n/g, \"<br>\")}</p>`)\n .join(\"\")\n}\n\nfunction displayParticipant(person: ConvParticipant) {\n return normalizeEmailSender({ name: person.name, email: person.email, fallbackName: person.email || person.name })\n}\n\nfunction firstName(name: string): string {\n return decodeEmailDisplayText(name).split(\" \")[0] || decodeEmailDisplayText(name)\n}\n\nfunction sameEmail(a?: string | null, b?: string | null): boolean {\n return Boolean(a && b && a.trim().toLowerCase() === b.trim().toLowerCase())\n}\n\nfunction messageBodySnippet(message: Pick<ConvMessage, \"body\" | \"bodyHtml\">, maxLength = 140): string {\n return emailBodySnippet({ bodyHtml: message.bodyHtml, body: message.body }, maxLength)\n}\n\n\ntype MessageTimestampValue = string | number | Date | null | undefined\n\nfunction parseMessageTimestampValue(value: MessageTimestampValue): number | null {\n if (value == null || value === \"\") return null\n\n if (value instanceof Date) {\n const time = value.getTime()\n return Number.isNaN(time) ? null : time\n }\n\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return null\n return value < 10_000_000_000 ? value * 1000 : value\n }\n\n const trimmed = value.trim()\n if (!trimmed) return null\n\n if (/^\\d+$/.test(trimmed)) {\n const numeric = Number(trimmed)\n if (!Number.isFinite(numeric)) return null\n return numeric < 10_000_000_000 ? numeric * 1000 : numeric\n }\n\n const parsed = Date.parse(trimmed)\n return Number.isNaN(parsed) ? null : parsed\n}\n\nfunction messageTimestamp(message: ConvMessage): number | null {\n const directional = message.direction === \"outbound\"\n ? [message.sentAt, message.sent_at]\n : [message.receivedAt, message.received_at]\n\n const candidates: MessageTimestampValue[] = [\n ...directional,\n message.rawTimestamp,\n message.timestamp,\n message.gmailInternalDate,\n message.internalDate,\n message.internal_date,\n message.rfc822Date,\n message.dateHeader,\n ]\n\n for (const candidate of candidates) {\n const parsed = parseMessageTimestampValue(candidate)\n if (parsed !== null) return parsed\n }\n\n return null\n}\n\nfunction sortMessagesChronologically(messages: ConvMessage[]): ConvMessage[] {\n return messages\n .map((message, index) => ({ message, index, timestamp: messageTimestamp(message) }))\n .sort((a, b) => {\n if (a.timestamp !== null && b.timestamp !== null && a.timestamp !== b.timestamp) {\n return a.timestamp - b.timestamp\n }\n return a.index - b.index\n })\n .map((entry) => entry.message)\n}\n\nfunction GmailMark({ size = 14 }: { size?: number }) {\n return (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={BRAND_ICONS.gmail.icon}\n alt=\"Gmail\"\n width={size}\n height={size}\n style={{ width: size, height: size, objectFit: \"contain\", display: \"block\" }}\n />\n )\n}\n\nfunction PersonAvatar({ person, size = \"sm\" }: { person: ConvParticipant; size?: \"sm\" | \"default\" }) {\n const display = displayParticipant(person)\n\n return (\n <Avatar size={size}>\n {person.avatarUrl ? <AvatarImage src={person.avatarUrl} alt={display.name} /> : null}\n <AvatarFallback className=\"bg-muted text-muted-foreground text-[10px] font-medium uppercase\">\n {getInitials({ name: display.name, email: display.email ?? person.email })}\n </AvatarFallback>\n </Avatar>\n )\n}\n\nconst STATUS_PILL: Record<ConvStatus, { label: string; cls: string }> = {\n responded: { label: \"NEW REPLY\", cls: \"bg-status-warning-bg text-status-warning-fg border-status-warning-border\" },\n draft: { label: \"Draft\", cls: \"bg-background text-foreground/80 border-border\" },\n awaiting: { label: \"SENT\", cls: \"bg-status-info-bg text-status-info-fg border-status-info-border\" },\n viewing: { label: \"Viewing\", cls: \"bg-muted text-muted-foreground border-border\" },\n}\n\nconst STATUS_DOT: Record<ConvStatus, string> = {\n responded: \"bg-status-warning-fg\",\n draft: \"bg-status-pending-fg\",\n awaiting: \"bg-status-info-fg\",\n viewing: \"bg-muted-foreground/50\",\n}\n\nconst THREAD_ROW_ACCENT: Record<ConvStatus, string> = {\n responded: \"border-l-4 border-l-status-warning-border bg-status-warning-bg/25\",\n awaiting: \"border-l-4 border-l-status-info-border bg-status-info-bg/25\",\n draft: \"border-l-4 border-l-status-pending-border bg-status-pending-bg/20\",\n viewing: \"\",\n}\n\nconst RECEIPT_CHIP: Record<NonNullable<ConvMessage[\"receipt\"]>[\"kind\"], string> = {\n new: \"border-status-warning-border bg-status-warning-bg text-status-warning-fg\",\n read: \"border-status-info-border bg-status-info-bg text-status-info-fg\",\n opened: \"border-status-info-border bg-status-info-bg text-status-info-fg\",\n sent: \"border-status-info-border bg-status-info-bg text-status-info-fg\",\n draft: \"border-status-pending-border bg-status-pending-bg text-status-pending-fg\",\n}\n\nfunction effectiveStatus(t: ConversationThread): ConvStatus {\n return t.canReply === false ? \"viewing\" : t.status\n}\n\nfunction disabledOpenInGmailReason(thread: ConversationThread): string {\n return (\n thread.openInGmailDisabledReason?.trim() ||\n thread.replyDisabledReason?.trim() ||\n \"Gmail access is not available for this thread.\"\n )\n}\n\nfunction canOpenInGmail(thread: ConversationThread, onOpenInGmail?: (threadId: string) => void): boolean {\n return thread.openInGmailDisabled !== true && Boolean(thread.openInGmailUrl || onOpenInGmail)\n}\n\nfunction OpenInGmailButton({\n thread,\n onOpenInGmail,\n label = \"Open in Gmail\",\n}: {\n thread: ConversationThread\n onOpenInGmail?: (threadId: string) => void\n label?: string\n}) {\n const hasConfiguredAction = Boolean(thread.openInGmailUrl || onOpenInGmail || thread.openInGmailDisabled || thread.openInGmailDisabledReason)\n if (!hasConfiguredAction) return null\n\n const disabled = !canOpenInGmail(thread, onOpenInGmail)\n const disabledReason = disabled\n ? disabledOpenInGmailReason(thread)\n : undefined\n\n if (!disabled && thread.openInGmailUrl) {\n return (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" asChild>\n <a href={thread.openInGmailUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n <GmailMark size={14} /> {label}\n </a>\n </Button>\n )\n }\n\n return (\n <span className=\"inline-flex\" title={disabledReason}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={disabled}\n aria-disabled={disabled || undefined}\n aria-label={disabledReason ? `${label}: ${disabledReason}` : label}\n onClick={disabled ? undefined : () => onOpenInGmail?.(thread.threadId)}\n >\n <GmailMark size={14} /> {label}\n </Button>\n </span>\n )\n}\n\n/* ── One message (collapsible) ──────────────────────────────────────────── */\n\nfunction MessageView({\n message,\n expanded,\n onToggle,\n me,\n}: {\n message: ConvMessage\n expanded: boolean\n onToggle: () => void\n me?: ConvParticipant\n}) {\n const [quoteOpen, setQuoteOpen] = React.useState(false)\n const fromDisplay = displayParticipant(message.from)\n const toDisplay = displayParticipant(message.to)\n\n if (!expanded) {\n const snippet = messageBodySnippet(message, 140)\n\n return (\n <button\n type=\"button\"\n data-slot=\"conv-message-collapsed\"\n onClick={onToggle}\n className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left hover:bg-muted/40\"\n >\n <PersonAvatar person={message.from} />\n <span className=\"text-muted-foreground min-w-0 flex-1 truncate text-[13px]\">\n <b className=\"text-foreground\">{firstName(fromDisplay.name)}</b> · {snippet}\n </span>\n <span className=\"text-muted-foreground/60 shrink-0 text-xs\">{message.ago ?? message.date}</span>\n <ChevronDown size={13} className=\"text-muted-foreground shrink-0\" />\n </button>\n )\n }\n\n const meDisplay = me ? displayParticipant(me) : null\n const toLabel = meDisplay && sameEmail(toDisplay.email, meDisplay.email) ? \"me\" : firstName(toDisplay.name)\n\n return (\n <div data-slot=\"conv-message\" className=\"rounded-md border border-border bg-background\">\n <button\n type=\"button\"\n onClick={onToggle}\n className=\"flex w-full items-start gap-2 px-3 py-2 text-left\"\n >\n <PersonAvatar person={message.from} size=\"default\" />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex flex-wrap items-baseline gap-x-1.5\">\n <span className=\"text-[13px] font-semibold\">{fromDisplay.name}</span>\n {fromDisplay.email ? (\n <span className=\"text-muted-foreground/60 truncate text-xs\">&lt;{fromDisplay.email}&gt;</span>\n ) : null}\n </span>\n <span className=\"text-muted-foreground block text-xs\">\n to <b>{toLabel}</b>\n </span>\n </span>\n <span className=\"flex shrink-0 items-center gap-2\">\n {message.receipt ? (\n <span className={cn(\"inline-flex items-center gap-1 rounded-md border px-1.5 py-px text-[10px] font-semibold leading-4\", RECEIPT_CHIP[message.receipt.kind])}>\n {message.receipt.kind === \"new\" ? (\n <CornerUpLeft size={11} />\n ) : message.receipt.kind === \"read\" || message.receipt.kind === \"sent\" ? (\n <CheckCheck size={11} />\n ) : message.receipt.kind === \"draft\" ? (\n <FilePenLine size={11} />\n ) : (\n <MailOpen size={11} />\n )}\n {message.receipt.label}\n </span>\n ) : null}\n <span className=\"text-muted-foreground/60 text-xs\">{message.date}</span>\n <ChevronUp size={13} className=\"text-muted-foreground\" />\n </span>\n </button>\n\n <div className=\"px-3 pb-2.5\">\n <div data-slot=\"conv-message-body\">\n <EmailBody\n html={message.bodyHtml}\n text={message.body}\n detailsHtml={message.signatureHtml ?? undefined}\n variant=\"history\"\n collapseDetails={true}\n className=\"text-sm\"\n />\n </div>\n\n {message.quoted ? (\n <div className=\"mt-2\">\n <button\n type=\"button\"\n onClick={() => setQuoteOpen((v) => !v)}\n className=\"text-muted-foreground hover:bg-muted rounded px-1.5 text-xs leading-5\"\n title={quoteOpen ? \"Hide quoted text\" : \"Show quoted text\"}\n >\n •••\n </button>\n {quoteOpen ? (\n <div className=\"border-border text-muted-foreground mt-1 border-l-2 pl-3 text-[13px]\">\n <p className=\"mb-1\">{decodeEmailDisplayText(message.quoted.attr)}</p>\n <div data-slot=\"conv-quoted-body\">\n <EmailBody html={message.quoted.html} variant=\"history\" collapseDetails={false} />\n </div>\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n\n/* ── Reply composer ─────────────────────────────────────────────────────── */\n\ntype PreviewState = {\n loading: boolean\n /** Sanitized HTML ready to render. */\n html: string | null\n /** Confirmation token returned by the server preview, retained for reuse on send. */\n confirmationToken: string | null\n error: string | null\n /** True when `html` came from the local fallback rather than the server. */\n local: boolean\n}\n\nconst IDLE_PREVIEW: PreviewState = { loading: false, html: null, confirmationToken: null, error: null, local: false }\n\nfunction ReplyComposer({\n thread,\n me,\n replyAll,\n tenantName,\n onClose,\n onSend,\n onDraft,\n onPreviewReply,\n draftDisabledReason,\n}: {\n thread: ConversationThread\n me?: ConvParticipant\n replyAll: boolean\n tenantName?: string\n onClose: () => void\n onSend: (body: string, includeSignature: boolean) => void | Promise<void>\n onDraft: (body: string, includeSignature: boolean) => void | Promise<void>\n onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>\n draftDisabledReason?: string | null\n}) {\n const [body, setBody] = React.useState(thread.draft ?? \"\")\n const [sig, setSig] = React.useState(true)\n const [preview, setPreview] = React.useState(false)\n const [previewState, setPreviewState] = React.useState<PreviewState>(IDLE_PREVIEW)\n const [sending, setSending] = React.useState(false)\n const [sendError, setSendError] = React.useState<string | null>(null)\n const ccList = replyAll ? thread.cc ?? [] : []\n const subject = /^re:/i.test(thread.subject) ? thread.subject : `Re: ${thread.subject}`\n const draftDisabled = Boolean(draftDisabledReason)\n\n const localPreviewHtml = textToHtml(body) + (sig && thread.signature ? textToHtml(thread.signature) : \"\")\n\n const openPreview = async () => {\n setPreview(true)\n setSendError(null)\n\n if (!onPreviewReply) {\n // No server preview contract: render a sanitized local draft preview only.\n setPreviewState({ loading: false, html: localPreviewHtml, confirmationToken: null, error: null, local: true })\n return\n }\n\n setPreviewState({ loading: true, html: null, confirmationToken: null, error: null, local: false })\n try {\n const result = await onPreviewReply({ threadId: thread.threadId, body, includeSignature: sig, replyAll })\n setPreviewState({\n loading: false,\n html: result.htmlBody ?? \"\",\n confirmationToken: result.confirmationToken ?? null,\n error: null,\n local: false,\n })\n } catch (error) {\n setPreviewState({\n loading: false,\n html: null,\n confirmationToken: null,\n error: error instanceof Error ? error.message : \"Could not load the preview. Please try again.\",\n local: false,\n })\n }\n }\n\n const handleSend = async () => {\n setSending(true)\n setSendError(null)\n try {\n await onSend(body, sig)\n setPreview(false)\n setPreviewState(IDLE_PREVIEW)\n } catch (error) {\n setSendError(error instanceof Error ? error.message : \"Could not send this reply. Please try again.\")\n } finally {\n setSending(false)\n }\n }\n\n const handleDraft = async () => {\n if (draftDisabled) return\n setSending(true)\n setSendError(null)\n try {\n await onDraft(body, sig)\n setPreview(false)\n setPreviewState(IDLE_PREVIEW)\n } catch (error) {\n setSendError(error instanceof Error ? error.message : \"Could not create the Gmail draft. Please try again.\")\n } finally {\n setSending(false)\n }\n }\n\n return (\n <div data-slot=\"conv-reply\" className=\"border-border bg-muted/20 rounded-md border p-3\">\n <div className=\"mb-2 flex items-center gap-2\">\n {me ? <PersonAvatar person={me} /> : null}\n <span className=\"flex-1 text-[13px] font-medium\">\n {replyAll ? (\n <>\n Reply all{\" \"}\n <span className=\"text-muted-foreground font-normal\">· {1 + ccList.length} recipients</span>\n </>\n ) : (\n <>\n Reply to <b>{firstName(displayParticipant(thread.contact).name)}</b>\n </>\n )}\n </span>\n <span className=\"text-muted-foreground inline-flex items-center gap-1 text-[11px]\">\n <GitMerge size={11} /> Same thread\n </span>\n <button type=\"button\" onClick={onClose} title=\"Discard reply\" className=\"text-muted-foreground hover:text-foreground\">\n <X size={15} />\n </button>\n </div>\n\n <div className=\"border-border mb-2 space-y-1 border-b pb-2 text-[13px]\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">To</span>\n <span className=\"font-medium\">{displayParticipant(thread.contact).name}</span>\n <span className=\"text-muted-foreground/60 truncate text-xs\">{displayParticipant(thread.contact).email ?? thread.contact.email}</span>\n </div>\n {replyAll && ccList.length ? (\n <div className=\"flex items-start gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">Cc</span>\n <span className=\"text-muted-foreground text-xs\">\n {formatAddressList(ccList.map((c) => `${displayParticipant(c).name} <${displayParticipant(c).email ?? c.email}>`))}\n </span>\n </div>\n ) : null}\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">Subject</span>\n <span className=\"truncate\">{subject}</span>\n <span className=\"text-muted-foreground/60 ml-auto inline-flex items-center gap-1 text-[11px]\">\n <Lock size={10} /> on thread\n </span>\n </div>\n </div>\n\n <Textarea\n value={body}\n onChange={(e) => setBody(e.target.value)}\n placeholder=\"Write your reply…\"\n className=\"min-h-28 resize-y text-sm\"\n onKeyDown={(e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === \"Enter\") {\n e.preventDefault()\n void openPreview()\n }\n }}\n />\n\n {sig && thread.signature ? (\n <div className=\"text-muted-foreground mt-2 whitespace-pre-line border-l-2 border-border pl-3 text-[13px]\">\n <span className=\"text-muted-foreground/60 mr-1\">--</span>\n {thread.signature}\n </div>\n ) : null}\n\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <RichTextToolbar />\n <label className=\"text-muted-foreground ml-auto inline-flex cursor-pointer items-center gap-1.5 text-[12px]\">\n <Switch checked={sig} onCheckedChange={setSig} aria-label=\"Toggle signature\" />\n Signature\n </label>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={sending} onClick={() => void openPreview()}>\n <Eye size={14} /> Preview\n </Button>\n <Button type=\"button\" size=\"sm\" disabled={sending} onClick={() => void openPreview()}>\n <Send size={14} /> Send\n </Button>\n </div>\n\n <Dialog open={preview} onOpenChange={(open) => { if (!sending) { setPreview(open); if (!open) setPreviewState(IDLE_PREVIEW) } }}>\n <DialogContent className=\"max-w-xl\">\n <DialogHeader>\n <DialogTitle className=\"flex items-center gap-1.5 text-[15px]\">\n <Eye size={15} /> {previewState.local ? \"Local draft preview\" : \"Reply preview\"}\n </DialogTitle>\n <DialogDescription>\n {previewState.local\n ? \"Local draft preview only — the server prepares the exact message on send.\"\n : <>Stays on the {subject.replace(/^Re:\\s*/i, \"\")} thread. Gmail keeps it threaded.</>}\n </DialogDescription>\n </DialogHeader>\n <div className=\"border-border space-y-1 rounded-md border p-3 text-[13px]\">\n <div>\n <span className=\"text-muted-foreground\">To </span>\n <b>{displayParticipant(thread.contact).name}</b>{\" \"}\n <span className=\"text-muted-foreground/60\">&lt;{displayParticipant(thread.contact).email ?? thread.contact.email}&gt;</span>\n </div>\n {replyAll && ccList.length ? (\n <div className=\"text-muted-foreground\">Cc {formatAddressList(ccList.map((c) => `${displayParticipant(c).name} <${displayParticipant(c).email ?? c.email}>`))}</div>\n ) : null}\n <div>\n <span className=\"text-muted-foreground\">Subject </span>\n {subject}\n </div>\n </div>\n {previewState.loading ? (\n <div data-slot=\"conv-preview-loading\" role=\"status\" className=\"text-muted-foreground flex items-center gap-2 px-1 py-6 text-[13px]\">\n <span className=\"border-muted-foreground/40 border-t-foreground size-4 animate-spin rounded-full border-2\" aria-hidden />\n Loading preview…\n </div>\n ) : previewState.error ? (\n <p role=\"alert\" className=\"text-destructive text-sm\">\n {previewState.error}\n </p>\n ) : (\n <div\n data-slot=\"conv-preview-body\"\n data-confirmation-token={previewState.confirmationToken ?? undefined}\n className=\"max-h-72 overflow-auto\"\n >\n <EmailBody html={previewState.html ?? \"\"} variant=\"preview\" collapseDetails={false} defaultDetailsOpen />\n </div>\n )}\n {sendError ? (\n <p role=\"alert\" className=\"text-destructive text-sm\">\n {sendError}\n </p>\n ) : null}\n <DialogFooter className=\"sm:justify-between\">\n <span className=\"inline-flex\" title={draftDisabledReason ?? undefined}>\n <button\n type=\"button\"\n disabled={sending || previewState.loading || draftDisabled}\n onClick={handleDraft}\n aria-label={draftDisabledReason ? `Open draft in Gmail: ${draftDisabledReason}` : \"Open draft in Gmail\"}\n className=\"text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-[13px] disabled:pointer-events-none disabled:opacity-50\"\n >\n <GmailMark size={14} /> Open draft in Gmail\n </button>\n </span>\n <span className=\"flex items-center gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={sending} onClick={() => { setPreview(false); setPreviewState(IDLE_PREVIEW) }}>\n Keep editing\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n disabled={sending || previewState.loading}\n onClick={handleSend}\n >\n <Send size={14} /> {sending ? \"Sending...\" : \"Send now\"}\n </Button>\n </span>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n {tenantName ? (\n <p className=\"text-muted-foreground/70 mt-2 text-[11px]\">Sends via Gmail · playbooks stay stopped.</p>\n ) : null}\n </div>\n )\n}\n\n/* ── Thread body (messages + footer/composer/done states) ───────────────── */\n\ntype ThreadMode = \"idle\" | \"replying\" | \"sent\" | \"draft\"\n\nfunction ThreadBody({\n thread,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onPreviewReply,\n onOpenInGmail,\n}: {\n thread: ConversationThread\n me?: ConvParticipant\n tenantName?: string\n onSendReply?: (p: ConversationReplyPayload) => void | Promise<void>\n onCreateGmailDraft?: (p: ConversationReplyPayload) => void | Promise<void>\n onPreviewReply?: (p: ConversationReplyPayload) => Promise<ConversationReplyPreview>\n onOpenInGmail?: (threadId: string) => void\n}) {\n const canReply = thread.canReply !== false\n const replyDisabledReason = thread.replyDisabledReason?.trim() || \"You are not a participant on this thread, so replying is disabled here.\"\n const draftDisabledReason = onCreateGmailDraft ? null : \"Gmail draft creation is not available for this thread.\"\n const hasCc = !!(thread.cc && thread.cc.length)\n const sortedMessages = React.useMemo(() => sortMessagesChronologically(thread.messages), [thread.messages])\n const [mode, setMode] = React.useState<ThreadMode>(\"idle\")\n const [replyAll, setReplyAll] = React.useState(false)\n const [expanded, setExpanded] = React.useState<Record<string, boolean>>(() => {\n const o: Record<string, boolean> = {}\n sortedMessages.forEach((m, i) => {\n o[m.id] = i === sortedMessages.length - 1\n })\n return o\n })\n\n React.useEffect(() => {\n setExpanded((current) => {\n const next = { ...current }\n sortedMessages.forEach((m, i) => {\n if (next[m.id] === undefined) next[m.id] = i === sortedMessages.length - 1\n })\n return next\n })\n }, [sortedMessages])\n\n const toggle = (id: string) => setExpanded((e) => ({ ...e, [id]: !e[id] }))\n\n return (\n <div data-slot=\"conv-thread-body\" className=\"space-y-2\">\n {canReply && thread.paused ? (\n <div className=\"border-status-warning-border bg-status-warning-bg text-status-warning-fg flex items-start gap-2 rounded-md border border-l-4 p-2.5 text-[12px]\">\n <Pause size={13} className=\"mt-0.5 shrink-0\" />\n <span>\n <b>Playbook stopped.</b> Follow-up actions for {thread.paused.playbook} won’t send\n automatically while this conversation is live. Continue it in {tenantName ?? \"the app\"} or Gmail.\n </span>\n </div>\n ) : null}\n\n <div className=\"space-y-1\">\n {sortedMessages.map((m) => (\n <MessageView key={m.id} message={m} expanded={!!expanded[m.id]} onToggle={() => toggle(m.id)} me={me} />\n ))}\n </div>\n\n {!canReply ? (\n <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]\">\n <Eye size={14} className=\"mt-0.5 shrink-0\" />\n <span className=\"min-w-0 flex-1\">\n <b>Viewing only.</b> {replyDisabledReason}\n </span>\n <OpenInGmailButton thread={thread} onOpenInGmail={onOpenInGmail} />\n </div>\n ) : null}\n\n {canReply && mode === \"idle\" ? (\n <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\">\n <Button type=\"button\" size=\"sm\" onClick={() => { setReplyAll(false); setMode(\"replying\") }}>\n <Reply size={15} /> Reply\n </Button>\n {hasCc ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => { setReplyAll(true); setMode(\"replying\") }}>\n <ReplyAll size={14} /> Reply all\n </Button>\n ) : null}\n <OpenInGmailButton thread={thread} onOpenInGmail={onOpenInGmail} />\n <span className=\"text-muted-foreground/70 ml-auto inline-flex items-center gap-1.5 text-[12px]\">\n <GitMerge size={13} /> Stays on this thread\n </span>\n </div>\n ) : null}\n\n {canReply && mode === \"replying\" ? (\n <ReplyComposer\n thread={thread}\n me={me}\n replyAll={replyAll}\n tenantName={tenantName}\n onPreviewReply={onPreviewReply}\n onClose={() => setMode(\"idle\")}\n onSend={async (body, includeSignature) => {\n await onSendReply?.({ threadId: thread.threadId, body, includeSignature, replyAll })\n setMode(\"sent\")\n }}\n onDraft={async (body, includeSignature) => {\n if (!onCreateGmailDraft) return\n await onCreateGmailDraft({ threadId: thread.threadId, body, includeSignature, replyAll })\n setMode(\"draft\")\n }}\n draftDisabledReason={draftDisabledReason}\n />\n ) : null}\n\n {canReply && mode === \"sent\" ? (\n <div className=\"border-status-active-border bg-status-active-bg flex items-center gap-2 rounded-md border p-3 text-[13px]\">\n <Check size={16} className=\"text-status-active-fg shrink-0\" />\n <span className=\"flex-1\">\n <b>{replyAll ? \"Reply all sent\" : \"Reply sent\"}</b> · added to the thread. Delivered to{\" \"}\n <b>{displayParticipant(thread.contact).name}</b>. This action stays <b>Pending</b>; playbooks remain stopped.\n </span>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setMode(\"idle\")}>\n Done\n </Button>\n </div>\n ) : null}\n\n {canReply && mode === \"draft\" ? (\n <div className=\"border-border bg-muted/30 flex items-center gap-2 rounded-md border p-3 text-[13px]\">\n <GmailMark size={16} />\n <span className=\"flex-1\">\n <b>Draft saved to Gmail.</b> Waiting on the <b>Re: {thread.subject}</b> thread; open it there to finish. Nothing was sent.\n </span>\n <OpenInGmailButton thread={thread} onOpenInGmail={onOpenInGmail} />\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setMode(\"idle\")}>\n Done\n </Button>\n </div>\n ) : null}\n </div>\n )\n}\n\n/* ── A thread row + its inline reader ───────────────────────────────────── */\n\nfunction ThreadRow({\n thread,\n open,\n onToggleOpen,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onPreviewReply,\n onOpenInGmail,\n}: {\n thread: ConversationThread\n open: boolean\n onToggleOpen: () => void\n} & Pick<ConversationPanelProps, \"me\" | \"tenantName\" | \"onSendReply\" | \"onCreateGmailDraft\" | \"onPreviewReply\" | \"onOpenInGmail\">) {\n const status = effectiveStatus(thread)\n const sortedMessages = React.useMemo(() => sortMessagesChronologically(thread.messages), [thread.messages])\n const last = sortedMessages[sortedMessages.length - 1]\n const lastSender = last ? displayParticipant(last.from) : null\n const meDisplay = me ? displayParticipant(me) : null\n const who = last?.direction === \"outbound\" && sameEmail(lastSender?.email, meDisplay?.email) ? \"You\" : firstName(lastSender?.name ?? \"\")\n const lastSnippet = last ? messageBodySnippet(last, 120) : \"\"\n const pill = STATUS_PILL[status]\n\n return (\n <div\n data-slot=\"conv-thread\"\n data-status={status}\n data-open={open ? \"true\" : undefined}\n className={cn(\"border-border border-b last:border-b-0\", THREAD_ROW_ACCENT[status])}\n >\n <button\n type=\"button\"\n onClick={onToggleOpen}\n aria-expanded={open}\n className=\"flex w-full items-center gap-2.5 px-3 py-2.5 text-left hover:bg-muted/30\"\n >\n <span className={cn(\"size-2 shrink-0 rounded-full\", STATUS_DOT[status])} aria-hidden />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex items-center gap-2\">\n <span className=\"truncate text-sm font-semibold\">{thread.subject}</span>\n <span className={cn(\"shrink-0 rounded-md border px-1.5 py-px text-[10px] font-medium leading-4\", pill.cls)}>\n {pill.label}\n </span>\n </span>\n <span className=\"text-muted-foreground block truncate text-xs\">\n <b className=\"text-foreground/80\">{displayParticipant(thread.contact).name}</b> · {who}: {lastSnippet}\n </span>\n </span>\n <span className=\"text-muted-foreground/60 shrink-0 text-xs\">{thread.lastWhen}</span>\n {open ? (\n <ChevronUp size={15} className=\"text-muted-foreground shrink-0\" />\n ) : (\n <ChevronDown size={15} className=\"text-muted-foreground shrink-0\" />\n )}\n </button>\n\n {open ? (\n <div className=\"px-3 pb-3\">\n <ThreadBody\n thread={thread}\n me={me}\n tenantName={tenantName}\n onSendReply={onSendReply}\n onCreateGmailDraft={onCreateGmailDraft}\n onPreviewReply={onPreviewReply}\n onOpenInGmail={onOpenInGmail}\n />\n </div>\n ) : null}\n </div>\n )\n}\n\n/* ── The hub ─────────────────────────────────────────────────────────────── */\n\nfunction ConversationPanel({\n threads,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onPreviewReply,\n onOpenInGmail,\n defaultOpenThreadId,\n className,\n}: ConversationPanelProps) {\n const responded = threads.filter((t) => t.status === \"responded\" && t.canReply !== false).length\n const draft = threads.filter((t) => effectiveStatus(t) === \"draft\").length\n const awaiting = threads.filter((t) => effectiveStatus(t) === \"awaiting\").length\n const anyPaused = threads.some((t) => t.paused)\n const prioritizedThread =\n threads.find((t) => t.status === \"responded\" && t.canReply !== false) ??\n threads.find((t) => effectiveStatus(t) === \"draft\") ??\n threads.find((t) => effectiveStatus(t) === \"awaiting\")\n const hubGmailThread =\n threads.find((t) => t.status === \"responded\" && t.canReply !== false && canOpenInGmail(t, onOpenInGmail)) ??\n threads.find((t) => effectiveStatus(t) === \"draft\" && canOpenInGmail(t, onOpenInGmail)) ??\n threads.find((t) => effectiveStatus(t) === \"awaiting\" && canOpenInGmail(t, onOpenInGmail)) ??\n threads.find((t) => canOpenInGmail(t, onOpenInGmail))\n const firstAwaiting = threads.find((t) => effectiveStatus(t) === \"awaiting\")\n\n const [hubOpen, setHubOpen] = React.useState(true)\n const [openId, setOpenId] = React.useState<string | null>(() => {\n if (defaultOpenThreadId) return defaultOpenThreadId\n return prioritizedThread ? prioritizedThread.threadId : null\n })\n\n if (!threads.length) return null\n\n // Header badge state: a responded reply leads, then drafts to finish, then sent mail awaiting a reply.\n const badge =\n responded > 0\n ? { label: \"Email response detected\", dot: \"bg-status-warning-fg\", ring: \"bg-status-warning-fg/30\" }\n : draft > 0\n ? { label: \"Draft ready\", dot: \"bg-status-pending-fg\", ring: \"bg-status-pending-fg/30\" }\n : awaiting > 0\n ? { label: \"Email sent · awaiting reply\", dot: \"bg-status-info-fg\", ring: \"bg-status-info-fg/30\" }\n : { label: \"Conversations\", dot: \"bg-muted-foreground/50\", ring: \"bg-muted-foreground/20\" }\n\n const headTitle =\n responded > 0\n ? `${responded} ${responded === 1 ? \"reply needs\" : \"replies need\"} your response`\n : draft > 0\n ? `Draft ready on ${draft} ${draft === 1 ? \"thread\" : \"threads\"}`\n : awaiting > 0\n ? awaiting === 1 && firstAwaiting\n ? `Awaiting a response from ${firstName(displayParticipant(firstAwaiting.contact).name)}`\n : `Awaiting responses on ${awaiting} threads`\n : `${threads.length} email ${threads.length === 1 ? \"thread\" : \"threads\"}`\n\n const panelState = responded > 0 ? \"responded\" : draft > 0 ? \"draft\" : awaiting > 0 ? \"awaiting\" : \"viewing\"\n\n return (\n <section\n data-slot=\"conversation-panel\"\n data-responded={responded > 0 ? \"true\" : undefined}\n data-state={panelState}\n className={cn(\n \"bg-background overflow-hidden rounded-xl border\",\n panelState === \"responded\"\n ? \"border-status-warning-border\"\n : panelState === \"awaiting\"\n ? \"border-status-info-border\"\n : \"border-border\",\n className,\n )}\n >\n <div\n data-slot=\"conversation-panel-header\"\n className={cn(\n \"flex w-full items-center gap-2 px-3 py-2.5\",\n panelState === \"responded\"\n ? \"bg-status-warning-bg/45\"\n : panelState === \"awaiting\"\n ? \"bg-status-info-bg/55\"\n : \"bg-background\",\n )}\n >\n <button\n type=\"button\"\n onClick={() => setHubOpen((v) => !v)}\n aria-expanded={hubOpen}\n className=\"flex min-w-0 flex-1 items-center gap-3 text-left\"\n >\n <span\n data-slot=\"conversation-badge\"\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-semibold\",\n responded > 0\n ? \"bg-status-warning-bg text-status-warning-fg\"\n : draft > 0\n ? \"bg-status-pending-bg text-status-pending-fg\"\n : awaiting > 0\n ? \"bg-status-info-bg text-status-info-fg\"\n : \"bg-muted text-muted-foreground\"\n )}\n >\n <span className=\"relative inline-flex size-2\">\n <span className={cn(\"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75\", badge.ring)} />\n <span className={cn(\"relative inline-flex size-2 rounded-full\", badge.dot)} />\n </span>\n {badge.label}\n </span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-semibold leading-tight\">{headTitle}</span>\n <span className=\"text-muted-foreground mt-0.5 block truncate text-xs\">\n {threads.length} {threads.length === 1 ? \"thread\" : \"threads\"} on this action\n {anyPaused ? <> · <b>playbook stopped</b></> : null}\n </span>\n </span>\n {hubOpen ? (\n <ChevronUp size={16} className=\"text-muted-foreground shrink-0\" />\n ) : (\n <ChevronDown size={16} className=\"text-muted-foreground shrink-0\" />\n )}\n </button>\n {hubGmailThread ? (\n <div className=\"shrink-0\" onClick={(event) => event.stopPropagation()}>\n <OpenInGmailButton thread={hubGmailThread} onOpenInGmail={onOpenInGmail} />\n </div>\n ) : null}\n </div>\n\n {hubOpen ? (\n <div className=\"border-border border-t\">\n {threads.map((t) => (\n <ThreadRow\n key={t.threadId}\n thread={t}\n open={openId === t.threadId}\n onToggleOpen={() => setOpenId((cur) => (cur === t.threadId ? null : t.threadId))}\n me={me}\n tenantName={tenantName}\n onSendReply={onSendReply}\n onCreateGmailDraft={onCreateGmailDraft}\n onPreviewReply={onPreviewReply}\n onOpenInGmail={onOpenInGmail}\n />\n ))}\n </div>\n ) : null}\n </section>\n )\n}\n\nexport { ConversationPanel }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA6RI,SAsVQ,UAtVR,KAcA,YAdA;AAtQJ,YAAY,WAAW;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,mBAAmB;AAC5B,SAAS,QAAQ,gBAAgB,mBAAmB;AACpD,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,uBAAuB;AAChC,SAAS,iBAAiB;AAC1B,SAAS,wBAAwB,kBAAkB,mBAAmB,4BAA4B;AAClG;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAgIP,SAAS,WAAW,GAAmB;AACrC,SAAO,EAAE,QAAQ,MAAM,OAAO,EAAE,QAAQ,MAAM,MAAM,EAAE,QAAQ,MAAM,MAAM;AAC5E;AAGA,SAAS,WAAW,MAAsB;AACxC,SAAO,uBAAuB,IAAI,EAC/B,MAAM,QAAQ,EACd,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO,EACd,IAAI,CAAC,MAAM,MAAM,WAAW,CAAC,EAAE,QAAQ,OAAO,MAAM,CAAC,MAAM,EAC3D,KAAK,EAAE;AACZ;AAEA,SAAS,mBAAmB,QAAyB;AACnD,SAAO,qBAAqB,EAAE,MAAM,OAAO,MAAM,OAAO,OAAO,OAAO,cAAc,OAAO,SAAS,OAAO,KAAK,CAAC;AACnH;AAEA,SAAS,UAAU,MAAsB;AACvC,SAAO,uBAAuB,IAAI,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,uBAAuB,IAAI;AAClF;AAEA,SAAS,UAAU,GAAmB,GAA4B;AAChE,SAAO,QAAQ,KAAK,KAAK,EAAE,KAAK,EAAE,YAAY,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC;AAC5E;AAEA,SAAS,mBAAmB,SAAiD,YAAY,KAAa;AACpG,SAAO,iBAAiB,EAAE,UAAU,QAAQ,UAAU,MAAM,QAAQ,KAAK,GAAG,SAAS;AACvF;AAKA,SAAS,2BAA2B,OAA6C;AAC/E,MAAI,SAAS,QAAQ,UAAU,GAAI,QAAO;AAE1C,MAAI,iBAAiB,MAAM;AACzB,UAAM,OAAO,MAAM,QAAQ;AAC3B,WAAO,OAAO,MAAM,IAAI,IAAI,OAAO;AAAA,EACrC;AAEA,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACpC,WAAO,QAAQ,OAAiB,QAAQ,MAAO;AAAA,EACjD;AAEA,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,QAAO;AAErB,MAAI,QAAQ,KAAK,OAAO,GAAG;AACzB,UAAM,UAAU,OAAO,OAAO;AAC9B,QAAI,CAAC,OAAO,SAAS,OAAO,EAAG,QAAO;AACtC,WAAO,UAAU,OAAiB,UAAU,MAAO;AAAA,EACrD;AAEA,QAAM,SAAS,KAAK,MAAM,OAAO;AACjC,SAAO,OAAO,MAAM,MAAM,IAAI,OAAO;AACvC;AAEA,SAAS,iBAAiB,SAAqC;AAC7D,QAAM,cAAc,QAAQ,cAAc,aACtC,CAAC,QAAQ,QAAQ,QAAQ,OAAO,IAChC,CAAC,QAAQ,YAAY,QAAQ,WAAW;AAE5C,QAAM,aAAsC;AAAA,IAC1C,GAAG;AAAA,IACH,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,EACV;AAEA,aAAW,aAAa,YAAY;AAClC,UAAM,SAAS,2BAA2B,SAAS;AACnD,QAAI,WAAW,KAAM,QAAO;AAAA,EAC9B;AAEA,SAAO;AACT;AAEA,SAAS,4BAA4B,UAAwC;AAC3E,SAAO,SACJ,IAAI,CAAC,SAAS,WAAW,EAAE,SAAS,OAAO,WAAW,iBAAiB,OAAO,EAAE,EAAE,EAClF,KAAK,CAAC,GAAG,MAAM;AACd,QAAI,EAAE,cAAc,QAAQ,EAAE,cAAc,QAAQ,EAAE,cAAc,EAAE,WAAW;AAC/E,aAAO,EAAE,YAAY,EAAE;AAAA,IACzB;AACA,WAAO,EAAE,QAAQ,EAAE;AAAA,EACrB,CAAC,EACA,IAAI,CAAC,UAAU,MAAM,OAAO;AACjC;AAEA,SAAS,UAAU,EAAE,OAAO,GAAG,GAAsB;AACnD;AAAA;AAAA,IAEE;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,YAAY,MAAM;AAAA,QACvB,KAAI;AAAA,QACJ,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,OAAO,EAAE,OAAO,MAAM,QAAQ,MAAM,WAAW,WAAW,SAAS,QAAQ;AAAA;AAAA,IAC7E;AAAA;AAEJ;AAEA,SAAS,aAAa,EAAE,QAAQ,OAAO,KAAK,GAAyD;AAvSrG;AAwSE,QAAM,UAAU,mBAAmB,MAAM;AAEzC,SACE,qBAAC,UAAO,MACL;AAAA,WAAO,YAAY,oBAAC,eAAY,KAAK,OAAO,WAAW,KAAK,QAAQ,MAAM,IAAK;AAAA,IAChF,oBAAC,kBAAe,WAAU,oEACvB,sBAAY,EAAE,MAAM,QAAQ,MAAM,QAAO,aAAQ,UAAR,YAAiB,OAAO,MAAM,CAAC,GAC3E;AAAA,KACF;AAEJ;AAEA,MAAM,cAAkE;AAAA,EACtE,WAAW,EAAE,OAAO,aAAa,KAAK,2EAA2E;AAAA,EACjH,OAAO,EAAE,OAAO,SAAS,KAAK,iDAAiD;AAAA,EAC/E,UAAU,EAAE,OAAO,QAAQ,KAAK,kEAAkE;AAAA,EAClG,SAAS,EAAE,OAAO,WAAW,KAAK,+CAA+C;AACnF;AAEA,MAAM,aAAyC;AAAA,EAC7C,WAAW;AAAA,EACX,OAAO;AAAA,EACP,UAAU;AAAA,EACV,SAAS;AACX;AAEA,MAAM,oBAAgD;AAAA,EACpD,WAAW;AAAA,EACX,UAAU;AAAA,EACV,OAAO;AAAA,EACP,SAAS;AACX;AAEA,MAAM,eAA4E;AAAA,EAChF,KAAK;AAAA,EACL,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,OAAO;AACT;AAEA,SAAS,gBAAgB,GAAmC;AAC1D,SAAO,EAAE,aAAa,QAAQ,YAAY,EAAE;AAC9C;AAEA,SAAS,0BAA0B,QAAoC;AArVvE;AAsVE,WACE,YAAO,8BAAP,mBAAkC,aAClC,YAAO,wBAAP,mBAA4B,WAC5B;AAEJ;AAEA,SAAS,eAAe,QAA4B,eAAqD;AACvG,SAAO,OAAO,wBAAwB,QAAQ,QAAQ,OAAO,kBAAkB,aAAa;AAC9F;AAEA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAIG;AACD,QAAM,sBAAsB,QAAQ,OAAO,kBAAkB,iBAAiB,OAAO,uBAAuB,OAAO,yBAAyB;AAC5I,MAAI,CAAC,oBAAqB,QAAO;AAEjC,QAAM,WAAW,CAAC,eAAe,QAAQ,aAAa;AACtD,QAAM,iBAAiB,WACnB,0BAA0B,MAAM,IAChC;AAEJ,MAAI,CAAC,YAAY,OAAO,gBAAgB;AACtC,WACE,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAO,MACrD,+BAAC,OAAE,MAAM,OAAO,gBAAgB,QAAO,UAAS,KAAI,uBAClD;AAAA,0BAAC,aAAU,MAAM,IAAI;AAAA,MAAE;AAAA,MAAE;AAAA,OAC3B,GACF;AAAA,EAEJ;AAEA,SACE,oBAAC,UAAK,WAAU,eAAc,OAAO,gBACnC;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAQ;AAAA,MACR,MAAK;AAAA,MACL;AAAA,MACA,iBAAe,YAAY;AAAA,MAC3B,cAAY,iBAAiB,GAAG,KAAK,KAAK,cAAc,KAAK;AAAA,MAC7D,SAAS,WAAW,SAAY,MAAM,+CAAgB,OAAO;AAAA,MAE7D;AAAA,4BAAC,aAAU,MAAM,IAAI;AAAA,QAAE;AAAA,QAAE;AAAA;AAAA;AAAA,EAC3B,GACF;AAEJ;AAIA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AAzZH;AA0ZE,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,KAAK;AACtD,QAAM,cAAc,mBAAmB,QAAQ,IAAI;AACnD,QAAM,YAAY,mBAAmB,QAAQ,EAAE;AAE/C,MAAI,CAAC,UAAU;AACb,UAAM,UAAU,mBAAmB,SAAS,GAAG;AAE/C,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,aAAU;AAAA,QACV,SAAS;AAAA,QACT,WAAU;AAAA,QAEV;AAAA,8BAAC,gBAAa,QAAQ,QAAQ,MAAM;AAAA,UACpC,qBAAC,UAAK,WAAU,6DACd;AAAA,gCAAC,OAAE,WAAU,mBAAmB,oBAAU,YAAY,IAAI,GAAE;AAAA,YAAI;AAAA,YAAI;AAAA,aACtE;AAAA,UACA,oBAAC,UAAK,WAAU,6CAA6C,wBAAQ,QAAR,YAAe,QAAQ,MAAK;AAAA,UACzF,oBAAC,eAAY,MAAM,IAAI,WAAU,kCAAiC;AAAA;AAAA;AAAA,IACpE;AAAA,EAEJ;AAEA,QAAM,YAAY,KAAK,mBAAmB,EAAE,IAAI;AAChD,QAAM,UAAU,aAAa,UAAU,UAAU,OAAO,UAAU,KAAK,IAAI,OAAO,UAAU,UAAU,IAAI;AAE1G,SACE,qBAAC,SAAI,aAAU,gBAAe,WAAU,iDACtC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS;AAAA,QACT,WAAU;AAAA,QAEV;AAAA,8BAAC,gBAAa,QAAQ,QAAQ,MAAM,MAAK,WAAU;AAAA,UACnD,qBAAC,UAAK,WAAU,kBACd;AAAA,iCAAC,UAAK,WAAU,2CACd;AAAA,kCAAC,UAAK,WAAU,6BAA6B,sBAAY,MAAK;AAAA,cAC7D,YAAY,QACX,qBAAC,UAAK,WAAU,6CAA4C;AAAA;AAAA,gBAAK,YAAY;AAAA,gBAAM;AAAA,iBAAI,IACrF;AAAA,eACN;AAAA,YACA,qBAAC,UAAK,WAAU,uCAAsC;AAAA;AAAA,cACjD,oBAAC,OAAG,mBAAQ;AAAA,eACjB;AAAA,aACF;AAAA,UACA,qBAAC,UAAK,WAAU,oCACb;AAAA,oBAAQ,UACP,qBAAC,UAAK,WAAW,GAAG,qGAAqG,aAAa,QAAQ,QAAQ,IAAI,CAAC,GACxJ;AAAA,sBAAQ,QAAQ,SAAS,QACxB,oBAAC,gBAAa,MAAM,IAAI,IACtB,QAAQ,QAAQ,SAAS,UAAU,QAAQ,QAAQ,SAAS,SAC9D,oBAAC,cAAW,MAAM,IAAI,IACpB,QAAQ,QAAQ,SAAS,UAC3B,oBAAC,eAAY,MAAM,IAAI,IAEvB,oBAAC,YAAS,MAAM,IAAI;AAAA,cAErB,QAAQ,QAAQ;AAAA,eACnB,IACE;AAAA,YACJ,oBAAC,UAAK,WAAU,oCAAoC,kBAAQ,MAAK;AAAA,YACjE,oBAAC,aAAU,MAAM,IAAI,WAAU,yBAAwB;AAAA,aACzD;AAAA;AAAA;AAAA,IACF;AAAA,IAEA,qBAAC,SAAI,WAAU,eACb;AAAA,0BAAC,SAAI,aAAU,qBACb;AAAA,QAAC;AAAA;AAAA,UACC,MAAM,QAAQ;AAAA,UACd,MAAM,QAAQ;AAAA,UACd,cAAa,aAAQ,kBAAR,YAAyB;AAAA,UACtC,SAAQ;AAAA,UACR,iBAAiB;AAAA,UACjB,WAAU;AAAA;AAAA,MACZ,GACF;AAAA,MAEC,QAAQ,SACP,qBAAC,SAAI,WAAU,QACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC;AAAA,YACrC,WAAU;AAAA,YACV,OAAO,YAAY,qBAAqB;AAAA,YACzC;AAAA;AAAA,QAED;AAAA,QACC,YACC,qBAAC,SAAI,WAAU,wEACb;AAAA,8BAAC,OAAE,WAAU,QAAQ,iCAAuB,QAAQ,OAAO,IAAI,GAAE;AAAA,UACjE,oBAAC,SAAI,aAAU,oBACb,8BAAC,aAAU,MAAM,QAAQ,OAAO,MAAM,SAAQ,WAAU,iBAAiB,OAAO,GAClF;AAAA,WACF,IACE;AAAA,SACN,IACE;AAAA,OACN;AAAA,KACF;AAEJ;AAeA,MAAM,eAA6B,EAAE,SAAS,OAAO,MAAM,MAAM,mBAAmB,MAAM,OAAO,MAAM,OAAO,MAAM;AAEpH,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAUG;AApiBH;AAqiBE,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,UAAS,YAAO,UAAP,YAAgB,EAAE;AACzD,QAAM,CAAC,KAAK,MAAM,IAAI,MAAM,SAAS,IAAI;AACzC,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAuB,YAAY;AACjF,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAwB,IAAI;AACpE,QAAM,SAAS,YAAW,YAAO,OAAP,YAAa,CAAC,IAAI,CAAC;AAC7C,QAAM,UAAU,QAAQ,KAAK,OAAO,OAAO,IAAI,OAAO,UAAU,OAAO,OAAO,OAAO;AACrF,QAAM,gBAAgB,QAAQ,mBAAmB;AAEjD,QAAM,mBAAmB,WAAW,IAAI,KAAK,OAAO,OAAO,YAAY,WAAW,OAAO,SAAS,IAAI;AAEtG,QAAM,cAAc,YAAY;AAjjBlC,QAAAA,KAAAC;AAkjBI,eAAW,IAAI;AACf,iBAAa,IAAI;AAEjB,QAAI,CAAC,gBAAgB;AAEnB,sBAAgB,EAAE,SAAS,OAAO,MAAM,kBAAkB,mBAAmB,MAAM,OAAO,MAAM,OAAO,KAAK,CAAC;AAC7G;AAAA,IACF;AAEA,oBAAgB,EAAE,SAAS,MAAM,MAAM,MAAM,mBAAmB,MAAM,OAAO,MAAM,OAAO,MAAM,CAAC;AACjG,QAAI;AACF,YAAM,SAAS,MAAM,eAAe,EAAE,UAAU,OAAO,UAAU,MAAM,kBAAkB,KAAK,SAAS,CAAC;AACxG,sBAAgB;AAAA,QACd,SAAS;AAAA,QACT,OAAMD,MAAA,OAAO,aAAP,OAAAA,MAAmB;AAAA,QACzB,oBAAmBC,MAAA,OAAO,sBAAP,OAAAA,MAA4B;AAAA,QAC/C,OAAO;AAAA,QACP,OAAO;AAAA,MACT,CAAC;AAAA,IACH,SAAS,OAAO;AACd,sBAAgB;AAAA,QACd,SAAS;AAAA,QACT,MAAM;AAAA,QACN,mBAAmB;AAAA,QACnB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QAChD,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,aAAa,YAAY;AAC7B,eAAW,IAAI;AACf,iBAAa,IAAI;AACjB,QAAI;AACF,YAAM,OAAO,MAAM,GAAG;AACtB,iBAAW,KAAK;AAChB,sBAAgB,YAAY;AAAA,IAC9B,SAAS,OAAO;AACd,mBAAa,iBAAiB,QAAQ,MAAM,UAAU,8CAA8C;AAAA,IACtG,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF;AAEA,QAAM,cAAc,YAAY;AAC9B,QAAI,cAAe;AACnB,eAAW,IAAI;AACf,iBAAa,IAAI;AACjB,QAAI;AACF,YAAM,QAAQ,MAAM,GAAG;AACvB,iBAAW,KAAK;AAChB,sBAAgB,YAAY;AAAA,IAC9B,SAAS,OAAO;AACd,mBAAa,iBAAiB,QAAQ,MAAM,UAAU,qDAAqD;AAAA,IAC7G,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF;AAEA,SACE,qBAAC,SAAI,aAAU,cAAa,WAAU,mDACpC;AAAA,yBAAC,SAAI,WAAU,gCACZ;AAAA,WAAK,oBAAC,gBAAa,QAAQ,IAAI,IAAK;AAAA,MACrC,oBAAC,UAAK,WAAU,kCACb,qBACC,iCAAE;AAAA;AAAA,QACU;AAAA,QACV,qBAAC,UAAK,WAAU,qCAAoC;AAAA;AAAA,UAAG,IAAI,OAAO;AAAA,UAAO;AAAA,WAAW;AAAA,SACtF,IAEA,iCAAE;AAAA;AAAA,QACS,oBAAC,OAAG,oBAAU,mBAAmB,OAAO,OAAO,EAAE,IAAI,GAAE;AAAA,SAClE,GAEJ;AAAA,MACA,qBAAC,UAAK,WAAU,oEACd;AAAA,4BAAC,YAAS,MAAM,IAAI;AAAA,QAAE;AAAA,SACxB;AAAA,MACA,oBAAC,YAAO,MAAK,UAAS,SAAS,SAAS,OAAM,iBAAgB,WAAU,+CACtE,8BAAC,KAAE,MAAM,IAAI,GACf;AAAA,OACF;AAAA,IAEA,qBAAC,SAAI,WAAU,0DACb;AAAA,2BAAC,SAAI,WAAU,6BACb;AAAA,4BAAC,UAAK,WAAU,+DAA8D,gBAAE;AAAA,QAChF,oBAAC,UAAK,WAAU,eAAe,6BAAmB,OAAO,OAAO,EAAE,MAAK;AAAA,QACvE,oBAAC,UAAK,WAAU,6CAA6C,mCAAmB,OAAO,OAAO,EAAE,UAAnC,YAA4C,OAAO,QAAQ,OAAM;AAAA,SAChI;AAAA,MACC,YAAY,OAAO,SAClB,qBAAC,SAAI,WAAU,4BACb;AAAA,4BAAC,UAAK,WAAU,+DAA8D,gBAAE;AAAA,QAChF,oBAAC,UAAK,WAAU,iCACb,4BAAkB,OAAO,IAAI,CAAC,MAAG;AA/oBhD,cAAAD;AA+oBmD,oBAAG,mBAAmB,CAAC,EAAE,IAAI,MAAKA,MAAA,mBAAmB,CAAC,EAAE,UAAtB,OAAAA,MAA+B,EAAE,KAAK;AAAA,SAAG,CAAC,GACnH;AAAA,SACF,IACE;AAAA,MACJ,qBAAC,SAAI,WAAU,6BACb;AAAA,4BAAC,UAAK,WAAU,+DAA8D,qBAAO;AAAA,QACrF,oBAAC,UAAK,WAAU,YAAY,mBAAQ;AAAA,QACpC,qBAAC,UAAK,WAAU,+EACd;AAAA,8BAAC,QAAK,MAAM,IAAI;AAAA,UAAE;AAAA,WACpB;AAAA,SACF;AAAA,OACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,QACP,UAAU,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK;AAAA,QACvC,aAAY;AAAA,QACZ,WAAU;AAAA,QACV,WAAW,CAAC,MAAM;AAChB,eAAK,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,SAAS;AACjD,cAAE,eAAe;AACjB,iBAAK,YAAY;AAAA,UACnB;AAAA,QACF;AAAA;AAAA,IACF;AAAA,IAEC,OAAO,OAAO,YACb,qBAAC,SAAI,WAAU,4FACb;AAAA,0BAAC,UAAK,WAAU,iCAAgC,gBAAE;AAAA,MACjD,OAAO;AAAA,OACV,IACE;AAAA,IAEJ,qBAAC,SAAI,WAAU,0CACb;AAAA,0BAAC,mBAAgB;AAAA,MACjB,qBAAC,WAAM,WAAU,6FACf;AAAA,4BAAC,UAAO,SAAS,KAAK,iBAAiB,QAAQ,cAAW,oBAAmB;AAAA,QAAE;AAAA,SAEjF;AAAA,MACA,qBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,UAAU,SAAS,SAAS,MAAM,KAAK,YAAY,GACnG;AAAA,4BAAC,OAAI,MAAM,IAAI;AAAA,QAAE;AAAA,SACnB;AAAA,MACA,qBAAC,UAAO,MAAK,UAAS,MAAK,MAAK,UAAU,SAAS,SAAS,MAAM,KAAK,YAAY,GACjF;AAAA,4BAAC,QAAK,MAAM,IAAI;AAAA,QAAE;AAAA,SACpB;AAAA,OACF;AAAA,IAEA,oBAAC,UAAO,MAAM,SAAS,cAAc,CAAC,SAAS;AAAE,UAAI,CAAC,SAAS;AAAE,mBAAW,IAAI;AAAG,YAAI,CAAC,KAAM,iBAAgB,YAAY;AAAA,MAAE;AAAA,IAAE,GAC5H,+BAAC,iBAAc,WAAU,YACvB;AAAA,2BAAC,gBACC;AAAA,6BAAC,eAAY,WAAU,yCACrB;AAAA,8BAAC,OAAI,MAAM,IAAI;AAAA,UAAE;AAAA,UAAE,aAAa,QAAQ,wBAAwB;AAAA,WAClE;AAAA,QACA,oBAAC,qBACE,uBAAa,QACV,mFACA,iCAAE;AAAA;AAAA,UAAc,QAAQ,QAAQ,YAAY,EAAE;AAAA,UAAE;AAAA,WAAiC,GACvF;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,6DACb;AAAA,6BAAC,SACC;AAAA,8BAAC,UAAK,WAAU,yBAAwB,iBAAG;AAAA,UAC3C,oBAAC,OAAG,6BAAmB,OAAO,OAAO,EAAE,MAAK;AAAA,UAAK;AAAA,UACjD,qBAAC,UAAK,WAAU,4BAA2B;AAAA;AAAA,aAAK,wBAAmB,OAAO,OAAO,EAAE,UAAnC,YAA4C,OAAO,QAAQ;AAAA,YAAM;AAAA,aAAI;AAAA,WACvH;AAAA,QACC,YAAY,OAAO,SAClB,qBAAC,SAAI,WAAU,yBAAwB;AAAA;AAAA,UAAI,kBAAkB,OAAO,IAAI,CAAC,MAAG;AAjtB1F,gBAAAA;AAitB6F,sBAAG,mBAAmB,CAAC,EAAE,IAAI,MAAKA,MAAA,mBAAmB,CAAC,EAAE,UAAtB,OAAAA,MAA+B,EAAE,KAAK;AAAA,WAAG,CAAC;AAAA,WAAE,IAC3J;AAAA,QACJ,qBAAC,SACC;AAAA,8BAAC,UAAK,WAAU,yBAAwB,sBAAQ;AAAA,UAC/C;AAAA,WACH;AAAA,SACF;AAAA,MACC,aAAa,UACZ,qBAAC,SAAI,aAAU,wBAAuB,MAAK,UAAS,WAAU,uEAC5D;AAAA,4BAAC,UAAK,WAAU,4FAA2F,eAAW,MAAC;AAAA,QAAE;AAAA,SAE3H,IACE,aAAa,QACf,oBAAC,OAAE,MAAK,SAAQ,WAAU,4BACvB,uBAAa,OAChB,IAEA;AAAA,QAAC;AAAA;AAAA,UACC,aAAU;AAAA,UACV,4BAAyB,kBAAa,sBAAb,YAAkC;AAAA,UAC3D,WAAU;AAAA,UAEV,8BAAC,aAAU,OAAM,kBAAa,SAAb,YAAqB,IAAI,SAAQ,WAAU,iBAAiB,OAAO,oBAAkB,MAAC;AAAA;AAAA,MACzG;AAAA,MAED,YACC,oBAAC,OAAE,MAAK,SAAQ,WAAU,4BACvB,qBACH,IACE;AAAA,MACJ,qBAAC,gBAAa,WAAU,sBACtB;AAAA,4BAAC,UAAK,WAAU,eAAc,OAAO,oDAAuB,QAC1D;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,UAAU,WAAW,aAAa,WAAW;AAAA,YAC7C,SAAS;AAAA,YACT,cAAY,sBAAsB,wBAAwB,mBAAmB,KAAK;AAAA,YAClF,WAAU;AAAA,YAEV;AAAA,kCAAC,aAAU,MAAM,IAAI;AAAA,cAAE;AAAA;AAAA;AAAA,QACzB,GACF;AAAA,QACA,qBAAC,UAAK,WAAU,2BACd;AAAA,8BAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,UAAU,SAAS,SAAS,MAAM;AAAE,uBAAW,KAAK;AAAG,4BAAgB,YAAY;AAAA,UAAE,GAAG,0BAE1I;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,MAAK;AAAA,cACL,UAAU,WAAW,aAAa;AAAA,cAClC,SAAS;AAAA,cAET;AAAA,oCAAC,QAAK,MAAM,IAAI;AAAA,gBAAE;AAAA,gBAAE,UAAU,eAAe;AAAA;AAAA;AAAA,UAC/C;AAAA,WACF;AAAA,SACF;AAAA,OACF,GACF;AAAA,IAEC,aACC,oBAAC,OAAE,WAAU,6CAA4C,0DAAyC,IAChG;AAAA,KACN;AAEJ;AAMA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AAvyBH;AAwyBE,QAAM,WAAW,OAAO,aAAa;AACrC,QAAM,wBAAsB,YAAO,wBAAP,mBAA4B,WAAU;AAClE,QAAM,sBAAsB,qBAAqB,OAAO;AACxD,QAAM,QAAQ,CAAC,EAAE,OAAO,MAAM,OAAO,GAAG;AACxC,QAAM,iBAAiB,MAAM,QAAQ,MAAM,4BAA4B,OAAO,QAAQ,GAAG,CAAC,OAAO,QAAQ,CAAC;AAC1G,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAqB,MAAM;AACzD,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,KAAK;AACpD,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAkC,MAAM;AAC5E,UAAM,IAA6B,CAAC;AACpC,mBAAe,QAAQ,CAAC,GAAG,MAAM;AAC/B,QAAE,EAAE,EAAE,IAAI,MAAM,eAAe,SAAS;AAAA,IAC1C,CAAC;AACD,WAAO;AAAA,EACT,CAAC;AAED,QAAM,UAAU,MAAM;AACpB,gBAAY,CAAC,YAAY;AACvB,YAAM,OAAO,mBAAK;AAClB,qBAAe,QAAQ,CAAC,GAAG,MAAM;AAC/B,YAAI,KAAK,EAAE,EAAE,MAAM,OAAW,MAAK,EAAE,EAAE,IAAI,MAAM,eAAe,SAAS;AAAA,MAC3E,CAAC;AACD,aAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAC,cAAc,CAAC;AAEnB,QAAM,SAAS,CAAC,OAAe,YAAY,CAAC,MAAO,iCAAK,IAAL,EAAQ,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE;AAE1E,SACE,qBAAC,SAAI,aAAU,oBAAmB,WAAU,aACzC;AAAA,gBAAY,OAAO,SAClB,qBAAC,SAAI,WAAU,kJACb;AAAA,0BAAC,SAAM,MAAM,IAAI,WAAU,mBAAkB;AAAA,MAC7C,qBAAC,UACC;AAAA,4BAAC,OAAE,+BAAiB;AAAA,QAAI;AAAA,QAAwB,OAAO,OAAO;AAAA,QAAS;AAAA,QACR,kCAAc;AAAA,QAAU;AAAA,SACzF;AAAA,OACF,IACE;AAAA,IAEJ,oBAAC,SAAI,WAAU,aACZ,yBAAe,IAAI,CAAC,MACnB,oBAAC,eAAuB,SAAS,GAAG,UAAU,CAAC,CAAC,SAAS,EAAE,EAAE,GAAG,UAAU,MAAM,OAAO,EAAE,EAAE,GAAG,MAA5E,EAAE,EAAkF,CACvG,GACH;AAAA,IAEC,CAAC,WACA,qBAAC,SAAI,WAAU,wHACb;AAAA,0BAAC,OAAI,MAAM,IAAI,WAAU,mBAAkB;AAAA,MAC3C,qBAAC,UAAK,WAAU,kBACd;AAAA,4BAAC,OAAE,2BAAa;AAAA,QAAI;AAAA,QAAE;AAAA,SACxB;AAAA,MACA,oBAAC,qBAAkB,QAAgB,eAA8B;AAAA,OACnE,IACE;AAAA,IAEH,YAAY,SAAS,SACpB,qBAAC,SAAI,aAAU,mBAAkB,WAAU,mFACzC;AAAA,2BAAC,UAAO,MAAK,UAAS,MAAK,MAAK,SAAS,MAAM;AAAE,oBAAY,KAAK;AAAG,gBAAQ,UAAU;AAAA,MAAE,GACvF;AAAA,4BAAC,SAAM,MAAM,IAAI;AAAA,QAAE;AAAA,SACrB;AAAA,MACC,QACC,qBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,MAAM;AAAE,oBAAY,IAAI;AAAG,gBAAQ,UAAU;AAAA,MAAE,GACxG;AAAA,4BAAC,YAAS,MAAM,IAAI;AAAA,QAAE;AAAA,SACxB,IACE;AAAA,MACJ,oBAAC,qBAAkB,QAAgB,eAA8B;AAAA,MACjE,qBAAC,UAAK,WAAU,iFACd;AAAA,4BAAC,YAAS,MAAM,IAAI;AAAA,QAAE;AAAA,SACxB;AAAA,OACF,IACE;AAAA,IAEH,YAAY,SAAS,aACpB;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,MAAM,QAAQ,MAAM;AAAA,QAC7B,QAAQ,OAAO,MAAM,qBAAqB;AACxC,iBAAM,2CAAc,EAAE,UAAU,OAAO,UAAU,MAAM,kBAAkB,SAAS;AAClF,kBAAQ,MAAM;AAAA,QAChB;AAAA,QACA,SAAS,OAAO,MAAM,qBAAqB;AACzC,cAAI,CAAC,mBAAoB;AACzB,gBAAM,mBAAmB,EAAE,UAAU,OAAO,UAAU,MAAM,kBAAkB,SAAS,CAAC;AACxF,kBAAQ,OAAO;AAAA,QACjB;AAAA,QACA;AAAA;AAAA,IACF,IACE;AAAA,IAEH,YAAY,SAAS,SACpB,qBAAC,SAAI,WAAU,6GACb;AAAA,0BAAC,SAAM,MAAM,IAAI,WAAU,kCAAiC;AAAA,MAC5D,qBAAC,UAAK,WAAU,UACd;AAAA,4BAAC,OAAG,qBAAW,mBAAmB,cAAa;AAAA,QAAI;AAAA,QAAqC;AAAA,QACxF,oBAAC,OAAG,6BAAmB,OAAO,OAAO,EAAE,MAAK;AAAA,QAAI;AAAA,QAAoB,oBAAC,OAAE,qBAAO;AAAA,QAAI;AAAA,SACpF;AAAA,MACA,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,QAAQ,MAAM,GAAG,kBAEhF;AAAA,OACF,IACE;AAAA,IAEH,YAAY,SAAS,UACpB,qBAAC,SAAI,WAAU,uFACb;AAAA,0BAAC,aAAU,MAAM,IAAI;AAAA,MACrB,qBAAC,UAAK,WAAU,UACd;AAAA,4BAAC,OAAE,mCAAqB;AAAA,QAAI;AAAA,QAAgB,qBAAC,OAAE;AAAA;AAAA,UAAK,OAAO;AAAA,WAAQ;AAAA,QAAI;AAAA,SACzE;AAAA,MACA,oBAAC,qBAAkB,QAAgB,eAA8B;AAAA,MACjE,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,QAAQ,MAAM,GAAG,kBAEhF;AAAA,OACF,IACE;AAAA,KACN;AAEJ;AAIA,SAAS,UAAU;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAImI;AAl7BnI;AAm7BE,QAAM,SAAS,gBAAgB,MAAM;AACrC,QAAM,iBAAiB,MAAM,QAAQ,MAAM,4BAA4B,OAAO,QAAQ,GAAG,CAAC,OAAO,QAAQ,CAAC;AAC1G,QAAM,OAAO,eAAe,eAAe,SAAS,CAAC;AACrD,QAAM,aAAa,OAAO,mBAAmB,KAAK,IAAI,IAAI;AAC1D,QAAM,YAAY,KAAK,mBAAmB,EAAE,IAAI;AAChD,QAAM,OAAM,6BAAM,eAAc,cAAc,UAAU,yCAAY,OAAO,uCAAW,KAAK,IAAI,QAAQ,WAAU,8CAAY,SAAZ,YAAoB,EAAE;AACvI,QAAM,cAAc,OAAO,mBAAmB,MAAM,GAAG,IAAI;AAC3D,QAAM,OAAO,YAAY,MAAM;AAE/B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,eAAa;AAAA,MACb,aAAW,OAAO,SAAS;AAAA,MAC3B,WAAW,GAAG,0CAA0C,kBAAkB,MAAM,CAAC;AAAA,MAEjF;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,iBAAe;AAAA,YACf,WAAU;AAAA,YAEV;AAAA,kCAAC,UAAK,WAAW,GAAG,gCAAgC,WAAW,MAAM,CAAC,GAAG,eAAW,MAAC;AAAA,cACrF,qBAAC,UAAK,WAAU,kBACd;AAAA,qCAAC,UAAK,WAAU,2BACd;AAAA,sCAAC,UAAK,WAAU,kCAAkC,iBAAO,SAAQ;AAAA,kBACjE,oBAAC,UAAK,WAAW,GAAG,6EAA6E,KAAK,GAAG,GACtG,eAAK,OACR;AAAA,mBACF;AAAA,gBACA,qBAAC,UAAK,WAAU,gDACd;AAAA,sCAAC,OAAE,WAAU,sBAAsB,6BAAmB,OAAO,OAAO,EAAE,MAAK;AAAA,kBAAI;AAAA,kBAAI;AAAA,kBAAI;AAAA,kBAAG;AAAA,mBAC5F;AAAA,iBACF;AAAA,cACA,oBAAC,UAAK,WAAU,6CAA6C,iBAAO,UAAS;AAAA,cAC5E,OACC,oBAAC,aAAU,MAAM,IAAI,WAAU,kCAAiC,IAEhE,oBAAC,eAAY,MAAM,IAAI,WAAU,kCAAiC;AAAA;AAAA;AAAA,QAEtE;AAAA,QAEC,OACC,oBAAC,SAAI,WAAU,aACb;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA;AAAA,QACF,GACF,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA2B;AA1/B3B;AA2/BE,QAAM,YAAY,QAAQ,OAAO,CAAC,MAAM,EAAE,WAAW,eAAe,EAAE,aAAa,KAAK,EAAE;AAC1F,QAAM,QAAQ,QAAQ,OAAO,CAAC,MAAM,gBAAgB,CAAC,MAAM,OAAO,EAAE;AACpE,QAAM,WAAW,QAAQ,OAAO,CAAC,MAAM,gBAAgB,CAAC,MAAM,UAAU,EAAE;AAC1E,QAAM,YAAY,QAAQ,KAAK,CAAC,MAAM,EAAE,MAAM;AAC9C,QAAM,qBACJ,mBAAQ,KAAK,CAAC,MAAM,EAAE,WAAW,eAAe,EAAE,aAAa,KAAK,MAApE,YACA,QAAQ,KAAK,CAAC,MAAM,gBAAgB,CAAC,MAAM,OAAO,MADlD,YAEA,QAAQ,KAAK,CAAC,MAAM,gBAAgB,CAAC,MAAM,UAAU;AACvD,QAAM,kBACJ,yBAAQ,KAAK,CAAC,MAAM,EAAE,WAAW,eAAe,EAAE,aAAa,SAAS,eAAe,GAAG,aAAa,CAAC,MAAxG,YACA,QAAQ,KAAK,CAAC,MAAM,gBAAgB,CAAC,MAAM,WAAW,eAAe,GAAG,aAAa,CAAC,MADtF,YAEA,QAAQ,KAAK,CAAC,MAAM,gBAAgB,CAAC,MAAM,cAAc,eAAe,GAAG,aAAa,CAAC,MAFzF,YAGA,QAAQ,KAAK,CAAC,MAAM,eAAe,GAAG,aAAa,CAAC;AACtD,QAAM,gBAAgB,QAAQ,KAAK,CAAC,MAAM,gBAAgB,CAAC,MAAM,UAAU;AAE3E,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAwB,MAAM;AAC9D,QAAI,oBAAqB,QAAO;AAChC,WAAO,oBAAoB,kBAAkB,WAAW;AAAA,EAC1D,CAAC;AAED,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAG5B,QAAM,QACJ,YAAY,IACR,EAAE,OAAO,2BAA2B,KAAK,wBAAwB,MAAM,0BAA0B,IACjG,QAAQ,IACN,EAAE,OAAO,eAAe,KAAK,wBAAwB,MAAM,0BAA0B,IACrF,WAAW,IACT,EAAE,OAAO,kCAA+B,KAAK,qBAAqB,MAAM,uBAAuB,IAC/F,EAAE,OAAO,iBAAiB,KAAK,0BAA0B,MAAM,yBAAyB;AAElG,QAAM,YACJ,YAAY,IACR,GAAG,SAAS,IAAI,cAAc,IAAI,gBAAgB,cAAc,mBAChE,QAAQ,IACN,kBAAkB,KAAK,IAAI,UAAU,IAAI,WAAW,SAAS,KAC7D,WAAW,IACT,aAAa,KAAK,gBAChB,4BAA4B,UAAU,mBAAmB,cAAc,OAAO,EAAE,IAAI,CAAC,KACrF,yBAAyB,QAAQ,aACnC,GAAG,QAAQ,MAAM,UAAU,QAAQ,WAAW,IAAI,WAAW,SAAS;AAEhF,QAAM,aAAa,YAAY,IAAI,cAAc,QAAQ,IAAI,UAAU,WAAW,IAAI,aAAa;AAEnG,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,kBAAgB,YAAY,IAAI,SAAS;AAAA,MACzC,cAAY;AAAA,MACZ,WAAW;AAAA,QACT;AAAA,QACA,eAAe,cACX,iCACA,eAAe,aACb,8BACA;AAAA,QACN;AAAA,MACF;AAAA,MAEA;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,aAAU;AAAA,YACV,WAAW;AAAA,cACT;AAAA,cACA,eAAe,cACX,4BACA,eAAe,aACb,yBACA;AAAA,YACR;AAAA,YAEA;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAS,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;AAAA,kBACnC,iBAAe;AAAA,kBACf,WAAU;AAAA,kBAEZ;AAAA;AAAA,sBAAC;AAAA;AAAA,wBACC,aAAU;AAAA,wBACV,WAAW;AAAA,0BACT;AAAA,0BACA,YAAY,IACR,gDACA,QAAQ,IACN,gDACA,WAAW,IACT,0CACA;AAAA,wBACV;AAAA,wBAEA;AAAA,+CAAC,UAAK,WAAU,+BACd;AAAA,gDAAC,UAAK,WAAW,GAAG,2EAA2E,MAAM,IAAI,GAAG;AAAA,4BAC5G,oBAAC,UAAK,WAAW,GAAG,4CAA4C,MAAM,GAAG,GAAG;AAAA,6BAC9E;AAAA,0BACC,MAAM;AAAA;AAAA;AAAA,oBACT;AAAA,oBACA,qBAAC,UAAK,WAAU,kBACd;AAAA,0CAAC,UAAK,WAAU,sDAAsD,qBAAU;AAAA,sBAChF,qBAAC,UAAK,WAAU,uDACb;AAAA,gCAAQ;AAAA,wBAAO;AAAA,wBAAE,QAAQ,WAAW,IAAI,WAAW;AAAA,wBAAU;AAAA,wBAC7D,YAAY,iCAAE;AAAA;AAAA,0BAAG,oBAAC,OAAE,8BAAgB;AAAA,2BAAI,IAAM;AAAA,yBACjD;AAAA,uBACF;AAAA,oBACG,UACC,oBAAC,aAAU,MAAM,IAAI,WAAU,kCAAiC,IAEhE,oBAAC,eAAY,MAAM,IAAI,WAAU,kCAAiC;AAAA;AAAA;AAAA,cAEtE;AAAA,cACC,iBACC,oBAAC,SAAI,WAAU,YAAW,SAAS,CAAC,UAAU,MAAM,gBAAgB,GAClE,8BAAC,qBAAkB,QAAQ,gBAAgB,eAA8B,GAC3E,IACE;AAAA;AAAA;AAAA,QACN;AAAA,QAEC,UACC,oBAAC,SAAI,WAAU,0BACZ,kBAAQ,IAAI,CAAC,MACZ;AAAA,UAAC;AAAA;AAAA,YAEC,QAAQ;AAAA,YACR,MAAM,WAAW,EAAE;AAAA,YACnB,cAAc,MAAM,UAAU,CAAC,QAAS,QAAQ,EAAE,WAAW,OAAO,EAAE,QAAS;AAAA,YAC/E;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA;AAAA,UATK,EAAE;AAAA,QAUT,CACD,GACH,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;","names":["_a","_b"]}
@@ -122,6 +122,33 @@ function sanitizeFontSize(value) {
122
122
  if (!Number.isFinite(amount) || amount <= 0 || amount > maxByUnit[unit]) return null;
123
123
  return `${amount}${unit}`;
124
124
  }
125
+ const COLOR_VALUE = /^(#[0-9a-f]{3,8}|rgba?\([\d\s,./%]+\)|[a-z]+)$/;
126
+ const LENGTH_BOX_VALUE = /^(-?[\d.]+(px|em|rem|%)?|auto|0)(\s+(-?[\d.]+(px|em|rem|%)?|auto|0)){0,3}$/;
127
+ const SAFE_TEXT_STYLE_PROPS = {
128
+ color: COLOR_VALUE,
129
+ "background-color": COLOR_VALUE,
130
+ "font-family": /^[a-z0-9\s,'"-]+$/,
131
+ "font-style": /^(italic|normal|oblique)$/,
132
+ "font-weight": /^(bold|bolder|lighter|normal|[1-9]00)$/,
133
+ "font-variant": /^[a-z\s-]+$/,
134
+ "text-decoration": /^[a-z\s-]+$/,
135
+ "text-decoration-line": /^[a-z\s-]+$/,
136
+ "text-transform": /^(none|capitalize|uppercase|lowercase)$/,
137
+ "text-align": /^(left|right|center|justify)$/,
138
+ "letter-spacing": /^(normal|-?[\d.]+(px|em|rem)?)$/,
139
+ "line-height": /^(normal|[\d.]+(px|em|rem|%)?)$/,
140
+ "white-space": /^[a-z-]+$/,
141
+ margin: LENGTH_BOX_VALUE,
142
+ "margin-top": LENGTH_BOX_VALUE,
143
+ "margin-right": LENGTH_BOX_VALUE,
144
+ "margin-bottom": LENGTH_BOX_VALUE,
145
+ "margin-left": LENGTH_BOX_VALUE,
146
+ padding: LENGTH_BOX_VALUE,
147
+ "padding-top": LENGTH_BOX_VALUE,
148
+ "padding-right": LENGTH_BOX_VALUE,
149
+ "padding-bottom": LENGTH_BOX_VALUE,
150
+ "padding-left": LENGTH_BOX_VALUE
151
+ };
125
152
  function sanitizeStyle(value) {
126
153
  const declarations = [];
127
154
  for (const rawDeclaration of value.split(";")) {
@@ -137,6 +164,11 @@ function sanitizeStyle(value) {
137
164
  if (property === "font-size") {
138
165
  const fontSize = sanitizeFontSize(rawValue);
139
166
  if (fontSize) declarations.push(`font-size: ${fontSize}`);
167
+ continue;
168
+ }
169
+ const allowedValue = SAFE_TEXT_STYLE_PROPS[property];
170
+ if (allowedValue == null ? void 0 : allowedValue.test(rawValue)) {
171
+ declarations.push(`${property}: ${rawValue}`);
140
172
  }
141
173
  }
142
174
  return declarations.length ? declarations.join("; ") : null;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/internal/safe-html.ts"],"sourcesContent":["const DANGEROUS_BLOCK_TAGS = new Set([\n \"script\",\n \"style\",\n \"iframe\",\n \"object\",\n \"embed\",\n \"svg\",\n \"math\",\n \"template\",\n \"noscript\",\n \"textarea\",\n \"select\",\n])\n\nconst ALLOWED_TAGS = new Set([\n \"a\",\n \"b\",\n \"blockquote\",\n \"br\",\n \"code\",\n \"del\",\n \"div\",\n \"em\",\n \"hr\",\n \"i\",\n \"img\",\n \"li\",\n \"ol\",\n \"p\",\n \"pre\",\n \"s\",\n \"span\",\n \"strong\",\n \"sub\",\n \"sup\",\n \"table\",\n \"tbody\",\n \"td\",\n \"th\",\n \"thead\",\n \"tr\",\n \"u\",\n \"ul\",\n])\n\nconst VOID_TAGS = new Set([\"br\", \"hr\", \"img\"])\nconst SAFE_GLOBAL_ATTRS = new Set([\"aria-label\", \"dir\", \"lang\", \"role\", \"title\"])\nconst SAFE_URL_PROTOCOLS = new Set([\"http:\", \"https:\", \"mailto:\", \"tel:\"])\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n}\n\nfunction escapeAttribute(value: string): string {\n return escapeHtml(value).replace(/\"/g, \"&quot;\")\n}\n\nfunction safeCodePoint(value: number): string {\n return Number.isInteger(value) && value >= 0 && value <= 0x10ffff ? String.fromCodePoint(value) : \"\"\n}\n\nfunction decodeHtmlEntities(value: string): string {\n const namedEntities: Record<string, string> = {\n amp: \"&\",\n apos: \"'\",\n colon: \":\",\n gt: \">\",\n lt: \"<\",\n newline: \"\\n\",\n quot: '\"',\n tab: \"\\t\",\n }\n\n let decoded = value\n for (let i = 0; i < 4; i += 1) {\n const next = decoded\n .replace(/&#x([0-9a-f]+);?/gi, (_match, hex: string) => {\n const codePoint = Number.parseInt(hex, 16)\n return safeCodePoint(codePoint)\n })\n .replace(/&#(\\d+);?/g, (_match, decimal: string) => {\n const codePoint = Number.parseInt(decimal, 10)\n return safeCodePoint(codePoint)\n })\n .replace(/&([a-z]+);/gi, (match, name: string) => namedEntities[name.toLowerCase()] ?? match)\n\n if (next === decoded) return decoded\n decoded = next\n }\n\n return decoded\n}\n\nfunction isSafeUrl(value: string): boolean {\n const decoded = decodeHtmlEntities(value).replace(/[\\u0000-\\u001f\\u007f\\s]+/g, \"\").trim()\n if (!decoded) return false\n if (decoded.startsWith(\"//\")) return false\n if (decoded.startsWith(\"#\") || decoded.startsWith(\"/\") || decoded.startsWith(\"./\") || decoded.startsWith(\"../\")) {\n return true\n }\n\n try {\n return SAFE_URL_PROTOCOLS.has(new URL(decoded, \"https://handled.local\").protocol)\n } catch {\n return false\n }\n}\n\nfunction sanitizeClassName(value: string): string | null {\n const safeTokens = value\n .split(/\\s+/)\n .map((token) => token.trim())\n .filter((token) => /^[A-Za-z0-9_-]+$/.test(token))\n\n return safeTokens.length ? safeTokens.join(\" \") : null\n}\n\nfunction sanitizeDimension(value: string): string | null {\n const trimmed = value.trim()\n if (/^\\d{1,4}$/.test(trimmed)) return trimmed\n return null\n}\n\nfunction sanitizeLanguage(value: string): string | null {\n const trimmed = value.trim()\n if (/^[A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*$/.test(trimmed)) return trimmed\n return null\n}\n\nfunction sanitizeDirection(value: string): string | null {\n const trimmed = value.trim().toLowerCase()\n return trimmed === \"ltr\" || trimmed === \"rtl\" || trimmed === \"auto\" ? trimmed : null\n}\n\nfunction sanitizeFontSize(value: string): string | null {\n const trimmed = value.trim().toLowerCase()\n const match = trimmed.match(/^(\\d+(?:\\.\\d+)?)(px|em|rem|%)$/)\n if (!match) return null\n\n const amount = Number.parseFloat(match[1])\n const unit = match[2]\n const maxByUnit: Record<string, number> = { px: 72, em: 4, rem: 4, \"%\": 400 }\n if (!Number.isFinite(amount) || amount <= 0 || amount > maxByUnit[unit]) return null\n return `${amount}${unit}`\n}\n\nfunction sanitizeStyle(value: string): string | null {\n const declarations: string[] = []\n\n for (const rawDeclaration of value.split(\";\")) {\n const separatorIndex = rawDeclaration.indexOf(\":\")\n if (separatorIndex === -1) continue\n\n const property = rawDeclaration.slice(0, separatorIndex).trim().toLowerCase()\n const rawValue = decodeHtmlEntities(rawDeclaration.slice(separatorIndex + 1)).trim().toLowerCase()\n if (!property || !rawValue || /url\\s*\\(|expression\\s*\\(/i.test(rawValue)) continue\n\n if (property === \"vertical-align\" && (rawValue === \"super\" || rawValue === \"sub\")) {\n declarations.push(`vertical-align: ${rawValue}`)\n continue\n }\n\n if (property === \"font-size\") {\n const fontSize = sanitizeFontSize(rawValue)\n if (fontSize) declarations.push(`font-size: ${fontSize}`)\n }\n }\n\n return declarations.length ? declarations.join(\"; \") : null\n}\n\nfunction sanitizeAttribute(tagName: string, name: string, value: string): string | null {\n const attr = name.toLowerCase()\n\n if (\n attr.startsWith(\"on\") ||\n attr === \"srcdoc\" ||\n attr === \"formaction\" ||\n attr === \"xlink:href\" ||\n attr === \"xmlns\"\n ) {\n return null\n }\n\n if (attr === \"style\") {\n const safeStyle = sanitizeStyle(value)\n return safeStyle ? `style=\"${escapeAttribute(safeStyle)}\"` : null\n }\n\n if (attr === \"class\") {\n const safeClassName = sanitizeClassName(value)\n return safeClassName ? `class=\"${escapeAttribute(safeClassName)}\"` : null\n }\n\n if (attr === \"dir\") {\n const safeDirection = sanitizeDirection(value)\n return safeDirection ? `dir=\"${escapeAttribute(safeDirection)}\"` : null\n }\n\n if (attr === \"lang\") {\n const safeLanguage = sanitizeLanguage(value)\n return safeLanguage ? `lang=\"${escapeAttribute(safeLanguage)}\"` : null\n }\n\n if (SAFE_GLOBAL_ATTRS.has(attr) || attr.startsWith(\"aria-\")) {\n return `${attr}=\"${escapeAttribute(value)}\"`\n }\n\n if (tagName === \"a\" && attr === \"href\" && isSafeUrl(value)) {\n return `href=\"${escapeAttribute(value)}\"`\n }\n\n if (tagName === \"img\" && attr === \"src\" && isSafeUrl(value)) {\n return `src=\"${escapeAttribute(value)}\"`\n }\n\n if (tagName === \"img\" && attr === \"alt\") {\n return `${attr}=\"${escapeAttribute(value)}\"`\n }\n\n if (tagName === \"img\" && (attr === \"width\" || attr === \"height\")) {\n const safeDimension = sanitizeDimension(value)\n return safeDimension ? `${attr}=\"${escapeAttribute(safeDimension)}\"` : null\n }\n\n if ((tagName === \"td\" || tagName === \"th\") && (attr === \"colspan\" || attr === \"rowspan\")) {\n return `${attr}=\"${escapeAttribute(value)}\"`\n }\n\n return null\n}\n\nfunction sanitizeAttributes(tagName: string, rawAttributes = \"\"): string {\n const attributes: string[] = []\n const attrPattern = /([A-Za-z_:][-A-Za-z0-9_:.]*)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'=<>`]+)))?/g\n let match: RegExpExecArray | null\n\n while ((match = attrPattern.exec(rawAttributes)) !== null) {\n const [, name, doubleQuotedValue, singleQuotedValue, unquotedValue] = match\n const value = doubleQuotedValue ?? singleQuotedValue ?? unquotedValue ?? \"\"\n const safeAttribute = sanitizeAttribute(tagName, name, value)\n if (safeAttribute) attributes.push(safeAttribute)\n }\n\n if (tagName === \"a\" && attributes.some((attr) => attr.startsWith(\"href=\"))) {\n attributes.push('target=\"_blank\"', 'rel=\"noopener noreferrer\"')\n }\n\n return attributes.length ? ` ${attributes.join(\" \")}` : \"\"\n}\n\nfunction findTagEnd(html: string, startIndex: number): number {\n let quote: '\"' | \"'\" | null = null\n\n for (let i = startIndex + 1; i < html.length; i += 1) {\n const char = html[i]\n if (quote) {\n if (char === quote) quote = null\n continue\n }\n\n if (char === '\"' || char === \"'\") {\n quote = char\n continue\n }\n\n if (char === \">\") return i\n }\n\n return -1\n}\n\nfunction parseTag(rawTag: string): { closing: boolean; name: string; attributes: string } | null {\n const match = rawTag.match(/^<\\s*(\\/)?\\s*([A-Za-z][A-Za-z0-9:-]*)\\b([\\s\\S]*?)\\/?>$/)\n if (!match) return null\n\n return {\n closing: !!match[1],\n name: match[2].toLowerCase(),\n attributes: match[3] ?? \"\",\n }\n}\n\nfunction findDangerousClose(html: string, tagName: string, fromIndex: number): number {\n const closePattern = new RegExp(`</\\\\s*${tagName}\\\\s*>`, \"ig\")\n closePattern.lastIndex = fromIndex\n const match = closePattern.exec(html)\n return match ? closePattern.lastIndex : -1\n}\n\n/**\n * Conservative, deterministic sanitizer for user/email supplied HTML rendered by\n * design-system components. It keeps common email formatting tags while removing\n * executable tags, event handlers, unsafe inline styles, and unsafe URLs. This stays\n * dependency-free for the shared package and intentionally favors stripping\n * ambiguous email content over preserving every possible HTML feature.\n */\nexport function sanitizeHtml(html: string): string {\n let output = \"\"\n let cursor = 0\n\n while (cursor < html.length) {\n const tagStart = html.indexOf(\"<\", cursor)\n if (tagStart === -1) {\n output += html.slice(cursor)\n break\n }\n\n output += html.slice(cursor, tagStart)\n\n if (html.startsWith(\"<!--\", tagStart)) {\n const commentEnd = html.indexOf(\"-->\", tagStart + 4)\n cursor = commentEnd === -1 ? html.length : commentEnd + 3\n continue\n }\n\n const tagEnd = findTagEnd(html, tagStart)\n if (tagEnd === -1) {\n output += escapeHtml(html.slice(tagStart))\n break\n }\n\n const rawTag = html.slice(tagStart, tagEnd + 1)\n const parsed = parseTag(rawTag)\n if (!parsed) {\n cursor = tagEnd + 1\n continue\n }\n\n if (DANGEROUS_BLOCK_TAGS.has(parsed.name)) {\n const closeEnd = parsed.closing ? -1 : findDangerousClose(html, parsed.name, tagEnd + 1)\n cursor = closeEnd === -1 ? tagEnd + 1 : closeEnd\n continue\n }\n\n if (ALLOWED_TAGS.has(parsed.name)) {\n if (parsed.closing) {\n if (!VOID_TAGS.has(parsed.name)) output += `</${parsed.name}>`\n } else {\n output += `<${parsed.name}${sanitizeAttributes(parsed.name, parsed.attributes)}>`\n }\n }\n\n cursor = tagEnd + 1\n }\n\n return output\n}\n\nexport function htmlToTextSnippet(html: string, maxLength = 140): string {\n return sanitizeHtml(html)\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim()\n .slice(0, maxLength)\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/internal/safe-html.ts"],"sourcesContent":["const DANGEROUS_BLOCK_TAGS = new Set([\n \"script\",\n \"style\",\n \"iframe\",\n \"object\",\n \"embed\",\n \"svg\",\n \"math\",\n \"template\",\n \"noscript\",\n \"textarea\",\n \"select\",\n])\n\nconst ALLOWED_TAGS = new Set([\n \"a\",\n \"b\",\n \"blockquote\",\n \"br\",\n \"code\",\n \"del\",\n \"div\",\n \"em\",\n \"hr\",\n \"i\",\n \"img\",\n \"li\",\n \"ol\",\n \"p\",\n \"pre\",\n \"s\",\n \"span\",\n \"strong\",\n \"sub\",\n \"sup\",\n \"table\",\n \"tbody\",\n \"td\",\n \"th\",\n \"thead\",\n \"tr\",\n \"u\",\n \"ul\",\n])\n\nconst VOID_TAGS = new Set([\"br\", \"hr\", \"img\"])\nconst SAFE_GLOBAL_ATTRS = new Set([\"aria-label\", \"dir\", \"lang\", \"role\", \"title\"])\nconst SAFE_URL_PROTOCOLS = new Set([\"http:\", \"https:\", \"mailto:\", \"tel:\"])\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n}\n\nfunction escapeAttribute(value: string): string {\n return escapeHtml(value).replace(/\"/g, \"&quot;\")\n}\n\nfunction safeCodePoint(value: number): string {\n return Number.isInteger(value) && value >= 0 && value <= 0x10ffff ? String.fromCodePoint(value) : \"\"\n}\n\nfunction decodeHtmlEntities(value: string): string {\n const namedEntities: Record<string, string> = {\n amp: \"&\",\n apos: \"'\",\n colon: \":\",\n gt: \">\",\n lt: \"<\",\n newline: \"\\n\",\n quot: '\"',\n tab: \"\\t\",\n }\n\n let decoded = value\n for (let i = 0; i < 4; i += 1) {\n const next = decoded\n .replace(/&#x([0-9a-f]+);?/gi, (_match, hex: string) => {\n const codePoint = Number.parseInt(hex, 16)\n return safeCodePoint(codePoint)\n })\n .replace(/&#(\\d+);?/g, (_match, decimal: string) => {\n const codePoint = Number.parseInt(decimal, 10)\n return safeCodePoint(codePoint)\n })\n .replace(/&([a-z]+);/gi, (match, name: string) => namedEntities[name.toLowerCase()] ?? match)\n\n if (next === decoded) return decoded\n decoded = next\n }\n\n return decoded\n}\n\nfunction isSafeUrl(value: string): boolean {\n const decoded = decodeHtmlEntities(value).replace(/[\\u0000-\\u001f\\u007f\\s]+/g, \"\").trim()\n if (!decoded) return false\n if (decoded.startsWith(\"//\")) return false\n if (decoded.startsWith(\"#\") || decoded.startsWith(\"/\") || decoded.startsWith(\"./\") || decoded.startsWith(\"../\")) {\n return true\n }\n\n try {\n return SAFE_URL_PROTOCOLS.has(new URL(decoded, \"https://handled.local\").protocol)\n } catch {\n return false\n }\n}\n\nfunction sanitizeClassName(value: string): string | null {\n const safeTokens = value\n .split(/\\s+/)\n .map((token) => token.trim())\n .filter((token) => /^[A-Za-z0-9_-]+$/.test(token))\n\n return safeTokens.length ? safeTokens.join(\" \") : null\n}\n\nfunction sanitizeDimension(value: string): string | null {\n const trimmed = value.trim()\n if (/^\\d{1,4}$/.test(trimmed)) return trimmed\n return null\n}\n\nfunction sanitizeLanguage(value: string): string | null {\n const trimmed = value.trim()\n if (/^[A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*$/.test(trimmed)) return trimmed\n return null\n}\n\nfunction sanitizeDirection(value: string): string | null {\n const trimmed = value.trim().toLowerCase()\n return trimmed === \"ltr\" || trimmed === \"rtl\" || trimmed === \"auto\" ? trimmed : null\n}\n\nfunction sanitizeFontSize(value: string): string | null {\n const trimmed = value.trim().toLowerCase()\n const match = trimmed.match(/^(\\d+(?:\\.\\d+)?)(px|em|rem|%)$/)\n if (!match) return null\n\n const amount = Number.parseFloat(match[1])\n const unit = match[2]\n const maxByUnit: Record<string, number> = { px: 72, em: 4, rem: 4, \"%\": 400 }\n if (!Number.isFinite(amount) || amount <= 0 || amount > maxByUnit[unit]) return null\n return `${amount}${unit}`\n}\n\n// Presentational properties preserved so email signatures keep their authored\n// Gmail look (serif/italic/gray etc.) when rendered in the conversation panel.\n// Values are validated per-property; anything containing url()/expression()\n// (or any property not listed) is still dropped — these are text-presentation\n// only, no layout escape or resource loading.\nconst COLOR_VALUE = /^(#[0-9a-f]{3,8}|rgba?\\([\\d\\s,./%]+\\)|[a-z]+)$/\nconst LENGTH_BOX_VALUE = /^(-?[\\d.]+(px|em|rem|%)?|auto|0)(\\s+(-?[\\d.]+(px|em|rem|%)?|auto|0)){0,3}$/\nconst SAFE_TEXT_STYLE_PROPS: Record<string, RegExp> = {\n color: COLOR_VALUE,\n \"background-color\": COLOR_VALUE,\n \"font-family\": /^[a-z0-9\\s,'\"-]+$/,\n \"font-style\": /^(italic|normal|oblique)$/,\n \"font-weight\": /^(bold|bolder|lighter|normal|[1-9]00)$/,\n \"font-variant\": /^[a-z\\s-]+$/,\n \"text-decoration\": /^[a-z\\s-]+$/,\n \"text-decoration-line\": /^[a-z\\s-]+$/,\n \"text-transform\": /^(none|capitalize|uppercase|lowercase)$/,\n \"text-align\": /^(left|right|center|justify)$/,\n \"letter-spacing\": /^(normal|-?[\\d.]+(px|em|rem)?)$/,\n \"line-height\": /^(normal|[\\d.]+(px|em|rem|%)?)$/,\n \"white-space\": /^[a-z-]+$/,\n margin: LENGTH_BOX_VALUE,\n \"margin-top\": LENGTH_BOX_VALUE,\n \"margin-right\": LENGTH_BOX_VALUE,\n \"margin-bottom\": LENGTH_BOX_VALUE,\n \"margin-left\": LENGTH_BOX_VALUE,\n padding: LENGTH_BOX_VALUE,\n \"padding-top\": LENGTH_BOX_VALUE,\n \"padding-right\": LENGTH_BOX_VALUE,\n \"padding-bottom\": LENGTH_BOX_VALUE,\n \"padding-left\": LENGTH_BOX_VALUE,\n}\n\nfunction sanitizeStyle(value: string): string | null {\n const declarations: string[] = []\n\n for (const rawDeclaration of value.split(\";\")) {\n const separatorIndex = rawDeclaration.indexOf(\":\")\n if (separatorIndex === -1) continue\n\n const property = rawDeclaration.slice(0, separatorIndex).trim().toLowerCase()\n const rawValue = decodeHtmlEntities(rawDeclaration.slice(separatorIndex + 1)).trim().toLowerCase()\n if (!property || !rawValue || /url\\s*\\(|expression\\s*\\(/i.test(rawValue)) continue\n\n if (property === \"vertical-align\" && (rawValue === \"super\" || rawValue === \"sub\")) {\n declarations.push(`vertical-align: ${rawValue}`)\n continue\n }\n\n if (property === \"font-size\") {\n const fontSize = sanitizeFontSize(rawValue)\n if (fontSize) declarations.push(`font-size: ${fontSize}`)\n continue\n }\n\n const allowedValue = SAFE_TEXT_STYLE_PROPS[property]\n if (allowedValue?.test(rawValue)) {\n declarations.push(`${property}: ${rawValue}`)\n }\n }\n\n return declarations.length ? declarations.join(\"; \") : null\n}\n\nfunction sanitizeAttribute(tagName: string, name: string, value: string): string | null {\n const attr = name.toLowerCase()\n\n if (\n attr.startsWith(\"on\") ||\n attr === \"srcdoc\" ||\n attr === \"formaction\" ||\n attr === \"xlink:href\" ||\n attr === \"xmlns\"\n ) {\n return null\n }\n\n if (attr === \"style\") {\n const safeStyle = sanitizeStyle(value)\n return safeStyle ? `style=\"${escapeAttribute(safeStyle)}\"` : null\n }\n\n if (attr === \"class\") {\n const safeClassName = sanitizeClassName(value)\n return safeClassName ? `class=\"${escapeAttribute(safeClassName)}\"` : null\n }\n\n if (attr === \"dir\") {\n const safeDirection = sanitizeDirection(value)\n return safeDirection ? `dir=\"${escapeAttribute(safeDirection)}\"` : null\n }\n\n if (attr === \"lang\") {\n const safeLanguage = sanitizeLanguage(value)\n return safeLanguage ? `lang=\"${escapeAttribute(safeLanguage)}\"` : null\n }\n\n if (SAFE_GLOBAL_ATTRS.has(attr) || attr.startsWith(\"aria-\")) {\n return `${attr}=\"${escapeAttribute(value)}\"`\n }\n\n if (tagName === \"a\" && attr === \"href\" && isSafeUrl(value)) {\n return `href=\"${escapeAttribute(value)}\"`\n }\n\n if (tagName === \"img\" && attr === \"src\" && isSafeUrl(value)) {\n return `src=\"${escapeAttribute(value)}\"`\n }\n\n if (tagName === \"img\" && attr === \"alt\") {\n return `${attr}=\"${escapeAttribute(value)}\"`\n }\n\n if (tagName === \"img\" && (attr === \"width\" || attr === \"height\")) {\n const safeDimension = sanitizeDimension(value)\n return safeDimension ? `${attr}=\"${escapeAttribute(safeDimension)}\"` : null\n }\n\n if ((tagName === \"td\" || tagName === \"th\") && (attr === \"colspan\" || attr === \"rowspan\")) {\n return `${attr}=\"${escapeAttribute(value)}\"`\n }\n\n return null\n}\n\nfunction sanitizeAttributes(tagName: string, rawAttributes = \"\"): string {\n const attributes: string[] = []\n const attrPattern = /([A-Za-z_:][-A-Za-z0-9_:.]*)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'=<>`]+)))?/g\n let match: RegExpExecArray | null\n\n while ((match = attrPattern.exec(rawAttributes)) !== null) {\n const [, name, doubleQuotedValue, singleQuotedValue, unquotedValue] = match\n const value = doubleQuotedValue ?? singleQuotedValue ?? unquotedValue ?? \"\"\n const safeAttribute = sanitizeAttribute(tagName, name, value)\n if (safeAttribute) attributes.push(safeAttribute)\n }\n\n if (tagName === \"a\" && attributes.some((attr) => attr.startsWith(\"href=\"))) {\n attributes.push('target=\"_blank\"', 'rel=\"noopener noreferrer\"')\n }\n\n return attributes.length ? ` ${attributes.join(\" \")}` : \"\"\n}\n\nfunction findTagEnd(html: string, startIndex: number): number {\n let quote: '\"' | \"'\" | null = null\n\n for (let i = startIndex + 1; i < html.length; i += 1) {\n const char = html[i]\n if (quote) {\n if (char === quote) quote = null\n continue\n }\n\n if (char === '\"' || char === \"'\") {\n quote = char\n continue\n }\n\n if (char === \">\") return i\n }\n\n return -1\n}\n\nfunction parseTag(rawTag: string): { closing: boolean; name: string; attributes: string } | null {\n const match = rawTag.match(/^<\\s*(\\/)?\\s*([A-Za-z][A-Za-z0-9:-]*)\\b([\\s\\S]*?)\\/?>$/)\n if (!match) return null\n\n return {\n closing: !!match[1],\n name: match[2].toLowerCase(),\n attributes: match[3] ?? \"\",\n }\n}\n\nfunction findDangerousClose(html: string, tagName: string, fromIndex: number): number {\n const closePattern = new RegExp(`</\\\\s*${tagName}\\\\s*>`, \"ig\")\n closePattern.lastIndex = fromIndex\n const match = closePattern.exec(html)\n return match ? closePattern.lastIndex : -1\n}\n\n/**\n * Conservative, deterministic sanitizer for user/email supplied HTML rendered by\n * design-system components. It keeps common email formatting tags while removing\n * executable tags, event handlers, unsafe inline styles, and unsafe URLs. This stays\n * dependency-free for the shared package and intentionally favors stripping\n * ambiguous email content over preserving every possible HTML feature.\n */\nexport function sanitizeHtml(html: string): string {\n let output = \"\"\n let cursor = 0\n\n while (cursor < html.length) {\n const tagStart = html.indexOf(\"<\", cursor)\n if (tagStart === -1) {\n output += html.slice(cursor)\n break\n }\n\n output += html.slice(cursor, tagStart)\n\n if (html.startsWith(\"<!--\", tagStart)) {\n const commentEnd = html.indexOf(\"-->\", tagStart + 4)\n cursor = commentEnd === -1 ? html.length : commentEnd + 3\n continue\n }\n\n const tagEnd = findTagEnd(html, tagStart)\n if (tagEnd === -1) {\n output += escapeHtml(html.slice(tagStart))\n break\n }\n\n const rawTag = html.slice(tagStart, tagEnd + 1)\n const parsed = parseTag(rawTag)\n if (!parsed) {\n cursor = tagEnd + 1\n continue\n }\n\n if (DANGEROUS_BLOCK_TAGS.has(parsed.name)) {\n const closeEnd = parsed.closing ? -1 : findDangerousClose(html, parsed.name, tagEnd + 1)\n cursor = closeEnd === -1 ? tagEnd + 1 : closeEnd\n continue\n }\n\n if (ALLOWED_TAGS.has(parsed.name)) {\n if (parsed.closing) {\n if (!VOID_TAGS.has(parsed.name)) output += `</${parsed.name}>`\n } else {\n output += `<${parsed.name}${sanitizeAttributes(parsed.name, parsed.attributes)}>`\n }\n }\n\n cursor = tagEnd + 1\n }\n\n return output\n}\n\nexport function htmlToTextSnippet(html: string, maxLength = 140): string {\n return sanitizeHtml(html)\n .replace(/<[^>]+>/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim()\n .slice(0, maxLength)\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handled-ai/design-system",
3
- "version": "0.20.22",
3
+ "version": "0.20.23",
4
4
  "description": "Handled UI component library (shadcn-style, New York)",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@9.12.0",
@@ -366,11 +366,50 @@ describe("ConversationPanel", () => {
366
366
  fireEvent.click(screen.getByTitle("Show quoted text"));
367
367
  const quoted = container.querySelector("blockquote.gmail_quote")!;
368
368
  expect(quoted).not.toBeNull();
369
- expect(quoted.outerHTML).not.toContain("style=");
369
+ // Presentational color survives sanitization (signature-fidelity
370
+ // allowlist); the unsafe data: URL is still gone.
371
+ expect(quoted.outerHTML).toContain('style="color: red"');
370
372
  expect(quoted.outerHTML).not.toContain("data:text/html");
371
373
  });
372
374
 
373
375
 
376
+ it("renders an explicit signatureHtml collapsed behind the details toggle, preserving authored styles", () => {
377
+ const sigThread = thread({
378
+ messages: [
379
+ {
380
+ id: "m1",
381
+ direction: "outbound",
382
+ from: me,
383
+ to: priya,
384
+ date: "Today",
385
+ receipt: { kind: "draft", label: "Draft" },
386
+ bodyHtml: "<p>Hi Priya,</p><p>Want to see if the math works?</p>",
387
+ signatureHtml:
388
+ '<div class="gmail_signature"><p style="font-style: italic; font-family: \'pt serif\', serif; color: rgb(102, 102, 102)">2261 Market Street, Suite 86807</p><p>Mercury is a fintech company, not an FDIC-insured bank.</p></div>',
389
+ },
390
+ ],
391
+ });
392
+
393
+ const { container } = render(<ConversationPanel threads={[sigThread]} me={me} />);
394
+ const message = container.querySelector('[data-slot="conv-message"]')!;
395
+
396
+ // Signature is collapsed by default — body visible, signature not.
397
+ expect(message.querySelector('[data-slot="conv-message-body"]')?.textContent).toContain("Want to see if the math works?");
398
+ expect(message.textContent).not.toContain("2261 Market Street");
399
+ expect(message.querySelector('[data-slot="email-body-details"]')).toBeNull();
400
+
401
+ fireEvent.click(screen.getByRole("button", { name: "•••" }));
402
+
403
+ const details = message.querySelector('[data-slot="email-body-details"]')!;
404
+ expect(details.textContent).toContain("2261 Market Street, Suite 86807");
405
+ expect(details.textContent).toContain("FDIC-insured");
406
+ // Authored Gmail-look styles survive into the rendered signature.
407
+ const styled = details.querySelector("p[style]")!;
408
+ expect(styled.getAttribute("style")).toContain("font-style: italic");
409
+ expect(styled.getAttribute("style")).toContain("font-family: 'pt serif', serif");
410
+ expect(styled.getAttribute("style")).toContain("color: rgb(102, 102, 102)");
411
+ });
412
+
374
413
  it("collapses trailing HTML signature and disclaimer behind a details toggle", () => {
375
414
  const htmlThread = thread({
376
415
  messages: [
@@ -99,6 +99,15 @@ export interface ConvMessage {
99
99
  receipt?: { kind: "new" | "read" | "opened" | "sent" | "draft"; label: string }
100
100
  /** HTML body (preferred). Sanitized by the component before rendering. */
101
101
  bodyHtml?: string
102
+ /**
103
+ * Pre-split signature HTML for this message, sanitized server-side by the
104
+ * sender's signature pipeline (which preserves the authored Gmail look —
105
+ * fonts, colors, logos). When present it renders inside the collapsed
106
+ * details section ("•••" toggle) instead of relying on the heuristic footer
107
+ * split of `bodyHtml`, so long signatures never expand the thread by
108
+ * default.
109
+ */
110
+ signatureHtml?: string | null
102
111
  /** Plain-text fallback when `bodyHtml` is absent. */
103
112
  body?: string
104
113
  /** Quoted prior message, collapsed behind a toggle. Sanitized before rendering. */
@@ -470,6 +479,7 @@ function MessageView({
470
479
  <EmailBody
471
480
  html={message.bodyHtml}
472
481
  text={message.body}
482
+ detailsHtml={message.signatureHtml ?? undefined}
473
483
  variant="history"
474
484
  collapseDetails={true}
475
485
  className="text-sm"
@@ -2,12 +2,14 @@ import { describe, expect, it } from "vitest"
2
2
  import { htmlToTextSnippet, sanitizeHtml } from "../safe-html"
3
3
 
4
4
  describe("sanitizeHtml", () => {
5
- it("removes executable tags, event handlers, styles, and unsafe urls", () => {
5
+ it("removes executable tags, event handlers, unsafe styles, and unsafe urls", () => {
6
6
  const html = sanitizeHtml(
7
- '<p style="color:red" onclick="alert(1)">Hi<script>alert(1)</script><iframe src="https://evil.test"></iframe><a href="java&#x3a;script:alert(1)">bad</a><img src="data:text/html,boom" onerror="alert(1)"></p>',
7
+ '<p style="color:red; background-image: url(https://evil.test/x)" onclick="alert(1)">Hi<script>alert(1)</script><iframe src="https://evil.test"></iframe><a href="java&#x3a;script:alert(1)">bad</a><img src="data:text/html,boom" onerror="alert(1)"></p>',
8
8
  )
9
9
 
10
- expect(html).toBe('<p>Hi<a>bad</a><img></p>')
10
+ // Presentational color survives (signature fidelity); the url()-bearing
11
+ // declaration and everything executable is gone.
12
+ expect(html).toBe('<p style="color: red">Hi<a>bad</a><img></p>')
11
13
  })
12
14
 
13
15
  it("removes svg, math, and other active embedded content", () => {
@@ -60,12 +62,29 @@ describe("sanitizeHtml", () => {
60
62
  expect(html).toContain('class="gmail_signature"')
61
63
  expect(html).toContain('<table><tbody><tr><td><img src="https://example.com/logo.png" width="120" height="48" alt="Acme &amp; Co"></td>')
62
64
  expect(html).toContain('<sup>TM</sup><sub>LLC</sub>')
63
- expect(html).toContain('style="vertical-align: super; font-size: 10px"')
65
+ // Authored presentational styles now survive (signature Gmail-look
66
+ // fidelity); url()-bearing declarations are still dropped.
67
+ expect(html).toContain('style="vertical-align: super; font-size: 10px; color: red"')
64
68
  expect(html).not.toContain("onerror")
65
- expect(html).not.toContain("color: red")
66
69
  expect(html).not.toContain("background-image")
67
70
  })
68
71
 
72
+ it("preserves authored signature typography and spacing styles, dropping unknown properties", () => {
73
+ const html = sanitizeHtml(
74
+ '<p style="font-family: \'pt serif\', serif; font-style: italic; font-size: 12px; color: rgb(102, 102, 102); line-height: 1.38; margin: 0 0 8px 0; position: fixed; z-index: 9999">2261 Market Street</p>',
75
+ )
76
+
77
+ expect(html).toContain("font-family: 'pt serif', serif")
78
+ expect(html).toContain("font-style: italic")
79
+ expect(html).toContain("font-size: 12px")
80
+ expect(html).toContain("color: rgb(102, 102, 102)")
81
+ expect(html).toContain("line-height: 1.38")
82
+ expect(html).toContain("margin: 0 0 8px 0")
83
+ // Layout-escape properties are not allowlisted.
84
+ expect(html).not.toContain("position")
85
+ expect(html).not.toContain("z-index")
86
+ })
87
+
69
88
  it("bounds image dimensions and inline font-size while preserving subscript alignment", () => {
70
89
  const html = sanitizeHtml(
71
90
  '<span style="vertical-align: sub; font-size: 500px">H2O</span><img src="https://example.com/x.png" width="99999" height="64" alt="x">',
@@ -147,6 +147,39 @@ function sanitizeFontSize(value: string): string | null {
147
147
  return `${amount}${unit}`
148
148
  }
149
149
 
150
+ // Presentational properties preserved so email signatures keep their authored
151
+ // Gmail look (serif/italic/gray etc.) when rendered in the conversation panel.
152
+ // Values are validated per-property; anything containing url()/expression()
153
+ // (or any property not listed) is still dropped — these are text-presentation
154
+ // only, no layout escape or resource loading.
155
+ const COLOR_VALUE = /^(#[0-9a-f]{3,8}|rgba?\([\d\s,./%]+\)|[a-z]+)$/
156
+ const LENGTH_BOX_VALUE = /^(-?[\d.]+(px|em|rem|%)?|auto|0)(\s+(-?[\d.]+(px|em|rem|%)?|auto|0)){0,3}$/
157
+ const SAFE_TEXT_STYLE_PROPS: Record<string, RegExp> = {
158
+ color: COLOR_VALUE,
159
+ "background-color": COLOR_VALUE,
160
+ "font-family": /^[a-z0-9\s,'"-]+$/,
161
+ "font-style": /^(italic|normal|oblique)$/,
162
+ "font-weight": /^(bold|bolder|lighter|normal|[1-9]00)$/,
163
+ "font-variant": /^[a-z\s-]+$/,
164
+ "text-decoration": /^[a-z\s-]+$/,
165
+ "text-decoration-line": /^[a-z\s-]+$/,
166
+ "text-transform": /^(none|capitalize|uppercase|lowercase)$/,
167
+ "text-align": /^(left|right|center|justify)$/,
168
+ "letter-spacing": /^(normal|-?[\d.]+(px|em|rem)?)$/,
169
+ "line-height": /^(normal|[\d.]+(px|em|rem|%)?)$/,
170
+ "white-space": /^[a-z-]+$/,
171
+ margin: LENGTH_BOX_VALUE,
172
+ "margin-top": LENGTH_BOX_VALUE,
173
+ "margin-right": LENGTH_BOX_VALUE,
174
+ "margin-bottom": LENGTH_BOX_VALUE,
175
+ "margin-left": LENGTH_BOX_VALUE,
176
+ padding: LENGTH_BOX_VALUE,
177
+ "padding-top": LENGTH_BOX_VALUE,
178
+ "padding-right": LENGTH_BOX_VALUE,
179
+ "padding-bottom": LENGTH_BOX_VALUE,
180
+ "padding-left": LENGTH_BOX_VALUE,
181
+ }
182
+
150
183
  function sanitizeStyle(value: string): string | null {
151
184
  const declarations: string[] = []
152
185
 
@@ -166,6 +199,12 @@ function sanitizeStyle(value: string): string | null {
166
199
  if (property === "font-size") {
167
200
  const fontSize = sanitizeFontSize(rawValue)
168
201
  if (fontSize) declarations.push(`font-size: ${fontSize}`)
202
+ continue
203
+ }
204
+
205
+ const allowedValue = SAFE_TEXT_STYLE_PROPS[property]
206
+ if (allowedValue?.test(rawValue)) {
207
+ declarations.push(`${property}: ${rawValue}`)
169
208
  }
170
209
  }
171
210