@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,145 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
- import { Paperclip } from "lucide-react";
3
- import { useState } from "react";
4
-
5
- import { ModelSelector } from "@/components/ai-model-selector";
6
- import { Button } from "@/components/button";
7
-
8
- import {
9
- PromptInput,
10
- PromptInputCounter,
11
- PromptInputSubmit,
12
- PromptInputTextarea,
13
- PromptInputToolbar,
14
- } from "./ai-prompt-input";
15
-
16
- const meta: Meta<typeof PromptInput> = {
17
- title: "AI/Prompt Input",
18
- component: PromptInput,
19
- parameters: { controls: { disable: true } },
20
- decorators: [
21
- (Story) => (
22
- <div className="w-[34rem]">
23
- <Story />
24
- </div>
25
- ),
26
- ],
27
- };
28
-
29
- export default meta;
30
- type Story = StoryObj<typeof PromptInput>;
31
-
32
- /** Enter sends, Shift+Enter adds a line — and neither fires mid-IME-composition. */
33
- export const Default: Story = {
34
- render: function Default() {
35
- const [sent, setSent] = useState<string[]>([]);
36
- const [value, setValue] = useState("");
37
-
38
- return (
39
- <div className="grid gap-3">
40
- <PromptInput
41
- onSubmit={(event) => {
42
- event.preventDefault();
43
- if (!value.trim()) return;
44
- setSent((current) => [...current, value.trim()]);
45
- setValue("");
46
- }}
47
- >
48
- <PromptInputTextarea
49
- aria-label="Message"
50
- placeholder="Ask anything…"
51
- value={value}
52
- onChange={(event) => {
53
- setValue(event.target.value);
54
- }}
55
- />
56
- <PromptInputToolbar>
57
- <Button variant="ghost" size="icon-sm" aria-label="Attach a file">
58
- <Paperclip />
59
- </Button>
60
- <PromptInputSubmit />
61
- </PromptInputToolbar>
62
- </PromptInput>
63
- {sent.length > 0 ? (
64
- <ul className="text-xs text-muted-foreground">
65
- {sent.map((message, index) => (
66
- <li key={index}>Sent: {message}</li>
67
- ))}
68
- </ul>
69
- ) : null}
70
- </div>
71
- );
72
- },
73
- };
74
-
75
- /** While a response streams, the one control becomes Stop — name included. */
76
- export const Busy: Story = {
77
- render: function Busy() {
78
- const [busy, setBusy] = useState(true);
79
-
80
- return (
81
- <PromptInput
82
- busy={busy}
83
- onSubmit={(event) => {
84
- event.preventDefault();
85
- }}
86
- >
87
- <PromptInputTextarea aria-label="Message" placeholder="Generating…" />
88
- <PromptInputToolbar>
89
- <PromptInputSubmit
90
- onStop={() => {
91
- setBusy(false);
92
- }}
93
- />
94
- </PromptInputToolbar>
95
- </PromptInput>
96
- );
97
- },
98
- };
99
-
100
- export const WithModelAndCounter: Story = {
101
- render: function WithModelAndCounter() {
102
- const [value, setValue] = useState("");
103
-
104
- return (
105
- <PromptInput
106
- onSubmit={(event) => {
107
- event.preventDefault();
108
- }}
109
- >
110
- <PromptInputTextarea
111
- aria-label="Message"
112
- placeholder="Ask anything…"
113
- value={value}
114
- onChange={(event) => {
115
- setValue(event.target.value);
116
- }}
117
- />
118
- <PromptInputToolbar>
119
- <ModelSelector
120
- aria-label="Model"
121
- defaultValue="balanced"
122
- triggerSize="sm"
123
- models={[
124
- { id: "fast", name: "Fast", description: "Quick answers." },
125
- { id: "balanced", name: "Balanced", description: "The default." },
126
- ]}
127
- />
128
- <PromptInputCounter value={value.length} max={280} />
129
- <PromptInputSubmit />
130
- </PromptInputToolbar>
131
- </PromptInput>
132
- );
133
- },
134
- };
135
-
136
- export const Disabled: Story = {
137
- render: () => (
138
- <PromptInput disabled>
139
- <PromptInputTextarea aria-label="Message" placeholder="Sign in to send a message" />
140
- <PromptInputToolbar>
141
- <PromptInputSubmit />
142
- </PromptInputToolbar>
143
- </PromptInput>
144
- ),
145
- };
@@ -1,273 +0,0 @@
1
- import { fireEvent, render, screen } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { useState } from "react";
4
- import { describe, expect, it, vi } from "vitest";
5
-
6
- import { expectNoA11yViolations } from "../../../test/a11y";
7
- import {
8
- PromptInput,
9
- PromptInputCounter,
10
- PromptInputSubmit,
11
- PromptInputTextarea,
12
- PromptInputToolbar,
13
- } from "./ai-prompt-input";
14
-
15
- function Example({
16
- onSubmit,
17
- onStop,
18
- busy = false,
19
- disabled = false,
20
- submitOnEnter = true,
21
- }: {
22
- onSubmit?: () => void;
23
- onStop?: () => void;
24
- busy?: boolean;
25
- disabled?: boolean;
26
- submitOnEnter?: boolean;
27
- } = {}) {
28
- return (
29
- <PromptInput
30
- busy={busy}
31
- disabled={disabled}
32
- onSubmit={(event) => {
33
- event.preventDefault();
34
- onSubmit?.();
35
- }}
36
- >
37
- <PromptInputTextarea
38
- aria-label="Message"
39
- placeholder="Ask anything…"
40
- submitOnEnter={submitOnEnter}
41
- />
42
- <PromptInputToolbar>
43
- <PromptInputSubmit onStop={onStop} />
44
- </PromptInputToolbar>
45
- </PromptInput>
46
- );
47
- }
48
-
49
- describe("PromptInput", () => {
50
- it("submits on Enter", async () => {
51
- const onSubmit = vi.fn();
52
- const user = userEvent.setup();
53
- render(<Example onSubmit={onSubmit} />);
54
-
55
- await user.click(screen.getByRole("textbox"));
56
- await user.keyboard("Hello{Enter}");
57
- expect(onSubmit).toHaveBeenCalledTimes(1);
58
- });
59
-
60
- it("inserts a newline on Shift+Enter instead of submitting", async () => {
61
- const onSubmit = vi.fn();
62
- const user = userEvent.setup();
63
- render(<Example onSubmit={onSubmit} />);
64
-
65
- const textarea = screen.getByRole<HTMLTextAreaElement>("textbox");
66
- await user.click(textarea);
67
- await user.keyboard("Hello{Shift>}{Enter}{/Shift}world");
68
-
69
- expect(onSubmit).not.toHaveBeenCalled();
70
- expect(textarea.value).toBe("Hello\nworld");
71
- });
72
-
73
- it("does not submit mid-IME-composition", () => {
74
- const onSubmit = vi.fn();
75
- render(<Example onSubmit={onSubmit} />);
76
-
77
- const textarea = screen.getByRole("textbox");
78
- // Enter while an IME composition is active confirms the candidate — for
79
- // Japanese, Chinese and Korean input it is part of typing a word. Sending
80
- // there truncates the sentence, and it is the most common way a chat
81
- // composer breaks for those users.
82
- fireEvent.keyDown(textarea, { key: "Enter", isComposing: true });
83
- expect(onSubmit).not.toHaveBeenCalled();
84
-
85
- fireEvent.keyDown(textarea, { key: "Enter", isComposing: false });
86
- expect(onSubmit).toHaveBeenCalledTimes(1);
87
- });
88
-
89
- it("can leave Enter as a newline entirely", async () => {
90
- const onSubmit = vi.fn();
91
- const user = userEvent.setup();
92
- render(<Example onSubmit={onSubmit} submitOnEnter={false} />);
93
-
94
- await user.click(screen.getByRole("textbox"));
95
- await user.keyboard("Hello{Enter}");
96
- expect(onSubmit).not.toHaveBeenCalled();
97
- });
98
-
99
- it("submits from the send button", async () => {
100
- const onSubmit = vi.fn();
101
- const user = userEvent.setup();
102
- render(<Example onSubmit={onSubmit} />);
103
-
104
- await user.click(screen.getByRole("button", { name: "Send message" }));
105
- expect(onSubmit).toHaveBeenCalledTimes(1);
106
- });
107
-
108
- describe("while busy", () => {
109
- it("renames the control to say what it now does", () => {
110
- render(<Example busy />);
111
- expect(screen.getByRole("button", { name: "Stop generating" })).toBeInTheDocument();
112
- expect(screen.queryByRole("button", { name: "Send message" })).not.toBeInTheDocument();
113
- });
114
-
115
- it("stops instead of submitting", async () => {
116
- const onSubmit = vi.fn();
117
- const onStop = vi.fn();
118
- const user = userEvent.setup();
119
- render(<Example busy onSubmit={onSubmit} onStop={onStop} />);
120
-
121
- await user.click(screen.getByRole("button", { name: "Stop generating" }));
122
- expect(onStop).toHaveBeenCalledTimes(1);
123
- expect(onSubmit).not.toHaveBeenCalled();
124
- });
125
-
126
- it("ignores Enter", async () => {
127
- const onSubmit = vi.fn();
128
- const user = userEvent.setup();
129
- render(<Example busy onSubmit={onSubmit} />);
130
-
131
- await user.click(screen.getByRole("textbox"));
132
- await user.keyboard("Hi{Enter}");
133
- expect(onSubmit).not.toHaveBeenCalled();
134
- });
135
- });
136
-
137
- it("does not submit while disabled", async () => {
138
- const onSubmit = vi.fn();
139
- const user = userEvent.setup();
140
- render(<Example disabled onSubmit={onSubmit} />);
141
-
142
- expect(screen.getByRole("textbox")).toBeDisabled();
143
- await user.click(screen.getByRole("button", { name: "Send message" }));
144
- expect(onSubmit).not.toHaveBeenCalled();
145
- });
146
-
147
- describe("auto-growing", () => {
148
- /**
149
- * jsdom performs no layout, so scrollHeight is always 0. Stubbing the
150
- * prototype getter before render is what lets the sizing logic run at all;
151
- * without it this would assert against a stub of nothing.
152
- */
153
- function withScrollHeight(height: number, run: () => void) {
154
- const original = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "scrollHeight");
155
- Object.defineProperty(HTMLElement.prototype, "scrollHeight", {
156
- configurable: true,
157
- get: () => height,
158
- });
159
-
160
- try {
161
- run();
162
- } finally {
163
- if (original) Object.defineProperty(HTMLElement.prototype, "scrollHeight", original);
164
- }
165
- }
166
-
167
- it("sizes itself to its content", () => {
168
- withScrollHeight(120, () => {
169
- render(<Example />);
170
- expect(screen.getByRole<HTMLTextAreaElement>("textbox").style.height).toBe("120px");
171
- });
172
- });
173
-
174
- it("stops growing at maxRows and scrolls instead", () => {
175
- withScrollHeight(4000, () => {
176
- render(
177
- <PromptInput>
178
- <PromptInputTextarea aria-label="Message" maxRows={4} />
179
- </PromptInput>,
180
- );
181
-
182
- const textarea = screen.getByRole<HTMLTextAreaElement>("textbox");
183
- // jsdom reports no line-height, so the component falls back to 20px.
184
- expect(textarea.style.height).toBe("80px");
185
- expect(textarea.style.overflowY).toBe("auto");
186
- });
187
- });
188
-
189
- it("does not scroll while it still fits", () => {
190
- withScrollHeight(40, () => {
191
- render(<Example />);
192
- expect(screen.getByRole<HTMLTextAreaElement>("textbox").style.overflowY).toBe("hidden");
193
- });
194
- });
195
- });
196
-
197
- it("throws a useful error if a part is used outside the root", () => {
198
- const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
199
- expect(() => render(<PromptInputSubmit />)).toThrow(
200
- /must be rendered inside <PromptInput>/,
201
- );
202
- consoleError.mockRestore();
203
- });
204
-
205
- it("has no accessibility violations", async () => {
206
- const { container } = render(<Example />);
207
- await expectNoA11yViolations(container);
208
- });
209
- });
210
-
211
- describe("PromptInputCounter", () => {
212
- function Counter({ value, max = 100 }: { value: number; max?: number }) {
213
- return <PromptInputCounter value={value} max={max} />;
214
- }
215
-
216
- it("shows the count", () => {
217
- render(<Counter value={12} />);
218
- expect(screen.getByText("12")).toBeInTheDocument();
219
- });
220
-
221
- it("stays silent during ordinary typing", () => {
222
- render(<Counter value={12} />);
223
- // The region exists and is live from the start, but has nothing in it —
224
- // announcing every keystroke would be unusable.
225
- expect(screen.getByRole("status")).toHaveTextContent("");
226
- });
227
-
228
- it("announces once the limit is close", () => {
229
- render(<Counter value={95} />);
230
- expect(screen.getByRole("status")).toHaveTextContent("5 characters remaining");
231
- });
232
-
233
- it("announces going over the limit", () => {
234
- render(<Counter value={110} />);
235
- expect(screen.getByRole("status")).toHaveTextContent("Over the limit by 10 characters");
236
- });
237
-
238
- it("is live from the start, so the first warning is not missed", () => {
239
- // A region that only becomes live when it matters misses the very change
240
- // that made it matter.
241
- render(<Counter value={1} />);
242
- expect(screen.getByRole("status")).toHaveAttribute("aria-live", "polite");
243
- });
244
-
245
- it("shows the limit visually once warning", () => {
246
- render(<Counter value={95} />);
247
- expect(screen.getByText("95 / 100")).toBeInTheDocument();
248
- });
249
-
250
- it("updates as the value changes", async () => {
251
- const user = userEvent.setup();
252
-
253
- function Live() {
254
- const [value, setValue] = useState("");
255
- return (
256
- <>
257
- <textarea
258
- aria-label="Message"
259
- value={value}
260
- onChange={(event) => {
261
- setValue(event.target.value);
262
- }}
263
- />
264
- <PromptInputCounter value={value.length} max={10} />
265
- </>
266
- );
267
- }
268
-
269
- render(<Live />);
270
- await user.type(screen.getByRole("textbox"), "abcdefghij");
271
- expect(screen.getByRole("status")).toHaveTextContent("0 characters remaining");
272
- });
273
- });
@@ -1,60 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Response } from "@/components/ai-response";
4
-
5
- import { Reasoning, ReasoningContent, ReasoningTrigger } from "./ai-reasoning";
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/Reasoning",
16
- component: Reasoning,
17
- parameters: { controls: { disable: true } },
18
- decorators: [withFixedWidth],
19
- } satisfies Meta<typeof Reasoning>;
20
-
21
- export default meta;
22
- type Story = StoryObj<typeof meta>;
23
-
24
- const WORKING =
25
- 'The question is about keyboard access to overflowing tables.\n\nA scroll container is not focusable by default, so its content past the edge cannot be reached without a pointer. Adding tabindex="0" makes it a focus stop, and a role plus a name stops it being an unlabelled one.';
26
-
27
- /** Collapsed by default: reasoning is supporting material, not the answer. */
28
- export const Default: Story = {
29
- render: () => (
30
- <div className="grid gap-2">
31
- <Reasoning>
32
- <ReasoningTrigger />
33
- <ReasoningContent>{WORKING}</ReasoningContent>
34
- </Reasoning>
35
- <Response>
36
- Give the scrolling wrapper `tabindex=&quot;0&quot;`, a role and an accessible name.
37
- </Response>
38
- </div>
39
- ),
40
- };
41
-
42
- export const Streaming: Story = {
43
- render: () => (
44
- <Reasoning defaultOpen>
45
- <ReasoningTrigger streaming />
46
- <ReasoningContent>
47
- The question is about keyboard access to overflowing tables. A scroll container
48
- </ReasoningContent>
49
- </Reasoning>
50
- ),
51
- };
52
-
53
- export const Expanded: Story = {
54
- render: () => (
55
- <Reasoning defaultOpen>
56
- <ReasoningTrigger />
57
- <ReasoningContent>{WORKING}</ReasoningContent>
58
- </Reasoning>
59
- ),
60
- };
@@ -1,95 +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 { Reasoning, ReasoningContent, ReasoningTrigger } from "./ai-reasoning";
7
-
8
- describe("Reasoning", () => {
9
- it("is collapsed by default, so the answer is not buried", () => {
10
- render(
11
- <Reasoning>
12
- <ReasoningTrigger />
13
- <ReasoningContent>Step one, then step two.</ReasoningContent>
14
- </Reasoning>,
15
- );
16
-
17
- expect(screen.getByRole("button", { name: "Reasoning" })).toHaveAttribute(
18
- "aria-expanded",
19
- "false",
20
- );
21
- expect(screen.queryByText("Step one, then step two.")).not.toBeInTheDocument();
22
- });
23
-
24
- it("opens on click", async () => {
25
- const user = userEvent.setup();
26
- render(
27
- <Reasoning>
28
- <ReasoningTrigger />
29
- <ReasoningContent>Step one, then step two.</ReasoningContent>
30
- </Reasoning>,
31
- );
32
-
33
- await user.click(screen.getByRole("button", { name: "Reasoning" }));
34
- expect(await screen.findByText("Step one, then step two.")).toBeInTheDocument();
35
- });
36
-
37
- it("says it is thinking while reasoning streams", () => {
38
- render(
39
- <Reasoning>
40
- <ReasoningTrigger streaming />
41
- <ReasoningContent>Partial…</ReasoningContent>
42
- </Reasoning>,
43
- );
44
- expect(screen.getByRole("button", { name: "Thinking…" })).toBeInTheDocument();
45
- });
46
-
47
- it("accepts custom labels", () => {
48
- render(
49
- <Reasoning>
50
- <ReasoningTrigger label="Show working" />
51
- <ReasoningContent>Work</ReasoningContent>
52
- </Reasoning>,
53
- );
54
- expect(screen.getByRole("button", { name: "Show working" })).toBeInTheDocument();
55
- });
56
-
57
- it("can be opened by default", () => {
58
- render(
59
- <Reasoning defaultOpen>
60
- <ReasoningTrigger />
61
- <ReasoningContent>Visible</ReasoningContent>
62
- </Reasoning>,
63
- );
64
- expect(screen.getByText("Visible")).toBeInTheDocument();
65
- });
66
-
67
- it("toggles from the keyboard", async () => {
68
- const user = userEvent.setup();
69
- render(
70
- <Reasoning>
71
- <ReasoningTrigger />
72
- <ReasoningContent>Work</ReasoningContent>
73
- </Reasoning>,
74
- );
75
-
76
- await user.tab();
77
- await user.keyboard("{Enter}");
78
- await waitFor(() => {
79
- expect(screen.getByRole("button", { name: "Reasoning" })).toHaveAttribute(
80
- "aria-expanded",
81
- "true",
82
- );
83
- });
84
- });
85
-
86
- it("has no accessibility violations", async () => {
87
- const { container } = render(
88
- <Reasoning defaultOpen>
89
- <ReasoningTrigger />
90
- <ReasoningContent>Step one, then step two.</ReasoningContent>
91
- </Reasoning>,
92
- );
93
- await expectNoA11yViolations(container);
94
- });
95
- });
@@ -1,75 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
- import { useEffect, useState } from "react";
3
-
4
- import { Response, ThinkingIndicator } from "./ai-response";
5
-
6
- const FULL =
7
- "Streaming works one token at a time. The caret marks where the text stops, and nothing is announced as it arrives — a live region firing on every token is unusable with a screen reader.";
8
-
9
- /** Named so its type is nameable in declaration output (TS2883). */
10
- const withFixedWidth: Decorator = (Story) => (
11
- <div className="w-[32rem]">
12
- <Story />
13
- </div>
14
- );
15
-
16
- const meta = {
17
- title: "AI/Response",
18
- component: Response,
19
- args: { streaming: false, children: FULL },
20
- argTypes: { streaming: { control: "boolean" } },
21
- decorators: [withFixedWidth],
22
- } satisfies Meta<typeof Response>;
23
-
24
- export default meta;
25
- type Story = StoryObj<typeof meta>;
26
-
27
- export const Default: Story = {};
28
-
29
- export const Streaming: Story = {
30
- args: { streaming: true, children: "Streaming works one token at a time. The caret" },
31
- };
32
-
33
- /** The real thing: text arriving, then settling. */
34
- export const Live: Story = {
35
- parameters: { controls: { disable: true } },
36
- render: function Live() {
37
- const [length, setLength] = useState(0);
38
-
39
- useEffect(() => {
40
- const timer = setInterval(() => {
41
- setLength((current) => (current >= FULL.length ? 0 : current + 2));
42
- }, 40);
43
- return () => {
44
- clearInterval(timer);
45
- };
46
- }, []);
47
-
48
- const done = length >= FULL.length;
49
- return <Response streaming={!done}>{FULL.slice(0, length)}</Response>;
50
- },
51
- };
52
-
53
- /** Before the first token there is nothing to put a caret after. */
54
- export const Thinking: Story = {
55
- parameters: { controls: { disable: true } },
56
- render: () => <ThinkingIndicator />,
57
- };
58
-
59
- export const WithRenderedMarkdown: Story = {
60
- parameters: { controls: { disable: true } },
61
- render: () => (
62
- <Response>
63
- <p>Three things matter here:</p>
64
- <ul>
65
- <li>
66
- The transcript is a <code>list</code>, not a live region.
67
- </li>
68
- <li>State is announced; content is not.</li>
69
- <li>
70
- See <a href="#a11y">the accessibility notes</a>.
71
- </li>
72
- </ul>
73
- </Response>
74
- ),
75
- };