@handled-ai/design-system 0.20.23 → 0.20.25

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.
@@ -23,7 +23,9 @@ interface SplitEmailTextResult {
23
23
  declare function decodeEmailDisplayText(value: string): string;
24
24
  declare function normalizeEmailSender(input: EmailDisplaySenderInput): NormalizedEmailSender;
25
25
  declare function formatAddressList(input?: string | string[] | null): string;
26
- declare function formatEmailTimestamp(value?: string | Date | null): string | null;
26
+ declare function formatEmailTimestamp(value?: string | Date | null, opts?: {
27
+ timeZone?: string;
28
+ }): string | null;
27
29
  declare function splitEmailHtmlForDisplay(html: string): SplitEmailHtmlResult;
28
30
  declare function splitEmailTextForDisplay(text: string): SplitEmailTextResult;
29
31
  declare function emailBodySnippet(input: {
@@ -1,3 +1,19 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
3
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
5
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
+ var __spreadValues = (a, b) => {
7
+ for (var prop in b || (b = {}))
8
+ if (__hasOwnProp.call(b, prop))
9
+ __defNormalProp(a, prop, b[prop]);
10
+ if (__getOwnPropSymbols)
11
+ for (var prop of __getOwnPropSymbols(b)) {
12
+ if (__propIsEnum.call(b, prop))
13
+ __defNormalProp(a, prop, b[prop]);
14
+ }
15
+ return a;
16
+ };
1
17
  import { htmlToTextSnippet, sanitizeHtml } from "../internal/safe-html.js";
2
18
  const HTML_ENTITY_RE = /&(?:#x[0-9a-f]+|#\d+|[a-z][a-z0-9]+);?/i;
3
19
  const EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i;
@@ -145,18 +161,17 @@ function formatAddressList(input) {
145
161
  const rawItems = Array.isArray(input) ? input : splitAddressList(input);
146
162
  return rawItems.map((item) => formatSingleAddress(item)).filter(Boolean).join(", ");
147
163
  }
148
- function formatEmailTimestamp(value) {
164
+ function formatEmailTimestamp(value, opts) {
149
165
  if (!value) return null;
150
166
  const date = value instanceof Date ? value : new Date(value);
151
167
  if (Number.isNaN(date.getTime())) return null;
152
- return new Intl.DateTimeFormat("en-US", {
168
+ return new Intl.DateTimeFormat("en-US", __spreadValues({
153
169
  month: "short",
154
170
  day: "numeric",
155
171
  year: "numeric",
156
172
  hour: "numeric",
157
- minute: "2-digit",
158
- timeZone: "UTC"
159
- }).format(date);
173
+ minute: "2-digit"
174
+ }, (opts == null ? void 0 : opts.timeZone) ? { timeZone: opts.timeZone } : {})).format(date);
160
175
  }
161
176
  function escapeHtmlText(value) {
162
177
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -212,6 +227,12 @@ function makeHtmlSegment(html) {
212
227
  if (!html.trim() || !visibleText && !/<(?:img|hr)\b/i.test(html)) return null;
213
228
  return { html, visibleText };
214
229
  }
230
+ function makeBlankLineSegment(html) {
231
+ return { html, visibleText: "" };
232
+ }
233
+ function isBlankLineHtml(html) {
234
+ return Boolean(html.trim()) && !htmlToVisibleText(html) && !/<(?:img|hr)\b/i.test(html);
235
+ }
215
236
  function splitInlineNodes(nodes, wrapper) {
216
237
  const containsBr = hasDirectBr(nodes);
217
238
  const chunks = [[]];
@@ -223,10 +244,20 @@ function splitInlineNodes(nodes, wrapper) {
223
244
  }
224
245
  (_a = chunks[chunks.length - 1]) == null ? void 0 : _a.push(serializeNode(node));
225
246
  });
226
- return chunks.map((chunk) => chunk.join("")).map((innerHtml) => {
227
- if (wrapper) return wrapHtmlLike(wrapper, innerHtml);
228
- return containsBr ? `<div>${innerHtml}</div>` : innerHtml;
229
- }).map(makeHtmlSegment).filter((segment) => Boolean(segment));
247
+ const rendered = chunks.map((chunk) => chunk.join(""));
248
+ const segments = [];
249
+ rendered.forEach((innerHtml, index) => {
250
+ const html = wrapper ? wrapHtmlLike(wrapper, innerHtml) : containsBr ? `<div>${innerHtml}</div>` : innerHtml;
251
+ const segment = makeHtmlSegment(html);
252
+ if (segment) {
253
+ segments.push(segment);
254
+ return;
255
+ }
256
+ if (containsBr && index > 0 && index < rendered.length - 1) {
257
+ segments.push(makeBlankLineSegment(wrapper ? wrapHtmlLike(wrapper, "<br>") : "<div><br></div>"));
258
+ }
259
+ });
260
+ return segments;
230
261
  }
231
262
  function splitElementSegment(element) {
232
263
  const tagName = element.tagName.toLowerCase();
@@ -234,6 +265,9 @@ function splitElementSegment(element) {
234
265
  const segment2 = makeHtmlSegment(element.outerHTML);
235
266
  return segment2 ? [segment2] : [];
236
267
  }
268
+ if (isBlankLineHtml(element.outerHTML)) {
269
+ return [makeBlankLineSegment(element.outerHTML)];
270
+ }
237
271
  if (tagName === "div" && hasDirectBlockChild(element)) {
238
272
  const childSegments = splitHtmlNodes(Array.from(element.childNodes));
239
273
  return childSegments.length ? childSegments : [makeHtmlSegment(element.outerHTML)].filter(Boolean);
@@ -287,9 +321,15 @@ function splitHtmlSegmentsFallback(html) {
287
321
  const pushInline = (inlineHtml) => {
288
322
  const chunks = inlineHtml.split(BR_TAG_RE);
289
323
  const hadBr = chunks.length > 1;
290
- chunks.forEach((chunk) => {
324
+ chunks.forEach((chunk, index) => {
291
325
  const segment = makeHtmlSegment(hadBr ? `<div>${chunk}</div>` : chunk);
292
- if (segment) segments.push(segment);
326
+ if (segment) {
327
+ segments.push(segment);
328
+ return;
329
+ }
330
+ if (hadBr && index > 0 && index < chunks.length - 1) {
331
+ segments.push(makeBlankLineSegment("<div><br /></div>"));
332
+ }
293
333
  });
294
334
  };
295
335
  while (cursor < html.length) {
@@ -306,13 +346,22 @@ function splitHtmlSegmentsFallback(html) {
306
346
  const openTagEnd = tagStart + rawOpen.length - 1;
307
347
  const segmentEnd = findMatchingCloseTag(html, tagName, openTagEnd);
308
348
  const blockHtml = html.slice(tagStart, segmentEnd);
309
- if (SPLITTABLE_BLOCK_TAGS.has(tagName) && BR_TAG_RE.test(blockHtml)) {
349
+ if (isBlankLineHtml(blockHtml)) {
350
+ segments.push(makeBlankLineSegment(blockHtml));
351
+ } else if (SPLITTABLE_BLOCK_TAGS.has(tagName) && BR_TAG_RE.test(blockHtml)) {
310
352
  const openTag = rawOpen;
311
353
  const closeTag = `</${tagName}>`;
312
354
  const inner = blockHtml.replace(new RegExp(`^${rawOpen.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i"), "").replace(new RegExp(`${closeTag}$`, "i"), "");
313
- inner.split(BR_TAG_RE).forEach((chunk) => {
355
+ const innerChunks = inner.split(BR_TAG_RE);
356
+ innerChunks.forEach((chunk, index) => {
314
357
  const segment = makeHtmlSegment(`${openTag}${chunk}${closeTag}`);
315
- if (segment) segments.push(segment);
358
+ if (segment) {
359
+ segments.push(segment);
360
+ return;
361
+ }
362
+ if (index > 0 && index < innerChunks.length - 1) {
363
+ segments.push(makeBlankLineSegment(`${openTag}<br />${closeTag}`));
364
+ }
316
365
  });
317
366
  } else {
318
367
  const segment = makeHtmlSegment(blockHtml);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/email-display-helpers.ts"],"sourcesContent":["import { htmlToTextSnippet, sanitizeHtml } from \"../internal/safe-html\"\n\nexport interface NormalizedEmailSender {\n name: string\n email: string | null\n}\n\nexport interface EmailDisplaySenderInput {\n name?: string | null\n email?: string | null\n fallbackName?: string\n}\n\nexport interface SplitEmailHtmlResult {\n bodyHtml: string\n detailsHtml: string\n}\n\nexport interface SplitEmailTextResult {\n bodyText: string\n detailsText: string\n}\n\nconst HTML_ENTITY_RE = /&(?:#x[0-9a-f]+|#\\d+|[a-z][a-z0-9]+);?/i\nconst EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i\nconst ANGLE_ADDRESS_RE = /^\\s*(.*?)\\s*<\\s*([^<>\\s]+@[^<>\\s]+)\\s*>\\s*$/\nconst BR_TAG_RE = /<br\\s*\\/?>/gi\n\nconst SPLITTABLE_BLOCK_TAGS = new Set([\"p\", \"div\"])\nconst WHOLE_BLOCK_TAGS = new Set([\"blockquote\", \"table\", \"ul\", \"ol\", \"hr\"])\nconst BLOCK_TAGS = new Set([...SPLITTABLE_BLOCK_TAGS, ...WHOLE_BLOCK_TAGS])\nconst HTML_BLOCK_START_RE = /<(p|div|blockquote|table|ul|ol|hr)\\b[^>]*>/i\n\nconst SIGNATURE_DELIMITER_RE = /^--\\s*$/\nconst GMAIL_SIGNATURE_RE = /\\b(?:gmail_signature|gmail_signature_prefix|gmail_extra)\\b/i\nconst GMAIL_QUOTE_RE = /<blockquote\\b[^>]*\\bclass=[\"'][^\"']*\\bgmail_quote\\b/i\nconst ON_WROTE_RE = /^On\\s.+wrote:\\s*$/i\nconst SIGNOFF_RE = /^(?:thanks,|thank you,|best,|regards,|sincerely,)$/i\nconst DETAILS_START_RE = /^(?:confidentiality notice\\b|this message and any attachments\\b|this email and any attachments\\b|the information contained in this message\\b|this communication may contain\\b|unsubscribe\\b|manage your preferences\\b)/i\nconst CONTACT_DETAIL_RE = /(?:@|https?:\\/\\/|www\\.|\\+?\\d[\\d\\s().-]{6,}\\d|\\b(?:ceo|cfo|cto|coo|founder|co-founder|director|manager|vp|vice president|president|head of|sales|marketing|operations|account|customer success|success|support|engineer|consultant|partner|principal|advisor|associate)\\b|\\b(?:inc|llc|ltd|corp|corporation|company|co\\.)\\b)/i\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 nbsp: \" \",\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) => safeCodePoint(Number.parseInt(hex, 16)))\n .replace(/&#(\\d+);?/g, (_match, decimal: string) => safeCodePoint(Number.parseInt(decimal, 10)))\n .replace(/&([a-z][a-z0-9]+);?/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 decodeJsonEscapes(value: string): string {\n return value\n .replace(/\\\\u\\{([0-9a-f]{1,6})\\}/gi, (_match, hex: string) => safeCodePoint(Number.parseInt(hex, 16)))\n .replace(/\\\\u([0-9a-f]{4})/gi, (_match, hex: string) => safeCodePoint(Number.parseInt(hex, 16)))\n .replace(/\\\\r\\\\n|\\\\n|\\\\r/g, \"\\n\")\n .replace(/\\\\t/g, \"\\t\")\n .replace(/\\\\([\"'\\\\/])/g, \"$1\")\n}\n\nfunction maybeParseJsonString(value: string): string {\n const trimmed = value.trim()\n if (!trimmed.startsWith('\"') || !trimmed.endsWith('\"')) return value\n if (!/[\\\\&]/.test(trimmed)) return value\n\n try {\n const parsed = JSON.parse(trimmed) as unknown\n return typeof parsed === \"string\" ? parsed : value\n } catch {\n return value\n }\n}\n\n/**\n * Decodes display-only email text so UI labels, body fallbacks, collapsed rows,\n * and snippets do not show HTML entities or JSON escape artifacts. It does not\n * sanitize HTML; sanitize at the HTML render boundary before using markup.\n */\nexport function decodeEmailDisplayText(value: string): string {\n let decoded = maybeParseJsonString(value).replace(/\\r\\n?/g, \"\\n\")\n\n for (let i = 0; i < 4; i += 1) {\n const next = decodeHtmlEntities(decodeJsonEscapes(decoded))\n if (next === decoded) break\n decoded = next\n }\n\n return decoded.replace(/\\u00a0/g, \" \")\n}\n\nfunction stripWrappingQuotes(value: string): string {\n const trimmed = value.trim()\n if ((trimmed.startsWith('\"') && trimmed.endsWith('\"')) || (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\"))) {\n return trimmed.slice(1, -1).trim()\n }\n return trimmed\n}\n\nfunction extractEmail(value: string): string | null {\n const decoded = decodeEmailDisplayText(value)\n const angleMatch = decoded.match(ANGLE_ADDRESS_RE)\n const email = angleMatch?.[2] ?? decoded.match(EMAIL_RE)?.[0]\n return email ? email.trim() : null\n}\n\nfunction extractNameFromAddress(value: string): string {\n const decoded = decodeEmailDisplayText(value)\n const angleMatch = decoded.match(ANGLE_ADDRESS_RE)\n if (angleMatch) return stripWrappingQuotes(angleMatch[1] ?? \"\")\n return stripWrappingQuotes(decoded.replace(EMAIL_RE, \"\").replace(/[<>]/g, \"\").trim())\n}\n\nfunction cleanDisplayName(value: string, email: string | null): string {\n let name = stripWrappingQuotes(decodeEmailDisplayText(value))\n if (email) {\n name = name\n .replace(new RegExp(`<\\\\s*${email.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}\\\\s*>`, \"i\"), \"\")\n .replace(new RegExp(`^${email.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}$`, \"i\"), \"\")\n .trim()\n }\n return stripWrappingQuotes(name)\n}\n\nexport function normalizeEmailSender(input: EmailDisplaySenderInput): NormalizedEmailSender {\n const fallbackName = decodeEmailDisplayText(input.fallbackName ?? \"Unknown sender\").trim() || \"Unknown sender\"\n const rawName = input.name ? decodeEmailDisplayText(input.name) : \"\"\n const rawEmail = input.email ? decodeEmailDisplayText(input.email) : \"\"\n const email = extractEmail(rawEmail) ?? extractEmail(rawName)\n\n const nameFromName = rawName ? cleanDisplayName(extractNameFromAddress(rawName) || rawName, email) : \"\"\n const nameFromEmail = rawEmail ? cleanDisplayName(extractNameFromAddress(rawEmail), email) : \"\"\n const name = nameFromName || nameFromEmail || email || fallbackName\n\n return { name, email }\n}\n\nfunction splitAddressList(value: string): string[] {\n const parts: string[] = []\n let current = \"\"\n let quote: '\"' | \"'\" | null = null\n let angleDepth = 0\n\n for (let index = 0; index < value.length; index += 1) {\n const char = value[index]\n\n if (quote) {\n current += char\n if (char === quote && value[index - 1] !== \"\\\\\") quote = null\n continue\n }\n\n if (char === '\"' || char === \"'\") {\n quote = char\n current += char\n continue\n }\n\n if (char === \"<\") angleDepth += 1\n if (char === \">\" && angleDepth > 0) angleDepth -= 1\n\n if (char === \",\" && angleDepth === 0) {\n if (current.trim()) parts.push(current.trim())\n current = \"\"\n continue\n }\n\n current += char\n }\n\n if (current.trim()) parts.push(current.trim())\n return parts\n}\n\nfunction formatSingleAddress(value: string): string {\n const decoded = decodeEmailDisplayText(value).trim()\n if (!decoded) return \"\"\n\n const email = extractEmail(decoded)\n const name = cleanDisplayName(extractNameFromAddress(decoded), email)\n\n if (email && name) return `${name} <${email}>`\n if (email) return email\n return stripWrappingQuotes(decoded)\n}\n\nexport function formatAddressList(input?: string | string[] | null): string {\n if (!input) return \"\"\n\n const rawItems = Array.isArray(input) ? input : splitAddressList(input)\n return rawItems\n .map((item) => formatSingleAddress(item))\n .filter(Boolean)\n .join(\", \")\n}\n\nexport function formatEmailTimestamp(value?: string | Date | null): string | null {\n if (!value) return null\n const date = value instanceof Date ? value : new Date(value)\n if (Number.isNaN(date.getTime())) return null\n\n return new Intl.DateTimeFormat(\"en-US\", {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n timeZone: \"UTC\",\n }).format(date)\n}\n\ntype MessageSegment = { html?: string; text?: string; visibleText: string }\n\nfunction escapeHtmlText(value: string): string {\n return value.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\")\n}\n\nfunction decodeHtmlTextNodes(html: string): string {\n if (!HTML_ENTITY_RE.test(html) && !/\\\\[nrt\"'\\\\/]|\\\\u/i.test(html)) return html\n\n if (typeof document !== \"undefined\") {\n const template = document.createElement(\"template\")\n template.innerHTML = html\n const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_TEXT)\n const textNodes: Text[] = []\n let node = walker.nextNode()\n while (node) {\n textNodes.push(node as Text)\n node = walker.nextNode()\n }\n textNodes.forEach((textNode) => {\n textNode.nodeValue = decodeEmailDisplayText(textNode.nodeValue ?? \"\")\n })\n return template.innerHTML\n }\n\n return html\n .split(/(<[^>]+>)/g)\n .map((part) => (part.startsWith(\"<\") && part.endsWith(\">\") ? part : escapeHtmlText(decodeEmailDisplayText(part))))\n .join(\"\")\n}\n\nfunction decodeHtmlText(value: string): string {\n const withoutTags = value\n .replace(BR_TAG_RE, \"\\n\")\n .replace(/<\\/(p|div|blockquote|li|tr|table|ul|ol)>/gi, \"\\n\")\n .replace(/<[^>]*>/g, \"\")\n\n return decodeEmailDisplayText(withoutTags)\n}\n\nfunction htmlToVisibleText(html: string): string {\n return decodeHtmlText(html).replace(/\\u00a0/g, \" \").replace(/[ \\t]+/g, \" \").trim()\n}\n\nfunction serializeNode(node: Node): string {\n const host = document.createElement(\"div\")\n host.appendChild(node.cloneNode(true))\n return host.innerHTML\n}\n\nfunction wrapHtmlLike(element: Element, innerHtml: string): string {\n const clone = element.cloneNode(false) as HTMLElement\n clone.innerHTML = innerHtml\n return clone.outerHTML\n}\n\nfunction hasDirectBr(nodes: readonly Node[]): boolean {\n return nodes.some((node) => node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName.toLowerCase() === \"br\")\n}\n\nfunction hasDirectBlockChild(element: Element): boolean {\n return Array.from(element.children).some((child) => {\n const tagName = child.tagName.toLowerCase()\n return tagName !== \"br\" && BLOCK_TAGS.has(tagName)\n })\n}\n\nfunction makeHtmlSegment(html: string): MessageSegment | null {\n const visibleText = htmlToVisibleText(html)\n if (!html.trim() || (!visibleText && !/<(?:img|hr)\\b/i.test(html))) return null\n return { html, visibleText }\n}\n\nfunction splitInlineNodes(nodes: readonly Node[], wrapper?: Element): MessageSegment[] {\n const containsBr = hasDirectBr(nodes)\n const chunks: string[][] = [[]]\n\n nodes.forEach((node) => {\n if (node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName.toLowerCase() === \"br\") {\n chunks.push([])\n return\n }\n\n chunks[chunks.length - 1]?.push(serializeNode(node))\n })\n\n return chunks\n .map((chunk) => chunk.join(\"\"))\n .map((innerHtml) => {\n if (wrapper) return wrapHtmlLike(wrapper, innerHtml)\n return containsBr ? `<div>${innerHtml}</div>` : innerHtml\n })\n .map(makeHtmlSegment)\n .filter((segment): segment is MessageSegment => Boolean(segment))\n}\n\nfunction splitElementSegment(element: Element): MessageSegment[] {\n const tagName = element.tagName.toLowerCase()\n\n if (GMAIL_SIGNATURE_RE.test(element.outerHTML) || GMAIL_QUOTE_RE.test(element.outerHTML)) {\n const segment = makeHtmlSegment(element.outerHTML)\n return segment ? [segment] : []\n }\n\n if (tagName === \"div\" && hasDirectBlockChild(element)) {\n const childSegments = splitHtmlNodes(Array.from(element.childNodes))\n return childSegments.length ? childSegments : ([makeHtmlSegment(element.outerHTML)].filter(Boolean) as MessageSegment[])\n }\n\n if (SPLITTABLE_BLOCK_TAGS.has(tagName) && hasDirectBr(Array.from(element.childNodes)) && !hasDirectBlockChild(element)) {\n return splitInlineNodes(Array.from(element.childNodes), element)\n }\n\n const segment = makeHtmlSegment(element.outerHTML)\n return segment ? [segment] : []\n}\n\nfunction splitHtmlNodes(nodes: readonly Node[]): MessageSegment[] {\n const segments: MessageSegment[] = []\n let inlineNodes: Node[] = []\n\n const flushInline = () => {\n if (!inlineNodes.length) return\n segments.push(...splitInlineNodes(inlineNodes))\n inlineNodes = []\n }\n\n nodes.forEach((node) => {\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as Element\n const tagName = element.tagName.toLowerCase()\n if (BLOCK_TAGS.has(tagName)) {\n flushInline()\n segments.push(...splitElementSegment(element))\n return\n }\n }\n\n inlineNodes.push(node)\n })\n\n flushInline()\n return segments\n}\n\nfunction findMatchingCloseTag(html: string, tagName: string, openTagEnd: number): number {\n if (tagName === \"hr\") return openTagEnd + 1\n\n const tagPattern = new RegExp(`</?${tagName}\\\\b[^>]*>`, \"gi\")\n tagPattern.lastIndex = openTagEnd + 1\n let depth = 1\n let match: RegExpExecArray | null\n\n while ((match = tagPattern.exec(html)) !== null) {\n const rawTag = match[0]\n if (/^<\\//.test(rawTag)) depth -= 1\n else if (!/\\/\\s*>$/.test(rawTag)) depth += 1\n if (depth === 0) return tagPattern.lastIndex\n }\n\n return html.length\n}\n\nfunction splitHtmlSegmentsFallback(html: string): MessageSegment[] {\n const segments: MessageSegment[] = []\n let cursor = 0\n\n const pushInline = (inlineHtml: string) => {\n const chunks = inlineHtml.split(BR_TAG_RE)\n const hadBr = chunks.length > 1\n chunks.forEach((chunk) => {\n const segment = makeHtmlSegment(hadBr ? `<div>${chunk}</div>` : chunk)\n if (segment) segments.push(segment)\n })\n }\n\n while (cursor < html.length) {\n const rest = html.slice(cursor)\n const match = HTML_BLOCK_START_RE.exec(rest)\n if (!match || match.index === undefined) {\n pushInline(rest)\n break\n }\n\n if (match.index > 0) pushInline(rest.slice(0, match.index))\n\n const tagStart = cursor + match.index\n const rawOpen = match[0]\n const tagName = match[1].toLowerCase()\n const openTagEnd = tagStart + rawOpen.length - 1\n const segmentEnd = findMatchingCloseTag(html, tagName, openTagEnd)\n const blockHtml = html.slice(tagStart, segmentEnd)\n\n if (SPLITTABLE_BLOCK_TAGS.has(tagName) && BR_TAG_RE.test(blockHtml)) {\n const openTag = rawOpen\n const closeTag = `</${tagName}>`\n const inner = blockHtml.replace(new RegExp(`^${rawOpen.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}`, \"i\"), \"\").replace(new RegExp(`${closeTag}$`, \"i\"), \"\")\n inner.split(BR_TAG_RE).forEach((chunk) => {\n const segment = makeHtmlSegment(`${openTag}${chunk}${closeTag}`)\n if (segment) segments.push(segment)\n })\n } else {\n const segment = makeHtmlSegment(blockHtml)\n if (segment) segments.push(segment)\n }\n\n cursor = segmentEnd\n }\n\n return segments\n}\n\nfunction segmentHtmlMessage(html: string): MessageSegment[] {\n if (typeof document === \"undefined\" || typeof Node === \"undefined\") {\n return splitHtmlSegmentsFallback(html)\n }\n\n const template = document.createElement(\"template\")\n template.innerHTML = html\n return splitHtmlNodes(Array.from(template.content.childNodes))\n}\n\nfunction segmentTextMessage(text: string): MessageSegment[] {\n const normalized = decodeEmailDisplayText(text)\n const lines: string[] = normalized.match(/[^\\n]*(?:\\n|$)/g) ?? []\n return lines\n .filter((line, index) => line.length > 0 && !(index === lines.length - 1 && line === \"\"))\n .map((line) => ({ text: line, visibleText: line.replace(/\\u00a0/g, \" \").trim() }))\n}\n\nfunction firstVisibleLine(text: string): string {\n return text.replace(/\\u00a0/g, \" \").trimStart().split(/\\r?\\n/).find((line) => line.trim())?.trim() ?? \"\"\n}\n\nfunction isLikelySenderNameLine(line: string): boolean {\n if (!line || line.length > 60) return false\n if (/[,@:;!?]|https?:\\/\\/|www\\.|\\d/.test(line)) return false\n\n const words = line.split(/\\s+/).filter(Boolean)\n if (words.length < 1 || words.length > 4) return false\n\n return words.every((word) => /^[A-Z][A-Za-z'.-]*$/.test(word))\n}\n\nfunction nextVisibleSegmentText(segments: MessageSegment[], fromIndex: number): string {\n for (let index = fromIndex + 1; index < segments.length; index += 1) {\n const text = firstVisibleLine(segments[index]?.visibleText ?? \"\")\n if (text) return text\n }\n return \"\"\n}\n\nfunction isGmailDetailsSegment(segment: MessageSegment): boolean {\n return Boolean(segment.html && (GMAIL_SIGNATURE_RE.test(segment.html) || GMAIL_QUOTE_RE.test(segment.html)))\n}\n\nfunction isFooterBoundary(segments: MessageSegment[], index: number): boolean {\n const segment = segments[index]\n if (!segment) return false\n if (isGmailDetailsSegment(segment)) return true\n\n const line = firstVisibleLine(segment.visibleText)\n if (!line) return false\n if (SIGNATURE_DELIMITER_RE.test(line) || DETAILS_START_RE.test(line) || ON_WROTE_RE.test(line)) return true\n\n const nextText = nextVisibleSegmentText(segments, index)\n if (SIGNOFF_RE.test(line)) return Boolean(nextText && (isLikelySenderNameLine(nextText) || CONTACT_DETAIL_RE.test(nextText)))\n\n return isLikelySenderNameLine(line) && CONTACT_DETAIL_RE.test(nextText)\n}\n\nfunction splitFooterSegments(segments: MessageSegment[]): { bodySegments: MessageSegment[]; detailsSegments: MessageSegment[] } {\n const visibleIndexes = segments\n .map((segment, index) => (segment.visibleText || isGmailDetailsSegment(segment) ? index : -1))\n .filter((index) => index >= 0)\n const visibleCount = visibleIndexes.length\n if (visibleCount < 2) return { bodySegments: segments, detailsSegments: [] }\n\n const trailingCount = Math.max(Math.ceil(visibleCount * 0.4), 8)\n const firstTrailingOrdinal = Math.max(1, visibleCount - trailingCount)\n\n for (let ordinal = firstTrailingOrdinal; ordinal < visibleCount; ordinal += 1) {\n const index = visibleIndexes[ordinal]\n if (ordinal > 0 && isFooterBoundary(segments, index)) {\n return { bodySegments: segments.slice(0, index), detailsSegments: segments.slice(index) }\n }\n }\n\n return { bodySegments: segments, detailsSegments: [] }\n}\n\nexport function splitEmailHtmlForDisplay(html: string): SplitEmailHtmlResult {\n const sanitizedHtml = sanitizeHtml(decodeHtmlTextNodes(html))\n const { bodySegments, detailsSegments } = splitFooterSegments(segmentHtmlMessage(sanitizedHtml))\n\n if (!detailsSegments.length) return { bodyHtml: sanitizedHtml, detailsHtml: \"\" }\n\n return {\n bodyHtml: bodySegments.map((segment) => segment.html ?? \"\").join(\"\"),\n detailsHtml: detailsSegments.map((segment) => segment.html ?? \"\").join(\"\"),\n }\n}\n\nexport function splitEmailTextForDisplay(text: string): SplitEmailTextResult {\n const decodedText = decodeEmailDisplayText(text)\n const { bodySegments, detailsSegments } = splitFooterSegments(segmentTextMessage(decodedText))\n\n if (!detailsSegments.length) return { bodyText: decodedText, detailsText: \"\" }\n\n return {\n bodyText: bodySegments.map((segment) => segment.text ?? \"\").join(\"\"),\n detailsText: detailsSegments.map((segment) => segment.text ?? \"\").join(\"\"),\n }\n}\n\nexport function emailBodySnippet(input: { bodyHtml?: string | null; body?: string | null }, maxLength = 140): string {\n const html = input.bodyHtml?.trim()\n if (html) {\n return decodeEmailDisplayText(htmlToTextSnippet(splitEmailHtmlForDisplay(html).bodyHtml, maxLength)).trim()\n }\n\n const text = input.body ?? \"\"\n const bodyText = splitEmailTextForDisplay(text).bodyText\n const firstLine = bodyText.split(\"\\n\").find((line) => line.trim())?.trim() ?? \"\"\n return decodeEmailDisplayText(firstLine).replace(/\\s+/g, \" \").trim().slice(0, maxLength)\n}\n"],"mappings":"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","names":["segment"]}
1
+ {"version":3,"sources":["../../src/components/email-display-helpers.ts"],"sourcesContent":["import { htmlToTextSnippet, sanitizeHtml } from \"../internal/safe-html\"\n\nexport interface NormalizedEmailSender {\n name: string\n email: string | null\n}\n\nexport interface EmailDisplaySenderInput {\n name?: string | null\n email?: string | null\n fallbackName?: string\n}\n\nexport interface SplitEmailHtmlResult {\n bodyHtml: string\n detailsHtml: string\n}\n\nexport interface SplitEmailTextResult {\n bodyText: string\n detailsText: string\n}\n\nconst HTML_ENTITY_RE = /&(?:#x[0-9a-f]+|#\\d+|[a-z][a-z0-9]+);?/i\nconst EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i\nconst ANGLE_ADDRESS_RE = /^\\s*(.*?)\\s*<\\s*([^<>\\s]+@[^<>\\s]+)\\s*>\\s*$/\nconst BR_TAG_RE = /<br\\s*\\/?>/gi\n\nconst SPLITTABLE_BLOCK_TAGS = new Set([\"p\", \"div\"])\nconst WHOLE_BLOCK_TAGS = new Set([\"blockquote\", \"table\", \"ul\", \"ol\", \"hr\"])\nconst BLOCK_TAGS = new Set([...SPLITTABLE_BLOCK_TAGS, ...WHOLE_BLOCK_TAGS])\nconst HTML_BLOCK_START_RE = /<(p|div|blockquote|table|ul|ol|hr)\\b[^>]*>/i\n\nconst SIGNATURE_DELIMITER_RE = /^--\\s*$/\nconst GMAIL_SIGNATURE_RE = /\\b(?:gmail_signature|gmail_signature_prefix|gmail_extra)\\b/i\nconst GMAIL_QUOTE_RE = /<blockquote\\b[^>]*\\bclass=[\"'][^\"']*\\bgmail_quote\\b/i\nconst ON_WROTE_RE = /^On\\s.+wrote:\\s*$/i\nconst SIGNOFF_RE = /^(?:thanks,|thank you,|best,|regards,|sincerely,)$/i\nconst DETAILS_START_RE = /^(?:confidentiality notice\\b|this message and any attachments\\b|this email and any attachments\\b|the information contained in this message\\b|this communication may contain\\b|unsubscribe\\b|manage your preferences\\b)/i\nconst CONTACT_DETAIL_RE = /(?:@|https?:\\/\\/|www\\.|\\+?\\d[\\d\\s().-]{6,}\\d|\\b(?:ceo|cfo|cto|coo|founder|co-founder|director|manager|vp|vice president|president|head of|sales|marketing|operations|account|customer success|success|support|engineer|consultant|partner|principal|advisor|associate)\\b|\\b(?:inc|llc|ltd|corp|corporation|company|co\\.)\\b)/i\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 nbsp: \" \",\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) => safeCodePoint(Number.parseInt(hex, 16)))\n .replace(/&#(\\d+);?/g, (_match, decimal: string) => safeCodePoint(Number.parseInt(decimal, 10)))\n .replace(/&([a-z][a-z0-9]+);?/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 decodeJsonEscapes(value: string): string {\n return value\n .replace(/\\\\u\\{([0-9a-f]{1,6})\\}/gi, (_match, hex: string) => safeCodePoint(Number.parseInt(hex, 16)))\n .replace(/\\\\u([0-9a-f]{4})/gi, (_match, hex: string) => safeCodePoint(Number.parseInt(hex, 16)))\n .replace(/\\\\r\\\\n|\\\\n|\\\\r/g, \"\\n\")\n .replace(/\\\\t/g, \"\\t\")\n .replace(/\\\\([\"'\\\\/])/g, \"$1\")\n}\n\nfunction maybeParseJsonString(value: string): string {\n const trimmed = value.trim()\n if (!trimmed.startsWith('\"') || !trimmed.endsWith('\"')) return value\n if (!/[\\\\&]/.test(trimmed)) return value\n\n try {\n const parsed = JSON.parse(trimmed) as unknown\n return typeof parsed === \"string\" ? parsed : value\n } catch {\n return value\n }\n}\n\n/**\n * Decodes display-only email text so UI labels, body fallbacks, collapsed rows,\n * and snippets do not show HTML entities or JSON escape artifacts. It does not\n * sanitize HTML; sanitize at the HTML render boundary before using markup.\n */\nexport function decodeEmailDisplayText(value: string): string {\n let decoded = maybeParseJsonString(value).replace(/\\r\\n?/g, \"\\n\")\n\n for (let i = 0; i < 4; i += 1) {\n const next = decodeHtmlEntities(decodeJsonEscapes(decoded))\n if (next === decoded) break\n decoded = next\n }\n\n return decoded.replace(/\\u00a0/g, \" \")\n}\n\nfunction stripWrappingQuotes(value: string): string {\n const trimmed = value.trim()\n if ((trimmed.startsWith('\"') && trimmed.endsWith('\"')) || (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\"))) {\n return trimmed.slice(1, -1).trim()\n }\n return trimmed\n}\n\nfunction extractEmail(value: string): string | null {\n const decoded = decodeEmailDisplayText(value)\n const angleMatch = decoded.match(ANGLE_ADDRESS_RE)\n const email = angleMatch?.[2] ?? decoded.match(EMAIL_RE)?.[0]\n return email ? email.trim() : null\n}\n\nfunction extractNameFromAddress(value: string): string {\n const decoded = decodeEmailDisplayText(value)\n const angleMatch = decoded.match(ANGLE_ADDRESS_RE)\n if (angleMatch) return stripWrappingQuotes(angleMatch[1] ?? \"\")\n return stripWrappingQuotes(decoded.replace(EMAIL_RE, \"\").replace(/[<>]/g, \"\").trim())\n}\n\nfunction cleanDisplayName(value: string, email: string | null): string {\n let name = stripWrappingQuotes(decodeEmailDisplayText(value))\n if (email) {\n name = name\n .replace(new RegExp(`<\\\\s*${email.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}\\\\s*>`, \"i\"), \"\")\n .replace(new RegExp(`^${email.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}$`, \"i\"), \"\")\n .trim()\n }\n return stripWrappingQuotes(name)\n}\n\nexport function normalizeEmailSender(input: EmailDisplaySenderInput): NormalizedEmailSender {\n const fallbackName = decodeEmailDisplayText(input.fallbackName ?? \"Unknown sender\").trim() || \"Unknown sender\"\n const rawName = input.name ? decodeEmailDisplayText(input.name) : \"\"\n const rawEmail = input.email ? decodeEmailDisplayText(input.email) : \"\"\n const email = extractEmail(rawEmail) ?? extractEmail(rawName)\n\n const nameFromName = rawName ? cleanDisplayName(extractNameFromAddress(rawName) || rawName, email) : \"\"\n const nameFromEmail = rawEmail ? cleanDisplayName(extractNameFromAddress(rawEmail), email) : \"\"\n const name = nameFromName || nameFromEmail || email || fallbackName\n\n return { name, email }\n}\n\nfunction splitAddressList(value: string): string[] {\n const parts: string[] = []\n let current = \"\"\n let quote: '\"' | \"'\" | null = null\n let angleDepth = 0\n\n for (let index = 0; index < value.length; index += 1) {\n const char = value[index]\n\n if (quote) {\n current += char\n if (char === quote && value[index - 1] !== \"\\\\\") quote = null\n continue\n }\n\n if (char === '\"' || char === \"'\") {\n quote = char\n current += char\n continue\n }\n\n if (char === \"<\") angleDepth += 1\n if (char === \">\" && angleDepth > 0) angleDepth -= 1\n\n if (char === \",\" && angleDepth === 0) {\n if (current.trim()) parts.push(current.trim())\n current = \"\"\n continue\n }\n\n current += char\n }\n\n if (current.trim()) parts.push(current.trim())\n return parts\n}\n\nfunction formatSingleAddress(value: string): string {\n const decoded = decodeEmailDisplayText(value).trim()\n if (!decoded) return \"\"\n\n const email = extractEmail(decoded)\n const name = cleanDisplayName(extractNameFromAddress(decoded), email)\n\n if (email && name) return `${name} <${email}>`\n if (email) return email\n return stripWrappingQuotes(decoded)\n}\n\nexport function formatAddressList(input?: string | string[] | null): string {\n if (!input) return \"\"\n\n const rawItems = Array.isArray(input) ? input : splitAddressList(input)\n return rawItems\n .map((item) => formatSingleAddress(item))\n .filter(Boolean)\n .join(\", \")\n}\n\nexport function formatEmailTimestamp(\n value?: string | Date | null,\n opts?: { timeZone?: string },\n): string | null {\n if (!value) return null\n const date = value instanceof Date ? value : new Date(value)\n if (Number.isNaN(date.getTime())) return null\n\n // Default = the runtime's local timezone (the viewer's, in the browser).\n // Callers that render during SSR/hydration pass timeZone \"UTC\" for the\n // first paint so server and client HTML match, then re-render local after\n // mount (see useHydrated in timeline-activity).\n return new Intl.DateTimeFormat(\"en-US\", {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n ...(opts?.timeZone ? { timeZone: opts.timeZone } : {}),\n }).format(date)\n}\n\ntype MessageSegment = { html?: string; text?: string; visibleText: string }\n\nfunction escapeHtmlText(value: string): string {\n return value.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\")\n}\n\nfunction decodeHtmlTextNodes(html: string): string {\n if (!HTML_ENTITY_RE.test(html) && !/\\\\[nrt\"'\\\\/]|\\\\u/i.test(html)) return html\n\n if (typeof document !== \"undefined\") {\n const template = document.createElement(\"template\")\n template.innerHTML = html\n const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_TEXT)\n const textNodes: Text[] = []\n let node = walker.nextNode()\n while (node) {\n textNodes.push(node as Text)\n node = walker.nextNode()\n }\n textNodes.forEach((textNode) => {\n textNode.nodeValue = decodeEmailDisplayText(textNode.nodeValue ?? \"\")\n })\n return template.innerHTML\n }\n\n return html\n .split(/(<[^>]+>)/g)\n .map((part) => (part.startsWith(\"<\") && part.endsWith(\">\") ? part : escapeHtmlText(decodeEmailDisplayText(part))))\n .join(\"\")\n}\n\nfunction decodeHtmlText(value: string): string {\n const withoutTags = value\n .replace(BR_TAG_RE, \"\\n\")\n .replace(/<\\/(p|div|blockquote|li|tr|table|ul|ol)>/gi, \"\\n\")\n .replace(/<[^>]*>/g, \"\")\n\n return decodeEmailDisplayText(withoutTags)\n}\n\nfunction htmlToVisibleText(html: string): string {\n return decodeHtmlText(html).replace(/\\u00a0/g, \" \").replace(/[ \\t]+/g, \" \").trim()\n}\n\nfunction serializeNode(node: Node): string {\n const host = document.createElement(\"div\")\n host.appendChild(node.cloneNode(true))\n return host.innerHTML\n}\n\nfunction wrapHtmlLike(element: Element, innerHtml: string): string {\n const clone = element.cloneNode(false) as HTMLElement\n clone.innerHTML = innerHtml\n return clone.outerHTML\n}\n\nfunction hasDirectBr(nodes: readonly Node[]): boolean {\n return nodes.some((node) => node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName.toLowerCase() === \"br\")\n}\n\nfunction hasDirectBlockChild(element: Element): boolean {\n return Array.from(element.children).some((child) => {\n const tagName = child.tagName.toLowerCase()\n return tagName !== \"br\" && BLOCK_TAGS.has(tagName)\n })\n}\n\nfunction makeHtmlSegment(html: string): MessageSegment | null {\n const visibleText = htmlToVisibleText(html)\n if (!html.trim() || (!visibleText && !/<(?:img|hr)\\b/i.test(html))) return null\n return { html, visibleText }\n}\n\n// Blank-line segments carry no visible text, so they never participate in\n// footer-boundary detection — but their html must survive the rebuild, or a\n// split body loses every intentional blank line between paragraphs (the\n// Gmail wire format marks them as <div><br></div>).\nfunction makeBlankLineSegment(html: string): MessageSegment {\n return { html, visibleText: \"\" }\n}\n\nfunction isBlankLineHtml(html: string): boolean {\n return Boolean(html.trim()) && !htmlToVisibleText(html) && !/<(?:img|hr)\\b/i.test(html)\n}\n\nfunction splitInlineNodes(nodes: readonly Node[], wrapper?: Element): MessageSegment[] {\n const containsBr = hasDirectBr(nodes)\n const chunks: string[][] = [[]]\n\n nodes.forEach((node) => {\n if (node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName.toLowerCase() === \"br\") {\n chunks.push([])\n return\n }\n\n chunks[chunks.length - 1]?.push(serializeNode(node))\n })\n\n const rendered = chunks.map((chunk) => chunk.join(\"\"))\n const segments: MessageSegment[] = []\n rendered.forEach((innerHtml, index) => {\n const html = wrapper ? wrapHtmlLike(wrapper, innerHtml) : containsBr ? `<div>${innerHtml}</div>` : innerHtml\n const segment = makeHtmlSegment(html)\n if (segment) {\n segments.push(segment)\n return\n }\n // An interior empty chunk sits between two <br>s: an intentional blank\n // line. Leading/trailing empties stay dropped, as before.\n if (containsBr && index > 0 && index < rendered.length - 1) {\n segments.push(makeBlankLineSegment(wrapper ? wrapHtmlLike(wrapper, \"<br>\") : \"<div><br></div>\"))\n }\n })\n return segments\n}\n\nfunction splitElementSegment(element: Element): MessageSegment[] {\n const tagName = element.tagName.toLowerCase()\n\n if (GMAIL_SIGNATURE_RE.test(element.outerHTML) || GMAIL_QUOTE_RE.test(element.outerHTML)) {\n const segment = makeHtmlSegment(element.outerHTML)\n return segment ? [segment] : []\n }\n\n // A block with no visible content (e.g. <div><br></div>, <p></p>) is a\n // blank-line marker — keep it whole instead of splitting/dropping it.\n if (isBlankLineHtml(element.outerHTML)) {\n return [makeBlankLineSegment(element.outerHTML)]\n }\n\n if (tagName === \"div\" && hasDirectBlockChild(element)) {\n const childSegments = splitHtmlNodes(Array.from(element.childNodes))\n return childSegments.length ? childSegments : ([makeHtmlSegment(element.outerHTML)].filter(Boolean) as MessageSegment[])\n }\n\n if (SPLITTABLE_BLOCK_TAGS.has(tagName) && hasDirectBr(Array.from(element.childNodes)) && !hasDirectBlockChild(element)) {\n return splitInlineNodes(Array.from(element.childNodes), element)\n }\n\n const segment = makeHtmlSegment(element.outerHTML)\n return segment ? [segment] : []\n}\n\nfunction splitHtmlNodes(nodes: readonly Node[]): MessageSegment[] {\n const segments: MessageSegment[] = []\n let inlineNodes: Node[] = []\n\n const flushInline = () => {\n if (!inlineNodes.length) return\n segments.push(...splitInlineNodes(inlineNodes))\n inlineNodes = []\n }\n\n nodes.forEach((node) => {\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as Element\n const tagName = element.tagName.toLowerCase()\n if (BLOCK_TAGS.has(tagName)) {\n flushInline()\n segments.push(...splitElementSegment(element))\n return\n }\n }\n\n inlineNodes.push(node)\n })\n\n flushInline()\n return segments\n}\n\nfunction findMatchingCloseTag(html: string, tagName: string, openTagEnd: number): number {\n if (tagName === \"hr\") return openTagEnd + 1\n\n const tagPattern = new RegExp(`</?${tagName}\\\\b[^>]*>`, \"gi\")\n tagPattern.lastIndex = openTagEnd + 1\n let depth = 1\n let match: RegExpExecArray | null\n\n while ((match = tagPattern.exec(html)) !== null) {\n const rawTag = match[0]\n if (/^<\\//.test(rawTag)) depth -= 1\n else if (!/\\/\\s*>$/.test(rawTag)) depth += 1\n if (depth === 0) return tagPattern.lastIndex\n }\n\n return html.length\n}\n\nfunction splitHtmlSegmentsFallback(html: string): MessageSegment[] {\n const segments: MessageSegment[] = []\n let cursor = 0\n\n const pushInline = (inlineHtml: string) => {\n const chunks = inlineHtml.split(BR_TAG_RE)\n const hadBr = chunks.length > 1\n chunks.forEach((chunk, index) => {\n const segment = makeHtmlSegment(hadBr ? `<div>${chunk}</div>` : chunk)\n if (segment) {\n segments.push(segment)\n return\n }\n if (hadBr && index > 0 && index < chunks.length - 1) {\n segments.push(makeBlankLineSegment(\"<div><br /></div>\"))\n }\n })\n }\n\n while (cursor < html.length) {\n const rest = html.slice(cursor)\n const match = HTML_BLOCK_START_RE.exec(rest)\n if (!match || match.index === undefined) {\n pushInline(rest)\n break\n }\n\n if (match.index > 0) pushInline(rest.slice(0, match.index))\n\n const tagStart = cursor + match.index\n const rawOpen = match[0]\n const tagName = match[1].toLowerCase()\n const openTagEnd = tagStart + rawOpen.length - 1\n const segmentEnd = findMatchingCloseTag(html, tagName, openTagEnd)\n const blockHtml = html.slice(tagStart, segmentEnd)\n\n if (isBlankLineHtml(blockHtml)) {\n segments.push(makeBlankLineSegment(blockHtml))\n } else if (SPLITTABLE_BLOCK_TAGS.has(tagName) && BR_TAG_RE.test(blockHtml)) {\n const openTag = rawOpen\n const closeTag = `</${tagName}>`\n const inner = blockHtml.replace(new RegExp(`^${rawOpen.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}`, \"i\"), \"\").replace(new RegExp(`${closeTag}$`, \"i\"), \"\")\n const innerChunks = inner.split(BR_TAG_RE)\n innerChunks.forEach((chunk, index) => {\n const segment = makeHtmlSegment(`${openTag}${chunk}${closeTag}`)\n if (segment) {\n segments.push(segment)\n return\n }\n if (index > 0 && index < innerChunks.length - 1) {\n segments.push(makeBlankLineSegment(`${openTag}<br />${closeTag}`))\n }\n })\n } else {\n const segment = makeHtmlSegment(blockHtml)\n if (segment) segments.push(segment)\n }\n\n cursor = segmentEnd\n }\n\n return segments\n}\n\nfunction segmentHtmlMessage(html: string): MessageSegment[] {\n if (typeof document === \"undefined\" || typeof Node === \"undefined\") {\n return splitHtmlSegmentsFallback(html)\n }\n\n const template = document.createElement(\"template\")\n template.innerHTML = html\n return splitHtmlNodes(Array.from(template.content.childNodes))\n}\n\nfunction segmentTextMessage(text: string): MessageSegment[] {\n const normalized = decodeEmailDisplayText(text)\n const lines: string[] = normalized.match(/[^\\n]*(?:\\n|$)/g) ?? []\n return lines\n .filter((line, index) => line.length > 0 && !(index === lines.length - 1 && line === \"\"))\n .map((line) => ({ text: line, visibleText: line.replace(/\\u00a0/g, \" \").trim() }))\n}\n\nfunction firstVisibleLine(text: string): string {\n return text.replace(/\\u00a0/g, \" \").trimStart().split(/\\r?\\n/).find((line) => line.trim())?.trim() ?? \"\"\n}\n\nfunction isLikelySenderNameLine(line: string): boolean {\n if (!line || line.length > 60) return false\n if (/[,@:;!?]|https?:\\/\\/|www\\.|\\d/.test(line)) return false\n\n const words = line.split(/\\s+/).filter(Boolean)\n if (words.length < 1 || words.length > 4) return false\n\n return words.every((word) => /^[A-Z][A-Za-z'.-]*$/.test(word))\n}\n\nfunction nextVisibleSegmentText(segments: MessageSegment[], fromIndex: number): string {\n for (let index = fromIndex + 1; index < segments.length; index += 1) {\n const text = firstVisibleLine(segments[index]?.visibleText ?? \"\")\n if (text) return text\n }\n return \"\"\n}\n\nfunction isGmailDetailsSegment(segment: MessageSegment): boolean {\n return Boolean(segment.html && (GMAIL_SIGNATURE_RE.test(segment.html) || GMAIL_QUOTE_RE.test(segment.html)))\n}\n\nfunction isFooterBoundary(segments: MessageSegment[], index: number): boolean {\n const segment = segments[index]\n if (!segment) return false\n if (isGmailDetailsSegment(segment)) return true\n\n const line = firstVisibleLine(segment.visibleText)\n if (!line) return false\n if (SIGNATURE_DELIMITER_RE.test(line) || DETAILS_START_RE.test(line) || ON_WROTE_RE.test(line)) return true\n\n const nextText = nextVisibleSegmentText(segments, index)\n if (SIGNOFF_RE.test(line)) return Boolean(nextText && (isLikelySenderNameLine(nextText) || CONTACT_DETAIL_RE.test(nextText)))\n\n return isLikelySenderNameLine(line) && CONTACT_DETAIL_RE.test(nextText)\n}\n\nfunction splitFooterSegments(segments: MessageSegment[]): { bodySegments: MessageSegment[]; detailsSegments: MessageSegment[] } {\n const visibleIndexes = segments\n .map((segment, index) => (segment.visibleText || isGmailDetailsSegment(segment) ? index : -1))\n .filter((index) => index >= 0)\n const visibleCount = visibleIndexes.length\n if (visibleCount < 2) return { bodySegments: segments, detailsSegments: [] }\n\n const trailingCount = Math.max(Math.ceil(visibleCount * 0.4), 8)\n const firstTrailingOrdinal = Math.max(1, visibleCount - trailingCount)\n\n for (let ordinal = firstTrailingOrdinal; ordinal < visibleCount; ordinal += 1) {\n const index = visibleIndexes[ordinal]\n if (ordinal > 0 && isFooterBoundary(segments, index)) {\n return { bodySegments: segments.slice(0, index), detailsSegments: segments.slice(index) }\n }\n }\n\n return { bodySegments: segments, detailsSegments: [] }\n}\n\nexport function splitEmailHtmlForDisplay(html: string): SplitEmailHtmlResult {\n const sanitizedHtml = sanitizeHtml(decodeHtmlTextNodes(html))\n const { bodySegments, detailsSegments } = splitFooterSegments(segmentHtmlMessage(sanitizedHtml))\n\n if (!detailsSegments.length) return { bodyHtml: sanitizedHtml, detailsHtml: \"\" }\n\n return {\n bodyHtml: bodySegments.map((segment) => segment.html ?? \"\").join(\"\"),\n detailsHtml: detailsSegments.map((segment) => segment.html ?? \"\").join(\"\"),\n }\n}\n\nexport function splitEmailTextForDisplay(text: string): SplitEmailTextResult {\n const decodedText = decodeEmailDisplayText(text)\n const { bodySegments, detailsSegments } = splitFooterSegments(segmentTextMessage(decodedText))\n\n if (!detailsSegments.length) return { bodyText: decodedText, detailsText: \"\" }\n\n return {\n bodyText: bodySegments.map((segment) => segment.text ?? \"\").join(\"\"),\n detailsText: detailsSegments.map((segment) => segment.text ?? \"\").join(\"\"),\n }\n}\n\nexport function emailBodySnippet(input: { bodyHtml?: string | null; body?: string | null }, maxLength = 140): string {\n const html = input.bodyHtml?.trim()\n if (html) {\n return decodeEmailDisplayText(htmlToTextSnippet(splitEmailHtmlForDisplay(html).bodyHtml, maxLength)).trim()\n }\n\n const text = input.body ?? \"\"\n const bodyText = splitEmailTextForDisplay(text).bodyText\n const firstLine = bodyText.split(\"\\n\").find((line) => line.trim())?.trim() ?? \"\"\n return decodeEmailDisplayText(firstLine).replace(/\\s+/g, \" \").trim().slice(0, maxLength)\n}\n"],"mappings":"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","names":["segment"]}
@@ -199,10 +199,20 @@ function cleanGongText(value) {
199
199
  if (!value) return "";
200
200
  return decodeEmailDisplayText(value).trim();
201
201
  }
202
- function getTimelineGongCallDisplay(gongCall) {
202
+ function useHydrated() {
203
+ const [hydrated, setHydrated] = React.useState(false);
204
+ React.useEffect(() => {
205
+ setHydrated(true);
206
+ }, []);
207
+ return hydrated;
208
+ }
209
+ function timestampTimeZone(opts) {
210
+ return (opts == null ? void 0 : opts.utcTimestamps) ? { timeZone: "UTC" } : {};
211
+ }
212
+ function getTimelineGongCallDisplay(gongCall, opts) {
203
213
  var _a, _b, _c, _d;
204
214
  const title = cleanGongText(gongCall.title);
205
- const startTime = (_a = formatEmailTimestamp(gongCall.startTime)) != null ? _a : "";
215
+ const startTime = (_a = formatEmailTimestamp(gongCall.startTime, timestampTimeZone(opts))) != null ? _a : "";
206
216
  const duration = formatCallDuration(gongCall.durationSeconds);
207
217
  const direction = cleanGongText(gongCall.direction);
208
218
  const outcome = cleanGongText(gongCall.outcome);
@@ -213,13 +223,13 @@ function getTimelineGongCallDisplay(gongCall) {
213
223
  const snippet = (_d = (_c = brief.split("\n").find((line) => line.trim())) == null ? void 0 : _c.trim()) != null ? _d : "";
214
224
  return { title, startTime, duration, direction, outcome, brief, keyPoints, nextSteps, url, snippet };
215
225
  }
216
- function getTimelineEmailDisplay(email) {
226
+ function getTimelineEmailDisplay(email, opts) {
217
227
  var _a, _b, _c, _d, _e;
218
228
  const sender = normalizeEmailSender({ name: email.from, email: email.fromEmail });
219
229
  const to = formatAddressList(email.to) || decodeEmailDisplayText((_a = email.to) != null ? _a : "");
220
230
  const cc = formatAddressList(email.cc) || decodeEmailDisplayText((_b = email.cc) != null ? _b : "");
221
231
  const bcc = formatAddressList(email.bcc) || decodeEmailDisplayText((_c = email.bcc) != null ? _c : "");
222
- const date = (_e = formatEmailTimestamp(email.date)) != null ? _e : decodeEmailDisplayText((_d = email.date) != null ? _d : "");
232
+ const date = (_e = formatEmailTimestamp(email.date, timestampTimeZone(opts))) != null ? _e : decodeEmailDisplayText((_d = email.date) != null ? _d : "");
223
233
  const subject = email.subject ? decodeEmailDisplayText(email.subject) : "";
224
234
  const bodyText = reactNodeToDisplayText(email.body);
225
235
  const snippet = emailBodySnippet({ bodyHtml: email.bodyHtml, body: bodyText }, 140);
@@ -231,7 +241,8 @@ function EmailMetadata({
231
241
  showAllRecipients,
232
242
  setShowAllRecipients
233
243
  }) {
234
- const display = getTimelineEmailDisplay(email);
244
+ const hydrated = useHydrated();
245
+ const display = getTimelineEmailDisplay(email, { utcTimestamps: !hydrated });
235
246
  const hasExpandableRecipients = Boolean(display.cc || display.bcc);
236
247
  return /* @__PURE__ */ jsxs(Fragment, { children: [
237
248
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-4", children: [
@@ -542,7 +553,8 @@ function GongCallCard({
542
553
  classes
543
554
  }) {
544
555
  const gongCall = event.gongCall;
545
- const display = getTimelineGongCallDisplay(gongCall);
556
+ const hydrated = useHydrated();
557
+ const display = getTimelineGongCallDisplay(gongCall, { utcTimestamps: !hydrated });
546
558
  if (variant === "default") {
547
559
  return /* @__PURE__ */ jsx("div", { className: classes.cardContainer, "data-variant": variant, "data-slot": "timeline-gong-card", children: /* @__PURE__ */ jsx(
548
560
  "div",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/timeline-activity.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../lib/utils\"\nimport { ChevronDown, ChevronUp, ExternalLink, Phone } from \"lucide-react\"\nimport { EmailBody as SharedEmailBody } from \"./email-body\"\nimport {\n decodeEmailDisplayText,\n emailBodySnippet,\n formatAddressList,\n formatEmailTimestamp,\n normalizeEmailSender,\n} from \"./email-display-helpers\"\n\nexport type TimelineActivityVariant = \"default\" | \"case-panel\"\n\nexport type TimelineEventTone =\n | \"red\"\n | \"amber\"\n | \"emerald\"\n | \"violet\"\n | \"blue\"\n | \"slate\"\n | \"salesforce\"\n | \"gmail\"\n\nexport interface TimelineEventActor {\n kind: \"user\" | \"integration\" | \"system\"\n name?: string\n initials?: string\n avatarUrl?: string\n verb?: string\n}\n\nexport interface TimelineEvent {\n id: string\n icon: React.ReactNode\n title: React.ReactNode\n time: string\n preview?: React.ReactNode\n email?: {\n from: string\n fromEmail?: string\n to: string\n cc?: string\n bcc?: string\n date?: string\n subject?: string\n body: React.ReactNode\n /**\n * HTML body. When provided, the card renders formatted, Gmail-like HTML\n * instead of the plain-text `body`. The component sanitizes it before\n * rendering. Opt-in: when absent, the plain-text `body` path is used and\n * existing consumers are unaffected.\n */\n bodyHtml?: string\n /**\n * Sender signature HTML, split out of the main body by the consuming app's\n * email pipeline. When present, it is hidden behind a subtle \"Show\n * signature\" toggle (collapsed by default) so signatures don't add noise to\n * the timeline. Sanitized before rendering. Optional — when absent the body\n * renders exactly as before. Mirrors `signatureHtml` on EmailPreviewCard.\n */\n signatureHtml?: string | null\n /**\n * Quoted / trailing thread content (the \"On <date> X wrote:\" history) split\n * out of the main body. When present, it is hidden behind a subtle \"Show\n * quoted text\" toggle (collapsed by default). Sanitized before rendering.\n * Optional — when absent the body renders exactly as before.\n */\n quotedHtml?: string | null\n }\n /**\n * Gong call payload. When present, the card renders a dedicated, first-class\n * Gong call card (mirroring the `email` treatment) instead of generic\n * content: a collapsed duration + brief-snippet preview, and an expanded\n * card with the call title, start time, direction/outcome chips, the AI\n * brief, key points, next steps, and a \"View in Gong\" link. Opt-in: when\n * absent, existing consumers are unaffected. Every field except `title` is\n * optional/nullable and renders nothing when absent (no empty sections,\n * no \"null\"/\"undefined\", no \"Invalid Date\").\n */\n gongCall?: {\n /** Gong call title. */\n title: string\n /** ISO datetime of the call start. */\n startTime?: string | null\n /** Call length in seconds. Formatted to a human duration when present. */\n durationSeconds?: number | null\n /** Call direction, e.g. \"Outbound\" / \"Inbound\". */\n direction?: string | null\n /** Call outcome, e.g. \"Connected\". */\n outcome?: string | null\n /** Gong AI call brief (plain text, may be multi-paragraph). */\n brief?: string | null\n /** Gong AI key points (plain text, often newline-delimited bullets). */\n keyPoints?: string | null\n /** Gong AI highlights / next steps (plain text). */\n nextSteps?: string | null\n /** Deep link to the call in Gong. */\n url?: string | null\n }\n content?: React.ReactNode\n source?: {\n label: string\n actionLabel?: string\n url: string\n }\n defaultExpanded?: boolean\n isInteractive?: boolean\n onSourceClick?: () => void\n tone?: TimelineEventTone\n actor?: TimelineEventActor\n isSystemNoise?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Tone class map — every class is a complete static string literal so\n// Tailwind's JIT scanner can detect them. NO interpolation.\n// ---------------------------------------------------------------------------\n\nexport const TONE_CLASSES: Record<\n TimelineEventTone,\n { dot: string; icon: string }\n> = {\n red: {\n dot: \"bg-red-50 border-red-200 dark:bg-red-950/30 dark:border-red-900/40\",\n icon: \"text-red-600 dark:text-red-300\",\n },\n amber: {\n dot: \"bg-amber-50 border-amber-200 dark:bg-amber-950/30 dark:border-amber-900/40\",\n icon: \"text-amber-600 dark:text-amber-300\",\n },\n emerald: {\n dot: \"bg-emerald-50 border-emerald-200 dark:bg-emerald-950/30 dark:border-emerald-900/40\",\n icon: \"text-emerald-600 dark:text-emerald-300\",\n },\n violet: {\n dot: \"bg-violet-50 border-violet-200 dark:bg-violet-950/30 dark:border-violet-900/40\",\n icon: \"text-violet-600 dark:text-violet-300\",\n },\n blue: {\n dot: \"bg-blue-50 border-blue-200 dark:bg-blue-950/30 dark:border-blue-900/40\",\n icon: \"text-blue-600 dark:text-blue-300\",\n },\n slate: {\n dot: \"bg-slate-100 border-slate-200 dark:bg-slate-800/50 dark:border-slate-700\",\n icon: \"text-slate-500 dark:text-slate-300\",\n },\n salesforce: {\n dot: \"bg-white border-[#00A1E0]/25 dark:bg-background dark:border-[#00A1E0]/25\",\n icon: \"text-[#00A1E0]\",\n },\n gmail: {\n dot: \"bg-white border-red-200 dark:bg-background dark:border-red-900/40\",\n icon: \"text-red-500 dark:text-red-300\",\n },\n}\n\nconst NEUTRAL_DOT_CLASSES = \"border-border/60 bg-background\"\nconst NEUTRAL_ICON_CLASSES = \"text-muted-foreground\"\n\ntype TimelineVariantClasses = {\n outerRowGap: string\n connector: string\n dotWrapperSize: string\n dot: string\n contentPadding: string\n titleRowSpacing: string\n title: string\n time: string\n cardContainer: string\n cardHeader: string\n cardBody: string\n cardFooter: string\n collapsedPreview: string\n actionLinkRow: string\n actionLink: string\n nonInteractiveContent: string\n}\n\nconst TIMELINE_VARIANT_CLASSES: Record<TimelineActivityVariant, TimelineVariantClasses> = {\n default: {\n outerRowGap: \"group relative flex gap-3.5\",\n connector: \"absolute left-[9px] top-5 bottom-[-6px] w-px bg-border/60\",\n dotWrapperSize: \"relative z-10 mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-background\",\n dot: \"flex h-4.5 w-4.5 items-center justify-center rounded-full border ring-4 ring-background\",\n contentPadding: \"flex-1 pb-5 pt-0.5\",\n titleRowSpacing: \"flex min-w-0 flex-col gap-1 sm:flex-row sm:items-start sm:justify-between\",\n title: \"pr-4 text-[13px] leading-relaxed text-foreground\",\n time: \"mt-0.5 shrink-0 whitespace-nowrap text-[11px] text-muted-foreground/70\",\n cardContainer: \"overflow-hidden rounded-md border border-border/80 bg-muted/20\",\n cardHeader: \"px-3 pt-2.5\",\n cardBody: \"px-3 py-2.5 text-sm\",\n cardFooter: \"px-3 pb-2.5\",\n collapsedPreview: \"flex items-center justify-between gap-2 px-3 py-2.5 text-sm text-muted-foreground\",\n actionLinkRow: \"flex items-center gap-3 px-3 pb-2.5\",\n actionLink: \"inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\",\n nonInteractiveContent: \"pr-2 text-sm leading-relaxed text-muted-foreground\",\n },\n \"case-panel\": {\n outerRowGap: \"group relative flex gap-3\",\n connector: \"absolute left-[11px] top-6 bottom-[-2px] w-px bg-border/50\",\n dotWrapperSize: \"relative z-10 mt-0.5 flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full bg-background\",\n dot: \"flex h-[22px] w-[22px] items-center justify-center rounded-full border ring-4 ring-background [&>svg]:h-3 [&>svg]:w-3\",\n contentPadding: \"flex-1 min-w-0 pb-5 pt-px\",\n titleRowSpacing: \"flex min-w-0 items-start justify-between gap-3\",\n title: \"min-w-0 pr-1 text-[13.5px] font-medium leading-tight text-foreground\",\n time: \"shrink-0 whitespace-nowrap pt-px text-[11px] leading-tight text-muted-foreground/60\",\n cardContainer: \"overflow-hidden rounded-lg border border-border/70 bg-card\",\n cardHeader: \"flex items-start justify-between border-b border-border/60 bg-muted/30 px-3 py-2 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70\",\n cardBody: \"px-3 py-2.5 text-[13px] leading-relaxed\",\n cardFooter: \"border-t border-border/60 bg-muted/10 px-3 py-1.5\",\n collapsedPreview: \"flex items-center justify-between gap-2 px-3 py-2 text-[13px] text-muted-foreground\",\n actionLinkRow: \"flex items-center justify-end gap-2 px-3 py-1.5\",\n actionLink: \"inline-flex items-center gap-1 text-[11px] font-medium text-muted-foreground/70 transition-colors hover:text-foreground\",\n nonInteractiveContent: \"pr-2 text-[13px] leading-relaxed text-muted-foreground\",\n },\n}\n\nexport interface TimelineActivityProps {\n events: TimelineEvent[]\n className?: string\n variant?: TimelineActivityVariant\n}\n\nexport function TimelineActivity({ events, className, variant = \"default\" }: TimelineActivityProps) {\n return (\n <div className={cn(\"space-y-0\", className)} data-variant={variant}>\n {events.map((event, index) => (\n <TimelineItem\n key={event.id}\n event={event}\n isLast={index === events.length - 1}\n variant={variant}\n />\n ))}\n </div>\n )\n}\n\nfunction ActorByline({ actor, time }: { actor: TimelineEventActor; time: string }) {\n if (actor.kind === \"system\") return null\n\n if (actor.kind === \"integration\") {\n return (\n <div className=\"mt-1 flex items-center gap-1.5 text-xs text-muted-foreground\" data-testid=\"actor-byline\">\n <span>Integration</span>\n <span className=\"text-muted-foreground/40\">&middot;</span>\n <span>{time}</span>\n </div>\n )\n }\n\n // actor.kind === \"user\"\n const verb = actor.verb ?? \"performed this action\"\n const displayInitials = actor.initials ?? (actor.name ? actor.name.charAt(0).toUpperCase() : \"?\")\n\n return (\n <div className=\"mt-1 flex items-center gap-1.5 text-xs text-muted-foreground\" data-testid=\"actor-byline\">\n {actor.avatarUrl ? (\n <img\n src={actor.avatarUrl}\n alt={actor.name ?? \"User\"}\n className=\"h-4 w-4 rounded-full object-cover\"\n />\n ) : (\n <span className=\"flex h-4 w-4 items-center justify-center rounded-full bg-muted-foreground/10 text-[8px] font-semibold text-muted-foreground\">\n {displayInitials}\n </span>\n )}\n {actor.name && <span className=\"text-foreground font-medium\">{actor.name}</span>}\n <span>{verb}</span>\n <span className=\"text-muted-foreground/40\">&middot;</span>\n <span>{time}</span>\n </div>\n )\n}\n\nfunction TimelineItem({\n event,\n isLast,\n variant,\n}: {\n event: TimelineEvent\n isLast: boolean\n variant: TimelineActivityVariant\n}) {\n const [expanded, setExpanded] = React.useState(event.defaultExpanded ?? false)\n const [showAllRecipients, setShowAllRecipients] = React.useState(false)\n const hasContent = !!event.content\n const hasEmail = !!event.email\n const hasGongCall = !!event.gongCall\n const classes = TIMELINE_VARIANT_CLASSES[variant]\n\n const toneStyle = event.tone ? TONE_CLASSES[event.tone] : null\n const dotClasses = toneStyle ? toneStyle.dot : NEUTRAL_DOT_CLASSES\n const iconClasses = toneStyle ? toneStyle.icon : NEUTRAL_ICON_CLASSES\n\n return (\n <div className={classes.outerRowGap}>\n {!isLast && (\n <div className={classes.connector} />\n )}\n\n <div className={classes.dotWrapperSize}>\n <div className={cn(classes.dot, dotClasses, iconClasses)} data-testid=\"timeline-dot\">\n {event.icon}\n </div>\n </div>\n\n <div className={classes.contentPadding}>\n <div className={classes.titleRowSpacing}>\n <div className={classes.title}>\n {event.title}\n </div>\n <span className={classes.time}>\n {event.time}\n </span>\n </div>\n\n {event.actor && <ActorByline actor={event.actor} time={event.time} />}\n\n {(hasContent || hasEmail || hasGongCall) && (\n <div className=\"mt-2\">\n {event.isInteractive ? (\n hasGongCall ? (\n <GongCallCard\n event={event}\n expanded={expanded}\n setExpanded={setExpanded}\n variant={variant}\n classes={classes}\n />\n ) : hasEmail ? (\n <EmailCard\n event={event}\n expanded={expanded}\n setExpanded={setExpanded}\n showAllRecipients={showAllRecipients}\n setShowAllRecipients={setShowAllRecipients}\n variant={variant}\n classes={classes}\n />\n ) : (\n <ContentCard\n event={event}\n expanded={expanded}\n setExpanded={setExpanded}\n variant={variant}\n classes={classes}\n />\n )\n ) : (\n <div className={classes.nonInteractiveContent}>\n {event.content}\n </div>\n )}\n </div>\n )}\n </div>\n </div>\n )\n}\n\ntype TimelineEmail = NonNullable<TimelineEvent[\"email\"]>\ntype TimelineGongCall = NonNullable<TimelineEvent[\"gongCall\"]>\ntype TimelineSource = NonNullable<TimelineEvent[\"source\"]>\n\nfunction reactNodeToDisplayText(value: React.ReactNode): string {\n if (typeof value === \"string\" || typeof value === \"number\") return decodeEmailDisplayText(String(value))\n return \"\"\n}\n\n/**\n * Formats a call length in seconds to a compact human duration:\n * \"38 min\", \">= 60 min\" → \"1 h 12 min\". Returns \"\" for absent/invalid\n * input so callers can omit the chip entirely (never render \"0 min\").\n */\nfunction formatCallDuration(durationSeconds?: number | null): string {\n if (durationSeconds == null || !Number.isFinite(durationSeconds) || durationSeconds <= 0) return \"\"\n const totalMinutes = Math.round(durationSeconds / 60)\n if (totalMinutes < 1) return \"< 1 min\"\n if (totalMinutes < 60) return `${totalMinutes} min`\n const hours = Math.floor(totalMinutes / 60)\n const minutes = totalMinutes % 60\n return minutes === 0 ? `${hours} h` : `${hours} h ${minutes} min`\n}\n\nfunction cleanGongText(value?: string | null): string {\n if (!value) return \"\"\n return decodeEmailDisplayText(value).trim()\n}\n\nfunction getTimelineGongCallDisplay(gongCall: TimelineGongCall) {\n const title = cleanGongText(gongCall.title)\n const startTime = formatEmailTimestamp(gongCall.startTime) ?? \"\"\n const duration = formatCallDuration(gongCall.durationSeconds)\n const direction = cleanGongText(gongCall.direction)\n const outcome = cleanGongText(gongCall.outcome)\n const brief = cleanGongText(gongCall.brief)\n const keyPoints = cleanGongText(gongCall.keyPoints)\n const nextSteps = cleanGongText(gongCall.nextSteps)\n const url = gongCall.url?.trim() || \"\"\n const snippet = brief.split(\"\\n\").find((line) => line.trim())?.trim() ?? \"\"\n\n return { title, startTime, duration, direction, outcome, brief, keyPoints, nextSteps, url, snippet }\n}\n\nfunction getTimelineEmailDisplay(email: TimelineEmail) {\n const sender = normalizeEmailSender({ name: email.from, email: email.fromEmail })\n const to = formatAddressList(email.to) || decodeEmailDisplayText(email.to ?? \"\")\n const cc = formatAddressList(email.cc) || decodeEmailDisplayText(email.cc ?? \"\")\n const bcc = formatAddressList(email.bcc) || decodeEmailDisplayText(email.bcc ?? \"\")\n const date = formatEmailTimestamp(email.date) ?? decodeEmailDisplayText(email.date ?? \"\")\n const subject = email.subject ? decodeEmailDisplayText(email.subject) : \"\"\n const bodyText = reactNodeToDisplayText(email.body)\n const snippet = emailBodySnippet({ bodyHtml: email.bodyHtml, body: bodyText }, 140)\n const hasRecipients = Boolean(to || cc || bcc)\n\n return { sender, to, cc, bcc, date, subject, bodyText, snippet, hasRecipients }\n}\n\nfunction EmailMetadata({\n email,\n showAllRecipients,\n setShowAllRecipients,\n}: {\n email: TimelineEmail\n showAllRecipients: boolean\n setShowAllRecipients: React.Dispatch<React.SetStateAction<boolean>>\n}) {\n const display = getTimelineEmailDisplay(email)\n const hasExpandableRecipients = Boolean(display.cc || display.bcc)\n\n return (\n <>\n {/* Row 1: sender name + email, with the date right-aligned. The `gap-4`\n guarantees whitespace between the sender block and the date so they\n never visually jam together. */}\n <div className=\"flex items-center justify-between gap-4\">\n <div className=\"flex min-w-0 items-baseline gap-1.5\">\n <span className=\"font-semibold text-foreground text-[13px] whitespace-nowrap\">{display.sender.name}</span>\n {display.sender.email ? (\n <span className=\"text-muted-foreground/60 text-xs truncate\">{display.sender.email}</span>\n ) : null}\n </div>\n {display.date ? (\n <span className=\"shrink-0 text-xs text-muted-foreground/50 whitespace-nowrap\">{display.date}</span>\n ) : null}\n </div>\n {/* Recipient lines live on their own full-width, stacked rows below the\n sender/date row. Each line truncates independently. The To-segment is\n suppressed entirely when there is no recipient data (history emails)\n rather than rendering a meaningless \"no recipient yet\" placeholder. */}\n {display.hasRecipients ? (\n <div className=\"mt-0.5 text-xs text-muted-foreground\">\n <div className=\"flex items-center gap-1\">\n <span className=\"min-w-0 flex-1 truncate\">\n {display.to ? (\n <>To {display.to}</>\n ) : (\n <>\n <span className=\"text-muted-foreground/40\">cc</span> {display.cc || display.bcc}\n </>\n )}\n {display.to && !showAllRecipients && hasExpandableRecipients ? (\n <>, &hellip;</>\n ) : null}\n </span>\n {hasExpandableRecipients ? (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n setShowAllRecipients((prev) => !prev)\n }}\n className=\"shrink-0 text-muted-foreground/40 hover:text-muted-foreground transition-colors\"\n aria-label={showAllRecipients ? \"Hide cc and bcc\" : \"Show cc and bcc\"}\n >\n <ChevronDown className={cn(\"h-3 w-3 transition-transform\", showAllRecipients && \"rotate-180\")} />\n </button>\n ) : null}\n </div>\n {showAllRecipients && display.to && display.cc ? (\n <div className=\"truncate\">\n <span className=\"text-muted-foreground/40\">cc</span> {display.cc}\n </div>\n ) : null}\n {showAllRecipients && display.bcc ? (\n <div className=\"truncate\">\n <span className=\"text-muted-foreground/40\">bcc</span> {display.bcc}\n </div>\n ) : null}\n </div>\n ) : null}\n </>\n )\n}\n\nfunction SuppressedHtmlSection({\n html,\n showLabel,\n hideLabel,\n slot,\n}: {\n html: string\n showLabel: string\n hideLabel: string\n slot: string\n}) {\n const [open, setOpen] = React.useState(false)\n\n return (\n <div className=\"mt-2\" data-slot={slot}>\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n setOpen((value) => !value)\n }}\n aria-expanded={open}\n className=\"inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground\"\n >\n {open ? hideLabel : showLabel}\n <ChevronDown className={cn(\"h-3 w-3 transition-transform\", open && \"rotate-180\")} />\n </button>\n {open ? (\n <div\n data-slot={`${slot}-content`}\n className=\"mt-2 border-l-2 border-border pl-3 text-sm leading-relaxed text-muted-foreground\"\n >\n <SharedEmailBody html={html} variant=\"history\" collapseDetails={false} />\n </div>\n ) : null}\n </div>\n )\n}\n\nfunction TimelineEmailBody({ email }: { email: TimelineEmail }) {\n const display = getTimelineEmailDisplay(email)\n const bodyFallback = display.bodyText || (typeof email.body === \"string\" ? email.body : \"\")\n\n if (!email.bodyHtml && !bodyFallback && email.body) {\n return <div className=\"whitespace-pre-line text-sm leading-relaxed text-foreground/90\">{email.body}</div>\n }\n\n const body = (\n <SharedEmailBody\n html={email.bodyHtml}\n text={bodyFallback}\n variant=\"history\"\n collapseDetails={true}\n className=\"text-sm leading-relaxed\"\n />\n )\n\n const hasSignature = Boolean(email.signatureHtml && email.signatureHtml.trim())\n const hasQuoted = Boolean(email.quotedHtml && email.quotedHtml.trim())\n\n // Fast path: no signature/quoted slots → identical output to before.\n if (!hasSignature && !hasQuoted) {\n return email.bodyHtml ? <div data-slot=\"timeline-email-html\">{body}</div> : body\n }\n\n return (\n <div data-slot={email.bodyHtml ? \"timeline-email-html\" : undefined}>\n {body}\n {hasSignature ? (\n <SuppressedHtmlSection\n html={email.signatureHtml as string}\n showLabel=\"Show signature\"\n hideLabel=\"Hide signature\"\n slot=\"timeline-email-signature\"\n />\n ) : null}\n {hasQuoted ? (\n <SuppressedHtmlSection\n html={email.quotedHtml as string}\n showLabel=\"Show quoted text\"\n hideLabel=\"Hide quoted text\"\n slot=\"timeline-email-quoted\"\n />\n ) : null}\n </div>\n )\n}\n\nfunction renderDecodedPreview(preview?: React.ReactNode): React.ReactNode {\n if (typeof preview === \"string\" || typeof preview === \"number\") return decodeEmailDisplayText(String(preview))\n return preview\n}\n\nfunction CollapsedEmailPreview({\n email,\n preview,\n className,\n actionClassName,\n onClick,\n}: {\n email?: TimelineEmail\n preview?: React.ReactNode\n className: string\n actionClassName: string\n onClick?: () => void\n}) {\n const display = email ? getTimelineEmailDisplay(email) : null\n const previewContent = display?.snippet || renderDecodedPreview(preview)\n\n return (\n <div className={className} onClick={onClick}>\n <span className=\"line-clamp-1 min-w-0 flex-1 pr-3 text-[13px]\">\n <span className=\"text-muted-foreground\">{display?.sender.name}</span>\n <span className=\"mx-1.5 text-muted-foreground/40\">&middot;</span>\n {display?.subject ? (\n <>\n <span className=\"text-muted-foreground\">{display.subject}</span>\n <span className=\"mx-1.5 text-muted-foreground/40\">&middot;</span>\n </>\n ) : null}\n <span className=\"text-muted-foreground\">{previewContent}</span>\n </span>\n <button type=\"button\" className={actionClassName}>\n Expand <ChevronDown className=\"h-3 w-3\" />\n </button>\n </div>\n )\n}\n\nfunction ShowLessButton({\n className,\n onClick,\n type,\n}: {\n className: string\n onClick: React.MouseEventHandler<HTMLButtonElement>\n type?: \"button\"\n}) {\n return (\n <button type={type} onClick={onClick} className={className}>\n Show less <ChevronUp className=\"h-3 w-3\" />\n </button>\n )\n}\n\n/**\n * Top-of-card collapse affordance. Long expanded emails are tedious to collapse\n * from the bottom alone, so the header row also carries a \"Show less\" control.\n */\nfunction CollapseTopButton({\n onClick,\n className,\n}: {\n onClick: React.MouseEventHandler<HTMLButtonElement>\n className?: string\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label=\"Show less\"\n data-slot=\"timeline-collapse-top\"\n className={cn(\n \"shrink-0 inline-flex items-center gap-1 text-[11px] font-medium text-muted-foreground/60 transition-colors hover:text-foreground\",\n className,\n )}\n >\n <span className=\"sr-only sm:not-sr-only\">Show less</span>\n <ChevronUp className=\"h-3 w-3\" />\n </button>\n )\n}\n\nfunction SourceAction({\n source,\n onSourceClick,\n className,\n}: {\n source: TimelineSource\n onSourceClick?: () => void\n className: string\n}) {\n const actionLabel = source.actionLabel ?? `Open in ${source.label}`\n\n if (onSourceClick) {\n return (\n <button\n type=\"button\"\n onClick={(e) => { e.stopPropagation(); onSourceClick(); }}\n className={className}\n >\n {actionLabel}\n <ExternalLink className=\"h-3 w-3\" />\n </button>\n )\n }\n\n return (\n <a\n href={source.url}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className={className}\n >\n {actionLabel}\n <ExternalLink className=\"h-3 w-3\" />\n </a>\n )\n}\n\nfunction GongLinkAction({\n url,\n className,\n}: {\n url: string\n className: string\n}) {\n return (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={(e) => e.stopPropagation()}\n className={className}\n data-slot=\"timeline-gong-link\"\n >\n View in Gong\n <ExternalLink className=\"h-3 w-3\" />\n </a>\n )\n}\n\n/**\n * The expanded Gong call card body — call title + start time header, optional\n * direction/outcome chips, the AI brief as primary body text, and labeled\n * Key points / Next steps subsections. Every section renders only when its\n * content is present.\n */\nfunction TimelineGongCallBody({\n display,\n labelClassName,\n}: {\n display: ReturnType<typeof getTimelineGongCallDisplay>\n labelClassName: string\n}) {\n return (\n <div className=\"space-y-3\" data-slot=\"timeline-gong-body\">\n {display.brief ? (\n <div className=\"whitespace-pre-line text-sm leading-relaxed text-foreground/90\">\n {display.brief}\n </div>\n ) : null}\n {display.keyPoints ? (\n <div data-slot=\"timeline-gong-key-points\">\n <div className={labelClassName}>Key points</div>\n <div className=\"mt-1 whitespace-pre-line text-sm leading-relaxed text-foreground/90\">\n {display.keyPoints}\n </div>\n </div>\n ) : null}\n {display.nextSteps ? (\n <div data-slot=\"timeline-gong-next-steps\">\n <div className={labelClassName}>Next steps</div>\n <div className=\"mt-1 whitespace-pre-line text-sm leading-relaxed text-foreground/90\">\n {display.nextSteps}\n </div>\n </div>\n ) : null}\n </div>\n )\n}\n\nfunction GongCallHeader({\n display,\n}: {\n display: ReturnType<typeof getTimelineGongCallDisplay>\n}) {\n return (\n <>\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <Phone className=\"h-3.5 w-3.5 shrink-0 text-muted-foreground/60\" />\n <span className=\"min-w-0 truncate font-semibold text-foreground text-[13px]\">{display.title}</span>\n </div>\n {display.startTime ? (\n <span className=\"shrink-0 text-xs text-muted-foreground/50 whitespace-nowrap\">{display.startTime}</span>\n ) : null}\n </div>\n {display.duration || display.direction || display.outcome ? (\n <div className=\"mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground\">\n {display.duration ? <span data-slot=\"timeline-gong-duration\">{display.duration}</span> : null}\n {display.direction ? (\n <span className=\"rounded bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground\">{display.direction}</span>\n ) : null}\n {display.outcome ? (\n <span className=\"rounded bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground\">{display.outcome}</span>\n ) : null}\n </div>\n ) : null}\n </>\n )\n}\n\nfunction CollapsedGongCallPreview({\n display,\n className,\n actionClassName,\n onClick,\n}: {\n display: ReturnType<typeof getTimelineGongCallDisplay>\n className: string\n actionClassName: string\n onClick?: () => void\n}) {\n return (\n <div className={className} onClick={onClick}>\n <span className=\"line-clamp-1 min-w-0 flex-1 pr-3 text-[13px] text-muted-foreground\">\n {display.duration ? (\n <>\n <span className=\"inline-flex items-center gap-1\">\n <Phone className=\"h-3 w-3\" />\n {display.duration}\n </span>\n {display.snippet ? <span className=\"mx-1.5 text-muted-foreground/40\">&middot;</span> : null}\n </>\n ) : null}\n {display.snippet ? <span>{display.snippet}</span> : null}\n </span>\n <button type=\"button\" className={actionClassName}>\n Expand <ChevronDown className=\"h-3 w-3\" />\n </button>\n </div>\n )\n}\n\nfunction GongCallCard({\n event,\n expanded,\n setExpanded,\n variant,\n classes,\n}: {\n event: TimelineEvent\n expanded: boolean\n setExpanded: React.Dispatch<React.SetStateAction<boolean>>\n variant: TimelineActivityVariant\n classes: TimelineVariantClasses\n}) {\n const gongCall = event.gongCall as TimelineGongCall\n const display = getTimelineGongCallDisplay(gongCall)\n\n if (variant === \"default\") {\n return (\n <div className={classes.cardContainer} data-variant={variant} data-slot=\"timeline-gong-card\">\n <div\n className={cn(\n \"px-3 py-2.5 text-sm\",\n !expanded && \"cursor-pointer hover:bg-muted/30 transition-colors\",\n )}\n onClick={() => !expanded && setExpanded(true)}\n >\n {expanded ? (\n <div className=\"space-y-3\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0 flex-1\">\n <GongCallHeader display={display} />\n </div>\n <CollapseTopButton\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className=\"mt-0.5\"\n />\n </div>\n\n <TimelineGongCallBody\n display={display}\n labelClassName=\"text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70\"\n />\n\n <div className=\"mt-2 flex items-center gap-3\">\n {display.url ? (\n <GongLinkAction\n url={display.url}\n className=\"mr-auto inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n />\n ) : null}\n <ShowLessButton\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className=\"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n />\n </div>\n </div>\n ) : (\n <CollapsedGongCallPreview\n display={display}\n className=\"flex items-center justify-between gap-2 text-muted-foreground\"\n actionClassName=\"flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground\"\n />\n )}\n </div>\n </div>\n )\n }\n\n return (\n <div className={classes.cardContainer} data-variant={variant} data-slot=\"timeline-gong-card\">\n {expanded ? (\n <>\n <div className={classes.cardHeader} data-slot=\"timeline-card-header\">\n <div className=\"min-w-0 flex-1 normal-case tracking-normal\">\n <GongCallHeader display={display} />\n </div>\n <CollapseTopButton\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className=\"ml-3 normal-case tracking-normal\"\n />\n </div>\n\n <div className={classes.cardBody} data-slot=\"timeline-card-body\">\n <TimelineGongCallBody\n display={display}\n labelClassName=\"text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70\"\n />\n </div>\n\n <div\n className={cn(classes.cardFooter, classes.actionLinkRow, display.url ? \"justify-between\" : \"justify-end\")}\n data-slot=\"timeline-card-footer\"\n >\n {display.url ? <GongLinkAction url={display.url} className={classes.actionLink} /> : null}\n <ShowLessButton\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className={classes.actionLink}\n />\n </div>\n </>\n ) : (\n <CollapsedGongCallPreview\n display={display}\n className={cn(classes.collapsedPreview, \"cursor-pointer hover:bg-muted/30 transition-colors\")}\n actionClassName={cn(classes.actionLink, \"shrink-0\")}\n onClick={() => setExpanded(true)}\n />\n )}\n </div>\n )\n}\n\nfunction EmailCard({\n event,\n expanded,\n setExpanded,\n showAllRecipients,\n setShowAllRecipients,\n variant,\n classes,\n}: {\n event: TimelineEvent\n expanded: boolean\n setExpanded: React.Dispatch<React.SetStateAction<boolean>>\n showAllRecipients: boolean\n setShowAllRecipients: React.Dispatch<React.SetStateAction<boolean>>\n variant: TimelineActivityVariant\n classes: TimelineVariantClasses\n}) {\n if (variant === \"default\") {\n return (\n <div className={classes.cardContainer} data-variant={variant}>\n <div\n className={cn(\n \"px-3 py-2.5 text-sm\",\n !expanded && \"cursor-pointer hover:bg-muted/30 transition-colors\"\n )}\n onClick={() => !expanded && setExpanded(true)}\n >\n {expanded && event.email ? (\n <div className=\"space-y-3\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0 flex-1\">\n <EmailMetadata\n email={event.email}\n showAllRecipients={showAllRecipients}\n setShowAllRecipients={setShowAllRecipients}\n />\n </div>\n <CollapseTopButton\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className=\"mt-0.5\"\n />\n </div>\n\n <TimelineEmailBody email={event.email} />\n\n {event.content ? (\n <div className=\"rounded-md bg-muted/30 px-2.5 py-2 text-xs text-muted-foreground\">\n {event.content}\n </div>\n ) : null}\n\n <div className=\"mt-2 flex items-center gap-3\">\n {event.source ? (\n <SourceAction\n source={event.source}\n onSourceClick={event.onSourceClick}\n className=\"mr-auto inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n />\n ) : null}\n <ShowLessButton\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className=\"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n />\n </div>\n </div>\n ) : (\n <CollapsedEmailPreview\n email={event.email}\n preview={event.preview}\n className=\"flex items-center justify-between gap-2 text-muted-foreground\"\n actionClassName=\"flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground\"\n />\n )}\n </div>\n </div>\n )\n }\n\n return (\n <div className={classes.cardContainer} data-variant={variant}>\n {expanded && event.email ? (\n <>\n <div className={classes.cardHeader} data-slot=\"timeline-card-header\">\n <div className=\"min-w-0 flex-1 normal-case tracking-normal\">\n <EmailMetadata\n email={event.email}\n showAllRecipients={showAllRecipients}\n setShowAllRecipients={setShowAllRecipients}\n />\n </div>\n <CollapseTopButton\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className=\"ml-3 normal-case tracking-normal\"\n />\n </div>\n\n <div className={classes.cardBody} data-slot=\"timeline-card-body\">\n <TimelineEmailBody email={event.email} />\n {event.content ? (\n <div className=\"mt-3 rounded-md bg-muted/30 px-2.5 py-2 text-xs text-muted-foreground\">\n {event.content}\n </div>\n ) : null}\n </div>\n\n <div\n className={cn(classes.cardFooter, classes.actionLinkRow, event.source ? \"justify-between\" : \"justify-end\")}\n data-slot=\"timeline-card-footer\"\n >\n {event.source ? (\n <SourceAction\n source={event.source}\n onSourceClick={event.onSourceClick}\n className={classes.actionLink}\n />\n ) : null}\n <ShowLessButton\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className={classes.actionLink}\n />\n </div>\n </>\n ) : (\n <CollapsedEmailPreview\n email={event.email}\n preview={event.preview}\n className={cn(classes.collapsedPreview, \"cursor-pointer hover:bg-muted/30 transition-colors\")}\n actionClassName={cn(classes.actionLink, \"shrink-0\")}\n onClick={() => setExpanded(true)}\n />\n )}\n </div>\n )\n}\n\nfunction ContentCard({\n event,\n expanded,\n setExpanded,\n variant,\n classes,\n}: {\n event: TimelineEvent\n expanded: boolean\n setExpanded: React.Dispatch<React.SetStateAction<boolean>>\n variant: TimelineActivityVariant\n classes: TimelineVariantClasses\n}) {\n if (variant === \"default\") {\n return (\n <div className={classes.cardContainer} data-variant={variant}>\n <div\n className={cn(\n \"px-3 py-2.5 text-sm\",\n !expanded && \"cursor-pointer hover:bg-muted/30 transition-colors\"\n )}\n onClick={() => !expanded && setExpanded(true)}\n >\n {expanded ? (\n <div className=\"space-y-2\">\n {event.content}\n <div className=\"mt-2 flex items-center gap-3\">\n {event.source ? (\n <SourceAction\n source={event.source}\n onSourceClick={event.onSourceClick}\n className=\"mr-auto inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n />\n ) : null}\n <ShowLessButton\n onClick={(e) => { e.stopPropagation(); setExpanded(false); }}\n className=\"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n />\n </div>\n </div>\n ) : (\n <div className=\"flex items-center justify-between gap-2 text-muted-foreground\">\n <span className=\"line-clamp-1 pr-3\">\n {event.preview ?? event.content}\n </span>\n <button className=\"flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground\">\n Expand <ChevronDown className=\"h-3 w-3\" />\n </button>\n </div>\n )}\n </div>\n </div>\n )\n }\n\n return (\n <div className={classes.cardContainer} data-variant={variant}>\n {expanded ? (\n <>\n <div className={classes.cardBody} data-slot=\"timeline-card-body\">\n {event.content}\n </div>\n <div className={cn(classes.cardFooter, classes.actionLinkRow, event.source ? \"justify-between\" : \"justify-end\")} data-slot=\"timeline-card-footer\">\n {event.source ? (\n <SourceAction\n source={event.source}\n onSourceClick={event.onSourceClick}\n className={classes.actionLink}\n />\n ) : null}\n <ShowLessButton\n type=\"button\"\n onClick={(e) => { e.stopPropagation(); setExpanded(false); }}\n className={classes.actionLink}\n />\n </div>\n </>\n ) : (\n <div\n className={cn(classes.collapsedPreview, \"cursor-pointer hover:bg-muted/30 transition-colors\")}\n onClick={() => setExpanded(true)}\n >\n <span className=\"line-clamp-1 pr-3\">\n {event.preview ?? event.content}\n </span>\n <button type=\"button\" className={cn(classes.actionLink, \"shrink-0\")}>\n Expand <ChevronDown className=\"h-3 w-3\" />\n </button>\n </div>\n )}\n </div>\n )\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/timeline-activity.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../lib/utils\"\nimport { ChevronDown, ChevronUp, ExternalLink, Phone } from \"lucide-react\"\nimport { EmailBody as SharedEmailBody } from \"./email-body\"\nimport {\n decodeEmailDisplayText,\n emailBodySnippet,\n formatAddressList,\n formatEmailTimestamp,\n normalizeEmailSender,\n} from \"./email-display-helpers\"\n\nexport type TimelineActivityVariant = \"default\" | \"case-panel\"\n\nexport type TimelineEventTone =\n | \"red\"\n | \"amber\"\n | \"emerald\"\n | \"violet\"\n | \"blue\"\n | \"slate\"\n | \"salesforce\"\n | \"gmail\"\n\nexport interface TimelineEventActor {\n kind: \"user\" | \"integration\" | \"system\"\n name?: string\n initials?: string\n avatarUrl?: string\n verb?: string\n}\n\nexport interface TimelineEvent {\n id: string\n icon: React.ReactNode\n title: React.ReactNode\n time: string\n preview?: React.ReactNode\n email?: {\n from: string\n fromEmail?: string\n to: string\n cc?: string\n bcc?: string\n date?: string\n subject?: string\n body: React.ReactNode\n /**\n * HTML body. When provided, the card renders formatted, Gmail-like HTML\n * instead of the plain-text `body`. The component sanitizes it before\n * rendering. Opt-in: when absent, the plain-text `body` path is used and\n * existing consumers are unaffected.\n */\n bodyHtml?: string\n /**\n * Sender signature HTML, split out of the main body by the consuming app's\n * email pipeline. When present, it is hidden behind a subtle \"Show\n * signature\" toggle (collapsed by default) so signatures don't add noise to\n * the timeline. Sanitized before rendering. Optional — when absent the body\n * renders exactly as before. Mirrors `signatureHtml` on EmailPreviewCard.\n */\n signatureHtml?: string | null\n /**\n * Quoted / trailing thread content (the \"On <date> X wrote:\" history) split\n * out of the main body. When present, it is hidden behind a subtle \"Show\n * quoted text\" toggle (collapsed by default). Sanitized before rendering.\n * Optional — when absent the body renders exactly as before.\n */\n quotedHtml?: string | null\n }\n /**\n * Gong call payload. When present, the card renders a dedicated, first-class\n * Gong call card (mirroring the `email` treatment) instead of generic\n * content: a collapsed duration + brief-snippet preview, and an expanded\n * card with the call title, start time, direction/outcome chips, the AI\n * brief, key points, next steps, and a \"View in Gong\" link. Opt-in: when\n * absent, existing consumers are unaffected. Every field except `title` is\n * optional/nullable and renders nothing when absent (no empty sections,\n * no \"null\"/\"undefined\", no \"Invalid Date\").\n */\n gongCall?: {\n /** Gong call title. */\n title: string\n /** ISO datetime of the call start. */\n startTime?: string | null\n /** Call length in seconds. Formatted to a human duration when present. */\n durationSeconds?: number | null\n /** Call direction, e.g. \"Outbound\" / \"Inbound\". */\n direction?: string | null\n /** Call outcome, e.g. \"Connected\". */\n outcome?: string | null\n /** Gong AI call brief (plain text, may be multi-paragraph). */\n brief?: string | null\n /** Gong AI key points (plain text, often newline-delimited bullets). */\n keyPoints?: string | null\n /** Gong AI highlights / next steps (plain text). */\n nextSteps?: string | null\n /** Deep link to the call in Gong. */\n url?: string | null\n }\n content?: React.ReactNode\n source?: {\n label: string\n actionLabel?: string\n url: string\n }\n defaultExpanded?: boolean\n isInteractive?: boolean\n onSourceClick?: () => void\n tone?: TimelineEventTone\n actor?: TimelineEventActor\n isSystemNoise?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Tone class map — every class is a complete static string literal so\n// Tailwind's JIT scanner can detect them. NO interpolation.\n// ---------------------------------------------------------------------------\n\nexport const TONE_CLASSES: Record<\n TimelineEventTone,\n { dot: string; icon: string }\n> = {\n red: {\n dot: \"bg-red-50 border-red-200 dark:bg-red-950/30 dark:border-red-900/40\",\n icon: \"text-red-600 dark:text-red-300\",\n },\n amber: {\n dot: \"bg-amber-50 border-amber-200 dark:bg-amber-950/30 dark:border-amber-900/40\",\n icon: \"text-amber-600 dark:text-amber-300\",\n },\n emerald: {\n dot: \"bg-emerald-50 border-emerald-200 dark:bg-emerald-950/30 dark:border-emerald-900/40\",\n icon: \"text-emerald-600 dark:text-emerald-300\",\n },\n violet: {\n dot: \"bg-violet-50 border-violet-200 dark:bg-violet-950/30 dark:border-violet-900/40\",\n icon: \"text-violet-600 dark:text-violet-300\",\n },\n blue: {\n dot: \"bg-blue-50 border-blue-200 dark:bg-blue-950/30 dark:border-blue-900/40\",\n icon: \"text-blue-600 dark:text-blue-300\",\n },\n slate: {\n dot: \"bg-slate-100 border-slate-200 dark:bg-slate-800/50 dark:border-slate-700\",\n icon: \"text-slate-500 dark:text-slate-300\",\n },\n salesforce: {\n dot: \"bg-white border-[#00A1E0]/25 dark:bg-background dark:border-[#00A1E0]/25\",\n icon: \"text-[#00A1E0]\",\n },\n gmail: {\n dot: \"bg-white border-red-200 dark:bg-background dark:border-red-900/40\",\n icon: \"text-red-500 dark:text-red-300\",\n },\n}\n\nconst NEUTRAL_DOT_CLASSES = \"border-border/60 bg-background\"\nconst NEUTRAL_ICON_CLASSES = \"text-muted-foreground\"\n\ntype TimelineVariantClasses = {\n outerRowGap: string\n connector: string\n dotWrapperSize: string\n dot: string\n contentPadding: string\n titleRowSpacing: string\n title: string\n time: string\n cardContainer: string\n cardHeader: string\n cardBody: string\n cardFooter: string\n collapsedPreview: string\n actionLinkRow: string\n actionLink: string\n nonInteractiveContent: string\n}\n\nconst TIMELINE_VARIANT_CLASSES: Record<TimelineActivityVariant, TimelineVariantClasses> = {\n default: {\n outerRowGap: \"group relative flex gap-3.5\",\n connector: \"absolute left-[9px] top-5 bottom-[-6px] w-px bg-border/60\",\n dotWrapperSize: \"relative z-10 mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-background\",\n dot: \"flex h-4.5 w-4.5 items-center justify-center rounded-full border ring-4 ring-background\",\n contentPadding: \"flex-1 pb-5 pt-0.5\",\n titleRowSpacing: \"flex min-w-0 flex-col gap-1 sm:flex-row sm:items-start sm:justify-between\",\n title: \"pr-4 text-[13px] leading-relaxed text-foreground\",\n time: \"mt-0.5 shrink-0 whitespace-nowrap text-[11px] text-muted-foreground/70\",\n cardContainer: \"overflow-hidden rounded-md border border-border/80 bg-muted/20\",\n cardHeader: \"px-3 pt-2.5\",\n cardBody: \"px-3 py-2.5 text-sm\",\n cardFooter: \"px-3 pb-2.5\",\n collapsedPreview: \"flex items-center justify-between gap-2 px-3 py-2.5 text-sm text-muted-foreground\",\n actionLinkRow: \"flex items-center gap-3 px-3 pb-2.5\",\n actionLink: \"inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\",\n nonInteractiveContent: \"pr-2 text-sm leading-relaxed text-muted-foreground\",\n },\n \"case-panel\": {\n outerRowGap: \"group relative flex gap-3\",\n connector: \"absolute left-[11px] top-6 bottom-[-2px] w-px bg-border/50\",\n dotWrapperSize: \"relative z-10 mt-0.5 flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full bg-background\",\n dot: \"flex h-[22px] w-[22px] items-center justify-center rounded-full border ring-4 ring-background [&>svg]:h-3 [&>svg]:w-3\",\n contentPadding: \"flex-1 min-w-0 pb-5 pt-px\",\n titleRowSpacing: \"flex min-w-0 items-start justify-between gap-3\",\n title: \"min-w-0 pr-1 text-[13.5px] font-medium leading-tight text-foreground\",\n time: \"shrink-0 whitespace-nowrap pt-px text-[11px] leading-tight text-muted-foreground/60\",\n cardContainer: \"overflow-hidden rounded-lg border border-border/70 bg-card\",\n cardHeader: \"flex items-start justify-between border-b border-border/60 bg-muted/30 px-3 py-2 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70\",\n cardBody: \"px-3 py-2.5 text-[13px] leading-relaxed\",\n cardFooter: \"border-t border-border/60 bg-muted/10 px-3 py-1.5\",\n collapsedPreview: \"flex items-center justify-between gap-2 px-3 py-2 text-[13px] text-muted-foreground\",\n actionLinkRow: \"flex items-center justify-end gap-2 px-3 py-1.5\",\n actionLink: \"inline-flex items-center gap-1 text-[11px] font-medium text-muted-foreground/70 transition-colors hover:text-foreground\",\n nonInteractiveContent: \"pr-2 text-[13px] leading-relaxed text-muted-foreground\",\n },\n}\n\nexport interface TimelineActivityProps {\n events: TimelineEvent[]\n className?: string\n variant?: TimelineActivityVariant\n}\n\nexport function TimelineActivity({ events, className, variant = \"default\" }: TimelineActivityProps) {\n return (\n <div className={cn(\"space-y-0\", className)} data-variant={variant}>\n {events.map((event, index) => (\n <TimelineItem\n key={event.id}\n event={event}\n isLast={index === events.length - 1}\n variant={variant}\n />\n ))}\n </div>\n )\n}\n\nfunction ActorByline({ actor, time }: { actor: TimelineEventActor; time: string }) {\n if (actor.kind === \"system\") return null\n\n if (actor.kind === \"integration\") {\n return (\n <div className=\"mt-1 flex items-center gap-1.5 text-xs text-muted-foreground\" data-testid=\"actor-byline\">\n <span>Integration</span>\n <span className=\"text-muted-foreground/40\">&middot;</span>\n <span>{time}</span>\n </div>\n )\n }\n\n // actor.kind === \"user\"\n const verb = actor.verb ?? \"performed this action\"\n const displayInitials = actor.initials ?? (actor.name ? actor.name.charAt(0).toUpperCase() : \"?\")\n\n return (\n <div className=\"mt-1 flex items-center gap-1.5 text-xs text-muted-foreground\" data-testid=\"actor-byline\">\n {actor.avatarUrl ? (\n <img\n src={actor.avatarUrl}\n alt={actor.name ?? \"User\"}\n className=\"h-4 w-4 rounded-full object-cover\"\n />\n ) : (\n <span className=\"flex h-4 w-4 items-center justify-center rounded-full bg-muted-foreground/10 text-[8px] font-semibold text-muted-foreground\">\n {displayInitials}\n </span>\n )}\n {actor.name && <span className=\"text-foreground font-medium\">{actor.name}</span>}\n <span>{verb}</span>\n <span className=\"text-muted-foreground/40\">&middot;</span>\n <span>{time}</span>\n </div>\n )\n}\n\nfunction TimelineItem({\n event,\n isLast,\n variant,\n}: {\n event: TimelineEvent\n isLast: boolean\n variant: TimelineActivityVariant\n}) {\n const [expanded, setExpanded] = React.useState(event.defaultExpanded ?? false)\n const [showAllRecipients, setShowAllRecipients] = React.useState(false)\n const hasContent = !!event.content\n const hasEmail = !!event.email\n const hasGongCall = !!event.gongCall\n const classes = TIMELINE_VARIANT_CLASSES[variant]\n\n const toneStyle = event.tone ? TONE_CLASSES[event.tone] : null\n const dotClasses = toneStyle ? toneStyle.dot : NEUTRAL_DOT_CLASSES\n const iconClasses = toneStyle ? toneStyle.icon : NEUTRAL_ICON_CLASSES\n\n return (\n <div className={classes.outerRowGap}>\n {!isLast && (\n <div className={classes.connector} />\n )}\n\n <div className={classes.dotWrapperSize}>\n <div className={cn(classes.dot, dotClasses, iconClasses)} data-testid=\"timeline-dot\">\n {event.icon}\n </div>\n </div>\n\n <div className={classes.contentPadding}>\n <div className={classes.titleRowSpacing}>\n <div className={classes.title}>\n {event.title}\n </div>\n <span className={classes.time}>\n {event.time}\n </span>\n </div>\n\n {event.actor && <ActorByline actor={event.actor} time={event.time} />}\n\n {(hasContent || hasEmail || hasGongCall) && (\n <div className=\"mt-2\">\n {event.isInteractive ? (\n hasGongCall ? (\n <GongCallCard\n event={event}\n expanded={expanded}\n setExpanded={setExpanded}\n variant={variant}\n classes={classes}\n />\n ) : hasEmail ? (\n <EmailCard\n event={event}\n expanded={expanded}\n setExpanded={setExpanded}\n showAllRecipients={showAllRecipients}\n setShowAllRecipients={setShowAllRecipients}\n variant={variant}\n classes={classes}\n />\n ) : (\n <ContentCard\n event={event}\n expanded={expanded}\n setExpanded={setExpanded}\n variant={variant}\n classes={classes}\n />\n )\n ) : (\n <div className={classes.nonInteractiveContent}>\n {event.content}\n </div>\n )}\n </div>\n )}\n </div>\n </div>\n )\n}\n\ntype TimelineEmail = NonNullable<TimelineEvent[\"email\"]>\ntype TimelineGongCall = NonNullable<TimelineEvent[\"gongCall\"]>\ntype TimelineSource = NonNullable<TimelineEvent[\"source\"]>\n\nfunction reactNodeToDisplayText(value: React.ReactNode): string {\n if (typeof value === \"string\" || typeof value === \"number\") return decodeEmailDisplayText(String(value))\n return \"\"\n}\n\n/**\n * Formats a call length in seconds to a compact human duration:\n * \"38 min\", \">= 60 min\" → \"1 h 12 min\". Returns \"\" for absent/invalid\n * input so callers can omit the chip entirely (never render \"0 min\").\n */\nfunction formatCallDuration(durationSeconds?: number | null): string {\n if (durationSeconds == null || !Number.isFinite(durationSeconds) || durationSeconds <= 0) return \"\"\n const totalMinutes = Math.round(durationSeconds / 60)\n if (totalMinutes < 1) return \"< 1 min\"\n if (totalMinutes < 60) return `${totalMinutes} min`\n const hours = Math.floor(totalMinutes / 60)\n const minutes = totalMinutes % 60\n return minutes === 0 ? `${hours} h` : `${hours} h ${minutes} min`\n}\n\nfunction cleanGongText(value?: string | null): string {\n if (!value) return \"\"\n return decodeEmailDisplayText(value).trim()\n}\n\n/**\n * True once the component has mounted on the client. Timestamps render in\n * UTC for SSR + the hydration pass (server and client HTML must match — the\n * server has no idea what timezone the viewer is in), then flip to the\n * viewer's local timezone immediately after mount.\n */\nfunction useHydrated(): boolean {\n const [hydrated, setHydrated] = React.useState(false)\n React.useEffect(() => {\n setHydrated(true)\n }, [])\n return hydrated\n}\n\ntype TimestampDisplayOptions = { utcTimestamps?: boolean }\n\nfunction timestampTimeZone(opts?: TimestampDisplayOptions): { timeZone?: string } {\n return opts?.utcTimestamps ? { timeZone: \"UTC\" } : {}\n}\n\nfunction getTimelineGongCallDisplay(gongCall: TimelineGongCall, opts?: TimestampDisplayOptions) {\n const title = cleanGongText(gongCall.title)\n const startTime = formatEmailTimestamp(gongCall.startTime, timestampTimeZone(opts)) ?? \"\"\n const duration = formatCallDuration(gongCall.durationSeconds)\n const direction = cleanGongText(gongCall.direction)\n const outcome = cleanGongText(gongCall.outcome)\n const brief = cleanGongText(gongCall.brief)\n const keyPoints = cleanGongText(gongCall.keyPoints)\n const nextSteps = cleanGongText(gongCall.nextSteps)\n const url = gongCall.url?.trim() || \"\"\n const snippet = brief.split(\"\\n\").find((line) => line.trim())?.trim() ?? \"\"\n\n return { title, startTime, duration, direction, outcome, brief, keyPoints, nextSteps, url, snippet }\n}\n\nfunction getTimelineEmailDisplay(email: TimelineEmail, opts?: TimestampDisplayOptions) {\n const sender = normalizeEmailSender({ name: email.from, email: email.fromEmail })\n const to = formatAddressList(email.to) || decodeEmailDisplayText(email.to ?? \"\")\n const cc = formatAddressList(email.cc) || decodeEmailDisplayText(email.cc ?? \"\")\n const bcc = formatAddressList(email.bcc) || decodeEmailDisplayText(email.bcc ?? \"\")\n const date = formatEmailTimestamp(email.date, timestampTimeZone(opts)) ?? decodeEmailDisplayText(email.date ?? \"\")\n const subject = email.subject ? decodeEmailDisplayText(email.subject) : \"\"\n const bodyText = reactNodeToDisplayText(email.body)\n const snippet = emailBodySnippet({ bodyHtml: email.bodyHtml, body: bodyText }, 140)\n const hasRecipients = Boolean(to || cc || bcc)\n\n return { sender, to, cc, bcc, date, subject, bodyText, snippet, hasRecipients }\n}\n\nfunction EmailMetadata({\n email,\n showAllRecipients,\n setShowAllRecipients,\n}: {\n email: TimelineEmail\n showAllRecipients: boolean\n setShowAllRecipients: React.Dispatch<React.SetStateAction<boolean>>\n}) {\n const hydrated = useHydrated()\n const display = getTimelineEmailDisplay(email, { utcTimestamps: !hydrated })\n const hasExpandableRecipients = Boolean(display.cc || display.bcc)\n\n return (\n <>\n {/* Row 1: sender name + email, with the date right-aligned. The `gap-4`\n guarantees whitespace between the sender block and the date so they\n never visually jam together. */}\n <div className=\"flex items-center justify-between gap-4\">\n <div className=\"flex min-w-0 items-baseline gap-1.5\">\n <span className=\"font-semibold text-foreground text-[13px] whitespace-nowrap\">{display.sender.name}</span>\n {display.sender.email ? (\n <span className=\"text-muted-foreground/60 text-xs truncate\">{display.sender.email}</span>\n ) : null}\n </div>\n {display.date ? (\n <span className=\"shrink-0 text-xs text-muted-foreground/50 whitespace-nowrap\">{display.date}</span>\n ) : null}\n </div>\n {/* Recipient lines live on their own full-width, stacked rows below the\n sender/date row. Each line truncates independently. The To-segment is\n suppressed entirely when there is no recipient data (history emails)\n rather than rendering a meaningless \"no recipient yet\" placeholder. */}\n {display.hasRecipients ? (\n <div className=\"mt-0.5 text-xs text-muted-foreground\">\n <div className=\"flex items-center gap-1\">\n <span className=\"min-w-0 flex-1 truncate\">\n {display.to ? (\n <>To {display.to}</>\n ) : (\n <>\n <span className=\"text-muted-foreground/40\">cc</span> {display.cc || display.bcc}\n </>\n )}\n {display.to && !showAllRecipients && hasExpandableRecipients ? (\n <>, &hellip;</>\n ) : null}\n </span>\n {hasExpandableRecipients ? (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n setShowAllRecipients((prev) => !prev)\n }}\n className=\"shrink-0 text-muted-foreground/40 hover:text-muted-foreground transition-colors\"\n aria-label={showAllRecipients ? \"Hide cc and bcc\" : \"Show cc and bcc\"}\n >\n <ChevronDown className={cn(\"h-3 w-3 transition-transform\", showAllRecipients && \"rotate-180\")} />\n </button>\n ) : null}\n </div>\n {showAllRecipients && display.to && display.cc ? (\n <div className=\"truncate\">\n <span className=\"text-muted-foreground/40\">cc</span> {display.cc}\n </div>\n ) : null}\n {showAllRecipients && display.bcc ? (\n <div className=\"truncate\">\n <span className=\"text-muted-foreground/40\">bcc</span> {display.bcc}\n </div>\n ) : null}\n </div>\n ) : null}\n </>\n )\n}\n\nfunction SuppressedHtmlSection({\n html,\n showLabel,\n hideLabel,\n slot,\n}: {\n html: string\n showLabel: string\n hideLabel: string\n slot: string\n}) {\n const [open, setOpen] = React.useState(false)\n\n return (\n <div className=\"mt-2\" data-slot={slot}>\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n setOpen((value) => !value)\n }}\n aria-expanded={open}\n className=\"inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground\"\n >\n {open ? hideLabel : showLabel}\n <ChevronDown className={cn(\"h-3 w-3 transition-transform\", open && \"rotate-180\")} />\n </button>\n {open ? (\n <div\n data-slot={`${slot}-content`}\n className=\"mt-2 border-l-2 border-border pl-3 text-sm leading-relaxed text-muted-foreground\"\n >\n <SharedEmailBody html={html} variant=\"history\" collapseDetails={false} />\n </div>\n ) : null}\n </div>\n )\n}\n\nfunction TimelineEmailBody({ email }: { email: TimelineEmail }) {\n const display = getTimelineEmailDisplay(email)\n const bodyFallback = display.bodyText || (typeof email.body === \"string\" ? email.body : \"\")\n\n if (!email.bodyHtml && !bodyFallback && email.body) {\n return <div className=\"whitespace-pre-line text-sm leading-relaxed text-foreground/90\">{email.body}</div>\n }\n\n const body = (\n <SharedEmailBody\n html={email.bodyHtml}\n text={bodyFallback}\n variant=\"history\"\n collapseDetails={true}\n className=\"text-sm leading-relaxed\"\n />\n )\n\n const hasSignature = Boolean(email.signatureHtml && email.signatureHtml.trim())\n const hasQuoted = Boolean(email.quotedHtml && email.quotedHtml.trim())\n\n // Fast path: no signature/quoted slots → identical output to before.\n if (!hasSignature && !hasQuoted) {\n return email.bodyHtml ? <div data-slot=\"timeline-email-html\">{body}</div> : body\n }\n\n return (\n <div data-slot={email.bodyHtml ? \"timeline-email-html\" : undefined}>\n {body}\n {hasSignature ? (\n <SuppressedHtmlSection\n html={email.signatureHtml as string}\n showLabel=\"Show signature\"\n hideLabel=\"Hide signature\"\n slot=\"timeline-email-signature\"\n />\n ) : null}\n {hasQuoted ? (\n <SuppressedHtmlSection\n html={email.quotedHtml as string}\n showLabel=\"Show quoted text\"\n hideLabel=\"Hide quoted text\"\n slot=\"timeline-email-quoted\"\n />\n ) : null}\n </div>\n )\n}\n\nfunction renderDecodedPreview(preview?: React.ReactNode): React.ReactNode {\n if (typeof preview === \"string\" || typeof preview === \"number\") return decodeEmailDisplayText(String(preview))\n return preview\n}\n\nfunction CollapsedEmailPreview({\n email,\n preview,\n className,\n actionClassName,\n onClick,\n}: {\n email?: TimelineEmail\n preview?: React.ReactNode\n className: string\n actionClassName: string\n onClick?: () => void\n}) {\n const display = email ? getTimelineEmailDisplay(email) : null\n const previewContent = display?.snippet || renderDecodedPreview(preview)\n\n return (\n <div className={className} onClick={onClick}>\n <span className=\"line-clamp-1 min-w-0 flex-1 pr-3 text-[13px]\">\n <span className=\"text-muted-foreground\">{display?.sender.name}</span>\n <span className=\"mx-1.5 text-muted-foreground/40\">&middot;</span>\n {display?.subject ? (\n <>\n <span className=\"text-muted-foreground\">{display.subject}</span>\n <span className=\"mx-1.5 text-muted-foreground/40\">&middot;</span>\n </>\n ) : null}\n <span className=\"text-muted-foreground\">{previewContent}</span>\n </span>\n <button type=\"button\" className={actionClassName}>\n Expand <ChevronDown className=\"h-3 w-3\" />\n </button>\n </div>\n )\n}\n\nfunction ShowLessButton({\n className,\n onClick,\n type,\n}: {\n className: string\n onClick: React.MouseEventHandler<HTMLButtonElement>\n type?: \"button\"\n}) {\n return (\n <button type={type} onClick={onClick} className={className}>\n Show less <ChevronUp className=\"h-3 w-3\" />\n </button>\n )\n}\n\n/**\n * Top-of-card collapse affordance. Long expanded emails are tedious to collapse\n * from the bottom alone, so the header row also carries a \"Show less\" control.\n */\nfunction CollapseTopButton({\n onClick,\n className,\n}: {\n onClick: React.MouseEventHandler<HTMLButtonElement>\n className?: string\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label=\"Show less\"\n data-slot=\"timeline-collapse-top\"\n className={cn(\n \"shrink-0 inline-flex items-center gap-1 text-[11px] font-medium text-muted-foreground/60 transition-colors hover:text-foreground\",\n className,\n )}\n >\n <span className=\"sr-only sm:not-sr-only\">Show less</span>\n <ChevronUp className=\"h-3 w-3\" />\n </button>\n )\n}\n\nfunction SourceAction({\n source,\n onSourceClick,\n className,\n}: {\n source: TimelineSource\n onSourceClick?: () => void\n className: string\n}) {\n const actionLabel = source.actionLabel ?? `Open in ${source.label}`\n\n if (onSourceClick) {\n return (\n <button\n type=\"button\"\n onClick={(e) => { e.stopPropagation(); onSourceClick(); }}\n className={className}\n >\n {actionLabel}\n <ExternalLink className=\"h-3 w-3\" />\n </button>\n )\n }\n\n return (\n <a\n href={source.url}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className={className}\n >\n {actionLabel}\n <ExternalLink className=\"h-3 w-3\" />\n </a>\n )\n}\n\nfunction GongLinkAction({\n url,\n className,\n}: {\n url: string\n className: string\n}) {\n return (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={(e) => e.stopPropagation()}\n className={className}\n data-slot=\"timeline-gong-link\"\n >\n View in Gong\n <ExternalLink className=\"h-3 w-3\" />\n </a>\n )\n}\n\n/**\n * The expanded Gong call card body — call title + start time header, optional\n * direction/outcome chips, the AI brief as primary body text, and labeled\n * Key points / Next steps subsections. Every section renders only when its\n * content is present.\n */\nfunction TimelineGongCallBody({\n display,\n labelClassName,\n}: {\n display: ReturnType<typeof getTimelineGongCallDisplay>\n labelClassName: string\n}) {\n return (\n <div className=\"space-y-3\" data-slot=\"timeline-gong-body\">\n {display.brief ? (\n <div className=\"whitespace-pre-line text-sm leading-relaxed text-foreground/90\">\n {display.brief}\n </div>\n ) : null}\n {display.keyPoints ? (\n <div data-slot=\"timeline-gong-key-points\">\n <div className={labelClassName}>Key points</div>\n <div className=\"mt-1 whitespace-pre-line text-sm leading-relaxed text-foreground/90\">\n {display.keyPoints}\n </div>\n </div>\n ) : null}\n {display.nextSteps ? (\n <div data-slot=\"timeline-gong-next-steps\">\n <div className={labelClassName}>Next steps</div>\n <div className=\"mt-1 whitespace-pre-line text-sm leading-relaxed text-foreground/90\">\n {display.nextSteps}\n </div>\n </div>\n ) : null}\n </div>\n )\n}\n\nfunction GongCallHeader({\n display,\n}: {\n display: ReturnType<typeof getTimelineGongCallDisplay>\n}) {\n return (\n <>\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <Phone className=\"h-3.5 w-3.5 shrink-0 text-muted-foreground/60\" />\n <span className=\"min-w-0 truncate font-semibold text-foreground text-[13px]\">{display.title}</span>\n </div>\n {display.startTime ? (\n <span className=\"shrink-0 text-xs text-muted-foreground/50 whitespace-nowrap\">{display.startTime}</span>\n ) : null}\n </div>\n {display.duration || display.direction || display.outcome ? (\n <div className=\"mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground\">\n {display.duration ? <span data-slot=\"timeline-gong-duration\">{display.duration}</span> : null}\n {display.direction ? (\n <span className=\"rounded bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground\">{display.direction}</span>\n ) : null}\n {display.outcome ? (\n <span className=\"rounded bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground\">{display.outcome}</span>\n ) : null}\n </div>\n ) : null}\n </>\n )\n}\n\nfunction CollapsedGongCallPreview({\n display,\n className,\n actionClassName,\n onClick,\n}: {\n display: ReturnType<typeof getTimelineGongCallDisplay>\n className: string\n actionClassName: string\n onClick?: () => void\n}) {\n return (\n <div className={className} onClick={onClick}>\n <span className=\"line-clamp-1 min-w-0 flex-1 pr-3 text-[13px] text-muted-foreground\">\n {display.duration ? (\n <>\n <span className=\"inline-flex items-center gap-1\">\n <Phone className=\"h-3 w-3\" />\n {display.duration}\n </span>\n {display.snippet ? <span className=\"mx-1.5 text-muted-foreground/40\">&middot;</span> : null}\n </>\n ) : null}\n {display.snippet ? <span>{display.snippet}</span> : null}\n </span>\n <button type=\"button\" className={actionClassName}>\n Expand <ChevronDown className=\"h-3 w-3\" />\n </button>\n </div>\n )\n}\n\nfunction GongCallCard({\n event,\n expanded,\n setExpanded,\n variant,\n classes,\n}: {\n event: TimelineEvent\n expanded: boolean\n setExpanded: React.Dispatch<React.SetStateAction<boolean>>\n variant: TimelineActivityVariant\n classes: TimelineVariantClasses\n}) {\n const gongCall = event.gongCall as TimelineGongCall\n const hydrated = useHydrated()\n const display = getTimelineGongCallDisplay(gongCall, { utcTimestamps: !hydrated })\n\n if (variant === \"default\") {\n return (\n <div className={classes.cardContainer} data-variant={variant} data-slot=\"timeline-gong-card\">\n <div\n className={cn(\n \"px-3 py-2.5 text-sm\",\n !expanded && \"cursor-pointer hover:bg-muted/30 transition-colors\",\n )}\n onClick={() => !expanded && setExpanded(true)}\n >\n {expanded ? (\n <div className=\"space-y-3\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0 flex-1\">\n <GongCallHeader display={display} />\n </div>\n <CollapseTopButton\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className=\"mt-0.5\"\n />\n </div>\n\n <TimelineGongCallBody\n display={display}\n labelClassName=\"text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70\"\n />\n\n <div className=\"mt-2 flex items-center gap-3\">\n {display.url ? (\n <GongLinkAction\n url={display.url}\n className=\"mr-auto inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n />\n ) : null}\n <ShowLessButton\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className=\"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n />\n </div>\n </div>\n ) : (\n <CollapsedGongCallPreview\n display={display}\n className=\"flex items-center justify-between gap-2 text-muted-foreground\"\n actionClassName=\"flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground\"\n />\n )}\n </div>\n </div>\n )\n }\n\n return (\n <div className={classes.cardContainer} data-variant={variant} data-slot=\"timeline-gong-card\">\n {expanded ? (\n <>\n <div className={classes.cardHeader} data-slot=\"timeline-card-header\">\n <div className=\"min-w-0 flex-1 normal-case tracking-normal\">\n <GongCallHeader display={display} />\n </div>\n <CollapseTopButton\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className=\"ml-3 normal-case tracking-normal\"\n />\n </div>\n\n <div className={classes.cardBody} data-slot=\"timeline-card-body\">\n <TimelineGongCallBody\n display={display}\n labelClassName=\"text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70\"\n />\n </div>\n\n <div\n className={cn(classes.cardFooter, classes.actionLinkRow, display.url ? \"justify-between\" : \"justify-end\")}\n data-slot=\"timeline-card-footer\"\n >\n {display.url ? <GongLinkAction url={display.url} className={classes.actionLink} /> : null}\n <ShowLessButton\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className={classes.actionLink}\n />\n </div>\n </>\n ) : (\n <CollapsedGongCallPreview\n display={display}\n className={cn(classes.collapsedPreview, \"cursor-pointer hover:bg-muted/30 transition-colors\")}\n actionClassName={cn(classes.actionLink, \"shrink-0\")}\n onClick={() => setExpanded(true)}\n />\n )}\n </div>\n )\n}\n\nfunction EmailCard({\n event,\n expanded,\n setExpanded,\n showAllRecipients,\n setShowAllRecipients,\n variant,\n classes,\n}: {\n event: TimelineEvent\n expanded: boolean\n setExpanded: React.Dispatch<React.SetStateAction<boolean>>\n showAllRecipients: boolean\n setShowAllRecipients: React.Dispatch<React.SetStateAction<boolean>>\n variant: TimelineActivityVariant\n classes: TimelineVariantClasses\n}) {\n if (variant === \"default\") {\n return (\n <div className={classes.cardContainer} data-variant={variant}>\n <div\n className={cn(\n \"px-3 py-2.5 text-sm\",\n !expanded && \"cursor-pointer hover:bg-muted/30 transition-colors\"\n )}\n onClick={() => !expanded && setExpanded(true)}\n >\n {expanded && event.email ? (\n <div className=\"space-y-3\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0 flex-1\">\n <EmailMetadata\n email={event.email}\n showAllRecipients={showAllRecipients}\n setShowAllRecipients={setShowAllRecipients}\n />\n </div>\n <CollapseTopButton\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className=\"mt-0.5\"\n />\n </div>\n\n <TimelineEmailBody email={event.email} />\n\n {event.content ? (\n <div className=\"rounded-md bg-muted/30 px-2.5 py-2 text-xs text-muted-foreground\">\n {event.content}\n </div>\n ) : null}\n\n <div className=\"mt-2 flex items-center gap-3\">\n {event.source ? (\n <SourceAction\n source={event.source}\n onSourceClick={event.onSourceClick}\n className=\"mr-auto inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n />\n ) : null}\n <ShowLessButton\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className=\"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n />\n </div>\n </div>\n ) : (\n <CollapsedEmailPreview\n email={event.email}\n preview={event.preview}\n className=\"flex items-center justify-between gap-2 text-muted-foreground\"\n actionClassName=\"flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground\"\n />\n )}\n </div>\n </div>\n )\n }\n\n return (\n <div className={classes.cardContainer} data-variant={variant}>\n {expanded && event.email ? (\n <>\n <div className={classes.cardHeader} data-slot=\"timeline-card-header\">\n <div className=\"min-w-0 flex-1 normal-case tracking-normal\">\n <EmailMetadata\n email={event.email}\n showAllRecipients={showAllRecipients}\n setShowAllRecipients={setShowAllRecipients}\n />\n </div>\n <CollapseTopButton\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className=\"ml-3 normal-case tracking-normal\"\n />\n </div>\n\n <div className={classes.cardBody} data-slot=\"timeline-card-body\">\n <TimelineEmailBody email={event.email} />\n {event.content ? (\n <div className=\"mt-3 rounded-md bg-muted/30 px-2.5 py-2 text-xs text-muted-foreground\">\n {event.content}\n </div>\n ) : null}\n </div>\n\n <div\n className={cn(classes.cardFooter, classes.actionLinkRow, event.source ? \"justify-between\" : \"justify-end\")}\n data-slot=\"timeline-card-footer\"\n >\n {event.source ? (\n <SourceAction\n source={event.source}\n onSourceClick={event.onSourceClick}\n className={classes.actionLink}\n />\n ) : null}\n <ShowLessButton\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className={classes.actionLink}\n />\n </div>\n </>\n ) : (\n <CollapsedEmailPreview\n email={event.email}\n preview={event.preview}\n className={cn(classes.collapsedPreview, \"cursor-pointer hover:bg-muted/30 transition-colors\")}\n actionClassName={cn(classes.actionLink, \"shrink-0\")}\n onClick={() => setExpanded(true)}\n />\n )}\n </div>\n )\n}\n\nfunction ContentCard({\n event,\n expanded,\n setExpanded,\n variant,\n classes,\n}: {\n event: TimelineEvent\n expanded: boolean\n setExpanded: React.Dispatch<React.SetStateAction<boolean>>\n variant: TimelineActivityVariant\n classes: TimelineVariantClasses\n}) {\n if (variant === \"default\") {\n return (\n <div className={classes.cardContainer} data-variant={variant}>\n <div\n className={cn(\n \"px-3 py-2.5 text-sm\",\n !expanded && \"cursor-pointer hover:bg-muted/30 transition-colors\"\n )}\n onClick={() => !expanded && setExpanded(true)}\n >\n {expanded ? (\n <div className=\"space-y-2\">\n {event.content}\n <div className=\"mt-2 flex items-center gap-3\">\n {event.source ? (\n <SourceAction\n source={event.source}\n onSourceClick={event.onSourceClick}\n className=\"mr-auto inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n />\n ) : null}\n <ShowLessButton\n onClick={(e) => { e.stopPropagation(); setExpanded(false); }}\n className=\"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n />\n </div>\n </div>\n ) : (\n <div className=\"flex items-center justify-between gap-2 text-muted-foreground\">\n <span className=\"line-clamp-1 pr-3\">\n {event.preview ?? event.content}\n </span>\n <button className=\"flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground\">\n Expand <ChevronDown className=\"h-3 w-3\" />\n </button>\n </div>\n )}\n </div>\n </div>\n )\n }\n\n return (\n <div className={classes.cardContainer} data-variant={variant}>\n {expanded ? (\n <>\n <div className={classes.cardBody} data-slot=\"timeline-card-body\">\n {event.content}\n </div>\n <div className={cn(classes.cardFooter, classes.actionLinkRow, event.source ? \"justify-between\" : \"justify-end\")} data-slot=\"timeline-card-footer\">\n {event.source ? (\n <SourceAction\n source={event.source}\n onSourceClick={event.onSourceClick}\n className={classes.actionLink}\n />\n ) : null}\n <ShowLessButton\n type=\"button\"\n onClick={(e) => { e.stopPropagation(); setExpanded(false); }}\n className={classes.actionLink}\n />\n </div>\n </>\n ) : (\n <div\n className={cn(classes.collapsedPreview, \"cursor-pointer hover:bg-muted/30 transition-colors\")}\n onClick={() => setExpanded(true)}\n >\n <span className=\"line-clamp-1 pr-3\">\n {event.preview ?? event.content}\n </span>\n <button type=\"button\" className={cn(classes.actionLink, \"shrink-0\")}>\n Expand <ChevronDown className=\"h-3 w-3\" />\n </button>\n </div>\n )}\n </div>\n )\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.23",
3
+ "version": "0.20.25",
4
4
  "description": "Handled UI component library (shadcn-style, New York)",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@9.12.0",
@@ -48,11 +48,21 @@ describe("email display helpers", () => {
48
48
  })
49
49
 
50
50
  it("formats timestamps deterministically and omits invalid dates", () => {
51
- expect(formatEmailTimestamp("2026-06-08T20:45:00.000Z")).toBe("Jun 8, 2026, 8:45 PM")
51
+ // Explicit timeZone keeps assertions deterministic across runner TZs.
52
+ expect(formatEmailTimestamp("2026-06-08T20:45:00.000Z", { timeZone: "UTC" })).toBe("Jun 8, 2026, 8:45 PM")
53
+ expect(formatEmailTimestamp("2026-06-08T20:45:00.000Z", { timeZone: "America/New_York" })).toBe("Jun 8, 2026, 4:45 PM")
52
54
  expect(formatEmailTimestamp("not-a-date")).toBeNull()
53
55
  expect(formatEmailTimestamp(null)).toBeNull()
54
56
  })
55
57
 
58
+ it("defaults to the runtime's local timezone (viewer-local in the browser)", () => {
59
+ const value = "2026-06-08T20:45:00.000Z"
60
+ const expected = new Intl.DateTimeFormat("en-US", {
61
+ month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit",
62
+ }).format(new Date(value))
63
+ expect(formatEmailTimestamp(value)).toBe(expected)
64
+ })
65
+
56
66
  it("splits HTML signatures, Gmail quotes, and disclaimers while preserving formatting", () => {
57
67
  const disclaimerSplit = splitEmailHtmlForDisplay("<p>Please review.</p><p>Confidentiality Notice: this message is private.</p>")
58
68
  expect(disclaimerSplit.bodyHtml).toContain("Please review")
@@ -69,6 +79,40 @@ describe("email display helpers", () => {
69
79
  expect(split.detailsHtml).toContain('class="gmail_quote"')
70
80
  })
71
81
 
82
+ it("keeps blank-line markers when the footer split rebuilds the body (Gmail wire format)", () => {
83
+ // The Gmail wire format marks blank lines as <div><br></div>; the rebuilt
84
+ // body must keep them or paragraph spacing collapses in preview/history.
85
+ const wire =
86
+ '<div style="margin:0; line-height:1.4;">Hey Clint,</div>' +
87
+ '<div style="margin:0; line-height:1.4;"><br /></div>' +
88
+ '<div style="margin:0; line-height:1.4;">Admin access roles come with overhead.</div>' +
89
+ '<div style="margin:0; line-height:1.4;"><br /></div>' +
90
+ '<div style="margin:0; line-height:1.4;">Best,</div>' +
91
+ '<div style="margin:0; line-height:1.4;">Cory</div>'
92
+
93
+ const split = splitEmailHtmlForDisplay(wire)
94
+
95
+ // The signoff split fires ("Best," + name moves to details)…
96
+ expect(split.detailsHtml).toContain("Best,")
97
+ expect(split.bodyHtml).not.toContain("Best,")
98
+ // …and the body keeps both blank-line markers between paragraphs.
99
+ expect(split.bodyHtml.match(/<br\s*\/?>/g)?.length).toBe(2)
100
+ expect(split.bodyHtml).toContain("Hey Clint,")
101
+ expect(split.bodyHtml).toContain("Admin access roles come with overhead.")
102
+ })
103
+
104
+ it("keeps interior blank lines made of consecutive <br>s inside one block", () => {
105
+ const split = splitEmailHtmlForDisplay(
106
+ "<div>First paragraph.<br /><br />Second paragraph.</div>" +
107
+ "<div><br /></div><div>Thanks,</div><div>Jane</div>",
108
+ )
109
+
110
+ expect(split.detailsHtml).toContain("Thanks,")
111
+ // One blank from the double <br>, one from the standalone marker div that
112
+ // precedes the signoff boundary.
113
+ expect(split.bodyHtml.match(/<br\s*\/?>/g)?.length).toBe(2)
114
+ })
115
+
72
116
  it("splits plain text signatures while preserving line boundaries", () => {
73
117
  const split = splitEmailTextForDisplay("Hi Dana,\n\nLooks good.\n\n-- \nJane Doe\nVP Sales\njane@example.com")
74
118
 
@@ -404,7 +404,13 @@ describe("TimelineActivity", () => {
404
404
 
405
405
  fireEvent.click(screen.getByRole("button", { name: /Expand/i }))
406
406
 
407
- expect(screen.getByText("Jun 8, 2026, 3:30 PM")).toBeTruthy()
407
+ // Post-hydration the card shows the viewer's LOCAL timezone (the UTC
408
+ // rendering only exists for the SSR/hydration paint). Mirror the local
409
+ // formatting so the assertion is deterministic across runner timezones.
410
+ const expectedLocalDate = new Intl.DateTimeFormat("en-US", {
411
+ month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit",
412
+ }).format(new Date("2026-06-08T15:30:00.000Z"))
413
+ expect(screen.getByText(expectedLocalDate)).toBeTruthy()
408
414
  expect(screen.getByText(/Jane & Team <jane@example.com>/)).toBeTruthy()
409
415
  expect(container.querySelector('[data-slot="email-body-details"]')).toBeNull()
410
416
  expect(screen.getByRole("button", { name: "•••" })).toBeTruthy()
@@ -214,18 +214,25 @@ export function formatAddressList(input?: string | string[] | null): string {
214
214
  .join(", ")
215
215
  }
216
216
 
217
- export function formatEmailTimestamp(value?: string | Date | null): string | null {
217
+ export function formatEmailTimestamp(
218
+ value?: string | Date | null,
219
+ opts?: { timeZone?: string },
220
+ ): string | null {
218
221
  if (!value) return null
219
222
  const date = value instanceof Date ? value : new Date(value)
220
223
  if (Number.isNaN(date.getTime())) return null
221
224
 
225
+ // Default = the runtime's local timezone (the viewer's, in the browser).
226
+ // Callers that render during SSR/hydration pass timeZone "UTC" for the
227
+ // first paint so server and client HTML match, then re-render local after
228
+ // mount (see useHydrated in timeline-activity).
222
229
  return new Intl.DateTimeFormat("en-US", {
223
230
  month: "short",
224
231
  day: "numeric",
225
232
  year: "numeric",
226
233
  hour: "numeric",
227
234
  minute: "2-digit",
228
- timeZone: "UTC",
235
+ ...(opts?.timeZone ? { timeZone: opts.timeZone } : {}),
229
236
  }).format(date)
230
237
  }
231
238
 
@@ -302,6 +309,18 @@ function makeHtmlSegment(html: string): MessageSegment | null {
302
309
  return { html, visibleText }
303
310
  }
304
311
 
312
+ // Blank-line segments carry no visible text, so they never participate in
313
+ // footer-boundary detection — but their html must survive the rebuild, or a
314
+ // split body loses every intentional blank line between paragraphs (the
315
+ // Gmail wire format marks them as <div><br></div>).
316
+ function makeBlankLineSegment(html: string): MessageSegment {
317
+ return { html, visibleText: "" }
318
+ }
319
+
320
+ function isBlankLineHtml(html: string): boolean {
321
+ return Boolean(html.trim()) && !htmlToVisibleText(html) && !/<(?:img|hr)\b/i.test(html)
322
+ }
323
+
305
324
  function splitInlineNodes(nodes: readonly Node[], wrapper?: Element): MessageSegment[] {
306
325
  const containsBr = hasDirectBr(nodes)
307
326
  const chunks: string[][] = [[]]
@@ -315,14 +334,22 @@ function splitInlineNodes(nodes: readonly Node[], wrapper?: Element): MessageSeg
315
334
  chunks[chunks.length - 1]?.push(serializeNode(node))
316
335
  })
317
336
 
318
- return chunks
319
- .map((chunk) => chunk.join(""))
320
- .map((innerHtml) => {
321
- if (wrapper) return wrapHtmlLike(wrapper, innerHtml)
322
- return containsBr ? `<div>${innerHtml}</div>` : innerHtml
323
- })
324
- .map(makeHtmlSegment)
325
- .filter((segment): segment is MessageSegment => Boolean(segment))
337
+ const rendered = chunks.map((chunk) => chunk.join(""))
338
+ const segments: MessageSegment[] = []
339
+ rendered.forEach((innerHtml, index) => {
340
+ const html = wrapper ? wrapHtmlLike(wrapper, innerHtml) : containsBr ? `<div>${innerHtml}</div>` : innerHtml
341
+ const segment = makeHtmlSegment(html)
342
+ if (segment) {
343
+ segments.push(segment)
344
+ return
345
+ }
346
+ // An interior empty chunk sits between two <br>s: an intentional blank
347
+ // line. Leading/trailing empties stay dropped, as before.
348
+ if (containsBr && index > 0 && index < rendered.length - 1) {
349
+ segments.push(makeBlankLineSegment(wrapper ? wrapHtmlLike(wrapper, "<br>") : "<div><br></div>"))
350
+ }
351
+ })
352
+ return segments
326
353
  }
327
354
 
328
355
  function splitElementSegment(element: Element): MessageSegment[] {
@@ -333,6 +360,12 @@ function splitElementSegment(element: Element): MessageSegment[] {
333
360
  return segment ? [segment] : []
334
361
  }
335
362
 
363
+ // A block with no visible content (e.g. <div><br></div>, <p></p>) is a
364
+ // blank-line marker — keep it whole instead of splitting/dropping it.
365
+ if (isBlankLineHtml(element.outerHTML)) {
366
+ return [makeBlankLineSegment(element.outerHTML)]
367
+ }
368
+
336
369
  if (tagName === "div" && hasDirectBlockChild(element)) {
337
370
  const childSegments = splitHtmlNodes(Array.from(element.childNodes))
338
371
  return childSegments.length ? childSegments : ([makeHtmlSegment(element.outerHTML)].filter(Boolean) as MessageSegment[])
@@ -399,9 +432,15 @@ function splitHtmlSegmentsFallback(html: string): MessageSegment[] {
399
432
  const pushInline = (inlineHtml: string) => {
400
433
  const chunks = inlineHtml.split(BR_TAG_RE)
401
434
  const hadBr = chunks.length > 1
402
- chunks.forEach((chunk) => {
435
+ chunks.forEach((chunk, index) => {
403
436
  const segment = makeHtmlSegment(hadBr ? `<div>${chunk}</div>` : chunk)
404
- if (segment) segments.push(segment)
437
+ if (segment) {
438
+ segments.push(segment)
439
+ return
440
+ }
441
+ if (hadBr && index > 0 && index < chunks.length - 1) {
442
+ segments.push(makeBlankLineSegment("<div><br /></div>"))
443
+ }
405
444
  })
406
445
  }
407
446
 
@@ -422,13 +461,22 @@ function splitHtmlSegmentsFallback(html: string): MessageSegment[] {
422
461
  const segmentEnd = findMatchingCloseTag(html, tagName, openTagEnd)
423
462
  const blockHtml = html.slice(tagStart, segmentEnd)
424
463
 
425
- if (SPLITTABLE_BLOCK_TAGS.has(tagName) && BR_TAG_RE.test(blockHtml)) {
464
+ if (isBlankLineHtml(blockHtml)) {
465
+ segments.push(makeBlankLineSegment(blockHtml))
466
+ } else if (SPLITTABLE_BLOCK_TAGS.has(tagName) && BR_TAG_RE.test(blockHtml)) {
426
467
  const openTag = rawOpen
427
468
  const closeTag = `</${tagName}>`
428
469
  const inner = blockHtml.replace(new RegExp(`^${rawOpen.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i"), "").replace(new RegExp(`${closeTag}$`, "i"), "")
429
- inner.split(BR_TAG_RE).forEach((chunk) => {
470
+ const innerChunks = inner.split(BR_TAG_RE)
471
+ innerChunks.forEach((chunk, index) => {
430
472
  const segment = makeHtmlSegment(`${openTag}${chunk}${closeTag}`)
431
- if (segment) segments.push(segment)
473
+ if (segment) {
474
+ segments.push(segment)
475
+ return
476
+ }
477
+ if (index > 0 && index < innerChunks.length - 1) {
478
+ segments.push(makeBlankLineSegment(`${openTag}<br />${closeTag}`))
479
+ }
432
480
  })
433
481
  } else {
434
482
  const segment = makeHtmlSegment(blockHtml)
@@ -392,9 +392,29 @@ function cleanGongText(value?: string | null): string {
392
392
  return decodeEmailDisplayText(value).trim()
393
393
  }
394
394
 
395
- function getTimelineGongCallDisplay(gongCall: TimelineGongCall) {
395
+ /**
396
+ * True once the component has mounted on the client. Timestamps render in
397
+ * UTC for SSR + the hydration pass (server and client HTML must match — the
398
+ * server has no idea what timezone the viewer is in), then flip to the
399
+ * viewer's local timezone immediately after mount.
400
+ */
401
+ function useHydrated(): boolean {
402
+ const [hydrated, setHydrated] = React.useState(false)
403
+ React.useEffect(() => {
404
+ setHydrated(true)
405
+ }, [])
406
+ return hydrated
407
+ }
408
+
409
+ type TimestampDisplayOptions = { utcTimestamps?: boolean }
410
+
411
+ function timestampTimeZone(opts?: TimestampDisplayOptions): { timeZone?: string } {
412
+ return opts?.utcTimestamps ? { timeZone: "UTC" } : {}
413
+ }
414
+
415
+ function getTimelineGongCallDisplay(gongCall: TimelineGongCall, opts?: TimestampDisplayOptions) {
396
416
  const title = cleanGongText(gongCall.title)
397
- const startTime = formatEmailTimestamp(gongCall.startTime) ?? ""
417
+ const startTime = formatEmailTimestamp(gongCall.startTime, timestampTimeZone(opts)) ?? ""
398
418
  const duration = formatCallDuration(gongCall.durationSeconds)
399
419
  const direction = cleanGongText(gongCall.direction)
400
420
  const outcome = cleanGongText(gongCall.outcome)
@@ -407,12 +427,12 @@ function getTimelineGongCallDisplay(gongCall: TimelineGongCall) {
407
427
  return { title, startTime, duration, direction, outcome, brief, keyPoints, nextSteps, url, snippet }
408
428
  }
409
429
 
410
- function getTimelineEmailDisplay(email: TimelineEmail) {
430
+ function getTimelineEmailDisplay(email: TimelineEmail, opts?: TimestampDisplayOptions) {
411
431
  const sender = normalizeEmailSender({ name: email.from, email: email.fromEmail })
412
432
  const to = formatAddressList(email.to) || decodeEmailDisplayText(email.to ?? "")
413
433
  const cc = formatAddressList(email.cc) || decodeEmailDisplayText(email.cc ?? "")
414
434
  const bcc = formatAddressList(email.bcc) || decodeEmailDisplayText(email.bcc ?? "")
415
- const date = formatEmailTimestamp(email.date) ?? decodeEmailDisplayText(email.date ?? "")
435
+ const date = formatEmailTimestamp(email.date, timestampTimeZone(opts)) ?? decodeEmailDisplayText(email.date ?? "")
416
436
  const subject = email.subject ? decodeEmailDisplayText(email.subject) : ""
417
437
  const bodyText = reactNodeToDisplayText(email.body)
418
438
  const snippet = emailBodySnippet({ bodyHtml: email.bodyHtml, body: bodyText }, 140)
@@ -430,7 +450,8 @@ function EmailMetadata({
430
450
  showAllRecipients: boolean
431
451
  setShowAllRecipients: React.Dispatch<React.SetStateAction<boolean>>
432
452
  }) {
433
- const display = getTimelineEmailDisplay(email)
453
+ const hydrated = useHydrated()
454
+ const display = getTimelineEmailDisplay(email, { utcTimestamps: !hydrated })
434
455
  const hasExpandableRecipients = Boolean(display.cc || display.bcc)
435
456
 
436
457
  return (
@@ -847,7 +868,8 @@ function GongCallCard({
847
868
  classes: TimelineVariantClasses
848
869
  }) {
849
870
  const gongCall = event.gongCall as TimelineGongCall
850
- const display = getTimelineGongCallDisplay(gongCall)
871
+ const hydrated = useHydrated()
872
+ const display = getTimelineGongCallDisplay(gongCall, { utcTimestamps: !hydrated })
851
873
 
852
874
  if (variant === "default") {
853
875
  return (