@dowel-ui/react 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +187 -0
  3. package/dist/components/ai-action-ledger/ai-action-ledger.d.ts +77 -0
  4. package/dist/components/ai-action-ledger/ai-action-ledger.d.ts.map +1 -0
  5. package/dist/components/ai-action-ledger/ai-action-ledger.js +224 -0
  6. package/dist/components/ai-action-ledger/ai-action-ledger.js.map +1 -0
  7. package/dist/components/ai-action-ledger/index.d.ts +2 -0
  8. package/dist/components/ai-action-ledger/index.js +2 -0
  9. package/dist/components/ai-action-ledger/meta.js +17 -0
  10. package/dist/components/ai-action-ledger/meta.js.map +1 -0
  11. package/dist/components/ai-agent-status/ai-agent-status.d.ts +1 -1
  12. package/dist/components/ai-inline-completion/ai-inline-completion.d.ts +45 -0
  13. package/dist/components/ai-inline-completion/ai-inline-completion.d.ts.map +1 -0
  14. package/dist/components/ai-inline-completion/ai-inline-completion.js +110 -0
  15. package/dist/components/ai-inline-completion/ai-inline-completion.js.map +1 -0
  16. package/dist/components/ai-inline-completion/index.d.ts +2 -0
  17. package/dist/components/ai-inline-completion/index.js +2 -0
  18. package/dist/components/ai-inline-completion/meta.js +17 -0
  19. package/dist/components/ai-inline-completion/meta.js.map +1 -0
  20. package/dist/components/ai-structured-output/ai-structured-output.d.ts +75 -0
  21. package/dist/components/ai-structured-output/ai-structured-output.d.ts.map +1 -0
  22. package/dist/components/ai-structured-output/ai-structured-output.js +126 -0
  23. package/dist/components/ai-structured-output/ai-structured-output.js.map +1 -0
  24. package/dist/components/ai-structured-output/index.d.ts +2 -0
  25. package/dist/components/ai-structured-output/index.js +2 -0
  26. package/dist/components/ai-structured-output/meta.js +17 -0
  27. package/dist/components/ai-structured-output/meta.js.map +1 -0
  28. package/dist/components/ai-tool/ai-tool.d.ts +1 -1
  29. package/dist/components/alert/alert.d.ts +1 -1
  30. package/dist/components/avatar/avatar.d.ts +1 -1
  31. package/dist/components/badge/badge.d.ts +1 -1
  32. package/dist/components/button/button.d.ts +1 -1
  33. package/dist/components/meter/index.d.ts +2 -0
  34. package/dist/components/meter/index.js +2 -0
  35. package/dist/components/meter/meta.js +17 -0
  36. package/dist/components/meter/meta.js.map +1 -0
  37. package/dist/components/meter/meter.d.ts +69 -0
  38. package/dist/components/meter/meter.d.ts.map +1 -0
  39. package/dist/components/meter/meter.js +144 -0
  40. package/dist/components/meter/meter.js.map +1 -0
  41. package/dist/components/metric-delta/index.d.ts +2 -0
  42. package/dist/components/metric-delta/index.js +2 -0
  43. package/dist/components/metric-delta/meta.js +17 -0
  44. package/dist/components/metric-delta/meta.js.map +1 -0
  45. package/dist/components/metric-delta/metric-delta.d.ts +59 -0
  46. package/dist/components/metric-delta/metric-delta.d.ts.map +1 -0
  47. package/dist/components/metric-delta/metric-delta.js +82 -0
  48. package/dist/components/metric-delta/metric-delta.js.map +1 -0
  49. package/dist/components/progress/progress.d.ts +1 -1
  50. package/dist/components/record-diff/index.d.ts +2 -0
  51. package/dist/components/record-diff/index.js +2 -0
  52. package/dist/components/record-diff/meta.js +17 -0
  53. package/dist/components/record-diff/meta.js.map +1 -0
  54. package/dist/components/record-diff/record-diff.d.ts +56 -0
  55. package/dist/components/record-diff/record-diff.d.ts.map +1 -0
  56. package/dist/components/record-diff/record-diff.js +199 -0
  57. package/dist/components/record-diff/record-diff.js.map +1 -0
  58. package/dist/components/spinner/spinner.d.ts +1 -1
  59. package/dist/components/toast/toast.d.ts +1 -1
  60. package/dist/index.d.ts +13 -1
  61. package/dist/index.js +7 -1
  62. package/dist/registry/components.d.ts.map +1 -1
  63. package/dist/registry/components.js +57 -45
  64. package/dist/registry/components.js.map +1 -1
  65. package/package.json +40 -5
  66. package/src/components/ai-action-ledger/ai-action-ledger.tsx +381 -0
  67. package/src/components/ai-action-ledger/index.ts +14 -0
  68. package/src/components/ai-action-ledger/meta.ts +19 -0
  69. package/src/components/ai-inline-completion/ai-inline-completion.tsx +214 -0
  70. package/src/components/ai-inline-completion/index.ts +1 -0
  71. package/src/components/ai-inline-completion/meta.ts +20 -0
  72. package/src/components/ai-structured-output/ai-structured-output.tsx +244 -0
  73. package/src/components/ai-structured-output/index.ts +10 -0
  74. package/src/components/ai-structured-output/meta.ts +18 -0
  75. package/src/components/meter/index.ts +8 -0
  76. package/src/components/meter/meta.ts +20 -0
  77. package/src/components/meter/meter.tsx +219 -0
  78. package/src/components/metric-delta/index.ts +6 -0
  79. package/src/components/metric-delta/meta.ts +18 -0
  80. package/src/components/metric-delta/metric-delta.tsx +180 -0
  81. package/src/components/record-diff/index.ts +10 -0
  82. package/src/components/record-diff/meta.ts +19 -0
  83. package/src/components/record-diff/record-diff.tsx +341 -0
  84. package/src/index.ts +6 -0
  85. package/src/registry/components.ts +12 -0
  86. package/src/blocks/admin-users/admin-users.stories.tsx +0 -83
  87. package/src/blocks/admin-users/admin-users.test.tsx +0 -159
  88. package/src/blocks/ai-chat/ai-chat.stories.tsx +0 -149
  89. package/src/blocks/ai-chat/ai-chat.test.tsx +0 -166
  90. package/src/blocks/dashboard/dashboard.stories.tsx +0 -106
  91. package/src/blocks/dashboard/dashboard.test.tsx +0 -94
  92. package/src/blocks/forgot-password/forgot-password.stories.tsx +0 -41
  93. package/src/blocks/forgot-password/forgot-password.test.tsx +0 -62
  94. package/src/blocks/login/login.stories.tsx +0 -46
  95. package/src/blocks/login/login.test.tsx +0 -100
  96. package/src/blocks/pricing/pricing.stories.tsx +0 -62
  97. package/src/blocks/pricing/pricing.test.tsx +0 -101
  98. package/src/blocks/settings/settings.stories.tsx +0 -94
  99. package/src/blocks/settings/settings.test.tsx +0 -167
  100. package/src/blocks/signup/signup.stories.tsx +0 -27
  101. package/src/blocks/signup/signup.test.tsx +0 -97
  102. package/src/components/accordion/accordion.stories.tsx +0 -90
  103. package/src/components/accordion/accordion.test.tsx +0 -156
  104. package/src/components/activity-feed/activity-feed.stories.tsx +0 -163
  105. package/src/components/activity-feed/activity-feed.test.tsx +0 -125
  106. package/src/components/ai-agent-status/ai-agent-status.stories.tsx +0 -86
  107. package/src/components/ai-agent-status/ai-agent-status.test.tsx +0 -66
  108. package/src/components/ai-conversation/ai-conversation.stories.tsx +0 -126
  109. package/src/components/ai-conversation/ai-conversation.test.tsx +0 -144
  110. package/src/components/ai-message/ai-message.stories.tsx +0 -106
  111. package/src/components/ai-message/ai-message.test.tsx +0 -160
  112. package/src/components/ai-model-selector/ai-model-selector.stories.tsx +0 -66
  113. package/src/components/ai-model-selector/ai-model-selector.test.tsx +0 -120
  114. package/src/components/ai-prompt-input/ai-prompt-input.stories.tsx +0 -145
  115. package/src/components/ai-prompt-input/ai-prompt-input.test.tsx +0 -273
  116. package/src/components/ai-reasoning/ai-reasoning.stories.tsx +0 -60
  117. package/src/components/ai-reasoning/ai-reasoning.test.tsx +0 -95
  118. package/src/components/ai-response/ai-response.stories.tsx +0 -75
  119. package/src/components/ai-response/ai-response.test.tsx +0 -96
  120. package/src/components/ai-sources/ai-sources.stories.tsx +0 -93
  121. package/src/components/ai-sources/ai-sources.test.tsx +0 -102
  122. package/src/components/ai-token-usage/ai-token-usage.stories.tsx +0 -59
  123. package/src/components/ai-token-usage/ai-token-usage.test.tsx +0 -83
  124. package/src/components/ai-tool/ai-tool.stories.tsx +0 -96
  125. package/src/components/ai-tool/ai-tool.test.tsx +0 -137
  126. package/src/components/alert/alert.stories.tsx +0 -101
  127. package/src/components/alert/alert.test.tsx +0 -94
  128. package/src/components/avatar/avatar.stories.tsx +0 -58
  129. package/src/components/avatar/avatar.test.tsx +0 -64
  130. package/src/components/badge/badge.stories.tsx +0 -68
  131. package/src/components/badge/badge.test.tsx +0 -61
  132. package/src/components/button/button.stories.tsx +0 -128
  133. package/src/components/button/button.test.tsx +0 -224
  134. package/src/components/calendar/calendar.stories.tsx +0 -105
  135. package/src/components/calendar/calendar.test.tsx +0 -255
  136. package/src/components/card/card.stories.tsx +0 -61
  137. package/src/components/card/card.test.tsx +0 -66
  138. package/src/components/checkbox/checkbox.stories.tsx +0 -124
  139. package/src/components/checkbox/checkbox.test.tsx +0 -121
  140. package/src/components/code-block/code-block.stories.tsx +0 -62
  141. package/src/components/code-block/code-block.test.tsx +0 -139
  142. package/src/components/combobox/combobox.stories.tsx +0 -171
  143. package/src/components/combobox/combobox.test.tsx +0 -429
  144. package/src/components/command/command.stories.tsx +0 -152
  145. package/src/components/command/command.test.tsx +0 -380
  146. package/src/components/data-table/data-table.stories.tsx +0 -151
  147. package/src/components/data-table/data-table.test.tsx +0 -288
  148. package/src/components/date-picker/date-picker.stories.tsx +0 -103
  149. package/src/components/date-picker/date-picker.test.tsx +0 -205
  150. package/src/components/dialog/dialog.stories.tsx +0 -140
  151. package/src/components/dialog/dialog.test.tsx +0 -186
  152. package/src/components/drawer/drawer.stories.tsx +0 -111
  153. package/src/components/drawer/drawer.test.tsx +0 -187
  154. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +0 -128
  155. package/src/components/dropdown-menu/dropdown-menu.test.tsx +0 -321
  156. package/src/components/empty-state/empty-state.stories.tsx +0 -70
  157. package/src/components/empty-state/empty-state.test.tsx +0 -97
  158. package/src/components/form/form.stories.tsx +0 -144
  159. package/src/components/form/form.test.tsx +0 -203
  160. package/src/components/input/input.stories.tsx +0 -84
  161. package/src/components/input/input.test.tsx +0 -108
  162. package/src/components/label/label.stories.tsx +0 -51
  163. package/src/components/label/label.test.tsx +0 -55
  164. package/src/components/pagination/pagination.stories.tsx +0 -129
  165. package/src/components/pagination/pagination.test.tsx +0 -119
  166. package/src/components/popover/popover.stories.tsx +0 -84
  167. package/src/components/popover/popover.test.tsx +0 -163
  168. package/src/components/progress/progress.stories.tsx +0 -90
  169. package/src/components/progress/progress.test.tsx +0 -78
  170. package/src/components/radio-group/radio-group.stories.tsx +0 -102
  171. package/src/components/radio-group/radio-group.test.tsx +0 -152
  172. package/src/components/select/select.stories.tsx +0 -137
  173. package/src/components/select/select.test.tsx +0 -209
  174. package/src/components/separator/separator.stories.tsx +0 -42
  175. package/src/components/separator/separator.test.tsx +0 -46
  176. package/src/components/sheet/sheet.stories.tsx +0 -107
  177. package/src/components/sheet/sheet.test.tsx +0 -152
  178. package/src/components/skeleton/skeleton.stories.tsx +0 -46
  179. package/src/components/skeleton/skeleton.test.tsx +0 -57
  180. package/src/components/slider/slider.stories.tsx +0 -87
  181. package/src/components/slider/slider.test.tsx +0 -148
  182. package/src/components/spinner/spinner.stories.tsx +0 -47
  183. package/src/components/spinner/spinner.test.tsx +0 -41
  184. package/src/components/switch/switch.stories.tsx +0 -70
  185. package/src/components/switch/switch.test.tsx +0 -101
  186. package/src/components/table/table.stories.tsx +0 -131
  187. package/src/components/table/table.test.tsx +0 -153
  188. package/src/components/tabs/tabs.stories.tsx +0 -139
  189. package/src/components/tabs/tabs.test.tsx +0 -148
  190. package/src/components/toast/toast.stories.tsx +0 -167
  191. package/src/components/toast/toast.test.tsx +0 -306
  192. package/src/components/tooltip/tooltip.stories.tsx +0 -89
  193. package/src/components/tooltip/tooltip.test.tsx +0 -120
  194. package/src/lib/utils.test.ts +0 -32
  195. 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
- };