@dmitryvim/form-builder 0.2.27 → 0.2.29

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/dist/esm/index.js CHANGED
@@ -2104,30 +2104,58 @@ function isFileExtensionAllowed(fileName, allowedExtensions) {
2104
2104
  const ext = fileName.split(".").pop()?.toLowerCase() ?? "";
2105
2105
  return allowedExtensions.includes(ext);
2106
2106
  }
2107
- function isFileSizeAllowed(file, maxSizeMB) {
2107
+ function isSizeWithinLimit(bytes, maxSizeMB) {
2108
2108
  if (maxSizeMB === Infinity) return true;
2109
- return file.size <= maxSizeMB * 1024 * 1024;
2109
+ return bytes <= maxSizeMB * 1024 * 1024;
2110
+ }
2111
+ function isFileSizeAllowed(file, maxSizeMB) {
2112
+ return isSizeWithinLimit(file.size, maxSizeMB);
2113
+ }
2114
+ function getAllowedMimes(accept) {
2115
+ if (!accept) return [];
2116
+ if (typeof accept === "string") return [];
2117
+ if (!Array.isArray(accept.mime)) return [];
2118
+ return accept.mime.map((m) => m.toLowerCase());
2119
+ }
2120
+ function isMimeAllowed(mimeType, allowedMimes) {
2121
+ if (allowedMimes.length === 0) return true;
2122
+ const normalizedType = mimeType.toLowerCase();
2123
+ return allowedMimes.some((allowed) => {
2124
+ if (allowed.endsWith("/*")) {
2125
+ const prefix = allowed.slice(0, -1);
2126
+ return normalizedType.startsWith(prefix);
2127
+ }
2128
+ return normalizedType === allowed;
2129
+ });
2130
+ }
2131
+ function inferMimeFromResourceId(resourceId) {
2132
+ const filename = resourceId.split("/").pop() || "file";
2133
+ const extension = filename.split(".").pop()?.toLowerCase();
2134
+ if (extension) {
2135
+ if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
2136
+ return `image/${extension === "jpg" ? "jpeg" : extension}`;
2137
+ }
2138
+ if (["mp4", "webm", "mov", "avi"].includes(extension)) {
2139
+ return `video/${extension === "mov" ? "quicktime" : extension}`;
2140
+ }
2141
+ }
2142
+ return "application/octet-stream";
2143
+ }
2144
+ function seedInferredResource(resourceId, resourceIndex) {
2145
+ if (resourceIndex.has(resourceId)) return;
2146
+ const filename = resourceId.split("/").pop() || "file";
2147
+ resourceIndex.set(resourceId, {
2148
+ name: filename,
2149
+ type: inferMimeFromResourceId(resourceId),
2150
+ size: 0,
2151
+ uploadedAt: /* @__PURE__ */ new Date(),
2152
+ file: void 0,
2153
+ inferredFromExtension: true
2154
+ });
2110
2155
  }
2111
2156
  function addPrefillFilesToIndex(initialFiles, resourceIndex) {
2112
2157
  for (const resourceId of initialFiles) {
2113
- if (resourceIndex.has(resourceId)) continue;
2114
- const filename = resourceId.split("/").pop() || "file";
2115
- const extension = filename.split(".").pop()?.toLowerCase();
2116
- let fileType = "application/octet-stream";
2117
- if (extension) {
2118
- if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
2119
- fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
2120
- } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
2121
- fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
2122
- }
2123
- }
2124
- resourceIndex.set(resourceId, {
2125
- name: filename,
2126
- type: fileType,
2127
- size: 0,
2128
- uploadedAt: /* @__PURE__ */ new Date(),
2129
- file: void 0
2130
- });
2158
+ seedInferredResource(resourceId, resourceIndex);
2131
2159
  }
2132
2160
  }
2133
2161
 
@@ -2404,6 +2432,84 @@ function ensureFileStyles() {
2404
2432
  z-index: 10000;
2405
2433
  }
2406
2434
 
2435
+ /* Two-card empty-state layout (upload card + library card) */
2436
+ .fb-file-card-row {
2437
+ display: flex;
2438
+ gap: 8px;
2439
+ align-items: stretch;
2440
+ }
2441
+ .fb-file-card-row .fb-file-dropzone,
2442
+ .fb-file-card-row .fb-file-library-card {
2443
+ flex: 1;
2444
+ min-width: 0;
2445
+ }
2446
+
2447
+ /* Library picker card \u2014 mirrors .fb-file-dropzone styling */
2448
+ .fb-file-library-card {
2449
+ height: 128px;
2450
+ border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2451
+ border-radius: var(--fb-border-radius, 0.5rem);
2452
+ display: flex;
2453
+ flex-direction: column;
2454
+ align-items: center;
2455
+ justify-content: center;
2456
+ gap: 4px;
2457
+ cursor: pointer;
2458
+ background: none;
2459
+ padding: 0;
2460
+ transition:
2461
+ border-color var(--fb-transition-duration, 200ms),
2462
+ background var(--fb-transition-duration, 200ms);
2463
+ width: 100%;
2464
+ }
2465
+ .fb-file-library-card:hover,
2466
+ .fb-file-library-card:focus-visible {
2467
+ border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2468
+ background: var(--fb-background-hover-color, #f9fafb);
2469
+ outline: none;
2470
+ }
2471
+ .fb-file-library-card-icon {
2472
+ font-size: 24px;
2473
+ line-height: 1;
2474
+ flex-shrink: 0;
2475
+ }
2476
+ .fb-file-library-card-label {
2477
+ font-size: 13px;
2478
+ color: var(--fb-text-secondary-color, #6b7280);
2479
+ }
2480
+ .fb-file-library-card-hint {
2481
+ font-size: 11px;
2482
+ color: var(--fb-file-upload-text-color, #9ca3af);
2483
+ }
2484
+
2485
+ /* Library "\u{1F4DA}" add-tile \u2014 same size/style as the "+" add tile */
2486
+ .fb-tile-add-library {
2487
+ border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2488
+ display: flex;
2489
+ align-items: center;
2490
+ justify-content: center;
2491
+ cursor: pointer;
2492
+ font-size: 24px;
2493
+ color: var(--fb-file-upload-text-color, #9ca3af);
2494
+ transition:
2495
+ border-color var(--fb-transition-duration, 200ms),
2496
+ color var(--fb-transition-duration, 200ms);
2497
+ background: none;
2498
+ padding: 0;
2499
+ width: var(--fb-tile-size, 160px);
2500
+ height: var(--fb-tile-size, 160px);
2501
+ flex-shrink: 0;
2502
+ position: relative;
2503
+ overflow: hidden;
2504
+ border-radius: var(--fb-border-radius, 0.5rem);
2505
+ }
2506
+ .fb-tile-add-library:hover,
2507
+ .fb-tile-add-library:focus-visible {
2508
+ border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2509
+ color: var(--fb-text-color, #1f2937);
2510
+ outline: none;
2511
+ }
2512
+
2407
2513
  /* Hover zoom preview popup for image tiles \u2014 appended to document.body (fixed) */
2408
2514
  .fb-tile-zoom-preview {
2409
2515
  position: fixed;
@@ -2522,22 +2628,34 @@ function setEmptyFileContainer(fileContainer, state, hint) {
2522
2628
  </div>
2523
2629
  `;
2524
2630
  }
2631
+ var dragDropHandlers = /* @__PURE__ */ new WeakMap();
2525
2632
  function setupDragAndDrop(element, dropHandler) {
2526
- element.addEventListener("dragover", (e) => {
2633
+ const prev = dragDropHandlers.get(element);
2634
+ if (prev) {
2635
+ element.removeEventListener("dragover", prev.dragover);
2636
+ element.removeEventListener("dragleave", prev.dragleave);
2637
+ element.removeEventListener("drop", prev.drop);
2638
+ }
2639
+ const dragover = (e) => {
2527
2640
  e.preventDefault();
2528
2641
  element.classList.add("border-blue-500", "bg-blue-50");
2529
- });
2530
- element.addEventListener("dragleave", (e) => {
2642
+ };
2643
+ const dragleave = (e) => {
2531
2644
  e.preventDefault();
2532
2645
  element.classList.remove("border-blue-500", "bg-blue-50");
2533
- });
2534
- element.addEventListener("drop", (e) => {
2646
+ };
2647
+ const drop = (e) => {
2535
2648
  e.preventDefault();
2536
2649
  element.classList.remove("border-blue-500", "bg-blue-50");
2537
- if (e.dataTransfer?.files) {
2538
- dropHandler(e.dataTransfer.files);
2650
+ const files = e.dataTransfer?.files;
2651
+ if (files) {
2652
+ dropHandler(files);
2539
2653
  }
2540
- });
2654
+ };
2655
+ element.addEventListener("dragover", dragover);
2656
+ element.addEventListener("dragleave", dragleave);
2657
+ element.addEventListener("drop", drop);
2658
+ dragDropHandlers.set(element, { dragover, dragleave, drop });
2541
2659
  }
2542
2660
 
2543
2661
  // src/components/file/preview.ts
@@ -3222,7 +3340,18 @@ async function uploadSingleFile(file, state) {
3222
3340
  throw new Error(`File upload failed: ${err.message}`);
3223
3341
  }
3224
3342
  }
3225
- async function handleFileSelect(file, container, fieldName, state, deps = null, instance, allowedExtensions = [], maxSizeMB = Infinity) {
3343
+ async function handleFileSelect(opts) {
3344
+ const {
3345
+ file,
3346
+ container,
3347
+ fieldName,
3348
+ state,
3349
+ deps = null,
3350
+ instance = null,
3351
+ allowedExtensions = [],
3352
+ allowedMimes = [],
3353
+ maxSizeMB = Infinity
3354
+ } = opts;
3226
3355
  if (!isFileExtensionAllowed(file.name, allowedExtensions)) {
3227
3356
  const formats = allowedExtensions.join(", ");
3228
3357
  showFileError(
@@ -3231,6 +3360,14 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
3231
3360
  );
3232
3361
  return;
3233
3362
  }
3363
+ if (!isMimeAllowed(file.type, allowedMimes)) {
3364
+ const mimes = allowedMimes.join(", ");
3365
+ showFileError(
3366
+ container,
3367
+ t("invalidFileMime", state, { name: file.name, type: file.type, mimes })
3368
+ );
3369
+ return;
3370
+ }
3234
3371
  if (!isFileSizeAllowed(file, maxSizeMB)) {
3235
3372
  showFileError(
3236
3373
  container,
@@ -3290,10 +3427,16 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
3290
3427
  const afterExt = allFiles.filter(
3291
3428
  (f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
3292
3429
  );
3293
- const rejectedBySize = afterExt.filter(
3430
+ const rejectedByMime = afterExt.filter(
3431
+ (f) => !isMimeAllowed(f.type, constraints.allowedMimes)
3432
+ );
3433
+ const afterMime = afterExt.filter(
3434
+ (f) => isMimeAllowed(f.type, constraints.allowedMimes)
3435
+ );
3436
+ const rejectedBySize = afterMime.filter(
3294
3437
  (f) => !isFileSizeAllowed(f, constraints.maxSize)
3295
3438
  );
3296
- const valid = afterExt.filter((f) => isFileSizeAllowed(f, constraints.maxSize));
3439
+ const valid = afterMime.filter((f) => isFileSizeAllowed(f, constraints.maxSize));
3297
3440
  const remaining = constraints.maxCount === Infinity ? valid.length : Math.max(0, constraints.maxCount - currentCount);
3298
3441
  const accepted = valid.slice(0, remaining);
3299
3442
  const skippedByCount = valid.length - accepted.length;
@@ -3303,6 +3446,11 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
3303
3446
  const names = rejectedByExt.map((f) => f.name).join(", ");
3304
3447
  errorParts.push(t("invalidFileExtension", state, { name: names, formats }));
3305
3448
  }
3449
+ if (rejectedByMime.length > 0) {
3450
+ const mimes = constraints.allowedMimes.join(", ");
3451
+ const names = rejectedByMime.map((f) => f.name).join(", ");
3452
+ errorParts.push(t("invalidFileMime", state, { name: names, type: rejectedByMime.map((f) => f.type).join(", "), mimes }));
3453
+ }
3306
3454
  if (rejectedBySize.length > 0) {
3307
3455
  const names = rejectedBySize.map((f) => f.name).join(", ");
3308
3456
  errorParts.push(
@@ -3394,27 +3542,159 @@ function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback
3394
3542
  };
3395
3543
  }
3396
3544
 
3397
- // src/components/file/render-edit.ts
3398
- function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
3399
- if (!state.resourceIndex.has(initial)) {
3400
- const filename = initial.split("/").pop() || "file";
3401
- const extension = filename.split(".").pop()?.toLowerCase();
3402
- let fileType = "application/octet-stream";
3403
- if (extension) {
3404
- if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
3405
- fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
3406
- } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
3407
- fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
3408
- }
3409
- }
3410
- state.resourceIndex.set(initial, {
3411
- name: filename,
3412
- type: fileType,
3413
- size: 0,
3414
- uploadedAt: /* @__PURE__ */ new Date(),
3415
- file: void 0
3545
+ // src/components/file/library.ts
3546
+ function buildAcceptContext(element) {
3547
+ if (!element.accept) return void 0;
3548
+ if (typeof element.accept === "string") {
3549
+ const exts2 = getAllowedExtensions(element.accept);
3550
+ return exts2.length > 0 ? { extensions: exts2 } : void 0;
3551
+ }
3552
+ const exts = element.accept.extensions ?? [];
3553
+ const mime = element.accept.mime ?? [];
3554
+ const normalizedExts = exts.map((e) => e.toLowerCase());
3555
+ if (normalizedExts.length === 0 && mime.length === 0) return void 0;
3556
+ const result = {};
3557
+ if (normalizedExts.length > 0) result.extensions = normalizedExts;
3558
+ if (mime.length > 0) result.mime = mime;
3559
+ return result;
3560
+ }
3561
+ function validatePickedResource(resource, allowedExtensions, allowedMimes, maxSizeMB, state) {
3562
+ if (!isFileExtensionAllowed(resource.name, allowedExtensions)) {
3563
+ const formats = allowedExtensions.join(", ");
3564
+ return t("invalidFileExtension", state, { name: resource.name, formats });
3565
+ }
3566
+ if (!isMimeAllowed(resource.type, allowedMimes)) {
3567
+ const mimes = allowedMimes.join(", ");
3568
+ return t("invalidFileMime", state, { name: resource.name, type: resource.type, mimes });
3569
+ }
3570
+ if (!isSizeWithinLimit(resource.size, maxSizeMB)) {
3571
+ return t("fileTooLarge", state, { name: resource.name, maxSize: maxSizeMB });
3572
+ }
3573
+ return null;
3574
+ }
3575
+ function readCurrentResourceIds(wrapper) {
3576
+ const raw = wrapper.dataset.resourceIds;
3577
+ if (!raw) return [];
3578
+ try {
3579
+ const parsed = JSON.parse(raw);
3580
+ return Array.isArray(parsed) ? parsed : [];
3581
+ } catch {
3582
+ return [];
3583
+ }
3584
+ }
3585
+ function registerPickedResource(resource, state) {
3586
+ const existing = state.resourceIndex.get(resource.resourceId);
3587
+ state.resourceIndex.set(resource.resourceId, {
3588
+ name: resource.name,
3589
+ type: resource.type,
3590
+ size: resource.size,
3591
+ uploadedAt: existing?.uploadedAt ?? /* @__PURE__ */ new Date(),
3592
+ file: existing?.file
3593
+ });
3594
+ }
3595
+ function extractPickerError(error, state) {
3596
+ if (error instanceof Error && error.message) return error.message;
3597
+ return t("pickerError", state);
3598
+ }
3599
+ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resourceIds, maxCount, updateCallback, instance) {
3600
+ if (!state.config.pickExistingFiles) return;
3601
+ const allowedExtensions = getAllowedExtensions(element.accept);
3602
+ const allowedMimes = getAllowedMimes(element.accept);
3603
+ const maxSizeMB = element.maxSize ?? Infinity;
3604
+ const currentIds = readCurrentResourceIds(wrapper);
3605
+ const remaining = maxCount === Infinity ? Infinity : Math.max(0, maxCount - currentIds.length);
3606
+ let picked;
3607
+ try {
3608
+ picked = await state.config.pickExistingFiles({
3609
+ fieldPath,
3610
+ mode: "multiple",
3611
+ accept: buildAcceptContext(element),
3612
+ maxSizeMB: maxSizeMB === Infinity ? void 0 : maxSizeMB,
3613
+ remainingSlots: remaining === Infinity ? void 0 : remaining,
3614
+ selectedResourceIds: [...currentIds]
3416
3615
  });
3616
+ } catch (error) {
3617
+ showFileError(wrapper, extractPickerError(error, state));
3618
+ return;
3619
+ }
3620
+ if (picked.length === 0) return;
3621
+ const existingSet = new Set(currentIds);
3622
+ const seen = /* @__PURE__ */ new Set();
3623
+ const deduped = picked.filter((r) => {
3624
+ if (existingSet.has(r.resourceId)) return false;
3625
+ if (seen.has(r.resourceId)) return false;
3626
+ seen.add(r.resourceId);
3627
+ return true;
3628
+ });
3629
+ const validItems = deduped.filter((r) => {
3630
+ const err = validatePickedResource(r, allowedExtensions, allowedMimes, maxSizeMB, state);
3631
+ return err === null;
3632
+ });
3633
+ const freshRemaining = maxCount === Infinity ? validItems.length : Math.max(0, maxCount - resourceIds.length);
3634
+ const accepted = validItems.slice(0, freshRemaining);
3635
+ const skipped = validItems.length - accepted.length;
3636
+ if (accepted.length === 0) return;
3637
+ clearFileError(wrapper);
3638
+ if (skipped > 0) {
3639
+ showFileError(
3640
+ wrapper,
3641
+ t("filesLimitExceeded", state, { skipped, max: maxCount })
3642
+ );
3643
+ }
3644
+ for (const resource of accepted) {
3645
+ registerPickedResource(resource, state);
3646
+ resourceIds.push(resource.resourceId);
3647
+ }
3648
+ wrapper.dataset.resourceIds = JSON.stringify(resourceIds);
3649
+ updateCallback();
3650
+ if (!state.config.readonly) {
3651
+ instance.triggerOnChange(fieldPath, resourceIds);
3652
+ }
3653
+ }
3654
+ async function handleLibraryPickSingle(state, element, container, fileWrapper, pathKey, fieldPath, renderCallback, instance) {
3655
+ if (!state.config.pickExistingFiles) return;
3656
+ const allowedExtensions = getAllowedExtensions(element.accept);
3657
+ const allowedMimes = getAllowedMimes(element.accept);
3658
+ const maxSizeMB = element.maxSize ?? Infinity;
3659
+ let picked;
3660
+ try {
3661
+ picked = await state.config.pickExistingFiles({
3662
+ fieldPath,
3663
+ mode: "single",
3664
+ accept: buildAcceptContext(element),
3665
+ maxSizeMB: maxSizeMB === Infinity ? void 0 : maxSizeMB,
3666
+ selectedResourceIds: []
3667
+ });
3668
+ } catch (error) {
3669
+ showFileError(container, extractPickerError(error, state));
3670
+ return;
3671
+ }
3672
+ if (picked.length === 0) return;
3673
+ const first = picked[0];
3674
+ const validationError = validatePickedResource(first, allowedExtensions, allowedMimes, maxSizeMB, state);
3675
+ if (validationError !== null) {
3676
+ showFileError(container, validationError);
3677
+ return;
3678
+ }
3679
+ clearFileError(container);
3680
+ registerPickedResource(first, state);
3681
+ let hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
3682
+ if (!hiddenInput) {
3683
+ hiddenInput = document.createElement("input");
3684
+ hiddenInput.type = "hidden";
3685
+ hiddenInput.name = pathKey;
3686
+ fileWrapper.appendChild(hiddenInput);
3687
+ }
3688
+ hiddenInput.value = first.resourceId;
3689
+ await renderCallback(first.resourceId);
3690
+ if (!state.config.readonly) {
3691
+ instance.triggerOnChange(fieldPath, first.resourceId);
3417
3692
  }
3693
+ }
3694
+
3695
+ // src/components/file/render-edit.ts
3696
+ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
3697
+ seedInferredResource(initial, state.resourceIndex);
3418
3698
  const meta = state.resourceIndex.get(initial);
3419
3699
  const isVideo = meta?.type?.startsWith("video/");
3420
3700
  if (isVideo) {
@@ -3432,7 +3712,50 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
3432
3712
  hiddenInput.value = initial;
3433
3713
  fileWrapper.appendChild(hiddenInput);
3434
3714
  }
3435
- function renderResourcePills(container, rids, state, onRemove, hint, countInfo, maxCount, isReadonly = false) {
3715
+ var UPLOAD_SVG = `<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" style="flex-shrink:0;color:var(--fb-file-upload-text-color,#9ca3af);">
3716
+ <path d="M19.35 10.04A7.49 7.49 0 0012 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 000 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"/>
3717
+ </svg>`;
3718
+ function buildEmptyDropzone(state, primaryText, subHint, openPicker) {
3719
+ const dropzone = document.createElement("div");
3720
+ dropzone.className = "fb-file-dropzone";
3721
+ dropzone.innerHTML = `
3722
+ ${UPLOAD_SVG}
3723
+ <div class="fb-dropzone-primary-text">${escapeHtml(primaryText)}</div>
3724
+ ${subHint ? `<div class="fb-dropzone-hint-text">${escapeHtml(subHint)}</div>` : ""}
3725
+ `;
3726
+ dropzone.onclick = openPicker;
3727
+ return dropzone;
3728
+ }
3729
+ function buildLibraryButton(variant, state, onClick) {
3730
+ const btn = document.createElement("button");
3731
+ btn.type = "button";
3732
+ btn.className = variant === "card" ? "fb-file-library-card" : "fb-tile fb-tile-add-library";
3733
+ if (variant === "card") {
3734
+ btn.innerHTML = `
3735
+ <span class="fb-file-library-card-icon" aria-hidden="true">\u{1F4DA}</span>
3736
+ <span class="fb-file-library-card-label">${escapeHtml(t("fromLibrary", state))}</span>
3737
+ <span class="fb-file-library-card-hint">${escapeHtml(t("libraryHint", state))}</span>
3738
+ `;
3739
+ } else {
3740
+ btn.innerHTML = `<span aria-hidden="true">\u{1F4DA}</span>`;
3741
+ btn.title = t("fromLibrary", state);
3742
+ btn.setAttribute("aria-label", t("fromLibrary", state));
3743
+ }
3744
+ btn.addEventListener("click", onClick);
3745
+ return btn;
3746
+ }
3747
+ function renderResourcePills(opts) {
3748
+ const {
3749
+ container,
3750
+ rids,
3751
+ state,
3752
+ onRemove,
3753
+ hint,
3754
+ countInfo,
3755
+ maxCount,
3756
+ isReadonly = false,
3757
+ onLibraryPick
3758
+ } = opts;
3436
3759
  ensureFileStyles();
3437
3760
  const wrapper = container.closest("[data-files-wrapper]");
3438
3761
  if (wrapper) {
@@ -3441,6 +3764,7 @@ function renderResourcePills(container, rids, state, onRemove, hint, countInfo,
3441
3764
  while (container.firstChild) container.removeChild(container.firstChild);
3442
3765
  const ridList = rids ?? [];
3443
3766
  const atMax = maxCount !== void 0 && ridList.length >= maxCount;
3767
+ const hasLibrary = !isReadonly && typeof onLibraryPick === "function";
3444
3768
  const buildSubHint = () => {
3445
3769
  const parts = [];
3446
3770
  if (hint) parts.push(hint);
@@ -3457,18 +3781,26 @@ function renderResourcePills(container, rids, state, onRemove, hint, countInfo,
3457
3781
  emptyEl.className = "fb-tile-empty-text";
3458
3782
  emptyEl.textContent = t("noFilesSelected", state);
3459
3783
  container.appendChild(emptyEl);
3784
+ } else if (hasLibrary) {
3785
+ const row = document.createElement("div");
3786
+ row.className = "fb-file-card-row";
3787
+ const dropzone = buildEmptyDropzone(
3788
+ state,
3789
+ t("clickDragTextMultiple", state),
3790
+ buildSubHint(),
3791
+ openPicker
3792
+ );
3793
+ const libraryBtn = buildLibraryButton("card", state, onLibraryPick);
3794
+ row.appendChild(dropzone);
3795
+ row.appendChild(libraryBtn);
3796
+ container.appendChild(row);
3460
3797
  } else {
3461
- const dropzone = document.createElement("div");
3462
- dropzone.className = "fb-file-dropzone";
3463
- const subHint2 = buildSubHint();
3464
- dropzone.innerHTML = `
3465
- <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" style="flex-shrink:0;color:var(--fb-file-upload-text-color,#9ca3af);">
3466
- <path d="M19.35 10.04A7.49 7.49 0 0012 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 000 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"/>
3467
- </svg>
3468
- <div class="fb-dropzone-primary-text">${escapeHtml(t("clickDragTextMultiple", state))}</div>
3469
- ${subHint2 ? `<div class="fb-dropzone-hint-text">${escapeHtml(subHint2)}</div>` : ""}
3470
- `;
3471
- dropzone.onclick = openPicker;
3798
+ const dropzone = buildEmptyDropzone(
3799
+ state,
3800
+ t("clickDragTextMultiple", state),
3801
+ buildSubHint(),
3802
+ openPicker
3803
+ );
3472
3804
  container.appendChild(dropzone);
3473
3805
  }
3474
3806
  return;
@@ -3499,6 +3831,10 @@ function renderResourcePills(container, rids, state, onRemove, hint, countInfo,
3499
3831
  addTile.innerHTML = "+";
3500
3832
  addTile.onclick = openPicker;
3501
3833
  tilesWrap.appendChild(addTile);
3834
+ if (hasLibrary) {
3835
+ const libraryTile = buildLibraryButton("tile", state, onLibraryPick);
3836
+ tilesWrap.appendChild(libraryTile);
3837
+ }
3502
3838
  } else if (!isReadonly && atMax) {
3503
3839
  const chip = document.createElement("div");
3504
3840
  chip.className = "fb-tile-counter";
@@ -3526,12 +3862,16 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3526
3862
  picker.name = pathKey;
3527
3863
  picker.style.display = "none";
3528
3864
  if (element.accept) {
3529
- picker.accept = typeof element.accept === "string" ? element.accept : element.accept.extensions?.map((ext) => `.${ext}`).join(",") || "";
3865
+ picker.accept = typeof element.accept === "string" ? element.accept : [
3866
+ ...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
3867
+ ...element.accept.mime ?? []
3868
+ ].join(",") || "";
3530
3869
  }
3531
3870
  const fileContainer = document.createElement("div");
3532
3871
  fileContainer.className = "file-preview-container";
3533
3872
  const initial = ctx.prefill[element.key];
3534
3873
  const allowedExts = getAllowedExtensions(element.accept);
3874
+ const allowedMimes = getAllowedMimes(element.accept);
3535
3875
  const maxSizeMB = element.maxSize ?? Infinity;
3536
3876
  const handlers = {
3537
3877
  fileUploadHandler() {
@@ -3539,16 +3879,17 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3539
3879
  },
3540
3880
  dragHandler(files) {
3541
3881
  if (files.length > 0) {
3542
- handleFileSelect(
3543
- files[0],
3544
- fileContainer,
3545
- pathKey,
3882
+ handleFileSelect({
3883
+ file: files[0],
3884
+ container: fileContainer,
3885
+ fieldName: pathKey,
3546
3886
  state,
3547
- buildDeps(),
3548
- ctx.instance,
3549
- allowedExts,
3887
+ deps: buildDeps(),
3888
+ instance: ctx.instance,
3889
+ allowedExtensions: allowedExts,
3890
+ allowedMimes,
3550
3891
  maxSizeMB
3551
- );
3892
+ });
3552
3893
  }
3553
3894
  },
3554
3895
  setupDrop(container) {
@@ -3569,7 +3910,7 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3569
3910
  releaseLocalFileUrl(state.resourceIndex.get(currentRid)?.file);
3570
3911
  }
3571
3912
  if (hiddenInput) hiddenInput.value = "";
3572
- handlers.restoreDropzone();
3913
+ renderEmptySingleState();
3573
3914
  }
3574
3915
  };
3575
3916
  const buildDeps = () => ({
@@ -3579,6 +3920,50 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3579
3920
  setupDrop: handlers.setupDrop,
3580
3921
  onRemove: handlers.onRemove
3581
3922
  });
3923
+ const renderEmptySingleState = () => {
3924
+ if (state.config.pickExistingFiles && !element.disableLibrary) {
3925
+ fileContainer.className = "file-preview-container";
3926
+ fileContainer.removeAttribute("style");
3927
+ fileContainer.onclick = null;
3928
+ while (fileContainer.firstChild) {
3929
+ fileContainer.removeChild(fileContainer.firstChild);
3930
+ }
3931
+ const row = document.createElement("div");
3932
+ row.className = "fb-file-card-row";
3933
+ row.style.cssText = "display:flex;gap:8px;align-items:stretch;";
3934
+ const hint = makeFieldHint(element, state);
3935
+ const uploadCard = buildEmptyDropzone(
3936
+ state,
3937
+ t("clickDragText", state),
3938
+ hint,
3939
+ handlers.fileUploadHandler
3940
+ );
3941
+ uploadCard.style.cssText = "flex:1;min-width:0;height:128px;";
3942
+ setupDragAndDrop(uploadCard, handlers.dragHandler);
3943
+ const libraryBtn = buildLibraryButton("card", state, () => {
3944
+ handleLibraryPickSingle(
3945
+ state,
3946
+ element,
3947
+ fileContainer,
3948
+ fileWrapper,
3949
+ pathKey,
3950
+ pathKey,
3951
+ async (rid) => {
3952
+ await renderSingleFileEditTile(fileContainer, rid, state, buildDeps());
3953
+ },
3954
+ ctx.instance
3955
+ ).catch((err) => {
3956
+ console.error("Library pick failed:", err);
3957
+ });
3958
+ });
3959
+ libraryBtn.style.cssText = "flex:1;min-width:0;";
3960
+ row.appendChild(uploadCard);
3961
+ row.appendChild(libraryBtn);
3962
+ fileContainer.appendChild(row);
3963
+ } else {
3964
+ handlers.restoreDropzone();
3965
+ }
3966
+ };
3582
3967
  if (initial) {
3583
3968
  handleInitialFileData(
3584
3969
  initial,
@@ -3594,20 +3979,21 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3594
3979
  setupDragAndDrop(fileContainer, handlers.dragHandler);
3595
3980
  }
3596
3981
  } else {
3597
- handlers.restoreDropzone();
3982
+ renderEmptySingleState();
3598
3983
  }
3599
3984
  picker.onchange = () => {
3600
3985
  if (picker.files && picker.files.length > 0) {
3601
- handleFileSelect(
3602
- picker.files[0],
3603
- fileContainer,
3604
- pathKey,
3986
+ handleFileSelect({
3987
+ file: picker.files[0],
3988
+ container: fileContainer,
3989
+ fieldName: pathKey,
3605
3990
  state,
3606
- buildDeps(),
3607
- ctx.instance,
3608
- allowedExts,
3991
+ deps: buildDeps(),
3992
+ instance: ctx.instance,
3993
+ allowedExtensions: allowedExts,
3994
+ allowedMimes,
3609
3995
  maxSizeMB
3610
- );
3996
+ });
3611
3997
  }
3612
3998
  };
3613
3999
  fileWrapper.appendChild(fileContainer);
@@ -3625,7 +4011,10 @@ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
3625
4011
  filesPicker.multiple = true;
3626
4012
  filesPicker.style.display = "none";
3627
4013
  if (element.accept) {
3628
- filesPicker.accept = typeof element.accept === "string" ? element.accept : element.accept.extensions?.map((ext) => `.${ext}`).join(",") || "";
4014
+ filesPicker.accept = typeof element.accept === "string" ? element.accept : [
4015
+ ...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
4016
+ ...element.accept.mime ?? []
4017
+ ].join(",") || "";
3629
4018
  }
3630
4019
  const filesContainer = document.createElement("div");
3631
4020
  filesContainer.className = "files-list-wrapper";
@@ -3639,29 +4028,43 @@ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
3639
4028
  const filesConstraints = {
3640
4029
  maxCount: Infinity,
3641
4030
  allowedExtensions: getAllowedExtensions(element.accept),
4031
+ allowedMimes: getAllowedMimes(element.accept),
3642
4032
  maxSize: element.maxSize ?? Infinity
3643
4033
  };
3644
4034
  filesContainer.appendChild(list);
3645
4035
  filesWrapper.appendChild(filesPicker);
3646
4036
  filesWrapper.appendChild(filesContainer);
3647
4037
  wrapper.appendChild(filesWrapper);
4038
+ const onLibraryPickFiles = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4039
+ handleLibraryPickMulti(
4040
+ state,
4041
+ element,
4042
+ filesWrapper,
4043
+ pathKey,
4044
+ initialFiles,
4045
+ Infinity,
4046
+ updateFilesList,
4047
+ ctx.instance
4048
+ ).catch((err) => {
4049
+ console.error("Library pick failed:", err);
4050
+ });
4051
+ } : null;
3648
4052
  function updateFilesList() {
3649
4053
  const currentlyReadonly = isElementReadonly(element, state);
3650
- renderResourcePills(
3651
- list,
3652
- initialFiles,
4054
+ renderResourcePills({
4055
+ container: list,
4056
+ rids: initialFiles,
3653
4057
  state,
3654
- currentlyReadonly ? null : (ridToRemove) => {
4058
+ onRemove: currentlyReadonly ? null : (ridToRemove) => {
3655
4059
  releaseLocalFileUrl(state.resourceIndex.get(ridToRemove)?.file);
3656
4060
  const index = initialFiles.indexOf(ridToRemove);
3657
4061
  if (index > -1) initialFiles.splice(index, 1);
3658
4062
  updateFilesList();
3659
4063
  },
3660
- filesFieldHint,
3661
- void 0,
3662
- void 0,
3663
- currentlyReadonly
3664
- );
4064
+ hint: filesFieldHint,
4065
+ isReadonly: currentlyReadonly,
4066
+ onLibraryPick: currentlyReadonly ? null : onLibraryPickFiles
4067
+ });
3665
4068
  }
3666
4069
  updateFilesList();
3667
4070
  setupFilesDropHandler(
@@ -3696,7 +4099,10 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
3696
4099
  filesPicker.multiple = true;
3697
4100
  filesPicker.style.display = "none";
3698
4101
  if (element.accept) {
3699
- filesPicker.accept = typeof element.accept === "string" ? element.accept : element.accept.extensions?.map((ext) => `.${ext}`).join(",") || "";
4102
+ filesPicker.accept = typeof element.accept === "string" ? element.accept : [
4103
+ ...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
4104
+ ...element.accept.mime ?? []
4105
+ ].join(",") || "";
3700
4106
  }
3701
4107
  const filesContainer = document.createElement("div");
3702
4108
  filesContainer.className = "files-list-wrapper";
@@ -3713,6 +4119,7 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
3713
4119
  const multipleConstraints = {
3714
4120
  maxCount: maxFiles,
3715
4121
  allowedExtensions: getAllowedExtensions(element.accept),
4122
+ allowedMimes: getAllowedMimes(element.accept),
3716
4123
  maxSize: element.maxSize ?? Infinity
3717
4124
  };
3718
4125
  const buildCountInfo = () => {
@@ -3720,22 +4127,37 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
3720
4127
  const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
3721
4128
  return countText + minMaxText;
3722
4129
  };
4130
+ const onLibraryPickMultiple = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4131
+ handleLibraryPickMulti(
4132
+ state,
4133
+ element,
4134
+ filesWrapper,
4135
+ pathKey,
4136
+ initialFiles,
4137
+ maxFiles,
4138
+ updateFilesDisplay,
4139
+ ctx.instance
4140
+ ).catch((err) => {
4141
+ console.error("Library pick failed:", err);
4142
+ });
4143
+ } : null;
3723
4144
  const updateFilesDisplay = () => {
3724
4145
  const currentlyReadonly = isElementReadonly(element, state);
3725
- renderResourcePills(
3726
- list,
3727
- initialFiles,
4146
+ renderResourcePills({
4147
+ container: list,
4148
+ rids: initialFiles,
3728
4149
  state,
3729
- currentlyReadonly ? null : (index) => {
4150
+ onRemove: currentlyReadonly ? null : (index) => {
3730
4151
  releaseLocalFileUrl(state.resourceIndex.get(index)?.file);
3731
4152
  initialFiles.splice(initialFiles.indexOf(index), 1);
3732
4153
  updateFilesDisplay();
3733
4154
  },
3734
- multipleFilesHint,
3735
- buildCountInfo(),
3736
- maxFiles < Infinity ? maxFiles : void 0,
3737
- currentlyReadonly
3738
- );
4155
+ hint: multipleFilesHint,
4156
+ countInfo: buildCountInfo(),
4157
+ maxCount: maxFiles < Infinity ? maxFiles : void 0,
4158
+ isReadonly: currentlyReadonly,
4159
+ onLibraryPick: currentlyReadonly ? null : onLibraryPickMultiple
4160
+ });
3739
4161
  };
3740
4162
  setupFilesDropHandler(
3741
4163
  filesContainer,
@@ -3792,18 +4214,29 @@ function validateFileCount(key, resourceIds, element, state, errors) {
3792
4214
  errors.push(`${key}: ${t("maxFiles", state, { max: maxFiles })}`);
3793
4215
  }
3794
4216
  }
3795
- function validateFileExtensions(key, resourceIds, element, state, errors) {
4217
+ function validateFileTypes(key, resourceIds, element, state, errors) {
3796
4218
  const acceptField = "accept" in element ? element.accept : void 0;
3797
4219
  const allowedExtensions = getAllowedExtensions(acceptField);
3798
- if (allowedExtensions.length === 0) return;
4220
+ const allowedMimes = getAllowedMimes(acceptField);
4221
+ if (allowedExtensions.length === 0 && allowedMimes.length === 0) return;
3799
4222
  const formats = allowedExtensions.join(", ");
4223
+ const mimes = allowedMimes.join(", ");
3800
4224
  for (const rid of resourceIds) {
3801
4225
  const meta = state.resourceIndex.get(rid);
3802
4226
  const fileName = meta?.name ?? rid;
3803
- if (!isFileExtensionAllowed(fileName, allowedExtensions)) {
4227
+ if (allowedExtensions.length > 0 && !isFileExtensionAllowed(fileName, allowedExtensions)) {
3804
4228
  errors.push(
3805
4229
  `${key}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
3806
4230
  );
4231
+ continue;
4232
+ }
4233
+ if (allowedMimes.length > 0 && !meta?.inferredFromExtension) {
4234
+ const mimeType = meta?.type ?? "";
4235
+ if (!isMimeAllowed(mimeType, allowedMimes)) {
4236
+ errors.push(
4237
+ `${key}: ${t("invalidFileMime", state, { name: fileName, type: mimeType, mimes })}`
4238
+ );
4239
+ }
3807
4240
  }
3808
4241
  }
3809
4242
  }
@@ -3827,7 +4260,7 @@ function validateMultiFile(element, key, context) {
3827
4260
  const resourceIds = readMultiFileResourceIds(scopeRoot, fullKey);
3828
4261
  if (!skipValidation) {
3829
4262
  validateFileCount(key, resourceIds, element, state, errors);
3830
- validateFileExtensions(key, resourceIds, element, state, errors);
4263
+ validateFileTypes(key, resourceIds, element, state, errors);
3831
4264
  validateFileSizes(key, resourceIds, element, state, errors);
3832
4265
  }
3833
4266
  return { value: resourceIds, errors };
@@ -3844,7 +4277,7 @@ function validateSingleFile(element, key, context) {
3844
4277
  return { value: null, errors };
3845
4278
  }
3846
4279
  if (!skipValidation && rid !== "") {
3847
- validateFileExtensions(key, [rid], element, state, errors);
4280
+ validateFileTypes(key, [rid], element, state, errors);
3848
4281
  validateFileSizes(key, [rid], element, state, errors);
3849
4282
  }
3850
4283
  return { value: rid || null, errors };
@@ -3970,9 +4403,7 @@ function updateFileField(element, fieldPath, value, context) {
3970
4403
  }
3971
4404
  value.forEach((resourceId) => {
3972
4405
  if (resourceId && typeof resourceId === "string") {
3973
- if (!state.resourceIndex.has(resourceId)) {
3974
- addResourceToIndex(resourceId, state);
3975
- }
4406
+ seedInferredResource(resourceId, state.resourceIndex);
3976
4407
  }
3977
4408
  });
3978
4409
  const filesWrapper = scopeRoot.querySelector(
@@ -3997,34 +4428,13 @@ function updateFileField(element, fieldPath, value, context) {
3997
4428
  }
3998
4429
  hiddenInput.value = value != null ? String(value) : "";
3999
4430
  if (value && typeof value === "string") {
4000
- if (!state.resourceIndex.has(value)) {
4001
- addResourceToIndex(value, state);
4002
- }
4431
+ seedInferredResource(value, state.resourceIndex);
4003
4432
  console.info(
4004
4433
  `updateFileField: File field "${fieldPath}" updated. Preview update requires re-render.`
4005
4434
  );
4006
4435
  }
4007
4436
  }
4008
4437
  }
4009
- function addResourceToIndex(resourceId, state) {
4010
- const filename = resourceId.split("/").pop() || "file";
4011
- const extension = filename.split(".").pop()?.toLowerCase();
4012
- let fileType = "application/octet-stream";
4013
- if (extension) {
4014
- if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
4015
- fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
4016
- } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
4017
- fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
4018
- }
4019
- }
4020
- state.resourceIndex.set(resourceId, {
4021
- name: filename,
4022
- type: fileType,
4023
- size: 0,
4024
- uploadedAt: /* @__PURE__ */ new Date(),
4025
- file: void 0
4026
- });
4027
- }
4028
4438
 
4029
4439
  // src/components/colour.ts
4030
4440
  function normalizeColourValue(value) {
@@ -8668,6 +9078,7 @@ var defaultConfig = {
8668
9078
  enableFilePreview: true,
8669
9079
  maxPreviewSize: "200px",
8670
9080
  readonly: false,
9081
+ pickExistingFiles: null,
8671
9082
  parseTableFile: null,
8672
9083
  locale: "en",
8673
9084
  translations: {
@@ -8704,6 +9115,10 @@ var defaultConfig = {
8704
9115
  fileCountRange: "({min}-{max})",
8705
9116
  uploadingFile: "Uploading\u2026",
8706
9117
  filesCounter: "{count}/{max}",
9118
+ fromLibrary: "From library",
9119
+ libraryEmpty: "Library is empty",
9120
+ libraryHint: "Choose from previously uploaded files",
9121
+ pickerError: "Failed to load files from library",
8707
9122
  // Validation errors
8708
9123
  required: "Required",
8709
9124
  minItems: "Minimum {min} items required",
@@ -8719,6 +9134,7 @@ var defaultConfig = {
8719
9134
  minFiles: "Minimum {min} files required",
8720
9135
  maxFiles: "Maximum {max} files allowed",
8721
9136
  invalidFileExtension: 'File "{name}" has unsupported format. Allowed: {formats}',
9137
+ invalidFileMime: 'File "{name}": file type {type} not allowed (allowed: {mimes})',
8722
9138
  fileTooLarge: 'File "{name}" exceeds maximum size of {maxSize}MB',
8723
9139
  filesLimitExceeded: "{skipped} file(s) skipped: maximum {max} files allowed",
8724
9140
  unsupportedFieldType: "Unsupported field type: {type}",
@@ -8770,6 +9186,10 @@ var defaultConfig = {
8770
9186
  fileCountRange: "({min}-{max})",
8771
9187
  uploadingFile: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026",
8772
9188
  filesCounter: "{count}/{max}",
9189
+ fromLibrary: "\u0418\u0437 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438",
9190
+ libraryEmpty: "\u0411\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0430 \u043F\u0443\u0441\u0442\u0430",
9191
+ libraryHint: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0438\u0437 \u0440\u0430\u043D\u0435\u0435 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043D\u044B\u0445 \u0444\u0430\u0439\u043B\u043E\u0432",
9192
+ pickerError: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B\u044B \u0438\u0437 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438",
8773
9193
  // Validation errors
8774
9194
  required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
8775
9195
  minItems: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
@@ -8785,6 +9205,7 @@ var defaultConfig = {
8785
9205
  minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
8786
9206
  maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
8787
9207
  invalidFileExtension: '\u0424\u0430\u0439\u043B "{name}" \u0438\u043C\u0435\u0435\u0442 \u043D\u0435\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442. \u0414\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0435: {formats}',
9208
+ invalidFileMime: '\u0424\u0430\u0439\u043B "{name}": \u0442\u0438\u043F \u0444\u0430\u0439\u043B\u0430 {type} \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D (\u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u044B: {mimes})',
8788
9209
  fileTooLarge: '\u0424\u0430\u0439\u043B "{name}" \u043F\u0440\u0435\u0432\u044B\u0448\u0430\u0435\u0442 \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 {maxSize}\u041C\u0411',
8789
9210
  filesLimitExceeded: "{skipped} \u0444\u0430\u0439\u043B(\u043E\u0432) \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u043E: \u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
8790
9211
  unsupportedFieldType: "\u041D\u0435\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0439 \u0442\u0438\u043F \u043F\u043E\u043B\u044F: {type}",