@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
|
@@ -36,6 +36,8 @@ const MAX_FIGMA_IMAGE_BYTES = 15 * 1024 * 1024;
|
|
|
36
36
|
const MAX_TOTAL_FIGMA_IMAGE_BYTES = 64 * 1024 * 1024;
|
|
37
37
|
const MAX_FIGMA_IMAGE_REFERENCES = 256;
|
|
38
38
|
const MAX_FIGMA_IMAGE_IDS_PER_REQUEST = 50;
|
|
39
|
+
// Figma's `/images` endpoint takes ids in the query string; cap the batch by
|
|
40
|
+
// both count and URL length so a complex frame's ids are fetched in full.
|
|
39
41
|
const MAX_FIGMA_IMAGE_IDS_QUERY_CHARS = 1_800;
|
|
40
42
|
const MAX_CONCURRENT_FIGMA_IMAGE_UPLOADS = 4;
|
|
41
43
|
const FIGMA_IMAGE_FETCH_TIMEOUT_MS = 20_000;
|
|
@@ -415,6 +417,7 @@ type FigmaProviderEnvelope = {
|
|
|
415
417
|
ok?: boolean;
|
|
416
418
|
status?: number;
|
|
417
419
|
statusText?: string;
|
|
420
|
+
headers?: Record<string, string>;
|
|
418
421
|
json?: unknown;
|
|
419
422
|
text?: string;
|
|
420
423
|
truncated?: boolean;
|
|
@@ -422,6 +425,46 @@ type FigmaProviderEnvelope = {
|
|
|
422
425
|
};
|
|
423
426
|
};
|
|
424
427
|
|
|
428
|
+
export type FigmaRateLimitError = Error & {
|
|
429
|
+
statusCode: 429;
|
|
430
|
+
retryAfterSeconds?: number;
|
|
431
|
+
figmaPlanTier?: string;
|
|
432
|
+
figmaRateLimitType?: "low" | "high";
|
|
433
|
+
figmaUpgradeUrl?: string;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const FIGMA_PLAN_TIERS = new Set([
|
|
437
|
+
"enterprise",
|
|
438
|
+
"org",
|
|
439
|
+
"pro",
|
|
440
|
+
"starter",
|
|
441
|
+
"student",
|
|
442
|
+
]);
|
|
443
|
+
|
|
444
|
+
export function isFigmaRateLimitError(
|
|
445
|
+
err: unknown,
|
|
446
|
+
): err is FigmaRateLimitError {
|
|
447
|
+
return (
|
|
448
|
+
err instanceof Error && (err as { statusCode?: unknown }).statusCode === 429
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function figmaUpgradeUrl(value: string | undefined): string | undefined {
|
|
453
|
+
if (!value) return undefined;
|
|
454
|
+
try {
|
|
455
|
+
const url = new URL(value);
|
|
456
|
+
if (
|
|
457
|
+
url.protocol === "https:" &&
|
|
458
|
+
(url.hostname === "figma.com" || url.hostname.endsWith(".figma.com"))
|
|
459
|
+
) {
|
|
460
|
+
return value;
|
|
461
|
+
}
|
|
462
|
+
} catch {
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
|
|
425
468
|
export function providerJson(envelope: unknown, label: string): unknown {
|
|
426
469
|
const response = (envelope as FigmaProviderEnvelope | null)?.response;
|
|
427
470
|
if (!response) throw new Error(`Figma ${label} response was empty.`);
|
|
@@ -431,11 +474,41 @@ export function providerJson(envelope: unknown, label: string): unknown {
|
|
|
431
474
|
);
|
|
432
475
|
}
|
|
433
476
|
if (response.ok === false) {
|
|
477
|
+
const jsonBody = response.json as {
|
|
478
|
+
message?: string;
|
|
479
|
+
error?: string;
|
|
480
|
+
} | null;
|
|
434
481
|
const detail =
|
|
435
482
|
(typeof response.text === "string" && response.text.trim()) ||
|
|
483
|
+
(typeof jsonBody?.message === "string" && jsonBody.message) ||
|
|
436
484
|
response.statusText ||
|
|
437
485
|
`HTTP ${response.status ?? "error"}`;
|
|
438
|
-
|
|
486
|
+
const err = new Error(`Figma ${label} request failed: ${detail}`);
|
|
487
|
+
if (response.status === 429) {
|
|
488
|
+
const rateLimitError = err as FigmaRateLimitError;
|
|
489
|
+
rateLimitError.statusCode = 429;
|
|
490
|
+
|
|
491
|
+
const retryAfterHeader = response.headers?.["retry-after"];
|
|
492
|
+
if (retryAfterHeader) {
|
|
493
|
+
rateLimitError.retryAfterSeconds = parseInt(retryAfterHeader, 10);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const planTier = response.headers?.["x-figma-plan-tier"];
|
|
497
|
+
if (planTier && FIGMA_PLAN_TIERS.has(planTier)) {
|
|
498
|
+
rateLimitError.figmaPlanTier = planTier;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const rateLimitType = response.headers?.["x-figma-rate-limit-type"];
|
|
502
|
+
if (rateLimitType === "low" || rateLimitType === "high") {
|
|
503
|
+
rateLimitError.figmaRateLimitType = rateLimitType;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const upgradeUrl = figmaUpgradeUrl(
|
|
507
|
+
response.headers?.["x-figma-upgrade-link"],
|
|
508
|
+
);
|
|
509
|
+
if (upgradeUrl) rateLimitError.figmaUpgradeUrl = upgradeUrl;
|
|
510
|
+
}
|
|
511
|
+
throw err;
|
|
439
512
|
}
|
|
440
513
|
return response.json;
|
|
441
514
|
}
|
|
@@ -567,47 +640,39 @@ async function fetchFallbackImageUrls(
|
|
|
567
640
|
nodeIds: string[],
|
|
568
641
|
): Promise<Record<string, string>> {
|
|
569
642
|
if (nodeIds.length === 0) return {};
|
|
570
|
-
const
|
|
643
|
+
const result: Record<string, string> = {};
|
|
644
|
+
const fetchBatch = async (ids: string[]) => {
|
|
645
|
+
if (ids.length === 0) return;
|
|
646
|
+
const envelope = await figmaGet(`/images/${fileKey}`, {
|
|
647
|
+
ids: ids.join(","),
|
|
648
|
+
format: "png",
|
|
649
|
+
scale: 2,
|
|
650
|
+
});
|
|
651
|
+
const json = providerJson(envelope, "images") as {
|
|
652
|
+
images?: Record<string, string | null | undefined>;
|
|
653
|
+
};
|
|
654
|
+
for (const [id, url] of Object.entries(json.images ?? {})) {
|
|
655
|
+
if (typeof url === "string" && url) result[id] = url;
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
// Batch over the full ID list by count and query length so complex frames
|
|
659
|
+
// don't silently drop layers beyond the first request.
|
|
571
660
|
let batch: string[] = [];
|
|
572
661
|
let queryChars = 0;
|
|
573
662
|
for (const nodeId of nodeIds) {
|
|
574
|
-
|
|
575
|
-
throw new Error("A Figma image fallback node id was unexpectedly long.");
|
|
576
|
-
}
|
|
577
|
-
const addedChars = nodeId.length + (batch.length > 0 ? 1 : 0);
|
|
663
|
+
const addedChars = nodeId.length + 1;
|
|
578
664
|
if (
|
|
579
665
|
batch.length >= MAX_FIGMA_IMAGE_IDS_PER_REQUEST ||
|
|
580
666
|
queryChars + addedChars > MAX_FIGMA_IMAGE_IDS_QUERY_CHARS
|
|
581
667
|
) {
|
|
582
|
-
|
|
668
|
+
await fetchBatch(batch);
|
|
583
669
|
batch = [];
|
|
584
670
|
queryChars = 0;
|
|
585
671
|
}
|
|
586
672
|
batch.push(nodeId);
|
|
587
|
-
queryChars +=
|
|
588
|
-
}
|
|
589
|
-
if (batch.length > 0) batches.push(batch);
|
|
590
|
-
|
|
591
|
-
const responses = await mapWithConcurrency(
|
|
592
|
-
batches,
|
|
593
|
-
MAX_CONCURRENT_FIGMA_IMAGE_UPLOADS,
|
|
594
|
-
async (ids) => {
|
|
595
|
-
const envelope = await figmaGet(`/images/${fileKey}`, {
|
|
596
|
-
ids: ids.join(","),
|
|
597
|
-
format: "png",
|
|
598
|
-
scale: 2,
|
|
599
|
-
});
|
|
600
|
-
return providerJson(envelope, "images") as {
|
|
601
|
-
images?: Record<string, string | null | undefined>;
|
|
602
|
-
};
|
|
603
|
-
},
|
|
604
|
-
);
|
|
605
|
-
const result: Record<string, string> = {};
|
|
606
|
-
for (const response of responses) {
|
|
607
|
-
for (const [id, url] of Object.entries(response.images ?? {})) {
|
|
608
|
-
if (typeof url === "string" && url) result[id] = url;
|
|
609
|
-
}
|
|
673
|
+
queryChars += addedChars;
|
|
610
674
|
}
|
|
675
|
+
await fetchBatch(batch);
|
|
611
676
|
return result;
|
|
612
677
|
}
|
|
613
678
|
|
|
@@ -628,6 +693,34 @@ async function fetchImageFillUrls(
|
|
|
628
693
|
return result;
|
|
629
694
|
}
|
|
630
695
|
|
|
696
|
+
/**
|
|
697
|
+
* Fetch CDN URLs for the given image-fill hex hashes via Figma's
|
|
698
|
+
* `/files/:key/images` endpoint, then mirror them to durable storage.
|
|
699
|
+
* Returns a Map from hex hash to durable URL. Hashes that Figma cannot
|
|
700
|
+
* resolve (deleted images, permission gaps) are omitted from the result.
|
|
701
|
+
*
|
|
702
|
+
* Used by `hydrate-figma-paste-images` to fill in `about:blank` placeholders
|
|
703
|
+
* that the local-kiwi clipboard decode path leaves behind.
|
|
704
|
+
*/
|
|
705
|
+
export async function resolveImageFillRefs(
|
|
706
|
+
fileKey: string,
|
|
707
|
+
hexHashes: string[],
|
|
708
|
+
): Promise<Map<string, string>> {
|
|
709
|
+
if (hexHashes.length === 0) return new Map();
|
|
710
|
+
const cdnUrls = await fetchImageFillUrls(fileKey, hexHashes);
|
|
711
|
+
const cdnUrlList = Object.values(cdnUrls).filter(
|
|
712
|
+
(u): u is string => typeof u === "string" && u.length > 0,
|
|
713
|
+
);
|
|
714
|
+
if (cdnUrlList.length === 0) return new Map();
|
|
715
|
+
const durableMap = await mirrorFigmaImageUrls(cdnUrlList);
|
|
716
|
+
const result = new Map<string, string>();
|
|
717
|
+
for (const [hash, cdnUrl] of Object.entries(cdnUrls)) {
|
|
718
|
+
const durableUrl = durableMap.get(cdnUrl);
|
|
719
|
+
if (durableUrl) result.set(hash, durableUrl);
|
|
720
|
+
}
|
|
721
|
+
return result;
|
|
722
|
+
}
|
|
723
|
+
|
|
631
724
|
export function sanitizeTitle(
|
|
632
725
|
name: string | undefined,
|
|
633
726
|
fallback: string,
|
|
@@ -673,7 +766,11 @@ export async function buildScreenFilesFromFigmaNodes(
|
|
|
673
766
|
source?: (nodeId: string, node: FigmaNode) => Record<string, unknown>;
|
|
674
767
|
sourceLabel?: (nodeId: string, node: FigmaNode) => string;
|
|
675
768
|
} = {},
|
|
676
|
-
): Promise<{
|
|
769
|
+
): Promise<{
|
|
770
|
+
files: ImportedDesignFile[];
|
|
771
|
+
fidelityEntries: FidelityEntry[];
|
|
772
|
+
missingImageFillCount: number;
|
|
773
|
+
}> {
|
|
677
774
|
const entries = Object.entries(nodesById);
|
|
678
775
|
const fallbackNodeIds = new Set<string>();
|
|
679
776
|
const imageFillRefs = new Set<string>();
|
|
@@ -696,16 +793,16 @@ export async function buildScreenFilesFromFigmaNodes(
|
|
|
696
793
|
(nodeId) => !fallbackImageUrls[nodeId],
|
|
697
794
|
);
|
|
698
795
|
if (missingFallbackNodeIds.length > 0) {
|
|
699
|
-
|
|
700
|
-
`
|
|
796
|
+
console.warn(
|
|
797
|
+
`[figma-import] ${missingFallbackNodeIds.length} fallback layer(s) could not be rendered and will be omitted (${missingFallbackNodeIds.slice(0, 5).join(", ")}${missingFallbackNodeIds.length > 5 ? ", …" : ""}).`,
|
|
701
798
|
);
|
|
702
799
|
}
|
|
703
800
|
const missingImageFillRefs = Array.from(imageFillRefs).filter(
|
|
704
801
|
(imageRef) => !imageFillUrls[imageRef],
|
|
705
802
|
);
|
|
706
803
|
if (missingImageFillRefs.length > 0) {
|
|
707
|
-
|
|
708
|
-
`
|
|
804
|
+
console.warn(
|
|
805
|
+
`[figma-import] ${missingImageFillRefs.length} image fill ref(s) could not be resolved (likely from a component library file); those fills will be omitted.`,
|
|
709
806
|
);
|
|
710
807
|
}
|
|
711
808
|
const durableUrls = await mirrorFigmaImageUrls([
|
|
@@ -763,5 +860,8 @@ export async function buildScreenFilesFromFigmaNodes(
|
|
|
763
860
|
});
|
|
764
861
|
}
|
|
765
862
|
|
|
766
|
-
|
|
863
|
+
const finalMissingCount = missingImageFillRefs.filter(
|
|
864
|
+
(r) => !imageFillUrls[r],
|
|
865
|
+
).length;
|
|
866
|
+
return { files, fidelityEntries, missingImageFillCount: finalMissingCount };
|
|
767
867
|
}
|
|
@@ -65,7 +65,18 @@ registerRequiredSecret({
|
|
|
65
65
|
"User-Agent": "AgentNative/1.0",
|
|
66
66
|
},
|
|
67
67
|
});
|
|
68
|
-
|
|
68
|
+
console.log("[figma-validator] GET /v1/me status:", res.status);
|
|
69
|
+
if (res.ok) {
|
|
70
|
+
console.log("[figma-validator] token accepted");
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
let body = "";
|
|
74
|
+
try {
|
|
75
|
+
body = await res.text();
|
|
76
|
+
} catch {
|
|
77
|
+
/* ignore */
|
|
78
|
+
}
|
|
79
|
+
console.log("[figma-validator] Figma error body:", body);
|
|
69
80
|
if (res.status === 401 || res.status === 403) {
|
|
70
81
|
return {
|
|
71
82
|
ok: false,
|
|
@@ -73,7 +84,8 @@ registerRequiredSecret({
|
|
|
73
84
|
};
|
|
74
85
|
}
|
|
75
86
|
return { ok: false, error: `Figma returned ${res.status}.` };
|
|
76
|
-
} catch {
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.log("[figma-validator] fetch threw:", err);
|
|
77
89
|
return {
|
|
78
90
|
ok: false,
|
|
79
91
|
error: "Could not reach Figma. Check your network and try again.",
|
|
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
62
62
|
error: string;
|
|
63
63
|
states?: undefined;
|
|
64
64
|
} | {
|
|
65
|
-
error?: undefined;
|
|
66
65
|
states: {
|
|
67
66
|
clientId: number;
|
|
68
67
|
state: string;
|
|
69
68
|
}[];
|
|
69
|
+
error?: undefined;
|
|
70
70
|
}>>;
|
|
71
71
|
/**
|
|
72
72
|
* GET /_agent-native/collab/:docId/users
|
|
@@ -77,10 +77,10 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
|
|
|
77
77
|
error: string;
|
|
78
78
|
users?: undefined;
|
|
79
79
|
} | {
|
|
80
|
-
error?: undefined;
|
|
81
80
|
users: {
|
|
82
81
|
clientId: number;
|
|
83
82
|
lastSeen: number;
|
|
84
83
|
}[];
|
|
84
|
+
error?: undefined;
|
|
85
85
|
}>>;
|
|
86
86
|
//# sourceMappingURL=awareness.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"awareness.d.ts","sourceRoot":"","sources":["../../src/collab/awareness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAgB3C,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAOD,eAAO,MAAM,sBAAsB,EAAG,kBAA2B,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAOD,wBAAgB,mBAAmB,IAAI,YAAY,CAElD;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,cAAc,GAAG,SAAS,GAChC,IAAI,CAON;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,KAAK,CAAC,EAAE,cAAc,GACrB,IAAI,CAgBN;AAoBD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAmB,GAC7B,IAAI,CAON;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE1E;AAiBD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAO1E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,IAAI,CAOnE;AAkCD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa
|
|
1
|
+
{"version":3,"file":"awareness.d.ts","sourceRoot":"","sources":["../../src/collab/awareness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAgB3C,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAOD,eAAO,MAAM,sBAAsB,EAAG,kBAA2B,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAOD,wBAAgB,mBAAmB,IAAI,YAAY,CAElD;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,cAAc,GAAG,SAAS,GAChC,IAAI,CAON;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,KAAK,CAAC,EAAE,cAAc,GACrB,IAAI,CAgBN;AAoBD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAmB,GAC7B,IAAI,CAON;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE1E;AAiBD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAO1E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,IAAI,CAOnE;AAkCD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;;kBAwDa,MAAM;eAAS,MAAM;;;GAoB1D,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;kBAYM,MAAM;kBAAY,MAAM;;;GAMvD,CAAC"}
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
|
|
14
14
|
*/
|
|
15
15
|
export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
16
|
-
error: string;
|
|
17
16
|
ok?: undefined;
|
|
17
|
+
error: string;
|
|
18
18
|
} | {
|
|
19
19
|
error?: undefined;
|
|
20
20
|
ok: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"figma-node-to-html.d.ts","sourceRoot":"","sources":["../../src/ingestion/figma-node-to-html.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;
|
|
1
|
+
{"version":3,"file":"figma-node-to-html.d.ts","sourceRoot":"","sources":["../../src/ingestion/figma-node-to-html.ts"],"names":[],"mappings":"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"}
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
* which is a documented follow-up if a specific design surfaces a visible
|
|
62
62
|
* mismatch.
|
|
63
63
|
*/
|
|
64
|
+
import { cssBlendMode, gradientAngleDegrees as gradientAngleDegreesMath, remapLinearStopPosition as remapLinearStopPositionMath, resolveGradientHandles, vectorLength, } from "./figma-paint-math.js";
|
|
64
65
|
const UNSUPPORTED_STRUCTURAL_TYPES = new Set([
|
|
65
66
|
"BOOLEAN_OPERATION",
|
|
66
67
|
"VECTOR",
|
|
@@ -196,11 +197,11 @@ class FidelityTracker {
|
|
|
196
197
|
return { entries, summary };
|
|
197
198
|
}
|
|
198
199
|
}
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Gradient angle / position derivation
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
199
203
|
function resolveGradientGeometry(paint) {
|
|
200
|
-
|
|
201
|
-
if (!handles || handles.length < 3)
|
|
202
|
-
return null;
|
|
203
|
-
return { start: handles[0], end: handles[1], width: handles[2] };
|
|
204
|
+
return resolveGradientHandles(paint.gradientHandlePositions);
|
|
204
205
|
}
|
|
205
206
|
/**
|
|
206
207
|
* Derive a CSS `linear-gradient()` angle (degrees) from Figma's normalized
|
|
@@ -218,14 +219,7 @@ function resolveGradientGeometry(paint) {
|
|
|
218
219
|
* dx = 0, dy = 1*h -> atan2(dy, 0) = 90deg -> +90 = 180deg. Matches.
|
|
219
220
|
*/
|
|
220
221
|
export function gradientAngleDegrees(paint, box) {
|
|
221
|
-
|
|
222
|
-
if (!geometry)
|
|
223
|
-
return null;
|
|
224
|
-
const dx = (geometry.end.x - geometry.start.x) * box.width;
|
|
225
|
-
const dy = (geometry.end.y - geometry.start.y) * box.height;
|
|
226
|
-
const angleRad = Math.atan2(dy, dx);
|
|
227
|
-
const angleDeg = (angleRad * 180) / Math.PI + 90;
|
|
228
|
-
return ((angleDeg % 360) + 360) % 360;
|
|
222
|
+
return gradientAngleDegreesMath(paint, box);
|
|
229
223
|
}
|
|
230
224
|
function gradientStopsCss(paint, remapPosition) {
|
|
231
225
|
const stops = paint.gradientStops ?? [];
|
|
@@ -239,51 +233,8 @@ function gradientStopsCss(paint, remapPosition) {
|
|
|
239
233
|
})
|
|
240
234
|
.join(", ");
|
|
241
235
|
}
|
|
242
|
-
/**
|
|
243
|
-
* CSS `linear-gradient(angle, ...)` always stretches its 0%/100% stops
|
|
244
|
-
* across the box's FULL diagonal extent at that angle (the CSS spec's
|
|
245
|
-
* "gradient line" always spans corner-to-corner) -- it has no way to say
|
|
246
|
-
* "start partway in, end partway in" the way Figma's actual gradient handles
|
|
247
|
-
* can (a designer can drag the start/end handles anywhere, including short
|
|
248
|
-
* of the shape's edges, or past them). Figma's own stop positions are
|
|
249
|
-
* fractions of the literal start-handle-to-end-handle distance, which only
|
|
250
|
-
* happens to coincide with the CSS full-box span when the handles are
|
|
251
|
-
* dragged exactly corner-to-corner -- a common case, but far from the only
|
|
252
|
-
* one, and the divergence gets worse the more the box's aspect ratio departs
|
|
253
|
-
* from square (rotated/skewed handles included, e.g. gradientTransform-authored
|
|
254
|
-
* paints). This projects each Figma stop's real pixel position onto the same
|
|
255
|
-
* angle CSS will use and re-expresses it as a percentage of the CSS line's
|
|
256
|
-
* length, so a partial/offset gradient renders at the same actual pixel
|
|
257
|
-
* positions Figma draws it at instead of silently stretching to fill the box.
|
|
258
|
-
*/
|
|
259
236
|
function remapLinearStopPosition(geometry, box, angleDeg) {
|
|
260
|
-
|
|
261
|
-
const ux = Math.sin(angleRad);
|
|
262
|
-
const uy = -Math.cos(angleRad);
|
|
263
|
-
const lineLength = box.width * Math.abs(ux) + box.height * Math.abs(uy);
|
|
264
|
-
if (lineLength < 1e-6)
|
|
265
|
-
return (position) => position;
|
|
266
|
-
const startPx = {
|
|
267
|
-
x: geometry.start.x * box.width,
|
|
268
|
-
y: geometry.start.y * box.height,
|
|
269
|
-
};
|
|
270
|
-
const endPx = {
|
|
271
|
-
x: geometry.end.x * box.width,
|
|
272
|
-
y: geometry.end.y * box.height,
|
|
273
|
-
};
|
|
274
|
-
const centerX = box.width / 2;
|
|
275
|
-
const centerY = box.height / 2;
|
|
276
|
-
return (position) => {
|
|
277
|
-
const pointX = startPx.x + position * (endPx.x - startPx.x);
|
|
278
|
-
const pointY = startPx.y + position * (endPx.y - startPx.y);
|
|
279
|
-
const projected = (pointX - centerX) * ux + (pointY - centerY) * uy;
|
|
280
|
-
return (projected + lineLength / 2) / lineLength;
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
function vectorLength(from, to, box) {
|
|
284
|
-
const dx = (to.x - from.x) * box.width;
|
|
285
|
-
const dy = (to.y - from.y) * box.height;
|
|
286
|
-
return Math.sqrt(dx * dx + dy * dy);
|
|
237
|
+
return remapLinearStopPositionMath(geometry, box, angleDeg);
|
|
287
238
|
}
|
|
288
239
|
/**
|
|
289
240
|
* Convert one Figma paint layer to a CSS `background-image` value (or plain
|
|
@@ -581,42 +532,17 @@ function buildEffects(node, isTextNode, tracker) {
|
|
|
581
532
|
// ---------------------------------------------------------------------------
|
|
582
533
|
// Blend modes
|
|
583
534
|
// ---------------------------------------------------------------------------
|
|
584
|
-
const CSS_BLEND_MODES = new Set([
|
|
585
|
-
"multiply",
|
|
586
|
-
"screen",
|
|
587
|
-
"overlay",
|
|
588
|
-
"darken",
|
|
589
|
-
"lighten",
|
|
590
|
-
"color-dodge",
|
|
591
|
-
"color-burn",
|
|
592
|
-
"hard-light",
|
|
593
|
-
"soft-light",
|
|
594
|
-
"difference",
|
|
595
|
-
"exclusion",
|
|
596
|
-
"hue",
|
|
597
|
-
"saturation",
|
|
598
|
-
"color",
|
|
599
|
-
"luminosity",
|
|
600
|
-
]);
|
|
601
|
-
const FIGMA_ONLY_BLEND_MODE_FALLBACK = {
|
|
602
|
-
LINEAR_BURN: "multiply",
|
|
603
|
-
LINEAR_DODGE: "plus-lighter",
|
|
604
|
-
LIGHTER: "plus-lighter",
|
|
605
|
-
DARKER: "darken",
|
|
606
|
-
};
|
|
607
535
|
function buildBlendMode(node, tracker) {
|
|
608
536
|
const mode = node.blendMode;
|
|
609
|
-
if (!mode
|
|
537
|
+
if (!mode)
|
|
538
|
+
return undefined;
|
|
539
|
+
const result = cssBlendMode(mode);
|
|
540
|
+
if (!result)
|
|
610
541
|
return undefined;
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
return cssMode;
|
|
614
|
-
const fallback = FIGMA_ONLY_BLEND_MODE_FALLBACK[mode];
|
|
615
|
-
if (fallback) {
|
|
616
|
-
tracker.record(node, "approximated", `Figma blend mode "${mode}" has no CSS equivalent; approximated as mix-blend-mode: ${fallback}.`);
|
|
617
|
-
return fallback;
|
|
542
|
+
if (result.verdict === "approximated") {
|
|
543
|
+
tracker.record(node, "approximated", `Figma blend mode "${mode}" has no CSS equivalent; approximated as mix-blend-mode: ${result.cssMode}.`);
|
|
618
544
|
}
|
|
619
|
-
return
|
|
545
|
+
return result.cssMode;
|
|
620
546
|
}
|
|
621
547
|
// ---------------------------------------------------------------------------
|
|
622
548
|
// Text styling
|