@dmitryvim/form-builder 0.2.30 → 0.2.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -46,8 +46,9 @@ function addRangeHint(element, parts, state) {
46
46
  }
47
47
  }
48
48
  function addFileSizeHint(element, parts, state) {
49
- if (element.maxSizeMB) {
50
- parts.push(t("hintMaxSize", state, { size: element.maxSizeMB }));
49
+ const sizeMB = element.maxSize ?? element.maxSizeMB;
50
+ if (sizeMB && sizeMB !== Infinity) {
51
+ parts.push(t("hintMaxSize", state, { size: sizeMB }));
51
52
  }
52
53
  }
53
54
  function addFormatHint(element, parts, state) {
@@ -120,6 +121,25 @@ function validateSchema(schema) {
120
121
  });
121
122
  }
122
123
  }
124
+ function validateContainerProps(element, elementPath, errors2) {
125
+ if ("columns" in element && element.columns !== void 0) {
126
+ const columns = element.columns;
127
+ const validColumns = [1, 2, 3, 4];
128
+ if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
129
+ errors2.push(
130
+ `${elementPath}: columns must be 1, 2, 3, or 4 (got ${columns})`
131
+ );
132
+ }
133
+ }
134
+ if ("displayMode" in element && element.displayMode !== void 0) {
135
+ const displayMode = element.displayMode;
136
+ if (displayMode !== "stack" && displayMode !== "slides") {
137
+ errors2.push(
138
+ `${elementPath}: displayMode must be "stack" or "slides" (got ${JSON.stringify(displayMode)})`
139
+ );
140
+ }
141
+ }
142
+ }
123
143
  function checkFlatOutputCollisions(elements, scopePath) {
124
144
  const allOutputKeys = /* @__PURE__ */ new Set();
125
145
  for (const el of elements) {
@@ -199,15 +219,7 @@ function validateSchema(schema) {
199
219
  validateElements(element.elements, `${elementPath}.elements`);
200
220
  }
201
221
  if (element.type === "container" && element.elements) {
202
- if ("columns" in element && element.columns !== void 0) {
203
- const columns = element.columns;
204
- const validColumns = [1, 2, 3, 4];
205
- if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
206
- errors.push(
207
- `${elementPath}: columns must be 1, 2, 3, or 4 (got ${columns})`
208
- );
209
- }
210
- }
222
+ validateContainerProps(element, elementPath, errors);
211
223
  if ("prefillHints" in element && element.prefillHints) {
212
224
  const prefillHints = element.prefillHints;
213
225
  if (Array.isArray(prefillHints)) {
@@ -840,8 +852,12 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
840
852
  const textareaWrapper = document.createElement("div");
841
853
  textareaWrapper.style.cssText = "position: relative;";
842
854
  const textareaInput = document.createElement("textarea");
843
- textareaInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
844
- textareaInput.style.cssText = "padding-bottom: 24px;";
855
+ textareaInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
856
+ textareaInput.style.cssText = `
857
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x) 24px var(--fb-input-padding-x);
858
+ font-size: var(--fb-font-size);
859
+ font-family: var(--fb-font-family);
860
+ `;
845
861
  textareaInput.name = pathKey;
846
862
  textareaInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
847
863
  textareaInput.rows = element.rows || 4;
@@ -893,8 +909,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
893
909
  const textareaContainer = document.createElement("div");
894
910
  textareaContainer.style.cssText = "position: relative;";
895
911
  const textareaInput = document.createElement("textarea");
896
- textareaInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
897
- textareaInput.style.cssText = "padding-bottom: 24px;";
912
+ textareaInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
913
+ textareaInput.style.cssText = `
914
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x) 24px var(--fb-input-padding-x);
915
+ font-size: var(--fb-font-size);
916
+ font-family: var(--fb-font-family);
917
+ `;
898
918
  textareaInput.placeholder = element.placeholder || t("placeholderText", state);
899
919
  textareaInput.rows = element.rows || 4;
900
920
  textareaInput.value = value;
@@ -1080,8 +1100,14 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1080
1100
  inputWrapper.style.cssText = "position: relative;";
1081
1101
  const numberInput = document.createElement("input");
1082
1102
  numberInput.type = "number";
1083
- numberInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1084
- numberInput.style.cssText = "padding-right: 60px; width: 100%; box-sizing: border-box;";
1103
+ numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1104
+ numberInput.style.cssText = `
1105
+ padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
1106
+ font-size: var(--fb-font-size);
1107
+ font-family: var(--fb-font-family);
1108
+ width: 100%;
1109
+ box-sizing: border-box;
1110
+ `;
1085
1111
  numberInput.name = pathKey;
1086
1112
  numberInput.placeholder = element.placeholder || "0";
1087
1113
  if (element.min !== void 0) numberInput.min = element.min.toString();
@@ -1133,8 +1159,14 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1133
1159
  inputContainer.style.cssText = "position: relative; flex: 1;";
1134
1160
  const numberInput = document.createElement("input");
1135
1161
  numberInput.type = "number";
1136
- numberInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1137
- numberInput.style.cssText = "padding-right: 60px; width: 100%; box-sizing: border-box;";
1162
+ numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1163
+ numberInput.style.cssText = `
1164
+ padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
1165
+ font-size: var(--fb-font-size);
1166
+ font-family: var(--fb-font-family);
1167
+ width: 100%;
1168
+ box-sizing: border-box;
1169
+ `;
1138
1170
  numberInput.placeholder = element.placeholder || "0";
1139
1171
  if (element.min !== void 0) numberInput.min = element.min.toString();
1140
1172
  if (element.max !== void 0) numberInput.max = element.max.toString();
@@ -1405,7 +1437,12 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
1405
1437
  const state = ctx.state;
1406
1438
  const readonly = isElementReadonly(element, state, ctx);
1407
1439
  const selectInput = document.createElement("select");
1408
- selectInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1440
+ selectInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1441
+ selectInput.style.cssText = `
1442
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
1443
+ font-size: var(--fb-font-size);
1444
+ font-family: var(--fb-font-family);
1445
+ `;
1409
1446
  selectInput.name = pathKey;
1410
1447
  selectInput.disabled = readonly;
1411
1448
  (element.options || []).forEach((option) => {
@@ -1457,7 +1494,12 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1457
1494
  const itemWrapper = document.createElement("div");
1458
1495
  itemWrapper.className = "multiple-select-item flex items-center gap-2";
1459
1496
  const selectInput = document.createElement("select");
1460
- selectInput.className = "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1497
+ selectInput.className = "flex-1 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1498
+ selectInput.style.cssText = `
1499
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
1500
+ font-size: var(--fb-font-size);
1501
+ font-family: var(--fb-font-family);
1502
+ `;
1461
1503
  selectInput.disabled = readonly;
1462
1504
  (element.options || []).forEach((option) => {
1463
1505
  const optionElement = document.createElement("option");
@@ -2192,7 +2234,13 @@ function ensureFileStyles() {
2192
2234
  style.textContent = `
2193
2235
  @keyframes fb-spin { to { transform: rotate(360deg); } }
2194
2236
 
2195
- /* Spinner used during single-file and multi-file upload */
2237
+ /* \u2500\u2500\u2500 Checker background utility \u2500\u2500\u2500 */
2238
+ /* Neutral diagonal-stripe background for image previews (never crops) */
2239
+ .fb-checker {
2240
+ background-image: repeating-linear-gradient(45deg, #fafafa 0 6px, #f3f4f6 6px 12px);
2241
+ }
2242
+
2243
+ /* \u2500\u2500\u2500 Spinner \u2500\u2500\u2500 */
2196
2244
  .fb-spinner {
2197
2245
  width: 36px;
2198
2246
  height: 36px;
@@ -2203,207 +2251,271 @@ function ensureFileStyles() {
2203
2251
  flex-shrink: 0;
2204
2252
  }
2205
2253
 
2206
- /* Base tile: fixed 160\xD7160 square, theme-aware background */
2207
- .fb-tile {
2208
- width: var(--fb-tile-size, 160px);
2209
- height: var(--fb-tile-size, 160px);
2210
- flex-shrink: 0;
2211
- position: relative;
2254
+ /* \u2500\u2500\u2500 Wide single-file add tile (empty state) \u2500\u2500\u2500 */
2255
+ .fb-wide-tile {
2256
+ width: 100%;
2257
+ border-radius: 0.75rem;
2258
+ border: 1px dashed #60a5fa;
2259
+ background: rgba(239,246,255,0.5);
2260
+ display: flex;
2212
2261
  overflow: hidden;
2213
- border-radius: var(--fb-border-radius, 0.5rem);
2214
- background: var(--fb-file-upload-bg-color, #f3f4f6);
2262
+ height: 180px;
2263
+ transition: border-color 150ms, background 150ms, box-shadow 150ms;
2264
+ cursor: pointer;
2215
2265
  }
2216
-
2217
- /* Uploaded resource tile \u2014 adds a visible border */
2218
- .fb-tile-resource {
2219
- border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
2266
+ .fb-wide-tile:hover {
2267
+ background: #eff6ff;
2220
2268
  }
2221
-
2222
- /* Uploading placeholder tile \u2014 dashed border, uploading indicator */
2223
- .fb-tile-uploading {
2224
- border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2269
+ .fb-wide-tile.fb-drag-over {
2270
+ border-color: #3b82f6;
2271
+ border-width: 2px;
2272
+ background: #eff6ff;
2273
+ box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
2225
2274
  }
2226
2275
 
2227
- /* "+" add-more tile */
2228
- .fb-tile-add {
2229
- border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2276
+ /* Upload zone inside wide tile */
2277
+ .fb-wide-tile-upload {
2278
+ flex: 1;
2230
2279
  display: flex;
2280
+ flex-direction: column;
2231
2281
  align-items: center;
2232
2282
  justify-content: center;
2283
+ gap: 8px;
2284
+ color: #2563eb;
2285
+ padding: 16px;
2286
+ transition: background 150ms;
2233
2287
  cursor: pointer;
2234
- font-size: 32px;
2235
- color: var(--fb-file-upload-text-color, #9ca3af);
2236
- transition:
2237
- border-color var(--fb-transition-duration, 200ms),
2238
- color var(--fb-transition-duration, 200ms);
2288
+ background: transparent;
2289
+ border: none;
2290
+ font-family: inherit;
2239
2291
  }
2240
- .fb-tile-add:hover {
2241
- border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2242
- color: var(--fb-text-color, #1f2937);
2292
+ .fb-wide-tile-upload:hover {
2293
+ background: rgba(191,219,254,0.25);
2243
2294
  }
2244
2295
 
2245
- /* Count chip shown when at maxCount */
2246
- .fb-tile-counter {
2247
- font-size: 11px;
2248
- color: var(--fb-text-secondary-color, #6b7280);
2249
- background: var(--fb-file-upload-bg-color, #f3f4f6);
2250
- border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
2251
- border-radius: 4px;
2252
- padding: 2px 6px;
2253
- align-self: flex-end;
2254
- margin-bottom: 4px;
2296
+ /* Vertical dashed divider between upload and library zones */
2297
+ .fb-wide-tile-divider {
2298
+ width: 1px;
2299
+ margin: 16px 0;
2300
+ border-left: 1px dashed rgba(96,165,250,0.5);
2301
+ background: transparent;
2302
+ flex-shrink: 0;
2255
2303
  }
2256
2304
 
2257
- /* Empty-state dropzone */
2258
- .fb-file-dropzone {
2259
- width: 100%;
2260
- height: 128px;
2261
- border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2262
- border-radius: var(--fb-border-radius, 0.5rem);
2305
+ /* Library zone inside wide tile */
2306
+ .fb-wide-tile-library {
2307
+ width: 176px;
2308
+ flex-shrink: 0;
2263
2309
  display: flex;
2264
2310
  flex-direction: column;
2265
2311
  align-items: center;
2266
2312
  justify-content: center;
2267
- gap: 4px;
2313
+ gap: 8px;
2314
+ color: #2563eb;
2315
+ padding: 12px;
2316
+ transition: background 150ms;
2268
2317
  cursor: pointer;
2269
- transition:
2270
- border-color var(--fb-transition-duration, 200ms),
2271
- background var(--fb-transition-duration, 200ms);
2318
+ background: transparent;
2319
+ border: none;
2320
+ font-family: inherit;
2272
2321
  }
2273
- .fb-file-dropzone:hover {
2274
- border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2275
- background: var(--fb-background-hover-color, #f9fafb);
2322
+ .fb-wide-tile-library:hover {
2323
+ background: rgba(191,219,254,0.25);
2276
2324
  }
2277
2325
 
2278
- /* Inline text inside tiles */
2279
- .fb-tile-label {
2280
- font-size: 9px;
2281
- color: var(--fb-text-secondary-color, #6b7280);
2282
- text-align: center;
2283
- overflow: hidden;
2284
- word-break: break-all;
2285
- max-height: 28px;
2326
+ /* \u2500\u2500\u2500 Multi-file outer grid container \u2500\u2500\u2500 */
2327
+ .fb-multi-outer {
2328
+ border-radius: 0.75rem;
2329
+ border: 1px dashed #cbd5e1;
2330
+ background: rgba(248,250,252,0.4);
2331
+ padding: 12px;
2332
+ transition: border-color 150ms, background 150ms, box-shadow 150ms;
2333
+ }
2334
+ .fb-multi-outer.fb-drag-over {
2335
+ border-width: 2px;
2336
+ border-color: #3b82f6;
2337
+ background: rgba(239,246,255,0.4);
2338
+ box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
2286
2339
  }
2287
- .fb-tile-uploading-text {
2288
- font-size: 8px;
2289
- color: var(--fb-file-upload-text-color, #9ca3af);
2340
+
2341
+ /* With files present: white solid border */
2342
+ .fb-multi-outer.fb-multi-has-files {
2343
+ border-style: solid;
2344
+ border-color: #e2e8f0;
2345
+ background: #fff;
2290
2346
  }
2291
- .fb-tile-hint {
2292
- font-size: 11px;
2293
- color: var(--fb-file-upload-text-color, #9ca3af);
2294
- margin-top: 4px;
2347
+
2348
+ /* The CSS grid inside */
2349
+ .fb-multi-grid {
2350
+ display: grid;
2351
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
2352
+ gap: 10px;
2295
2353
  }
2296
- .fb-tile-empty-text {
2297
- font-size: 12px;
2298
- color: var(--fb-text-secondary-color, #6b7280);
2299
- padding: 4px 0;
2354
+
2355
+ /* \u2500\u2500\u2500 Multi square add-tile (combined upload + library) \u2500\u2500\u2500 */
2356
+ .fb-multi-add-tile {
2357
+ aspect-ratio: 1 / 1;
2358
+ border-radius: 0.5rem;
2359
+ border: 1px dashed #60a5fa;
2360
+ background: rgba(239,246,255,0.5);
2361
+ display: flex;
2362
+ flex-direction: column;
2363
+ overflow: hidden;
2364
+ transition: background 150ms;
2300
2365
  }
2301
- .fb-dropzone-primary-text {
2302
- font-size: 13px;
2303
- color: var(--fb-text-secondary-color, #6b7280);
2366
+ .fb-multi-add-tile:hover {
2367
+ background: #eff6ff;
2304
2368
  }
2305
- .fb-dropzone-hint-text {
2306
- font-size: 11px;
2307
- color: var(--fb-file-upload-text-color, #9ca3af);
2369
+ .fb-multi-add-tile.fb-drag-over-tile {
2370
+ border-width: 2px;
2371
+ border-color: #3b82f6;
2372
+ background: rgba(255,255,255,0.8);
2308
2373
  }
2309
2374
 
2310
- /* Hover overlay + X-button on resource tiles */
2311
- .fb-tile-overlay {
2312
- position: absolute;
2313
- inset: 0;
2314
- background: transparent;
2315
- transition: background var(--fb-transition-duration, 200ms);
2316
- display: flex;
2317
- align-items: flex-start;
2318
- justify-content: flex-end;
2319
- }
2320
- .fb-tile-resource:hover .fb-tile-overlay {
2321
- background: var(--fb-tile-hover-overlay-color, rgba(0,0,0,0.4));
2322
- }
2323
- .fb-tile-x-btn {
2324
- margin: 3px;
2325
- width: 18px;
2326
- height: 18px;
2327
- background: var(--fb-error-color, #ef4444);
2328
- color: var(--fb-file-bg-color, #fff);
2329
- border: none;
2330
- border-radius: 50%;
2331
- font-size: 11px;
2332
- line-height: 1;
2333
- cursor: pointer;
2375
+ /* Upload half of add-tile */
2376
+ .fb-multi-add-upload {
2377
+ flex: 1;
2334
2378
  display: flex;
2379
+ flex-direction: column;
2335
2380
  align-items: center;
2336
2381
  justify-content: center;
2337
- opacity: 0;
2338
- transition: opacity var(--fb-transition-duration, 200ms);
2382
+ gap: 4px;
2383
+ color: #2563eb;
2384
+ cursor: pointer;
2385
+ background: transparent;
2386
+ border: none;
2387
+ font-family: inherit;
2388
+ width: 100%;
2389
+ transition: background 150ms;
2339
2390
  }
2340
- .fb-tile-resource:hover .fb-tile-x-btn {
2341
- opacity: 1;
2391
+ .fb-multi-add-upload:hover {
2392
+ background: rgba(191,219,254,0.35);
2342
2393
  }
2343
2394
 
2344
- /* Video play button overlay (readonly tiles with video thumbnails) */
2345
- .fb-video-overlay {
2346
- position: absolute;
2347
- inset: 0;
2348
- display: flex;
2349
- align-items: center;
2350
- justify-content: center;
2351
- background: var(--fb-tile-hover-overlay-color, rgba(0,0,0,0.25));
2395
+ /* Horizontal dashed divider inside add-tile */
2396
+ .fb-multi-add-divider {
2397
+ border-top: 1px dashed rgba(96,165,250,0.5);
2398
+ margin: 0;
2399
+ flex-shrink: 0;
2352
2400
  }
2353
- .fb-play-btn {
2354
- background: var(--fb-file-bg-color, rgba(255,255,255,0.9));
2355
- border-radius: 50%;
2401
+
2402
+ /* Library strip at bottom of add-tile */
2403
+ .fb-multi-add-library {
2404
+ padding: 6px 0;
2356
2405
  display: flex;
2357
2406
  align-items: center;
2358
2407
  justify-content: center;
2408
+ gap: 4px;
2409
+ color: #2563eb;
2410
+ font-size: 11px;
2411
+ font-weight: 500;
2412
+ cursor: pointer;
2413
+ background: transparent;
2414
+ border: none;
2415
+ font-family: inherit;
2416
+ width: 100%;
2417
+ transition: background 150ms;
2418
+ flex-shrink: 0;
2419
+ }
2420
+ .fb-multi-add-library:hover {
2421
+ background: rgba(191,219,254,0.35);
2359
2422
  }
2360
2423
 
2361
- /* Edit-mode local video preview wrapper */
2362
- .fb-video-preview-wrap {
2424
+ /* \u2500\u2500\u2500 Capacity placeholder squares \u2500\u2500\u2500 */
2425
+ .fb-multi-placeholder {
2426
+ aspect-ratio: 1 / 1;
2427
+ border-radius: 0.5rem;
2428
+ border: 1px solid #e2e8f0;
2429
+ }
2430
+ .fb-multi-placeholder.fb-drag-over {
2431
+ border-width: 2px;
2432
+ border-style: dashed;
2433
+ border-color: #93c5fd;
2434
+ background: rgba(219,234,254,0.6);
2435
+ }
2436
+
2437
+ /* \u2500\u2500\u2500 Filled preview tile \u2500\u2500\u2500 */
2438
+ .fb-preview-tile {
2439
+ aspect-ratio: 1 / 1;
2440
+ border-radius: 0.5rem;
2441
+ border: 1px solid #e2e8f0;
2442
+ overflow: hidden;
2363
2443
  position: relative;
2444
+ cursor: pointer;
2445
+ }
2446
+ .fb-preview-tile img {
2364
2447
  width: 100%;
2365
2448
  height: 100%;
2449
+ object-fit: contain;
2450
+ display: block;
2366
2451
  }
2367
2452
 
2368
- /* Hover overlay for edit-mode local video (Remove / Change buttons) */
2369
- .fb-video-btn-overlay {
2370
- position: absolute;
2371
- top: 8px;
2372
- right: 8px;
2373
- z-index: 10;
2453
+ /* \u2500\u2500\u2500 Uploading placeholder tile \u2500\u2500\u2500 */
2454
+ .fb-uploading-tile {
2455
+ aspect-ratio: 1 / 1;
2456
+ border-radius: 0.5rem;
2457
+ border: 2px dashed #d1d5db;
2374
2458
  display: flex;
2375
- gap: 4px;
2376
- opacity: 0;
2377
- transition: opacity var(--fb-transition-duration, 200ms);
2378
- pointer-events: none;
2459
+ flex-direction: column;
2460
+ align-items: center;
2461
+ justify-content: center;
2462
+ gap: 6px;
2463
+ padding: 6px;
2379
2464
  }
2380
- .fb-video-preview-wrap:hover .fb-video-btn-overlay {
2381
- opacity: 1;
2382
- pointer-events: auto;
2465
+
2466
+ /* \u2500\u2500\u2500 Meta line below multi grid \u2500\u2500\u2500 */
2467
+ .fb-meta-line {
2468
+ margin-top: 10px;
2469
+ display: flex;
2470
+ align-items: center;
2471
+ justify-content: space-between;
2472
+ gap: 8px;
2473
+ flex-wrap: wrap;
2383
2474
  }
2384
- .fb-video-btn {
2385
- border: none;
2386
- border-radius: var(--fb-border-radius, 4px);
2387
- font-size: 11px;
2388
- padding: 4px 8px;
2389
- cursor: pointer;
2390
- color: #fff;
2391
- line-height: 1.2;
2475
+ .fb-meta-text {
2476
+ font-size: 12px;
2477
+ color: #94a3b8;
2478
+ display: flex;
2479
+ align-items: center;
2480
+ gap: 8px;
2481
+ flex-wrap: wrap;
2392
2482
  }
2393
- .fb-video-btn-delete {
2394
- background: rgba(220, 38, 38, 0.85);
2483
+ .fb-meta-dot {
2484
+ width: 4px;
2485
+ height: 4px;
2486
+ border-radius: 50%;
2487
+ background: #cbd5e1;
2488
+ flex-shrink: 0;
2395
2489
  }
2396
- .fb-video-btn-delete:hover {
2397
- background: rgba(185, 28, 28, 0.95);
2490
+ .fb-meta-mono {
2491
+ font-family: ui-monospace, 'JetBrains Mono', monospace;
2492
+ font-size: 11px;
2493
+ letter-spacing: -0.02em;
2398
2494
  }
2399
- .fb-video-btn-change {
2400
- background: rgba(31, 41, 55, 0.85);
2495
+ .fb-clear-all-btn {
2496
+ font-size: 12px;
2497
+ color: #94a3b8;
2498
+ background: none;
2499
+ border: none;
2500
+ cursor: pointer;
2501
+ padding: 0;
2502
+ font-family: inherit;
2503
+ transition: color 150ms;
2504
+ white-space: nowrap;
2505
+ flex-shrink: 0;
2401
2506
  }
2402
- .fb-video-btn-change:hover {
2403
- background: rgba(17, 24, 39, 0.95);
2507
+ .fb-clear-all-btn:hover {
2508
+ color: #dc2626;
2404
2509
  }
2405
2510
 
2406
- /* Tile action icon buttons (download / open / remove) \u2014 shown on tile hover */
2511
+ /* \u2500\u2500\u2500 Empty text (readonly) \u2500\u2500\u2500 */
2512
+ .fb-tile-empty-text {
2513
+ font-size: 11px;
2514
+ color: var(--fb-text-secondary-color, #6b7280);
2515
+ padding: 4px 0;
2516
+ }
2517
+
2518
+ /* \u2500\u2500\u2500 Tile action buttons (for zoom popup, compat) \u2500\u2500\u2500 */
2407
2519
  .fb-tile-actions {
2408
2520
  position: absolute;
2409
2521
  top: 3px;
@@ -2415,37 +2527,35 @@ function ensureFileStyles() {
2415
2527
  transition: opacity var(--fb-transition-duration, 200ms);
2416
2528
  z-index: 10;
2417
2529
  }
2418
- .fb-tile-resource:hover .fb-tile-actions {
2530
+ .fb-preview-tile:hover .fb-tile-actions {
2419
2531
  opacity: 1;
2420
2532
  }
2421
2533
  .fb-tile-action-btn {
2422
- width: 28px;
2423
- height: 28px;
2534
+ width: 24px;
2535
+ height: 24px;
2424
2536
  display: flex;
2425
2537
  align-items: center;
2426
2538
  justify-content: center;
2427
- border: none;
2428
- border-radius: 50%;
2539
+ border: 1px solid rgba(15,23,42,0.08);
2540
+ border-radius: 0.375rem;
2429
2541
  cursor: pointer;
2430
- background: rgba(31, 41, 55, 0.75);
2431
- color: #fff;
2542
+ background: rgba(255,255,255,0.92);
2543
+ color: #374151;
2432
2544
  padding: 0;
2433
2545
  flex-shrink: 0;
2434
- transition:
2435
- background var(--fb-transition-duration, 200ms),
2436
- opacity var(--fb-transition-duration, 200ms);
2546
+ box-shadow: 0 1px 2px rgba(0,0,0,0.06);
2547
+ transition: background var(--fb-transition-duration, 200ms),
2548
+ color var(--fb-transition-duration, 200ms);
2437
2549
  }
2438
2550
  .fb-tile-action-btn:hover {
2439
- background: rgba(17, 24, 39, 0.95);
2440
- }
2441
- .fb-tile-action-remove {
2442
- background: rgba(220, 38, 38, 0.8);
2551
+ background: #ffffff;
2552
+ color: #0f172a;
2443
2553
  }
2444
2554
  .fb-tile-action-remove:hover {
2445
- background: rgba(185, 28, 28, 0.95);
2555
+ color: #dc2626;
2446
2556
  }
2447
2557
 
2448
- /* Actions row inside zoom popup \u2014 always visible while popup is shown */
2558
+ /* Zoom popup action buttons always visible */
2449
2559
  .fb-tile-zoom-preview .fb-tile-actions {
2450
2560
  position: absolute;
2451
2561
  top: 6px;
@@ -2454,116 +2564,145 @@ function ensureFileStyles() {
2454
2564
  z-index: 10000;
2455
2565
  }
2456
2566
 
2457
- /* Two-card empty-state layout (upload card + library card) */
2458
- .fb-file-card-row {
2459
- display: flex;
2460
- gap: 8px;
2461
- align-items: stretch;
2567
+ /* \u2500\u2500\u2500 Hover zoom preview popup \u2500\u2500\u2500 */
2568
+ .fb-tile-zoom-preview {
2569
+ position: fixed;
2570
+ z-index: 9999;
2571
+ background: var(--fb-background-color, #fff);
2572
+ border: 1px solid #e2e8f0;
2573
+ border-radius: 0.5rem;
2574
+ box-shadow: 0 4px 16px rgba(0,0,0,0.18);
2575
+ padding: 4px;
2576
+ width: 350px;
2577
+ height: 350px;
2578
+ pointer-events: none;
2579
+ opacity: 0;
2580
+ transition: opacity 150ms ease;
2462
2581
  }
2463
- .fb-file-card-row .fb-file-dropzone,
2464
- .fb-file-card-row .fb-file-library-card {
2465
- flex: 1;
2466
- min-width: 0;
2582
+ .fb-tile-zoom-preview.fb-tile-zoom-preview--visible {
2583
+ opacity: 1;
2584
+ }
2585
+ .fb-tile-zoom-preview-img {
2586
+ width: 100%;
2587
+ height: 100%;
2588
+ object-fit: contain;
2589
+ display: block;
2590
+ border-radius: calc(0.5rem - 2px);
2467
2591
  }
2468
2592
 
2469
- /* Library picker card \u2014 mirrors .fb-file-dropzone styling */
2470
- .fb-file-library-card {
2471
- height: 128px;
2472
- border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2473
- border-radius: var(--fb-border-radius, 0.5rem);
2593
+ /* \u2500\u2500\u2500 Single-file uploading state \u2500\u2500\u2500 */
2594
+ .fb-single-uploading {
2595
+ height: 180px;
2596
+ border-radius: 0.75rem;
2597
+ border: 1px dashed #60a5fa;
2598
+ background: rgba(239,246,255,0.5);
2474
2599
  display: flex;
2475
2600
  flex-direction: column;
2476
2601
  align-items: center;
2477
2602
  justify-content: center;
2478
- gap: 4px;
2479
- cursor: pointer;
2480
- background: none;
2481
- padding: 0;
2482
- transition:
2483
- border-color var(--fb-transition-duration, 200ms),
2484
- background var(--fb-transition-duration, 200ms);
2485
- width: 100%;
2603
+ gap: 8px;
2486
2604
  }
2487
- .fb-file-library-card:hover,
2488
- .fb-file-library-card:focus-visible {
2489
- border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2490
- background: var(--fb-background-hover-color, #f9fafb);
2491
- outline: none;
2605
+
2606
+ /* \u2500\u2500\u2500 Video overlays \u2500\u2500\u2500 */
2607
+ .fb-video-overlay {
2608
+ position: absolute;
2609
+ inset: 0;
2610
+ display: flex;
2611
+ align-items: center;
2612
+ justify-content: center;
2613
+ background: rgba(0,0,0,0.25);
2492
2614
  }
2493
- .fb-file-library-card-icon {
2494
- font-size: 24px;
2495
- line-height: 1;
2496
- flex-shrink: 0;
2615
+ .fb-play-btn {
2616
+ background: rgba(255,255,255,0.9);
2617
+ border-radius: 50%;
2618
+ display: flex;
2619
+ align-items: center;
2620
+ justify-content: center;
2497
2621
  }
2498
- .fb-file-library-card-label {
2499
- font-size: 13px;
2500
- color: var(--fb-text-secondary-color, #6b7280);
2622
+ .fb-video-preview-wrap {
2623
+ position: relative;
2624
+ width: 100%;
2625
+ height: 100%;
2626
+ }
2627
+ .fb-video-btn-overlay {
2628
+ position: absolute;
2629
+ top: 8px;
2630
+ right: 8px;
2631
+ z-index: 10;
2632
+ display: flex;
2633
+ gap: 4px;
2634
+ opacity: 0;
2635
+ transition: opacity 150ms;
2636
+ pointer-events: none;
2637
+ }
2638
+ .fb-video-preview-wrap:hover .fb-video-btn-overlay {
2639
+ opacity: 1;
2640
+ pointer-events: auto;
2501
2641
  }
2502
- .fb-file-library-card-hint {
2642
+ .fb-video-btn {
2643
+ border: none;
2644
+ border-radius: 4px;
2503
2645
  font-size: 11px;
2504
- color: var(--fb-file-upload-text-color, #9ca3af);
2646
+ padding: 4px 8px;
2647
+ cursor: pointer;
2648
+ color: #fff;
2649
+ line-height: 1.2;
2505
2650
  }
2651
+ .fb-video-btn-delete { background: rgba(220,38,38,0.85); }
2652
+ .fb-video-btn-delete:hover { background: rgba(185,28,28,0.95); }
2653
+ .fb-video-btn-change { background: rgba(31,41,55,0.85); }
2654
+ .fb-video-btn-change:hover { background: rgba(17,24,39,0.95); }
2506
2655
 
2507
- /* Library "\u{1F4DA}" add-tile \u2014 same size/style as the "+" add tile */
2508
- .fb-tile-add-library {
2509
- border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2510
- display: flex;
2511
- align-items: center;
2512
- justify-content: center;
2513
- cursor: pointer;
2514
- font-size: 24px;
2515
- color: var(--fb-file-upload-text-color, #9ca3af);
2516
- transition:
2517
- border-color var(--fb-transition-duration, 200ms),
2518
- color var(--fb-transition-duration, 200ms);
2519
- background: none;
2520
- padding: 0;
2521
- width: var(--fb-tile-size, 160px);
2522
- height: var(--fb-tile-size, 160px);
2523
- flex-shrink: 0;
2524
- position: relative;
2656
+ /* \u2500\u2500\u2500 Readonly readonly tile \u2500\u2500\u2500 */
2657
+ .fb-readonly-tile {
2658
+ aspect-ratio: 1 / 1;
2659
+ border-radius: 0.5rem;
2660
+ border: 1px solid #e2e8f0;
2525
2661
  overflow: hidden;
2526
- border-radius: var(--fb-border-radius, 0.5rem);
2662
+ position: relative;
2663
+ cursor: pointer;
2527
2664
  }
2528
- .fb-tile-add-library:hover,
2529
- .fb-tile-add-library:focus-visible {
2530
- border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2531
- color: var(--fb-text-color, #1f2937);
2532
- outline: none;
2665
+ .fb-readonly-tile img {
2666
+ width: 100%;
2667
+ height: 100%;
2668
+ object-fit: contain;
2669
+ display: block;
2533
2670
  }
2534
-
2535
- /* Hover zoom preview popup for image tiles \u2014 appended to document.body (fixed) */
2536
- .fb-tile-zoom-preview {
2537
- position: fixed;
2538
- z-index: 9999;
2539
- background: var(--fb-background-color, #fff);
2540
- border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
2541
- border-radius: var(--fb-border-radius, 0.5rem);
2542
- box-shadow: 0 4px 16px rgba(0,0,0,0.18);
2543
- padding: 4px;
2544
- width: 350px;
2545
- height: 350px;
2546
- pointer-events: none;
2671
+ .fb-readonly-tile .fb-tile-actions {
2547
2672
  opacity: 0;
2548
- transition: opacity 150ms ease;
2549
2673
  }
2550
- .fb-tile-zoom-preview.fb-tile-zoom-preview--visible {
2674
+ .fb-readonly-tile:hover .fb-tile-actions {
2551
2675
  opacity: 1;
2552
2676
  }
2553
- .fb-tile-zoom-preview-img {
2677
+
2678
+ /* \u2500\u2500\u2500 Readonly single-file filled \u2500\u2500\u2500 */
2679
+ .fb-single-readonly-filled {
2680
+ position: relative;
2681
+ border-radius: 0.75rem;
2682
+ border: 1px solid #e2e8f0;
2683
+ overflow: hidden;
2684
+ height: 220px;
2685
+ display: block;
2686
+ cursor: pointer;
2687
+ }
2688
+ .fb-single-readonly-filled img {
2554
2689
  width: 100%;
2555
2690
  height: 100%;
2556
2691
  object-fit: contain;
2557
2692
  display: block;
2558
- background: var(--fb-file-upload-bg-color, #f3f4f6);
2559
- border-radius: calc(var(--fb-border-radius, 0.5rem) - 2px);
2693
+ }
2694
+
2695
+ /* \u2500\u2500\u2500 Readonly multi grid \u2500\u2500\u2500 */
2696
+ .fb-multi-readonly-grid {
2697
+ display: grid;
2698
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
2699
+ gap: 10px;
2560
2700
  }
2561
2701
  `;
2562
2702
  document.head.appendChild(style);
2563
2703
  }
2564
2704
 
2565
2705
  // src/components/file/dom.ts
2566
- var TILE_SIZE = "160px";
2567
2706
  function createFileTile() {
2568
2707
  ensureFileStyles();
2569
2708
  const tile = document.createElement("div");
@@ -2571,7 +2710,7 @@ function createFileTile() {
2571
2710
  return tile;
2572
2711
  }
2573
2712
  function showFileError(container, message) {
2574
- const existing = container.closest(".space-y-2")?.querySelector(".file-error-message");
2713
+ const existing = container.closest("[data-files-wrapper]")?.querySelector(".file-error-message");
2575
2714
  if (existing) existing.remove();
2576
2715
  const errorEl = document.createElement("div");
2577
2716
  errorEl.className = "file-error-message error-message";
@@ -2581,10 +2720,10 @@ function showFileError(container, message) {
2581
2720
  margin-top: 0.25rem;
2582
2721
  `;
2583
2722
  errorEl.textContent = message;
2584
- container.closest(".space-y-2")?.appendChild(errorEl);
2723
+ container.closest("[data-files-wrapper]")?.appendChild(errorEl);
2585
2724
  }
2586
2725
  function clearFileError(container) {
2587
- const existing = container.closest(".space-y-2")?.querySelector(".file-error-message");
2726
+ const existing = container.closest("[data-files-wrapper]")?.querySelector(".file-error-message");
2588
2727
  if (existing) existing.remove();
2589
2728
  }
2590
2729
  function addDeleteButton(container, state, onDelete) {
@@ -2602,13 +2741,6 @@ function addDeleteButton(container, state, onDelete) {
2602
2741
  overlay.appendChild(deleteBtn);
2603
2742
  container.appendChild(overlay);
2604
2743
  }
2605
- function findFilePicker(container) {
2606
- let el = container.parentElement;
2607
- while (el && !el.dataset.filesWrapper) {
2608
- el = el.parentElement;
2609
- }
2610
- return el?.querySelector('input[type="file"]') ?? null;
2611
- }
2612
2744
  function createUploadingTile(fileName, state) {
2613
2745
  ensureFileStyles();
2614
2746
  const tile = createFileTile();
@@ -2624,10 +2756,13 @@ function createUploadingTile(fileName, state) {
2624
2756
  return tile;
2625
2757
  }
2626
2758
  function ensureTilesWrap(list) {
2759
+ const existingGrid = list.querySelector(".fb-multi-grid");
2760
+ if (existingGrid) return existingGrid;
2627
2761
  const existing = list.querySelector(".fb-tiles-wrap");
2628
2762
  if (existing) return existing;
2629
- const dropzone = list.querySelector(".fb-file-dropzone");
2630
- if (dropzone) dropzone.remove();
2763
+ list.querySelector(".fb-file-dropzone")?.remove();
2764
+ list.querySelector(".fb-wide-tile")?.remove();
2765
+ list.querySelector(".fb-multi-outer")?.remove();
2631
2766
  const tilesWrap = document.createElement("div");
2632
2767
  tilesWrap.className = "fb-tiles-wrap";
2633
2768
  tilesWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start;";
@@ -2639,7 +2774,7 @@ function ensureTilesWrap(list) {
2639
2774
  return tilesWrap;
2640
2775
  }
2641
2776
  function setEmptyFileContainer(fileContainer, state, hint) {
2642
- const hintHtml = hint ? `<div class="text-xs text-gray-500 mt-1">${escapeHtml(hint)}</div>` : "";
2777
+ const hintHtml = "";
2643
2778
  fileContainer.innerHTML = `
2644
2779
  <div class="flex flex-col items-center justify-center h-full text-gray-400">
2645
2780
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
@@ -2681,9 +2816,11 @@ function setupDragAndDrop(element, dropHandler) {
2681
2816
  }
2682
2817
 
2683
2818
  // src/components/file/preview.ts
2684
- 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>`;
2685
- 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>`;
2686
- 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>`;
2819
+ var ICON_DOWNLOAD = `<svg width="16" height="16" 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>`;
2820
+ var ICON_OPEN = `<svg width="16" height="16" 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>`;
2821
+ var ICON_REMOVE = `<svg width="16" height="16" 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>`;
2822
+ var ICON_REPLACE = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 18a4 4 0 000-8 6 6 0 00-11.5 2A4 4 0 006 20h11M12 12v7M12 12l-3 3M12 12l3 3"/></svg>`;
2823
+ var ICON_LIBRARY = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4h4v16H4z"/><path d="M10 4h4v16h-4z"/><path d="M16 5l3.5 1-3 14L13 19"/></svg>`;
2687
2824
  function canDownload(state, meta) {
2688
2825
  return Boolean(
2689
2826
  state.config.downloadFile || state.config.getDownloadUrl || state.config.getThumbnail || meta?.file
@@ -2695,7 +2832,16 @@ function canOpenInTab(state, meta) {
2695
2832
  );
2696
2833
  }
2697
2834
  function createTileActions(options) {
2698
- const { canRemove, removeHandler, state, resourceId, fileName, meta } = options;
2835
+ const {
2836
+ canRemove,
2837
+ removeHandler,
2838
+ state,
2839
+ resourceId,
2840
+ fileName,
2841
+ meta,
2842
+ replaceHandler,
2843
+ libraryHandler
2844
+ } = options;
2699
2845
  const group = document.createElement("div");
2700
2846
  group.className = "fb-tile-actions";
2701
2847
  const makeBtn = (icon, label, cls) => {
@@ -2710,6 +2856,16 @@ function createTileActions(options) {
2710
2856
  });
2711
2857
  return btn;
2712
2858
  };
2859
+ if (replaceHandler) {
2860
+ const replaceBtn = makeBtn(ICON_REPLACE, t("replaceFile", state), "fb-tile-action-replace");
2861
+ replaceBtn.addEventListener("click", () => replaceHandler());
2862
+ group.appendChild(replaceBtn);
2863
+ }
2864
+ if (libraryHandler) {
2865
+ const libBtn = makeBtn(ICON_LIBRARY, t("fromLibrary", state), "fb-tile-action-library");
2866
+ libBtn.addEventListener("click", () => libraryHandler());
2867
+ group.appendChild(libBtn);
2868
+ }
2713
2869
  if (canDownload(state, meta)) {
2714
2870
  const dlBtn = makeBtn(ICON_DOWNLOAD, t("downloadFile", state), "fb-tile-action-download");
2715
2871
  dlBtn.addEventListener("click", () => {
@@ -2895,8 +3051,7 @@ function attachClonedActionListeners(cloned, original) {
2895
3051
  }
2896
3052
  function renderLocalImagePreview(container, file, fileName, state) {
2897
3053
  const img = document.createElement("img");
2898
- img.className = "w-full h-full object-contain";
2899
- img.style.background = "var(--fb-file-upload-bg-color,#f3f4f6)";
3054
+ img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
2900
3055
  img.alt = fileName || t("previewAlt", state);
2901
3056
  const reader = new FileReader();
2902
3057
  reader.onload = (e) => {
@@ -2918,7 +3073,7 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
2918
3073
  const newContainer = setupDragDropless(container);
2919
3074
  newContainer.innerHTML = `
2920
3075
  <div class="fb-video-preview-wrap">
2921
- <video class="w-full h-full object-contain" controls preload="auto" muted src="${videoUrl}">
3076
+ <video style="width:100%;height:100%;object-fit:contain;" controls preload="auto" muted src="${videoUrl}">
2922
3077
  ${escapeHtml(t("videoNotSupported", state))}
2923
3078
  </video>
2924
3079
  <div class="fb-video-btn-overlay">
@@ -2962,11 +3117,11 @@ function handleVideoDelete(container, resourceId, state, deps) {
2962
3117
  container.onclick = deps.fileUploadHandler;
2963
3118
  }
2964
3119
  container.innerHTML = `
2965
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
2966
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
3120
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);">
3121
+ <svg style="width:1.5rem;height:1.5rem;margin-bottom:0.5rem;" fill="currentColor" viewBox="0 0 24 24">
2967
3122
  <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"/>
2968
3123
  </svg>
2969
- <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
3124
+ <div style="font-size:0.875rem;text-align:center;">${escapeHtml(t("clickDragText", state))}</div>
2970
3125
  </div>
2971
3126
  `;
2972
3127
  if (deps?.setupDrop) {
@@ -2983,11 +3138,11 @@ function renderDeleteButton(container, resourceId, state) {
2983
3138
  hiddenInput.value = "";
2984
3139
  }
2985
3140
  container.innerHTML = `
2986
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
2987
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
3141
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);">
3142
+ <svg style="width:1.5rem;height:1.5rem;margin-bottom:0.5rem;" fill="currentColor" viewBox="0 0 24 24">
2988
3143
  <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"/>
2989
3144
  </svg>
2990
- <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
3145
+ <div style="font-size:0.875rem;text-align:center;">${escapeHtml(t("clickDragText", state))}</div>
2991
3146
  </div>
2992
3147
  `;
2993
3148
  });
@@ -3006,7 +3161,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
3006
3161
  deps
3007
3162
  );
3008
3163
  } else {
3009
- 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>`;
3164
+ container.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);"><div style="font-size:36px;margin-bottom:0.5rem;">\u{1F4C1}</div><div style="font-size:0.875rem;">${escapeHtml(fileName)}</div></div>`;
3010
3165
  }
3011
3166
  if (!isReadonly && !meta.type?.startsWith("video/")) {
3012
3167
  renderDeleteButton(container, resourceId, state);
@@ -3014,7 +3169,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
3014
3169
  }
3015
3170
  function renderUploadedVideoPreview(container, thumbnailUrl, state) {
3016
3171
  const video = document.createElement("video");
3017
- video.className = "w-full h-full object-contain";
3172
+ video.style.cssText = "width:100%;height:100%;object-fit:contain;";
3018
3173
  video.controls = true;
3019
3174
  video.preload = "metadata";
3020
3175
  video.muted = true;
@@ -3035,8 +3190,7 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
3035
3190
  renderUploadedVideoPreview(container, thumbnailUrl, state);
3036
3191
  } else {
3037
3192
  const img = document.createElement("img");
3038
- img.className = "w-full h-full object-contain";
3039
- img.style.background = "var(--fb-file-upload-bg-color,#f3f4f6)";
3193
+ img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
3040
3194
  img.alt = fileName || t("previewAlt", state);
3041
3195
  img.src = thumbnailUrl;
3042
3196
  container.appendChild(img);
@@ -3047,11 +3201,11 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
3047
3201
  } catch (error) {
3048
3202
  console.error("Failed to get thumbnail:", error);
3049
3203
  container.innerHTML = `
3050
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
3051
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
3204
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);">
3205
+ <svg style="width:1.5rem;height:1.5rem;margin-bottom:0.5rem;" fill="currentColor" viewBox="0 0 24 24">
3052
3206
  <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"/>
3053
3207
  </svg>
3054
- <div class="text-sm text-center">${escapeHtml(fileName || t("previewUnavailable", state))}</div>
3208
+ <div style="font-size:0.875rem;text-align:center;">${escapeHtml(fileName || t("previewUnavailable", state))}</div>
3055
3209
  </div>
3056
3210
  `;
3057
3211
  }
@@ -3225,13 +3379,9 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
3225
3379
  const img = document.createElement("img");
3226
3380
  img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
3227
3381
  img.alt = meta.name;
3228
- const reader = new FileReader();
3229
- reader.onload = (e) => {
3230
- img.src = e.target?.result || "";
3231
- attachZoomHover(tile, img.src, meta.name, actionsEl ?? null);
3232
- };
3233
- reader.readAsDataURL(meta.file);
3382
+ img.src = getLocalFileUrl(meta.file);
3234
3383
  tile.appendChild(img);
3384
+ attachZoomHover(tile, img.src, meta.name, actionsEl ?? null);
3235
3385
  } else if (state.config.getThumbnail) {
3236
3386
  try {
3237
3387
  const url = await state.config.getThumbnail(rid);
@@ -3288,17 +3438,20 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
3288
3438
  }
3289
3439
  if (actionsEl) tile.appendChild(actionsEl);
3290
3440
  } else {
3291
- const name = meta?.name ?? "";
3292
- const hasExtension = name.includes(".");
3293
- const captionHtml = hasExtension ? `<div class="fb-tile-label">${escapeHtml(name.length > 10 ? name.substring(0, 8) + "\u2026" : name)}</div>` : "";
3294
- tile.innerHTML = `
3295
- <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
3296
- <div style="font-size:36px;">\u{1F4C1}</div>
3297
- ${captionHtml}
3298
- </div>`;
3299
- if (actionsEl) tile.appendChild(actionsEl);
3441
+ fillDocumentFallback(tile, rid, meta, actionsEl);
3300
3442
  }
3301
3443
  }
3444
+ function fillDocumentFallback(tile, rid, meta, actionsEl) {
3445
+ const fileName = meta?.name ?? rid.split("/").pop() ?? "";
3446
+ if (fileName) tile.title = fileName;
3447
+ const labelHtml = fileName ? `<div style="font-size:11px;line-height:1.2;text-align:center;color:var(--fb-text-secondary-color,#6b7280);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(fileName)}</div>` : "";
3448
+ tile.innerHTML = `
3449
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
3450
+ <div style="font-size:36px;">\u{1F4C1}</div>
3451
+ ${labelHtml}
3452
+ </div>`;
3453
+ if (actionsEl) tile.appendChild(actionsEl);
3454
+ }
3302
3455
  async function forceDownload(resourceId, fileName, state) {
3303
3456
  try {
3304
3457
  let fileUrl = null;
@@ -3398,6 +3551,10 @@ async function handleFileSelect(opts) {
3398
3551
  return;
3399
3552
  }
3400
3553
  clearFileError(container);
3554
+ const existingHiddenInput = container.parentElement?.querySelector(
3555
+ 'input[type="hidden"]'
3556
+ );
3557
+ const previousRid = existingHiddenInput?.value || null;
3401
3558
  ensureFileStyles();
3402
3559
  container.innerHTML = `
3403
3560
  <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:6px;padding:6px;">
@@ -3408,7 +3565,13 @@ async function handleFileSelect(opts) {
3408
3565
  try {
3409
3566
  rid = await uploadSingleFile(file, state);
3410
3567
  } catch (error) {
3411
- setEmptyFileContainer(container, state);
3568
+ if (previousRid && deps?.onAfterUpload) {
3569
+ deps.onAfterUpload(container, previousRid);
3570
+ } else if (deps?.onRemove) {
3571
+ deps.onRemove();
3572
+ } else {
3573
+ setEmptyFileContainer(container, state);
3574
+ }
3412
3575
  throw error;
3413
3576
  }
3414
3577
  state.resourceIndex.set(rid, {
@@ -3418,9 +3581,10 @@ async function handleFileSelect(opts) {
3418
3581
  uploadedAt: /* @__PURE__ */ new Date(),
3419
3582
  file
3420
3583
  });
3421
- let hiddenInput = container.parentElement?.querySelector(
3422
- 'input[type="hidden"]'
3423
- );
3584
+ if (previousRid && previousRid !== rid) {
3585
+ releaseLocalFileUrl(state.resourceIndex.get(previousRid)?.file);
3586
+ }
3587
+ let hiddenInput = existingHiddenInput;
3424
3588
  if (!hiddenInput) {
3425
3589
  hiddenInput = document.createElement("input");
3426
3590
  hiddenInput.type = "hidden";
@@ -3429,7 +3593,9 @@ async function handleFileSelect(opts) {
3429
3593
  }
3430
3594
  hiddenInput.value = rid;
3431
3595
  const isVideo = file.type.startsWith("video/");
3432
- if (!isVideo && deps) {
3596
+ if (!isVideo && deps?.onAfterUpload) {
3597
+ deps.onAfterUpload(container, rid);
3598
+ } else if (!isVideo && deps) {
3433
3599
  renderSingleFileEditTile(container, rid, state, deps).catch(console.error);
3434
3600
  } else {
3435
3601
  renderFilePreview(container, rid, state, {
@@ -3490,17 +3656,17 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
3490
3656
  return { accepted, errorMessage: errorParts.join(" \u2022 ") };
3491
3657
  }
3492
3658
  async function uploadBatch(accepted, resourceIds, listEl, state) {
3659
+ if (listEl) {
3660
+ const tilesWrap = ensureTilesWrap(listEl);
3661
+ const addTile = tilesWrap.querySelector(".fb-multi-add-tile-js") ?? tilesWrap.querySelector(".fb-tile-add");
3662
+ if (addTile) addTile.style.display = "none";
3663
+ }
3493
3664
  await Promise.all(
3494
3665
  accepted.map(async (file) => {
3495
3666
  const placeholder = createUploadingTile(file.name, state);
3496
3667
  if (listEl) {
3497
3668
  const tilesWrap = ensureTilesWrap(listEl);
3498
- const addTile = tilesWrap.querySelector(".fb-tile-add");
3499
- if (addTile) {
3500
- tilesWrap.insertBefore(placeholder, addTile);
3501
- } else {
3502
- tilesWrap.appendChild(placeholder);
3503
- }
3669
+ tilesWrap.appendChild(placeholder);
3504
3670
  }
3505
3671
  try {
3506
3672
  const rid = await uploadSingleFile(file, state);
@@ -3542,7 +3708,7 @@ function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallbac
3542
3708
  function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback, constraints, pathKey, instance) {
3543
3709
  filesPicker.onchange = async () => {
3544
3710
  if (!filesPicker.files) return;
3545
- const wrapperEl = filesPicker.closest(".space-y-2") || filesPicker.parentElement;
3711
+ const wrapperEl = filesPicker.closest("[data-files-wrapper]") || filesPicker.parentElement;
3546
3712
  const { accepted, errorMessage } = filterAndSlice(
3547
3713
  Array.from(filesPicker.files),
3548
3714
  resourceIds.length,
@@ -3707,6 +3873,10 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
3707
3873
  hiddenInput.name = pathKey;
3708
3874
  fileWrapper.appendChild(hiddenInput);
3709
3875
  }
3876
+ const previousRid = hiddenInput.value || null;
3877
+ if (previousRid && previousRid !== first.resourceId) {
3878
+ releaseLocalFileUrl(state.resourceIndex.get(previousRid)?.file);
3879
+ }
3710
3880
  hiddenInput.value = first.resourceId;
3711
3881
  await renderCallback(first.resourceId);
3712
3882
  if (!state.config.readonly) {
@@ -3715,7 +3885,9 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
3715
3885
  }
3716
3886
 
3717
3887
  // src/components/file/render-edit.ts
3718
- function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
3888
+ var ICON_CLOUD = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 18a4 4 0 000-8 6 6 0 00-11.5 2A4 4 0 006 20h11M12 12v7M12 12l-3 3M12 12l3 3"/></svg>`;
3889
+ var ICON_LIBRARY2 = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4h4v16H4z"/><path d="M10 4h4v16h-4z"/><path d="M16 5l3.5 1-3 14L13 19"/></svg>`;
3890
+ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps, extras) {
3719
3891
  seedInferredResource(initial, state.resourceIndex);
3720
3892
  const meta = state.resourceIndex.get(initial);
3721
3893
  const isVideo = meta?.type?.startsWith("video/");
@@ -3726,7 +3898,7 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
3726
3898
  deps
3727
3899
  }).catch(console.error);
3728
3900
  } else {
3729
- renderSingleFileEditTile(fileContainer, initial, state, deps).catch(console.error);
3901
+ renderSingleFileFilled(fileContainer, initial, state, deps, extras);
3730
3902
  }
3731
3903
  const hiddenInput = document.createElement("input");
3732
3904
  hiddenInput.type = "hidden";
@@ -3734,161 +3906,423 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
3734
3906
  hiddenInput.value = initial;
3735
3907
  fileWrapper.appendChild(hiddenInput);
3736
3908
  }
3737
- 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);">
3738
- <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"/>
3739
- </svg>`;
3740
- function buildEmptyDropzone(state, primaryText, subHint, openPicker) {
3741
- const dropzone = document.createElement("div");
3742
- dropzone.className = "fb-file-dropzone";
3743
- dropzone.innerHTML = `
3744
- ${UPLOAD_SVG}
3745
- <div class="fb-dropzone-primary-text">${escapeHtml(primaryText)}</div>
3746
- ${subHint ? `<div class="fb-dropzone-hint-text">${escapeHtml(subHint)}</div>` : ""}
3747
- `;
3748
- dropzone.onclick = openPicker;
3749
- return dropzone;
3909
+ function buildWideTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragOver = false, constraintHint = "") {
3910
+ ensureFileStyles();
3911
+ const outer = document.createElement("div");
3912
+ outer.className = `fb-wide-tile${hasLibrary ? " fb-file-card-row" : ""}${isDragOver ? " fb-drag-over" : ""}`;
3913
+ const uploadBtn = document.createElement("button");
3914
+ uploadBtn.type = "button";
3915
+ uploadBtn.className = "fb-wide-tile-upload fb-file-dropzone";
3916
+ const cloudIcon = document.createElement("span");
3917
+ cloudIcon.style.cssText = "width:36px;height:36px;display:block;flex-shrink:0;";
3918
+ cloudIcon.innerHTML = ICON_CLOUD;
3919
+ uploadBtn.appendChild(cloudIcon);
3920
+ const primaryText = document.createElement("div");
3921
+ primaryText.className = "fb-wide-tile-label";
3922
+ primaryText.style.cssText = "font-size:14px;font-weight:600;";
3923
+ primaryText.textContent = isDragOver ? t("dropToUpload", state) : t("clickDragText", state);
3924
+ uploadBtn.appendChild(primaryText);
3925
+ if (constraintHint) {
3926
+ const hintEl = document.createElement("div");
3927
+ hintEl.style.cssText = "font-size:11px;opacity:0.65;margin-top:2px;";
3928
+ hintEl.textContent = constraintHint;
3929
+ uploadBtn.appendChild(hintEl);
3930
+ }
3931
+ uploadBtn.onclick = (e) => {
3932
+ e.stopPropagation();
3933
+ onUploadClick();
3934
+ };
3935
+ outer.appendChild(uploadBtn);
3936
+ if (hasLibrary && onLibraryClick) {
3937
+ const divider = document.createElement("div");
3938
+ divider.className = "fb-wide-tile-divider";
3939
+ outer.appendChild(divider);
3940
+ const libBtn = document.createElement("button");
3941
+ libBtn.type = "button";
3942
+ libBtn.className = "fb-wide-tile-library fb-file-library-card";
3943
+ const libIcon = document.createElement("span");
3944
+ libIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
3945
+ libIcon.innerHTML = ICON_LIBRARY2;
3946
+ libBtn.appendChild(libIcon);
3947
+ const libLabel = document.createElement("div");
3948
+ libLabel.style.cssText = "font-size:13px;font-weight:600;text-align:center;";
3949
+ libLabel.textContent = t("fromLibrary", state);
3950
+ libBtn.appendChild(libLabel);
3951
+ const libHint = document.createElement("div");
3952
+ libHint.style.cssText = "font-size:11px;opacity:0.75;text-align:center;";
3953
+ libHint.textContent = t("libraryHint", state);
3954
+ libBtn.appendChild(libHint);
3955
+ libBtn.onclick = (e) => {
3956
+ e.stopPropagation();
3957
+ onLibraryClick();
3958
+ };
3959
+ outer.appendChild(libBtn);
3960
+ }
3961
+ attachDragOverFeedback(outer, {
3962
+ onEnter: () => {
3963
+ const primaryText2 = outer.querySelector(".fb-wide-tile-label");
3964
+ if (primaryText2) primaryText2.textContent = t("dropToUpload", state);
3965
+ },
3966
+ onLeave: () => {
3967
+ const primaryText2 = outer.querySelector(".fb-wide-tile-label");
3968
+ if (primaryText2) primaryText2.textContent = t("clickDragText", state);
3969
+ },
3970
+ activeClass: "fb-drag-over"
3971
+ });
3972
+ return outer;
3750
3973
  }
3751
- function buildLibraryButton(variant, state, onClick) {
3752
- const btn = document.createElement("button");
3753
- btn.type = "button";
3754
- btn.className = variant === "card" ? "fb-file-library-card" : "fb-tile fb-tile-add-library";
3755
- if (variant === "card") {
3756
- btn.innerHTML = `
3757
- <span class="fb-file-library-card-icon" aria-hidden="true">\u{1F4DA}</span>
3758
- <span class="fb-file-library-card-label">${escapeHtml(t("fromLibrary", state))}</span>
3759
- <span class="fb-file-library-card-hint">${escapeHtml(t("libraryHint", state))}</span>
3760
- `;
3974
+ function attachDragOverFeedback(el, hooks) {
3975
+ let depth = 0;
3976
+ el.addEventListener("dragover", (e) => {
3977
+ e.preventDefault();
3978
+ });
3979
+ el.addEventListener("dragenter", (e) => {
3980
+ e.preventDefault();
3981
+ depth++;
3982
+ if (depth === 1) {
3983
+ el.classList.add(hooks.activeClass);
3984
+ hooks.onEnter();
3985
+ }
3986
+ });
3987
+ el.addEventListener("dragleave", (e) => {
3988
+ e.preventDefault();
3989
+ depth = Math.max(0, depth - 1);
3990
+ if (depth === 0) {
3991
+ el.classList.remove(hooks.activeClass);
3992
+ hooks.onLeave();
3993
+ }
3994
+ });
3995
+ el.addEventListener("drop", () => {
3996
+ depth = 0;
3997
+ el.classList.remove(hooks.activeClass);
3998
+ hooks.onLeave();
3999
+ });
4000
+ }
4001
+ function renderSingleFileFilled(fileContainer, resourceId, state, deps, extras) {
4002
+ const meta = state.resourceIndex.get(resourceId);
4003
+ const isVideo = meta?.type?.startsWith("video/");
4004
+ if (isVideo) {
4005
+ renderFilePreview(fileContainer, resourceId, state, {
4006
+ fileName: meta?.name ?? "",
4007
+ isReadonly: false,
4008
+ deps
4009
+ }).catch(console.error);
4010
+ return;
4011
+ }
4012
+ ensureFileStyles();
4013
+ const outer = document.createElement("div");
4014
+ outer.className = "fb-multi-outer fb-multi-has-files";
4015
+ const grid = document.createElement("div");
4016
+ grid.className = "fb-multi-grid fb-tiles-wrap";
4017
+ outer.appendChild(grid);
4018
+ const tile = buildPreviewTile(
4019
+ resourceId,
4020
+ state,
4021
+ Boolean(deps.onRemove),
4022
+ deps.onRemove ? () => deps.onRemove?.() : null,
4023
+ extras
4024
+ );
4025
+ grid.appendChild(tile);
4026
+ fileContainer.className = "file-preview-container";
4027
+ fileContainer.removeAttribute("style");
4028
+ while (fileContainer.firstChild) fileContainer.removeChild(fileContainer.firstChild);
4029
+ fileContainer.appendChild(outer);
4030
+ }
4031
+ function buildMultiAddTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragOver = false) {
4032
+ const tile = document.createElement("div");
4033
+ tile.className = `fb-multi-add-tile fb-multi-add-tile-js${isDragOver ? " fb-drag-over-tile" : ""}`;
4034
+ const uploadBtn = document.createElement("button");
4035
+ uploadBtn.type = "button";
4036
+ uploadBtn.className = "fb-multi-add-upload fb-tile-add fb-file-dropzone";
4037
+ const cloudIcon = document.createElement("span");
4038
+ cloudIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
4039
+ cloudIcon.innerHTML = ICON_CLOUD;
4040
+ uploadBtn.appendChild(cloudIcon);
4041
+ const uploadLabel = document.createElement("span");
4042
+ uploadLabel.className = "fb-multi-add-label";
4043
+ uploadLabel.style.cssText = "font-size:11px;font-weight:600;";
4044
+ uploadLabel.textContent = isDragOver ? t("dropToUpload", state) : t("clickDragTextMultiple", state);
4045
+ uploadBtn.appendChild(uploadLabel);
4046
+ uploadBtn.onclick = (e) => {
4047
+ e.stopPropagation();
4048
+ onUploadClick();
4049
+ };
4050
+ tile.appendChild(uploadBtn);
4051
+ if (hasLibrary && onLibraryClick) {
4052
+ const divider = document.createElement("div");
4053
+ divider.className = "fb-multi-add-divider";
4054
+ tile.appendChild(divider);
4055
+ const libBtn = document.createElement("button");
4056
+ libBtn.type = "button";
4057
+ libBtn.className = "fb-multi-add-library fb-tile-add-library fb-file-library-card";
4058
+ libBtn.setAttribute("aria-label", t("fromLibrary", state));
4059
+ const libIcon = document.createElement("span");
4060
+ libIcon.style.cssText = "width:14px;height:14px;display:block;flex-shrink:0;";
4061
+ libIcon.innerHTML = ICON_LIBRARY2;
4062
+ libBtn.appendChild(libIcon);
4063
+ libBtn.appendChild(document.createTextNode(t("fromLibrary", state)));
4064
+ libBtn.onclick = (e) => {
4065
+ e.stopPropagation();
4066
+ onLibraryClick();
4067
+ };
4068
+ tile.appendChild(libBtn);
4069
+ }
4070
+ return tile;
4071
+ }
4072
+ function buildPreviewTile(rid, state, canRemove, onRemove, extras) {
4073
+ ensureFileStyles();
4074
+ const meta = state.resourceIndex.get(rid);
4075
+ const tile = document.createElement("div");
4076
+ tile.className = "fb-preview-tile fb-checker fb-tile-resource resource-pill";
4077
+ tile.dataset.resourceId = rid;
4078
+ const actionsEl = createTileActions({
4079
+ canRemove: canRemove && onRemove !== null,
4080
+ removeHandler: onRemove,
4081
+ state,
4082
+ resourceId: rid,
4083
+ fileName: meta?.name ?? "",
4084
+ meta,
4085
+ replaceHandler: extras?.replaceHandler ?? null,
4086
+ libraryHandler: extras?.libraryHandler ?? null
4087
+ });
4088
+ fillTileContent(tile, rid, meta, state, actionsEl).catch((err) => {
4089
+ console.error("Failed to render tile:", err);
4090
+ });
4091
+ return tile;
4092
+ }
4093
+ function buildPlaceholderTile(isDragOver = false) {
4094
+ const div = document.createElement("div");
4095
+ div.className = `fb-multi-placeholder fb-checker${isDragOver ? " fb-drag-over" : ""}`;
4096
+ return div;
4097
+ }
4098
+ function buildMetaLine(state, element, ridCount, maxCount, canClearAll, onClearAll) {
4099
+ const line = document.createElement("div");
4100
+ line.className = "fb-meta-line";
4101
+ const metaText = document.createElement("div");
4102
+ metaText.className = "fb-meta-text";
4103
+ if (element.maxSize && element.maxSize !== Infinity) {
4104
+ const sizeSpan = document.createElement("span");
4105
+ sizeSpan.textContent = t("hintMaxSize", state, { size: element.maxSize });
4106
+ metaText.appendChild(sizeSpan);
4107
+ metaText.appendChild(buildMetaDot());
4108
+ }
4109
+ const exts = getAllowedExtensions(element.accept);
4110
+ if (exts.length > 0) {
4111
+ const fmtSpan = document.createElement("span");
4112
+ fmtSpan.className = "fb-meta-mono";
4113
+ fmtSpan.textContent = exts.map((e) => e.toUpperCase()).join(", ");
4114
+ metaText.appendChild(fmtSpan);
4115
+ metaText.appendChild(buildMetaDot());
4116
+ }
4117
+ const countSpan = document.createElement("span");
4118
+ if (maxCount < Infinity) {
4119
+ countSpan.textContent = t("fileCountWithMax", state, {
4120
+ count: ridCount,
4121
+ max: maxCount
4122
+ });
3761
4123
  } else {
3762
- btn.innerHTML = `<span aria-hidden="true">\u{1F4DA}</span>`;
3763
- btn.title = t("fromLibrary", state);
3764
- btn.setAttribute("aria-label", t("fromLibrary", state));
4124
+ const countKey = ridCount === 1 ? "fileCountSingle" : "fileCountPlural";
4125
+ countSpan.textContent = t(countKey, state, { count: ridCount });
4126
+ }
4127
+ metaText.appendChild(countSpan);
4128
+ line.appendChild(metaText);
4129
+ if (canClearAll && ridCount > 1) {
4130
+ const clearBtn = document.createElement("button");
4131
+ clearBtn.type = "button";
4132
+ clearBtn.className = "fb-clear-all-btn";
4133
+ clearBtn.textContent = t("clearAll", state);
4134
+ clearBtn.onclick = (e) => {
4135
+ e.stopPropagation();
4136
+ if (window.confirm(t("clearAll", state) + "?")) {
4137
+ onClearAll();
4138
+ }
4139
+ };
4140
+ line.appendChild(clearBtn);
3765
4141
  }
3766
- btn.addEventListener("click", onClick);
3767
- return btn;
4142
+ return line;
4143
+ }
4144
+ function buildMetaDot() {
4145
+ const dot = document.createElement("span");
4146
+ dot.className = "fb-meta-dot";
4147
+ return dot;
3768
4148
  }
4149
+ var gridResizeObservers = /* @__PURE__ */ new WeakMap();
3769
4150
  function renderResourcePills(opts) {
3770
4151
  const {
3771
4152
  container,
3772
4153
  rids,
3773
4154
  state,
3774
4155
  onRemove,
3775
- hint,
3776
- countInfo,
3777
4156
  maxCount,
3778
4157
  isReadonly = false,
3779
- onLibraryPick
4158
+ onLibraryPick,
4159
+ element,
4160
+ onClearAll,
4161
+ openPicker: openPickerProp
3780
4162
  } = opts;
3781
4163
  ensureFileStyles();
3782
4164
  const wrapper = container.closest("[data-files-wrapper]");
3783
4165
  if (wrapper) {
3784
4166
  wrapper.dataset.resourceIds = JSON.stringify(rids ?? []);
3785
4167
  }
4168
+ const previousObserver = gridResizeObservers.get(container);
4169
+ if (previousObserver) {
4170
+ previousObserver.disconnect();
4171
+ gridResizeObservers.delete(container);
4172
+ }
3786
4173
  while (container.firstChild) container.removeChild(container.firstChild);
3787
4174
  const ridList = rids ?? [];
3788
- const atMax = maxCount !== void 0 && ridList.length >= maxCount;
4175
+ const effectiveMax = maxCount ?? Infinity;
4176
+ const atMax = effectiveMax !== Infinity && ridList.length >= effectiveMax;
3789
4177
  const hasLibrary = !isReadonly && typeof onLibraryPick === "function";
3790
- const buildSubHint = () => {
3791
- const parts = [];
3792
- if (hint) parts.push(hint);
3793
- if (countInfo) parts.push(countInfo);
3794
- return parts.join(" \u2022 ");
3795
- };
3796
- const openPicker = () => {
3797
- const picker = findFilePicker(container);
3798
- if (picker) picker.click();
3799
- };
3800
- if (ridList.length === 0) {
3801
- if (isReadonly) {
4178
+ const openPicker = openPickerProp ?? (() => {
4179
+ const pickerEl = container.closest("[data-files-wrapper]")?.querySelector('input[type="file"]');
4180
+ if (pickerEl) pickerEl.click();
4181
+ });
4182
+ if (isReadonly) {
4183
+ if (ridList.length === 0) {
3802
4184
  const emptyEl = document.createElement("div");
3803
4185
  emptyEl.className = "fb-tile-empty-text";
3804
4186
  emptyEl.textContent = t("noFilesSelected", state);
3805
4187
  container.appendChild(emptyEl);
3806
- } else if (hasLibrary) {
3807
- const row = document.createElement("div");
3808
- row.className = "fb-file-card-row";
3809
- const dropzone = buildEmptyDropzone(
3810
- state,
3811
- t("clickDragTextMultiple", state),
3812
- buildSubHint(),
3813
- openPicker
3814
- );
3815
- const libraryBtn = buildLibraryButton("card", state, onLibraryPick);
3816
- row.appendChild(dropzone);
3817
- row.appendChild(libraryBtn);
3818
- container.appendChild(row);
3819
4188
  } else {
3820
- const dropzone = buildEmptyDropzone(
3821
- state,
3822
- t("clickDragTextMultiple", state),
3823
- buildSubHint(),
3824
- openPicker
3825
- );
3826
- container.appendChild(dropzone);
4189
+ const grid2 = document.createElement("div");
4190
+ grid2.className = "fb-multi-readonly-grid";
4191
+ container.appendChild(grid2);
4192
+ for (const rid of ridList) {
4193
+ const meta = state.resourceIndex.get(rid);
4194
+ const tile = document.createElement("div");
4195
+ tile.className = "fb-readonly-tile fb-checker fb-tile fb-tile-resource";
4196
+ tile.dataset.resourceId = rid;
4197
+ const actionsEl = createTileActions({
4198
+ canRemove: false,
4199
+ removeHandler: null,
4200
+ state,
4201
+ resourceId: rid,
4202
+ fileName: meta?.name ?? "",
4203
+ meta
4204
+ });
4205
+ fillTileContent(tile, rid, meta, state, actionsEl).catch(console.error);
4206
+ tile.onclick = async () => {
4207
+ let url = null;
4208
+ if (state.config.getDownloadUrl) {
4209
+ url = state.config.getDownloadUrl(rid);
4210
+ } else if (state.config.getThumbnail) {
4211
+ url = await state.config.getThumbnail(rid);
4212
+ } else if (meta?.file instanceof File) {
4213
+ url = URL.createObjectURL(meta.file);
4214
+ }
4215
+ if (url) {
4216
+ window.open(url, "_blank");
4217
+ } else if (state.config.downloadFile) {
4218
+ state.config.downloadFile(rid, meta?.name ?? "");
4219
+ }
4220
+ };
4221
+ grid2.appendChild(tile);
4222
+ }
3827
4223
  }
3828
4224
  return;
3829
4225
  }
3830
- const tilesWrap = document.createElement("div");
3831
- tilesWrap.className = "fb-tiles-wrap";
3832
- tilesWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start;";
3833
- for (const rid of ridList) {
3834
- const meta = state.resourceIndex.get(rid);
3835
- const tile = createFileTile();
3836
- tile.classList.add("fb-tile-resource", "resource-pill");
3837
- tile.dataset.resourceId = rid;
3838
- const actionsEl = createTileActions({
3839
- canRemove: !isReadonly && onRemove !== null,
3840
- removeHandler: onRemove ? () => onRemove(rid) : null,
4226
+ const outerDiv = document.createElement("div");
4227
+ outerDiv.className = `fb-multi-outer${ridList.length > 0 ? " fb-multi-has-files" : ""}`;
4228
+ const grid = document.createElement("div");
4229
+ grid.className = "fb-multi-grid fb-tiles-wrap";
4230
+ outerDiv.appendChild(grid);
4231
+ container.appendChild(outerDiv);
4232
+ for (let i = 0; i < ridList.length; i++) {
4233
+ const rid = ridList[i];
4234
+ const tile = buildPreviewTile(
4235
+ rid,
3841
4236
  state,
3842
- resourceId: rid,
3843
- fileName: meta?.name ?? ""
3844
- });
3845
- fillTileContent(tile, rid, meta, state, actionsEl).catch((err) => {
3846
- console.error("Failed to render tile:", err);
3847
- });
3848
- tilesWrap.appendChild(tile);
3849
- }
3850
- if (!isReadonly && !atMax) {
3851
- const addTile = document.createElement("div");
3852
- addTile.className = "fb-tile fb-tile-add";
3853
- addTile.innerHTML = "+";
3854
- addTile.onclick = openPicker;
3855
- tilesWrap.appendChild(addTile);
3856
- if (hasLibrary) {
3857
- const libraryTile = buildLibraryButton("tile", state, onLibraryPick);
3858
- tilesWrap.appendChild(libraryTile);
3859
- }
3860
- } else if (!isReadonly && atMax) {
3861
- const chip = document.createElement("div");
3862
- chip.className = "fb-tile-counter";
3863
- chip.textContent = t("filesCounter", state, {
3864
- count: ridList.length,
3865
- max: maxCount
3866
- });
3867
- tilesWrap.appendChild(chip);
4237
+ onRemove !== null,
4238
+ onRemove ? () => onRemove(rid) : null
4239
+ );
4240
+ grid.appendChild(tile);
3868
4241
  }
3869
- container.appendChild(tilesWrap);
3870
- const subHint = buildSubHint();
3871
- if (subHint) {
3872
- const hintEl = document.createElement("div");
3873
- hintEl.className = "fb-tile-hint";
3874
- hintEl.textContent = subHint;
3875
- container.appendChild(hintEl);
4242
+ if (!atMax) {
4243
+ const addTile = buildMultiAddTile(
4244
+ state,
4245
+ hasLibrary,
4246
+ openPicker,
4247
+ onLibraryPick ?? null
4248
+ );
4249
+ grid.appendChild(addTile);
4250
+ }
4251
+ const occupied = ridList.length + (atMax ? 0 : 1);
4252
+ const adjustPlaceholders = () => {
4253
+ const tpl = getComputedStyle(grid).gridTemplateColumns;
4254
+ const cols = tpl ? tpl.split(" ").filter(Boolean).length : 0;
4255
+ if (!cols) return;
4256
+ const remainder = occupied % cols;
4257
+ const rowFill = remainder === 0 ? 0 : cols - remainder;
4258
+ const capacityRemaining = effectiveMax === Infinity ? rowFill : Math.max(0, effectiveMax - occupied);
4259
+ const needed = Math.min(rowFill, capacityRemaining);
4260
+ const existing = grid.querySelectorAll(".fb-multi-placeholder");
4261
+ if (existing.length > needed) {
4262
+ for (let i = existing.length - 1; i >= needed; i--) existing[i].remove();
4263
+ } else if (existing.length < needed) {
4264
+ for (let i = existing.length; i < needed; i++) {
4265
+ grid.appendChild(buildPlaceholderTile());
4266
+ }
4267
+ }
4268
+ };
4269
+ if (effectiveMax === Infinity || effectiveMax > occupied) {
4270
+ grid.appendChild(buildPlaceholderTile());
4271
+ }
4272
+ requestAnimationFrame(adjustPlaceholders);
4273
+ if (typeof ResizeObserver !== "undefined") {
4274
+ const ro = new ResizeObserver(() => adjustPlaceholders());
4275
+ ro.observe(grid);
4276
+ gridResizeObservers.set(container, ro);
4277
+ }
4278
+ attachDragOverFeedback(outerDiv, {
4279
+ activeClass: "fb-drag-over",
4280
+ onEnter: () => {
4281
+ grid.querySelectorAll(".fb-multi-placeholder").forEach((p) => {
4282
+ p.classList.add("fb-drag-over");
4283
+ });
4284
+ const addTile = grid.querySelector(".fb-multi-add-tile-js");
4285
+ if (addTile) {
4286
+ addTile.classList.add("fb-drag-over-tile");
4287
+ const label = addTile.querySelector(".fb-multi-add-label");
4288
+ if (label) label.textContent = t("dropToUpload", state);
4289
+ }
4290
+ },
4291
+ onLeave: () => {
4292
+ grid.querySelectorAll(".fb-multi-placeholder").forEach((p) => {
4293
+ p.classList.remove("fb-drag-over");
4294
+ });
4295
+ const addTile = grid.querySelector(".fb-multi-add-tile-js");
4296
+ if (addTile) {
4297
+ addTile.classList.remove("fb-drag-over-tile");
4298
+ const label = addTile.querySelector(".fb-multi-add-label");
4299
+ if (label) label.textContent = t("clickDragTextMultiple", state);
4300
+ }
4301
+ }
4302
+ });
4303
+ if (element) {
4304
+ const metaLine = buildMetaLine(
4305
+ state,
4306
+ element,
4307
+ ridList.length,
4308
+ effectiveMax,
4309
+ Boolean(onClearAll),
4310
+ onClearAll ?? (() => {
4311
+ })
4312
+ );
4313
+ container.appendChild(metaLine);
3876
4314
  }
3877
4315
  }
3878
4316
  function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3879
4317
  const state = ctx.state;
3880
4318
  const fileWrapper = document.createElement("div");
3881
4319
  fileWrapper.className = "space-y-2";
4320
+ fileWrapper.dataset.filesWrapper = pathKey;
3882
4321
  const picker = document.createElement("input");
3883
4322
  picker.type = "file";
3884
4323
  picker.name = pathKey;
3885
4324
  picker.style.display = "none";
3886
- if (element.accept) {
3887
- picker.accept = typeof element.accept === "string" ? element.accept : [
3888
- ...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
3889
- ...element.accept.mime ?? []
3890
- ].join(",") || "";
3891
- }
4325
+ picker.accept = buildAcceptAttribute(element.accept);
3892
4326
  const fileContainer = document.createElement("div");
3893
4327
  fileContainer.className = "file-preview-container";
3894
4328
  const initial = ctx.prefill[element.key];
@@ -3917,14 +4351,6 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3917
4351
  setupDrop(container) {
3918
4352
  setupDragAndDrop(container, handlers.dragHandler);
3919
4353
  },
3920
- restoreDropzone() {
3921
- const hint = makeFieldHint(element, state);
3922
- fileContainer.className = "file-preview-container w-full max-w-md bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
3923
- fileContainer.style.height = "128px";
3924
- setEmptyFileContainer(fileContainer, state, hint);
3925
- fileContainer.onclick = handlers.fileUploadHandler;
3926
- setupDragAndDrop(fileContainer, handlers.dragHandler);
3927
- },
3928
4354
  onRemove() {
3929
4355
  const hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
3930
4356
  const currentRid = hiddenInput?.value;
@@ -3935,34 +4361,11 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3935
4361
  renderEmptySingleState();
3936
4362
  }
3937
4363
  };
3938
- const buildDeps = () => ({
3939
- picker,
3940
- fileUploadHandler: handlers.fileUploadHandler,
3941
- dragHandler: handlers.dragHandler,
3942
- setupDrop: handlers.setupDrop,
3943
- onRemove: handlers.onRemove
3944
- });
3945
- const renderEmptySingleState = () => {
3946
- if (state.config.pickExistingFiles && !element.disableLibrary) {
3947
- fileContainer.className = "file-preview-container";
3948
- fileContainer.removeAttribute("style");
3949
- fileContainer.onclick = null;
3950
- while (fileContainer.firstChild) {
3951
- fileContainer.removeChild(fileContainer.firstChild);
3952
- }
3953
- const row = document.createElement("div");
3954
- row.className = "fb-file-card-row";
3955
- row.style.cssText = "display:flex;gap:8px;align-items:stretch;";
3956
- const hint = makeFieldHint(element, state);
3957
- const uploadCard = buildEmptyDropzone(
3958
- state,
3959
- t("clickDragText", state),
3960
- hint,
3961
- handlers.fileUploadHandler
3962
- );
3963
- uploadCard.style.cssText = "flex:1;min-width:0;height:128px;";
3964
- setupDragAndDrop(uploadCard, handlers.dragHandler);
3965
- const libraryBtn = buildLibraryButton("card", state, () => {
4364
+ const buildSingleExtras = () => {
4365
+ const hasLibrary = Boolean(state.config.pickExistingFiles && !element.disableLibrary);
4366
+ return {
4367
+ replaceHandler: state.config.uploadFile ? () => picker.click() : null,
4368
+ libraryHandler: hasLibrary ? () => {
3966
4369
  handleLibraryPickSingle(
3967
4370
  state,
3968
4371
  element,
@@ -3971,20 +4374,41 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3971
4374
  pathKey,
3972
4375
  pathKey,
3973
4376
  async (rid) => {
3974
- await renderSingleFileEditTile(fileContainer, rid, state, buildDeps());
4377
+ renderSingleFileFilled(fileContainer, rid, state, buildDeps(), buildSingleExtras());
3975
4378
  },
3976
4379
  ctx.instance
3977
4380
  ).catch((err) => {
3978
4381
  console.error("Library pick failed:", err);
3979
4382
  });
3980
- });
3981
- libraryBtn.style.cssText = "flex:1;min-width:0;";
3982
- row.appendChild(uploadCard);
3983
- row.appendChild(libraryBtn);
3984
- fileContainer.appendChild(row);
3985
- } else {
3986
- handlers.restoreDropzone();
4383
+ } : null
4384
+ };
4385
+ };
4386
+ const buildDeps = () => ({
4387
+ picker,
4388
+ fileUploadHandler: handlers.fileUploadHandler,
4389
+ dragHandler: handlers.dragHandler,
4390
+ setupDrop: handlers.setupDrop,
4391
+ onRemove: handlers.onRemove,
4392
+ onAfterUpload: (container, rid) => {
4393
+ renderSingleFileFilled(container, rid, state, buildDeps(), buildSingleExtras());
3987
4394
  }
4395
+ });
4396
+ const renderEmptySingleState = () => {
4397
+ ensureFileStyles();
4398
+ fileContainer.className = "file-preview-container";
4399
+ fileContainer.removeAttribute("style");
4400
+ while (fileContainer.firstChild) fileContainer.removeChild(fileContainer.firstChild);
4401
+ const onLibraryClick = buildSingleExtras().libraryHandler;
4402
+ const wideTile = buildWideTile(
4403
+ state,
4404
+ onLibraryClick !== null,
4405
+ handlers.fileUploadHandler,
4406
+ onLibraryClick,
4407
+ false,
4408
+ makeFieldHint(element, state)
4409
+ );
4410
+ fileContainer.appendChild(wideTile);
4411
+ setupDragAndDrop(fileContainer, handlers.dragHandler);
3988
4412
  };
3989
4413
  if (initial) {
3990
4414
  handleInitialFileData(
@@ -3993,11 +4417,11 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3993
4417
  pathKey,
3994
4418
  fileWrapper,
3995
4419
  state,
3996
- buildDeps()
4420
+ buildDeps(),
4421
+ buildSingleExtras()
3997
4422
  );
3998
4423
  const prefillMeta = state.resourceIndex.get(initial);
3999
4424
  if (prefillMeta?.type?.startsWith("video/")) {
4000
- fileContainer.onclick = handlers.fileUploadHandler;
4001
4425
  setupDragAndDrop(fileContainer, handlers.dragHandler);
4002
4426
  }
4003
4427
  } else {
@@ -4005,113 +4429,23 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
4005
4429
  }
4006
4430
  picker.onchange = () => {
4007
4431
  if (picker.files && picker.files.length > 0) {
4008
- handleFileSelect({
4009
- file: picker.files[0],
4010
- container: fileContainer,
4011
- fieldName: pathKey,
4012
- state,
4013
- deps: buildDeps(),
4014
- instance: ctx.instance,
4015
- allowedExtensions: allowedExts,
4016
- allowedMimes,
4017
- maxSizeMB
4018
- });
4432
+ handlers.dragHandler(picker.files);
4019
4433
  }
4020
4434
  };
4021
4435
  fileWrapper.appendChild(fileContainer);
4022
4436
  fileWrapper.appendChild(picker);
4023
4437
  wrapper.appendChild(fileWrapper);
4024
4438
  }
4025
- function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
4026
- const state = ctx.state;
4027
- const filesWrapper = document.createElement("div");
4028
- filesWrapper.className = "space-y-2";
4029
- filesWrapper.dataset.filesWrapper = pathKey;
4030
- const filesPicker = document.createElement("input");
4031
- filesPicker.type = "file";
4032
- filesPicker.name = pathKey;
4033
- filesPicker.multiple = true;
4034
- filesPicker.style.display = "none";
4035
- if (element.accept) {
4036
- filesPicker.accept = typeof element.accept === "string" ? element.accept : [
4037
- ...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
4038
- ...element.accept.mime ?? []
4039
- ].join(",") || "";
4040
- }
4041
- const filesContainer = document.createElement("div");
4042
- filesContainer.className = "files-list-wrapper";
4043
- 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);";
4044
- const list = document.createElement("div");
4045
- list.className = "files-list";
4046
- const initialFiles = ctx.prefill[element.key] || [];
4047
- addPrefillFilesToIndex(initialFiles, state.resourceIndex);
4048
- filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
4049
- const filesFieldHint = makeFieldHint(element, state);
4050
- const filesConstraints = {
4051
- maxCount: Infinity,
4052
- allowedExtensions: getAllowedExtensions(element.accept),
4053
- allowedMimes: getAllowedMimes(element.accept),
4054
- maxSize: element.maxSize ?? Infinity
4055
- };
4056
- filesContainer.appendChild(list);
4057
- filesWrapper.appendChild(filesPicker);
4058
- filesWrapper.appendChild(filesContainer);
4059
- wrapper.appendChild(filesWrapper);
4060
- const onLibraryPickFiles = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4061
- handleLibraryPickMulti(
4062
- state,
4063
- element,
4064
- filesWrapper,
4065
- pathKey,
4066
- initialFiles,
4067
- Infinity,
4068
- updateFilesList,
4069
- ctx.instance
4070
- ).catch((err) => {
4071
- console.error("Library pick failed:", err);
4072
- });
4073
- } : null;
4074
- function updateFilesList() {
4075
- const currentlyReadonly = isElementReadonly(element, state);
4076
- renderResourcePills({
4077
- container: list,
4078
- rids: initialFiles,
4079
- state,
4080
- onRemove: currentlyReadonly ? null : (ridToRemove) => {
4081
- releaseLocalFileUrl(state.resourceIndex.get(ridToRemove)?.file);
4082
- const index = initialFiles.indexOf(ridToRemove);
4083
- if (index > -1) initialFiles.splice(index, 1);
4084
- updateFilesList();
4085
- },
4086
- hint: filesFieldHint,
4087
- isReadonly: currentlyReadonly,
4088
- onLibraryPick: currentlyReadonly ? null : onLibraryPickFiles
4089
- });
4090
- }
4091
- updateFilesList();
4092
- setupFilesDropHandler(
4093
- filesContainer,
4094
- initialFiles,
4095
- state,
4096
- updateFilesList,
4097
- filesConstraints,
4098
- pathKey,
4099
- ctx.instance
4100
- );
4101
- setupFilesPickerHandler(
4102
- filesPicker,
4103
- initialFiles,
4104
- state,
4105
- updateFilesList,
4106
- filesConstraints,
4107
- pathKey,
4108
- ctx.instance
4109
- );
4439
+ function buildAcceptAttribute(accept) {
4440
+ if (!accept) return "";
4441
+ if (typeof accept === "string") return accept;
4442
+ return [
4443
+ ...accept.extensions?.map((ext) => `.${ext}`) ?? [],
4444
+ ...accept.mime ?? []
4445
+ ].join(",");
4110
4446
  }
4111
- function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4447
+ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
4112
4448
  const state = ctx.state;
4113
- const minFiles = element.minCount ?? 0;
4114
- const maxFiles = element.maxCount ?? Infinity;
4115
4449
  const filesWrapper = document.createElement("div");
4116
4450
  filesWrapper.className = "space-y-2";
4117
4451
  filesWrapper.dataset.filesWrapper = pathKey;
@@ -4120,15 +4454,9 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4120
4454
  filesPicker.name = pathKey;
4121
4455
  filesPicker.multiple = true;
4122
4456
  filesPicker.style.display = "none";
4123
- if (element.accept) {
4124
- filesPicker.accept = typeof element.accept === "string" ? element.accept : [
4125
- ...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
4126
- ...element.accept.mime ?? []
4127
- ].join(",") || "";
4128
- }
4457
+ filesPicker.accept = buildAcceptAttribute(element.accept);
4129
4458
  const filesContainer = document.createElement("div");
4130
4459
  filesContainer.className = "files-list-wrapper";
4131
- 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);";
4132
4460
  const list = document.createElement("div");
4133
4461
  list.className = "files-list";
4134
4462
  filesWrapper.appendChild(filesPicker);
@@ -4137,19 +4465,18 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4137
4465
  const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
4138
4466
  addPrefillFilesToIndex(initialFiles, state.resourceIndex);
4139
4467
  filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
4140
- const multipleFilesHint = makeFieldHint(element, state);
4141
- const multipleConstraints = {
4468
+ const constraints = {
4142
4469
  maxCount: maxFiles,
4143
4470
  allowedExtensions: getAllowedExtensions(element.accept),
4144
4471
  allowedMimes: getAllowedMimes(element.accept),
4145
- maxSize: element.maxSize ?? Infinity
4472
+ // Prefer schema's `maxSize`; fall back to legacy `maxSizeMB` for
4473
+ // backward compatibility (matches addFileSizeHint in validation.ts).
4474
+ maxSize: element.maxSize ?? element.maxSizeMB ?? Infinity
4146
4475
  };
4147
- const buildCountInfo = () => {
4148
- const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
4149
- const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
4150
- return countText + minMaxText;
4476
+ const openPicker = () => {
4477
+ filesPicker.click();
4151
4478
  };
4152
- const onLibraryPickMultiple = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4479
+ const onLibraryPick = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4153
4480
  handleLibraryPickMulti(
4154
4481
  state,
4155
4482
  element,
@@ -4163,30 +4490,35 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4163
4490
  console.error("Library pick failed:", err);
4164
4491
  });
4165
4492
  } : null;
4166
- const updateFilesDisplay = () => {
4493
+ function updateFilesDisplay() {
4167
4494
  const currentlyReadonly = isElementReadonly(element, state);
4168
4495
  renderResourcePills({
4169
4496
  container: list,
4170
4497
  rids: initialFiles,
4171
4498
  state,
4172
- onRemove: currentlyReadonly ? null : (index) => {
4173
- releaseLocalFileUrl(state.resourceIndex.get(index)?.file);
4174
- initialFiles.splice(initialFiles.indexOf(index), 1);
4499
+ onRemove: currentlyReadonly ? null : (ridToRemove) => {
4500
+ releaseLocalFileUrl(state.resourceIndex.get(ridToRemove)?.file);
4501
+ const index = initialFiles.indexOf(ridToRemove);
4502
+ if (index > -1) initialFiles.splice(index, 1);
4175
4503
  updateFilesDisplay();
4176
4504
  },
4177
- hint: multipleFilesHint,
4178
- countInfo: buildCountInfo(),
4179
4505
  maxCount: maxFiles < Infinity ? maxFiles : void 0,
4180
4506
  isReadonly: currentlyReadonly,
4181
- onLibraryPick: currentlyReadonly ? null : onLibraryPickMultiple
4507
+ onLibraryPick: currentlyReadonly ? null : onLibraryPick,
4508
+ element,
4509
+ onClearAll: currentlyReadonly ? void 0 : () => {
4510
+ initialFiles.splice(0);
4511
+ updateFilesDisplay();
4512
+ },
4513
+ openPicker
4182
4514
  });
4183
- };
4515
+ }
4184
4516
  setupFilesDropHandler(
4185
4517
  filesContainer,
4186
4518
  initialFiles,
4187
4519
  state,
4188
4520
  updateFilesDisplay,
4189
- multipleConstraints,
4521
+ constraints,
4190
4522
  pathKey,
4191
4523
  ctx.instance
4192
4524
  );
@@ -4195,13 +4527,19 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4195
4527
  initialFiles,
4196
4528
  state,
4197
4529
  updateFilesDisplay,
4198
- multipleConstraints,
4530
+ constraints,
4199
4531
  pathKey,
4200
4532
  ctx.instance
4201
4533
  );
4202
4534
  updateFilesDisplay();
4203
4535
  wrapper.appendChild(filesWrapper);
4204
4536
  }
4537
+ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
4538
+ setupMultiFileEditMode(element, ctx, wrapper, pathKey, Infinity);
4539
+ }
4540
+ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4541
+ setupMultiFileEditMode(element, ctx, wrapper, pathKey, element.maxCount ?? Infinity);
4542
+ }
4205
4543
 
4206
4544
  // src/components/file/validate.ts
4207
4545
  function readMultiFileResourceIds(scopeRoot, fullKey) {
@@ -4324,33 +4662,36 @@ function renderFileElementReadonly(element, ctx, wrapper, pathKey) {
4324
4662
  hiddenInput.name = pathKey;
4325
4663
  hiddenInput.value = initial;
4326
4664
  wrapper.appendChild(hiddenInput);
4327
- renderFilePreviewReadonly(initial, state).then((filePreview) => {
4328
- wrapper.appendChild(filePreview);
4329
- }).catch((err) => {
4330
- console.error("Failed to render file preview:", err);
4331
- wrapper.appendChild(buildEmptyReadonlyTile(state));
4332
- });
4665
+ renderFilePreviewReadonly(initial, state).then((tile) => {
4666
+ tile.classList.add(
4667
+ "fb-single-readonly-filled",
4668
+ "fb-readonly-tile",
4669
+ "fb-checker"
4670
+ );
4671
+ wrapper.appendChild(tile);
4672
+ }).catch(console.error);
4333
4673
  } else {
4334
4674
  wrapper.appendChild(buildEmptyReadonlyTile(state));
4335
4675
  }
4336
4676
  }
4337
4677
  function buildEmptyReadonlyTile(state) {
4678
+ ensureFileStyles();
4338
4679
  const emptyState = document.createElement("div");
4339
4680
  emptyState.style.cssText = `
4340
- width:${TILE_SIZE};
4341
- height:${TILE_SIZE};
4681
+ height: 220px;
4342
4682
  display:flex;
4343
4683
  align-items:center;
4344
4684
  justify-content:center;
4345
- background:var(--fb-file-upload-bg-color,#f3f4f6);
4346
- border-radius:var(--fb-border-radius,0.5rem);
4347
- border:1px solid var(--fb-file-upload-border-color,#d1d5db);
4685
+ background: repeating-linear-gradient(45deg, #fafafa 0 6px, #f3f4f6 6px 12px);
4686
+ border-radius:0.75rem;
4687
+ border:1px solid #e2e8f0;
4348
4688
  `;
4349
4689
  emptyState.innerHTML = `<div style="font-size:11px;text-align:center;color:var(--fb-text-secondary-color,#6b7280);">${escapeHtml(t("noFileSelected", state))}</div>`;
4350
4690
  return emptyState;
4351
4691
  }
4352
- function renderMultiFileReadonly(rids, state, wrapper, pathKey, marginTop) {
4692
+ function renderMultiFileReadonly(rids, state, wrapper, pathKey, _marginTop) {
4353
4693
  addPrefillFilesToIndex(rids, state.resourceIndex);
4694
+ ensureFileStyles();
4354
4695
  const filesWrapper = document.createElement("div");
4355
4696
  filesWrapper.dataset.filesWrapper = pathKey;
4356
4697
  filesWrapper.dataset.resourceIds = JSON.stringify(rids);
@@ -4362,22 +4703,28 @@ function renderMultiFileReadonly(rids, state, wrapper, pathKey, marginTop) {
4362
4703
  filesWrapper.appendChild(emptyEl);
4363
4704
  return;
4364
4705
  }
4365
- const tilesWrap = document.createElement("div");
4366
- tilesWrap.style.cssText = `display:flex;flex-wrap:wrap;gap:6px;${marginTop ? `margin-top:${marginTop};` : ""}`;
4367
- filesWrapper.appendChild(tilesWrap);
4706
+ const grid = document.createElement("div");
4707
+ grid.className = "fb-multi-readonly-grid";
4708
+ filesWrapper.appendChild(grid);
4368
4709
  const placeholders = rids.map(() => {
4369
- const placeholder = document.createElement("div");
4370
- placeholder.style.cssText = `width:${TILE_SIZE};height:${TILE_SIZE};`;
4371
- tilesWrap.appendChild(placeholder);
4372
- return placeholder;
4710
+ const ph = document.createElement("div");
4711
+ ph.className = "fb-readonly-tile fb-checker fb-tile";
4712
+ grid.appendChild(ph);
4713
+ return ph;
4373
4714
  });
4374
4715
  for (let i = 0; i < rids.length; i++) {
4375
4716
  const resourceId = rids[i];
4376
4717
  const placeholder = placeholders[i];
4377
- renderFilePreviewReadonly(resourceId, state).then((tileEl) => {
4378
- placeholder.replaceWith(tileEl);
4379
- }).catch((err) => {
4380
- console.error("Failed to render readonly tile:", err);
4718
+ const meta = state.resourceIndex.get(resourceId);
4719
+ renderFilePreviewReadonly(resourceId, state, meta?.name).then((tile) => {
4720
+ tile.classList.add("fb-readonly-tile", "fb-checker", "fb-tile-resource");
4721
+ tile.dataset.resourceId = resourceId;
4722
+ placeholder.replaceWith(tile);
4723
+ }).catch(() => {
4724
+ const tile = document.createElement("div");
4725
+ tile.className = "fb-readonly-tile fb-checker fb-tile fb-tile-resource";
4726
+ tile.dataset.resourceId = resourceId;
4727
+ placeholder.replaceWith(tile);
4381
4728
  });
4382
4729
  }
4383
4730
  }
@@ -4389,7 +4736,7 @@ function renderFilesElementReadonly(element, ctx, wrapper, pathKey) {
4389
4736
  function renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey) {
4390
4737
  const rawPrefill = ctx.prefill[element.key];
4391
4738
  const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
4392
- renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey, "4px");
4739
+ renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey);
4393
4740
  }
4394
4741
 
4395
4742
  // src/components/file.ts
@@ -5527,7 +5874,7 @@ function createPrefillHints(element, pathKey) {
5527
5874
  return null;
5528
5875
  }
5529
5876
  const hintsContainer = document.createElement("div");
5530
- hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-4";
5877
+ hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-2";
5531
5878
  element.prefillHints.forEach((hint, index) => {
5532
5879
  const hintButton = document.createElement("button");
5533
5880
  hintButton.type = "button";
@@ -5542,14 +5889,14 @@ function createPrefillHints(element, pathKey) {
5542
5889
  }
5543
5890
  function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
5544
5891
  const containerWrap = document.createElement("div");
5545
- containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
5892
+ containerWrap.className = "border border-gray-200 rounded-lg p-2 bg-gray-50";
5546
5893
  containerWrap.setAttribute("data-container", pathKey);
5547
5894
  const itemsWrap = document.createElement("div");
5548
5895
  const columns = element.columns || 1;
5549
5896
  if (columns === 1) {
5550
- itemsWrap.className = "space-y-4";
5897
+ itemsWrap.className = "space-y-2";
5551
5898
  } else {
5552
- itemsWrap.className = `grid grid-cols-${columns} gap-4`;
5899
+ itemsWrap.className = `grid grid-cols-${columns} gap-2`;
5553
5900
  }
5554
5901
  const containerIsReadonly = isElementReadonly(element, ctx.state, ctx);
5555
5902
  if (!containerIsReadonly) {
@@ -5583,16 +5930,31 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
5583
5930
  containerWrap.appendChild(itemsWrap);
5584
5931
  wrapper.appendChild(containerWrap);
5585
5932
  }
5933
+ function getChildWrapperClass(isSlides, columns) {
5934
+ if (isSlides) {
5935
+ return "space-y-2";
5936
+ }
5937
+ const cols = columns || 1;
5938
+ return cols === 1 ? "space-y-2" : `grid grid-cols-${cols} gap-2`;
5939
+ }
5586
5940
  function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5587
5941
  const state = ctx.state;
5588
5942
  const containerIsReadonly = isElementReadonly(element, state, ctx);
5589
5943
  const childInheritedReadonly = containerIsReadonly || ctx.inheritedReadonly;
5590
5944
  const containerWrap = document.createElement("div");
5591
- containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
5945
+ containerWrap.className = "border border-gray-200 rounded-lg p-2 bg-gray-50";
5592
5946
  const countDisplay = document.createElement("span");
5593
5947
  countDisplay.className = "text-sm text-gray-500";
5594
5948
  const itemsWrap = document.createElement("div");
5595
- itemsWrap.className = "space-y-4";
5949
+ const isSlides = element.displayMode === "slides";
5950
+ if (isSlides) {
5951
+ itemsWrap.className = "fb-container-slides";
5952
+ const slideCols = element.columns;
5953
+ const gridTemplateColumns = typeof slideCols === "number" && slideCols > 0 ? `repeat(${slideCols}, 1fr)` : "repeat(auto-fit, minmax(280px, 1fr))";
5954
+ itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:8px;align-items:start;`;
5955
+ } else {
5956
+ itemsWrap.className = "space-y-2";
5957
+ }
5596
5958
  if (!containerIsReadonly) {
5597
5959
  const hintsElement = createPrefillHints(element, element.key);
5598
5960
  if (hintsElement) {
@@ -5636,15 +5998,10 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5636
5998
  inheritedReadonly: childInheritedReadonly
5637
5999
  };
5638
6000
  const item = document.createElement("div");
5639
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
6001
+ item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
5640
6002
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
5641
6003
  const childWrapper = document.createElement("div");
5642
- const columns = element.columns || 1;
5643
- if (columns === 1) {
5644
- childWrapper.className = "space-y-4";
5645
- } else {
5646
- childWrapper.className = `grid grid-cols-${columns} gap-4`;
5647
- }
6004
+ childWrapper.className = getChildWrapperClass(isSlides, element.columns);
5648
6005
  element.elements.forEach((child) => {
5649
6006
  if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
5650
6007
  childWrapper.appendChild(
@@ -5713,14 +6070,18 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5713
6070
  inheritedReadonly: childInheritedReadonly
5714
6071
  };
5715
6072
  const item = document.createElement("div");
5716
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
6073
+ item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
5717
6074
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
5718
6075
  const childWrapper = document.createElement("div");
5719
- const columns = element.columns || 1;
5720
- if (columns === 1) {
5721
- childWrapper.className = "space-y-4";
6076
+ if (isSlides) {
6077
+ childWrapper.className = "space-y-2";
5722
6078
  } else {
5723
- childWrapper.className = `grid grid-cols-${columns} gap-4`;
6079
+ const columns = element.columns || 1;
6080
+ if (columns === 1) {
6081
+ childWrapper.className = "space-y-2";
6082
+ } else {
6083
+ childWrapper.className = `grid grid-cols-${columns} gap-2`;
6084
+ }
5724
6085
  }
5725
6086
  element.elements.forEach((child) => {
5726
6087
  if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
@@ -5769,14 +6130,18 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5769
6130
  inheritedReadonly: childInheritedReadonly
5770
6131
  };
5771
6132
  const item = document.createElement("div");
5772
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
6133
+ item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
5773
6134
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
5774
6135
  const childWrapper = document.createElement("div");
5775
- const columns = element.columns || 1;
5776
- if (columns === 1) {
5777
- childWrapper.className = "space-y-4";
6136
+ if (isSlides) {
6137
+ childWrapper.className = "space-y-2";
5778
6138
  } else {
5779
- childWrapper.className = `grid grid-cols-${columns} gap-4`;
6139
+ const columns = element.columns || 1;
6140
+ if (columns === 1) {
6141
+ childWrapper.className = "space-y-2";
6142
+ } else {
6143
+ childWrapper.className = `grid grid-cols-${columns} gap-2`;
6144
+ }
5780
6145
  }
5781
6146
  element.elements.forEach((child) => {
5782
6147
  if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
@@ -7804,7 +8169,7 @@ function filterFilesForDropdown(query, files, labels) {
7804
8169
  });
7805
8170
  }
7806
8171
  var TEXTAREA_FONT = "font-size: var(--fb-font-size, 14px); font-family: var(--fb-font-family, inherit); line-height: 1.6;";
7807
- var TEXTAREA_PADDING = "padding: 12px 52px 12px 14px;";
8172
+ var TEXTAREA_PADDING = "padding: 8px 40px 8px 10px;";
7808
8173
  function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
7809
8174
  const state = ctx.state;
7810
8175
  const files = [...initialValue.files];
@@ -7851,7 +8216,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
7851
8216
  });
7852
8217
  const errorEl = document.createElement("div");
7853
8218
  errorEl.className = "fb-richinput-error";
7854
- errorEl.style.cssText = "display: none; color: var(--fb-error-color, #ef4444); font-size: var(--fb-font-size-small, 12px); padding: 4px 14px 8px;";
8219
+ errorEl.style.cssText = "display: none; color: var(--fb-error-color, #ef4444); font-size: var(--fb-font-size-small, 12px); padding: 4px 10px 6px;";
7855
8220
  let errorTimer = null;
7856
8221
  function showUploadError(message) {
7857
8222
  errorEl.textContent = message;
@@ -7927,7 +8292,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
7927
8292
  });
7928
8293
  const filesRow = document.createElement("div");
7929
8294
  filesRow.className = "fb-richinput-files";
7930
- filesRow.style.cssText = "display: none; flex-wrap: wrap; gap: 6px; padding: 10px 14px 0; align-items: center;";
8295
+ filesRow.style.cssText = "display: none; flex-wrap: wrap; gap: 4px; padding: 6px 10px 0; align-items: center;";
7931
8296
  const fileInput = document.createElement("input");
7932
8297
  fileInput.type = "file";
7933
8298
  fileInput.multiple = true;
@@ -8057,13 +8422,13 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
8057
8422
  paperclipBtn.title = t("richinputAttachFile", state);
8058
8423
  paperclipBtn.style.cssText = `
8059
8424
  position: absolute;
8060
- right: 10px;
8061
- bottom: 10px;
8425
+ right: 6px;
8426
+ bottom: 6px;
8062
8427
  z-index: 2;
8063
- width: 32px;
8064
- height: 32px;
8428
+ width: 28px;
8429
+ height: 28px;
8065
8430
  border: none;
8066
- border-radius: 8px;
8431
+ border-radius: 6px;
8067
8432
  background: transparent;
8068
8433
  cursor: pointer;
8069
8434
  display: flex;
@@ -8417,7 +8782,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
8417
8782
  outerDiv.appendChild(errorEl);
8418
8783
  if (element.minLength != null || element.maxLength != null) {
8419
8784
  const counterRow = document.createElement("div");
8420
- counterRow.style.cssText = "position: relative; padding: 2px 14px 6px; text-align: right;";
8785
+ counterRow.style.cssText = "position: relative; padding: 2px 10px 4px; text-align: right;";
8421
8786
  const counter = createCharCounter(element, textarea, false);
8422
8787
  counter.style.cssText = `
8423
8788
  position: static;
@@ -9211,7 +9576,7 @@ function createInfoButton(element) {
9211
9576
  }
9212
9577
  function createLabelContainer(element) {
9213
9578
  const label = document.createElement("div");
9214
- label.className = "flex items-center mb-2";
9579
+ label.className = "flex items-center mb-1";
9215
9580
  const title = createFieldLabel(element);
9216
9581
  label.appendChild(title);
9217
9582
  if (element.description || element.hint) {
@@ -9318,7 +9683,7 @@ function renderElement2(element, ctx) {
9318
9683
  }
9319
9684
  const initiallyDisabled2 = shouldDisableElement(element, ctx);
9320
9685
  const outerWrapper = document.createElement("div");
9321
- outerWrapper.className = "mb-6 fb-field-wrapper fb-markdown-wrapper";
9686
+ outerWrapper.className = "mb-2 fb-field-wrapper fb-markdown-wrapper";
9322
9687
  outerWrapper.setAttribute(
9323
9688
  "data-field-key",
9324
9689
  getElementLookupKey(element, ctx.state)
@@ -9334,7 +9699,7 @@ function renderElement2(element, ctx) {
9334
9699
  }
9335
9700
  const initiallyDisabled = shouldDisableElement(element, ctx);
9336
9701
  const wrapper = document.createElement("div");
9337
- wrapper.className = "mb-6 fb-field-wrapper";
9702
+ wrapper.className = "mb-2 fb-field-wrapper";
9338
9703
  wrapper.setAttribute("data-field-key", element.key);
9339
9704
  const label = createLabelContainer(element);
9340
9705
  wrapper.appendChild(label);
@@ -9401,12 +9766,16 @@ var defaultConfig = {
9401
9766
  hintPattern: "Format: {pattern}",
9402
9767
  fileCountSingle: "{count} file",
9403
9768
  fileCountPlural: "{count} files",
9769
+ fileCountWithMax: "{count} / {max} files",
9404
9770
  fileCountRange: "({min}-{max})",
9405
9771
  uploadingFile: "Uploading\u2026",
9406
9772
  filesCounter: "{count}/{max}",
9407
9773
  fromLibrary: "From library",
9408
9774
  libraryEmpty: "Library is empty",
9409
9775
  libraryHint: "Choose from previously uploaded files",
9776
+ dropToUpload: "Release to upload",
9777
+ replaceFile: "Replace",
9778
+ clearAll: "Clear all",
9410
9779
  pickerError: "Failed to load files from library",
9411
9780
  // Validation errors
9412
9781
  required: "Required",
@@ -9472,12 +9841,16 @@ var defaultConfig = {
9472
9841
  hintPattern: "\u0424\u043E\u0440\u043C\u0430\u0442: {pattern}",
9473
9842
  fileCountSingle: "{count} \u0444\u0430\u0439\u043B",
9474
9843
  fileCountPlural: "{count} \u0444\u0430\u0439\u043B\u043E\u0432",
9844
+ fileCountWithMax: "{count} / {max} \u0444\u0430\u0439\u043B\u043E\u0432",
9475
9845
  fileCountRange: "({min}-{max})",
9476
9846
  uploadingFile: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026",
9477
9847
  filesCounter: "{count}/{max}",
9478
9848
  fromLibrary: "\u0418\u0437 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438",
9479
9849
  libraryEmpty: "\u0411\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0430 \u043F\u0443\u0441\u0442\u0430",
9480
9850
  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",
9851
+ dropToUpload: "\u041E\u0442\u043F\u0443\u0441\u0442\u0438\u0442\u0435, \u0447\u0442\u043E\u0431\u044B \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C",
9852
+ replaceFile: "\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C",
9853
+ clearAll: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0441\u0435",
9481
9854
  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",
9482
9855
  // Validation errors
9483
9856
  required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
@@ -9618,10 +9991,10 @@ var defaultTheme = {
9618
9991
  fileUploadHoverBorderColor: "#3b82f6",
9619
9992
  // blue-500
9620
9993
  // Spacing
9621
- inputPaddingX: "0.75rem",
9622
- // 3 (12px)
9623
- inputPaddingY: "0.5rem",
9624
- // 2 (8px)
9994
+ inputPaddingX: "0.5rem",
9995
+ // 8px (compact density v2)
9996
+ inputPaddingY: "0.25rem",
9997
+ // 4px (compact density v2)
9625
9998
  borderRadius: "0.5rem",
9626
9999
  // rounded-lg (8px)
9627
10000
  borderWidth: "1px",
@@ -10066,7 +10439,7 @@ var FormBuilderInstance = class {
10066
10439
  existingContainer.remove();
10067
10440
  }
10068
10441
  const actionsContainer = document.createElement("div");
10069
- actionsContainer.className = "form-level-actions-container mt-6 pt-4 flex flex-wrap gap-3 justify-center";
10442
+ actionsContainer.className = "form-level-actions-container mt-3 pt-2 flex flex-wrap gap-2 justify-center";
10070
10443
  actionsContainer.style.cssText = `
10071
10444
  border-top: var(--fb-border-width) solid var(--fb-border-color);
10072
10445
  `;
@@ -10207,7 +10580,7 @@ var FormBuilderInstance = class {
10207
10580
  */
10208
10581
  createRootPrefillHints(hints) {
10209
10582
  const hintsContainer = document.createElement("div");
10210
- hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-4";
10583
+ hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-2";
10211
10584
  hints.forEach((hint) => {
10212
10585
  const hintButton = document.createElement("button");
10213
10586
  hintButton.type = "button";
@@ -10240,7 +10613,7 @@ var FormBuilderInstance = class {
10240
10613
  root.setAttribute("data-fb-root", "true");
10241
10614
  injectThemeVariables(root, this.state.config.theme);
10242
10615
  const rootContainer = document.createElement("div");
10243
- rootContainer.className = "space-y-6";
10616
+ rootContainer.className = "space-y-2";
10244
10617
  if (schema.prefillHints && !this.state.config.readonly) {
10245
10618
  const hintsContainer = this.createRootPrefillHints(schema.prefillHints);
10246
10619
  rootContainer.appendChild(hintsContainer);
@@ -10248,9 +10621,9 @@ var FormBuilderInstance = class {
10248
10621
  const fieldsWrapper = document.createElement("div");
10249
10622
  const columns = schema.columns || 1;
10250
10623
  if (columns === 1) {
10251
- fieldsWrapper.className = "space-y-4";
10624
+ fieldsWrapper.className = "space-y-2";
10252
10625
  } else {
10253
- fieldsWrapper.className = `grid grid-cols-${columns} gap-4`;
10626
+ fieldsWrapper.className = `grid grid-cols-${columns} gap-2`;
10254
10627
  }
10255
10628
  schema.elements.forEach((element) => {
10256
10629
  if (element.type !== "markdown" && (element.hidden || element.type === "hidden")) {