@dowel-ui/react 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ai-agent-status/ai-agent-status.d.ts +1 -1
- package/dist/components/ai-tool/ai-tool.d.ts +1 -1
- package/dist/components/alert/alert.d.ts +1 -1
- package/dist/components/avatar/avatar.d.ts +1 -1
- package/dist/components/badge/badge.d.ts +1 -1
- package/dist/components/button/button.d.ts +1 -1
- package/dist/components/progress/progress.d.ts +1 -1
- package/dist/components/spinner/spinner.d.ts +1 -1
- package/dist/components/toast/toast.d.ts +1 -1
- package/package.json +7 -4
- package/src/blocks/admin-users/admin-users.stories.tsx +0 -83
- package/src/blocks/admin-users/admin-users.test.tsx +0 -159
- package/src/blocks/ai-chat/ai-chat.stories.tsx +0 -149
- package/src/blocks/ai-chat/ai-chat.test.tsx +0 -166
- package/src/blocks/dashboard/dashboard.stories.tsx +0 -106
- package/src/blocks/dashboard/dashboard.test.tsx +0 -94
- package/src/blocks/forgot-password/forgot-password.stories.tsx +0 -41
- package/src/blocks/forgot-password/forgot-password.test.tsx +0 -62
- package/src/blocks/login/login.stories.tsx +0 -46
- package/src/blocks/login/login.test.tsx +0 -100
- package/src/blocks/pricing/pricing.stories.tsx +0 -62
- package/src/blocks/pricing/pricing.test.tsx +0 -101
- package/src/blocks/settings/settings.stories.tsx +0 -94
- package/src/blocks/settings/settings.test.tsx +0 -167
- package/src/blocks/signup/signup.stories.tsx +0 -27
- package/src/blocks/signup/signup.test.tsx +0 -97
- package/src/components/accordion/accordion.stories.tsx +0 -90
- package/src/components/accordion/accordion.test.tsx +0 -156
- package/src/components/activity-feed/activity-feed.stories.tsx +0 -163
- package/src/components/activity-feed/activity-feed.test.tsx +0 -125
- package/src/components/ai-agent-status/ai-agent-status.stories.tsx +0 -86
- package/src/components/ai-agent-status/ai-agent-status.test.tsx +0 -66
- package/src/components/ai-conversation/ai-conversation.stories.tsx +0 -126
- package/src/components/ai-conversation/ai-conversation.test.tsx +0 -144
- package/src/components/ai-message/ai-message.stories.tsx +0 -106
- package/src/components/ai-message/ai-message.test.tsx +0 -160
- package/src/components/ai-model-selector/ai-model-selector.stories.tsx +0 -66
- package/src/components/ai-model-selector/ai-model-selector.test.tsx +0 -120
- package/src/components/ai-prompt-input/ai-prompt-input.stories.tsx +0 -145
- package/src/components/ai-prompt-input/ai-prompt-input.test.tsx +0 -273
- package/src/components/ai-reasoning/ai-reasoning.stories.tsx +0 -60
- package/src/components/ai-reasoning/ai-reasoning.test.tsx +0 -95
- package/src/components/ai-response/ai-response.stories.tsx +0 -75
- package/src/components/ai-response/ai-response.test.tsx +0 -96
- package/src/components/ai-sources/ai-sources.stories.tsx +0 -93
- package/src/components/ai-sources/ai-sources.test.tsx +0 -102
- package/src/components/ai-token-usage/ai-token-usage.stories.tsx +0 -59
- package/src/components/ai-token-usage/ai-token-usage.test.tsx +0 -83
- package/src/components/ai-tool/ai-tool.stories.tsx +0 -96
- package/src/components/ai-tool/ai-tool.test.tsx +0 -137
- package/src/components/alert/alert.stories.tsx +0 -101
- package/src/components/alert/alert.test.tsx +0 -94
- package/src/components/avatar/avatar.stories.tsx +0 -58
- package/src/components/avatar/avatar.test.tsx +0 -64
- package/src/components/badge/badge.stories.tsx +0 -68
- package/src/components/badge/badge.test.tsx +0 -61
- package/src/components/button/button.stories.tsx +0 -128
- package/src/components/button/button.test.tsx +0 -224
- package/src/components/calendar/calendar.stories.tsx +0 -105
- package/src/components/calendar/calendar.test.tsx +0 -255
- package/src/components/card/card.stories.tsx +0 -61
- package/src/components/card/card.test.tsx +0 -66
- package/src/components/checkbox/checkbox.stories.tsx +0 -124
- package/src/components/checkbox/checkbox.test.tsx +0 -121
- package/src/components/code-block/code-block.stories.tsx +0 -62
- package/src/components/code-block/code-block.test.tsx +0 -139
- package/src/components/combobox/combobox.stories.tsx +0 -171
- package/src/components/combobox/combobox.test.tsx +0 -429
- package/src/components/command/command.stories.tsx +0 -152
- package/src/components/command/command.test.tsx +0 -380
- package/src/components/data-table/data-table.stories.tsx +0 -151
- package/src/components/data-table/data-table.test.tsx +0 -288
- package/src/components/date-picker/date-picker.stories.tsx +0 -103
- package/src/components/date-picker/date-picker.test.tsx +0 -205
- package/src/components/dialog/dialog.stories.tsx +0 -140
- package/src/components/dialog/dialog.test.tsx +0 -186
- package/src/components/drawer/drawer.stories.tsx +0 -111
- package/src/components/drawer/drawer.test.tsx +0 -187
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +0 -128
- package/src/components/dropdown-menu/dropdown-menu.test.tsx +0 -321
- package/src/components/empty-state/empty-state.stories.tsx +0 -70
- package/src/components/empty-state/empty-state.test.tsx +0 -97
- package/src/components/form/form.stories.tsx +0 -144
- package/src/components/form/form.test.tsx +0 -203
- package/src/components/input/input.stories.tsx +0 -84
- package/src/components/input/input.test.tsx +0 -108
- package/src/components/label/label.stories.tsx +0 -51
- package/src/components/label/label.test.tsx +0 -55
- package/src/components/pagination/pagination.stories.tsx +0 -129
- package/src/components/pagination/pagination.test.tsx +0 -119
- package/src/components/popover/popover.stories.tsx +0 -84
- package/src/components/popover/popover.test.tsx +0 -163
- package/src/components/progress/progress.stories.tsx +0 -90
- package/src/components/progress/progress.test.tsx +0 -78
- package/src/components/radio-group/radio-group.stories.tsx +0 -102
- package/src/components/radio-group/radio-group.test.tsx +0 -152
- package/src/components/select/select.stories.tsx +0 -137
- package/src/components/select/select.test.tsx +0 -209
- package/src/components/separator/separator.stories.tsx +0 -42
- package/src/components/separator/separator.test.tsx +0 -46
- package/src/components/sheet/sheet.stories.tsx +0 -107
- package/src/components/sheet/sheet.test.tsx +0 -152
- package/src/components/skeleton/skeleton.stories.tsx +0 -46
- package/src/components/skeleton/skeleton.test.tsx +0 -57
- package/src/components/slider/slider.stories.tsx +0 -87
- package/src/components/slider/slider.test.tsx +0 -148
- package/src/components/spinner/spinner.stories.tsx +0 -47
- package/src/components/spinner/spinner.test.tsx +0 -41
- package/src/components/switch/switch.stories.tsx +0 -70
- package/src/components/switch/switch.test.tsx +0 -101
- package/src/components/table/table.stories.tsx +0 -131
- package/src/components/table/table.test.tsx +0 -153
- package/src/components/tabs/tabs.stories.tsx +0 -139
- package/src/components/tabs/tabs.test.tsx +0 -148
- package/src/components/toast/toast.stories.tsx +0 -167
- package/src/components/toast/toast.test.tsx +0 -306
- package/src/components/tooltip/tooltip.stories.tsx +0 -89
- package/src/components/tooltip/tooltip.test.tsx +0 -120
- package/src/lib/utils.test.ts +0 -32
- package/src/registry/meta.test.ts +0 -234
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
import { CircleCheck, GitBranch, GitPullRequest, Rocket, TriangleAlert } from "lucide-react";
|
|
3
|
-
|
|
4
|
-
import { Avatar, AvatarFallback } from "@/components/avatar";
|
|
5
|
-
import { Badge } from "@/components/badge";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
ActivityContent,
|
|
9
|
-
ActivityDescription,
|
|
10
|
-
ActivityFeed,
|
|
11
|
-
ActivityIndicator,
|
|
12
|
-
ActivityItem,
|
|
13
|
-
ActivityTime,
|
|
14
|
-
ActivityTitle,
|
|
15
|
-
} from "./activity-feed";
|
|
16
|
-
|
|
17
|
-
/** Named so its type is nameable in declaration output (TS2883). */
|
|
18
|
-
const withFixedWidth: Decorator = (Story) => (
|
|
19
|
-
<div className="w-96">
|
|
20
|
-
<Story />
|
|
21
|
-
</div>
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
const meta = {
|
|
25
|
-
title: "Data/Activity Feed",
|
|
26
|
-
component: ActivityFeed,
|
|
27
|
-
parameters: { controls: { disable: true } },
|
|
28
|
-
decorators: [withFixedWidth],
|
|
29
|
-
} satisfies Meta<typeof ActivityFeed>;
|
|
30
|
-
|
|
31
|
-
export default meta;
|
|
32
|
-
type Story = StoryObj<typeof meta>;
|
|
33
|
-
|
|
34
|
-
const EVENTS = [
|
|
35
|
-
{
|
|
36
|
-
id: "1",
|
|
37
|
-
icon: Rocket,
|
|
38
|
-
title: "Deployed to production",
|
|
39
|
-
detail: "Build 1420 · main",
|
|
40
|
-
at: "2026-09-01T09:12:00Z",
|
|
41
|
-
label: "12 minutes ago",
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: "2",
|
|
45
|
-
icon: CircleCheck,
|
|
46
|
-
title: "All checks passed",
|
|
47
|
-
detail: "24 tests, 0 failures",
|
|
48
|
-
at: "2026-09-01T09:04:00Z",
|
|
49
|
-
label: "20 minutes ago",
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
id: "3",
|
|
53
|
-
icon: GitPullRequest,
|
|
54
|
-
title: "Opened pull request #482",
|
|
55
|
-
detail: "Add keyboard shortcuts to the command palette",
|
|
56
|
-
at: "2026-08-31T17:20:00Z",
|
|
57
|
-
label: "Yesterday",
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
id: "4",
|
|
61
|
-
icon: GitBranch,
|
|
62
|
-
title: "Created branch feat/shortcuts",
|
|
63
|
-
detail: "From main",
|
|
64
|
-
at: "2026-08-31T16:02:00Z",
|
|
65
|
-
label: "Yesterday",
|
|
66
|
-
},
|
|
67
|
-
];
|
|
68
|
-
|
|
69
|
-
export const Default: Story = {
|
|
70
|
-
render: () => (
|
|
71
|
-
<ActivityFeed>
|
|
72
|
-
{EVENTS.map((event, index) => {
|
|
73
|
-
const Icon = event.icon;
|
|
74
|
-
return (
|
|
75
|
-
<ActivityItem key={event.id} last={index === EVENTS.length - 1}>
|
|
76
|
-
<ActivityIndicator>
|
|
77
|
-
<Icon />
|
|
78
|
-
</ActivityIndicator>
|
|
79
|
-
<ActivityContent>
|
|
80
|
-
<ActivityTitle>{event.title}</ActivityTitle>
|
|
81
|
-
<ActivityDescription>{event.detail}</ActivityDescription>
|
|
82
|
-
<ActivityTime dateTime={event.at}>{event.label}</ActivityTime>
|
|
83
|
-
</ActivityContent>
|
|
84
|
-
</ActivityItem>
|
|
85
|
-
);
|
|
86
|
-
})}
|
|
87
|
-
</ActivityFeed>
|
|
88
|
-
),
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
/** Avatars as the rail markers, via asChild. */
|
|
92
|
-
export const WithAvatars: Story = {
|
|
93
|
-
render: () => (
|
|
94
|
-
<ActivityFeed>
|
|
95
|
-
{[
|
|
96
|
-
{
|
|
97
|
-
id: "1",
|
|
98
|
-
who: "AL",
|
|
99
|
-
title: "Ada Lovelace approved the pull request",
|
|
100
|
-
at: "2026-09-01T09:12:00Z",
|
|
101
|
-
label: "12 minutes ago",
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
id: "2",
|
|
105
|
-
who: "GH",
|
|
106
|
-
title: "Grace Hopper left a comment",
|
|
107
|
-
at: "2026-09-01T08:40:00Z",
|
|
108
|
-
label: "44 minutes ago",
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
id: "3",
|
|
112
|
-
who: "KJ",
|
|
113
|
-
title: "Katherine Johnson requested changes",
|
|
114
|
-
at: "2026-08-31T18:05:00Z",
|
|
115
|
-
label: "Yesterday",
|
|
116
|
-
},
|
|
117
|
-
].map((event, index, all) => (
|
|
118
|
-
<ActivityItem key={event.id} last={index === all.length - 1}>
|
|
119
|
-
<ActivityIndicator asChild>
|
|
120
|
-
<Avatar size="xs">
|
|
121
|
-
<AvatarFallback>{event.who}</AvatarFallback>
|
|
122
|
-
</Avatar>
|
|
123
|
-
</ActivityIndicator>
|
|
124
|
-
<ActivityContent>
|
|
125
|
-
<ActivityTitle>{event.title}</ActivityTitle>
|
|
126
|
-
<ActivityTime dateTime={event.at}>{event.label}</ActivityTime>
|
|
127
|
-
</ActivityContent>
|
|
128
|
-
</ActivityItem>
|
|
129
|
-
))}
|
|
130
|
-
</ActivityFeed>
|
|
131
|
-
),
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
export const WithStatus: Story = {
|
|
135
|
-
render: () => (
|
|
136
|
-
<ActivityFeed>
|
|
137
|
-
<ActivityItem>
|
|
138
|
-
<ActivityIndicator className="border-success/40 text-success">
|
|
139
|
-
<CircleCheck />
|
|
140
|
-
</ActivityIndicator>
|
|
141
|
-
<ActivityContent>
|
|
142
|
-
<ActivityTitle className="flex items-center gap-2">
|
|
143
|
-
Deployment succeeded
|
|
144
|
-
<Badge variant="success" size="sm">
|
|
145
|
-
Live
|
|
146
|
-
</Badge>
|
|
147
|
-
</ActivityTitle>
|
|
148
|
-
<ActivityTime dateTime="2026-09-01T09:12:00Z">12 minutes ago</ActivityTime>
|
|
149
|
-
</ActivityContent>
|
|
150
|
-
</ActivityItem>
|
|
151
|
-
<ActivityItem last>
|
|
152
|
-
<ActivityIndicator className="border-warning/40 text-warning">
|
|
153
|
-
<TriangleAlert />
|
|
154
|
-
</ActivityIndicator>
|
|
155
|
-
<ActivityContent>
|
|
156
|
-
<ActivityTitle>Build finished with warnings</ActivityTitle>
|
|
157
|
-
<ActivityDescription>3 unused exports detected.</ActivityDescription>
|
|
158
|
-
<ActivityTime dateTime="2026-09-01T09:05:00Z">19 minutes ago</ActivityTime>
|
|
159
|
-
</ActivityContent>
|
|
160
|
-
</ActivityItem>
|
|
161
|
-
</ActivityFeed>
|
|
162
|
-
),
|
|
163
|
-
};
|
|
@@ -1,125 +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 {
|
|
6
|
-
ActivityContent,
|
|
7
|
-
ActivityDescription,
|
|
8
|
-
ActivityFeed,
|
|
9
|
-
ActivityIndicator,
|
|
10
|
-
ActivityItem,
|
|
11
|
-
ActivityTime,
|
|
12
|
-
ActivityTitle,
|
|
13
|
-
} from "./activity-feed";
|
|
14
|
-
|
|
15
|
-
const EVENTS = [
|
|
16
|
-
{ id: "1", title: "Deployed to production", at: "2026-09-01T09:00:00Z" },
|
|
17
|
-
{ id: "2", title: "Opened a pull request", at: "2026-08-31T17:20:00Z" },
|
|
18
|
-
{ id: "3", title: "Created the project", at: "2026-08-30T11:05:00Z" },
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
function Example() {
|
|
22
|
-
return (
|
|
23
|
-
<ActivityFeed>
|
|
24
|
-
{EVENTS.map((event, index) => (
|
|
25
|
-
<ActivityItem key={event.id} last={index === EVENTS.length - 1}>
|
|
26
|
-
<ActivityIndicator>
|
|
27
|
-
<svg />
|
|
28
|
-
</ActivityIndicator>
|
|
29
|
-
<ActivityContent>
|
|
30
|
-
<ActivityTitle>{event.title}</ActivityTitle>
|
|
31
|
-
<ActivityTime dateTime={event.at}>Recently</ActivityTime>
|
|
32
|
-
</ActivityContent>
|
|
33
|
-
</ActivityItem>
|
|
34
|
-
))}
|
|
35
|
-
</ActivityFeed>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
describe("ActivityFeed", () => {
|
|
40
|
-
it("renders an ordered list, so position and count are announced", () => {
|
|
41
|
-
render(<Example />);
|
|
42
|
-
expect(screen.getByRole("list").tagName).toBe("OL");
|
|
43
|
-
expect(screen.getAllByRole("listitem")).toHaveLength(3);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("renders each event", () => {
|
|
47
|
-
render(<Example />);
|
|
48
|
-
for (const event of EVENTS) {
|
|
49
|
-
expect(screen.getByText(event.title)).toBeInTheDocument();
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("hides the indicator from assistive technology", () => {
|
|
54
|
-
const { container } = render(<Example />);
|
|
55
|
-
for (const indicator of container.querySelectorAll("[data-slot='activity-indicator']")) {
|
|
56
|
-
expect(indicator).toHaveAttribute("aria-hidden", "true");
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("carries a machine-readable timestamp", () => {
|
|
61
|
-
const { container } = render(<Example />);
|
|
62
|
-
const time = container.querySelector("time");
|
|
63
|
-
expect(time).toHaveAttribute("datetime", "2026-09-01T09:00:00Z");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("marks the last item so no rail is drawn past it", () => {
|
|
67
|
-
const { container } = render(<Example />);
|
|
68
|
-
const items = container.querySelectorAll("[data-slot='activity-item']");
|
|
69
|
-
expect(items[items.length - 1]).toHaveAttribute("data-last", "true");
|
|
70
|
-
expect(items[0]).not.toHaveAttribute("data-last");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("renders the indicator as another element with asChild", () => {
|
|
74
|
-
render(
|
|
75
|
-
<ActivityFeed>
|
|
76
|
-
<ActivityItem>
|
|
77
|
-
<ActivityIndicator asChild>
|
|
78
|
-
<span data-testid="custom">A</span>
|
|
79
|
-
</ActivityIndicator>
|
|
80
|
-
<ActivityContent>
|
|
81
|
-
<ActivityTitle>Event</ActivityTitle>
|
|
82
|
-
</ActivityContent>
|
|
83
|
-
</ActivityItem>
|
|
84
|
-
</ActivityFeed>,
|
|
85
|
-
);
|
|
86
|
-
expect(screen.getByTestId("custom")).toHaveAttribute("aria-hidden", "true");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("renders a description when given one", () => {
|
|
90
|
-
render(
|
|
91
|
-
<ActivityFeed>
|
|
92
|
-
<ActivityItem last>
|
|
93
|
-
<ActivityIndicator />
|
|
94
|
-
<ActivityContent>
|
|
95
|
-
<ActivityTitle>Deployed</ActivityTitle>
|
|
96
|
-
<ActivityDescription>Build 1420 is live.</ActivityDescription>
|
|
97
|
-
</ActivityContent>
|
|
98
|
-
</ActivityItem>
|
|
99
|
-
</ActivityFeed>,
|
|
100
|
-
);
|
|
101
|
-
expect(screen.getByText("Build 1420 is live.")).toBeInTheDocument();
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("lets a consumer className override a conflicting utility", () => {
|
|
105
|
-
const { container } = render(
|
|
106
|
-
<ActivityFeed className="gap-4">
|
|
107
|
-
<ActivityItem last className="pb-0">
|
|
108
|
-
<ActivityIndicator />
|
|
109
|
-
<ActivityContent>
|
|
110
|
-
<ActivityTitle>Event</ActivityTitle>
|
|
111
|
-
</ActivityContent>
|
|
112
|
-
</ActivityItem>
|
|
113
|
-
</ActivityFeed>,
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
const item = container.querySelector("[data-slot='activity-item']");
|
|
117
|
-
expect(item).toHaveClass("pb-0");
|
|
118
|
-
expect(item).not.toHaveClass("pb-6");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("has no accessibility violations", async () => {
|
|
122
|
-
const { container } = render(<Example />);
|
|
123
|
-
await expectNoA11yViolations(container);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
ActivityContent,
|
|
5
|
-
ActivityFeed,
|
|
6
|
-
ActivityIndicator,
|
|
7
|
-
ActivityItem,
|
|
8
|
-
ActivityTime,
|
|
9
|
-
ActivityTitle,
|
|
10
|
-
} from "@/components/activity-feed";
|
|
11
|
-
|
|
12
|
-
import { AgentStatus } from "./ai-agent-status";
|
|
13
|
-
|
|
14
|
-
const meta = {
|
|
15
|
-
title: "AI/Agent Status",
|
|
16
|
-
component: AgentStatus,
|
|
17
|
-
args: { state: "working", live: false },
|
|
18
|
-
argTypes: {
|
|
19
|
-
state: {
|
|
20
|
-
control: "select",
|
|
21
|
-
options: ["idle", "thinking", "working", "waiting", "done", "error"],
|
|
22
|
-
},
|
|
23
|
-
live: { control: "boolean" },
|
|
24
|
-
},
|
|
25
|
-
} satisfies Meta<typeof AgentStatus>;
|
|
26
|
-
|
|
27
|
-
export default meta;
|
|
28
|
-
type Story = StoryObj<typeof meta>;
|
|
29
|
-
|
|
30
|
-
export const Default: Story = {};
|
|
31
|
-
|
|
32
|
-
/** Every state is a word. Colour never carries it alone. */
|
|
33
|
-
export const States: Story = {
|
|
34
|
-
parameters: { controls: { disable: true } },
|
|
35
|
-
render: () => (
|
|
36
|
-
<div className="flex flex-wrap gap-2">
|
|
37
|
-
{(["idle", "thinking", "working", "waiting", "done", "error"] as const).map((state) => (
|
|
38
|
-
<AgentStatus key={state} state={state} />
|
|
39
|
-
))}
|
|
40
|
-
</div>
|
|
41
|
-
),
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export const CustomWording: Story = {
|
|
45
|
-
parameters: { controls: { disable: true } },
|
|
46
|
-
render: () => (
|
|
47
|
-
<div className="flex flex-wrap gap-2">
|
|
48
|
-
<AgentStatus state="working" label="Indexing 412 files" />
|
|
49
|
-
<AgentStatus state="waiting" label="Needs your approval" />
|
|
50
|
-
<AgentStatus state="error" label="Rate limited — retrying in 30s" />
|
|
51
|
-
</div>
|
|
52
|
-
),
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/** In a fleet view, announcements stay off: six agents narrating is noise. */
|
|
56
|
-
export const AgentFleet: Story = {
|
|
57
|
-
parameters: { controls: { disable: true } },
|
|
58
|
-
render: () => (
|
|
59
|
-
<div className="w-96">
|
|
60
|
-
<ActivityFeed>
|
|
61
|
-
{[
|
|
62
|
-
{
|
|
63
|
-
id: "1",
|
|
64
|
-
name: "Researcher",
|
|
65
|
-
state: "working" as const,
|
|
66
|
-
at: "2026-09-01T09:12:00Z",
|
|
67
|
-
},
|
|
68
|
-
{ id: "2", name: "Writer", state: "waiting" as const, at: "2026-09-01T09:10:00Z" },
|
|
69
|
-
{ id: "3", name: "Reviewer", state: "done" as const, at: "2026-09-01T09:02:00Z" },
|
|
70
|
-
{ id: "4", name: "Publisher", state: "error" as const, at: "2026-09-01T08:58:00Z" },
|
|
71
|
-
].map((agent, index, all) => (
|
|
72
|
-
<ActivityItem key={agent.id} last={index === all.length - 1}>
|
|
73
|
-
<ActivityIndicator />
|
|
74
|
-
<ActivityContent>
|
|
75
|
-
<ActivityTitle className="flex items-center gap-2">
|
|
76
|
-
{agent.name}
|
|
77
|
-
<AgentStatus state={agent.state} />
|
|
78
|
-
</ActivityTitle>
|
|
79
|
-
<ActivityTime dateTime={agent.at}>Updated recently</ActivityTime>
|
|
80
|
-
</ActivityContent>
|
|
81
|
-
</ActivityItem>
|
|
82
|
-
))}
|
|
83
|
-
</ActivityFeed>
|
|
84
|
-
</div>
|
|
85
|
-
),
|
|
86
|
-
};
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
|
|
4
|
-
import { expectNoA11yViolations } from "../../../test/a11y";
|
|
5
|
-
import { AgentStatus, type AgentState } from "./ai-agent-status";
|
|
6
|
-
|
|
7
|
-
describe("AgentStatus", () => {
|
|
8
|
-
it.each([
|
|
9
|
-
["idle", "Idle"],
|
|
10
|
-
["thinking", "Thinking"],
|
|
11
|
-
["working", "Working"],
|
|
12
|
-
["waiting", "Waiting for input"],
|
|
13
|
-
["done", "Done"],
|
|
14
|
-
["error", "Error"],
|
|
15
|
-
] as [AgentState, string][])("states %s in words", (state, label) => {
|
|
16
|
-
render(<AgentStatus state={state} />);
|
|
17
|
-
// Agent states matter most when something has failed — exactly when a
|
|
18
|
-
// colour-only signal fails the people who most need to notice.
|
|
19
|
-
expect(screen.getByText(label)).toBeInTheDocument();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("accepts custom wording", () => {
|
|
23
|
-
render(<AgentStatus state="working" label="Indexing 412 files" />);
|
|
24
|
-
expect(screen.getByText("Indexing 412 files")).toBeInTheDocument();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("marks the state for styling", () => {
|
|
28
|
-
const { container } = render(<AgentStatus state="error" />);
|
|
29
|
-
expect(container.firstElementChild).toHaveAttribute("data-state", "error");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("does not announce by default", () => {
|
|
33
|
-
// Several agents each announcing their transitions turns a dashboard into
|
|
34
|
-
// noise.
|
|
35
|
-
render(<AgentStatus state="working" />);
|
|
36
|
-
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("announces politely when asked", () => {
|
|
40
|
-
render(<AgentStatus state="working" live />);
|
|
41
|
-
const status = screen.getByRole("status");
|
|
42
|
-
expect(status).toHaveAttribute("aria-live", "polite");
|
|
43
|
-
expect(status).toHaveTextContent("Working");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("hides its dot from assistive technology", () => {
|
|
47
|
-
const { container } = render(<AgentStatus state="done" />);
|
|
48
|
-
expect(container.querySelector("span[aria-hidden='true']")).toBeInTheDocument();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("lets a consumer className override a conflicting utility", () => {
|
|
52
|
-
const { container } = render(<AgentStatus state="idle" className="text-base" />);
|
|
53
|
-
expect(container.firstElementChild).toHaveClass("text-base");
|
|
54
|
-
expect(container.firstElementChild).not.toHaveClass("text-xs");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("has no accessibility violations", async () => {
|
|
58
|
-
const { container } = render(
|
|
59
|
-
<div>
|
|
60
|
-
<AgentStatus state="working" />
|
|
61
|
-
<AgentStatus state="error" />
|
|
62
|
-
</div>,
|
|
63
|
-
);
|
|
64
|
-
await expectNoA11yViolations(container);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
|
|
4
|
-
import { Message, MessageAvatar, MessageBody } from "@/components/ai-message";
|
|
5
|
-
import { Response, ThinkingIndicator } from "@/components/ai-response";
|
|
6
|
-
import { Button } from "@/components/button";
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
Conversation,
|
|
10
|
-
ConversationMessages,
|
|
11
|
-
ConversationScrollButton,
|
|
12
|
-
ConversationStatus,
|
|
13
|
-
} from "./ai-conversation";
|
|
14
|
-
|
|
15
|
-
const meta: Meta<typeof Conversation> = {
|
|
16
|
-
title: "AI/Conversation",
|
|
17
|
-
component: Conversation,
|
|
18
|
-
parameters: { controls: { disable: true } },
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export default meta;
|
|
22
|
-
type Story = StoryObj<typeof Conversation>;
|
|
23
|
-
|
|
24
|
-
const TURNS = [
|
|
25
|
-
{ role: "user" as const, text: "What's the difference between a Sheet and a Drawer?" },
|
|
26
|
-
{
|
|
27
|
-
role: "assistant" as const,
|
|
28
|
-
text: "A Sheet slides in from any edge and is dismissed with Escape, the overlay or a close button. A Drawer is bottom-anchored and adds a drag gesture — but the gesture is never the only way out.",
|
|
29
|
-
},
|
|
30
|
-
{ role: "user" as const, text: "When would I use one over the other?" },
|
|
31
|
-
{
|
|
32
|
-
role: "assistant" as const,
|
|
33
|
-
text: "Use a Sheet for secondary content on any viewport: filters, details, navigation. Use a Drawer when the surface is primarily touch-driven and dragging it away is the natural gesture.",
|
|
34
|
-
},
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
export const Default: Story = {
|
|
38
|
-
render: () => (
|
|
39
|
-
<div className="flex h-96 w-[34rem] flex-col rounded-xl border border-border">
|
|
40
|
-
<Conversation>
|
|
41
|
-
<ConversationMessages>
|
|
42
|
-
{TURNS.map((turn, index) => (
|
|
43
|
-
<Message key={index} from={turn.role}>
|
|
44
|
-
{turn.role === "assistant" ? <MessageAvatar>AI</MessageAvatar> : null}
|
|
45
|
-
<MessageBody from={turn.role}>
|
|
46
|
-
{turn.role === "assistant" ? <Response>{turn.text}</Response> : turn.text}
|
|
47
|
-
</MessageBody>
|
|
48
|
-
</Message>
|
|
49
|
-
))}
|
|
50
|
-
</ConversationMessages>
|
|
51
|
-
<ConversationScrollButton />
|
|
52
|
-
</Conversation>
|
|
53
|
-
</div>
|
|
54
|
-
),
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* State goes in the status region; the response text never does. A live region
|
|
59
|
-
* that updates per token is unusable with a screen reader.
|
|
60
|
-
*/
|
|
61
|
-
export const Streaming: Story = {
|
|
62
|
-
render: function Streaming() {
|
|
63
|
-
const [streaming, setStreaming] = useState(false);
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
<div className="flex h-96 w-[34rem] flex-col rounded-xl border border-border">
|
|
67
|
-
<Conversation>
|
|
68
|
-
<ConversationMessages>
|
|
69
|
-
<Message from="user">
|
|
70
|
-
<MessageBody from="user">Explain streaming responses.</MessageBody>
|
|
71
|
-
</Message>
|
|
72
|
-
<Message from="assistant">
|
|
73
|
-
<MessageAvatar>AI</MessageAvatar>
|
|
74
|
-
<MessageBody from="assistant">
|
|
75
|
-
{streaming ? (
|
|
76
|
-
<Response streaming>Tokens arrive one at a time, and the caret</Response>
|
|
77
|
-
) : (
|
|
78
|
-
<ThinkingIndicator />
|
|
79
|
-
)}
|
|
80
|
-
</MessageBody>
|
|
81
|
-
</Message>
|
|
82
|
-
</ConversationMessages>
|
|
83
|
-
<ConversationStatus>
|
|
84
|
-
{streaming ? "Generating response" : "Waiting for the first token"}
|
|
85
|
-
</ConversationStatus>
|
|
86
|
-
</Conversation>
|
|
87
|
-
<div className="border-t border-border p-3">
|
|
88
|
-
<Button
|
|
89
|
-
size="sm"
|
|
90
|
-
variant="outline"
|
|
91
|
-
onClick={() => {
|
|
92
|
-
setStreaming((current) => !current);
|
|
93
|
-
}}
|
|
94
|
-
>
|
|
95
|
-
Toggle streaming
|
|
96
|
-
</Button>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
);
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
/** Scroll up: following stops, and an explicit way back appears. */
|
|
104
|
-
export const LongTranscript: Story = {
|
|
105
|
-
render: () => (
|
|
106
|
-
<div className="flex h-96 w-[34rem] flex-col rounded-xl border border-border">
|
|
107
|
-
<Conversation>
|
|
108
|
-
<ConversationMessages>
|
|
109
|
-
{Array.from({ length: 14 }, (_, index) => {
|
|
110
|
-
const role = index % 2 === 0 ? ("user" as const) : ("assistant" as const);
|
|
111
|
-
return (
|
|
112
|
-
<Message key={index} from={role}>
|
|
113
|
-
{role === "assistant" ? <MessageAvatar>AI</MessageAvatar> : null}
|
|
114
|
-
<MessageBody from={role}>
|
|
115
|
-
Turn {index + 1}.{" "}
|
|
116
|
-
{role === "assistant" ? "An answer of some length." : "A question."}
|
|
117
|
-
</MessageBody>
|
|
118
|
-
</Message>
|
|
119
|
-
);
|
|
120
|
-
})}
|
|
121
|
-
</ConversationMessages>
|
|
122
|
-
<ConversationScrollButton />
|
|
123
|
-
</Conversation>
|
|
124
|
-
</div>
|
|
125
|
-
),
|
|
126
|
-
};
|