@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,148 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { describe, expect, it, vi } from "vitest";
4
-
5
- import { expectNoA11yViolations } from "../../../test/a11y";
6
- import { Tabs, TabsContent, TabsList, TabsTrigger, type TabsListProps } from "./tabs";
7
-
8
- function Example({
9
- variant,
10
- onValueChange,
11
- activationMode,
12
- }: {
13
- variant?: TabsListProps["variant"];
14
- onValueChange?: (value: string) => void;
15
- activationMode?: "automatic" | "manual";
16
- } = {}) {
17
- return (
18
- <Tabs defaultValue="account" onValueChange={onValueChange} activationMode={activationMode}>
19
- <TabsList variant={variant}>
20
- <TabsTrigger value="account" variant={variant}>
21
- Account
22
- </TabsTrigger>
23
- <TabsTrigger value="password" variant={variant}>
24
- Password
25
- </TabsTrigger>
26
- <TabsTrigger value="team" variant={variant} disabled>
27
- Team
28
- </TabsTrigger>
29
- </TabsList>
30
- <TabsContent value="account">Account panel</TabsContent>
31
- <TabsContent value="password">Password panel</TabsContent>
32
- <TabsContent value="team">Team panel</TabsContent>
33
- </Tabs>
34
- );
35
- }
36
-
37
- describe("Tabs", () => {
38
- it("shows only the active panel", () => {
39
- render(<Example />);
40
- expect(screen.getByText("Account panel")).toBeInTheDocument();
41
- expect(screen.queryByText("Password panel")).not.toBeInTheDocument();
42
- });
43
-
44
- it("marks the active tab as selected", () => {
45
- render(<Example />);
46
- expect(screen.getByRole("tab", { name: "Account" })).toHaveAttribute(
47
- "aria-selected",
48
- "true",
49
- );
50
- expect(screen.getByRole("tab", { name: "Password" })).toHaveAttribute(
51
- "aria-selected",
52
- "false",
53
- );
54
- });
55
-
56
- it("switches panels on click", async () => {
57
- const user = userEvent.setup();
58
- render(<Example />);
59
-
60
- await user.click(screen.getByRole("tab", { name: "Password" }));
61
- expect(screen.getByText("Password panel")).toBeInTheDocument();
62
- expect(screen.queryByText("Account panel")).not.toBeInTheDocument();
63
- });
64
-
65
- it("moves between tabs with the arrow keys", async () => {
66
- const user = userEvent.setup();
67
- render(<Example />);
68
-
69
- await user.click(screen.getByRole("tab", { name: "Account" }));
70
- await user.keyboard("{ArrowRight}");
71
-
72
- expect(screen.getByRole("tab", { name: "Password" })).toHaveFocus();
73
- expect(screen.getByText("Password panel")).toBeInTheDocument();
74
- });
75
-
76
- it("keeps only the active tab in the tab sequence", async () => {
77
- const user = userEvent.setup();
78
- render(<Example />);
79
-
80
- await user.tab();
81
- expect(screen.getByRole("tab", { name: "Account" })).toHaveFocus();
82
-
83
- // A second Tab leaves the tablist entirely rather than visiting each tab.
84
- await user.tab();
85
- expect(screen.getByRole("tab", { name: "Password" })).not.toHaveFocus();
86
- });
87
-
88
- it("skips disabled tabs when arrowing", async () => {
89
- const user = userEvent.setup();
90
- render(<Example />);
91
-
92
- await user.click(screen.getByRole("tab", { name: "Password" }));
93
- await user.keyboard("{ArrowRight}");
94
-
95
- expect(screen.getByRole("tab", { name: "Team" })).not.toHaveFocus();
96
- });
97
-
98
- it("defers activation in manual mode", async () => {
99
- const user = userEvent.setup();
100
- render(<Example activationMode="manual" />);
101
-
102
- await user.click(screen.getByRole("tab", { name: "Account" }));
103
- await user.keyboard("{ArrowRight}");
104
-
105
- expect(screen.getByRole("tab", { name: "Password" })).toHaveFocus();
106
- expect(screen.getByText("Account panel")).toBeInTheDocument();
107
-
108
- await user.keyboard("{Enter}");
109
- expect(screen.getByText("Password panel")).toBeInTheDocument();
110
- });
111
-
112
- it("reports value changes", async () => {
113
- const onValueChange = vi.fn();
114
- const user = userEvent.setup();
115
- render(<Example onValueChange={onValueChange} />);
116
-
117
- await user.click(screen.getByRole("tab", { name: "Password" }));
118
- expect(onValueChange).toHaveBeenCalledWith("password");
119
- });
120
-
121
- it.each([
122
- ["solid", "bg-muted"],
123
- ["underline", "border-b"],
124
- ] as const)("applies the %s list variant", (variant, expectedClass) => {
125
- render(<Example variant={variant} />);
126
- expect(screen.getByRole("tablist")).toHaveClass(expectedClass);
127
- });
128
-
129
- it("lets a consumer className override a conflicting utility", () => {
130
- render(
131
- <Tabs defaultValue="a">
132
- <TabsList className="bg-card">
133
- <TabsTrigger value="a">A</TabsTrigger>
134
- </TabsList>
135
- <TabsContent value="a">Panel</TabsContent>
136
- </Tabs>,
137
- );
138
-
139
- const list = screen.getByRole("tablist");
140
- expect(list).toHaveClass("bg-card");
141
- expect(list).not.toHaveClass("bg-muted");
142
- });
143
-
144
- it("has no accessibility violations", async () => {
145
- const { container } = render(<Example />);
146
- await expectNoA11yViolations(container);
147
- });
148
- });
@@ -1,167 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Button } from "@/components/button";
4
-
5
- import { Toaster, type ToastPosition } from "./toast";
6
- import { toast } from "./toast-store";
7
-
8
- const meta = {
9
- title: "Feedback/Toast",
10
- component: Toaster,
11
- args: { position: "bottom-right" },
12
- argTypes: {
13
- position: {
14
- control: "select",
15
- options: [
16
- "top-left",
17
- "top-center",
18
- "top-right",
19
- "bottom-left",
20
- "bottom-center",
21
- "bottom-right",
22
- ] satisfies ToastPosition[],
23
- },
24
- duration: { control: { type: "number", min: 1000, max: 20000, step: 500 } },
25
- },
26
- } satisfies Meta<typeof Toaster>;
27
-
28
- export default meta;
29
- type Story = StoryObj<typeof meta>;
30
-
31
- export const Default: Story = {
32
- render: (args) => (
33
- <>
34
- <Button onClick={() => toast("Project created")}>Show toast</Button>
35
- <Toaster {...args} />
36
- </>
37
- ),
38
- };
39
-
40
- export const Variants: Story = {
41
- render: (args) => (
42
- <>
43
- <div className="flex flex-wrap gap-2">
44
- <Button variant="outline" onClick={() => toast("Saved")}>
45
- Default
46
- </Button>
47
- <Button variant="outline" onClick={() => toast.success("Deployment succeeded")}>
48
- Success
49
- </Button>
50
- <Button variant="outline" onClick={() => toast.info("Build queued")}>
51
- Info
52
- </Button>
53
- <Button variant="outline" onClick={() => toast.warning("Approaching your quota")}>
54
- Warning
55
- </Button>
56
- <Button variant="outline" onClick={() => toast.error("Payment failed")}>
57
- Error
58
- </Button>
59
- </div>
60
- <Toaster {...args} />
61
- </>
62
- ),
63
- };
64
-
65
- export const WithDescription: Story = {
66
- render: (args) => (
67
- <>
68
- <Button
69
- onClick={() =>
70
- toast.success("Project created", {
71
- description: "acme-inc is ready. Deploy when you are.",
72
- })
73
- }
74
- >
75
- Show toast
76
- </Button>
77
- <Toaster {...args} />
78
- </>
79
- ),
80
- };
81
-
82
- /** An action needs alt text: "Undo" alone is meaningless once the toast is gone. */
83
- export const WithAction: Story = {
84
- render: (args) => (
85
- <>
86
- <Button
87
- onClick={() =>
88
- toast("Message archived", {
89
- action: {
90
- label: "Undo",
91
- altText: "Undo archiving the message",
92
- onClick: () => toast.success("Message restored"),
93
- },
94
- })
95
- }
96
- >
97
- Archive message
98
- </Button>
99
- <Toaster {...args} />
100
- </>
101
- ),
102
- };
103
-
104
- export const Persistent: Story = {
105
- render: (args) => (
106
- <>
107
- <Button
108
- variant="outline"
109
- onClick={() =>
110
- toast.custom({
111
- title: "Connection lost",
112
- description: "Reconnecting…",
113
- variant: "destructive",
114
- duration: Infinity,
115
- })
116
- }
117
- >
118
- Show until dismissed
119
- </Button>
120
- <Toaster {...args} />
121
- </>
122
- ),
123
- };
124
-
125
- export const Promise: Story = {
126
- render: (args) => (
127
- <>
128
- <Button
129
- variant="outline"
130
- onClick={() => {
131
- const work = new window.Promise<string>((resolve) => {
132
- window.setTimeout(() => {
133
- resolve("acme-inc");
134
- }, 1500);
135
- });
136
-
137
- void toast.promise(work, {
138
- loading: "Creating project…",
139
- success: (name) => `Created ${name}`,
140
- error: "Could not create the project",
141
- });
142
- }}
143
- >
144
- Create project
145
- </Button>
146
- <Toaster {...args} />
147
- </>
148
- ),
149
- };
150
-
151
- export const Stacking: Story = {
152
- render: (args) => (
153
- <>
154
- <Button
155
- variant="outline"
156
- onClick={() => {
157
- for (let index = 1; index <= 6; index += 1) {
158
- toast(`Event ${String(index)}`);
159
- }
160
- }}
161
- >
162
- Raise six toasts
163
- </Button>
164
- <Toaster {...args} />
165
- </>
166
- ),
167
- };
@@ -1,306 +0,0 @@
1
- import { act, 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 { Toaster } from "./toast";
7
- import { TOAST_LIMIT, resetToasts, toast } from "./toast-store";
8
-
9
- afterEach(() => {
10
- act(() => {
11
- resetToasts();
12
- });
13
- });
14
-
15
- function raise(fn: () => void) {
16
- act(() => {
17
- fn();
18
- });
19
- }
20
-
21
- describe("toast store", () => {
22
- it("returns the id it created", () => {
23
- let id = "";
24
- raise(() => {
25
- id = toast("Saved");
26
- });
27
- expect(id).toMatch(/^toast-\d+$/);
28
- });
29
-
30
- it("honours an explicit id, replacing rather than duplicating", async () => {
31
- render(<Toaster />);
32
-
33
- raise(() => toast.custom({ id: "sync", title: "Syncing" }));
34
- expect(await screen.findByText("Syncing")).toBeInTheDocument();
35
-
36
- raise(() => toast.custom({ id: "sync", title: "Synced" }));
37
- await waitFor(() => {
38
- expect(screen.getByText("Synced")).toBeInTheDocument();
39
- });
40
- expect(screen.queryByText("Syncing")).not.toBeInTheDocument();
41
- });
42
-
43
- it("keeps only the most recent toasts", async () => {
44
- render(<Toaster />);
45
-
46
- raise(() => {
47
- for (let index = 0; index < TOAST_LIMIT + 3; index += 1) {
48
- toast(`Message ${String(index)}`);
49
- }
50
- });
51
-
52
- await waitFor(() => {
53
- expect(document.querySelectorAll("[data-slot='toast']")).toHaveLength(TOAST_LIMIT);
54
- });
55
- expect(screen.queryByText("Message 0")).not.toBeInTheDocument();
56
- expect(screen.getByText(`Message ${String(TOAST_LIMIT + 2)}`)).toBeInTheDocument();
57
- });
58
-
59
- it("calls onDismiss when a toast is removed", () => {
60
- const onDismiss = vi.fn();
61
- let id = "";
62
- raise(() => {
63
- id = toast.custom({ title: "Saved", onDismiss });
64
- });
65
-
66
- raise(() => {
67
- toast.dismiss(id);
68
- });
69
- expect(onDismiss).toHaveBeenCalledWith(id);
70
- });
71
-
72
- it("updates a toast in place", async () => {
73
- render(<Toaster />);
74
-
75
- let id = "";
76
- raise(() => {
77
- id = toast("Uploading");
78
- });
79
- await screen.findByText("Uploading");
80
-
81
- raise(() => {
82
- toast.update(id, { title: "Uploaded", variant: "success" });
83
- });
84
-
85
- await waitFor(() => {
86
- expect(screen.getByText("Uploaded")).toBeInTheDocument();
87
- });
88
- expect(document.querySelector("[data-slot='toast']")).toHaveClass("border-success/30");
89
- });
90
-
91
- it("exposes a helper per variant", async () => {
92
- render(<Toaster />);
93
-
94
- raise(() => {
95
- toast.default("Plain");
96
- toast.info("Informational");
97
- });
98
-
99
- expect(await screen.findByText("Plain")).toBeInTheDocument();
100
- expect(screen.getByText("Informational")).toBeInTheDocument();
101
- });
102
-
103
- it("dismisses everything at once", async () => {
104
- render(<Toaster />);
105
-
106
- raise(() => {
107
- toast("One");
108
- toast("Two");
109
- });
110
- await screen.findByText("One");
111
-
112
- raise(() => {
113
- toast.dismissAll();
114
- });
115
- await waitFor(() => {
116
- expect(screen.queryByText("One")).not.toBeInTheDocument();
117
- });
118
- });
119
-
120
- describe("promise", () => {
121
- it("resolves the pending toast into a success toast", async () => {
122
- render(<Toaster />);
123
-
124
- let resolve!: (value: string) => void;
125
- const pending = new Promise<string>((r) => {
126
- resolve = r;
127
- });
128
-
129
- raise(() => {
130
- void toast.promise(pending, {
131
- loading: "Saving…",
132
- success: (value) => `Saved ${value}`,
133
- error: "Failed",
134
- });
135
- });
136
-
137
- expect(await screen.findByText("Saving…")).toBeInTheDocument();
138
-
139
- await act(async () => {
140
- resolve("project");
141
- await pending;
142
- });
143
-
144
- await waitFor(() => {
145
- expect(screen.getByText("Saved project")).toBeInTheDocument();
146
- });
147
- });
148
-
149
- it("turns a rejection into an error toast and still rejects", async () => {
150
- render(<Toaster />);
151
-
152
- const failure = new Error("network");
153
- const pending = Promise.reject(failure);
154
-
155
- let returned!: Promise<never>;
156
- raise(() => {
157
- returned = toast.promise(pending, {
158
- loading: "Saving…",
159
- success: "Saved",
160
- error: (error) => `Failed: ${(error as Error).message}`,
161
- });
162
- });
163
-
164
- // The original promise is returned untouched, so callers still handle it.
165
- await expect(returned).rejects.toThrow("network");
166
- await waitFor(() => {
167
- expect(screen.getByText("Failed: network")).toBeInTheDocument();
168
- });
169
- });
170
- });
171
- });
172
-
173
- describe("Toaster", () => {
174
- it("renders nothing until a toast is raised", () => {
175
- render(<Toaster />);
176
- expect(document.querySelector("[data-slot='toast']")).not.toBeInTheDocument();
177
- });
178
-
179
- it("renders a title and description", async () => {
180
- render(<Toaster />);
181
- raise(() => toast("Project created", { description: "It is ready to use." }));
182
-
183
- expect(await screen.findByText("Project created")).toBeInTheDocument();
184
- expect(screen.getByText("It is ready to use.")).toBeInTheDocument();
185
- });
186
-
187
- it.each([
188
- ["success", "border-success/30"],
189
- ["warning", "border-warning/35"],
190
- ["destructive", "border-destructive/30"],
191
- ["info", "border-info/30"],
192
- ] as const)("applies the %s variant", async (variant, expectedClass) => {
193
- render(<Toaster />);
194
- raise(() => toast.custom({ title: "Message", variant }));
195
- await screen.findByText("Message");
196
-
197
- expect(document.querySelector("[data-slot='toast']")).toHaveClass(expectedClass);
198
- });
199
-
200
- it("interrupts for problems and waits for a pause otherwise", async () => {
201
- render(<Toaster />);
202
-
203
- raise(() => toast.error("Payment failed"));
204
- const problem = await screen.findByText("Payment failed");
205
- expect(problem.closest("[data-slot='toast']")).toHaveAttribute("data-type", "foreground");
206
-
207
- raise(() => toast.success("Saved"));
208
- const routine = await screen.findByText("Saved");
209
- expect(routine.closest("[data-slot='toast']")).toHaveAttribute("data-type", "background");
210
- });
211
-
212
- it("announces a problem assertively through a live region", async () => {
213
- render(<Toaster />);
214
- raise(() => toast.error("Payment failed"));
215
-
216
- const announcement = await screen.findByRole("status");
217
- expect(announcement).toHaveAttribute("aria-live", "assertive");
218
- // The primitive mounts the region first and fills it on a later tick.
219
- await waitFor(() => {
220
- expect(announcement).toHaveTextContent("Payment failed");
221
- });
222
- });
223
-
224
- it("dismisses from the close button", async () => {
225
- const user = userEvent.setup();
226
- render(<Toaster />);
227
- raise(() => toast("Saved"));
228
- await screen.findByText("Saved");
229
-
230
- await user.click(screen.getByRole("button", { name: "Dismiss" }));
231
- await waitFor(() => {
232
- expect(screen.queryByText("Saved")).not.toBeInTheDocument();
233
- });
234
- });
235
-
236
- it("runs an action and exposes alt text for screen readers", async () => {
237
- const onClick = vi.fn();
238
- const user = userEvent.setup();
239
- render(<Toaster />);
240
-
241
- raise(() =>
242
- toast.custom({
243
- title: "Message archived",
244
- action: { label: "Undo", altText: "Undo archiving the message", onClick },
245
- }),
246
- );
247
-
248
- const action = await screen.findByRole("button", { name: "Undo" });
249
- // The primitive announces the alt text in place of the visible label, since
250
- // "Undo" alone is meaningless once the toast is gone.
251
- expect(action).toHaveAttribute(
252
- "data-radix-toast-announce-alt",
253
- "Undo archiving the message",
254
- );
255
- const announcement = await screen.findByRole("status");
256
- await waitFor(() => {
257
- expect(announcement).toHaveTextContent("Undo archiving the message");
258
- });
259
-
260
- await user.click(action);
261
- expect(onClick).toHaveBeenCalledTimes(1);
262
- });
263
-
264
- it("falls back to the action label for alt text", async () => {
265
- render(<Toaster />);
266
- raise(() =>
267
- toast.custom({
268
- title: "Message archived",
269
- action: { label: "Undo", onClick: () => undefined },
270
- }),
271
- );
272
-
273
- expect(await screen.findByRole("button", { name: "Undo" })).toHaveAttribute(
274
- "data-radix-toast-announce-alt",
275
- "Undo",
276
- );
277
- });
278
-
279
- it.each([
280
- ["top-left", "top-0"],
281
- ["bottom-right", "bottom-0"],
282
- ["bottom-center", "left-1/2"],
283
- ] as const)("positions the viewport for %s", async (position, expectedClass) => {
284
- const { baseElement } = render(<Toaster position={position} />);
285
- raise(() => toast("Message"));
286
- await screen.findByText("Message");
287
-
288
- expect(baseElement.querySelector("[data-slot='toast-viewport']")).toHaveClass(
289
- expectedClass,
290
- );
291
- });
292
-
293
- it("has no accessibility violations", async () => {
294
- const { baseElement } = render(<Toaster />);
295
- raise(() =>
296
- toast.custom({
297
- title: "Project created",
298
- description: "It is ready to use.",
299
- action: { label: "View", onClick: () => undefined },
300
- }),
301
- );
302
- await screen.findByText("Project created");
303
-
304
- await expectNoA11yViolations(baseElement);
305
- });
306
- });
@@ -1,89 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
- import { Bold, Italic, Underline } from "lucide-react";
3
-
4
- import { Button } from "@/components/button";
5
-
6
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
7
-
8
- const meta = {
9
- title: "Overlays/Tooltip",
10
- component: Tooltip,
11
- args: { delayDuration: 200 },
12
- argTypes: {
13
- delayDuration: { control: { type: "number", min: 0, max: 1000, step: 50 } },
14
- },
15
- } satisfies Meta<typeof Tooltip>;
16
-
17
- export default meta;
18
- type Story = StoryObj<typeof meta>;
19
-
20
- export const Default: Story = {
21
- render: (args) => (
22
- <Tooltip {...args}>
23
- <TooltipTrigger asChild>
24
- <Button variant="outline">Hover or focus me</Button>
25
- </TooltipTrigger>
26
- <TooltipContent>Deploys the current branch</TooltipContent>
27
- </Tooltip>
28
- ),
29
- };
30
-
31
- /**
32
- * On an icon-only control the accessible name comes from aria-label. The
33
- * tooltip repeats it visually; it does not supply it.
34
- */
35
- export const IconButtons: Story = {
36
- parameters: { controls: { disable: true } },
37
- render: () => (
38
- <TooltipProvider delayDuration={150}>
39
- <div className="flex items-center gap-1">
40
- {[
41
- { icon: Bold, label: "Bold" },
42
- { icon: Italic, label: "Italic" },
43
- { icon: Underline, label: "Underline" },
44
- ].map(({ icon: Icon, label }) => (
45
- <Tooltip key={label}>
46
- <TooltipTrigger asChild>
47
- <Button variant="ghost" size="icon" aria-label={label}>
48
- <Icon />
49
- </Button>
50
- </TooltipTrigger>
51
- <TooltipContent>{label}</TooltipContent>
52
- </Tooltip>
53
- ))}
54
- </div>
55
- </TooltipProvider>
56
- ),
57
- };
58
-
59
- export const Sides: Story = {
60
- parameters: { controls: { disable: true } },
61
- render: () => (
62
- <TooltipProvider delayDuration={0}>
63
- <div className="flex flex-wrap gap-3">
64
- {(["top", "right", "bottom", "left"] as const).map((side) => (
65
- <Tooltip key={side}>
66
- <TooltipTrigger asChild>
67
- <Button variant="outline">{side}</Button>
68
- </TooltipTrigger>
69
- <TooltipContent side={side}>Anchored {side}</TooltipContent>
70
- </Tooltip>
71
- ))}
72
- </div>
73
- </TooltipProvider>
74
- ),
75
- };
76
-
77
- export const LongText: Story = {
78
- parameters: { controls: { disable: true } },
79
- render: () => (
80
- <Tooltip delayDuration={0}>
81
- <TooltipTrigger asChild>
82
- <Button variant="outline">Retention</Button>
83
- </TooltipTrigger>
84
- <TooltipContent>
85
- Logs are kept for 30 days on the current plan, then aggregated into daily summaries.
86
- </TooltipContent>
87
- </Tooltip>
88
- ),
89
- };