@agent-native/core 0.84.32 → 0.84.34
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 +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/use-action.ts +5 -1
- 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/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/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/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-engine-api-key-route.d.ts +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
|
@@ -111,6 +111,7 @@ import {
|
|
|
111
111
|
IconDownload,
|
|
112
112
|
IconClipboard,
|
|
113
113
|
IconFileExport,
|
|
114
|
+
IconFileImport,
|
|
114
115
|
IconPlayerPlay,
|
|
115
116
|
IconDeviceFloppy,
|
|
116
117
|
IconRocket,
|
|
@@ -165,6 +166,7 @@ import {
|
|
|
165
166
|
DesignExtensionsPanel,
|
|
166
167
|
type DesignExtensionSlotContext,
|
|
167
168
|
} from "@/components/design/DesignExtensionsPanel";
|
|
169
|
+
import { DesignImportPanel } from "@/components/design/DesignImportPanel";
|
|
168
170
|
import {
|
|
169
171
|
EditPanel,
|
|
170
172
|
type InspectCodeData,
|
|
@@ -3521,6 +3523,7 @@ type DesignLeftPanel =
|
|
|
3521
3523
|
| "assets"
|
|
3522
3524
|
| "tools"
|
|
3523
3525
|
| "tokens"
|
|
3526
|
+
| "import"
|
|
3524
3527
|
| "code";
|
|
3525
3528
|
|
|
3526
3529
|
const INITIAL_GENERATION_DISABLED_LEFT_PANELS = new Set<DesignLeftPanel>([
|
|
@@ -3528,6 +3531,7 @@ const INITIAL_GENERATION_DISABLED_LEFT_PANELS = new Set<DesignLeftPanel>([
|
|
|
3528
3531
|
"assets",
|
|
3529
3532
|
"tools",
|
|
3530
3533
|
"tokens",
|
|
3534
|
+
"import",
|
|
3531
3535
|
"code",
|
|
3532
3536
|
]);
|
|
3533
3537
|
|
|
@@ -3538,6 +3542,7 @@ function normalizeDesignLeftPanel(value: unknown): DesignLeftPanel | undefined {
|
|
|
3538
3542
|
value === "assets" ||
|
|
3539
3543
|
value === "tools" ||
|
|
3540
3544
|
value === "tokens" ||
|
|
3545
|
+
value === "import" ||
|
|
3541
3546
|
value === "code"
|
|
3542
3547
|
? value
|
|
3543
3548
|
: undefined;
|
|
@@ -3581,6 +3586,11 @@ function DesignWorkspaceRail({
|
|
|
3581
3586
|
label: t("designEditor.leftRail.assets"),
|
|
3582
3587
|
icon: <IconPhoto className="size-[15px]" />,
|
|
3583
3588
|
},
|
|
3589
|
+
{
|
|
3590
|
+
panel: "import",
|
|
3591
|
+
label: t("designEditor.leftRail.import"),
|
|
3592
|
+
icon: <IconFileImport className="size-[15px]" />,
|
|
3593
|
+
},
|
|
3584
3594
|
{
|
|
3585
3595
|
panel: "tools",
|
|
3586
3596
|
label: t("designEditor.leftRail.tools"),
|
|
@@ -16874,6 +16884,23 @@ ${serializedHtml}
|
|
|
16874
16884
|
/>
|
|
16875
16885
|
)}
|
|
16876
16886
|
</div>
|
|
16887
|
+
<div
|
|
16888
|
+
className={cn(
|
|
16889
|
+
"min-h-0 flex-1 flex-col overflow-hidden",
|
|
16890
|
+
activeLeftPanel === "import" ? "flex" : "hidden",
|
|
16891
|
+
)}
|
|
16892
|
+
>
|
|
16893
|
+
{canEditDesign ? (
|
|
16894
|
+
<DesignImportPanel context={designExtensionContext} />
|
|
16895
|
+
) : (
|
|
16896
|
+
<ReadOnlyEditorPanel
|
|
16897
|
+
title={"Import requires editor access" /* i18n-ignore */}
|
|
16898
|
+
description={
|
|
16899
|
+
"Ask an owner for edit access before importing files into this design." /* i18n-ignore */
|
|
16900
|
+
}
|
|
16901
|
+
/>
|
|
16902
|
+
)}
|
|
16903
|
+
</div>
|
|
16877
16904
|
<div
|
|
16878
16905
|
className={cn(
|
|
16879
16906
|
"min-h-0 flex-1 flex-col overflow-hidden",
|
|
@@ -24,12 +24,15 @@
|
|
|
24
24
|
"@paper-design/shaders-react": "0.0.76",
|
|
25
25
|
"@tabler/icons-react": "catalog:",
|
|
26
26
|
"drizzle-orm": "^0.45.2",
|
|
27
|
+
"fzstd": "^0.1.1",
|
|
27
28
|
"h3": "^2.0.1-rc.20",
|
|
28
29
|
"html2canvas": "^1.4.1",
|
|
29
30
|
"isbot": "^5",
|
|
30
31
|
"jspdf": "^4.2.1",
|
|
31
32
|
"jszip": "^3.10.1",
|
|
33
|
+
"kiwi-schema": "^0.5.0",
|
|
32
34
|
"nanoid": "^5.1.9",
|
|
35
|
+
"pako": "^3.0.0",
|
|
33
36
|
"pdf-parse": "^2.4.5",
|
|
34
37
|
"yjs": "^13.6.31",
|
|
35
38
|
"zod": "^4.3.6"
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { getSession } from "@agent-native/core/server";
|
|
4
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
5
|
+
import {
|
|
6
|
+
defineEventHandler,
|
|
7
|
+
getQuery,
|
|
8
|
+
getRequestHeader,
|
|
9
|
+
readMultipartFormData,
|
|
10
|
+
setResponseStatus,
|
|
11
|
+
} from "h3";
|
|
12
|
+
|
|
13
|
+
import { isFigKiwiBuffer, isZipBuffer } from "../lib/figma-import/decode.js";
|
|
14
|
+
import { importFigmaBuffer } from "../lib/figma-import/processor.js";
|
|
15
|
+
import {
|
|
16
|
+
normalizeImportedHtmlDocument,
|
|
17
|
+
saveImportedDesignFiles,
|
|
18
|
+
type ImportedDesignFile,
|
|
19
|
+
} from "../lib/import-design-files.js";
|
|
20
|
+
|
|
21
|
+
const MAX_HTML_BYTES = 2 * 1024 * 1024;
|
|
22
|
+
const MAX_FIG_BYTES = 50 * 1024 * 1024;
|
|
23
|
+
const TOTAL_BODY_LIMIT = MAX_FIG_BYTES + 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
function fieldText(
|
|
26
|
+
parts: Awaited<ReturnType<typeof readMultipartFormData>>,
|
|
27
|
+
name: string,
|
|
28
|
+
) {
|
|
29
|
+
const part = parts?.find((candidate) => candidate.name === name);
|
|
30
|
+
return part?.data
|
|
31
|
+
? Buffer.from(part.data).toString("utf8").trim()
|
|
32
|
+
: undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function statusForError(message: string): number {
|
|
36
|
+
if (/unauthorized/i.test(message)) return 401;
|
|
37
|
+
if (/access|permission|not allowed/i.test(message)) return 403;
|
|
38
|
+
if (/too large|max/i.test(message)) return 413;
|
|
39
|
+
return 400;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const importDesignFile = defineEventHandler(async (event) => {
|
|
43
|
+
const session = await getSession(event).catch(() => null);
|
|
44
|
+
if (!session?.email) {
|
|
45
|
+
setResponseStatus(event, 401);
|
|
46
|
+
return { error: "Unauthorized" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const rawContentLength = getRequestHeader(event, "content-length");
|
|
50
|
+
const contentLength = Number(rawContentLength);
|
|
51
|
+
if (!rawContentLength || !Number.isFinite(contentLength)) {
|
|
52
|
+
setResponseStatus(event, 411);
|
|
53
|
+
return { error: "Content-Length header is required" };
|
|
54
|
+
}
|
|
55
|
+
if (contentLength > TOTAL_BODY_LIMIT) {
|
|
56
|
+
setResponseStatus(event, 413);
|
|
57
|
+
return { error: "Request body too large" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const query = getQuery(event);
|
|
62
|
+
const queryDesignId =
|
|
63
|
+
typeof query.designId === "string" ? query.designId.trim() : undefined;
|
|
64
|
+
let accessChecked = false;
|
|
65
|
+
if (queryDesignId) {
|
|
66
|
+
await assertAccess("design", queryDesignId, "editor");
|
|
67
|
+
accessChecked = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const parts = await readMultipartFormData(event);
|
|
71
|
+
const bodyDesignId = fieldText(parts, "designId");
|
|
72
|
+
if (queryDesignId && bodyDesignId && bodyDesignId !== queryDesignId) {
|
|
73
|
+
setResponseStatus(event, 400);
|
|
74
|
+
return { error: "Mismatched designId" };
|
|
75
|
+
}
|
|
76
|
+
const designId = queryDesignId ?? bodyDesignId;
|
|
77
|
+
const filePart = parts?.find((part) => part.name === "file" && part.data);
|
|
78
|
+
if (!designId) {
|
|
79
|
+
setResponseStatus(event, 400);
|
|
80
|
+
return { error: "Missing designId" };
|
|
81
|
+
}
|
|
82
|
+
if (!accessChecked) {
|
|
83
|
+
await assertAccess("design", designId, "editor");
|
|
84
|
+
}
|
|
85
|
+
if (!filePart?.data) {
|
|
86
|
+
setResponseStatus(event, 400);
|
|
87
|
+
return { error: "No file uploaded" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const originalName = filePart.filename || "import";
|
|
91
|
+
const ext = path.extname(originalName).toLowerCase();
|
|
92
|
+
const data = Buffer.from(filePart.data);
|
|
93
|
+
|
|
94
|
+
if (ext === ".html" || ext === ".htm") {
|
|
95
|
+
if (data.length > MAX_HTML_BYTES) {
|
|
96
|
+
throw new Error("HTML file is too large (max 2 MB).");
|
|
97
|
+
}
|
|
98
|
+
const saved = await saveImportedDesignFiles({
|
|
99
|
+
designId,
|
|
100
|
+
sourceType: "html-upload",
|
|
101
|
+
files: [
|
|
102
|
+
{
|
|
103
|
+
filename: originalName,
|
|
104
|
+
fileType: "html",
|
|
105
|
+
content: normalizeImportedHtmlDocument(
|
|
106
|
+
data.toString("utf8"),
|
|
107
|
+
"uploaded HTML file",
|
|
108
|
+
),
|
|
109
|
+
source: { sourceType: "html-upload", originalName },
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
importKind: "html",
|
|
115
|
+
...saved,
|
|
116
|
+
stats: { sourceKind: "html-upload", frameCount: saved.files.length },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (ext !== ".fig") {
|
|
121
|
+
throw new Error("Unsupported file type. Upload .fig, .html, or .htm.");
|
|
122
|
+
}
|
|
123
|
+
if (data.length > MAX_FIG_BYTES) {
|
|
124
|
+
throw new Error("Figma file is too large (max 50 MB).");
|
|
125
|
+
}
|
|
126
|
+
if (!isFigKiwiBuffer(data) && !isZipBuffer(data)) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
"Figma file contents do not match .fig or fig-kiwi format.",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const imported = await importFigmaBuffer({
|
|
133
|
+
buffer: data,
|
|
134
|
+
filename: originalName,
|
|
135
|
+
sourceKind: "fig-file",
|
|
136
|
+
});
|
|
137
|
+
const files: ImportedDesignFile[] = imported.files.map((file) => ({
|
|
138
|
+
filename: file.filename,
|
|
139
|
+
fileType: "html",
|
|
140
|
+
content: file.content,
|
|
141
|
+
source: { ...file.source, originalName },
|
|
142
|
+
preferredFrame: {
|
|
143
|
+
title:
|
|
144
|
+
typeof file.source?.frameName === "string"
|
|
145
|
+
? file.source.frameName
|
|
146
|
+
: undefined,
|
|
147
|
+
width: file.width,
|
|
148
|
+
height: file.height,
|
|
149
|
+
},
|
|
150
|
+
}));
|
|
151
|
+
const saved = await saveImportedDesignFiles({
|
|
152
|
+
designId,
|
|
153
|
+
sourceType: "fig-file",
|
|
154
|
+
files,
|
|
155
|
+
warnings: imported.warnings,
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
importKind: "fig",
|
|
159
|
+
...saved,
|
|
160
|
+
stats: imported.stats,
|
|
161
|
+
};
|
|
162
|
+
} catch (error) {
|
|
163
|
+
const message =
|
|
164
|
+
error instanceof Error ? error.message : "File import failed.";
|
|
165
|
+
setResponseStatus(event, statusForError(message));
|
|
166
|
+
return { error: message };
|
|
167
|
+
}
|
|
168
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { FigmaClipboardMeta, ParsedFigmaClipboardHtml } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const MAX_CLIPBOARD_HTML_BYTES = 2 * 1024 * 1024;
|
|
4
|
+
const MAX_DECODED_BUFFER_BYTES = 25 * 1024 * 1024;
|
|
5
|
+
const FIG_KIWI_MAGIC = Buffer.from("fig-kiwi", "utf8");
|
|
6
|
+
|
|
7
|
+
function byteLength(value: string): number {
|
|
8
|
+
return Buffer.byteLength(value, "utf8");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function stripWrapper(value: string, marker: "figmeta" | "figma"): string {
|
|
12
|
+
return value
|
|
13
|
+
.trim()
|
|
14
|
+
.replace(new RegExp(`^<!--\\(${marker}\\)`, "i"), "")
|
|
15
|
+
.replace(new RegExp(`\\(/${marker}\\)-->$`, "i"), "")
|
|
16
|
+
.trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function decodeBase64(value: string, label: string): Buffer {
|
|
20
|
+
const normalized = value.replace(/\s+/g, "");
|
|
21
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
|
|
22
|
+
throw new Error(`${label} is not valid base64.`);
|
|
23
|
+
}
|
|
24
|
+
const decoded = Buffer.from(normalized, "base64");
|
|
25
|
+
const roundTrip = decoded.toString("base64").replace(/=+$/, "");
|
|
26
|
+
if (roundTrip !== normalized.replace(/=+$/, "")) {
|
|
27
|
+
throw new Error(`${label} is malformed base64.`);
|
|
28
|
+
}
|
|
29
|
+
return decoded;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractDataAttribute(html: string, attribute: string): string | null {
|
|
33
|
+
const pattern = new RegExp(`${attribute}\\s*=\\s*(["'])([\\s\\S]*?)\\1`, "i");
|
|
34
|
+
const match = pattern.exec(html);
|
|
35
|
+
return match?.[2] ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function removeFigmaHiddenSpans(html: string): string {
|
|
39
|
+
return html
|
|
40
|
+
.replace(
|
|
41
|
+
/<span\b[^>]*\bdata-metadata\s*=\s*(["'])[\s\S]*?\1[^>]*>\s*<\/span>/gi,
|
|
42
|
+
"",
|
|
43
|
+
)
|
|
44
|
+
.replace(
|
|
45
|
+
/<span\b[^>]*\bdata-buffer\s*=\s*(["'])[\s\S]*?\1[^>]*>\s*<\/span>/gi,
|
|
46
|
+
"",
|
|
47
|
+
)
|
|
48
|
+
.replace(/<meta\b[^>]*charset[^>]*>/gi, "")
|
|
49
|
+
.trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeMeta(value: unknown): FigmaClipboardMeta {
|
|
53
|
+
const raw =
|
|
54
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
55
|
+
? (value as Record<string, unknown>)
|
|
56
|
+
: {};
|
|
57
|
+
const selectedNodeData =
|
|
58
|
+
typeof raw.selectedNodeData === "string" ? raw.selectedNodeData : undefined;
|
|
59
|
+
const selectedNodeId = selectedNodeData?.split("|")[0]?.trim() || undefined;
|
|
60
|
+
return {
|
|
61
|
+
fileKey: typeof raw.fileKey === "string" ? raw.fileKey : undefined,
|
|
62
|
+
pasteID:
|
|
63
|
+
typeof raw.pasteID === "string" || typeof raw.pasteID === "number"
|
|
64
|
+
? raw.pasteID
|
|
65
|
+
: undefined,
|
|
66
|
+
dataType: typeof raw.dataType === "string" ? raw.dataType : undefined,
|
|
67
|
+
environment:
|
|
68
|
+
typeof raw.environment === "string" ? raw.environment : undefined,
|
|
69
|
+
selectedNodeData,
|
|
70
|
+
selectedNodeId,
|
|
71
|
+
raw,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function looksLikeFigmaClipboardHtml(html: string): boolean {
|
|
76
|
+
return /\(figmeta\)|\(figma\)|data-metadata=|data-buffer=/i.test(html);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function parseFigmaClipboardHtml(
|
|
80
|
+
html: string,
|
|
81
|
+
): ParsedFigmaClipboardHtml {
|
|
82
|
+
if (byteLength(html) > MAX_CLIPBOARD_HTML_BYTES) {
|
|
83
|
+
throw new Error("Clipboard HTML is too large to import (max 2 MB).");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const metadataAttr = extractDataAttribute(html, "data-metadata");
|
|
87
|
+
const bufferAttr = extractDataAttribute(html, "data-buffer");
|
|
88
|
+
const fallbackHtml = removeFigmaHiddenSpans(html);
|
|
89
|
+
|
|
90
|
+
let meta: FigmaClipboardMeta | null = null;
|
|
91
|
+
if (metadataAttr) {
|
|
92
|
+
const metadataBase64 = stripWrapper(metadataAttr, "figmeta");
|
|
93
|
+
const metadataJson = decodeBase64(
|
|
94
|
+
metadataBase64,
|
|
95
|
+
"Figma metadata",
|
|
96
|
+
).toString("utf8");
|
|
97
|
+
meta = normalizeMeta(JSON.parse(metadataJson));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let buffer: Buffer | undefined;
|
|
101
|
+
if (bufferAttr) {
|
|
102
|
+
const bufferBase64 = stripWrapper(bufferAttr, "figma");
|
|
103
|
+
buffer = decodeBase64(bufferBase64, "Figma buffer");
|
|
104
|
+
if (buffer.length > MAX_DECODED_BUFFER_BYTES) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"Figma clipboard buffer is too large to import (max 25 MB).",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const hasFigmaBuffer =
|
|
112
|
+
!!buffer &&
|
|
113
|
+
buffer.length >= FIG_KIWI_MAGIC.length &&
|
|
114
|
+
buffer.subarray(0, FIG_KIWI_MAGIC.length).equals(FIG_KIWI_MAGIC);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
meta,
|
|
118
|
+
buffer,
|
|
119
|
+
hasFigmaBuffer,
|
|
120
|
+
fallbackHtml: fallbackHtml || undefined,
|
|
121
|
+
};
|
|
122
|
+
}
|