@handled-ai/design-system 0.20.30 → 0.20.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/account-contacts-popover.d.ts +1 -1
- package/dist/components/account-contacts-popover.js +25 -20
- package/dist/components/account-contacts-popover.js.map +1 -1
- package/dist/components/email-recipient-field.d.ts +9 -1
- package/dist/components/email-recipient-field.js +30 -4
- package/dist/components/email-recipient-field.js.map +1 -1
- package/dist/components/suggested-actions.d.ts +2 -1
- package/dist/components/suggested-actions.js.map +1 -1
- package/dist/components/timeline-activity.js +51 -41
- package/dist/components/timeline-activity.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/account-contacts-popover.test.tsx +71 -0
- package/src/components/__tests__/email-recipient-field.test.tsx +123 -0
- package/src/components/__tests__/timeline-activity.test.tsx +16 -0
- package/src/components/account-contacts-popover.tsx +33 -19
- package/src/components/email-recipient-field.tsx +45 -0
- package/src/components/suggested-actions.tsx +4 -3
- package/src/components/timeline-activity.tsx +7 -1
|
@@ -17,7 +17,7 @@ interface AccountContactsPopoverProps {
|
|
|
17
17
|
/** Optional replacement-selection callback. When provided, row clicks call this instead of additive onSelect/onSelectTo. */
|
|
18
18
|
onSelectSwitch?: (contact: SuggestedContact) => void;
|
|
19
19
|
onViewAll?: () => void;
|
|
20
|
-
onOpenRecentActivity?: () => void;
|
|
20
|
+
onOpenRecentActivity?: (contact: SuggestedContact) => void;
|
|
21
21
|
trigger: React.ReactNode;
|
|
22
22
|
iconMap?: SuggestedActionsIconMap;
|
|
23
23
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
Clock,
|
|
8
8
|
ExternalLink
|
|
9
9
|
} from "lucide-react";
|
|
10
|
+
import { cn } from "../lib/utils.js";
|
|
10
11
|
function BrandIcon({ src, alt, className }) {
|
|
11
12
|
return /* @__PURE__ */ jsx(
|
|
12
13
|
"img",
|
|
@@ -87,26 +88,30 @@ function AccountContactsPopover({
|
|
|
87
88
|
" \xB7 ",
|
|
88
89
|
(_f = (_e = (_c = (_b = c.email) != null ? _b : (_a = c.emails) == null ? void 0 : _a[0]) != null ? _c : c.phone) != null ? _e : (_d = c.phones) == null ? void 0 : _d[0]) != null ? _f : ""
|
|
89
90
|
] }),
|
|
90
|
-
c.lastActivity &&
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
91
|
+
c.lastActivity && (() => {
|
|
92
|
+
const activityContent = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
93
|
+
/* @__PURE__ */ jsx(Clock, { className: "w-3 h-3 shrink-0" }),
|
|
94
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 font-medium", children: "Last activity" }),
|
|
95
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground/60", children: "\xB7" }),
|
|
96
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0", children: c.lastActivity.date }),
|
|
97
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground/60", children: "\xB7" }),
|
|
98
|
+
/* @__PURE__ */ jsx("span", { className: "truncate capitalize", children: c.lastActivity.type })
|
|
99
|
+
] });
|
|
100
|
+
const chipBaseClass = "mt-1.5 flex max-w-full items-center gap-1.5 overflow-hidden rounded-md border border-border/70 bg-muted/30 px-2 py-1 text-[11px] text-muted-foreground";
|
|
101
|
+
return onOpenRecentActivity && c.lastActivity.timelineEventId ? /* @__PURE__ */ jsx(
|
|
102
|
+
"button",
|
|
103
|
+
{
|
|
104
|
+
type: "button",
|
|
105
|
+
onClick: (e) => {
|
|
106
|
+
e.stopPropagation();
|
|
107
|
+
onOpenRecentActivity(c);
|
|
108
|
+
setOpen(false);
|
|
109
|
+
},
|
|
110
|
+
className: cn(chipBaseClass, "hover:text-foreground hover:bg-muted/50 transition-colors"),
|
|
111
|
+
children: activityContent
|
|
112
|
+
}
|
|
113
|
+
) : /* @__PURE__ */ jsx("div", { className: chipBaseClass, children: activityContent });
|
|
114
|
+
})()
|
|
110
115
|
] }),
|
|
111
116
|
/* @__PURE__ */ jsxs("div", { className: "ml-2 flex items-center gap-1.5 shrink-0", children: [
|
|
112
117
|
resolvedDefaultSelectLabel && /* @__PURE__ */ jsx(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/account-contacts-popover.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport {\n Clock,\n ExternalLink,\n} from \"lucide-react\"\nimport type { SuggestedContact, SuggestedActionsIconMap } from \"./suggested-actions\"\n\n// ---------------------------------------------------------------------------\n// BrandIcon\n// ---------------------------------------------------------------------------\n\nexport function BrandIcon({ src, alt, className }: { src: string; alt: string; className?: string }) {\n return (\n <img\n src={src}\n alt={alt}\n className={`${className ?? \"\"} object-contain`}\n draggable={false}\n />\n )\n}\n\n// ---------------------------------------------------------------------------\n// AccountContactsPopover\n// ---------------------------------------------------------------------------\n\nexport interface AccountContactsPopoverProps {\n contacts: SuggestedContact[]\n onSelect?: (contact: SuggestedContact) => void\n onSelectTo?: (contact: SuggestedContact) => void\n onSelectCc?: (contact: SuggestedContact) => void\n onSelectBcc?: (contact: SuggestedContact) => void\n /** Label for the default contact row action. Defaults to \"Add\" or \"Switch\" when onSelectSwitch is provided. */\n defaultSelectLabel?: string\n /** Optional replacement-selection callback. When provided, row clicks call this instead of additive onSelect/onSelectTo. */\n onSelectSwitch?: (contact: SuggestedContact) => void\n onViewAll?: () => void\n onOpenRecentActivity?: () => void\n trigger: React.ReactNode\n iconMap?: SuggestedActionsIconMap\n}\n\nexport function AccountContactsPopover({\n contacts,\n onSelect,\n onSelectTo,\n onSelectCc,\n onSelectBcc,\n defaultSelectLabel,\n onSelectSwitch,\n onViewAll,\n onOpenRecentActivity,\n trigger,\n iconMap,\n}: AccountContactsPopoverProps) {\n const [open, setOpen] = React.useState(false)\n const triggerRef = React.useRef<HTMLDivElement>(null)\n const [popoverStyle, setPopoverStyle] = React.useState<React.CSSProperties>({})\n const isSwitchMode = Boolean(onSelectSwitch)\n const resolvedDefaultSelectLabel = defaultSelectLabel ?? (isSwitchMode ? \"Switch\" : undefined)\n const handleDefaultSelect = React.useCallback((contact: SuggestedContact) => {\n if (onSelectSwitch) {\n onSelectSwitch(contact)\n setOpen(false)\n return\n }\n\n const additiveSelect = onSelectTo ?? onSelect\n if (additiveSelect) {\n additiveSelect(contact)\n setOpen(false)\n }\n }, [onSelect, onSelectSwitch, onSelectTo])\n\n React.useEffect(() => {\n if (open && triggerRef.current) {\n const rect = triggerRef.current.getBoundingClientRect()\n const popoverWidth = Math.min(448, window.innerWidth - 32)\n let left = rect.right - popoverWidth\n if (left < 16) left = 16\n if (left + popoverWidth > window.innerWidth - 16) left = window.innerWidth - 16 - popoverWidth\n const popoverHeight = 320;\n const spaceBelow = window.innerHeight - rect.bottom - 8;\n const spaceAbove = rect.top - 8;\n const placeAbove = spaceBelow < popoverHeight && spaceAbove > spaceBelow;\n const top = placeAbove ? rect.top - popoverHeight - 4 : rect.bottom + 4;\n setPopoverStyle({ position: \"fixed\", top: Math.max(8, top), left, maxHeight: placeAbove ? spaceAbove : spaceBelow })\n }\n }, [open])\n\n return (\n <div>\n <div ref={triggerRef} onClick={() => setOpen(!open)}>{trigger}</div>\n {open && (\n <>\n <div className=\"fixed inset-0 z-40\" onClick={() => setOpen(false)} />\n <div style={popoverStyle} className=\"fixed bg-background border border-border rounded-lg shadow-xl z-50 w-[28rem] max-w-[calc(100vw-2rem)] py-2 animate-in fade-in slide-in-from-top-1 duration-150 overflow-y-auto\">\n <div className=\"px-3 py-1.5 text-[11px] font-medium text-muted-foreground/60 uppercase tracking-wide\">\n Account Contacts\n </div>\n <div className=\"max-h-48 overflow-y-auto\">\n {contacts.map((c, i) => (\n <div\n key={i}\n role=\"button\"\n onClick={() => handleDefaultSelect(c)}\n aria-label={resolvedDefaultSelectLabel ? `${resolvedDefaultSelectLabel} ${c.name}` : undefined}\n className=\"flex items-center gap-3 w-full px-3 py-2 text-left hover:bg-muted/50 transition-colors cursor-pointer\"\n >\n <div className=\"w-7 h-7 rounded-full bg-muted flex items-center justify-center text-[10px] font-medium text-muted-foreground shrink-0\">\n {c.name.split(\" \").map((n) => n[0]).join(\"\")}\n </div>\n <div className=\"flex-1 min-w-0 overflow-hidden\">\n <div className=\"truncate text-sm font-medium text-foreground\">{c.name}</div>\n <div className=\"truncate text-xs text-muted-foreground leading-tight\">\n {c.role} · {c.email ?? c.emails?.[0] ?? c.phone ?? c.phones?.[0] ?? \"\"}\n </div>\n {c.lastActivity && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n onOpenRecentActivity?.()\n setOpen(false)\n }}\n className=\"mt-1.5 flex max-w-full items-center gap-1.5 overflow-hidden rounded-md border border-border/70 bg-muted/30 px-2 py-1 text-[11px] text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors\"\n >\n <Clock className=\"w-3 h-3 shrink-0\" />\n <span className=\"shrink-0 font-medium\">Last activity</span>\n <span className=\"shrink-0 text-muted-foreground/60\">·</span>\n <span className=\"shrink-0\">{c.lastActivity.date}</span>\n <span className=\"shrink-0 text-muted-foreground/60\">·</span>\n <span className=\"truncate capitalize\">{c.lastActivity.type}</span>\n </button>\n )}\n </div>\n <div className=\"ml-2 flex items-center gap-1.5 shrink-0\">\n {resolvedDefaultSelectLabel && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n handleDefaultSelect(c)\n }}\n className=\"h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40\"\n >\n {resolvedDefaultSelectLabel}\n </button>\n )}\n {!isSwitchMode && onSelectTo && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n onSelectTo(c)\n setOpen(false)\n }}\n className=\"h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40\"\n >\n To\n </button>\n )}\n {!isSwitchMode && onSelectCc && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n onSelectCc(c)\n setOpen(false)\n }}\n className=\"h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40\"\n >\n Cc\n </button>\n )}\n {!isSwitchMode && onSelectBcc && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n onSelectBcc(c)\n setOpen(false)\n }}\n className=\"h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40\"\n >\n Bcc\n </button>\n )}\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n if (c.salesforceUrl) {\n window.open(c.salesforceUrl, \"_blank\", \"noopener,noreferrer\")\n } else {\n onViewAll?.()\n }\n }}\n className=\"h-7 w-7 inline-flex items-center justify-center rounded-md border border-border bg-background hover:bg-muted/40 transition-colors shrink-0\"\n aria-label={`Open ${c.name} in Salesforce`}\n >\n {iconMap?.salesforce ? (\n <BrandIcon src={iconMap.salesforce} alt=\"Salesforce\" className=\"w-3.5 h-3.5\" />\n ) : (\n <ExternalLink className=\"w-3.5 h-3.5 text-muted-foreground\" />\n )}\n </button>\n </div>\n </div>\n ))}\n </div>\n {onViewAll && (\n <>\n <div className=\"h-px bg-border mx-3 my-1\" />\n <button\n onClick={() => { onViewAll(); setOpen(false) }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-left text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors\"\n >\n <ExternalLink className=\"w-3 h-3\" />\n View all contacts\n </button>\n </>\n )}\n </div>\n </>\n )}\n </div>\n )\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/account-contacts-popover.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport {\n Clock,\n ExternalLink,\n} from \"lucide-react\"\nimport { cn } from \"../lib/utils\"\nimport type { SuggestedContact, SuggestedActionsIconMap } from \"./suggested-actions\"\n\n// ---------------------------------------------------------------------------\n// BrandIcon\n// ---------------------------------------------------------------------------\n\nexport function BrandIcon({ src, alt, className }: { src: string; alt: string; className?: string }) {\n return (\n <img\n src={src}\n alt={alt}\n className={`${className ?? \"\"} object-contain`}\n draggable={false}\n />\n )\n}\n\n// ---------------------------------------------------------------------------\n// AccountContactsPopover\n// ---------------------------------------------------------------------------\n\nexport interface AccountContactsPopoverProps {\n contacts: SuggestedContact[]\n onSelect?: (contact: SuggestedContact) => void\n onSelectTo?: (contact: SuggestedContact) => void\n onSelectCc?: (contact: SuggestedContact) => void\n onSelectBcc?: (contact: SuggestedContact) => void\n /** Label for the default contact row action. Defaults to \"Add\" or \"Switch\" when onSelectSwitch is provided. */\n defaultSelectLabel?: string\n /** Optional replacement-selection callback. When provided, row clicks call this instead of additive onSelect/onSelectTo. */\n onSelectSwitch?: (contact: SuggestedContact) => void\n onViewAll?: () => void\n onOpenRecentActivity?: (contact: SuggestedContact) => void\n trigger: React.ReactNode\n iconMap?: SuggestedActionsIconMap\n}\n\nexport function AccountContactsPopover({\n contacts,\n onSelect,\n onSelectTo,\n onSelectCc,\n onSelectBcc,\n defaultSelectLabel,\n onSelectSwitch,\n onViewAll,\n onOpenRecentActivity,\n trigger,\n iconMap,\n}: AccountContactsPopoverProps) {\n const [open, setOpen] = React.useState(false)\n const triggerRef = React.useRef<HTMLDivElement>(null)\n const [popoverStyle, setPopoverStyle] = React.useState<React.CSSProperties>({})\n const isSwitchMode = Boolean(onSelectSwitch)\n const resolvedDefaultSelectLabel = defaultSelectLabel ?? (isSwitchMode ? \"Switch\" : undefined)\n const handleDefaultSelect = React.useCallback((contact: SuggestedContact) => {\n if (onSelectSwitch) {\n onSelectSwitch(contact)\n setOpen(false)\n return\n }\n\n const additiveSelect = onSelectTo ?? onSelect\n if (additiveSelect) {\n additiveSelect(contact)\n setOpen(false)\n }\n }, [onSelect, onSelectSwitch, onSelectTo])\n\n React.useEffect(() => {\n if (open && triggerRef.current) {\n const rect = triggerRef.current.getBoundingClientRect()\n const popoverWidth = Math.min(448, window.innerWidth - 32)\n let left = rect.right - popoverWidth\n if (left < 16) left = 16\n if (left + popoverWidth > window.innerWidth - 16) left = window.innerWidth - 16 - popoverWidth\n const popoverHeight = 320;\n const spaceBelow = window.innerHeight - rect.bottom - 8;\n const spaceAbove = rect.top - 8;\n const placeAbove = spaceBelow < popoverHeight && spaceAbove > spaceBelow;\n const top = placeAbove ? rect.top - popoverHeight - 4 : rect.bottom + 4;\n setPopoverStyle({ position: \"fixed\", top: Math.max(8, top), left, maxHeight: placeAbove ? spaceAbove : spaceBelow })\n }\n }, [open])\n\n return (\n <div>\n <div ref={triggerRef} onClick={() => setOpen(!open)}>{trigger}</div>\n {open && (\n <>\n <div className=\"fixed inset-0 z-40\" onClick={() => setOpen(false)} />\n <div style={popoverStyle} className=\"fixed bg-background border border-border rounded-lg shadow-xl z-50 w-[28rem] max-w-[calc(100vw-2rem)] py-2 animate-in fade-in slide-in-from-top-1 duration-150 overflow-y-auto\">\n <div className=\"px-3 py-1.5 text-[11px] font-medium text-muted-foreground/60 uppercase tracking-wide\">\n Account Contacts\n </div>\n <div className=\"max-h-48 overflow-y-auto\">\n {contacts.map((c, i) => (\n <div\n key={i}\n role=\"button\"\n onClick={() => handleDefaultSelect(c)}\n aria-label={resolvedDefaultSelectLabel ? `${resolvedDefaultSelectLabel} ${c.name}` : undefined}\n className=\"flex items-center gap-3 w-full px-3 py-2 text-left hover:bg-muted/50 transition-colors cursor-pointer\"\n >\n <div className=\"w-7 h-7 rounded-full bg-muted flex items-center justify-center text-[10px] font-medium text-muted-foreground shrink-0\">\n {c.name.split(\" \").map((n) => n[0]).join(\"\")}\n </div>\n <div className=\"flex-1 min-w-0 overflow-hidden\">\n <div className=\"truncate text-sm font-medium text-foreground\">{c.name}</div>\n <div className=\"truncate text-xs text-muted-foreground leading-tight\">\n {c.role} · {c.email ?? c.emails?.[0] ?? c.phone ?? c.phones?.[0] ?? \"\"}\n </div>\n {c.lastActivity && (() => {\n const activityContent = (\n <>\n <Clock className=\"w-3 h-3 shrink-0\" />\n <span className=\"shrink-0 font-medium\">Last activity</span>\n <span className=\"shrink-0 text-muted-foreground/60\">·</span>\n <span className=\"shrink-0\">{c.lastActivity.date}</span>\n <span className=\"shrink-0 text-muted-foreground/60\">·</span>\n <span className=\"truncate capitalize\">{c.lastActivity.type}</span>\n </>\n )\n const chipBaseClass =\n \"mt-1.5 flex max-w-full items-center gap-1.5 overflow-hidden rounded-md border border-border/70 bg-muted/30 px-2 py-1 text-[11px] text-muted-foreground\"\n return onOpenRecentActivity && c.lastActivity.timelineEventId ? (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n onOpenRecentActivity(c)\n setOpen(false)\n }}\n className={cn(chipBaseClass, \"hover:text-foreground hover:bg-muted/50 transition-colors\")}\n >\n {activityContent}\n </button>\n ) : (\n <div className={chipBaseClass}>\n {activityContent}\n </div>\n )\n })()}\n </div>\n <div className=\"ml-2 flex items-center gap-1.5 shrink-0\">\n {resolvedDefaultSelectLabel && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n handleDefaultSelect(c)\n }}\n className=\"h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40\"\n >\n {resolvedDefaultSelectLabel}\n </button>\n )}\n {!isSwitchMode && onSelectTo && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n onSelectTo(c)\n setOpen(false)\n }}\n className=\"h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40\"\n >\n To\n </button>\n )}\n {!isSwitchMode && onSelectCc && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n onSelectCc(c)\n setOpen(false)\n }}\n className=\"h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40\"\n >\n Cc\n </button>\n )}\n {!isSwitchMode && onSelectBcc && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n onSelectBcc(c)\n setOpen(false)\n }}\n className=\"h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40\"\n >\n Bcc\n </button>\n )}\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n if (c.salesforceUrl) {\n window.open(c.salesforceUrl, \"_blank\", \"noopener,noreferrer\")\n } else {\n onViewAll?.()\n }\n }}\n className=\"h-7 w-7 inline-flex items-center justify-center rounded-md border border-border bg-background hover:bg-muted/40 transition-colors shrink-0\"\n aria-label={`Open ${c.name} in Salesforce`}\n >\n {iconMap?.salesforce ? (\n <BrandIcon src={iconMap.salesforce} alt=\"Salesforce\" className=\"w-3.5 h-3.5\" />\n ) : (\n <ExternalLink className=\"w-3.5 h-3.5 text-muted-foreground\" />\n )}\n </button>\n </div>\n </div>\n ))}\n </div>\n {onViewAll && (\n <>\n <div className=\"h-px bg-border mx-3 my-1\" />\n <button\n onClick={() => { onViewAll(); setOpen(false) }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-left text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors\"\n >\n <ExternalLink className=\"w-3 h-3\" />\n View all contacts\n </button>\n </>\n )}\n </div>\n </>\n )}\n </div>\n )\n}\n"],"mappings":";AAgBI,SA0GoB,UA1GpB,KAqGgB,YArGhB;AAdJ,YAAY,WAAW;AACvB;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,UAAU;AAOZ,SAAS,UAAU,EAAE,KAAK,KAAK,UAAU,GAAqD;AACnG,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA,WAAW,GAAG,gCAAa,EAAE;AAAA,MAC7B,WAAW;AAAA;AAAA,EACb;AAEJ;AAsBO,SAAS,uBAAuB;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAgC;AAC9B,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,KAAK;AAC5C,QAAM,aAAa,MAAM,OAAuB,IAAI;AACpD,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAA8B,CAAC,CAAC;AAC9E,QAAM,eAAe,QAAQ,cAAc;AAC3C,QAAM,6BAA6B,kDAAuB,eAAe,WAAW;AACpF,QAAM,sBAAsB,MAAM,YAAY,CAAC,YAA8B;AAC3E,QAAI,gBAAgB;AAClB,qBAAe,OAAO;AACtB,cAAQ,KAAK;AACb;AAAA,IACF;AAEA,UAAM,iBAAiB,kCAAc;AACrC,QAAI,gBAAgB;AAClB,qBAAe,OAAO;AACtB,cAAQ,KAAK;AAAA,IACf;AAAA,EACF,GAAG,CAAC,UAAU,gBAAgB,UAAU,CAAC;AAEzC,QAAM,UAAU,MAAM;AACpB,QAAI,QAAQ,WAAW,SAAS;AAC9B,YAAM,OAAO,WAAW,QAAQ,sBAAsB;AACtD,YAAM,eAAe,KAAK,IAAI,KAAK,OAAO,aAAa,EAAE;AACzD,UAAI,OAAO,KAAK,QAAQ;AACxB,UAAI,OAAO,GAAI,QAAO;AACtB,UAAI,OAAO,eAAe,OAAO,aAAa,GAAI,QAAO,OAAO,aAAa,KAAK;AAClF,YAAM,gBAAgB;AACtB,YAAM,aAAa,OAAO,cAAc,KAAK,SAAS;AACtD,YAAM,aAAa,KAAK,MAAM;AAC9B,YAAM,aAAa,aAAa,iBAAiB,aAAa;AAC9D,YAAM,MAAM,aAAa,KAAK,MAAM,gBAAgB,IAAI,KAAK,SAAS;AACtE,sBAAgB,EAAE,UAAU,SAAS,KAAK,KAAK,IAAI,GAAG,GAAG,GAAG,MAAM,WAAW,aAAa,aAAa,WAAW,CAAC;AAAA,IACrH;AAAA,EACF,GAAG,CAAC,IAAI,CAAC;AAET,SACE,qBAAC,SACC;AAAA,wBAAC,SAAI,KAAK,YAAY,SAAS,MAAM,QAAQ,CAAC,IAAI,GAAI,mBAAQ;AAAA,IAC7D,QACC,iCACE;AAAA,0BAAC,SAAI,WAAU,sBAAqB,SAAS,MAAM,QAAQ,KAAK,GAAG;AAAA,MACnE,qBAAC,SAAI,OAAO,cAAc,WAAU,kLAClC;AAAA,4BAAC,SAAI,WAAU,wFAAuF,8BAEtG;AAAA,QACA,oBAAC,SAAI,WAAU,4BACZ,mBAAS,IAAI,CAAC,GAAG,MAAG;AAxGnC;AAyGgB;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,SAAS,MAAM,oBAAoB,CAAC;AAAA,cACpC,cAAY,6BAA6B,GAAG,0BAA0B,IAAI,EAAE,IAAI,KAAK;AAAA,cACrF,WAAU;AAAA,cAEV;AAAA,oCAAC,SAAI,WAAU,yHACZ,YAAE,KAAK,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,GAC7C;AAAA,gBACA,qBAAC,SAAI,WAAU,kCACb;AAAA,sCAAC,SAAI,WAAU,gDAAgD,YAAE,MAAK;AAAA,kBACtE,qBAAC,SAAI,WAAU,wDACZ;AAAA,sBAAE;AAAA,oBAAK;AAAA,qBAAI,yBAAE,UAAF,aAAW,OAAE,WAAF,mBAAW,OAAtB,YAA4B,EAAE,UAA9B,aAAuC,OAAE,WAAF,mBAAW,OAAlD,YAAwD;AAAA,qBACtE;AAAA,kBACC,EAAE,iBAAiB,MAAM;AACxB,0BAAM,kBACJ,iCACE;AAAA,0CAAC,SAAM,WAAU,oBAAmB;AAAA,sBACpC,oBAAC,UAAK,WAAU,wBAAuB,2BAAa;AAAA,sBACpD,oBAAC,UAAK,WAAU,qCAAoC,kBAAC;AAAA,sBACrD,oBAAC,UAAK,WAAU,YAAY,YAAE,aAAa,MAAK;AAAA,sBAChD,oBAAC,UAAK,WAAU,qCAAoC,kBAAC;AAAA,sBACrD,oBAAC,UAAK,WAAU,uBAAuB,YAAE,aAAa,MAAK;AAAA,uBAC7D;AAEF,0BAAM,gBACJ;AACF,2BAAO,wBAAwB,EAAE,aAAa,kBAC5C;AAAA,sBAAC;AAAA;AAAA,wBACC,MAAK;AAAA,wBACL,SAAS,CAAC,MAAM;AACd,4BAAE,gBAAgB;AAClB,+CAAqB,CAAC;AACtB,kCAAQ,KAAK;AAAA,wBACf;AAAA,wBACA,WAAW,GAAG,eAAe,2DAA2D;AAAA,wBAEvF;AAAA;AAAA,oBACH,IAEA,oBAAC,SAAI,WAAW,eACb,2BACH;AAAA,kBAEJ,GAAG;AAAA,mBACL;AAAA,gBACA,qBAAC,SAAI,WAAU,2CACZ;AAAA,gDACC;AAAA,oBAAC;AAAA;AAAA,sBACC,MAAK;AAAA,sBACL,SAAS,CAAC,MAAM;AACd,0BAAE,gBAAgB;AAClB,4CAAoB,CAAC;AAAA,sBACvB;AAAA,sBACA,WAAU;AAAA,sBAET;AAAA;AAAA,kBACH;AAAA,kBAED,CAAC,gBAAgB,cAChB;AAAA,oBAAC;AAAA;AAAA,sBACC,MAAK;AAAA,sBACL,SAAS,CAAC,MAAM;AACd,0BAAE,gBAAgB;AAClB,mCAAW,CAAC;AACZ,gCAAQ,KAAK;AAAA,sBACf;AAAA,sBACA,WAAU;AAAA,sBACX;AAAA;AAAA,kBAED;AAAA,kBAED,CAAC,gBAAgB,cAChB;AAAA,oBAAC;AAAA;AAAA,sBACC,MAAK;AAAA,sBACL,SAAS,CAAC,MAAM;AACd,0BAAE,gBAAgB;AAClB,mCAAW,CAAC;AACZ,gCAAQ,KAAK;AAAA,sBACf;AAAA,sBACA,WAAU;AAAA,sBACX;AAAA;AAAA,kBAED;AAAA,kBAED,CAAC,gBAAgB,eAChB;AAAA,oBAAC;AAAA;AAAA,sBACC,MAAK;AAAA,sBACL,SAAS,CAAC,MAAM;AACd,0BAAE,gBAAgB;AAClB,oCAAY,CAAC;AACb,gCAAQ,KAAK;AAAA,sBACf;AAAA,sBACA,WAAU;AAAA,sBACX;AAAA;AAAA,kBAED;AAAA,kBAEF;AAAA,oBAAC;AAAA;AAAA,sBACC,MAAK;AAAA,sBACL,SAAS,CAAC,MAAM;AACd,0BAAE,gBAAgB;AAClB,4BAAI,EAAE,eAAe;AACnB,iCAAO,KAAK,EAAE,eAAe,UAAU,qBAAqB;AAAA,wBAC9D,OAAO;AACL;AAAA,wBACF;AAAA,sBACF;AAAA,sBACA,WAAU;AAAA,sBACV,cAAY,QAAQ,EAAE,IAAI;AAAA,sBAEzB,8CAAS,cACR,oBAAC,aAAU,KAAK,QAAQ,YAAY,KAAI,cAAa,WAAU,eAAc,IAE7E,oBAAC,gBAAa,WAAU,qCAAoC;AAAA;AAAA,kBAEhE;AAAA,mBACF;AAAA;AAAA;AAAA,YArHK;AAAA,UAsHP;AAAA,SACD,GACH;AAAA,QACC,aACC,iCACE;AAAA,8BAAC,SAAI,WAAU,4BAA2B;AAAA,UAC1C;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,MAAM;AAAE,0BAAU;AAAG,wBAAQ,KAAK;AAAA,cAAE;AAAA,cAC7C,WAAU;AAAA,cAEV;AAAA,oCAAC,gBAAa,WAAU,WAAU;AAAA,gBAAE;AAAA;AAAA;AAAA,UAEtC;AAAA,WACF;AAAA,SAEJ;AAAA,OACF;AAAA,KAEJ;AAEJ;","names":[]}
|
|
@@ -28,7 +28,15 @@ interface EmailRecipientFieldProps {
|
|
|
28
28
|
onSearch?: (query: string) => void;
|
|
29
29
|
/** Shows a "Searching contacts..." indicator while async results load. */
|
|
30
30
|
searchLoading?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Opens the recent activity for a contact in the Account Panel timeline.
|
|
33
|
+
* When provided AND the contact's `lastActivity.timelineEventId` exists, the
|
|
34
|
+
* last-activity line renders as a button that calls this callback (and stops
|
|
35
|
+
* propagation so it never adds/selects the recipient). Otherwise the line
|
|
36
|
+
* renders as non-clickable text.
|
|
37
|
+
*/
|
|
38
|
+
onOpenRecentActivity?: (contact: SuggestedContact) => void;
|
|
31
39
|
}
|
|
32
|
-
declare function EmailRecipientField({ label, recipients, onRecipientsChange, amber, contacts, showPicker, showCcBcc, ccBccOpen, onCcBccToggle, addedEmails, placeholder, contactToRecipient, onSearch, searchLoading, }: EmailRecipientFieldProps): React.JSX.Element;
|
|
40
|
+
declare function EmailRecipientField({ label, recipients, onRecipientsChange, amber, contacts, showPicker, showCcBcc, ccBccOpen, onCcBccToggle, addedEmails, placeholder, contactToRecipient, onSearch, searchLoading, onOpenRecentActivity, }: EmailRecipientFieldProps): React.JSX.Element;
|
|
33
41
|
|
|
34
42
|
export { EmailRecipientField, type EmailRecipientFieldProps, type RecipientChip };
|
|
@@ -25,6 +25,7 @@ import * as React from "react";
|
|
|
25
25
|
import { Popover as PopoverPrimitive } from "radix-ui";
|
|
26
26
|
import {
|
|
27
27
|
ChevronDown,
|
|
28
|
+
Clock,
|
|
28
29
|
CornerDownLeft,
|
|
29
30
|
Plus,
|
|
30
31
|
Search,
|
|
@@ -102,7 +103,8 @@ function ContactPickerContents({
|
|
|
102
103
|
onSelect,
|
|
103
104
|
onAddEmail,
|
|
104
105
|
onSearch,
|
|
105
|
-
searchLoading = false
|
|
106
|
+
searchLoading = false,
|
|
107
|
+
onOpenRecentActivity
|
|
106
108
|
}) {
|
|
107
109
|
const [query, setQuery] = React.useState("");
|
|
108
110
|
const asyncMode = typeof onSearch === "function";
|
|
@@ -184,7 +186,29 @@ function ContactPickerContents({
|
|
|
184
186
|
/* @__PURE__ */ jsx("span", { className: "truncate text-[13px] font-medium text-foreground", children: contact.name }),
|
|
185
187
|
/* @__PURE__ */ jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: contact.role })
|
|
186
188
|
] }),
|
|
187
|
-
email ? /* @__PURE__ */ jsx("div", { className: "truncate text-[11px] text-muted-foreground", children: email }) : null
|
|
189
|
+
email ? /* @__PURE__ */ jsx("div", { className: "truncate text-[11px] text-muted-foreground", children: email }) : null,
|
|
190
|
+
contact.lastActivity ? (() => {
|
|
191
|
+
const activityContent = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
192
|
+
/* @__PURE__ */ jsx(Clock, { className: "size-3 shrink-0" }),
|
|
193
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0", children: "Last activity" }),
|
|
194
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground/60", children: "\xB7" }),
|
|
195
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0", children: contact.lastActivity.date }),
|
|
196
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground/60", children: "\xB7" }),
|
|
197
|
+
/* @__PURE__ */ jsx("span", { className: "truncate capitalize", children: contact.lastActivity.type })
|
|
198
|
+
] });
|
|
199
|
+
return onOpenRecentActivity && contact.lastActivity.timelineEventId ? /* @__PURE__ */ jsx(
|
|
200
|
+
"button",
|
|
201
|
+
{
|
|
202
|
+
type: "button",
|
|
203
|
+
onClick: (event) => {
|
|
204
|
+
event.stopPropagation();
|
|
205
|
+
onOpenRecentActivity(contact);
|
|
206
|
+
},
|
|
207
|
+
className: "mt-1 flex max-w-full items-center gap-1 overflow-hidden text-[11px] text-muted-foreground hover:text-foreground transition-colors pointer-events-auto",
|
|
208
|
+
children: activityContent
|
|
209
|
+
}
|
|
210
|
+
) : /* @__PURE__ */ jsx("div", { className: "mt-1 flex max-w-full items-center gap-1 overflow-hidden text-[11px] text-muted-foreground", children: activityContent });
|
|
211
|
+
})() : null
|
|
188
212
|
] }),
|
|
189
213
|
alreadyAdded ? /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[10.5px] font-medium text-muted-foreground", children: "Added" }) : noEmail ? /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[10.5px] font-medium text-muted-foreground", children: "No email" }) : null
|
|
190
214
|
]
|
|
@@ -212,7 +236,8 @@ function EmailRecipientField({
|
|
|
212
236
|
placeholder,
|
|
213
237
|
contactToRecipient,
|
|
214
238
|
onSearch,
|
|
215
|
-
searchLoading
|
|
239
|
+
searchLoading,
|
|
240
|
+
onOpenRecentActivity
|
|
216
241
|
}) {
|
|
217
242
|
const [value, setValue] = React.useState("");
|
|
218
243
|
const [pickerOpen, setPickerOpen] = React.useState(false);
|
|
@@ -351,7 +376,8 @@ function EmailRecipientField({
|
|
|
351
376
|
setPickerOpen(false);
|
|
352
377
|
},
|
|
353
378
|
onSearch,
|
|
354
|
-
searchLoading
|
|
379
|
+
searchLoading,
|
|
380
|
+
onOpenRecentActivity
|
|
355
381
|
}
|
|
356
382
|
)
|
|
357
383
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/email-recipient-field.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Popover as PopoverPrimitive } from \"radix-ui\"\nimport {\n ChevronDown,\n CornerDownLeft,\n Plus,\n Search,\n Users,\n X,\n} from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport type { SuggestedContact } from \"./suggested-actions\"\n\nconst EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nfunction isValidEmail(value: string): boolean {\n return EMAIL_REGEX.test(value.trim())\n}\n\nfunction contactEmail(contact: SuggestedContact): string | undefined {\n return contact.email ?? contact.emails?.[0]\n}\n\nfunction getInitials(name: string, fallback: string): string {\n const source = name?.trim() || fallback\n return source\n .split(/[\\s@.]+/)\n .map((part) => part[0])\n .filter(Boolean)\n .slice(0, 2)\n .join(\"\")\n .toUpperCase()\n}\n\nexport interface RecipientChip {\n id: string\n email: string\n name: string\n confirmed: boolean\n}\n\nexport interface EmailRecipientFieldProps {\n label: string\n recipients: RecipientChip[]\n onRecipientsChange: (recipients: RecipientChip[]) => void\n amber?: boolean\n contacts?: SuggestedContact[]\n showPicker?: boolean\n showCcBcc?: boolean\n ccBccOpen?: boolean\n onCcBccToggle?: () => void\n addedEmails?: Set<string>\n placeholder?: string\n contactToRecipient?: (contact: SuggestedContact) => RecipientChip\n /**\n * Async search mode. When provided, the picker forwards the typed query to\n * this callback (the caller debounces + fetches) and treats `contacts` as the\n * already server-filtered result set instead of filtering it client-side.\n */\n onSearch?: (query: string) => void\n /** Shows a \"Searching contacts...\" indicator while async results load. */\n searchLoading?: boolean\n}\n\nfunction RecipientChipPill({\n recipient,\n onConfirm,\n onRemove,\n}: {\n recipient: RecipientChip\n onConfirm: () => void\n onRemove: () => void\n}) {\n const primary = recipient.name || recipient.email\n const secondary = recipient.name ? recipient.email : \"\"\n const display = primary || recipient.email\n\n if (!recipient.confirmed) {\n return (\n <span className=\"inline-flex max-w-full items-center gap-1 rounded text-xs border border-amber-300 bg-amber-50 px-2 py-0.5 text-amber-800\">\n <span className=\"min-w-0 inline-flex items-baseline gap-1\">\n <span className=\"truncate font-medium max-w-[150px]\">{primary}</span>\n {secondary ? (\n <span className=\"truncate max-w-[190px] text-amber-800/70\">\n {secondary}\n </span>\n ) : null}\n </span>\n <button\n type=\"button\"\n onClick={onConfirm}\n className=\"ml-1 rounded bg-amber-200/60 px-2 py-0.5 text-xs font-semibold text-amber-800 hover:bg-amber-200\"\n >\n Confirm\n </button>\n <button\n type=\"button\"\n aria-label={`Remove ${display}`}\n onClick={onRemove}\n className=\"inline-flex size-4 items-center justify-center rounded text-amber-700/80 hover:bg-amber-200/70\"\n >\n <X className=\"size-3\" />\n </button>\n </span>\n )\n }\n\n return (\n <span className=\"inline-flex max-w-full items-center gap-1 rounded border border-border bg-background px-2 py-0.5 text-xs text-foreground\">\n <span className=\"min-w-0 inline-flex items-baseline gap-1\">\n <span className=\"truncate font-medium max-w-[150px]\">{primary}</span>\n {secondary ? (\n <span className=\"truncate max-w-[190px] text-muted-foreground\">\n {secondary}\n </span>\n ) : null}\n </span>\n <button\n type=\"button\"\n aria-label={`Remove ${display}`}\n onClick={onRemove}\n className=\"inline-flex size-4 items-center justify-center rounded text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n >\n <X className=\"size-3\" />\n </button>\n </span>\n )\n}\n\n// Contents of the contact picker dropdown. Rendered inside a Radix\n// `Popover.Content` so its focus scope pushes onto the focus-scope stack and\n// PAUSES any parent modal's scope (e.g. the quick-action Dialog). This is what\n// makes the search input typeable: a plain `createPortal(..., document.body)`\n// element renders outside the Dialog's `DialogContent`, so the Dialog's\n// FocusScope kept yanking focus back (input un-typeable) and its modal\n// `pointer-events: none` on <body> left the portal click-dead. A stacked Radix\n// Popover layer gets `pointer-events: auto` and its own (paused-parent) focus\n// scope, fixing both. See WIT-800 / WIT-770.\nfunction ContactPickerContents({\n contacts,\n addedEmails,\n onSelect,\n onAddEmail,\n onSearch,\n searchLoading = false,\n}: {\n contacts: SuggestedContact[]\n addedEmails: Set<string>\n onSelect: (contact: SuggestedContact) => void\n onAddEmail: (email: string) => void\n onSearch?: (query: string) => void\n searchLoading?: boolean\n}) {\n const [query, setQuery] = React.useState(\"\")\n\n const asyncMode = typeof onSearch === \"function\"\n\n // Keep the latest onSearch in a ref so the search effect below fires only when\n // the query (or async mode) changes, not when the callback identity changes.\n // This lets callers pass an inline handler without triggering duplicate\n // fetches on unrelated parent re-renders.\n const onSearchRef = React.useRef(onSearch)\n React.useEffect(() => {\n onSearchRef.current = onSearch\n }, [onSearch])\n\n // Async mode: forward the query upward (caller debounces + fetches) and treat\n // `contacts` as the already server-filtered result set. Local mode: filter the\n // static `contacts` array client-side.\n React.useEffect(() => {\n if (asyncMode) {\n onSearchRef.current?.(query)\n }\n }, [asyncMode, query])\n\n const normalizedQuery = query.trim().toLowerCase()\n const filtered = asyncMode\n ? contacts\n : normalizedQuery\n ? contacts.filter((contact) => {\n const email = contactEmail(contact) ?? \"\"\n return (\n contact.name.toLowerCase().includes(normalizedQuery) ||\n contact.role.toLowerCase().includes(normalizedQuery) ||\n email.toLowerCase().includes(normalizedQuery)\n )\n })\n : contacts\n\n const queryIsEmail = isValidEmail(query)\n\n function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {\n if (event.key === \"Enter\" && queryIsEmail) {\n event.preventDefault()\n onAddEmail(query.trim())\n setQuery(\"\")\n }\n }\n\n return (\n <>\n <div className=\"flex items-center gap-2 px-3 py-2.5 border-b border-border/50\">\n <Search className=\"size-4 text-muted-foreground shrink-0\" />\n <input\n autoFocus\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onKeyDown={handleKeyDown}\n className=\"flex-1 text-[13px] bg-transparent outline-none\"\n placeholder=\"Search contacts...\"\n />\n </div>\n\n <div role=\"listbox\" className=\"max-h-[208px] overflow-y-auto p-1\">\n {searchLoading ? (\n <div className=\"px-3 py-5 text-center text-[13px] text-muted-foreground\">\n Searching contacts...\n </div>\n ) : asyncMode && normalizedQuery.length === 0 ? (\n <div className=\"px-3 py-5 text-center text-[13px] text-muted-foreground\">\n Type a name or email to search contacts.\n </div>\n ) : !asyncMode && contacts.length === 0 ? (\n <div className=\"px-3 py-5 text-center text-[13px] text-muted-foreground\">\n <div className=\"font-medium text-foreground/80\">\n No contacts for this account\n </div>\n <div className=\"mt-1\">\n Type an email address above and press Enter to add a recipient.\n </div>\n </div>\n ) : filtered.length === 0 ? (\n <div className=\"px-3 py-4 text-center text-[13px] text-muted-foreground\">\n <div>No contact matches ‘{query}’.</div>\n {queryIsEmail ? (\n <div className=\"mt-1\">Press Enter to add {query}.</div>\n ) : null}\n </div>\n ) : (\n filtered.map((contact, index) => {\n const email = contactEmail(contact)\n const noEmail = !email || !isValidEmail(email)\n const alreadyAdded = email\n ? addedEmails.has(email.toLowerCase())\n : false\n const disabled = noEmail || alreadyAdded\n\n return (\n <div\n key={`${contact.name}-${email ?? index}`}\n role=\"option\"\n aria-selected={false}\n aria-disabled={disabled}\n onClick={() => {\n if (!disabled) onSelect(contact)\n }}\n className={cn(\n \"flex items-center gap-2.5 px-2 py-1.5 rounded-md cursor-pointer hover:bg-muted/60\",\n disabled && \"opacity-45 pointer-events-none\",\n )}\n >\n <div className=\"flex size-7 shrink-0 items-center justify-center rounded-[7px] bg-muted text-[11px] font-medium text-muted-foreground\">\n {getInitials(contact.name, email ?? \"?\")}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"truncate text-[13px] font-medium text-foreground\">\n {contact.name}\n </span>\n <span className=\"truncate text-[11px] text-muted-foreground\">\n {contact.role}\n </span>\n </div>\n {email ? (\n <div className=\"truncate text-[11px] text-muted-foreground\">\n {email}\n </div>\n ) : null}\n </div>\n {alreadyAdded ? (\n <span className=\"shrink-0 text-[10.5px] font-medium text-muted-foreground\">\n Added\n </span>\n ) : noEmail ? (\n <span className=\"shrink-0 text-[10.5px] font-medium text-muted-foreground\">\n No email\n </span>\n ) : null}\n </div>\n )\n })\n )}\n </div>\n\n <div className=\"flex items-center gap-1.5 px-3 py-2 border-t border-border/50 text-[11px] text-muted-foreground\">\n <CornerDownLeft className=\"size-3 shrink-0\" />\n <span>Type an address and press Enter to add someone not listed.</span>\n </div>\n </>\n )\n}\n\nexport function EmailRecipientField({\n label,\n recipients,\n onRecipientsChange,\n amber = false,\n contacts = [],\n showPicker = false,\n showCcBcc = false,\n ccBccOpen = false,\n onCcBccToggle,\n addedEmails,\n placeholder,\n contactToRecipient,\n onSearch,\n searchLoading,\n}: EmailRecipientFieldProps) {\n const [value, setValue] = React.useState(\"\")\n const [pickerOpen, setPickerOpen] = React.useState(false)\n\n const hasUnconfirmed = recipients.some((r) => !r.confirmed)\n const state: \"default\" | \"amber\" =\n amber && hasUnconfirmed ? \"amber\" : \"default\"\n const amberRow = state === \"amber\"\n\n const added = addedEmails ?? new Set<string>()\n\n const resolvedPlaceholder =\n placeholder ?? (recipients.length > 0 ? \"Add another...\" : \"Add email...\")\n\n const committedByKeyRef = React.useRef<string | null>(null)\n\n function addEmail(email: string): boolean {\n const trimmed = email.trim()\n if (!isValidEmail(trimmed)) return false\n if (added.has(trimmed.toLowerCase())) return false\n onRecipientsChange([\n ...recipients,\n { id: trimmed, email: trimmed, name: \"\", confirmed: false },\n ])\n setValue(\"\")\n return true\n }\n\n function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {\n if ((event.key === \"Enter\" || event.key === \",\") && value.trim()) {\n event.preventDefault()\n addEmail(value)\n return\n }\n if (event.key === \"Tab\" && value.trim()) {\n const trimmed = value.trim()\n if (addEmail(trimmed)) {\n committedByKeyRef.current = trimmed.toLowerCase()\n }\n return\n }\n if (event.key === \"Backspace\" && value === \"\" && recipients.length > 0) {\n event.preventDefault()\n onRecipientsChange(recipients.slice(0, -1))\n }\n }\n\n function confirmRecipient(id: string) {\n onRecipientsChange(\n recipients.map((r) => (r.id === id ? { ...r, confirmed: true } : r)),\n )\n }\n\n function removeRecipient(id: string) {\n onRecipientsChange(recipients.filter((r) => r.id !== id))\n }\n\n function selectContact(contact: SuggestedContact) {\n const recipient =\n contactToRecipient?.(contact) ??\n ({\n id: contactEmail(contact) ?? contact.name,\n email: contactEmail(contact) ?? \"\",\n name: contact.name,\n confirmed: true,\n } satisfies RecipientChip)\n onRecipientsChange([...recipients, recipient])\n setPickerOpen(false)\n }\n\n return (\n <div\n className={cn(\n \"grid grid-cols-[60px_1fr] gap-2 px-[18px] py-[9px] border-b border-border/70 items-start text-sm\",\n amberRow && \"bg-amber-50/35 border-amber-200/80\",\n )}\n >\n <div\n className={cn(\n \"text-[11px] font-semibold uppercase tracking-wide text-muted-foreground pt-[7px]\",\n amberRow && \"text-amber-700\",\n )}\n >\n {label}\n </div>\n\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap gap-1.5 items-center\">\n {recipients.map((recipient) => (\n <RecipientChipPill\n key={recipient.id}\n recipient={recipient}\n onConfirm={() => confirmRecipient(recipient.id)}\n onRemove={() => removeRecipient(recipient.id)}\n />\n ))}\n <input\n value={value}\n onChange={(event) => setValue(event.target.value)}\n onKeyDown={handleKeyDown}\n onBlur={() => {\n const trimmed = value.trim().toLowerCase()\n if (trimmed && committedByKeyRef.current === trimmed) {\n committedByKeyRef.current = null\n return\n }\n // Commit any valid pending email so it is not silently dropped\n // when the user clicks Send without pressing Enter/comma first.\n addEmail(value)\n }}\n placeholder={resolvedPlaceholder}\n className=\"min-w-[130px] flex-1 h-6 text-[13px] bg-transparent border-0 outline-none placeholder:text-muted-foreground\"\n />\n </div>\n\n {showPicker || showCcBcc ? (\n <div className=\"flex gap-1.5 mt-2\">\n {showPicker ? (\n <PopoverPrimitive.Root open={pickerOpen} onOpenChange={setPickerOpen}>\n <PopoverPrimitive.Trigger asChild>\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1 h-6 px-2.5 rounded-md border border-border bg-background text-[11px] font-medium text-muted-foreground hover:bg-muted/60 hover:text-foreground transition-colors duration-[120ms]\"\n >\n <Users className=\"size-3\" />\n Contacts\n <ChevronDown className=\"size-3\" />\n </button>\n </PopoverPrimitive.Trigger>\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n side=\"bottom\"\n align=\"start\"\n sideOffset={4}\n collisionPadding={16}\n className=\"z-50 w-[min(448px,calc(100vw-2rem))] rounded-lg border bg-background shadow-xl p-0\"\n >\n <ContactPickerContents\n contacts={contacts}\n addedEmails={added}\n onSelect={selectContact}\n onAddEmail={(email) => {\n addEmail(email)\n setPickerOpen(false)\n }}\n onSearch={onSearch}\n searchLoading={searchLoading}\n />\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n ) : null}\n {showCcBcc ? (\n <button\n type=\"button\"\n onClick={onCcBccToggle}\n className=\"inline-flex items-center gap-1 h-6 px-2.5 rounded-md border border-border bg-background text-[11px] font-medium text-muted-foreground hover:bg-muted/60 hover:text-foreground transition-colors duration-[120ms]\"\n >\n <Plus className=\"size-3\" />\n {ccBccOpen ? \"Hide Cc/Bcc\" : \"Add Cc/Bcc\"}\n </button>\n ) : null}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/email-recipient-field.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Popover as PopoverPrimitive } from \"radix-ui\"\nimport {\n ChevronDown,\n Clock,\n CornerDownLeft,\n Plus,\n Search,\n Users,\n X,\n} from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport type { SuggestedContact } from \"./suggested-actions\"\n\nconst EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nfunction isValidEmail(value: string): boolean {\n return EMAIL_REGEX.test(value.trim())\n}\n\nfunction contactEmail(contact: SuggestedContact): string | undefined {\n return contact.email ?? contact.emails?.[0]\n}\n\nfunction getInitials(name: string, fallback: string): string {\n const source = name?.trim() || fallback\n return source\n .split(/[\\s@.]+/)\n .map((part) => part[0])\n .filter(Boolean)\n .slice(0, 2)\n .join(\"\")\n .toUpperCase()\n}\n\nexport interface RecipientChip {\n id: string\n email: string\n name: string\n confirmed: boolean\n}\n\nexport interface EmailRecipientFieldProps {\n label: string\n recipients: RecipientChip[]\n onRecipientsChange: (recipients: RecipientChip[]) => void\n amber?: boolean\n contacts?: SuggestedContact[]\n showPicker?: boolean\n showCcBcc?: boolean\n ccBccOpen?: boolean\n onCcBccToggle?: () => void\n addedEmails?: Set<string>\n placeholder?: string\n contactToRecipient?: (contact: SuggestedContact) => RecipientChip\n /**\n * Async search mode. When provided, the picker forwards the typed query to\n * this callback (the caller debounces + fetches) and treats `contacts` as the\n * already server-filtered result set instead of filtering it client-side.\n */\n onSearch?: (query: string) => void\n /** Shows a \"Searching contacts...\" indicator while async results load. */\n searchLoading?: boolean\n /**\n * Opens the recent activity for a contact in the Account Panel timeline.\n * When provided AND the contact's `lastActivity.timelineEventId` exists, the\n * last-activity line renders as a button that calls this callback (and stops\n * propagation so it never adds/selects the recipient). Otherwise the line\n * renders as non-clickable text.\n */\n onOpenRecentActivity?: (contact: SuggestedContact) => void\n}\n\nfunction RecipientChipPill({\n recipient,\n onConfirm,\n onRemove,\n}: {\n recipient: RecipientChip\n onConfirm: () => void\n onRemove: () => void\n}) {\n const primary = recipient.name || recipient.email\n const secondary = recipient.name ? recipient.email : \"\"\n const display = primary || recipient.email\n\n if (!recipient.confirmed) {\n return (\n <span className=\"inline-flex max-w-full items-center gap-1 rounded text-xs border border-amber-300 bg-amber-50 px-2 py-0.5 text-amber-800\">\n <span className=\"min-w-0 inline-flex items-baseline gap-1\">\n <span className=\"truncate font-medium max-w-[150px]\">{primary}</span>\n {secondary ? (\n <span className=\"truncate max-w-[190px] text-amber-800/70\">\n {secondary}\n </span>\n ) : null}\n </span>\n <button\n type=\"button\"\n onClick={onConfirm}\n className=\"ml-1 rounded bg-amber-200/60 px-2 py-0.5 text-xs font-semibold text-amber-800 hover:bg-amber-200\"\n >\n Confirm\n </button>\n <button\n type=\"button\"\n aria-label={`Remove ${display}`}\n onClick={onRemove}\n className=\"inline-flex size-4 items-center justify-center rounded text-amber-700/80 hover:bg-amber-200/70\"\n >\n <X className=\"size-3\" />\n </button>\n </span>\n )\n }\n\n return (\n <span className=\"inline-flex max-w-full items-center gap-1 rounded border border-border bg-background px-2 py-0.5 text-xs text-foreground\">\n <span className=\"min-w-0 inline-flex items-baseline gap-1\">\n <span className=\"truncate font-medium max-w-[150px]\">{primary}</span>\n {secondary ? (\n <span className=\"truncate max-w-[190px] text-muted-foreground\">\n {secondary}\n </span>\n ) : null}\n </span>\n <button\n type=\"button\"\n aria-label={`Remove ${display}`}\n onClick={onRemove}\n className=\"inline-flex size-4 items-center justify-center rounded text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n >\n <X className=\"size-3\" />\n </button>\n </span>\n )\n}\n\n// Contents of the contact picker dropdown. Rendered inside a Radix\n// `Popover.Content` so its focus scope pushes onto the focus-scope stack and\n// PAUSES any parent modal's scope (e.g. the quick-action Dialog). This is what\n// makes the search input typeable: a plain `createPortal(..., document.body)`\n// element renders outside the Dialog's `DialogContent`, so the Dialog's\n// FocusScope kept yanking focus back (input un-typeable) and its modal\n// `pointer-events: none` on <body> left the portal click-dead. A stacked Radix\n// Popover layer gets `pointer-events: auto` and its own (paused-parent) focus\n// scope, fixing both. See WIT-800 / WIT-770.\nfunction ContactPickerContents({\n contacts,\n addedEmails,\n onSelect,\n onAddEmail,\n onSearch,\n searchLoading = false,\n onOpenRecentActivity,\n}: {\n contacts: SuggestedContact[]\n addedEmails: Set<string>\n onSelect: (contact: SuggestedContact) => void\n onAddEmail: (email: string) => void\n onSearch?: (query: string) => void\n searchLoading?: boolean\n onOpenRecentActivity?: (contact: SuggestedContact) => void\n}) {\n const [query, setQuery] = React.useState(\"\")\n\n const asyncMode = typeof onSearch === \"function\"\n\n // Keep the latest onSearch in a ref so the search effect below fires only when\n // the query (or async mode) changes, not when the callback identity changes.\n // This lets callers pass an inline handler without triggering duplicate\n // fetches on unrelated parent re-renders.\n const onSearchRef = React.useRef(onSearch)\n React.useEffect(() => {\n onSearchRef.current = onSearch\n }, [onSearch])\n\n // Async mode: forward the query upward (caller debounces + fetches) and treat\n // `contacts` as the already server-filtered result set. Local mode: filter the\n // static `contacts` array client-side.\n React.useEffect(() => {\n if (asyncMode) {\n onSearchRef.current?.(query)\n }\n }, [asyncMode, query])\n\n const normalizedQuery = query.trim().toLowerCase()\n const filtered = asyncMode\n ? contacts\n : normalizedQuery\n ? contacts.filter((contact) => {\n const email = contactEmail(contact) ?? \"\"\n return (\n contact.name.toLowerCase().includes(normalizedQuery) ||\n contact.role.toLowerCase().includes(normalizedQuery) ||\n email.toLowerCase().includes(normalizedQuery)\n )\n })\n : contacts\n\n const queryIsEmail = isValidEmail(query)\n\n function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {\n if (event.key === \"Enter\" && queryIsEmail) {\n event.preventDefault()\n onAddEmail(query.trim())\n setQuery(\"\")\n }\n }\n\n return (\n <>\n <div className=\"flex items-center gap-2 px-3 py-2.5 border-b border-border/50\">\n <Search className=\"size-4 text-muted-foreground shrink-0\" />\n <input\n autoFocus\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onKeyDown={handleKeyDown}\n className=\"flex-1 text-[13px] bg-transparent outline-none\"\n placeholder=\"Search contacts...\"\n />\n </div>\n\n <div role=\"listbox\" className=\"max-h-[208px] overflow-y-auto p-1\">\n {searchLoading ? (\n <div className=\"px-3 py-5 text-center text-[13px] text-muted-foreground\">\n Searching contacts...\n </div>\n ) : asyncMode && normalizedQuery.length === 0 ? (\n <div className=\"px-3 py-5 text-center text-[13px] text-muted-foreground\">\n Type a name or email to search contacts.\n </div>\n ) : !asyncMode && contacts.length === 0 ? (\n <div className=\"px-3 py-5 text-center text-[13px] text-muted-foreground\">\n <div className=\"font-medium text-foreground/80\">\n No contacts for this account\n </div>\n <div className=\"mt-1\">\n Type an email address above and press Enter to add a recipient.\n </div>\n </div>\n ) : filtered.length === 0 ? (\n <div className=\"px-3 py-4 text-center text-[13px] text-muted-foreground\">\n <div>No contact matches ‘{query}’.</div>\n {queryIsEmail ? (\n <div className=\"mt-1\">Press Enter to add {query}.</div>\n ) : null}\n </div>\n ) : (\n filtered.map((contact, index) => {\n const email = contactEmail(contact)\n const noEmail = !email || !isValidEmail(email)\n const alreadyAdded = email\n ? addedEmails.has(email.toLowerCase())\n : false\n const disabled = noEmail || alreadyAdded\n\n return (\n <div\n key={`${contact.name}-${email ?? index}`}\n role=\"option\"\n aria-selected={false}\n aria-disabled={disabled}\n onClick={() => {\n if (!disabled) onSelect(contact)\n }}\n className={cn(\n \"flex items-center gap-2.5 px-2 py-1.5 rounded-md cursor-pointer hover:bg-muted/60\",\n disabled && \"opacity-45 pointer-events-none\",\n )}\n >\n <div className=\"flex size-7 shrink-0 items-center justify-center rounded-[7px] bg-muted text-[11px] font-medium text-muted-foreground\">\n {getInitials(contact.name, email ?? \"?\")}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"truncate text-[13px] font-medium text-foreground\">\n {contact.name}\n </span>\n <span className=\"truncate text-[11px] text-muted-foreground\">\n {contact.role}\n </span>\n </div>\n {email ? (\n <div className=\"truncate text-[11px] text-muted-foreground\">\n {email}\n </div>\n ) : null}\n {contact.lastActivity ? (() => {\n const activityContent = (\n <>\n <Clock className=\"size-3 shrink-0\" />\n <span className=\"shrink-0\">Last activity</span>\n <span className=\"shrink-0 text-muted-foreground/60\">·</span>\n <span className=\"shrink-0\">{contact.lastActivity.date}</span>\n <span className=\"shrink-0 text-muted-foreground/60\">·</span>\n <span className=\"truncate capitalize\">{contact.lastActivity.type}</span>\n </>\n )\n return onOpenRecentActivity && contact.lastActivity.timelineEventId ? (\n <button\n type=\"button\"\n onClick={(event) => {\n event.stopPropagation()\n onOpenRecentActivity(contact)\n }}\n // `pointer-events-auto` keeps the activity button clickable even\n // when the parent row is disabled (no email / already added) and\n // gets `pointer-events-none` — the timeline activity is still\n // openable for those contacts.\n className=\"mt-1 flex max-w-full items-center gap-1 overflow-hidden text-[11px] text-muted-foreground hover:text-foreground transition-colors pointer-events-auto\"\n >\n {activityContent}\n </button>\n ) : (\n <div className=\"mt-1 flex max-w-full items-center gap-1 overflow-hidden text-[11px] text-muted-foreground\">\n {activityContent}\n </div>\n )\n })() : null}\n </div>\n {alreadyAdded ? (\n <span className=\"shrink-0 text-[10.5px] font-medium text-muted-foreground\">\n Added\n </span>\n ) : noEmail ? (\n <span className=\"shrink-0 text-[10.5px] font-medium text-muted-foreground\">\n No email\n </span>\n ) : null}\n </div>\n )\n })\n )}\n </div>\n\n <div className=\"flex items-center gap-1.5 px-3 py-2 border-t border-border/50 text-[11px] text-muted-foreground\">\n <CornerDownLeft className=\"size-3 shrink-0\" />\n <span>Type an address and press Enter to add someone not listed.</span>\n </div>\n </>\n )\n}\n\nexport function EmailRecipientField({\n label,\n recipients,\n onRecipientsChange,\n amber = false,\n contacts = [],\n showPicker = false,\n showCcBcc = false,\n ccBccOpen = false,\n onCcBccToggle,\n addedEmails,\n placeholder,\n contactToRecipient,\n onSearch,\n searchLoading,\n onOpenRecentActivity,\n}: EmailRecipientFieldProps) {\n const [value, setValue] = React.useState(\"\")\n const [pickerOpen, setPickerOpen] = React.useState(false)\n\n const hasUnconfirmed = recipients.some((r) => !r.confirmed)\n const state: \"default\" | \"amber\" =\n amber && hasUnconfirmed ? \"amber\" : \"default\"\n const amberRow = state === \"amber\"\n\n const added = addedEmails ?? new Set<string>()\n\n const resolvedPlaceholder =\n placeholder ?? (recipients.length > 0 ? \"Add another...\" : \"Add email...\")\n\n const committedByKeyRef = React.useRef<string | null>(null)\n\n function addEmail(email: string): boolean {\n const trimmed = email.trim()\n if (!isValidEmail(trimmed)) return false\n if (added.has(trimmed.toLowerCase())) return false\n onRecipientsChange([\n ...recipients,\n { id: trimmed, email: trimmed, name: \"\", confirmed: false },\n ])\n setValue(\"\")\n return true\n }\n\n function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {\n if ((event.key === \"Enter\" || event.key === \",\") && value.trim()) {\n event.preventDefault()\n addEmail(value)\n return\n }\n if (event.key === \"Tab\" && value.trim()) {\n const trimmed = value.trim()\n if (addEmail(trimmed)) {\n committedByKeyRef.current = trimmed.toLowerCase()\n }\n return\n }\n if (event.key === \"Backspace\" && value === \"\" && recipients.length > 0) {\n event.preventDefault()\n onRecipientsChange(recipients.slice(0, -1))\n }\n }\n\n function confirmRecipient(id: string) {\n onRecipientsChange(\n recipients.map((r) => (r.id === id ? { ...r, confirmed: true } : r)),\n )\n }\n\n function removeRecipient(id: string) {\n onRecipientsChange(recipients.filter((r) => r.id !== id))\n }\n\n function selectContact(contact: SuggestedContact) {\n const recipient =\n contactToRecipient?.(contact) ??\n ({\n id: contactEmail(contact) ?? contact.name,\n email: contactEmail(contact) ?? \"\",\n name: contact.name,\n confirmed: true,\n } satisfies RecipientChip)\n onRecipientsChange([...recipients, recipient])\n setPickerOpen(false)\n }\n\n return (\n <div\n className={cn(\n \"grid grid-cols-[60px_1fr] gap-2 px-[18px] py-[9px] border-b border-border/70 items-start text-sm\",\n amberRow && \"bg-amber-50/35 border-amber-200/80\",\n )}\n >\n <div\n className={cn(\n \"text-[11px] font-semibold uppercase tracking-wide text-muted-foreground pt-[7px]\",\n amberRow && \"text-amber-700\",\n )}\n >\n {label}\n </div>\n\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap gap-1.5 items-center\">\n {recipients.map((recipient) => (\n <RecipientChipPill\n key={recipient.id}\n recipient={recipient}\n onConfirm={() => confirmRecipient(recipient.id)}\n onRemove={() => removeRecipient(recipient.id)}\n />\n ))}\n <input\n value={value}\n onChange={(event) => setValue(event.target.value)}\n onKeyDown={handleKeyDown}\n onBlur={() => {\n const trimmed = value.trim().toLowerCase()\n if (trimmed && committedByKeyRef.current === trimmed) {\n committedByKeyRef.current = null\n return\n }\n // Commit any valid pending email so it is not silently dropped\n // when the user clicks Send without pressing Enter/comma first.\n addEmail(value)\n }}\n placeholder={resolvedPlaceholder}\n className=\"min-w-[130px] flex-1 h-6 text-[13px] bg-transparent border-0 outline-none placeholder:text-muted-foreground\"\n />\n </div>\n\n {showPicker || showCcBcc ? (\n <div className=\"flex gap-1.5 mt-2\">\n {showPicker ? (\n <PopoverPrimitive.Root open={pickerOpen} onOpenChange={setPickerOpen}>\n <PopoverPrimitive.Trigger asChild>\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1 h-6 px-2.5 rounded-md border border-border bg-background text-[11px] font-medium text-muted-foreground hover:bg-muted/60 hover:text-foreground transition-colors duration-[120ms]\"\n >\n <Users className=\"size-3\" />\n Contacts\n <ChevronDown className=\"size-3\" />\n </button>\n </PopoverPrimitive.Trigger>\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n side=\"bottom\"\n align=\"start\"\n sideOffset={4}\n collisionPadding={16}\n className=\"z-50 w-[min(448px,calc(100vw-2rem))] rounded-lg border bg-background shadow-xl p-0\"\n >\n <ContactPickerContents\n contacts={contacts}\n addedEmails={added}\n onSelect={selectContact}\n onAddEmail={(email) => {\n addEmail(email)\n setPickerOpen(false)\n }}\n onSearch={onSearch}\n searchLoading={searchLoading}\n onOpenRecentActivity={onOpenRecentActivity}\n />\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n ) : null}\n {showCcBcc ? (\n <button\n type=\"button\"\n onClick={onCcBccToggle}\n className=\"inline-flex items-center gap-1 h-6 px-2.5 rounded-md border border-border bg-background text-[11px] font-medium text-muted-foreground hover:bg-muted/60 hover:text-foreground transition-colors duration-[120ms]\"\n >\n <Plus className=\"size-3\" />\n {ccBccOpen ? \"Hide Cc/Bcc\" : \"Add Cc/Bcc\"}\n </button>\n ) : null}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA4FQ,SA0Mc,UAzMZ,KADF;AA1FR,YAAY,WAAW;AACvB,SAAS,WAAW,wBAAwB;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,UAAU;AAGnB,MAAM,cAAc;AAEpB,SAAS,aAAa,OAAwB;AAC5C,SAAO,YAAY,KAAK,MAAM,KAAK,CAAC;AACtC;AAEA,SAAS,aAAa,SAA+C;AAvBrE;AAwBE,UAAO,aAAQ,UAAR,aAAiB,aAAQ,WAAR,mBAAiB;AAC3C;AAEA,SAAS,YAAY,MAAc,UAA0B;AAC3D,QAAM,UAAS,6BAAM,WAAU;AAC/B,SAAO,OACJ,MAAM,SAAS,EACf,IAAI,CAAC,SAAS,KAAK,CAAC,CAAC,EACrB,OAAO,OAAO,EACd,MAAM,GAAG,CAAC,EACV,KAAK,EAAE,EACP,YAAY;AACjB;AAwCA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,UAAU,UAAU,QAAQ,UAAU;AAC5C,QAAM,YAAY,UAAU,OAAO,UAAU,QAAQ;AACrD,QAAM,UAAU,WAAW,UAAU;AAErC,MAAI,CAAC,UAAU,WAAW;AACxB,WACE,qBAAC,UAAK,WAAU,4HACd;AAAA,2BAAC,UAAK,WAAU,4CACd;AAAA,4BAAC,UAAK,WAAU,sCAAsC,mBAAQ;AAAA,QAC7D,YACC,oBAAC,UAAK,WAAU,4CACb,qBACH,IACE;AAAA,SACN;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS;AAAA,UACT,WAAU;AAAA,UACX;AAAA;AAAA,MAED;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,cAAY,UAAU,OAAO;AAAA,UAC7B,SAAS;AAAA,UACT,WAAU;AAAA,UAEV,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,MACxB;AAAA,OACF;AAAA,EAEJ;AAEA,SACE,qBAAC,UAAK,WAAU,4HACd;AAAA,yBAAC,UAAK,WAAU,4CACd;AAAA,0BAAC,UAAK,WAAU,sCAAsC,mBAAQ;AAAA,MAC7D,YACC,oBAAC,UAAK,WAAU,gDACb,qBACH,IACE;AAAA,OACN;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,cAAY,UAAU,OAAO;AAAA,QAC7B,SAAS;AAAA,QACT,WAAU;AAAA,QAEV,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,IACxB;AAAA,KACF;AAEJ;AAWA,SAAS,sBAAsB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AACF,GAQG;AACD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,EAAE;AAE3C,QAAM,YAAY,OAAO,aAAa;AAMtC,QAAM,cAAc,MAAM,OAAO,QAAQ;AACzC,QAAM,UAAU,MAAM;AACpB,gBAAY,UAAU;AAAA,EACxB,GAAG,CAAC,QAAQ,CAAC;AAKb,QAAM,UAAU,MAAM;AAvLxB;AAwLI,QAAI,WAAW;AACb,wBAAY,YAAZ,qCAAsB;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,WAAW,KAAK,CAAC;AAErB,QAAM,kBAAkB,MAAM,KAAK,EAAE,YAAY;AACjD,QAAM,WAAW,YACb,WACA,kBACE,SAAS,OAAO,CAAC,YAAY;AAjMrC;AAkMU,UAAM,SAAQ,kBAAa,OAAO,MAApB,YAAyB;AACvC,WACE,QAAQ,KAAK,YAAY,EAAE,SAAS,eAAe,KACnD,QAAQ,KAAK,YAAY,EAAE,SAAS,eAAe,KACnD,MAAM,YAAY,EAAE,SAAS,eAAe;AAAA,EAEhD,CAAC,IACD;AAEN,QAAM,eAAe,aAAa,KAAK;AAEvC,WAAS,cAAc,OAA8C;AACnE,QAAI,MAAM,QAAQ,WAAW,cAAc;AACzC,YAAM,eAAe;AACrB,iBAAW,MAAM,KAAK,CAAC;AACvB,eAAS,EAAE;AAAA,IACb;AAAA,EACF;AAEA,SACE,iCACE;AAAA,yBAAC,SAAI,WAAU,iEACb;AAAA,0BAAC,UAAO,WAAU,yCAAwC;AAAA,MAC1D;AAAA,QAAC;AAAA;AAAA,UACC,WAAS;AAAA,UACT,OAAO;AAAA,UACP,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK;AAAA,UAChD,WAAW;AAAA,UACX,WAAU;AAAA,UACV,aAAY;AAAA;AAAA,MACd;AAAA,OACF;AAAA,IAEA,oBAAC,SAAI,MAAK,WAAU,WAAU,qCAC3B,0BACC,oBAAC,SAAI,WAAU,2DAA0D,mCAEzE,IACE,aAAa,gBAAgB,WAAW,IAC1C,oBAAC,SAAI,WAAU,2DAA0D,sDAEzE,IACE,CAAC,aAAa,SAAS,WAAW,IACpC,qBAAC,SAAI,WAAU,2DACb;AAAA,0BAAC,SAAI,WAAU,kCAAiC,0CAEhD;AAAA,MACA,oBAAC,SAAI,WAAU,QAAO,6EAEtB;AAAA,OACF,IACE,SAAS,WAAW,IACtB,qBAAC,SAAI,WAAU,2DACb;AAAA,2BAAC,SAAI;AAAA;AAAA,QAA2B;AAAA,QAAM;AAAA,SAAQ;AAAA,MAC7C,eACC,qBAAC,SAAI,WAAU,QAAO;AAAA;AAAA,QAAoB;AAAA,QAAM;AAAA,SAAC,IAC/C;AAAA,OACN,IAEA,SAAS,IAAI,CAAC,SAAS,UAAU;AAC/B,YAAM,QAAQ,aAAa,OAAO;AAClC,YAAM,UAAU,CAAC,SAAS,CAAC,aAAa,KAAK;AAC7C,YAAM,eAAe,QACjB,YAAY,IAAI,MAAM,YAAY,CAAC,IACnC;AACJ,YAAM,WAAW,WAAW;AAE5B,aACE;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,iBAAe;AAAA,UACf,iBAAe;AAAA,UACf,SAAS,MAAM;AACb,gBAAI,CAAC,SAAU,UAAS,OAAO;AAAA,UACjC;AAAA,UACA,WAAW;AAAA,YACT;AAAA,YACA,YAAY;AAAA,UACd;AAAA,UAEA;AAAA,gCAAC,SAAI,WAAU,yHACZ,sBAAY,QAAQ,MAAM,wBAAS,GAAG,GACzC;AAAA,YACA,qBAAC,SAAI,WAAU,kBACb;AAAA,mCAAC,SAAI,WAAU,6BACb;AAAA,oCAAC,UAAK,WAAU,oDACb,kBAAQ,MACX;AAAA,gBACA,oBAAC,UAAK,WAAU,8CACb,kBAAQ,MACX;AAAA,iBACF;AAAA,cACC,QACC,oBAAC,SAAI,WAAU,8CACZ,iBACH,IACE;AAAA,cACH,QAAQ,gBAAgB,MAAM;AAC7B,sBAAM,kBACJ,iCACE;AAAA,sCAAC,SAAM,WAAU,mBAAkB;AAAA,kBACnC,oBAAC,UAAK,WAAU,YAAW,2BAAa;AAAA,kBACxC,oBAAC,UAAK,WAAU,qCAAoC,kBAAC;AAAA,kBACrD,oBAAC,UAAK,WAAU,YAAY,kBAAQ,aAAa,MAAK;AAAA,kBACtD,oBAAC,UAAK,WAAU,qCAAoC,kBAAC;AAAA,kBACrD,oBAAC,UAAK,WAAU,uBAAuB,kBAAQ,aAAa,MAAK;AAAA,mBACnE;AAEF,uBAAO,wBAAwB,QAAQ,aAAa,kBAClD;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,SAAS,CAAC,UAAU;AAClB,4BAAM,gBAAgB;AACtB,2CAAqB,OAAO;AAAA,oBAC9B;AAAA,oBAKA,WAAU;AAAA,oBAET;AAAA;AAAA,gBACH,IAEA,oBAAC,SAAI,WAAU,6FACZ,2BACH;AAAA,cAEJ,GAAG,IAAI;AAAA,eACT;AAAA,YACC,eACC,oBAAC,UAAK,WAAU,4DAA2D,mBAE3E,IACE,UACF,oBAAC,UAAK,WAAU,4DAA2D,sBAE3E,IACE;AAAA;AAAA;AAAA,QAtEC,GAAG,QAAQ,IAAI,IAAI,wBAAS,KAAK;AAAA,MAuExC;AAAA,IAEJ,CAAC,GAEL;AAAA,IAEA,qBAAC,SAAI,WAAU,mGACb;AAAA,0BAAC,kBAAe,WAAU,mBAAkB;AAAA,MAC5C,oBAAC,UAAK,wEAA0D;AAAA,OAClE;AAAA,KACF;AAEJ;AAEO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,WAAW,CAAC;AAAA,EACZ,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6B;AAC3B,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,EAAE;AAC3C,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,KAAK;AAExD,QAAM,iBAAiB,WAAW,KAAK,CAAC,MAAM,CAAC,EAAE,SAAS;AAC1D,QAAM,QACJ,SAAS,iBAAiB,UAAU;AACtC,QAAM,WAAW,UAAU;AAE3B,QAAM,QAAQ,oCAAe,oBAAI,IAAY;AAE7C,QAAM,sBACJ,oCAAgB,WAAW,SAAS,IAAI,mBAAmB;AAE7D,QAAM,oBAAoB,MAAM,OAAsB,IAAI;AAE1D,WAAS,SAAS,OAAwB;AACxC,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,CAAC,aAAa,OAAO,EAAG,QAAO;AACnC,QAAI,MAAM,IAAI,QAAQ,YAAY,CAAC,EAAG,QAAO;AAC7C,uBAAmB;AAAA,MACjB,GAAG;AAAA,MACH,EAAE,IAAI,SAAS,OAAO,SAAS,MAAM,IAAI,WAAW,MAAM;AAAA,IAC5D,CAAC;AACD,aAAS,EAAE;AACX,WAAO;AAAA,EACT;AAEA,WAAS,cAAc,OAA8C;AACnE,SAAK,MAAM,QAAQ,WAAW,MAAM,QAAQ,QAAQ,MAAM,KAAK,GAAG;AAChE,YAAM,eAAe;AACrB,eAAS,KAAK;AACd;AAAA,IACF;AACA,QAAI,MAAM,QAAQ,SAAS,MAAM,KAAK,GAAG;AACvC,YAAM,UAAU,MAAM,KAAK;AAC3B,UAAI,SAAS,OAAO,GAAG;AACrB,0BAAkB,UAAU,QAAQ,YAAY;AAAA,MAClD;AACA;AAAA,IACF;AACA,QAAI,MAAM,QAAQ,eAAe,UAAU,MAAM,WAAW,SAAS,GAAG;AACtE,YAAM,eAAe;AACrB,yBAAmB,WAAW,MAAM,GAAG,EAAE,CAAC;AAAA,IAC5C;AAAA,EACF;AAEA,WAAS,iBAAiB,IAAY;AACpC;AAAA,MACE,WAAW,IAAI,CAAC,MAAO,EAAE,OAAO,KAAK,iCAAK,IAAL,EAAQ,WAAW,KAAK,KAAI,CAAE;AAAA,IACrE;AAAA,EACF;AAEA,WAAS,gBAAgB,IAAY;AACnC,uBAAmB,WAAW,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;AAAA,EAC1D;AAEA,WAAS,cAAc,SAA2B;AArapD;AAsaI,UAAM,aACJ,8DAAqB,aAArB,YACC;AAAA,MACC,KAAI,kBAAa,OAAO,MAApB,YAAyB,QAAQ;AAAA,MACrC,QAAO,kBAAa,OAAO,MAApB,YAAyB;AAAA,MAChC,MAAM,QAAQ;AAAA,MACd,WAAW;AAAA,IACb;AACF,uBAAmB,CAAC,GAAG,YAAY,SAAS,CAAC;AAC7C,kBAAc,KAAK;AAAA,EACrB;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,YAAY;AAAA,MACd;AAAA,MAEA;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,YAAY;AAAA,YACd;AAAA,YAEC;AAAA;AAAA,QACH;AAAA,QAEA,qBAAC,SAAI,WAAU,WACb;AAAA,+BAAC,SAAI,WAAU,uCACZ;AAAA,uBAAW,IAAI,CAAC,cACf;AAAA,cAAC;AAAA;AAAA,gBAEC;AAAA,gBACA,WAAW,MAAM,iBAAiB,UAAU,EAAE;AAAA,gBAC9C,UAAU,MAAM,gBAAgB,UAAU,EAAE;AAAA;AAAA,cAHvC,UAAU;AAAA,YAIjB,CACD;AAAA,YACD;AAAA,cAAC;AAAA;AAAA,gBACC;AAAA,gBACA,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK;AAAA,gBAChD,WAAW;AAAA,gBACX,QAAQ,MAAM;AACZ,wBAAM,UAAU,MAAM,KAAK,EAAE,YAAY;AACzC,sBAAI,WAAW,kBAAkB,YAAY,SAAS;AACpD,sCAAkB,UAAU;AAC5B;AAAA,kBACF;AAGA,2BAAS,KAAK;AAAA,gBAChB;AAAA,gBACA,aAAa;AAAA,gBACb,WAAU;AAAA;AAAA,YACZ;AAAA,aACF;AAAA,UAEC,cAAc,YACb,qBAAC,SAAI,WAAU,qBACZ;AAAA,yBACC,qBAAC,iBAAiB,MAAjB,EAAsB,MAAM,YAAY,cAAc,eACrD;AAAA,kCAAC,iBAAiB,SAAjB,EAAyB,SAAO,MAC/B;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,WAAU;AAAA,kBAEV;AAAA,wCAAC,SAAM,WAAU,UAAS;AAAA,oBAAE;AAAA,oBAE5B,oBAAC,eAAY,WAAU,UAAS;AAAA;AAAA;AAAA,cAClC,GACF;AAAA,cACA,oBAAC,iBAAiB,QAAjB,EACC;AAAA,gBAAC,iBAAiB;AAAA,gBAAjB;AAAA,kBACC,MAAK;AAAA,kBACL,OAAM;AAAA,kBACN,YAAY;AAAA,kBACZ,kBAAkB;AAAA,kBAClB,WAAU;AAAA,kBAEV;AAAA,oBAAC;AAAA;AAAA,sBACC;AAAA,sBACA,aAAa;AAAA,sBACb,UAAU;AAAA,sBACV,YAAY,CAAC,UAAU;AACrB,iCAAS,KAAK;AACd,sCAAc,KAAK;AAAA,sBACrB;AAAA,sBACA;AAAA,sBACA;AAAA,sBACA;AAAA;AAAA,kBACF;AAAA;AAAA,cACF,GACF;AAAA,eACF,IACE;AAAA,YACH,YACC;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAS;AAAA,gBACT,WAAU;AAAA,gBAEV;AAAA,sCAAC,QAAK,WAAU,UAAS;AAAA,kBACxB,YAAY,gBAAgB;AAAA;AAAA;AAAA,YAC/B,IACE;AAAA,aACN,IACE;AAAA,WACN;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -19,6 +19,7 @@ interface SuggestedContact {
|
|
|
19
19
|
lastActivity?: {
|
|
20
20
|
date: string;
|
|
21
21
|
type: string;
|
|
22
|
+
timelineEventId?: string;
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
25
|
interface SuggestedActionThreadMessage {
|
|
@@ -94,7 +95,7 @@ interface SuggestedActionsProps {
|
|
|
94
95
|
signature?: string | React.ReactNode;
|
|
95
96
|
onDuplicate?: (id: number | string) => void;
|
|
96
97
|
onOpenAccountDetails?: () => void;
|
|
97
|
-
onOpenRecentActivity?: () => void;
|
|
98
|
+
onOpenRecentActivity?: (contact: SuggestedContact) => void;
|
|
98
99
|
onMarkComplete?: (id: number | string) => void;
|
|
99
100
|
onDispatchAgent?: (id: number | string, editedContent?: string, settings?: {
|
|
100
101
|
aiDisclosureEnabled?: boolean;
|