@dmitryvim/form-builder 0.2.26 → 0.2.28

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.
@@ -2119,7 +2119,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
2119
2119
  }
2120
2120
  }
2121
2121
 
2122
- // src/components/file.ts
2122
+ // src/components/file/constraints.ts
2123
2123
  function getAllowedExtensions(accept) {
2124
2124
  if (!accept) return [];
2125
2125
  if (typeof accept === "object" && Array.isArray(accept.extensions)) {
@@ -2131,18 +2131,775 @@ function getAllowedExtensions(accept) {
2131
2131
  return [];
2132
2132
  }
2133
2133
  function isFileExtensionAllowed(fileName, allowedExtensions) {
2134
- var _a;
2134
+ var _a, _b;
2135
2135
  if (allowedExtensions.length === 0) return true;
2136
- const ext = ((_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase()) || "";
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
+ }
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) {
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) {
2190
+ for (const resourceId of initialFiles) {
2191
+ seedInferredResource(resourceId, resourceIndex);
2192
+ }
2193
+ }
2194
+
2195
+ // src/components/file/styles.ts
2196
+ var STYLE_ID = "fb-file-styles";
2197
+ function ensureFileStyles() {
2198
+ if (typeof document === "undefined") return;
2199
+ if (document.getElementById(STYLE_ID)) return;
2200
+ const style = document.createElement("style");
2201
+ style.id = STYLE_ID;
2202
+ style.setAttribute("data-fb-file-styles", "true");
2203
+ style.textContent = `
2204
+ @keyframes fb-spin { to { transform: rotate(360deg); } }
2205
+
2206
+ /* Spinner used during single-file and multi-file upload */
2207
+ .fb-spinner {
2208
+ width: 36px;
2209
+ height: 36px;
2210
+ border: 3px solid rgba(0,0,0,0.12);
2211
+ border-top-color: var(--fb-text-secondary-color, #6b7280);
2212
+ border-radius: 50%;
2213
+ animation: fb-spin 0.7s linear infinite;
2214
+ flex-shrink: 0;
2215
+ }
2216
+
2217
+ /* Base tile: fixed 160\xD7160 square, theme-aware background */
2218
+ .fb-tile {
2219
+ width: var(--fb-tile-size, 160px);
2220
+ height: var(--fb-tile-size, 160px);
2221
+ flex-shrink: 0;
2222
+ position: relative;
2223
+ overflow: hidden;
2224
+ border-radius: var(--fb-border-radius, 0.5rem);
2225
+ background: var(--fb-file-upload-bg-color, #f3f4f6);
2226
+ }
2227
+
2228
+ /* Uploaded resource tile \u2014 adds a visible border */
2229
+ .fb-tile-resource {
2230
+ border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
2231
+ }
2232
+
2233
+ /* Uploading placeholder tile \u2014 dashed border, uploading indicator */
2234
+ .fb-tile-uploading {
2235
+ border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2236
+ }
2237
+
2238
+ /* "+" add-more tile */
2239
+ .fb-tile-add {
2240
+ border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2241
+ display: flex;
2242
+ align-items: center;
2243
+ justify-content: center;
2244
+ cursor: pointer;
2245
+ font-size: 32px;
2246
+ color: var(--fb-file-upload-text-color, #9ca3af);
2247
+ transition:
2248
+ border-color var(--fb-transition-duration, 200ms),
2249
+ color var(--fb-transition-duration, 200ms);
2250
+ }
2251
+ .fb-tile-add:hover {
2252
+ border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2253
+ color: var(--fb-text-color, #1f2937);
2254
+ }
2255
+
2256
+ /* Count chip shown when at maxCount */
2257
+ .fb-tile-counter {
2258
+ font-size: 11px;
2259
+ color: var(--fb-text-secondary-color, #6b7280);
2260
+ background: var(--fb-file-upload-bg-color, #f3f4f6);
2261
+ border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
2262
+ border-radius: 4px;
2263
+ padding: 2px 6px;
2264
+ align-self: flex-end;
2265
+ margin-bottom: 4px;
2266
+ }
2267
+
2268
+ /* Empty-state dropzone */
2269
+ .fb-file-dropzone {
2270
+ width: 100%;
2271
+ height: 128px;
2272
+ border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2273
+ border-radius: var(--fb-border-radius, 0.5rem);
2274
+ display: flex;
2275
+ flex-direction: column;
2276
+ align-items: center;
2277
+ justify-content: center;
2278
+ gap: 4px;
2279
+ cursor: pointer;
2280
+ transition:
2281
+ border-color var(--fb-transition-duration, 200ms),
2282
+ background var(--fb-transition-duration, 200ms);
2283
+ }
2284
+ .fb-file-dropzone:hover {
2285
+ border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2286
+ background: var(--fb-background-hover-color, #f9fafb);
2287
+ }
2288
+
2289
+ /* Inline text inside tiles */
2290
+ .fb-tile-label {
2291
+ font-size: 9px;
2292
+ color: var(--fb-text-secondary-color, #6b7280);
2293
+ text-align: center;
2294
+ overflow: hidden;
2295
+ word-break: break-all;
2296
+ max-height: 28px;
2297
+ }
2298
+ .fb-tile-uploading-text {
2299
+ font-size: 8px;
2300
+ color: var(--fb-file-upload-text-color, #9ca3af);
2301
+ }
2302
+ .fb-tile-hint {
2303
+ font-size: 11px;
2304
+ color: var(--fb-file-upload-text-color, #9ca3af);
2305
+ margin-top: 4px;
2306
+ }
2307
+ .fb-tile-empty-text {
2308
+ font-size: 12px;
2309
+ color: var(--fb-text-secondary-color, #6b7280);
2310
+ padding: 4px 0;
2311
+ }
2312
+ .fb-dropzone-primary-text {
2313
+ font-size: 13px;
2314
+ color: var(--fb-text-secondary-color, #6b7280);
2315
+ }
2316
+ .fb-dropzone-hint-text {
2317
+ font-size: 11px;
2318
+ color: var(--fb-file-upload-text-color, #9ca3af);
2319
+ }
2320
+
2321
+ /* Hover overlay + X-button on resource tiles */
2322
+ .fb-tile-overlay {
2323
+ position: absolute;
2324
+ inset: 0;
2325
+ background: transparent;
2326
+ transition: background var(--fb-transition-duration, 200ms);
2327
+ display: flex;
2328
+ align-items: flex-start;
2329
+ justify-content: flex-end;
2330
+ }
2331
+ .fb-tile-resource:hover .fb-tile-overlay {
2332
+ background: var(--fb-tile-hover-overlay-color, rgba(0,0,0,0.4));
2333
+ }
2334
+ .fb-tile-x-btn {
2335
+ margin: 3px;
2336
+ width: 18px;
2337
+ height: 18px;
2338
+ background: var(--fb-error-color, #ef4444);
2339
+ color: var(--fb-file-bg-color, #fff);
2340
+ border: none;
2341
+ border-radius: 50%;
2342
+ font-size: 11px;
2343
+ line-height: 1;
2344
+ cursor: pointer;
2345
+ display: flex;
2346
+ align-items: center;
2347
+ justify-content: center;
2348
+ opacity: 0;
2349
+ transition: opacity var(--fb-transition-duration, 200ms);
2350
+ }
2351
+ .fb-tile-resource:hover .fb-tile-x-btn {
2352
+ opacity: 1;
2353
+ }
2354
+
2355
+ /* Video play button overlay (readonly tiles with video thumbnails) */
2356
+ .fb-video-overlay {
2357
+ position: absolute;
2358
+ inset: 0;
2359
+ display: flex;
2360
+ align-items: center;
2361
+ justify-content: center;
2362
+ background: var(--fb-tile-hover-overlay-color, rgba(0,0,0,0.25));
2363
+ }
2364
+ .fb-play-btn {
2365
+ background: var(--fb-file-bg-color, rgba(255,255,255,0.9));
2366
+ border-radius: 50%;
2367
+ display: flex;
2368
+ align-items: center;
2369
+ justify-content: center;
2370
+ }
2371
+
2372
+ /* Edit-mode local video preview wrapper */
2373
+ .fb-video-preview-wrap {
2374
+ position: relative;
2375
+ width: 100%;
2376
+ height: 100%;
2377
+ }
2378
+
2379
+ /* Hover overlay for edit-mode local video (Remove / Change buttons) */
2380
+ .fb-video-btn-overlay {
2381
+ position: absolute;
2382
+ top: 8px;
2383
+ right: 8px;
2384
+ z-index: 10;
2385
+ display: flex;
2386
+ gap: 4px;
2387
+ opacity: 0;
2388
+ transition: opacity var(--fb-transition-duration, 200ms);
2389
+ pointer-events: none;
2390
+ }
2391
+ .fb-video-preview-wrap:hover .fb-video-btn-overlay {
2392
+ opacity: 1;
2393
+ pointer-events: auto;
2394
+ }
2395
+ .fb-video-btn {
2396
+ border: none;
2397
+ border-radius: var(--fb-border-radius, 4px);
2398
+ font-size: 11px;
2399
+ padding: 4px 8px;
2400
+ cursor: pointer;
2401
+ color: #fff;
2402
+ line-height: 1.2;
2403
+ }
2404
+ .fb-video-btn-delete {
2405
+ background: rgba(220, 38, 38, 0.85);
2406
+ }
2407
+ .fb-video-btn-delete:hover {
2408
+ background: rgba(185, 28, 28, 0.95);
2409
+ }
2410
+ .fb-video-btn-change {
2411
+ background: rgba(31, 41, 55, 0.85);
2412
+ }
2413
+ .fb-video-btn-change:hover {
2414
+ background: rgba(17, 24, 39, 0.95);
2415
+ }
2416
+
2417
+ /* Tile action icon buttons (download / open / remove) \u2014 shown on tile hover */
2418
+ .fb-tile-actions {
2419
+ position: absolute;
2420
+ top: 3px;
2421
+ right: 3px;
2422
+ display: flex;
2423
+ flex-direction: row;
2424
+ gap: 3px;
2425
+ opacity: 0;
2426
+ transition: opacity var(--fb-transition-duration, 200ms);
2427
+ z-index: 10;
2428
+ }
2429
+ .fb-tile-resource:hover .fb-tile-actions {
2430
+ opacity: 1;
2431
+ }
2432
+ .fb-tile-action-btn {
2433
+ width: 28px;
2434
+ height: 28px;
2435
+ display: flex;
2436
+ align-items: center;
2437
+ justify-content: center;
2438
+ border: none;
2439
+ border-radius: 50%;
2440
+ cursor: pointer;
2441
+ background: rgba(31, 41, 55, 0.75);
2442
+ color: #fff;
2443
+ padding: 0;
2444
+ flex-shrink: 0;
2445
+ transition:
2446
+ background var(--fb-transition-duration, 200ms),
2447
+ opacity var(--fb-transition-duration, 200ms);
2448
+ }
2449
+ .fb-tile-action-btn:hover {
2450
+ background: rgba(17, 24, 39, 0.95);
2451
+ }
2452
+ .fb-tile-action-remove {
2453
+ background: rgba(220, 38, 38, 0.8);
2454
+ }
2455
+ .fb-tile-action-remove:hover {
2456
+ background: rgba(185, 28, 28, 0.95);
2457
+ }
2458
+
2459
+ /* Actions row inside zoom popup \u2014 always visible while popup is shown */
2460
+ .fb-tile-zoom-preview .fb-tile-actions {
2461
+ position: absolute;
2462
+ top: 6px;
2463
+ right: 6px;
2464
+ opacity: 1;
2465
+ z-index: 10000;
2466
+ }
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
+
2546
+ /* Hover zoom preview popup for image tiles \u2014 appended to document.body (fixed) */
2547
+ .fb-tile-zoom-preview {
2548
+ position: fixed;
2549
+ z-index: 9999;
2550
+ background: var(--fb-background-color, #fff);
2551
+ border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
2552
+ border-radius: var(--fb-border-radius, 0.5rem);
2553
+ box-shadow: 0 4px 16px rgba(0,0,0,0.18);
2554
+ padding: 4px;
2555
+ width: 350px;
2556
+ height: 350px;
2557
+ pointer-events: none;
2558
+ opacity: 0;
2559
+ transition: opacity 150ms ease;
2560
+ }
2561
+ .fb-tile-zoom-preview.fb-tile-zoom-preview--visible {
2562
+ opacity: 1;
2563
+ }
2564
+ .fb-tile-zoom-preview-img {
2565
+ width: 100%;
2566
+ height: 100%;
2567
+ object-fit: contain;
2568
+ display: block;
2569
+ background: var(--fb-file-upload-bg-color, #f3f4f6);
2570
+ border-radius: calc(var(--fb-border-radius, 0.5rem) - 2px);
2571
+ }
2572
+ `;
2573
+ document.head.appendChild(style);
2574
+ }
2575
+
2576
+ // src/components/file/dom.ts
2577
+ var TILE_SIZE = "160px";
2578
+ function createFileTile() {
2579
+ ensureFileStyles();
2580
+ const tile = document.createElement("div");
2581
+ tile.className = "fb-tile";
2582
+ return tile;
2583
+ }
2584
+ function showFileError(container, message) {
2585
+ var _a, _b;
2586
+ const existing = (_a = container.closest(".space-y-2")) == null ? void 0 : _a.querySelector(".file-error-message");
2587
+ if (existing) existing.remove();
2588
+ const errorEl = document.createElement("div");
2589
+ errorEl.className = "file-error-message error-message";
2590
+ errorEl.style.cssText = `
2591
+ color: var(--fb-error-color);
2592
+ font-size: var(--fb-font-size-small);
2593
+ margin-top: 0.25rem;
2594
+ `;
2595
+ errorEl.textContent = message;
2596
+ (_b = container.closest(".space-y-2")) == null ? void 0 : _b.appendChild(errorEl);
2597
+ }
2598
+ function clearFileError(container) {
2599
+ var _a;
2600
+ const existing = (_a = container.closest(".space-y-2")) == null ? void 0 : _a.querySelector(".file-error-message");
2601
+ if (existing) existing.remove();
2602
+ }
2603
+ function addDeleteButton(container, state, onDelete) {
2604
+ const existingOverlay = container.querySelector(".delete-overlay");
2605
+ if (existingOverlay) existingOverlay.remove();
2606
+ const overlay = document.createElement("div");
2607
+ overlay.className = "delete-overlay absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center";
2608
+ const deleteBtn = document.createElement("button");
2609
+ deleteBtn.className = "bg-red-600 text-white px-3 py-1 rounded text-sm hover:bg-red-700 transition-colors";
2610
+ deleteBtn.textContent = t("removeElement", state);
2611
+ deleteBtn.onclick = (e) => {
2612
+ e.stopPropagation();
2613
+ onDelete();
2614
+ };
2615
+ overlay.appendChild(deleteBtn);
2616
+ container.appendChild(overlay);
2617
+ }
2618
+ function findFilePicker(container) {
2619
+ var _a;
2620
+ let el = container.parentElement;
2621
+ while (el && !el.dataset.filesWrapper) {
2622
+ el = el.parentElement;
2623
+ }
2624
+ return (_a = el == null ? void 0 : el.querySelector('input[type="file"]')) != null ? _a : null;
2625
+ }
2626
+ function createUploadingTile(fileName, state) {
2627
+ ensureFileStyles();
2628
+ const tile = createFileTile();
2629
+ tile.classList.add("fb-tile-uploading");
2630
+ tile.className += " fb-uploading-tile";
2631
+ const label = fileName.length > 10 ? fileName.substring(0, 8) + "\u2026" : fileName;
2632
+ tile.innerHTML = `
2633
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:6px;padding:4px;">
2634
+ <div class="fb-spinner"></div>
2635
+ <div class="fb-tile-label">${escapeHtml(label)}</div>
2636
+ <div class="fb-tile-uploading-text">${escapeHtml(t("uploadingFile", state))}</div>
2637
+ </div>`;
2638
+ return tile;
2639
+ }
2640
+ function ensureTilesWrap(list) {
2641
+ const existing = list.querySelector(".fb-tiles-wrap");
2642
+ if (existing) return existing;
2643
+ const dropzone = list.querySelector(".fb-file-dropzone");
2644
+ if (dropzone) dropzone.remove();
2645
+ const tilesWrap = document.createElement("div");
2646
+ tilesWrap.className = "fb-tiles-wrap";
2647
+ tilesWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start;";
2648
+ const addTile = document.createElement("div");
2649
+ addTile.className = "fb-tile fb-tile-add";
2650
+ addTile.innerHTML = "+";
2651
+ tilesWrap.appendChild(addTile);
2652
+ list.appendChild(tilesWrap);
2653
+ return tilesWrap;
2654
+ }
2655
+ function setEmptyFileContainer(fileContainer, state, hint) {
2656
+ const hintHtml = hint ? `<div class="text-xs text-gray-500 mt-1">${escapeHtml(hint)}</div>` : "";
2657
+ fileContainer.innerHTML = `
2658
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
2659
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
2660
+ <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
2661
+ </svg>
2662
+ <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
2663
+ ${hintHtml}
2664
+ </div>
2665
+ `;
2666
+ }
2667
+ function setupDragAndDrop(element, dropHandler) {
2668
+ element.addEventListener("dragover", (e) => {
2669
+ e.preventDefault();
2670
+ element.classList.add("border-blue-500", "bg-blue-50");
2671
+ });
2672
+ element.addEventListener("dragleave", (e) => {
2673
+ e.preventDefault();
2674
+ element.classList.remove("border-blue-500", "bg-blue-50");
2675
+ });
2676
+ element.addEventListener("drop", (e) => {
2677
+ var _a;
2678
+ e.preventDefault();
2679
+ element.classList.remove("border-blue-500", "bg-blue-50");
2680
+ if ((_a = e.dataTransfer) == null ? void 0 : _a.files) {
2681
+ dropHandler(e.dataTransfer.files);
2682
+ }
2683
+ });
2684
+ }
2685
+
2686
+ // src/components/file/preview.ts
2687
+ var ICON_DOWNLOAD = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
2688
+ var ICON_OPEN = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>`;
2689
+ var ICON_REMOVE = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/></svg>`;
2690
+ function canDownload(state, meta) {
2691
+ return Boolean(
2692
+ state.config.downloadFile || state.config.getDownloadUrl || state.config.getThumbnail || (meta == null ? void 0 : meta.file)
2693
+ );
2694
+ }
2695
+ function canOpenInTab(state, meta) {
2696
+ return Boolean(
2697
+ state.config.getDownloadUrl || state.config.getThumbnail || (meta == null ? void 0 : meta.file)
2698
+ );
2699
+ }
2700
+ function createTileActions(options) {
2701
+ const { canRemove, removeHandler, state, resourceId, fileName, meta } = options;
2702
+ const group = document.createElement("div");
2703
+ group.className = "fb-tile-actions";
2704
+ const makeBtn = (icon, label, cls) => {
2705
+ const btn = document.createElement("button");
2706
+ btn.type = "button";
2707
+ btn.className = `fb-tile-action-btn ${cls}`;
2708
+ btn.innerHTML = icon;
2709
+ btn.title = label;
2710
+ btn.setAttribute("aria-label", label);
2711
+ btn.addEventListener("click", (e) => {
2712
+ e.stopPropagation();
2713
+ });
2714
+ return btn;
2715
+ };
2716
+ if (canDownload(state, meta)) {
2717
+ const dlBtn = makeBtn(ICON_DOWNLOAD, t("downloadFile", state), "fb-tile-action-download");
2718
+ dlBtn.addEventListener("click", () => {
2719
+ triggerTileDownload(resourceId, fileName, state, meta);
2720
+ });
2721
+ group.appendChild(dlBtn);
2722
+ }
2723
+ if (canOpenInTab(state, meta)) {
2724
+ const openBtn = makeBtn(ICON_OPEN, t("openInNewTab", state), "fb-tile-action-open");
2725
+ openBtn.addEventListener("click", () => {
2726
+ triggerTileOpen(resourceId, state, meta).catch((err) => {
2727
+ console.error("Open failed:", err);
2728
+ });
2729
+ });
2730
+ group.appendChild(openBtn);
2731
+ }
2732
+ if (canRemove && removeHandler) {
2733
+ const rmBtn = makeBtn(ICON_REMOVE, t("removeElement", state), "fb-tile-action-remove");
2734
+ rmBtn.addEventListener("click", () => {
2735
+ removeHandler();
2736
+ });
2737
+ group.appendChild(rmBtn);
2738
+ }
2739
+ return group;
2740
+ }
2741
+ var localFileUrlCache = /* @__PURE__ */ new WeakMap();
2742
+ function getLocalFileUrl(file) {
2743
+ let url = localFileUrlCache.get(file);
2744
+ if (!url) {
2745
+ url = URL.createObjectURL(file);
2746
+ localFileUrlCache.set(file, url);
2747
+ }
2748
+ return url;
2749
+ }
2750
+ function releaseLocalFileUrl(file) {
2751
+ if (!file) return;
2752
+ const url = localFileUrlCache.get(file);
2753
+ if (url) {
2754
+ URL.revokeObjectURL(url);
2755
+ localFileUrlCache.delete(file);
2756
+ }
2757
+ }
2758
+ function triggerTileDownload(resourceId, fileName, state, meta) {
2759
+ if (state.config.downloadFile) {
2760
+ state.config.downloadFile(resourceId, fileName);
2761
+ return;
2762
+ }
2763
+ if ((meta == null ? void 0 : meta.file) instanceof File) {
2764
+ downloadBlob(meta.file, fileName || meta.file.name);
2765
+ return;
2766
+ }
2767
+ forceDownload(resourceId, fileName, state).catch((err) => {
2768
+ console.error("Download failed:", err);
2769
+ });
2770
+ }
2771
+ async function triggerTileOpen(resourceId, state, meta) {
2772
+ let url = null;
2773
+ if (state.config.getDownloadUrl) {
2774
+ url = state.config.getDownloadUrl(resourceId);
2775
+ } else if (state.config.getThumbnail) {
2776
+ url = await state.config.getThumbnail(resourceId);
2777
+ } else if ((meta == null ? void 0 : meta.file) instanceof File) {
2778
+ url = getLocalFileUrl(meta.file);
2779
+ }
2780
+ if (url) {
2781
+ window.open(url, "_blank");
2782
+ }
2783
+ }
2784
+ var sharedZoomPopup = null;
2785
+ var zoomTimer = null;
2786
+ var zoomHideTimer = null;
2787
+ var zoomOwner = null;
2788
+ function getOrCreateZoomPopup() {
2789
+ if (!sharedZoomPopup) {
2790
+ sharedZoomPopup = document.createElement("div");
2791
+ sharedZoomPopup.className = "fb-tile-zoom-preview";
2792
+ const img = document.createElement("img");
2793
+ img.className = "fb-tile-zoom-preview-img";
2794
+ sharedZoomPopup.appendChild(img);
2795
+ sharedZoomPopup.addEventListener("mouseenter", cancelHideZoomPopup);
2796
+ sharedZoomPopup.addEventListener("mouseleave", scheduleHideZoomPopup);
2797
+ }
2798
+ return sharedZoomPopup;
2799
+ }
2800
+ function positionZoomPopup(popup, tile) {
2801
+ const tileRect = tile.getBoundingClientRect();
2802
+ const popupSize = 350;
2803
+ const margin = 6;
2804
+ const padding = 8;
2805
+ let top;
2806
+ if (tileRect.top - popupSize - margin >= padding) {
2807
+ top = tileRect.top - popupSize - margin;
2808
+ } else if (tileRect.bottom + margin + popupSize + padding <= window.innerHeight) {
2809
+ top = tileRect.bottom + margin;
2810
+ } else {
2811
+ top = Math.max(padding, Math.min(window.innerHeight - popupSize - padding, tileRect.top));
2812
+ }
2813
+ const tileCenterX = tileRect.left + tileRect.width / 2;
2814
+ let left = tileCenterX - popupSize / 2;
2815
+ left = Math.max(padding, Math.min(window.innerWidth - popupSize - padding, left));
2816
+ popup.style.top = `${top}px`;
2817
+ popup.style.left = `${left}px`;
2818
+ }
2819
+ function scheduleHideZoomPopup() {
2820
+ if (zoomHideTimer !== null) {
2821
+ clearTimeout(zoomHideTimer);
2822
+ }
2823
+ zoomHideTimer = setTimeout(() => {
2824
+ zoomHideTimer = null;
2825
+ removeZoomPopupNow();
2826
+ }, 100);
2827
+ }
2828
+ function cancelHideZoomPopup() {
2829
+ if (zoomHideTimer !== null) {
2830
+ clearTimeout(zoomHideTimer);
2831
+ zoomHideTimer = null;
2832
+ }
2833
+ }
2834
+ function removeZoomPopupNow() {
2835
+ if (zoomTimer !== null) {
2836
+ clearTimeout(zoomTimer);
2837
+ zoomTimer = null;
2838
+ }
2839
+ if (sharedZoomPopup && sharedZoomPopup.parentNode) {
2840
+ sharedZoomPopup.classList.remove("fb-tile-zoom-preview--visible");
2841
+ sharedZoomPopup.parentNode.removeChild(sharedZoomPopup);
2842
+ }
2843
+ zoomOwner = null;
2844
+ }
2845
+ function attachZoomHover(tile, src, alt, actionsEl) {
2846
+ tile.dataset.zoomSrc = src;
2847
+ tile.dataset.zoomAlt = alt;
2848
+ tile.addEventListener("mouseenter", () => {
2849
+ cancelHideZoomPopup();
2850
+ if (zoomOwner !== tile) {
2851
+ removeZoomPopupNow();
2852
+ }
2853
+ zoomOwner = tile;
2854
+ zoomTimer = setTimeout(() => {
2855
+ zoomTimer = null;
2856
+ const popup = getOrCreateZoomPopup();
2857
+ const existingActions = popup.querySelector(".fb-tile-actions");
2858
+ if (existingActions) existingActions.remove();
2859
+ const img = popup.querySelector(".fb-tile-zoom-preview-img");
2860
+ img.src = src;
2861
+ img.alt = alt;
2862
+ if (actionsEl) {
2863
+ popup.appendChild(actionsEl.cloneNode(true));
2864
+ attachClonedActionListeners(
2865
+ popup.querySelector(".fb-tile-actions"),
2866
+ actionsEl
2867
+ );
2868
+ }
2869
+ popup.style.pointerEvents = "auto";
2870
+ positionZoomPopup(popup, tile);
2871
+ document.body.appendChild(popup);
2872
+ popup.getBoundingClientRect();
2873
+ popup.classList.add("fb-tile-zoom-preview--visible");
2874
+ }, 200);
2875
+ });
2876
+ tile.addEventListener("mouseleave", () => {
2877
+ if (zoomTimer !== null) {
2878
+ clearTimeout(zoomTimer);
2879
+ zoomTimer = null;
2880
+ zoomOwner = null;
2881
+ } else {
2882
+ scheduleHideZoomPopup();
2883
+ }
2884
+ });
2885
+ }
2886
+ function attachClonedActionListeners(cloned, original) {
2887
+ const originalBtns = Array.from(original.querySelectorAll(".fb-tile-action-btn"));
2888
+ const clonedBtns = Array.from(cloned.querySelectorAll(".fb-tile-action-btn"));
2889
+ clonedBtns.forEach((clonedBtn, i) => {
2890
+ const origBtn = originalBtns[i];
2891
+ if (origBtn) {
2892
+ clonedBtn.addEventListener("click", (e) => {
2893
+ e.stopPropagation();
2894
+ origBtn.click();
2895
+ });
2896
+ }
2897
+ });
2142
2898
  }
2143
2899
  function renderLocalImagePreview(container, file, fileName, state) {
2144
2900
  const img = document.createElement("img");
2145
2901
  img.className = "w-full h-full object-contain";
2902
+ img.style.background = "var(--fb-file-upload-bg-color,#f3f4f6)";
2146
2903
  img.alt = fileName || t("previewAlt", state);
2147
2904
  const reader = new FileReader();
2148
2905
  reader.onload = (e) => {
@@ -2152,23 +2909,27 @@ function renderLocalImagePreview(container, file, fileName, state) {
2152
2909
  reader.readAsDataURL(file);
2153
2910
  container.appendChild(img);
2154
2911
  }
2155
- function renderLocalVideoPreview(container, file, videoType, resourceId, state, deps) {
2156
- const videoUrl = URL.createObjectURL(file);
2912
+ function setupDragDropless(container, _deps) {
2157
2913
  container.onclick = null;
2158
2914
  const newContainer = container.cloneNode(false);
2159
2915
  if (container.parentNode) {
2160
2916
  container.parentNode.replaceChild(newContainer, container);
2161
2917
  }
2918
+ return newContainer;
2919
+ }
2920
+ function renderLocalVideoPreview(container, file, videoType, resourceId, state, deps) {
2921
+ const videoUrl = URL.createObjectURL(file);
2922
+ const newContainer = setupDragDropless(container);
2162
2923
  newContainer.innerHTML = `
2163
- <div class="relative group h-full">
2924
+ <div class="fb-video-preview-wrap">
2164
2925
  <video class="w-full h-full object-contain" controls preload="auto" muted src="${videoUrl}">
2165
2926
  ${escapeHtml(t("videoNotSupported", state))}
2166
2927
  </video>
2167
- <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
2168
- <button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
2928
+ <div class="fb-video-btn-overlay">
2929
+ <button class="fb-video-btn fb-video-btn-delete delete-file-btn">
2169
2930
  ${escapeHtml(t("removeElement", state))}
2170
2931
  </button>
2171
- <button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
2932
+ <button class="fb-video-btn fb-video-btn-change change-file-btn">
2172
2933
  ${escapeHtml(t("changeButton", state))}
2173
2934
  </button>
2174
2935
  </div>
@@ -2178,20 +2939,15 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
2178
2939
  return newContainer;
2179
2940
  }
2180
2941
  function attachVideoButtonHandlers(container, resourceId, state, deps) {
2181
- const changeBtn = container.querySelector(
2182
- ".change-file-btn"
2183
- );
2942
+ const changeBtn = container.querySelector(".change-file-btn");
2184
2943
  if (changeBtn) {
2185
2944
  changeBtn.onclick = (e) => {
2945
+ var _a;
2186
2946
  e.stopPropagation();
2187
- if (deps == null ? void 0 : deps.picker) {
2188
- deps.picker.click();
2189
- }
2947
+ (_a = deps == null ? void 0 : deps.picker) == null ? void 0 : _a.click();
2190
2948
  };
2191
2949
  }
2192
- const deleteBtn = container.querySelector(
2193
- ".delete-file-btn"
2194
- );
2950
+ const deleteBtn = container.querySelector(".delete-file-btn");
2195
2951
  if (deleteBtn) {
2196
2952
  deleteBtn.onclick = (e) => {
2197
2953
  e.stopPropagation();
@@ -2211,9 +2967,6 @@ function handleVideoDelete(container, resourceId, state, deps) {
2211
2967
  if (deps == null ? void 0 : deps.fileUploadHandler) {
2212
2968
  container.onclick = deps.fileUploadHandler;
2213
2969
  }
2214
- if (deps == null ? void 0 : deps.dragHandler) {
2215
- setupDragAndDrop(container, deps.dragHandler);
2216
- }
2217
2970
  container.innerHTML = `
2218
2971
  <div class="flex flex-col items-center justify-center h-full text-gray-400">
2219
2972
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
@@ -2222,16 +2975,9 @@ function handleVideoDelete(container, resourceId, state, deps) {
2222
2975
  <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
2223
2976
  </div>
2224
2977
  `;
2225
- }
2226
- function renderUploadedVideoPreview(container, thumbnailUrl, _videoType, state) {
2227
- const video = document.createElement("video");
2228
- video.className = "w-full h-full object-contain";
2229
- video.controls = true;
2230
- video.preload = "metadata";
2231
- video.muted = true;
2232
- video.src = thumbnailUrl;
2233
- video.appendChild(document.createTextNode(t("videoNotSupported", state)));
2234
- container.appendChild(video);
2978
+ if (deps == null ? void 0 : deps.setupDrop) {
2979
+ deps.setupDrop(container);
2980
+ }
2235
2981
  }
2236
2982
  function renderDeleteButton(container, resourceId, state) {
2237
2983
  addDeleteButton(container, state, () => {
@@ -2254,13 +3000,12 @@ function renderDeleteButton(container, resourceId, state) {
2254
3000
  });
2255
3001
  }
2256
3002
  async function renderLocalFilePreview(container, meta, fileName, resourceId, isReadonly, state, deps) {
2257
- if (!meta.file || !(meta.file instanceof File)) {
2258
- return;
2259
- }
2260
- if (meta.type && meta.type.startsWith("image/")) {
3003
+ var _a, _b, _c;
3004
+ if (!meta.file || !(meta.file instanceof File)) return;
3005
+ if ((_a = meta.type) == null ? void 0 : _a.startsWith("image/")) {
2261
3006
  renderLocalImagePreview(container, meta.file, fileName, state);
2262
- } else if (meta.type && meta.type.startsWith("video/")) {
2263
- const newContainer = renderLocalVideoPreview(
3007
+ } else if ((_b = meta.type) == null ? void 0 : _b.startsWith("video/")) {
3008
+ container = renderLocalVideoPreview(
2264
3009
  container,
2265
3010
  meta.file,
2266
3011
  meta.type,
@@ -2268,15 +3013,25 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
2268
3013
  state,
2269
3014
  deps
2270
3015
  );
2271
- container = newContainer;
2272
3016
  } else {
2273
- container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">\u{1F4C1}</div><div class="text-sm">${escapeHtml(fileName)}</div></div>`;
3017
+ container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div style="font-size:36px;" class="mb-2">\u{1F4C1}</div><div class="text-sm">${escapeHtml(fileName)}</div></div>`;
2274
3018
  }
2275
- if (!isReadonly && !(meta.type && meta.type.startsWith("video/"))) {
3019
+ if (!isReadonly && !((_c = meta.type) == null ? void 0 : _c.startsWith("video/"))) {
2276
3020
  renderDeleteButton(container, resourceId, state);
2277
3021
  }
2278
3022
  }
3023
+ function renderUploadedVideoPreview(container, thumbnailUrl, state) {
3024
+ const video = document.createElement("video");
3025
+ video.className = "w-full h-full object-contain";
3026
+ video.controls = true;
3027
+ video.preload = "metadata";
3028
+ video.muted = true;
3029
+ video.src = thumbnailUrl;
3030
+ video.appendChild(document.createTextNode(t("videoNotSupported", state)));
3031
+ container.appendChild(video);
3032
+ }
2279
3033
  async function renderUploadedFilePreview(container, resourceId, fileName, meta, state) {
3034
+ var _a;
2280
3035
  if (!state.config.getThumbnail) {
2281
3036
  setEmptyFileContainer(container, state);
2282
3037
  return;
@@ -2285,11 +3040,12 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
2285
3040
  const thumbnailUrl = await state.config.getThumbnail(resourceId);
2286
3041
  if (thumbnailUrl) {
2287
3042
  clear(container);
2288
- if (meta && meta.type && meta.type.startsWith("video/")) {
2289
- renderUploadedVideoPreview(container, thumbnailUrl, meta.type, state);
3043
+ if ((_a = meta == null ? void 0 : meta.type) == null ? void 0 : _a.startsWith("video/")) {
3044
+ renderUploadedVideoPreview(container, thumbnailUrl, state);
2290
3045
  } else {
2291
3046
  const img = document.createElement("img");
2292
3047
  img.className = "w-full h-full object-contain";
3048
+ img.style.background = "var(--fb-file-upload-bg-color,#f3f4f6)";
2293
3049
  img.alt = fileName || t("previewAlt", state);
2294
3050
  img.src = thumbnailUrl;
2295
3051
  container.appendChild(img);
@@ -2318,9 +3074,6 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
2318
3074
  );
2319
3075
  }
2320
3076
  clear(container);
2321
- if (isReadonly) {
2322
- container.classList.add("cursor-pointer");
2323
- }
2324
3077
  const meta = state.resourceIndex.get(resourceId);
2325
3078
  if (meta && meta.file && meta.file instanceof File) {
2326
3079
  await renderLocalFilePreview(
@@ -2333,372 +3086,310 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
2333
3086
  deps
2334
3087
  );
2335
3088
  } else {
2336
- await renderUploadedFilePreview(
2337
- container,
2338
- resourceId,
2339
- fileName,
2340
- meta,
2341
- state
2342
- );
3089
+ await renderUploadedFilePreview(container, resourceId, fileName, meta, state);
2343
3090
  const isVideo = (_a = meta == null ? void 0 : meta.type) == null ? void 0 : _a.startsWith("video/");
2344
3091
  if (!isReadonly && !isVideo) {
2345
3092
  renderDeleteButton(container, resourceId, state);
2346
3093
  }
2347
3094
  }
2348
3095
  }
2349
- async function renderFilePreviewReadonly(resourceId, state, fileName) {
3096
+ function resolveFileName(resourceId, meta, fileName) {
3097
+ var _a;
3098
+ if (fileName) return fileName;
3099
+ if ((_a = meta == null ? void 0 : meta.name) == null ? void 0 : _a.includes(".")) return meta.name;
3100
+ const basename = resourceId.includes("/") ? resourceId.split("/").pop() : resourceId;
3101
+ return (basename == null ? void 0 : basename.includes(".")) ? basename : "";
3102
+ }
3103
+ async function renderFilePreviewReadonly(resourceId, state, fileName, options = {}) {
2350
3104
  var _a, _b;
2351
3105
  const meta = state.resourceIndex.get(resourceId);
2352
- const actualFileName = (meta == null ? void 0 : meta.name) || resourceId.split("/").pop() || "file";
2353
- const isPSD = actualFileName.toLowerCase().match(/\.psd$/);
2354
- const fileResult = document.createElement("div");
2355
- fileResult.className = isPSD ? "space-y-2" : "space-y-3";
2356
- const previewContainer = document.createElement("div");
2357
- if (isPSD) {
2358
- previewContainer.className = "bg-gray-100 rounded-lg overflow-hidden cursor-pointer hover:opacity-90 transition-opacity flex items-center p-3 max-w-sm";
2359
- } else {
2360
- previewContainer.className = "bg-gray-100 rounded-lg overflow-hidden cursor-pointer hover:opacity-90 transition-opacity";
2361
- }
2362
- const isImage = !isPSD && (((_a = meta == null ? void 0 : meta.type) == null ? void 0 : _a.startsWith("image/")) || actualFileName.toLowerCase().match(/\.(jpg|jpeg|png|gif|webp)$/));
2363
- const isVideo = ((_b = meta == null ? void 0 : meta.type) == null ? void 0 : _b.startsWith("video/")) || actualFileName.toLowerCase().match(/\.(mp4|webm|avi|mov)$/);
2364
- if (isImage) {
3106
+ const actualFileName = resolveFileName(resourceId, meta, fileName);
3107
+ const { canRemove = false, removeHandler = null } = options;
3108
+ const isImage = ((_a = meta == null ? void 0 : meta.type) == null ? void 0 : _a.startsWith("image/")) || Boolean(actualFileName.toLowerCase().match(/\.(jpg|jpeg|png|gif|webp)$/));
3109
+ const isVideo = ((_b = meta == null ? void 0 : meta.type) == null ? void 0 : _b.startsWith("video/")) || Boolean(actualFileName.toLowerCase().match(/\.(mp4|webm|avi|mov)$/));
3110
+ const tile = createFileTile();
3111
+ tile.classList.add("fb-tile-resource");
3112
+ tile.style.cursor = "pointer";
3113
+ if (actualFileName) {
3114
+ tile.title = actualFileName;
3115
+ }
3116
+ const localFileUrl = (meta == null ? void 0 : meta.file) instanceof File ? getLocalFileUrl(meta.file) : null;
3117
+ const resolveOpenUrl = async () => {
3118
+ if (state.config.getDownloadUrl) return state.config.getDownloadUrl(resourceId);
3119
+ if (state.config.getThumbnail) return state.config.getThumbnail(resourceId);
3120
+ return localFileUrl;
3121
+ };
3122
+ tile.onclick = async () => {
3123
+ const url = await resolveOpenUrl();
3124
+ if (url) {
3125
+ window.open(url, "_blank");
3126
+ } else if (state.config.downloadFile) {
3127
+ state.config.downloadFile(resourceId, actualFileName);
3128
+ } else {
3129
+ forceDownload(resourceId, actualFileName, state).catch((err) => {
3130
+ console.error("Download failed:", err);
3131
+ });
3132
+ }
3133
+ };
3134
+ const actionsEl = createTileActions({
3135
+ canRemove,
3136
+ removeHandler,
3137
+ state,
3138
+ resourceId,
3139
+ fileName: actualFileName,
3140
+ meta
3141
+ });
3142
+ const resolveImageDisplayUrl = async () => {
2365
3143
  if (state.config.getThumbnail) {
2366
3144
  try {
2367
- const thumbnailUrl = await state.config.getThumbnail(resourceId);
2368
- if (thumbnailUrl) {
2369
- previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${escapeHtml(actualFileName)}" class="w-full h-auto">`;
2370
- } else {
2371
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F5BC}\uFE0F</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
2372
- }
2373
- } catch (error) {
2374
- console.warn("getThumbnail failed for", resourceId, error);
2375
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F5BC}\uFE0F</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
3145
+ const url = await state.config.getThumbnail(resourceId);
3146
+ if (url) return url;
3147
+ } catch {
2376
3148
  }
3149
+ }
3150
+ return localFileUrl;
3151
+ };
3152
+ const renderImageFallback = () => {
3153
+ tile.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F5BC}\uFE0F</div>`;
3154
+ tile.appendChild(actionsEl);
3155
+ };
3156
+ if (isImage) {
3157
+ const displayUrl = await resolveImageDisplayUrl();
3158
+ if (displayUrl) {
3159
+ const img = document.createElement("img");
3160
+ img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
3161
+ img.alt = actualFileName;
3162
+ img.src = displayUrl;
3163
+ tile.appendChild(img);
3164
+ tile.appendChild(actionsEl);
3165
+ attachZoomHover(tile, displayUrl, actualFileName, actionsEl);
2377
3166
  } else {
2378
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F5BC}\uFE0F</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
3167
+ renderImageFallback();
2379
3168
  }
2380
3169
  } else if (isVideo) {
2381
3170
  if (state.config.getThumbnail) {
2382
3171
  try {
2383
3172
  const videoUrl = await state.config.getThumbnail(resourceId);
2384
3173
  if (videoUrl) {
2385
- previewContainer.innerHTML = `
2386
- <div class="relative group">
2387
- <video class="w-full h-auto" controls preload="auto" muted src="${videoUrl}">
2388
- ${escapeHtml(t("videoNotSupported", state))}
2389
- </video>
2390
- <div class="absolute inset-0 bg-black bg-opacity-20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center pointer-events-none">
2391
- <div class="bg-white bg-opacity-90 rounded-full p-3">
2392
- <svg class="w-8 h-8 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
2393
- <path d="M8 5v14l11-7z"/>
2394
- </svg>
2395
- </div>
3174
+ tile.innerHTML = `
3175
+ <img style="width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);" alt="${escapeHtml(actualFileName)}" src="${videoUrl}">
3176
+ <div class="fb-video-overlay">
3177
+ <div class="fb-play-btn" style="width:22px;height:22px;">
3178
+ <svg width="10" height="12" viewBox="0 0 10 12" fill="currentColor"><path d="M0 0l10 6-10 6z"/></svg>
2396
3179
  </div>
2397
- </div>
2398
- `;
3180
+ </div>`;
3181
+ tile.appendChild(actionsEl);
2399
3182
  } else {
2400
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F3A5}</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
3183
+ tile.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
3184
+ tile.appendChild(actionsEl);
2401
3185
  }
2402
- } catch (error) {
2403
- console.warn("getThumbnail failed for video", resourceId, error);
2404
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F3A5}</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
3186
+ } catch {
3187
+ tile.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
3188
+ tile.appendChild(actionsEl);
2405
3189
  }
2406
3190
  } else {
2407
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F3A5}</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
3191
+ tile.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
3192
+ tile.appendChild(actionsEl);
2408
3193
  }
2409
3194
  } else {
2410
- const fileIcon = isPSD ? "\u{1F3A8}" : "\u{1F4C1}";
2411
- const fileDescription = isPSD ? "PSD File" : "Document";
2412
- if (isPSD) {
2413
- previewContainer.innerHTML = `
2414
- <div class="flex items-center space-x-3">
2415
- <div class="text-3xl text-gray-400">${fileIcon}</div>
2416
- <div class="flex-1 min-w-0">
2417
- <div class="text-sm font-medium text-gray-900 truncate">${escapeHtml(actualFileName)}</div>
2418
- <div class="text-xs text-gray-500">${fileDescription}</div>
2419
- </div>
2420
- </div>
2421
- `;
2422
- } else {
2423
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">${fileIcon}</div><div class="text-sm">${escapeHtml(actualFileName)}</div><div class="text-xs text-gray-500 mt-1">${fileDescription}</div></div></div>`;
3195
+ if (state.config.getThumbnail) {
3196
+ try {
3197
+ const thumbUrl = await state.config.getThumbnail(resourceId);
3198
+ if (thumbUrl) {
3199
+ const img = document.createElement("img");
3200
+ img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
3201
+ img.alt = actualFileName || resourceId;
3202
+ img.src = thumbUrl;
3203
+ tile.appendChild(img);
3204
+ tile.appendChild(actionsEl);
3205
+ return tile;
3206
+ }
3207
+ } catch {
3208
+ }
2424
3209
  }
3210
+ const captionHtml = actualFileName ? `<div class="fb-tile-label">${escapeHtml(actualFileName.length > 10 ? actualFileName.substring(0, 8) + "\u2026" : actualFileName)}</div>
3211
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zm-7 9H5v2h14v-2h-7z"/></svg>` : "";
3212
+ tile.innerHTML = `
3213
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
3214
+ <div style="font-size:36px;">\u{1F4C1}</div>
3215
+ ${captionHtml}
3216
+ </div>`;
3217
+ tile.appendChild(actionsEl);
2425
3218
  }
2426
- const fileNameElement = document.createElement("p");
2427
- fileNameElement.className = isPSD ? "hidden" : "text-sm font-medium text-gray-900 text-center";
2428
- fileNameElement.textContent = actualFileName;
2429
- const downloadButton = document.createElement("button");
2430
- downloadButton.className = "w-full px-3 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors";
2431
- downloadButton.textContent = t("downloadButton", state);
2432
- downloadButton.onclick = (e) => {
2433
- e.preventDefault();
2434
- e.stopPropagation();
2435
- if (state.config.downloadFile) {
2436
- state.config.downloadFile(resourceId, actualFileName);
2437
- } else {
2438
- forceDownload(resourceId, actualFileName, state);
2439
- }
2440
- };
2441
- fileResult.appendChild(previewContainer);
2442
- fileResult.appendChild(fileNameElement);
2443
- fileResult.appendChild(downloadButton);
2444
- return fileResult;
3219
+ return tile;
2445
3220
  }
2446
- function renderResourcePills(container, rids, state, onRemove, hint, countInfo) {
2447
- clear(container);
2448
- const buildHintLine = () => {
2449
- const parts = [t("clickDragTextMultiple", state)];
2450
- if (hint) parts.push(hint);
2451
- if (countInfo) parts.push(countInfo);
2452
- return parts.join(" \u2022 ");
2453
- };
2454
- const isInitialRender = !container.classList.contains("grid");
2455
- if ((!rids || rids.length === 0) && isInitialRender) {
2456
- const gridContainer2 = document.createElement("div");
2457
- gridContainer2.className = "grid grid-cols-4 gap-3 mb-3";
2458
- for (let i = 0; i < 4; i++) {
2459
- const slot = document.createElement("div");
2460
- slot.className = "aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors";
2461
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2462
- svg.setAttribute("class", "w-12 h-12 text-gray-400");
2463
- svg.setAttribute("fill", "currentColor");
2464
- svg.setAttribute("viewBox", "0 0 24 24");
2465
- const path = document.createElementNS(
2466
- "http://www.w3.org/2000/svg",
2467
- "path"
2468
- );
2469
- path.setAttribute(
2470
- "d",
2471
- "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"
2472
- );
2473
- svg.appendChild(path);
2474
- slot.appendChild(svg);
2475
- slot.onclick = () => {
2476
- let filesWrapper = container.parentElement;
2477
- while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
2478
- filesWrapper = filesWrapper.parentElement;
2479
- }
2480
- if (!filesWrapper && container.classList.contains("space-y-2")) {
2481
- filesWrapper = container;
2482
- }
2483
- const fileInput = filesWrapper == null ? void 0 : filesWrapper.querySelector(
2484
- 'input[type="file"]'
2485
- );
2486
- if (fileInput) fileInput.click();
2487
- };
2488
- gridContainer2.appendChild(slot);
2489
- }
2490
- const hintText2 = document.createElement("div");
2491
- hintText2.className = "text-center text-xs text-gray-500 mt-2";
2492
- hintText2.textContent = buildHintLine();
2493
- container.appendChild(gridContainer2);
2494
- container.appendChild(hintText2);
2495
- return;
2496
- }
2497
- const gridContainer = document.createElement("div");
2498
- gridContainer.className = "files-list grid grid-cols-4 gap-3";
2499
- const currentImagesCount = rids ? rids.length : 0;
2500
- const rowsNeeded = Math.floor(currentImagesCount / 4) + 1;
2501
- const slotsNeeded = rowsNeeded * 4;
2502
- for (let i = 0; i < slotsNeeded; i++) {
2503
- const slot = document.createElement("div");
2504
- if (rids && i < rids.length) {
2505
- const rid = rids[i];
2506
- const meta = state.resourceIndex.get(rid);
2507
- slot.className = "resource-pill aspect-square bg-gray-100 rounded-lg overflow-hidden relative group border border-gray-300";
2508
- slot.dataset.resourceId = rid;
2509
- renderThumbnailForResource(slot, rid, meta, state).catch((err) => {
2510
- console.error("Failed to render thumbnail:", err);
2511
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
2512
- <div class="text-2xl mb-1">\u{1F4C1}</div>
2513
- <div class="text-xs">${escapeHtml(t("previewError", state))}</div>
2514
- </div>`;
2515
- });
2516
- if (onRemove) {
2517
- const overlay = document.createElement("div");
2518
- overlay.className = "absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center";
2519
- const removeBtn = document.createElement("button");
2520
- removeBtn.className = "bg-red-600 text-white px-2 py-1 rounded text-xs";
2521
- removeBtn.textContent = t("removeElement", state);
2522
- removeBtn.onclick = (e) => {
2523
- e.stopPropagation();
2524
- onRemove(rid);
2525
- };
2526
- overlay.appendChild(removeBtn);
2527
- slot.appendChild(overlay);
2528
- }
2529
- } else {
2530
- slot.className = "aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors";
2531
- slot.innerHTML = '<svg class="w-12 h-12 text-gray-400" fill="currentColor" viewBox="0 0 24 24"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>';
2532
- slot.onclick = () => {
2533
- let filesWrapper = container.parentElement;
2534
- while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
2535
- filesWrapper = filesWrapper.parentElement;
2536
- }
2537
- if (!filesWrapper && container.classList.contains("space-y-2")) {
2538
- filesWrapper = container;
2539
- }
2540
- const fileInput = filesWrapper == null ? void 0 : filesWrapper.querySelector(
2541
- 'input[type="file"]'
2542
- );
2543
- if (fileInput) fileInput.click();
2544
- };
2545
- }
2546
- gridContainer.appendChild(slot);
2547
- }
2548
- container.appendChild(gridContainer);
2549
- const hintText = document.createElement("div");
2550
- hintText.className = "text-center text-xs text-gray-500 mt-2";
2551
- hintText.textContent = buildHintLine();
2552
- container.appendChild(hintText);
2553
- }
2554
- function renderThumbnailError(slot, state, iconSize = "w-12 h-12") {
2555
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
2556
- <svg class="${escapeHtml(iconSize)} text-red-400" fill="currentColor" viewBox="0 0 24 24">
2557
- <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
2558
- </svg>
2559
- <div class="text-xs mt-1 text-red-600">${escapeHtml(t("previewError", state))}</div>
2560
- </div>`;
3221
+ async function renderSingleFileEditTile(fileContainer, resourceId, state, deps) {
3222
+ var _a, _b, _c;
3223
+ const meta = state.resourceIndex.get(resourceId);
3224
+ const fileName = (_b = (_a = meta == null ? void 0 : meta.name) != null ? _a : resourceId.split("/").pop()) != null ? _b : "";
3225
+ const removeHandler = (_c = deps.onRemove) != null ? _c : null;
3226
+ const tile = await renderFilePreviewReadonly(resourceId, state, fileName, {
3227
+ canRemove: true,
3228
+ removeHandler
3229
+ });
3230
+ fileContainer.className = "file-preview-container";
3231
+ fileContainer.removeAttribute("style");
3232
+ clear(fileContainer);
3233
+ fileContainer.appendChild(tile);
2561
3234
  }
2562
- async function renderThumbnailForResource(slot, rid, meta, state) {
2563
- var _a, _b;
2564
- if (meta && ((_a = meta.type) == null ? void 0 : _a.startsWith("image/"))) {
3235
+ async function fillTileContent(tile, rid, meta, state, actionsEl) {
3236
+ var _a, _b, _c;
3237
+ if ((_a = meta == null ? void 0 : meta.type) == null ? void 0 : _a.startsWith("image/")) {
2565
3238
  if (meta.file && meta.file instanceof File) {
2566
3239
  const img = document.createElement("img");
2567
- img.className = "w-full h-full object-contain";
3240
+ img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
2568
3241
  img.alt = meta.name;
2569
3242
  const reader = new FileReader();
2570
3243
  reader.onload = (e) => {
2571
3244
  var _a2;
2572
3245
  img.src = ((_a2 = e.target) == null ? void 0 : _a2.result) || "";
3246
+ attachZoomHover(tile, img.src, meta.name, actionsEl != null ? actionsEl : null);
2573
3247
  };
2574
3248
  reader.readAsDataURL(meta.file);
2575
- slot.appendChild(img);
3249
+ tile.appendChild(img);
2576
3250
  } else if (state.config.getThumbnail) {
2577
3251
  try {
2578
3252
  const url = await state.config.getThumbnail(rid);
2579
3253
  if (url) {
2580
3254
  const img = document.createElement("img");
2581
- img.className = "w-full h-full object-contain";
3255
+ img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
2582
3256
  img.alt = meta.name;
2583
3257
  img.src = url;
2584
- slot.appendChild(img);
3258
+ tile.appendChild(img);
3259
+ attachZoomHover(tile, url, meta.name, actionsEl != null ? actionsEl : null);
2585
3260
  } else {
2586
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
2587
- <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
2588
- <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
2589
- </svg>
2590
- </div>`;
3261
+ tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F5BC}\uFE0F</div>`;
2591
3262
  }
2592
3263
  } catch (error) {
2593
3264
  const err = error instanceof Error ? error : new Error(String(error));
2594
- if (state.config.onThumbnailError) {
2595
- state.config.onThumbnailError(err, rid);
2596
- }
2597
- renderThumbnailError(slot, state);
3265
+ if (state.config.onThumbnailError) state.config.onThumbnailError(err, rid);
3266
+ tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:16px;color:var(--fb-error-color,#ef4444);">\u2715</div>`;
2598
3267
  }
2599
3268
  } else {
2600
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
2601
- <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
2602
- <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
2603
- </svg>
2604
- </div>`;
3269
+ tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F5BC}\uFE0F</div>`;
2605
3270
  }
2606
- } else if (meta && ((_b = meta.type) == null ? void 0 : _b.startsWith("video/"))) {
3271
+ if (actionsEl) tile.appendChild(actionsEl);
3272
+ } else if ((_b = meta == null ? void 0 : meta.type) == null ? void 0 : _b.startsWith("video/")) {
2607
3273
  if (meta.file && meta.file instanceof File) {
2608
3274
  const videoUrl = URL.createObjectURL(meta.file);
2609
- slot.innerHTML = `
2610
- <div class="relative group h-full w-full">
2611
- <video class="w-full h-full object-contain" preload="metadata" muted src="${videoUrl}">
2612
- </video>
2613
- <div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
2614
- <div class="bg-white bg-opacity-90 rounded-full p-1">
2615
- <svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
2616
- <path d="M8 5v14l11-7z"/>
2617
- </svg>
2618
- </div>
3275
+ tile.innerHTML = `
3276
+ <video style="width:100%;height:100%;" preload="metadata" muted src="${videoUrl}"></video>
3277
+ <div class="fb-video-overlay">
3278
+ <div class="fb-play-btn" style="width:20px;height:20px;">
3279
+ <svg width="8" height="10" viewBox="0 0 8 10" fill="currentColor"><path d="M0 0l8 5-8 5z"/></svg>
2619
3280
  </div>
2620
- </div>
2621
- `;
3281
+ </div>`;
2622
3282
  } else if (state.config.getThumbnail) {
2623
3283
  try {
2624
3284
  const videoUrl = await state.config.getThumbnail(rid);
2625
3285
  if (videoUrl) {
2626
- slot.innerHTML = `
2627
- <div class="relative group h-full w-full">
2628
- <video class="w-full h-full object-contain" preload="metadata" muted src="${videoUrl}">
2629
- </video>
2630
- <div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
2631
- <div class="bg-white bg-opacity-90 rounded-full p-1">
2632
- <svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
2633
- <path d="M8 5v14l11-7z"/>
2634
- </svg>
2635
- </div>
3286
+ tile.innerHTML = `
3287
+ <img style="width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);" alt="${escapeHtml(meta.name)}" src="${videoUrl}">
3288
+ <div class="fb-video-overlay">
3289
+ <div class="fb-play-btn" style="width:20px;height:20px;">
3290
+ <svg width="8" height="10" viewBox="0 0 8 10" fill="currentColor"><path d="M0 0l8 5-8 5z"/></svg>
2636
3291
  </div>
2637
- </div>
2638
- `;
3292
+ </div>`;
2639
3293
  } else {
2640
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
2641
- <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
2642
- <path d="M8 5v14l11-7z"/>
2643
- </svg>
2644
- <div class="text-xs mt-1">${escapeHtml((meta == null ? void 0 : meta.name) || "Video")}</div>
2645
- </div>`;
3294
+ tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
2646
3295
  }
2647
3296
  } catch (error) {
2648
3297
  const err = error instanceof Error ? error : new Error(String(error));
2649
- if (state.config.onThumbnailError) {
2650
- state.config.onThumbnailError(err, rid);
2651
- }
2652
- renderThumbnailError(slot, state, "w-8 h-8");
3298
+ if (state.config.onThumbnailError) state.config.onThumbnailError(err, rid);
3299
+ tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:16px;color:var(--fb-error-color,#ef4444);">\u2715</div>`;
2653
3300
  }
2654
3301
  } else {
2655
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
2656
- <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
2657
- <path d="M8 5v14l11-7z"/>
2658
- </svg>
2659
- <div class="text-xs mt-1">${escapeHtml((meta == null ? void 0 : meta.name) || "Video")}</div>
2660
- </div>`;
3302
+ tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:36px;">\u{1F3A5}</div>`;
2661
3303
  }
3304
+ if (actionsEl) tile.appendChild(actionsEl);
2662
3305
  } else {
2663
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
2664
- <div class="text-2xl mb-1">\u{1F4C1}</div>
2665
- <div class="text-xs">${escapeHtml((meta == null ? void 0 : meta.name) || "File")}</div>
2666
- </div>`;
3306
+ const name = (_c = meta == null ? void 0 : meta.name) != null ? _c : "";
3307
+ const hasExtension = name.includes(".");
3308
+ const captionHtml = hasExtension ? `<div class="fb-tile-label">${escapeHtml(name.length > 10 ? name.substring(0, 8) + "\u2026" : name)}</div>` : "";
3309
+ tile.innerHTML = `
3310
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
3311
+ <div style="font-size:36px;">\u{1F4C1}</div>
3312
+ ${captionHtml}
3313
+ </div>`;
3314
+ if (actionsEl) tile.appendChild(actionsEl);
2667
3315
  }
2668
3316
  }
2669
- function setEmptyFileContainer(fileContainer, state, hint) {
2670
- const hintHtml = hint ? `<div class="text-xs text-gray-500 mt-1">${escapeHtml(hint)}</div>` : "";
2671
- fileContainer.innerHTML = `
2672
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
2673
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
2674
- <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
2675
- </svg>
2676
- <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
2677
- ${hintHtml}
2678
- </div>
2679
- `;
3317
+ async function forceDownload(resourceId, fileName, state) {
3318
+ try {
3319
+ let fileUrl = null;
3320
+ if (state.config.getDownloadUrl) {
3321
+ fileUrl = state.config.getDownloadUrl(resourceId);
3322
+ } else if (state.config.getThumbnail) {
3323
+ fileUrl = await state.config.getThumbnail(resourceId);
3324
+ }
3325
+ if (fileUrl) {
3326
+ const finalUrl = fileUrl.startsWith("http") ? fileUrl : new URL(fileUrl, window.location.href).href;
3327
+ const response = await fetch(finalUrl);
3328
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
3329
+ const blob = await response.blob();
3330
+ downloadBlob(blob, fileName);
3331
+ } else {
3332
+ throw new Error("No download URL available for resource");
3333
+ }
3334
+ } catch (error) {
3335
+ const err = error instanceof Error ? error : new Error(String(error));
3336
+ if (state.config.onDownloadError) {
3337
+ state.config.onDownloadError(err, resourceId, fileName);
3338
+ }
3339
+ console.error(`File download failed for ${fileName}:`, err);
3340
+ throw err;
3341
+ }
2680
3342
  }
2681
- function showFileError(container, message) {
2682
- var _a, _b;
2683
- const existing = (_a = container.closest(".space-y-2")) == null ? void 0 : _a.querySelector(".file-error-message");
2684
- if (existing) existing.remove();
2685
- const errorEl = document.createElement("div");
2686
- errorEl.className = "file-error-message error-message";
2687
- errorEl.style.cssText = `
2688
- color: var(--fb-error-color);
2689
- font-size: var(--fb-font-size-small);
2690
- margin-top: 0.25rem;
2691
- `;
2692
- errorEl.textContent = message;
2693
- (_b = container.closest(".space-y-2")) == null ? void 0 : _b.appendChild(errorEl);
3343
+ function downloadBlob(blob, fileName) {
3344
+ try {
3345
+ const blobUrl = URL.createObjectURL(blob);
3346
+ const link = document.createElement("a");
3347
+ link.href = blobUrl;
3348
+ link.download = fileName;
3349
+ link.style.display = "none";
3350
+ document.body.appendChild(link);
3351
+ link.click();
3352
+ document.body.removeChild(link);
3353
+ setTimeout(() => {
3354
+ URL.revokeObjectURL(blobUrl);
3355
+ }, 100);
3356
+ } catch (error) {
3357
+ throw new Error(`Blob download failed: ${error.message}`);
3358
+ }
2694
3359
  }
2695
- function clearFileError(container) {
2696
- var _a;
2697
- const existing = (_a = container.closest(".space-y-2")) == null ? void 0 : _a.querySelector(".file-error-message");
2698
- if (existing) existing.remove();
3360
+
3361
+ // src/components/file/upload.ts
3362
+ async function uploadSingleFile(file, state) {
3363
+ if (!state.config.uploadFile) {
3364
+ throw new Error(
3365
+ "No upload handler configured. Set uploadHandler via FormBuilder.setUploadHandler()"
3366
+ );
3367
+ }
3368
+ try {
3369
+ const rid = await state.config.uploadFile(file);
3370
+ if (typeof rid !== "string") {
3371
+ throw new Error("Upload handler must return a string resource ID");
3372
+ }
3373
+ return rid;
3374
+ } catch (error) {
3375
+ const err = error instanceof Error ? error : new Error(String(error));
3376
+ if (state.config.onUploadError) state.config.onUploadError(err, file);
3377
+ throw new Error(`File upload failed: ${err.message}`);
3378
+ }
2699
3379
  }
2700
- async function handleFileSelect(file, container, fieldName, state, deps = null, instance, allowedExtensions = [], maxSizeMB = Infinity) {
3380
+ async function handleFileSelect(opts) {
2701
3381
  var _a, _b;
3382
+ const {
3383
+ file,
3384
+ container,
3385
+ fieldName,
3386
+ state,
3387
+ deps = null,
3388
+ instance = null,
3389
+ allowedExtensions = [],
3390
+ allowedMimes = [],
3391
+ maxSizeMB = Infinity
3392
+ } = opts;
2702
3393
  if (!isFileExtensionAllowed(file.name, allowedExtensions)) {
2703
3394
  const formats = allowedExtensions.join(", ");
2704
3395
  showFileError(
@@ -2707,6 +3398,14 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
2707
3398
  );
2708
3399
  return;
2709
3400
  }
3401
+ if (!isMimeAllowed(file.type, allowedMimes)) {
3402
+ const mimes = allowedMimes.join(", ");
3403
+ showFileError(
3404
+ container,
3405
+ t("invalidFileMime", state, { name: file.name, type: file.type, mimes })
3406
+ );
3407
+ return;
3408
+ }
2710
3409
  if (!isFileSizeAllowed(file, maxSizeMB)) {
2711
3410
  showFileError(
2712
3411
  container,
@@ -2715,24 +3414,18 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
2715
3414
  return;
2716
3415
  }
2717
3416
  clearFileError(container);
3417
+ ensureFileStyles();
3418
+ container.innerHTML = `
3419
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:6px;padding:6px;">
3420
+ <div class="fb-spinner"></div>
3421
+ <div style="font-size:11px;color:var(--fb-text-secondary-color,#6b7280);text-align:center;">${escapeHtml(t("uploadingFile", state))}</div>
3422
+ </div>`;
2718
3423
  let rid;
2719
- if (state.config.uploadFile) {
2720
- try {
2721
- rid = await state.config.uploadFile(file);
2722
- if (typeof rid !== "string") {
2723
- throw new Error("Upload handler must return a string resource ID");
2724
- }
2725
- } catch (error) {
2726
- const err = error instanceof Error ? error : new Error(String(error));
2727
- if (state.config.onUploadError) {
2728
- state.config.onUploadError(err, file);
2729
- }
2730
- throw new Error(`File upload failed: ${err.message}`);
2731
- }
2732
- } else {
2733
- throw new Error(
2734
- "No upload handler configured. Set uploadHandler via FormBuilder.setUploadHandler()"
2735
- );
3424
+ try {
3425
+ rid = await uploadSingleFile(file, state);
3426
+ } catch (error) {
3427
+ setEmptyFileContainer(container, state);
3428
+ throw error;
2736
3429
  }
2737
3430
  state.resourceIndex.set(rid, {
2738
3431
  name: file.name,
@@ -2740,7 +3433,6 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
2740
3433
  size: file.size,
2741
3434
  uploadedAt: /* @__PURE__ */ new Date(),
2742
3435
  file
2743
- // Store the file object for local preview
2744
3436
  });
2745
3437
  let hiddenInput = (_a = container.parentElement) == null ? void 0 : _a.querySelector(
2746
3438
  'input[type="hidden"]'
@@ -2752,693 +3444,1009 @@ async function handleFileSelect(file, container, fieldName, state, deps = null,
2752
3444
  (_b = container.parentElement) == null ? void 0 : _b.appendChild(hiddenInput);
2753
3445
  }
2754
3446
  hiddenInput.value = rid;
2755
- renderFilePreview(container, rid, state, {
2756
- fileName: file.name,
2757
- isReadonly: false,
2758
- deps
2759
- }).catch(console.error);
3447
+ const isVideo = file.type.startsWith("video/");
3448
+ if (!isVideo && deps) {
3449
+ renderSingleFileEditTile(container, rid, state, deps).catch(console.error);
3450
+ } else {
3451
+ renderFilePreview(container, rid, state, {
3452
+ fileName: file.name,
3453
+ isReadonly: false,
3454
+ deps
3455
+ }).catch(console.error);
3456
+ }
2760
3457
  if (instance && !state.config.readonly) {
2761
3458
  instance.triggerOnChange(fieldName, rid);
2762
3459
  }
2763
3460
  }
2764
- function setupDragAndDrop(element, dropHandler) {
2765
- element.addEventListener("dragover", (e) => {
2766
- e.preventDefault();
2767
- element.classList.add("border-blue-500", "bg-blue-50");
2768
- });
2769
- element.addEventListener("dragleave", (e) => {
2770
- e.preventDefault();
2771
- element.classList.remove("border-blue-500", "bg-blue-50");
2772
- });
2773
- element.addEventListener("drop", (e) => {
3461
+ function filterAndSlice(allFiles, currentCount, constraints, state) {
3462
+ const rejectedByExt = allFiles.filter(
3463
+ (f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
3464
+ );
3465
+ const afterExt = allFiles.filter(
3466
+ (f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
3467
+ );
3468
+ const rejectedByMime = afterExt.filter(
3469
+ (f) => !isMimeAllowed(f.type, constraints.allowedMimes)
3470
+ );
3471
+ const afterMime = afterExt.filter(
3472
+ (f) => isMimeAllowed(f.type, constraints.allowedMimes)
3473
+ );
3474
+ const rejectedBySize = afterMime.filter(
3475
+ (f) => !isFileSizeAllowed(f, constraints.maxSize)
3476
+ );
3477
+ const valid = afterMime.filter((f) => isFileSizeAllowed(f, constraints.maxSize));
3478
+ const remaining = constraints.maxCount === Infinity ? valid.length : Math.max(0, constraints.maxCount - currentCount);
3479
+ const accepted = valid.slice(0, remaining);
3480
+ const skippedByCount = valid.length - accepted.length;
3481
+ const errorParts = [];
3482
+ if (rejectedByExt.length > 0) {
3483
+ const formats = constraints.allowedExtensions.join(", ");
3484
+ const names = rejectedByExt.map((f) => f.name).join(", ");
3485
+ errorParts.push(t("invalidFileExtension", state, { name: names, formats }));
3486
+ }
3487
+ if (rejectedByMime.length > 0) {
3488
+ const mimes = constraints.allowedMimes.join(", ");
3489
+ const names = rejectedByMime.map((f) => f.name).join(", ");
3490
+ errorParts.push(t("invalidFileMime", state, { name: names, type: rejectedByMime.map((f) => f.type).join(", "), mimes }));
3491
+ }
3492
+ if (rejectedBySize.length > 0) {
3493
+ const names = rejectedBySize.map((f) => f.name).join(", ");
3494
+ errorParts.push(
3495
+ t("fileTooLarge", state, { name: names, maxSize: constraints.maxSize })
3496
+ );
3497
+ }
3498
+ if (skippedByCount > 0) {
3499
+ errorParts.push(
3500
+ t("filesLimitExceeded", state, {
3501
+ skipped: skippedByCount,
3502
+ max: constraints.maxCount
3503
+ })
3504
+ );
3505
+ }
3506
+ return { accepted, errorMessage: errorParts.join(" \u2022 ") };
3507
+ }
3508
+ async function uploadBatch(accepted, resourceIds, listEl, state) {
3509
+ await Promise.all(
3510
+ accepted.map(async (file) => {
3511
+ const placeholder = createUploadingTile(file.name, state);
3512
+ if (listEl) {
3513
+ const tilesWrap = ensureTilesWrap(listEl);
3514
+ const addTile = tilesWrap.querySelector(".fb-tile-add");
3515
+ if (addTile) {
3516
+ tilesWrap.insertBefore(placeholder, addTile);
3517
+ } else {
3518
+ tilesWrap.appendChild(placeholder);
3519
+ }
3520
+ }
3521
+ try {
3522
+ const rid = await uploadSingleFile(file, state);
3523
+ state.resourceIndex.set(rid, {
3524
+ name: file.name,
3525
+ type: file.type,
3526
+ size: file.size,
3527
+ uploadedAt: /* @__PURE__ */ new Date(),
3528
+ file: void 0
3529
+ });
3530
+ resourceIds.push(rid);
3531
+ } finally {
3532
+ placeholder.remove();
3533
+ }
3534
+ })
3535
+ );
3536
+ }
3537
+ function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallback, constraints, pathKey, instance) {
3538
+ setupDragAndDrop(filesContainer, async (files) => {
2774
3539
  var _a;
2775
- e.preventDefault();
2776
- element.classList.remove("border-blue-500", "bg-blue-50");
2777
- if ((_a = e.dataTransfer) == null ? void 0 : _a.files) {
2778
- dropHandler(e.dataTransfer.files);
3540
+ const { accepted, errorMessage } = filterAndSlice(
3541
+ Array.from(files),
3542
+ resourceIds.length,
3543
+ constraints,
3544
+ state
3545
+ );
3546
+ if (errorMessage) {
3547
+ showFileError(filesContainer, errorMessage);
3548
+ } else {
3549
+ clearFileError(filesContainer);
3550
+ }
3551
+ const list = (_a = filesContainer.querySelector(".files-list")) != null ? _a : filesContainer;
3552
+ await uploadBatch(accepted, resourceIds, list, state);
3553
+ updateCallback();
3554
+ if (instance && pathKey && !state.config.readonly) {
3555
+ instance.triggerOnChange(pathKey, resourceIds);
2779
3556
  }
2780
3557
  });
2781
3558
  }
2782
- function addDeleteButton(container, state, onDelete) {
2783
- const existingOverlay = container.querySelector(".delete-overlay");
2784
- if (existingOverlay) {
2785
- existingOverlay.remove();
2786
- }
2787
- const overlay = document.createElement("div");
2788
- overlay.className = "delete-overlay absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center";
2789
- const deleteBtn = document.createElement("button");
2790
- deleteBtn.className = "bg-red-600 text-white px-3 py-1 rounded text-sm hover:bg-red-700 transition-colors";
2791
- deleteBtn.textContent = t("removeElement", state);
2792
- deleteBtn.onclick = (e) => {
2793
- e.stopPropagation();
2794
- onDelete();
3559
+ function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback, constraints, pathKey, instance) {
3560
+ filesPicker.onchange = async () => {
3561
+ if (!filesPicker.files) return;
3562
+ const wrapperEl = filesPicker.closest(".space-y-2") || filesPicker.parentElement;
3563
+ const { accepted, errorMessage } = filterAndSlice(
3564
+ Array.from(filesPicker.files),
3565
+ resourceIds.length,
3566
+ constraints,
3567
+ state
3568
+ );
3569
+ if (errorMessage && wrapperEl) {
3570
+ showFileError(wrapperEl, errorMessage);
3571
+ } else if (wrapperEl) {
3572
+ clearFileError(wrapperEl);
3573
+ }
3574
+ const listEl = wrapperEl == null ? void 0 : wrapperEl.querySelector(".files-list");
3575
+ await uploadBatch(accepted, resourceIds, listEl != null ? listEl : null, state);
3576
+ updateCallback();
3577
+ filesPicker.value = "";
3578
+ if (instance && pathKey && !state.config.readonly) {
3579
+ instance.triggerOnChange(pathKey, resourceIds);
3580
+ }
2795
3581
  };
2796
- overlay.appendChild(deleteBtn);
2797
- container.appendChild(overlay);
2798
3582
  }
2799
- async function uploadSingleFile(file, state) {
2800
- if (state.config.uploadFile) {
2801
- try {
2802
- const rid = await state.config.uploadFile(file);
2803
- if (typeof rid !== "string") {
2804
- throw new Error("Upload handler must return a string resource ID");
2805
- }
2806
- return rid;
2807
- } catch (error) {
2808
- const err = error instanceof Error ? error : new Error(String(error));
2809
- if (state.config.onUploadError) {
2810
- state.config.onUploadError(err, file);
2811
- }
2812
- throw new Error(`File upload failed: ${err.message}`);
2813
- }
2814
- } else {
2815
- throw new Error(
2816
- "No upload handler configured. Set uploadHandler via FormBuilder.setUploadHandler()"
2817
- );
3583
+
3584
+ // src/components/file/library.ts
3585
+ function buildAcceptContext(element) {
3586
+ var _a, _b;
3587
+ if (!element.accept) return void 0;
3588
+ if (typeof element.accept === "string") {
3589
+ const exts2 = getAllowedExtensions(element.accept);
3590
+ return exts2.length > 0 ? { extensions: exts2 } : void 0;
3591
+ }
3592
+ const exts = (_a = element.accept.extensions) != null ? _a : [];
3593
+ const mime = (_b = element.accept.mime) != null ? _b : [];
3594
+ const normalizedExts = exts.map((e) => e.toLowerCase());
3595
+ if (normalizedExts.length === 0 && mime.length === 0) return void 0;
3596
+ const result = {};
3597
+ if (normalizedExts.length > 0) result.extensions = normalizedExts;
3598
+ if (mime.length > 0) result.mime = mime;
3599
+ return result;
3600
+ }
3601
+ function validatePickedResource(resource, allowedExtensions, allowedMimes, maxSizeMB, state) {
3602
+ if (!isFileExtensionAllowed(resource.name, allowedExtensions)) {
3603
+ const formats = allowedExtensions.join(", ");
3604
+ return t("invalidFileExtension", state, { name: resource.name, formats });
3605
+ }
3606
+ if (!isMimeAllowed(resource.type, allowedMimes)) {
3607
+ const mimes = allowedMimes.join(", ");
3608
+ return t("invalidFileMime", state, { name: resource.name, type: resource.type, mimes });
3609
+ }
3610
+ if (!isSizeWithinLimit(resource.size, maxSizeMB)) {
3611
+ return t("fileTooLarge", state, { name: resource.name, maxSize: maxSizeMB });
2818
3612
  }
3613
+ return null;
2819
3614
  }
2820
- async function forceDownload(resourceId, fileName, state) {
3615
+ function readCurrentResourceIds(wrapper) {
3616
+ const raw = wrapper.dataset.resourceIds;
3617
+ if (!raw) return [];
2821
3618
  try {
2822
- let fileUrl = null;
2823
- if (state.config.getDownloadUrl) {
2824
- fileUrl = state.config.getDownloadUrl(resourceId);
2825
- } else if (state.config.getThumbnail) {
2826
- fileUrl = await state.config.getThumbnail(resourceId);
2827
- }
2828
- if (fileUrl) {
2829
- const finalUrl = fileUrl.startsWith("http") ? fileUrl : new URL(fileUrl, window.location.href).href;
2830
- const response = await fetch(finalUrl);
2831
- if (!response.ok) {
2832
- throw new Error(`HTTP error! status: ${response.status}`);
2833
- }
2834
- const blob = await response.blob();
2835
- downloadBlob(blob, fileName);
2836
- } else {
2837
- throw new Error("No download URL available for resource");
2838
- }
2839
- } catch (error) {
2840
- const err = error instanceof Error ? error : new Error(String(error));
2841
- if (state.config.onDownloadError) {
2842
- state.config.onDownloadError(err, resourceId, fileName);
2843
- }
2844
- console.error(`File download failed for ${fileName}:`, err);
2845
- throw err;
3619
+ const parsed = JSON.parse(raw);
3620
+ return Array.isArray(parsed) ? parsed : [];
3621
+ } catch {
3622
+ return [];
2846
3623
  }
2847
3624
  }
2848
- function downloadBlob(blob, fileName) {
2849
- try {
2850
- const blobUrl = URL.createObjectURL(blob);
2851
- const link = document.createElement("a");
2852
- link.href = blobUrl;
2853
- link.download = fileName;
2854
- link.style.display = "none";
2855
- document.body.appendChild(link);
2856
- link.click();
2857
- document.body.removeChild(link);
2858
- setTimeout(() => {
2859
- URL.revokeObjectURL(blobUrl);
2860
- }, 100);
3625
+ function registerPickedResource(resource, state) {
3626
+ var _a;
3627
+ const existing = state.resourceIndex.get(resource.resourceId);
3628
+ state.resourceIndex.set(resource.resourceId, {
3629
+ name: resource.name,
3630
+ type: resource.type,
3631
+ size: resource.size,
3632
+ uploadedAt: (_a = existing == null ? void 0 : existing.uploadedAt) != null ? _a : /* @__PURE__ */ new Date(),
3633
+ file: existing == null ? void 0 : existing.file
3634
+ });
3635
+ }
3636
+ function extractPickerError(error, state) {
3637
+ if (error instanceof Error && error.message) return error.message;
3638
+ return t("pickerError", state);
3639
+ }
3640
+ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resourceIds, maxCount, updateCallback, instance) {
3641
+ var _a;
3642
+ if (!state.config.pickExistingFiles) return;
3643
+ const allowedExtensions = getAllowedExtensions(element.accept);
3644
+ const allowedMimes = getAllowedMimes(element.accept);
3645
+ const maxSizeMB = (_a = element.maxSize) != null ? _a : Infinity;
3646
+ const currentIds = readCurrentResourceIds(wrapper);
3647
+ const remaining = maxCount === Infinity ? Infinity : Math.max(0, maxCount - currentIds.length);
3648
+ let picked;
3649
+ try {
3650
+ picked = await state.config.pickExistingFiles({
3651
+ fieldPath,
3652
+ mode: "multiple",
3653
+ accept: buildAcceptContext(element),
3654
+ maxSizeMB: maxSizeMB === Infinity ? void 0 : maxSizeMB,
3655
+ remainingSlots: remaining === Infinity ? void 0 : remaining,
3656
+ selectedResourceIds: [...currentIds]
3657
+ });
2861
3658
  } catch (error) {
2862
- throw new Error(`Blob download failed: ${error.message}`);
3659
+ showFileError(wrapper, extractPickerError(error, state));
3660
+ return;
3661
+ }
3662
+ if (picked.length === 0) return;
3663
+ const existingSet = new Set(currentIds);
3664
+ const seen = /* @__PURE__ */ new Set();
3665
+ const deduped = picked.filter((r) => {
3666
+ if (existingSet.has(r.resourceId)) return false;
3667
+ if (seen.has(r.resourceId)) return false;
3668
+ seen.add(r.resourceId);
3669
+ return true;
3670
+ });
3671
+ const validItems = deduped.filter((r) => {
3672
+ const err = validatePickedResource(r, allowedExtensions, allowedMimes, maxSizeMB, state);
3673
+ return err === null;
3674
+ });
3675
+ const freshRemaining = maxCount === Infinity ? validItems.length : Math.max(0, maxCount - resourceIds.length);
3676
+ const accepted = validItems.slice(0, freshRemaining);
3677
+ const skipped = validItems.length - accepted.length;
3678
+ if (accepted.length === 0) return;
3679
+ clearFileError(wrapper);
3680
+ if (skipped > 0) {
3681
+ showFileError(
3682
+ wrapper,
3683
+ t("filesLimitExceeded", state, { skipped, max: maxCount })
3684
+ );
3685
+ }
3686
+ for (const resource of accepted) {
3687
+ registerPickedResource(resource, state);
3688
+ resourceIds.push(resource.resourceId);
3689
+ }
3690
+ wrapper.dataset.resourceIds = JSON.stringify(resourceIds);
3691
+ updateCallback();
3692
+ if (!state.config.readonly) {
3693
+ instance.triggerOnChange(fieldPath, resourceIds);
2863
3694
  }
2864
3695
  }
2865
- function addPrefillFilesToIndex(initialFiles, state) {
2866
- if (initialFiles.length > 0) {
2867
- initialFiles.forEach((resourceId) => {
2868
- var _a;
2869
- if (!state.resourceIndex.has(resourceId)) {
2870
- const filename = resourceId.split("/").pop() || "file";
2871
- const extension = (_a = filename.split(".").pop()) == null ? void 0 : _a.toLowerCase();
2872
- let fileType = "application/octet-stream";
2873
- if (extension) {
2874
- if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
2875
- fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
2876
- } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
2877
- fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
2878
- }
2879
- }
2880
- state.resourceIndex.set(resourceId, {
2881
- name: filename,
2882
- type: fileType,
2883
- size: 0,
2884
- uploadedAt: /* @__PURE__ */ new Date(),
2885
- file: void 0
2886
- });
2887
- }
3696
+ async function handleLibraryPickSingle(state, element, container, fileWrapper, pathKey, fieldPath, renderCallback, instance) {
3697
+ var _a;
3698
+ if (!state.config.pickExistingFiles) return;
3699
+ const allowedExtensions = getAllowedExtensions(element.accept);
3700
+ const allowedMimes = getAllowedMimes(element.accept);
3701
+ const maxSizeMB = (_a = element.maxSize) != null ? _a : Infinity;
3702
+ let picked;
3703
+ try {
3704
+ picked = await state.config.pickExistingFiles({
3705
+ fieldPath,
3706
+ mode: "single",
3707
+ accept: buildAcceptContext(element),
3708
+ maxSizeMB: maxSizeMB === Infinity ? void 0 : maxSizeMB,
3709
+ selectedResourceIds: []
2888
3710
  });
3711
+ } catch (error) {
3712
+ showFileError(container, extractPickerError(error, state));
3713
+ return;
3714
+ }
3715
+ if (picked.length === 0) return;
3716
+ const first = picked[0];
3717
+ const validationError = validatePickedResource(first, allowedExtensions, allowedMimes, maxSizeMB, state);
3718
+ if (validationError !== null) {
3719
+ showFileError(container, validationError);
3720
+ return;
3721
+ }
3722
+ clearFileError(container);
3723
+ registerPickedResource(first, state);
3724
+ let hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
3725
+ if (!hiddenInput) {
3726
+ hiddenInput = document.createElement("input");
3727
+ hiddenInput.type = "hidden";
3728
+ hiddenInput.name = pathKey;
3729
+ fileWrapper.appendChild(hiddenInput);
3730
+ }
3731
+ hiddenInput.value = first.resourceId;
3732
+ await renderCallback(first.resourceId);
3733
+ if (!state.config.readonly) {
3734
+ instance.triggerOnChange(fieldPath, first.resourceId);
2889
3735
  }
2890
3736
  }
3737
+
3738
+ // src/components/file/render-edit.ts
2891
3739
  function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
2892
3740
  var _a;
2893
- if (!state.resourceIndex.has(initial)) {
2894
- const filename = initial.split("/").pop() || "file";
2895
- const extension = (_a = filename.split(".").pop()) == null ? void 0 : _a.toLowerCase();
2896
- let fileType = "application/octet-stream";
2897
- if (extension) {
2898
- if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
2899
- fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
2900
- } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
2901
- fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
2902
- }
2903
- }
2904
- state.resourceIndex.set(initial, {
2905
- name: filename,
2906
- type: fileType,
2907
- size: 0,
2908
- uploadedAt: /* @__PURE__ */ new Date(),
2909
- file: void 0
2910
- });
3741
+ seedInferredResource(initial, state.resourceIndex);
3742
+ const meta = state.resourceIndex.get(initial);
3743
+ const isVideo = (_a = meta == null ? void 0 : meta.type) == null ? void 0 : _a.startsWith("video/");
3744
+ if (isVideo) {
3745
+ renderFilePreview(fileContainer, initial, state, {
3746
+ fileName: initial,
3747
+ isReadonly: false,
3748
+ deps
3749
+ }).catch(console.error);
3750
+ } else {
3751
+ renderSingleFileEditTile(fileContainer, initial, state, deps).catch(console.error);
2911
3752
  }
2912
- renderFilePreview(fileContainer, initial, state, {
2913
- fileName: initial,
2914
- isReadonly: false,
2915
- deps
2916
- }).catch(console.error);
2917
3753
  const hiddenInput = document.createElement("input");
2918
3754
  hiddenInput.type = "hidden";
2919
3755
  hiddenInput.name = pathKey;
2920
3756
  hiddenInput.value = initial;
2921
3757
  fileWrapper.appendChild(hiddenInput);
2922
3758
  }
2923
- function setupFilesDropHandler(filesContainer, initialFiles, state, updateCallback, constraints, pathKey, instance) {
2924
- setupDragAndDrop(filesContainer, async (files) => {
2925
- const allFiles = Array.from(files);
2926
- const rejectedByExtension = allFiles.filter(
2927
- (f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2928
- );
2929
- const afterExtension = allFiles.filter(
2930
- (f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2931
- );
2932
- const rejectedBySize = afterExtension.filter(
2933
- (f) => !isFileSizeAllowed(f, constraints.maxSize)
2934
- );
2935
- const validFiles = afterExtension.filter(
2936
- (f) => isFileSizeAllowed(f, constraints.maxSize)
2937
- );
2938
- const remaining = constraints.maxCount === Infinity ? validFiles.length : Math.max(0, constraints.maxCount - initialFiles.length);
2939
- const arr = validFiles.slice(0, remaining);
2940
- const skippedByCount = validFiles.length - arr.length;
2941
- const errorParts = [];
2942
- if (rejectedByExtension.length > 0) {
2943
- const formats = constraints.allowedExtensions.join(", ");
2944
- const names = rejectedByExtension.map((f) => f.name).join(", ");
2945
- errorParts.push(
2946
- t("invalidFileExtension", state, { name: names, formats })
2947
- );
2948
- }
2949
- if (rejectedBySize.length > 0) {
2950
- const names = rejectedBySize.map((f) => f.name).join(", ");
2951
- errorParts.push(
2952
- t("fileTooLarge", state, { name: names, maxSize: constraints.maxSize })
2953
- );
2954
- }
2955
- if (skippedByCount > 0) {
2956
- errorParts.push(
2957
- t("filesLimitExceeded", state, {
2958
- skipped: skippedByCount,
2959
- max: constraints.maxCount
2960
- })
2961
- );
2962
- }
2963
- if (errorParts.length > 0) {
2964
- showFileError(filesContainer, errorParts.join(" \u2022 "));
2965
- } else {
2966
- clearFileError(filesContainer);
2967
- }
2968
- for (const file of arr) {
2969
- const rid = await uploadSingleFile(file, state);
2970
- state.resourceIndex.set(rid, {
2971
- name: file.name,
2972
- type: file.type,
2973
- size: file.size,
2974
- uploadedAt: /* @__PURE__ */ new Date(),
2975
- file: void 0
2976
- });
2977
- initialFiles.push(rid);
2978
- }
2979
- updateCallback();
2980
- if (instance && pathKey && !state.config.readonly) {
2981
- instance.triggerOnChange(pathKey, initialFiles);
2982
- }
2983
- });
3759
+ 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);">
3760
+ <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"/>
3761
+ </svg>`;
3762
+ function buildEmptyDropzone(state, primaryText, subHint, openPicker) {
3763
+ const dropzone = document.createElement("div");
3764
+ dropzone.className = "fb-file-dropzone";
3765
+ dropzone.innerHTML = `
3766
+ ${UPLOAD_SVG}
3767
+ <div class="fb-dropzone-primary-text">${escapeHtml(primaryText)}</div>
3768
+ ${subHint ? `<div class="fb-dropzone-hint-text">${escapeHtml(subHint)}</div>` : ""}
3769
+ `;
3770
+ dropzone.onclick = openPicker;
3771
+ return dropzone;
2984
3772
  }
2985
- function setupFilesPickerHandler(filesPicker, initialFiles, state, updateCallback, constraints, pathKey, instance) {
2986
- filesPicker.onchange = async () => {
2987
- if (filesPicker.files) {
2988
- const allFiles = Array.from(filesPicker.files);
2989
- const rejectedByExtension = allFiles.filter(
2990
- (f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2991
- );
2992
- const afterExtension = allFiles.filter(
2993
- (f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2994
- );
2995
- const rejectedBySize = afterExtension.filter(
2996
- (f) => !isFileSizeAllowed(f, constraints.maxSize)
3773
+ function buildLibraryButton(variant, state, onClick) {
3774
+ const btn = document.createElement("button");
3775
+ btn.type = "button";
3776
+ btn.className = variant === "card" ? "fb-file-library-card" : "fb-tile fb-tile-add-library";
3777
+ if (variant === "card") {
3778
+ btn.innerHTML = `
3779
+ <span class="fb-file-library-card-icon" aria-hidden="true">\u{1F4DA}</span>
3780
+ <span class="fb-file-library-card-label">${escapeHtml(t("fromLibrary", state))}</span>
3781
+ <span class="fb-file-library-card-hint">${escapeHtml(t("libraryHint", state))}</span>
3782
+ `;
3783
+ } else {
3784
+ btn.innerHTML = `<span aria-hidden="true">\u{1F4DA}</span>`;
3785
+ btn.title = t("fromLibrary", state);
3786
+ btn.setAttribute("aria-label", t("fromLibrary", state));
3787
+ }
3788
+ btn.addEventListener("click", onClick);
3789
+ return btn;
3790
+ }
3791
+ function renderResourcePills(opts) {
3792
+ var _a;
3793
+ const {
3794
+ container,
3795
+ rids,
3796
+ state,
3797
+ onRemove,
3798
+ hint,
3799
+ countInfo,
3800
+ maxCount,
3801
+ isReadonly = false,
3802
+ onLibraryPick
3803
+ } = opts;
3804
+ ensureFileStyles();
3805
+ const wrapper = container.closest("[data-files-wrapper]");
3806
+ if (wrapper) {
3807
+ wrapper.dataset.resourceIds = JSON.stringify(rids != null ? rids : []);
3808
+ }
3809
+ while (container.firstChild) container.removeChild(container.firstChild);
3810
+ const ridList = rids != null ? rids : [];
3811
+ const atMax = maxCount !== void 0 && ridList.length >= maxCount;
3812
+ const hasLibrary = !isReadonly && typeof onLibraryPick === "function";
3813
+ const buildSubHint = () => {
3814
+ const parts = [];
3815
+ if (hint) parts.push(hint);
3816
+ if (countInfo) parts.push(countInfo);
3817
+ return parts.join(" \u2022 ");
3818
+ };
3819
+ const openPicker = () => {
3820
+ const picker = findFilePicker(container);
3821
+ if (picker) picker.click();
3822
+ };
3823
+ if (ridList.length === 0) {
3824
+ if (isReadonly) {
3825
+ const emptyEl = document.createElement("div");
3826
+ emptyEl.className = "fb-tile-empty-text";
3827
+ emptyEl.textContent = t("noFilesSelected", state);
3828
+ container.appendChild(emptyEl);
3829
+ } else if (hasLibrary) {
3830
+ const row = document.createElement("div");
3831
+ row.className = "fb-file-card-row";
3832
+ const dropzone = buildEmptyDropzone(
3833
+ state,
3834
+ t("clickDragTextMultiple", state),
3835
+ buildSubHint(),
3836
+ openPicker
2997
3837
  );
2998
- const validFiles = afterExtension.filter(
2999
- (f) => isFileSizeAllowed(f, constraints.maxSize)
3838
+ const libraryBtn = buildLibraryButton("card", state, onLibraryPick);
3839
+ row.appendChild(dropzone);
3840
+ row.appendChild(libraryBtn);
3841
+ container.appendChild(row);
3842
+ } else {
3843
+ const dropzone = buildEmptyDropzone(
3844
+ state,
3845
+ t("clickDragTextMultiple", state),
3846
+ buildSubHint(),
3847
+ openPicker
3000
3848
  );
3001
- const remaining = constraints.maxCount === Infinity ? validFiles.length : Math.max(0, constraints.maxCount - initialFiles.length);
3002
- const arr = validFiles.slice(0, remaining);
3003
- const skippedByCount = validFiles.length - arr.length;
3004
- const errorParts = [];
3005
- if (rejectedByExtension.length > 0) {
3006
- const formats = constraints.allowedExtensions.join(", ");
3007
- const names = rejectedByExtension.map((f) => f.name).join(", ");
3008
- errorParts.push(
3009
- t("invalidFileExtension", state, { name: names, formats })
3010
- );
3011
- }
3012
- if (rejectedBySize.length > 0) {
3013
- const names = rejectedBySize.map((f) => f.name).join(", ");
3014
- errorParts.push(
3015
- t("fileTooLarge", state, {
3016
- name: names,
3017
- maxSize: constraints.maxSize
3018
- })
3019
- );
3020
- }
3021
- if (skippedByCount > 0) {
3022
- errorParts.push(
3023
- t("filesLimitExceeded", state, {
3024
- skipped: skippedByCount,
3025
- max: constraints.maxCount
3026
- })
3027
- );
3028
- }
3029
- const wrapper = filesPicker.closest(".space-y-2") || filesPicker.parentElement;
3030
- if (errorParts.length > 0 && wrapper) {
3031
- showFileError(wrapper, errorParts.join(" \u2022 "));
3032
- } else if (wrapper) {
3033
- clearFileError(wrapper);
3034
- }
3035
- for (const file of arr) {
3036
- const rid = await uploadSingleFile(file, state);
3037
- state.resourceIndex.set(rid, {
3038
- name: file.name,
3039
- type: file.type,
3040
- size: file.size,
3041
- uploadedAt: /* @__PURE__ */ new Date(),
3042
- file: void 0
3043
- });
3044
- initialFiles.push(rid);
3045
- }
3046
- }
3047
- updateCallback();
3048
- filesPicker.value = "";
3049
- if (instance && pathKey && !state.config.readonly) {
3050
- instance.triggerOnChange(pathKey, initialFiles);
3849
+ container.appendChild(dropzone);
3051
3850
  }
3052
- };
3851
+ return;
3852
+ }
3853
+ const tilesWrap = document.createElement("div");
3854
+ tilesWrap.className = "fb-tiles-wrap";
3855
+ tilesWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start;";
3856
+ for (const rid of ridList) {
3857
+ const meta = state.resourceIndex.get(rid);
3858
+ const tile = createFileTile();
3859
+ tile.classList.add("fb-tile-resource", "resource-pill");
3860
+ tile.dataset.resourceId = rid;
3861
+ const actionsEl = createTileActions({
3862
+ canRemove: !isReadonly && onRemove !== null,
3863
+ removeHandler: onRemove ? () => onRemove(rid) : null,
3864
+ state,
3865
+ resourceId: rid,
3866
+ fileName: (_a = meta == null ? void 0 : meta.name) != null ? _a : ""
3867
+ });
3868
+ fillTileContent(tile, rid, meta, state, actionsEl).catch((err) => {
3869
+ console.error("Failed to render tile:", err);
3870
+ });
3871
+ tilesWrap.appendChild(tile);
3872
+ }
3873
+ if (!isReadonly && !atMax) {
3874
+ const addTile = document.createElement("div");
3875
+ addTile.className = "fb-tile fb-tile-add";
3876
+ addTile.innerHTML = "+";
3877
+ addTile.onclick = openPicker;
3878
+ tilesWrap.appendChild(addTile);
3879
+ if (hasLibrary) {
3880
+ const libraryTile = buildLibraryButton("tile", state, onLibraryPick);
3881
+ tilesWrap.appendChild(libraryTile);
3882
+ }
3883
+ } else if (!isReadonly && atMax) {
3884
+ const chip = document.createElement("div");
3885
+ chip.className = "fb-tile-counter";
3886
+ chip.textContent = t("filesCounter", state, {
3887
+ count: ridList.length,
3888
+ max: maxCount
3889
+ });
3890
+ tilesWrap.appendChild(chip);
3891
+ }
3892
+ container.appendChild(tilesWrap);
3893
+ const subHint = buildSubHint();
3894
+ if (subHint) {
3895
+ const hintEl = document.createElement("div");
3896
+ hintEl.className = "fb-tile-hint";
3897
+ hintEl.textContent = subHint;
3898
+ container.appendChild(hintEl);
3899
+ }
3053
3900
  }
3054
- function renderFileElement(element, ctx, wrapper, pathKey) {
3055
- var _a, _b;
3901
+ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3902
+ var _a, _b, _c, _d, _e;
3056
3903
  const state = ctx.state;
3057
- if (isElementReadonly(element, state, ctx)) {
3058
- const rawInitial = ctx.prefill[element.key];
3059
- const initial = typeof rawInitial === "string" ? rawInitial : "";
3060
- if (initial) {
3061
- const hiddenInput = document.createElement("input");
3062
- hiddenInput.type = "hidden";
3063
- hiddenInput.name = pathKey;
3064
- hiddenInput.value = initial;
3065
- wrapper.appendChild(hiddenInput);
3066
- renderFilePreviewReadonly(initial, state).then((filePreview) => {
3067
- wrapper.appendChild(filePreview);
3068
- }).catch((err) => {
3069
- console.error("Failed to render file preview:", err);
3070
- const emptyState = document.createElement("div");
3071
- emptyState.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
3072
- emptyState.innerHTML = `<div class="text-center">${escapeHtml(t("previewUnavailable", state))}</div>`;
3073
- wrapper.appendChild(emptyState);
3074
- });
3075
- } else {
3076
- const emptyState = document.createElement("div");
3077
- emptyState.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
3078
- emptyState.innerHTML = `<div class="text-center">${escapeHtml(t("noFileSelected", state))}</div>`;
3079
- wrapper.appendChild(emptyState);
3080
- }
3081
- } else {
3082
- const fileWrapper = document.createElement("div");
3083
- fileWrapper.className = "space-y-2";
3084
- const picker = document.createElement("input");
3085
- picker.type = "file";
3086
- picker.name = pathKey;
3087
- picker.style.display = "none";
3088
- if (element.accept) {
3089
- picker.accept = typeof element.accept === "string" ? element.accept : ((_a = element.accept.extensions) == null ? void 0 : _a.map((ext) => `.${ext}`).join(",")) || "";
3090
- }
3091
- const fileContainer = document.createElement("div");
3092
- fileContainer.className = "file-preview-container w-full aspect-square max-w-xs bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
3093
- const initial = ctx.prefill[element.key];
3094
- const allowedExts = getAllowedExtensions(element.accept);
3095
- const maxSizeMB = (_b = element.maxSize) != null ? _b : Infinity;
3096
- const fileUploadHandler = () => picker.click();
3097
- const dragHandler = (files) => {
3904
+ const fileWrapper = document.createElement("div");
3905
+ fileWrapper.className = "space-y-2";
3906
+ const picker = document.createElement("input");
3907
+ picker.type = "file";
3908
+ picker.name = pathKey;
3909
+ picker.style.display = "none";
3910
+ if (element.accept) {
3911
+ picker.accept = typeof element.accept === "string" ? element.accept : [
3912
+ ...(_b = (_a = element.accept.extensions) == null ? void 0 : _a.map((ext) => `.${ext}`)) != null ? _b : [],
3913
+ ...(_c = element.accept.mime) != null ? _c : []
3914
+ ].join(",") || "";
3915
+ }
3916
+ const fileContainer = document.createElement("div");
3917
+ fileContainer.className = "file-preview-container";
3918
+ const initial = ctx.prefill[element.key];
3919
+ const allowedExts = getAllowedExtensions(element.accept);
3920
+ const allowedMimes = getAllowedMimes(element.accept);
3921
+ const maxSizeMB = (_d = element.maxSize) != null ? _d : Infinity;
3922
+ const handlers = {
3923
+ fileUploadHandler() {
3924
+ picker.click();
3925
+ },
3926
+ dragHandler(files) {
3098
3927
  if (files.length > 0) {
3099
- const deps = { picker, fileUploadHandler, dragHandler };
3100
- handleFileSelect(
3101
- files[0],
3102
- fileContainer,
3103
- pathKey,
3928
+ handleFileSelect({
3929
+ file: files[0],
3930
+ container: fileContainer,
3931
+ fieldName: pathKey,
3104
3932
  state,
3105
- deps,
3106
- ctx.instance,
3107
- allowedExts,
3933
+ deps: buildDeps(),
3934
+ instance: ctx.instance,
3935
+ allowedExtensions: allowedExts,
3936
+ allowedMimes,
3108
3937
  maxSizeMB
3109
- );
3938
+ });
3110
3939
  }
3111
- };
3112
- if (initial) {
3113
- handleInitialFileData(
3114
- initial,
3115
- fileContainer,
3116
- pathKey,
3117
- fileWrapper,
3118
- state,
3119
- {
3120
- picker,
3121
- fileUploadHandler,
3122
- dragHandler
3123
- }
3124
- );
3125
- } else {
3940
+ },
3941
+ setupDrop(container) {
3942
+ setupDragAndDrop(container, handlers.dragHandler);
3943
+ },
3944
+ restoreDropzone() {
3126
3945
  const hint = makeFieldHint(element, state);
3946
+ fileContainer.className = "file-preview-container w-full max-w-md bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
3947
+ fileContainer.style.height = "128px";
3127
3948
  setEmptyFileContainer(fileContainer, state, hint);
3949
+ fileContainer.onclick = handlers.fileUploadHandler;
3950
+ setupDragAndDrop(fileContainer, handlers.dragHandler);
3951
+ },
3952
+ onRemove() {
3953
+ var _a2;
3954
+ const hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
3955
+ const currentRid = hiddenInput == null ? void 0 : hiddenInput.value;
3956
+ if (currentRid) {
3957
+ releaseLocalFileUrl((_a2 = state.resourceIndex.get(currentRid)) == null ? void 0 : _a2.file);
3958
+ }
3959
+ if (hiddenInput) hiddenInput.value = "";
3960
+ handlers.restoreDropzone();
3128
3961
  }
3129
- fileContainer.onclick = fileUploadHandler;
3130
- setupDragAndDrop(fileContainer, dragHandler);
3131
- picker.onchange = () => {
3132
- if (picker.files && picker.files.length > 0) {
3133
- const deps = { picker, fileUploadHandler, dragHandler };
3134
- handleFileSelect(
3135
- picker.files[0],
3962
+ };
3963
+ const buildDeps = () => ({
3964
+ picker,
3965
+ fileUploadHandler: handlers.fileUploadHandler,
3966
+ dragHandler: handlers.dragHandler,
3967
+ setupDrop: handlers.setupDrop,
3968
+ onRemove: handlers.onRemove
3969
+ });
3970
+ const renderEmptySingleState = () => {
3971
+ if (state.config.pickExistingFiles && !element.disableLibrary) {
3972
+ fileContainer.className = "file-preview-container";
3973
+ fileContainer.removeAttribute("style");
3974
+ fileContainer.onclick = null;
3975
+ const row = document.createElement("div");
3976
+ row.className = "fb-file-card-row";
3977
+ row.style.cssText = "display:flex;gap:8px;align-items:stretch;";
3978
+ const hint = makeFieldHint(element, state);
3979
+ const uploadCard = buildEmptyDropzone(
3980
+ state,
3981
+ t("clickDragText", state),
3982
+ hint,
3983
+ handlers.fileUploadHandler
3984
+ );
3985
+ uploadCard.style.cssText = "flex:1;min-width:0;height:128px;";
3986
+ setupDragAndDrop(uploadCard, handlers.dragHandler);
3987
+ const libraryBtn = buildLibraryButton("card", state, () => {
3988
+ handleLibraryPickSingle(
3989
+ state,
3990
+ element,
3136
3991
  fileContainer,
3992
+ fileWrapper,
3137
3993
  pathKey,
3138
- state,
3139
- deps,
3140
- ctx.instance,
3141
- allowedExts,
3142
- maxSizeMB
3143
- );
3144
- }
3145
- };
3146
- fileWrapper.appendChild(fileContainer);
3147
- fileWrapper.appendChild(picker);
3148
- wrapper.appendChild(fileWrapper);
3149
- }
3150
- }
3151
- function renderFilesElement(element, ctx, wrapper, pathKey) {
3152
- var _a, _b;
3153
- const state = ctx.state;
3154
- if (isElementReadonly(element, state, ctx)) {
3155
- const rawPrefill = ctx.prefill[element.key];
3156
- const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
3157
- const filesWrapper = document.createElement("div");
3158
- filesWrapper.className = "space-y-2";
3159
- filesWrapper.dataset.filesWrapper = pathKey;
3160
- const filesList = document.createElement("div");
3161
- filesList.className = "files-list";
3162
- initialFiles.forEach((resourceId) => {
3163
- const pill = document.createElement("div");
3164
- pill.className = "resource-pill";
3165
- pill.dataset.resourceId = resourceId;
3166
- filesList.appendChild(pill);
3167
- });
3168
- filesWrapper.appendChild(filesList);
3169
- wrapper.appendChild(filesWrapper);
3170
- const resultsWrapper = document.createElement("div");
3171
- resultsWrapper.className = "space-y-4";
3172
- if (initialFiles.length > 0) {
3173
- initialFiles.forEach((resourceId) => {
3174
- renderFilePreviewReadonly(resourceId, state).then((filePreview) => {
3175
- resultsWrapper.appendChild(filePreview);
3176
- }).catch((err) => {
3177
- console.error("Failed to render file preview:", err);
3994
+ pathKey,
3995
+ async (rid) => {
3996
+ await renderSingleFileEditTile(fileContainer, rid, state, buildDeps());
3997
+ },
3998
+ ctx.instance
3999
+ ).catch((err) => {
4000
+ console.error("Library pick failed:", err);
3178
4001
  });
3179
4002
  });
4003
+ libraryBtn.style.cssText = "flex:1;min-width:0;";
4004
+ row.appendChild(uploadCard);
4005
+ row.appendChild(libraryBtn);
4006
+ fileContainer.appendChild(row);
3180
4007
  } else {
3181
- resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${escapeHtml(t("noFilesSelected", state))}</div></div>`;
4008
+ handlers.restoreDropzone();
4009
+ }
4010
+ };
4011
+ if (initial) {
4012
+ handleInitialFileData(
4013
+ initial,
4014
+ fileContainer,
4015
+ pathKey,
4016
+ fileWrapper,
4017
+ state,
4018
+ buildDeps()
4019
+ );
4020
+ const prefillMeta = state.resourceIndex.get(initial);
4021
+ if ((_e = prefillMeta == null ? void 0 : prefillMeta.type) == null ? void 0 : _e.startsWith("video/")) {
4022
+ fileContainer.onclick = handlers.fileUploadHandler;
4023
+ setupDragAndDrop(fileContainer, handlers.dragHandler);
3182
4024
  }
3183
- wrapper.appendChild(resultsWrapper);
3184
4025
  } else {
3185
- let updateFilesList2 = function() {
3186
- renderResourcePills(
3187
- list,
3188
- initialFiles,
4026
+ renderEmptySingleState();
4027
+ }
4028
+ picker.onchange = () => {
4029
+ if (picker.files && picker.files.length > 0) {
4030
+ handleFileSelect({
4031
+ file: picker.files[0],
4032
+ container: fileContainer,
4033
+ fieldName: pathKey,
3189
4034
  state,
3190
- (ridToRemove) => {
3191
- const index = initialFiles.indexOf(ridToRemove);
3192
- if (index > -1) {
3193
- initialFiles.splice(index, 1);
3194
- }
3195
- updateFilesList2();
3196
- },
3197
- filesFieldHint
3198
- );
3199
- };
3200
- const filesWrapper = document.createElement("div");
3201
- filesWrapper.className = "space-y-2";
3202
- filesWrapper.dataset.filesWrapper = pathKey;
3203
- const filesPicker = document.createElement("input");
3204
- filesPicker.type = "file";
3205
- filesPicker.name = pathKey;
3206
- filesPicker.multiple = true;
3207
- filesPicker.style.display = "none";
3208
- if (element.accept) {
3209
- filesPicker.accept = typeof element.accept === "string" ? element.accept : ((_a = element.accept.extensions) == null ? void 0 : _a.map((ext) => `.${ext}`).join(",")) || "";
3210
- }
3211
- const filesContainer = document.createElement("div");
3212
- filesContainer.className = "border-2 border-dashed border-gray-300 rounded-lg p-3 hover:border-gray-400 transition-colors";
3213
- const list = document.createElement("div");
3214
- list.className = "files-list";
3215
- const initialFiles = ctx.prefill[element.key] || [];
3216
- addPrefillFilesToIndex(initialFiles, state);
3217
- const filesFieldHint = makeFieldHint(element, state);
3218
- const filesConstraints = {
3219
- maxCount: Infinity,
3220
- allowedExtensions: getAllowedExtensions(element.accept),
3221
- maxSize: (_b = element.maxSize) != null ? _b : Infinity
3222
- };
3223
- updateFilesList2();
3224
- setupFilesDropHandler(
3225
- filesContainer,
3226
- initialFiles,
4035
+ deps: buildDeps(),
4036
+ instance: ctx.instance,
4037
+ allowedExtensions: allowedExts,
4038
+ allowedMimes,
4039
+ maxSizeMB
4040
+ });
4041
+ }
4042
+ };
4043
+ fileWrapper.appendChild(fileContainer);
4044
+ fileWrapper.appendChild(picker);
4045
+ wrapper.appendChild(fileWrapper);
4046
+ }
4047
+ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
4048
+ var _a, _b, _c, _d;
4049
+ const state = ctx.state;
4050
+ const filesWrapper = document.createElement("div");
4051
+ filesWrapper.className = "space-y-2";
4052
+ filesWrapper.dataset.filesWrapper = pathKey;
4053
+ const filesPicker = document.createElement("input");
4054
+ filesPicker.type = "file";
4055
+ filesPicker.name = pathKey;
4056
+ filesPicker.multiple = true;
4057
+ filesPicker.style.display = "none";
4058
+ if (element.accept) {
4059
+ filesPicker.accept = typeof element.accept === "string" ? element.accept : [
4060
+ ...(_b = (_a = element.accept.extensions) == null ? void 0 : _a.map((ext) => `.${ext}`)) != null ? _b : [],
4061
+ ...(_c = element.accept.mime) != null ? _c : []
4062
+ ].join(",") || "";
4063
+ }
4064
+ const filesContainer = document.createElement("div");
4065
+ filesContainer.className = "files-list-wrapper";
4066
+ filesContainer.style.cssText = "border:2px dashed var(--fb-file-upload-border-color,#d1d5db);border-radius:var(--fb-border-radius,0.5rem);padding:8px;transition:border-color var(--fb-transition-duration,200ms),background var(--fb-transition-duration,200ms);";
4067
+ const list = document.createElement("div");
4068
+ list.className = "files-list";
4069
+ const initialFiles = ctx.prefill[element.key] || [];
4070
+ addPrefillFilesToIndex(initialFiles, state.resourceIndex);
4071
+ filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
4072
+ const filesFieldHint = makeFieldHint(element, state);
4073
+ const filesConstraints = {
4074
+ maxCount: Infinity,
4075
+ allowedExtensions: getAllowedExtensions(element.accept),
4076
+ allowedMimes: getAllowedMimes(element.accept),
4077
+ maxSize: (_d = element.maxSize) != null ? _d : Infinity
4078
+ };
4079
+ filesContainer.appendChild(list);
4080
+ filesWrapper.appendChild(filesPicker);
4081
+ filesWrapper.appendChild(filesContainer);
4082
+ wrapper.appendChild(filesWrapper);
4083
+ const onLibraryPickFiles = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4084
+ handleLibraryPickMulti(
3227
4085
  state,
3228
- updateFilesList2,
3229
- filesConstraints,
4086
+ element,
4087
+ filesWrapper,
3230
4088
  pathKey,
3231
- ctx.instance
3232
- );
3233
- setupFilesPickerHandler(
3234
- filesPicker,
3235
4089
  initialFiles,
3236
- state,
3237
- updateFilesList2,
3238
- filesConstraints,
3239
- pathKey,
4090
+ Infinity,
4091
+ updateFilesList,
3240
4092
  ctx.instance
3241
- );
3242
- filesContainer.appendChild(list);
3243
- filesWrapper.appendChild(filesContainer);
3244
- filesWrapper.appendChild(filesPicker);
3245
- wrapper.appendChild(filesWrapper);
4093
+ ).catch((err) => {
4094
+ console.error("Library pick failed:", err);
4095
+ });
4096
+ } : null;
4097
+ function updateFilesList() {
4098
+ const currentlyReadonly = isElementReadonly(element, state);
4099
+ renderResourcePills({
4100
+ container: list,
4101
+ rids: initialFiles,
4102
+ state,
4103
+ onRemove: currentlyReadonly ? null : (ridToRemove) => {
4104
+ var _a2;
4105
+ releaseLocalFileUrl((_a2 = state.resourceIndex.get(ridToRemove)) == null ? void 0 : _a2.file);
4106
+ const index = initialFiles.indexOf(ridToRemove);
4107
+ if (index > -1) initialFiles.splice(index, 1);
4108
+ updateFilesList();
4109
+ },
4110
+ hint: filesFieldHint,
4111
+ isReadonly: currentlyReadonly,
4112
+ onLibraryPick: currentlyReadonly ? null : onLibraryPickFiles
4113
+ });
3246
4114
  }
4115
+ updateFilesList();
4116
+ setupFilesDropHandler(
4117
+ filesContainer,
4118
+ initialFiles,
4119
+ state,
4120
+ updateFilesList,
4121
+ filesConstraints,
4122
+ pathKey,
4123
+ ctx.instance
4124
+ );
4125
+ setupFilesPickerHandler(
4126
+ filesPicker,
4127
+ initialFiles,
4128
+ state,
4129
+ updateFilesList,
4130
+ filesConstraints,
4131
+ pathKey,
4132
+ ctx.instance
4133
+ );
3247
4134
  }
3248
- function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
3249
- var _a, _b, _c, _d;
4135
+ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4136
+ var _a, _b, _c, _d, _e, _f;
3250
4137
  const state = ctx.state;
3251
4138
  const minFiles = (_a = element.minCount) != null ? _a : 0;
3252
4139
  const maxFiles = (_b = element.maxCount) != null ? _b : Infinity;
3253
- if (isElementReadonly(element, state, ctx)) {
3254
- const rawPrefill = ctx.prefill[element.key];
3255
- const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
3256
- const filesWrapper = document.createElement("div");
3257
- filesWrapper.className = "space-y-2";
3258
- filesWrapper.dataset.filesWrapper = pathKey;
3259
- const filesList = document.createElement("div");
3260
- filesList.className = "files-list";
3261
- initialFiles.forEach((resourceId) => {
3262
- const pill = document.createElement("div");
3263
- pill.className = "resource-pill";
3264
- pill.dataset.resourceId = resourceId;
3265
- filesList.appendChild(pill);
3266
- });
3267
- filesWrapper.appendChild(filesList);
3268
- wrapper.appendChild(filesWrapper);
3269
- const resultsWrapper = document.createElement("div");
3270
- resultsWrapper.className = "space-y-4";
3271
- if (initialFiles.length > 0) {
3272
- initialFiles.forEach((resourceId) => {
3273
- renderFilePreviewReadonly(resourceId, state).then((filePreview) => {
3274
- resultsWrapper.appendChild(filePreview);
3275
- }).catch((err) => {
3276
- console.error("Failed to render file preview:", err);
3277
- });
3278
- });
3279
- } else {
3280
- resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${escapeHtml(t("noFilesSelected", state))}</div></div>`;
3281
- }
3282
- wrapper.appendChild(resultsWrapper);
3283
- } else {
3284
- const filesWrapper = document.createElement("div");
3285
- filesWrapper.className = "space-y-2";
3286
- filesWrapper.dataset.filesWrapper = pathKey;
3287
- const filesPicker = document.createElement("input");
3288
- filesPicker.type = "file";
3289
- filesPicker.name = pathKey;
3290
- filesPicker.multiple = true;
3291
- filesPicker.style.display = "none";
3292
- if (element.accept) {
3293
- filesPicker.accept = typeof element.accept === "string" ? element.accept : ((_c = element.accept.extensions) == null ? void 0 : _c.map((ext) => `.${ext}`).join(",")) || "";
3294
- }
3295
- const filesContainer = document.createElement("div");
3296
- filesContainer.className = "files-list space-y-2";
3297
- filesWrapper.appendChild(filesPicker);
3298
- filesWrapper.appendChild(filesContainer);
3299
- const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
3300
- addPrefillFilesToIndex(initialFiles, state);
3301
- const multipleFilesHint = makeFieldHint(element, state);
3302
- const multipleConstraints = {
3303
- maxCount: maxFiles,
3304
- allowedExtensions: getAllowedExtensions(element.accept),
3305
- maxSize: (_d = element.maxSize) != null ? _d : Infinity
3306
- };
3307
- const buildCountInfo = () => {
3308
- const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
3309
- const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
3310
- return countText + minMaxText;
3311
- };
3312
- const updateFilesDisplay = () => {
3313
- renderResourcePills(
3314
- filesContainer,
3315
- initialFiles,
3316
- state,
3317
- (index) => {
3318
- initialFiles.splice(initialFiles.indexOf(index), 1);
3319
- updateFilesDisplay();
3320
- },
3321
- multipleFilesHint,
3322
- buildCountInfo()
3323
- );
3324
- };
3325
- setupFilesDropHandler(
3326
- filesContainer,
3327
- initialFiles,
4140
+ const filesWrapper = document.createElement("div");
4141
+ filesWrapper.className = "space-y-2";
4142
+ filesWrapper.dataset.filesWrapper = pathKey;
4143
+ const filesPicker = document.createElement("input");
4144
+ filesPicker.type = "file";
4145
+ filesPicker.name = pathKey;
4146
+ filesPicker.multiple = true;
4147
+ filesPicker.style.display = "none";
4148
+ if (element.accept) {
4149
+ filesPicker.accept = typeof element.accept === "string" ? element.accept : [
4150
+ ...(_d = (_c = element.accept.extensions) == null ? void 0 : _c.map((ext) => `.${ext}`)) != null ? _d : [],
4151
+ ...(_e = element.accept.mime) != null ? _e : []
4152
+ ].join(",") || "";
4153
+ }
4154
+ const filesContainer = document.createElement("div");
4155
+ filesContainer.className = "files-list-wrapper";
4156
+ filesContainer.style.cssText = "border:2px dashed var(--fb-file-upload-border-color,#d1d5db);border-radius:var(--fb-border-radius,0.5rem);padding:8px;transition:border-color var(--fb-transition-duration,200ms),background var(--fb-transition-duration,200ms);";
4157
+ const list = document.createElement("div");
4158
+ list.className = "files-list";
4159
+ filesWrapper.appendChild(filesPicker);
4160
+ filesWrapper.appendChild(filesContainer);
4161
+ filesContainer.appendChild(list);
4162
+ const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
4163
+ addPrefillFilesToIndex(initialFiles, state.resourceIndex);
4164
+ filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
4165
+ const multipleFilesHint = makeFieldHint(element, state);
4166
+ const multipleConstraints = {
4167
+ maxCount: maxFiles,
4168
+ allowedExtensions: getAllowedExtensions(element.accept),
4169
+ allowedMimes: getAllowedMimes(element.accept),
4170
+ maxSize: (_f = element.maxSize) != null ? _f : Infinity
4171
+ };
4172
+ const buildCountInfo = () => {
4173
+ const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
4174
+ const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
4175
+ return countText + minMaxText;
4176
+ };
4177
+ const onLibraryPickMultiple = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4178
+ handleLibraryPickMulti(
3328
4179
  state,
3329
- updateFilesDisplay,
3330
- multipleConstraints,
4180
+ element,
4181
+ filesWrapper,
3331
4182
  pathKey,
3332
- ctx.instance
3333
- );
3334
- setupFilesPickerHandler(
3335
- filesPicker,
3336
4183
  initialFiles,
3337
- state,
4184
+ maxFiles,
3338
4185
  updateFilesDisplay,
3339
- multipleConstraints,
3340
- pathKey,
3341
4186
  ctx.instance
4187
+ ).catch((err) => {
4188
+ console.error("Library pick failed:", err);
4189
+ });
4190
+ } : null;
4191
+ const updateFilesDisplay = () => {
4192
+ const currentlyReadonly = isElementReadonly(element, state);
4193
+ renderResourcePills({
4194
+ container: list,
4195
+ rids: initialFiles,
4196
+ state,
4197
+ onRemove: currentlyReadonly ? null : (index) => {
4198
+ var _a2;
4199
+ releaseLocalFileUrl((_a2 = state.resourceIndex.get(index)) == null ? void 0 : _a2.file);
4200
+ initialFiles.splice(initialFiles.indexOf(index), 1);
4201
+ updateFilesDisplay();
4202
+ },
4203
+ hint: multipleFilesHint,
4204
+ countInfo: buildCountInfo(),
4205
+ maxCount: maxFiles < Infinity ? maxFiles : void 0,
4206
+ isReadonly: currentlyReadonly,
4207
+ onLibraryPick: currentlyReadonly ? null : onLibraryPickMultiple
4208
+ });
4209
+ };
4210
+ setupFilesDropHandler(
4211
+ filesContainer,
4212
+ initialFiles,
4213
+ state,
4214
+ updateFilesDisplay,
4215
+ multipleConstraints,
4216
+ pathKey,
4217
+ ctx.instance
4218
+ );
4219
+ setupFilesPickerHandler(
4220
+ filesPicker,
4221
+ initialFiles,
4222
+ state,
4223
+ updateFilesDisplay,
4224
+ multipleConstraints,
4225
+ pathKey,
4226
+ ctx.instance
4227
+ );
4228
+ updateFilesDisplay();
4229
+ wrapper.appendChild(filesWrapper);
4230
+ }
4231
+
4232
+ // src/components/file/validate.ts
4233
+ function readMultiFileResourceIds(scopeRoot, fullKey) {
4234
+ const wrapper = scopeRoot.querySelector(
4235
+ `[data-files-wrapper="${fullKey}"]`
4236
+ );
4237
+ if (!wrapper) return [];
4238
+ const encoded = wrapper.dataset.resourceIds;
4239
+ if (encoded === void 0) {
4240
+ throw new Error(
4241
+ `readMultiFileResourceIds: [data-files-wrapper="${fullKey}"] is missing data-resource-ids attribute. This is a render bug.`
3342
4242
  );
3343
- updateFilesDisplay();
3344
- wrapper.appendChild(filesWrapper);
3345
4243
  }
4244
+ const parsed = JSON.parse(encoded);
4245
+ if (!Array.isArray(parsed)) {
4246
+ throw new Error(
4247
+ `readMultiFileResourceIds: data-resource-ids on [data-files-wrapper="${fullKey}"] is not a JSON array. Got: ${encoded}`
4248
+ );
4249
+ }
4250
+ return parsed;
3346
4251
  }
3347
- function validateFileElement(element, key, context) {
3348
- var _a;
3349
- const errors = [];
3350
- const { scopeRoot, skipValidation, path } = context;
3351
- const isMultipleField = element.type === "files" || "multiple" in element && Boolean(element.multiple);
3352
- const validateFileCount = (key2, resourceIds, element2) => {
3353
- var _a2, _b;
3354
- if (skipValidation) return;
3355
- const { state } = context;
3356
- const minFiles = "minCount" in element2 ? (_a2 = element2.minCount) != null ? _a2 : 0 : 0;
3357
- const maxFiles = "maxCount" in element2 ? (_b = element2.maxCount) != null ? _b : Infinity : Infinity;
3358
- if (element2.required && resourceIds.length === 0) {
3359
- errors.push(`${key2}: ${t("required", state)}`);
3360
- }
3361
- if (resourceIds.length < minFiles) {
3362
- errors.push(`${key2}: ${t("minFiles", state, { min: minFiles })}`);
3363
- }
3364
- if (resourceIds.length > maxFiles) {
3365
- errors.push(`${key2}: ${t("maxFiles", state, { max: maxFiles })}`);
4252
+ function validateFileCount(key, resourceIds, element, state, errors) {
4253
+ var _a, _b;
4254
+ const minFiles = "minCount" in element ? (_a = element.minCount) != null ? _a : 0 : 0;
4255
+ const maxFiles = "maxCount" in element ? (_b = element.maxCount) != null ? _b : Infinity : Infinity;
4256
+ if (element.required && resourceIds.length === 0) {
4257
+ errors.push(`${key}: ${t("required", state)}`);
4258
+ }
4259
+ if (resourceIds.length < minFiles) {
4260
+ errors.push(`${key}: ${t("minFiles", state, { min: minFiles })}`);
4261
+ }
4262
+ if (resourceIds.length > maxFiles) {
4263
+ errors.push(`${key}: ${t("maxFiles", state, { max: maxFiles })}`);
4264
+ }
4265
+ }
4266
+ function validateFileTypes(key, resourceIds, element, state, errors) {
4267
+ var _a, _b;
4268
+ const acceptField = "accept" in element ? element.accept : void 0;
4269
+ const allowedExtensions = getAllowedExtensions(acceptField);
4270
+ const allowedMimes = getAllowedMimes(acceptField);
4271
+ if (allowedExtensions.length === 0 && allowedMimes.length === 0) return;
4272
+ const formats = allowedExtensions.join(", ");
4273
+ const mimes = allowedMimes.join(", ");
4274
+ for (const rid of resourceIds) {
4275
+ const meta = state.resourceIndex.get(rid);
4276
+ const fileName = (_a = meta == null ? void 0 : meta.name) != null ? _a : rid;
4277
+ if (allowedExtensions.length > 0 && !isFileExtensionAllowed(fileName, allowedExtensions)) {
4278
+ errors.push(
4279
+ `${key}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
4280
+ );
4281
+ continue;
3366
4282
  }
3367
- };
3368
- const validateFileExtensions = (key2, resourceIds, element2) => {
3369
- var _a2;
3370
- if (skipValidation) return;
3371
- const { state } = context;
3372
- const acceptField = "accept" in element2 ? element2.accept : void 0;
3373
- const allowedExtensions = getAllowedExtensions(acceptField);
3374
- if (allowedExtensions.length === 0) return;
3375
- const formats = allowedExtensions.join(", ");
3376
- for (const rid of resourceIds) {
3377
- const meta = state.resourceIndex.get(rid);
3378
- const fileName = (_a2 = meta == null ? void 0 : meta.name) != null ? _a2 : rid;
3379
- if (!isFileExtensionAllowed(fileName, allowedExtensions)) {
4283
+ if (allowedMimes.length > 0 && !(meta == null ? void 0 : meta.inferredFromExtension)) {
4284
+ const mimeType = (_b = meta == null ? void 0 : meta.type) != null ? _b : "";
4285
+ if (!isMimeAllowed(mimeType, allowedMimes)) {
3380
4286
  errors.push(
3381
- `${key2}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
4287
+ `${key}: ${t("invalidFileMime", state, { name: fileName, type: mimeType, mimes })}`
3382
4288
  );
3383
4289
  }
3384
4290
  }
3385
- };
3386
- const validateFileSizes = (key2, resourceIds, element2) => {
3387
- var _a2;
3388
- if (skipValidation) return;
3389
- const { state } = context;
3390
- const maxSizeMB = "maxSize" in element2 ? (_a2 = element2.maxSize) != null ? _a2 : Infinity : Infinity;
3391
- if (maxSizeMB === Infinity) return;
3392
- for (const rid of resourceIds) {
3393
- const meta = state.resourceIndex.get(rid);
3394
- if (!meta) continue;
3395
- if (meta.size > maxSizeMB * 1024 * 1024) {
3396
- errors.push(
3397
- `${key2}: ${t("fileTooLarge", state, { name: meta.name, maxSize: maxSizeMB })}`
3398
- );
3399
- }
4291
+ }
4292
+ }
4293
+ function validateFileSizes(key, resourceIds, element, state, errors) {
4294
+ var _a;
4295
+ const maxSizeMB = "maxSize" in element ? (_a = element.maxSize) != null ? _a : Infinity : Infinity;
4296
+ if (maxSizeMB === Infinity) return;
4297
+ for (const rid of resourceIds) {
4298
+ const meta = state.resourceIndex.get(rid);
4299
+ if (!meta) continue;
4300
+ if (meta.size > maxSizeMB * 1024 * 1024) {
4301
+ errors.push(
4302
+ `${key}: ${t("fileTooLarge", state, { name: meta.name, maxSize: maxSizeMB })}`
4303
+ );
3400
4304
  }
3401
- };
4305
+ }
4306
+ }
4307
+ function validateMultiFile(element, key, context) {
4308
+ const { scopeRoot, skipValidation, path, state } = context;
4309
+ const errors = [];
4310
+ const fullKey = pathJoin(path, key);
4311
+ const resourceIds = readMultiFileResourceIds(scopeRoot, fullKey);
4312
+ if (!skipValidation) {
4313
+ validateFileCount(key, resourceIds, element, state, errors);
4314
+ validateFileTypes(key, resourceIds, element, state, errors);
4315
+ validateFileSizes(key, resourceIds, element, state, errors);
4316
+ }
4317
+ return { value: resourceIds, errors };
4318
+ }
4319
+ function validateSingleFile(element, key, context) {
4320
+ var _a;
4321
+ const { scopeRoot, skipValidation, state } = context;
4322
+ const errors = [];
4323
+ const input = scopeRoot.querySelector(
4324
+ `input[name$="${key}"][type="hidden"]`
4325
+ );
4326
+ const rid = (_a = input == null ? void 0 : input.value) != null ? _a : "";
4327
+ if (!skipValidation && element.required && rid === "") {
4328
+ errors.push(`${key}: ${t("required", state)}`);
4329
+ return { value: null, errors };
4330
+ }
4331
+ if (!skipValidation && rid !== "") {
4332
+ validateFileTypes(key, [rid], element, state, errors);
4333
+ validateFileSizes(key, [rid], element, state, errors);
4334
+ }
4335
+ return { value: rid || null, errors };
4336
+ }
4337
+ function validateFileElement(element, key, context) {
4338
+ const isMultipleField = element.type === "files" || "multiple" in element && Boolean(element.multiple);
3402
4339
  if (isMultipleField) {
3403
- const fullKey = pathJoin(path, key);
3404
- const filesWrapper = scopeRoot.querySelector(
3405
- `[data-files-wrapper="${fullKey}"]`
3406
- );
3407
- const container = (filesWrapper == null ? void 0 : filesWrapper.querySelector(".files-list")) || null;
3408
- const resourceIds = [];
3409
- if (container) {
3410
- const pills = container.querySelectorAll(".resource-pill");
3411
- pills.forEach((pill) => {
3412
- const resourceId = pill.dataset.resourceId;
3413
- if (resourceId) {
3414
- resourceIds.push(resourceId);
3415
- }
3416
- });
3417
- }
3418
- validateFileCount(key, resourceIds, element);
3419
- validateFileExtensions(key, resourceIds, element);
3420
- validateFileSizes(key, resourceIds, element);
3421
- return { value: resourceIds, errors };
4340
+ return validateMultiFile(element, key, context);
4341
+ }
4342
+ return validateSingleFile(element, key, context);
4343
+ }
4344
+
4345
+ // src/components/file/render-readonly.ts
4346
+ function renderFileElementReadonly(element, ctx, wrapper, pathKey) {
4347
+ const state = ctx.state;
4348
+ const rawInitial = ctx.prefill[element.key];
4349
+ const initial = typeof rawInitial === "string" ? rawInitial : "";
4350
+ if (initial) {
4351
+ addPrefillFilesToIndex([initial], state.resourceIndex);
4352
+ const hiddenInput = document.createElement("input");
4353
+ hiddenInput.type = "hidden";
4354
+ hiddenInput.name = pathKey;
4355
+ hiddenInput.value = initial;
4356
+ wrapper.appendChild(hiddenInput);
4357
+ renderFilePreviewReadonly(initial, state).then((filePreview) => {
4358
+ wrapper.appendChild(filePreview);
4359
+ }).catch((err) => {
4360
+ console.error("Failed to render file preview:", err);
4361
+ wrapper.appendChild(buildEmptyReadonlyTile(state));
4362
+ });
3422
4363
  } else {
3423
- const input = scopeRoot.querySelector(
3424
- `input[name$="${key}"][type="hidden"]`
3425
- );
3426
- const rid = (_a = input == null ? void 0 : input.value) != null ? _a : "";
3427
- if (!skipValidation && element.required && rid === "") {
3428
- errors.push(`${key}: ${t("required", context.state)}`);
3429
- return { value: null, errors };
3430
- }
3431
- if (!skipValidation && rid !== "") {
3432
- validateFileExtensions(key, [rid], element);
3433
- validateFileSizes(key, [rid], element);
3434
- }
3435
- return { value: rid || null, errors };
4364
+ wrapper.appendChild(buildEmptyReadonlyTile(state));
4365
+ }
4366
+ }
4367
+ function buildEmptyReadonlyTile(state) {
4368
+ const emptyState = document.createElement("div");
4369
+ emptyState.style.cssText = `
4370
+ width:${TILE_SIZE};
4371
+ height:${TILE_SIZE};
4372
+ display:flex;
4373
+ align-items:center;
4374
+ justify-content:center;
4375
+ background:var(--fb-file-upload-bg-color,#f3f4f6);
4376
+ border-radius:var(--fb-border-radius,0.5rem);
4377
+ border:1px solid var(--fb-file-upload-border-color,#d1d5db);
4378
+ `;
4379
+ emptyState.innerHTML = `<div style="font-size:11px;text-align:center;color:var(--fb-text-secondary-color,#6b7280);">${escapeHtml(t("noFileSelected", state))}</div>`;
4380
+ return emptyState;
4381
+ }
4382
+ function renderMultiFileReadonly(rids, state, wrapper, pathKey, marginTop) {
4383
+ addPrefillFilesToIndex(rids, state.resourceIndex);
4384
+ const filesWrapper = document.createElement("div");
4385
+ filesWrapper.dataset.filesWrapper = pathKey;
4386
+ filesWrapper.dataset.resourceIds = JSON.stringify(rids);
4387
+ wrapper.appendChild(filesWrapper);
4388
+ if (rids.length === 0) {
4389
+ const emptyEl = document.createElement("div");
4390
+ emptyEl.className = "fb-tile-empty-text";
4391
+ emptyEl.textContent = t("noFilesSelected", state);
4392
+ filesWrapper.appendChild(emptyEl);
4393
+ return;
4394
+ }
4395
+ const tilesWrap = document.createElement("div");
4396
+ tilesWrap.style.cssText = `display:flex;flex-wrap:wrap;gap:6px;${marginTop ? `margin-top:${marginTop};` : ""}`;
4397
+ filesWrapper.appendChild(tilesWrap);
4398
+ const placeholders = rids.map(() => {
4399
+ const placeholder = document.createElement("div");
4400
+ placeholder.style.cssText = `width:${TILE_SIZE};height:${TILE_SIZE};`;
4401
+ tilesWrap.appendChild(placeholder);
4402
+ return placeholder;
4403
+ });
4404
+ for (let i = 0; i < rids.length; i++) {
4405
+ const resourceId = rids[i];
4406
+ const placeholder = placeholders[i];
4407
+ renderFilePreviewReadonly(resourceId, state).then((tileEl) => {
4408
+ placeholder.replaceWith(tileEl);
4409
+ }).catch((err) => {
4410
+ console.error("Failed to render readonly tile:", err);
4411
+ });
4412
+ }
4413
+ }
4414
+ function renderFilesElementReadonly(element, ctx, wrapper, pathKey) {
4415
+ const rawPrefill = ctx.prefill[element.key];
4416
+ const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
4417
+ renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey);
4418
+ }
4419
+ function renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey) {
4420
+ const rawPrefill = ctx.prefill[element.key];
4421
+ const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
4422
+ renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey, "4px");
4423
+ }
4424
+
4425
+ // src/components/file.ts
4426
+ function renderFileElement(element, ctx, wrapper, pathKey) {
4427
+ if (isElementReadonly(element, ctx.state, ctx)) {
4428
+ renderFileElementReadonly(element, ctx, wrapper, pathKey);
4429
+ } else {
4430
+ renderFileElementEdit(element, ctx, wrapper, pathKey);
4431
+ }
4432
+ }
4433
+ function renderFilesElement(element, ctx, wrapper, pathKey) {
4434
+ if (isElementReadonly(element, ctx.state, ctx)) {
4435
+ renderFilesElementReadonly(element, ctx, wrapper, pathKey);
4436
+ } else {
4437
+ renderFilesElementEdit(element, ctx, wrapper, pathKey);
4438
+ }
4439
+ }
4440
+ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
4441
+ if (isElementReadonly(element, ctx.state, ctx)) {
4442
+ renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey);
4443
+ } else {
4444
+ renderMultipleFileElementEdit(element, ctx, wrapper, pathKey);
3436
4445
  }
3437
4446
  }
3438
4447
  function updateFileField(element, fieldPath, value, context) {
3439
- var _a;
3440
4448
  const { scopeRoot, state } = context;
3441
- if ("multiple" in element && element.multiple) {
4449
+ if (element.type === "files" || "multiple" in element && element.multiple) {
3442
4450
  if (!Array.isArray(value)) {
3443
4451
  console.warn(
3444
4452
  `updateFileField: Expected array for multiple file field "${fieldPath}", got ${typeof value}`
@@ -3446,32 +4454,20 @@ function updateFileField(element, fieldPath, value, context) {
3446
4454
  return;
3447
4455
  }
3448
4456
  value.forEach((resourceId) => {
3449
- var _a2;
3450
4457
  if (resourceId && typeof resourceId === "string") {
3451
- if (!state.resourceIndex.has(resourceId)) {
3452
- const filename = resourceId.split("/").pop() || "file";
3453
- const extension = (_a2 = filename.split(".").pop()) == null ? void 0 : _a2.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(resourceId, {
3463
- name: filename,
3464
- type: fileType,
3465
- size: 0,
3466
- uploadedAt: /* @__PURE__ */ new Date(),
3467
- file: void 0
3468
- });
3469
- }
4458
+ seedInferredResource(resourceId, state.resourceIndex);
3470
4459
  }
3471
4460
  });
3472
- console.info(
3473
- `updateFileField: Multiple file field "${fieldPath}" updated. Preview update requires re-render.`
4461
+ const filesWrapper = scopeRoot.querySelector(
4462
+ `[data-files-wrapper="${fieldPath}"]`
3474
4463
  );
4464
+ if (filesWrapper) {
4465
+ filesWrapper.dataset.resourceIds = JSON.stringify(value);
4466
+ } else {
4467
+ console.warn(
4468
+ `updateFileField: [data-files-wrapper="${fieldPath}"] not found in DOM; data-resource-ids not updated`
4469
+ );
4470
+ }
3475
4471
  } else {
3476
4472
  const hiddenInput = scopeRoot.querySelector(
3477
4473
  `input[name="${fieldPath}"][type="hidden"]`
@@ -3484,25 +4480,7 @@ function updateFileField(element, fieldPath, value, context) {
3484
4480
  }
3485
4481
  hiddenInput.value = value != null ? String(value) : "";
3486
4482
  if (value && typeof value === "string") {
3487
- if (!state.resourceIndex.has(value)) {
3488
- const filename = value.split("/").pop() || "file";
3489
- const extension = (_a = filename.split(".").pop()) == null ? void 0 : _a.toLowerCase();
3490
- let fileType = "application/octet-stream";
3491
- if (extension) {
3492
- if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
3493
- fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
3494
- } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
3495
- fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
3496
- }
3497
- }
3498
- state.resourceIndex.set(value, {
3499
- name: filename,
3500
- type: fileType,
3501
- size: 0,
3502
- uploadedAt: /* @__PURE__ */ new Date(),
3503
- file: void 0
3504
- });
3505
- }
4483
+ seedInferredResource(value, state.resourceIndex);
3506
4484
  console.info(
3507
4485
  `updateFileField: File field "${fieldPath}" updated. Preview update requires re-render.`
3508
4486
  );
@@ -8245,6 +9223,7 @@ var defaultConfig = {
8245
9223
  enableFilePreview: true,
8246
9224
  maxPreviewSize: "200px",
8247
9225
  readonly: false,
9226
+ pickExistingFiles: null,
8248
9227
  parseTableFile: null,
8249
9228
  locale: "en",
8250
9229
  translations: {
@@ -8256,6 +9235,8 @@ var defaultConfig = {
8256
9235
  noFileSelected: "No file selected",
8257
9236
  noFilesSelected: "No files selected",
8258
9237
  downloadButton: "Download",
9238
+ downloadFile: "Download",
9239
+ openInNewTab: "Open in new tab",
8259
9240
  changeButton: "Change",
8260
9241
  placeholderText: "Enter text",
8261
9242
  previewAlt: "Preview",
@@ -8277,6 +9258,12 @@ var defaultConfig = {
8277
9258
  fileCountSingle: "{count} file",
8278
9259
  fileCountPlural: "{count} files",
8279
9260
  fileCountRange: "({min}-{max})",
9261
+ uploadingFile: "Uploading\u2026",
9262
+ filesCounter: "{count}/{max}",
9263
+ fromLibrary: "From library",
9264
+ libraryEmpty: "Library is empty",
9265
+ libraryHint: "Choose from previously uploaded files",
9266
+ pickerError: "Failed to load files from library",
8280
9267
  // Validation errors
8281
9268
  required: "Required",
8282
9269
  minItems: "Minimum {min} items required",
@@ -8292,6 +9279,7 @@ var defaultConfig = {
8292
9279
  minFiles: "Minimum {min} files required",
8293
9280
  maxFiles: "Maximum {max} files allowed",
8294
9281
  invalidFileExtension: 'File "{name}" has unsupported format. Allowed: {formats}',
9282
+ invalidFileMime: 'File "{name}": file type {type} not allowed (allowed: {mimes})',
8295
9283
  fileTooLarge: 'File "{name}" exceeds maximum size of {maxSize}MB',
8296
9284
  filesLimitExceeded: "{skipped} file(s) skipped: maximum {max} files allowed",
8297
9285
  unsupportedFieldType: "Unsupported field type: {type}",
@@ -8318,6 +9306,8 @@ var defaultConfig = {
8318
9306
  noFileSelected: "\u0424\u0430\u0439\u043B \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D",
8319
9307
  noFilesSelected: "\u041D\u0435\u0442 \u0444\u0430\u0439\u043B\u043E\u0432",
8320
9308
  downloadButton: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C",
9309
+ downloadFile: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C",
9310
+ openInNewTab: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0432 \u043D\u043E\u0432\u043E\u0439 \u0432\u043A\u043B\u0430\u0434\u043A\u0435",
8321
9311
  changeButton: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C",
8322
9312
  placeholderText: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442",
8323
9313
  previewAlt: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440",
@@ -8339,6 +9329,12 @@ var defaultConfig = {
8339
9329
  fileCountSingle: "{count} \u0444\u0430\u0439\u043B",
8340
9330
  fileCountPlural: "{count} \u0444\u0430\u0439\u043B\u043E\u0432",
8341
9331
  fileCountRange: "({min}-{max})",
9332
+ uploadingFile: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026",
9333
+ filesCounter: "{count}/{max}",
9334
+ fromLibrary: "\u0418\u0437 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438",
9335
+ libraryEmpty: "\u0411\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0430 \u043F\u0443\u0441\u0442\u0430",
9336
+ 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",
9337
+ 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",
8342
9338
  // Validation errors
8343
9339
  required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
8344
9340
  minItems: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
@@ -8354,6 +9350,7 @@ var defaultConfig = {
8354
9350
  minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
8355
9351
  maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
8356
9352
  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}',
9353
+ 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})',
8357
9354
  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',
8358
9355
  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",
8359
9356
  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}",