@agent-native/core 0.137.7 → 0.137.8
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/clips/app/components/recorder/pre-record-panel.tsx +65 -18
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
- package/corpus/templates/clips/desktop/package.json +1 -0
- package/corpus/templates/clips/desktop/src/app.tsx +70 -6
- package/corpus/templates/clips/desktop/src/components/AlertDialog.tsx +131 -0
- package/corpus/templates/clips/desktop/src/styles.css +129 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +2 -0
- package/corpus/templates/design/actions/generate-design.ts +3 -2
- package/corpus/templates/design/actions/update-design.ts +35 -14
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +7 -2
- package/corpus/templates/design/app/components/design/FigmaHydrationDialog.tsx +11 -2
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +8 -1
- package/corpus/templates/design/app/i18n-data.ts +53 -22
- package/corpus/templates/design/app/lib/design-file-upload.ts +2 -3
- package/corpus/templates/design/app/lib/upload-limits.ts +4 -0
- package/corpus/templates/design/changelog/2026-08-05-chat-attachments-now-warn-before-upload-when-they-exceed-the.md +6 -0
- package/corpus/templates/design/changelog/2026-08-05-figma-fig-uploads-now-show-a-clear-4-mb-limit-up-front-inste.md +6 -0
- package/corpus/templates/design/server/handlers/import-design-file.ts +9 -5
- package/corpus/templates/design/server/handlers/uploads.ts +13 -9
- package/corpus/templates/design/server/lib/fig-file-limits.ts +2 -1
- package/corpus/templates/design/server/lib/figma-image-hydration.ts +1 -1
- package/corpus/templates/design/server/lib/request-body-limits.ts +11 -0
- package/corpus/templates/design/shared/canvas-frames.ts +81 -0
- package/dist/client/require-session.js +13 -4
- package/dist/client/use-session.d.ts +11 -0
- package/dist/client/use-session.js +34 -6
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
getFigmaConnectionStatus,
|
|
30
30
|
saveFigmaAccessToken,
|
|
31
31
|
} from "@/lib/figma-connection";
|
|
32
|
+
import { MAX_UPLOAD_MB } from "@/lib/upload-limits";
|
|
32
33
|
|
|
33
34
|
interface FigmaHydrationDialogProps {
|
|
34
35
|
open: boolean;
|
|
@@ -70,8 +71,16 @@ export function FigmaHydrationDialog({
|
|
|
70
71
|
const file = e.target.files?.[0];
|
|
71
72
|
e.target.value = "";
|
|
72
73
|
if (!file) return;
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
const validationError = validateFigUploadFile(file);
|
|
75
|
+
if (validationError) {
|
|
76
|
+
setError(
|
|
77
|
+
t(
|
|
78
|
+
validationError === "too-large"
|
|
79
|
+
? "designEditor.import.errors.figFileTooLarge"
|
|
80
|
+
: "designEditor.import.figmaHydrationInvalidFig",
|
|
81
|
+
{ max: MAX_UPLOAD_MB },
|
|
82
|
+
),
|
|
83
|
+
);
|
|
75
84
|
return;
|
|
76
85
|
}
|
|
77
86
|
setBusy(true);
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
TooltipContent,
|
|
53
53
|
TooltipTrigger,
|
|
54
54
|
} from "@/components/ui/tooltip";
|
|
55
|
+
import { MAX_UPLOAD_BYTES, MAX_UPLOAD_MB } from "@/lib/upload-limits";
|
|
55
56
|
import { cn } from "@/lib/utils";
|
|
56
57
|
|
|
57
58
|
export interface UploadedFile {
|
|
@@ -513,6 +514,12 @@ export default function PromptPopover({
|
|
|
513
514
|
const uploadFiles = useCallback(
|
|
514
515
|
async (files: File[]): Promise<UploadedFile[]> => {
|
|
515
516
|
if (files.length === 0) return [];
|
|
517
|
+
const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
518
|
+
if (totalBytes > MAX_UPLOAD_BYTES) {
|
|
519
|
+
throw new Error(
|
|
520
|
+
t("promptDialog.attachmentsTooLarge", { max: MAX_UPLOAD_MB }),
|
|
521
|
+
);
|
|
522
|
+
}
|
|
516
523
|
setUploading(true);
|
|
517
524
|
try {
|
|
518
525
|
const formData = new FormData();
|
|
@@ -552,7 +559,7 @@ export default function PromptPopover({
|
|
|
552
559
|
setUploading(false);
|
|
553
560
|
}
|
|
554
561
|
},
|
|
555
|
-
[],
|
|
562
|
+
[t],
|
|
556
563
|
);
|
|
557
564
|
|
|
558
565
|
const handleSubmit = useCallback(
|
|
@@ -635,7 +635,7 @@ const enUS = {
|
|
|
635
635
|
figmaHydrationFigError: "Couldn't read images from that .fig file.",
|
|
636
636
|
figUploadTitle: "Upload .fig",
|
|
637
637
|
figUploadDescription:
|
|
638
|
-
"Local import using Figma's .fig format — no API quota. Embedded images are included. Format may change between Figma versions. Maximum
|
|
638
|
+
"Local import using Figma's .fig format — no API quota. Embedded images are included. Format may change between Figma versions. Maximum {{max}} MB.",
|
|
639
639
|
chooseFigFile: "Choose .fig file",
|
|
640
640
|
figUploadUploading: "Uploading {{progress}}%",
|
|
641
641
|
figUploadProcessing: "Converting…",
|
|
@@ -683,7 +683,8 @@ const enUS = {
|
|
|
683
683
|
figmaPasteFailed: "Figma paste import failed",
|
|
684
684
|
uploadFailed: "File upload failed",
|
|
685
685
|
invalidFigFile: "Choose a file ending in .fig.",
|
|
686
|
-
figFileTooLarge:
|
|
686
|
+
figFileTooLarge:
|
|
687
|
+
"That .fig is too large — uploads are limited to {{max}} MB. In Figma, copy just the frame you want into a new file and export that as .fig, or use Paste from Figma instead.",
|
|
687
688
|
},
|
|
688
689
|
},
|
|
689
690
|
generationMayHaveStopped:
|
|
@@ -1249,6 +1250,8 @@ const enUS = {
|
|
|
1249
1250
|
assetAdded: "Asset added",
|
|
1250
1251
|
assetsNoImageUrl: "Assets did not return an image URL.",
|
|
1251
1252
|
failedToUploadFile: "Failed to upload file",
|
|
1253
|
+
attachmentsTooLarge:
|
|
1254
|
+
"Those attachments are too large. Uploads are limited to {{max}} MB in total — attach fewer or smaller files.",
|
|
1252
1255
|
failedToSubmitPrompt: "Failed to submit prompt",
|
|
1253
1256
|
skipPrompt: "Skip prompt",
|
|
1254
1257
|
designSystem: "Design system",
|
|
@@ -5590,6 +5593,8 @@ const designRawLiteralOverrides = {
|
|
|
5590
5593
|
assetAdded: "添加资产",
|
|
5591
5594
|
assetsNoImageUrl: "Assets 未返回图片 URL。",
|
|
5592
5595
|
failedToUploadFile: "上传文件失败",
|
|
5596
|
+
attachmentsTooLarge:
|
|
5597
|
+
"这些附件太大。上传总大小上限为 {{max}} MB — 请减少文件数量或使用更小的文件。",
|
|
5593
5598
|
designSystem: "Design系统",
|
|
5594
5599
|
noDesignSystem: "没有设计系统",
|
|
5595
5600
|
newDesignSystem: "新",
|
|
@@ -5749,6 +5754,8 @@ const designRawLiteralOverrides = {
|
|
|
5749
5754
|
assetAdded: "Activo agregado",
|
|
5750
5755
|
assetsNoImageUrl: "Assets no devolvió una URL de imagen.",
|
|
5751
5756
|
failedToUploadFile: "No se pudo cargar el archivo",
|
|
5757
|
+
attachmentsTooLarge:
|
|
5758
|
+
"Esos adjuntos son demasiado grandes. Las subidas están limitadas a {{max}} MB en total: adjunta menos archivos o más pequeños.",
|
|
5752
5759
|
designSystem: "sistema Design",
|
|
5753
5760
|
noDesignSystem: "Sin sistema de diseño",
|
|
5754
5761
|
newDesignSystem: "Nuevo",
|
|
@@ -5912,6 +5919,8 @@ const designRawLiteralOverrides = {
|
|
|
5912
5919
|
assetAdded: "Actif ajouté",
|
|
5913
5920
|
assetsNoImageUrl: "Assets n'a pas renvoyé d'URL d'image.",
|
|
5914
5921
|
failedToUploadFile: "Échec du téléchargement du fichier",
|
|
5922
|
+
attachmentsTooLarge:
|
|
5923
|
+
"Ces pièces jointes sont trop volumineuses. Les téléversements sont limités à {{max}} Mo au total : joignez moins de fichiers ou des fichiers plus petits.",
|
|
5915
5924
|
designSystem: "Système Design",
|
|
5916
5925
|
noDesignSystem: "Aucun système de conception",
|
|
5917
5926
|
newDesignSystem: "Nouveau",
|
|
@@ -6075,6 +6084,8 @@ const designRawLiteralOverrides = {
|
|
|
6075
6084
|
assetAdded: "Asset hinzugefügt",
|
|
6076
6085
|
assetsNoImageUrl: "Assets hat keine Bild-URL zurückgegeben.",
|
|
6077
6086
|
failedToUploadFile: "Datei konnte nicht hochgeladen werden",
|
|
6087
|
+
attachmentsTooLarge:
|
|
6088
|
+
"Diese Anhänge sind zu groß. Uploads sind auf insgesamt {{max}} MB begrenzt – hänge weniger oder kleinere Dateien an.",
|
|
6078
6089
|
designSystem: "Design-System",
|
|
6079
6090
|
noDesignSystem: "Kein Designsystem",
|
|
6080
6091
|
newDesignSystem: "Neu",
|
|
@@ -6238,6 +6249,8 @@ const designRawLiteralOverrides = {
|
|
|
6238
6249
|
assetAdded: "アセットが追加されました",
|
|
6239
6250
|
assetsNoImageUrl: "Assets が画像 URL を返しませんでした。",
|
|
6240
6251
|
failedToUploadFile: "ファイルのアップロードに失敗しました",
|
|
6252
|
+
attachmentsTooLarge:
|
|
6253
|
+
"添付ファイルが大きすぎます。アップロードは合計 {{max}} MB までです。ファイル数を減らすか、より小さいファイルを添付してください。",
|
|
6241
6254
|
designSystem: "Designシステム",
|
|
6242
6255
|
noDesignSystem: "デザインシステムがない",
|
|
6243
6256
|
newDesignSystem: "新しい",
|
|
@@ -6396,6 +6409,8 @@ const designRawLiteralOverrides = {
|
|
|
6396
6409
|
assetAdded: "자산이 추가됨",
|
|
6397
6410
|
assetsNoImageUrl: "Assets에서 이미지 URL을 반환하지 않았습니다.",
|
|
6398
6411
|
failedToUploadFile: "파일을 업로드하지 못했습니다.",
|
|
6412
|
+
attachmentsTooLarge:
|
|
6413
|
+
"첨부 파일이 너무 큽니다. 업로드는 총 {{max}}MB까지 지원됩니다. 파일 수를 줄이거나 더 작은 파일을 첨부하세요.",
|
|
6399
6414
|
designSystem: "Design 시스템",
|
|
6400
6415
|
noDesignSystem: "디자인 시스템 없음",
|
|
6401
6416
|
newDesignSystem: "새로운",
|
|
@@ -6556,6 +6571,8 @@ const designRawLiteralOverrides = {
|
|
|
6556
6571
|
assetAdded: "Recurso adicionado",
|
|
6557
6572
|
assetsNoImageUrl: "Assets não retornou uma URL de imagem.",
|
|
6558
6573
|
failedToUploadFile: "Falha ao carregar o arquivo",
|
|
6574
|
+
attachmentsTooLarge:
|
|
6575
|
+
"Esses anexos são grandes demais. Os envios são limitados a {{max}} MB no total — anexe menos arquivos ou arquivos menores.",
|
|
6559
6576
|
designSystem: "Sistema Design",
|
|
6560
6577
|
noDesignSystem: "Nenhum sistema de design",
|
|
6561
6578
|
newDesignSystem: "Novo",
|
|
@@ -6717,6 +6734,8 @@ const designRawLiteralOverrides = {
|
|
|
6717
6734
|
assetAdded: "संपत्ति जोड़ी गई",
|
|
6718
6735
|
assetsNoImageUrl: "Assets ने कोई छवि URL नहीं लौटाया।",
|
|
6719
6736
|
failedToUploadFile: "फ़ाइल अपलोड करने में विफल",
|
|
6737
|
+
attachmentsTooLarge:
|
|
6738
|
+
"ये attachments बहुत बड़े हैं। Uploads की कुल सीमा {{max}} MB है — कम या छोटी files attach करें।",
|
|
6720
6739
|
designSystem: "Design प्रणाली",
|
|
6721
6740
|
noDesignSystem: "कोई डिज़ाइन प्रणाली नहीं",
|
|
6722
6741
|
newDesignSystem: "नया",
|
|
@@ -6875,6 +6894,8 @@ const designRawLiteralOverrides = {
|
|
|
6875
6894
|
assetAdded: "تمت إضافة الأصول",
|
|
6876
6895
|
assetsNoImageUrl: "لم تُرجع Assets عنوان URL للصورة.",
|
|
6877
6896
|
failedToUploadFile: "فشل تحميل الملف",
|
|
6897
|
+
attachmentsTooLarge:
|
|
6898
|
+
"المرفقات كبيرة جدًا. الحد الأقصى للرفع هو {{max}} ميغابايت إجمالًا — أرفق ملفات أقل أو أصغر.",
|
|
6878
6899
|
designSystem: "نظام Design",
|
|
6879
6900
|
noDesignSystem: "لا يوجد نظام التصميم",
|
|
6880
6901
|
newDesignSystem: "جديد",
|
|
@@ -12233,7 +12254,7 @@ const designImportOverrides = {
|
|
|
12233
12254
|
"已從 Figma 填入 {{count}} 個圖片{{plural}}。",
|
|
12234
12255
|
figUploadTitle: "上傳 .fig",
|
|
12235
12256
|
figUploadDescription:
|
|
12236
|
-
"實驗性功能:Figma 的 .fig 格式為專有格式且可能變更。支援的圖層會轉為可編輯螢幕,部分功能可能不同。上限為
|
|
12257
|
+
"實驗性功能:Figma 的 .fig 格式為專有格式且可能變更。支援的圖層會轉為可編輯螢幕,部分功能可能不同。上限為 {{max}} MB。",
|
|
12237
12258
|
chooseFigFile: "選擇 .fig 檔案",
|
|
12238
12259
|
figUploadUploading: "上傳中 {{progress}}%",
|
|
12239
12260
|
figUploadProcessing: "轉換中…",
|
|
@@ -12265,7 +12286,8 @@ const designImportOverrides = {
|
|
|
12265
12286
|
figmaPasteFailed: "Figma 貼上匯入失敗",
|
|
12266
12287
|
uploadFailed: "檔案上傳失敗",
|
|
12267
12288
|
invalidFigFile: "請選擇副檔名為 .fig 的檔案。",
|
|
12268
|
-
figFileTooLarge:
|
|
12289
|
+
figFileTooLarge:
|
|
12290
|
+
"此 .fig 檔案太大 — 上傳上限為 {{max}} MB。請在 Figma 中將要匯入的畫框複製到新檔案,並匯出該檔案為 .fig,或改用「從 Figma 貼上」。",
|
|
12269
12291
|
},
|
|
12270
12292
|
},
|
|
12271
12293
|
},
|
|
@@ -12296,7 +12318,7 @@ const designImportOverrides = {
|
|
|
12296
12318
|
"已从 Figma 填入 {{count}} 张图片{{plural}}。",
|
|
12297
12319
|
figUploadTitle: "上传 .fig",
|
|
12298
12320
|
figUploadDescription:
|
|
12299
|
-
"实验性功能:Figma 的 .fig 格式为专有格式且可能变化。支持的图层会转换为可编辑屏幕,部分功能可能有所不同。最大
|
|
12321
|
+
"实验性功能:Figma 的 .fig 格式为专有格式且可能变化。支持的图层会转换为可编辑屏幕,部分功能可能有所不同。最大 {{max}} MB。",
|
|
12300
12322
|
chooseFigFile: "选择 .fig 文件",
|
|
12301
12323
|
figUploadUploading: "正在上传 {{progress}}%",
|
|
12302
12324
|
figUploadProcessing: "正在转换…",
|
|
@@ -12328,7 +12350,8 @@ const designImportOverrides = {
|
|
|
12328
12350
|
figmaPasteFailed: "Figma 粘贴导入失败",
|
|
12329
12351
|
uploadFailed: "文件上传失败",
|
|
12330
12352
|
invalidFigFile: "请选择以 .fig 结尾的文件。",
|
|
12331
|
-
figFileTooLarge:
|
|
12353
|
+
figFileTooLarge:
|
|
12354
|
+
"该 .fig 文件太大 — 上传上限为 {{max}} MB。请在 Figma 中将需要导入的画板复制到新文件,然后导出该文件为 .fig,或改用“从 Figma 粘贴”。",
|
|
12332
12355
|
},
|
|
12333
12356
|
},
|
|
12334
12357
|
},
|
|
@@ -12362,7 +12385,7 @@ const designImportOverrides = {
|
|
|
12362
12385
|
"{{count}} imagen{{plural}} rellenada{{plural}} desde Figma.",
|
|
12363
12386
|
figUploadTitle: "Subir .fig",
|
|
12364
12387
|
figUploadDescription:
|
|
12365
|
-
"Experimental: el formato .fig de Figma es propietario y puede cambiar. Las capas compatibles se convierten en pantallas editables; algunas funciones pueden variar. Máximo
|
|
12388
|
+
"Experimental: el formato .fig de Figma es propietario y puede cambiar. Las capas compatibles se convierten en pantallas editables; algunas funciones pueden variar. Máximo {{max}} MB.",
|
|
12366
12389
|
chooseFigFile: "Elegir archivo .fig",
|
|
12367
12390
|
figUploadUploading: "Subiendo {{progress}}%",
|
|
12368
12391
|
figUploadProcessing: "Convirtiendo…",
|
|
@@ -12396,7 +12419,8 @@ const designImportOverrides = {
|
|
|
12396
12419
|
figmaPasteFailed: "Error al importar el pegado de Figma",
|
|
12397
12420
|
uploadFailed: "Error al subir el archivo",
|
|
12398
12421
|
invalidFigFile: "Elige un archivo que termine en .fig.",
|
|
12399
|
-
figFileTooLarge:
|
|
12422
|
+
figFileTooLarge:
|
|
12423
|
+
"Ese .fig es demasiado grande: las subidas están limitadas a {{max}} MB. En Figma, copia solo el frame que quieras a un archivo nuevo y exporta ese archivo como .fig, o usa Pegar desde Figma.",
|
|
12400
12424
|
},
|
|
12401
12425
|
},
|
|
12402
12426
|
},
|
|
@@ -12430,7 +12454,7 @@ const designImportOverrides = {
|
|
|
12430
12454
|
"{{count}} image{{plural}} remplie{{plural}} depuis Figma.",
|
|
12431
12455
|
figUploadTitle: "Téléverser .fig",
|
|
12432
12456
|
figUploadDescription:
|
|
12433
|
-
"Expérimental : le format .fig de Figma est propriétaire et peut évoluer. Les calques pris en charge deviennent des écrans modifiables ; certaines fonctions peuvent différer. Maximum
|
|
12457
|
+
"Expérimental : le format .fig de Figma est propriétaire et peut évoluer. Les calques pris en charge deviennent des écrans modifiables ; certaines fonctions peuvent différer. Maximum {{max}} Mo.",
|
|
12434
12458
|
chooseFigFile: "Choisir un fichier .fig",
|
|
12435
12459
|
figUploadUploading: "Téléversement {{progress}} %",
|
|
12436
12460
|
figUploadProcessing: "Conversion…",
|
|
@@ -12464,7 +12488,8 @@ const designImportOverrides = {
|
|
|
12464
12488
|
figmaPasteFailed: "Échec de l’import du collage Figma",
|
|
12465
12489
|
uploadFailed: "Échec du téléversement",
|
|
12466
12490
|
invalidFigFile: "Choisissez un fichier se terminant par .fig.",
|
|
12467
|
-
figFileTooLarge:
|
|
12491
|
+
figFileTooLarge:
|
|
12492
|
+
"Ce .fig est trop volumineux : les téléversements sont limités à {{max}} Mo. Dans Figma, copiez uniquement le frame souhaité dans un nouveau fichier et exportez ce fichier en .fig, ou utilisez Coller depuis Figma.",
|
|
12468
12493
|
},
|
|
12469
12494
|
},
|
|
12470
12495
|
},
|
|
@@ -12498,7 +12523,7 @@ const designImportOverrides = {
|
|
|
12498
12523
|
"{{count}} Bild{{plural}} aus Figma ausgefüllt.",
|
|
12499
12524
|
figUploadTitle: ".fig hochladen",
|
|
12500
12525
|
figUploadDescription:
|
|
12501
|
-
"Experimentell: Das .fig-Format von Figma ist proprietär und kann sich ändern. Unterstützte Ebenen werden zu bearbeitbaren Screens; einige Funktionen können abweichen. Maximal
|
|
12526
|
+
"Experimentell: Das .fig-Format von Figma ist proprietär und kann sich ändern. Unterstützte Ebenen werden zu bearbeitbaren Screens; einige Funktionen können abweichen. Maximal {{max}} MB.",
|
|
12502
12527
|
chooseFigFile: ".fig-Datei wählen",
|
|
12503
12528
|
figUploadUploading: "Wird hochgeladen: {{progress}} %",
|
|
12504
12529
|
figUploadProcessing: "Wird konvertiert…",
|
|
@@ -12531,7 +12556,8 @@ const designImportOverrides = {
|
|
|
12531
12556
|
figmaPasteFailed: "Figma-Einfügeimport fehlgeschlagen",
|
|
12532
12557
|
uploadFailed: "Dateiupload fehlgeschlagen",
|
|
12533
12558
|
invalidFigFile: "Wähle eine Datei mit der Endung .fig.",
|
|
12534
|
-
figFileTooLarge:
|
|
12559
|
+
figFileTooLarge:
|
|
12560
|
+
"Diese .fig-Datei ist zu groß – Uploads sind auf {{max}} MB begrenzt. Kopiere in Figma nur den gewünschten Frame in eine neue Datei und exportiere diese als .fig, oder nutze „Aus Figma einfügen“.",
|
|
12535
12561
|
},
|
|
12536
12562
|
},
|
|
12537
12563
|
},
|
|
@@ -12565,7 +12591,7 @@ const designImportOverrides = {
|
|
|
12565
12591
|
"Figma から {{count}} 枚の画像{{plural}}が入力されました。",
|
|
12566
12592
|
figUploadTitle: ".fig をアップロード",
|
|
12567
12593
|
figUploadDescription:
|
|
12568
|
-
"試験的機能:Figma の .fig 形式は独自仕様で、変更される可能性があります。対応レイヤーは編集可能な画面になりますが、一部の機能は異なる場合があります。最大
|
|
12594
|
+
"試験的機能:Figma の .fig 形式は独自仕様で、変更される可能性があります。対応レイヤーは編集可能な画面になりますが、一部の機能は異なる場合があります。最大 {{max}} MB。",
|
|
12569
12595
|
chooseFigFile: ".fig ファイルを選択",
|
|
12570
12596
|
figUploadUploading: "アップロード中 {{progress}}%",
|
|
12571
12597
|
figUploadProcessing: "変換中…",
|
|
@@ -12599,7 +12625,8 @@ const designImportOverrides = {
|
|
|
12599
12625
|
figmaPasteFailed: "Figma 貼り付けのインポートに失敗しました",
|
|
12600
12626
|
uploadFailed: "ファイルのアップロードに失敗しました",
|
|
12601
12627
|
invalidFigFile: ".fig で終わるファイルを選択してください。",
|
|
12602
|
-
figFileTooLarge:
|
|
12628
|
+
figFileTooLarge:
|
|
12629
|
+
"この .fig ファイルは大きすぎます。アップロードは {{max}} MB までです。Figma で目的のフレームだけを新しいファイルにコピーし、そのファイルを .fig として書き出すか、「Figma から貼り付け」をご利用ください。",
|
|
12603
12630
|
},
|
|
12604
12631
|
},
|
|
12605
12632
|
},
|
|
@@ -12632,7 +12659,7 @@ const designImportOverrides = {
|
|
|
12632
12659
|
"Figma에서 {{count}}개의 이미지{{plural}}가 채워졌습니다.",
|
|
12633
12660
|
figUploadTitle: ".fig 업로드",
|
|
12634
12661
|
figUploadDescription:
|
|
12635
|
-
"실험적 기능: Figma의 .fig 형식은 독점 형식이며 변경될 수 있습니다. 지원되는 레이어는 편집 가능한 화면으로 변환되지만 일부 기능은 다를 수 있습니다. 최대
|
|
12662
|
+
"실험적 기능: Figma의 .fig 형식은 독점 형식이며 변경될 수 있습니다. 지원되는 레이어는 편집 가능한 화면으로 변환되지만 일부 기능은 다를 수 있습니다. 최대 {{max}}MB.",
|
|
12636
12663
|
chooseFigFile: ".fig 파일 선택",
|
|
12637
12664
|
figUploadUploading: "업로드 중 {{progress}}%",
|
|
12638
12665
|
figUploadProcessing: "변환 중…",
|
|
@@ -12666,7 +12693,8 @@ const designImportOverrides = {
|
|
|
12666
12693
|
figmaPasteFailed: "Figma 붙여넣기 가져오기 실패",
|
|
12667
12694
|
uploadFailed: "파일 업로드 실패",
|
|
12668
12695
|
invalidFigFile: ".fig로 끝나는 파일을 선택하세요.",
|
|
12669
|
-
figFileTooLarge:
|
|
12696
|
+
figFileTooLarge:
|
|
12697
|
+
"이 .fig 파일은 너무 큽니다. 업로드는 {{max}}MB까지 지원됩니다. Figma에서 원하는 프레임만 새 파일로 복사한 뒤 그 파일을 .fig로 내보내거나, 'Figma에서 붙여넣기'를 사용하세요.",
|
|
12670
12698
|
},
|
|
12671
12699
|
},
|
|
12672
12700
|
},
|
|
@@ -12701,7 +12729,7 @@ const designImportOverrides = {
|
|
|
12701
12729
|
"{{count}} imagem{{plural}} preenchida{{plural}} do Figma.",
|
|
12702
12730
|
figUploadTitle: "Enviar .fig",
|
|
12703
12731
|
figUploadDescription:
|
|
12704
|
-
"Experimental: o formato .fig do Figma é proprietário e pode mudar. As camadas compatíveis viram telas editáveis; alguns recursos podem ser diferentes. Máximo de
|
|
12732
|
+
"Experimental: o formato .fig do Figma é proprietário e pode mudar. As camadas compatíveis viram telas editáveis; alguns recursos podem ser diferentes. Máximo de {{max}} MB.",
|
|
12705
12733
|
chooseFigFile: "Escolher arquivo .fig",
|
|
12706
12734
|
figUploadUploading: "Enviando {{progress}}%",
|
|
12707
12735
|
figUploadProcessing: "Convertendo…",
|
|
@@ -12735,7 +12763,8 @@ const designImportOverrides = {
|
|
|
12735
12763
|
figmaPasteFailed: "Falha ao importar colagem do Figma",
|
|
12736
12764
|
uploadFailed: "Falha no upload do arquivo",
|
|
12737
12765
|
invalidFigFile: "Escolha um arquivo que termine em .fig.",
|
|
12738
|
-
figFileTooLarge:
|
|
12766
|
+
figFileTooLarge:
|
|
12767
|
+
"Esse .fig é grande demais — os envios são limitados a {{max}} MB. No Figma, copie apenas o frame desejado para um novo arquivo e exporte esse arquivo como .fig, ou use Colar do Figma.",
|
|
12739
12768
|
},
|
|
12740
12769
|
},
|
|
12741
12770
|
},
|
|
@@ -12769,7 +12798,7 @@ const designImportOverrides = {
|
|
|
12769
12798
|
"Figma से {{count}} छवि{{plural}} भरी गई{{plural}}।",
|
|
12770
12799
|
figUploadTitle: ".fig अपलोड करें",
|
|
12771
12800
|
figUploadDescription:
|
|
12772
|
-
"प्रायोगिक: Figma का .fig format proprietary है और बदल सकता है। समर्थित layers editable screens बनती हैं; कुछ features अलग हो सकते हैं। अधिकतम
|
|
12801
|
+
"प्रायोगिक: Figma का .fig format proprietary है और बदल सकता है। समर्थित layers editable screens बनती हैं; कुछ features अलग हो सकते हैं। अधिकतम {{max}} MB।",
|
|
12773
12802
|
chooseFigFile: ".fig file चुनें",
|
|
12774
12803
|
figUploadUploading: "अपलोड हो रहा है {{progress}}%",
|
|
12775
12804
|
figUploadProcessing: "बदला जा रहा है…",
|
|
@@ -12803,7 +12832,8 @@ const designImportOverrides = {
|
|
|
12803
12832
|
figmaPasteFailed: "Figma paste आयात विफल रहा",
|
|
12804
12833
|
uploadFailed: "File upload विफल रहा",
|
|
12805
12834
|
invalidFigFile: ".fig पर समाप्त होने वाली file चुनें।",
|
|
12806
|
-
figFileTooLarge:
|
|
12835
|
+
figFileTooLarge:
|
|
12836
|
+
"यह .fig बहुत बड़ी है — uploads की सीमा {{max}} MB है। Figma में जिस frame को import करना है उसे एक नई file में copy करें और उस file को .fig के रूप में export करें, या Paste from Figma का उपयोग करें।",
|
|
12807
12837
|
},
|
|
12808
12838
|
},
|
|
12809
12839
|
},
|
|
@@ -12836,7 +12866,7 @@ const designImportOverrides = {
|
|
|
12836
12866
|
"تم ملء {{count}} صورة{{plural}} من Figma.",
|
|
12837
12867
|
figUploadTitle: "رفع .fig",
|
|
12838
12868
|
figUploadDescription:
|
|
12839
|
-
"ميزة تجريبية: تنسيق .fig في Figma مملوك وقد يتغير. تتحول الطبقات المدعومة إلى شاشات قابلة للتحرير، وقد تختلف بعض الميزات. الحد الأقصى
|
|
12869
|
+
"ميزة تجريبية: تنسيق .fig في Figma مملوك وقد يتغير. تتحول الطبقات المدعومة إلى شاشات قابلة للتحرير، وقد تختلف بعض الميزات. الحد الأقصى {{max}} ميغابايت.",
|
|
12840
12870
|
chooseFigFile: "اختر ملف .fig",
|
|
12841
12871
|
figUploadUploading: "جار الرفع {{progress}}%",
|
|
12842
12872
|
figUploadProcessing: "جار التحويل…",
|
|
@@ -12869,7 +12899,8 @@ const designImportOverrides = {
|
|
|
12869
12899
|
figmaPasteFailed: "فشل استيراد لصق Figma",
|
|
12870
12900
|
uploadFailed: "فشل رفع الملف",
|
|
12871
12901
|
invalidFigFile: "اختر ملفا ينتهي بـ .fig.",
|
|
12872
|
-
figFileTooLarge:
|
|
12902
|
+
figFileTooLarge:
|
|
12903
|
+
"ملف .fig كبير جدًا — الحد الأقصى للرفع {{max}} ميغابايت. في Figma، انسخ الإطار المطلوب فقط إلى ملف جديد وصدّر ذلك الملف بصيغة .fig، أو استخدم اللصق من Figma.",
|
|
12873
12904
|
},
|
|
12874
12905
|
},
|
|
12875
12906
|
},
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { parseUploadResponse, type ImportResult } from "@/lib/design-import";
|
|
2
|
-
|
|
3
|
-
export const MAX_FIG_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
2
|
+
import { MAX_UPLOAD_BYTES } from "@/lib/upload-limits";
|
|
4
3
|
|
|
5
4
|
export type FigUploadValidationError = "invalid-extension" | "too-large";
|
|
6
5
|
|
|
@@ -8,7 +7,7 @@ export function validateFigUploadFile(
|
|
|
8
7
|
file: Pick<File, "name" | "size">,
|
|
9
8
|
): FigUploadValidationError | null {
|
|
10
9
|
if (!file.name.toLowerCase().endsWith(".fig")) return "invalid-extension";
|
|
11
|
-
if (file.size >
|
|
10
|
+
if (file.size > MAX_UPLOAD_BYTES) return "too-large";
|
|
12
11
|
return null;
|
|
13
12
|
}
|
|
14
13
|
|
|
@@ -10,15 +10,17 @@ import {
|
|
|
10
10
|
setResponseStatus,
|
|
11
11
|
} from "h3";
|
|
12
12
|
|
|
13
|
-
import { MAX_FIG_FILE_BYTES } from "../lib/fig-file-limits.js";
|
|
14
13
|
import {
|
|
15
14
|
normalizeImportedHtmlDocument,
|
|
16
15
|
saveImportedDesignFiles,
|
|
17
16
|
} from "../lib/import-design-files.js";
|
|
17
|
+
import {
|
|
18
|
+
MAX_UPLOAD_BYTES,
|
|
19
|
+
MAX_UPLOAD_MB,
|
|
20
|
+
TOTAL_BODY_LIMIT,
|
|
21
|
+
} from "../lib/request-body-limits.js";
|
|
18
22
|
|
|
19
23
|
const MAX_HTML_BYTES = 2 * 1024 * 1024;
|
|
20
|
-
const MULTIPART_OVERHEAD_BYTES = 1024 * 1024;
|
|
21
|
-
const TOTAL_BODY_LIMIT = MAX_FIG_FILE_BYTES + MULTIPART_OVERHEAD_BYTES;
|
|
22
24
|
|
|
23
25
|
// Matches the local-kiwi clipboard frame cap so a token-free .fig hydration of
|
|
24
26
|
// a multi-frame paste can fill every imported screen in one upload.
|
|
@@ -130,8 +132,10 @@ export const importDesignFile = defineEventHandler(async (event) => {
|
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
if (ext === ".fig") {
|
|
133
|
-
if (data.length >
|
|
134
|
-
throw new Error(
|
|
135
|
+
if (data.length > MAX_UPLOAD_BYTES) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`.fig file is too large (max ${MAX_UPLOAD_MB} MB). In Figma, copy just the frame you want into a new file and export that as .fig.`,
|
|
138
|
+
);
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
// Token-free hydration: fill the image placeholders left by a no-token
|
|
@@ -12,6 +12,12 @@ import {
|
|
|
12
12
|
} from "h3";
|
|
13
13
|
import { nanoid } from "nanoid";
|
|
14
14
|
|
|
15
|
+
import {
|
|
16
|
+
MAX_UPLOAD_BYTES,
|
|
17
|
+
MAX_UPLOAD_MB,
|
|
18
|
+
TOTAL_BODY_LIMIT,
|
|
19
|
+
} from "../lib/request-body-limits.js";
|
|
20
|
+
|
|
15
21
|
function isServerlessRuntime(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
16
22
|
return Boolean(
|
|
17
23
|
env.NETLIFY ||
|
|
@@ -176,12 +182,8 @@ export const uploadFiles = defineEventHandler(async (event) => {
|
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
const MAX_FILES = 20;
|
|
179
|
-
|
|
180
|
-
//
|
|
181
|
-
// over the theoretical max (MAX_FILES × MAX_FILE_SIZE each, plus overhead).
|
|
182
|
-
// Content-Length can be spoofed but cheaply eliminates accidental oversized
|
|
183
|
-
// uploads without allocating any memory for the body first.
|
|
184
|
-
const TOTAL_BODY_LIMIT = MAX_FILES * MAX_FILE_SIZE;
|
|
185
|
+
// Content-Length can be spoofed, so the parsed-byte check below is the real
|
|
186
|
+
// limit; this only avoids buffering an obviously oversized body first.
|
|
185
187
|
const contentLength = Number(
|
|
186
188
|
getRequestHeader(event, "content-length") ?? "0",
|
|
187
189
|
);
|
|
@@ -203,10 +205,12 @@ export const uploadFiles = defineEventHandler(async (event) => {
|
|
|
203
205
|
return { error: `Too many files (max ${MAX_FILES})` };
|
|
204
206
|
}
|
|
205
207
|
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
208
|
+
const totalBytes = fileParts.reduce((sum, p) => sum + p.data.length, 0);
|
|
209
|
+
if (totalBytes > MAX_UPLOAD_BYTES) {
|
|
208
210
|
setResponseStatus(event, 413);
|
|
209
|
-
return {
|
|
211
|
+
return {
|
|
212
|
+
error: `Attachments are too large (max ${MAX_UPLOAD_MB} MB total)`,
|
|
213
|
+
};
|
|
210
214
|
}
|
|
211
215
|
|
|
212
216
|
const results = await Promise.allSettled<InternalUploadedFileResult>(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Decoder caps for the experimental untrusted `.fig` path. The wire cap that
|
|
2
|
+
* bounds what can actually be uploaded lives in `request-body-limits.ts`. */
|
|
2
3
|
export const MAX_FIG_FILE_BYTES = 50 * 1024 * 1024;
|
|
3
4
|
export const MAX_FIG_DECOMPRESSED_BYTES = 96 * 1024 * 1024;
|
|
@@ -294,7 +294,7 @@ function mimeTypeForExt(ext: string): string {
|
|
|
294
294
|
/**
|
|
295
295
|
* Decode a `.fig` and index its embedded images by SHA-1 hash. Decode once per
|
|
296
296
|
* upload and reuse the index across screens — a multi-screen hydration must not
|
|
297
|
-
* re-parse the whole
|
|
297
|
+
* re-parse the whole file per screen.
|
|
298
298
|
*/
|
|
299
299
|
export function indexFigImages(figBytes: Buffer): Map<string, DecodedFigImage> {
|
|
300
300
|
const byHash = new Map<string, DecodedFigImage>();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Netlify base64-encodes the body into a 6 MB function payload, so uploads need
|
|
3
|
+
* ~33% headroom; above this the platform 413s with an empty body before any
|
|
4
|
+
* handler runs, and raising the number here cannot help.
|
|
5
|
+
*/
|
|
6
|
+
export const MAX_UPLOAD_BYTES = 4 * 1024 * 1024;
|
|
7
|
+
export const MAX_UPLOAD_MB = MAX_UPLOAD_BYTES / 1024 / 1024;
|
|
8
|
+
|
|
9
|
+
/** Multipart boundaries and field headers carried on top of the file bytes. */
|
|
10
|
+
export const MULTIPART_OVERHEAD_BYTES = 64 * 1024;
|
|
11
|
+
export const TOTAL_BODY_LIMIT = MAX_UPLOAD_BYTES + MULTIPART_OVERHEAD_BYTES;
|
|
@@ -58,6 +58,87 @@ export function parseCanvasFrameGeometryById(
|
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** Design-data maps whose per-entry dimension keys must be persisted as JSON
|
|
62
|
+
* numbers. Every reader treats a non-number as absent, so accepting `"800"`
|
|
63
|
+
* would silently drop the write instead of resizing anything. */
|
|
64
|
+
const NUMERIC_DESIGN_DATA_ENTRY_KEYS: Record<string, ReadonlySet<string>> = {
|
|
65
|
+
canvasFrames: new Set(CANVAS_FRAME_GEOMETRY_KEYS),
|
|
66
|
+
screenMetadata: new Set(["width", "height"]),
|
|
67
|
+
localhostScreens: new Set(["width", "height"]),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function describeRejectedValue(value: unknown): string {
|
|
71
|
+
if (value === null) return "null";
|
|
72
|
+
if (typeof value === "string") return `the string ${JSON.stringify(value)}`;
|
|
73
|
+
if (typeof value === "number") return `the non-finite number ${value}`;
|
|
74
|
+
if (Array.isArray(value)) return "an array";
|
|
75
|
+
return `a ${typeof value}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function numericValueError(
|
|
79
|
+
map: string,
|
|
80
|
+
key: string,
|
|
81
|
+
value: unknown,
|
|
82
|
+
): string | null {
|
|
83
|
+
if (typeof value === "number" && Number.isFinite(value)) return null;
|
|
84
|
+
return (
|
|
85
|
+
`Design ${map} "${key}" must be a finite JSON number, received ${describeRejectedValue(value)}. ` +
|
|
86
|
+
`Write dimensions and positions as numbers (800), not strings ("800" or "800px"); ` +
|
|
87
|
+
`use a delete operation to clear one.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function numericEntryError(
|
|
92
|
+
map: string,
|
|
93
|
+
entry: unknown,
|
|
94
|
+
numericKeys: ReadonlySet<string>,
|
|
95
|
+
): string | null {
|
|
96
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
97
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
98
|
+
if (!numericKeys.has(key)) continue;
|
|
99
|
+
const error = numericValueError(map, key, value);
|
|
100
|
+
if (error) return error;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Message describing why a path-addressed design-data write carries a
|
|
107
|
+
* non-numeric dimension, or null when the write is acceptable.
|
|
108
|
+
*
|
|
109
|
+
* Callers must reject on a message rather than coercing: the readers below
|
|
110
|
+
* drop non-numbers, so a coerced write and an ignored one are indistinguishable
|
|
111
|
+
* to whoever asked for the resize.
|
|
112
|
+
*/
|
|
113
|
+
export function numericDesignDataWriteError(
|
|
114
|
+
path: readonly string[],
|
|
115
|
+
value: unknown,
|
|
116
|
+
): string | null {
|
|
117
|
+
const map = path[0];
|
|
118
|
+
const numericKeys = map ? NUMERIC_DESIGN_DATA_ENTRY_KEYS[map] : undefined;
|
|
119
|
+
if (!map || !numericKeys) return null;
|
|
120
|
+
|
|
121
|
+
if (path.length === 1) {
|
|
122
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
for (const entry of Object.values(value)) {
|
|
126
|
+
const error = numericEntryError(map, entry, numericKeys);
|
|
127
|
+
if (error) return error;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (path.length === 2) return numericEntryError(map, value, numericKeys);
|
|
133
|
+
|
|
134
|
+
const key = path[2]!;
|
|
135
|
+
if (!numericKeys.has(key)) return null;
|
|
136
|
+
if (path.length > 3) {
|
|
137
|
+
return `Design ${map} "${key}" is a single number and has no nested values.`;
|
|
138
|
+
}
|
|
139
|
+
return numericValueError(map, key, value);
|
|
140
|
+
}
|
|
141
|
+
|
|
61
142
|
/** Y a new group must start at to clear existing frames, or 0 when the board is
|
|
62
143
|
* empty. Placing at y=0 unconditionally stacks each new group on the last. */
|
|
63
144
|
export function nextFreeCanvasRowY(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* Client-side session gate for an authenticated app shell.
|
|
4
4
|
*
|
|
@@ -70,12 +70,12 @@ export function RequireSession({ children, fallback, redirect = true, signedOut,
|
|
|
70
70
|
return (_jsx(ResolvedSessionGate, { fallback: fallback, redirect: redirect, signedOut: signedOut, children: children }));
|
|
71
71
|
}
|
|
72
72
|
function ResolvedSessionGate({ children, fallback, redirect = true, signedOut, }) {
|
|
73
|
-
const { session,
|
|
73
|
+
const { session, status, retry } = useSession();
|
|
74
74
|
// Guard against firing the redirect more than once (effect re-runs, React
|
|
75
75
|
// StrictMode double-invoke) — a second navigation while the first is in
|
|
76
76
|
// flight is harmless but noisy.
|
|
77
77
|
const redirectedRef = useRef(false);
|
|
78
|
-
const mustRedirect =
|
|
78
|
+
const mustRedirect = status === "unauthenticated" && redirect;
|
|
79
79
|
useEffect(() => {
|
|
80
80
|
if (!mustRedirect)
|
|
81
81
|
return;
|
|
@@ -97,8 +97,14 @@ function ResolvedSessionGate({ children, fallback, redirect = true, signedOut, }
|
|
|
97
97
|
}, [mustRedirect]);
|
|
98
98
|
// Still resolving, or redirect already in flight: show the loading fallback
|
|
99
99
|
// rather than flashing app chrome the visitor can't use.
|
|
100
|
-
if (
|
|
100
|
+
if (status === "loading")
|
|
101
101
|
return _jsx(_Fragment, { children: fallback ?? _jsx(DefaultSpinner, {}) });
|
|
102
|
+
// Unreadable is not signed-out. Redirecting here would bounce a signed-in
|
|
103
|
+
// user to the sign-in page over a transient 5xx, and rendering the spinner
|
|
104
|
+
// would strand them on a screen that never resolves.
|
|
105
|
+
if (status === "unavailable") {
|
|
106
|
+
return _jsx(SessionUnavailableNotice, { retry: retry });
|
|
107
|
+
}
|
|
102
108
|
if (!session) {
|
|
103
109
|
if (redirect)
|
|
104
110
|
return _jsx(_Fragment, { children: fallback ?? _jsx(DefaultSpinner, {}) });
|
|
@@ -106,4 +112,7 @@ function ResolvedSessionGate({ children, fallback, redirect = true, signedOut, }
|
|
|
106
112
|
}
|
|
107
113
|
return _jsx(_Fragment, { children: children });
|
|
108
114
|
}
|
|
115
|
+
function SessionUnavailableNotice({ retry }) {
|
|
116
|
+
return (_jsxs("div", { className: "flex h-screen w-full flex-col items-center justify-center gap-4 px-6 text-center", children: [_jsx("p", { className: "max-w-md text-sm text-muted-foreground", children: "We couldn't reach the server to confirm you're signed in. This is usually temporary." }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { type: "button", onClick: retry, className: "rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground", children: "Try again" }), _jsx("button", { type: "button", onClick: () => window.location.reload(), className: "rounded-md border border-border px-3 py-1.5 text-sm font-medium", children: "Reload page" })] })] }));
|
|
117
|
+
}
|
|
109
118
|
//# sourceMappingURL=require-session.js.map
|