@dmitryvim/form-builder 0.2.34 → 0.3.1

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.
@@ -407,6 +407,114 @@ function deepEqual(a, b) {
407
407
  }
408
408
 
409
409
  // src/utils/styles.ts
410
+ function clearFieldError(input) {
411
+ const name = input.getAttribute("name");
412
+ if (!name) return;
413
+ const doc = input.ownerDocument || document;
414
+ const errorNode = doc.getElementById(`error-${name}`);
415
+ if (errorNode) errorNode.remove();
416
+ }
417
+ var BIN_ICON_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
418
+ function ensureThemingHooks(doc) {
419
+ if (doc.head.querySelector("[data-fb-theming-hooks]")) return;
420
+ const style = doc.createElement("style");
421
+ style.setAttribute("data-fb-theming-hooks", "");
422
+ style.textContent = `
423
+ [data-fb-slide-card] {
424
+ background: var(--fb-slide-card-bg);
425
+ box-shadow: var(--fb-slide-card-shadow);
426
+ border-radius: var(--fb-slide-card-radius);
427
+ min-height: var(--fb-slide-card-min-height);
428
+ padding: var(--fb-slide-card-padding);
429
+ }
430
+ [data-fb-label-row] > label {
431
+ font-size: var(--fb-label-section-font-size);
432
+ letter-spacing: var(--fb-label-section-letter-spacing);
433
+ text-transform: var(--fb-label-section-text-transform);
434
+ }
435
+ /* Per-item remove (trash) button used by multi-container items. Shares the
436
+ same faint-on-rest, error-on-hover palette as .fb-chip-remove. */
437
+ .fb-item-remove {
438
+ color: var(--fb-text-faint-color, #94a3b8);
439
+ background-color: transparent;
440
+ transition: color var(--fb-transition-duration), background-color var(--fb-transition-duration);
441
+ }
442
+ .fb-item-remove:hover {
443
+ color: var(--fb-error-color);
444
+ background-color: var(--fb-background-hover-color);
445
+ }
446
+ /* Prefill-suggestion pills rendered by createPrefillHints. Outline pill at rest,
447
+ soft-fill on hover, solid-fill when selected. All colors flow from the active
448
+ theme \u2014 consumers don't need to ship their own CSS. */
449
+ .fb-prefill-hint {
450
+ padding: 0.25rem 0.625rem;
451
+ border: var(--fb-border-width) solid var(--fb-primary-color);
452
+ border-radius: 9999px;
453
+ background: var(--fb-background-color);
454
+ color: var(--fb-primary-color);
455
+ font-size: var(--fb-font-size-small);
456
+ font-weight: var(--fb-font-weight-medium);
457
+ font-family: var(--fb-font-family);
458
+ cursor: pointer;
459
+ transition: background-color var(--fb-transition-duration), border-color var(--fb-transition-duration), color var(--fb-transition-duration);
460
+ }
461
+ .fb-prefill-hint:hover {
462
+ background: var(--fb-primary-soft-color);
463
+ border-color: var(--fb-primary-hover-color);
464
+ color: var(--fb-primary-hover-color);
465
+ }
466
+ .fb-prefill-hint:focus-visible {
467
+ outline: var(--fb-focus-ring-width) solid var(--fb-focus-ring-color);
468
+ outline-offset: 2px;
469
+ }
470
+ .fb-prefill-hint[aria-pressed="true"],
471
+ .fb-prefill-hint.active {
472
+ background: var(--fb-primary-color);
473
+ color: #ffffff;
474
+ border-color: var(--fb-primary-color);
475
+ }
476
+ `;
477
+ doc.head.appendChild(style);
478
+ }
479
+ function applyAutoExpand(textarea) {
480
+ textarea.style.overflow = "hidden";
481
+ textarea.style.resize = "none";
482
+ const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
483
+ textarea.rows = Math.max(1, lineCount);
484
+ const resize = () => {
485
+ if (!textarea.isConnected) return;
486
+ textarea.style.height = "0";
487
+ const cs = getComputedStyle(textarea);
488
+ const borderY = parseFloat(cs.borderTopWidth || "0") + parseFloat(cs.borderBottomWidth || "0");
489
+ textarea.style.height = `${textarea.scrollHeight + borderY}px`;
490
+ };
491
+ textarea.addEventListener("input", resize);
492
+ setTimeout(() => {
493
+ if (textarea.isConnected) resize();
494
+ }, 0);
495
+ }
496
+ function applySingleLineMode(textarea) {
497
+ textarea.addEventListener("keydown", (e) => {
498
+ if (e.key === "Enter") {
499
+ e.preventDefault();
500
+ }
501
+ });
502
+ textarea.addEventListener("paste", (e) => {
503
+ var _a, _b, _c, _d;
504
+ const pasted = (_b = (_a = e.clipboardData) == null ? void 0 : _a.getData("text")) != null ? _b : "";
505
+ if (!/[\r\n]/.test(pasted)) return;
506
+ e.preventDefault();
507
+ const cleaned = pasted.replace(/[\r\n]+/g, " ");
508
+ const start = (_c = textarea.selectionStart) != null ? _c : textarea.value.length;
509
+ const end = (_d = textarea.selectionEnd) != null ? _d : textarea.value.length;
510
+ const before = textarea.value.slice(0, start);
511
+ const after = textarea.value.slice(end);
512
+ textarea.value = before + cleaned + after;
513
+ const pos = start + cleaned.length;
514
+ textarea.setSelectionRange(pos, pos);
515
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
516
+ });
517
+ }
410
518
  function mountCounterInLabel(wrapper, counter) {
411
519
  const labelRow = wrapper.querySelector(
412
520
  ":scope > [data-fb-label-row]"
@@ -570,46 +678,94 @@ function applyActionButtonStyles(button, isFormLevel = false) {
570
678
  }
571
679
 
572
680
  // src/components/text.ts
573
- function createCharCounter(element, input, isTextarea = false) {
681
+ function ensureChipStyles(doc) {
682
+ if (doc.head.querySelector("[data-fb-chip-styles]")) return;
683
+ const style = doc.createElement("style");
684
+ style.setAttribute("data-fb-chip-styles", "");
685
+ style.textContent = `
686
+ .fb-chip-list { display: flex; flex-direction: column; gap: 4px; }
687
+ .fb-chip {
688
+ display: flex;
689
+ align-items: center;
690
+ gap: 8px;
691
+ padding: 5px 6px 5px 10px;
692
+ background: var(--fb-chip-bg, var(--fb-background-color, #fff));
693
+ border: 1px solid var(--fb-chip-border, var(--fb-border-color, #e2e8f0));
694
+ border-radius: 6px;
695
+ position: relative;
696
+ transition: border-color var(--fb-transition-duration, 0.15s);
697
+ }
698
+ .fb-chip:hover { border-color: var(--fb-border-hover-color, var(--fb-border-color, #cbd5e1)); }
699
+ .fb-chip:focus-within { border-color: var(--fb-border-focus-color, var(--fb-primary-color, #2f5bea)); }
700
+ .fb-chip-dot {
701
+ flex: 0 0 6px;
702
+ width: 6px;
703
+ height: 6px;
704
+ border-radius: 50%;
705
+ background: var(--fb-chip-dot, var(--fb-primary-color, #2f5bea));
706
+ }
707
+ .fb-chip-input {
708
+ flex: 1;
709
+ min-width: 0;
710
+ padding: 2px 0;
711
+ border: 0;
712
+ outline: none;
713
+ background: transparent;
714
+ color: var(--fb-chip-text, var(--fb-text-color, inherit));
715
+ font-size: var(--fb-font-size, 14px);
716
+ font-family: var(--fb-font-family, inherit);
717
+ line-height: 1.4;
718
+ }
719
+ .fb-chip-input::placeholder { color: var(--fb-text-placeholder-color, #94a3b8); }
720
+ .fb-chip-input:read-only { color: var(--fb-text-secondary-color, #475569); }
721
+ .fb-chip-remove {
722
+ flex: 0 0 auto;
723
+ width: 22px;
724
+ height: 22px;
725
+ display: inline-flex;
726
+ align-items: center;
727
+ justify-content: center;
728
+ padding: 0;
729
+ border: 0;
730
+ border-radius: 4px;
731
+ background: transparent;
732
+ color: var(--fb-text-faint-color, #94a3b8);
733
+ cursor: pointer;
734
+ opacity: 0;
735
+ transition: opacity 0.12s, color 0.12s, background-color 0.12s;
736
+ }
737
+ .fb-chip:hover .fb-chip-remove,
738
+ .fb-chip-remove:focus-visible { opacity: 1; }
739
+ .fb-chip-remove:hover {
740
+ color: var(--fb-error-color, #dc2626);
741
+ background: var(--fb-background-hover-color, #f1f5f9);
742
+ }
743
+ .fb-chip-remove:disabled { opacity: 0 !important; pointer-events: none; }
744
+ `;
745
+ doc.head.appendChild(style);
746
+ }
747
+ function createCharCounter(element, input) {
574
748
  const counter = document.createElement("span");
575
749
  counter.className = "char-counter";
576
750
  counter.style.cssText = `
577
- position: absolute;
578
- ${isTextarea ? "bottom: 8px" : "top: 50%; transform: translateY(-50%)"};
579
- right: 10px;
751
+ margin-top: 4px;
752
+ padding-right: 12px;
753
+ text-align: right;
580
754
  font-size: var(--fb-font-size-small);
581
- color: var(--fb-text-secondary-color);
755
+ line-height: 1;
756
+ color: var(--fb-error-color);
582
757
  pointer-events: none;
583
- background: var(--fb-background-color);
584
- padding: 0 4px;
758
+ display: none;
585
759
  `;
586
760
  const updateCounter = () => {
587
761
  const len = input.value.length;
588
- const min = element.minLength;
589
762
  const max = element.maxLength;
590
- if (min == null && max == null) {
591
- counter.textContent = "";
592
- return;
593
- }
594
- if (len === 0 || min != null && len < min) {
595
- if (min != null && max != null) {
596
- counter.textContent = `${min}-${max}`;
597
- } else if (max != null) {
598
- counter.textContent = `\u2264${max}`;
599
- } else if (min != null) {
600
- counter.textContent = `\u2265${min}`;
601
- }
602
- counter.style.color = "var(--fb-text-secondary-color)";
603
- } else if (max != null && len > max) {
763
+ if (max != null && len > max) {
604
764
  counter.textContent = `${len}/${max}`;
605
- counter.style.color = "var(--fb-error-color)";
765
+ counter.style.display = "block";
606
766
  } else {
607
- if (max != null) {
608
- counter.textContent = `${len}/${max}`;
609
- } else {
610
- counter.textContent = `${len}`;
611
- }
612
- counter.style.color = "var(--fb-text-secondary-color)";
767
+ counter.textContent = "";
768
+ counter.style.display = "none";
613
769
  }
614
770
  };
615
771
  input.addEventListener("input", updateCounter);
@@ -622,26 +778,32 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
622
778
  const inputWrapper = document.createElement("div");
623
779
  inputWrapper.style.cssText = "position: relative;";
624
780
  const hasCharCounter = !readonly && (element.minLength != null || element.maxLength != null);
625
- const textInput = document.createElement("input");
626
- textInput.type = "text";
781
+ const textInput = document.createElement("textarea");
782
+ textInput.rows = 1;
627
783
  textInput.className = "w-full rounded-lg";
628
784
  textInput.style.cssText = `
629
785
  padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
630
- ${hasCharCounter ? "padding-right: 60px;" : ""}
631
786
  border: var(--fb-border-width) solid var(--fb-border-color);
632
787
  border-radius: var(--fb-border-radius);
633
788
  background-color: ${readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
634
789
  color: var(--fb-text-color);
635
790
  font-size: var(--fb-font-size);
636
791
  font-family: var(--fb-font-family);
792
+ line-height: var(--fb-line-height, 1.5);
637
793
  transition: all var(--fb-transition-duration) ease-in-out;
638
794
  width: 100%;
639
795
  box-sizing: border-box;
796
+ resize: none;
797
+ overflow: hidden;
798
+ word-break: break-word;
799
+ overflow-wrap: anywhere;
640
800
  `;
641
801
  textInput.name = pathKey;
642
802
  textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
643
803
  textInput.value = ctx.prefill[element.key] || element.default || "";
644
804
  textInput.readOnly = readonly;
805
+ applySingleLineMode(textInput);
806
+ applyAutoExpand(textInput);
645
807
  if (!readonly) {
646
808
  textInput.addEventListener("focus", () => {
647
809
  textInput.style.borderColor = "var(--fb-border-focus-color)";
@@ -673,7 +835,7 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
673
835
  }
674
836
  inputWrapper.appendChild(textInput);
675
837
  if (hasCharCounter) {
676
- const counter = createCharCounter(element, textInput, false);
838
+ const counter = createCharCounter(element, textInput);
677
839
  inputWrapper.appendChild(counter);
678
840
  }
679
841
  wrapper.appendChild(inputWrapper);
@@ -684,132 +846,83 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
684
846
  const readonly = isElementReadonly(element, state, ctx);
685
847
  const prefillValues = ctx.prefill[element.key] || [];
686
848
  const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
687
- const hasCharCounter = !readonly && (element.minLength != null || element.maxLength != null);
688
849
  const minCount = (_a = element.minCount) != null ? _a : 1;
689
850
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
690
851
  while (values.length < minCount) {
691
852
  values.push(element.default || "");
692
853
  }
693
- const container = document.createElement("div");
694
- container.className = "space-y-2";
695
- wrapper.appendChild(container);
854
+ ensureChipStyles(document);
855
+ const list = document.createElement("div");
856
+ list.className = "fb-chip-list";
857
+ wrapper.appendChild(list);
696
858
  function updateIndices() {
697
- const items = container.querySelectorAll(".multiple-text-item");
698
- items.forEach((item, index) => {
699
- const input = item.querySelector("input");
700
- if (input) {
701
- input.name = `${pathKey}[${index}]`;
859
+ const items = list.querySelectorAll(".fb-chip-input");
860
+ items.forEach((input, index) => {
861
+ input.name = `${pathKey}[${index}]`;
862
+ const chip = input.closest(".fb-chip");
863
+ const sib = chip == null ? void 0 : chip.nextElementSibling;
864
+ if (sib && sib.classList.contains("error-message")) {
865
+ sib.id = `error-${input.name}`;
702
866
  }
703
867
  });
704
868
  }
705
- function addTextItem(value = "", index = -1) {
706
- const itemWrapper = document.createElement("div");
707
- itemWrapper.className = "multiple-text-item flex items-center gap-2";
708
- const inputContainer = document.createElement("div");
709
- inputContainer.style.cssText = "position: relative; flex: 1;";
710
- const textInput = document.createElement("input");
711
- textInput.type = "text";
712
- textInput.style.cssText = `
713
- padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
714
- ${hasCharCounter ? "padding-right: 60px;" : ""}
715
- border: var(--fb-border-width) solid var(--fb-border-color);
716
- border-radius: var(--fb-border-radius);
717
- background-color: ${readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
718
- color: var(--fb-text-color);
719
- font-size: var(--fb-font-size);
720
- font-family: var(--fb-font-family);
721
- transition: all var(--fb-transition-duration) ease-in-out;
722
- width: 100%;
723
- box-sizing: border-box;
724
- `;
725
- textInput.placeholder = element.placeholder || t("placeholderText", state);
726
- textInput.value = value;
727
- textInput.readOnly = readonly;
728
- if (!readonly) {
729
- textInput.addEventListener("focus", () => {
730
- textInput.style.borderColor = "var(--fb-border-focus-color)";
731
- textInput.style.outline = `var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)`;
732
- textInput.style.outlineOffset = "0";
733
- });
734
- textInput.addEventListener("blur", () => {
735
- textInput.style.borderColor = "var(--fb-border-color)";
736
- textInput.style.outline = "none";
737
- });
738
- textInput.addEventListener("mouseenter", () => {
739
- if (document.activeElement !== textInput) {
740
- textInput.style.borderColor = "var(--fb-border-hover-color)";
741
- }
742
- });
743
- textInput.addEventListener("mouseleave", () => {
744
- if (document.activeElement !== textInput) {
745
- textInput.style.borderColor = "var(--fb-border-color)";
746
- }
747
- });
748
- }
869
+ function addChip(value = "") {
870
+ const chip = document.createElement("div");
871
+ chip.className = "fb-chip";
872
+ const dot = document.createElement("span");
873
+ dot.className = "fb-chip-dot";
874
+ dot.setAttribute("aria-hidden", "true");
875
+ chip.appendChild(dot);
876
+ const input = document.createElement("input");
877
+ input.type = "text";
878
+ input.className = "fb-chip-input";
879
+ input.value = value;
880
+ input.placeholder = element.placeholder || t("placeholderText", state);
881
+ input.readOnly = readonly;
882
+ chip.appendChild(input);
749
883
  if (!readonly && ctx.instance) {
750
884
  const handleChange = () => {
751
- const value2 = textInput.value === "" ? null : textInput.value;
752
- ctx.instance.triggerOnChange(textInput.name, value2);
885
+ ctx.instance.triggerOnChange(
886
+ input.name,
887
+ input.value === "" ? null : input.value
888
+ );
753
889
  };
754
- textInput.addEventListener("blur", handleChange);
755
- textInput.addEventListener("input", handleChange);
756
- }
757
- inputContainer.appendChild(textInput);
758
- if (hasCharCounter) {
759
- const counter = createCharCounter(element, textInput, false);
760
- inputContainer.appendChild(counter);
890
+ input.addEventListener("blur", handleChange);
891
+ input.addEventListener("input", handleChange);
761
892
  }
762
- itemWrapper.appendChild(inputContainer);
763
- if (index === -1) {
764
- container.appendChild(itemWrapper);
765
- } else {
766
- container.insertBefore(itemWrapper, container.children[index]);
893
+ if (!readonly) {
894
+ const rem = document.createElement("button");
895
+ rem.type = "button";
896
+ rem.className = "fb-chip-remove";
897
+ rem.setAttribute("aria-label", t("removeElement", state));
898
+ rem.innerHTML = BIN_ICON_SVG;
899
+ rem.onclick = () => {
900
+ const chips = list.querySelectorAll(".fb-chip");
901
+ const idx = Array.prototype.indexOf.call(chips, chip);
902
+ if (idx < 0) return;
903
+ if (chips.length <= minCount) return;
904
+ values.splice(idx, 1);
905
+ const trailingError = chip.nextElementSibling;
906
+ if (trailingError && trailingError.classList.contains("error-message")) {
907
+ trailingError.remove();
908
+ }
909
+ chip.remove();
910
+ updateIndices();
911
+ updateAddButton();
912
+ updateRemoveButtons();
913
+ };
914
+ chip.appendChild(rem);
767
915
  }
916
+ list.appendChild(chip);
768
917
  updateIndices();
769
- return itemWrapper;
918
+ return chip;
770
919
  }
771
920
  function updateRemoveButtons() {
772
921
  if (readonly) return;
773
- const items = container.querySelectorAll(".multiple-text-item");
774
- const currentCount = items.length;
775
- items.forEach((item) => {
776
- let removeBtn = item.querySelector(
777
- ".remove-item-btn"
778
- );
779
- if (!removeBtn) {
780
- removeBtn = document.createElement("button");
781
- removeBtn.type = "button";
782
- removeBtn.className = "remove-item-btn px-2 py-1 rounded";
783
- removeBtn.style.cssText = `
784
- color: var(--fb-error-color);
785
- background-color: transparent;
786
- transition: background-color var(--fb-transition-duration);
787
- `;
788
- removeBtn.innerHTML = "\u2715";
789
- removeBtn.addEventListener("mouseenter", () => {
790
- removeBtn.style.backgroundColor = "var(--fb-background-hover-color)";
791
- });
792
- removeBtn.addEventListener("mouseleave", () => {
793
- removeBtn.style.backgroundColor = "transparent";
794
- });
795
- removeBtn.onclick = () => {
796
- const currentIndex = Array.from(container.children).indexOf(
797
- item
798
- );
799
- if (container.children.length > minCount) {
800
- values.splice(currentIndex, 1);
801
- item.remove();
802
- updateIndices();
803
- updateAddButton();
804
- updateRemoveButtons();
805
- }
806
- };
807
- item.appendChild(removeBtn);
808
- }
809
- const disabled = currentCount <= minCount;
810
- removeBtn.disabled = disabled;
811
- removeBtn.style.opacity = disabled ? "0.5" : "1";
812
- removeBtn.style.pointerEvents = disabled ? "none" : "auto";
922
+ const chipCount = list.querySelectorAll(".fb-chip").length;
923
+ const disabled = chipCount <= minCount;
924
+ list.querySelectorAll(".fb-chip-remove").forEach((btn) => {
925
+ btn.disabled = disabled;
813
926
  });
814
927
  }
815
928
  let addUpdate = null;
@@ -818,7 +931,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
818
931
  "text",
819
932
  () => {
820
933
  values.push(element.default || "");
821
- addTextItem(element.default || "");
934
+ addChip(element.default || "");
822
935
  updateAddButton();
823
936
  updateRemoveButtons();
824
937
  },
@@ -831,7 +944,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
831
944
  function updateAddButton() {
832
945
  if (addUpdate) addUpdate(values.length, maxCount);
833
946
  }
834
- values.forEach((value) => addTextItem(value));
947
+ values.forEach((value) => addChip(value));
835
948
  updateAddButton();
836
949
  updateRemoveButtons();
837
950
  }
@@ -840,7 +953,7 @@ function validateTextElement(element, key, context) {
840
953
  const errors = [];
841
954
  const { scopeRoot, skipValidation } = context;
842
955
  const markValidity = (input, errorMessage) => {
843
- var _a2, _b2;
956
+ var _a2, _b2, _c2;
844
957
  if (!input) return;
845
958
  const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
846
959
  let errorElement = document.getElementById(errorId);
@@ -856,10 +969,12 @@ function validateTextElement(element, key, context) {
856
969
  font-size: var(--fb-font-size-small);
857
970
  margin-top: 0.25rem;
858
971
  `;
859
- if (input.nextSibling) {
860
- (_a2 = input.parentNode) == null ? void 0 : _a2.insertBefore(errorElement, input.nextSibling);
972
+ const chipAncestor = (_a2 = input.closest) == null ? void 0 : _a2.call(input, ".fb-chip");
973
+ const anchor = chipAncestor || input;
974
+ if (anchor.nextSibling) {
975
+ (_b2 = anchor.parentNode) == null ? void 0 : _b2.insertBefore(errorElement, anchor.nextSibling);
861
976
  } else {
862
- (_b2 = input.parentNode) == null ? void 0 : _b2.appendChild(errorElement);
977
+ (_c2 = anchor.parentNode) == null ? void 0 : _c2.appendChild(errorElement);
863
978
  }
864
979
  }
865
980
  errorElement.textContent = errorMessage;
@@ -908,7 +1023,7 @@ function validateTextElement(element, key, context) {
908
1023
  }
909
1024
  };
910
1025
  if (element.multiple) {
911
- const inputs = scopeRoot.querySelectorAll(`[name^="${key}["]`);
1026
+ const inputs = scopeRoot.querySelectorAll(`[name^="${key}\\["]`);
912
1027
  const values = [];
913
1028
  const rawValues = [];
914
1029
  inputs.forEach((input, index) => {
@@ -958,12 +1073,14 @@ function updateTextField(element, fieldPath, value, context) {
958
1073
  );
959
1074
  return;
960
1075
  }
961
- const inputs = scopeRoot.querySelectorAll(`[name^="${fieldPath}["]`);
1076
+ const inputs = scopeRoot.querySelectorAll(`[name^="${fieldPath}\\["]`);
962
1077
  inputs.forEach((input, index) => {
963
1078
  if (index < value.length) {
964
1079
  input.value = value[index] != null ? String(value[index]) : "";
965
1080
  input.classList.remove("invalid");
966
1081
  input.title = "";
1082
+ clearFieldError(input);
1083
+ input.dispatchEvent(new Event("input", { bubbles: true }));
967
1084
  }
968
1085
  });
969
1086
  if (value.length !== inputs.length) {
@@ -977,26 +1094,15 @@ function updateTextField(element, fieldPath, value, context) {
977
1094
  input.value = value != null ? String(value) : "";
978
1095
  input.classList.remove("invalid");
979
1096
  input.title = "";
1097
+ clearFieldError(input);
1098
+ if (input instanceof HTMLTextAreaElement) {
1099
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1100
+ }
980
1101
  }
981
1102
  }
982
1103
  }
983
1104
 
984
1105
  // src/components/textarea.ts
985
- function applyAutoExpand(textarea) {
986
- textarea.style.overflow = "hidden";
987
- textarea.style.resize = "none";
988
- const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
989
- textarea.rows = Math.max(1, lineCount);
990
- const resize = () => {
991
- if (!textarea.isConnected) return;
992
- textarea.style.height = "0";
993
- textarea.style.height = `${textarea.scrollHeight}px`;
994
- };
995
- textarea.addEventListener("input", resize);
996
- setTimeout(() => {
997
- if (textarea.isConnected) resize();
998
- }, 0);
999
- }
1000
1106
  function renderTextareaElement(element, ctx, wrapper, pathKey) {
1001
1107
  const state = ctx.state;
1002
1108
  const readonly = isElementReadonly(element, state, ctx);
@@ -1027,7 +1133,7 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
1027
1133
  }
1028
1134
  textareaWrapper.appendChild(textareaInput);
1029
1135
  if (!readonly && (element.minLength != null || element.maxLength != null)) {
1030
- const counter = createCharCounter(element, textareaInput, true);
1136
+ const counter = createCharCounter(element, textareaInput);
1031
1137
  textareaWrapper.appendChild(counter);
1032
1138
  }
1033
1139
  wrapper.appendChild(textareaWrapper);
@@ -1084,7 +1190,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1084
1190
  }
1085
1191
  textareaContainer.appendChild(textareaInput);
1086
1192
  if (!readonly && (element.minLength != null || element.maxLength != null)) {
1087
- const counter = createCharCounter(element, textareaInput, true);
1193
+ const counter = createCharCounter(element, textareaInput);
1088
1194
  textareaContainer.appendChild(counter);
1089
1195
  }
1090
1196
  itemWrapper.appendChild(textareaContainer);
@@ -1178,6 +1284,91 @@ function updateTextareaField(element, fieldPath, value, context) {
1178
1284
  }
1179
1285
 
1180
1286
  // src/components/number.ts
1287
+ function ensureStepperStyles(doc) {
1288
+ const ID = "fb-number-stepper-styles";
1289
+ if (doc.getElementById(ID)) return;
1290
+ const style = doc.createElement("style");
1291
+ style.id = ID;
1292
+ style.textContent = `
1293
+ .fb-stepper-input::-webkit-outer-spin-button,
1294
+ .fb-stepper-input::-webkit-inner-spin-button {
1295
+ -webkit-appearance: none;
1296
+ margin: 0;
1297
+ }
1298
+ .fb-stepper-input { -moz-appearance: textfield; }
1299
+ `;
1300
+ doc.head.appendChild(style);
1301
+ }
1302
+ function buildStepper(input, element, readonly) {
1303
+ var _a;
1304
+ ensureStepperStyles(input.ownerDocument);
1305
+ const step = (_a = element.step) != null ? _a : 1;
1306
+ const min = element.min;
1307
+ const max = element.max;
1308
+ const wrap = document.createElement("div");
1309
+ wrap.className = "fb-stepper";
1310
+ wrap.style.cssText = `
1311
+ display: inline-flex;
1312
+ align-items: stretch;
1313
+ border: var(--fb-border-width) solid var(--fb-border-color);
1314
+ border-radius: var(--fb-border-radius);
1315
+ overflow: hidden;
1316
+ background: var(--fb-background-color);
1317
+ `;
1318
+ const makeBtn = (label, delta) => {
1319
+ const b = document.createElement("button");
1320
+ b.type = "button";
1321
+ b.textContent = label;
1322
+ b.tabIndex = -1;
1323
+ b.style.cssText = `
1324
+ width: 32px;
1325
+ border: none;
1326
+ background: transparent;
1327
+ color: var(--fb-text-color);
1328
+ font-size: var(--fb-font-size);
1329
+ font-family: var(--fb-font-family);
1330
+ cursor: ${readonly ? "default" : "pointer"};
1331
+ user-select: none;
1332
+ `;
1333
+ if (readonly) {
1334
+ b.disabled = true;
1335
+ b.style.opacity = "0.5";
1336
+ } else {
1337
+ b.addEventListener("click", (e) => {
1338
+ var _a2;
1339
+ e.preventDefault();
1340
+ const current = parseFloat(input.value);
1341
+ const base = Number.isFinite(current) ? current : (_a2 = min != null ? min : element.default) != null ? _a2 : 0;
1342
+ let next = parseFloat((base + delta * step).toPrecision(12));
1343
+ if (min != null) next = Math.max(min, next);
1344
+ if (max != null) next = Math.min(max, next);
1345
+ input.value = String(next);
1346
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1347
+ input.dispatchEvent(new Event("change", { bubbles: true }));
1348
+ });
1349
+ }
1350
+ return b;
1351
+ };
1352
+ input.classList.add("fb-stepper-input");
1353
+ input.style.cssText = `
1354
+ width: 56px;
1355
+ border: none;
1356
+ border-left: var(--fb-border-width) solid var(--fb-border-color);
1357
+ border-right: var(--fb-border-width) solid var(--fb-border-color);
1358
+ padding: var(--fb-input-padding-y) 0;
1359
+ font-size: var(--fb-font-size);
1360
+ font-family: var(--fb-font-family);
1361
+ text-align: center;
1362
+ background: transparent;
1363
+ color: var(--fb-text-color);
1364
+ -moz-appearance: textfield;
1365
+ box-sizing: border-box;
1366
+ `;
1367
+ wrap.appendChild(makeBtn("\u2212", -1));
1368
+ wrap.appendChild(input);
1369
+ wrap.appendChild(makeBtn("+", 1));
1370
+ return wrap;
1371
+ }
1181
1372
  function createNumberRangeHint(element, input) {
1182
1373
  const hint = document.createElement("span");
1183
1374
  hint.className = "number-range-hint";
@@ -1224,14 +1415,6 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1224
1415
  inputWrapper.style.cssText = "position: relative;";
1225
1416
  const numberInput = document.createElement("input");
1226
1417
  numberInput.type = "number";
1227
- numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1228
- numberInput.style.cssText = `
1229
- padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
1230
- font-size: var(--fb-font-size);
1231
- font-family: var(--fb-font-family);
1232
- width: 100%;
1233
- box-sizing: border-box;
1234
- `;
1235
1418
  numberInput.name = pathKey;
1236
1419
  numberInput.placeholder = element.placeholder || "0";
1237
1420
  if (element.min !== void 0) numberInput.min = element.min.toString();
@@ -1239,6 +1422,16 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1239
1422
  if (element.step !== void 0) numberInput.step = element.step.toString();
1240
1423
  numberInput.value = ctx.prefill[element.key] || element.default || "";
1241
1424
  numberInput.readOnly = readonly;
1425
+ if (!element.stepper) {
1426
+ numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1427
+ numberInput.style.cssText = `
1428
+ padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
1429
+ font-size: var(--fb-font-size);
1430
+ font-family: var(--fb-font-family);
1431
+ width: 100%;
1432
+ box-sizing: border-box;
1433
+ `;
1434
+ }
1242
1435
  if (!readonly && ctx.instance) {
1243
1436
  const handleChange = () => {
1244
1437
  const value = numberInput.value ? parseFloat(numberInput.value) : null;
@@ -1247,10 +1440,14 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1247
1440
  numberInput.addEventListener("blur", handleChange);
1248
1441
  numberInput.addEventListener("input", handleChange);
1249
1442
  }
1250
- inputWrapper.appendChild(numberInput);
1251
- if (!readonly && (element.min != null || element.max != null)) {
1252
- const counter = createNumberRangeHint(element, numberInput);
1253
- inputWrapper.appendChild(counter);
1443
+ if (element.stepper) {
1444
+ inputWrapper.appendChild(buildStepper(numberInput, element, readonly));
1445
+ } else {
1446
+ inputWrapper.appendChild(numberInput);
1447
+ if (!readonly && (element.min != null || element.max != null)) {
1448
+ const counter = createNumberRangeHint(element, numberInput);
1449
+ inputWrapper.appendChild(counter);
1450
+ }
1254
1451
  }
1255
1452
  wrapper.appendChild(inputWrapper);
1256
1453
  }
@@ -1508,13 +1705,14 @@ function updateNumberField(element, fieldPath, value, context) {
1508
1705
  return;
1509
1706
  }
1510
1707
  const inputs = scopeRoot.querySelectorAll(
1511
- `[name^="${fieldPath}["]`
1708
+ `[name^="${fieldPath}\\["]`
1512
1709
  );
1513
1710
  inputs.forEach((input, index) => {
1514
1711
  if (index < value.length) {
1515
1712
  input.value = value[index] != null ? String(value[index]) : "";
1516
1713
  input.classList.remove("invalid");
1517
1714
  input.title = "";
1715
+ clearFieldError(input);
1518
1716
  }
1519
1717
  });
1520
1718
  if (value.length !== inputs.length) {
@@ -1530,6 +1728,7 @@ function updateNumberField(element, fieldPath, value, context) {
1530
1728
  input.value = value != null ? String(value) : "";
1531
1729
  input.classList.remove("invalid");
1532
1730
  input.title = "";
1731
+ clearFieldError(input);
1533
1732
  }
1534
1733
  }
1535
1734
  }
@@ -1746,7 +1945,7 @@ function validateSelectElement(element, key, context) {
1746
1945
  };
1747
1946
  if ("multiple" in element && element.multiple) {
1748
1947
  const inputs = scopeRoot.querySelectorAll(
1749
- `[name^="${key}["]`
1948
+ `[name^="${key}\\["]`
1750
1949
  );
1751
1950
  const values = [];
1752
1951
  inputs.forEach((input) => {
@@ -1783,7 +1982,7 @@ function updateSelectField(element, fieldPath, value, context) {
1783
1982
  return;
1784
1983
  }
1785
1984
  const selects = scopeRoot.querySelectorAll(
1786
- `[name^="${fieldPath}["]`
1985
+ `[name^="${fieldPath}\\["]`
1787
1986
  );
1788
1987
  selects.forEach((select, index) => {
1789
1988
  if (index < value.length) {
@@ -1794,6 +1993,7 @@ function updateSelectField(element, fieldPath, value, context) {
1794
1993
  });
1795
1994
  select.classList.remove("invalid");
1796
1995
  select.title = "";
1996
+ clearFieldError(select);
1797
1997
  }
1798
1998
  });
1799
1999
  if (value.length !== selects.length) {
@@ -1813,72 +2013,147 @@ function updateSelectField(element, fieldPath, value, context) {
1813
2013
  });
1814
2014
  select.classList.remove("invalid");
1815
2015
  select.title = "";
2016
+ clearFieldError(select);
1816
2017
  }
1817
2018
  }
1818
2019
  }
1819
2020
 
1820
2021
  // src/components/switcher.ts
1821
- function applySelectedStyle(btn) {
1822
- btn.style.backgroundColor = "var(--fb-primary-color)";
1823
- btn.style.color = "#ffffff";
1824
- btn.style.borderColor = "var(--fb-primary-color)";
2022
+ function applySelectedStyle(btn, isPreset) {
2023
+ if (isPreset) {
2024
+ btn.style.backgroundColor = "var(--fb-primary-soft-color)";
2025
+ btn.style.color = "var(--fb-primary-color)";
2026
+ btn.style.borderColor = "var(--fb-primary-color)";
2027
+ } else {
2028
+ btn.style.backgroundColor = "var(--fb-primary-color)";
2029
+ btn.style.color = "#ffffff";
2030
+ btn.style.borderColor = "var(--fb-primary-color)";
2031
+ }
1825
2032
  }
1826
- function applyUnselectedStyle(btn) {
1827
- btn.style.backgroundColor = "transparent";
2033
+ function applyUnselectedStyle(btn, isPreset) {
2034
+ btn.style.backgroundColor = isPreset ? "var(--fb-background-color)" : "transparent";
1828
2035
  btn.style.color = "var(--fb-text-color)";
1829
2036
  btn.style.borderColor = "var(--fb-border-color)";
1830
2037
  }
2038
+ function isPresetButton(btn) {
2039
+ return btn.classList.contains("fb-switcher-preset");
2040
+ }
2041
+ function buildPresetCard(option, readonly) {
2042
+ const btn = document.createElement("button");
2043
+ btn.type = "button";
2044
+ btn.className = "fb-switcher-btn fb-switcher-preset";
2045
+ btn.dataset.value = option.value;
2046
+ btn.style.cssText = `
2047
+ display: inline-flex;
2048
+ align-items: center;
2049
+ gap: 8px;
2050
+ padding: 7px 12px 7px 10px;
2051
+ border-width: var(--fb-border-width);
2052
+ border-style: solid;
2053
+ border-radius: 999px;
2054
+ background: var(--fb-background-color);
2055
+ font-size: var(--fb-font-size);
2056
+ font-family: var(--fb-font-family);
2057
+ line-height: 1.25;
2058
+ cursor: ${readonly ? "default" : "pointer"};
2059
+ transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
2060
+ outline: none;
2061
+ `;
2062
+ if (option.iconUrl) {
2063
+ const icon = document.createElement("img");
2064
+ icon.className = "fb-switcher-icon";
2065
+ icon.src = option.iconUrl;
2066
+ icon.alt = "";
2067
+ icon.setAttribute("aria-hidden", "true");
2068
+ icon.style.cssText = `
2069
+ display: block;
2070
+ flex: 0 0 auto;
2071
+ width: 20px;
2072
+ height: 20px;
2073
+ object-fit: contain;
2074
+ `;
2075
+ btn.appendChild(icon);
2076
+ }
2077
+ const name = document.createElement("span");
2078
+ name.className = "fb-switcher-name";
2079
+ name.textContent = option.label;
2080
+ name.style.cssText = "font-weight: 600;";
2081
+ btn.appendChild(name);
2082
+ if (option.subtitle) {
2083
+ const sub = document.createElement("span");
2084
+ sub.className = "fb-switcher-subtitle";
2085
+ sub.textContent = option.subtitle;
2086
+ sub.style.cssText = `
2087
+ font-size: var(--fb-font-size-small);
2088
+ opacity: 0.7;
2089
+ font-variant-numeric: tabular-nums;
2090
+ `;
2091
+ btn.appendChild(sub);
2092
+ }
2093
+ return btn;
2094
+ }
1831
2095
  function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onChange) {
1832
2096
  const options = element.options || [];
2097
+ const isPresetMode = options.some((o) => o.subtitle || o.iconUrl);
1833
2098
  const group = document.createElement("div");
1834
2099
  group.className = "fb-switcher-group";
1835
- group.style.cssText = `
1836
- display: inline-flex;
1837
- flex-direction: row;
1838
- flex-wrap: nowrap;
1839
- `;
2100
+ group.style.cssText = isPresetMode ? `
2101
+ display: flex;
2102
+ flex-direction: row;
2103
+ flex-wrap: wrap;
2104
+ gap: 6px;
2105
+ ` : `
2106
+ display: inline-flex;
2107
+ flex-direction: row;
2108
+ flex-wrap: nowrap;
2109
+ `;
1840
2110
  const buttons = [];
1841
2111
  options.forEach((option, index) => {
1842
- const btn = document.createElement("button");
1843
- btn.type = "button";
1844
- btn.className = "fb-switcher-btn";
1845
- btn.dataset.value = option.value;
1846
- btn.textContent = option.label;
1847
- btn.style.cssText = `
1848
- padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
1849
- font-size: var(--fb-font-size);
1850
- border-width: var(--fb-border-width);
1851
- border-style: solid;
1852
- cursor: ${readonly ? "default" : "pointer"};
1853
- transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
1854
- white-space: nowrap;
1855
- line-height: 1.25;
1856
- outline: none;
1857
- `;
1858
- if (options.length === 1) {
1859
- btn.style.borderRadius = "var(--fb-border-radius)";
1860
- } else if (index === 0) {
1861
- btn.style.borderRadius = "var(--fb-border-radius) 0 0 var(--fb-border-radius)";
1862
- btn.style.borderRightWidth = "0";
1863
- } else if (index === options.length - 1) {
1864
- btn.style.borderRadius = "0 var(--fb-border-radius) var(--fb-border-radius) 0";
2112
+ let btn;
2113
+ if (isPresetMode) {
2114
+ btn = buildPresetCard(option, readonly);
1865
2115
  } else {
1866
- btn.style.borderRadius = "0";
1867
- btn.style.borderRightWidth = "0";
2116
+ btn = document.createElement("button");
2117
+ btn.type = "button";
2118
+ btn.className = "fb-switcher-btn";
2119
+ btn.dataset.value = option.value;
2120
+ btn.textContent = option.label;
2121
+ btn.style.cssText = `
2122
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
2123
+ font-size: var(--fb-font-size);
2124
+ border-width: var(--fb-border-width);
2125
+ border-style: solid;
2126
+ cursor: ${readonly ? "default" : "pointer"};
2127
+ transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
2128
+ white-space: nowrap;
2129
+ line-height: 1.25;
2130
+ outline: none;
2131
+ `;
2132
+ if (options.length === 1) {
2133
+ btn.style.borderRadius = "var(--fb-border-radius)";
2134
+ } else if (index === 0) {
2135
+ btn.style.borderRadius = "var(--fb-border-radius) 0 0 var(--fb-border-radius)";
2136
+ btn.style.borderRightWidth = "0";
2137
+ } else if (index === options.length - 1) {
2138
+ btn.style.borderRadius = "0 var(--fb-border-radius) var(--fb-border-radius) 0";
2139
+ } else {
2140
+ btn.style.borderRadius = "0";
2141
+ btn.style.borderRightWidth = "0";
2142
+ }
1868
2143
  }
1869
2144
  if (option.value === currentValue) {
1870
- applySelectedStyle(btn);
2145
+ applySelectedStyle(btn, isPresetMode);
1871
2146
  } else {
1872
- applyUnselectedStyle(btn);
2147
+ applyUnselectedStyle(btn, isPresetMode);
1873
2148
  }
1874
2149
  if (!readonly) {
1875
2150
  btn.addEventListener("click", () => {
1876
2151
  hiddenInput.value = option.value;
1877
2152
  buttons.forEach((b) => {
1878
2153
  if (b.dataset.value === option.value) {
1879
- applySelectedStyle(b);
2154
+ applySelectedStyle(b, isPresetMode);
1880
2155
  } else {
1881
- applyUnselectedStyle(b);
2156
+ applyUnselectedStyle(b, isPresetMode);
1882
2157
  }
1883
2158
  });
1884
2159
  if (onChange) {
@@ -1892,7 +2167,7 @@ function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onCha
1892
2167
  });
1893
2168
  btn.addEventListener("mouseleave", () => {
1894
2169
  if (hiddenInput.value !== option.value) {
1895
- btn.style.backgroundColor = "transparent";
2170
+ btn.style.backgroundColor = isPresetMode ? "var(--fb-background-color)" : "transparent";
1896
2171
  }
1897
2172
  });
1898
2173
  }
@@ -2117,7 +2392,7 @@ function validateSwitcherElement(element, key, context) {
2117
2392
  );
2118
2393
  if ("multiple" in element && element.multiple) {
2119
2394
  const inputs = scopeRoot.querySelectorAll(
2120
- `input[type="hidden"][name^="${key}["]`
2395
+ `input[type="hidden"][name^="${key}\\["]`
2121
2396
  );
2122
2397
  const values = [];
2123
2398
  inputs.forEach((input) => {
@@ -2166,7 +2441,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
2166
2441
  return;
2167
2442
  }
2168
2443
  const inputs = scopeRoot.querySelectorAll(
2169
- `input[type="hidden"][name^="${fieldPath}["]`
2444
+ `input[type="hidden"][name^="${fieldPath}\\["]`
2170
2445
  );
2171
2446
  inputs.forEach((input, index) => {
2172
2447
  var _a2;
@@ -2176,15 +2451,17 @@ function updateSwitcherField(element, fieldPath, value, context) {
2176
2451
  const group = (_a2 = input.parentElement) == null ? void 0 : _a2.querySelector(".fb-switcher-group");
2177
2452
  if (group) {
2178
2453
  group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
2454
+ const isPreset = isPresetButton(btn);
2179
2455
  if (btn.dataset.value === newVal) {
2180
- applySelectedStyle(btn);
2456
+ applySelectedStyle(btn, isPreset);
2181
2457
  } else {
2182
- applyUnselectedStyle(btn);
2458
+ applyUnselectedStyle(btn, isPreset);
2183
2459
  }
2184
2460
  });
2185
2461
  }
2186
2462
  input.classList.remove("invalid");
2187
2463
  input.title = "";
2464
+ clearFieldError(input);
2188
2465
  }
2189
2466
  });
2190
2467
  if (value.length !== inputs.length) {
@@ -2202,16 +2479,212 @@ function updateSwitcherField(element, fieldPath, value, context) {
2202
2479
  const group = (_a = input.parentElement) == null ? void 0 : _a.querySelector(".fb-switcher-group");
2203
2480
  if (group) {
2204
2481
  group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
2482
+ const isPreset = isPresetButton(btn);
2205
2483
  if (btn.dataset.value === newVal) {
2206
- applySelectedStyle(btn);
2484
+ applySelectedStyle(btn, isPreset);
2207
2485
  } else {
2208
- applyUnselectedStyle(btn);
2486
+ applyUnselectedStyle(btn, isPreset);
2209
2487
  }
2210
2488
  });
2211
2489
  }
2212
2490
  input.classList.remove("invalid");
2213
2491
  input.title = "";
2492
+ clearFieldError(input);
2493
+ }
2494
+ }
2495
+ }
2496
+
2497
+ // src/components/boolean.ts
2498
+ var TOGGLE_W = 36;
2499
+ var TOGGLE_H = 20;
2500
+ var KNOB = 16;
2501
+ function ensureStyles(doc) {
2502
+ const ID = "fb-boolean-styles";
2503
+ if (doc.getElementById(ID)) return;
2504
+ const style = doc.createElement("style");
2505
+ style.id = ID;
2506
+ style.textContent = `
2507
+ .fb-toggle {
2508
+ position: relative;
2509
+ display: inline-block;
2510
+ width: ${TOGGLE_W}px;
2511
+ height: ${TOGGLE_H}px;
2512
+ border-radius: ${TOGGLE_H}px;
2513
+ background: var(--fb-border-color);
2514
+ transition: background-color var(--fb-transition-duration);
2515
+ flex-shrink: 0;
2516
+ }
2517
+ .fb-toggle::after {
2518
+ content: "";
2519
+ position: absolute;
2520
+ top: ${(TOGGLE_H - KNOB) / 2}px;
2521
+ left: ${(TOGGLE_H - KNOB) / 2}px;
2522
+ width: ${KNOB}px;
2523
+ height: ${KNOB}px;
2524
+ border-radius: 50%;
2525
+ background: #ffffff;
2526
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
2527
+ transition: transform var(--fb-transition-duration);
2528
+ }
2529
+ .fb-toggle.fb-on {
2530
+ background: var(--fb-primary-color);
2531
+ }
2532
+ .fb-toggle.fb-on::after {
2533
+ transform: translateX(${TOGGLE_W - KNOB - (TOGGLE_H - KNOB)}px);
2534
+ }
2535
+ .fb-toggle-row {
2536
+ display: flex;
2537
+ align-items: center;
2538
+ gap: 12px;
2539
+ padding: 12px 14px;
2540
+ background: var(--fb-surface-soft-color);
2541
+ border: var(--fb-border-width) solid var(--fb-border-color);
2542
+ border-radius: var(--fb-border-radius);
2543
+ cursor: pointer;
2544
+ user-select: none;
2545
+ }
2546
+ .fb-toggle-row[aria-disabled="true"] {
2547
+ cursor: default;
2548
+ opacity: 0.7;
2549
+ }
2550
+ .fb-toggle-row:focus-visible {
2551
+ outline: var(--fb-focus-ring-width) solid var(--fb-focus-ring-color);
2552
+ outline-offset: var(--fb-focus-ring-offset);
2553
+ }
2554
+ .fb-toggle-text { flex: 1; min-width: 0; }
2555
+ .fb-toggle-title {
2556
+ display: flex;
2557
+ align-items: center;
2558
+ gap: 4px;
2559
+ font-size: var(--fb-font-size);
2560
+ font-weight: 500;
2561
+ color: var(--fb-text-color);
2562
+ line-height: 1.3;
2214
2563
  }
2564
+ .fb-toggle-subtitle {
2565
+ font-size: var(--fb-font-size-small);
2566
+ color: var(--fb-text-secondary-color);
2567
+ margin-top: 2px;
2568
+ line-height: 1.35;
2569
+ }
2570
+ .fb-toggle-info {
2571
+ flex: 0 0 14px;
2572
+ display: inline-flex;
2573
+ align-items: center;
2574
+ justify-content: center;
2575
+ width: 14px;
2576
+ height: 14px;
2577
+ border-radius: 50%;
2578
+ background: var(--fb-border-color);
2579
+ color: #fff;
2580
+ font-size: 10px;
2581
+ font-weight: 700;
2582
+ font-style: italic;
2583
+ font-family: serif;
2584
+ cursor: help;
2585
+ }
2586
+ `;
2587
+ doc.head.appendChild(style);
2588
+ }
2589
+ function parseBool(v) {
2590
+ if (typeof v === "boolean") return v;
2591
+ if (typeof v === "string") return v === "true" || v === "on" || v === "1";
2592
+ return false;
2593
+ }
2594
+ function renderBooleanElement(element, ctx, wrapper, pathKey) {
2595
+ var _a;
2596
+ ensureStyles(document);
2597
+ const state = ctx.state;
2598
+ const readonly = isElementReadonly(element, state, ctx);
2599
+ const prefillRaw = ctx.prefill[element.key];
2600
+ const initial = prefillRaw !== void 0 ? parseBool(prefillRaw) : parseBool(element.default);
2601
+ const hiddenInput = document.createElement("input");
2602
+ hiddenInput.type = "hidden";
2603
+ hiddenInput.name = pathKey;
2604
+ hiddenInput.value = initial ? "true" : "false";
2605
+ const row = document.createElement("div");
2606
+ row.className = "fb-toggle-row";
2607
+ row.setAttribute("role", "switch");
2608
+ row.setAttribute("aria-checked", initial ? "true" : "false");
2609
+ if (readonly) {
2610
+ row.setAttribute("aria-disabled", "true");
2611
+ } else {
2612
+ row.tabIndex = 0;
2613
+ }
2614
+ const pill = document.createElement("span");
2615
+ pill.className = "fb-toggle" + (initial ? " fb-on" : "");
2616
+ pill.setAttribute("aria-hidden", "true");
2617
+ row.appendChild(pill);
2618
+ const textBlock = document.createElement("div");
2619
+ textBlock.className = "fb-toggle-text";
2620
+ const titleEl = document.createElement("div");
2621
+ titleEl.className = "fb-toggle-title";
2622
+ titleEl.appendChild(document.createTextNode((_a = element.label) != null ? _a : ""));
2623
+ if (element.description) {
2624
+ const info = document.createElement("span");
2625
+ info.className = "fb-toggle-info";
2626
+ info.textContent = "i";
2627
+ info.title = element.description;
2628
+ titleEl.appendChild(info);
2629
+ }
2630
+ textBlock.appendChild(titleEl);
2631
+ if (element.hint) {
2632
+ const subtitle = document.createElement("div");
2633
+ subtitle.className = "fb-toggle-subtitle";
2634
+ subtitle.textContent = element.hint;
2635
+ textBlock.appendChild(subtitle);
2636
+ }
2637
+ row.appendChild(textBlock);
2638
+ if (!readonly) {
2639
+ const toggle = () => {
2640
+ const next = hiddenInput.value !== "true";
2641
+ hiddenInput.value = next ? "true" : "false";
2642
+ pill.classList.toggle("fb-on", next);
2643
+ row.setAttribute("aria-checked", next ? "true" : "false");
2644
+ if (ctx.instance) ctx.instance.triggerOnChange(pathKey, next);
2645
+ };
2646
+ row.addEventListener("click", (e) => {
2647
+ var _a2;
2648
+ if ((_a2 = e.target) == null ? void 0 : _a2.classList.contains("fb-toggle-info")) {
2649
+ return;
2650
+ }
2651
+ toggle();
2652
+ });
2653
+ row.addEventListener("keydown", (e) => {
2654
+ if (e.key === " " || e.key === "Enter") {
2655
+ e.preventDefault();
2656
+ toggle();
2657
+ }
2658
+ });
2659
+ }
2660
+ wrapper.appendChild(hiddenInput);
2661
+ wrapper.appendChild(row);
2662
+ }
2663
+ function validateBooleanElement(element, key, context) {
2664
+ var _a;
2665
+ const { scopeRoot } = context;
2666
+ const input = scopeRoot.querySelector(
2667
+ `input[type="hidden"][name="${key}"]`
2668
+ );
2669
+ const raw = (_a = input == null ? void 0 : input.value) != null ? _a : "";
2670
+ const value = parseBool(raw);
2671
+ const errors = [];
2672
+ return { value, errors };
2673
+ }
2674
+ function updateBooleanField(_element, fieldPath, value, context) {
2675
+ var _a;
2676
+ const { scopeRoot } = context;
2677
+ const input = scopeRoot.querySelector(
2678
+ `input[type="hidden"][name="${fieldPath}"]`
2679
+ );
2680
+ if (!input) return;
2681
+ const bool = parseBool(value);
2682
+ input.value = bool ? "true" : "false";
2683
+ const row = (_a = input.parentElement) == null ? void 0 : _a.querySelector(".fb-toggle-row");
2684
+ if (row) {
2685
+ row.setAttribute("aria-checked", bool ? "true" : "false");
2686
+ const pill = row.querySelector(".fb-toggle");
2687
+ if (pill) pill.classList.toggle("fb-on", bool);
2215
2688
  }
2216
2689
  }
2217
2690
 
@@ -2317,14 +2790,20 @@ function ensureFileStyles() {
2317
2790
  }
2318
2791
 
2319
2792
  /* \u2500\u2500\u2500 Wide single-file add tile (empty state) \u2500\u2500\u2500 */
2793
+ /* Flex-wraps: side-by-side when wide enough, stacks upload/library
2794
+ vertically when narrow (e.g. inside a 50/50 container column). */
2320
2795
  .fb-wide-tile {
2321
2796
  width: 100%;
2797
+ box-sizing: border-box;
2322
2798
  border-radius: 0.75rem;
2323
2799
  border: 1px dashed #60a5fa;
2324
2800
  background: rgba(239,246,255,0.5);
2325
2801
  display: flex;
2802
+ flex-wrap: wrap;
2803
+ align-items: stretch;
2804
+ gap: 0;
2326
2805
  overflow: hidden;
2327
- height: 180px;
2806
+ min-height: 180px;
2328
2807
  transition: border-color 150ms, background 150ms, box-shadow 150ms;
2329
2808
  cursor: pointer;
2330
2809
  }
@@ -2338,9 +2817,12 @@ function ensureFileStyles() {
2338
2817
  box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
2339
2818
  }
2340
2819
 
2341
- /* Upload zone inside wide tile */
2820
+ /* Upload zone inside wide tile.
2821
+ flex: 1 1 220px \u2014 wants at least 220px; if the container can't fit
2822
+ upload + library on one row (~220 + 176), library wraps below. */
2342
2823
  .fb-wide-tile-upload {
2343
- flex: 1;
2824
+ flex: 1 1 220px;
2825
+ min-height: 140px;
2344
2826
  display: flex;
2345
2827
  flex-direction: column;
2346
2828
  align-items: center;
@@ -2353,24 +2835,21 @@ function ensureFileStyles() {
2353
2835
  background: transparent;
2354
2836
  border: none;
2355
2837
  font-family: inherit;
2838
+ /* Dashed separator from library: right side when in a row, bottom when
2839
+ wrapped (the line then sits between the two stacked cards). */
2840
+ border-right: 1px dashed rgba(96,165,250,0.5);
2356
2841
  }
2357
2842
  .fb-wide-tile-upload:hover {
2358
2843
  background: rgba(191,219,254,0.25);
2359
2844
  }
2360
-
2361
- /* Vertical dashed divider between upload and library zones */
2362
- .fb-wide-tile-divider {
2363
- width: 1px;
2364
- margin: 16px 0;
2365
- border-left: 1px dashed rgba(96,165,250,0.5);
2366
- background: transparent;
2367
- flex-shrink: 0;
2368
- }
2369
-
2370
- /* Library zone inside wide tile */
2845
+ /* Library zone inside wide tile.
2846
+ flex: 0 0 176px \u2014 fixed 176px, never grows. Upload fills the rest in
2847
+ row layout. When the tile wraps to two rows on narrow containers,
2848
+ library stays 176px wide on its own row (left-aligned), preserving the
2849
+ visual hierarchy "upload > library" in both layouts. */
2371
2850
  .fb-wide-tile-library {
2372
- width: 176px;
2373
- flex-shrink: 0;
2851
+ flex: 0 0 176px;
2852
+ min-height: 120px;
2374
2853
  display: flex;
2375
2854
  flex-direction: column;
2376
2855
  align-items: center;
@@ -2387,6 +2866,10 @@ function ensureFileStyles() {
2387
2866
  .fb-wide-tile-library:hover {
2388
2867
  background: rgba(191,219,254,0.25);
2389
2868
  }
2869
+ /* Narrow-tile mode lives in a separate <style> tag (see below) \u2014 the
2870
+ @container rule is appended only when the runtime actually supports
2871
+ container queries, so jsdom (which doesn't) never sees it and stays
2872
+ quiet in test logs. */
2390
2873
 
2391
2874
  /* \u2500\u2500\u2500 Multi-file outer grid container \u2500\u2500\u2500 */
2392
2875
  .fb-multi-outer {
@@ -2765,6 +3248,39 @@ function ensureFileStyles() {
2765
3248
  }
2766
3249
  `;
2767
3250
  document.head.appendChild(style);
3251
+ if (typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("container-type", "inline-size")) {
3252
+ const cq = document.createElement("style");
3253
+ cq.setAttribute("data-fb-file-styles-cq", "true");
3254
+ cq.textContent = `
3255
+ .fb-wide-tile { container-type: inline-size; }
3256
+ @container (max-width: 408px) {
3257
+ .fb-wide-tile-upload {
3258
+ border-right: none;
3259
+ border-bottom: 1px dashed rgba(96,165,250,0.5);
3260
+ }
3261
+ .fb-wide-tile-library {
3262
+ flex: 1 0 100%;
3263
+ min-height: 0;
3264
+ flex-direction: row;
3265
+ gap: 6px;
3266
+ padding: 8px 12px;
3267
+ font-size: 12px;
3268
+ }
3269
+ .fb-wide-tile-library .fb-wide-tile-library-icon {
3270
+ width: 16px;
3271
+ height: 16px;
3272
+ }
3273
+ .fb-wide-tile-library .fb-wide-tile-library-label {
3274
+ font-size: 12px;
3275
+ font-weight: 500;
3276
+ }
3277
+ .fb-wide-tile-library .fb-wide-tile-library-hint {
3278
+ display: none;
3279
+ }
3280
+ }
3281
+ `;
3282
+ document.head.appendChild(cq);
3283
+ }
2768
3284
  }
2769
3285
 
2770
3286
  // src/components/file/dom.ts
@@ -3791,16 +4307,26 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
3791
4307
  }
3792
4308
  return { accepted, errorMessage: errorParts.join(" \u2022 ") };
3793
4309
  }
3794
- async function uploadBatch(accepted, resourceIds, listEl, state) {
4310
+ async function uploadBatch(opts) {
3795
4311
  var _a;
3796
- if (listEl) {
4312
+ const {
4313
+ accepted,
4314
+ listEl,
4315
+ state,
4316
+ shouldHideAddTile,
4317
+ buildSuccessTile,
4318
+ prepareForUpload
4319
+ } = opts;
4320
+ if (listEl && shouldHideAddTile) {
3797
4321
  const tilesWrap = ensureTilesWrap(listEl);
3798
4322
  const addTile = (_a = tilesWrap.querySelector(".fb-multi-add-tile-js")) != null ? _a : tilesWrap.querySelector(".fb-tile-add");
3799
4323
  if (addTile) addTile.style.display = "none";
3800
4324
  }
4325
+ prepareForUpload == null ? void 0 : prepareForUpload();
4326
+ const orderedIds = new Array(accepted.length).fill(null);
3801
4327
  const failures = [];
3802
4328
  await Promise.allSettled(
3803
- accepted.map(async (file) => {
4329
+ accepted.map(async (file, index) => {
3804
4330
  const placeholder = createUploadingTile(file.name, state);
3805
4331
  if (listEl) {
3806
4332
  const tilesWrap = ensureTilesWrap(listEl);
@@ -3813,20 +4339,24 @@ async function uploadBatch(accepted, resourceIds, listEl, state) {
3813
4339
  type: file.type,
3814
4340
  size: file.size,
3815
4341
  uploadedAt: /* @__PURE__ */ new Date(),
3816
- file: void 0
4342
+ file
3817
4343
  });
3818
- resourceIds.push(rid);
4344
+ orderedIds[index] = rid;
4345
+ if (buildSuccessTile && placeholder.parentNode) {
4346
+ placeholder.replaceWith(buildSuccessTile(rid));
4347
+ } else {
4348
+ placeholder.remove();
4349
+ }
3819
4350
  } catch (err) {
3820
4351
  const wrapped = err instanceof Error ? err : new Error(String(err));
3821
4352
  const cause = wrapped.cause;
3822
4353
  const root = cause instanceof Error ? cause : cause !== void 0 ? new Error(String(cause)) : wrapped;
3823
4354
  failures.push({ file, error: root });
3824
- } finally {
3825
4355
  placeholder.remove();
3826
4356
  }
3827
4357
  })
3828
4358
  );
3829
- return { failures };
4359
+ return { failures, orderedIds };
3830
4360
  }
3831
4361
  function buildBatchErrorMessage(filterError, failures, state) {
3832
4362
  if (failures.length === 0) return filterError;
@@ -3838,69 +4368,72 @@ function buildBatchErrorMessage(filterError, failures, state) {
3838
4368
  ).join(" \u2022 ");
3839
4369
  return filterError ? `${filterError} \u2022 ${uploadMsg}` : uploadMsg;
3840
4370
  }
3841
- function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallback, constraints, pathKey, instance) {
4371
+ async function runMultiFileBatch(opts, files, listEl, errorTarget) {
4372
+ const {
4373
+ resourceIds,
4374
+ state,
4375
+ updateCallback,
4376
+ constraints,
4377
+ pathKey,
4378
+ instance,
4379
+ buildSuccessTile,
4380
+ prepareForUpload,
4381
+ coordinator
4382
+ } = opts;
4383
+ const { accepted, errorMessage } = filterAndSlice(
4384
+ files,
4385
+ coordinator.getOccupiedCount(),
4386
+ constraints,
4387
+ state
4388
+ );
4389
+ if (errorTarget) {
4390
+ if (errorMessage) showFileError(errorTarget, errorMessage);
4391
+ else clearFileError(errorTarget);
4392
+ }
4393
+ const handle = coordinator.beginBatch(accepted.length);
4394
+ const shouldHideAddTile = coordinator.getOccupiedCount() >= constraints.maxCount;
4395
+ const { failures, orderedIds } = await uploadBatch({
4396
+ accepted,
4397
+ listEl,
4398
+ state,
4399
+ shouldHideAddTile,
4400
+ buildSuccessTile,
4401
+ prepareForUpload
4402
+ });
4403
+ handle.setResults(orderedIds);
4404
+ if (instance && pathKey && !state.config.readonly) {
4405
+ instance.triggerOnChange(pathKey, resourceIds);
4406
+ }
4407
+ const { wasLast } = handle.end();
4408
+ if (wasLast) updateCallback();
4409
+ if (errorTarget) {
4410
+ const combined = buildBatchErrorMessage(errorMessage, failures, state);
4411
+ if (combined) showFileError(errorTarget, combined);
4412
+ else clearFileError(errorTarget);
4413
+ }
4414
+ }
4415
+ function setupFilesDropHandler(opts) {
4416
+ const { filesContainer } = opts;
3842
4417
  setupDragAndDrop(filesContainer, async (files) => {
3843
4418
  var _a;
3844
- const { accepted, errorMessage } = filterAndSlice(
3845
- Array.from(files),
3846
- resourceIds.length,
3847
- constraints,
3848
- state
3849
- );
3850
- if (errorMessage) {
3851
- showFileError(filesContainer, errorMessage);
3852
- } else {
3853
- clearFileError(filesContainer);
3854
- }
3855
4419
  const list = (_a = filesContainer.querySelector(".files-list")) != null ? _a : filesContainer;
3856
- const { failures } = await uploadBatch(accepted, resourceIds, list, state);
3857
- const combined = buildBatchErrorMessage(errorMessage, failures, state);
3858
- if (combined) {
3859
- showFileError(filesContainer, combined);
3860
- } else {
3861
- clearFileError(filesContainer);
3862
- }
3863
- updateCallback();
3864
- if (instance && pathKey && !state.config.readonly) {
3865
- instance.triggerOnChange(pathKey, resourceIds);
3866
- }
4420
+ await runMultiFileBatch(opts, Array.from(files), list, filesContainer);
3867
4421
  });
3868
4422
  }
3869
- function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback, constraints, pathKey, instance) {
4423
+ function setupFilesPickerHandler(opts) {
4424
+ const { filesPicker } = opts;
3870
4425
  filesPicker.onchange = async () => {
4426
+ var _a, _b;
3871
4427
  if (!filesPicker.files) return;
3872
- const wrapperEl = filesPicker.closest("[data-files-wrapper]") || filesPicker.parentElement;
3873
- const { accepted, errorMessage } = filterAndSlice(
4428
+ const wrapperEl = (_a = filesPicker.closest("[data-files-wrapper]")) != null ? _a : filesPicker.parentElement;
4429
+ const listEl = (_b = wrapperEl == null ? void 0 : wrapperEl.querySelector(".files-list")) != null ? _b : null;
4430
+ await runMultiFileBatch(
4431
+ opts,
3874
4432
  Array.from(filesPicker.files),
3875
- resourceIds.length,
3876
- constraints,
3877
- state
3878
- );
3879
- if (errorMessage && wrapperEl) {
3880
- showFileError(wrapperEl, errorMessage);
3881
- } else if (wrapperEl) {
3882
- clearFileError(wrapperEl);
3883
- }
3884
- const listEl = wrapperEl == null ? void 0 : wrapperEl.querySelector(".files-list");
3885
- const { failures } = await uploadBatch(
3886
- accepted,
3887
- resourceIds,
3888
- listEl != null ? listEl : null,
3889
- state
4433
+ listEl,
4434
+ wrapperEl
3890
4435
  );
3891
- if (wrapperEl) {
3892
- const combined = buildBatchErrorMessage(errorMessage, failures, state);
3893
- if (combined) {
3894
- showFileError(wrapperEl, combined);
3895
- } else {
3896
- clearFileError(wrapperEl);
3897
- }
3898
- }
3899
- updateCallback();
3900
4436
  filesPicker.value = "";
3901
- if (instance && pathKey && !state.config.readonly) {
3902
- instance.triggerOnChange(pathKey, resourceIds);
3903
- }
3904
4437
  };
3905
4438
  }
3906
4439
 
@@ -3942,16 +4475,6 @@ function validatePickedResource(resource, allowedExtensions, allowedMimes, maxSi
3942
4475
  }
3943
4476
  return null;
3944
4477
  }
3945
- function readCurrentResourceIds(wrapper) {
3946
- const raw = wrapper.dataset.resourceIds;
3947
- if (!raw) return [];
3948
- try {
3949
- const parsed = JSON.parse(raw);
3950
- return Array.isArray(parsed) ? parsed : [];
3951
- } catch {
3952
- return [];
3953
- }
3954
- }
3955
4478
  function registerPickedResource(resource, state) {
3956
4479
  var _a;
3957
4480
  const existing = state.resourceIndex.get(resource.resourceId);
@@ -3967,14 +4490,28 @@ function extractPickerError(error, state) {
3967
4490
  if (error instanceof Error && error.message) return error.message;
3968
4491
  return t("pickerError", state);
3969
4492
  }
3970
- async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resourceIds, maxCount, updateCallback, instance) {
4493
+ async function handleLibraryPickMulti(opts) {
3971
4494
  var _a;
4495
+ const {
4496
+ state,
4497
+ element,
4498
+ wrapper,
4499
+ fieldPath,
4500
+ resourceIds,
4501
+ maxCount,
4502
+ updateCallback,
4503
+ instance,
4504
+ coordinator,
4505
+ list,
4506
+ buildSuccessTile
4507
+ } = opts;
3972
4508
  if (!state.config.pickExistingFiles) return;
3973
4509
  const allowedExtensions = getAllowedExtensions(element.accept);
3974
4510
  const allowedMimes = getAllowedMimes(element.accept);
3975
4511
  const maxSizeMB = (_a = element.maxSize) != null ? _a : Infinity;
3976
- const currentIds = readCurrentResourceIds(wrapper);
3977
- const remaining = maxCount === Infinity ? Infinity : Math.max(0, maxCount - currentIds.length);
4512
+ const knownRids = coordinator.getAllKnownRids();
4513
+ const existingSet = new Set(knownRids);
4514
+ const remaining = maxCount === Infinity ? Infinity : Math.max(0, maxCount - coordinator.getOccupiedCount());
3978
4515
  let picked;
3979
4516
  try {
3980
4517
  picked = await state.config.pickExistingFiles({
@@ -3983,14 +4520,15 @@ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resour
3983
4520
  accept: buildAcceptContext(element),
3984
4521
  maxSizeMB: maxSizeMB === Infinity ? void 0 : maxSizeMB,
3985
4522
  remainingSlots: remaining === Infinity ? void 0 : remaining,
3986
- selectedResourceIds: [...currentIds]
4523
+ // Hand the host every rid that's already selected (committed or
4524
+ // staged) so it can grey them out or filter them.
4525
+ selectedResourceIds: knownRids
3987
4526
  });
3988
4527
  } catch (error) {
3989
4528
  showFileError(wrapper, extractPickerError(error, state));
3990
4529
  return;
3991
4530
  }
3992
4531
  if (picked.length === 0) return;
3993
- const existingSet = new Set(currentIds);
3994
4532
  const seen = /* @__PURE__ */ new Set();
3995
4533
  const deduped = picked.filter((r) => {
3996
4534
  if (existingSet.has(r.resourceId)) return false;
@@ -4008,10 +4546,18 @@ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resour
4008
4546
  );
4009
4547
  return err === null;
4010
4548
  });
4011
- const freshRemaining = maxCount === Infinity ? validItems.length : Math.max(0, maxCount - resourceIds.length);
4549
+ const freshRemaining = maxCount === Infinity ? validItems.length : Math.max(0, maxCount - coordinator.getOccupiedCount());
4012
4550
  const accepted = validItems.slice(0, freshRemaining);
4013
4551
  const skipped = validItems.length - accepted.length;
4014
- if (accepted.length === 0) return;
4552
+ if (accepted.length === 0) {
4553
+ if (skipped > 0) {
4554
+ showFileError(
4555
+ wrapper,
4556
+ t("filesLimitExceeded", state, { skipped, max: maxCount })
4557
+ );
4558
+ }
4559
+ return;
4560
+ }
4015
4561
  clearFileError(wrapper);
4016
4562
  if (skipped > 0) {
4017
4563
  showFileError(
@@ -4021,13 +4567,22 @@ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resour
4021
4567
  }
4022
4568
  for (const resource of accepted) {
4023
4569
  registerPickedResource(resource, state);
4024
- resourceIds.push(resource.resourceId);
4025
4570
  }
4026
- wrapper.dataset.resourceIds = JSON.stringify(resourceIds);
4027
- updateCallback();
4571
+ const acceptedIds = accepted.map((r) => r.resourceId);
4572
+ const handle = coordinator.beginBatch(accepted.length);
4573
+ handle.setResults(acceptedIds);
4028
4574
  if (!state.config.readonly) {
4029
4575
  instance.triggerOnChange(fieldPath, resourceIds);
4030
4576
  }
4577
+ const { wasLast } = handle.end();
4578
+ if (wasLast) {
4579
+ updateCallback();
4580
+ } else {
4581
+ const tilesWrap = ensureTilesWrap(list);
4582
+ for (const rid of acceptedIds) {
4583
+ tilesWrap.appendChild(buildSuccessTile(rid));
4584
+ }
4585
+ }
4031
4586
  }
4032
4587
  async function handleLibraryPickSingle(state, element, container, fileWrapper, pathKey, fieldPath, renderCallback, instance) {
4033
4588
  var _a, _b;
@@ -4134,21 +4689,21 @@ function buildWideTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragO
4134
4689
  };
4135
4690
  outer.appendChild(uploadBtn);
4136
4691
  if (hasLibrary && onLibraryClick) {
4137
- const divider = document.createElement("div");
4138
- divider.className = "fb-wide-tile-divider";
4139
- outer.appendChild(divider);
4140
4692
  const libBtn = document.createElement("button");
4141
4693
  libBtn.type = "button";
4142
4694
  libBtn.className = "fb-wide-tile-library fb-file-library-card";
4143
4695
  const libIcon = document.createElement("span");
4696
+ libIcon.className = "fb-wide-tile-library-icon";
4144
4697
  libIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
4145
4698
  libIcon.innerHTML = ICON_LIBRARY2;
4146
4699
  libBtn.appendChild(libIcon);
4147
4700
  const libLabel = document.createElement("div");
4701
+ libLabel.className = "fb-wide-tile-library-label";
4148
4702
  libLabel.style.cssText = "font-size:13px;font-weight:600;text-align:center;";
4149
4703
  libLabel.textContent = t("fromLibrary", state);
4150
4704
  libBtn.appendChild(libLabel);
4151
4705
  const libHint = document.createElement("div");
4706
+ libHint.className = "fb-wide-tile-library-hint";
4152
4707
  libHint.style.cssText = "font-size:11px;opacity:0.75;text-align:center;";
4153
4708
  libHint.textContent = t("libraryHint", state);
4154
4709
  libBtn.appendChild(libHint);
@@ -4355,6 +4910,14 @@ function buildMetaDot() {
4355
4910
  return dot;
4356
4911
  }
4357
4912
  var gridResizeObservers = /* @__PURE__ */ new WeakMap();
4913
+ function disposePlaceholdersForUpload(container) {
4914
+ const observer = gridResizeObservers.get(container);
4915
+ if (observer) {
4916
+ observer.disconnect();
4917
+ gridResizeObservers.delete(container);
4918
+ }
4919
+ container.querySelectorAll(".fb-multi-placeholder").forEach((p) => p.remove());
4920
+ }
4358
4921
  function renderResourcePills(opts) {
4359
4922
  var _a;
4360
4923
  const {
@@ -4709,16 +5272,19 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
4709
5272
  filesPicker.click();
4710
5273
  };
4711
5274
  const onLibraryPick = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4712
- handleLibraryPickMulti(
5275
+ handleLibraryPickMulti({
4713
5276
  state,
4714
5277
  element,
4715
- filesWrapper,
4716
- pathKey,
4717
- initialFiles,
4718
- maxFiles,
4719
- updateFilesDisplay,
4720
- ctx.instance
4721
- ).catch((err) => {
5278
+ wrapper: filesWrapper,
5279
+ fieldPath: pathKey,
5280
+ resourceIds: initialFiles,
5281
+ maxCount: maxFiles,
5282
+ updateCallback: updateFilesDisplay,
5283
+ instance: ctx.instance,
5284
+ coordinator,
5285
+ list,
5286
+ buildSuccessTile
5287
+ }).catch((err) => {
4722
5288
  console.error("Library pick failed:", err);
4723
5289
  });
4724
5290
  } : null;
@@ -4729,41 +5295,160 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
4729
5295
  rids: initialFiles,
4730
5296
  state,
4731
5297
  onRemove: currentlyReadonly ? null : (ridToRemove) => {
4732
- var _a2;
5298
+ var _a2, _b2;
4733
5299
  releaseLocalFileUrl((_a2 = state.resourceIndex.get(ridToRemove)) == null ? void 0 : _a2.file);
4734
5300
  const index = initialFiles.indexOf(ridToRemove);
4735
5301
  if (index > -1) initialFiles.splice(index, 1);
4736
- updateFilesDisplay();
5302
+ if (coordinator.hasInFlightBatches()) {
5303
+ pendingRemovals.add(ridToRemove);
5304
+ (_b2 = list.querySelector(`[data-resource-id="${ridToRemove}"]`)) == null ? void 0 : _b2.remove();
5305
+ filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
5306
+ } else {
5307
+ updateFilesDisplay();
5308
+ }
5309
+ if (ctx.instance && pathKey && !state.config.readonly) {
5310
+ ctx.instance.triggerOnChange(pathKey, initialFiles);
5311
+ }
4737
5312
  },
4738
5313
  maxCount: maxFiles < Infinity ? maxFiles : void 0,
4739
5314
  isReadonly: currentlyReadonly,
4740
5315
  onLibraryPick: currentlyReadonly ? null : onLibraryPick,
4741
5316
  element,
4742
5317
  onClearAll: currentlyReadonly ? void 0 : () => {
5318
+ var _a2, _b2;
5319
+ for (const rid of initialFiles) {
5320
+ releaseLocalFileUrl((_a2 = state.resourceIndex.get(rid)) == null ? void 0 : _a2.file);
5321
+ }
4743
5322
  initialFiles.splice(0);
4744
- updateFilesDisplay();
5323
+ if (coordinator.hasInFlightBatches()) {
5324
+ const visibleTiles = list.querySelectorAll("[data-resource-id]");
5325
+ for (const tile of visibleTiles) {
5326
+ const rid = tile.dataset.resourceId;
5327
+ if (rid) {
5328
+ releaseLocalFileUrl((_b2 = state.resourceIndex.get(rid)) == null ? void 0 : _b2.file);
5329
+ pendingRemovals.add(rid);
5330
+ }
5331
+ tile.remove();
5332
+ }
5333
+ filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
5334
+ } else {
5335
+ updateFilesDisplay();
5336
+ }
5337
+ if (ctx.instance && pathKey && !state.config.readonly) {
5338
+ ctx.instance.triggerOnChange(pathKey, initialFiles);
5339
+ }
4745
5340
  },
4746
5341
  openPicker
4747
5342
  });
4748
5343
  }
4749
- setupFilesDropHandler(
4750
- filesContainer,
4751
- initialFiles,
4752
- state,
4753
- updateFilesDisplay,
4754
- constraints,
4755
- pathKey,
4756
- ctx.instance
4757
- );
4758
- setupFilesPickerHandler(
4759
- filesPicker,
4760
- initialFiles,
5344
+ let inFlightFiles = 0;
5345
+ let activeBatches = 0;
5346
+ let nextBatchOrdinal = 0;
5347
+ let nextCommitOrdinal = 0;
5348
+ const stagedResults = /* @__PURE__ */ new Map();
5349
+ const batchReservations = /* @__PURE__ */ new Map();
5350
+ const pendingRemovals = /* @__PURE__ */ new Set();
5351
+ const drainContiguousStagedResults = () => {
5352
+ var _a2;
5353
+ while (stagedResults.has(nextCommitOrdinal)) {
5354
+ const ordinal = nextCommitOrdinal;
5355
+ const ids = stagedResults.get(ordinal);
5356
+ stagedResults.delete(ordinal);
5357
+ nextCommitOrdinal += 1;
5358
+ const reservation = (_a2 = batchReservations.get(ordinal)) != null ? _a2 : 0;
5359
+ batchReservations.delete(ordinal);
5360
+ inFlightFiles -= reservation;
5361
+ for (const rid of ids) {
5362
+ if (rid === null) continue;
5363
+ if (pendingRemovals.has(rid)) {
5364
+ pendingRemovals.delete(rid);
5365
+ continue;
5366
+ }
5367
+ initialFiles.push(rid);
5368
+ }
5369
+ }
5370
+ filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
5371
+ };
5372
+ const coordinator = {
5373
+ getOccupiedCount: () => initialFiles.length + inFlightFiles,
5374
+ getAllKnownRids: () => {
5375
+ const out = [...initialFiles];
5376
+ for (const ids of stagedResults.values()) {
5377
+ for (const rid of ids) {
5378
+ if (rid !== null) out.push(rid);
5379
+ }
5380
+ }
5381
+ return out;
5382
+ },
5383
+ hasInFlightBatches: () => activeBatches > 0 || batchReservations.size > 0,
5384
+ wasRemovedDuringBatch: (rid) => pendingRemovals.has(rid),
5385
+ beginBatch: (count) => {
5386
+ inFlightFiles += count;
5387
+ activeBatches += 1;
5388
+ const ordinal = nextBatchOrdinal++;
5389
+ batchReservations.set(ordinal, count);
5390
+ return {
5391
+ setResults: (orderedIds) => {
5392
+ stagedResults.set(ordinal, orderedIds);
5393
+ drainContiguousStagedResults();
5394
+ },
5395
+ end: () => {
5396
+ activeBatches -= 1;
5397
+ const wasLast = activeBatches === 0 && batchReservations.size === 0;
5398
+ if (wasLast) {
5399
+ pendingRemovals.clear();
5400
+ }
5401
+ return { wasLast };
5402
+ }
5403
+ };
5404
+ }
5405
+ };
5406
+ const buildSuccessTile = (rid) => {
5407
+ const currentlyReadonly = isElementReadonly(element, state);
5408
+ return buildPreviewTile(
5409
+ rid,
5410
+ state,
5411
+ !currentlyReadonly,
5412
+ currentlyReadonly ? null : () => {
5413
+ var _a2, _b2, _c;
5414
+ releaseLocalFileUrl((_a2 = state.resourceIndex.get(rid)) == null ? void 0 : _a2.file);
5415
+ const idx = initialFiles.indexOf(rid);
5416
+ if (idx > -1) {
5417
+ initialFiles.splice(idx, 1);
5418
+ if (coordinator.hasInFlightBatches()) {
5419
+ pendingRemovals.add(rid);
5420
+ (_b2 = list.querySelector(`[data-resource-id="${rid}"]`)) == null ? void 0 : _b2.remove();
5421
+ filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
5422
+ } else {
5423
+ updateFilesDisplay();
5424
+ }
5425
+ if (ctx.instance && pathKey && !state.config.readonly) {
5426
+ ctx.instance.triggerOnChange(pathKey, initialFiles);
5427
+ }
5428
+ return;
5429
+ }
5430
+ pendingRemovals.add(rid);
5431
+ (_c = list.querySelector(`[data-resource-id="${rid}"]`)) == null ? void 0 : _c.remove();
5432
+ if (ctx.instance && pathKey && !state.config.readonly) {
5433
+ ctx.instance.triggerOnChange(pathKey, initialFiles);
5434
+ }
5435
+ }
5436
+ );
5437
+ };
5438
+ const prepareForUpload = () => disposePlaceholdersForUpload(list);
5439
+ const sharedHandlerOpts = {
5440
+ resourceIds: initialFiles,
4761
5441
  state,
4762
- updateFilesDisplay,
5442
+ updateCallback: updateFilesDisplay,
4763
5443
  constraints,
4764
5444
  pathKey,
4765
- ctx.instance
4766
- );
5445
+ instance: ctx.instance,
5446
+ buildSuccessTile,
5447
+ prepareForUpload,
5448
+ coordinator
5449
+ };
5450
+ setupFilesDropHandler({ ...sharedHandlerOpts, filesContainer });
5451
+ setupFilesPickerHandler({ ...sharedHandlerOpts, filesPicker });
4767
5452
  updateFilesDisplay();
4768
5453
  wrapper.appendChild(filesWrapper);
4769
5454
  }
@@ -5411,7 +6096,7 @@ function validateColourElement(element, key, context) {
5411
6096
  };
5412
6097
  if (element.multiple) {
5413
6098
  const hexInputs = scopeRoot.querySelectorAll(
5414
- `[name^="${key}["].colour-hex-input`
6099
+ `[name^="${key}\\["].colour-hex-input`
5415
6100
  );
5416
6101
  const values = [];
5417
6102
  hexInputs.forEach((input, index) => {
@@ -5461,7 +6146,7 @@ function updateColourField(element, fieldPath, value, context) {
5461
6146
  return;
5462
6147
  }
5463
6148
  const hexInputs = scopeRoot.querySelectorAll(
5464
- `[name^="${fieldPath}["].colour-hex-input`
6149
+ `[name^="${fieldPath}\\["].colour-hex-input`
5465
6150
  );
5466
6151
  hexInputs.forEach((hexInput, index) => {
5467
6152
  if (index < value.length) {
@@ -5469,6 +6154,7 @@ function updateColourField(element, fieldPath, value, context) {
5469
6154
  hexInput.value = normalized;
5470
6155
  hexInput.classList.remove("invalid");
5471
6156
  hexInput.title = "";
6157
+ clearFieldError(hexInput);
5472
6158
  const wrapper = hexInput.closest(".colour-picker-wrapper");
5473
6159
  if (wrapper) {
5474
6160
  const swatch = wrapper.querySelector(".colour-swatch");
@@ -5498,6 +6184,7 @@ function updateColourField(element, fieldPath, value, context) {
5498
6184
  hexInput.value = normalized;
5499
6185
  hexInput.classList.remove("invalid");
5500
6186
  hexInput.title = "";
6187
+ clearFieldError(hexInput);
5501
6188
  const wrapper = hexInput.closest(".colour-picker-wrapper");
5502
6189
  if (wrapper) {
5503
6190
  const swatch = wrapper.querySelector(".colour-swatch");
@@ -5905,7 +6592,7 @@ function validateSliderElement(element, key, context) {
5905
6592
  };
5906
6593
  if (element.multiple) {
5907
6594
  const sliders = scopeRoot.querySelectorAll(
5908
- `input[type="range"][name^="${key}["]`
6595
+ `input[type="range"][name^="${key}\\["]`
5909
6596
  );
5910
6597
  const values = [];
5911
6598
  sliders.forEach((slider, index) => {
@@ -5957,7 +6644,7 @@ function updateSliderField(element, fieldPath, value, context) {
5957
6644
  return;
5958
6645
  }
5959
6646
  const sliders = scopeRoot.querySelectorAll(
5960
- `input[type="range"][name^="${fieldPath}["]`
6647
+ `input[type="range"][name^="${fieldPath}\\["]`
5961
6648
  );
5962
6649
  sliders.forEach((slider, index) => {
5963
6650
  if (index < value.length && value[index] !== null) {
@@ -5985,6 +6672,7 @@ function updateSliderField(element, fieldPath, value, context) {
5985
6672
  }
5986
6673
  slider.classList.remove("invalid");
5987
6674
  slider.title = "";
6675
+ clearFieldError(slider);
5988
6676
  }
5989
6677
  });
5990
6678
  if (value.length !== sliders.length) {
@@ -6021,6 +6709,7 @@ function updateSliderField(element, fieldPath, value, context) {
6021
6709
  }
6022
6710
  slider.classList.remove("invalid");
6023
6711
  slider.title = "";
6712
+ clearFieldError(slider);
6024
6713
  }
6025
6714
  }
6026
6715
  }
@@ -6145,34 +6834,24 @@ function getChildWrapperClass(isSlides, columns) {
6145
6834
  const cols = columns || 1;
6146
6835
  return cols === 1 ? "space-y-2" : `grid grid-cols-${cols} gap-2`;
6147
6836
  }
6148
- function mountRemoveButton(item, onRemove) {
6837
+ function mountRemoveButton(item, onRemove, state) {
6149
6838
  const rem = document.createElement("button");
6150
6839
  rem.type = "button";
6151
6840
  rem.className = "fb-item-remove";
6841
+ rem.setAttribute("aria-label", t("removeElement", state));
6152
6842
  rem.style.cssText = `
6153
- width: 22px;
6154
- height: 22px;
6843
+ width: 24px;
6844
+ height: 24px;
6155
6845
  display: inline-flex;
6156
6846
  align-items: center;
6157
6847
  justify-content: center;
6158
6848
  padding: 0;
6159
- line-height: 1;
6160
- font-size: 14px;
6161
- color: var(--fb-error-color);
6162
- background-color: transparent;
6163
6849
  border: 0;
6164
6850
  border-radius: 4px;
6165
6851
  cursor: pointer;
6166
6852
  flex-shrink: 0;
6167
- transition: background-color var(--fb-transition-duration);
6168
6853
  `;
6169
- rem.textContent = "\u2715";
6170
- rem.addEventListener("mouseenter", () => {
6171
- rem.style.backgroundColor = "var(--fb-background-hover-color)";
6172
- });
6173
- rem.addEventListener("mouseleave", () => {
6174
- rem.style.backgroundColor = "transparent";
6175
- });
6854
+ rem.innerHTML = BIN_ICON_SVG;
6176
6855
  rem.onclick = onRemove;
6177
6856
  const labelRow = item.querySelector("[data-fb-label-row]");
6178
6857
  if (labelRow) {
@@ -6199,7 +6878,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6199
6878
  itemsWrap.className = "fb-container-slides";
6200
6879
  const slideCols = element.columns;
6201
6880
  const gridTemplateColumns = typeof slideCols === "number" && slideCols > 0 ? `repeat(${slideCols}, 1fr)` : "repeat(auto-fit, minmax(280px, 1fr))";
6202
- itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:8px;align-items:start;`;
6881
+ itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:var(--fb-slides-gap, 14px);align-items:start;`;
6203
6882
  } else {
6204
6883
  itemsWrap.className = "space-y-2";
6205
6884
  }
@@ -6230,6 +6909,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6230
6909
  const item = document.createElement("div");
6231
6910
  item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6232
6911
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6912
+ if (isSlides) {
6913
+ item.setAttribute("data-fb-slide-card", "");
6914
+ }
6233
6915
  const childWrapper = document.createElement("div");
6234
6916
  childWrapper.className = getChildWrapperClass(isSlides, element.columns);
6235
6917
  element.elements.forEach((child) => {
@@ -6247,7 +6929,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6247
6929
  });
6248
6930
  item.appendChild(childWrapper);
6249
6931
  if (!containerIsReadonly) {
6250
- mountRemoveButton(item, () => handleRemoveItem(item));
6932
+ mountRemoveButton(item, () => handleRemoveItem(item), state);
6251
6933
  }
6252
6934
  if (slideAddTile && slideAddTile.parentElement === itemsWrap) {
6253
6935
  itemsWrap.insertBefore(item, slideAddTile);
@@ -6294,6 +6976,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6294
6976
  const item = document.createElement("div");
6295
6977
  item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6296
6978
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6979
+ if (isSlides) {
6980
+ item.setAttribute("data-fb-slide-card", "");
6981
+ }
6297
6982
  const childWrapper = document.createElement("div");
6298
6983
  if (isSlides) {
6299
6984
  childWrapper.className = "space-y-2";
@@ -6318,7 +7003,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6318
7003
  });
6319
7004
  item.appendChild(childWrapper);
6320
7005
  if (!containerIsReadonly) {
6321
- mountRemoveButton(item, () => handleRemoveItem(item));
7006
+ mountRemoveButton(item, () => handleRemoveItem(item), ctx.state);
6322
7007
  }
6323
7008
  itemsWrap.appendChild(item);
6324
7009
  });
@@ -6338,6 +7023,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6338
7023
  const item = document.createElement("div");
6339
7024
  item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6340
7025
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
7026
+ if (isSlides) {
7027
+ item.setAttribute("data-fb-slide-card", "");
7028
+ }
6341
7029
  const childWrapper = document.createElement("div");
6342
7030
  if (isSlides) {
6343
7031
  childWrapper.className = "space-y-2";
@@ -6363,11 +7051,15 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6363
7051
  }
6364
7052
  });
6365
7053
  item.appendChild(childWrapper);
6366
- mountRemoveButton(item, () => {
6367
- if (countItems() > min) {
6368
- handleRemoveItem(item);
6369
- }
6370
- });
7054
+ mountRemoveButton(
7055
+ item,
7056
+ () => {
7057
+ if (countItems() > min) {
7058
+ handleRemoveItem(item);
7059
+ }
7060
+ },
7061
+ ctx.state
7062
+ );
6371
7063
  itemsWrap.appendChild(item);
6372
7064
  }
6373
7065
  }
@@ -9060,7 +9752,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
9060
9752
  if (element.minLength != null || element.maxLength != null) {
9061
9753
  const counterRow = document.createElement("div");
9062
9754
  counterRow.style.cssText = "position: relative; padding: 2px 10px 4px; text-align: right;";
9063
- const counter = createCharCounter(element, textarea, false);
9755
+ const counter = createCharCounter(element, textarea);
9064
9756
  counter.style.cssText = `
9065
9757
  position: static;
9066
9758
  display: inline-block;
@@ -9592,6 +10284,118 @@ function validateMarkdown(_element, _key, _context) {
9592
10284
  function updateMarkdown(_element, _fieldPath, _value, _context) {
9593
10285
  }
9594
10286
 
10287
+ // src/components/registry.ts
10288
+ function validateHiddenElement(element, key, context) {
10289
+ var _a;
10290
+ const { scopeRoot } = context;
10291
+ const input = scopeRoot.querySelector(
10292
+ `input[type="hidden"][data-hidden-field="true"][name="${key}"]`
10293
+ );
10294
+ const raw = (_a = input == null ? void 0 : input.value) != null ? _a : "";
10295
+ if (raw === "") {
10296
+ const defaultVal = "default" in element ? element.default : null;
10297
+ return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
10298
+ }
10299
+ return { value: deserializeHiddenValue(raw), errors: [] };
10300
+ }
10301
+ function updateHiddenField(_element, fieldPath, value, context) {
10302
+ const { scopeRoot } = context;
10303
+ const input = scopeRoot.querySelector(
10304
+ `input[type="hidden"][data-hidden-field="true"][name="${fieldPath}"]`
10305
+ );
10306
+ if (!input) return;
10307
+ input.value = serializeHiddenValue(value);
10308
+ }
10309
+ var componentRegistry = {
10310
+ text: {
10311
+ validate: validateTextElement,
10312
+ update: updateTextField
10313
+ },
10314
+ textarea: {
10315
+ validate: validateTextareaElement,
10316
+ update: updateTextareaField
10317
+ },
10318
+ number: {
10319
+ validate: validateNumberElement,
10320
+ update: updateNumberField
10321
+ },
10322
+ select: {
10323
+ validate: validateSelectElement,
10324
+ update: updateSelectField
10325
+ },
10326
+ switcher: {
10327
+ validate: validateSwitcherElement,
10328
+ update: updateSwitcherField
10329
+ },
10330
+ boolean: {
10331
+ validate: validateBooleanElement,
10332
+ update: updateBooleanField,
10333
+ ownsLabel: true
10334
+ },
10335
+ file: {
10336
+ validate: validateFileElement,
10337
+ update: updateFileField
10338
+ },
10339
+ files: {
10340
+ // Legacy type - delegates to file
10341
+ validate: validateFileElement,
10342
+ update: updateFileField
10343
+ },
10344
+ colour: {
10345
+ validate: validateColourElement,
10346
+ update: updateColourField
10347
+ },
10348
+ slider: {
10349
+ validate: validateSliderElement,
10350
+ update: updateSliderField
10351
+ },
10352
+ container: {
10353
+ validate: validateContainerElement,
10354
+ update: updateContainerField
10355
+ },
10356
+ group: {
10357
+ // Deprecated type - delegates to container
10358
+ validate: validateGroupElement,
10359
+ update: updateGroupField
10360
+ },
10361
+ table: {
10362
+ validate: validateTableElement,
10363
+ update: updateTableField
10364
+ },
10365
+ richinput: {
10366
+ validate: validateRichInputElement,
10367
+ update: updateRichInputField
10368
+ },
10369
+ hidden: {
10370
+ // Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
10371
+ validate: validateHiddenElement,
10372
+ update: updateHiddenField
10373
+ },
10374
+ markdown: {
10375
+ // Display-only element — no value, no errors, skip from form data
10376
+ validate: validateMarkdown,
10377
+ update: updateMarkdown
10378
+ }
10379
+ };
10380
+ function getComponentOperations(elementType) {
10381
+ return componentRegistry[elementType] || null;
10382
+ }
10383
+ function validateElementWithComponent(element, key, context) {
10384
+ const ops = getComponentOperations(element.type);
10385
+ if (ops && ops.validate) {
10386
+ return ops.validate(element, key, context);
10387
+ }
10388
+ return null;
10389
+ }
10390
+ function updateElementWithComponent(element, fieldPath, value, context) {
10391
+ const ops = getComponentOperations(element.type);
10392
+ if (ops && ops.update) {
10393
+ ops.update(element, fieldPath, value, context);
10394
+ return true;
10395
+ }
10396
+ return false;
10397
+ }
10398
+
9595
10399
  // src/components/index.ts
9596
10400
  function showTooltip(tooltipId, button) {
9597
10401
  const tooltip = document.getElementById(tooltipId);
@@ -9904,6 +10708,9 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
9904
10708
  renderSwitcherElement(element, ctx, wrapper, pathKey);
9905
10709
  }
9906
10710
  break;
10711
+ case "boolean":
10712
+ renderBooleanElement(element, ctx, wrapper, pathKey);
10713
+ break;
9907
10714
  case "file":
9908
10715
  if (isMultiple) {
9909
10716
  renderMultipleFileElement(element, ctx, wrapper, pathKey);
@@ -9982,8 +10789,11 @@ function renderElement2(element, ctx) {
9982
10789
  const wrapper = document.createElement("div");
9983
10790
  wrapper.className = "mb-2 fb-field-wrapper";
9984
10791
  wrapper.setAttribute("data-field-key", element.key);
9985
- const label = createLabelContainer(element);
9986
- wrapper.appendChild(label);
10792
+ const ops = getComponentOperations(element.type);
10793
+ if (!(ops == null ? void 0 : ops.ownsLabel)) {
10794
+ const label = createLabelContainer(element);
10795
+ wrapper.appendChild(label);
10796
+ }
9987
10797
  const pathKey = pathJoin(ctx.path, element.key);
9988
10798
  dispatchToRenderer(element, ctx, wrapper, pathKey);
9989
10799
  if (initiallyDisabled) {
@@ -10216,28 +11026,52 @@ var defaultTheme = {
10216
11026
  // blue-500
10217
11027
  primaryHoverColor: "#2563eb",
10218
11028
  // blue-600
11029
+ primarySoftColor: "#dbeafe",
11030
+ // blue-100
11031
+ primarySoftHoverColor: "#bfdbfe",
11032
+ // blue-200
10219
11033
  errorColor: "#ef4444",
10220
11034
  // red-500
10221
11035
  errorHoverColor: "#dc2626",
10222
11036
  // red-600
10223
11037
  successColor: "#10b981",
10224
11038
  // green-500
11039
+ accentColor: "#f59e0b",
11040
+ // amber-500
11041
+ accentSoftColor: "#fef3c7",
11042
+ // amber-100
11043
+ accentBorderColor: "#fde68a",
11044
+ // amber-200
11045
+ accentTextColor: "#92400e",
11046
+ // amber-800
10225
11047
  borderColor: "#d1d5db",
10226
11048
  // gray-300
10227
11049
  borderHoverColor: "#9ca3af",
10228
11050
  // gray-400
10229
11051
  borderFocusColor: "#3b82f6",
10230
11052
  // blue-500
11053
+ borderStrongColor: "#9ca3af",
11054
+ // gray-400
10231
11055
  backgroundColor: "#ffffff",
10232
11056
  // white
10233
11057
  backgroundHoverColor: "#f9fafb",
10234
11058
  // gray-50
10235
11059
  backgroundReadonlyColor: "#f3f4f6",
10236
11060
  // gray-100
11061
+ pageBackgroundColor: "#f9fafb",
11062
+ // gray-50
11063
+ surfaceSoftColor: "#eff6ff",
11064
+ // blue-50
11065
+ surfaceTintColor: "#f8fafc",
11066
+ // slate-50
10237
11067
  textColor: "#1f2937",
10238
11068
  // gray-800
10239
11069
  textSecondaryColor: "#6b7280",
10240
11070
  // gray-500
11071
+ textMutedColor: "#9ca3af",
11072
+ // gray-400
11073
+ textFaintColor: "#cbd5e1",
11074
+ // slate-300
10241
11075
  textPlaceholderColor: "#9ca3af",
10242
11076
  // gray-400
10243
11077
  textDisabledColor: "#d1d5db",
@@ -10280,6 +11114,12 @@ var defaultTheme = {
10280
11114
  // 4px (compact density v2)
10281
11115
  borderRadius: "0.5rem",
10282
11116
  // rounded-lg (8px)
11117
+ borderRadiusSmall: "0.375rem",
11118
+ // 6px
11119
+ borderRadiusLarge: "0.75rem",
11120
+ // 12px
11121
+ borderRadiusXLarge: "1rem",
11122
+ // 16px
10283
11123
  borderWidth: "1px",
10284
11124
  // Typography
10285
11125
  fontSize: "0.875rem",
@@ -10291,13 +11131,31 @@ var defaultTheme = {
10291
11131
  fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
10292
11132
  fontWeightNormal: "400",
10293
11133
  fontWeightMedium: "500",
11134
+ lineHeight: "1.5",
11135
+ // Shadows
11136
+ shadowCard: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
11137
+ shadowPopover: "0 12px 32px -12px rgba(15,23,42,.18), 0 4px 12px -6px rgba(15,23,42,.08)",
10294
11138
  // Focus ring
10295
11139
  focusRingWidth: "2px",
10296
11140
  focusRingColor: "#3b82f6",
10297
11141
  // blue-500
10298
11142
  focusRingOpacity: "0.5",
10299
11143
  // Transitions
10300
- transitionDuration: "200ms"
11144
+ transitionDuration: "200ms",
11145
+ // Slide-card defaults — flat-white to match every other item card. The
11146
+ // Picaz theme overrides this with a gradient + shadow to lift the slides.
11147
+ slideCardBg: "#ffffff",
11148
+ slideCardShadow: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
11149
+ slideCardRadius: "0.5rem",
11150
+ // matches borderRadius
11151
+ slideCardMinHeight: "0",
11152
+ slideCardPadding: "12px",
11153
+ // Section-label defaults — same look as the regular field label. Themes
11154
+ // that want the "ПРЕИМУЩЕСТВА" caps style override these three vars.
11155
+ labelSectionFontSize: "0.875rem",
11156
+ // matches fontSize
11157
+ labelSectionLetterSpacing: "normal",
11158
+ labelSectionTextTransform: "none"
10301
11159
  };
10302
11160
  function generateCSSVariables(theme) {
10303
11161
  const mergedTheme = { ...defaultTheme, ...theme };
@@ -10309,6 +11167,7 @@ function generateCSSVariables(theme) {
10309
11167
  return cssVars.join("\n");
10310
11168
  }
10311
11169
  function injectThemeVariables(container, theme) {
11170
+ ensureThemingHooks(container.ownerDocument || document);
10312
11171
  const cssVariables = generateCSSVariables(theme);
10313
11172
  let styleTag = container.querySelector(
10314
11173
  "style[data-fb-theme]"
@@ -10371,115 +11230,64 @@ var exampleThemes = {
10371
11230
  fontSize: "16px",
10372
11231
  fontSizeSmall: "14px",
10373
11232
  fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif'
10374
- }
10375
- };
10376
-
10377
- // src/components/registry.ts
10378
- function validateHiddenElement(element, key, context) {
10379
- var _a;
10380
- const { scopeRoot } = context;
10381
- const input = scopeRoot.querySelector(
10382
- `input[type="hidden"][data-hidden-field="true"][name="${key}"]`
10383
- );
10384
- const raw = (_a = input == null ? void 0 : input.value) != null ? _a : "";
10385
- if (raw === "") {
10386
- const defaultVal = "default" in element ? element.default : null;
10387
- return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
10388
- }
10389
- return { value: deserializeHiddenValue(raw), errors: [] };
10390
- }
10391
- function updateHiddenField(_element, fieldPath, value, context) {
10392
- const { scopeRoot } = context;
10393
- const input = scopeRoot.querySelector(
10394
- `input[type="hidden"][data-hidden-field="true"][name="${fieldPath}"]`
10395
- );
10396
- if (!input) return;
10397
- input.value = serializeHiddenValue(value);
10398
- }
10399
- var componentRegistry = {
10400
- text: {
10401
- validate: validateTextElement,
10402
- update: updateTextField
10403
- },
10404
- textarea: {
10405
- validate: validateTextareaElement,
10406
- update: updateTextareaField
10407
- },
10408
- number: {
10409
- validate: validateNumberElement,
10410
- update: updateNumberField
10411
- },
10412
- select: {
10413
- validate: validateSelectElement,
10414
- update: updateSelectField
10415
- },
10416
- switcher: {
10417
- validate: validateSwitcherElement,
10418
- update: updateSwitcherField
10419
- },
10420
- file: {
10421
- validate: validateFileElement,
10422
- update: updateFileField
10423
- },
10424
- files: {
10425
- // Legacy type - delegates to file
10426
- validate: validateFileElement,
10427
- update: updateFileField
10428
- },
10429
- colour: {
10430
- validate: validateColourElement,
10431
- update: updateColourField
10432
- },
10433
- slider: {
10434
- validate: validateSliderElement,
10435
- update: updateSliderField
10436
- },
10437
- container: {
10438
- validate: validateContainerElement,
10439
- update: updateContainerField
10440
- },
10441
- group: {
10442
- // Deprecated type - delegates to container
10443
- validate: validateGroupElement,
10444
- update: updateGroupField
10445
- },
10446
- table: {
10447
- validate: validateTableElement,
10448
- update: updateTableField
10449
- },
10450
- richinput: {
10451
- validate: validateRichInputElement,
10452
- update: updateRichInputField
10453
- },
10454
- hidden: {
10455
- // Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
10456
- validate: validateHiddenElement,
10457
- update: updateHiddenField
10458
11233
  },
10459
- markdown: {
10460
- // Display-only element no value, no errors, skip from form data
10461
- validate: validateMarkdown,
10462
- update: updateMarkdown
11234
+ // Picaz wizard design tokens — derived from the Picaz Wizard mockups.
11235
+ // Pairs with the host-side .card / .section-num / .lede chrome that wraps the form.
11236
+ // Assumes Inter is loaded by the host (e.g. via Google Fonts in index.html).
11237
+ picaz: {
11238
+ ...defaultTheme,
11239
+ primaryColor: "#2f5bea",
11240
+ primaryHoverColor: "#2349c8",
11241
+ primarySoftColor: "#eaf0ff",
11242
+ primarySoftHoverColor: "#d6e0ff",
11243
+ errorColor: "#ef4444",
11244
+ successColor: "#16a34a",
11245
+ accentColor: "#ffb020",
11246
+ accentSoftColor: "#fff7e6",
11247
+ accentBorderColor: "#fde7b5",
11248
+ accentTextColor: "#92400e",
11249
+ borderColor: "#e3e8f0",
11250
+ borderHoverColor: "#cdd6e3",
11251
+ borderFocusColor: "#2f5bea",
11252
+ borderStrongColor: "#cdd6e3",
11253
+ backgroundColor: "#ffffff",
11254
+ backgroundHoverColor: "#f3f7ff",
11255
+ pageBackgroundColor: "#f6f8fb",
11256
+ surfaceSoftColor: "#eef4ff",
11257
+ surfaceTintColor: "#f3f7ff",
11258
+ textColor: "#0f172a",
11259
+ textSecondaryColor: "#334155",
11260
+ textMutedColor: "#64748b",
11261
+ textFaintColor: "#94a3b8",
11262
+ textPlaceholderColor: "#94a3b8",
11263
+ buttonBgColor: "#2f5bea",
11264
+ buttonHoverBgColor: "#2349c8",
11265
+ fileUploadBgColor: "#fafcff",
11266
+ fileUploadBorderColor: "#cdd6e3",
11267
+ fileUploadHoverBorderColor: "#2f5bea",
11268
+ // Picaz uses roomier inputs (11/14px) than the defaultTheme compact density.
11269
+ inputPaddingX: "14px",
11270
+ inputPaddingY: "11px",
11271
+ borderRadius: "12px",
11272
+ borderRadiusSmall: "8px",
11273
+ borderRadiusLarge: "16px",
11274
+ borderRadiusXLarge: "22px",
11275
+ fontFamily: '"Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
11276
+ shadowCard: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
11277
+ shadowPopover: "0 12px 32px -12px rgba(15,23,42,.18), 0 4px 12px -6px rgba(15,23,42,.08)",
11278
+ focusRingColor: "#2f5bea",
11279
+ // Slide cards: subtle gradient lift, no rest-state shadow (mockup adds it
11280
+ // only on hover, which form-builder doesn't yet differentiate).
11281
+ slideCardBg: "linear-gradient(180deg, #f7f9fc 0%, #dde3ee 100%)",
11282
+ slideCardShadow: "none",
11283
+ slideCardRadius: "16px",
11284
+ // Tiny uppercase captions above grouped lists ("ПРЕИМУЩЕСТВА" in the mockup).
11285
+ labelSectionFontSize: "0.625rem",
11286
+ // 10px
11287
+ labelSectionLetterSpacing: "0.07em",
11288
+ labelSectionTextTransform: "uppercase"
10463
11289
  }
10464
11290
  };
10465
- function getComponentOperations(elementType) {
10466
- return componentRegistry[elementType] || null;
10467
- }
10468
- function validateElementWithComponent(element, key, context) {
10469
- const ops = getComponentOperations(element.type);
10470
- if (ops && ops.validate) {
10471
- return ops.validate(element, key, context);
10472
- }
10473
- return null;
10474
- }
10475
- function updateElementWithComponent(element, fieldPath, value, context) {
10476
- const ops = getComponentOperations(element.type);
10477
- if (ops && ops.update) {
10478
- ops.update(element, fieldPath, value, context);
10479
- return true;
10480
- }
10481
- return false;
10482
- }
10483
11291
 
10484
11292
  // src/instance/FormBuilderInstance.ts
10485
11293
  var FormBuilderInstance = class {
@@ -10604,26 +11412,37 @@ var FormBuilderInstance = class {
10604
11412
  }
10605
11413
  }
10606
11414
  /**
10607
- * Find the DOM element corresponding to a field path (instance-scoped)
11415
+ * Find the DOM element corresponding to a field path (instance-scoped).
11416
+ *
11417
+ * Strategy:
11418
+ * 1. Try a `[name="…"]` lookup first — works for any field that renders
11419
+ * an input/hidden with the path as its name, in either mode. Some
11420
+ * readonly renderers still emit a hidden input (boolean, switcher),
11421
+ * so this path must run regardless of `state.config.readonly`. A
11422
+ * prior version gated this on edit mode only, which made
11423
+ * `updateField` / `setFormData` silently miss readonly boolean
11424
+ * fields whose component also opts out of the standard label row
11425
+ * (`ownsLabel: true`).
11426
+ * 2. If no input matched, fall back to locating the field wrapper by
11427
+ * its visible label text — needed for readonly previews that don't
11428
+ * emit any `name=` attribute (e.g. file/markdown previews).
10608
11429
  */
10609
11430
  findFormElementByFieldPath(fieldPath) {
10610
11431
  if (!this.state.formRoot) return null;
10611
- if (!this.state.config.readonly) {
10612
- let element = this.state.formRoot.querySelector(
10613
- `[name="${fieldPath}"]`
11432
+ let element = this.state.formRoot.querySelector(
11433
+ `[name="${fieldPath}"]`
11434
+ );
11435
+ if (element) return element;
11436
+ const variations = [
11437
+ fieldPath,
11438
+ fieldPath.replace(/\[(\d+)\]/g, "[$1]"),
11439
+ fieldPath.replace(/\./g, "[") + "]".repeat((fieldPath.match(/\./g) || []).length)
11440
+ ];
11441
+ for (const variation of variations) {
11442
+ element = this.state.formRoot.querySelector(
11443
+ `[name="${variation}"]`
10614
11444
  );
10615
11445
  if (element) return element;
10616
- const variations = [
10617
- fieldPath,
10618
- fieldPath.replace(/\[(\d+)\]/g, "[$1]"),
10619
- fieldPath.replace(/\./g, "[") + "]".repeat((fieldPath.match(/\./g) || []).length)
10620
- ];
10621
- for (const variation of variations) {
10622
- element = this.state.formRoot.querySelector(
10623
- `[name="${variation}"]`
10624
- );
10625
- if (element) return element;
10626
- }
10627
11446
  }
10628
11447
  const schemaElement = this.findSchemaElement(fieldPath);
10629
11448
  if (!schemaElement) return null;
@@ -10832,6 +11651,13 @@ var FormBuilderInstance = class {
10832
11651
  const value = hintValues[fieldKey];
10833
11652
  this.updateField(fullPath, value);
10834
11653
  }
11654
+ const group = target.closest(".fb-prefill-hints");
11655
+ if (group) {
11656
+ group.querySelectorAll(
11657
+ '.fb-prefill-hint[aria-pressed="true"]'
11658
+ ).forEach((el) => el.removeAttribute("aria-pressed"));
11659
+ }
11660
+ target.setAttribute("aria-pressed", "true");
10835
11661
  } catch (error) {
10836
11662
  console.error("Error parsing prefill hint values:", error);
10837
11663
  }