@handled-ai/design-system 0.20.12 → 0.20.13
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/comment-composer.js +49 -69
- package/dist/components/comment-composer.js.map +1 -1
- package/dist/components/conversation-panel.js +127 -84
- package/dist/components/conversation-panel.js.map +1 -1
- package/dist/components/pill.d.ts +1 -1
- package/dist/components/tabs.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.js +62 -107
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/comment-composer.test.tsx +3 -7
- package/src/components/__tests__/conversation-panel.test.tsx +75 -11
- package/src/components/comment-composer.tsx +14 -26
- package/src/components/conversation-panel.tsx +83 -29
- package/src/prototype/__tests__/detail-view-case-panel-v2.test.tsx +3 -3
- package/src/prototype/__tests__/detail-view-timeline-system-events.test.tsx +14 -17
- package/src/prototype/prototype-inbox-view.tsx +44 -72
|
@@ -55,34 +55,29 @@ function CommentComposer({
|
|
|
55
55
|
data-slot="comment-composer"
|
|
56
56
|
data-open={open ? "true" : undefined}
|
|
57
57
|
className={cn(
|
|
58
|
-
"flex items-start gap-
|
|
58
|
+
"border-border bg-background flex items-start gap-2 rounded-lg border px-2 py-1.5 transition-colors",
|
|
59
|
+
open && "ring-ring/30 ring-2",
|
|
59
60
|
className
|
|
60
61
|
)}
|
|
61
62
|
>
|
|
62
|
-
<Avatar size="sm" className="mt-
|
|
63
|
+
<Avatar size="sm" className="mt-px">
|
|
63
64
|
{author?.avatarUrl ? <AvatarImage src={author.avatarUrl} alt={author.name ?? "You"} /> : null}
|
|
64
|
-
<AvatarFallback className="bg-
|
|
65
|
+
<AvatarFallback className="bg-muted text-muted-foreground text-[10px] font-medium uppercase">
|
|
65
66
|
{getInitials({ name: author?.name, email: author?.email })}
|
|
66
67
|
</AvatarFallback>
|
|
67
68
|
</Avatar>
|
|
68
69
|
|
|
69
|
-
<div
|
|
70
|
-
data-slot="comment-composer-shell"
|
|
71
|
-
className={cn(
|
|
72
|
-
"min-w-0 flex-1 rounded-xl border border-border bg-background transition-[box-shadow,border-color]",
|
|
73
|
-
open ? "overflow-hidden shadow-sm" : "shadow-none"
|
|
74
|
-
)}
|
|
75
|
-
>
|
|
70
|
+
<div className="min-w-0 flex-1">
|
|
76
71
|
<Textarea
|
|
77
72
|
data-slot="comment-composer-input"
|
|
78
73
|
value={text}
|
|
79
74
|
onChange={(e) => setText(e.target.value)}
|
|
80
75
|
onFocus={() => setFocused(true)}
|
|
81
76
|
placeholder={placeholder}
|
|
82
|
-
rows={open ?
|
|
77
|
+
rows={open ? 3 : 1}
|
|
83
78
|
className={cn(
|
|
84
|
-
"resize-none
|
|
85
|
-
open
|
|
79
|
+
"resize-none border-0 bg-transparent px-1 py-0.5 text-sm leading-snug shadow-none focus-visible:ring-0",
|
|
80
|
+
!open && "min-h-0"
|
|
86
81
|
)}
|
|
87
82
|
onKeyDown={(e) => {
|
|
88
83
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
@@ -93,16 +88,15 @@ function CommentComposer({
|
|
|
93
88
|
/>
|
|
94
89
|
|
|
95
90
|
{open ? (
|
|
96
|
-
<div className="flex items-center justify-between gap-
|
|
97
|
-
<span className="inline-flex items-center gap-
|
|
98
|
-
<Lock size={
|
|
91
|
+
<div className="mt-0.5 flex items-center justify-between gap-2">
|
|
92
|
+
<span className="text-muted-foreground inline-flex items-center gap-1 text-[11px]">
|
|
93
|
+
<Lock size={12} /> {hint}
|
|
99
94
|
</span>
|
|
100
|
-
<span className="flex items-center gap-
|
|
95
|
+
<span className="flex items-center gap-2">
|
|
101
96
|
<Button
|
|
102
97
|
type="button"
|
|
103
98
|
variant="ghost"
|
|
104
99
|
size="sm"
|
|
105
|
-
className="px-2 text-sm font-medium text-muted-foreground hover:bg-transparent hover:text-foreground"
|
|
106
100
|
onClick={() => {
|
|
107
101
|
setText("")
|
|
108
102
|
setFocused(false)
|
|
@@ -110,15 +104,9 @@ function CommentComposer({
|
|
|
110
104
|
>
|
|
111
105
|
Cancel
|
|
112
106
|
</Button>
|
|
113
|
-
<Button
|
|
114
|
-
type="button"
|
|
115
|
-
size="sm"
|
|
116
|
-
disabled={!canPost}
|
|
117
|
-
onClick={post}
|
|
118
|
-
className="rounded-lg bg-foreground px-4 text-sm font-semibold text-background shadow-none hover:bg-foreground/90"
|
|
119
|
-
>
|
|
107
|
+
<Button type="button" size="sm" disabled={!canPost} onClick={post}>
|
|
120
108
|
Comment
|
|
121
|
-
<kbd className="ml-1 rounded px-1 text-[10px]
|
|
109
|
+
<kbd className="bg-primary-foreground/15 ml-1 rounded px-1 text-[10px]">⌘↵</kbd>
|
|
122
110
|
</Button>
|
|
123
111
|
</span>
|
|
124
112
|
</div>
|
|
@@ -298,19 +298,34 @@ function PersonAvatar({ person, size = "sm" }: { person: ConvParticipant; size?:
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
const STATUS_PILL: Record<ConvStatus, { label: string; cls: string }> = {
|
|
301
|
-
responded: { label: "
|
|
301
|
+
responded: { label: "NEW REPLY", cls: "bg-status-warning-bg text-status-warning-fg border-status-warning-border" },
|
|
302
302
|
draft: { label: "Draft", cls: "bg-background text-foreground/80 border-border" },
|
|
303
|
-
awaiting: { label: "
|
|
303
|
+
awaiting: { label: "SENT", cls: "bg-status-info-bg text-status-info-fg border-status-info-border" },
|
|
304
304
|
viewing: { label: "Viewing", cls: "bg-muted text-muted-foreground border-border" },
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
const STATUS_DOT: Record<ConvStatus, string> = {
|
|
308
|
-
responded: "bg-status-
|
|
308
|
+
responded: "bg-status-warning-fg",
|
|
309
309
|
draft: "bg-status-pending-fg",
|
|
310
|
-
awaiting: "bg-status-
|
|
310
|
+
awaiting: "bg-status-info-fg",
|
|
311
311
|
viewing: "bg-muted-foreground/50",
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
const THREAD_ROW_ACCENT: Record<ConvStatus, string> = {
|
|
315
|
+
responded: "border-l-4 border-l-status-warning-border bg-status-warning-bg/25",
|
|
316
|
+
awaiting: "border-l-4 border-l-status-info-border bg-status-info-bg/25",
|
|
317
|
+
draft: "border-l-4 border-l-status-pending-border bg-status-pending-bg/20",
|
|
318
|
+
viewing: "",
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const RECEIPT_CHIP: Record<NonNullable<ConvMessage["receipt"]>["kind"], string> = {
|
|
322
|
+
new: "border-status-warning-border bg-status-warning-bg text-status-warning-fg",
|
|
323
|
+
read: "border-status-info-border bg-status-info-bg text-status-info-fg",
|
|
324
|
+
opened: "border-status-info-border bg-status-info-bg text-status-info-fg",
|
|
325
|
+
sent: "border-status-info-border bg-status-info-bg text-status-info-fg",
|
|
326
|
+
draft: "border-status-pending-border bg-status-pending-bg text-status-pending-fg",
|
|
327
|
+
}
|
|
328
|
+
|
|
314
329
|
function effectiveStatus(t: ConversationThread): ConvStatus {
|
|
315
330
|
return t.canReply === false ? "viewing" : t.status
|
|
316
331
|
}
|
|
@@ -432,10 +447,10 @@ function MessageView({
|
|
|
432
447
|
</span>
|
|
433
448
|
<span className="flex shrink-0 items-center gap-2">
|
|
434
449
|
{message.receipt ? (
|
|
435
|
-
<span className="
|
|
450
|
+
<span className={cn("inline-flex items-center gap-1 rounded-md border px-1.5 py-px text-[10px] font-semibold leading-4", RECEIPT_CHIP[message.receipt.kind])}>
|
|
436
451
|
{message.receipt.kind === "new" ? (
|
|
437
452
|
<CornerUpLeft size={11} />
|
|
438
|
-
) : message.receipt.kind === "read" ? (
|
|
453
|
+
) : message.receipt.kind === "read" || message.receipt.kind === "sent" ? (
|
|
439
454
|
<CheckCheck size={11} />
|
|
440
455
|
) : message.receipt.kind === "draft" ? (
|
|
441
456
|
<FilePenLine size={11} />
|
|
@@ -811,10 +826,10 @@ function ThreadBody({
|
|
|
811
826
|
return (
|
|
812
827
|
<div data-slot="conv-thread-body" className="space-y-2">
|
|
813
828
|
{canReply && thread.paused ? (
|
|
814
|
-
<div className="border-status-
|
|
829
|
+
<div className="border-status-warning-border bg-status-warning-bg text-status-warning-fg flex items-start gap-2 rounded-md border border-l-4 p-2.5 text-[12px]">
|
|
815
830
|
<Pause size={13} className="mt-0.5 shrink-0" />
|
|
816
831
|
<span>
|
|
817
|
-
<b>Follow-up actions
|
|
832
|
+
<b>Playbook stopped.</b> Follow-up actions for {thread.paused.playbook} won’t send
|
|
818
833
|
automatically while this conversation is live. Continue it in {tenantName ?? "the app"} or Gmail.
|
|
819
834
|
</span>
|
|
820
835
|
</div>
|
|
@@ -930,7 +945,12 @@ function ThreadRow({
|
|
|
930
945
|
const pill = STATUS_PILL[status]
|
|
931
946
|
|
|
932
947
|
return (
|
|
933
|
-
<div
|
|
948
|
+
<div
|
|
949
|
+
data-slot="conv-thread"
|
|
950
|
+
data-status={status}
|
|
951
|
+
data-open={open ? "true" : undefined}
|
|
952
|
+
className={cn("border-border border-b last:border-b-0", THREAD_ROW_ACCENT[status])}
|
|
953
|
+
>
|
|
934
954
|
<button
|
|
935
955
|
type="button"
|
|
936
956
|
onClick={onToggleOpen}
|
|
@@ -991,11 +1011,13 @@ function ConversationPanel({
|
|
|
991
1011
|
const draft = threads.filter((t) => effectiveStatus(t) === "draft").length
|
|
992
1012
|
const awaiting = threads.filter((t) => effectiveStatus(t) === "awaiting").length
|
|
993
1013
|
const anyPaused = threads.some((t) => t.paused)
|
|
1014
|
+
const hubGmailThread = threads.find((t) => canOpenInGmail(t, onOpenInGmail))
|
|
1015
|
+
const firstAwaiting = threads.find((t) => effectiveStatus(t) === "awaiting")
|
|
994
1016
|
|
|
995
1017
|
const [hubOpen, setHubOpen] = React.useState(true)
|
|
996
1018
|
const [openId, setOpenId] = React.useState<string | null>(() => {
|
|
997
1019
|
if (defaultOpenThreadId) return defaultOpenThreadId
|
|
998
|
-
const firstActionable = threads.find((t) => ["responded", "draft"].includes(t.status) && t.canReply !== false)
|
|
1020
|
+
const firstActionable = threads.find((t) => ["responded", "draft", "awaiting"].includes(t.status) && t.canReply !== false)
|
|
999
1021
|
return firstActionable ? firstActionable.threadId : null
|
|
1000
1022
|
})
|
|
1001
1023
|
|
|
@@ -1004,11 +1026,11 @@ function ConversationPanel({
|
|
|
1004
1026
|
// Header badge state: a responded reply leads, then drafts to finish, then sent mail awaiting a reply.
|
|
1005
1027
|
const badge =
|
|
1006
1028
|
responded > 0
|
|
1007
|
-
? { label: "Email response detected", dot: "bg-status-
|
|
1029
|
+
? { label: "Email response detected", dot: "bg-status-warning-fg", ring: "bg-status-warning-fg/30" }
|
|
1008
1030
|
: draft > 0
|
|
1009
1031
|
? { label: "Draft ready", dot: "bg-status-pending-fg", ring: "bg-status-pending-fg/30" }
|
|
1010
1032
|
: awaiting > 0
|
|
1011
|
-
? { label: "
|
|
1033
|
+
? { label: "Email sent · awaiting reply", dot: "bg-status-info-fg", ring: "bg-status-info-fg/30" }
|
|
1012
1034
|
: { label: "Conversations", dot: "bg-muted-foreground/50", ring: "bg-muted-foreground/20" }
|
|
1013
1035
|
|
|
1014
1036
|
const headTitle =
|
|
@@ -1017,30 +1039,56 @@ function ConversationPanel({
|
|
|
1017
1039
|
: draft > 0
|
|
1018
1040
|
? `Draft ready on ${draft} ${draft === 1 ? "thread" : "threads"}`
|
|
1019
1041
|
: awaiting > 0
|
|
1020
|
-
?
|
|
1042
|
+
? awaiting === 1 && firstAwaiting
|
|
1043
|
+
? `Awaiting a response from ${firstName(displayParticipant(firstAwaiting.contact).name)}`
|
|
1044
|
+
: `Awaiting responses on ${awaiting} threads`
|
|
1021
1045
|
: `${threads.length} email ${threads.length === 1 ? "thread" : "threads"}`
|
|
1022
1046
|
|
|
1047
|
+
const panelState = responded > 0 ? "responded" : draft > 0 ? "draft" : awaiting > 0 ? "awaiting" : "viewing"
|
|
1048
|
+
|
|
1023
1049
|
return (
|
|
1024
1050
|
<section
|
|
1025
1051
|
data-slot="conversation-panel"
|
|
1026
1052
|
data-responded={responded > 0 ? "true" : undefined}
|
|
1027
|
-
|
|
1053
|
+
data-state={panelState}
|
|
1054
|
+
className={cn(
|
|
1055
|
+
"bg-background overflow-hidden rounded-xl border",
|
|
1056
|
+
panelState === "responded"
|
|
1057
|
+
? "border-status-warning-border"
|
|
1058
|
+
: panelState === "awaiting"
|
|
1059
|
+
? "border-status-info-border"
|
|
1060
|
+
: "border-border",
|
|
1061
|
+
className,
|
|
1062
|
+
)}
|
|
1028
1063
|
>
|
|
1029
|
-
<
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1064
|
+
<div
|
|
1065
|
+
data-slot="conversation-panel-header"
|
|
1066
|
+
className={cn(
|
|
1067
|
+
"flex w-full items-center gap-2 px-3 py-2.5",
|
|
1068
|
+
panelState === "responded"
|
|
1069
|
+
? "bg-status-warning-bg/45"
|
|
1070
|
+
: panelState === "awaiting"
|
|
1071
|
+
? "bg-status-info-bg/55"
|
|
1072
|
+
: "bg-background",
|
|
1073
|
+
)}
|
|
1034
1074
|
>
|
|
1075
|
+
<button
|
|
1076
|
+
type="button"
|
|
1077
|
+
onClick={() => setHubOpen((v) => !v)}
|
|
1078
|
+
aria-expanded={hubOpen}
|
|
1079
|
+
className="flex min-w-0 flex-1 items-center gap-3 text-left"
|
|
1080
|
+
>
|
|
1035
1081
|
<span
|
|
1036
1082
|
data-slot="conversation-badge"
|
|
1037
1083
|
className={cn(
|
|
1038
1084
|
"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-semibold",
|
|
1039
1085
|
responded > 0
|
|
1040
|
-
? "bg-status-
|
|
1041
|
-
:
|
|
1042
|
-
? "bg-status-
|
|
1043
|
-
:
|
|
1086
|
+
? "bg-status-warning-bg text-status-warning-fg"
|
|
1087
|
+
: awaiting > 0
|
|
1088
|
+
? "bg-status-info-bg text-status-info-fg"
|
|
1089
|
+
: draft > 0
|
|
1090
|
+
? "bg-status-pending-bg text-status-pending-fg"
|
|
1091
|
+
: "bg-muted text-muted-foreground"
|
|
1044
1092
|
)}
|
|
1045
1093
|
>
|
|
1046
1094
|
<span className="relative inline-flex size-2">
|
|
@@ -1056,12 +1104,18 @@ function ConversationPanel({
|
|
|
1056
1104
|
{anyPaused ? <> · <b>playbook stopped</b></> : null}
|
|
1057
1105
|
</span>
|
|
1058
1106
|
</span>
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1107
|
+
{hubOpen ? (
|
|
1108
|
+
<ChevronUp size={16} className="text-muted-foreground shrink-0" />
|
|
1109
|
+
) : (
|
|
1110
|
+
<ChevronDown size={16} className="text-muted-foreground shrink-0" />
|
|
1111
|
+
)}
|
|
1112
|
+
</button>
|
|
1113
|
+
{hubGmailThread ? (
|
|
1114
|
+
<div className="shrink-0" onClick={(event) => event.stopPropagation()}>
|
|
1115
|
+
<OpenInGmailButton thread={hubGmailThread} onOpenInGmail={onOpenInGmail} />
|
|
1116
|
+
</div>
|
|
1117
|
+
) : null}
|
|
1118
|
+
</div>
|
|
1065
1119
|
|
|
1066
1120
|
{hubOpen ? (
|
|
1067
1121
|
<div className="border-border border-t">
|
|
@@ -101,7 +101,7 @@ describe("DetailView case-panel-v2 section layout", () => {
|
|
|
101
101
|
screen.getByText("Cash movement"),
|
|
102
102
|
screen.getByText("Approve action"),
|
|
103
103
|
screen.getByText("After-score marker"),
|
|
104
|
-
screen.getByText(
|
|
104
|
+
screen.getByText("Activity timeline"),
|
|
105
105
|
screen.getByText("Legacy detail extra marker"),
|
|
106
106
|
)
|
|
107
107
|
})
|
|
@@ -125,7 +125,7 @@ describe("DetailView case-panel-v2 section layout", () => {
|
|
|
125
125
|
screen.getByText("Opportunity marker"),
|
|
126
126
|
screen.getByText("Primary action marker"),
|
|
127
127
|
screen.getByText("Comment area marker"),
|
|
128
|
-
screen.getByText(
|
|
128
|
+
screen.getByText("Activity timeline"),
|
|
129
129
|
)
|
|
130
130
|
})
|
|
131
131
|
|
|
@@ -148,7 +148,7 @@ describe("DetailView case-panel-v2 section layout", () => {
|
|
|
148
148
|
|
|
149
149
|
expectInDocumentOrder(
|
|
150
150
|
screen.getByText("Comment composer marker"),
|
|
151
|
-
screen.getByText(
|
|
151
|
+
screen.getByText("Activity timeline"),
|
|
152
152
|
)
|
|
153
153
|
})
|
|
154
154
|
|
|
@@ -209,22 +209,20 @@ describe("DetailView timeline system-events toggle", () => {
|
|
|
209
209
|
const badge = container.querySelector('[data-testid="hidden-count-badge"]')
|
|
210
210
|
expect(badge).not.toBeNull()
|
|
211
211
|
expect(badge?.textContent).toBe("2")
|
|
212
|
-
expect(badge).toHaveClass("min-w-[
|
|
212
|
+
expect(badge).toHaveClass("min-w-[18px]")
|
|
213
213
|
})
|
|
214
214
|
|
|
215
|
-
it("calls localStorage.setItem when toggle changes and shows
|
|
215
|
+
it("calls localStorage.setItem when toggle changes and shows a stronger pressed style", () => {
|
|
216
216
|
const { container } = render(<DetailView {...baseProps()} />)
|
|
217
217
|
expandTimeline(container)
|
|
218
218
|
const toggle = container.querySelector(
|
|
219
219
|
'[data-testid="system-events-toggle"]',
|
|
220
220
|
) as HTMLElement
|
|
221
221
|
expect(toggle).toHaveAttribute("aria-pressed", "false")
|
|
222
|
-
expect(toggle).toHaveAttribute("title", "Score changes are hidden.")
|
|
223
222
|
fireEvent.click(toggle)
|
|
224
223
|
expect(toggle).toHaveAttribute("aria-pressed", "true")
|
|
225
|
-
expect(toggle).
|
|
226
|
-
expect(toggle).
|
|
227
|
-
expect(toggle).toHaveAttribute("title", "Showing 2 score changes.")
|
|
224
|
+
expect(toggle.className).toContain("border-primary/40")
|
|
225
|
+
expect(toggle.className).toContain("bg-primary/10")
|
|
228
226
|
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
|
229
227
|
"test-show-score-changes",
|
|
230
228
|
"true",
|
|
@@ -314,19 +312,18 @@ describe("DetailView timeline system-events toggle", () => {
|
|
|
314
312
|
expect(toggle).toBeNull()
|
|
315
313
|
})
|
|
316
314
|
|
|
317
|
-
it("
|
|
315
|
+
it("shows footer hint below the case-panel timeline when timeline is expanded and system events are hidden", () => {
|
|
318
316
|
const { container } = render(<DetailView {...baseProps()} />)
|
|
319
317
|
expandTimeline(container)
|
|
320
318
|
const timeline = container.querySelector('[data-variant="case-panel"]')
|
|
321
319
|
const hint = container.querySelector('[data-testid="timeline-footer-hint"]')
|
|
322
|
-
const toggle = container.querySelector('[data-testid="system-events-toggle"]')
|
|
323
320
|
expect(timeline).not.toBeNull()
|
|
324
|
-
expect(hint).toBeNull()
|
|
325
|
-
expect(
|
|
326
|
-
expect(
|
|
321
|
+
expect(hint).not.toBeNull()
|
|
322
|
+
expect(hint?.textContent).toBe("Score changes are hidden.")
|
|
323
|
+
expect(timeline?.compareDocumentPosition(hint as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING)
|
|
327
324
|
})
|
|
328
325
|
|
|
329
|
-
it("
|
|
326
|
+
it("shows visible footer hint with count when system events are shown", () => {
|
|
330
327
|
const { container } = render(<DetailView {...baseProps()} />)
|
|
331
328
|
expandTimeline(container)
|
|
332
329
|
// Toggle on
|
|
@@ -335,8 +332,8 @@ describe("DetailView timeline system-events toggle", () => {
|
|
|
335
332
|
) as HTMLElement
|
|
336
333
|
fireEvent.click(toggle)
|
|
337
334
|
const hint = container.querySelector('[data-testid="timeline-footer-hint"]')
|
|
338
|
-
expect(hint).toBeNull()
|
|
339
|
-
expect(
|
|
335
|
+
expect(hint).not.toBeNull()
|
|
336
|
+
expect(hint?.textContent).toBe("Showing 2 score changes.")
|
|
340
337
|
})
|
|
341
338
|
|
|
342
339
|
// --- Toggle always renders when system-noise events exist (review fix #1) ---
|
|
@@ -417,10 +414,10 @@ describe("DetailView timeline system-events toggle", () => {
|
|
|
417
414
|
const toggle = container.querySelector('[data-testid="system-events-toggle"]')
|
|
418
415
|
expect(toggle).not.toBeNull()
|
|
419
416
|
expect(toggle?.textContent).toContain("Legacy label")
|
|
420
|
-
//
|
|
417
|
+
// Footer hint should work too
|
|
421
418
|
expandTimeline(container)
|
|
422
419
|
const hint = container.querySelector('[data-testid="timeline-footer-hint"]')
|
|
423
|
-
expect(hint).toBeNull()
|
|
424
|
-
expect(
|
|
420
|
+
expect(hint).not.toBeNull()
|
|
421
|
+
expect(hint?.textContent).toBe("Legacy hidden hint.")
|
|
425
422
|
})
|
|
426
423
|
})
|
|
@@ -222,7 +222,6 @@ function TimelineSection({
|
|
|
222
222
|
attentionCount,
|
|
223
223
|
sysEvtConfig,
|
|
224
224
|
lastActivityTime,
|
|
225
|
-
isCasePanel = false,
|
|
226
225
|
}: {
|
|
227
226
|
timelineEvents: TimelineEvent[]
|
|
228
227
|
showTimeline: boolean
|
|
@@ -232,7 +231,6 @@ function TimelineSection({
|
|
|
232
231
|
attentionCount?: number
|
|
233
232
|
sysEvtConfig?: TimelineSystemEventsConfig
|
|
234
233
|
lastActivityTime?: string
|
|
235
|
-
isCasePanel?: boolean
|
|
236
234
|
}) {
|
|
237
235
|
// Single-pass partition: compute visibleEvents and hiddenCount together
|
|
238
236
|
const visibleEvents: TimelineEvent[] = []
|
|
@@ -247,9 +245,6 @@ function TimelineSection({
|
|
|
247
245
|
// config was provided — so consumers that emit `isSystemNoise: true` always
|
|
248
246
|
// give users a way to reveal those events.
|
|
249
247
|
const toggleLabel = sysEvtConfig?.toggleLabel ?? "System events"
|
|
250
|
-
const toggleHelp = showSystemEvents
|
|
251
|
-
? sysEvtConfig?.visibleHint?.replace("{count}", String(hiddenCount)) ?? "Hide system events"
|
|
252
|
-
: sysEvtConfig?.hiddenHint ?? "Show system events"
|
|
253
248
|
|
|
254
249
|
// Derive "Last activity" from the first *visible* event so the collapsed
|
|
255
250
|
// header never points at a hidden score-update. The caller-supplied
|
|
@@ -267,106 +262,84 @@ function TimelineSection({
|
|
|
267
262
|
const eventCountLabel = `${visibleCount} ${visibleCount === 1 ? "event" : "events"}`
|
|
268
263
|
|
|
269
264
|
return (
|
|
270
|
-
<div
|
|
271
|
-
className={cn(
|
|
272
|
-
isCasePanel ? "mt-8 border-t border-border pt-8 pb-8" : "mb-8"
|
|
273
|
-
)}
|
|
274
|
-
>
|
|
265
|
+
<div className="mb-8">
|
|
275
266
|
{/* Header — outer non-interactive container */}
|
|
276
267
|
<div
|
|
277
|
-
className=
|
|
278
|
-
"flex w-full items-center justify-between",
|
|
279
|
-
isCasePanel
|
|
280
|
-
? "gap-4 border-b border-border pb-5"
|
|
281
|
-
: "group/timeline gap-2 rounded-md py-2 transition-colors hover:bg-muted/40 -mx-2 px-2"
|
|
282
|
-
)}
|
|
268
|
+
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"
|
|
283
269
|
data-testid="timeline-header"
|
|
284
270
|
>
|
|
285
271
|
{/* Left: collapse/expand button */}
|
|
286
272
|
<button
|
|
287
273
|
type="button"
|
|
288
274
|
onClick={() => setShowTimeline((prev) => !prev)}
|
|
289
|
-
className="flex
|
|
275
|
+
className="flex items-center gap-2 cursor-pointer bg-transparent border-0 p-0"
|
|
290
276
|
data-testid="timeline-collapse-btn"
|
|
291
277
|
>
|
|
292
|
-
<h3 className="text-xs font-bold
|
|
278
|
+
<h3 className="text-xs font-bold text-muted-foreground uppercase tracking-wider group-hover/timeline:text-foreground transition-colors">Activity timeline</h3>
|
|
293
279
|
{!showTimeline && attentionCount != null && attentionCount > 0 && (
|
|
294
280
|
<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">
|
|
295
281
|
{attentionCount} new
|
|
296
282
|
</span>
|
|
297
283
|
)}
|
|
298
|
-
{!
|
|
284
|
+
{!showTimeline && firstVisibleTime && (
|
|
299
285
|
<span className="text-[11px] text-muted-foreground/60" data-testid="last-activity-hint">
|
|
300
286
|
· Last activity {firstVisibleTime}
|
|
301
287
|
</span>
|
|
302
288
|
)}
|
|
289
|
+
<div className="flex items-center gap-1.5">
|
|
290
|
+
<span className="text-[11px] font-medium text-muted-foreground" data-testid="event-count">{eventCountLabel}</span>
|
|
291
|
+
<ChevronDown className={`h-3.5 w-3.5 text-muted-foreground transition-transform duration-200 ${showTimeline ? "rotate-180" : ""}`} />
|
|
292
|
+
</div>
|
|
303
293
|
</button>
|
|
304
294
|
|
|
305
|
-
{/* Right: system-events toggle
|
|
306
|
-
|
|
307
|
-
{hasSystemNoise && (
|
|
308
|
-
<button
|
|
309
|
-
type="button"
|
|
310
|
-
onClick={() => setShowSystemEvents((prev) => !prev)}
|
|
311
|
-
className={cn(
|
|
312
|
-
"inline-flex shrink-0 cursor-pointer items-center gap-3 rounded-full border px-3.5 py-2 text-sm font-semibold transition-colors",
|
|
313
|
-
showSystemEvents
|
|
314
|
-
? "border-foreground bg-foreground text-background shadow-sm hover:bg-foreground/90"
|
|
315
|
-
: "border-border bg-background text-muted-foreground shadow-sm hover:bg-muted/40 hover:text-foreground"
|
|
316
|
-
)}
|
|
317
|
-
aria-pressed={showSystemEvents}
|
|
318
|
-
aria-label={toggleHelp}
|
|
319
|
-
title={toggleHelp}
|
|
320
|
-
data-testid="system-events-toggle"
|
|
321
|
-
>
|
|
322
|
-
<span
|
|
323
|
-
className={cn(
|
|
324
|
-
"relative inline-flex h-4 w-8 shrink-0 items-center rounded-full p-0.5 transition-colors",
|
|
325
|
-
showSystemEvents ? "bg-teal-600" : "bg-muted-foreground/30"
|
|
326
|
-
)}
|
|
327
|
-
aria-hidden="true"
|
|
328
|
-
data-testid="system-events-indicator"
|
|
329
|
-
>
|
|
330
|
-
<span
|
|
331
|
-
className={cn(
|
|
332
|
-
"block h-3 w-3 rounded-full bg-white shadow-sm transition-transform",
|
|
333
|
-
showSystemEvents ? "translate-x-4" : "translate-x-0"
|
|
334
|
-
)}
|
|
335
|
-
/>
|
|
336
|
-
</span>
|
|
337
|
-
<span>{toggleLabel}</span>
|
|
338
|
-
{!showSystemEvents ? (
|
|
339
|
-
<span
|
|
340
|
-
className="inline-flex min-w-[22px] items-center justify-center rounded-full bg-muted px-1.5 text-xs font-bold tabular-nums text-muted-foreground"
|
|
341
|
-
data-testid="hidden-count-badge"
|
|
342
|
-
>
|
|
343
|
-
{hiddenCount}
|
|
344
|
-
</span>
|
|
345
|
-
) : null}
|
|
346
|
-
</button>
|
|
347
|
-
)}
|
|
348
|
-
|
|
295
|
+
{/* Right: system-events toggle — always rendered when noise events exist */}
|
|
296
|
+
{hasSystemNoise && (
|
|
349
297
|
<button
|
|
350
298
|
type="button"
|
|
351
|
-
onClick={() =>
|
|
299
|
+
onClick={() => setShowSystemEvents((prev) => !prev)}
|
|
352
300
|
className={cn(
|
|
353
|
-
"
|
|
354
|
-
|
|
301
|
+
"flex shrink-0 cursor-pointer items-center gap-1.5 rounded-full border px-2.5 py-1 text-[11px] font-medium transition-colors hover:text-foreground",
|
|
302
|
+
showSystemEvents
|
|
303
|
+
? "border-primary/40 bg-primary/10 text-primary shadow-sm hover:bg-primary/15"
|
|
304
|
+
: "border-border bg-background text-muted-foreground hover:bg-muted/40"
|
|
355
305
|
)}
|
|
356
|
-
aria-
|
|
306
|
+
aria-pressed={showSystemEvents}
|
|
307
|
+
data-testid="system-events-toggle"
|
|
357
308
|
>
|
|
358
|
-
|
|
359
|
-
<
|
|
309
|
+
{toggleLabel}
|
|
310
|
+
<span
|
|
311
|
+
className={cn(
|
|
312
|
+
"inline-flex min-w-[18px] items-center justify-center rounded-full px-1.5 text-[10px] font-semibold tabular-nums",
|
|
313
|
+
showSystemEvents
|
|
314
|
+
? "bg-primary/15 text-primary ring-1 ring-primary/30"
|
|
315
|
+
: "bg-muted text-muted-foreground ring-1 ring-border/70"
|
|
316
|
+
)}
|
|
317
|
+
data-testid="hidden-count-badge"
|
|
318
|
+
>
|
|
319
|
+
{hiddenCount}
|
|
320
|
+
</span>
|
|
360
321
|
</button>
|
|
361
|
-
|
|
322
|
+
)}
|
|
362
323
|
</div>
|
|
363
324
|
|
|
364
325
|
{/* Timeline body */}
|
|
365
326
|
{showTimeline && visibleEvents.length > 0 && (
|
|
366
|
-
<div className="mt-
|
|
327
|
+
<div className="mt-3">
|
|
367
328
|
<TimelineActivity events={visibleEvents} variant="case-panel" />
|
|
368
329
|
</div>
|
|
369
330
|
)}
|
|
331
|
+
|
|
332
|
+
{/* Footer hint */}
|
|
333
|
+
{showTimeline && !showSystemEvents && sysEvtConfig?.hiddenHint && hasSystemNoise && (
|
|
334
|
+
<p className="mt-2 text-[11px] text-muted-foreground/60 border-t border-dashed border-border pt-2" data-testid="timeline-footer-hint">
|
|
335
|
+
{sysEvtConfig.hiddenHint}
|
|
336
|
+
</p>
|
|
337
|
+
)}
|
|
338
|
+
{showTimeline && showSystemEvents && sysEvtConfig?.visibleHint && hasSystemNoise && (
|
|
339
|
+
<p className="mt-2 text-[11px] text-muted-foreground/60 border-t border-dashed border-border pt-2" data-testid="timeline-footer-hint">
|
|
340
|
+
{sysEvtConfig.visibleHint.replace("{count}", String(hiddenCount))}
|
|
341
|
+
</p>
|
|
342
|
+
)}
|
|
370
343
|
</div>
|
|
371
344
|
)
|
|
372
345
|
}
|
|
@@ -610,7 +583,6 @@ export function DetailView({
|
|
|
610
583
|
attentionCount={attentionCount}
|
|
611
584
|
sysEvtConfig={sysEvtConfig}
|
|
612
585
|
lastActivityTime={lastActivityTime}
|
|
613
|
-
isCasePanel={isCasePanelV2}
|
|
614
586
|
/>
|
|
615
587
|
) : null
|
|
616
588
|
|