@handled-ai/design-system 0.18.3 → 0.18.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/charts/chart.d.ts +1 -1
- package/dist/components/feedback-primitives.d.ts +41 -2
- package/dist/components/feedback-primitives.js +241 -6
- package/dist/components/feedback-primitives.js.map +1 -1
- package/dist/components/score-why-chips.d.ts +1 -1
- package/dist/components/score-why-chips.js +26 -5
- package/dist/components/score-why-chips.js.map +1 -1
- package/dist/components/signal-priority-popover.d.ts +1 -1
- package/dist/components/signal-priority-popover.js +32 -6
- package/dist/components/signal-priority-popover.js.map +1 -1
- package/dist/components/timeline-activity.d.ts +1 -16
- package/dist/components/timeline-activity.js +1 -69
- package/dist/components/timeline-activity.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- 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 +2 -12
- package/dist/prototype/prototype-inbox-view.js +37 -102
- 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-DQ_VuHac.d.ts → signal-priority-popover-DWaAMhPI.d.ts} +26 -2
- package/package.json +3 -1
- package/src/components/__tests__/wit-636-feedback-states.test.tsx +546 -0
- package/src/components/feedback-primitives.tsx +333 -26
- package/src/components/score-why-chips.tsx +28 -2
- package/src/components/signal-priority-popover.tsx +44 -4
- package/src/components/timeline-activity.tsx +1 -112
- package/src/index.ts +2 -2
- package/src/prototype/__tests__/detail-view-attention.test.tsx +2 -2
- package/src/prototype/prototype-config.ts +11 -1
- package/src/prototype/prototype-inbox-view.tsx +33 -131
- package/src/components/__tests__/timeline-activity.test.tsx +0 -137
- package/src/prototype/__tests__/detail-view-timeline-system-events.test.tsx +0 -322
|
@@ -4,24 +4,6 @@ import * as React from "react"
|
|
|
4
4
|
import { cn } from "../lib/utils"
|
|
5
5
|
import { ChevronDown, ChevronUp, ExternalLink } from "lucide-react"
|
|
6
6
|
|
|
7
|
-
export type TimelineEventTone =
|
|
8
|
-
| "red"
|
|
9
|
-
| "amber"
|
|
10
|
-
| "emerald"
|
|
11
|
-
| "violet"
|
|
12
|
-
| "blue"
|
|
13
|
-
| "slate"
|
|
14
|
-
| "salesforce"
|
|
15
|
-
| "gmail"
|
|
16
|
-
|
|
17
|
-
export interface TimelineEventActor {
|
|
18
|
-
kind: "user" | "integration" | "system"
|
|
19
|
-
name?: string
|
|
20
|
-
initials?: string
|
|
21
|
-
avatarUrl?: string
|
|
22
|
-
verb?: string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
7
|
export interface TimelineEvent {
|
|
26
8
|
id: string
|
|
27
9
|
icon: React.ReactNode
|
|
@@ -46,57 +28,8 @@ export interface TimelineEvent {
|
|
|
46
28
|
defaultExpanded?: boolean
|
|
47
29
|
isInteractive?: boolean
|
|
48
30
|
onSourceClick?: () => void
|
|
49
|
-
tone?: TimelineEventTone
|
|
50
|
-
actor?: TimelineEventActor
|
|
51
|
-
isSystemNoise?: boolean
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
// Tone class map — every class is a complete static string literal so
|
|
56
|
-
// Tailwind's JIT scanner can detect them. NO interpolation.
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
|
|
59
|
-
export const TONE_CLASSES: Record<
|
|
60
|
-
TimelineEventTone,
|
|
61
|
-
{ dot: string; icon: string }
|
|
62
|
-
> = {
|
|
63
|
-
red: {
|
|
64
|
-
dot: "bg-red-50 border-red-200 dark:bg-red-950/30 dark:border-red-900/40",
|
|
65
|
-
icon: "text-red-600 dark:text-red-300",
|
|
66
|
-
},
|
|
67
|
-
amber: {
|
|
68
|
-
dot: "bg-amber-50 border-amber-200 dark:bg-amber-950/30 dark:border-amber-900/40",
|
|
69
|
-
icon: "text-amber-600 dark:text-amber-300",
|
|
70
|
-
},
|
|
71
|
-
emerald: {
|
|
72
|
-
dot: "bg-emerald-50 border-emerald-200 dark:bg-emerald-950/30 dark:border-emerald-900/40",
|
|
73
|
-
icon: "text-emerald-600 dark:text-emerald-300",
|
|
74
|
-
},
|
|
75
|
-
violet: {
|
|
76
|
-
dot: "bg-violet-50 border-violet-200 dark:bg-violet-950/30 dark:border-violet-900/40",
|
|
77
|
-
icon: "text-violet-600 dark:text-violet-300",
|
|
78
|
-
},
|
|
79
|
-
blue: {
|
|
80
|
-
dot: "bg-blue-50 border-blue-200 dark:bg-blue-950/30 dark:border-blue-900/40",
|
|
81
|
-
icon: "text-blue-600 dark:text-blue-300",
|
|
82
|
-
},
|
|
83
|
-
slate: {
|
|
84
|
-
dot: "bg-slate-100 border-slate-200 dark:bg-slate-800/50 dark:border-slate-700",
|
|
85
|
-
icon: "text-slate-500 dark:text-slate-300",
|
|
86
|
-
},
|
|
87
|
-
salesforce: {
|
|
88
|
-
dot: "bg-white border-[#00A1E0]/25 dark:bg-background dark:border-[#00A1E0]/25",
|
|
89
|
-
icon: "text-[#00A1E0]",
|
|
90
|
-
},
|
|
91
|
-
gmail: {
|
|
92
|
-
dot: "bg-white border-red-200 dark:bg-background dark:border-red-900/40",
|
|
93
|
-
icon: "text-red-500 dark:text-red-300",
|
|
94
|
-
},
|
|
95
31
|
}
|
|
96
32
|
|
|
97
|
-
const NEUTRAL_DOT_CLASSES = "border-border/60 bg-background"
|
|
98
|
-
const NEUTRAL_ICON_CLASSES = "text-muted-foreground"
|
|
99
|
-
|
|
100
33
|
export interface TimelineActivityProps {
|
|
101
34
|
events: TimelineEvent[]
|
|
102
35
|
className?: string
|
|
@@ -116,54 +49,12 @@ export function TimelineActivity({ events, className }: TimelineActivityProps) {
|
|
|
116
49
|
)
|
|
117
50
|
}
|
|
118
51
|
|
|
119
|
-
function ActorByline({ actor, time }: { actor: TimelineEventActor; time: string }) {
|
|
120
|
-
if (actor.kind === "system") return null
|
|
121
|
-
|
|
122
|
-
if (actor.kind === "integration") {
|
|
123
|
-
return (
|
|
124
|
-
<div className="mt-1 flex items-center gap-1.5 text-xs text-muted-foreground" data-testid="actor-byline">
|
|
125
|
-
<span>Integration</span>
|
|
126
|
-
<span className="text-muted-foreground/40">·</span>
|
|
127
|
-
<span>{time}</span>
|
|
128
|
-
</div>
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// actor.kind === "user"
|
|
133
|
-
const verb = actor.verb ?? "performed this action"
|
|
134
|
-
const displayInitials = actor.initials ?? (actor.name ? actor.name.charAt(0).toUpperCase() : "?")
|
|
135
|
-
|
|
136
|
-
return (
|
|
137
|
-
<div className="mt-1 flex items-center gap-1.5 text-xs text-muted-foreground" data-testid="actor-byline">
|
|
138
|
-
{actor.avatarUrl ? (
|
|
139
|
-
<img
|
|
140
|
-
src={actor.avatarUrl}
|
|
141
|
-
alt={actor.name ?? "User"}
|
|
142
|
-
className="h-4 w-4 rounded-full object-cover"
|
|
143
|
-
/>
|
|
144
|
-
) : (
|
|
145
|
-
<span className="flex h-4 w-4 items-center justify-center rounded-full bg-muted-foreground/10 text-[8px] font-semibold text-muted-foreground">
|
|
146
|
-
{displayInitials}
|
|
147
|
-
</span>
|
|
148
|
-
)}
|
|
149
|
-
<span className="text-foreground font-medium">{actor.name}</span>
|
|
150
|
-
<span>{verb}</span>
|
|
151
|
-
<span className="text-muted-foreground/40">·</span>
|
|
152
|
-
<span>{time}</span>
|
|
153
|
-
</div>
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
52
|
function TimelineItem({ event, isLast }: { event: TimelineEvent; isLast: boolean }) {
|
|
158
53
|
const [expanded, setExpanded] = React.useState(event.defaultExpanded ?? false)
|
|
159
54
|
const [showAllRecipients, setShowAllRecipients] = React.useState(false)
|
|
160
55
|
const hasContent = !!event.content
|
|
161
56
|
const hasEmail = !!event.email
|
|
162
57
|
|
|
163
|
-
const toneStyle = event.tone ? TONE_CLASSES[event.tone] : null
|
|
164
|
-
const dotClasses = toneStyle ? toneStyle.dot : NEUTRAL_DOT_CLASSES
|
|
165
|
-
const iconClasses = toneStyle ? toneStyle.icon : NEUTRAL_ICON_CLASSES
|
|
166
|
-
|
|
167
58
|
return (
|
|
168
59
|
<div className="group relative flex gap-3.5">
|
|
169
60
|
{!isLast && (
|
|
@@ -171,7 +62,7 @@ function TimelineItem({ event, isLast }: { event: TimelineEvent; isLast: boolean
|
|
|
171
62
|
)}
|
|
172
63
|
|
|
173
64
|
<div className="relative z-10 mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-background">
|
|
174
|
-
<div className=
|
|
65
|
+
<div className="flex h-4.5 w-4.5 items-center justify-center rounded-full border border-border/60 bg-background text-muted-foreground ring-4 ring-background">
|
|
175
66
|
{event.icon}
|
|
176
67
|
</div>
|
|
177
68
|
</div>
|
|
@@ -186,8 +77,6 @@ function TimelineItem({ event, isLast }: { event: TimelineEvent; isLast: boolean
|
|
|
186
77
|
</span>
|
|
187
78
|
</div>
|
|
188
79
|
|
|
189
|
-
{event.actor && <ActorByline actor={event.actor} time={event.time} />}
|
|
190
|
-
|
|
191
80
|
{(hasContent || hasEmail) && (
|
|
192
81
|
<div className="mt-2">
|
|
193
82
|
{event.isInteractive ? (
|
package/src/index.ts
CHANGED
|
@@ -37,8 +37,8 @@ export * from "./components/dialog"
|
|
|
37
37
|
export * from "./components/dropdown-menu"
|
|
38
38
|
export * from "./components/empty-state"
|
|
39
39
|
export * from "./components/entity-panel"
|
|
40
|
-
export { FeedbackFooter, FeedbackChipGroup, FeedbackInput, FeedbackActions } from "./components/feedback-primitives"
|
|
41
|
-
export type { FeedbackFooterProps, FeedbackChipTree, FeedbackChipGroupProps, FeedbackInputProps, FeedbackActionsProps, FeedbackSubmitData } from "./components/feedback-primitives"
|
|
40
|
+
export { FeedbackFooter, FeedbackChipGroup, FeedbackInput, FeedbackActions, InlineFeedbackControl } from "./components/feedback-primitives"
|
|
41
|
+
export type { FeedbackFooterProps, FeedbackChipTree, FeedbackChipGroupProps, FeedbackInputProps, FeedbackActionsProps, FeedbackSubmitData, PersistedFeedbackData, InlineFeedbackControlProps } from "./components/feedback-primitives"
|
|
42
42
|
export { SignalPriorityPopover } from "./components/signal-priority-popover"
|
|
43
43
|
export type { SignalPriorityPopoverProps, PriorityFactor } from "./components/signal-priority-popover"
|
|
44
44
|
export * from "./components/filter-chip"
|
|
@@ -89,8 +89,8 @@ describe("DetailView attentionCount", () => {
|
|
|
89
89
|
expect(pill).not.toBeNull();
|
|
90
90
|
expect(pill!.textContent).toContain("5");
|
|
91
91
|
|
|
92
|
-
// Click the timeline
|
|
93
|
-
const timelineButton = container.querySelector(
|
|
92
|
+
// Click the timeline header button to expand
|
|
93
|
+
const timelineButton = container.querySelector("button.group\\/timeline") as HTMLElement;
|
|
94
94
|
expect(timelineButton).not.toBeNull();
|
|
95
95
|
fireEvent.click(timelineButton);
|
|
96
96
|
|
|
@@ -16,7 +16,7 @@ import type { TimelineEvent } from "../components/timeline-activity"
|
|
|
16
16
|
import type { ApprovalState } from "../components/signal-feedback-inline"
|
|
17
17
|
import type { LucideIcon } from "lucide-react"
|
|
18
18
|
import type { PriorityFactor } from "../components/signal-priority-popover"
|
|
19
|
-
import type { FeedbackChipTree, FeedbackSubmitData } from "../components/feedback-primitives"
|
|
19
|
+
import type { FeedbackChipTree, FeedbackSubmitData, PersistedFeedbackData } from "../components/feedback-primitives"
|
|
20
20
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
// Shared
|
|
@@ -57,6 +57,10 @@ export interface SignalScoreExplanationSignal {
|
|
|
57
57
|
counterparty?: string
|
|
58
58
|
/** Component breakdown for combined signals. */
|
|
59
59
|
components?: Array<{ type: string; count: number }>
|
|
60
|
+
/** Current balance value (e.g., "$3.0M") for balance context strip. */
|
|
61
|
+
currentBalance?: string
|
|
62
|
+
/** Additional balance context text (e.g., "down from $23M"). */
|
|
63
|
+
balanceContext?: string
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
export interface SignalScoreExplanationBucket {
|
|
@@ -97,6 +101,12 @@ export interface SignalScoreData {
|
|
|
97
101
|
/** @deprecated The compact score UX no longer renders score-level thumbs by default. */
|
|
98
102
|
initialScoreFeedback?: { type: "up" | "down"; pills: string[]; detail: string } | null
|
|
99
103
|
initialFactorFeedback?: Record<string, { type: "up" | "down"; detail: string }>
|
|
104
|
+
/** Factor-level feedback for the priority popover rows (keyed by factor key). */
|
|
105
|
+
initialFactorPopoverFeedback?: Record<string, { type: "up" | "down"; detail: string; ownershipLabel?: string }>
|
|
106
|
+
/** Persisted bucket-level feedback, keyed by bucket key. */
|
|
107
|
+
initialBucketFeedback?: Record<string, PersistedFeedbackData>
|
|
108
|
+
/** Persisted priority-level feedback for the popover footer. */
|
|
109
|
+
initialPriorityFeedback?: PersistedFeedbackData | null
|
|
100
110
|
/** Priority factors for the popover breakdown. */
|
|
101
111
|
priorityFactors?: PriorityFactor[]
|
|
102
112
|
/** Negative feedback chip tree for the priority popover. */
|
|
@@ -150,16 +150,6 @@ export interface DetailViewProps {
|
|
|
150
150
|
onRequestApproval?: () => Promise<void>
|
|
151
151
|
/** Number of important/attention-worthy events to highlight on the collapsed timeline header. */
|
|
152
152
|
attentionCount?: number
|
|
153
|
-
/** Label for the system-events toggle button (e.g. "Score changes"). */
|
|
154
|
-
timelineSystemEventsToggleLabel?: string
|
|
155
|
-
/** localStorage key for persisting the system-events toggle state. */
|
|
156
|
-
timelineSystemEventsStorageKey?: string
|
|
157
|
-
/** Whether system-noise events are visible by default. @default false */
|
|
158
|
-
timelineSystemEventsDefaultVisible?: boolean
|
|
159
|
-
/** Hint text shown below the timeline when system events are hidden. */
|
|
160
|
-
timelineSystemEventsHiddenHint?: string
|
|
161
|
-
/** Hint text shown below the timeline when system events are visible. Uses {count} as placeholder. */
|
|
162
|
-
timelineSystemEventsVisibleHint?: string
|
|
163
153
|
}
|
|
164
154
|
|
|
165
155
|
export function DetailView({
|
|
@@ -194,47 +184,10 @@ export function DetailView({
|
|
|
194
184
|
opportunityPreview,
|
|
195
185
|
onRequestApproval,
|
|
196
186
|
attentionCount,
|
|
197
|
-
timelineSystemEventsToggleLabel,
|
|
198
|
-
timelineSystemEventsStorageKey,
|
|
199
|
-
timelineSystemEventsDefaultVisible = false,
|
|
200
|
-
timelineSystemEventsHiddenHint,
|
|
201
|
-
timelineSystemEventsVisibleHint,
|
|
202
187
|
}: DetailViewProps) {
|
|
203
188
|
const [showTimeline, setShowTimeline] = React.useState(false)
|
|
204
189
|
const [extraActions, setExtraActions] = React.useState<SuggestedAction[]>([])
|
|
205
190
|
|
|
206
|
-
// ---- System-noise toggle state ----
|
|
207
|
-
const [showSystemEvents, setShowSystemEvents] = React.useState(timelineSystemEventsDefaultVisible)
|
|
208
|
-
const initialReadDoneRef = React.useRef(false)
|
|
209
|
-
|
|
210
|
-
// Read persisted value from localStorage on mount
|
|
211
|
-
React.useEffect(() => {
|
|
212
|
-
if (!timelineSystemEventsStorageKey) {
|
|
213
|
-
initialReadDoneRef.current = true
|
|
214
|
-
return
|
|
215
|
-
}
|
|
216
|
-
try {
|
|
217
|
-
const stored = localStorage.getItem(timelineSystemEventsStorageKey)
|
|
218
|
-
if (stored !== null) {
|
|
219
|
-
setShowSystemEvents(stored === "true")
|
|
220
|
-
}
|
|
221
|
-
} catch {
|
|
222
|
-
// localStorage unavailable — ignore
|
|
223
|
-
}
|
|
224
|
-
initialReadDoneRef.current = true
|
|
225
|
-
}, [timelineSystemEventsStorageKey])
|
|
226
|
-
|
|
227
|
-
// Write to localStorage when the toggle changes (skip initial if matching default)
|
|
228
|
-
React.useEffect(() => {
|
|
229
|
-
if (!timelineSystemEventsStorageKey) return
|
|
230
|
-
if (!initialReadDoneRef.current) return
|
|
231
|
-
try {
|
|
232
|
-
localStorage.setItem(timelineSystemEventsStorageKey, String(showSystemEvents))
|
|
233
|
-
} catch {
|
|
234
|
-
// localStorage unavailable — ignore
|
|
235
|
-
}
|
|
236
|
-
}, [showSystemEvents, timelineSystemEventsStorageKey])
|
|
237
|
-
|
|
238
191
|
React.useEffect(() => {
|
|
239
192
|
setShowTimeline(false)
|
|
240
193
|
setExtraActions([])
|
|
@@ -319,6 +272,9 @@ export function DetailView({
|
|
|
319
272
|
metaText={undefined}
|
|
320
273
|
feedbackChips={signalData.priorityFeedbackChips}
|
|
321
274
|
onFeedbackSubmit={signalData.onPriorityFeedback}
|
|
275
|
+
initialFactorFeedback={signalData.initialFactorPopoverFeedback}
|
|
276
|
+
onFactorFeedback={signalData.onFactorFeedback}
|
|
277
|
+
initialPriorityFeedback={signalData.initialPriorityFeedback}
|
|
322
278
|
/>
|
|
323
279
|
{signalData.timeChipLabel && (
|
|
324
280
|
<Badge variant="outline" title={signalData.timeChipDetail ?? undefined}>
|
|
@@ -398,92 +354,38 @@ export function DetailView({
|
|
|
398
354
|
{renderAfterScore?.(item)}
|
|
399
355
|
|
|
400
356
|
{/* Activity Timeline */}
|
|
401
|
-
{sections.timeline && timelineEvents.length > 0 && (
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
className="group/timeline flex w-full items-center justify-between gap-2 py-2 rounded-md transition-colors hover:bg-muted/40 -mx-2 px-2"
|
|
420
|
-
data-testid="timeline-header"
|
|
421
|
-
>
|
|
422
|
-
{/* Left: collapse/expand button */}
|
|
423
|
-
<button
|
|
424
|
-
type="button"
|
|
425
|
-
onClick={() => setShowTimeline((prev) => !prev)}
|
|
426
|
-
className="flex items-center gap-2 cursor-pointer bg-transparent border-0 p-0"
|
|
427
|
-
data-testid="timeline-collapse-btn"
|
|
428
|
-
>
|
|
429
|
-
<h3 className="text-xs font-bold text-muted-foreground uppercase tracking-wider group-hover/timeline:text-foreground transition-colors">Activity timeline</h3>
|
|
430
|
-
{!showTimeline && attentionCount != null && attentionCount > 0 && (
|
|
431
|
-
<span className="inline-flex items-center gap-1 rounded-full bg-destructive/10 px-1.5 py-0.5 text-[10px] font-semibold text-destructive border border-destructive/20">
|
|
432
|
-
{attentionCount} new
|
|
433
|
-
</span>
|
|
434
|
-
)}
|
|
435
|
-
{!showTimeline && firstVisibleTime && (
|
|
436
|
-
<span className="text-[11px] text-muted-foreground/60" data-testid="last-activity-hint">
|
|
437
|
-
· Last activity {firstVisibleTime}
|
|
438
|
-
</span>
|
|
439
|
-
)}
|
|
440
|
-
<div className="flex items-center gap-1.5">
|
|
441
|
-
<span className="text-[11px] font-medium text-muted-foreground" data-testid="event-count">{eventCountLabel}</span>
|
|
442
|
-
<ChevronDown className={`h-3.5 w-3.5 text-muted-foreground transition-transform duration-200 ${showTimeline ? "rotate-180" : ""}`} />
|
|
443
|
-
</div>
|
|
444
|
-
</button>
|
|
445
|
-
|
|
446
|
-
{/* Right: system-events toggle */}
|
|
447
|
-
{showToggle && (
|
|
448
|
-
<button
|
|
449
|
-
type="button"
|
|
450
|
-
onClick={() => setShowSystemEvents((prev) => !prev)}
|
|
451
|
-
className="flex shrink-0 items-center gap-1.5 rounded-full border border-border bg-background px-2.5 py-1 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-muted/40 hover:text-foreground cursor-pointer"
|
|
452
|
-
aria-pressed={showSystemEvents}
|
|
453
|
-
data-testid="system-events-toggle"
|
|
454
|
-
>
|
|
455
|
-
{timelineSystemEventsToggleLabel ?? "System events"}
|
|
456
|
-
<span
|
|
457
|
-
className="inline-flex items-center justify-center rounded-full bg-muted px-1.5 text-[10px] font-semibold min-w-[18px] tabular-nums"
|
|
458
|
-
data-testid="hidden-count-badge"
|
|
459
|
-
>
|
|
460
|
-
{hiddenCount}
|
|
461
|
-
</span>
|
|
462
|
-
</button>
|
|
357
|
+
{sections.timeline && timelineEvents.length > 0 && (
|
|
358
|
+
<div className="mb-8">
|
|
359
|
+
<button
|
|
360
|
+
type="button"
|
|
361
|
+
onClick={() => setShowTimeline((prev) => !prev)}
|
|
362
|
+
className="group/timeline flex w-full items-center justify-between gap-2 py-2 rounded-md transition-colors hover:bg-muted/40 -mx-2 px-2 cursor-pointer"
|
|
363
|
+
>
|
|
364
|
+
<div className="flex items-center gap-2">
|
|
365
|
+
<h3 className="text-xs font-bold text-muted-foreground uppercase tracking-wider group-hover/timeline:text-foreground transition-colors">Activity timeline</h3>
|
|
366
|
+
{!showTimeline && attentionCount != null && attentionCount > 0 && (
|
|
367
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-destructive/10 px-1.5 py-0.5 text-[10px] font-semibold text-destructive border border-destructive/20">
|
|
368
|
+
{attentionCount} new
|
|
369
|
+
</span>
|
|
370
|
+
)}
|
|
371
|
+
{!showTimeline && (lastActivityTime || (timelineEvents.length > 0 && timelineEvents[0].time)) && (
|
|
372
|
+
<span className="text-[11px] text-muted-foreground/60">
|
|
373
|
+
· Last activity {lastActivityTime ?? timelineEvents[0]?.time ?? ''}
|
|
374
|
+
</span>
|
|
463
375
|
)}
|
|
464
376
|
</div>
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
</p>
|
|
478
|
-
)}
|
|
479
|
-
{showTimeline && showSystemEvents && timelineSystemEventsVisibleHint && hasSystemNoise && (
|
|
480
|
-
<p className="mt-2 text-[11px] text-muted-foreground/60 border-t border-dashed border-border pt-2" data-testid="timeline-footer-hint">
|
|
481
|
-
{timelineSystemEventsVisibleHint.replace("{count}", String(hiddenCount))}
|
|
482
|
-
</p>
|
|
483
|
-
)}
|
|
484
|
-
</div>
|
|
485
|
-
)
|
|
486
|
-
})()}
|
|
377
|
+
<div className="flex items-center gap-1.5">
|
|
378
|
+
<span className="text-[11px] font-medium text-muted-foreground">{timelineEvents.length} events</span>
|
|
379
|
+
<ChevronDown className={`h-3.5 w-3.5 text-muted-foreground transition-transform duration-200 ${showTimeline ? "rotate-180" : ""}`} />
|
|
380
|
+
</div>
|
|
381
|
+
</button>
|
|
382
|
+
{showTimeline && (
|
|
383
|
+
<div className="mt-3">
|
|
384
|
+
<TimelineActivity events={timelineEvents} />
|
|
385
|
+
</div>
|
|
386
|
+
)}
|
|
387
|
+
</div>
|
|
388
|
+
)}
|
|
487
389
|
</div>
|
|
488
390
|
|
|
489
391
|
{/* Suggested Actions */}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest"
|
|
2
|
-
import React from "react"
|
|
3
|
-
import { render, screen } from "@testing-library/react"
|
|
4
|
-
import {
|
|
5
|
-
TimelineActivity,
|
|
6
|
-
TONE_CLASSES,
|
|
7
|
-
type TimelineEvent,
|
|
8
|
-
} from "../timeline-activity"
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Helpers
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
function minimal(overrides: Partial<TimelineEvent> = {}): TimelineEvent {
|
|
15
|
-
return {
|
|
16
|
-
id: "e1",
|
|
17
|
-
icon: React.createElement("span", { "data-testid": "icon" }, "⚡"),
|
|
18
|
-
title: "Test event",
|
|
19
|
-
time: "2h ago",
|
|
20
|
-
...overrides,
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// Tests
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
describe("TimelineActivity", () => {
|
|
29
|
-
// --- Tone rendering ---
|
|
30
|
-
|
|
31
|
-
it("renders red dot classes when tone is 'red'", () => {
|
|
32
|
-
const event = minimal({ tone: "red" })
|
|
33
|
-
const { container } = render(<TimelineActivity events={[event]} />)
|
|
34
|
-
const dot = container.querySelector('[data-testid="timeline-dot"]')!
|
|
35
|
-
expect(dot).not.toBeNull()
|
|
36
|
-
const cls = dot.className
|
|
37
|
-
// Should contain all the red tone dot classes
|
|
38
|
-
expect(cls).toContain("bg-red-50")
|
|
39
|
-
expect(cls).toContain("border-red-200")
|
|
40
|
-
// Should contain the red icon classes
|
|
41
|
-
expect(cls).toContain("text-red-600")
|
|
42
|
-
// Should NOT contain neutral classes
|
|
43
|
-
expect(cls).not.toContain("border-border/60")
|
|
44
|
-
expect(cls).not.toContain("text-muted-foreground")
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it("renders neutral dot classes when tone is absent", () => {
|
|
48
|
-
const event = minimal()
|
|
49
|
-
const { container } = render(<TimelineActivity events={[event]} />)
|
|
50
|
-
const dot = container.querySelector('[data-testid="timeline-dot"]')!
|
|
51
|
-
expect(dot).not.toBeNull()
|
|
52
|
-
const cls = dot.className
|
|
53
|
-
expect(cls).toContain("border-border/60")
|
|
54
|
-
expect(cls).toContain("bg-background")
|
|
55
|
-
expect(cls).toContain("text-muted-foreground")
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
// --- Actor byline ---
|
|
59
|
-
|
|
60
|
-
it("renders actor byline with name when actor.kind is 'user'", () => {
|
|
61
|
-
const event = minimal({
|
|
62
|
-
actor: { kind: "user", name: "Alice" },
|
|
63
|
-
})
|
|
64
|
-
render(<TimelineActivity events={[event]} />)
|
|
65
|
-
const byline = screen.getByTestId("actor-byline")
|
|
66
|
-
expect(byline).not.toBeNull()
|
|
67
|
-
expect(byline.textContent).toContain("Alice")
|
|
68
|
-
expect(byline.textContent).toContain("performed this action")
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it("renders no byline when actor.kind is 'system'", () => {
|
|
72
|
-
const event = minimal({
|
|
73
|
-
actor: { kind: "system" },
|
|
74
|
-
})
|
|
75
|
-
const { container } = render(<TimelineActivity events={[event]} />)
|
|
76
|
-
const byline = container.querySelector('[data-testid="actor-byline"]')
|
|
77
|
-
expect(byline).toBeNull()
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it("renders 'Integration' text when actor.kind is 'integration'", () => {
|
|
81
|
-
const event = minimal({
|
|
82
|
-
actor: { kind: "integration" },
|
|
83
|
-
})
|
|
84
|
-
render(<TimelineActivity events={[event]} />)
|
|
85
|
-
const byline = screen.getByTestId("actor-byline")
|
|
86
|
-
expect(byline).not.toBeNull()
|
|
87
|
-
expect(byline.textContent).toContain("Integration")
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it("renders custom verb for user actor", () => {
|
|
91
|
-
const event = minimal({
|
|
92
|
-
actor: { kind: "user", name: "Bob", verb: "approved this case" },
|
|
93
|
-
})
|
|
94
|
-
render(<TimelineActivity events={[event]} />)
|
|
95
|
-
const byline = screen.getByTestId("actor-byline")
|
|
96
|
-
expect(byline.textContent).toContain("approved this case")
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it("renders no byline when actor is absent", () => {
|
|
100
|
-
const event = minimal()
|
|
101
|
-
const { container } = render(<TimelineActivity events={[event]} />)
|
|
102
|
-
const byline = container.querySelector('[data-testid="actor-byline"]')
|
|
103
|
-
expect(byline).toBeNull()
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
// --- Backwards compatibility ---
|
|
107
|
-
|
|
108
|
-
it("renders correctly with minimal TimelineEvent (only id, icon, title, time)", () => {
|
|
109
|
-
const event: TimelineEvent = {
|
|
110
|
-
id: "min-1",
|
|
111
|
-
icon: React.createElement("span", null, "📌"),
|
|
112
|
-
title: "Minimal event",
|
|
113
|
-
time: "5m ago",
|
|
114
|
-
}
|
|
115
|
-
const { container } = render(<TimelineActivity events={[event]} />)
|
|
116
|
-
// Should render without errors
|
|
117
|
-
expect(container.textContent).toContain("Minimal event")
|
|
118
|
-
expect(container.textContent).toContain("5m ago")
|
|
119
|
-
// Dot should have neutral classes
|
|
120
|
-
const dot = container.querySelector('[data-testid="timeline-dot"]')!
|
|
121
|
-
expect(dot.className).toContain("border-border/60")
|
|
122
|
-
// No byline
|
|
123
|
-
const byline = container.querySelector('[data-testid="actor-byline"]')
|
|
124
|
-
expect(byline).toBeNull()
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
// --- TONE_CLASSES export ---
|
|
128
|
-
|
|
129
|
-
it("exports TONE_CLASSES with all expected tones", () => {
|
|
130
|
-
const tones = ["red", "amber", "emerald", "violet", "blue", "slate", "salesforce", "gmail"] as const
|
|
131
|
-
for (const tone of tones) {
|
|
132
|
-
expect(TONE_CLASSES[tone]).toBeDefined()
|
|
133
|
-
expect(TONE_CLASSES[tone].dot).toBeTruthy()
|
|
134
|
-
expect(TONE_CLASSES[tone].icon).toBeTruthy()
|
|
135
|
-
}
|
|
136
|
-
})
|
|
137
|
-
})
|