@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,66 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import { describe, expect, it } from "vitest";
3
-
4
- import { expectNoA11yViolations } from "../../../test/a11y";
5
- import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./card";
6
-
7
- describe("Card", () => {
8
- it("renders every part", () => {
9
- render(
10
- <Card>
11
- <CardHeader>
12
- <CardTitle>Create project</CardTitle>
13
- <CardDescription>Projects group your deployments.</CardDescription>
14
- </CardHeader>
15
- <CardContent>Body</CardContent>
16
- <CardFooter>Footer</CardFooter>
17
- </Card>,
18
- );
19
-
20
- expect(screen.getByRole("heading", { name: "Create project" })).toBeInTheDocument();
21
- expect(screen.getByText("Projects group your deployments.")).toBeInTheDocument();
22
- expect(screen.getByText("Body")).toBeInTheDocument();
23
- expect(screen.getByText("Footer")).toBeInTheDocument();
24
- });
25
-
26
- it("renders the title as a level 3 heading by default", () => {
27
- render(<CardTitle>Title</CardTitle>);
28
- expect(screen.getByRole("heading", { level: 3 })).toHaveTextContent("Title");
29
- });
30
-
31
- it("exposes slot attributes for styling hooks", () => {
32
- const { container } = render(
33
- <Card>
34
- <CardHeader>
35
- <CardTitle>Title</CardTitle>
36
- </CardHeader>
37
- </Card>,
38
- );
39
- expect(container.querySelector("[data-slot='card']")).toBeInTheDocument();
40
- expect(container.querySelector("[data-slot='card-header']")).toBeInTheDocument();
41
- });
42
-
43
- it("merges a consumer className on every part", () => {
44
- const { container } = render(<Card className="max-w-sm">Content</Card>);
45
- const card = container.querySelector("[data-slot='card']");
46
- expect(card).toHaveClass("max-w-sm", "bg-card");
47
- });
48
-
49
- it("forwards arbitrary props", () => {
50
- render(<Card data-testid="card">Content</Card>);
51
- expect(screen.getByTestId("card")).toBeInTheDocument();
52
- });
53
-
54
- it("has no accessibility violations", async () => {
55
- const { container } = render(
56
- <Card>
57
- <CardHeader>
58
- <CardTitle>Create project</CardTitle>
59
- <CardDescription>Projects group your deployments.</CardDescription>
60
- </CardHeader>
61
- <CardContent>Body</CardContent>
62
- </Card>,
63
- );
64
- await expectNoA11yViolations(container);
65
- });
66
- });
@@ -1,124 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
- import { useState } from "react";
3
-
4
- import { Label } from "@/components/label";
5
-
6
- import { Checkbox } from "./checkbox";
7
-
8
- const meta = {
9
- title: "Forms/Checkbox",
10
- component: Checkbox,
11
- args: { disabled: false },
12
- argTypes: { disabled: { control: "boolean" } },
13
- } satisfies Meta<typeof Checkbox>;
14
-
15
- export default meta;
16
- type Story = StoryObj<typeof meta>;
17
-
18
- export const Default: Story = {
19
- render: (args) => (
20
- <div className="flex items-center gap-2">
21
- <Checkbox id="terms" {...args} />
22
- <Label htmlFor="terms">Accept terms and conditions</Label>
23
- </div>
24
- ),
25
- };
26
-
27
- export const WithDescription: Story = {
28
- parameters: { controls: { disable: true } },
29
- render: () => (
30
- <div className="flex items-start gap-2">
31
- <Checkbox id="marketing" className="mt-0.5" />
32
- <div className="grid gap-1">
33
- <Label htmlFor="marketing">Product updates</Label>
34
- <p className="text-xs text-muted-foreground">
35
- Occasional email about new features. No more than once a month.
36
- </p>
37
- </div>
38
- </div>
39
- ),
40
- };
41
-
42
- export const States: Story = {
43
- parameters: { controls: { disable: true } },
44
- render: () => (
45
- <div className="grid gap-3">
46
- {[
47
- { id: "s1", label: "Unchecked", checked: false as const },
48
- { id: "s2", label: "Checked", checked: true as const },
49
- { id: "s3", label: "Indeterminate", checked: "indeterminate" as const },
50
- ].map((state) => (
51
- <div key={state.id} className="flex items-center gap-2">
52
- <Checkbox id={state.id} checked={state.checked} />
53
- <Label htmlFor={state.id}>{state.label}</Label>
54
- </div>
55
- ))}
56
- <div className="flex items-center gap-2">
57
- <Checkbox id="s4" disabled />
58
- <Label htmlFor="s4">Disabled</Label>
59
- </div>
60
- <div className="flex items-center gap-2">
61
- <Checkbox id="s5" disabled checked />
62
- <Label htmlFor="s5">Disabled and checked</Label>
63
- </div>
64
- </div>
65
- ),
66
- };
67
-
68
- /** Indeterminate summarises a partial selection; the user never cycles into it. */
69
- export const SelectAll: Story = {
70
- parameters: { controls: { disable: true } },
71
- render: function SelectAll() {
72
- const items = ["Analytics", "Deployments", "Logs"];
73
- const [selected, setSelected] = useState<string[]>(["Analytics"]);
74
-
75
- const all = selected.length === items.length;
76
- const some = selected.length > 0 && !all;
77
-
78
- return (
79
- <div className="grid gap-3">
80
- <div className="flex items-center gap-2">
81
- <Checkbox
82
- id="all"
83
- checked={all ? true : some ? "indeterminate" : false}
84
- onCheckedChange={(checked) => {
85
- setSelected(checked === true ? items : []);
86
- }}
87
- />
88
- <Label htmlFor="all">Select all</Label>
89
- </div>
90
- <div className="ml-6 grid gap-3">
91
- {items.map((item) => (
92
- <div key={item} className="flex items-center gap-2">
93
- <Checkbox
94
- id={item}
95
- checked={selected.includes(item)}
96
- onCheckedChange={(checked) => {
97
- setSelected((current) =>
98
- checked ? [...current, item] : current.filter((entry) => entry !== item),
99
- );
100
- }}
101
- />
102
- <Label htmlFor={item}>{item}</Label>
103
- </div>
104
- ))}
105
- </div>
106
- </div>
107
- );
108
- },
109
- };
110
-
111
- export const Invalid: Story = {
112
- parameters: { controls: { disable: true } },
113
- render: () => (
114
- <div className="grid gap-2">
115
- <div className="flex items-center gap-2">
116
- <Checkbox id="invalid" aria-invalid aria-describedby="invalid-error" />
117
- <Label htmlFor="invalid">Accept terms</Label>
118
- </div>
119
- <p id="invalid-error" className="text-xs text-destructive">
120
- You must accept the terms to continue.
121
- </p>
122
- </div>
123
- ),
124
- };
@@ -1,121 +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 { Label } from "../label";
8
- import { Checkbox } from "./checkbox";
9
-
10
- describe("Checkbox", () => {
11
- it("renders unchecked by default", () => {
12
- render(<Checkbox aria-label="Accept terms" />);
13
- expect(screen.getByRole("checkbox", { name: "Accept terms" })).not.toBeChecked();
14
- });
15
-
16
- it("takes its accessible name from an associated label", () => {
17
- render(
18
- <>
19
- <Checkbox id="terms" />
20
- <Label htmlFor="terms">Accept terms</Label>
21
- </>,
22
- );
23
- expect(screen.getByRole("checkbox", { name: "Accept terms" })).toBeInTheDocument();
24
- });
25
-
26
- it("toggles on click", async () => {
27
- const user = userEvent.setup();
28
- render(<Checkbox aria-label="Accept" />);
29
-
30
- const checkbox = screen.getByRole("checkbox");
31
- await user.click(checkbox);
32
- expect(checkbox).toBeChecked();
33
-
34
- await user.click(checkbox);
35
- expect(checkbox).not.toBeChecked();
36
- });
37
-
38
- it("toggles with the Space key", async () => {
39
- const user = userEvent.setup();
40
- render(<Checkbox aria-label="Accept" />);
41
-
42
- await user.tab();
43
- expect(screen.getByRole("checkbox")).toHaveFocus();
44
-
45
- await user.keyboard(" ");
46
- expect(screen.getByRole("checkbox")).toBeChecked();
47
- });
48
-
49
- it("reports changes", async () => {
50
- const onCheckedChange = vi.fn();
51
- const user = userEvent.setup();
52
- render(<Checkbox aria-label="Accept" onCheckedChange={onCheckedChange} />);
53
-
54
- await user.click(screen.getByRole("checkbox"));
55
- expect(onCheckedChange).toHaveBeenCalledWith(true);
56
- });
57
-
58
- it("works controlled", async () => {
59
- const user = userEvent.setup();
60
-
61
- function Controlled() {
62
- const [checked, setChecked] = useState(false);
63
- // onCheckedChange yields CheckedState (boolean | "indeterminate"), not a
64
- // boolean — indeterminate is a state the application sets, so a boolean
65
- // store narrows it here rather than widening the state.
66
- return (
67
- <Checkbox
68
- aria-label="Accept"
69
- checked={checked}
70
- onCheckedChange={(next) => {
71
- setChecked(next === true);
72
- }}
73
- />
74
- );
75
- }
76
-
77
- render(<Controlled />);
78
- const checkbox = screen.getByRole("checkbox");
79
- await user.click(checkbox);
80
- expect(checkbox).toBeChecked();
81
- });
82
-
83
- it("announces the indeterminate state as mixed", () => {
84
- render(<Checkbox aria-label="Select all" checked="indeterminate" />);
85
- expect(screen.getByRole("checkbox")).toHaveAttribute("aria-checked", "mixed");
86
- expect(screen.getByRole("checkbox")).toHaveAttribute("data-state", "indeterminate");
87
- });
88
-
89
- it("does not toggle while disabled", async () => {
90
- const onCheckedChange = vi.fn();
91
- const user = userEvent.setup();
92
- render(<Checkbox aria-label="Accept" disabled onCheckedChange={onCheckedChange} />);
93
-
94
- const checkbox = screen.getByRole("checkbox");
95
- expect(checkbox).toBeDisabled();
96
- await user.click(checkbox);
97
- expect(onCheckedChange).not.toHaveBeenCalled();
98
- });
99
-
100
- it("exposes the invalid state", () => {
101
- render(<Checkbox aria-label="Accept" aria-invalid />);
102
- expect(screen.getByRole("checkbox")).toHaveAttribute("aria-invalid", "true");
103
- });
104
-
105
- it("lets a consumer className override a conflicting utility", () => {
106
- render(<Checkbox aria-label="Accept" className="size-6" />);
107
- const checkbox = screen.getByRole("checkbox");
108
- expect(checkbox).toHaveClass("size-6");
109
- expect(checkbox).not.toHaveClass("size-4");
110
- });
111
-
112
- it("has no accessibility violations", async () => {
113
- const { container } = render(
114
- <div className="flex items-center gap-2">
115
- <Checkbox id="a11y-terms" />
116
- <Label htmlFor="a11y-terms">Accept terms</Label>
117
- </div>,
118
- );
119
- await expectNoA11yViolations(container);
120
- });
121
- });
@@ -1,62 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { CodeBlock } from "./code-block";
4
-
5
- /** Named so its type is nameable in declaration output (TS2883). */
6
- const withFixedWidth: Decorator = (Story) => (
7
- <div className="w-[34rem]">
8
- <Story />
9
- </div>
10
- );
11
-
12
- const SAMPLE = `export function cn(...inputs: ClassValue[]): string {
13
- return twMerge(clsx(inputs));
14
- }`;
15
-
16
- const meta = {
17
- title: "Data/Code Block",
18
- component: CodeBlock,
19
- args: { language: "ts", children: SAMPLE },
20
- decorators: [withFixedWidth],
21
- } satisfies Meta<typeof CodeBlock>;
22
-
23
- export default meta;
24
- type Story = StoryObj<typeof meta>;
25
-
26
- export const Default: Story = {};
27
-
28
- export const WithFilename: Story = {
29
- args: { title: "lib/utils.ts" },
30
- };
31
-
32
- export const WithoutCopy: Story = {
33
- args: { hideCopy: true },
34
- };
35
-
36
- /** Overflowing code: the pre is focusable, so it can be scrolled by keyboard. */
37
- export const Overflowing: Story = {
38
- args: {
39
- title: "one-liner.ts",
40
- children:
41
- 'const message = "a single very long line that runs well past the edge of the container and keeps going for a while yet";',
42
- },
43
- };
44
-
45
- /**
46
- * Highlighting is left to the application. Pass rendered markup as children and
47
- * the real text as `code`, since reading text back out of highlighted DOM loses
48
- * whitespace in ways that break pasted code.
49
- */
50
- export const PreHighlighted: Story = {
51
- args: {
52
- title: "highlighted.ts",
53
- code: 'const answer = "Paris";',
54
- children: (
55
- <>
56
- <span className="text-info">const</span> answer{" "}
57
- <span className="text-muted-foreground">=</span>{" "}
58
- <span className="text-success">&quot;Paris&quot;</span>;
59
- </>
60
- ),
61
- },
62
- };
@@ -1,139 +0,0 @@
1
- import { render, screen, waitFor } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { afterEach, describe, expect, it, vi } from "vitest";
4
-
5
- import { expectNoA11yViolations } from "../../../test/a11y";
6
- import { CodeBlock } from "./code-block";
7
-
8
- const SAMPLE = 'const answer = "Paris";';
9
-
10
- /**
11
- * Replaces the clipboard.
12
- *
13
- * Must be called *after* `userEvent.setup()`, which installs a clipboard stub
14
- * of its own — applying ours first means testing theirs.
15
- */
16
- function mockClipboard(impl: () => Promise<void>) {
17
- const writeText = vi.fn(impl);
18
- Object.defineProperty(navigator, "clipboard", {
19
- value: { writeText },
20
- configurable: true,
21
- });
22
- return writeText;
23
- }
24
-
25
- afterEach(() => {
26
- vi.restoreAllMocks();
27
- });
28
-
29
- describe("CodeBlock", () => {
30
- it("renders the code", () => {
31
- render(<CodeBlock language="ts">{SAMPLE}</CodeBlock>);
32
- expect(screen.getByText(SAMPLE)).toBeInTheDocument();
33
- });
34
-
35
- it("shows the language in the header", () => {
36
- render(<CodeBlock language="typescript">{SAMPLE}</CodeBlock>);
37
- expect(screen.getByText("typescript")).toBeInTheDocument();
38
- });
39
-
40
- it("prefers a title over the language", () => {
41
- render(
42
- <CodeBlock language="ts" title="button.tsx">
43
- {SAMPLE}
44
- </CodeBlock>,
45
- );
46
- expect(screen.getByText("button.tsx")).toBeInTheDocument();
47
- expect(screen.queryByText("ts")).not.toBeInTheDocument();
48
- });
49
-
50
- it("is a focusable named region, since code scrolls sideways", () => {
51
- render(<CodeBlock language="ts">{SAMPLE}</CodeBlock>);
52
-
53
- const region = screen.getByRole("region", { name: "ts code" });
54
- expect(region).toHaveAttribute("tabindex", "0");
55
- });
56
-
57
- it("names the region after the title when there is one", () => {
58
- render(<CodeBlock title="button.tsx">{SAMPLE}</CodeBlock>);
59
- expect(screen.getByRole("region", { name: "button.tsx" })).toBeInTheDocument();
60
- });
61
-
62
- it("renders markup a highlighter has already produced", () => {
63
- render(
64
- <CodeBlock language="ts" code={SAMPLE}>
65
- <span className="token">const</span>
66
- <span> answer</span>
67
- </CodeBlock>,
68
- );
69
- expect(screen.getByText("const")).toBeInTheDocument();
70
- });
71
-
72
- describe("copying", () => {
73
- it("copies the text content", async () => {
74
- const user = userEvent.setup();
75
- const writeText = mockClipboard(() => Promise.resolve());
76
- render(<CodeBlock language="ts">{SAMPLE}</CodeBlock>);
77
-
78
- await user.click(screen.getByRole("button", { name: "Copy code" }));
79
- expect(writeText).toHaveBeenCalledWith(SAMPLE);
80
- });
81
-
82
- it("prefers the explicit code prop over reading the DOM", async () => {
83
- const user = userEvent.setup();
84
- const writeText = mockClipboard(() => Promise.resolve());
85
- render(
86
- <CodeBlock language="ts" code={SAMPLE}>
87
- <span>const</span>
88
- <span> answer</span>
89
- </CodeBlock>,
90
- );
91
-
92
- // Reading highlighted DOM back loses whitespace in ways that break pasted
93
- // code, which is why `code` exists.
94
- await user.click(screen.getByRole("button", { name: "Copy code" }));
95
- expect(writeText).toHaveBeenCalledWith(SAMPLE);
96
- });
97
-
98
- it("announces success, not just draws a tick", async () => {
99
- const user = userEvent.setup();
100
- mockClipboard(() => Promise.resolve());
101
- render(<CodeBlock language="ts">{SAMPLE}</CodeBlock>);
102
-
103
- await user.click(screen.getByRole("button", { name: "Copy code" }));
104
- await waitFor(() => {
105
- expect(screen.getByRole("status")).toHaveTextContent("Copied");
106
- });
107
- });
108
-
109
- it("reports a refused clipboard rather than looking like success", async () => {
110
- const user = userEvent.setup();
111
- mockClipboard(() => Promise.reject(new Error("denied")));
112
- render(<CodeBlock language="ts">{SAMPLE}</CodeBlock>);
113
-
114
- await user.click(screen.getByRole("button", { name: "Copy code" }));
115
- await waitFor(() => {
116
- expect(screen.getByRole("status")).toHaveTextContent("Copy failed");
117
- });
118
- });
119
-
120
- it("can be hidden", () => {
121
- render(
122
- <CodeBlock language="ts" hideCopy>
123
- {SAMPLE}
124
- </CodeBlock>,
125
- );
126
- expect(screen.queryByRole("button", { name: "Copy code" })).not.toBeInTheDocument();
127
- });
128
- });
129
-
130
- it("has no accessibility violations", async () => {
131
- mockClipboard(() => Promise.resolve());
132
- const { container } = render(
133
- <CodeBlock language="ts" title="answer.ts">
134
- {SAMPLE}
135
- </CodeBlock>,
136
- );
137
- await expectNoA11yViolations(container);
138
- });
139
- });
@@ -1,171 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
- import { useState } from "react";
3
-
4
- import { Label } from "@/components/label";
5
-
6
- import {
7
- Combobox,
8
- ComboboxContent,
9
- ComboboxEmpty,
10
- ComboboxInput,
11
- ComboboxItem,
12
- ComboboxList,
13
- ComboboxTrigger,
14
- } from "./combobox";
15
-
16
- const FRAMEWORKS = [
17
- { value: "Next.js", keywords: ["vercel", "react", "ssr"] },
18
- { value: "SvelteKit", keywords: ["svelte"] },
19
- { value: "Nuxt", keywords: ["vue"] },
20
- { value: "Remix", keywords: ["react"] },
21
- { value: "Astro", keywords: ["islands", "content"] },
22
- { value: "SolidStart", keywords: ["solid"] },
23
- ];
24
-
25
- /** Named so its type is nameable in declaration output (TS2883). */
26
- const withFixedWidth: Decorator = (Story) => (
27
- <div className="w-64">
28
- <Story />
29
- </div>
30
- );
31
-
32
- const meta = {
33
- title: "Forms/Combobox",
34
- component: Combobox,
35
- parameters: { controls: { disable: true } },
36
- decorators: [withFixedWidth],
37
- } satisfies Meta<typeof Combobox>;
38
-
39
- export default meta;
40
- type Story = StoryObj<typeof meta>;
41
-
42
- export const Default: Story = {
43
- render: () => (
44
- <div className="grid gap-2">
45
- <Label id="framework-label">Framework</Label>
46
- <Combobox>
47
- <ComboboxTrigger aria-labelledby="framework-label" placeholder="Select framework…" />
48
- <ComboboxContent label="Search frameworks">
49
- <ComboboxInput placeholder="Search framework…" aria-label="Search framework" />
50
- <ComboboxEmpty>No framework found.</ComboboxEmpty>
51
- <ComboboxList>
52
- {FRAMEWORKS.map((framework) => (
53
- <ComboboxItem
54
- key={framework.value}
55
- value={framework.value}
56
- keywords={framework.keywords}
57
- />
58
- ))}
59
- </ComboboxList>
60
- </ComboboxContent>
61
- </Combobox>
62
- </div>
63
- ),
64
- };
65
-
66
- /** Keywords are searchable but never shown — try typing "vercel" or "vue". */
67
- export const HiddenKeywords: Story = {
68
- render: () => (
69
- <Combobox>
70
- <ComboboxTrigger aria-label="Framework" placeholder="Try typing 'vue'…" />
71
- <ComboboxContent label="Search frameworks">
72
- <ComboboxInput placeholder="Search framework…" aria-label="Search framework" />
73
- <ComboboxEmpty>No framework found.</ComboboxEmpty>
74
- <ComboboxList>
75
- {FRAMEWORKS.map((framework) => (
76
- <ComboboxItem
77
- key={framework.value}
78
- value={framework.value}
79
- keywords={framework.keywords}
80
- />
81
- ))}
82
- </ComboboxList>
83
- </ComboboxContent>
84
- </Combobox>
85
- ),
86
- };
87
-
88
- export const RichOptions: Story = {
89
- render: () => (
90
- <Combobox>
91
- <ComboboxTrigger aria-label="Assignee" placeholder="Assign to…" />
92
- <ComboboxContent label="Search people">
93
- <ComboboxInput placeholder="Search people…" aria-label="Search people" />
94
- <ComboboxEmpty>No one found.</ComboboxEmpty>
95
- <ComboboxList>
96
- {[
97
- { value: "Ada Lovelace", role: "Engineering" },
98
- { value: "Grace Hopper", role: "Engineering" },
99
- { value: "Katherine Johnson", role: "Research" },
100
- ].map((person) => (
101
- <ComboboxItem key={person.value} value={person.value} keywords={[person.role]}>
102
- <span className="flex-1 truncate">{person.value}</span>
103
- <span className="text-xs text-muted-foreground">{person.role}</span>
104
- </ComboboxItem>
105
- ))}
106
- </ComboboxList>
107
- </ComboboxContent>
108
- </Combobox>
109
- ),
110
- };
111
-
112
- export const WithDisabledOption: Story = {
113
- render: () => (
114
- <Combobox>
115
- <ComboboxTrigger aria-label="Plan" placeholder="Select a plan…" />
116
- <ComboboxContent label="Search plans">
117
- <ComboboxInput placeholder="Search plans…" aria-label="Search plans" />
118
- <ComboboxEmpty>No plan found.</ComboboxEmpty>
119
- <ComboboxList>
120
- <ComboboxItem value="Hobby" />
121
- <ComboboxItem value="Pro" />
122
- <ComboboxItem value="Enterprise" disabled>
123
- Enterprise (contact sales)
124
- </ComboboxItem>
125
- </ComboboxList>
126
- </ComboboxContent>
127
- </Combobox>
128
- ),
129
- };
130
-
131
- export const Controlled: Story = {
132
- render: function Controlled() {
133
- const [value, setValue] = useState("Astro");
134
- return (
135
- <div className="grid gap-3">
136
- <Combobox value={value} onValueChange={setValue}>
137
- <ComboboxTrigger aria-label="Framework" />
138
- <ComboboxContent label="Search frameworks">
139
- <ComboboxInput placeholder="Search framework…" aria-label="Search framework" />
140
- <ComboboxEmpty>No framework found.</ComboboxEmpty>
141
- <ComboboxList>
142
- {FRAMEWORKS.map((framework) => (
143
- <ComboboxItem key={framework.value} value={framework.value} />
144
- ))}
145
- </ComboboxList>
146
- </ComboboxContent>
147
- </Combobox>
148
- <p className="text-xs text-muted-foreground">Selected: {value}</p>
149
- </div>
150
- );
151
- },
152
- };
153
-
154
- export const LongList: Story = {
155
- render: () => (
156
- <Combobox>
157
- <ComboboxTrigger aria-label="Timezone" placeholder="Select a timezone…" />
158
- <ComboboxContent label="Search timezones">
159
- <ComboboxInput placeholder="Search timezones…" aria-label="Search timezones" />
160
- <ComboboxEmpty>No timezone found.</ComboboxEmpty>
161
- <ComboboxList>
162
- {Array.from({ length: 40 }, (_, index) => {
163
- const offset = index - 12;
164
- const label = `UTC${offset >= 0 ? "+" : "-"}${String(Math.abs(offset)).padStart(2, "0")}:00`;
165
- return <ComboboxItem key={index} value={`${label} · Zone ${String(index + 1)}`} />;
166
- })}
167
- </ComboboxList>
168
- </ComboboxContent>
169
- </Combobox>
170
- ),
171
- };