@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.
Files changed (141) hide show
  1. package/agent-native.eject.json +11 -0
  2. package/dist/agentkit.d.ts +5 -1
  3. package/dist/agentkit.d.ts.map +1 -1
  4. package/dist/agentkit.js +2 -0
  5. package/dist/agentkit.js.map +1 -1
  6. package/dist/app-shell/PromptHome.d.ts +26 -0
  7. package/dist/app-shell/PromptHome.d.ts.map +1 -0
  8. package/dist/app-shell/PromptHome.js +19 -0
  9. package/dist/app-shell/PromptHome.js.map +1 -0
  10. package/dist/app-shell/PromptHome.spec.d.ts +2 -0
  11. package/dist/app-shell/PromptHome.spec.d.ts.map +1 -0
  12. package/dist/app-shell/PromptHome.spec.js +206 -0
  13. package/dist/app-shell/PromptHome.spec.js.map +1 -0
  14. package/dist/app-shell/TemplateLibraryGrid.d.ts +48 -0
  15. package/dist/app-shell/TemplateLibraryGrid.d.ts.map +1 -0
  16. package/dist/app-shell/TemplateLibraryGrid.js +20 -0
  17. package/dist/app-shell/TemplateLibraryGrid.js.map +1 -0
  18. package/dist/app-shell/TemplatePreviewDialog.d.ts +41 -0
  19. package/dist/app-shell/TemplatePreviewDialog.d.ts.map +1 -0
  20. package/dist/app-shell/TemplatePreviewDialog.js +50 -0
  21. package/dist/app-shell/TemplatePreviewDialog.js.map +1 -0
  22. package/dist/app-shell/TemplatePreviewDialog.spec.d.ts +2 -0
  23. package/dist/app-shell/TemplatePreviewDialog.spec.d.ts.map +1 -0
  24. package/dist/app-shell/TemplatePreviewDialog.spec.js +258 -0
  25. package/dist/app-shell/TemplatePreviewDialog.spec.js.map +1 -0
  26. package/dist/app-shell/index.d.ts +3 -0
  27. package/dist/app-shell/index.d.ts.map +1 -1
  28. package/dist/app-shell/index.js +3 -0
  29. package/dist/app-shell/index.js.map +1 -1
  30. package/dist/app-shell/prompt-home.css +159 -0
  31. package/dist/composer/ComposerContextMenu.d.ts +48 -0
  32. package/dist/composer/ComposerContextMenu.d.ts.map +1 -0
  33. package/dist/composer/ComposerContextMenu.js +295 -0
  34. package/dist/composer/ComposerContextMenu.js.map +1 -0
  35. package/dist/composer/ComposerContextMenu.spec.d.ts +2 -0
  36. package/dist/composer/ComposerContextMenu.spec.d.ts.map +1 -0
  37. package/dist/composer/ComposerContextMenu.spec.js +270 -0
  38. package/dist/composer/ComposerContextMenu.spec.js.map +1 -0
  39. package/dist/composer/ComposerContextMenuSearch.d.ts +11 -0
  40. package/dist/composer/ComposerContextMenuSearch.d.ts.map +1 -0
  41. package/dist/composer/ComposerContextMenuSearch.js +42 -0
  42. package/dist/composer/ComposerContextMenuSearch.js.map +1 -0
  43. package/dist/composer/ComposerContextPicker.d.ts +7 -0
  44. package/dist/composer/ComposerContextPicker.d.ts.map +1 -0
  45. package/dist/composer/ComposerContextPicker.js +71 -0
  46. package/dist/composer/ComposerContextPicker.js.map +1 -0
  47. package/dist/composer/ComposerContextPicker.spec.d.ts +2 -0
  48. package/dist/composer/ComposerContextPicker.spec.d.ts.map +1 -0
  49. package/dist/composer/ComposerContextPicker.spec.js +454 -0
  50. package/dist/composer/ComposerContextPicker.spec.js.map +1 -0
  51. package/dist/composer/ComposerContextPickerDialog.d.ts +8 -0
  52. package/dist/composer/ComposerContextPickerDialog.d.ts.map +1 -0
  53. package/dist/composer/ComposerContextPickerDialog.js +101 -0
  54. package/dist/composer/ComposerContextPickerDialog.js.map +1 -0
  55. package/dist/composer/ComposerContextPickerDialog.spec.d.ts +2 -0
  56. package/dist/composer/ComposerContextPickerDialog.spec.d.ts.map +1 -0
  57. package/dist/composer/ComposerContextPickerDialog.spec.js +329 -0
  58. package/dist/composer/ComposerContextPickerDialog.spec.js.map +1 -0
  59. package/dist/composer/ComposerContextSearchInput.d.ts +7 -0
  60. package/dist/composer/ComposerContextSearchInput.d.ts.map +1 -0
  61. package/dist/composer/ComposerContextSearchInput.js +18 -0
  62. package/dist/composer/ComposerContextSearchInput.js.map +1 -0
  63. package/dist/composer/PromptComposer.context.spec.d.ts +2 -0
  64. package/dist/composer/PromptComposer.context.spec.d.ts.map +1 -0
  65. package/dist/composer/PromptComposer.context.spec.js +383 -0
  66. package/dist/composer/PromptComposer.context.spec.js.map +1 -0
  67. package/dist/composer/PromptComposer.d.ts +19 -5
  68. package/dist/composer/PromptComposer.d.ts.map +1 -1
  69. package/dist/composer/PromptComposer.js +30 -25
  70. package/dist/composer/PromptComposer.js.map +1 -1
  71. package/dist/composer/PromptComposer.spec.js +24 -1
  72. package/dist/composer/PromptComposer.spec.js.map +1 -1
  73. package/dist/composer/TiptapComposer.d.ts +13 -2
  74. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  75. package/dist/composer/TiptapComposer.js +89 -32
  76. package/dist/composer/TiptapComposer.js.map +1 -1
  77. package/dist/composer/context-items.d.ts +12 -0
  78. package/dist/composer/context-items.d.ts.map +1 -0
  79. package/dist/composer/context-items.js +46 -0
  80. package/dist/composer/context-items.js.map +1 -0
  81. package/dist/composer/context-items.spec.d.ts +2 -0
  82. package/dist/composer/context-items.spec.d.ts.map +1 -0
  83. package/dist/composer/context-items.spec.js +43 -0
  84. package/dist/composer/context-items.spec.js.map +1 -0
  85. package/dist/composer/index.d.ts +4 -1
  86. package/dist/composer/index.d.ts.map +1 -1
  87. package/dist/composer/index.js +3 -1
  88. package/dist/composer/index.js.map +1 -1
  89. package/dist/composer/runtime-adapters.d.ts +6 -1
  90. package/dist/composer/runtime-adapters.d.ts.map +1 -1
  91. package/dist/composer/runtime-adapters.js.map +1 -1
  92. package/dist/composer/useComposerContextPicker.d.ts +111 -0
  93. package/dist/composer/useComposerContextPicker.d.ts.map +1 -0
  94. package/dist/composer/useComposerContextPicker.js +299 -0
  95. package/dist/composer/useComposerContextPicker.js.map +1 -0
  96. package/dist/design-system/default-adapter.d.ts.map +1 -1
  97. package/dist/design-system/default-adapter.js +13 -7
  98. package/dist/design-system/default-adapter.js.map +1 -1
  99. package/dist/design-system/default-adapter.spec.js +13 -0
  100. package/dist/design-system/default-adapter.spec.js.map +1 -1
  101. package/dist/design-system/types.d.ts +4 -1
  102. package/dist/design-system/types.d.ts.map +1 -1
  103. package/dist/styles.css +1 -0
  104. package/dist/ui/command.d.ts +8 -2
  105. package/dist/ui/command.d.ts.map +1 -1
  106. package/dist/ui/command.js +9 -2
  107. package/dist/ui/command.js.map +1 -1
  108. package/dist/ui/command.spec.js +20 -1
  109. package/dist/ui/command.spec.js.map +1 -1
  110. package/package.json +2 -1
  111. package/src/agentkit.ts +26 -0
  112. package/src/app-shell/PromptHome.spec.tsx +404 -0
  113. package/src/app-shell/PromptHome.tsx +111 -0
  114. package/src/app-shell/TemplateLibraryGrid.tsx +212 -0
  115. package/src/app-shell/TemplatePreviewDialog.spec.tsx +473 -0
  116. package/src/app-shell/TemplatePreviewDialog.tsx +224 -0
  117. package/src/app-shell/index.ts +3 -0
  118. package/src/app-shell/prompt-home.css +159 -0
  119. package/src/composer/ComposerContextMenu.spec.tsx +343 -0
  120. package/src/composer/ComposerContextMenu.tsx +610 -0
  121. package/src/composer/ComposerContextMenuSearch.tsx +69 -0
  122. package/src/composer/ComposerContextPicker.spec.tsx +520 -0
  123. package/src/composer/ComposerContextPicker.tsx +280 -0
  124. package/src/composer/ComposerContextPickerDialog.spec.tsx +405 -0
  125. package/src/composer/ComposerContextPickerDialog.tsx +274 -0
  126. package/src/composer/ComposerContextSearchInput.tsx +48 -0
  127. package/src/composer/PromptComposer.context.spec.tsx +577 -0
  128. package/src/composer/PromptComposer.spec.ts +24 -1
  129. package/src/composer/PromptComposer.tsx +60 -26
  130. package/src/composer/TiptapComposer.tsx +198 -48
  131. package/src/composer/context-items.spec.ts +68 -0
  132. package/src/composer/context-items.ts +72 -0
  133. package/src/composer/index.ts +27 -0
  134. package/src/composer/runtime-adapters.tsx +6 -1
  135. package/src/composer/useComposerContextPicker.ts +441 -0
  136. package/src/design-system/default-adapter.spec.tsx +23 -0
  137. package/src/design-system/default-adapter.tsx +52 -23
  138. package/src/design-system/types.ts +4 -1
  139. package/src/styles.css +1 -0
  140. package/src/ui/command.spec.tsx +33 -1
  141. 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
+ }