@handled-ai/design-system 0.18.38 → 0.18.40
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/chart.d.ts +1 -1
- package/dist/components/draft-feedback-inline.d.ts +1 -1
- package/dist/components/draft-feedback-inline.js +10 -10
- package/dist/components/draft-feedback-inline.js.map +1 -1
- package/dist/components/email-recipient-field.js +107 -166
- package/dist/components/email-recipient-field.js.map +1 -1
- package/dist/components/related-record-action-card.d.ts +19 -0
- package/dist/components/related-record-action-card.js +150 -0
- package/dist/components/related-record-action-card.js.map +1 -0
- package/dist/components/score-feedback.js +6 -6
- package/dist/components/score-feedback.js.map +1 -1
- package/dist/components/suggested-actions.js +17 -5
- package/dist/components/suggested-actions.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/draft-feedback-inline.test.tsx +72 -0
- package/src/components/__tests__/email-recipient-field.test.tsx +116 -1
- package/src/components/__tests__/related-record-action-card.test.tsx +123 -0
- package/src/components/__tests__/suggested-actions-feedback-header.test.tsx +86 -0
- package/src/components/draft-feedback-inline.tsx +13 -13
- package/src/components/email-recipient-field.tsx +55 -101
- package/src/components/related-record-action-card.tsx +169 -0
- package/src/components/score-feedback.tsx +7 -7
- package/src/components/suggested-actions.tsx +19 -5
- package/src/index.ts +1 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for DraftFeedbackInline.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Clicking the up thumb activates the positive state (monochrome bg-muted)
|
|
6
|
+
* - Clicking the down thumb activates the negative state (restrained red)
|
|
7
|
+
* - Thumb icons render outline-only (never fill="currentColor")
|
|
8
|
+
* - initialDirection="down" renders the down state pre-selected
|
|
9
|
+
* - initialDirection="up" renders the up state pre-selected
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from "vitest"
|
|
13
|
+
import React from "react"
|
|
14
|
+
import { render, screen, fireEvent } from "@testing-library/react"
|
|
15
|
+
import { DraftFeedbackInline } from "../draft-feedback-inline"
|
|
16
|
+
|
|
17
|
+
describe("DraftFeedbackInline", () => {
|
|
18
|
+
it("renders idle with no expanded feedback area", () => {
|
|
19
|
+
render(<DraftFeedbackInline />)
|
|
20
|
+
expect(screen.getByText("How's this draft?")).toBeTruthy()
|
|
21
|
+
expect(screen.queryByText("What worked well?")).toBeNull()
|
|
22
|
+
expect(screen.queryByText("What needs improvement?")).toBeNull()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("activates the positive state with monochrome tokens when up is clicked", () => {
|
|
26
|
+
const { container } = render(<DraftFeedbackInline />)
|
|
27
|
+
const buttons = container.querySelectorAll("button")
|
|
28
|
+
const upButton = buttons[0] as HTMLButtonElement
|
|
29
|
+
fireEvent.click(upButton)
|
|
30
|
+
|
|
31
|
+
expect(screen.getByText("What worked well?")).toBeTruthy()
|
|
32
|
+
expect(upButton.className).toContain("bg-muted")
|
|
33
|
+
expect(upButton.className).toContain("text-foreground")
|
|
34
|
+
expect(upButton.className).not.toContain("emerald")
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("activates the negative state with restrained red when down is clicked", () => {
|
|
38
|
+
const { container } = render(<DraftFeedbackInline />)
|
|
39
|
+
const buttons = container.querySelectorAll("button")
|
|
40
|
+
const downButton = buttons[1] as HTMLButtonElement
|
|
41
|
+
fireEvent.click(downButton)
|
|
42
|
+
|
|
43
|
+
expect(screen.getByText("What needs improvement?")).toBeTruthy()
|
|
44
|
+
expect(downButton.className).toContain("bg-muted")
|
|
45
|
+
expect(downButton.className).toContain("text-foreground")
|
|
46
|
+
expect(downButton.className).not.toContain("bg-red")
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("renders thumb icons outline-only (never fill=currentColor)", () => {
|
|
50
|
+
const { container } = render(<DraftFeedbackInline />)
|
|
51
|
+
const buttons = container.querySelectorAll("button")
|
|
52
|
+
// Activate up so the icon would be "filled" under the old behavior.
|
|
53
|
+
fireEvent.click(buttons[0] as HTMLButtonElement)
|
|
54
|
+
|
|
55
|
+
const svgs = container.querySelectorAll("svg")
|
|
56
|
+
svgs.forEach((svg) => {
|
|
57
|
+
expect(svg.getAttribute("fill")).not.toBe("currentColor")
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("renders the down state pre-selected with initialDirection='down'", () => {
|
|
62
|
+
render(<DraftFeedbackInline initialDirection="down" />)
|
|
63
|
+
expect(screen.getByText("What needs improvement?")).toBeTruthy()
|
|
64
|
+
expect(screen.queryByText("What worked well?")).toBeNull()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("renders the up state pre-selected with initialDirection='up'", () => {
|
|
68
|
+
render(<DraftFeedbackInline initialDirection="up" />)
|
|
69
|
+
expect(screen.getByText("What worked well?")).toBeTruthy()
|
|
70
|
+
expect(screen.queryByText("What needs improvement?")).toBeNull()
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { describe, it, expect, vi, afterEach } from "vitest"
|
|
2
2
|
import React from "react"
|
|
3
|
-
import { render, screen, fireEvent, cleanup } from "@testing-library/react"
|
|
3
|
+
import { render, screen, fireEvent, cleanup, within } from "@testing-library/react"
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
EmailRecipientField,
|
|
7
7
|
type RecipientChip,
|
|
8
8
|
} from "../email-recipient-field"
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
} from "../dialog"
|
|
9
14
|
import type { SuggestedContact } from "../suggested-actions"
|
|
10
15
|
|
|
11
16
|
afterEach(() => {
|
|
@@ -253,4 +258,114 @@ describe("EmailRecipientField", () => {
|
|
|
253
258
|
expect(noEmailRow.className).toContain("pointer-events-none")
|
|
254
259
|
expect(noEmailRow.textContent).toContain("No email")
|
|
255
260
|
})
|
|
261
|
+
|
|
262
|
+
it("autofocuses the search input when the picker opens", () => {
|
|
263
|
+
render(
|
|
264
|
+
<EmailRecipientField
|
|
265
|
+
label="To"
|
|
266
|
+
recipients={[]}
|
|
267
|
+
onRecipientsChange={vi.fn()}
|
|
268
|
+
showPicker
|
|
269
|
+
contacts={contacts}
|
|
270
|
+
/>,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
fireEvent.click(screen.getByRole("button", { name: /Contacts/ }))
|
|
274
|
+
const search = screen.getByPlaceholderText("Search contacts...")
|
|
275
|
+
expect(document.activeElement).toBe(search)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it("accepts typing in the search box and filters the list", () => {
|
|
279
|
+
render(
|
|
280
|
+
<EmailRecipientField
|
|
281
|
+
label="To"
|
|
282
|
+
recipients={[]}
|
|
283
|
+
onRecipientsChange={vi.fn()}
|
|
284
|
+
showPicker
|
|
285
|
+
contacts={contacts}
|
|
286
|
+
/>,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
fireEvent.click(screen.getByRole("button", { name: /Contacts/ }))
|
|
290
|
+
const search = screen.getByPlaceholderText("Search contacts...") as HTMLInputElement
|
|
291
|
+
|
|
292
|
+
fireEvent.change(search, { target: { value: "Bea" } })
|
|
293
|
+
expect(search.value).toBe("Bea")
|
|
294
|
+
expect(screen.getByText("Bea Buyer")).toBeTruthy()
|
|
295
|
+
expect(screen.queryByText("Alex Admin")).toBeNull()
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it("adds a typed email from the picker search box on Enter", () => {
|
|
299
|
+
const onChange = vi.fn()
|
|
300
|
+
render(
|
|
301
|
+
<EmailRecipientField
|
|
302
|
+
label="To"
|
|
303
|
+
recipients={[]}
|
|
304
|
+
onRecipientsChange={onChange}
|
|
305
|
+
showPicker
|
|
306
|
+
contacts={contacts}
|
|
307
|
+
/>,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
fireEvent.click(screen.getByRole("button", { name: /Contacts/ }))
|
|
311
|
+
const search = screen.getByPlaceholderText("Search contacts...")
|
|
312
|
+
|
|
313
|
+
fireEvent.change(search, { target: { value: "typed@example.com" } })
|
|
314
|
+
fireEvent.keyDown(search, { key: "Enter" })
|
|
315
|
+
|
|
316
|
+
expect(onChange).toHaveBeenCalledWith([
|
|
317
|
+
{ id: "typed@example.com", email: "typed@example.com", name: "", confirmed: false },
|
|
318
|
+
])
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it("shows a clear empty state when there are no contacts", () => {
|
|
322
|
+
render(
|
|
323
|
+
<EmailRecipientField
|
|
324
|
+
label="To"
|
|
325
|
+
recipients={[]}
|
|
326
|
+
onRecipientsChange={vi.fn()}
|
|
327
|
+
showPicker
|
|
328
|
+
contacts={[]}
|
|
329
|
+
/>,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
fireEvent.click(screen.getByRole("button", { name: /Contacts/ }))
|
|
333
|
+
expect(screen.getByText("No contacts for this account")).toBeTruthy()
|
|
334
|
+
// The empty state guides the user to type an address instead of looking broken.
|
|
335
|
+
expect(
|
|
336
|
+
screen.getByText(/Type an email address above and press Enter/),
|
|
337
|
+
).toBeTruthy()
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it("remains typeable when rendered inside a modal Dialog (WIT-800 focus trap)", () => {
|
|
341
|
+
// Reproduces the staging bug: the picker used to render via
|
|
342
|
+
// createPortal(document.body), outside the Dialog's FocusScope, so the
|
|
343
|
+
// search input could not hold focus. The Radix Popover rebuild keeps the
|
|
344
|
+
// input inside its own (parent-pausing) focus scope.
|
|
345
|
+
render(
|
|
346
|
+
<Dialog open>
|
|
347
|
+
<DialogContent aria-describedby={undefined}>
|
|
348
|
+
<DialogTitle>Compose</DialogTitle>
|
|
349
|
+
<EmailRecipientField
|
|
350
|
+
label="To"
|
|
351
|
+
recipients={[]}
|
|
352
|
+
onRecipientsChange={vi.fn()}
|
|
353
|
+
showPicker
|
|
354
|
+
contacts={contacts}
|
|
355
|
+
/>
|
|
356
|
+
</DialogContent>
|
|
357
|
+
</Dialog>,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
fireEvent.click(screen.getByRole("button", { name: /Contacts/ }))
|
|
361
|
+
const search = screen.getByPlaceholderText("Search contacts...") as HTMLInputElement
|
|
362
|
+
|
|
363
|
+
search.focus()
|
|
364
|
+
expect(document.activeElement).toBe(search)
|
|
365
|
+
|
|
366
|
+
fireEvent.change(search, { target: { value: "Alex" } })
|
|
367
|
+
expect(search.value).toBe("Alex")
|
|
368
|
+
const listbox = screen.getByRole("listbox")
|
|
369
|
+
expect(within(listbox).getByText("Alex Admin")).toBeTruthy()
|
|
370
|
+
})
|
|
256
371
|
})
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import "@testing-library/jest-dom/vitest"
|
|
2
|
+
|
|
3
|
+
import React from "react"
|
|
4
|
+
import { fireEvent, render, screen } from "@testing-library/react"
|
|
5
|
+
import { describe, expect, it, vi } from "vitest"
|
|
6
|
+
|
|
7
|
+
import { BRAND_ICONS } from "../../lib/icons"
|
|
8
|
+
import { RelatedRecordActionCard } from "../related-record-action-card"
|
|
9
|
+
|
|
10
|
+
describe("RelatedRecordActionCard", () => {
|
|
11
|
+
it("renders an enabled href as a link with pointer affordance", () => {
|
|
12
|
+
render(
|
|
13
|
+
<RelatedRecordActionCard
|
|
14
|
+
kind="account"
|
|
15
|
+
label="Open account"
|
|
16
|
+
subtitle="Acme Corp"
|
|
17
|
+
href="/accounts/acme"
|
|
18
|
+
testId="record-card"
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const card = screen.getByTestId("record-card")
|
|
23
|
+
|
|
24
|
+
expect(card.tagName).toBe("A")
|
|
25
|
+
expect(card).toHaveAttribute("href", "/accounts/acme")
|
|
26
|
+
expect(card.className).toContain("cursor-pointer")
|
|
27
|
+
expect(card.className).toContain("hover:bg-muted/50")
|
|
28
|
+
expect(card.className).toContain("hover:border-border/80")
|
|
29
|
+
expect(card.className).toContain("focus-visible:ring-2")
|
|
30
|
+
expect(card.className).toContain("active:text-primary")
|
|
31
|
+
expect(screen.getByText("Acme Corp")).toBeInTheDocument()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("renders an enabled onClick action as a button and invokes the callback", () => {
|
|
35
|
+
const onClick = vi.fn()
|
|
36
|
+
|
|
37
|
+
render(
|
|
38
|
+
<RelatedRecordActionCard
|
|
39
|
+
kind="case"
|
|
40
|
+
label="Open case"
|
|
41
|
+
onClick={onClick}
|
|
42
|
+
testId="record-card"
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const card = screen.getByRole("button", { name: /open case/i })
|
|
47
|
+
|
|
48
|
+
expect(card.tagName).toBe("BUTTON")
|
|
49
|
+
expect(card).toHaveAttribute("type", "button")
|
|
50
|
+
expect(card.className).toContain("cursor-pointer")
|
|
51
|
+
|
|
52
|
+
fireEvent.click(card)
|
|
53
|
+
|
|
54
|
+
expect(onClick).toHaveBeenCalledTimes(1)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("renders a disabled card when disabledReason is present", () => {
|
|
58
|
+
render(
|
|
59
|
+
<RelatedRecordActionCard
|
|
60
|
+
kind="opportunity"
|
|
61
|
+
label="Open opportunity"
|
|
62
|
+
disabledReason="Connect Salesforce to view this opportunity."
|
|
63
|
+
href="/opportunities/1"
|
|
64
|
+
testId="record-card"
|
|
65
|
+
/>
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const card = screen.getByTestId("record-card")
|
|
69
|
+
|
|
70
|
+
expect(card.tagName).toBe("DIV")
|
|
71
|
+
expect(card).toHaveAttribute("aria-disabled", "true")
|
|
72
|
+
expect(card.className).toContain("cursor-not-allowed")
|
|
73
|
+
expect(card.className).toContain("text-muted-foreground")
|
|
74
|
+
expect(card.className).not.toContain("cursor-pointer")
|
|
75
|
+
expect(card.className).not.toContain("hover:bg-muted/50")
|
|
76
|
+
expect(screen.getByText("Connect Salesforce to view this opportunity.")).toBeInTheDocument()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("renders a disabled card when no action is available", () => {
|
|
80
|
+
render(<RelatedRecordActionCard kind="generic" label="Unavailable record" testId="record-card" />)
|
|
81
|
+
|
|
82
|
+
const card = screen.getByTestId("record-card")
|
|
83
|
+
|
|
84
|
+
expect(card.tagName).toBe("DIV")
|
|
85
|
+
expect(card).toHaveAttribute("aria-disabled", "true")
|
|
86
|
+
expect(card.className).toContain("cursor-not-allowed")
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("sets external link attributes and exposes an accessible external-link cue", () => {
|
|
90
|
+
const { container } = render(
|
|
91
|
+
<RelatedRecordActionCard
|
|
92
|
+
kind="salesforce"
|
|
93
|
+
label="Open in Salesforce"
|
|
94
|
+
href="https://example.salesforce.com/001"
|
|
95
|
+
external
|
|
96
|
+
testId="record-card"
|
|
97
|
+
/>
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const card = screen.getByTestId("record-card")
|
|
101
|
+
|
|
102
|
+
expect(card).toHaveAttribute("target", "_blank")
|
|
103
|
+
expect(card).toHaveAttribute("rel", "noopener noreferrer")
|
|
104
|
+
expect(screen.getByText("opens in a new tab")).toHaveClass("sr-only")
|
|
105
|
+
expect(container.querySelector('[data-slot="related-record-action-card-external-icon"]')).not.toBeNull()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it("renders the Salesforce brand icon when icon is salesforce", () => {
|
|
109
|
+
const { container } = render(
|
|
110
|
+
<RelatedRecordActionCard
|
|
111
|
+
kind="generic"
|
|
112
|
+
label="Salesforce record"
|
|
113
|
+
href="https://example.salesforce.com/001"
|
|
114
|
+
icon="salesforce"
|
|
115
|
+
/>
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const icon = container.querySelector('[data-slot="related-record-action-card-icon"] img')
|
|
119
|
+
|
|
120
|
+
expect(icon).toHaveAttribute("src", BRAND_ICONS.salesforce)
|
|
121
|
+
expect(icon).toHaveAttribute("alt", "Salesforce")
|
|
122
|
+
})
|
|
123
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the SuggestedActions card header thumbs (feedback direction).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Clicking up opens the feedback panel with direction "up"
|
|
6
|
+
* - Clicking down opens the feedback panel with direction "down"
|
|
7
|
+
* - Clicking the same direction again closes the panel
|
|
8
|
+
* - Switching direction remounts DraftFeedbackInline (panel stays open, content switches)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from "vitest"
|
|
12
|
+
import React from "react"
|
|
13
|
+
import { render, screen, fireEvent } from "@testing-library/react"
|
|
14
|
+
import { SuggestedActions } from "../suggested-actions"
|
|
15
|
+
import type { SuggestedAction } from "../suggested-actions"
|
|
16
|
+
|
|
17
|
+
const action: SuggestedAction = {
|
|
18
|
+
id: 1,
|
|
19
|
+
type: "email",
|
|
20
|
+
label: "Send follow-up email",
|
|
21
|
+
status: "pending",
|
|
22
|
+
content: "<p>Hello there.</p>",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The header thumbs are the only buttons that wrap a thumbs-up/down SVG and
|
|
26
|
+
// live in the card header. We resolve them by locating the lucide icon class.
|
|
27
|
+
function getHeaderThumbs(container: HTMLElement) {
|
|
28
|
+
const up = container.querySelector("button .lucide-thumbs-up")?.closest("button")
|
|
29
|
+
const down = container.querySelector("button .lucide-thumbs-down")?.closest("button")
|
|
30
|
+
return { up: up as HTMLButtonElement, down: down as HTMLButtonElement }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("SuggestedActions header thumbs", () => {
|
|
34
|
+
it("does not show the feedback panel initially", () => {
|
|
35
|
+
render(<SuggestedActions actions={[action]} />)
|
|
36
|
+
expect(screen.queryByText("How's this draft?")).toBeNull()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("opens the feedback panel with direction up when up is clicked", () => {
|
|
40
|
+
const { container } = render(<SuggestedActions actions={[action]} />)
|
|
41
|
+
const { up } = getHeaderThumbs(container)
|
|
42
|
+
fireEvent.click(up)
|
|
43
|
+
|
|
44
|
+
expect(screen.getByText("How's this draft?")).toBeTruthy()
|
|
45
|
+
expect(screen.getByText("What worked well?")).toBeTruthy()
|
|
46
|
+
expect(screen.queryByText("What needs improvement?")).toBeNull()
|
|
47
|
+
expect(up.className).toContain("bg-muted")
|
|
48
|
+
expect(up.className).toContain("text-foreground")
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("opens the feedback panel with direction down when down is clicked", () => {
|
|
52
|
+
const { container } = render(<SuggestedActions actions={[action]} />)
|
|
53
|
+
const { down } = getHeaderThumbs(container)
|
|
54
|
+
fireEvent.click(down)
|
|
55
|
+
|
|
56
|
+
expect(screen.getByText("How's this draft?")).toBeTruthy()
|
|
57
|
+
expect(screen.getByText("What needs improvement?")).toBeTruthy()
|
|
58
|
+
expect(screen.queryByText("What worked well?")).toBeNull()
|
|
59
|
+
expect(down.className).toContain("bg-muted")
|
|
60
|
+
expect(down.className).toContain("text-foreground")
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("closes the panel when the same direction is clicked again", () => {
|
|
64
|
+
const { container } = render(<SuggestedActions actions={[action]} />)
|
|
65
|
+
const { up } = getHeaderThumbs(container)
|
|
66
|
+
fireEvent.click(up)
|
|
67
|
+
expect(screen.getByText("How's this draft?")).toBeTruthy()
|
|
68
|
+
|
|
69
|
+
fireEvent.click(up)
|
|
70
|
+
expect(screen.queryByText("How's this draft?")).toBeNull()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("switches direction (remounts DraftFeedbackInline) while keeping the panel open", () => {
|
|
74
|
+
const { container } = render(<SuggestedActions actions={[action]} />)
|
|
75
|
+
const { up, down } = getHeaderThumbs(container)
|
|
76
|
+
|
|
77
|
+
fireEvent.click(up)
|
|
78
|
+
expect(screen.getByText("What worked well?")).toBeTruthy()
|
|
79
|
+
|
|
80
|
+
fireEvent.click(down)
|
|
81
|
+
// Panel still open, but now showing the negative (down) variant.
|
|
82
|
+
expect(screen.getByText("How's this draft?")).toBeTruthy()
|
|
83
|
+
expect(screen.getByText("What needs improvement?")).toBeTruthy()
|
|
84
|
+
expect(screen.queryByText("What worked well?")).toBeNull()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -16,7 +16,7 @@ const positivePills = ["Tone", "Personalization", "Length", "CTA", "Other"]
|
|
|
16
16
|
const negativePills = ["Too formal", "Too casual", "Too long", "Missing context", "Wrong angle", "Factual error", "Other"]
|
|
17
17
|
|
|
18
18
|
export interface DraftFeedbackInlineProps {
|
|
19
|
-
initialDirection?:
|
|
19
|
+
initialDirection?: "up" | "down" | null
|
|
20
20
|
onRegenerateRequest?: (pills: string[], detail: string) => void
|
|
21
21
|
onSubmitFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
|
|
22
22
|
onDiscardRequest?: (pills: string[], detail: string) => void
|
|
@@ -70,7 +70,7 @@ export function DraftFeedbackInline({
|
|
|
70
70
|
if (noted) {
|
|
71
71
|
return (
|
|
72
72
|
<div className="flex items-center gap-1.5 py-1 animate-in fade-in slide-in-from-top-1 duration-200">
|
|
73
|
-
<Check className="w-3.5 h-3.5 text-
|
|
73
|
+
<Check className="w-3.5 h-3.5 text-foreground" />
|
|
74
74
|
<span className="text-xs text-muted-foreground">Feedback recorded</span>
|
|
75
75
|
</div>
|
|
76
76
|
)
|
|
@@ -88,7 +88,7 @@ export function DraftFeedbackInline({
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
return (
|
|
91
|
-
<div className="space-y-
|
|
91
|
+
<div className="space-y-2">
|
|
92
92
|
<div className="flex items-center justify-between">
|
|
93
93
|
<span className="text-sm text-foreground font-medium">How's this draft?</span>
|
|
94
94
|
<div className="flex gap-1">
|
|
@@ -100,11 +100,11 @@ export function DraftFeedbackInline({
|
|
|
100
100
|
}}
|
|
101
101
|
className={`p-1.5 rounded transition-colors ${
|
|
102
102
|
thumbState === "up"
|
|
103
|
-
? "bg-
|
|
103
|
+
? "bg-muted text-foreground"
|
|
104
104
|
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
|
105
105
|
}`}
|
|
106
106
|
>
|
|
107
|
-
<ThumbsUp className="w-4 h-4"
|
|
107
|
+
<ThumbsUp className="w-4 h-4" />
|
|
108
108
|
</button>
|
|
109
109
|
<button
|
|
110
110
|
onClick={() => {
|
|
@@ -114,31 +114,31 @@ export function DraftFeedbackInline({
|
|
|
114
114
|
}}
|
|
115
115
|
className={`p-1.5 rounded transition-colors ${
|
|
116
116
|
thumbState === "down"
|
|
117
|
-
? "bg-
|
|
117
|
+
? "bg-muted text-foreground"
|
|
118
118
|
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
|
119
119
|
}`}
|
|
120
120
|
>
|
|
121
|
-
<ThumbsDown className="w-4 h-4"
|
|
121
|
+
<ThumbsDown className="w-4 h-4" />
|
|
122
122
|
</button>
|
|
123
123
|
</div>
|
|
124
124
|
</div>
|
|
125
125
|
|
|
126
126
|
{thumbState && (
|
|
127
|
-
<div className="pt-
|
|
127
|
+
<div className="pt-2 space-y-2.5 animate-in fade-in slide-in-from-top-2 duration-200">
|
|
128
128
|
<div>
|
|
129
129
|
<span className="text-xs text-muted-foreground mb-2 block font-medium">
|
|
130
130
|
{thumbState === "up" ? "What worked well?" : "What needs improvement?"}
|
|
131
131
|
</span>
|
|
132
|
-
<div className="flex flex-wrap gap-
|
|
132
|
+
<div className="flex flex-wrap gap-2">
|
|
133
133
|
{(thumbState === "up" ? positivePills : negativePills).map((pill) => (
|
|
134
134
|
<button
|
|
135
135
|
key={pill}
|
|
136
136
|
onClick={() => togglePill(pill)}
|
|
137
|
-
className={`px-
|
|
137
|
+
className={`px-3 py-1.5 rounded-full text-[11px] font-medium border transition-colors ${
|
|
138
138
|
selectedPills.includes(pill)
|
|
139
139
|
? thumbState === "up"
|
|
140
|
-
? "bg-
|
|
141
|
-
: "bg-red-
|
|
140
|
+
? "bg-muted text-foreground border-border"
|
|
141
|
+
: "bg-red-50 text-red-700 border-red-200 dark:bg-red-950/30 dark:text-red-300 dark:border-red-800"
|
|
142
142
|
: "bg-background text-muted-foreground border-border hover:bg-muted/50 hover:text-foreground"
|
|
143
143
|
}`}
|
|
144
144
|
>
|
|
@@ -155,7 +155,7 @@ export function DraftFeedbackInline({
|
|
|
155
155
|
className="w-full text-xs bg-background border border-border rounded-md px-2.5 py-2 text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-indigo-500/50 focus:border-indigo-500/50 resize-none min-h-[60px]"
|
|
156
156
|
/>
|
|
157
157
|
|
|
158
|
-
<div className="flex items-center gap-2 pt-
|
|
158
|
+
<div className="flex items-center gap-2.5 pt-2">
|
|
159
159
|
{thumbState === "down" ? (
|
|
160
160
|
<>
|
|
161
161
|
<button
|