@agent-native/core 0.84.32 → 0.84.35
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +20 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/response-text.ts +10 -1
- package/corpus/core/src/agent/production-agent.ts +23 -20
- package/corpus/core/src/agent/run-manager.ts +46 -1
- package/corpus/core/src/agent/thread-data-builder.ts +15 -2
- package/corpus/core/src/client/code-agent-chat-adapter.ts +8 -1
- package/corpus/core/src/client/sse-event-processor.ts +21 -3
- package/corpus/core/src/client/use-action.ts +5 -1
- package/corpus/core/src/code-agents/transcript-normalizer.ts +31 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +8 -2
- package/corpus/core/src/server/agent-teams.ts +24 -2
- package/corpus/core/src/server/ssr-handler.ts +12 -0
- package/corpus/core/src/vite/action-types-plugin.ts +6 -2
- package/corpus/core/src/vite/client.ts +152 -17
- package/corpus/templates/analytics/changelog/2026-07-01-smoother-chat-streaming.md +6 -0
- package/corpus/templates/design/actions/import-design-source.ts +158 -0
- package/corpus/templates/design/actions/navigate.ts +3 -2
- package/corpus/templates/design/actions/view-screen.ts +1 -1
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +519 -0
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +28 -5
- package/corpus/templates/design/app/i18n/zh-TW.ts +40 -0
- package/corpus/templates/design/app/i18n-data.ts +508 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +27 -0
- package/corpus/templates/design/changelog/2026-07-01-design-can-import-figma-paste-fig-files-and-standalone-html.md +6 -0
- package/corpus/templates/design/package.json +3 -0
- package/corpus/templates/design/server/handlers/import-design-file.ts +168 -0
- package/corpus/templates/design/server/lib/figma-import/clipboard.ts +122 -0
- package/corpus/templates/design/server/lib/figma-import/decode.ts +455 -0
- package/corpus/templates/design/server/lib/figma-import/processor.ts +116 -0
- package/corpus/templates/design/server/lib/figma-import/render-html.ts +397 -0
- package/corpus/templates/design/server/lib/figma-import/types.ts +60 -0
- package/corpus/templates/design/server/lib/import-design-files.ts +314 -0
- package/corpus/templates/design/server/routes/api/import-design-file.post.ts +1 -0
- package/dist/a2a/response-text.d.ts +1 -0
- package/dist/a2a/response-text.d.ts.map +1 -1
- package/dist/a2a/response-text.js +8 -2
- package/dist/a2a/response-text.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +31 -27
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +12 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +32 -1
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/thread-data-builder.d.ts.map +1 -1
- package/dist/agent/thread-data-builder.js +15 -2
- package/dist/agent/thread-data-builder.js.map +1 -1
- package/dist/client/code-agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/code-agent-chat-adapter.js +5 -1
- package/dist/client/code-agent-chat-adapter.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +21 -3
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-action.d.ts +5 -1
- package/dist/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js.map +1 -1
- package/dist/code-agents/transcript-normalizer.d.ts.map +1 -1
- package/dist/code-agents/transcript-normalizer.js +24 -0
- package/dist/code-agents/transcript-normalizer.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +2 -3
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-teams.d.ts.map +1 -1
- package/dist/server/agent-teams.js +17 -2
- package/dist/server/agent-teams.js.map +1 -1
- package/dist/server/ssr-handler.d.ts.map +1 -1
- package/dist/server/ssr-handler.js +7 -0
- package/dist/server/ssr-handler.js.map +1 -1
- package/dist/vite/action-types-plugin.d.ts.map +1 -1
- package/dist/vite/action-types-plugin.js +6 -2
- package/dist/vite/action-types-plugin.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +132 -11
- package/dist/vite/client.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import { appApiPath, useActionMutation, useT } from "@agent-native/core/client";
|
|
2
|
+
import {
|
|
3
|
+
IconBrandFigma,
|
|
4
|
+
IconBrandGithub,
|
|
5
|
+
IconChevronRight,
|
|
6
|
+
IconCircleCheck,
|
|
7
|
+
IconCode,
|
|
8
|
+
IconHtml,
|
|
9
|
+
IconUpload,
|
|
10
|
+
} from "@tabler/icons-react";
|
|
11
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
12
|
+
import {
|
|
13
|
+
useCallback,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
type ClipboardEvent,
|
|
17
|
+
type ReactNode,
|
|
18
|
+
} from "react";
|
|
19
|
+
import { useNavigate } from "react-router";
|
|
20
|
+
import { toast } from "sonner";
|
|
21
|
+
|
|
22
|
+
import { Badge } from "@/components/ui/badge";
|
|
23
|
+
import { Button } from "@/components/ui/button";
|
|
24
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
25
|
+
import { sendToDesignAgentChat } from "@/lib/agent-chat";
|
|
26
|
+
import { cn } from "@/lib/utils";
|
|
27
|
+
|
|
28
|
+
import type { DesignExtensionSlotContext } from "./DesignExtensionsPanel";
|
|
29
|
+
|
|
30
|
+
interface DesignImportPanelProps {
|
|
31
|
+
context: Pick<DesignExtensionSlotContext, "designId" | "viewMode">;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ImportResult {
|
|
35
|
+
designId?: string;
|
|
36
|
+
files?: Array<{ id: string; filename: string }>;
|
|
37
|
+
warnings?: string[];
|
|
38
|
+
error?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type ImportMode = "figma-paste" | "fig-file" | "html";
|
|
42
|
+
|
|
43
|
+
function hasFigmaPayload(html: string): boolean {
|
|
44
|
+
return /\(figmeta\)|\(figma\)|data-metadata=|data-buffer=/i.test(html);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function looksLikeHtml(value: string): boolean {
|
|
48
|
+
return /<(html|body|main|section|div|article|header|footer|button|img)\b/i.test(
|
|
49
|
+
value,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resultSummary(result: ImportResult | undefined, fallback: string) {
|
|
54
|
+
const count = result?.files?.length ?? 0;
|
|
55
|
+
if (count === 0) return fallback;
|
|
56
|
+
if (count === 1) return `Imported ${result!.files![0]!.filename}.`;
|
|
57
|
+
return `Imported ${count} screens.`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function DesignImportPanel({ context }: DesignImportPanelProps) {
|
|
61
|
+
const t = useT();
|
|
62
|
+
const navigate = useNavigate();
|
|
63
|
+
const queryClient = useQueryClient();
|
|
64
|
+
const importSource = useActionMutation("import-design-source");
|
|
65
|
+
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
66
|
+
const htmlFileInputRef = useRef<HTMLInputElement | null>(null);
|
|
67
|
+
const [htmlText, setHtmlText] = useState("");
|
|
68
|
+
const [uploading, setUploading] = useState(false);
|
|
69
|
+
const [activeMode, setActiveMode] = useState<ImportMode | null>(null);
|
|
70
|
+
const [activeUploadName, setActiveUploadName] = useState<string | null>(null);
|
|
71
|
+
const [lastResult, setLastResult] = useState<ImportResult | null>(null);
|
|
72
|
+
|
|
73
|
+
const finishImport = useCallback(
|
|
74
|
+
async (result: ImportResult | undefined, fallback: string) => {
|
|
75
|
+
if (result?.error) throw new Error(result.error);
|
|
76
|
+
setLastResult(result ?? null);
|
|
77
|
+
await Promise.all([
|
|
78
|
+
queryClient.invalidateQueries({ queryKey: ["action", "get-design"] }),
|
|
79
|
+
queryClient.invalidateQueries({ queryKey: ["action"] }),
|
|
80
|
+
]);
|
|
81
|
+
toast.success(resultSummary(result, fallback));
|
|
82
|
+
if (result?.warnings?.length) {
|
|
83
|
+
toast.warning(t("designEditor.import.warningsToast"), {
|
|
84
|
+
description: result.warnings[0],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
navigate(`/design/${result?.designId ?? context.designId}?view=overview`);
|
|
88
|
+
},
|
|
89
|
+
[context.designId, navigate, queryClient, t],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const importHtmlString = useCallback(
|
|
93
|
+
(content: string, originalName?: string) => {
|
|
94
|
+
if (!looksLikeHtml(content)) {
|
|
95
|
+
toast.error(t("designEditor.import.errors.notHtml"));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
importSource.mutate(
|
|
99
|
+
{
|
|
100
|
+
designId: context.designId,
|
|
101
|
+
sourceType: "html-string",
|
|
102
|
+
content,
|
|
103
|
+
originalName,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
onSuccess: (result: unknown) => {
|
|
107
|
+
void finishImport(
|
|
108
|
+
result as ImportResult,
|
|
109
|
+
t("designEditor.import.htmlSuccess"),
|
|
110
|
+
);
|
|
111
|
+
},
|
|
112
|
+
onError: (error: unknown) => {
|
|
113
|
+
toast.error(t("designEditor.import.errors.importFailed"), {
|
|
114
|
+
description:
|
|
115
|
+
error instanceof Error
|
|
116
|
+
? error.message
|
|
117
|
+
: t("common.genericError"),
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
[context.designId, finishImport, importSource, t],
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const handlePaste = useCallback(
|
|
127
|
+
(event: ClipboardEvent<HTMLDivElement>) => {
|
|
128
|
+
const html = event.clipboardData.getData("text/html");
|
|
129
|
+
const text = event.clipboardData.getData("text/plain");
|
|
130
|
+
const content = html || text;
|
|
131
|
+
if (!content) return;
|
|
132
|
+
if (hasFigmaPayload(content)) {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
importSource.mutate(
|
|
135
|
+
{
|
|
136
|
+
designId: context.designId,
|
|
137
|
+
sourceType: "figma-paste-html",
|
|
138
|
+
content,
|
|
139
|
+
originalName: "figma-paste.html",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
onSuccess: (result: unknown) => {
|
|
143
|
+
void finishImport(
|
|
144
|
+
result as ImportResult,
|
|
145
|
+
t("designEditor.import.figmaSuccess"),
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
onError: (error: unknown) => {
|
|
149
|
+
toast.error(t("designEditor.import.errors.figmaPasteFailed"), {
|
|
150
|
+
description:
|
|
151
|
+
error instanceof Error
|
|
152
|
+
? error.message
|
|
153
|
+
: t("common.genericError"),
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (looksLikeHtml(content)) {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
importHtmlString(content, "pasted-html.html");
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
[context.designId, finishImport, importHtmlString, importSource, t],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const uploadFile = useCallback(
|
|
169
|
+
async (file: File) => {
|
|
170
|
+
setUploading(true);
|
|
171
|
+
setActiveUploadName(file.name);
|
|
172
|
+
const body = new FormData();
|
|
173
|
+
body.append("designId", context.designId);
|
|
174
|
+
body.append("file", file);
|
|
175
|
+
try {
|
|
176
|
+
const response = await fetch(
|
|
177
|
+
appApiPath(
|
|
178
|
+
`/api/import-design-file?designId=${encodeURIComponent(context.designId)}`,
|
|
179
|
+
),
|
|
180
|
+
{
|
|
181
|
+
method: "POST",
|
|
182
|
+
body,
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
const result = (await response.json()) as ImportResult;
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
result.error || t("designEditor.import.errors.uploadFailed"),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
await finishImport(result, t("designEditor.import.uploadSuccess"));
|
|
192
|
+
} catch (error) {
|
|
193
|
+
toast.error(t("designEditor.import.errors.uploadFailed"), {
|
|
194
|
+
description:
|
|
195
|
+
error instanceof Error ? error.message : t("common.genericError"),
|
|
196
|
+
});
|
|
197
|
+
} finally {
|
|
198
|
+
setUploading(false);
|
|
199
|
+
setActiveUploadName(null);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
[context.designId, finishImport, t],
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const handleFigmaFileChange = useCallback(
|
|
206
|
+
(file: File | undefined) => {
|
|
207
|
+
if (!file) return;
|
|
208
|
+
setActiveMode("fig-file");
|
|
209
|
+
void uploadFile(file);
|
|
210
|
+
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
211
|
+
},
|
|
212
|
+
[uploadFile],
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const handleHtmlFileChange = useCallback(
|
|
216
|
+
async (file: File | undefined) => {
|
|
217
|
+
if (!file) return;
|
|
218
|
+
setActiveMode("html");
|
|
219
|
+
try {
|
|
220
|
+
importHtmlString(await file.text(), file.name);
|
|
221
|
+
} finally {
|
|
222
|
+
if (htmlFileInputRef.current) htmlFileInputRef.current.value = "";
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
[importHtmlString],
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const askVisualEdit = useCallback(() => {
|
|
229
|
+
sendToDesignAgentChat({
|
|
230
|
+
message:
|
|
231
|
+
"Use the visual-edit skill to connect my local app to this Design project. Run the app if needed, call `npx @agent-native/core@latest design connect`, then add URL-backed screens to this design.",
|
|
232
|
+
} as Parameters<typeof sendToDesignAgentChat>[0]);
|
|
233
|
+
toast.success(t("designEditor.import.visualEditSent"));
|
|
234
|
+
}, [t]);
|
|
235
|
+
|
|
236
|
+
const busy = importSource.isPending || uploading;
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<div className="flex min-h-0 flex-1 flex-col bg-background">
|
|
240
|
+
<div className="flex h-16 shrink-0 items-center border-b border-border/60 px-4">
|
|
241
|
+
<div className="min-w-0">
|
|
242
|
+
<h3 className="truncate text-xl font-semibold tracking-tight text-foreground">
|
|
243
|
+
{t("designEditor.import.title")}
|
|
244
|
+
</h3>
|
|
245
|
+
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
246
|
+
{"Bring source screens into this design" /* i18n-ignore */}
|
|
247
|
+
</p>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pb-4 pt-4">
|
|
252
|
+
<div className="space-y-1">
|
|
253
|
+
<ImportSourceRow
|
|
254
|
+
id="figma-paste-import"
|
|
255
|
+
icon={<IconBrandFigma className="size-3.5" />}
|
|
256
|
+
title={t("designEditor.import.figmaPasteTitle")}
|
|
257
|
+
description={
|
|
258
|
+
"Copy a frame in Figma, then paste here." /* i18n-ignore */
|
|
259
|
+
}
|
|
260
|
+
isOpen={activeMode === "figma-paste"}
|
|
261
|
+
onToggle={() =>
|
|
262
|
+
setActiveMode((mode) =>
|
|
263
|
+
mode === "figma-paste" ? null : "figma-paste",
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
>
|
|
267
|
+
<div className="p-2.5">
|
|
268
|
+
<div
|
|
269
|
+
role="textbox"
|
|
270
|
+
tabIndex={0}
|
|
271
|
+
aria-label={t("designEditor.import.figmaPasteTarget")}
|
|
272
|
+
onPaste={handlePaste}
|
|
273
|
+
className={cn(
|
|
274
|
+
"flex min-h-24 cursor-text items-center justify-center rounded-md border border-dashed border-border bg-muted/30 px-3 py-4 text-center text-xs text-muted-foreground outline-none transition-colors",
|
|
275
|
+
"focus:border-primary/60 focus:bg-background focus:ring-2 focus:ring-primary/15",
|
|
276
|
+
)}
|
|
277
|
+
>
|
|
278
|
+
{t("designEditor.import.figmaPasteTarget")}
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</ImportSourceRow>
|
|
282
|
+
|
|
283
|
+
<ImportSourceRow
|
|
284
|
+
id="fig-file-import"
|
|
285
|
+
icon={<IconUpload className="size-3.5" />}
|
|
286
|
+
title={t("designEditor.import.figUploadTitle")}
|
|
287
|
+
description={"Upload exported Figma frames." /* i18n-ignore */}
|
|
288
|
+
isOpen={activeMode === "fig-file"}
|
|
289
|
+
onToggle={() =>
|
|
290
|
+
setActiveMode((mode) => (mode === "fig-file" ? null : "fig-file"))
|
|
291
|
+
}
|
|
292
|
+
>
|
|
293
|
+
<div className="space-y-2 p-2.5">
|
|
294
|
+
<input
|
|
295
|
+
ref={fileInputRef}
|
|
296
|
+
type="file"
|
|
297
|
+
accept=".fig"
|
|
298
|
+
className="hidden"
|
|
299
|
+
onChange={(event) =>
|
|
300
|
+
handleFigmaFileChange(event.target.files?.[0])
|
|
301
|
+
}
|
|
302
|
+
/>
|
|
303
|
+
<Button
|
|
304
|
+
size="sm"
|
|
305
|
+
variant="outline"
|
|
306
|
+
className="h-8 w-full justify-center"
|
|
307
|
+
disabled={busy}
|
|
308
|
+
onClick={() => fileInputRef.current?.click()}
|
|
309
|
+
>
|
|
310
|
+
{uploading
|
|
311
|
+
? "Importing..." /* i18n-ignore */
|
|
312
|
+
: t("designEditor.import.chooseFigFile")}
|
|
313
|
+
</Button>
|
|
314
|
+
<p className="truncate text-[10px] leading-snug text-muted-foreground">
|
|
315
|
+
{
|
|
316
|
+
activeUploadName ??
|
|
317
|
+
"Export only the frames you need." /* i18n-ignore */
|
|
318
|
+
}
|
|
319
|
+
</p>
|
|
320
|
+
</div>
|
|
321
|
+
</ImportSourceRow>
|
|
322
|
+
|
|
323
|
+
<ImportSourceRow
|
|
324
|
+
id="html-import"
|
|
325
|
+
icon={<IconHtml className="size-3.5" />}
|
|
326
|
+
title={t("designEditor.import.htmlTitle")}
|
|
327
|
+
description={"Paste or choose a standalone file." /* i18n-ignore */}
|
|
328
|
+
isOpen={activeMode === "html"}
|
|
329
|
+
onToggle={() =>
|
|
330
|
+
setActiveMode((mode) => (mode === "html" ? null : "html"))
|
|
331
|
+
}
|
|
332
|
+
>
|
|
333
|
+
<div className="space-y-2 p-2.5">
|
|
334
|
+
<Textarea
|
|
335
|
+
value={htmlText}
|
|
336
|
+
onChange={(event) => setHtmlText(event.target.value)}
|
|
337
|
+
placeholder={t("designEditor.import.htmlPlaceholder")}
|
|
338
|
+
className="min-h-24 resize-none text-xs"
|
|
339
|
+
/>
|
|
340
|
+
<div className="flex gap-1.5">
|
|
341
|
+
<Button
|
|
342
|
+
size="sm"
|
|
343
|
+
className="h-8 flex-1 px-2"
|
|
344
|
+
disabled={busy || !htmlText.trim()}
|
|
345
|
+
onClick={() => importHtmlString(htmlText, "html-import.html")}
|
|
346
|
+
>
|
|
347
|
+
{t("designEditor.import.importHtml")}
|
|
348
|
+
</Button>
|
|
349
|
+
<input
|
|
350
|
+
ref={htmlFileInputRef}
|
|
351
|
+
type="file"
|
|
352
|
+
accept=".html,.htm"
|
|
353
|
+
className="hidden"
|
|
354
|
+
onChange={(event) =>
|
|
355
|
+
handleHtmlFileChange(event.target.files?.[0])
|
|
356
|
+
}
|
|
357
|
+
/>
|
|
358
|
+
<Button
|
|
359
|
+
size="sm"
|
|
360
|
+
variant="outline"
|
|
361
|
+
className="h-8 px-2"
|
|
362
|
+
disabled={busy}
|
|
363
|
+
onClick={() => htmlFileInputRef.current?.click()}
|
|
364
|
+
>
|
|
365
|
+
{t("designEditor.import.chooseHtmlFile")}
|
|
366
|
+
</Button>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
</ImportSourceRow>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
<div className="mt-5 border-t border-border/60 pt-4">
|
|
373
|
+
<p className="mb-2 text-xs font-medium text-muted-foreground">
|
|
374
|
+
{"More sources" /* i18n-ignore */}
|
|
375
|
+
</p>
|
|
376
|
+
<div className="space-y-1">
|
|
377
|
+
<CompactSourceRow
|
|
378
|
+
icon={<IconBrandGithub className="size-3.5" />}
|
|
379
|
+
title={t("designEditor.import.githubTitle")}
|
|
380
|
+
description={
|
|
381
|
+
"Repository import is coming soon." /* i18n-ignore */
|
|
382
|
+
}
|
|
383
|
+
badge={t("designEditor.import.comingSoon")}
|
|
384
|
+
/>
|
|
385
|
+
<CompactSourceRow
|
|
386
|
+
icon={<IconCode className="size-3.5" />}
|
|
387
|
+
title={t("designEditor.import.localTitle")}
|
|
388
|
+
description={
|
|
389
|
+
"Connect a running app with visual-edit." /* i18n-ignore */
|
|
390
|
+
}
|
|
391
|
+
badge={t("designEditor.import.comingSoon")}
|
|
392
|
+
action={
|
|
393
|
+
<Button
|
|
394
|
+
size="sm"
|
|
395
|
+
variant="ghost"
|
|
396
|
+
className="h-7 px-2 text-[11px]"
|
|
397
|
+
onClick={askVisualEdit}
|
|
398
|
+
>
|
|
399
|
+
{t("designEditor.import.useVisualEditNow")}
|
|
400
|
+
</Button>
|
|
401
|
+
}
|
|
402
|
+
/>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
{lastResult?.files?.length ? (
|
|
407
|
+
<div className="mt-5 rounded-md border border-border/70 bg-muted/30 px-2.5 py-2">
|
|
408
|
+
<div className="flex items-center gap-1.5 text-xs font-medium text-foreground">
|
|
409
|
+
<IconCircleCheck className="size-3.5 text-muted-foreground" />
|
|
410
|
+
{t("designEditor.import.lastImport")}
|
|
411
|
+
</div>
|
|
412
|
+
<ul className="mt-1.5 space-y-0.5 text-[11px] text-muted-foreground">
|
|
413
|
+
{lastResult.files.slice(0, 3).map((file) => (
|
|
414
|
+
<li key={file.id} className="truncate">
|
|
415
|
+
{file.filename}
|
|
416
|
+
</li>
|
|
417
|
+
))}
|
|
418
|
+
</ul>
|
|
419
|
+
</div>
|
|
420
|
+
) : null}
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function ImportSourceRow({
|
|
427
|
+
id,
|
|
428
|
+
icon,
|
|
429
|
+
title,
|
|
430
|
+
description,
|
|
431
|
+
isOpen,
|
|
432
|
+
onToggle,
|
|
433
|
+
children,
|
|
434
|
+
}: {
|
|
435
|
+
id: string;
|
|
436
|
+
icon: ReactNode;
|
|
437
|
+
title: string;
|
|
438
|
+
description: string;
|
|
439
|
+
isOpen: boolean;
|
|
440
|
+
onToggle: () => void;
|
|
441
|
+
children: ReactNode;
|
|
442
|
+
}) {
|
|
443
|
+
return (
|
|
444
|
+
<div className="overflow-hidden rounded-md">
|
|
445
|
+
<button
|
|
446
|
+
type="button"
|
|
447
|
+
aria-expanded={isOpen}
|
|
448
|
+
aria-controls={id}
|
|
449
|
+
onClick={onToggle}
|
|
450
|
+
className={cn(
|
|
451
|
+
"group flex w-full cursor-pointer items-center gap-3 rounded-md px-2 py-2.5 text-left transition-colors hover:bg-accent/60 active:bg-accent",
|
|
452
|
+
isOpen && "bg-accent/45",
|
|
453
|
+
)}
|
|
454
|
+
>
|
|
455
|
+
<span className="flex size-9 shrink-0 items-center justify-center rounded-md border border-border/70 bg-muted/70 text-muted-foreground transition-colors group-hover:border-border group-hover:bg-muted">
|
|
456
|
+
{icon}
|
|
457
|
+
</span>
|
|
458
|
+
<span className="min-w-0 flex-1">
|
|
459
|
+
<span className="block truncate text-sm font-medium leading-tight text-foreground">
|
|
460
|
+
{title}
|
|
461
|
+
</span>
|
|
462
|
+
<span className="mt-0.5 line-clamp-1 text-xs leading-snug text-muted-foreground">
|
|
463
|
+
{description}
|
|
464
|
+
</span>
|
|
465
|
+
</span>
|
|
466
|
+
<IconChevronRight
|
|
467
|
+
className={cn(
|
|
468
|
+
"size-4 shrink-0 text-muted-foreground transition-transform",
|
|
469
|
+
isOpen && "rotate-90",
|
|
470
|
+
)}
|
|
471
|
+
/>
|
|
472
|
+
</button>
|
|
473
|
+
{isOpen ? (
|
|
474
|
+
<div
|
|
475
|
+
id={id}
|
|
476
|
+
className="mb-2 mt-1 overflow-hidden rounded-md border border-border/70 bg-background/70"
|
|
477
|
+
>
|
|
478
|
+
{children}
|
|
479
|
+
</div>
|
|
480
|
+
) : null}
|
|
481
|
+
</div>
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function CompactSourceRow({
|
|
486
|
+
icon,
|
|
487
|
+
title,
|
|
488
|
+
description,
|
|
489
|
+
badge,
|
|
490
|
+
action,
|
|
491
|
+
}: {
|
|
492
|
+
icon: ReactNode;
|
|
493
|
+
title: string;
|
|
494
|
+
description: string;
|
|
495
|
+
badge: string;
|
|
496
|
+
action?: ReactNode;
|
|
497
|
+
}) {
|
|
498
|
+
return (
|
|
499
|
+
<div className="flex items-center gap-2 rounded-md px-2 py-2 text-left opacity-85">
|
|
500
|
+
<span className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border/60 bg-muted/50 text-muted-foreground">
|
|
501
|
+
{icon}
|
|
502
|
+
</span>
|
|
503
|
+
<span className="min-w-0 flex-1">
|
|
504
|
+
<span className="flex items-center gap-1.5">
|
|
505
|
+
<span className="truncate text-xs font-medium text-foreground">
|
|
506
|
+
{title}
|
|
507
|
+
</span>
|
|
508
|
+
<Badge variant="secondary" className="h-4 px-1 text-[9px]">
|
|
509
|
+
{badge}
|
|
510
|
+
</Badge>
|
|
511
|
+
</span>
|
|
512
|
+
<span className="mt-0.5 block truncate text-[10px] text-muted-foreground">
|
|
513
|
+
{description}
|
|
514
|
+
</span>
|
|
515
|
+
</span>
|
|
516
|
+
{action}
|
|
517
|
+
</div>
|
|
518
|
+
);
|
|
519
|
+
}
|
|
@@ -12,8 +12,15 @@ export interface NavigationState {
|
|
|
12
12
|
editorView?: "single" | "overview";
|
|
13
13
|
inspectorTab?: "design" | "tweaks" | "extensions";
|
|
14
14
|
inspector?: "design" | "tweaks" | "extensions";
|
|
15
|
-
leftPanel?:
|
|
16
|
-
|
|
15
|
+
leftPanel?:
|
|
16
|
+
| "file"
|
|
17
|
+
| "agent"
|
|
18
|
+
| "assets"
|
|
19
|
+
| "tools"
|
|
20
|
+
| "tokens"
|
|
21
|
+
| "import"
|
|
22
|
+
| "code";
|
|
23
|
+
panel?: "file" | "agent" | "assets" | "tools" | "tokens" | "import" | "code";
|
|
17
24
|
fileId?: string;
|
|
18
25
|
screenId?: string;
|
|
19
26
|
filename?: string;
|
|
@@ -47,8 +54,15 @@ export interface DesignEditorCommand {
|
|
|
47
54
|
viewMode?: "single" | "overview";
|
|
48
55
|
inspectorTab?: "design" | "tweaks" | "extensions";
|
|
49
56
|
inspector?: "design" | "tweaks" | "extensions";
|
|
50
|
-
leftPanel?:
|
|
51
|
-
|
|
57
|
+
leftPanel?:
|
|
58
|
+
| "file"
|
|
59
|
+
| "agent"
|
|
60
|
+
| "assets"
|
|
61
|
+
| "tools"
|
|
62
|
+
| "tokens"
|
|
63
|
+
| "import"
|
|
64
|
+
| "code";
|
|
65
|
+
panel?: "file" | "agent" | "assets" | "tools" | "tokens" | "import" | "code";
|
|
52
66
|
fileId?: string;
|
|
53
67
|
screenId?: string;
|
|
54
68
|
filename?: string;
|
|
@@ -88,13 +102,22 @@ function normalizeInspectorTab(
|
|
|
88
102
|
|
|
89
103
|
function normalizeLeftPanel(
|
|
90
104
|
value: unknown,
|
|
91
|
-
):
|
|
105
|
+
):
|
|
106
|
+
| "file"
|
|
107
|
+
| "agent"
|
|
108
|
+
| "assets"
|
|
109
|
+
| "tools"
|
|
110
|
+
| "tokens"
|
|
111
|
+
| "import"
|
|
112
|
+
| "code"
|
|
113
|
+
| undefined {
|
|
92
114
|
if (value === "extensions") return "tools";
|
|
93
115
|
return value === "file" ||
|
|
94
116
|
value === "agent" ||
|
|
95
117
|
value === "assets" ||
|
|
96
118
|
value === "tools" ||
|
|
97
119
|
value === "tokens" ||
|
|
120
|
+
value === "import" ||
|
|
98
121
|
value === "code"
|
|
99
122
|
? value
|
|
100
123
|
: undefined;
|
|
@@ -248,6 +248,7 @@ const messages = {
|
|
|
248
248
|
file: "檔案",
|
|
249
249
|
agent: "代理",
|
|
250
250
|
assets: "資源",
|
|
251
|
+
import: "匯入",
|
|
251
252
|
tools: "工具",
|
|
252
253
|
tokens: "權杖",
|
|
253
254
|
label: "設計工作區",
|
|
@@ -272,6 +273,45 @@ const messages = {
|
|
|
272
273
|
emptyHint: "新增設計權杖以重複使用顏色、間距等。",
|
|
273
274
|
applying: "套用中…",
|
|
274
275
|
},
|
|
276
|
+
import: {
|
|
277
|
+
title: "匯入",
|
|
278
|
+
description:
|
|
279
|
+
"將 Figma 選取內容、.fig 匯出檔或獨立 HTML 帶入為 Design 螢幕。",
|
|
280
|
+
figmaPasteTitle: "從 Figma 貼上",
|
|
281
|
+
figmaPasteDescription:
|
|
282
|
+
"在 Figma 複製畫框或圖層,然後聚焦此目標並貼上。可用時會直接匯入 Figma 中繼資料。",
|
|
283
|
+
figmaPasteTarget: "在此貼上 Figma 內容",
|
|
284
|
+
figUploadTitle: "上傳 .fig",
|
|
285
|
+
figUploadDescription:
|
|
286
|
+
"只匯出需要的畫框。含有許多嵌入圖片的大型檔案可能超過匯入限制。",
|
|
287
|
+
chooseFigFile: "選擇 .fig 檔案",
|
|
288
|
+
htmlTitle: "匯入 HTML",
|
|
289
|
+
htmlDescription:
|
|
290
|
+
"貼上或上傳獨立 HTML。Design 會將其儲存為新螢幕,不會注入到此編輯器 UI。",
|
|
291
|
+
htmlPlaceholder: "<main>在此貼上獨立 HTML...</main>",
|
|
292
|
+
importHtml: "匯入 HTML",
|
|
293
|
+
chooseHtmlFile: "選擇 HTML 檔案",
|
|
294
|
+
githubTitle: "GitHub",
|
|
295
|
+
githubDescription: "即將推出:直接從儲存庫匯入螢幕和元件。",
|
|
296
|
+
localTitle: "本機 app / VS Code",
|
|
297
|
+
localDescription: "即將推出:連接執行中的本機 app 並匯入 URL 螢幕。",
|
|
298
|
+
visualEditGuidance:
|
|
299
|
+
"目前可請代理使用 visual-edit。它可以執行 app、呼叫 `npx @agent-native/core@latest design connect`,並新增 URL 螢幕。",
|
|
300
|
+
useVisualEditNow: "立即使用 visual-edit",
|
|
301
|
+
comingSoon: "即將推出",
|
|
302
|
+
warningsToast: "匯入完成但有警告",
|
|
303
|
+
figmaSuccess: "已匯入 Figma 貼上內容",
|
|
304
|
+
htmlSuccess: "已匯入 HTML",
|
|
305
|
+
uploadSuccess: "已匯入檔案",
|
|
306
|
+
visualEditSent: "已將 visual-edit 請求傳送給代理",
|
|
307
|
+
lastImport: "上次匯入",
|
|
308
|
+
errors: {
|
|
309
|
+
notHtml: "請貼上或選擇有效的 HTML 以匯入。",
|
|
310
|
+
importFailed: "匯入失敗",
|
|
311
|
+
figmaPasteFailed: "Figma 貼上匯入失敗",
|
|
312
|
+
uploadFailed: "檔案上傳失敗",
|
|
313
|
+
},
|
|
314
|
+
},
|
|
275
315
|
generationMayHaveStopped:
|
|
276
316
|
"生成可能在建立檔案前停止。請檢視代理訊息或重試。",
|
|
277
317
|
generationMayHaveStoppedToast: "生成可能在建立檔案前停止。",
|