form-page-builder 2.2.0 → 2.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.
package/README.md CHANGED
@@ -107,6 +107,7 @@ Every optional UI surface can be switched on or off independently through one `f
107
107
  | Key | Type | Default | Controls |
108
108
  |---|---|---|---|
109
109
  | `naming` | `boolean` | `true` | The editable form-title input in the toolbar. |
110
+ | `formTitle` | `boolean` | `false` | Renders the form title as an `<h2>` heading above the fields in Preview. Off by default — the host app usually shows the form's name in its own chrome, so leave this off to avoid a duplicate. |
110
111
  | `templates` | `boolean \| { manage?: boolean; max?: number }` | `true` | The Templates library and the "Save" button. `true` = full library (browse / apply / save / overwrite / delete). `false` = no template UI. `{ manage: false }` = pick-and-apply only: the library lists templates and the user can apply one as a starting point, but can't create, overwrite, or delete them and the "Save" button is hidden. `{ max: n }` caps how many templates can be stored (default 5). |
111
112
  | `newForm` | `boolean` | `true` | The "New Form" reset button. |
112
113
  | `autosave` | `boolean` | `true` | Autosaving the draft to `storage`. The initial draft *load* always happens regardless — this only gates the write path. |
@@ -122,10 +123,24 @@ Every optional UI surface can be switched on or off independently through one `f
122
123
  | `sectionBackground` | `boolean` | inherits `sections` | Per-section background-color swatches + custom-color picker in the section header. Independent of `sections` — set it `false` to lock section backgrounds while keeping the structural controls, or leave it unset and it follows whatever `sections` is. |
123
124
  | `dragReorder` | `boolean` | `true` | Drag-to-reorder fields within a section. |
124
125
  | `deviceToggle` | `boolean` | `true` | The Laptop/Tablet/Mobile width switcher above the Preview canvas. When `false`, Preview always renders at the Laptop (full) width. |
125
- | `maxFields` | `number` | `undefined` (unlimited) | Caps the total number of input-type fields (not content blocks) addable across the whole document. Once reached, the Form Fields palette buttons disable until a field is removed. |
126
+ | `maxFields` | `number` | `undefined` (unlimited) | Caps the total number of input-type fields (not content blocks) addable across the whole document. The palette shows a live "Fields: n / max" count; once reached, the Form Fields buttons disable and a limit message replaces the count until a field is removed. |
126
127
 
127
128
  Disabling `contentBlocks`/`fieldTypes` for a given type only hides it from the palette going forward — if a document loaded via `initialDocument` (or a saved template) already contains fields of a now-disabled type, they still render and remain editable in Build mode; nothing is stripped or hidden.
128
129
 
130
+ ### Styling hooks
131
+
132
+ The widget styles itself with inline styles and CSS custom properties (see theming below), but the rendered form also carries stable, rule-free class names so a host stylesheet can target it:
133
+
134
+ | Class | On |
135
+ |---|---|
136
+ | `.fb-form` | The Preview form container. |
137
+ | `.fb-section` | Each section wrapper (also `data-section-id`). |
138
+ | `.fb-field` | Every field wrapper, in Preview and on the Build canvas (also `data-field-id`). |
139
+ | `.fb-field--<type>` | Same wrapper, by field type — `fb-field--input`, `fb-field--select`, `fb-field--button`, … |
140
+ | `.fb-field--<id>` | Same wrapper, by field id (`fb-field--field_1`). Field ids are sequential (`field_1`, `field_2`, …) and stable across reloads. |
141
+
142
+ These are additive hooks only — the package ships no rules for them, so anything you write wins without `!important`.
143
+
129
144
  ### Features vs. theming
130
145
 
131
146
  `features` and `theme` are separate, composable concerns: `features.design` decides whether the Design tab's color/spacing *controls* are shown at all, while `theme` (and the Design tab, when shown) decide what those colors/spacing values *are*. You can, for instance, pass a fixed `theme` with `features.design` and `features.blockStyling` both `false` to lock a form to your brand's colors with zero styling UI exposed to the builder's user.
@@ -258,6 +273,20 @@ const json = serializeTemplate(ref.current.getDocument());
258
273
  const doc = parseTemplate(json); // also accepts bare "View JSON" output; null if not a document
259
274
  ```
260
275
 
276
+ ### Smaller stored documents
277
+
278
+ Drafts and saved templates written through `storage` are compacted automatically: props equal to their default (`hideLabel`, `width`, `verticalAlign`, `labelPosition`, `showIcon`, `required`, the type's default icon, empty placeholders/default values, empty non-`en` translations) are omitted and the document is stamped `compact: 1`. Loading restores them, so older full documents still load unchanged.
279
+
280
+ `getDocument()` still returns the full document. If you store it yourself, compact it first:
281
+
282
+ ```tsx
283
+ import { compactDocument } from "form-page-builder";
284
+
285
+ await save(JSON.stringify(compactDocument(ref.current.getDocument())));
286
+ ```
287
+
288
+ Documents saved in the compact form need this package version (or newer) to be read back correctly, so upgrade every app that reads them together.
289
+
261
290
  ## Using in a plain HTML page (no bundler)
262
291
 
263
292
  There's intentionally no UMD/IIFE global-script build (see below), but you don't need a bundler to use this package — a browser-native ES module setup works too, via an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) and a CDN like [esm.sh](https://esm.sh/):
package/dist/index.cjs CHANGED
@@ -24,9 +24,11 @@ __export(index_exports, {
24
24
  CLIPBOARD_KEY: () => CLIPBOARD_KEY,
25
25
  DARK_THEME: () => DARK_THEME,
26
26
  DEFAULT_THEME: () => DEFAULT_THEME,
27
+ DOCUMENT_VERSION: () => DOCUMENT_VERSION,
27
28
  DRAFT_KEY: () => DRAFT_KEY,
28
29
  FormBuilder: () => FormBuilder_default,
29
30
  INDEX_KEY: () => INDEX_KEY,
31
+ compactDocument: () => compactDocument,
30
32
  default: () => FormBuilder_default,
31
33
  formKey: () => formKey,
32
34
  localStorageAdapter: () => localStorageAdapter,
@@ -221,6 +223,8 @@ var CHROME = {
221
223
  loadingDraft: "Loading your draft...",
222
224
  templatesLimitReached: (max) => `You can save up to ${max} template${max === 1 ? "" : "s"}. Delete one to save another.`,
223
225
  templatesLimitTitle: "Template limit reached",
226
+ fieldCount: (n, max) => `Fields: ${n} / ${max}`,
227
+ fieldLimitReached: (max) => `Field limit reached (${max}). Delete a field to add another.`,
224
228
  confirm: "Confirm",
225
229
  cancel: "Cancel",
226
230
  dismiss: "OK",
@@ -374,6 +378,8 @@ var CHROME = {
374
378
  loadingDraft: "\u4E0B\u66F8\u304D\u3092\u8AAD\u307F\u8FBC\u3093\u3067\u3044\u307E\u3059...",
375
379
  templatesLimitReached: (max) => `\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u306F${max}\u4EF6\u307E\u3067\u4FDD\u5B58\u3067\u304D\u307E\u3059\u30021\u4EF6\u524A\u9664\u3057\u3066\u304B\u3089\u4FDD\u5B58\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
376
380
  templatesLimitTitle: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u306E\u4E0A\u9650\u306B\u9054\u3057\u307E\u3057\u305F",
381
+ fieldCount: (n, max) => `\u9805\u76EE\u6570: ${n} / ${max}`,
382
+ fieldLimitReached: (max) => `\u9805\u76EE\u6570\u306E\u4E0A\u9650\uFF08${max}\u4EF6\uFF09\u306B\u9054\u3057\u307E\u3057\u305F\u3002\u8FFD\u52A0\u3059\u308B\u306B\u306F\u9805\u76EE\u3092\u524A\u9664\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
377
383
  confirm: "\u78BA\u5B9A",
378
384
  cancel: "\u30AD\u30E3\u30F3\u30BB\u30EB",
379
385
  dismiss: "OK",
@@ -583,12 +589,8 @@ function defaultSection(titleEn = "") {
583
589
  return { id: genSectionId(), title: bi(titleEn, ""), background: "", collapsed: false, fields: [] };
584
590
  }
585
591
 
586
- // src/lib/submittable.ts
587
- function sectionHasFormFields(section) {
588
- return section.fields.some((f) => !getMeta(f.type).isContent);
589
- }
590
-
591
592
  // src/lib/migrate.ts
593
+ var DOCUMENT_VERSION = 5;
592
594
  function migrateField(field) {
593
595
  let f = { ...field };
594
596
  if (LEGACY_INPUT_TYPES.includes(f.type)) f = { ...f, type: "input", inputType: f.type };
@@ -614,30 +616,21 @@ function migrateField(field) {
614
616
  f.submitScope = f.submitScope === "section" ? "section" : "form";
615
617
  }
616
618
  f.verticalAlign = f.verticalAlign || "top";
619
+ const meta = getMeta(f.type);
620
+ f.width = f.width || "1/1";
621
+ f.labelPosition = f.labelPosition || "top";
622
+ f.showIcon = !!f.showIcon;
623
+ f.displayIcon = f.displayIcon || meta.defaultIcon;
624
+ if (!meta.isContent) {
625
+ f.required = !!f.required;
626
+ if (meta.placeholder && !f.placeholder) f.placeholder = bi();
627
+ if (f.defaultValue === void 0) f.defaultValue = meta.boolean ? false : meta.multiValue ? [] : "";
628
+ }
617
629
  return f;
618
630
  }
619
631
  function migrateFields(fields) {
620
632
  return (fields || []).map(migrateField);
621
633
  }
622
- function synthesizeButtonField(label, style, scope) {
623
- const resolvedLabel = typeof label === "string" && label ? bi(label) : label && typeof label === "object" && (label.en || label.ja) ? label : bi("Submit", "");
624
- return {
625
- id: nextId(),
626
- type: "button",
627
- label: resolvedLabel,
628
- hideLabel: false,
629
- width: "1/1",
630
- verticalAlign: "top",
631
- labelPosition: "top",
632
- showIcon: false,
633
- displayIcon: "Type",
634
- action: "submit",
635
- submitScope: scope,
636
- buttonStyle: style || { color: "", size: "md" },
637
- href: "",
638
- target: "_self"
639
- };
640
- }
641
634
  function migrateDocument(raw) {
642
635
  if (!raw) return null;
643
636
  const title = typeof raw.title === "string" ? bi(raw.title) : raw.title || bi();
@@ -654,25 +647,6 @@ function migrateDocument(raw) {
654
647
  } else {
655
648
  sections = [{ ...defaultSection(), fields: migrateFields(raw.fields || []) }];
656
649
  }
657
- const legacy = raw.version === void 0 || raw.version < 5;
658
- if (legacy) {
659
- resyncIdCounter(sections.flatMap((s) => s.fields));
660
- const submitMode = raw.submitMode === "perSection" ? "perSection" : "combined";
661
- const submitLabel = raw.submitLabel;
662
- const submitStyle = raw.submitStyle || { color: "", size: "md" };
663
- if (submitMode === "perSection" && raw.sections) {
664
- sections = sections.map((s, i) => {
665
- if (!sectionHasFormFields(s)) return s;
666
- const rawSection = raw.sections[i] || {};
667
- const label = rawSection.submitLabel || submitLabel;
668
- const style = rawSection.submitStyle || submitStyle;
669
- return { ...s, fields: [...s.fields, synthesizeButtonField(label, style, "section")] };
670
- });
671
- } else if (sections.some(sectionHasFormFields)) {
672
- const lastIdx = sections.length - 1;
673
- sections = sections.map((s, i) => i === lastIdx ? { ...s, fields: [...s.fields, synthesizeButtonField(submitLabel, submitStyle, "form")] } : s);
674
- }
675
- }
676
650
  return { title, themeOverrides, sections };
677
651
  }
678
652
 
@@ -700,6 +674,7 @@ function parseTemplate(raw) {
700
674
  var DEFAULT_MAX_TEMPLATES = 5;
701
675
  var DEFAULT_FEATURES = {
702
676
  naming: true,
677
+ formTitle: false,
703
678
  templates: { enabled: true, manage: true, max: DEFAULT_MAX_TEMPLATES },
704
679
  templateClipboard: true,
705
680
  newForm: true,
@@ -903,7 +878,9 @@ function useFormDocument({ language, chrome }) {
903
878
  ...s,
904
879
  fields: s.fields.map((f) => {
905
880
  if (f.id !== fieldId || !("options" in f) || !f.options) return f;
906
- const n = f.options.length + 1;
881
+ const used = new Set(f.options.map((o) => o.value));
882
+ let n = f.options.length + 1;
883
+ while (used.has(`option_${n}`)) n++;
907
884
  return { ...f, options: [...f.options, { label: withLang(bi(), language, chrome.optionSeed(n)), value: `option_${n}` }] };
908
885
  })
909
886
  })));
@@ -911,7 +888,14 @@ function useFormDocument({ language, chrome }) {
911
888
  function removeOption(fieldId, optIdx) {
912
889
  setSections((prev) => prev.map((s) => ({
913
890
  ...s,
914
- fields: s.fields.map((f) => f.id !== fieldId || !("options" in f) || !f.options ? f : { ...f, options: f.options.filter((_, i) => i !== optIdx) })
891
+ fields: s.fields.map((f) => {
892
+ if (f.id !== fieldId || !("options" in f) || !f.options) return f;
893
+ const removed = f.options[optIdx]?.value;
894
+ const options = f.options.filter((_, i) => i !== optIdx);
895
+ if (f.type === "checkboxGroup") return { ...f, options, defaultValue: f.defaultValue.filter((v) => v !== removed) };
896
+ if ((f.type === "select" || f.type === "radio") && f.defaultValue === removed) return { ...f, options, defaultValue: "" };
897
+ return { ...f, options };
898
+ })
915
899
  })));
916
900
  }
917
901
  function loadDocument(doc) {
@@ -971,6 +955,51 @@ var formKey = (id) => `form-page-builder:saved:${id}`;
971
955
  var savedFormId = (key) => key.startsWith("form-page-builder:saved:") ? key.slice("form-page-builder:saved:".length) : null;
972
956
  var CLIPBOARD_KEY = "form-page-builder:clipboard";
973
957
 
958
+ // src/lib/compact.ts
959
+ var BASE_DEFAULTS = {
960
+ hideLabel: false,
961
+ width: "1/1",
962
+ verticalAlign: "top",
963
+ labelPosition: "top",
964
+ showIcon: false,
965
+ required: false
966
+ };
967
+ var COMPACT_VERSION = 1;
968
+ var slimLocalized = (v) => v && typeof v === "object" ? Object.fromEntries(Object.entries(v).filter(([lang, s]) => lang === "en" || s !== "")) : v;
969
+ var isEmptyLocalized = (v) => !!v && typeof v === "object" && Object.values(v).every((s) => !s);
970
+ var isEmptyDefault = (v) => v === "" || v === false || Array.isArray(v) && v.length === 0;
971
+ function compactField(field) {
972
+ const out = {};
973
+ const meta = getMeta(field.type);
974
+ for (const [key, value] of Object.entries(field)) {
975
+ if (key in BASE_DEFAULTS && value === BASE_DEFAULTS[key]) continue;
976
+ if (key === "displayIcon" && value === meta.defaultIcon) continue;
977
+ if (key === "placeholder" && isEmptyLocalized(value)) continue;
978
+ if (key === "defaultValue" && !meta.isContent && isEmptyDefault(value)) continue;
979
+ if (key === "label" || key === "placeholder" || key === "content" || key === "alt") out[key] = slimLocalized(value);
980
+ else if (key === "options" && Array.isArray(value)) out[key] = value.map((o) => ({ ...o, label: slimLocalized(o.label) }));
981
+ else out[key] = value;
982
+ }
983
+ return out;
984
+ }
985
+ function compactDocument(doc) {
986
+ return {
987
+ ...doc,
988
+ compact: COMPACT_VERSION,
989
+ ...doc.title !== void 0 ? { title: slimLocalized(doc.title) } : {},
990
+ sections: doc.sections.map((s) => {
991
+ const { background, collapsed, fields, ...rest } = s;
992
+ return {
993
+ ...rest,
994
+ ...s.title !== void 0 ? { title: slimLocalized(s.title) } : {},
995
+ ...background ? { background } : {},
996
+ ...collapsed ? { collapsed } : {},
997
+ fields: fields.map(compactField)
998
+ };
999
+ })
1000
+ };
1001
+ }
1002
+
974
1003
  // src/hooks/usePersistence.ts
975
1004
  function snapshotOf(d) {
976
1005
  return JSON.stringify({ title: d.title, themeOverrides: d.themeOverrides, sections: d.sections });
@@ -1063,7 +1092,7 @@ function usePersistence({
1063
1092
  if (autosaveTimer.current) clearTimeout(autosaveTimer.current);
1064
1093
  autosaveTimer.current = setTimeout(async () => {
1065
1094
  try {
1066
- await storage.set(DRAFT_KEY, JSON.stringify({ ...document2, currentFormId }));
1095
+ await storage.set(DRAFT_KEY, JSON.stringify({ version: DOCUMENT_VERSION, ...compactDocument(document2), currentFormId }));
1067
1096
  setSaveState("saved");
1068
1097
  } catch {
1069
1098
  setSaveState("error");
@@ -1086,7 +1115,7 @@ function usePersistence({
1086
1115
  try {
1087
1116
  const newTitle = bi(name, "");
1088
1117
  const docToSave = { ...document2, title: newTitle };
1089
- await storage.set(formKey(id), JSON.stringify({ ...docToSave, id, updatedAt: now }));
1118
+ await storage.set(formKey(id), JSON.stringify({ version: DOCUMENT_VERSION, ...compactDocument(docToSave), id, updatedAt: now }));
1090
1119
  const next = [...savedForms, { id, title: name, updatedAt: now }];
1091
1120
  await storage.set(INDEX_KEY, JSON.stringify(next));
1092
1121
  setSavedForms(next);
@@ -1109,7 +1138,7 @@ function usePersistence({
1109
1138
  const now = Date.now();
1110
1139
  setTemplateState("saving");
1111
1140
  try {
1112
- await storage.set(formKey(currentFormId), JSON.stringify({ ...document2, id: currentFormId, updatedAt: now }));
1141
+ await storage.set(formKey(currentFormId), JSON.stringify({ version: DOCUMENT_VERSION, ...compactDocument(document2), id: currentFormId, updatedAt: now }));
1113
1142
  const next = savedForms.map((f) => f.id === currentFormId ? { ...f, title: t(document2.title, "en"), updatedAt: now } : f);
1114
1143
  setSavedForms(next);
1115
1144
  await storage.set(INDEX_KEY, JSON.stringify(next));
@@ -1125,11 +1154,13 @@ function usePersistence({
1125
1154
  try {
1126
1155
  const raw = await storage.get(formKey(id));
1127
1156
  if (!raw) {
1157
+ console.error(`[form-page-builder] loadForm(${id}): storage returned no data for this id`);
1128
1158
  setTemplateState("error");
1129
1159
  return false;
1130
1160
  }
1131
1161
  const doc = migrateDocument(JSON.parse(raw));
1132
1162
  if (!doc) {
1163
+ console.error(`[form-page-builder] loadForm(${id}): stored data failed migrateDocument()`, raw);
1133
1164
  setTemplateState("error");
1134
1165
  return false;
1135
1166
  }
@@ -1141,7 +1172,8 @@ function usePersistence({
1141
1172
  setTemplateState("idle");
1142
1173
  onTemplateChange?.({ id, title: t(doc.title, language), source: "applied" });
1143
1174
  return true;
1144
- } catch {
1175
+ } catch (err) {
1176
+ console.error(`[form-page-builder] loadForm(${id}) threw`, err);
1145
1177
  setTemplateState("error");
1146
1178
  return false;
1147
1179
  }
@@ -1149,6 +1181,7 @@ function usePersistence({
1149
1181
  async function deleteForm(id) {
1150
1182
  if (!templateManage) return;
1151
1183
  const removed = savedForms.find((f) => f.id === id);
1184
+ setTemplateState("loading");
1152
1185
  try {
1153
1186
  await storage.delete(formKey(id));
1154
1187
  const next = savedForms.filter((f) => f.id !== id);
@@ -1158,6 +1191,7 @@ function usePersistence({
1158
1191
  setCurrentFormId(null);
1159
1192
  savedSnapshot.current = null;
1160
1193
  }
1194
+ setTemplateState("idle");
1161
1195
  onTemplateChange?.({ id: currentFormId === id ? null : id, title: removed?.title ?? "", source: "deleted" });
1162
1196
  } catch {
1163
1197
  setTemplateState("error");
@@ -1665,6 +1699,7 @@ function Palette({
1665
1699
  ] }),
1666
1700
  formTypes.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1667
1701
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...styles.panelHeading, marginTop: 16 }, children: chrome.formFields }),
1702
+ typeof features.maxFields === "number" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { role: "status", style: { ...styles.activeSectionHint, marginBottom: 8, ...atFieldCap ? { color: "var(--fb-danger)" } : {} }, children: atFieldCap ? chrome.fieldLimitReached(features.maxFields) : chrome.fieldCount(fieldCount, features.maxFields) }),
1668
1703
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: styles.paletteList, children: formTypes.map((f) => {
1669
1704
  const Icon = f.icon;
1670
1705
  const itemStyle = atFieldCap ? { ...styles.paletteItem, opacity: 0.45, cursor: "not-allowed" } : styles.paletteItem;
@@ -1910,6 +1945,8 @@ function FieldTicket({
1910
1945
  {
1911
1946
  ...dropZoneHandlers,
1912
1947
  onClick: onSelect,
1948
+ className: `fb-field fb-field--${field.type} fb-field--${field.id}`,
1949
+ "data-field-id": field.id,
1913
1950
  style: {
1914
1951
  ...styles.ticket,
1915
1952
  flex: `1 1 calc(${widthPct} - 10px)`,
@@ -2439,16 +2476,16 @@ function PreviewPane({ title, sections, onFieldChange, language, strings, chrome
2439
2476
  return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: styles.previewWrap, children: [
2440
2477
  /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: { width: "100%", maxWidth, display: "flex", flexDirection: "column", alignItems: "center" }, children: [
2441
2478
  features.deviceToggle && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: styles.previewToolbar, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Segmented, { options: deviceOptions, value: device, onChange: setDevice }) }),
2442
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: { ...styles.previewCard, maxWidth, width: "100%" }, children: [
2443
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h2", { style: styles.previewTitle, children: t(title, language) }),
2479
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "fb-form", style: { ...styles.previewCard, maxWidth, width: "100%" }, children: [
2480
+ features.formTitle && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h2", { style: styles.previewTitle, children: t(title, language) }),
2444
2481
  allFields.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { style: { color: "var(--fb-muted)", fontSize: 14 }, children: strings.addFieldsHint }),
2445
2482
  sections.map((section) => {
2446
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: { background: section.background || "transparent", padding: section.background ? 16 : 0, borderRadius: section.background ? 10 : 0, marginBottom: "var(--fb-space-section)" }, children: [
2483
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "fb-section", "data-section-id": section.id, style: { background: section.background || "transparent", padding: section.background ? 16 : 0, borderRadius: section.background ? 10 : 0, marginBottom: "var(--fb-space-section)" }, children: [
2447
2484
  t(section.title, language) && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h3", { style: styles.sectionRuntimeTitle, children: t(section.title, language) }),
2448
2485
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: styles.previewGrid, children: section.fields.map((field) => {
2449
2486
  const width = effectiveWidth(field.width, device);
2450
2487
  const flexBasis = `1 1 calc(${WIDTH_PERCENT[width]} - 14px)`;
2451
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: { flex: flexBasis, minWidth: 0, alignSelf: ALIGN_MAP[field.verticalAlign || "top"] }, children: [
2488
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: `fb-field fb-field--${field.type} fb-field--${field.id}`, "data-field-id": field.id, style: { flex: flexBasis, minWidth: 0, alignSelf: ALIGN_MAP[field.verticalAlign || "top"] }, children: [
2452
2489
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(FieldBlock, { field, lang: language, onFieldChange, strings, chrome, error: errors[field.id], onButtonAction: handleButtonAction }),
2453
2490
  field.type === "button" && field.id === attemptedButtonId && Object.keys(errors).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("p", { style: styles.formErrorNote, children: [
2454
2491
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react14.CircleAlert, { size: 13 }),
@@ -2743,7 +2780,7 @@ var FormBuilder = (0, import_react10.forwardRef)(function FormBuilder2({
2743
2780
  ensureActiveSection: () => doc.setActiveSectionId((prev) => prev ?? doc.sections[0]?.id ?? null)
2744
2781
  });
2745
2782
  const jsonDoc = (0, import_react10.useMemo)(() => ({
2746
- version: 5,
2783
+ version: DOCUMENT_VERSION,
2747
2784
  title: doc.title,
2748
2785
  theme,
2749
2786
  themeOverrides,
@@ -3021,9 +3058,11 @@ var DARK_THEME = {
3021
3058
  CLIPBOARD_KEY,
3022
3059
  DARK_THEME,
3023
3060
  DEFAULT_THEME,
3061
+ DOCUMENT_VERSION,
3024
3062
  DRAFT_KEY,
3025
3063
  FormBuilder,
3026
3064
  INDEX_KEY,
3065
+ compactDocument,
3027
3066
  formKey,
3028
3067
  localStorageAdapter,
3029
3068
  parseTemplate,