@dowel-ui/react 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/components/ai-agent-status/ai-agent-status.d.ts +1 -1
  2. package/dist/components/ai-tool/ai-tool.d.ts +1 -1
  3. package/dist/components/alert/alert.d.ts +1 -1
  4. package/dist/components/avatar/avatar.d.ts +1 -1
  5. package/dist/components/badge/badge.d.ts +1 -1
  6. package/dist/components/button/button.d.ts +1 -1
  7. package/dist/components/progress/progress.d.ts +1 -1
  8. package/dist/components/spinner/spinner.d.ts +1 -1
  9. package/dist/components/toast/toast.d.ts +1 -1
  10. package/package.json +7 -4
  11. package/src/blocks/admin-users/admin-users.stories.tsx +0 -83
  12. package/src/blocks/admin-users/admin-users.test.tsx +0 -159
  13. package/src/blocks/ai-chat/ai-chat.stories.tsx +0 -149
  14. package/src/blocks/ai-chat/ai-chat.test.tsx +0 -166
  15. package/src/blocks/dashboard/dashboard.stories.tsx +0 -106
  16. package/src/blocks/dashboard/dashboard.test.tsx +0 -94
  17. package/src/blocks/forgot-password/forgot-password.stories.tsx +0 -41
  18. package/src/blocks/forgot-password/forgot-password.test.tsx +0 -62
  19. package/src/blocks/login/login.stories.tsx +0 -46
  20. package/src/blocks/login/login.test.tsx +0 -100
  21. package/src/blocks/pricing/pricing.stories.tsx +0 -62
  22. package/src/blocks/pricing/pricing.test.tsx +0 -101
  23. package/src/blocks/settings/settings.stories.tsx +0 -94
  24. package/src/blocks/settings/settings.test.tsx +0 -167
  25. package/src/blocks/signup/signup.stories.tsx +0 -27
  26. package/src/blocks/signup/signup.test.tsx +0 -97
  27. package/src/components/accordion/accordion.stories.tsx +0 -90
  28. package/src/components/accordion/accordion.test.tsx +0 -156
  29. package/src/components/activity-feed/activity-feed.stories.tsx +0 -163
  30. package/src/components/activity-feed/activity-feed.test.tsx +0 -125
  31. package/src/components/ai-agent-status/ai-agent-status.stories.tsx +0 -86
  32. package/src/components/ai-agent-status/ai-agent-status.test.tsx +0 -66
  33. package/src/components/ai-conversation/ai-conversation.stories.tsx +0 -126
  34. package/src/components/ai-conversation/ai-conversation.test.tsx +0 -144
  35. package/src/components/ai-message/ai-message.stories.tsx +0 -106
  36. package/src/components/ai-message/ai-message.test.tsx +0 -160
  37. package/src/components/ai-model-selector/ai-model-selector.stories.tsx +0 -66
  38. package/src/components/ai-model-selector/ai-model-selector.test.tsx +0 -120
  39. package/src/components/ai-prompt-input/ai-prompt-input.stories.tsx +0 -145
  40. package/src/components/ai-prompt-input/ai-prompt-input.test.tsx +0 -273
  41. package/src/components/ai-reasoning/ai-reasoning.stories.tsx +0 -60
  42. package/src/components/ai-reasoning/ai-reasoning.test.tsx +0 -95
  43. package/src/components/ai-response/ai-response.stories.tsx +0 -75
  44. package/src/components/ai-response/ai-response.test.tsx +0 -96
  45. package/src/components/ai-sources/ai-sources.stories.tsx +0 -93
  46. package/src/components/ai-sources/ai-sources.test.tsx +0 -102
  47. package/src/components/ai-token-usage/ai-token-usage.stories.tsx +0 -59
  48. package/src/components/ai-token-usage/ai-token-usage.test.tsx +0 -83
  49. package/src/components/ai-tool/ai-tool.stories.tsx +0 -96
  50. package/src/components/ai-tool/ai-tool.test.tsx +0 -137
  51. package/src/components/alert/alert.stories.tsx +0 -101
  52. package/src/components/alert/alert.test.tsx +0 -94
  53. package/src/components/avatar/avatar.stories.tsx +0 -58
  54. package/src/components/avatar/avatar.test.tsx +0 -64
  55. package/src/components/badge/badge.stories.tsx +0 -68
  56. package/src/components/badge/badge.test.tsx +0 -61
  57. package/src/components/button/button.stories.tsx +0 -128
  58. package/src/components/button/button.test.tsx +0 -224
  59. package/src/components/calendar/calendar.stories.tsx +0 -105
  60. package/src/components/calendar/calendar.test.tsx +0 -255
  61. package/src/components/card/card.stories.tsx +0 -61
  62. package/src/components/card/card.test.tsx +0 -66
  63. package/src/components/checkbox/checkbox.stories.tsx +0 -124
  64. package/src/components/checkbox/checkbox.test.tsx +0 -121
  65. package/src/components/code-block/code-block.stories.tsx +0 -62
  66. package/src/components/code-block/code-block.test.tsx +0 -139
  67. package/src/components/combobox/combobox.stories.tsx +0 -171
  68. package/src/components/combobox/combobox.test.tsx +0 -429
  69. package/src/components/command/command.stories.tsx +0 -152
  70. package/src/components/command/command.test.tsx +0 -380
  71. package/src/components/data-table/data-table.stories.tsx +0 -151
  72. package/src/components/data-table/data-table.test.tsx +0 -288
  73. package/src/components/date-picker/date-picker.stories.tsx +0 -103
  74. package/src/components/date-picker/date-picker.test.tsx +0 -205
  75. package/src/components/dialog/dialog.stories.tsx +0 -140
  76. package/src/components/dialog/dialog.test.tsx +0 -186
  77. package/src/components/drawer/drawer.stories.tsx +0 -111
  78. package/src/components/drawer/drawer.test.tsx +0 -187
  79. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +0 -128
  80. package/src/components/dropdown-menu/dropdown-menu.test.tsx +0 -321
  81. package/src/components/empty-state/empty-state.stories.tsx +0 -70
  82. package/src/components/empty-state/empty-state.test.tsx +0 -97
  83. package/src/components/form/form.stories.tsx +0 -144
  84. package/src/components/form/form.test.tsx +0 -203
  85. package/src/components/input/input.stories.tsx +0 -84
  86. package/src/components/input/input.test.tsx +0 -108
  87. package/src/components/label/label.stories.tsx +0 -51
  88. package/src/components/label/label.test.tsx +0 -55
  89. package/src/components/pagination/pagination.stories.tsx +0 -129
  90. package/src/components/pagination/pagination.test.tsx +0 -119
  91. package/src/components/popover/popover.stories.tsx +0 -84
  92. package/src/components/popover/popover.test.tsx +0 -163
  93. package/src/components/progress/progress.stories.tsx +0 -90
  94. package/src/components/progress/progress.test.tsx +0 -78
  95. package/src/components/radio-group/radio-group.stories.tsx +0 -102
  96. package/src/components/radio-group/radio-group.test.tsx +0 -152
  97. package/src/components/select/select.stories.tsx +0 -137
  98. package/src/components/select/select.test.tsx +0 -209
  99. package/src/components/separator/separator.stories.tsx +0 -42
  100. package/src/components/separator/separator.test.tsx +0 -46
  101. package/src/components/sheet/sheet.stories.tsx +0 -107
  102. package/src/components/sheet/sheet.test.tsx +0 -152
  103. package/src/components/skeleton/skeleton.stories.tsx +0 -46
  104. package/src/components/skeleton/skeleton.test.tsx +0 -57
  105. package/src/components/slider/slider.stories.tsx +0 -87
  106. package/src/components/slider/slider.test.tsx +0 -148
  107. package/src/components/spinner/spinner.stories.tsx +0 -47
  108. package/src/components/spinner/spinner.test.tsx +0 -41
  109. package/src/components/switch/switch.stories.tsx +0 -70
  110. package/src/components/switch/switch.test.tsx +0 -101
  111. package/src/components/table/table.stories.tsx +0 -131
  112. package/src/components/table/table.test.tsx +0 -153
  113. package/src/components/tabs/tabs.stories.tsx +0 -139
  114. package/src/components/tabs/tabs.test.tsx +0 -148
  115. package/src/components/toast/toast.stories.tsx +0 -167
  116. package/src/components/toast/toast.test.tsx +0 -306
  117. package/src/components/tooltip/tooltip.stories.tsx +0 -89
  118. package/src/components/tooltip/tooltip.test.tsx +0 -120
  119. package/src/lib/utils.test.ts +0 -32
  120. package/src/registry/meta.test.ts +0 -234
@@ -1,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
- };