@handled-ai/design-system 0.20.3 → 0.20.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/comment-composer.js +4 -4
- package/dist/components/comment-composer.js.map +1 -1
- package/dist/components/conversation-panel.d.ts +20 -2
- package/dist/components/conversation-panel.js +80 -23
- package/dist/components/conversation-panel.js.map +1 -1
- package/dist/components/owner-chips.js +9 -9
- package/dist/components/owner-chips.js.map +1 -1
- package/dist/components/timeline-activity.js +14 -14
- package/dist/components/timeline-activity.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/comment-composer.test.tsx +28 -0
- package/src/components/__tests__/conversation-panel.test.tsx +169 -0
- package/src/components/__tests__/owner-chips.test.tsx +27 -0
- package/src/components/__tests__/timeline-activity.test.tsx +33 -0
- package/src/components/comment-composer.tsx +4 -4
- package/src/components/conversation-panel.tsx +114 -21
- package/src/components/owner-chips.tsx +12 -10
- package/src/components/timeline-activity.tsx +14 -14
|
@@ -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 { sanitizeHtml } from \"../internal/safe-html\"\nimport { ChevronDown, ChevronUp, ExternalLink } from \"lucide-react\"\n\n/**\n * Gmail-like reading-pane typography for rendered, sanitized email HTML.\n * Self-contained Tailwind child-variant utilities — no global CSS. Links use\n * Gmail blue; quoted history (`blockquote.gmail_quote`) is de-emphasized with a\n * left rule and muted, slightly smaller text.\n */\nconst EMAIL_HTML_CLASS = cn(\n \"text-sm leading-[1.62] text-foreground/90 break-words\",\n \"[&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0\",\n \"[&_a]:text-[#1a73e8] [&_a]:underline-offset-2 hover:[&_a]:underline\",\n \"[&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5\",\n \"[&_img]:max-w-full [&_img]:h-auto\",\n \"[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground [&_blockquote]:text-[13px]\",\n \"[&_.gmail_quote]:border-l-2 [&_.gmail_quote]:border-border [&_.gmail_quote]:pl-3 [&_.gmail_quote]:text-muted-foreground [&_.gmail_quote]:text-[13px]\"\n)\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 content?: React.ReactNode\n source?: {\n label: 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-[7px] top-[18px] bottom-[-4px] w-px bg-border/60\",\n dotWrapperSize: \"relative z-10 mt-1 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-background\",\n dot: \"flex h-3.5 w-3.5 items-center justify-center rounded-full border ring-[3px] ring-background\",\n contentPadding: \"flex-1 pb-4 pt-0.5\",\n titleRowSpacing: \"flex min-w-0 flex-col gap-0.5 sm:flex-row sm:items-start sm:justify-between\",\n title: \"pr-3 text-[13px] leading-snug text-foreground\",\n time: \"mt-0.5 shrink-0 whitespace-nowrap text-[11px] leading-snug text-muted-foreground/70\",\n cardContainer: \"overflow-hidden rounded-lg border border-border/70 bg-card shadow-sm\",\n cardHeader: \"border-b border-border/60 bg-background px-3 py-2\",\n cardBody: \"px-3 py-2.5 text-sm\",\n cardFooter: \"border-t border-border/60 bg-background/50 px-3 py-1.5\",\n collapsedPreview: \"flex items-center justify-between gap-2 px-3 py-2 text-sm 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.5 text-[11px] font-medium text-muted-foreground/70 transition-colors hover:text-foreground\",\n nonInteractiveContent: \"pr-2 text-[13px] leading-snug 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\">·</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\">·</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 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) && (\n <div className=\"mt-2\">\n {event.isInteractive ? (\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 TimelineSource = NonNullable<TimelineEvent[\"source\"]>\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 return (\n <>\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\">{email.from}</span>\n {email.fromEmail && (\n <span className=\"text-muted-foreground/60 text-xs truncate\">{email.fromEmail}</span>\n )}\n </div>\n {email.date && (\n <span className=\"shrink-0 text-xs text-muted-foreground/50 whitespace-nowrap\">{email.date}</span>\n )}\n </div>\n <div className=\"mt-0.5 flex items-center gap-1 text-xs text-muted-foreground\">\n <span className=\"truncate\">\n To {email.to}\n {!showAllRecipients && (email.cc || email.bcc) ? (\n <>, ...</>\n ) : null}\n {showAllRecipients && email.cc ? (\n <>, {email.cc}</>\n ) : null}\n {showAllRecipients && email.bcc ? (\n <> <span className=\"text-muted-foreground/40\">bcc</span> {email.bcc}</>\n ) : null}\n </span>\n {(email.cc || email.bcc) && (\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 >\n <ChevronDown className={cn(\"h-3 w-3 transition-transform\", showAllRecipients && \"rotate-180\")} />\n </button>\n )}\n </div>\n </>\n )\n}\n\nfunction EmailBody({ email }: { email: TimelineEmail }) {\n if (email.bodyHtml) {\n return (\n // Gmail reading-pane typography; quoted history\n // (blockquote.gmail_quote) is de-emphasized with a left rule.\n <div\n data-slot=\"timeline-email-html\"\n className={EMAIL_HTML_CLASS}\n dangerouslySetInnerHTML={{ __html: sanitizeHtml(email.bodyHtml) }}\n />\n )\n }\n\n return (\n <div className=\"whitespace-pre-line text-sm leading-relaxed text-foreground/90\">\n {email.body}\n </div>\n )\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 return (\n <div className={className} onClick={onClick}>\n <span className=\"line-clamp-1 pr-3 text-[13px]\">\n <span className=\"text-muted-foreground\">{email?.from}</span>\n <span className=\"mx-1.5 text-muted-foreground/40\">·</span>\n {email?.subject ? (\n <>\n <span className=\"text-muted-foreground\">{email.subject}</span>\n <span className=\"mx-1.5 text-muted-foreground/40\">·</span>\n </>\n ) : null}\n <span className=\"text-muted-foreground\">{preview}</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\nfunction SourceAction({\n source,\n onSourceClick,\n className,\n}: {\n source: TimelineSource\n onSourceClick?: () => void\n className: string\n}) {\n if (onSourceClick) {\n return (\n <button\n type=\"button\"\n onClick={(e) => { e.stopPropagation(); onSourceClick(); }}\n className={className}\n >\n Open in {source.label}\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 Open in {source.label}\n <ExternalLink className=\"h-3 w-3\" />\n </a>\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>\n <EmailMetadata\n email={event.email}\n showAllRecipients={showAllRecipients}\n setShowAllRecipients={setShowAllRecipients}\n />\n </div>\n\n <EmailBody email={event.email} />\n\n <ShowLessButton\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className=\"mt-2 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 ) : (\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 <EmailMetadata\n email={event.email}\n showAllRecipients={showAllRecipients}\n setShowAllRecipients={setShowAllRecipients}\n />\n </div>\n\n <div className={classes.cardBody} data-slot=\"timeline-card-body\">\n <EmailBody email={event.email} />\n </div>\n\n <div className={cn(classes.cardFooter, classes.actionLinkRow)} data-slot=\"timeline-card-footer\">\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 { sanitizeHtml } from \"../internal/safe-html\"\nimport { ChevronDown, ChevronUp, ExternalLink } from \"lucide-react\"\n\n/**\n * Gmail-like reading-pane typography for rendered, sanitized email HTML.\n * Self-contained Tailwind child-variant utilities — no global CSS. Links use\n * Gmail blue; quoted history (`blockquote.gmail_quote`) is de-emphasized with a\n * left rule and muted, slightly smaller text.\n */\nconst EMAIL_HTML_CLASS = cn(\n \"text-sm leading-[1.62] text-foreground/90 break-words\",\n \"[&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0\",\n \"[&_a]:text-[#1a73e8] [&_a]:underline-offset-2 hover:[&_a]:underline\",\n \"[&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5\",\n \"[&_img]:max-w-full [&_img]:h-auto\",\n \"[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground [&_blockquote]:text-[13px]\",\n \"[&_.gmail_quote]:border-l-2 [&_.gmail_quote]:border-border [&_.gmail_quote]:pl-3 [&_.gmail_quote]:text-muted-foreground [&_.gmail_quote]:text-[13px]\"\n)\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 content?: React.ReactNode\n source?: {\n label: 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-center justify-between border-b border-border/60 bg-muted/30 px-3 py-1.5 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\">·</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\">·</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 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) && (\n <div className=\"mt-2\">\n {event.isInteractive ? (\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 TimelineSource = NonNullable<TimelineEvent[\"source\"]>\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 return (\n <>\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\">{email.from}</span>\n {email.fromEmail && (\n <span className=\"text-muted-foreground/60 text-xs truncate\">{email.fromEmail}</span>\n )}\n </div>\n {email.date && (\n <span className=\"shrink-0 text-xs text-muted-foreground/50 whitespace-nowrap\">{email.date}</span>\n )}\n </div>\n <div className=\"mt-0.5 flex items-center gap-1 text-xs text-muted-foreground\">\n <span className=\"truncate\">\n To {email.to}\n {!showAllRecipients && (email.cc || email.bcc) ? (\n <>, ...</>\n ) : null}\n {showAllRecipients && email.cc ? (\n <>, {email.cc}</>\n ) : null}\n {showAllRecipients && email.bcc ? (\n <> <span className=\"text-muted-foreground/40\">bcc</span> {email.bcc}</>\n ) : null}\n </span>\n {(email.cc || email.bcc) && (\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 >\n <ChevronDown className={cn(\"h-3 w-3 transition-transform\", showAllRecipients && \"rotate-180\")} />\n </button>\n )}\n </div>\n </>\n )\n}\n\nfunction EmailBody({ email }: { email: TimelineEmail }) {\n if (email.bodyHtml) {\n return (\n // Gmail reading-pane typography; quoted history\n // (blockquote.gmail_quote) is de-emphasized with a left rule.\n <div\n data-slot=\"timeline-email-html\"\n className={EMAIL_HTML_CLASS}\n dangerouslySetInnerHTML={{ __html: sanitizeHtml(email.bodyHtml) }}\n />\n )\n }\n\n return (\n <div className=\"whitespace-pre-line text-sm leading-relaxed text-foreground/90\">\n {email.body}\n </div>\n )\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 return (\n <div className={className} onClick={onClick}>\n <span className=\"line-clamp-1 pr-3 text-[13px]\">\n <span className=\"text-muted-foreground\">{email?.from}</span>\n <span className=\"mx-1.5 text-muted-foreground/40\">·</span>\n {email?.subject ? (\n <>\n <span className=\"text-muted-foreground\">{email.subject}</span>\n <span className=\"mx-1.5 text-muted-foreground/40\">·</span>\n </>\n ) : null}\n <span className=\"text-muted-foreground\">{preview}</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\nfunction SourceAction({\n source,\n onSourceClick,\n className,\n}: {\n source: TimelineSource\n onSourceClick?: () => void\n className: string\n}) {\n if (onSourceClick) {\n return (\n <button\n type=\"button\"\n onClick={(e) => { e.stopPropagation(); onSourceClick(); }}\n className={className}\n >\n Open in {source.label}\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 Open in {source.label}\n <ExternalLink className=\"h-3 w-3\" />\n </a>\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>\n <EmailMetadata\n email={event.email}\n showAllRecipients={showAllRecipients}\n setShowAllRecipients={setShowAllRecipients}\n />\n </div>\n\n <EmailBody email={event.email} />\n\n <ShowLessButton\n onClick={(e) => {\n e.stopPropagation()\n setExpanded(false)\n }}\n className=\"mt-2 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 ) : (\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 <EmailMetadata\n email={event.email}\n showAllRecipients={showAllRecipients}\n setShowAllRecipients={setShowAllRecipients}\n />\n </div>\n\n <div className={classes.cardBody} data-slot=\"timeline-card-body\">\n <EmailBody email={event.email} />\n </div>\n\n <div className={cn(classes.cardFooter, classes.actionLinkRow)} data-slot=\"timeline-card-footer\">\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/dist/index.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export { ComplianceBadge, ComplianceBadgeProps, ComplianceStatus } from './compo
|
|
|
24
24
|
export { ContactChip, ContactChipProps } from './components/contact-chip.js';
|
|
25
25
|
export { ContactChannel, ContactItem, ContactList, ContactListProps } from './components/contact-list.js';
|
|
26
26
|
export { ContextualQuickActionContextLabel, ContextualQuickActionContextLabelProps, ContextualQuickActionItem, ContextualQuickActionLauncher, ContextualQuickActionLauncherProps, ContextualQuickActionLauncherVariant } from './components/contextual-quick-action-launcher.js';
|
|
27
|
-
export { ConvMessage, ConvParticipant, ConvStatus, ConversationPanel, ConversationPanelProps, ConversationReplyPayload, ConversationThread } from './components/conversation-panel.js';
|
|
27
|
+
export { ConvMessage, ConvParticipant, ConvStatus, ConversationPanel, ConversationPanelProps, ConversationReplyPayload, ConversationReplyPreview, ConversationThread } from './components/conversation-panel.js';
|
|
28
28
|
export { CheckInsCard, RecentlyCompletedCard, TopTasksCard, UpcomingMeetingsCard } from './components/dashboard-cards.js';
|
|
29
29
|
export { DataRow, DataTable, DataTableProps } from './components/data-table.js';
|
|
30
30
|
export { ConditionFieldDef, ConditionFieldOption, ConditionFilterValue, ConditionOperator, ConditionOptionObject, DEFAULT_OPERATORS, DataTableConditionFilter, DataTableConditionFilterProps, OPERATOR_LABELS, generateConditionId, getOperators, shouldShowOptionSearch } from './components/data-table-condition-filter.js';
|
package/package.json
CHANGED
|
@@ -17,6 +17,34 @@ describe("CommentComposer", () => {
|
|
|
17
17
|
expect(screen.getByText(/Internal note/i)).toBeDefined();
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
it("exposes data-open only when expanded (collapsed vs open structure)", () => {
|
|
21
|
+
const { container } = render(<CommentComposer onPost={() => {}} />);
|
|
22
|
+
const root = container.querySelector('[data-slot="comment-composer"]')!;
|
|
23
|
+
// Collapsed: no data-open attribute, compact single-row textarea, no footer.
|
|
24
|
+
expect(root.getAttribute("data-open")).toBeNull();
|
|
25
|
+
const input = screen.getByRole("textbox") as HTMLTextAreaElement;
|
|
26
|
+
expect(Number(input.rows)).toBe(1);
|
|
27
|
+
expect(screen.queryByText("Comment")).toBeNull();
|
|
28
|
+
|
|
29
|
+
// Open: data-open=true, taller textarea, footer with Cancel + Comment.
|
|
30
|
+
fireEvent.focus(input);
|
|
31
|
+
expect(root.getAttribute("data-open")).toBe("true");
|
|
32
|
+
expect(Number(input.rows)).toBe(3);
|
|
33
|
+
expect(screen.getByText("Cancel")).toBeDefined();
|
|
34
|
+
expect(screen.getByText("Comment")).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("collapses back to single-row structure on Cancel", () => {
|
|
38
|
+
render(<CommentComposer onPost={() => {}} />);
|
|
39
|
+
const input = screen.getByRole("textbox") as HTMLTextAreaElement;
|
|
40
|
+
fireEvent.change(input, { target: { value: "draft" } });
|
|
41
|
+
expect(Number(input.rows)).toBe(3);
|
|
42
|
+
fireEvent.click(screen.getByText("Cancel").closest("button")!);
|
|
43
|
+
expect(input.value).toBe("");
|
|
44
|
+
expect(Number(input.rows)).toBe(1);
|
|
45
|
+
expect(screen.queryByText("Comment")).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
20
48
|
it("disables Comment until there is non-whitespace text", () => {
|
|
21
49
|
render(<CommentComposer onPost={() => {}} />);
|
|
22
50
|
const input = screen.getByRole("textbox");
|
|
@@ -461,4 +461,173 @@ describe("ConversationPanel", () => {
|
|
|
461
461
|
fireEvent.click(screen.getByText("Reply"));
|
|
462
462
|
expect(screen.getByPlaceholderText("Write your reply…")).toBeDefined();
|
|
463
463
|
});
|
|
464
|
+
|
|
465
|
+
it("renders draft-ready header copy and the per-thread Draft pill", () => {
|
|
466
|
+
render(
|
|
467
|
+
<ConversationPanel
|
|
468
|
+
threads={[
|
|
469
|
+
thread({
|
|
470
|
+
threadId: "d1",
|
|
471
|
+
subject: "Saw you're hiring, Jason",
|
|
472
|
+
status: "draft",
|
|
473
|
+
lastWhen: "10h ago",
|
|
474
|
+
messages: [
|
|
475
|
+
{
|
|
476
|
+
id: "draft-1",
|
|
477
|
+
direction: "outbound",
|
|
478
|
+
from: me,
|
|
479
|
+
to: priya,
|
|
480
|
+
date: "Jun 7, 2026, 11:42 AM",
|
|
481
|
+
receipt: { kind: "draft", label: "Draft" },
|
|
482
|
+
bodyHtml: "<p>Hi Jason,</p>",
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
}),
|
|
486
|
+
]}
|
|
487
|
+
me={me}
|
|
488
|
+
/>,
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
expect(screen.getByText("Draft ready")).toBeDefined();
|
|
492
|
+
expect(screen.getByText("Draft ready on 1 thread")).toBeDefined();
|
|
493
|
+
expect(screen.getByText("1 thread on this action")).toBeDefined();
|
|
494
|
+
// Both the row pill and the message receipt render "Draft".
|
|
495
|
+
expect(screen.getAllByText("Draft").length).toBeGreaterThanOrEqual(2);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("expands two distinct same-subject threads independently", () => {
|
|
499
|
+
const sharedSubject = "Saw you're hiring, Jason";
|
|
500
|
+
const draftThread = (id: string): ConversationThread =>
|
|
501
|
+
thread({
|
|
502
|
+
threadId: id,
|
|
503
|
+
subject: sharedSubject,
|
|
504
|
+
status: "draft",
|
|
505
|
+
contact: priya,
|
|
506
|
+
messages: [
|
|
507
|
+
{
|
|
508
|
+
id: `${id}-m1`,
|
|
509
|
+
direction: "outbound",
|
|
510
|
+
from: me,
|
|
511
|
+
to: priya,
|
|
512
|
+
date: "Jun 7",
|
|
513
|
+
receipt: { kind: "draft", label: "Draft" },
|
|
514
|
+
bodyHtml: `<p>Draft body for ${id}</p>`,
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const { container } = render(
|
|
520
|
+
<ConversationPanel
|
|
521
|
+
threads={[draftThread("ta"), draftThread("tb")]}
|
|
522
|
+
me={me}
|
|
523
|
+
defaultOpenThreadId="ta"
|
|
524
|
+
/>,
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const rows = container.querySelectorAll('[data-slot="conv-thread"]');
|
|
528
|
+
expect(rows.length).toBe(2);
|
|
529
|
+
|
|
530
|
+
// Only the first thread is open initially.
|
|
531
|
+
expect(rows[0].getAttribute("data-open")).toBe("true");
|
|
532
|
+
expect(rows[1].getAttribute("data-open")).toBeNull();
|
|
533
|
+
expect(screen.getByText("Draft body for ta")).toBeDefined();
|
|
534
|
+
expect(screen.queryByText("Draft body for tb")).toBeNull();
|
|
535
|
+
|
|
536
|
+
// Opening the second thread (same subject) opens it without affecting the first row's identity.
|
|
537
|
+
fireEvent.click(rows[1].querySelector("button")!);
|
|
538
|
+
const rowsAfter = container.querySelectorAll('[data-slot="conv-thread"]');
|
|
539
|
+
expect(rowsAfter[1].getAttribute("data-open")).toBe("true");
|
|
540
|
+
expect(screen.getByText("Draft body for tb")).toBeDefined();
|
|
541
|
+
// The first row collapsed (single-open hub), proving rows track independently by threadId.
|
|
542
|
+
expect(rowsAfter[0].getAttribute("data-open")).toBeNull();
|
|
543
|
+
expect(screen.queryByText("Draft body for ta")).toBeNull();
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("shows an async loading state, then renders resolved server preview HTML", async () => {
|
|
547
|
+
let resolvePreview: (value: { htmlBody: string; confirmationToken?: string }) => void = () => {};
|
|
548
|
+
const onPreviewReply = vi.fn().mockImplementation(
|
|
549
|
+
() =>
|
|
550
|
+
new Promise<{ htmlBody: string; confirmationToken?: string }>((resolve) => {
|
|
551
|
+
resolvePreview = resolve;
|
|
552
|
+
}),
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
render(
|
|
556
|
+
<ConversationPanel
|
|
557
|
+
threads={[thread({ draft: "Sounds good" })]}
|
|
558
|
+
me={me}
|
|
559
|
+
onPreviewReply={onPreviewReply}
|
|
560
|
+
/>,
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
fireEvent.click(screen.getByText("Reply"));
|
|
564
|
+
fireEvent.click(screen.getByText("Preview"));
|
|
565
|
+
|
|
566
|
+
expect(onPreviewReply).toHaveBeenCalledTimes(1);
|
|
567
|
+
expect(screen.getByText("Loading preview…")).toBeDefined();
|
|
568
|
+
|
|
569
|
+
resolvePreview({ htmlBody: "<p>Server prepared reply</p>" });
|
|
570
|
+
|
|
571
|
+
await waitFor(() => expect(screen.getByText("Server prepared reply")).toBeDefined());
|
|
572
|
+
expect(screen.queryByText("Loading preview…")).toBeNull();
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it("retains the confirmation token in preview state after the preview resolves", async () => {
|
|
576
|
+
const onPreviewReply = vi.fn().mockResolvedValue({
|
|
577
|
+
htmlBody: "<p>Server prepared reply</p>",
|
|
578
|
+
confirmationToken: "conf-token-123",
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
render(
|
|
582
|
+
<ConversationPanel
|
|
583
|
+
threads={[thread({ draft: "Sounds good" })]}
|
|
584
|
+
me={me}
|
|
585
|
+
onPreviewReply={onPreviewReply}
|
|
586
|
+
/>,
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
fireEvent.click(screen.getByText("Reply"));
|
|
590
|
+
fireEvent.click(screen.getByText("Preview"));
|
|
591
|
+
|
|
592
|
+
await waitFor(() => expect(screen.getByText("Server prepared reply")).toBeDefined());
|
|
593
|
+
|
|
594
|
+
const body = document.querySelector('[data-slot="conv-preview-body"]')!;
|
|
595
|
+
expect(body.getAttribute("data-confirmation-token")).toBe("conf-token-123");
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("sanitizes server preview HTML before rendering it", async () => {
|
|
599
|
+
const onPreviewReply = vi.fn().mockResolvedValue({
|
|
600
|
+
htmlBody:
|
|
601
|
+
'<p onclick="alert(1)">Hi Jason</p><a href="javascript:alert(1)">bad link</a><script>alert(1)</script>',
|
|
602
|
+
confirmationToken: "conf-token-xyz",
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
render(
|
|
606
|
+
<ConversationPanel
|
|
607
|
+
threads={[thread({ draft: "Sounds good" })]}
|
|
608
|
+
me={me}
|
|
609
|
+
onPreviewReply={onPreviewReply}
|
|
610
|
+
/>,
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
fireEvent.click(screen.getByText("Reply"));
|
|
614
|
+
fireEvent.click(screen.getByText("Preview"));
|
|
615
|
+
|
|
616
|
+
await waitFor(() => expect(screen.getByText("Hi Jason")).toBeDefined());
|
|
617
|
+
|
|
618
|
+
const body = document.querySelector('[data-slot="conv-preview-body"]')!;
|
|
619
|
+
expect(body.innerHTML).not.toContain("script");
|
|
620
|
+
expect(body.innerHTML).not.toContain("onclick");
|
|
621
|
+
expect(body.innerHTML).not.toContain("javascript:");
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it("labels the fallback preview as a local draft preview when no server preview prop is provided", () => {
|
|
625
|
+
render(<ConversationPanel threads={[thread({ draft: "Sounds good" })]} me={me} />);
|
|
626
|
+
|
|
627
|
+
fireEvent.click(screen.getByText("Reply"));
|
|
628
|
+
fireEvent.click(screen.getByText("Preview"));
|
|
629
|
+
|
|
630
|
+
expect(screen.getByText("Local draft preview")).toBeDefined();
|
|
631
|
+
expect(screen.getByText(/Local draft preview only/i)).toBeDefined();
|
|
632
|
+
});
|
|
464
633
|
});
|
|
@@ -94,6 +94,21 @@ describe("SignalOwnerChip", () => {
|
|
|
94
94
|
expect(screen.getByText("Dana Okafor")).toBeDefined();
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
it("applies the compact component-owned size/padding/type classes", () => {
|
|
98
|
+
const { container } = render(<SignalOwnerChip owner={dana} />);
|
|
99
|
+
const el = container.querySelector('[data-slot="signal-owner-chip"]')!;
|
|
100
|
+
const cls = el.className;
|
|
101
|
+
// Compact sizing is owned by the component, not consumer overrides.
|
|
102
|
+
expect(cls).toContain("h-7");
|
|
103
|
+
expect(cls).toContain("px-2");
|
|
104
|
+
expect(cls).toContain("gap-1");
|
|
105
|
+
expect(cls).toContain("text-[12px]");
|
|
106
|
+
// No longer the larger pre-polish sizing.
|
|
107
|
+
expect(cls).not.toContain("h-8");
|
|
108
|
+
expect(cls).not.toContain("px-2.5");
|
|
109
|
+
expect(cls).not.toContain("text-[13px]");
|
|
110
|
+
});
|
|
111
|
+
|
|
97
112
|
it("renders a static span (no button) when read-only / no handlers", () => {
|
|
98
113
|
const { container } = render(<SignalOwnerChip owner={dana} />);
|
|
99
114
|
const el = container.querySelector('[data-slot="signal-owner-chip"]');
|
|
@@ -128,6 +143,18 @@ describe("AccountOwnerChip", () => {
|
|
|
128
143
|
expect(within(el as HTMLElement).getByText("Dana Okafor")).toBeDefined();
|
|
129
144
|
});
|
|
130
145
|
|
|
146
|
+
it("applies the compact component-owned size/padding/type classes to the chip", () => {
|
|
147
|
+
const { container } = render(<AccountOwnerChip owners={[dana]} />);
|
|
148
|
+
const el = container.querySelector('[data-slot="account-owner-chip"]')!;
|
|
149
|
+
const cls = el.className;
|
|
150
|
+
expect(cls).toContain("h-7");
|
|
151
|
+
expect(cls).toContain("px-2");
|
|
152
|
+
expect(cls).toContain("gap-1");
|
|
153
|
+
expect(cls).toContain("text-[12px]");
|
|
154
|
+
expect(cls).not.toContain("h-8");
|
|
155
|
+
expect(cls).not.toContain("px-2.5");
|
|
156
|
+
});
|
|
157
|
+
|
|
131
158
|
it("single owner menu shows role, email, helper text, and Salesforce link only with href", () => {
|
|
132
159
|
const { container } = render(
|
|
133
160
|
<AccountOwnerChip owners={[{ ...dana, href: "https://sf.example/u1" }]} />,
|
|
@@ -56,6 +56,39 @@ describe("TimelineActivity", () => {
|
|
|
56
56
|
expect(card?.querySelector('[data-slot="timeline-card-footer"]')).not.toBeNull()
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
+
it("renders the case-panel rail, dot, title, and content card structure", () => {
|
|
60
|
+
const event = minimal({
|
|
61
|
+
title: "Playbook stopped, customer responded",
|
|
62
|
+
isInteractive: true,
|
|
63
|
+
defaultExpanded: true,
|
|
64
|
+
content: "Playbook stopped automatically",
|
|
65
|
+
source: { label: "Salesforce", url: "https://salesforce.example/case/1" },
|
|
66
|
+
})
|
|
67
|
+
const second = minimal({ id: "e2", title: "Priya Raman replied to your outreach" })
|
|
68
|
+
|
|
69
|
+
const { container } = render(
|
|
70
|
+
<TimelineActivity events={[event, second]} variant="case-panel" />,
|
|
71
|
+
)
|
|
72
|
+
const root = container.firstElementChild
|
|
73
|
+
|
|
74
|
+
// Rail/connector is rendered between rows (present on the non-last row).
|
|
75
|
+
const connector = root?.querySelector(".bg-border\\/50")
|
|
76
|
+
expect(connector).not.toBeNull()
|
|
77
|
+
|
|
78
|
+
// Dot is present with the tone/neutral classes.
|
|
79
|
+
const dot = root?.querySelector('[data-testid="timeline-dot"]')
|
|
80
|
+
expect(dot).not.toBeNull()
|
|
81
|
+
|
|
82
|
+
// Title text renders.
|
|
83
|
+
expect(screen.getByText("Playbook stopped, customer responded")).toBeDefined()
|
|
84
|
+
|
|
85
|
+
// Interactive content card renders the case-panel body + footer slots.
|
|
86
|
+
const card = root?.querySelector('[data-variant="case-panel"]')
|
|
87
|
+
expect(card).not.toBeNull()
|
|
88
|
+
expect(card?.querySelector('[data-slot="timeline-card-body"]')).not.toBeNull()
|
|
89
|
+
expect(card?.querySelector('[data-slot="timeline-card-footer"]')).not.toBeNull()
|
|
90
|
+
})
|
|
91
|
+
|
|
59
92
|
it("preserves the default expanded email card single-container layout", () => {
|
|
60
93
|
const event = minimal({
|
|
61
94
|
isInteractive: true,
|
|
@@ -55,12 +55,12 @@ function CommentComposer({
|
|
|
55
55
|
data-slot="comment-composer"
|
|
56
56
|
data-open={open ? "true" : undefined}
|
|
57
57
|
className={cn(
|
|
58
|
-
"border-border bg-background flex items-start gap-2 rounded-lg border
|
|
58
|
+
"border-border bg-background flex items-start gap-2 rounded-lg border px-2 py-1.5 transition-colors",
|
|
59
59
|
open && "ring-ring/30 ring-2",
|
|
60
60
|
className
|
|
61
61
|
)}
|
|
62
62
|
>
|
|
63
|
-
<Avatar size="sm" className="mt-
|
|
63
|
+
<Avatar size="sm" className="mt-px">
|
|
64
64
|
{author?.avatarUrl ? <AvatarImage src={author.avatarUrl} alt={author.name ?? "You"} /> : null}
|
|
65
65
|
<AvatarFallback className="bg-muted text-muted-foreground text-[10px] font-medium uppercase">
|
|
66
66
|
{getInitials({ name: author?.name, email: author?.email })}
|
|
@@ -76,7 +76,7 @@ function CommentComposer({
|
|
|
76
76
|
placeholder={placeholder}
|
|
77
77
|
rows={open ? 3 : 1}
|
|
78
78
|
className={cn(
|
|
79
|
-
"resize-none border-0 bg-transparent
|
|
79
|
+
"resize-none border-0 bg-transparent px-1 py-0.5 text-sm leading-snug shadow-none focus-visible:ring-0",
|
|
80
80
|
!open && "min-h-0"
|
|
81
81
|
)}
|
|
82
82
|
onKeyDown={(e) => {
|
|
@@ -88,7 +88,7 @@ function CommentComposer({
|
|
|
88
88
|
/>
|
|
89
89
|
|
|
90
90
|
{open ? (
|
|
91
|
-
<div className="mt-
|
|
91
|
+
<div className="mt-0.5 flex items-center justify-between gap-2">
|
|
92
92
|
<span className="text-muted-foreground inline-flex items-center gap-1 text-[11px]">
|
|
93
93
|
<Lock size={12} /> {hint}
|
|
94
94
|
</span>
|