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/dist/index.d.cts CHANGED
@@ -592,6 +592,10 @@ interface FormBuilderFeatures {
592
592
  sections?: boolean;
593
593
  /** Drag-to-reorder fields within a section. Default true. */
594
594
  dragReorder?: boolean;
595
+ /** Laptop/Tablet/Mobile width switcher above the Preview canvas. Default true. */
596
+ deviceToggle?: boolean;
597
+ /** Caps the number of input-type fields (not content blocks) addable across the whole document. Default undefined (unlimited). */
598
+ maxFields?: number;
595
599
  }
596
600
  interface FormBuilderProps {
597
601
  theme?: Partial<Theme>;
@@ -605,6 +609,10 @@ interface FormBuilderProps {
605
609
  onSubmit?: (payload: SubmitPayload) => void;
606
610
  /** Seeds the builder with this document on mount instead of the autosaved draft — e.g. a template fetched from your own backend. */
607
611
  initialDocument?: FormDocument;
612
+ /** Which mode the widget mounts into. Default "build". Pair with `features.previewMode: false` to lock a consumer to just one mode with no tabs. */
613
+ initialMode?: "build" | "preview";
614
+ /** Fires on mount and on every Build/Preview toggle — lets a host mirror the current mode without its own tab UI. */
615
+ onModeChange?: (mode: "build" | "preview") => void;
608
616
  }
609
617
 
610
618
  declare const FormBuilder: react.ForwardRefExoticComponent<FormBuilderProps & react.RefAttributes<FormBuilderHandle>>;
package/dist/index.d.ts CHANGED
@@ -592,6 +592,10 @@ interface FormBuilderFeatures {
592
592
  sections?: boolean;
593
593
  /** Drag-to-reorder fields within a section. Default true. */
594
594
  dragReorder?: boolean;
595
+ /** Laptop/Tablet/Mobile width switcher above the Preview canvas. Default true. */
596
+ deviceToggle?: boolean;
597
+ /** Caps the number of input-type fields (not content blocks) addable across the whole document. Default undefined (unlimited). */
598
+ maxFields?: number;
595
599
  }
596
600
  interface FormBuilderProps {
597
601
  theme?: Partial<Theme>;
@@ -605,6 +609,10 @@ interface FormBuilderProps {
605
609
  onSubmit?: (payload: SubmitPayload) => void;
606
610
  /** Seeds the builder with this document on mount instead of the autosaved draft — e.g. a template fetched from your own backend. */
607
611
  initialDocument?: FormDocument;
612
+ /** Which mode the widget mounts into. Default "build". Pair with `features.previewMode: false` to lock a consumer to just one mode with no tabs. */
613
+ initialMode?: "build" | "preview";
614
+ /** Fires on mount and on every Build/Preview toggle — lets a host mirror the current mode without its own tab UI. */
615
+ onModeChange?: (mode: "build" | "preview") => void;
608
616
  }
609
617
 
610
618
  declare const FormBuilder: react.ForwardRefExoticComponent<FormBuilderProps & react.RefAttributes<FormBuilderHandle>>;
package/dist/index.js CHANGED
@@ -1,9 +1,70 @@
1
1
  "use client";
2
2
 
3
3
  // src/FormBuilder.tsx
4
- import { forwardRef, useImperativeHandle, useState as useState8 } from "react";
4
+ import { forwardRef, useEffect as useEffect2, useImperativeHandle, useState as useState8 } from "react";
5
5
  import { Loader2 as Loader22, Menu, SlidersHorizontal } from "lucide-react";
6
6
 
7
+ // src/constants/fieldTypes.ts
8
+ import {
9
+ Type,
10
+ AlignLeft,
11
+ ChevronDown,
12
+ Circle,
13
+ ListChecks,
14
+ CheckSquare,
15
+ ToggleLeft,
16
+ FileText,
17
+ Image as ImageIcon,
18
+ MousePointerClick
19
+ } from "lucide-react";
20
+ var FIELD_TYPES = [
21
+ { type: "paragraph", icon: FileText, isContent: true, defaultIcon: "FileText" },
22
+ { type: "image", icon: ImageIcon, isContent: true, isImage: true, defaultIcon: "Type" },
23
+ { type: "button", icon: MousePointerClick, isContent: true, defaultIcon: "Type" },
24
+ { type: "input", icon: Type, placeholder: true, hasSubtype: true, defaultIcon: "Type" },
25
+ { type: "textarea", icon: AlignLeft, placeholder: true, defaultIcon: "AlignLeft" },
26
+ { type: "select", icon: ChevronDown, options: true, defaultIcon: "ChevronDown" },
27
+ { type: "radio", icon: Circle, options: true, defaultIcon: "Circle" },
28
+ { type: "checkboxGroup", icon: ListChecks, options: true, multiValue: true, defaultIcon: "ListChecks" },
29
+ { type: "checkbox", icon: CheckSquare, boolean: true, defaultIcon: "CheckSquare" },
30
+ { type: "toggle", icon: ToggleLeft, boolean: true, defaultIcon: "ToggleLeft" }
31
+ ];
32
+ var TYPE_MAP = Object.fromEntries(FIELD_TYPES.map((f) => [f.type, f]));
33
+ var CONTENT_TYPES = FIELD_TYPES.filter((f) => f.isContent);
34
+ var FORM_TYPES = FIELD_TYPES.filter((f) => !f.isContent);
35
+ function getMeta(type) {
36
+ return TYPE_MAP[type] || TYPE_MAP.input;
37
+ }
38
+ var LEGACY_INPUT_TYPES = ["text", "email", "phone", "number", "password", "date"];
39
+ var INPUT_SUBTYPES = ["text", "email", "phone", "number", "password", "date", "time"];
40
+ var IMAGE_SHAPES = ["square", "circle", "banner"];
41
+ var FIELD_TYPE_CHROME_KEY = {
42
+ paragraph: "fieldTypeParagraph",
43
+ image: "fieldTypeImage",
44
+ button: "fieldTypeButton",
45
+ input: "fieldTypeInput",
46
+ textarea: "fieldTypeTextarea",
47
+ select: "fieldTypeSelect",
48
+ radio: "fieldTypeRadio",
49
+ checkboxGroup: "fieldTypeCheckboxGroup",
50
+ checkbox: "fieldTypeCheckbox",
51
+ toggle: "fieldTypeToggle"
52
+ };
53
+ var INPUT_SUBTYPE_CHROME_KEY = {
54
+ text: "subtypeText",
55
+ email: "subtypeEmail",
56
+ phone: "subtypePhone",
57
+ number: "subtypeNumber",
58
+ password: "subtypePassword",
59
+ date: "subtypeDate",
60
+ time: "subtypeTime"
61
+ };
62
+ var IMAGE_SHAPE_CHROME_KEY = {
63
+ square: "shapeSquare",
64
+ circle: "shapeCircle",
65
+ banner: "shapeBanner"
66
+ };
67
+
7
68
  // src/i18n/languages.ts
8
69
  var DEFAULT_LANGUAGES = [
9
70
  { code: "en", label: "EN" },
@@ -391,67 +452,6 @@ var localStorageAdapter = {
391
452
  }
392
453
  };
393
454
 
394
- // src/constants/fieldTypes.ts
395
- import {
396
- Type,
397
- AlignLeft,
398
- ChevronDown,
399
- Circle,
400
- ListChecks,
401
- CheckSquare,
402
- ToggleLeft,
403
- FileText,
404
- Image as ImageIcon,
405
- MousePointerClick
406
- } from "lucide-react";
407
- var FIELD_TYPES = [
408
- { type: "paragraph", icon: FileText, isContent: true, defaultIcon: "FileText" },
409
- { type: "image", icon: ImageIcon, isContent: true, isImage: true, defaultIcon: "Type" },
410
- { type: "button", icon: MousePointerClick, isContent: true, defaultIcon: "Type" },
411
- { type: "input", icon: Type, placeholder: true, hasSubtype: true, defaultIcon: "Type" },
412
- { type: "textarea", icon: AlignLeft, placeholder: true, defaultIcon: "AlignLeft" },
413
- { type: "select", icon: ChevronDown, options: true, defaultIcon: "ChevronDown" },
414
- { type: "radio", icon: Circle, options: true, defaultIcon: "Circle" },
415
- { type: "checkboxGroup", icon: ListChecks, options: true, multiValue: true, defaultIcon: "ListChecks" },
416
- { type: "checkbox", icon: CheckSquare, boolean: true, defaultIcon: "CheckSquare" },
417
- { type: "toggle", icon: ToggleLeft, boolean: true, defaultIcon: "ToggleLeft" }
418
- ];
419
- var TYPE_MAP = Object.fromEntries(FIELD_TYPES.map((f) => [f.type, f]));
420
- var CONTENT_TYPES = FIELD_TYPES.filter((f) => f.isContent);
421
- var FORM_TYPES = FIELD_TYPES.filter((f) => !f.isContent);
422
- function getMeta(type) {
423
- return TYPE_MAP[type] || TYPE_MAP.input;
424
- }
425
- var LEGACY_INPUT_TYPES = ["text", "email", "phone", "number", "password", "date"];
426
- var INPUT_SUBTYPES = ["text", "email", "phone", "number", "password", "date", "time"];
427
- var IMAGE_SHAPES = ["square", "circle", "banner"];
428
- var FIELD_TYPE_CHROME_KEY = {
429
- paragraph: "fieldTypeParagraph",
430
- image: "fieldTypeImage",
431
- button: "fieldTypeButton",
432
- input: "fieldTypeInput",
433
- textarea: "fieldTypeTextarea",
434
- select: "fieldTypeSelect",
435
- radio: "fieldTypeRadio",
436
- checkboxGroup: "fieldTypeCheckboxGroup",
437
- checkbox: "fieldTypeCheckbox",
438
- toggle: "fieldTypeToggle"
439
- };
440
- var INPUT_SUBTYPE_CHROME_KEY = {
441
- text: "subtypeText",
442
- email: "subtypeEmail",
443
- phone: "subtypePhone",
444
- number: "subtypeNumber",
445
- password: "subtypePassword",
446
- date: "subtypeDate",
447
- time: "subtypeTime"
448
- };
449
- var IMAGE_SHAPE_CHROME_KEY = {
450
- square: "shapeSquare",
451
- circle: "shapeCircle",
452
- banner: "shapeBanner"
453
- };
454
-
455
455
  // src/lib/id.ts
456
456
  var genFormId = () => `form_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
457
457
  var genSectionId = () => `section_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
@@ -629,7 +629,8 @@ var DEFAULT_FEATURES = {
629
629
  contentBlocks: true,
630
630
  fieldTypes: true,
631
631
  sections: true,
632
- dragReorder: true
632
+ dragReorder: true,
633
+ deviceToggle: true
633
634
  };
634
635
  function resolveFeatures(features) {
635
636
  return {
@@ -1393,11 +1394,13 @@ function Palette({
1393
1394
  theme,
1394
1395
  updateThemeColor,
1395
1396
  updateThemeLayout,
1396
- resetTheme
1397
+ resetTheme,
1398
+ fieldCount
1397
1399
  }) {
1398
1400
  const [tab, setTab] = useState5("blocks");
1399
1401
  const contentTypes = CONTENT_TYPES.filter((f) => isContentBlockEnabled(features, f.type));
1400
1402
  const formTypes = FORM_TYPES.filter((f) => isFieldTypeEnabled(features, f.type));
1403
+ const atFieldCap = typeof features.maxFields === "number" && fieldCount >= features.maxFields;
1401
1404
  return /* @__PURE__ */ jsxs5("div", { className: "fb-palette", style: styles.palette, children: [
1402
1405
  features.design && /* @__PURE__ */ jsx5("div", { style: { marginBottom: 14 }, children: /* @__PURE__ */ jsx5(
1403
1406
  Segmented,
@@ -1440,7 +1443,8 @@ function Palette({
1440
1443
  /* @__PURE__ */ jsx5("div", { style: { ...styles.panelHeading, marginTop: 16 }, children: chrome.formFields }),
1441
1444
  /* @__PURE__ */ jsx5("div", { style: styles.paletteList, children: formTypes.map((f) => {
1442
1445
  const Icon = f.icon;
1443
- return /* @__PURE__ */ jsxs5("button", { style: styles.paletteItem, onClick: () => onAddField(f.type), children: [
1446
+ const itemStyle = atFieldCap ? { ...styles.paletteItem, opacity: 0.45, cursor: "not-allowed" } : styles.paletteItem;
1447
+ return /* @__PURE__ */ jsxs5("button", { type: "button", disabled: atFieldCap, style: itemStyle, onClick: () => onAddField(f.type), children: [
1444
1448
  /* @__PURE__ */ jsx5(Icon, { size: 16, color: "var(--fb-primary)" }),
1445
1449
  /* @__PURE__ */ jsx5("span", { children: chrome[FIELD_TYPE_CHROME_KEY[f.type]] }),
1446
1450
  /* @__PURE__ */ jsx5(Plus, { size: 13, color: "var(--fb-muted)", style: { marginLeft: "auto" } })
@@ -2171,7 +2175,7 @@ function collectFieldValues(fields) {
2171
2175
 
2172
2176
  // src/components/PreviewPane.tsx
2173
2177
  import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
2174
- function PreviewPane({ title, sections, onFieldChange, language, strings, chrome, baseMaxWidth, onSubmit }) {
2178
+ function PreviewPane({ title, sections, onFieldChange, language, strings, chrome, baseMaxWidth, features, onSubmit }) {
2175
2179
  const [device, setDevice] = useState6("laptop");
2176
2180
  const [errors, setErrors] = useState6({});
2177
2181
  const [submitted, setSubmitted] = useState6(null);
@@ -2234,7 +2238,7 @@ function PreviewPane({ title, sections, onFieldChange, language, strings, chrome
2234
2238
  }
2235
2239
  return /* @__PURE__ */ jsxs12("div", { style: styles.previewWrap, children: [
2236
2240
  /* @__PURE__ */ jsxs12("div", { style: { width: "100%", maxWidth, display: "flex", flexDirection: "column", alignItems: "center" }, children: [
2237
- /* @__PURE__ */ jsx14("div", { style: styles.previewToolbar, children: /* @__PURE__ */ jsx14(Segmented, { options: deviceOptions, value: device, onChange: setDevice }) }),
2241
+ features.deviceToggle && /* @__PURE__ */ jsx14("div", { style: styles.previewToolbar, children: /* @__PURE__ */ jsx14(Segmented, { options: deviceOptions, value: device, onChange: setDevice }) }),
2238
2242
  /* @__PURE__ */ jsxs12("div", { style: { ...styles.previewCard, maxWidth, width: "100%" }, children: [
2239
2243
  /* @__PURE__ */ jsx14("h2", { style: styles.previewTitle, children: t(title, language) }),
2240
2244
  allFields.length === 0 && /* @__PURE__ */ jsx14("p", { style: { color: "var(--fb-muted)", fontSize: 14 }, children: strings.addFieldsHint }),
@@ -2405,7 +2409,9 @@ var FormBuilder = forwardRef(function FormBuilder2({
2405
2409
  features: featuresProp,
2406
2410
  storage: storageProp,
2407
2411
  onSubmit,
2408
- initialDocument
2412
+ initialDocument,
2413
+ initialMode,
2414
+ onModeChange
2409
2415
  }, ref) {
2410
2416
  const features = resolveFeatures(featuresProp);
2411
2417
  const storage = storageProp ?? localStorageAdapter;
@@ -2415,7 +2421,14 @@ var FormBuilder = forwardRef(function FormBuilder2({
2415
2421
  const chrome = { ...CHROME[language] || CHROME.en, ...chromeOverride && chromeOverride[language] || {} };
2416
2422
  const doc = useFormDocument({ language, chrome });
2417
2423
  const drag = useDragReorder(doc.reorderWithinSection);
2418
- const [mode, setMode] = useState8("build");
2424
+ const [mode, setModeState] = useState8(initialMode ?? "build");
2425
+ const setMode = (next) => {
2426
+ setModeState(next);
2427
+ onModeChange?.(next);
2428
+ };
2429
+ useEffect2(() => {
2430
+ onModeChange?.(mode);
2431
+ }, []);
2419
2432
  const [showJson, setShowJson] = useState8(false);
2420
2433
  const [showLibrary, setShowLibrary] = useState8(false);
2421
2434
  const [copied, setCopied] = useState8(false);
@@ -2462,6 +2475,7 @@ var FormBuilder = forwardRef(function FormBuilder2({
2462
2475
  }
2463
2476
  // eslint-disable-next-line react-hooks/exhaustive-deps
2464
2477
  }), [jsonDoc, jsonString]);
2478
+ const fieldCount = doc.sections.reduce((n, s) => n + s.fields.filter((f) => !getMeta(f.type).isContent).length, 0);
2465
2479
  const activeSectionIdx = doc.sections.findIndex((s) => s.id === doc.activeSection?.id);
2466
2480
  const activeSectionLabel = t(doc.activeSection?.title, language) || `#${activeSectionIdx + 1}`;
2467
2481
  const rootStyle = {
@@ -2553,6 +2567,7 @@ var FormBuilder = forwardRef(function FormBuilder2({
2553
2567
  setMobilePanel("none");
2554
2568
  },
2555
2569
  features,
2570
+ fieldCount,
2556
2571
  theme,
2557
2572
  updateThemeColor,
2558
2573
  updateThemeLayout,
@@ -2616,6 +2631,7 @@ var FormBuilder = forwardRef(function FormBuilder2({
2616
2631
  strings,
2617
2632
  chrome,
2618
2633
  baseMaxWidth: theme.layout.maxWidth,
2634
+ features,
2619
2635
  onSubmit
2620
2636
  }
2621
2637
  ),