@handled-ai/design-system 0.18.58 → 0.19.0-rc.1
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/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/case-panel-activity-timeline.d.ts +2 -0
- package/dist/components/case-panel-activity-timeline.js +22 -1
- package/dist/components/case-panel-activity-timeline.js.map +1 -1
- package/dist/components/comment-composer.d.ts +29 -0
- package/dist/components/comment-composer.js +102 -0
- package/dist/components/comment-composer.js.map +1 -0
- package/dist/components/conversation-panel.d.ts +95 -0
- package/dist/components/conversation-panel.js +636 -0
- package/dist/components/conversation-panel.js.map +1 -0
- package/dist/components/detail-view.js +1 -1
- package/dist/components/detail-view.js.map +1 -1
- package/dist/components/owner-chips.d.ts +59 -0
- package/dist/components/owner-chips.js +256 -0
- package/dist/components/owner-chips.js.map +1 -0
- package/dist/components/pill.d.ts +1 -1
- package/dist/components/score-why-chips.d.ts +1 -1
- package/dist/components/signal-priority-popover.d.ts +1 -1
- package/dist/components/signal-priority-popover.js +16 -7
- package/dist/components/signal-priority-popover.js.map +1 -1
- package/dist/components/tabs.d.ts +1 -1
- package/dist/components/timeline-activity.d.ts +7 -0
- package/dist/components/timeline-activity.js +22 -1
- package/dist/components/timeline-activity.js.map +1 -1
- package/dist/components/virtualized-data-table.js +4 -4
- package/dist/components/virtualized-data-table.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/safe-html.d.ts +11 -0
- package/dist/internal/safe-html.js +222 -0
- package/dist/internal/safe-html.js.map +1 -0
- package/dist/prototype/index.d.ts +1 -1
- package/dist/prototype/prototype-accounts-view.d.ts +1 -1
- package/dist/prototype/prototype-admin-view.d.ts +1 -1
- package/dist/prototype/prototype-config.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.js +2 -0
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/dist/prototype/prototype-insights-view.d.ts +1 -1
- package/dist/prototype/prototype-shell.d.ts +1 -1
- package/dist/{signal-priority-popover-QJngMAj7.d.ts → signal-priority-popover-CZitE9xq.d.ts} +11 -2
- package/package.json +1 -1
- package/src/components/__tests__/comment-composer.test.tsx +57 -0
- package/src/components/__tests__/conversation-panel.test.tsx +157 -0
- package/src/components/__tests__/owner-chips.test.tsx +100 -0
- package/src/components/__tests__/signal-priority-popover.test.tsx +41 -4
- package/src/components/__tests__/timeline-activity.test.tsx +55 -0
- package/src/components/__tests__/virtualized-data-table-resize.test.tsx +18 -0
- package/src/components/case-panel-activity-timeline.tsx +20 -0
- package/src/components/comment-composer.tsx +119 -0
- package/src/components/conversation-panel.tsx +790 -0
- package/src/components/detail-view.tsx +3 -1
- package/src/components/owner-chips.tsx +335 -0
- package/src/components/signal-priority-popover.tsx +19 -6
- package/src/components/timeline-activity.tsx +37 -3
- package/src/components/virtualized-data-table.tsx +4 -4
- package/src/index.ts +4 -1
- package/src/internal/__tests__/safe-html.test.ts +53 -0
- package/src/internal/safe-html.ts +284 -0
- package/src/prototype/__tests__/detail-view-score-why.test.tsx +34 -0
- package/src/prototype/prototype-config.ts +5 -1
- package/src/prototype/prototype-inbox-view.tsx +2 -0
|
@@ -3,7 +3,7 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const badgeVariants: (props?: ({
|
|
6
|
-
variant?: "
|
|
6
|
+
variant?: "default" | "secondary" | "destructive" | "outline" | "ghost" | "link" | null | undefined;
|
|
7
7
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
8
8
|
declare function Badge({ className, variant, asChild, ...props }: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & {
|
|
9
9
|
asChild?: boolean;
|
|
@@ -3,7 +3,7 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const buttonVariants: (props?: ({
|
|
6
|
-
variant?: "
|
|
6
|
+
variant?: "default" | "secondary" | "destructive" | "outline" | "ghost" | "link" | null | undefined;
|
|
7
7
|
size?: "default" | "sm" | "lg" | "icon" | null | undefined;
|
|
8
8
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
9
9
|
declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
|
|
@@ -57,6 +57,8 @@ type CasePanelActivityPayload = {
|
|
|
57
57
|
dueLabel: string;
|
|
58
58
|
status?: "upcoming" | "due" | "overdue" | "met";
|
|
59
59
|
description?: string;
|
|
60
|
+
/** 0..1 elapsed toward the deadline; renders a thin progress bar. */
|
|
61
|
+
progress?: number;
|
|
60
62
|
} | {
|
|
61
63
|
kind: "operatorNote";
|
|
62
64
|
note: string;
|
|
@@ -228,7 +228,28 @@ function renderPayloadContent(payload, eventId, onPayloadAction) {
|
|
|
228
228
|
/* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: payload.dueLabel }),
|
|
229
229
|
payload.status ? /* @__PURE__ */ jsx("span", { className: cn("rounded-full border px-2 py-0.5 text-[11px] font-medium", STATUS_CLASSES[payload.status]), children: payload.status }) : null
|
|
230
230
|
] }),
|
|
231
|
-
payload.description ? /* @__PURE__ */ jsx("p", { className: "text-xs leading-relaxed text-muted-foreground", children: payload.description }) : null
|
|
231
|
+
payload.description ? /* @__PURE__ */ jsx("p", { className: "text-xs leading-relaxed text-muted-foreground", children: payload.description }) : null,
|
|
232
|
+
typeof payload.progress === "number" ? /* @__PURE__ */ jsx(
|
|
233
|
+
"div",
|
|
234
|
+
{
|
|
235
|
+
"data-slot": "deadline-progress",
|
|
236
|
+
role: "progressbar",
|
|
237
|
+
"aria-valuenow": Math.round(Math.min(1, Math.max(0, payload.progress)) * 100),
|
|
238
|
+
"aria-valuemin": 0,
|
|
239
|
+
"aria-valuemax": 100,
|
|
240
|
+
className: "bg-muted h-1.5 w-full overflow-hidden rounded-full",
|
|
241
|
+
children: /* @__PURE__ */ jsx(
|
|
242
|
+
"div",
|
|
243
|
+
{
|
|
244
|
+
className: cn(
|
|
245
|
+
"h-full rounded-full",
|
|
246
|
+
payload.status === "overdue" ? "bg-red-500" : payload.status === "due" ? "bg-amber-500" : "bg-foreground/70"
|
|
247
|
+
),
|
|
248
|
+
style: { width: `${Math.min(1, Math.max(0, payload.progress)) * 100}%` }
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
) : null
|
|
232
253
|
] });
|
|
233
254
|
case "operatorNote":
|
|
234
255
|
return /* @__PURE__ */ jsx("p", { className: "whitespace-pre-line text-sm leading-relaxed text-foreground", children: payload.note });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/case-panel-activity-timeline.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDown, ExternalLink } from \"lucide-react\"\nimport { cn } from \"../lib/utils\"\nimport { TONE_CLASSES, type TimelineEventTone } from \"./timeline-activity\"\n\nexport type CasePanelActivityTone = TimelineEventTone\n\nexport type CasePanelActivityActor =\n | { kind: \"system\" }\n | { kind: \"integration\"; name: string; iconUrl?: string }\n | { kind: \"user\"; name: string; avatarUrl?: string; verb?: string }\n\nexport type CasePanelPayloadAction = {\n kind: \"openSignal\"\n key: string\n eventId: string\n}\n\nexport type CasePanelActivityPayload =\n | {\n kind: \"signal\"\n key: string\n summary: string\n detail?: string\n actionLabel?: string\n }\n | {\n kind: \"scoreUpdate\"\n label?: string\n previousScore?: number\n nextScore: number\n reason?: string\n }\n | {\n kind: \"recommendation\"\n recommendation: string\n rationale?: string\n actionLabel?: string\n }\n | {\n kind: \"email\"\n from: string\n to?: string\n subject: string\n preview?: string\n body?: string\n }\n | {\n kind: \"salesforce\"\n objectLabel: string\n recordLabel?: string\n changeSummary: string\n deepLink?: {\n href: string\n label?: string\n }\n }\n | {\n kind: \"deadline\"\n dueLabel: string\n status?: \"upcoming\" | \"due\" | \"overdue\" | \"met\"\n description?: string\n }\n | {\n kind: \"operatorNote\"\n note: string\n }\n | {\n kind: \"assignment\"\n assignee: string\n from?: string\n role?: string\n }\n | {\n kind: \"caseOpened\"\n source?: string\n openedBy?: string\n description?: string\n }\n | {\n kind: \"generic\"\n description: string\n metadata?: Array<{ label: string; value: string }>\n }\n\nexport interface CasePanelActivityEvent {\n id: string\n title: string\n timeLabel: string\n tone: CasePanelActivityTone\n actor?: CasePanelActivityActor\n isSystemNoise?: boolean\n payload: CasePanelActivityPayload\n}\n\nexport interface CasePanelActivityTimelineProps {\n events: CasePanelActivityEvent[]\n className?: string\n title?: string\n defaultExpanded?: boolean\n defaultShowSystemEvents?: boolean\n onPayloadAction?: (action: CasePanelPayloadAction) => void\n}\n\nconst NEUTRAL_DOT_CLASSES = \"bg-background border-border/60\"\nconst NEUTRAL_ICON_CLASSES = \"text-muted-foreground\"\n\nconst PAYLOAD_LABELS: Record<CasePanelActivityPayload[\"kind\"], string> = {\n signal: \"Signal\",\n scoreUpdate: \"Score update\",\n recommendation: \"Recommendation\",\n email: \"Email\",\n salesforce: \"Salesforce\",\n deadline: \"Deadline\",\n operatorNote: \"Operator note\",\n assignment: \"Assignment\",\n caseOpened: \"Case opened\",\n generic: \"Update\",\n}\n\nconst STATUS_CLASSES: Record<NonNullable<Extract<CasePanelActivityPayload, { kind: \"deadline\" }>[\"status\"]>, string> = {\n upcoming: \"border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-900/40 dark:bg-blue-950/30 dark:text-blue-300\",\n due: \"border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/40 dark:bg-amber-950/30 dark:text-amber-300\",\n overdue: \"border-red-200 bg-red-50 text-red-700 dark:border-red-900/40 dark:bg-red-950/30 dark:text-red-300\",\n met: \"border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/40 dark:bg-emerald-950/30 dark:text-emerald-300\",\n}\n\nexport function CasePanelActivityTimeline({\n events,\n className,\n title = \"Activity\",\n defaultExpanded = false,\n defaultShowSystemEvents = false,\n onPayloadAction,\n}: CasePanelActivityTimelineProps) {\n const [expanded, setExpanded] = React.useState(defaultExpanded)\n const [showSystemEvents, setShowSystemEvents] = React.useState(defaultShowSystemEvents)\n\n const nonSystemEvents = events.filter((event) => !event.isSystemNoise)\n const visibleEvents = showSystemEvents ? events : nonSystemEvents\n const systemNoiseCount = events.length - nonSystemEvents.length\n const lastActivityText = nonSystemEvents[0]?.timeLabel ?? events[0]?.timeLabel ?? \"No activity yet\"\n\n return (\n <section className={cn(\"rounded-xl border border-border bg-card text-card-foreground shadow-sm\", className)}>\n <button\n type=\"button\"\n className=\"flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition-colors hover:bg-muted/30\"\n aria-expanded={expanded}\n onClick={() => setExpanded((current) => !current)}\n >\n <span className=\"min-w-0\">\n <span className=\"flex items-center gap-2\">\n <span className=\"text-sm font-semibold text-foreground\">{title}</span>\n <span className=\"rounded-full border border-border bg-muted/40 px-2 py-0.5 text-[11px] font-medium text-muted-foreground\">\n {nonSystemEvents.length} {nonSystemEvents.length === 1 ? \"event\" : \"events\"}\n </span>\n </span>\n <span className=\"mt-1 block truncate text-xs text-muted-foreground\">Last activity {lastActivityText}</span>\n </span>\n <ChevronDown className={cn(\"h-4 w-4 shrink-0 text-muted-foreground transition-transform\", expanded && \"rotate-180\")} />\n </button>\n\n {expanded ? (\n <div className=\"border-t border-border px-4 py-4\">\n {systemNoiseCount > 0 ? (\n <label className=\"mb-4 flex items-center justify-between gap-3 rounded-lg border border-border/70 bg-muted/20 px-3 py-2 text-xs text-muted-foreground\">\n <span>Show system events</span>\n <input\n type=\"checkbox\"\n className=\"h-4 w-4 rounded border-border text-primary accent-current\"\n checked={showSystemEvents}\n onChange={(event) => setShowSystemEvents(event.target.checked)}\n />\n </label>\n ) : null}\n\n {visibleEvents.length > 0 ? (\n <div className=\"space-y-0\">\n {visibleEvents.map((event, index) => (\n <CasePanelActivityTimelineItem\n key={event.id}\n event={event}\n isLast={index === visibleEvents.length - 1}\n onPayloadAction={onPayloadAction}\n />\n ))}\n </div>\n ) : (\n <p className=\"rounded-lg border border-dashed border-border px-3 py-6 text-center text-sm text-muted-foreground\">\n No activity to show.\n </p>\n )}\n </div>\n ) : null}\n </section>\n )\n}\n\nfunction CasePanelActivityTimelineItem({\n event,\n isLast,\n onPayloadAction,\n}: {\n event: CasePanelActivityEvent\n isLast: boolean\n onPayloadAction?: (action: CasePanelPayloadAction) => void\n}) {\n const toneStyle = TONE_CLASSES[event.tone]\n const dotClasses = toneStyle ? toneStyle.dot : NEUTRAL_DOT_CLASSES\n const iconClasses = toneStyle ? toneStyle.icon : NEUTRAL_ICON_CLASSES\n\n return (\n <article className=\"group relative flex gap-3.5\" data-testid=\"case-panel-activity-event\">\n {!isLast ? <div className=\"absolute bottom-[-6px] left-[9px] top-5 w-px bg-border/60\" /> : null}\n <div className=\"relative z-10 mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-card\">\n <span\n aria-hidden=\"true\"\n className={cn(\"flex h-4.5 w-4.5 items-center justify-center rounded-full border ring-4 ring-card\", dotClasses, iconClasses)}\n data-testid=\"case-panel-activity-dot\"\n >\n <span className=\"h-1.5 w-1.5 rounded-full bg-current\" />\n </span>\n </div>\n <div className=\"min-w-0 flex-1 pb-5 pt-0.5\">\n <div className=\"flex min-w-0 flex-col gap-1 sm:flex-row sm:items-start sm:justify-between\">\n <h3 className=\"pr-4 text-[13px] font-medium leading-relaxed text-foreground\">{event.title}</h3>\n <time className=\"mt-0.5 shrink-0 whitespace-nowrap text-[11px] text-muted-foreground/70\">{event.timeLabel}</time>\n </div>\n <ActorByline actor={event.actor} timeLabel={event.timeLabel} />\n <PayloadCard event={event} onPayloadAction={onPayloadAction} />\n </div>\n </article>\n )\n}\n\nfunction ActorByline({ actor, timeLabel }: { actor?: CasePanelActivityActor; timeLabel: string }) {\n if (!actor || 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=\"case-panel-activity-byline\">\n {actor.iconUrl ? <img src={actor.iconUrl} alt=\"\" className=\"h-4 w-4 rounded-sm object-cover\" /> : null}\n <span className=\"font-medium text-foreground\">{actor.name}</span>\n <span>synced this update</span>\n <span className=\"text-muted-foreground/40\">·</span>\n <span>{timeLabel}</span>\n </div>\n )\n }\n\n const verb = actor.verb ?? \"updated this case\"\n const initial = 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=\"case-panel-activity-byline\">\n {actor.avatarUrl ? (\n <img src={actor.avatarUrl} alt={actor.name} className=\"h-4 w-4 rounded-full object-cover\" />\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 {initial}\n </span>\n )}\n <span className=\"font-medium text-foreground\">{actor.name}</span>\n <span>{verb}</span>\n <span className=\"text-muted-foreground/40\">·</span>\n <span>{timeLabel}</span>\n </div>\n )\n}\n\nfunction PayloadCard({\n event,\n onPayloadAction,\n}: {\n event: CasePanelActivityEvent\n onPayloadAction?: (action: CasePanelPayloadAction) => void\n}) {\n const payload = event.payload\n\n return (\n <div className=\"mt-2 rounded-lg border border-border/80 bg-muted/20 px-3 py-2.5 text-sm\" data-testid={`case-panel-payload-${payload.kind}`}>\n <div className=\"mb-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground\">\n {PAYLOAD_LABELS[payload.kind]}\n </div>\n {renderPayloadContent(payload, event.id, onPayloadAction)}\n </div>\n )\n}\n\nfunction renderPayloadContent(\n payload: CasePanelActivityPayload,\n eventId: string,\n onPayloadAction?: (action: CasePanelPayloadAction) => void\n) {\n switch (payload.kind) {\n case \"signal\":\n return (\n <div className=\"space-y-2\">\n <p className=\"text-foreground\">{payload.summary}</p>\n {payload.detail ? <p className=\"text-xs leading-relaxed text-muted-foreground\">{payload.detail}</p> : null}\n {onPayloadAction && payload.actionLabel ? (\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n onClick={() => onPayloadAction({ kind: \"openSignal\", key: payload.key, eventId })}\n >\n {payload.actionLabel}\n </button>\n ) : null}\n </div>\n )\n case \"scoreUpdate\":\n return (\n <div className=\"space-y-1\">\n <p className=\"text-foreground\">\n {payload.label ?? \"Score\"}: {payload.previousScore !== undefined ? `${payload.previousScore} → ` : \"\"}{payload.nextScore}\n </p>\n {payload.reason ? <p className=\"text-xs leading-relaxed text-muted-foreground\">{payload.reason}</p> : null}\n </div>\n )\n case \"recommendation\":\n return (\n <div className=\"space-y-1\">\n <p className=\"font-medium text-foreground\">{payload.recommendation}</p>\n {payload.rationale ? <p className=\"text-xs leading-relaxed text-muted-foreground\">{payload.rationale}</p> : null}\n {payload.actionLabel ? <p className=\"text-xs font-medium text-muted-foreground\">{payload.actionLabel}</p> : null}\n </div>\n )\n case \"email\":\n return (\n <div className=\"space-y-1\">\n <p className=\"font-medium text-foreground\">{payload.subject}</p>\n <p className=\"text-xs text-muted-foreground\">\n From {payload.from}{payload.to ? ` to ${payload.to}` : \"\"}\n </p>\n {payload.preview ? <p className=\"text-sm text-foreground/90\">{payload.preview}</p> : null}\n {payload.body ? <p className=\"whitespace-pre-line text-xs leading-relaxed text-muted-foreground\">{payload.body}</p> : null}\n </div>\n )\n case \"salesforce\":\n return (\n <div className=\"space-y-2\">\n <p className=\"text-foreground\">\n <span className=\"font-medium\">{payload.objectLabel}</span>\n {payload.recordLabel ? <span className=\"text-muted-foreground\"> · {payload.recordLabel}</span> : null}\n </p>\n <p className=\"text-xs leading-relaxed text-muted-foreground\">{payload.changeSummary}</p>\n {payload.deepLink ? (\n <a\n href={payload.deepLink.href}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className=\"inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n >\n {payload.deepLink.label ?? \"Open in Salesforce\"}\n <ExternalLink className=\"h-3 w-3\" />\n </a>\n ) : null}\n </div>\n )\n case \"deadline\":\n return (\n <div className=\"space-y-2\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"font-medium text-foreground\">{payload.dueLabel}</span>\n {payload.status ? (\n <span className={cn(\"rounded-full border px-2 py-0.5 text-[11px] font-medium\", STATUS_CLASSES[payload.status])}>\n {payload.status}\n </span>\n ) : null}\n </div>\n {payload.description ? <p className=\"text-xs leading-relaxed text-muted-foreground\">{payload.description}</p> : null}\n </div>\n )\n case \"operatorNote\":\n return <p className=\"whitespace-pre-line text-sm leading-relaxed text-foreground\">{payload.note}</p>\n case \"assignment\":\n return (\n <p className=\"text-foreground\">\n Assigned to <span className=\"font-medium\">{payload.assignee}</span>\n {payload.role ? <span className=\"text-muted-foreground\"> as {payload.role}</span> : null}\n {payload.from ? <span className=\"text-muted-foreground\"> from {payload.from}</span> : null}\n </p>\n )\n case \"caseOpened\":\n return (\n <div className=\"space-y-1\">\n <p className=\"text-foreground\">\n Case opened{payload.openedBy ? ` by ${payload.openedBy}` : \"\"}{payload.source ? ` from ${payload.source}` : \"\"}\n </p>\n {payload.description ? <p className=\"text-xs leading-relaxed text-muted-foreground\">{payload.description}</p> : null}\n </div>\n )\n case \"generic\":\n return (\n <div className=\"space-y-2\">\n <p className=\"text-foreground\">{payload.description}</p>\n {payload.metadata && payload.metadata.length > 0 ? (\n <dl className=\"grid gap-1 text-xs text-muted-foreground\">\n {payload.metadata.map((item) => (\n <div key={`${item.label}-${item.value}`} className=\"flex gap-2\">\n <dt className=\"font-medium text-foreground\">{item.label}</dt>\n <dd>{item.value}</dd>\n </div>\n ))}\n </dl>\n ) : null}\n </div>\n )\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/case-panel-activity-timeline.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDown, ExternalLink } from \"lucide-react\"\nimport { cn } from \"../lib/utils\"\nimport { TONE_CLASSES, type TimelineEventTone } from \"./timeline-activity\"\n\nexport type CasePanelActivityTone = TimelineEventTone\n\nexport type CasePanelActivityActor =\n | { kind: \"system\" }\n | { kind: \"integration\"; name: string; iconUrl?: string }\n | { kind: \"user\"; name: string; avatarUrl?: string; verb?: string }\n\nexport type CasePanelPayloadAction = {\n kind: \"openSignal\"\n key: string\n eventId: string\n}\n\nexport type CasePanelActivityPayload =\n | {\n kind: \"signal\"\n key: string\n summary: string\n detail?: string\n actionLabel?: string\n }\n | {\n kind: \"scoreUpdate\"\n label?: string\n previousScore?: number\n nextScore: number\n reason?: string\n }\n | {\n kind: \"recommendation\"\n recommendation: string\n rationale?: string\n actionLabel?: string\n }\n | {\n kind: \"email\"\n from: string\n to?: string\n subject: string\n preview?: string\n body?: string\n }\n | {\n kind: \"salesforce\"\n objectLabel: string\n recordLabel?: string\n changeSummary: string\n deepLink?: {\n href: string\n label?: string\n }\n }\n | {\n kind: \"deadline\"\n dueLabel: string\n status?: \"upcoming\" | \"due\" | \"overdue\" | \"met\"\n description?: string\n /** 0..1 elapsed toward the deadline; renders a thin progress bar. */\n progress?: number\n }\n | {\n kind: \"operatorNote\"\n note: string\n }\n | {\n kind: \"assignment\"\n assignee: string\n from?: string\n role?: string\n }\n | {\n kind: \"caseOpened\"\n source?: string\n openedBy?: string\n description?: string\n }\n | {\n kind: \"generic\"\n description: string\n metadata?: Array<{ label: string; value: string }>\n }\n\nexport interface CasePanelActivityEvent {\n id: string\n title: string\n timeLabel: string\n tone: CasePanelActivityTone\n actor?: CasePanelActivityActor\n isSystemNoise?: boolean\n payload: CasePanelActivityPayload\n}\n\nexport interface CasePanelActivityTimelineProps {\n events: CasePanelActivityEvent[]\n className?: string\n title?: string\n defaultExpanded?: boolean\n defaultShowSystemEvents?: boolean\n onPayloadAction?: (action: CasePanelPayloadAction) => void\n}\n\nconst NEUTRAL_DOT_CLASSES = \"bg-background border-border/60\"\nconst NEUTRAL_ICON_CLASSES = \"text-muted-foreground\"\n\nconst PAYLOAD_LABELS: Record<CasePanelActivityPayload[\"kind\"], string> = {\n signal: \"Signal\",\n scoreUpdate: \"Score update\",\n recommendation: \"Recommendation\",\n email: \"Email\",\n salesforce: \"Salesforce\",\n deadline: \"Deadline\",\n operatorNote: \"Operator note\",\n assignment: \"Assignment\",\n caseOpened: \"Case opened\",\n generic: \"Update\",\n}\n\nconst STATUS_CLASSES: Record<NonNullable<Extract<CasePanelActivityPayload, { kind: \"deadline\" }>[\"status\"]>, string> = {\n upcoming: \"border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-900/40 dark:bg-blue-950/30 dark:text-blue-300\",\n due: \"border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/40 dark:bg-amber-950/30 dark:text-amber-300\",\n overdue: \"border-red-200 bg-red-50 text-red-700 dark:border-red-900/40 dark:bg-red-950/30 dark:text-red-300\",\n met: \"border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/40 dark:bg-emerald-950/30 dark:text-emerald-300\",\n}\n\nexport function CasePanelActivityTimeline({\n events,\n className,\n title = \"Activity\",\n defaultExpanded = false,\n defaultShowSystemEvents = false,\n onPayloadAction,\n}: CasePanelActivityTimelineProps) {\n const [expanded, setExpanded] = React.useState(defaultExpanded)\n const [showSystemEvents, setShowSystemEvents] = React.useState(defaultShowSystemEvents)\n\n const nonSystemEvents = events.filter((event) => !event.isSystemNoise)\n const visibleEvents = showSystemEvents ? events : nonSystemEvents\n const systemNoiseCount = events.length - nonSystemEvents.length\n const lastActivityText = nonSystemEvents[0]?.timeLabel ?? events[0]?.timeLabel ?? \"No activity yet\"\n\n return (\n <section className={cn(\"rounded-xl border border-border bg-card text-card-foreground shadow-sm\", className)}>\n <button\n type=\"button\"\n className=\"flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition-colors hover:bg-muted/30\"\n aria-expanded={expanded}\n onClick={() => setExpanded((current) => !current)}\n >\n <span className=\"min-w-0\">\n <span className=\"flex items-center gap-2\">\n <span className=\"text-sm font-semibold text-foreground\">{title}</span>\n <span className=\"rounded-full border border-border bg-muted/40 px-2 py-0.5 text-[11px] font-medium text-muted-foreground\">\n {nonSystemEvents.length} {nonSystemEvents.length === 1 ? \"event\" : \"events\"}\n </span>\n </span>\n <span className=\"mt-1 block truncate text-xs text-muted-foreground\">Last activity {lastActivityText}</span>\n </span>\n <ChevronDown className={cn(\"h-4 w-4 shrink-0 text-muted-foreground transition-transform\", expanded && \"rotate-180\")} />\n </button>\n\n {expanded ? (\n <div className=\"border-t border-border px-4 py-4\">\n {systemNoiseCount > 0 ? (\n <label className=\"mb-4 flex items-center justify-between gap-3 rounded-lg border border-border/70 bg-muted/20 px-3 py-2 text-xs text-muted-foreground\">\n <span>Show system events</span>\n <input\n type=\"checkbox\"\n className=\"h-4 w-4 rounded border-border text-primary accent-current\"\n checked={showSystemEvents}\n onChange={(event) => setShowSystemEvents(event.target.checked)}\n />\n </label>\n ) : null}\n\n {visibleEvents.length > 0 ? (\n <div className=\"space-y-0\">\n {visibleEvents.map((event, index) => (\n <CasePanelActivityTimelineItem\n key={event.id}\n event={event}\n isLast={index === visibleEvents.length - 1}\n onPayloadAction={onPayloadAction}\n />\n ))}\n </div>\n ) : (\n <p className=\"rounded-lg border border-dashed border-border px-3 py-6 text-center text-sm text-muted-foreground\">\n No activity to show.\n </p>\n )}\n </div>\n ) : null}\n </section>\n )\n}\n\nfunction CasePanelActivityTimelineItem({\n event,\n isLast,\n onPayloadAction,\n}: {\n event: CasePanelActivityEvent\n isLast: boolean\n onPayloadAction?: (action: CasePanelPayloadAction) => void\n}) {\n const toneStyle = TONE_CLASSES[event.tone]\n const dotClasses = toneStyle ? toneStyle.dot : NEUTRAL_DOT_CLASSES\n const iconClasses = toneStyle ? toneStyle.icon : NEUTRAL_ICON_CLASSES\n\n return (\n <article className=\"group relative flex gap-3.5\" data-testid=\"case-panel-activity-event\">\n {!isLast ? <div className=\"absolute bottom-[-6px] left-[9px] top-5 w-px bg-border/60\" /> : null}\n <div className=\"relative z-10 mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-card\">\n <span\n aria-hidden=\"true\"\n className={cn(\"flex h-4.5 w-4.5 items-center justify-center rounded-full border ring-4 ring-card\", dotClasses, iconClasses)}\n data-testid=\"case-panel-activity-dot\"\n >\n <span className=\"h-1.5 w-1.5 rounded-full bg-current\" />\n </span>\n </div>\n <div className=\"min-w-0 flex-1 pb-5 pt-0.5\">\n <div className=\"flex min-w-0 flex-col gap-1 sm:flex-row sm:items-start sm:justify-between\">\n <h3 className=\"pr-4 text-[13px] font-medium leading-relaxed text-foreground\">{event.title}</h3>\n <time className=\"mt-0.5 shrink-0 whitespace-nowrap text-[11px] text-muted-foreground/70\">{event.timeLabel}</time>\n </div>\n <ActorByline actor={event.actor} timeLabel={event.timeLabel} />\n <PayloadCard event={event} onPayloadAction={onPayloadAction} />\n </div>\n </article>\n )\n}\n\nfunction ActorByline({ actor, timeLabel }: { actor?: CasePanelActivityActor; timeLabel: string }) {\n if (!actor || 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=\"case-panel-activity-byline\">\n {actor.iconUrl ? <img src={actor.iconUrl} alt=\"\" className=\"h-4 w-4 rounded-sm object-cover\" /> : null}\n <span className=\"font-medium text-foreground\">{actor.name}</span>\n <span>synced this update</span>\n <span className=\"text-muted-foreground/40\">·</span>\n <span>{timeLabel}</span>\n </div>\n )\n }\n\n const verb = actor.verb ?? \"updated this case\"\n const initial = 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=\"case-panel-activity-byline\">\n {actor.avatarUrl ? (\n <img src={actor.avatarUrl} alt={actor.name} className=\"h-4 w-4 rounded-full object-cover\" />\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 {initial}\n </span>\n )}\n <span className=\"font-medium text-foreground\">{actor.name}</span>\n <span>{verb}</span>\n <span className=\"text-muted-foreground/40\">·</span>\n <span>{timeLabel}</span>\n </div>\n )\n}\n\nfunction PayloadCard({\n event,\n onPayloadAction,\n}: {\n event: CasePanelActivityEvent\n onPayloadAction?: (action: CasePanelPayloadAction) => void\n}) {\n const payload = event.payload\n\n return (\n <div className=\"mt-2 rounded-lg border border-border/80 bg-muted/20 px-3 py-2.5 text-sm\" data-testid={`case-panel-payload-${payload.kind}`}>\n <div className=\"mb-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground\">\n {PAYLOAD_LABELS[payload.kind]}\n </div>\n {renderPayloadContent(payload, event.id, onPayloadAction)}\n </div>\n )\n}\n\nfunction renderPayloadContent(\n payload: CasePanelActivityPayload,\n eventId: string,\n onPayloadAction?: (action: CasePanelPayloadAction) => void\n) {\n switch (payload.kind) {\n case \"signal\":\n return (\n <div className=\"space-y-2\">\n <p className=\"text-foreground\">{payload.summary}</p>\n {payload.detail ? <p className=\"text-xs leading-relaxed text-muted-foreground\">{payload.detail}</p> : null}\n {onPayloadAction && payload.actionLabel ? (\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n onClick={() => onPayloadAction({ kind: \"openSignal\", key: payload.key, eventId })}\n >\n {payload.actionLabel}\n </button>\n ) : null}\n </div>\n )\n case \"scoreUpdate\":\n return (\n <div className=\"space-y-1\">\n <p className=\"text-foreground\">\n {payload.label ?? \"Score\"}: {payload.previousScore !== undefined ? `${payload.previousScore} → ` : \"\"}{payload.nextScore}\n </p>\n {payload.reason ? <p className=\"text-xs leading-relaxed text-muted-foreground\">{payload.reason}</p> : null}\n </div>\n )\n case \"recommendation\":\n return (\n <div className=\"space-y-1\">\n <p className=\"font-medium text-foreground\">{payload.recommendation}</p>\n {payload.rationale ? <p className=\"text-xs leading-relaxed text-muted-foreground\">{payload.rationale}</p> : null}\n {payload.actionLabel ? <p className=\"text-xs font-medium text-muted-foreground\">{payload.actionLabel}</p> : null}\n </div>\n )\n case \"email\":\n return (\n <div className=\"space-y-1\">\n <p className=\"font-medium text-foreground\">{payload.subject}</p>\n <p className=\"text-xs text-muted-foreground\">\n From {payload.from}{payload.to ? ` to ${payload.to}` : \"\"}\n </p>\n {payload.preview ? <p className=\"text-sm text-foreground/90\">{payload.preview}</p> : null}\n {payload.body ? <p className=\"whitespace-pre-line text-xs leading-relaxed text-muted-foreground\">{payload.body}</p> : null}\n </div>\n )\n case \"salesforce\":\n return (\n <div className=\"space-y-2\">\n <p className=\"text-foreground\">\n <span className=\"font-medium\">{payload.objectLabel}</span>\n {payload.recordLabel ? <span className=\"text-muted-foreground\"> · {payload.recordLabel}</span> : null}\n </p>\n <p className=\"text-xs leading-relaxed text-muted-foreground\">{payload.changeSummary}</p>\n {payload.deepLink ? (\n <a\n href={payload.deepLink.href}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className=\"inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground\"\n >\n {payload.deepLink.label ?? \"Open in Salesforce\"}\n <ExternalLink className=\"h-3 w-3\" />\n </a>\n ) : null}\n </div>\n )\n case \"deadline\":\n return (\n <div className=\"space-y-2\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"font-medium text-foreground\">{payload.dueLabel}</span>\n {payload.status ? (\n <span className={cn(\"rounded-full border px-2 py-0.5 text-[11px] font-medium\", STATUS_CLASSES[payload.status])}>\n {payload.status}\n </span>\n ) : null}\n </div>\n {payload.description ? <p className=\"text-xs leading-relaxed text-muted-foreground\">{payload.description}</p> : null}\n {typeof payload.progress === \"number\" ? (\n <div\n data-slot=\"deadline-progress\"\n role=\"progressbar\"\n aria-valuenow={Math.round(Math.min(1, Math.max(0, payload.progress)) * 100)}\n aria-valuemin={0}\n aria-valuemax={100}\n className=\"bg-muted h-1.5 w-full overflow-hidden rounded-full\"\n >\n <div\n className={cn(\n \"h-full rounded-full\",\n payload.status === \"overdue\" ? \"bg-red-500\" : payload.status === \"due\" ? \"bg-amber-500\" : \"bg-foreground/70\"\n )}\n style={{ width: `${Math.min(1, Math.max(0, payload.progress)) * 100}%` }}\n />\n </div>\n ) : null}\n </div>\n )\n case \"operatorNote\":\n return <p className=\"whitespace-pre-line text-sm leading-relaxed text-foreground\">{payload.note}</p>\n case \"assignment\":\n return (\n <p className=\"text-foreground\">\n Assigned to <span className=\"font-medium\">{payload.assignee}</span>\n {payload.role ? <span className=\"text-muted-foreground\"> as {payload.role}</span> : null}\n {payload.from ? <span className=\"text-muted-foreground\"> from {payload.from}</span> : null}\n </p>\n )\n case \"caseOpened\":\n return (\n <div className=\"space-y-1\">\n <p className=\"text-foreground\">\n Case opened{payload.openedBy ? ` by ${payload.openedBy}` : \"\"}{payload.source ? ` from ${payload.source}` : \"\"}\n </p>\n {payload.description ? <p className=\"text-xs leading-relaxed text-muted-foreground\">{payload.description}</p> : null}\n </div>\n )\n case \"generic\":\n return (\n <div className=\"space-y-2\">\n <p className=\"text-foreground\">{payload.description}</p>\n {payload.metadata && payload.metadata.length > 0 ? (\n <dl className=\"grid gap-1 text-xs text-muted-foreground\">\n {payload.metadata.map((item) => (\n <div key={`${item.label}-${item.value}`} className=\"flex gap-2\">\n <dt className=\"font-medium text-foreground\">{item.label}</dt>\n <dd>{item.value}</dd>\n </div>\n ))}\n </dl>\n ) : null}\n </div>\n )\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* comment-composer.tsx — an internal-note composer for the case activity
|
|
5
|
+
* timeline. Posting a comment prepends an `operatorNote` event to the log
|
|
6
|
+
* (wired by the consumer). Collapses to a single line; expands on focus or
|
|
7
|
+
* when it has text. ⌘↵ / Ctrl↵ posts.
|
|
8
|
+
*
|
|
9
|
+
* Presentational: `onPost` does the work (the consumer persists the note and
|
|
10
|
+
* adds it to the timeline). Reuses Avatar / Button / Textarea primitives.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface CommentComposerProps {
|
|
14
|
+
/** Called with the trimmed note text when the operator posts. */
|
|
15
|
+
onPost: (text: string) => void;
|
|
16
|
+
/** Current operator (for the avatar). */
|
|
17
|
+
author?: {
|
|
18
|
+
name?: string;
|
|
19
|
+
email?: string;
|
|
20
|
+
avatarUrl?: string | null;
|
|
21
|
+
};
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
/** Hint shown in the footer; defaults to the internal-note reassurance. */
|
|
24
|
+
hint?: string;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
declare function CommentComposer({ onPost, author, placeholder, hint, className, }: CommentComposerProps): React.JSX.Element;
|
|
28
|
+
|
|
29
|
+
export { CommentComposer, type CommentComposerProps };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
"use client";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { Lock } from "lucide-react";
|
|
7
|
+
import { cn } from "../lib/utils.js";
|
|
8
|
+
import { getInitials } from "../lib/user-display.js";
|
|
9
|
+
import { Avatar, AvatarFallback, AvatarImage } from "./avatar.js";
|
|
10
|
+
import { Button } from "./button.js";
|
|
11
|
+
import { Textarea } from "./textarea.js";
|
|
12
|
+
function CommentComposer({
|
|
13
|
+
onPost,
|
|
14
|
+
author,
|
|
15
|
+
placeholder = "Add a comment or internal note\u2026",
|
|
16
|
+
hint = "Internal note: only your team sees this",
|
|
17
|
+
className
|
|
18
|
+
}) {
|
|
19
|
+
var _a;
|
|
20
|
+
const [text, setText] = React.useState("");
|
|
21
|
+
const [focused, setFocused] = React.useState(false);
|
|
22
|
+
const open = focused || text.length > 0;
|
|
23
|
+
const canPost = text.trim().length > 0;
|
|
24
|
+
const post = () => {
|
|
25
|
+
const value = text.trim();
|
|
26
|
+
if (!value) return;
|
|
27
|
+
onPost(value);
|
|
28
|
+
setText("");
|
|
29
|
+
setFocused(false);
|
|
30
|
+
};
|
|
31
|
+
return /* @__PURE__ */ jsxs(
|
|
32
|
+
"div",
|
|
33
|
+
{
|
|
34
|
+
"data-slot": "comment-composer",
|
|
35
|
+
"data-open": open ? "true" : void 0,
|
|
36
|
+
className: cn(
|
|
37
|
+
"border-border bg-background flex items-start gap-2 rounded-lg border p-2 transition-colors",
|
|
38
|
+
open && "ring-ring/30 ring-2",
|
|
39
|
+
className
|
|
40
|
+
),
|
|
41
|
+
children: [
|
|
42
|
+
/* @__PURE__ */ jsxs(Avatar, { size: "sm", className: "mt-0.5", children: [
|
|
43
|
+
(author == null ? void 0 : author.avatarUrl) ? /* @__PURE__ */ jsx(AvatarImage, { src: author.avatarUrl, alt: (_a = author.name) != null ? _a : "You" }) : null,
|
|
44
|
+
/* @__PURE__ */ jsx(AvatarFallback, { className: "bg-muted text-muted-foreground text-[10px] font-medium uppercase", children: getInitials({ name: author == null ? void 0 : author.name, email: author == null ? void 0 : author.email }) })
|
|
45
|
+
] }),
|
|
46
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
47
|
+
/* @__PURE__ */ jsx(
|
|
48
|
+
Textarea,
|
|
49
|
+
{
|
|
50
|
+
"data-slot": "comment-composer-input",
|
|
51
|
+
value: text,
|
|
52
|
+
onChange: (e) => setText(e.target.value),
|
|
53
|
+
onFocus: () => setFocused(true),
|
|
54
|
+
placeholder,
|
|
55
|
+
rows: open ? 3 : 1,
|
|
56
|
+
className: cn(
|
|
57
|
+
"resize-none border-0 bg-transparent p-1 text-sm shadow-none focus-visible:ring-0",
|
|
58
|
+
!open && "min-h-0"
|
|
59
|
+
),
|
|
60
|
+
onKeyDown: (e) => {
|
|
61
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
post();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
),
|
|
68
|
+
open ? /* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center justify-between gap-2", children: [
|
|
69
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground inline-flex items-center gap-1 text-[11px]", children: [
|
|
70
|
+
/* @__PURE__ */ jsx(Lock, { size: 12 }),
|
|
71
|
+
" ",
|
|
72
|
+
hint
|
|
73
|
+
] }),
|
|
74
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
75
|
+
/* @__PURE__ */ jsx(
|
|
76
|
+
Button,
|
|
77
|
+
{
|
|
78
|
+
type: "button",
|
|
79
|
+
variant: "ghost",
|
|
80
|
+
size: "sm",
|
|
81
|
+
onClick: () => {
|
|
82
|
+
setText("");
|
|
83
|
+
setFocused(false);
|
|
84
|
+
},
|
|
85
|
+
children: "Cancel"
|
|
86
|
+
}
|
|
87
|
+
),
|
|
88
|
+
/* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", disabled: !canPost, onClick: post, children: [
|
|
89
|
+
"Comment",
|
|
90
|
+
/* @__PURE__ */ jsx("kbd", { className: "bg-primary-foreground/15 ml-1 rounded px-1 text-[10px]", children: "\u2318\u21B5" })
|
|
91
|
+
] })
|
|
92
|
+
] })
|
|
93
|
+
] }) : null
|
|
94
|
+
] })
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
CommentComposer
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=comment-composer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/comment-composer.tsx"],"sourcesContent":["\"use client\"\n\n/**\n * comment-composer.tsx — an internal-note composer for the case activity\n * timeline. Posting a comment prepends an `operatorNote` event to the log\n * (wired by the consumer). Collapses to a single line; expands on focus or\n * when it has text. ⌘↵ / Ctrl↵ posts.\n *\n * Presentational: `onPost` does the work (the consumer persists the note and\n * adds it to the timeline). Reuses Avatar / Button / Textarea primitives.\n */\n\nimport * as React from \"react\"\nimport { Lock } from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { getInitials } from \"../lib/user-display\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"./avatar\"\nimport { Button } from \"./button\"\nimport { Textarea } from \"./textarea\"\n\nexport interface CommentComposerProps {\n /** Called with the trimmed note text when the operator posts. */\n onPost: (text: string) => void\n /** Current operator (for the avatar). */\n author?: { name?: string; email?: string; avatarUrl?: string | null }\n placeholder?: string\n /** Hint shown in the footer; defaults to the internal-note reassurance. */\n hint?: string\n className?: string\n}\n\nfunction CommentComposer({\n onPost,\n author,\n placeholder = \"Add a comment or internal note…\",\n hint = \"Internal note: only your team sees this\",\n className,\n}: CommentComposerProps) {\n const [text, setText] = React.useState(\"\")\n const [focused, setFocused] = React.useState(false)\n const open = focused || text.length > 0\n const canPost = text.trim().length > 0\n\n const post = () => {\n const value = text.trim()\n if (!value) return\n onPost(value)\n setText(\"\")\n setFocused(false)\n }\n\n return (\n <div\n data-slot=\"comment-composer\"\n data-open={open ? \"true\" : undefined}\n className={cn(\n \"border-border bg-background flex items-start gap-2 rounded-lg border p-2 transition-colors\",\n open && \"ring-ring/30 ring-2\",\n className\n )}\n >\n <Avatar size=\"sm\" className=\"mt-0.5\">\n {author?.avatarUrl ? <AvatarImage src={author.avatarUrl} alt={author.name ?? \"You\"} /> : null}\n <AvatarFallback className=\"bg-muted text-muted-foreground text-[10px] font-medium uppercase\">\n {getInitials({ name: author?.name, email: author?.email })}\n </AvatarFallback>\n </Avatar>\n\n <div className=\"min-w-0 flex-1\">\n <Textarea\n data-slot=\"comment-composer-input\"\n value={text}\n onChange={(e) => setText(e.target.value)}\n onFocus={() => setFocused(true)}\n placeholder={placeholder}\n rows={open ? 3 : 1}\n className={cn(\n \"resize-none border-0 bg-transparent p-1 text-sm shadow-none focus-visible:ring-0\",\n !open && \"min-h-0\"\n )}\n onKeyDown={(e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === \"Enter\") {\n e.preventDefault()\n post()\n }\n }}\n />\n\n {open ? (\n <div className=\"mt-1 flex items-center justify-between gap-2\">\n <span className=\"text-muted-foreground inline-flex items-center gap-1 text-[11px]\">\n <Lock size={12} /> {hint}\n </span>\n <span className=\"flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => {\n setText(\"\")\n setFocused(false)\n }}\n >\n Cancel\n </Button>\n <Button type=\"button\" size=\"sm\" disabled={!canPost} onClick={post}>\n Comment\n <kbd className=\"bg-primary-foreground/15 ml-1 rounded px-1 text-[10px]\">⌘↵</kbd>\n </Button>\n </span>\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n\nexport { CommentComposer }\n"],"mappings":";AA8DM,SACuB,KADvB;AAlDN,YAAY,WAAW;AACvB,SAAS,YAAY;AAErB,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,QAAQ,gBAAgB,mBAAmB;AACpD,SAAS,cAAc;AACvB,SAAS,gBAAgB;AAazB,SAAS,gBAAgB;AAAA,EACvB;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,OAAO;AAAA,EACP;AACF,GAAyB;AAtCzB;AAuCE,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,EAAE;AACzC,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,OAAO,WAAW,KAAK,SAAS;AACtC,QAAM,UAAU,KAAK,KAAK,EAAE,SAAS;AAErC,QAAM,OAAO,MAAM;AACjB,UAAM,QAAQ,KAAK,KAAK;AACxB,QAAI,CAAC,MAAO;AACZ,WAAO,KAAK;AACZ,YAAQ,EAAE;AACV,eAAW,KAAK;AAAA,EAClB;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,aAAW,OAAO,SAAS;AAAA,MAC3B,WAAW;AAAA,QACT;AAAA,QACA,QAAQ;AAAA,QACR;AAAA,MACF;AAAA,MAEA;AAAA,6BAAC,UAAO,MAAK,MAAK,WAAU,UACzB;AAAA,4CAAQ,aAAY,oBAAC,eAAY,KAAK,OAAO,WAAW,MAAK,YAAO,SAAP,YAAe,OAAO,IAAK;AAAA,UACzF,oBAAC,kBAAe,WAAU,oEACvB,sBAAY,EAAE,MAAM,iCAAQ,MAAM,OAAO,iCAAQ,MAAM,CAAC,GAC3D;AAAA,WACF;AAAA,QAEA,qBAAC,SAAI,WAAU,kBACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,aAAU;AAAA,cACV,OAAO;AAAA,cACP,UAAU,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK;AAAA,cACvC,SAAS,MAAM,WAAW,IAAI;AAAA,cAC9B;AAAA,cACA,MAAM,OAAO,IAAI;AAAA,cACjB,WAAW;AAAA,gBACT;AAAA,gBACA,CAAC,QAAQ;AAAA,cACX;AAAA,cACA,WAAW,CAAC,MAAM;AAChB,qBAAK,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,SAAS;AACjD,oBAAE,eAAe;AACjB,uBAAK;AAAA,gBACP;AAAA,cACF;AAAA;AAAA,UACF;AAAA,UAEC,OACC,qBAAC,SAAI,WAAU,gDACb;AAAA,iCAAC,UAAK,WAAU,oEACd;AAAA,kCAAC,QAAK,MAAM,IAAI;AAAA,cAAE;AAAA,cAAE;AAAA,eACtB;AAAA,YACA,qBAAC,UAAK,WAAU,2BACd;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAQ;AAAA,kBACR,MAAK;AAAA,kBACL,SAAS,MAAM;AACb,4BAAQ,EAAE;AACV,+BAAW,KAAK;AAAA,kBAClB;AAAA,kBACD;AAAA;AAAA,cAED;AAAA,cACA,qBAAC,UAAO,MAAK,UAAS,MAAK,MAAK,UAAU,CAAC,SAAS,SAAS,MAAM;AAAA;AAAA,gBAEjE,oBAAC,SAAI,WAAU,0DAAyD,0BAAE;AAAA,iBAC5E;AAAA,eACF;AAAA,aACF,IACE;AAAA,WACN;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* conversation-panel.tsx — in-case email-thread reader + reply, for the case
|
|
5
|
+
* panel ("Email response detected" hub).
|
|
6
|
+
*
|
|
7
|
+
* v1 scope (WIT-853 / WIT-802): INLINE thread reading + reply only.
|
|
8
|
+
* - A collapsible hub header with a pulse badge whose color reflects state:
|
|
9
|
+
* responded (a reply needs you) / awaiting (sent, waiting) / viewing (read-only).
|
|
10
|
+
* - A list of thread rows; clicking one opens a Gmail-style reader inline,
|
|
11
|
+
* right under the row, with collapsible messages + quoted-history toggle.
|
|
12
|
+
* - Reply / Reply-all composer with a signature toggle and two send paths:
|
|
13
|
+
* Preview→Send (in-app) and "Open draft in Gmail" (deep link).
|
|
14
|
+
* - A "playbook stopped" banner when the customer's reply halted the sequence,
|
|
15
|
+
* and a read-only notice when the operator is not a thread participant.
|
|
16
|
+
*
|
|
17
|
+
* The bottom-right floating dock / side-by-side compare is intentionally OUT of
|
|
18
|
+
* scope here and tracked separately (WIT-855).
|
|
19
|
+
*
|
|
20
|
+
* Presentational: all data + side effects come from the consumer. Email bodies
|
|
21
|
+
* (`bodyHtml`, `quoted.html`) are sanitized before rendering.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
interface ConvParticipant {
|
|
25
|
+
name: string;
|
|
26
|
+
email: string;
|
|
27
|
+
avatarUrl?: string | null;
|
|
28
|
+
role?: string;
|
|
29
|
+
}
|
|
30
|
+
interface ConvMessage {
|
|
31
|
+
id: string;
|
|
32
|
+
direction: "inbound" | "outbound";
|
|
33
|
+
from: ConvParticipant;
|
|
34
|
+
to: ConvParticipant;
|
|
35
|
+
/** Absolute timestamp label, e.g. "Jun 1, 2026, 9:12 AM". */
|
|
36
|
+
date: string;
|
|
37
|
+
/** Relative label, e.g. "2 days ago". */
|
|
38
|
+
ago?: string;
|
|
39
|
+
receipt?: {
|
|
40
|
+
kind: "new" | "read" | "opened" | "sent";
|
|
41
|
+
label: string;
|
|
42
|
+
};
|
|
43
|
+
/** HTML body (preferred). Sanitized by the component before rendering. */
|
|
44
|
+
bodyHtml?: string;
|
|
45
|
+
/** Plain-text fallback when `bodyHtml` is absent. */
|
|
46
|
+
body?: string;
|
|
47
|
+
/** Quoted prior message, collapsed behind a toggle. Sanitized before rendering. */
|
|
48
|
+
quoted?: {
|
|
49
|
+
attr: string;
|
|
50
|
+
html: string;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
type ConvStatus = "responded" | "awaiting" | "viewing";
|
|
54
|
+
interface ConversationThread {
|
|
55
|
+
threadId: string;
|
|
56
|
+
subject: string;
|
|
57
|
+
status: ConvStatus;
|
|
58
|
+
/** Relative label for the most recent activity. */
|
|
59
|
+
lastWhen?: string;
|
|
60
|
+
contact: ConvParticipant;
|
|
61
|
+
cc?: ConvParticipant[];
|
|
62
|
+
/** Set when this thread's reply halted a playbook (terminal). */
|
|
63
|
+
paused?: {
|
|
64
|
+
playbook: string;
|
|
65
|
+
} | null;
|
|
66
|
+
/** false => operator is not a participant; reply disabled (read-only). */
|
|
67
|
+
canReply?: boolean;
|
|
68
|
+
messages: ConvMessage[];
|
|
69
|
+
/** Prefilled reply draft body. */
|
|
70
|
+
draft?: string;
|
|
71
|
+
/** Signature text appended to replies (plain text). */
|
|
72
|
+
signature?: string;
|
|
73
|
+
}
|
|
74
|
+
interface ConversationReplyPayload {
|
|
75
|
+
threadId: string;
|
|
76
|
+
body: string;
|
|
77
|
+
includeSignature: boolean;
|
|
78
|
+
replyAll: boolean;
|
|
79
|
+
}
|
|
80
|
+
interface ConversationPanelProps {
|
|
81
|
+
threads: ConversationThread[];
|
|
82
|
+
/** Current operator: drives "to me" + the reply avatar. */
|
|
83
|
+
me?: ConvParticipant;
|
|
84
|
+
/** Deployment brand, used in the paused-playbook copy. */
|
|
85
|
+
tenantName?: string;
|
|
86
|
+
onSendReply?: (payload: ConversationReplyPayload) => void | Promise<void>;
|
|
87
|
+
onCreateGmailDraft?: (payload: ConversationReplyPayload) => void;
|
|
88
|
+
onOpenInGmail?: (threadId: string) => void;
|
|
89
|
+
/** Inline-open this thread initially (defaults to the first responded one). */
|
|
90
|
+
defaultOpenThreadId?: string;
|
|
91
|
+
className?: string;
|
|
92
|
+
}
|
|
93
|
+
declare function ConversationPanel({ threads, me, tenantName, onSendReply, onCreateGmailDraft, onOpenInGmail, defaultOpenThreadId, className, }: ConversationPanelProps): React.JSX.Element | null;
|
|
94
|
+
|
|
95
|
+
export { type ConvMessage, type ConvParticipant, type ConvStatus, ConversationPanel, type ConversationPanelProps, type ConversationReplyPayload, type ConversationThread };
|