@handled-ai/design-system 0.18.56 → 0.18.58

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.
@@ -43,7 +43,7 @@ export function EmailPreviewCard({
43
43
  signatureHtml,
44
44
  className,
45
45
  }: EmailPreviewCardProps) {
46
- const recipientLabel = to || "the recipient"
46
+ const recipientLabel = to ? `${to}'s` : "the recipient's"
47
47
  const bodyHtml = htmlBody ?? (textBody ? escapeHtml(textBody) : "")
48
48
 
49
49
  return (
@@ -51,7 +51,8 @@ export function EmailPreviewCard({
51
51
  <div className="flex items-start gap-2 mb-3 py-2.5 px-3 border rounded-lg bg-background text-[11.5px] text-muted-foreground">
52
52
  <Eye className="size-4 shrink-0 mt-0.5" />
53
53
  <span>
54
- This is a preview for {recipientLabel}. Nothing has been sent yet.
54
+ This is how your email lands in {recipientLabel} inbox. Nothing has
55
+ been sent yet.
55
56
  </span>
56
57
  </div>
57
58
 
@@ -76,24 +77,17 @@ export function EmailPreviewCard({
76
77
  <div className="text-xs text-muted-foreground shrink-0">just now</div>
77
78
  </div>
78
79
 
79
- <div className="flex gap-3 px-[18px] pt-3 pb-4">
80
- <div className="size-9 shrink-0" aria-hidden="true" />
81
- <div className="min-w-0 flex-1 space-y-3">
82
- <div
83
- className="text-[13.5px] leading-relaxed whitespace-pre-wrap"
84
- data-testid="email-preview-body"
85
- dangerouslySetInnerHTML={{ __html: bodyHtml }}
86
- />
80
+ <div
81
+ className="px-[18px] py-2 ml-[47px] text-[13.5px] leading-relaxed whitespace-pre-wrap"
82
+ dangerouslySetInnerHTML={{ __html: bodyHtml }}
83
+ />
87
84
 
88
- {signatureHtml ? (
89
- <div
90
- className="border-t border-border/50 pt-3 text-xs leading-relaxed text-muted-foreground"
91
- data-testid="email-preview-signature"
92
- dangerouslySetInnerHTML={{ __html: signatureHtml }}
93
- />
94
- ) : null}
95
- </div>
96
- </div>
85
+ {signatureHtml ? (
86
+ <div
87
+ className="ml-[47px] px-[18px] pt-3 mt-3 border-t border-border/50 pb-4 text-xs leading-relaxed text-muted-foreground"
88
+ dangerouslySetInnerHTML={{ __html: signatureHtml }}
89
+ />
90
+ ) : null}
97
91
  </div>
98
92
  </div>
99
93
  )
@@ -90,6 +90,7 @@ interface OpportunityPreview {
90
90
  description?: string | null
91
91
  churnType?: string | null
92
92
  churnTypeOptions?: Array<string | OpportunityPreviewOption>
93
+ nextStep?: string | null
93
94
  }
94
95
 
95
96
  interface OpportunityDraft {
@@ -97,6 +98,7 @@ interface OpportunityDraft {
97
98
  amount: string
98
99
  description: string
99
100
  churnType: string
101
+ nextStep?: string
100
102
  }
101
103
 
102
104
  interface SignalApprovalLabels {
@@ -459,7 +461,7 @@ function formatAmountDraftValue(value: string | number | null | undefined): stri
459
461
  }
460
462
 
461
463
  function buildOpportunityDraft(preview?: OpportunityPreview): OpportunityDraft {
462
- return {
464
+ const draft: OpportunityDraft = {
463
465
  closeDate: preview?.closeDateValue ?? preview?.closeDate ?? "",
464
466
  amount: preview?.amountValue === undefined
465
467
  ? preview?.amount ?? ""
@@ -467,6 +469,10 @@ function buildOpportunityDraft(preview?: OpportunityPreview): OpportunityDraft {
467
469
  description: preview?.description ?? "",
468
470
  churnType: preview?.churnType ?? "",
469
471
  }
472
+ if (preview?.nextStep != null) {
473
+ draft.nextStep = preview.nextStep
474
+ }
475
+ return draft
470
476
  }
471
477
 
472
478
  function hasEditableOpportunityPreview(preview?: OpportunityPreview): boolean {
@@ -898,6 +904,17 @@ function Actions() {
898
904
  )}
899
905
  </label>
900
906
 
907
+ <label className="space-y-1 text-xs">
908
+ <span className="font-medium text-muted-foreground">Next Step</span>
909
+ <textarea
910
+ value={opportunityDraft.nextStep ?? ""}
911
+ onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, nextStep: event.target.value }))}
912
+ rows={2}
913
+ placeholder="No next step set"
914
+ className="w-full resize-none rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-ring"
915
+ />
916
+ </label>
917
+
901
918
  <label className="space-y-1 text-xs">
902
919
  <span className="font-medium text-muted-foreground">Description</span>
903
920
  <textarea
@@ -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
  })
@@ -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,23 +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
- })
53
-
54
- expect(screen.queryByRole("button", { name: "Back" })).toBeNull()
55
- expect(screen.getByRole("heading", { name: "Churn risk case title" })).toBeTruthy()
56
- expect(screen.getAllByText("Acme Corp").length).toBeGreaterThan(0)
57
- expect(screen.getByRole("button", { name: "Full-width quick action" })).toBeTruthy()
58
- expect(screen.getByTestId("metadata-extra").textContent).toBe("Owner: Lee")
59
- expect(screen.getByTestId("priority-popover-trigger")).toBeTruthy()
60
- expect(screen.getByRole("button", { name: "View account details for Acme Corp" })).toBeTruthy()
61
- })
62
-
63
42
  it("renders supporting title text below the main title", () => {
64
43
  renderDetailView({
65
44
  renderTitleSubtext: (item) => <p data-testid="title-subtext">Full title: {item.title}</p>,
@@ -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