form-page-builder 2.1.0 → 2.2.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 +48 -15
- package/dist/index.cjs +491 -139
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +86 -16
- package/dist/index.d.ts +86 -16
- package/dist/index.js +487 -142
- package/dist/index.js.map +1 -1
- package/package.json +12 -2
package/dist/index.cjs
CHANGED
|
@@ -21,17 +21,24 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
// src/index.ts
|
|
22
22
|
var index_exports = {};
|
|
23
23
|
__export(index_exports, {
|
|
24
|
+
CLIPBOARD_KEY: () => CLIPBOARD_KEY,
|
|
24
25
|
DARK_THEME: () => DARK_THEME,
|
|
25
26
|
DEFAULT_THEME: () => DEFAULT_THEME,
|
|
27
|
+
DRAFT_KEY: () => DRAFT_KEY,
|
|
26
28
|
FormBuilder: () => FormBuilder_default,
|
|
29
|
+
INDEX_KEY: () => INDEX_KEY,
|
|
27
30
|
default: () => FormBuilder_default,
|
|
28
|
-
|
|
31
|
+
formKey: () => formKey,
|
|
32
|
+
localStorageAdapter: () => localStorageAdapter,
|
|
33
|
+
parseTemplate: () => parseTemplate,
|
|
34
|
+
savedFormId: () => savedFormId,
|
|
35
|
+
serializeTemplate: () => serializeTemplate
|
|
29
36
|
});
|
|
30
37
|
module.exports = __toCommonJS(index_exports);
|
|
31
38
|
|
|
32
39
|
// src/FormBuilder.tsx
|
|
33
|
-
var
|
|
34
|
-
var
|
|
40
|
+
var import_react10 = require("react");
|
|
41
|
+
var import_lucide_react19 = require("lucide-react");
|
|
35
42
|
|
|
36
43
|
// src/constants/fieldTypes.ts
|
|
37
44
|
var import_lucide_react = require("lucide-react");
|
|
@@ -122,6 +129,7 @@ var DEFAULT_STRINGS = {
|
|
|
122
129
|
tooLarge: (n) => `${n}\u4EE5\u4E0B\u306E\u5024\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
123
130
|
}
|
|
124
131
|
};
|
|
132
|
+
DEFAULT_STRINGS.ja;
|
|
125
133
|
|
|
126
134
|
// src/i18n/chrome.ts
|
|
127
135
|
var CHROME = {
|
|
@@ -197,16 +205,33 @@ var CHROME = {
|
|
|
197
205
|
formJson: "Form JSON",
|
|
198
206
|
copy: "Copy",
|
|
199
207
|
copied: "Copied",
|
|
208
|
+
copyTemplate: "Copy template",
|
|
209
|
+
pasteTemplate: "Paste template",
|
|
210
|
+
copyTemplateTitle: "Copy this form as a template you can paste into another builder",
|
|
211
|
+
pasteTemplateTitle: "Replace this form with the copied template",
|
|
212
|
+
pasteTemplateConfirm: "Replace the current form with the copied template? This can't be undone.",
|
|
200
213
|
noTemplates: 'No templates saved yet. Click "Save" in the toolbar to store this form here.',
|
|
214
|
+
noTemplatesReadonly: "No templates available yet.",
|
|
201
215
|
updated: "Updated",
|
|
202
216
|
open: "Open",
|
|
217
|
+
applyTemplate: "Apply",
|
|
203
218
|
saveForm: "Save as template",
|
|
204
219
|
formName: "Template name",
|
|
205
220
|
saveToLibrary: "Save template",
|
|
206
221
|
loadingDraft: "Loading your draft...",
|
|
207
|
-
templatesLimitReached:
|
|
222
|
+
templatesLimitReached: (max) => `You can save up to ${max} template${max === 1 ? "" : "s"}. Delete one to save another.`,
|
|
223
|
+
templatesLimitTitle: "Template limit reached",
|
|
224
|
+
confirm: "Confirm",
|
|
225
|
+
cancel: "Cancel",
|
|
226
|
+
dismiss: "OK",
|
|
227
|
+
confirmReplace: "Replace",
|
|
208
228
|
current: "current",
|
|
209
229
|
customColor: "Custom color",
|
|
230
|
+
templateLabel: (name) => `Template: ${name}`,
|
|
231
|
+
templateEdited: "Edited",
|
|
232
|
+
templateLoading: "Loading template...",
|
|
233
|
+
templateLoadFailed: "Couldn't load that template.",
|
|
234
|
+
templateSaveFailed: "Couldn't save the template.",
|
|
210
235
|
fieldsCount: (n) => `${n} field${n === 1 ? "" : "s"}`,
|
|
211
236
|
fieldTypeInput: "Input",
|
|
212
237
|
fieldTypeTextarea: "Textarea",
|
|
@@ -333,16 +358,33 @@ var CHROME = {
|
|
|
333
358
|
formJson: "\u30D5\u30A9\u30FC\u30E0JSON",
|
|
334
359
|
copy: "\u30B3\u30D4\u30FC",
|
|
335
360
|
copied: "\u30B3\u30D4\u30FC\u3057\u307E\u3057\u305F",
|
|
361
|
+
copyTemplate: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u30B3\u30D4\u30FC",
|
|
362
|
+
pasteTemplate: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u8CBC\u308A\u4ED8\u3051",
|
|
363
|
+
copyTemplateTitle: "\u3053\u306E\u30D5\u30A9\u30FC\u30E0\u3092\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3068\u3057\u3066\u30B3\u30D4\u30FC\u3057\u3001\u4ED6\u306E\u30D3\u30EB\u30C0\u30FC\u306B\u8CBC\u308A\u4ED8\u3051\u307E\u3059",
|
|
364
|
+
pasteTemplateTitle: "\u30B3\u30D4\u30FC\u3057\u305F\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3067\u3053\u306E\u30D5\u30A9\u30FC\u30E0\u3092\u7F6E\u304D\u63DB\u3048\u307E\u3059",
|
|
365
|
+
pasteTemplateConfirm: "\u73FE\u5728\u306E\u30D5\u30A9\u30FC\u30E0\u3092\u30B3\u30D4\u30FC\u3057\u305F\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3067\u7F6E\u304D\u63DB\u3048\u307E\u3059\u304B\uFF1F\u3053\u306E\u64CD\u4F5C\u306F\u53D6\u308A\u6D88\u305B\u307E\u305B\u3093\u3002",
|
|
336
366
|
noTemplates: "\u307E\u3060\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u3042\u308A\u307E\u305B\u3093\u3002\u30C4\u30FC\u30EB\u30D0\u30FC\u306E\u300C\u4FDD\u5B58\u300D\u3092\u30AF\u30EA\u30C3\u30AF\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
367
|
+
noTemplatesReadonly: "\u5229\u7528\u3067\u304D\u308B\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u307E\u3060\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
337
368
|
updated: "\u66F4\u65B0\u65E5\u6642",
|
|
338
369
|
open: "\u958B\u304F",
|
|
370
|
+
applyTemplate: "\u9069\u7528",
|
|
339
371
|
saveForm: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3068\u3057\u3066\u4FDD\u5B58",
|
|
340
372
|
formName: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u540D",
|
|
341
373
|
saveToLibrary: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u4FDD\u5B58",
|
|
342
374
|
loadingDraft: "\u4E0B\u66F8\u304D\u3092\u8AAD\u307F\u8FBC\u3093\u3067\u3044\u307E\u3059...",
|
|
343
|
-
templatesLimitReached:
|
|
375
|
+
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
|
+
templatesLimitTitle: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u306E\u4E0A\u9650\u306B\u9054\u3057\u307E\u3057\u305F",
|
|
377
|
+
confirm: "\u78BA\u5B9A",
|
|
378
|
+
cancel: "\u30AD\u30E3\u30F3\u30BB\u30EB",
|
|
379
|
+
dismiss: "OK",
|
|
380
|
+
confirmReplace: "\u7F6E\u304D\u63DB\u3048\u308B",
|
|
344
381
|
current: "\u73FE\u5728\u7DE8\u96C6\u4E2D",
|
|
345
382
|
customColor: "\u30AB\u30B9\u30BF\u30E0\u30AB\u30E9\u30FC",
|
|
383
|
+
templateLabel: (name) => `\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8: ${name}`,
|
|
384
|
+
templateEdited: "\u7DE8\u96C6\u6E08\u307F",
|
|
385
|
+
templateLoading: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u8AAD\u307F\u8FBC\u307F\u4E2D...",
|
|
386
|
+
templateLoadFailed: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F\u3002",
|
|
387
|
+
templateSaveFailed: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u4FDD\u5B58\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002",
|
|
346
388
|
fieldsCount: (n) => `${n}\u4EF6\u306E\u30D5\u30A3\u30FC\u30EB\u30C9`,
|
|
347
389
|
fieldTypeInput: "\u5165\u529B",
|
|
348
390
|
fieldTypeTextarea: "\u30C6\u30AD\u30B9\u30C8\u30A8\u30EA\u30A2",
|
|
@@ -398,6 +440,7 @@ var CHROME = {
|
|
|
398
440
|
spacingTicketPadding: "\u30D5\u30A3\u30FC\u30EB\u30C9\u5185\u4F59\u767D"
|
|
399
441
|
}
|
|
400
442
|
};
|
|
443
|
+
CHROME.ja;
|
|
401
444
|
|
|
402
445
|
// src/lib/bilingual.ts
|
|
403
446
|
function bi(en = "", ja = "") {
|
|
@@ -633,10 +676,32 @@ function migrateDocument(raw) {
|
|
|
633
676
|
return { title, themeOverrides, sections };
|
|
634
677
|
}
|
|
635
678
|
|
|
679
|
+
// src/lib/template.ts
|
|
680
|
+
var TEMPLATE_FORMAT = 1;
|
|
681
|
+
function serializeTemplate(document2) {
|
|
682
|
+
const envelope = { __fpb: "template", v: TEMPLATE_FORMAT, document: document2 };
|
|
683
|
+
return JSON.stringify(envelope);
|
|
684
|
+
}
|
|
685
|
+
function parseTemplate(raw) {
|
|
686
|
+
if (!raw) return null;
|
|
687
|
+
let parsed;
|
|
688
|
+
try {
|
|
689
|
+
parsed = JSON.parse(raw);
|
|
690
|
+
} catch {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
694
|
+
const obj = parsed;
|
|
695
|
+
const candidate = obj.__fpb === "template" && obj.document ? obj.document : obj;
|
|
696
|
+
return migrateDocument(candidate);
|
|
697
|
+
}
|
|
698
|
+
|
|
636
699
|
// src/lib/features.ts
|
|
700
|
+
var DEFAULT_MAX_TEMPLATES = 5;
|
|
637
701
|
var DEFAULT_FEATURES = {
|
|
638
702
|
naming: true,
|
|
639
|
-
templates: true,
|
|
703
|
+
templates: { enabled: true, manage: true, max: DEFAULT_MAX_TEMPLATES },
|
|
704
|
+
templateClipboard: true,
|
|
640
705
|
newForm: true,
|
|
641
706
|
autosave: true,
|
|
642
707
|
jsonView: true,
|
|
@@ -647,15 +712,24 @@ var DEFAULT_FEATURES = {
|
|
|
647
712
|
contentBlocks: true,
|
|
648
713
|
fieldTypes: true,
|
|
649
714
|
sections: true,
|
|
715
|
+
sectionBackground: true,
|
|
650
716
|
dragReorder: true,
|
|
651
717
|
deviceToggle: true
|
|
652
718
|
};
|
|
719
|
+
function resolveTemplates(t2) {
|
|
720
|
+
if (t2 === false) return { enabled: false, manage: false, max: DEFAULT_MAX_TEMPLATES };
|
|
721
|
+
if (t2 === void 0 || t2 === true) return { enabled: true, manage: true, max: DEFAULT_MAX_TEMPLATES };
|
|
722
|
+
return { enabled: true, manage: t2.manage ?? true, max: t2.max ?? DEFAULT_MAX_TEMPLATES };
|
|
723
|
+
}
|
|
653
724
|
function resolveFeatures(features) {
|
|
654
725
|
return {
|
|
655
726
|
...DEFAULT_FEATURES,
|
|
656
727
|
...features,
|
|
728
|
+
templates: resolveTemplates(features?.templates),
|
|
657
729
|
contentBlocks: Array.isArray(features?.contentBlocks) ? new Set(features.contentBlocks) : features?.contentBlocks ?? DEFAULT_FEATURES.contentBlocks,
|
|
658
|
-
fieldTypes: Array.isArray(features?.fieldTypes) ? new Set(features.fieldTypes) : features?.fieldTypes ?? DEFAULT_FEATURES.fieldTypes
|
|
730
|
+
fieldTypes: Array.isArray(features?.fieldTypes) ? new Set(features.fieldTypes) : features?.fieldTypes ?? DEFAULT_FEATURES.fieldTypes,
|
|
731
|
+
// Unset → follow `sections`, so an existing `sections: false` still hides the picker.
|
|
732
|
+
sectionBackground: features?.sectionBackground ?? features?.sections ?? DEFAULT_FEATURES.sectionBackground
|
|
659
733
|
};
|
|
660
734
|
}
|
|
661
735
|
function isContentBlockEnabled(features, type) {
|
|
@@ -685,17 +759,17 @@ var DEFAULT_THEME = {
|
|
|
685
759
|
|
|
686
760
|
// src/hooks/useTheme.ts
|
|
687
761
|
function useTheme(themeOverrideProp) {
|
|
688
|
-
const
|
|
762
|
+
const [themeOverrides, setThemeOverrides] = (0, import_react.useState)({});
|
|
763
|
+
const baseTheme = (0, import_react.useMemo)(() => ({
|
|
689
764
|
...DEFAULT_THEME,
|
|
690
765
|
...themeOverrideProp || {},
|
|
691
766
|
layout: { ...DEFAULT_THEME.layout, ...themeOverrideProp && themeOverrideProp.layout || {} }
|
|
692
|
-
};
|
|
693
|
-
const
|
|
694
|
-
const theme = {
|
|
767
|
+
}), [themeOverrideProp]);
|
|
768
|
+
const theme = (0, import_react.useMemo)(() => ({
|
|
695
769
|
...baseTheme,
|
|
696
770
|
...themeOverrides,
|
|
697
771
|
layout: { ...baseTheme.layout, ...themeOverrides.layout || {} }
|
|
698
|
-
};
|
|
772
|
+
}), [baseTheme, themeOverrides]);
|
|
699
773
|
function updateThemeColor(key, value) {
|
|
700
774
|
setThemeOverrides((prev) => ({ ...prev, [key]: value }));
|
|
701
775
|
}
|
|
@@ -894,35 +968,49 @@ var import_react3 = require("react");
|
|
|
894
968
|
var DRAFT_KEY = "form-page-builder:draft";
|
|
895
969
|
var INDEX_KEY = "form-page-builder:index";
|
|
896
970
|
var formKey = (id) => `form-page-builder:saved:${id}`;
|
|
971
|
+
var savedFormId = (key) => key.startsWith("form-page-builder:saved:") ? key.slice("form-page-builder:saved:".length) : null;
|
|
972
|
+
var CLIPBOARD_KEY = "form-page-builder:clipboard";
|
|
897
973
|
|
|
898
974
|
// src/hooks/usePersistence.ts
|
|
899
|
-
|
|
975
|
+
function snapshotOf(d) {
|
|
976
|
+
return JSON.stringify({ title: d.title, themeOverrides: d.themeOverrides, sections: d.sections });
|
|
977
|
+
}
|
|
900
978
|
function usePersistence({
|
|
901
979
|
storage,
|
|
902
980
|
autosave,
|
|
981
|
+
templateMax,
|
|
982
|
+
templateManage,
|
|
903
983
|
language,
|
|
904
984
|
chrome,
|
|
905
|
-
document,
|
|
985
|
+
document: document2,
|
|
906
986
|
initialDocument,
|
|
907
987
|
onLoadDocument,
|
|
908
988
|
onLoadThemeOverrides,
|
|
909
989
|
onTitleChange,
|
|
910
990
|
onNewForm,
|
|
991
|
+
onTemplateChange,
|
|
911
992
|
ensureActiveSection
|
|
912
993
|
}) {
|
|
913
994
|
const [currentFormId, setCurrentFormId] = (0, import_react3.useState)(null);
|
|
914
995
|
const [loadingDraft, setLoadingDraft] = (0, import_react3.useState)(true);
|
|
915
996
|
const [saveState, setSaveState] = (0, import_react3.useState)("idle");
|
|
997
|
+
const [templateState, setTemplateState] = (0, import_react3.useState)("idle");
|
|
916
998
|
const [savedForms, setSavedForms] = (0, import_react3.useState)([]);
|
|
917
999
|
const [saveAsPrompt, setSaveAsPrompt] = (0, import_react3.useState)(null);
|
|
1000
|
+
const [notice, setNotice] = (0, import_react3.useState)(null);
|
|
918
1001
|
const autosaveTimer = (0, import_react3.useRef)(null);
|
|
919
1002
|
const hasLoadedOnce = (0, import_react3.useRef)(false);
|
|
1003
|
+
const savedSnapshot = (0, import_react3.useRef)(null);
|
|
1004
|
+
function flashSaved() {
|
|
1005
|
+
setTemplateState("saved");
|
|
1006
|
+
setTimeout(() => setTemplateState((s) => s === "saved" ? "idle" : s), 1500);
|
|
1007
|
+
}
|
|
920
1008
|
async function refreshLibrary() {
|
|
921
1009
|
try {
|
|
922
1010
|
const raw = await storage.get(INDEX_KEY);
|
|
923
1011
|
const list = raw ? JSON.parse(raw) : [];
|
|
924
1012
|
setSavedForms(list.sort((a, b) => b.updatedAt - a.updatedAt));
|
|
925
|
-
} catch
|
|
1013
|
+
} catch {
|
|
926
1014
|
setSavedForms([]);
|
|
927
1015
|
}
|
|
928
1016
|
}
|
|
@@ -946,12 +1034,21 @@ function usePersistence({
|
|
|
946
1034
|
onLoadDocument(doc);
|
|
947
1035
|
onLoadThemeOverrides(doc.themeOverrides);
|
|
948
1036
|
}
|
|
949
|
-
if (parsed.currentFormId)
|
|
1037
|
+
if (parsed.currentFormId && templateManage) {
|
|
1038
|
+
setCurrentFormId(parsed.currentFormId);
|
|
1039
|
+
try {
|
|
1040
|
+
const tplRaw = await storage.get(formKey(parsed.currentFormId));
|
|
1041
|
+
const tplDoc = tplRaw ? migrateDocument(JSON.parse(tplRaw)) : null;
|
|
1042
|
+
savedSnapshot.current = tplDoc ? snapshotOf(tplDoc) : null;
|
|
1043
|
+
} catch {
|
|
1044
|
+
savedSnapshot.current = null;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
950
1047
|
} else {
|
|
951
1048
|
ensureActiveSection();
|
|
952
1049
|
}
|
|
953
1050
|
}
|
|
954
|
-
} catch
|
|
1051
|
+
} catch {
|
|
955
1052
|
ensureActiveSection();
|
|
956
1053
|
} finally {
|
|
957
1054
|
setLoadingDraft(false);
|
|
@@ -966,86 +1063,125 @@ function usePersistence({
|
|
|
966
1063
|
if (autosaveTimer.current) clearTimeout(autosaveTimer.current);
|
|
967
1064
|
autosaveTimer.current = setTimeout(async () => {
|
|
968
1065
|
try {
|
|
969
|
-
await storage.set(DRAFT_KEY, JSON.stringify({ ...
|
|
1066
|
+
await storage.set(DRAFT_KEY, JSON.stringify({ ...document2, currentFormId }));
|
|
970
1067
|
setSaveState("saved");
|
|
971
|
-
} catch
|
|
1068
|
+
} catch {
|
|
972
1069
|
setSaveState("error");
|
|
973
1070
|
}
|
|
974
1071
|
}, 600);
|
|
975
1072
|
return () => {
|
|
976
1073
|
if (autosaveTimer.current) clearTimeout(autosaveTimer.current);
|
|
977
1074
|
};
|
|
978
|
-
}, [
|
|
1075
|
+
}, [document2.title, document2.themeOverrides, document2.sections, currentFormId]);
|
|
979
1076
|
async function saveAs(name) {
|
|
980
|
-
if (
|
|
981
|
-
|
|
1077
|
+
if (!templateManage) return;
|
|
1078
|
+
if (savedForms.length >= templateMax) {
|
|
1079
|
+
setSaveAsPrompt(null);
|
|
1080
|
+
setNotice({ title: chrome.templatesLimitTitle, message: chrome.templatesLimitReached(templateMax) });
|
|
982
1081
|
return;
|
|
983
1082
|
}
|
|
984
1083
|
const id = genFormId();
|
|
985
1084
|
const now = Date.now();
|
|
1085
|
+
setTemplateState("saving");
|
|
986
1086
|
try {
|
|
987
1087
|
const newTitle = bi(name, "");
|
|
988
|
-
|
|
1088
|
+
const docToSave = { ...document2, title: newTitle };
|
|
1089
|
+
await storage.set(formKey(id), JSON.stringify({ ...docToSave, id, updatedAt: now }));
|
|
989
1090
|
const next = [...savedForms, { id, title: name, updatedAt: now }];
|
|
990
1091
|
await storage.set(INDEX_KEY, JSON.stringify(next));
|
|
991
1092
|
setSavedForms(next);
|
|
992
1093
|
setCurrentFormId(id);
|
|
993
1094
|
onTitleChange(newTitle);
|
|
994
1095
|
setSaveAsPrompt(null);
|
|
995
|
-
|
|
996
|
-
|
|
1096
|
+
savedSnapshot.current = snapshotOf(docToSave);
|
|
1097
|
+
flashSaved();
|
|
1098
|
+
onTemplateChange?.({ id, title: name, source: "new" });
|
|
1099
|
+
} catch {
|
|
1100
|
+
setTemplateState("error");
|
|
997
1101
|
}
|
|
998
1102
|
}
|
|
999
1103
|
async function saveExisting() {
|
|
1104
|
+
if (!templateManage) return;
|
|
1000
1105
|
if (!currentFormId) {
|
|
1001
|
-
setSaveAsPrompt({ open: true, suggestedName: t(
|
|
1106
|
+
setSaveAsPrompt({ open: true, suggestedName: t(document2.title, language) });
|
|
1002
1107
|
return;
|
|
1003
1108
|
}
|
|
1004
1109
|
const now = Date.now();
|
|
1110
|
+
setTemplateState("saving");
|
|
1005
1111
|
try {
|
|
1006
|
-
await storage.set(formKey(currentFormId), JSON.stringify({ ...
|
|
1007
|
-
const next = savedForms.map((f) => f.id === currentFormId ? { ...f, title: t(
|
|
1112
|
+
await storage.set(formKey(currentFormId), JSON.stringify({ ...document2, id: currentFormId, updatedAt: now }));
|
|
1113
|
+
const next = savedForms.map((f) => f.id === currentFormId ? { ...f, title: t(document2.title, "en"), updatedAt: now } : f);
|
|
1008
1114
|
setSavedForms(next);
|
|
1009
1115
|
await storage.set(INDEX_KEY, JSON.stringify(next));
|
|
1010
|
-
|
|
1011
|
-
|
|
1116
|
+
savedSnapshot.current = snapshotOf(document2);
|
|
1117
|
+
flashSaved();
|
|
1118
|
+
onTemplateChange?.({ id: currentFormId, title: t(document2.title, language), source: "saved" });
|
|
1119
|
+
} catch {
|
|
1120
|
+
setTemplateState("error");
|
|
1012
1121
|
}
|
|
1013
1122
|
}
|
|
1014
1123
|
async function loadForm(id) {
|
|
1124
|
+
setTemplateState("loading");
|
|
1015
1125
|
try {
|
|
1016
1126
|
const raw = await storage.get(formKey(id));
|
|
1017
|
-
if (!raw)
|
|
1127
|
+
if (!raw) {
|
|
1128
|
+
setTemplateState("error");
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1018
1131
|
const doc = migrateDocument(JSON.parse(raw));
|
|
1019
|
-
if (!doc)
|
|
1132
|
+
if (!doc) {
|
|
1133
|
+
setTemplateState("error");
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1020
1136
|
onLoadDocument(doc);
|
|
1021
1137
|
onLoadThemeOverrides(doc.themeOverrides);
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1138
|
+
const boundId = templateManage ? id : null;
|
|
1139
|
+
setCurrentFormId(boundId);
|
|
1140
|
+
savedSnapshot.current = boundId ? snapshotOf(doc) : null;
|
|
1141
|
+
setTemplateState("idle");
|
|
1142
|
+
onTemplateChange?.({ id, title: t(doc.title, language), source: "applied" });
|
|
1143
|
+
return true;
|
|
1144
|
+
} catch {
|
|
1145
|
+
setTemplateState("error");
|
|
1146
|
+
return false;
|
|
1025
1147
|
}
|
|
1026
1148
|
}
|
|
1027
1149
|
async function deleteForm(id) {
|
|
1150
|
+
if (!templateManage) return;
|
|
1151
|
+
const removed = savedForms.find((f) => f.id === id);
|
|
1028
1152
|
try {
|
|
1029
1153
|
await storage.delete(formKey(id));
|
|
1030
1154
|
const next = savedForms.filter((f) => f.id !== id);
|
|
1031
1155
|
setSavedForms(next);
|
|
1032
1156
|
await storage.set(INDEX_KEY, JSON.stringify(next));
|
|
1033
|
-
if (currentFormId === id)
|
|
1034
|
-
|
|
1157
|
+
if (currentFormId === id) {
|
|
1158
|
+
setCurrentFormId(null);
|
|
1159
|
+
savedSnapshot.current = null;
|
|
1160
|
+
}
|
|
1161
|
+
onTemplateChange?.({ id: currentFormId === id ? null : id, title: removed?.title ?? "", source: "deleted" });
|
|
1162
|
+
} catch {
|
|
1163
|
+
setTemplateState("error");
|
|
1035
1164
|
}
|
|
1036
1165
|
}
|
|
1037
1166
|
function newForm() {
|
|
1038
1167
|
onNewForm();
|
|
1039
1168
|
setCurrentFormId(null);
|
|
1169
|
+
savedSnapshot.current = null;
|
|
1170
|
+
setTemplateState("idle");
|
|
1040
1171
|
}
|
|
1172
|
+
const activeTemplate = currentFormId ? savedForms.find((f) => f.id === currentFormId) ?? null : null;
|
|
1173
|
+
const isTemplateDirty = savedSnapshot.current !== null && snapshotOf(document2) !== savedSnapshot.current;
|
|
1041
1174
|
function dismissSaveAsPrompt() {
|
|
1042
1175
|
setSaveAsPrompt(null);
|
|
1043
1176
|
}
|
|
1044
1177
|
return {
|
|
1045
1178
|
loadingDraft,
|
|
1046
1179
|
saveState,
|
|
1180
|
+
templateState,
|
|
1047
1181
|
savedForms,
|
|
1048
1182
|
currentFormId,
|
|
1183
|
+
activeTemplateTitle: activeTemplate?.title ?? null,
|
|
1184
|
+
isTemplateDirty,
|
|
1049
1185
|
saveAs,
|
|
1050
1186
|
saveExisting,
|
|
1051
1187
|
loadForm,
|
|
@@ -1053,15 +1189,64 @@ function usePersistence({
|
|
|
1053
1189
|
refreshLibrary,
|
|
1054
1190
|
newForm,
|
|
1055
1191
|
saveAsPrompt,
|
|
1056
|
-
dismissSaveAsPrompt
|
|
1192
|
+
dismissSaveAsPrompt,
|
|
1193
|
+
notice,
|
|
1194
|
+
dismissNotice: () => setNotice(null)
|
|
1057
1195
|
};
|
|
1058
1196
|
}
|
|
1059
1197
|
|
|
1060
|
-
// src/hooks/
|
|
1198
|
+
// src/hooks/useTemplateClipboard.ts
|
|
1061
1199
|
var import_react4 = require("react");
|
|
1200
|
+
var sameTabListeners = /* @__PURE__ */ new Set();
|
|
1201
|
+
function notifySameTab() {
|
|
1202
|
+
sameTabListeners.forEach((fn) => fn());
|
|
1203
|
+
}
|
|
1204
|
+
function readKey(key) {
|
|
1205
|
+
try {
|
|
1206
|
+
return typeof window !== "undefined" && window.localStorage ? window.localStorage.getItem(key) : null;
|
|
1207
|
+
} catch {
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
function useTemplateClipboard(clipboardKey = CLIPBOARD_KEY) {
|
|
1212
|
+
const keyRef = (0, import_react4.useRef)(clipboardKey);
|
|
1213
|
+
keyRef.current = clipboardKey;
|
|
1214
|
+
const [hasClipboard, setHasClipboard] = (0, import_react4.useState)(false);
|
|
1215
|
+
const refresh = (0, import_react4.useCallback)(() => {
|
|
1216
|
+
setHasClipboard(parseTemplate(readKey(keyRef.current)) !== null);
|
|
1217
|
+
}, []);
|
|
1218
|
+
(0, import_react4.useEffect)(() => {
|
|
1219
|
+
refresh();
|
|
1220
|
+
const onStorage = (e) => {
|
|
1221
|
+
if (e.key === null || e.key === keyRef.current) refresh();
|
|
1222
|
+
};
|
|
1223
|
+
sameTabListeners.add(refresh);
|
|
1224
|
+
window.addEventListener("storage", onStorage);
|
|
1225
|
+
return () => {
|
|
1226
|
+
sameTabListeners.delete(refresh);
|
|
1227
|
+
window.removeEventListener("storage", onStorage);
|
|
1228
|
+
};
|
|
1229
|
+
}, [refresh]);
|
|
1230
|
+
const copyTemplate = (0, import_react4.useCallback)((document2) => {
|
|
1231
|
+
const str = serializeTemplate(document2);
|
|
1232
|
+
try {
|
|
1233
|
+
window.localStorage?.setItem(keyRef.current, str);
|
|
1234
|
+
} catch {
|
|
1235
|
+
}
|
|
1236
|
+
notifySameTab();
|
|
1237
|
+
refresh();
|
|
1238
|
+
}, [refresh]);
|
|
1239
|
+
const readTemplate = (0, import_react4.useCallback)(() => {
|
|
1240
|
+
return parseTemplate(readKey(keyRef.current));
|
|
1241
|
+
}, []);
|
|
1242
|
+
return { hasClipboard, copyTemplate, readTemplate };
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/hooks/useDragReorder.ts
|
|
1246
|
+
var import_react5 = require("react");
|
|
1062
1247
|
function useDragReorder(reorderWithinSection) {
|
|
1063
|
-
const dragRef = (0,
|
|
1064
|
-
const [dragOverKey, setDragOverKey] = (0,
|
|
1248
|
+
const dragRef = (0, import_react5.useRef)({ sectionId: null, index: null });
|
|
1249
|
+
const [dragOverKey, setDragOverKey] = (0, import_react5.useState)(null);
|
|
1065
1250
|
function getDropZoneHandlers(sectionId, index) {
|
|
1066
1251
|
const dragKey = `${sectionId}:${index}`;
|
|
1067
1252
|
return {
|
|
@@ -1095,7 +1280,7 @@ var import_lucide_react2 = require("lucide-react");
|
|
|
1095
1280
|
var styles = {
|
|
1096
1281
|
app: { fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif", background: "var(--fb-canvas)", borderRadius: 12, overflow: "hidden", border: "1px solid var(--fb-border)", color: "var(--fb-ink)", display: "flex", flexDirection: "column" },
|
|
1097
1282
|
toolbar: { display: "flex", alignItems: "center", justifyContent: "space-between", padding: "var(--fb-space-toolbar) 16px", borderBottom: "1px solid var(--fb-border)", background: "var(--fb-surface)", flexWrap: "wrap", gap: 8, position: "relative" },
|
|
1098
|
-
toolbarLeft: { display: "flex", alignItems: "center", gap: 10 },
|
|
1283
|
+
toolbarLeft: { display: "flex", alignItems: "center", gap: 10, minWidth: 0, flexShrink: 1 },
|
|
1099
1284
|
logoMark: { width: 26, height: 26, borderRadius: 7, background: "var(--fb-primary)", color: "#fff", fontSize: 11, fontWeight: 700, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "'JetBrains Mono', monospace", letterSpacing: "-0.5px" },
|
|
1100
1285
|
titleInput: { border: "none", outline: "none", fontSize: 14, fontWeight: 600, background: "transparent", color: "var(--fb-ink)", padding: "4px 6px", borderRadius: 6, minWidth: 160 },
|
|
1101
1286
|
toolbarRight: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" },
|
|
@@ -1105,6 +1290,8 @@ var styles = {
|
|
|
1105
1290
|
ghostBtn: { display: "flex", alignItems: "center", gap: 6, padding: "6px 12px", borderRadius: 7, border: "1px solid var(--fb-border)", background: "var(--fb-surface)", color: "var(--fb-ink)", fontSize: 13, fontWeight: 500 },
|
|
1106
1291
|
primaryBtn: { display: "flex", alignItems: "center", gap: 6, padding: "6px 13px", borderRadius: 7, border: "1px solid var(--fb-primary)", background: "var(--fb-primary)", color: "#fff", fontSize: 13, fontWeight: 600 },
|
|
1107
1292
|
saveStatus: { display: "flex", alignItems: "center", gap: 5, fontSize: 12, color: "var(--fb-muted)", minWidth: 60 },
|
|
1293
|
+
templateTag: { display: "flex", alignItems: "center", gap: 6, fontSize: 12, color: "var(--fb-muted)", minWidth: 0, flexShrink: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
1294
|
+
toolbarIconBtn: { display: "flex", alignItems: "center", justifyContent: "center", padding: "6px 9px", borderRadius: 7, border: "1px solid var(--fb-border)", background: "var(--fb-surface)", color: "var(--fb-ink)" },
|
|
1108
1295
|
countBadge: { fontSize: 10.5, fontWeight: 700, background: "var(--fb-primary-soft)", color: "var(--fb-primary)", borderRadius: 9, padding: "1px 6px", marginLeft: 2 },
|
|
1109
1296
|
loadingScreen: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", color: "var(--fb-muted)" },
|
|
1110
1297
|
libraryBody: { padding: "var(--fb-space-panel)", overflowY: "auto", display: "flex", flexDirection: "column", gap: 4 },
|
|
@@ -1241,21 +1428,36 @@ function Toolbar({
|
|
|
1241
1428
|
languages,
|
|
1242
1429
|
mode,
|
|
1243
1430
|
saveState,
|
|
1431
|
+
templateState,
|
|
1432
|
+
activeTemplateTitle,
|
|
1433
|
+
templateDirty,
|
|
1244
1434
|
chrome,
|
|
1245
1435
|
features,
|
|
1246
1436
|
savedFormsCount,
|
|
1437
|
+
clipboardReady,
|
|
1438
|
+
templateCopied,
|
|
1247
1439
|
onTitleChange,
|
|
1248
1440
|
onLanguageChange,
|
|
1249
1441
|
onModeChange,
|
|
1250
1442
|
onNewForm,
|
|
1251
1443
|
onOpenLibrary,
|
|
1252
1444
|
onSaveExisting,
|
|
1253
|
-
onOpenJson
|
|
1445
|
+
onOpenJson,
|
|
1446
|
+
onCopyTemplate,
|
|
1447
|
+
onPasteTemplate
|
|
1254
1448
|
}) {
|
|
1255
1449
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles.toolbar, children: [
|
|
1256
1450
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles.toolbarLeft, children: [
|
|
1257
1451
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles.logoMark, children: "FB" }),
|
|
1258
|
-
features.naming ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { value: t(title, language), onChange: (e) => onTitleChange(e.target.value), style: styles.titleInput, "aria-label": "Form title" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles.titleInput, children: t(title, language) })
|
|
1452
|
+
features.naming ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { value: t(title, language), onChange: (e) => onTitleChange(e.target.value), style: styles.titleInput, "aria-label": "Form title" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles.titleInput, children: t(title, language) }),
|
|
1453
|
+
templateState === "loading" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: styles.saveStatus, children: [
|
|
1454
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.Loader2, { size: 12, className: "spin" }),
|
|
1455
|
+
" ",
|
|
1456
|
+
chrome.templateLoading
|
|
1457
|
+
] }) : activeTemplateTitle && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "fb-template-tag", style: styles.templateTag, title: chrome.templateLabel(activeTemplateTitle), children: [
|
|
1458
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { overflow: "hidden", textOverflow: "ellipsis" }, children: chrome.templateLabel(activeTemplateTitle) }),
|
|
1459
|
+
templateDirty && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles.currentBadge, children: chrome.templateEdited })
|
|
1460
|
+
] })
|
|
1259
1461
|
] }),
|
|
1260
1462
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles.toolbarRight, children: [
|
|
1261
1463
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: styles.saveStatus, children: [
|
|
@@ -1288,20 +1490,20 @@ function Toolbar({
|
|
|
1288
1490
|
chrome.preview
|
|
1289
1491
|
] })
|
|
1290
1492
|
] }),
|
|
1291
|
-
(features.newForm || features.templates) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles.toolbarDivider }),
|
|
1493
|
+
(features.newForm || features.templates.enabled) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles.toolbarDivider }),
|
|
1292
1494
|
features.newForm && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { style: styles.ghostBtn, onClick: onNewForm, title: chrome.startNewForm, children: [
|
|
1293
1495
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.FilePlus2, { size: 14 }),
|
|
1294
1496
|
" ",
|
|
1295
1497
|
chrome.newForm
|
|
1296
1498
|
] }),
|
|
1297
|
-
features.templates && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
1499
|
+
features.templates.enabled && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
1298
1500
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { style: styles.ghostBtn, onClick: onOpenLibrary, title: chrome.openTemplatesTitle, children: [
|
|
1299
1501
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.FolderOpen, { size: 14 }),
|
|
1300
1502
|
" ",
|
|
1301
1503
|
chrome.templates,
|
|
1302
1504
|
savedFormsCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles.countBadge, children: savedFormsCount })
|
|
1303
1505
|
] }),
|
|
1304
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { style: styles.primaryBtn, onClick: onSaveExisting, title: chrome.saveToLibraryTitle, children: [
|
|
1506
|
+
features.templates.manage && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { style: styles.primaryBtn, onClick: onSaveExisting, title: chrome.saveToLibraryTitle, children: [
|
|
1305
1507
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.Save, { size: 14 }),
|
|
1306
1508
|
" ",
|
|
1307
1509
|
chrome.save
|
|
@@ -1311,13 +1513,17 @@ function Toolbar({
|
|
|
1311
1513
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.Code2, { size: 14 }),
|
|
1312
1514
|
" ",
|
|
1313
1515
|
chrome.viewJson
|
|
1516
|
+
] }),
|
|
1517
|
+
features.templateClipboard && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
1518
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: styles.toolbarIconBtn, onClick: onCopyTemplate, title: chrome.copyTemplateTitle, "aria-label": chrome.copyTemplate, children: templateCopied ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.Check, { size: 14 }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.ClipboardCopy, { size: 14 }) }),
|
|
1519
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: styles.toolbarIconBtn, onClick: onPasteTemplate, disabled: !clipboardReady, title: chrome.pasteTemplateTitle, "aria-label": chrome.pasteTemplate, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.ClipboardPaste, { size: 14 }) })
|
|
1314
1520
|
] })
|
|
1315
1521
|
] })
|
|
1316
1522
|
] });
|
|
1317
1523
|
}
|
|
1318
1524
|
|
|
1319
1525
|
// src/components/Palette.tsx
|
|
1320
|
-
var
|
|
1526
|
+
var import_react6 = require("react");
|
|
1321
1527
|
var import_lucide_react5 = require("lucide-react");
|
|
1322
1528
|
|
|
1323
1529
|
// src/components/Segmented.tsx
|
|
@@ -1415,7 +1621,7 @@ function Palette({
|
|
|
1415
1621
|
resetTheme,
|
|
1416
1622
|
fieldCount
|
|
1417
1623
|
}) {
|
|
1418
|
-
const [tab, setTab] = (0,
|
|
1624
|
+
const [tab, setTab] = (0, import_react6.useState)("blocks");
|
|
1419
1625
|
const contentTypes = CONTENT_TYPES.filter((f) => isContentBlockEnabled(features, f.type));
|
|
1420
1626
|
const formTypes = FORM_TYPES.filter((f) => isFieldTypeEnabled(features, f.type));
|
|
1421
1627
|
const atFieldCap = typeof features.maxFields === "number" && fieldCount >= features.maxFields;
|
|
@@ -1800,7 +2006,7 @@ function SectionCard({
|
|
|
1800
2006
|
),
|
|
1801
2007
|
section.collapsed && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: styles.miniBadge, children: chrome.fieldsCount(section.fields.length) }),
|
|
1802
2008
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: styles.sectionHeaderActions, onClick: (e) => e.stopPropagation(), children: [
|
|
1803
|
-
features.
|
|
2009
|
+
features.sectionBackground && !section.collapsed && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: styles.swatchRow, children: [
|
|
1804
2010
|
SECTION_BG_SWATCHES.map((c) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", title: c || chrome.none, style: { ...styles.swatchBtn, background: c || "var(--fb-canvas)", ...section.background === c ? styles.swatchBtnActive : {} }, onClick: () => onUpdateBackground(c) }, c || "none")),
|
|
1805
2011
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("input", { type: "color", title: chrome.customColor, value: /^#/.test(section.background) ? section.background : "#ffffff", onChange: (e) => onUpdateBackground(e.target.value), style: styles.colorPickerInput })
|
|
1806
2012
|
] }),
|
|
@@ -2121,7 +2327,7 @@ function InspectorBody({ selected, language, chrome, features, onUpdateField, on
|
|
|
2121
2327
|
}
|
|
2122
2328
|
|
|
2123
2329
|
// src/components/PreviewPane.tsx
|
|
2124
|
-
var
|
|
2330
|
+
var import_react7 = require("react");
|
|
2125
2331
|
var import_lucide_react14 = require("lucide-react");
|
|
2126
2332
|
|
|
2127
2333
|
// src/lib/validate.ts
|
|
@@ -2170,10 +2376,10 @@ function collectFieldValues(fields) {
|
|
|
2170
2376
|
// src/components/PreviewPane.tsx
|
|
2171
2377
|
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
2172
2378
|
function PreviewPane({ title, sections, onFieldChange, language, strings, chrome, baseMaxWidth, features, onSubmit }) {
|
|
2173
|
-
const [device, setDevice] = (0,
|
|
2174
|
-
const [errors, setErrors] = (0,
|
|
2175
|
-
const [submitted, setSubmitted] = (0,
|
|
2176
|
-
const [attemptedButtonId, setAttemptedButtonId] = (0,
|
|
2379
|
+
const [device, setDevice] = (0, import_react7.useState)("laptop");
|
|
2380
|
+
const [errors, setErrors] = (0, import_react7.useState)({});
|
|
2381
|
+
const [submitted, setSubmitted] = (0, import_react7.useState)(null);
|
|
2382
|
+
const [attemptedButtonId, setAttemptedButtonId] = (0, import_react7.useState)(null);
|
|
2177
2383
|
const deviceOptions = buildDeviceOptions(baseMaxWidth, chrome);
|
|
2178
2384
|
const maxWidth = (deviceOptions.find((d) => d.value === device) || deviceOptions[0]).maxWidth;
|
|
2179
2385
|
const allFields = sections.flatMap((s) => s.fields);
|
|
@@ -2278,17 +2484,66 @@ function PreviewPane({ title, sections, onFieldChange, language, strings, chrome
|
|
|
2278
2484
|
|
|
2279
2485
|
// src/components/modals/JsonModal.tsx
|
|
2280
2486
|
var import_lucide_react15 = require("lucide-react");
|
|
2487
|
+
|
|
2488
|
+
// src/hooks/useModalA11y.ts
|
|
2489
|
+
var import_react8 = require("react");
|
|
2490
|
+
var FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
2491
|
+
function useModalA11y(onClose) {
|
|
2492
|
+
const panelRef = (0, import_react8.useRef)(null);
|
|
2493
|
+
const onCloseRef = (0, import_react8.useRef)(onClose);
|
|
2494
|
+
onCloseRef.current = onClose;
|
|
2495
|
+
(0, import_react8.useEffect)(() => {
|
|
2496
|
+
const opener = document.activeElement;
|
|
2497
|
+
const panel = panelRef.current;
|
|
2498
|
+
const focusables = () => Array.from(panel?.querySelectorAll(FOCUSABLE) ?? []);
|
|
2499
|
+
if (panel && !panel.contains(document.activeElement)) {
|
|
2500
|
+
(panel.querySelector("[autofocus]") ?? focusables()[0] ?? panel).focus();
|
|
2501
|
+
}
|
|
2502
|
+
const onKeyDown = (e) => {
|
|
2503
|
+
if (e.key === "Escape") {
|
|
2504
|
+
e.stopPropagation();
|
|
2505
|
+
onCloseRef.current();
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
if (e.key !== "Tab") return;
|
|
2509
|
+
const items = focusables();
|
|
2510
|
+
if (items.length === 0) {
|
|
2511
|
+
e.preventDefault();
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
const first = items[0];
|
|
2515
|
+
const last = items[items.length - 1];
|
|
2516
|
+
const active = document.activeElement;
|
|
2517
|
+
if (e.shiftKey && (active === first || !panel?.contains(active))) {
|
|
2518
|
+
e.preventDefault();
|
|
2519
|
+
last.focus();
|
|
2520
|
+
} else if (!e.shiftKey && active === last) {
|
|
2521
|
+
e.preventDefault();
|
|
2522
|
+
first.focus();
|
|
2523
|
+
}
|
|
2524
|
+
};
|
|
2525
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
2526
|
+
return () => {
|
|
2527
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
2528
|
+
opener?.focus?.();
|
|
2529
|
+
};
|
|
2530
|
+
}, []);
|
|
2531
|
+
return panelRef;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
// src/components/modals/JsonModal.tsx
|
|
2281
2535
|
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
2282
2536
|
function JsonModal({ chrome, jsonString, copied, onCopy, onClose }) {
|
|
2283
|
-
|
|
2537
|
+
const panelRef = useModalA11y(onClose);
|
|
2538
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: styles.modalOverlay, onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { ref: panelRef, style: styles.modal, role: "dialog", "aria-modal": "true", "aria-label": chrome.formJson, onClick: (e) => e.stopPropagation(), children: [
|
|
2284
2539
|
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: styles.modalHeader, children: [
|
|
2285
2540
|
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: { fontWeight: 600, fontSize: 14 }, children: chrome.formJson }),
|
|
2286
2541
|
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { display: "flex", gap: 8 }, children: [
|
|
2287
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("button", { style: styles.ghostBtn, onClick: onCopy, children: [
|
|
2542
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("button", { type: "button", style: styles.ghostBtn, onClick: onCopy, children: [
|
|
2288
2543
|
copied ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react15.Check, { size: 14 }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react15.ClipboardCopy, { size: 14 }),
|
|
2289
2544
|
copied ? chrome.copied : chrome.copy
|
|
2290
2545
|
] }),
|
|
2291
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react15.X, { size: 16 }) })
|
|
2546
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { type: "button", style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react15.X, { size: 16 }) })
|
|
2292
2547
|
] })
|
|
2293
2548
|
] }),
|
|
2294
2549
|
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("pre", { style: styles.jsonPre, children: jsonString })
|
|
@@ -2298,55 +2553,97 @@ function JsonModal({ chrome, jsonString, copied, onCopy, onClose }) {
|
|
|
2298
2553
|
// src/components/modals/TemplatesModal.tsx
|
|
2299
2554
|
var import_lucide_react16 = require("lucide-react");
|
|
2300
2555
|
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
2301
|
-
function TemplatesModal({ chrome, savedForms, currentFormId, onOpen, onDelete, onClose }) {
|
|
2302
|
-
|
|
2556
|
+
function TemplatesModal({ chrome, savedForms, currentFormId, manage, templateState, onOpen, onDelete, onClose }) {
|
|
2557
|
+
const busy = templateState === "loading";
|
|
2558
|
+
const panelRef = useModalA11y(onClose);
|
|
2559
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: styles.modalOverlay, onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { ref: panelRef, style: styles.modal, role: "dialog", "aria-modal": "true", "aria-label": chrome.templates, onClick: (e) => e.stopPropagation(), children: [
|
|
2303
2560
|
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: styles.modalHeader, children: [
|
|
2304
2561
|
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: { fontWeight: 600, fontSize: 14 }, children: chrome.templates }),
|
|
2305
|
-
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react16.X, { size: 16 }) })
|
|
2562
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react16.X, { size: 16 }) })
|
|
2306
2563
|
] }),
|
|
2307
|
-
/* @__PURE__ */ (0, import_jsx_runtime16.
|
|
2308
|
-
/* @__PURE__ */ (0, import_jsx_runtime16.
|
|
2309
|
-
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style:
|
|
2310
|
-
|
|
2311
|
-
|
|
2564
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: styles.libraryBody, children: [
|
|
2565
|
+
savedForms.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: styles.inspectorEmpty, children: manage ? chrome.noTemplates : chrome.noTemplatesReadonly }) : savedForms.map((f) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: styles.libraryRow, children: [
|
|
2566
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { minWidth: 0 }, children: [
|
|
2567
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: styles.libraryRowTitle, children: [
|
|
2568
|
+
f.title,
|
|
2569
|
+
f.id === currentFormId && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: styles.currentBadge, children: chrome.current })
|
|
2570
|
+
] }),
|
|
2571
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: styles.libraryRowMeta, children: [
|
|
2572
|
+
chrome.updated,
|
|
2573
|
+
" ",
|
|
2574
|
+
new Date(f.updatedAt).toLocaleString()
|
|
2575
|
+
] })
|
|
2312
2576
|
] }),
|
|
2313
|
-
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style:
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2577
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { display: "flex", gap: 6, flexShrink: 0 }, children: [
|
|
2578
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("button", { type: "button", style: styles.ghostBtn, disabled: busy, onClick: () => onOpen(f.id), children: [
|
|
2579
|
+
busy && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react16.Loader2, { size: 12, className: "spin" }),
|
|
2580
|
+
" ",
|
|
2581
|
+
manage ? chrome.open : chrome.applyTemplate
|
|
2582
|
+
] }),
|
|
2583
|
+
manage && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", style: { ...styles.iconBtn, ...styles.iconBtnDanger }, disabled: busy, title: chrome.delete, onClick: () => onDelete(f.id), children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react16.Trash2, { size: 13 }) })
|
|
2317
2584
|
] })
|
|
2318
|
-
] }),
|
|
2319
|
-
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { display: "flex", gap:
|
|
2320
|
-
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
2321
|
-
|
|
2585
|
+
] }, f.id)),
|
|
2586
|
+
templateState === "error" && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { style: { ...styles.libraryRowMeta, color: "var(--fb-danger)", display: "flex", alignItems: "center", gap: 5, marginTop: 4 }, children: [
|
|
2587
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react16.AlertCircle, { size: 12 }),
|
|
2588
|
+
" ",
|
|
2589
|
+
chrome.templateLoadFailed
|
|
2322
2590
|
] })
|
|
2323
|
-
] }
|
|
2591
|
+
] })
|
|
2324
2592
|
] }) });
|
|
2325
2593
|
}
|
|
2326
2594
|
|
|
2327
2595
|
// src/components/modals/SaveAsModal.tsx
|
|
2328
|
-
var
|
|
2596
|
+
var import_react9 = require("react");
|
|
2329
2597
|
var import_lucide_react17 = require("lucide-react");
|
|
2330
2598
|
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
2331
|
-
function SaveAsModal({ chrome, suggestedName, onSave, onClose }) {
|
|
2332
|
-
const [name, setName] = (0,
|
|
2333
|
-
|
|
2599
|
+
function SaveAsModal({ chrome, suggestedName, templateState, onSave, onClose }) {
|
|
2600
|
+
const [name, setName] = (0, import_react9.useState)(suggestedName);
|
|
2601
|
+
const saving = templateState === "saving";
|
|
2602
|
+
const canSave = !!name.trim() && !saving;
|
|
2603
|
+
const panelRef = useModalA11y(onClose);
|
|
2604
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: styles.modalOverlay, onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { ref: panelRef, style: { ...styles.modal, maxWidth: 380 }, role: "dialog", "aria-modal": "true", "aria-label": chrome.saveForm, onClick: (e) => e.stopPropagation(), children: [
|
|
2334
2605
|
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { style: styles.modalHeader, children: [
|
|
2335
2606
|
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { style: { fontWeight: 600, fontSize: 14 }, children: chrome.saveForm }),
|
|
2336
|
-
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react17.X, { size: 16 }) })
|
|
2607
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react17.X, { size: 16 }) })
|
|
2337
2608
|
] }),
|
|
2338
2609
|
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { style: { padding: 16 }, children: [
|
|
2339
2610
|
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("label", { style: styles.propLabel, children: chrome.formName }),
|
|
2340
|
-
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("input", { autoFocus: true, style: styles.propInput, value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => e.key === "Enter" &&
|
|
2341
|
-
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { style: { ...styles.primaryBtn, width: "100%", justifyContent: "center", marginTop: 14 }, disabled: !
|
|
2342
|
-
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react17.Save, { size: 14 }),
|
|
2611
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("input", { autoFocus: true, "aria-label": chrome.formName, style: styles.propInput, value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => e.key === "Enter" && canSave && onSave(name.trim()) }),
|
|
2612
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", style: { ...styles.primaryBtn, width: "100%", justifyContent: "center", marginTop: 14 }, disabled: !canSave, onClick: () => onSave(name.trim()), children: [
|
|
2613
|
+
saving ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react17.Loader2, { size: 14, className: "spin" }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react17.Save, { size: 14 }),
|
|
2343
2614
|
" ",
|
|
2344
2615
|
chrome.saveToLibrary
|
|
2616
|
+
] }),
|
|
2617
|
+
templateState === "error" && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { style: { fontSize: 12, color: "var(--fb-danger)", display: "flex", alignItems: "center", gap: 5, marginTop: 10 }, children: [
|
|
2618
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react17.AlertCircle, { size: 12 }),
|
|
2619
|
+
" ",
|
|
2620
|
+
chrome.templateSaveFailed
|
|
2345
2621
|
] })
|
|
2346
2622
|
] })
|
|
2347
2623
|
] }) });
|
|
2348
2624
|
}
|
|
2349
2625
|
|
|
2626
|
+
// src/components/modals/ConfirmModal.tsx
|
|
2627
|
+
var import_lucide_react18 = require("lucide-react");
|
|
2628
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
2629
|
+
function ConfirmModal({ chrome, title, message, onConfirm, confirmLabel, tone = "default", onClose }) {
|
|
2630
|
+
const panelRef = useModalA11y(onClose);
|
|
2631
|
+
const confirmStyle = tone === "danger" ? { ...styles.primaryBtn, background: "var(--fb-danger)", borderColor: "var(--fb-danger)" } : styles.primaryBtn;
|
|
2632
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: styles.modalOverlay, onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { ref: panelRef, style: { ...styles.modal, maxWidth: 380 }, role: "alertdialog", "aria-modal": "true", "aria-label": title, onClick: (e) => e.stopPropagation(), children: [
|
|
2633
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: styles.modalHeader, children: [
|
|
2634
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { style: { fontWeight: 600, fontSize: 14 }, children: title }),
|
|
2635
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react18.X, { size: 16 }) })
|
|
2636
|
+
] }),
|
|
2637
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { padding: 16 }, children: [
|
|
2638
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { style: { margin: 0, fontSize: 13, lineHeight: 1.5, color: "var(--fb-ink)" }, children: message }),
|
|
2639
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 16 }, children: onConfirm ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
|
|
2640
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", autoFocus: true, style: styles.ghostBtn, onClick: onClose, children: chrome.cancel }),
|
|
2641
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", style: confirmStyle, onClick: onConfirm, children: confirmLabel ?? chrome.confirm })
|
|
2642
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", autoFocus: true, style: styles.primaryBtn, onClick: onClose, children: chrome.dismiss }) })
|
|
2643
|
+
] })
|
|
2644
|
+
] }) });
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2350
2647
|
// src/styles/globalCss.ts
|
|
2351
2648
|
var css = `
|
|
2352
2649
|
* { box-sizing: border-box; }
|
|
@@ -2354,20 +2651,14 @@ var css = `
|
|
|
2354
2651
|
input, textarea, select { font-family: inherit; }
|
|
2355
2652
|
@keyframes fb-spin { to { transform: rotate(360deg); } }
|
|
2356
2653
|
.spin { animation: fb-spin 0.8s linear infinite; }
|
|
2357
|
-
/*
|
|
2358
|
-
than growing past whatever space its host container gives it; the
|
|
2359
|
-
second declaration upgrades to the dynamic viewport unit on browsers
|
|
2360
|
-
that support it (mobile browser chrome resizing the viewport), and is
|
|
2361
|
-
simply ignored by ones that don't. */
|
|
2654
|
+
/* Cap at viewport height so the widget scrolls internally; dvh upgrade is ignored where unsupported. */
|
|
2362
2655
|
.fb-root { max-height: 100vh; max-height: 100dvh; }
|
|
2363
2656
|
|
|
2364
|
-
/*
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
buttons, tracked by .fb-work-area's data-mobile-panel attribute,
|
|
2370
|
-
and dismissible via .fb-mobile-backdrop. */
|
|
2657
|
+
/* Toolbar's active-template label is nice-to-have \u2014 drop it first when width is tight. */
|
|
2658
|
+
@media (max-width: 600px) { .fb-template-tag { display: none; } }
|
|
2659
|
+
|
|
2660
|
+
/* Under 720px: Palette/Inspector become drawers over Canvas, toggled by .fb-mobile-bar,
|
|
2661
|
+
tracked by .fb-work-area[data-mobile-panel], dismissed via .fb-mobile-backdrop. */
|
|
2371
2662
|
.fb-mobile-bar { display: none; align-items: center; gap: 6px; padding: 6px 10px; border-bottom: 1px solid var(--fb-border); background: var(--fb-surface); flex-shrink: 0; }
|
|
2372
2663
|
.fb-mobile-btn { display: flex; align-items: center; gap: 6px; padding: 6px 10px; border-radius: 7px; border: 1px solid var(--fb-border); background: var(--fb-surface); color: var(--fb-muted); font-size: 12.5px; font-weight: 600; }
|
|
2373
2664
|
.fb-mobile-btn[aria-pressed="true"] { border-color: var(--fb-primary); background: var(--fb-primary-soft); color: var(--fb-primary); }
|
|
@@ -2393,8 +2684,8 @@ var css = `
|
|
|
2393
2684
|
`;
|
|
2394
2685
|
|
|
2395
2686
|
// src/FormBuilder.tsx
|
|
2396
|
-
var
|
|
2397
|
-
var FormBuilder = (0,
|
|
2687
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
2688
|
+
var FormBuilder = (0, import_react10.forwardRef)(function FormBuilder2({
|
|
2398
2689
|
theme: themeOverrideProp,
|
|
2399
2690
|
language: languageOverride,
|
|
2400
2691
|
languages = DEFAULT_LANGUAGES,
|
|
@@ -2405,31 +2696,38 @@ var FormBuilder = (0, import_react8.forwardRef)(function FormBuilder2({
|
|
|
2405
2696
|
onSubmit,
|
|
2406
2697
|
initialDocument,
|
|
2407
2698
|
initialMode,
|
|
2408
|
-
onModeChange
|
|
2699
|
+
onModeChange,
|
|
2700
|
+
onTemplateChange,
|
|
2701
|
+
templateClipboardKey
|
|
2409
2702
|
}, ref) {
|
|
2410
2703
|
const features = resolveFeatures(featuresProp);
|
|
2411
2704
|
const storage = storageProp ?? localStorageAdapter;
|
|
2412
2705
|
const { theme, updateThemeColor, updateThemeLayout, resetTheme, replaceThemeOverrides, themeOverrides } = useTheme(themeOverrideProp);
|
|
2413
|
-
const [language, setLanguage] = (0,
|
|
2706
|
+
const [language, setLanguage] = (0, import_react10.useState)(languageOverride || languages[0]?.code || "en");
|
|
2414
2707
|
const strings = { ...DEFAULT_STRINGS[language] || DEFAULT_STRINGS.en, ...stringsOverride && stringsOverride[language] || {} };
|
|
2415
2708
|
const chrome = { ...CHROME[language] || CHROME.en, ...chromeOverride && chromeOverride[language] || {} };
|
|
2416
2709
|
const doc = useFormDocument({ language, chrome });
|
|
2417
2710
|
const drag = useDragReorder(doc.reorderWithinSection);
|
|
2418
|
-
const [mode, setModeState] = (0,
|
|
2711
|
+
const [mode, setModeState] = (0, import_react10.useState)(initialMode ?? "build");
|
|
2419
2712
|
const setMode = (next) => {
|
|
2420
2713
|
setModeState(next);
|
|
2421
2714
|
onModeChange?.(next);
|
|
2422
2715
|
};
|
|
2423
|
-
(0,
|
|
2716
|
+
(0, import_react10.useEffect)(() => {
|
|
2424
2717
|
onModeChange?.(mode);
|
|
2425
2718
|
}, []);
|
|
2426
|
-
const [showJson, setShowJson] = (0,
|
|
2427
|
-
const [showLibrary, setShowLibrary] = (0,
|
|
2428
|
-
const [copied, setCopied] = (0,
|
|
2429
|
-
const [
|
|
2719
|
+
const [showJson, setShowJson] = (0, import_react10.useState)(false);
|
|
2720
|
+
const [showLibrary, setShowLibrary] = (0, import_react10.useState)(false);
|
|
2721
|
+
const [copied, setCopied] = (0, import_react10.useState)(false);
|
|
2722
|
+
const [templateCopied, setTemplateCopied] = (0, import_react10.useState)(false);
|
|
2723
|
+
const [pastePrompt, setPastePrompt] = (0, import_react10.useState)(false);
|
|
2724
|
+
const clipboard = useTemplateClipboard(templateClipboardKey);
|
|
2725
|
+
const [mobilePanel, setMobilePanel] = (0, import_react10.useState)("none");
|
|
2430
2726
|
const persistence = usePersistence({
|
|
2431
2727
|
storage,
|
|
2432
2728
|
autosave: features.autosave,
|
|
2729
|
+
templateMax: features.templates.max,
|
|
2730
|
+
templateManage: features.templates.manage,
|
|
2433
2731
|
language,
|
|
2434
2732
|
chrome,
|
|
2435
2733
|
document: { title: doc.title, themeOverrides, sections: doc.sections },
|
|
@@ -2441,16 +2739,17 @@ var FormBuilder = (0, import_react8.forwardRef)(function FormBuilder2({
|
|
|
2441
2739
|
doc.resetToBlank();
|
|
2442
2740
|
resetTheme();
|
|
2443
2741
|
},
|
|
2742
|
+
onTemplateChange,
|
|
2444
2743
|
ensureActiveSection: () => doc.setActiveSectionId((prev) => prev ?? doc.sections[0]?.id ?? null)
|
|
2445
2744
|
});
|
|
2446
|
-
const jsonDoc = {
|
|
2745
|
+
const jsonDoc = (0, import_react10.useMemo)(() => ({
|
|
2447
2746
|
version: 5,
|
|
2448
2747
|
title: doc.title,
|
|
2449
2748
|
theme,
|
|
2450
2749
|
themeOverrides,
|
|
2451
2750
|
sections: doc.sections.map((s) => ({ id: s.id, title: s.title, background: s.background, collapsed: s.collapsed, fields: s.fields }))
|
|
2452
|
-
};
|
|
2453
|
-
const jsonString = JSON.stringify(jsonDoc, null, 2);
|
|
2751
|
+
}), [doc.title, doc.sections, theme, themeOverrides]);
|
|
2752
|
+
const jsonString = (0, import_react10.useMemo)(() => JSON.stringify(jsonDoc, null, 2), [jsonDoc]);
|
|
2454
2753
|
function copyJson() {
|
|
2455
2754
|
navigator.clipboard.writeText(jsonString).then(() => {
|
|
2456
2755
|
setCopied(true);
|
|
@@ -2458,15 +2757,28 @@ var FormBuilder = (0, import_react8.forwardRef)(function FormBuilder2({
|
|
|
2458
2757
|
}).catch(() => {
|
|
2459
2758
|
});
|
|
2460
2759
|
}
|
|
2461
|
-
|
|
2760
|
+
function applyDocument(migrated) {
|
|
2761
|
+
if (!migrated) return false;
|
|
2762
|
+
doc.loadDocument(migrated);
|
|
2763
|
+
replaceThemeOverrides(migrated.themeOverrides);
|
|
2764
|
+
return true;
|
|
2765
|
+
}
|
|
2766
|
+
function copyTemplate() {
|
|
2767
|
+
clipboard.copyTemplate(jsonDoc);
|
|
2768
|
+
setTemplateCopied(true);
|
|
2769
|
+
setTimeout(() => setTemplateCopied(false), 1500);
|
|
2770
|
+
}
|
|
2771
|
+
function pasteTemplate() {
|
|
2772
|
+
if (clipboard.readTemplate()) setPastePrompt(true);
|
|
2773
|
+
}
|
|
2774
|
+
(0, import_react10.useImperativeHandle)(ref, () => ({
|
|
2462
2775
|
getDocument: () => jsonDoc,
|
|
2463
2776
|
exportJson: () => jsonString,
|
|
2464
2777
|
loadDocument: (raw) => {
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
}
|
|
2778
|
+
applyDocument(migrateDocument(raw));
|
|
2779
|
+
},
|
|
2780
|
+
getTemplate: () => ({ __fpb: "template", v: TEMPLATE_FORMAT, document: jsonDoc }),
|
|
2781
|
+
loadTemplate: (input) => applyDocument(parseTemplate(typeof input === "string" ? input : JSON.stringify(input)))
|
|
2470
2782
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2471
2783
|
}), [jsonDoc, jsonString]);
|
|
2472
2784
|
const fieldCount = doc.sections.reduce((n, s) => n + s.fields.filter((f) => !getMeta(f.type).isContent).length, 0);
|
|
@@ -2493,9 +2805,9 @@ var FormBuilder = (0, import_react8.forwardRef)(function FormBuilder2({
|
|
|
2493
2805
|
"--fb-space-panel": `${theme.layout.panelPadding}px`,
|
|
2494
2806
|
"--fb-space-ticket": `${theme.layout.ticketPadding}px`
|
|
2495
2807
|
};
|
|
2496
|
-
return /* @__PURE__ */ (0,
|
|
2497
|
-
/* @__PURE__ */ (0,
|
|
2498
|
-
/* @__PURE__ */ (0,
|
|
2808
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "fb-root", style: rootStyle, children: [
|
|
2809
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("style", { children: css }),
|
|
2810
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
2499
2811
|
Toolbar,
|
|
2500
2812
|
{
|
|
2501
2813
|
title: doc.title,
|
|
@@ -2503,24 +2815,31 @@ var FormBuilder = (0, import_react8.forwardRef)(function FormBuilder2({
|
|
|
2503
2815
|
languages,
|
|
2504
2816
|
mode,
|
|
2505
2817
|
saveState: persistence.saveState,
|
|
2818
|
+
templateState: persistence.templateState,
|
|
2819
|
+
activeTemplateTitle: persistence.activeTemplateTitle,
|
|
2820
|
+
templateDirty: persistence.isTemplateDirty,
|
|
2506
2821
|
chrome,
|
|
2507
2822
|
features,
|
|
2508
2823
|
savedFormsCount: persistence.savedForms.length,
|
|
2824
|
+
clipboardReady: clipboard.hasClipboard,
|
|
2825
|
+
templateCopied,
|
|
2509
2826
|
onTitleChange: doc.updateTitle,
|
|
2510
2827
|
onLanguageChange: setLanguage,
|
|
2511
2828
|
onModeChange: setMode,
|
|
2512
2829
|
onNewForm: persistence.newForm,
|
|
2513
2830
|
onOpenLibrary: () => setShowLibrary(true),
|
|
2514
2831
|
onSaveExisting: persistence.saveExisting,
|
|
2515
|
-
onOpenJson: () => setShowJson(true)
|
|
2832
|
+
onOpenJson: () => setShowJson(true),
|
|
2833
|
+
onCopyTemplate: copyTemplate,
|
|
2834
|
+
onPasteTemplate: pasteTemplate
|
|
2516
2835
|
}
|
|
2517
2836
|
),
|
|
2518
|
-
persistence.loadingDraft ? /* @__PURE__ */ (0,
|
|
2519
|
-
/* @__PURE__ */ (0,
|
|
2520
|
-
/* @__PURE__ */ (0,
|
|
2521
|
-
] }) : mode === "build" ? /* @__PURE__ */ (0,
|
|
2522
|
-
/* @__PURE__ */ (0,
|
|
2523
|
-
/* @__PURE__ */ (0,
|
|
2837
|
+
persistence.loadingDraft ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { style: styles.loadingScreen, children: [
|
|
2838
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react19.Loader2, { size: 20, className: "spin" }),
|
|
2839
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { style: { marginTop: 8, fontSize: 13, color: "var(--fb-muted)" }, children: chrome.loadingDraft })
|
|
2840
|
+
] }) : mode === "build" ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "fb-work-area", style: styles.workArea, "data-mobile-panel": mobilePanel, children: [
|
|
2841
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "fb-mobile-bar", children: [
|
|
2842
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
|
|
2524
2843
|
"button",
|
|
2525
2844
|
{
|
|
2526
2845
|
type: "button",
|
|
@@ -2528,13 +2847,13 @@ var FormBuilder = (0, import_react8.forwardRef)(function FormBuilder2({
|
|
|
2528
2847
|
"aria-pressed": mobilePanel === "palette",
|
|
2529
2848
|
onClick: () => setMobilePanel((p) => p === "palette" ? "none" : "palette"),
|
|
2530
2849
|
children: [
|
|
2531
|
-
/* @__PURE__ */ (0,
|
|
2850
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react19.Menu, { size: 14 }),
|
|
2532
2851
|
" ",
|
|
2533
2852
|
chrome.paletteTabBlocks
|
|
2534
2853
|
]
|
|
2535
2854
|
}
|
|
2536
2855
|
),
|
|
2537
|
-
/* @__PURE__ */ (0,
|
|
2856
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
|
|
2538
2857
|
"button",
|
|
2539
2858
|
{
|
|
2540
2859
|
type: "button",
|
|
@@ -2542,16 +2861,16 @@ var FormBuilder = (0, import_react8.forwardRef)(function FormBuilder2({
|
|
|
2542
2861
|
"aria-pressed": mobilePanel === "inspector",
|
|
2543
2862
|
onClick: () => setMobilePanel((p) => p === "inspector" ? "none" : "inspector"),
|
|
2544
2863
|
children: [
|
|
2545
|
-
/* @__PURE__ */ (0,
|
|
2864
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react19.SlidersHorizontal, { size: 14 }),
|
|
2546
2865
|
" ",
|
|
2547
2866
|
chrome.properties
|
|
2548
2867
|
]
|
|
2549
2868
|
}
|
|
2550
2869
|
)
|
|
2551
2870
|
] }),
|
|
2552
|
-
/* @__PURE__ */ (0,
|
|
2553
|
-
mobilePanel !== "none" && /* @__PURE__ */ (0,
|
|
2554
|
-
/* @__PURE__ */ (0,
|
|
2871
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "fb-canvas-area", style: styles.workArea, children: [
|
|
2872
|
+
mobilePanel !== "none" && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "fb-mobile-backdrop", onClick: () => setMobilePanel("none") }),
|
|
2873
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
2555
2874
|
Palette,
|
|
2556
2875
|
{
|
|
2557
2876
|
activeSectionLabel,
|
|
@@ -2568,7 +2887,7 @@ var FormBuilder = (0, import_react8.forwardRef)(function FormBuilder2({
|
|
|
2568
2887
|
resetTheme
|
|
2569
2888
|
}
|
|
2570
2889
|
),
|
|
2571
|
-
/* @__PURE__ */ (0,
|
|
2890
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
2572
2891
|
Canvas,
|
|
2573
2892
|
{
|
|
2574
2893
|
sections: doc.sections,
|
|
@@ -2600,7 +2919,7 @@ var FormBuilder = (0, import_react8.forwardRef)(function FormBuilder2({
|
|
|
2600
2919
|
getDragHandleProps: drag.getDragHandleProps
|
|
2601
2920
|
}
|
|
2602
2921
|
),
|
|
2603
|
-
/* @__PURE__ */ (0,
|
|
2922
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
2604
2923
|
Inspector,
|
|
2605
2924
|
{
|
|
2606
2925
|
selected: doc.selected,
|
|
@@ -2615,7 +2934,7 @@ var FormBuilder = (0, import_react8.forwardRef)(function FormBuilder2({
|
|
|
2615
2934
|
}
|
|
2616
2935
|
)
|
|
2617
2936
|
] })
|
|
2618
|
-
] }) : /* @__PURE__ */ (0,
|
|
2937
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
2619
2938
|
PreviewPane,
|
|
2620
2939
|
{
|
|
2621
2940
|
title: doc.title,
|
|
@@ -2629,29 +2948,55 @@ var FormBuilder = (0, import_react8.forwardRef)(function FormBuilder2({
|
|
|
2629
2948
|
onSubmit
|
|
2630
2949
|
}
|
|
2631
2950
|
),
|
|
2632
|
-
showJson && /* @__PURE__ */ (0,
|
|
2633
|
-
showLibrary && /* @__PURE__ */ (0,
|
|
2951
|
+
showJson && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(JsonModal, { chrome, jsonString, copied, onCopy: copyJson, onClose: () => setShowJson(false) }),
|
|
2952
|
+
showLibrary && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
2634
2953
|
TemplatesModal,
|
|
2635
2954
|
{
|
|
2636
2955
|
chrome,
|
|
2637
2956
|
savedForms: persistence.savedForms,
|
|
2638
2957
|
currentFormId: persistence.currentFormId,
|
|
2958
|
+
manage: features.templates.manage,
|
|
2959
|
+
templateState: persistence.templateState,
|
|
2639
2960
|
onOpen: async (id) => {
|
|
2640
|
-
await persistence.loadForm(id);
|
|
2641
|
-
setShowLibrary(false);
|
|
2961
|
+
if (await persistence.loadForm(id)) setShowLibrary(false);
|
|
2642
2962
|
},
|
|
2643
2963
|
onDelete: persistence.deleteForm,
|
|
2644
2964
|
onClose: () => setShowLibrary(false)
|
|
2645
2965
|
}
|
|
2646
2966
|
),
|
|
2647
|
-
persistence.saveAsPrompt && /* @__PURE__ */ (0,
|
|
2967
|
+
persistence.saveAsPrompt && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
2648
2968
|
SaveAsModal,
|
|
2649
2969
|
{
|
|
2650
2970
|
chrome,
|
|
2651
2971
|
suggestedName: persistence.saveAsPrompt.suggestedName,
|
|
2972
|
+
templateState: persistence.templateState,
|
|
2652
2973
|
onSave: persistence.saveAs,
|
|
2653
2974
|
onClose: persistence.dismissSaveAsPrompt
|
|
2654
2975
|
}
|
|
2976
|
+
),
|
|
2977
|
+
pastePrompt && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
2978
|
+
ConfirmModal,
|
|
2979
|
+
{
|
|
2980
|
+
chrome,
|
|
2981
|
+
title: chrome.pasteTemplate,
|
|
2982
|
+
message: chrome.pasteTemplateConfirm,
|
|
2983
|
+
confirmLabel: chrome.confirmReplace,
|
|
2984
|
+
tone: "danger",
|
|
2985
|
+
onConfirm: () => {
|
|
2986
|
+
applyDocument(clipboard.readTemplate());
|
|
2987
|
+
setPastePrompt(false);
|
|
2988
|
+
},
|
|
2989
|
+
onClose: () => setPastePrompt(false)
|
|
2990
|
+
}
|
|
2991
|
+
),
|
|
2992
|
+
persistence.notice && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
2993
|
+
ConfirmModal,
|
|
2994
|
+
{
|
|
2995
|
+
chrome,
|
|
2996
|
+
title: persistence.notice.title,
|
|
2997
|
+
message: persistence.notice.message,
|
|
2998
|
+
onClose: persistence.dismissNotice
|
|
2999
|
+
}
|
|
2655
3000
|
)
|
|
2656
3001
|
] });
|
|
2657
3002
|
});
|
|
@@ -2673,9 +3018,16 @@ var DARK_THEME = {
|
|
|
2673
3018
|
};
|
|
2674
3019
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2675
3020
|
0 && (module.exports = {
|
|
3021
|
+
CLIPBOARD_KEY,
|
|
2676
3022
|
DARK_THEME,
|
|
2677
3023
|
DEFAULT_THEME,
|
|
3024
|
+
DRAFT_KEY,
|
|
2678
3025
|
FormBuilder,
|
|
2679
|
-
|
|
3026
|
+
INDEX_KEY,
|
|
3027
|
+
formKey,
|
|
3028
|
+
localStorageAdapter,
|
|
3029
|
+
parseTemplate,
|
|
3030
|
+
savedFormId,
|
|
3031
|
+
serializeTemplate
|
|
2680
3032
|
});
|
|
2681
3033
|
//# sourceMappingURL=index.cjs.map
|