@agent-native/toolkit 0.21.3 → 0.22.0-nightly-20260926111339
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 -20
- 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/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.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 -15
- 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/index.ts +0 -1
- package/src/styles.css +1 -0
- package/src/ui/command.spec.tsx +33 -1
- package/src/ui/command.tsx +52 -9
- package/dist/marketing/MarketingHome.d.ts +0 -36
- package/dist/marketing/MarketingHome.d.ts.map +0 -1
- package/dist/marketing/MarketingHome.js +0 -28
- package/dist/marketing/MarketingHome.js.map +0 -1
- package/dist/marketing/MarketingHome.spec.d.ts +0 -2
- package/dist/marketing/MarketingHome.spec.d.ts.map +0 -1
- package/dist/marketing/MarketingHome.spec.js +0 -44
- package/dist/marketing/MarketingHome.spec.js.map +0 -1
- package/dist/marketing/Starfield.d.ts +0 -8
- package/dist/marketing/Starfield.d.ts.map +0 -1
- package/dist/marketing/Starfield.js +0 -349
- package/dist/marketing/Starfield.js.map +0 -1
- package/dist/marketing/index.d.ts +0 -3
- package/dist/marketing/index.d.ts.map +0 -1
- package/dist/marketing/index.js +0 -3
- package/dist/marketing/index.js.map +0 -1
- package/src/marketing/MarketingHome.spec.tsx +0 -75
- package/src/marketing/MarketingHome.tsx +0 -183
- package/src/marketing/Starfield.tsx +0 -403
- package/src/marketing/index.ts +0 -6
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActionButton,
|
|
3
|
+
Skeleton,
|
|
4
|
+
Surface,
|
|
5
|
+
} from "@agent-native/toolkit/design-system";
|
|
6
|
+
import { useId, useRef, type ReactElement, type ReactNode } from "react";
|
|
7
|
+
|
|
8
|
+
export interface TemplateLibraryItem {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
description?: string | null;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type TemplateLibraryActivation<T extends TemplateLibraryItem> =
|
|
16
|
+
| {
|
|
17
|
+
onSelect: (item: T, element: HTMLElement) => void;
|
|
18
|
+
renderLink?: never;
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
renderLink: (item: T, children: ReactNode) => ReactElement;
|
|
22
|
+
onSelect?: never;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type TemplateLibraryCardProps<T extends TemplateLibraryItem> = {
|
|
26
|
+
item: T;
|
|
27
|
+
preview: ReactNode;
|
|
28
|
+
actions?: ReactNode;
|
|
29
|
+
metadata?: ReactNode;
|
|
30
|
+
actionsVisible?: boolean;
|
|
31
|
+
selected?: boolean;
|
|
32
|
+
pending?: boolean;
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
} & TemplateLibraryActivation<T>;
|
|
35
|
+
|
|
36
|
+
export function TemplateLibraryCard<T extends TemplateLibraryItem>({
|
|
37
|
+
item,
|
|
38
|
+
preview,
|
|
39
|
+
actions,
|
|
40
|
+
metadata,
|
|
41
|
+
actionsVisible = false,
|
|
42
|
+
selected = false,
|
|
43
|
+
pending = false,
|
|
44
|
+
disabled = false,
|
|
45
|
+
onSelect,
|
|
46
|
+
renderLink,
|
|
47
|
+
}: TemplateLibraryCardProps<T>) {
|
|
48
|
+
const titleId = useId();
|
|
49
|
+
const container = useRef<HTMLDivElement>(null);
|
|
50
|
+
const unavailable = disabled || item.disabled || pending;
|
|
51
|
+
const card = (
|
|
52
|
+
<Surface
|
|
53
|
+
padding="none"
|
|
54
|
+
elevation="none"
|
|
55
|
+
interactive={Boolean(onSelect) && !unavailable}
|
|
56
|
+
aria-labelledby={titleId}
|
|
57
|
+
aria-pressed={onSelect && !unavailable ? selected : undefined}
|
|
58
|
+
onPress={
|
|
59
|
+
onSelect && !unavailable
|
|
60
|
+
? (event) => {
|
|
61
|
+
const element = event?.currentTarget ?? container.current;
|
|
62
|
+
if (element) onSelect(item, element);
|
|
63
|
+
}
|
|
64
|
+
: undefined
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
<Surface padding="none">
|
|
68
|
+
<div className="agent-template-library-preview">{preview}</div>
|
|
69
|
+
</Surface>
|
|
70
|
+
<div className="agent-template-library-caption grid content-start gap-1 pt-3">
|
|
71
|
+
<h3 id={titleId} className="truncate text-sm font-medium">
|
|
72
|
+
{item.title}
|
|
73
|
+
</h3>
|
|
74
|
+
{item.description ? (
|
|
75
|
+
<p className="line-clamp-2 text-sm text-muted-foreground">
|
|
76
|
+
{item.description}
|
|
77
|
+
</p>
|
|
78
|
+
) : null}
|
|
79
|
+
{metadata ? (
|
|
80
|
+
<div className="min-w-0 text-xs text-muted-foreground">
|
|
81
|
+
{metadata}
|
|
82
|
+
</div>
|
|
83
|
+
) : null}
|
|
84
|
+
</div>
|
|
85
|
+
</Surface>
|
|
86
|
+
);
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
ref={container}
|
|
90
|
+
className="agent-template-library-card min-w-0"
|
|
91
|
+
data-selected={selected || undefined}
|
|
92
|
+
aria-busy={pending || undefined}
|
|
93
|
+
>
|
|
94
|
+
<div
|
|
95
|
+
className="agent-template-library-primary"
|
|
96
|
+
role={unavailable ? (renderLink ? "link" : "button") : undefined}
|
|
97
|
+
aria-pressed={unavailable && onSelect ? selected : undefined}
|
|
98
|
+
aria-disabled={unavailable || undefined}
|
|
99
|
+
>
|
|
100
|
+
{renderLink && !unavailable ? renderLink(item, card) : card}
|
|
101
|
+
</div>
|
|
102
|
+
{actions ? (
|
|
103
|
+
<div
|
|
104
|
+
className={`agent-template-library-actions flex items-center gap-2 pt-3${
|
|
105
|
+
actionsVisible ? " !opacity-100" : ""
|
|
106
|
+
}`}
|
|
107
|
+
>
|
|
108
|
+
{actions}
|
|
109
|
+
</div>
|
|
110
|
+
) : null}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type TemplateLibraryGridProps<T extends TemplateLibraryItem> = {
|
|
116
|
+
items: readonly T[];
|
|
117
|
+
renderPreview: (item: T) => ReactNode;
|
|
118
|
+
renderActions?: (item: T) => ReactNode;
|
|
119
|
+
renderMetadata?: (item: T) => ReactNode;
|
|
120
|
+
actionsVisible?: (item: T) => boolean;
|
|
121
|
+
isSelected?: (item: T) => boolean;
|
|
122
|
+
selectedId?: string | null;
|
|
123
|
+
pendingId?: string | null;
|
|
124
|
+
disabled?: boolean;
|
|
125
|
+
loading?: boolean;
|
|
126
|
+
error?: string | null;
|
|
127
|
+
onRetry?: () => void;
|
|
128
|
+
labels: { loading: string; empty: string; retry: string };
|
|
129
|
+
empty?: ReactNode;
|
|
130
|
+
} & TemplateLibraryActivation<T>;
|
|
131
|
+
|
|
132
|
+
export function TemplateLibraryGrid<T extends TemplateLibraryItem>({
|
|
133
|
+
items,
|
|
134
|
+
renderPreview,
|
|
135
|
+
renderActions,
|
|
136
|
+
renderMetadata,
|
|
137
|
+
actionsVisible,
|
|
138
|
+
isSelected,
|
|
139
|
+
selectedId,
|
|
140
|
+
pendingId,
|
|
141
|
+
disabled,
|
|
142
|
+
loading = false,
|
|
143
|
+
error,
|
|
144
|
+
onRetry,
|
|
145
|
+
labels,
|
|
146
|
+
empty,
|
|
147
|
+
onSelect,
|
|
148
|
+
renderLink,
|
|
149
|
+
}: TemplateLibraryGridProps<T>) {
|
|
150
|
+
return (
|
|
151
|
+
<div className="grid gap-4" aria-busy={loading || undefined}>
|
|
152
|
+
{error ? (
|
|
153
|
+
<div className="flex flex-wrap items-center gap-3" role="alert">
|
|
154
|
+
<span className="text-sm text-destructive">{error}</span>
|
|
155
|
+
{onRetry ? (
|
|
156
|
+
<ActionButton
|
|
157
|
+
emphasis="outline"
|
|
158
|
+
disabled={loading}
|
|
159
|
+
onPress={onRetry}
|
|
160
|
+
>
|
|
161
|
+
{labels.retry}
|
|
162
|
+
</ActionButton>
|
|
163
|
+
) : null}
|
|
164
|
+
</div>
|
|
165
|
+
) : null}
|
|
166
|
+
{items.length ? (
|
|
167
|
+
<div className="agent-template-library-grid">
|
|
168
|
+
{items.map((item) => (
|
|
169
|
+
<TemplateLibraryCard
|
|
170
|
+
key={item.id}
|
|
171
|
+
item={item}
|
|
172
|
+
preview={renderPreview(item)}
|
|
173
|
+
actions={renderActions?.(item)}
|
|
174
|
+
metadata={renderMetadata?.(item)}
|
|
175
|
+
actionsVisible={actionsVisible?.(item)}
|
|
176
|
+
selected={isSelected?.(item) ?? item.id === selectedId}
|
|
177
|
+
pending={item.id === pendingId}
|
|
178
|
+
disabled={disabled}
|
|
179
|
+
{...(renderLink ? { renderLink } : { onSelect: onSelect! })}
|
|
180
|
+
/>
|
|
181
|
+
))}
|
|
182
|
+
</div>
|
|
183
|
+
) : error ? null : loading ? (
|
|
184
|
+
<div
|
|
185
|
+
className="agent-template-library-grid"
|
|
186
|
+
role="status"
|
|
187
|
+
aria-label={labels.loading}
|
|
188
|
+
>
|
|
189
|
+
{Array.from({ length: 4 }, (_, index) => (
|
|
190
|
+
<div key={index}>
|
|
191
|
+
<Surface padding="none">
|
|
192
|
+
<div className="agent-template-library-preview">
|
|
193
|
+
<Skeleton shape="rectangle" width="100%" height="100%" />
|
|
194
|
+
</div>
|
|
195
|
+
</Surface>
|
|
196
|
+
<div className="pt-3">
|
|
197
|
+
<Skeleton width="70%" height="1.25rem" />
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
))}
|
|
201
|
+
</div>
|
|
202
|
+
) : (
|
|
203
|
+
<div
|
|
204
|
+
role="status"
|
|
205
|
+
className="py-8 text-center text-sm text-muted-foreground"
|
|
206
|
+
>
|
|
207
|
+
{empty ?? labels.empty}
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DesignSystemContext,
|
|
5
|
+
Dialog,
|
|
6
|
+
} from "@agent-native/toolkit/design-system";
|
|
7
|
+
import { act, useRef, useState, type ReactNode } from "react";
|
|
8
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
DropdownMenu,
|
|
13
|
+
DropdownMenuContent,
|
|
14
|
+
DropdownMenuGroup,
|
|
15
|
+
DropdownMenuItem,
|
|
16
|
+
DropdownMenuTrigger,
|
|
17
|
+
} from "../ui/dropdown-menu.js";
|
|
18
|
+
import { TemplateLibraryCard } from "./TemplateLibraryGrid.js";
|
|
19
|
+
import { TemplatePreviewDialog } from "./TemplatePreviewDialog.js";
|
|
20
|
+
|
|
21
|
+
const labels = {
|
|
22
|
+
close: "Close preview",
|
|
23
|
+
useTemplate: "Use template",
|
|
24
|
+
loading: "Loading preview",
|
|
25
|
+
empty: "No preview available",
|
|
26
|
+
retry: "Retry",
|
|
27
|
+
thumbnails: "Slides",
|
|
28
|
+
};
|
|
29
|
+
const thumbnails = [
|
|
30
|
+
{ id: "one", title: "Overview", preview: <img alt="" src="/one.png" /> },
|
|
31
|
+
{ id: "two", title: "Unavailable", preview: null, disabled: true },
|
|
32
|
+
{ id: "three", title: "Details", preview: <img alt="" src="/three.png" /> },
|
|
33
|
+
];
|
|
34
|
+
const props = { open: true, onOpenChange: vi.fn(), title: "Pitch", labels };
|
|
35
|
+
|
|
36
|
+
describe("template preview dialog", () => {
|
|
37
|
+
let container: HTMLDivElement;
|
|
38
|
+
let root: Root;
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
41
|
+
container = document.createElement("div");
|
|
42
|
+
document.body.appendChild(container);
|
|
43
|
+
root = createRoot(container);
|
|
44
|
+
});
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
act(() => root.unmount());
|
|
47
|
+
container.remove();
|
|
48
|
+
vi.restoreAllMocks();
|
|
49
|
+
vi.unstubAllGlobals();
|
|
50
|
+
});
|
|
51
|
+
const render = async (node: ReactNode) => {
|
|
52
|
+
await act(async () => {
|
|
53
|
+
root.render(node);
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
const button = (label: string) =>
|
|
58
|
+
[...document.querySelectorAll<HTMLElement>("button, [role=button]")].find(
|
|
59
|
+
(element) =>
|
|
60
|
+
element.getAttribute("aria-label") === label ||
|
|
61
|
+
element.textContent === label,
|
|
62
|
+
)!;
|
|
63
|
+
const key = (element: HTMLElement, value: string) =>
|
|
64
|
+
act(() =>
|
|
65
|
+
element.dispatchEvent(
|
|
66
|
+
new KeyboardEvent("keydown", {
|
|
67
|
+
key: value,
|
|
68
|
+
bubbles: true,
|
|
69
|
+
cancelable: true,
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
it("opens a title-only semantic dialog without a dangling description", async () => {
|
|
75
|
+
const warning = vi.spyOn(console, "warn");
|
|
76
|
+
await render(
|
|
77
|
+
<TemplatePreviewDialog {...props} open={false}>
|
|
78
|
+
<div>Real preview</div>
|
|
79
|
+
</TemplatePreviewDialog>,
|
|
80
|
+
);
|
|
81
|
+
expect(document.querySelector('[role="dialog"]')).toBeNull();
|
|
82
|
+
await render(
|
|
83
|
+
<TemplatePreviewDialog {...props}>
|
|
84
|
+
<div>Real preview</div>
|
|
85
|
+
</TemplatePreviewDialog>,
|
|
86
|
+
);
|
|
87
|
+
const dialog = document.querySelector('[role="dialog"]')!;
|
|
88
|
+
expect(dialog.getAttribute("data-size")).toBe("viewport");
|
|
89
|
+
expect(
|
|
90
|
+
document.getElementById(dialog.getAttribute("aria-labelledby")!)
|
|
91
|
+
?.textContent,
|
|
92
|
+
).toBe("Pitch");
|
|
93
|
+
expect(dialog.hasAttribute("aria-describedby")).toBe(false);
|
|
94
|
+
expect(dialog.textContent).toContain("Real preview");
|
|
95
|
+
expect(dialog.querySelector("nav")).toBeNull();
|
|
96
|
+
expect(
|
|
97
|
+
[...dialog.querySelectorAll("button")].map(
|
|
98
|
+
(element) => element.textContent,
|
|
99
|
+
),
|
|
100
|
+
).toEqual([labels.close]);
|
|
101
|
+
expect(warning).not.toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("replaces the close control with the primary use action without disabling click-away dismissal", async () => {
|
|
105
|
+
const useTemplate = vi.fn();
|
|
106
|
+
await render(
|
|
107
|
+
<TemplatePreviewDialog {...props} onUseTemplate={useTemplate}>
|
|
108
|
+
<div>Real preview</div>
|
|
109
|
+
</TemplatePreviewDialog>,
|
|
110
|
+
);
|
|
111
|
+
expect(button(labels.useTemplate)).toBeTruthy();
|
|
112
|
+
expect(
|
|
113
|
+
document.querySelector(`button[aria-label="${labels.close}"]`),
|
|
114
|
+
).toBeNull();
|
|
115
|
+
act(() => button(labels.useTemplate).click());
|
|
116
|
+
expect(useTemplate).toHaveBeenCalledOnce();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("preserves the semantic description association when supplied", async () => {
|
|
120
|
+
await render(
|
|
121
|
+
<Dialog
|
|
122
|
+
open
|
|
123
|
+
onOpenChange={vi.fn()}
|
|
124
|
+
title="Preview"
|
|
125
|
+
description="Saved content"
|
|
126
|
+
>
|
|
127
|
+
<div>Content</div>
|
|
128
|
+
</Dialog>,
|
|
129
|
+
);
|
|
130
|
+
const dialog = document.querySelector('[role="dialog"]')!;
|
|
131
|
+
expect(
|
|
132
|
+
document.getElementById(dialog.getAttribute("aria-describedby")!)
|
|
133
|
+
?.textContent,
|
|
134
|
+
).toBe("Saved content");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("keeps direct card activation separate from the caption menu preview handoff", async () => {
|
|
138
|
+
const copy = vi.fn();
|
|
139
|
+
function Harness() {
|
|
140
|
+
const [open, setOpen] = useState(false);
|
|
141
|
+
const pending = useRef(false);
|
|
142
|
+
const trigger = useRef<HTMLButtonElement>(null);
|
|
143
|
+
return (
|
|
144
|
+
<>
|
|
145
|
+
<TemplateLibraryCard
|
|
146
|
+
item={{
|
|
147
|
+
id: "one",
|
|
148
|
+
title: "A long stored template title for a narrow card",
|
|
149
|
+
description: "A stored description beneath the title",
|
|
150
|
+
}}
|
|
151
|
+
preview={<img alt="Template thumbnail" src="/one.png" />}
|
|
152
|
+
onSelect={copy}
|
|
153
|
+
actions={
|
|
154
|
+
<DropdownMenu>
|
|
155
|
+
<DropdownMenuTrigger asChild>
|
|
156
|
+
<button ref={trigger} aria-label="Template options">
|
|
157
|
+
...
|
|
158
|
+
</button>
|
|
159
|
+
</DropdownMenuTrigger>
|
|
160
|
+
<DropdownMenuContent
|
|
161
|
+
onCloseAutoFocus={(event) => {
|
|
162
|
+
if (!pending.current) return;
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
pending.current = false;
|
|
165
|
+
setOpen(true);
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
<DropdownMenuGroup>
|
|
169
|
+
<DropdownMenuItem
|
|
170
|
+
onSelect={() => {
|
|
171
|
+
pending.current = true;
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
Preview
|
|
175
|
+
</DropdownMenuItem>
|
|
176
|
+
</DropdownMenuGroup>
|
|
177
|
+
</DropdownMenuContent>
|
|
178
|
+
</DropdownMenu>
|
|
179
|
+
}
|
|
180
|
+
/>
|
|
181
|
+
<TemplatePreviewDialog
|
|
182
|
+
{...props}
|
|
183
|
+
open={open}
|
|
184
|
+
onOpenChange={setOpen}
|
|
185
|
+
restoreFocusRef={trigger}
|
|
186
|
+
>
|
|
187
|
+
<div>Large rendered content</div>
|
|
188
|
+
</TemplatePreviewDialog>
|
|
189
|
+
</>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
await render(<Harness />);
|
|
193
|
+
act(() => {
|
|
194
|
+
container.querySelector("img")!.click();
|
|
195
|
+
container.querySelector("h3")!.click();
|
|
196
|
+
});
|
|
197
|
+
expect(copy).toHaveBeenCalledTimes(2);
|
|
198
|
+
expect(document.querySelector('[role="dialog"]')).toBeNull();
|
|
199
|
+
expect(container.querySelector("p")?.textContent).toBe(
|
|
200
|
+
"A stored description beneath the title",
|
|
201
|
+
);
|
|
202
|
+
const trigger = button("Template options");
|
|
203
|
+
expect(trigger.closest('[role="button"]')).toBeNull();
|
|
204
|
+
await act(async () => {
|
|
205
|
+
trigger.focus();
|
|
206
|
+
trigger.dispatchEvent(
|
|
207
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
await act(async () =>
|
|
211
|
+
document.querySelector<HTMLElement>('[role="menuitem"]')!.click(),
|
|
212
|
+
);
|
|
213
|
+
await act(async () => {
|
|
214
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
215
|
+
});
|
|
216
|
+
expect(document.querySelector('[role="menu"]')).toBeNull();
|
|
217
|
+
expect(document.querySelector('[role="dialog"]')?.textContent).toContain(
|
|
218
|
+
"Large rendered content",
|
|
219
|
+
);
|
|
220
|
+
expect(copy).toHaveBeenCalledTimes(2);
|
|
221
|
+
await act(async () => button(labels.close).click());
|
|
222
|
+
await act(async () => {
|
|
223
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
224
|
+
});
|
|
225
|
+
expect(document.activeElement).toBe(trigger);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it.each(["Escape", "close"])(
|
|
229
|
+
"restores focus and retains the draft after %s dismissal",
|
|
230
|
+
async (dismissal) => {
|
|
231
|
+
function Harness() {
|
|
232
|
+
const [open, setOpen] = useState(false);
|
|
233
|
+
const trigger = useRef<HTMLButtonElement>(null);
|
|
234
|
+
const initial = useRef<HTMLButtonElement>(null);
|
|
235
|
+
return (
|
|
236
|
+
<>
|
|
237
|
+
<textarea defaultValue="Unsaved prompt" />
|
|
238
|
+
<button ref={trigger} onClick={() => setOpen(true)}>
|
|
239
|
+
Preview
|
|
240
|
+
</button>
|
|
241
|
+
<TemplatePreviewDialog
|
|
242
|
+
{...props}
|
|
243
|
+
open={open}
|
|
244
|
+
onOpenChange={setOpen}
|
|
245
|
+
restoreFocusRef={trigger}
|
|
246
|
+
initialFocusRef={initial}
|
|
247
|
+
>
|
|
248
|
+
<button ref={initial}>Inspect content</button>
|
|
249
|
+
</TemplatePreviewDialog>
|
|
250
|
+
</>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
await render(<Harness />);
|
|
254
|
+
const draft = container.querySelector("textarea")!;
|
|
255
|
+
const trigger = button("Preview");
|
|
256
|
+
await act(async () => {
|
|
257
|
+
trigger.focus();
|
|
258
|
+
trigger.click();
|
|
259
|
+
});
|
|
260
|
+
expect(document.activeElement).toBe(button("Inspect content"));
|
|
261
|
+
await act(async () => {
|
|
262
|
+
if (dismissal === "Escape")
|
|
263
|
+
document.activeElement!.dispatchEvent(
|
|
264
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
|
|
265
|
+
);
|
|
266
|
+
else button(labels.close).click();
|
|
267
|
+
});
|
|
268
|
+
await act(async () => {
|
|
269
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
270
|
+
});
|
|
271
|
+
expect(document.querySelector('[role="dialog"]')).toBeNull();
|
|
272
|
+
expect(document.activeElement).toBe(trigger);
|
|
273
|
+
expect(container.querySelector("textarea")).toBe(draft);
|
|
274
|
+
expect(draft.value).toBe("Unsaved prompt");
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
it("selects rail items with arrows, Home, End, and click while skipping disabled thumbnails", async () => {
|
|
279
|
+
function Harness() {
|
|
280
|
+
const [selectedId, setSelectedId] = useState("one");
|
|
281
|
+
return (
|
|
282
|
+
<TemplatePreviewDialog
|
|
283
|
+
{...props}
|
|
284
|
+
thumbnails={thumbnails}
|
|
285
|
+
selectedId={selectedId}
|
|
286
|
+
onSelectedIdChange={setSelectedId}
|
|
287
|
+
>
|
|
288
|
+
<div data-current>{selectedId}</div>
|
|
289
|
+
</TemplatePreviewDialog>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
await render(<Harness />);
|
|
293
|
+
const first = button("Overview");
|
|
294
|
+
const last = button("Details");
|
|
295
|
+
const disabled = button("Unavailable");
|
|
296
|
+
expect(document.querySelector("nav")?.getAttribute("aria-label")).toBe(
|
|
297
|
+
labels.thumbnails,
|
|
298
|
+
);
|
|
299
|
+
expect(disabled.getAttribute("aria-disabled")).toBe("true");
|
|
300
|
+
act(() => {
|
|
301
|
+
first.focus();
|
|
302
|
+
disabled.click();
|
|
303
|
+
});
|
|
304
|
+
expect(first.getAttribute("aria-pressed")).toBe("true");
|
|
305
|
+
for (const direction of ["ArrowRight", "ArrowDown", "End"]) {
|
|
306
|
+
key(first, direction);
|
|
307
|
+
expect(document.activeElement).toBe(last);
|
|
308
|
+
expect(last.getAttribute("aria-pressed")).toBe("true");
|
|
309
|
+
key(last, "Home");
|
|
310
|
+
expect(document.activeElement).toBe(first);
|
|
311
|
+
}
|
|
312
|
+
key(first, "ArrowLeft");
|
|
313
|
+
expect(document.activeElement).toBe(last);
|
|
314
|
+
key(last, "ArrowUp");
|
|
315
|
+
expect(document.activeElement).toBe(first);
|
|
316
|
+
act(() => last.click());
|
|
317
|
+
expect(document.querySelector("[data-current]")?.textContent).toBe("three");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("keeps rail previews inert and does not intercept keys in main content", async () => {
|
|
321
|
+
const select = vi.fn();
|
|
322
|
+
await render(
|
|
323
|
+
<TemplatePreviewDialog
|
|
324
|
+
{...props}
|
|
325
|
+
thumbnails={thumbnails}
|
|
326
|
+
selectedId="one"
|
|
327
|
+
onSelectedIdChange={select}
|
|
328
|
+
>
|
|
329
|
+
<input aria-label="Preview field" />
|
|
330
|
+
</TemplatePreviewDialog>,
|
|
331
|
+
);
|
|
332
|
+
expect(
|
|
333
|
+
document.querySelector("nav img")?.closest("[inert]"),
|
|
334
|
+
).not.toBeNull();
|
|
335
|
+
const input = document.querySelector("input")!;
|
|
336
|
+
act(() => input.focus());
|
|
337
|
+
key(input, "ArrowRight");
|
|
338
|
+
expect(document.activeElement).toBe(input);
|
|
339
|
+
expect(select).not.toHaveBeenCalled();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("renders exactly one readiness state and hides unready content and rail", async () => {
|
|
343
|
+
const base = {
|
|
344
|
+
...props,
|
|
345
|
+
thumbnails,
|
|
346
|
+
selectedId: "one",
|
|
347
|
+
onSelectedIdChange: vi.fn(),
|
|
348
|
+
};
|
|
349
|
+
await render(
|
|
350
|
+
<TemplatePreviewDialog {...base} loading empty>
|
|
351
|
+
<div>Stale content</div>
|
|
352
|
+
</TemplatePreviewDialog>,
|
|
353
|
+
);
|
|
354
|
+
expect(document.querySelectorAll('[role="status"]')).toHaveLength(1);
|
|
355
|
+
expect(
|
|
356
|
+
document.querySelector('[role="status"]')?.getAttribute("aria-label"),
|
|
357
|
+
).toBe(labels.loading);
|
|
358
|
+
expect(document.body.textContent).not.toContain(labels.empty);
|
|
359
|
+
expect(document.body.textContent).not.toContain("Stale content");
|
|
360
|
+
expect(document.querySelector("nav")).toBeNull();
|
|
361
|
+
await render(
|
|
362
|
+
<TemplatePreviewDialog
|
|
363
|
+
{...base}
|
|
364
|
+
loading
|
|
365
|
+
empty
|
|
366
|
+
error="Preview unavailable"
|
|
367
|
+
>
|
|
368
|
+
<div>Stale content</div>
|
|
369
|
+
</TemplatePreviewDialog>,
|
|
370
|
+
);
|
|
371
|
+
expect(document.querySelectorAll('[role="alert"]')).toHaveLength(1);
|
|
372
|
+
expect(document.querySelector('[role="status"]')).toBeNull();
|
|
373
|
+
await render(
|
|
374
|
+
<TemplatePreviewDialog {...base} empty>
|
|
375
|
+
<div>Stale content</div>
|
|
376
|
+
</TemplatePreviewDialog>,
|
|
377
|
+
);
|
|
378
|
+
expect(document.querySelector('[role="status"]')?.textContent).toBe(
|
|
379
|
+
labels.empty,
|
|
380
|
+
);
|
|
381
|
+
expect(document.querySelector('[role="alert"]')).toBeNull();
|
|
382
|
+
await render(
|
|
383
|
+
<TemplatePreviewDialog {...base}>
|
|
384
|
+
<div>Ready content</div>
|
|
385
|
+
</TemplatePreviewDialog>,
|
|
386
|
+
);
|
|
387
|
+
expect(document.querySelector('[role="status"]')).toBeNull();
|
|
388
|
+
expect(document.body.textContent).toContain("Ready content");
|
|
389
|
+
expect(document.querySelector("nav")).not.toBeNull();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("exposes retry without allowing duplicate pending requests", async () => {
|
|
393
|
+
const retry = vi.fn();
|
|
394
|
+
await render(
|
|
395
|
+
<TemplatePreviewDialog {...props} error="Read failed" onRetry={retry}>
|
|
396
|
+
<div>Content</div>
|
|
397
|
+
</TemplatePreviewDialog>,
|
|
398
|
+
);
|
|
399
|
+
act(() => button(labels.retry).click());
|
|
400
|
+
expect(retry).toHaveBeenCalledOnce();
|
|
401
|
+
await render(
|
|
402
|
+
<TemplatePreviewDialog
|
|
403
|
+
{...props}
|
|
404
|
+
error="Read failed"
|
|
405
|
+
onRetry={retry}
|
|
406
|
+
loading
|
|
407
|
+
>
|
|
408
|
+
<div>Content</div>
|
|
409
|
+
</TemplatePreviewDialog>,
|
|
410
|
+
);
|
|
411
|
+
expect((button(labels.retry) as HTMLButtonElement).disabled).toBe(true);
|
|
412
|
+
act(() => button(labels.retry).click());
|
|
413
|
+
expect(retry).toHaveBeenCalledOnce();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("honors custom semantic Dialog and native-button Surface implementations", async () => {
|
|
417
|
+
const dialog = vi.fn();
|
|
418
|
+
const select = vi.fn();
|
|
419
|
+
await render(
|
|
420
|
+
<DesignSystemContext.Provider
|
|
421
|
+
value={{
|
|
422
|
+
definition: {
|
|
423
|
+
components: {
|
|
424
|
+
Dialog: (value) => {
|
|
425
|
+
dialog(value);
|
|
426
|
+
return value.open ? (
|
|
427
|
+
<section aria-label="Company dialog">
|
|
428
|
+
{value.children}
|
|
429
|
+
</section>
|
|
430
|
+
) : null;
|
|
431
|
+
},
|
|
432
|
+
Surface: ({ children, interactive, onPress, ...aria }) =>
|
|
433
|
+
interactive ? (
|
|
434
|
+
<button
|
|
435
|
+
aria-label={aria["aria-label"]}
|
|
436
|
+
aria-pressed={aria["aria-pressed"]}
|
|
437
|
+
onClick={() => onPress?.()}
|
|
438
|
+
>
|
|
439
|
+
{children}
|
|
440
|
+
</button>
|
|
441
|
+
) : (
|
|
442
|
+
<div>{children}</div>
|
|
443
|
+
),
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
}}
|
|
447
|
+
>
|
|
448
|
+
<TemplatePreviewDialog
|
|
449
|
+
{...props}
|
|
450
|
+
size="large"
|
|
451
|
+
thumbnails={thumbnails}
|
|
452
|
+
selectedId="one"
|
|
453
|
+
onSelectedIdChange={select}
|
|
454
|
+
>
|
|
455
|
+
<div>Rendered design</div>
|
|
456
|
+
</TemplatePreviewDialog>
|
|
457
|
+
</DesignSystemContext.Provider>,
|
|
458
|
+
);
|
|
459
|
+
expect(dialog).toHaveBeenCalledWith(
|
|
460
|
+
expect.objectContaining({
|
|
461
|
+
size: "large",
|
|
462
|
+
title: "Pitch",
|
|
463
|
+
closeLabel: labels.close,
|
|
464
|
+
}),
|
|
465
|
+
);
|
|
466
|
+
expect(dialog.mock.calls[0]![0].footer).toBeUndefined();
|
|
467
|
+
act(() => button("Overview").focus());
|
|
468
|
+
key(button("Overview"), "ArrowRight");
|
|
469
|
+
expect(document.activeElement).toBe(button("Details"));
|
|
470
|
+
expect(select).toHaveBeenCalledWith("three");
|
|
471
|
+
expect(document.body.textContent).toContain("Rendered design");
|
|
472
|
+
});
|
|
473
|
+
});
|