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 +5 -1
- package/dist/index.cjs +73 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +85 -69
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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,
|
|
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
|
),
|