@agent-native/toolkit 0.9.0 → 0.10.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.
- package/README.md +12 -3
- package/agent-native.eject.json +71 -0
- package/dist/chat-history/ChatHistoryRail.d.ts +33 -0
- package/dist/chat-history/ChatHistoryRail.d.ts.map +1 -0
- package/dist/chat-history/ChatHistoryRail.js +35 -0
- package/dist/chat-history/ChatHistoryRail.js.map +1 -0
- package/dist/chat-history/ChatHistoryRail.spec.d.ts +2 -0
- package/dist/chat-history/ChatHistoryRail.spec.d.ts.map +1 -0
- package/dist/chat-history/ChatHistoryRail.spec.js +157 -0
- package/dist/chat-history/ChatHistoryRail.spec.js.map +1 -0
- package/dist/chat-history/index.d.ts +2 -0
- package/dist/chat-history/index.d.ts.map +1 -1
- package/dist/chat-history/index.js +2 -0
- package/dist/chat-history/index.js.map +1 -1
- package/dist/chat-history/useChatHistoryRailController.d.ts +26 -0
- package/dist/chat-history/useChatHistoryRailController.d.ts.map +1 -0
- package/dist/chat-history/useChatHistoryRailController.js +36 -0
- package/dist/chat-history/useChatHistoryRailController.js.map +1 -0
- package/dist/chat-history/useChatHistoryRailController.spec.d.ts +2 -0
- package/dist/chat-history/useChatHistoryRailController.spec.d.ts.map +1 -0
- package/dist/chat-history/useChatHistoryRailController.spec.js +101 -0
- package/dist/chat-history/useChatHistoryRailController.spec.js.map +1 -0
- package/dist/chat-history.css +61 -0
- package/dist/composer/ComposerPlusMenu.js +1 -1
- package/dist/composer/ComposerPlusMenu.js.map +1 -1
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +22 -4
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/RealtimeVoiceMode.d.ts +5 -1
- package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/RealtimeVoiceMode.js +27 -8
- package/dist/composer/RealtimeVoiceMode.js.map +1 -1
- package/dist/composer/RealtimeVoiceMode.spec.js +51 -11
- package/dist/composer/RealtimeVoiceMode.spec.js.map +1 -1
- package/dist/composer/VoiceButton.d.ts +2 -0
- package/dist/composer/VoiceButton.d.ts.map +1 -1
- package/dist/composer/VoiceButton.js +33 -1
- package/dist/composer/VoiceButton.js.map +1 -1
- package/dist/composer/VoiceButton.spec.js +9 -1
- package/dist/composer/VoiceButton.spec.js.map +1 -1
- package/dist/composer/realtime-voice-audio-level.d.ts.map +1 -1
- package/dist/composer/realtime-voice-audio-level.js +2 -1
- package/dist/composer/realtime-voice-audio-level.js.map +1 -1
- package/dist/composer/realtime-voice-audio-level.spec.js +7 -0
- package/dist/composer/realtime-voice-audio-level.spec.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.d.ts +7 -0
- package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.js +95 -23
- package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.spec.js +58 -9
- package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -1
- package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts +7 -0
- package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts.map +1 -0
- package/dist/conformance/__fixtures__/css-in-js-adapter.js +84 -0
- package/dist/conformance/__fixtures__/css-in-js-adapter.js.map +1 -0
- package/dist/conformance/conformance.spec.d.ts +2 -0
- package/dist/conformance/conformance.spec.d.ts.map +1 -0
- package/dist/conformance/conformance.spec.js +58 -0
- package/dist/conformance/conformance.spec.js.map +1 -0
- package/dist/conformance/index.d.ts +4 -0
- package/dist/conformance/index.d.ts.map +1 -0
- package/dist/conformance/index.js +4 -0
- package/dist/conformance/index.js.map +1 -0
- package/dist/conformance/runner.d.ts +12 -0
- package/dist/conformance/runner.d.ts.map +1 -0
- package/dist/conformance/runner.js +441 -0
- package/dist/conformance/runner.js.map +1 -0
- package/dist/conformance/types.d.ts +26 -0
- package/dist/conformance/types.d.ts.map +1 -0
- package/dist/conformance/types.js +13 -0
- package/dist/conformance/types.js.map +1 -0
- package/dist/conformance/version.d.ts +6 -0
- package/dist/conformance/version.d.ts.map +1 -0
- package/dist/conformance/version.js +14 -0
- package/dist/conformance/version.js.map +1 -0
- package/dist/design-system/components.d.ts +64 -0
- package/dist/design-system/components.d.ts.map +1 -0
- package/dist/design-system/components.js +50 -0
- package/dist/design-system/components.js.map +1 -0
- package/dist/design-system/context.d.ts +11 -0
- package/dist/design-system/context.d.ts.map +1 -0
- package/dist/design-system/context.js +17 -0
- package/dist/design-system/context.js.map +1 -0
- package/dist/design-system/default-adapter.d.ts +3 -0
- package/dist/design-system/default-adapter.d.ts.map +1 -0
- package/dist/design-system/default-adapter.js +166 -0
- package/dist/design-system/default-adapter.js.map +1 -0
- package/dist/design-system/default-adapter.spec.d.ts +2 -0
- package/dist/design-system/default-adapter.spec.d.ts.map +1 -0
- package/dist/design-system/default-adapter.spec.js +96 -0
- package/dist/design-system/default-adapter.spec.js.map +1 -0
- package/dist/design-system/definition.d.ts +9 -0
- package/dist/design-system/definition.d.ts.map +1 -0
- package/dist/design-system/definition.js +4 -0
- package/dist/design-system/definition.js.map +1 -0
- package/dist/design-system/design-system.spec.d.ts +2 -0
- package/dist/design-system/design-system.spec.d.ts.map +1 -0
- package/dist/design-system/design-system.spec.js +121 -0
- package/dist/design-system/design-system.spec.js.map +1 -0
- package/dist/design-system/error-boundary.d.ts +18 -0
- package/dist/design-system/error-boundary.d.ts.map +1 -0
- package/dist/design-system/error-boundary.js +16 -0
- package/dist/design-system/error-boundary.js.map +1 -0
- package/dist/design-system/index.d.ts +8 -0
- package/dist/design-system/index.d.ts.map +1 -0
- package/dist/design-system/index.js +8 -0
- package/dist/design-system/index.js.map +1 -0
- package/dist/design-system/theme.d.ts +30 -0
- package/dist/design-system/theme.d.ts.map +1 -0
- package/dist/design-system/theme.js +95 -0
- package/dist/design-system/theme.js.map +1 -0
- package/dist/design-system/theme.spec.d.ts +2 -0
- package/dist/design-system/theme.spec.d.ts.map +1 -0
- package/dist/design-system/theme.spec.js +43 -0
- package/dist/design-system/theme.spec.js.map +1 -0
- package/dist/design-system/types.d.ts +288 -0
- package/dist/design-system/types.d.ts.map +1 -0
- package/dist/design-system/types.js +2 -0
- package/dist/design-system/types.js.map +1 -0
- package/dist/provider.d.ts +4 -3
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +46 -4
- package/dist/provider.js.map +1 -1
- package/dist/styles.css +35 -0
- package/dist/ui/button.d.ts +5 -0
- package/dist/ui/button.d.ts.map +1 -1
- package/dist/ui/button.js +25 -4
- package/dist/ui/button.js.map +1 -1
- package/dist/ui/dialog.d.ts +4 -0
- package/dist/ui/dialog.d.ts.map +1 -1
- package/dist/ui/dialog.js +2 -2
- package/dist/ui/dialog.js.map +1 -1
- package/dist/ui/popover.d.ts +1 -0
- package/dist/ui/popover.d.ts.map +1 -1
- package/dist/ui/popover.js +2 -2
- package/dist/ui/popover.js.map +1 -1
- package/dist/ui/select.d.ts +3 -1
- package/dist/ui/select.d.ts.map +1 -1
- package/dist/ui/select.js +1 -1
- package/dist/ui/select.js.map +1 -1
- package/dist/ui/sidebar.d.ts +6 -2
- package/dist/ui/sidebar.d.ts.map +1 -1
- package/dist/ui/sidebar.js +12 -15
- package/dist/ui/sidebar.js.map +1 -1
- package/dist/ui/tooltip.d.ts +3 -1
- package/dist/ui/tooltip.d.ts.map +1 -1
- package/dist/ui/tooltip.js +2 -2
- package/dist/ui/tooltip.js.map +1 -1
- package/package.json +31 -1
- package/src/chat-history/ChatHistoryRail.spec.tsx +312 -0
- package/src/chat-history/ChatHistoryRail.tsx +150 -0
- package/src/chat-history/index.ts +14 -0
- package/src/chat-history/useChatHistoryRailController.spec.tsx +131 -0
- package/src/chat-history/useChatHistoryRailController.ts +76 -0
- package/src/chat-history.css +61 -0
- package/src/composer/ComposerPlusMenu.tsx +11 -8
- package/src/composer/PromptComposer.tsx +27 -6
- package/src/composer/RealtimeVoiceMode.spec.tsx +116 -11
- package/src/composer/RealtimeVoiceMode.tsx +106 -36
- package/src/composer/VoiceButton.spec.ts +17 -1
- package/src/composer/VoiceButton.tsx +49 -0
- package/src/composer/realtime-voice-audio-level.spec.ts +15 -0
- package/src/composer/realtime-voice-audio-level.ts +2 -1
- package/src/composer/useRealtimeVoiceMode.spec.ts +82 -8
- package/src/composer/useRealtimeVoiceMode.tsx +112 -25
- package/src/conformance/__fixtures__/css-in-js-adapter.tsx +401 -0
- package/src/conformance/conformance.spec.tsx +83 -0
- package/src/conformance/index.ts +3 -0
- package/src/conformance/runner.tsx +710 -0
- package/src/conformance/types.ts +45 -0
- package/src/conformance/version.ts +22 -0
- package/src/design-system/components.tsx +146 -0
- package/src/design-system/context.tsx +31 -0
- package/src/design-system/default-adapter.spec.tsx +143 -0
- package/src/design-system/default-adapter.tsx +915 -0
- package/src/design-system/definition.ts +14 -0
- package/src/design-system/design-system.spec.tsx +183 -0
- package/src/design-system/error-boundary.tsx +37 -0
- package/src/design-system/index.ts +7 -0
- package/src/design-system/theme.spec.ts +61 -0
- package/src/design-system/theme.ts +145 -0
- package/src/design-system/types.ts +363 -0
- package/src/provider.tsx +98 -5
- package/src/styles.css +35 -0
- package/src/ui/button.tsx +95 -6
- package/src/ui/dialog.tsx +48 -23
- package/src/ui/popover.tsx +16 -2
- package/src/ui/select.tsx +7 -3
- package/src/ui/sidebar.tsx +47 -48
- package/src/ui/tooltip.tsx +7 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Reusable app-building UI and helpers for Agent-Native apps.",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -103,6 +103,20 @@
|
|
|
103
103
|
"import": "./dist/composer/*.js",
|
|
104
104
|
"default": "./dist/composer/*.js"
|
|
105
105
|
},
|
|
106
|
+
"./conformance": {
|
|
107
|
+
"types": "./dist/conformance/index.d.ts",
|
|
108
|
+
"browser": "./dist/conformance/index.js",
|
|
109
|
+
"development": "./dist/conformance/index.js",
|
|
110
|
+
"import": "./dist/conformance/index.js",
|
|
111
|
+
"default": "./dist/conformance/index.js"
|
|
112
|
+
},
|
|
113
|
+
"./conformance/*": {
|
|
114
|
+
"types": "./dist/conformance/*.d.ts",
|
|
115
|
+
"browser": "./dist/conformance/*.js",
|
|
116
|
+
"development": "./dist/conformance/*.js",
|
|
117
|
+
"import": "./dist/conformance/*.js",
|
|
118
|
+
"default": "./dist/conformance/*.js"
|
|
119
|
+
},
|
|
106
120
|
"./context-ui": {
|
|
107
121
|
"types": "./dist/context-ui/index.d.ts",
|
|
108
122
|
"browser": "./dist/context-ui/index.js",
|
|
@@ -117,6 +131,20 @@
|
|
|
117
131
|
"import": "./dist/design-tweaks/index.js",
|
|
118
132
|
"default": "./dist/design-tweaks/index.js"
|
|
119
133
|
},
|
|
134
|
+
"./design-system": {
|
|
135
|
+
"types": "./dist/design-system/index.d.ts",
|
|
136
|
+
"browser": "./dist/design-system/index.js",
|
|
137
|
+
"development": "./dist/design-system/index.js",
|
|
138
|
+
"import": "./dist/design-system/index.js",
|
|
139
|
+
"default": "./dist/design-system/index.js"
|
|
140
|
+
},
|
|
141
|
+
"./design-system/*": {
|
|
142
|
+
"types": "./dist/design-system/*.d.ts",
|
|
143
|
+
"browser": "./dist/design-system/*.js",
|
|
144
|
+
"development": "./dist/design-system/*.js",
|
|
145
|
+
"import": "./dist/design-system/*.js",
|
|
146
|
+
"default": "./dist/design-system/*.js"
|
|
147
|
+
},
|
|
120
148
|
"./editor": {
|
|
121
149
|
"types": "./dist/editor/index.d.ts",
|
|
122
150
|
"browser": "./dist/editor/index.js",
|
|
@@ -261,6 +289,7 @@
|
|
|
261
289
|
"class-variance-authority": "^0.7.1",
|
|
262
290
|
"clsx": "^2.1.1",
|
|
263
291
|
"cmdk": "^1.1.1",
|
|
292
|
+
"culori": "^4.0.2",
|
|
264
293
|
"date-fns": "^4.4.0",
|
|
265
294
|
"embla-carousel-react": "^8.6.0",
|
|
266
295
|
"highlight.js": "^11.11.1",
|
|
@@ -279,6 +308,7 @@
|
|
|
279
308
|
"yjs": "^13.6.31"
|
|
280
309
|
},
|
|
281
310
|
"devDependencies": {
|
|
311
|
+
"@types/culori": "^4.0.1",
|
|
282
312
|
"@types/react": "^19.2.14",
|
|
283
313
|
"@types/react-dom": "^19.2.3",
|
|
284
314
|
"typescript-7": "npm:typescript@^7.0.2",
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { ActionButton, IconButton } from "../design-system/components.js";
|
|
8
|
+
import { defineDesignSystem } from "../design-system/definition.js";
|
|
9
|
+
import { ToolkitProvider } from "../provider.js";
|
|
10
|
+
import type { ChatHistoryItem } from "./ChatHistoryList.js";
|
|
11
|
+
import { ChatHistoryRail } from "./ChatHistoryRail.js";
|
|
12
|
+
|
|
13
|
+
const railLabels = {
|
|
14
|
+
newChat: "New chat",
|
|
15
|
+
showMore: "Show more chats",
|
|
16
|
+
showLess: "Show fewer chats",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function makeItems(count: number): ChatHistoryItem[] {
|
|
20
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
21
|
+
id: `thread-${index + 1}`,
|
|
22
|
+
title: `Chat ${index + 1}`,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("ChatHistoryRail", () => {
|
|
27
|
+
let container: HTMLDivElement;
|
|
28
|
+
let root: Root;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
32
|
+
container = document.createElement("div");
|
|
33
|
+
document.body.appendChild(container);
|
|
34
|
+
root = createRoot(container);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
act(() => root.unmount());
|
|
39
|
+
container.remove();
|
|
40
|
+
vi.restoreAllMocks();
|
|
41
|
+
vi.unstubAllGlobals();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("shows five recent chats before progressively disclosing up to fifteen", () => {
|
|
45
|
+
act(() => {
|
|
46
|
+
root.render(
|
|
47
|
+
<ChatHistoryRail
|
|
48
|
+
items={makeItems(20)}
|
|
49
|
+
onSelect={() => {}}
|
|
50
|
+
onNewChat={() => {}}
|
|
51
|
+
railLabels={railLabels}
|
|
52
|
+
/>,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(container.querySelectorAll(".an-chat-history-row")).toHaveLength(5);
|
|
57
|
+
const disclosure = container.querySelector<HTMLButtonElement>(
|
|
58
|
+
".an-chat-history-rail__disclosure",
|
|
59
|
+
);
|
|
60
|
+
expect(disclosure?.getAttribute("aria-label")).toBe("Show more chats");
|
|
61
|
+
|
|
62
|
+
act(() => disclosure?.click());
|
|
63
|
+
expect(container.querySelectorAll(".an-chat-history-row")).toHaveLength(15);
|
|
64
|
+
expect(disclosure?.getAttribute("aria-label")).toBe("Show fewer chats");
|
|
65
|
+
|
|
66
|
+
act(() => disclosure?.click());
|
|
67
|
+
expect(container.querySelectorAll(".an-chat-history-row")).toHaveLength(5);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("keeps the disclosure to the right of new chat and calls its handler", () => {
|
|
71
|
+
const onNewChat = vi.fn();
|
|
72
|
+
act(() => {
|
|
73
|
+
root.render(
|
|
74
|
+
<ChatHistoryRail
|
|
75
|
+
items={makeItems(6)}
|
|
76
|
+
onSelect={() => {}}
|
|
77
|
+
onNewChat={onNewChat}
|
|
78
|
+
railLabels={railLabels}
|
|
79
|
+
/>,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const disclosure = container.querySelector<HTMLButtonElement>(
|
|
84
|
+
".an-chat-history-rail__disclosure",
|
|
85
|
+
);
|
|
86
|
+
const newChat = container.querySelector<HTMLButtonElement>(
|
|
87
|
+
".an-chat-history-rail__new-chat",
|
|
88
|
+
);
|
|
89
|
+
expect(newChat?.textContent).toBe("New chat");
|
|
90
|
+
expect(newChat?.nextElementSibling).toBe(disclosure);
|
|
91
|
+
expect(newChat?.parentElement?.lastElementChild).toBe(disclosure);
|
|
92
|
+
|
|
93
|
+
act(() => newChat?.click());
|
|
94
|
+
expect(onNewChat).toHaveBeenCalledOnce();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("lets new chat fill the footer when there are no more chats", () => {
|
|
98
|
+
act(() => {
|
|
99
|
+
root.render(
|
|
100
|
+
<ChatHistoryRail
|
|
101
|
+
items={makeItems(3)}
|
|
102
|
+
onSelect={() => {}}
|
|
103
|
+
onNewChat={() => {}}
|
|
104
|
+
railLabels={railLabels}
|
|
105
|
+
/>,
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(
|
|
110
|
+
container.querySelector(".an-chat-history-rail__disclosure"),
|
|
111
|
+
).toBeNull();
|
|
112
|
+
expect(
|
|
113
|
+
container.querySelector(".an-chat-history-rail__new-chat"),
|
|
114
|
+
).not.toBeNull();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("preserves list actions for progressively disclosed rows", () => {
|
|
118
|
+
const onSelect = vi.fn();
|
|
119
|
+
act(() => {
|
|
120
|
+
root.render(
|
|
121
|
+
<ChatHistoryRail
|
|
122
|
+
items={makeItems(6)}
|
|
123
|
+
onSelect={onSelect}
|
|
124
|
+
onNewChat={() => {}}
|
|
125
|
+
railLabels={railLabels}
|
|
126
|
+
/>,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
act(() => {
|
|
131
|
+
container
|
|
132
|
+
.querySelector<HTMLButtonElement>(".an-chat-history-rail__disclosure")
|
|
133
|
+
?.click();
|
|
134
|
+
});
|
|
135
|
+
const rows = container.querySelectorAll<HTMLButtonElement>(
|
|
136
|
+
".an-chat-history-row__button",
|
|
137
|
+
);
|
|
138
|
+
act(() => rows[5]?.click());
|
|
139
|
+
expect(onSelect).toHaveBeenCalledWith("thread-6");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("lets a design system replace the whole view without replacing its controller", () => {
|
|
143
|
+
const onNewChat = vi.fn();
|
|
144
|
+
const CustomActionButton = vi.fn(
|
|
145
|
+
({ children, onPress }: Parameters<typeof ActionButton>[0]) => (
|
|
146
|
+
<button
|
|
147
|
+
data-acme-action
|
|
148
|
+
type="button"
|
|
149
|
+
onClick={(event) => onPress?.(event)}
|
|
150
|
+
>
|
|
151
|
+
{children}
|
|
152
|
+
</button>
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
const designSystem = defineDesignSystem({
|
|
156
|
+
name: "Acme",
|
|
157
|
+
components: { ActionButton: CustomActionButton },
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
act(() => {
|
|
161
|
+
root.render(
|
|
162
|
+
<ToolkitProvider designSystem={designSystem}>
|
|
163
|
+
<ChatHistoryRail
|
|
164
|
+
items={makeItems(20)}
|
|
165
|
+
onSelect={() => {}}
|
|
166
|
+
onNewChat={onNewChat}
|
|
167
|
+
railLabels={railLabels}
|
|
168
|
+
renderRail={({ controller }) => (
|
|
169
|
+
<section data-acme-rail>
|
|
170
|
+
<output>{controller.visibleItems.length}</output>
|
|
171
|
+
<ActionButton onPress={controller.onNewChat}>
|
|
172
|
+
{controller.newChatLabel}
|
|
173
|
+
</ActionButton>
|
|
174
|
+
<ActionButton onPress={controller.toggleExpanded}>
|
|
175
|
+
{controller.disclosureLabel}
|
|
176
|
+
</ActionButton>
|
|
177
|
+
</section>
|
|
178
|
+
)}
|
|
179
|
+
/>
|
|
180
|
+
</ToolkitProvider>,
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const actions =
|
|
185
|
+
container.querySelectorAll<HTMLButtonElement>("[data-acme-action]");
|
|
186
|
+
expect(container.querySelector("[data-acme-rail]")).not.toBeNull();
|
|
187
|
+
expect(container.querySelector("output")?.textContent).toBe("5");
|
|
188
|
+
expect(actions[1]?.textContent).toBe("Show more chats");
|
|
189
|
+
|
|
190
|
+
act(() => actions[1]?.click());
|
|
191
|
+
expect(container.querySelector("output")?.textContent).toBe("15");
|
|
192
|
+
expect(actions[1]?.textContent).toBe("Show fewer chats");
|
|
193
|
+
|
|
194
|
+
act(() => actions[0]?.click());
|
|
195
|
+
expect(onNewChat).toHaveBeenCalledOnce();
|
|
196
|
+
expect(CustomActionButton).toHaveBeenCalled();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("routes the default footer through the semantic action bridge", () => {
|
|
200
|
+
const onNewChat = vi.fn();
|
|
201
|
+
const CustomActionButton = vi.fn(
|
|
202
|
+
({ children, onPress }: Parameters<typeof ActionButton>[0]) => (
|
|
203
|
+
<button
|
|
204
|
+
data-semantic-action
|
|
205
|
+
type="button"
|
|
206
|
+
onClick={(event) => onPress?.(event)}
|
|
207
|
+
>
|
|
208
|
+
{children}
|
|
209
|
+
</button>
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
const CustomIconButton = vi.fn(
|
|
213
|
+
({ icon, label, onPress }: Parameters<typeof IconButton>[0]) => (
|
|
214
|
+
<button
|
|
215
|
+
data-semantic-icon
|
|
216
|
+
type="button"
|
|
217
|
+
aria-label={label}
|
|
218
|
+
onClick={(event) => onPress?.(event)}
|
|
219
|
+
>
|
|
220
|
+
{icon}
|
|
221
|
+
</button>
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
const designSystem = defineDesignSystem({
|
|
225
|
+
name: "Acme",
|
|
226
|
+
components: {
|
|
227
|
+
ActionButton: CustomActionButton,
|
|
228
|
+
IconButton: CustomIconButton,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
act(() => {
|
|
233
|
+
root.render(
|
|
234
|
+
<ToolkitProvider designSystem={designSystem}>
|
|
235
|
+
<ChatHistoryRail
|
|
236
|
+
items={makeItems(6)}
|
|
237
|
+
onSelect={() => {}}
|
|
238
|
+
onNewChat={onNewChat}
|
|
239
|
+
railLabels={railLabels}
|
|
240
|
+
/>
|
|
241
|
+
</ToolkitProvider>,
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const newChat = container.querySelector<HTMLButtonElement>(
|
|
246
|
+
"[data-semantic-action]",
|
|
247
|
+
);
|
|
248
|
+
const disclosure = container.querySelector<HTMLButtonElement>(
|
|
249
|
+
"[data-semantic-icon]",
|
|
250
|
+
);
|
|
251
|
+
expect(newChat).not.toBeNull();
|
|
252
|
+
expect(disclosure?.getAttribute("aria-label")).toBe("Show more chats");
|
|
253
|
+
expect(CustomActionButton.mock.calls[0]?.[0]).toMatchObject({
|
|
254
|
+
emphasis: "ghost",
|
|
255
|
+
size: "compact",
|
|
256
|
+
leadingIcon: expect.anything(),
|
|
257
|
+
});
|
|
258
|
+
expect(CustomIconButton.mock.calls[0]?.[0]).toMatchObject({
|
|
259
|
+
size: "compact",
|
|
260
|
+
"aria-expanded": false,
|
|
261
|
+
label: "Show more chats",
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
act(() => newChat?.click());
|
|
265
|
+
expect(onNewChat).toHaveBeenCalledOnce();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("keeps native button semantics and focus in the default view", () => {
|
|
269
|
+
const onNewChat = vi.fn();
|
|
270
|
+
act(() => {
|
|
271
|
+
root.render(
|
|
272
|
+
<ChatHistoryRail
|
|
273
|
+
items={makeItems(6)}
|
|
274
|
+
onSelect={() => {}}
|
|
275
|
+
onNewChat={onNewChat}
|
|
276
|
+
railLabels={railLabels}
|
|
277
|
+
/>,
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const newChat = container.querySelector<HTMLButtonElement>(
|
|
282
|
+
".an-chat-history-rail__new-chat",
|
|
283
|
+
);
|
|
284
|
+
newChat?.focus();
|
|
285
|
+
act(() => newChat?.click());
|
|
286
|
+
|
|
287
|
+
expect(newChat?.type).toBe("button");
|
|
288
|
+
expect(document.activeElement).toBe(newChat);
|
|
289
|
+
expect(onNewChat).toHaveBeenCalledOnce();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("falls back to the default rail when a product renderer fails", () => {
|
|
293
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
294
|
+
act(() => {
|
|
295
|
+
root.render(
|
|
296
|
+
<ChatHistoryRail
|
|
297
|
+
items={makeItems(6)}
|
|
298
|
+
onSelect={() => {}}
|
|
299
|
+
onNewChat={() => {}}
|
|
300
|
+
railLabels={railLabels}
|
|
301
|
+
renderRail={() => {
|
|
302
|
+
throw new Error("broken company rail");
|
|
303
|
+
}}
|
|
304
|
+
/>,
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(
|
|
309
|
+
container.querySelector(".an-chat-history-rail__new-chat"),
|
|
310
|
+
).not.toBeNull();
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { IconDots, IconPlus } from "@tabler/icons-react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { ActionButton, IconButton } from "../design-system/components.js";
|
|
5
|
+
import { DesignSystemErrorBoundary } from "../design-system/error-boundary.js";
|
|
6
|
+
import {
|
|
7
|
+
ChatHistoryList,
|
|
8
|
+
type ChatHistoryItem,
|
|
9
|
+
type ChatHistoryListProps,
|
|
10
|
+
} from "./ChatHistoryList.js";
|
|
11
|
+
import {
|
|
12
|
+
type ChatHistoryRailController,
|
|
13
|
+
useChatHistoryRailController,
|
|
14
|
+
} from "./useChatHistoryRailController.js";
|
|
15
|
+
|
|
16
|
+
export interface ChatHistoryRailLabels {
|
|
17
|
+
newChat: string;
|
|
18
|
+
showMore: string;
|
|
19
|
+
showLess: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ChatHistoryRailProps extends Omit<
|
|
23
|
+
ChatHistoryListProps,
|
|
24
|
+
"footer" | "items" | "sections" | "variant"
|
|
25
|
+
> {
|
|
26
|
+
items: ChatHistoryItem[];
|
|
27
|
+
onNewChat: () => void;
|
|
28
|
+
railLabels: ChatHistoryRailLabels;
|
|
29
|
+
previewCount?: number;
|
|
30
|
+
expandedCount?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Product-level presentation slot for design systems that want to replace
|
|
33
|
+
* the rail wholesale. State and actions still come from the same controller
|
|
34
|
+
* as the default view.
|
|
35
|
+
*/
|
|
36
|
+
renderRail?: (context: ChatHistoryRailRenderContext) => ReactNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ChatHistoryRailRenderContext {
|
|
40
|
+
controller: ChatHistoryRailController<ChatHistoryItem>;
|
|
41
|
+
listProps: Omit<
|
|
42
|
+
ChatHistoryListProps,
|
|
43
|
+
"footer" | "items" | "sections" | "variant"
|
|
44
|
+
>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type DefaultChatHistoryRailViewProps = ChatHistoryRailRenderContext;
|
|
48
|
+
|
|
49
|
+
export function DefaultChatHistoryRailView({
|
|
50
|
+
controller,
|
|
51
|
+
listProps,
|
|
52
|
+
}: ChatHistoryRailRenderContext) {
|
|
53
|
+
const {
|
|
54
|
+
canExpand,
|
|
55
|
+
disclosureLabel,
|
|
56
|
+
expanded,
|
|
57
|
+
newChatLabel,
|
|
58
|
+
onNewChat,
|
|
59
|
+
toggleExpanded,
|
|
60
|
+
visibleItems,
|
|
61
|
+
} = controller;
|
|
62
|
+
const { className, emptyLabel, ...rest } = listProps;
|
|
63
|
+
|
|
64
|
+
const footer = (
|
|
65
|
+
<div className="an-chat-history-rail__footer">
|
|
66
|
+
<ActionButton
|
|
67
|
+
type="button"
|
|
68
|
+
className="an-chat-history-rail__new-chat"
|
|
69
|
+
emphasis="ghost"
|
|
70
|
+
size="compact"
|
|
71
|
+
leadingIcon={
|
|
72
|
+
<IconPlus size={13} strokeWidth={1.8} aria-hidden="true" />
|
|
73
|
+
}
|
|
74
|
+
onPress={onNewChat}
|
|
75
|
+
>
|
|
76
|
+
<span>{newChatLabel}</span>
|
|
77
|
+
</ActionButton>
|
|
78
|
+
{canExpand && (
|
|
79
|
+
<IconButton
|
|
80
|
+
type="button"
|
|
81
|
+
className="an-chat-history-rail__disclosure"
|
|
82
|
+
size="compact"
|
|
83
|
+
icon={<IconDots size={14} strokeWidth={1.8} aria-hidden="true" />}
|
|
84
|
+
onPress={toggleExpanded}
|
|
85
|
+
aria-expanded={expanded}
|
|
86
|
+
label={disclosureLabel}
|
|
87
|
+
title={disclosureLabel}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<ChatHistoryList
|
|
95
|
+
{...rest}
|
|
96
|
+
items={visibleItems}
|
|
97
|
+
footer={footer}
|
|
98
|
+
emptyLabel={emptyLabel ?? null}
|
|
99
|
+
variant="rail"
|
|
100
|
+
className={["an-chat-history-rail", className].filter(Boolean).join(" ")}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Compact recent-chat rail for app sidebars. Hosts own thread persistence,
|
|
107
|
+
* sorting, routing, and mutations; the rail only owns progressive disclosure.
|
|
108
|
+
*/
|
|
109
|
+
export function ChatHistoryRail({
|
|
110
|
+
items,
|
|
111
|
+
onNewChat,
|
|
112
|
+
railLabels,
|
|
113
|
+
previewCount = 5,
|
|
114
|
+
expandedCount = 15,
|
|
115
|
+
renderRail,
|
|
116
|
+
className,
|
|
117
|
+
emptyLabel,
|
|
118
|
+
...listProps
|
|
119
|
+
}: ChatHistoryRailProps) {
|
|
120
|
+
const controller = useChatHistoryRailController({
|
|
121
|
+
items,
|
|
122
|
+
onNewChat,
|
|
123
|
+
labels: railLabels,
|
|
124
|
+
previewCount,
|
|
125
|
+
expandedCount,
|
|
126
|
+
});
|
|
127
|
+
const context: ChatHistoryRailRenderContext = {
|
|
128
|
+
controller,
|
|
129
|
+
listProps: { ...listProps, className, emptyLabel },
|
|
130
|
+
};
|
|
131
|
+
const fallback = <DefaultChatHistoryRailView {...context} />;
|
|
132
|
+
|
|
133
|
+
return renderRail ? (
|
|
134
|
+
<DesignSystemErrorBoundary component="ChatHistoryRail" fallback={fallback}>
|
|
135
|
+
<ChatHistoryRailCustomView renderRail={renderRail} context={context} />
|
|
136
|
+
</DesignSystemErrorBoundary>
|
|
137
|
+
) : (
|
|
138
|
+
fallback
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function ChatHistoryRailCustomView({
|
|
143
|
+
renderRail,
|
|
144
|
+
context,
|
|
145
|
+
}: {
|
|
146
|
+
renderRail: NonNullable<ChatHistoryRailProps["renderRail"]>;
|
|
147
|
+
context: ChatHistoryRailRenderContext;
|
|
148
|
+
}) {
|
|
149
|
+
return renderRail(context);
|
|
150
|
+
}
|
|
@@ -5,3 +5,17 @@ export {
|
|
|
5
5
|
type ChatHistoryListProps,
|
|
6
6
|
type ChatHistorySection,
|
|
7
7
|
} from "./ChatHistoryList.js";
|
|
8
|
+
export {
|
|
9
|
+
ChatHistoryRail,
|
|
10
|
+
DefaultChatHistoryRailView,
|
|
11
|
+
type DefaultChatHistoryRailViewProps,
|
|
12
|
+
type ChatHistoryRailLabels,
|
|
13
|
+
type ChatHistoryRailProps,
|
|
14
|
+
type ChatHistoryRailRenderContext,
|
|
15
|
+
} from "./ChatHistoryRail.js";
|
|
16
|
+
export {
|
|
17
|
+
type ChatHistoryRailController,
|
|
18
|
+
type ChatHistoryRailControllerLabels,
|
|
19
|
+
type UseChatHistoryRailControllerOptions,
|
|
20
|
+
useChatHistoryRailController,
|
|
21
|
+
} from "./useChatHistoryRailController.js";
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type ChatHistoryRailController,
|
|
9
|
+
type UseChatHistoryRailControllerOptions,
|
|
10
|
+
useChatHistoryRailController,
|
|
11
|
+
} from "./useChatHistoryRailController.js";
|
|
12
|
+
|
|
13
|
+
interface Item {
|
|
14
|
+
id: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const labels = {
|
|
18
|
+
newChat: "New chat",
|
|
19
|
+
showMore: "Show more chats",
|
|
20
|
+
showLess: "Show fewer chats",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function makeItems(count: number): Item[] {
|
|
24
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
25
|
+
id: `thread-${index + 1}`,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("useChatHistoryRailController", () => {
|
|
30
|
+
let container: HTMLDivElement;
|
|
31
|
+
let root: Root;
|
|
32
|
+
let controller: ChatHistoryRailController<Item>;
|
|
33
|
+
|
|
34
|
+
function Harness(props: UseChatHistoryRailControllerOptions<Item>) {
|
|
35
|
+
controller = useChatHistoryRailController(props);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function render(options: Partial<UseChatHistoryRailControllerOptions<Item>>) {
|
|
40
|
+
const props: UseChatHistoryRailControllerOptions<Item> = {
|
|
41
|
+
items: makeItems(20),
|
|
42
|
+
labels,
|
|
43
|
+
onNewChat: () => {},
|
|
44
|
+
...options,
|
|
45
|
+
};
|
|
46
|
+
act(() => root.render(<Harness {...props} />));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
51
|
+
container = document.createElement("div");
|
|
52
|
+
document.body.appendChild(container);
|
|
53
|
+
root = createRoot(container);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
act(() => root.unmount());
|
|
58
|
+
container.remove();
|
|
59
|
+
vi.restoreAllMocks();
|
|
60
|
+
vi.unstubAllGlobals();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("progressively discloses the default five and fifteen item limits", () => {
|
|
64
|
+
const items = makeItems(20);
|
|
65
|
+
render({ items });
|
|
66
|
+
|
|
67
|
+
expect(controller.expanded).toBe(false);
|
|
68
|
+
expect(controller.collapsedLimit).toBe(5);
|
|
69
|
+
expect(controller.expandedLimit).toBe(15);
|
|
70
|
+
expect(controller.visibleItems).toEqual(items.slice(0, 5));
|
|
71
|
+
expect(controller.canExpand).toBe(true);
|
|
72
|
+
expect(controller.disclosureLabel).toBe("Show more chats");
|
|
73
|
+
|
|
74
|
+
act(() => controller.toggleExpanded());
|
|
75
|
+
|
|
76
|
+
expect(controller.expanded).toBe(true);
|
|
77
|
+
expect(controller.visibleItems).toEqual(items.slice(0, 15));
|
|
78
|
+
expect(controller.disclosureLabel).toBe("Show fewer chats");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("clamps finite limits while preserving legacy disclosure behavior", () => {
|
|
82
|
+
render({ previewCount: -2.5, expandedCount: 1.9 });
|
|
83
|
+
|
|
84
|
+
expect(controller.collapsedLimit).toBe(1);
|
|
85
|
+
expect(controller.expandedLimit).toBe(1);
|
|
86
|
+
expect(controller.visibleItems).toHaveLength(1);
|
|
87
|
+
expect(controller.canExpand).toBe(true);
|
|
88
|
+
|
|
89
|
+
act(() => controller.toggleExpanded());
|
|
90
|
+
expect(controller.expanded).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("uses defaults for non-finite limits and never puts expanded below preview", () => {
|
|
94
|
+
render({
|
|
95
|
+
previewCount: Number.NaN,
|
|
96
|
+
expandedCount: Number.POSITIVE_INFINITY,
|
|
97
|
+
});
|
|
98
|
+
expect(controller.collapsedLimit).toBe(5);
|
|
99
|
+
expect(controller.expandedLimit).toBe(15);
|
|
100
|
+
|
|
101
|
+
render({ previewCount: 8, expandedCount: 3 });
|
|
102
|
+
expect(controller.collapsedLimit).toBe(8);
|
|
103
|
+
expect(controller.expandedLimit).toBe(8);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("resets disclosure explicitly and when the collection becomes too short", () => {
|
|
107
|
+
render({});
|
|
108
|
+
act(() => controller.toggleExpanded());
|
|
109
|
+
expect(controller.expanded).toBe(true);
|
|
110
|
+
|
|
111
|
+
act(() => controller.resetExpanded());
|
|
112
|
+
expect(controller.expanded).toBe(false);
|
|
113
|
+
|
|
114
|
+
act(() => controller.toggleExpanded());
|
|
115
|
+
expect(controller.expanded).toBe(true);
|
|
116
|
+
|
|
117
|
+
render({ items: makeItems(3) });
|
|
118
|
+
expect(controller.expanded).toBe(false);
|
|
119
|
+
expect(controller.canExpand).toBe(false);
|
|
120
|
+
expect(controller.visibleItems).toHaveLength(3);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("exposes view labels and the host new-chat action without UI assumptions", () => {
|
|
124
|
+
const onNewChat = vi.fn();
|
|
125
|
+
render({ onNewChat });
|
|
126
|
+
|
|
127
|
+
expect(controller.newChatLabel).toBe("New chat");
|
|
128
|
+
act(() => controller.onNewChat());
|
|
129
|
+
expect(onNewChat).toHaveBeenCalledOnce();
|
|
130
|
+
});
|
|
131
|
+
});
|