@agent-native/toolkit 0.21.3 → 0.22.0-nightly-20260926081002
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/agent-native.eject.json +11 -0
- package/dist/agentkit.d.ts +5 -1
- package/dist/agentkit.d.ts.map +1 -1
- package/dist/agentkit.js +2 -0
- package/dist/agentkit.js.map +1 -1
- package/dist/app-shell/PromptHome.d.ts +26 -0
- package/dist/app-shell/PromptHome.d.ts.map +1 -0
- package/dist/app-shell/PromptHome.js +19 -0
- package/dist/app-shell/PromptHome.js.map +1 -0
- package/dist/app-shell/PromptHome.spec.d.ts +2 -0
- package/dist/app-shell/PromptHome.spec.d.ts.map +1 -0
- package/dist/app-shell/PromptHome.spec.js +206 -0
- package/dist/app-shell/PromptHome.spec.js.map +1 -0
- package/dist/app-shell/TemplateLibraryGrid.d.ts +48 -0
- package/dist/app-shell/TemplateLibraryGrid.d.ts.map +1 -0
- package/dist/app-shell/TemplateLibraryGrid.js +20 -0
- package/dist/app-shell/TemplateLibraryGrid.js.map +1 -0
- package/dist/app-shell/TemplatePreviewDialog.d.ts +41 -0
- package/dist/app-shell/TemplatePreviewDialog.d.ts.map +1 -0
- package/dist/app-shell/TemplatePreviewDialog.js +50 -0
- package/dist/app-shell/TemplatePreviewDialog.js.map +1 -0
- package/dist/app-shell/TemplatePreviewDialog.spec.d.ts +2 -0
- package/dist/app-shell/TemplatePreviewDialog.spec.d.ts.map +1 -0
- package/dist/app-shell/TemplatePreviewDialog.spec.js +258 -0
- package/dist/app-shell/TemplatePreviewDialog.spec.js.map +1 -0
- package/dist/app-shell/index.d.ts +3 -0
- package/dist/app-shell/index.d.ts.map +1 -1
- package/dist/app-shell/index.js +3 -0
- package/dist/app-shell/index.js.map +1 -1
- package/dist/app-shell/prompt-home.css +159 -0
- package/dist/composer/ComposerContextMenu.d.ts +48 -0
- package/dist/composer/ComposerContextMenu.d.ts.map +1 -0
- package/dist/composer/ComposerContextMenu.js +295 -0
- package/dist/composer/ComposerContextMenu.js.map +1 -0
- package/dist/composer/ComposerContextMenu.spec.d.ts +2 -0
- package/dist/composer/ComposerContextMenu.spec.d.ts.map +1 -0
- package/dist/composer/ComposerContextMenu.spec.js +270 -0
- package/dist/composer/ComposerContextMenu.spec.js.map +1 -0
- package/dist/composer/ComposerContextMenuSearch.d.ts +11 -0
- package/dist/composer/ComposerContextMenuSearch.d.ts.map +1 -0
- package/dist/composer/ComposerContextMenuSearch.js +42 -0
- package/dist/composer/ComposerContextMenuSearch.js.map +1 -0
- package/dist/composer/ComposerContextPicker.d.ts +7 -0
- package/dist/composer/ComposerContextPicker.d.ts.map +1 -0
- package/dist/composer/ComposerContextPicker.js +71 -0
- package/dist/composer/ComposerContextPicker.js.map +1 -0
- package/dist/composer/ComposerContextPicker.spec.d.ts +2 -0
- package/dist/composer/ComposerContextPicker.spec.d.ts.map +1 -0
- package/dist/composer/ComposerContextPicker.spec.js +454 -0
- package/dist/composer/ComposerContextPicker.spec.js.map +1 -0
- package/dist/composer/ComposerContextPickerDialog.d.ts +8 -0
- package/dist/composer/ComposerContextPickerDialog.d.ts.map +1 -0
- package/dist/composer/ComposerContextPickerDialog.js +101 -0
- package/dist/composer/ComposerContextPickerDialog.js.map +1 -0
- package/dist/composer/ComposerContextPickerDialog.spec.d.ts +2 -0
- package/dist/composer/ComposerContextPickerDialog.spec.d.ts.map +1 -0
- package/dist/composer/ComposerContextPickerDialog.spec.js +329 -0
- package/dist/composer/ComposerContextPickerDialog.spec.js.map +1 -0
- package/dist/composer/ComposerContextSearchInput.d.ts +7 -0
- package/dist/composer/ComposerContextSearchInput.d.ts.map +1 -0
- package/dist/composer/ComposerContextSearchInput.js +18 -0
- package/dist/composer/ComposerContextSearchInput.js.map +1 -0
- package/dist/composer/PromptComposer.context.spec.d.ts +2 -0
- package/dist/composer/PromptComposer.context.spec.d.ts.map +1 -0
- package/dist/composer/PromptComposer.context.spec.js +383 -0
- package/dist/composer/PromptComposer.context.spec.js.map +1 -0
- package/dist/composer/PromptComposer.d.ts +19 -5
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +30 -25
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/PromptComposer.spec.js +24 -1
- package/dist/composer/PromptComposer.spec.js.map +1 -1
- package/dist/composer/TiptapComposer.d.ts +13 -2
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +89 -32
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/composer/context-items.d.ts +12 -0
- package/dist/composer/context-items.d.ts.map +1 -0
- package/dist/composer/context-items.js +46 -0
- package/dist/composer/context-items.js.map +1 -0
- package/dist/composer/context-items.spec.d.ts +2 -0
- package/dist/composer/context-items.spec.d.ts.map +1 -0
- package/dist/composer/context-items.spec.js +43 -0
- package/dist/composer/context-items.spec.js.map +1 -0
- package/dist/composer/index.d.ts +4 -1
- package/dist/composer/index.d.ts.map +1 -1
- package/dist/composer/index.js +3 -1
- package/dist/composer/index.js.map +1 -1
- package/dist/composer/runtime-adapters.d.ts +6 -1
- package/dist/composer/runtime-adapters.d.ts.map +1 -1
- package/dist/composer/runtime-adapters.js.map +1 -1
- package/dist/composer/useComposerContextPicker.d.ts +111 -0
- package/dist/composer/useComposerContextPicker.d.ts.map +1 -0
- package/dist/composer/useComposerContextPicker.js +299 -0
- package/dist/composer/useComposerContextPicker.js.map +1 -0
- package/dist/design-system/default-adapter.d.ts.map +1 -1
- package/dist/design-system/default-adapter.js +13 -7
- package/dist/design-system/default-adapter.js.map +1 -1
- package/dist/design-system/default-adapter.spec.js +13 -0
- package/dist/design-system/default-adapter.spec.js.map +1 -1
- package/dist/design-system/types.d.ts +4 -1
- package/dist/design-system/types.d.ts.map +1 -1
- package/dist/styles.css +1 -0
- package/dist/ui/command.d.ts +8 -2
- package/dist/ui/command.d.ts.map +1 -1
- package/dist/ui/command.js +9 -2
- package/dist/ui/command.js.map +1 -1
- package/dist/ui/command.spec.js +20 -1
- package/dist/ui/command.spec.js.map +1 -1
- package/package.json +2 -1
- package/src/agentkit.ts +26 -0
- package/src/app-shell/PromptHome.spec.tsx +404 -0
- package/src/app-shell/PromptHome.tsx +111 -0
- package/src/app-shell/TemplateLibraryGrid.tsx +212 -0
- package/src/app-shell/TemplatePreviewDialog.spec.tsx +473 -0
- package/src/app-shell/TemplatePreviewDialog.tsx +224 -0
- package/src/app-shell/index.ts +3 -0
- package/src/app-shell/prompt-home.css +159 -0
- package/src/composer/ComposerContextMenu.spec.tsx +343 -0
- package/src/composer/ComposerContextMenu.tsx +610 -0
- package/src/composer/ComposerContextMenuSearch.tsx +69 -0
- package/src/composer/ComposerContextPicker.spec.tsx +520 -0
- package/src/composer/ComposerContextPicker.tsx +280 -0
- package/src/composer/ComposerContextPickerDialog.spec.tsx +405 -0
- package/src/composer/ComposerContextPickerDialog.tsx +274 -0
- package/src/composer/ComposerContextSearchInput.tsx +48 -0
- package/src/composer/PromptComposer.context.spec.tsx +577 -0
- package/src/composer/PromptComposer.spec.ts +24 -1
- package/src/composer/PromptComposer.tsx +60 -26
- package/src/composer/TiptapComposer.tsx +198 -48
- package/src/composer/context-items.spec.ts +68 -0
- package/src/composer/context-items.ts +72 -0
- package/src/composer/index.ts +27 -0
- package/src/composer/runtime-adapters.tsx +6 -1
- package/src/composer/useComposerContextPicker.ts +441 -0
- package/src/design-system/default-adapter.spec.tsx +23 -0
- package/src/design-system/default-adapter.tsx +52 -23
- package/src/design-system/types.ts +4 -1
- package/src/styles.css +1 -0
- package/src/ui/command.spec.tsx +33 -1
- package/src/ui/command.tsx +52 -9
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { DesignSystemContext } from "@agent-native/toolkit/design-system";
|
|
4
|
+
import { act, useState, type ReactNode } from "react";
|
|
5
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
|
|
8
|
+
import { PromptHome, PromptHomeLibrary } from "./PromptHome.js";
|
|
9
|
+
import {
|
|
10
|
+
TemplateLibraryCard,
|
|
11
|
+
TemplateLibraryGrid,
|
|
12
|
+
} from "./TemplateLibraryGrid.js";
|
|
13
|
+
|
|
14
|
+
const labels = {
|
|
15
|
+
loading: "Loading templates",
|
|
16
|
+
empty: "No templates",
|
|
17
|
+
retry: "Retry",
|
|
18
|
+
};
|
|
19
|
+
const items = [
|
|
20
|
+
{ id: "one", title: "Pitch", description: "Stored description" },
|
|
21
|
+
{ id: "two", title: "Update" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
describe("prompt home and library", () => {
|
|
25
|
+
let container: HTMLDivElement;
|
|
26
|
+
let root: Root;
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
29
|
+
container = document.createElement("div");
|
|
30
|
+
document.body.appendChild(container);
|
|
31
|
+
root = createRoot(container);
|
|
32
|
+
});
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
act(() => root.unmount());
|
|
35
|
+
container.remove();
|
|
36
|
+
vi.unstubAllGlobals();
|
|
37
|
+
});
|
|
38
|
+
const render = (node: ReactNode) => act(() => root.render(node));
|
|
39
|
+
|
|
40
|
+
it("places the prompt above the full-width library without extra chrome", () => {
|
|
41
|
+
render(
|
|
42
|
+
<PromptHome
|
|
43
|
+
title="Create"
|
|
44
|
+
composer={<textarea defaultValue="Draft" />}
|
|
45
|
+
connection={<button>Connect</button>}
|
|
46
|
+
mobileToolbar={<button>Import</button>}
|
|
47
|
+
quickActions={<button>Start</button>}
|
|
48
|
+
>
|
|
49
|
+
<div data-library>Templates</div>
|
|
50
|
+
</PromptHome>,
|
|
51
|
+
);
|
|
52
|
+
expect(container.querySelectorAll("main")).toHaveLength(0);
|
|
53
|
+
expect(container.querySelector("h2")?.textContent).toBe("Create");
|
|
54
|
+
expect(
|
|
55
|
+
container
|
|
56
|
+
.querySelector(".agent-prompt-home-hero")
|
|
57
|
+
?.nextElementSibling?.hasAttribute("data-library"),
|
|
58
|
+
).toBe(true);
|
|
59
|
+
expect(
|
|
60
|
+
container
|
|
61
|
+
.querySelector("textarea")
|
|
62
|
+
?.closest(".agent-prompt-home-composer"),
|
|
63
|
+
).not.toBeNull();
|
|
64
|
+
expect(container.textContent).not.toContain("Getting Started");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("leaves the main landmark owned by the app shell", () => {
|
|
68
|
+
render(
|
|
69
|
+
<main>
|
|
70
|
+
<PromptHome title="Create" composer={<textarea />} />
|
|
71
|
+
</main>,
|
|
72
|
+
);
|
|
73
|
+
expect(container.querySelectorAll("main")).toHaveLength(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("keeps the existing composer mounted when connection or library changes", () => {
|
|
77
|
+
const home = (connected: boolean) => (
|
|
78
|
+
<PromptHome
|
|
79
|
+
title="Create"
|
|
80
|
+
composer={<textarea defaultValue="Draft" />}
|
|
81
|
+
connection={connected ? undefined : <button>Connect</button>}
|
|
82
|
+
>
|
|
83
|
+
<div>{connected ? "Recent" : "Templates"}</div>
|
|
84
|
+
</PromptHome>
|
|
85
|
+
);
|
|
86
|
+
render(home(false));
|
|
87
|
+
const textarea = container.querySelector("textarea")!;
|
|
88
|
+
textarea.value = "Unsaved user draft";
|
|
89
|
+
render(home(true));
|
|
90
|
+
expect(container.querySelector("textarea")).toBe(textarea);
|
|
91
|
+
expect(textarea.value).toBe("Unsaved user draft");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("shows templates without fake tabs when recents are unavailable", () => {
|
|
95
|
+
const onValueChange = vi.fn();
|
|
96
|
+
render(
|
|
97
|
+
<PromptHomeLibrary
|
|
98
|
+
value="recent"
|
|
99
|
+
onValueChange={onValueChange}
|
|
100
|
+
labels={{ templates: "Templates", recent: "Recent" }}
|
|
101
|
+
showRecent={false}
|
|
102
|
+
browseAll={<a href="/templates">Browse all</a>}
|
|
103
|
+
templates={<div>Real catalog</div>}
|
|
104
|
+
recent={<div>Private history</div>}
|
|
105
|
+
/>,
|
|
106
|
+
);
|
|
107
|
+
expect(container.querySelector('[role="tablist"]')).toBeNull();
|
|
108
|
+
expect(container.textContent).toContain("Real catalog");
|
|
109
|
+
expect(container.textContent).not.toContain("Private history");
|
|
110
|
+
expect(container.querySelector("a")?.getAttribute("href")).toBe(
|
|
111
|
+
"/templates",
|
|
112
|
+
);
|
|
113
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("uses controlled semantic tabs with keyboard selection and active actions", async () => {
|
|
117
|
+
function Library() {
|
|
118
|
+
const [value, setValue] = useState<"templates" | "recent">("templates");
|
|
119
|
+
return (
|
|
120
|
+
<PromptHomeLibrary
|
|
121
|
+
value={value}
|
|
122
|
+
onValueChange={setValue}
|
|
123
|
+
labels={{ templates: "Templates", recent: "Recent" }}
|
|
124
|
+
showRecent
|
|
125
|
+
browseAll={<a href="/templates">Browse all</a>}
|
|
126
|
+
recentActions={<button>Filter</button>}
|
|
127
|
+
templates={<div>Catalog</div>}
|
|
128
|
+
recent={<div>Owned work</div>}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
render(<Library />);
|
|
133
|
+
const tabs = container.querySelectorAll<HTMLElement>('[role="tab"]');
|
|
134
|
+
expect(tabs).toHaveLength(2);
|
|
135
|
+
expect(
|
|
136
|
+
container
|
|
137
|
+
.querySelector('[role="tablist"]')
|
|
138
|
+
?.parentElement?.querySelector('a[href="/templates"]'),
|
|
139
|
+
).not.toBeNull();
|
|
140
|
+
act(() => tabs[0]!.focus());
|
|
141
|
+
await act(async () => {
|
|
142
|
+
tabs[0]!.dispatchEvent(
|
|
143
|
+
new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }),
|
|
144
|
+
);
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
146
|
+
});
|
|
147
|
+
expect(tabs[1]!.getAttribute("aria-selected")).toBe("true");
|
|
148
|
+
expect(container.textContent).toContain("Owned work");
|
|
149
|
+
expect(container.textContent).toContain("Filter");
|
|
150
|
+
expect(container.querySelector('a[href="/templates"]')).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("renders host previews and only stored descriptions", () => {
|
|
154
|
+
render(
|
|
155
|
+
<TemplateLibraryGrid
|
|
156
|
+
items={items}
|
|
157
|
+
labels={labels}
|
|
158
|
+
renderPreview={(item) => <img alt={item.title} src="/preview.png" />}
|
|
159
|
+
onSelect={vi.fn()}
|
|
160
|
+
/>,
|
|
161
|
+
);
|
|
162
|
+
expect(container.querySelectorAll("img")).toHaveLength(2);
|
|
163
|
+
expect(container.querySelectorAll("p")).toHaveLength(1);
|
|
164
|
+
expect(container.querySelector("p")?.textContent).toBe(
|
|
165
|
+
"Stored description",
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("activates image and caption through one focus target while actions stay separate", () => {
|
|
170
|
+
const onSelect = vi.fn();
|
|
171
|
+
const secondary = vi.fn();
|
|
172
|
+
render(
|
|
173
|
+
<TemplateLibraryCard
|
|
174
|
+
item={items[0]!}
|
|
175
|
+
preview={<img alt="Preview" src="/preview.png" />}
|
|
176
|
+
actions={<button onClick={secondary}>Delete</button>}
|
|
177
|
+
onSelect={onSelect}
|
|
178
|
+
/>,
|
|
179
|
+
);
|
|
180
|
+
expect(container.querySelectorAll('[role="button"]')).toHaveLength(1);
|
|
181
|
+
for (const selector of ["img", "h3", "p"])
|
|
182
|
+
act(() => container.querySelector<HTMLElement>(selector)!.click());
|
|
183
|
+
expect(onSelect).toHaveBeenCalledTimes(3);
|
|
184
|
+
act(() => container.querySelector("button")!.click());
|
|
185
|
+
expect(secondary).toHaveBeenCalledOnce();
|
|
186
|
+
expect(onSelect).toHaveBeenCalledTimes(3);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("exposes selected state and supports Enter and Space selection", () => {
|
|
190
|
+
const onSelect = vi.fn();
|
|
191
|
+
render(
|
|
192
|
+
<TemplateLibraryGrid
|
|
193
|
+
items={items}
|
|
194
|
+
labels={labels}
|
|
195
|
+
selectedId="one"
|
|
196
|
+
renderPreview={() => null}
|
|
197
|
+
onSelect={onSelect}
|
|
198
|
+
/>,
|
|
199
|
+
);
|
|
200
|
+
const card = container.querySelector<HTMLElement>('[role="button"]')!;
|
|
201
|
+
act(() => card.focus());
|
|
202
|
+
expect(document.activeElement).toBe(card);
|
|
203
|
+
expect(card.getAttribute("aria-pressed")).toBe("true");
|
|
204
|
+
for (const key of ["Enter", " "])
|
|
205
|
+
act(() =>
|
|
206
|
+
card.dispatchEvent(
|
|
207
|
+
new KeyboardEvent("keydown", { key, bubbles: true }),
|
|
208
|
+
),
|
|
209
|
+
);
|
|
210
|
+
expect(onSelect).toHaveBeenCalledTimes(2);
|
|
211
|
+
expect(onSelect).toHaveBeenLastCalledWith(items[0], card);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it.each(["disabled", "pending", "item-disabled"])(
|
|
215
|
+
"prevents %s card activation",
|
|
216
|
+
(mode) => {
|
|
217
|
+
const onSelect = vi.fn();
|
|
218
|
+
render(
|
|
219
|
+
<TemplateLibraryCard
|
|
220
|
+
item={{ ...items[0]!, disabled: mode === "item-disabled" }}
|
|
221
|
+
preview={null}
|
|
222
|
+
disabled={mode === "disabled"}
|
|
223
|
+
pending={mode === "pending"}
|
|
224
|
+
onSelect={onSelect}
|
|
225
|
+
/>,
|
|
226
|
+
);
|
|
227
|
+
const card = container.querySelector<HTMLElement>('[role="button"]')!;
|
|
228
|
+
expect(card.getAttribute("aria-disabled")).toBe("true");
|
|
229
|
+
act(() => {
|
|
230
|
+
card.click();
|
|
231
|
+
card.dispatchEvent(
|
|
232
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
236
|
+
},
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
it("preserves native link modifier-click and keeps secondary actions outside", () => {
|
|
240
|
+
const action = vi.fn();
|
|
241
|
+
render(
|
|
242
|
+
<TemplateLibraryGrid
|
|
243
|
+
items={[items[0]!]}
|
|
244
|
+
labels={labels}
|
|
245
|
+
renderPreview={() => null}
|
|
246
|
+
renderLink={(item, children) => (
|
|
247
|
+
<a href={`/templates/${item.id}`}>{children}</a>
|
|
248
|
+
)}
|
|
249
|
+
renderActions={() => <button onClick={action}>Delete</button>}
|
|
250
|
+
/>,
|
|
251
|
+
);
|
|
252
|
+
const link = container.querySelector("a")!;
|
|
253
|
+
expect(link.getAttribute("href")).toBe("/templates/one");
|
|
254
|
+
expect(link.querySelector("button")).toBeNull();
|
|
255
|
+
const modified = new MouseEvent("click", {
|
|
256
|
+
ctrlKey: true,
|
|
257
|
+
bubbles: true,
|
|
258
|
+
cancelable: true,
|
|
259
|
+
});
|
|
260
|
+
act(() => link.dispatchEvent(modified));
|
|
261
|
+
expect(modified.defaultPrevented).toBe(false);
|
|
262
|
+
act(() => container.querySelector("button")!.click());
|
|
263
|
+
expect(action).toHaveBeenCalledOnce();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("removes navigation while a link card is pending", () => {
|
|
267
|
+
render(
|
|
268
|
+
<TemplateLibraryCard
|
|
269
|
+
item={items[0]!}
|
|
270
|
+
preview={null}
|
|
271
|
+
pending
|
|
272
|
+
renderLink={(_, children) => <a href="/template">{children}</a>}
|
|
273
|
+
/>,
|
|
274
|
+
);
|
|
275
|
+
expect(container.querySelector("a")).toBeNull();
|
|
276
|
+
expect(
|
|
277
|
+
container.querySelector('[role="link"]')?.getAttribute("aria-disabled"),
|
|
278
|
+
).toBe("true");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("keeps secondary actions outside the disabled primary card semantics", () => {
|
|
282
|
+
const onSelect = vi.fn();
|
|
283
|
+
const onAction = vi.fn();
|
|
284
|
+
render(
|
|
285
|
+
<TemplateLibraryCard
|
|
286
|
+
item={items[0]!}
|
|
287
|
+
preview={null}
|
|
288
|
+
disabled
|
|
289
|
+
onSelect={onSelect}
|
|
290
|
+
actions={<button onClick={onAction}>Manage</button>}
|
|
291
|
+
/>,
|
|
292
|
+
);
|
|
293
|
+
expect(
|
|
294
|
+
container.querySelector('[role="button"]')?.querySelector("button"),
|
|
295
|
+
).toBeNull();
|
|
296
|
+
act(() => container.querySelector("button")!.click());
|
|
297
|
+
expect(onAction).toHaveBeenCalledOnce();
|
|
298
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("makes initial loading, error, and empty mutually exclusive", () => {
|
|
302
|
+
const props = {
|
|
303
|
+
items: [],
|
|
304
|
+
labels,
|
|
305
|
+
renderPreview: () => null,
|
|
306
|
+
onSelect: vi.fn(),
|
|
307
|
+
};
|
|
308
|
+
render(<TemplateLibraryGrid {...props} loading />);
|
|
309
|
+
expect(
|
|
310
|
+
container.querySelector('[role="status"]')?.getAttribute("aria-label"),
|
|
311
|
+
).toBe(labels.loading);
|
|
312
|
+
expect(container.textContent).not.toContain(labels.empty);
|
|
313
|
+
render(<TemplateLibraryGrid {...props} loading error="Access denied" />);
|
|
314
|
+
expect(container.querySelectorAll('[role="alert"]')).toHaveLength(1);
|
|
315
|
+
expect(container.querySelector('[role="status"]')).toBeNull();
|
|
316
|
+
render(<TemplateLibraryGrid {...props} />);
|
|
317
|
+
expect(container.querySelector('[role="status"]')?.textContent).toBe(
|
|
318
|
+
labels.empty,
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("retains loaded cards after a refresh failure and exposes retry", () => {
|
|
323
|
+
const onRetry = vi.fn();
|
|
324
|
+
render(
|
|
325
|
+
<TemplateLibraryGrid
|
|
326
|
+
items={items}
|
|
327
|
+
labels={labels}
|
|
328
|
+
error="Refresh failed"
|
|
329
|
+
onRetry={onRetry}
|
|
330
|
+
renderPreview={() => null}
|
|
331
|
+
onSelect={vi.fn()}
|
|
332
|
+
/>,
|
|
333
|
+
);
|
|
334
|
+
expect(container.querySelectorAll("h3")).toHaveLength(2);
|
|
335
|
+
act(() => container.querySelector("button")!.click());
|
|
336
|
+
expect(onRetry).toHaveBeenCalledOnce();
|
|
337
|
+
expect(container.querySelector('[role="status"]')).toBeNull();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("honors a non-Tailwind semantic Surface, Tabs, and ActionButton adapter", () => {
|
|
341
|
+
const surface = vi.fn();
|
|
342
|
+
const onSelect = vi.fn();
|
|
343
|
+
render(
|
|
344
|
+
<DesignSystemContext.Provider
|
|
345
|
+
value={{
|
|
346
|
+
definition: {
|
|
347
|
+
components: {
|
|
348
|
+
Surface: ({ children, interactive, onPress }) => {
|
|
349
|
+
surface();
|
|
350
|
+
return interactive ? (
|
|
351
|
+
<button data-company-surface onClick={() => onPress?.()}>
|
|
352
|
+
{children}
|
|
353
|
+
</button>
|
|
354
|
+
) : (
|
|
355
|
+
<article data-company-surface>{children}</article>
|
|
356
|
+
);
|
|
357
|
+
},
|
|
358
|
+
ActionButton: ({ children, onPress }) => (
|
|
359
|
+
<button data-company-action onClick={() => onPress?.()}>
|
|
360
|
+
{children}
|
|
361
|
+
</button>
|
|
362
|
+
),
|
|
363
|
+
Tabs: ({ items: tabs, value, onChange }) => (
|
|
364
|
+
<section data-company-tabs>
|
|
365
|
+
{tabs.map((tab) => (
|
|
366
|
+
<button key={tab.value} onClick={() => onChange(tab.value)}>
|
|
367
|
+
{tab.label}
|
|
368
|
+
</button>
|
|
369
|
+
))}
|
|
370
|
+
{tabs.find((tab) => tab.value === value)?.content}
|
|
371
|
+
</section>
|
|
372
|
+
),
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
}}
|
|
376
|
+
>
|
|
377
|
+
<PromptHomeLibrary
|
|
378
|
+
value="templates"
|
|
379
|
+
onValueChange={vi.fn()}
|
|
380
|
+
labels={{ templates: "Templates", recent: "Recent" }}
|
|
381
|
+
showRecent
|
|
382
|
+
templates={
|
|
383
|
+
<TemplateLibraryGrid
|
|
384
|
+
items={items}
|
|
385
|
+
labels={labels}
|
|
386
|
+
error="Refresh failed"
|
|
387
|
+
onRetry={vi.fn()}
|
|
388
|
+
renderPreview={() => null}
|
|
389
|
+
onSelect={onSelect}
|
|
390
|
+
/>
|
|
391
|
+
}
|
|
392
|
+
/>
|
|
393
|
+
</DesignSystemContext.Provider>,
|
|
394
|
+
);
|
|
395
|
+
expect(container.querySelector("[data-company-tabs]")).not.toBeNull();
|
|
396
|
+
expect(container.querySelector("[data-company-action]")).not.toBeNull();
|
|
397
|
+
const card = container.querySelector<HTMLButtonElement>(
|
|
398
|
+
"[data-company-surface]",
|
|
399
|
+
)!;
|
|
400
|
+
act(() => card.click());
|
|
401
|
+
expect(onSelect).toHaveBeenCalledWith(items[0], expect.any(HTMLElement));
|
|
402
|
+
expect(surface).toHaveBeenCalled();
|
|
403
|
+
});
|
|
404
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Tabs } from "@agent-native/toolkit/design-system";
|
|
2
|
+
import { useId, type ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export interface PromptHomeProps {
|
|
5
|
+
title: ReactNode;
|
|
6
|
+
composer: ReactNode;
|
|
7
|
+
mobileToolbar?: ReactNode;
|
|
8
|
+
connection?: ReactNode;
|
|
9
|
+
quickActions?: ReactNode;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function PromptHome({
|
|
14
|
+
title,
|
|
15
|
+
composer,
|
|
16
|
+
mobileToolbar,
|
|
17
|
+
connection,
|
|
18
|
+
quickActions,
|
|
19
|
+
children,
|
|
20
|
+
}: PromptHomeProps) {
|
|
21
|
+
const titleId = useId();
|
|
22
|
+
return (
|
|
23
|
+
<div className="agent-prompt-home mx-auto w-full min-w-0 px-4 pb-14 sm:px-6 lg:px-8">
|
|
24
|
+
{mobileToolbar ? (
|
|
25
|
+
<div className="agent-prompt-home-composer mx-auto flex items-center gap-2 pt-3 md:hidden">
|
|
26
|
+
{mobileToolbar}
|
|
27
|
+
</div>
|
|
28
|
+
) : null}
|
|
29
|
+
<section className="agent-prompt-home-hero" aria-labelledby={titleId}>
|
|
30
|
+
{connection ? (
|
|
31
|
+
<div className="absolute top-7 flex flex-col items-center gap-2">
|
|
32
|
+
{connection}
|
|
33
|
+
</div>
|
|
34
|
+
) : null}
|
|
35
|
+
<h2
|
|
36
|
+
id={titleId}
|
|
37
|
+
className="text-2xl font-semibold tracking-tight text-foreground"
|
|
38
|
+
>
|
|
39
|
+
{title}
|
|
40
|
+
</h2>
|
|
41
|
+
<div className="agent-prompt-home-composer mt-4 text-start">
|
|
42
|
+
{composer}
|
|
43
|
+
</div>
|
|
44
|
+
{quickActions ? (
|
|
45
|
+
<div className="agent-prompt-home-composer mt-4 flex flex-wrap justify-center gap-2">
|
|
46
|
+
{quickActions}
|
|
47
|
+
</div>
|
|
48
|
+
) : null}
|
|
49
|
+
</section>
|
|
50
|
+
{children}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type PromptHomeLibraryTab = "templates" | "recent";
|
|
56
|
+
|
|
57
|
+
export interface PromptHomeLibraryProps {
|
|
58
|
+
value: PromptHomeLibraryTab;
|
|
59
|
+
onValueChange: (value: PromptHomeLibraryTab) => void;
|
|
60
|
+
labels: { templates: string; recent: string };
|
|
61
|
+
showRecent: boolean;
|
|
62
|
+
browseAll?: ReactNode;
|
|
63
|
+
recentActions?: ReactNode;
|
|
64
|
+
templates: ReactNode;
|
|
65
|
+
recent?: ReactNode;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function PromptHomeLibrary({
|
|
69
|
+
value,
|
|
70
|
+
onValueChange,
|
|
71
|
+
labels,
|
|
72
|
+
showRecent,
|
|
73
|
+
browseAll,
|
|
74
|
+
recentActions,
|
|
75
|
+
templates,
|
|
76
|
+
recent,
|
|
77
|
+
}: PromptHomeLibraryProps) {
|
|
78
|
+
const titleId = useId();
|
|
79
|
+
return (
|
|
80
|
+
<section
|
|
81
|
+
className="agent-prompt-home-library"
|
|
82
|
+
aria-label={labels.templates}
|
|
83
|
+
>
|
|
84
|
+
{showRecent ? (
|
|
85
|
+
<Tabs<PromptHomeLibraryTab>
|
|
86
|
+
value={value}
|
|
87
|
+
onChange={onValueChange}
|
|
88
|
+
headerActions={value === "templates" ? browseAll : recentActions}
|
|
89
|
+
items={[
|
|
90
|
+
{ value: "templates", label: labels.templates, content: templates },
|
|
91
|
+
{
|
|
92
|
+
value: "recent",
|
|
93
|
+
label: labels.recent,
|
|
94
|
+
content: recent,
|
|
95
|
+
},
|
|
96
|
+
]}
|
|
97
|
+
/>
|
|
98
|
+
) : (
|
|
99
|
+
<div className="grid gap-4" aria-labelledby={titleId}>
|
|
100
|
+
<div className="flex items-center justify-between gap-4">
|
|
101
|
+
<h2 id={titleId} className="text-sm font-semibold">
|
|
102
|
+
{labels.templates}
|
|
103
|
+
</h2>
|
|
104
|
+
{browseAll}
|
|
105
|
+
</div>
|
|
106
|
+
{templates}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</section>
|
|
110
|
+
);
|
|
111
|
+
}
|