@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,83 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { AdminUsersBlock, type AdminUser } from "./admin-users";
4
-
5
- /** Named so its type is nameable in declaration output (TS2883). */
6
- const withPageWidth: Decorator = (Story) => (
7
- <div className="w-[64rem] max-w-full">
8
- <Story />
9
- </div>
10
- );
11
-
12
- const USERS: AdminUser[] = [
13
- {
14
- id: "1",
15
- name: "Ada Lovelace",
16
- email: "ada@example.com",
17
- role: "Owner",
18
- status: "Active",
19
- lastActive: "2 hours ago",
20
- },
21
- {
22
- id: "2",
23
- name: "Grace Hopper",
24
- email: "grace@example.com",
25
- role: "Admin",
26
- status: "Active",
27
- lastActive: "Yesterday",
28
- },
29
- {
30
- id: "3",
31
- name: "Katherine Johnson",
32
- email: "kj@example.com",
33
- role: "Member",
34
- status: "Invited",
35
- lastActive: "Never",
36
- },
37
- {
38
- id: "4",
39
- name: "Radia Perlman",
40
- email: "radia@example.com",
41
- role: "Viewer",
42
- status: "Suspended",
43
- lastActive: "3 weeks ago",
44
- },
45
- {
46
- id: "5",
47
- name: "Barbara Liskov",
48
- email: "barbara@example.com",
49
- role: "Member",
50
- status: "Active",
51
- lastActive: "5 minutes ago",
52
- },
53
- {
54
- id: "6",
55
- name: "Margaret Hamilton",
56
- email: "margaret@example.com",
57
- role: "Admin",
58
- status: "Active",
59
- lastActive: "Last week",
60
- },
61
- ];
62
-
63
- const meta = {
64
- title: "Blocks/Admin — users",
65
- component: AdminUsersBlock,
66
- args: { users: USERS },
67
- parameters: { controls: { disable: true } },
68
- decorators: [withPageWidth],
69
- } satisfies Meta<typeof AdminUsersBlock>;
70
-
71
- export default meta;
72
- type Story = StoryObj<typeof meta>;
73
-
74
- /** Filter, sort and hide columns. The matching count is announced as you type. */
75
- export const Default: Story = {};
76
-
77
- export const Empty: Story = {
78
- args: { users: [] },
79
- };
80
-
81
- export const SinglePerson: Story = {
82
- args: { users: [USERS[0]!] },
83
- };
@@ -1,159 +0,0 @@
1
- import { render, screen, waitFor, within } 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 { AdminUsersBlock, type AdminUser } from "./admin-users";
7
-
8
- const USERS: AdminUser[] = [
9
- {
10
- id: "1",
11
- name: "Ada Lovelace",
12
- email: "ada@example.com",
13
- role: "Owner",
14
- status: "Active",
15
- lastActive: "2 hours ago",
16
- },
17
- {
18
- id: "2",
19
- name: "Grace Hopper",
20
- email: "grace@example.com",
21
- role: "Admin",
22
- status: "Active",
23
- lastActive: "Yesterday",
24
- },
25
- {
26
- id: "3",
27
- name: "Katherine Johnson",
28
- email: "kj@example.com",
29
- role: "Member",
30
- status: "Invited",
31
- lastActive: "Never",
32
- },
33
- {
34
- id: "4",
35
- name: "Radia Perlman",
36
- email: "radia@example.com",
37
- role: "Viewer",
38
- status: "Suspended",
39
- lastActive: "3 weeks ago",
40
- },
41
- ];
42
-
43
- function rowCount(): number {
44
- const body = screen.getAllByRole("rowgroup")[1];
45
- return body ? within(body).queryAllByRole("row").length : 0;
46
- }
47
-
48
- describe("AdminUsersBlock", () => {
49
- it("renders a row per user", () => {
50
- render(<AdminUsersBlock users={USERS} />);
51
- expect(rowCount()).toBe(4);
52
- });
53
-
54
- it("gives the filter a real label, not just a placeholder", () => {
55
- render(<AdminUsersBlock users={USERS} />);
56
- // A placeholder disappears the moment anyone types, taking the field's name
57
- // with it.
58
- expect(screen.getByLabelText("Filter by name or email")).toBeInTheDocument();
59
- });
60
-
61
- it("filters by name and by email", async () => {
62
- const user = userEvent.setup();
63
- render(<AdminUsersBlock users={USERS} />);
64
-
65
- await user.type(screen.getByLabelText("Filter by name or email"), "grace");
66
- await waitFor(() => {
67
- expect(rowCount()).toBe(1);
68
- });
69
- expect(screen.getByText("Grace Hopper")).toBeInTheDocument();
70
-
71
- await user.clear(screen.getByLabelText("Filter by name or email"));
72
- await user.type(screen.getByLabelText("Filter by name or email"), "kj@example");
73
- await waitFor(() => {
74
- expect(screen.getByText("Katherine Johnson")).toBeInTheDocument();
75
- });
76
- });
77
-
78
- it("announces the matching count, since results change unbidden", async () => {
79
- const user = userEvent.setup();
80
- const { container } = render(<AdminUsersBlock users={USERS} />);
81
-
82
- const status = container.querySelector("[aria-live='polite']");
83
- expect(status).toHaveTextContent("4 people match the filter.");
84
-
85
- await user.type(screen.getByLabelText("Filter by name or email"), "grace");
86
- await waitFor(() => {
87
- expect(status).toHaveTextContent("1 person match");
88
- });
89
- });
90
-
91
- it("shows an empty state naming the query that found nothing", async () => {
92
- const user = userEvent.setup();
93
- render(<AdminUsersBlock users={USERS} />);
94
-
95
- await user.type(screen.getByLabelText("Filter by name or email"), "zzzz");
96
- expect(await screen.findByText(/No one matches/)).toBeInTheDocument();
97
- });
98
-
99
- it("names each row's action button after its row", () => {
100
- render(<AdminUsersBlock users={USERS} />);
101
- // Four identical "Actions" buttons say nothing about which row you are on.
102
- expect(
103
- screen.getByRole("button", { name: "Actions for Ada Lovelace" }),
104
- ).toBeInTheDocument();
105
- expect(
106
- screen.getByRole("button", { name: "Actions for Grace Hopper" }),
107
- ).toBeInTheDocument();
108
- });
109
-
110
- it("runs the row actions", async () => {
111
- const onEdit = vi.fn();
112
- const onRemove = vi.fn();
113
- const user = userEvent.setup();
114
- render(<AdminUsersBlock users={USERS} onEdit={onEdit} onRemove={onRemove} />);
115
-
116
- await user.click(screen.getByRole("button", { name: "Actions for Ada Lovelace" }));
117
- await user.click(await screen.findByRole("menuitem", { name: "Edit member" }));
118
- expect(onEdit).toHaveBeenCalledWith(expect.objectContaining({ name: "Ada Lovelace" }));
119
-
120
- await user.click(screen.getByRole("button", { name: "Actions for Grace Hopper" }));
121
- await user.click(await screen.findByRole("menuitem", { name: "Remove from team" }));
122
- expect(onRemove).toHaveBeenCalledWith(expect.objectContaining({ name: "Grace Hopper" }));
123
- });
124
-
125
- it("states each status in words", () => {
126
- render(<AdminUsersBlock users={USERS} />);
127
- expect(screen.getByText("Suspended")).toBeInTheDocument();
128
- expect(screen.getByText("Invited")).toBeInTheDocument();
129
- });
130
-
131
- it("sorts by a column", async () => {
132
- const user = userEvent.setup();
133
- render(<AdminUsersBlock users={USERS} />);
134
-
135
- await user.click(screen.getByRole("button", { name: /User, not sorted/ }));
136
- await user.click(await screen.findByRole("menuitem", { name: "Sort descending" }));
137
-
138
- await waitFor(() => {
139
- expect(screen.getByRole("columnheader", { name: /User/ })).toHaveAttribute(
140
- "aria-sort",
141
- "descending",
142
- );
143
- });
144
- });
145
-
146
- it("invites people", async () => {
147
- const onInvite = vi.fn();
148
- const user = userEvent.setup();
149
- render(<AdminUsersBlock users={USERS} onInvite={onInvite} />);
150
-
151
- await user.click(screen.getByRole("button", { name: "Invite people" }));
152
- expect(onInvite).toHaveBeenCalledTimes(1);
153
- });
154
-
155
- it("has no accessibility violations", async () => {
156
- const { container } = render(<AdminUsersBlock users={USERS} />);
157
- await expectNoA11yViolations(container);
158
- });
159
- });
@@ -1,149 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
- import { useState } from "react";
3
-
4
- import { AiChatBlock, type ChatMessage } from "./ai-chat";
5
-
6
- /** Named so its type is nameable in declaration output (TS2883). */
7
- const withChatFrame: Decorator = (Story) => (
8
- <div className="h-[36rem] w-[42rem] max-w-full overflow-hidden rounded-xl border border-border">
9
- <Story />
10
- </div>
11
- );
12
-
13
- const MODELS = [
14
- { id: "fast", name: "Fast", description: "Lowest latency." },
15
- { id: "balanced", name: "Balanced", description: "The default." },
16
- { id: "deep", name: "Deep Research", description: "Multi-step, with citations." },
17
- ];
18
-
19
- const CONVERSATION: ChatMessage[] = [
20
- { id: "1", from: "user", content: "How do I make an overflowing table keyboard-scrollable?" },
21
- {
22
- id: "2",
23
- from: "assistant",
24
- content:
25
- 'Give the scrolling wrapper tabindex="0", a role of "region" and an accessible name. Without a focus stop, the columns past the edge are unreachable without a pointer.',
26
- reasoning:
27
- "The question is about keyboard access to overflowing tables.\n\nA scroll container is not focusable by default, so its content past the edge cannot be reached without a pointer. Adding a focus stop with a role and a name fixes both the reachability and the unlabelled-stop problem.",
28
- tools: [
29
- {
30
- id: "t1",
31
- name: "search_docs",
32
- status: "success",
33
- arguments: JSON.stringify({ query: "scrollable region keyboard", limit: 5 }, null, 2),
34
- result: JSON.stringify({ hits: 3, top: "ARIA Authoring Practices" }, null, 2),
35
- },
36
- ],
37
- sources: [
38
- {
39
- index: 1,
40
- title: "ARIA Authoring Practices — scrollable regions",
41
- origin: "w3.org",
42
- href: "#1",
43
- },
44
- { index: 2, title: "Accessible data tables", origin: "example.org", href: "#2" },
45
- ],
46
- tokens: 148,
47
- },
48
- ];
49
-
50
- const meta = {
51
- title: "Blocks/AI Chat",
52
- component: AiChatBlock,
53
- args: { messages: CONVERSATION, models: MODELS, model: "balanced" },
54
- parameters: { controls: { disable: true } },
55
- decorators: [withChatFrame],
56
- } satisfies Meta<typeof AiChatBlock>;
57
-
58
- export default meta;
59
- type Story = StoryObj<typeof meta>;
60
-
61
- export const Default: Story = {};
62
-
63
- export const Empty: Story = {
64
- args: { messages: [] },
65
- };
66
-
67
- export const Waiting: Story = {
68
- args: { messages: [CONVERSATION[0]!], waiting: true },
69
- };
70
-
71
- export const Streaming: Story = {
72
- args: {
73
- messages: [
74
- CONVERSATION[0]!,
75
- {
76
- id: "2",
77
- from: "assistant",
78
- content: 'Give the scrolling wrapper tabindex="0", a role',
79
- streaming: true,
80
- },
81
- ],
82
- busy: true,
83
- },
84
- };
85
-
86
- /** Send a message and watch it stream back. */
87
- export const Interactive: Story = {
88
- render: function Interactive() {
89
- const [messages, setMessages] = useState<ChatMessage[]>([]);
90
- const [waiting, setWaiting] = useState(false);
91
- const [busy, setBusy] = useState(false);
92
-
93
- function reply(text: string) {
94
- const answer = `You asked: “${text}”. In a real product this is where the response would stream in, one token at a time.`;
95
- let shown = 0;
96
-
97
- const id = String(Date.now());
98
- setWaiting(false);
99
- setBusy(true);
100
- setMessages((current) => [
101
- ...current,
102
- { id, from: "assistant", content: "", streaming: true },
103
- ]);
104
-
105
- const timer = window.setInterval(() => {
106
- shown += 2;
107
- const done = shown >= answer.length;
108
-
109
- setMessages((current) =>
110
- current.map((message) =>
111
- message.id === id
112
- ? {
113
- ...message,
114
- content: answer.slice(0, shown),
115
- streaming: !done,
116
- tokens: done ? 42 : undefined,
117
- }
118
- : message,
119
- ),
120
- );
121
-
122
- if (done) {
123
- window.clearInterval(timer);
124
- setBusy(false);
125
- }
126
- }, 30);
127
- }
128
-
129
- return (
130
- <AiChatBlock
131
- messages={messages}
132
- models={MODELS}
133
- model="balanced"
134
- busy={busy}
135
- waiting={waiting}
136
- onSend={(text) => {
137
- setMessages((current) => [
138
- ...current,
139
- { id: `u${String(current.length)}`, from: "user", content: text },
140
- ]);
141
- setWaiting(true);
142
- window.setTimeout(() => {
143
- reply(text);
144
- }, 700);
145
- }}
146
- />
147
- );
148
- },
149
- };
@@ -1,166 +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 { AiChatBlock, type ChatMessage } from "./ai-chat";
7
-
8
- const MESSAGES: ChatMessage[] = [
9
- { id: "1", from: "user", content: "How do I make an overflowing table keyboard-scrollable?" },
10
- {
11
- id: "2",
12
- from: "assistant",
13
- content: 'Give the scrolling wrapper tabindex="0", a role and an accessible name.',
14
- reasoning: "The container is not focusable by default, so its overflow is unreachable.",
15
- tools: [
16
- {
17
- id: "t1",
18
- name: "search_docs",
19
- status: "success",
20
- arguments: '{ "query": "scrollable region" }',
21
- result: '{ "hits": 3 }',
22
- },
23
- ],
24
- sources: [{ index: 1, title: "ARIA Authoring Practices", origin: "w3.org", href: "#1" }],
25
- tokens: 148,
26
- },
27
- ];
28
-
29
- describe("AiChatBlock", () => {
30
- it("shows an empty state before anything is said", () => {
31
- render(<AiChatBlock messages={[]} />);
32
- expect(screen.getByText("Start a conversation")).toBeInTheDocument();
33
- });
34
-
35
- it("renders the transcript as a list, never a live region", () => {
36
- const { container } = render(<AiChatBlock messages={MESSAGES} />);
37
-
38
- const list = container.querySelector("[data-slot='conversation-messages']");
39
- expect(list?.tagName).toBe("OL");
40
- // The decision the whole AI layer rests on: announcing streamed text token
41
- // by token is unusable with a screen reader.
42
- expect(list).not.toHaveAttribute("aria-live");
43
- });
44
-
45
- it("names the speaker of every turn in text", () => {
46
- render(<AiChatBlock messages={MESSAGES} />);
47
- expect(screen.getByText("You said:")).toBeInTheDocument();
48
- expect(screen.getByText("Assistant said:")).toBeInTheDocument();
49
- });
50
-
51
- it("announces state, not content", () => {
52
- const { container } = render(<AiChatBlock messages={MESSAGES} busy />);
53
-
54
- const status = container.querySelector("[data-slot='conversation-status']");
55
- expect(status).toHaveAttribute("aria-live", "polite");
56
- expect(status).toHaveTextContent("Generating response");
57
- expect(status).not.toHaveTextContent("tabindex");
58
- });
59
-
60
- it("distinguishes waiting from generating", () => {
61
- const { container } = render(<AiChatBlock messages={MESSAGES} waiting />);
62
- expect(container.querySelector("[data-slot='conversation-status']")).toHaveTextContent(
63
- "Waiting for a response",
64
- );
65
- expect(screen.getByText("Thinking")).toBeInTheDocument();
66
- });
67
-
68
- it("collapses reasoning and tool calls by default", () => {
69
- render(<AiChatBlock messages={MESSAGES} />);
70
-
71
- expect(screen.getByRole("button", { name: "Reasoning" })).toHaveAttribute(
72
- "aria-expanded",
73
- "false",
74
- );
75
- expect(screen.getByRole("button", { name: /search_docs/ })).toHaveAttribute(
76
- "aria-expanded",
77
- "false",
78
- );
79
- });
80
-
81
- it("names tool payloads per tool, so two open tools do not collide", async () => {
82
- const user = userEvent.setup();
83
- render(<AiChatBlock messages={MESSAGES} />);
84
-
85
- await user.click(screen.getByRole("button", { name: /search_docs/ }));
86
- expect(
87
- await screen.findByRole("region", { name: "search_docs arguments" }),
88
- ).toBeInTheDocument();
89
- expect(screen.getByRole("region", { name: "search_docs result" })).toBeInTheDocument();
90
- });
91
-
92
- it("lists sources behind a disclosure", async () => {
93
- const user = userEvent.setup();
94
- render(<AiChatBlock messages={MESSAGES} />);
95
-
96
- await user.click(screen.getByRole("button", { name: "1 source" }));
97
- expect(await screen.findByText("ARIA Authoring Practices")).toBeInTheDocument();
98
- });
99
-
100
- it("shows the token count for a finished response", () => {
101
- render(<AiChatBlock messages={MESSAGES} />);
102
- expect(screen.getByText("148 tokens")).toBeInTheDocument();
103
- });
104
-
105
- it("sends a message and clears the composer", async () => {
106
- const onSend = vi.fn();
107
- const user = userEvent.setup();
108
- render(<AiChatBlock messages={MESSAGES} onSend={onSend} />);
109
-
110
- const composer = screen.getByRole("textbox", { name: "Message" });
111
- await user.type(composer, "Thanks{Enter}");
112
-
113
- expect(onSend).toHaveBeenCalledWith("Thanks");
114
- await waitFor(() => {
115
- expect(composer).toHaveValue("");
116
- });
117
- });
118
-
119
- it("does not send an empty message", async () => {
120
- const onSend = vi.fn();
121
- const user = userEvent.setup();
122
- render(<AiChatBlock messages={MESSAGES} onSend={onSend} />);
123
-
124
- await user.type(screen.getByRole("textbox", { name: "Message" }), " {Enter}");
125
- expect(onSend).not.toHaveBeenCalled();
126
- });
127
-
128
- it("offers Stop while a response is in flight", async () => {
129
- const onStop = vi.fn();
130
- const user = userEvent.setup();
131
- render(<AiChatBlock messages={MESSAGES} busy onStop={onStop} />);
132
-
133
- await user.click(screen.getByRole("button", { name: "Stop generating" }));
134
- expect(onStop).toHaveBeenCalledTimes(1);
135
- });
136
-
137
- it("offers a model picker when models are given", async () => {
138
- const onModelChange = vi.fn();
139
- const user = userEvent.setup();
140
- render(
141
- <AiChatBlock
142
- messages={MESSAGES}
143
- model="fast"
144
- onModelChange={onModelChange}
145
- models={[
146
- { id: "fast", name: "Fast" },
147
- { id: "deep", name: "Deep" },
148
- ]}
149
- />,
150
- );
151
-
152
- await user.click(screen.getByRole("combobox", { name: "Model" }));
153
- await user.click(await screen.findByRole("option", { name: "Deep" }));
154
- expect(onModelChange).toHaveBeenCalledWith("deep");
155
- });
156
-
157
- it("hides the model picker when there are no models", () => {
158
- render(<AiChatBlock messages={MESSAGES} />);
159
- expect(screen.queryByRole("combobox", { name: "Model" })).not.toBeInTheDocument();
160
- });
161
-
162
- it("has no accessibility violations", async () => {
163
- const { container } = render(<AiChatBlock messages={MESSAGES} />);
164
- await expectNoA11yViolations(container);
165
- });
166
- });
@@ -1,106 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Button } from "@/components/button";
4
-
5
- import { DashboardBlock, type DashboardEvent, type DashboardStat } from "./dashboard";
6
-
7
- /** Named so its type is nameable in declaration output (TS2883). */
8
- const withPageWidth: Decorator = (Story) => (
9
- <div className="w-[64rem] max-w-full">
10
- <Story />
11
- </div>
12
- );
13
-
14
- const STATS: DashboardStat[] = [
15
- {
16
- id: "mrr",
17
- label: "Monthly revenue",
18
- value: "$48,120",
19
- change: 12.4,
20
- comparison: "on last month",
21
- },
22
- {
23
- id: "users",
24
- label: "Active users",
25
- value: "2,410",
26
- change: 4.2,
27
- comparison: "on last week",
28
- },
29
- {
30
- id: "churn",
31
- label: "Churn",
32
- value: "1.8%",
33
- change: 0.4,
34
- comparison: "on last month",
35
- higherIsBetter: false,
36
- },
37
- { id: "uptime", label: "Uptime", value: "99.98%" },
38
- ];
39
-
40
- const EVENTS: DashboardEvent[] = [
41
- {
42
- id: "1",
43
- title: "Deployed to production",
44
- detail: "Build 1420 · main",
45
- at: "2026-09-01T09:12:00Z",
46
- label: "12 minutes ago",
47
- tone: "success",
48
- },
49
- {
50
- id: "2",
51
- title: "Build finished with warnings",
52
- detail: "3 unused exports",
53
- at: "2026-09-01T09:04:00Z",
54
- label: "20 minutes ago",
55
- tone: "warning",
56
- },
57
- {
58
- id: "3",
59
- title: "Build failed",
60
- detail: "Type error in table.tsx",
61
- at: "2026-09-01T08:31:00Z",
62
- label: "53 minutes ago",
63
- tone: "destructive",
64
- },
65
- {
66
- id: "4",
67
- title: "Opened pull request #482",
68
- at: "2026-08-31T17:20:00Z",
69
- label: "Yesterday",
70
- },
71
- ];
72
-
73
- const meta = {
74
- title: "Blocks/Dashboard",
75
- component: DashboardBlock,
76
- args: { stats: STATS, events: EVENTS },
77
- parameters: { controls: { disable: true } },
78
- decorators: [withPageWidth],
79
- } satisfies Meta<typeof DashboardBlock>;
80
-
81
- export default meta;
82
- type Story = StoryObj<typeof meta>;
83
-
84
- /** Every change is stated in words — "up 12.4%", not a green triangle. */
85
- export const Default: Story = {};
86
-
87
- export const Loading: Story = {
88
- args: { loading: true },
89
- };
90
-
91
- export const MetricsOnly: Story = {
92
- args: { events: [] },
93
- };
94
-
95
- export const WithActions: Story = {
96
- args: {
97
- actions: (
98
- <div className="flex gap-2">
99
- <Button variant="outline" size="sm">
100
- Last 30 days
101
- </Button>
102
- <Button size="sm">Export</Button>
103
- </div>
104
- ),
105
- },
106
- };