@handled-ai/design-system 0.17.1 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/charts/empty-chart-state.d.ts +11 -0
- package/dist/charts/empty-chart-state.js +70 -0
- package/dist/charts/empty-chart-state.js.map +1 -0
- package/dist/charts/index.d.ts +1 -0
- package/dist/charts/index.js +1 -0
- package/dist/charts/index.js.map +1 -1
- package/dist/charts/pipeline-overview.d.ts +2 -1
- package/dist/charts/pipeline-overview.js +29 -1
- package/dist/charts/pipeline-overview.js.map +1 -1
- package/dist/components/actor-byline.d.ts +3 -0
- package/dist/components/actor-byline.js +5 -0
- package/dist/components/actor-byline.js.map +1 -0
- package/dist/components/days-open-cell.d.ts +16 -0
- package/dist/components/days-open-cell.js +73 -0
- package/dist/components/days-open-cell.js.map +1 -0
- package/dist/components/detail-drawer.d.ts +16 -0
- package/dist/components/detail-drawer.js +45 -0
- package/dist/components/detail-drawer.js.map +1 -0
- 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/insights-filter-bar.d.ts +2 -1
- package/dist/components/insights-filter-bar.js +13 -5
- package/dist/components/insights-filter-bar.js.map +1 -1
- package/dist/components/linked-entity-cell.d.ts +14 -0
- package/dist/components/linked-entity-cell.js +96 -0
- package/dist/components/linked-entity-cell.js.map +1 -0
- package/dist/components/metric-card.d.ts +14 -1
- package/dist/components/metric-card.js +86 -0
- package/dist/components/metric-card.js.map +1 -1
- package/dist/components/performance-metrics-table.d.ts +2 -1
- package/dist/components/performance-metrics-table.js +78 -46
- package/dist/components/performance-metrics-table.js.map +1 -1
- package/dist/components/pill.d.ts +26 -0
- package/dist/components/pill.js +77 -0
- package/dist/components/pill.js.map +1 -0
- package/dist/components/quick-segment.d.ts +13 -0
- package/dist/components/quick-segment.js +96 -0
- package/dist/components/quick-segment.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/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 +13 -4
- package/dist/index.js +17 -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 -332
- package/dist/prototype/prototype-inbox-view.d.ts +2 -1
- package/dist/prototype/prototype-inbox-view.js +11 -12
- 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/charts/__tests__/insights-charts.test.tsx +62 -0
- package/src/charts/empty-chart-state.tsx +44 -0
- package/src/charts/index.ts +1 -0
- package/src/charts/pipeline-overview.tsx +38 -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__/insights-primitives.test.tsx +117 -0
- package/src/components/__tests__/performance-metrics-table.test.tsx +54 -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/__tests__/user-display.test.tsx +75 -0
- package/src/components/actor-byline.tsx +1 -0
- package/src/components/days-open-cell.tsx +50 -0
- package/src/components/detail-drawer.tsx +60 -0
- package/src/components/feedback-primitives.tsx +424 -0
- package/src/components/insights-filter-bar.tsx +13 -4
- package/src/components/linked-entity-cell.tsx +74 -0
- package/src/components/metric-card.tsx +82 -0
- package/src/components/performance-metrics-table.tsx +99 -63
- package/src/components/pill.tsx +67 -0
- package/src/components/quick-segment.tsx +68 -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 +11 -0
- package/src/lib/__tests__/user-display.test.ts +85 -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 -4
- package/src/prototype/prototype-inbox-view.tsx +8 -10
- package/src/prototype/__tests__/detail-view-title-subtext.test.tsx +0 -72
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for SignalPriorityPopover.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Renders trigger chip with correct urgency label
|
|
6
|
+
* - Popover opens on trigger click and shows content
|
|
7
|
+
* - Factor rows render with correct direction labels
|
|
8
|
+
* - Urgency class maps applied based on data-state
|
|
9
|
+
* - Popover closes on second click (toggle)
|
|
10
|
+
* - Feedback footer expands on "Not helpful" click
|
|
11
|
+
* - onFeedbackSubmit callback fires
|
|
12
|
+
* - Default feedback chips used when feedbackChips prop is omitted
|
|
13
|
+
* - Falls back to Activity icon for unknown icon names
|
|
14
|
+
* - No feedback footer when onFeedbackSubmit is not provided
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect, vi } from "vitest"
|
|
18
|
+
import React from "react"
|
|
19
|
+
import { render, screen, fireEvent } from "@testing-library/react"
|
|
20
|
+
import { SignalPriorityPopover } from "../signal-priority-popover"
|
|
21
|
+
import type { PriorityFactor, SignalPriorityPopoverProps } from "../signal-priority-popover"
|
|
22
|
+
import type { FeedbackChipTree } from "../feedback-primitives"
|
|
23
|
+
|
|
24
|
+
// ─── Mock data ───────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const mockFactors: PriorityFactor[] = [
|
|
27
|
+
{
|
|
28
|
+
key: "test_severity",
|
|
29
|
+
label: "Test Transaction Severity",
|
|
30
|
+
icon: "radar",
|
|
31
|
+
tone: "alert",
|
|
32
|
+
direction: "raises",
|
|
33
|
+
score: 85,
|
|
34
|
+
rationale: "Multiple test transactions detected in the last 24 hours.",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: "account_depth",
|
|
38
|
+
label: "Account Relationship Depth",
|
|
39
|
+
icon: "link-2",
|
|
40
|
+
tone: "info",
|
|
41
|
+
direction: "lowers",
|
|
42
|
+
score: 30,
|
|
43
|
+
rationale: "Long-standing account with deep relationship history.",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: "outflow_severity",
|
|
47
|
+
label: "Cumulative Outflow Severity",
|
|
48
|
+
icon: "arrow-up-right",
|
|
49
|
+
tone: "warn",
|
|
50
|
+
direction: "neutral",
|
|
51
|
+
score: 50,
|
|
52
|
+
rationale: "Moderate outflow levels observed.",
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const mockFeedbackChips: FeedbackChipTree[] = [
|
|
57
|
+
{ label: "Wrong factor weighting" },
|
|
58
|
+
{ label: "Missing context" },
|
|
59
|
+
{
|
|
60
|
+
label: "Inaccurate data",
|
|
61
|
+
subPrompt: "Which field?",
|
|
62
|
+
subChips: ["Balance figures", "Counterparty", "Timestamp"],
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
const defaultProps: SignalPriorityPopoverProps = {
|
|
67
|
+
score: 79,
|
|
68
|
+
urgencyLabel: "High",
|
|
69
|
+
urgencyExplanation: "Multiple risk signals detected across treasury accounts.",
|
|
70
|
+
factors: mockFactors,
|
|
71
|
+
metaText: "Updated 4m ago - model v3.2",
|
|
72
|
+
feedbackChips: mockFeedbackChips,
|
|
73
|
+
onFeedbackSubmit: vi.fn(),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
describe("SignalPriorityPopover", () => {
|
|
79
|
+
it("renders the trigger chip with urgency label", () => {
|
|
80
|
+
render(<SignalPriorityPopover {...defaultProps} />)
|
|
81
|
+
const trigger = screen.getByTestId("priority-popover-trigger")
|
|
82
|
+
expect(trigger).toBeTruthy()
|
|
83
|
+
expect(trigger.textContent).toContain("High Priority")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("derives urgency label from score when not provided", () => {
|
|
87
|
+
render(<SignalPriorityPopover {...defaultProps} urgencyLabel={undefined} score={90} />)
|
|
88
|
+
const trigger = screen.getByTestId("priority-popover-trigger")
|
|
89
|
+
expect(trigger.textContent).toContain("Urgent Priority")
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("applies urgency-specific default classes to the trigger", () => {
|
|
93
|
+
render(<SignalPriorityPopover {...defaultProps} urgencyLabel="Urgent" />)
|
|
94
|
+
const trigger = screen.getByTestId("priority-popover-trigger")
|
|
95
|
+
expect(trigger.className).toContain("border-red-200")
|
|
96
|
+
expect(trigger.className).toContain("bg-red-50")
|
|
97
|
+
expect(trigger.className).toContain("text-red-700")
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it("applies Low urgency classes", () => {
|
|
101
|
+
render(<SignalPriorityPopover {...defaultProps} urgencyLabel="Low" score={20} />)
|
|
102
|
+
const trigger = screen.getByTestId("priority-popover-trigger")
|
|
103
|
+
expect(trigger.className).toContain("border-blue-200")
|
|
104
|
+
expect(trigger.className).toContain("bg-blue-50")
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it("opens the popover on trigger click and shows content", () => {
|
|
108
|
+
render(<SignalPriorityPopover {...defaultProps} />)
|
|
109
|
+
|
|
110
|
+
// Content should not be visible initially
|
|
111
|
+
expect(screen.queryByTestId("priority-popover-content")).toBeNull()
|
|
112
|
+
|
|
113
|
+
// Click to open
|
|
114
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
115
|
+
|
|
116
|
+
const content = screen.getByTestId("priority-popover-content")
|
|
117
|
+
expect(content).toBeTruthy()
|
|
118
|
+
|
|
119
|
+
// Check head section
|
|
120
|
+
expect(content.textContent).toContain("Why this is high priority")
|
|
121
|
+
expect(content.textContent).toContain("79")
|
|
122
|
+
expect(content.textContent).toContain("/100")
|
|
123
|
+
expect(content.textContent).toContain("High range")
|
|
124
|
+
expect(content.textContent).toContain("60-79")
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it("renders the urgency explanation synthesis sentence", () => {
|
|
128
|
+
render(<SignalPriorityPopover {...defaultProps} />)
|
|
129
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
130
|
+
|
|
131
|
+
const content = screen.getByTestId("priority-popover-content")
|
|
132
|
+
expect(content.textContent).toContain(
|
|
133
|
+
"Multiple risk signals detected across treasury accounts.",
|
|
134
|
+
)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it("renders factor rows with correct direction labels", () => {
|
|
138
|
+
render(<SignalPriorityPopover {...defaultProps} />)
|
|
139
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
140
|
+
|
|
141
|
+
// "raises" factor
|
|
142
|
+
const raisesRow = screen.getByTestId("factor-row-test_severity")
|
|
143
|
+
expect(raisesRow.textContent).toContain("Test Transaction Severity")
|
|
144
|
+
expect(raisesRow.textContent).toContain("Raises")
|
|
145
|
+
expect(raisesRow.textContent).toContain("85")
|
|
146
|
+
|
|
147
|
+
// "lowers" factor
|
|
148
|
+
const lowersRow = screen.getByTestId("factor-row-account_depth")
|
|
149
|
+
expect(lowersRow.textContent).toContain("Account Relationship Depth")
|
|
150
|
+
expect(lowersRow.textContent).toContain("Lowers")
|
|
151
|
+
expect(lowersRow.textContent).toContain("30")
|
|
152
|
+
|
|
153
|
+
// "neutral" factor
|
|
154
|
+
const neutralRow = screen.getByTestId("factor-row-outflow_severity")
|
|
155
|
+
expect(neutralRow.textContent).toContain("Cumulative Outflow Severity")
|
|
156
|
+
expect(neutralRow.textContent).toContain("Neutral")
|
|
157
|
+
expect(neutralRow.textContent).toContain("50")
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it("renders Contributing factors section label", () => {
|
|
161
|
+
render(<SignalPriorityPopover {...defaultProps} />)
|
|
162
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
163
|
+
|
|
164
|
+
const content = screen.getByTestId("priority-popover-content")
|
|
165
|
+
expect(content.textContent).toContain("Contributing factors")
|
|
166
|
+
expect(content.textContent).toContain("Score = weighted sum")
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it("renders score track bars with correct width percentage", () => {
|
|
170
|
+
render(<SignalPriorityPopover {...defaultProps} />)
|
|
171
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
172
|
+
|
|
173
|
+
const raisesRow = screen.getByTestId("factor-row-test_severity")
|
|
174
|
+
const trackFill = raisesRow.querySelector(".bg-foreground\\/20") as HTMLElement
|
|
175
|
+
expect(trackFill).toBeTruthy()
|
|
176
|
+
expect(trackFill.style.width).toBe("85%")
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it("renders factor rationale text", () => {
|
|
180
|
+
render(<SignalPriorityPopover {...defaultProps} />)
|
|
181
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
182
|
+
|
|
183
|
+
const content = screen.getByTestId("priority-popover-content")
|
|
184
|
+
expect(content.textContent).toContain(
|
|
185
|
+
"Multiple test transactions detected in the last 24 hours.",
|
|
186
|
+
)
|
|
187
|
+
expect(content.textContent).toContain(
|
|
188
|
+
"Long-standing account with deep relationship history.",
|
|
189
|
+
)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it("renders feedback footer with 'Not helpful' button when onFeedbackSubmit is provided", () => {
|
|
193
|
+
render(<SignalPriorityPopover {...defaultProps} />)
|
|
194
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
195
|
+
|
|
196
|
+
const content = screen.getByTestId("priority-popover-content")
|
|
197
|
+
expect(content.textContent).toContain("Not helpful")
|
|
198
|
+
expect(content.textContent).toContain("Helpful")
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it("does not render feedback footer when onFeedbackSubmit is not provided", () => {
|
|
202
|
+
render(
|
|
203
|
+
<SignalPriorityPopover {...defaultProps} onFeedbackSubmit={undefined} />,
|
|
204
|
+
)
|
|
205
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
206
|
+
|
|
207
|
+
const content = screen.getByTestId("priority-popover-content")
|
|
208
|
+
expect(content.textContent).not.toContain("Not helpful")
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it("expands negative feedback chips on 'Not helpful' click", () => {
|
|
212
|
+
render(<SignalPriorityPopover {...defaultProps} />)
|
|
213
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
214
|
+
|
|
215
|
+
// Click "Not helpful" button
|
|
216
|
+
const notHelpfulBtn = screen.getByRole("button", { name: /not helpful/i })
|
|
217
|
+
fireEvent.click(notHelpfulBtn)
|
|
218
|
+
|
|
219
|
+
const content = screen.getByTestId("priority-popover-content")
|
|
220
|
+
// The negative chips should now be visible
|
|
221
|
+
expect(content.textContent).toContain("Wrong factor weighting")
|
|
222
|
+
expect(content.textContent).toContain("Missing context")
|
|
223
|
+
expect(content.textContent).toContain("Inaccurate data")
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it("does not render Contributing factors section when factors array is empty", () => {
|
|
227
|
+
render(<SignalPriorityPopover {...defaultProps} factors={[]} />)
|
|
228
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
229
|
+
|
|
230
|
+
const content = screen.getByTestId("priority-popover-content")
|
|
231
|
+
expect(content.textContent).not.toContain("Contributing factors")
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it("does not render urgency explanation when not provided", () => {
|
|
235
|
+
render(
|
|
236
|
+
<SignalPriorityPopover
|
|
237
|
+
{...defaultProps}
|
|
238
|
+
urgencyExplanation={undefined}
|
|
239
|
+
/>,
|
|
240
|
+
)
|
|
241
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
242
|
+
|
|
243
|
+
const content = screen.getByTestId("priority-popover-content")
|
|
244
|
+
// Should still show the title
|
|
245
|
+
expect(content.textContent).toContain("Why this is high priority")
|
|
246
|
+
// But not the explanation
|
|
247
|
+
expect(content.textContent).not.toContain(
|
|
248
|
+
"Multiple risk signals detected across treasury accounts.",
|
|
249
|
+
)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it("clamps score track width between 0% and 100%", () => {
|
|
253
|
+
const edgeFactors: PriorityFactor[] = [
|
|
254
|
+
{
|
|
255
|
+
key: "over",
|
|
256
|
+
label: "Over 100",
|
|
257
|
+
icon: "activity",
|
|
258
|
+
tone: "alert",
|
|
259
|
+
direction: "raises",
|
|
260
|
+
score: 150,
|
|
261
|
+
rationale: "Exceeds max",
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
key: "under",
|
|
265
|
+
label: "Under 0",
|
|
266
|
+
icon: "activity",
|
|
267
|
+
tone: "info",
|
|
268
|
+
direction: "lowers",
|
|
269
|
+
score: -10,
|
|
270
|
+
rationale: "Below min",
|
|
271
|
+
},
|
|
272
|
+
]
|
|
273
|
+
render(<SignalPriorityPopover {...defaultProps} factors={edgeFactors} />)
|
|
274
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
275
|
+
|
|
276
|
+
const overRow = screen.getByTestId("factor-row-over")
|
|
277
|
+
const overFill = overRow.querySelector(".bg-foreground\\/20") as HTMLElement
|
|
278
|
+
expect(overFill.style.width).toBe("100%")
|
|
279
|
+
|
|
280
|
+
const underRow = screen.getByTestId("factor-row-under")
|
|
281
|
+
const underFill = underRow.querySelector(".bg-foreground\\/20") as HTMLElement
|
|
282
|
+
expect(underFill.style.width).toBe("0%")
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it("renders tone-specific icon background classes", () => {
|
|
286
|
+
render(<SignalPriorityPopover {...defaultProps} />)
|
|
287
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
288
|
+
|
|
289
|
+
// Alert tone should have bg-red-50 text-red-600
|
|
290
|
+
const alertRow = screen.getByTestId("factor-row-test_severity")
|
|
291
|
+
const alertIcon = alertRow.querySelector(".bg-red-50")
|
|
292
|
+
expect(alertIcon).toBeTruthy()
|
|
293
|
+
|
|
294
|
+
// Info tone should have bg-blue-50 text-blue-600
|
|
295
|
+
const infoRow = screen.getByTestId("factor-row-account_depth")
|
|
296
|
+
const infoIcon = infoRow.querySelector(".bg-blue-50")
|
|
297
|
+
expect(infoIcon).toBeTruthy()
|
|
298
|
+
|
|
299
|
+
// Warn tone should have bg-amber-50 text-amber-600
|
|
300
|
+
const warnRow = screen.getByTestId("factor-row-outflow_severity")
|
|
301
|
+
const warnIcon = warnRow.querySelector(".bg-amber-50")
|
|
302
|
+
expect(warnIcon).toBeTruthy()
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it("applies Medium urgency classes correctly", () => {
|
|
306
|
+
render(<SignalPriorityPopover {...defaultProps} urgencyLabel="Medium" score={45} />)
|
|
307
|
+
const trigger = screen.getByTestId("priority-popover-trigger")
|
|
308
|
+
expect(trigger.className).toContain("border-amber-200")
|
|
309
|
+
expect(trigger.className).toContain("bg-amber-50")
|
|
310
|
+
expect(trigger.textContent).toContain("Medium Priority")
|
|
311
|
+
})
|
|
312
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { render, screen } from "@testing-library/react"
|
|
3
|
+
import { describe, expect, it } from "vitest"
|
|
4
|
+
|
|
5
|
+
import { ActorByline } from "../actor-byline"
|
|
6
|
+
import { UserPill } from "../user-pill"
|
|
7
|
+
|
|
8
|
+
describe("UserPill", () => {
|
|
9
|
+
it("renders a display name and derived initials from profile", () => {
|
|
10
|
+
const { container } = render(
|
|
11
|
+
<UserPill profile={{ first_name: "Sarah", last_name: "Mitchell", email: "sarah@example.com" }} />
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
expect(container.querySelector('[data-slot="user-pill"]')).not.toBeNull()
|
|
15
|
+
expect(screen.getByText("Sarah Mitchell")).not.toBeNull()
|
|
16
|
+
expect(screen.getByText("SM")).not.toBeNull()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("does not require profile and can render direct name/email props with subtitle", () => {
|
|
20
|
+
render(<UserPill name="Marcus Webb" email="marcus@example.com" subtitle="Account executive" variant="compact" />)
|
|
21
|
+
|
|
22
|
+
expect(screen.getByText("Marcus Webb")).not.toBeNull()
|
|
23
|
+
expect(screen.getByText("Account executive")).not.toBeNull()
|
|
24
|
+
expect(screen.getByText("MW")).not.toBeNull()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("falls back to email local part and initials from direct email prop", () => {
|
|
28
|
+
render(<UserPill email="fallback@example.com" />)
|
|
29
|
+
|
|
30
|
+
expect(screen.getByText("fallback")).not.toBeNull()
|
|
31
|
+
expect(screen.getByText("FA")).not.toBeNull()
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe("ActorByline", () => {
|
|
36
|
+
it("renders plain byline text with actor name, verb, subject, and string timestamp", () => {
|
|
37
|
+
const { container } = render(
|
|
38
|
+
<ActorByline
|
|
39
|
+
actor={{ first_name: "Cory", last_name: "Pitt", email: "cory@example.com" }}
|
|
40
|
+
verb="closed"
|
|
41
|
+
subject="this opportunity"
|
|
42
|
+
timestamp="3 min ago"
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
expect(container.querySelector('[data-slot="actor-byline"]')).not.toBeNull()
|
|
47
|
+
expect(container.textContent).toBe("Cory Pitt closed this opportunity · 3 min ago")
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
it("renders Date timestamps deterministically", () => {
|
|
52
|
+
const { container } = render(
|
|
53
|
+
<ActorByline
|
|
54
|
+
actor={{ name: "Cory Pitt", email: "cory@withhandled.com" }}
|
|
55
|
+
verb="closed"
|
|
56
|
+
subject="this opportunity"
|
|
57
|
+
timestamp={new Date("2026-05-24T18:30:00.000Z")}
|
|
58
|
+
/>,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
expect(container.textContent).toBe("Cory Pitt closed this opportunity · 2026-05-24T18:30:00.000Z")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("omits missing pieces and null timestamp cleanly", () => {
|
|
65
|
+
const { container } = render(<ActorByline actor={{ name: "Handled AI", email: "agent@handled.ai" }} subject="a task" timestamp={null} />)
|
|
66
|
+
|
|
67
|
+
expect(container.textContent).toBe("Handled AI a task")
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("falls back through shared user-display rules", () => {
|
|
71
|
+
const { container } = render(<ActorByline actor={{ first_name: null, last_name: null, name: null, email: "fallback@example.com" }} />)
|
|
72
|
+
|
|
73
|
+
expect(container.textContent).toBe("fallback")
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ActorByline, type ActorBylineProps } from "./user-display"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
import { StatusPill, type PillStatus } from "./pill"
|
|
7
|
+
|
|
8
|
+
export interface DaysOpenCellProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
9
|
+
days: number | null | undefined
|
|
10
|
+
warningAt?: number
|
|
11
|
+
criticalAt?: number
|
|
12
|
+
emptyLabel?: string
|
|
13
|
+
suffix?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getDaysOpenIntent(days: number, warningAt: number, criticalAt: number): PillStatus {
|
|
17
|
+
if (days >= criticalAt) return "error"
|
|
18
|
+
if (days >= warningAt) return "warning"
|
|
19
|
+
return "success"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function DaysOpenCell({
|
|
23
|
+
days,
|
|
24
|
+
warningAt = 7,
|
|
25
|
+
criticalAt = 30,
|
|
26
|
+
emptyLabel = "—",
|
|
27
|
+
suffix = "d open",
|
|
28
|
+
className,
|
|
29
|
+
...props
|
|
30
|
+
}: DaysOpenCellProps) {
|
|
31
|
+
if (days === null || days === undefined) {
|
|
32
|
+
return (
|
|
33
|
+
<div data-slot="days-open-cell" className={cn("text-sm text-muted-foreground", className)} {...props}>
|
|
34
|
+
{emptyLabel}
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const intent = getDaysOpenIntent(days, warningAt, criticalAt)
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div data-slot="days-open-cell" className={cn("inline-flex items-center", className)} {...props}>
|
|
43
|
+
<StatusPill data-testid="days-open-pill" status={`${days} ${suffix}`} intent={intent}>
|
|
44
|
+
{days} {suffix}
|
|
45
|
+
</StatusPill>
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { getDaysOpenIntent }
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
import {
|
|
7
|
+
Sheet,
|
|
8
|
+
SheetContent,
|
|
9
|
+
SheetDescription,
|
|
10
|
+
SheetFooter,
|
|
11
|
+
SheetHeader,
|
|
12
|
+
SheetTitle,
|
|
13
|
+
} from "./sheet"
|
|
14
|
+
|
|
15
|
+
export interface DetailDrawerProps {
|
|
16
|
+
open: boolean
|
|
17
|
+
onOpenChange: (open: boolean) => void
|
|
18
|
+
title: React.ReactNode
|
|
19
|
+
description?: React.ReactNode
|
|
20
|
+
children: React.ReactNode
|
|
21
|
+
footer?: React.ReactNode
|
|
22
|
+
side?: "right" | "left"
|
|
23
|
+
className?: string
|
|
24
|
+
contentClassName?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function DetailDrawer({
|
|
28
|
+
open,
|
|
29
|
+
onOpenChange,
|
|
30
|
+
title,
|
|
31
|
+
description,
|
|
32
|
+
children,
|
|
33
|
+
footer,
|
|
34
|
+
side = "right",
|
|
35
|
+
className,
|
|
36
|
+
contentClassName,
|
|
37
|
+
}: DetailDrawerProps) {
|
|
38
|
+
return (
|
|
39
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
40
|
+
<SheetContent
|
|
41
|
+
data-slot="detail-drawer"
|
|
42
|
+
side={side}
|
|
43
|
+
className={cn("w-full gap-0 p-0 sm:max-w-xl", className)}
|
|
44
|
+
>
|
|
45
|
+
<SheetHeader data-slot="detail-drawer-header" className="border-b border-border p-5">
|
|
46
|
+
<SheetTitle>{title}</SheetTitle>
|
|
47
|
+
{description ? <SheetDescription>{description}</SheetDescription> : null}
|
|
48
|
+
</SheetHeader>
|
|
49
|
+
<div data-slot="detail-drawer-content" className={cn("flex-1 overflow-y-auto p-5", contentClassName)}>
|
|
50
|
+
{children}
|
|
51
|
+
</div>
|
|
52
|
+
{footer ? (
|
|
53
|
+
<SheetFooter data-slot="detail-drawer-footer" className="border-t border-border p-5">
|
|
54
|
+
{footer}
|
|
55
|
+
</SheetFooter>
|
|
56
|
+
) : null}
|
|
57
|
+
</SheetContent>
|
|
58
|
+
</Sheet>
|
|
59
|
+
)
|
|
60
|
+
}
|