@handled-ai/design-system 0.20.1 → 0.20.3
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/conversation-panel.d.ts +1 -1
- package/dist/components/conversation-panel.js +282 -15
- package/dist/components/conversation-panel.js.map +1 -1
- package/dist/components/owner-chips.d.ts +3 -4
- package/dist/components/owner-chips.js +77 -41
- package/dist/components/owner-chips.js.map +1 -1
- package/dist/components/timeline-activity.d.ts +4 -2
- package/dist/components/timeline-activity.js +366 -154
- package/dist/components/timeline-activity.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.js +10 -8
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/conversation-panel.test.tsx +276 -0
- package/src/components/__tests__/owner-chips.test.tsx +137 -17
- package/src/components/__tests__/timeline-activity.test.tsx +92 -1
- package/src/components/conversation-panel.tsx +358 -21
- package/src/components/owner-chips.tsx +98 -63
- package/src/components/timeline-activity.tsx +452 -160
- package/src/prototype/__tests__/detail-view-case-panel-v2.test.tsx +6 -8
- package/src/prototype/__tests__/detail-view-timeline-system-events.test.tsx +16 -2
- package/src/prototype/prototype-inbox-view.tsx +14 -15
|
@@ -21,6 +21,8 @@ const EMAIL_HTML_CLASS = cn(
|
|
|
21
21
|
"[&_.gmail_quote]:border-l-2 [&_.gmail_quote]:border-border [&_.gmail_quote]:pl-3 [&_.gmail_quote]:text-muted-foreground [&_.gmail_quote]:text-[13px]"
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
+
export type TimelineActivityVariant = "default" | "case-panel"
|
|
25
|
+
|
|
24
26
|
export type TimelineEventTone =
|
|
25
27
|
| "red"
|
|
26
28
|
| "amber"
|
|
@@ -121,19 +123,79 @@ export const TONE_CLASSES: Record<
|
|
|
121
123
|
const NEUTRAL_DOT_CLASSES = "border-border/60 bg-background"
|
|
122
124
|
const NEUTRAL_ICON_CLASSES = "text-muted-foreground"
|
|
123
125
|
|
|
126
|
+
type TimelineVariantClasses = {
|
|
127
|
+
outerRowGap: string
|
|
128
|
+
connector: string
|
|
129
|
+
dotWrapperSize: string
|
|
130
|
+
dot: string
|
|
131
|
+
contentPadding: string
|
|
132
|
+
titleRowSpacing: string
|
|
133
|
+
title: string
|
|
134
|
+
time: string
|
|
135
|
+
cardContainer: string
|
|
136
|
+
cardHeader: string
|
|
137
|
+
cardBody: string
|
|
138
|
+
cardFooter: string
|
|
139
|
+
collapsedPreview: string
|
|
140
|
+
actionLinkRow: string
|
|
141
|
+
actionLink: string
|
|
142
|
+
nonInteractiveContent: string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const TIMELINE_VARIANT_CLASSES: Record<TimelineActivityVariant, TimelineVariantClasses> = {
|
|
146
|
+
default: {
|
|
147
|
+
outerRowGap: "group relative flex gap-3.5",
|
|
148
|
+
connector: "absolute left-[9px] top-5 bottom-[-6px] w-px bg-border/60",
|
|
149
|
+
dotWrapperSize: "relative z-10 mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-background",
|
|
150
|
+
dot: "flex h-4.5 w-4.5 items-center justify-center rounded-full border ring-4 ring-background",
|
|
151
|
+
contentPadding: "flex-1 pb-5 pt-0.5",
|
|
152
|
+
titleRowSpacing: "flex min-w-0 flex-col gap-1 sm:flex-row sm:items-start sm:justify-between",
|
|
153
|
+
title: "pr-4 text-[13px] leading-relaxed text-foreground",
|
|
154
|
+
time: "mt-0.5 shrink-0 whitespace-nowrap text-[11px] text-muted-foreground/70",
|
|
155
|
+
cardContainer: "overflow-hidden rounded-md border border-border/80 bg-muted/20",
|
|
156
|
+
cardHeader: "px-3 pt-2.5",
|
|
157
|
+
cardBody: "px-3 py-2.5 text-sm",
|
|
158
|
+
cardFooter: "px-3 pb-2.5",
|
|
159
|
+
collapsedPreview: "flex items-center justify-between gap-2 px-3 py-2.5 text-sm text-muted-foreground",
|
|
160
|
+
actionLinkRow: "flex items-center gap-3 px-3 pb-2.5",
|
|
161
|
+
actionLink: "inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground",
|
|
162
|
+
nonInteractiveContent: "pr-2 text-sm leading-relaxed text-muted-foreground",
|
|
163
|
+
},
|
|
164
|
+
"case-panel": {
|
|
165
|
+
outerRowGap: "group relative flex gap-3",
|
|
166
|
+
connector: "absolute left-[7px] top-[18px] bottom-[-4px] w-px bg-border/60",
|
|
167
|
+
dotWrapperSize: "relative z-10 mt-1 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-background",
|
|
168
|
+
dot: "flex h-3.5 w-3.5 items-center justify-center rounded-full border ring-[3px] ring-background",
|
|
169
|
+
contentPadding: "flex-1 pb-4 pt-0.5",
|
|
170
|
+
titleRowSpacing: "flex min-w-0 flex-col gap-0.5 sm:flex-row sm:items-start sm:justify-between",
|
|
171
|
+
title: "pr-3 text-[13px] leading-snug text-foreground",
|
|
172
|
+
time: "mt-0.5 shrink-0 whitespace-nowrap text-[11px] leading-snug text-muted-foreground/70",
|
|
173
|
+
cardContainer: "overflow-hidden rounded-lg border border-border/70 bg-card shadow-sm",
|
|
174
|
+
cardHeader: "border-b border-border/60 bg-background px-3 py-2",
|
|
175
|
+
cardBody: "px-3 py-2.5 text-sm",
|
|
176
|
+
cardFooter: "border-t border-border/60 bg-background/50 px-3 py-1.5",
|
|
177
|
+
collapsedPreview: "flex items-center justify-between gap-2 px-3 py-2 text-sm text-muted-foreground",
|
|
178
|
+
actionLinkRow: "flex items-center justify-end gap-2 px-3 py-1.5",
|
|
179
|
+
actionLink: "inline-flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground/70 transition-colors hover:text-foreground",
|
|
180
|
+
nonInteractiveContent: "pr-2 text-[13px] leading-snug text-muted-foreground",
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
|
|
124
184
|
export interface TimelineActivityProps {
|
|
125
185
|
events: TimelineEvent[]
|
|
126
186
|
className?: string
|
|
187
|
+
variant?: TimelineActivityVariant
|
|
127
188
|
}
|
|
128
189
|
|
|
129
|
-
export function TimelineActivity({ events, className }: TimelineActivityProps) {
|
|
190
|
+
export function TimelineActivity({ events, className, variant = "default" }: TimelineActivityProps) {
|
|
130
191
|
return (
|
|
131
|
-
<div className={cn("space-y-0", className)}>
|
|
192
|
+
<div className={cn("space-y-0", className)} data-variant={variant}>
|
|
132
193
|
{events.map((event, index) => (
|
|
133
194
|
<TimelineItem
|
|
134
195
|
key={event.id}
|
|
135
196
|
event={event}
|
|
136
197
|
isLast={index === events.length - 1}
|
|
198
|
+
variant={variant}
|
|
137
199
|
/>
|
|
138
200
|
))}
|
|
139
201
|
</div>
|
|
@@ -178,34 +240,43 @@ function ActorByline({ actor, time }: { actor: TimelineEventActor; time: string
|
|
|
178
240
|
)
|
|
179
241
|
}
|
|
180
242
|
|
|
181
|
-
function TimelineItem({
|
|
243
|
+
function TimelineItem({
|
|
244
|
+
event,
|
|
245
|
+
isLast,
|
|
246
|
+
variant,
|
|
247
|
+
}: {
|
|
248
|
+
event: TimelineEvent
|
|
249
|
+
isLast: boolean
|
|
250
|
+
variant: TimelineActivityVariant
|
|
251
|
+
}) {
|
|
182
252
|
const [expanded, setExpanded] = React.useState(event.defaultExpanded ?? false)
|
|
183
253
|
const [showAllRecipients, setShowAllRecipients] = React.useState(false)
|
|
184
254
|
const hasContent = !!event.content
|
|
185
255
|
const hasEmail = !!event.email
|
|
256
|
+
const classes = TIMELINE_VARIANT_CLASSES[variant]
|
|
186
257
|
|
|
187
258
|
const toneStyle = event.tone ? TONE_CLASSES[event.tone] : null
|
|
188
259
|
const dotClasses = toneStyle ? toneStyle.dot : NEUTRAL_DOT_CLASSES
|
|
189
260
|
const iconClasses = toneStyle ? toneStyle.icon : NEUTRAL_ICON_CLASSES
|
|
190
261
|
|
|
191
262
|
return (
|
|
192
|
-
<div className=
|
|
263
|
+
<div className={classes.outerRowGap}>
|
|
193
264
|
{!isLast && (
|
|
194
|
-
<div className=
|
|
265
|
+
<div className={classes.connector} />
|
|
195
266
|
)}
|
|
196
267
|
|
|
197
|
-
<div className=
|
|
198
|
-
<div className={cn(
|
|
268
|
+
<div className={classes.dotWrapperSize}>
|
|
269
|
+
<div className={cn(classes.dot, dotClasses, iconClasses)} data-testid="timeline-dot">
|
|
199
270
|
{event.icon}
|
|
200
271
|
</div>
|
|
201
272
|
</div>
|
|
202
273
|
|
|
203
|
-
<div className=
|
|
204
|
-
<div className=
|
|
205
|
-
<div className=
|
|
274
|
+
<div className={classes.contentPadding}>
|
|
275
|
+
<div className={classes.titleRowSpacing}>
|
|
276
|
+
<div className={classes.title}>
|
|
206
277
|
{event.title}
|
|
207
278
|
</div>
|
|
208
|
-
<span className=
|
|
279
|
+
<span className={classes.time}>
|
|
209
280
|
{event.time}
|
|
210
281
|
</span>
|
|
211
282
|
</div>
|
|
@@ -216,158 +287,26 @@ function TimelineItem({ event, isLast }: { event: TimelineEvent; isLast: boolean
|
|
|
216
287
|
<div className="mt-2">
|
|
217
288
|
{event.isInteractive ? (
|
|
218
289
|
hasEmail ? (
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
<div className="space-y-3">
|
|
229
|
-
<div>
|
|
230
|
-
<div className="flex items-center justify-between gap-4">
|
|
231
|
-
<div className="flex min-w-0 items-baseline gap-1.5">
|
|
232
|
-
<span className="font-semibold text-foreground text-[13px] whitespace-nowrap">{event.email.from}</span>
|
|
233
|
-
{event.email.fromEmail && (
|
|
234
|
-
<span className="text-muted-foreground/60 text-xs truncate">{event.email.fromEmail}</span>
|
|
235
|
-
)}
|
|
236
|
-
</div>
|
|
237
|
-
{event.email.date && (
|
|
238
|
-
<span className="shrink-0 text-xs text-muted-foreground/50 whitespace-nowrap">{event.email.date}</span>
|
|
239
|
-
)}
|
|
240
|
-
</div>
|
|
241
|
-
<div className="mt-0.5 flex items-center gap-1 text-xs text-muted-foreground">
|
|
242
|
-
<span className="truncate">
|
|
243
|
-
To {event.email.to}
|
|
244
|
-
{!showAllRecipients && (event.email.cc || event.email.bcc) ? (
|
|
245
|
-
<>, ...</>
|
|
246
|
-
) : null}
|
|
247
|
-
{showAllRecipients && event.email.cc ? (
|
|
248
|
-
<>, {event.email.cc}</>
|
|
249
|
-
) : null}
|
|
250
|
-
{showAllRecipients && event.email.bcc ? (
|
|
251
|
-
<> <span className="text-muted-foreground/40">bcc</span> {event.email.bcc}</>
|
|
252
|
-
) : null}
|
|
253
|
-
</span>
|
|
254
|
-
{(event.email.cc || event.email.bcc) && (
|
|
255
|
-
<button
|
|
256
|
-
type="button"
|
|
257
|
-
onClick={(e) => {
|
|
258
|
-
e.stopPropagation()
|
|
259
|
-
setShowAllRecipients((prev) => !prev)
|
|
260
|
-
}}
|
|
261
|
-
className="shrink-0 text-muted-foreground/40 hover:text-muted-foreground transition-colors"
|
|
262
|
-
>
|
|
263
|
-
<ChevronDown className={cn("h-3 w-3 transition-transform", showAllRecipients && "rotate-180")} />
|
|
264
|
-
</button>
|
|
265
|
-
)}
|
|
266
|
-
</div>
|
|
267
|
-
</div>
|
|
268
|
-
|
|
269
|
-
{event.email.bodyHtml ? (
|
|
270
|
-
// Gmail reading-pane typography; quoted history
|
|
271
|
-
// (blockquote.gmail_quote) is de-emphasized with a left rule.
|
|
272
|
-
<div
|
|
273
|
-
data-slot="timeline-email-html"
|
|
274
|
-
className={EMAIL_HTML_CLASS}
|
|
275
|
-
dangerouslySetInnerHTML={{ __html: sanitizeHtml(event.email.bodyHtml) }}
|
|
276
|
-
/>
|
|
277
|
-
) : (
|
|
278
|
-
<div className="whitespace-pre-line text-sm leading-relaxed text-foreground/90">
|
|
279
|
-
{event.email.body}
|
|
280
|
-
</div>
|
|
281
|
-
)}
|
|
282
|
-
|
|
283
|
-
<button
|
|
284
|
-
onClick={(e) => {
|
|
285
|
-
e.stopPropagation()
|
|
286
|
-
setExpanded(false)
|
|
287
|
-
}}
|
|
288
|
-
className="mt-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground"
|
|
289
|
-
>
|
|
290
|
-
Show less <ChevronUp className="h-3 w-3" />
|
|
291
|
-
</button>
|
|
292
|
-
</div>
|
|
293
|
-
) : (
|
|
294
|
-
<div className="flex items-center justify-between gap-2 text-muted-foreground">
|
|
295
|
-
<span className="line-clamp-1 pr-3 text-[13px]">
|
|
296
|
-
<span className="text-muted-foreground">{event.email?.from}</span>
|
|
297
|
-
<span className="mx-1.5 text-muted-foreground/40">·</span>
|
|
298
|
-
{event.email?.subject ? (
|
|
299
|
-
<>
|
|
300
|
-
<span className="text-muted-foreground">{event.email.subject}</span>
|
|
301
|
-
<span className="mx-1.5 text-muted-foreground/40">·</span>
|
|
302
|
-
</>
|
|
303
|
-
) : null}
|
|
304
|
-
<span className="text-muted-foreground">{event.preview}</span>
|
|
305
|
-
</span>
|
|
306
|
-
<button className="flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground">
|
|
307
|
-
Expand <ChevronDown className="h-3 w-3" />
|
|
308
|
-
</button>
|
|
309
|
-
</div>
|
|
310
|
-
)}
|
|
311
|
-
</div>
|
|
312
|
-
</div>
|
|
290
|
+
<EmailCard
|
|
291
|
+
event={event}
|
|
292
|
+
expanded={expanded}
|
|
293
|
+
setExpanded={setExpanded}
|
|
294
|
+
showAllRecipients={showAllRecipients}
|
|
295
|
+
setShowAllRecipients={setShowAllRecipients}
|
|
296
|
+
variant={variant}
|
|
297
|
+
classes={classes}
|
|
298
|
+
/>
|
|
313
299
|
) : (
|
|
314
|
-
<
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
>
|
|
322
|
-
{expanded ? (
|
|
323
|
-
<div className="space-y-2">
|
|
324
|
-
{event.content}
|
|
325
|
-
<div className="mt-2 flex items-center gap-3">
|
|
326
|
-
{event.source ? (
|
|
327
|
-
event.onSourceClick ? (
|
|
328
|
-
<button
|
|
329
|
-
type="button"
|
|
330
|
-
onClick={(e) => { e.stopPropagation(); event.onSourceClick?.(); }}
|
|
331
|
-
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"
|
|
332
|
-
>
|
|
333
|
-
Open in {event.source.label}
|
|
334
|
-
<ExternalLink className="h-3 w-3" />
|
|
335
|
-
</button>
|
|
336
|
-
) : (
|
|
337
|
-
<a
|
|
338
|
-
href={event.source.url}
|
|
339
|
-
target="_blank"
|
|
340
|
-
rel="noreferrer noopener"
|
|
341
|
-
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"
|
|
342
|
-
>
|
|
343
|
-
Open in {event.source.label}
|
|
344
|
-
<ExternalLink className="h-3 w-3" />
|
|
345
|
-
</a>
|
|
346
|
-
)
|
|
347
|
-
) : null}
|
|
348
|
-
<button
|
|
349
|
-
onClick={(e) => { e.stopPropagation(); setExpanded(false); }}
|
|
350
|
-
className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground"
|
|
351
|
-
>
|
|
352
|
-
Show less <ChevronUp className="h-3 w-3" />
|
|
353
|
-
</button>
|
|
354
|
-
</div>
|
|
355
|
-
</div>
|
|
356
|
-
) : (
|
|
357
|
-
<div className="flex items-center justify-between gap-2 text-muted-foreground">
|
|
358
|
-
<span className="line-clamp-1 pr-3">
|
|
359
|
-
{event.preview ?? event.content}
|
|
360
|
-
</span>
|
|
361
|
-
<button className="flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground">
|
|
362
|
-
Expand <ChevronDown className="h-3 w-3" />
|
|
363
|
-
</button>
|
|
364
|
-
</div>
|
|
365
|
-
)}
|
|
366
|
-
</div>
|
|
367
|
-
</div>
|
|
300
|
+
<ContentCard
|
|
301
|
+
event={event}
|
|
302
|
+
expanded={expanded}
|
|
303
|
+
setExpanded={setExpanded}
|
|
304
|
+
variant={variant}
|
|
305
|
+
classes={classes}
|
|
306
|
+
/>
|
|
368
307
|
)
|
|
369
308
|
) : (
|
|
370
|
-
<div className=
|
|
309
|
+
<div className={classes.nonInteractiveContent}>
|
|
371
310
|
{event.content}
|
|
372
311
|
</div>
|
|
373
312
|
)}
|
|
@@ -377,3 +316,356 @@ function TimelineItem({ event, isLast }: { event: TimelineEvent; isLast: boolean
|
|
|
377
316
|
</div>
|
|
378
317
|
)
|
|
379
318
|
}
|
|
319
|
+
|
|
320
|
+
type TimelineEmail = NonNullable<TimelineEvent["email"]>
|
|
321
|
+
type TimelineSource = NonNullable<TimelineEvent["source"]>
|
|
322
|
+
|
|
323
|
+
function EmailMetadata({
|
|
324
|
+
email,
|
|
325
|
+
showAllRecipients,
|
|
326
|
+
setShowAllRecipients,
|
|
327
|
+
}: {
|
|
328
|
+
email: TimelineEmail
|
|
329
|
+
showAllRecipients: boolean
|
|
330
|
+
setShowAllRecipients: React.Dispatch<React.SetStateAction<boolean>>
|
|
331
|
+
}) {
|
|
332
|
+
return (
|
|
333
|
+
<>
|
|
334
|
+
<div className="flex items-center justify-between gap-4">
|
|
335
|
+
<div className="flex min-w-0 items-baseline gap-1.5">
|
|
336
|
+
<span className="font-semibold text-foreground text-[13px] whitespace-nowrap">{email.from}</span>
|
|
337
|
+
{email.fromEmail && (
|
|
338
|
+
<span className="text-muted-foreground/60 text-xs truncate">{email.fromEmail}</span>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
{email.date && (
|
|
342
|
+
<span className="shrink-0 text-xs text-muted-foreground/50 whitespace-nowrap">{email.date}</span>
|
|
343
|
+
)}
|
|
344
|
+
</div>
|
|
345
|
+
<div className="mt-0.5 flex items-center gap-1 text-xs text-muted-foreground">
|
|
346
|
+
<span className="truncate">
|
|
347
|
+
To {email.to}
|
|
348
|
+
{!showAllRecipients && (email.cc || email.bcc) ? (
|
|
349
|
+
<>, ...</>
|
|
350
|
+
) : null}
|
|
351
|
+
{showAllRecipients && email.cc ? (
|
|
352
|
+
<>, {email.cc}</>
|
|
353
|
+
) : null}
|
|
354
|
+
{showAllRecipients && email.bcc ? (
|
|
355
|
+
<> <span className="text-muted-foreground/40">bcc</span> {email.bcc}</>
|
|
356
|
+
) : null}
|
|
357
|
+
</span>
|
|
358
|
+
{(email.cc || email.bcc) && (
|
|
359
|
+
<button
|
|
360
|
+
type="button"
|
|
361
|
+
onClick={(e) => {
|
|
362
|
+
e.stopPropagation()
|
|
363
|
+
setShowAllRecipients((prev) => !prev)
|
|
364
|
+
}}
|
|
365
|
+
className="shrink-0 text-muted-foreground/40 hover:text-muted-foreground transition-colors"
|
|
366
|
+
>
|
|
367
|
+
<ChevronDown className={cn("h-3 w-3 transition-transform", showAllRecipients && "rotate-180")} />
|
|
368
|
+
</button>
|
|
369
|
+
)}
|
|
370
|
+
</div>
|
|
371
|
+
</>
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function EmailBody({ email }: { email: TimelineEmail }) {
|
|
376
|
+
if (email.bodyHtml) {
|
|
377
|
+
return (
|
|
378
|
+
// Gmail reading-pane typography; quoted history
|
|
379
|
+
// (blockquote.gmail_quote) is de-emphasized with a left rule.
|
|
380
|
+
<div
|
|
381
|
+
data-slot="timeline-email-html"
|
|
382
|
+
className={EMAIL_HTML_CLASS}
|
|
383
|
+
dangerouslySetInnerHTML={{ __html: sanitizeHtml(email.bodyHtml) }}
|
|
384
|
+
/>
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return (
|
|
389
|
+
<div className="whitespace-pre-line text-sm leading-relaxed text-foreground/90">
|
|
390
|
+
{email.body}
|
|
391
|
+
</div>
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function CollapsedEmailPreview({
|
|
396
|
+
email,
|
|
397
|
+
preview,
|
|
398
|
+
className,
|
|
399
|
+
actionClassName,
|
|
400
|
+
onClick,
|
|
401
|
+
}: {
|
|
402
|
+
email?: TimelineEmail
|
|
403
|
+
preview?: React.ReactNode
|
|
404
|
+
className: string
|
|
405
|
+
actionClassName: string
|
|
406
|
+
onClick?: () => void
|
|
407
|
+
}) {
|
|
408
|
+
return (
|
|
409
|
+
<div className={className} onClick={onClick}>
|
|
410
|
+
<span className="line-clamp-1 pr-3 text-[13px]">
|
|
411
|
+
<span className="text-muted-foreground">{email?.from}</span>
|
|
412
|
+
<span className="mx-1.5 text-muted-foreground/40">·</span>
|
|
413
|
+
{email?.subject ? (
|
|
414
|
+
<>
|
|
415
|
+
<span className="text-muted-foreground">{email.subject}</span>
|
|
416
|
+
<span className="mx-1.5 text-muted-foreground/40">·</span>
|
|
417
|
+
</>
|
|
418
|
+
) : null}
|
|
419
|
+
<span className="text-muted-foreground">{preview}</span>
|
|
420
|
+
</span>
|
|
421
|
+
<button type="button" className={actionClassName}>
|
|
422
|
+
Expand <ChevronDown className="h-3 w-3" />
|
|
423
|
+
</button>
|
|
424
|
+
</div>
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function ShowLessButton({
|
|
429
|
+
className,
|
|
430
|
+
onClick,
|
|
431
|
+
type,
|
|
432
|
+
}: {
|
|
433
|
+
className: string
|
|
434
|
+
onClick: React.MouseEventHandler<HTMLButtonElement>
|
|
435
|
+
type?: "button"
|
|
436
|
+
}) {
|
|
437
|
+
return (
|
|
438
|
+
<button type={type} onClick={onClick} className={className}>
|
|
439
|
+
Show less <ChevronUp className="h-3 w-3" />
|
|
440
|
+
</button>
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function SourceAction({
|
|
445
|
+
source,
|
|
446
|
+
onSourceClick,
|
|
447
|
+
className,
|
|
448
|
+
}: {
|
|
449
|
+
source: TimelineSource
|
|
450
|
+
onSourceClick?: () => void
|
|
451
|
+
className: string
|
|
452
|
+
}) {
|
|
453
|
+
if (onSourceClick) {
|
|
454
|
+
return (
|
|
455
|
+
<button
|
|
456
|
+
type="button"
|
|
457
|
+
onClick={(e) => { e.stopPropagation(); onSourceClick(); }}
|
|
458
|
+
className={className}
|
|
459
|
+
>
|
|
460
|
+
Open in {source.label}
|
|
461
|
+
<ExternalLink className="h-3 w-3" />
|
|
462
|
+
</button>
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return (
|
|
467
|
+
<a
|
|
468
|
+
href={source.url}
|
|
469
|
+
target="_blank"
|
|
470
|
+
rel="noreferrer noopener"
|
|
471
|
+
className={className}
|
|
472
|
+
>
|
|
473
|
+
Open in {source.label}
|
|
474
|
+
<ExternalLink className="h-3 w-3" />
|
|
475
|
+
</a>
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function EmailCard({
|
|
480
|
+
event,
|
|
481
|
+
expanded,
|
|
482
|
+
setExpanded,
|
|
483
|
+
showAllRecipients,
|
|
484
|
+
setShowAllRecipients,
|
|
485
|
+
variant,
|
|
486
|
+
classes,
|
|
487
|
+
}: {
|
|
488
|
+
event: TimelineEvent
|
|
489
|
+
expanded: boolean
|
|
490
|
+
setExpanded: React.Dispatch<React.SetStateAction<boolean>>
|
|
491
|
+
showAllRecipients: boolean
|
|
492
|
+
setShowAllRecipients: React.Dispatch<React.SetStateAction<boolean>>
|
|
493
|
+
variant: TimelineActivityVariant
|
|
494
|
+
classes: TimelineVariantClasses
|
|
495
|
+
}) {
|
|
496
|
+
if (variant === "default") {
|
|
497
|
+
return (
|
|
498
|
+
<div className={classes.cardContainer} data-variant={variant}>
|
|
499
|
+
<div
|
|
500
|
+
className={cn(
|
|
501
|
+
"px-3 py-2.5 text-sm",
|
|
502
|
+
!expanded && "cursor-pointer hover:bg-muted/30 transition-colors"
|
|
503
|
+
)}
|
|
504
|
+
onClick={() => !expanded && setExpanded(true)}
|
|
505
|
+
>
|
|
506
|
+
{expanded && event.email ? (
|
|
507
|
+
<div className="space-y-3">
|
|
508
|
+
<div>
|
|
509
|
+
<EmailMetadata
|
|
510
|
+
email={event.email}
|
|
511
|
+
showAllRecipients={showAllRecipients}
|
|
512
|
+
setShowAllRecipients={setShowAllRecipients}
|
|
513
|
+
/>
|
|
514
|
+
</div>
|
|
515
|
+
|
|
516
|
+
<EmailBody email={event.email} />
|
|
517
|
+
|
|
518
|
+
<ShowLessButton
|
|
519
|
+
onClick={(e) => {
|
|
520
|
+
e.stopPropagation()
|
|
521
|
+
setExpanded(false)
|
|
522
|
+
}}
|
|
523
|
+
className="mt-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground"
|
|
524
|
+
/>
|
|
525
|
+
</div>
|
|
526
|
+
) : (
|
|
527
|
+
<CollapsedEmailPreview
|
|
528
|
+
email={event.email}
|
|
529
|
+
preview={event.preview}
|
|
530
|
+
className="flex items-center justify-between gap-2 text-muted-foreground"
|
|
531
|
+
actionClassName="flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground"
|
|
532
|
+
/>
|
|
533
|
+
)}
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return (
|
|
540
|
+
<div className={classes.cardContainer} data-variant={variant}>
|
|
541
|
+
{expanded && event.email ? (
|
|
542
|
+
<>
|
|
543
|
+
<div className={classes.cardHeader} data-slot="timeline-card-header">
|
|
544
|
+
<EmailMetadata
|
|
545
|
+
email={event.email}
|
|
546
|
+
showAllRecipients={showAllRecipients}
|
|
547
|
+
setShowAllRecipients={setShowAllRecipients}
|
|
548
|
+
/>
|
|
549
|
+
</div>
|
|
550
|
+
|
|
551
|
+
<div className={classes.cardBody} data-slot="timeline-card-body">
|
|
552
|
+
<EmailBody email={event.email} />
|
|
553
|
+
</div>
|
|
554
|
+
|
|
555
|
+
<div className={cn(classes.cardFooter, classes.actionLinkRow)} data-slot="timeline-card-footer">
|
|
556
|
+
<ShowLessButton
|
|
557
|
+
type="button"
|
|
558
|
+
onClick={(e) => {
|
|
559
|
+
e.stopPropagation()
|
|
560
|
+
setExpanded(false)
|
|
561
|
+
}}
|
|
562
|
+
className={classes.actionLink}
|
|
563
|
+
/>
|
|
564
|
+
</div>
|
|
565
|
+
</>
|
|
566
|
+
) : (
|
|
567
|
+
<CollapsedEmailPreview
|
|
568
|
+
email={event.email}
|
|
569
|
+
preview={event.preview}
|
|
570
|
+
className={cn(classes.collapsedPreview, "cursor-pointer hover:bg-muted/30 transition-colors")}
|
|
571
|
+
actionClassName={cn(classes.actionLink, "shrink-0")}
|
|
572
|
+
onClick={() => setExpanded(true)}
|
|
573
|
+
/>
|
|
574
|
+
)}
|
|
575
|
+
</div>
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function ContentCard({
|
|
580
|
+
event,
|
|
581
|
+
expanded,
|
|
582
|
+
setExpanded,
|
|
583
|
+
variant,
|
|
584
|
+
classes,
|
|
585
|
+
}: {
|
|
586
|
+
event: TimelineEvent
|
|
587
|
+
expanded: boolean
|
|
588
|
+
setExpanded: React.Dispatch<React.SetStateAction<boolean>>
|
|
589
|
+
variant: TimelineActivityVariant
|
|
590
|
+
classes: TimelineVariantClasses
|
|
591
|
+
}) {
|
|
592
|
+
if (variant === "default") {
|
|
593
|
+
return (
|
|
594
|
+
<div className={classes.cardContainer} data-variant={variant}>
|
|
595
|
+
<div
|
|
596
|
+
className={cn(
|
|
597
|
+
"px-3 py-2.5 text-sm",
|
|
598
|
+
!expanded && "cursor-pointer hover:bg-muted/30 transition-colors"
|
|
599
|
+
)}
|
|
600
|
+
onClick={() => !expanded && setExpanded(true)}
|
|
601
|
+
>
|
|
602
|
+
{expanded ? (
|
|
603
|
+
<div className="space-y-2">
|
|
604
|
+
{event.content}
|
|
605
|
+
<div className="mt-2 flex items-center gap-3">
|
|
606
|
+
{event.source ? (
|
|
607
|
+
<SourceAction
|
|
608
|
+
source={event.source}
|
|
609
|
+
onSourceClick={event.onSourceClick}
|
|
610
|
+
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"
|
|
611
|
+
/>
|
|
612
|
+
) : null}
|
|
613
|
+
<ShowLessButton
|
|
614
|
+
onClick={(e) => { e.stopPropagation(); setExpanded(false); }}
|
|
615
|
+
className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground"
|
|
616
|
+
/>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
) : (
|
|
620
|
+
<div className="flex items-center justify-between gap-2 text-muted-foreground">
|
|
621
|
+
<span className="line-clamp-1 pr-3">
|
|
622
|
+
{event.preview ?? event.content}
|
|
623
|
+
</span>
|
|
624
|
+
<button className="flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground">
|
|
625
|
+
Expand <ChevronDown className="h-3 w-3" />
|
|
626
|
+
</button>
|
|
627
|
+
</div>
|
|
628
|
+
)}
|
|
629
|
+
</div>
|
|
630
|
+
</div>
|
|
631
|
+
)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return (
|
|
635
|
+
<div className={classes.cardContainer} data-variant={variant}>
|
|
636
|
+
{expanded ? (
|
|
637
|
+
<>
|
|
638
|
+
<div className={classes.cardBody} data-slot="timeline-card-body">
|
|
639
|
+
{event.content}
|
|
640
|
+
</div>
|
|
641
|
+
<div className={cn(classes.cardFooter, classes.actionLinkRow, event.source ? "justify-between" : "justify-end")} data-slot="timeline-card-footer">
|
|
642
|
+
{event.source ? (
|
|
643
|
+
<SourceAction
|
|
644
|
+
source={event.source}
|
|
645
|
+
onSourceClick={event.onSourceClick}
|
|
646
|
+
className={classes.actionLink}
|
|
647
|
+
/>
|
|
648
|
+
) : null}
|
|
649
|
+
<ShowLessButton
|
|
650
|
+
type="button"
|
|
651
|
+
onClick={(e) => { e.stopPropagation(); setExpanded(false); }}
|
|
652
|
+
className={classes.actionLink}
|
|
653
|
+
/>
|
|
654
|
+
</div>
|
|
655
|
+
</>
|
|
656
|
+
) : (
|
|
657
|
+
<div
|
|
658
|
+
className={cn(classes.collapsedPreview, "cursor-pointer hover:bg-muted/30 transition-colors")}
|
|
659
|
+
onClick={() => setExpanded(true)}
|
|
660
|
+
>
|
|
661
|
+
<span className="line-clamp-1 pr-3">
|
|
662
|
+
{event.preview ?? event.content}
|
|
663
|
+
</span>
|
|
664
|
+
<button type="button" className={cn(classes.actionLink, "shrink-0")}>
|
|
665
|
+
Expand <ChevronDown className="h-3 w-3" />
|
|
666
|
+
</button>
|
|
667
|
+
</div>
|
|
668
|
+
)}
|
|
669
|
+
</div>
|
|
670
|
+
)
|
|
671
|
+
}
|