form-page-builder 2.0.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/dist/index.js CHANGED
@@ -1,8 +1,69 @@
1
1
  "use client";
2
2
 
3
3
  // src/FormBuilder.tsx
4
- import { forwardRef, useImperativeHandle, useState as useState8 } from "react";
5
- import { Loader2 as Loader22, Menu, SlidersHorizontal } from "lucide-react";
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
+
7
+ // src/constants/fieldTypes.ts
8
+ import {
9
+ Type,
10
+ AlignLeft,
11
+ ChevronDown,
12
+ Circle,
13
+ ListChecks,
14
+ CheckSquare,
15
+ ToggleLeft,
16
+ FileText,
17
+ Image as ImageIcon,
18
+ MousePointerClick
19
+ } from "lucide-react";
20
+ var FIELD_TYPES = [
21
+ { type: "paragraph", icon: FileText, isContent: true, defaultIcon: "FileText" },
22
+ { type: "image", icon: ImageIcon, isContent: true, isImage: true, defaultIcon: "Type" },
23
+ { type: "button", icon: MousePointerClick, isContent: true, defaultIcon: "Type" },
24
+ { type: "input", icon: Type, placeholder: true, hasSubtype: true, defaultIcon: "Type" },
25
+ { type: "textarea", icon: AlignLeft, placeholder: true, defaultIcon: "AlignLeft" },
26
+ { type: "select", icon: ChevronDown, options: true, defaultIcon: "ChevronDown" },
27
+ { type: "radio", icon: Circle, options: true, defaultIcon: "Circle" },
28
+ { type: "checkboxGroup", icon: ListChecks, options: true, multiValue: true, defaultIcon: "ListChecks" },
29
+ { type: "checkbox", icon: CheckSquare, boolean: true, defaultIcon: "CheckSquare" },
30
+ { type: "toggle", icon: ToggleLeft, boolean: true, defaultIcon: "ToggleLeft" }
31
+ ];
32
+ var TYPE_MAP = Object.fromEntries(FIELD_TYPES.map((f) => [f.type, f]));
33
+ var CONTENT_TYPES = FIELD_TYPES.filter((f) => f.isContent);
34
+ var FORM_TYPES = FIELD_TYPES.filter((f) => !f.isContent);
35
+ function getMeta(type) {
36
+ return TYPE_MAP[type] || TYPE_MAP.input;
37
+ }
38
+ var LEGACY_INPUT_TYPES = ["text", "email", "phone", "number", "password", "date"];
39
+ var INPUT_SUBTYPES = ["text", "email", "phone", "number", "password", "date", "time"];
40
+ var IMAGE_SHAPES = ["square", "circle", "banner"];
41
+ var FIELD_TYPE_CHROME_KEY = {
42
+ paragraph: "fieldTypeParagraph",
43
+ image: "fieldTypeImage",
44
+ button: "fieldTypeButton",
45
+ input: "fieldTypeInput",
46
+ textarea: "fieldTypeTextarea",
47
+ select: "fieldTypeSelect",
48
+ radio: "fieldTypeRadio",
49
+ checkboxGroup: "fieldTypeCheckboxGroup",
50
+ checkbox: "fieldTypeCheckbox",
51
+ toggle: "fieldTypeToggle"
52
+ };
53
+ var INPUT_SUBTYPE_CHROME_KEY = {
54
+ text: "subtypeText",
55
+ email: "subtypeEmail",
56
+ phone: "subtypePhone",
57
+ number: "subtypeNumber",
58
+ password: "subtypePassword",
59
+ date: "subtypeDate",
60
+ time: "subtypeTime"
61
+ };
62
+ var IMAGE_SHAPE_CHROME_KEY = {
63
+ square: "shapeSquare",
64
+ circle: "shapeCircle",
65
+ banner: "shapeBanner"
66
+ };
6
67
 
7
68
  // src/i18n/languages.ts
8
69
  var DEFAULT_LANGUAGES = [
@@ -43,6 +104,7 @@ var DEFAULT_STRINGS = {
43
104
  tooLarge: (n) => `${n}\u4EE5\u4E0B\u306E\u5024\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
44
105
  }
45
106
  };
107
+ DEFAULT_STRINGS.ja;
46
108
 
47
109
  // src/i18n/chrome.ts
48
110
  var CHROME = {
@@ -118,16 +180,33 @@ var CHROME = {
118
180
  formJson: "Form JSON",
119
181
  copy: "Copy",
120
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.",
121
188
  noTemplates: 'No templates saved yet. Click "Save" in the toolbar to store this form here.',
189
+ noTemplatesReadonly: "No templates available yet.",
122
190
  updated: "Updated",
123
191
  open: "Open",
192
+ applyTemplate: "Apply",
124
193
  saveForm: "Save as template",
125
194
  formName: "Template name",
126
195
  saveToLibrary: "Save template",
127
196
  loadingDraft: "Loading your draft...",
128
- templatesLimitReached: "You can save up to 5 templates. Delete one to save another.",
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",
129
203
  current: "current",
130
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.",
131
210
  fieldsCount: (n) => `${n} field${n === 1 ? "" : "s"}`,
132
211
  fieldTypeInput: "Input",
133
212
  fieldTypeTextarea: "Textarea",
@@ -254,16 +333,33 @@ var CHROME = {
254
333
  formJson: "\u30D5\u30A9\u30FC\u30E0JSON",
255
334
  copy: "\u30B3\u30D4\u30FC",
256
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",
257
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",
258
343
  updated: "\u66F4\u65B0\u65E5\u6642",
259
344
  open: "\u958B\u304F",
345
+ applyTemplate: "\u9069\u7528",
260
346
  saveForm: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3068\u3057\u3066\u4FDD\u5B58",
261
347
  formName: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u540D",
262
348
  saveToLibrary: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u4FDD\u5B58",
263
349
  loadingDraft: "\u4E0B\u66F8\u304D\u3092\u8AAD\u307F\u8FBC\u3093\u3067\u3044\u307E\u3059...",
264
- templatesLimitReached: "\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u306F5\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",
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",
265
356
  current: "\u73FE\u5728\u7DE8\u96C6\u4E2D",
266
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",
267
363
  fieldsCount: (n) => `${n}\u4EF6\u306E\u30D5\u30A3\u30FC\u30EB\u30C9`,
268
364
  fieldTypeInput: "\u5165\u529B",
269
365
  fieldTypeTextarea: "\u30C6\u30AD\u30B9\u30C8\u30A8\u30EA\u30A2",
@@ -319,6 +415,7 @@ var CHROME = {
319
415
  spacingTicketPadding: "\u30D5\u30A3\u30FC\u30EB\u30C9\u5185\u4F59\u767D"
320
416
  }
321
417
  };
418
+ CHROME.ja;
322
419
 
323
420
  // src/lib/bilingual.ts
324
421
  function bi(en = "", ja = "") {
@@ -391,67 +488,6 @@ var localStorageAdapter = {
391
488
  }
392
489
  };
393
490
 
394
- // src/constants/fieldTypes.ts
395
- import {
396
- Type,
397
- AlignLeft,
398
- ChevronDown,
399
- Circle,
400
- ListChecks,
401
- CheckSquare,
402
- ToggleLeft,
403
- FileText,
404
- Image as ImageIcon,
405
- MousePointerClick
406
- } from "lucide-react";
407
- var FIELD_TYPES = [
408
- { type: "paragraph", icon: FileText, isContent: true, defaultIcon: "FileText" },
409
- { type: "image", icon: ImageIcon, isContent: true, isImage: true, defaultIcon: "Type" },
410
- { type: "button", icon: MousePointerClick, isContent: true, defaultIcon: "Type" },
411
- { type: "input", icon: Type, placeholder: true, hasSubtype: true, defaultIcon: "Type" },
412
- { type: "textarea", icon: AlignLeft, placeholder: true, defaultIcon: "AlignLeft" },
413
- { type: "select", icon: ChevronDown, options: true, defaultIcon: "ChevronDown" },
414
- { type: "radio", icon: Circle, options: true, defaultIcon: "Circle" },
415
- { type: "checkboxGroup", icon: ListChecks, options: true, multiValue: true, defaultIcon: "ListChecks" },
416
- { type: "checkbox", icon: CheckSquare, boolean: true, defaultIcon: "CheckSquare" },
417
- { type: "toggle", icon: ToggleLeft, boolean: true, defaultIcon: "ToggleLeft" }
418
- ];
419
- var TYPE_MAP = Object.fromEntries(FIELD_TYPES.map((f) => [f.type, f]));
420
- var CONTENT_TYPES = FIELD_TYPES.filter((f) => f.isContent);
421
- var FORM_TYPES = FIELD_TYPES.filter((f) => !f.isContent);
422
- function getMeta(type) {
423
- return TYPE_MAP[type] || TYPE_MAP.input;
424
- }
425
- var LEGACY_INPUT_TYPES = ["text", "email", "phone", "number", "password", "date"];
426
- var INPUT_SUBTYPES = ["text", "email", "phone", "number", "password", "date", "time"];
427
- var IMAGE_SHAPES = ["square", "circle", "banner"];
428
- var FIELD_TYPE_CHROME_KEY = {
429
- paragraph: "fieldTypeParagraph",
430
- image: "fieldTypeImage",
431
- button: "fieldTypeButton",
432
- input: "fieldTypeInput",
433
- textarea: "fieldTypeTextarea",
434
- select: "fieldTypeSelect",
435
- radio: "fieldTypeRadio",
436
- checkboxGroup: "fieldTypeCheckboxGroup",
437
- checkbox: "fieldTypeCheckbox",
438
- toggle: "fieldTypeToggle"
439
- };
440
- var INPUT_SUBTYPE_CHROME_KEY = {
441
- text: "subtypeText",
442
- email: "subtypeEmail",
443
- phone: "subtypePhone",
444
- number: "subtypeNumber",
445
- password: "subtypePassword",
446
- date: "subtypeDate",
447
- time: "subtypeTime"
448
- };
449
- var IMAGE_SHAPE_CHROME_KEY = {
450
- square: "shapeSquare",
451
- circle: "shapeCircle",
452
- banner: "shapeBanner"
453
- };
454
-
455
491
  // src/lib/id.ts
456
492
  var genFormId = () => `form_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
457
493
  var genSectionId = () => `section_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
@@ -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,14 +687,24 @@ var DEFAULT_FEATURES = {
629
687
  contentBlocks: true,
630
688
  fieldTypes: true,
631
689
  sections: true,
632
- dragReorder: true
690
+ sectionBackground: true,
691
+ dragReorder: true,
692
+ deviceToggle: true
633
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
+ }
634
699
  function resolveFeatures(features) {
635
700
  return {
636
701
  ...DEFAULT_FEATURES,
637
702
  ...features,
703
+ templates: resolveTemplates(features?.templates),
638
704
  contentBlocks: Array.isArray(features?.contentBlocks) ? new Set(features.contentBlocks) : features?.contentBlocks ?? DEFAULT_FEATURES.contentBlocks,
639
- 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
640
708
  };
641
709
  }
642
710
  function isContentBlockEnabled(features, type) {
@@ -647,7 +715,7 @@ function isFieldTypeEnabled(features, type) {
647
715
  }
648
716
 
649
717
  // src/hooks/useTheme.ts
650
- import { useState } from "react";
718
+ import { useMemo, useState } from "react";
651
719
 
652
720
  // src/theme/defaultTheme.ts
653
721
  var DEFAULT_THEME = {
@@ -666,17 +734,17 @@ var DEFAULT_THEME = {
666
734
 
667
735
  // src/hooks/useTheme.ts
668
736
  function useTheme(themeOverrideProp) {
669
- const baseTheme = {
737
+ const [themeOverrides, setThemeOverrides] = useState({});
738
+ const baseTheme = useMemo(() => ({
670
739
  ...DEFAULT_THEME,
671
740
  ...themeOverrideProp || {},
672
741
  layout: { ...DEFAULT_THEME.layout, ...themeOverrideProp && themeOverrideProp.layout || {} }
673
- };
674
- const [themeOverrides, setThemeOverrides] = useState({});
675
- const theme = {
742
+ }), [themeOverrideProp]);
743
+ const theme = useMemo(() => ({
676
744
  ...baseTheme,
677
745
  ...themeOverrides,
678
746
  layout: { ...baseTheme.layout, ...themeOverrides.layout || {} }
679
- };
747
+ }), [baseTheme, themeOverrides]);
680
748
  function updateThemeColor(key, value) {
681
749
  setThemeOverrides((prev) => ({ ...prev, [key]: value }));
682
750
  }
@@ -875,35 +943,49 @@ import { useEffect, useRef, useState as useState3 } from "react";
875
943
  var DRAFT_KEY = "form-page-builder:draft";
876
944
  var INDEX_KEY = "form-page-builder:index";
877
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";
878
948
 
879
949
  // src/hooks/usePersistence.ts
880
- var MAX_TEMPLATES = 5;
950
+ function snapshotOf(d) {
951
+ return JSON.stringify({ title: d.title, themeOverrides: d.themeOverrides, sections: d.sections });
952
+ }
881
953
  function usePersistence({
882
954
  storage,
883
955
  autosave,
956
+ templateMax,
957
+ templateManage,
884
958
  language,
885
959
  chrome,
886
- document,
960
+ document: document2,
887
961
  initialDocument,
888
962
  onLoadDocument,
889
963
  onLoadThemeOverrides,
890
964
  onTitleChange,
891
965
  onNewForm,
966
+ onTemplateChange,
892
967
  ensureActiveSection
893
968
  }) {
894
969
  const [currentFormId, setCurrentFormId] = useState3(null);
895
970
  const [loadingDraft, setLoadingDraft] = useState3(true);
896
971
  const [saveState, setSaveState] = useState3("idle");
972
+ const [templateState, setTemplateState] = useState3("idle");
897
973
  const [savedForms, setSavedForms] = useState3([]);
898
974
  const [saveAsPrompt, setSaveAsPrompt] = useState3(null);
975
+ const [notice, setNotice] = useState3(null);
899
976
  const autosaveTimer = useRef(null);
900
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
+ }
901
983
  async function refreshLibrary() {
902
984
  try {
903
985
  const raw = await storage.get(INDEX_KEY);
904
986
  const list = raw ? JSON.parse(raw) : [];
905
987
  setSavedForms(list.sort((a, b) => b.updatedAt - a.updatedAt));
906
- } catch (err) {
988
+ } catch {
907
989
  setSavedForms([]);
908
990
  }
909
991
  }
@@ -927,12 +1009,21 @@ function usePersistence({
927
1009
  onLoadDocument(doc);
928
1010
  onLoadThemeOverrides(doc.themeOverrides);
929
1011
  }
930
- if (parsed.currentFormId) setCurrentFormId(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
+ }
931
1022
  } else {
932
1023
  ensureActiveSection();
933
1024
  }
934
1025
  }
935
- } catch (err) {
1026
+ } catch {
936
1027
  ensureActiveSection();
937
1028
  } finally {
938
1029
  setLoadingDraft(false);
@@ -947,86 +1038,125 @@ function usePersistence({
947
1038
  if (autosaveTimer.current) clearTimeout(autosaveTimer.current);
948
1039
  autosaveTimer.current = setTimeout(async () => {
949
1040
  try {
950
- await storage.set(DRAFT_KEY, JSON.stringify({ ...document, currentFormId }));
1041
+ await storage.set(DRAFT_KEY, JSON.stringify({ ...document2, currentFormId }));
951
1042
  setSaveState("saved");
952
- } catch (err) {
1043
+ } catch {
953
1044
  setSaveState("error");
954
1045
  }
955
1046
  }, 600);
956
1047
  return () => {
957
1048
  if (autosaveTimer.current) clearTimeout(autosaveTimer.current);
958
1049
  };
959
- }, [document.title, document.themeOverrides, document.sections, currentFormId]);
1050
+ }, [document2.title, document2.themeOverrides, document2.sections, currentFormId]);
960
1051
  async function saveAs(name) {
961
- if (savedForms.length >= MAX_TEMPLATES) {
962
- alert(chrome.templatesLimitReached);
1052
+ if (!templateManage) return;
1053
+ if (savedForms.length >= templateMax) {
1054
+ setSaveAsPrompt(null);
1055
+ setNotice({ title: chrome.templatesLimitTitle, message: chrome.templatesLimitReached(templateMax) });
963
1056
  return;
964
1057
  }
965
1058
  const id = genFormId();
966
1059
  const now = Date.now();
1060
+ setTemplateState("saving");
967
1061
  try {
968
1062
  const newTitle = bi(name, "");
969
- await storage.set(formKey(id), JSON.stringify({ ...document, title: newTitle, id, updatedAt: now }));
1063
+ const docToSave = { ...document2, title: newTitle };
1064
+ await storage.set(formKey(id), JSON.stringify({ ...docToSave, id, updatedAt: now }));
970
1065
  const next = [...savedForms, { id, title: name, updatedAt: now }];
971
1066
  await storage.set(INDEX_KEY, JSON.stringify(next));
972
1067
  setSavedForms(next);
973
1068
  setCurrentFormId(id);
974
1069
  onTitleChange(newTitle);
975
1070
  setSaveAsPrompt(null);
976
- } catch (err) {
977
- alert("Couldn't save the form. Please try again.");
1071
+ savedSnapshot.current = snapshotOf(docToSave);
1072
+ flashSaved();
1073
+ onTemplateChange?.({ id, title: name, source: "new" });
1074
+ } catch {
1075
+ setTemplateState("error");
978
1076
  }
979
1077
  }
980
1078
  async function saveExisting() {
1079
+ if (!templateManage) return;
981
1080
  if (!currentFormId) {
982
- setSaveAsPrompt({ open: true, suggestedName: t(document.title, language) });
1081
+ setSaveAsPrompt({ open: true, suggestedName: t(document2.title, language) });
983
1082
  return;
984
1083
  }
985
1084
  const now = Date.now();
1085
+ setTemplateState("saving");
986
1086
  try {
987
- await storage.set(formKey(currentFormId), JSON.stringify({ ...document, id: currentFormId, updatedAt: now }));
988
- const next = savedForms.map((f) => f.id === currentFormId ? { ...f, title: t(document.title, "en"), updatedAt: now } : f);
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);
989
1089
  setSavedForms(next);
990
1090
  await storage.set(INDEX_KEY, JSON.stringify(next));
991
- } catch (err) {
992
- alert("Couldn't save the form. Please try again.");
1091
+ savedSnapshot.current = snapshotOf(document2);
1092
+ flashSaved();
1093
+ onTemplateChange?.({ id: currentFormId, title: t(document2.title, language), source: "saved" });
1094
+ } catch {
1095
+ setTemplateState("error");
993
1096
  }
994
1097
  }
995
1098
  async function loadForm(id) {
1099
+ setTemplateState("loading");
996
1100
  try {
997
1101
  const raw = await storage.get(formKey(id));
998
- if (!raw) return;
1102
+ if (!raw) {
1103
+ setTemplateState("error");
1104
+ return false;
1105
+ }
999
1106
  const doc = migrateDocument(JSON.parse(raw));
1000
- if (!doc) return;
1107
+ if (!doc) {
1108
+ setTemplateState("error");
1109
+ return false;
1110
+ }
1001
1111
  onLoadDocument(doc);
1002
1112
  onLoadThemeOverrides(doc.themeOverrides);
1003
- setCurrentFormId(id);
1004
- } catch (err) {
1005
- alert("Couldn't load that form.");
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;
1006
1122
  }
1007
1123
  }
1008
1124
  async function deleteForm(id) {
1125
+ if (!templateManage) return;
1126
+ const removed = savedForms.find((f) => f.id === id);
1009
1127
  try {
1010
1128
  await storage.delete(formKey(id));
1011
1129
  const next = savedForms.filter((f) => f.id !== id);
1012
1130
  setSavedForms(next);
1013
1131
  await storage.set(INDEX_KEY, JSON.stringify(next));
1014
- if (currentFormId === id) setCurrentFormId(null);
1015
- } catch (err) {
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");
1016
1139
  }
1017
1140
  }
1018
1141
  function newForm() {
1019
1142
  onNewForm();
1020
1143
  setCurrentFormId(null);
1144
+ savedSnapshot.current = null;
1145
+ setTemplateState("idle");
1021
1146
  }
1147
+ const activeTemplate = currentFormId ? savedForms.find((f) => f.id === currentFormId) ?? null : null;
1148
+ const isTemplateDirty = savedSnapshot.current !== null && snapshotOf(document2) !== savedSnapshot.current;
1022
1149
  function dismissSaveAsPrompt() {
1023
1150
  setSaveAsPrompt(null);
1024
1151
  }
1025
1152
  return {
1026
1153
  loadingDraft,
1027
1154
  saveState,
1155
+ templateState,
1028
1156
  savedForms,
1029
1157
  currentFormId,
1158
+ activeTemplateTitle: activeTemplate?.title ?? null,
1159
+ isTemplateDirty,
1030
1160
  saveAs,
1031
1161
  saveExisting,
1032
1162
  loadForm,
@@ -1034,15 +1164,64 @@ function usePersistence({
1034
1164
  refreshLibrary,
1035
1165
  newForm,
1036
1166
  saveAsPrompt,
1037
- dismissSaveAsPrompt
1167
+ dismissSaveAsPrompt,
1168
+ notice,
1169
+ dismissNotice: () => setNotice(null)
1038
1170
  };
1039
1171
  }
1040
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
+
1041
1220
  // src/hooks/useDragReorder.ts
1042
- import { useRef as useRef2, useState as useState4 } from "react";
1221
+ import { useRef as useRef3, useState as useState5 } from "react";
1043
1222
  function useDragReorder(reorderWithinSection) {
1044
- const dragRef = useRef2({ sectionId: null, index: null });
1045
- const [dragOverKey, setDragOverKey] = useState4(null);
1223
+ const dragRef = useRef3({ sectionId: null, index: null });
1224
+ const [dragOverKey, setDragOverKey] = useState5(null);
1046
1225
  function getDropZoneHandlers(sectionId, index) {
1047
1226
  const dragKey = `${sectionId}:${index}`;
1048
1227
  return {
@@ -1070,13 +1249,13 @@ function useDragReorder(reorderWithinSection) {
1070
1249
  }
1071
1250
 
1072
1251
  // src/components/Toolbar.tsx
1073
- 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";
1074
1253
 
1075
1254
  // src/styles/styles.ts
1076
1255
  var styles = {
1077
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" },
1078
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" },
1079
- toolbarLeft: { display: "flex", alignItems: "center", gap: 10 },
1258
+ toolbarLeft: { display: "flex", alignItems: "center", gap: 10, minWidth: 0, flexShrink: 1 },
1080
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" },
1081
1260
  titleInput: { border: "none", outline: "none", fontSize: 14, fontWeight: 600, background: "transparent", color: "var(--fb-ink)", padding: "4px 6px", borderRadius: 6, minWidth: 160 },
1082
1261
  toolbarRight: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" },
@@ -1086,6 +1265,8 @@ var styles = {
1086
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 },
1087
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 },
1088
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)" },
1089
1270
  countBadge: { fontSize: 10.5, fontWeight: 700, background: "var(--fb-primary-soft)", color: "var(--fb-primary)", borderRadius: 9, padding: "1px 6px", marginLeft: 2 },
1090
1271
  loadingScreen: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", color: "var(--fb-muted)" },
1091
1272
  libraryBody: { padding: "var(--fb-space-panel)", overflowY: "auto", display: "flex", flexDirection: "column", gap: 4 },
@@ -1222,21 +1403,36 @@ function Toolbar({
1222
1403
  languages,
1223
1404
  mode,
1224
1405
  saveState,
1406
+ templateState,
1407
+ activeTemplateTitle,
1408
+ templateDirty,
1225
1409
  chrome,
1226
1410
  features,
1227
1411
  savedFormsCount,
1412
+ clipboardReady,
1413
+ templateCopied,
1228
1414
  onTitleChange,
1229
1415
  onLanguageChange,
1230
1416
  onModeChange,
1231
1417
  onNewForm,
1232
1418
  onOpenLibrary,
1233
1419
  onSaveExisting,
1234
- onOpenJson
1420
+ onOpenJson,
1421
+ onCopyTemplate,
1422
+ onPasteTemplate
1235
1423
  }) {
1236
1424
  return /* @__PURE__ */ jsxs2("div", { style: styles.toolbar, children: [
1237
1425
  /* @__PURE__ */ jsxs2("div", { style: styles.toolbarLeft, children: [
1238
1426
  /* @__PURE__ */ jsx2("div", { style: styles.logoMark, children: "FB" }),
1239
- 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
+ ] })
1240
1436
  ] }),
1241
1437
  /* @__PURE__ */ jsxs2("div", { style: styles.toolbarRight, children: [
1242
1438
  /* @__PURE__ */ jsxs2("span", { style: styles.saveStatus, children: [
@@ -1269,20 +1465,20 @@ function Toolbar({
1269
1465
  chrome.preview
1270
1466
  ] })
1271
1467
  ] }),
1272
- (features.newForm || features.templates) && /* @__PURE__ */ jsx2("div", { style: styles.toolbarDivider }),
1468
+ (features.newForm || features.templates.enabled) && /* @__PURE__ */ jsx2("div", { style: styles.toolbarDivider }),
1273
1469
  features.newForm && /* @__PURE__ */ jsxs2("button", { style: styles.ghostBtn, onClick: onNewForm, title: chrome.startNewForm, children: [
1274
1470
  /* @__PURE__ */ jsx2(FilePlus2, { size: 14 }),
1275
1471
  " ",
1276
1472
  chrome.newForm
1277
1473
  ] }),
1278
- features.templates && /* @__PURE__ */ jsxs2(Fragment, { children: [
1474
+ features.templates.enabled && /* @__PURE__ */ jsxs2(Fragment, { children: [
1279
1475
  /* @__PURE__ */ jsxs2("button", { style: styles.ghostBtn, onClick: onOpenLibrary, title: chrome.openTemplatesTitle, children: [
1280
1476
  /* @__PURE__ */ jsx2(FolderOpen, { size: 14 }),
1281
1477
  " ",
1282
1478
  chrome.templates,
1283
1479
  savedFormsCount > 0 && /* @__PURE__ */ jsx2("span", { style: styles.countBadge, children: savedFormsCount })
1284
1480
  ] }),
1285
- /* @__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: [
1286
1482
  /* @__PURE__ */ jsx2(Save, { size: 14 }),
1287
1483
  " ",
1288
1484
  chrome.save
@@ -1292,13 +1488,17 @@ function Toolbar({
1292
1488
  /* @__PURE__ */ jsx2(Code2, { size: 14 }),
1293
1489
  " ",
1294
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 }) })
1295
1495
  ] })
1296
1496
  ] })
1297
1497
  ] });
1298
1498
  }
1299
1499
 
1300
1500
  // src/components/Palette.tsx
1301
- import { useState as useState5 } from "react";
1501
+ import { useState as useState6 } from "react";
1302
1502
  import { Plus } from "lucide-react";
1303
1503
 
1304
1504
  // src/components/Segmented.tsx
@@ -1393,11 +1593,13 @@ function Palette({
1393
1593
  theme,
1394
1594
  updateThemeColor,
1395
1595
  updateThemeLayout,
1396
- resetTheme
1596
+ resetTheme,
1597
+ fieldCount
1397
1598
  }) {
1398
- const [tab, setTab] = useState5("blocks");
1599
+ const [tab, setTab] = useState6("blocks");
1399
1600
  const contentTypes = CONTENT_TYPES.filter((f) => isContentBlockEnabled(features, f.type));
1400
1601
  const formTypes = FORM_TYPES.filter((f) => isFieldTypeEnabled(features, f.type));
1602
+ const atFieldCap = typeof features.maxFields === "number" && fieldCount >= features.maxFields;
1401
1603
  return /* @__PURE__ */ jsxs5("div", { className: "fb-palette", style: styles.palette, children: [
1402
1604
  features.design && /* @__PURE__ */ jsx5("div", { style: { marginBottom: 14 }, children: /* @__PURE__ */ jsx5(
1403
1605
  Segmented,
@@ -1440,7 +1642,8 @@ function Palette({
1440
1642
  /* @__PURE__ */ jsx5("div", { style: { ...styles.panelHeading, marginTop: 16 }, children: chrome.formFields }),
1441
1643
  /* @__PURE__ */ jsx5("div", { style: styles.paletteList, children: formTypes.map((f) => {
1442
1644
  const Icon = f.icon;
1443
- return /* @__PURE__ */ jsxs5("button", { style: styles.paletteItem, onClick: () => onAddField(f.type), children: [
1645
+ const itemStyle = atFieldCap ? { ...styles.paletteItem, opacity: 0.45, cursor: "not-allowed" } : styles.paletteItem;
1646
+ return /* @__PURE__ */ jsxs5("button", { type: "button", disabled: atFieldCap, style: itemStyle, onClick: () => onAddField(f.type), children: [
1444
1647
  /* @__PURE__ */ jsx5(Icon, { size: 16, color: "var(--fb-primary)" }),
1445
1648
  /* @__PURE__ */ jsx5("span", { children: chrome[FIELD_TYPE_CHROME_KEY[f.type]] }),
1446
1649
  /* @__PURE__ */ jsx5(Plus, { size: 13, color: "var(--fb-muted)", style: { marginLeft: "auto" } })
@@ -1802,7 +2005,7 @@ function SectionCard({
1802
2005
  ),
1803
2006
  section.collapsed && /* @__PURE__ */ jsx10("span", { style: styles.miniBadge, children: chrome.fieldsCount(section.fields.length) }),
1804
2007
  /* @__PURE__ */ jsxs8("div", { style: styles.sectionHeaderActions, onClick: (e) => e.stopPropagation(), children: [
1805
- features.sections && !section.collapsed && /* @__PURE__ */ jsxs8("div", { style: styles.swatchRow, children: [
2008
+ features.sectionBackground && !section.collapsed && /* @__PURE__ */ jsxs8("div", { style: styles.swatchRow, children: [
1806
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")),
1807
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 })
1808
2011
  ] }),
@@ -2123,7 +2326,7 @@ function InspectorBody({ selected, language, chrome, features, onUpdateField, on
2123
2326
  }
2124
2327
 
2125
2328
  // src/components/PreviewPane.tsx
2126
- import { useState as useState6 } from "react";
2329
+ import { useState as useState7 } from "react";
2127
2330
  import { PartyPopper, X as X2, CircleAlert as CircleAlert2 } from "lucide-react";
2128
2331
 
2129
2332
  // src/lib/validate.ts
@@ -2171,11 +2374,11 @@ function collectFieldValues(fields) {
2171
2374
 
2172
2375
  // src/components/PreviewPane.tsx
2173
2376
  import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
2174
- function PreviewPane({ title, sections, onFieldChange, language, strings, chrome, baseMaxWidth, onSubmit }) {
2175
- const [device, setDevice] = useState6("laptop");
2176
- const [errors, setErrors] = useState6({});
2177
- const [submitted, setSubmitted] = useState6(null);
2178
- const [attemptedButtonId, setAttemptedButtonId] = useState6(null);
2377
+ function PreviewPane({ title, sections, onFieldChange, language, strings, chrome, baseMaxWidth, features, onSubmit }) {
2378
+ const [device, setDevice] = useState7("laptop");
2379
+ const [errors, setErrors] = useState7({});
2380
+ const [submitted, setSubmitted] = useState7(null);
2381
+ const [attemptedButtonId, setAttemptedButtonId] = useState7(null);
2179
2382
  const deviceOptions = buildDeviceOptions(baseMaxWidth, chrome);
2180
2383
  const maxWidth = (deviceOptions.find((d) => d.value === device) || deviceOptions[0]).maxWidth;
2181
2384
  const allFields = sections.flatMap((s) => s.fields);
@@ -2234,7 +2437,7 @@ function PreviewPane({ title, sections, onFieldChange, language, strings, chrome
2234
2437
  }
2235
2438
  return /* @__PURE__ */ jsxs12("div", { style: styles.previewWrap, children: [
2236
2439
  /* @__PURE__ */ jsxs12("div", { style: { width: "100%", maxWidth, display: "flex", flexDirection: "column", alignItems: "center" }, children: [
2237
- /* @__PURE__ */ jsx14("div", { style: styles.previewToolbar, children: /* @__PURE__ */ jsx14(Segmented, { options: deviceOptions, value: device, onChange: setDevice }) }),
2440
+ features.deviceToggle && /* @__PURE__ */ jsx14("div", { style: styles.previewToolbar, children: /* @__PURE__ */ jsx14(Segmented, { options: deviceOptions, value: device, onChange: setDevice }) }),
2238
2441
  /* @__PURE__ */ jsxs12("div", { style: { ...styles.previewCard, maxWidth, width: "100%" }, children: [
2239
2442
  /* @__PURE__ */ jsx14("h2", { style: styles.previewTitle, children: t(title, language) }),
2240
2443
  allFields.length === 0 && /* @__PURE__ */ jsx14("p", { style: { color: "var(--fb-muted)", fontSize: 14 }, children: strings.addFieldsHint }),
@@ -2279,18 +2482,67 @@ function PreviewPane({ title, sections, onFieldChange, language, strings, chrome
2279
2482
  }
2280
2483
 
2281
2484
  // src/components/modals/JsonModal.tsx
2282
- 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
2283
2534
  import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
2284
2535
  function JsonModal({ chrome, jsonString, copied, onCopy, onClose }) {
2285
- return /* @__PURE__ */ jsx15("div", { style: styles.modalOverlay, onClick: onClose, children: /* @__PURE__ */ jsxs13("div", { style: styles.modal, onClick: (e) => e.stopPropagation(), children: [
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: [
2286
2538
  /* @__PURE__ */ jsxs13("div", { style: styles.modalHeader, children: [
2287
2539
  /* @__PURE__ */ jsx15("span", { style: { fontWeight: 600, fontSize: 14 }, children: chrome.formJson }),
2288
2540
  /* @__PURE__ */ jsxs13("div", { style: { display: "flex", gap: 8 }, children: [
2289
- /* @__PURE__ */ jsxs13("button", { style: styles.ghostBtn, onClick: onCopy, children: [
2290
- copied ? /* @__PURE__ */ jsx15(Check, { size: 14 }) : /* @__PURE__ */ jsx15(ClipboardCopy, { size: 14 }),
2541
+ /* @__PURE__ */ jsxs13("button", { type: "button", style: styles.ghostBtn, onClick: onCopy, children: [
2542
+ copied ? /* @__PURE__ */ jsx15(Check2, { size: 14 }) : /* @__PURE__ */ jsx15(ClipboardCopy2, { size: 14 }),
2291
2543
  copied ? chrome.copied : chrome.copy
2292
2544
  ] }),
2293
- /* @__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 }) })
2294
2546
  ] })
2295
2547
  ] }),
2296
2548
  /* @__PURE__ */ jsx15("pre", { style: styles.jsonPre, children: jsonString })
@@ -2298,57 +2550,99 @@ function JsonModal({ chrome, jsonString, copied, onCopy, onClose }) {
2298
2550
  }
2299
2551
 
2300
2552
  // src/components/modals/TemplatesModal.tsx
2301
- 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";
2302
2554
  import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
2303
- function TemplatesModal({ chrome, savedForms, currentFormId, onOpen, onDelete, onClose }) {
2304
- return /* @__PURE__ */ jsx16("div", { style: styles.modalOverlay, onClick: onClose, children: /* @__PURE__ */ jsxs14("div", { style: styles.modal, onClick: (e) => e.stopPropagation(), children: [
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: [
2305
2559
  /* @__PURE__ */ jsxs14("div", { style: styles.modalHeader, children: [
2306
2560
  /* @__PURE__ */ jsx16("span", { style: { fontWeight: 600, fontSize: 14 }, children: chrome.templates }),
2307
- /* @__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 }) })
2308
2562
  ] }),
2309
- /* @__PURE__ */ jsx16("div", { style: styles.libraryBody, children: savedForms.length === 0 ? /* @__PURE__ */ jsx16("div", { style: styles.inspectorEmpty, children: chrome.noTemplates }) : savedForms.map((f) => /* @__PURE__ */ jsxs14("div", { style: styles.libraryRow, children: [
2310
- /* @__PURE__ */ jsxs14("div", { style: { minWidth: 0 }, children: [
2311
- /* @__PURE__ */ jsxs14("div", { style: styles.libraryRowTitle, children: [
2312
- f.title,
2313
- f.id === currentFormId && /* @__PURE__ */ jsx16("span", { style: styles.currentBadge, children: chrome.current })
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
+ ] })
2314
2575
  ] }),
2315
- /* @__PURE__ */ jsxs14("div", { style: styles.libraryRowMeta, children: [
2316
- chrome.updated,
2317
- " ",
2318
- new Date(f.updatedAt).toLocaleString()
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 }) })
2319
2583
  ] })
2320
- ] }),
2321
- /* @__PURE__ */ jsxs14("div", { style: { display: "flex", gap: 6, flexShrink: 0 }, children: [
2322
- /* @__PURE__ */ jsx16("button", { style: styles.ghostBtn, onClick: () => onOpen(f.id), children: chrome.open }),
2323
- /* @__PURE__ */ jsx16("button", { style: { ...styles.iconBtn, ...styles.iconBtnDanger }, title: chrome.delete, onClick: () => onDelete(f.id), children: /* @__PURE__ */ jsx16(Trash24, { size: 13 }) })
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
2324
2589
  ] })
2325
- ] }, f.id)) })
2590
+ ] })
2326
2591
  ] }) });
2327
2592
  }
2328
2593
 
2329
2594
  // src/components/modals/SaveAsModal.tsx
2330
- import { useState as useState7 } from "react";
2331
- 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";
2332
2597
  import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
2333
- function SaveAsModal({ chrome, suggestedName, onSave, onClose }) {
2334
- const [name, setName] = useState7(suggestedName);
2335
- return /* @__PURE__ */ jsx17("div", { style: styles.modalOverlay, onClick: onClose, children: /* @__PURE__ */ jsxs15("div", { style: { ...styles.modal, maxWidth: 380 }, onClick: (e) => e.stopPropagation(), children: [
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: [
2336
2604
  /* @__PURE__ */ jsxs15("div", { style: styles.modalHeader, children: [
2337
2605
  /* @__PURE__ */ jsx17("span", { style: { fontWeight: 600, fontSize: 14 }, children: chrome.saveForm }),
2338
- /* @__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 }) })
2339
2607
  ] }),
2340
2608
  /* @__PURE__ */ jsxs15("div", { style: { padding: 16 }, children: [
2341
2609
  /* @__PURE__ */ jsx17("label", { style: styles.propLabel, children: chrome.formName }),
2342
- /* @__PURE__ */ jsx17("input", { autoFocus: true, style: styles.propInput, value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => e.key === "Enter" && name.trim() && onSave(name.trim()) }),
2343
- /* @__PURE__ */ jsxs15("button", { style: { ...styles.primaryBtn, width: "100%", justifyContent: "center", marginTop: 14 }, disabled: !name.trim(), onClick: () => onSave(name.trim()), children: [
2344
- /* @__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 }),
2345
2613
  " ",
2346
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
2347
2620
  ] })
2348
2621
  ] })
2349
2622
  ] }) });
2350
2623
  }
2351
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
+
2352
2646
  // src/styles/globalCss.ts
2353
2647
  var css = `
2354
2648
  * { box-sizing: border-box; }
@@ -2356,20 +2650,14 @@ var css = `
2356
2650
  input, textarea, select { font-family: inherit; }
2357
2651
  @keyframes fb-spin { to { transform: rotate(360deg); } }
2358
2652
  .spin { animation: fb-spin 0.8s linear infinite; }
2359
- /* Caps the widget at the viewport height so it scrolls internally rather
2360
- than growing past whatever space its host container gives it; the
2361
- second declaration upgrades to the dynamic viewport unit on browsers
2362
- that support it (mobile browser chrome resizing the viewport), and is
2363
- simply ignored by ones that don't. */
2653
+ /* Cap at viewport height so the widget scrolls internally; dvh upgrade is ignored where unsupported. */
2364
2654
  .fb-root { max-height: 100vh; max-height: 100dvh; }
2365
2655
 
2366
- /* Below this width the fixed 200px Palette + 270px Inspector columns
2367
- don't leave room for a usable Canvas, so instead of stacking all
2368
- three in flow (which buries Canvas under a tall Palette), Palette
2369
- and Inspector become narrow drawers over Canvas (which stays
2370
- partially visible behind them), toggled by .fb-mobile-bar's two
2371
- buttons, tracked by .fb-work-area's data-mobile-panel attribute,
2372
- 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. */
2373
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; }
2374
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; }
2375
2663
  .fb-mobile-btn[aria-pressed="true"] { border-color: var(--fb-primary); background: var(--fb-primary-soft); color: var(--fb-primary); }
@@ -2395,7 +2683,7 @@ var css = `
2395
2683
  `;
2396
2684
 
2397
2685
  // src/FormBuilder.tsx
2398
- import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
2686
+ import { jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
2399
2687
  var FormBuilder = forwardRef(function FormBuilder2({
2400
2688
  theme: themeOverrideProp,
2401
2689
  language: languageOverride,
@@ -2405,24 +2693,40 @@ var FormBuilder = forwardRef(function FormBuilder2({
2405
2693
  features: featuresProp,
2406
2694
  storage: storageProp,
2407
2695
  onSubmit,
2408
- initialDocument
2696
+ initialDocument,
2697
+ initialMode,
2698
+ onModeChange,
2699
+ onTemplateChange,
2700
+ templateClipboardKey
2409
2701
  }, ref) {
2410
2702
  const features = resolveFeatures(featuresProp);
2411
2703
  const storage = storageProp ?? localStorageAdapter;
2412
2704
  const { theme, updateThemeColor, updateThemeLayout, resetTheme, replaceThemeOverrides, themeOverrides } = useTheme(themeOverrideProp);
2413
- const [language, setLanguage] = useState8(languageOverride || languages[0]?.code || "en");
2705
+ const [language, setLanguage] = useState9(languageOverride || languages[0]?.code || "en");
2414
2706
  const strings = { ...DEFAULT_STRINGS[language] || DEFAULT_STRINGS.en, ...stringsOverride && stringsOverride[language] || {} };
2415
2707
  const chrome = { ...CHROME[language] || CHROME.en, ...chromeOverride && chromeOverride[language] || {} };
2416
2708
  const doc = useFormDocument({ language, chrome });
2417
2709
  const drag = useDragReorder(doc.reorderWithinSection);
2418
- const [mode, setMode] = useState8("build");
2419
- const [showJson, setShowJson] = useState8(false);
2420
- const [showLibrary, setShowLibrary] = useState8(false);
2421
- const [copied, setCopied] = useState8(false);
2422
- const [mobilePanel, setMobilePanel] = useState8("none");
2710
+ const [mode, setModeState] = useState9(initialMode ?? "build");
2711
+ const setMode = (next) => {
2712
+ setModeState(next);
2713
+ onModeChange?.(next);
2714
+ };
2715
+ useEffect4(() => {
2716
+ onModeChange?.(mode);
2717
+ }, []);
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");
2423
2725
  const persistence = usePersistence({
2424
2726
  storage,
2425
2727
  autosave: features.autosave,
2728
+ templateMax: features.templates.max,
2729
+ templateManage: features.templates.manage,
2426
2730
  language,
2427
2731
  chrome,
2428
2732
  document: { title: doc.title, themeOverrides, sections: doc.sections },
@@ -2434,16 +2738,17 @@ var FormBuilder = forwardRef(function FormBuilder2({
2434
2738
  doc.resetToBlank();
2435
2739
  resetTheme();
2436
2740
  },
2741
+ onTemplateChange,
2437
2742
  ensureActiveSection: () => doc.setActiveSectionId((prev) => prev ?? doc.sections[0]?.id ?? null)
2438
2743
  });
2439
- const jsonDoc = {
2744
+ const jsonDoc = useMemo2(() => ({
2440
2745
  version: 5,
2441
2746
  title: doc.title,
2442
2747
  theme,
2443
2748
  themeOverrides,
2444
2749
  sections: doc.sections.map((s) => ({ id: s.id, title: s.title, background: s.background, collapsed: s.collapsed, fields: s.fields }))
2445
- };
2446
- const jsonString = JSON.stringify(jsonDoc, null, 2);
2750
+ }), [doc.title, doc.sections, theme, themeOverrides]);
2751
+ const jsonString = useMemo2(() => JSON.stringify(jsonDoc, null, 2), [jsonDoc]);
2447
2752
  function copyJson() {
2448
2753
  navigator.clipboard.writeText(jsonString).then(() => {
2449
2754
  setCopied(true);
@@ -2451,17 +2756,31 @@ var FormBuilder = forwardRef(function FormBuilder2({
2451
2756
  }).catch(() => {
2452
2757
  });
2453
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
+ }
2454
2773
  useImperativeHandle(ref, () => ({
2455
2774
  getDocument: () => jsonDoc,
2456
2775
  exportJson: () => jsonString,
2457
2776
  loadDocument: (raw) => {
2458
- const migrated = migrateDocument(raw);
2459
- if (!migrated) return;
2460
- doc.loadDocument(migrated);
2461
- replaceThemeOverrides(migrated.themeOverrides);
2462
- }
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)))
2463
2781
  // eslint-disable-next-line react-hooks/exhaustive-deps
2464
2782
  }), [jsonDoc, jsonString]);
2783
+ const fieldCount = doc.sections.reduce((n, s) => n + s.fields.filter((f) => !getMeta(f.type).isContent).length, 0);
2465
2784
  const activeSectionIdx = doc.sections.findIndex((s) => s.id === doc.activeSection?.id);
2466
2785
  const activeSectionLabel = t(doc.activeSection?.title, language) || `#${activeSectionIdx + 1}`;
2467
2786
  const rootStyle = {
@@ -2485,9 +2804,9 @@ var FormBuilder = forwardRef(function FormBuilder2({
2485
2804
  "--fb-space-panel": `${theme.layout.panelPadding}px`,
2486
2805
  "--fb-space-ticket": `${theme.layout.ticketPadding}px`
2487
2806
  };
2488
- return /* @__PURE__ */ jsxs16("div", { className: "fb-root", style: rootStyle, children: [
2489
- /* @__PURE__ */ jsx18("style", { children: css }),
2490
- /* @__PURE__ */ jsx18(
2807
+ return /* @__PURE__ */ jsxs17("div", { className: "fb-root", style: rootStyle, children: [
2808
+ /* @__PURE__ */ jsx19("style", { children: css }),
2809
+ /* @__PURE__ */ jsx19(
2491
2810
  Toolbar,
2492
2811
  {
2493
2812
  title: doc.title,
@@ -2495,24 +2814,31 @@ var FormBuilder = forwardRef(function FormBuilder2({
2495
2814
  languages,
2496
2815
  mode,
2497
2816
  saveState: persistence.saveState,
2817
+ templateState: persistence.templateState,
2818
+ activeTemplateTitle: persistence.activeTemplateTitle,
2819
+ templateDirty: persistence.isTemplateDirty,
2498
2820
  chrome,
2499
2821
  features,
2500
2822
  savedFormsCount: persistence.savedForms.length,
2823
+ clipboardReady: clipboard.hasClipboard,
2824
+ templateCopied,
2501
2825
  onTitleChange: doc.updateTitle,
2502
2826
  onLanguageChange: setLanguage,
2503
2827
  onModeChange: setMode,
2504
2828
  onNewForm: persistence.newForm,
2505
2829
  onOpenLibrary: () => setShowLibrary(true),
2506
2830
  onSaveExisting: persistence.saveExisting,
2507
- onOpenJson: () => setShowJson(true)
2831
+ onOpenJson: () => setShowJson(true),
2832
+ onCopyTemplate: copyTemplate,
2833
+ onPasteTemplate: pasteTemplate
2508
2834
  }
2509
2835
  ),
2510
- persistence.loadingDraft ? /* @__PURE__ */ jsxs16("div", { style: styles.loadingScreen, children: [
2511
- /* @__PURE__ */ jsx18(Loader22, { size: 20, className: "spin" }),
2512
- /* @__PURE__ */ jsx18("span", { style: { marginTop: 8, fontSize: 13, color: "var(--fb-muted)" }, children: chrome.loadingDraft })
2513
- ] }) : mode === "build" ? /* @__PURE__ */ jsxs16("div", { className: "fb-work-area", style: styles.workArea, "data-mobile-panel": mobilePanel, children: [
2514
- /* @__PURE__ */ jsxs16("div", { className: "fb-mobile-bar", children: [
2515
- /* @__PURE__ */ jsxs16(
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(
2516
2842
  "button",
2517
2843
  {
2518
2844
  type: "button",
@@ -2520,13 +2846,13 @@ var FormBuilder = forwardRef(function FormBuilder2({
2520
2846
  "aria-pressed": mobilePanel === "palette",
2521
2847
  onClick: () => setMobilePanel((p) => p === "palette" ? "none" : "palette"),
2522
2848
  children: [
2523
- /* @__PURE__ */ jsx18(Menu, { size: 14 }),
2849
+ /* @__PURE__ */ jsx19(Menu, { size: 14 }),
2524
2850
  " ",
2525
2851
  chrome.paletteTabBlocks
2526
2852
  ]
2527
2853
  }
2528
2854
  ),
2529
- /* @__PURE__ */ jsxs16(
2855
+ /* @__PURE__ */ jsxs17(
2530
2856
  "button",
2531
2857
  {
2532
2858
  type: "button",
@@ -2534,16 +2860,16 @@ var FormBuilder = forwardRef(function FormBuilder2({
2534
2860
  "aria-pressed": mobilePanel === "inspector",
2535
2861
  onClick: () => setMobilePanel((p) => p === "inspector" ? "none" : "inspector"),
2536
2862
  children: [
2537
- /* @__PURE__ */ jsx18(SlidersHorizontal, { size: 14 }),
2863
+ /* @__PURE__ */ jsx19(SlidersHorizontal, { size: 14 }),
2538
2864
  " ",
2539
2865
  chrome.properties
2540
2866
  ]
2541
2867
  }
2542
2868
  )
2543
2869
  ] }),
2544
- /* @__PURE__ */ jsxs16("div", { className: "fb-canvas-area", style: styles.workArea, children: [
2545
- mobilePanel !== "none" && /* @__PURE__ */ jsx18("div", { className: "fb-mobile-backdrop", onClick: () => setMobilePanel("none") }),
2546
- /* @__PURE__ */ jsx18(
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(
2547
2873
  Palette,
2548
2874
  {
2549
2875
  activeSectionLabel,
@@ -2553,13 +2879,14 @@ var FormBuilder = forwardRef(function FormBuilder2({
2553
2879
  setMobilePanel("none");
2554
2880
  },
2555
2881
  features,
2882
+ fieldCount,
2556
2883
  theme,
2557
2884
  updateThemeColor,
2558
2885
  updateThemeLayout,
2559
2886
  resetTheme
2560
2887
  }
2561
2888
  ),
2562
- /* @__PURE__ */ jsx18(
2889
+ /* @__PURE__ */ jsx19(
2563
2890
  Canvas,
2564
2891
  {
2565
2892
  sections: doc.sections,
@@ -2591,7 +2918,7 @@ var FormBuilder = forwardRef(function FormBuilder2({
2591
2918
  getDragHandleProps: drag.getDragHandleProps
2592
2919
  }
2593
2920
  ),
2594
- /* @__PURE__ */ jsx18(
2921
+ /* @__PURE__ */ jsx19(
2595
2922
  Inspector,
2596
2923
  {
2597
2924
  selected: doc.selected,
@@ -2606,7 +2933,7 @@ var FormBuilder = forwardRef(function FormBuilder2({
2606
2933
  }
2607
2934
  )
2608
2935
  ] })
2609
- ] }) : /* @__PURE__ */ jsx18(
2936
+ ] }) : /* @__PURE__ */ jsx19(
2610
2937
  PreviewPane,
2611
2938
  {
2612
2939
  title: doc.title,
@@ -2616,32 +2943,59 @@ var FormBuilder = forwardRef(function FormBuilder2({
2616
2943
  strings,
2617
2944
  chrome,
2618
2945
  baseMaxWidth: theme.layout.maxWidth,
2946
+ features,
2619
2947
  onSubmit
2620
2948
  }
2621
2949
  ),
2622
- showJson && /* @__PURE__ */ jsx18(JsonModal, { chrome, jsonString, copied, onCopy: copyJson, onClose: () => setShowJson(false) }),
2623
- showLibrary && /* @__PURE__ */ jsx18(
2950
+ showJson && /* @__PURE__ */ jsx19(JsonModal, { chrome, jsonString, copied, onCopy: copyJson, onClose: () => setShowJson(false) }),
2951
+ showLibrary && /* @__PURE__ */ jsx19(
2624
2952
  TemplatesModal,
2625
2953
  {
2626
2954
  chrome,
2627
2955
  savedForms: persistence.savedForms,
2628
2956
  currentFormId: persistence.currentFormId,
2957
+ manage: features.templates.manage,
2958
+ templateState: persistence.templateState,
2629
2959
  onOpen: async (id) => {
2630
- await persistence.loadForm(id);
2631
- setShowLibrary(false);
2960
+ if (await persistence.loadForm(id)) setShowLibrary(false);
2632
2961
  },
2633
2962
  onDelete: persistence.deleteForm,
2634
2963
  onClose: () => setShowLibrary(false)
2635
2964
  }
2636
2965
  ),
2637
- persistence.saveAsPrompt && /* @__PURE__ */ jsx18(
2966
+ persistence.saveAsPrompt && /* @__PURE__ */ jsx19(
2638
2967
  SaveAsModal,
2639
2968
  {
2640
2969
  chrome,
2641
2970
  suggestedName: persistence.saveAsPrompt.suggestedName,
2971
+ templateState: persistence.templateState,
2642
2972
  onSave: persistence.saveAs,
2643
2973
  onClose: persistence.dismissSaveAsPrompt
2644
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
+ }
2645
2999
  )
2646
3000
  ] });
2647
3001
  });
@@ -2662,10 +3016,17 @@ var DARK_THEME = {
2662
3016
  layout: DEFAULT_THEME.layout
2663
3017
  };
2664
3018
  export {
3019
+ CLIPBOARD_KEY,
2665
3020
  DARK_THEME,
2666
3021
  DEFAULT_THEME,
3022
+ DRAFT_KEY,
2667
3023
  FormBuilder_default as FormBuilder,
3024
+ INDEX_KEY,
2668
3025
  FormBuilder_default as default,
2669
- localStorageAdapter
3026
+ formKey,
3027
+ localStorageAdapter,
3028
+ parseTemplate,
3029
+ savedFormId,
3030
+ serializeTemplate
2670
3031
  };
2671
3032
  //# sourceMappingURL=index.js.map