@handled-ai/design-system 0.17.0 → 0.17.2
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/feedback-primitives.d.ts +66 -0
- package/dist/components/feedback-primitives.js +295 -0
- package/dist/components/feedback-primitives.js.map +1 -0
- package/dist/components/score-why-chips.d.ts +8 -17
- package/dist/components/score-why-chips.js +266 -180
- package/dist/components/score-why-chips.js.map +1 -1
- package/dist/components/signal-priority-popover.d.ts +17 -0
- package/dist/components/signal-priority-popover.js +247 -0
- package/dist/components/signal-priority-popover.js.map +1 -0
- package/dist/components/tabs.d.ts +1 -1
- package/dist/components/user-display.d.ts +22 -0
- package/dist/components/user-display.js +138 -0
- package/dist/components/user-display.js.map +1 -0
- package/dist/components/user-pill.d.ts +3 -0
- package/dist/components/user-pill.js +5 -0
- package/dist/components/user-pill.js.map +1 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/user-display.d.ts +31 -0
- package/dist/lib/user-display.js +57 -0
- package/dist/lib/user-display.js.map +1 -0
- package/dist/prototype/index.d.ts +2 -1
- package/dist/prototype/prototype-accounts-view.d.ts +2 -1
- package/dist/prototype/prototype-admin-view.d.ts +2 -1
- package/dist/prototype/prototype-config.d.ts +15 -328
- package/dist/prototype/prototype-inbox-view.d.ts +8 -3
- package/dist/prototype/prototype-inbox-view.js +24 -13
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/dist/prototype/prototype-insights-view.d.ts +2 -1
- package/dist/prototype/prototype-shell.d.ts +2 -1
- package/dist/signal-priority-popover-DQ_VuHac.d.ts +390 -0
- package/package.json +1 -1
- package/src/components/__tests__/contextual-quick-action-launcher.test.tsx +99 -188
- package/src/components/__tests__/feedback-primitives.test.tsx +509 -0
- package/src/components/__tests__/score-why-chips.test.tsx +540 -0
- package/src/components/__tests__/signal-priority-popover.test.tsx +312 -0
- package/src/components/feedback-primitives.tsx +424 -0
- package/src/components/score-why-chips.tsx +413 -203
- package/src/components/signal-priority-popover.tsx +359 -0
- package/src/components/user-display.tsx +96 -0
- package/src/components/user-pill.tsx +1 -0
- package/src/index.ts +6 -0
- package/src/lib/__tests__/user-display.test.ts +43 -0
- package/src/lib/user-display.ts +88 -0
- package/src/prototype/__tests__/detail-view-score-why.test.tsx +33 -29
- package/src/prototype/__tests__/detail-view-title-slots.test.tsx +65 -0
- package/src/prototype/prototype-config.ts +28 -0
- package/src/prototype/prototype-inbox-view.tsx +25 -11
|
@@ -80,16 +80,12 @@ describe("DetailView corrected compact score WHY UX", () => {
|
|
|
80
80
|
|
|
81
81
|
fireEvent.click(screen.getByRole("button", { name: /urgent priority/i }));
|
|
82
82
|
|
|
83
|
+
// The new popover shows urgency explanation and score info in a different format
|
|
83
84
|
expect(screen.getByText("Customer activity spiked today.")).toBeInTheDocument();
|
|
84
|
-
expect(screen.getByText(
|
|
85
|
-
expect(screen.getByText(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
expect(within(priorityPanel).getByText("Strong signals detected.")).toBeInTheDocument();
|
|
89
|
-
expect(screen.getByText("Top factor")).toBeInTheDocument();
|
|
90
|
-
expect(screen.getAllByText("Strong signal").length).toBeGreaterThan(0);
|
|
91
|
-
expect(screen.getByText("Trigger strength")).toBeInTheDocument();
|
|
92
|
-
expect(screen.getByText("Company fit")).toBeInTheDocument();
|
|
85
|
+
expect(screen.getByText(/82/)).toBeInTheDocument();
|
|
86
|
+
expect(screen.getByText(/\/100/)).toBeInTheDocument();
|
|
87
|
+
// Popover shows "Why this is urgent priority" heading
|
|
88
|
+
expect(screen.getByText(/why this is urgent priority/i)).toBeInTheDocument();
|
|
93
89
|
expect(screen.queryByText("How's this score?")).toBeNull();
|
|
94
90
|
});
|
|
95
91
|
|
|
@@ -116,21 +112,40 @@ describe("DetailView corrected compact score WHY UX", () => {
|
|
|
116
112
|
expect(screen.getByRole("region", { name: /treasury activity details/i })).toBeInTheDocument();
|
|
117
113
|
});
|
|
118
114
|
|
|
119
|
-
it("resets selected bucket
|
|
120
|
-
const { rerender } = render(
|
|
115
|
+
it("resets selected bucket when item id changes", () => {
|
|
116
|
+
const { rerender } = render(
|
|
117
|
+
<DetailView
|
|
118
|
+
{...baseProps({
|
|
119
|
+
getSignalScore: () =>
|
|
120
|
+
makeSignalScore({
|
|
121
|
+
explanationBuckets: [
|
|
122
|
+
{ key: "signal-a", label: "Treasury activity", kind: "signal", primarySignalId: "sig-1", signals: [{ id: "sig-1", label: "Treasury signal" }] },
|
|
123
|
+
],
|
|
124
|
+
}),
|
|
125
|
+
})}
|
|
126
|
+
/>,
|
|
127
|
+
);
|
|
121
128
|
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
// Expand a WHY bucket
|
|
130
|
+
fireEvent.click(screen.getByRole("button", { name: /treasury activity/i }));
|
|
131
|
+
expect(screen.getByRole("region", { name: /treasury activity details/i })).toBeInTheDocument();
|
|
124
132
|
|
|
125
133
|
rerender(
|
|
126
134
|
<DetailView
|
|
127
135
|
{...baseProps({
|
|
128
136
|
item: { ...baseItem, id: "case-2", title: "Second Signal" },
|
|
137
|
+
getSignalScore: () =>
|
|
138
|
+
makeSignalScore({
|
|
139
|
+
explanationBuckets: [
|
|
140
|
+
{ key: "signal-a", label: "Treasury activity", kind: "signal", primarySignalId: "sig-1", signals: [{ id: "sig-1", label: "Treasury signal" }] },
|
|
141
|
+
],
|
|
142
|
+
}),
|
|
129
143
|
})}
|
|
130
144
|
/>,
|
|
131
145
|
);
|
|
132
146
|
|
|
133
|
-
|
|
147
|
+
// Bucket should be collapsed after item change
|
|
148
|
+
expect(screen.queryByRole("region", { name: /treasury activity details/i })).toBeNull();
|
|
134
149
|
});
|
|
135
150
|
|
|
136
151
|
it("does not render factor-derived WHY chips when no signal buckets are present", () => {
|
|
@@ -167,7 +182,7 @@ describe("DetailView corrected compact score WHY UX", () => {
|
|
|
167
182
|
expect(screen.queryByRole("button", { name: /company fit/i })).toBeNull();
|
|
168
183
|
});
|
|
169
184
|
|
|
170
|
-
it("exposes aria-expanded and aria-controls for
|
|
185
|
+
it("exposes aria-expanded and aria-controls for bucket triggers", () => {
|
|
171
186
|
render(
|
|
172
187
|
<DetailView
|
|
173
188
|
{...baseProps({
|
|
@@ -181,13 +196,6 @@ describe("DetailView corrected compact score WHY UX", () => {
|
|
|
181
196
|
/>,
|
|
182
197
|
);
|
|
183
198
|
|
|
184
|
-
const priorityButton = screen.getByRole("button", { name: /urgent priority/i });
|
|
185
|
-
expect(priorityButton.getAttribute("aria-expanded")).toBe("false");
|
|
186
|
-
expect(priorityButton.getAttribute("aria-controls")).toBeTruthy();
|
|
187
|
-
fireEvent.click(priorityButton);
|
|
188
|
-
expect(priorityButton.getAttribute("aria-expanded")).toBe("true");
|
|
189
|
-
expect(document.getElementById(priorityButton.getAttribute("aria-controls")!)).toBeInTheDocument();
|
|
190
|
-
|
|
191
199
|
const bucketButton = screen.getByRole("button", { name: /signal a/i });
|
|
192
200
|
expect(bucketButton.getAttribute("aria-expanded")).toBe("false");
|
|
193
201
|
expect(bucketButton.getAttribute("aria-controls")).toBeTruthy();
|
|
@@ -213,7 +221,8 @@ describe("DetailView corrected compact score WHY UX", () => {
|
|
|
213
221
|
);
|
|
214
222
|
|
|
215
223
|
expect(screen.getAllByRole("button", { name: /treasury activity/i })).toHaveLength(1);
|
|
216
|
-
|
|
224
|
+
// Task 3 redesigned WHY pills: count badge now uses "x{N}" format
|
|
225
|
+
expect(screen.getByText(/x\s*3/)).toBeInTheDocument();
|
|
217
226
|
|
|
218
227
|
fireEvent.click(screen.getByRole("button", { name: /treasury activity/i }));
|
|
219
228
|
const matchingSignals = screen.getByRole("list", { name: /matching signals/i });
|
|
@@ -297,7 +306,7 @@ describe("DetailView corrected compact score WHY UX", () => {
|
|
|
297
306
|
});
|
|
298
307
|
});
|
|
299
308
|
|
|
300
|
-
it("
|
|
309
|
+
it("signal WHY panels do not render factor feedback UI", () => {
|
|
301
310
|
const onFactorFeedback = vi.fn();
|
|
302
311
|
render(
|
|
303
312
|
<DetailView
|
|
@@ -317,10 +326,5 @@ describe("DetailView corrected compact score WHY UX", () => {
|
|
|
317
326
|
fireEvent.click(screen.getByRole("button", { name: /signal only/i }));
|
|
318
327
|
const signalPanel = screen.getByRole("region", { name: /signal only details/i });
|
|
319
328
|
expect(within(signalPanel).queryByTitle("This factor is accurate")).toBeNull();
|
|
320
|
-
|
|
321
|
-
fireEvent.click(screen.getByRole("button", { name: /urgent priority/i }));
|
|
322
|
-
const priorityPanel = screen.getByRole("region", { name: /priority explanation/i });
|
|
323
|
-
expect(within(priorityPanel).getAllByTitle("This factor is accurate").length).toBeGreaterThan(0);
|
|
324
|
-
expect(within(priorityPanel).getByText("Needs review")).toBeInTheDocument();
|
|
325
329
|
});
|
|
326
330
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react"
|
|
2
|
+
import { describe, expect, it, vi } from "vitest"
|
|
3
|
+
|
|
4
|
+
import { DetailView } from "../prototype-inbox-view"
|
|
5
|
+
import type { DetailViewProps } from "../prototype-inbox-view"
|
|
6
|
+
|
|
7
|
+
const baseItem = {
|
|
8
|
+
id: "case-1",
|
|
9
|
+
title: "Churn risk case title",
|
|
10
|
+
details: "Case details",
|
|
11
|
+
statusColor: "red",
|
|
12
|
+
time: "1h ago",
|
|
13
|
+
company: "Acme Corp",
|
|
14
|
+
tag1: "Signal",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function renderDetailView(overrides: Partial<DetailViewProps> = {}) {
|
|
18
|
+
const props: DetailViewProps = {
|
|
19
|
+
item: baseItem,
|
|
20
|
+
sections: { signalBrief: false, suggestedActions: false, timeline: false },
|
|
21
|
+
getSignalScore: () => ({
|
|
22
|
+
score: 72,
|
|
23
|
+
factors: [],
|
|
24
|
+
whyNow: "Why now",
|
|
25
|
+
evidence: [],
|
|
26
|
+
confidence: 80,
|
|
27
|
+
urgencyLabel: "High",
|
|
28
|
+
urgencyExplanation: "High priority",
|
|
29
|
+
}),
|
|
30
|
+
buildSuggestedActions: () => [],
|
|
31
|
+
buildSourceItems: () => [],
|
|
32
|
+
accountContacts: [],
|
|
33
|
+
emailSignature: "",
|
|
34
|
+
iconMap: {},
|
|
35
|
+
...overrides,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return render(<DetailView {...props} />)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("DetailView title slots", () => {
|
|
42
|
+
it("renders supporting title text below the main title", () => {
|
|
43
|
+
renderDetailView({
|
|
44
|
+
renderTitleSubtext: (item) => <p data-testid="title-subtext">Full title: {item.title}</p>,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expect(screen.getByTestId("title-subtext").textContent).toBe(
|
|
48
|
+
"Full title: Churn risk case title",
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("renders title extra content beside the main title", () => {
|
|
53
|
+
const onClick = vi.fn()
|
|
54
|
+
|
|
55
|
+
renderDetailView({
|
|
56
|
+
renderTitleExtra: () => (
|
|
57
|
+
<button type="button" onClick={onClick}>
|
|
58
|
+
Quick action
|
|
59
|
+
</button>
|
|
60
|
+
),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
expect(screen.getByRole("button", { name: "Quick action" })).toBeTruthy()
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -15,6 +15,8 @@ import type {
|
|
|
15
15
|
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
|
+
import type { PriorityFactor } from "../components/signal-priority-popover"
|
|
19
|
+
import type { FeedbackChipTree, FeedbackSubmitData } from "../components/feedback-primitives"
|
|
18
20
|
|
|
19
21
|
// ---------------------------------------------------------------------------
|
|
20
22
|
// Shared
|
|
@@ -45,6 +47,16 @@ export interface SignalScoreExplanationSignal {
|
|
|
45
47
|
source?: string
|
|
46
48
|
time?: string
|
|
47
49
|
metric?: string
|
|
50
|
+
/** Signal type name (e.g., "treasury_liquidation"). Used for combined signal component identification. */
|
|
51
|
+
signalTypeName?: string
|
|
52
|
+
/** Primary display value (e.g., "-$1,724,310.11"). */
|
|
53
|
+
primaryValue?: string
|
|
54
|
+
/** Qualifier text (e.g., "100% of balance"). */
|
|
55
|
+
qualifier?: string
|
|
56
|
+
/** Counterparty / destination (e.g., "-> JPMorgan Chase --6042"). */
|
|
57
|
+
counterparty?: string
|
|
58
|
+
/** Component breakdown for combined signals. */
|
|
59
|
+
components?: Array<{ type: string; count: number }>
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
export interface SignalScoreExplanationBucket {
|
|
@@ -62,6 +74,10 @@ export interface SignalScoreExplanationBucket {
|
|
|
62
74
|
signalIds?: string[]
|
|
63
75
|
primarySignalId?: string
|
|
64
76
|
factorKeys?: string[]
|
|
77
|
+
/** Lucide icon name for the bucket type (e.g., "trending-down"). */
|
|
78
|
+
icon?: string
|
|
79
|
+
/** Tonal styling hint for the bucket. */
|
|
80
|
+
tone?: "alert" | "warn" | "info"
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
export interface SignalScoreData {
|
|
@@ -81,6 +97,14 @@ export interface SignalScoreData {
|
|
|
81
97
|
/** @deprecated The compact score UX no longer renders score-level thumbs by default. */
|
|
82
98
|
initialScoreFeedback?: { type: "up" | "down"; pills: string[]; detail: string } | null
|
|
83
99
|
initialFactorFeedback?: Record<string, { type: "up" | "down"; detail: string }>
|
|
100
|
+
/** Priority factors for the popover breakdown. */
|
|
101
|
+
priorityFactors?: PriorityFactor[]
|
|
102
|
+
/** Negative feedback chip tree for the priority popover. */
|
|
103
|
+
priorityFeedbackChips?: FeedbackChipTree[]
|
|
104
|
+
/** Callback when user submits priority-level feedback. */
|
|
105
|
+
onPriorityFeedback?: (data: FeedbackSubmitData) => void
|
|
106
|
+
/** Callback when user submits bucket-level feedback. */
|
|
107
|
+
onBucketFeedback?: (bucketKey: string, data: FeedbackSubmitData) => void
|
|
84
108
|
/** AI-generated signal brief text. When present, rendered in a dedicated section. */
|
|
85
109
|
signalBrief?: string
|
|
86
110
|
/** Compact label for time-remaining chip (e.g., "13 days left"). */
|
|
@@ -172,6 +196,10 @@ export interface InboxViewConfig {
|
|
|
172
196
|
renderAfterScore?: (item: QueueItem) => React.ReactNode
|
|
173
197
|
/** Formatted string for "Last activity X ago" in the collapsed timeline header. If omitted, falls back to the first event's time. */
|
|
174
198
|
lastActivityTime?: string
|
|
199
|
+
/** Render extra content inline with the detail title. */
|
|
200
|
+
renderTitleExtra?: (item: QueueItem) => React.ReactNode
|
|
201
|
+
/** Render supporting content below the detail title. */
|
|
202
|
+
renderTitleSubtext?: (item: QueueItem) => React.ReactNode
|
|
175
203
|
/** Sort options for the inbox. When provided, a sort dropdown is rendered in the split view toolbar. */
|
|
176
204
|
sortOptions?: InboxSortOption[]
|
|
177
205
|
/** Currently active sort option id. */
|
|
@@ -38,7 +38,8 @@ import {
|
|
|
38
38
|
} from "../components/inbox-toolbar"
|
|
39
39
|
import { GroupedListView, type GroupedListGroup } from "../components/item-list"
|
|
40
40
|
import { SignalApproval, type ApprovalState, type OpportunityPreview } from "../components/signal-feedback-inline"
|
|
41
|
-
import { ScoreWhyChips
|
|
41
|
+
import { ScoreWhyChips } from "../components/score-why-chips"
|
|
42
|
+
import { SignalPriorityPopover } from "../components/signal-priority-popover"
|
|
42
43
|
import { type SourceDef } from "../components/detail-view"
|
|
43
44
|
import {
|
|
44
45
|
SuggestedActions,
|
|
@@ -137,6 +138,10 @@ export interface DetailViewProps {
|
|
|
137
138
|
/** Render content between the signal score section and the activity timeline. */
|
|
138
139
|
renderAfterScore?: (item: QueueItem) => React.ReactNode
|
|
139
140
|
lastActivityTime?: string
|
|
141
|
+
/** Render extra content inline with the detail title. */
|
|
142
|
+
renderTitleExtra?: (item: QueueItem) => React.ReactNode
|
|
143
|
+
/** Render supporting content below the detail title. */
|
|
144
|
+
renderTitleSubtext?: (item: QueueItem) => React.ReactNode
|
|
140
145
|
/** Render extra metadata chips (e.g. assignee) inside the chips row below the title. */
|
|
141
146
|
renderMetadataExtra?: (item: QueueItem) => React.ReactNode
|
|
142
147
|
onOpenSignalBucket?: (args: { item: QueueItem; bucketKey: string; signalId: string }) => void
|
|
@@ -171,6 +176,8 @@ export function DetailView({
|
|
|
171
176
|
renderBeforeScore,
|
|
172
177
|
renderAfterScore,
|
|
173
178
|
lastActivityTime,
|
|
179
|
+
renderTitleExtra,
|
|
180
|
+
renderTitleSubtext,
|
|
174
181
|
renderMetadataExtra,
|
|
175
182
|
onOpenSignalBucket,
|
|
176
183
|
approveButtonIconUrl,
|
|
@@ -180,13 +187,10 @@ export function DetailView({
|
|
|
180
187
|
}: DetailViewProps) {
|
|
181
188
|
const [showTimeline, setShowTimeline] = React.useState(false)
|
|
182
189
|
const [extraActions, setExtraActions] = React.useState<SuggestedAction[]>([])
|
|
183
|
-
const [priorityOpen, setPriorityOpen] = React.useState(false)
|
|
184
|
-
const priorityPanelId = React.useId()
|
|
185
190
|
|
|
186
191
|
React.useEffect(() => {
|
|
187
192
|
setShowTimeline(false)
|
|
188
193
|
setExtraActions([])
|
|
189
|
-
setPriorityOpen(false)
|
|
190
194
|
}, [item.id])
|
|
191
195
|
|
|
192
196
|
const signalData = React.useMemo(
|
|
@@ -251,15 +255,23 @@ export function DetailView({
|
|
|
251
255
|
<span className="text-xs text-muted-foreground">{item.company}</span>
|
|
252
256
|
</div>
|
|
253
257
|
|
|
254
|
-
<
|
|
258
|
+
<div className="mb-3 flex flex-wrap items-start gap-x-3 gap-y-2">
|
|
259
|
+
<div className="min-w-0 flex-1">
|
|
260
|
+
<h1 className="text-2xl font-bold tracking-tight text-foreground">{item.title}</h1>
|
|
261
|
+
{renderTitleSubtext?.(item)}
|
|
262
|
+
</div>
|
|
263
|
+
{renderTitleExtra?.(item)}
|
|
264
|
+
</div>
|
|
255
265
|
|
|
256
266
|
<div className="mb-6 flex flex-wrap items-center gap-2">
|
|
257
|
-
<
|
|
267
|
+
<SignalPriorityPopover
|
|
258
268
|
score={signalData.score}
|
|
259
269
|
urgencyLabel={signalData.urgencyLabel}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
270
|
+
urgencyExplanation={signalData.urgencyExplanation ?? signalData.signalBrief}
|
|
271
|
+
factors={signalData.priorityFactors ?? []}
|
|
272
|
+
metaText={undefined}
|
|
273
|
+
feedbackChips={signalData.priorityFeedbackChips}
|
|
274
|
+
onFeedbackSubmit={signalData.onPriorityFeedback}
|
|
263
275
|
/>
|
|
264
276
|
{signalData.timeChipLabel && (
|
|
265
277
|
<Badge variant="outline" title={signalData.timeChipDetail ?? undefined}>
|
|
@@ -280,8 +292,6 @@ export function DetailView({
|
|
|
280
292
|
{renderMetadataExtra?.(item)}
|
|
281
293
|
</div>
|
|
282
294
|
|
|
283
|
-
{priorityOpen && <SignalPriorityPanel id={priorityPanelId} signalData={signalData} className="mb-6" />}
|
|
284
|
-
|
|
285
295
|
{/* Signal Brief */}
|
|
286
296
|
{sections.signalBrief && (() => {
|
|
287
297
|
const briefHeading = signalBriefCopy?.heading ?? "Signal brief"
|
|
@@ -440,6 +450,8 @@ export function PrototypeInboxView({
|
|
|
440
450
|
renderBeforeScore,
|
|
441
451
|
renderAfterScore,
|
|
442
452
|
lastActivityTime,
|
|
453
|
+
renderTitleExtra,
|
|
454
|
+
renderTitleSubtext,
|
|
443
455
|
sortOptions,
|
|
444
456
|
activeSortId,
|
|
445
457
|
onSortChange,
|
|
@@ -678,6 +690,8 @@ export function PrototypeInboxView({
|
|
|
678
690
|
renderBeforeScore,
|
|
679
691
|
renderAfterScore,
|
|
680
692
|
lastActivityTime,
|
|
693
|
+
renderTitleExtra,
|
|
694
|
+
renderTitleSubtext,
|
|
681
695
|
onOpenSignalBucket,
|
|
682
696
|
}
|
|
683
697
|
|