form-page-builder 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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. |
@@ -126,6 +127,20 @@ Every optional UI surface can be switched on or off independently through one `f
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.
package/dist/index.cjs CHANGED
@@ -24,6 +24,7 @@ __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,
@@ -583,12 +584,8 @@ function defaultSection(titleEn = "") {
583
584
  return { id: genSectionId(), title: bi(titleEn, ""), background: "", collapsed: false, fields: [] };
584
585
  }
585
586
 
586
- // src/lib/submittable.ts
587
- function sectionHasFormFields(section) {
588
- return section.fields.some((f) => !getMeta(f.type).isContent);
589
- }
590
-
591
587
  // src/lib/migrate.ts
588
+ var DOCUMENT_VERSION = 5;
592
589
  function migrateField(field) {
593
590
  let f = { ...field };
594
591
  if (LEGACY_INPUT_TYPES.includes(f.type)) f = { ...f, type: "input", inputType: f.type };
@@ -619,25 +616,6 @@ function migrateField(field) {
619
616
  function migrateFields(fields) {
620
617
  return (fields || []).map(migrateField);
621
618
  }
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
619
  function migrateDocument(raw) {
642
620
  if (!raw) return null;
643
621
  const title = typeof raw.title === "string" ? bi(raw.title) : raw.title || bi();
@@ -654,25 +632,6 @@ function migrateDocument(raw) {
654
632
  } else {
655
633
  sections = [{ ...defaultSection(), fields: migrateFields(raw.fields || []) }];
656
634
  }
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
635
  return { title, themeOverrides, sections };
677
636
  }
678
637
 
@@ -700,6 +659,7 @@ function parseTemplate(raw) {
700
659
  var DEFAULT_MAX_TEMPLATES = 5;
701
660
  var DEFAULT_FEATURES = {
702
661
  naming: true,
662
+ formTitle: false,
703
663
  templates: { enabled: true, manage: true, max: DEFAULT_MAX_TEMPLATES },
704
664
  templateClipboard: true,
705
665
  newForm: true,
@@ -1063,7 +1023,7 @@ function usePersistence({
1063
1023
  if (autosaveTimer.current) clearTimeout(autosaveTimer.current);
1064
1024
  autosaveTimer.current = setTimeout(async () => {
1065
1025
  try {
1066
- await storage.set(DRAFT_KEY, JSON.stringify({ ...document2, currentFormId }));
1026
+ await storage.set(DRAFT_KEY, JSON.stringify({ version: DOCUMENT_VERSION, ...document2, currentFormId }));
1067
1027
  setSaveState("saved");
1068
1028
  } catch {
1069
1029
  setSaveState("error");
@@ -1086,7 +1046,7 @@ function usePersistence({
1086
1046
  try {
1087
1047
  const newTitle = bi(name, "");
1088
1048
  const docToSave = { ...document2, title: newTitle };
1089
- await storage.set(formKey(id), JSON.stringify({ ...docToSave, id, updatedAt: now }));
1049
+ await storage.set(formKey(id), JSON.stringify({ version: DOCUMENT_VERSION, ...docToSave, id, updatedAt: now }));
1090
1050
  const next = [...savedForms, { id, title: name, updatedAt: now }];
1091
1051
  await storage.set(INDEX_KEY, JSON.stringify(next));
1092
1052
  setSavedForms(next);
@@ -1109,7 +1069,7 @@ function usePersistence({
1109
1069
  const now = Date.now();
1110
1070
  setTemplateState("saving");
1111
1071
  try {
1112
- await storage.set(formKey(currentFormId), JSON.stringify({ ...document2, id: currentFormId, updatedAt: now }));
1072
+ await storage.set(formKey(currentFormId), JSON.stringify({ version: DOCUMENT_VERSION, ...document2, id: currentFormId, updatedAt: now }));
1113
1073
  const next = savedForms.map((f) => f.id === currentFormId ? { ...f, title: t(document2.title, "en"), updatedAt: now } : f);
1114
1074
  setSavedForms(next);
1115
1075
  await storage.set(INDEX_KEY, JSON.stringify(next));
@@ -1910,6 +1870,8 @@ function FieldTicket({
1910
1870
  {
1911
1871
  ...dropZoneHandlers,
1912
1872
  onClick: onSelect,
1873
+ className: `fb-field fb-field--${field.type} fb-field--${field.id}`,
1874
+ "data-field-id": field.id,
1913
1875
  style: {
1914
1876
  ...styles.ticket,
1915
1877
  flex: `1 1 calc(${widthPct} - 10px)`,
@@ -2439,16 +2401,16 @@ function PreviewPane({ title, sections, onFieldChange, language, strings, chrome
2439
2401
  return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: styles.previewWrap, children: [
2440
2402
  /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: { width: "100%", maxWidth, display: "flex", flexDirection: "column", alignItems: "center" }, children: [
2441
2403
  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) }),
2404
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "fb-form", style: { ...styles.previewCard, maxWidth, width: "100%" }, children: [
2405
+ features.formTitle && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h2", { style: styles.previewTitle, children: t(title, language) }),
2444
2406
  allFields.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { style: { color: "var(--fb-muted)", fontSize: 14 }, children: strings.addFieldsHint }),
2445
2407
  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: [
2408
+ 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
2409
  t(section.title, language) && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h3", { style: styles.sectionRuntimeTitle, children: t(section.title, language) }),
2448
2410
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: styles.previewGrid, children: section.fields.map((field) => {
2449
2411
  const width = effectiveWidth(field.width, device);
2450
2412
  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: [
2413
+ 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
2414
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(FieldBlock, { field, lang: language, onFieldChange, strings, chrome, error: errors[field.id], onButtonAction: handleButtonAction }),
2453
2415
  field.type === "button" && field.id === attemptedButtonId && Object.keys(errors).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("p", { style: styles.formErrorNote, children: [
2454
2416
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react14.CircleAlert, { size: 13 }),
@@ -2743,7 +2705,7 @@ var FormBuilder = (0, import_react10.forwardRef)(function FormBuilder2({
2743
2705
  ensureActiveSection: () => doc.setActiveSectionId((prev) => prev ?? doc.sections[0]?.id ?? null)
2744
2706
  });
2745
2707
  const jsonDoc = (0, import_react10.useMemo)(() => ({
2746
- version: 5,
2708
+ version: DOCUMENT_VERSION,
2747
2709
  title: doc.title,
2748
2710
  theme,
2749
2711
  themeOverrides,
@@ -3021,6 +2983,7 @@ var DARK_THEME = {
3021
2983
  CLIPBOARD_KEY,
3022
2984
  DARK_THEME,
3023
2985
  DEFAULT_THEME,
2986
+ DOCUMENT_VERSION,
3024
2987
  DRAFT_KEY,
3025
2988
  FormBuilder,
3026
2989
  INDEX_KEY,