@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,129 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
- import { useState } from "react";
3
-
4
- import {
5
- Pagination,
6
- PaginationContent,
7
- PaginationEllipsis,
8
- PaginationItem,
9
- PaginationLink,
10
- PaginationNext,
11
- PaginationPrevious,
12
- } from "./pagination";
13
-
14
- const meta = {
15
- title: "Navigation/Pagination",
16
- component: Pagination,
17
- parameters: { controls: { disable: true } },
18
- } satisfies Meta<typeof Pagination>;
19
-
20
- export default meta;
21
- type Story = StoryObj<typeof meta>;
22
-
23
- export const Default: Story = {
24
- render: () => (
25
- <Pagination>
26
- <PaginationContent>
27
- <PaginationItem>
28
- <PaginationPrevious href="#1" />
29
- </PaginationItem>
30
- <PaginationItem>
31
- <PaginationLink href="#1">1</PaginationLink>
32
- </PaginationItem>
33
- <PaginationItem>
34
- <PaginationLink href="#2" isActive>
35
- 2
36
- </PaginationLink>
37
- </PaginationItem>
38
- <PaginationItem>
39
- <PaginationLink href="#3">3</PaginationLink>
40
- </PaginationItem>
41
- <PaginationItem>
42
- <PaginationEllipsis />
43
- </PaginationItem>
44
- <PaginationItem>
45
- <PaginationLink href="#12">12</PaginationLink>
46
- </PaginationItem>
47
- <PaginationItem>
48
- <PaginationNext href="#3" />
49
- </PaginationItem>
50
- </PaginationContent>
51
- </Pagination>
52
- ),
53
- };
54
-
55
- export const Compact: Story = {
56
- render: () => (
57
- <Pagination>
58
- <PaginationContent>
59
- <PaginationItem>
60
- <PaginationPrevious href="#1" />
61
- </PaginationItem>
62
- <PaginationItem>
63
- <PaginationNext href="#3" />
64
- </PaginationItem>
65
- </PaginationContent>
66
- </Pagination>
67
- ),
68
- };
69
-
70
- /**
71
- * When paging only changes client state there is no URL to link to, so the
72
- * pages render as buttons instead — same styling, correct semantics.
73
- */
74
- export const AsButtons: Story = {
75
- render: function AsButtons() {
76
- const [page, setPage] = useState(2);
77
- const pages = [1, 2, 3, 4, 5];
78
-
79
- return (
80
- <Pagination>
81
- <PaginationContent>
82
- <PaginationItem>
83
- <PaginationLink asChild size="sm" className="gap-1 px-2.5">
84
- <button
85
- type="button"
86
- aria-label="Go to previous page"
87
- disabled={page === 1}
88
- onClick={() => {
89
- setPage((current) => Math.max(1, current - 1));
90
- }}
91
- >
92
- Previous
93
- </button>
94
- </PaginationLink>
95
- </PaginationItem>
96
- {pages.map((value) => (
97
- <PaginationItem key={value}>
98
- <PaginationLink asChild isActive={value === page}>
99
- <button
100
- type="button"
101
- aria-label={`Go to page ${String(value)}`}
102
- onClick={() => {
103
- setPage(value);
104
- }}
105
- >
106
- {value}
107
- </button>
108
- </PaginationLink>
109
- </PaginationItem>
110
- ))}
111
- <PaginationItem>
112
- <PaginationLink asChild size="sm" className="gap-1 px-2.5">
113
- <button
114
- type="button"
115
- aria-label="Go to next page"
116
- disabled={page === pages.length}
117
- onClick={() => {
118
- setPage((current) => Math.min(pages.length, current + 1));
119
- }}
120
- >
121
- Next
122
- </button>
123
- </PaginationLink>
124
- </PaginationItem>
125
- </PaginationContent>
126
- </Pagination>
127
- );
128
- },
129
- };
@@ -1,119 +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 {
7
- Pagination,
8
- PaginationContent,
9
- PaginationEllipsis,
10
- PaginationItem,
11
- PaginationLink,
12
- PaginationNext,
13
- PaginationPrevious,
14
- } from "./pagination";
15
-
16
- function Example({ current = 2 }: { current?: number } = {}) {
17
- return (
18
- <Pagination>
19
- <PaginationContent>
20
- <PaginationItem>
21
- <PaginationPrevious href="#1" />
22
- </PaginationItem>
23
- {[1, 2, 3].map((page) => (
24
- <PaginationItem key={page}>
25
- <PaginationLink href={`#${String(page)}`} isActive={page === current}>
26
- {page}
27
- </PaginationLink>
28
- </PaginationItem>
29
- ))}
30
- <PaginationItem>
31
- <PaginationEllipsis />
32
- </PaginationItem>
33
- <PaginationItem>
34
- <PaginationLink href="#12">12</PaginationLink>
35
- </PaginationItem>
36
- <PaginationItem>
37
- <PaginationNext href="#3" />
38
- </PaginationItem>
39
- </PaginationContent>
40
- </Pagination>
41
- );
42
- }
43
-
44
- describe("Pagination", () => {
45
- it("is a named navigation landmark", () => {
46
- render(<Example />);
47
- expect(screen.getByRole("navigation", { name: "Pagination" })).toBeInTheDocument();
48
- });
49
-
50
- it("marks the current page with aria-current", () => {
51
- render(<Example current={2} />);
52
-
53
- const current = screen.getByRole("link", { name: "2" });
54
- expect(current).toHaveAttribute("aria-current", "page");
55
- expect(screen.getByRole("link", { name: "1" })).not.toHaveAttribute("aria-current");
56
- });
57
-
58
- it("names the previous and next controls", () => {
59
- render(<Example />);
60
- expect(screen.getByRole("link", { name: "Go to previous page" })).toBeInTheDocument();
61
- expect(screen.getByRole("link", { name: "Go to next page" })).toBeInTheDocument();
62
- });
63
-
64
- it("renders pages as links, so they can be shared and opened in a tab", () => {
65
- render(<Example />);
66
- expect(screen.getByRole("link", { name: "2" })).toHaveAttribute("href", "#2");
67
- });
68
-
69
- it("hides the ellipsis but keeps a readable equivalent", () => {
70
- const { container } = render(<Example />);
71
- const ellipsis = container.querySelector("[data-slot='pagination-ellipsis']");
72
-
73
- expect(ellipsis).toHaveAttribute("aria-hidden", "true");
74
- expect(ellipsis).toHaveTextContent("More pages");
75
- });
76
-
77
- it("renders as buttons via asChild when paging is client state", async () => {
78
- const onClick = vi.fn();
79
- const user = userEvent.setup();
80
-
81
- render(
82
- <Pagination>
83
- <PaginationContent>
84
- <PaginationItem>
85
- <PaginationLink asChild isActive>
86
- <button type="button" onClick={onClick}>
87
- 1
88
- </button>
89
- </PaginationLink>
90
- </PaginationItem>
91
- </PaginationContent>
92
- </Pagination>,
93
- );
94
-
95
- const button = screen.getByRole("button", { name: "1" });
96
- expect(button).toHaveAttribute("aria-current", "page");
97
-
98
- await user.click(button);
99
- expect(onClick).toHaveBeenCalledTimes(1);
100
- });
101
-
102
- it("is reachable by keyboard", async () => {
103
- const user = userEvent.setup();
104
- render(<Example />);
105
-
106
- await user.tab();
107
- expect(screen.getByRole("link", { name: "Go to previous page" })).toHaveFocus();
108
- });
109
-
110
- it("styles the active page differently", () => {
111
- render(<Example current={3} />);
112
- expect(screen.getByRole("link", { name: "3" })).toHaveAttribute("data-active", "true");
113
- });
114
-
115
- it("has no accessibility violations", async () => {
116
- const { container } = render(<Example />);
117
- await expectNoA11yViolations(container);
118
- });
119
- });
@@ -1,84 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Button } from "@/components/button";
4
- import { Input } from "@/components/input";
5
- import { Label } from "@/components/label";
6
-
7
- import { Popover, PopoverArrow, PopoverContent, PopoverTrigger } from "./popover";
8
-
9
- const meta = {
10
- title: "Overlays/Popover",
11
- component: PopoverContent,
12
- args: { side: "bottom", align: "center" },
13
- argTypes: {
14
- side: { control: "inline-radio", options: ["top", "right", "bottom", "left"] },
15
- align: { control: "inline-radio", options: ["start", "center", "end"] },
16
- },
17
- } satisfies Meta<typeof PopoverContent>;
18
-
19
- export default meta;
20
- type Story = StoryObj<typeof meta>;
21
-
22
- export const Default: Story = {
23
- render: (args) => (
24
- <Popover>
25
- <PopoverTrigger asChild>
26
- <Button variant="outline">Dimensions</Button>
27
- </PopoverTrigger>
28
- <PopoverContent {...args} aria-labelledby="dimensions-heading">
29
- <div className="grid gap-3">
30
- <div className="space-y-1">
31
- <h4 id="dimensions-heading" className="text-sm font-medium">
32
- Dimensions
33
- </h4>
34
- <p className="text-xs text-muted-foreground">Set the layout for the layer.</p>
35
- </div>
36
- <div className="grid gap-2">
37
- <div className="grid grid-cols-3 items-center gap-2">
38
- <Label htmlFor="width">Width</Label>
39
- <Input id="width" inputSize="sm" defaultValue="100%" className="col-span-2" />
40
- </div>
41
- <div className="grid grid-cols-3 items-center gap-2">
42
- <Label htmlFor="height">Height</Label>
43
- <Input id="height" inputSize="sm" defaultValue="24px" className="col-span-2" />
44
- </div>
45
- </div>
46
- </div>
47
- </PopoverContent>
48
- </Popover>
49
- ),
50
- };
51
-
52
- /** Growth direction follows the anchor, so every side reads as one motion. */
53
- export const Sides: Story = {
54
- parameters: { controls: { disable: true } },
55
- render: () => (
56
- <div className="flex flex-wrap gap-3">
57
- {(["top", "right", "bottom", "left"] as const).map((side) => (
58
- <Popover key={side}>
59
- <PopoverTrigger asChild>
60
- <Button variant="outline">{side}</Button>
61
- </PopoverTrigger>
62
- <PopoverContent side={side} className="w-48" aria-label={`Opened from the ${side}`}>
63
- <p className="text-sm">Opened from the {side}.</p>
64
- </PopoverContent>
65
- </Popover>
66
- ))}
67
- </div>
68
- ),
69
- };
70
-
71
- export const WithArrow: Story = {
72
- parameters: { controls: { disable: true } },
73
- render: () => (
74
- <Popover>
75
- <PopoverTrigger asChild>
76
- <Button variant="outline">Show details</Button>
77
- </PopoverTrigger>
78
- <PopoverContent className="w-56" aria-label="Deployment details">
79
- <p className="text-sm">Deployed 4 minutes ago from main.</p>
80
- <PopoverArrow />
81
- </PopoverContent>
82
- </Popover>
83
- ),
84
- };
@@ -1,163 +0,0 @@
1
- import { render, screen, waitFor } 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 { Button } from "../button";
7
- import { Popover, PopoverArrow, PopoverClose, PopoverContent, PopoverTrigger } from "./popover";
8
-
9
- function Example() {
10
- return (
11
- <Popover>
12
- <PopoverTrigger asChild>
13
- <Button>Open settings</Button>
14
- </PopoverTrigger>
15
- <PopoverContent aria-labelledby="dimensions-heading">
16
- <p id="dimensions-heading">Dimensions</p>
17
- <PopoverClose>Done</PopoverClose>
18
- </PopoverContent>
19
- </Popover>
20
- );
21
- }
22
-
23
- describe("Popover", () => {
24
- it("is closed until the trigger is activated", async () => {
25
- const user = userEvent.setup();
26
- render(<Example />);
27
-
28
- expect(screen.queryByText("Dimensions")).not.toBeInTheDocument();
29
- await user.click(screen.getByRole("button", { name: "Open settings" }));
30
- expect(await screen.findByText("Dimensions")).toBeInTheDocument();
31
- });
32
-
33
- it("reports its state on the trigger", async () => {
34
- const user = userEvent.setup();
35
- render(<Example />);
36
-
37
- const trigger = screen.getByRole("button", { name: "Open settings" });
38
- expect(trigger).toHaveAttribute("aria-expanded", "false");
39
-
40
- await user.click(trigger);
41
- await waitFor(() => {
42
- expect(trigger).toHaveAttribute("aria-expanded", "true");
43
- });
44
- });
45
-
46
- it("opens from the keyboard", async () => {
47
- const user = userEvent.setup();
48
- render(<Example />);
49
-
50
- await user.tab();
51
- await user.keyboard("{Enter}");
52
- expect(await screen.findByText("Dimensions")).toBeInTheDocument();
53
- });
54
-
55
- it("closes on Escape and restores focus to the trigger", async () => {
56
- const user = userEvent.setup();
57
- render(<Example />);
58
-
59
- const trigger = screen.getByRole("button", { name: "Open settings" });
60
- await user.click(trigger);
61
- await screen.findByText("Dimensions");
62
-
63
- await user.keyboard("{Escape}");
64
- await waitFor(() => {
65
- expect(screen.queryByText("Dimensions")).not.toBeInTheDocument();
66
- });
67
- await waitFor(() => {
68
- expect(trigger).toHaveFocus();
69
- });
70
- });
71
-
72
- it("closes from PopoverClose", async () => {
73
- const user = userEvent.setup();
74
- render(<Example />);
75
- await user.click(screen.getByRole("button", { name: "Open settings" }));
76
- await screen.findByText("Dimensions");
77
-
78
- await user.click(screen.getByText("Done"));
79
- await waitFor(() => {
80
- expect(screen.queryByText("Dimensions")).not.toBeInTheDocument();
81
- });
82
- });
83
-
84
- it("merges a consumer className", async () => {
85
- const user = userEvent.setup();
86
- render(
87
- <Popover>
88
- <PopoverTrigger>Open</PopoverTrigger>
89
- <PopoverContent className="w-96" aria-label="Details" data-testid="content">
90
- Content
91
- </PopoverContent>
92
- </Popover>,
93
- );
94
-
95
- await user.click(screen.getByText("Open"));
96
- const content = await screen.findByTestId("content");
97
- expect(content).toHaveClass("w-96");
98
- expect(content).not.toHaveClass("w-72");
99
- });
100
-
101
- it("warns in development when the content has no accessible name", async () => {
102
- const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
103
- const user = userEvent.setup();
104
- render(
105
- <Popover>
106
- <PopoverTrigger>Open</PopoverTrigger>
107
- <PopoverContent>Unnamed</PopoverContent>
108
- </Popover>,
109
- );
110
-
111
- await user.click(screen.getByText("Open"));
112
- await screen.findByText("Unnamed");
113
-
114
- expect(warn).toHaveBeenCalledWith(expect.stringContaining("Missing accessible name"));
115
- warn.mockRestore();
116
- });
117
-
118
- it("does not warn when a name is supplied", async () => {
119
- const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
120
- const user = userEvent.setup();
121
- render(
122
- <Popover>
123
- <PopoverTrigger>Open</PopoverTrigger>
124
- <PopoverContent aria-label="Details">Named</PopoverContent>
125
- </Popover>,
126
- );
127
-
128
- await user.click(screen.getByText("Open"));
129
- await screen.findByText("Named");
130
-
131
- expect(warn).not.toHaveBeenCalled();
132
- warn.mockRestore();
133
- });
134
-
135
- it("renders an arrow that is hidden from assistive technology", async () => {
136
- const user = userEvent.setup();
137
- const { baseElement } = render(
138
- <Popover>
139
- <PopoverTrigger>Open</PopoverTrigger>
140
- <PopoverContent aria-label="Details">
141
- Details
142
- <PopoverArrow />
143
- </PopoverContent>
144
- </Popover>,
145
- );
146
-
147
- await user.click(screen.getByText("Open"));
148
- await screen.findByText("Details");
149
-
150
- const arrow = baseElement.querySelector("[data-slot='popover-arrow']");
151
- expect(arrow).toBeInTheDocument();
152
- expect(arrow).toHaveClass("fill-popover");
153
- });
154
-
155
- it("has no accessibility violations while open", async () => {
156
- const user = userEvent.setup();
157
- const { baseElement } = render(<Example />);
158
- await user.click(screen.getByRole("button", { name: "Open settings" }));
159
- await screen.findByText("Dimensions");
160
-
161
- await expectNoA11yViolations(baseElement);
162
- });
163
- });
@@ -1,90 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
- import { useEffect, useState } from "react";
3
-
4
- import { Progress } from "./progress";
5
-
6
- /** Named so its type is nameable in declaration output (TS2883). */
7
- const withFixedWidth: Decorator = (Story) => (
8
- <div className="w-72">
9
- <Story />
10
- </div>
11
- );
12
-
13
- const meta = {
14
- title: "Feedback/Progress",
15
- component: Progress,
16
- args: { value: 40, size: "md", tone: "primary", "aria-label": "Upload" },
17
- argTypes: {
18
- value: { control: { type: "range", min: 0, max: 100 } },
19
- size: { control: "inline-radio", options: ["sm", "md", "lg"] },
20
- tone: { control: "select", options: ["primary", "success", "warning", "destructive"] },
21
- },
22
- decorators: [withFixedWidth],
23
- } satisfies Meta<typeof Progress>;
24
-
25
- export default meta;
26
- type Story = StoryObj<typeof meta>;
27
-
28
- export const Default: Story = {};
29
-
30
- /** The number belongs on screen too — a bar alone is hard to read precisely. */
31
- export const WithLabel: Story = {
32
- parameters: { controls: { disable: true } },
33
- render: function WithLabel() {
34
- const [value, setValue] = useState(12);
35
-
36
- useEffect(() => {
37
- const timer = setInterval(() => {
38
- setValue((current) => (current >= 100 ? 0 : current + 4));
39
- }, 400);
40
- return () => {
41
- clearInterval(timer);
42
- };
43
- }, []);
44
-
45
- return (
46
- <div className="grid gap-2">
47
- <div className="flex items-center justify-between text-sm">
48
- <span id="upload-label">Uploading assets</span>
49
- <span className="text-muted-foreground tabular-nums">{value}%</span>
50
- </div>
51
- <Progress value={value} aria-labelledby="upload-label" />
52
- </div>
53
- );
54
- },
55
- };
56
-
57
- /** No known duration — announced as indeterminate, not as zero percent. */
58
- export const Indeterminate: Story = {
59
- parameters: { controls: { disable: true } },
60
- render: () => (
61
- <div className="grid gap-2">
62
- <span className="text-sm" id="indeterminate-label">
63
- Preparing your export…
64
- </span>
65
- <Progress aria-labelledby="indeterminate-label" />
66
- </div>
67
- ),
68
- };
69
-
70
- export const Tones: Story = {
71
- parameters: { controls: { disable: true } },
72
- render: () => (
73
- <div className="grid gap-4">
74
- {(["primary", "success", "warning", "destructive"] as const).map((tone) => (
75
- <Progress key={tone} tone={tone} value={65} aria-label={tone} />
76
- ))}
77
- </div>
78
- ),
79
- };
80
-
81
- export const Sizes: Story = {
82
- parameters: { controls: { disable: true } },
83
- render: () => (
84
- <div className="grid gap-4">
85
- {(["sm", "md", "lg"] as const).map((size) => (
86
- <Progress key={size} size={size} value={55} aria-label={size} />
87
- ))}
88
- </div>
89
- ),
90
- };
@@ -1,78 +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 { Progress } from "./progress";
6
-
7
- describe("Progress", () => {
8
- it("exposes a progressbar with its value", () => {
9
- render(<Progress value={40} aria-label="Upload" />);
10
-
11
- const bar = screen.getByRole("progressbar", { name: "Upload" });
12
- expect(bar).toHaveAttribute("aria-valuenow", "40");
13
- expect(bar).toHaveAttribute("aria-valuemax", "100");
14
- });
15
-
16
- it("reports an indeterminate state differently from zero", () => {
17
- render(<Progress aria-label="Working" />);
18
-
19
- const bar = screen.getByRole("progressbar");
20
- expect(bar).not.toHaveAttribute("aria-valuenow");
21
- expect(bar).toHaveAttribute("data-state", "indeterminate");
22
- });
23
-
24
- it("treats zero as a real value, not as unknown", () => {
25
- render(<Progress value={0} aria-label="Upload" />);
26
-
27
- const bar = screen.getByRole("progressbar");
28
- expect(bar).toHaveAttribute("aria-valuenow", "0");
29
- expect(bar).toHaveAttribute("data-state", "loading");
30
- });
31
-
32
- it("fills proportionally to the value", () => {
33
- const { container } = render(<Progress value={25} aria-label="Upload" />);
34
- const indicator = container.querySelector("[data-slot='progress-indicator']");
35
- expect(indicator).toHaveStyle({ transform: "translateX(-75%)" });
36
- });
37
-
38
- it("clamps a value outside the range", () => {
39
- const { container } = render(<Progress value={150} aria-label="Upload" max={100} />);
40
- const indicator = container.querySelector("[data-slot='progress-indicator']");
41
- expect(indicator).toHaveStyle({ transform: "translateX(-0%)" });
42
- });
43
-
44
- it.each([
45
- ["sm", "h-1"],
46
- ["md", "h-2"],
47
- ["lg", "h-3"],
48
- ] as const)("applies the %s size", (size, expectedClass) => {
49
- render(<Progress value={40} size={size} aria-label="Upload" />);
50
- expect(screen.getByRole("progressbar")).toHaveClass(expectedClass);
51
- });
52
-
53
- it.each([
54
- ["primary", "--progress-fill:var(--color-primary)"],
55
- ["success", "--progress-fill:var(--color-success)"],
56
- ["destructive", "--progress-fill:var(--color-destructive)"],
57
- ] as const)("applies the %s tone", (tone, expectedClass) => {
58
- render(<Progress value={40} tone={tone} aria-label="Upload" />);
59
- expect(screen.getByRole("progressbar").className).toContain(expectedClass);
60
- });
61
-
62
- it("lets a consumer className override a conflicting utility", () => {
63
- render(<Progress value={40} className="h-6" aria-label="Upload" />);
64
- const bar = screen.getByRole("progressbar");
65
- expect(bar).toHaveClass("h-6");
66
- expect(bar).not.toHaveClass("h-2");
67
- });
68
-
69
- it("has no accessibility violations", async () => {
70
- const { container } = render(
71
- <div>
72
- <span id="upload-label">Uploading</span>
73
- <Progress value={40} aria-labelledby="upload-label" />
74
- </div>,
75
- );
76
- await expectNoA11yViolations(container);
77
- });
78
- });