form-page-builder 2.0.0 → 2.1.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
@@ -70,6 +70,8 @@ Above ~720px wide, Build mode is the usual three-column Palette/Canvas/Inspector
70
70
  | `storage` | `StorageAdapter` | Pluggable persistence backend for the builder's own draft/Templates library — see below. |
71
71
  | `onSubmit` | `(payload: SubmitPayload) => void` | Called when Preview mode's Submit button is clicked and validation passes — see below. |
72
72
  | `initialDocument` | `FormDocument` | Seeds the builder with this document on mount instead of the autosaved draft — see "Programmatic integration" below. |
73
+ | `initialMode` | `"build" \| "preview"` | Which mode the widget mounts into (default `"build"`). Pair with `features.previewMode: false` to lock a consumer to just one mode with no tabs — e.g. a fill-only embed that never shows the Build canvas. Or leave `previewMode` on and pick `initialMode` per document (e.g. `"preview"` once a document already has saved data) so the widget opens on the right mode while still letting the built-in tabs switch it. |
74
+ | `onModeChange` | `(mode: "build" \| "preview") => void` | Fires on mount and on every Build/Preview toggle. Lets a host mirror the current mode (e.g. to show its own Save button only in Build mode) without building a separate tab UI around the widget. |
73
75
 
74
76
  A `ref` on `<FormBuilder />` gives you a `FormBuilderHandle` (`getDocument()` / `loadDocument()` / `exportJson()`) — see "Programmatic integration" below.
75
77
 
@@ -106,7 +108,7 @@ Every optional UI surface can be switched on or off independently through one `f
106
108
  | `newForm` | `boolean` | `true` | The "New Form" reset button. |
107
109
  | `autosave` | `boolean` | `true` | Autosaving the draft to `storage`. The initial draft *load* always happens regardless — this only gates the write path. |
108
110
  | `jsonView` | `boolean` | `true` | The "View JSON" button and modal. |
109
- | `previewMode` | `boolean` | `true` | The Build/Preview tabs. When `false`, the builder stays in Build mode and the tabs are hidden. |
111
+ | `previewMode` | `boolean` | `true` | The Build/Preview tabs. When `false`, the tabs are hidden and the builder stays in whichever mode it started in (`initialMode`, Build by default). |
110
112
  | `languageSwitcher` | `boolean` | `true` | The language-switcher pill in the toolbar. |
111
113
  | `design` | `boolean` | `false` | The global "Design" tab (colors/spacing), i.e. the old `themeEditable` prop. |
112
114
  | `blockStyling` | `boolean` | `true` | Per-field styling controls: paragraph heading/font/color, button color — independent of `design`. |
@@ -114,6 +116,8 @@ Every optional UI surface can be switched on or off independently through one `f
114
116
  | `fieldTypes` | `boolean \| ("input" \| "textarea" \| "select" \| "radio" \| "checkboxGroup" \| "checkbox" \| "toggle")[]` | `true` | Which form field types can be *added* from the palette. `true` = all, `false` = none, or an allowlist. |
115
117
  | `sections` | `boolean` | `true` | Add/duplicate/move/delete-section controls and the section background picker. |
116
118
  | `dragReorder` | `boolean` | `true` | Drag-to-reorder fields within a section. |
119
+ | `deviceToggle` | `boolean` | `true` | The Laptop/Tablet/Mobile width switcher above the Preview canvas. When `false`, Preview always renders at the Laptop (full) width. |
120
+ | `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. |
117
121
 
118
122
  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.
119
123
 
package/dist/index.cjs CHANGED
@@ -33,6 +33,56 @@ module.exports = __toCommonJS(index_exports);
33
33
  var import_react8 = require("react");
34
34
  var import_lucide_react18 = require("lucide-react");
35
35
 
36
+ // src/constants/fieldTypes.ts
37
+ var import_lucide_react = require("lucide-react");
38
+ var FIELD_TYPES = [
39
+ { type: "paragraph", icon: import_lucide_react.FileText, isContent: true, defaultIcon: "FileText" },
40
+ { type: "image", icon: import_lucide_react.Image, isContent: true, isImage: true, defaultIcon: "Type" },
41
+ { type: "button", icon: import_lucide_react.MousePointerClick, isContent: true, defaultIcon: "Type" },
42
+ { type: "input", icon: import_lucide_react.Type, placeholder: true, hasSubtype: true, defaultIcon: "Type" },
43
+ { type: "textarea", icon: import_lucide_react.AlignLeft, placeholder: true, defaultIcon: "AlignLeft" },
44
+ { type: "select", icon: import_lucide_react.ChevronDown, options: true, defaultIcon: "ChevronDown" },
45
+ { type: "radio", icon: import_lucide_react.Circle, options: true, defaultIcon: "Circle" },
46
+ { type: "checkboxGroup", icon: import_lucide_react.ListChecks, options: true, multiValue: true, defaultIcon: "ListChecks" },
47
+ { type: "checkbox", icon: import_lucide_react.CheckSquare, boolean: true, defaultIcon: "CheckSquare" },
48
+ { type: "toggle", icon: import_lucide_react.ToggleLeft, boolean: true, defaultIcon: "ToggleLeft" }
49
+ ];
50
+ var TYPE_MAP = Object.fromEntries(FIELD_TYPES.map((f) => [f.type, f]));
51
+ var CONTENT_TYPES = FIELD_TYPES.filter((f) => f.isContent);
52
+ var FORM_TYPES = FIELD_TYPES.filter((f) => !f.isContent);
53
+ function getMeta(type) {
54
+ return TYPE_MAP[type] || TYPE_MAP.input;
55
+ }
56
+ var LEGACY_INPUT_TYPES = ["text", "email", "phone", "number", "password", "date"];
57
+ var INPUT_SUBTYPES = ["text", "email", "phone", "number", "password", "date", "time"];
58
+ var IMAGE_SHAPES = ["square", "circle", "banner"];
59
+ var FIELD_TYPE_CHROME_KEY = {
60
+ paragraph: "fieldTypeParagraph",
61
+ image: "fieldTypeImage",
62
+ button: "fieldTypeButton",
63
+ input: "fieldTypeInput",
64
+ textarea: "fieldTypeTextarea",
65
+ select: "fieldTypeSelect",
66
+ radio: "fieldTypeRadio",
67
+ checkboxGroup: "fieldTypeCheckboxGroup",
68
+ checkbox: "fieldTypeCheckbox",
69
+ toggle: "fieldTypeToggle"
70
+ };
71
+ var INPUT_SUBTYPE_CHROME_KEY = {
72
+ text: "subtypeText",
73
+ email: "subtypeEmail",
74
+ phone: "subtypePhone",
75
+ number: "subtypeNumber",
76
+ password: "subtypePassword",
77
+ date: "subtypeDate",
78
+ time: "subtypeTime"
79
+ };
80
+ var IMAGE_SHAPE_CHROME_KEY = {
81
+ square: "shapeSquare",
82
+ circle: "shapeCircle",
83
+ banner: "shapeBanner"
84
+ };
85
+
36
86
  // src/i18n/languages.ts
37
87
  var DEFAULT_LANGUAGES = [
38
88
  { code: "en", label: "EN" },
@@ -420,56 +470,6 @@ var localStorageAdapter = {
420
470
  }
421
471
  };
422
472
 
423
- // src/constants/fieldTypes.ts
424
- var import_lucide_react = require("lucide-react");
425
- var FIELD_TYPES = [
426
- { type: "paragraph", icon: import_lucide_react.FileText, isContent: true, defaultIcon: "FileText" },
427
- { type: "image", icon: import_lucide_react.Image, isContent: true, isImage: true, defaultIcon: "Type" },
428
- { type: "button", icon: import_lucide_react.MousePointerClick, isContent: true, defaultIcon: "Type" },
429
- { type: "input", icon: import_lucide_react.Type, placeholder: true, hasSubtype: true, defaultIcon: "Type" },
430
- { type: "textarea", icon: import_lucide_react.AlignLeft, placeholder: true, defaultIcon: "AlignLeft" },
431
- { type: "select", icon: import_lucide_react.ChevronDown, options: true, defaultIcon: "ChevronDown" },
432
- { type: "radio", icon: import_lucide_react.Circle, options: true, defaultIcon: "Circle" },
433
- { type: "checkboxGroup", icon: import_lucide_react.ListChecks, options: true, multiValue: true, defaultIcon: "ListChecks" },
434
- { type: "checkbox", icon: import_lucide_react.CheckSquare, boolean: true, defaultIcon: "CheckSquare" },
435
- { type: "toggle", icon: import_lucide_react.ToggleLeft, boolean: true, defaultIcon: "ToggleLeft" }
436
- ];
437
- var TYPE_MAP = Object.fromEntries(FIELD_TYPES.map((f) => [f.type, f]));
438
- var CONTENT_TYPES = FIELD_TYPES.filter((f) => f.isContent);
439
- var FORM_TYPES = FIELD_TYPES.filter((f) => !f.isContent);
440
- function getMeta(type) {
441
- return TYPE_MAP[type] || TYPE_MAP.input;
442
- }
443
- var LEGACY_INPUT_TYPES = ["text", "email", "phone", "number", "password", "date"];
444
- var INPUT_SUBTYPES = ["text", "email", "phone", "number", "password", "date", "time"];
445
- var IMAGE_SHAPES = ["square", "circle", "banner"];
446
- var FIELD_TYPE_CHROME_KEY = {
447
- paragraph: "fieldTypeParagraph",
448
- image: "fieldTypeImage",
449
- button: "fieldTypeButton",
450
- input: "fieldTypeInput",
451
- textarea: "fieldTypeTextarea",
452
- select: "fieldTypeSelect",
453
- radio: "fieldTypeRadio",
454
- checkboxGroup: "fieldTypeCheckboxGroup",
455
- checkbox: "fieldTypeCheckbox",
456
- toggle: "fieldTypeToggle"
457
- };
458
- var INPUT_SUBTYPE_CHROME_KEY = {
459
- text: "subtypeText",
460
- email: "subtypeEmail",
461
- phone: "subtypePhone",
462
- number: "subtypeNumber",
463
- password: "subtypePassword",
464
- date: "subtypeDate",
465
- time: "subtypeTime"
466
- };
467
- var IMAGE_SHAPE_CHROME_KEY = {
468
- square: "shapeSquare",
469
- circle: "shapeCircle",
470
- banner: "shapeBanner"
471
- };
472
-
473
473
  // src/lib/id.ts
474
474
  var genFormId = () => `form_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
475
475
  var genSectionId = () => `section_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
@@ -647,7 +647,8 @@ var DEFAULT_FEATURES = {
647
647
  contentBlocks: true,
648
648
  fieldTypes: true,
649
649
  sections: true,
650
- dragReorder: true
650
+ dragReorder: true,
651
+ deviceToggle: true
651
652
  };
652
653
  function resolveFeatures(features) {
653
654
  return {
@@ -1411,11 +1412,13 @@ function Palette({
1411
1412
  theme,
1412
1413
  updateThemeColor,
1413
1414
  updateThemeLayout,
1414
- resetTheme
1415
+ resetTheme,
1416
+ fieldCount
1415
1417
  }) {
1416
1418
  const [tab, setTab] = (0, import_react5.useState)("blocks");
1417
1419
  const contentTypes = CONTENT_TYPES.filter((f) => isContentBlockEnabled(features, f.type));
1418
1420
  const formTypes = FORM_TYPES.filter((f) => isFieldTypeEnabled(features, f.type));
1421
+ const atFieldCap = typeof features.maxFields === "number" && fieldCount >= features.maxFields;
1419
1422
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "fb-palette", style: styles.palette, children: [
1420
1423
  features.design && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { marginBottom: 14 }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1421
1424
  Segmented,
@@ -1458,7 +1461,8 @@ function Palette({
1458
1461
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...styles.panelHeading, marginTop: 16 }, children: chrome.formFields }),
1459
1462
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: styles.paletteList, children: formTypes.map((f) => {
1460
1463
  const Icon = f.icon;
1461
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("button", { style: styles.paletteItem, onClick: () => onAddField(f.type), children: [
1464
+ const itemStyle = atFieldCap ? { ...styles.paletteItem, opacity: 0.45, cursor: "not-allowed" } : styles.paletteItem;
1465
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("button", { type: "button", disabled: atFieldCap, style: itemStyle, onClick: () => onAddField(f.type), children: [
1462
1466
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Icon, { size: 16, color: "var(--fb-primary)" }),
1463
1467
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: chrome[FIELD_TYPE_CHROME_KEY[f.type]] }),
1464
1468
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react5.Plus, { size: 13, color: "var(--fb-muted)", style: { marginLeft: "auto" } })
@@ -2165,7 +2169,7 @@ function collectFieldValues(fields) {
2165
2169
 
2166
2170
  // src/components/PreviewPane.tsx
2167
2171
  var import_jsx_runtime14 = require("react/jsx-runtime");
2168
- function PreviewPane({ title, sections, onFieldChange, language, strings, chrome, baseMaxWidth, onSubmit }) {
2172
+ function PreviewPane({ title, sections, onFieldChange, language, strings, chrome, baseMaxWidth, features, onSubmit }) {
2169
2173
  const [device, setDevice] = (0, import_react6.useState)("laptop");
2170
2174
  const [errors, setErrors] = (0, import_react6.useState)({});
2171
2175
  const [submitted, setSubmitted] = (0, import_react6.useState)(null);
@@ -2228,7 +2232,7 @@ function PreviewPane({ title, sections, onFieldChange, language, strings, chrome
2228
2232
  }
2229
2233
  return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: styles.previewWrap, children: [
2230
2234
  /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: { width: "100%", maxWidth, display: "flex", flexDirection: "column", alignItems: "center" }, children: [
2231
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: styles.previewToolbar, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Segmented, { options: deviceOptions, value: device, onChange: setDevice }) }),
2235
+ 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 }) }),
2232
2236
  /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: { ...styles.previewCard, maxWidth, width: "100%" }, children: [
2233
2237
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h2", { style: styles.previewTitle, children: t(title, language) }),
2234
2238
  allFields.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { style: { color: "var(--fb-muted)", fontSize: 14 }, children: strings.addFieldsHint }),
@@ -2399,7 +2403,9 @@ var FormBuilder = (0, import_react8.forwardRef)(function FormBuilder2({
2399
2403
  features: featuresProp,
2400
2404
  storage: storageProp,
2401
2405
  onSubmit,
2402
- initialDocument
2406
+ initialDocument,
2407
+ initialMode,
2408
+ onModeChange
2403
2409
  }, ref) {
2404
2410
  const features = resolveFeatures(featuresProp);
2405
2411
  const storage = storageProp ?? localStorageAdapter;
@@ -2409,7 +2415,14 @@ var FormBuilder = (0, import_react8.forwardRef)(function FormBuilder2({
2409
2415
  const chrome = { ...CHROME[language] || CHROME.en, ...chromeOverride && chromeOverride[language] || {} };
2410
2416
  const doc = useFormDocument({ language, chrome });
2411
2417
  const drag = useDragReorder(doc.reorderWithinSection);
2412
- const [mode, setMode] = (0, import_react8.useState)("build");
2418
+ const [mode, setModeState] = (0, import_react8.useState)(initialMode ?? "build");
2419
+ const setMode = (next) => {
2420
+ setModeState(next);
2421
+ onModeChange?.(next);
2422
+ };
2423
+ (0, import_react8.useEffect)(() => {
2424
+ onModeChange?.(mode);
2425
+ }, []);
2413
2426
  const [showJson, setShowJson] = (0, import_react8.useState)(false);
2414
2427
  const [showLibrary, setShowLibrary] = (0, import_react8.useState)(false);
2415
2428
  const [copied, setCopied] = (0, import_react8.useState)(false);
@@ -2456,6 +2469,7 @@ var FormBuilder = (0, import_react8.forwardRef)(function FormBuilder2({
2456
2469
  }
2457
2470
  // eslint-disable-next-line react-hooks/exhaustive-deps
2458
2471
  }), [jsonDoc, jsonString]);
2472
+ const fieldCount = doc.sections.reduce((n, s) => n + s.fields.filter((f) => !getMeta(f.type).isContent).length, 0);
2459
2473
  const activeSectionIdx = doc.sections.findIndex((s) => s.id === doc.activeSection?.id);
2460
2474
  const activeSectionLabel = t(doc.activeSection?.title, language) || `#${activeSectionIdx + 1}`;
2461
2475
  const rootStyle = {
@@ -2547,6 +2561,7 @@ var FormBuilder = (0, import_react8.forwardRef)(function FormBuilder2({
2547
2561
  setMobilePanel("none");
2548
2562
  },
2549
2563
  features,
2564
+ fieldCount,
2550
2565
  theme,
2551
2566
  updateThemeColor,
2552
2567
  updateThemeLayout,
@@ -2610,6 +2625,7 @@ var FormBuilder = (0, import_react8.forwardRef)(function FormBuilder2({
2610
2625
  strings,
2611
2626
  chrome,
2612
2627
  baseMaxWidth: theme.layout.maxWidth,
2628
+ features,
2613
2629
  onSubmit
2614
2630
  }
2615
2631
  ),