@handled-ai/design-system 0.19.0-rc.2 → 0.19.0

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 (40) hide show
  1. package/dist/components/badge.d.ts +1 -1
  2. package/dist/components/button.d.ts +1 -1
  3. package/dist/components/detail-view.js +1 -1
  4. package/dist/components/detail-view.js.map +1 -1
  5. package/dist/components/pill.d.ts +1 -1
  6. package/dist/components/score-why-chips.d.ts +1 -1
  7. package/dist/components/signal-feedback-inline.d.ts +2 -0
  8. package/dist/components/signal-feedback-inline.js +21 -4
  9. package/dist/components/signal-feedback-inline.js.map +1 -1
  10. package/dist/components/signal-priority-popover.d.ts +1 -1
  11. package/dist/components/signal-priority-popover.js +16 -7
  12. package/dist/components/signal-priority-popover.js.map +1 -1
  13. package/dist/components/tabs.d.ts +1 -1
  14. package/dist/components/virtualized-data-table.js +4 -4
  15. package/dist/components/virtualized-data-table.js.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/prototype/index.d.ts +1 -1
  19. package/dist/prototype/prototype-accounts-view.d.ts +1 -1
  20. package/dist/prototype/prototype-admin-view.d.ts +1 -1
  21. package/dist/prototype/prototype-config.d.ts +1 -1
  22. package/dist/prototype/prototype-inbox-view.d.ts +1 -1
  23. package/dist/prototype/prototype-inbox-view.js +17 -1
  24. package/dist/prototype/prototype-inbox-view.js.map +1 -1
  25. package/dist/prototype/prototype-insights-view.d.ts +1 -1
  26. package/dist/prototype/prototype-shell.d.ts +1 -1
  27. package/dist/{signal-priority-popover-QJngMAj7.d.ts → signal-priority-popover-CZitE9xq.d.ts} +11 -2
  28. package/package.json +1 -1
  29. package/src/components/__tests__/signal-priority-popover.test.tsx +41 -4
  30. package/src/components/__tests__/virtualized-data-table-resize.test.tsx +18 -0
  31. package/src/components/detail-view.tsx +3 -1
  32. package/src/components/signal-feedback-inline.tsx +18 -1
  33. package/src/components/signal-priority-popover.tsx +19 -6
  34. package/src/components/virtualized-data-table.tsx +4 -4
  35. package/src/index.ts +1 -1
  36. package/src/prototype/__tests__/detail-view-opportunity-preview.test.tsx +76 -1
  37. package/src/prototype/__tests__/detail-view-score-why.test.tsx +34 -0
  38. package/src/prototype/__tests__/detail-view-title-slots.test.tsx +2 -30
  39. package/src/prototype/prototype-config.ts +5 -1
  40. package/src/prototype/prototype-inbox-view.tsx +10 -0
@@ -50,8 +50,14 @@ export interface PriorityFactor {
50
50
  rationale: string
51
51
  }
52
52
 
53
+ export type SignalPriorityScoreDisplay = "label" | "number"
54
+
53
55
  export interface SignalPriorityPopoverProps {
54
56
  score: number
57
+ /** Controls whether the overall score number is shown in the popover header. @default "number" */
58
+ scoreDisplay?: SignalPriorityScoreDisplay
59
+ /** Short formula/context label shown beside the contributing factors heading. @default "Priority factors" */
60
+ formulaLabel?: string
55
61
  urgencyLabel?: SignalScoreUrgencyLabel
56
62
  /** Synthesis sentence displayed in the popover head. */
57
63
  urgencyExplanation?: string
@@ -282,6 +288,8 @@ export function SignalPriorityPopover({
282
288
  initialFactorFeedback,
283
289
  onFactorFeedback,
284
290
  initialPriorityFeedback,
291
+ scoreDisplay = "number",
292
+ formulaLabel = "Priority factors",
285
293
  }: SignalPriorityPopoverProps) {
286
294
  const urgencyLabel = providedLabel ?? getUrgencyLevel(score)
287
295
  const scoreRange = getUrgencyRange(urgencyLabel)
@@ -330,15 +338,20 @@ export function SignalPriorityPopover({
330
338
  data-testid="priority-popover-content"
331
339
  >
332
340
  {/* Head section */}
333
- <div className="p-4 pb-3">
341
+ <div className="p-4 pb-3" data-testid="priority-popover-header">
334
342
  <div className="flex items-start justify-between gap-3">
335
343
  <p className="text-sm font-semibold text-foreground">
336
344
  Why this is {urgencyLabel.toLowerCase()} priority
337
345
  </p>
338
- <span className="text-2xl font-bold tabular-nums text-foreground">
339
- {score}
340
- <span className="text-sm font-normal text-muted-foreground">/100</span>
341
- </span>
346
+ {scoreDisplay === "number" && (
347
+ <span
348
+ className="text-2xl font-bold tabular-nums text-foreground"
349
+ data-testid="priority-overall-score"
350
+ >
351
+ {score}
352
+ <span className="text-sm font-normal text-muted-foreground">/100</span>
353
+ </span>
354
+ )}
342
355
  </div>
343
356
 
344
357
  {/* Band indicator */}
@@ -369,7 +382,7 @@ export function SignalPriorityPopover({
369
382
  </span>
370
383
  <span className="flex items-center gap-1 text-[10px] text-muted-foreground">
371
384
  <Info className="h-3 w-3" />
372
- Score = weighted sum
385
+ {formulaLabel}
373
386
  </span>
374
387
  </div>
375
388
 
@@ -375,10 +375,10 @@ export function VirtualizedDataTable<TData>({
375
375
  onMouseDown={header.getResizeHandler()}
376
376
  onTouchStart={header.getResizeHandler()}
377
377
  className={cn(
378
- "absolute right-0 top-0 h-full w-3 -mr-1.5 cursor-col-resize select-none touch-none",
379
- "after:absolute after:right-1.5 after:top-0 after:h-full after:w-px",
380
- "after:bg-transparent hover:after:bg-primary/30",
381
- header.column.getIsResizing() && "after:bg-primary/50",
378
+ "absolute right-0 top-0 z-20 h-full w-4 -mr-2 cursor-col-resize select-none touch-none",
379
+ "after:absolute after:right-2 after:top-1 after:h-[calc(100%-0.5rem)] after:w-px after:rounded-full",
380
+ "after:bg-border/70 after:transition-colors hover:after:bg-primary/60",
381
+ header.column.getIsResizing() && "after:bg-primary/70",
382
382
  )}
383
383
  role="separator"
384
384
  aria-orientation="vertical"
package/src/index.ts CHANGED
@@ -52,7 +52,7 @@ export * from "./components/related-record-action-card"
52
52
  export { FeedbackFooter, FeedbackChipGroup, FeedbackInput, FeedbackActions, InlineFeedbackControl } from "./components/feedback-primitives"
53
53
  export type { FeedbackFooterProps, FeedbackChipTree, FeedbackChipGroupProps, FeedbackInputProps, FeedbackActionsProps, FeedbackSubmitData, PersistedFeedbackData, InlineFeedbackControlProps } from "./components/feedback-primitives"
54
54
  export { SignalPriorityPopover } from "./components/signal-priority-popover"
55
- export type { SignalPriorityPopoverProps, PriorityFactor } from "./components/signal-priority-popover"
55
+ export type { SignalPriorityPopoverProps, SignalPriorityScoreDisplay, PriorityFactor } from "./components/signal-priority-popover"
56
56
  export * from "./components/filter-chip"
57
57
  export * from "./components/inbox-row"
58
58
  export * from "./components/inbox-toolbar"
@@ -1,5 +1,5 @@
1
1
  import React from "react"
2
- import { describe, expect, it } from "vitest"
2
+ import { describe, expect, it, vi } from "vitest"
3
3
  import { fireEvent, render, screen } from "@testing-library/react"
4
4
 
5
5
  import { DetailView, type DetailViewProps } from "../prototype-inbox-view"
@@ -66,6 +66,7 @@ describe("DetailView opportunity approval preview", () => {
66
66
  description: "Initial description",
67
67
  churnType: "Churn Risk",
68
68
  churnTypeOptions: ["Churn Risk", "Win Back"],
69
+ nextStep: "Initial next step",
69
70
  })
70
71
  },
71
72
  })}
@@ -84,7 +85,81 @@ describe("DetailView opportunity approval preview", () => {
84
85
  expect((await screen.findByLabelText("Close Date") as HTMLInputElement).value).toBe("2026-06-30")
85
86
  expect((screen.getByLabelText("Amount") as HTMLInputElement).value).toBe("$75,000")
86
87
  expect((screen.getByLabelText("Churn Type") as HTMLSelectElement).value).toBe("Churn Risk")
88
+ expect((screen.getByLabelText("Next Step") as HTMLTextAreaElement).value).toBe("Initial next step")
87
89
  expect((screen.getByLabelText("Description") as HTMLTextAreaElement).value).toBe("Initial description")
88
90
  expect((screen.getByRole("button", { name: /confirm/i }) as HTMLButtonElement).disabled).toBe(false)
89
91
  })
92
+
93
+ it("passes edited next step with the opportunity draft on confirm", async () => {
94
+ const onSignalApprove = vi.fn()
95
+
96
+ render(
97
+ <DetailView
98
+ {...baseProps({
99
+ getSignalApprovalState: () => "confirming",
100
+ opportunityPreview: {
101
+ name: "Churn Risk - WIT-825 Fixture Account",
102
+ accountName: "WIT-825 Fixture Account",
103
+ stage: "Prospecting",
104
+ closeDate: "Jun 30, 2026",
105
+ closeDateValue: "2026-06-30",
106
+ amount: "$75,000",
107
+ amountValue: 75000,
108
+ description: "Initial description",
109
+ churnType: "Churn Risk",
110
+ churnTypeOptions: ["Churn Risk", "Win Back"],
111
+ nextStep: "Initial next step",
112
+ },
113
+ onSignalApprove,
114
+ })}
115
+ />,
116
+ )
117
+
118
+ const nextStepInput = screen.getByLabelText("Next Step") as HTMLTextAreaElement
119
+ fireEvent.change(nextStepInput, { target: { value: "Schedule validation call" } })
120
+ fireEvent.click(screen.getByRole("button", { name: /confirm/i }))
121
+
122
+ expect(onSignalApprove).toHaveBeenCalledWith(baseItem, {
123
+ closeDate: "2026-06-30",
124
+ amount: "$75,000",
125
+ churnType: "Churn Risk",
126
+ description: "Initial description",
127
+ nextStep: "Schedule validation call",
128
+ })
129
+ })
130
+
131
+ it("omits untouched empty next step from the opportunity draft on confirm", async () => {
132
+ const onSignalApprove = vi.fn()
133
+
134
+ render(
135
+ <DetailView
136
+ {...baseProps({
137
+ getSignalApprovalState: () => "confirming",
138
+ opportunityPreview: {
139
+ name: "Churn Risk - WIT-825 Fixture Account",
140
+ accountName: "WIT-825 Fixture Account",
141
+ stage: "Prospecting",
142
+ closeDate: "Jun 30, 2026",
143
+ closeDateValue: "2026-06-30",
144
+ amount: "$75,000",
145
+ amountValue: 75000,
146
+ description: "Initial description",
147
+ churnType: "Churn Risk",
148
+ churnTypeOptions: ["Churn Risk", "Win Back"],
149
+ },
150
+ onSignalApprove,
151
+ })}
152
+ />,
153
+ )
154
+
155
+ expect((screen.getByLabelText("Next Step") as HTMLTextAreaElement).value).toBe("")
156
+ fireEvent.click(screen.getByRole("button", { name: /confirm/i }))
157
+
158
+ expect(onSignalApprove).toHaveBeenCalledWith(baseItem, {
159
+ closeDate: "2026-06-30",
160
+ amount: "$75,000",
161
+ churnType: "Churn Risk",
162
+ description: "Initial description",
163
+ })
164
+ })
90
165
  })
@@ -89,6 +89,40 @@ describe("DetailView corrected compact score WHY UX", () => {
89
89
  expect(screen.queryByText("How's this score?")).toBeNull();
90
90
  });
91
91
 
92
+
93
+ it("passes priorityScoreDisplay and priorityFormulaLabel through to the priority popover", () => {
94
+ render(
95
+ <DetailView
96
+ {...baseProps({
97
+ getSignalScore: () =>
98
+ makeSignalScore({
99
+ urgencyLabel: "High",
100
+ priorityScoreDisplay: "label",
101
+ priorityFormulaLabel: "Priority = weighted signals + calibration",
102
+ priorityFactors: [
103
+ {
104
+ key: "treasury",
105
+ label: "Treasury activity",
106
+ icon: "wallet",
107
+ tone: "alert",
108
+ direction: "raises",
109
+ score: 85,
110
+ rationale: "Full liquidation detected.",
111
+ },
112
+ ],
113
+ }),
114
+ })}
115
+ />,
116
+ );
117
+
118
+ fireEvent.click(screen.getByRole("button", { name: /high priority/i }));
119
+
120
+ expect(screen.queryByTestId("priority-overall-score")).toBeNull();
121
+ expect(screen.getByTestId("priority-popover-header").textContent).not.toContain("82/100");
122
+ expect(screen.getByText("Priority = weighted signals + calibration")).toBeInTheDocument();
123
+ expect(screen.getByTestId("factor-row-treasury").textContent).toContain("85/100");
124
+ });
125
+
92
126
  it("keeps signal WHY chips collapsed by default and excludes factor-only buckets", () => {
93
127
  render(
94
128
  <DetailView
@@ -1,5 +1,5 @@
1
- import { cleanup, render, screen } from "@testing-library/react"
2
- import { afterEach, describe, expect, it, vi } from "vitest"
1
+ import { render, screen } from "@testing-library/react"
2
+ import { describe, expect, it, vi } from "vitest"
3
3
 
4
4
  import { DetailView } from "../prototype-inbox-view"
5
5
  import type { DetailViewProps } from "../prototype-inbox-view"
@@ -14,10 +14,6 @@ const baseItem = {
14
14
  tag1: "Signal",
15
15
  }
16
16
 
17
- afterEach(() => {
18
- cleanup()
19
- })
20
-
21
17
  function renderDetailView(overrides: Partial<DetailViewProps> = {}) {
22
18
  const props: DetailViewProps = {
23
19
  item: baseItem,
@@ -43,30 +39,6 @@ function renderDetailView(overrides: Partial<DetailViewProps> = {}) {
43
39
  }
44
40
 
45
41
  describe("DetailView title slots", () => {
46
- it("does not render the inert detail Back button while preserving title and metadata slots", () => {
47
- renderDetailView({
48
- renderTitleActionRow: () => (
49
- <button type="button">Full-width quick action</button>
50
- ),
51
- renderMetadataExtra: () => <span data-testid="metadata-extra">Owner: Lee</span>,
52
- accountDetailsButtonLabel: (item) => (
53
- <>
54
- <span>View account details</span>
55
- <span>{item.company}</span>
56
- </>
57
- ),
58
- getAccountDetailsButtonAriaLabel: (item) => `View account details for ${item.company}`,
59
- })
60
-
61
- expect(screen.queryByRole("button", { name: "Back" })).toBeNull()
62
- expect(screen.getByRole("heading", { name: "Churn risk case title" })).toBeTruthy()
63
- expect(screen.getAllByText("Acme Corp").length).toBeGreaterThan(0)
64
- expect(screen.getByRole("button", { name: "Full-width quick action" })).toBeTruthy()
65
- expect(screen.getByTestId("metadata-extra").textContent).toBe("Owner: Lee")
66
- expect(screen.getByTestId("priority-popover-trigger")).toBeTruthy()
67
- expect(screen.getByRole("button", { name: "View account details for Acme Corp" })).toBeTruthy()
68
- })
69
-
70
42
  it("renders supporting title text below the main title", () => {
71
43
  renderDetailView({
72
44
  renderTitleSubtext: (item) => <p data-testid="title-subtext">Full title: {item.title}</p>,
@@ -15,7 +15,7 @@ import type {
15
15
  import type { TimelineEvent } from "../components/timeline-activity"
16
16
  import type { ApprovalState, OpportunityDraft } from "../components/signal-feedback-inline"
17
17
  import type { LucideIcon } from "lucide-react"
18
- import type { PriorityFactor } from "../components/signal-priority-popover"
18
+ import type { PriorityFactor, SignalPriorityScoreDisplay } from "../components/signal-priority-popover"
19
19
  import type { FeedbackChipTree, FeedbackSubmitData, PersistedFeedbackData } from "../components/feedback-primitives"
20
20
 
21
21
  // ---------------------------------------------------------------------------
@@ -109,6 +109,10 @@ export interface SignalScoreData {
109
109
  confidence: number
110
110
  urgencyLabel?: SignalScoreUrgencyLabel
111
111
  urgencyExplanation?: string
112
+ /** Controls whether the priority popover header shows the raw overall score number. @default "number" */
113
+ priorityScoreDisplay?: SignalPriorityScoreDisplay
114
+ /** Formula/context label shown in the priority popover factor section. @default "Priority factors" */
115
+ priorityFormulaLabel?: string
112
116
  explanationBuckets?: SignalScoreExplanationBucket[]
113
117
  onFactorFeedback?: (factorKey: string, type: "up" | "down" | null, detail?: string) => void
114
118
  /** @deprecated The compact score UX no longer renders score-level thumbs by default. */
@@ -488,6 +488,14 @@ export function DetailView({
488
488
  <div className="pb-8">
489
489
  {/* Header */}
490
490
  <div className="mb-4 flex items-center gap-2">
491
+ <button
492
+ type="button"
493
+ className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground"
494
+ >
495
+ <ArrowLeft className="h-3.5 w-3.5" />
496
+ Back
497
+ </button>
498
+ <span className="text-muted-foreground/40">&middot;</span>
491
499
  <span className="text-xs text-muted-foreground">{item.company}</span>
492
500
  </div>
493
501
 
@@ -511,6 +519,8 @@ export function DetailView({
511
519
  score={signalData.score}
512
520
  urgencyLabel={signalData.urgencyLabel}
513
521
  urgencyExplanation={signalData.urgencyExplanation ?? signalData.signalBrief}
522
+ scoreDisplay={signalData.priorityScoreDisplay}
523
+ formulaLabel={signalData.priorityFormulaLabel}
514
524
  factors={signalData.priorityFactors ?? []}
515
525
  metaText={undefined}
516
526
  feedbackChips={signalData.priorityFeedbackChips}