@agent-native/core 0.119.0 → 0.119.1
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 +2 -2
- package/corpus/core/CHANGELOG.md +8 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/ingestion/figma-node-to-html.ts +20 -99
- package/corpus/core/src/ingestion/figma-paint-math.ts +371 -0
- package/corpus/core/src/ingestion/index.ts +20 -0
- package/corpus/templates/assets/app/routes/library.tsx +17 -9
- package/corpus/templates/design/AGENTS.md +18 -5
- package/corpus/templates/design/FIGMA_INTEROPERABILITY.md +25 -12
- package/corpus/templates/design/actions/hydrate-figma-paste-images.ts +115 -0
- package/corpus/templates/design/actions/import-figma-clipboard.ts +83 -18
- package/corpus/templates/design/actions/import-figma-frame.ts +34 -21
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +28 -1
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +106 -11
- package/corpus/templates/design/app/components/design/FigmaHydrationDialog.tsx +273 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +7 -7
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +57 -9
- package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +10 -0
- package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +8 -1
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -0
- package/corpus/templates/design/app/i18n-data.ts +150 -2
- package/corpus/templates/design/app/lib/design-file-upload.ts +85 -0
- package/corpus/templates/design/app/lib/design-import.ts +19 -1
- package/corpus/templates/design/app/lib/figma-clipboard.ts +110 -10
- package/corpus/templates/design/changelog/2026-07-20-paste-figma-frames-directly-onto-the-canvas-no-token-needed.md +6 -0
- package/corpus/templates/design/changelog/2026-07-21-fig-upload-frame-url-and-rate-limit-errors.md +5 -0
- package/corpus/templates/design/changelog/2026-07-22-connecting-a-figma-token-now-actually-fills-in-a-pasted-desi.md +6 -0
- package/corpus/templates/design/changelog/2026-07-22-figma-line-arrow-vectors-and-stroked-icons-now-render-instea.md +6 -0
- package/corpus/templates/design/changelog/2026-07-22-fill-a-no-token-figma-paste-s-images-by-dropping-the-origina.md +6 -0
- package/corpus/templates/design/changelog/2026-07-22-imported-and-pasted-figma-frames-now-render-faithfully-fixed.md +6 -0
- package/corpus/templates/design/server/handlers/import-design-file.ts +50 -0
- package/corpus/templates/design/server/lib/fig-file-decoder.ts +90 -49
- package/corpus/templates/design/server/lib/fig-file-import.ts +19 -5
- package/corpus/templates/design/server/lib/fig-file-to-html.ts +583 -78
- package/corpus/templates/design/server/lib/figma-clipboard-local-decode.ts +238 -0
- package/corpus/templates/design/server/lib/figma-image-hydration.ts +389 -0
- package/corpus/templates/design/server/lib/figma-node-import.ts +136 -36
- package/corpus/templates/design/server/register-secrets.ts +14 -2
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/ingestion/figma-node-to-html.d.ts.map +1 -1
- package/dist/ingestion/figma-node-to-html.js +14 -88
- package/dist/ingestion/figma-node-to-html.js.map +1 -1
- package/dist/ingestion/figma-paint-math.d.ts +157 -0
- package/dist/ingestion/figma-paint-math.d.ts.map +1 -0
- package/dist/ingestion/figma-paint-math.js +274 -0
- package/dist/ingestion/figma-paint-math.js.map +1 -0
- package/dist/ingestion/index.d.ts +1 -0
- package/dist/ingestion/index.d.ts.map +1 -1
- package/dist/ingestion/index.js +1 -0
- package/dist/ingestion/index.js.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
- package/src/ingestion/figma-node-to-html.ts +20 -99
- package/src/ingestion/figma-paint-math.ts +371 -0
- package/src/ingestion/index.ts +20 -0
|
@@ -109,8 +109,11 @@ ladder.
|
|
|
109
109
|
`design-systems` skill's "Import from Figma" section.
|
|
110
110
|
- Uploading a raw `.fig` file in the Design editor's Import panel decodes the
|
|
111
111
|
container/Kiwi document locally into editable screens — no Builder
|
|
112
|
-
connection needed — and
|
|
113
|
-
|
|
112
|
+
connection needed — and accepts an optional Figma frame URL alongside the
|
|
113
|
+
file. When the URL contains a `node-id`, only that frame or its containing
|
|
114
|
+
top-level frame is imported; a mismatch falls back to all frames. This path
|
|
115
|
+
uses no Figma REST API quota and is scoped to screens only; it never creates
|
|
116
|
+
or updates a design system. This is separate from uploading `.fig` on the
|
|
114
117
|
Design System Setup page, which still indexes tokens/brand-kit data through
|
|
115
118
|
Builder and does not parse `.fig` locally. See the `design-systems` skill
|
|
116
119
|
for both paths.
|
|
@@ -118,9 +121,19 @@ ladder.
|
|
|
118
121
|
`figmeta.selectedNodeData`; `import-figma-clipboard` uses those before any
|
|
119
122
|
heuristic matching and supports multi-selection. Clipboard metadata is not a
|
|
120
123
|
public Figma contract, so a copied frame link remains the stable exact path
|
|
121
|
-
if Figma changes that field. Without a token,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
if Figma changes that field. Without a token, `import-figma-clipboard` falls
|
|
125
|
+
back to a local Kiwi binary decode: geometry, auto-layout, text, solid fills,
|
|
126
|
+
and strokes are editable immediately; image fills become annotated
|
|
127
|
+
placeholders (`data-figma-image-ref="<sha1>"`) that can be filled in later two
|
|
128
|
+
ways. (1) Token-free: upload the original `.fig` — the paste-result dialog's
|
|
129
|
+
"Fill images from .fig" option, or `hydrateFileIds` on the `.fig` upload route
|
|
130
|
+
(`/api/import-design-file`) — which matches each placeholder's SHA-1 hash to
|
|
131
|
+
the `.fig`'s embedded `images/` bytes, mirrors them to durable storage, and
|
|
132
|
+
needs no Figma API. (2) With a token: `hydrate-figma-paste-images` resolves the
|
|
133
|
+
same placeholders through Figma's REST image endpoint. This is not a
|
|
134
|
+
full-fidelity import — report which image fills are still unresolved and offer
|
|
135
|
+
to hydrate them. Clipboard paste is the fast no-quota path; the original `.fig`
|
|
136
|
+
is what carries the real image bytes for both upload and paste-hydration.
|
|
124
137
|
- For "what's in this Figma file/frame?" or "show me a screenshot of this
|
|
125
138
|
frame" without importing anything, use `get-figma-design-context` — no
|
|
126
139
|
`nodeId` lists pages/top-level frames (like the official Figma MCP's
|
|
@@ -15,18 +15,19 @@ product must report them instead of claiming success.
|
|
|
15
15
|
|
|
16
16
|
## Capability matrix
|
|
17
17
|
|
|
18
|
-
| Workflow or feature | Current behavior
|
|
19
|
-
| ---------------------------- |
|
|
20
|
-
| Figma frame URL / file key | Reads the exact node through `file_content:read`, converts it to a new Design screen, mirrors expiring images into durable storage, and returns a per-node fidelity report.
|
|
21
|
-
| Figma URL without a node id | Imports the first top-level object on the first page. A specific frame URL is recommended for deterministic results.
|
|
22
|
-
| Figma branch URL | Uses the branch key and imports that branch's node.
|
|
23
|
-
| Figma clipboard to Design | Uses private `figmeta.selectedNodeData` ids when present, then the same REST converter.
|
|
24
|
-
| `.fig` upload | Bounded best-effort decoding of known Kiwi/ZIP variants into editable HTML. Embedded images are moved to durable storage.
|
|
25
|
-
|
|
|
26
|
-
| Design
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
18
|
+
| Workflow or feature | Current behavior | Fidelity | Required verification |
|
|
19
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
20
|
+
| Figma frame URL / file key | Reads the exact node through `file_content:read`, converts it to a new Design screen, mirrors expiring images into durable storage, and returns a per-node fidelity report. | Mixed; see node matrix below. | REST fixture, authenticated file, screenshot comparison. |
|
|
21
|
+
| Figma URL without a node id | Imports the first top-level object on the first page. A specific frame URL is recommended for deterministic results. | Same as node import. | Multi-page and empty-page fixtures. |
|
|
22
|
+
| Figma branch URL | Uses the branch key and imports that branch's node. | Same as node import. | Main/branch pair with divergent content. |
|
|
23
|
+
| Figma clipboard to Design | Uses private `figmeta.selectedNodeData` ids when present, then the same REST converter. With a token: full fidelity matching `import-figma-frame`. Without a token: local Kiwi binary decode — geometry, auto-layout, text, solid fills, and strokes are editable; image fills are stamped with `data-figma-image-ref="<sha1>"` placeholders and can be resolved retroactively two ways: token-free by uploading the original `.fig` (the paste dialog's "Fill images from .fig" / `hydrateFileIds` on the `.fig` upload route), which matches each placeholder hash to the `.fig`'s embedded image bytes; or with a token via `hydrate-figma-paste-images`. | Exact selection identity while Figma's private metadata shape remains compatible; node fidelity is mixed. No-token imports resolve images retroactively — from the `.fig` (no quota) or a connected token. | Real Chrome copy from single, multi, nested, and 100+ node selections; token-less copy followed by `.fig` hydration and by deferred token connect, verifying image resolution both ways. |
|
|
24
|
+
| `.fig` upload | Bounded best-effort decoding of known Kiwi/ZIP variants into editable HTML. Embedded images are moved to durable storage. Optionally accepts a Figma frame URL: when its `node-id` matches the decoded file, Design imports only that top-level frame (or its ancestor for a nested node); a mismatch imports all frames with a warning. No Figma REST API calls are made. | Experimental. The format is proprietary and has no compatibility guarantee. | Corpus of real files from multiple Figma versions; never only generated containers. |
|
|
25
|
+
| `.fig` upload + frame URL | Accepts an optional Figma frame link. Normalizes the node-id and matches the decoded .fig GUID (`sessionID:localID`) to the matching top-level frame. Nested node IDs resolve to their top-level frame. On mismatch, all frames are imported. No Figma API quota is used. | Best-effort. The GUID mapping is reliable for frames in the same file but undocumented — test with real files before relying on it. | Real .fig/frame-link pairs from Figma across file versions. |
|
|
26
|
+
| Design to Figma clipboard | Copies an SVG built from the live rendered DOM. Figma imports supported SVG primitives as editable layers. | Visual/vector handoff, not a native semantic round trip. Auto layout, variables, components, prototypes, HTML state, and code identity are not recreated by SVG. | Paste into real Figma and inspect layer types, text, images, effects, clipping, and bounds. |
|
|
27
|
+
| Design SVG download | Same conversion as clipboard, with a server-render fallback when a live DOM is unavailable. | Same SVG limits; the export report lists approximations and omissions. | Live and server paths, selected layer and whole screen. |
|
|
28
|
+
| Native Design to Figma write | Use Figma's official MCP `use_figma` write-to-canvas path when the connected client/account supports it. | Native Figma structures, subject to Figma MCP beta limitations and permissions. | Full-seat/edit-permission account and a real destination file. |
|
|
29
|
+
| `.fig` download | Not supported. There is no documented public `.fig` authoring contract. | Unsupported. | Do not label SVG/ZIP as `.fig`. |
|
|
30
|
+
| Open-ended Figma chat | Provider catalog/docs/request expose the REST surface allowed by the user's scoped token; non-read calls require approval. Native canvas authoring requires official Figma MCP, not a personal access token alone. | Endpoint-dependent. | Read scopes, expired/revoked token, rate limiting, Enterprise-variable permissions, MCP connection. |
|
|
30
31
|
|
|
31
32
|
## REST node conversion matrix
|
|
32
33
|
|
|
@@ -54,6 +55,18 @@ product must report them instead of claiming success.
|
|
|
54
55
|
| Videos, emoji paints, FigJam-only and unknown node types | Rendered fallback when Figma can render the node. | Visual fallback only. |
|
|
55
56
|
| Hidden or 0%-opacity subtrees | Omitted without downloading their assets. | Visually exact and avoids unnecessary work. |
|
|
56
57
|
|
|
58
|
+
## Figma REST rate limits
|
|
59
|
+
|
|
60
|
+
- Viewer and Collab seats may receive up to 6 Tier 1 requests per month for
|
|
61
|
+
file, node, and image endpoints. The actual limit may be lower.
|
|
62
|
+
- Dev and Full seats receive 10–20 Tier 1 requests per minute, depending on the
|
|
63
|
+
resource's plan.
|
|
64
|
+
- On HTTP 429, Figma returns `Retry-After` in seconds plus
|
|
65
|
+
`X-Figma-Plan-Tier`, `X-Figma-Rate-Limit-Type`, and
|
|
66
|
+
`X-Figma-Upgrade-Link` metadata.
|
|
67
|
+
- Figma does not expose a requests-remaining counter.
|
|
68
|
+
- Clipboard paste and `.fig` upload are zero-quota local alternatives.
|
|
69
|
+
|
|
57
70
|
## Safety and scale limits
|
|
58
71
|
|
|
59
72
|
- REST responses are capped at 4 MB. Multi-selection requests split
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retroactively resolve unresolved Figma image fills for a screen that was
|
|
3
|
+
* imported via the local-kiwi clipboard path, using Figma's REST image
|
|
4
|
+
* endpoint (requires a saved FIGMA_ACCESS_TOKEN).
|
|
5
|
+
*
|
|
6
|
+
* When a Figma paste is imported without an access token, IMAGE fills render as
|
|
7
|
+
* `url("about:blank")` placeholders. The originating elements are annotated
|
|
8
|
+
* with `data-figma-image-ref="hash1 hash2 …"` so this action can find them
|
|
9
|
+
* without a full re-parse. Each Nth hash in the attribute maps to the Nth
|
|
10
|
+
* `url("about:blank")` occurrence in the element's style attribute.
|
|
11
|
+
*
|
|
12
|
+
* The read → collect → resolve → persist pipeline lives in
|
|
13
|
+
* `server/lib/figma-image-hydration.ts` and is shared with the token-free
|
|
14
|
+
* `.fig` hydration path (see `hydrateFileImagesFromFig`). This action only
|
|
15
|
+
* supplies the REST resolver.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { defineAction } from "@agent-native/core";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
applyHydration,
|
|
23
|
+
collectImageRefHashes,
|
|
24
|
+
hydrateImageRefsInHtml,
|
|
25
|
+
loadHydratableFile,
|
|
26
|
+
} from "../server/lib/figma-image-hydration.js";
|
|
27
|
+
import { resolveImageFillRefs } from "../server/lib/figma-node-import.js";
|
|
28
|
+
import { readLiveSourceFile } from "../server/source-workspace.js";
|
|
29
|
+
|
|
30
|
+
// Re-exported for direct unit testing of the pure HTML helpers.
|
|
31
|
+
export { collectImageRefHashes, hydrateImageRefsInHtml };
|
|
32
|
+
|
|
33
|
+
export default defineAction({
|
|
34
|
+
description:
|
|
35
|
+
'Retroactively resolve unresolved Figma image fills for a screen imported via the no-token local-kiwi path (import-figma-clipboard returned strategy:"localKiwi"). Requires a saved FIGMA_ACCESS_TOKEN. Fetches CDN URLs from Figma\'s /files/:key/images endpoint, mirrors them to durable blob storage, and replaces every url("about:blank") placeholder stamped by the local-kiwi decoder with the real durable URL. Fully resolved elements have their data-figma-image-ref annotation removed; partially resolved elements retain it for a future retry. Returns resolved/missing/skipped counts. Call after connecting Figma in Settings to fill in images from a no-token paste. For a token-free path when the original .fig file is available, upload the .fig with hydrateFileIds via /api/import-design-file instead.',
|
|
36
|
+
schema: z.object({
|
|
37
|
+
fileId: z
|
|
38
|
+
.string()
|
|
39
|
+
.describe(
|
|
40
|
+
"ID of the design_files row to hydrate. Use the fileId returned by import-figma-clipboard.",
|
|
41
|
+
),
|
|
42
|
+
}),
|
|
43
|
+
run: async ({ fileId }) => {
|
|
44
|
+
const { workspaceFile, designId, figmaFileKey } =
|
|
45
|
+
await loadHydratableFile(fileId);
|
|
46
|
+
|
|
47
|
+
if (!figmaFileKey) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`No Figma file key found for file ${fileId}. This file may not have been imported via a Figma clipboard paste.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const live = await readLiveSourceFile(workspaceFile);
|
|
54
|
+
|
|
55
|
+
const hashesToResolve = collectImageRefHashes(live.content);
|
|
56
|
+
if (hashesToResolve.length === 0) {
|
|
57
|
+
return {
|
|
58
|
+
fileId,
|
|
59
|
+
resolved: 0,
|
|
60
|
+
missing: 0,
|
|
61
|
+
skipped: 0,
|
|
62
|
+
message: "No unresolved image refs found in this file.",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let resolvedUrls: Map<string, string>;
|
|
67
|
+
try {
|
|
68
|
+
resolvedUrls = await resolveImageFillRefs(figmaFileKey, hashesToResolve);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
71
|
+
if (/quota cooldown|provider.*quota/i.test(msg)) {
|
|
72
|
+
const retryAfterSeconds =
|
|
73
|
+
(err as { retryAfterSeconds?: number }).retryAfterSeconds ?? 0;
|
|
74
|
+
const waitHint =
|
|
75
|
+
retryAfterSeconds > 0
|
|
76
|
+
? retryAfterSeconds >= 60
|
|
77
|
+
? `${Math.ceil(retryAfterSeconds / 60)} min`
|
|
78
|
+
: `${retryAfterSeconds}s`
|
|
79
|
+
: "~1 min";
|
|
80
|
+
throw Object.assign(
|
|
81
|
+
new Error(`Figma API rate limited — try again in ${waitHint}.`),
|
|
82
|
+
{ statusCode: 429 },
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (resolvedUrls.size === 0) {
|
|
89
|
+
return {
|
|
90
|
+
fileId,
|
|
91
|
+
resolved: 0,
|
|
92
|
+
missing: hashesToResolve.length,
|
|
93
|
+
skipped: 0,
|
|
94
|
+
message: `Figma returned no image URLs for ${hashesToResolve.length} hash${hashesToResolve.length === 1 ? "" : "es"}. The images may have been deleted from the Figma file or the access token lacks file_content:read scope.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const result = await applyHydration({
|
|
99
|
+
file: workspaceFile,
|
|
100
|
+
designId,
|
|
101
|
+
fileId,
|
|
102
|
+
liveContent: live.content,
|
|
103
|
+
liveVersionHash: live.versionHash,
|
|
104
|
+
requestedHashes: hashesToResolve,
|
|
105
|
+
resolvedUrls,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
fileId,
|
|
110
|
+
resolved: result.resolved,
|
|
111
|
+
missing: result.missing,
|
|
112
|
+
skipped: result.skipped,
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
|
+
import { importFigmaClipboardFromBuffer } from "../server/lib/figma-clipboard-local-decode.js";
|
|
4
5
|
import {
|
|
5
6
|
buildFigmaNodeCandidates,
|
|
6
7
|
extractVisibleTexts,
|
|
@@ -19,7 +20,13 @@ import { parseFigmaFileKey } from "../shared/figma-url.js";
|
|
|
19
20
|
|
|
20
21
|
const NODE_STRUCTURE_DEPTH = 3;
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
// Also matches a Figma 403 that occurs when the token is saved but lacks
|
|
24
|
+
// file_content:read scope — the validator only checks current_user:read.
|
|
25
|
+
const CREDENTIAL_MISSING_RE =
|
|
26
|
+
/credential not configured|figma.*request failed:.*403|figma.*request failed:.*forbidden/i;
|
|
27
|
+
// Transient errors should not block local-kiwi fallback when the buffer is present.
|
|
28
|
+
const TRANSIENT_ERROR_RE =
|
|
29
|
+
/quota cooldown|provider.*quota|rate.?limit|fetch failed|network.*error|timeout|ECONNRESET|ENOTFOUND|ERR_NETWORK/i;
|
|
23
30
|
const DURABLE_STORAGE_REQUIRED_RE =
|
|
24
31
|
/authenticated user so assets can be stored durably|could not store a Figma image durably|needs durable file storage/i;
|
|
25
32
|
|
|
@@ -69,6 +76,13 @@ export default defineAction({
|
|
|
69
76
|
.describe(
|
|
70
77
|
"Figma clipboard HTML used for fallback matching. When exact node ids are present, the client removes the large private data-buffer while retaining figmeta and visible HTML.",
|
|
71
78
|
),
|
|
79
|
+
clipboardBuffer: z
|
|
80
|
+
.string()
|
|
81
|
+
.max(15_000_000)
|
|
82
|
+
.optional()
|
|
83
|
+
.describe(
|
|
84
|
+
"Base64-encoded fig-kiwi binary from the clipboard's data-buffer. Present when the client used the local-kiwi strategy (no Figma access token). The server decodes this to build editable HTML from geometry, text, and fills without a REST call.",
|
|
85
|
+
),
|
|
72
86
|
originalName: z.string().optional(),
|
|
73
87
|
}),
|
|
74
88
|
run: async ({
|
|
@@ -77,6 +91,7 @@ export default defineAction({
|
|
|
77
91
|
selectedNodeIds,
|
|
78
92
|
selectedNodeIdsTruncated,
|
|
79
93
|
clipboardHtml,
|
|
94
|
+
clipboardBuffer,
|
|
80
95
|
originalName,
|
|
81
96
|
}) => {
|
|
82
97
|
const fileKey = parseFigmaFileKey(figmetaFileKey);
|
|
@@ -98,10 +113,8 @@ export default defineAction({
|
|
|
98
113
|
try {
|
|
99
114
|
if (selectedNodeIds?.length) {
|
|
100
115
|
const nodesById = await fetchFigmaNodes(fileKey, selectedNodeIds);
|
|
101
|
-
const { files, fidelityEntries } =
|
|
102
|
-
fileKey,
|
|
103
|
-
nodesById,
|
|
104
|
-
);
|
|
116
|
+
const { files, fidelityEntries, missingImageFillCount } =
|
|
117
|
+
await buildScreenFilesFromFigmaNodes(fileKey, nodesById);
|
|
105
118
|
const saved = await saveImportedDesignFiles({
|
|
106
119
|
designId,
|
|
107
120
|
sourceType: "figma-clipboard-rest",
|
|
@@ -110,9 +123,15 @@ export default defineAction({
|
|
|
110
123
|
const selectionWarnings = selectedNodeIdsTruncated
|
|
111
124
|
? [SELECTION_TRUNCATED_GUIDANCE]
|
|
112
125
|
: [];
|
|
126
|
+
const fillWarnings =
|
|
127
|
+
missingImageFillCount > 0
|
|
128
|
+
? [
|
|
129
|
+
`${missingImageFillCount} image fill${missingImageFillCount === 1 ? "" : "s"} could not be fetched from Figma and were omitted. This can happen for deleted images or very large assets.`,
|
|
130
|
+
]
|
|
131
|
+
: [];
|
|
113
132
|
return {
|
|
114
133
|
...saved,
|
|
115
|
-
warnings: [...saved.warnings, ...selectionWarnings],
|
|
134
|
+
warnings: [...saved.warnings, ...selectionWarnings, ...fillWarnings],
|
|
116
135
|
strategy: "restNodes" as const,
|
|
117
136
|
figma: {
|
|
118
137
|
fileKey,
|
|
@@ -142,17 +161,22 @@ export default defineAction({
|
|
|
142
161
|
if (matchResult.status === "matched") {
|
|
143
162
|
const nodeIds = matchResult.matches.map((match) => match.id);
|
|
144
163
|
const nodesById = await fetchFigmaNodes(fileKey, nodeIds);
|
|
145
|
-
const { files, fidelityEntries } =
|
|
146
|
-
fileKey,
|
|
147
|
-
nodesById,
|
|
148
|
-
);
|
|
164
|
+
const { files, fidelityEntries, missingImageFillCount } =
|
|
165
|
+
await buildScreenFilesFromFigmaNodes(fileKey, nodesById);
|
|
149
166
|
const saved = await saveImportedDesignFiles({
|
|
150
167
|
designId,
|
|
151
168
|
sourceType: "figma-clipboard-rest",
|
|
152
169
|
files,
|
|
153
170
|
});
|
|
171
|
+
const fillWarnings =
|
|
172
|
+
missingImageFillCount > 0
|
|
173
|
+
? [
|
|
174
|
+
`${missingImageFillCount} image fill${missingImageFillCount === 1 ? "" : "s"} could not be fetched from Figma and were omitted.`,
|
|
175
|
+
]
|
|
176
|
+
: [];
|
|
154
177
|
return {
|
|
155
178
|
...saved,
|
|
179
|
+
warnings: [...(saved.warnings ?? []), ...fillWarnings],
|
|
156
180
|
strategy: "restNodes" as const,
|
|
157
181
|
figma: {
|
|
158
182
|
fileKey,
|
|
@@ -174,26 +198,67 @@ export default defineAction({
|
|
|
174
198
|
throw error;
|
|
175
199
|
}
|
|
176
200
|
figmaApiKeyMissing = CREDENTIAL_MISSING_RE.test(errorMessage);
|
|
201
|
+
const isTransient = TRANSIENT_ERROR_RE.test(errorMessage);
|
|
177
202
|
if (
|
|
178
203
|
selectedNodeIds?.length &&
|
|
179
204
|
!parsedClipboard.fallbackHtml &&
|
|
180
|
-
!figmaApiKeyMissing
|
|
205
|
+
!figmaApiKeyMissing &&
|
|
206
|
+
(!isTransient || !clipboardBuffer)
|
|
181
207
|
) {
|
|
182
208
|
// Exact ids prove this was a current Figma clipboard. With no visible
|
|
183
|
-
// fallback, a
|
|
184
|
-
//
|
|
185
|
-
//
|
|
209
|
+
// fallback, a permanent REST failure must surface as a real error rather
|
|
210
|
+
// than silently degrading. Transient errors fall through to local-kiwi
|
|
211
|
+
// only when a buffer is present to decode; without a buffer there is
|
|
212
|
+
// nothing to fall back to, so even transient errors must propagate.
|
|
186
213
|
throw error;
|
|
187
214
|
}
|
|
188
215
|
if (!figmaApiKeyMissing) {
|
|
189
|
-
// A real (non-credential) REST failure — network error, revoked
|
|
190
|
-
// token, file access issue, etc. Still fall back to the honest
|
|
191
|
-
// clipboard preview rather than losing the paste entirely, but this
|
|
192
|
-
// isn't a "no confident match" case, so don't claim ambiguity.
|
|
193
216
|
matchStatus = "error";
|
|
194
217
|
}
|
|
195
218
|
}
|
|
196
219
|
|
|
220
|
+
// Local-kiwi fallback: decode the binary buffer when REST failed for any
|
|
221
|
+
// reason (missing token, 403, quota cooldown, network error) and the buffer
|
|
222
|
+
// is present. Always produces editable geometry, text, and auto-layout.
|
|
223
|
+
// IMAGE fills land as about:blank placeholders that hydrate-figma-paste-images
|
|
224
|
+
// resolves retroactively once the quota clears or the token is configured.
|
|
225
|
+
if ((figmaApiKeyMissing || matchStatus === "error") && clipboardBuffer) {
|
|
226
|
+
try {
|
|
227
|
+
const localResult = await importFigmaClipboardFromBuffer({
|
|
228
|
+
bufferBase64: clipboardBuffer,
|
|
229
|
+
fileKey,
|
|
230
|
+
originalName,
|
|
231
|
+
});
|
|
232
|
+
if (localResult.files.length > 0) {
|
|
233
|
+
const saved = await saveImportedDesignFiles({
|
|
234
|
+
designId,
|
|
235
|
+
sourceType: "figma-clipboard-local-kiwi",
|
|
236
|
+
files: localResult.files,
|
|
237
|
+
});
|
|
238
|
+
return {
|
|
239
|
+
...saved,
|
|
240
|
+
warnings: [...saved.warnings, ...localResult.warnings],
|
|
241
|
+
strategy: "localKiwi" as const,
|
|
242
|
+
figmaApiKeyMissing,
|
|
243
|
+
figma: { fileKey, selectedNodeIds },
|
|
244
|
+
unresolvedImages: localResult.unresolvedImageRefs.length,
|
|
245
|
+
fidelityReport: {
|
|
246
|
+
exactCount: 0,
|
|
247
|
+
approximated: [],
|
|
248
|
+
imageFallbacks: [],
|
|
249
|
+
unresolvedImages: localResult.unresolvedImageRefs.length,
|
|
250
|
+
},
|
|
251
|
+
guidance:
|
|
252
|
+
localResult.unresolvedImageRefs.length > 0
|
|
253
|
+
? `Imported from Figma using local decode — geometry, text, and styles are editable. ${localResult.unresolvedImageRefs.length} image${localResult.unresolvedImageRefs.length === 1 ? "" : "s"} need a Figma access token to load. Connect Figma in Settings to fill them in, or use "Copy as PNG" for individual images.`
|
|
254
|
+
: "Imported from Figma using local decode — geometry, text, and styles are fully editable. Connect Figma in Settings for highest-fidelity REST imports.",
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
// Local decode failed — fall through to html-fallback below.
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
197
262
|
if (!parsedClipboard.fallbackHtml) {
|
|
198
263
|
return {
|
|
199
264
|
designId,
|
|
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import {
|
|
6
6
|
buildScreenFilesFromFigmaNodes,
|
|
7
7
|
fetchFigmaNode,
|
|
8
|
+
isFigmaRateLimitError,
|
|
8
9
|
resolveTargetNodeId,
|
|
9
10
|
summarizeFidelity,
|
|
10
11
|
} from "../server/lib/figma-node-import.js";
|
|
@@ -78,29 +79,41 @@ export default defineAction({
|
|
|
78
79
|
const designId = await resolveImportDesignId(args.designId);
|
|
79
80
|
await assertAccess("design", designId, "editor");
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
try {
|
|
83
|
+
const nodeId = await resolveTargetNodeId(fileKey, requestedNodeId);
|
|
84
|
+
const rootNode = await fetchFigmaNode(fileKey, nodeId);
|
|
83
85
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
const { files, fidelityEntries } = await buildScreenFilesFromFigmaNodes(
|
|
87
|
+
fileKey,
|
|
88
|
+
{ [nodeId]: rootNode },
|
|
89
|
+
{
|
|
90
|
+
source: () => ({ figmaUrl: args.figmaUrl ?? null }),
|
|
91
|
+
},
|
|
92
|
+
);
|
|
91
93
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
const saved = await saveImportedDesignFiles({
|
|
95
|
+
designId,
|
|
96
|
+
sourceType: "figma-import",
|
|
97
|
+
files,
|
|
98
|
+
});
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
return {
|
|
101
|
+
...saved,
|
|
102
|
+
figma: { fileKey, nodeId, nodeName: rootNode.name ?? null },
|
|
103
|
+
fidelityReport: summarizeFidelity(fidelityEntries),
|
|
104
|
+
guidance:
|
|
105
|
+
"Review fidelityReport.imageFallbacks for subtrees rendered as PNG (masks, vector/boolean geometry, lines/arcs, advanced strokes/text, transformed image crops, and unsupported node types) and fidelityReport.approximated for properties CSS cannot express exactly (rotation, per-side stroke alignment, radial/angular/diamond gradients, blur radius scale, and live component/variable/prototype semantics).",
|
|
106
|
+
};
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (isFigmaRateLimitError(err)) {
|
|
109
|
+
throw Object.assign(err, {
|
|
110
|
+
rateLimitRetryAfter: err.retryAfterSeconds,
|
|
111
|
+
rateLimitPlanTier: err.figmaPlanTier,
|
|
112
|
+
rateLimitType: err.figmaRateLimitType,
|
|
113
|
+
rateLimitUpgradeUrl: err.figmaUpgradeUrl,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
105
118
|
},
|
|
106
119
|
});
|
|
@@ -97,6 +97,7 @@ import type {
|
|
|
97
97
|
IframeContextMenuPayload,
|
|
98
98
|
IframeFigmaClipboardPastePayload,
|
|
99
99
|
IframeHotkeyPayload,
|
|
100
|
+
IframeImagePastePayload,
|
|
100
101
|
} from "./design-canvas/iframe-events";
|
|
101
102
|
import {
|
|
102
103
|
forwardEmbeddedCanvasPanMessage,
|
|
@@ -473,6 +474,7 @@ interface DesignCanvasProps {
|
|
|
473
474
|
onElementDblClickText?: (info: ElementInfo) => void;
|
|
474
475
|
onIframeHotkey?: (event: IframeHotkeyPayload) => void;
|
|
475
476
|
onFigmaClipboardPaste?: (event: IframeFigmaClipboardPastePayload) => void;
|
|
477
|
+
onImagePaste?: (event: IframeImagePastePayload) => void;
|
|
476
478
|
onIframeContextMenu?: (event: IframeContextMenuPayload) => void;
|
|
477
479
|
onEditorDragStateChange?: (active: boolean) => void;
|
|
478
480
|
onVisualStructureChange?: (
|
|
@@ -1045,6 +1047,7 @@ export function DesignCanvas({
|
|
|
1045
1047
|
onElementDblClickText,
|
|
1046
1048
|
onIframeHotkey,
|
|
1047
1049
|
onFigmaClipboardPaste,
|
|
1050
|
+
onImagePaste,
|
|
1048
1051
|
onIframeContextMenu,
|
|
1049
1052
|
onEditorDragStateChange,
|
|
1050
1053
|
onVisualStructureChange,
|
|
@@ -2135,6 +2138,7 @@ export function DesignCanvas({
|
|
|
2135
2138
|
// static-fallback board thumbnails that call appendHitTestResponder.
|
|
2136
2139
|
// Without this, a drop onto a live/active screen has no responder and
|
|
2137
2140
|
// always falls through to the 50ms request timeout.
|
|
2141
|
+
const imageDiagBridge = "";
|
|
2138
2142
|
const bridgeToInject =
|
|
2139
2143
|
MOTION_PREVIEW_BRIDGE_SCRIPT +
|
|
2140
2144
|
SHADER_FILL_PREVIEW_BRIDGE_SCRIPT +
|
|
@@ -2143,7 +2147,8 @@ export function DesignCanvas({
|
|
|
2143
2147
|
NAV_BRIDGE_SCRIPT +
|
|
2144
2148
|
LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT +
|
|
2145
2149
|
embeddedGestureBridgeForCurrentState +
|
|
2146
|
-
editorChromeBridge
|
|
2150
|
+
editorChromeBridge +
|
|
2151
|
+
imageDiagBridge;
|
|
2147
2152
|
const frameContent = getEmbeddedFrameDocumentContent({
|
|
2148
2153
|
content: iframeRenderContent,
|
|
2149
2154
|
embeddedFrameBackground,
|
|
@@ -2687,6 +2692,27 @@ export function DesignCanvas({
|
|
|
2687
2692
|
if (content) onFigmaClipboardPaste?.({ content });
|
|
2688
2693
|
return;
|
|
2689
2694
|
}
|
|
2695
|
+
if (e.data.type === "canvas-image-paste") {
|
|
2696
|
+
const raw = Array.isArray(e.data.files) ? e.data.files : [];
|
|
2697
|
+
const MAX_IMAGE_PASTE_FILES = 20;
|
|
2698
|
+
const MAX_DATA_URL_BYTES = 20 * 1024 * 1024; // 20 MB per file
|
|
2699
|
+
const files = raw
|
|
2700
|
+
.slice(0, MAX_IMAGE_PASTE_FILES)
|
|
2701
|
+
.filter(
|
|
2702
|
+
(
|
|
2703
|
+
f: unknown,
|
|
2704
|
+
): f is { dataUrl: string; type: string; name: string } => {
|
|
2705
|
+
if (!f || typeof f !== "object") return false;
|
|
2706
|
+
const dataUrl = (f as { dataUrl?: unknown }).dataUrl;
|
|
2707
|
+
if (typeof dataUrl !== "string") return false;
|
|
2708
|
+
if (!dataUrl.startsWith("data:image/")) return false;
|
|
2709
|
+
if (dataUrl.length > MAX_DATA_URL_BYTES) return false;
|
|
2710
|
+
return true;
|
|
2711
|
+
},
|
|
2712
|
+
);
|
|
2713
|
+
if (files.length > 0) onImagePaste?.({ files });
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2690
2716
|
if (e.data.type === "element-contextmenu") {
|
|
2691
2717
|
const clientX = Number(e.data.clientX);
|
|
2692
2718
|
const clientY = Number(e.data.clientY);
|
|
@@ -2879,6 +2905,7 @@ export function DesignCanvas({
|
|
|
2879
2905
|
onElementDblClickText,
|
|
2880
2906
|
onIframeHotkey,
|
|
2881
2907
|
onFigmaClipboardPaste,
|
|
2908
|
+
onImagePaste,
|
|
2882
2909
|
onIframeContextMenu,
|
|
2883
2910
|
onEditorDragStateChange,
|
|
2884
2911
|
onVisualStructureChange,
|