@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,144 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
- import { useState } from "react";
3
-
4
- import { Button } from "@/components/button";
5
- import { Checkbox } from "@/components/checkbox";
6
- import { Input } from "@/components/input";
7
- import {
8
- Select,
9
- SelectContent,
10
- SelectItem,
11
- SelectTrigger,
12
- SelectValue,
13
- } from "@/components/select";
14
-
15
- import { Form, FormControl, FormDescription, FormField, FormLabel, FormMessage } from "./form";
16
-
17
- /** Named so its type is nameable in declaration output (TS2883). */
18
- const withFixedWidth: Decorator = (Story) => (
19
- <div className="w-80">
20
- <Story />
21
- </div>
22
- );
23
-
24
- const meta = {
25
- title: "Forms/Form",
26
- component: Form,
27
- parameters: { controls: { disable: true } },
28
- decorators: [withFixedWidth],
29
- } satisfies Meta<typeof Form>;
30
-
31
- export default meta;
32
- type Story = StoryObj<typeof meta>;
33
-
34
- export const Default: Story = {
35
- render: () => (
36
- <Form>
37
- <FormField name="username">
38
- <FormLabel>Username</FormLabel>
39
- <FormControl>
40
- <Input placeholder="ada" />
41
- </FormControl>
42
- <FormDescription>This is your public display name.</FormDescription>
43
- <FormMessage />
44
- </FormField>
45
- <Button type="submit">Save</Button>
46
- </Form>
47
- ),
48
- };
49
-
50
- export const WithError: Story = {
51
- render: () => (
52
- <Form>
53
- <FormField name="email" error="Enter a valid email address.">
54
- <FormLabel>Email</FormLabel>
55
- <FormControl>
56
- <Input type="email" defaultValue="not-an-email" />
57
- </FormControl>
58
- <FormDescription>We will never share it.</FormDescription>
59
- <FormMessage />
60
- </FormField>
61
- <Button type="submit">Save</Button>
62
- </Form>
63
- ),
64
- };
65
-
66
- /**
67
- * The field wiring is state-agnostic — this uses plain `useState`, and the
68
- * markup would be identical with React Hook Form or a server action. Only the
69
- * source of `error` changes.
70
- */
71
- export const LiveValidation: Story = {
72
- render: function LiveValidation() {
73
- const [email, setEmail] = useState("");
74
- const [touched, setTouched] = useState(false);
75
- const error = touched && !email.includes("@") ? "Enter a valid email address." : undefined;
76
-
77
- return (
78
- <Form
79
- onSubmit={(event) => {
80
- event.preventDefault();
81
- setTouched(true);
82
- }}
83
- >
84
- <FormField name="email" error={error}>
85
- <FormLabel>Email</FormLabel>
86
- <FormControl>
87
- <Input
88
- type="email"
89
- value={email}
90
- onChange={(event) => setEmail(event.target.value)}
91
- onBlur={() => setTouched(true)}
92
- placeholder="you@example.com"
93
- />
94
- </FormControl>
95
- <FormDescription>Used for deploy notifications.</FormDescription>
96
- <FormMessage />
97
- </FormField>
98
- <Button type="submit">Continue</Button>
99
- </Form>
100
- );
101
- },
102
- };
103
-
104
- export const MixedControls: Story = {
105
- render: () => (
106
- <Form>
107
- <FormField name="project">
108
- <FormLabel>Project name</FormLabel>
109
- <FormControl>
110
- <Input placeholder="acme-inc" />
111
- </FormControl>
112
- <FormMessage />
113
- </FormField>
114
-
115
- <FormField name="region" error="Choose a region to continue.">
116
- <FormLabel>Region</FormLabel>
117
- <FormControl>
118
- <Select>
119
- <SelectTrigger>
120
- <SelectValue placeholder="Select a region" />
121
- </SelectTrigger>
122
- <SelectContent>
123
- <SelectItem value="us">United States</SelectItem>
124
- <SelectItem value="eu">Europe</SelectItem>
125
- </SelectContent>
126
- </Select>
127
- </FormControl>
128
- <FormMessage />
129
- </FormField>
130
-
131
- <FormField name="terms">
132
- <div className="flex items-center gap-2">
133
- <FormControl>
134
- <Checkbox />
135
- </FormControl>
136
- <FormLabel>Accept terms and conditions</FormLabel>
137
- </div>
138
- <FormMessage />
139
- </FormField>
140
-
141
- <Button type="submit">Create project</Button>
142
- </Form>
143
- ),
144
- };
@@ -1,203 +0,0 @@
1
- import { 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 { Input } from "../input";
8
- import { Form, FormControl, FormDescription, FormField, FormLabel, FormMessage } from "./form";
9
-
10
- function Example({
11
- error,
12
- withDescription = true,
13
- }: {
14
- error?: string;
15
- withDescription?: boolean;
16
- } = {}) {
17
- return (
18
- <Form>
19
- <FormField name="email" error={error}>
20
- <FormLabel>Email</FormLabel>
21
- <FormControl>
22
- <Input type="email" />
23
- </FormControl>
24
- {withDescription ? <FormDescription>We will never share it.</FormDescription> : null}
25
- <FormMessage />
26
- </FormField>
27
- </Form>
28
- );
29
- }
30
-
31
- describe("Form", () => {
32
- it("disables native validation so custom messages are the only ones shown", () => {
33
- const { container } = render(<Example />);
34
- expect(container.querySelector("form")).toHaveAttribute("novalidate");
35
- });
36
-
37
- it("allows native validation back in", () => {
38
- const { container } = render(
39
- <Form noValidate={false}>
40
- <div />
41
- </Form>,
42
- );
43
- expect(container.querySelector("form")).not.toHaveAttribute("novalidate");
44
- });
45
- });
46
-
47
- describe("FormField", () => {
48
- it("points the label at the control", () => {
49
- render(<Example />);
50
- expect(screen.getByLabelText("Email")).toBe(screen.getByRole("textbox"));
51
- });
52
-
53
- it("describes the control with its description", () => {
54
- render(<Example />);
55
- expect(screen.getByRole("textbox")).toHaveAccessibleDescription("We will never share it.");
56
- });
57
-
58
- it("does not reference a description that was never rendered", () => {
59
- render(<Example withDescription={false} />);
60
- expect(screen.getByRole("textbox")).not.toHaveAttribute("aria-describedby");
61
- });
62
-
63
- it("is not invalid without an error", () => {
64
- render(<Example />);
65
- expect(screen.getByRole("textbox")).not.toHaveAttribute("aria-invalid");
66
- expect(screen.queryByText("Enter a valid email address.")).not.toBeInTheDocument();
67
- });
68
-
69
- describe("with an error", () => {
70
- it("marks the control invalid", () => {
71
- render(<Example error="Enter a valid email address." />);
72
- expect(screen.getByRole("textbox")).toHaveAttribute("aria-invalid", "true");
73
- });
74
-
75
- it("renders the message", () => {
76
- render(<Example error="Enter a valid email address." />);
77
- expect(screen.getByText("Enter a valid email address.")).toBeInTheDocument();
78
- });
79
-
80
- it("describes the control with both the description and the message", () => {
81
- render(<Example error="Enter a valid email address." />);
82
- expect(screen.getByRole("textbox")).toHaveAccessibleDescription(
83
- "We will never share it. Enter a valid email address.",
84
- );
85
- });
86
-
87
- it("announces the message politely", () => {
88
- render(<Example error="Enter a valid email address." />);
89
- const message = screen.getByText("Enter a valid email address.");
90
- expect(message).toHaveAttribute("role", "status");
91
- expect(message).toHaveAttribute("aria-live", "polite");
92
- });
93
-
94
- it("marks the label and wrapper as invalid for styling", () => {
95
- const { container } = render(<Example error="Bad" />);
96
- expect(container.querySelector("[data-slot='form-field']")).toHaveAttribute(
97
- "data-invalid",
98
- "true",
99
- );
100
- expect(screen.getByText("Email")).toHaveClass("text-destructive");
101
- });
102
- });
103
-
104
- it("mirrors the field name for form-library integration", () => {
105
- const { container } = render(<Example />);
106
- expect(container.querySelector("[data-slot='form-field']")).toHaveAttribute(
107
- "data-name",
108
- "email",
109
- );
110
- });
111
-
112
- it("gives each field its own ids", () => {
113
- render(
114
- <Form>
115
- <FormField name="first">
116
- <FormLabel>First</FormLabel>
117
- <FormControl>
118
- <Input />
119
- </FormControl>
120
- </FormField>
121
- <FormField name="last">
122
- <FormLabel>Last</FormLabel>
123
- <FormControl>
124
- <Input />
125
- </FormControl>
126
- </FormField>
127
- </Form>,
128
- );
129
-
130
- const first = screen.getByLabelText("First");
131
- const last = screen.getByLabelText("Last");
132
- expect(first.id).not.toBe(last.id);
133
- });
134
-
135
- it("works with any state source, updating as validation changes", async () => {
136
- const user = userEvent.setup();
137
-
138
- function Validated() {
139
- const [value, setValue] = useState("");
140
- const error = value.includes("@") ? undefined : "Enter a valid email address.";
141
- return (
142
- <Form>
143
- <FormField name="email" error={value ? error : undefined}>
144
- <FormLabel>Email</FormLabel>
145
- <FormControl>
146
- <Input value={value} onChange={(event) => setValue(event.target.value)} />
147
- </FormControl>
148
- <FormMessage />
149
- </FormField>
150
- </Form>
151
- );
152
- }
153
-
154
- render(<Validated />);
155
- const input = screen.getByRole("textbox");
156
-
157
- await user.type(input, "nope");
158
- expect(input).toHaveAttribute("aria-invalid", "true");
159
- expect(screen.getByText("Enter a valid email address.")).toBeInTheDocument();
160
-
161
- await user.type(input, "@example.com");
162
- expect(input).not.toHaveAttribute("aria-invalid");
163
- expect(screen.queryByText("Enter a valid email address.")).not.toBeInTheDocument();
164
- });
165
-
166
- it("lets FormMessage render its own children", () => {
167
- render(
168
- <Form>
169
- <FormField name="email">
170
- <FormLabel>Email</FormLabel>
171
- <FormControl>
172
- <Input />
173
- </FormControl>
174
- <FormMessage>Custom hint</FormMessage>
175
- </FormField>
176
- </Form>,
177
- );
178
- expect(screen.getByText("Custom hint")).toBeInTheDocument();
179
- });
180
-
181
- it.each([
182
- ["FormLabel", <FormLabel key="l">Label</FormLabel>],
183
- [
184
- "FormControl",
185
- <FormControl key="c">
186
- <input />
187
- </FormControl>,
188
- ],
189
- ["FormDescription", <FormDescription key="d">Text</FormDescription>],
190
- ["FormMessage", <FormMessage key="m">Text</FormMessage>],
191
- ])("throws a useful error if %s is used outside FormField", (name, element) => {
192
- const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
193
- expect(() => render(element)).toThrow(
194
- new RegExp(`${name} must be rendered inside <FormField>`),
195
- );
196
- consoleError.mockRestore();
197
- });
198
-
199
- it("has no accessibility violations", async () => {
200
- const { container } = render(<Example error="Enter a valid email address." />);
201
- await expectNoA11yViolations(container);
202
- });
203
- });
@@ -1,84 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Label } from "@/components/label";
4
-
5
- import { Input } from "./input";
6
-
7
- /** Named so its type is nameable in declaration output (TS2883). */
8
- const withFixedWidth: Decorator = (Story) => (
9
- <div className="w-72">
10
- <Story />
11
- </div>
12
- );
13
-
14
- const meta = {
15
- title: "Forms/Input",
16
- component: Input,
17
- args: {
18
- placeholder: "acme-inc",
19
- inputSize: "md",
20
- },
21
- argTypes: {
22
- inputSize: { control: "select", options: ["sm", "md", "lg"] },
23
- disabled: { control: "boolean" },
24
- type: { control: "select", options: ["text", "email", "password", "number", "search"] },
25
- },
26
- decorators: [withFixedWidth],
27
- } satisfies Meta<typeof Input>;
28
-
29
- export default meta;
30
- type Story = StoryObj<typeof meta>;
31
-
32
- export const Default: Story = {
33
- render: (args) => (
34
- <div className="grid gap-2">
35
- <Label htmlFor="project">Project name</Label>
36
- <Input id="project" {...args} />
37
- </div>
38
- ),
39
- };
40
-
41
- export const Sizes: Story = {
42
- parameters: { controls: { disable: true } },
43
- render: () => (
44
- <div className="grid gap-3">
45
- <Input inputSize="sm" placeholder="Small" aria-label="Small" />
46
- <Input inputSize="md" placeholder="Medium" aria-label="Medium" />
47
- <Input inputSize="lg" placeholder="Large" aria-label="Large" />
48
- </div>
49
- ),
50
- };
51
-
52
- export const Invalid: Story = {
53
- render: () => (
54
- <div className="grid gap-2">
55
- <Label htmlFor="email">Email</Label>
56
- <Input
57
- id="email"
58
- type="email"
59
- defaultValue="not-an-email"
60
- aria-invalid
61
- aria-describedby="email-error"
62
- />
63
- <p id="email-error" className="text-xs text-destructive">
64
- Enter a valid email address.
65
- </p>
66
- </div>
67
- ),
68
- };
69
-
70
- export const Disabled: Story = {
71
- args: { disabled: true, value: "Read only" },
72
- };
73
-
74
- export const Types: Story = {
75
- parameters: { controls: { disable: true } },
76
- render: () => (
77
- <div className="grid gap-3">
78
- <Input type="email" placeholder="you@example.com" aria-label="Email" />
79
- <Input type="password" placeholder="••••••••" aria-label="Password" />
80
- <Input type="number" placeholder="42" aria-label="Quantity" />
81
- <Input type="search" placeholder="Search…" aria-label="Search" />
82
- </div>
83
- ),
84
- };
@@ -1,108 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { createRef, useState } from "react";
4
- import { describe, expect, it, vi } from "vitest";
5
-
6
- import { expectNoA11yViolations } from "../../../test/a11y";
7
- import { Input } from "./input";
8
-
9
- describe("Input", () => {
10
- it("renders a text input by default", () => {
11
- render(<Input aria-label="Name" />);
12
- expect(screen.getByRole("textbox", { name: "Name" })).toHaveAttribute("type", "text");
13
- });
14
-
15
- it("honours an explicit type", () => {
16
- render(<Input type="email" aria-label="Email" />);
17
- expect(screen.getByRole("textbox")).toHaveAttribute("type", "email");
18
- });
19
-
20
- it.each([
21
- ["sm", "h-8"],
22
- ["md", "h-9"],
23
- ["lg", "h-10"],
24
- ] as const)("applies the %s size", (inputSize, expectedClass) => {
25
- render(<Input inputSize={inputSize} aria-label="Name" />);
26
- expect(screen.getByRole("textbox")).toHaveClass(expectedClass);
27
- });
28
-
29
- it("does not shadow the native size attribute", () => {
30
- render(<Input size={5} aria-label="Name" />);
31
- expect(screen.getByRole("textbox")).toHaveAttribute("size", "5");
32
- });
33
-
34
- it("lets a consumer className override a conflicting utility", () => {
35
- render(<Input className="h-16" aria-label="Name" />);
36
- const input = screen.getByRole("textbox");
37
- expect(input).toHaveClass("h-16");
38
- expect(input).not.toHaveClass("h-9");
39
- });
40
-
41
- it("forwards a ref", () => {
42
- const ref = createRef<HTMLInputElement>();
43
- render(<Input ref={ref} aria-label="Name" />);
44
- expect(ref.current).toBeInstanceOf(HTMLInputElement);
45
- });
46
-
47
- it("works uncontrolled", async () => {
48
- const user = userEvent.setup();
49
- render(<Input defaultValue="ab" aria-label="Name" />);
50
-
51
- const input = screen.getByRole<HTMLInputElement>("textbox");
52
- await user.type(input, "cd");
53
- expect(input.value).toBe("abcd");
54
- });
55
-
56
- it("works controlled", async () => {
57
- const user = userEvent.setup();
58
-
59
- function Controlled() {
60
- const [value, setValue] = useState("");
61
- return (
62
- <Input
63
- value={value}
64
- onChange={(event) => setValue(event.target.value)}
65
- aria-label="Name"
66
- />
67
- );
68
- }
69
-
70
- render(<Controlled />);
71
- const input = screen.getByRole<HTMLInputElement>("textbox");
72
- await user.type(input, "hello");
73
- expect(input.value).toBe("hello");
74
- });
75
-
76
- it("does not accept input while disabled", async () => {
77
- const onChange = vi.fn();
78
- const user = userEvent.setup();
79
- render(<Input disabled onChange={onChange} aria-label="Name" />);
80
-
81
- const input = screen.getByRole("textbox");
82
- expect(input).toBeDisabled();
83
- await user.type(input, "x");
84
- expect(onChange).not.toHaveBeenCalled();
85
- });
86
-
87
- it("exposes the invalid state through aria-invalid", () => {
88
- render(<Input aria-invalid aria-label="Name" />);
89
- expect(screen.getByRole("textbox")).toHaveAttribute("aria-invalid", "true");
90
- });
91
-
92
- it("is reachable by keyboard", async () => {
93
- const user = userEvent.setup();
94
- render(<Input aria-label="Name" />);
95
- await user.tab();
96
- expect(screen.getByRole("textbox")).toHaveFocus();
97
- });
98
-
99
- it("has no accessibility violations", async () => {
100
- const { container } = render(
101
- <>
102
- <label htmlFor="field">Name</label>
103
- <Input id="field" />
104
- </>,
105
- );
106
- await expectNoA11yViolations(container);
107
- });
108
- });
@@ -1,51 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Input } from "@/components/input";
4
-
5
- import { Label } from "./label";
6
-
7
- const meta = {
8
- title: "Forms/Label",
9
- component: Label,
10
- args: { children: "Project name" },
11
- } satisfies Meta<typeof Label>;
12
-
13
- export default meta;
14
- type Story = StoryObj<typeof meta>;
15
-
16
- export const Default: Story = {
17
- render: (args) => (
18
- <div className="grid w-72 gap-2">
19
- <Label htmlFor="project" {...args} />
20
- <Input id="project" placeholder="acme-inc" />
21
- </div>
22
- ),
23
- };
24
-
25
- export const Required: Story = {
26
- parameters: { controls: { disable: true } },
27
- render: () => (
28
- <div className="grid w-72 gap-2">
29
- <Label htmlFor="email">
30
- Email
31
- <span aria-hidden="true" className="text-destructive">
32
- *
33
- </span>
34
- </Label>
35
- <Input id="email" type="email" required placeholder="you@example.com" />
36
- </div>
37
- ),
38
- };
39
-
40
- /** A label dims alongside the control it describes. */
41
- export const WithDisabledControl: Story = {
42
- parameters: { controls: { disable: true } },
43
- render: () => (
44
- <div className="grid w-72 gap-2">
45
- <Input id="locked" disabled placeholder="Locked" className="peer order-2" />
46
- <Label htmlFor="locked" className="order-1">
47
- Workspace slug
48
- </Label>
49
- </div>
50
- ),
51
- };
@@ -1,55 +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 { Label } from "./label";
7
-
8
- describe("Label", () => {
9
- it("renders its text", () => {
10
- render(<Label htmlFor="name">Full name</Label>);
11
- expect(screen.getByText("Full name")).toBeInTheDocument();
12
- });
13
-
14
- it("associates itself with a control via htmlFor", () => {
15
- render(
16
- <>
17
- <Label htmlFor="name">Full name</Label>
18
- <input id="name" />
19
- </>,
20
- );
21
- expect(screen.getByLabelText("Full name")).toBeInTheDocument();
22
- });
23
-
24
- it("moves focus to its control when clicked", async () => {
25
- const user = userEvent.setup();
26
- render(
27
- <>
28
- <Label htmlFor="name">Full name</Label>
29
- <input id="name" />
30
- </>,
31
- );
32
-
33
- await user.click(screen.getByText("Full name"));
34
- expect(screen.getByLabelText("Full name")).toHaveFocus();
35
- });
36
-
37
- it("merges a consumer className", () => {
38
- render(
39
- <Label htmlFor="name" className="text-destructive">
40
- Full name
41
- </Label>,
42
- );
43
- expect(screen.getByText("Full name")).toHaveClass("text-destructive", "font-medium");
44
- });
45
-
46
- it("has no accessibility violations", async () => {
47
- const { container } = render(
48
- <>
49
- <Label htmlFor="name">Full name</Label>
50
- <input id="name" />
51
- </>,
52
- );
53
- await expectNoA11yViolations(container);
54
- });
55
- });