@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.
Files changed (45) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/use-action.ts +5 -1
  5. package/corpus/core/src/server/ssr-handler.ts +12 -0
  6. package/corpus/core/src/vite/action-types-plugin.ts +6 -2
  7. package/corpus/core/src/vite/client.ts +152 -17
  8. package/corpus/templates/design/actions/import-design-source.ts +158 -0
  9. package/corpus/templates/design/actions/navigate.ts +3 -2
  10. package/corpus/templates/design/actions/view-screen.ts +1 -1
  11. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +519 -0
  12. package/corpus/templates/design/app/hooks/use-navigation-state.ts +28 -5
  13. package/corpus/templates/design/app/i18n/zh-TW.ts +40 -0
  14. package/corpus/templates/design/app/i18n-data.ts +508 -0
  15. package/corpus/templates/design/app/pages/DesignEditor.tsx +27 -0
  16. package/corpus/templates/design/changelog/2026-07-01-design-can-import-figma-paste-fig-files-and-standalone-html.md +6 -0
  17. package/corpus/templates/design/package.json +3 -0
  18. package/corpus/templates/design/server/handlers/import-design-file.ts +168 -0
  19. package/corpus/templates/design/server/lib/figma-import/clipboard.ts +122 -0
  20. package/corpus/templates/design/server/lib/figma-import/decode.ts +455 -0
  21. package/corpus/templates/design/server/lib/figma-import/processor.ts +116 -0
  22. package/corpus/templates/design/server/lib/figma-import/render-html.ts +397 -0
  23. package/corpus/templates/design/server/lib/figma-import/types.ts +60 -0
  24. package/corpus/templates/design/server/lib/import-design-files.ts +314 -0
  25. package/corpus/templates/design/server/routes/api/import-design-file.post.ts +1 -0
  26. package/dist/client/use-action.d.ts +5 -1
  27. package/dist/client/use-action.d.ts.map +1 -1
  28. package/dist/client/use-action.js.map +1 -1
  29. package/dist/collab/awareness.d.ts +2 -2
  30. package/dist/collab/awareness.d.ts.map +1 -1
  31. package/dist/collab/routes.d.ts +1 -1
  32. package/dist/notifications/routes.d.ts +3 -3
  33. package/dist/observability/routes.d.ts +3 -3
  34. package/dist/resources/handlers.d.ts +2 -2
  35. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  36. package/dist/server/ssr-handler.d.ts.map +1 -1
  37. package/dist/server/ssr-handler.js +7 -0
  38. package/dist/server/ssr-handler.js.map +1 -1
  39. package/dist/vite/action-types-plugin.d.ts.map +1 -1
  40. package/dist/vite/action-types-plugin.js +6 -2
  41. package/dist/vite/action-types-plugin.js.map +1 -1
  42. package/dist/vite/client.d.ts.map +1 -1
  43. package/dist/vite/client.js +132 -11
  44. package/dist/vite/client.js.map +1 -1
  45. 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",
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Design can import Figma paste, .fig files, and standalone HTML from a new Import tab.
@@ -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
+ }