@dmitryvim/form-builder 0.2.34 → 0.3.0

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,82 @@ 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
+ `;
447
+ doc.head.appendChild(style);
448
+ }
449
+ function applyAutoExpand(textarea) {
450
+ textarea.style.overflow = "hidden";
451
+ textarea.style.resize = "none";
452
+ const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
453
+ textarea.rows = Math.max(1, lineCount);
454
+ const resize = () => {
455
+ if (!textarea.isConnected) return;
456
+ textarea.style.height = "0";
457
+ textarea.style.height = `${textarea.scrollHeight}px`;
458
+ };
459
+ textarea.addEventListener("input", resize);
460
+ setTimeout(() => {
461
+ if (textarea.isConnected) resize();
462
+ }, 0);
463
+ }
464
+ function applySingleLineMode(textarea) {
465
+ textarea.addEventListener("keydown", (e) => {
466
+ if (e.key === "Enter") {
467
+ e.preventDefault();
468
+ }
469
+ });
470
+ textarea.addEventListener("paste", (e) => {
471
+ var _a, _b, _c, _d;
472
+ const pasted = (_b = (_a = e.clipboardData) == null ? void 0 : _a.getData("text")) != null ? _b : "";
473
+ if (!/[\r\n]/.test(pasted)) return;
474
+ e.preventDefault();
475
+ const cleaned = pasted.replace(/[\r\n]+/g, " ");
476
+ const start = (_c = textarea.selectionStart) != null ? _c : textarea.value.length;
477
+ const end = (_d = textarea.selectionEnd) != null ? _d : textarea.value.length;
478
+ const before = textarea.value.slice(0, start);
479
+ const after = textarea.value.slice(end);
480
+ textarea.value = before + cleaned + after;
481
+ const pos = start + cleaned.length;
482
+ textarea.setSelectionRange(pos, pos);
483
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
484
+ });
485
+ }
410
486
  function mountCounterInLabel(wrapper, counter) {
411
487
  const labelRow = wrapper.querySelector(
412
488
  ":scope > [data-fb-label-row]"
@@ -570,46 +646,94 @@ function applyActionButtonStyles(button, isFormLevel = false) {
570
646
  }
571
647
 
572
648
  // src/components/text.ts
573
- function createCharCounter(element, input, isTextarea = false) {
649
+ function ensureChipStyles(doc) {
650
+ if (doc.head.querySelector("[data-fb-chip-styles]")) return;
651
+ const style = doc.createElement("style");
652
+ style.setAttribute("data-fb-chip-styles", "");
653
+ style.textContent = `
654
+ .fb-chip-list { display: flex; flex-direction: column; gap: 4px; }
655
+ .fb-chip {
656
+ display: flex;
657
+ align-items: center;
658
+ gap: 8px;
659
+ padding: 5px 6px 5px 10px;
660
+ background: var(--fb-chip-bg, var(--fb-background-color, #fff));
661
+ border: 1px solid var(--fb-chip-border, var(--fb-border-color, #e2e8f0));
662
+ border-radius: 6px;
663
+ position: relative;
664
+ transition: border-color var(--fb-transition-duration, 0.15s);
665
+ }
666
+ .fb-chip:hover { border-color: var(--fb-border-hover-color, var(--fb-border-color, #cbd5e1)); }
667
+ .fb-chip:focus-within { border-color: var(--fb-border-focus-color, var(--fb-primary-color, #2f5bea)); }
668
+ .fb-chip-dot {
669
+ flex: 0 0 6px;
670
+ width: 6px;
671
+ height: 6px;
672
+ border-radius: 50%;
673
+ background: var(--fb-chip-dot, var(--fb-primary-color, #2f5bea));
674
+ }
675
+ .fb-chip-input {
676
+ flex: 1;
677
+ min-width: 0;
678
+ padding: 2px 0;
679
+ border: 0;
680
+ outline: none;
681
+ background: transparent;
682
+ color: var(--fb-chip-text, var(--fb-text-color, inherit));
683
+ font-size: var(--fb-font-size, 14px);
684
+ font-family: var(--fb-font-family, inherit);
685
+ line-height: 1.4;
686
+ }
687
+ .fb-chip-input::placeholder { color: var(--fb-text-placeholder-color, #94a3b8); }
688
+ .fb-chip-input:read-only { color: var(--fb-text-secondary-color, #475569); }
689
+ .fb-chip-remove {
690
+ flex: 0 0 auto;
691
+ width: 22px;
692
+ height: 22px;
693
+ display: inline-flex;
694
+ align-items: center;
695
+ justify-content: center;
696
+ padding: 0;
697
+ border: 0;
698
+ border-radius: 4px;
699
+ background: transparent;
700
+ color: var(--fb-text-faint-color, #94a3b8);
701
+ cursor: pointer;
702
+ opacity: 0;
703
+ transition: opacity 0.12s, color 0.12s, background-color 0.12s;
704
+ }
705
+ .fb-chip:hover .fb-chip-remove,
706
+ .fb-chip-remove:focus-visible { opacity: 1; }
707
+ .fb-chip-remove:hover {
708
+ color: var(--fb-error-color, #dc2626);
709
+ background: var(--fb-background-hover-color, #f1f5f9);
710
+ }
711
+ .fb-chip-remove:disabled { opacity: 0 !important; pointer-events: none; }
712
+ `;
713
+ doc.head.appendChild(style);
714
+ }
715
+ function createCharCounter(element, input) {
574
716
  const counter = document.createElement("span");
575
717
  counter.className = "char-counter";
576
718
  counter.style.cssText = `
577
- position: absolute;
578
- ${isTextarea ? "bottom: 8px" : "top: 50%; transform: translateY(-50%)"};
579
- right: 10px;
719
+ margin-top: 4px;
720
+ padding-right: 12px;
721
+ text-align: right;
580
722
  font-size: var(--fb-font-size-small);
581
- color: var(--fb-text-secondary-color);
723
+ line-height: 1;
724
+ color: var(--fb-error-color);
582
725
  pointer-events: none;
583
- background: var(--fb-background-color);
584
- padding: 0 4px;
726
+ display: none;
585
727
  `;
586
728
  const updateCounter = () => {
587
729
  const len = input.value.length;
588
- const min = element.minLength;
589
730
  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) {
731
+ if (max != null && len > max) {
604
732
  counter.textContent = `${len}/${max}`;
605
- counter.style.color = "var(--fb-error-color)";
733
+ counter.style.display = "block";
606
734
  } 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)";
735
+ counter.textContent = "";
736
+ counter.style.display = "none";
613
737
  }
614
738
  };
615
739
  input.addEventListener("input", updateCounter);
@@ -622,26 +746,32 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
622
746
  const inputWrapper = document.createElement("div");
623
747
  inputWrapper.style.cssText = "position: relative;";
624
748
  const hasCharCounter = !readonly && (element.minLength != null || element.maxLength != null);
625
- const textInput = document.createElement("input");
626
- textInput.type = "text";
749
+ const textInput = document.createElement("textarea");
750
+ textInput.rows = 1;
627
751
  textInput.className = "w-full rounded-lg";
628
752
  textInput.style.cssText = `
629
753
  padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
630
- ${hasCharCounter ? "padding-right: 60px;" : ""}
631
754
  border: var(--fb-border-width) solid var(--fb-border-color);
632
755
  border-radius: var(--fb-border-radius);
633
756
  background-color: ${readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
634
757
  color: var(--fb-text-color);
635
758
  font-size: var(--fb-font-size);
636
759
  font-family: var(--fb-font-family);
760
+ line-height: var(--fb-line-height, 1.5);
637
761
  transition: all var(--fb-transition-duration) ease-in-out;
638
762
  width: 100%;
639
763
  box-sizing: border-box;
764
+ resize: none;
765
+ overflow: hidden;
766
+ word-break: break-word;
767
+ overflow-wrap: anywhere;
640
768
  `;
641
769
  textInput.name = pathKey;
642
770
  textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
643
771
  textInput.value = ctx.prefill[element.key] || element.default || "";
644
772
  textInput.readOnly = readonly;
773
+ applySingleLineMode(textInput);
774
+ applyAutoExpand(textInput);
645
775
  if (!readonly) {
646
776
  textInput.addEventListener("focus", () => {
647
777
  textInput.style.borderColor = "var(--fb-border-focus-color)";
@@ -673,7 +803,7 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
673
803
  }
674
804
  inputWrapper.appendChild(textInput);
675
805
  if (hasCharCounter) {
676
- const counter = createCharCounter(element, textInput, false);
806
+ const counter = createCharCounter(element, textInput);
677
807
  inputWrapper.appendChild(counter);
678
808
  }
679
809
  wrapper.appendChild(inputWrapper);
@@ -684,132 +814,83 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
684
814
  const readonly = isElementReadonly(element, state, ctx);
685
815
  const prefillValues = ctx.prefill[element.key] || [];
686
816
  const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
687
- const hasCharCounter = !readonly && (element.minLength != null || element.maxLength != null);
688
817
  const minCount = (_a = element.minCount) != null ? _a : 1;
689
818
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
690
819
  while (values.length < minCount) {
691
820
  values.push(element.default || "");
692
821
  }
693
- const container = document.createElement("div");
694
- container.className = "space-y-2";
695
- wrapper.appendChild(container);
822
+ ensureChipStyles(document);
823
+ const list = document.createElement("div");
824
+ list.className = "fb-chip-list";
825
+ wrapper.appendChild(list);
696
826
  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}]`;
827
+ const items = list.querySelectorAll(".fb-chip-input");
828
+ items.forEach((input, index) => {
829
+ input.name = `${pathKey}[${index}]`;
830
+ const chip = input.closest(".fb-chip");
831
+ const sib = chip == null ? void 0 : chip.nextElementSibling;
832
+ if (sib && sib.classList.contains("error-message")) {
833
+ sib.id = `error-${input.name}`;
702
834
  }
703
835
  });
704
836
  }
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
- }
837
+ function addChip(value = "") {
838
+ const chip = document.createElement("div");
839
+ chip.className = "fb-chip";
840
+ const dot = document.createElement("span");
841
+ dot.className = "fb-chip-dot";
842
+ dot.setAttribute("aria-hidden", "true");
843
+ chip.appendChild(dot);
844
+ const input = document.createElement("input");
845
+ input.type = "text";
846
+ input.className = "fb-chip-input";
847
+ input.value = value;
848
+ input.placeholder = element.placeholder || t("placeholderText", state);
849
+ input.readOnly = readonly;
850
+ chip.appendChild(input);
749
851
  if (!readonly && ctx.instance) {
750
852
  const handleChange = () => {
751
- const value2 = textInput.value === "" ? null : textInput.value;
752
- ctx.instance.triggerOnChange(textInput.name, value2);
853
+ ctx.instance.triggerOnChange(
854
+ input.name,
855
+ input.value === "" ? null : input.value
856
+ );
753
857
  };
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);
858
+ input.addEventListener("blur", handleChange);
859
+ input.addEventListener("input", handleChange);
761
860
  }
762
- itemWrapper.appendChild(inputContainer);
763
- if (index === -1) {
764
- container.appendChild(itemWrapper);
765
- } else {
766
- container.insertBefore(itemWrapper, container.children[index]);
861
+ if (!readonly) {
862
+ const rem = document.createElement("button");
863
+ rem.type = "button";
864
+ rem.className = "fb-chip-remove";
865
+ rem.setAttribute("aria-label", t("removeElement", state));
866
+ rem.innerHTML = BIN_ICON_SVG;
867
+ rem.onclick = () => {
868
+ const chips = list.querySelectorAll(".fb-chip");
869
+ const idx = Array.prototype.indexOf.call(chips, chip);
870
+ if (idx < 0) return;
871
+ if (chips.length <= minCount) return;
872
+ values.splice(idx, 1);
873
+ const trailingError = chip.nextElementSibling;
874
+ if (trailingError && trailingError.classList.contains("error-message")) {
875
+ trailingError.remove();
876
+ }
877
+ chip.remove();
878
+ updateIndices();
879
+ updateAddButton();
880
+ updateRemoveButtons();
881
+ };
882
+ chip.appendChild(rem);
767
883
  }
884
+ list.appendChild(chip);
768
885
  updateIndices();
769
- return itemWrapper;
886
+ return chip;
770
887
  }
771
888
  function updateRemoveButtons() {
772
889
  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";
890
+ const chipCount = list.querySelectorAll(".fb-chip").length;
891
+ const disabled = chipCount <= minCount;
892
+ list.querySelectorAll(".fb-chip-remove").forEach((btn) => {
893
+ btn.disabled = disabled;
813
894
  });
814
895
  }
815
896
  let addUpdate = null;
@@ -818,7 +899,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
818
899
  "text",
819
900
  () => {
820
901
  values.push(element.default || "");
821
- addTextItem(element.default || "");
902
+ addChip(element.default || "");
822
903
  updateAddButton();
823
904
  updateRemoveButtons();
824
905
  },
@@ -831,7 +912,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
831
912
  function updateAddButton() {
832
913
  if (addUpdate) addUpdate(values.length, maxCount);
833
914
  }
834
- values.forEach((value) => addTextItem(value));
915
+ values.forEach((value) => addChip(value));
835
916
  updateAddButton();
836
917
  updateRemoveButtons();
837
918
  }
@@ -840,7 +921,7 @@ function validateTextElement(element, key, context) {
840
921
  const errors = [];
841
922
  const { scopeRoot, skipValidation } = context;
842
923
  const markValidity = (input, errorMessage) => {
843
- var _a2, _b2;
924
+ var _a2, _b2, _c2;
844
925
  if (!input) return;
845
926
  const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
846
927
  let errorElement = document.getElementById(errorId);
@@ -856,10 +937,12 @@ function validateTextElement(element, key, context) {
856
937
  font-size: var(--fb-font-size-small);
857
938
  margin-top: 0.25rem;
858
939
  `;
859
- if (input.nextSibling) {
860
- (_a2 = input.parentNode) == null ? void 0 : _a2.insertBefore(errorElement, input.nextSibling);
940
+ const chipAncestor = (_a2 = input.closest) == null ? void 0 : _a2.call(input, ".fb-chip");
941
+ const anchor = chipAncestor || input;
942
+ if (anchor.nextSibling) {
943
+ (_b2 = anchor.parentNode) == null ? void 0 : _b2.insertBefore(errorElement, anchor.nextSibling);
861
944
  } else {
862
- (_b2 = input.parentNode) == null ? void 0 : _b2.appendChild(errorElement);
945
+ (_c2 = anchor.parentNode) == null ? void 0 : _c2.appendChild(errorElement);
863
946
  }
864
947
  }
865
948
  errorElement.textContent = errorMessage;
@@ -908,7 +991,7 @@ function validateTextElement(element, key, context) {
908
991
  }
909
992
  };
910
993
  if (element.multiple) {
911
- const inputs = scopeRoot.querySelectorAll(`[name^="${key}["]`);
994
+ const inputs = scopeRoot.querySelectorAll(`[name^="${key}\\["]`);
912
995
  const values = [];
913
996
  const rawValues = [];
914
997
  inputs.forEach((input, index) => {
@@ -958,12 +1041,14 @@ function updateTextField(element, fieldPath, value, context) {
958
1041
  );
959
1042
  return;
960
1043
  }
961
- const inputs = scopeRoot.querySelectorAll(`[name^="${fieldPath}["]`);
1044
+ const inputs = scopeRoot.querySelectorAll(`[name^="${fieldPath}\\["]`);
962
1045
  inputs.forEach((input, index) => {
963
1046
  if (index < value.length) {
964
1047
  input.value = value[index] != null ? String(value[index]) : "";
965
1048
  input.classList.remove("invalid");
966
1049
  input.title = "";
1050
+ clearFieldError(input);
1051
+ input.dispatchEvent(new Event("input", { bubbles: true }));
967
1052
  }
968
1053
  });
969
1054
  if (value.length !== inputs.length) {
@@ -977,26 +1062,15 @@ function updateTextField(element, fieldPath, value, context) {
977
1062
  input.value = value != null ? String(value) : "";
978
1063
  input.classList.remove("invalid");
979
1064
  input.title = "";
1065
+ clearFieldError(input);
1066
+ if (input instanceof HTMLTextAreaElement) {
1067
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1068
+ }
980
1069
  }
981
1070
  }
982
1071
  }
983
1072
 
984
1073
  // 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
1074
  function renderTextareaElement(element, ctx, wrapper, pathKey) {
1001
1075
  const state = ctx.state;
1002
1076
  const readonly = isElementReadonly(element, state, ctx);
@@ -1027,7 +1101,7 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
1027
1101
  }
1028
1102
  textareaWrapper.appendChild(textareaInput);
1029
1103
  if (!readonly && (element.minLength != null || element.maxLength != null)) {
1030
- const counter = createCharCounter(element, textareaInput, true);
1104
+ const counter = createCharCounter(element, textareaInput);
1031
1105
  textareaWrapper.appendChild(counter);
1032
1106
  }
1033
1107
  wrapper.appendChild(textareaWrapper);
@@ -1084,7 +1158,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1084
1158
  }
1085
1159
  textareaContainer.appendChild(textareaInput);
1086
1160
  if (!readonly && (element.minLength != null || element.maxLength != null)) {
1087
- const counter = createCharCounter(element, textareaInput, true);
1161
+ const counter = createCharCounter(element, textareaInput);
1088
1162
  textareaContainer.appendChild(counter);
1089
1163
  }
1090
1164
  itemWrapper.appendChild(textareaContainer);
@@ -1178,6 +1252,91 @@ function updateTextareaField(element, fieldPath, value, context) {
1178
1252
  }
1179
1253
 
1180
1254
  // src/components/number.ts
1255
+ function ensureStepperStyles(doc) {
1256
+ const ID = "fb-number-stepper-styles";
1257
+ if (doc.getElementById(ID)) return;
1258
+ const style = doc.createElement("style");
1259
+ style.id = ID;
1260
+ style.textContent = `
1261
+ .fb-stepper-input::-webkit-outer-spin-button,
1262
+ .fb-stepper-input::-webkit-inner-spin-button {
1263
+ -webkit-appearance: none;
1264
+ margin: 0;
1265
+ }
1266
+ .fb-stepper-input { -moz-appearance: textfield; }
1267
+ `;
1268
+ doc.head.appendChild(style);
1269
+ }
1270
+ function buildStepper(input, element, readonly) {
1271
+ var _a;
1272
+ ensureStepperStyles(input.ownerDocument);
1273
+ const step = (_a = element.step) != null ? _a : 1;
1274
+ const min = element.min;
1275
+ const max = element.max;
1276
+ const wrap = document.createElement("div");
1277
+ wrap.className = "fb-stepper";
1278
+ wrap.style.cssText = `
1279
+ display: inline-flex;
1280
+ align-items: stretch;
1281
+ border: var(--fb-border-width) solid var(--fb-border-color);
1282
+ border-radius: var(--fb-border-radius);
1283
+ overflow: hidden;
1284
+ background: var(--fb-background-color);
1285
+ `;
1286
+ const makeBtn = (label, delta) => {
1287
+ const b = document.createElement("button");
1288
+ b.type = "button";
1289
+ b.textContent = label;
1290
+ b.tabIndex = -1;
1291
+ b.style.cssText = `
1292
+ width: 32px;
1293
+ border: none;
1294
+ background: transparent;
1295
+ color: var(--fb-text-color);
1296
+ font-size: var(--fb-font-size);
1297
+ font-family: var(--fb-font-family);
1298
+ cursor: ${readonly ? "default" : "pointer"};
1299
+ user-select: none;
1300
+ `;
1301
+ if (readonly) {
1302
+ b.disabled = true;
1303
+ b.style.opacity = "0.5";
1304
+ } else {
1305
+ b.addEventListener("click", (e) => {
1306
+ var _a2;
1307
+ e.preventDefault();
1308
+ const current = parseFloat(input.value);
1309
+ const base = Number.isFinite(current) ? current : (_a2 = min != null ? min : element.default) != null ? _a2 : 0;
1310
+ let next = parseFloat((base + delta * step).toPrecision(12));
1311
+ if (min != null) next = Math.max(min, next);
1312
+ if (max != null) next = Math.min(max, next);
1313
+ input.value = String(next);
1314
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1315
+ input.dispatchEvent(new Event("change", { bubbles: true }));
1316
+ });
1317
+ }
1318
+ return b;
1319
+ };
1320
+ input.classList.add("fb-stepper-input");
1321
+ input.style.cssText = `
1322
+ width: 56px;
1323
+ border: none;
1324
+ border-left: var(--fb-border-width) solid var(--fb-border-color);
1325
+ border-right: var(--fb-border-width) solid var(--fb-border-color);
1326
+ padding: var(--fb-input-padding-y) 0;
1327
+ font-size: var(--fb-font-size);
1328
+ font-family: var(--fb-font-family);
1329
+ text-align: center;
1330
+ background: transparent;
1331
+ color: var(--fb-text-color);
1332
+ -moz-appearance: textfield;
1333
+ box-sizing: border-box;
1334
+ `;
1335
+ wrap.appendChild(makeBtn("\u2212", -1));
1336
+ wrap.appendChild(input);
1337
+ wrap.appendChild(makeBtn("+", 1));
1338
+ return wrap;
1339
+ }
1181
1340
  function createNumberRangeHint(element, input) {
1182
1341
  const hint = document.createElement("span");
1183
1342
  hint.className = "number-range-hint";
@@ -1224,14 +1383,6 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1224
1383
  inputWrapper.style.cssText = "position: relative;";
1225
1384
  const numberInput = document.createElement("input");
1226
1385
  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
1386
  numberInput.name = pathKey;
1236
1387
  numberInput.placeholder = element.placeholder || "0";
1237
1388
  if (element.min !== void 0) numberInput.min = element.min.toString();
@@ -1239,6 +1390,16 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1239
1390
  if (element.step !== void 0) numberInput.step = element.step.toString();
1240
1391
  numberInput.value = ctx.prefill[element.key] || element.default || "";
1241
1392
  numberInput.readOnly = readonly;
1393
+ if (!element.stepper) {
1394
+ numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1395
+ numberInput.style.cssText = `
1396
+ padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
1397
+ font-size: var(--fb-font-size);
1398
+ font-family: var(--fb-font-family);
1399
+ width: 100%;
1400
+ box-sizing: border-box;
1401
+ `;
1402
+ }
1242
1403
  if (!readonly && ctx.instance) {
1243
1404
  const handleChange = () => {
1244
1405
  const value = numberInput.value ? parseFloat(numberInput.value) : null;
@@ -1247,10 +1408,14 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1247
1408
  numberInput.addEventListener("blur", handleChange);
1248
1409
  numberInput.addEventListener("input", handleChange);
1249
1410
  }
1250
- inputWrapper.appendChild(numberInput);
1251
- if (!readonly && (element.min != null || element.max != null)) {
1252
- const counter = createNumberRangeHint(element, numberInput);
1253
- inputWrapper.appendChild(counter);
1411
+ if (element.stepper) {
1412
+ inputWrapper.appendChild(buildStepper(numberInput, element, readonly));
1413
+ } else {
1414
+ inputWrapper.appendChild(numberInput);
1415
+ if (!readonly && (element.min != null || element.max != null)) {
1416
+ const counter = createNumberRangeHint(element, numberInput);
1417
+ inputWrapper.appendChild(counter);
1418
+ }
1254
1419
  }
1255
1420
  wrapper.appendChild(inputWrapper);
1256
1421
  }
@@ -1508,13 +1673,14 @@ function updateNumberField(element, fieldPath, value, context) {
1508
1673
  return;
1509
1674
  }
1510
1675
  const inputs = scopeRoot.querySelectorAll(
1511
- `[name^="${fieldPath}["]`
1676
+ `[name^="${fieldPath}\\["]`
1512
1677
  );
1513
1678
  inputs.forEach((input, index) => {
1514
1679
  if (index < value.length) {
1515
1680
  input.value = value[index] != null ? String(value[index]) : "";
1516
1681
  input.classList.remove("invalid");
1517
1682
  input.title = "";
1683
+ clearFieldError(input);
1518
1684
  }
1519
1685
  });
1520
1686
  if (value.length !== inputs.length) {
@@ -1530,6 +1696,7 @@ function updateNumberField(element, fieldPath, value, context) {
1530
1696
  input.value = value != null ? String(value) : "";
1531
1697
  input.classList.remove("invalid");
1532
1698
  input.title = "";
1699
+ clearFieldError(input);
1533
1700
  }
1534
1701
  }
1535
1702
  }
@@ -1746,7 +1913,7 @@ function validateSelectElement(element, key, context) {
1746
1913
  };
1747
1914
  if ("multiple" in element && element.multiple) {
1748
1915
  const inputs = scopeRoot.querySelectorAll(
1749
- `[name^="${key}["]`
1916
+ `[name^="${key}\\["]`
1750
1917
  );
1751
1918
  const values = [];
1752
1919
  inputs.forEach((input) => {
@@ -1783,7 +1950,7 @@ function updateSelectField(element, fieldPath, value, context) {
1783
1950
  return;
1784
1951
  }
1785
1952
  const selects = scopeRoot.querySelectorAll(
1786
- `[name^="${fieldPath}["]`
1953
+ `[name^="${fieldPath}\\["]`
1787
1954
  );
1788
1955
  selects.forEach((select, index) => {
1789
1956
  if (index < value.length) {
@@ -1794,6 +1961,7 @@ function updateSelectField(element, fieldPath, value, context) {
1794
1961
  });
1795
1962
  select.classList.remove("invalid");
1796
1963
  select.title = "";
1964
+ clearFieldError(select);
1797
1965
  }
1798
1966
  });
1799
1967
  if (value.length !== selects.length) {
@@ -1813,72 +1981,147 @@ function updateSelectField(element, fieldPath, value, context) {
1813
1981
  });
1814
1982
  select.classList.remove("invalid");
1815
1983
  select.title = "";
1984
+ clearFieldError(select);
1816
1985
  }
1817
1986
  }
1818
1987
  }
1819
1988
 
1820
1989
  // 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)";
1990
+ function applySelectedStyle(btn, isPreset) {
1991
+ if (isPreset) {
1992
+ btn.style.backgroundColor = "var(--fb-primary-soft-color)";
1993
+ btn.style.color = "var(--fb-primary-color)";
1994
+ btn.style.borderColor = "var(--fb-primary-color)";
1995
+ } else {
1996
+ btn.style.backgroundColor = "var(--fb-primary-color)";
1997
+ btn.style.color = "#ffffff";
1998
+ btn.style.borderColor = "var(--fb-primary-color)";
1999
+ }
1825
2000
  }
1826
- function applyUnselectedStyle(btn) {
1827
- btn.style.backgroundColor = "transparent";
2001
+ function applyUnselectedStyle(btn, isPreset) {
2002
+ btn.style.backgroundColor = isPreset ? "var(--fb-background-color)" : "transparent";
1828
2003
  btn.style.color = "var(--fb-text-color)";
1829
2004
  btn.style.borderColor = "var(--fb-border-color)";
1830
2005
  }
2006
+ function isPresetButton(btn) {
2007
+ return btn.classList.contains("fb-switcher-preset");
2008
+ }
2009
+ function buildPresetCard(option, readonly) {
2010
+ const btn = document.createElement("button");
2011
+ btn.type = "button";
2012
+ btn.className = "fb-switcher-btn fb-switcher-preset";
2013
+ btn.dataset.value = option.value;
2014
+ btn.style.cssText = `
2015
+ display: inline-flex;
2016
+ align-items: center;
2017
+ gap: 8px;
2018
+ padding: 7px 12px 7px 10px;
2019
+ border-width: var(--fb-border-width);
2020
+ border-style: solid;
2021
+ border-radius: 999px;
2022
+ background: var(--fb-background-color);
2023
+ font-size: var(--fb-font-size);
2024
+ font-family: var(--fb-font-family);
2025
+ line-height: 1.25;
2026
+ cursor: ${readonly ? "default" : "pointer"};
2027
+ transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
2028
+ outline: none;
2029
+ `;
2030
+ if (option.iconUrl) {
2031
+ const icon = document.createElement("img");
2032
+ icon.className = "fb-switcher-icon";
2033
+ icon.src = option.iconUrl;
2034
+ icon.alt = "";
2035
+ icon.setAttribute("aria-hidden", "true");
2036
+ icon.style.cssText = `
2037
+ display: block;
2038
+ flex: 0 0 auto;
2039
+ width: 20px;
2040
+ height: 20px;
2041
+ object-fit: contain;
2042
+ `;
2043
+ btn.appendChild(icon);
2044
+ }
2045
+ const name = document.createElement("span");
2046
+ name.className = "fb-switcher-name";
2047
+ name.textContent = option.label;
2048
+ name.style.cssText = "font-weight: 600;";
2049
+ btn.appendChild(name);
2050
+ if (option.subtitle) {
2051
+ const sub = document.createElement("span");
2052
+ sub.className = "fb-switcher-subtitle";
2053
+ sub.textContent = option.subtitle;
2054
+ sub.style.cssText = `
2055
+ font-size: var(--fb-font-size-small);
2056
+ opacity: 0.7;
2057
+ font-variant-numeric: tabular-nums;
2058
+ `;
2059
+ btn.appendChild(sub);
2060
+ }
2061
+ return btn;
2062
+ }
1831
2063
  function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onChange) {
1832
2064
  const options = element.options || [];
2065
+ const isPresetMode = options.some((o) => o.subtitle || o.iconUrl);
1833
2066
  const group = document.createElement("div");
1834
2067
  group.className = "fb-switcher-group";
1835
- group.style.cssText = `
1836
- display: inline-flex;
1837
- flex-direction: row;
1838
- flex-wrap: nowrap;
1839
- `;
2068
+ group.style.cssText = isPresetMode ? `
2069
+ display: flex;
2070
+ flex-direction: row;
2071
+ flex-wrap: wrap;
2072
+ gap: 6px;
2073
+ ` : `
2074
+ display: inline-flex;
2075
+ flex-direction: row;
2076
+ flex-wrap: nowrap;
2077
+ `;
1840
2078
  const buttons = [];
1841
2079
  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";
2080
+ let btn;
2081
+ if (isPresetMode) {
2082
+ btn = buildPresetCard(option, readonly);
1865
2083
  } else {
1866
- btn.style.borderRadius = "0";
1867
- btn.style.borderRightWidth = "0";
2084
+ btn = document.createElement("button");
2085
+ btn.type = "button";
2086
+ btn.className = "fb-switcher-btn";
2087
+ btn.dataset.value = option.value;
2088
+ btn.textContent = option.label;
2089
+ btn.style.cssText = `
2090
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
2091
+ font-size: var(--fb-font-size);
2092
+ border-width: var(--fb-border-width);
2093
+ border-style: solid;
2094
+ cursor: ${readonly ? "default" : "pointer"};
2095
+ transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
2096
+ white-space: nowrap;
2097
+ line-height: 1.25;
2098
+ outline: none;
2099
+ `;
2100
+ if (options.length === 1) {
2101
+ btn.style.borderRadius = "var(--fb-border-radius)";
2102
+ } else if (index === 0) {
2103
+ btn.style.borderRadius = "var(--fb-border-radius) 0 0 var(--fb-border-radius)";
2104
+ btn.style.borderRightWidth = "0";
2105
+ } else if (index === options.length - 1) {
2106
+ btn.style.borderRadius = "0 var(--fb-border-radius) var(--fb-border-radius) 0";
2107
+ } else {
2108
+ btn.style.borderRadius = "0";
2109
+ btn.style.borderRightWidth = "0";
2110
+ }
1868
2111
  }
1869
2112
  if (option.value === currentValue) {
1870
- applySelectedStyle(btn);
2113
+ applySelectedStyle(btn, isPresetMode);
1871
2114
  } else {
1872
- applyUnselectedStyle(btn);
2115
+ applyUnselectedStyle(btn, isPresetMode);
1873
2116
  }
1874
2117
  if (!readonly) {
1875
2118
  btn.addEventListener("click", () => {
1876
2119
  hiddenInput.value = option.value;
1877
2120
  buttons.forEach((b) => {
1878
2121
  if (b.dataset.value === option.value) {
1879
- applySelectedStyle(b);
2122
+ applySelectedStyle(b, isPresetMode);
1880
2123
  } else {
1881
- applyUnselectedStyle(b);
2124
+ applyUnselectedStyle(b, isPresetMode);
1882
2125
  }
1883
2126
  });
1884
2127
  if (onChange) {
@@ -1892,7 +2135,7 @@ function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onCha
1892
2135
  });
1893
2136
  btn.addEventListener("mouseleave", () => {
1894
2137
  if (hiddenInput.value !== option.value) {
1895
- btn.style.backgroundColor = "transparent";
2138
+ btn.style.backgroundColor = isPresetMode ? "var(--fb-background-color)" : "transparent";
1896
2139
  }
1897
2140
  });
1898
2141
  }
@@ -2117,7 +2360,7 @@ function validateSwitcherElement(element, key, context) {
2117
2360
  );
2118
2361
  if ("multiple" in element && element.multiple) {
2119
2362
  const inputs = scopeRoot.querySelectorAll(
2120
- `input[type="hidden"][name^="${key}["]`
2363
+ `input[type="hidden"][name^="${key}\\["]`
2121
2364
  );
2122
2365
  const values = [];
2123
2366
  inputs.forEach((input) => {
@@ -2166,7 +2409,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
2166
2409
  return;
2167
2410
  }
2168
2411
  const inputs = scopeRoot.querySelectorAll(
2169
- `input[type="hidden"][name^="${fieldPath}["]`
2412
+ `input[type="hidden"][name^="${fieldPath}\\["]`
2170
2413
  );
2171
2414
  inputs.forEach((input, index) => {
2172
2415
  var _a2;
@@ -2176,15 +2419,17 @@ function updateSwitcherField(element, fieldPath, value, context) {
2176
2419
  const group = (_a2 = input.parentElement) == null ? void 0 : _a2.querySelector(".fb-switcher-group");
2177
2420
  if (group) {
2178
2421
  group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
2422
+ const isPreset = isPresetButton(btn);
2179
2423
  if (btn.dataset.value === newVal) {
2180
- applySelectedStyle(btn);
2424
+ applySelectedStyle(btn, isPreset);
2181
2425
  } else {
2182
- applyUnselectedStyle(btn);
2426
+ applyUnselectedStyle(btn, isPreset);
2183
2427
  }
2184
2428
  });
2185
2429
  }
2186
2430
  input.classList.remove("invalid");
2187
2431
  input.title = "";
2432
+ clearFieldError(input);
2188
2433
  }
2189
2434
  });
2190
2435
  if (value.length !== inputs.length) {
@@ -2202,19 +2447,215 @@ function updateSwitcherField(element, fieldPath, value, context) {
2202
2447
  const group = (_a = input.parentElement) == null ? void 0 : _a.querySelector(".fb-switcher-group");
2203
2448
  if (group) {
2204
2449
  group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
2450
+ const isPreset = isPresetButton(btn);
2205
2451
  if (btn.dataset.value === newVal) {
2206
- applySelectedStyle(btn);
2452
+ applySelectedStyle(btn, isPreset);
2207
2453
  } else {
2208
- applyUnselectedStyle(btn);
2454
+ applyUnselectedStyle(btn, isPreset);
2209
2455
  }
2210
2456
  });
2211
2457
  }
2212
2458
  input.classList.remove("invalid");
2213
2459
  input.title = "";
2460
+ clearFieldError(input);
2214
2461
  }
2215
2462
  }
2216
2463
  }
2217
2464
 
2465
+ // src/components/boolean.ts
2466
+ var TOGGLE_W = 36;
2467
+ var TOGGLE_H = 20;
2468
+ var KNOB = 16;
2469
+ function ensureStyles(doc) {
2470
+ const ID = "fb-boolean-styles";
2471
+ if (doc.getElementById(ID)) return;
2472
+ const style = doc.createElement("style");
2473
+ style.id = ID;
2474
+ style.textContent = `
2475
+ .fb-toggle {
2476
+ position: relative;
2477
+ display: inline-block;
2478
+ width: ${TOGGLE_W}px;
2479
+ height: ${TOGGLE_H}px;
2480
+ border-radius: ${TOGGLE_H}px;
2481
+ background: var(--fb-border-color);
2482
+ transition: background-color var(--fb-transition-duration);
2483
+ flex-shrink: 0;
2484
+ }
2485
+ .fb-toggle::after {
2486
+ content: "";
2487
+ position: absolute;
2488
+ top: ${(TOGGLE_H - KNOB) / 2}px;
2489
+ left: ${(TOGGLE_H - KNOB) / 2}px;
2490
+ width: ${KNOB}px;
2491
+ height: ${KNOB}px;
2492
+ border-radius: 50%;
2493
+ background: #ffffff;
2494
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
2495
+ transition: transform var(--fb-transition-duration);
2496
+ }
2497
+ .fb-toggle.fb-on {
2498
+ background: var(--fb-primary-color);
2499
+ }
2500
+ .fb-toggle.fb-on::after {
2501
+ transform: translateX(${TOGGLE_W - KNOB - (TOGGLE_H - KNOB)}px);
2502
+ }
2503
+ .fb-toggle-row {
2504
+ display: flex;
2505
+ align-items: center;
2506
+ gap: 12px;
2507
+ padding: 12px 14px;
2508
+ background: var(--fb-surface-soft-color);
2509
+ border: var(--fb-border-width) solid var(--fb-border-color);
2510
+ border-radius: var(--fb-border-radius);
2511
+ cursor: pointer;
2512
+ user-select: none;
2513
+ }
2514
+ .fb-toggle-row[aria-disabled="true"] {
2515
+ cursor: default;
2516
+ opacity: 0.7;
2517
+ }
2518
+ .fb-toggle-row:focus-visible {
2519
+ outline: var(--fb-focus-ring-width) solid var(--fb-focus-ring-color);
2520
+ outline-offset: var(--fb-focus-ring-offset);
2521
+ }
2522
+ .fb-toggle-text { flex: 1; min-width: 0; }
2523
+ .fb-toggle-title {
2524
+ display: flex;
2525
+ align-items: center;
2526
+ gap: 4px;
2527
+ font-size: var(--fb-font-size);
2528
+ font-weight: 500;
2529
+ color: var(--fb-text-color);
2530
+ line-height: 1.3;
2531
+ }
2532
+ .fb-toggle-subtitle {
2533
+ font-size: var(--fb-font-size-small);
2534
+ color: var(--fb-text-secondary-color);
2535
+ margin-top: 2px;
2536
+ line-height: 1.35;
2537
+ }
2538
+ .fb-toggle-info {
2539
+ flex: 0 0 14px;
2540
+ display: inline-flex;
2541
+ align-items: center;
2542
+ justify-content: center;
2543
+ width: 14px;
2544
+ height: 14px;
2545
+ border-radius: 50%;
2546
+ background: var(--fb-border-color);
2547
+ color: #fff;
2548
+ font-size: 10px;
2549
+ font-weight: 700;
2550
+ font-style: italic;
2551
+ font-family: serif;
2552
+ cursor: help;
2553
+ }
2554
+ `;
2555
+ doc.head.appendChild(style);
2556
+ }
2557
+ function parseBool(v) {
2558
+ if (typeof v === "boolean") return v;
2559
+ if (typeof v === "string") return v === "true" || v === "on" || v === "1";
2560
+ return false;
2561
+ }
2562
+ function renderBooleanElement(element, ctx, wrapper, pathKey) {
2563
+ var _a;
2564
+ ensureStyles(document);
2565
+ const state = ctx.state;
2566
+ const readonly = isElementReadonly(element, state, ctx);
2567
+ const prefillRaw = ctx.prefill[element.key];
2568
+ const initial = prefillRaw !== void 0 ? parseBool(prefillRaw) : parseBool(element.default);
2569
+ const hiddenInput = document.createElement("input");
2570
+ hiddenInput.type = "hidden";
2571
+ hiddenInput.name = pathKey;
2572
+ hiddenInput.value = initial ? "true" : "false";
2573
+ const row = document.createElement("div");
2574
+ row.className = "fb-toggle-row";
2575
+ row.setAttribute("role", "switch");
2576
+ row.setAttribute("aria-checked", initial ? "true" : "false");
2577
+ if (readonly) {
2578
+ row.setAttribute("aria-disabled", "true");
2579
+ } else {
2580
+ row.tabIndex = 0;
2581
+ }
2582
+ const pill = document.createElement("span");
2583
+ pill.className = "fb-toggle" + (initial ? " fb-on" : "");
2584
+ pill.setAttribute("aria-hidden", "true");
2585
+ row.appendChild(pill);
2586
+ const textBlock = document.createElement("div");
2587
+ textBlock.className = "fb-toggle-text";
2588
+ const titleEl = document.createElement("div");
2589
+ titleEl.className = "fb-toggle-title";
2590
+ titleEl.appendChild(document.createTextNode((_a = element.label) != null ? _a : ""));
2591
+ if (element.description) {
2592
+ const info = document.createElement("span");
2593
+ info.className = "fb-toggle-info";
2594
+ info.textContent = "i";
2595
+ info.title = element.description;
2596
+ titleEl.appendChild(info);
2597
+ }
2598
+ textBlock.appendChild(titleEl);
2599
+ if (element.hint) {
2600
+ const subtitle = document.createElement("div");
2601
+ subtitle.className = "fb-toggle-subtitle";
2602
+ subtitle.textContent = element.hint;
2603
+ textBlock.appendChild(subtitle);
2604
+ }
2605
+ row.appendChild(textBlock);
2606
+ if (!readonly) {
2607
+ const toggle = () => {
2608
+ const next = hiddenInput.value !== "true";
2609
+ hiddenInput.value = next ? "true" : "false";
2610
+ pill.classList.toggle("fb-on", next);
2611
+ row.setAttribute("aria-checked", next ? "true" : "false");
2612
+ if (ctx.instance) ctx.instance.triggerOnChange(pathKey, next);
2613
+ };
2614
+ row.addEventListener("click", (e) => {
2615
+ var _a2;
2616
+ if ((_a2 = e.target) == null ? void 0 : _a2.classList.contains("fb-toggle-info")) {
2617
+ return;
2618
+ }
2619
+ toggle();
2620
+ });
2621
+ row.addEventListener("keydown", (e) => {
2622
+ if (e.key === " " || e.key === "Enter") {
2623
+ e.preventDefault();
2624
+ toggle();
2625
+ }
2626
+ });
2627
+ }
2628
+ wrapper.appendChild(hiddenInput);
2629
+ wrapper.appendChild(row);
2630
+ }
2631
+ function validateBooleanElement(element, key, context) {
2632
+ var _a;
2633
+ const { scopeRoot } = context;
2634
+ const input = scopeRoot.querySelector(
2635
+ `input[type="hidden"][name="${key}"]`
2636
+ );
2637
+ const raw = (_a = input == null ? void 0 : input.value) != null ? _a : "";
2638
+ const value = parseBool(raw);
2639
+ const errors = [];
2640
+ return { value, errors };
2641
+ }
2642
+ function updateBooleanField(_element, fieldPath, value, context) {
2643
+ var _a;
2644
+ const { scopeRoot } = context;
2645
+ const input = scopeRoot.querySelector(
2646
+ `input[type="hidden"][name="${fieldPath}"]`
2647
+ );
2648
+ if (!input) return;
2649
+ const bool = parseBool(value);
2650
+ input.value = bool ? "true" : "false";
2651
+ const row = (_a = input.parentElement) == null ? void 0 : _a.querySelector(".fb-toggle-row");
2652
+ if (row) {
2653
+ row.setAttribute("aria-checked", bool ? "true" : "false");
2654
+ const pill = row.querySelector(".fb-toggle");
2655
+ if (pill) pill.classList.toggle("fb-on", bool);
2656
+ }
2657
+ }
2658
+
2218
2659
  // src/components/file/constraints.ts
2219
2660
  function getAllowedExtensions(accept) {
2220
2661
  if (!accept) return [];
@@ -2317,14 +2758,20 @@ function ensureFileStyles() {
2317
2758
  }
2318
2759
 
2319
2760
  /* \u2500\u2500\u2500 Wide single-file add tile (empty state) \u2500\u2500\u2500 */
2761
+ /* Flex-wraps: side-by-side when wide enough, stacks upload/library
2762
+ vertically when narrow (e.g. inside a 50/50 container column). */
2320
2763
  .fb-wide-tile {
2321
2764
  width: 100%;
2765
+ box-sizing: border-box;
2322
2766
  border-radius: 0.75rem;
2323
2767
  border: 1px dashed #60a5fa;
2324
2768
  background: rgba(239,246,255,0.5);
2325
2769
  display: flex;
2770
+ flex-wrap: wrap;
2771
+ align-items: stretch;
2772
+ gap: 0;
2326
2773
  overflow: hidden;
2327
- height: 180px;
2774
+ min-height: 180px;
2328
2775
  transition: border-color 150ms, background 150ms, box-shadow 150ms;
2329
2776
  cursor: pointer;
2330
2777
  }
@@ -2338,9 +2785,12 @@ function ensureFileStyles() {
2338
2785
  box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
2339
2786
  }
2340
2787
 
2341
- /* Upload zone inside wide tile */
2788
+ /* Upload zone inside wide tile.
2789
+ flex: 1 1 220px \u2014 wants at least 220px; if the container can't fit
2790
+ upload + library on one row (~220 + 176), library wraps below. */
2342
2791
  .fb-wide-tile-upload {
2343
- flex: 1;
2792
+ flex: 1 1 220px;
2793
+ min-height: 140px;
2344
2794
  display: flex;
2345
2795
  flex-direction: column;
2346
2796
  align-items: center;
@@ -2353,24 +2803,21 @@ function ensureFileStyles() {
2353
2803
  background: transparent;
2354
2804
  border: none;
2355
2805
  font-family: inherit;
2806
+ /* Dashed separator from library: right side when in a row, bottom when
2807
+ wrapped (the line then sits between the two stacked cards). */
2808
+ border-right: 1px dashed rgba(96,165,250,0.5);
2356
2809
  }
2357
2810
  .fb-wide-tile-upload:hover {
2358
2811
  background: rgba(191,219,254,0.25);
2359
2812
  }
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 */
2813
+ /* Library zone inside wide tile.
2814
+ flex: 0 0 176px \u2014 fixed 176px, never grows. Upload fills the rest in
2815
+ row layout. When the tile wraps to two rows on narrow containers,
2816
+ library stays 176px wide on its own row (left-aligned), preserving the
2817
+ visual hierarchy "upload > library" in both layouts. */
2371
2818
  .fb-wide-tile-library {
2372
- width: 176px;
2373
- flex-shrink: 0;
2819
+ flex: 0 0 176px;
2820
+ min-height: 120px;
2374
2821
  display: flex;
2375
2822
  flex-direction: column;
2376
2823
  align-items: center;
@@ -2387,6 +2834,10 @@ function ensureFileStyles() {
2387
2834
  .fb-wide-tile-library:hover {
2388
2835
  background: rgba(191,219,254,0.25);
2389
2836
  }
2837
+ /* Narrow-tile mode lives in a separate <style> tag (see below) \u2014 the
2838
+ @container rule is appended only when the runtime actually supports
2839
+ container queries, so jsdom (which doesn't) never sees it and stays
2840
+ quiet in test logs. */
2390
2841
 
2391
2842
  /* \u2500\u2500\u2500 Multi-file outer grid container \u2500\u2500\u2500 */
2392
2843
  .fb-multi-outer {
@@ -2765,6 +3216,39 @@ function ensureFileStyles() {
2765
3216
  }
2766
3217
  `;
2767
3218
  document.head.appendChild(style);
3219
+ if (typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("container-type", "inline-size")) {
3220
+ const cq = document.createElement("style");
3221
+ cq.setAttribute("data-fb-file-styles-cq", "true");
3222
+ cq.textContent = `
3223
+ .fb-wide-tile { container-type: inline-size; }
3224
+ @container (max-width: 408px) {
3225
+ .fb-wide-tile-upload {
3226
+ border-right: none;
3227
+ border-bottom: 1px dashed rgba(96,165,250,0.5);
3228
+ }
3229
+ .fb-wide-tile-library {
3230
+ flex: 1 0 100%;
3231
+ min-height: 0;
3232
+ flex-direction: row;
3233
+ gap: 6px;
3234
+ padding: 8px 12px;
3235
+ font-size: 12px;
3236
+ }
3237
+ .fb-wide-tile-library .fb-wide-tile-library-icon {
3238
+ width: 16px;
3239
+ height: 16px;
3240
+ }
3241
+ .fb-wide-tile-library .fb-wide-tile-library-label {
3242
+ font-size: 12px;
3243
+ font-weight: 500;
3244
+ }
3245
+ .fb-wide-tile-library .fb-wide-tile-library-hint {
3246
+ display: none;
3247
+ }
3248
+ }
3249
+ `;
3250
+ document.head.appendChild(cq);
3251
+ }
2768
3252
  }
2769
3253
 
2770
3254
  // src/components/file/dom.ts
@@ -4134,21 +4618,21 @@ function buildWideTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragO
4134
4618
  };
4135
4619
  outer.appendChild(uploadBtn);
4136
4620
  if (hasLibrary && onLibraryClick) {
4137
- const divider = document.createElement("div");
4138
- divider.className = "fb-wide-tile-divider";
4139
- outer.appendChild(divider);
4140
4621
  const libBtn = document.createElement("button");
4141
4622
  libBtn.type = "button";
4142
4623
  libBtn.className = "fb-wide-tile-library fb-file-library-card";
4143
4624
  const libIcon = document.createElement("span");
4625
+ libIcon.className = "fb-wide-tile-library-icon";
4144
4626
  libIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
4145
4627
  libIcon.innerHTML = ICON_LIBRARY2;
4146
4628
  libBtn.appendChild(libIcon);
4147
4629
  const libLabel = document.createElement("div");
4630
+ libLabel.className = "fb-wide-tile-library-label";
4148
4631
  libLabel.style.cssText = "font-size:13px;font-weight:600;text-align:center;";
4149
4632
  libLabel.textContent = t("fromLibrary", state);
4150
4633
  libBtn.appendChild(libLabel);
4151
4634
  const libHint = document.createElement("div");
4635
+ libHint.className = "fb-wide-tile-library-hint";
4152
4636
  libHint.style.cssText = "font-size:11px;opacity:0.75;text-align:center;";
4153
4637
  libHint.textContent = t("libraryHint", state);
4154
4638
  libBtn.appendChild(libHint);
@@ -5411,7 +5895,7 @@ function validateColourElement(element, key, context) {
5411
5895
  };
5412
5896
  if (element.multiple) {
5413
5897
  const hexInputs = scopeRoot.querySelectorAll(
5414
- `[name^="${key}["].colour-hex-input`
5898
+ `[name^="${key}\\["].colour-hex-input`
5415
5899
  );
5416
5900
  const values = [];
5417
5901
  hexInputs.forEach((input, index) => {
@@ -5461,7 +5945,7 @@ function updateColourField(element, fieldPath, value, context) {
5461
5945
  return;
5462
5946
  }
5463
5947
  const hexInputs = scopeRoot.querySelectorAll(
5464
- `[name^="${fieldPath}["].colour-hex-input`
5948
+ `[name^="${fieldPath}\\["].colour-hex-input`
5465
5949
  );
5466
5950
  hexInputs.forEach((hexInput, index) => {
5467
5951
  if (index < value.length) {
@@ -5469,6 +5953,7 @@ function updateColourField(element, fieldPath, value, context) {
5469
5953
  hexInput.value = normalized;
5470
5954
  hexInput.classList.remove("invalid");
5471
5955
  hexInput.title = "";
5956
+ clearFieldError(hexInput);
5472
5957
  const wrapper = hexInput.closest(".colour-picker-wrapper");
5473
5958
  if (wrapper) {
5474
5959
  const swatch = wrapper.querySelector(".colour-swatch");
@@ -5498,6 +5983,7 @@ function updateColourField(element, fieldPath, value, context) {
5498
5983
  hexInput.value = normalized;
5499
5984
  hexInput.classList.remove("invalid");
5500
5985
  hexInput.title = "";
5986
+ clearFieldError(hexInput);
5501
5987
  const wrapper = hexInput.closest(".colour-picker-wrapper");
5502
5988
  if (wrapper) {
5503
5989
  const swatch = wrapper.querySelector(".colour-swatch");
@@ -5905,7 +6391,7 @@ function validateSliderElement(element, key, context) {
5905
6391
  };
5906
6392
  if (element.multiple) {
5907
6393
  const sliders = scopeRoot.querySelectorAll(
5908
- `input[type="range"][name^="${key}["]`
6394
+ `input[type="range"][name^="${key}\\["]`
5909
6395
  );
5910
6396
  const values = [];
5911
6397
  sliders.forEach((slider, index) => {
@@ -5957,7 +6443,7 @@ function updateSliderField(element, fieldPath, value, context) {
5957
6443
  return;
5958
6444
  }
5959
6445
  const sliders = scopeRoot.querySelectorAll(
5960
- `input[type="range"][name^="${fieldPath}["]`
6446
+ `input[type="range"][name^="${fieldPath}\\["]`
5961
6447
  );
5962
6448
  sliders.forEach((slider, index) => {
5963
6449
  if (index < value.length && value[index] !== null) {
@@ -5985,6 +6471,7 @@ function updateSliderField(element, fieldPath, value, context) {
5985
6471
  }
5986
6472
  slider.classList.remove("invalid");
5987
6473
  slider.title = "";
6474
+ clearFieldError(slider);
5988
6475
  }
5989
6476
  });
5990
6477
  if (value.length !== sliders.length) {
@@ -6021,6 +6508,7 @@ function updateSliderField(element, fieldPath, value, context) {
6021
6508
  }
6022
6509
  slider.classList.remove("invalid");
6023
6510
  slider.title = "";
6511
+ clearFieldError(slider);
6024
6512
  }
6025
6513
  }
6026
6514
  }
@@ -6145,34 +6633,24 @@ function getChildWrapperClass(isSlides, columns) {
6145
6633
  const cols = columns || 1;
6146
6634
  return cols === 1 ? "space-y-2" : `grid grid-cols-${cols} gap-2`;
6147
6635
  }
6148
- function mountRemoveButton(item, onRemove) {
6636
+ function mountRemoveButton(item, onRemove, state) {
6149
6637
  const rem = document.createElement("button");
6150
6638
  rem.type = "button";
6151
6639
  rem.className = "fb-item-remove";
6640
+ rem.setAttribute("aria-label", t("removeElement", state));
6152
6641
  rem.style.cssText = `
6153
- width: 22px;
6154
- height: 22px;
6642
+ width: 24px;
6643
+ height: 24px;
6155
6644
  display: inline-flex;
6156
6645
  align-items: center;
6157
6646
  justify-content: center;
6158
6647
  padding: 0;
6159
- line-height: 1;
6160
- font-size: 14px;
6161
- color: var(--fb-error-color);
6162
- background-color: transparent;
6163
6648
  border: 0;
6164
6649
  border-radius: 4px;
6165
6650
  cursor: pointer;
6166
6651
  flex-shrink: 0;
6167
- transition: background-color var(--fb-transition-duration);
6168
6652
  `;
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
- });
6653
+ rem.innerHTML = BIN_ICON_SVG;
6176
6654
  rem.onclick = onRemove;
6177
6655
  const labelRow = item.querySelector("[data-fb-label-row]");
6178
6656
  if (labelRow) {
@@ -6199,7 +6677,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6199
6677
  itemsWrap.className = "fb-container-slides";
6200
6678
  const slideCols = element.columns;
6201
6679
  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;`;
6680
+ itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:var(--fb-slides-gap, 14px);align-items:start;`;
6203
6681
  } else {
6204
6682
  itemsWrap.className = "space-y-2";
6205
6683
  }
@@ -6230,6 +6708,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6230
6708
  const item = document.createElement("div");
6231
6709
  item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6232
6710
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6711
+ if (isSlides) {
6712
+ item.setAttribute("data-fb-slide-card", "");
6713
+ }
6233
6714
  const childWrapper = document.createElement("div");
6234
6715
  childWrapper.className = getChildWrapperClass(isSlides, element.columns);
6235
6716
  element.elements.forEach((child) => {
@@ -6247,7 +6728,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6247
6728
  });
6248
6729
  item.appendChild(childWrapper);
6249
6730
  if (!containerIsReadonly) {
6250
- mountRemoveButton(item, () => handleRemoveItem(item));
6731
+ mountRemoveButton(item, () => handleRemoveItem(item), state);
6251
6732
  }
6252
6733
  if (slideAddTile && slideAddTile.parentElement === itemsWrap) {
6253
6734
  itemsWrap.insertBefore(item, slideAddTile);
@@ -6294,6 +6775,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6294
6775
  const item = document.createElement("div");
6295
6776
  item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6296
6777
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6778
+ if (isSlides) {
6779
+ item.setAttribute("data-fb-slide-card", "");
6780
+ }
6297
6781
  const childWrapper = document.createElement("div");
6298
6782
  if (isSlides) {
6299
6783
  childWrapper.className = "space-y-2";
@@ -6318,7 +6802,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6318
6802
  });
6319
6803
  item.appendChild(childWrapper);
6320
6804
  if (!containerIsReadonly) {
6321
- mountRemoveButton(item, () => handleRemoveItem(item));
6805
+ mountRemoveButton(item, () => handleRemoveItem(item), ctx.state);
6322
6806
  }
6323
6807
  itemsWrap.appendChild(item);
6324
6808
  });
@@ -6338,6 +6822,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6338
6822
  const item = document.createElement("div");
6339
6823
  item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6340
6824
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6825
+ if (isSlides) {
6826
+ item.setAttribute("data-fb-slide-card", "");
6827
+ }
6341
6828
  const childWrapper = document.createElement("div");
6342
6829
  if (isSlides) {
6343
6830
  childWrapper.className = "space-y-2";
@@ -6363,11 +6850,15 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6363
6850
  }
6364
6851
  });
6365
6852
  item.appendChild(childWrapper);
6366
- mountRemoveButton(item, () => {
6367
- if (countItems() > min) {
6368
- handleRemoveItem(item);
6369
- }
6370
- });
6853
+ mountRemoveButton(
6854
+ item,
6855
+ () => {
6856
+ if (countItems() > min) {
6857
+ handleRemoveItem(item);
6858
+ }
6859
+ },
6860
+ ctx.state
6861
+ );
6371
6862
  itemsWrap.appendChild(item);
6372
6863
  }
6373
6864
  }
@@ -9060,7 +9551,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
9060
9551
  if (element.minLength != null || element.maxLength != null) {
9061
9552
  const counterRow = document.createElement("div");
9062
9553
  counterRow.style.cssText = "position: relative; padding: 2px 10px 4px; text-align: right;";
9063
- const counter = createCharCounter(element, textarea, false);
9554
+ const counter = createCharCounter(element, textarea);
9064
9555
  counter.style.cssText = `
9065
9556
  position: static;
9066
9557
  display: inline-block;
@@ -9592,6 +10083,118 @@ function validateMarkdown(_element, _key, _context) {
9592
10083
  function updateMarkdown(_element, _fieldPath, _value, _context) {
9593
10084
  }
9594
10085
 
10086
+ // src/components/registry.ts
10087
+ function validateHiddenElement(element, key, context) {
10088
+ var _a;
10089
+ const { scopeRoot } = context;
10090
+ const input = scopeRoot.querySelector(
10091
+ `input[type="hidden"][data-hidden-field="true"][name="${key}"]`
10092
+ );
10093
+ const raw = (_a = input == null ? void 0 : input.value) != null ? _a : "";
10094
+ if (raw === "") {
10095
+ const defaultVal = "default" in element ? element.default : null;
10096
+ return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
10097
+ }
10098
+ return { value: deserializeHiddenValue(raw), errors: [] };
10099
+ }
10100
+ function updateHiddenField(_element, fieldPath, value, context) {
10101
+ const { scopeRoot } = context;
10102
+ const input = scopeRoot.querySelector(
10103
+ `input[type="hidden"][data-hidden-field="true"][name="${fieldPath}"]`
10104
+ );
10105
+ if (!input) return;
10106
+ input.value = serializeHiddenValue(value);
10107
+ }
10108
+ var componentRegistry = {
10109
+ text: {
10110
+ validate: validateTextElement,
10111
+ update: updateTextField
10112
+ },
10113
+ textarea: {
10114
+ validate: validateTextareaElement,
10115
+ update: updateTextareaField
10116
+ },
10117
+ number: {
10118
+ validate: validateNumberElement,
10119
+ update: updateNumberField
10120
+ },
10121
+ select: {
10122
+ validate: validateSelectElement,
10123
+ update: updateSelectField
10124
+ },
10125
+ switcher: {
10126
+ validate: validateSwitcherElement,
10127
+ update: updateSwitcherField
10128
+ },
10129
+ boolean: {
10130
+ validate: validateBooleanElement,
10131
+ update: updateBooleanField,
10132
+ ownsLabel: true
10133
+ },
10134
+ file: {
10135
+ validate: validateFileElement,
10136
+ update: updateFileField
10137
+ },
10138
+ files: {
10139
+ // Legacy type - delegates to file
10140
+ validate: validateFileElement,
10141
+ update: updateFileField
10142
+ },
10143
+ colour: {
10144
+ validate: validateColourElement,
10145
+ update: updateColourField
10146
+ },
10147
+ slider: {
10148
+ validate: validateSliderElement,
10149
+ update: updateSliderField
10150
+ },
10151
+ container: {
10152
+ validate: validateContainerElement,
10153
+ update: updateContainerField
10154
+ },
10155
+ group: {
10156
+ // Deprecated type - delegates to container
10157
+ validate: validateGroupElement,
10158
+ update: updateGroupField
10159
+ },
10160
+ table: {
10161
+ validate: validateTableElement,
10162
+ update: updateTableField
10163
+ },
10164
+ richinput: {
10165
+ validate: validateRichInputElement,
10166
+ update: updateRichInputField
10167
+ },
10168
+ hidden: {
10169
+ // Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
10170
+ validate: validateHiddenElement,
10171
+ update: updateHiddenField
10172
+ },
10173
+ markdown: {
10174
+ // Display-only element — no value, no errors, skip from form data
10175
+ validate: validateMarkdown,
10176
+ update: updateMarkdown
10177
+ }
10178
+ };
10179
+ function getComponentOperations(elementType) {
10180
+ return componentRegistry[elementType] || null;
10181
+ }
10182
+ function validateElementWithComponent(element, key, context) {
10183
+ const ops = getComponentOperations(element.type);
10184
+ if (ops && ops.validate) {
10185
+ return ops.validate(element, key, context);
10186
+ }
10187
+ return null;
10188
+ }
10189
+ function updateElementWithComponent(element, fieldPath, value, context) {
10190
+ const ops = getComponentOperations(element.type);
10191
+ if (ops && ops.update) {
10192
+ ops.update(element, fieldPath, value, context);
10193
+ return true;
10194
+ }
10195
+ return false;
10196
+ }
10197
+
9595
10198
  // src/components/index.ts
9596
10199
  function showTooltip(tooltipId, button) {
9597
10200
  const tooltip = document.getElementById(tooltipId);
@@ -9904,6 +10507,9 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
9904
10507
  renderSwitcherElement(element, ctx, wrapper, pathKey);
9905
10508
  }
9906
10509
  break;
10510
+ case "boolean":
10511
+ renderBooleanElement(element, ctx, wrapper, pathKey);
10512
+ break;
9907
10513
  case "file":
9908
10514
  if (isMultiple) {
9909
10515
  renderMultipleFileElement(element, ctx, wrapper, pathKey);
@@ -9982,8 +10588,11 @@ function renderElement2(element, ctx) {
9982
10588
  const wrapper = document.createElement("div");
9983
10589
  wrapper.className = "mb-2 fb-field-wrapper";
9984
10590
  wrapper.setAttribute("data-field-key", element.key);
9985
- const label = createLabelContainer(element);
9986
- wrapper.appendChild(label);
10591
+ const ops = getComponentOperations(element.type);
10592
+ if (!(ops == null ? void 0 : ops.ownsLabel)) {
10593
+ const label = createLabelContainer(element);
10594
+ wrapper.appendChild(label);
10595
+ }
9987
10596
  const pathKey = pathJoin(ctx.path, element.key);
9988
10597
  dispatchToRenderer(element, ctx, wrapper, pathKey);
9989
10598
  if (initiallyDisabled) {
@@ -10216,28 +10825,52 @@ var defaultTheme = {
10216
10825
  // blue-500
10217
10826
  primaryHoverColor: "#2563eb",
10218
10827
  // blue-600
10828
+ primarySoftColor: "#dbeafe",
10829
+ // blue-100
10830
+ primarySoftHoverColor: "#bfdbfe",
10831
+ // blue-200
10219
10832
  errorColor: "#ef4444",
10220
10833
  // red-500
10221
10834
  errorHoverColor: "#dc2626",
10222
10835
  // red-600
10223
10836
  successColor: "#10b981",
10224
10837
  // green-500
10838
+ accentColor: "#f59e0b",
10839
+ // amber-500
10840
+ accentSoftColor: "#fef3c7",
10841
+ // amber-100
10842
+ accentBorderColor: "#fde68a",
10843
+ // amber-200
10844
+ accentTextColor: "#92400e",
10845
+ // amber-800
10225
10846
  borderColor: "#d1d5db",
10226
10847
  // gray-300
10227
10848
  borderHoverColor: "#9ca3af",
10228
10849
  // gray-400
10229
10850
  borderFocusColor: "#3b82f6",
10230
10851
  // blue-500
10852
+ borderStrongColor: "#9ca3af",
10853
+ // gray-400
10231
10854
  backgroundColor: "#ffffff",
10232
10855
  // white
10233
10856
  backgroundHoverColor: "#f9fafb",
10234
10857
  // gray-50
10235
10858
  backgroundReadonlyColor: "#f3f4f6",
10236
10859
  // gray-100
10860
+ pageBackgroundColor: "#f9fafb",
10861
+ // gray-50
10862
+ surfaceSoftColor: "#eff6ff",
10863
+ // blue-50
10864
+ surfaceTintColor: "#f8fafc",
10865
+ // slate-50
10237
10866
  textColor: "#1f2937",
10238
10867
  // gray-800
10239
10868
  textSecondaryColor: "#6b7280",
10240
10869
  // gray-500
10870
+ textMutedColor: "#9ca3af",
10871
+ // gray-400
10872
+ textFaintColor: "#cbd5e1",
10873
+ // slate-300
10241
10874
  textPlaceholderColor: "#9ca3af",
10242
10875
  // gray-400
10243
10876
  textDisabledColor: "#d1d5db",
@@ -10280,6 +10913,12 @@ var defaultTheme = {
10280
10913
  // 4px (compact density v2)
10281
10914
  borderRadius: "0.5rem",
10282
10915
  // rounded-lg (8px)
10916
+ borderRadiusSmall: "0.375rem",
10917
+ // 6px
10918
+ borderRadiusLarge: "0.75rem",
10919
+ // 12px
10920
+ borderRadiusXLarge: "1rem",
10921
+ // 16px
10283
10922
  borderWidth: "1px",
10284
10923
  // Typography
10285
10924
  fontSize: "0.875rem",
@@ -10291,13 +10930,31 @@ var defaultTheme = {
10291
10930
  fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
10292
10931
  fontWeightNormal: "400",
10293
10932
  fontWeightMedium: "500",
10933
+ lineHeight: "1.5",
10934
+ // Shadows
10935
+ shadowCard: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
10936
+ shadowPopover: "0 12px 32px -12px rgba(15,23,42,.18), 0 4px 12px -6px rgba(15,23,42,.08)",
10294
10937
  // Focus ring
10295
10938
  focusRingWidth: "2px",
10296
10939
  focusRingColor: "#3b82f6",
10297
10940
  // blue-500
10298
10941
  focusRingOpacity: "0.5",
10299
10942
  // Transitions
10300
- transitionDuration: "200ms"
10943
+ transitionDuration: "200ms",
10944
+ // Slide-card defaults — flat-white to match every other item card. The
10945
+ // Picaz theme overrides this with a gradient + shadow to lift the slides.
10946
+ slideCardBg: "#ffffff",
10947
+ slideCardShadow: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
10948
+ slideCardRadius: "0.5rem",
10949
+ // matches borderRadius
10950
+ slideCardMinHeight: "0",
10951
+ slideCardPadding: "12px",
10952
+ // Section-label defaults — same look as the regular field label. Themes
10953
+ // that want the "ПРЕИМУЩЕСТВА" caps style override these three vars.
10954
+ labelSectionFontSize: "0.875rem",
10955
+ // matches fontSize
10956
+ labelSectionLetterSpacing: "normal",
10957
+ labelSectionTextTransform: "none"
10301
10958
  };
10302
10959
  function generateCSSVariables(theme) {
10303
10960
  const mergedTheme = { ...defaultTheme, ...theme };
@@ -10309,6 +10966,7 @@ function generateCSSVariables(theme) {
10309
10966
  return cssVars.join("\n");
10310
10967
  }
10311
10968
  function injectThemeVariables(container, theme) {
10969
+ ensureThemingHooks(container.ownerDocument || document);
10312
10970
  const cssVariables = generateCSSVariables(theme);
10313
10971
  let styleTag = container.querySelector(
10314
10972
  "style[data-fb-theme]"
@@ -10371,115 +11029,50 @@ var exampleThemes = {
10371
11029
  fontSize: "16px",
10372
11030
  fontSizeSmall: "14px",
10373
11031
  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
11032
  },
10459
- markdown: {
10460
- // Display-only element no value, no errors, skip from form data
10461
- validate: validateMarkdown,
10462
- update: updateMarkdown
11033
+ // Picaz wizard design tokens — derived from the Picaz Wizard mockups.
11034
+ // Pairs with the host-side .card / .section-num / .lede chrome that wraps the form.
11035
+ picaz: {
11036
+ ...defaultTheme,
11037
+ primaryColor: "#2f5bea",
11038
+ primaryHoverColor: "#2349c8",
11039
+ primarySoftColor: "#eaf0ff",
11040
+ primarySoftHoverColor: "#d6e0ff",
11041
+ errorColor: "#ef4444",
11042
+ successColor: "#16a34a",
11043
+ accentColor: "#ffb020",
11044
+ accentSoftColor: "#fff7e6",
11045
+ accentBorderColor: "#fde7b5",
11046
+ accentTextColor: "#92400e",
11047
+ borderColor: "#e3e8f0",
11048
+ borderHoverColor: "#cdd6e3",
11049
+ borderFocusColor: "#2f5bea",
11050
+ borderStrongColor: "#cdd6e3",
11051
+ backgroundColor: "#ffffff",
11052
+ backgroundHoverColor: "#f3f7ff",
11053
+ pageBackgroundColor: "#f6f8fb",
11054
+ surfaceSoftColor: "#eef4ff",
11055
+ surfaceTintColor: "#f3f7ff",
11056
+ textColor: "#0f172a",
11057
+ textSecondaryColor: "#334155",
11058
+ textMutedColor: "#64748b",
11059
+ textFaintColor: "#94a3b8",
11060
+ textPlaceholderColor: "#94a3b8",
11061
+ buttonBgColor: "#2f5bea",
11062
+ buttonHoverBgColor: "#2349c8",
11063
+ fileUploadBgColor: "#fafcff",
11064
+ fileUploadBorderColor: "#cdd6e3",
11065
+ fileUploadHoverBorderColor: "#2f5bea",
11066
+ borderRadius: "12px",
11067
+ borderRadiusSmall: "8px",
11068
+ borderRadiusLarge: "16px",
11069
+ borderRadiusXLarge: "22px",
11070
+ fontFamily: '"Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
11071
+ shadowCard: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
11072
+ shadowPopover: "0 12px 32px -12px rgba(15,23,42,.18), 0 4px 12px -6px rgba(15,23,42,.08)",
11073
+ focusRingColor: "#2f5bea"
10463
11074
  }
10464
11075
  };
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
11076
 
10484
11077
  // src/instance/FormBuilderInstance.ts
10485
11078
  var FormBuilderInstance = class {
@@ -10604,26 +11197,37 @@ var FormBuilderInstance = class {
10604
11197
  }
10605
11198
  }
10606
11199
  /**
10607
- * Find the DOM element corresponding to a field path (instance-scoped)
11200
+ * Find the DOM element corresponding to a field path (instance-scoped).
11201
+ *
11202
+ * Strategy:
11203
+ * 1. Try a `[name="…"]` lookup first — works for any field that renders
11204
+ * an input/hidden with the path as its name, in either mode. Some
11205
+ * readonly renderers still emit a hidden input (boolean, switcher),
11206
+ * so this path must run regardless of `state.config.readonly`. A
11207
+ * prior version gated this on edit mode only, which made
11208
+ * `updateField` / `setFormData` silently miss readonly boolean
11209
+ * fields whose component also opts out of the standard label row
11210
+ * (`ownsLabel: true`).
11211
+ * 2. If no input matched, fall back to locating the field wrapper by
11212
+ * its visible label text — needed for readonly previews that don't
11213
+ * emit any `name=` attribute (e.g. file/markdown previews).
10608
11214
  */
10609
11215
  findFormElementByFieldPath(fieldPath) {
10610
11216
  if (!this.state.formRoot) return null;
10611
- if (!this.state.config.readonly) {
10612
- let element = this.state.formRoot.querySelector(
10613
- `[name="${fieldPath}"]`
11217
+ let element = this.state.formRoot.querySelector(
11218
+ `[name="${fieldPath}"]`
11219
+ );
11220
+ if (element) return element;
11221
+ const variations = [
11222
+ fieldPath,
11223
+ fieldPath.replace(/\[(\d+)\]/g, "[$1]"),
11224
+ fieldPath.replace(/\./g, "[") + "]".repeat((fieldPath.match(/\./g) || []).length)
11225
+ ];
11226
+ for (const variation of variations) {
11227
+ element = this.state.formRoot.querySelector(
11228
+ `[name="${variation}"]`
10614
11229
  );
10615
11230
  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
11231
  }
10628
11232
  const schemaElement = this.findSchemaElement(fieldPath);
10629
11233
  if (!schemaElement) return null;