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