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.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/FormBuilder.tsx
|
|
4
|
-
import { forwardRef, useEffect as
|
|
5
|
-
import { Loader2 as
|
|
4
|
+
import { forwardRef, useEffect as useEffect4, useImperativeHandle, useMemo as useMemo2, useState as useState9 } from "react";
|
|
5
|
+
import { Loader2 as Loader24, Menu, SlidersHorizontal } from "lucide-react";
|
|
6
6
|
|
|
7
7
|
// src/constants/fieldTypes.ts
|
|
8
8
|
import {
|
|
@@ -104,6 +104,7 @@ var DEFAULT_STRINGS = {
|
|
|
104
104
|
tooLarge: (n) => `${n}\u4EE5\u4E0B\u306E\u5024\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
105
105
|
}
|
|
106
106
|
};
|
|
107
|
+
DEFAULT_STRINGS.ja;
|
|
107
108
|
|
|
108
109
|
// src/i18n/chrome.ts
|
|
109
110
|
var CHROME = {
|
|
@@ -179,16 +180,33 @@ var CHROME = {
|
|
|
179
180
|
formJson: "Form JSON",
|
|
180
181
|
copy: "Copy",
|
|
181
182
|
copied: "Copied",
|
|
183
|
+
copyTemplate: "Copy template",
|
|
184
|
+
pasteTemplate: "Paste template",
|
|
185
|
+
copyTemplateTitle: "Copy this form as a template you can paste into another builder",
|
|
186
|
+
pasteTemplateTitle: "Replace this form with the copied template",
|
|
187
|
+
pasteTemplateConfirm: "Replace the current form with the copied template? This can't be undone.",
|
|
182
188
|
noTemplates: 'No templates saved yet. Click "Save" in the toolbar to store this form here.',
|
|
189
|
+
noTemplatesReadonly: "No templates available yet.",
|
|
183
190
|
updated: "Updated",
|
|
184
191
|
open: "Open",
|
|
192
|
+
applyTemplate: "Apply",
|
|
185
193
|
saveForm: "Save as template",
|
|
186
194
|
formName: "Template name",
|
|
187
195
|
saveToLibrary: "Save template",
|
|
188
196
|
loadingDraft: "Loading your draft...",
|
|
189
|
-
templatesLimitReached:
|
|
197
|
+
templatesLimitReached: (max) => `You can save up to ${max} template${max === 1 ? "" : "s"}. Delete one to save another.`,
|
|
198
|
+
templatesLimitTitle: "Template limit reached",
|
|
199
|
+
confirm: "Confirm",
|
|
200
|
+
cancel: "Cancel",
|
|
201
|
+
dismiss: "OK",
|
|
202
|
+
confirmReplace: "Replace",
|
|
190
203
|
current: "current",
|
|
191
204
|
customColor: "Custom color",
|
|
205
|
+
templateLabel: (name) => `Template: ${name}`,
|
|
206
|
+
templateEdited: "Edited",
|
|
207
|
+
templateLoading: "Loading template...",
|
|
208
|
+
templateLoadFailed: "Couldn't load that template.",
|
|
209
|
+
templateSaveFailed: "Couldn't save the template.",
|
|
192
210
|
fieldsCount: (n) => `${n} field${n === 1 ? "" : "s"}`,
|
|
193
211
|
fieldTypeInput: "Input",
|
|
194
212
|
fieldTypeTextarea: "Textarea",
|
|
@@ -315,16 +333,33 @@ var CHROME = {
|
|
|
315
333
|
formJson: "\u30D5\u30A9\u30FC\u30E0JSON",
|
|
316
334
|
copy: "\u30B3\u30D4\u30FC",
|
|
317
335
|
copied: "\u30B3\u30D4\u30FC\u3057\u307E\u3057\u305F",
|
|
336
|
+
copyTemplate: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u30B3\u30D4\u30FC",
|
|
337
|
+
pasteTemplate: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u8CBC\u308A\u4ED8\u3051",
|
|
338
|
+
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",
|
|
339
|
+
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",
|
|
340
|
+
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",
|
|
318
341
|
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",
|
|
342
|
+
noTemplatesReadonly: "\u5229\u7528\u3067\u304D\u308B\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u307E\u3060\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
319
343
|
updated: "\u66F4\u65B0\u65E5\u6642",
|
|
320
344
|
open: "\u958B\u304F",
|
|
345
|
+
applyTemplate: "\u9069\u7528",
|
|
321
346
|
saveForm: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3068\u3057\u3066\u4FDD\u5B58",
|
|
322
347
|
formName: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u540D",
|
|
323
348
|
saveToLibrary: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u4FDD\u5B58",
|
|
324
349
|
loadingDraft: "\u4E0B\u66F8\u304D\u3092\u8AAD\u307F\u8FBC\u3093\u3067\u3044\u307E\u3059...",
|
|
325
|
-
templatesLimitReached:
|
|
350
|
+
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`,
|
|
351
|
+
templatesLimitTitle: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u306E\u4E0A\u9650\u306B\u9054\u3057\u307E\u3057\u305F",
|
|
352
|
+
confirm: "\u78BA\u5B9A",
|
|
353
|
+
cancel: "\u30AD\u30E3\u30F3\u30BB\u30EB",
|
|
354
|
+
dismiss: "OK",
|
|
355
|
+
confirmReplace: "\u7F6E\u304D\u63DB\u3048\u308B",
|
|
326
356
|
current: "\u73FE\u5728\u7DE8\u96C6\u4E2D",
|
|
327
357
|
customColor: "\u30AB\u30B9\u30BF\u30E0\u30AB\u30E9\u30FC",
|
|
358
|
+
templateLabel: (name) => `\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8: ${name}`,
|
|
359
|
+
templateEdited: "\u7DE8\u96C6\u6E08\u307F",
|
|
360
|
+
templateLoading: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u8AAD\u307F\u8FBC\u307F\u4E2D...",
|
|
361
|
+
templateLoadFailed: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F\u3002",
|
|
362
|
+
templateSaveFailed: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u4FDD\u5B58\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002",
|
|
328
363
|
fieldsCount: (n) => `${n}\u4EF6\u306E\u30D5\u30A3\u30FC\u30EB\u30C9`,
|
|
329
364
|
fieldTypeInput: "\u5165\u529B",
|
|
330
365
|
fieldTypeTextarea: "\u30C6\u30AD\u30B9\u30C8\u30A8\u30EA\u30A2",
|
|
@@ -380,6 +415,7 @@ var CHROME = {
|
|
|
380
415
|
spacingTicketPadding: "\u30D5\u30A3\u30FC\u30EB\u30C9\u5185\u4F59\u767D"
|
|
381
416
|
}
|
|
382
417
|
};
|
|
418
|
+
CHROME.ja;
|
|
383
419
|
|
|
384
420
|
// src/lib/bilingual.ts
|
|
385
421
|
function bi(en = "", ja = "") {
|
|
@@ -615,10 +651,32 @@ function migrateDocument(raw) {
|
|
|
615
651
|
return { title, themeOverrides, sections };
|
|
616
652
|
}
|
|
617
653
|
|
|
654
|
+
// src/lib/template.ts
|
|
655
|
+
var TEMPLATE_FORMAT = 1;
|
|
656
|
+
function serializeTemplate(document2) {
|
|
657
|
+
const envelope = { __fpb: "template", v: TEMPLATE_FORMAT, document: document2 };
|
|
658
|
+
return JSON.stringify(envelope);
|
|
659
|
+
}
|
|
660
|
+
function parseTemplate(raw) {
|
|
661
|
+
if (!raw) return null;
|
|
662
|
+
let parsed;
|
|
663
|
+
try {
|
|
664
|
+
parsed = JSON.parse(raw);
|
|
665
|
+
} catch {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
669
|
+
const obj = parsed;
|
|
670
|
+
const candidate = obj.__fpb === "template" && obj.document ? obj.document : obj;
|
|
671
|
+
return migrateDocument(candidate);
|
|
672
|
+
}
|
|
673
|
+
|
|
618
674
|
// src/lib/features.ts
|
|
675
|
+
var DEFAULT_MAX_TEMPLATES = 5;
|
|
619
676
|
var DEFAULT_FEATURES = {
|
|
620
677
|
naming: true,
|
|
621
|
-
templates: true,
|
|
678
|
+
templates: { enabled: true, manage: true, max: DEFAULT_MAX_TEMPLATES },
|
|
679
|
+
templateClipboard: true,
|
|
622
680
|
newForm: true,
|
|
623
681
|
autosave: true,
|
|
624
682
|
jsonView: true,
|
|
@@ -629,15 +687,24 @@ var DEFAULT_FEATURES = {
|
|
|
629
687
|
contentBlocks: true,
|
|
630
688
|
fieldTypes: true,
|
|
631
689
|
sections: true,
|
|
690
|
+
sectionBackground: true,
|
|
632
691
|
dragReorder: true,
|
|
633
692
|
deviceToggle: true
|
|
634
693
|
};
|
|
694
|
+
function resolveTemplates(t2) {
|
|
695
|
+
if (t2 === false) return { enabled: false, manage: false, max: DEFAULT_MAX_TEMPLATES };
|
|
696
|
+
if (t2 === void 0 || t2 === true) return { enabled: true, manage: true, max: DEFAULT_MAX_TEMPLATES };
|
|
697
|
+
return { enabled: true, manage: t2.manage ?? true, max: t2.max ?? DEFAULT_MAX_TEMPLATES };
|
|
698
|
+
}
|
|
635
699
|
function resolveFeatures(features) {
|
|
636
700
|
return {
|
|
637
701
|
...DEFAULT_FEATURES,
|
|
638
702
|
...features,
|
|
703
|
+
templates: resolveTemplates(features?.templates),
|
|
639
704
|
contentBlocks: Array.isArray(features?.contentBlocks) ? new Set(features.contentBlocks) : features?.contentBlocks ?? DEFAULT_FEATURES.contentBlocks,
|
|
640
|
-
fieldTypes: Array.isArray(features?.fieldTypes) ? new Set(features.fieldTypes) : features?.fieldTypes ?? DEFAULT_FEATURES.fieldTypes
|
|
705
|
+
fieldTypes: Array.isArray(features?.fieldTypes) ? new Set(features.fieldTypes) : features?.fieldTypes ?? DEFAULT_FEATURES.fieldTypes,
|
|
706
|
+
// Unset → follow `sections`, so an existing `sections: false` still hides the picker.
|
|
707
|
+
sectionBackground: features?.sectionBackground ?? features?.sections ?? DEFAULT_FEATURES.sectionBackground
|
|
641
708
|
};
|
|
642
709
|
}
|
|
643
710
|
function isContentBlockEnabled(features, type) {
|
|
@@ -648,7 +715,7 @@ function isFieldTypeEnabled(features, type) {
|
|
|
648
715
|
}
|
|
649
716
|
|
|
650
717
|
// src/hooks/useTheme.ts
|
|
651
|
-
import { useState } from "react";
|
|
718
|
+
import { useMemo, useState } from "react";
|
|
652
719
|
|
|
653
720
|
// src/theme/defaultTheme.ts
|
|
654
721
|
var DEFAULT_THEME = {
|
|
@@ -667,17 +734,17 @@ var DEFAULT_THEME = {
|
|
|
667
734
|
|
|
668
735
|
// src/hooks/useTheme.ts
|
|
669
736
|
function useTheme(themeOverrideProp) {
|
|
670
|
-
const
|
|
737
|
+
const [themeOverrides, setThemeOverrides] = useState({});
|
|
738
|
+
const baseTheme = useMemo(() => ({
|
|
671
739
|
...DEFAULT_THEME,
|
|
672
740
|
...themeOverrideProp || {},
|
|
673
741
|
layout: { ...DEFAULT_THEME.layout, ...themeOverrideProp && themeOverrideProp.layout || {} }
|
|
674
|
-
};
|
|
675
|
-
const
|
|
676
|
-
const theme = {
|
|
742
|
+
}), [themeOverrideProp]);
|
|
743
|
+
const theme = useMemo(() => ({
|
|
677
744
|
...baseTheme,
|
|
678
745
|
...themeOverrides,
|
|
679
746
|
layout: { ...baseTheme.layout, ...themeOverrides.layout || {} }
|
|
680
|
-
};
|
|
747
|
+
}), [baseTheme, themeOverrides]);
|
|
681
748
|
function updateThemeColor(key, value) {
|
|
682
749
|
setThemeOverrides((prev) => ({ ...prev, [key]: value }));
|
|
683
750
|
}
|
|
@@ -876,35 +943,49 @@ import { useEffect, useRef, useState as useState3 } from "react";
|
|
|
876
943
|
var DRAFT_KEY = "form-page-builder:draft";
|
|
877
944
|
var INDEX_KEY = "form-page-builder:index";
|
|
878
945
|
var formKey = (id) => `form-page-builder:saved:${id}`;
|
|
946
|
+
var savedFormId = (key) => key.startsWith("form-page-builder:saved:") ? key.slice("form-page-builder:saved:".length) : null;
|
|
947
|
+
var CLIPBOARD_KEY = "form-page-builder:clipboard";
|
|
879
948
|
|
|
880
949
|
// src/hooks/usePersistence.ts
|
|
881
|
-
|
|
950
|
+
function snapshotOf(d) {
|
|
951
|
+
return JSON.stringify({ title: d.title, themeOverrides: d.themeOverrides, sections: d.sections });
|
|
952
|
+
}
|
|
882
953
|
function usePersistence({
|
|
883
954
|
storage,
|
|
884
955
|
autosave,
|
|
956
|
+
templateMax,
|
|
957
|
+
templateManage,
|
|
885
958
|
language,
|
|
886
959
|
chrome,
|
|
887
|
-
document,
|
|
960
|
+
document: document2,
|
|
888
961
|
initialDocument,
|
|
889
962
|
onLoadDocument,
|
|
890
963
|
onLoadThemeOverrides,
|
|
891
964
|
onTitleChange,
|
|
892
965
|
onNewForm,
|
|
966
|
+
onTemplateChange,
|
|
893
967
|
ensureActiveSection
|
|
894
968
|
}) {
|
|
895
969
|
const [currentFormId, setCurrentFormId] = useState3(null);
|
|
896
970
|
const [loadingDraft, setLoadingDraft] = useState3(true);
|
|
897
971
|
const [saveState, setSaveState] = useState3("idle");
|
|
972
|
+
const [templateState, setTemplateState] = useState3("idle");
|
|
898
973
|
const [savedForms, setSavedForms] = useState3([]);
|
|
899
974
|
const [saveAsPrompt, setSaveAsPrompt] = useState3(null);
|
|
975
|
+
const [notice, setNotice] = useState3(null);
|
|
900
976
|
const autosaveTimer = useRef(null);
|
|
901
977
|
const hasLoadedOnce = useRef(false);
|
|
978
|
+
const savedSnapshot = useRef(null);
|
|
979
|
+
function flashSaved() {
|
|
980
|
+
setTemplateState("saved");
|
|
981
|
+
setTimeout(() => setTemplateState((s) => s === "saved" ? "idle" : s), 1500);
|
|
982
|
+
}
|
|
902
983
|
async function refreshLibrary() {
|
|
903
984
|
try {
|
|
904
985
|
const raw = await storage.get(INDEX_KEY);
|
|
905
986
|
const list = raw ? JSON.parse(raw) : [];
|
|
906
987
|
setSavedForms(list.sort((a, b) => b.updatedAt - a.updatedAt));
|
|
907
|
-
} catch
|
|
988
|
+
} catch {
|
|
908
989
|
setSavedForms([]);
|
|
909
990
|
}
|
|
910
991
|
}
|
|
@@ -928,12 +1009,21 @@ function usePersistence({
|
|
|
928
1009
|
onLoadDocument(doc);
|
|
929
1010
|
onLoadThemeOverrides(doc.themeOverrides);
|
|
930
1011
|
}
|
|
931
|
-
if (parsed.currentFormId)
|
|
1012
|
+
if (parsed.currentFormId && templateManage) {
|
|
1013
|
+
setCurrentFormId(parsed.currentFormId);
|
|
1014
|
+
try {
|
|
1015
|
+
const tplRaw = await storage.get(formKey(parsed.currentFormId));
|
|
1016
|
+
const tplDoc = tplRaw ? migrateDocument(JSON.parse(tplRaw)) : null;
|
|
1017
|
+
savedSnapshot.current = tplDoc ? snapshotOf(tplDoc) : null;
|
|
1018
|
+
} catch {
|
|
1019
|
+
savedSnapshot.current = null;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
932
1022
|
} else {
|
|
933
1023
|
ensureActiveSection();
|
|
934
1024
|
}
|
|
935
1025
|
}
|
|
936
|
-
} catch
|
|
1026
|
+
} catch {
|
|
937
1027
|
ensureActiveSection();
|
|
938
1028
|
} finally {
|
|
939
1029
|
setLoadingDraft(false);
|
|
@@ -948,86 +1038,125 @@ function usePersistence({
|
|
|
948
1038
|
if (autosaveTimer.current) clearTimeout(autosaveTimer.current);
|
|
949
1039
|
autosaveTimer.current = setTimeout(async () => {
|
|
950
1040
|
try {
|
|
951
|
-
await storage.set(DRAFT_KEY, JSON.stringify({ ...
|
|
1041
|
+
await storage.set(DRAFT_KEY, JSON.stringify({ ...document2, currentFormId }));
|
|
952
1042
|
setSaveState("saved");
|
|
953
|
-
} catch
|
|
1043
|
+
} catch {
|
|
954
1044
|
setSaveState("error");
|
|
955
1045
|
}
|
|
956
1046
|
}, 600);
|
|
957
1047
|
return () => {
|
|
958
1048
|
if (autosaveTimer.current) clearTimeout(autosaveTimer.current);
|
|
959
1049
|
};
|
|
960
|
-
}, [
|
|
1050
|
+
}, [document2.title, document2.themeOverrides, document2.sections, currentFormId]);
|
|
961
1051
|
async function saveAs(name) {
|
|
962
|
-
if (
|
|
963
|
-
|
|
1052
|
+
if (!templateManage) return;
|
|
1053
|
+
if (savedForms.length >= templateMax) {
|
|
1054
|
+
setSaveAsPrompt(null);
|
|
1055
|
+
setNotice({ title: chrome.templatesLimitTitle, message: chrome.templatesLimitReached(templateMax) });
|
|
964
1056
|
return;
|
|
965
1057
|
}
|
|
966
1058
|
const id = genFormId();
|
|
967
1059
|
const now = Date.now();
|
|
1060
|
+
setTemplateState("saving");
|
|
968
1061
|
try {
|
|
969
1062
|
const newTitle = bi(name, "");
|
|
970
|
-
|
|
1063
|
+
const docToSave = { ...document2, title: newTitle };
|
|
1064
|
+
await storage.set(formKey(id), JSON.stringify({ ...docToSave, id, updatedAt: now }));
|
|
971
1065
|
const next = [...savedForms, { id, title: name, updatedAt: now }];
|
|
972
1066
|
await storage.set(INDEX_KEY, JSON.stringify(next));
|
|
973
1067
|
setSavedForms(next);
|
|
974
1068
|
setCurrentFormId(id);
|
|
975
1069
|
onTitleChange(newTitle);
|
|
976
1070
|
setSaveAsPrompt(null);
|
|
977
|
-
|
|
978
|
-
|
|
1071
|
+
savedSnapshot.current = snapshotOf(docToSave);
|
|
1072
|
+
flashSaved();
|
|
1073
|
+
onTemplateChange?.({ id, title: name, source: "new" });
|
|
1074
|
+
} catch {
|
|
1075
|
+
setTemplateState("error");
|
|
979
1076
|
}
|
|
980
1077
|
}
|
|
981
1078
|
async function saveExisting() {
|
|
1079
|
+
if (!templateManage) return;
|
|
982
1080
|
if (!currentFormId) {
|
|
983
|
-
setSaveAsPrompt({ open: true, suggestedName: t(
|
|
1081
|
+
setSaveAsPrompt({ open: true, suggestedName: t(document2.title, language) });
|
|
984
1082
|
return;
|
|
985
1083
|
}
|
|
986
1084
|
const now = Date.now();
|
|
1085
|
+
setTemplateState("saving");
|
|
987
1086
|
try {
|
|
988
|
-
await storage.set(formKey(currentFormId), JSON.stringify({ ...
|
|
989
|
-
const next = savedForms.map((f) => f.id === currentFormId ? { ...f, title: t(
|
|
1087
|
+
await storage.set(formKey(currentFormId), JSON.stringify({ ...document2, id: currentFormId, updatedAt: now }));
|
|
1088
|
+
const next = savedForms.map((f) => f.id === currentFormId ? { ...f, title: t(document2.title, "en"), updatedAt: now } : f);
|
|
990
1089
|
setSavedForms(next);
|
|
991
1090
|
await storage.set(INDEX_KEY, JSON.stringify(next));
|
|
992
|
-
|
|
993
|
-
|
|
1091
|
+
savedSnapshot.current = snapshotOf(document2);
|
|
1092
|
+
flashSaved();
|
|
1093
|
+
onTemplateChange?.({ id: currentFormId, title: t(document2.title, language), source: "saved" });
|
|
1094
|
+
} catch {
|
|
1095
|
+
setTemplateState("error");
|
|
994
1096
|
}
|
|
995
1097
|
}
|
|
996
1098
|
async function loadForm(id) {
|
|
1099
|
+
setTemplateState("loading");
|
|
997
1100
|
try {
|
|
998
1101
|
const raw = await storage.get(formKey(id));
|
|
999
|
-
if (!raw)
|
|
1102
|
+
if (!raw) {
|
|
1103
|
+
setTemplateState("error");
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1000
1106
|
const doc = migrateDocument(JSON.parse(raw));
|
|
1001
|
-
if (!doc)
|
|
1107
|
+
if (!doc) {
|
|
1108
|
+
setTemplateState("error");
|
|
1109
|
+
return false;
|
|
1110
|
+
}
|
|
1002
1111
|
onLoadDocument(doc);
|
|
1003
1112
|
onLoadThemeOverrides(doc.themeOverrides);
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1113
|
+
const boundId = templateManage ? id : null;
|
|
1114
|
+
setCurrentFormId(boundId);
|
|
1115
|
+
savedSnapshot.current = boundId ? snapshotOf(doc) : null;
|
|
1116
|
+
setTemplateState("idle");
|
|
1117
|
+
onTemplateChange?.({ id, title: t(doc.title, language), source: "applied" });
|
|
1118
|
+
return true;
|
|
1119
|
+
} catch {
|
|
1120
|
+
setTemplateState("error");
|
|
1121
|
+
return false;
|
|
1007
1122
|
}
|
|
1008
1123
|
}
|
|
1009
1124
|
async function deleteForm(id) {
|
|
1125
|
+
if (!templateManage) return;
|
|
1126
|
+
const removed = savedForms.find((f) => f.id === id);
|
|
1010
1127
|
try {
|
|
1011
1128
|
await storage.delete(formKey(id));
|
|
1012
1129
|
const next = savedForms.filter((f) => f.id !== id);
|
|
1013
1130
|
setSavedForms(next);
|
|
1014
1131
|
await storage.set(INDEX_KEY, JSON.stringify(next));
|
|
1015
|
-
if (currentFormId === id)
|
|
1016
|
-
|
|
1132
|
+
if (currentFormId === id) {
|
|
1133
|
+
setCurrentFormId(null);
|
|
1134
|
+
savedSnapshot.current = null;
|
|
1135
|
+
}
|
|
1136
|
+
onTemplateChange?.({ id: currentFormId === id ? null : id, title: removed?.title ?? "", source: "deleted" });
|
|
1137
|
+
} catch {
|
|
1138
|
+
setTemplateState("error");
|
|
1017
1139
|
}
|
|
1018
1140
|
}
|
|
1019
1141
|
function newForm() {
|
|
1020
1142
|
onNewForm();
|
|
1021
1143
|
setCurrentFormId(null);
|
|
1144
|
+
savedSnapshot.current = null;
|
|
1145
|
+
setTemplateState("idle");
|
|
1022
1146
|
}
|
|
1147
|
+
const activeTemplate = currentFormId ? savedForms.find((f) => f.id === currentFormId) ?? null : null;
|
|
1148
|
+
const isTemplateDirty = savedSnapshot.current !== null && snapshotOf(document2) !== savedSnapshot.current;
|
|
1023
1149
|
function dismissSaveAsPrompt() {
|
|
1024
1150
|
setSaveAsPrompt(null);
|
|
1025
1151
|
}
|
|
1026
1152
|
return {
|
|
1027
1153
|
loadingDraft,
|
|
1028
1154
|
saveState,
|
|
1155
|
+
templateState,
|
|
1029
1156
|
savedForms,
|
|
1030
1157
|
currentFormId,
|
|
1158
|
+
activeTemplateTitle: activeTemplate?.title ?? null,
|
|
1159
|
+
isTemplateDirty,
|
|
1031
1160
|
saveAs,
|
|
1032
1161
|
saveExisting,
|
|
1033
1162
|
loadForm,
|
|
@@ -1035,15 +1164,64 @@ function usePersistence({
|
|
|
1035
1164
|
refreshLibrary,
|
|
1036
1165
|
newForm,
|
|
1037
1166
|
saveAsPrompt,
|
|
1038
|
-
dismissSaveAsPrompt
|
|
1167
|
+
dismissSaveAsPrompt,
|
|
1168
|
+
notice,
|
|
1169
|
+
dismissNotice: () => setNotice(null)
|
|
1039
1170
|
};
|
|
1040
1171
|
}
|
|
1041
1172
|
|
|
1173
|
+
// src/hooks/useTemplateClipboard.ts
|
|
1174
|
+
import { useCallback, useEffect as useEffect2, useRef as useRef2, useState as useState4 } from "react";
|
|
1175
|
+
var sameTabListeners = /* @__PURE__ */ new Set();
|
|
1176
|
+
function notifySameTab() {
|
|
1177
|
+
sameTabListeners.forEach((fn) => fn());
|
|
1178
|
+
}
|
|
1179
|
+
function readKey(key) {
|
|
1180
|
+
try {
|
|
1181
|
+
return typeof window !== "undefined" && window.localStorage ? window.localStorage.getItem(key) : null;
|
|
1182
|
+
} catch {
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
function useTemplateClipboard(clipboardKey = CLIPBOARD_KEY) {
|
|
1187
|
+
const keyRef = useRef2(clipboardKey);
|
|
1188
|
+
keyRef.current = clipboardKey;
|
|
1189
|
+
const [hasClipboard, setHasClipboard] = useState4(false);
|
|
1190
|
+
const refresh = useCallback(() => {
|
|
1191
|
+
setHasClipboard(parseTemplate(readKey(keyRef.current)) !== null);
|
|
1192
|
+
}, []);
|
|
1193
|
+
useEffect2(() => {
|
|
1194
|
+
refresh();
|
|
1195
|
+
const onStorage = (e) => {
|
|
1196
|
+
if (e.key === null || e.key === keyRef.current) refresh();
|
|
1197
|
+
};
|
|
1198
|
+
sameTabListeners.add(refresh);
|
|
1199
|
+
window.addEventListener("storage", onStorage);
|
|
1200
|
+
return () => {
|
|
1201
|
+
sameTabListeners.delete(refresh);
|
|
1202
|
+
window.removeEventListener("storage", onStorage);
|
|
1203
|
+
};
|
|
1204
|
+
}, [refresh]);
|
|
1205
|
+
const copyTemplate = useCallback((document2) => {
|
|
1206
|
+
const str = serializeTemplate(document2);
|
|
1207
|
+
try {
|
|
1208
|
+
window.localStorage?.setItem(keyRef.current, str);
|
|
1209
|
+
} catch {
|
|
1210
|
+
}
|
|
1211
|
+
notifySameTab();
|
|
1212
|
+
refresh();
|
|
1213
|
+
}, [refresh]);
|
|
1214
|
+
const readTemplate = useCallback(() => {
|
|
1215
|
+
return parseTemplate(readKey(keyRef.current));
|
|
1216
|
+
}, []);
|
|
1217
|
+
return { hasClipboard, copyTemplate, readTemplate };
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1042
1220
|
// src/hooks/useDragReorder.ts
|
|
1043
|
-
import { useRef as
|
|
1221
|
+
import { useRef as useRef3, useState as useState5 } from "react";
|
|
1044
1222
|
function useDragReorder(reorderWithinSection) {
|
|
1045
|
-
const dragRef =
|
|
1046
|
-
const [dragOverKey, setDragOverKey] =
|
|
1223
|
+
const dragRef = useRef3({ sectionId: null, index: null });
|
|
1224
|
+
const [dragOverKey, setDragOverKey] = useState5(null);
|
|
1047
1225
|
function getDropZoneHandlers(sectionId, index) {
|
|
1048
1226
|
const dragKey = `${sectionId}:${index}`;
|
|
1049
1227
|
return {
|
|
@@ -1071,13 +1249,13 @@ function useDragReorder(reorderWithinSection) {
|
|
|
1071
1249
|
}
|
|
1072
1250
|
|
|
1073
1251
|
// src/components/Toolbar.tsx
|
|
1074
|
-
import { Loader2, AlertCircle, Pencil, Eye, FilePlus2, FolderOpen, Save, Code2 } from "lucide-react";
|
|
1252
|
+
import { Loader2, AlertCircle, Pencil, Eye, FilePlus2, FolderOpen, Save, Code2, ClipboardCopy, ClipboardPaste, Check } from "lucide-react";
|
|
1075
1253
|
|
|
1076
1254
|
// src/styles/styles.ts
|
|
1077
1255
|
var styles = {
|
|
1078
1256
|
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" },
|
|
1079
1257
|
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" },
|
|
1080
|
-
toolbarLeft: { display: "flex", alignItems: "center", gap: 10 },
|
|
1258
|
+
toolbarLeft: { display: "flex", alignItems: "center", gap: 10, minWidth: 0, flexShrink: 1 },
|
|
1081
1259
|
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" },
|
|
1082
1260
|
titleInput: { border: "none", outline: "none", fontSize: 14, fontWeight: 600, background: "transparent", color: "var(--fb-ink)", padding: "4px 6px", borderRadius: 6, minWidth: 160 },
|
|
1083
1261
|
toolbarRight: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" },
|
|
@@ -1087,6 +1265,8 @@ var styles = {
|
|
|
1087
1265
|
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 },
|
|
1088
1266
|
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 },
|
|
1089
1267
|
saveStatus: { display: "flex", alignItems: "center", gap: 5, fontSize: 12, color: "var(--fb-muted)", minWidth: 60 },
|
|
1268
|
+
templateTag: { display: "flex", alignItems: "center", gap: 6, fontSize: 12, color: "var(--fb-muted)", minWidth: 0, flexShrink: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
1269
|
+
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)" },
|
|
1090
1270
|
countBadge: { fontSize: 10.5, fontWeight: 700, background: "var(--fb-primary-soft)", color: "var(--fb-primary)", borderRadius: 9, padding: "1px 6px", marginLeft: 2 },
|
|
1091
1271
|
loadingScreen: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", color: "var(--fb-muted)" },
|
|
1092
1272
|
libraryBody: { padding: "var(--fb-space-panel)", overflowY: "auto", display: "flex", flexDirection: "column", gap: 4 },
|
|
@@ -1223,21 +1403,36 @@ function Toolbar({
|
|
|
1223
1403
|
languages,
|
|
1224
1404
|
mode,
|
|
1225
1405
|
saveState,
|
|
1406
|
+
templateState,
|
|
1407
|
+
activeTemplateTitle,
|
|
1408
|
+
templateDirty,
|
|
1226
1409
|
chrome,
|
|
1227
1410
|
features,
|
|
1228
1411
|
savedFormsCount,
|
|
1412
|
+
clipboardReady,
|
|
1413
|
+
templateCopied,
|
|
1229
1414
|
onTitleChange,
|
|
1230
1415
|
onLanguageChange,
|
|
1231
1416
|
onModeChange,
|
|
1232
1417
|
onNewForm,
|
|
1233
1418
|
onOpenLibrary,
|
|
1234
1419
|
onSaveExisting,
|
|
1235
|
-
onOpenJson
|
|
1420
|
+
onOpenJson,
|
|
1421
|
+
onCopyTemplate,
|
|
1422
|
+
onPasteTemplate
|
|
1236
1423
|
}) {
|
|
1237
1424
|
return /* @__PURE__ */ jsxs2("div", { style: styles.toolbar, children: [
|
|
1238
1425
|
/* @__PURE__ */ jsxs2("div", { style: styles.toolbarLeft, children: [
|
|
1239
1426
|
/* @__PURE__ */ jsx2("div", { style: styles.logoMark, children: "FB" }),
|
|
1240
|
-
features.naming ? /* @__PURE__ */ jsx2("input", { value: t(title, language), onChange: (e) => onTitleChange(e.target.value), style: styles.titleInput, "aria-label": "Form title" }) : /* @__PURE__ */ jsx2("span", { style: styles.titleInput, children: t(title, language) })
|
|
1427
|
+
features.naming ? /* @__PURE__ */ jsx2("input", { value: t(title, language), onChange: (e) => onTitleChange(e.target.value), style: styles.titleInput, "aria-label": "Form title" }) : /* @__PURE__ */ jsx2("span", { style: styles.titleInput, children: t(title, language) }),
|
|
1428
|
+
templateState === "loading" ? /* @__PURE__ */ jsxs2("span", { style: styles.saveStatus, children: [
|
|
1429
|
+
/* @__PURE__ */ jsx2(Loader2, { size: 12, className: "spin" }),
|
|
1430
|
+
" ",
|
|
1431
|
+
chrome.templateLoading
|
|
1432
|
+
] }) : activeTemplateTitle && /* @__PURE__ */ jsxs2("span", { className: "fb-template-tag", style: styles.templateTag, title: chrome.templateLabel(activeTemplateTitle), children: [
|
|
1433
|
+
/* @__PURE__ */ jsx2("span", { style: { overflow: "hidden", textOverflow: "ellipsis" }, children: chrome.templateLabel(activeTemplateTitle) }),
|
|
1434
|
+
templateDirty && /* @__PURE__ */ jsx2("span", { style: styles.currentBadge, children: chrome.templateEdited })
|
|
1435
|
+
] })
|
|
1241
1436
|
] }),
|
|
1242
1437
|
/* @__PURE__ */ jsxs2("div", { style: styles.toolbarRight, children: [
|
|
1243
1438
|
/* @__PURE__ */ jsxs2("span", { style: styles.saveStatus, children: [
|
|
@@ -1270,20 +1465,20 @@ function Toolbar({
|
|
|
1270
1465
|
chrome.preview
|
|
1271
1466
|
] })
|
|
1272
1467
|
] }),
|
|
1273
|
-
(features.newForm || features.templates) && /* @__PURE__ */ jsx2("div", { style: styles.toolbarDivider }),
|
|
1468
|
+
(features.newForm || features.templates.enabled) && /* @__PURE__ */ jsx2("div", { style: styles.toolbarDivider }),
|
|
1274
1469
|
features.newForm && /* @__PURE__ */ jsxs2("button", { style: styles.ghostBtn, onClick: onNewForm, title: chrome.startNewForm, children: [
|
|
1275
1470
|
/* @__PURE__ */ jsx2(FilePlus2, { size: 14 }),
|
|
1276
1471
|
" ",
|
|
1277
1472
|
chrome.newForm
|
|
1278
1473
|
] }),
|
|
1279
|
-
features.templates && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1474
|
+
features.templates.enabled && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1280
1475
|
/* @__PURE__ */ jsxs2("button", { style: styles.ghostBtn, onClick: onOpenLibrary, title: chrome.openTemplatesTitle, children: [
|
|
1281
1476
|
/* @__PURE__ */ jsx2(FolderOpen, { size: 14 }),
|
|
1282
1477
|
" ",
|
|
1283
1478
|
chrome.templates,
|
|
1284
1479
|
savedFormsCount > 0 && /* @__PURE__ */ jsx2("span", { style: styles.countBadge, children: savedFormsCount })
|
|
1285
1480
|
] }),
|
|
1286
|
-
/* @__PURE__ */ jsxs2("button", { style: styles.primaryBtn, onClick: onSaveExisting, title: chrome.saveToLibraryTitle, children: [
|
|
1481
|
+
features.templates.manage && /* @__PURE__ */ jsxs2("button", { style: styles.primaryBtn, onClick: onSaveExisting, title: chrome.saveToLibraryTitle, children: [
|
|
1287
1482
|
/* @__PURE__ */ jsx2(Save, { size: 14 }),
|
|
1288
1483
|
" ",
|
|
1289
1484
|
chrome.save
|
|
@@ -1293,13 +1488,17 @@ function Toolbar({
|
|
|
1293
1488
|
/* @__PURE__ */ jsx2(Code2, { size: 14 }),
|
|
1294
1489
|
" ",
|
|
1295
1490
|
chrome.viewJson
|
|
1491
|
+
] }),
|
|
1492
|
+
features.templateClipboard && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1493
|
+
/* @__PURE__ */ jsx2("button", { style: styles.toolbarIconBtn, onClick: onCopyTemplate, title: chrome.copyTemplateTitle, "aria-label": chrome.copyTemplate, children: templateCopied ? /* @__PURE__ */ jsx2(Check, { size: 14 }) : /* @__PURE__ */ jsx2(ClipboardCopy, { size: 14 }) }),
|
|
1494
|
+
/* @__PURE__ */ jsx2("button", { style: styles.toolbarIconBtn, onClick: onPasteTemplate, disabled: !clipboardReady, title: chrome.pasteTemplateTitle, "aria-label": chrome.pasteTemplate, children: /* @__PURE__ */ jsx2(ClipboardPaste, { size: 14 }) })
|
|
1296
1495
|
] })
|
|
1297
1496
|
] })
|
|
1298
1497
|
] });
|
|
1299
1498
|
}
|
|
1300
1499
|
|
|
1301
1500
|
// src/components/Palette.tsx
|
|
1302
|
-
import { useState as
|
|
1501
|
+
import { useState as useState6 } from "react";
|
|
1303
1502
|
import { Plus } from "lucide-react";
|
|
1304
1503
|
|
|
1305
1504
|
// src/components/Segmented.tsx
|
|
@@ -1397,7 +1596,7 @@ function Palette({
|
|
|
1397
1596
|
resetTheme,
|
|
1398
1597
|
fieldCount
|
|
1399
1598
|
}) {
|
|
1400
|
-
const [tab, setTab] =
|
|
1599
|
+
const [tab, setTab] = useState6("blocks");
|
|
1401
1600
|
const contentTypes = CONTENT_TYPES.filter((f) => isContentBlockEnabled(features, f.type));
|
|
1402
1601
|
const formTypes = FORM_TYPES.filter((f) => isFieldTypeEnabled(features, f.type));
|
|
1403
1602
|
const atFieldCap = typeof features.maxFields === "number" && fieldCount >= features.maxFields;
|
|
@@ -1806,7 +2005,7 @@ function SectionCard({
|
|
|
1806
2005
|
),
|
|
1807
2006
|
section.collapsed && /* @__PURE__ */ jsx10("span", { style: styles.miniBadge, children: chrome.fieldsCount(section.fields.length) }),
|
|
1808
2007
|
/* @__PURE__ */ jsxs8("div", { style: styles.sectionHeaderActions, onClick: (e) => e.stopPropagation(), children: [
|
|
1809
|
-
features.
|
|
2008
|
+
features.sectionBackground && !section.collapsed && /* @__PURE__ */ jsxs8("div", { style: styles.swatchRow, children: [
|
|
1810
2009
|
SECTION_BG_SWATCHES.map((c) => /* @__PURE__ */ jsx10("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")),
|
|
1811
2010
|
/* @__PURE__ */ jsx10("input", { type: "color", title: chrome.customColor, value: /^#/.test(section.background) ? section.background : "#ffffff", onChange: (e) => onUpdateBackground(e.target.value), style: styles.colorPickerInput })
|
|
1812
2011
|
] }),
|
|
@@ -2127,7 +2326,7 @@ function InspectorBody({ selected, language, chrome, features, onUpdateField, on
|
|
|
2127
2326
|
}
|
|
2128
2327
|
|
|
2129
2328
|
// src/components/PreviewPane.tsx
|
|
2130
|
-
import { useState as
|
|
2329
|
+
import { useState as useState7 } from "react";
|
|
2131
2330
|
import { PartyPopper, X as X2, CircleAlert as CircleAlert2 } from "lucide-react";
|
|
2132
2331
|
|
|
2133
2332
|
// src/lib/validate.ts
|
|
@@ -2176,10 +2375,10 @@ function collectFieldValues(fields) {
|
|
|
2176
2375
|
// src/components/PreviewPane.tsx
|
|
2177
2376
|
import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2178
2377
|
function PreviewPane({ title, sections, onFieldChange, language, strings, chrome, baseMaxWidth, features, onSubmit }) {
|
|
2179
|
-
const [device, setDevice] =
|
|
2180
|
-
const [errors, setErrors] =
|
|
2181
|
-
const [submitted, setSubmitted] =
|
|
2182
|
-
const [attemptedButtonId, setAttemptedButtonId] =
|
|
2378
|
+
const [device, setDevice] = useState7("laptop");
|
|
2379
|
+
const [errors, setErrors] = useState7({});
|
|
2380
|
+
const [submitted, setSubmitted] = useState7(null);
|
|
2381
|
+
const [attemptedButtonId, setAttemptedButtonId] = useState7(null);
|
|
2183
2382
|
const deviceOptions = buildDeviceOptions(baseMaxWidth, chrome);
|
|
2184
2383
|
const maxWidth = (deviceOptions.find((d) => d.value === device) || deviceOptions[0]).maxWidth;
|
|
2185
2384
|
const allFields = sections.flatMap((s) => s.fields);
|
|
@@ -2283,18 +2482,67 @@ function PreviewPane({ title, sections, onFieldChange, language, strings, chrome
|
|
|
2283
2482
|
}
|
|
2284
2483
|
|
|
2285
2484
|
// src/components/modals/JsonModal.tsx
|
|
2286
|
-
import { Check, ClipboardCopy, X as X3 } from "lucide-react";
|
|
2485
|
+
import { Check as Check2, ClipboardCopy as ClipboardCopy2, X as X3 } from "lucide-react";
|
|
2486
|
+
|
|
2487
|
+
// src/hooks/useModalA11y.ts
|
|
2488
|
+
import { useEffect as useEffect3, useRef as useRef4 } from "react";
|
|
2489
|
+
var FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
2490
|
+
function useModalA11y(onClose) {
|
|
2491
|
+
const panelRef = useRef4(null);
|
|
2492
|
+
const onCloseRef = useRef4(onClose);
|
|
2493
|
+
onCloseRef.current = onClose;
|
|
2494
|
+
useEffect3(() => {
|
|
2495
|
+
const opener = document.activeElement;
|
|
2496
|
+
const panel = panelRef.current;
|
|
2497
|
+
const focusables = () => Array.from(panel?.querySelectorAll(FOCUSABLE) ?? []);
|
|
2498
|
+
if (panel && !panel.contains(document.activeElement)) {
|
|
2499
|
+
(panel.querySelector("[autofocus]") ?? focusables()[0] ?? panel).focus();
|
|
2500
|
+
}
|
|
2501
|
+
const onKeyDown = (e) => {
|
|
2502
|
+
if (e.key === "Escape") {
|
|
2503
|
+
e.stopPropagation();
|
|
2504
|
+
onCloseRef.current();
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
if (e.key !== "Tab") return;
|
|
2508
|
+
const items = focusables();
|
|
2509
|
+
if (items.length === 0) {
|
|
2510
|
+
e.preventDefault();
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
const first = items[0];
|
|
2514
|
+
const last = items[items.length - 1];
|
|
2515
|
+
const active = document.activeElement;
|
|
2516
|
+
if (e.shiftKey && (active === first || !panel?.contains(active))) {
|
|
2517
|
+
e.preventDefault();
|
|
2518
|
+
last.focus();
|
|
2519
|
+
} else if (!e.shiftKey && active === last) {
|
|
2520
|
+
e.preventDefault();
|
|
2521
|
+
first.focus();
|
|
2522
|
+
}
|
|
2523
|
+
};
|
|
2524
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
2525
|
+
return () => {
|
|
2526
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
2527
|
+
opener?.focus?.();
|
|
2528
|
+
};
|
|
2529
|
+
}, []);
|
|
2530
|
+
return panelRef;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
// src/components/modals/JsonModal.tsx
|
|
2287
2534
|
import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2288
2535
|
function JsonModal({ chrome, jsonString, copied, onCopy, onClose }) {
|
|
2289
|
-
|
|
2536
|
+
const panelRef = useModalA11y(onClose);
|
|
2537
|
+
return /* @__PURE__ */ jsx15("div", { style: styles.modalOverlay, onClick: onClose, children: /* @__PURE__ */ jsxs13("div", { ref: panelRef, style: styles.modal, role: "dialog", "aria-modal": "true", "aria-label": chrome.formJson, onClick: (e) => e.stopPropagation(), children: [
|
|
2290
2538
|
/* @__PURE__ */ jsxs13("div", { style: styles.modalHeader, children: [
|
|
2291
2539
|
/* @__PURE__ */ jsx15("span", { style: { fontWeight: 600, fontSize: 14 }, children: chrome.formJson }),
|
|
2292
2540
|
/* @__PURE__ */ jsxs13("div", { style: { display: "flex", gap: 8 }, children: [
|
|
2293
|
-
/* @__PURE__ */ jsxs13("button", { style: styles.ghostBtn, onClick: onCopy, children: [
|
|
2294
|
-
copied ? /* @__PURE__ */ jsx15(
|
|
2541
|
+
/* @__PURE__ */ jsxs13("button", { type: "button", style: styles.ghostBtn, onClick: onCopy, children: [
|
|
2542
|
+
copied ? /* @__PURE__ */ jsx15(Check2, { size: 14 }) : /* @__PURE__ */ jsx15(ClipboardCopy2, { size: 14 }),
|
|
2295
2543
|
copied ? chrome.copied : chrome.copy
|
|
2296
2544
|
] }),
|
|
2297
|
-
/* @__PURE__ */ jsx15("button", { style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ jsx15(X3, { size: 16 }) })
|
|
2545
|
+
/* @__PURE__ */ jsx15("button", { type: "button", style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ jsx15(X3, { size: 16 }) })
|
|
2298
2546
|
] })
|
|
2299
2547
|
] }),
|
|
2300
2548
|
/* @__PURE__ */ jsx15("pre", { style: styles.jsonPre, children: jsonString })
|
|
@@ -2302,57 +2550,99 @@ function JsonModal({ chrome, jsonString, copied, onCopy, onClose }) {
|
|
|
2302
2550
|
}
|
|
2303
2551
|
|
|
2304
2552
|
// src/components/modals/TemplatesModal.tsx
|
|
2305
|
-
import { Trash2 as Trash24, X as X4 } from "lucide-react";
|
|
2553
|
+
import { AlertCircle as AlertCircle2, Loader2 as Loader22, Trash2 as Trash24, X as X4 } from "lucide-react";
|
|
2306
2554
|
import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2307
|
-
function TemplatesModal({ chrome, savedForms, currentFormId, onOpen, onDelete, onClose }) {
|
|
2308
|
-
|
|
2555
|
+
function TemplatesModal({ chrome, savedForms, currentFormId, manage, templateState, onOpen, onDelete, onClose }) {
|
|
2556
|
+
const busy = templateState === "loading";
|
|
2557
|
+
const panelRef = useModalA11y(onClose);
|
|
2558
|
+
return /* @__PURE__ */ jsx16("div", { style: styles.modalOverlay, onClick: onClose, children: /* @__PURE__ */ jsxs14("div", { ref: panelRef, style: styles.modal, role: "dialog", "aria-modal": "true", "aria-label": chrome.templates, onClick: (e) => e.stopPropagation(), children: [
|
|
2309
2559
|
/* @__PURE__ */ jsxs14("div", { style: styles.modalHeader, children: [
|
|
2310
2560
|
/* @__PURE__ */ jsx16("span", { style: { fontWeight: 600, fontSize: 14 }, children: chrome.templates }),
|
|
2311
|
-
/* @__PURE__ */ jsx16("button", { style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ jsx16(X4, { size: 16 }) })
|
|
2561
|
+
/* @__PURE__ */ jsx16("button", { type: "button", style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ jsx16(X4, { size: 16 }) })
|
|
2312
2562
|
] }),
|
|
2313
|
-
/* @__PURE__ */
|
|
2314
|
-
/* @__PURE__ */
|
|
2315
|
-
/* @__PURE__ */ jsxs14("div", { style:
|
|
2316
|
-
|
|
2317
|
-
|
|
2563
|
+
/* @__PURE__ */ jsxs14("div", { style: styles.libraryBody, children: [
|
|
2564
|
+
savedForms.length === 0 ? /* @__PURE__ */ jsx16("div", { style: styles.inspectorEmpty, children: manage ? chrome.noTemplates : chrome.noTemplatesReadonly }) : savedForms.map((f) => /* @__PURE__ */ jsxs14("div", { style: styles.libraryRow, children: [
|
|
2565
|
+
/* @__PURE__ */ jsxs14("div", { style: { minWidth: 0 }, children: [
|
|
2566
|
+
/* @__PURE__ */ jsxs14("div", { style: styles.libraryRowTitle, children: [
|
|
2567
|
+
f.title,
|
|
2568
|
+
f.id === currentFormId && /* @__PURE__ */ jsx16("span", { style: styles.currentBadge, children: chrome.current })
|
|
2569
|
+
] }),
|
|
2570
|
+
/* @__PURE__ */ jsxs14("div", { style: styles.libraryRowMeta, children: [
|
|
2571
|
+
chrome.updated,
|
|
2572
|
+
" ",
|
|
2573
|
+
new Date(f.updatedAt).toLocaleString()
|
|
2574
|
+
] })
|
|
2318
2575
|
] }),
|
|
2319
|
-
/* @__PURE__ */ jsxs14("div", { style:
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2576
|
+
/* @__PURE__ */ jsxs14("div", { style: { display: "flex", gap: 6, flexShrink: 0 }, children: [
|
|
2577
|
+
/* @__PURE__ */ jsxs14("button", { type: "button", style: styles.ghostBtn, disabled: busy, onClick: () => onOpen(f.id), children: [
|
|
2578
|
+
busy && /* @__PURE__ */ jsx16(Loader22, { size: 12, className: "spin" }),
|
|
2579
|
+
" ",
|
|
2580
|
+
manage ? chrome.open : chrome.applyTemplate
|
|
2581
|
+
] }),
|
|
2582
|
+
manage && /* @__PURE__ */ jsx16("button", { type: "button", style: { ...styles.iconBtn, ...styles.iconBtnDanger }, disabled: busy, title: chrome.delete, onClick: () => onDelete(f.id), children: /* @__PURE__ */ jsx16(Trash24, { size: 13 }) })
|
|
2323
2583
|
] })
|
|
2324
|
-
] }),
|
|
2325
|
-
/* @__PURE__ */ jsxs14("div", { style: { display: "flex", gap:
|
|
2326
|
-
/* @__PURE__ */ jsx16(
|
|
2327
|
-
|
|
2584
|
+
] }, f.id)),
|
|
2585
|
+
templateState === "error" && /* @__PURE__ */ jsxs14("div", { style: { ...styles.libraryRowMeta, color: "var(--fb-danger)", display: "flex", alignItems: "center", gap: 5, marginTop: 4 }, children: [
|
|
2586
|
+
/* @__PURE__ */ jsx16(AlertCircle2, { size: 12 }),
|
|
2587
|
+
" ",
|
|
2588
|
+
chrome.templateLoadFailed
|
|
2328
2589
|
] })
|
|
2329
|
-
] }
|
|
2590
|
+
] })
|
|
2330
2591
|
] }) });
|
|
2331
2592
|
}
|
|
2332
2593
|
|
|
2333
2594
|
// src/components/modals/SaveAsModal.tsx
|
|
2334
|
-
import { useState as
|
|
2335
|
-
import { Save as Save2, X as X5 } from "lucide-react";
|
|
2595
|
+
import { useState as useState8 } from "react";
|
|
2596
|
+
import { AlertCircle as AlertCircle3, Loader2 as Loader23, Save as Save2, X as X5 } from "lucide-react";
|
|
2336
2597
|
import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
2337
|
-
function SaveAsModal({ chrome, suggestedName, onSave, onClose }) {
|
|
2338
|
-
const [name, setName] =
|
|
2339
|
-
|
|
2598
|
+
function SaveAsModal({ chrome, suggestedName, templateState, onSave, onClose }) {
|
|
2599
|
+
const [name, setName] = useState8(suggestedName);
|
|
2600
|
+
const saving = templateState === "saving";
|
|
2601
|
+
const canSave = !!name.trim() && !saving;
|
|
2602
|
+
const panelRef = useModalA11y(onClose);
|
|
2603
|
+
return /* @__PURE__ */ jsx17("div", { style: styles.modalOverlay, onClick: onClose, children: /* @__PURE__ */ jsxs15("div", { ref: panelRef, style: { ...styles.modal, maxWidth: 380 }, role: "dialog", "aria-modal": "true", "aria-label": chrome.saveForm, onClick: (e) => e.stopPropagation(), children: [
|
|
2340
2604
|
/* @__PURE__ */ jsxs15("div", { style: styles.modalHeader, children: [
|
|
2341
2605
|
/* @__PURE__ */ jsx17("span", { style: { fontWeight: 600, fontSize: 14 }, children: chrome.saveForm }),
|
|
2342
|
-
/* @__PURE__ */ jsx17("button", { style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ jsx17(X5, { size: 16 }) })
|
|
2606
|
+
/* @__PURE__ */ jsx17("button", { type: "button", style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ jsx17(X5, { size: 16 }) })
|
|
2343
2607
|
] }),
|
|
2344
2608
|
/* @__PURE__ */ jsxs15("div", { style: { padding: 16 }, children: [
|
|
2345
2609
|
/* @__PURE__ */ jsx17("label", { style: styles.propLabel, children: chrome.formName }),
|
|
2346
|
-
/* @__PURE__ */ jsx17("input", { autoFocus: true, style: styles.propInput, value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => e.key === "Enter" &&
|
|
2347
|
-
/* @__PURE__ */ jsxs15("button", { style: { ...styles.primaryBtn, width: "100%", justifyContent: "center", marginTop: 14 }, disabled: !
|
|
2348
|
-
/* @__PURE__ */ jsx17(Save2, { size: 14 }),
|
|
2610
|
+
/* @__PURE__ */ jsx17("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()) }),
|
|
2611
|
+
/* @__PURE__ */ jsxs15("button", { type: "button", style: { ...styles.primaryBtn, width: "100%", justifyContent: "center", marginTop: 14 }, disabled: !canSave, onClick: () => onSave(name.trim()), children: [
|
|
2612
|
+
saving ? /* @__PURE__ */ jsx17(Loader23, { size: 14, className: "spin" }) : /* @__PURE__ */ jsx17(Save2, { size: 14 }),
|
|
2349
2613
|
" ",
|
|
2350
2614
|
chrome.saveToLibrary
|
|
2615
|
+
] }),
|
|
2616
|
+
templateState === "error" && /* @__PURE__ */ jsxs15("div", { style: { fontSize: 12, color: "var(--fb-danger)", display: "flex", alignItems: "center", gap: 5, marginTop: 10 }, children: [
|
|
2617
|
+
/* @__PURE__ */ jsx17(AlertCircle3, { size: 12 }),
|
|
2618
|
+
" ",
|
|
2619
|
+
chrome.templateSaveFailed
|
|
2351
2620
|
] })
|
|
2352
2621
|
] })
|
|
2353
2622
|
] }) });
|
|
2354
2623
|
}
|
|
2355
2624
|
|
|
2625
|
+
// src/components/modals/ConfirmModal.tsx
|
|
2626
|
+
import { X as X6 } from "lucide-react";
|
|
2627
|
+
import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2628
|
+
function ConfirmModal({ chrome, title, message, onConfirm, confirmLabel, tone = "default", onClose }) {
|
|
2629
|
+
const panelRef = useModalA11y(onClose);
|
|
2630
|
+
const confirmStyle = tone === "danger" ? { ...styles.primaryBtn, background: "var(--fb-danger)", borderColor: "var(--fb-danger)" } : styles.primaryBtn;
|
|
2631
|
+
return /* @__PURE__ */ jsx18("div", { style: styles.modalOverlay, onClick: onClose, children: /* @__PURE__ */ jsxs16("div", { ref: panelRef, style: { ...styles.modal, maxWidth: 380 }, role: "alertdialog", "aria-modal": "true", "aria-label": title, onClick: (e) => e.stopPropagation(), children: [
|
|
2632
|
+
/* @__PURE__ */ jsxs16("div", { style: styles.modalHeader, children: [
|
|
2633
|
+
/* @__PURE__ */ jsx18("span", { style: { fontWeight: 600, fontSize: 14 }, children: title }),
|
|
2634
|
+
/* @__PURE__ */ jsx18("button", { type: "button", style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ jsx18(X6, { size: 16 }) })
|
|
2635
|
+
] }),
|
|
2636
|
+
/* @__PURE__ */ jsxs16("div", { style: { padding: 16 }, children: [
|
|
2637
|
+
/* @__PURE__ */ jsx18("p", { style: { margin: 0, fontSize: 13, lineHeight: 1.5, color: "var(--fb-ink)" }, children: message }),
|
|
2638
|
+
/* @__PURE__ */ jsx18("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 16 }, children: onConfirm ? /* @__PURE__ */ jsxs16(Fragment6, { children: [
|
|
2639
|
+
/* @__PURE__ */ jsx18("button", { type: "button", autoFocus: true, style: styles.ghostBtn, onClick: onClose, children: chrome.cancel }),
|
|
2640
|
+
/* @__PURE__ */ jsx18("button", { type: "button", style: confirmStyle, onClick: onConfirm, children: confirmLabel ?? chrome.confirm })
|
|
2641
|
+
] }) : /* @__PURE__ */ jsx18("button", { type: "button", autoFocus: true, style: styles.primaryBtn, onClick: onClose, children: chrome.dismiss }) })
|
|
2642
|
+
] })
|
|
2643
|
+
] }) });
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2356
2646
|
// src/styles/globalCss.ts
|
|
2357
2647
|
var css = `
|
|
2358
2648
|
* { box-sizing: border-box; }
|
|
@@ -2360,20 +2650,14 @@ var css = `
|
|
|
2360
2650
|
input, textarea, select { font-family: inherit; }
|
|
2361
2651
|
@keyframes fb-spin { to { transform: rotate(360deg); } }
|
|
2362
2652
|
.spin { animation: fb-spin 0.8s linear infinite; }
|
|
2363
|
-
/*
|
|
2364
|
-
than growing past whatever space its host container gives it; the
|
|
2365
|
-
second declaration upgrades to the dynamic viewport unit on browsers
|
|
2366
|
-
that support it (mobile browser chrome resizing the viewport), and is
|
|
2367
|
-
simply ignored by ones that don't. */
|
|
2653
|
+
/* Cap at viewport height so the widget scrolls internally; dvh upgrade is ignored where unsupported. */
|
|
2368
2654
|
.fb-root { max-height: 100vh; max-height: 100dvh; }
|
|
2369
2655
|
|
|
2370
|
-
/*
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
buttons, tracked by .fb-work-area's data-mobile-panel attribute,
|
|
2376
|
-
and dismissible via .fb-mobile-backdrop. */
|
|
2656
|
+
/* Toolbar's active-template label is nice-to-have \u2014 drop it first when width is tight. */
|
|
2657
|
+
@media (max-width: 600px) { .fb-template-tag { display: none; } }
|
|
2658
|
+
|
|
2659
|
+
/* Under 720px: Palette/Inspector become drawers over Canvas, toggled by .fb-mobile-bar,
|
|
2660
|
+
tracked by .fb-work-area[data-mobile-panel], dismissed via .fb-mobile-backdrop. */
|
|
2377
2661
|
.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; }
|
|
2378
2662
|
.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; }
|
|
2379
2663
|
.fb-mobile-btn[aria-pressed="true"] { border-color: var(--fb-primary); background: var(--fb-primary-soft); color: var(--fb-primary); }
|
|
@@ -2399,7 +2683,7 @@ var css = `
|
|
|
2399
2683
|
`;
|
|
2400
2684
|
|
|
2401
2685
|
// src/FormBuilder.tsx
|
|
2402
|
-
import { jsx as
|
|
2686
|
+
import { jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
2403
2687
|
var FormBuilder = forwardRef(function FormBuilder2({
|
|
2404
2688
|
theme: themeOverrideProp,
|
|
2405
2689
|
language: languageOverride,
|
|
@@ -2411,31 +2695,38 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2411
2695
|
onSubmit,
|
|
2412
2696
|
initialDocument,
|
|
2413
2697
|
initialMode,
|
|
2414
|
-
onModeChange
|
|
2698
|
+
onModeChange,
|
|
2699
|
+
onTemplateChange,
|
|
2700
|
+
templateClipboardKey
|
|
2415
2701
|
}, ref) {
|
|
2416
2702
|
const features = resolveFeatures(featuresProp);
|
|
2417
2703
|
const storage = storageProp ?? localStorageAdapter;
|
|
2418
2704
|
const { theme, updateThemeColor, updateThemeLayout, resetTheme, replaceThemeOverrides, themeOverrides } = useTheme(themeOverrideProp);
|
|
2419
|
-
const [language, setLanguage] =
|
|
2705
|
+
const [language, setLanguage] = useState9(languageOverride || languages[0]?.code || "en");
|
|
2420
2706
|
const strings = { ...DEFAULT_STRINGS[language] || DEFAULT_STRINGS.en, ...stringsOverride && stringsOverride[language] || {} };
|
|
2421
2707
|
const chrome = { ...CHROME[language] || CHROME.en, ...chromeOverride && chromeOverride[language] || {} };
|
|
2422
2708
|
const doc = useFormDocument({ language, chrome });
|
|
2423
2709
|
const drag = useDragReorder(doc.reorderWithinSection);
|
|
2424
|
-
const [mode, setModeState] =
|
|
2710
|
+
const [mode, setModeState] = useState9(initialMode ?? "build");
|
|
2425
2711
|
const setMode = (next) => {
|
|
2426
2712
|
setModeState(next);
|
|
2427
2713
|
onModeChange?.(next);
|
|
2428
2714
|
};
|
|
2429
|
-
|
|
2715
|
+
useEffect4(() => {
|
|
2430
2716
|
onModeChange?.(mode);
|
|
2431
2717
|
}, []);
|
|
2432
|
-
const [showJson, setShowJson] =
|
|
2433
|
-
const [showLibrary, setShowLibrary] =
|
|
2434
|
-
const [copied, setCopied] =
|
|
2435
|
-
const [
|
|
2718
|
+
const [showJson, setShowJson] = useState9(false);
|
|
2719
|
+
const [showLibrary, setShowLibrary] = useState9(false);
|
|
2720
|
+
const [copied, setCopied] = useState9(false);
|
|
2721
|
+
const [templateCopied, setTemplateCopied] = useState9(false);
|
|
2722
|
+
const [pastePrompt, setPastePrompt] = useState9(false);
|
|
2723
|
+
const clipboard = useTemplateClipboard(templateClipboardKey);
|
|
2724
|
+
const [mobilePanel, setMobilePanel] = useState9("none");
|
|
2436
2725
|
const persistence = usePersistence({
|
|
2437
2726
|
storage,
|
|
2438
2727
|
autosave: features.autosave,
|
|
2728
|
+
templateMax: features.templates.max,
|
|
2729
|
+
templateManage: features.templates.manage,
|
|
2439
2730
|
language,
|
|
2440
2731
|
chrome,
|
|
2441
2732
|
document: { title: doc.title, themeOverrides, sections: doc.sections },
|
|
@@ -2447,16 +2738,17 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2447
2738
|
doc.resetToBlank();
|
|
2448
2739
|
resetTheme();
|
|
2449
2740
|
},
|
|
2741
|
+
onTemplateChange,
|
|
2450
2742
|
ensureActiveSection: () => doc.setActiveSectionId((prev) => prev ?? doc.sections[0]?.id ?? null)
|
|
2451
2743
|
});
|
|
2452
|
-
const jsonDoc = {
|
|
2744
|
+
const jsonDoc = useMemo2(() => ({
|
|
2453
2745
|
version: 5,
|
|
2454
2746
|
title: doc.title,
|
|
2455
2747
|
theme,
|
|
2456
2748
|
themeOverrides,
|
|
2457
2749
|
sections: doc.sections.map((s) => ({ id: s.id, title: s.title, background: s.background, collapsed: s.collapsed, fields: s.fields }))
|
|
2458
|
-
};
|
|
2459
|
-
const jsonString = JSON.stringify(jsonDoc, null, 2);
|
|
2750
|
+
}), [doc.title, doc.sections, theme, themeOverrides]);
|
|
2751
|
+
const jsonString = useMemo2(() => JSON.stringify(jsonDoc, null, 2), [jsonDoc]);
|
|
2460
2752
|
function copyJson() {
|
|
2461
2753
|
navigator.clipboard.writeText(jsonString).then(() => {
|
|
2462
2754
|
setCopied(true);
|
|
@@ -2464,15 +2756,28 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2464
2756
|
}).catch(() => {
|
|
2465
2757
|
});
|
|
2466
2758
|
}
|
|
2759
|
+
function applyDocument(migrated) {
|
|
2760
|
+
if (!migrated) return false;
|
|
2761
|
+
doc.loadDocument(migrated);
|
|
2762
|
+
replaceThemeOverrides(migrated.themeOverrides);
|
|
2763
|
+
return true;
|
|
2764
|
+
}
|
|
2765
|
+
function copyTemplate() {
|
|
2766
|
+
clipboard.copyTemplate(jsonDoc);
|
|
2767
|
+
setTemplateCopied(true);
|
|
2768
|
+
setTimeout(() => setTemplateCopied(false), 1500);
|
|
2769
|
+
}
|
|
2770
|
+
function pasteTemplate() {
|
|
2771
|
+
if (clipboard.readTemplate()) setPastePrompt(true);
|
|
2772
|
+
}
|
|
2467
2773
|
useImperativeHandle(ref, () => ({
|
|
2468
2774
|
getDocument: () => jsonDoc,
|
|
2469
2775
|
exportJson: () => jsonString,
|
|
2470
2776
|
loadDocument: (raw) => {
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
}
|
|
2777
|
+
applyDocument(migrateDocument(raw));
|
|
2778
|
+
},
|
|
2779
|
+
getTemplate: () => ({ __fpb: "template", v: TEMPLATE_FORMAT, document: jsonDoc }),
|
|
2780
|
+
loadTemplate: (input) => applyDocument(parseTemplate(typeof input === "string" ? input : JSON.stringify(input)))
|
|
2476
2781
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2477
2782
|
}), [jsonDoc, jsonString]);
|
|
2478
2783
|
const fieldCount = doc.sections.reduce((n, s) => n + s.fields.filter((f) => !getMeta(f.type).isContent).length, 0);
|
|
@@ -2499,9 +2804,9 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2499
2804
|
"--fb-space-panel": `${theme.layout.panelPadding}px`,
|
|
2500
2805
|
"--fb-space-ticket": `${theme.layout.ticketPadding}px`
|
|
2501
2806
|
};
|
|
2502
|
-
return /* @__PURE__ */
|
|
2503
|
-
/* @__PURE__ */
|
|
2504
|
-
/* @__PURE__ */
|
|
2807
|
+
return /* @__PURE__ */ jsxs17("div", { className: "fb-root", style: rootStyle, children: [
|
|
2808
|
+
/* @__PURE__ */ jsx19("style", { children: css }),
|
|
2809
|
+
/* @__PURE__ */ jsx19(
|
|
2505
2810
|
Toolbar,
|
|
2506
2811
|
{
|
|
2507
2812
|
title: doc.title,
|
|
@@ -2509,24 +2814,31 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2509
2814
|
languages,
|
|
2510
2815
|
mode,
|
|
2511
2816
|
saveState: persistence.saveState,
|
|
2817
|
+
templateState: persistence.templateState,
|
|
2818
|
+
activeTemplateTitle: persistence.activeTemplateTitle,
|
|
2819
|
+
templateDirty: persistence.isTemplateDirty,
|
|
2512
2820
|
chrome,
|
|
2513
2821
|
features,
|
|
2514
2822
|
savedFormsCount: persistence.savedForms.length,
|
|
2823
|
+
clipboardReady: clipboard.hasClipboard,
|
|
2824
|
+
templateCopied,
|
|
2515
2825
|
onTitleChange: doc.updateTitle,
|
|
2516
2826
|
onLanguageChange: setLanguage,
|
|
2517
2827
|
onModeChange: setMode,
|
|
2518
2828
|
onNewForm: persistence.newForm,
|
|
2519
2829
|
onOpenLibrary: () => setShowLibrary(true),
|
|
2520
2830
|
onSaveExisting: persistence.saveExisting,
|
|
2521
|
-
onOpenJson: () => setShowJson(true)
|
|
2831
|
+
onOpenJson: () => setShowJson(true),
|
|
2832
|
+
onCopyTemplate: copyTemplate,
|
|
2833
|
+
onPasteTemplate: pasteTemplate
|
|
2522
2834
|
}
|
|
2523
2835
|
),
|
|
2524
|
-
persistence.loadingDraft ? /* @__PURE__ */
|
|
2525
|
-
/* @__PURE__ */
|
|
2526
|
-
/* @__PURE__ */
|
|
2527
|
-
] }) : mode === "build" ? /* @__PURE__ */
|
|
2528
|
-
/* @__PURE__ */
|
|
2529
|
-
/* @__PURE__ */
|
|
2836
|
+
persistence.loadingDraft ? /* @__PURE__ */ jsxs17("div", { style: styles.loadingScreen, children: [
|
|
2837
|
+
/* @__PURE__ */ jsx19(Loader24, { size: 20, className: "spin" }),
|
|
2838
|
+
/* @__PURE__ */ jsx19("span", { style: { marginTop: 8, fontSize: 13, color: "var(--fb-muted)" }, children: chrome.loadingDraft })
|
|
2839
|
+
] }) : mode === "build" ? /* @__PURE__ */ jsxs17("div", { className: "fb-work-area", style: styles.workArea, "data-mobile-panel": mobilePanel, children: [
|
|
2840
|
+
/* @__PURE__ */ jsxs17("div", { className: "fb-mobile-bar", children: [
|
|
2841
|
+
/* @__PURE__ */ jsxs17(
|
|
2530
2842
|
"button",
|
|
2531
2843
|
{
|
|
2532
2844
|
type: "button",
|
|
@@ -2534,13 +2846,13 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2534
2846
|
"aria-pressed": mobilePanel === "palette",
|
|
2535
2847
|
onClick: () => setMobilePanel((p) => p === "palette" ? "none" : "palette"),
|
|
2536
2848
|
children: [
|
|
2537
|
-
/* @__PURE__ */
|
|
2849
|
+
/* @__PURE__ */ jsx19(Menu, { size: 14 }),
|
|
2538
2850
|
" ",
|
|
2539
2851
|
chrome.paletteTabBlocks
|
|
2540
2852
|
]
|
|
2541
2853
|
}
|
|
2542
2854
|
),
|
|
2543
|
-
/* @__PURE__ */
|
|
2855
|
+
/* @__PURE__ */ jsxs17(
|
|
2544
2856
|
"button",
|
|
2545
2857
|
{
|
|
2546
2858
|
type: "button",
|
|
@@ -2548,16 +2860,16 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2548
2860
|
"aria-pressed": mobilePanel === "inspector",
|
|
2549
2861
|
onClick: () => setMobilePanel((p) => p === "inspector" ? "none" : "inspector"),
|
|
2550
2862
|
children: [
|
|
2551
|
-
/* @__PURE__ */
|
|
2863
|
+
/* @__PURE__ */ jsx19(SlidersHorizontal, { size: 14 }),
|
|
2552
2864
|
" ",
|
|
2553
2865
|
chrome.properties
|
|
2554
2866
|
]
|
|
2555
2867
|
}
|
|
2556
2868
|
)
|
|
2557
2869
|
] }),
|
|
2558
|
-
/* @__PURE__ */
|
|
2559
|
-
mobilePanel !== "none" && /* @__PURE__ */
|
|
2560
|
-
/* @__PURE__ */
|
|
2870
|
+
/* @__PURE__ */ jsxs17("div", { className: "fb-canvas-area", style: styles.workArea, children: [
|
|
2871
|
+
mobilePanel !== "none" && /* @__PURE__ */ jsx19("div", { className: "fb-mobile-backdrop", onClick: () => setMobilePanel("none") }),
|
|
2872
|
+
/* @__PURE__ */ jsx19(
|
|
2561
2873
|
Palette,
|
|
2562
2874
|
{
|
|
2563
2875
|
activeSectionLabel,
|
|
@@ -2574,7 +2886,7 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2574
2886
|
resetTheme
|
|
2575
2887
|
}
|
|
2576
2888
|
),
|
|
2577
|
-
/* @__PURE__ */
|
|
2889
|
+
/* @__PURE__ */ jsx19(
|
|
2578
2890
|
Canvas,
|
|
2579
2891
|
{
|
|
2580
2892
|
sections: doc.sections,
|
|
@@ -2606,7 +2918,7 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2606
2918
|
getDragHandleProps: drag.getDragHandleProps
|
|
2607
2919
|
}
|
|
2608
2920
|
),
|
|
2609
|
-
/* @__PURE__ */
|
|
2921
|
+
/* @__PURE__ */ jsx19(
|
|
2610
2922
|
Inspector,
|
|
2611
2923
|
{
|
|
2612
2924
|
selected: doc.selected,
|
|
@@ -2621,7 +2933,7 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2621
2933
|
}
|
|
2622
2934
|
)
|
|
2623
2935
|
] })
|
|
2624
|
-
] }) : /* @__PURE__ */
|
|
2936
|
+
] }) : /* @__PURE__ */ jsx19(
|
|
2625
2937
|
PreviewPane,
|
|
2626
2938
|
{
|
|
2627
2939
|
title: doc.title,
|
|
@@ -2635,29 +2947,55 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2635
2947
|
onSubmit
|
|
2636
2948
|
}
|
|
2637
2949
|
),
|
|
2638
|
-
showJson && /* @__PURE__ */
|
|
2639
|
-
showLibrary && /* @__PURE__ */
|
|
2950
|
+
showJson && /* @__PURE__ */ jsx19(JsonModal, { chrome, jsonString, copied, onCopy: copyJson, onClose: () => setShowJson(false) }),
|
|
2951
|
+
showLibrary && /* @__PURE__ */ jsx19(
|
|
2640
2952
|
TemplatesModal,
|
|
2641
2953
|
{
|
|
2642
2954
|
chrome,
|
|
2643
2955
|
savedForms: persistence.savedForms,
|
|
2644
2956
|
currentFormId: persistence.currentFormId,
|
|
2957
|
+
manage: features.templates.manage,
|
|
2958
|
+
templateState: persistence.templateState,
|
|
2645
2959
|
onOpen: async (id) => {
|
|
2646
|
-
await persistence.loadForm(id);
|
|
2647
|
-
setShowLibrary(false);
|
|
2960
|
+
if (await persistence.loadForm(id)) setShowLibrary(false);
|
|
2648
2961
|
},
|
|
2649
2962
|
onDelete: persistence.deleteForm,
|
|
2650
2963
|
onClose: () => setShowLibrary(false)
|
|
2651
2964
|
}
|
|
2652
2965
|
),
|
|
2653
|
-
persistence.saveAsPrompt && /* @__PURE__ */
|
|
2966
|
+
persistence.saveAsPrompt && /* @__PURE__ */ jsx19(
|
|
2654
2967
|
SaveAsModal,
|
|
2655
2968
|
{
|
|
2656
2969
|
chrome,
|
|
2657
2970
|
suggestedName: persistence.saveAsPrompt.suggestedName,
|
|
2971
|
+
templateState: persistence.templateState,
|
|
2658
2972
|
onSave: persistence.saveAs,
|
|
2659
2973
|
onClose: persistence.dismissSaveAsPrompt
|
|
2660
2974
|
}
|
|
2975
|
+
),
|
|
2976
|
+
pastePrompt && /* @__PURE__ */ jsx19(
|
|
2977
|
+
ConfirmModal,
|
|
2978
|
+
{
|
|
2979
|
+
chrome,
|
|
2980
|
+
title: chrome.pasteTemplate,
|
|
2981
|
+
message: chrome.pasteTemplateConfirm,
|
|
2982
|
+
confirmLabel: chrome.confirmReplace,
|
|
2983
|
+
tone: "danger",
|
|
2984
|
+
onConfirm: () => {
|
|
2985
|
+
applyDocument(clipboard.readTemplate());
|
|
2986
|
+
setPastePrompt(false);
|
|
2987
|
+
},
|
|
2988
|
+
onClose: () => setPastePrompt(false)
|
|
2989
|
+
}
|
|
2990
|
+
),
|
|
2991
|
+
persistence.notice && /* @__PURE__ */ jsx19(
|
|
2992
|
+
ConfirmModal,
|
|
2993
|
+
{
|
|
2994
|
+
chrome,
|
|
2995
|
+
title: persistence.notice.title,
|
|
2996
|
+
message: persistence.notice.message,
|
|
2997
|
+
onClose: persistence.dismissNotice
|
|
2998
|
+
}
|
|
2661
2999
|
)
|
|
2662
3000
|
] });
|
|
2663
3001
|
});
|
|
@@ -2678,10 +3016,17 @@ var DARK_THEME = {
|
|
|
2678
3016
|
layout: DEFAULT_THEME.layout
|
|
2679
3017
|
};
|
|
2680
3018
|
export {
|
|
3019
|
+
CLIPBOARD_KEY,
|
|
2681
3020
|
DARK_THEME,
|
|
2682
3021
|
DEFAULT_THEME,
|
|
3022
|
+
DRAFT_KEY,
|
|
2683
3023
|
FormBuilder_default as FormBuilder,
|
|
3024
|
+
INDEX_KEY,
|
|
2684
3025
|
FormBuilder_default as default,
|
|
2685
|
-
|
|
3026
|
+
formKey,
|
|
3027
|
+
localStorageAdapter,
|
|
3028
|
+
parseTemplate,
|
|
3029
|
+
savedFormId,
|
|
3030
|
+
serializeTemplate
|
|
2686
3031
|
};
|
|
2687
3032
|
//# sourceMappingURL=index.js.map
|