form-page-builder 2.1.0 → 2.3.0

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