@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,96 +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 { Response, ThinkingIndicator } from "./ai-response";
6
-
7
- describe("Response", () => {
8
- it("renders its text", () => {
9
- render(<Response>The capital of France is Paris.</Response>);
10
- expect(screen.getByText("The capital of France is Paris.")).toBeInTheDocument();
11
- });
12
-
13
- it("is NOT a live region", () => {
14
- // Announcing streamed text as it arrives interrupts a screen reader on
15
- // every token; state goes through ConversationStatus instead.
16
- const { container } = render(<Response streaming>Partial…</Response>);
17
- const response = container.querySelector("[data-slot='response']");
18
-
19
- expect(response).not.toHaveAttribute("aria-live");
20
- expect(response).not.toHaveAttribute("role");
21
- });
22
-
23
- it("shows a decorative caret while streaming", () => {
24
- const { container } = render(<Response streaming>Partial</Response>);
25
- const caret = container.querySelector("[data-slot='response-caret']");
26
-
27
- expect(caret).toBeInTheDocument();
28
- expect(caret).toHaveAttribute("aria-hidden", "true");
29
- });
30
-
31
- it("has no caret when it is not streaming", () => {
32
- const { container } = render(<Response>Done</Response>);
33
- expect(container.querySelector("[data-slot='response-caret']")).not.toBeInTheDocument();
34
- });
35
-
36
- it("marks the streaming state for styling", () => {
37
- const { container } = render(<Response streaming>Partial</Response>);
38
- expect(container.querySelector("[data-slot='response']")).toHaveAttribute(
39
- "data-streaming",
40
- "true",
41
- );
42
- });
43
-
44
- it("preserves paragraph whitespace", () => {
45
- const { container } = render(<Response>{"line one\nline two"}</Response>);
46
- expect(container.querySelector("[data-slot='response']")).toHaveClass(
47
- "whitespace-pre-wrap",
48
- );
49
- });
50
-
51
- it("renders markup a consumer has already rendered", () => {
52
- render(
53
- <Response>
54
- <p>First</p>
55
- <p>Second</p>
56
- </Response>,
57
- );
58
- expect(screen.getByText("First")).toBeInTheDocument();
59
- expect(screen.getByText("Second")).toBeInTheDocument();
60
- });
61
-
62
- it("lets a consumer className override a conflicting utility", () => {
63
- const { container } = render(<Response className="text-base">Hi</Response>);
64
- const response = container.querySelector("[data-slot='response']");
65
- expect(response).toHaveClass("text-base");
66
- expect(response).not.toHaveClass("text-sm");
67
- });
68
-
69
- it("has no accessibility violations", async () => {
70
- const { container } = render(<Response streaming>Streaming text…</Response>);
71
- await expectNoA11yViolations(container);
72
- });
73
- });
74
-
75
- describe("ThinkingIndicator", () => {
76
- it("is labelled, because it is the only thing on screen", () => {
77
- render(<ThinkingIndicator />);
78
- expect(screen.getByText("Thinking")).toBeInTheDocument();
79
- });
80
-
81
- it("accepts a custom label", () => {
82
- render(<ThinkingIndicator label="Searching the web" />);
83
- expect(screen.getByText("Searching the web")).toBeInTheDocument();
84
- });
85
-
86
- it("hides its dots from assistive technology", () => {
87
- const { container } = render(<ThinkingIndicator />);
88
- const dots = container.querySelectorAll("span[aria-hidden='true']");
89
- expect(dots).toHaveLength(3);
90
- });
91
-
92
- it("has no accessibility violations", async () => {
93
- const { container } = render(<ThinkingIndicator />);
94
- await expectNoA11yViolations(container);
95
- });
96
- });
@@ -1,93 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Response } from "@/components/ai-response";
4
-
5
- import { InlineCitation, Source, Sources, SourcesContent, SourcesTrigger } from "./ai-sources";
6
-
7
- /** Named so its type is nameable in declaration output (TS2883). */
8
- const withFixedWidth: Decorator = (Story) => (
9
- <div className="w-[32rem]">
10
- <Story />
11
- </div>
12
- );
13
-
14
- const meta = {
15
- title: "AI/Sources",
16
- component: Sources,
17
- parameters: { controls: { disable: true } },
18
- decorators: [withFixedWidth],
19
- } satisfies Meta<typeof Sources>;
20
-
21
- export default meta;
22
- type Story = StoryObj<typeof meta>;
23
-
24
- const SOURCES = [
25
- {
26
- index: 1,
27
- title: "ARIA Authoring Practices — Table pattern",
28
- origin: "w3.org",
29
- excerpt: "Scrollable regions should be focusable so keyboard users can scroll them.",
30
- },
31
- {
32
- index: 2,
33
- title: "Accessible data tables",
34
- origin: "example.org",
35
- excerpt: "Native table semantics convey row and column position without extra ARIA.",
36
- },
37
- ];
38
-
39
- /** Markers next to the claim, the list underneath saying what each one is. */
40
- export const Default: Story = {
41
- render: () => (
42
- <div className="grid gap-3">
43
- <Response>
44
- A scroll container needs an explicit focus stop to be reachable by keyboard
45
- <InlineCitation index={1} title={SOURCES[0]!.title} href="#1" />, and native table
46
- semantics carry position without extra ARIA
47
- <InlineCitation index={2} title={SOURCES[1]!.title} href="#2" />.
48
- </Response>
49
- <Sources>
50
- <SourcesTrigger count={SOURCES.length} />
51
- <SourcesContent>
52
- {SOURCES.map((source) => (
53
- <Source key={source.index} {...source} href={`#${String(source.index)}`} />
54
- ))}
55
- </SourcesContent>
56
- </Sources>
57
- </div>
58
- ),
59
- };
60
-
61
- export const Expanded: Story = {
62
- render: () => (
63
- <Sources defaultOpen>
64
- <SourcesTrigger count={SOURCES.length} />
65
- <SourcesContent>
66
- {SOURCES.map((source) => (
67
- <Source key={source.index} {...source} href={`#${String(source.index)}`} />
68
- ))}
69
- </SourcesContent>
70
- </Sources>
71
- ),
72
- };
73
-
74
- /** A marker with nowhere to go renders as text, not a link that does nothing. */
75
- export const WithoutLinks: Story = {
76
- render: () => (
77
- <Response>
78
- Internal policy requires two approvals
79
- <InlineCitation index={1} title="Engineering handbook, section 4" />.
80
- </Response>
81
- ),
82
- };
83
-
84
- export const SingleSource: Story = {
85
- render: () => (
86
- <Sources defaultOpen>
87
- <SourcesTrigger count={1} />
88
- <SourcesContent>
89
- <Source index={1} title="Engineering handbook" origin="internal" href="#1" />
90
- </SourcesContent>
91
- </Sources>
92
- ),
93
- };
@@ -1,102 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { describe, expect, it } from "vitest";
4
-
5
- import { expectNoA11yViolations } from "../../../test/a11y";
6
- import { InlineCitation, Source, Sources, SourcesContent, SourcesTrigger } from "./ai-sources";
7
-
8
- describe("InlineCitation", () => {
9
- it("carries the source title in its accessible name", () => {
10
- render(<InlineCitation index={1} title="Encyclopaedia entry on France" href="#1" />);
11
- // The visible text is a bare number, which conveys nothing on its own.
12
- expect(
13
- screen.getByRole("link", { name: "Source 1: Encyclopaedia entry on France" }),
14
- ).toBeInTheDocument();
15
- });
16
-
17
- it("renders as text, not a dead link, without an href", () => {
18
- render(<InlineCitation index={2} title="Internal document" />);
19
- expect(screen.queryByRole("link")).not.toBeInTheDocument();
20
- expect(screen.getByText("Source 2: Internal document")).toBeInTheDocument();
21
- });
22
-
23
- it("shows the marker number visually", () => {
24
- const { container } = render(<InlineCitation index={3} title="A source" />);
25
- expect(container.querySelector("[aria-hidden='true']")).toHaveTextContent("3");
26
- });
27
- });
28
-
29
- describe("Sources", () => {
30
- function Example() {
31
- return (
32
- <Sources>
33
- <SourcesTrigger count={2} />
34
- <SourcesContent>
35
- <Source index={1} title="Paris — overview" origin="example.org" href="#1" />
36
- <Source
37
- index={2}
38
- title="France factbook"
39
- origin="factbook.example"
40
- excerpt="Paris is the capital and most populous city."
41
- href="#2"
42
- />
43
- </SourcesContent>
44
- </Sources>
45
- );
46
- }
47
-
48
- it("is collapsed by default", () => {
49
- render(<Example />);
50
- expect(screen.getByRole("button", { name: "2 sources" })).toHaveAttribute(
51
- "aria-expanded",
52
- "false",
53
- );
54
- });
55
-
56
- it("pluralises the count", () => {
57
- render(
58
- <Sources>
59
- <SourcesTrigger count={1} />
60
- <SourcesContent>
61
- <Source index={1} title="Only one" href="#1" />
62
- </SourcesContent>
63
- </Sources>,
64
- );
65
- expect(screen.getByRole("button", { name: "1 source" })).toBeInTheDocument();
66
- });
67
-
68
- it("lists the sources in order, matching the marker numbers", async () => {
69
- const user = userEvent.setup();
70
- render(<Example />);
71
-
72
- await user.click(screen.getByRole("button", { name: "2 sources" }));
73
-
74
- const list = await screen.findByRole("list");
75
- expect(list.tagName).toBe("OL");
76
- expect(screen.getAllByRole("listitem")).toHaveLength(2);
77
- });
78
-
79
- it("shows origin and excerpt when given", async () => {
80
- const user = userEvent.setup();
81
- render(<Example />);
82
-
83
- await user.click(screen.getByRole("button", { name: "2 sources" }));
84
- expect(await screen.findByText("factbook.example")).toBeInTheDocument();
85
- expect(
86
- screen.getByText(/Paris is the capital and most populous city\./),
87
- ).toBeInTheDocument();
88
- });
89
-
90
- it("has no accessibility violations", async () => {
91
- const { container } = render(
92
- <Sources defaultOpen>
93
- <SourcesTrigger count={2} />
94
- <SourcesContent>
95
- <Source index={1} title="Paris — overview" origin="example.org" href="#1" />
96
- <Source index={2} title="France factbook" href="#2" />
97
- </SourcesContent>
98
- </Sources>,
99
- );
100
- await expectNoA11yViolations(container);
101
- });
102
- });
@@ -1,59 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { TokenCount, TokenUsage } from "./ai-token-usage";
4
-
5
- /** Named so its type is nameable in declaration output (TS2883). */
6
- const withFixedWidth: Decorator = (Story) => (
7
- <div className="w-72">
8
- <Story />
9
- </div>
10
- );
11
-
12
- const meta = {
13
- title: "AI/Token Usage",
14
- component: TokenUsage,
15
- args: { used: 42000, limit: 200000 },
16
- argTypes: {
17
- used: { control: { type: "range", min: 0, max: 240000, step: 1000 } },
18
- },
19
- decorators: [withFixedWidth],
20
- } satisfies Meta<typeof TokenUsage>;
21
-
22
- export default meta;
23
- type Story = StoryObj<typeof meta>;
24
-
25
- export const Default: Story = {};
26
-
27
- /** Running out of context is a cliff, not a slope — so the gauge warns early. */
28
- export const Thresholds: Story = {
29
- parameters: { controls: { disable: true } },
30
- render: () => (
31
- <div className="grid gap-5">
32
- <TokenUsage used={42000} limit={200000} />
33
- <TokenUsage used={175000} limit={200000} />
34
- <TokenUsage used={212000} limit={200000} />
35
- </div>
36
- ),
37
- };
38
-
39
- export const CustomFormat: Story = {
40
- parameters: { controls: { disable: true } },
41
- render: () => (
42
- <TokenUsage
43
- used={128000}
44
- limit={200000}
45
- label="Context"
46
- format={(value) => `${String(Math.round(value / 1000))}k`}
47
- />
48
- ),
49
- };
50
-
51
- export const InlineCount: Story = {
52
- parameters: { controls: { disable: true } },
53
- render: () => (
54
- <div className="flex items-center gap-3">
55
- <TokenCount value={1284} />
56
- <TokenCount value={412} label="output tokens" />
57
- </div>
58
- ),
59
- };
@@ -1,83 +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 { TokenCount, TokenUsage } from "./ai-token-usage";
6
-
7
- describe("TokenUsage", () => {
8
- it("shows the numbers, which are the actual content", () => {
9
- render(<TokenUsage used={12000} limit={200000} />);
10
- expect(screen.getByText("12,000 / 200,000")).toBeInTheDocument();
11
- });
12
-
13
- it("labels what is being measured", () => {
14
- render(<TokenUsage used={100} limit={1000} />);
15
- expect(screen.getByText("Context used")).toBeInTheDocument();
16
- });
17
-
18
- it("accepts a custom label and formatter", () => {
19
- render(
20
- <TokenUsage
21
- used={1500}
22
- limit={4000}
23
- label="Budget"
24
- format={(value) => `${String(Math.round(value / 1000))}k`}
25
- />,
26
- );
27
- expect(screen.getByText("Budget")).toBeInTheDocument();
28
- expect(screen.getByText("2k / 4k")).toBeInTheDocument();
29
- });
30
-
31
- it("hides the bar, since it only restates the numbers", () => {
32
- const { container } = render(<TokenUsage used={100} limit={1000} />);
33
- // Not a second progressbar to read past.
34
- expect(container.querySelector("[aria-hidden='true']")).toBeInTheDocument();
35
- expect(screen.queryByRole("progressbar")).not.toBeInTheDocument();
36
- });
37
-
38
- it("marks the warning state past the threshold", () => {
39
- const { container } = render(<TokenUsage used={900} limit={1000} />);
40
- expect(container.firstElementChild).toHaveAttribute("data-warning", "true");
41
- });
42
-
43
- it("does not warn below the threshold", () => {
44
- const { container } = render(<TokenUsage used={100} limit={1000} />);
45
- expect(container.firstElementChild).not.toHaveAttribute("data-warning");
46
- });
47
-
48
- it("marks going over the limit", () => {
49
- const { container } = render(<TokenUsage used={1200} limit={1000} />);
50
- expect(container.firstElementChild).toHaveAttribute("data-over", "true");
51
- // The figures stay readable even when over.
52
- expect(screen.getByText("1,200 / 1,000")).toBeInTheDocument();
53
- });
54
-
55
- it("clamps the bar rather than overflowing it", () => {
56
- const { container } = render(<TokenUsage used={5000} limit={1000} />);
57
- const fill = container.querySelector("[aria-hidden='true'] > div");
58
- expect(fill).toHaveStyle({ width: "100%" });
59
- });
60
-
61
- it("survives a zero limit without dividing by it", () => {
62
- const { container } = render(<TokenUsage used={10} limit={0} />);
63
- const fill = container.querySelector("[aria-hidden='true'] > div");
64
- expect(fill).toHaveStyle({ width: "0%" });
65
- });
66
-
67
- it("has no accessibility violations", async () => {
68
- const { container } = render(<TokenUsage used={180000} limit={200000} />);
69
- await expectNoA11yViolations(container);
70
- });
71
- });
72
-
73
- describe("TokenCount", () => {
74
- it("formats the count", () => {
75
- render(<TokenCount value={12345} />);
76
- expect(screen.getByText("12,345 tokens")).toBeInTheDocument();
77
- });
78
-
79
- it("accepts a custom unit", () => {
80
- render(<TokenCount value={42} label="output tokens" />);
81
- expect(screen.getByText("42 output tokens")).toBeInTheDocument();
82
- });
83
- });
@@ -1,96 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Tool, ToolContent, ToolHeader, ToolPayload, ToolSection } from "./ai-tool";
4
-
5
- /** Named so its type is nameable in declaration output (TS2883). */
6
- const withFixedWidth: Decorator = (Story) => (
7
- <div className="w-[32rem]">
8
- <Story />
9
- </div>
10
- );
11
-
12
- const meta = {
13
- title: "AI/Tool Call",
14
- component: Tool,
15
- args: { status: "success" },
16
- argTypes: {
17
- status: { control: "inline-radio", options: ["pending", "running", "success", "error"] },
18
- },
19
- decorators: [withFixedWidth],
20
- } satisfies Meta<typeof Tool>;
21
-
22
- export default meta;
23
- type Story = StoryObj<typeof meta>;
24
-
25
- export const Default: Story = {
26
- render: (args) => (
27
- <Tool {...args}>
28
- <ToolHeader name="search_web" status={args.status ?? "success"} />
29
- <ToolContent>
30
- <ToolSection label="Arguments">
31
- <ToolPayload label="Arguments">
32
- {JSON.stringify({ query: "accessible data tables", limit: 5 }, null, 2)}
33
- </ToolPayload>
34
- </ToolSection>
35
- <ToolSection label="Result">
36
- <ToolPayload label="Result">
37
- {JSON.stringify(
38
- { results: [{ title: "ARIA authoring practices", url: "https://example.org" }] },
39
- null,
40
- 2,
41
- )}
42
- </ToolPayload>
43
- </ToolSection>
44
- </ToolContent>
45
- </Tool>
46
- ),
47
- };
48
-
49
- /** Status is always a word, never only a colour. */
50
- export const Statuses: Story = {
51
- parameters: { controls: { disable: true } },
52
- render: () => (
53
- <div className="grid gap-2">
54
- {(["pending", "running", "success", "error"] as const).map((status) => (
55
- <Tool key={status} status={status}>
56
- <ToolHeader name={`tool_${status}`} status={status} />
57
- <ToolContent>
58
- <ToolSection label="Result">
59
- <ToolPayload label="Result">{`Status: ${status}`}</ToolPayload>
60
- </ToolSection>
61
- </ToolContent>
62
- </Tool>
63
- ))}
64
- </div>
65
- ),
66
- };
67
-
68
- export const CustomStatusWording: Story = {
69
- parameters: { controls: { disable: true } },
70
- render: () => (
71
- <Tool status="running">
72
- <ToolHeader name="crawl_site" status="running" statusLabel="Fetching page 12 of 40" />
73
- <ToolContent>
74
- <ToolSection label="Progress">
75
- <ToolPayload label="Progress">Crawling https://example.org…</ToolPayload>
76
- </ToolSection>
77
- </ToolContent>
78
- </Tool>
79
- ),
80
- };
81
-
82
- export const Failed: Story = {
83
- parameters: { controls: { disable: true } },
84
- render: () => (
85
- <Tool status="error" defaultOpen>
86
- <ToolHeader name="read_file" status="error" />
87
- <ToolContent>
88
- <ToolSection label="Error">
89
- <ToolPayload label="Error">
90
- {"ENOENT: no such file or directory, open '/app/missing.ts'"}
91
- </ToolPayload>
92
- </ToolSection>
93
- </ToolContent>
94
- </Tool>
95
- ),
96
- };
@@ -1,137 +0,0 @@
1
- import { render, screen, waitFor } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { describe, expect, it } from "vitest";
4
-
5
- import { expectNoA11yViolations } from "../../../test/a11y";
6
- import {
7
- Tool,
8
- ToolContent,
9
- ToolHeader,
10
- ToolPayload,
11
- ToolSection,
12
- type ToolStatus,
13
- } from "./ai-tool";
14
-
15
- function Example({ status = "success" }: { status?: ToolStatus } = {}) {
16
- return (
17
- <Tool status={status}>
18
- <ToolHeader name="search_web" status={status} />
19
- <ToolContent>
20
- <ToolSection label="Arguments">
21
- <ToolPayload label="Arguments">{'{ "query": "capital of France" }'}</ToolPayload>
22
- </ToolSection>
23
- <ToolSection label="Result">
24
- <ToolPayload label="Result">{'{ "answer": "Paris" }'}</ToolPayload>
25
- </ToolSection>
26
- </ToolContent>
27
- </Tool>
28
- );
29
- }
30
-
31
- describe("Tool", () => {
32
- it("is collapsed by default, because provenance is not the answer", () => {
33
- render(<Example />);
34
- expect(screen.getByRole("button", { name: /search_web/ })).toHaveAttribute(
35
- "aria-expanded",
36
- "false",
37
- );
38
- expect(screen.queryByText(/capital of France/)).not.toBeInTheDocument();
39
- });
40
-
41
- it("opens on click", async () => {
42
- const user = userEvent.setup();
43
- render(<Example />);
44
-
45
- await user.click(screen.getByRole("button", { name: /search_web/ }));
46
- expect(await screen.findByText(/capital of France/)).toBeInTheDocument();
47
- });
48
-
49
- it("opens from the keyboard", async () => {
50
- const user = userEvent.setup();
51
- render(<Example />);
52
-
53
- await user.tab();
54
- await user.keyboard("{Enter}");
55
- await waitFor(() => {
56
- expect(screen.getByRole("button", { name: /search_web/ })).toHaveAttribute(
57
- "aria-expanded",
58
- "true",
59
- );
60
- });
61
- });
62
-
63
- it.each([
64
- ["pending", "Queued"],
65
- ["running", "Running"],
66
- ["success", "Completed"],
67
- ["error", "Failed"],
68
- ] as const)("states the %s status in words, not only in colour", (status, label) => {
69
- render(<Example status={status} />);
70
- // A coloured dot says nothing to a screen reader, and nothing to anyone who
71
- // cannot distinguish the colours.
72
- expect(screen.getByRole("button", { name: new RegExp(label) })).toBeInTheDocument();
73
- });
74
-
75
- it("accepts custom status wording", () => {
76
- render(
77
- <Tool status="running">
78
- <ToolHeader name="fetch" status="running" statusLabel="Fetching page 2 of 5" />
79
- <ToolContent>Body</ToolContent>
80
- </Tool>,
81
- );
82
- expect(screen.getByText("Fetching page 2 of 5")).toBeInTheDocument();
83
- });
84
-
85
- it("marks the status for styling", () => {
86
- const { container } = render(<Example status="error" />);
87
- expect(container.querySelector("[data-slot='tool']")).toHaveAttribute(
88
- "data-status",
89
- "error",
90
- );
91
- });
92
-
93
- it("makes payloads focusable named regions", async () => {
94
- const user = userEvent.setup();
95
- render(<Example />);
96
-
97
- await user.click(screen.getByRole("button", { name: /search_web/ }));
98
-
99
- // JSON scrolls, and a scroll box that cannot take focus is unreachable by
100
- // keyboard.
101
- const payload = await screen.findByRole("region", { name: "Arguments" });
102
- expect(payload).toHaveAttribute("tabindex", "0");
103
- });
104
-
105
- it("labels each section", async () => {
106
- const user = userEvent.setup();
107
- render(<Example />);
108
-
109
- await user.click(screen.getByRole("button", { name: /search_web/ }));
110
- expect(await screen.findByText("Arguments")).toBeInTheDocument();
111
- expect(screen.getByText("Result")).toBeInTheDocument();
112
- });
113
-
114
- it("can be opened by default", () => {
115
- render(
116
- <Tool status="success" defaultOpen>
117
- <ToolHeader name="read_file" status="success" />
118
- <ToolContent>Contents</ToolContent>
119
- </Tool>,
120
- );
121
- expect(screen.getByText("Contents")).toBeInTheDocument();
122
- });
123
-
124
- it("has no accessibility violations when open", async () => {
125
- const { container } = render(
126
- <Tool status="success" defaultOpen>
127
- <ToolHeader name="search_web" status="success" />
128
- <ToolContent>
129
- <ToolSection label="Result">
130
- <ToolPayload label="Result">{'{ "answer": "Paris" }'}</ToolPayload>
131
- </ToolSection>
132
- </ToolContent>
133
- </Tool>,
134
- );
135
- await expectNoA11yViolations(container);
136
- });
137
- });