@dmitryvim/form-builder 0.3.3 → 0.4.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.
@@ -473,20 +473,61 @@ function ensureThemingHooks(doc) {
473
473
  color: #ffffff;
474
474
  border-color: var(--fb-primary-color);
475
475
  }
476
+ /* \u2500\u2500\u2500 Row layout: flex-wrap container with per-child width tokens \u2500\u2500\u2500
477
+ Replaces the older Tailwind \`space-y-2\` class. Children without
478
+ data-fb-width (or width="full") take 100% basis \u2192 forced wrap \u2192 behaves
479
+ like a vertical stack. Mixed widths sit side-by-side in the same row. */
480
+ .fb-row {
481
+ display: flex;
482
+ flex-wrap: wrap;
483
+ gap: var(--fb-row-gap, 0.25rem);
484
+ align-items: flex-start;
485
+ }
486
+ .fb-row > [data-fb-width="full"],
487
+ .fb-row > :not([data-fb-width]) { flex: 0 0 100%; min-width: 0; }
488
+ .fb-row > [data-fb-width="1/2"] { flex: 0 0 calc(50% - var(--fb-row-gap, 0.25rem) / 2); min-width: 0; }
489
+ .fb-row > [data-fb-width="1/3"] { flex: 0 0 calc(33.333% - var(--fb-row-gap, 0.25rem) * 2 / 3); min-width: 0; }
490
+ .fb-row > [data-fb-width="2/3"] { flex: 0 0 calc(66.666% - var(--fb-row-gap, 0.25rem) / 3); min-width: 0; }
491
+ /* \u2500\u2500\u2500 Density tokens: re-bind --fb-input-padding-* and --fb-font-size at
492
+ the wrapper level so every component that reads those vars (text, textarea,
493
+ select, number, colour, switcher, file) picks the size up via cascade. md
494
+ uses the defaults set in theme.ts (2px / 8px / 14px). The scale primarily
495
+ varies font-size; vertical padding stays close to md so an "lg" heading
496
+ field does not visually break the form's row rhythm \u2014 only the typography
497
+ announces "this is bigger". Picaz theme overrides padding directly for
498
+ its own density, independent of this scale. */
499
+ /* size-token side-effects on file tiles: --fb-file-wide-min controls the
500
+ single-file empty dropzone height, --fb-file-zone-min controls the inner
501
+ upload/library zones. Both read in src/components/file/styles.ts. */
502
+ .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; }
503
+ .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; }
504
+ /* .fb-size-md uses defaults \u2014 no override needed */
505
+ .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; }
506
+ .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; }
476
507
  `;
477
508
  doc.head.appendChild(style);
478
509
  }
479
- function applyAutoExpand(textarea) {
510
+ function applyAutoExpand(textarea, options = {}) {
511
+ var _a;
480
512
  textarea.style.overflow = "hidden";
481
513
  textarea.style.resize = "none";
514
+ const minRows = Math.max(1, (_a = options.minRows) != null ? _a : 1);
482
515
  const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
483
- textarea.rows = Math.max(1, lineCount);
516
+ textarea.rows = Math.max(minRows, lineCount);
484
517
  const resize = () => {
485
518
  if (!textarea.isConnected) return;
486
- textarea.style.height = "0";
519
+ textarea.style.height = "";
487
520
  const cs = getComputedStyle(textarea);
488
521
  const borderY = parseFloat(cs.borderTopWidth || "0") + parseFloat(cs.borderBottomWidth || "0");
489
- textarea.style.height = `${textarea.scrollHeight + borderY}px`;
522
+ const lineHeight = parseFloat(cs.lineHeight || "0") || parseFloat(cs.fontSize || "0") * 1.5 || 20;
523
+ const padY = parseFloat(cs.paddingTop || "0") + parseFloat(cs.paddingBottom || "0");
524
+ const minHeight = lineHeight * minRows + padY + borderY;
525
+ if (textarea.value === "") {
526
+ textarea.style.height = `${minHeight}px`;
527
+ return;
528
+ }
529
+ const contentHeight = textarea.scrollHeight + borderY;
530
+ textarea.style.height = `${Math.max(contentHeight, minHeight)}px`;
490
531
  };
491
532
  textarea.addEventListener("input", resize);
492
533
  setTimeout(() => {
@@ -495,13 +536,13 @@ function applyAutoExpand(textarea) {
495
536
  if (typeof ResizeObserver === "undefined") return;
496
537
  let lastWidth = -1;
497
538
  const ro = new ResizeObserver((entries) => {
498
- var _a, _b, _c, _d, _e;
539
+ var _a2, _b, _c, _d, _e;
499
540
  if (!textarea.isConnected) {
500
541
  ro.disconnect();
501
542
  return;
502
543
  }
503
544
  const entry = entries[0];
504
- const w = (_e = (_d = (_b = (_a = entry == null ? void 0 : entry.contentBoxSize) == null ? void 0 : _a[0]) == null ? void 0 : _b.inlineSize) != null ? _d : (_c = entry == null ? void 0 : entry.contentRect) == null ? void 0 : _c.width) != null ? _e : 0;
545
+ const w = (_e = (_d = (_b = (_a2 = entry == null ? void 0 : entry.contentBoxSize) == null ? void 0 : _a2[0]) == null ? void 0 : _b.inlineSize) != null ? _d : (_c = entry == null ? void 0 : entry.contentRect) == null ? void 0 : _c.width) != null ? _e : 0;
505
546
  if (w === lastWidth) return;
506
547
  lastWidth = w;
507
548
  resize();
@@ -541,8 +582,8 @@ function createAddItemRow(classNameSuffix, onClick, options = {}) {
541
582
  const label = (_a = options.label) != null ? _a : "";
542
583
  const showCounter = options.showCounter !== false;
543
584
  const row = document.createElement("div");
544
- row.className = "fb-add-row mt-2";
545
- row.style.cssText = "display:flex;align-items:stretch;width:100%;";
585
+ row.className = "fb-add-row";
586
+ row.style.cssText = "display:flex;align-items:stretch;width:100%;margin-top:4px;";
546
587
  const button = document.createElement("button");
547
588
  button.type = "button";
548
589
  button.className = `add-${classNameSuffix}-btn`;
@@ -551,8 +592,8 @@ function createAddItemRow(classNameSuffix, onClick, options = {}) {
551
592
  display: inline-flex;
552
593
  align-items: center;
553
594
  justify-content: center;
554
- gap: 6px;
555
- padding: 6px 10px;
595
+ gap: 4px;
596
+ padding: 3px 10px;
556
597
  border: 1px dashed var(--fb-primary-color);
557
598
  border-radius: var(--fb-border-radius);
558
599
  background: transparent;
@@ -698,12 +739,12 @@ function ensureChipStyles(doc) {
698
739
  const style = doc.createElement("style");
699
740
  style.setAttribute("data-fb-chip-styles", "");
700
741
  style.textContent = `
701
- .fb-chip-list { display: flex; flex-direction: column; gap: 4px; }
742
+ .fb-chip-list { display: flex; flex-direction: column; gap: 2px; }
702
743
  .fb-chip {
703
744
  display: flex;
704
745
  align-items: center;
705
- gap: 8px;
706
- padding: 5px 6px 5px 10px;
746
+ gap: 6px;
747
+ padding: 3px 6px 3px 8px;
707
748
  background: var(--fb-chip-bg, var(--fb-background-color, #fff));
708
749
  border: 1px solid var(--fb-chip-border, var(--fb-border-color, #e2e8f0));
709
750
  border-radius: 6px;
@@ -729,7 +770,13 @@ function ensureChipStyles(doc) {
729
770
  color: var(--fb-chip-text, var(--fb-text-color, inherit));
730
771
  font-size: var(--fb-font-size, 14px);
731
772
  font-family: var(--fb-font-family, inherit);
732
- line-height: 1.4;
773
+ line-height: var(--fb-line-height, 1.4);
774
+ /* Soft-wrap support (v0.4.0): chip-input is now a <textarea rows=1> that
775
+ grows vertically when content overflows the chip width. */
776
+ resize: none;
777
+ overflow: hidden;
778
+ word-break: break-word;
779
+ overflow-wrap: anywhere;
733
780
  }
734
781
  .fb-chip-input::placeholder { color: var(--fb-text-placeholder-color, #94a3b8); }
735
782
  .fb-chip-input:read-only { color: var(--fb-text-secondary-color, #475569); }
@@ -788,6 +835,7 @@ function createCharCounter(element, input) {
788
835
  return counter;
789
836
  }
790
837
  function renderTextElement(element, ctx, wrapper, pathKey) {
838
+ var _a;
791
839
  const state = ctx.state;
792
840
  const readonly = isElementReadonly(element, state, ctx);
793
841
  const inputWrapper = document.createElement("div");
@@ -814,7 +862,7 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
814
862
  overflow-wrap: anywhere;
815
863
  `;
816
864
  textInput.name = pathKey;
817
- textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
865
+ textInput.placeholder = (_a = element.placeholder) != null ? _a : "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
818
866
  textInput.value = ctx.prefill[element.key] || element.default || "";
819
867
  textInput.readOnly = readonly;
820
868
  applySingleLineMode(textInput);
@@ -861,7 +909,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
861
909
  const readonly = isElementReadonly(element, state, ctx);
862
910
  const prefillValues = ctx.prefill[element.key] || [];
863
911
  const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
864
- const minCount = (_a = element.minCount) != null ? _a : 1;
912
+ const minCount = (_a = element.minCount) != null ? _a : element.required ? 1 : 0;
865
913
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
866
914
  while (values.length < minCount) {
867
915
  values.push(element.default || "");
@@ -888,8 +936,8 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
888
936
  dot.className = "fb-chip-dot";
889
937
  dot.setAttribute("aria-hidden", "true");
890
938
  chip.appendChild(dot);
891
- const input = document.createElement("input");
892
- input.type = "text";
939
+ const input = document.createElement("textarea");
940
+ input.rows = 1;
893
941
  input.className = "fb-chip-input";
894
942
  input.value = value;
895
943
  input.placeholder = element.placeholder || t("placeholderText", state);
@@ -905,6 +953,8 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
905
953
  input.addEventListener("blur", handleChange);
906
954
  input.addEventListener("input", handleChange);
907
955
  }
956
+ applySingleLineMode(input);
957
+ applyAutoExpand(input);
908
958
  if (!readonly) {
909
959
  const rem = document.createElement("button");
910
960
  rem.type = "button";
@@ -1050,7 +1100,7 @@ function validateTextElement(element, key, context) {
1050
1100
  });
1051
1101
  if (!skipValidation) {
1052
1102
  const { state } = context;
1053
- const minCount = (_a = element.minCount) != null ? _a : 1;
1103
+ const minCount = (_a = element.minCount) != null ? _a : 0;
1054
1104
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
1055
1105
  const filteredValues = rawValues.filter((v) => v.trim() !== "");
1056
1106
  if (element.required && filteredValues.length === 0) {
@@ -1119,6 +1169,7 @@ function updateTextField(element, fieldPath, value, context) {
1119
1169
 
1120
1170
  // src/components/textarea.ts
1121
1171
  function renderTextareaElement(element, ctx, wrapper, pathKey) {
1172
+ var _a, _b;
1122
1173
  const state = ctx.state;
1123
1174
  const readonly = isElementReadonly(element, state, ctx);
1124
1175
  const textareaWrapper = document.createElement("div");
@@ -1126,13 +1177,13 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
1126
1177
  const textareaInput = document.createElement("textarea");
1127
1178
  textareaInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
1128
1179
  textareaInput.style.cssText = `
1129
- padding: var(--fb-input-padding-y) var(--fb-input-padding-x) 24px var(--fb-input-padding-x);
1180
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
1130
1181
  font-size: var(--fb-font-size);
1131
1182
  font-family: var(--fb-font-family);
1183
+ line-height: var(--fb-line-height, 1.5);
1132
1184
  `;
1133
1185
  textareaInput.name = pathKey;
1134
- textareaInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
1135
- textareaInput.rows = element.rows || 4;
1186
+ textareaInput.placeholder = (_a = element.placeholder) != null ? _a : "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
1136
1187
  textareaInput.value = ctx.prefill[element.key] || element.default || "";
1137
1188
  textareaInput.readOnly = readonly;
1138
1189
  if (!readonly && ctx.instance) {
@@ -1143,9 +1194,7 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
1143
1194
  textareaInput.addEventListener("blur", handleChange);
1144
1195
  textareaInput.addEventListener("input", handleChange);
1145
1196
  }
1146
- if (element.autoExpand || readonly) {
1147
- applyAutoExpand(textareaInput);
1148
- }
1197
+ applyAutoExpand(textareaInput, { minRows: (_b = element.rows) != null ? _b : 1 });
1149
1198
  textareaWrapper.appendChild(textareaInput);
1150
1199
  if (!readonly && (element.minLength != null || element.maxLength != null)) {
1151
1200
  const counter = createCharCounter(element, textareaInput);
@@ -1159,13 +1208,13 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1159
1208
  const readonly = isElementReadonly(element, state, ctx);
1160
1209
  const prefillValues = ctx.prefill[element.key] || [];
1161
1210
  const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
1162
- const minCount = (_a = element.minCount) != null ? _a : 1;
1211
+ const minCount = (_a = element.minCount) != null ? _a : element.required ? 1 : 0;
1163
1212
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
1164
1213
  while (values.length < minCount) {
1165
1214
  values.push(element.default || "");
1166
1215
  }
1167
1216
  const container = document.createElement("div");
1168
- container.className = "space-y-2";
1217
+ container.className = "fb-row";
1169
1218
  wrapper.appendChild(container);
1170
1219
  function updateIndices() {
1171
1220
  const items = container.querySelectorAll(".multiple-textarea-item");
@@ -1177,6 +1226,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1177
1226
  });
1178
1227
  }
1179
1228
  function addTextareaItem(value = "", index = -1) {
1229
+ var _a2;
1180
1230
  const itemWrapper = document.createElement("div");
1181
1231
  itemWrapper.className = "multiple-textarea-item";
1182
1232
  const textareaContainer = document.createElement("div");
@@ -1184,12 +1234,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1184
1234
  const textareaInput = document.createElement("textarea");
1185
1235
  textareaInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
1186
1236
  textareaInput.style.cssText = `
1187
- padding: var(--fb-input-padding-y) var(--fb-input-padding-x) 24px var(--fb-input-padding-x);
1237
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
1188
1238
  font-size: var(--fb-font-size);
1189
1239
  font-family: var(--fb-font-family);
1240
+ line-height: var(--fb-line-height, 1.5);
1190
1241
  `;
1191
1242
  textareaInput.placeholder = element.placeholder || t("placeholderText", state);
1192
- textareaInput.rows = element.rows || 4;
1193
1243
  textareaInput.value = value;
1194
1244
  textareaInput.readOnly = readonly;
1195
1245
  if (!readonly && ctx.instance) {
@@ -1200,9 +1250,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1200
1250
  textareaInput.addEventListener("blur", handleChange);
1201
1251
  textareaInput.addEventListener("input", handleChange);
1202
1252
  }
1203
- if (element.autoExpand || readonly) {
1204
- applyAutoExpand(textareaInput);
1205
- }
1253
+ applyAutoExpand(textareaInput, { minRows: (_a2 = element.rows) != null ? _a2 : 1 });
1206
1254
  textareaContainer.appendChild(textareaInput);
1207
1255
  if (!readonly && (element.minLength != null || element.maxLength != null)) {
1208
1256
  const counter = createCharCounter(element, textareaInput);
@@ -1278,9 +1326,7 @@ function validateTextareaElement(element, key, context) {
1278
1326
  }
1279
1327
  function updateTextareaField(element, fieldPath, value, context) {
1280
1328
  updateTextField(element, fieldPath, value, context);
1281
- const { scopeRoot, state } = context;
1282
- const shouldAutoExpand = element.autoExpand || isElementReadonly(element, state);
1283
- if (!shouldAutoExpand) return;
1329
+ const { scopeRoot } = context;
1284
1330
  if (element.multiple) {
1285
1331
  const textareas = scopeRoot.querySelectorAll(
1286
1332
  `textarea[name^="${fieldPath}["]`
@@ -1472,13 +1518,13 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1472
1518
  const readonly = isElementReadonly(element, state, ctx);
1473
1519
  const prefillValues = ctx.prefill[element.key] || [];
1474
1520
  const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
1475
- const minCount = (_a = element.minCount) != null ? _a : 1;
1521
+ const minCount = (_a = element.minCount) != null ? _a : element.required ? 1 : 0;
1476
1522
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
1477
1523
  while (values.length < minCount) {
1478
1524
  values.push(element.default || "");
1479
1525
  }
1480
1526
  const container = document.createElement("div");
1481
- container.className = "space-y-2";
1527
+ container.className = "fb-row";
1482
1528
  wrapper.appendChild(container);
1483
1529
  function updateIndices() {
1484
1530
  const items = container.querySelectorAll(".multiple-number-item");
@@ -1645,7 +1691,7 @@ function validateNumberElement(element, key, context) {
1645
1691
  };
1646
1692
  if (element.multiple) {
1647
1693
  const inputs = scopeRoot.querySelectorAll(
1648
- `[name^="${key}["]`
1694
+ `[name^="${key}\\["]`
1649
1695
  );
1650
1696
  const values = [];
1651
1697
  inputs.forEach((input, index) => {
@@ -1670,7 +1716,7 @@ function validateNumberElement(element, key, context) {
1670
1716
  });
1671
1717
  if (!skipValidation) {
1672
1718
  const { state } = context;
1673
- const minCount = (_a = element.minCount) != null ? _a : 1;
1719
+ const minCount = (_a = element.minCount) != null ? _a : 0;
1674
1720
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
1675
1721
  const filteredValues = values.filter((v) => v !== null);
1676
1722
  if (element.required && filteredValues.length === 0) {
@@ -1790,13 +1836,13 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1790
1836
  const readonly = isElementReadonly(element, state, ctx);
1791
1837
  const prefillValues = ctx.prefill[element.key] || [];
1792
1838
  const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
1793
- const minCount = (_a = element.minCount) != null ? _a : 1;
1839
+ const minCount = (_a = element.minCount) != null ? _a : element.required ? 1 : 0;
1794
1840
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
1795
1841
  while (values.length < minCount) {
1796
1842
  values.push(element.default || ((_d = (_c = element.options) == null ? void 0 : _c[0]) == null ? void 0 : _d.value) || "");
1797
1843
  }
1798
1844
  const container = document.createElement("div");
1799
- container.className = "space-y-2";
1845
+ container.className = "fb-row";
1800
1846
  wrapper.appendChild(container);
1801
1847
  function updateIndices() {
1802
1848
  const items = container.querySelectorAll(".multiple-select-item");
@@ -1946,7 +1992,7 @@ function validateSelectElement(element, key, context) {
1946
1992
  if (skipValidation) return;
1947
1993
  const { state } = context;
1948
1994
  const filteredValues = values.filter(filterFn);
1949
- const minCount = "minCount" in element2 ? (_a2 = element2.minCount) != null ? _a2 : 1 : 1;
1995
+ const minCount = "minCount" in element2 ? (_a2 = element2.minCount) != null ? _a2 : 0 : 0;
1950
1996
  const maxCount = "maxCount" in element2 ? (_b = element2.maxCount) != null ? _b : Infinity : Infinity;
1951
1997
  if (element2.required && filteredValues.length === 0) {
1952
1998
  errors.push(`${key2}: ${t("required", state)}`);
@@ -2226,14 +2272,14 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
2226
2272
  const state = ctx.state;
2227
2273
  const prefillValues = ctx.prefill[element.key] || [];
2228
2274
  const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
2229
- const minCount = (_a = element.minCount) != null ? _a : 1;
2275
+ const minCount = (_a = element.minCount) != null ? _a : element.required ? 1 : 0;
2230
2276
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
2231
2277
  while (values.length < minCount) {
2232
2278
  values.push(element.default || ((_d = (_c = element.options) == null ? void 0 : _c[0]) == null ? void 0 : _d.value) || "");
2233
2279
  }
2234
2280
  const readonly = isElementReadonly(element, state, ctx);
2235
2281
  const container = document.createElement("div");
2236
- container.className = "space-y-2";
2282
+ container.className = "fb-row";
2237
2283
  wrapper.appendChild(container);
2238
2284
  function updateIndices() {
2239
2285
  const items = container.querySelectorAll(".multiple-switcher-item");
@@ -2390,7 +2436,7 @@ function validateSwitcherElement(element, key, context) {
2390
2436
  if (skipValidation) return;
2391
2437
  const { state } = context;
2392
2438
  const filteredValues = values.filter(filterFn);
2393
- const minCount = "minCount" in el ? (_a2 = el.minCount) != null ? _a2 : 1 : 1;
2439
+ const minCount = "minCount" in el ? (_a2 = el.minCount) != null ? _a2 : 0 : 0;
2394
2440
  const maxCount = "maxCount" in el ? (_b = el.maxCount) != null ? _b : Infinity : Infinity;
2395
2441
  if (el.required && filteredValues.length === 0) {
2396
2442
  errors.push(`${fieldKey}: ${t("required", state)}`);
@@ -2818,7 +2864,9 @@ function ensureFileStyles() {
2818
2864
  align-items: stretch;
2819
2865
  gap: 0;
2820
2866
  overflow: hidden;
2821
- min-height: 180px;
2867
+ /* min-height driven by --fb-file-wide-min so size-tokens (.fb-size-{xs|sm|lg|xl})
2868
+ can override per-field. Default 96px matches v0.4.0 compact density. */
2869
+ min-height: var(--fb-file-wide-min, 96px);
2822
2870
  transition: border-color 150ms, background 150ms, box-shadow 150ms;
2823
2871
  cursor: pointer;
2824
2872
  }
@@ -2837,14 +2885,15 @@ function ensureFileStyles() {
2837
2885
  upload + library on one row (~220 + 176), library wraps below. */
2838
2886
  .fb-wide-tile-upload {
2839
2887
  flex: 1 1 220px;
2840
- min-height: 140px;
2888
+ /* min-height theme-driven; default 72px matches v0.4.0 compact density. */
2889
+ min-height: var(--fb-file-zone-min, 72px);
2841
2890
  display: flex;
2842
2891
  flex-direction: column;
2843
2892
  align-items: center;
2844
2893
  justify-content: center;
2845
- gap: 8px;
2894
+ gap: 4px;
2846
2895
  color: #2563eb;
2847
- padding: 16px;
2896
+ padding: var(--fb-file-zone-padding, 8px);
2848
2897
  transition: background 150ms;
2849
2898
  cursor: pointer;
2850
2899
  background: transparent;
@@ -2864,14 +2913,16 @@ function ensureFileStyles() {
2864
2913
  visual hierarchy "upload > library" in both layouts. */
2865
2914
  .fb-wide-tile-library {
2866
2915
  flex: 0 0 176px;
2867
- min-height: 120px;
2916
+ /* Shares --fb-file-zone-min with the upload zone so size-tokens scale both
2917
+ uniformly. Default 72px matches v0.4.0 compact density. */
2918
+ min-height: var(--fb-file-zone-min, 72px);
2868
2919
  display: flex;
2869
2920
  flex-direction: column;
2870
2921
  align-items: center;
2871
2922
  justify-content: center;
2872
- gap: 8px;
2923
+ gap: 4px;
2873
2924
  color: #2563eb;
2874
- padding: 12px;
2925
+ padding: var(--fb-file-zone-padding, 8px);
2875
2926
  transition: background 150ms;
2876
2927
  cursor: pointer;
2877
2928
  background: transparent;
@@ -4148,7 +4199,9 @@ function downloadBlob(blob, fileName) {
4148
4199
  URL.revokeObjectURL(blobUrl);
4149
4200
  }, 100);
4150
4201
  } catch (error) {
4151
- throw new Error(`Blob download failed: ${error.message}`);
4202
+ throw new Error(`Blob download failed: ${error.message}`, {
4203
+ cause: error
4204
+ });
4152
4205
  }
4153
4206
  }
4154
4207
 
@@ -5106,7 +5159,7 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
5106
5159
  var _a, _b;
5107
5160
  const state = ctx.state;
5108
5161
  const fileWrapper = document.createElement("div");
5109
- fileWrapper.className = "space-y-2";
5162
+ fileWrapper.className = "fb-row";
5110
5163
  fileWrapper.dataset.filesWrapper = pathKey;
5111
5164
  const picker = document.createElement("input");
5112
5165
  picker.type = "file";
@@ -5257,7 +5310,7 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
5257
5310
  var _a, _b;
5258
5311
  const state = ctx.state;
5259
5312
  const filesWrapper = document.createElement("div");
5260
- filesWrapper.className = "space-y-2";
5313
+ filesWrapper.className = "fb-row";
5261
5314
  filesWrapper.dataset.filesWrapper = pathKey;
5262
5315
  const filesPicker = document.createElement("input");
5263
5316
  filesPicker.type = "file";
@@ -5933,13 +5986,13 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
5933
5986
  const readonly = isElementReadonly(element, state, ctx);
5934
5987
  const prefillValues = ctx.prefill[element.key] || [];
5935
5988
  const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
5936
- const minCount = (_a = element.minCount) != null ? _a : 1;
5989
+ const minCount = (_a = element.minCount) != null ? _a : element.required ? 1 : 0;
5937
5990
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
5938
5991
  while (values.length < minCount) {
5939
5992
  values.push(element.default || "#000000");
5940
5993
  }
5941
5994
  const container = document.createElement("div");
5942
- container.className = "space-y-2";
5995
+ container.className = "fb-row";
5943
5996
  wrapper.appendChild(container);
5944
5997
  function updateIndices() {
5945
5998
  const items = container.querySelectorAll(".multiple-colour-item");
@@ -6122,7 +6175,7 @@ function validateColourElement(element, key, context) {
6122
6175
  });
6123
6176
  if (!skipValidation) {
6124
6177
  const { state } = context;
6125
- const minCount = (_a = element.minCount) != null ? _a : 1;
6178
+ const minCount = (_a = element.minCount) != null ? _a : 0;
6126
6179
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
6127
6180
  const filteredValues = values.filter((v) => v !== "");
6128
6181
  if (element.required && filteredValues.length === 0) {
@@ -6400,7 +6453,7 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
6400
6453
  const readonly = isElementReadonly(element, state, ctx);
6401
6454
  const prefillValues = ctx.prefill[element.key] || [];
6402
6455
  const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
6403
- const minCount = (_a = element.minCount) != null ? _a : 1;
6456
+ const minCount = (_a = element.minCount) != null ? _a : element.required ? 1 : 0;
6404
6457
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
6405
6458
  const defaultValue = element.default !== void 0 ? element.default : (element.min + element.max) / 2;
6406
6459
  while (values.length < minCount) {
@@ -6616,7 +6669,7 @@ function validateSliderElement(element, key, context) {
6616
6669
  });
6617
6670
  if (!skipValidation) {
6618
6671
  const { state } = context;
6619
- const minCount = (_b = element.minCount) != null ? _b : 1;
6672
+ const minCount = (_b = element.minCount) != null ? _b : 0;
6620
6673
  const maxCount = (_c = element.maxCount) != null ? _c : Infinity;
6621
6674
  const filteredValues = values.filter((v) => v !== null);
6622
6675
  if (element.required && filteredValues.length === 0) {
@@ -6805,7 +6858,7 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
6805
6858
  const itemsWrap = document.createElement("div");
6806
6859
  const columns = element.columns || 1;
6807
6860
  if (columns === 1) {
6808
- itemsWrap.className = "space-y-2";
6861
+ itemsWrap.className = "fb-row";
6809
6862
  } else {
6810
6863
  itemsWrap.className = `grid grid-cols-${columns} gap-2`;
6811
6864
  }
@@ -6845,10 +6898,10 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
6845
6898
  }
6846
6899
  function getChildWrapperClass(isSlides, columns) {
6847
6900
  if (isSlides) {
6848
- return "space-y-2";
6901
+ return "fb-row";
6849
6902
  }
6850
6903
  const cols = columns || 1;
6851
- return cols === 1 ? "space-y-2" : `grid grid-cols-${cols} gap-2`;
6904
+ return cols === 1 ? "fb-row" : `grid grid-cols-${cols} gap-2`;
6852
6905
  }
6853
6906
  function mountRemoveButton(item, onRemove, state) {
6854
6907
  const rem = document.createElement("button");
@@ -6896,7 +6949,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6896
6949
  const gridTemplateColumns = typeof slideCols === "number" && slideCols > 0 ? `repeat(${slideCols}, 1fr)` : "repeat(auto-fit, minmax(280px, 1fr))";
6897
6950
  itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:var(--fb-slides-gap, 14px);align-items:start;`;
6898
6951
  } else {
6899
- itemsWrap.className = "space-y-2";
6952
+ itemsWrap.className = "fb-row";
6900
6953
  }
6901
6954
  if (!containerIsReadonly) {
6902
6955
  const hintsElement = createPrefillHints(element, element.key);
@@ -6999,11 +7052,11 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6999
7052
  }
7000
7053
  const childWrapper = document.createElement("div");
7001
7054
  if (isSlides) {
7002
- childWrapper.className = "space-y-2";
7055
+ childWrapper.className = "fb-row";
7003
7056
  } else {
7004
7057
  const columns = element.columns || 1;
7005
7058
  if (columns === 1) {
7006
- childWrapper.className = "space-y-2";
7059
+ childWrapper.className = "fb-row";
7007
7060
  } else {
7008
7061
  childWrapper.className = `grid grid-cols-${columns} gap-2`;
7009
7062
  }
@@ -7047,11 +7100,11 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
7047
7100
  }
7048
7101
  const childWrapper = document.createElement("div");
7049
7102
  if (isSlides) {
7050
- childWrapper.className = "space-y-2";
7103
+ childWrapper.className = "fb-row";
7051
7104
  } else {
7052
7105
  const columns = element.columns || 1;
7053
7106
  if (columns === 1) {
7054
- childWrapper.className = "space-y-2";
7107
+ childWrapper.className = "fb-row";
7055
7108
  } else {
7056
7109
  childWrapper.className = `grid grid-cols-${columns} gap-2`;
7057
7110
  }
@@ -8637,7 +8690,7 @@ function validateTableElement(element, key, context) {
8637
8690
  if (!hiddenInput) {
8638
8691
  return { value: null, errors };
8639
8692
  }
8640
- let value = null;
8693
+ let value;
8641
8694
  try {
8642
8695
  value = JSON.parse(hiddenInput.value);
8643
8696
  } catch {
@@ -10004,7 +10057,7 @@ function validateRichInputElement(element, key, context) {
10004
10057
  if (!hiddenInput) {
10005
10058
  return { value: null, errors };
10006
10059
  }
10007
- let rawValue = {};
10060
+ let rawValue;
10008
10061
  try {
10009
10062
  const parsed = JSON.parse(hiddenInput.value);
10010
10063
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
@@ -10651,7 +10704,8 @@ function setupEnableIfListeners(wrapper, element, ctx) {
10651
10704
  function createFieldLabel(element) {
10652
10705
  var _a, _b;
10653
10706
  const title = document.createElement("label");
10654
- title.className = "text-sm font-medium text-gray-900";
10707
+ title.className = "font-medium text-gray-900";
10708
+ title.style.cssText = "font-size: var(--fb-label-font-size, 0.75rem); line-height: var(--fb-label-line-height, 1.2);";
10655
10709
  title.textContent = (_b = (_a = element.label) != null ? _a : element.key) != null ? _b : "";
10656
10710
  if (element.required) {
10657
10711
  const req = document.createElement("span");
@@ -10682,7 +10736,8 @@ function createInfoButton(element) {
10682
10736
  }
10683
10737
  function createLabelContainer(element) {
10684
10738
  const label = document.createElement("div");
10685
- label.className = "flex items-center mb-1";
10739
+ label.className = "flex items-center";
10740
+ label.style.marginBottom = "var(--fb-label-margin-bottom, 2px)";
10686
10741
  label.dataset.fbLabelRow = "";
10687
10742
  const title = createFieldLabel(element);
10688
10743
  label.appendChild(title);
@@ -10793,11 +10848,12 @@ function renderElement2(element, ctx) {
10793
10848
  }
10794
10849
  const initiallyDisabled2 = shouldDisableElement(element, ctx);
10795
10850
  const outerWrapper = document.createElement("div");
10796
- outerWrapper.className = "mb-2 fb-field-wrapper fb-markdown-wrapper";
10851
+ outerWrapper.className = `fb-field-wrapper fb-markdown-wrapper fb-size-${element.size || "md"}`;
10797
10852
  outerWrapper.setAttribute(
10798
10853
  "data-field-key",
10799
10854
  getElementLookupKey(element, ctx.state)
10800
10855
  );
10856
+ outerWrapper.setAttribute("data-fb-width", element.width || "full");
10801
10857
  renderMarkdown(element, ctx, outerWrapper);
10802
10858
  if (initiallyDisabled2) {
10803
10859
  outerWrapper.style.display = "none";
@@ -10809,8 +10865,9 @@ function renderElement2(element, ctx) {
10809
10865
  }
10810
10866
  const initiallyDisabled = shouldDisableElement(element, ctx);
10811
10867
  const wrapper = document.createElement("div");
10812
- wrapper.className = "mb-2 fb-field-wrapper";
10868
+ wrapper.className = `fb-field-wrapper fb-size-${element.size || "md"}`;
10813
10869
  wrapper.setAttribute("data-field-key", element.key);
10870
+ wrapper.setAttribute("data-fb-width", element.width || "full");
10814
10871
  const ops = getComponentOperations(element.type);
10815
10872
  if (!(ops == null ? void 0 : ops.ownsLabel)) {
10816
10873
  const label = createLabelContainer(element);
@@ -11132,9 +11189,9 @@ var defaultTheme = {
11132
11189
  // blue-500
11133
11190
  // Spacing
11134
11191
  inputPaddingX: "0.5rem",
11135
- // 8px (compact density v2)
11136
- inputPaddingY: "0.25rem",
11137
- // 4px (compact density v2)
11192
+ // 8px (compact density v3 — v0.4.0)
11193
+ inputPaddingY: "0.125rem",
11194
+ // 2px (compact density v3 — v0.4.0)
11138
11195
  borderRadius: "0.5rem",
11139
11196
  // rounded-lg (8px)
11140
11197
  borderRadiusSmall: "0.375rem",
@@ -11172,13 +11229,20 @@ var defaultTheme = {
11172
11229
  slideCardRadius: "0.5rem",
11173
11230
  // matches borderRadius
11174
11231
  slideCardMinHeight: "0",
11175
- slideCardPadding: "12px",
11232
+ slideCardPadding: "8px",
11176
11233
  // Section-label defaults — same look as the regular field label. Themes
11177
11234
  // that want the "ПРЕИМУЩЕСТВА" caps style override these three vars.
11178
11235
  labelSectionFontSize: "0.875rem",
11179
11236
  // matches fontSize
11180
11237
  labelSectionLetterSpacing: "normal",
11181
- labelSectionTextTransform: "none"
11238
+ labelSectionTextTransform: "none",
11239
+ // Per-field label defaults — compact density: 12px @ 1.2 line-height with
11240
+ // a 2px gap to the input. Themes that prefer roomier labels (Picaz, klein)
11241
+ // can bump these via their override block.
11242
+ labelFontSize: "0.75rem",
11243
+ // 12px
11244
+ labelLineHeight: "1.2",
11245
+ labelMarginBottom: "2px"
11182
11246
  };
11183
11247
  function generateCSSVariables(theme) {
11184
11248
  const mergedTheme = { ...defaultTheme, ...theme };
@@ -11724,7 +11788,7 @@ var FormBuilderInstance = class {
11724
11788
  root.setAttribute("data-fb-root", "true");
11725
11789
  injectThemeVariables(root, this.state.config.theme);
11726
11790
  const rootContainer = document.createElement("div");
11727
- rootContainer.className = "space-y-2";
11791
+ rootContainer.className = "fb-row";
11728
11792
  if (schema.prefillHints && !this.state.config.readonly) {
11729
11793
  const hintsContainer = this.createRootPrefillHints(schema.prefillHints);
11730
11794
  rootContainer.appendChild(hintsContainer);
@@ -11732,7 +11796,7 @@ var FormBuilderInstance = class {
11732
11796
  const fieldsWrapper = document.createElement("div");
11733
11797
  const columns = schema.columns || 1;
11734
11798
  if (columns === 1) {
11735
- fieldsWrapper.className = "space-y-2";
11799
+ fieldsWrapper.className = "fb-row";
11736
11800
  } else {
11737
11801
  fieldsWrapper.className = `grid grid-cols-${columns} gap-2`;
11738
11802
  }