@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.
Files changed (56) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +6 -0
  3. package/corpus/templates/calendar/CHANGELOG.md +1 -0
  4. package/corpus/templates/clips/CHANGELOG.md +7 -0
  5. package/corpus/templates/clips/app/components/library/sort-menu.tsx +2 -2
  6. package/corpus/templates/clips/changelog/2026-09-17-clips-waits-for-authentication-before-showing-the-recording-.md +6 -0
  7. package/corpus/templates/clips/changelog/2026-09-17-the-clips-sort-control-keeps-its-keyboard-focus-ring-visible.md +6 -0
  8. package/corpus/templates/clips/chrome-extension/src/popup.html +1 -1
  9. package/corpus/templates/clips/chrome-extension/src/popup.ts +30 -21
  10. package/corpus/templates/content/CHANGELOG.md +7 -0
  11. package/corpus/templates/content/actions/resolve-preview-document-draft.ts +181 -16
  12. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +28 -15
  13. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +11 -0
  14. package/corpus/templates/content/app/components/editor/document-save-rebase.ts +13 -10
  15. package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +80 -0
  16. package/corpus/templates/content/changelog/2026-09-16-recovery-choices-stay-attached-to-the-draft-you-reviewed.md +6 -0
  17. package/corpus/templates/design/CHANGELOG.md +29 -0
  18. package/corpus/templates/design/actions/import-design-source.ts +42 -2
  19. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +26 -15
  20. package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +83 -1
  21. package/corpus/templates/design/app/i18n-data.ts +3 -1
  22. package/corpus/templates/design/app/lib/design-file-upload.ts +6 -4
  23. package/corpus/templates/design/app/lib/design-import.ts +2 -0
  24. package/corpus/templates/design/app/lib/fig-client-import.ts +207 -49
  25. package/corpus/templates/design/changelog/2026-09-17-large-fig-files-import-directly-in-the-browser-without-the-o.md +6 -0
  26. package/corpus/templates/design/server/lib/fig-file-decoder.ts +31 -8
  27. package/corpus/templates/design/server/lib/import-design-files.ts +188 -75
  28. package/corpus/templates/design/server/plugins/db.ts +16 -7
  29. package/corpus/templates/design/shared/fig-to-frames.ts +55 -25
  30. package/corpus/templates/factory/CHANGELOG.md +6 -0
  31. package/corpus/templates/mail/CHANGELOG.md +6 -0
  32. package/corpus/templates/slides/CHANGELOG.md +12 -0
  33. package/corpus/templates/slides/app/components/editor/AssetLibraryPanel.tsx +1 -1
  34. package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +1 -2
  35. package/corpus/templates/slides/app/hooks/use-agent-generating.ts +82 -2
  36. package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +9 -2
  37. package/corpus/templates/slides/app/lib/slide-image-replacement.ts +1 -1
  38. package/corpus/templates/slides/app/pages/DeckEditor.tsx +1 -1
  39. package/corpus/templates/slides/server/handlers/assets.ts +134 -8
  40. package/corpus/templates/slides/server/handlers/uploads.ts +23 -10
  41. package/corpus/templates/slides/shared/upload-types.ts +1 -0
  42. package/dist/application-state/store.d.ts +9 -0
  43. package/dist/application-state/store.js +18 -0
  44. package/dist/automation/index.d.ts +1 -1
  45. package/dist/collab/struct-routes.d.ts +1 -1
  46. package/dist/deploy/build.js +7 -0
  47. package/dist/file-upload/actions/upload-image.d.ts +41 -10
  48. package/dist/file-upload/actions/upload-image.js +399 -19
  49. package/dist/jobs/scheduler.js +11 -0
  50. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  51. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  52. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  53. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  54. package/dist/resources/handlers.d.ts +1 -1
  55. package/dist/secrets/routes.d.ts +3 -3
  56. 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,
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-16
4
+ ---
5
+
6
+ Content now combines non-overlapping edits from another tab or agent automatically, while recovery choices stay attached to the exact draft and saved page version you reviewed.
@@ -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 = 2 * 1024 * 1024;
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: { sourceType: "fig-frame", originalName },
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
- MAX_FIG_UPLOAD_MB,
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 (selection) throw localError;
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.figUploadDescription", {
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
- return `${previewOpeningTag}\n ...\n</${tagName}>`;
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. Embedded images included.",
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
- /** Mirrors `server/lib/fig-file-limits.ts`'s decoder cap — the real ceiling for
5
- * a `.fig` import now that files above the wire cap upload in chunks. Held to
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
- if (file.size > MAX_FIG_UPLOAD_BYTES) return "too-large";
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;