@dmitryvim/form-builder 0.2.30 → 0.2.34

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)) {
@@ -386,6 +398,167 @@ function deepEqual(a, b) {
386
398
  return a === b;
387
399
  }
388
400
 
401
+ // src/utils/styles.ts
402
+ function mountCounterInLabel(wrapper, counter) {
403
+ const labelRow = wrapper.querySelector(
404
+ ":scope > [data-fb-label-row]"
405
+ );
406
+ if (labelRow) labelRow.appendChild(counter);
407
+ }
408
+ function createAddItemRow(classNameSuffix, onClick, options = {}) {
409
+ const label = options.label ?? "";
410
+ const showCounter = options.showCounter !== false;
411
+ const row = document.createElement("div");
412
+ row.className = "fb-add-row mt-2";
413
+ row.style.cssText = "display:flex;align-items:stretch;width:100%;";
414
+ const button = document.createElement("button");
415
+ button.type = "button";
416
+ button.className = `add-${classNameSuffix}-btn`;
417
+ button.style.cssText = `
418
+ flex: 1 1 auto;
419
+ display: inline-flex;
420
+ align-items: center;
421
+ justify-content: center;
422
+ gap: 6px;
423
+ padding: 6px 10px;
424
+ border: 1px dashed var(--fb-primary-color);
425
+ border-radius: var(--fb-border-radius);
426
+ background: transparent;
427
+ color: var(--fb-primary-color);
428
+ font-size: var(--fb-font-size-small, var(--fb-font-size));
429
+ font-weight: 500;
430
+ font-family: var(--fb-font-family);
431
+ cursor: pointer;
432
+ transition: border-color var(--fb-transition-duration), color var(--fb-transition-duration), background-color var(--fb-transition-duration);
433
+ `;
434
+ button.textContent = label ? `+ ${label}` : "+";
435
+ button.addEventListener("mouseenter", () => {
436
+ if (button.disabled) return;
437
+ button.style.borderStyle = "solid";
438
+ button.style.backgroundColor = "var(--fb-background-hover-color)";
439
+ });
440
+ button.addEventListener("mouseleave", () => {
441
+ button.style.borderStyle = "dashed";
442
+ button.style.backgroundColor = "transparent";
443
+ });
444
+ button.onclick = onClick;
445
+ const counter = document.createElement("span");
446
+ counter.className = "fb-add-counter";
447
+ counter.style.cssText = `
448
+ margin-left: auto;
449
+ font-size: var(--fb-font-size-small, 0.875rem);
450
+ color: var(--fb-text-secondary-color);
451
+ font-weight: 400;
452
+ `;
453
+ if (!showCounter) counter.style.display = "none";
454
+ row.appendChild(button);
455
+ const update = (current, max) => {
456
+ const reached = current >= max;
457
+ row.style.display = reached ? "none" : "flex";
458
+ button.style.display = reached ? "none" : "inline-flex";
459
+ button.disabled = reached;
460
+ if (showCounter) {
461
+ counter.textContent = `${current}/${max === Infinity ? "\u221E" : max}`;
462
+ }
463
+ };
464
+ return { row, button, counter, update };
465
+ }
466
+ function createSlideAddTile(onClick, options = {}) {
467
+ const label = options.label ?? "";
468
+ const tile = document.createElement("button");
469
+ tile.type = "button";
470
+ tile.className = "add-container-btn fb-slide-add";
471
+ tile.style.cssText = `
472
+ display: flex;
473
+ flex-direction: column;
474
+ align-items: center;
475
+ justify-content: center;
476
+ gap: 12px;
477
+ width: 100%;
478
+ min-height: 180px;
479
+ align-self: stretch;
480
+ padding: 24px 16px;
481
+ border: 1.5px dashed var(--fb-primary-color);
482
+ border-radius: var(--fb-border-radius);
483
+ background: transparent;
484
+ color: var(--fb-primary-color);
485
+ font-size: var(--fb-font-size-small, var(--fb-font-size));
486
+ font-weight: 500;
487
+ font-family: var(--fb-font-family);
488
+ cursor: pointer;
489
+ transition: border-color var(--fb-transition-duration), color var(--fb-transition-duration), background-color var(--fb-transition-duration);
490
+ `;
491
+ const circle = document.createElement("span");
492
+ circle.className = "fb-slide-add-circle";
493
+ circle.style.cssText = `
494
+ display: inline-flex;
495
+ align-items: center;
496
+ justify-content: center;
497
+ width: 36px;
498
+ height: 36px;
499
+ border: 1px solid var(--fb-primary-color);
500
+ border-radius: 50%;
501
+ background: var(--fb-background-color);
502
+ font-size: 20px;
503
+ line-height: 1;
504
+ color: inherit;
505
+ transition: inherit;
506
+ `;
507
+ circle.textContent = "+";
508
+ tile.appendChild(circle);
509
+ if (label) {
510
+ const text = document.createElement("span");
511
+ text.textContent = label;
512
+ tile.appendChild(text);
513
+ }
514
+ tile.addEventListener("mouseenter", () => {
515
+ if (tile.disabled) return;
516
+ tile.style.borderStyle = "solid";
517
+ tile.style.backgroundColor = "var(--fb-background-hover-color)";
518
+ });
519
+ tile.addEventListener("mouseleave", () => {
520
+ tile.style.borderStyle = "dashed";
521
+ tile.style.backgroundColor = "transparent";
522
+ });
523
+ tile.onclick = onClick;
524
+ const counter = document.createElement("span");
525
+ counter.className = "fb-add-counter";
526
+ counter.style.cssText = `
527
+ margin-left: auto;
528
+ font-size: var(--fb-font-size-small, 0.875rem);
529
+ color: var(--fb-text-secondary-color);
530
+ font-weight: 400;
531
+ `;
532
+ const update = (current, max) => {
533
+ const reached = current >= max;
534
+ tile.style.display = reached ? "none" : "flex";
535
+ tile.disabled = reached;
536
+ counter.textContent = `${current}/${max === Infinity ? "\u221E" : max}`;
537
+ };
538
+ return { tile, counter, update };
539
+ }
540
+ function applyActionButtonStyles(button, isFormLevel = false) {
541
+ button.style.cssText = `
542
+ background-color: var(--fb-action-bg-color);
543
+ color: var(--fb-action-text-color);
544
+ border: var(--fb-border-width) solid var(--fb-action-border-color);
545
+ padding: ${isFormLevel ? "0.5rem 1rem" : "0.5rem 0.75rem"};
546
+ font-size: var(--fb-font-size);
547
+ font-weight: var(--fb-font-weight-medium);
548
+ border-radius: var(--fb-border-radius);
549
+ transition: all var(--fb-transition-duration);
550
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
551
+ `;
552
+ button.addEventListener("mouseenter", () => {
553
+ button.style.backgroundColor = "var(--fb-action-hover-bg-color)";
554
+ button.style.borderColor = "var(--fb-action-hover-border-color)";
555
+ });
556
+ button.addEventListener("mouseleave", () => {
557
+ button.style.backgroundColor = "var(--fb-action-bg-color)";
558
+ button.style.borderColor = "var(--fb-action-border-color)";
559
+ });
560
+ }
561
+
389
562
  // src/components/text.ts
390
563
  function createCharCounter(element, input, isTextarea = false) {
391
564
  const counter = document.createElement("span");
@@ -438,12 +611,13 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
438
611
  const readonly = isElementReadonly(element, state, ctx);
439
612
  const inputWrapper = document.createElement("div");
440
613
  inputWrapper.style.cssText = "position: relative;";
614
+ const hasCharCounter = !readonly && (element.minLength != null || element.maxLength != null);
441
615
  const textInput = document.createElement("input");
442
616
  textInput.type = "text";
443
617
  textInput.className = "w-full rounded-lg";
444
618
  textInput.style.cssText = `
445
619
  padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
446
- padding-right: 60px;
620
+ ${hasCharCounter ? "padding-right: 60px;" : ""}
447
621
  border: var(--fb-border-width) solid var(--fb-border-color);
448
622
  border-radius: var(--fb-border-radius);
449
623
  background-color: ${readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
@@ -488,7 +662,7 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
488
662
  textInput.addEventListener("input", handleChange);
489
663
  }
490
664
  inputWrapper.appendChild(textInput);
491
- if (!readonly && (element.minLength != null || element.maxLength != null)) {
665
+ if (hasCharCounter) {
492
666
  const counter = createCharCounter(element, textInput, false);
493
667
  inputWrapper.appendChild(counter);
494
668
  }
@@ -499,6 +673,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
499
673
  const readonly = isElementReadonly(element, state, ctx);
500
674
  const prefillValues = ctx.prefill[element.key] || [];
501
675
  const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
676
+ const hasCharCounter = !readonly && (element.minLength != null || element.maxLength != null);
502
677
  const minCount = element.minCount ?? 1;
503
678
  const maxCount = element.maxCount ?? Infinity;
504
679
  while (values.length < minCount) {
@@ -525,7 +700,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
525
700
  textInput.type = "text";
526
701
  textInput.style.cssText = `
527
702
  padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
528
- padding-right: 60px;
703
+ ${hasCharCounter ? "padding-right: 60px;" : ""}
529
704
  border: var(--fb-border-width) solid var(--fb-border-color);
530
705
  border-radius: var(--fb-border-radius);
531
706
  background-color: ${readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
@@ -569,7 +744,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
569
744
  textInput.addEventListener("input", handleChange);
570
745
  }
571
746
  inputContainer.appendChild(textInput);
572
- if (!readonly && (element.minLength != null || element.maxLength != null)) {
747
+ if (hasCharCounter) {
573
748
  const counter = createCharCounter(element, textInput, false);
574
749
  inputContainer.appendChild(counter);
575
750
  }
@@ -626,50 +801,24 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
626
801
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
627
802
  });
628
803
  }
629
- let addRow = null;
630
- let countDisplay = null;
804
+ let addUpdate = null;
631
805
  if (!readonly) {
632
- addRow = document.createElement("div");
633
- addRow.className = "flex items-center gap-3 mt-2";
634
- const addBtn = document.createElement("button");
635
- addBtn.type = "button";
636
- addBtn.className = "add-text-btn px-3 py-1 rounded";
637
- addBtn.style.cssText = `
638
- color: var(--fb-primary-color);
639
- border: var(--fb-border-width) solid var(--fb-primary-color);
640
- background-color: transparent;
641
- font-size: var(--fb-font-size);
642
- transition: all var(--fb-transition-duration);
643
- `;
644
- addBtn.textContent = "+";
645
- addBtn.addEventListener("mouseenter", () => {
646
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
647
- });
648
- addBtn.addEventListener("mouseleave", () => {
649
- addBtn.style.backgroundColor = "transparent";
650
- });
651
- addBtn.onclick = () => {
652
- values.push(element.default || "");
653
- addTextItem(element.default || "");
654
- updateAddButton();
655
- updateRemoveButtons();
656
- };
657
- countDisplay = document.createElement("span");
658
- countDisplay.className = "text-sm text-gray-500";
659
- addRow.appendChild(addBtn);
660
- addRow.appendChild(countDisplay);
661
- wrapper.appendChild(addRow);
806
+ const handle = createAddItemRow(
807
+ "text",
808
+ () => {
809
+ values.push(element.default || "");
810
+ addTextItem(element.default || "");
811
+ updateAddButton();
812
+ updateRemoveButtons();
813
+ },
814
+ { label: element.addLabel }
815
+ );
816
+ addUpdate = handle.update;
817
+ mountCounterInLabel(wrapper, handle.counter);
818
+ wrapper.appendChild(handle.row);
662
819
  }
663
820
  function updateAddButton() {
664
- if (!addRow || !countDisplay) return;
665
- const addBtn = addRow.querySelector(".add-text-btn");
666
- if (addBtn) {
667
- const disabled = values.length >= maxCount;
668
- addBtn.disabled = disabled;
669
- addBtn.style.opacity = disabled ? "0.5" : "1";
670
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
671
- }
672
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
821
+ if (addUpdate) addUpdate(values.length, maxCount);
673
822
  }
674
823
  values.forEach((value) => addTextItem(value));
675
824
  updateAddButton();
@@ -840,8 +989,12 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
840
989
  const textareaWrapper = document.createElement("div");
841
990
  textareaWrapper.style.cssText = "position: relative;";
842
991
  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;";
992
+ textareaInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
993
+ textareaInput.style.cssText = `
994
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x) 24px var(--fb-input-padding-x);
995
+ font-size: var(--fb-font-size);
996
+ font-family: var(--fb-font-family);
997
+ `;
845
998
  textareaInput.name = pathKey;
846
999
  textareaInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
847
1000
  textareaInput.rows = element.rows || 4;
@@ -893,8 +1046,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
893
1046
  const textareaContainer = document.createElement("div");
894
1047
  textareaContainer.style.cssText = "position: relative;";
895
1048
  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;";
1049
+ textareaInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
1050
+ textareaInput.style.cssText = `
1051
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x) 24px var(--fb-input-padding-x);
1052
+ font-size: var(--fb-font-size);
1053
+ font-family: var(--fb-font-family);
1054
+ `;
898
1055
  textareaInput.placeholder = element.placeholder || t("placeholderText", state);
899
1056
  textareaInput.rows = element.rows || 4;
900
1057
  textareaInput.value = value;
@@ -957,52 +1114,24 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
957
1114
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
958
1115
  });
959
1116
  }
960
- let addRow = null;
961
- let countDisplay = null;
1117
+ let addUpdate = null;
962
1118
  if (!readonly) {
963
- addRow = document.createElement("div");
964
- addRow.className = "flex items-center gap-3 mt-2";
965
- const addBtn = document.createElement("button");
966
- addBtn.type = "button";
967
- addBtn.className = "add-textarea-btn px-3 py-1 rounded";
968
- addBtn.style.cssText = `
969
- color: var(--fb-primary-color);
970
- border: var(--fb-border-width) solid var(--fb-primary-color);
971
- background-color: transparent;
972
- font-size: var(--fb-font-size);
973
- transition: all var(--fb-transition-duration);
974
- `;
975
- addBtn.textContent = "+";
976
- addBtn.addEventListener("mouseenter", () => {
977
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
978
- });
979
- addBtn.addEventListener("mouseleave", () => {
980
- addBtn.style.backgroundColor = "transparent";
981
- });
982
- addBtn.onclick = () => {
983
- values.push(element.default || "");
984
- addTextareaItem(element.default || "");
985
- updateAddButton();
986
- updateRemoveButtons();
987
- };
988
- countDisplay = document.createElement("span");
989
- countDisplay.className = "text-sm text-gray-500";
990
- addRow.appendChild(addBtn);
991
- addRow.appendChild(countDisplay);
992
- wrapper.appendChild(addRow);
1119
+ const handle = createAddItemRow(
1120
+ "textarea",
1121
+ () => {
1122
+ values.push(element.default || "");
1123
+ addTextareaItem(element.default || "");
1124
+ updateAddButton();
1125
+ updateRemoveButtons();
1126
+ },
1127
+ { label: element.addLabel }
1128
+ );
1129
+ addUpdate = handle.update;
1130
+ mountCounterInLabel(wrapper, handle.counter);
1131
+ wrapper.appendChild(handle.row);
993
1132
  }
994
1133
  function updateAddButton() {
995
- if (!addRow || !countDisplay) return;
996
- const addBtn = addRow.querySelector(
997
- ".add-textarea-btn"
998
- );
999
- if (addBtn) {
1000
- const disabled = values.length >= maxCount;
1001
- addBtn.disabled = disabled;
1002
- addBtn.style.opacity = disabled ? "0.5" : "1";
1003
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
1004
- }
1005
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
1134
+ if (addUpdate) addUpdate(values.length, maxCount);
1006
1135
  }
1007
1136
  values.forEach((value) => addTextareaItem(value));
1008
1137
  updateAddButton();
@@ -1080,8 +1209,14 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1080
1209
  inputWrapper.style.cssText = "position: relative;";
1081
1210
  const numberInput = document.createElement("input");
1082
1211
  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;";
1212
+ numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1213
+ numberInput.style.cssText = `
1214
+ padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
1215
+ font-size: var(--fb-font-size);
1216
+ font-family: var(--fb-font-family);
1217
+ width: 100%;
1218
+ box-sizing: border-box;
1219
+ `;
1085
1220
  numberInput.name = pathKey;
1086
1221
  numberInput.placeholder = element.placeholder || "0";
1087
1222
  if (element.min !== void 0) numberInput.min = element.min.toString();
@@ -1133,8 +1268,14 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1133
1268
  inputContainer.style.cssText = "position: relative; flex: 1;";
1134
1269
  const numberInput = document.createElement("input");
1135
1270
  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;";
1271
+ numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1272
+ numberInput.style.cssText = `
1273
+ padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
1274
+ font-size: var(--fb-font-size);
1275
+ font-family: var(--fb-font-family);
1276
+ width: 100%;
1277
+ box-sizing: border-box;
1278
+ `;
1138
1279
  numberInput.placeholder = element.placeholder || "0";
1139
1280
  if (element.min !== void 0) numberInput.min = element.min.toString();
1140
1281
  if (element.max !== void 0) numberInput.max = element.max.toString();
@@ -1196,50 +1337,24 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1196
1337
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1197
1338
  });
1198
1339
  }
1199
- let addRow = null;
1200
- let countDisplay = null;
1340
+ let addUpdate = null;
1201
1341
  if (!readonly) {
1202
- addRow = document.createElement("div");
1203
- addRow.className = "flex items-center gap-3 mt-2";
1204
- const addBtn = document.createElement("button");
1205
- addBtn.type = "button";
1206
- addBtn.className = "add-number-btn px-3 py-1 rounded";
1207
- addBtn.style.cssText = `
1208
- color: var(--fb-primary-color);
1209
- border: var(--fb-border-width) solid var(--fb-primary-color);
1210
- background-color: transparent;
1211
- font-size: var(--fb-font-size);
1212
- transition: all var(--fb-transition-duration);
1213
- `;
1214
- addBtn.textContent = "+";
1215
- addBtn.addEventListener("mouseenter", () => {
1216
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
1217
- });
1218
- addBtn.addEventListener("mouseleave", () => {
1219
- addBtn.style.backgroundColor = "transparent";
1220
- });
1221
- addBtn.onclick = () => {
1222
- values.push(element.default || "");
1223
- addNumberItem(element.default || "");
1224
- updateAddButton();
1225
- updateRemoveButtons();
1226
- };
1227
- countDisplay = document.createElement("span");
1228
- countDisplay.className = "text-sm text-gray-500";
1229
- addRow.appendChild(addBtn);
1230
- addRow.appendChild(countDisplay);
1231
- wrapper.appendChild(addRow);
1342
+ const handle = createAddItemRow(
1343
+ "number",
1344
+ () => {
1345
+ values.push(element.default || "");
1346
+ addNumberItem(element.default || "");
1347
+ updateAddButton();
1348
+ updateRemoveButtons();
1349
+ },
1350
+ { label: element.addLabel }
1351
+ );
1352
+ addUpdate = handle.update;
1353
+ mountCounterInLabel(wrapper, handle.counter);
1354
+ wrapper.appendChild(handle.row);
1232
1355
  }
1233
1356
  function updateAddButton() {
1234
- if (!addRow || !countDisplay) return;
1235
- const addBtn = addRow.querySelector(".add-number-btn");
1236
- if (addBtn) {
1237
- const disabled = values.length >= maxCount;
1238
- addBtn.disabled = disabled;
1239
- addBtn.style.opacity = disabled ? "0.5" : "1";
1240
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
1241
- }
1242
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
1357
+ if (addUpdate) addUpdate(values.length, maxCount);
1243
1358
  }
1244
1359
  values.forEach((value) => addNumberItem(value));
1245
1360
  updateAddButton();
@@ -1405,7 +1520,12 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
1405
1520
  const state = ctx.state;
1406
1521
  const readonly = isElementReadonly(element, state, ctx);
1407
1522
  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";
1523
+ selectInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1524
+ selectInput.style.cssText = `
1525
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
1526
+ font-size: var(--fb-font-size);
1527
+ font-family: var(--fb-font-family);
1528
+ `;
1409
1529
  selectInput.name = pathKey;
1410
1530
  selectInput.disabled = readonly;
1411
1531
  (element.options || []).forEach((option) => {
@@ -1457,7 +1577,12 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1457
1577
  const itemWrapper = document.createElement("div");
1458
1578
  itemWrapper.className = "multiple-select-item flex items-center gap-2";
1459
1579
  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";
1580
+ selectInput.className = "flex-1 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1581
+ selectInput.style.cssText = `
1582
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
1583
+ font-size: var(--fb-font-size);
1584
+ font-family: var(--fb-font-family);
1585
+ `;
1461
1586
  selectInput.disabled = readonly;
1462
1587
  (element.options || []).forEach((option) => {
1463
1588
  const optionElement = document.createElement("option");
@@ -1514,51 +1639,25 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1514
1639
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1515
1640
  });
1516
1641
  }
1517
- let addRow = null;
1518
- let countDisplay = null;
1642
+ let addUpdate = null;
1519
1643
  if (!readonly) {
1520
- addRow = document.createElement("div");
1521
- addRow.className = "flex items-center gap-3 mt-2";
1522
- const addBtn = document.createElement("button");
1523
- addBtn.type = "button";
1524
- addBtn.className = "add-select-btn px-3 py-1 rounded";
1525
- addBtn.style.cssText = `
1526
- color: var(--fb-primary-color);
1527
- border: var(--fb-border-width) solid var(--fb-primary-color);
1528
- background-color: transparent;
1529
- font-size: var(--fb-font-size);
1530
- transition: all var(--fb-transition-duration);
1531
- `;
1532
- addBtn.textContent = "+";
1533
- addBtn.addEventListener("mouseenter", () => {
1534
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
1535
- });
1536
- addBtn.addEventListener("mouseleave", () => {
1537
- addBtn.style.backgroundColor = "transparent";
1538
- });
1539
- addBtn.onclick = () => {
1540
- const defaultValue = element.default || element.options?.[0]?.value || "";
1541
- values.push(defaultValue);
1542
- addSelectItem(defaultValue);
1543
- updateAddButton();
1544
- updateRemoveButtons();
1545
- };
1546
- countDisplay = document.createElement("span");
1547
- countDisplay.className = "text-sm text-gray-500";
1548
- addRow.appendChild(addBtn);
1549
- addRow.appendChild(countDisplay);
1550
- wrapper.appendChild(addRow);
1644
+ const handle = createAddItemRow(
1645
+ "select",
1646
+ () => {
1647
+ const defaultValue = element.default || element.options?.[0]?.value || "";
1648
+ values.push(defaultValue);
1649
+ addSelectItem(defaultValue);
1650
+ updateAddButton();
1651
+ updateRemoveButtons();
1652
+ },
1653
+ { label: element.addLabel }
1654
+ );
1655
+ addUpdate = handle.update;
1656
+ mountCounterInLabel(wrapper, handle.counter);
1657
+ wrapper.appendChild(handle.row);
1551
1658
  }
1552
1659
  function updateAddButton() {
1553
- if (!addRow || !countDisplay) return;
1554
- const addBtn = addRow.querySelector(".add-select-btn");
1555
- if (addBtn) {
1556
- const disabled = values.length >= maxCount;
1557
- addBtn.disabled = disabled;
1558
- addBtn.style.opacity = disabled ? "0.5" : "1";
1559
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
1560
- }
1561
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
1660
+ if (addUpdate) addUpdate(values.length, maxCount);
1562
1661
  }
1563
1662
  values.forEach((value) => addSelectItem(value));
1564
1663
  updateAddButton();
@@ -1901,53 +2000,25 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
1901
2000
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1902
2001
  });
1903
2002
  }
1904
- let addRow = null;
1905
- let countDisplay = null;
2003
+ let addUpdate = null;
1906
2004
  if (!readonly) {
1907
- addRow = document.createElement("div");
1908
- addRow.className = "flex items-center gap-3 mt-2";
1909
- const addBtn = document.createElement("button");
1910
- addBtn.type = "button";
1911
- addBtn.className = "add-switcher-btn px-3 py-1 rounded";
1912
- addBtn.style.cssText = `
1913
- color: var(--fb-primary-color);
1914
- border: var(--fb-border-width) solid var(--fb-primary-color);
1915
- background-color: transparent;
1916
- font-size: var(--fb-font-size);
1917
- transition: all var(--fb-transition-duration);
1918
- `;
1919
- addBtn.textContent = "+";
1920
- addBtn.addEventListener("mouseenter", () => {
1921
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
1922
- });
1923
- addBtn.addEventListener("mouseleave", () => {
1924
- addBtn.style.backgroundColor = "transparent";
1925
- });
1926
- addBtn.onclick = () => {
1927
- const defaultValue = element.default || element.options?.[0]?.value || "";
1928
- values.push(defaultValue);
1929
- addSwitcherItem(defaultValue);
1930
- updateAddButton();
1931
- updateRemoveButtons();
1932
- };
1933
- countDisplay = document.createElement("span");
1934
- countDisplay.className = "text-sm text-gray-500";
1935
- addRow.appendChild(addBtn);
1936
- addRow.appendChild(countDisplay);
1937
- wrapper.appendChild(addRow);
2005
+ const handle = createAddItemRow(
2006
+ "switcher",
2007
+ () => {
2008
+ const defaultValue = element.default || element.options?.[0]?.value || "";
2009
+ values.push(defaultValue);
2010
+ addSwitcherItem(defaultValue);
2011
+ updateAddButton();
2012
+ updateRemoveButtons();
2013
+ },
2014
+ { label: element.addLabel }
2015
+ );
2016
+ addUpdate = handle.update;
2017
+ mountCounterInLabel(wrapper, handle.counter);
2018
+ wrapper.appendChild(handle.row);
1938
2019
  }
1939
2020
  function updateAddButton() {
1940
- if (!addRow || !countDisplay) return;
1941
- const addBtn = addRow.querySelector(
1942
- ".add-switcher-btn"
1943
- );
1944
- if (addBtn) {
1945
- const disabled = values.length >= maxCount;
1946
- addBtn.disabled = disabled;
1947
- addBtn.style.opacity = disabled ? "0.5" : "1";
1948
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
1949
- }
1950
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
2021
+ if (addUpdate) addUpdate(values.length, maxCount);
1951
2022
  }
1952
2023
  values.forEach((value) => addSwitcherItem(value));
1953
2024
  updateAddButton();
@@ -2192,7 +2263,13 @@ function ensureFileStyles() {
2192
2263
  style.textContent = `
2193
2264
  @keyframes fb-spin { to { transform: rotate(360deg); } }
2194
2265
 
2195
- /* Spinner used during single-file and multi-file upload */
2266
+ /* \u2500\u2500\u2500 Checker background utility \u2500\u2500\u2500 */
2267
+ /* Neutral diagonal-stripe background for image previews (never crops) */
2268
+ .fb-checker {
2269
+ background-image: repeating-linear-gradient(45deg, #fafafa 0 6px, #f3f4f6 6px 12px);
2270
+ }
2271
+
2272
+ /* \u2500\u2500\u2500 Spinner \u2500\u2500\u2500 */
2196
2273
  .fb-spinner {
2197
2274
  width: 36px;
2198
2275
  height: 36px;
@@ -2203,207 +2280,271 @@ function ensureFileStyles() {
2203
2280
  flex-shrink: 0;
2204
2281
  }
2205
2282
 
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;
2283
+ /* \u2500\u2500\u2500 Wide single-file add tile (empty state) \u2500\u2500\u2500 */
2284
+ .fb-wide-tile {
2285
+ width: 100%;
2286
+ border-radius: 0.75rem;
2287
+ border: 1px dashed #60a5fa;
2288
+ background: rgba(239,246,255,0.5);
2289
+ display: flex;
2212
2290
  overflow: hidden;
2213
- border-radius: var(--fb-border-radius, 0.5rem);
2214
- background: var(--fb-file-upload-bg-color, #f3f4f6);
2291
+ height: 180px;
2292
+ transition: border-color 150ms, background 150ms, box-shadow 150ms;
2293
+ cursor: pointer;
2215
2294
  }
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);
2295
+ .fb-wide-tile:hover {
2296
+ background: #eff6ff;
2220
2297
  }
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);
2298
+ .fb-wide-tile.fb-drag-over {
2299
+ border-color: #3b82f6;
2300
+ border-width: 2px;
2301
+ background: #eff6ff;
2302
+ box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
2225
2303
  }
2226
2304
 
2227
- /* "+" add-more tile */
2228
- .fb-tile-add {
2229
- border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2305
+ /* Upload zone inside wide tile */
2306
+ .fb-wide-tile-upload {
2307
+ flex: 1;
2230
2308
  display: flex;
2309
+ flex-direction: column;
2231
2310
  align-items: center;
2232
2311
  justify-content: center;
2312
+ gap: 8px;
2313
+ color: #2563eb;
2314
+ padding: 16px;
2315
+ transition: background 150ms;
2233
2316
  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);
2317
+ background: transparent;
2318
+ border: none;
2319
+ font-family: inherit;
2239
2320
  }
2240
- .fb-tile-add:hover {
2241
- border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2242
- color: var(--fb-text-color, #1f2937);
2321
+ .fb-wide-tile-upload:hover {
2322
+ background: rgba(191,219,254,0.25);
2243
2323
  }
2244
2324
 
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;
2325
+ /* Vertical dashed divider between upload and library zones */
2326
+ .fb-wide-tile-divider {
2327
+ width: 1px;
2328
+ margin: 16px 0;
2329
+ border-left: 1px dashed rgba(96,165,250,0.5);
2330
+ background: transparent;
2331
+ flex-shrink: 0;
2255
2332
  }
2256
2333
 
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);
2334
+ /* Library zone inside wide tile */
2335
+ .fb-wide-tile-library {
2336
+ width: 176px;
2337
+ flex-shrink: 0;
2263
2338
  display: flex;
2264
2339
  flex-direction: column;
2265
2340
  align-items: center;
2266
2341
  justify-content: center;
2267
- gap: 4px;
2342
+ gap: 8px;
2343
+ color: #2563eb;
2344
+ padding: 12px;
2345
+ transition: background 150ms;
2268
2346
  cursor: pointer;
2269
- transition:
2270
- border-color var(--fb-transition-duration, 200ms),
2271
- background var(--fb-transition-duration, 200ms);
2347
+ background: transparent;
2348
+ border: none;
2349
+ font-family: inherit;
2272
2350
  }
2273
- .fb-file-dropzone:hover {
2274
- border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2275
- background: var(--fb-background-hover-color, #f9fafb);
2351
+ .fb-wide-tile-library:hover {
2352
+ background: rgba(191,219,254,0.25);
2276
2353
  }
2277
2354
 
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;
2355
+ /* \u2500\u2500\u2500 Multi-file outer grid container \u2500\u2500\u2500 */
2356
+ .fb-multi-outer {
2357
+ border-radius: 0.75rem;
2358
+ border: 1px dashed #cbd5e1;
2359
+ background: rgba(248,250,252,0.4);
2360
+ padding: 12px;
2361
+ transition: border-color 150ms, background 150ms, box-shadow 150ms;
2362
+ }
2363
+ .fb-multi-outer.fb-drag-over {
2364
+ border-width: 2px;
2365
+ border-color: #3b82f6;
2366
+ background: rgba(239,246,255,0.4);
2367
+ box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
2286
2368
  }
2287
- .fb-tile-uploading-text {
2288
- font-size: 8px;
2289
- color: var(--fb-file-upload-text-color, #9ca3af);
2369
+
2370
+ /* With files present: white solid border */
2371
+ .fb-multi-outer.fb-multi-has-files {
2372
+ border-style: solid;
2373
+ border-color: #e2e8f0;
2374
+ background: #fff;
2290
2375
  }
2291
- .fb-tile-hint {
2292
- font-size: 11px;
2293
- color: var(--fb-file-upload-text-color, #9ca3af);
2294
- margin-top: 4px;
2376
+
2377
+ /* The CSS grid inside */
2378
+ .fb-multi-grid {
2379
+ display: grid;
2380
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
2381
+ gap: 10px;
2295
2382
  }
2296
- .fb-tile-empty-text {
2297
- font-size: 12px;
2298
- color: var(--fb-text-secondary-color, #6b7280);
2299
- padding: 4px 0;
2383
+
2384
+ /* \u2500\u2500\u2500 Multi square add-tile (combined upload + library) \u2500\u2500\u2500 */
2385
+ .fb-multi-add-tile {
2386
+ aspect-ratio: 1 / 1;
2387
+ border-radius: 0.5rem;
2388
+ border: 1px dashed #60a5fa;
2389
+ background: rgba(239,246,255,0.5);
2390
+ display: flex;
2391
+ flex-direction: column;
2392
+ overflow: hidden;
2393
+ transition: background 150ms;
2300
2394
  }
2301
- .fb-dropzone-primary-text {
2302
- font-size: 13px;
2303
- color: var(--fb-text-secondary-color, #6b7280);
2395
+ .fb-multi-add-tile:hover {
2396
+ background: #eff6ff;
2304
2397
  }
2305
- .fb-dropzone-hint-text {
2306
- font-size: 11px;
2307
- color: var(--fb-file-upload-text-color, #9ca3af);
2398
+ .fb-multi-add-tile.fb-drag-over-tile {
2399
+ border-width: 2px;
2400
+ border-color: #3b82f6;
2401
+ background: rgba(255,255,255,0.8);
2308
2402
  }
2309
2403
 
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;
2404
+ /* Upload half of add-tile */
2405
+ .fb-multi-add-upload {
2406
+ flex: 1;
2334
2407
  display: flex;
2408
+ flex-direction: column;
2335
2409
  align-items: center;
2336
2410
  justify-content: center;
2337
- opacity: 0;
2338
- transition: opacity var(--fb-transition-duration, 200ms);
2411
+ gap: 4px;
2412
+ color: #2563eb;
2413
+ cursor: pointer;
2414
+ background: transparent;
2415
+ border: none;
2416
+ font-family: inherit;
2417
+ width: 100%;
2418
+ transition: background 150ms;
2339
2419
  }
2340
- .fb-tile-resource:hover .fb-tile-x-btn {
2341
- opacity: 1;
2420
+ .fb-multi-add-upload:hover {
2421
+ background: rgba(191,219,254,0.35);
2342
2422
  }
2343
2423
 
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));
2424
+ /* Horizontal dashed divider inside add-tile */
2425
+ .fb-multi-add-divider {
2426
+ border-top: 1px dashed rgba(96,165,250,0.5);
2427
+ margin: 0;
2428
+ flex-shrink: 0;
2352
2429
  }
2353
- .fb-play-btn {
2354
- background: var(--fb-file-bg-color, rgba(255,255,255,0.9));
2355
- border-radius: 50%;
2430
+
2431
+ /* Library strip at bottom of add-tile */
2432
+ .fb-multi-add-library {
2433
+ padding: 6px 0;
2356
2434
  display: flex;
2357
2435
  align-items: center;
2358
2436
  justify-content: center;
2437
+ gap: 4px;
2438
+ color: #2563eb;
2439
+ font-size: 11px;
2440
+ font-weight: 500;
2441
+ cursor: pointer;
2442
+ background: transparent;
2443
+ border: none;
2444
+ font-family: inherit;
2445
+ width: 100%;
2446
+ transition: background 150ms;
2447
+ flex-shrink: 0;
2448
+ }
2449
+ .fb-multi-add-library:hover {
2450
+ background: rgba(191,219,254,0.35);
2359
2451
  }
2360
2452
 
2361
- /* Edit-mode local video preview wrapper */
2362
- .fb-video-preview-wrap {
2453
+ /* \u2500\u2500\u2500 Capacity placeholder squares \u2500\u2500\u2500 */
2454
+ .fb-multi-placeholder {
2455
+ aspect-ratio: 1 / 1;
2456
+ border-radius: 0.5rem;
2457
+ border: 1px solid #e2e8f0;
2458
+ }
2459
+ .fb-multi-placeholder.fb-drag-over {
2460
+ border-width: 2px;
2461
+ border-style: dashed;
2462
+ border-color: #93c5fd;
2463
+ background: rgba(219,234,254,0.6);
2464
+ }
2465
+
2466
+ /* \u2500\u2500\u2500 Filled preview tile \u2500\u2500\u2500 */
2467
+ .fb-preview-tile {
2468
+ aspect-ratio: 1 / 1;
2469
+ border-radius: 0.5rem;
2470
+ border: 1px solid #e2e8f0;
2471
+ overflow: hidden;
2363
2472
  position: relative;
2473
+ cursor: pointer;
2474
+ }
2475
+ .fb-preview-tile img {
2364
2476
  width: 100%;
2365
2477
  height: 100%;
2478
+ object-fit: contain;
2479
+ display: block;
2366
2480
  }
2367
2481
 
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;
2482
+ /* \u2500\u2500\u2500 Uploading placeholder tile \u2500\u2500\u2500 */
2483
+ .fb-uploading-tile {
2484
+ aspect-ratio: 1 / 1;
2485
+ border-radius: 0.5rem;
2486
+ border: 2px dashed #d1d5db;
2374
2487
  display: flex;
2375
- gap: 4px;
2376
- opacity: 0;
2377
- transition: opacity var(--fb-transition-duration, 200ms);
2378
- pointer-events: none;
2488
+ flex-direction: column;
2489
+ align-items: center;
2490
+ justify-content: center;
2491
+ gap: 6px;
2492
+ padding: 6px;
2379
2493
  }
2380
- .fb-video-preview-wrap:hover .fb-video-btn-overlay {
2381
- opacity: 1;
2382
- pointer-events: auto;
2494
+
2495
+ /* \u2500\u2500\u2500 Meta line below multi grid \u2500\u2500\u2500 */
2496
+ .fb-meta-line {
2497
+ margin-top: 10px;
2498
+ display: flex;
2499
+ align-items: center;
2500
+ justify-content: space-between;
2501
+ gap: 8px;
2502
+ flex-wrap: wrap;
2383
2503
  }
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;
2504
+ .fb-meta-text {
2505
+ font-size: 12px;
2506
+ color: #94a3b8;
2507
+ display: flex;
2508
+ align-items: center;
2509
+ gap: 8px;
2510
+ flex-wrap: wrap;
2392
2511
  }
2393
- .fb-video-btn-delete {
2394
- background: rgba(220, 38, 38, 0.85);
2512
+ .fb-meta-dot {
2513
+ width: 4px;
2514
+ height: 4px;
2515
+ border-radius: 50%;
2516
+ background: #cbd5e1;
2517
+ flex-shrink: 0;
2395
2518
  }
2396
- .fb-video-btn-delete:hover {
2397
- background: rgba(185, 28, 28, 0.95);
2519
+ .fb-meta-mono {
2520
+ font-family: ui-monospace, 'JetBrains Mono', monospace;
2521
+ font-size: 11px;
2522
+ letter-spacing: -0.02em;
2523
+ }
2524
+ .fb-clear-all-btn {
2525
+ font-size: 12px;
2526
+ color: #94a3b8;
2527
+ background: none;
2528
+ border: none;
2529
+ cursor: pointer;
2530
+ padding: 0;
2531
+ font-family: inherit;
2532
+ transition: color 150ms;
2533
+ white-space: nowrap;
2534
+ flex-shrink: 0;
2398
2535
  }
2399
- .fb-video-btn-change {
2400
- background: rgba(31, 41, 55, 0.85);
2536
+ .fb-clear-all-btn:hover {
2537
+ color: #dc2626;
2401
2538
  }
2402
- .fb-video-btn-change:hover {
2403
- background: rgba(17, 24, 39, 0.95);
2539
+
2540
+ /* \u2500\u2500\u2500 Empty text (readonly) \u2500\u2500\u2500 */
2541
+ .fb-tile-empty-text {
2542
+ font-size: 11px;
2543
+ color: var(--fb-text-secondary-color, #6b7280);
2544
+ padding: 4px 0;
2404
2545
  }
2405
2546
 
2406
- /* Tile action icon buttons (download / open / remove) \u2014 shown on tile hover */
2547
+ /* \u2500\u2500\u2500 Tile action buttons (for zoom popup, compat) \u2500\u2500\u2500 */
2407
2548
  .fb-tile-actions {
2408
2549
  position: absolute;
2409
2550
  top: 3px;
@@ -2415,37 +2556,35 @@ function ensureFileStyles() {
2415
2556
  transition: opacity var(--fb-transition-duration, 200ms);
2416
2557
  z-index: 10;
2417
2558
  }
2418
- .fb-tile-resource:hover .fb-tile-actions {
2559
+ .fb-preview-tile:hover .fb-tile-actions {
2419
2560
  opacity: 1;
2420
2561
  }
2421
2562
  .fb-tile-action-btn {
2422
- width: 28px;
2423
- height: 28px;
2563
+ width: 24px;
2564
+ height: 24px;
2424
2565
  display: flex;
2425
2566
  align-items: center;
2426
2567
  justify-content: center;
2427
- border: none;
2428
- border-radius: 50%;
2568
+ border: 1px solid rgba(15,23,42,0.08);
2569
+ border-radius: 0.375rem;
2429
2570
  cursor: pointer;
2430
- background: rgba(31, 41, 55, 0.75);
2431
- color: #fff;
2571
+ background: rgba(255,255,255,0.92);
2572
+ color: #374151;
2432
2573
  padding: 0;
2433
2574
  flex-shrink: 0;
2434
- transition:
2435
- background var(--fb-transition-duration, 200ms),
2436
- opacity var(--fb-transition-duration, 200ms);
2575
+ box-shadow: 0 1px 2px rgba(0,0,0,0.06);
2576
+ transition: background var(--fb-transition-duration, 200ms),
2577
+ color var(--fb-transition-duration, 200ms);
2437
2578
  }
2438
2579
  .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);
2580
+ background: #ffffff;
2581
+ color: #0f172a;
2443
2582
  }
2444
2583
  .fb-tile-action-remove:hover {
2445
- background: rgba(185, 28, 28, 0.95);
2584
+ color: #dc2626;
2446
2585
  }
2447
2586
 
2448
- /* Actions row inside zoom popup \u2014 always visible while popup is shown */
2587
+ /* Zoom popup action buttons always visible */
2449
2588
  .fb-tile-zoom-preview .fb-tile-actions {
2450
2589
  position: absolute;
2451
2590
  top: 6px;
@@ -2454,116 +2593,145 @@ function ensureFileStyles() {
2454
2593
  z-index: 10000;
2455
2594
  }
2456
2595
 
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;
2596
+ /* \u2500\u2500\u2500 Hover zoom preview popup \u2500\u2500\u2500 */
2597
+ .fb-tile-zoom-preview {
2598
+ position: fixed;
2599
+ z-index: 9999;
2600
+ background: var(--fb-background-color, #fff);
2601
+ border: 1px solid #e2e8f0;
2602
+ border-radius: 0.5rem;
2603
+ box-shadow: 0 4px 16px rgba(0,0,0,0.18);
2604
+ padding: 4px;
2605
+ width: 350px;
2606
+ height: 350px;
2607
+ pointer-events: none;
2608
+ opacity: 0;
2609
+ transition: opacity 150ms ease;
2462
2610
  }
2463
- .fb-file-card-row .fb-file-dropzone,
2464
- .fb-file-card-row .fb-file-library-card {
2465
- flex: 1;
2466
- min-width: 0;
2611
+ .fb-tile-zoom-preview.fb-tile-zoom-preview--visible {
2612
+ opacity: 1;
2613
+ }
2614
+ .fb-tile-zoom-preview-img {
2615
+ width: 100%;
2616
+ height: 100%;
2617
+ object-fit: contain;
2618
+ display: block;
2619
+ border-radius: calc(0.5rem - 2px);
2467
2620
  }
2468
2621
 
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);
2622
+ /* \u2500\u2500\u2500 Single-file uploading state \u2500\u2500\u2500 */
2623
+ .fb-single-uploading {
2624
+ height: 180px;
2625
+ border-radius: 0.75rem;
2626
+ border: 1px dashed #60a5fa;
2627
+ background: rgba(239,246,255,0.5);
2474
2628
  display: flex;
2475
2629
  flex-direction: column;
2476
2630
  align-items: center;
2477
2631
  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%;
2632
+ gap: 8px;
2486
2633
  }
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;
2634
+
2635
+ /* \u2500\u2500\u2500 Video overlays \u2500\u2500\u2500 */
2636
+ .fb-video-overlay {
2637
+ position: absolute;
2638
+ inset: 0;
2639
+ display: flex;
2640
+ align-items: center;
2641
+ justify-content: center;
2642
+ background: rgba(0,0,0,0.25);
2492
2643
  }
2493
- .fb-file-library-card-icon {
2494
- font-size: 24px;
2495
- line-height: 1;
2496
- flex-shrink: 0;
2644
+ .fb-play-btn {
2645
+ background: rgba(255,255,255,0.9);
2646
+ border-radius: 50%;
2647
+ display: flex;
2648
+ align-items: center;
2649
+ justify-content: center;
2497
2650
  }
2498
- .fb-file-library-card-label {
2499
- font-size: 13px;
2500
- color: var(--fb-text-secondary-color, #6b7280);
2651
+ .fb-video-preview-wrap {
2652
+ position: relative;
2653
+ width: 100%;
2654
+ height: 100%;
2655
+ }
2656
+ .fb-video-btn-overlay {
2657
+ position: absolute;
2658
+ top: 8px;
2659
+ right: 8px;
2660
+ z-index: 10;
2661
+ display: flex;
2662
+ gap: 4px;
2663
+ opacity: 0;
2664
+ transition: opacity 150ms;
2665
+ pointer-events: none;
2666
+ }
2667
+ .fb-video-preview-wrap:hover .fb-video-btn-overlay {
2668
+ opacity: 1;
2669
+ pointer-events: auto;
2501
2670
  }
2502
- .fb-file-library-card-hint {
2671
+ .fb-video-btn {
2672
+ border: none;
2673
+ border-radius: 4px;
2503
2674
  font-size: 11px;
2504
- color: var(--fb-file-upload-text-color, #9ca3af);
2675
+ padding: 4px 8px;
2676
+ cursor: pointer;
2677
+ color: #fff;
2678
+ line-height: 1.2;
2505
2679
  }
2680
+ .fb-video-btn-delete { background: rgba(220,38,38,0.85); }
2681
+ .fb-video-btn-delete:hover { background: rgba(185,28,28,0.95); }
2682
+ .fb-video-btn-change { background: rgba(31,41,55,0.85); }
2683
+ .fb-video-btn-change:hover { background: rgba(17,24,39,0.95); }
2506
2684
 
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;
2685
+ /* \u2500\u2500\u2500 Readonly readonly tile \u2500\u2500\u2500 */
2686
+ .fb-readonly-tile {
2687
+ aspect-ratio: 1 / 1;
2688
+ border-radius: 0.5rem;
2689
+ border: 1px solid #e2e8f0;
2525
2690
  overflow: hidden;
2526
- border-radius: var(--fb-border-radius, 0.5rem);
2691
+ position: relative;
2692
+ cursor: pointer;
2527
2693
  }
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;
2694
+ .fb-readonly-tile img {
2695
+ width: 100%;
2696
+ height: 100%;
2697
+ object-fit: contain;
2698
+ display: block;
2533
2699
  }
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;
2700
+ .fb-readonly-tile .fb-tile-actions {
2547
2701
  opacity: 0;
2548
- transition: opacity 150ms ease;
2549
2702
  }
2550
- .fb-tile-zoom-preview.fb-tile-zoom-preview--visible {
2703
+ .fb-readonly-tile:hover .fb-tile-actions {
2551
2704
  opacity: 1;
2552
2705
  }
2553
- .fb-tile-zoom-preview-img {
2706
+
2707
+ /* \u2500\u2500\u2500 Readonly single-file filled \u2500\u2500\u2500 */
2708
+ .fb-single-readonly-filled {
2709
+ position: relative;
2710
+ border-radius: 0.75rem;
2711
+ border: 1px solid #e2e8f0;
2712
+ overflow: hidden;
2713
+ height: 220px;
2714
+ display: block;
2715
+ cursor: pointer;
2716
+ }
2717
+ .fb-single-readonly-filled img {
2554
2718
  width: 100%;
2555
2719
  height: 100%;
2556
2720
  object-fit: contain;
2557
2721
  display: block;
2558
- background: var(--fb-file-upload-bg-color, #f3f4f6);
2559
- border-radius: calc(var(--fb-border-radius, 0.5rem) - 2px);
2722
+ }
2723
+
2724
+ /* \u2500\u2500\u2500 Readonly multi grid \u2500\u2500\u2500 */
2725
+ .fb-multi-readonly-grid {
2726
+ display: grid;
2727
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
2728
+ gap: 10px;
2560
2729
  }
2561
2730
  `;
2562
2731
  document.head.appendChild(style);
2563
2732
  }
2564
2733
 
2565
2734
  // src/components/file/dom.ts
2566
- var TILE_SIZE = "160px";
2567
2735
  function createFileTile() {
2568
2736
  ensureFileStyles();
2569
2737
  const tile = document.createElement("div");
@@ -2571,7 +2739,7 @@ function createFileTile() {
2571
2739
  return tile;
2572
2740
  }
2573
2741
  function showFileError(container, message) {
2574
- const existing = container.closest(".space-y-2")?.querySelector(".file-error-message");
2742
+ const existing = container.closest("[data-files-wrapper]")?.querySelector(".file-error-message");
2575
2743
  if (existing) existing.remove();
2576
2744
  const errorEl = document.createElement("div");
2577
2745
  errorEl.className = "file-error-message error-message";
@@ -2581,10 +2749,10 @@ function showFileError(container, message) {
2581
2749
  margin-top: 0.25rem;
2582
2750
  `;
2583
2751
  errorEl.textContent = message;
2584
- container.closest(".space-y-2")?.appendChild(errorEl);
2752
+ container.closest("[data-files-wrapper]")?.appendChild(errorEl);
2585
2753
  }
2586
2754
  function clearFileError(container) {
2587
- const existing = container.closest(".space-y-2")?.querySelector(".file-error-message");
2755
+ const existing = container.closest("[data-files-wrapper]")?.querySelector(".file-error-message");
2588
2756
  if (existing) existing.remove();
2589
2757
  }
2590
2758
  function addDeleteButton(container, state, onDelete) {
@@ -2602,13 +2770,6 @@ function addDeleteButton(container, state, onDelete) {
2602
2770
  overlay.appendChild(deleteBtn);
2603
2771
  container.appendChild(overlay);
2604
2772
  }
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
2773
  function createUploadingTile(fileName, state) {
2613
2774
  ensureFileStyles();
2614
2775
  const tile = createFileTile();
@@ -2624,10 +2785,13 @@ function createUploadingTile(fileName, state) {
2624
2785
  return tile;
2625
2786
  }
2626
2787
  function ensureTilesWrap(list) {
2788
+ const existingGrid = list.querySelector(".fb-multi-grid");
2789
+ if (existingGrid) return existingGrid;
2627
2790
  const existing = list.querySelector(".fb-tiles-wrap");
2628
2791
  if (existing) return existing;
2629
- const dropzone = list.querySelector(".fb-file-dropzone");
2630
- if (dropzone) dropzone.remove();
2792
+ list.querySelector(".fb-file-dropzone")?.remove();
2793
+ list.querySelector(".fb-wide-tile")?.remove();
2794
+ list.querySelector(".fb-multi-outer")?.remove();
2631
2795
  const tilesWrap = document.createElement("div");
2632
2796
  tilesWrap.className = "fb-tiles-wrap";
2633
2797
  tilesWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start;";
@@ -2639,7 +2803,7 @@ function ensureTilesWrap(list) {
2639
2803
  return tilesWrap;
2640
2804
  }
2641
2805
  function setEmptyFileContainer(fileContainer, state, hint) {
2642
- const hintHtml = hint ? `<div class="text-xs text-gray-500 mt-1">${escapeHtml(hint)}</div>` : "";
2806
+ const hintHtml = "";
2643
2807
  fileContainer.innerHTML = `
2644
2808
  <div class="flex flex-col items-center justify-center h-full text-gray-400">
2645
2809
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
@@ -2681,9 +2845,11 @@ function setupDragAndDrop(element, dropHandler) {
2681
2845
  }
2682
2846
 
2683
2847
  // 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>`;
2848
+ 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>`;
2849
+ 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>`;
2850
+ 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>`;
2851
+ 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>`;
2852
+ 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
2853
  function canDownload(state, meta) {
2688
2854
  return Boolean(
2689
2855
  state.config.downloadFile || state.config.getDownloadUrl || state.config.getThumbnail || meta?.file
@@ -2695,7 +2861,16 @@ function canOpenInTab(state, meta) {
2695
2861
  );
2696
2862
  }
2697
2863
  function createTileActions(options) {
2698
- const { canRemove, removeHandler, state, resourceId, fileName, meta } = options;
2864
+ const {
2865
+ canRemove,
2866
+ removeHandler,
2867
+ state,
2868
+ resourceId,
2869
+ fileName,
2870
+ meta,
2871
+ replaceHandler,
2872
+ libraryHandler
2873
+ } = options;
2699
2874
  const group = document.createElement("div");
2700
2875
  group.className = "fb-tile-actions";
2701
2876
  const makeBtn = (icon, label, cls) => {
@@ -2710,15 +2885,41 @@ function createTileActions(options) {
2710
2885
  });
2711
2886
  return btn;
2712
2887
  };
2888
+ if (replaceHandler) {
2889
+ const replaceBtn = makeBtn(
2890
+ ICON_REPLACE,
2891
+ t("replaceFile", state),
2892
+ "fb-tile-action-replace"
2893
+ );
2894
+ replaceBtn.addEventListener("click", () => replaceHandler());
2895
+ group.appendChild(replaceBtn);
2896
+ }
2897
+ if (libraryHandler) {
2898
+ const libBtn = makeBtn(
2899
+ ICON_LIBRARY,
2900
+ t("fromLibrary", state),
2901
+ "fb-tile-action-library"
2902
+ );
2903
+ libBtn.addEventListener("click", () => libraryHandler());
2904
+ group.appendChild(libBtn);
2905
+ }
2713
2906
  if (canDownload(state, meta)) {
2714
- const dlBtn = makeBtn(ICON_DOWNLOAD, t("downloadFile", state), "fb-tile-action-download");
2907
+ const dlBtn = makeBtn(
2908
+ ICON_DOWNLOAD,
2909
+ t("downloadFile", state),
2910
+ "fb-tile-action-download"
2911
+ );
2715
2912
  dlBtn.addEventListener("click", () => {
2716
2913
  triggerTileDownload(resourceId, fileName, state, meta);
2717
2914
  });
2718
2915
  group.appendChild(dlBtn);
2719
2916
  }
2720
2917
  if (canOpenInTab(state, meta)) {
2721
- const openBtn = makeBtn(ICON_OPEN, t("openInNewTab", state), "fb-tile-action-open");
2918
+ const openBtn = makeBtn(
2919
+ ICON_OPEN,
2920
+ t("openInNewTab", state),
2921
+ "fb-tile-action-open"
2922
+ );
2722
2923
  openBtn.addEventListener("click", () => {
2723
2924
  triggerTileOpen(resourceId, state, meta).catch((err) => {
2724
2925
  console.error("Open failed:", err);
@@ -2727,7 +2928,11 @@ function createTileActions(options) {
2727
2928
  group.appendChild(openBtn);
2728
2929
  }
2729
2930
  if (canRemove && removeHandler) {
2730
- const rmBtn = makeBtn(ICON_REMOVE, t("removeElement", state), "fb-tile-action-remove");
2931
+ const rmBtn = makeBtn(
2932
+ ICON_REMOVE,
2933
+ t("removeElement", state),
2934
+ "fb-tile-action-remove"
2935
+ );
2731
2936
  rmBtn.addEventListener("click", () => {
2732
2937
  removeHandler();
2733
2938
  });
@@ -2805,11 +3010,17 @@ function positionZoomPopup(popup, tile) {
2805
3010
  } else if (tileRect.bottom + margin + popupSize + padding <= window.innerHeight) {
2806
3011
  top = tileRect.bottom + margin;
2807
3012
  } else {
2808
- top = Math.max(padding, Math.min(window.innerHeight - popupSize - padding, tileRect.top));
3013
+ top = Math.max(
3014
+ padding,
3015
+ Math.min(window.innerHeight - popupSize - padding, tileRect.top)
3016
+ );
2809
3017
  }
2810
3018
  const tileCenterX = tileRect.left + tileRect.width / 2;
2811
3019
  let left = tileCenterX - popupSize / 2;
2812
- left = Math.max(padding, Math.min(window.innerWidth - popupSize - padding, left));
3020
+ left = Math.max(
3021
+ padding,
3022
+ Math.min(window.innerWidth - popupSize - padding, left)
3023
+ );
2813
3024
  popup.style.top = `${top}px`;
2814
3025
  popup.style.left = `${left}px`;
2815
3026
  }
@@ -2853,7 +3064,9 @@ function attachZoomHover(tile, src, alt, actionsEl) {
2853
3064
  const popup = getOrCreateZoomPopup();
2854
3065
  const existingActions = popup.querySelector(".fb-tile-actions");
2855
3066
  if (existingActions) existingActions.remove();
2856
- const img = popup.querySelector(".fb-tile-zoom-preview-img");
3067
+ const img = popup.querySelector(
3068
+ ".fb-tile-zoom-preview-img"
3069
+ );
2857
3070
  img.src = src;
2858
3071
  img.alt = alt;
2859
3072
  if (actionsEl) {
@@ -2881,7 +3094,9 @@ function attachZoomHover(tile, src, alt, actionsEl) {
2881
3094
  });
2882
3095
  }
2883
3096
  function attachClonedActionListeners(cloned, original) {
2884
- const originalBtns = Array.from(original.querySelectorAll(".fb-tile-action-btn"));
3097
+ const originalBtns = Array.from(
3098
+ original.querySelectorAll(".fb-tile-action-btn")
3099
+ );
2885
3100
  const clonedBtns = Array.from(cloned.querySelectorAll(".fb-tile-action-btn"));
2886
3101
  clonedBtns.forEach((clonedBtn, i) => {
2887
3102
  const origBtn = originalBtns[i];
@@ -2895,8 +3110,7 @@ function attachClonedActionListeners(cloned, original) {
2895
3110
  }
2896
3111
  function renderLocalImagePreview(container, file, fileName, state) {
2897
3112
  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)";
3113
+ img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
2900
3114
  img.alt = fileName || t("previewAlt", state);
2901
3115
  const reader = new FileReader();
2902
3116
  reader.onload = (e) => {
@@ -2918,7 +3132,7 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
2918
3132
  const newContainer = setupDragDropless(container);
2919
3133
  newContainer.innerHTML = `
2920
3134
  <div class="fb-video-preview-wrap">
2921
- <video class="w-full h-full object-contain" controls preload="auto" muted src="${videoUrl}">
3135
+ <video style="width:100%;height:100%;object-fit:contain;" controls preload="auto" muted src="${videoUrl}">
2922
3136
  ${escapeHtml(t("videoNotSupported", state))}
2923
3137
  </video>
2924
3138
  <div class="fb-video-btn-overlay">
@@ -2935,14 +3149,18 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
2935
3149
  return newContainer;
2936
3150
  }
2937
3151
  function attachVideoButtonHandlers(container, resourceId, state, deps) {
2938
- const changeBtn = container.querySelector(".change-file-btn");
3152
+ const changeBtn = container.querySelector(
3153
+ ".change-file-btn"
3154
+ );
2939
3155
  if (changeBtn) {
2940
3156
  changeBtn.onclick = (e) => {
2941
3157
  e.stopPropagation();
2942
3158
  deps?.picker?.click();
2943
3159
  };
2944
3160
  }
2945
- const deleteBtn = container.querySelector(".delete-file-btn");
3161
+ const deleteBtn = container.querySelector(
3162
+ ".delete-file-btn"
3163
+ );
2946
3164
  if (deleteBtn) {
2947
3165
  deleteBtn.onclick = (e) => {
2948
3166
  e.stopPropagation();
@@ -2962,11 +3180,11 @@ function handleVideoDelete(container, resourceId, state, deps) {
2962
3180
  container.onclick = deps.fileUploadHandler;
2963
3181
  }
2964
3182
  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">
3183
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);">
3184
+ <svg style="width:1.5rem;height:1.5rem;margin-bottom:0.5rem;" fill="currentColor" viewBox="0 0 24 24">
2967
3185
  <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
3186
  </svg>
2969
- <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
3187
+ <div style="font-size:0.875rem;text-align:center;">${escapeHtml(t("clickDragText", state))}</div>
2970
3188
  </div>
2971
3189
  `;
2972
3190
  if (deps?.setupDrop) {
@@ -2983,11 +3201,11 @@ function renderDeleteButton(container, resourceId, state) {
2983
3201
  hiddenInput.value = "";
2984
3202
  }
2985
3203
  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">
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">
2988
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"/>
2989
3207
  </svg>
2990
- <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
3208
+ <div style="font-size:0.875rem;text-align:center;">${escapeHtml(t("clickDragText", state))}</div>
2991
3209
  </div>
2992
3210
  `;
2993
3211
  });
@@ -3006,7 +3224,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
3006
3224
  deps
3007
3225
  );
3008
3226
  } 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>`;
3227
+ 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
3228
  }
3011
3229
  if (!isReadonly && !meta.type?.startsWith("video/")) {
3012
3230
  renderDeleteButton(container, resourceId, state);
@@ -3014,7 +3232,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
3014
3232
  }
3015
3233
  function renderUploadedVideoPreview(container, thumbnailUrl, state) {
3016
3234
  const video = document.createElement("video");
3017
- video.className = "w-full h-full object-contain";
3235
+ video.style.cssText = "width:100%;height:100%;object-fit:contain;";
3018
3236
  video.controls = true;
3019
3237
  video.preload = "metadata";
3020
3238
  video.muted = true;
@@ -3035,8 +3253,7 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
3035
3253
  renderUploadedVideoPreview(container, thumbnailUrl, state);
3036
3254
  } else {
3037
3255
  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)";
3256
+ img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
3040
3257
  img.alt = fileName || t("previewAlt", state);
3041
3258
  img.src = thumbnailUrl;
3042
3259
  container.appendChild(img);
@@ -3047,11 +3264,11 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
3047
3264
  } catch (error) {
3048
3265
  console.error("Failed to get thumbnail:", error);
3049
3266
  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">
3267
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);">
3268
+ <svg style="width:1.5rem;height:1.5rem;margin-bottom:0.5rem;" fill="currentColor" viewBox="0 0 24 24">
3052
3269
  <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
3270
  </svg>
3054
- <div class="text-sm text-center">${escapeHtml(fileName || t("previewUnavailable", state))}</div>
3271
+ <div style="font-size:0.875rem;text-align:center;">${escapeHtml(fileName || t("previewUnavailable", state))}</div>
3055
3272
  </div>
3056
3273
  `;
3057
3274
  }
@@ -3076,7 +3293,13 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
3076
3293
  deps
3077
3294
  );
3078
3295
  } else {
3079
- await renderUploadedFilePreview(container, resourceId, fileName, meta, state);
3296
+ await renderUploadedFilePreview(
3297
+ container,
3298
+ resourceId,
3299
+ fileName,
3300
+ meta,
3301
+ state
3302
+ );
3080
3303
  const isVideo = meta?.type?.startsWith("video/");
3081
3304
  if (!isReadonly && !isVideo) {
3082
3305
  renderDeleteButton(container, resourceId, state);
@@ -3103,7 +3326,8 @@ async function renderFilePreviewReadonly(resourceId, state, fileName, options =
3103
3326
  }
3104
3327
  const localFileUrl = meta?.file instanceof File ? getLocalFileUrl(meta.file) : null;
3105
3328
  const resolveOpenUrl = async () => {
3106
- if (state.config.getDownloadUrl) return state.config.getDownloadUrl(resourceId);
3329
+ if (state.config.getDownloadUrl)
3330
+ return state.config.getDownloadUrl(resourceId);
3107
3331
  if (state.config.getThumbnail) return state.config.getThumbnail(resourceId);
3108
3332
  return localFileUrl;
3109
3333
  };
@@ -3225,13 +3449,9 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
3225
3449
  const img = document.createElement("img");
3226
3450
  img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
3227
3451
  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);
3452
+ img.src = getLocalFileUrl(meta.file);
3234
3453
  tile.appendChild(img);
3454
+ attachZoomHover(tile, img.src, meta.name, actionsEl ?? null);
3235
3455
  } else if (state.config.getThumbnail) {
3236
3456
  try {
3237
3457
  const url = await state.config.getThumbnail(rid);
@@ -3247,7 +3467,8 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
3247
3467
  }
3248
3468
  } catch (error) {
3249
3469
  const err = error instanceof Error ? error : new Error(String(error));
3250
- if (state.config.onThumbnailError) state.config.onThumbnailError(err, rid);
3470
+ if (state.config.onThumbnailError)
3471
+ state.config.onThumbnailError(err, rid);
3251
3472
  tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:16px;color:var(--fb-error-color,#ef4444);">\u2715</div>`;
3252
3473
  }
3253
3474
  } else {
@@ -3280,7 +3501,8 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
3280
3501
  }
3281
3502
  } catch (error) {
3282
3503
  const err = error instanceof Error ? error : new Error(String(error));
3283
- if (state.config.onThumbnailError) state.config.onThumbnailError(err, rid);
3504
+ if (state.config.onThumbnailError)
3505
+ state.config.onThumbnailError(err, rid);
3284
3506
  tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:16px;color:var(--fb-error-color,#ef4444);">\u2715</div>`;
3285
3507
  }
3286
3508
  } else {
@@ -3288,17 +3510,20 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
3288
3510
  }
3289
3511
  if (actionsEl) tile.appendChild(actionsEl);
3290
3512
  } 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);
3513
+ fillDocumentFallback(tile, rid, meta, actionsEl);
3300
3514
  }
3301
3515
  }
3516
+ function fillDocumentFallback(tile, rid, meta, actionsEl) {
3517
+ const fileName = meta?.name ?? rid.split("/").pop() ?? "";
3518
+ if (fileName) tile.title = fileName;
3519
+ 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>` : "";
3520
+ tile.innerHTML = `
3521
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
3522
+ <div style="font-size:36px;">\u{1F4C1}</div>
3523
+ ${labelHtml}
3524
+ </div>`;
3525
+ if (actionsEl) tile.appendChild(actionsEl);
3526
+ }
3302
3527
  async function forceDownload(resourceId, fileName, state) {
3303
3528
  try {
3304
3529
  let fileUrl = null;
@@ -3310,7 +3535,8 @@ async function forceDownload(resourceId, fileName, state) {
3310
3535
  if (fileUrl) {
3311
3536
  const finalUrl = fileUrl.startsWith("http") ? fileUrl : new URL(fileUrl, window.location.href).href;
3312
3537
  const response = await fetch(finalUrl);
3313
- if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
3538
+ if (!response.ok)
3539
+ throw new Error(`HTTP error! status: ${response.status}`);
3314
3540
  const blob = await response.blob();
3315
3541
  downloadBlob(blob, fileName);
3316
3542
  } else {
@@ -3359,7 +3585,9 @@ async function uploadSingleFile(file, state) {
3359
3585
  } catch (error) {
3360
3586
  const err = error instanceof Error ? error : new Error(String(error));
3361
3587
  if (state.config.onUploadError) state.config.onUploadError(err, file);
3362
- throw new Error(`File upload failed: ${err.message}`);
3588
+ const wrapped = new Error(`File upload failed: ${err.message}`);
3589
+ wrapped.cause = err;
3590
+ throw wrapped;
3363
3591
  }
3364
3592
  }
3365
3593
  async function handleFileSelect(opts) {
@@ -3398,6 +3626,10 @@ async function handleFileSelect(opts) {
3398
3626
  return;
3399
3627
  }
3400
3628
  clearFileError(container);
3629
+ const existingHiddenInput = container.parentElement?.querySelector(
3630
+ 'input[type="hidden"]'
3631
+ );
3632
+ const previousRid = existingHiddenInput?.value || null;
3401
3633
  ensureFileStyles();
3402
3634
  container.innerHTML = `
3403
3635
  <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:6px;padding:6px;">
@@ -3408,7 +3640,13 @@ async function handleFileSelect(opts) {
3408
3640
  try {
3409
3641
  rid = await uploadSingleFile(file, state);
3410
3642
  } catch (error) {
3411
- setEmptyFileContainer(container, state);
3643
+ if (previousRid && deps?.onAfterUpload) {
3644
+ deps.onAfterUpload(container, previousRid);
3645
+ } else if (deps?.onRemove) {
3646
+ deps.onRemove();
3647
+ } else {
3648
+ setEmptyFileContainer(container, state);
3649
+ }
3412
3650
  throw error;
3413
3651
  }
3414
3652
  state.resourceIndex.set(rid, {
@@ -3418,9 +3656,10 @@ async function handleFileSelect(opts) {
3418
3656
  uploadedAt: /* @__PURE__ */ new Date(),
3419
3657
  file
3420
3658
  });
3421
- let hiddenInput = container.parentElement?.querySelector(
3422
- 'input[type="hidden"]'
3423
- );
3659
+ if (previousRid && previousRid !== rid) {
3660
+ releaseLocalFileUrl(state.resourceIndex.get(previousRid)?.file);
3661
+ }
3662
+ let hiddenInput = existingHiddenInput;
3424
3663
  if (!hiddenInput) {
3425
3664
  hiddenInput = document.createElement("input");
3426
3665
  hiddenInput.type = "hidden";
@@ -3429,7 +3668,9 @@ async function handleFileSelect(opts) {
3429
3668
  }
3430
3669
  hiddenInput.value = rid;
3431
3670
  const isVideo = file.type.startsWith("video/");
3432
- if (!isVideo && deps) {
3671
+ if (!isVideo && deps?.onAfterUpload) {
3672
+ deps.onAfterUpload(container, rid);
3673
+ } else if (!isVideo && deps) {
3433
3674
  renderSingleFileEditTile(container, rid, state, deps).catch(console.error);
3434
3675
  } else {
3435
3676
  renderFilePreview(container, rid, state, {
@@ -3458,7 +3699,9 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
3458
3699
  const rejectedBySize = afterMime.filter(
3459
3700
  (f) => !isFileSizeAllowed(f, constraints.maxSize)
3460
3701
  );
3461
- const valid = afterMime.filter((f) => isFileSizeAllowed(f, constraints.maxSize));
3702
+ const valid = afterMime.filter(
3703
+ (f) => isFileSizeAllowed(f, constraints.maxSize)
3704
+ );
3462
3705
  const remaining = constraints.maxCount === Infinity ? valid.length : Math.max(0, constraints.maxCount - currentCount);
3463
3706
  const accepted = valid.slice(0, remaining);
3464
3707
  const skippedByCount = valid.length - accepted.length;
@@ -3471,7 +3714,13 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
3471
3714
  if (rejectedByMime.length > 0) {
3472
3715
  const mimes = constraints.allowedMimes.join(", ");
3473
3716
  const names = rejectedByMime.map((f) => f.name).join(", ");
3474
- errorParts.push(t("invalidFileMime", state, { name: names, type: rejectedByMime.map((f) => f.type).join(", "), mimes }));
3717
+ errorParts.push(
3718
+ t("invalidFileMime", state, {
3719
+ name: names,
3720
+ type: rejectedByMime.map((f) => f.type).join(", "),
3721
+ mimes
3722
+ })
3723
+ );
3475
3724
  }
3476
3725
  if (rejectedBySize.length > 0) {
3477
3726
  const names = rejectedBySize.map((f) => f.name).join(", ");
@@ -3490,17 +3739,18 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
3490
3739
  return { accepted, errorMessage: errorParts.join(" \u2022 ") };
3491
3740
  }
3492
3741
  async function uploadBatch(accepted, resourceIds, listEl, state) {
3493
- await Promise.all(
3742
+ if (listEl) {
3743
+ const tilesWrap = ensureTilesWrap(listEl);
3744
+ const addTile = tilesWrap.querySelector(".fb-multi-add-tile-js") ?? tilesWrap.querySelector(".fb-tile-add");
3745
+ if (addTile) addTile.style.display = "none";
3746
+ }
3747
+ const failures = [];
3748
+ await Promise.allSettled(
3494
3749
  accepted.map(async (file) => {
3495
3750
  const placeholder = createUploadingTile(file.name, state);
3496
3751
  if (listEl) {
3497
3752
  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
- }
3753
+ tilesWrap.appendChild(placeholder);
3504
3754
  }
3505
3755
  try {
3506
3756
  const rid = await uploadSingleFile(file, state);
@@ -3512,11 +3762,27 @@ async function uploadBatch(accepted, resourceIds, listEl, state) {
3512
3762
  file: void 0
3513
3763
  });
3514
3764
  resourceIds.push(rid);
3765
+ } catch (err) {
3766
+ const wrapped = err instanceof Error ? err : new Error(String(err));
3767
+ const cause = wrapped.cause;
3768
+ const root = cause instanceof Error ? cause : cause !== void 0 ? new Error(String(cause)) : wrapped;
3769
+ failures.push({ file, error: root });
3515
3770
  } finally {
3516
3771
  placeholder.remove();
3517
3772
  }
3518
3773
  })
3519
3774
  );
3775
+ return { failures };
3776
+ }
3777
+ function buildBatchErrorMessage(filterError, failures, state) {
3778
+ if (failures.length === 0) return filterError;
3779
+ const uploadMsg = failures.map(
3780
+ (f) => t("uploadFailed", state, {
3781
+ name: f.file.name,
3782
+ error: f.error.message
3783
+ })
3784
+ ).join(" \u2022 ");
3785
+ return filterError ? `${filterError} \u2022 ${uploadMsg}` : uploadMsg;
3520
3786
  }
3521
3787
  function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallback, constraints, pathKey, instance) {
3522
3788
  setupDragAndDrop(filesContainer, async (files) => {
@@ -3532,7 +3798,13 @@ function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallbac
3532
3798
  clearFileError(filesContainer);
3533
3799
  }
3534
3800
  const list = filesContainer.querySelector(".files-list") ?? filesContainer;
3535
- await uploadBatch(accepted, resourceIds, list, state);
3801
+ const { failures } = await uploadBatch(accepted, resourceIds, list, state);
3802
+ const combined = buildBatchErrorMessage(errorMessage, failures, state);
3803
+ if (combined) {
3804
+ showFileError(filesContainer, combined);
3805
+ } else {
3806
+ clearFileError(filesContainer);
3807
+ }
3536
3808
  updateCallback();
3537
3809
  if (instance && pathKey && !state.config.readonly) {
3538
3810
  instance.triggerOnChange(pathKey, resourceIds);
@@ -3542,7 +3814,7 @@ function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallbac
3542
3814
  function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback, constraints, pathKey, instance) {
3543
3815
  filesPicker.onchange = async () => {
3544
3816
  if (!filesPicker.files) return;
3545
- const wrapperEl = filesPicker.closest(".space-y-2") || filesPicker.parentElement;
3817
+ const wrapperEl = filesPicker.closest("[data-files-wrapper]") || filesPicker.parentElement;
3546
3818
  const { accepted, errorMessage } = filterAndSlice(
3547
3819
  Array.from(filesPicker.files),
3548
3820
  resourceIds.length,
@@ -3555,7 +3827,20 @@ function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback
3555
3827
  clearFileError(wrapperEl);
3556
3828
  }
3557
3829
  const listEl = wrapperEl?.querySelector(".files-list");
3558
- await uploadBatch(accepted, resourceIds, listEl ?? null, state);
3830
+ const { failures } = await uploadBatch(
3831
+ accepted,
3832
+ resourceIds,
3833
+ listEl ?? null,
3834
+ state
3835
+ );
3836
+ if (wrapperEl) {
3837
+ const combined = buildBatchErrorMessage(errorMessage, failures, state);
3838
+ if (combined) {
3839
+ showFileError(wrapperEl, combined);
3840
+ } else {
3841
+ clearFileError(wrapperEl);
3842
+ }
3843
+ }
3559
3844
  updateCallback();
3560
3845
  filesPicker.value = "";
3561
3846
  if (instance && pathKey && !state.config.readonly) {
@@ -3587,10 +3872,17 @@ function validatePickedResource(resource, allowedExtensions, allowedMimes, maxSi
3587
3872
  }
3588
3873
  if (!isMimeAllowed(resource.type, allowedMimes)) {
3589
3874
  const mimes = allowedMimes.join(", ");
3590
- return t("invalidFileMime", state, { name: resource.name, type: resource.type, mimes });
3875
+ return t("invalidFileMime", state, {
3876
+ name: resource.name,
3877
+ type: resource.type,
3878
+ mimes
3879
+ });
3591
3880
  }
3592
3881
  if (!isSizeWithinLimit(resource.size, maxSizeMB)) {
3593
- return t("fileTooLarge", state, { name: resource.name, maxSize: maxSizeMB });
3882
+ return t("fileTooLarge", state, {
3883
+ name: resource.name,
3884
+ maxSize: maxSizeMB
3885
+ });
3594
3886
  }
3595
3887
  return null;
3596
3888
  }
@@ -3649,7 +3941,13 @@ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resour
3649
3941
  return true;
3650
3942
  });
3651
3943
  const validItems = deduped.filter((r) => {
3652
- const err = validatePickedResource(r, allowedExtensions, allowedMimes, maxSizeMB, state);
3944
+ const err = validatePickedResource(
3945
+ r,
3946
+ allowedExtensions,
3947
+ allowedMimes,
3948
+ maxSizeMB,
3949
+ state
3950
+ );
3653
3951
  return err === null;
3654
3952
  });
3655
3953
  const freshRemaining = maxCount === Infinity ? validItems.length : Math.max(0, maxCount - resourceIds.length);
@@ -3693,20 +3991,32 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
3693
3991
  }
3694
3992
  if (picked.length === 0) return;
3695
3993
  const first = picked[0];
3696
- const validationError = validatePickedResource(first, allowedExtensions, allowedMimes, maxSizeMB, state);
3994
+ const validationError = validatePickedResource(
3995
+ first,
3996
+ allowedExtensions,
3997
+ allowedMimes,
3998
+ maxSizeMB,
3999
+ state
4000
+ );
3697
4001
  if (validationError !== null) {
3698
4002
  showFileError(container, validationError);
3699
4003
  return;
3700
4004
  }
3701
4005
  clearFileError(container);
3702
4006
  registerPickedResource(first, state);
3703
- let hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
4007
+ let hiddenInput = fileWrapper.querySelector(
4008
+ 'input[type="hidden"]'
4009
+ );
3704
4010
  if (!hiddenInput) {
3705
4011
  hiddenInput = document.createElement("input");
3706
4012
  hiddenInput.type = "hidden";
3707
4013
  hiddenInput.name = pathKey;
3708
4014
  fileWrapper.appendChild(hiddenInput);
3709
4015
  }
4016
+ const previousRid = hiddenInput.value || null;
4017
+ if (previousRid && previousRid !== first.resourceId) {
4018
+ releaseLocalFileUrl(state.resourceIndex.get(previousRid)?.file);
4019
+ }
3710
4020
  hiddenInput.value = first.resourceId;
3711
4021
  await renderCallback(first.resourceId);
3712
4022
  if (!state.config.readonly) {
@@ -3715,7 +4025,9 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
3715
4025
  }
3716
4026
 
3717
4027
  // src/components/file/render-edit.ts
3718
- function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
4028
+ 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>`;
4029
+ 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>`;
4030
+ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps, extras) {
3719
4031
  seedInferredResource(initial, state.resourceIndex);
3720
4032
  const meta = state.resourceIndex.get(initial);
3721
4033
  const isVideo = meta?.type?.startsWith("video/");
@@ -3726,7 +4038,7 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
3726
4038
  deps
3727
4039
  }).catch(console.error);
3728
4040
  } else {
3729
- renderSingleFileEditTile(fileContainer, initial, state, deps).catch(console.error);
4041
+ renderSingleFileFilled(fileContainer, initial, state, deps, extras);
3730
4042
  }
3731
4043
  const hiddenInput = document.createElement("input");
3732
4044
  hiddenInput.type = "hidden";
@@ -3734,161 +4046,426 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
3734
4046
  hiddenInput.value = initial;
3735
4047
  fileWrapper.appendChild(hiddenInput);
3736
4048
  }
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;
4049
+ function buildWideTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragOver = false, constraintHint = "") {
4050
+ ensureFileStyles();
4051
+ const outer = document.createElement("div");
4052
+ outer.className = `fb-wide-tile${hasLibrary ? " fb-file-card-row" : ""}${isDragOver ? " fb-drag-over" : ""}`;
4053
+ const uploadBtn = document.createElement("button");
4054
+ uploadBtn.type = "button";
4055
+ uploadBtn.className = "fb-wide-tile-upload fb-file-dropzone";
4056
+ const cloudIcon = document.createElement("span");
4057
+ cloudIcon.style.cssText = "width:36px;height:36px;display:block;flex-shrink:0;";
4058
+ cloudIcon.innerHTML = ICON_CLOUD;
4059
+ uploadBtn.appendChild(cloudIcon);
4060
+ const primaryText = document.createElement("div");
4061
+ primaryText.className = "fb-wide-tile-label";
4062
+ primaryText.style.cssText = "font-size:14px;font-weight:600;";
4063
+ primaryText.textContent = isDragOver ? t("dropToUpload", state) : t("clickDragText", state);
4064
+ uploadBtn.appendChild(primaryText);
4065
+ if (constraintHint) {
4066
+ const hintEl = document.createElement("div");
4067
+ hintEl.style.cssText = "font-size:11px;opacity:0.65;margin-top:2px;";
4068
+ hintEl.textContent = constraintHint;
4069
+ uploadBtn.appendChild(hintEl);
4070
+ }
4071
+ uploadBtn.onclick = (e) => {
4072
+ e.stopPropagation();
4073
+ onUploadClick();
4074
+ };
4075
+ outer.appendChild(uploadBtn);
4076
+ if (hasLibrary && onLibraryClick) {
4077
+ const divider = document.createElement("div");
4078
+ divider.className = "fb-wide-tile-divider";
4079
+ outer.appendChild(divider);
4080
+ const libBtn = document.createElement("button");
4081
+ libBtn.type = "button";
4082
+ libBtn.className = "fb-wide-tile-library fb-file-library-card";
4083
+ const libIcon = document.createElement("span");
4084
+ libIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
4085
+ libIcon.innerHTML = ICON_LIBRARY2;
4086
+ libBtn.appendChild(libIcon);
4087
+ const libLabel = document.createElement("div");
4088
+ libLabel.style.cssText = "font-size:13px;font-weight:600;text-align:center;";
4089
+ libLabel.textContent = t("fromLibrary", state);
4090
+ libBtn.appendChild(libLabel);
4091
+ const libHint = document.createElement("div");
4092
+ libHint.style.cssText = "font-size:11px;opacity:0.75;text-align:center;";
4093
+ libHint.textContent = t("libraryHint", state);
4094
+ libBtn.appendChild(libHint);
4095
+ libBtn.onclick = (e) => {
4096
+ e.stopPropagation();
4097
+ onLibraryClick();
4098
+ };
4099
+ outer.appendChild(libBtn);
4100
+ }
4101
+ attachDragOverFeedback(outer, {
4102
+ onEnter: () => {
4103
+ const primaryText2 = outer.querySelector(".fb-wide-tile-label");
4104
+ if (primaryText2) primaryText2.textContent = t("dropToUpload", state);
4105
+ },
4106
+ onLeave: () => {
4107
+ const primaryText2 = outer.querySelector(".fb-wide-tile-label");
4108
+ if (primaryText2) primaryText2.textContent = t("clickDragText", state);
4109
+ },
4110
+ activeClass: "fb-drag-over"
4111
+ });
4112
+ return outer;
3750
4113
  }
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
- `;
4114
+ function attachDragOverFeedback(el, hooks) {
4115
+ let depth = 0;
4116
+ el.addEventListener("dragover", (e) => {
4117
+ e.preventDefault();
4118
+ });
4119
+ el.addEventListener("dragenter", (e) => {
4120
+ e.preventDefault();
4121
+ depth++;
4122
+ if (depth === 1) {
4123
+ el.classList.add(hooks.activeClass);
4124
+ hooks.onEnter();
4125
+ }
4126
+ });
4127
+ el.addEventListener("dragleave", (e) => {
4128
+ e.preventDefault();
4129
+ depth = Math.max(0, depth - 1);
4130
+ if (depth === 0) {
4131
+ el.classList.remove(hooks.activeClass);
4132
+ hooks.onLeave();
4133
+ }
4134
+ });
4135
+ el.addEventListener("drop", () => {
4136
+ depth = 0;
4137
+ el.classList.remove(hooks.activeClass);
4138
+ hooks.onLeave();
4139
+ });
4140
+ }
4141
+ function renderSingleFileFilled(fileContainer, resourceId, state, deps, extras) {
4142
+ const meta = state.resourceIndex.get(resourceId);
4143
+ const isVideo = meta?.type?.startsWith("video/");
4144
+ if (isVideo) {
4145
+ renderFilePreview(fileContainer, resourceId, state, {
4146
+ fileName: meta?.name ?? "",
4147
+ isReadonly: false,
4148
+ deps
4149
+ }).catch(console.error);
4150
+ return;
4151
+ }
4152
+ ensureFileStyles();
4153
+ const outer = document.createElement("div");
4154
+ outer.className = "fb-multi-outer fb-multi-has-files";
4155
+ const grid = document.createElement("div");
4156
+ grid.className = "fb-multi-grid fb-tiles-wrap";
4157
+ outer.appendChild(grid);
4158
+ const tile = buildPreviewTile(
4159
+ resourceId,
4160
+ state,
4161
+ Boolean(deps.onRemove),
4162
+ deps.onRemove ? () => deps.onRemove?.() : null,
4163
+ extras
4164
+ );
4165
+ grid.appendChild(tile);
4166
+ fileContainer.className = "file-preview-container";
4167
+ fileContainer.removeAttribute("style");
4168
+ while (fileContainer.firstChild)
4169
+ fileContainer.removeChild(fileContainer.firstChild);
4170
+ fileContainer.appendChild(outer);
4171
+ }
4172
+ function buildMultiAddTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragOver = false) {
4173
+ const tile = document.createElement("div");
4174
+ tile.className = `fb-multi-add-tile fb-multi-add-tile-js${isDragOver ? " fb-drag-over-tile" : ""}`;
4175
+ const uploadBtn = document.createElement("button");
4176
+ uploadBtn.type = "button";
4177
+ uploadBtn.className = "fb-multi-add-upload fb-tile-add fb-file-dropzone";
4178
+ const cloudIcon = document.createElement("span");
4179
+ cloudIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
4180
+ cloudIcon.innerHTML = ICON_CLOUD;
4181
+ uploadBtn.appendChild(cloudIcon);
4182
+ const uploadLabel = document.createElement("span");
4183
+ uploadLabel.className = "fb-multi-add-label";
4184
+ uploadLabel.style.cssText = "font-size:11px;font-weight:600;";
4185
+ uploadLabel.textContent = isDragOver ? t("dropToUpload", state) : t("clickDragTextMultiple", state);
4186
+ uploadBtn.appendChild(uploadLabel);
4187
+ uploadBtn.onclick = (e) => {
4188
+ e.stopPropagation();
4189
+ onUploadClick();
4190
+ };
4191
+ tile.appendChild(uploadBtn);
4192
+ if (hasLibrary && onLibraryClick) {
4193
+ const divider = document.createElement("div");
4194
+ divider.className = "fb-multi-add-divider";
4195
+ tile.appendChild(divider);
4196
+ const libBtn = document.createElement("button");
4197
+ libBtn.type = "button";
4198
+ libBtn.className = "fb-multi-add-library fb-tile-add-library fb-file-library-card";
4199
+ libBtn.setAttribute("aria-label", t("fromLibrary", state));
4200
+ const libIcon = document.createElement("span");
4201
+ libIcon.style.cssText = "width:14px;height:14px;display:block;flex-shrink:0;";
4202
+ libIcon.innerHTML = ICON_LIBRARY2;
4203
+ libBtn.appendChild(libIcon);
4204
+ libBtn.appendChild(document.createTextNode(t("fromLibrary", state)));
4205
+ libBtn.onclick = (e) => {
4206
+ e.stopPropagation();
4207
+ onLibraryClick();
4208
+ };
4209
+ tile.appendChild(libBtn);
4210
+ }
4211
+ return tile;
4212
+ }
4213
+ function buildPreviewTile(rid, state, canRemove, onRemove, extras) {
4214
+ ensureFileStyles();
4215
+ const meta = state.resourceIndex.get(rid);
4216
+ const tile = document.createElement("div");
4217
+ tile.className = "fb-preview-tile fb-checker fb-tile-resource resource-pill";
4218
+ tile.dataset.resourceId = rid;
4219
+ const actionsEl = createTileActions({
4220
+ canRemove: canRemove && onRemove !== null,
4221
+ removeHandler: onRemove,
4222
+ state,
4223
+ resourceId: rid,
4224
+ fileName: meta?.name ?? "",
4225
+ meta,
4226
+ replaceHandler: extras?.replaceHandler ?? null,
4227
+ libraryHandler: extras?.libraryHandler ?? null
4228
+ });
4229
+ fillTileContent(tile, rid, meta, state, actionsEl).catch((err) => {
4230
+ console.error("Failed to render tile:", err);
4231
+ });
4232
+ return tile;
4233
+ }
4234
+ function buildPlaceholderTile(isDragOver = false) {
4235
+ const div = document.createElement("div");
4236
+ div.className = `fb-multi-placeholder fb-checker${isDragOver ? " fb-drag-over" : ""}`;
4237
+ return div;
4238
+ }
4239
+ function buildMetaLine(state, element, ridCount, maxCount, canClearAll, onClearAll) {
4240
+ const line = document.createElement("div");
4241
+ line.className = "fb-meta-line";
4242
+ const metaText = document.createElement("div");
4243
+ metaText.className = "fb-meta-text";
4244
+ if (element.maxSize && element.maxSize !== Infinity) {
4245
+ const sizeSpan = document.createElement("span");
4246
+ sizeSpan.textContent = t("hintMaxSize", state, { size: element.maxSize });
4247
+ metaText.appendChild(sizeSpan);
4248
+ metaText.appendChild(buildMetaDot());
4249
+ }
4250
+ const exts = getAllowedExtensions(
4251
+ element.accept
4252
+ );
4253
+ if (exts.length > 0) {
4254
+ const fmtSpan = document.createElement("span");
4255
+ fmtSpan.className = "fb-meta-mono";
4256
+ fmtSpan.textContent = exts.map((e) => e.toUpperCase()).join(", ");
4257
+ metaText.appendChild(fmtSpan);
4258
+ metaText.appendChild(buildMetaDot());
4259
+ }
4260
+ const countSpan = document.createElement("span");
4261
+ if (maxCount < Infinity) {
4262
+ countSpan.textContent = t("fileCountWithMax", state, {
4263
+ count: ridCount,
4264
+ max: maxCount
4265
+ });
3761
4266
  } 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));
4267
+ const countKey = ridCount === 1 ? "fileCountSingle" : "fileCountPlural";
4268
+ countSpan.textContent = t(countKey, state, { count: ridCount });
4269
+ }
4270
+ metaText.appendChild(countSpan);
4271
+ line.appendChild(metaText);
4272
+ if (canClearAll && ridCount > 1) {
4273
+ const clearBtn = document.createElement("button");
4274
+ clearBtn.type = "button";
4275
+ clearBtn.className = "fb-clear-all-btn";
4276
+ clearBtn.textContent = t("clearAll", state);
4277
+ clearBtn.onclick = (e) => {
4278
+ e.stopPropagation();
4279
+ if (window.confirm(t("clearAll", state) + "?")) {
4280
+ onClearAll();
4281
+ }
4282
+ };
4283
+ line.appendChild(clearBtn);
3765
4284
  }
3766
- btn.addEventListener("click", onClick);
3767
- return btn;
4285
+ return line;
4286
+ }
4287
+ function buildMetaDot() {
4288
+ const dot = document.createElement("span");
4289
+ dot.className = "fb-meta-dot";
4290
+ return dot;
3768
4291
  }
4292
+ var gridResizeObservers = /* @__PURE__ */ new WeakMap();
3769
4293
  function renderResourcePills(opts) {
3770
4294
  const {
3771
4295
  container,
3772
4296
  rids,
3773
4297
  state,
3774
4298
  onRemove,
3775
- hint,
3776
- countInfo,
3777
4299
  maxCount,
3778
4300
  isReadonly = false,
3779
- onLibraryPick
4301
+ onLibraryPick,
4302
+ element,
4303
+ onClearAll,
4304
+ openPicker: openPickerProp
3780
4305
  } = opts;
3781
4306
  ensureFileStyles();
3782
4307
  const wrapper = container.closest("[data-files-wrapper]");
3783
4308
  if (wrapper) {
3784
4309
  wrapper.dataset.resourceIds = JSON.stringify(rids ?? []);
3785
4310
  }
4311
+ const previousObserver = gridResizeObservers.get(container);
4312
+ if (previousObserver) {
4313
+ previousObserver.disconnect();
4314
+ gridResizeObservers.delete(container);
4315
+ }
3786
4316
  while (container.firstChild) container.removeChild(container.firstChild);
3787
4317
  const ridList = rids ?? [];
3788
- const atMax = maxCount !== void 0 && ridList.length >= maxCount;
4318
+ const effectiveMax = maxCount ?? Infinity;
4319
+ const atMax = effectiveMax !== Infinity && ridList.length >= effectiveMax;
3789
4320
  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) {
4321
+ const openPicker = openPickerProp ?? (() => {
4322
+ const pickerEl = container.closest("[data-files-wrapper]")?.querySelector('input[type="file"]');
4323
+ if (pickerEl) pickerEl.click();
4324
+ });
4325
+ if (isReadonly) {
4326
+ if (ridList.length === 0) {
3802
4327
  const emptyEl = document.createElement("div");
3803
4328
  emptyEl.className = "fb-tile-empty-text";
3804
4329
  emptyEl.textContent = t("noFilesSelected", state);
3805
4330
  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
4331
  } else {
3820
- const dropzone = buildEmptyDropzone(
3821
- state,
3822
- t("clickDragTextMultiple", state),
3823
- buildSubHint(),
3824
- openPicker
3825
- );
3826
- container.appendChild(dropzone);
4332
+ const grid2 = document.createElement("div");
4333
+ grid2.className = "fb-multi-readonly-grid";
4334
+ container.appendChild(grid2);
4335
+ for (const rid of ridList) {
4336
+ const meta = state.resourceIndex.get(rid);
4337
+ const tile = document.createElement("div");
4338
+ tile.className = "fb-readonly-tile fb-checker fb-tile fb-tile-resource";
4339
+ tile.dataset.resourceId = rid;
4340
+ const actionsEl = createTileActions({
4341
+ canRemove: false,
4342
+ removeHandler: null,
4343
+ state,
4344
+ resourceId: rid,
4345
+ fileName: meta?.name ?? "",
4346
+ meta
4347
+ });
4348
+ fillTileContent(tile, rid, meta, state, actionsEl).catch(console.error);
4349
+ tile.onclick = async () => {
4350
+ let url = null;
4351
+ if (state.config.getDownloadUrl) {
4352
+ url = state.config.getDownloadUrl(rid);
4353
+ } else if (state.config.getThumbnail) {
4354
+ url = await state.config.getThumbnail(rid);
4355
+ } else if (meta?.file instanceof File) {
4356
+ url = URL.createObjectURL(meta.file);
4357
+ }
4358
+ if (url) {
4359
+ window.open(url, "_blank");
4360
+ } else if (state.config.downloadFile) {
4361
+ state.config.downloadFile(rid, meta?.name ?? "");
4362
+ }
4363
+ };
4364
+ grid2.appendChild(tile);
4365
+ }
3827
4366
  }
3828
4367
  return;
3829
4368
  }
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,
4369
+ const outerDiv = document.createElement("div");
4370
+ outerDiv.className = `fb-multi-outer${ridList.length > 0 ? " fb-multi-has-files" : ""}`;
4371
+ const grid = document.createElement("div");
4372
+ grid.className = "fb-multi-grid fb-tiles-wrap";
4373
+ outerDiv.appendChild(grid);
4374
+ container.appendChild(outerDiv);
4375
+ for (let i = 0; i < ridList.length; i++) {
4376
+ const rid = ridList[i];
4377
+ const tile = buildPreviewTile(
4378
+ rid,
3841
4379
  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);
4380
+ onRemove !== null,
4381
+ onRemove ? () => onRemove(rid) : null
4382
+ );
4383
+ grid.appendChild(tile);
3868
4384
  }
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);
4385
+ if (!atMax) {
4386
+ const addTile = buildMultiAddTile(
4387
+ state,
4388
+ hasLibrary,
4389
+ openPicker,
4390
+ onLibraryPick ?? null
4391
+ );
4392
+ grid.appendChild(addTile);
4393
+ }
4394
+ const occupied = ridList.length + (atMax ? 0 : 1);
4395
+ const adjustPlaceholders = () => {
4396
+ const tpl = getComputedStyle(grid).gridTemplateColumns;
4397
+ const cols = tpl ? tpl.split(" ").filter(Boolean).length : 0;
4398
+ if (!cols) return;
4399
+ const remainder = occupied % cols;
4400
+ const rowFill = remainder === 0 ? 0 : cols - remainder;
4401
+ const capacityRemaining = effectiveMax === Infinity ? rowFill : Math.max(0, effectiveMax - occupied);
4402
+ const needed = Math.min(rowFill, capacityRemaining);
4403
+ const existing = grid.querySelectorAll(".fb-multi-placeholder");
4404
+ if (existing.length > needed) {
4405
+ for (let i = existing.length - 1; i >= needed; i--) existing[i].remove();
4406
+ } else if (existing.length < needed) {
4407
+ for (let i = existing.length; i < needed; i++) {
4408
+ grid.appendChild(buildPlaceholderTile());
4409
+ }
4410
+ }
4411
+ };
4412
+ if (effectiveMax === Infinity || effectiveMax > occupied) {
4413
+ grid.appendChild(buildPlaceholderTile());
4414
+ }
4415
+ requestAnimationFrame(adjustPlaceholders);
4416
+ if (typeof ResizeObserver !== "undefined") {
4417
+ const ro = new ResizeObserver(() => adjustPlaceholders());
4418
+ ro.observe(grid);
4419
+ gridResizeObservers.set(container, ro);
4420
+ }
4421
+ attachDragOverFeedback(outerDiv, {
4422
+ activeClass: "fb-drag-over",
4423
+ onEnter: () => {
4424
+ grid.querySelectorAll(".fb-multi-placeholder").forEach((p) => {
4425
+ p.classList.add("fb-drag-over");
4426
+ });
4427
+ const addTile = grid.querySelector(".fb-multi-add-tile-js");
4428
+ if (addTile) {
4429
+ addTile.classList.add("fb-drag-over-tile");
4430
+ const label = addTile.querySelector(".fb-multi-add-label");
4431
+ if (label) label.textContent = t("dropToUpload", state);
4432
+ }
4433
+ },
4434
+ onLeave: () => {
4435
+ grid.querySelectorAll(".fb-multi-placeholder").forEach((p) => {
4436
+ p.classList.remove("fb-drag-over");
4437
+ });
4438
+ const addTile = grid.querySelector(".fb-multi-add-tile-js");
4439
+ if (addTile) {
4440
+ addTile.classList.remove("fb-drag-over-tile");
4441
+ const label = addTile.querySelector(".fb-multi-add-label");
4442
+ if (label) label.textContent = t("clickDragTextMultiple", state);
4443
+ }
4444
+ }
4445
+ });
4446
+ if (element) {
4447
+ const metaLine = buildMetaLine(
4448
+ state,
4449
+ element,
4450
+ ridList.length,
4451
+ effectiveMax,
4452
+ Boolean(onClearAll),
4453
+ onClearAll ?? (() => {
4454
+ })
4455
+ );
4456
+ container.appendChild(metaLine);
3876
4457
  }
3877
4458
  }
3878
4459
  function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3879
4460
  const state = ctx.state;
3880
4461
  const fileWrapper = document.createElement("div");
3881
4462
  fileWrapper.className = "space-y-2";
4463
+ fileWrapper.dataset.filesWrapper = pathKey;
3882
4464
  const picker = document.createElement("input");
3883
4465
  picker.type = "file";
3884
4466
  picker.name = pathKey;
3885
4467
  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
- }
4468
+ picker.accept = buildAcceptAttribute(element.accept);
3892
4469
  const fileContainer = document.createElement("div");
3893
4470
  fileContainer.className = "file-preview-container";
3894
4471
  const initial = ctx.prefill[element.key];
@@ -3917,16 +4494,10 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3917
4494
  setupDrop(container) {
3918
4495
  setupDragAndDrop(container, handlers.dragHandler);
3919
4496
  },
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
4497
  onRemove() {
3929
- const hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
4498
+ const hiddenInput = fileWrapper.querySelector(
4499
+ 'input[type="hidden"]'
4500
+ );
3930
4501
  const currentRid = hiddenInput?.value;
3931
4502
  if (currentRid) {
3932
4503
  releaseLocalFileUrl(state.resourceIndex.get(currentRid)?.file);
@@ -3935,34 +4506,13 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3935
4506
  renderEmptySingleState();
3936
4507
  }
3937
4508
  };
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, () => {
4509
+ const buildSingleExtras = () => {
4510
+ const hasLibrary = Boolean(
4511
+ state.config.pickExistingFiles && !element.disableLibrary
4512
+ );
4513
+ return {
4514
+ replaceHandler: state.config.uploadFile ? () => picker.click() : null,
4515
+ libraryHandler: hasLibrary ? () => {
3966
4516
  handleLibraryPickSingle(
3967
4517
  state,
3968
4518
  element,
@@ -3971,20 +4521,54 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3971
4521
  pathKey,
3972
4522
  pathKey,
3973
4523
  async (rid) => {
3974
- await renderSingleFileEditTile(fileContainer, rid, state, buildDeps());
4524
+ renderSingleFileFilled(
4525
+ fileContainer,
4526
+ rid,
4527
+ state,
4528
+ buildDeps(),
4529
+ buildSingleExtras()
4530
+ );
3975
4531
  },
3976
4532
  ctx.instance
3977
4533
  ).catch((err) => {
3978
4534
  console.error("Library pick failed:", err);
3979
4535
  });
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();
4536
+ } : null
4537
+ };
4538
+ };
4539
+ const buildDeps = () => ({
4540
+ picker,
4541
+ fileUploadHandler: handlers.fileUploadHandler,
4542
+ dragHandler: handlers.dragHandler,
4543
+ setupDrop: handlers.setupDrop,
4544
+ onRemove: handlers.onRemove,
4545
+ onAfterUpload: (container, rid) => {
4546
+ renderSingleFileFilled(
4547
+ container,
4548
+ rid,
4549
+ state,
4550
+ buildDeps(),
4551
+ buildSingleExtras()
4552
+ );
3987
4553
  }
4554
+ });
4555
+ const renderEmptySingleState = () => {
4556
+ ensureFileStyles();
4557
+ fileContainer.className = "file-preview-container";
4558
+ fileContainer.removeAttribute("style");
4559
+ while (fileContainer.firstChild)
4560
+ fileContainer.removeChild(fileContainer.firstChild);
4561
+ const onLibraryClick = buildSingleExtras().libraryHandler;
4562
+ const wideTile = buildWideTile(
4563
+ state,
4564
+ onLibraryClick !== null,
4565
+ handlers.fileUploadHandler,
4566
+ onLibraryClick,
4567
+ false,
4568
+ makeFieldHint(element, state)
4569
+ );
4570
+ fileContainer.appendChild(wideTile);
4571
+ setupDragAndDrop(fileContainer, handlers.dragHandler);
3988
4572
  };
3989
4573
  if (initial) {
3990
4574
  handleInitialFileData(
@@ -3993,11 +4577,11 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3993
4577
  pathKey,
3994
4578
  fileWrapper,
3995
4579
  state,
3996
- buildDeps()
4580
+ buildDeps(),
4581
+ buildSingleExtras()
3997
4582
  );
3998
4583
  const prefillMeta = state.resourceIndex.get(initial);
3999
4584
  if (prefillMeta?.type?.startsWith("video/")) {
4000
- fileContainer.onclick = handlers.fileUploadHandler;
4001
4585
  setupDragAndDrop(fileContainer, handlers.dragHandler);
4002
4586
  }
4003
4587
  } else {
@@ -4005,113 +4589,23 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
4005
4589
  }
4006
4590
  picker.onchange = () => {
4007
4591
  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
- });
4592
+ handlers.dragHandler(picker.files);
4019
4593
  }
4020
4594
  };
4021
4595
  fileWrapper.appendChild(fileContainer);
4022
4596
  fileWrapper.appendChild(picker);
4023
4597
  wrapper.appendChild(fileWrapper);
4024
4598
  }
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
- );
4599
+ function buildAcceptAttribute(accept) {
4600
+ if (!accept) return "";
4601
+ if (typeof accept === "string") return accept;
4602
+ return [
4603
+ ...accept.extensions?.map((ext) => `.${ext}`) ?? [],
4604
+ ...accept.mime ?? []
4605
+ ].join(",");
4110
4606
  }
4111
- function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4607
+ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
4112
4608
  const state = ctx.state;
4113
- const minFiles = element.minCount ?? 0;
4114
- const maxFiles = element.maxCount ?? Infinity;
4115
4609
  const filesWrapper = document.createElement("div");
4116
4610
  filesWrapper.className = "space-y-2";
4117
4611
  filesWrapper.dataset.filesWrapper = pathKey;
@@ -4120,15 +4614,9 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4120
4614
  filesPicker.name = pathKey;
4121
4615
  filesPicker.multiple = true;
4122
4616
  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
- }
4617
+ filesPicker.accept = buildAcceptAttribute(element.accept);
4129
4618
  const filesContainer = document.createElement("div");
4130
4619
  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
4620
  const list = document.createElement("div");
4133
4621
  list.className = "files-list";
4134
4622
  filesWrapper.appendChild(filesPicker);
@@ -4137,19 +4625,18 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4137
4625
  const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
4138
4626
  addPrefillFilesToIndex(initialFiles, state.resourceIndex);
4139
4627
  filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
4140
- const multipleFilesHint = makeFieldHint(element, state);
4141
- const multipleConstraints = {
4628
+ const constraints = {
4142
4629
  maxCount: maxFiles,
4143
4630
  allowedExtensions: getAllowedExtensions(element.accept),
4144
4631
  allowedMimes: getAllowedMimes(element.accept),
4145
- maxSize: element.maxSize ?? Infinity
4632
+ // Prefer schema's `maxSize`; fall back to legacy `maxSizeMB` for
4633
+ // backward compatibility (matches addFileSizeHint in validation.ts).
4634
+ maxSize: element.maxSize ?? element.maxSizeMB ?? Infinity
4146
4635
  };
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;
4636
+ const openPicker = () => {
4637
+ filesPicker.click();
4151
4638
  };
4152
- const onLibraryPickMultiple = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4639
+ const onLibraryPick = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4153
4640
  handleLibraryPickMulti(
4154
4641
  state,
4155
4642
  element,
@@ -4163,30 +4650,35 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4163
4650
  console.error("Library pick failed:", err);
4164
4651
  });
4165
4652
  } : null;
4166
- const updateFilesDisplay = () => {
4653
+ function updateFilesDisplay() {
4167
4654
  const currentlyReadonly = isElementReadonly(element, state);
4168
4655
  renderResourcePills({
4169
4656
  container: list,
4170
4657
  rids: initialFiles,
4171
4658
  state,
4172
- onRemove: currentlyReadonly ? null : (index) => {
4173
- releaseLocalFileUrl(state.resourceIndex.get(index)?.file);
4174
- initialFiles.splice(initialFiles.indexOf(index), 1);
4659
+ onRemove: currentlyReadonly ? null : (ridToRemove) => {
4660
+ releaseLocalFileUrl(state.resourceIndex.get(ridToRemove)?.file);
4661
+ const index = initialFiles.indexOf(ridToRemove);
4662
+ if (index > -1) initialFiles.splice(index, 1);
4175
4663
  updateFilesDisplay();
4176
4664
  },
4177
- hint: multipleFilesHint,
4178
- countInfo: buildCountInfo(),
4179
4665
  maxCount: maxFiles < Infinity ? maxFiles : void 0,
4180
4666
  isReadonly: currentlyReadonly,
4181
- onLibraryPick: currentlyReadonly ? null : onLibraryPickMultiple
4667
+ onLibraryPick: currentlyReadonly ? null : onLibraryPick,
4668
+ element,
4669
+ onClearAll: currentlyReadonly ? void 0 : () => {
4670
+ initialFiles.splice(0);
4671
+ updateFilesDisplay();
4672
+ },
4673
+ openPicker
4182
4674
  });
4183
- };
4675
+ }
4184
4676
  setupFilesDropHandler(
4185
4677
  filesContainer,
4186
4678
  initialFiles,
4187
4679
  state,
4188
4680
  updateFilesDisplay,
4189
- multipleConstraints,
4681
+ constraints,
4190
4682
  pathKey,
4191
4683
  ctx.instance
4192
4684
  );
@@ -4195,13 +4687,25 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4195
4687
  initialFiles,
4196
4688
  state,
4197
4689
  updateFilesDisplay,
4198
- multipleConstraints,
4690
+ constraints,
4199
4691
  pathKey,
4200
4692
  ctx.instance
4201
4693
  );
4202
4694
  updateFilesDisplay();
4203
4695
  wrapper.appendChild(filesWrapper);
4204
4696
  }
4697
+ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
4698
+ setupMultiFileEditMode(element, ctx, wrapper, pathKey, Infinity);
4699
+ }
4700
+ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4701
+ setupMultiFileEditMode(
4702
+ element,
4703
+ ctx,
4704
+ wrapper,
4705
+ pathKey,
4706
+ element.maxCount ?? Infinity
4707
+ );
4708
+ }
4205
4709
 
4206
4710
  // src/components/file/validate.ts
4207
4711
  function readMultiFileResourceIds(scopeRoot, fullKey) {
@@ -4324,33 +4828,36 @@ function renderFileElementReadonly(element, ctx, wrapper, pathKey) {
4324
4828
  hiddenInput.name = pathKey;
4325
4829
  hiddenInput.value = initial;
4326
4830
  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
- });
4831
+ renderFilePreviewReadonly(initial, state).then((tile) => {
4832
+ tile.classList.add(
4833
+ "fb-single-readonly-filled",
4834
+ "fb-readonly-tile",
4835
+ "fb-checker"
4836
+ );
4837
+ wrapper.appendChild(tile);
4838
+ }).catch(console.error);
4333
4839
  } else {
4334
4840
  wrapper.appendChild(buildEmptyReadonlyTile(state));
4335
4841
  }
4336
4842
  }
4337
4843
  function buildEmptyReadonlyTile(state) {
4844
+ ensureFileStyles();
4338
4845
  const emptyState = document.createElement("div");
4339
4846
  emptyState.style.cssText = `
4340
- width:${TILE_SIZE};
4341
- height:${TILE_SIZE};
4847
+ height: 220px;
4342
4848
  display:flex;
4343
4849
  align-items:center;
4344
4850
  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);
4851
+ background: repeating-linear-gradient(45deg, #fafafa 0 6px, #f3f4f6 6px 12px);
4852
+ border-radius:0.75rem;
4853
+ border:1px solid #e2e8f0;
4348
4854
  `;
4349
4855
  emptyState.innerHTML = `<div style="font-size:11px;text-align:center;color:var(--fb-text-secondary-color,#6b7280);">${escapeHtml(t("noFileSelected", state))}</div>`;
4350
4856
  return emptyState;
4351
4857
  }
4352
- function renderMultiFileReadonly(rids, state, wrapper, pathKey, marginTop) {
4858
+ function renderMultiFileReadonly(rids, state, wrapper, pathKey, _marginTop) {
4353
4859
  addPrefillFilesToIndex(rids, state.resourceIndex);
4860
+ ensureFileStyles();
4354
4861
  const filesWrapper = document.createElement("div");
4355
4862
  filesWrapper.dataset.filesWrapper = pathKey;
4356
4863
  filesWrapper.dataset.resourceIds = JSON.stringify(rids);
@@ -4362,22 +4869,32 @@ function renderMultiFileReadonly(rids, state, wrapper, pathKey, marginTop) {
4362
4869
  filesWrapper.appendChild(emptyEl);
4363
4870
  return;
4364
4871
  }
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);
4872
+ const grid = document.createElement("div");
4873
+ grid.className = "fb-multi-readonly-grid";
4874
+ filesWrapper.appendChild(grid);
4368
4875
  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;
4876
+ const ph = document.createElement("div");
4877
+ ph.className = "fb-readonly-tile fb-checker fb-tile";
4878
+ grid.appendChild(ph);
4879
+ return ph;
4373
4880
  });
4374
4881
  for (let i = 0; i < rids.length; i++) {
4375
4882
  const resourceId = rids[i];
4376
4883
  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);
4884
+ const meta = state.resourceIndex.get(resourceId);
4885
+ renderFilePreviewReadonly(resourceId, state, meta?.name).then((tile) => {
4886
+ tile.classList.add(
4887
+ "fb-readonly-tile",
4888
+ "fb-checker",
4889
+ "fb-tile-resource"
4890
+ );
4891
+ tile.dataset.resourceId = resourceId;
4892
+ placeholder.replaceWith(tile);
4893
+ }).catch(() => {
4894
+ const tile = document.createElement("div");
4895
+ tile.className = "fb-readonly-tile fb-checker fb-tile fb-tile-resource";
4896
+ tile.dataset.resourceId = resourceId;
4897
+ placeholder.replaceWith(tile);
4381
4898
  });
4382
4899
  }
4383
4900
  }
@@ -4389,7 +4906,7 @@ function renderFilesElementReadonly(element, ctx, wrapper, pathKey) {
4389
4906
  function renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey) {
4390
4907
  const rawPrefill = ctx.prefill[element.key];
4391
4908
  const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
4392
- renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey, "4px");
4909
+ renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey);
4393
4910
  }
4394
4911
 
4395
4912
  // src/components/file.ts
@@ -4720,51 +5237,25 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
4720
5237
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
4721
5238
  });
4722
5239
  }
4723
- let addRow = null;
4724
- let countDisplay = null;
5240
+ let addUpdate = null;
4725
5241
  if (!readonly) {
4726
- addRow = document.createElement("div");
4727
- addRow.className = "flex items-center gap-3 mt-2";
4728
- const addBtn = document.createElement("button");
4729
- addBtn.type = "button";
4730
- addBtn.className = "add-colour-btn px-3 py-1 rounded";
4731
- addBtn.style.cssText = `
4732
- color: var(--fb-primary-color);
4733
- border: var(--fb-border-width) solid var(--fb-primary-color);
4734
- background-color: transparent;
4735
- font-size: var(--fb-font-size);
4736
- transition: all var(--fb-transition-duration);
4737
- `;
4738
- addBtn.textContent = "+";
4739
- addBtn.addEventListener("mouseenter", () => {
4740
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
4741
- });
4742
- addBtn.addEventListener("mouseleave", () => {
4743
- addBtn.style.backgroundColor = "transparent";
4744
- });
4745
- addBtn.onclick = () => {
4746
- const defaultColour = element.default || "#000000";
4747
- values.push(defaultColour);
4748
- addColourItem(defaultColour);
4749
- updateAddButton();
4750
- updateRemoveButtons();
4751
- };
4752
- countDisplay = document.createElement("span");
4753
- countDisplay.className = "text-sm text-gray-500";
4754
- addRow.appendChild(addBtn);
4755
- addRow.appendChild(countDisplay);
4756
- wrapper.appendChild(addRow);
5242
+ const handle = createAddItemRow(
5243
+ "colour",
5244
+ () => {
5245
+ const defaultColour = element.default || "#000000";
5246
+ values.push(defaultColour);
5247
+ addColourItem(defaultColour);
5248
+ updateAddButton();
5249
+ updateRemoveButtons();
5250
+ },
5251
+ { label: element.addLabel }
5252
+ );
5253
+ addUpdate = handle.update;
5254
+ mountCounterInLabel(wrapper, handle.counter);
5255
+ wrapper.appendChild(handle.row);
4757
5256
  }
4758
5257
  function updateAddButton() {
4759
- if (!addRow || !countDisplay) return;
4760
- const addBtn = addRow.querySelector(".add-colour-btn");
4761
- if (addBtn) {
4762
- const disabled = values.length >= maxCount;
4763
- addBtn.disabled = disabled;
4764
- addBtn.style.opacity = disabled ? "0.5" : "1";
4765
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
4766
- }
4767
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
5258
+ if (addUpdate) addUpdate(values.length, maxCount);
4768
5259
  }
4769
5260
  values.forEach((value) => addColourItem(value));
4770
5261
  updateAddButton();
@@ -5200,50 +5691,24 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
5200
5691
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
5201
5692
  });
5202
5693
  }
5203
- let addRow = null;
5204
- let countDisplay = null;
5694
+ let addUpdate = null;
5205
5695
  if (!readonly) {
5206
- addRow = document.createElement("div");
5207
- addRow.className = "flex items-center gap-3 mt-2";
5208
- const addBtn = document.createElement("button");
5209
- addBtn.type = "button";
5210
- addBtn.className = "add-slider-btn px-3 py-1 rounded";
5211
- addBtn.style.cssText = `
5212
- color: var(--fb-primary-color);
5213
- border: var(--fb-border-width) solid var(--fb-primary-color);
5214
- background-color: transparent;
5215
- font-size: var(--fb-font-size);
5216
- transition: all var(--fb-transition-duration);
5217
- `;
5218
- addBtn.textContent = "+";
5219
- addBtn.addEventListener("mouseenter", () => {
5220
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
5221
- });
5222
- addBtn.addEventListener("mouseleave", () => {
5223
- addBtn.style.backgroundColor = "transparent";
5224
- });
5225
- addBtn.onclick = () => {
5226
- values.push(defaultValue);
5227
- addSliderItem(defaultValue);
5228
- updateAddButton();
5229
- updateRemoveButtons();
5230
- };
5231
- countDisplay = document.createElement("span");
5232
- countDisplay.className = "text-sm text-gray-500";
5233
- addRow.appendChild(addBtn);
5234
- addRow.appendChild(countDisplay);
5235
- wrapper.appendChild(addRow);
5696
+ const handle = createAddItemRow(
5697
+ "slider",
5698
+ () => {
5699
+ values.push(defaultValue);
5700
+ addSliderItem(defaultValue);
5701
+ updateAddButton();
5702
+ updateRemoveButtons();
5703
+ },
5704
+ { label: element.addLabel }
5705
+ );
5706
+ addUpdate = handle.update;
5707
+ mountCounterInLabel(wrapper, handle.counter);
5708
+ wrapper.appendChild(handle.row);
5236
5709
  }
5237
5710
  function updateAddButton() {
5238
- if (!addRow || !countDisplay) return;
5239
- const addBtn = addRow.querySelector(".add-slider-btn");
5240
- if (addBtn) {
5241
- const disabled = values.length >= maxCount;
5242
- addBtn.disabled = disabled;
5243
- addBtn.style.opacity = disabled ? "0.5" : "1";
5244
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
5245
- }
5246
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
5711
+ if (addUpdate) addUpdate(values.length, maxCount);
5247
5712
  }
5248
5713
  values.forEach((value) => addSliderItem(value));
5249
5714
  updateAddButton();
@@ -5527,7 +5992,7 @@ function createPrefillHints(element, pathKey) {
5527
5992
  return null;
5528
5993
  }
5529
5994
  const hintsContainer = document.createElement("div");
5530
- hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-4";
5995
+ hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-2";
5531
5996
  element.prefillHints.forEach((hint, index) => {
5532
5997
  const hintButton = document.createElement("button");
5533
5998
  hintButton.type = "button";
@@ -5542,14 +6007,14 @@ function createPrefillHints(element, pathKey) {
5542
6007
  }
5543
6008
  function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
5544
6009
  const containerWrap = document.createElement("div");
5545
- containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
6010
+ containerWrap.className = "border border-gray-200 rounded-lg p-2 bg-gray-50";
5546
6011
  containerWrap.setAttribute("data-container", pathKey);
5547
6012
  const itemsWrap = document.createElement("div");
5548
6013
  const columns = element.columns || 1;
5549
6014
  if (columns === 1) {
5550
- itemsWrap.className = "space-y-4";
6015
+ itemsWrap.className = "space-y-2";
5551
6016
  } else {
5552
- itemsWrap.className = `grid grid-cols-${columns} gap-4`;
6017
+ itemsWrap.className = `grid grid-cols-${columns} gap-2`;
5553
6018
  }
5554
6019
  const containerIsReadonly = isElementReadonly(element, ctx.state, ctx);
5555
6020
  if (!containerIsReadonly) {
@@ -5583,16 +6048,70 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
5583
6048
  containerWrap.appendChild(itemsWrap);
5584
6049
  wrapper.appendChild(containerWrap);
5585
6050
  }
6051
+ function getChildWrapperClass(isSlides, columns) {
6052
+ if (isSlides) {
6053
+ return "space-y-2";
6054
+ }
6055
+ const cols = columns || 1;
6056
+ return cols === 1 ? "space-y-2" : `grid grid-cols-${cols} gap-2`;
6057
+ }
6058
+ function mountRemoveButton(item, onRemove) {
6059
+ const rem = document.createElement("button");
6060
+ rem.type = "button";
6061
+ rem.className = "fb-item-remove";
6062
+ rem.style.cssText = `
6063
+ width: 22px;
6064
+ height: 22px;
6065
+ display: inline-flex;
6066
+ align-items: center;
6067
+ justify-content: center;
6068
+ padding: 0;
6069
+ line-height: 1;
6070
+ font-size: 14px;
6071
+ color: var(--fb-error-color);
6072
+ background-color: transparent;
6073
+ border: 0;
6074
+ border-radius: 4px;
6075
+ cursor: pointer;
6076
+ flex-shrink: 0;
6077
+ transition: background-color var(--fb-transition-duration);
6078
+ `;
6079
+ rem.textContent = "\u2715";
6080
+ rem.addEventListener("mouseenter", () => {
6081
+ rem.style.backgroundColor = "var(--fb-background-hover-color)";
6082
+ });
6083
+ rem.addEventListener("mouseleave", () => {
6084
+ rem.style.backgroundColor = "transparent";
6085
+ });
6086
+ rem.onclick = onRemove;
6087
+ const labelRow = item.querySelector("[data-fb-label-row]");
6088
+ if (labelRow) {
6089
+ rem.style.marginLeft = "auto";
6090
+ labelRow.appendChild(rem);
6091
+ return;
6092
+ }
6093
+ rem.style.position = "absolute";
6094
+ rem.style.top = "8px";
6095
+ rem.style.right = "8px";
6096
+ item.style.position = "relative";
6097
+ item.appendChild(rem);
6098
+ }
5586
6099
  function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5587
6100
  const state = ctx.state;
5588
6101
  const containerIsReadonly = isElementReadonly(element, state, ctx);
5589
6102
  const childInheritedReadonly = containerIsReadonly || ctx.inheritedReadonly;
5590
6103
  const containerWrap = document.createElement("div");
5591
- containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
5592
- const countDisplay = document.createElement("span");
5593
- countDisplay.className = "text-sm text-gray-500";
6104
+ containerWrap.className = "border border-gray-200 rounded-lg p-2 bg-gray-50";
5594
6105
  const itemsWrap = document.createElement("div");
5595
- itemsWrap.className = "space-y-4";
6106
+ const isSlides = element.displayMode === "slides";
6107
+ if (isSlides) {
6108
+ itemsWrap.className = "fb-container-slides";
6109
+ const slideCols = element.columns;
6110
+ const gridTemplateColumns = typeof slideCols === "number" && slideCols > 0 ? `repeat(${slideCols}, 1fr)` : "repeat(auto-fit, minmax(280px, 1fr))";
6111
+ itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:8px;align-items:start;`;
6112
+ } else {
6113
+ itemsWrap.className = "space-y-2";
6114
+ }
5596
6115
  if (!containerIsReadonly) {
5597
6116
  const hintsElement = createPrefillHints(element, element.key);
5598
6117
  if (hintsElement) {
@@ -5604,97 +6123,64 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5604
6123
  const pre = Array.isArray(ctx.prefill?.[element.key]) ? ctx.prefill[element.key] : null;
5605
6124
  const childDefaults = extractChildDefaults(element.elements);
5606
6125
  const countItems = () => itemsWrap.querySelectorAll(":scope > .containerItem").length;
5607
- const createAddButton = () => {
5608
- const add = document.createElement("button");
5609
- add.type = "button";
5610
- add.className = "add-container-btn px-3 py-1 rounded";
5611
- add.style.cssText = `
5612
- color: var(--fb-primary-color);
5613
- border: var(--fb-border-width) solid var(--fb-primary-color);
5614
- background-color: transparent;
5615
- font-size: var(--fb-font-size);
5616
- transition: all var(--fb-transition-duration);
5617
- `;
5618
- add.textContent = "+";
5619
- add.addEventListener("mouseenter", () => {
5620
- add.style.backgroundColor = "var(--fb-background-hover-color)";
5621
- });
5622
- add.addEventListener("mouseleave", () => {
5623
- add.style.backgroundColor = "transparent";
5624
- });
5625
- add.onclick = () => {
5626
- if (countItems() < max) {
5627
- const idx = countItems();
5628
- const currentFormData = state.formRoot ? extractRootFormData(state.formRoot) : {};
5629
- const subCtx = {
5630
- state: ctx.state,
5631
- path: pathJoin(ctx.path, `${element.key}[${idx}]`),
5632
- prefill: childDefaults,
5633
- // Defaults for enableIf evaluation
5634
- formData: currentFormData,
5635
- // Current root data from DOM for enableIf
5636
- inheritedReadonly: childInheritedReadonly
5637
- };
5638
- const item = document.createElement("div");
5639
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
5640
- item.setAttribute("data-container-item", `${element.key}[${idx}]`);
5641
- 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
- }
5648
- element.elements.forEach((child) => {
5649
- if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
5650
- childWrapper.appendChild(
5651
- createHiddenInput(
5652
- pathJoin(subCtx.path, child.key),
5653
- ("default" in child ? child.default : null) ?? null
5654
- )
5655
- );
5656
- } else {
5657
- childWrapper.appendChild(renderElement(child, subCtx));
5658
- }
5659
- });
5660
- item.appendChild(childWrapper);
5661
- if (!containerIsReadonly) {
5662
- const rem = document.createElement("button");
5663
- rem.type = "button";
5664
- rem.className = "absolute top-2 right-2 px-2 py-1 rounded";
5665
- rem.style.cssText = `
5666
- color: var(--fb-error-color);
5667
- background-color: transparent;
5668
- transition: background-color var(--fb-transition-duration);
5669
- `;
5670
- rem.textContent = "\u2715";
5671
- rem.addEventListener("mouseenter", () => {
5672
- rem.style.backgroundColor = "var(--fb-background-hover-color)";
5673
- });
5674
- rem.addEventListener("mouseleave", () => {
5675
- rem.style.backgroundColor = "transparent";
5676
- });
5677
- rem.onclick = () => handleRemoveItem(item);
5678
- item.style.position = "relative";
5679
- item.appendChild(rem);
5680
- }
5681
- itemsWrap.appendChild(item);
5682
- updateAddButton();
5683
- }
6126
+ const handleAddItem = () => {
6127
+ if (countItems() >= max) return;
6128
+ const idx = countItems();
6129
+ const currentFormData = state.formRoot ? extractRootFormData(state.formRoot) : {};
6130
+ const subCtx = {
6131
+ state: ctx.state,
6132
+ path: pathJoin(ctx.path, `${element.key}[${idx}]`),
6133
+ prefill: childDefaults,
6134
+ // Defaults for enableIf evaluation
6135
+ formData: currentFormData,
6136
+ // Current root data from DOM for enableIf
6137
+ inheritedReadonly: childInheritedReadonly
5684
6138
  };
5685
- return add;
6139
+ const item = document.createElement("div");
6140
+ item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6141
+ item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6142
+ const childWrapper = document.createElement("div");
6143
+ childWrapper.className = getChildWrapperClass(isSlides, element.columns);
6144
+ element.elements.forEach((child) => {
6145
+ if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
6146
+ childWrapper.appendChild(
6147
+ createHiddenInput(
6148
+ pathJoin(subCtx.path, child.key),
6149
+ ("default" in child ? child.default : null) ?? null
6150
+ )
6151
+ );
6152
+ } else {
6153
+ childWrapper.appendChild(renderElement(child, subCtx));
6154
+ }
6155
+ });
6156
+ item.appendChild(childWrapper);
6157
+ if (!containerIsReadonly) {
6158
+ mountRemoveButton(item, () => handleRemoveItem(item));
6159
+ }
6160
+ if (slideAddTile && slideAddTile.parentElement === itemsWrap) {
6161
+ itemsWrap.insertBefore(item, slideAddTile);
6162
+ } else {
6163
+ itemsWrap.appendChild(item);
6164
+ }
6165
+ updateAddButton();
5686
6166
  };
5687
- const updateAddButton = () => {
5688
- const currentCount = countItems();
5689
- const existingAddBtn = containerWrap.querySelector(
5690
- ".add-container-btn"
6167
+ let slideAddTile = null;
6168
+ let slideAddUpdate = null;
6169
+ let pillAddUpdate = null;
6170
+ const syncSlideTileSize = () => {
6171
+ if (!slideAddTile) return;
6172
+ const firstSlide = itemsWrap.querySelector(
6173
+ ":scope > .containerItem"
5691
6174
  );
5692
- if (existingAddBtn) {
5693
- existingAddBtn.disabled = currentCount >= max;
5694
- existingAddBtn.style.opacity = currentCount >= max ? "0.5" : "1";
5695
- existingAddBtn.style.pointerEvents = currentCount >= max ? "none" : "auto";
6175
+ if (firstSlide && firstSlide.offsetHeight > 0) {
6176
+ slideAddTile.style.minHeight = `${firstSlide.offsetHeight}px`;
5696
6177
  }
5697
- countDisplay.textContent = `${currentCount}/${max === Infinity ? "\u221E" : max}`;
6178
+ };
6179
+ const updateAddButton = () => {
6180
+ const currentCount = countItems();
6181
+ if (slideAddUpdate) slideAddUpdate(currentCount, max);
6182
+ if (pillAddUpdate) pillAddUpdate(currentCount, max);
6183
+ if (slideAddTile) syncSlideTileSize();
5698
6184
  };
5699
6185
  const handleRemoveItem = (item) => {
5700
6186
  item.remove();
@@ -5713,14 +6199,18 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5713
6199
  inheritedReadonly: childInheritedReadonly
5714
6200
  };
5715
6201
  const item = document.createElement("div");
5716
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
6202
+ item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
5717
6203
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
5718
6204
  const childWrapper = document.createElement("div");
5719
- const columns = element.columns || 1;
5720
- if (columns === 1) {
5721
- childWrapper.className = "space-y-4";
6205
+ if (isSlides) {
6206
+ childWrapper.className = "space-y-2";
5722
6207
  } else {
5723
- childWrapper.className = `grid grid-cols-${columns} gap-4`;
6208
+ const columns = element.columns || 1;
6209
+ if (columns === 1) {
6210
+ childWrapper.className = "space-y-2";
6211
+ } else {
6212
+ childWrapper.className = `grid grid-cols-${columns} gap-2`;
6213
+ }
5724
6214
  }
5725
6215
  element.elements.forEach((child) => {
5726
6216
  if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
@@ -5734,24 +6224,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5734
6224
  });
5735
6225
  item.appendChild(childWrapper);
5736
6226
  if (!containerIsReadonly) {
5737
- const rem = document.createElement("button");
5738
- rem.type = "button";
5739
- rem.className = "absolute top-2 right-2 px-2 py-1 rounded";
5740
- rem.style.cssText = `
5741
- color: var(--fb-error-color);
5742
- background-color: transparent;
5743
- transition: background-color var(--fb-transition-duration);
5744
- `;
5745
- rem.textContent = "\u2715";
5746
- rem.addEventListener("mouseenter", () => {
5747
- rem.style.backgroundColor = "var(--fb-background-hover-color)";
5748
- });
5749
- rem.addEventListener("mouseleave", () => {
5750
- rem.style.backgroundColor = "transparent";
5751
- });
5752
- rem.onclick = () => handleRemoveItem(item);
5753
- item.style.position = "relative";
5754
- item.appendChild(rem);
6227
+ mountRemoveButton(item, () => handleRemoveItem(item));
5755
6228
  }
5756
6229
  itemsWrap.appendChild(item);
5757
6230
  });
@@ -5769,14 +6242,18 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5769
6242
  inheritedReadonly: childInheritedReadonly
5770
6243
  };
5771
6244
  const item = document.createElement("div");
5772
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
6245
+ item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
5773
6246
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
5774
6247
  const childWrapper = document.createElement("div");
5775
- const columns = element.columns || 1;
5776
- if (columns === 1) {
5777
- childWrapper.className = "space-y-4";
6248
+ if (isSlides) {
6249
+ childWrapper.className = "space-y-2";
5778
6250
  } else {
5779
- childWrapper.className = `grid grid-cols-${columns} gap-4`;
6251
+ const columns = element.columns || 1;
6252
+ if (columns === 1) {
6253
+ childWrapper.className = "space-y-2";
6254
+ } else {
6255
+ childWrapper.className = `grid grid-cols-${columns} gap-2`;
6256
+ }
5780
6257
  }
5781
6258
  element.elements.forEach((child) => {
5782
6259
  if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
@@ -5791,41 +6268,43 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5791
6268
  }
5792
6269
  });
5793
6270
  item.appendChild(childWrapper);
5794
- const rem = document.createElement("button");
5795
- rem.type = "button";
5796
- rem.className = "absolute top-2 right-2 px-2 py-1 rounded";
5797
- rem.style.cssText = `
5798
- color: var(--fb-error-color);
5799
- background-color: transparent;
5800
- transition: background-color var(--fb-transition-duration);
5801
- `;
5802
- rem.textContent = "\u2715";
5803
- rem.addEventListener("mouseenter", () => {
5804
- rem.style.backgroundColor = "var(--fb-background-hover-color)";
5805
- });
5806
- rem.addEventListener("mouseleave", () => {
5807
- rem.style.backgroundColor = "transparent";
5808
- });
5809
- rem.onclick = () => {
6271
+ mountRemoveButton(item, () => {
5810
6272
  if (countItems() > min) {
5811
6273
  handleRemoveItem(item);
5812
6274
  }
5813
- };
5814
- item.style.position = "relative";
5815
- item.appendChild(rem);
6275
+ });
5816
6276
  itemsWrap.appendChild(item);
5817
6277
  }
5818
6278
  }
5819
6279
  containerWrap.appendChild(itemsWrap);
5820
6280
  if (!containerIsReadonly) {
5821
- const addRow = document.createElement("div");
5822
- addRow.className = "flex items-center gap-3 mt-2";
5823
- addRow.appendChild(createAddButton());
5824
- addRow.appendChild(countDisplay);
5825
- containerWrap.appendChild(addRow);
6281
+ if (isSlides) {
6282
+ itemsWrap.style.alignItems = "stretch";
6283
+ const handle = createSlideAddTile(handleAddItem, {
6284
+ label: element.addLabel
6285
+ });
6286
+ slideAddTile = handle.tile;
6287
+ slideAddUpdate = handle.update;
6288
+ mountCounterInLabel(wrapper, handle.counter);
6289
+ itemsWrap.appendChild(handle.tile);
6290
+ } else {
6291
+ const handle = createAddItemRow("container", handleAddItem, {
6292
+ label: element.addLabel
6293
+ });
6294
+ pillAddUpdate = handle.update;
6295
+ mountCounterInLabel(wrapper, handle.counter);
6296
+ containerWrap.appendChild(handle.row);
6297
+ }
5826
6298
  }
5827
6299
  updateAddButton();
5828
6300
  wrapper.appendChild(containerWrap);
6301
+ if (slideAddTile) {
6302
+ if (typeof requestAnimationFrame === "function") {
6303
+ requestAnimationFrame(syncSlideTileSize);
6304
+ } else {
6305
+ syncSlideTileSize();
6306
+ }
6307
+ }
5829
6308
  }
5830
6309
  var validateElementFunc = null;
5831
6310
  function setValidateElement(fn) {
@@ -7804,7 +8283,7 @@ function filterFilesForDropdown(query, files, labels) {
7804
8283
  });
7805
8284
  }
7806
8285
  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;";
8286
+ var TEXTAREA_PADDING = "padding: 8px 40px 8px 10px;";
7808
8287
  function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
7809
8288
  const state = ctx.state;
7810
8289
  const files = [...initialValue.files];
@@ -7851,7 +8330,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
7851
8330
  });
7852
8331
  const errorEl = document.createElement("div");
7853
8332
  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;";
8333
+ errorEl.style.cssText = "display: none; color: var(--fb-error-color, #ef4444); font-size: var(--fb-font-size-small, 12px); padding: 4px 10px 6px;";
7855
8334
  let errorTimer = null;
7856
8335
  function showUploadError(message) {
7857
8336
  errorEl.textContent = message;
@@ -7927,7 +8406,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
7927
8406
  });
7928
8407
  const filesRow = document.createElement("div");
7929
8408
  filesRow.className = "fb-richinput-files";
7930
- filesRow.style.cssText = "display: none; flex-wrap: wrap; gap: 6px; padding: 10px 14px 0; align-items: center;";
8409
+ filesRow.style.cssText = "display: none; flex-wrap: wrap; gap: 4px; padding: 6px 10px 0; align-items: center;";
7931
8410
  const fileInput = document.createElement("input");
7932
8411
  fileInput.type = "file";
7933
8412
  fileInput.multiple = true;
@@ -8057,13 +8536,13 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
8057
8536
  paperclipBtn.title = t("richinputAttachFile", state);
8058
8537
  paperclipBtn.style.cssText = `
8059
8538
  position: absolute;
8060
- right: 10px;
8061
- bottom: 10px;
8539
+ right: 6px;
8540
+ bottom: 6px;
8062
8541
  z-index: 2;
8063
- width: 32px;
8064
- height: 32px;
8542
+ width: 28px;
8543
+ height: 28px;
8065
8544
  border: none;
8066
- border-radius: 8px;
8545
+ border-radius: 6px;
8067
8546
  background: transparent;
8068
8547
  cursor: pointer;
8069
8548
  display: flex;
@@ -8417,7 +8896,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
8417
8896
  outerDiv.appendChild(errorEl);
8418
8897
  if (element.minLength != null || element.maxLength != null) {
8419
8898
  const counterRow = document.createElement("div");
8420
- counterRow.style.cssText = "position: relative; padding: 2px 14px 6px; text-align: right;";
8899
+ counterRow.style.cssText = "position: relative; padding: 2px 10px 4px; text-align: right;";
8421
8900
  const counter = createCharCounter(element, textarea, false);
8422
8901
  counter.style.cssText = `
8423
8902
  position: static;
@@ -8720,10 +9199,7 @@ var TAGS = {
8720
9199
  "-": ["<hr />"]
8721
9200
  };
8722
9201
  function outdent(str) {
8723
- return str.replace(
8724
- RegExp("^" + (str.match(/^(\t| )+/) || "")[0], "gm"),
8725
- ""
8726
- );
9202
+ return str.replace(RegExp("^" + (str.match(/^(\t| )+/) || "")[0], "gm"), "");
8727
9203
  }
8728
9204
  function encodeAttr(str) {
8729
9205
  return (str + "").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -8886,12 +9362,7 @@ function ensureMarkdownStyles() {
8886
9362
  `;
8887
9363
  document.head.appendChild(style);
8888
9364
  }
8889
- var ANCHOR_DANGEROUS_SCHEMES = [
8890
- "javascript:",
8891
- "data:",
8892
- "vbscript:",
8893
- "blob:"
8894
- ];
9365
+ var ANCHOR_DANGEROUS_SCHEMES = ["javascript:", "data:", "vbscript:", "blob:"];
8895
9366
  var IMG_DANGEROUS_SCHEMES = ["javascript:", "vbscript:", "blob:"];
8896
9367
  function isImgSrcDangerous(normalized) {
8897
9368
  if (IMG_DANGEROUS_SCHEMES.some((scheme) => normalized.startsWith(scheme))) {
@@ -9211,7 +9682,8 @@ function createInfoButton(element) {
9211
9682
  }
9212
9683
  function createLabelContainer(element) {
9213
9684
  const label = document.createElement("div");
9214
- label.className = "flex items-center mb-2";
9685
+ label.className = "flex items-center mb-1";
9686
+ label.dataset.fbLabelRow = "";
9215
9687
  const title = createFieldLabel(element);
9216
9688
  label.appendChild(title);
9217
9689
  if (element.description || element.hint) {
@@ -9318,7 +9790,7 @@ function renderElement2(element, ctx) {
9318
9790
  }
9319
9791
  const initiallyDisabled2 = shouldDisableElement(element, ctx);
9320
9792
  const outerWrapper = document.createElement("div");
9321
- outerWrapper.className = "mb-6 fb-field-wrapper fb-markdown-wrapper";
9793
+ outerWrapper.className = "mb-2 fb-field-wrapper fb-markdown-wrapper";
9322
9794
  outerWrapper.setAttribute(
9323
9795
  "data-field-key",
9324
9796
  getElementLookupKey(element, ctx.state)
@@ -9334,7 +9806,7 @@ function renderElement2(element, ctx) {
9334
9806
  }
9335
9807
  const initiallyDisabled = shouldDisableElement(element, ctx);
9336
9808
  const wrapper = document.createElement("div");
9337
- wrapper.className = "mb-6 fb-field-wrapper";
9809
+ wrapper.className = "mb-2 fb-field-wrapper";
9338
9810
  wrapper.setAttribute("data-field-key", element.key);
9339
9811
  const label = createLabelContainer(element);
9340
9812
  wrapper.appendChild(label);
@@ -9401,12 +9873,16 @@ var defaultConfig = {
9401
9873
  hintPattern: "Format: {pattern}",
9402
9874
  fileCountSingle: "{count} file",
9403
9875
  fileCountPlural: "{count} files",
9876
+ fileCountWithMax: "{count} / {max} files",
9404
9877
  fileCountRange: "({min}-{max})",
9405
9878
  uploadingFile: "Uploading\u2026",
9406
9879
  filesCounter: "{count}/{max}",
9407
9880
  fromLibrary: "From library",
9408
9881
  libraryEmpty: "Library is empty",
9409
9882
  libraryHint: "Choose from previously uploaded files",
9883
+ dropToUpload: "Release to upload",
9884
+ replaceFile: "Replace",
9885
+ clearAll: "Clear all",
9410
9886
  pickerError: "Failed to load files from library",
9411
9887
  // Validation errors
9412
9888
  required: "Required",
@@ -9425,6 +9901,7 @@ var defaultConfig = {
9425
9901
  invalidFileExtension: 'File "{name}" has unsupported format. Allowed: {formats}',
9426
9902
  invalidFileMime: 'File "{name}": file type {type} not allowed (allowed: {mimes})',
9427
9903
  fileTooLarge: 'File "{name}" exceeds maximum size of {maxSize}MB',
9904
+ uploadFailed: 'Failed to upload "{name}": {error}',
9428
9905
  filesLimitExceeded: "{skipped} file(s) skipped: maximum {max} files allowed",
9429
9906
  unsupportedFieldType: "Unsupported field type: {type}",
9430
9907
  invalidOption: "Invalid option",
@@ -9472,12 +9949,16 @@ var defaultConfig = {
9472
9949
  hintPattern: "\u0424\u043E\u0440\u043C\u0430\u0442: {pattern}",
9473
9950
  fileCountSingle: "{count} \u0444\u0430\u0439\u043B",
9474
9951
  fileCountPlural: "{count} \u0444\u0430\u0439\u043B\u043E\u0432",
9952
+ fileCountWithMax: "{count} / {max} \u0444\u0430\u0439\u043B\u043E\u0432",
9475
9953
  fileCountRange: "({min}-{max})",
9476
9954
  uploadingFile: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026",
9477
9955
  filesCounter: "{count}/{max}",
9478
9956
  fromLibrary: "\u0418\u0437 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438",
9479
9957
  libraryEmpty: "\u0411\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0430 \u043F\u0443\u0441\u0442\u0430",
9480
9958
  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",
9959
+ dropToUpload: "\u041E\u0442\u043F\u0443\u0441\u0442\u0438\u0442\u0435, \u0447\u0442\u043E\u0431\u044B \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C",
9960
+ replaceFile: "\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C",
9961
+ clearAll: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0441\u0435",
9481
9962
  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
9963
  // Validation errors
9483
9964
  required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
@@ -9496,6 +9977,7 @@ var defaultConfig = {
9496
9977
  invalidFileExtension: '\u0424\u0430\u0439\u043B "{name}" \u0438\u043C\u0435\u0435\u0442 \u043D\u0435\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442. \u0414\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0435: {formats}',
9497
9978
  invalidFileMime: '\u0424\u0430\u0439\u043B "{name}": \u0442\u0438\u043F \u0444\u0430\u0439\u043B\u0430 {type} \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D (\u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u044B: {mimes})',
9498
9979
  fileTooLarge: '\u0424\u0430\u0439\u043B "{name}" \u043F\u0440\u0435\u0432\u044B\u0448\u0430\u0435\u0442 \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 {maxSize}\u041C\u0411',
9980
+ uploadFailed: '\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C "{name}": {error}',
9499
9981
  filesLimitExceeded: "{skipped} \u0444\u0430\u0439\u043B(\u043E\u0432) \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u043E: \u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
9500
9982
  unsupportedFieldType: "\u041D\u0435\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0439 \u0442\u0438\u043F \u043F\u043E\u043B\u044F: {type}",
9501
9983
  invalidOption: "\u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435",
@@ -9618,10 +10100,10 @@ var defaultTheme = {
9618
10100
  fileUploadHoverBorderColor: "#3b82f6",
9619
10101
  // blue-500
9620
10102
  // Spacing
9621
- inputPaddingX: "0.75rem",
9622
- // 3 (12px)
9623
- inputPaddingY: "0.5rem",
9624
- // 2 (8px)
10103
+ inputPaddingX: "0.5rem",
10104
+ // 8px (compact density v2)
10105
+ inputPaddingY: "0.25rem",
10106
+ // 4px (compact density v2)
9625
10107
  borderRadius: "0.5rem",
9626
10108
  // rounded-lg (8px)
9627
10109
  borderWidth: "1px",
@@ -9718,29 +10200,6 @@ var exampleThemes = {
9718
10200
  }
9719
10201
  };
9720
10202
 
9721
- // src/utils/styles.ts
9722
- function applyActionButtonStyles(button, isFormLevel = false) {
9723
- button.style.cssText = `
9724
- background-color: var(--fb-action-bg-color);
9725
- color: var(--fb-action-text-color);
9726
- border: var(--fb-border-width) solid var(--fb-action-border-color);
9727
- padding: ${isFormLevel ? "0.5rem 1rem" : "0.5rem 0.75rem"};
9728
- font-size: var(--fb-font-size);
9729
- font-weight: var(--fb-font-weight-medium);
9730
- border-radius: var(--fb-border-radius);
9731
- transition: all var(--fb-transition-duration);
9732
- box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
9733
- `;
9734
- button.addEventListener("mouseenter", () => {
9735
- button.style.backgroundColor = "var(--fb-action-hover-bg-color)";
9736
- button.style.borderColor = "var(--fb-action-hover-border-color)";
9737
- });
9738
- button.addEventListener("mouseleave", () => {
9739
- button.style.backgroundColor = "var(--fb-action-bg-color)";
9740
- button.style.borderColor = "var(--fb-action-border-color)";
9741
- });
9742
- }
9743
-
9744
10203
  // src/components/registry.ts
9745
10204
  function validateHiddenElement(element, key, context) {
9746
10205
  const { scopeRoot } = context;
@@ -10066,7 +10525,7 @@ var FormBuilderInstance = class {
10066
10525
  existingContainer.remove();
10067
10526
  }
10068
10527
  const actionsContainer = document.createElement("div");
10069
- actionsContainer.className = "form-level-actions-container mt-6 pt-4 flex flex-wrap gap-3 justify-center";
10528
+ actionsContainer.className = "form-level-actions-container mt-3 pt-2 flex flex-wrap gap-2 justify-center";
10070
10529
  actionsContainer.style.cssText = `
10071
10530
  border-top: var(--fb-border-width) solid var(--fb-border-color);
10072
10531
  `;
@@ -10207,7 +10666,7 @@ var FormBuilderInstance = class {
10207
10666
  */
10208
10667
  createRootPrefillHints(hints) {
10209
10668
  const hintsContainer = document.createElement("div");
10210
- hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-4";
10669
+ hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-2";
10211
10670
  hints.forEach((hint) => {
10212
10671
  const hintButton = document.createElement("button");
10213
10672
  hintButton.type = "button";
@@ -10240,7 +10699,7 @@ var FormBuilderInstance = class {
10240
10699
  root.setAttribute("data-fb-root", "true");
10241
10700
  injectThemeVariables(root, this.state.config.theme);
10242
10701
  const rootContainer = document.createElement("div");
10243
- rootContainer.className = "space-y-6";
10702
+ rootContainer.className = "space-y-2";
10244
10703
  if (schema.prefillHints && !this.state.config.readonly) {
10245
10704
  const hintsContainer = this.createRootPrefillHints(schema.prefillHints);
10246
10705
  rootContainer.appendChild(hintsContainer);
@@ -10248,9 +10707,9 @@ var FormBuilderInstance = class {
10248
10707
  const fieldsWrapper = document.createElement("div");
10249
10708
  const columns = schema.columns || 1;
10250
10709
  if (columns === 1) {
10251
- fieldsWrapper.className = "space-y-4";
10710
+ fieldsWrapper.className = "space-y-2";
10252
10711
  } else {
10253
- fieldsWrapper.className = `grid grid-cols-${columns} gap-4`;
10712
+ fieldsWrapper.className = `grid grid-cols-${columns} gap-2`;
10254
10713
  }
10255
10714
  schema.elements.forEach((element) => {
10256
10715
  if (element.type !== "markdown" && (element.hidden || element.type === "hidden")) {