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/dist/index.d.cts CHANGED
@@ -119,6 +119,8 @@ declare const CHROME: {
119
119
  loadingDraft: string;
120
120
  templatesLimitReached: (max: number) => string;
121
121
  templatesLimitTitle: string;
122
+ fieldCount: (n: number, max: number) => string;
123
+ fieldLimitReached: (max: number) => string;
122
124
  confirm: string;
123
125
  cancel: string;
124
126
  dismiss: string;
@@ -272,6 +274,8 @@ declare const CHROME: {
272
274
  loadingDraft: string;
273
275
  templatesLimitReached: (max: number) => string;
274
276
  templatesLimitTitle: string;
277
+ fieldCount: (n: number, max: number) => string;
278
+ fieldLimitReached: (max: number) => string;
275
279
  confirm: string;
276
280
  cancel: string;
277
281
  dismiss: string;
@@ -619,6 +623,8 @@ interface TemplateChange {
619
623
  interface FormBuilderFeatures {
620
624
  /** Editable form title input in the Toolbar. Default true. */
621
625
  naming?: boolean;
626
+ /** Render the form title as a heading above the fields in Preview. Default false — the host usually shows the form name in its own chrome. */
627
+ formTitle?: boolean;
622
628
  /**
623
629
  * Templates library. `true` (default) = full CRUD + toolbar "Save"; `false` = hidden.
624
630
  * `{ manage: false }` = pick-and-apply only (no save/overwrite/delete). `{ max }` caps the count (default 5).
@@ -698,6 +704,25 @@ declare function serializeTemplate(document: FormDocument): string;
698
704
  /** Accepts a `serializeTemplate` envelope or a bare document (e.g. "View JSON" output); `null` if not a document. */
699
705
  declare function parseTemplate(raw: string | null | undefined): DocumentFields | null;
700
706
 
707
+ /** Current document schema version — stamped on everything persisted. */
708
+ declare const DOCUMENT_VERSION: 5;
709
+
710
+ /**
711
+ * Drops every field/section prop that equals its default, so persisted documents are smaller.
712
+ * Lossless: `migrateDocument` restores the defaults, so `migrateDocument(compactDocument(d))` equals `migrateDocument(d)`.
713
+ * Empty non-`en` translations are dropped too, and the result carries `compact: COMPACT_VERSION`.
714
+ * Extra top-level keys (`version`, `theme`, `themeOverrides`, …) pass through untouched.
715
+ */
716
+ declare function compactDocument<T extends {
717
+ title?: unknown;
718
+ sections: {
719
+ title?: unknown;
720
+ fields: FormField[];
721
+ background?: string;
722
+ collapsed?: boolean;
723
+ }[];
724
+ }>(doc: T): T;
725
+
701
726
  declare const DRAFT_KEY = "form-page-builder:draft";
702
727
  declare const INDEX_KEY = "form-page-builder:index";
703
728
  declare const formKey: (id: string) => string;
@@ -706,4 +731,4 @@ declare const savedFormId: (key: string) => string | null;
706
731
  /** Copy/paste-template scratch slot — always localStorage, never the StorageAdapter. */
707
732
  declare const CLIPBOARD_KEY = "form-page-builder:clipboard";
708
733
 
709
- export { CLIPBOARD_KEY, type CheckboxField, type CheckboxGroupField, type ChromeShape, type ContentBlockType, DARK_THEME, DEFAULT_THEME, DRAFT_KEY, type DocumentFields, type FieldPatch, type FieldType, FormBuilder, type FormBuilderFeatures, type FormBuilderHandle, type FormBuilderProps, type FormDocument, type FormField, type FormTemplate, INDEX_KEY, type ImageField, type InputField, type InputFieldType, type LanguageOption, type LocalizedString, type Option, type ParagraphField, type RadioField, type SavedFormMeta, type Section, type SelectField, type StorageAdapter, type StringsShape, type SubmitPayload, type SubmitStyle, type TemplateChange, type TextareaField, type Theme, type ThemeLayout, type ThemeOverrides, type ToggleField, FormBuilder as default, formKey, localStorageAdapter, parseTemplate, savedFormId, serializeTemplate };
734
+ export { CLIPBOARD_KEY, type CheckboxField, type CheckboxGroupField, type ChromeShape, type ContentBlockType, DARK_THEME, DEFAULT_THEME, DOCUMENT_VERSION, DRAFT_KEY, type DocumentFields, type FieldPatch, type FieldType, FormBuilder, type FormBuilderFeatures, type FormBuilderHandle, type FormBuilderProps, type FormDocument, type FormField, type FormTemplate, INDEX_KEY, type ImageField, type InputField, type InputFieldType, type LanguageOption, type LocalizedString, type Option, type ParagraphField, type RadioField, type SavedFormMeta, type Section, type SelectField, type StorageAdapter, type StringsShape, type SubmitPayload, type SubmitStyle, type TemplateChange, type TextareaField, type Theme, type ThemeLayout, type ThemeOverrides, type ToggleField, compactDocument, FormBuilder as default, formKey, localStorageAdapter, parseTemplate, savedFormId, serializeTemplate };
package/dist/index.d.ts CHANGED
@@ -119,6 +119,8 @@ declare const CHROME: {
119
119
  loadingDraft: string;
120
120
  templatesLimitReached: (max: number) => string;
121
121
  templatesLimitTitle: string;
122
+ fieldCount: (n: number, max: number) => string;
123
+ fieldLimitReached: (max: number) => string;
122
124
  confirm: string;
123
125
  cancel: string;
124
126
  dismiss: string;
@@ -272,6 +274,8 @@ declare const CHROME: {
272
274
  loadingDraft: string;
273
275
  templatesLimitReached: (max: number) => string;
274
276
  templatesLimitTitle: string;
277
+ fieldCount: (n: number, max: number) => string;
278
+ fieldLimitReached: (max: number) => string;
275
279
  confirm: string;
276
280
  cancel: string;
277
281
  dismiss: string;
@@ -619,6 +623,8 @@ interface TemplateChange {
619
623
  interface FormBuilderFeatures {
620
624
  /** Editable form title input in the Toolbar. Default true. */
621
625
  naming?: boolean;
626
+ /** Render the form title as a heading above the fields in Preview. Default false — the host usually shows the form name in its own chrome. */
627
+ formTitle?: boolean;
622
628
  /**
623
629
  * Templates library. `true` (default) = full CRUD + toolbar "Save"; `false` = hidden.
624
630
  * `{ manage: false }` = pick-and-apply only (no save/overwrite/delete). `{ max }` caps the count (default 5).
@@ -698,6 +704,25 @@ declare function serializeTemplate(document: FormDocument): string;
698
704
  /** Accepts a `serializeTemplate` envelope or a bare document (e.g. "View JSON" output); `null` if not a document. */
699
705
  declare function parseTemplate(raw: string | null | undefined): DocumentFields | null;
700
706
 
707
+ /** Current document schema version — stamped on everything persisted. */
708
+ declare const DOCUMENT_VERSION: 5;
709
+
710
+ /**
711
+ * Drops every field/section prop that equals its default, so persisted documents are smaller.
712
+ * Lossless: `migrateDocument` restores the defaults, so `migrateDocument(compactDocument(d))` equals `migrateDocument(d)`.
713
+ * Empty non-`en` translations are dropped too, and the result carries `compact: COMPACT_VERSION`.
714
+ * Extra top-level keys (`version`, `theme`, `themeOverrides`, …) pass through untouched.
715
+ */
716
+ declare function compactDocument<T extends {
717
+ title?: unknown;
718
+ sections: {
719
+ title?: unknown;
720
+ fields: FormField[];
721
+ background?: string;
722
+ collapsed?: boolean;
723
+ }[];
724
+ }>(doc: T): T;
725
+
701
726
  declare const DRAFT_KEY = "form-page-builder:draft";
702
727
  declare const INDEX_KEY = "form-page-builder:index";
703
728
  declare const formKey: (id: string) => string;
@@ -706,4 +731,4 @@ declare const savedFormId: (key: string) => string | null;
706
731
  /** Copy/paste-template scratch slot — always localStorage, never the StorageAdapter. */
707
732
  declare const CLIPBOARD_KEY = "form-page-builder:clipboard";
708
733
 
709
- export { CLIPBOARD_KEY, type CheckboxField, type CheckboxGroupField, type ChromeShape, type ContentBlockType, DARK_THEME, DEFAULT_THEME, DRAFT_KEY, type DocumentFields, type FieldPatch, type FieldType, FormBuilder, type FormBuilderFeatures, type FormBuilderHandle, type FormBuilderProps, type FormDocument, type FormField, type FormTemplate, INDEX_KEY, type ImageField, type InputField, type InputFieldType, type LanguageOption, type LocalizedString, type Option, type ParagraphField, type RadioField, type SavedFormMeta, type Section, type SelectField, type StorageAdapter, type StringsShape, type SubmitPayload, type SubmitStyle, type TemplateChange, type TextareaField, type Theme, type ThemeLayout, type ThemeOverrides, type ToggleField, FormBuilder as default, formKey, localStorageAdapter, parseTemplate, savedFormId, serializeTemplate };
734
+ export { CLIPBOARD_KEY, type CheckboxField, type CheckboxGroupField, type ChromeShape, type ContentBlockType, DARK_THEME, DEFAULT_THEME, DOCUMENT_VERSION, DRAFT_KEY, type DocumentFields, type FieldPatch, type FieldType, FormBuilder, type FormBuilderFeatures, type FormBuilderHandle, type FormBuilderProps, type FormDocument, type FormField, type FormTemplate, INDEX_KEY, type ImageField, type InputField, type InputFieldType, type LanguageOption, type LocalizedString, type Option, type ParagraphField, type RadioField, type SavedFormMeta, type Section, type SelectField, type StorageAdapter, type StringsShape, type SubmitPayload, type SubmitStyle, type TemplateChange, type TextareaField, type Theme, type ThemeLayout, type ThemeOverrides, type ToggleField, compactDocument, FormBuilder as default, formKey, localStorageAdapter, parseTemplate, savedFormId, serializeTemplate };
package/dist/index.js CHANGED
@@ -196,6 +196,8 @@ var CHROME = {
196
196
  loadingDraft: "Loading your draft...",
197
197
  templatesLimitReached: (max) => `You can save up to ${max} template${max === 1 ? "" : "s"}. Delete one to save another.`,
198
198
  templatesLimitTitle: "Template limit reached",
199
+ fieldCount: (n, max) => `Fields: ${n} / ${max}`,
200
+ fieldLimitReached: (max) => `Field limit reached (${max}). Delete a field to add another.`,
199
201
  confirm: "Confirm",
200
202
  cancel: "Cancel",
201
203
  dismiss: "OK",
@@ -349,6 +351,8 @@ var CHROME = {
349
351
  loadingDraft: "\u4E0B\u66F8\u304D\u3092\u8AAD\u307F\u8FBC\u3093\u3067\u3044\u307E\u3059...",
350
352
  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`,
351
353
  templatesLimitTitle: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u306E\u4E0A\u9650\u306B\u9054\u3057\u307E\u3057\u305F",
354
+ fieldCount: (n, max) => `\u9805\u76EE\u6570: ${n} / ${max}`,
355
+ 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`,
352
356
  confirm: "\u78BA\u5B9A",
353
357
  cancel: "\u30AD\u30E3\u30F3\u30BB\u30EB",
354
358
  dismiss: "OK",
@@ -558,12 +562,8 @@ function defaultSection(titleEn = "") {
558
562
  return { id: genSectionId(), title: bi(titleEn, ""), background: "", collapsed: false, fields: [] };
559
563
  }
560
564
 
561
- // src/lib/submittable.ts
562
- function sectionHasFormFields(section) {
563
- return section.fields.some((f) => !getMeta(f.type).isContent);
564
- }
565
-
566
565
  // src/lib/migrate.ts
566
+ var DOCUMENT_VERSION = 5;
567
567
  function migrateField(field) {
568
568
  let f = { ...field };
569
569
  if (LEGACY_INPUT_TYPES.includes(f.type)) f = { ...f, type: "input", inputType: f.type };
@@ -589,30 +589,21 @@ function migrateField(field) {
589
589
  f.submitScope = f.submitScope === "section" ? "section" : "form";
590
590
  }
591
591
  f.verticalAlign = f.verticalAlign || "top";
592
+ const meta = getMeta(f.type);
593
+ f.width = f.width || "1/1";
594
+ f.labelPosition = f.labelPosition || "top";
595
+ f.showIcon = !!f.showIcon;
596
+ f.displayIcon = f.displayIcon || meta.defaultIcon;
597
+ if (!meta.isContent) {
598
+ f.required = !!f.required;
599
+ if (meta.placeholder && !f.placeholder) f.placeholder = bi();
600
+ if (f.defaultValue === void 0) f.defaultValue = meta.boolean ? false : meta.multiValue ? [] : "";
601
+ }
592
602
  return f;
593
603
  }
594
604
  function migrateFields(fields) {
595
605
  return (fields || []).map(migrateField);
596
606
  }
597
- function synthesizeButtonField(label, style, scope) {
598
- const resolvedLabel = typeof label === "string" && label ? bi(label) : label && typeof label === "object" && (label.en || label.ja) ? label : bi("Submit", "");
599
- return {
600
- id: nextId(),
601
- type: "button",
602
- label: resolvedLabel,
603
- hideLabel: false,
604
- width: "1/1",
605
- verticalAlign: "top",
606
- labelPosition: "top",
607
- showIcon: false,
608
- displayIcon: "Type",
609
- action: "submit",
610
- submitScope: scope,
611
- buttonStyle: style || { color: "", size: "md" },
612
- href: "",
613
- target: "_self"
614
- };
615
- }
616
607
  function migrateDocument(raw) {
617
608
  if (!raw) return null;
618
609
  const title = typeof raw.title === "string" ? bi(raw.title) : raw.title || bi();
@@ -629,25 +620,6 @@ function migrateDocument(raw) {
629
620
  } else {
630
621
  sections = [{ ...defaultSection(), fields: migrateFields(raw.fields || []) }];
631
622
  }
632
- const legacy = raw.version === void 0 || raw.version < 5;
633
- if (legacy) {
634
- resyncIdCounter(sections.flatMap((s) => s.fields));
635
- const submitMode = raw.submitMode === "perSection" ? "perSection" : "combined";
636
- const submitLabel = raw.submitLabel;
637
- const submitStyle = raw.submitStyle || { color: "", size: "md" };
638
- if (submitMode === "perSection" && raw.sections) {
639
- sections = sections.map((s, i) => {
640
- if (!sectionHasFormFields(s)) return s;
641
- const rawSection = raw.sections[i] || {};
642
- const label = rawSection.submitLabel || submitLabel;
643
- const style = rawSection.submitStyle || submitStyle;
644
- return { ...s, fields: [...s.fields, synthesizeButtonField(label, style, "section")] };
645
- });
646
- } else if (sections.some(sectionHasFormFields)) {
647
- const lastIdx = sections.length - 1;
648
- sections = sections.map((s, i) => i === lastIdx ? { ...s, fields: [...s.fields, synthesizeButtonField(submitLabel, submitStyle, "form")] } : s);
649
- }
650
- }
651
623
  return { title, themeOverrides, sections };
652
624
  }
653
625
 
@@ -675,6 +647,7 @@ function parseTemplate(raw) {
675
647
  var DEFAULT_MAX_TEMPLATES = 5;
676
648
  var DEFAULT_FEATURES = {
677
649
  naming: true,
650
+ formTitle: false,
678
651
  templates: { enabled: true, manage: true, max: DEFAULT_MAX_TEMPLATES },
679
652
  templateClipboard: true,
680
653
  newForm: true,
@@ -878,7 +851,9 @@ function useFormDocument({ language, chrome }) {
878
851
  ...s,
879
852
  fields: s.fields.map((f) => {
880
853
  if (f.id !== fieldId || !("options" in f) || !f.options) return f;
881
- const n = f.options.length + 1;
854
+ const used = new Set(f.options.map((o) => o.value));
855
+ let n = f.options.length + 1;
856
+ while (used.has(`option_${n}`)) n++;
882
857
  return { ...f, options: [...f.options, { label: withLang(bi(), language, chrome.optionSeed(n)), value: `option_${n}` }] };
883
858
  })
884
859
  })));
@@ -886,7 +861,14 @@ function useFormDocument({ language, chrome }) {
886
861
  function removeOption(fieldId, optIdx) {
887
862
  setSections((prev) => prev.map((s) => ({
888
863
  ...s,
889
- fields: s.fields.map((f) => f.id !== fieldId || !("options" in f) || !f.options ? f : { ...f, options: f.options.filter((_, i) => i !== optIdx) })
864
+ fields: s.fields.map((f) => {
865
+ if (f.id !== fieldId || !("options" in f) || !f.options) return f;
866
+ const removed = f.options[optIdx]?.value;
867
+ const options = f.options.filter((_, i) => i !== optIdx);
868
+ if (f.type === "checkboxGroup") return { ...f, options, defaultValue: f.defaultValue.filter((v) => v !== removed) };
869
+ if ((f.type === "select" || f.type === "radio") && f.defaultValue === removed) return { ...f, options, defaultValue: "" };
870
+ return { ...f, options };
871
+ })
890
872
  })));
891
873
  }
892
874
  function loadDocument(doc) {
@@ -946,6 +928,51 @@ var formKey = (id) => `form-page-builder:saved:${id}`;
946
928
  var savedFormId = (key) => key.startsWith("form-page-builder:saved:") ? key.slice("form-page-builder:saved:".length) : null;
947
929
  var CLIPBOARD_KEY = "form-page-builder:clipboard";
948
930
 
931
+ // src/lib/compact.ts
932
+ var BASE_DEFAULTS = {
933
+ hideLabel: false,
934
+ width: "1/1",
935
+ verticalAlign: "top",
936
+ labelPosition: "top",
937
+ showIcon: false,
938
+ required: false
939
+ };
940
+ var COMPACT_VERSION = 1;
941
+ var slimLocalized = (v) => v && typeof v === "object" ? Object.fromEntries(Object.entries(v).filter(([lang, s]) => lang === "en" || s !== "")) : v;
942
+ var isEmptyLocalized = (v) => !!v && typeof v === "object" && Object.values(v).every((s) => !s);
943
+ var isEmptyDefault = (v) => v === "" || v === false || Array.isArray(v) && v.length === 0;
944
+ function compactField(field) {
945
+ const out = {};
946
+ const meta = getMeta(field.type);
947
+ for (const [key, value] of Object.entries(field)) {
948
+ if (key in BASE_DEFAULTS && value === BASE_DEFAULTS[key]) continue;
949
+ if (key === "displayIcon" && value === meta.defaultIcon) continue;
950
+ if (key === "placeholder" && isEmptyLocalized(value)) continue;
951
+ if (key === "defaultValue" && !meta.isContent && isEmptyDefault(value)) continue;
952
+ if (key === "label" || key === "placeholder" || key === "content" || key === "alt") out[key] = slimLocalized(value);
953
+ else if (key === "options" && Array.isArray(value)) out[key] = value.map((o) => ({ ...o, label: slimLocalized(o.label) }));
954
+ else out[key] = value;
955
+ }
956
+ return out;
957
+ }
958
+ function compactDocument(doc) {
959
+ return {
960
+ ...doc,
961
+ compact: COMPACT_VERSION,
962
+ ...doc.title !== void 0 ? { title: slimLocalized(doc.title) } : {},
963
+ sections: doc.sections.map((s) => {
964
+ const { background, collapsed, fields, ...rest } = s;
965
+ return {
966
+ ...rest,
967
+ ...s.title !== void 0 ? { title: slimLocalized(s.title) } : {},
968
+ ...background ? { background } : {},
969
+ ...collapsed ? { collapsed } : {},
970
+ fields: fields.map(compactField)
971
+ };
972
+ })
973
+ };
974
+ }
975
+
949
976
  // src/hooks/usePersistence.ts
950
977
  function snapshotOf(d) {
951
978
  return JSON.stringify({ title: d.title, themeOverrides: d.themeOverrides, sections: d.sections });
@@ -1038,7 +1065,7 @@ function usePersistence({
1038
1065
  if (autosaveTimer.current) clearTimeout(autosaveTimer.current);
1039
1066
  autosaveTimer.current = setTimeout(async () => {
1040
1067
  try {
1041
- await storage.set(DRAFT_KEY, JSON.stringify({ ...document2, currentFormId }));
1068
+ await storage.set(DRAFT_KEY, JSON.stringify({ version: DOCUMENT_VERSION, ...compactDocument(document2), currentFormId }));
1042
1069
  setSaveState("saved");
1043
1070
  } catch {
1044
1071
  setSaveState("error");
@@ -1061,7 +1088,7 @@ function usePersistence({
1061
1088
  try {
1062
1089
  const newTitle = bi(name, "");
1063
1090
  const docToSave = { ...document2, title: newTitle };
1064
- await storage.set(formKey(id), JSON.stringify({ ...docToSave, id, updatedAt: now }));
1091
+ await storage.set(formKey(id), JSON.stringify({ version: DOCUMENT_VERSION, ...compactDocument(docToSave), id, updatedAt: now }));
1065
1092
  const next = [...savedForms, { id, title: name, updatedAt: now }];
1066
1093
  await storage.set(INDEX_KEY, JSON.stringify(next));
1067
1094
  setSavedForms(next);
@@ -1084,7 +1111,7 @@ function usePersistence({
1084
1111
  const now = Date.now();
1085
1112
  setTemplateState("saving");
1086
1113
  try {
1087
- await storage.set(formKey(currentFormId), JSON.stringify({ ...document2, id: currentFormId, updatedAt: now }));
1114
+ await storage.set(formKey(currentFormId), JSON.stringify({ version: DOCUMENT_VERSION, ...compactDocument(document2), id: currentFormId, updatedAt: now }));
1088
1115
  const next = savedForms.map((f) => f.id === currentFormId ? { ...f, title: t(document2.title, "en"), updatedAt: now } : f);
1089
1116
  setSavedForms(next);
1090
1117
  await storage.set(INDEX_KEY, JSON.stringify(next));
@@ -1100,11 +1127,13 @@ function usePersistence({
1100
1127
  try {
1101
1128
  const raw = await storage.get(formKey(id));
1102
1129
  if (!raw) {
1130
+ console.error(`[form-page-builder] loadForm(${id}): storage returned no data for this id`);
1103
1131
  setTemplateState("error");
1104
1132
  return false;
1105
1133
  }
1106
1134
  const doc = migrateDocument(JSON.parse(raw));
1107
1135
  if (!doc) {
1136
+ console.error(`[form-page-builder] loadForm(${id}): stored data failed migrateDocument()`, raw);
1108
1137
  setTemplateState("error");
1109
1138
  return false;
1110
1139
  }
@@ -1116,7 +1145,8 @@ function usePersistence({
1116
1145
  setTemplateState("idle");
1117
1146
  onTemplateChange?.({ id, title: t(doc.title, language), source: "applied" });
1118
1147
  return true;
1119
- } catch {
1148
+ } catch (err) {
1149
+ console.error(`[form-page-builder] loadForm(${id}) threw`, err);
1120
1150
  setTemplateState("error");
1121
1151
  return false;
1122
1152
  }
@@ -1124,6 +1154,7 @@ function usePersistence({
1124
1154
  async function deleteForm(id) {
1125
1155
  if (!templateManage) return;
1126
1156
  const removed = savedForms.find((f) => f.id === id);
1157
+ setTemplateState("loading");
1127
1158
  try {
1128
1159
  await storage.delete(formKey(id));
1129
1160
  const next = savedForms.filter((f) => f.id !== id);
@@ -1133,6 +1164,7 @@ function usePersistence({
1133
1164
  setCurrentFormId(null);
1134
1165
  savedSnapshot.current = null;
1135
1166
  }
1167
+ setTemplateState("idle");
1136
1168
  onTemplateChange?.({ id: currentFormId === id ? null : id, title: removed?.title ?? "", source: "deleted" });
1137
1169
  } catch {
1138
1170
  setTemplateState("error");
@@ -1640,6 +1672,7 @@ function Palette({
1640
1672
  ] }),
1641
1673
  formTypes.length > 0 && /* @__PURE__ */ jsxs5(Fragment2, { children: [
1642
1674
  /* @__PURE__ */ jsx5("div", { style: { ...styles.panelHeading, marginTop: 16 }, children: chrome.formFields }),
1675
+ typeof features.maxFields === "number" && /* @__PURE__ */ jsx5("div", { role: "status", style: { ...styles.activeSectionHint, marginBottom: 8, ...atFieldCap ? { color: "var(--fb-danger)" } : {} }, children: atFieldCap ? chrome.fieldLimitReached(features.maxFields) : chrome.fieldCount(fieldCount, features.maxFields) }),
1643
1676
  /* @__PURE__ */ jsx5("div", { style: styles.paletteList, children: formTypes.map((f) => {
1644
1677
  const Icon = f.icon;
1645
1678
  const itemStyle = atFieldCap ? { ...styles.paletteItem, opacity: 0.45, cursor: "not-allowed" } : styles.paletteItem;
@@ -1909,6 +1942,8 @@ function FieldTicket({
1909
1942
  {
1910
1943
  ...dropZoneHandlers,
1911
1944
  onClick: onSelect,
1945
+ className: `fb-field fb-field--${field.type} fb-field--${field.id}`,
1946
+ "data-field-id": field.id,
1912
1947
  style: {
1913
1948
  ...styles.ticket,
1914
1949
  flex: `1 1 calc(${widthPct} - 10px)`,
@@ -2438,16 +2473,16 @@ function PreviewPane({ title, sections, onFieldChange, language, strings, chrome
2438
2473
  return /* @__PURE__ */ jsxs12("div", { style: styles.previewWrap, children: [
2439
2474
  /* @__PURE__ */ jsxs12("div", { style: { width: "100%", maxWidth, display: "flex", flexDirection: "column", alignItems: "center" }, children: [
2440
2475
  features.deviceToggle && /* @__PURE__ */ jsx14("div", { style: styles.previewToolbar, children: /* @__PURE__ */ jsx14(Segmented, { options: deviceOptions, value: device, onChange: setDevice }) }),
2441
- /* @__PURE__ */ jsxs12("div", { style: { ...styles.previewCard, maxWidth, width: "100%" }, children: [
2442
- /* @__PURE__ */ jsx14("h2", { style: styles.previewTitle, children: t(title, language) }),
2476
+ /* @__PURE__ */ jsxs12("div", { className: "fb-form", style: { ...styles.previewCard, maxWidth, width: "100%" }, children: [
2477
+ features.formTitle && /* @__PURE__ */ jsx14("h2", { style: styles.previewTitle, children: t(title, language) }),
2443
2478
  allFields.length === 0 && /* @__PURE__ */ jsx14("p", { style: { color: "var(--fb-muted)", fontSize: 14 }, children: strings.addFieldsHint }),
2444
2479
  sections.map((section) => {
2445
- return /* @__PURE__ */ jsxs12("div", { style: { background: section.background || "transparent", padding: section.background ? 16 : 0, borderRadius: section.background ? 10 : 0, marginBottom: "var(--fb-space-section)" }, children: [
2480
+ return /* @__PURE__ */ jsxs12("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: [
2446
2481
  t(section.title, language) && /* @__PURE__ */ jsx14("h3", { style: styles.sectionRuntimeTitle, children: t(section.title, language) }),
2447
2482
  /* @__PURE__ */ jsx14("div", { style: styles.previewGrid, children: section.fields.map((field) => {
2448
2483
  const width = effectiveWidth(field.width, device);
2449
2484
  const flexBasis = `1 1 calc(${WIDTH_PERCENT[width]} - 14px)`;
2450
- return /* @__PURE__ */ jsxs12("div", { style: { flex: flexBasis, minWidth: 0, alignSelf: ALIGN_MAP[field.verticalAlign || "top"] }, children: [
2485
+ return /* @__PURE__ */ jsxs12("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: [
2451
2486
  /* @__PURE__ */ jsx14(FieldBlock, { field, lang: language, onFieldChange, strings, chrome, error: errors[field.id], onButtonAction: handleButtonAction }),
2452
2487
  field.type === "button" && field.id === attemptedButtonId && Object.keys(errors).length > 0 && /* @__PURE__ */ jsxs12("p", { style: styles.formErrorNote, children: [
2453
2488
  /* @__PURE__ */ jsx14(CircleAlert2, { size: 13 }),
@@ -2742,7 +2777,7 @@ var FormBuilder = forwardRef(function FormBuilder2({
2742
2777
  ensureActiveSection: () => doc.setActiveSectionId((prev) => prev ?? doc.sections[0]?.id ?? null)
2743
2778
  });
2744
2779
  const jsonDoc = useMemo2(() => ({
2745
- version: 5,
2780
+ version: DOCUMENT_VERSION,
2746
2781
  title: doc.title,
2747
2782
  theme,
2748
2783
  themeOverrides,
@@ -3019,9 +3054,11 @@ export {
3019
3054
  CLIPBOARD_KEY,
3020
3055
  DARK_THEME,
3021
3056
  DEFAULT_THEME,
3057
+ DOCUMENT_VERSION,
3022
3058
  DRAFT_KEY,
3023
3059
  FormBuilder_default as FormBuilder,
3024
3060
  INDEX_KEY,
3061
+ compactDocument,
3025
3062
  FormBuilder_default as default,
3026
3063
  formKey,
3027
3064
  localStorageAdapter,