@dmitryvim/form-builder 0.3.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -419,6 +419,20 @@ function ensureThemingHooks(doc) {
419
419
  min-height: var(--fb-slide-card-min-height);
420
420
  padding: var(--fb-slide-card-padding);
421
421
  }
422
+ /* Slides-mode outer grid. Responsive by design \u2014 column count derives from
423
+ container width and --fb-slide-min-width, not from a schema "columns" prop.
424
+ min(100%, ...) prevents the card from overflowing a narrow container.
425
+ Override per-form: set --fb-slide-min-width / --fb-slides-gap on any
426
+ ancestor (theme, host CSS, or the form root). */
427
+ .fb-container-slides {
428
+ display: grid;
429
+ gap: var(--fb-slides-gap, 14px);
430
+ align-items: stretch;
431
+ grid-template-columns: repeat(
432
+ auto-fit,
433
+ minmax(min(100%, var(--fb-slide-min-width, 280px)), 1fr)
434
+ );
435
+ }
422
436
  [data-fb-label-row] > label {
423
437
  font-size: var(--fb-label-section-font-size);
424
438
  letter-spacing: var(--fb-label-section-letter-spacing);
@@ -465,20 +479,60 @@ function ensureThemingHooks(doc) {
465
479
  color: #ffffff;
466
480
  border-color: var(--fb-primary-color);
467
481
  }
482
+ /* \u2500\u2500\u2500 Row layout: flex-wrap container with per-child width tokens \u2500\u2500\u2500
483
+ Replaces the older Tailwind \`space-y-2\` class. Children without
484
+ data-fb-width (or width="full") take 100% basis \u2192 forced wrap \u2192 behaves
485
+ like a vertical stack. Mixed widths sit side-by-side in the same row. */
486
+ .fb-row {
487
+ display: flex;
488
+ flex-wrap: wrap;
489
+ gap: var(--fb-row-gap, 0.25rem);
490
+ align-items: flex-start;
491
+ }
492
+ .fb-row > [data-fb-width="full"],
493
+ .fb-row > :not([data-fb-width]) { flex: 0 0 100%; min-width: 0; }
494
+ .fb-row > [data-fb-width="1/2"] { flex: 0 0 calc(50% - var(--fb-row-gap, 0.25rem) / 2); min-width: 0; }
495
+ .fb-row > [data-fb-width="1/3"] { flex: 0 0 calc(33.333% - var(--fb-row-gap, 0.25rem) * 2 / 3); min-width: 0; }
496
+ .fb-row > [data-fb-width="2/3"] { flex: 0 0 calc(66.666% - var(--fb-row-gap, 0.25rem) / 3); min-width: 0; }
497
+ /* \u2500\u2500\u2500 Density tokens: re-bind --fb-input-padding-* and --fb-font-size at
498
+ the wrapper level so every component that reads those vars (text, textarea,
499
+ select, number, colour, switcher, file) picks the size up via cascade. md
500
+ uses the defaults set in theme.ts (2px / 8px / 14px). The scale primarily
501
+ varies font-size; vertical padding stays close to md so an "lg" heading
502
+ field does not visually break the form's row rhythm \u2014 only the typography
503
+ announces "this is bigger". Picaz theme overrides padding directly for
504
+ its own density, independent of this scale. */
505
+ /* size-token side-effects on file tiles: --fb-file-wide-min controls the
506
+ single-file empty dropzone height, --fb-file-zone-min controls the inner
507
+ upload/library zones. Both read in src/components/file/styles.ts. */
508
+ .fb-size-xs { --fb-input-padding-y: 1px; --fb-input-padding-x: 6px; --fb-font-size: 12px; --fb-font-size-small: 10px; --fb-line-height: 1.25; --fb-file-wide-min: 64px; --fb-file-zone-min: 48px; --fb-file-zone-padding: 6px; }
509
+ .fb-size-sm { --fb-input-padding-y: 2px; --fb-input-padding-x: 6px; --fb-font-size: 13px; --fb-font-size-small: 11px; --fb-line-height: 1.35; --fb-file-wide-min: 80px; --fb-file-zone-min: 60px; --fb-file-zone-padding: 7px; }
510
+ /* .fb-size-md uses defaults \u2014 no override needed */
511
+ .fb-size-lg { --fb-input-padding-y: 2px; --fb-input-padding-x: 8px; --fb-font-size: 16px; --fb-font-size-small: 13px; --fb-file-wide-min: 128px; --fb-file-zone-min: 96px; --fb-file-zone-padding: 12px; }
512
+ .fb-size-xl { --fb-input-padding-y: 3px; --fb-input-padding-x: 8px; --fb-font-size: 18px; --fb-font-size-small: 14px; --fb-file-wide-min: 160px; --fb-file-zone-min: 120px; --fb-file-zone-padding: 14px; }
468
513
  `;
469
514
  doc.head.appendChild(style);
470
515
  }
471
- function applyAutoExpand(textarea) {
516
+ function applyAutoExpand(textarea, options = {}) {
472
517
  textarea.style.overflow = "hidden";
473
518
  textarea.style.resize = "none";
519
+ const minRows = Math.max(1, options.minRows ?? 1);
474
520
  const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
475
- textarea.rows = Math.max(1, lineCount);
521
+ textarea.rows = Math.max(minRows, lineCount);
476
522
  const resize = () => {
477
523
  if (!textarea.isConnected) return;
478
- textarea.style.height = "0";
524
+ textarea.style.height = "";
479
525
  const cs = getComputedStyle(textarea);
480
526
  const borderY = parseFloat(cs.borderTopWidth || "0") + parseFloat(cs.borderBottomWidth || "0");
481
- textarea.style.height = `${textarea.scrollHeight + borderY}px`;
527
+ const lineHeight = parseFloat(cs.lineHeight || "0") || parseFloat(cs.fontSize || "0") * 1.5 || 20;
528
+ const padY = parseFloat(cs.paddingTop || "0") + parseFloat(cs.paddingBottom || "0");
529
+ const minHeight = lineHeight * minRows + padY + borderY;
530
+ if (textarea.value === "") {
531
+ textarea.style.height = `${minHeight}px`;
532
+ return;
533
+ }
534
+ const contentHeight = textarea.scrollHeight + borderY;
535
+ textarea.style.height = `${Math.max(contentHeight, minHeight)}px`;
482
536
  };
483
537
  textarea.addEventListener("input", resize);
484
538
  setTimeout(() => {
@@ -530,8 +584,8 @@ function createAddItemRow(classNameSuffix, onClick, options = {}) {
530
584
  const label = options.label ?? "";
531
585
  const showCounter = options.showCounter !== false;
532
586
  const row = document.createElement("div");
533
- row.className = "fb-add-row mt-2";
534
- row.style.cssText = "display:flex;align-items:stretch;width:100%;";
587
+ row.className = "fb-add-row";
588
+ row.style.cssText = "display:flex;align-items:stretch;width:100%;margin-top:4px;";
535
589
  const button = document.createElement("button");
536
590
  button.type = "button";
537
591
  button.className = `add-${classNameSuffix}-btn`;
@@ -540,8 +594,8 @@ function createAddItemRow(classNameSuffix, onClick, options = {}) {
540
594
  display: inline-flex;
541
595
  align-items: center;
542
596
  justify-content: center;
543
- gap: 6px;
544
- padding: 6px 10px;
597
+ gap: 4px;
598
+ padding: 3px 10px;
545
599
  border: 1px dashed var(--fb-primary-color);
546
600
  border-radius: var(--fb-border-radius);
547
601
  background: transparent;
@@ -686,12 +740,12 @@ function ensureChipStyles(doc) {
686
740
  const style = doc.createElement("style");
687
741
  style.setAttribute("data-fb-chip-styles", "");
688
742
  style.textContent = `
689
- .fb-chip-list { display: flex; flex-direction: column; gap: 4px; }
743
+ .fb-chip-list { display: flex; flex-direction: column; gap: 2px; }
690
744
  .fb-chip {
691
745
  display: flex;
692
746
  align-items: center;
693
- gap: 8px;
694
- padding: 5px 6px 5px 10px;
747
+ gap: 6px;
748
+ padding: 3px 6px 3px 8px;
695
749
  background: var(--fb-chip-bg, var(--fb-background-color, #fff));
696
750
  border: 1px solid var(--fb-chip-border, var(--fb-border-color, #e2e8f0));
697
751
  border-radius: 6px;
@@ -717,7 +771,13 @@ function ensureChipStyles(doc) {
717
771
  color: var(--fb-chip-text, var(--fb-text-color, inherit));
718
772
  font-size: var(--fb-font-size, 14px);
719
773
  font-family: var(--fb-font-family, inherit);
720
- line-height: 1.4;
774
+ line-height: var(--fb-line-height, 1.4);
775
+ /* Soft-wrap support (v0.4.0): chip-input is now a <textarea rows=1> that
776
+ grows vertically when content overflows the chip width. */
777
+ resize: none;
778
+ overflow: hidden;
779
+ word-break: break-word;
780
+ overflow-wrap: anywhere;
721
781
  }
722
782
  .fb-chip-input::placeholder { color: var(--fb-text-placeholder-color, #94a3b8); }
723
783
  .fb-chip-input:read-only { color: var(--fb-text-secondary-color, #475569); }
@@ -802,7 +862,7 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
802
862
  overflow-wrap: anywhere;
803
863
  `;
804
864
  textInput.name = pathKey;
805
- textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
865
+ textInput.placeholder = element.placeholder ?? "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
806
866
  textInput.value = ctx.prefill[element.key] || element.default || "";
807
867
  textInput.readOnly = readonly;
808
868
  applySingleLineMode(textInput);
@@ -875,8 +935,8 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
875
935
  dot.className = "fb-chip-dot";
876
936
  dot.setAttribute("aria-hidden", "true");
877
937
  chip.appendChild(dot);
878
- const input = document.createElement("input");
879
- input.type = "text";
938
+ const input = document.createElement("textarea");
939
+ input.rows = 1;
880
940
  input.className = "fb-chip-input";
881
941
  input.value = value;
882
942
  input.placeholder = element.placeholder || t("placeholderText", state);
@@ -892,6 +952,8 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
892
952
  input.addEventListener("blur", handleChange);
893
953
  input.addEventListener("input", handleChange);
894
954
  }
955
+ applySingleLineMode(input);
956
+ applyAutoExpand(input);
895
957
  if (!readonly) {
896
958
  const rem = document.createElement("button");
897
959
  rem.type = "button";
@@ -1110,13 +1172,13 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
1110
1172
  const textareaInput = document.createElement("textarea");
1111
1173
  textareaInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
1112
1174
  textareaInput.style.cssText = `
1113
- padding: var(--fb-input-padding-y) var(--fb-input-padding-x) 24px var(--fb-input-padding-x);
1175
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
1114
1176
  font-size: var(--fb-font-size);
1115
1177
  font-family: var(--fb-font-family);
1178
+ line-height: var(--fb-line-height, 1.5);
1116
1179
  `;
1117
1180
  textareaInput.name = pathKey;
1118
- textareaInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
1119
- textareaInput.rows = element.rows || 4;
1181
+ textareaInput.placeholder = element.placeholder ?? "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
1120
1182
  textareaInput.value = ctx.prefill[element.key] || element.default || "";
1121
1183
  textareaInput.readOnly = readonly;
1122
1184
  if (!readonly && ctx.instance) {
@@ -1127,9 +1189,7 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
1127
1189
  textareaInput.addEventListener("blur", handleChange);
1128
1190
  textareaInput.addEventListener("input", handleChange);
1129
1191
  }
1130
- if (element.autoExpand || readonly) {
1131
- applyAutoExpand(textareaInput);
1132
- }
1192
+ applyAutoExpand(textareaInput, { minRows: element.rows ?? 1 });
1133
1193
  textareaWrapper.appendChild(textareaInput);
1134
1194
  if (!readonly && (element.minLength != null || element.maxLength != null)) {
1135
1195
  const counter = createCharCounter(element, textareaInput);
@@ -1148,7 +1208,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1148
1208
  values.push(element.default || "");
1149
1209
  }
1150
1210
  const container = document.createElement("div");
1151
- container.className = "space-y-2";
1211
+ container.className = "fb-row";
1152
1212
  wrapper.appendChild(container);
1153
1213
  function updateIndices() {
1154
1214
  const items = container.querySelectorAll(".multiple-textarea-item");
@@ -1167,12 +1227,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1167
1227
  const textareaInput = document.createElement("textarea");
1168
1228
  textareaInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
1169
1229
  textareaInput.style.cssText = `
1170
- padding: var(--fb-input-padding-y) var(--fb-input-padding-x) 24px var(--fb-input-padding-x);
1230
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
1171
1231
  font-size: var(--fb-font-size);
1172
1232
  font-family: var(--fb-font-family);
1233
+ line-height: var(--fb-line-height, 1.5);
1173
1234
  `;
1174
1235
  textareaInput.placeholder = element.placeholder || t("placeholderText", state);
1175
- textareaInput.rows = element.rows || 4;
1176
1236
  textareaInput.value = value;
1177
1237
  textareaInput.readOnly = readonly;
1178
1238
  if (!readonly && ctx.instance) {
@@ -1183,9 +1243,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1183
1243
  textareaInput.addEventListener("blur", handleChange);
1184
1244
  textareaInput.addEventListener("input", handleChange);
1185
1245
  }
1186
- if (element.autoExpand || readonly) {
1187
- applyAutoExpand(textareaInput);
1188
- }
1246
+ applyAutoExpand(textareaInput, { minRows: element.rows ?? 1 });
1189
1247
  textareaContainer.appendChild(textareaInput);
1190
1248
  if (!readonly && (element.minLength != null || element.maxLength != null)) {
1191
1249
  const counter = createCharCounter(element, textareaInput);
@@ -1261,9 +1319,7 @@ function validateTextareaElement(element, key, context) {
1261
1319
  }
1262
1320
  function updateTextareaField(element, fieldPath, value, context) {
1263
1321
  updateTextField(element, fieldPath, value, context);
1264
- const { scopeRoot, state } = context;
1265
- const shouldAutoExpand = element.autoExpand || isElementReadonly(element, state);
1266
- if (!shouldAutoExpand) return;
1322
+ const { scopeRoot } = context;
1267
1323
  if (element.multiple) {
1268
1324
  const textareas = scopeRoot.querySelectorAll(
1269
1325
  `textarea[name^="${fieldPath}["]`
@@ -1458,7 +1514,7 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1458
1514
  values.push(element.default || "");
1459
1515
  }
1460
1516
  const container = document.createElement("div");
1461
- container.className = "space-y-2";
1517
+ container.className = "fb-row";
1462
1518
  wrapper.appendChild(container);
1463
1519
  function updateIndices() {
1464
1520
  const items = container.querySelectorAll(".multiple-number-item");
@@ -1772,7 +1828,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1772
1828
  values.push(element.default || element.options?.[0]?.value || "");
1773
1829
  }
1774
1830
  const container = document.createElement("div");
1775
- container.className = "space-y-2";
1831
+ container.className = "fb-row";
1776
1832
  wrapper.appendChild(container);
1777
1833
  function updateIndices() {
1778
1834
  const items = container.querySelectorAll(".multiple-select-item");
@@ -2202,7 +2258,7 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
2202
2258
  }
2203
2259
  const readonly = isElementReadonly(element, state, ctx);
2204
2260
  const container = document.createElement("div");
2205
- container.className = "space-y-2";
2261
+ container.className = "fb-row";
2206
2262
  wrapper.appendChild(container);
2207
2263
  function updateIndices() {
2208
2264
  const items = container.querySelectorAll(".multiple-switcher-item");
@@ -2774,7 +2830,9 @@ function ensureFileStyles() {
2774
2830
  align-items: stretch;
2775
2831
  gap: 0;
2776
2832
  overflow: hidden;
2777
- min-height: 180px;
2833
+ /* min-height driven by --fb-file-wide-min so size-tokens (.fb-size-{xs|sm|lg|xl})
2834
+ can override per-field. Default 96px matches v0.4.0 compact density. */
2835
+ min-height: var(--fb-file-wide-min, 96px);
2778
2836
  transition: border-color 150ms, background 150ms, box-shadow 150ms;
2779
2837
  cursor: pointer;
2780
2838
  }
@@ -2793,14 +2851,15 @@ function ensureFileStyles() {
2793
2851
  upload + library on one row (~220 + 176), library wraps below. */
2794
2852
  .fb-wide-tile-upload {
2795
2853
  flex: 1 1 220px;
2796
- min-height: 140px;
2854
+ /* min-height theme-driven; default 72px matches v0.4.0 compact density. */
2855
+ min-height: var(--fb-file-zone-min, 72px);
2797
2856
  display: flex;
2798
2857
  flex-direction: column;
2799
2858
  align-items: center;
2800
2859
  justify-content: center;
2801
- gap: 8px;
2860
+ gap: 4px;
2802
2861
  color: #2563eb;
2803
- padding: 16px;
2862
+ padding: var(--fb-file-zone-padding, 8px);
2804
2863
  transition: background 150ms;
2805
2864
  cursor: pointer;
2806
2865
  background: transparent;
@@ -2820,14 +2879,16 @@ function ensureFileStyles() {
2820
2879
  visual hierarchy "upload > library" in both layouts. */
2821
2880
  .fb-wide-tile-library {
2822
2881
  flex: 0 0 176px;
2823
- min-height: 120px;
2882
+ /* Shares --fb-file-zone-min with the upload zone so size-tokens scale both
2883
+ uniformly. Default 72px matches v0.4.0 compact density. */
2884
+ min-height: var(--fb-file-zone-min, 72px);
2824
2885
  display: flex;
2825
2886
  flex-direction: column;
2826
2887
  align-items: center;
2827
2888
  justify-content: center;
2828
- gap: 8px;
2889
+ gap: 4px;
2829
2890
  color: #2563eb;
2830
- padding: 12px;
2891
+ padding: var(--fb-file-zone-padding, 8px);
2831
2892
  transition: background 150ms;
2832
2893
  cursor: pointer;
2833
2894
  background: transparent;
@@ -4088,7 +4149,9 @@ function downloadBlob(blob, fileName) {
4088
4149
  URL.revokeObjectURL(blobUrl);
4089
4150
  }, 100);
4090
4151
  } catch (error) {
4091
- throw new Error(`Blob download failed: ${error.message}`);
4152
+ throw new Error(`Blob download failed: ${error.message}`, {
4153
+ cause: error
4154
+ });
4092
4155
  }
4093
4156
  }
4094
4157
 
@@ -5028,7 +5091,7 @@ function renderResourcePills(opts) {
5028
5091
  function renderFileElementEdit(element, ctx, wrapper, pathKey) {
5029
5092
  const state = ctx.state;
5030
5093
  const fileWrapper = document.createElement("div");
5031
- fileWrapper.className = "space-y-2";
5094
+ fileWrapper.className = "fb-row";
5032
5095
  fileWrapper.dataset.filesWrapper = pathKey;
5033
5096
  const picker = document.createElement("input");
5034
5097
  picker.type = "file";
@@ -5176,7 +5239,7 @@ function buildAcceptAttribute(accept) {
5176
5239
  function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
5177
5240
  const state = ctx.state;
5178
5241
  const filesWrapper = document.createElement("div");
5179
- filesWrapper.className = "space-y-2";
5242
+ filesWrapper.className = "fb-row";
5180
5243
  filesWrapper.dataset.filesWrapper = pathKey;
5181
5244
  const filesPicker = document.createElement("input");
5182
5245
  filesPicker.type = "file";
@@ -5848,7 +5911,7 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
5848
5911
  values.push(element.default || "#000000");
5849
5912
  }
5850
5913
  const container = document.createElement("div");
5851
- container.className = "space-y-2";
5914
+ container.className = "fb-row";
5852
5915
  wrapper.appendChild(container);
5853
5916
  function updateIndices() {
5854
5917
  const items = container.querySelectorAll(".multiple-colour-item");
@@ -6704,7 +6767,7 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
6704
6767
  const itemsWrap = document.createElement("div");
6705
6768
  const columns = element.columns || 1;
6706
6769
  if (columns === 1) {
6707
- itemsWrap.className = "space-y-2";
6770
+ itemsWrap.className = "fb-row";
6708
6771
  } else {
6709
6772
  itemsWrap.className = `grid grid-cols-${columns} gap-2`;
6710
6773
  }
@@ -6741,12 +6804,9 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
6741
6804
  containerWrap.appendChild(itemsWrap);
6742
6805
  wrapper.appendChild(containerWrap);
6743
6806
  }
6744
- function getChildWrapperClass(isSlides, columns) {
6745
- if (isSlides) {
6746
- return "space-y-2";
6747
- }
6807
+ function getChildWrapperClass(columns) {
6748
6808
  const cols = columns || 1;
6749
- return cols === 1 ? "space-y-2" : `grid grid-cols-${cols} gap-2`;
6809
+ return cols === 1 ? "fb-row" : `grid grid-cols-${cols} gap-2`;
6750
6810
  }
6751
6811
  function mountRemoveButton(item, onRemove, state) {
6752
6812
  const rem = document.createElement("button");
@@ -6789,11 +6849,8 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6789
6849
  const isSlides = element.displayMode === "slides";
6790
6850
  if (isSlides) {
6791
6851
  itemsWrap.className = "fb-container-slides";
6792
- const slideCols = element.columns;
6793
- const gridTemplateColumns = typeof slideCols === "number" && slideCols > 0 ? `repeat(${slideCols}, 1fr)` : "repeat(auto-fit, minmax(280px, 1fr))";
6794
- itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:var(--fb-slides-gap, 14px);align-items:start;`;
6795
6852
  } else {
6796
- itemsWrap.className = "space-y-2";
6853
+ itemsWrap.className = "fb-row";
6797
6854
  }
6798
6855
  if (!containerIsReadonly) {
6799
6856
  const hintsElement = createPrefillHints(element, element.key);
@@ -6827,7 +6884,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6827
6884
  item.setAttribute("data-fb-slide-card", "");
6828
6885
  }
6829
6886
  const childWrapper = document.createElement("div");
6830
- childWrapper.className = getChildWrapperClass(isSlides, element.columns);
6887
+ childWrapper.className = getChildWrapperClass(
6888
+ isSlides ? void 0 : element.columns
6889
+ );
6831
6890
  element.elements.forEach((child) => {
6832
6891
  if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
6833
6892
  childWrapper.appendChild(
@@ -6893,16 +6952,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6893
6952
  item.setAttribute("data-fb-slide-card", "");
6894
6953
  }
6895
6954
  const childWrapper = document.createElement("div");
6896
- if (isSlides) {
6897
- childWrapper.className = "space-y-2";
6898
- } else {
6899
- const columns = element.columns || 1;
6900
- if (columns === 1) {
6901
- childWrapper.className = "space-y-2";
6902
- } else {
6903
- childWrapper.className = `grid grid-cols-${columns} gap-2`;
6904
- }
6905
- }
6955
+ childWrapper.className = getChildWrapperClass(
6956
+ isSlides ? void 0 : element.columns
6957
+ );
6906
6958
  element.elements.forEach((child) => {
6907
6959
  if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
6908
6960
  const prefillVal = prefillObj?.[child.key] ?? ("default" in child ? child.default : null) ?? null;
@@ -6940,16 +6992,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6940
6992
  item.setAttribute("data-fb-slide-card", "");
6941
6993
  }
6942
6994
  const childWrapper = document.createElement("div");
6943
- if (isSlides) {
6944
- childWrapper.className = "space-y-2";
6945
- } else {
6946
- const columns = element.columns || 1;
6947
- if (columns === 1) {
6948
- childWrapper.className = "space-y-2";
6949
- } else {
6950
- childWrapper.className = `grid grid-cols-${columns} gap-2`;
6951
- }
6952
- }
6995
+ childWrapper.className = getChildWrapperClass(
6996
+ isSlides ? void 0 : element.columns
6997
+ );
6953
6998
  element.elements.forEach((child) => {
6954
6999
  if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
6955
7000
  childWrapper.appendChild(
@@ -6978,7 +7023,6 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6978
7023
  containerWrap.appendChild(itemsWrap);
6979
7024
  if (!containerIsReadonly) {
6980
7025
  if (isSlides) {
6981
- itemsWrap.style.alignItems = "stretch";
6982
7026
  const handle = createSlideAddTile(handleAddItem, {
6983
7027
  label: element.addLabel
6984
7028
  });
@@ -8499,7 +8543,7 @@ function validateTableElement(element, key, context) {
8499
8543
  if (!hiddenInput) {
8500
8544
  return { value: null, errors };
8501
8545
  }
8502
- let value = null;
8546
+ let value;
8503
8547
  try {
8504
8548
  value = JSON.parse(hiddenInput.value);
8505
8549
  } catch {
@@ -9824,7 +9868,7 @@ function validateRichInputElement(element, key, context) {
9824
9868
  if (!hiddenInput) {
9825
9869
  return { value: null, errors };
9826
9870
  }
9827
- let rawValue = {};
9871
+ let rawValue;
9828
9872
  try {
9829
9873
  const parsed = JSON.parse(hiddenInput.value);
9830
9874
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
@@ -10463,7 +10507,8 @@ function setupEnableIfListeners(wrapper, element, ctx) {
10463
10507
  }
10464
10508
  function createFieldLabel(element) {
10465
10509
  const title = document.createElement("label");
10466
- title.className = "text-sm font-medium text-gray-900";
10510
+ title.className = "font-medium text-gray-900";
10511
+ title.style.cssText = "font-size: var(--fb-label-font-size, 0.75rem); line-height: var(--fb-label-line-height, 1.2);";
10467
10512
  title.textContent = element.label ?? element.key ?? "";
10468
10513
  if (element.required) {
10469
10514
  const req = document.createElement("span");
@@ -10494,7 +10539,8 @@ function createInfoButton(element) {
10494
10539
  }
10495
10540
  function createLabelContainer(element) {
10496
10541
  const label = document.createElement("div");
10497
- label.className = "flex items-center mb-1";
10542
+ label.className = "flex items-center";
10543
+ label.style.marginBottom = "var(--fb-label-margin-bottom, 2px)";
10498
10544
  label.dataset.fbLabelRow = "";
10499
10545
  const title = createFieldLabel(element);
10500
10546
  label.appendChild(title);
@@ -10605,11 +10651,12 @@ function renderElement2(element, ctx) {
10605
10651
  }
10606
10652
  const initiallyDisabled2 = shouldDisableElement(element, ctx);
10607
10653
  const outerWrapper = document.createElement("div");
10608
- outerWrapper.className = "mb-2 fb-field-wrapper fb-markdown-wrapper";
10654
+ outerWrapper.className = `fb-field-wrapper fb-markdown-wrapper fb-size-${element.size || "md"}`;
10609
10655
  outerWrapper.setAttribute(
10610
10656
  "data-field-key",
10611
10657
  getElementLookupKey(element, ctx.state)
10612
10658
  );
10659
+ outerWrapper.setAttribute("data-fb-width", element.width || "full");
10613
10660
  renderMarkdown(element, ctx, outerWrapper);
10614
10661
  if (initiallyDisabled2) {
10615
10662
  outerWrapper.style.display = "none";
@@ -10621,8 +10668,9 @@ function renderElement2(element, ctx) {
10621
10668
  }
10622
10669
  const initiallyDisabled = shouldDisableElement(element, ctx);
10623
10670
  const wrapper = document.createElement("div");
10624
- wrapper.className = "mb-2 fb-field-wrapper";
10671
+ wrapper.className = `fb-field-wrapper fb-size-${element.size || "md"}`;
10625
10672
  wrapper.setAttribute("data-field-key", element.key);
10673
+ wrapper.setAttribute("data-fb-width", element.width || "full");
10626
10674
  const ops = getComponentOperations(element.type);
10627
10675
  if (!ops?.ownsLabel) {
10628
10676
  const label = createLabelContainer(element);
@@ -10944,9 +10992,9 @@ var defaultTheme = {
10944
10992
  // blue-500
10945
10993
  // Spacing
10946
10994
  inputPaddingX: "0.5rem",
10947
- // 8px (compact density v2)
10948
- inputPaddingY: "0.25rem",
10949
- // 4px (compact density v2)
10995
+ // 8px (compact density v3 — v0.4.0)
10996
+ inputPaddingY: "0.125rem",
10997
+ // 2px (compact density v3 — v0.4.0)
10950
10998
  borderRadius: "0.5rem",
10951
10999
  // rounded-lg (8px)
10952
11000
  borderRadiusSmall: "0.375rem",
@@ -10984,13 +11032,20 @@ var defaultTheme = {
10984
11032
  slideCardRadius: "0.5rem",
10985
11033
  // matches borderRadius
10986
11034
  slideCardMinHeight: "0",
10987
- slideCardPadding: "12px",
11035
+ slideCardPadding: "8px",
10988
11036
  // Section-label defaults — same look as the regular field label. Themes
10989
11037
  // that want the "ПРЕИМУЩЕСТВА" caps style override these three vars.
10990
11038
  labelSectionFontSize: "0.875rem",
10991
11039
  // matches fontSize
10992
11040
  labelSectionLetterSpacing: "normal",
10993
- labelSectionTextTransform: "none"
11041
+ labelSectionTextTransform: "none",
11042
+ // Per-field label defaults — compact density: 12px @ 1.2 line-height with
11043
+ // a 2px gap to the input. Themes that prefer roomier labels (Picaz, klein)
11044
+ // can bump these via their override block.
11045
+ labelFontSize: "0.75rem",
11046
+ // 12px
11047
+ labelLineHeight: "1.2",
11048
+ labelMarginBottom: "2px"
10994
11049
  };
10995
11050
  function generateCSSVariables(theme) {
10996
11051
  const mergedTheme = { ...defaultTheme, ...theme };
@@ -11536,7 +11591,7 @@ var FormBuilderInstance = class {
11536
11591
  root.setAttribute("data-fb-root", "true");
11537
11592
  injectThemeVariables(root, this.state.config.theme);
11538
11593
  const rootContainer = document.createElement("div");
11539
- rootContainer.className = "space-y-2";
11594
+ rootContainer.className = "fb-row";
11540
11595
  if (schema.prefillHints && !this.state.config.readonly) {
11541
11596
  const hintsContainer = this.createRootPrefillHints(schema.prefillHints);
11542
11597
  rootContainer.appendChild(hintsContainer);
@@ -11544,7 +11599,7 @@ var FormBuilderInstance = class {
11544
11599
  const fieldsWrapper = document.createElement("div");
11545
11600
  const columns = schema.columns || 1;
11546
11601
  if (columns === 1) {
11547
- fieldsWrapper.className = "space-y-2";
11602
+ fieldsWrapper.className = "fb-row";
11548
11603
  } else {
11549
11604
  fieldsWrapper.className = `grid grid-cols-${columns} gap-2`;
11550
11605
  }