@agent-native/core 0.182.1-nightly-20260917183518 → 0.182.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 +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +6 -0
- package/corpus/templates/calendar/CHANGELOG.md +1 -0
- package/corpus/templates/clips/CHANGELOG.md +7 -0
- package/corpus/templates/clips/app/components/library/sort-menu.tsx +2 -2
- package/corpus/templates/clips/changelog/2026-09-17-clips-waits-for-authentication-before-showing-the-recording-.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-17-the-clips-sort-control-keeps-its-keyboard-focus-ring-visible.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/popup.html +1 -1
- package/corpus/templates/clips/chrome-extension/src/popup.ts +30 -21
- package/corpus/templates/content/CHANGELOG.md +7 -0
- package/corpus/templates/content/actions/resolve-preview-document-draft.ts +181 -16
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +28 -15
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +11 -0
- package/corpus/templates/content/app/components/editor/document-save-rebase.ts +13 -10
- package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +80 -0
- package/corpus/templates/content/changelog/2026-09-16-recovery-choices-stay-attached-to-the-draft-you-reviewed.md +6 -0
- package/corpus/templates/design/CHANGELOG.md +29 -0
- package/corpus/templates/design/actions/import-design-source.ts +42 -2
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +26 -15
- package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +83 -1
- package/corpus/templates/design/app/i18n-data.ts +3 -1
- package/corpus/templates/design/app/lib/design-file-upload.ts +6 -4
- package/corpus/templates/design/app/lib/design-import.ts +2 -0
- package/corpus/templates/design/app/lib/fig-client-import.ts +207 -49
- package/corpus/templates/design/changelog/2026-09-17-large-fig-files-import-directly-in-the-browser-without-the-o.md +6 -0
- package/corpus/templates/design/server/lib/fig-file-decoder.ts +31 -8
- package/corpus/templates/design/server/lib/import-design-files.ts +188 -75
- package/corpus/templates/design/server/plugins/db.ts +16 -7
- package/corpus/templates/design/shared/fig-to-frames.ts +55 -25
- package/corpus/templates/factory/CHANGELOG.md +6 -0
- package/corpus/templates/mail/CHANGELOG.md +6 -0
- package/corpus/templates/slides/CHANGELOG.md +12 -0
- package/corpus/templates/slides/app/components/editor/AssetLibraryPanel.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +1 -2
- package/corpus/templates/slides/app/hooks/use-agent-generating.ts +82 -2
- package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +9 -2
- package/corpus/templates/slides/app/lib/slide-image-replacement.ts +1 -1
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +1 -1
- package/corpus/templates/slides/server/handlers/assets.ts +134 -8
- package/corpus/templates/slides/server/handlers/uploads.ts +23 -10
- package/corpus/templates/slides/shared/upload-types.ts +1 -0
- package/dist/application-state/store.d.ts +9 -0
- package/dist/application-state/store.js +18 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +7 -0
- package/dist/file-upload/actions/upload-image.d.ts +41 -10
- package/dist/file-upload/actions/upload-image.js +399 -19
- package/dist/jobs/scheduler.js +11 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/package.json +4 -4
|
@@ -173,6 +173,85 @@ export function useDocumentReconcileRecovery({
|
|
|
173
173
|
[current, latestDraft, publish, retainLatest],
|
|
174
174
|
);
|
|
175
175
|
|
|
176
|
+
const resolveAutomatically = useCallback(
|
|
177
|
+
async (
|
|
178
|
+
initialDraft: ReconcileRecoveryDraft,
|
|
179
|
+
base: ReconcileSaveBase,
|
|
180
|
+
): Promise<boolean> => {
|
|
181
|
+
if (current.current || inFlight.current) {
|
|
182
|
+
generation.current += 1;
|
|
183
|
+
publish({
|
|
184
|
+
reason: current.current?.reason ?? "conflict",
|
|
185
|
+
...initialDraft,
|
|
186
|
+
saving: false,
|
|
187
|
+
});
|
|
188
|
+
try {
|
|
189
|
+
if (callbacks.current.retain)
|
|
190
|
+
await callbacks.current.retain(initialDraft);
|
|
191
|
+
} catch {
|
|
192
|
+
const latest = current.current;
|
|
193
|
+
if (latest)
|
|
194
|
+
publish({ ...latest, reason: "save-failed", saving: false });
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
inFlight.current = true;
|
|
199
|
+
const started = generation.current;
|
|
200
|
+
let saveBase: ReconcileSaveBase | undefined = base;
|
|
201
|
+
let draft = initialDraft;
|
|
202
|
+
let attempts = 0;
|
|
203
|
+
try {
|
|
204
|
+
while (
|
|
205
|
+
generation.current === started &&
|
|
206
|
+
!current.current &&
|
|
207
|
+
attempts < 3
|
|
208
|
+
) {
|
|
209
|
+
attempts += 1;
|
|
210
|
+
const identity = callbacks.current.getSaveIdentity();
|
|
211
|
+
const persisted = await callbacks.current.save(draft, saveBase);
|
|
212
|
+
if (generation.current !== started || current.current) {
|
|
213
|
+
await retainLatest();
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (!persisted) {
|
|
217
|
+
await retainLatest();
|
|
218
|
+
publish({
|
|
219
|
+
reason: "conflict",
|
|
220
|
+
...latestDraft(),
|
|
221
|
+
saving: false,
|
|
222
|
+
});
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (callbacks.current.getSaveIdentity() === identity) return true;
|
|
226
|
+
draft = latestDraft();
|
|
227
|
+
saveBase = undefined;
|
|
228
|
+
}
|
|
229
|
+
await retainLatest();
|
|
230
|
+
if (generation.current === started && !current.current) {
|
|
231
|
+
publish({ reason: "conflict", ...latestDraft(), saving: false });
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
} catch {
|
|
235
|
+
if (generation.current === started && !current.current) {
|
|
236
|
+
try {
|
|
237
|
+
await retainLatest();
|
|
238
|
+
} catch {
|
|
239
|
+
// The visible recovery state remains the final fallback.
|
|
240
|
+
}
|
|
241
|
+
publish({
|
|
242
|
+
reason: "save-failed",
|
|
243
|
+
...latestDraft(),
|
|
244
|
+
saving: false,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
} finally {
|
|
249
|
+
inFlight.current = false;
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
[current, latestDraft, publish, retainLatest],
|
|
253
|
+
);
|
|
254
|
+
|
|
176
255
|
const resolveChoice = useCallback(
|
|
177
256
|
async (
|
|
178
257
|
base: ReconcileSaveBase,
|
|
@@ -241,6 +320,7 @@ export function useDocumentReconcileRecovery({
|
|
|
241
320
|
updateDraft,
|
|
242
321
|
reportRetentionFailure,
|
|
243
322
|
resolve,
|
|
323
|
+
resolveAutomatically,
|
|
244
324
|
resolveChoice,
|
|
245
325
|
release,
|
|
246
326
|
current,
|
|
@@ -3,15 +3,35 @@
|
|
|
3
3
|
All notable user-facing changes to Design are documented here. Open it any time
|
|
4
4
|
from the command menu (Cmd+K → "What's new") or from Settings.
|
|
5
5
|
|
|
6
|
+
## 2026-09-17
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Design can open running localhost apps through the current browser session
|
|
11
|
+
|
|
12
|
+
### Improved
|
|
13
|
+
|
|
14
|
+
- Design loads faster and large .fig imports can target selected frames
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Large .fig files import directly in the browser without the old upload ceiling
|
|
19
|
+
|
|
6
20
|
## 2026-09-16
|
|
7
21
|
|
|
8
22
|
### Improved
|
|
9
23
|
|
|
24
|
+
- Design files avoid cold-start scans and open faster after idle periods
|
|
25
|
+
- Design marquee selections stay responsive on large screens
|
|
10
26
|
- Design editor agent chat uses shared conversation tabs and stays usable at narrow widths
|
|
11
27
|
- Empty template libraries now link directly back to your designs.
|
|
12
28
|
|
|
13
29
|
### Fixed
|
|
14
30
|
|
|
31
|
+
- Design comments keep keyboard mentions and selected recipients through drafts and replies
|
|
32
|
+
- Design Fill editing uses the visible background paint on text-bearing controls
|
|
33
|
+
- Design inspector controls match compact Figma spacing
|
|
34
|
+
- URL-backed visual edits keep their editor state through consent and source writeback
|
|
15
35
|
- Canvas zoom controls and keyboard shortcuts now update the canvas immediately
|
|
16
36
|
- Design auto layout controls stay aligned to the 28-column inspector grid
|
|
17
37
|
- Design review pins follow screens and keep comment actions together
|
|
@@ -26,6 +46,10 @@ from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
26
46
|
|
|
27
47
|
## 2026-09-15
|
|
28
48
|
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- Added draggable corner-radius handles to the selection outline (drag inward to round corners, like Figma) and polished the outer resize handle styling
|
|
52
|
+
|
|
29
53
|
### Improved
|
|
30
54
|
|
|
31
55
|
- Auto layout controls use a compact alignment and gap layout with smaller constraint controls.
|
|
@@ -41,6 +65,11 @@ from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
41
65
|
|
|
42
66
|
### Fixed
|
|
43
67
|
|
|
68
|
+
- Fixed .fig file import placing multi-frame flows in reverse order — frames now import in left-to-right canvas order instead of layer/creation order
|
|
69
|
+
- Fixed fill dialog tooltips (paint type tabs, eyedropper, document colors) rendering behind the dialog instead of in front of it
|
|
70
|
+
- Fixed the live width × height readout shown while drawing a new shape to render as a rounded blue pill, matching the equivalent resize readout
|
|
71
|
+
- Fixed polygon and star shapes getting letterboxed instead of resizing with their bounding box
|
|
72
|
+
- Fixed the width × height label shown while resizing an element rendering as plain unstyled text instead of a visible colored badge
|
|
44
73
|
- Alt-dragging a layer inside a group now keeps the duplicate under the cursor for the whole drag instead of jumping into place on release.
|
|
45
74
|
- Breakpoint width edits preserve the existing breakpoint, and new Screens stay clear of visible breakpoint previews.
|
|
46
75
|
- Changing a grid's column or row count no longer turns fill or hug tracks into fixed pixel sizes
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core/action";
|
|
2
|
+
import { commitUploadReceiptsForImport } from "@agent-native/core/file-upload/actions/upload-image";
|
|
2
3
|
import { assertAccess } from "@agent-native/core/sharing";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
|
|
@@ -6,11 +7,13 @@ import { snapshotDesignBeforeAgentEdit } from "../server/lib/design-versions.js"
|
|
|
6
7
|
import { saveFigmaPasteHtmlFallback } from "../server/lib/figma-paste-fallback.js";
|
|
7
8
|
import {
|
|
8
9
|
normalizeImportedHtmlDocument,
|
|
10
|
+
findImportedDesignFileByOperationSource,
|
|
9
11
|
resolveImportDesignId,
|
|
10
12
|
saveImportedDesignFiles,
|
|
11
13
|
} from "../server/lib/import-design-files.js";
|
|
14
|
+
import { MAX_FIG_FRAME_HTML_BYTES } from "../shared/fig-to-frames.js";
|
|
12
15
|
|
|
13
|
-
const MAX_HTML_IMPORT_BYTES =
|
|
16
|
+
const MAX_HTML_IMPORT_BYTES = MAX_FIG_FRAME_HTML_BYTES;
|
|
14
17
|
|
|
15
18
|
function ensureHtmlSize(content: string) {
|
|
16
19
|
if (Buffer.byteLength(content, "utf8") > MAX_HTML_IMPORT_BYTES) {
|
|
@@ -42,6 +45,9 @@ export default defineAction({
|
|
|
42
45
|
frameTitle: z.string().optional(),
|
|
43
46
|
frameWidth: z.number().optional(),
|
|
44
47
|
frameHeight: z.number().optional(),
|
|
48
|
+
clientImportId: z.string().max(200).optional(),
|
|
49
|
+
clientImportBatchId: z.string().max(200).optional(),
|
|
50
|
+
clientImportFinalFrame: z.boolean().optional(),
|
|
45
51
|
}),
|
|
46
52
|
run: async (
|
|
47
53
|
{
|
|
@@ -52,6 +58,9 @@ export default defineAction({
|
|
|
52
58
|
frameTitle,
|
|
53
59
|
frameWidth,
|
|
54
60
|
frameHeight,
|
|
61
|
+
clientImportId,
|
|
62
|
+
clientImportBatchId,
|
|
63
|
+
clientImportFinalFrame,
|
|
55
64
|
},
|
|
56
65
|
context,
|
|
57
66
|
) => {
|
|
@@ -64,6 +73,29 @@ export default defineAction({
|
|
|
64
73
|
// keeps every request far below the ~6MB a Netlify function will accept —
|
|
65
74
|
// the cap the server route has to chunk around.
|
|
66
75
|
if (sourceType === "fig-frame") {
|
|
76
|
+
const operationSource = clientImportId
|
|
77
|
+
? `fig-import:${clientImportId}`
|
|
78
|
+
: undefined;
|
|
79
|
+
if (operationSource) {
|
|
80
|
+
const existing = await findImportedDesignFileByOperationSource(
|
|
81
|
+
resolvedDesignId,
|
|
82
|
+
operationSource,
|
|
83
|
+
);
|
|
84
|
+
if (existing?.placed) {
|
|
85
|
+
if (clientImportBatchId && clientImportFinalFrame) {
|
|
86
|
+
await commitUploadReceiptsForImport(clientImportBatchId);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
designId: resolvedDesignId,
|
|
90
|
+
files: [existing.file],
|
|
91
|
+
warnings: [],
|
|
92
|
+
placedFrames: [],
|
|
93
|
+
overview: true,
|
|
94
|
+
urlPath: `/design/${resolvedDesignId}`,
|
|
95
|
+
stats: { sourceKind: "fig-frame", frameCount: 1 },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
67
99
|
await snapshotDesignBeforeAgentEdit(resolvedDesignId, context);
|
|
68
100
|
const saved = await saveImportedDesignFiles({
|
|
69
101
|
designId: resolvedDesignId,
|
|
@@ -76,7 +108,12 @@ export default defineAction({
|
|
|
76
108
|
content,
|
|
77
109
|
`experimental .fig upload ${originalName ?? "design"}`,
|
|
78
110
|
),
|
|
79
|
-
source: {
|
|
111
|
+
source: {
|
|
112
|
+
sourceType: "fig-frame",
|
|
113
|
+
originalName,
|
|
114
|
+
...(operationSource ? { operationSource } : {}),
|
|
115
|
+
},
|
|
116
|
+
operationSource,
|
|
80
117
|
preferredFrame: {
|
|
81
118
|
title: frameTitle,
|
|
82
119
|
width: frameWidth,
|
|
@@ -85,6 +122,9 @@ export default defineAction({
|
|
|
85
122
|
},
|
|
86
123
|
],
|
|
87
124
|
});
|
|
125
|
+
if (clientImportBatchId && clientImportFinalFrame) {
|
|
126
|
+
await commitUploadReceiptsForImport(clientImportBatchId);
|
|
127
|
+
}
|
|
88
128
|
return {
|
|
89
129
|
...saved,
|
|
90
130
|
stats: { sourceKind: "fig-frame", frameCount: saved.files.length },
|
|
@@ -25,7 +25,7 @@ import { Input } from "@/components/ui/input";
|
|
|
25
25
|
import { Label } from "@/components/ui/label";
|
|
26
26
|
import { Textarea } from "@/components/ui/textarea";
|
|
27
27
|
import {
|
|
28
|
-
|
|
28
|
+
MAX_FIG_UPLOAD_BYTES,
|
|
29
29
|
uploadDesignFile,
|
|
30
30
|
validateFigUploadFile,
|
|
31
31
|
} from "@/lib/design-file-upload";
|
|
@@ -144,6 +144,14 @@ export function DesignImportPanel(p: DesignImportPanelProps) {
|
|
|
144
144
|
}),
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
|
+
const skippedEmbeddedImageCount = result?.skippedEmbeddedImageCount;
|
|
148
|
+
if (skippedEmbeddedImageCount) {
|
|
149
|
+
fidelityWarnings.push(
|
|
150
|
+
t("designEditor.import.figUploadImagesSkippedWarning", {
|
|
151
|
+
count: formatNumber(skippedEmbeddedImageCount),
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
147
155
|
const notification = importResultNotification(result, fallback, {
|
|
148
156
|
fidelityWarnings,
|
|
149
157
|
});
|
|
@@ -322,7 +330,14 @@ export function DesignImportPanel(p: DesignImportPanelProps) {
|
|
|
322
330
|
},
|
|
323
331
|
});
|
|
324
332
|
} catch (localError) {
|
|
325
|
-
if (
|
|
333
|
+
if (
|
|
334
|
+
selection ||
|
|
335
|
+
prepared.file.size > MAX_FIG_UPLOAD_BYTES ||
|
|
336
|
+
(localError as { remoteMutationStarted?: unknown })
|
|
337
|
+
.remoteMutationStarted === true
|
|
338
|
+
) {
|
|
339
|
+
throw localError;
|
|
340
|
+
}
|
|
326
341
|
console.warn(
|
|
327
342
|
"[fig-import] in-browser conversion failed; falling back to the upload route.",
|
|
328
343
|
localError,
|
|
@@ -348,7 +363,7 @@ export function DesignImportPanel(p: DesignImportPanelProps) {
|
|
|
348
363
|
async (file: File | undefined) => {
|
|
349
364
|
if (!file) return;
|
|
350
365
|
setActiveMode("fig-upload");
|
|
351
|
-
const validationError = validateFigUploadFile(file);
|
|
366
|
+
const validationError = validateFigUploadFile(file, { maxBytes: null });
|
|
352
367
|
if (validationError === "invalid-extension") {
|
|
353
368
|
toast.error(t("designEditor.import.errors.uploadFailed"), {
|
|
354
369
|
description: t("designEditor.import.errors.invalidFigFile"),
|
|
@@ -356,15 +371,6 @@ export function DesignImportPanel(p: DesignImportPanelProps) {
|
|
|
356
371
|
if (figFileInputRef.current) figFileInputRef.current.value = "";
|
|
357
372
|
return;
|
|
358
373
|
}
|
|
359
|
-
if (validationError === "too-large") {
|
|
360
|
-
toast.error(t("designEditor.import.errors.uploadFailed"), {
|
|
361
|
-
description: t("designEditor.import.errors.figFileTooLarge", {
|
|
362
|
-
max: MAX_FIG_UPLOAD_MB,
|
|
363
|
-
}),
|
|
364
|
-
});
|
|
365
|
-
if (figFileInputRef.current) figFileInputRef.current.value = "";
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
374
|
|
|
369
375
|
setFigUploadName(file.name);
|
|
370
376
|
setFigUploadProgress(0);
|
|
@@ -393,6 +399,13 @@ export function DesignImportPanel(p: DesignImportPanelProps) {
|
|
|
393
399
|
return;
|
|
394
400
|
}
|
|
395
401
|
} catch (localError) {
|
|
402
|
+
if (
|
|
403
|
+
file.size > MAX_FIG_UPLOAD_BYTES ||
|
|
404
|
+
(localError as { remoteMutationStarted?: unknown })
|
|
405
|
+
.remoteMutationStarted === true
|
|
406
|
+
) {
|
|
407
|
+
throw localError;
|
|
408
|
+
}
|
|
396
409
|
console.warn(
|
|
397
410
|
"[fig-import] in-browser decode failed; falling back to the upload route.",
|
|
398
411
|
localError,
|
|
@@ -681,9 +694,7 @@ export function DesignImportPanel(p: DesignImportPanelProps) {
|
|
|
681
694
|
>
|
|
682
695
|
<div className="space-y-2 p-2">
|
|
683
696
|
<p className="text-[11px] leading-snug text-muted-foreground">
|
|
684
|
-
{t("designEditor.import.
|
|
685
|
-
max: MAX_FIG_UPLOAD_MB,
|
|
686
|
-
})}
|
|
697
|
+
{t("designEditor.import.figUploadDescriptionShort")}
|
|
687
698
|
</p>
|
|
688
699
|
{figImportPreview ? (
|
|
689
700
|
<div
|
|
@@ -20,6 +20,14 @@ const VOID_HTML_TAGS = new Set([
|
|
|
20
20
|
]);
|
|
21
21
|
|
|
22
22
|
const INSPECT_CODE_MAX_INLINE_TAG_LENGTH = 48;
|
|
23
|
+
const INSPECT_CODE_OPENING_TAG_PATTERN =
|
|
24
|
+
/<[a-zA-Z][\w:-]*(?:"[^"]*"|'[^']*'|[^'"<>])*\/?>/g;
|
|
25
|
+
const INSPECT_CODE_RAW_TEXT_TAGS = new Set([
|
|
26
|
+
"script",
|
|
27
|
+
"style",
|
|
28
|
+
"textarea",
|
|
29
|
+
"title",
|
|
30
|
+
]);
|
|
23
31
|
|
|
24
32
|
interface ParsedOpeningTag {
|
|
25
33
|
tagName: string;
|
|
@@ -163,9 +171,65 @@ function fallbackOpeningTag(
|
|
|
163
171
|
return `<${tag}${attrs.length ? ` ${attrs.join(" ")}` : ""}>`;
|
|
164
172
|
}
|
|
165
173
|
|
|
174
|
+
function formatInspectCodeMarkup(markup: string): string {
|
|
175
|
+
const formatted: string[] = [];
|
|
176
|
+
let cursor = 0;
|
|
177
|
+
|
|
178
|
+
while (cursor < markup.length) {
|
|
179
|
+
INSPECT_CODE_OPENING_TAG_PATTERN.lastIndex = cursor;
|
|
180
|
+
const match = INSPECT_CODE_OPENING_TAG_PATTERN.exec(markup);
|
|
181
|
+
if (!match) {
|
|
182
|
+
formatted.push(markup.slice(cursor));
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const tagIndex = match.index ?? cursor;
|
|
187
|
+
const commentIndex = markup.indexOf("<!--", cursor);
|
|
188
|
+
if (commentIndex !== -1 && commentIndex < tagIndex) {
|
|
189
|
+
const commentEnd = markup.indexOf("-->", commentIndex + 4);
|
|
190
|
+
if (commentEnd === -1) {
|
|
191
|
+
formatted.push(markup.slice(cursor));
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
formatted.push(markup.slice(cursor, commentEnd + 3));
|
|
195
|
+
cursor = commentEnd + 3;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const openingTag = match[0];
|
|
200
|
+
formatted.push(markup.slice(cursor, tagIndex));
|
|
201
|
+
formatted.push(formatInspectCodeOpeningTag(openingTag));
|
|
202
|
+
cursor = tagIndex + openingTag.length;
|
|
203
|
+
|
|
204
|
+
const tagName = tagNameFromOpeningTag(openingTag);
|
|
205
|
+
if (
|
|
206
|
+
!tagName ||
|
|
207
|
+
!INSPECT_CODE_RAW_TEXT_TAGS.has(tagName) ||
|
|
208
|
+
isSelfClosingOpeningTag(openingTag, tagName)
|
|
209
|
+
) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const closingTag = new RegExp(`</\\s*${tagName}\\s*>`, "i").exec(
|
|
214
|
+
markup.slice(cursor),
|
|
215
|
+
);
|
|
216
|
+
if (!closingTag) {
|
|
217
|
+
formatted.push(markup.slice(cursor));
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
const closingIndex = cursor + (closingTag.index ?? 0);
|
|
221
|
+
const closingEnd = closingIndex + closingTag[0].length;
|
|
222
|
+
formatted.push(markup.slice(cursor, closingEnd));
|
|
223
|
+
cursor = closingEnd;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return formatted.join("");
|
|
227
|
+
}
|
|
228
|
+
|
|
166
229
|
export function elementHtmlPreview(
|
|
167
230
|
data: Pick<InspectCodeData, "html" | "tagName" | "id" | "classes">,
|
|
168
231
|
): string | null {
|
|
232
|
+
const sourceHtml = data.html?.trim();
|
|
169
233
|
const openingTag = openingTagOf(data.html);
|
|
170
234
|
const hasFallbackMetadata = Boolean(
|
|
171
235
|
data.tagName?.trim() ||
|
|
@@ -182,7 +246,25 @@ export function elementHtmlPreview(
|
|
|
182
246
|
if (isSelfClosingOpeningTag(previewOpeningTag, tagName)) {
|
|
183
247
|
return previewOpeningTag;
|
|
184
248
|
}
|
|
185
|
-
|
|
249
|
+
const closingTag = sourceHtml
|
|
250
|
+
? new RegExp(`</\\s*${tagName}\\s*>\\s*$`, "i").exec(sourceHtml)
|
|
251
|
+
: null;
|
|
252
|
+
if (
|
|
253
|
+
!sourceHtml ||
|
|
254
|
+
!openingTag ||
|
|
255
|
+
!sourceHtml.startsWith(openingTag) ||
|
|
256
|
+
!closingTag
|
|
257
|
+
) {
|
|
258
|
+
return `${previewOpeningTag}\n ...\n</${tagName}>`;
|
|
259
|
+
}
|
|
260
|
+
const innerHtml = sourceHtml
|
|
261
|
+
.slice(openingTag.length, closingTag.index)
|
|
262
|
+
.trim();
|
|
263
|
+
if (!innerHtml) return `${previewOpeningTag}</${tagName}>`;
|
|
264
|
+
const formattedInnerHtml = formatInspectCodeMarkup(innerHtml)
|
|
265
|
+
.trim()
|
|
266
|
+
.replace(/^/gm, " ");
|
|
267
|
+
return `${previewOpeningTag}\n${formattedInnerHtml}\n</${tagName}>`;
|
|
186
268
|
}
|
|
187
269
|
|
|
188
270
|
type HtmlTokenKind = "plain" | "punctuation" | "tag" | "attribute" | "value";
|
|
@@ -693,7 +693,9 @@ const enUS = {
|
|
|
693
693
|
figImportAll: "Import all",
|
|
694
694
|
figImportAnalyzing: "Analyzing…",
|
|
695
695
|
figUploadDescriptionShort:
|
|
696
|
-
"Local import — no Figma API quota used.
|
|
696
|
+
"Local import — no Figma API quota used. Supported embedded images are included.",
|
|
697
|
+
figUploadImagesSkippedWarning:
|
|
698
|
+
"Skipped embedded images ({{count}}) because browser uploads have a per-image limit.",
|
|
697
699
|
figmaPasteBodyUnlimited:
|
|
698
700
|
"Works without a Figma token — geometry, layout, and text import immediately.",
|
|
699
701
|
figmaPasteBodyImages:
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { parseUploadResponse, type ImportResult } from "@/lib/design-import";
|
|
2
2
|
import { MAX_UPLOAD_BYTES } from "@/lib/upload-limits";
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
6
|
-
* the server value by the alignment test in `design-file-upload.test.ts`. */
|
|
4
|
+
/** Server fallback and token-free image-hydration ceiling. Browser-local `.fig`
|
|
5
|
+
* imports do not use this raw-file cap. */
|
|
7
6
|
export const MAX_FIG_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
8
7
|
export const MAX_FIG_UPLOAD_MB = MAX_FIG_UPLOAD_BYTES / 1024 / 1024;
|
|
9
8
|
|
|
@@ -15,9 +14,12 @@ export type FigUploadValidationError = "invalid-extension" | "too-large";
|
|
|
15
14
|
|
|
16
15
|
export function validateFigUploadFile(
|
|
17
16
|
file: Pick<File, "name" | "size">,
|
|
17
|
+
options?: { maxBytes?: number | null },
|
|
18
18
|
): FigUploadValidationError | null {
|
|
19
19
|
if (!file.name.toLowerCase().endsWith(".fig")) return "invalid-extension";
|
|
20
|
-
|
|
20
|
+
const maxBytes =
|
|
21
|
+
options?.maxBytes === undefined ? MAX_FIG_UPLOAD_BYTES : options.maxBytes;
|
|
22
|
+
if (maxBytes !== null && file.size > maxBytes) return "too-large";
|
|
21
23
|
return null;
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -41,6 +41,8 @@ export interface ImportResult {
|
|
|
41
41
|
unresolvedImages?: number;
|
|
42
42
|
/** Set by .fig file upload: number of IMAGE fills not in the embedded blobs (need Figma API to resolve). */
|
|
43
43
|
unresolvedImageRefCount?: number;
|
|
44
|
+
/** Set by the browser .fig importer when transport-sized embedded images are skipped. */
|
|
45
|
+
skippedEmbeddedImageCount?: number;
|
|
44
46
|
/** Set by import-figma-clipboard when it fell back: why the REST match didn't happen. */
|
|
45
47
|
matchStatus?: "matched" | "ambiguous" | "none" | "error";
|
|
46
48
|
rateLimitRetryAfter?: number;
|