@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.
Files changed (101) hide show
  1. package/dist/charts/empty-chart-state.d.ts +11 -0
  2. package/dist/charts/empty-chart-state.js +70 -0
  3. package/dist/charts/empty-chart-state.js.map +1 -0
  4. package/dist/charts/index.d.ts +1 -0
  5. package/dist/charts/index.js +1 -0
  6. package/dist/charts/index.js.map +1 -1
  7. package/dist/charts/pipeline-overview.d.ts +2 -1
  8. package/dist/charts/pipeline-overview.js +29 -1
  9. package/dist/charts/pipeline-overview.js.map +1 -1
  10. package/dist/components/actor-byline.d.ts +3 -0
  11. package/dist/components/actor-byline.js +5 -0
  12. package/dist/components/actor-byline.js.map +1 -0
  13. package/dist/components/days-open-cell.d.ts +16 -0
  14. package/dist/components/days-open-cell.js +73 -0
  15. package/dist/components/days-open-cell.js.map +1 -0
  16. package/dist/components/detail-drawer.d.ts +16 -0
  17. package/dist/components/detail-drawer.js +45 -0
  18. package/dist/components/detail-drawer.js.map +1 -0
  19. package/dist/components/feedback-primitives.d.ts +66 -0
  20. package/dist/components/feedback-primitives.js +295 -0
  21. package/dist/components/feedback-primitives.js.map +1 -0
  22. package/dist/components/insights-filter-bar.d.ts +2 -1
  23. package/dist/components/insights-filter-bar.js +13 -5
  24. package/dist/components/insights-filter-bar.js.map +1 -1
  25. package/dist/components/linked-entity-cell.d.ts +14 -0
  26. package/dist/components/linked-entity-cell.js +96 -0
  27. package/dist/components/linked-entity-cell.js.map +1 -0
  28. package/dist/components/metric-card.d.ts +14 -1
  29. package/dist/components/metric-card.js +86 -0
  30. package/dist/components/metric-card.js.map +1 -1
  31. package/dist/components/performance-metrics-table.d.ts +2 -1
  32. package/dist/components/performance-metrics-table.js +78 -46
  33. package/dist/components/performance-metrics-table.js.map +1 -1
  34. package/dist/components/pill.d.ts +26 -0
  35. package/dist/components/pill.js +77 -0
  36. package/dist/components/pill.js.map +1 -0
  37. package/dist/components/quick-segment.d.ts +13 -0
  38. package/dist/components/quick-segment.js +96 -0
  39. package/dist/components/quick-segment.js.map +1 -0
  40. package/dist/components/score-why-chips.d.ts +8 -17
  41. package/dist/components/score-why-chips.js +266 -180
  42. package/dist/components/score-why-chips.js.map +1 -1
  43. package/dist/components/signal-priority-popover.d.ts +17 -0
  44. package/dist/components/signal-priority-popover.js +247 -0
  45. package/dist/components/signal-priority-popover.js.map +1 -0
  46. package/dist/components/user-display.d.ts +22 -0
  47. package/dist/components/user-display.js +138 -0
  48. package/dist/components/user-display.js.map +1 -0
  49. package/dist/components/user-pill.d.ts +3 -0
  50. package/dist/components/user-pill.js +5 -0
  51. package/dist/components/user-pill.js.map +1 -0
  52. package/dist/index.d.ts +13 -4
  53. package/dist/index.js +17 -0
  54. package/dist/index.js.map +1 -1
  55. package/dist/lib/user-display.d.ts +31 -0
  56. package/dist/lib/user-display.js +57 -0
  57. package/dist/lib/user-display.js.map +1 -0
  58. package/dist/prototype/index.d.ts +2 -1
  59. package/dist/prototype/prototype-accounts-view.d.ts +2 -1
  60. package/dist/prototype/prototype-admin-view.d.ts +2 -1
  61. package/dist/prototype/prototype-config.d.ts +15 -332
  62. package/dist/prototype/prototype-inbox-view.d.ts +2 -1
  63. package/dist/prototype/prototype-inbox-view.js +11 -12
  64. package/dist/prototype/prototype-inbox-view.js.map +1 -1
  65. package/dist/prototype/prototype-insights-view.d.ts +2 -1
  66. package/dist/prototype/prototype-shell.d.ts +2 -1
  67. package/dist/signal-priority-popover-DQ_VuHac.d.ts +390 -0
  68. package/package.json +1 -1
  69. package/src/charts/__tests__/insights-charts.test.tsx +62 -0
  70. package/src/charts/empty-chart-state.tsx +44 -0
  71. package/src/charts/index.ts +1 -0
  72. package/src/charts/pipeline-overview.tsx +38 -1
  73. package/src/components/__tests__/contextual-quick-action-launcher.test.tsx +99 -188
  74. package/src/components/__tests__/feedback-primitives.test.tsx +509 -0
  75. package/src/components/__tests__/insights-primitives.test.tsx +117 -0
  76. package/src/components/__tests__/performance-metrics-table.test.tsx +54 -0
  77. package/src/components/__tests__/score-why-chips.test.tsx +540 -0
  78. package/src/components/__tests__/signal-priority-popover.test.tsx +312 -0
  79. package/src/components/__tests__/user-display.test.tsx +75 -0
  80. package/src/components/actor-byline.tsx +1 -0
  81. package/src/components/days-open-cell.tsx +50 -0
  82. package/src/components/detail-drawer.tsx +60 -0
  83. package/src/components/feedback-primitives.tsx +424 -0
  84. package/src/components/insights-filter-bar.tsx +13 -4
  85. package/src/components/linked-entity-cell.tsx +74 -0
  86. package/src/components/metric-card.tsx +82 -0
  87. package/src/components/performance-metrics-table.tsx +99 -63
  88. package/src/components/pill.tsx +67 -0
  89. package/src/components/quick-segment.tsx +68 -0
  90. package/src/components/score-why-chips.tsx +413 -203
  91. package/src/components/signal-priority-popover.tsx +359 -0
  92. package/src/components/user-display.tsx +96 -0
  93. package/src/components/user-pill.tsx +1 -0
  94. package/src/index.ts +11 -0
  95. package/src/lib/__tests__/user-display.test.ts +85 -0
  96. package/src/lib/user-display.ts +88 -0
  97. package/src/prototype/__tests__/detail-view-score-why.test.tsx +33 -29
  98. package/src/prototype/__tests__/detail-view-title-slots.test.tsx +65 -0
  99. package/src/prototype/prototype-config.ts +28 -4
  100. package/src/prototype/prototype-inbox-view.tsx +8 -10
  101. 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
+ }