form-page-builder 2.1.0 → 2.2.0

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