form-page-builder 2.2.0 → 2.4.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 +30 -1
- package/dist/index.cjs +93 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +91 -54
- 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. |
|
|
@@ -122,10 +123,24 @@ Every optional UI surface can be switched on or off independently through one `f
|
|
|
122
123
|
| `sectionBackground` | `boolean` | inherits `sections` | Per-section background-color swatches + custom-color picker in the section header. Independent of `sections` — set it `false` to lock section backgrounds while keeping the structural controls, or leave it unset and it follows whatever `sections` is. |
|
|
123
124
|
| `dragReorder` | `boolean` | `true` | Drag-to-reorder fields within a section. |
|
|
124
125
|
| `deviceToggle` | `boolean` | `true` | The Laptop/Tablet/Mobile width switcher above the Preview canvas. When `false`, Preview always renders at the Laptop (full) width. |
|
|
125
|
-
| `maxFields` | `number` | `undefined` (unlimited) | Caps the total number of input-type fields (not content blocks) addable across the whole document.
|
|
126
|
+
| `maxFields` | `number` | `undefined` (unlimited) | Caps the total number of input-type fields (not content blocks) addable across the whole document. The palette shows a live "Fields: n / max" count; once reached, the Form Fields buttons disable and a limit message replaces the count until a field is removed. |
|
|
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.
|
|
@@ -258,6 +273,20 @@ const json = serializeTemplate(ref.current.getDocument());
|
|
|
258
273
|
const doc = parseTemplate(json); // also accepts bare "View JSON" output; null if not a document
|
|
259
274
|
```
|
|
260
275
|
|
|
276
|
+
### Smaller stored documents
|
|
277
|
+
|
|
278
|
+
Drafts and saved templates written through `storage` are compacted automatically: props equal to their default (`hideLabel`, `width`, `verticalAlign`, `labelPosition`, `showIcon`, `required`, the type's default icon, empty placeholders/default values, empty non-`en` translations) are omitted and the document is stamped `compact: 1`. Loading restores them, so older full documents still load unchanged.
|
|
279
|
+
|
|
280
|
+
`getDocument()` still returns the full document. If you store it yourself, compact it first:
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
import { compactDocument } from "form-page-builder";
|
|
284
|
+
|
|
285
|
+
await save(JSON.stringify(compactDocument(ref.current.getDocument())));
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Documents saved in the compact form need this package version (or newer) to be read back correctly, so upgrade every app that reads them together.
|
|
289
|
+
|
|
261
290
|
## Using in a plain HTML page (no bundler)
|
|
262
291
|
|
|
263
292
|
There's intentionally no UMD/IIFE global-script build (see below), but you don't need a bundler to use this package — a browser-native ES module setup works too, via an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) and a CDN like [esm.sh](https://esm.sh/):
|
package/dist/index.cjs
CHANGED
|
@@ -24,9 +24,11 @@ __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,
|
|
31
|
+
compactDocument: () => compactDocument,
|
|
30
32
|
default: () => FormBuilder_default,
|
|
31
33
|
formKey: () => formKey,
|
|
32
34
|
localStorageAdapter: () => localStorageAdapter,
|
|
@@ -221,6 +223,8 @@ var CHROME = {
|
|
|
221
223
|
loadingDraft: "Loading your draft...",
|
|
222
224
|
templatesLimitReached: (max) => `You can save up to ${max} template${max === 1 ? "" : "s"}. Delete one to save another.`,
|
|
223
225
|
templatesLimitTitle: "Template limit reached",
|
|
226
|
+
fieldCount: (n, max) => `Fields: ${n} / ${max}`,
|
|
227
|
+
fieldLimitReached: (max) => `Field limit reached (${max}). Delete a field to add another.`,
|
|
224
228
|
confirm: "Confirm",
|
|
225
229
|
cancel: "Cancel",
|
|
226
230
|
dismiss: "OK",
|
|
@@ -374,6 +378,8 @@ var CHROME = {
|
|
|
374
378
|
loadingDraft: "\u4E0B\u66F8\u304D\u3092\u8AAD\u307F\u8FBC\u3093\u3067\u3044\u307E\u3059...",
|
|
375
379
|
templatesLimitReached: (max) => `\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u306F${max}\u4EF6\u307E\u3067\u4FDD\u5B58\u3067\u304D\u307E\u3059\u30021\u4EF6\u524A\u9664\u3057\u3066\u304B\u3089\u4FDD\u5B58\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
|
|
376
380
|
templatesLimitTitle: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u306E\u4E0A\u9650\u306B\u9054\u3057\u307E\u3057\u305F",
|
|
381
|
+
fieldCount: (n, max) => `\u9805\u76EE\u6570: ${n} / ${max}`,
|
|
382
|
+
fieldLimitReached: (max) => `\u9805\u76EE\u6570\u306E\u4E0A\u9650\uFF08${max}\u4EF6\uFF09\u306B\u9054\u3057\u307E\u3057\u305F\u3002\u8FFD\u52A0\u3059\u308B\u306B\u306F\u9805\u76EE\u3092\u524A\u9664\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
|
|
377
383
|
confirm: "\u78BA\u5B9A",
|
|
378
384
|
cancel: "\u30AD\u30E3\u30F3\u30BB\u30EB",
|
|
379
385
|
dismiss: "OK",
|
|
@@ -583,12 +589,8 @@ function defaultSection(titleEn = "") {
|
|
|
583
589
|
return { id: genSectionId(), title: bi(titleEn, ""), background: "", collapsed: false, fields: [] };
|
|
584
590
|
}
|
|
585
591
|
|
|
586
|
-
// src/lib/submittable.ts
|
|
587
|
-
function sectionHasFormFields(section) {
|
|
588
|
-
return section.fields.some((f) => !getMeta(f.type).isContent);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
592
|
// src/lib/migrate.ts
|
|
593
|
+
var DOCUMENT_VERSION = 5;
|
|
592
594
|
function migrateField(field) {
|
|
593
595
|
let f = { ...field };
|
|
594
596
|
if (LEGACY_INPUT_TYPES.includes(f.type)) f = { ...f, type: "input", inputType: f.type };
|
|
@@ -614,30 +616,21 @@ function migrateField(field) {
|
|
|
614
616
|
f.submitScope = f.submitScope === "section" ? "section" : "form";
|
|
615
617
|
}
|
|
616
618
|
f.verticalAlign = f.verticalAlign || "top";
|
|
619
|
+
const meta = getMeta(f.type);
|
|
620
|
+
f.width = f.width || "1/1";
|
|
621
|
+
f.labelPosition = f.labelPosition || "top";
|
|
622
|
+
f.showIcon = !!f.showIcon;
|
|
623
|
+
f.displayIcon = f.displayIcon || meta.defaultIcon;
|
|
624
|
+
if (!meta.isContent) {
|
|
625
|
+
f.required = !!f.required;
|
|
626
|
+
if (meta.placeholder && !f.placeholder) f.placeholder = bi();
|
|
627
|
+
if (f.defaultValue === void 0) f.defaultValue = meta.boolean ? false : meta.multiValue ? [] : "";
|
|
628
|
+
}
|
|
617
629
|
return f;
|
|
618
630
|
}
|
|
619
631
|
function migrateFields(fields) {
|
|
620
632
|
return (fields || []).map(migrateField);
|
|
621
633
|
}
|
|
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
634
|
function migrateDocument(raw) {
|
|
642
635
|
if (!raw) return null;
|
|
643
636
|
const title = typeof raw.title === "string" ? bi(raw.title) : raw.title || bi();
|
|
@@ -654,25 +647,6 @@ function migrateDocument(raw) {
|
|
|
654
647
|
} else {
|
|
655
648
|
sections = [{ ...defaultSection(), fields: migrateFields(raw.fields || []) }];
|
|
656
649
|
}
|
|
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
650
|
return { title, themeOverrides, sections };
|
|
677
651
|
}
|
|
678
652
|
|
|
@@ -700,6 +674,7 @@ function parseTemplate(raw) {
|
|
|
700
674
|
var DEFAULT_MAX_TEMPLATES = 5;
|
|
701
675
|
var DEFAULT_FEATURES = {
|
|
702
676
|
naming: true,
|
|
677
|
+
formTitle: false,
|
|
703
678
|
templates: { enabled: true, manage: true, max: DEFAULT_MAX_TEMPLATES },
|
|
704
679
|
templateClipboard: true,
|
|
705
680
|
newForm: true,
|
|
@@ -903,7 +878,9 @@ function useFormDocument({ language, chrome }) {
|
|
|
903
878
|
...s,
|
|
904
879
|
fields: s.fields.map((f) => {
|
|
905
880
|
if (f.id !== fieldId || !("options" in f) || !f.options) return f;
|
|
906
|
-
const
|
|
881
|
+
const used = new Set(f.options.map((o) => o.value));
|
|
882
|
+
let n = f.options.length + 1;
|
|
883
|
+
while (used.has(`option_${n}`)) n++;
|
|
907
884
|
return { ...f, options: [...f.options, { label: withLang(bi(), language, chrome.optionSeed(n)), value: `option_${n}` }] };
|
|
908
885
|
})
|
|
909
886
|
})));
|
|
@@ -911,7 +888,14 @@ function useFormDocument({ language, chrome }) {
|
|
|
911
888
|
function removeOption(fieldId, optIdx) {
|
|
912
889
|
setSections((prev) => prev.map((s) => ({
|
|
913
890
|
...s,
|
|
914
|
-
fields: s.fields.map((f) =>
|
|
891
|
+
fields: s.fields.map((f) => {
|
|
892
|
+
if (f.id !== fieldId || !("options" in f) || !f.options) return f;
|
|
893
|
+
const removed = f.options[optIdx]?.value;
|
|
894
|
+
const options = f.options.filter((_, i) => i !== optIdx);
|
|
895
|
+
if (f.type === "checkboxGroup") return { ...f, options, defaultValue: f.defaultValue.filter((v) => v !== removed) };
|
|
896
|
+
if ((f.type === "select" || f.type === "radio") && f.defaultValue === removed) return { ...f, options, defaultValue: "" };
|
|
897
|
+
return { ...f, options };
|
|
898
|
+
})
|
|
915
899
|
})));
|
|
916
900
|
}
|
|
917
901
|
function loadDocument(doc) {
|
|
@@ -971,6 +955,51 @@ var formKey = (id) => `form-page-builder:saved:${id}`;
|
|
|
971
955
|
var savedFormId = (key) => key.startsWith("form-page-builder:saved:") ? key.slice("form-page-builder:saved:".length) : null;
|
|
972
956
|
var CLIPBOARD_KEY = "form-page-builder:clipboard";
|
|
973
957
|
|
|
958
|
+
// src/lib/compact.ts
|
|
959
|
+
var BASE_DEFAULTS = {
|
|
960
|
+
hideLabel: false,
|
|
961
|
+
width: "1/1",
|
|
962
|
+
verticalAlign: "top",
|
|
963
|
+
labelPosition: "top",
|
|
964
|
+
showIcon: false,
|
|
965
|
+
required: false
|
|
966
|
+
};
|
|
967
|
+
var COMPACT_VERSION = 1;
|
|
968
|
+
var slimLocalized = (v) => v && typeof v === "object" ? Object.fromEntries(Object.entries(v).filter(([lang, s]) => lang === "en" || s !== "")) : v;
|
|
969
|
+
var isEmptyLocalized = (v) => !!v && typeof v === "object" && Object.values(v).every((s) => !s);
|
|
970
|
+
var isEmptyDefault = (v) => v === "" || v === false || Array.isArray(v) && v.length === 0;
|
|
971
|
+
function compactField(field) {
|
|
972
|
+
const out = {};
|
|
973
|
+
const meta = getMeta(field.type);
|
|
974
|
+
for (const [key, value] of Object.entries(field)) {
|
|
975
|
+
if (key in BASE_DEFAULTS && value === BASE_DEFAULTS[key]) continue;
|
|
976
|
+
if (key === "displayIcon" && value === meta.defaultIcon) continue;
|
|
977
|
+
if (key === "placeholder" && isEmptyLocalized(value)) continue;
|
|
978
|
+
if (key === "defaultValue" && !meta.isContent && isEmptyDefault(value)) continue;
|
|
979
|
+
if (key === "label" || key === "placeholder" || key === "content" || key === "alt") out[key] = slimLocalized(value);
|
|
980
|
+
else if (key === "options" && Array.isArray(value)) out[key] = value.map((o) => ({ ...o, label: slimLocalized(o.label) }));
|
|
981
|
+
else out[key] = value;
|
|
982
|
+
}
|
|
983
|
+
return out;
|
|
984
|
+
}
|
|
985
|
+
function compactDocument(doc) {
|
|
986
|
+
return {
|
|
987
|
+
...doc,
|
|
988
|
+
compact: COMPACT_VERSION,
|
|
989
|
+
...doc.title !== void 0 ? { title: slimLocalized(doc.title) } : {},
|
|
990
|
+
sections: doc.sections.map((s) => {
|
|
991
|
+
const { background, collapsed, fields, ...rest } = s;
|
|
992
|
+
return {
|
|
993
|
+
...rest,
|
|
994
|
+
...s.title !== void 0 ? { title: slimLocalized(s.title) } : {},
|
|
995
|
+
...background ? { background } : {},
|
|
996
|
+
...collapsed ? { collapsed } : {},
|
|
997
|
+
fields: fields.map(compactField)
|
|
998
|
+
};
|
|
999
|
+
})
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
974
1003
|
// src/hooks/usePersistence.ts
|
|
975
1004
|
function snapshotOf(d) {
|
|
976
1005
|
return JSON.stringify({ title: d.title, themeOverrides: d.themeOverrides, sections: d.sections });
|
|
@@ -1063,7 +1092,7 @@ function usePersistence({
|
|
|
1063
1092
|
if (autosaveTimer.current) clearTimeout(autosaveTimer.current);
|
|
1064
1093
|
autosaveTimer.current = setTimeout(async () => {
|
|
1065
1094
|
try {
|
|
1066
|
-
await storage.set(DRAFT_KEY, JSON.stringify({ ...document2, currentFormId }));
|
|
1095
|
+
await storage.set(DRAFT_KEY, JSON.stringify({ version: DOCUMENT_VERSION, ...compactDocument(document2), currentFormId }));
|
|
1067
1096
|
setSaveState("saved");
|
|
1068
1097
|
} catch {
|
|
1069
1098
|
setSaveState("error");
|
|
@@ -1086,7 +1115,7 @@ function usePersistence({
|
|
|
1086
1115
|
try {
|
|
1087
1116
|
const newTitle = bi(name, "");
|
|
1088
1117
|
const docToSave = { ...document2, title: newTitle };
|
|
1089
|
-
await storage.set(formKey(id), JSON.stringify({ ...docToSave, id, updatedAt: now }));
|
|
1118
|
+
await storage.set(formKey(id), JSON.stringify({ version: DOCUMENT_VERSION, ...compactDocument(docToSave), id, updatedAt: now }));
|
|
1090
1119
|
const next = [...savedForms, { id, title: name, updatedAt: now }];
|
|
1091
1120
|
await storage.set(INDEX_KEY, JSON.stringify(next));
|
|
1092
1121
|
setSavedForms(next);
|
|
@@ -1109,7 +1138,7 @@ function usePersistence({
|
|
|
1109
1138
|
const now = Date.now();
|
|
1110
1139
|
setTemplateState("saving");
|
|
1111
1140
|
try {
|
|
1112
|
-
await storage.set(formKey(currentFormId), JSON.stringify({ ...document2, id: currentFormId, updatedAt: now }));
|
|
1141
|
+
await storage.set(formKey(currentFormId), JSON.stringify({ version: DOCUMENT_VERSION, ...compactDocument(document2), id: currentFormId, updatedAt: now }));
|
|
1113
1142
|
const next = savedForms.map((f) => f.id === currentFormId ? { ...f, title: t(document2.title, "en"), updatedAt: now } : f);
|
|
1114
1143
|
setSavedForms(next);
|
|
1115
1144
|
await storage.set(INDEX_KEY, JSON.stringify(next));
|
|
@@ -1125,11 +1154,13 @@ function usePersistence({
|
|
|
1125
1154
|
try {
|
|
1126
1155
|
const raw = await storage.get(formKey(id));
|
|
1127
1156
|
if (!raw) {
|
|
1157
|
+
console.error(`[form-page-builder] loadForm(${id}): storage returned no data for this id`);
|
|
1128
1158
|
setTemplateState("error");
|
|
1129
1159
|
return false;
|
|
1130
1160
|
}
|
|
1131
1161
|
const doc = migrateDocument(JSON.parse(raw));
|
|
1132
1162
|
if (!doc) {
|
|
1163
|
+
console.error(`[form-page-builder] loadForm(${id}): stored data failed migrateDocument()`, raw);
|
|
1133
1164
|
setTemplateState("error");
|
|
1134
1165
|
return false;
|
|
1135
1166
|
}
|
|
@@ -1141,7 +1172,8 @@ function usePersistence({
|
|
|
1141
1172
|
setTemplateState("idle");
|
|
1142
1173
|
onTemplateChange?.({ id, title: t(doc.title, language), source: "applied" });
|
|
1143
1174
|
return true;
|
|
1144
|
-
} catch {
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
console.error(`[form-page-builder] loadForm(${id}) threw`, err);
|
|
1145
1177
|
setTemplateState("error");
|
|
1146
1178
|
return false;
|
|
1147
1179
|
}
|
|
@@ -1149,6 +1181,7 @@ function usePersistence({
|
|
|
1149
1181
|
async function deleteForm(id) {
|
|
1150
1182
|
if (!templateManage) return;
|
|
1151
1183
|
const removed = savedForms.find((f) => f.id === id);
|
|
1184
|
+
setTemplateState("loading");
|
|
1152
1185
|
try {
|
|
1153
1186
|
await storage.delete(formKey(id));
|
|
1154
1187
|
const next = savedForms.filter((f) => f.id !== id);
|
|
@@ -1158,6 +1191,7 @@ function usePersistence({
|
|
|
1158
1191
|
setCurrentFormId(null);
|
|
1159
1192
|
savedSnapshot.current = null;
|
|
1160
1193
|
}
|
|
1194
|
+
setTemplateState("idle");
|
|
1161
1195
|
onTemplateChange?.({ id: currentFormId === id ? null : id, title: removed?.title ?? "", source: "deleted" });
|
|
1162
1196
|
} catch {
|
|
1163
1197
|
setTemplateState("error");
|
|
@@ -1665,6 +1699,7 @@ function Palette({
|
|
|
1665
1699
|
] }),
|
|
1666
1700
|
formTypes.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
1667
1701
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...styles.panelHeading, marginTop: 16 }, children: chrome.formFields }),
|
|
1702
|
+
typeof features.maxFields === "number" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { role: "status", style: { ...styles.activeSectionHint, marginBottom: 8, ...atFieldCap ? { color: "var(--fb-danger)" } : {} }, children: atFieldCap ? chrome.fieldLimitReached(features.maxFields) : chrome.fieldCount(fieldCount, features.maxFields) }),
|
|
1668
1703
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: styles.paletteList, children: formTypes.map((f) => {
|
|
1669
1704
|
const Icon = f.icon;
|
|
1670
1705
|
const itemStyle = atFieldCap ? { ...styles.paletteItem, opacity: 0.45, cursor: "not-allowed" } : styles.paletteItem;
|
|
@@ -1910,6 +1945,8 @@ function FieldTicket({
|
|
|
1910
1945
|
{
|
|
1911
1946
|
...dropZoneHandlers,
|
|
1912
1947
|
onClick: onSelect,
|
|
1948
|
+
className: `fb-field fb-field--${field.type} fb-field--${field.id}`,
|
|
1949
|
+
"data-field-id": field.id,
|
|
1913
1950
|
style: {
|
|
1914
1951
|
...styles.ticket,
|
|
1915
1952
|
flex: `1 1 calc(${widthPct} - 10px)`,
|
|
@@ -2439,16 +2476,16 @@ function PreviewPane({ title, sections, onFieldChange, language, strings, chrome
|
|
|
2439
2476
|
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: styles.previewWrap, children: [
|
|
2440
2477
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: { width: "100%", maxWidth, display: "flex", flexDirection: "column", alignItems: "center" }, children: [
|
|
2441
2478
|
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) }),
|
|
2479
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "fb-form", style: { ...styles.previewCard, maxWidth, width: "100%" }, children: [
|
|
2480
|
+
features.formTitle && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h2", { style: styles.previewTitle, children: t(title, language) }),
|
|
2444
2481
|
allFields.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { style: { color: "var(--fb-muted)", fontSize: 14 }, children: strings.addFieldsHint }),
|
|
2445
2482
|
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: [
|
|
2483
|
+
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
2484
|
t(section.title, language) && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h3", { style: styles.sectionRuntimeTitle, children: t(section.title, language) }),
|
|
2448
2485
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: styles.previewGrid, children: section.fields.map((field) => {
|
|
2449
2486
|
const width = effectiveWidth(field.width, device);
|
|
2450
2487
|
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: [
|
|
2488
|
+
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
2489
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(FieldBlock, { field, lang: language, onFieldChange, strings, chrome, error: errors[field.id], onButtonAction: handleButtonAction }),
|
|
2453
2490
|
field.type === "button" && field.id === attemptedButtonId && Object.keys(errors).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("p", { style: styles.formErrorNote, children: [
|
|
2454
2491
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react14.CircleAlert, { size: 13 }),
|
|
@@ -2743,7 +2780,7 @@ var FormBuilder = (0, import_react10.forwardRef)(function FormBuilder2({
|
|
|
2743
2780
|
ensureActiveSection: () => doc.setActiveSectionId((prev) => prev ?? doc.sections[0]?.id ?? null)
|
|
2744
2781
|
});
|
|
2745
2782
|
const jsonDoc = (0, import_react10.useMemo)(() => ({
|
|
2746
|
-
version:
|
|
2783
|
+
version: DOCUMENT_VERSION,
|
|
2747
2784
|
title: doc.title,
|
|
2748
2785
|
theme,
|
|
2749
2786
|
themeOverrides,
|
|
@@ -3021,9 +3058,11 @@ var DARK_THEME = {
|
|
|
3021
3058
|
CLIPBOARD_KEY,
|
|
3022
3059
|
DARK_THEME,
|
|
3023
3060
|
DEFAULT_THEME,
|
|
3061
|
+
DOCUMENT_VERSION,
|
|
3024
3062
|
DRAFT_KEY,
|
|
3025
3063
|
FormBuilder,
|
|
3026
3064
|
INDEX_KEY,
|
|
3065
|
+
compactDocument,
|
|
3027
3066
|
formKey,
|
|
3028
3067
|
localStorageAdapter,
|
|
3029
3068
|
parseTemplate,
|