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 +15 -0
- package/dist/index.cjs +14 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +13 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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:
|
|
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,
|