form-page-builder 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -15
- package/dist/index.cjs +502 -187
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -16
- package/dist/index.d.ts +91 -16
- package/dist/index.js +497 -190
- 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 = "") {
|
|
@@ -522,12 +558,8 @@ function defaultSection(titleEn = "") {
|
|
|
522
558
|
return { id: genSectionId(), title: bi(titleEn, ""), background: "", collapsed: false, fields: [] };
|
|
523
559
|
}
|
|
524
560
|
|
|
525
|
-
// src/lib/submittable.ts
|
|
526
|
-
function sectionHasFormFields(section) {
|
|
527
|
-
return section.fields.some((f) => !getMeta(f.type).isContent);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
561
|
// src/lib/migrate.ts
|
|
562
|
+
var DOCUMENT_VERSION = 5;
|
|
531
563
|
function migrateField(field) {
|
|
532
564
|
let f = { ...field };
|
|
533
565
|
if (LEGACY_INPUT_TYPES.includes(f.type)) f = { ...f, type: "input", inputType: f.type };
|
|
@@ -558,25 +590,6 @@ function migrateField(field) {
|
|
|
558
590
|
function migrateFields(fields) {
|
|
559
591
|
return (fields || []).map(migrateField);
|
|
560
592
|
}
|
|
561
|
-
function synthesizeButtonField(label, style, scope) {
|
|
562
|
-
const resolvedLabel = typeof label === "string" && label ? bi(label) : label && typeof label === "object" && (label.en || label.ja) ? label : bi("Submit", "");
|
|
563
|
-
return {
|
|
564
|
-
id: nextId(),
|
|
565
|
-
type: "button",
|
|
566
|
-
label: resolvedLabel,
|
|
567
|
-
hideLabel: false,
|
|
568
|
-
width: "1/1",
|
|
569
|
-
verticalAlign: "top",
|
|
570
|
-
labelPosition: "top",
|
|
571
|
-
showIcon: false,
|
|
572
|
-
displayIcon: "Type",
|
|
573
|
-
action: "submit",
|
|
574
|
-
submitScope: scope,
|
|
575
|
-
buttonStyle: style || { color: "", size: "md" },
|
|
576
|
-
href: "",
|
|
577
|
-
target: "_self"
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
593
|
function migrateDocument(raw) {
|
|
581
594
|
if (!raw) return null;
|
|
582
595
|
const title = typeof raw.title === "string" ? bi(raw.title) : raw.title || bi();
|
|
@@ -593,32 +606,36 @@ function migrateDocument(raw) {
|
|
|
593
606
|
} else {
|
|
594
607
|
sections = [{ ...defaultSection(), fields: migrateFields(raw.fields || []) }];
|
|
595
608
|
}
|
|
596
|
-
const legacy = raw.version === void 0 || raw.version < 5;
|
|
597
|
-
if (legacy) {
|
|
598
|
-
resyncIdCounter(sections.flatMap((s) => s.fields));
|
|
599
|
-
const submitMode = raw.submitMode === "perSection" ? "perSection" : "combined";
|
|
600
|
-
const submitLabel = raw.submitLabel;
|
|
601
|
-
const submitStyle = raw.submitStyle || { color: "", size: "md" };
|
|
602
|
-
if (submitMode === "perSection" && raw.sections) {
|
|
603
|
-
sections = sections.map((s, i) => {
|
|
604
|
-
if (!sectionHasFormFields(s)) return s;
|
|
605
|
-
const rawSection = raw.sections[i] || {};
|
|
606
|
-
const label = rawSection.submitLabel || submitLabel;
|
|
607
|
-
const style = rawSection.submitStyle || submitStyle;
|
|
608
|
-
return { ...s, fields: [...s.fields, synthesizeButtonField(label, style, "section")] };
|
|
609
|
-
});
|
|
610
|
-
} else if (sections.some(sectionHasFormFields)) {
|
|
611
|
-
const lastIdx = sections.length - 1;
|
|
612
|
-
sections = sections.map((s, i) => i === lastIdx ? { ...s, fields: [...s.fields, synthesizeButtonField(submitLabel, submitStyle, "form")] } : s);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
609
|
return { title, themeOverrides, sections };
|
|
616
610
|
}
|
|
617
611
|
|
|
612
|
+
// src/lib/template.ts
|
|
613
|
+
var TEMPLATE_FORMAT = 1;
|
|
614
|
+
function serializeTemplate(document2) {
|
|
615
|
+
const envelope = { __fpb: "template", v: TEMPLATE_FORMAT, document: document2 };
|
|
616
|
+
return JSON.stringify(envelope);
|
|
617
|
+
}
|
|
618
|
+
function parseTemplate(raw) {
|
|
619
|
+
if (!raw) return null;
|
|
620
|
+
let parsed;
|
|
621
|
+
try {
|
|
622
|
+
parsed = JSON.parse(raw);
|
|
623
|
+
} catch {
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
627
|
+
const obj = parsed;
|
|
628
|
+
const candidate = obj.__fpb === "template" && obj.document ? obj.document : obj;
|
|
629
|
+
return migrateDocument(candidate);
|
|
630
|
+
}
|
|
631
|
+
|
|
618
632
|
// src/lib/features.ts
|
|
633
|
+
var DEFAULT_MAX_TEMPLATES = 5;
|
|
619
634
|
var DEFAULT_FEATURES = {
|
|
620
635
|
naming: true,
|
|
621
|
-
|
|
636
|
+
formTitle: false,
|
|
637
|
+
templates: { enabled: true, manage: true, max: DEFAULT_MAX_TEMPLATES },
|
|
638
|
+
templateClipboard: true,
|
|
622
639
|
newForm: true,
|
|
623
640
|
autosave: true,
|
|
624
641
|
jsonView: true,
|
|
@@ -629,15 +646,24 @@ var DEFAULT_FEATURES = {
|
|
|
629
646
|
contentBlocks: true,
|
|
630
647
|
fieldTypes: true,
|
|
631
648
|
sections: true,
|
|
649
|
+
sectionBackground: true,
|
|
632
650
|
dragReorder: true,
|
|
633
651
|
deviceToggle: true
|
|
634
652
|
};
|
|
653
|
+
function resolveTemplates(t2) {
|
|
654
|
+
if (t2 === false) return { enabled: false, manage: false, max: DEFAULT_MAX_TEMPLATES };
|
|
655
|
+
if (t2 === void 0 || t2 === true) return { enabled: true, manage: true, max: DEFAULT_MAX_TEMPLATES };
|
|
656
|
+
return { enabled: true, manage: t2.manage ?? true, max: t2.max ?? DEFAULT_MAX_TEMPLATES };
|
|
657
|
+
}
|
|
635
658
|
function resolveFeatures(features) {
|
|
636
659
|
return {
|
|
637
660
|
...DEFAULT_FEATURES,
|
|
638
661
|
...features,
|
|
662
|
+
templates: resolveTemplates(features?.templates),
|
|
639
663
|
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
|
|
664
|
+
fieldTypes: Array.isArray(features?.fieldTypes) ? new Set(features.fieldTypes) : features?.fieldTypes ?? DEFAULT_FEATURES.fieldTypes,
|
|
665
|
+
// Unset → follow `sections`, so an existing `sections: false` still hides the picker.
|
|
666
|
+
sectionBackground: features?.sectionBackground ?? features?.sections ?? DEFAULT_FEATURES.sectionBackground
|
|
641
667
|
};
|
|
642
668
|
}
|
|
643
669
|
function isContentBlockEnabled(features, type) {
|
|
@@ -648,7 +674,7 @@ function isFieldTypeEnabled(features, type) {
|
|
|
648
674
|
}
|
|
649
675
|
|
|
650
676
|
// src/hooks/useTheme.ts
|
|
651
|
-
import { useState } from "react";
|
|
677
|
+
import { useMemo, useState } from "react";
|
|
652
678
|
|
|
653
679
|
// src/theme/defaultTheme.ts
|
|
654
680
|
var DEFAULT_THEME = {
|
|
@@ -667,17 +693,17 @@ var DEFAULT_THEME = {
|
|
|
667
693
|
|
|
668
694
|
// src/hooks/useTheme.ts
|
|
669
695
|
function useTheme(themeOverrideProp) {
|
|
670
|
-
const
|
|
696
|
+
const [themeOverrides, setThemeOverrides] = useState({});
|
|
697
|
+
const baseTheme = useMemo(() => ({
|
|
671
698
|
...DEFAULT_THEME,
|
|
672
699
|
...themeOverrideProp || {},
|
|
673
700
|
layout: { ...DEFAULT_THEME.layout, ...themeOverrideProp && themeOverrideProp.layout || {} }
|
|
674
|
-
};
|
|
675
|
-
const
|
|
676
|
-
const theme = {
|
|
701
|
+
}), [themeOverrideProp]);
|
|
702
|
+
const theme = useMemo(() => ({
|
|
677
703
|
...baseTheme,
|
|
678
704
|
...themeOverrides,
|
|
679
705
|
layout: { ...baseTheme.layout, ...themeOverrides.layout || {} }
|
|
680
|
-
};
|
|
706
|
+
}), [baseTheme, themeOverrides]);
|
|
681
707
|
function updateThemeColor(key, value) {
|
|
682
708
|
setThemeOverrides((prev) => ({ ...prev, [key]: value }));
|
|
683
709
|
}
|
|
@@ -876,35 +902,49 @@ import { useEffect, useRef, useState as useState3 } from "react";
|
|
|
876
902
|
var DRAFT_KEY = "form-page-builder:draft";
|
|
877
903
|
var INDEX_KEY = "form-page-builder:index";
|
|
878
904
|
var formKey = (id) => `form-page-builder:saved:${id}`;
|
|
905
|
+
var savedFormId = (key) => key.startsWith("form-page-builder:saved:") ? key.slice("form-page-builder:saved:".length) : null;
|
|
906
|
+
var CLIPBOARD_KEY = "form-page-builder:clipboard";
|
|
879
907
|
|
|
880
908
|
// src/hooks/usePersistence.ts
|
|
881
|
-
|
|
909
|
+
function snapshotOf(d) {
|
|
910
|
+
return JSON.stringify({ title: d.title, themeOverrides: d.themeOverrides, sections: d.sections });
|
|
911
|
+
}
|
|
882
912
|
function usePersistence({
|
|
883
913
|
storage,
|
|
884
914
|
autosave,
|
|
915
|
+
templateMax,
|
|
916
|
+
templateManage,
|
|
885
917
|
language,
|
|
886
918
|
chrome,
|
|
887
|
-
document,
|
|
919
|
+
document: document2,
|
|
888
920
|
initialDocument,
|
|
889
921
|
onLoadDocument,
|
|
890
922
|
onLoadThemeOverrides,
|
|
891
923
|
onTitleChange,
|
|
892
924
|
onNewForm,
|
|
925
|
+
onTemplateChange,
|
|
893
926
|
ensureActiveSection
|
|
894
927
|
}) {
|
|
895
928
|
const [currentFormId, setCurrentFormId] = useState3(null);
|
|
896
929
|
const [loadingDraft, setLoadingDraft] = useState3(true);
|
|
897
930
|
const [saveState, setSaveState] = useState3("idle");
|
|
931
|
+
const [templateState, setTemplateState] = useState3("idle");
|
|
898
932
|
const [savedForms, setSavedForms] = useState3([]);
|
|
899
933
|
const [saveAsPrompt, setSaveAsPrompt] = useState3(null);
|
|
934
|
+
const [notice, setNotice] = useState3(null);
|
|
900
935
|
const autosaveTimer = useRef(null);
|
|
901
936
|
const hasLoadedOnce = useRef(false);
|
|
937
|
+
const savedSnapshot = useRef(null);
|
|
938
|
+
function flashSaved() {
|
|
939
|
+
setTemplateState("saved");
|
|
940
|
+
setTimeout(() => setTemplateState((s) => s === "saved" ? "idle" : s), 1500);
|
|
941
|
+
}
|
|
902
942
|
async function refreshLibrary() {
|
|
903
943
|
try {
|
|
904
944
|
const raw = await storage.get(INDEX_KEY);
|
|
905
945
|
const list = raw ? JSON.parse(raw) : [];
|
|
906
946
|
setSavedForms(list.sort((a, b) => b.updatedAt - a.updatedAt));
|
|
907
|
-
} catch
|
|
947
|
+
} catch {
|
|
908
948
|
setSavedForms([]);
|
|
909
949
|
}
|
|
910
950
|
}
|
|
@@ -928,12 +968,21 @@ function usePersistence({
|
|
|
928
968
|
onLoadDocument(doc);
|
|
929
969
|
onLoadThemeOverrides(doc.themeOverrides);
|
|
930
970
|
}
|
|
931
|
-
if (parsed.currentFormId)
|
|
971
|
+
if (parsed.currentFormId && templateManage) {
|
|
972
|
+
setCurrentFormId(parsed.currentFormId);
|
|
973
|
+
try {
|
|
974
|
+
const tplRaw = await storage.get(formKey(parsed.currentFormId));
|
|
975
|
+
const tplDoc = tplRaw ? migrateDocument(JSON.parse(tplRaw)) : null;
|
|
976
|
+
savedSnapshot.current = tplDoc ? snapshotOf(tplDoc) : null;
|
|
977
|
+
} catch {
|
|
978
|
+
savedSnapshot.current = null;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
932
981
|
} else {
|
|
933
982
|
ensureActiveSection();
|
|
934
983
|
}
|
|
935
984
|
}
|
|
936
|
-
} catch
|
|
985
|
+
} catch {
|
|
937
986
|
ensureActiveSection();
|
|
938
987
|
} finally {
|
|
939
988
|
setLoadingDraft(false);
|
|
@@ -948,86 +997,125 @@ function usePersistence({
|
|
|
948
997
|
if (autosaveTimer.current) clearTimeout(autosaveTimer.current);
|
|
949
998
|
autosaveTimer.current = setTimeout(async () => {
|
|
950
999
|
try {
|
|
951
|
-
await storage.set(DRAFT_KEY, JSON.stringify({ ...
|
|
1000
|
+
await storage.set(DRAFT_KEY, JSON.stringify({ version: DOCUMENT_VERSION, ...document2, currentFormId }));
|
|
952
1001
|
setSaveState("saved");
|
|
953
|
-
} catch
|
|
1002
|
+
} catch {
|
|
954
1003
|
setSaveState("error");
|
|
955
1004
|
}
|
|
956
1005
|
}, 600);
|
|
957
1006
|
return () => {
|
|
958
1007
|
if (autosaveTimer.current) clearTimeout(autosaveTimer.current);
|
|
959
1008
|
};
|
|
960
|
-
}, [
|
|
1009
|
+
}, [document2.title, document2.themeOverrides, document2.sections, currentFormId]);
|
|
961
1010
|
async function saveAs(name) {
|
|
962
|
-
if (
|
|
963
|
-
|
|
1011
|
+
if (!templateManage) return;
|
|
1012
|
+
if (savedForms.length >= templateMax) {
|
|
1013
|
+
setSaveAsPrompt(null);
|
|
1014
|
+
setNotice({ title: chrome.templatesLimitTitle, message: chrome.templatesLimitReached(templateMax) });
|
|
964
1015
|
return;
|
|
965
1016
|
}
|
|
966
1017
|
const id = genFormId();
|
|
967
1018
|
const now = Date.now();
|
|
1019
|
+
setTemplateState("saving");
|
|
968
1020
|
try {
|
|
969
1021
|
const newTitle = bi(name, "");
|
|
970
|
-
|
|
1022
|
+
const docToSave = { ...document2, title: newTitle };
|
|
1023
|
+
await storage.set(formKey(id), JSON.stringify({ version: DOCUMENT_VERSION, ...docToSave, id, updatedAt: now }));
|
|
971
1024
|
const next = [...savedForms, { id, title: name, updatedAt: now }];
|
|
972
1025
|
await storage.set(INDEX_KEY, JSON.stringify(next));
|
|
973
1026
|
setSavedForms(next);
|
|
974
1027
|
setCurrentFormId(id);
|
|
975
1028
|
onTitleChange(newTitle);
|
|
976
1029
|
setSaveAsPrompt(null);
|
|
977
|
-
|
|
978
|
-
|
|
1030
|
+
savedSnapshot.current = snapshotOf(docToSave);
|
|
1031
|
+
flashSaved();
|
|
1032
|
+
onTemplateChange?.({ id, title: name, source: "new" });
|
|
1033
|
+
} catch {
|
|
1034
|
+
setTemplateState("error");
|
|
979
1035
|
}
|
|
980
1036
|
}
|
|
981
1037
|
async function saveExisting() {
|
|
1038
|
+
if (!templateManage) return;
|
|
982
1039
|
if (!currentFormId) {
|
|
983
|
-
setSaveAsPrompt({ open: true, suggestedName: t(
|
|
1040
|
+
setSaveAsPrompt({ open: true, suggestedName: t(document2.title, language) });
|
|
984
1041
|
return;
|
|
985
1042
|
}
|
|
986
1043
|
const now = Date.now();
|
|
1044
|
+
setTemplateState("saving");
|
|
987
1045
|
try {
|
|
988
|
-
await storage.set(formKey(currentFormId), JSON.stringify({ ...
|
|
989
|
-
const next = savedForms.map((f) => f.id === currentFormId ? { ...f, title: t(
|
|
1046
|
+
await storage.set(formKey(currentFormId), JSON.stringify({ version: DOCUMENT_VERSION, ...document2, id: currentFormId, updatedAt: now }));
|
|
1047
|
+
const next = savedForms.map((f) => f.id === currentFormId ? { ...f, title: t(document2.title, "en"), updatedAt: now } : f);
|
|
990
1048
|
setSavedForms(next);
|
|
991
1049
|
await storage.set(INDEX_KEY, JSON.stringify(next));
|
|
992
|
-
|
|
993
|
-
|
|
1050
|
+
savedSnapshot.current = snapshotOf(document2);
|
|
1051
|
+
flashSaved();
|
|
1052
|
+
onTemplateChange?.({ id: currentFormId, title: t(document2.title, language), source: "saved" });
|
|
1053
|
+
} catch {
|
|
1054
|
+
setTemplateState("error");
|
|
994
1055
|
}
|
|
995
1056
|
}
|
|
996
1057
|
async function loadForm(id) {
|
|
1058
|
+
setTemplateState("loading");
|
|
997
1059
|
try {
|
|
998
1060
|
const raw = await storage.get(formKey(id));
|
|
999
|
-
if (!raw)
|
|
1061
|
+
if (!raw) {
|
|
1062
|
+
setTemplateState("error");
|
|
1063
|
+
return false;
|
|
1064
|
+
}
|
|
1000
1065
|
const doc = migrateDocument(JSON.parse(raw));
|
|
1001
|
-
if (!doc)
|
|
1066
|
+
if (!doc) {
|
|
1067
|
+
setTemplateState("error");
|
|
1068
|
+
return false;
|
|
1069
|
+
}
|
|
1002
1070
|
onLoadDocument(doc);
|
|
1003
1071
|
onLoadThemeOverrides(doc.themeOverrides);
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1072
|
+
const boundId = templateManage ? id : null;
|
|
1073
|
+
setCurrentFormId(boundId);
|
|
1074
|
+
savedSnapshot.current = boundId ? snapshotOf(doc) : null;
|
|
1075
|
+
setTemplateState("idle");
|
|
1076
|
+
onTemplateChange?.({ id, title: t(doc.title, language), source: "applied" });
|
|
1077
|
+
return true;
|
|
1078
|
+
} catch {
|
|
1079
|
+
setTemplateState("error");
|
|
1080
|
+
return false;
|
|
1007
1081
|
}
|
|
1008
1082
|
}
|
|
1009
1083
|
async function deleteForm(id) {
|
|
1084
|
+
if (!templateManage) return;
|
|
1085
|
+
const removed = savedForms.find((f) => f.id === id);
|
|
1010
1086
|
try {
|
|
1011
1087
|
await storage.delete(formKey(id));
|
|
1012
1088
|
const next = savedForms.filter((f) => f.id !== id);
|
|
1013
1089
|
setSavedForms(next);
|
|
1014
1090
|
await storage.set(INDEX_KEY, JSON.stringify(next));
|
|
1015
|
-
if (currentFormId === id)
|
|
1016
|
-
|
|
1091
|
+
if (currentFormId === id) {
|
|
1092
|
+
setCurrentFormId(null);
|
|
1093
|
+
savedSnapshot.current = null;
|
|
1094
|
+
}
|
|
1095
|
+
onTemplateChange?.({ id: currentFormId === id ? null : id, title: removed?.title ?? "", source: "deleted" });
|
|
1096
|
+
} catch {
|
|
1097
|
+
setTemplateState("error");
|
|
1017
1098
|
}
|
|
1018
1099
|
}
|
|
1019
1100
|
function newForm() {
|
|
1020
1101
|
onNewForm();
|
|
1021
1102
|
setCurrentFormId(null);
|
|
1103
|
+
savedSnapshot.current = null;
|
|
1104
|
+
setTemplateState("idle");
|
|
1022
1105
|
}
|
|
1106
|
+
const activeTemplate = currentFormId ? savedForms.find((f) => f.id === currentFormId) ?? null : null;
|
|
1107
|
+
const isTemplateDirty = savedSnapshot.current !== null && snapshotOf(document2) !== savedSnapshot.current;
|
|
1023
1108
|
function dismissSaveAsPrompt() {
|
|
1024
1109
|
setSaveAsPrompt(null);
|
|
1025
1110
|
}
|
|
1026
1111
|
return {
|
|
1027
1112
|
loadingDraft,
|
|
1028
1113
|
saveState,
|
|
1114
|
+
templateState,
|
|
1029
1115
|
savedForms,
|
|
1030
1116
|
currentFormId,
|
|
1117
|
+
activeTemplateTitle: activeTemplate?.title ?? null,
|
|
1118
|
+
isTemplateDirty,
|
|
1031
1119
|
saveAs,
|
|
1032
1120
|
saveExisting,
|
|
1033
1121
|
loadForm,
|
|
@@ -1035,15 +1123,64 @@ function usePersistence({
|
|
|
1035
1123
|
refreshLibrary,
|
|
1036
1124
|
newForm,
|
|
1037
1125
|
saveAsPrompt,
|
|
1038
|
-
dismissSaveAsPrompt
|
|
1126
|
+
dismissSaveAsPrompt,
|
|
1127
|
+
notice,
|
|
1128
|
+
dismissNotice: () => setNotice(null)
|
|
1039
1129
|
};
|
|
1040
1130
|
}
|
|
1041
1131
|
|
|
1132
|
+
// src/hooks/useTemplateClipboard.ts
|
|
1133
|
+
import { useCallback, useEffect as useEffect2, useRef as useRef2, useState as useState4 } from "react";
|
|
1134
|
+
var sameTabListeners = /* @__PURE__ */ new Set();
|
|
1135
|
+
function notifySameTab() {
|
|
1136
|
+
sameTabListeners.forEach((fn) => fn());
|
|
1137
|
+
}
|
|
1138
|
+
function readKey(key) {
|
|
1139
|
+
try {
|
|
1140
|
+
return typeof window !== "undefined" && window.localStorage ? window.localStorage.getItem(key) : null;
|
|
1141
|
+
} catch {
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
function useTemplateClipboard(clipboardKey = CLIPBOARD_KEY) {
|
|
1146
|
+
const keyRef = useRef2(clipboardKey);
|
|
1147
|
+
keyRef.current = clipboardKey;
|
|
1148
|
+
const [hasClipboard, setHasClipboard] = useState4(false);
|
|
1149
|
+
const refresh = useCallback(() => {
|
|
1150
|
+
setHasClipboard(parseTemplate(readKey(keyRef.current)) !== null);
|
|
1151
|
+
}, []);
|
|
1152
|
+
useEffect2(() => {
|
|
1153
|
+
refresh();
|
|
1154
|
+
const onStorage = (e) => {
|
|
1155
|
+
if (e.key === null || e.key === keyRef.current) refresh();
|
|
1156
|
+
};
|
|
1157
|
+
sameTabListeners.add(refresh);
|
|
1158
|
+
window.addEventListener("storage", onStorage);
|
|
1159
|
+
return () => {
|
|
1160
|
+
sameTabListeners.delete(refresh);
|
|
1161
|
+
window.removeEventListener("storage", onStorage);
|
|
1162
|
+
};
|
|
1163
|
+
}, [refresh]);
|
|
1164
|
+
const copyTemplate = useCallback((document2) => {
|
|
1165
|
+
const str = serializeTemplate(document2);
|
|
1166
|
+
try {
|
|
1167
|
+
window.localStorage?.setItem(keyRef.current, str);
|
|
1168
|
+
} catch {
|
|
1169
|
+
}
|
|
1170
|
+
notifySameTab();
|
|
1171
|
+
refresh();
|
|
1172
|
+
}, [refresh]);
|
|
1173
|
+
const readTemplate = useCallback(() => {
|
|
1174
|
+
return parseTemplate(readKey(keyRef.current));
|
|
1175
|
+
}, []);
|
|
1176
|
+
return { hasClipboard, copyTemplate, readTemplate };
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1042
1179
|
// src/hooks/useDragReorder.ts
|
|
1043
|
-
import { useRef as
|
|
1180
|
+
import { useRef as useRef3, useState as useState5 } from "react";
|
|
1044
1181
|
function useDragReorder(reorderWithinSection) {
|
|
1045
|
-
const dragRef =
|
|
1046
|
-
const [dragOverKey, setDragOverKey] =
|
|
1182
|
+
const dragRef = useRef3({ sectionId: null, index: null });
|
|
1183
|
+
const [dragOverKey, setDragOverKey] = useState5(null);
|
|
1047
1184
|
function getDropZoneHandlers(sectionId, index) {
|
|
1048
1185
|
const dragKey = `${sectionId}:${index}`;
|
|
1049
1186
|
return {
|
|
@@ -1071,13 +1208,13 @@ function useDragReorder(reorderWithinSection) {
|
|
|
1071
1208
|
}
|
|
1072
1209
|
|
|
1073
1210
|
// src/components/Toolbar.tsx
|
|
1074
|
-
import { Loader2, AlertCircle, Pencil, Eye, FilePlus2, FolderOpen, Save, Code2 } from "lucide-react";
|
|
1211
|
+
import { Loader2, AlertCircle, Pencil, Eye, FilePlus2, FolderOpen, Save, Code2, ClipboardCopy, ClipboardPaste, Check } from "lucide-react";
|
|
1075
1212
|
|
|
1076
1213
|
// src/styles/styles.ts
|
|
1077
1214
|
var styles = {
|
|
1078
1215
|
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
1216
|
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 },
|
|
1217
|
+
toolbarLeft: { display: "flex", alignItems: "center", gap: 10, minWidth: 0, flexShrink: 1 },
|
|
1081
1218
|
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
1219
|
titleInput: { border: "none", outline: "none", fontSize: 14, fontWeight: 600, background: "transparent", color: "var(--fb-ink)", padding: "4px 6px", borderRadius: 6, minWidth: 160 },
|
|
1083
1220
|
toolbarRight: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" },
|
|
@@ -1087,6 +1224,8 @@ var styles = {
|
|
|
1087
1224
|
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
1225
|
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
1226
|
saveStatus: { display: "flex", alignItems: "center", gap: 5, fontSize: 12, color: "var(--fb-muted)", minWidth: 60 },
|
|
1227
|
+
templateTag: { display: "flex", alignItems: "center", gap: 6, fontSize: 12, color: "var(--fb-muted)", minWidth: 0, flexShrink: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
1228
|
+
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
1229
|
countBadge: { fontSize: 10.5, fontWeight: 700, background: "var(--fb-primary-soft)", color: "var(--fb-primary)", borderRadius: 9, padding: "1px 6px", marginLeft: 2 },
|
|
1091
1230
|
loadingScreen: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", color: "var(--fb-muted)" },
|
|
1092
1231
|
libraryBody: { padding: "var(--fb-space-panel)", overflowY: "auto", display: "flex", flexDirection: "column", gap: 4 },
|
|
@@ -1223,21 +1362,36 @@ function Toolbar({
|
|
|
1223
1362
|
languages,
|
|
1224
1363
|
mode,
|
|
1225
1364
|
saveState,
|
|
1365
|
+
templateState,
|
|
1366
|
+
activeTemplateTitle,
|
|
1367
|
+
templateDirty,
|
|
1226
1368
|
chrome,
|
|
1227
1369
|
features,
|
|
1228
1370
|
savedFormsCount,
|
|
1371
|
+
clipboardReady,
|
|
1372
|
+
templateCopied,
|
|
1229
1373
|
onTitleChange,
|
|
1230
1374
|
onLanguageChange,
|
|
1231
1375
|
onModeChange,
|
|
1232
1376
|
onNewForm,
|
|
1233
1377
|
onOpenLibrary,
|
|
1234
1378
|
onSaveExisting,
|
|
1235
|
-
onOpenJson
|
|
1379
|
+
onOpenJson,
|
|
1380
|
+
onCopyTemplate,
|
|
1381
|
+
onPasteTemplate
|
|
1236
1382
|
}) {
|
|
1237
1383
|
return /* @__PURE__ */ jsxs2("div", { style: styles.toolbar, children: [
|
|
1238
1384
|
/* @__PURE__ */ jsxs2("div", { style: styles.toolbarLeft, children: [
|
|
1239
1385
|
/* @__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) })
|
|
1386
|
+
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) }),
|
|
1387
|
+
templateState === "loading" ? /* @__PURE__ */ jsxs2("span", { style: styles.saveStatus, children: [
|
|
1388
|
+
/* @__PURE__ */ jsx2(Loader2, { size: 12, className: "spin" }),
|
|
1389
|
+
" ",
|
|
1390
|
+
chrome.templateLoading
|
|
1391
|
+
] }) : activeTemplateTitle && /* @__PURE__ */ jsxs2("span", { className: "fb-template-tag", style: styles.templateTag, title: chrome.templateLabel(activeTemplateTitle), children: [
|
|
1392
|
+
/* @__PURE__ */ jsx2("span", { style: { overflow: "hidden", textOverflow: "ellipsis" }, children: chrome.templateLabel(activeTemplateTitle) }),
|
|
1393
|
+
templateDirty && /* @__PURE__ */ jsx2("span", { style: styles.currentBadge, children: chrome.templateEdited })
|
|
1394
|
+
] })
|
|
1241
1395
|
] }),
|
|
1242
1396
|
/* @__PURE__ */ jsxs2("div", { style: styles.toolbarRight, children: [
|
|
1243
1397
|
/* @__PURE__ */ jsxs2("span", { style: styles.saveStatus, children: [
|
|
@@ -1270,20 +1424,20 @@ function Toolbar({
|
|
|
1270
1424
|
chrome.preview
|
|
1271
1425
|
] })
|
|
1272
1426
|
] }),
|
|
1273
|
-
(features.newForm || features.templates) && /* @__PURE__ */ jsx2("div", { style: styles.toolbarDivider }),
|
|
1427
|
+
(features.newForm || features.templates.enabled) && /* @__PURE__ */ jsx2("div", { style: styles.toolbarDivider }),
|
|
1274
1428
|
features.newForm && /* @__PURE__ */ jsxs2("button", { style: styles.ghostBtn, onClick: onNewForm, title: chrome.startNewForm, children: [
|
|
1275
1429
|
/* @__PURE__ */ jsx2(FilePlus2, { size: 14 }),
|
|
1276
1430
|
" ",
|
|
1277
1431
|
chrome.newForm
|
|
1278
1432
|
] }),
|
|
1279
|
-
features.templates && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1433
|
+
features.templates.enabled && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1280
1434
|
/* @__PURE__ */ jsxs2("button", { style: styles.ghostBtn, onClick: onOpenLibrary, title: chrome.openTemplatesTitle, children: [
|
|
1281
1435
|
/* @__PURE__ */ jsx2(FolderOpen, { size: 14 }),
|
|
1282
1436
|
" ",
|
|
1283
1437
|
chrome.templates,
|
|
1284
1438
|
savedFormsCount > 0 && /* @__PURE__ */ jsx2("span", { style: styles.countBadge, children: savedFormsCount })
|
|
1285
1439
|
] }),
|
|
1286
|
-
/* @__PURE__ */ jsxs2("button", { style: styles.primaryBtn, onClick: onSaveExisting, title: chrome.saveToLibraryTitle, children: [
|
|
1440
|
+
features.templates.manage && /* @__PURE__ */ jsxs2("button", { style: styles.primaryBtn, onClick: onSaveExisting, title: chrome.saveToLibraryTitle, children: [
|
|
1287
1441
|
/* @__PURE__ */ jsx2(Save, { size: 14 }),
|
|
1288
1442
|
" ",
|
|
1289
1443
|
chrome.save
|
|
@@ -1293,13 +1447,17 @@ function Toolbar({
|
|
|
1293
1447
|
/* @__PURE__ */ jsx2(Code2, { size: 14 }),
|
|
1294
1448
|
" ",
|
|
1295
1449
|
chrome.viewJson
|
|
1450
|
+
] }),
|
|
1451
|
+
features.templateClipboard && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1452
|
+
/* @__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 }) }),
|
|
1453
|
+
/* @__PURE__ */ jsx2("button", { style: styles.toolbarIconBtn, onClick: onPasteTemplate, disabled: !clipboardReady, title: chrome.pasteTemplateTitle, "aria-label": chrome.pasteTemplate, children: /* @__PURE__ */ jsx2(ClipboardPaste, { size: 14 }) })
|
|
1296
1454
|
] })
|
|
1297
1455
|
] })
|
|
1298
1456
|
] });
|
|
1299
1457
|
}
|
|
1300
1458
|
|
|
1301
1459
|
// src/components/Palette.tsx
|
|
1302
|
-
import { useState as
|
|
1460
|
+
import { useState as useState6 } from "react";
|
|
1303
1461
|
import { Plus } from "lucide-react";
|
|
1304
1462
|
|
|
1305
1463
|
// src/components/Segmented.tsx
|
|
@@ -1397,7 +1555,7 @@ function Palette({
|
|
|
1397
1555
|
resetTheme,
|
|
1398
1556
|
fieldCount
|
|
1399
1557
|
}) {
|
|
1400
|
-
const [tab, setTab] =
|
|
1558
|
+
const [tab, setTab] = useState6("blocks");
|
|
1401
1559
|
const contentTypes = CONTENT_TYPES.filter((f) => isContentBlockEnabled(features, f.type));
|
|
1402
1560
|
const formTypes = FORM_TYPES.filter((f) => isFieldTypeEnabled(features, f.type));
|
|
1403
1561
|
const atFieldCap = typeof features.maxFields === "number" && fieldCount >= features.maxFields;
|
|
@@ -1710,6 +1868,8 @@ function FieldTicket({
|
|
|
1710
1868
|
{
|
|
1711
1869
|
...dropZoneHandlers,
|
|
1712
1870
|
onClick: onSelect,
|
|
1871
|
+
className: `fb-field fb-field--${field.type} fb-field--${field.id}`,
|
|
1872
|
+
"data-field-id": field.id,
|
|
1713
1873
|
style: {
|
|
1714
1874
|
...styles.ticket,
|
|
1715
1875
|
flex: `1 1 calc(${widthPct} - 10px)`,
|
|
@@ -1806,7 +1966,7 @@ function SectionCard({
|
|
|
1806
1966
|
),
|
|
1807
1967
|
section.collapsed && /* @__PURE__ */ jsx10("span", { style: styles.miniBadge, children: chrome.fieldsCount(section.fields.length) }),
|
|
1808
1968
|
/* @__PURE__ */ jsxs8("div", { style: styles.sectionHeaderActions, onClick: (e) => e.stopPropagation(), children: [
|
|
1809
|
-
features.
|
|
1969
|
+
features.sectionBackground && !section.collapsed && /* @__PURE__ */ jsxs8("div", { style: styles.swatchRow, children: [
|
|
1810
1970
|
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
1971
|
/* @__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
1972
|
] }),
|
|
@@ -2127,7 +2287,7 @@ function InspectorBody({ selected, language, chrome, features, onUpdateField, on
|
|
|
2127
2287
|
}
|
|
2128
2288
|
|
|
2129
2289
|
// src/components/PreviewPane.tsx
|
|
2130
|
-
import { useState as
|
|
2290
|
+
import { useState as useState7 } from "react";
|
|
2131
2291
|
import { PartyPopper, X as X2, CircleAlert as CircleAlert2 } from "lucide-react";
|
|
2132
2292
|
|
|
2133
2293
|
// src/lib/validate.ts
|
|
@@ -2176,10 +2336,10 @@ function collectFieldValues(fields) {
|
|
|
2176
2336
|
// src/components/PreviewPane.tsx
|
|
2177
2337
|
import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2178
2338
|
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] =
|
|
2339
|
+
const [device, setDevice] = useState7("laptop");
|
|
2340
|
+
const [errors, setErrors] = useState7({});
|
|
2341
|
+
const [submitted, setSubmitted] = useState7(null);
|
|
2342
|
+
const [attemptedButtonId, setAttemptedButtonId] = useState7(null);
|
|
2183
2343
|
const deviceOptions = buildDeviceOptions(baseMaxWidth, chrome);
|
|
2184
2344
|
const maxWidth = (deviceOptions.find((d) => d.value === device) || deviceOptions[0]).maxWidth;
|
|
2185
2345
|
const allFields = sections.flatMap((s) => s.fields);
|
|
@@ -2239,16 +2399,16 @@ function PreviewPane({ title, sections, onFieldChange, language, strings, chrome
|
|
|
2239
2399
|
return /* @__PURE__ */ jsxs12("div", { style: styles.previewWrap, children: [
|
|
2240
2400
|
/* @__PURE__ */ jsxs12("div", { style: { width: "100%", maxWidth, display: "flex", flexDirection: "column", alignItems: "center" }, children: [
|
|
2241
2401
|
features.deviceToggle && /* @__PURE__ */ jsx14("div", { style: styles.previewToolbar, children: /* @__PURE__ */ jsx14(Segmented, { options: deviceOptions, value: device, onChange: setDevice }) }),
|
|
2242
|
-
/* @__PURE__ */ jsxs12("div", { style: { ...styles.previewCard, maxWidth, width: "100%" }, children: [
|
|
2243
|
-
/* @__PURE__ */ jsx14("h2", { style: styles.previewTitle, children: t(title, language) }),
|
|
2402
|
+
/* @__PURE__ */ jsxs12("div", { className: "fb-form", style: { ...styles.previewCard, maxWidth, width: "100%" }, children: [
|
|
2403
|
+
features.formTitle && /* @__PURE__ */ jsx14("h2", { style: styles.previewTitle, children: t(title, language) }),
|
|
2244
2404
|
allFields.length === 0 && /* @__PURE__ */ jsx14("p", { style: { color: "var(--fb-muted)", fontSize: 14 }, children: strings.addFieldsHint }),
|
|
2245
2405
|
sections.map((section) => {
|
|
2246
|
-
return /* @__PURE__ */ jsxs12("div", { style: { background: section.background || "transparent", padding: section.background ? 16 : 0, borderRadius: section.background ? 10 : 0, marginBottom: "var(--fb-space-section)" }, children: [
|
|
2406
|
+
return /* @__PURE__ */ jsxs12("div", { className: "fb-section", "data-section-id": section.id, style: { background: section.background || "transparent", padding: section.background ? 16 : 0, borderRadius: section.background ? 10 : 0, marginBottom: "var(--fb-space-section)" }, children: [
|
|
2247
2407
|
t(section.title, language) && /* @__PURE__ */ jsx14("h3", { style: styles.sectionRuntimeTitle, children: t(section.title, language) }),
|
|
2248
2408
|
/* @__PURE__ */ jsx14("div", { style: styles.previewGrid, children: section.fields.map((field) => {
|
|
2249
2409
|
const width = effectiveWidth(field.width, device);
|
|
2250
2410
|
const flexBasis = `1 1 calc(${WIDTH_PERCENT[width]} - 14px)`;
|
|
2251
|
-
return /* @__PURE__ */ jsxs12("div", { style: { flex: flexBasis, minWidth: 0, alignSelf: ALIGN_MAP[field.verticalAlign || "top"] }, children: [
|
|
2411
|
+
return /* @__PURE__ */ jsxs12("div", { className: `fb-field fb-field--${field.type} fb-field--${field.id}`, "data-field-id": field.id, style: { flex: flexBasis, minWidth: 0, alignSelf: ALIGN_MAP[field.verticalAlign || "top"] }, children: [
|
|
2252
2412
|
/* @__PURE__ */ jsx14(FieldBlock, { field, lang: language, onFieldChange, strings, chrome, error: errors[field.id], onButtonAction: handleButtonAction }),
|
|
2253
2413
|
field.type === "button" && field.id === attemptedButtonId && Object.keys(errors).length > 0 && /* @__PURE__ */ jsxs12("p", { style: styles.formErrorNote, children: [
|
|
2254
2414
|
/* @__PURE__ */ jsx14(CircleAlert2, { size: 13 }),
|
|
@@ -2283,18 +2443,67 @@ function PreviewPane({ title, sections, onFieldChange, language, strings, chrome
|
|
|
2283
2443
|
}
|
|
2284
2444
|
|
|
2285
2445
|
// src/components/modals/JsonModal.tsx
|
|
2286
|
-
import { Check, ClipboardCopy, X as X3 } from "lucide-react";
|
|
2446
|
+
import { Check as Check2, ClipboardCopy as ClipboardCopy2, X as X3 } from "lucide-react";
|
|
2447
|
+
|
|
2448
|
+
// src/hooks/useModalA11y.ts
|
|
2449
|
+
import { useEffect as useEffect3, useRef as useRef4 } from "react";
|
|
2450
|
+
var FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
2451
|
+
function useModalA11y(onClose) {
|
|
2452
|
+
const panelRef = useRef4(null);
|
|
2453
|
+
const onCloseRef = useRef4(onClose);
|
|
2454
|
+
onCloseRef.current = onClose;
|
|
2455
|
+
useEffect3(() => {
|
|
2456
|
+
const opener = document.activeElement;
|
|
2457
|
+
const panel = panelRef.current;
|
|
2458
|
+
const focusables = () => Array.from(panel?.querySelectorAll(FOCUSABLE) ?? []);
|
|
2459
|
+
if (panel && !panel.contains(document.activeElement)) {
|
|
2460
|
+
(panel.querySelector("[autofocus]") ?? focusables()[0] ?? panel).focus();
|
|
2461
|
+
}
|
|
2462
|
+
const onKeyDown = (e) => {
|
|
2463
|
+
if (e.key === "Escape") {
|
|
2464
|
+
e.stopPropagation();
|
|
2465
|
+
onCloseRef.current();
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
if (e.key !== "Tab") return;
|
|
2469
|
+
const items = focusables();
|
|
2470
|
+
if (items.length === 0) {
|
|
2471
|
+
e.preventDefault();
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
const first = items[0];
|
|
2475
|
+
const last = items[items.length - 1];
|
|
2476
|
+
const active = document.activeElement;
|
|
2477
|
+
if (e.shiftKey && (active === first || !panel?.contains(active))) {
|
|
2478
|
+
e.preventDefault();
|
|
2479
|
+
last.focus();
|
|
2480
|
+
} else if (!e.shiftKey && active === last) {
|
|
2481
|
+
e.preventDefault();
|
|
2482
|
+
first.focus();
|
|
2483
|
+
}
|
|
2484
|
+
};
|
|
2485
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
2486
|
+
return () => {
|
|
2487
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
2488
|
+
opener?.focus?.();
|
|
2489
|
+
};
|
|
2490
|
+
}, []);
|
|
2491
|
+
return panelRef;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// src/components/modals/JsonModal.tsx
|
|
2287
2495
|
import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2288
2496
|
function JsonModal({ chrome, jsonString, copied, onCopy, onClose }) {
|
|
2289
|
-
|
|
2497
|
+
const panelRef = useModalA11y(onClose);
|
|
2498
|
+
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
2499
|
/* @__PURE__ */ jsxs13("div", { style: styles.modalHeader, children: [
|
|
2291
2500
|
/* @__PURE__ */ jsx15("span", { style: { fontWeight: 600, fontSize: 14 }, children: chrome.formJson }),
|
|
2292
2501
|
/* @__PURE__ */ jsxs13("div", { style: { display: "flex", gap: 8 }, children: [
|
|
2293
|
-
/* @__PURE__ */ jsxs13("button", { style: styles.ghostBtn, onClick: onCopy, children: [
|
|
2294
|
-
copied ? /* @__PURE__ */ jsx15(
|
|
2502
|
+
/* @__PURE__ */ jsxs13("button", { type: "button", style: styles.ghostBtn, onClick: onCopy, children: [
|
|
2503
|
+
copied ? /* @__PURE__ */ jsx15(Check2, { size: 14 }) : /* @__PURE__ */ jsx15(ClipboardCopy2, { size: 14 }),
|
|
2295
2504
|
copied ? chrome.copied : chrome.copy
|
|
2296
2505
|
] }),
|
|
2297
|
-
/* @__PURE__ */ jsx15("button", { style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ jsx15(X3, { size: 16 }) })
|
|
2506
|
+
/* @__PURE__ */ jsx15("button", { type: "button", style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ jsx15(X3, { size: 16 }) })
|
|
2298
2507
|
] })
|
|
2299
2508
|
] }),
|
|
2300
2509
|
/* @__PURE__ */ jsx15("pre", { style: styles.jsonPre, children: jsonString })
|
|
@@ -2302,57 +2511,99 @@ function JsonModal({ chrome, jsonString, copied, onCopy, onClose }) {
|
|
|
2302
2511
|
}
|
|
2303
2512
|
|
|
2304
2513
|
// src/components/modals/TemplatesModal.tsx
|
|
2305
|
-
import { Trash2 as Trash24, X as X4 } from "lucide-react";
|
|
2514
|
+
import { AlertCircle as AlertCircle2, Loader2 as Loader22, Trash2 as Trash24, X as X4 } from "lucide-react";
|
|
2306
2515
|
import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2307
|
-
function TemplatesModal({ chrome, savedForms, currentFormId, onOpen, onDelete, onClose }) {
|
|
2308
|
-
|
|
2516
|
+
function TemplatesModal({ chrome, savedForms, currentFormId, manage, templateState, onOpen, onDelete, onClose }) {
|
|
2517
|
+
const busy = templateState === "loading";
|
|
2518
|
+
const panelRef = useModalA11y(onClose);
|
|
2519
|
+
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
2520
|
/* @__PURE__ */ jsxs14("div", { style: styles.modalHeader, children: [
|
|
2310
2521
|
/* @__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 }) })
|
|
2522
|
+
/* @__PURE__ */ jsx16("button", { type: "button", style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ jsx16(X4, { size: 16 }) })
|
|
2312
2523
|
] }),
|
|
2313
|
-
/* @__PURE__ */
|
|
2314
|
-
/* @__PURE__ */
|
|
2315
|
-
/* @__PURE__ */ jsxs14("div", { style:
|
|
2316
|
-
|
|
2317
|
-
|
|
2524
|
+
/* @__PURE__ */ jsxs14("div", { style: styles.libraryBody, children: [
|
|
2525
|
+
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: [
|
|
2526
|
+
/* @__PURE__ */ jsxs14("div", { style: { minWidth: 0 }, children: [
|
|
2527
|
+
/* @__PURE__ */ jsxs14("div", { style: styles.libraryRowTitle, children: [
|
|
2528
|
+
f.title,
|
|
2529
|
+
f.id === currentFormId && /* @__PURE__ */ jsx16("span", { style: styles.currentBadge, children: chrome.current })
|
|
2530
|
+
] }),
|
|
2531
|
+
/* @__PURE__ */ jsxs14("div", { style: styles.libraryRowMeta, children: [
|
|
2532
|
+
chrome.updated,
|
|
2533
|
+
" ",
|
|
2534
|
+
new Date(f.updatedAt).toLocaleString()
|
|
2535
|
+
] })
|
|
2318
2536
|
] }),
|
|
2319
|
-
/* @__PURE__ */ jsxs14("div", { style:
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2537
|
+
/* @__PURE__ */ jsxs14("div", { style: { display: "flex", gap: 6, flexShrink: 0 }, children: [
|
|
2538
|
+
/* @__PURE__ */ jsxs14("button", { type: "button", style: styles.ghostBtn, disabled: busy, onClick: () => onOpen(f.id), children: [
|
|
2539
|
+
busy && /* @__PURE__ */ jsx16(Loader22, { size: 12, className: "spin" }),
|
|
2540
|
+
" ",
|
|
2541
|
+
manage ? chrome.open : chrome.applyTemplate
|
|
2542
|
+
] }),
|
|
2543
|
+
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
2544
|
] })
|
|
2324
|
-
] }),
|
|
2325
|
-
/* @__PURE__ */ jsxs14("div", { style: { display: "flex", gap:
|
|
2326
|
-
/* @__PURE__ */ jsx16(
|
|
2327
|
-
|
|
2545
|
+
] }, f.id)),
|
|
2546
|
+
templateState === "error" && /* @__PURE__ */ jsxs14("div", { style: { ...styles.libraryRowMeta, color: "var(--fb-danger)", display: "flex", alignItems: "center", gap: 5, marginTop: 4 }, children: [
|
|
2547
|
+
/* @__PURE__ */ jsx16(AlertCircle2, { size: 12 }),
|
|
2548
|
+
" ",
|
|
2549
|
+
chrome.templateLoadFailed
|
|
2328
2550
|
] })
|
|
2329
|
-
] }
|
|
2551
|
+
] })
|
|
2330
2552
|
] }) });
|
|
2331
2553
|
}
|
|
2332
2554
|
|
|
2333
2555
|
// src/components/modals/SaveAsModal.tsx
|
|
2334
|
-
import { useState as
|
|
2335
|
-
import { Save as Save2, X as X5 } from "lucide-react";
|
|
2556
|
+
import { useState as useState8 } from "react";
|
|
2557
|
+
import { AlertCircle as AlertCircle3, Loader2 as Loader23, Save as Save2, X as X5 } from "lucide-react";
|
|
2336
2558
|
import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
2337
|
-
function SaveAsModal({ chrome, suggestedName, onSave, onClose }) {
|
|
2338
|
-
const [name, setName] =
|
|
2339
|
-
|
|
2559
|
+
function SaveAsModal({ chrome, suggestedName, templateState, onSave, onClose }) {
|
|
2560
|
+
const [name, setName] = useState8(suggestedName);
|
|
2561
|
+
const saving = templateState === "saving";
|
|
2562
|
+
const canSave = !!name.trim() && !saving;
|
|
2563
|
+
const panelRef = useModalA11y(onClose);
|
|
2564
|
+
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
2565
|
/* @__PURE__ */ jsxs15("div", { style: styles.modalHeader, children: [
|
|
2341
2566
|
/* @__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 }) })
|
|
2567
|
+
/* @__PURE__ */ jsx17("button", { type: "button", style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ jsx17(X5, { size: 16 }) })
|
|
2343
2568
|
] }),
|
|
2344
2569
|
/* @__PURE__ */ jsxs15("div", { style: { padding: 16 }, children: [
|
|
2345
2570
|
/* @__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 }),
|
|
2571
|
+
/* @__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()) }),
|
|
2572
|
+
/* @__PURE__ */ jsxs15("button", { type: "button", style: { ...styles.primaryBtn, width: "100%", justifyContent: "center", marginTop: 14 }, disabled: !canSave, onClick: () => onSave(name.trim()), children: [
|
|
2573
|
+
saving ? /* @__PURE__ */ jsx17(Loader23, { size: 14, className: "spin" }) : /* @__PURE__ */ jsx17(Save2, { size: 14 }),
|
|
2349
2574
|
" ",
|
|
2350
2575
|
chrome.saveToLibrary
|
|
2576
|
+
] }),
|
|
2577
|
+
templateState === "error" && /* @__PURE__ */ jsxs15("div", { style: { fontSize: 12, color: "var(--fb-danger)", display: "flex", alignItems: "center", gap: 5, marginTop: 10 }, children: [
|
|
2578
|
+
/* @__PURE__ */ jsx17(AlertCircle3, { size: 12 }),
|
|
2579
|
+
" ",
|
|
2580
|
+
chrome.templateSaveFailed
|
|
2351
2581
|
] })
|
|
2352
2582
|
] })
|
|
2353
2583
|
] }) });
|
|
2354
2584
|
}
|
|
2355
2585
|
|
|
2586
|
+
// src/components/modals/ConfirmModal.tsx
|
|
2587
|
+
import { X as X6 } from "lucide-react";
|
|
2588
|
+
import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2589
|
+
function ConfirmModal({ chrome, title, message, onConfirm, confirmLabel, tone = "default", onClose }) {
|
|
2590
|
+
const panelRef = useModalA11y(onClose);
|
|
2591
|
+
const confirmStyle = tone === "danger" ? { ...styles.primaryBtn, background: "var(--fb-danger)", borderColor: "var(--fb-danger)" } : styles.primaryBtn;
|
|
2592
|
+
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: [
|
|
2593
|
+
/* @__PURE__ */ jsxs16("div", { style: styles.modalHeader, children: [
|
|
2594
|
+
/* @__PURE__ */ jsx18("span", { style: { fontWeight: 600, fontSize: 14 }, children: title }),
|
|
2595
|
+
/* @__PURE__ */ jsx18("button", { type: "button", style: styles.iconBtn, onClick: onClose, children: /* @__PURE__ */ jsx18(X6, { size: 16 }) })
|
|
2596
|
+
] }),
|
|
2597
|
+
/* @__PURE__ */ jsxs16("div", { style: { padding: 16 }, children: [
|
|
2598
|
+
/* @__PURE__ */ jsx18("p", { style: { margin: 0, fontSize: 13, lineHeight: 1.5, color: "var(--fb-ink)" }, children: message }),
|
|
2599
|
+
/* @__PURE__ */ jsx18("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 16 }, children: onConfirm ? /* @__PURE__ */ jsxs16(Fragment6, { children: [
|
|
2600
|
+
/* @__PURE__ */ jsx18("button", { type: "button", autoFocus: true, style: styles.ghostBtn, onClick: onClose, children: chrome.cancel }),
|
|
2601
|
+
/* @__PURE__ */ jsx18("button", { type: "button", style: confirmStyle, onClick: onConfirm, children: confirmLabel ?? chrome.confirm })
|
|
2602
|
+
] }) : /* @__PURE__ */ jsx18("button", { type: "button", autoFocus: true, style: styles.primaryBtn, onClick: onClose, children: chrome.dismiss }) })
|
|
2603
|
+
] })
|
|
2604
|
+
] }) });
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2356
2607
|
// src/styles/globalCss.ts
|
|
2357
2608
|
var css = `
|
|
2358
2609
|
* { box-sizing: border-box; }
|
|
@@ -2360,20 +2611,14 @@ var css = `
|
|
|
2360
2611
|
input, textarea, select { font-family: inherit; }
|
|
2361
2612
|
@keyframes fb-spin { to { transform: rotate(360deg); } }
|
|
2362
2613
|
.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. */
|
|
2614
|
+
/* Cap at viewport height so the widget scrolls internally; dvh upgrade is ignored where unsupported. */
|
|
2368
2615
|
.fb-root { max-height: 100vh; max-height: 100dvh; }
|
|
2369
2616
|
|
|
2370
|
-
/*
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
buttons, tracked by .fb-work-area's data-mobile-panel attribute,
|
|
2376
|
-
and dismissible via .fb-mobile-backdrop. */
|
|
2617
|
+
/* Toolbar's active-template label is nice-to-have \u2014 drop it first when width is tight. */
|
|
2618
|
+
@media (max-width: 600px) { .fb-template-tag { display: none; } }
|
|
2619
|
+
|
|
2620
|
+
/* Under 720px: Palette/Inspector become drawers over Canvas, toggled by .fb-mobile-bar,
|
|
2621
|
+
tracked by .fb-work-area[data-mobile-panel], dismissed via .fb-mobile-backdrop. */
|
|
2377
2622
|
.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
2623
|
.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
2624
|
.fb-mobile-btn[aria-pressed="true"] { border-color: var(--fb-primary); background: var(--fb-primary-soft); color: var(--fb-primary); }
|
|
@@ -2399,7 +2644,7 @@ var css = `
|
|
|
2399
2644
|
`;
|
|
2400
2645
|
|
|
2401
2646
|
// src/FormBuilder.tsx
|
|
2402
|
-
import { jsx as
|
|
2647
|
+
import { jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
2403
2648
|
var FormBuilder = forwardRef(function FormBuilder2({
|
|
2404
2649
|
theme: themeOverrideProp,
|
|
2405
2650
|
language: languageOverride,
|
|
@@ -2411,31 +2656,38 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2411
2656
|
onSubmit,
|
|
2412
2657
|
initialDocument,
|
|
2413
2658
|
initialMode,
|
|
2414
|
-
onModeChange
|
|
2659
|
+
onModeChange,
|
|
2660
|
+
onTemplateChange,
|
|
2661
|
+
templateClipboardKey
|
|
2415
2662
|
}, ref) {
|
|
2416
2663
|
const features = resolveFeatures(featuresProp);
|
|
2417
2664
|
const storage = storageProp ?? localStorageAdapter;
|
|
2418
2665
|
const { theme, updateThemeColor, updateThemeLayout, resetTheme, replaceThemeOverrides, themeOverrides } = useTheme(themeOverrideProp);
|
|
2419
|
-
const [language, setLanguage] =
|
|
2666
|
+
const [language, setLanguage] = useState9(languageOverride || languages[0]?.code || "en");
|
|
2420
2667
|
const strings = { ...DEFAULT_STRINGS[language] || DEFAULT_STRINGS.en, ...stringsOverride && stringsOverride[language] || {} };
|
|
2421
2668
|
const chrome = { ...CHROME[language] || CHROME.en, ...chromeOverride && chromeOverride[language] || {} };
|
|
2422
2669
|
const doc = useFormDocument({ language, chrome });
|
|
2423
2670
|
const drag = useDragReorder(doc.reorderWithinSection);
|
|
2424
|
-
const [mode, setModeState] =
|
|
2671
|
+
const [mode, setModeState] = useState9(initialMode ?? "build");
|
|
2425
2672
|
const setMode = (next) => {
|
|
2426
2673
|
setModeState(next);
|
|
2427
2674
|
onModeChange?.(next);
|
|
2428
2675
|
};
|
|
2429
|
-
|
|
2676
|
+
useEffect4(() => {
|
|
2430
2677
|
onModeChange?.(mode);
|
|
2431
2678
|
}, []);
|
|
2432
|
-
const [showJson, setShowJson] =
|
|
2433
|
-
const [showLibrary, setShowLibrary] =
|
|
2434
|
-
const [copied, setCopied] =
|
|
2435
|
-
const [
|
|
2679
|
+
const [showJson, setShowJson] = useState9(false);
|
|
2680
|
+
const [showLibrary, setShowLibrary] = useState9(false);
|
|
2681
|
+
const [copied, setCopied] = useState9(false);
|
|
2682
|
+
const [templateCopied, setTemplateCopied] = useState9(false);
|
|
2683
|
+
const [pastePrompt, setPastePrompt] = useState9(false);
|
|
2684
|
+
const clipboard = useTemplateClipboard(templateClipboardKey);
|
|
2685
|
+
const [mobilePanel, setMobilePanel] = useState9("none");
|
|
2436
2686
|
const persistence = usePersistence({
|
|
2437
2687
|
storage,
|
|
2438
2688
|
autosave: features.autosave,
|
|
2689
|
+
templateMax: features.templates.max,
|
|
2690
|
+
templateManage: features.templates.manage,
|
|
2439
2691
|
language,
|
|
2440
2692
|
chrome,
|
|
2441
2693
|
document: { title: doc.title, themeOverrides, sections: doc.sections },
|
|
@@ -2447,16 +2699,17 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2447
2699
|
doc.resetToBlank();
|
|
2448
2700
|
resetTheme();
|
|
2449
2701
|
},
|
|
2702
|
+
onTemplateChange,
|
|
2450
2703
|
ensureActiveSection: () => doc.setActiveSectionId((prev) => prev ?? doc.sections[0]?.id ?? null)
|
|
2451
2704
|
});
|
|
2452
|
-
const jsonDoc = {
|
|
2453
|
-
version:
|
|
2705
|
+
const jsonDoc = useMemo2(() => ({
|
|
2706
|
+
version: DOCUMENT_VERSION,
|
|
2454
2707
|
title: doc.title,
|
|
2455
2708
|
theme,
|
|
2456
2709
|
themeOverrides,
|
|
2457
2710
|
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);
|
|
2711
|
+
}), [doc.title, doc.sections, theme, themeOverrides]);
|
|
2712
|
+
const jsonString = useMemo2(() => JSON.stringify(jsonDoc, null, 2), [jsonDoc]);
|
|
2460
2713
|
function copyJson() {
|
|
2461
2714
|
navigator.clipboard.writeText(jsonString).then(() => {
|
|
2462
2715
|
setCopied(true);
|
|
@@ -2464,15 +2717,28 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2464
2717
|
}).catch(() => {
|
|
2465
2718
|
});
|
|
2466
2719
|
}
|
|
2720
|
+
function applyDocument(migrated) {
|
|
2721
|
+
if (!migrated) return false;
|
|
2722
|
+
doc.loadDocument(migrated);
|
|
2723
|
+
replaceThemeOverrides(migrated.themeOverrides);
|
|
2724
|
+
return true;
|
|
2725
|
+
}
|
|
2726
|
+
function copyTemplate() {
|
|
2727
|
+
clipboard.copyTemplate(jsonDoc);
|
|
2728
|
+
setTemplateCopied(true);
|
|
2729
|
+
setTimeout(() => setTemplateCopied(false), 1500);
|
|
2730
|
+
}
|
|
2731
|
+
function pasteTemplate() {
|
|
2732
|
+
if (clipboard.readTemplate()) setPastePrompt(true);
|
|
2733
|
+
}
|
|
2467
2734
|
useImperativeHandle(ref, () => ({
|
|
2468
2735
|
getDocument: () => jsonDoc,
|
|
2469
2736
|
exportJson: () => jsonString,
|
|
2470
2737
|
loadDocument: (raw) => {
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
}
|
|
2738
|
+
applyDocument(migrateDocument(raw));
|
|
2739
|
+
},
|
|
2740
|
+
getTemplate: () => ({ __fpb: "template", v: TEMPLATE_FORMAT, document: jsonDoc }),
|
|
2741
|
+
loadTemplate: (input) => applyDocument(parseTemplate(typeof input === "string" ? input : JSON.stringify(input)))
|
|
2476
2742
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2477
2743
|
}), [jsonDoc, jsonString]);
|
|
2478
2744
|
const fieldCount = doc.sections.reduce((n, s) => n + s.fields.filter((f) => !getMeta(f.type).isContent).length, 0);
|
|
@@ -2499,9 +2765,9 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2499
2765
|
"--fb-space-panel": `${theme.layout.panelPadding}px`,
|
|
2500
2766
|
"--fb-space-ticket": `${theme.layout.ticketPadding}px`
|
|
2501
2767
|
};
|
|
2502
|
-
return /* @__PURE__ */
|
|
2503
|
-
/* @__PURE__ */
|
|
2504
|
-
/* @__PURE__ */
|
|
2768
|
+
return /* @__PURE__ */ jsxs17("div", { className: "fb-root", style: rootStyle, children: [
|
|
2769
|
+
/* @__PURE__ */ jsx19("style", { children: css }),
|
|
2770
|
+
/* @__PURE__ */ jsx19(
|
|
2505
2771
|
Toolbar,
|
|
2506
2772
|
{
|
|
2507
2773
|
title: doc.title,
|
|
@@ -2509,24 +2775,31 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2509
2775
|
languages,
|
|
2510
2776
|
mode,
|
|
2511
2777
|
saveState: persistence.saveState,
|
|
2778
|
+
templateState: persistence.templateState,
|
|
2779
|
+
activeTemplateTitle: persistence.activeTemplateTitle,
|
|
2780
|
+
templateDirty: persistence.isTemplateDirty,
|
|
2512
2781
|
chrome,
|
|
2513
2782
|
features,
|
|
2514
2783
|
savedFormsCount: persistence.savedForms.length,
|
|
2784
|
+
clipboardReady: clipboard.hasClipboard,
|
|
2785
|
+
templateCopied,
|
|
2515
2786
|
onTitleChange: doc.updateTitle,
|
|
2516
2787
|
onLanguageChange: setLanguage,
|
|
2517
2788
|
onModeChange: setMode,
|
|
2518
2789
|
onNewForm: persistence.newForm,
|
|
2519
2790
|
onOpenLibrary: () => setShowLibrary(true),
|
|
2520
2791
|
onSaveExisting: persistence.saveExisting,
|
|
2521
|
-
onOpenJson: () => setShowJson(true)
|
|
2792
|
+
onOpenJson: () => setShowJson(true),
|
|
2793
|
+
onCopyTemplate: copyTemplate,
|
|
2794
|
+
onPasteTemplate: pasteTemplate
|
|
2522
2795
|
}
|
|
2523
2796
|
),
|
|
2524
|
-
persistence.loadingDraft ? /* @__PURE__ */
|
|
2525
|
-
/* @__PURE__ */
|
|
2526
|
-
/* @__PURE__ */
|
|
2527
|
-
] }) : mode === "build" ? /* @__PURE__ */
|
|
2528
|
-
/* @__PURE__ */
|
|
2529
|
-
/* @__PURE__ */
|
|
2797
|
+
persistence.loadingDraft ? /* @__PURE__ */ jsxs17("div", { style: styles.loadingScreen, children: [
|
|
2798
|
+
/* @__PURE__ */ jsx19(Loader24, { size: 20, className: "spin" }),
|
|
2799
|
+
/* @__PURE__ */ jsx19("span", { style: { marginTop: 8, fontSize: 13, color: "var(--fb-muted)" }, children: chrome.loadingDraft })
|
|
2800
|
+
] }) : mode === "build" ? /* @__PURE__ */ jsxs17("div", { className: "fb-work-area", style: styles.workArea, "data-mobile-panel": mobilePanel, children: [
|
|
2801
|
+
/* @__PURE__ */ jsxs17("div", { className: "fb-mobile-bar", children: [
|
|
2802
|
+
/* @__PURE__ */ jsxs17(
|
|
2530
2803
|
"button",
|
|
2531
2804
|
{
|
|
2532
2805
|
type: "button",
|
|
@@ -2534,13 +2807,13 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2534
2807
|
"aria-pressed": mobilePanel === "palette",
|
|
2535
2808
|
onClick: () => setMobilePanel((p) => p === "palette" ? "none" : "palette"),
|
|
2536
2809
|
children: [
|
|
2537
|
-
/* @__PURE__ */
|
|
2810
|
+
/* @__PURE__ */ jsx19(Menu, { size: 14 }),
|
|
2538
2811
|
" ",
|
|
2539
2812
|
chrome.paletteTabBlocks
|
|
2540
2813
|
]
|
|
2541
2814
|
}
|
|
2542
2815
|
),
|
|
2543
|
-
/* @__PURE__ */
|
|
2816
|
+
/* @__PURE__ */ jsxs17(
|
|
2544
2817
|
"button",
|
|
2545
2818
|
{
|
|
2546
2819
|
type: "button",
|
|
@@ -2548,16 +2821,16 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2548
2821
|
"aria-pressed": mobilePanel === "inspector",
|
|
2549
2822
|
onClick: () => setMobilePanel((p) => p === "inspector" ? "none" : "inspector"),
|
|
2550
2823
|
children: [
|
|
2551
|
-
/* @__PURE__ */
|
|
2824
|
+
/* @__PURE__ */ jsx19(SlidersHorizontal, { size: 14 }),
|
|
2552
2825
|
" ",
|
|
2553
2826
|
chrome.properties
|
|
2554
2827
|
]
|
|
2555
2828
|
}
|
|
2556
2829
|
)
|
|
2557
2830
|
] }),
|
|
2558
|
-
/* @__PURE__ */
|
|
2559
|
-
mobilePanel !== "none" && /* @__PURE__ */
|
|
2560
|
-
/* @__PURE__ */
|
|
2831
|
+
/* @__PURE__ */ jsxs17("div", { className: "fb-canvas-area", style: styles.workArea, children: [
|
|
2832
|
+
mobilePanel !== "none" && /* @__PURE__ */ jsx19("div", { className: "fb-mobile-backdrop", onClick: () => setMobilePanel("none") }),
|
|
2833
|
+
/* @__PURE__ */ jsx19(
|
|
2561
2834
|
Palette,
|
|
2562
2835
|
{
|
|
2563
2836
|
activeSectionLabel,
|
|
@@ -2574,7 +2847,7 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2574
2847
|
resetTheme
|
|
2575
2848
|
}
|
|
2576
2849
|
),
|
|
2577
|
-
/* @__PURE__ */
|
|
2850
|
+
/* @__PURE__ */ jsx19(
|
|
2578
2851
|
Canvas,
|
|
2579
2852
|
{
|
|
2580
2853
|
sections: doc.sections,
|
|
@@ -2606,7 +2879,7 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2606
2879
|
getDragHandleProps: drag.getDragHandleProps
|
|
2607
2880
|
}
|
|
2608
2881
|
),
|
|
2609
|
-
/* @__PURE__ */
|
|
2882
|
+
/* @__PURE__ */ jsx19(
|
|
2610
2883
|
Inspector,
|
|
2611
2884
|
{
|
|
2612
2885
|
selected: doc.selected,
|
|
@@ -2621,7 +2894,7 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2621
2894
|
}
|
|
2622
2895
|
)
|
|
2623
2896
|
] })
|
|
2624
|
-
] }) : /* @__PURE__ */
|
|
2897
|
+
] }) : /* @__PURE__ */ jsx19(
|
|
2625
2898
|
PreviewPane,
|
|
2626
2899
|
{
|
|
2627
2900
|
title: doc.title,
|
|
@@ -2635,29 +2908,55 @@ var FormBuilder = forwardRef(function FormBuilder2({
|
|
|
2635
2908
|
onSubmit
|
|
2636
2909
|
}
|
|
2637
2910
|
),
|
|
2638
|
-
showJson && /* @__PURE__ */
|
|
2639
|
-
showLibrary && /* @__PURE__ */
|
|
2911
|
+
showJson && /* @__PURE__ */ jsx19(JsonModal, { chrome, jsonString, copied, onCopy: copyJson, onClose: () => setShowJson(false) }),
|
|
2912
|
+
showLibrary && /* @__PURE__ */ jsx19(
|
|
2640
2913
|
TemplatesModal,
|
|
2641
2914
|
{
|
|
2642
2915
|
chrome,
|
|
2643
2916
|
savedForms: persistence.savedForms,
|
|
2644
2917
|
currentFormId: persistence.currentFormId,
|
|
2918
|
+
manage: features.templates.manage,
|
|
2919
|
+
templateState: persistence.templateState,
|
|
2645
2920
|
onOpen: async (id) => {
|
|
2646
|
-
await persistence.loadForm(id);
|
|
2647
|
-
setShowLibrary(false);
|
|
2921
|
+
if (await persistence.loadForm(id)) setShowLibrary(false);
|
|
2648
2922
|
},
|
|
2649
2923
|
onDelete: persistence.deleteForm,
|
|
2650
2924
|
onClose: () => setShowLibrary(false)
|
|
2651
2925
|
}
|
|
2652
2926
|
),
|
|
2653
|
-
persistence.saveAsPrompt && /* @__PURE__ */
|
|
2927
|
+
persistence.saveAsPrompt && /* @__PURE__ */ jsx19(
|
|
2654
2928
|
SaveAsModal,
|
|
2655
2929
|
{
|
|
2656
2930
|
chrome,
|
|
2657
2931
|
suggestedName: persistence.saveAsPrompt.suggestedName,
|
|
2932
|
+
templateState: persistence.templateState,
|
|
2658
2933
|
onSave: persistence.saveAs,
|
|
2659
2934
|
onClose: persistence.dismissSaveAsPrompt
|
|
2660
2935
|
}
|
|
2936
|
+
),
|
|
2937
|
+
pastePrompt && /* @__PURE__ */ jsx19(
|
|
2938
|
+
ConfirmModal,
|
|
2939
|
+
{
|
|
2940
|
+
chrome,
|
|
2941
|
+
title: chrome.pasteTemplate,
|
|
2942
|
+
message: chrome.pasteTemplateConfirm,
|
|
2943
|
+
confirmLabel: chrome.confirmReplace,
|
|
2944
|
+
tone: "danger",
|
|
2945
|
+
onConfirm: () => {
|
|
2946
|
+
applyDocument(clipboard.readTemplate());
|
|
2947
|
+
setPastePrompt(false);
|
|
2948
|
+
},
|
|
2949
|
+
onClose: () => setPastePrompt(false)
|
|
2950
|
+
}
|
|
2951
|
+
),
|
|
2952
|
+
persistence.notice && /* @__PURE__ */ jsx19(
|
|
2953
|
+
ConfirmModal,
|
|
2954
|
+
{
|
|
2955
|
+
chrome,
|
|
2956
|
+
title: persistence.notice.title,
|
|
2957
|
+
message: persistence.notice.message,
|
|
2958
|
+
onClose: persistence.dismissNotice
|
|
2959
|
+
}
|
|
2661
2960
|
)
|
|
2662
2961
|
] });
|
|
2663
2962
|
});
|
|
@@ -2678,10 +2977,18 @@ var DARK_THEME = {
|
|
|
2678
2977
|
layout: DEFAULT_THEME.layout
|
|
2679
2978
|
};
|
|
2680
2979
|
export {
|
|
2980
|
+
CLIPBOARD_KEY,
|
|
2681
2981
|
DARK_THEME,
|
|
2682
2982
|
DEFAULT_THEME,
|
|
2983
|
+
DOCUMENT_VERSION,
|
|
2984
|
+
DRAFT_KEY,
|
|
2683
2985
|
FormBuilder_default as FormBuilder,
|
|
2986
|
+
INDEX_KEY,
|
|
2684
2987
|
FormBuilder_default as default,
|
|
2685
|
-
|
|
2988
|
+
formKey,
|
|
2989
|
+
localStorageAdapter,
|
|
2990
|
+
parseTemplate,
|
|
2991
|
+
savedFormId,
|
|
2992
|
+
serializeTemplate
|
|
2686
2993
|
};
|
|
2687
2994
|
//# sourceMappingURL=index.js.map
|