@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.
@@ -406,47 +406,334 @@ function deepEqual(a, b) {
406
406
  return a === b;
407
407
  }
408
408
 
409
+ // src/utils/styles.ts
410
+ function clearFieldError(input) {
411
+ const name = input.getAttribute("name");
412
+ if (!name) return;
413
+ const doc = input.ownerDocument || document;
414
+ const errorNode = doc.getElementById(`error-${name}`);
415
+ if (errorNode) errorNode.remove();
416
+ }
417
+ var BIN_ICON_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
418
+ function ensureThemingHooks(doc) {
419
+ if (doc.head.querySelector("[data-fb-theming-hooks]")) return;
420
+ const style = doc.createElement("style");
421
+ style.setAttribute("data-fb-theming-hooks", "");
422
+ style.textContent = `
423
+ [data-fb-slide-card] {
424
+ background: var(--fb-slide-card-bg);
425
+ box-shadow: var(--fb-slide-card-shadow);
426
+ border-radius: var(--fb-slide-card-radius);
427
+ min-height: var(--fb-slide-card-min-height);
428
+ padding: var(--fb-slide-card-padding);
429
+ }
430
+ [data-fb-label-row] > label {
431
+ font-size: var(--fb-label-section-font-size);
432
+ letter-spacing: var(--fb-label-section-letter-spacing);
433
+ text-transform: var(--fb-label-section-text-transform);
434
+ }
435
+ /* Per-item remove (trash) button used by multi-container items. Shares the
436
+ same faint-on-rest, error-on-hover palette as .fb-chip-remove. */
437
+ .fb-item-remove {
438
+ color: var(--fb-text-faint-color, #94a3b8);
439
+ background-color: transparent;
440
+ transition: color var(--fb-transition-duration), background-color var(--fb-transition-duration);
441
+ }
442
+ .fb-item-remove:hover {
443
+ color: var(--fb-error-color);
444
+ background-color: var(--fb-background-hover-color);
445
+ }
446
+ `;
447
+ doc.head.appendChild(style);
448
+ }
449
+ function applyAutoExpand(textarea) {
450
+ textarea.style.overflow = "hidden";
451
+ textarea.style.resize = "none";
452
+ const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
453
+ textarea.rows = Math.max(1, lineCount);
454
+ const resize = () => {
455
+ if (!textarea.isConnected) return;
456
+ textarea.style.height = "0";
457
+ textarea.style.height = `${textarea.scrollHeight}px`;
458
+ };
459
+ textarea.addEventListener("input", resize);
460
+ setTimeout(() => {
461
+ if (textarea.isConnected) resize();
462
+ }, 0);
463
+ }
464
+ function applySingleLineMode(textarea) {
465
+ textarea.addEventListener("keydown", (e) => {
466
+ if (e.key === "Enter") {
467
+ e.preventDefault();
468
+ }
469
+ });
470
+ textarea.addEventListener("paste", (e) => {
471
+ var _a, _b, _c, _d;
472
+ const pasted = (_b = (_a = e.clipboardData) == null ? void 0 : _a.getData("text")) != null ? _b : "";
473
+ if (!/[\r\n]/.test(pasted)) return;
474
+ e.preventDefault();
475
+ const cleaned = pasted.replace(/[\r\n]+/g, " ");
476
+ const start = (_c = textarea.selectionStart) != null ? _c : textarea.value.length;
477
+ const end = (_d = textarea.selectionEnd) != null ? _d : textarea.value.length;
478
+ const before = textarea.value.slice(0, start);
479
+ const after = textarea.value.slice(end);
480
+ textarea.value = before + cleaned + after;
481
+ const pos = start + cleaned.length;
482
+ textarea.setSelectionRange(pos, pos);
483
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
484
+ });
485
+ }
486
+ function mountCounterInLabel(wrapper, counter) {
487
+ const labelRow = wrapper.querySelector(
488
+ ":scope > [data-fb-label-row]"
489
+ );
490
+ if (labelRow) labelRow.appendChild(counter);
491
+ }
492
+ function createAddItemRow(classNameSuffix, onClick, options = {}) {
493
+ var _a;
494
+ const label = (_a = options.label) != null ? _a : "";
495
+ const showCounter = options.showCounter !== false;
496
+ const row = document.createElement("div");
497
+ row.className = "fb-add-row mt-2";
498
+ row.style.cssText = "display:flex;align-items:stretch;width:100%;";
499
+ const button = document.createElement("button");
500
+ button.type = "button";
501
+ button.className = `add-${classNameSuffix}-btn`;
502
+ button.style.cssText = `
503
+ flex: 1 1 auto;
504
+ display: inline-flex;
505
+ align-items: center;
506
+ justify-content: center;
507
+ gap: 6px;
508
+ padding: 6px 10px;
509
+ border: 1px dashed var(--fb-primary-color);
510
+ border-radius: var(--fb-border-radius);
511
+ background: transparent;
512
+ color: var(--fb-primary-color);
513
+ font-size: var(--fb-font-size-small, var(--fb-font-size));
514
+ font-weight: 500;
515
+ font-family: var(--fb-font-family);
516
+ cursor: pointer;
517
+ transition: border-color var(--fb-transition-duration), color var(--fb-transition-duration), background-color var(--fb-transition-duration);
518
+ `;
519
+ button.textContent = label ? `+ ${label}` : "+";
520
+ button.addEventListener("mouseenter", () => {
521
+ if (button.disabled) return;
522
+ button.style.borderStyle = "solid";
523
+ button.style.backgroundColor = "var(--fb-background-hover-color)";
524
+ });
525
+ button.addEventListener("mouseleave", () => {
526
+ button.style.borderStyle = "dashed";
527
+ button.style.backgroundColor = "transparent";
528
+ });
529
+ button.onclick = onClick;
530
+ const counter = document.createElement("span");
531
+ counter.className = "fb-add-counter";
532
+ counter.style.cssText = `
533
+ margin-left: auto;
534
+ font-size: var(--fb-font-size-small, 0.875rem);
535
+ color: var(--fb-text-secondary-color);
536
+ font-weight: 400;
537
+ `;
538
+ if (!showCounter) counter.style.display = "none";
539
+ row.appendChild(button);
540
+ const update = (current, max) => {
541
+ const reached = current >= max;
542
+ row.style.display = reached ? "none" : "flex";
543
+ button.style.display = reached ? "none" : "inline-flex";
544
+ button.disabled = reached;
545
+ if (showCounter) {
546
+ counter.textContent = `${current}/${max === Infinity ? "\u221E" : max}`;
547
+ }
548
+ };
549
+ return { row, button, counter, update };
550
+ }
551
+ function createSlideAddTile(onClick, options = {}) {
552
+ var _a;
553
+ const label = (_a = options.label) != null ? _a : "";
554
+ const tile = document.createElement("button");
555
+ tile.type = "button";
556
+ tile.className = "add-container-btn fb-slide-add";
557
+ tile.style.cssText = `
558
+ display: flex;
559
+ flex-direction: column;
560
+ align-items: center;
561
+ justify-content: center;
562
+ gap: 12px;
563
+ width: 100%;
564
+ min-height: 180px;
565
+ align-self: stretch;
566
+ padding: 24px 16px;
567
+ border: 1.5px dashed var(--fb-primary-color);
568
+ border-radius: var(--fb-border-radius);
569
+ background: transparent;
570
+ color: var(--fb-primary-color);
571
+ font-size: var(--fb-font-size-small, var(--fb-font-size));
572
+ font-weight: 500;
573
+ font-family: var(--fb-font-family);
574
+ cursor: pointer;
575
+ transition: border-color var(--fb-transition-duration), color var(--fb-transition-duration), background-color var(--fb-transition-duration);
576
+ `;
577
+ const circle = document.createElement("span");
578
+ circle.className = "fb-slide-add-circle";
579
+ circle.style.cssText = `
580
+ display: inline-flex;
581
+ align-items: center;
582
+ justify-content: center;
583
+ width: 36px;
584
+ height: 36px;
585
+ border: 1px solid var(--fb-primary-color);
586
+ border-radius: 50%;
587
+ background: var(--fb-background-color);
588
+ font-size: 20px;
589
+ line-height: 1;
590
+ color: inherit;
591
+ transition: inherit;
592
+ `;
593
+ circle.textContent = "+";
594
+ tile.appendChild(circle);
595
+ if (label) {
596
+ const text = document.createElement("span");
597
+ text.textContent = label;
598
+ tile.appendChild(text);
599
+ }
600
+ tile.addEventListener("mouseenter", () => {
601
+ if (tile.disabled) return;
602
+ tile.style.borderStyle = "solid";
603
+ tile.style.backgroundColor = "var(--fb-background-hover-color)";
604
+ });
605
+ tile.addEventListener("mouseleave", () => {
606
+ tile.style.borderStyle = "dashed";
607
+ tile.style.backgroundColor = "transparent";
608
+ });
609
+ tile.onclick = onClick;
610
+ const counter = document.createElement("span");
611
+ counter.className = "fb-add-counter";
612
+ counter.style.cssText = `
613
+ margin-left: auto;
614
+ font-size: var(--fb-font-size-small, 0.875rem);
615
+ color: var(--fb-text-secondary-color);
616
+ font-weight: 400;
617
+ `;
618
+ const update = (current, max) => {
619
+ const reached = current >= max;
620
+ tile.style.display = reached ? "none" : "flex";
621
+ tile.disabled = reached;
622
+ counter.textContent = `${current}/${max === Infinity ? "\u221E" : max}`;
623
+ };
624
+ return { tile, counter, update };
625
+ }
626
+ function applyActionButtonStyles(button, isFormLevel = false) {
627
+ button.style.cssText = `
628
+ background-color: var(--fb-action-bg-color);
629
+ color: var(--fb-action-text-color);
630
+ border: var(--fb-border-width) solid var(--fb-action-border-color);
631
+ padding: ${isFormLevel ? "0.5rem 1rem" : "0.5rem 0.75rem"};
632
+ font-size: var(--fb-font-size);
633
+ font-weight: var(--fb-font-weight-medium);
634
+ border-radius: var(--fb-border-radius);
635
+ transition: all var(--fb-transition-duration);
636
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
637
+ `;
638
+ button.addEventListener("mouseenter", () => {
639
+ button.style.backgroundColor = "var(--fb-action-hover-bg-color)";
640
+ button.style.borderColor = "var(--fb-action-hover-border-color)";
641
+ });
642
+ button.addEventListener("mouseleave", () => {
643
+ button.style.backgroundColor = "var(--fb-action-bg-color)";
644
+ button.style.borderColor = "var(--fb-action-border-color)";
645
+ });
646
+ }
647
+
409
648
  // src/components/text.ts
410
- function createCharCounter(element, input, isTextarea = false) {
649
+ function ensureChipStyles(doc) {
650
+ if (doc.head.querySelector("[data-fb-chip-styles]")) return;
651
+ const style = doc.createElement("style");
652
+ style.setAttribute("data-fb-chip-styles", "");
653
+ style.textContent = `
654
+ .fb-chip-list { display: flex; flex-direction: column; gap: 4px; }
655
+ .fb-chip {
656
+ display: flex;
657
+ align-items: center;
658
+ gap: 8px;
659
+ padding: 5px 6px 5px 10px;
660
+ background: var(--fb-chip-bg, var(--fb-background-color, #fff));
661
+ border: 1px solid var(--fb-chip-border, var(--fb-border-color, #e2e8f0));
662
+ border-radius: 6px;
663
+ position: relative;
664
+ transition: border-color var(--fb-transition-duration, 0.15s);
665
+ }
666
+ .fb-chip:hover { border-color: var(--fb-border-hover-color, var(--fb-border-color, #cbd5e1)); }
667
+ .fb-chip:focus-within { border-color: var(--fb-border-focus-color, var(--fb-primary-color, #2f5bea)); }
668
+ .fb-chip-dot {
669
+ flex: 0 0 6px;
670
+ width: 6px;
671
+ height: 6px;
672
+ border-radius: 50%;
673
+ background: var(--fb-chip-dot, var(--fb-primary-color, #2f5bea));
674
+ }
675
+ .fb-chip-input {
676
+ flex: 1;
677
+ min-width: 0;
678
+ padding: 2px 0;
679
+ border: 0;
680
+ outline: none;
681
+ background: transparent;
682
+ color: var(--fb-chip-text, var(--fb-text-color, inherit));
683
+ font-size: var(--fb-font-size, 14px);
684
+ font-family: var(--fb-font-family, inherit);
685
+ line-height: 1.4;
686
+ }
687
+ .fb-chip-input::placeholder { color: var(--fb-text-placeholder-color, #94a3b8); }
688
+ .fb-chip-input:read-only { color: var(--fb-text-secondary-color, #475569); }
689
+ .fb-chip-remove {
690
+ flex: 0 0 auto;
691
+ width: 22px;
692
+ height: 22px;
693
+ display: inline-flex;
694
+ align-items: center;
695
+ justify-content: center;
696
+ padding: 0;
697
+ border: 0;
698
+ border-radius: 4px;
699
+ background: transparent;
700
+ color: var(--fb-text-faint-color, #94a3b8);
701
+ cursor: pointer;
702
+ opacity: 0;
703
+ transition: opacity 0.12s, color 0.12s, background-color 0.12s;
704
+ }
705
+ .fb-chip:hover .fb-chip-remove,
706
+ .fb-chip-remove:focus-visible { opacity: 1; }
707
+ .fb-chip-remove:hover {
708
+ color: var(--fb-error-color, #dc2626);
709
+ background: var(--fb-background-hover-color, #f1f5f9);
710
+ }
711
+ .fb-chip-remove:disabled { opacity: 0 !important; pointer-events: none; }
712
+ `;
713
+ doc.head.appendChild(style);
714
+ }
715
+ function createCharCounter(element, input) {
411
716
  const counter = document.createElement("span");
412
717
  counter.className = "char-counter";
413
718
  counter.style.cssText = `
414
- position: absolute;
415
- ${isTextarea ? "bottom: 8px" : "top: 50%; transform: translateY(-50%)"};
416
- right: 10px;
719
+ margin-top: 4px;
720
+ padding-right: 12px;
721
+ text-align: right;
417
722
  font-size: var(--fb-font-size-small);
418
- color: var(--fb-text-secondary-color);
723
+ line-height: 1;
724
+ color: var(--fb-error-color);
419
725
  pointer-events: none;
420
- background: var(--fb-background-color);
421
- padding: 0 4px;
726
+ display: none;
422
727
  `;
423
728
  const updateCounter = () => {
424
729
  const len = input.value.length;
425
- const min = element.minLength;
426
730
  const max = element.maxLength;
427
- if (min == null && max == null) {
428
- counter.textContent = "";
429
- return;
430
- }
431
- if (len === 0 || min != null && len < min) {
432
- if (min != null && max != null) {
433
- counter.textContent = `${min}-${max}`;
434
- } else if (max != null) {
435
- counter.textContent = `\u2264${max}`;
436
- } else if (min != null) {
437
- counter.textContent = `\u2265${min}`;
438
- }
439
- counter.style.color = "var(--fb-text-secondary-color)";
440
- } else if (max != null && len > max) {
731
+ if (max != null && len > max) {
441
732
  counter.textContent = `${len}/${max}`;
442
- counter.style.color = "var(--fb-error-color)";
733
+ counter.style.display = "block";
443
734
  } else {
444
- if (max != null) {
445
- counter.textContent = `${len}/${max}`;
446
- } else {
447
- counter.textContent = `${len}`;
448
- }
449
- counter.style.color = "var(--fb-text-secondary-color)";
735
+ counter.textContent = "";
736
+ counter.style.display = "none";
450
737
  }
451
738
  };
452
739
  input.addEventListener("input", updateCounter);
@@ -458,26 +745,33 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
458
745
  const readonly = isElementReadonly(element, state, ctx);
459
746
  const inputWrapper = document.createElement("div");
460
747
  inputWrapper.style.cssText = "position: relative;";
461
- const textInput = document.createElement("input");
462
- textInput.type = "text";
748
+ const hasCharCounter = !readonly && (element.minLength != null || element.maxLength != null);
749
+ const textInput = document.createElement("textarea");
750
+ textInput.rows = 1;
463
751
  textInput.className = "w-full rounded-lg";
464
752
  textInput.style.cssText = `
465
753
  padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
466
- padding-right: 60px;
467
754
  border: var(--fb-border-width) solid var(--fb-border-color);
468
755
  border-radius: var(--fb-border-radius);
469
756
  background-color: ${readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
470
757
  color: var(--fb-text-color);
471
758
  font-size: var(--fb-font-size);
472
759
  font-family: var(--fb-font-family);
760
+ line-height: var(--fb-line-height, 1.5);
473
761
  transition: all var(--fb-transition-duration) ease-in-out;
474
762
  width: 100%;
475
763
  box-sizing: border-box;
764
+ resize: none;
765
+ overflow: hidden;
766
+ word-break: break-word;
767
+ overflow-wrap: anywhere;
476
768
  `;
477
769
  textInput.name = pathKey;
478
770
  textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
479
771
  textInput.value = ctx.prefill[element.key] || element.default || "";
480
772
  textInput.readOnly = readonly;
773
+ applySingleLineMode(textInput);
774
+ applyAutoExpand(textInput);
481
775
  if (!readonly) {
482
776
  textInput.addEventListener("focus", () => {
483
777
  textInput.style.borderColor = "var(--fb-border-focus-color)";
@@ -508,8 +802,8 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
508
802
  textInput.addEventListener("input", handleChange);
509
803
  }
510
804
  inputWrapper.appendChild(textInput);
511
- if (!readonly && (element.minLength != null || element.maxLength != null)) {
512
- const counter = createCharCounter(element, textInput, false);
805
+ if (hasCharCounter) {
806
+ const counter = createCharCounter(element, textInput);
513
807
  inputWrapper.appendChild(counter);
514
808
  }
515
809
  wrapper.appendChild(inputWrapper);
@@ -525,174 +819,100 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
525
819
  while (values.length < minCount) {
526
820
  values.push(element.default || "");
527
821
  }
528
- const container = document.createElement("div");
529
- container.className = "space-y-2";
530
- wrapper.appendChild(container);
822
+ ensureChipStyles(document);
823
+ const list = document.createElement("div");
824
+ list.className = "fb-chip-list";
825
+ wrapper.appendChild(list);
531
826
  function updateIndices() {
532
- const items = container.querySelectorAll(".multiple-text-item");
533
- items.forEach((item, index) => {
534
- const input = item.querySelector("input");
535
- if (input) {
536
- input.name = `${pathKey}[${index}]`;
827
+ const items = list.querySelectorAll(".fb-chip-input");
828
+ items.forEach((input, index) => {
829
+ input.name = `${pathKey}[${index}]`;
830
+ const chip = input.closest(".fb-chip");
831
+ const sib = chip == null ? void 0 : chip.nextElementSibling;
832
+ if (sib && sib.classList.contains("error-message")) {
833
+ sib.id = `error-${input.name}`;
537
834
  }
538
835
  });
539
836
  }
540
- function addTextItem(value = "", index = -1) {
541
- const itemWrapper = document.createElement("div");
542
- itemWrapper.className = "multiple-text-item flex items-center gap-2";
543
- const inputContainer = document.createElement("div");
544
- inputContainer.style.cssText = "position: relative; flex: 1;";
545
- const textInput = document.createElement("input");
546
- textInput.type = "text";
547
- textInput.style.cssText = `
548
- padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
549
- padding-right: 60px;
550
- border: var(--fb-border-width) solid var(--fb-border-color);
551
- border-radius: var(--fb-border-radius);
552
- background-color: ${readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
553
- color: var(--fb-text-color);
554
- font-size: var(--fb-font-size);
555
- font-family: var(--fb-font-family);
556
- transition: all var(--fb-transition-duration) ease-in-out;
557
- width: 100%;
558
- box-sizing: border-box;
559
- `;
560
- textInput.placeholder = element.placeholder || t("placeholderText", state);
561
- textInput.value = value;
562
- textInput.readOnly = readonly;
563
- if (!readonly) {
564
- textInput.addEventListener("focus", () => {
565
- textInput.style.borderColor = "var(--fb-border-focus-color)";
566
- textInput.style.outline = `var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)`;
567
- textInput.style.outlineOffset = "0";
568
- });
569
- textInput.addEventListener("blur", () => {
570
- textInput.style.borderColor = "var(--fb-border-color)";
571
- textInput.style.outline = "none";
572
- });
573
- textInput.addEventListener("mouseenter", () => {
574
- if (document.activeElement !== textInput) {
575
- textInput.style.borderColor = "var(--fb-border-hover-color)";
576
- }
577
- });
578
- textInput.addEventListener("mouseleave", () => {
579
- if (document.activeElement !== textInput) {
580
- textInput.style.borderColor = "var(--fb-border-color)";
581
- }
582
- });
583
- }
837
+ function addChip(value = "") {
838
+ const chip = document.createElement("div");
839
+ chip.className = "fb-chip";
840
+ const dot = document.createElement("span");
841
+ dot.className = "fb-chip-dot";
842
+ dot.setAttribute("aria-hidden", "true");
843
+ chip.appendChild(dot);
844
+ const input = document.createElement("input");
845
+ input.type = "text";
846
+ input.className = "fb-chip-input";
847
+ input.value = value;
848
+ input.placeholder = element.placeholder || t("placeholderText", state);
849
+ input.readOnly = readonly;
850
+ chip.appendChild(input);
584
851
  if (!readonly && ctx.instance) {
585
852
  const handleChange = () => {
586
- const value2 = textInput.value === "" ? null : textInput.value;
587
- ctx.instance.triggerOnChange(textInput.name, value2);
853
+ ctx.instance.triggerOnChange(
854
+ input.name,
855
+ input.value === "" ? null : input.value
856
+ );
588
857
  };
589
- textInput.addEventListener("blur", handleChange);
590
- textInput.addEventListener("input", handleChange);
858
+ input.addEventListener("blur", handleChange);
859
+ input.addEventListener("input", handleChange);
591
860
  }
592
- inputContainer.appendChild(textInput);
593
- if (!readonly && (element.minLength != null || element.maxLength != null)) {
594
- const counter = createCharCounter(element, textInput, false);
595
- inputContainer.appendChild(counter);
596
- }
597
- itemWrapper.appendChild(inputContainer);
598
- if (index === -1) {
599
- container.appendChild(itemWrapper);
600
- } else {
601
- container.insertBefore(itemWrapper, container.children[index]);
861
+ if (!readonly) {
862
+ const rem = document.createElement("button");
863
+ rem.type = "button";
864
+ rem.className = "fb-chip-remove";
865
+ rem.setAttribute("aria-label", t("removeElement", state));
866
+ rem.innerHTML = BIN_ICON_SVG;
867
+ rem.onclick = () => {
868
+ const chips = list.querySelectorAll(".fb-chip");
869
+ const idx = Array.prototype.indexOf.call(chips, chip);
870
+ if (idx < 0) return;
871
+ if (chips.length <= minCount) return;
872
+ values.splice(idx, 1);
873
+ const trailingError = chip.nextElementSibling;
874
+ if (trailingError && trailingError.classList.contains("error-message")) {
875
+ trailingError.remove();
876
+ }
877
+ chip.remove();
878
+ updateIndices();
879
+ updateAddButton();
880
+ updateRemoveButtons();
881
+ };
882
+ chip.appendChild(rem);
602
883
  }
884
+ list.appendChild(chip);
603
885
  updateIndices();
604
- return itemWrapper;
886
+ return chip;
605
887
  }
606
888
  function updateRemoveButtons() {
607
889
  if (readonly) return;
608
- const items = container.querySelectorAll(".multiple-text-item");
609
- const currentCount = items.length;
610
- items.forEach((item) => {
611
- let removeBtn = item.querySelector(
612
- ".remove-item-btn"
613
- );
614
- if (!removeBtn) {
615
- removeBtn = document.createElement("button");
616
- removeBtn.type = "button";
617
- removeBtn.className = "remove-item-btn px-2 py-1 rounded";
618
- removeBtn.style.cssText = `
619
- color: var(--fb-error-color);
620
- background-color: transparent;
621
- transition: background-color var(--fb-transition-duration);
622
- `;
623
- removeBtn.innerHTML = "\u2715";
624
- removeBtn.addEventListener("mouseenter", () => {
625
- removeBtn.style.backgroundColor = "var(--fb-background-hover-color)";
626
- });
627
- removeBtn.addEventListener("mouseleave", () => {
628
- removeBtn.style.backgroundColor = "transparent";
629
- });
630
- removeBtn.onclick = () => {
631
- const currentIndex = Array.from(container.children).indexOf(
632
- item
633
- );
634
- if (container.children.length > minCount) {
635
- values.splice(currentIndex, 1);
636
- item.remove();
637
- updateIndices();
638
- updateAddButton();
639
- updateRemoveButtons();
640
- }
641
- };
642
- item.appendChild(removeBtn);
643
- }
644
- const disabled = currentCount <= minCount;
645
- removeBtn.disabled = disabled;
646
- removeBtn.style.opacity = disabled ? "0.5" : "1";
647
- removeBtn.style.pointerEvents = disabled ? "none" : "auto";
890
+ const chipCount = list.querySelectorAll(".fb-chip").length;
891
+ const disabled = chipCount <= minCount;
892
+ list.querySelectorAll(".fb-chip-remove").forEach((btn) => {
893
+ btn.disabled = disabled;
648
894
  });
649
895
  }
650
- let addRow = null;
651
- let countDisplay = null;
896
+ let addUpdate = null;
652
897
  if (!readonly) {
653
- addRow = document.createElement("div");
654
- addRow.className = "flex items-center gap-3 mt-2";
655
- const addBtn = document.createElement("button");
656
- addBtn.type = "button";
657
- addBtn.className = "add-text-btn px-3 py-1 rounded";
658
- addBtn.style.cssText = `
659
- color: var(--fb-primary-color);
660
- border: var(--fb-border-width) solid var(--fb-primary-color);
661
- background-color: transparent;
662
- font-size: var(--fb-font-size);
663
- transition: all var(--fb-transition-duration);
664
- `;
665
- addBtn.textContent = "+";
666
- addBtn.addEventListener("mouseenter", () => {
667
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
668
- });
669
- addBtn.addEventListener("mouseleave", () => {
670
- addBtn.style.backgroundColor = "transparent";
671
- });
672
- addBtn.onclick = () => {
673
- values.push(element.default || "");
674
- addTextItem(element.default || "");
675
- updateAddButton();
676
- updateRemoveButtons();
677
- };
678
- countDisplay = document.createElement("span");
679
- countDisplay.className = "text-sm text-gray-500";
680
- addRow.appendChild(addBtn);
681
- addRow.appendChild(countDisplay);
682
- wrapper.appendChild(addRow);
898
+ const handle = createAddItemRow(
899
+ "text",
900
+ () => {
901
+ values.push(element.default || "");
902
+ addChip(element.default || "");
903
+ updateAddButton();
904
+ updateRemoveButtons();
905
+ },
906
+ { label: element.addLabel }
907
+ );
908
+ addUpdate = handle.update;
909
+ mountCounterInLabel(wrapper, handle.counter);
910
+ wrapper.appendChild(handle.row);
683
911
  }
684
912
  function updateAddButton() {
685
- if (!addRow || !countDisplay) return;
686
- const addBtn = addRow.querySelector(".add-text-btn");
687
- if (addBtn) {
688
- const disabled = values.length >= maxCount;
689
- addBtn.disabled = disabled;
690
- addBtn.style.opacity = disabled ? "0.5" : "1";
691
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
692
- }
693
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
913
+ if (addUpdate) addUpdate(values.length, maxCount);
694
914
  }
695
- values.forEach((value) => addTextItem(value));
915
+ values.forEach((value) => addChip(value));
696
916
  updateAddButton();
697
917
  updateRemoveButtons();
698
918
  }
@@ -701,7 +921,7 @@ function validateTextElement(element, key, context) {
701
921
  const errors = [];
702
922
  const { scopeRoot, skipValidation } = context;
703
923
  const markValidity = (input, errorMessage) => {
704
- var _a2, _b2;
924
+ var _a2, _b2, _c2;
705
925
  if (!input) return;
706
926
  const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
707
927
  let errorElement = document.getElementById(errorId);
@@ -717,10 +937,12 @@ function validateTextElement(element, key, context) {
717
937
  font-size: var(--fb-font-size-small);
718
938
  margin-top: 0.25rem;
719
939
  `;
720
- if (input.nextSibling) {
721
- (_a2 = input.parentNode) == null ? void 0 : _a2.insertBefore(errorElement, input.nextSibling);
940
+ const chipAncestor = (_a2 = input.closest) == null ? void 0 : _a2.call(input, ".fb-chip");
941
+ const anchor = chipAncestor || input;
942
+ if (anchor.nextSibling) {
943
+ (_b2 = anchor.parentNode) == null ? void 0 : _b2.insertBefore(errorElement, anchor.nextSibling);
722
944
  } else {
723
- (_b2 = input.parentNode) == null ? void 0 : _b2.appendChild(errorElement);
945
+ (_c2 = anchor.parentNode) == null ? void 0 : _c2.appendChild(errorElement);
724
946
  }
725
947
  }
726
948
  errorElement.textContent = errorMessage;
@@ -769,7 +991,7 @@ function validateTextElement(element, key, context) {
769
991
  }
770
992
  };
771
993
  if (element.multiple) {
772
- const inputs = scopeRoot.querySelectorAll(`[name^="${key}["]`);
994
+ const inputs = scopeRoot.querySelectorAll(`[name^="${key}\\["]`);
773
995
  const values = [];
774
996
  const rawValues = [];
775
997
  inputs.forEach((input, index) => {
@@ -819,12 +1041,14 @@ function updateTextField(element, fieldPath, value, context) {
819
1041
  );
820
1042
  return;
821
1043
  }
822
- const inputs = scopeRoot.querySelectorAll(`[name^="${fieldPath}["]`);
1044
+ const inputs = scopeRoot.querySelectorAll(`[name^="${fieldPath}\\["]`);
823
1045
  inputs.forEach((input, index) => {
824
1046
  if (index < value.length) {
825
1047
  input.value = value[index] != null ? String(value[index]) : "";
826
1048
  input.classList.remove("invalid");
827
1049
  input.title = "";
1050
+ clearFieldError(input);
1051
+ input.dispatchEvent(new Event("input", { bubbles: true }));
828
1052
  }
829
1053
  });
830
1054
  if (value.length !== inputs.length) {
@@ -838,26 +1062,15 @@ function updateTextField(element, fieldPath, value, context) {
838
1062
  input.value = value != null ? String(value) : "";
839
1063
  input.classList.remove("invalid");
840
1064
  input.title = "";
1065
+ clearFieldError(input);
1066
+ if (input instanceof HTMLTextAreaElement) {
1067
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1068
+ }
841
1069
  }
842
1070
  }
843
1071
  }
844
1072
 
845
1073
  // src/components/textarea.ts
846
- function applyAutoExpand(textarea) {
847
- textarea.style.overflow = "hidden";
848
- textarea.style.resize = "none";
849
- const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
850
- textarea.rows = Math.max(1, lineCount);
851
- const resize = () => {
852
- if (!textarea.isConnected) return;
853
- textarea.style.height = "0";
854
- textarea.style.height = `${textarea.scrollHeight}px`;
855
- };
856
- textarea.addEventListener("input", resize);
857
- setTimeout(() => {
858
- if (textarea.isConnected) resize();
859
- }, 0);
860
- }
861
1074
  function renderTextareaElement(element, ctx, wrapper, pathKey) {
862
1075
  const state = ctx.state;
863
1076
  const readonly = isElementReadonly(element, state, ctx);
@@ -888,7 +1101,7 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
888
1101
  }
889
1102
  textareaWrapper.appendChild(textareaInput);
890
1103
  if (!readonly && (element.minLength != null || element.maxLength != null)) {
891
- const counter = createCharCounter(element, textareaInput, true);
1104
+ const counter = createCharCounter(element, textareaInput);
892
1105
  textareaWrapper.appendChild(counter);
893
1106
  }
894
1107
  wrapper.appendChild(textareaWrapper);
@@ -945,7 +1158,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
945
1158
  }
946
1159
  textareaContainer.appendChild(textareaInput);
947
1160
  if (!readonly && (element.minLength != null || element.maxLength != null)) {
948
- const counter = createCharCounter(element, textareaInput, true);
1161
+ const counter = createCharCounter(element, textareaInput);
949
1162
  textareaContainer.appendChild(counter);
950
1163
  }
951
1164
  itemWrapper.appendChild(textareaContainer);
@@ -990,52 +1203,24 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
990
1203
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
991
1204
  });
992
1205
  }
993
- let addRow = null;
994
- let countDisplay = null;
1206
+ let addUpdate = null;
995
1207
  if (!readonly) {
996
- addRow = document.createElement("div");
997
- addRow.className = "flex items-center gap-3 mt-2";
998
- const addBtn = document.createElement("button");
999
- addBtn.type = "button";
1000
- addBtn.className = "add-textarea-btn px-3 py-1 rounded";
1001
- addBtn.style.cssText = `
1002
- color: var(--fb-primary-color);
1003
- border: var(--fb-border-width) solid var(--fb-primary-color);
1004
- background-color: transparent;
1005
- font-size: var(--fb-font-size);
1006
- transition: all var(--fb-transition-duration);
1007
- `;
1008
- addBtn.textContent = "+";
1009
- addBtn.addEventListener("mouseenter", () => {
1010
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
1011
- });
1012
- addBtn.addEventListener("mouseleave", () => {
1013
- addBtn.style.backgroundColor = "transparent";
1014
- });
1015
- addBtn.onclick = () => {
1016
- values.push(element.default || "");
1017
- addTextareaItem(element.default || "");
1018
- updateAddButton();
1019
- updateRemoveButtons();
1020
- };
1021
- countDisplay = document.createElement("span");
1022
- countDisplay.className = "text-sm text-gray-500";
1023
- addRow.appendChild(addBtn);
1024
- addRow.appendChild(countDisplay);
1025
- wrapper.appendChild(addRow);
1208
+ const handle = createAddItemRow(
1209
+ "textarea",
1210
+ () => {
1211
+ values.push(element.default || "");
1212
+ addTextareaItem(element.default || "");
1213
+ updateAddButton();
1214
+ updateRemoveButtons();
1215
+ },
1216
+ { label: element.addLabel }
1217
+ );
1218
+ addUpdate = handle.update;
1219
+ mountCounterInLabel(wrapper, handle.counter);
1220
+ wrapper.appendChild(handle.row);
1026
1221
  }
1027
1222
  function updateAddButton() {
1028
- if (!addRow || !countDisplay) return;
1029
- const addBtn = addRow.querySelector(
1030
- ".add-textarea-btn"
1031
- );
1032
- if (addBtn) {
1033
- const disabled = values.length >= maxCount;
1034
- addBtn.disabled = disabled;
1035
- addBtn.style.opacity = disabled ? "0.5" : "1";
1036
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
1037
- }
1038
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
1223
+ if (addUpdate) addUpdate(values.length, maxCount);
1039
1224
  }
1040
1225
  values.forEach((value) => addTextareaItem(value));
1041
1226
  updateAddButton();
@@ -1067,6 +1252,91 @@ function updateTextareaField(element, fieldPath, value, context) {
1067
1252
  }
1068
1253
 
1069
1254
  // src/components/number.ts
1255
+ function ensureStepperStyles(doc) {
1256
+ const ID = "fb-number-stepper-styles";
1257
+ if (doc.getElementById(ID)) return;
1258
+ const style = doc.createElement("style");
1259
+ style.id = ID;
1260
+ style.textContent = `
1261
+ .fb-stepper-input::-webkit-outer-spin-button,
1262
+ .fb-stepper-input::-webkit-inner-spin-button {
1263
+ -webkit-appearance: none;
1264
+ margin: 0;
1265
+ }
1266
+ .fb-stepper-input { -moz-appearance: textfield; }
1267
+ `;
1268
+ doc.head.appendChild(style);
1269
+ }
1270
+ function buildStepper(input, element, readonly) {
1271
+ var _a;
1272
+ ensureStepperStyles(input.ownerDocument);
1273
+ const step = (_a = element.step) != null ? _a : 1;
1274
+ const min = element.min;
1275
+ const max = element.max;
1276
+ const wrap = document.createElement("div");
1277
+ wrap.className = "fb-stepper";
1278
+ wrap.style.cssText = `
1279
+ display: inline-flex;
1280
+ align-items: stretch;
1281
+ border: var(--fb-border-width) solid var(--fb-border-color);
1282
+ border-radius: var(--fb-border-radius);
1283
+ overflow: hidden;
1284
+ background: var(--fb-background-color);
1285
+ `;
1286
+ const makeBtn = (label, delta) => {
1287
+ const b = document.createElement("button");
1288
+ b.type = "button";
1289
+ b.textContent = label;
1290
+ b.tabIndex = -1;
1291
+ b.style.cssText = `
1292
+ width: 32px;
1293
+ border: none;
1294
+ background: transparent;
1295
+ color: var(--fb-text-color);
1296
+ font-size: var(--fb-font-size);
1297
+ font-family: var(--fb-font-family);
1298
+ cursor: ${readonly ? "default" : "pointer"};
1299
+ user-select: none;
1300
+ `;
1301
+ if (readonly) {
1302
+ b.disabled = true;
1303
+ b.style.opacity = "0.5";
1304
+ } else {
1305
+ b.addEventListener("click", (e) => {
1306
+ var _a2;
1307
+ e.preventDefault();
1308
+ const current = parseFloat(input.value);
1309
+ const base = Number.isFinite(current) ? current : (_a2 = min != null ? min : element.default) != null ? _a2 : 0;
1310
+ let next = parseFloat((base + delta * step).toPrecision(12));
1311
+ if (min != null) next = Math.max(min, next);
1312
+ if (max != null) next = Math.min(max, next);
1313
+ input.value = String(next);
1314
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1315
+ input.dispatchEvent(new Event("change", { bubbles: true }));
1316
+ });
1317
+ }
1318
+ return b;
1319
+ };
1320
+ input.classList.add("fb-stepper-input");
1321
+ input.style.cssText = `
1322
+ width: 56px;
1323
+ border: none;
1324
+ border-left: var(--fb-border-width) solid var(--fb-border-color);
1325
+ border-right: var(--fb-border-width) solid var(--fb-border-color);
1326
+ padding: var(--fb-input-padding-y) 0;
1327
+ font-size: var(--fb-font-size);
1328
+ font-family: var(--fb-font-family);
1329
+ text-align: center;
1330
+ background: transparent;
1331
+ color: var(--fb-text-color);
1332
+ -moz-appearance: textfield;
1333
+ box-sizing: border-box;
1334
+ `;
1335
+ wrap.appendChild(makeBtn("\u2212", -1));
1336
+ wrap.appendChild(input);
1337
+ wrap.appendChild(makeBtn("+", 1));
1338
+ return wrap;
1339
+ }
1070
1340
  function createNumberRangeHint(element, input) {
1071
1341
  const hint = document.createElement("span");
1072
1342
  hint.className = "number-range-hint";
@@ -1113,14 +1383,6 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1113
1383
  inputWrapper.style.cssText = "position: relative;";
1114
1384
  const numberInput = document.createElement("input");
1115
1385
  numberInput.type = "number";
1116
- numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1117
- numberInput.style.cssText = `
1118
- padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
1119
- font-size: var(--fb-font-size);
1120
- font-family: var(--fb-font-family);
1121
- width: 100%;
1122
- box-sizing: border-box;
1123
- `;
1124
1386
  numberInput.name = pathKey;
1125
1387
  numberInput.placeholder = element.placeholder || "0";
1126
1388
  if (element.min !== void 0) numberInput.min = element.min.toString();
@@ -1128,6 +1390,16 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1128
1390
  if (element.step !== void 0) numberInput.step = element.step.toString();
1129
1391
  numberInput.value = ctx.prefill[element.key] || element.default || "";
1130
1392
  numberInput.readOnly = readonly;
1393
+ if (!element.stepper) {
1394
+ numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1395
+ numberInput.style.cssText = `
1396
+ padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
1397
+ font-size: var(--fb-font-size);
1398
+ font-family: var(--fb-font-family);
1399
+ width: 100%;
1400
+ box-sizing: border-box;
1401
+ `;
1402
+ }
1131
1403
  if (!readonly && ctx.instance) {
1132
1404
  const handleChange = () => {
1133
1405
  const value = numberInput.value ? parseFloat(numberInput.value) : null;
@@ -1136,10 +1408,14 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1136
1408
  numberInput.addEventListener("blur", handleChange);
1137
1409
  numberInput.addEventListener("input", handleChange);
1138
1410
  }
1139
- inputWrapper.appendChild(numberInput);
1140
- if (!readonly && (element.min != null || element.max != null)) {
1141
- const counter = createNumberRangeHint(element, numberInput);
1142
- inputWrapper.appendChild(counter);
1411
+ if (element.stepper) {
1412
+ inputWrapper.appendChild(buildStepper(numberInput, element, readonly));
1413
+ } else {
1414
+ inputWrapper.appendChild(numberInput);
1415
+ if (!readonly && (element.min != null || element.max != null)) {
1416
+ const counter = createNumberRangeHint(element, numberInput);
1417
+ inputWrapper.appendChild(counter);
1418
+ }
1143
1419
  }
1144
1420
  wrapper.appendChild(inputWrapper);
1145
1421
  }
@@ -1242,50 +1518,24 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1242
1518
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1243
1519
  });
1244
1520
  }
1245
- let addRow = null;
1246
- let countDisplay = null;
1521
+ let addUpdate = null;
1247
1522
  if (!readonly) {
1248
- addRow = document.createElement("div");
1249
- addRow.className = "flex items-center gap-3 mt-2";
1250
- const addBtn = document.createElement("button");
1251
- addBtn.type = "button";
1252
- addBtn.className = "add-number-btn px-3 py-1 rounded";
1253
- addBtn.style.cssText = `
1254
- color: var(--fb-primary-color);
1255
- border: var(--fb-border-width) solid var(--fb-primary-color);
1256
- background-color: transparent;
1257
- font-size: var(--fb-font-size);
1258
- transition: all var(--fb-transition-duration);
1259
- `;
1260
- addBtn.textContent = "+";
1261
- addBtn.addEventListener("mouseenter", () => {
1262
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
1263
- });
1264
- addBtn.addEventListener("mouseleave", () => {
1265
- addBtn.style.backgroundColor = "transparent";
1266
- });
1267
- addBtn.onclick = () => {
1268
- values.push(element.default || "");
1269
- addNumberItem(element.default || "");
1270
- updateAddButton();
1271
- updateRemoveButtons();
1272
- };
1273
- countDisplay = document.createElement("span");
1274
- countDisplay.className = "text-sm text-gray-500";
1275
- addRow.appendChild(addBtn);
1276
- addRow.appendChild(countDisplay);
1277
- wrapper.appendChild(addRow);
1523
+ const handle = createAddItemRow(
1524
+ "number",
1525
+ () => {
1526
+ values.push(element.default || "");
1527
+ addNumberItem(element.default || "");
1528
+ updateAddButton();
1529
+ updateRemoveButtons();
1530
+ },
1531
+ { label: element.addLabel }
1532
+ );
1533
+ addUpdate = handle.update;
1534
+ mountCounterInLabel(wrapper, handle.counter);
1535
+ wrapper.appendChild(handle.row);
1278
1536
  }
1279
1537
  function updateAddButton() {
1280
- if (!addRow || !countDisplay) return;
1281
- const addBtn = addRow.querySelector(".add-number-btn");
1282
- if (addBtn) {
1283
- const disabled = values.length >= maxCount;
1284
- addBtn.disabled = disabled;
1285
- addBtn.style.opacity = disabled ? "0.5" : "1";
1286
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
1287
- }
1288
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
1538
+ if (addUpdate) addUpdate(values.length, maxCount);
1289
1539
  }
1290
1540
  values.forEach((value) => addNumberItem(value));
1291
1541
  updateAddButton();
@@ -1423,13 +1673,14 @@ function updateNumberField(element, fieldPath, value, context) {
1423
1673
  return;
1424
1674
  }
1425
1675
  const inputs = scopeRoot.querySelectorAll(
1426
- `[name^="${fieldPath}["]`
1676
+ `[name^="${fieldPath}\\["]`
1427
1677
  );
1428
1678
  inputs.forEach((input, index) => {
1429
1679
  if (index < value.length) {
1430
1680
  input.value = value[index] != null ? String(value[index]) : "";
1431
1681
  input.classList.remove("invalid");
1432
1682
  input.title = "";
1683
+ clearFieldError(input);
1433
1684
  }
1434
1685
  });
1435
1686
  if (value.length !== inputs.length) {
@@ -1445,6 +1696,7 @@ function updateNumberField(element, fieldPath, value, context) {
1445
1696
  input.value = value != null ? String(value) : "";
1446
1697
  input.classList.remove("invalid");
1447
1698
  input.title = "";
1699
+ clearFieldError(input);
1448
1700
  }
1449
1701
  }
1450
1702
  }
@@ -1574,52 +1826,26 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1574
1826
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1575
1827
  });
1576
1828
  }
1577
- let addRow = null;
1578
- let countDisplay = null;
1829
+ let addUpdate = null;
1579
1830
  if (!readonly) {
1580
- addRow = document.createElement("div");
1581
- addRow.className = "flex items-center gap-3 mt-2";
1582
- const addBtn = document.createElement("button");
1583
- addBtn.type = "button";
1584
- addBtn.className = "add-select-btn px-3 py-1 rounded";
1585
- addBtn.style.cssText = `
1586
- color: var(--fb-primary-color);
1587
- border: var(--fb-border-width) solid var(--fb-primary-color);
1588
- background-color: transparent;
1589
- font-size: var(--fb-font-size);
1590
- transition: all var(--fb-transition-duration);
1591
- `;
1592
- addBtn.textContent = "+";
1593
- addBtn.addEventListener("mouseenter", () => {
1594
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
1595
- });
1596
- addBtn.addEventListener("mouseleave", () => {
1597
- addBtn.style.backgroundColor = "transparent";
1598
- });
1599
- addBtn.onclick = () => {
1600
- var _a2, _b2;
1601
- const defaultValue = element.default || ((_b2 = (_a2 = element.options) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.value) || "";
1602
- values.push(defaultValue);
1603
- addSelectItem(defaultValue);
1604
- updateAddButton();
1605
- updateRemoveButtons();
1606
- };
1607
- countDisplay = document.createElement("span");
1608
- countDisplay.className = "text-sm text-gray-500";
1609
- addRow.appendChild(addBtn);
1610
- addRow.appendChild(countDisplay);
1611
- wrapper.appendChild(addRow);
1831
+ const handle = createAddItemRow(
1832
+ "select",
1833
+ () => {
1834
+ var _a2, _b2;
1835
+ const defaultValue = element.default || ((_b2 = (_a2 = element.options) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.value) || "";
1836
+ values.push(defaultValue);
1837
+ addSelectItem(defaultValue);
1838
+ updateAddButton();
1839
+ updateRemoveButtons();
1840
+ },
1841
+ { label: element.addLabel }
1842
+ );
1843
+ addUpdate = handle.update;
1844
+ mountCounterInLabel(wrapper, handle.counter);
1845
+ wrapper.appendChild(handle.row);
1612
1846
  }
1613
1847
  function updateAddButton() {
1614
- if (!addRow || !countDisplay) return;
1615
- const addBtn = addRow.querySelector(".add-select-btn");
1616
- if (addBtn) {
1617
- const disabled = values.length >= maxCount;
1618
- addBtn.disabled = disabled;
1619
- addBtn.style.opacity = disabled ? "0.5" : "1";
1620
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
1621
- }
1622
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
1848
+ if (addUpdate) addUpdate(values.length, maxCount);
1623
1849
  }
1624
1850
  values.forEach((value) => addSelectItem(value));
1625
1851
  updateAddButton();
@@ -1687,7 +1913,7 @@ function validateSelectElement(element, key, context) {
1687
1913
  };
1688
1914
  if ("multiple" in element && element.multiple) {
1689
1915
  const inputs = scopeRoot.querySelectorAll(
1690
- `[name^="${key}["]`
1916
+ `[name^="${key}\\["]`
1691
1917
  );
1692
1918
  const values = [];
1693
1919
  inputs.forEach((input) => {
@@ -1724,7 +1950,7 @@ function updateSelectField(element, fieldPath, value, context) {
1724
1950
  return;
1725
1951
  }
1726
1952
  const selects = scopeRoot.querySelectorAll(
1727
- `[name^="${fieldPath}["]`
1953
+ `[name^="${fieldPath}\\["]`
1728
1954
  );
1729
1955
  selects.forEach((select, index) => {
1730
1956
  if (index < value.length) {
@@ -1735,6 +1961,7 @@ function updateSelectField(element, fieldPath, value, context) {
1735
1961
  });
1736
1962
  select.classList.remove("invalid");
1737
1963
  select.title = "";
1964
+ clearFieldError(select);
1738
1965
  }
1739
1966
  });
1740
1967
  if (value.length !== selects.length) {
@@ -1754,72 +1981,147 @@ function updateSelectField(element, fieldPath, value, context) {
1754
1981
  });
1755
1982
  select.classList.remove("invalid");
1756
1983
  select.title = "";
1984
+ clearFieldError(select);
1757
1985
  }
1758
1986
  }
1759
1987
  }
1760
1988
 
1761
1989
  // src/components/switcher.ts
1762
- function applySelectedStyle(btn) {
1763
- btn.style.backgroundColor = "var(--fb-primary-color)";
1764
- btn.style.color = "#ffffff";
1765
- btn.style.borderColor = "var(--fb-primary-color)";
1990
+ function applySelectedStyle(btn, isPreset) {
1991
+ if (isPreset) {
1992
+ btn.style.backgroundColor = "var(--fb-primary-soft-color)";
1993
+ btn.style.color = "var(--fb-primary-color)";
1994
+ btn.style.borderColor = "var(--fb-primary-color)";
1995
+ } else {
1996
+ btn.style.backgroundColor = "var(--fb-primary-color)";
1997
+ btn.style.color = "#ffffff";
1998
+ btn.style.borderColor = "var(--fb-primary-color)";
1999
+ }
1766
2000
  }
1767
- function applyUnselectedStyle(btn) {
1768
- btn.style.backgroundColor = "transparent";
2001
+ function applyUnselectedStyle(btn, isPreset) {
2002
+ btn.style.backgroundColor = isPreset ? "var(--fb-background-color)" : "transparent";
1769
2003
  btn.style.color = "var(--fb-text-color)";
1770
2004
  btn.style.borderColor = "var(--fb-border-color)";
1771
2005
  }
2006
+ function isPresetButton(btn) {
2007
+ return btn.classList.contains("fb-switcher-preset");
2008
+ }
2009
+ function buildPresetCard(option, readonly) {
2010
+ const btn = document.createElement("button");
2011
+ btn.type = "button";
2012
+ btn.className = "fb-switcher-btn fb-switcher-preset";
2013
+ btn.dataset.value = option.value;
2014
+ btn.style.cssText = `
2015
+ display: inline-flex;
2016
+ align-items: center;
2017
+ gap: 8px;
2018
+ padding: 7px 12px 7px 10px;
2019
+ border-width: var(--fb-border-width);
2020
+ border-style: solid;
2021
+ border-radius: 999px;
2022
+ background: var(--fb-background-color);
2023
+ font-size: var(--fb-font-size);
2024
+ font-family: var(--fb-font-family);
2025
+ line-height: 1.25;
2026
+ cursor: ${readonly ? "default" : "pointer"};
2027
+ transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
2028
+ outline: none;
2029
+ `;
2030
+ if (option.iconUrl) {
2031
+ const icon = document.createElement("img");
2032
+ icon.className = "fb-switcher-icon";
2033
+ icon.src = option.iconUrl;
2034
+ icon.alt = "";
2035
+ icon.setAttribute("aria-hidden", "true");
2036
+ icon.style.cssText = `
2037
+ display: block;
2038
+ flex: 0 0 auto;
2039
+ width: 20px;
2040
+ height: 20px;
2041
+ object-fit: contain;
2042
+ `;
2043
+ btn.appendChild(icon);
2044
+ }
2045
+ const name = document.createElement("span");
2046
+ name.className = "fb-switcher-name";
2047
+ name.textContent = option.label;
2048
+ name.style.cssText = "font-weight: 600;";
2049
+ btn.appendChild(name);
2050
+ if (option.subtitle) {
2051
+ const sub = document.createElement("span");
2052
+ sub.className = "fb-switcher-subtitle";
2053
+ sub.textContent = option.subtitle;
2054
+ sub.style.cssText = `
2055
+ font-size: var(--fb-font-size-small);
2056
+ opacity: 0.7;
2057
+ font-variant-numeric: tabular-nums;
2058
+ `;
2059
+ btn.appendChild(sub);
2060
+ }
2061
+ return btn;
2062
+ }
1772
2063
  function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onChange) {
1773
2064
  const options = element.options || [];
2065
+ const isPresetMode = options.some((o) => o.subtitle || o.iconUrl);
1774
2066
  const group = document.createElement("div");
1775
2067
  group.className = "fb-switcher-group";
1776
- group.style.cssText = `
1777
- display: inline-flex;
1778
- flex-direction: row;
1779
- flex-wrap: nowrap;
1780
- `;
2068
+ group.style.cssText = isPresetMode ? `
2069
+ display: flex;
2070
+ flex-direction: row;
2071
+ flex-wrap: wrap;
2072
+ gap: 6px;
2073
+ ` : `
2074
+ display: inline-flex;
2075
+ flex-direction: row;
2076
+ flex-wrap: nowrap;
2077
+ `;
1781
2078
  const buttons = [];
1782
2079
  options.forEach((option, index) => {
1783
- const btn = document.createElement("button");
1784
- btn.type = "button";
1785
- btn.className = "fb-switcher-btn";
1786
- btn.dataset.value = option.value;
1787
- btn.textContent = option.label;
1788
- btn.style.cssText = `
1789
- padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
1790
- font-size: var(--fb-font-size);
1791
- border-width: var(--fb-border-width);
1792
- border-style: solid;
1793
- cursor: ${readonly ? "default" : "pointer"};
1794
- transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
1795
- white-space: nowrap;
1796
- line-height: 1.25;
1797
- outline: none;
1798
- `;
1799
- if (options.length === 1) {
1800
- btn.style.borderRadius = "var(--fb-border-radius)";
1801
- } else if (index === 0) {
1802
- btn.style.borderRadius = "var(--fb-border-radius) 0 0 var(--fb-border-radius)";
1803
- btn.style.borderRightWidth = "0";
1804
- } else if (index === options.length - 1) {
1805
- btn.style.borderRadius = "0 var(--fb-border-radius) var(--fb-border-radius) 0";
2080
+ let btn;
2081
+ if (isPresetMode) {
2082
+ btn = buildPresetCard(option, readonly);
1806
2083
  } else {
1807
- btn.style.borderRadius = "0";
1808
- btn.style.borderRightWidth = "0";
2084
+ btn = document.createElement("button");
2085
+ btn.type = "button";
2086
+ btn.className = "fb-switcher-btn";
2087
+ btn.dataset.value = option.value;
2088
+ btn.textContent = option.label;
2089
+ btn.style.cssText = `
2090
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
2091
+ font-size: var(--fb-font-size);
2092
+ border-width: var(--fb-border-width);
2093
+ border-style: solid;
2094
+ cursor: ${readonly ? "default" : "pointer"};
2095
+ transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
2096
+ white-space: nowrap;
2097
+ line-height: 1.25;
2098
+ outline: none;
2099
+ `;
2100
+ if (options.length === 1) {
2101
+ btn.style.borderRadius = "var(--fb-border-radius)";
2102
+ } else if (index === 0) {
2103
+ btn.style.borderRadius = "var(--fb-border-radius) 0 0 var(--fb-border-radius)";
2104
+ btn.style.borderRightWidth = "0";
2105
+ } else if (index === options.length - 1) {
2106
+ btn.style.borderRadius = "0 var(--fb-border-radius) var(--fb-border-radius) 0";
2107
+ } else {
2108
+ btn.style.borderRadius = "0";
2109
+ btn.style.borderRightWidth = "0";
2110
+ }
1809
2111
  }
1810
2112
  if (option.value === currentValue) {
1811
- applySelectedStyle(btn);
2113
+ applySelectedStyle(btn, isPresetMode);
1812
2114
  } else {
1813
- applyUnselectedStyle(btn);
2115
+ applyUnselectedStyle(btn, isPresetMode);
1814
2116
  }
1815
2117
  if (!readonly) {
1816
2118
  btn.addEventListener("click", () => {
1817
2119
  hiddenInput.value = option.value;
1818
2120
  buttons.forEach((b) => {
1819
2121
  if (b.dataset.value === option.value) {
1820
- applySelectedStyle(b);
2122
+ applySelectedStyle(b, isPresetMode);
1821
2123
  } else {
1822
- applyUnselectedStyle(b);
2124
+ applyUnselectedStyle(b, isPresetMode);
1823
2125
  }
1824
2126
  });
1825
2127
  if (onChange) {
@@ -1833,7 +2135,7 @@ function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onCha
1833
2135
  });
1834
2136
  btn.addEventListener("mouseleave", () => {
1835
2137
  if (hiddenInput.value !== option.value) {
1836
- btn.style.backgroundColor = "transparent";
2138
+ btn.style.backgroundColor = isPresetMode ? "var(--fb-background-color)" : "transparent";
1837
2139
  }
1838
2140
  });
1839
2141
  }
@@ -1968,54 +2270,26 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
1968
2270
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1969
2271
  });
1970
2272
  }
1971
- let addRow = null;
1972
- let countDisplay = null;
2273
+ let addUpdate = null;
1973
2274
  if (!readonly) {
1974
- addRow = document.createElement("div");
1975
- addRow.className = "flex items-center gap-3 mt-2";
1976
- const addBtn = document.createElement("button");
1977
- addBtn.type = "button";
1978
- addBtn.className = "add-switcher-btn px-3 py-1 rounded";
1979
- addBtn.style.cssText = `
1980
- color: var(--fb-primary-color);
1981
- border: var(--fb-border-width) solid var(--fb-primary-color);
1982
- background-color: transparent;
1983
- font-size: var(--fb-font-size);
1984
- transition: all var(--fb-transition-duration);
1985
- `;
1986
- addBtn.textContent = "+";
1987
- addBtn.addEventListener("mouseenter", () => {
1988
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
1989
- });
1990
- addBtn.addEventListener("mouseleave", () => {
1991
- addBtn.style.backgroundColor = "transparent";
1992
- });
1993
- addBtn.onclick = () => {
1994
- var _a2, _b2;
1995
- const defaultValue = element.default || ((_b2 = (_a2 = element.options) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.value) || "";
1996
- values.push(defaultValue);
1997
- addSwitcherItem(defaultValue);
1998
- updateAddButton();
1999
- updateRemoveButtons();
2000
- };
2001
- countDisplay = document.createElement("span");
2002
- countDisplay.className = "text-sm text-gray-500";
2003
- addRow.appendChild(addBtn);
2004
- addRow.appendChild(countDisplay);
2005
- wrapper.appendChild(addRow);
2275
+ const handle = createAddItemRow(
2276
+ "switcher",
2277
+ () => {
2278
+ var _a2, _b2;
2279
+ const defaultValue = element.default || ((_b2 = (_a2 = element.options) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.value) || "";
2280
+ values.push(defaultValue);
2281
+ addSwitcherItem(defaultValue);
2282
+ updateAddButton();
2283
+ updateRemoveButtons();
2284
+ },
2285
+ { label: element.addLabel }
2286
+ );
2287
+ addUpdate = handle.update;
2288
+ mountCounterInLabel(wrapper, handle.counter);
2289
+ wrapper.appendChild(handle.row);
2006
2290
  }
2007
2291
  function updateAddButton() {
2008
- if (!addRow || !countDisplay) return;
2009
- const addBtn = addRow.querySelector(
2010
- ".add-switcher-btn"
2011
- );
2012
- if (addBtn) {
2013
- const disabled = values.length >= maxCount;
2014
- addBtn.disabled = disabled;
2015
- addBtn.style.opacity = disabled ? "0.5" : "1";
2016
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
2017
- }
2018
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
2292
+ if (addUpdate) addUpdate(values.length, maxCount);
2019
2293
  }
2020
2294
  values.forEach((value) => addSwitcherItem(value));
2021
2295
  updateAddButton();
@@ -2086,7 +2360,7 @@ function validateSwitcherElement(element, key, context) {
2086
2360
  );
2087
2361
  if ("multiple" in element && element.multiple) {
2088
2362
  const inputs = scopeRoot.querySelectorAll(
2089
- `input[type="hidden"][name^="${key}["]`
2363
+ `input[type="hidden"][name^="${key}\\["]`
2090
2364
  );
2091
2365
  const values = [];
2092
2366
  inputs.forEach((input) => {
@@ -2135,7 +2409,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
2135
2409
  return;
2136
2410
  }
2137
2411
  const inputs = scopeRoot.querySelectorAll(
2138
- `input[type="hidden"][name^="${fieldPath}["]`
2412
+ `input[type="hidden"][name^="${fieldPath}\\["]`
2139
2413
  );
2140
2414
  inputs.forEach((input, index) => {
2141
2415
  var _a2;
@@ -2145,15 +2419,17 @@ function updateSwitcherField(element, fieldPath, value, context) {
2145
2419
  const group = (_a2 = input.parentElement) == null ? void 0 : _a2.querySelector(".fb-switcher-group");
2146
2420
  if (group) {
2147
2421
  group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
2422
+ const isPreset = isPresetButton(btn);
2148
2423
  if (btn.dataset.value === newVal) {
2149
- applySelectedStyle(btn);
2424
+ applySelectedStyle(btn, isPreset);
2150
2425
  } else {
2151
- applyUnselectedStyle(btn);
2426
+ applyUnselectedStyle(btn, isPreset);
2152
2427
  }
2153
2428
  });
2154
2429
  }
2155
2430
  input.classList.remove("invalid");
2156
2431
  input.title = "";
2432
+ clearFieldError(input);
2157
2433
  }
2158
2434
  });
2159
2435
  if (value.length !== inputs.length) {
@@ -2171,16 +2447,212 @@ function updateSwitcherField(element, fieldPath, value, context) {
2171
2447
  const group = (_a = input.parentElement) == null ? void 0 : _a.querySelector(".fb-switcher-group");
2172
2448
  if (group) {
2173
2449
  group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
2450
+ const isPreset = isPresetButton(btn);
2174
2451
  if (btn.dataset.value === newVal) {
2175
- applySelectedStyle(btn);
2452
+ applySelectedStyle(btn, isPreset);
2176
2453
  } else {
2177
- applyUnselectedStyle(btn);
2454
+ applyUnselectedStyle(btn, isPreset);
2178
2455
  }
2179
2456
  });
2180
2457
  }
2181
2458
  input.classList.remove("invalid");
2182
2459
  input.title = "";
2460
+ clearFieldError(input);
2461
+ }
2462
+ }
2463
+ }
2464
+
2465
+ // src/components/boolean.ts
2466
+ var TOGGLE_W = 36;
2467
+ var TOGGLE_H = 20;
2468
+ var KNOB = 16;
2469
+ function ensureStyles(doc) {
2470
+ const ID = "fb-boolean-styles";
2471
+ if (doc.getElementById(ID)) return;
2472
+ const style = doc.createElement("style");
2473
+ style.id = ID;
2474
+ style.textContent = `
2475
+ .fb-toggle {
2476
+ position: relative;
2477
+ display: inline-block;
2478
+ width: ${TOGGLE_W}px;
2479
+ height: ${TOGGLE_H}px;
2480
+ border-radius: ${TOGGLE_H}px;
2481
+ background: var(--fb-border-color);
2482
+ transition: background-color var(--fb-transition-duration);
2483
+ flex-shrink: 0;
2484
+ }
2485
+ .fb-toggle::after {
2486
+ content: "";
2487
+ position: absolute;
2488
+ top: ${(TOGGLE_H - KNOB) / 2}px;
2489
+ left: ${(TOGGLE_H - KNOB) / 2}px;
2490
+ width: ${KNOB}px;
2491
+ height: ${KNOB}px;
2492
+ border-radius: 50%;
2493
+ background: #ffffff;
2494
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
2495
+ transition: transform var(--fb-transition-duration);
2496
+ }
2497
+ .fb-toggle.fb-on {
2498
+ background: var(--fb-primary-color);
2499
+ }
2500
+ .fb-toggle.fb-on::after {
2501
+ transform: translateX(${TOGGLE_W - KNOB - (TOGGLE_H - KNOB)}px);
2502
+ }
2503
+ .fb-toggle-row {
2504
+ display: flex;
2505
+ align-items: center;
2506
+ gap: 12px;
2507
+ padding: 12px 14px;
2508
+ background: var(--fb-surface-soft-color);
2509
+ border: var(--fb-border-width) solid var(--fb-border-color);
2510
+ border-radius: var(--fb-border-radius);
2511
+ cursor: pointer;
2512
+ user-select: none;
2513
+ }
2514
+ .fb-toggle-row[aria-disabled="true"] {
2515
+ cursor: default;
2516
+ opacity: 0.7;
2517
+ }
2518
+ .fb-toggle-row:focus-visible {
2519
+ outline: var(--fb-focus-ring-width) solid var(--fb-focus-ring-color);
2520
+ outline-offset: var(--fb-focus-ring-offset);
2521
+ }
2522
+ .fb-toggle-text { flex: 1; min-width: 0; }
2523
+ .fb-toggle-title {
2524
+ display: flex;
2525
+ align-items: center;
2526
+ gap: 4px;
2527
+ font-size: var(--fb-font-size);
2528
+ font-weight: 500;
2529
+ color: var(--fb-text-color);
2530
+ line-height: 1.3;
2531
+ }
2532
+ .fb-toggle-subtitle {
2533
+ font-size: var(--fb-font-size-small);
2534
+ color: var(--fb-text-secondary-color);
2535
+ margin-top: 2px;
2536
+ line-height: 1.35;
2537
+ }
2538
+ .fb-toggle-info {
2539
+ flex: 0 0 14px;
2540
+ display: inline-flex;
2541
+ align-items: center;
2542
+ justify-content: center;
2543
+ width: 14px;
2544
+ height: 14px;
2545
+ border-radius: 50%;
2546
+ background: var(--fb-border-color);
2547
+ color: #fff;
2548
+ font-size: 10px;
2549
+ font-weight: 700;
2550
+ font-style: italic;
2551
+ font-family: serif;
2552
+ cursor: help;
2183
2553
  }
2554
+ `;
2555
+ doc.head.appendChild(style);
2556
+ }
2557
+ function parseBool(v) {
2558
+ if (typeof v === "boolean") return v;
2559
+ if (typeof v === "string") return v === "true" || v === "on" || v === "1";
2560
+ return false;
2561
+ }
2562
+ function renderBooleanElement(element, ctx, wrapper, pathKey) {
2563
+ var _a;
2564
+ ensureStyles(document);
2565
+ const state = ctx.state;
2566
+ const readonly = isElementReadonly(element, state, ctx);
2567
+ const prefillRaw = ctx.prefill[element.key];
2568
+ const initial = prefillRaw !== void 0 ? parseBool(prefillRaw) : parseBool(element.default);
2569
+ const hiddenInput = document.createElement("input");
2570
+ hiddenInput.type = "hidden";
2571
+ hiddenInput.name = pathKey;
2572
+ hiddenInput.value = initial ? "true" : "false";
2573
+ const row = document.createElement("div");
2574
+ row.className = "fb-toggle-row";
2575
+ row.setAttribute("role", "switch");
2576
+ row.setAttribute("aria-checked", initial ? "true" : "false");
2577
+ if (readonly) {
2578
+ row.setAttribute("aria-disabled", "true");
2579
+ } else {
2580
+ row.tabIndex = 0;
2581
+ }
2582
+ const pill = document.createElement("span");
2583
+ pill.className = "fb-toggle" + (initial ? " fb-on" : "");
2584
+ pill.setAttribute("aria-hidden", "true");
2585
+ row.appendChild(pill);
2586
+ const textBlock = document.createElement("div");
2587
+ textBlock.className = "fb-toggle-text";
2588
+ const titleEl = document.createElement("div");
2589
+ titleEl.className = "fb-toggle-title";
2590
+ titleEl.appendChild(document.createTextNode((_a = element.label) != null ? _a : ""));
2591
+ if (element.description) {
2592
+ const info = document.createElement("span");
2593
+ info.className = "fb-toggle-info";
2594
+ info.textContent = "i";
2595
+ info.title = element.description;
2596
+ titleEl.appendChild(info);
2597
+ }
2598
+ textBlock.appendChild(titleEl);
2599
+ if (element.hint) {
2600
+ const subtitle = document.createElement("div");
2601
+ subtitle.className = "fb-toggle-subtitle";
2602
+ subtitle.textContent = element.hint;
2603
+ textBlock.appendChild(subtitle);
2604
+ }
2605
+ row.appendChild(textBlock);
2606
+ if (!readonly) {
2607
+ const toggle = () => {
2608
+ const next = hiddenInput.value !== "true";
2609
+ hiddenInput.value = next ? "true" : "false";
2610
+ pill.classList.toggle("fb-on", next);
2611
+ row.setAttribute("aria-checked", next ? "true" : "false");
2612
+ if (ctx.instance) ctx.instance.triggerOnChange(pathKey, next);
2613
+ };
2614
+ row.addEventListener("click", (e) => {
2615
+ var _a2;
2616
+ if ((_a2 = e.target) == null ? void 0 : _a2.classList.contains("fb-toggle-info")) {
2617
+ return;
2618
+ }
2619
+ toggle();
2620
+ });
2621
+ row.addEventListener("keydown", (e) => {
2622
+ if (e.key === " " || e.key === "Enter") {
2623
+ e.preventDefault();
2624
+ toggle();
2625
+ }
2626
+ });
2627
+ }
2628
+ wrapper.appendChild(hiddenInput);
2629
+ wrapper.appendChild(row);
2630
+ }
2631
+ function validateBooleanElement(element, key, context) {
2632
+ var _a;
2633
+ const { scopeRoot } = context;
2634
+ const input = scopeRoot.querySelector(
2635
+ `input[type="hidden"][name="${key}"]`
2636
+ );
2637
+ const raw = (_a = input == null ? void 0 : input.value) != null ? _a : "";
2638
+ const value = parseBool(raw);
2639
+ const errors = [];
2640
+ return { value, errors };
2641
+ }
2642
+ function updateBooleanField(_element, fieldPath, value, context) {
2643
+ var _a;
2644
+ const { scopeRoot } = context;
2645
+ const input = scopeRoot.querySelector(
2646
+ `input[type="hidden"][name="${fieldPath}"]`
2647
+ );
2648
+ if (!input) return;
2649
+ const bool = parseBool(value);
2650
+ input.value = bool ? "true" : "false";
2651
+ const row = (_a = input.parentElement) == null ? void 0 : _a.querySelector(".fb-toggle-row");
2652
+ if (row) {
2653
+ row.setAttribute("aria-checked", bool ? "true" : "false");
2654
+ const pill = row.querySelector(".fb-toggle");
2655
+ if (pill) pill.classList.toggle("fb-on", bool);
2184
2656
  }
2185
2657
  }
2186
2658
 
@@ -2286,14 +2758,20 @@ function ensureFileStyles() {
2286
2758
  }
2287
2759
 
2288
2760
  /* \u2500\u2500\u2500 Wide single-file add tile (empty state) \u2500\u2500\u2500 */
2761
+ /* Flex-wraps: side-by-side when wide enough, stacks upload/library
2762
+ vertically when narrow (e.g. inside a 50/50 container column). */
2289
2763
  .fb-wide-tile {
2290
2764
  width: 100%;
2765
+ box-sizing: border-box;
2291
2766
  border-radius: 0.75rem;
2292
2767
  border: 1px dashed #60a5fa;
2293
2768
  background: rgba(239,246,255,0.5);
2294
2769
  display: flex;
2770
+ flex-wrap: wrap;
2771
+ align-items: stretch;
2772
+ gap: 0;
2295
2773
  overflow: hidden;
2296
- height: 180px;
2774
+ min-height: 180px;
2297
2775
  transition: border-color 150ms, background 150ms, box-shadow 150ms;
2298
2776
  cursor: pointer;
2299
2777
  }
@@ -2307,9 +2785,12 @@ function ensureFileStyles() {
2307
2785
  box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
2308
2786
  }
2309
2787
 
2310
- /* Upload zone inside wide tile */
2788
+ /* Upload zone inside wide tile.
2789
+ flex: 1 1 220px \u2014 wants at least 220px; if the container can't fit
2790
+ upload + library on one row (~220 + 176), library wraps below. */
2311
2791
  .fb-wide-tile-upload {
2312
- flex: 1;
2792
+ flex: 1 1 220px;
2793
+ min-height: 140px;
2313
2794
  display: flex;
2314
2795
  flex-direction: column;
2315
2796
  align-items: center;
@@ -2322,24 +2803,21 @@ function ensureFileStyles() {
2322
2803
  background: transparent;
2323
2804
  border: none;
2324
2805
  font-family: inherit;
2806
+ /* Dashed separator from library: right side when in a row, bottom when
2807
+ wrapped (the line then sits between the two stacked cards). */
2808
+ border-right: 1px dashed rgba(96,165,250,0.5);
2325
2809
  }
2326
2810
  .fb-wide-tile-upload:hover {
2327
2811
  background: rgba(191,219,254,0.25);
2328
2812
  }
2329
-
2330
- /* Vertical dashed divider between upload and library zones */
2331
- .fb-wide-tile-divider {
2332
- width: 1px;
2333
- margin: 16px 0;
2334
- border-left: 1px dashed rgba(96,165,250,0.5);
2335
- background: transparent;
2336
- flex-shrink: 0;
2337
- }
2338
-
2339
- /* Library zone inside wide tile */
2813
+ /* Library zone inside wide tile.
2814
+ flex: 0 0 176px \u2014 fixed 176px, never grows. Upload fills the rest in
2815
+ row layout. When the tile wraps to two rows on narrow containers,
2816
+ library stays 176px wide on its own row (left-aligned), preserving the
2817
+ visual hierarchy "upload > library" in both layouts. */
2340
2818
  .fb-wide-tile-library {
2341
- width: 176px;
2342
- flex-shrink: 0;
2819
+ flex: 0 0 176px;
2820
+ min-height: 120px;
2343
2821
  display: flex;
2344
2822
  flex-direction: column;
2345
2823
  align-items: center;
@@ -2356,6 +2834,10 @@ function ensureFileStyles() {
2356
2834
  .fb-wide-tile-library:hover {
2357
2835
  background: rgba(191,219,254,0.25);
2358
2836
  }
2837
+ /* Narrow-tile mode lives in a separate <style> tag (see below) \u2014 the
2838
+ @container rule is appended only when the runtime actually supports
2839
+ container queries, so jsdom (which doesn't) never sees it and stays
2840
+ quiet in test logs. */
2359
2841
 
2360
2842
  /* \u2500\u2500\u2500 Multi-file outer grid container \u2500\u2500\u2500 */
2361
2843
  .fb-multi-outer {
@@ -2734,6 +3216,39 @@ function ensureFileStyles() {
2734
3216
  }
2735
3217
  `;
2736
3218
  document.head.appendChild(style);
3219
+ if (typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("container-type", "inline-size")) {
3220
+ const cq = document.createElement("style");
3221
+ cq.setAttribute("data-fb-file-styles-cq", "true");
3222
+ cq.textContent = `
3223
+ .fb-wide-tile { container-type: inline-size; }
3224
+ @container (max-width: 408px) {
3225
+ .fb-wide-tile-upload {
3226
+ border-right: none;
3227
+ border-bottom: 1px dashed rgba(96,165,250,0.5);
3228
+ }
3229
+ .fb-wide-tile-library {
3230
+ flex: 1 0 100%;
3231
+ min-height: 0;
3232
+ flex-direction: row;
3233
+ gap: 6px;
3234
+ padding: 8px 12px;
3235
+ font-size: 12px;
3236
+ }
3237
+ .fb-wide-tile-library .fb-wide-tile-library-icon {
3238
+ width: 16px;
3239
+ height: 16px;
3240
+ }
3241
+ .fb-wide-tile-library .fb-wide-tile-library-label {
3242
+ font-size: 12px;
3243
+ font-weight: 500;
3244
+ }
3245
+ .fb-wide-tile-library .fb-wide-tile-library-hint {
3246
+ display: none;
3247
+ }
3248
+ }
3249
+ `;
3250
+ document.head.appendChild(cq);
3251
+ }
2737
3252
  }
2738
3253
 
2739
3254
  // src/components/file/dom.ts
@@ -2895,24 +3410,40 @@ function createTileActions(options) {
2895
3410
  return btn;
2896
3411
  };
2897
3412
  if (replaceHandler) {
2898
- const replaceBtn = makeBtn(ICON_REPLACE, t("replaceFile", state), "fb-tile-action-replace");
3413
+ const replaceBtn = makeBtn(
3414
+ ICON_REPLACE,
3415
+ t("replaceFile", state),
3416
+ "fb-tile-action-replace"
3417
+ );
2899
3418
  replaceBtn.addEventListener("click", () => replaceHandler());
2900
3419
  group.appendChild(replaceBtn);
2901
3420
  }
2902
3421
  if (libraryHandler) {
2903
- const libBtn = makeBtn(ICON_LIBRARY, t("fromLibrary", state), "fb-tile-action-library");
3422
+ const libBtn = makeBtn(
3423
+ ICON_LIBRARY,
3424
+ t("fromLibrary", state),
3425
+ "fb-tile-action-library"
3426
+ );
2904
3427
  libBtn.addEventListener("click", () => libraryHandler());
2905
3428
  group.appendChild(libBtn);
2906
3429
  }
2907
3430
  if (canDownload(state, meta)) {
2908
- const dlBtn = makeBtn(ICON_DOWNLOAD, t("downloadFile", state), "fb-tile-action-download");
3431
+ const dlBtn = makeBtn(
3432
+ ICON_DOWNLOAD,
3433
+ t("downloadFile", state),
3434
+ "fb-tile-action-download"
3435
+ );
2909
3436
  dlBtn.addEventListener("click", () => {
2910
3437
  triggerTileDownload(resourceId, fileName, state, meta);
2911
3438
  });
2912
3439
  group.appendChild(dlBtn);
2913
3440
  }
2914
3441
  if (canOpenInTab(state, meta)) {
2915
- const openBtn = makeBtn(ICON_OPEN, t("openInNewTab", state), "fb-tile-action-open");
3442
+ const openBtn = makeBtn(
3443
+ ICON_OPEN,
3444
+ t("openInNewTab", state),
3445
+ "fb-tile-action-open"
3446
+ );
2916
3447
  openBtn.addEventListener("click", () => {
2917
3448
  triggerTileOpen(resourceId, state, meta).catch((err) => {
2918
3449
  console.error("Open failed:", err);
@@ -2921,7 +3452,11 @@ function createTileActions(options) {
2921
3452
  group.appendChild(openBtn);
2922
3453
  }
2923
3454
  if (canRemove && removeHandler) {
2924
- const rmBtn = makeBtn(ICON_REMOVE, t("removeElement", state), "fb-tile-action-remove");
3455
+ const rmBtn = makeBtn(
3456
+ ICON_REMOVE,
3457
+ t("removeElement", state),
3458
+ "fb-tile-action-remove"
3459
+ );
2925
3460
  rmBtn.addEventListener("click", () => {
2926
3461
  removeHandler();
2927
3462
  });
@@ -2999,11 +3534,17 @@ function positionZoomPopup(popup, tile) {
2999
3534
  } else if (tileRect.bottom + margin + popupSize + padding <= window.innerHeight) {
3000
3535
  top = tileRect.bottom + margin;
3001
3536
  } else {
3002
- top = Math.max(padding, Math.min(window.innerHeight - popupSize - padding, tileRect.top));
3537
+ top = Math.max(
3538
+ padding,
3539
+ Math.min(window.innerHeight - popupSize - padding, tileRect.top)
3540
+ );
3003
3541
  }
3004
3542
  const tileCenterX = tileRect.left + tileRect.width / 2;
3005
3543
  let left = tileCenterX - popupSize / 2;
3006
- left = Math.max(padding, Math.min(window.innerWidth - popupSize - padding, left));
3544
+ left = Math.max(
3545
+ padding,
3546
+ Math.min(window.innerWidth - popupSize - padding, left)
3547
+ );
3007
3548
  popup.style.top = `${top}px`;
3008
3549
  popup.style.left = `${left}px`;
3009
3550
  }
@@ -3047,7 +3588,9 @@ function attachZoomHover(tile, src, alt, actionsEl) {
3047
3588
  const popup = getOrCreateZoomPopup();
3048
3589
  const existingActions = popup.querySelector(".fb-tile-actions");
3049
3590
  if (existingActions) existingActions.remove();
3050
- const img = popup.querySelector(".fb-tile-zoom-preview-img");
3591
+ const img = popup.querySelector(
3592
+ ".fb-tile-zoom-preview-img"
3593
+ );
3051
3594
  img.src = src;
3052
3595
  img.alt = alt;
3053
3596
  if (actionsEl) {
@@ -3075,7 +3618,9 @@ function attachZoomHover(tile, src, alt, actionsEl) {
3075
3618
  });
3076
3619
  }
3077
3620
  function attachClonedActionListeners(cloned, original) {
3078
- const originalBtns = Array.from(original.querySelectorAll(".fb-tile-action-btn"));
3621
+ const originalBtns = Array.from(
3622
+ original.querySelectorAll(".fb-tile-action-btn")
3623
+ );
3079
3624
  const clonedBtns = Array.from(cloned.querySelectorAll(".fb-tile-action-btn"));
3080
3625
  clonedBtns.forEach((clonedBtn, i) => {
3081
3626
  const origBtn = originalBtns[i];
@@ -3129,7 +3674,9 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
3129
3674
  return newContainer;
3130
3675
  }
3131
3676
  function attachVideoButtonHandlers(container, resourceId, state, deps) {
3132
- const changeBtn = container.querySelector(".change-file-btn");
3677
+ const changeBtn = container.querySelector(
3678
+ ".change-file-btn"
3679
+ );
3133
3680
  if (changeBtn) {
3134
3681
  changeBtn.onclick = (e) => {
3135
3682
  var _a;
@@ -3137,7 +3684,9 @@ function attachVideoButtonHandlers(container, resourceId, state, deps) {
3137
3684
  (_a = deps == null ? void 0 : deps.picker) == null ? void 0 : _a.click();
3138
3685
  };
3139
3686
  }
3140
- const deleteBtn = container.querySelector(".delete-file-btn");
3687
+ const deleteBtn = container.querySelector(
3688
+ ".delete-file-btn"
3689
+ );
3141
3690
  if (deleteBtn) {
3142
3691
  deleteBtn.onclick = (e) => {
3143
3692
  e.stopPropagation();
@@ -3275,7 +3824,13 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
3275
3824
  deps
3276
3825
  );
3277
3826
  } else {
3278
- await renderUploadedFilePreview(container, resourceId, fileName, meta, state);
3827
+ await renderUploadedFilePreview(
3828
+ container,
3829
+ resourceId,
3830
+ fileName,
3831
+ meta,
3832
+ state
3833
+ );
3279
3834
  const isVideo = (_a = meta == null ? void 0 : meta.type) == null ? void 0 : _a.startsWith("video/");
3280
3835
  if (!isReadonly && !isVideo) {
3281
3836
  renderDeleteButton(container, resourceId, state);
@@ -3304,7 +3859,8 @@ async function renderFilePreviewReadonly(resourceId, state, fileName, options =
3304
3859
  }
3305
3860
  const localFileUrl = (meta == null ? void 0 : meta.file) instanceof File ? getLocalFileUrl(meta.file) : null;
3306
3861
  const resolveOpenUrl = async () => {
3307
- if (state.config.getDownloadUrl) return state.config.getDownloadUrl(resourceId);
3862
+ if (state.config.getDownloadUrl)
3863
+ return state.config.getDownloadUrl(resourceId);
3308
3864
  if (state.config.getThumbnail) return state.config.getThumbnail(resourceId);
3309
3865
  return localFileUrl;
3310
3866
  };
@@ -3446,7 +4002,8 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
3446
4002
  }
3447
4003
  } catch (error) {
3448
4004
  const err = error instanceof Error ? error : new Error(String(error));
3449
- if (state.config.onThumbnailError) state.config.onThumbnailError(err, rid);
4005
+ if (state.config.onThumbnailError)
4006
+ state.config.onThumbnailError(err, rid);
3450
4007
  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>`;
3451
4008
  }
3452
4009
  } else {
@@ -3479,7 +4036,8 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
3479
4036
  }
3480
4037
  } catch (error) {
3481
4038
  const err = error instanceof Error ? error : new Error(String(error));
3482
- if (state.config.onThumbnailError) state.config.onThumbnailError(err, rid);
4039
+ if (state.config.onThumbnailError)
4040
+ state.config.onThumbnailError(err, rid);
3483
4041
  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>`;
3484
4042
  }
3485
4043
  } else {
@@ -3513,7 +4071,8 @@ async function forceDownload(resourceId, fileName, state) {
3513
4071
  if (fileUrl) {
3514
4072
  const finalUrl = fileUrl.startsWith("http") ? fileUrl : new URL(fileUrl, window.location.href).href;
3515
4073
  const response = await fetch(finalUrl);
3516
- if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
4074
+ if (!response.ok)
4075
+ throw new Error(`HTTP error! status: ${response.status}`);
3517
4076
  const blob = await response.blob();
3518
4077
  downloadBlob(blob, fileName);
3519
4078
  } else {
@@ -3562,7 +4121,9 @@ async function uploadSingleFile(file, state) {
3562
4121
  } catch (error) {
3563
4122
  const err = error instanceof Error ? error : new Error(String(error));
3564
4123
  if (state.config.onUploadError) state.config.onUploadError(err, file);
3565
- throw new Error(`File upload failed: ${err.message}`);
4124
+ const wrapped = new Error(`File upload failed: ${err.message}`);
4125
+ wrapped.cause = err;
4126
+ throw wrapped;
3566
4127
  }
3567
4128
  }
3568
4129
  async function handleFileSelect(opts) {
@@ -3675,7 +4236,9 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
3675
4236
  const rejectedBySize = afterMime.filter(
3676
4237
  (f) => !isFileSizeAllowed(f, constraints.maxSize)
3677
4238
  );
3678
- const valid = afterMime.filter((f) => isFileSizeAllowed(f, constraints.maxSize));
4239
+ const valid = afterMime.filter(
4240
+ (f) => isFileSizeAllowed(f, constraints.maxSize)
4241
+ );
3679
4242
  const remaining = constraints.maxCount === Infinity ? valid.length : Math.max(0, constraints.maxCount - currentCount);
3680
4243
  const accepted = valid.slice(0, remaining);
3681
4244
  const skippedByCount = valid.length - accepted.length;
@@ -3688,7 +4251,13 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
3688
4251
  if (rejectedByMime.length > 0) {
3689
4252
  const mimes = constraints.allowedMimes.join(", ");
3690
4253
  const names = rejectedByMime.map((f) => f.name).join(", ");
3691
- errorParts.push(t("invalidFileMime", state, { name: names, type: rejectedByMime.map((f) => f.type).join(", "), mimes }));
4254
+ errorParts.push(
4255
+ t("invalidFileMime", state, {
4256
+ name: names,
4257
+ type: rejectedByMime.map((f) => f.type).join(", "),
4258
+ mimes
4259
+ })
4260
+ );
3692
4261
  }
3693
4262
  if (rejectedBySize.length > 0) {
3694
4263
  const names = rejectedBySize.map((f) => f.name).join(", ");
@@ -3713,7 +4282,8 @@ async function uploadBatch(accepted, resourceIds, listEl, state) {
3713
4282
  const addTile = (_a = tilesWrap.querySelector(".fb-multi-add-tile-js")) != null ? _a : tilesWrap.querySelector(".fb-tile-add");
3714
4283
  if (addTile) addTile.style.display = "none";
3715
4284
  }
3716
- await Promise.all(
4285
+ const failures = [];
4286
+ await Promise.allSettled(
3717
4287
  accepted.map(async (file) => {
3718
4288
  const placeholder = createUploadingTile(file.name, state);
3719
4289
  if (listEl) {
@@ -3730,11 +4300,27 @@ async function uploadBatch(accepted, resourceIds, listEl, state) {
3730
4300
  file: void 0
3731
4301
  });
3732
4302
  resourceIds.push(rid);
4303
+ } catch (err) {
4304
+ const wrapped = err instanceof Error ? err : new Error(String(err));
4305
+ const cause = wrapped.cause;
4306
+ const root = cause instanceof Error ? cause : cause !== void 0 ? new Error(String(cause)) : wrapped;
4307
+ failures.push({ file, error: root });
3733
4308
  } finally {
3734
4309
  placeholder.remove();
3735
4310
  }
3736
4311
  })
3737
4312
  );
4313
+ return { failures };
4314
+ }
4315
+ function buildBatchErrorMessage(filterError, failures, state) {
4316
+ if (failures.length === 0) return filterError;
4317
+ const uploadMsg = failures.map(
4318
+ (f) => t("uploadFailed", state, {
4319
+ name: f.file.name,
4320
+ error: f.error.message
4321
+ })
4322
+ ).join(" \u2022 ");
4323
+ return filterError ? `${filterError} \u2022 ${uploadMsg}` : uploadMsg;
3738
4324
  }
3739
4325
  function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallback, constraints, pathKey, instance) {
3740
4326
  setupDragAndDrop(filesContainer, async (files) => {
@@ -3751,7 +4337,13 @@ function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallbac
3751
4337
  clearFileError(filesContainer);
3752
4338
  }
3753
4339
  const list = (_a = filesContainer.querySelector(".files-list")) != null ? _a : filesContainer;
3754
- await uploadBatch(accepted, resourceIds, list, state);
4340
+ const { failures } = await uploadBatch(accepted, resourceIds, list, state);
4341
+ const combined = buildBatchErrorMessage(errorMessage, failures, state);
4342
+ if (combined) {
4343
+ showFileError(filesContainer, combined);
4344
+ } else {
4345
+ clearFileError(filesContainer);
4346
+ }
3755
4347
  updateCallback();
3756
4348
  if (instance && pathKey && !state.config.readonly) {
3757
4349
  instance.triggerOnChange(pathKey, resourceIds);
@@ -3774,7 +4366,20 @@ function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback
3774
4366
  clearFileError(wrapperEl);
3775
4367
  }
3776
4368
  const listEl = wrapperEl == null ? void 0 : wrapperEl.querySelector(".files-list");
3777
- await uploadBatch(accepted, resourceIds, listEl != null ? listEl : null, state);
4369
+ const { failures } = await uploadBatch(
4370
+ accepted,
4371
+ resourceIds,
4372
+ listEl != null ? listEl : null,
4373
+ state
4374
+ );
4375
+ if (wrapperEl) {
4376
+ const combined = buildBatchErrorMessage(errorMessage, failures, state);
4377
+ if (combined) {
4378
+ showFileError(wrapperEl, combined);
4379
+ } else {
4380
+ clearFileError(wrapperEl);
4381
+ }
4382
+ }
3778
4383
  updateCallback();
3779
4384
  filesPicker.value = "";
3780
4385
  if (instance && pathKey && !state.config.readonly) {
@@ -3807,10 +4412,17 @@ function validatePickedResource(resource, allowedExtensions, allowedMimes, maxSi
3807
4412
  }
3808
4413
  if (!isMimeAllowed(resource.type, allowedMimes)) {
3809
4414
  const mimes = allowedMimes.join(", ");
3810
- return t("invalidFileMime", state, { name: resource.name, type: resource.type, mimes });
4415
+ return t("invalidFileMime", state, {
4416
+ name: resource.name,
4417
+ type: resource.type,
4418
+ mimes
4419
+ });
3811
4420
  }
3812
4421
  if (!isSizeWithinLimit(resource.size, maxSizeMB)) {
3813
- return t("fileTooLarge", state, { name: resource.name, maxSize: maxSizeMB });
4422
+ return t("fileTooLarge", state, {
4423
+ name: resource.name,
4424
+ maxSize: maxSizeMB
4425
+ });
3814
4426
  }
3815
4427
  return null;
3816
4428
  }
@@ -3871,7 +4483,13 @@ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resour
3871
4483
  return true;
3872
4484
  });
3873
4485
  const validItems = deduped.filter((r) => {
3874
- const err = validatePickedResource(r, allowedExtensions, allowedMimes, maxSizeMB, state);
4486
+ const err = validatePickedResource(
4487
+ r,
4488
+ allowedExtensions,
4489
+ allowedMimes,
4490
+ maxSizeMB,
4491
+ state
4492
+ );
3875
4493
  return err === null;
3876
4494
  });
3877
4495
  const freshRemaining = maxCount === Infinity ? validItems.length : Math.max(0, maxCount - resourceIds.length);
@@ -3916,14 +4534,22 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
3916
4534
  }
3917
4535
  if (picked.length === 0) return;
3918
4536
  const first = picked[0];
3919
- const validationError = validatePickedResource(first, allowedExtensions, allowedMimes, maxSizeMB, state);
4537
+ const validationError = validatePickedResource(
4538
+ first,
4539
+ allowedExtensions,
4540
+ allowedMimes,
4541
+ maxSizeMB,
4542
+ state
4543
+ );
3920
4544
  if (validationError !== null) {
3921
4545
  showFileError(container, validationError);
3922
4546
  return;
3923
4547
  }
3924
4548
  clearFileError(container);
3925
4549
  registerPickedResource(first, state);
3926
- let hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
4550
+ let hiddenInput = fileWrapper.querySelector(
4551
+ 'input[type="hidden"]'
4552
+ );
3927
4553
  if (!hiddenInput) {
3928
4554
  hiddenInput = document.createElement("input");
3929
4555
  hiddenInput.type = "hidden";
@@ -3992,21 +4618,21 @@ function buildWideTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragO
3992
4618
  };
3993
4619
  outer.appendChild(uploadBtn);
3994
4620
  if (hasLibrary && onLibraryClick) {
3995
- const divider = document.createElement("div");
3996
- divider.className = "fb-wide-tile-divider";
3997
- outer.appendChild(divider);
3998
4621
  const libBtn = document.createElement("button");
3999
4622
  libBtn.type = "button";
4000
4623
  libBtn.className = "fb-wide-tile-library fb-file-library-card";
4001
4624
  const libIcon = document.createElement("span");
4625
+ libIcon.className = "fb-wide-tile-library-icon";
4002
4626
  libIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
4003
4627
  libIcon.innerHTML = ICON_LIBRARY2;
4004
4628
  libBtn.appendChild(libIcon);
4005
4629
  const libLabel = document.createElement("div");
4630
+ libLabel.className = "fb-wide-tile-library-label";
4006
4631
  libLabel.style.cssText = "font-size:13px;font-weight:600;text-align:center;";
4007
4632
  libLabel.textContent = t("fromLibrary", state);
4008
4633
  libBtn.appendChild(libLabel);
4009
4634
  const libHint = document.createElement("div");
4635
+ libHint.className = "fb-wide-tile-library-hint";
4010
4636
  libHint.style.cssText = "font-size:11px;opacity:0.75;text-align:center;";
4011
4637
  libHint.textContent = t("libraryHint", state);
4012
4638
  libBtn.appendChild(libHint);
@@ -4087,7 +4713,8 @@ function renderSingleFileFilled(fileContainer, resourceId, state, deps, extras)
4087
4713
  grid.appendChild(tile);
4088
4714
  fileContainer.className = "file-preview-container";
4089
4715
  fileContainer.removeAttribute("style");
4090
- while (fileContainer.firstChild) fileContainer.removeChild(fileContainer.firstChild);
4716
+ while (fileContainer.firstChild)
4717
+ fileContainer.removeChild(fileContainer.firstChild);
4091
4718
  fileContainer.appendChild(outer);
4092
4719
  }
4093
4720
  function buildMultiAddTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragOver = false) {
@@ -4169,7 +4796,9 @@ function buildMetaLine(state, element, ridCount, maxCount, canClearAll, onClearA
4169
4796
  metaText.appendChild(sizeSpan);
4170
4797
  metaText.appendChild(buildMetaDot());
4171
4798
  }
4172
- const exts = getAllowedExtensions(element.accept);
4799
+ const exts = getAllowedExtensions(
4800
+ element.accept
4801
+ );
4173
4802
  if (exts.length > 0) {
4174
4803
  const fmtSpan = document.createElement("span");
4175
4804
  fmtSpan.className = "fb-meta-mono";
@@ -4420,7 +5049,9 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
4420
5049
  },
4421
5050
  onRemove() {
4422
5051
  var _a2;
4423
- const hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
5052
+ const hiddenInput = fileWrapper.querySelector(
5053
+ 'input[type="hidden"]'
5054
+ );
4424
5055
  const currentRid = hiddenInput == null ? void 0 : hiddenInput.value;
4425
5056
  if (currentRid) {
4426
5057
  releaseLocalFileUrl((_a2 = state.resourceIndex.get(currentRid)) == null ? void 0 : _a2.file);
@@ -4430,7 +5061,9 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
4430
5061
  }
4431
5062
  };
4432
5063
  const buildSingleExtras = () => {
4433
- const hasLibrary = Boolean(state.config.pickExistingFiles && !element.disableLibrary);
5064
+ const hasLibrary = Boolean(
5065
+ state.config.pickExistingFiles && !element.disableLibrary
5066
+ );
4434
5067
  return {
4435
5068
  replaceHandler: state.config.uploadFile ? () => picker.click() : null,
4436
5069
  libraryHandler: hasLibrary ? () => {
@@ -4442,7 +5075,13 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
4442
5075
  pathKey,
4443
5076
  pathKey,
4444
5077
  async (rid) => {
4445
- renderSingleFileFilled(fileContainer, rid, state, buildDeps(), buildSingleExtras());
5078
+ renderSingleFileFilled(
5079
+ fileContainer,
5080
+ rid,
5081
+ state,
5082
+ buildDeps(),
5083
+ buildSingleExtras()
5084
+ );
4446
5085
  },
4447
5086
  ctx.instance
4448
5087
  ).catch((err) => {
@@ -4458,14 +5097,21 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
4458
5097
  setupDrop: handlers.setupDrop,
4459
5098
  onRemove: handlers.onRemove,
4460
5099
  onAfterUpload: (container, rid) => {
4461
- renderSingleFileFilled(container, rid, state, buildDeps(), buildSingleExtras());
5100
+ renderSingleFileFilled(
5101
+ container,
5102
+ rid,
5103
+ state,
5104
+ buildDeps(),
5105
+ buildSingleExtras()
5106
+ );
4462
5107
  }
4463
5108
  });
4464
5109
  const renderEmptySingleState = () => {
4465
5110
  ensureFileStyles();
4466
5111
  fileContainer.className = "file-preview-container";
4467
5112
  fileContainer.removeAttribute("style");
4468
- while (fileContainer.firstChild) fileContainer.removeChild(fileContainer.firstChild);
5113
+ while (fileContainer.firstChild)
5114
+ fileContainer.removeChild(fileContainer.firstChild);
4469
5115
  const onLibraryClick = buildSingleExtras().libraryHandler;
4470
5116
  const wideTile = buildWideTile(
4471
5117
  state,
@@ -4610,7 +5256,13 @@ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
4610
5256
  }
4611
5257
  function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4612
5258
  var _a;
4613
- setupMultiFileEditMode(element, ctx, wrapper, pathKey, (_a = element.maxCount) != null ? _a : Infinity);
5259
+ setupMultiFileEditMode(
5260
+ element,
5261
+ ctx,
5262
+ wrapper,
5263
+ pathKey,
5264
+ (_a = element.maxCount) != null ? _a : Infinity
5265
+ );
4614
5266
  }
4615
5267
 
4616
5268
  // src/components/file/validate.ts
@@ -4793,7 +5445,11 @@ function renderMultiFileReadonly(rids, state, wrapper, pathKey, _marginTop) {
4793
5445
  const placeholder = placeholders[i];
4794
5446
  const meta = state.resourceIndex.get(resourceId);
4795
5447
  renderFilePreviewReadonly(resourceId, state, meta == null ? void 0 : meta.name).then((tile) => {
4796
- tile.classList.add("fb-readonly-tile", "fb-checker", "fb-tile-resource");
5448
+ tile.classList.add(
5449
+ "fb-readonly-tile",
5450
+ "fb-checker",
5451
+ "fb-tile-resource"
5452
+ );
4797
5453
  tile.dataset.resourceId = resourceId;
4798
5454
  placeholder.replaceWith(tile);
4799
5455
  }).catch(() => {
@@ -5144,51 +5800,25 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
5144
5800
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
5145
5801
  });
5146
5802
  }
5147
- let addRow = null;
5148
- let countDisplay = null;
5803
+ let addUpdate = null;
5149
5804
  if (!readonly) {
5150
- addRow = document.createElement("div");
5151
- addRow.className = "flex items-center gap-3 mt-2";
5152
- const addBtn = document.createElement("button");
5153
- addBtn.type = "button";
5154
- addBtn.className = "add-colour-btn px-3 py-1 rounded";
5155
- addBtn.style.cssText = `
5156
- color: var(--fb-primary-color);
5157
- border: var(--fb-border-width) solid var(--fb-primary-color);
5158
- background-color: transparent;
5159
- font-size: var(--fb-font-size);
5160
- transition: all var(--fb-transition-duration);
5161
- `;
5162
- addBtn.textContent = "+";
5163
- addBtn.addEventListener("mouseenter", () => {
5164
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
5165
- });
5166
- addBtn.addEventListener("mouseleave", () => {
5167
- addBtn.style.backgroundColor = "transparent";
5168
- });
5169
- addBtn.onclick = () => {
5170
- const defaultColour = element.default || "#000000";
5171
- values.push(defaultColour);
5172
- addColourItem(defaultColour);
5173
- updateAddButton();
5174
- updateRemoveButtons();
5175
- };
5176
- countDisplay = document.createElement("span");
5177
- countDisplay.className = "text-sm text-gray-500";
5178
- addRow.appendChild(addBtn);
5179
- addRow.appendChild(countDisplay);
5180
- wrapper.appendChild(addRow);
5805
+ const handle = createAddItemRow(
5806
+ "colour",
5807
+ () => {
5808
+ const defaultColour = element.default || "#000000";
5809
+ values.push(defaultColour);
5810
+ addColourItem(defaultColour);
5811
+ updateAddButton();
5812
+ updateRemoveButtons();
5813
+ },
5814
+ { label: element.addLabel }
5815
+ );
5816
+ addUpdate = handle.update;
5817
+ mountCounterInLabel(wrapper, handle.counter);
5818
+ wrapper.appendChild(handle.row);
5181
5819
  }
5182
5820
  function updateAddButton() {
5183
- if (!addRow || !countDisplay) return;
5184
- const addBtn = addRow.querySelector(".add-colour-btn");
5185
- if (addBtn) {
5186
- const disabled = values.length >= maxCount;
5187
- addBtn.disabled = disabled;
5188
- addBtn.style.opacity = disabled ? "0.5" : "1";
5189
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
5190
- }
5191
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
5821
+ if (addUpdate) addUpdate(values.length, maxCount);
5192
5822
  }
5193
5823
  values.forEach((value) => addColourItem(value));
5194
5824
  updateAddButton();
@@ -5265,7 +5895,7 @@ function validateColourElement(element, key, context) {
5265
5895
  };
5266
5896
  if (element.multiple) {
5267
5897
  const hexInputs = scopeRoot.querySelectorAll(
5268
- `[name^="${key}["].colour-hex-input`
5898
+ `[name^="${key}\\["].colour-hex-input`
5269
5899
  );
5270
5900
  const values = [];
5271
5901
  hexInputs.forEach((input, index) => {
@@ -5315,7 +5945,7 @@ function updateColourField(element, fieldPath, value, context) {
5315
5945
  return;
5316
5946
  }
5317
5947
  const hexInputs = scopeRoot.querySelectorAll(
5318
- `[name^="${fieldPath}["].colour-hex-input`
5948
+ `[name^="${fieldPath}\\["].colour-hex-input`
5319
5949
  );
5320
5950
  hexInputs.forEach((hexInput, index) => {
5321
5951
  if (index < value.length) {
@@ -5323,6 +5953,7 @@ function updateColourField(element, fieldPath, value, context) {
5323
5953
  hexInput.value = normalized;
5324
5954
  hexInput.classList.remove("invalid");
5325
5955
  hexInput.title = "";
5956
+ clearFieldError(hexInput);
5326
5957
  const wrapper = hexInput.closest(".colour-picker-wrapper");
5327
5958
  if (wrapper) {
5328
5959
  const swatch = wrapper.querySelector(".colour-swatch");
@@ -5352,6 +5983,7 @@ function updateColourField(element, fieldPath, value, context) {
5352
5983
  hexInput.value = normalized;
5353
5984
  hexInput.classList.remove("invalid");
5354
5985
  hexInput.title = "";
5986
+ clearFieldError(hexInput);
5355
5987
  const wrapper = hexInput.closest(".colour-picker-wrapper");
5356
5988
  if (wrapper) {
5357
5989
  const swatch = wrapper.querySelector(".colour-swatch");
@@ -5630,50 +6262,24 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
5630
6262
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
5631
6263
  });
5632
6264
  }
5633
- let addRow = null;
5634
- let countDisplay = null;
6265
+ let addUpdate = null;
5635
6266
  if (!readonly) {
5636
- addRow = document.createElement("div");
5637
- addRow.className = "flex items-center gap-3 mt-2";
5638
- const addBtn = document.createElement("button");
5639
- addBtn.type = "button";
5640
- addBtn.className = "add-slider-btn px-3 py-1 rounded";
5641
- addBtn.style.cssText = `
5642
- color: var(--fb-primary-color);
5643
- border: var(--fb-border-width) solid var(--fb-primary-color);
5644
- background-color: transparent;
5645
- font-size: var(--fb-font-size);
5646
- transition: all var(--fb-transition-duration);
5647
- `;
5648
- addBtn.textContent = "+";
5649
- addBtn.addEventListener("mouseenter", () => {
5650
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
5651
- });
5652
- addBtn.addEventListener("mouseleave", () => {
5653
- addBtn.style.backgroundColor = "transparent";
5654
- });
5655
- addBtn.onclick = () => {
5656
- values.push(defaultValue);
5657
- addSliderItem(defaultValue);
5658
- updateAddButton();
5659
- updateRemoveButtons();
5660
- };
5661
- countDisplay = document.createElement("span");
5662
- countDisplay.className = "text-sm text-gray-500";
5663
- addRow.appendChild(addBtn);
5664
- addRow.appendChild(countDisplay);
5665
- wrapper.appendChild(addRow);
6267
+ const handle = createAddItemRow(
6268
+ "slider",
6269
+ () => {
6270
+ values.push(defaultValue);
6271
+ addSliderItem(defaultValue);
6272
+ updateAddButton();
6273
+ updateRemoveButtons();
6274
+ },
6275
+ { label: element.addLabel }
6276
+ );
6277
+ addUpdate = handle.update;
6278
+ mountCounterInLabel(wrapper, handle.counter);
6279
+ wrapper.appendChild(handle.row);
5666
6280
  }
5667
6281
  function updateAddButton() {
5668
- if (!addRow || !countDisplay) return;
5669
- const addBtn = addRow.querySelector(".add-slider-btn");
5670
- if (addBtn) {
5671
- const disabled = values.length >= maxCount;
5672
- addBtn.disabled = disabled;
5673
- addBtn.style.opacity = disabled ? "0.5" : "1";
5674
- addBtn.style.pointerEvents = disabled ? "none" : "auto";
5675
- }
5676
- countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
6282
+ if (addUpdate) addUpdate(values.length, maxCount);
5677
6283
  }
5678
6284
  values.forEach((value) => addSliderItem(value));
5679
6285
  updateAddButton();
@@ -5785,7 +6391,7 @@ function validateSliderElement(element, key, context) {
5785
6391
  };
5786
6392
  if (element.multiple) {
5787
6393
  const sliders = scopeRoot.querySelectorAll(
5788
- `input[type="range"][name^="${key}["]`
6394
+ `input[type="range"][name^="${key}\\["]`
5789
6395
  );
5790
6396
  const values = [];
5791
6397
  sliders.forEach((slider, index) => {
@@ -5837,7 +6443,7 @@ function updateSliderField(element, fieldPath, value, context) {
5837
6443
  return;
5838
6444
  }
5839
6445
  const sliders = scopeRoot.querySelectorAll(
5840
- `input[type="range"][name^="${fieldPath}["]`
6446
+ `input[type="range"][name^="${fieldPath}\\["]`
5841
6447
  );
5842
6448
  sliders.forEach((slider, index) => {
5843
6449
  if (index < value.length && value[index] !== null) {
@@ -5865,6 +6471,7 @@ function updateSliderField(element, fieldPath, value, context) {
5865
6471
  }
5866
6472
  slider.classList.remove("invalid");
5867
6473
  slider.title = "";
6474
+ clearFieldError(slider);
5868
6475
  }
5869
6476
  });
5870
6477
  if (value.length !== sliders.length) {
@@ -5901,6 +6508,7 @@ function updateSliderField(element, fieldPath, value, context) {
5901
6508
  }
5902
6509
  slider.classList.remove("invalid");
5903
6510
  slider.title = "";
6511
+ clearFieldError(slider);
5904
6512
  }
5905
6513
  }
5906
6514
  }
@@ -6025,6 +6633,37 @@ function getChildWrapperClass(isSlides, columns) {
6025
6633
  const cols = columns || 1;
6026
6634
  return cols === 1 ? "space-y-2" : `grid grid-cols-${cols} gap-2`;
6027
6635
  }
6636
+ function mountRemoveButton(item, onRemove, state) {
6637
+ const rem = document.createElement("button");
6638
+ rem.type = "button";
6639
+ rem.className = "fb-item-remove";
6640
+ rem.setAttribute("aria-label", t("removeElement", state));
6641
+ rem.style.cssText = `
6642
+ width: 24px;
6643
+ height: 24px;
6644
+ display: inline-flex;
6645
+ align-items: center;
6646
+ justify-content: center;
6647
+ padding: 0;
6648
+ border: 0;
6649
+ border-radius: 4px;
6650
+ cursor: pointer;
6651
+ flex-shrink: 0;
6652
+ `;
6653
+ rem.innerHTML = BIN_ICON_SVG;
6654
+ rem.onclick = onRemove;
6655
+ const labelRow = item.querySelector("[data-fb-label-row]");
6656
+ if (labelRow) {
6657
+ rem.style.marginLeft = "auto";
6658
+ labelRow.appendChild(rem);
6659
+ return;
6660
+ }
6661
+ rem.style.position = "absolute";
6662
+ rem.style.top = "8px";
6663
+ rem.style.right = "8px";
6664
+ item.style.position = "relative";
6665
+ item.appendChild(rem);
6666
+ }
6028
6667
  function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6029
6668
  var _a, _b, _c, _d;
6030
6669
  const state = ctx.state;
@@ -6032,15 +6671,13 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6032
6671
  const childInheritedReadonly = containerIsReadonly || ctx.inheritedReadonly;
6033
6672
  const containerWrap = document.createElement("div");
6034
6673
  containerWrap.className = "border border-gray-200 rounded-lg p-2 bg-gray-50";
6035
- const countDisplay = document.createElement("span");
6036
- countDisplay.className = "text-sm text-gray-500";
6037
6674
  const itemsWrap = document.createElement("div");
6038
6675
  const isSlides = element.displayMode === "slides";
6039
6676
  if (isSlides) {
6040
6677
  itemsWrap.className = "fb-container-slides";
6041
6678
  const slideCols = element.columns;
6042
6679
  const gridTemplateColumns = typeof slideCols === "number" && slideCols > 0 ? `repeat(${slideCols}, 1fr)` : "repeat(auto-fit, minmax(280px, 1fr))";
6043
- itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:8px;align-items:start;`;
6680
+ itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:var(--fb-slides-gap, 14px);align-items:start;`;
6044
6681
  } else {
6045
6682
  itemsWrap.className = "space-y-2";
6046
6683
  }
@@ -6055,93 +6692,68 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6055
6692
  const pre = Array.isArray((_c = ctx.prefill) == null ? void 0 : _c[element.key]) ? ctx.prefill[element.key] : null;
6056
6693
  const childDefaults = extractChildDefaults(element.elements);
6057
6694
  const countItems = () => itemsWrap.querySelectorAll(":scope > .containerItem").length;
6058
- const createAddButton = () => {
6059
- const add = document.createElement("button");
6060
- add.type = "button";
6061
- add.className = "add-container-btn px-3 py-1 rounded";
6062
- add.style.cssText = `
6063
- color: var(--fb-primary-color);
6064
- border: var(--fb-border-width) solid var(--fb-primary-color);
6065
- background-color: transparent;
6066
- font-size: var(--fb-font-size);
6067
- transition: all var(--fb-transition-duration);
6068
- `;
6069
- add.textContent = "+";
6070
- add.addEventListener("mouseenter", () => {
6071
- add.style.backgroundColor = "var(--fb-background-hover-color)";
6072
- });
6073
- add.addEventListener("mouseleave", () => {
6074
- add.style.backgroundColor = "transparent";
6075
- });
6076
- add.onclick = () => {
6077
- if (countItems() < max) {
6078
- const idx = countItems();
6079
- const currentFormData = state.formRoot ? extractRootFormData(state.formRoot) : {};
6080
- const subCtx = {
6081
- state: ctx.state,
6082
- path: pathJoin(ctx.path, `${element.key}[${idx}]`),
6083
- prefill: childDefaults,
6084
- // Defaults for enableIf evaluation
6085
- formData: currentFormData,
6086
- // Current root data from DOM for enableIf
6087
- inheritedReadonly: childInheritedReadonly
6088
- };
6089
- const item = document.createElement("div");
6090
- item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6091
- item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6092
- const childWrapper = document.createElement("div");
6093
- childWrapper.className = getChildWrapperClass(isSlides, element.columns);
6094
- element.elements.forEach((child) => {
6095
- var _a2;
6096
- if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
6097
- childWrapper.appendChild(
6098
- createHiddenInput(
6099
- pathJoin(subCtx.path, child.key),
6100
- (_a2 = "default" in child ? child.default : null) != null ? _a2 : null
6101
- )
6102
- );
6103
- } else {
6104
- childWrapper.appendChild(renderElement(child, subCtx));
6105
- }
6106
- });
6107
- item.appendChild(childWrapper);
6108
- if (!containerIsReadonly) {
6109
- const rem = document.createElement("button");
6110
- rem.type = "button";
6111
- rem.className = "absolute top-2 right-2 px-2 py-1 rounded";
6112
- rem.style.cssText = `
6113
- color: var(--fb-error-color);
6114
- background-color: transparent;
6115
- transition: background-color var(--fb-transition-duration);
6116
- `;
6117
- rem.textContent = "\u2715";
6118
- rem.addEventListener("mouseenter", () => {
6119
- rem.style.backgroundColor = "var(--fb-background-hover-color)";
6120
- });
6121
- rem.addEventListener("mouseleave", () => {
6122
- rem.style.backgroundColor = "transparent";
6123
- });
6124
- rem.onclick = () => handleRemoveItem(item);
6125
- item.style.position = "relative";
6126
- item.appendChild(rem);
6127
- }
6128
- itemsWrap.appendChild(item);
6129
- updateAddButton();
6130
- }
6695
+ const handleAddItem = () => {
6696
+ if (countItems() >= max) return;
6697
+ const idx = countItems();
6698
+ const currentFormData = state.formRoot ? extractRootFormData(state.formRoot) : {};
6699
+ const subCtx = {
6700
+ state: ctx.state,
6701
+ path: pathJoin(ctx.path, `${element.key}[${idx}]`),
6702
+ prefill: childDefaults,
6703
+ // Defaults for enableIf evaluation
6704
+ formData: currentFormData,
6705
+ // Current root data from DOM for enableIf
6706
+ inheritedReadonly: childInheritedReadonly
6131
6707
  };
6132
- return add;
6708
+ const item = document.createElement("div");
6709
+ item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6710
+ item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6711
+ if (isSlides) {
6712
+ item.setAttribute("data-fb-slide-card", "");
6713
+ }
6714
+ const childWrapper = document.createElement("div");
6715
+ childWrapper.className = getChildWrapperClass(isSlides, element.columns);
6716
+ element.elements.forEach((child) => {
6717
+ var _a2;
6718
+ if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
6719
+ childWrapper.appendChild(
6720
+ createHiddenInput(
6721
+ pathJoin(subCtx.path, child.key),
6722
+ (_a2 = "default" in child ? child.default : null) != null ? _a2 : null
6723
+ )
6724
+ );
6725
+ } else {
6726
+ childWrapper.appendChild(renderElement(child, subCtx));
6727
+ }
6728
+ });
6729
+ item.appendChild(childWrapper);
6730
+ if (!containerIsReadonly) {
6731
+ mountRemoveButton(item, () => handleRemoveItem(item), state);
6732
+ }
6733
+ if (slideAddTile && slideAddTile.parentElement === itemsWrap) {
6734
+ itemsWrap.insertBefore(item, slideAddTile);
6735
+ } else {
6736
+ itemsWrap.appendChild(item);
6737
+ }
6738
+ updateAddButton();
6133
6739
  };
6134
- const updateAddButton = () => {
6135
- const currentCount = countItems();
6136
- const existingAddBtn = containerWrap.querySelector(
6137
- ".add-container-btn"
6740
+ let slideAddTile = null;
6741
+ let slideAddUpdate = null;
6742
+ let pillAddUpdate = null;
6743
+ const syncSlideTileSize = () => {
6744
+ if (!slideAddTile) return;
6745
+ const firstSlide = itemsWrap.querySelector(
6746
+ ":scope > .containerItem"
6138
6747
  );
6139
- if (existingAddBtn) {
6140
- existingAddBtn.disabled = currentCount >= max;
6141
- existingAddBtn.style.opacity = currentCount >= max ? "0.5" : "1";
6142
- existingAddBtn.style.pointerEvents = currentCount >= max ? "none" : "auto";
6748
+ if (firstSlide && firstSlide.offsetHeight > 0) {
6749
+ slideAddTile.style.minHeight = `${firstSlide.offsetHeight}px`;
6143
6750
  }
6144
- countDisplay.textContent = `${currentCount}/${max === Infinity ? "\u221E" : max}`;
6751
+ };
6752
+ const updateAddButton = () => {
6753
+ const currentCount = countItems();
6754
+ if (slideAddUpdate) slideAddUpdate(currentCount, max);
6755
+ if (pillAddUpdate) pillAddUpdate(currentCount, max);
6756
+ if (slideAddTile) syncSlideTileSize();
6145
6757
  };
6146
6758
  const handleRemoveItem = (item) => {
6147
6759
  item.remove();
@@ -6163,6 +6775,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6163
6775
  const item = document.createElement("div");
6164
6776
  item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6165
6777
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6778
+ if (isSlides) {
6779
+ item.setAttribute("data-fb-slide-card", "");
6780
+ }
6166
6781
  const childWrapper = document.createElement("div");
6167
6782
  if (isSlides) {
6168
6783
  childWrapper.className = "space-y-2";
@@ -6187,24 +6802,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6187
6802
  });
6188
6803
  item.appendChild(childWrapper);
6189
6804
  if (!containerIsReadonly) {
6190
- const rem = document.createElement("button");
6191
- rem.type = "button";
6192
- rem.className = "absolute top-2 right-2 px-2 py-1 rounded";
6193
- rem.style.cssText = `
6194
- color: var(--fb-error-color);
6195
- background-color: transparent;
6196
- transition: background-color var(--fb-transition-duration);
6197
- `;
6198
- rem.textContent = "\u2715";
6199
- rem.addEventListener("mouseenter", () => {
6200
- rem.style.backgroundColor = "var(--fb-background-hover-color)";
6201
- });
6202
- rem.addEventListener("mouseleave", () => {
6203
- rem.style.backgroundColor = "transparent";
6204
- });
6205
- rem.onclick = () => handleRemoveItem(item);
6206
- item.style.position = "relative";
6207
- item.appendChild(rem);
6805
+ mountRemoveButton(item, () => handleRemoveItem(item), ctx.state);
6208
6806
  }
6209
6807
  itemsWrap.appendChild(item);
6210
6808
  });
@@ -6224,6 +6822,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6224
6822
  const item = document.createElement("div");
6225
6823
  item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
6226
6824
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
6825
+ if (isSlides) {
6826
+ item.setAttribute("data-fb-slide-card", "");
6827
+ }
6227
6828
  const childWrapper = document.createElement("div");
6228
6829
  if (isSlides) {
6229
6830
  childWrapper.className = "space-y-2";
@@ -6249,41 +6850,47 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6249
6850
  }
6250
6851
  });
6251
6852
  item.appendChild(childWrapper);
6252
- const rem = document.createElement("button");
6253
- rem.type = "button";
6254
- rem.className = "absolute top-2 right-2 px-2 py-1 rounded";
6255
- rem.style.cssText = `
6256
- color: var(--fb-error-color);
6257
- background-color: transparent;
6258
- transition: background-color var(--fb-transition-duration);
6259
- `;
6260
- rem.textContent = "\u2715";
6261
- rem.addEventListener("mouseenter", () => {
6262
- rem.style.backgroundColor = "var(--fb-background-hover-color)";
6263
- });
6264
- rem.addEventListener("mouseleave", () => {
6265
- rem.style.backgroundColor = "transparent";
6266
- });
6267
- rem.onclick = () => {
6268
- if (countItems() > min) {
6269
- handleRemoveItem(item);
6270
- }
6271
- };
6272
- item.style.position = "relative";
6273
- item.appendChild(rem);
6853
+ mountRemoveButton(
6854
+ item,
6855
+ () => {
6856
+ if (countItems() > min) {
6857
+ handleRemoveItem(item);
6858
+ }
6859
+ },
6860
+ ctx.state
6861
+ );
6274
6862
  itemsWrap.appendChild(item);
6275
6863
  }
6276
6864
  }
6277
6865
  containerWrap.appendChild(itemsWrap);
6278
6866
  if (!containerIsReadonly) {
6279
- const addRow = document.createElement("div");
6280
- addRow.className = "flex items-center gap-3 mt-2";
6281
- addRow.appendChild(createAddButton());
6282
- addRow.appendChild(countDisplay);
6283
- containerWrap.appendChild(addRow);
6867
+ if (isSlides) {
6868
+ itemsWrap.style.alignItems = "stretch";
6869
+ const handle = createSlideAddTile(handleAddItem, {
6870
+ label: element.addLabel
6871
+ });
6872
+ slideAddTile = handle.tile;
6873
+ slideAddUpdate = handle.update;
6874
+ mountCounterInLabel(wrapper, handle.counter);
6875
+ itemsWrap.appendChild(handle.tile);
6876
+ } else {
6877
+ const handle = createAddItemRow("container", handleAddItem, {
6878
+ label: element.addLabel
6879
+ });
6880
+ pillAddUpdate = handle.update;
6881
+ mountCounterInLabel(wrapper, handle.counter);
6882
+ containerWrap.appendChild(handle.row);
6883
+ }
6284
6884
  }
6285
6885
  updateAddButton();
6286
6886
  wrapper.appendChild(containerWrap);
6887
+ if (slideAddTile) {
6888
+ if (typeof requestAnimationFrame === "function") {
6889
+ requestAnimationFrame(syncSlideTileSize);
6890
+ } else {
6891
+ syncSlideTileSize();
6892
+ }
6893
+ }
6287
6894
  }
6288
6895
  var validateElementFunc = null;
6289
6896
  function setValidateElement(fn) {
@@ -8944,7 +9551,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
8944
9551
  if (element.minLength != null || element.maxLength != null) {
8945
9552
  const counterRow = document.createElement("div");
8946
9553
  counterRow.style.cssText = "position: relative; padding: 2px 10px 4px; text-align: right;";
8947
- const counter = createCharCounter(element, textarea, false);
9554
+ const counter = createCharCounter(element, textarea);
8948
9555
  counter.style.cssText = `
8949
9556
  position: static;
8950
9557
  display: inline-block;
@@ -9251,10 +9858,7 @@ var TAGS = {
9251
9858
  "-": ["<hr />"]
9252
9859
  };
9253
9860
  function outdent(str) {
9254
- return str.replace(
9255
- RegExp("^" + (str.match(/^(\t| )+/) || "")[0], "gm"),
9256
- ""
9257
- );
9861
+ return str.replace(RegExp("^" + (str.match(/^(\t| )+/) || "")[0], "gm"), "");
9258
9862
  }
9259
9863
  function encodeAttr(str) {
9260
9864
  return (str + "").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -9417,12 +10021,7 @@ function ensureMarkdownStyles() {
9417
10021
  `;
9418
10022
  document.head.appendChild(style);
9419
10023
  }
9420
- var ANCHOR_DANGEROUS_SCHEMES = [
9421
- "javascript:",
9422
- "data:",
9423
- "vbscript:",
9424
- "blob:"
9425
- ];
10024
+ var ANCHOR_DANGEROUS_SCHEMES = ["javascript:", "data:", "vbscript:", "blob:"];
9426
10025
  var IMG_DANGEROUS_SCHEMES = ["javascript:", "vbscript:", "blob:"];
9427
10026
  function isImgSrcDangerous(normalized) {
9428
10027
  if (IMG_DANGEROUS_SCHEMES.some((scheme) => normalized.startsWith(scheme))) {
@@ -9484,6 +10083,118 @@ function validateMarkdown(_element, _key, _context) {
9484
10083
  function updateMarkdown(_element, _fieldPath, _value, _context) {
9485
10084
  }
9486
10085
 
10086
+ // src/components/registry.ts
10087
+ function validateHiddenElement(element, key, context) {
10088
+ var _a;
10089
+ const { scopeRoot } = context;
10090
+ const input = scopeRoot.querySelector(
10091
+ `input[type="hidden"][data-hidden-field="true"][name="${key}"]`
10092
+ );
10093
+ const raw = (_a = input == null ? void 0 : input.value) != null ? _a : "";
10094
+ if (raw === "") {
10095
+ const defaultVal = "default" in element ? element.default : null;
10096
+ return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
10097
+ }
10098
+ return { value: deserializeHiddenValue(raw), errors: [] };
10099
+ }
10100
+ function updateHiddenField(_element, fieldPath, value, context) {
10101
+ const { scopeRoot } = context;
10102
+ const input = scopeRoot.querySelector(
10103
+ `input[type="hidden"][data-hidden-field="true"][name="${fieldPath}"]`
10104
+ );
10105
+ if (!input) return;
10106
+ input.value = serializeHiddenValue(value);
10107
+ }
10108
+ var componentRegistry = {
10109
+ text: {
10110
+ validate: validateTextElement,
10111
+ update: updateTextField
10112
+ },
10113
+ textarea: {
10114
+ validate: validateTextareaElement,
10115
+ update: updateTextareaField
10116
+ },
10117
+ number: {
10118
+ validate: validateNumberElement,
10119
+ update: updateNumberField
10120
+ },
10121
+ select: {
10122
+ validate: validateSelectElement,
10123
+ update: updateSelectField
10124
+ },
10125
+ switcher: {
10126
+ validate: validateSwitcherElement,
10127
+ update: updateSwitcherField
10128
+ },
10129
+ boolean: {
10130
+ validate: validateBooleanElement,
10131
+ update: updateBooleanField,
10132
+ ownsLabel: true
10133
+ },
10134
+ file: {
10135
+ validate: validateFileElement,
10136
+ update: updateFileField
10137
+ },
10138
+ files: {
10139
+ // Legacy type - delegates to file
10140
+ validate: validateFileElement,
10141
+ update: updateFileField
10142
+ },
10143
+ colour: {
10144
+ validate: validateColourElement,
10145
+ update: updateColourField
10146
+ },
10147
+ slider: {
10148
+ validate: validateSliderElement,
10149
+ update: updateSliderField
10150
+ },
10151
+ container: {
10152
+ validate: validateContainerElement,
10153
+ update: updateContainerField
10154
+ },
10155
+ group: {
10156
+ // Deprecated type - delegates to container
10157
+ validate: validateGroupElement,
10158
+ update: updateGroupField
10159
+ },
10160
+ table: {
10161
+ validate: validateTableElement,
10162
+ update: updateTableField
10163
+ },
10164
+ richinput: {
10165
+ validate: validateRichInputElement,
10166
+ update: updateRichInputField
10167
+ },
10168
+ hidden: {
10169
+ // Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
10170
+ validate: validateHiddenElement,
10171
+ update: updateHiddenField
10172
+ },
10173
+ markdown: {
10174
+ // Display-only element — no value, no errors, skip from form data
10175
+ validate: validateMarkdown,
10176
+ update: updateMarkdown
10177
+ }
10178
+ };
10179
+ function getComponentOperations(elementType) {
10180
+ return componentRegistry[elementType] || null;
10181
+ }
10182
+ function validateElementWithComponent(element, key, context) {
10183
+ const ops = getComponentOperations(element.type);
10184
+ if (ops && ops.validate) {
10185
+ return ops.validate(element, key, context);
10186
+ }
10187
+ return null;
10188
+ }
10189
+ function updateElementWithComponent(element, fieldPath, value, context) {
10190
+ const ops = getComponentOperations(element.type);
10191
+ if (ops && ops.update) {
10192
+ ops.update(element, fieldPath, value, context);
10193
+ return true;
10194
+ }
10195
+ return false;
10196
+ }
10197
+
9487
10198
  // src/components/index.ts
9488
10199
  function showTooltip(tooltipId, button) {
9489
10200
  const tooltip = document.getElementById(tooltipId);
@@ -9749,6 +10460,7 @@ function createInfoButton(element) {
9749
10460
  function createLabelContainer(element) {
9750
10461
  const label = document.createElement("div");
9751
10462
  label.className = "flex items-center mb-1";
10463
+ label.dataset.fbLabelRow = "";
9752
10464
  const title = createFieldLabel(element);
9753
10465
  label.appendChild(title);
9754
10466
  if (element.description || element.hint) {
@@ -9795,6 +10507,9 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
9795
10507
  renderSwitcherElement(element, ctx, wrapper, pathKey);
9796
10508
  }
9797
10509
  break;
10510
+ case "boolean":
10511
+ renderBooleanElement(element, ctx, wrapper, pathKey);
10512
+ break;
9798
10513
  case "file":
9799
10514
  if (isMultiple) {
9800
10515
  renderMultipleFileElement(element, ctx, wrapper, pathKey);
@@ -9873,8 +10588,11 @@ function renderElement2(element, ctx) {
9873
10588
  const wrapper = document.createElement("div");
9874
10589
  wrapper.className = "mb-2 fb-field-wrapper";
9875
10590
  wrapper.setAttribute("data-field-key", element.key);
9876
- const label = createLabelContainer(element);
9877
- wrapper.appendChild(label);
10591
+ const ops = getComponentOperations(element.type);
10592
+ if (!(ops == null ? void 0 : ops.ownsLabel)) {
10593
+ const label = createLabelContainer(element);
10594
+ wrapper.appendChild(label);
10595
+ }
9878
10596
  const pathKey = pathJoin(ctx.path, element.key);
9879
10597
  dispatchToRenderer(element, ctx, wrapper, pathKey);
9880
10598
  if (initiallyDisabled) {
@@ -9966,6 +10684,7 @@ var defaultConfig = {
9966
10684
  invalidFileExtension: 'File "{name}" has unsupported format. Allowed: {formats}',
9967
10685
  invalidFileMime: 'File "{name}": file type {type} not allowed (allowed: {mimes})',
9968
10686
  fileTooLarge: 'File "{name}" exceeds maximum size of {maxSize}MB',
10687
+ uploadFailed: 'Failed to upload "{name}": {error}',
9969
10688
  filesLimitExceeded: "{skipped} file(s) skipped: maximum {max} files allowed",
9970
10689
  unsupportedFieldType: "Unsupported field type: {type}",
9971
10690
  invalidOption: "Invalid option",
@@ -10041,6 +10760,7 @@ var defaultConfig = {
10041
10760
  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}',
10042
10761
  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})',
10043
10762
  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',
10763
+ uploadFailed: '\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C "{name}": {error}',
10044
10764
  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",
10045
10765
  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}",
10046
10766
  invalidOption: "\u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435",
@@ -10105,28 +10825,52 @@ var defaultTheme = {
10105
10825
  // blue-500
10106
10826
  primaryHoverColor: "#2563eb",
10107
10827
  // blue-600
10828
+ primarySoftColor: "#dbeafe",
10829
+ // blue-100
10830
+ primarySoftHoverColor: "#bfdbfe",
10831
+ // blue-200
10108
10832
  errorColor: "#ef4444",
10109
10833
  // red-500
10110
10834
  errorHoverColor: "#dc2626",
10111
10835
  // red-600
10112
10836
  successColor: "#10b981",
10113
10837
  // green-500
10838
+ accentColor: "#f59e0b",
10839
+ // amber-500
10840
+ accentSoftColor: "#fef3c7",
10841
+ // amber-100
10842
+ accentBorderColor: "#fde68a",
10843
+ // amber-200
10844
+ accentTextColor: "#92400e",
10845
+ // amber-800
10114
10846
  borderColor: "#d1d5db",
10115
10847
  // gray-300
10116
10848
  borderHoverColor: "#9ca3af",
10117
10849
  // gray-400
10118
10850
  borderFocusColor: "#3b82f6",
10119
10851
  // blue-500
10852
+ borderStrongColor: "#9ca3af",
10853
+ // gray-400
10120
10854
  backgroundColor: "#ffffff",
10121
10855
  // white
10122
10856
  backgroundHoverColor: "#f9fafb",
10123
10857
  // gray-50
10124
10858
  backgroundReadonlyColor: "#f3f4f6",
10125
10859
  // gray-100
10860
+ pageBackgroundColor: "#f9fafb",
10861
+ // gray-50
10862
+ surfaceSoftColor: "#eff6ff",
10863
+ // blue-50
10864
+ surfaceTintColor: "#f8fafc",
10865
+ // slate-50
10126
10866
  textColor: "#1f2937",
10127
10867
  // gray-800
10128
10868
  textSecondaryColor: "#6b7280",
10129
10869
  // gray-500
10870
+ textMutedColor: "#9ca3af",
10871
+ // gray-400
10872
+ textFaintColor: "#cbd5e1",
10873
+ // slate-300
10130
10874
  textPlaceholderColor: "#9ca3af",
10131
10875
  // gray-400
10132
10876
  textDisabledColor: "#d1d5db",
@@ -10169,6 +10913,12 @@ var defaultTheme = {
10169
10913
  // 4px (compact density v2)
10170
10914
  borderRadius: "0.5rem",
10171
10915
  // rounded-lg (8px)
10916
+ borderRadiusSmall: "0.375rem",
10917
+ // 6px
10918
+ borderRadiusLarge: "0.75rem",
10919
+ // 12px
10920
+ borderRadiusXLarge: "1rem",
10921
+ // 16px
10172
10922
  borderWidth: "1px",
10173
10923
  // Typography
10174
10924
  fontSize: "0.875rem",
@@ -10180,13 +10930,31 @@ var defaultTheme = {
10180
10930
  fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
10181
10931
  fontWeightNormal: "400",
10182
10932
  fontWeightMedium: "500",
10933
+ lineHeight: "1.5",
10934
+ // Shadows
10935
+ shadowCard: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
10936
+ shadowPopover: "0 12px 32px -12px rgba(15,23,42,.18), 0 4px 12px -6px rgba(15,23,42,.08)",
10183
10937
  // Focus ring
10184
10938
  focusRingWidth: "2px",
10185
10939
  focusRingColor: "#3b82f6",
10186
10940
  // blue-500
10187
10941
  focusRingOpacity: "0.5",
10188
10942
  // Transitions
10189
- transitionDuration: "200ms"
10943
+ transitionDuration: "200ms",
10944
+ // Slide-card defaults — flat-white to match every other item card. The
10945
+ // Picaz theme overrides this with a gradient + shadow to lift the slides.
10946
+ slideCardBg: "#ffffff",
10947
+ slideCardShadow: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
10948
+ slideCardRadius: "0.5rem",
10949
+ // matches borderRadius
10950
+ slideCardMinHeight: "0",
10951
+ slideCardPadding: "12px",
10952
+ // Section-label defaults — same look as the regular field label. Themes
10953
+ // that want the "ПРЕИМУЩЕСТВА" caps style override these three vars.
10954
+ labelSectionFontSize: "0.875rem",
10955
+ // matches fontSize
10956
+ labelSectionLetterSpacing: "normal",
10957
+ labelSectionTextTransform: "none"
10190
10958
  };
10191
10959
  function generateCSSVariables(theme) {
10192
10960
  const mergedTheme = { ...defaultTheme, ...theme };
@@ -10198,6 +10966,7 @@ function generateCSSVariables(theme) {
10198
10966
  return cssVars.join("\n");
10199
10967
  }
10200
10968
  function injectThemeVariables(container, theme) {
10969
+ ensureThemingHooks(container.ownerDocument || document);
10201
10970
  const cssVariables = generateCSSVariables(theme);
10202
10971
  let styleTag = container.querySelector(
10203
10972
  "style[data-fb-theme]"
@@ -10260,138 +11029,50 @@ var exampleThemes = {
10260
11029
  fontSize: "16px",
10261
11030
  fontSizeSmall: "14px",
10262
11031
  fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif'
10263
- }
10264
- };
10265
-
10266
- // src/utils/styles.ts
10267
- function applyActionButtonStyles(button, isFormLevel = false) {
10268
- button.style.cssText = `
10269
- background-color: var(--fb-action-bg-color);
10270
- color: var(--fb-action-text-color);
10271
- border: var(--fb-border-width) solid var(--fb-action-border-color);
10272
- padding: ${isFormLevel ? "0.5rem 1rem" : "0.5rem 0.75rem"};
10273
- font-size: var(--fb-font-size);
10274
- font-weight: var(--fb-font-weight-medium);
10275
- border-radius: var(--fb-border-radius);
10276
- transition: all var(--fb-transition-duration);
10277
- box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
10278
- `;
10279
- button.addEventListener("mouseenter", () => {
10280
- button.style.backgroundColor = "var(--fb-action-hover-bg-color)";
10281
- button.style.borderColor = "var(--fb-action-hover-border-color)";
10282
- });
10283
- button.addEventListener("mouseleave", () => {
10284
- button.style.backgroundColor = "var(--fb-action-bg-color)";
10285
- button.style.borderColor = "var(--fb-action-border-color)";
10286
- });
10287
- }
10288
-
10289
- // src/components/registry.ts
10290
- function validateHiddenElement(element, key, context) {
10291
- var _a;
10292
- const { scopeRoot } = context;
10293
- const input = scopeRoot.querySelector(
10294
- `input[type="hidden"][data-hidden-field="true"][name="${key}"]`
10295
- );
10296
- const raw = (_a = input == null ? void 0 : input.value) != null ? _a : "";
10297
- if (raw === "") {
10298
- const defaultVal = "default" in element ? element.default : null;
10299
- return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
10300
- }
10301
- return { value: deserializeHiddenValue(raw), errors: [] };
10302
- }
10303
- function updateHiddenField(_element, fieldPath, value, context) {
10304
- const { scopeRoot } = context;
10305
- const input = scopeRoot.querySelector(
10306
- `input[type="hidden"][data-hidden-field="true"][name="${fieldPath}"]`
10307
- );
10308
- if (!input) return;
10309
- input.value = serializeHiddenValue(value);
10310
- }
10311
- var componentRegistry = {
10312
- text: {
10313
- validate: validateTextElement,
10314
- update: updateTextField
10315
- },
10316
- textarea: {
10317
- validate: validateTextareaElement,
10318
- update: updateTextareaField
10319
- },
10320
- number: {
10321
- validate: validateNumberElement,
10322
- update: updateNumberField
10323
- },
10324
- select: {
10325
- validate: validateSelectElement,
10326
- update: updateSelectField
10327
- },
10328
- switcher: {
10329
- validate: validateSwitcherElement,
10330
- update: updateSwitcherField
10331
- },
10332
- file: {
10333
- validate: validateFileElement,
10334
- update: updateFileField
10335
- },
10336
- files: {
10337
- // Legacy type - delegates to file
10338
- validate: validateFileElement,
10339
- update: updateFileField
10340
- },
10341
- colour: {
10342
- validate: validateColourElement,
10343
- update: updateColourField
10344
- },
10345
- slider: {
10346
- validate: validateSliderElement,
10347
- update: updateSliderField
10348
- },
10349
- container: {
10350
- validate: validateContainerElement,
10351
- update: updateContainerField
10352
- },
10353
- group: {
10354
- // Deprecated type - delegates to container
10355
- validate: validateGroupElement,
10356
- update: updateGroupField
10357
- },
10358
- table: {
10359
- validate: validateTableElement,
10360
- update: updateTableField
10361
- },
10362
- richinput: {
10363
- validate: validateRichInputElement,
10364
- update: updateRichInputField
10365
- },
10366
- hidden: {
10367
- // Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
10368
- validate: validateHiddenElement,
10369
- update: updateHiddenField
10370
11032
  },
10371
- markdown: {
10372
- // Display-only element no value, no errors, skip from form data
10373
- validate: validateMarkdown,
10374
- update: updateMarkdown
11033
+ // Picaz wizard design tokens — derived from the Picaz Wizard mockups.
11034
+ // Pairs with the host-side .card / .section-num / .lede chrome that wraps the form.
11035
+ picaz: {
11036
+ ...defaultTheme,
11037
+ primaryColor: "#2f5bea",
11038
+ primaryHoverColor: "#2349c8",
11039
+ primarySoftColor: "#eaf0ff",
11040
+ primarySoftHoverColor: "#d6e0ff",
11041
+ errorColor: "#ef4444",
11042
+ successColor: "#16a34a",
11043
+ accentColor: "#ffb020",
11044
+ accentSoftColor: "#fff7e6",
11045
+ accentBorderColor: "#fde7b5",
11046
+ accentTextColor: "#92400e",
11047
+ borderColor: "#e3e8f0",
11048
+ borderHoverColor: "#cdd6e3",
11049
+ borderFocusColor: "#2f5bea",
11050
+ borderStrongColor: "#cdd6e3",
11051
+ backgroundColor: "#ffffff",
11052
+ backgroundHoverColor: "#f3f7ff",
11053
+ pageBackgroundColor: "#f6f8fb",
11054
+ surfaceSoftColor: "#eef4ff",
11055
+ surfaceTintColor: "#f3f7ff",
11056
+ textColor: "#0f172a",
11057
+ textSecondaryColor: "#334155",
11058
+ textMutedColor: "#64748b",
11059
+ textFaintColor: "#94a3b8",
11060
+ textPlaceholderColor: "#94a3b8",
11061
+ buttonBgColor: "#2f5bea",
11062
+ buttonHoverBgColor: "#2349c8",
11063
+ fileUploadBgColor: "#fafcff",
11064
+ fileUploadBorderColor: "#cdd6e3",
11065
+ fileUploadHoverBorderColor: "#2f5bea",
11066
+ borderRadius: "12px",
11067
+ borderRadiusSmall: "8px",
11068
+ borderRadiusLarge: "16px",
11069
+ borderRadiusXLarge: "22px",
11070
+ fontFamily: '"Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
11071
+ shadowCard: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
11072
+ shadowPopover: "0 12px 32px -12px rgba(15,23,42,.18), 0 4px 12px -6px rgba(15,23,42,.08)",
11073
+ focusRingColor: "#2f5bea"
10375
11074
  }
10376
11075
  };
10377
- function getComponentOperations(elementType) {
10378
- return componentRegistry[elementType] || null;
10379
- }
10380
- function validateElementWithComponent(element, key, context) {
10381
- const ops = getComponentOperations(element.type);
10382
- if (ops && ops.validate) {
10383
- return ops.validate(element, key, context);
10384
- }
10385
- return null;
10386
- }
10387
- function updateElementWithComponent(element, fieldPath, value, context) {
10388
- const ops = getComponentOperations(element.type);
10389
- if (ops && ops.update) {
10390
- ops.update(element, fieldPath, value, context);
10391
- return true;
10392
- }
10393
- return false;
10394
- }
10395
11076
 
10396
11077
  // src/instance/FormBuilderInstance.ts
10397
11078
  var FormBuilderInstance = class {
@@ -10516,26 +11197,37 @@ var FormBuilderInstance = class {
10516
11197
  }
10517
11198
  }
10518
11199
  /**
10519
- * Find the DOM element corresponding to a field path (instance-scoped)
11200
+ * Find the DOM element corresponding to a field path (instance-scoped).
11201
+ *
11202
+ * Strategy:
11203
+ * 1. Try a `[name="…"]` lookup first — works for any field that renders
11204
+ * an input/hidden with the path as its name, in either mode. Some
11205
+ * readonly renderers still emit a hidden input (boolean, switcher),
11206
+ * so this path must run regardless of `state.config.readonly`. A
11207
+ * prior version gated this on edit mode only, which made
11208
+ * `updateField` / `setFormData` silently miss readonly boolean
11209
+ * fields whose component also opts out of the standard label row
11210
+ * (`ownsLabel: true`).
11211
+ * 2. If no input matched, fall back to locating the field wrapper by
11212
+ * its visible label text — needed for readonly previews that don't
11213
+ * emit any `name=` attribute (e.g. file/markdown previews).
10520
11214
  */
10521
11215
  findFormElementByFieldPath(fieldPath) {
10522
11216
  if (!this.state.formRoot) return null;
10523
- if (!this.state.config.readonly) {
10524
- let element = this.state.formRoot.querySelector(
10525
- `[name="${fieldPath}"]`
11217
+ let element = this.state.formRoot.querySelector(
11218
+ `[name="${fieldPath}"]`
11219
+ );
11220
+ if (element) return element;
11221
+ const variations = [
11222
+ fieldPath,
11223
+ fieldPath.replace(/\[(\d+)\]/g, "[$1]"),
11224
+ fieldPath.replace(/\./g, "[") + "]".repeat((fieldPath.match(/\./g) || []).length)
11225
+ ];
11226
+ for (const variation of variations) {
11227
+ element = this.state.formRoot.querySelector(
11228
+ `[name="${variation}"]`
10526
11229
  );
10527
11230
  if (element) return element;
10528
- const variations = [
10529
- fieldPath,
10530
- fieldPath.replace(/\[(\d+)\]/g, "[$1]"),
10531
- fieldPath.replace(/\./g, "[") + "]".repeat((fieldPath.match(/\./g) || []).length)
10532
- ];
10533
- for (const variation of variations) {
10534
- element = this.state.formRoot.querySelector(
10535
- `[name="${variation}"]`
10536
- );
10537
- if (element) return element;
10538
- }
10539
11231
  }
10540
11232
  const schemaElement = this.findSchemaElement(fieldPath);
10541
11233
  if (!schemaElement) return null;