@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.
package/dist/esm/index.js CHANGED
@@ -399,6 +399,81 @@ function deepEqual(a, b) {
399
399
  }
400
400
 
401
401
  // src/utils/styles.ts
402
+ function clearFieldError(input) {
403
+ const name = input.getAttribute("name");
404
+ if (!name) return;
405
+ const doc = input.ownerDocument || document;
406
+ const errorNode = doc.getElementById(`error-${name}`);
407
+ if (errorNode) errorNode.remove();
408
+ }
409
+ 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>';
410
+ function ensureThemingHooks(doc) {
411
+ if (doc.head.querySelector("[data-fb-theming-hooks]")) return;
412
+ const style = doc.createElement("style");
413
+ style.setAttribute("data-fb-theming-hooks", "");
414
+ style.textContent = `
415
+ [data-fb-slide-card] {
416
+ background: var(--fb-slide-card-bg);
417
+ box-shadow: var(--fb-slide-card-shadow);
418
+ border-radius: var(--fb-slide-card-radius);
419
+ min-height: var(--fb-slide-card-min-height);
420
+ padding: var(--fb-slide-card-padding);
421
+ }
422
+ [data-fb-label-row] > label {
423
+ font-size: var(--fb-label-section-font-size);
424
+ letter-spacing: var(--fb-label-section-letter-spacing);
425
+ text-transform: var(--fb-label-section-text-transform);
426
+ }
427
+ /* Per-item remove (trash) button used by multi-container items. Shares the
428
+ same faint-on-rest, error-on-hover palette as .fb-chip-remove. */
429
+ .fb-item-remove {
430
+ color: var(--fb-text-faint-color, #94a3b8);
431
+ background-color: transparent;
432
+ transition: color var(--fb-transition-duration), background-color var(--fb-transition-duration);
433
+ }
434
+ .fb-item-remove:hover {
435
+ color: var(--fb-error-color);
436
+ background-color: var(--fb-background-hover-color);
437
+ }
438
+ `;
439
+ doc.head.appendChild(style);
440
+ }
441
+ function applyAutoExpand(textarea) {
442
+ textarea.style.overflow = "hidden";
443
+ textarea.style.resize = "none";
444
+ const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
445
+ textarea.rows = Math.max(1, lineCount);
446
+ const resize = () => {
447
+ if (!textarea.isConnected) return;
448
+ textarea.style.height = "0";
449
+ textarea.style.height = `${textarea.scrollHeight}px`;
450
+ };
451
+ textarea.addEventListener("input", resize);
452
+ setTimeout(() => {
453
+ if (textarea.isConnected) resize();
454
+ }, 0);
455
+ }
456
+ function applySingleLineMode(textarea) {
457
+ textarea.addEventListener("keydown", (e) => {
458
+ if (e.key === "Enter") {
459
+ e.preventDefault();
460
+ }
461
+ });
462
+ textarea.addEventListener("paste", (e) => {
463
+ const pasted = e.clipboardData?.getData("text") ?? "";
464
+ if (!/[\r\n]/.test(pasted)) return;
465
+ e.preventDefault();
466
+ const cleaned = pasted.replace(/[\r\n]+/g, " ");
467
+ const start = textarea.selectionStart ?? textarea.value.length;
468
+ const end = textarea.selectionEnd ?? textarea.value.length;
469
+ const before = textarea.value.slice(0, start);
470
+ const after = textarea.value.slice(end);
471
+ textarea.value = before + cleaned + after;
472
+ const pos = start + cleaned.length;
473
+ textarea.setSelectionRange(pos, pos);
474
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
475
+ });
476
+ }
402
477
  function mountCounterInLabel(wrapper, counter) {
403
478
  const labelRow = wrapper.querySelector(
404
479
  ":scope > [data-fb-label-row]"
@@ -560,46 +635,94 @@ function applyActionButtonStyles(button, isFormLevel = false) {
560
635
  }
561
636
 
562
637
  // src/components/text.ts
563
- function createCharCounter(element, input, isTextarea = false) {
638
+ function ensureChipStyles(doc) {
639
+ if (doc.head.querySelector("[data-fb-chip-styles]")) return;
640
+ const style = doc.createElement("style");
641
+ style.setAttribute("data-fb-chip-styles", "");
642
+ style.textContent = `
643
+ .fb-chip-list { display: flex; flex-direction: column; gap: 4px; }
644
+ .fb-chip {
645
+ display: flex;
646
+ align-items: center;
647
+ gap: 8px;
648
+ padding: 5px 6px 5px 10px;
649
+ background: var(--fb-chip-bg, var(--fb-background-color, #fff));
650
+ border: 1px solid var(--fb-chip-border, var(--fb-border-color, #e2e8f0));
651
+ border-radius: 6px;
652
+ position: relative;
653
+ transition: border-color var(--fb-transition-duration, 0.15s);
654
+ }
655
+ .fb-chip:hover { border-color: var(--fb-border-hover-color, var(--fb-border-color, #cbd5e1)); }
656
+ .fb-chip:focus-within { border-color: var(--fb-border-focus-color, var(--fb-primary-color, #2f5bea)); }
657
+ .fb-chip-dot {
658
+ flex: 0 0 6px;
659
+ width: 6px;
660
+ height: 6px;
661
+ border-radius: 50%;
662
+ background: var(--fb-chip-dot, var(--fb-primary-color, #2f5bea));
663
+ }
664
+ .fb-chip-input {
665
+ flex: 1;
666
+ min-width: 0;
667
+ padding: 2px 0;
668
+ border: 0;
669
+ outline: none;
670
+ background: transparent;
671
+ color: var(--fb-chip-text, var(--fb-text-color, inherit));
672
+ font-size: var(--fb-font-size, 14px);
673
+ font-family: var(--fb-font-family, inherit);
674
+ line-height: 1.4;
675
+ }
676
+ .fb-chip-input::placeholder { color: var(--fb-text-placeholder-color, #94a3b8); }
677
+ .fb-chip-input:read-only { color: var(--fb-text-secondary-color, #475569); }
678
+ .fb-chip-remove {
679
+ flex: 0 0 auto;
680
+ width: 22px;
681
+ height: 22px;
682
+ display: inline-flex;
683
+ align-items: center;
684
+ justify-content: center;
685
+ padding: 0;
686
+ border: 0;
687
+ border-radius: 4px;
688
+ background: transparent;
689
+ color: var(--fb-text-faint-color, #94a3b8);
690
+ cursor: pointer;
691
+ opacity: 0;
692
+ transition: opacity 0.12s, color 0.12s, background-color 0.12s;
693
+ }
694
+ .fb-chip:hover .fb-chip-remove,
695
+ .fb-chip-remove:focus-visible { opacity: 1; }
696
+ .fb-chip-remove:hover {
697
+ color: var(--fb-error-color, #dc2626);
698
+ background: var(--fb-background-hover-color, #f1f5f9);
699
+ }
700
+ .fb-chip-remove:disabled { opacity: 0 !important; pointer-events: none; }
701
+ `;
702
+ doc.head.appendChild(style);
703
+ }
704
+ function createCharCounter(element, input) {
564
705
  const counter = document.createElement("span");
565
706
  counter.className = "char-counter";
566
707
  counter.style.cssText = `
567
- position: absolute;
568
- ${isTextarea ? "bottom: 8px" : "top: 50%; transform: translateY(-50%)"};
569
- right: 10px;
708
+ margin-top: 4px;
709
+ padding-right: 12px;
710
+ text-align: right;
570
711
  font-size: var(--fb-font-size-small);
571
- color: var(--fb-text-secondary-color);
712
+ line-height: 1;
713
+ color: var(--fb-error-color);
572
714
  pointer-events: none;
573
- background: var(--fb-background-color);
574
- padding: 0 4px;
715
+ display: none;
575
716
  `;
576
717
  const updateCounter = () => {
577
718
  const len = input.value.length;
578
- const min = element.minLength;
579
719
  const max = element.maxLength;
580
- if (min == null && max == null) {
581
- counter.textContent = "";
582
- return;
583
- }
584
- if (len === 0 || min != null && len < min) {
585
- if (min != null && max != null) {
586
- counter.textContent = `${min}-${max}`;
587
- } else if (max != null) {
588
- counter.textContent = `\u2264${max}`;
589
- } else if (min != null) {
590
- counter.textContent = `\u2265${min}`;
591
- }
592
- counter.style.color = "var(--fb-text-secondary-color)";
593
- } else if (max != null && len > max) {
720
+ if (max != null && len > max) {
594
721
  counter.textContent = `${len}/${max}`;
595
- counter.style.color = "var(--fb-error-color)";
722
+ counter.style.display = "block";
596
723
  } else {
597
- if (max != null) {
598
- counter.textContent = `${len}/${max}`;
599
- } else {
600
- counter.textContent = `${len}`;
601
- }
602
- counter.style.color = "var(--fb-text-secondary-color)";
724
+ counter.textContent = "";
725
+ counter.style.display = "none";
603
726
  }
604
727
  };
605
728
  input.addEventListener("input", updateCounter);
@@ -612,26 +735,32 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
612
735
  const inputWrapper = document.createElement("div");
613
736
  inputWrapper.style.cssText = "position: relative;";
614
737
  const hasCharCounter = !readonly && (element.minLength != null || element.maxLength != null);
615
- const textInput = document.createElement("input");
616
- textInput.type = "text";
738
+ const textInput = document.createElement("textarea");
739
+ textInput.rows = 1;
617
740
  textInput.className = "w-full rounded-lg";
618
741
  textInput.style.cssText = `
619
742
  padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
620
- ${hasCharCounter ? "padding-right: 60px;" : ""}
621
743
  border: var(--fb-border-width) solid var(--fb-border-color);
622
744
  border-radius: var(--fb-border-radius);
623
745
  background-color: ${readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
624
746
  color: var(--fb-text-color);
625
747
  font-size: var(--fb-font-size);
626
748
  font-family: var(--fb-font-family);
749
+ line-height: var(--fb-line-height, 1.5);
627
750
  transition: all var(--fb-transition-duration) ease-in-out;
628
751
  width: 100%;
629
752
  box-sizing: border-box;
753
+ resize: none;
754
+ overflow: hidden;
755
+ word-break: break-word;
756
+ overflow-wrap: anywhere;
630
757
  `;
631
758
  textInput.name = pathKey;
632
759
  textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
633
760
  textInput.value = ctx.prefill[element.key] || element.default || "";
634
761
  textInput.readOnly = readonly;
762
+ applySingleLineMode(textInput);
763
+ applyAutoExpand(textInput);
635
764
  if (!readonly) {
636
765
  textInput.addEventListener("focus", () => {
637
766
  textInput.style.borderColor = "var(--fb-border-focus-color)";
@@ -663,7 +792,7 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
663
792
  }
664
793
  inputWrapper.appendChild(textInput);
665
794
  if (hasCharCounter) {
666
- const counter = createCharCounter(element, textInput, false);
795
+ const counter = createCharCounter(element, textInput);
667
796
  inputWrapper.appendChild(counter);
668
797
  }
669
798
  wrapper.appendChild(inputWrapper);
@@ -673,132 +802,83 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
673
802
  const readonly = isElementReadonly(element, state, ctx);
674
803
  const prefillValues = ctx.prefill[element.key] || [];
675
804
  const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
676
- const hasCharCounter = !readonly && (element.minLength != null || element.maxLength != null);
677
805
  const minCount = element.minCount ?? 1;
678
806
  const maxCount = element.maxCount ?? Infinity;
679
807
  while (values.length < minCount) {
680
808
  values.push(element.default || "");
681
809
  }
682
- const container = document.createElement("div");
683
- container.className = "space-y-2";
684
- wrapper.appendChild(container);
810
+ ensureChipStyles(document);
811
+ const list = document.createElement("div");
812
+ list.className = "fb-chip-list";
813
+ wrapper.appendChild(list);
685
814
  function updateIndices() {
686
- const items = container.querySelectorAll(".multiple-text-item");
687
- items.forEach((item, index) => {
688
- const input = item.querySelector("input");
689
- if (input) {
690
- input.name = `${pathKey}[${index}]`;
815
+ const items = list.querySelectorAll(".fb-chip-input");
816
+ items.forEach((input, index) => {
817
+ input.name = `${pathKey}[${index}]`;
818
+ const chip = input.closest(".fb-chip");
819
+ const sib = chip?.nextElementSibling;
820
+ if (sib && sib.classList.contains("error-message")) {
821
+ sib.id = `error-${input.name}`;
691
822
  }
692
823
  });
693
824
  }
694
- function addTextItem(value = "", index = -1) {
695
- const itemWrapper = document.createElement("div");
696
- itemWrapper.className = "multiple-text-item flex items-center gap-2";
697
- const inputContainer = document.createElement("div");
698
- inputContainer.style.cssText = "position: relative; flex: 1;";
699
- const textInput = document.createElement("input");
700
- textInput.type = "text";
701
- textInput.style.cssText = `
702
- padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
703
- ${hasCharCounter ? "padding-right: 60px;" : ""}
704
- border: var(--fb-border-width) solid var(--fb-border-color);
705
- border-radius: var(--fb-border-radius);
706
- background-color: ${readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
707
- color: var(--fb-text-color);
708
- font-size: var(--fb-font-size);
709
- font-family: var(--fb-font-family);
710
- transition: all var(--fb-transition-duration) ease-in-out;
711
- width: 100%;
712
- box-sizing: border-box;
713
- `;
714
- textInput.placeholder = element.placeholder || t("placeholderText", state);
715
- textInput.value = value;
716
- textInput.readOnly = readonly;
717
- if (!readonly) {
718
- textInput.addEventListener("focus", () => {
719
- textInput.style.borderColor = "var(--fb-border-focus-color)";
720
- textInput.style.outline = `var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)`;
721
- textInput.style.outlineOffset = "0";
722
- });
723
- textInput.addEventListener("blur", () => {
724
- textInput.style.borderColor = "var(--fb-border-color)";
725
- textInput.style.outline = "none";
726
- });
727
- textInput.addEventListener("mouseenter", () => {
728
- if (document.activeElement !== textInput) {
729
- textInput.style.borderColor = "var(--fb-border-hover-color)";
730
- }
731
- });
732
- textInput.addEventListener("mouseleave", () => {
733
- if (document.activeElement !== textInput) {
734
- textInput.style.borderColor = "var(--fb-border-color)";
735
- }
736
- });
737
- }
825
+ function addChip(value = "") {
826
+ const chip = document.createElement("div");
827
+ chip.className = "fb-chip";
828
+ const dot = document.createElement("span");
829
+ dot.className = "fb-chip-dot";
830
+ dot.setAttribute("aria-hidden", "true");
831
+ chip.appendChild(dot);
832
+ const input = document.createElement("input");
833
+ input.type = "text";
834
+ input.className = "fb-chip-input";
835
+ input.value = value;
836
+ input.placeholder = element.placeholder || t("placeholderText", state);
837
+ input.readOnly = readonly;
838
+ chip.appendChild(input);
738
839
  if (!readonly && ctx.instance) {
739
840
  const handleChange = () => {
740
- const value2 = textInput.value === "" ? null : textInput.value;
741
- ctx.instance.triggerOnChange(textInput.name, value2);
841
+ ctx.instance.triggerOnChange(
842
+ input.name,
843
+ input.value === "" ? null : input.value
844
+ );
742
845
  };
743
- textInput.addEventListener("blur", handleChange);
744
- textInput.addEventListener("input", handleChange);
745
- }
746
- inputContainer.appendChild(textInput);
747
- if (hasCharCounter) {
748
- const counter = createCharCounter(element, textInput, false);
749
- inputContainer.appendChild(counter);
846
+ input.addEventListener("blur", handleChange);
847
+ input.addEventListener("input", handleChange);
750
848
  }
751
- itemWrapper.appendChild(inputContainer);
752
- if (index === -1) {
753
- container.appendChild(itemWrapper);
754
- } else {
755
- container.insertBefore(itemWrapper, container.children[index]);
849
+ if (!readonly) {
850
+ const rem = document.createElement("button");
851
+ rem.type = "button";
852
+ rem.className = "fb-chip-remove";
853
+ rem.setAttribute("aria-label", t("removeElement", state));
854
+ rem.innerHTML = BIN_ICON_SVG;
855
+ rem.onclick = () => {
856
+ const chips = list.querySelectorAll(".fb-chip");
857
+ const idx = Array.prototype.indexOf.call(chips, chip);
858
+ if (idx < 0) return;
859
+ if (chips.length <= minCount) return;
860
+ values.splice(idx, 1);
861
+ const trailingError = chip.nextElementSibling;
862
+ if (trailingError && trailingError.classList.contains("error-message")) {
863
+ trailingError.remove();
864
+ }
865
+ chip.remove();
866
+ updateIndices();
867
+ updateAddButton();
868
+ updateRemoveButtons();
869
+ };
870
+ chip.appendChild(rem);
756
871
  }
872
+ list.appendChild(chip);
757
873
  updateIndices();
758
- return itemWrapper;
874
+ return chip;
759
875
  }
760
876
  function updateRemoveButtons() {
761
877
  if (readonly) return;
762
- const items = container.querySelectorAll(".multiple-text-item");
763
- const currentCount = items.length;
764
- items.forEach((item) => {
765
- let removeBtn = item.querySelector(
766
- ".remove-item-btn"
767
- );
768
- if (!removeBtn) {
769
- removeBtn = document.createElement("button");
770
- removeBtn.type = "button";
771
- removeBtn.className = "remove-item-btn px-2 py-1 rounded";
772
- removeBtn.style.cssText = `
773
- color: var(--fb-error-color);
774
- background-color: transparent;
775
- transition: background-color var(--fb-transition-duration);
776
- `;
777
- removeBtn.innerHTML = "\u2715";
778
- removeBtn.addEventListener("mouseenter", () => {
779
- removeBtn.style.backgroundColor = "var(--fb-background-hover-color)";
780
- });
781
- removeBtn.addEventListener("mouseleave", () => {
782
- removeBtn.style.backgroundColor = "transparent";
783
- });
784
- removeBtn.onclick = () => {
785
- const currentIndex = Array.from(container.children).indexOf(
786
- item
787
- );
788
- if (container.children.length > minCount) {
789
- values.splice(currentIndex, 1);
790
- item.remove();
791
- updateIndices();
792
- updateAddButton();
793
- updateRemoveButtons();
794
- }
795
- };
796
- item.appendChild(removeBtn);
797
- }
798
- const disabled = currentCount <= minCount;
799
- removeBtn.disabled = disabled;
800
- removeBtn.style.opacity = disabled ? "0.5" : "1";
801
- removeBtn.style.pointerEvents = disabled ? "none" : "auto";
878
+ const chipCount = list.querySelectorAll(".fb-chip").length;
879
+ const disabled = chipCount <= minCount;
880
+ list.querySelectorAll(".fb-chip-remove").forEach((btn) => {
881
+ btn.disabled = disabled;
802
882
  });
803
883
  }
804
884
  let addUpdate = null;
@@ -807,7 +887,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
807
887
  "text",
808
888
  () => {
809
889
  values.push(element.default || "");
810
- addTextItem(element.default || "");
890
+ addChip(element.default || "");
811
891
  updateAddButton();
812
892
  updateRemoveButtons();
813
893
  },
@@ -820,7 +900,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
820
900
  function updateAddButton() {
821
901
  if (addUpdate) addUpdate(values.length, maxCount);
822
902
  }
823
- values.forEach((value) => addTextItem(value));
903
+ values.forEach((value) => addChip(value));
824
904
  updateAddButton();
825
905
  updateRemoveButtons();
826
906
  }
@@ -843,10 +923,12 @@ function validateTextElement(element, key, context) {
843
923
  font-size: var(--fb-font-size-small);
844
924
  margin-top: 0.25rem;
845
925
  `;
846
- if (input.nextSibling) {
847
- input.parentNode?.insertBefore(errorElement, input.nextSibling);
926
+ const chipAncestor = input.closest?.(".fb-chip");
927
+ const anchor = chipAncestor || input;
928
+ if (anchor.nextSibling) {
929
+ anchor.parentNode?.insertBefore(errorElement, anchor.nextSibling);
848
930
  } else {
849
- input.parentNode?.appendChild(errorElement);
931
+ anchor.parentNode?.appendChild(errorElement);
850
932
  }
851
933
  }
852
934
  errorElement.textContent = errorMessage;
@@ -895,7 +977,7 @@ function validateTextElement(element, key, context) {
895
977
  }
896
978
  };
897
979
  if (element.multiple) {
898
- const inputs = scopeRoot.querySelectorAll(`[name^="${key}["]`);
980
+ const inputs = scopeRoot.querySelectorAll(`[name^="${key}\\["]`);
899
981
  const values = [];
900
982
  const rawValues = [];
901
983
  inputs.forEach((input, index) => {
@@ -944,12 +1026,14 @@ function updateTextField(element, fieldPath, value, context) {
944
1026
  );
945
1027
  return;
946
1028
  }
947
- const inputs = scopeRoot.querySelectorAll(`[name^="${fieldPath}["]`);
1029
+ const inputs = scopeRoot.querySelectorAll(`[name^="${fieldPath}\\["]`);
948
1030
  inputs.forEach((input, index) => {
949
1031
  if (index < value.length) {
950
1032
  input.value = value[index] != null ? String(value[index]) : "";
951
1033
  input.classList.remove("invalid");
952
1034
  input.title = "";
1035
+ clearFieldError(input);
1036
+ input.dispatchEvent(new Event("input", { bubbles: true }));
953
1037
  }
954
1038
  });
955
1039
  if (value.length !== inputs.length) {
@@ -963,26 +1047,15 @@ function updateTextField(element, fieldPath, value, context) {
963
1047
  input.value = value != null ? String(value) : "";
964
1048
  input.classList.remove("invalid");
965
1049
  input.title = "";
1050
+ clearFieldError(input);
1051
+ if (input instanceof HTMLTextAreaElement) {
1052
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1053
+ }
966
1054
  }
967
1055
  }
968
1056
  }
969
1057
 
970
1058
  // src/components/textarea.ts
971
- function applyAutoExpand(textarea) {
972
- textarea.style.overflow = "hidden";
973
- textarea.style.resize = "none";
974
- const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
975
- textarea.rows = Math.max(1, lineCount);
976
- const resize = () => {
977
- if (!textarea.isConnected) return;
978
- textarea.style.height = "0";
979
- textarea.style.height = `${textarea.scrollHeight}px`;
980
- };
981
- textarea.addEventListener("input", resize);
982
- setTimeout(() => {
983
- if (textarea.isConnected) resize();
984
- }, 0);
985
- }
986
1059
  function renderTextareaElement(element, ctx, wrapper, pathKey) {
987
1060
  const state = ctx.state;
988
1061
  const readonly = isElementReadonly(element, state, ctx);
@@ -1013,7 +1086,7 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
1013
1086
  }
1014
1087
  textareaWrapper.appendChild(textareaInput);
1015
1088
  if (!readonly && (element.minLength != null || element.maxLength != null)) {
1016
- const counter = createCharCounter(element, textareaInput, true);
1089
+ const counter = createCharCounter(element, textareaInput);
1017
1090
  textareaWrapper.appendChild(counter);
1018
1091
  }
1019
1092
  wrapper.appendChild(textareaWrapper);
@@ -1069,7 +1142,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1069
1142
  }
1070
1143
  textareaContainer.appendChild(textareaInput);
1071
1144
  if (!readonly && (element.minLength != null || element.maxLength != null)) {
1072
- const counter = createCharCounter(element, textareaInput, true);
1145
+ const counter = createCharCounter(element, textareaInput);
1073
1146
  textareaContainer.appendChild(counter);
1074
1147
  }
1075
1148
  itemWrapper.appendChild(textareaContainer);
@@ -1163,6 +1236,89 @@ function updateTextareaField(element, fieldPath, value, context) {
1163
1236
  }
1164
1237
 
1165
1238
  // src/components/number.ts
1239
+ function ensureStepperStyles(doc) {
1240
+ const ID = "fb-number-stepper-styles";
1241
+ if (doc.getElementById(ID)) return;
1242
+ const style = doc.createElement("style");
1243
+ style.id = ID;
1244
+ style.textContent = `
1245
+ .fb-stepper-input::-webkit-outer-spin-button,
1246
+ .fb-stepper-input::-webkit-inner-spin-button {
1247
+ -webkit-appearance: none;
1248
+ margin: 0;
1249
+ }
1250
+ .fb-stepper-input { -moz-appearance: textfield; }
1251
+ `;
1252
+ doc.head.appendChild(style);
1253
+ }
1254
+ function buildStepper(input, element, readonly) {
1255
+ ensureStepperStyles(input.ownerDocument);
1256
+ const step = element.step ?? 1;
1257
+ const min = element.min;
1258
+ const max = element.max;
1259
+ const wrap = document.createElement("div");
1260
+ wrap.className = "fb-stepper";
1261
+ wrap.style.cssText = `
1262
+ display: inline-flex;
1263
+ align-items: stretch;
1264
+ border: var(--fb-border-width) solid var(--fb-border-color);
1265
+ border-radius: var(--fb-border-radius);
1266
+ overflow: hidden;
1267
+ background: var(--fb-background-color);
1268
+ `;
1269
+ const makeBtn = (label, delta) => {
1270
+ const b = document.createElement("button");
1271
+ b.type = "button";
1272
+ b.textContent = label;
1273
+ b.tabIndex = -1;
1274
+ b.style.cssText = `
1275
+ width: 32px;
1276
+ border: none;
1277
+ background: transparent;
1278
+ color: var(--fb-text-color);
1279
+ font-size: var(--fb-font-size);
1280
+ font-family: var(--fb-font-family);
1281
+ cursor: ${readonly ? "default" : "pointer"};
1282
+ user-select: none;
1283
+ `;
1284
+ if (readonly) {
1285
+ b.disabled = true;
1286
+ b.style.opacity = "0.5";
1287
+ } else {
1288
+ b.addEventListener("click", (e) => {
1289
+ e.preventDefault();
1290
+ const current = parseFloat(input.value);
1291
+ const base = Number.isFinite(current) ? current : min ?? element.default ?? 0;
1292
+ let next = parseFloat((base + delta * step).toPrecision(12));
1293
+ if (min != null) next = Math.max(min, next);
1294
+ if (max != null) next = Math.min(max, next);
1295
+ input.value = String(next);
1296
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1297
+ input.dispatchEvent(new Event("change", { bubbles: true }));
1298
+ });
1299
+ }
1300
+ return b;
1301
+ };
1302
+ input.classList.add("fb-stepper-input");
1303
+ input.style.cssText = `
1304
+ width: 56px;
1305
+ border: none;
1306
+ border-left: var(--fb-border-width) solid var(--fb-border-color);
1307
+ border-right: var(--fb-border-width) solid var(--fb-border-color);
1308
+ padding: var(--fb-input-padding-y) 0;
1309
+ font-size: var(--fb-font-size);
1310
+ font-family: var(--fb-font-family);
1311
+ text-align: center;
1312
+ background: transparent;
1313
+ color: var(--fb-text-color);
1314
+ -moz-appearance: textfield;
1315
+ box-sizing: border-box;
1316
+ `;
1317
+ wrap.appendChild(makeBtn("\u2212", -1));
1318
+ wrap.appendChild(input);
1319
+ wrap.appendChild(makeBtn("+", 1));
1320
+ return wrap;
1321
+ }
1166
1322
  function createNumberRangeHint(element, input) {
1167
1323
  const hint = document.createElement("span");
1168
1324
  hint.className = "number-range-hint";
@@ -1209,14 +1365,6 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1209
1365
  inputWrapper.style.cssText = "position: relative;";
1210
1366
  const numberInput = document.createElement("input");
1211
1367
  numberInput.type = "number";
1212
- numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1213
- numberInput.style.cssText = `
1214
- padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
1215
- font-size: var(--fb-font-size);
1216
- font-family: var(--fb-font-family);
1217
- width: 100%;
1218
- box-sizing: border-box;
1219
- `;
1220
1368
  numberInput.name = pathKey;
1221
1369
  numberInput.placeholder = element.placeholder || "0";
1222
1370
  if (element.min !== void 0) numberInput.min = element.min.toString();
@@ -1224,6 +1372,16 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1224
1372
  if (element.step !== void 0) numberInput.step = element.step.toString();
1225
1373
  numberInput.value = ctx.prefill[element.key] || element.default || "";
1226
1374
  numberInput.readOnly = readonly;
1375
+ if (!element.stepper) {
1376
+ numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1377
+ numberInput.style.cssText = `
1378
+ padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
1379
+ font-size: var(--fb-font-size);
1380
+ font-family: var(--fb-font-family);
1381
+ width: 100%;
1382
+ box-sizing: border-box;
1383
+ `;
1384
+ }
1227
1385
  if (!readonly && ctx.instance) {
1228
1386
  const handleChange = () => {
1229
1387
  const value = numberInput.value ? parseFloat(numberInput.value) : null;
@@ -1232,10 +1390,14 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1232
1390
  numberInput.addEventListener("blur", handleChange);
1233
1391
  numberInput.addEventListener("input", handleChange);
1234
1392
  }
1235
- inputWrapper.appendChild(numberInput);
1236
- if (!readonly && (element.min != null || element.max != null)) {
1237
- const counter = createNumberRangeHint(element, numberInput);
1238
- inputWrapper.appendChild(counter);
1393
+ if (element.stepper) {
1394
+ inputWrapper.appendChild(buildStepper(numberInput, element, readonly));
1395
+ } else {
1396
+ inputWrapper.appendChild(numberInput);
1397
+ if (!readonly && (element.min != null || element.max != null)) {
1398
+ const counter = createNumberRangeHint(element, numberInput);
1399
+ inputWrapper.appendChild(counter);
1400
+ }
1239
1401
  }
1240
1402
  wrapper.appendChild(inputWrapper);
1241
1403
  }
@@ -1489,13 +1651,14 @@ function updateNumberField(element, fieldPath, value, context) {
1489
1651
  return;
1490
1652
  }
1491
1653
  const inputs = scopeRoot.querySelectorAll(
1492
- `[name^="${fieldPath}["]`
1654
+ `[name^="${fieldPath}\\["]`
1493
1655
  );
1494
1656
  inputs.forEach((input, index) => {
1495
1657
  if (index < value.length) {
1496
1658
  input.value = value[index] != null ? String(value[index]) : "";
1497
1659
  input.classList.remove("invalid");
1498
1660
  input.title = "";
1661
+ clearFieldError(input);
1499
1662
  }
1500
1663
  });
1501
1664
  if (value.length !== inputs.length) {
@@ -1511,6 +1674,7 @@ function updateNumberField(element, fieldPath, value, context) {
1511
1674
  input.value = value != null ? String(value) : "";
1512
1675
  input.classList.remove("invalid");
1513
1676
  input.title = "";
1677
+ clearFieldError(input);
1514
1678
  }
1515
1679
  }
1516
1680
  }
@@ -1722,7 +1886,7 @@ function validateSelectElement(element, key, context) {
1722
1886
  };
1723
1887
  if ("multiple" in element && element.multiple) {
1724
1888
  const inputs = scopeRoot.querySelectorAll(
1725
- `[name^="${key}["]`
1889
+ `[name^="${key}\\["]`
1726
1890
  );
1727
1891
  const values = [];
1728
1892
  inputs.forEach((input) => {
@@ -1758,7 +1922,7 @@ function updateSelectField(element, fieldPath, value, context) {
1758
1922
  return;
1759
1923
  }
1760
1924
  const selects = scopeRoot.querySelectorAll(
1761
- `[name^="${fieldPath}["]`
1925
+ `[name^="${fieldPath}\\["]`
1762
1926
  );
1763
1927
  selects.forEach((select, index) => {
1764
1928
  if (index < value.length) {
@@ -1769,6 +1933,7 @@ function updateSelectField(element, fieldPath, value, context) {
1769
1933
  });
1770
1934
  select.classList.remove("invalid");
1771
1935
  select.title = "";
1936
+ clearFieldError(select);
1772
1937
  }
1773
1938
  });
1774
1939
  if (value.length !== selects.length) {
@@ -1788,72 +1953,147 @@ function updateSelectField(element, fieldPath, value, context) {
1788
1953
  });
1789
1954
  select.classList.remove("invalid");
1790
1955
  select.title = "";
1956
+ clearFieldError(select);
1791
1957
  }
1792
1958
  }
1793
1959
  }
1794
1960
 
1795
1961
  // src/components/switcher.ts
1796
- function applySelectedStyle(btn) {
1797
- btn.style.backgroundColor = "var(--fb-primary-color)";
1798
- btn.style.color = "#ffffff";
1799
- btn.style.borderColor = "var(--fb-primary-color)";
1962
+ function applySelectedStyle(btn, isPreset) {
1963
+ if (isPreset) {
1964
+ btn.style.backgroundColor = "var(--fb-primary-soft-color)";
1965
+ btn.style.color = "var(--fb-primary-color)";
1966
+ btn.style.borderColor = "var(--fb-primary-color)";
1967
+ } else {
1968
+ btn.style.backgroundColor = "var(--fb-primary-color)";
1969
+ btn.style.color = "#ffffff";
1970
+ btn.style.borderColor = "var(--fb-primary-color)";
1971
+ }
1800
1972
  }
1801
- function applyUnselectedStyle(btn) {
1802
- btn.style.backgroundColor = "transparent";
1973
+ function applyUnselectedStyle(btn, isPreset) {
1974
+ btn.style.backgroundColor = isPreset ? "var(--fb-background-color)" : "transparent";
1803
1975
  btn.style.color = "var(--fb-text-color)";
1804
1976
  btn.style.borderColor = "var(--fb-border-color)";
1805
1977
  }
1978
+ function isPresetButton(btn) {
1979
+ return btn.classList.contains("fb-switcher-preset");
1980
+ }
1981
+ function buildPresetCard(option, readonly) {
1982
+ const btn = document.createElement("button");
1983
+ btn.type = "button";
1984
+ btn.className = "fb-switcher-btn fb-switcher-preset";
1985
+ btn.dataset.value = option.value;
1986
+ btn.style.cssText = `
1987
+ display: inline-flex;
1988
+ align-items: center;
1989
+ gap: 8px;
1990
+ padding: 7px 12px 7px 10px;
1991
+ border-width: var(--fb-border-width);
1992
+ border-style: solid;
1993
+ border-radius: 999px;
1994
+ background: var(--fb-background-color);
1995
+ font-size: var(--fb-font-size);
1996
+ font-family: var(--fb-font-family);
1997
+ line-height: 1.25;
1998
+ cursor: ${readonly ? "default" : "pointer"};
1999
+ transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
2000
+ outline: none;
2001
+ `;
2002
+ if (option.iconUrl) {
2003
+ const icon = document.createElement("img");
2004
+ icon.className = "fb-switcher-icon";
2005
+ icon.src = option.iconUrl;
2006
+ icon.alt = "";
2007
+ icon.setAttribute("aria-hidden", "true");
2008
+ icon.style.cssText = `
2009
+ display: block;
2010
+ flex: 0 0 auto;
2011
+ width: 20px;
2012
+ height: 20px;
2013
+ object-fit: contain;
2014
+ `;
2015
+ btn.appendChild(icon);
2016
+ }
2017
+ const name = document.createElement("span");
2018
+ name.className = "fb-switcher-name";
2019
+ name.textContent = option.label;
2020
+ name.style.cssText = "font-weight: 600;";
2021
+ btn.appendChild(name);
2022
+ if (option.subtitle) {
2023
+ const sub = document.createElement("span");
2024
+ sub.className = "fb-switcher-subtitle";
2025
+ sub.textContent = option.subtitle;
2026
+ sub.style.cssText = `
2027
+ font-size: var(--fb-font-size-small);
2028
+ opacity: 0.7;
2029
+ font-variant-numeric: tabular-nums;
2030
+ `;
2031
+ btn.appendChild(sub);
2032
+ }
2033
+ return btn;
2034
+ }
1806
2035
  function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onChange) {
1807
2036
  const options = element.options || [];
2037
+ const isPresetMode = options.some((o) => o.subtitle || o.iconUrl);
1808
2038
  const group = document.createElement("div");
1809
2039
  group.className = "fb-switcher-group";
1810
- group.style.cssText = `
1811
- display: inline-flex;
1812
- flex-direction: row;
1813
- flex-wrap: nowrap;
1814
- `;
2040
+ group.style.cssText = isPresetMode ? `
2041
+ display: flex;
2042
+ flex-direction: row;
2043
+ flex-wrap: wrap;
2044
+ gap: 6px;
2045
+ ` : `
2046
+ display: inline-flex;
2047
+ flex-direction: row;
2048
+ flex-wrap: nowrap;
2049
+ `;
1815
2050
  const buttons = [];
1816
2051
  options.forEach((option, index) => {
1817
- const btn = document.createElement("button");
1818
- btn.type = "button";
1819
- btn.className = "fb-switcher-btn";
1820
- btn.dataset.value = option.value;
1821
- btn.textContent = option.label;
1822
- btn.style.cssText = `
1823
- padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
1824
- font-size: var(--fb-font-size);
1825
- border-width: var(--fb-border-width);
1826
- border-style: solid;
1827
- cursor: ${readonly ? "default" : "pointer"};
1828
- transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
1829
- white-space: nowrap;
1830
- line-height: 1.25;
1831
- outline: none;
1832
- `;
1833
- if (options.length === 1) {
1834
- btn.style.borderRadius = "var(--fb-border-radius)";
1835
- } else if (index === 0) {
1836
- btn.style.borderRadius = "var(--fb-border-radius) 0 0 var(--fb-border-radius)";
1837
- btn.style.borderRightWidth = "0";
1838
- } else if (index === options.length - 1) {
1839
- btn.style.borderRadius = "0 var(--fb-border-radius) var(--fb-border-radius) 0";
2052
+ let btn;
2053
+ if (isPresetMode) {
2054
+ btn = buildPresetCard(option, readonly);
1840
2055
  } else {
1841
- btn.style.borderRadius = "0";
1842
- btn.style.borderRightWidth = "0";
2056
+ btn = document.createElement("button");
2057
+ btn.type = "button";
2058
+ btn.className = "fb-switcher-btn";
2059
+ btn.dataset.value = option.value;
2060
+ btn.textContent = option.label;
2061
+ btn.style.cssText = `
2062
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
2063
+ font-size: var(--fb-font-size);
2064
+ border-width: var(--fb-border-width);
2065
+ border-style: solid;
2066
+ cursor: ${readonly ? "default" : "pointer"};
2067
+ transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
2068
+ white-space: nowrap;
2069
+ line-height: 1.25;
2070
+ outline: none;
2071
+ `;
2072
+ if (options.length === 1) {
2073
+ btn.style.borderRadius = "var(--fb-border-radius)";
2074
+ } else if (index === 0) {
2075
+ btn.style.borderRadius = "var(--fb-border-radius) 0 0 var(--fb-border-radius)";
2076
+ btn.style.borderRightWidth = "0";
2077
+ } else if (index === options.length - 1) {
2078
+ btn.style.borderRadius = "0 var(--fb-border-radius) var(--fb-border-radius) 0";
2079
+ } else {
2080
+ btn.style.borderRadius = "0";
2081
+ btn.style.borderRightWidth = "0";
2082
+ }
1843
2083
  }
1844
2084
  if (option.value === currentValue) {
1845
- applySelectedStyle(btn);
2085
+ applySelectedStyle(btn, isPresetMode);
1846
2086
  } else {
1847
- applyUnselectedStyle(btn);
2087
+ applyUnselectedStyle(btn, isPresetMode);
1848
2088
  }
1849
2089
  if (!readonly) {
1850
2090
  btn.addEventListener("click", () => {
1851
2091
  hiddenInput.value = option.value;
1852
2092
  buttons.forEach((b) => {
1853
2093
  if (b.dataset.value === option.value) {
1854
- applySelectedStyle(b);
2094
+ applySelectedStyle(b, isPresetMode);
1855
2095
  } else {
1856
- applyUnselectedStyle(b);
2096
+ applyUnselectedStyle(b, isPresetMode);
1857
2097
  }
1858
2098
  });
1859
2099
  if (onChange) {
@@ -1867,7 +2107,7 @@ function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onCha
1867
2107
  });
1868
2108
  btn.addEventListener("mouseleave", () => {
1869
2109
  if (hiddenInput.value !== option.value) {
1870
- btn.style.backgroundColor = "transparent";
2110
+ btn.style.backgroundColor = isPresetMode ? "var(--fb-background-color)" : "transparent";
1871
2111
  }
1872
2112
  });
1873
2113
  }
@@ -2086,7 +2326,7 @@ function validateSwitcherElement(element, key, context) {
2086
2326
  );
2087
2327
  if ("multiple" in element && element.multiple) {
2088
2328
  const inputs = scopeRoot.querySelectorAll(
2089
- `input[type="hidden"][name^="${key}["]`
2329
+ `input[type="hidden"][name^="${key}\\["]`
2090
2330
  );
2091
2331
  const values = [];
2092
2332
  inputs.forEach((input) => {
@@ -2133,7 +2373,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
2133
2373
  return;
2134
2374
  }
2135
2375
  const inputs = scopeRoot.querySelectorAll(
2136
- `input[type="hidden"][name^="${fieldPath}["]`
2376
+ `input[type="hidden"][name^="${fieldPath}\\["]`
2137
2377
  );
2138
2378
  inputs.forEach((input, index) => {
2139
2379
  if (index < value.length) {
@@ -2142,15 +2382,17 @@ function updateSwitcherField(element, fieldPath, value, context) {
2142
2382
  const group = input.parentElement?.querySelector(".fb-switcher-group");
2143
2383
  if (group) {
2144
2384
  group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
2385
+ const isPreset = isPresetButton(btn);
2145
2386
  if (btn.dataset.value === newVal) {
2146
- applySelectedStyle(btn);
2387
+ applySelectedStyle(btn, isPreset);
2147
2388
  } else {
2148
- applyUnselectedStyle(btn);
2389
+ applyUnselectedStyle(btn, isPreset);
2149
2390
  }
2150
2391
  });
2151
2392
  }
2152
2393
  input.classList.remove("invalid");
2153
2394
  input.title = "";
2395
+ clearFieldError(input);
2154
2396
  }
2155
2397
  });
2156
2398
  if (value.length !== inputs.length) {
@@ -2168,19 +2410,211 @@ function updateSwitcherField(element, fieldPath, value, context) {
2168
2410
  const group = input.parentElement?.querySelector(".fb-switcher-group");
2169
2411
  if (group) {
2170
2412
  group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
2413
+ const isPreset = isPresetButton(btn);
2171
2414
  if (btn.dataset.value === newVal) {
2172
- applySelectedStyle(btn);
2415
+ applySelectedStyle(btn, isPreset);
2173
2416
  } else {
2174
- applyUnselectedStyle(btn);
2417
+ applyUnselectedStyle(btn, isPreset);
2175
2418
  }
2176
2419
  });
2177
2420
  }
2178
2421
  input.classList.remove("invalid");
2179
2422
  input.title = "";
2423
+ clearFieldError(input);
2180
2424
  }
2181
2425
  }
2182
2426
  }
2183
2427
 
2428
+ // src/components/boolean.ts
2429
+ var TOGGLE_W = 36;
2430
+ var TOGGLE_H = 20;
2431
+ var KNOB = 16;
2432
+ function ensureStyles(doc) {
2433
+ const ID = "fb-boolean-styles";
2434
+ if (doc.getElementById(ID)) return;
2435
+ const style = doc.createElement("style");
2436
+ style.id = ID;
2437
+ style.textContent = `
2438
+ .fb-toggle {
2439
+ position: relative;
2440
+ display: inline-block;
2441
+ width: ${TOGGLE_W}px;
2442
+ height: ${TOGGLE_H}px;
2443
+ border-radius: ${TOGGLE_H}px;
2444
+ background: var(--fb-border-color);
2445
+ transition: background-color var(--fb-transition-duration);
2446
+ flex-shrink: 0;
2447
+ }
2448
+ .fb-toggle::after {
2449
+ content: "";
2450
+ position: absolute;
2451
+ top: ${(TOGGLE_H - KNOB) / 2}px;
2452
+ left: ${(TOGGLE_H - KNOB) / 2}px;
2453
+ width: ${KNOB}px;
2454
+ height: ${KNOB}px;
2455
+ border-radius: 50%;
2456
+ background: #ffffff;
2457
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
2458
+ transition: transform var(--fb-transition-duration);
2459
+ }
2460
+ .fb-toggle.fb-on {
2461
+ background: var(--fb-primary-color);
2462
+ }
2463
+ .fb-toggle.fb-on::after {
2464
+ transform: translateX(${TOGGLE_W - KNOB - (TOGGLE_H - KNOB)}px);
2465
+ }
2466
+ .fb-toggle-row {
2467
+ display: flex;
2468
+ align-items: center;
2469
+ gap: 12px;
2470
+ padding: 12px 14px;
2471
+ background: var(--fb-surface-soft-color);
2472
+ border: var(--fb-border-width) solid var(--fb-border-color);
2473
+ border-radius: var(--fb-border-radius);
2474
+ cursor: pointer;
2475
+ user-select: none;
2476
+ }
2477
+ .fb-toggle-row[aria-disabled="true"] {
2478
+ cursor: default;
2479
+ opacity: 0.7;
2480
+ }
2481
+ .fb-toggle-row:focus-visible {
2482
+ outline: var(--fb-focus-ring-width) solid var(--fb-focus-ring-color);
2483
+ outline-offset: var(--fb-focus-ring-offset);
2484
+ }
2485
+ .fb-toggle-text { flex: 1; min-width: 0; }
2486
+ .fb-toggle-title {
2487
+ display: flex;
2488
+ align-items: center;
2489
+ gap: 4px;
2490
+ font-size: var(--fb-font-size);
2491
+ font-weight: 500;
2492
+ color: var(--fb-text-color);
2493
+ line-height: 1.3;
2494
+ }
2495
+ .fb-toggle-subtitle {
2496
+ font-size: var(--fb-font-size-small);
2497
+ color: var(--fb-text-secondary-color);
2498
+ margin-top: 2px;
2499
+ line-height: 1.35;
2500
+ }
2501
+ .fb-toggle-info {
2502
+ flex: 0 0 14px;
2503
+ display: inline-flex;
2504
+ align-items: center;
2505
+ justify-content: center;
2506
+ width: 14px;
2507
+ height: 14px;
2508
+ border-radius: 50%;
2509
+ background: var(--fb-border-color);
2510
+ color: #fff;
2511
+ font-size: 10px;
2512
+ font-weight: 700;
2513
+ font-style: italic;
2514
+ font-family: serif;
2515
+ cursor: help;
2516
+ }
2517
+ `;
2518
+ doc.head.appendChild(style);
2519
+ }
2520
+ function parseBool(v) {
2521
+ if (typeof v === "boolean") return v;
2522
+ if (typeof v === "string") return v === "true" || v === "on" || v === "1";
2523
+ return false;
2524
+ }
2525
+ function renderBooleanElement(element, ctx, wrapper, pathKey) {
2526
+ ensureStyles(document);
2527
+ const state = ctx.state;
2528
+ const readonly = isElementReadonly(element, state, ctx);
2529
+ const prefillRaw = ctx.prefill[element.key];
2530
+ const initial = prefillRaw !== void 0 ? parseBool(prefillRaw) : parseBool(element.default);
2531
+ const hiddenInput = document.createElement("input");
2532
+ hiddenInput.type = "hidden";
2533
+ hiddenInput.name = pathKey;
2534
+ hiddenInput.value = initial ? "true" : "false";
2535
+ const row = document.createElement("div");
2536
+ row.className = "fb-toggle-row";
2537
+ row.setAttribute("role", "switch");
2538
+ row.setAttribute("aria-checked", initial ? "true" : "false");
2539
+ if (readonly) {
2540
+ row.setAttribute("aria-disabled", "true");
2541
+ } else {
2542
+ row.tabIndex = 0;
2543
+ }
2544
+ const pill = document.createElement("span");
2545
+ pill.className = "fb-toggle" + (initial ? " fb-on" : "");
2546
+ pill.setAttribute("aria-hidden", "true");
2547
+ row.appendChild(pill);
2548
+ const textBlock = document.createElement("div");
2549
+ textBlock.className = "fb-toggle-text";
2550
+ const titleEl = document.createElement("div");
2551
+ titleEl.className = "fb-toggle-title";
2552
+ titleEl.appendChild(document.createTextNode(element.label ?? ""));
2553
+ if (element.description) {
2554
+ const info = document.createElement("span");
2555
+ info.className = "fb-toggle-info";
2556
+ info.textContent = "i";
2557
+ info.title = element.description;
2558
+ titleEl.appendChild(info);
2559
+ }
2560
+ textBlock.appendChild(titleEl);
2561
+ if (element.hint) {
2562
+ const subtitle = document.createElement("div");
2563
+ subtitle.className = "fb-toggle-subtitle";
2564
+ subtitle.textContent = element.hint;
2565
+ textBlock.appendChild(subtitle);
2566
+ }
2567
+ row.appendChild(textBlock);
2568
+ if (!readonly) {
2569
+ const toggle = () => {
2570
+ const next = hiddenInput.value !== "true";
2571
+ hiddenInput.value = next ? "true" : "false";
2572
+ pill.classList.toggle("fb-on", next);
2573
+ row.setAttribute("aria-checked", next ? "true" : "false");
2574
+ if (ctx.instance) ctx.instance.triggerOnChange(pathKey, next);
2575
+ };
2576
+ row.addEventListener("click", (e) => {
2577
+ if (e.target?.classList.contains("fb-toggle-info")) {
2578
+ return;
2579
+ }
2580
+ toggle();
2581
+ });
2582
+ row.addEventListener("keydown", (e) => {
2583
+ if (e.key === " " || e.key === "Enter") {
2584
+ e.preventDefault();
2585
+ toggle();
2586
+ }
2587
+ });
2588
+ }
2589
+ wrapper.appendChild(hiddenInput);
2590
+ wrapper.appendChild(row);
2591
+ }
2592
+ function validateBooleanElement(element, key, context) {
2593
+ const { scopeRoot } = context;
2594
+ const input = scopeRoot.querySelector(
2595
+ `input[type="hidden"][name="${key}"]`
2596
+ );
2597
+ const raw = input?.value ?? "";
2598
+ const value = parseBool(raw);
2599
+ const errors = [];
2600
+ return { value, errors };
2601
+ }
2602
+ function updateBooleanField(_element, fieldPath, value, context) {
2603
+ const { scopeRoot } = context;
2604
+ const input = scopeRoot.querySelector(
2605
+ `input[type="hidden"][name="${fieldPath}"]`
2606
+ );
2607
+ if (!input) return;
2608
+ const bool = parseBool(value);
2609
+ input.value = bool ? "true" : "false";
2610
+ const row = input.parentElement?.querySelector(".fb-toggle-row");
2611
+ if (row) {
2612
+ row.setAttribute("aria-checked", bool ? "true" : "false");
2613
+ const pill = row.querySelector(".fb-toggle");
2614
+ if (pill) pill.classList.toggle("fb-on", bool);
2615
+ }
2616
+ }
2617
+
2184
2618
  // src/components/file/constraints.ts
2185
2619
  function getAllowedExtensions(accept) {
2186
2620
  if (!accept) return [];
@@ -2281,14 +2715,20 @@ function ensureFileStyles() {
2281
2715
  }
2282
2716
 
2283
2717
  /* \u2500\u2500\u2500 Wide single-file add tile (empty state) \u2500\u2500\u2500 */
2718
+ /* Flex-wraps: side-by-side when wide enough, stacks upload/library
2719
+ vertically when narrow (e.g. inside a 50/50 container column). */
2284
2720
  .fb-wide-tile {
2285
2721
  width: 100%;
2722
+ box-sizing: border-box;
2286
2723
  border-radius: 0.75rem;
2287
2724
  border: 1px dashed #60a5fa;
2288
2725
  background: rgba(239,246,255,0.5);
2289
2726
  display: flex;
2727
+ flex-wrap: wrap;
2728
+ align-items: stretch;
2729
+ gap: 0;
2290
2730
  overflow: hidden;
2291
- height: 180px;
2731
+ min-height: 180px;
2292
2732
  transition: border-color 150ms, background 150ms, box-shadow 150ms;
2293
2733
  cursor: pointer;
2294
2734
  }
@@ -2302,9 +2742,12 @@ function ensureFileStyles() {
2302
2742
  box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
2303
2743
  }
2304
2744
 
2305
- /* Upload zone inside wide tile */
2745
+ /* Upload zone inside wide tile.
2746
+ flex: 1 1 220px \u2014 wants at least 220px; if the container can't fit
2747
+ upload + library on one row (~220 + 176), library wraps below. */
2306
2748
  .fb-wide-tile-upload {
2307
- flex: 1;
2749
+ flex: 1 1 220px;
2750
+ min-height: 140px;
2308
2751
  display: flex;
2309
2752
  flex-direction: column;
2310
2753
  align-items: center;
@@ -2317,24 +2760,21 @@ function ensureFileStyles() {
2317
2760
  background: transparent;
2318
2761
  border: none;
2319
2762
  font-family: inherit;
2763
+ /* Dashed separator from library: right side when in a row, bottom when
2764
+ wrapped (the line then sits between the two stacked cards). */
2765
+ border-right: 1px dashed rgba(96,165,250,0.5);
2320
2766
  }
2321
2767
  .fb-wide-tile-upload:hover {
2322
- background: rgba(191,219,254,0.25);
2323
- }
2324
-
2325
- /* Vertical dashed divider between upload and library zones */
2326
- .fb-wide-tile-divider {
2327
- width: 1px;
2328
- margin: 16px 0;
2329
- border-left: 1px dashed rgba(96,165,250,0.5);
2330
- background: transparent;
2331
- flex-shrink: 0;
2768
+ background: rgba(191,219,254,0.25);
2332
2769
  }
2333
-
2334
- /* Library zone inside wide tile */
2770
+ /* Library zone inside wide tile.
2771
+ flex: 0 0 176px \u2014 fixed 176px, never grows. Upload fills the rest in
2772
+ row layout. When the tile wraps to two rows on narrow containers,
2773
+ library stays 176px wide on its own row (left-aligned), preserving the
2774
+ visual hierarchy "upload > library" in both layouts. */
2335
2775
  .fb-wide-tile-library {
2336
- width: 176px;
2337
- flex-shrink: 0;
2776
+ flex: 0 0 176px;
2777
+ min-height: 120px;
2338
2778
  display: flex;
2339
2779
  flex-direction: column;
2340
2780
  align-items: center;
@@ -2351,6 +2791,10 @@ function ensureFileStyles() {
2351
2791
  .fb-wide-tile-library:hover {
2352
2792
  background: rgba(191,219,254,0.25);
2353
2793
  }
2794
+ /* Narrow-tile mode lives in a separate <style> tag (see below) \u2014 the
2795
+ @container rule is appended only when the runtime actually supports
2796
+ container queries, so jsdom (which doesn't) never sees it and stays
2797
+ quiet in test logs. */
2354
2798
 
2355
2799
  /* \u2500\u2500\u2500 Multi-file outer grid container \u2500\u2500\u2500 */
2356
2800
  .fb-multi-outer {
@@ -2729,6 +3173,39 @@ function ensureFileStyles() {
2729
3173
  }
2730
3174
  `;
2731
3175
  document.head.appendChild(style);
3176
+ if (typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("container-type", "inline-size")) {
3177
+ const cq = document.createElement("style");
3178
+ cq.setAttribute("data-fb-file-styles-cq", "true");
3179
+ cq.textContent = `
3180
+ .fb-wide-tile { container-type: inline-size; }
3181
+ @container (max-width: 408px) {
3182
+ .fb-wide-tile-upload {
3183
+ border-right: none;
3184
+ border-bottom: 1px dashed rgba(96,165,250,0.5);
3185
+ }
3186
+ .fb-wide-tile-library {
3187
+ flex: 1 0 100%;
3188
+ min-height: 0;
3189
+ flex-direction: row;
3190
+ gap: 6px;
3191
+ padding: 8px 12px;
3192
+ font-size: 12px;
3193
+ }
3194
+ .fb-wide-tile-library .fb-wide-tile-library-icon {
3195
+ width: 16px;
3196
+ height: 16px;
3197
+ }
3198
+ .fb-wide-tile-library .fb-wide-tile-library-label {
3199
+ font-size: 12px;
3200
+ font-weight: 500;
3201
+ }
3202
+ .fb-wide-tile-library .fb-wide-tile-library-hint {
3203
+ display: none;
3204
+ }
3205
+ }
3206
+ `;
3207
+ document.head.appendChild(cq);
3208
+ }
2732
3209
  }
2733
3210
 
2734
3211
  // src/components/file/dom.ts
@@ -4074,21 +4551,21 @@ function buildWideTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragO
4074
4551
  };
4075
4552
  outer.appendChild(uploadBtn);
4076
4553
  if (hasLibrary && onLibraryClick) {
4077
- const divider = document.createElement("div");
4078
- divider.className = "fb-wide-tile-divider";
4079
- outer.appendChild(divider);
4080
4554
  const libBtn = document.createElement("button");
4081
4555
  libBtn.type = "button";
4082
4556
  libBtn.className = "fb-wide-tile-library fb-file-library-card";
4083
4557
  const libIcon = document.createElement("span");
4558
+ libIcon.className = "fb-wide-tile-library-icon";
4084
4559
  libIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
4085
4560
  libIcon.innerHTML = ICON_LIBRARY2;
4086
4561
  libBtn.appendChild(libIcon);
4087
4562
  const libLabel = document.createElement("div");
4563
+ libLabel.className = "fb-wide-tile-library-label";
4088
4564
  libLabel.style.cssText = "font-size:13px;font-weight:600;text-align:center;";
4089
4565
  libLabel.textContent = t("fromLibrary", state);
4090
4566
  libBtn.appendChild(libLabel);
4091
4567
  const libHint = document.createElement("div");
4568
+ libHint.className = "fb-wide-tile-library-hint";
4092
4569
  libHint.style.cssText = "font-size:11px;opacity:0.75;text-align:center;";
4093
4570
  libHint.textContent = t("libraryHint", state);
4094
4571
  libBtn.appendChild(libHint);
@@ -5330,7 +5807,7 @@ function validateColourElement(element, key, context) {
5330
5807
  };
5331
5808
  if (element.multiple) {
5332
5809
  const hexInputs = scopeRoot.querySelectorAll(
5333
- `[name^="${key}["].colour-hex-input`
5810
+ `[name^="${key}\\["].colour-hex-input`
5334
5811
  );
5335
5812
  const values = [];
5336
5813
  hexInputs.forEach((input, index) => {
@@ -5379,7 +5856,7 @@ function updateColourField(element, fieldPath, value, context) {
5379
5856
  return;
5380
5857
  }
5381
5858
  const hexInputs = scopeRoot.querySelectorAll(
5382
- `[name^="${fieldPath}["].colour-hex-input`
5859
+ `[name^="${fieldPath}\\["].colour-hex-input`
5383
5860
  );
5384
5861
  hexInputs.forEach((hexInput, index) => {
5385
5862
  if (index < value.length) {
@@ -5387,6 +5864,7 @@ function updateColourField(element, fieldPath, value, context) {
5387
5864
  hexInput.value = normalized;
5388
5865
  hexInput.classList.remove("invalid");
5389
5866
  hexInput.title = "";
5867
+ clearFieldError(hexInput);
5390
5868
  const wrapper = hexInput.closest(".colour-picker-wrapper");
5391
5869
  if (wrapper) {
5392
5870
  const swatch = wrapper.querySelector(".colour-swatch");
@@ -5416,6 +5894,7 @@ function updateColourField(element, fieldPath, value, context) {
5416
5894
  hexInput.value = normalized;
5417
5895
  hexInput.classList.remove("invalid");
5418
5896
  hexInput.title = "";
5897
+ clearFieldError(hexInput);
5419
5898
  const wrapper = hexInput.closest(".colour-picker-wrapper");
5420
5899
  if (wrapper) {
5421
5900
  const swatch = wrapper.querySelector(".colour-swatch");
@@ -5818,7 +6297,7 @@ function validateSliderElement(element, key, context) {
5818
6297
  };
5819
6298
  if (element.multiple) {
5820
6299
  const sliders = scopeRoot.querySelectorAll(
5821
- `input[type="range"][name^="${key}["]`
6300
+ `input[type="range"][name^="${key}\\["]`
5822
6301
  );
5823
6302
  const values = [];
5824
6303
  sliders.forEach((slider, index) => {
@@ -5869,7 +6348,7 @@ function updateSliderField(element, fieldPath, value, context) {
5869
6348
  return;
5870
6349
  }
5871
6350
  const sliders = scopeRoot.querySelectorAll(
5872
- `input[type="range"][name^="${fieldPath}["]`
6351
+ `input[type="range"][name^="${fieldPath}\\["]`
5873
6352
  );
5874
6353
  sliders.forEach((slider, index) => {
5875
6354
  if (index < value.length && value[index] !== null) {
@@ -5897,6 +6376,7 @@ function updateSliderField(element, fieldPath, value, context) {
5897
6376
  }
5898
6377
  slider.classList.remove("invalid");
5899
6378
  slider.title = "";
6379
+ clearFieldError(slider);
5900
6380
  }
5901
6381
  });
5902
6382
  if (value.length !== sliders.length) {
@@ -5933,6 +6413,7 @@ function updateSliderField(element, fieldPath, value, context) {
5933
6413
  }
5934
6414
  slider.classList.remove("invalid");
5935
6415
  slider.title = "";
6416
+ clearFieldError(slider);
5936
6417
  }
5937
6418
  }
5938
6419
  }
@@ -6055,34 +6536,24 @@ function getChildWrapperClass(isSlides, columns) {
6055
6536
  const cols = columns || 1;
6056
6537
  return cols === 1 ? "space-y-2" : `grid grid-cols-${cols} gap-2`;
6057
6538
  }
6058
- function mountRemoveButton(item, onRemove) {
6539
+ function mountRemoveButton(item, onRemove, state) {
6059
6540
  const rem = document.createElement("button");
6060
6541
  rem.type = "button";
6061
6542
  rem.className = "fb-item-remove";
6543
+ rem.setAttribute("aria-label", t("removeElement", state));
6062
6544
  rem.style.cssText = `
6063
- width: 22px;
6064
- height: 22px;
6545
+ width: 24px;
6546
+ height: 24px;
6065
6547
  display: inline-flex;
6066
6548
  align-items: center;
6067
6549
  justify-content: center;
6068
6550
  padding: 0;
6069
- line-height: 1;
6070
- font-size: 14px;
6071
- color: var(--fb-error-color);
6072
- background-color: transparent;
6073
6551
  border: 0;
6074
6552
  border-radius: 4px;
6075
6553
  cursor: pointer;
6076
6554
  flex-shrink: 0;
6077
- transition: background-color var(--fb-transition-duration);
6078
6555
  `;
6079
- rem.textContent = "\u2715";
6080
- rem.addEventListener("mouseenter", () => {
6081
- rem.style.backgroundColor = "var(--fb-background-hover-color)";
6082
- });
6083
- rem.addEventListener("mouseleave", () => {
6084
- rem.style.backgroundColor = "transparent";
6085
- });
6556
+ rem.innerHTML = BIN_ICON_SVG;
6086
6557
  rem.onclick = onRemove;
6087
6558
  const labelRow = item.querySelector("[data-fb-label-row]");
6088
6559
  if (labelRow) {
@@ -6108,7 +6579,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6108
6579
  itemsWrap.className = "fb-container-slides";
6109
6580
  const slideCols = element.columns;
6110
6581
  const gridTemplateColumns = typeof slideCols === "number" && slideCols > 0 ? `repeat(${slideCols}, 1fr)` : "repeat(auto-fit, minmax(280px, 1fr))";
6111
- itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:8px;align-items:start;`;
6582
+ itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:var(--fb-slides-gap, 14px);align-items:start;`;
6112
6583
  } else {
6113
6584
  itemsWrap.className = "space-y-2";
6114
6585
  }
@@ -6139,6 +6610,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6139
6610
  const item = document.createElement("div");
6140
6611
  item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6141
6612
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6613
+ if (isSlides) {
6614
+ item.setAttribute("data-fb-slide-card", "");
6615
+ }
6142
6616
  const childWrapper = document.createElement("div");
6143
6617
  childWrapper.className = getChildWrapperClass(isSlides, element.columns);
6144
6618
  element.elements.forEach((child) => {
@@ -6155,7 +6629,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6155
6629
  });
6156
6630
  item.appendChild(childWrapper);
6157
6631
  if (!containerIsReadonly) {
6158
- mountRemoveButton(item, () => handleRemoveItem(item));
6632
+ mountRemoveButton(item, () => handleRemoveItem(item), state);
6159
6633
  }
6160
6634
  if (slideAddTile && slideAddTile.parentElement === itemsWrap) {
6161
6635
  itemsWrap.insertBefore(item, slideAddTile);
@@ -6201,6 +6675,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6201
6675
  const item = document.createElement("div");
6202
6676
  item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6203
6677
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6678
+ if (isSlides) {
6679
+ item.setAttribute("data-fb-slide-card", "");
6680
+ }
6204
6681
  const childWrapper = document.createElement("div");
6205
6682
  if (isSlides) {
6206
6683
  childWrapper.className = "space-y-2";
@@ -6224,7 +6701,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6224
6701
  });
6225
6702
  item.appendChild(childWrapper);
6226
6703
  if (!containerIsReadonly) {
6227
- mountRemoveButton(item, () => handleRemoveItem(item));
6704
+ mountRemoveButton(item, () => handleRemoveItem(item), ctx.state);
6228
6705
  }
6229
6706
  itemsWrap.appendChild(item);
6230
6707
  });
@@ -6244,6 +6721,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6244
6721
  const item = document.createElement("div");
6245
6722
  item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6246
6723
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6724
+ if (isSlides) {
6725
+ item.setAttribute("data-fb-slide-card", "");
6726
+ }
6247
6727
  const childWrapper = document.createElement("div");
6248
6728
  if (isSlides) {
6249
6729
  childWrapper.className = "space-y-2";
@@ -6268,11 +6748,15 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6268
6748
  }
6269
6749
  });
6270
6750
  item.appendChild(childWrapper);
6271
- mountRemoveButton(item, () => {
6272
- if (countItems() > min) {
6273
- handleRemoveItem(item);
6274
- }
6275
- });
6751
+ mountRemoveButton(
6752
+ item,
6753
+ () => {
6754
+ if (countItems() > min) {
6755
+ handleRemoveItem(item);
6756
+ }
6757
+ },
6758
+ ctx.state
6759
+ );
6276
6760
  itemsWrap.appendChild(item);
6277
6761
  }
6278
6762
  }
@@ -8897,7 +9381,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
8897
9381
  if (element.minLength != null || element.maxLength != null) {
8898
9382
  const counterRow = document.createElement("div");
8899
9383
  counterRow.style.cssText = "position: relative; padding: 2px 10px 4px; text-align: right;";
8900
- const counter = createCharCounter(element, textarea, false);
9384
+ const counter = createCharCounter(element, textarea);
8901
9385
  counter.style.cssText = `
8902
9386
  position: static;
8903
9387
  display: inline-block;
@@ -9422,6 +9906,117 @@ function validateMarkdown(_element, _key, _context) {
9422
9906
  function updateMarkdown(_element, _fieldPath, _value, _context) {
9423
9907
  }
9424
9908
 
9909
+ // src/components/registry.ts
9910
+ function validateHiddenElement(element, key, context) {
9911
+ const { scopeRoot } = context;
9912
+ const input = scopeRoot.querySelector(
9913
+ `input[type="hidden"][data-hidden-field="true"][name="${key}"]`
9914
+ );
9915
+ const raw = input?.value ?? "";
9916
+ if (raw === "") {
9917
+ const defaultVal = "default" in element ? element.default : null;
9918
+ return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
9919
+ }
9920
+ return { value: deserializeHiddenValue(raw), errors: [] };
9921
+ }
9922
+ function updateHiddenField(_element, fieldPath, value, context) {
9923
+ const { scopeRoot } = context;
9924
+ const input = scopeRoot.querySelector(
9925
+ `input[type="hidden"][data-hidden-field="true"][name="${fieldPath}"]`
9926
+ );
9927
+ if (!input) return;
9928
+ input.value = serializeHiddenValue(value);
9929
+ }
9930
+ var componentRegistry = {
9931
+ text: {
9932
+ validate: validateTextElement,
9933
+ update: updateTextField
9934
+ },
9935
+ textarea: {
9936
+ validate: validateTextareaElement,
9937
+ update: updateTextareaField
9938
+ },
9939
+ number: {
9940
+ validate: validateNumberElement,
9941
+ update: updateNumberField
9942
+ },
9943
+ select: {
9944
+ validate: validateSelectElement,
9945
+ update: updateSelectField
9946
+ },
9947
+ switcher: {
9948
+ validate: validateSwitcherElement,
9949
+ update: updateSwitcherField
9950
+ },
9951
+ boolean: {
9952
+ validate: validateBooleanElement,
9953
+ update: updateBooleanField,
9954
+ ownsLabel: true
9955
+ },
9956
+ file: {
9957
+ validate: validateFileElement,
9958
+ update: updateFileField
9959
+ },
9960
+ files: {
9961
+ // Legacy type - delegates to file
9962
+ validate: validateFileElement,
9963
+ update: updateFileField
9964
+ },
9965
+ colour: {
9966
+ validate: validateColourElement,
9967
+ update: updateColourField
9968
+ },
9969
+ slider: {
9970
+ validate: validateSliderElement,
9971
+ update: updateSliderField
9972
+ },
9973
+ container: {
9974
+ validate: validateContainerElement,
9975
+ update: updateContainerField
9976
+ },
9977
+ group: {
9978
+ // Deprecated type - delegates to container
9979
+ validate: validateGroupElement,
9980
+ update: updateGroupField
9981
+ },
9982
+ table: {
9983
+ validate: validateTableElement,
9984
+ update: updateTableField
9985
+ },
9986
+ richinput: {
9987
+ validate: validateRichInputElement,
9988
+ update: updateRichInputField
9989
+ },
9990
+ hidden: {
9991
+ // Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
9992
+ validate: validateHiddenElement,
9993
+ update: updateHiddenField
9994
+ },
9995
+ markdown: {
9996
+ // Display-only element — no value, no errors, skip from form data
9997
+ validate: validateMarkdown,
9998
+ update: updateMarkdown
9999
+ }
10000
+ };
10001
+ function getComponentOperations(elementType) {
10002
+ return componentRegistry[elementType] || null;
10003
+ }
10004
+ function validateElementWithComponent(element, key, context) {
10005
+ const ops = getComponentOperations(element.type);
10006
+ if (ops && ops.validate) {
10007
+ return ops.validate(element, key, context);
10008
+ }
10009
+ return null;
10010
+ }
10011
+ function updateElementWithComponent(element, fieldPath, value, context) {
10012
+ const ops = getComponentOperations(element.type);
10013
+ if (ops && ops.update) {
10014
+ ops.update(element, fieldPath, value, context);
10015
+ return true;
10016
+ }
10017
+ return false;
10018
+ }
10019
+
9425
10020
  // src/components/index.ts
9426
10021
  function showTooltip(tooltipId, button) {
9427
10022
  const tooltip = document.getElementById(tooltipId);
@@ -9730,6 +10325,9 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
9730
10325
  renderSwitcherElement(element, ctx, wrapper, pathKey);
9731
10326
  }
9732
10327
  break;
10328
+ case "boolean":
10329
+ renderBooleanElement(element, ctx, wrapper, pathKey);
10330
+ break;
9733
10331
  case "file":
9734
10332
  if (isMultiple) {
9735
10333
  renderMultipleFileElement(element, ctx, wrapper, pathKey);
@@ -9808,8 +10406,11 @@ function renderElement2(element, ctx) {
9808
10406
  const wrapper = document.createElement("div");
9809
10407
  wrapper.className = "mb-2 fb-field-wrapper";
9810
10408
  wrapper.setAttribute("data-field-key", element.key);
9811
- const label = createLabelContainer(element);
9812
- wrapper.appendChild(label);
10409
+ const ops = getComponentOperations(element.type);
10410
+ if (!ops?.ownsLabel) {
10411
+ const label = createLabelContainer(element);
10412
+ wrapper.appendChild(label);
10413
+ }
9813
10414
  const pathKey = pathJoin(ctx.path, element.key);
9814
10415
  dispatchToRenderer(element, ctx, wrapper, pathKey);
9815
10416
  if (initiallyDisabled) {
@@ -10042,28 +10643,52 @@ var defaultTheme = {
10042
10643
  // blue-500
10043
10644
  primaryHoverColor: "#2563eb",
10044
10645
  // blue-600
10646
+ primarySoftColor: "#dbeafe",
10647
+ // blue-100
10648
+ primarySoftHoverColor: "#bfdbfe",
10649
+ // blue-200
10045
10650
  errorColor: "#ef4444",
10046
10651
  // red-500
10047
10652
  errorHoverColor: "#dc2626",
10048
10653
  // red-600
10049
10654
  successColor: "#10b981",
10050
10655
  // green-500
10656
+ accentColor: "#f59e0b",
10657
+ // amber-500
10658
+ accentSoftColor: "#fef3c7",
10659
+ // amber-100
10660
+ accentBorderColor: "#fde68a",
10661
+ // amber-200
10662
+ accentTextColor: "#92400e",
10663
+ // amber-800
10051
10664
  borderColor: "#d1d5db",
10052
10665
  // gray-300
10053
10666
  borderHoverColor: "#9ca3af",
10054
10667
  // gray-400
10055
10668
  borderFocusColor: "#3b82f6",
10056
10669
  // blue-500
10670
+ borderStrongColor: "#9ca3af",
10671
+ // gray-400
10057
10672
  backgroundColor: "#ffffff",
10058
10673
  // white
10059
10674
  backgroundHoverColor: "#f9fafb",
10060
10675
  // gray-50
10061
10676
  backgroundReadonlyColor: "#f3f4f6",
10062
10677
  // gray-100
10678
+ pageBackgroundColor: "#f9fafb",
10679
+ // gray-50
10680
+ surfaceSoftColor: "#eff6ff",
10681
+ // blue-50
10682
+ surfaceTintColor: "#f8fafc",
10683
+ // slate-50
10063
10684
  textColor: "#1f2937",
10064
10685
  // gray-800
10065
10686
  textSecondaryColor: "#6b7280",
10066
10687
  // gray-500
10688
+ textMutedColor: "#9ca3af",
10689
+ // gray-400
10690
+ textFaintColor: "#cbd5e1",
10691
+ // slate-300
10067
10692
  textPlaceholderColor: "#9ca3af",
10068
10693
  // gray-400
10069
10694
  textDisabledColor: "#d1d5db",
@@ -10106,6 +10731,12 @@ var defaultTheme = {
10106
10731
  // 4px (compact density v2)
10107
10732
  borderRadius: "0.5rem",
10108
10733
  // rounded-lg (8px)
10734
+ borderRadiusSmall: "0.375rem",
10735
+ // 6px
10736
+ borderRadiusLarge: "0.75rem",
10737
+ // 12px
10738
+ borderRadiusXLarge: "1rem",
10739
+ // 16px
10109
10740
  borderWidth: "1px",
10110
10741
  // Typography
10111
10742
  fontSize: "0.875rem",
@@ -10117,13 +10748,31 @@ var defaultTheme = {
10117
10748
  fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
10118
10749
  fontWeightNormal: "400",
10119
10750
  fontWeightMedium: "500",
10751
+ lineHeight: "1.5",
10752
+ // Shadows
10753
+ shadowCard: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
10754
+ shadowPopover: "0 12px 32px -12px rgba(15,23,42,.18), 0 4px 12px -6px rgba(15,23,42,.08)",
10120
10755
  // Focus ring
10121
10756
  focusRingWidth: "2px",
10122
10757
  focusRingColor: "#3b82f6",
10123
10758
  // blue-500
10124
10759
  focusRingOpacity: "0.5",
10125
10760
  // Transitions
10126
- transitionDuration: "200ms"
10761
+ transitionDuration: "200ms",
10762
+ // Slide-card defaults — flat-white to match every other item card. The
10763
+ // Picaz theme overrides this with a gradient + shadow to lift the slides.
10764
+ slideCardBg: "#ffffff",
10765
+ slideCardShadow: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
10766
+ slideCardRadius: "0.5rem",
10767
+ // matches borderRadius
10768
+ slideCardMinHeight: "0",
10769
+ slideCardPadding: "12px",
10770
+ // Section-label defaults — same look as the regular field label. Themes
10771
+ // that want the "ПРЕИМУЩЕСТВА" caps style override these three vars.
10772
+ labelSectionFontSize: "0.875rem",
10773
+ // matches fontSize
10774
+ labelSectionLetterSpacing: "normal",
10775
+ labelSectionTextTransform: "none"
10127
10776
  };
10128
10777
  function generateCSSVariables(theme) {
10129
10778
  const mergedTheme = { ...defaultTheme, ...theme };
@@ -10135,6 +10784,7 @@ function generateCSSVariables(theme) {
10135
10784
  return cssVars.join("\n");
10136
10785
  }
10137
10786
  function injectThemeVariables(container, theme) {
10787
+ ensureThemingHooks(container.ownerDocument || document);
10138
10788
  const cssVariables = generateCSSVariables(theme);
10139
10789
  let styleTag = container.querySelector(
10140
10790
  "style[data-fb-theme]"
@@ -10197,114 +10847,50 @@ var exampleThemes = {
10197
10847
  fontSize: "16px",
10198
10848
  fontSizeSmall: "14px",
10199
10849
  fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif'
10200
- }
10201
- };
10202
-
10203
- // src/components/registry.ts
10204
- function validateHiddenElement(element, key, context) {
10205
- const { scopeRoot } = context;
10206
- const input = scopeRoot.querySelector(
10207
- `input[type="hidden"][data-hidden-field="true"][name="${key}"]`
10208
- );
10209
- const raw = input?.value ?? "";
10210
- if (raw === "") {
10211
- const defaultVal = "default" in element ? element.default : null;
10212
- return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
10213
- }
10214
- return { value: deserializeHiddenValue(raw), errors: [] };
10215
- }
10216
- function updateHiddenField(_element, fieldPath, value, context) {
10217
- const { scopeRoot } = context;
10218
- const input = scopeRoot.querySelector(
10219
- `input[type="hidden"][data-hidden-field="true"][name="${fieldPath}"]`
10220
- );
10221
- if (!input) return;
10222
- input.value = serializeHiddenValue(value);
10223
- }
10224
- var componentRegistry = {
10225
- text: {
10226
- validate: validateTextElement,
10227
- update: updateTextField
10228
- },
10229
- textarea: {
10230
- validate: validateTextareaElement,
10231
- update: updateTextareaField
10232
- },
10233
- number: {
10234
- validate: validateNumberElement,
10235
- update: updateNumberField
10236
- },
10237
- select: {
10238
- validate: validateSelectElement,
10239
- update: updateSelectField
10240
- },
10241
- switcher: {
10242
- validate: validateSwitcherElement,
10243
- update: updateSwitcherField
10244
- },
10245
- file: {
10246
- validate: validateFileElement,
10247
- update: updateFileField
10248
- },
10249
- files: {
10250
- // Legacy type - delegates to file
10251
- validate: validateFileElement,
10252
- update: updateFileField
10253
- },
10254
- colour: {
10255
- validate: validateColourElement,
10256
- update: updateColourField
10257
- },
10258
- slider: {
10259
- validate: validateSliderElement,
10260
- update: updateSliderField
10261
- },
10262
- container: {
10263
- validate: validateContainerElement,
10264
- update: updateContainerField
10265
- },
10266
- group: {
10267
- // Deprecated type - delegates to container
10268
- validate: validateGroupElement,
10269
- update: updateGroupField
10270
- },
10271
- table: {
10272
- validate: validateTableElement,
10273
- update: updateTableField
10274
- },
10275
- richinput: {
10276
- validate: validateRichInputElement,
10277
- update: updateRichInputField
10278
- },
10279
- hidden: {
10280
- // Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
10281
- validate: validateHiddenElement,
10282
- update: updateHiddenField
10283
10850
  },
10284
- markdown: {
10285
- // Display-only element no value, no errors, skip from form data
10286
- validate: validateMarkdown,
10287
- update: updateMarkdown
10851
+ // Picaz wizard design tokens — derived from the Picaz Wizard mockups.
10852
+ // Pairs with the host-side .card / .section-num / .lede chrome that wraps the form.
10853
+ picaz: {
10854
+ ...defaultTheme,
10855
+ primaryColor: "#2f5bea",
10856
+ primaryHoverColor: "#2349c8",
10857
+ primarySoftColor: "#eaf0ff",
10858
+ primarySoftHoverColor: "#d6e0ff",
10859
+ errorColor: "#ef4444",
10860
+ successColor: "#16a34a",
10861
+ accentColor: "#ffb020",
10862
+ accentSoftColor: "#fff7e6",
10863
+ accentBorderColor: "#fde7b5",
10864
+ accentTextColor: "#92400e",
10865
+ borderColor: "#e3e8f0",
10866
+ borderHoverColor: "#cdd6e3",
10867
+ borderFocusColor: "#2f5bea",
10868
+ borderStrongColor: "#cdd6e3",
10869
+ backgroundColor: "#ffffff",
10870
+ backgroundHoverColor: "#f3f7ff",
10871
+ pageBackgroundColor: "#f6f8fb",
10872
+ surfaceSoftColor: "#eef4ff",
10873
+ surfaceTintColor: "#f3f7ff",
10874
+ textColor: "#0f172a",
10875
+ textSecondaryColor: "#334155",
10876
+ textMutedColor: "#64748b",
10877
+ textFaintColor: "#94a3b8",
10878
+ textPlaceholderColor: "#94a3b8",
10879
+ buttonBgColor: "#2f5bea",
10880
+ buttonHoverBgColor: "#2349c8",
10881
+ fileUploadBgColor: "#fafcff",
10882
+ fileUploadBorderColor: "#cdd6e3",
10883
+ fileUploadHoverBorderColor: "#2f5bea",
10884
+ borderRadius: "12px",
10885
+ borderRadiusSmall: "8px",
10886
+ borderRadiusLarge: "16px",
10887
+ borderRadiusXLarge: "22px",
10888
+ fontFamily: '"Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
10889
+ shadowCard: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
10890
+ shadowPopover: "0 12px 32px -12px rgba(15,23,42,.18), 0 4px 12px -6px rgba(15,23,42,.08)",
10891
+ focusRingColor: "#2f5bea"
10288
10892
  }
10289
10893
  };
10290
- function getComponentOperations(elementType) {
10291
- return componentRegistry[elementType] || null;
10292
- }
10293
- function validateElementWithComponent(element, key, context) {
10294
- const ops = getComponentOperations(element.type);
10295
- if (ops && ops.validate) {
10296
- return ops.validate(element, key, context);
10297
- }
10298
- return null;
10299
- }
10300
- function updateElementWithComponent(element, fieldPath, value, context) {
10301
- const ops = getComponentOperations(element.type);
10302
- if (ops && ops.update) {
10303
- ops.update(element, fieldPath, value, context);
10304
- return true;
10305
- }
10306
- return false;
10307
- }
10308
10894
 
10309
10895
  // src/instance/FormBuilderInstance.ts
10310
10896
  var FormBuilderInstance = class {
@@ -10429,26 +11015,37 @@ var FormBuilderInstance = class {
10429
11015
  }
10430
11016
  }
10431
11017
  /**
10432
- * Find the DOM element corresponding to a field path (instance-scoped)
11018
+ * Find the DOM element corresponding to a field path (instance-scoped).
11019
+ *
11020
+ * Strategy:
11021
+ * 1. Try a `[name="…"]` lookup first — works for any field that renders
11022
+ * an input/hidden with the path as its name, in either mode. Some
11023
+ * readonly renderers still emit a hidden input (boolean, switcher),
11024
+ * so this path must run regardless of `state.config.readonly`. A
11025
+ * prior version gated this on edit mode only, which made
11026
+ * `updateField` / `setFormData` silently miss readonly boolean
11027
+ * fields whose component also opts out of the standard label row
11028
+ * (`ownsLabel: true`).
11029
+ * 2. If no input matched, fall back to locating the field wrapper by
11030
+ * its visible label text — needed for readonly previews that don't
11031
+ * emit any `name=` attribute (e.g. file/markdown previews).
10433
11032
  */
10434
11033
  findFormElementByFieldPath(fieldPath) {
10435
11034
  if (!this.state.formRoot) return null;
10436
- if (!this.state.config.readonly) {
10437
- let element = this.state.formRoot.querySelector(
10438
- `[name="${fieldPath}"]`
11035
+ let element = this.state.formRoot.querySelector(
11036
+ `[name="${fieldPath}"]`
11037
+ );
11038
+ if (element) return element;
11039
+ const variations = [
11040
+ fieldPath,
11041
+ fieldPath.replace(/\[(\d+)\]/g, "[$1]"),
11042
+ fieldPath.replace(/\./g, "[") + "]".repeat((fieldPath.match(/\./g) || []).length)
11043
+ ];
11044
+ for (const variation of variations) {
11045
+ element = this.state.formRoot.querySelector(
11046
+ `[name="${variation}"]`
10439
11047
  );
10440
11048
  if (element) return element;
10441
- const variations = [
10442
- fieldPath,
10443
- fieldPath.replace(/\[(\d+)\]/g, "[$1]"),
10444
- fieldPath.replace(/\./g, "[") + "]".repeat((fieldPath.match(/\./g) || []).length)
10445
- ];
10446
- for (const variation of variations) {
10447
- element = this.state.formRoot.querySelector(
10448
- `[name="${variation}"]`
10449
- );
10450
- if (element) return element;
10451
- }
10452
11049
  }
10453
11050
  const schemaElement = this.findSchemaElement(fieldPath);
10454
11051
  if (!schemaElement) return null;