@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.
@@ -2136,31 +2136,59 @@ function isFileExtensionAllowed(fileName, allowedExtensions) {
2136
2136
  const ext = (_b = (_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase()) != null ? _b : "";
2137
2137
  return allowedExtensions.includes(ext);
2138
2138
  }
2139
- function isFileSizeAllowed(file, maxSizeMB) {
2139
+ function isSizeWithinLimit(bytes, maxSizeMB) {
2140
2140
  if (maxSizeMB === Infinity) return true;
2141
- return file.size <= maxSizeMB * 1024 * 1024;
2141
+ return bytes <= maxSizeMB * 1024 * 1024;
2142
2142
  }
2143
- function addPrefillFilesToIndex(initialFiles, resourceIndex) {
2143
+ function isFileSizeAllowed(file, maxSizeMB) {
2144
+ return isSizeWithinLimit(file.size, maxSizeMB);
2145
+ }
2146
+ function getAllowedMimes(accept) {
2147
+ if (!accept) return [];
2148
+ if (typeof accept === "string") return [];
2149
+ if (!Array.isArray(accept.mime)) return [];
2150
+ return accept.mime.map((m) => m.toLowerCase());
2151
+ }
2152
+ function isMimeAllowed(mimeType, allowedMimes) {
2153
+ if (allowedMimes.length === 0) return true;
2154
+ const normalizedType = mimeType.toLowerCase();
2155
+ return allowedMimes.some((allowed) => {
2156
+ if (allowed.endsWith("/*")) {
2157
+ const prefix = allowed.slice(0, -1);
2158
+ return normalizedType.startsWith(prefix);
2159
+ }
2160
+ return normalizedType === allowed;
2161
+ });
2162
+ }
2163
+ function inferMimeFromResourceId(resourceId) {
2144
2164
  var _a;
2165
+ const filename = resourceId.split("/").pop() || "file";
2166
+ const extension = (_a = filename.split(".").pop()) == null ? void 0 : _a.toLowerCase();
2167
+ if (extension) {
2168
+ if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
2169
+ return `image/${extension === "jpg" ? "jpeg" : extension}`;
2170
+ }
2171
+ if (["mp4", "webm", "mov", "avi"].includes(extension)) {
2172
+ return `video/${extension === "mov" ? "quicktime" : extension}`;
2173
+ }
2174
+ }
2175
+ return "application/octet-stream";
2176
+ }
2177
+ function seedInferredResource(resourceId, resourceIndex) {
2178
+ if (resourceIndex.has(resourceId)) return;
2179
+ const filename = resourceId.split("/").pop() || "file";
2180
+ resourceIndex.set(resourceId, {
2181
+ name: filename,
2182
+ type: inferMimeFromResourceId(resourceId),
2183
+ size: 0,
2184
+ uploadedAt: /* @__PURE__ */ new Date(),
2185
+ file: void 0,
2186
+ inferredFromExtension: true
2187
+ });
2188
+ }
2189
+ function addPrefillFilesToIndex(initialFiles, resourceIndex) {
2145
2190
  for (const resourceId of initialFiles) {
2146
- if (resourceIndex.has(resourceId)) continue;
2147
- const filename = resourceId.split("/").pop() || "file";
2148
- const extension = (_a = filename.split(".").pop()) == null ? void 0 : _a.toLowerCase();
2149
- let fileType = "application/octet-stream";
2150
- if (extension) {
2151
- if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
2152
- fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
2153
- } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
2154
- fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
2155
- }
2156
- }
2157
- resourceIndex.set(resourceId, {
2158
- name: filename,
2159
- type: fileType,
2160
- size: 0,
2161
- uploadedAt: /* @__PURE__ */ new Date(),
2162
- file: void 0
2163
- });
2191
+ seedInferredResource(resourceId, resourceIndex);
2164
2192
  }
2165
2193
  }
2166
2194
 
@@ -2437,6 +2465,84 @@ function ensureFileStyles() {
2437
2465
  z-index: 10000;
2438
2466
  }
2439
2467
 
2468
+ /* Two-card empty-state layout (upload card + library card) */
2469
+ .fb-file-card-row {
2470
+ display: flex;
2471
+ gap: 8px;
2472
+ align-items: stretch;
2473
+ }
2474
+ .fb-file-card-row .fb-file-dropzone,
2475
+ .fb-file-card-row .fb-file-library-card {
2476
+ flex: 1;
2477
+ min-width: 0;
2478
+ }
2479
+
2480
+ /* Library picker card \u2014 mirrors .fb-file-dropzone styling */
2481
+ .fb-file-library-card {
2482
+ height: 128px;
2483
+ border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2484
+ border-radius: var(--fb-border-radius, 0.5rem);
2485
+ display: flex;
2486
+ flex-direction: column;
2487
+ align-items: center;
2488
+ justify-content: center;
2489
+ gap: 4px;
2490
+ cursor: pointer;
2491
+ background: none;
2492
+ padding: 0;
2493
+ transition:
2494
+ border-color var(--fb-transition-duration, 200ms),
2495
+ background var(--fb-transition-duration, 200ms);
2496
+ width: 100%;
2497
+ }
2498
+ .fb-file-library-card:hover,
2499
+ .fb-file-library-card:focus-visible {
2500
+ border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2501
+ background: var(--fb-background-hover-color, #f9fafb);
2502
+ outline: none;
2503
+ }
2504
+ .fb-file-library-card-icon {
2505
+ font-size: 24px;
2506
+ line-height: 1;
2507
+ flex-shrink: 0;
2508
+ }
2509
+ .fb-file-library-card-label {
2510
+ font-size: 13px;
2511
+ color: var(--fb-text-secondary-color, #6b7280);
2512
+ }
2513
+ .fb-file-library-card-hint {
2514
+ font-size: 11px;
2515
+ color: var(--fb-file-upload-text-color, #9ca3af);
2516
+ }
2517
+
2518
+ /* Library "\u{1F4DA}" add-tile \u2014 same size/style as the "+" add tile */
2519
+ .fb-tile-add-library {
2520
+ border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2521
+ display: flex;
2522
+ align-items: center;
2523
+ justify-content: center;
2524
+ cursor: pointer;
2525
+ font-size: 24px;
2526
+ color: var(--fb-file-upload-text-color, #9ca3af);
2527
+ transition:
2528
+ border-color var(--fb-transition-duration, 200ms),
2529
+ color var(--fb-transition-duration, 200ms);
2530
+ background: none;
2531
+ padding: 0;
2532
+ width: var(--fb-tile-size, 160px);
2533
+ height: var(--fb-tile-size, 160px);
2534
+ flex-shrink: 0;
2535
+ position: relative;
2536
+ overflow: hidden;
2537
+ border-radius: var(--fb-border-radius, 0.5rem);
2538
+ }
2539
+ .fb-tile-add-library:hover,
2540
+ .fb-tile-add-library:focus-visible {
2541
+ border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2542
+ color: var(--fb-text-color, #1f2937);
2543
+ outline: none;
2544
+ }
2545
+
2440
2546
  /* Hover zoom preview popup for image tiles \u2014 appended to document.body (fixed) */
2441
2547
  .fb-tile-zoom-preview {
2442
2548
  position: fixed;
@@ -2558,23 +2664,35 @@ function setEmptyFileContainer(fileContainer, state, hint) {
2558
2664
  </div>
2559
2665
  `;
2560
2666
  }
2667
+ var dragDropHandlers = /* @__PURE__ */ new WeakMap();
2561
2668
  function setupDragAndDrop(element, dropHandler) {
2562
- element.addEventListener("dragover", (e) => {
2669
+ const prev = dragDropHandlers.get(element);
2670
+ if (prev) {
2671
+ element.removeEventListener("dragover", prev.dragover);
2672
+ element.removeEventListener("dragleave", prev.dragleave);
2673
+ element.removeEventListener("drop", prev.drop);
2674
+ }
2675
+ const dragover = (e) => {
2563
2676
  e.preventDefault();
2564
2677
  element.classList.add("border-blue-500", "bg-blue-50");
2565
- });
2566
- element.addEventListener("dragleave", (e) => {
2678
+ };
2679
+ const dragleave = (e) => {
2567
2680
  e.preventDefault();
2568
2681
  element.classList.remove("border-blue-500", "bg-blue-50");
2569
- });
2570
- element.addEventListener("drop", (e) => {
2682
+ };
2683
+ const drop = (e) => {
2571
2684
  var _a;
2572
2685
  e.preventDefault();
2573
2686
  element.classList.remove("border-blue-500", "bg-blue-50");
2574
- if ((_a = e.dataTransfer) == null ? void 0 : _a.files) {
2575
- dropHandler(e.dataTransfer.files);
2687
+ const files = (_a = e.dataTransfer) == null ? void 0 : _a.files;
2688
+ if (files) {
2689
+ dropHandler(files);
2576
2690
  }
2577
- });
2691
+ };
2692
+ element.addEventListener("dragover", dragover);
2693
+ element.addEventListener("dragleave", dragleave);
2694
+ element.addEventListener("drop", drop);
2695
+ dragDropHandlers.set(element, { dragover, dragleave, drop });
2578
2696
  }
2579
2697
 
2580
2698
  // src/components/file/preview.ts
@@ -3271,8 +3389,19 @@ async function uploadSingleFile(file, state) {
3271
3389
  throw new Error(`File upload failed: ${err.message}`);
3272
3390
  }
3273
3391
  }
3274
- async function handleFileSelect(file, container, fieldName, state, deps = null, instance, allowedExtensions = [], maxSizeMB = Infinity) {
3392
+ async function handleFileSelect(opts) {
3275
3393
  var _a, _b;
3394
+ const {
3395
+ file,
3396
+ container,
3397
+ fieldName,
3398
+ state,
3399
+ deps = null,
3400
+ instance = null,
3401
+ allowedExtensions = [],
3402
+ allowedMimes = [],
3403
+ maxSizeMB = Infinity
3404
+ } = opts;
3276
3405
  if (!isFileExtensionAllowed(file.name, allowedExtensions)) {
3277
3406
  const formats = allowedExtensions.join(", ");
3278
3407
  showFileError(
@@ -3281,6 +3410,14 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
3281
3410
  );
3282
3411
  return;
3283
3412
  }
3413
+ if (!isMimeAllowed(file.type, allowedMimes)) {
3414
+ const mimes = allowedMimes.join(", ");
3415
+ showFileError(
3416
+ container,
3417
+ t("invalidFileMime", state, { name: file.name, type: file.type, mimes })
3418
+ );
3419
+ return;
3420
+ }
3284
3421
  if (!isFileSizeAllowed(file, maxSizeMB)) {
3285
3422
  showFileError(
3286
3423
  container,
@@ -3340,10 +3477,16 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
3340
3477
  const afterExt = allFiles.filter(
3341
3478
  (f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
3342
3479
  );
3343
- const rejectedBySize = afterExt.filter(
3480
+ const rejectedByMime = afterExt.filter(
3481
+ (f) => !isMimeAllowed(f.type, constraints.allowedMimes)
3482
+ );
3483
+ const afterMime = afterExt.filter(
3484
+ (f) => isMimeAllowed(f.type, constraints.allowedMimes)
3485
+ );
3486
+ const rejectedBySize = afterMime.filter(
3344
3487
  (f) => !isFileSizeAllowed(f, constraints.maxSize)
3345
3488
  );
3346
- const valid = afterExt.filter((f) => isFileSizeAllowed(f, constraints.maxSize));
3489
+ const valid = afterMime.filter((f) => isFileSizeAllowed(f, constraints.maxSize));
3347
3490
  const remaining = constraints.maxCount === Infinity ? valid.length : Math.max(0, constraints.maxCount - currentCount);
3348
3491
  const accepted = valid.slice(0, remaining);
3349
3492
  const skippedByCount = valid.length - accepted.length;
@@ -3353,6 +3496,11 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
3353
3496
  const names = rejectedByExt.map((f) => f.name).join(", ");
3354
3497
  errorParts.push(t("invalidFileExtension", state, { name: names, formats }));
3355
3498
  }
3499
+ if (rejectedByMime.length > 0) {
3500
+ const mimes = constraints.allowedMimes.join(", ");
3501
+ const names = rejectedByMime.map((f) => f.name).join(", ");
3502
+ errorParts.push(t("invalidFileMime", state, { name: names, type: rejectedByMime.map((f) => f.type).join(", "), mimes }));
3503
+ }
3356
3504
  if (rejectedBySize.length > 0) {
3357
3505
  const names = rejectedBySize.map((f) => f.name).join(", ");
3358
3506
  errorParts.push(
@@ -3445,30 +3593,166 @@ function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback
3445
3593
  };
3446
3594
  }
3447
3595
 
3448
- // src/components/file/render-edit.ts
3449
- function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
3596
+ // src/components/file/library.ts
3597
+ function buildAcceptContext(element) {
3450
3598
  var _a, _b;
3451
- if (!state.resourceIndex.has(initial)) {
3452
- const filename = initial.split("/").pop() || "file";
3453
- const extension = (_a = filename.split(".").pop()) == null ? void 0 : _a.toLowerCase();
3454
- let fileType = "application/octet-stream";
3455
- if (extension) {
3456
- if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
3457
- fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
3458
- } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
3459
- fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
3460
- }
3461
- }
3462
- state.resourceIndex.set(initial, {
3463
- name: filename,
3464
- type: fileType,
3465
- size: 0,
3466
- uploadedAt: /* @__PURE__ */ new Date(),
3467
- file: void 0
3599
+ if (!element.accept) return void 0;
3600
+ if (typeof element.accept === "string") {
3601
+ const exts2 = getAllowedExtensions(element.accept);
3602
+ return exts2.length > 0 ? { extensions: exts2 } : void 0;
3603
+ }
3604
+ const exts = (_a = element.accept.extensions) != null ? _a : [];
3605
+ const mime = (_b = element.accept.mime) != null ? _b : [];
3606
+ const normalizedExts = exts.map((e) => e.toLowerCase());
3607
+ if (normalizedExts.length === 0 && mime.length === 0) return void 0;
3608
+ const result = {};
3609
+ if (normalizedExts.length > 0) result.extensions = normalizedExts;
3610
+ if (mime.length > 0) result.mime = mime;
3611
+ return result;
3612
+ }
3613
+ function validatePickedResource(resource, allowedExtensions, allowedMimes, maxSizeMB, state) {
3614
+ if (!isFileExtensionAllowed(resource.name, allowedExtensions)) {
3615
+ const formats = allowedExtensions.join(", ");
3616
+ return t("invalidFileExtension", state, { name: resource.name, formats });
3617
+ }
3618
+ if (!isMimeAllowed(resource.type, allowedMimes)) {
3619
+ const mimes = allowedMimes.join(", ");
3620
+ return t("invalidFileMime", state, { name: resource.name, type: resource.type, mimes });
3621
+ }
3622
+ if (!isSizeWithinLimit(resource.size, maxSizeMB)) {
3623
+ return t("fileTooLarge", state, { name: resource.name, maxSize: maxSizeMB });
3624
+ }
3625
+ return null;
3626
+ }
3627
+ function readCurrentResourceIds(wrapper) {
3628
+ const raw = wrapper.dataset.resourceIds;
3629
+ if (!raw) return [];
3630
+ try {
3631
+ const parsed = JSON.parse(raw);
3632
+ return Array.isArray(parsed) ? parsed : [];
3633
+ } catch {
3634
+ return [];
3635
+ }
3636
+ }
3637
+ function registerPickedResource(resource, state) {
3638
+ var _a;
3639
+ const existing = state.resourceIndex.get(resource.resourceId);
3640
+ state.resourceIndex.set(resource.resourceId, {
3641
+ name: resource.name,
3642
+ type: resource.type,
3643
+ size: resource.size,
3644
+ uploadedAt: (_a = existing == null ? void 0 : existing.uploadedAt) != null ? _a : /* @__PURE__ */ new Date(),
3645
+ file: existing == null ? void 0 : existing.file
3646
+ });
3647
+ }
3648
+ function extractPickerError(error, state) {
3649
+ if (error instanceof Error && error.message) return error.message;
3650
+ return t("pickerError", state);
3651
+ }
3652
+ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resourceIds, maxCount, updateCallback, instance) {
3653
+ var _a;
3654
+ if (!state.config.pickExistingFiles) return;
3655
+ const allowedExtensions = getAllowedExtensions(element.accept);
3656
+ const allowedMimes = getAllowedMimes(element.accept);
3657
+ const maxSizeMB = (_a = element.maxSize) != null ? _a : Infinity;
3658
+ const currentIds = readCurrentResourceIds(wrapper);
3659
+ const remaining = maxCount === Infinity ? Infinity : Math.max(0, maxCount - currentIds.length);
3660
+ let picked;
3661
+ try {
3662
+ picked = await state.config.pickExistingFiles({
3663
+ fieldPath,
3664
+ mode: "multiple",
3665
+ accept: buildAcceptContext(element),
3666
+ maxSizeMB: maxSizeMB === Infinity ? void 0 : maxSizeMB,
3667
+ remainingSlots: remaining === Infinity ? void 0 : remaining,
3668
+ selectedResourceIds: [...currentIds]
3468
3669
  });
3670
+ } catch (error) {
3671
+ showFileError(wrapper, extractPickerError(error, state));
3672
+ return;
3673
+ }
3674
+ if (picked.length === 0) return;
3675
+ const existingSet = new Set(currentIds);
3676
+ const seen = /* @__PURE__ */ new Set();
3677
+ const deduped = picked.filter((r) => {
3678
+ if (existingSet.has(r.resourceId)) return false;
3679
+ if (seen.has(r.resourceId)) return false;
3680
+ seen.add(r.resourceId);
3681
+ return true;
3682
+ });
3683
+ const validItems = deduped.filter((r) => {
3684
+ const err = validatePickedResource(r, allowedExtensions, allowedMimes, maxSizeMB, state);
3685
+ return err === null;
3686
+ });
3687
+ const freshRemaining = maxCount === Infinity ? validItems.length : Math.max(0, maxCount - resourceIds.length);
3688
+ const accepted = validItems.slice(0, freshRemaining);
3689
+ const skipped = validItems.length - accepted.length;
3690
+ if (accepted.length === 0) return;
3691
+ clearFileError(wrapper);
3692
+ if (skipped > 0) {
3693
+ showFileError(
3694
+ wrapper,
3695
+ t("filesLimitExceeded", state, { skipped, max: maxCount })
3696
+ );
3469
3697
  }
3698
+ for (const resource of accepted) {
3699
+ registerPickedResource(resource, state);
3700
+ resourceIds.push(resource.resourceId);
3701
+ }
3702
+ wrapper.dataset.resourceIds = JSON.stringify(resourceIds);
3703
+ updateCallback();
3704
+ if (!state.config.readonly) {
3705
+ instance.triggerOnChange(fieldPath, resourceIds);
3706
+ }
3707
+ }
3708
+ async function handleLibraryPickSingle(state, element, container, fileWrapper, pathKey, fieldPath, renderCallback, instance) {
3709
+ var _a;
3710
+ if (!state.config.pickExistingFiles) return;
3711
+ const allowedExtensions = getAllowedExtensions(element.accept);
3712
+ const allowedMimes = getAllowedMimes(element.accept);
3713
+ const maxSizeMB = (_a = element.maxSize) != null ? _a : Infinity;
3714
+ let picked;
3715
+ try {
3716
+ picked = await state.config.pickExistingFiles({
3717
+ fieldPath,
3718
+ mode: "single",
3719
+ accept: buildAcceptContext(element),
3720
+ maxSizeMB: maxSizeMB === Infinity ? void 0 : maxSizeMB,
3721
+ selectedResourceIds: []
3722
+ });
3723
+ } catch (error) {
3724
+ showFileError(container, extractPickerError(error, state));
3725
+ return;
3726
+ }
3727
+ if (picked.length === 0) return;
3728
+ const first = picked[0];
3729
+ const validationError = validatePickedResource(first, allowedExtensions, allowedMimes, maxSizeMB, state);
3730
+ if (validationError !== null) {
3731
+ showFileError(container, validationError);
3732
+ return;
3733
+ }
3734
+ clearFileError(container);
3735
+ registerPickedResource(first, state);
3736
+ let hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
3737
+ if (!hiddenInput) {
3738
+ hiddenInput = document.createElement("input");
3739
+ hiddenInput.type = "hidden";
3740
+ hiddenInput.name = pathKey;
3741
+ fileWrapper.appendChild(hiddenInput);
3742
+ }
3743
+ hiddenInput.value = first.resourceId;
3744
+ await renderCallback(first.resourceId);
3745
+ if (!state.config.readonly) {
3746
+ instance.triggerOnChange(fieldPath, first.resourceId);
3747
+ }
3748
+ }
3749
+
3750
+ // src/components/file/render-edit.ts
3751
+ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
3752
+ var _a;
3753
+ seedInferredResource(initial, state.resourceIndex);
3470
3754
  const meta = state.resourceIndex.get(initial);
3471
- const isVideo = (_b = meta == null ? void 0 : meta.type) == null ? void 0 : _b.startsWith("video/");
3755
+ const isVideo = (_a = meta == null ? void 0 : meta.type) == null ? void 0 : _a.startsWith("video/");
3472
3756
  if (isVideo) {
3473
3757
  renderFilePreview(fileContainer, initial, state, {
3474
3758
  fileName: initial,
@@ -3484,8 +3768,51 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
3484
3768
  hiddenInput.value = initial;
3485
3769
  fileWrapper.appendChild(hiddenInput);
3486
3770
  }
3487
- function renderResourcePills(container, rids, state, onRemove, hint, countInfo, maxCount, isReadonly = false) {
3771
+ 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);">
3772
+ <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"/>
3773
+ </svg>`;
3774
+ function buildEmptyDropzone(state, primaryText, subHint, openPicker) {
3775
+ const dropzone = document.createElement("div");
3776
+ dropzone.className = "fb-file-dropzone";
3777
+ dropzone.innerHTML = `
3778
+ ${UPLOAD_SVG}
3779
+ <div class="fb-dropzone-primary-text">${escapeHtml(primaryText)}</div>
3780
+ ${subHint ? `<div class="fb-dropzone-hint-text">${escapeHtml(subHint)}</div>` : ""}
3781
+ `;
3782
+ dropzone.onclick = openPicker;
3783
+ return dropzone;
3784
+ }
3785
+ function buildLibraryButton(variant, state, onClick) {
3786
+ const btn = document.createElement("button");
3787
+ btn.type = "button";
3788
+ btn.className = variant === "card" ? "fb-file-library-card" : "fb-tile fb-tile-add-library";
3789
+ if (variant === "card") {
3790
+ btn.innerHTML = `
3791
+ <span class="fb-file-library-card-icon" aria-hidden="true">\u{1F4DA}</span>
3792
+ <span class="fb-file-library-card-label">${escapeHtml(t("fromLibrary", state))}</span>
3793
+ <span class="fb-file-library-card-hint">${escapeHtml(t("libraryHint", state))}</span>
3794
+ `;
3795
+ } else {
3796
+ btn.innerHTML = `<span aria-hidden="true">\u{1F4DA}</span>`;
3797
+ btn.title = t("fromLibrary", state);
3798
+ btn.setAttribute("aria-label", t("fromLibrary", state));
3799
+ }
3800
+ btn.addEventListener("click", onClick);
3801
+ return btn;
3802
+ }
3803
+ function renderResourcePills(opts) {
3488
3804
  var _a;
3805
+ const {
3806
+ container,
3807
+ rids,
3808
+ state,
3809
+ onRemove,
3810
+ hint,
3811
+ countInfo,
3812
+ maxCount,
3813
+ isReadonly = false,
3814
+ onLibraryPick
3815
+ } = opts;
3489
3816
  ensureFileStyles();
3490
3817
  const wrapper = container.closest("[data-files-wrapper]");
3491
3818
  if (wrapper) {
@@ -3494,6 +3821,7 @@ function renderResourcePills(container, rids, state, onRemove, hint, countInfo,
3494
3821
  while (container.firstChild) container.removeChild(container.firstChild);
3495
3822
  const ridList = rids != null ? rids : [];
3496
3823
  const atMax = maxCount !== void 0 && ridList.length >= maxCount;
3824
+ const hasLibrary = !isReadonly && typeof onLibraryPick === "function";
3497
3825
  const buildSubHint = () => {
3498
3826
  const parts = [];
3499
3827
  if (hint) parts.push(hint);
@@ -3510,18 +3838,26 @@ function renderResourcePills(container, rids, state, onRemove, hint, countInfo,
3510
3838
  emptyEl.className = "fb-tile-empty-text";
3511
3839
  emptyEl.textContent = t("noFilesSelected", state);
3512
3840
  container.appendChild(emptyEl);
3841
+ } else if (hasLibrary) {
3842
+ const row = document.createElement("div");
3843
+ row.className = "fb-file-card-row";
3844
+ const dropzone = buildEmptyDropzone(
3845
+ state,
3846
+ t("clickDragTextMultiple", state),
3847
+ buildSubHint(),
3848
+ openPicker
3849
+ );
3850
+ const libraryBtn = buildLibraryButton("card", state, onLibraryPick);
3851
+ row.appendChild(dropzone);
3852
+ row.appendChild(libraryBtn);
3853
+ container.appendChild(row);
3513
3854
  } else {
3514
- const dropzone = document.createElement("div");
3515
- dropzone.className = "fb-file-dropzone";
3516
- const subHint2 = buildSubHint();
3517
- dropzone.innerHTML = `
3518
- <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" style="flex-shrink:0;color:var(--fb-file-upload-text-color,#9ca3af);">
3519
- <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"/>
3520
- </svg>
3521
- <div class="fb-dropzone-primary-text">${escapeHtml(t("clickDragTextMultiple", state))}</div>
3522
- ${subHint2 ? `<div class="fb-dropzone-hint-text">${escapeHtml(subHint2)}</div>` : ""}
3523
- `;
3524
- dropzone.onclick = openPicker;
3855
+ const dropzone = buildEmptyDropzone(
3856
+ state,
3857
+ t("clickDragTextMultiple", state),
3858
+ buildSubHint(),
3859
+ openPicker
3860
+ );
3525
3861
  container.appendChild(dropzone);
3526
3862
  }
3527
3863
  return;
@@ -3552,6 +3888,10 @@ function renderResourcePills(container, rids, state, onRemove, hint, countInfo,
3552
3888
  addTile.innerHTML = "+";
3553
3889
  addTile.onclick = openPicker;
3554
3890
  tilesWrap.appendChild(addTile);
3891
+ if (hasLibrary) {
3892
+ const libraryTile = buildLibraryButton("tile", state, onLibraryPick);
3893
+ tilesWrap.appendChild(libraryTile);
3894
+ }
3555
3895
  } else if (!isReadonly && atMax) {
3556
3896
  const chip = document.createElement("div");
3557
3897
  chip.className = "fb-tile-counter";
@@ -3571,7 +3911,7 @@ function renderResourcePills(container, rids, state, onRemove, hint, countInfo,
3571
3911
  }
3572
3912
  }
3573
3913
  function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3574
- var _a, _b, _c;
3914
+ var _a, _b, _c, _d, _e;
3575
3915
  const state = ctx.state;
3576
3916
  const fileWrapper = document.createElement("div");
3577
3917
  fileWrapper.className = "space-y-2";
@@ -3580,29 +3920,34 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3580
3920
  picker.name = pathKey;
3581
3921
  picker.style.display = "none";
3582
3922
  if (element.accept) {
3583
- picker.accept = typeof element.accept === "string" ? element.accept : ((_a = element.accept.extensions) == null ? void 0 : _a.map((ext) => `.${ext}`).join(",")) || "";
3923
+ picker.accept = typeof element.accept === "string" ? element.accept : [
3924
+ ...(_b = (_a = element.accept.extensions) == null ? void 0 : _a.map((ext) => `.${ext}`)) != null ? _b : [],
3925
+ ...(_c = element.accept.mime) != null ? _c : []
3926
+ ].join(",") || "";
3584
3927
  }
3585
3928
  const fileContainer = document.createElement("div");
3586
3929
  fileContainer.className = "file-preview-container";
3587
3930
  const initial = ctx.prefill[element.key];
3588
3931
  const allowedExts = getAllowedExtensions(element.accept);
3589
- const maxSizeMB = (_b = element.maxSize) != null ? _b : Infinity;
3932
+ const allowedMimes = getAllowedMimes(element.accept);
3933
+ const maxSizeMB = (_d = element.maxSize) != null ? _d : Infinity;
3590
3934
  const handlers = {
3591
3935
  fileUploadHandler() {
3592
3936
  picker.click();
3593
3937
  },
3594
3938
  dragHandler(files) {
3595
3939
  if (files.length > 0) {
3596
- handleFileSelect(
3597
- files[0],
3598
- fileContainer,
3599
- pathKey,
3940
+ handleFileSelect({
3941
+ file: files[0],
3942
+ container: fileContainer,
3943
+ fieldName: pathKey,
3600
3944
  state,
3601
- buildDeps(),
3602
- ctx.instance,
3603
- allowedExts,
3945
+ deps: buildDeps(),
3946
+ instance: ctx.instance,
3947
+ allowedExtensions: allowedExts,
3948
+ allowedMimes,
3604
3949
  maxSizeMB
3605
- );
3950
+ });
3606
3951
  }
3607
3952
  },
3608
3953
  setupDrop(container) {
@@ -3624,7 +3969,7 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3624
3969
  releaseLocalFileUrl((_a2 = state.resourceIndex.get(currentRid)) == null ? void 0 : _a2.file);
3625
3970
  }
3626
3971
  if (hiddenInput) hiddenInput.value = "";
3627
- handlers.restoreDropzone();
3972
+ renderEmptySingleState();
3628
3973
  }
3629
3974
  };
3630
3975
  const buildDeps = () => ({
@@ -3634,6 +3979,50 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3634
3979
  setupDrop: handlers.setupDrop,
3635
3980
  onRemove: handlers.onRemove
3636
3981
  });
3982
+ const renderEmptySingleState = () => {
3983
+ if (state.config.pickExistingFiles && !element.disableLibrary) {
3984
+ fileContainer.className = "file-preview-container";
3985
+ fileContainer.removeAttribute("style");
3986
+ fileContainer.onclick = null;
3987
+ while (fileContainer.firstChild) {
3988
+ fileContainer.removeChild(fileContainer.firstChild);
3989
+ }
3990
+ const row = document.createElement("div");
3991
+ row.className = "fb-file-card-row";
3992
+ row.style.cssText = "display:flex;gap:8px;align-items:stretch;";
3993
+ const hint = makeFieldHint(element, state);
3994
+ const uploadCard = buildEmptyDropzone(
3995
+ state,
3996
+ t("clickDragText", state),
3997
+ hint,
3998
+ handlers.fileUploadHandler
3999
+ );
4000
+ uploadCard.style.cssText = "flex:1;min-width:0;height:128px;";
4001
+ setupDragAndDrop(uploadCard, handlers.dragHandler);
4002
+ const libraryBtn = buildLibraryButton("card", state, () => {
4003
+ handleLibraryPickSingle(
4004
+ state,
4005
+ element,
4006
+ fileContainer,
4007
+ fileWrapper,
4008
+ pathKey,
4009
+ pathKey,
4010
+ async (rid) => {
4011
+ await renderSingleFileEditTile(fileContainer, rid, state, buildDeps());
4012
+ },
4013
+ ctx.instance
4014
+ ).catch((err) => {
4015
+ console.error("Library pick failed:", err);
4016
+ });
4017
+ });
4018
+ libraryBtn.style.cssText = "flex:1;min-width:0;";
4019
+ row.appendChild(uploadCard);
4020
+ row.appendChild(libraryBtn);
4021
+ fileContainer.appendChild(row);
4022
+ } else {
4023
+ handlers.restoreDropzone();
4024
+ }
4025
+ };
3637
4026
  if (initial) {
3638
4027
  handleInitialFileData(
3639
4028
  initial,
@@ -3644,25 +4033,26 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3644
4033
  buildDeps()
3645
4034
  );
3646
4035
  const prefillMeta = state.resourceIndex.get(initial);
3647
- if ((_c = prefillMeta == null ? void 0 : prefillMeta.type) == null ? void 0 : _c.startsWith("video/")) {
4036
+ if ((_e = prefillMeta == null ? void 0 : prefillMeta.type) == null ? void 0 : _e.startsWith("video/")) {
3648
4037
  fileContainer.onclick = handlers.fileUploadHandler;
3649
4038
  setupDragAndDrop(fileContainer, handlers.dragHandler);
3650
4039
  }
3651
4040
  } else {
3652
- handlers.restoreDropzone();
4041
+ renderEmptySingleState();
3653
4042
  }
3654
4043
  picker.onchange = () => {
3655
4044
  if (picker.files && picker.files.length > 0) {
3656
- handleFileSelect(
3657
- picker.files[0],
3658
- fileContainer,
3659
- pathKey,
4045
+ handleFileSelect({
4046
+ file: picker.files[0],
4047
+ container: fileContainer,
4048
+ fieldName: pathKey,
3660
4049
  state,
3661
- buildDeps(),
3662
- ctx.instance,
3663
- allowedExts,
4050
+ deps: buildDeps(),
4051
+ instance: ctx.instance,
4052
+ allowedExtensions: allowedExts,
4053
+ allowedMimes,
3664
4054
  maxSizeMB
3665
- );
4055
+ });
3666
4056
  }
3667
4057
  };
3668
4058
  fileWrapper.appendChild(fileContainer);
@@ -3670,7 +4060,7 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3670
4060
  wrapper.appendChild(fileWrapper);
3671
4061
  }
3672
4062
  function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
3673
- var _a, _b;
4063
+ var _a, _b, _c, _d;
3674
4064
  const state = ctx.state;
3675
4065
  const filesWrapper = document.createElement("div");
3676
4066
  filesWrapper.className = "space-y-2";
@@ -3681,7 +4071,10 @@ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
3681
4071
  filesPicker.multiple = true;
3682
4072
  filesPicker.style.display = "none";
3683
4073
  if (element.accept) {
3684
- filesPicker.accept = typeof element.accept === "string" ? element.accept : ((_a = element.accept.extensions) == null ? void 0 : _a.map((ext) => `.${ext}`).join(",")) || "";
4074
+ filesPicker.accept = typeof element.accept === "string" ? element.accept : [
4075
+ ...(_b = (_a = element.accept.extensions) == null ? void 0 : _a.map((ext) => `.${ext}`)) != null ? _b : [],
4076
+ ...(_c = element.accept.mime) != null ? _c : []
4077
+ ].join(",") || "";
3685
4078
  }
3686
4079
  const filesContainer = document.createElement("div");
3687
4080
  filesContainer.className = "files-list-wrapper";
@@ -3695,30 +4088,44 @@ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
3695
4088
  const filesConstraints = {
3696
4089
  maxCount: Infinity,
3697
4090
  allowedExtensions: getAllowedExtensions(element.accept),
3698
- maxSize: (_b = element.maxSize) != null ? _b : Infinity
4091
+ allowedMimes: getAllowedMimes(element.accept),
4092
+ maxSize: (_d = element.maxSize) != null ? _d : Infinity
3699
4093
  };
3700
4094
  filesContainer.appendChild(list);
3701
4095
  filesWrapper.appendChild(filesPicker);
3702
4096
  filesWrapper.appendChild(filesContainer);
3703
4097
  wrapper.appendChild(filesWrapper);
4098
+ const onLibraryPickFiles = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4099
+ handleLibraryPickMulti(
4100
+ state,
4101
+ element,
4102
+ filesWrapper,
4103
+ pathKey,
4104
+ initialFiles,
4105
+ Infinity,
4106
+ updateFilesList,
4107
+ ctx.instance
4108
+ ).catch((err) => {
4109
+ console.error("Library pick failed:", err);
4110
+ });
4111
+ } : null;
3704
4112
  function updateFilesList() {
3705
4113
  const currentlyReadonly = isElementReadonly(element, state);
3706
- renderResourcePills(
3707
- list,
3708
- initialFiles,
4114
+ renderResourcePills({
4115
+ container: list,
4116
+ rids: initialFiles,
3709
4117
  state,
3710
- currentlyReadonly ? null : (ridToRemove) => {
4118
+ onRemove: currentlyReadonly ? null : (ridToRemove) => {
3711
4119
  var _a2;
3712
4120
  releaseLocalFileUrl((_a2 = state.resourceIndex.get(ridToRemove)) == null ? void 0 : _a2.file);
3713
4121
  const index = initialFiles.indexOf(ridToRemove);
3714
4122
  if (index > -1) initialFiles.splice(index, 1);
3715
4123
  updateFilesList();
3716
4124
  },
3717
- filesFieldHint,
3718
- void 0,
3719
- void 0,
3720
- currentlyReadonly
3721
- );
4125
+ hint: filesFieldHint,
4126
+ isReadonly: currentlyReadonly,
4127
+ onLibraryPick: currentlyReadonly ? null : onLibraryPickFiles
4128
+ });
3722
4129
  }
3723
4130
  updateFilesList();
3724
4131
  setupFilesDropHandler(
@@ -3741,7 +4148,7 @@ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
3741
4148
  );
3742
4149
  }
3743
4150
  function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
3744
- var _a, _b, _c, _d;
4151
+ var _a, _b, _c, _d, _e, _f;
3745
4152
  const state = ctx.state;
3746
4153
  const minFiles = (_a = element.minCount) != null ? _a : 0;
3747
4154
  const maxFiles = (_b = element.maxCount) != null ? _b : Infinity;
@@ -3754,7 +4161,10 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
3754
4161
  filesPicker.multiple = true;
3755
4162
  filesPicker.style.display = "none";
3756
4163
  if (element.accept) {
3757
- filesPicker.accept = typeof element.accept === "string" ? element.accept : ((_c = element.accept.extensions) == null ? void 0 : _c.map((ext) => `.${ext}`).join(",")) || "";
4164
+ filesPicker.accept = typeof element.accept === "string" ? element.accept : [
4165
+ ...(_d = (_c = element.accept.extensions) == null ? void 0 : _c.map((ext) => `.${ext}`)) != null ? _d : [],
4166
+ ...(_e = element.accept.mime) != null ? _e : []
4167
+ ].join(",") || "";
3758
4168
  }
3759
4169
  const filesContainer = document.createElement("div");
3760
4170
  filesContainer.className = "files-list-wrapper";
@@ -3771,30 +4181,46 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
3771
4181
  const multipleConstraints = {
3772
4182
  maxCount: maxFiles,
3773
4183
  allowedExtensions: getAllowedExtensions(element.accept),
3774
- maxSize: (_d = element.maxSize) != null ? _d : Infinity
4184
+ allowedMimes: getAllowedMimes(element.accept),
4185
+ maxSize: (_f = element.maxSize) != null ? _f : Infinity
3775
4186
  };
3776
4187
  const buildCountInfo = () => {
3777
4188
  const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
3778
4189
  const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
3779
4190
  return countText + minMaxText;
3780
4191
  };
4192
+ const onLibraryPickMultiple = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4193
+ handleLibraryPickMulti(
4194
+ state,
4195
+ element,
4196
+ filesWrapper,
4197
+ pathKey,
4198
+ initialFiles,
4199
+ maxFiles,
4200
+ updateFilesDisplay,
4201
+ ctx.instance
4202
+ ).catch((err) => {
4203
+ console.error("Library pick failed:", err);
4204
+ });
4205
+ } : null;
3781
4206
  const updateFilesDisplay = () => {
3782
4207
  const currentlyReadonly = isElementReadonly(element, state);
3783
- renderResourcePills(
3784
- list,
3785
- initialFiles,
4208
+ renderResourcePills({
4209
+ container: list,
4210
+ rids: initialFiles,
3786
4211
  state,
3787
- currentlyReadonly ? null : (index) => {
4212
+ onRemove: currentlyReadonly ? null : (index) => {
3788
4213
  var _a2;
3789
4214
  releaseLocalFileUrl((_a2 = state.resourceIndex.get(index)) == null ? void 0 : _a2.file);
3790
4215
  initialFiles.splice(initialFiles.indexOf(index), 1);
3791
4216
  updateFilesDisplay();
3792
4217
  },
3793
- multipleFilesHint,
3794
- buildCountInfo(),
3795
- maxFiles < Infinity ? maxFiles : void 0,
3796
- currentlyReadonly
3797
- );
4218
+ hint: multipleFilesHint,
4219
+ countInfo: buildCountInfo(),
4220
+ maxCount: maxFiles < Infinity ? maxFiles : void 0,
4221
+ isReadonly: currentlyReadonly,
4222
+ onLibraryPick: currentlyReadonly ? null : onLibraryPickMultiple
4223
+ });
3798
4224
  };
3799
4225
  setupFilesDropHandler(
3800
4226
  filesContainer,
@@ -3852,19 +4278,30 @@ function validateFileCount(key, resourceIds, element, state, errors) {
3852
4278
  errors.push(`${key}: ${t("maxFiles", state, { max: maxFiles })}`);
3853
4279
  }
3854
4280
  }
3855
- function validateFileExtensions(key, resourceIds, element, state, errors) {
3856
- var _a;
4281
+ function validateFileTypes(key, resourceIds, element, state, errors) {
4282
+ var _a, _b;
3857
4283
  const acceptField = "accept" in element ? element.accept : void 0;
3858
4284
  const allowedExtensions = getAllowedExtensions(acceptField);
3859
- if (allowedExtensions.length === 0) return;
4285
+ const allowedMimes = getAllowedMimes(acceptField);
4286
+ if (allowedExtensions.length === 0 && allowedMimes.length === 0) return;
3860
4287
  const formats = allowedExtensions.join(", ");
4288
+ const mimes = allowedMimes.join(", ");
3861
4289
  for (const rid of resourceIds) {
3862
4290
  const meta = state.resourceIndex.get(rid);
3863
4291
  const fileName = (_a = meta == null ? void 0 : meta.name) != null ? _a : rid;
3864
- if (!isFileExtensionAllowed(fileName, allowedExtensions)) {
4292
+ if (allowedExtensions.length > 0 && !isFileExtensionAllowed(fileName, allowedExtensions)) {
3865
4293
  errors.push(
3866
4294
  `${key}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
3867
4295
  );
4296
+ continue;
4297
+ }
4298
+ if (allowedMimes.length > 0 && !(meta == null ? void 0 : meta.inferredFromExtension)) {
4299
+ const mimeType = (_b = meta == null ? void 0 : meta.type) != null ? _b : "";
4300
+ if (!isMimeAllowed(mimeType, allowedMimes)) {
4301
+ errors.push(
4302
+ `${key}: ${t("invalidFileMime", state, { name: fileName, type: mimeType, mimes })}`
4303
+ );
4304
+ }
3868
4305
  }
3869
4306
  }
3870
4307
  }
@@ -3889,7 +4326,7 @@ function validateMultiFile(element, key, context) {
3889
4326
  const resourceIds = readMultiFileResourceIds(scopeRoot, fullKey);
3890
4327
  if (!skipValidation) {
3891
4328
  validateFileCount(key, resourceIds, element, state, errors);
3892
- validateFileExtensions(key, resourceIds, element, state, errors);
4329
+ validateFileTypes(key, resourceIds, element, state, errors);
3893
4330
  validateFileSizes(key, resourceIds, element, state, errors);
3894
4331
  }
3895
4332
  return { value: resourceIds, errors };
@@ -3907,7 +4344,7 @@ function validateSingleFile(element, key, context) {
3907
4344
  return { value: null, errors };
3908
4345
  }
3909
4346
  if (!skipValidation && rid !== "") {
3910
- validateFileExtensions(key, [rid], element, state, errors);
4347
+ validateFileTypes(key, [rid], element, state, errors);
3911
4348
  validateFileSizes(key, [rid], element, state, errors);
3912
4349
  }
3913
4350
  return { value: rid || null, errors };
@@ -4033,9 +4470,7 @@ function updateFileField(element, fieldPath, value, context) {
4033
4470
  }
4034
4471
  value.forEach((resourceId) => {
4035
4472
  if (resourceId && typeof resourceId === "string") {
4036
- if (!state.resourceIndex.has(resourceId)) {
4037
- addResourceToIndex(resourceId, state);
4038
- }
4473
+ seedInferredResource(resourceId, state.resourceIndex);
4039
4474
  }
4040
4475
  });
4041
4476
  const filesWrapper = scopeRoot.querySelector(
@@ -4060,35 +4495,13 @@ function updateFileField(element, fieldPath, value, context) {
4060
4495
  }
4061
4496
  hiddenInput.value = value != null ? String(value) : "";
4062
4497
  if (value && typeof value === "string") {
4063
- if (!state.resourceIndex.has(value)) {
4064
- addResourceToIndex(value, state);
4065
- }
4498
+ seedInferredResource(value, state.resourceIndex);
4066
4499
  console.info(
4067
4500
  `updateFileField: File field "${fieldPath}" updated. Preview update requires re-render.`
4068
4501
  );
4069
4502
  }
4070
4503
  }
4071
4504
  }
4072
- function addResourceToIndex(resourceId, state) {
4073
- var _a;
4074
- const filename = resourceId.split("/").pop() || "file";
4075
- const extension = (_a = filename.split(".").pop()) == null ? void 0 : _a.toLowerCase();
4076
- let fileType = "application/octet-stream";
4077
- if (extension) {
4078
- if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
4079
- fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
4080
- } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
4081
- fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
4082
- }
4083
- }
4084
- state.resourceIndex.set(resourceId, {
4085
- name: filename,
4086
- type: fileType,
4087
- size: 0,
4088
- uploadedAt: /* @__PURE__ */ new Date(),
4089
- file: void 0
4090
- });
4091
- }
4092
4505
 
4093
4506
  // src/components/colour.ts
4094
4507
  function normalizeColourValue(value) {
@@ -8825,6 +9238,7 @@ var defaultConfig = {
8825
9238
  enableFilePreview: true,
8826
9239
  maxPreviewSize: "200px",
8827
9240
  readonly: false,
9241
+ pickExistingFiles: null,
8828
9242
  parseTableFile: null,
8829
9243
  locale: "en",
8830
9244
  translations: {
@@ -8861,6 +9275,10 @@ var defaultConfig = {
8861
9275
  fileCountRange: "({min}-{max})",
8862
9276
  uploadingFile: "Uploading\u2026",
8863
9277
  filesCounter: "{count}/{max}",
9278
+ fromLibrary: "From library",
9279
+ libraryEmpty: "Library is empty",
9280
+ libraryHint: "Choose from previously uploaded files",
9281
+ pickerError: "Failed to load files from library",
8864
9282
  // Validation errors
8865
9283
  required: "Required",
8866
9284
  minItems: "Minimum {min} items required",
@@ -8876,6 +9294,7 @@ var defaultConfig = {
8876
9294
  minFiles: "Minimum {min} files required",
8877
9295
  maxFiles: "Maximum {max} files allowed",
8878
9296
  invalidFileExtension: 'File "{name}" has unsupported format. Allowed: {formats}',
9297
+ invalidFileMime: 'File "{name}": file type {type} not allowed (allowed: {mimes})',
8879
9298
  fileTooLarge: 'File "{name}" exceeds maximum size of {maxSize}MB',
8880
9299
  filesLimitExceeded: "{skipped} file(s) skipped: maximum {max} files allowed",
8881
9300
  unsupportedFieldType: "Unsupported field type: {type}",
@@ -8927,6 +9346,10 @@ var defaultConfig = {
8927
9346
  fileCountRange: "({min}-{max})",
8928
9347
  uploadingFile: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026",
8929
9348
  filesCounter: "{count}/{max}",
9349
+ fromLibrary: "\u0418\u0437 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438",
9350
+ libraryEmpty: "\u0411\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0430 \u043F\u0443\u0441\u0442\u0430",
9351
+ 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",
9352
+ 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",
8930
9353
  // Validation errors
8931
9354
  required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
8932
9355
  minItems: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
@@ -8942,6 +9365,7 @@ var defaultConfig = {
8942
9365
  minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
8943
9366
  maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
8944
9367
  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}',
9368
+ 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})',
8945
9369
  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',
8946
9370
  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",
8947
9371
  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}",