@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,397 @@
|
|
|
1
|
+
import type { ImportedFigmaHtmlFile } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const MAX_RENDERED_FRAMES = 24;
|
|
4
|
+
|
|
5
|
+
export interface RenderFigmaHtmlOptions {
|
|
6
|
+
filename: string;
|
|
7
|
+
document: unknown;
|
|
8
|
+
imageMap?: Map<string, string>;
|
|
9
|
+
selectionNodeId?: string;
|
|
10
|
+
meta?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RenderFigmaHtmlResult {
|
|
14
|
+
files: ImportedFigmaHtmlFile[];
|
|
15
|
+
warnings: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface RenderContext {
|
|
19
|
+
imageMap: Map<string, string>;
|
|
20
|
+
unresolvedImages: Set<string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
24
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function childrenOf(node: Record<string, unknown>): Record<string, unknown>[] {
|
|
28
|
+
const candidates = [
|
|
29
|
+
node.children,
|
|
30
|
+
node.childNodes,
|
|
31
|
+
node.nodes,
|
|
32
|
+
node.canvasChildren,
|
|
33
|
+
node.rootChildren,
|
|
34
|
+
];
|
|
35
|
+
for (const candidate of candidates) {
|
|
36
|
+
if (!Array.isArray(candidate)) continue;
|
|
37
|
+
return candidate.filter(isRecord);
|
|
38
|
+
}
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function findRootNode(document: unknown): Record<string, unknown> | null {
|
|
43
|
+
if (!isRecord(document)) return null;
|
|
44
|
+
const direct = [
|
|
45
|
+
document.document,
|
|
46
|
+
document.root,
|
|
47
|
+
document.canvas,
|
|
48
|
+
document.node,
|
|
49
|
+
];
|
|
50
|
+
for (const candidate of direct) {
|
|
51
|
+
if (isRecord(candidate)) return candidate;
|
|
52
|
+
}
|
|
53
|
+
return document;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function nodeId(node: Record<string, unknown>): string | undefined {
|
|
57
|
+
for (const key of ["id", "guid", "nodeId"]) {
|
|
58
|
+
const value = node[key];
|
|
59
|
+
if (typeof value === "string") return value;
|
|
60
|
+
if (typeof value === "number") return String(value);
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function nodeName(node: Record<string, unknown>): string {
|
|
66
|
+
for (const key of ["name", "title", "label"]) {
|
|
67
|
+
const value = node[key];
|
|
68
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
69
|
+
}
|
|
70
|
+
return "Frame";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function nodeType(node: Record<string, unknown>): string {
|
|
74
|
+
for (const key of ["type", "nodeType", "class", "kind"]) {
|
|
75
|
+
const value = node[key];
|
|
76
|
+
if (typeof value === "string") return value.toUpperCase();
|
|
77
|
+
}
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isTopLevelFrameNode(node: Record<string, unknown>): boolean {
|
|
82
|
+
return ["FRAME", "COMPONENT", "INSTANCE", "CANVAS", "SECTION"].includes(
|
|
83
|
+
nodeType(node),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function slug(value: string): string {
|
|
88
|
+
const normalized = value
|
|
89
|
+
.toLowerCase()
|
|
90
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
91
|
+
.replace(/^-+|-+$/g, "");
|
|
92
|
+
return normalized || "frame";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function escapeHtml(value: string): string {
|
|
96
|
+
return value
|
|
97
|
+
.replace(/&/g, "&")
|
|
98
|
+
.replace(/</g, "<")
|
|
99
|
+
.replace(/>/g, ">")
|
|
100
|
+
.replace(/"/g, """);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function numberValue(...values: unknown[]): number | undefined {
|
|
104
|
+
for (const value of values) {
|
|
105
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
106
|
+
if (typeof value === "string") {
|
|
107
|
+
const parsed = Number(value);
|
|
108
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function dimension(node: Record<string, unknown>, key: "width" | "height") {
|
|
115
|
+
const absoluteBoundingBox = isRecord(node.absoluteBoundingBox)
|
|
116
|
+
? node.absoluteBoundingBox
|
|
117
|
+
: {};
|
|
118
|
+
const size = isRecord(node.size) ? node.size : {};
|
|
119
|
+
const bounds = isRecord(node.bounds) ? node.bounds : {};
|
|
120
|
+
return numberValue(
|
|
121
|
+
node[key],
|
|
122
|
+
absoluteBoundingBox[key],
|
|
123
|
+
size[key],
|
|
124
|
+
bounds[key],
|
|
125
|
+
key === "width" ? node.w : node.h,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function colorToCss(value: unknown): string | undefined {
|
|
130
|
+
if (typeof value === "string") return value;
|
|
131
|
+
if (!isRecord(value)) return undefined;
|
|
132
|
+
const r = numberValue(value.r, value.red);
|
|
133
|
+
const g = numberValue(value.g, value.green);
|
|
134
|
+
const b = numberValue(value.b, value.blue);
|
|
135
|
+
if (r === undefined || g === undefined || b === undefined) return undefined;
|
|
136
|
+
const alpha = numberValue(value.a, value.alpha, value.opacity) ?? 1;
|
|
137
|
+
const rr = r <= 1 ? Math.round(r * 255) : Math.round(r);
|
|
138
|
+
const gg = g <= 1 ? Math.round(g * 255) : Math.round(g);
|
|
139
|
+
const bb = b <= 1 ? Math.round(b * 255) : Math.round(b);
|
|
140
|
+
return `rgba(${rr}, ${gg}, ${bb}, ${Math.max(0, Math.min(alpha, 1))})`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function firstVisibleFill(
|
|
144
|
+
node: Record<string, unknown>,
|
|
145
|
+
): Record<string, unknown> | null {
|
|
146
|
+
const fills = Array.isArray(node.fills)
|
|
147
|
+
? node.fills
|
|
148
|
+
: Array.isArray(node.fillPaints)
|
|
149
|
+
? node.fillPaints
|
|
150
|
+
: [];
|
|
151
|
+
for (const fill of fills) {
|
|
152
|
+
if (!isRecord(fill)) continue;
|
|
153
|
+
if (fill.visible === false) continue;
|
|
154
|
+
return fill;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function imageRefFromFill(fill: Record<string, unknown>): string | undefined {
|
|
160
|
+
for (const key of ["imageRef", "imageHash", "hash", "ref"]) {
|
|
161
|
+
const value = fill[key];
|
|
162
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function imageUrl(hashHex: string, ctx: RenderContext): string | null {
|
|
168
|
+
const mapped = ctx.imageMap.get(hashHex);
|
|
169
|
+
if (!mapped) {
|
|
170
|
+
ctx.unresolvedImages.add(hashHex);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
if (
|
|
174
|
+
mapped.startsWith("data:") ||
|
|
175
|
+
mapped.startsWith("http:") ||
|
|
176
|
+
mapped.startsWith("https:") ||
|
|
177
|
+
mapped.startsWith("/")
|
|
178
|
+
) {
|
|
179
|
+
return mapped;
|
|
180
|
+
}
|
|
181
|
+
return `./${mapped.replace(/^\.?\//, "")}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function nodeText(node: Record<string, unknown>): string {
|
|
185
|
+
for (const key of ["characters", "text", "content"]) {
|
|
186
|
+
const value = node[key];
|
|
187
|
+
if (typeof value === "string") return value;
|
|
188
|
+
}
|
|
189
|
+
return "";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function styleForNode(node: Record<string, unknown>): string {
|
|
193
|
+
const styles: string[] = ["box-sizing: border-box"];
|
|
194
|
+
const width = dimension(node, "width");
|
|
195
|
+
const height = dimension(node, "height");
|
|
196
|
+
if (width !== undefined) styles.push(`width: ${Math.round(width)}px`);
|
|
197
|
+
if (height !== undefined) styles.push(`min-height: ${Math.round(height)}px`);
|
|
198
|
+
|
|
199
|
+
const layoutMode =
|
|
200
|
+
typeof node.layoutMode === "string" ? node.layoutMode.toUpperCase() : "";
|
|
201
|
+
if (layoutMode === "HORIZONTAL" || layoutMode === "VERTICAL") {
|
|
202
|
+
styles.push("display: flex");
|
|
203
|
+
styles.push(
|
|
204
|
+
`flex-direction: ${layoutMode === "HORIZONTAL" ? "row" : "column"}`,
|
|
205
|
+
);
|
|
206
|
+
const gap = numberValue(node.itemSpacing, node.gap);
|
|
207
|
+
if (gap !== undefined) styles.push(`gap: ${Math.round(gap)}px`);
|
|
208
|
+
const padding = [
|
|
209
|
+
numberValue(node.paddingTop) ?? 0,
|
|
210
|
+
numberValue(node.paddingRight) ?? 0,
|
|
211
|
+
numberValue(node.paddingBottom) ?? 0,
|
|
212
|
+
numberValue(node.paddingLeft) ?? 0,
|
|
213
|
+
];
|
|
214
|
+
if (padding.some((value) => value > 0)) {
|
|
215
|
+
styles.push(
|
|
216
|
+
`padding: ${padding.map((value) => `${Math.round(value)}px`).join(" ")}`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
styles.push("position: relative");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const fill = firstVisibleFill(node);
|
|
224
|
+
const fillType =
|
|
225
|
+
typeof fill?.type === "string" ? fill.type.toUpperCase() : "";
|
|
226
|
+
const color = colorToCss(fill?.color ?? node.backgroundColor);
|
|
227
|
+
if (fillType !== "IMAGE" && color) styles.push(`background: ${color}`);
|
|
228
|
+
|
|
229
|
+
const radius = numberValue(node.cornerRadius, node.borderRadius);
|
|
230
|
+
if (radius !== undefined)
|
|
231
|
+
styles.push(`border-radius: ${Math.round(radius)}px`);
|
|
232
|
+
const opacity = numberValue(node.opacity);
|
|
233
|
+
if (opacity !== undefined && opacity < 1) styles.push(`opacity: ${opacity}`);
|
|
234
|
+
const fontSize = numberValue(node.fontSize);
|
|
235
|
+
if (fontSize !== undefined)
|
|
236
|
+
styles.push(`font-size: ${Math.round(fontSize)}px`);
|
|
237
|
+
const fontWeight = numberValue(node.fontWeight);
|
|
238
|
+
if (fontWeight !== undefined)
|
|
239
|
+
styles.push(`font-weight: ${Math.round(fontWeight)}`);
|
|
240
|
+
const lineHeight = numberValue(node.lineHeightPx, node.lineHeight);
|
|
241
|
+
if (lineHeight !== undefined)
|
|
242
|
+
styles.push(`line-height: ${Math.round(lineHeight)}px`);
|
|
243
|
+
const textColor = colorToCss(
|
|
244
|
+
node.style && isRecord(node.style) ? node.style.color : undefined,
|
|
245
|
+
);
|
|
246
|
+
if (textColor) styles.push(`color: ${textColor}`);
|
|
247
|
+
return styles.join("; ");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function renderNode(node: Record<string, unknown>, ctx: RenderContext): string {
|
|
251
|
+
const type = nodeType(node);
|
|
252
|
+
const id = nodeId(node);
|
|
253
|
+
const name = nodeName(node);
|
|
254
|
+
const attrs = [
|
|
255
|
+
`data-figma-node-name="${escapeHtml(name)}"`,
|
|
256
|
+
id ? `data-figma-node-id="${escapeHtml(id)}"` : "",
|
|
257
|
+
`data-figma-node-type="${escapeHtml(type || "NODE")}"`,
|
|
258
|
+
`style="${escapeHtml(styleForNode(node))}"`,
|
|
259
|
+
]
|
|
260
|
+
.filter(Boolean)
|
|
261
|
+
.join(" ");
|
|
262
|
+
|
|
263
|
+
const fill = firstVisibleFill(node);
|
|
264
|
+
const imageRef = fill ? imageRefFromFill(fill) : undefined;
|
|
265
|
+
if (imageRef) {
|
|
266
|
+
const src = imageUrl(imageRef, ctx);
|
|
267
|
+
if (src) {
|
|
268
|
+
return `<div ${attrs}><img src="${escapeHtml(src)}" alt="${escapeHtml(
|
|
269
|
+
name,
|
|
270
|
+
)}" style="width:100%;height:100%;object-fit:cover;display:block" /></div>`;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (type === "TEXT") {
|
|
275
|
+
return `<div ${attrs}>${escapeHtml(nodeText(node)).replace(/\n/g, "<br />")}</div>`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const children = childrenOf(node);
|
|
279
|
+
const inner = children.map((child) => renderNode(child, ctx)).join("\n");
|
|
280
|
+
return `<section ${attrs}>${inner}</section>`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function topLevelFrames(
|
|
284
|
+
root: Record<string, unknown>,
|
|
285
|
+
selectionNodeId?: string,
|
|
286
|
+
): Record<string, unknown>[] {
|
|
287
|
+
const children = childrenOf(root);
|
|
288
|
+
const candidates = children.length > 0 ? children : [root];
|
|
289
|
+
if (selectionNodeId) {
|
|
290
|
+
const selected = findNode(root, selectionNodeId);
|
|
291
|
+
if (selected) return [selected];
|
|
292
|
+
}
|
|
293
|
+
if (isTopLevelFrameNode(root)) return [root];
|
|
294
|
+
const frames = candidates.filter((node) => {
|
|
295
|
+
return isTopLevelFrameNode(node);
|
|
296
|
+
});
|
|
297
|
+
return frames.length > 0 ? frames : candidates.filter(isRecord).slice(0, 12);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function findNode(
|
|
301
|
+
node: Record<string, unknown>,
|
|
302
|
+
targetId: string,
|
|
303
|
+
): Record<string, unknown> | null {
|
|
304
|
+
if (nodeId(node) === targetId) return node;
|
|
305
|
+
for (const child of childrenOf(node)) {
|
|
306
|
+
const found = findNode(child, targetId);
|
|
307
|
+
if (found) return found;
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function standaloneHtml(
|
|
313
|
+
title: string,
|
|
314
|
+
body: string,
|
|
315
|
+
meta: Record<string, unknown>,
|
|
316
|
+
) {
|
|
317
|
+
const metaJson = escapeHtml(JSON.stringify(meta));
|
|
318
|
+
return `<!doctype html>
|
|
319
|
+
<html>
|
|
320
|
+
<head>
|
|
321
|
+
<meta charset="utf-8" />
|
|
322
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
323
|
+
<title>${escapeHtml(title)}</title>
|
|
324
|
+
<!-- Imported from Figma data. Source metadata: ${metaJson} -->
|
|
325
|
+
<style>
|
|
326
|
+
:root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
327
|
+
body { margin: 0; min-height: 100vh; background: #f8fafc; color: #0f172a; }
|
|
328
|
+
.design-import-frame { margin: 0 auto; background: white; overflow: hidden; box-shadow: 0 24px 80px rgba(15, 23, 42, 0.14); }
|
|
329
|
+
</style>
|
|
330
|
+
</head>
|
|
331
|
+
<body>
|
|
332
|
+
<main class="design-import-frame">
|
|
333
|
+
${body
|
|
334
|
+
.split("\n")
|
|
335
|
+
.map((line) => ` ${line}`)
|
|
336
|
+
.join("\n")}
|
|
337
|
+
</main>
|
|
338
|
+
</body>
|
|
339
|
+
</html>`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function renderFigmaHtml(
|
|
343
|
+
options: RenderFigmaHtmlOptions,
|
|
344
|
+
): RenderFigmaHtmlResult {
|
|
345
|
+
const root = findRootNode(options.document);
|
|
346
|
+
if (!root) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
"Decoded Figma document did not contain a renderable root.",
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
const ctx: RenderContext = {
|
|
352
|
+
imageMap: options.imageMap ?? new Map(),
|
|
353
|
+
unresolvedImages: new Set(),
|
|
354
|
+
};
|
|
355
|
+
const frameCandidates = topLevelFrames(root, options.selectionNodeId);
|
|
356
|
+
const frames = frameCandidates.slice(0, MAX_RENDERED_FRAMES);
|
|
357
|
+
if (frames.length === 0) {
|
|
358
|
+
throw new Error("Decoded Figma document did not contain any frames.");
|
|
359
|
+
}
|
|
360
|
+
const base = options.filename.replace(/\.[^.]+$/, "") || "figma-import";
|
|
361
|
+
const files = frames.map((frame, index) => {
|
|
362
|
+
const title = nodeName(frame);
|
|
363
|
+
const width = dimension(frame, "width") ?? 1440;
|
|
364
|
+
const height = dimension(frame, "height") ?? 900;
|
|
365
|
+
const filename =
|
|
366
|
+
frames.length === 1
|
|
367
|
+
? `${slug(base)}.html`
|
|
368
|
+
: `${slug(base)}-${slug(title || `frame-${index + 1}`)}.html`;
|
|
369
|
+
return {
|
|
370
|
+
filename,
|
|
371
|
+
width,
|
|
372
|
+
height,
|
|
373
|
+
source: {
|
|
374
|
+
...options.meta,
|
|
375
|
+
sourceType: "figma",
|
|
376
|
+
frameName: title,
|
|
377
|
+
frameNodeId: nodeId(frame),
|
|
378
|
+
},
|
|
379
|
+
content: standaloneHtml(title, renderNode(frame, ctx), {
|
|
380
|
+
...options.meta,
|
|
381
|
+
frameName: title,
|
|
382
|
+
frameNodeId: nodeId(frame),
|
|
383
|
+
}),
|
|
384
|
+
};
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const warnings = Array.from(ctx.unresolvedImages).map(
|
|
388
|
+
(hash) =>
|
|
389
|
+
`Image ${hash} was referenced by the Figma file but was not available in the import payload.`,
|
|
390
|
+
);
|
|
391
|
+
if (frameCandidates.length > MAX_RENDERED_FRAMES) {
|
|
392
|
+
warnings.push(
|
|
393
|
+
`Only the first ${MAX_RENDERED_FRAMES} top-level Figma frames were imported. Export fewer frames or select a specific frame to import the rest.`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
return { files, warnings };
|
|
397
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type ImportedDesignFileType = "html" | "css" | "jsx" | "asset";
|
|
2
|
+
|
|
3
|
+
export interface FigmaClipboardMeta {
|
|
4
|
+
fileKey?: string;
|
|
5
|
+
pasteID?: number | string;
|
|
6
|
+
dataType?: string;
|
|
7
|
+
environment?: string;
|
|
8
|
+
selectedNodeData?: string;
|
|
9
|
+
selectedNodeId?: string;
|
|
10
|
+
raw?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ParsedFigmaClipboardHtml {
|
|
14
|
+
meta: FigmaClipboardMeta | null;
|
|
15
|
+
buffer?: Buffer;
|
|
16
|
+
hasFigmaBuffer: boolean;
|
|
17
|
+
fallbackHtml?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DecodedFigImage {
|
|
21
|
+
/** SHA1 of the image bytes. Figma image refs use the same hex hash. */
|
|
22
|
+
hash: string;
|
|
23
|
+
ext: "png" | "jpg" | "webp" | "gif";
|
|
24
|
+
bytes: Buffer;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DecodedFig {
|
|
28
|
+
format: "kiwi" | "zip";
|
|
29
|
+
version?: number;
|
|
30
|
+
document: unknown;
|
|
31
|
+
images: DecodedFigImage[];
|
|
32
|
+
thumbnail: Buffer | null;
|
|
33
|
+
blobs: Buffer[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface FigmaImportSelection {
|
|
37
|
+
nodeId?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ImportedFigmaHtmlFile {
|
|
41
|
+
filename: string;
|
|
42
|
+
content: string;
|
|
43
|
+
width?: number;
|
|
44
|
+
height?: number;
|
|
45
|
+
source?: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface FigmaImportStats {
|
|
49
|
+
sourceKind: "figma-paste" | "fig-file";
|
|
50
|
+
format?: "kiwi" | "zip" | "html-fallback";
|
|
51
|
+
frameCount: number;
|
|
52
|
+
imageCount: number;
|
|
53
|
+
selectedNodeId?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface FigmaImportResult {
|
|
57
|
+
files: ImportedFigmaHtmlFile[];
|
|
58
|
+
warnings: string[];
|
|
59
|
+
stats: FigmaImportStats;
|
|
60
|
+
}
|