@dowel-ui/react 0.1.0 → 0.1.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 (120) hide show
  1. package/dist/components/ai-agent-status/ai-agent-status.d.ts +1 -1
  2. package/dist/components/ai-tool/ai-tool.d.ts +1 -1
  3. package/dist/components/alert/alert.d.ts +1 -1
  4. package/dist/components/avatar/avatar.d.ts +1 -1
  5. package/dist/components/badge/badge.d.ts +1 -1
  6. package/dist/components/button/button.d.ts +1 -1
  7. package/dist/components/progress/progress.d.ts +1 -1
  8. package/dist/components/spinner/spinner.d.ts +1 -1
  9. package/dist/components/toast/toast.d.ts +1 -1
  10. package/package.json +7 -4
  11. package/src/blocks/admin-users/admin-users.stories.tsx +0 -83
  12. package/src/blocks/admin-users/admin-users.test.tsx +0 -159
  13. package/src/blocks/ai-chat/ai-chat.stories.tsx +0 -149
  14. package/src/blocks/ai-chat/ai-chat.test.tsx +0 -166
  15. package/src/blocks/dashboard/dashboard.stories.tsx +0 -106
  16. package/src/blocks/dashboard/dashboard.test.tsx +0 -94
  17. package/src/blocks/forgot-password/forgot-password.stories.tsx +0 -41
  18. package/src/blocks/forgot-password/forgot-password.test.tsx +0 -62
  19. package/src/blocks/login/login.stories.tsx +0 -46
  20. package/src/blocks/login/login.test.tsx +0 -100
  21. package/src/blocks/pricing/pricing.stories.tsx +0 -62
  22. package/src/blocks/pricing/pricing.test.tsx +0 -101
  23. package/src/blocks/settings/settings.stories.tsx +0 -94
  24. package/src/blocks/settings/settings.test.tsx +0 -167
  25. package/src/blocks/signup/signup.stories.tsx +0 -27
  26. package/src/blocks/signup/signup.test.tsx +0 -97
  27. package/src/components/accordion/accordion.stories.tsx +0 -90
  28. package/src/components/accordion/accordion.test.tsx +0 -156
  29. package/src/components/activity-feed/activity-feed.stories.tsx +0 -163
  30. package/src/components/activity-feed/activity-feed.test.tsx +0 -125
  31. package/src/components/ai-agent-status/ai-agent-status.stories.tsx +0 -86
  32. package/src/components/ai-agent-status/ai-agent-status.test.tsx +0 -66
  33. package/src/components/ai-conversation/ai-conversation.stories.tsx +0 -126
  34. package/src/components/ai-conversation/ai-conversation.test.tsx +0 -144
  35. package/src/components/ai-message/ai-message.stories.tsx +0 -106
  36. package/src/components/ai-message/ai-message.test.tsx +0 -160
  37. package/src/components/ai-model-selector/ai-model-selector.stories.tsx +0 -66
  38. package/src/components/ai-model-selector/ai-model-selector.test.tsx +0 -120
  39. package/src/components/ai-prompt-input/ai-prompt-input.stories.tsx +0 -145
  40. package/src/components/ai-prompt-input/ai-prompt-input.test.tsx +0 -273
  41. package/src/components/ai-reasoning/ai-reasoning.stories.tsx +0 -60
  42. package/src/components/ai-reasoning/ai-reasoning.test.tsx +0 -95
  43. package/src/components/ai-response/ai-response.stories.tsx +0 -75
  44. package/src/components/ai-response/ai-response.test.tsx +0 -96
  45. package/src/components/ai-sources/ai-sources.stories.tsx +0 -93
  46. package/src/components/ai-sources/ai-sources.test.tsx +0 -102
  47. package/src/components/ai-token-usage/ai-token-usage.stories.tsx +0 -59
  48. package/src/components/ai-token-usage/ai-token-usage.test.tsx +0 -83
  49. package/src/components/ai-tool/ai-tool.stories.tsx +0 -96
  50. package/src/components/ai-tool/ai-tool.test.tsx +0 -137
  51. package/src/components/alert/alert.stories.tsx +0 -101
  52. package/src/components/alert/alert.test.tsx +0 -94
  53. package/src/components/avatar/avatar.stories.tsx +0 -58
  54. package/src/components/avatar/avatar.test.tsx +0 -64
  55. package/src/components/badge/badge.stories.tsx +0 -68
  56. package/src/components/badge/badge.test.tsx +0 -61
  57. package/src/components/button/button.stories.tsx +0 -128
  58. package/src/components/button/button.test.tsx +0 -224
  59. package/src/components/calendar/calendar.stories.tsx +0 -105
  60. package/src/components/calendar/calendar.test.tsx +0 -255
  61. package/src/components/card/card.stories.tsx +0 -61
  62. package/src/components/card/card.test.tsx +0 -66
  63. package/src/components/checkbox/checkbox.stories.tsx +0 -124
  64. package/src/components/checkbox/checkbox.test.tsx +0 -121
  65. package/src/components/code-block/code-block.stories.tsx +0 -62
  66. package/src/components/code-block/code-block.test.tsx +0 -139
  67. package/src/components/combobox/combobox.stories.tsx +0 -171
  68. package/src/components/combobox/combobox.test.tsx +0 -429
  69. package/src/components/command/command.stories.tsx +0 -152
  70. package/src/components/command/command.test.tsx +0 -380
  71. package/src/components/data-table/data-table.stories.tsx +0 -151
  72. package/src/components/data-table/data-table.test.tsx +0 -288
  73. package/src/components/date-picker/date-picker.stories.tsx +0 -103
  74. package/src/components/date-picker/date-picker.test.tsx +0 -205
  75. package/src/components/dialog/dialog.stories.tsx +0 -140
  76. package/src/components/dialog/dialog.test.tsx +0 -186
  77. package/src/components/drawer/drawer.stories.tsx +0 -111
  78. package/src/components/drawer/drawer.test.tsx +0 -187
  79. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +0 -128
  80. package/src/components/dropdown-menu/dropdown-menu.test.tsx +0 -321
  81. package/src/components/empty-state/empty-state.stories.tsx +0 -70
  82. package/src/components/empty-state/empty-state.test.tsx +0 -97
  83. package/src/components/form/form.stories.tsx +0 -144
  84. package/src/components/form/form.test.tsx +0 -203
  85. package/src/components/input/input.stories.tsx +0 -84
  86. package/src/components/input/input.test.tsx +0 -108
  87. package/src/components/label/label.stories.tsx +0 -51
  88. package/src/components/label/label.test.tsx +0 -55
  89. package/src/components/pagination/pagination.stories.tsx +0 -129
  90. package/src/components/pagination/pagination.test.tsx +0 -119
  91. package/src/components/popover/popover.stories.tsx +0 -84
  92. package/src/components/popover/popover.test.tsx +0 -163
  93. package/src/components/progress/progress.stories.tsx +0 -90
  94. package/src/components/progress/progress.test.tsx +0 -78
  95. package/src/components/radio-group/radio-group.stories.tsx +0 -102
  96. package/src/components/radio-group/radio-group.test.tsx +0 -152
  97. package/src/components/select/select.stories.tsx +0 -137
  98. package/src/components/select/select.test.tsx +0 -209
  99. package/src/components/separator/separator.stories.tsx +0 -42
  100. package/src/components/separator/separator.test.tsx +0 -46
  101. package/src/components/sheet/sheet.stories.tsx +0 -107
  102. package/src/components/sheet/sheet.test.tsx +0 -152
  103. package/src/components/skeleton/skeleton.stories.tsx +0 -46
  104. package/src/components/skeleton/skeleton.test.tsx +0 -57
  105. package/src/components/slider/slider.stories.tsx +0 -87
  106. package/src/components/slider/slider.test.tsx +0 -148
  107. package/src/components/spinner/spinner.stories.tsx +0 -47
  108. package/src/components/spinner/spinner.test.tsx +0 -41
  109. package/src/components/switch/switch.stories.tsx +0 -70
  110. package/src/components/switch/switch.test.tsx +0 -101
  111. package/src/components/table/table.stories.tsx +0 -131
  112. package/src/components/table/table.test.tsx +0 -153
  113. package/src/components/tabs/tabs.stories.tsx +0 -139
  114. package/src/components/tabs/tabs.test.tsx +0 -148
  115. package/src/components/toast/toast.stories.tsx +0 -167
  116. package/src/components/toast/toast.test.tsx +0 -306
  117. package/src/components/tooltip/tooltip.stories.tsx +0 -89
  118. package/src/components/tooltip/tooltip.test.tsx +0 -120
  119. package/src/lib/utils.test.ts +0 -32
  120. package/src/registry/meta.test.ts +0 -234
@@ -1,96 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import { describe, expect, it } from "vitest";
3
-
4
- import { expectNoA11yViolations } from "../../../test/a11y";
5
- import { Response, ThinkingIndicator } from "./ai-response";
6
-
7
- describe("Response", () => {
8
- it("renders its text", () => {
9
- render(<Response>The capital of France is Paris.</Response>);
10
- expect(screen.getByText("The capital of France is Paris.")).toBeInTheDocument();
11
- });
12
-
13
- it("is NOT a live region", () => {
14
- // Announcing streamed text as it arrives interrupts a screen reader on
15
- // every token; state goes through ConversationStatus instead.
16
- const { container } = render(<Response streaming>Partial…</Response>);
17
- const response = container.querySelector("[data-slot='response']");
18
-
19
- expect(response).not.toHaveAttribute("aria-live");
20
- expect(response).not.toHaveAttribute("role");
21
- });
22
-
23
- it("shows a decorative caret while streaming", () => {
24
- const { container } = render(<Response streaming>Partial</Response>);
25
- const caret = container.querySelector("[data-slot='response-caret']");
26
-
27
- expect(caret).toBeInTheDocument();
28
- expect(caret).toHaveAttribute("aria-hidden", "true");
29
- });
30
-
31
- it("has no caret when it is not streaming", () => {
32
- const { container } = render(<Response>Done</Response>);
33
- expect(container.querySelector("[data-slot='response-caret']")).not.toBeInTheDocument();
34
- });
35
-
36
- it("marks the streaming state for styling", () => {
37
- const { container } = render(<Response streaming>Partial</Response>);
38
- expect(container.querySelector("[data-slot='response']")).toHaveAttribute(
39
- "data-streaming",
40
- "true",
41
- );
42
- });
43
-
44
- it("preserves paragraph whitespace", () => {
45
- const { container } = render(<Response>{"line one\nline two"}</Response>);
46
- expect(container.querySelector("[data-slot='response']")).toHaveClass(
47
- "whitespace-pre-wrap",
48
- );
49
- });
50
-
51
- it("renders markup a consumer has already rendered", () => {
52
- render(
53
- <Response>
54
- <p>First</p>
55
- <p>Second</p>
56
- </Response>,
57
- );
58
- expect(screen.getByText("First")).toBeInTheDocument();
59
- expect(screen.getByText("Second")).toBeInTheDocument();
60
- });
61
-
62
- it("lets a consumer className override a conflicting utility", () => {
63
- const { container } = render(<Response className="text-base">Hi</Response>);
64
- const response = container.querySelector("[data-slot='response']");
65
- expect(response).toHaveClass("text-base");
66
- expect(response).not.toHaveClass("text-sm");
67
- });
68
-
69
- it("has no accessibility violations", async () => {
70
- const { container } = render(<Response streaming>Streaming text…</Response>);
71
- await expectNoA11yViolations(container);
72
- });
73
- });
74
-
75
- describe("ThinkingIndicator", () => {
76
- it("is labelled, because it is the only thing on screen", () => {
77
- render(<ThinkingIndicator />);
78
- expect(screen.getByText("Thinking")).toBeInTheDocument();
79
- });
80
-
81
- it("accepts a custom label", () => {
82
- render(<ThinkingIndicator label="Searching the web" />);
83
- expect(screen.getByText("Searching the web")).toBeInTheDocument();
84
- });
85
-
86
- it("hides its dots from assistive technology", () => {
87
- const { container } = render(<ThinkingIndicator />);
88
- const dots = container.querySelectorAll("span[aria-hidden='true']");
89
- expect(dots).toHaveLength(3);
90
- });
91
-
92
- it("has no accessibility violations", async () => {
93
- const { container } = render(<ThinkingIndicator />);
94
- await expectNoA11yViolations(container);
95
- });
96
- });
@@ -1,93 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Response } from "@/components/ai-response";
4
-
5
- import { InlineCitation, Source, Sources, SourcesContent, SourcesTrigger } from "./ai-sources";
6
-
7
- /** Named so its type is nameable in declaration output (TS2883). */
8
- const withFixedWidth: Decorator = (Story) => (
9
- <div className="w-[32rem]">
10
- <Story />
11
- </div>
12
- );
13
-
14
- const meta = {
15
- title: "AI/Sources",
16
- component: Sources,
17
- parameters: { controls: { disable: true } },
18
- decorators: [withFixedWidth],
19
- } satisfies Meta<typeof Sources>;
20
-
21
- export default meta;
22
- type Story = StoryObj<typeof meta>;
23
-
24
- const SOURCES = [
25
- {
26
- index: 1,
27
- title: "ARIA Authoring Practices — Table pattern",
28
- origin: "w3.org",
29
- excerpt: "Scrollable regions should be focusable so keyboard users can scroll them.",
30
- },
31
- {
32
- index: 2,
33
- title: "Accessible data tables",
34
- origin: "example.org",
35
- excerpt: "Native table semantics convey row and column position without extra ARIA.",
36
- },
37
- ];
38
-
39
- /** Markers next to the claim, the list underneath saying what each one is. */
40
- export const Default: Story = {
41
- render: () => (
42
- <div className="grid gap-3">
43
- <Response>
44
- A scroll container needs an explicit focus stop to be reachable by keyboard
45
- <InlineCitation index={1} title={SOURCES[0]!.title} href="#1" />, and native table
46
- semantics carry position without extra ARIA
47
- <InlineCitation index={2} title={SOURCES[1]!.title} href="#2" />.
48
- </Response>
49
- <Sources>
50
- <SourcesTrigger count={SOURCES.length} />
51
- <SourcesContent>
52
- {SOURCES.map((source) => (
53
- <Source key={source.index} {...source} href={`#${String(source.index)}`} />
54
- ))}
55
- </SourcesContent>
56
- </Sources>
57
- </div>
58
- ),
59
- };
60
-
61
- export const Expanded: Story = {
62
- render: () => (
63
- <Sources defaultOpen>
64
- <SourcesTrigger count={SOURCES.length} />
65
- <SourcesContent>
66
- {SOURCES.map((source) => (
67
- <Source key={source.index} {...source} href={`#${String(source.index)}`} />
68
- ))}
69
- </SourcesContent>
70
- </Sources>
71
- ),
72
- };
73
-
74
- /** A marker with nowhere to go renders as text, not a link that does nothing. */
75
- export const WithoutLinks: Story = {
76
- render: () => (
77
- <Response>
78
- Internal policy requires two approvals
79
- <InlineCitation index={1} title="Engineering handbook, section 4" />.
80
- </Response>
81
- ),
82
- };
83
-
84
- export const SingleSource: Story = {
85
- render: () => (
86
- <Sources defaultOpen>
87
- <SourcesTrigger count={1} />
88
- <SourcesContent>
89
- <Source index={1} title="Engineering handbook" origin="internal" href="#1" />
90
- </SourcesContent>
91
- </Sources>
92
- ),
93
- };
@@ -1,102 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { describe, expect, it } from "vitest";
4
-
5
- import { expectNoA11yViolations } from "../../../test/a11y";
6
- import { InlineCitation, Source, Sources, SourcesContent, SourcesTrigger } from "./ai-sources";
7
-
8
- describe("InlineCitation", () => {
9
- it("carries the source title in its accessible name", () => {
10
- render(<InlineCitation index={1} title="Encyclopaedia entry on France" href="#1" />);
11
- // The visible text is a bare number, which conveys nothing on its own.
12
- expect(
13
- screen.getByRole("link", { name: "Source 1: Encyclopaedia entry on France" }),
14
- ).toBeInTheDocument();
15
- });
16
-
17
- it("renders as text, not a dead link, without an href", () => {
18
- render(<InlineCitation index={2} title="Internal document" />);
19
- expect(screen.queryByRole("link")).not.toBeInTheDocument();
20
- expect(screen.getByText("Source 2: Internal document")).toBeInTheDocument();
21
- });
22
-
23
- it("shows the marker number visually", () => {
24
- const { container } = render(<InlineCitation index={3} title="A source" />);
25
- expect(container.querySelector("[aria-hidden='true']")).toHaveTextContent("3");
26
- });
27
- });
28
-
29
- describe("Sources", () => {
30
- function Example() {
31
- return (
32
- <Sources>
33
- <SourcesTrigger count={2} />
34
- <SourcesContent>
35
- <Source index={1} title="Paris — overview" origin="example.org" href="#1" />
36
- <Source
37
- index={2}
38
- title="France factbook"
39
- origin="factbook.example"
40
- excerpt="Paris is the capital and most populous city."
41
- href="#2"
42
- />
43
- </SourcesContent>
44
- </Sources>
45
- );
46
- }
47
-
48
- it("is collapsed by default", () => {
49
- render(<Example />);
50
- expect(screen.getByRole("button", { name: "2 sources" })).toHaveAttribute(
51
- "aria-expanded",
52
- "false",
53
- );
54
- });
55
-
56
- it("pluralises the count", () => {
57
- render(
58
- <Sources>
59
- <SourcesTrigger count={1} />
60
- <SourcesContent>
61
- <Source index={1} title="Only one" href="#1" />
62
- </SourcesContent>
63
- </Sources>,
64
- );
65
- expect(screen.getByRole("button", { name: "1 source" })).toBeInTheDocument();
66
- });
67
-
68
- it("lists the sources in order, matching the marker numbers", async () => {
69
- const user = userEvent.setup();
70
- render(<Example />);
71
-
72
- await user.click(screen.getByRole("button", { name: "2 sources" }));
73
-
74
- const list = await screen.findByRole("list");
75
- expect(list.tagName).toBe("OL");
76
- expect(screen.getAllByRole("listitem")).toHaveLength(2);
77
- });
78
-
79
- it("shows origin and excerpt when given", async () => {
80
- const user = userEvent.setup();
81
- render(<Example />);
82
-
83
- await user.click(screen.getByRole("button", { name: "2 sources" }));
84
- expect(await screen.findByText("factbook.example")).toBeInTheDocument();
85
- expect(
86
- screen.getByText(/Paris is the capital and most populous city\./),
87
- ).toBeInTheDocument();
88
- });
89
-
90
- it("has no accessibility violations", async () => {
91
- const { container } = render(
92
- <Sources defaultOpen>
93
- <SourcesTrigger count={2} />
94
- <SourcesContent>
95
- <Source index={1} title="Paris — overview" origin="example.org" href="#1" />
96
- <Source index={2} title="France factbook" href="#2" />
97
- </SourcesContent>
98
- </Sources>,
99
- );
100
- await expectNoA11yViolations(container);
101
- });
102
- });
@@ -1,59 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { TokenCount, TokenUsage } from "./ai-token-usage";
4
-
5
- /** Named so its type is nameable in declaration output (TS2883). */
6
- const withFixedWidth: Decorator = (Story) => (
7
- <div className="w-72">
8
- <Story />
9
- </div>
10
- );
11
-
12
- const meta = {
13
- title: "AI/Token Usage",
14
- component: TokenUsage,
15
- args: { used: 42000, limit: 200000 },
16
- argTypes: {
17
- used: { control: { type: "range", min: 0, max: 240000, step: 1000 } },
18
- },
19
- decorators: [withFixedWidth],
20
- } satisfies Meta<typeof TokenUsage>;
21
-
22
- export default meta;
23
- type Story = StoryObj<typeof meta>;
24
-
25
- export const Default: Story = {};
26
-
27
- /** Running out of context is a cliff, not a slope — so the gauge warns early. */
28
- export const Thresholds: Story = {
29
- parameters: { controls: { disable: true } },
30
- render: () => (
31
- <div className="grid gap-5">
32
- <TokenUsage used={42000} limit={200000} />
33
- <TokenUsage used={175000} limit={200000} />
34
- <TokenUsage used={212000} limit={200000} />
35
- </div>
36
- ),
37
- };
38
-
39
- export const CustomFormat: Story = {
40
- parameters: { controls: { disable: true } },
41
- render: () => (
42
- <TokenUsage
43
- used={128000}
44
- limit={200000}
45
- label="Context"
46
- format={(value) => `${String(Math.round(value / 1000))}k`}
47
- />
48
- ),
49
- };
50
-
51
- export const InlineCount: Story = {
52
- parameters: { controls: { disable: true } },
53
- render: () => (
54
- <div className="flex items-center gap-3">
55
- <TokenCount value={1284} />
56
- <TokenCount value={412} label="output tokens" />
57
- </div>
58
- ),
59
- };
@@ -1,83 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import { describe, expect, it } from "vitest";
3
-
4
- import { expectNoA11yViolations } from "../../../test/a11y";
5
- import { TokenCount, TokenUsage } from "./ai-token-usage";
6
-
7
- describe("TokenUsage", () => {
8
- it("shows the numbers, which are the actual content", () => {
9
- render(<TokenUsage used={12000} limit={200000} />);
10
- expect(screen.getByText("12,000 / 200,000")).toBeInTheDocument();
11
- });
12
-
13
- it("labels what is being measured", () => {
14
- render(<TokenUsage used={100} limit={1000} />);
15
- expect(screen.getByText("Context used")).toBeInTheDocument();
16
- });
17
-
18
- it("accepts a custom label and formatter", () => {
19
- render(
20
- <TokenUsage
21
- used={1500}
22
- limit={4000}
23
- label="Budget"
24
- format={(value) => `${String(Math.round(value / 1000))}k`}
25
- />,
26
- );
27
- expect(screen.getByText("Budget")).toBeInTheDocument();
28
- expect(screen.getByText("2k / 4k")).toBeInTheDocument();
29
- });
30
-
31
- it("hides the bar, since it only restates the numbers", () => {
32
- const { container } = render(<TokenUsage used={100} limit={1000} />);
33
- // Not a second progressbar to read past.
34
- expect(container.querySelector("[aria-hidden='true']")).toBeInTheDocument();
35
- expect(screen.queryByRole("progressbar")).not.toBeInTheDocument();
36
- });
37
-
38
- it("marks the warning state past the threshold", () => {
39
- const { container } = render(<TokenUsage used={900} limit={1000} />);
40
- expect(container.firstElementChild).toHaveAttribute("data-warning", "true");
41
- });
42
-
43
- it("does not warn below the threshold", () => {
44
- const { container } = render(<TokenUsage used={100} limit={1000} />);
45
- expect(container.firstElementChild).not.toHaveAttribute("data-warning");
46
- });
47
-
48
- it("marks going over the limit", () => {
49
- const { container } = render(<TokenUsage used={1200} limit={1000} />);
50
- expect(container.firstElementChild).toHaveAttribute("data-over", "true");
51
- // The figures stay readable even when over.
52
- expect(screen.getByText("1,200 / 1,000")).toBeInTheDocument();
53
- });
54
-
55
- it("clamps the bar rather than overflowing it", () => {
56
- const { container } = render(<TokenUsage used={5000} limit={1000} />);
57
- const fill = container.querySelector("[aria-hidden='true'] > div");
58
- expect(fill).toHaveStyle({ width: "100%" });
59
- });
60
-
61
- it("survives a zero limit without dividing by it", () => {
62
- const { container } = render(<TokenUsage used={10} limit={0} />);
63
- const fill = container.querySelector("[aria-hidden='true'] > div");
64
- expect(fill).toHaveStyle({ width: "0%" });
65
- });
66
-
67
- it("has no accessibility violations", async () => {
68
- const { container } = render(<TokenUsage used={180000} limit={200000} />);
69
- await expectNoA11yViolations(container);
70
- });
71
- });
72
-
73
- describe("TokenCount", () => {
74
- it("formats the count", () => {
75
- render(<TokenCount value={12345} />);
76
- expect(screen.getByText("12,345 tokens")).toBeInTheDocument();
77
- });
78
-
79
- it("accepts a custom unit", () => {
80
- render(<TokenCount value={42} label="output tokens" />);
81
- expect(screen.getByText("42 output tokens")).toBeInTheDocument();
82
- });
83
- });
@@ -1,96 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Tool, ToolContent, ToolHeader, ToolPayload, ToolSection } from "./ai-tool";
4
-
5
- /** Named so its type is nameable in declaration output (TS2883). */
6
- const withFixedWidth: Decorator = (Story) => (
7
- <div className="w-[32rem]">
8
- <Story />
9
- </div>
10
- );
11
-
12
- const meta = {
13
- title: "AI/Tool Call",
14
- component: Tool,
15
- args: { status: "success" },
16
- argTypes: {
17
- status: { control: "inline-radio", options: ["pending", "running", "success", "error"] },
18
- },
19
- decorators: [withFixedWidth],
20
- } satisfies Meta<typeof Tool>;
21
-
22
- export default meta;
23
- type Story = StoryObj<typeof meta>;
24
-
25
- export const Default: Story = {
26
- render: (args) => (
27
- <Tool {...args}>
28
- <ToolHeader name="search_web" status={args.status ?? "success"} />
29
- <ToolContent>
30
- <ToolSection label="Arguments">
31
- <ToolPayload label="Arguments">
32
- {JSON.stringify({ query: "accessible data tables", limit: 5 }, null, 2)}
33
- </ToolPayload>
34
- </ToolSection>
35
- <ToolSection label="Result">
36
- <ToolPayload label="Result">
37
- {JSON.stringify(
38
- { results: [{ title: "ARIA authoring practices", url: "https://example.org" }] },
39
- null,
40
- 2,
41
- )}
42
- </ToolPayload>
43
- </ToolSection>
44
- </ToolContent>
45
- </Tool>
46
- ),
47
- };
48
-
49
- /** Status is always a word, never only a colour. */
50
- export const Statuses: Story = {
51
- parameters: { controls: { disable: true } },
52
- render: () => (
53
- <div className="grid gap-2">
54
- {(["pending", "running", "success", "error"] as const).map((status) => (
55
- <Tool key={status} status={status}>
56
- <ToolHeader name={`tool_${status}`} status={status} />
57
- <ToolContent>
58
- <ToolSection label="Result">
59
- <ToolPayload label="Result">{`Status: ${status}`}</ToolPayload>
60
- </ToolSection>
61
- </ToolContent>
62
- </Tool>
63
- ))}
64
- </div>
65
- ),
66
- };
67
-
68
- export const CustomStatusWording: Story = {
69
- parameters: { controls: { disable: true } },
70
- render: () => (
71
- <Tool status="running">
72
- <ToolHeader name="crawl_site" status="running" statusLabel="Fetching page 12 of 40" />
73
- <ToolContent>
74
- <ToolSection label="Progress">
75
- <ToolPayload label="Progress">Crawling https://example.org…</ToolPayload>
76
- </ToolSection>
77
- </ToolContent>
78
- </Tool>
79
- ),
80
- };
81
-
82
- export const Failed: Story = {
83
- parameters: { controls: { disable: true } },
84
- render: () => (
85
- <Tool status="error" defaultOpen>
86
- <ToolHeader name="read_file" status="error" />
87
- <ToolContent>
88
- <ToolSection label="Error">
89
- <ToolPayload label="Error">
90
- {"ENOENT: no such file or directory, open '/app/missing.ts'"}
91
- </ToolPayload>
92
- </ToolSection>
93
- </ToolContent>
94
- </Tool>
95
- ),
96
- };
@@ -1,137 +0,0 @@
1
- import { render, screen, waitFor } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { describe, expect, it } from "vitest";
4
-
5
- import { expectNoA11yViolations } from "../../../test/a11y";
6
- import {
7
- Tool,
8
- ToolContent,
9
- ToolHeader,
10
- ToolPayload,
11
- ToolSection,
12
- type ToolStatus,
13
- } from "./ai-tool";
14
-
15
- function Example({ status = "success" }: { status?: ToolStatus } = {}) {
16
- return (
17
- <Tool status={status}>
18
- <ToolHeader name="search_web" status={status} />
19
- <ToolContent>
20
- <ToolSection label="Arguments">
21
- <ToolPayload label="Arguments">{'{ "query": "capital of France" }'}</ToolPayload>
22
- </ToolSection>
23
- <ToolSection label="Result">
24
- <ToolPayload label="Result">{'{ "answer": "Paris" }'}</ToolPayload>
25
- </ToolSection>
26
- </ToolContent>
27
- </Tool>
28
- );
29
- }
30
-
31
- describe("Tool", () => {
32
- it("is collapsed by default, because provenance is not the answer", () => {
33
- render(<Example />);
34
- expect(screen.getByRole("button", { name: /search_web/ })).toHaveAttribute(
35
- "aria-expanded",
36
- "false",
37
- );
38
- expect(screen.queryByText(/capital of France/)).not.toBeInTheDocument();
39
- });
40
-
41
- it("opens on click", async () => {
42
- const user = userEvent.setup();
43
- render(<Example />);
44
-
45
- await user.click(screen.getByRole("button", { name: /search_web/ }));
46
- expect(await screen.findByText(/capital of France/)).toBeInTheDocument();
47
- });
48
-
49
- it("opens from the keyboard", async () => {
50
- const user = userEvent.setup();
51
- render(<Example />);
52
-
53
- await user.tab();
54
- await user.keyboard("{Enter}");
55
- await waitFor(() => {
56
- expect(screen.getByRole("button", { name: /search_web/ })).toHaveAttribute(
57
- "aria-expanded",
58
- "true",
59
- );
60
- });
61
- });
62
-
63
- it.each([
64
- ["pending", "Queued"],
65
- ["running", "Running"],
66
- ["success", "Completed"],
67
- ["error", "Failed"],
68
- ] as const)("states the %s status in words, not only in colour", (status, label) => {
69
- render(<Example status={status} />);
70
- // A coloured dot says nothing to a screen reader, and nothing to anyone who
71
- // cannot distinguish the colours.
72
- expect(screen.getByRole("button", { name: new RegExp(label) })).toBeInTheDocument();
73
- });
74
-
75
- it("accepts custom status wording", () => {
76
- render(
77
- <Tool status="running">
78
- <ToolHeader name="fetch" status="running" statusLabel="Fetching page 2 of 5" />
79
- <ToolContent>Body</ToolContent>
80
- </Tool>,
81
- );
82
- expect(screen.getByText("Fetching page 2 of 5")).toBeInTheDocument();
83
- });
84
-
85
- it("marks the status for styling", () => {
86
- const { container } = render(<Example status="error" />);
87
- expect(container.querySelector("[data-slot='tool']")).toHaveAttribute(
88
- "data-status",
89
- "error",
90
- );
91
- });
92
-
93
- it("makes payloads focusable named regions", async () => {
94
- const user = userEvent.setup();
95
- render(<Example />);
96
-
97
- await user.click(screen.getByRole("button", { name: /search_web/ }));
98
-
99
- // JSON scrolls, and a scroll box that cannot take focus is unreachable by
100
- // keyboard.
101
- const payload = await screen.findByRole("region", { name: "Arguments" });
102
- expect(payload).toHaveAttribute("tabindex", "0");
103
- });
104
-
105
- it("labels each section", async () => {
106
- const user = userEvent.setup();
107
- render(<Example />);
108
-
109
- await user.click(screen.getByRole("button", { name: /search_web/ }));
110
- expect(await screen.findByText("Arguments")).toBeInTheDocument();
111
- expect(screen.getByText("Result")).toBeInTheDocument();
112
- });
113
-
114
- it("can be opened by default", () => {
115
- render(
116
- <Tool status="success" defaultOpen>
117
- <ToolHeader name="read_file" status="success" />
118
- <ToolContent>Contents</ToolContent>
119
- </Tool>,
120
- );
121
- expect(screen.getByText("Contents")).toBeInTheDocument();
122
- });
123
-
124
- it("has no accessibility violations when open", async () => {
125
- const { container } = render(
126
- <Tool status="success" defaultOpen>
127
- <ToolHeader name="search_web" status="success" />
128
- <ToolContent>
129
- <ToolSection label="Result">
130
- <ToolPayload label="Result">{'{ "answer": "Paris" }'}</ToolPayload>
131
- </ToolSection>
132
- </ToolContent>
133
- </Tool>,
134
- );
135
- await expectNoA11yViolations(container);
136
- });
137
- });