@dmitryvim/form-builder 0.2.32 → 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
@@ -398,47 +398,331 @@ function deepEqual(a, b) {
398
398
  return a === b;
399
399
  }
400
400
 
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
+ }
477
+ function mountCounterInLabel(wrapper, counter) {
478
+ const labelRow = wrapper.querySelector(
479
+ ":scope > [data-fb-label-row]"
480
+ );
481
+ if (labelRow) labelRow.appendChild(counter);
482
+ }
483
+ function createAddItemRow(classNameSuffix, onClick, options = {}) {
484
+ const label = options.label ?? "";
485
+ const showCounter = options.showCounter !== false;
486
+ const row = document.createElement("div");
487
+ row.className = "fb-add-row mt-2";
488
+ row.style.cssText = "display:flex;align-items:stretch;width:100%;";
489
+ const button = document.createElement("button");
490
+ button.type = "button";
491
+ button.className = `add-${classNameSuffix}-btn`;
492
+ button.style.cssText = `
493
+ flex: 1 1 auto;
494
+ display: inline-flex;
495
+ align-items: center;
496
+ justify-content: center;
497
+ gap: 6px;
498
+ padding: 6px 10px;
499
+ border: 1px dashed var(--fb-primary-color);
500
+ border-radius: var(--fb-border-radius);
501
+ background: transparent;
502
+ color: var(--fb-primary-color);
503
+ font-size: var(--fb-font-size-small, var(--fb-font-size));
504
+ font-weight: 500;
505
+ font-family: var(--fb-font-family);
506
+ cursor: pointer;
507
+ transition: border-color var(--fb-transition-duration), color var(--fb-transition-duration), background-color var(--fb-transition-duration);
508
+ `;
509
+ button.textContent = label ? `+ ${label}` : "+";
510
+ button.addEventListener("mouseenter", () => {
511
+ if (button.disabled) return;
512
+ button.style.borderStyle = "solid";
513
+ button.style.backgroundColor = "var(--fb-background-hover-color)";
514
+ });
515
+ button.addEventListener("mouseleave", () => {
516
+ button.style.borderStyle = "dashed";
517
+ button.style.backgroundColor = "transparent";
518
+ });
519
+ button.onclick = onClick;
520
+ const counter = document.createElement("span");
521
+ counter.className = "fb-add-counter";
522
+ counter.style.cssText = `
523
+ margin-left: auto;
524
+ font-size: var(--fb-font-size-small, 0.875rem);
525
+ color: var(--fb-text-secondary-color);
526
+ font-weight: 400;
527
+ `;
528
+ if (!showCounter) counter.style.display = "none";
529
+ row.appendChild(button);
530
+ const update = (current, max) => {
531
+ const reached = current >= max;
532
+ row.style.display = reached ? "none" : "flex";
533
+ button.style.display = reached ? "none" : "inline-flex";
534
+ button.disabled = reached;
535
+ if (showCounter) {
536
+ counter.textContent = `${current}/${max === Infinity ? "\u221E" : max}`;
537
+ }
538
+ };
539
+ return { row, button, counter, update };
540
+ }
541
+ function createSlideAddTile(onClick, options = {}) {
542
+ const label = options.label ?? "";
543
+ const tile = document.createElement("button");
544
+ tile.type = "button";
545
+ tile.className = "add-container-btn fb-slide-add";
546
+ tile.style.cssText = `
547
+ display: flex;
548
+ flex-direction: column;
549
+ align-items: center;
550
+ justify-content: center;
551
+ gap: 12px;
552
+ width: 100%;
553
+ min-height: 180px;
554
+ align-self: stretch;
555
+ padding: 24px 16px;
556
+ border: 1.5px dashed var(--fb-primary-color);
557
+ border-radius: var(--fb-border-radius);
558
+ background: transparent;
559
+ color: var(--fb-primary-color);
560
+ font-size: var(--fb-font-size-small, var(--fb-font-size));
561
+ font-weight: 500;
562
+ font-family: var(--fb-font-family);
563
+ cursor: pointer;
564
+ transition: border-color var(--fb-transition-duration), color var(--fb-transition-duration), background-color var(--fb-transition-duration);
565
+ `;
566
+ const circle = document.createElement("span");
567
+ circle.className = "fb-slide-add-circle";
568
+ circle.style.cssText = `
569
+ display: inline-flex;
570
+ align-items: center;
571
+ justify-content: center;
572
+ width: 36px;
573
+ height: 36px;
574
+ border: 1px solid var(--fb-primary-color);
575
+ border-radius: 50%;
576
+ background: var(--fb-background-color);
577
+ font-size: 20px;
578
+ line-height: 1;
579
+ color: inherit;
580
+ transition: inherit;
581
+ `;
582
+ circle.textContent = "+";
583
+ tile.appendChild(circle);
584
+ if (label) {
585
+ const text = document.createElement("span");
586
+ text.textContent = label;
587
+ tile.appendChild(text);
588
+ }
589
+ tile.addEventListener("mouseenter", () => {
590
+ if (tile.disabled) return;
591
+ tile.style.borderStyle = "solid";
592
+ tile.style.backgroundColor = "var(--fb-background-hover-color)";
593
+ });
594
+ tile.addEventListener("mouseleave", () => {
595
+ tile.style.borderStyle = "dashed";
596
+ tile.style.backgroundColor = "transparent";
597
+ });
598
+ tile.onclick = onClick;
599
+ const counter = document.createElement("span");
600
+ counter.className = "fb-add-counter";
601
+ counter.style.cssText = `
602
+ margin-left: auto;
603
+ font-size: var(--fb-font-size-small, 0.875rem);
604
+ color: var(--fb-text-secondary-color);
605
+ font-weight: 400;
606
+ `;
607
+ const update = (current, max) => {
608
+ const reached = current >= max;
609
+ tile.style.display = reached ? "none" : "flex";
610
+ tile.disabled = reached;
611
+ counter.textContent = `${current}/${max === Infinity ? "\u221E" : max}`;
612
+ };
613
+ return { tile, counter, update };
614
+ }
615
+ function applyActionButtonStyles(button, isFormLevel = false) {
616
+ button.style.cssText = `
617
+ background-color: var(--fb-action-bg-color);
618
+ color: var(--fb-action-text-color);
619
+ border: var(--fb-border-width) solid var(--fb-action-border-color);
620
+ padding: ${isFormLevel ? "0.5rem 1rem" : "0.5rem 0.75rem"};
621
+ font-size: var(--fb-font-size);
622
+ font-weight: var(--fb-font-weight-medium);
623
+ border-radius: var(--fb-border-radius);
624
+ transition: all var(--fb-transition-duration);
625
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
626
+ `;
627
+ button.addEventListener("mouseenter", () => {
628
+ button.style.backgroundColor = "var(--fb-action-hover-bg-color)";
629
+ button.style.borderColor = "var(--fb-action-hover-border-color)";
630
+ });
631
+ button.addEventListener("mouseleave", () => {
632
+ button.style.backgroundColor = "var(--fb-action-bg-color)";
633
+ button.style.borderColor = "var(--fb-action-border-color)";
634
+ });
635
+ }
636
+
401
637
  // src/components/text.ts
402
- 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) {
403
705
  const counter = document.createElement("span");
404
706
  counter.className = "char-counter";
405
707
  counter.style.cssText = `
406
- position: absolute;
407
- ${isTextarea ? "bottom: 8px" : "top: 50%; transform: translateY(-50%)"};
408
- right: 10px;
708
+ margin-top: 4px;
709
+ padding-right: 12px;
710
+ text-align: right;
409
711
  font-size: var(--fb-font-size-small);
410
- color: var(--fb-text-secondary-color);
712
+ line-height: 1;
713
+ color: var(--fb-error-color);
411
714
  pointer-events: none;
412
- background: var(--fb-background-color);
413
- padding: 0 4px;
715
+ display: none;
414
716
  `;
415
717
  const updateCounter = () => {
416
718
  const len = input.value.length;
417
- const min = element.minLength;
418
719
  const max = element.maxLength;
419
- if (min == null && max == null) {
420
- counter.textContent = "";
421
- return;
422
- }
423
- if (len === 0 || min != null && len < min) {
424
- if (min != null && max != null) {
425
- counter.textContent = `${min}-${max}`;
426
- } else if (max != null) {
427
- counter.textContent = `\u2264${max}`;
428
- } else if (min != null) {
429
- counter.textContent = `\u2265${min}`;
430
- }
431
- counter.style.color = "var(--fb-text-secondary-color)";
432
- } else if (max != null && len > max) {
720
+ if (max != null && len > max) {
433
721
  counter.textContent = `${len}/${max}`;
434
- counter.style.color = "var(--fb-error-color)";
722
+ counter.style.display = "block";
435
723
  } else {
436
- if (max != null) {
437
- counter.textContent = `${len}/${max}`;
438
- } else {
439
- counter.textContent = `${len}`;
440
- }
441
- counter.style.color = "var(--fb-text-secondary-color)";
724
+ counter.textContent = "";
725
+ counter.style.display = "none";
442
726
  }
443
727
  };
444
728
  input.addEventListener("input", updateCounter);
@@ -450,26 +734,33 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
450
734
  const readonly = isElementReadonly(element, state, ctx);
451
735
  const inputWrapper = document.createElement("div");
452
736
  inputWrapper.style.cssText = "position: relative;";
453
- const textInput = document.createElement("input");
454
- textInput.type = "text";
737
+ const hasCharCounter = !readonly && (element.minLength != null || element.maxLength != null);
738
+ const textInput = document.createElement("textarea");
739
+ textInput.rows = 1;
455
740
  textInput.className = "w-full rounded-lg";
456
741
  textInput.style.cssText = `
457
742
  padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
458
- padding-right: 60px;
459
743
  border: var(--fb-border-width) solid var(--fb-border-color);
460
744
  border-radius: var(--fb-border-radius);
461
745
  background-color: ${readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
462
746
  color: var(--fb-text-color);
463
747
  font-size: var(--fb-font-size);
464
748
  font-family: var(--fb-font-family);
749
+ line-height: var(--fb-line-height, 1.5);
465
750
  transition: all var(--fb-transition-duration) ease-in-out;
466
751
  width: 100%;
467
752
  box-sizing: border-box;
753
+ resize: none;
754
+ overflow: hidden;
755
+ word-break: break-word;
756
+ overflow-wrap: anywhere;
468
757
  `;
469
758
  textInput.name = pathKey;
470
759
  textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
471
760
  textInput.value = ctx.prefill[element.key] || element.default || "";
472
761
  textInput.readOnly = readonly;
762
+ applySingleLineMode(textInput);
763
+ applyAutoExpand(textInput);
473
764
  if (!readonly) {
474
765
  textInput.addEventListener("focus", () => {
475
766
  textInput.style.borderColor = "var(--fb-border-focus-color)";
@@ -500,8 +791,8 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
500
791
  textInput.addEventListener("input", handleChange);
501
792
  }
502
793
  inputWrapper.appendChild(textInput);
503
- if (!readonly && (element.minLength != null || element.maxLength != null)) {
504
- const counter = createCharCounter(element, textInput, false);
794
+ if (hasCharCounter) {
795
+ const counter = createCharCounter(element, textInput);
505
796
  inputWrapper.appendChild(counter);
506
797
  }
507
798
  wrapper.appendChild(inputWrapper);
@@ -516,174 +807,100 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
516
807
  while (values.length < minCount) {
517
808
  values.push(element.default || "");
518
809
  }
519
- const container = document.createElement("div");
520
- container.className = "space-y-2";
521
- wrapper.appendChild(container);
810
+ ensureChipStyles(document);
811
+ const list = document.createElement("div");
812
+ list.className = "fb-chip-list";
813
+ wrapper.appendChild(list);
522
814
  function updateIndices() {
523
- const items = container.querySelectorAll(".multiple-text-item");
524
- items.forEach((item, index) => {
525
- const input = item.querySelector("input");
526
- if (input) {
527
- 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}`;
528
822
  }
529
823
  });
530
824
  }
531
- function addTextItem(value = "", index = -1) {
532
- const itemWrapper = document.createElement("div");
533
- itemWrapper.className = "multiple-text-item flex items-center gap-2";
534
- const inputContainer = document.createElement("div");
535
- inputContainer.style.cssText = "position: relative; flex: 1;";
536
- const textInput = document.createElement("input");
537
- textInput.type = "text";
538
- textInput.style.cssText = `
539
- padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
540
- padding-right: 60px;
541
- border: var(--fb-border-width) solid var(--fb-border-color);
542
- border-radius: var(--fb-border-radius);
543
- background-color: ${readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
544
- color: var(--fb-text-color);
545
- font-size: var(--fb-font-size);
546
- font-family: var(--fb-font-family);
547
- transition: all var(--fb-transition-duration) ease-in-out;
548
- width: 100%;
549
- box-sizing: border-box;
550
- `;
551
- textInput.placeholder = element.placeholder || t("placeholderText", state);
552
- textInput.value = value;
553
- textInput.readOnly = readonly;
554
- if (!readonly) {
555
- textInput.addEventListener("focus", () => {
556
- textInput.style.borderColor = "var(--fb-border-focus-color)";
557
- textInput.style.outline = `var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)`;
558
- textInput.style.outlineOffset = "0";
559
- });
560
- textInput.addEventListener("blur", () => {
561
- textInput.style.borderColor = "var(--fb-border-color)";
562
- textInput.style.outline = "none";
563
- });
564
- textInput.addEventListener("mouseenter", () => {
565
- if (document.activeElement !== textInput) {
566
- textInput.style.borderColor = "var(--fb-border-hover-color)";
567
- }
568
- });
569
- textInput.addEventListener("mouseleave", () => {
570
- if (document.activeElement !== textInput) {
571
- textInput.style.borderColor = "var(--fb-border-color)";
572
- }
573
- });
574
- }
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);
575
839
  if (!readonly && ctx.instance) {
576
840
  const handleChange = () => {
577
- const value2 = textInput.value === "" ? null : textInput.value;
578
- ctx.instance.triggerOnChange(textInput.name, value2);
841
+ ctx.instance.triggerOnChange(
842
+ input.name,
843
+ input.value === "" ? null : input.value
844
+ );
579
845
  };
580
- textInput.addEventListener("blur", handleChange);
581
- textInput.addEventListener("input", handleChange);
582
- }
583
- inputContainer.appendChild(textInput);
584
- if (!readonly && (element.minLength != null || element.maxLength != null)) {
585
- const counter = createCharCounter(element, textInput, false);
586
- inputContainer.appendChild(counter);
846
+ input.addEventListener("blur", handleChange);
847
+ input.addEventListener("input", handleChange);
587
848
  }
588
- itemWrapper.appendChild(inputContainer);
589
- if (index === -1) {
590
- container.appendChild(itemWrapper);
591
- } else {
592
- 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);
593
871
  }
872
+ list.appendChild(chip);
594
873
  updateIndices();
595
- return itemWrapper;
874
+ return chip;
596
875
  }
597
876
  function updateRemoveButtons() {
598
877
  if (readonly) return;
599
- const items = container.querySelectorAll(".multiple-text-item");
600
- const currentCount = items.length;
601
- items.forEach((item) => {
602
- let removeBtn = item.querySelector(
603
- ".remove-item-btn"
604
- );
605
- if (!removeBtn) {
606
- removeBtn = document.createElement("button");
607
- removeBtn.type = "button";
608
- removeBtn.className = "remove-item-btn px-2 py-1 rounded";
609
- removeBtn.style.cssText = `
610
- color: var(--fb-error-color);
611
- background-color: transparent;
612
- transition: background-color var(--fb-transition-duration);
613
- `;
614
- removeBtn.innerHTML = "\u2715";
615
- removeBtn.addEventListener("mouseenter", () => {
616
- removeBtn.style.backgroundColor = "var(--fb-background-hover-color)";
617
- });
618
- removeBtn.addEventListener("mouseleave", () => {
619
- removeBtn.style.backgroundColor = "transparent";
620
- });
621
- removeBtn.onclick = () => {
622
- const currentIndex = Array.from(container.children).indexOf(
623
- item
624
- );
625
- if (container.children.length > minCount) {
626
- values.splice(currentIndex, 1);
627
- item.remove();
628
- updateIndices();
629
- updateAddButton();
630
- updateRemoveButtons();
631
- }
632
- };
633
- item.appendChild(removeBtn);
634
- }
635
- const disabled = currentCount <= minCount;
636
- removeBtn.disabled = disabled;
637
- removeBtn.style.opacity = disabled ? "0.5" : "1";
638
- 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;
639
882
  });
640
883
  }
641
- let addRow = null;
642
- let countDisplay = null;
884
+ let addUpdate = null;
643
885
  if (!readonly) {
644
- addRow = document.createElement("div");
645
- addRow.className = "flex items-center gap-3 mt-2";
646
- const addBtn = document.createElement("button");
647
- addBtn.type = "button";
648
- addBtn.className = "add-text-btn px-3 py-1 rounded";
649
- addBtn.style.cssText = `
650
- color: var(--fb-primary-color);
651
- border: var(--fb-border-width) solid var(--fb-primary-color);
652
- background-color: transparent;
653
- font-size: var(--fb-font-size);
654
- transition: all var(--fb-transition-duration);
655
- `;
656
- addBtn.textContent = "+";
657
- addBtn.addEventListener("mouseenter", () => {
658
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
659
- });
660
- addBtn.addEventListener("mouseleave", () => {
661
- addBtn.style.backgroundColor = "transparent";
662
- });
663
- addBtn.onclick = () => {
664
- values.push(element.default || "");
665
- addTextItem(element.default || "");
666
- updateAddButton();
667
- updateRemoveButtons();
668
- };
669
- countDisplay = document.createElement("span");
670
- countDisplay.className = "text-sm text-gray-500";
671
- addRow.appendChild(addBtn);
672
- addRow.appendChild(countDisplay);
673
- wrapper.appendChild(addRow);
886
+ const handle = createAddItemRow(
887
+ "text",
888
+ () => {
889
+ values.push(element.default || "");
890
+ addChip(element.default || "");
891
+ updateAddButton();
892
+ updateRemoveButtons();
893
+ },
894
+ { label: element.addLabel }
895
+ );
896
+ addUpdate = handle.update;
897
+ mountCounterInLabel(wrapper, handle.counter);
898
+ wrapper.appendChild(handle.row);
674
899
  }
675
900
  function updateAddButton() {
676
- if (!addRow || !countDisplay) return;
677
- const addBtn = addRow.querySelector(".add-text-btn");
678
- if (addBtn) {
679
- const disabled = values.length >= maxCount;
680
- addBtn.disabled = disabled;
681
- addBtn.style.opacity = disabled ? "0.5" : "1";
682
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
683
- }
684
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
901
+ if (addUpdate) addUpdate(values.length, maxCount);
685
902
  }
686
- values.forEach((value) => addTextItem(value));
903
+ values.forEach((value) => addChip(value));
687
904
  updateAddButton();
688
905
  updateRemoveButtons();
689
906
  }
@@ -706,10 +923,12 @@ function validateTextElement(element, key, context) {
706
923
  font-size: var(--fb-font-size-small);
707
924
  margin-top: 0.25rem;
708
925
  `;
709
- if (input.nextSibling) {
710
- 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);
711
930
  } else {
712
- input.parentNode?.appendChild(errorElement);
931
+ anchor.parentNode?.appendChild(errorElement);
713
932
  }
714
933
  }
715
934
  errorElement.textContent = errorMessage;
@@ -758,7 +977,7 @@ function validateTextElement(element, key, context) {
758
977
  }
759
978
  };
760
979
  if (element.multiple) {
761
- const inputs = scopeRoot.querySelectorAll(`[name^="${key}["]`);
980
+ const inputs = scopeRoot.querySelectorAll(`[name^="${key}\\["]`);
762
981
  const values = [];
763
982
  const rawValues = [];
764
983
  inputs.forEach((input, index) => {
@@ -807,12 +1026,14 @@ function updateTextField(element, fieldPath, value, context) {
807
1026
  );
808
1027
  return;
809
1028
  }
810
- const inputs = scopeRoot.querySelectorAll(`[name^="${fieldPath}["]`);
1029
+ const inputs = scopeRoot.querySelectorAll(`[name^="${fieldPath}\\["]`);
811
1030
  inputs.forEach((input, index) => {
812
1031
  if (index < value.length) {
813
1032
  input.value = value[index] != null ? String(value[index]) : "";
814
1033
  input.classList.remove("invalid");
815
1034
  input.title = "";
1035
+ clearFieldError(input);
1036
+ input.dispatchEvent(new Event("input", { bubbles: true }));
816
1037
  }
817
1038
  });
818
1039
  if (value.length !== inputs.length) {
@@ -826,26 +1047,15 @@ function updateTextField(element, fieldPath, value, context) {
826
1047
  input.value = value != null ? String(value) : "";
827
1048
  input.classList.remove("invalid");
828
1049
  input.title = "";
1050
+ clearFieldError(input);
1051
+ if (input instanceof HTMLTextAreaElement) {
1052
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1053
+ }
829
1054
  }
830
1055
  }
831
1056
  }
832
1057
 
833
1058
  // src/components/textarea.ts
834
- function applyAutoExpand(textarea) {
835
- textarea.style.overflow = "hidden";
836
- textarea.style.resize = "none";
837
- const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
838
- textarea.rows = Math.max(1, lineCount);
839
- const resize = () => {
840
- if (!textarea.isConnected) return;
841
- textarea.style.height = "0";
842
- textarea.style.height = `${textarea.scrollHeight}px`;
843
- };
844
- textarea.addEventListener("input", resize);
845
- setTimeout(() => {
846
- if (textarea.isConnected) resize();
847
- }, 0);
848
- }
849
1059
  function renderTextareaElement(element, ctx, wrapper, pathKey) {
850
1060
  const state = ctx.state;
851
1061
  const readonly = isElementReadonly(element, state, ctx);
@@ -876,7 +1086,7 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
876
1086
  }
877
1087
  textareaWrapper.appendChild(textareaInput);
878
1088
  if (!readonly && (element.minLength != null || element.maxLength != null)) {
879
- const counter = createCharCounter(element, textareaInput, true);
1089
+ const counter = createCharCounter(element, textareaInput);
880
1090
  textareaWrapper.appendChild(counter);
881
1091
  }
882
1092
  wrapper.appendChild(textareaWrapper);
@@ -932,7 +1142,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
932
1142
  }
933
1143
  textareaContainer.appendChild(textareaInput);
934
1144
  if (!readonly && (element.minLength != null || element.maxLength != null)) {
935
- const counter = createCharCounter(element, textareaInput, true);
1145
+ const counter = createCharCounter(element, textareaInput);
936
1146
  textareaContainer.appendChild(counter);
937
1147
  }
938
1148
  itemWrapper.appendChild(textareaContainer);
@@ -977,52 +1187,24 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
977
1187
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
978
1188
  });
979
1189
  }
980
- let addRow = null;
981
- let countDisplay = null;
1190
+ let addUpdate = null;
982
1191
  if (!readonly) {
983
- addRow = document.createElement("div");
984
- addRow.className = "flex items-center gap-3 mt-2";
985
- const addBtn = document.createElement("button");
986
- addBtn.type = "button";
987
- addBtn.className = "add-textarea-btn px-3 py-1 rounded";
988
- addBtn.style.cssText = `
989
- color: var(--fb-primary-color);
990
- border: var(--fb-border-width) solid var(--fb-primary-color);
991
- background-color: transparent;
992
- font-size: var(--fb-font-size);
993
- transition: all var(--fb-transition-duration);
994
- `;
995
- addBtn.textContent = "+";
996
- addBtn.addEventListener("mouseenter", () => {
997
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
998
- });
999
- addBtn.addEventListener("mouseleave", () => {
1000
- addBtn.style.backgroundColor = "transparent";
1001
- });
1002
- addBtn.onclick = () => {
1003
- values.push(element.default || "");
1004
- addTextareaItem(element.default || "");
1005
- updateAddButton();
1006
- updateRemoveButtons();
1007
- };
1008
- countDisplay = document.createElement("span");
1009
- countDisplay.className = "text-sm text-gray-500";
1010
- addRow.appendChild(addBtn);
1011
- addRow.appendChild(countDisplay);
1012
- wrapper.appendChild(addRow);
1192
+ const handle = createAddItemRow(
1193
+ "textarea",
1194
+ () => {
1195
+ values.push(element.default || "");
1196
+ addTextareaItem(element.default || "");
1197
+ updateAddButton();
1198
+ updateRemoveButtons();
1199
+ },
1200
+ { label: element.addLabel }
1201
+ );
1202
+ addUpdate = handle.update;
1203
+ mountCounterInLabel(wrapper, handle.counter);
1204
+ wrapper.appendChild(handle.row);
1013
1205
  }
1014
1206
  function updateAddButton() {
1015
- if (!addRow || !countDisplay) return;
1016
- const addBtn = addRow.querySelector(
1017
- ".add-textarea-btn"
1018
- );
1019
- if (addBtn) {
1020
- const disabled = values.length >= maxCount;
1021
- addBtn.disabled = disabled;
1022
- addBtn.style.opacity = disabled ? "0.5" : "1";
1023
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
1024
- }
1025
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
1207
+ if (addUpdate) addUpdate(values.length, maxCount);
1026
1208
  }
1027
1209
  values.forEach((value) => addTextareaItem(value));
1028
1210
  updateAddButton();
@@ -1054,6 +1236,89 @@ function updateTextareaField(element, fieldPath, value, context) {
1054
1236
  }
1055
1237
 
1056
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
+ }
1057
1322
  function createNumberRangeHint(element, input) {
1058
1323
  const hint = document.createElement("span");
1059
1324
  hint.className = "number-range-hint";
@@ -1100,14 +1365,6 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1100
1365
  inputWrapper.style.cssText = "position: relative;";
1101
1366
  const numberInput = document.createElement("input");
1102
1367
  numberInput.type = "number";
1103
- numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1104
- numberInput.style.cssText = `
1105
- padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
1106
- font-size: var(--fb-font-size);
1107
- font-family: var(--fb-font-family);
1108
- width: 100%;
1109
- box-sizing: border-box;
1110
- `;
1111
1368
  numberInput.name = pathKey;
1112
1369
  numberInput.placeholder = element.placeholder || "0";
1113
1370
  if (element.min !== void 0) numberInput.min = element.min.toString();
@@ -1115,6 +1372,16 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1115
1372
  if (element.step !== void 0) numberInput.step = element.step.toString();
1116
1373
  numberInput.value = ctx.prefill[element.key] || element.default || "";
1117
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
+ }
1118
1385
  if (!readonly && ctx.instance) {
1119
1386
  const handleChange = () => {
1120
1387
  const value = numberInput.value ? parseFloat(numberInput.value) : null;
@@ -1123,10 +1390,14 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1123
1390
  numberInput.addEventListener("blur", handleChange);
1124
1391
  numberInput.addEventListener("input", handleChange);
1125
1392
  }
1126
- inputWrapper.appendChild(numberInput);
1127
- if (!readonly && (element.min != null || element.max != null)) {
1128
- const counter = createNumberRangeHint(element, numberInput);
1129
- 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
+ }
1130
1401
  }
1131
1402
  wrapper.appendChild(inputWrapper);
1132
1403
  }
@@ -1228,50 +1499,24 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1228
1499
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1229
1500
  });
1230
1501
  }
1231
- let addRow = null;
1232
- let countDisplay = null;
1502
+ let addUpdate = null;
1233
1503
  if (!readonly) {
1234
- addRow = document.createElement("div");
1235
- addRow.className = "flex items-center gap-3 mt-2";
1236
- const addBtn = document.createElement("button");
1237
- addBtn.type = "button";
1238
- addBtn.className = "add-number-btn px-3 py-1 rounded";
1239
- addBtn.style.cssText = `
1240
- color: var(--fb-primary-color);
1241
- border: var(--fb-border-width) solid var(--fb-primary-color);
1242
- background-color: transparent;
1243
- font-size: var(--fb-font-size);
1244
- transition: all var(--fb-transition-duration);
1245
- `;
1246
- addBtn.textContent = "+";
1247
- addBtn.addEventListener("mouseenter", () => {
1248
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
1249
- });
1250
- addBtn.addEventListener("mouseleave", () => {
1251
- addBtn.style.backgroundColor = "transparent";
1252
- });
1253
- addBtn.onclick = () => {
1254
- values.push(element.default || "");
1255
- addNumberItem(element.default || "");
1256
- updateAddButton();
1257
- updateRemoveButtons();
1258
- };
1259
- countDisplay = document.createElement("span");
1260
- countDisplay.className = "text-sm text-gray-500";
1261
- addRow.appendChild(addBtn);
1262
- addRow.appendChild(countDisplay);
1263
- wrapper.appendChild(addRow);
1504
+ const handle = createAddItemRow(
1505
+ "number",
1506
+ () => {
1507
+ values.push(element.default || "");
1508
+ addNumberItem(element.default || "");
1509
+ updateAddButton();
1510
+ updateRemoveButtons();
1511
+ },
1512
+ { label: element.addLabel }
1513
+ );
1514
+ addUpdate = handle.update;
1515
+ mountCounterInLabel(wrapper, handle.counter);
1516
+ wrapper.appendChild(handle.row);
1264
1517
  }
1265
1518
  function updateAddButton() {
1266
- if (!addRow || !countDisplay) return;
1267
- const addBtn = addRow.querySelector(".add-number-btn");
1268
- if (addBtn) {
1269
- const disabled = values.length >= maxCount;
1270
- addBtn.disabled = disabled;
1271
- addBtn.style.opacity = disabled ? "0.5" : "1";
1272
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
1273
- }
1274
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
1519
+ if (addUpdate) addUpdate(values.length, maxCount);
1275
1520
  }
1276
1521
  values.forEach((value) => addNumberItem(value));
1277
1522
  updateAddButton();
@@ -1406,13 +1651,14 @@ function updateNumberField(element, fieldPath, value, context) {
1406
1651
  return;
1407
1652
  }
1408
1653
  const inputs = scopeRoot.querySelectorAll(
1409
- `[name^="${fieldPath}["]`
1654
+ `[name^="${fieldPath}\\["]`
1410
1655
  );
1411
1656
  inputs.forEach((input, index) => {
1412
1657
  if (index < value.length) {
1413
1658
  input.value = value[index] != null ? String(value[index]) : "";
1414
1659
  input.classList.remove("invalid");
1415
1660
  input.title = "";
1661
+ clearFieldError(input);
1416
1662
  }
1417
1663
  });
1418
1664
  if (value.length !== inputs.length) {
@@ -1428,6 +1674,7 @@ function updateNumberField(element, fieldPath, value, context) {
1428
1674
  input.value = value != null ? String(value) : "";
1429
1675
  input.classList.remove("invalid");
1430
1676
  input.title = "";
1677
+ clearFieldError(input);
1431
1678
  }
1432
1679
  }
1433
1680
  }
@@ -1556,51 +1803,25 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1556
1803
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1557
1804
  });
1558
1805
  }
1559
- let addRow = null;
1560
- let countDisplay = null;
1806
+ let addUpdate = null;
1561
1807
  if (!readonly) {
1562
- addRow = document.createElement("div");
1563
- addRow.className = "flex items-center gap-3 mt-2";
1564
- const addBtn = document.createElement("button");
1565
- addBtn.type = "button";
1566
- addBtn.className = "add-select-btn px-3 py-1 rounded";
1567
- addBtn.style.cssText = `
1568
- color: var(--fb-primary-color);
1569
- border: var(--fb-border-width) solid var(--fb-primary-color);
1570
- background-color: transparent;
1571
- font-size: var(--fb-font-size);
1572
- transition: all var(--fb-transition-duration);
1573
- `;
1574
- addBtn.textContent = "+";
1575
- addBtn.addEventListener("mouseenter", () => {
1576
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
1577
- });
1578
- addBtn.addEventListener("mouseleave", () => {
1579
- addBtn.style.backgroundColor = "transparent";
1580
- });
1581
- addBtn.onclick = () => {
1582
- const defaultValue = element.default || element.options?.[0]?.value || "";
1583
- values.push(defaultValue);
1584
- addSelectItem(defaultValue);
1585
- updateAddButton();
1586
- updateRemoveButtons();
1587
- };
1588
- countDisplay = document.createElement("span");
1589
- countDisplay.className = "text-sm text-gray-500";
1590
- addRow.appendChild(addBtn);
1591
- addRow.appendChild(countDisplay);
1592
- wrapper.appendChild(addRow);
1808
+ const handle = createAddItemRow(
1809
+ "select",
1810
+ () => {
1811
+ const defaultValue = element.default || element.options?.[0]?.value || "";
1812
+ values.push(defaultValue);
1813
+ addSelectItem(defaultValue);
1814
+ updateAddButton();
1815
+ updateRemoveButtons();
1816
+ },
1817
+ { label: element.addLabel }
1818
+ );
1819
+ addUpdate = handle.update;
1820
+ mountCounterInLabel(wrapper, handle.counter);
1821
+ wrapper.appendChild(handle.row);
1593
1822
  }
1594
1823
  function updateAddButton() {
1595
- if (!addRow || !countDisplay) return;
1596
- const addBtn = addRow.querySelector(".add-select-btn");
1597
- if (addBtn) {
1598
- const disabled = values.length >= maxCount;
1599
- addBtn.disabled = disabled;
1600
- addBtn.style.opacity = disabled ? "0.5" : "1";
1601
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
1602
- }
1603
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
1824
+ if (addUpdate) addUpdate(values.length, maxCount);
1604
1825
  }
1605
1826
  values.forEach((value) => addSelectItem(value));
1606
1827
  updateAddButton();
@@ -1665,7 +1886,7 @@ function validateSelectElement(element, key, context) {
1665
1886
  };
1666
1887
  if ("multiple" in element && element.multiple) {
1667
1888
  const inputs = scopeRoot.querySelectorAll(
1668
- `[name^="${key}["]`
1889
+ `[name^="${key}\\["]`
1669
1890
  );
1670
1891
  const values = [];
1671
1892
  inputs.forEach((input) => {
@@ -1701,7 +1922,7 @@ function updateSelectField(element, fieldPath, value, context) {
1701
1922
  return;
1702
1923
  }
1703
1924
  const selects = scopeRoot.querySelectorAll(
1704
- `[name^="${fieldPath}["]`
1925
+ `[name^="${fieldPath}\\["]`
1705
1926
  );
1706
1927
  selects.forEach((select, index) => {
1707
1928
  if (index < value.length) {
@@ -1712,6 +1933,7 @@ function updateSelectField(element, fieldPath, value, context) {
1712
1933
  });
1713
1934
  select.classList.remove("invalid");
1714
1935
  select.title = "";
1936
+ clearFieldError(select);
1715
1937
  }
1716
1938
  });
1717
1939
  if (value.length !== selects.length) {
@@ -1731,72 +1953,147 @@ function updateSelectField(element, fieldPath, value, context) {
1731
1953
  });
1732
1954
  select.classList.remove("invalid");
1733
1955
  select.title = "";
1956
+ clearFieldError(select);
1734
1957
  }
1735
1958
  }
1736
1959
  }
1737
1960
 
1738
1961
  // src/components/switcher.ts
1739
- function applySelectedStyle(btn) {
1740
- btn.style.backgroundColor = "var(--fb-primary-color)";
1741
- btn.style.color = "#ffffff";
1742
- 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
+ }
1743
1972
  }
1744
- function applyUnselectedStyle(btn) {
1745
- btn.style.backgroundColor = "transparent";
1973
+ function applyUnselectedStyle(btn, isPreset) {
1974
+ btn.style.backgroundColor = isPreset ? "var(--fb-background-color)" : "transparent";
1746
1975
  btn.style.color = "var(--fb-text-color)";
1747
1976
  btn.style.borderColor = "var(--fb-border-color)";
1748
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
+ }
1749
2035
  function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onChange) {
1750
2036
  const options = element.options || [];
2037
+ const isPresetMode = options.some((o) => o.subtitle || o.iconUrl);
1751
2038
  const group = document.createElement("div");
1752
2039
  group.className = "fb-switcher-group";
1753
- group.style.cssText = `
1754
- display: inline-flex;
1755
- flex-direction: row;
1756
- flex-wrap: nowrap;
1757
- `;
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
+ `;
1758
2050
  const buttons = [];
1759
2051
  options.forEach((option, index) => {
1760
- const btn = document.createElement("button");
1761
- btn.type = "button";
1762
- btn.className = "fb-switcher-btn";
1763
- btn.dataset.value = option.value;
1764
- btn.textContent = option.label;
1765
- btn.style.cssText = `
1766
- padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
1767
- font-size: var(--fb-font-size);
1768
- border-width: var(--fb-border-width);
1769
- border-style: solid;
1770
- cursor: ${readonly ? "default" : "pointer"};
1771
- transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
1772
- white-space: nowrap;
1773
- line-height: 1.25;
1774
- outline: none;
1775
- `;
1776
- if (options.length === 1) {
1777
- btn.style.borderRadius = "var(--fb-border-radius)";
1778
- } else if (index === 0) {
1779
- btn.style.borderRadius = "var(--fb-border-radius) 0 0 var(--fb-border-radius)";
1780
- btn.style.borderRightWidth = "0";
1781
- } else if (index === options.length - 1) {
1782
- btn.style.borderRadius = "0 var(--fb-border-radius) var(--fb-border-radius) 0";
2052
+ let btn;
2053
+ if (isPresetMode) {
2054
+ btn = buildPresetCard(option, readonly);
1783
2055
  } else {
1784
- btn.style.borderRadius = "0";
1785
- 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
+ }
1786
2083
  }
1787
2084
  if (option.value === currentValue) {
1788
- applySelectedStyle(btn);
2085
+ applySelectedStyle(btn, isPresetMode);
1789
2086
  } else {
1790
- applyUnselectedStyle(btn);
2087
+ applyUnselectedStyle(btn, isPresetMode);
1791
2088
  }
1792
2089
  if (!readonly) {
1793
2090
  btn.addEventListener("click", () => {
1794
2091
  hiddenInput.value = option.value;
1795
2092
  buttons.forEach((b) => {
1796
2093
  if (b.dataset.value === option.value) {
1797
- applySelectedStyle(b);
2094
+ applySelectedStyle(b, isPresetMode);
1798
2095
  } else {
1799
- applyUnselectedStyle(b);
2096
+ applyUnselectedStyle(b, isPresetMode);
1800
2097
  }
1801
2098
  });
1802
2099
  if (onChange) {
@@ -1810,7 +2107,7 @@ function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onCha
1810
2107
  });
1811
2108
  btn.addEventListener("mouseleave", () => {
1812
2109
  if (hiddenInput.value !== option.value) {
1813
- btn.style.backgroundColor = "transparent";
2110
+ btn.style.backgroundColor = isPresetMode ? "var(--fb-background-color)" : "transparent";
1814
2111
  }
1815
2112
  });
1816
2113
  }
@@ -1943,53 +2240,25 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
1943
2240
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1944
2241
  });
1945
2242
  }
1946
- let addRow = null;
1947
- let countDisplay = null;
2243
+ let addUpdate = null;
1948
2244
  if (!readonly) {
1949
- addRow = document.createElement("div");
1950
- addRow.className = "flex items-center gap-3 mt-2";
1951
- const addBtn = document.createElement("button");
1952
- addBtn.type = "button";
1953
- addBtn.className = "add-switcher-btn px-3 py-1 rounded";
1954
- addBtn.style.cssText = `
1955
- color: var(--fb-primary-color);
1956
- border: var(--fb-border-width) solid var(--fb-primary-color);
1957
- background-color: transparent;
1958
- font-size: var(--fb-font-size);
1959
- transition: all var(--fb-transition-duration);
1960
- `;
1961
- addBtn.textContent = "+";
1962
- addBtn.addEventListener("mouseenter", () => {
1963
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
1964
- });
1965
- addBtn.addEventListener("mouseleave", () => {
1966
- addBtn.style.backgroundColor = "transparent";
1967
- });
1968
- addBtn.onclick = () => {
1969
- const defaultValue = element.default || element.options?.[0]?.value || "";
1970
- values.push(defaultValue);
1971
- addSwitcherItem(defaultValue);
1972
- updateAddButton();
1973
- updateRemoveButtons();
1974
- };
1975
- countDisplay = document.createElement("span");
1976
- countDisplay.className = "text-sm text-gray-500";
1977
- addRow.appendChild(addBtn);
1978
- addRow.appendChild(countDisplay);
1979
- wrapper.appendChild(addRow);
2245
+ const handle = createAddItemRow(
2246
+ "switcher",
2247
+ () => {
2248
+ const defaultValue = element.default || element.options?.[0]?.value || "";
2249
+ values.push(defaultValue);
2250
+ addSwitcherItem(defaultValue);
2251
+ updateAddButton();
2252
+ updateRemoveButtons();
2253
+ },
2254
+ { label: element.addLabel }
2255
+ );
2256
+ addUpdate = handle.update;
2257
+ mountCounterInLabel(wrapper, handle.counter);
2258
+ wrapper.appendChild(handle.row);
1980
2259
  }
1981
2260
  function updateAddButton() {
1982
- if (!addRow || !countDisplay) return;
1983
- const addBtn = addRow.querySelector(
1984
- ".add-switcher-btn"
1985
- );
1986
- if (addBtn) {
1987
- const disabled = values.length >= maxCount;
1988
- addBtn.disabled = disabled;
1989
- addBtn.style.opacity = disabled ? "0.5" : "1";
1990
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
1991
- }
1992
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
2261
+ if (addUpdate) addUpdate(values.length, maxCount);
1993
2262
  }
1994
2263
  values.forEach((value) => addSwitcherItem(value));
1995
2264
  updateAddButton();
@@ -2057,7 +2326,7 @@ function validateSwitcherElement(element, key, context) {
2057
2326
  );
2058
2327
  if ("multiple" in element && element.multiple) {
2059
2328
  const inputs = scopeRoot.querySelectorAll(
2060
- `input[type="hidden"][name^="${key}["]`
2329
+ `input[type="hidden"][name^="${key}\\["]`
2061
2330
  );
2062
2331
  const values = [];
2063
2332
  inputs.forEach((input) => {
@@ -2104,7 +2373,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
2104
2373
  return;
2105
2374
  }
2106
2375
  const inputs = scopeRoot.querySelectorAll(
2107
- `input[type="hidden"][name^="${fieldPath}["]`
2376
+ `input[type="hidden"][name^="${fieldPath}\\["]`
2108
2377
  );
2109
2378
  inputs.forEach((input, index) => {
2110
2379
  if (index < value.length) {
@@ -2113,15 +2382,17 @@ function updateSwitcherField(element, fieldPath, value, context) {
2113
2382
  const group = input.parentElement?.querySelector(".fb-switcher-group");
2114
2383
  if (group) {
2115
2384
  group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
2385
+ const isPreset = isPresetButton(btn);
2116
2386
  if (btn.dataset.value === newVal) {
2117
- applySelectedStyle(btn);
2387
+ applySelectedStyle(btn, isPreset);
2118
2388
  } else {
2119
- applyUnselectedStyle(btn);
2389
+ applyUnselectedStyle(btn, isPreset);
2120
2390
  }
2121
2391
  });
2122
2392
  }
2123
2393
  input.classList.remove("invalid");
2124
2394
  input.title = "";
2395
+ clearFieldError(input);
2125
2396
  }
2126
2397
  });
2127
2398
  if (value.length !== inputs.length) {
@@ -2139,16 +2410,208 @@ function updateSwitcherField(element, fieldPath, value, context) {
2139
2410
  const group = input.parentElement?.querySelector(".fb-switcher-group");
2140
2411
  if (group) {
2141
2412
  group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
2413
+ const isPreset = isPresetButton(btn);
2142
2414
  if (btn.dataset.value === newVal) {
2143
- applySelectedStyle(btn);
2415
+ applySelectedStyle(btn, isPreset);
2144
2416
  } else {
2145
- applyUnselectedStyle(btn);
2417
+ applyUnselectedStyle(btn, isPreset);
2146
2418
  }
2147
2419
  });
2148
2420
  }
2149
2421
  input.classList.remove("invalid");
2150
2422
  input.title = "";
2423
+ clearFieldError(input);
2424
+ }
2425
+ }
2426
+ }
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;
2151
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);
2152
2615
  }
2153
2616
  }
2154
2617
 
@@ -2252,14 +2715,20 @@ function ensureFileStyles() {
2252
2715
  }
2253
2716
 
2254
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). */
2255
2720
  .fb-wide-tile {
2256
2721
  width: 100%;
2722
+ box-sizing: border-box;
2257
2723
  border-radius: 0.75rem;
2258
2724
  border: 1px dashed #60a5fa;
2259
2725
  background: rgba(239,246,255,0.5);
2260
2726
  display: flex;
2727
+ flex-wrap: wrap;
2728
+ align-items: stretch;
2729
+ gap: 0;
2261
2730
  overflow: hidden;
2262
- height: 180px;
2731
+ min-height: 180px;
2263
2732
  transition: border-color 150ms, background 150ms, box-shadow 150ms;
2264
2733
  cursor: pointer;
2265
2734
  }
@@ -2273,9 +2742,12 @@ function ensureFileStyles() {
2273
2742
  box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
2274
2743
  }
2275
2744
 
2276
- /* 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. */
2277
2748
  .fb-wide-tile-upload {
2278
- flex: 1;
2749
+ flex: 1 1 220px;
2750
+ min-height: 140px;
2279
2751
  display: flex;
2280
2752
  flex-direction: column;
2281
2753
  align-items: center;
@@ -2288,24 +2760,21 @@ function ensureFileStyles() {
2288
2760
  background: transparent;
2289
2761
  border: none;
2290
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);
2291
2766
  }
2292
2767
  .fb-wide-tile-upload:hover {
2293
2768
  background: rgba(191,219,254,0.25);
2294
2769
  }
2295
-
2296
- /* Vertical dashed divider between upload and library zones */
2297
- .fb-wide-tile-divider {
2298
- width: 1px;
2299
- margin: 16px 0;
2300
- border-left: 1px dashed rgba(96,165,250,0.5);
2301
- background: transparent;
2302
- flex-shrink: 0;
2303
- }
2304
-
2305
- /* 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. */
2306
2775
  .fb-wide-tile-library {
2307
- width: 176px;
2308
- flex-shrink: 0;
2776
+ flex: 0 0 176px;
2777
+ min-height: 120px;
2309
2778
  display: flex;
2310
2779
  flex-direction: column;
2311
2780
  align-items: center;
@@ -2322,6 +2791,10 @@ function ensureFileStyles() {
2322
2791
  .fb-wide-tile-library:hover {
2323
2792
  background: rgba(191,219,254,0.25);
2324
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. */
2325
2798
 
2326
2799
  /* \u2500\u2500\u2500 Multi-file outer grid container \u2500\u2500\u2500 */
2327
2800
  .fb-multi-outer {
@@ -2700,6 +3173,39 @@ function ensureFileStyles() {
2700
3173
  }
2701
3174
  `;
2702
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
+ }
2703
3209
  }
2704
3210
 
2705
3211
  // src/components/file/dom.ts
@@ -2857,24 +3363,40 @@ function createTileActions(options) {
2857
3363
  return btn;
2858
3364
  };
2859
3365
  if (replaceHandler) {
2860
- const replaceBtn = makeBtn(ICON_REPLACE, t("replaceFile", state), "fb-tile-action-replace");
3366
+ const replaceBtn = makeBtn(
3367
+ ICON_REPLACE,
3368
+ t("replaceFile", state),
3369
+ "fb-tile-action-replace"
3370
+ );
2861
3371
  replaceBtn.addEventListener("click", () => replaceHandler());
2862
3372
  group.appendChild(replaceBtn);
2863
3373
  }
2864
3374
  if (libraryHandler) {
2865
- const libBtn = makeBtn(ICON_LIBRARY, t("fromLibrary", state), "fb-tile-action-library");
3375
+ const libBtn = makeBtn(
3376
+ ICON_LIBRARY,
3377
+ t("fromLibrary", state),
3378
+ "fb-tile-action-library"
3379
+ );
2866
3380
  libBtn.addEventListener("click", () => libraryHandler());
2867
3381
  group.appendChild(libBtn);
2868
3382
  }
2869
3383
  if (canDownload(state, meta)) {
2870
- const dlBtn = makeBtn(ICON_DOWNLOAD, t("downloadFile", state), "fb-tile-action-download");
3384
+ const dlBtn = makeBtn(
3385
+ ICON_DOWNLOAD,
3386
+ t("downloadFile", state),
3387
+ "fb-tile-action-download"
3388
+ );
2871
3389
  dlBtn.addEventListener("click", () => {
2872
3390
  triggerTileDownload(resourceId, fileName, state, meta);
2873
3391
  });
2874
3392
  group.appendChild(dlBtn);
2875
3393
  }
2876
3394
  if (canOpenInTab(state, meta)) {
2877
- const openBtn = makeBtn(ICON_OPEN, t("openInNewTab", state), "fb-tile-action-open");
3395
+ const openBtn = makeBtn(
3396
+ ICON_OPEN,
3397
+ t("openInNewTab", state),
3398
+ "fb-tile-action-open"
3399
+ );
2878
3400
  openBtn.addEventListener("click", () => {
2879
3401
  triggerTileOpen(resourceId, state, meta).catch((err) => {
2880
3402
  console.error("Open failed:", err);
@@ -2883,7 +3405,11 @@ function createTileActions(options) {
2883
3405
  group.appendChild(openBtn);
2884
3406
  }
2885
3407
  if (canRemove && removeHandler) {
2886
- const rmBtn = makeBtn(ICON_REMOVE, t("removeElement", state), "fb-tile-action-remove");
3408
+ const rmBtn = makeBtn(
3409
+ ICON_REMOVE,
3410
+ t("removeElement", state),
3411
+ "fb-tile-action-remove"
3412
+ );
2887
3413
  rmBtn.addEventListener("click", () => {
2888
3414
  removeHandler();
2889
3415
  });
@@ -2961,11 +3487,17 @@ function positionZoomPopup(popup, tile) {
2961
3487
  } else if (tileRect.bottom + margin + popupSize + padding <= window.innerHeight) {
2962
3488
  top = tileRect.bottom + margin;
2963
3489
  } else {
2964
- top = Math.max(padding, Math.min(window.innerHeight - popupSize - padding, tileRect.top));
3490
+ top = Math.max(
3491
+ padding,
3492
+ Math.min(window.innerHeight - popupSize - padding, tileRect.top)
3493
+ );
2965
3494
  }
2966
3495
  const tileCenterX = tileRect.left + tileRect.width / 2;
2967
3496
  let left = tileCenterX - popupSize / 2;
2968
- left = Math.max(padding, Math.min(window.innerWidth - popupSize - padding, left));
3497
+ left = Math.max(
3498
+ padding,
3499
+ Math.min(window.innerWidth - popupSize - padding, left)
3500
+ );
2969
3501
  popup.style.top = `${top}px`;
2970
3502
  popup.style.left = `${left}px`;
2971
3503
  }
@@ -3009,7 +3541,9 @@ function attachZoomHover(tile, src, alt, actionsEl) {
3009
3541
  const popup = getOrCreateZoomPopup();
3010
3542
  const existingActions = popup.querySelector(".fb-tile-actions");
3011
3543
  if (existingActions) existingActions.remove();
3012
- const img = popup.querySelector(".fb-tile-zoom-preview-img");
3544
+ const img = popup.querySelector(
3545
+ ".fb-tile-zoom-preview-img"
3546
+ );
3013
3547
  img.src = src;
3014
3548
  img.alt = alt;
3015
3549
  if (actionsEl) {
@@ -3037,7 +3571,9 @@ function attachZoomHover(tile, src, alt, actionsEl) {
3037
3571
  });
3038
3572
  }
3039
3573
  function attachClonedActionListeners(cloned, original) {
3040
- const originalBtns = Array.from(original.querySelectorAll(".fb-tile-action-btn"));
3574
+ const originalBtns = Array.from(
3575
+ original.querySelectorAll(".fb-tile-action-btn")
3576
+ );
3041
3577
  const clonedBtns = Array.from(cloned.querySelectorAll(".fb-tile-action-btn"));
3042
3578
  clonedBtns.forEach((clonedBtn, i) => {
3043
3579
  const origBtn = originalBtns[i];
@@ -3090,14 +3626,18 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
3090
3626
  return newContainer;
3091
3627
  }
3092
3628
  function attachVideoButtonHandlers(container, resourceId, state, deps) {
3093
- const changeBtn = container.querySelector(".change-file-btn");
3629
+ const changeBtn = container.querySelector(
3630
+ ".change-file-btn"
3631
+ );
3094
3632
  if (changeBtn) {
3095
3633
  changeBtn.onclick = (e) => {
3096
3634
  e.stopPropagation();
3097
3635
  deps?.picker?.click();
3098
3636
  };
3099
3637
  }
3100
- const deleteBtn = container.querySelector(".delete-file-btn");
3638
+ const deleteBtn = container.querySelector(
3639
+ ".delete-file-btn"
3640
+ );
3101
3641
  if (deleteBtn) {
3102
3642
  deleteBtn.onclick = (e) => {
3103
3643
  e.stopPropagation();
@@ -3230,7 +3770,13 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
3230
3770
  deps
3231
3771
  );
3232
3772
  } else {
3233
- await renderUploadedFilePreview(container, resourceId, fileName, meta, state);
3773
+ await renderUploadedFilePreview(
3774
+ container,
3775
+ resourceId,
3776
+ fileName,
3777
+ meta,
3778
+ state
3779
+ );
3234
3780
  const isVideo = meta?.type?.startsWith("video/");
3235
3781
  if (!isReadonly && !isVideo) {
3236
3782
  renderDeleteButton(container, resourceId, state);
@@ -3257,7 +3803,8 @@ async function renderFilePreviewReadonly(resourceId, state, fileName, options =
3257
3803
  }
3258
3804
  const localFileUrl = meta?.file instanceof File ? getLocalFileUrl(meta.file) : null;
3259
3805
  const resolveOpenUrl = async () => {
3260
- if (state.config.getDownloadUrl) return state.config.getDownloadUrl(resourceId);
3806
+ if (state.config.getDownloadUrl)
3807
+ return state.config.getDownloadUrl(resourceId);
3261
3808
  if (state.config.getThumbnail) return state.config.getThumbnail(resourceId);
3262
3809
  return localFileUrl;
3263
3810
  };
@@ -3397,7 +3944,8 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
3397
3944
  }
3398
3945
  } catch (error) {
3399
3946
  const err = error instanceof Error ? error : new Error(String(error));
3400
- if (state.config.onThumbnailError) state.config.onThumbnailError(err, rid);
3947
+ if (state.config.onThumbnailError)
3948
+ state.config.onThumbnailError(err, rid);
3401
3949
  tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:16px;color:var(--fb-error-color,#ef4444);">\u2715</div>`;
3402
3950
  }
3403
3951
  } else {
@@ -3430,7 +3978,8 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
3430
3978
  }
3431
3979
  } catch (error) {
3432
3980
  const err = error instanceof Error ? error : new Error(String(error));
3433
- if (state.config.onThumbnailError) state.config.onThumbnailError(err, rid);
3981
+ if (state.config.onThumbnailError)
3982
+ state.config.onThumbnailError(err, rid);
3434
3983
  tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:16px;color:var(--fb-error-color,#ef4444);">\u2715</div>`;
3435
3984
  }
3436
3985
  } else {
@@ -3463,7 +4012,8 @@ async function forceDownload(resourceId, fileName, state) {
3463
4012
  if (fileUrl) {
3464
4013
  const finalUrl = fileUrl.startsWith("http") ? fileUrl : new URL(fileUrl, window.location.href).href;
3465
4014
  const response = await fetch(finalUrl);
3466
- if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
4015
+ if (!response.ok)
4016
+ throw new Error(`HTTP error! status: ${response.status}`);
3467
4017
  const blob = await response.blob();
3468
4018
  downloadBlob(blob, fileName);
3469
4019
  } else {
@@ -3512,7 +4062,9 @@ async function uploadSingleFile(file, state) {
3512
4062
  } catch (error) {
3513
4063
  const err = error instanceof Error ? error : new Error(String(error));
3514
4064
  if (state.config.onUploadError) state.config.onUploadError(err, file);
3515
- throw new Error(`File upload failed: ${err.message}`);
4065
+ const wrapped = new Error(`File upload failed: ${err.message}`);
4066
+ wrapped.cause = err;
4067
+ throw wrapped;
3516
4068
  }
3517
4069
  }
3518
4070
  async function handleFileSelect(opts) {
@@ -3624,7 +4176,9 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
3624
4176
  const rejectedBySize = afterMime.filter(
3625
4177
  (f) => !isFileSizeAllowed(f, constraints.maxSize)
3626
4178
  );
3627
- const valid = afterMime.filter((f) => isFileSizeAllowed(f, constraints.maxSize));
4179
+ const valid = afterMime.filter(
4180
+ (f) => isFileSizeAllowed(f, constraints.maxSize)
4181
+ );
3628
4182
  const remaining = constraints.maxCount === Infinity ? valid.length : Math.max(0, constraints.maxCount - currentCount);
3629
4183
  const accepted = valid.slice(0, remaining);
3630
4184
  const skippedByCount = valid.length - accepted.length;
@@ -3637,7 +4191,13 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
3637
4191
  if (rejectedByMime.length > 0) {
3638
4192
  const mimes = constraints.allowedMimes.join(", ");
3639
4193
  const names = rejectedByMime.map((f) => f.name).join(", ");
3640
- errorParts.push(t("invalidFileMime", state, { name: names, type: rejectedByMime.map((f) => f.type).join(", "), mimes }));
4194
+ errorParts.push(
4195
+ t("invalidFileMime", state, {
4196
+ name: names,
4197
+ type: rejectedByMime.map((f) => f.type).join(", "),
4198
+ mimes
4199
+ })
4200
+ );
3641
4201
  }
3642
4202
  if (rejectedBySize.length > 0) {
3643
4203
  const names = rejectedBySize.map((f) => f.name).join(", ");
@@ -3661,7 +4221,8 @@ async function uploadBatch(accepted, resourceIds, listEl, state) {
3661
4221
  const addTile = tilesWrap.querySelector(".fb-multi-add-tile-js") ?? tilesWrap.querySelector(".fb-tile-add");
3662
4222
  if (addTile) addTile.style.display = "none";
3663
4223
  }
3664
- await Promise.all(
4224
+ const failures = [];
4225
+ await Promise.allSettled(
3665
4226
  accepted.map(async (file) => {
3666
4227
  const placeholder = createUploadingTile(file.name, state);
3667
4228
  if (listEl) {
@@ -3678,11 +4239,27 @@ async function uploadBatch(accepted, resourceIds, listEl, state) {
3678
4239
  file: void 0
3679
4240
  });
3680
4241
  resourceIds.push(rid);
4242
+ } catch (err) {
4243
+ const wrapped = err instanceof Error ? err : new Error(String(err));
4244
+ const cause = wrapped.cause;
4245
+ const root = cause instanceof Error ? cause : cause !== void 0 ? new Error(String(cause)) : wrapped;
4246
+ failures.push({ file, error: root });
3681
4247
  } finally {
3682
4248
  placeholder.remove();
3683
4249
  }
3684
4250
  })
3685
4251
  );
4252
+ return { failures };
4253
+ }
4254
+ function buildBatchErrorMessage(filterError, failures, state) {
4255
+ if (failures.length === 0) return filterError;
4256
+ const uploadMsg = failures.map(
4257
+ (f) => t("uploadFailed", state, {
4258
+ name: f.file.name,
4259
+ error: f.error.message
4260
+ })
4261
+ ).join(" \u2022 ");
4262
+ return filterError ? `${filterError} \u2022 ${uploadMsg}` : uploadMsg;
3686
4263
  }
3687
4264
  function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallback, constraints, pathKey, instance) {
3688
4265
  setupDragAndDrop(filesContainer, async (files) => {
@@ -3698,7 +4275,13 @@ function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallbac
3698
4275
  clearFileError(filesContainer);
3699
4276
  }
3700
4277
  const list = filesContainer.querySelector(".files-list") ?? filesContainer;
3701
- await uploadBatch(accepted, resourceIds, list, state);
4278
+ const { failures } = await uploadBatch(accepted, resourceIds, list, state);
4279
+ const combined = buildBatchErrorMessage(errorMessage, failures, state);
4280
+ if (combined) {
4281
+ showFileError(filesContainer, combined);
4282
+ } else {
4283
+ clearFileError(filesContainer);
4284
+ }
3702
4285
  updateCallback();
3703
4286
  if (instance && pathKey && !state.config.readonly) {
3704
4287
  instance.triggerOnChange(pathKey, resourceIds);
@@ -3721,7 +4304,20 @@ function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback
3721
4304
  clearFileError(wrapperEl);
3722
4305
  }
3723
4306
  const listEl = wrapperEl?.querySelector(".files-list");
3724
- await uploadBatch(accepted, resourceIds, listEl ?? null, state);
4307
+ const { failures } = await uploadBatch(
4308
+ accepted,
4309
+ resourceIds,
4310
+ listEl ?? null,
4311
+ state
4312
+ );
4313
+ if (wrapperEl) {
4314
+ const combined = buildBatchErrorMessage(errorMessage, failures, state);
4315
+ if (combined) {
4316
+ showFileError(wrapperEl, combined);
4317
+ } else {
4318
+ clearFileError(wrapperEl);
4319
+ }
4320
+ }
3725
4321
  updateCallback();
3726
4322
  filesPicker.value = "";
3727
4323
  if (instance && pathKey && !state.config.readonly) {
@@ -3753,10 +4349,17 @@ function validatePickedResource(resource, allowedExtensions, allowedMimes, maxSi
3753
4349
  }
3754
4350
  if (!isMimeAllowed(resource.type, allowedMimes)) {
3755
4351
  const mimes = allowedMimes.join(", ");
3756
- return t("invalidFileMime", state, { name: resource.name, type: resource.type, mimes });
4352
+ return t("invalidFileMime", state, {
4353
+ name: resource.name,
4354
+ type: resource.type,
4355
+ mimes
4356
+ });
3757
4357
  }
3758
4358
  if (!isSizeWithinLimit(resource.size, maxSizeMB)) {
3759
- return t("fileTooLarge", state, { name: resource.name, maxSize: maxSizeMB });
4359
+ return t("fileTooLarge", state, {
4360
+ name: resource.name,
4361
+ maxSize: maxSizeMB
4362
+ });
3760
4363
  }
3761
4364
  return null;
3762
4365
  }
@@ -3815,7 +4418,13 @@ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resour
3815
4418
  return true;
3816
4419
  });
3817
4420
  const validItems = deduped.filter((r) => {
3818
- const err = validatePickedResource(r, allowedExtensions, allowedMimes, maxSizeMB, state);
4421
+ const err = validatePickedResource(
4422
+ r,
4423
+ allowedExtensions,
4424
+ allowedMimes,
4425
+ maxSizeMB,
4426
+ state
4427
+ );
3819
4428
  return err === null;
3820
4429
  });
3821
4430
  const freshRemaining = maxCount === Infinity ? validItems.length : Math.max(0, maxCount - resourceIds.length);
@@ -3859,14 +4468,22 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
3859
4468
  }
3860
4469
  if (picked.length === 0) return;
3861
4470
  const first = picked[0];
3862
- const validationError = validatePickedResource(first, allowedExtensions, allowedMimes, maxSizeMB, state);
4471
+ const validationError = validatePickedResource(
4472
+ first,
4473
+ allowedExtensions,
4474
+ allowedMimes,
4475
+ maxSizeMB,
4476
+ state
4477
+ );
3863
4478
  if (validationError !== null) {
3864
4479
  showFileError(container, validationError);
3865
4480
  return;
3866
4481
  }
3867
4482
  clearFileError(container);
3868
4483
  registerPickedResource(first, state);
3869
- let hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
4484
+ let hiddenInput = fileWrapper.querySelector(
4485
+ 'input[type="hidden"]'
4486
+ );
3870
4487
  if (!hiddenInput) {
3871
4488
  hiddenInput = document.createElement("input");
3872
4489
  hiddenInput.type = "hidden";
@@ -3934,21 +4551,21 @@ function buildWideTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragO
3934
4551
  };
3935
4552
  outer.appendChild(uploadBtn);
3936
4553
  if (hasLibrary && onLibraryClick) {
3937
- const divider = document.createElement("div");
3938
- divider.className = "fb-wide-tile-divider";
3939
- outer.appendChild(divider);
3940
4554
  const libBtn = document.createElement("button");
3941
4555
  libBtn.type = "button";
3942
4556
  libBtn.className = "fb-wide-tile-library fb-file-library-card";
3943
4557
  const libIcon = document.createElement("span");
4558
+ libIcon.className = "fb-wide-tile-library-icon";
3944
4559
  libIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
3945
4560
  libIcon.innerHTML = ICON_LIBRARY2;
3946
4561
  libBtn.appendChild(libIcon);
3947
4562
  const libLabel = document.createElement("div");
4563
+ libLabel.className = "fb-wide-tile-library-label";
3948
4564
  libLabel.style.cssText = "font-size:13px;font-weight:600;text-align:center;";
3949
4565
  libLabel.textContent = t("fromLibrary", state);
3950
4566
  libBtn.appendChild(libLabel);
3951
4567
  const libHint = document.createElement("div");
4568
+ libHint.className = "fb-wide-tile-library-hint";
3952
4569
  libHint.style.cssText = "font-size:11px;opacity:0.75;text-align:center;";
3953
4570
  libHint.textContent = t("libraryHint", state);
3954
4571
  libBtn.appendChild(libHint);
@@ -4025,7 +4642,8 @@ function renderSingleFileFilled(fileContainer, resourceId, state, deps, extras)
4025
4642
  grid.appendChild(tile);
4026
4643
  fileContainer.className = "file-preview-container";
4027
4644
  fileContainer.removeAttribute("style");
4028
- while (fileContainer.firstChild) fileContainer.removeChild(fileContainer.firstChild);
4645
+ while (fileContainer.firstChild)
4646
+ fileContainer.removeChild(fileContainer.firstChild);
4029
4647
  fileContainer.appendChild(outer);
4030
4648
  }
4031
4649
  function buildMultiAddTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragOver = false) {
@@ -4106,7 +4724,9 @@ function buildMetaLine(state, element, ridCount, maxCount, canClearAll, onClearA
4106
4724
  metaText.appendChild(sizeSpan);
4107
4725
  metaText.appendChild(buildMetaDot());
4108
4726
  }
4109
- const exts = getAllowedExtensions(element.accept);
4727
+ const exts = getAllowedExtensions(
4728
+ element.accept
4729
+ );
4110
4730
  if (exts.length > 0) {
4111
4731
  const fmtSpan = document.createElement("span");
4112
4732
  fmtSpan.className = "fb-meta-mono";
@@ -4352,7 +4972,9 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
4352
4972
  setupDragAndDrop(container, handlers.dragHandler);
4353
4973
  },
4354
4974
  onRemove() {
4355
- const hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
4975
+ const hiddenInput = fileWrapper.querySelector(
4976
+ 'input[type="hidden"]'
4977
+ );
4356
4978
  const currentRid = hiddenInput?.value;
4357
4979
  if (currentRid) {
4358
4980
  releaseLocalFileUrl(state.resourceIndex.get(currentRid)?.file);
@@ -4362,7 +4984,9 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
4362
4984
  }
4363
4985
  };
4364
4986
  const buildSingleExtras = () => {
4365
- const hasLibrary = Boolean(state.config.pickExistingFiles && !element.disableLibrary);
4987
+ const hasLibrary = Boolean(
4988
+ state.config.pickExistingFiles && !element.disableLibrary
4989
+ );
4366
4990
  return {
4367
4991
  replaceHandler: state.config.uploadFile ? () => picker.click() : null,
4368
4992
  libraryHandler: hasLibrary ? () => {
@@ -4374,7 +4998,13 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
4374
4998
  pathKey,
4375
4999
  pathKey,
4376
5000
  async (rid) => {
4377
- renderSingleFileFilled(fileContainer, rid, state, buildDeps(), buildSingleExtras());
5001
+ renderSingleFileFilled(
5002
+ fileContainer,
5003
+ rid,
5004
+ state,
5005
+ buildDeps(),
5006
+ buildSingleExtras()
5007
+ );
4378
5008
  },
4379
5009
  ctx.instance
4380
5010
  ).catch((err) => {
@@ -4390,14 +5020,21 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
4390
5020
  setupDrop: handlers.setupDrop,
4391
5021
  onRemove: handlers.onRemove,
4392
5022
  onAfterUpload: (container, rid) => {
4393
- renderSingleFileFilled(container, rid, state, buildDeps(), buildSingleExtras());
5023
+ renderSingleFileFilled(
5024
+ container,
5025
+ rid,
5026
+ state,
5027
+ buildDeps(),
5028
+ buildSingleExtras()
5029
+ );
4394
5030
  }
4395
5031
  });
4396
5032
  const renderEmptySingleState = () => {
4397
5033
  ensureFileStyles();
4398
5034
  fileContainer.className = "file-preview-container";
4399
5035
  fileContainer.removeAttribute("style");
4400
- while (fileContainer.firstChild) fileContainer.removeChild(fileContainer.firstChild);
5036
+ while (fileContainer.firstChild)
5037
+ fileContainer.removeChild(fileContainer.firstChild);
4401
5038
  const onLibraryClick = buildSingleExtras().libraryHandler;
4402
5039
  const wideTile = buildWideTile(
4403
5040
  state,
@@ -4538,7 +5175,13 @@ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
4538
5175
  setupMultiFileEditMode(element, ctx, wrapper, pathKey, Infinity);
4539
5176
  }
4540
5177
  function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4541
- setupMultiFileEditMode(element, ctx, wrapper, pathKey, element.maxCount ?? Infinity);
5178
+ setupMultiFileEditMode(
5179
+ element,
5180
+ ctx,
5181
+ wrapper,
5182
+ pathKey,
5183
+ element.maxCount ?? Infinity
5184
+ );
4542
5185
  }
4543
5186
 
4544
5187
  // src/components/file/validate.ts
@@ -4717,7 +5360,11 @@ function renderMultiFileReadonly(rids, state, wrapper, pathKey, _marginTop) {
4717
5360
  const placeholder = placeholders[i];
4718
5361
  const meta = state.resourceIndex.get(resourceId);
4719
5362
  renderFilePreviewReadonly(resourceId, state, meta?.name).then((tile) => {
4720
- tile.classList.add("fb-readonly-tile", "fb-checker", "fb-tile-resource");
5363
+ tile.classList.add(
5364
+ "fb-readonly-tile",
5365
+ "fb-checker",
5366
+ "fb-tile-resource"
5367
+ );
4721
5368
  tile.dataset.resourceId = resourceId;
4722
5369
  placeholder.replaceWith(tile);
4723
5370
  }).catch(() => {
@@ -5067,51 +5714,25 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
5067
5714
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
5068
5715
  });
5069
5716
  }
5070
- let addRow = null;
5071
- let countDisplay = null;
5717
+ let addUpdate = null;
5072
5718
  if (!readonly) {
5073
- addRow = document.createElement("div");
5074
- addRow.className = "flex items-center gap-3 mt-2";
5075
- const addBtn = document.createElement("button");
5076
- addBtn.type = "button";
5077
- addBtn.className = "add-colour-btn px-3 py-1 rounded";
5078
- addBtn.style.cssText = `
5079
- color: var(--fb-primary-color);
5080
- border: var(--fb-border-width) solid var(--fb-primary-color);
5081
- background-color: transparent;
5082
- font-size: var(--fb-font-size);
5083
- transition: all var(--fb-transition-duration);
5084
- `;
5085
- addBtn.textContent = "+";
5086
- addBtn.addEventListener("mouseenter", () => {
5087
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
5088
- });
5089
- addBtn.addEventListener("mouseleave", () => {
5090
- addBtn.style.backgroundColor = "transparent";
5091
- });
5092
- addBtn.onclick = () => {
5093
- const defaultColour = element.default || "#000000";
5094
- values.push(defaultColour);
5095
- addColourItem(defaultColour);
5096
- updateAddButton();
5097
- updateRemoveButtons();
5098
- };
5099
- countDisplay = document.createElement("span");
5100
- countDisplay.className = "text-sm text-gray-500";
5101
- addRow.appendChild(addBtn);
5102
- addRow.appendChild(countDisplay);
5103
- wrapper.appendChild(addRow);
5719
+ const handle = createAddItemRow(
5720
+ "colour",
5721
+ () => {
5722
+ const defaultColour = element.default || "#000000";
5723
+ values.push(defaultColour);
5724
+ addColourItem(defaultColour);
5725
+ updateAddButton();
5726
+ updateRemoveButtons();
5727
+ },
5728
+ { label: element.addLabel }
5729
+ );
5730
+ addUpdate = handle.update;
5731
+ mountCounterInLabel(wrapper, handle.counter);
5732
+ wrapper.appendChild(handle.row);
5104
5733
  }
5105
5734
  function updateAddButton() {
5106
- if (!addRow || !countDisplay) return;
5107
- const addBtn = addRow.querySelector(".add-colour-btn");
5108
- if (addBtn) {
5109
- const disabled = values.length >= maxCount;
5110
- addBtn.disabled = disabled;
5111
- addBtn.style.opacity = disabled ? "0.5" : "1";
5112
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
5113
- }
5114
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
5735
+ if (addUpdate) addUpdate(values.length, maxCount);
5115
5736
  }
5116
5737
  values.forEach((value) => addColourItem(value));
5117
5738
  updateAddButton();
@@ -5186,7 +5807,7 @@ function validateColourElement(element, key, context) {
5186
5807
  };
5187
5808
  if (element.multiple) {
5188
5809
  const hexInputs = scopeRoot.querySelectorAll(
5189
- `[name^="${key}["].colour-hex-input`
5810
+ `[name^="${key}\\["].colour-hex-input`
5190
5811
  );
5191
5812
  const values = [];
5192
5813
  hexInputs.forEach((input, index) => {
@@ -5235,7 +5856,7 @@ function updateColourField(element, fieldPath, value, context) {
5235
5856
  return;
5236
5857
  }
5237
5858
  const hexInputs = scopeRoot.querySelectorAll(
5238
- `[name^="${fieldPath}["].colour-hex-input`
5859
+ `[name^="${fieldPath}\\["].colour-hex-input`
5239
5860
  );
5240
5861
  hexInputs.forEach((hexInput, index) => {
5241
5862
  if (index < value.length) {
@@ -5243,6 +5864,7 @@ function updateColourField(element, fieldPath, value, context) {
5243
5864
  hexInput.value = normalized;
5244
5865
  hexInput.classList.remove("invalid");
5245
5866
  hexInput.title = "";
5867
+ clearFieldError(hexInput);
5246
5868
  const wrapper = hexInput.closest(".colour-picker-wrapper");
5247
5869
  if (wrapper) {
5248
5870
  const swatch = wrapper.querySelector(".colour-swatch");
@@ -5272,6 +5894,7 @@ function updateColourField(element, fieldPath, value, context) {
5272
5894
  hexInput.value = normalized;
5273
5895
  hexInput.classList.remove("invalid");
5274
5896
  hexInput.title = "";
5897
+ clearFieldError(hexInput);
5275
5898
  const wrapper = hexInput.closest(".colour-picker-wrapper");
5276
5899
  if (wrapper) {
5277
5900
  const swatch = wrapper.querySelector(".colour-swatch");
@@ -5547,50 +6170,24 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
5547
6170
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
5548
6171
  });
5549
6172
  }
5550
- let addRow = null;
5551
- let countDisplay = null;
6173
+ let addUpdate = null;
5552
6174
  if (!readonly) {
5553
- addRow = document.createElement("div");
5554
- addRow.className = "flex items-center gap-3 mt-2";
5555
- const addBtn = document.createElement("button");
5556
- addBtn.type = "button";
5557
- addBtn.className = "add-slider-btn px-3 py-1 rounded";
5558
- addBtn.style.cssText = `
5559
- color: var(--fb-primary-color);
5560
- border: var(--fb-border-width) solid var(--fb-primary-color);
5561
- background-color: transparent;
5562
- font-size: var(--fb-font-size);
5563
- transition: all var(--fb-transition-duration);
5564
- `;
5565
- addBtn.textContent = "+";
5566
- addBtn.addEventListener("mouseenter", () => {
5567
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
5568
- });
5569
- addBtn.addEventListener("mouseleave", () => {
5570
- addBtn.style.backgroundColor = "transparent";
5571
- });
5572
- addBtn.onclick = () => {
5573
- values.push(defaultValue);
5574
- addSliderItem(defaultValue);
5575
- updateAddButton();
5576
- updateRemoveButtons();
5577
- };
5578
- countDisplay = document.createElement("span");
5579
- countDisplay.className = "text-sm text-gray-500";
5580
- addRow.appendChild(addBtn);
5581
- addRow.appendChild(countDisplay);
5582
- wrapper.appendChild(addRow);
6175
+ const handle = createAddItemRow(
6176
+ "slider",
6177
+ () => {
6178
+ values.push(defaultValue);
6179
+ addSliderItem(defaultValue);
6180
+ updateAddButton();
6181
+ updateRemoveButtons();
6182
+ },
6183
+ { label: element.addLabel }
6184
+ );
6185
+ addUpdate = handle.update;
6186
+ mountCounterInLabel(wrapper, handle.counter);
6187
+ wrapper.appendChild(handle.row);
5583
6188
  }
5584
6189
  function updateAddButton() {
5585
- if (!addRow || !countDisplay) return;
5586
- const addBtn = addRow.querySelector(".add-slider-btn");
5587
- if (addBtn) {
5588
- const disabled = values.length >= maxCount;
5589
- addBtn.disabled = disabled;
5590
- addBtn.style.opacity = disabled ? "0.5" : "1";
5591
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
5592
- }
5593
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
6190
+ if (addUpdate) addUpdate(values.length, maxCount);
5594
6191
  }
5595
6192
  values.forEach((value) => addSliderItem(value));
5596
6193
  updateAddButton();
@@ -5700,7 +6297,7 @@ function validateSliderElement(element, key, context) {
5700
6297
  };
5701
6298
  if (element.multiple) {
5702
6299
  const sliders = scopeRoot.querySelectorAll(
5703
- `input[type="range"][name^="${key}["]`
6300
+ `input[type="range"][name^="${key}\\["]`
5704
6301
  );
5705
6302
  const values = [];
5706
6303
  sliders.forEach((slider, index) => {
@@ -5751,7 +6348,7 @@ function updateSliderField(element, fieldPath, value, context) {
5751
6348
  return;
5752
6349
  }
5753
6350
  const sliders = scopeRoot.querySelectorAll(
5754
- `input[type="range"][name^="${fieldPath}["]`
6351
+ `input[type="range"][name^="${fieldPath}\\["]`
5755
6352
  );
5756
6353
  sliders.forEach((slider, index) => {
5757
6354
  if (index < value.length && value[index] !== null) {
@@ -5779,6 +6376,7 @@ function updateSliderField(element, fieldPath, value, context) {
5779
6376
  }
5780
6377
  slider.classList.remove("invalid");
5781
6378
  slider.title = "";
6379
+ clearFieldError(slider);
5782
6380
  }
5783
6381
  });
5784
6382
  if (value.length !== sliders.length) {
@@ -5815,6 +6413,7 @@ function updateSliderField(element, fieldPath, value, context) {
5815
6413
  }
5816
6414
  slider.classList.remove("invalid");
5817
6415
  slider.title = "";
6416
+ clearFieldError(slider);
5818
6417
  }
5819
6418
  }
5820
6419
  }
@@ -5937,21 +6536,50 @@ function getChildWrapperClass(isSlides, columns) {
5937
6536
  const cols = columns || 1;
5938
6537
  return cols === 1 ? "space-y-2" : `grid grid-cols-${cols} gap-2`;
5939
6538
  }
6539
+ function mountRemoveButton(item, onRemove, state) {
6540
+ const rem = document.createElement("button");
6541
+ rem.type = "button";
6542
+ rem.className = "fb-item-remove";
6543
+ rem.setAttribute("aria-label", t("removeElement", state));
6544
+ rem.style.cssText = `
6545
+ width: 24px;
6546
+ height: 24px;
6547
+ display: inline-flex;
6548
+ align-items: center;
6549
+ justify-content: center;
6550
+ padding: 0;
6551
+ border: 0;
6552
+ border-radius: 4px;
6553
+ cursor: pointer;
6554
+ flex-shrink: 0;
6555
+ `;
6556
+ rem.innerHTML = BIN_ICON_SVG;
6557
+ rem.onclick = onRemove;
6558
+ const labelRow = item.querySelector("[data-fb-label-row]");
6559
+ if (labelRow) {
6560
+ rem.style.marginLeft = "auto";
6561
+ labelRow.appendChild(rem);
6562
+ return;
6563
+ }
6564
+ rem.style.position = "absolute";
6565
+ rem.style.top = "8px";
6566
+ rem.style.right = "8px";
6567
+ item.style.position = "relative";
6568
+ item.appendChild(rem);
6569
+ }
5940
6570
  function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5941
6571
  const state = ctx.state;
5942
6572
  const containerIsReadonly = isElementReadonly(element, state, ctx);
5943
6573
  const childInheritedReadonly = containerIsReadonly || ctx.inheritedReadonly;
5944
6574
  const containerWrap = document.createElement("div");
5945
6575
  containerWrap.className = "border border-gray-200 rounded-lg p-2 bg-gray-50";
5946
- const countDisplay = document.createElement("span");
5947
- countDisplay.className = "text-sm text-gray-500";
5948
6576
  const itemsWrap = document.createElement("div");
5949
6577
  const isSlides = element.displayMode === "slides";
5950
6578
  if (isSlides) {
5951
6579
  itemsWrap.className = "fb-container-slides";
5952
6580
  const slideCols = element.columns;
5953
6581
  const gridTemplateColumns = typeof slideCols === "number" && slideCols > 0 ? `repeat(${slideCols}, 1fr)` : "repeat(auto-fit, minmax(280px, 1fr))";
5954
- itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:8px;align-items:start;`;
6582
+ itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:var(--fb-slides-gap, 14px);align-items:start;`;
5955
6583
  } else {
5956
6584
  itemsWrap.className = "space-y-2";
5957
6585
  }
@@ -5966,92 +6594,67 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5966
6594
  const pre = Array.isArray(ctx.prefill?.[element.key]) ? ctx.prefill[element.key] : null;
5967
6595
  const childDefaults = extractChildDefaults(element.elements);
5968
6596
  const countItems = () => itemsWrap.querySelectorAll(":scope > .containerItem").length;
5969
- const createAddButton = () => {
5970
- const add = document.createElement("button");
5971
- add.type = "button";
5972
- add.className = "add-container-btn px-3 py-1 rounded";
5973
- add.style.cssText = `
5974
- color: var(--fb-primary-color);
5975
- border: var(--fb-border-width) solid var(--fb-primary-color);
5976
- background-color: transparent;
5977
- font-size: var(--fb-font-size);
5978
- transition: all var(--fb-transition-duration);
5979
- `;
5980
- add.textContent = "+";
5981
- add.addEventListener("mouseenter", () => {
5982
- add.style.backgroundColor = "var(--fb-background-hover-color)";
5983
- });
5984
- add.addEventListener("mouseleave", () => {
5985
- add.style.backgroundColor = "transparent";
5986
- });
5987
- add.onclick = () => {
5988
- if (countItems() < max) {
5989
- const idx = countItems();
5990
- const currentFormData = state.formRoot ? extractRootFormData(state.formRoot) : {};
5991
- const subCtx = {
5992
- state: ctx.state,
5993
- path: pathJoin(ctx.path, `${element.key}[${idx}]`),
5994
- prefill: childDefaults,
5995
- // Defaults for enableIf evaluation
5996
- formData: currentFormData,
5997
- // Current root data from DOM for enableIf
5998
- inheritedReadonly: childInheritedReadonly
5999
- };
6000
- const item = document.createElement("div");
6001
- item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6002
- item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6003
- const childWrapper = document.createElement("div");
6004
- childWrapper.className = getChildWrapperClass(isSlides, element.columns);
6005
- element.elements.forEach((child) => {
6006
- if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
6007
- childWrapper.appendChild(
6008
- createHiddenInput(
6009
- pathJoin(subCtx.path, child.key),
6010
- ("default" in child ? child.default : null) ?? null
6011
- )
6012
- );
6013
- } else {
6014
- childWrapper.appendChild(renderElement(child, subCtx));
6015
- }
6016
- });
6017
- item.appendChild(childWrapper);
6018
- if (!containerIsReadonly) {
6019
- const rem = document.createElement("button");
6020
- rem.type = "button";
6021
- rem.className = "absolute top-2 right-2 px-2 py-1 rounded";
6022
- rem.style.cssText = `
6023
- color: var(--fb-error-color);
6024
- background-color: transparent;
6025
- transition: background-color var(--fb-transition-duration);
6026
- `;
6027
- rem.textContent = "\u2715";
6028
- rem.addEventListener("mouseenter", () => {
6029
- rem.style.backgroundColor = "var(--fb-background-hover-color)";
6030
- });
6031
- rem.addEventListener("mouseleave", () => {
6032
- rem.style.backgroundColor = "transparent";
6033
- });
6034
- rem.onclick = () => handleRemoveItem(item);
6035
- item.style.position = "relative";
6036
- item.appendChild(rem);
6037
- }
6038
- itemsWrap.appendChild(item);
6039
- updateAddButton();
6040
- }
6597
+ const handleAddItem = () => {
6598
+ if (countItems() >= max) return;
6599
+ const idx = countItems();
6600
+ const currentFormData = state.formRoot ? extractRootFormData(state.formRoot) : {};
6601
+ const subCtx = {
6602
+ state: ctx.state,
6603
+ path: pathJoin(ctx.path, `${element.key}[${idx}]`),
6604
+ prefill: childDefaults,
6605
+ // Defaults for enableIf evaluation
6606
+ formData: currentFormData,
6607
+ // Current root data from DOM for enableIf
6608
+ inheritedReadonly: childInheritedReadonly
6041
6609
  };
6042
- return add;
6610
+ const item = document.createElement("div");
6611
+ item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6612
+ item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6613
+ if (isSlides) {
6614
+ item.setAttribute("data-fb-slide-card", "");
6615
+ }
6616
+ const childWrapper = document.createElement("div");
6617
+ childWrapper.className = getChildWrapperClass(isSlides, element.columns);
6618
+ element.elements.forEach((child) => {
6619
+ if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
6620
+ childWrapper.appendChild(
6621
+ createHiddenInput(
6622
+ pathJoin(subCtx.path, child.key),
6623
+ ("default" in child ? child.default : null) ?? null
6624
+ )
6625
+ );
6626
+ } else {
6627
+ childWrapper.appendChild(renderElement(child, subCtx));
6628
+ }
6629
+ });
6630
+ item.appendChild(childWrapper);
6631
+ if (!containerIsReadonly) {
6632
+ mountRemoveButton(item, () => handleRemoveItem(item), state);
6633
+ }
6634
+ if (slideAddTile && slideAddTile.parentElement === itemsWrap) {
6635
+ itemsWrap.insertBefore(item, slideAddTile);
6636
+ } else {
6637
+ itemsWrap.appendChild(item);
6638
+ }
6639
+ updateAddButton();
6043
6640
  };
6044
- const updateAddButton = () => {
6045
- const currentCount = countItems();
6046
- const existingAddBtn = containerWrap.querySelector(
6047
- ".add-container-btn"
6641
+ let slideAddTile = null;
6642
+ let slideAddUpdate = null;
6643
+ let pillAddUpdate = null;
6644
+ const syncSlideTileSize = () => {
6645
+ if (!slideAddTile) return;
6646
+ const firstSlide = itemsWrap.querySelector(
6647
+ ":scope > .containerItem"
6048
6648
  );
6049
- if (existingAddBtn) {
6050
- existingAddBtn.disabled = currentCount >= max;
6051
- existingAddBtn.style.opacity = currentCount >= max ? "0.5" : "1";
6052
- existingAddBtn.style.pointerEvents = currentCount >= max ? "none" : "auto";
6649
+ if (firstSlide && firstSlide.offsetHeight > 0) {
6650
+ slideAddTile.style.minHeight = `${firstSlide.offsetHeight}px`;
6053
6651
  }
6054
- countDisplay.textContent = `${currentCount}/${max === Infinity ? "\u221E" : max}`;
6652
+ };
6653
+ const updateAddButton = () => {
6654
+ const currentCount = countItems();
6655
+ if (slideAddUpdate) slideAddUpdate(currentCount, max);
6656
+ if (pillAddUpdate) pillAddUpdate(currentCount, max);
6657
+ if (slideAddTile) syncSlideTileSize();
6055
6658
  };
6056
6659
  const handleRemoveItem = (item) => {
6057
6660
  item.remove();
@@ -6072,6 +6675,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6072
6675
  const item = document.createElement("div");
6073
6676
  item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6074
6677
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6678
+ if (isSlides) {
6679
+ item.setAttribute("data-fb-slide-card", "");
6680
+ }
6075
6681
  const childWrapper = document.createElement("div");
6076
6682
  if (isSlides) {
6077
6683
  childWrapper.className = "space-y-2";
@@ -6095,24 +6701,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6095
6701
  });
6096
6702
  item.appendChild(childWrapper);
6097
6703
  if (!containerIsReadonly) {
6098
- const rem = document.createElement("button");
6099
- rem.type = "button";
6100
- rem.className = "absolute top-2 right-2 px-2 py-1 rounded";
6101
- rem.style.cssText = `
6102
- color: var(--fb-error-color);
6103
- background-color: transparent;
6104
- transition: background-color var(--fb-transition-duration);
6105
- `;
6106
- rem.textContent = "\u2715";
6107
- rem.addEventListener("mouseenter", () => {
6108
- rem.style.backgroundColor = "var(--fb-background-hover-color)";
6109
- });
6110
- rem.addEventListener("mouseleave", () => {
6111
- rem.style.backgroundColor = "transparent";
6112
- });
6113
- rem.onclick = () => handleRemoveItem(item);
6114
- item.style.position = "relative";
6115
- item.appendChild(rem);
6704
+ mountRemoveButton(item, () => handleRemoveItem(item), ctx.state);
6116
6705
  }
6117
6706
  itemsWrap.appendChild(item);
6118
6707
  });
@@ -6132,6 +6721,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6132
6721
  const item = document.createElement("div");
6133
6722
  item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6134
6723
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6724
+ if (isSlides) {
6725
+ item.setAttribute("data-fb-slide-card", "");
6726
+ }
6135
6727
  const childWrapper = document.createElement("div");
6136
6728
  if (isSlides) {
6137
6729
  childWrapper.className = "space-y-2";
@@ -6156,41 +6748,47 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6156
6748
  }
6157
6749
  });
6158
6750
  item.appendChild(childWrapper);
6159
- const rem = document.createElement("button");
6160
- rem.type = "button";
6161
- rem.className = "absolute top-2 right-2 px-2 py-1 rounded";
6162
- rem.style.cssText = `
6163
- color: var(--fb-error-color);
6164
- background-color: transparent;
6165
- transition: background-color var(--fb-transition-duration);
6166
- `;
6167
- rem.textContent = "\u2715";
6168
- rem.addEventListener("mouseenter", () => {
6169
- rem.style.backgroundColor = "var(--fb-background-hover-color)";
6751
+ mountRemoveButton(
6752
+ item,
6753
+ () => {
6754
+ if (countItems() > min) {
6755
+ handleRemoveItem(item);
6756
+ }
6757
+ },
6758
+ ctx.state
6759
+ );
6760
+ itemsWrap.appendChild(item);
6761
+ }
6762
+ }
6763
+ containerWrap.appendChild(itemsWrap);
6764
+ if (!containerIsReadonly) {
6765
+ if (isSlides) {
6766
+ itemsWrap.style.alignItems = "stretch";
6767
+ const handle = createSlideAddTile(handleAddItem, {
6768
+ label: element.addLabel
6170
6769
  });
6171
- rem.addEventListener("mouseleave", () => {
6172
- rem.style.backgroundColor = "transparent";
6770
+ slideAddTile = handle.tile;
6771
+ slideAddUpdate = handle.update;
6772
+ mountCounterInLabel(wrapper, handle.counter);
6773
+ itemsWrap.appendChild(handle.tile);
6774
+ } else {
6775
+ const handle = createAddItemRow("container", handleAddItem, {
6776
+ label: element.addLabel
6173
6777
  });
6174
- rem.onclick = () => {
6175
- if (countItems() > min) {
6176
- handleRemoveItem(item);
6177
- }
6178
- };
6179
- item.style.position = "relative";
6180
- item.appendChild(rem);
6181
- itemsWrap.appendChild(item);
6778
+ pillAddUpdate = handle.update;
6779
+ mountCounterInLabel(wrapper, handle.counter);
6780
+ containerWrap.appendChild(handle.row);
6182
6781
  }
6183
6782
  }
6184
- containerWrap.appendChild(itemsWrap);
6185
- if (!containerIsReadonly) {
6186
- const addRow = document.createElement("div");
6187
- addRow.className = "flex items-center gap-3 mt-2";
6188
- addRow.appendChild(createAddButton());
6189
- addRow.appendChild(countDisplay);
6190
- containerWrap.appendChild(addRow);
6191
- }
6192
6783
  updateAddButton();
6193
6784
  wrapper.appendChild(containerWrap);
6785
+ if (slideAddTile) {
6786
+ if (typeof requestAnimationFrame === "function") {
6787
+ requestAnimationFrame(syncSlideTileSize);
6788
+ } else {
6789
+ syncSlideTileSize();
6790
+ }
6791
+ }
6194
6792
  }
6195
6793
  var validateElementFunc = null;
6196
6794
  function setValidateElement(fn) {
@@ -8783,7 +9381,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
8783
9381
  if (element.minLength != null || element.maxLength != null) {
8784
9382
  const counterRow = document.createElement("div");
8785
9383
  counterRow.style.cssText = "position: relative; padding: 2px 10px 4px; text-align: right;";
8786
- const counter = createCharCounter(element, textarea, false);
9384
+ const counter = createCharCounter(element, textarea);
8787
9385
  counter.style.cssText = `
8788
9386
  position: static;
8789
9387
  display: inline-block;
@@ -9085,10 +9683,7 @@ var TAGS = {
9085
9683
  "-": ["<hr />"]
9086
9684
  };
9087
9685
  function outdent(str) {
9088
- return str.replace(
9089
- RegExp("^" + (str.match(/^(\t| )+/) || "")[0], "gm"),
9090
- ""
9091
- );
9686
+ return str.replace(RegExp("^" + (str.match(/^(\t| )+/) || "")[0], "gm"), "");
9092
9687
  }
9093
9688
  function encodeAttr(str) {
9094
9689
  return (str + "").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -9251,12 +9846,7 @@ function ensureMarkdownStyles() {
9251
9846
  `;
9252
9847
  document.head.appendChild(style);
9253
9848
  }
9254
- var ANCHOR_DANGEROUS_SCHEMES = [
9255
- "javascript:",
9256
- "data:",
9257
- "vbscript:",
9258
- "blob:"
9259
- ];
9849
+ var ANCHOR_DANGEROUS_SCHEMES = ["javascript:", "data:", "vbscript:", "blob:"];
9260
9850
  var IMG_DANGEROUS_SCHEMES = ["javascript:", "vbscript:", "blob:"];
9261
9851
  function isImgSrcDangerous(normalized) {
9262
9852
  if (IMG_DANGEROUS_SCHEMES.some((scheme) => normalized.startsWith(scheme))) {
@@ -9316,6 +9906,117 @@ function validateMarkdown(_element, _key, _context) {
9316
9906
  function updateMarkdown(_element, _fieldPath, _value, _context) {
9317
9907
  }
9318
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
+
9319
10020
  // src/components/index.ts
9320
10021
  function showTooltip(tooltipId, button) {
9321
10022
  const tooltip = document.getElementById(tooltipId);
@@ -9577,6 +10278,7 @@ function createInfoButton(element) {
9577
10278
  function createLabelContainer(element) {
9578
10279
  const label = document.createElement("div");
9579
10280
  label.className = "flex items-center mb-1";
10281
+ label.dataset.fbLabelRow = "";
9580
10282
  const title = createFieldLabel(element);
9581
10283
  label.appendChild(title);
9582
10284
  if (element.description || element.hint) {
@@ -9623,6 +10325,9 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
9623
10325
  renderSwitcherElement(element, ctx, wrapper, pathKey);
9624
10326
  }
9625
10327
  break;
10328
+ case "boolean":
10329
+ renderBooleanElement(element, ctx, wrapper, pathKey);
10330
+ break;
9626
10331
  case "file":
9627
10332
  if (isMultiple) {
9628
10333
  renderMultipleFileElement(element, ctx, wrapper, pathKey);
@@ -9701,8 +10406,11 @@ function renderElement2(element, ctx) {
9701
10406
  const wrapper = document.createElement("div");
9702
10407
  wrapper.className = "mb-2 fb-field-wrapper";
9703
10408
  wrapper.setAttribute("data-field-key", element.key);
9704
- const label = createLabelContainer(element);
9705
- wrapper.appendChild(label);
10409
+ const ops = getComponentOperations(element.type);
10410
+ if (!ops?.ownsLabel) {
10411
+ const label = createLabelContainer(element);
10412
+ wrapper.appendChild(label);
10413
+ }
9706
10414
  const pathKey = pathJoin(ctx.path, element.key);
9707
10415
  dispatchToRenderer(element, ctx, wrapper, pathKey);
9708
10416
  if (initiallyDisabled) {
@@ -9794,6 +10502,7 @@ var defaultConfig = {
9794
10502
  invalidFileExtension: 'File "{name}" has unsupported format. Allowed: {formats}',
9795
10503
  invalidFileMime: 'File "{name}": file type {type} not allowed (allowed: {mimes})',
9796
10504
  fileTooLarge: 'File "{name}" exceeds maximum size of {maxSize}MB',
10505
+ uploadFailed: 'Failed to upload "{name}": {error}',
9797
10506
  filesLimitExceeded: "{skipped} file(s) skipped: maximum {max} files allowed",
9798
10507
  unsupportedFieldType: "Unsupported field type: {type}",
9799
10508
  invalidOption: "Invalid option",
@@ -9869,6 +10578,7 @@ var defaultConfig = {
9869
10578
  invalidFileExtension: '\u0424\u0430\u0439\u043B "{name}" \u0438\u043C\u0435\u0435\u0442 \u043D\u0435\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442. \u0414\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0435: {formats}',
9870
10579
  invalidFileMime: '\u0424\u0430\u0439\u043B "{name}": \u0442\u0438\u043F \u0444\u0430\u0439\u043B\u0430 {type} \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D (\u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u044B: {mimes})',
9871
10580
  fileTooLarge: '\u0424\u0430\u0439\u043B "{name}" \u043F\u0440\u0435\u0432\u044B\u0448\u0430\u0435\u0442 \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 {maxSize}\u041C\u0411',
10581
+ uploadFailed: '\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C "{name}": {error}',
9872
10582
  filesLimitExceeded: "{skipped} \u0444\u0430\u0439\u043B(\u043E\u0432) \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u043E: \u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
9873
10583
  unsupportedFieldType: "\u041D\u0435\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0439 \u0442\u0438\u043F \u043F\u043E\u043B\u044F: {type}",
9874
10584
  invalidOption: "\u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435",
@@ -9933,28 +10643,52 @@ var defaultTheme = {
9933
10643
  // blue-500
9934
10644
  primaryHoverColor: "#2563eb",
9935
10645
  // blue-600
10646
+ primarySoftColor: "#dbeafe",
10647
+ // blue-100
10648
+ primarySoftHoverColor: "#bfdbfe",
10649
+ // blue-200
9936
10650
  errorColor: "#ef4444",
9937
10651
  // red-500
9938
10652
  errorHoverColor: "#dc2626",
9939
10653
  // red-600
9940
10654
  successColor: "#10b981",
9941
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
9942
10664
  borderColor: "#d1d5db",
9943
10665
  // gray-300
9944
10666
  borderHoverColor: "#9ca3af",
9945
10667
  // gray-400
9946
10668
  borderFocusColor: "#3b82f6",
9947
10669
  // blue-500
10670
+ borderStrongColor: "#9ca3af",
10671
+ // gray-400
9948
10672
  backgroundColor: "#ffffff",
9949
10673
  // white
9950
10674
  backgroundHoverColor: "#f9fafb",
9951
10675
  // gray-50
9952
10676
  backgroundReadonlyColor: "#f3f4f6",
9953
10677
  // gray-100
10678
+ pageBackgroundColor: "#f9fafb",
10679
+ // gray-50
10680
+ surfaceSoftColor: "#eff6ff",
10681
+ // blue-50
10682
+ surfaceTintColor: "#f8fafc",
10683
+ // slate-50
9954
10684
  textColor: "#1f2937",
9955
10685
  // gray-800
9956
10686
  textSecondaryColor: "#6b7280",
9957
10687
  // gray-500
10688
+ textMutedColor: "#9ca3af",
10689
+ // gray-400
10690
+ textFaintColor: "#cbd5e1",
10691
+ // slate-300
9958
10692
  textPlaceholderColor: "#9ca3af",
9959
10693
  // gray-400
9960
10694
  textDisabledColor: "#d1d5db",
@@ -9997,6 +10731,12 @@ var defaultTheme = {
9997
10731
  // 4px (compact density v2)
9998
10732
  borderRadius: "0.5rem",
9999
10733
  // rounded-lg (8px)
10734
+ borderRadiusSmall: "0.375rem",
10735
+ // 6px
10736
+ borderRadiusLarge: "0.75rem",
10737
+ // 12px
10738
+ borderRadiusXLarge: "1rem",
10739
+ // 16px
10000
10740
  borderWidth: "1px",
10001
10741
  // Typography
10002
10742
  fontSize: "0.875rem",
@@ -10008,13 +10748,31 @@ var defaultTheme = {
10008
10748
  fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
10009
10749
  fontWeightNormal: "400",
10010
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)",
10011
10755
  // Focus ring
10012
10756
  focusRingWidth: "2px",
10013
10757
  focusRingColor: "#3b82f6",
10014
10758
  // blue-500
10015
10759
  focusRingOpacity: "0.5",
10016
10760
  // Transitions
10017
- 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"
10018
10776
  };
10019
10777
  function generateCSSVariables(theme) {
10020
10778
  const mergedTheme = { ...defaultTheme, ...theme };
@@ -10026,6 +10784,7 @@ function generateCSSVariables(theme) {
10026
10784
  return cssVars.join("\n");
10027
10785
  }
10028
10786
  function injectThemeVariables(container, theme) {
10787
+ ensureThemingHooks(container.ownerDocument || document);
10029
10788
  const cssVariables = generateCSSVariables(theme);
10030
10789
  let styleTag = container.querySelector(
10031
10790
  "style[data-fb-theme]"
@@ -10088,137 +10847,50 @@ var exampleThemes = {
10088
10847
  fontSize: "16px",
10089
10848
  fontSizeSmall: "14px",
10090
10849
  fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif'
10091
- }
10092
- };
10093
-
10094
- // src/utils/styles.ts
10095
- function applyActionButtonStyles(button, isFormLevel = false) {
10096
- button.style.cssText = `
10097
- background-color: var(--fb-action-bg-color);
10098
- color: var(--fb-action-text-color);
10099
- border: var(--fb-border-width) solid var(--fb-action-border-color);
10100
- padding: ${isFormLevel ? "0.5rem 1rem" : "0.5rem 0.75rem"};
10101
- font-size: var(--fb-font-size);
10102
- font-weight: var(--fb-font-weight-medium);
10103
- border-radius: var(--fb-border-radius);
10104
- transition: all var(--fb-transition-duration);
10105
- box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
10106
- `;
10107
- button.addEventListener("mouseenter", () => {
10108
- button.style.backgroundColor = "var(--fb-action-hover-bg-color)";
10109
- button.style.borderColor = "var(--fb-action-hover-border-color)";
10110
- });
10111
- button.addEventListener("mouseleave", () => {
10112
- button.style.backgroundColor = "var(--fb-action-bg-color)";
10113
- button.style.borderColor = "var(--fb-action-border-color)";
10114
- });
10115
- }
10116
-
10117
- // src/components/registry.ts
10118
- function validateHiddenElement(element, key, context) {
10119
- const { scopeRoot } = context;
10120
- const input = scopeRoot.querySelector(
10121
- `input[type="hidden"][data-hidden-field="true"][name="${key}"]`
10122
- );
10123
- const raw = input?.value ?? "";
10124
- if (raw === "") {
10125
- const defaultVal = "default" in element ? element.default : null;
10126
- return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
10127
- }
10128
- return { value: deserializeHiddenValue(raw), errors: [] };
10129
- }
10130
- function updateHiddenField(_element, fieldPath, value, context) {
10131
- const { scopeRoot } = context;
10132
- const input = scopeRoot.querySelector(
10133
- `input[type="hidden"][data-hidden-field="true"][name="${fieldPath}"]`
10134
- );
10135
- if (!input) return;
10136
- input.value = serializeHiddenValue(value);
10137
- }
10138
- var componentRegistry = {
10139
- text: {
10140
- validate: validateTextElement,
10141
- update: updateTextField
10142
- },
10143
- textarea: {
10144
- validate: validateTextareaElement,
10145
- update: updateTextareaField
10146
- },
10147
- number: {
10148
- validate: validateNumberElement,
10149
- update: updateNumberField
10150
- },
10151
- select: {
10152
- validate: validateSelectElement,
10153
- update: updateSelectField
10154
- },
10155
- switcher: {
10156
- validate: validateSwitcherElement,
10157
- update: updateSwitcherField
10158
- },
10159
- file: {
10160
- validate: validateFileElement,
10161
- update: updateFileField
10162
- },
10163
- files: {
10164
- // Legacy type - delegates to file
10165
- validate: validateFileElement,
10166
- update: updateFileField
10167
- },
10168
- colour: {
10169
- validate: validateColourElement,
10170
- update: updateColourField
10171
- },
10172
- slider: {
10173
- validate: validateSliderElement,
10174
- update: updateSliderField
10175
- },
10176
- container: {
10177
- validate: validateContainerElement,
10178
- update: updateContainerField
10179
- },
10180
- group: {
10181
- // Deprecated type - delegates to container
10182
- validate: validateGroupElement,
10183
- update: updateGroupField
10184
- },
10185
- table: {
10186
- validate: validateTableElement,
10187
- update: updateTableField
10188
- },
10189
- richinput: {
10190
- validate: validateRichInputElement,
10191
- update: updateRichInputField
10192
- },
10193
- hidden: {
10194
- // Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
10195
- validate: validateHiddenElement,
10196
- update: updateHiddenField
10197
10850
  },
10198
- markdown: {
10199
- // Display-only element no value, no errors, skip from form data
10200
- validate: validateMarkdown,
10201
- 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"
10202
10892
  }
10203
10893
  };
10204
- function getComponentOperations(elementType) {
10205
- return componentRegistry[elementType] || null;
10206
- }
10207
- function validateElementWithComponent(element, key, context) {
10208
- const ops = getComponentOperations(element.type);
10209
- if (ops && ops.validate) {
10210
- return ops.validate(element, key, context);
10211
- }
10212
- return null;
10213
- }
10214
- function updateElementWithComponent(element, fieldPath, value, context) {
10215
- const ops = getComponentOperations(element.type);
10216
- if (ops && ops.update) {
10217
- ops.update(element, fieldPath, value, context);
10218
- return true;
10219
- }
10220
- return false;
10221
- }
10222
10894
 
10223
10895
  // src/instance/FormBuilderInstance.ts
10224
10896
  var FormBuilderInstance = class {
@@ -10343,26 +11015,37 @@ var FormBuilderInstance = class {
10343
11015
  }
10344
11016
  }
10345
11017
  /**
10346
- * 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).
10347
11032
  */
10348
11033
  findFormElementByFieldPath(fieldPath) {
10349
11034
  if (!this.state.formRoot) return null;
10350
- if (!this.state.config.readonly) {
10351
- let element = this.state.formRoot.querySelector(
10352
- `[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}"]`
10353
11047
  );
10354
11048
  if (element) return element;
10355
- const variations = [
10356
- fieldPath,
10357
- fieldPath.replace(/\[(\d+)\]/g, "[$1]"),
10358
- fieldPath.replace(/\./g, "[") + "]".repeat((fieldPath.match(/\./g) || []).length)
10359
- ];
10360
- for (const variation of variations) {
10361
- element = this.state.formRoot.querySelector(
10362
- `[name="${variation}"]`
10363
- );
10364
- if (element) return element;
10365
- }
10366
11049
  }
10367
11050
  const schemaElement = this.findSchemaElement(fieldPath);
10368
11051
  if (!schemaElement) return null;