@ohhwells/bridge 0.1.86 → 0.1.87-next.261

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
@@ -69,6 +69,10 @@ function isRenderableTree(value) {
69
69
 
70
70
  // src/lib/ai-sections-store.ts
71
71
  var AI_SECTIONS_KEY = "__ohw_ai_sections";
72
+ var AI_SLOT_KEY_PREFIX = "ai.";
73
+ function aiSlotKeyPrefixFor(sectionId) {
74
+ return `${AI_SLOT_KEY_PREFIX}${sectionId}.`;
75
+ }
72
76
  var EMPTY_AI_SECTIONS = { v: 1, sections: [] };
73
77
  function parseAiSectionsState(raw) {
74
78
  if (!raw) return EMPTY_AI_SECTIONS;
@@ -112,6 +116,63 @@ function applyTreeToState(state, payload) {
112
116
  const others = state.sections.filter((existing) => existing.id !== entry.id);
113
117
  return { ...state, v: 1, sections: [...others, entry] };
114
118
  }
119
+ function foldAlignIntoTrees(state, store) {
120
+ const byId = new Map(state.sections.map((entry) => [entry.id, entry]));
121
+ const nextTrees = /* @__PURE__ */ new Map();
122
+ const treeFor = (id) => {
123
+ const cloned = nextTrees.get(id);
124
+ if (cloned) return cloned;
125
+ const entry = byId.get(id);
126
+ if (!entry) return void 0;
127
+ const fresh = {
128
+ ...entry.tree,
129
+ rows: entry.tree.rows.map((row) => ({ ...row, blocks: row.blocks.map((block) => ({ ...block })) }))
130
+ };
131
+ nextTrees.set(id, fresh);
132
+ return fresh;
133
+ };
134
+ const nodes = {};
135
+ for (const [key, override] of Object.entries(store.nodes)) {
136
+ const match = override.align !== void 0 && key.startsWith(AI_SLOT_KEY_PREFIX) ? key.match(/^ai\.(.+?)\.r(\d+)\.b(\d+)(?:\.|$)/) : null;
137
+ const tree = match ? treeFor(match[1]) : void 0;
138
+ const block = match && tree ? tree.rows[Number(match[2])]?.blocks[Number(match[3])] : void 0;
139
+ if (!block) {
140
+ nodes[key] = override;
141
+ continue;
142
+ }
143
+ block.align = override.align;
144
+ const rest = { ...override };
145
+ delete rest.align;
146
+ if (Object.keys(rest).length > 0) nodes[key] = rest;
147
+ }
148
+ const sections = {};
149
+ for (const [sectionId, override] of Object.entries(store.sections)) {
150
+ const tree = override.align !== void 0 ? treeFor(sectionId) : void 0;
151
+ if (!tree) {
152
+ sections[sectionId] = override;
153
+ continue;
154
+ }
155
+ for (const row of tree.rows) {
156
+ for (const block of row.blocks) block.align = override.align;
157
+ }
158
+ const rest = { ...override };
159
+ delete rest.align;
160
+ if (Object.keys(rest).length > 0) sections[sectionId] = rest;
161
+ }
162
+ if (nextTrees.size === 0) return { state, store, changed: false };
163
+ return {
164
+ state: {
165
+ ...state,
166
+ v: 1,
167
+ sections: state.sections.map((entry) => {
168
+ const tree = nextTrees.get(entry.id);
169
+ return tree ? { ...entry, tree } : entry;
170
+ })
171
+ },
172
+ store: { v: 1, sections, nodes },
173
+ changed: true
174
+ };
175
+ }
115
176
  function removeFromState(state, id) {
116
177
  return { ...state, v: 1, sections: state.sections.filter((entry) => entry.id !== id) };
117
178
  }
@@ -123,6 +184,33 @@ function deleteSectionFromState(state, sectionId) {
123
184
  if (removed.includes(sectionId)) return state;
124
185
  return { ...state, v: 1, sections: state.sections, removed: [...removed, sectionId] };
125
186
  }
187
+ function reapRemovedAiSections(state, store, removedIds, excludeIds = /* @__PURE__ */ new Set()) {
188
+ const generated = new Set(state.sections.map((entry) => entry.id));
189
+ const reapedIds = [...new Set(removedIds)].filter((id) => generated.has(id) && !excludeIds.has(id));
190
+ if (reapedIds.length === 0) {
191
+ return { state, store, reapedIds: [], slotPrefixes: [], changed: false };
192
+ }
193
+ const reaped = new Set(reapedIds);
194
+ const slotPrefixes = reapedIds.map(aiSlotKeyPrefixFor);
195
+ const nextState = {
196
+ ...state,
197
+ v: 1,
198
+ sections: state.sections.filter((entry) => !reaped.has(entry.id))
199
+ };
200
+ let nextStore = store;
201
+ if (store) {
202
+ const sections = {};
203
+ for (const [key, override] of Object.entries(store.sections)) {
204
+ if (!reaped.has(key)) sections[key] = override;
205
+ }
206
+ const nodes = {};
207
+ for (const [key, override] of Object.entries(store.nodes)) {
208
+ if (!slotPrefixes.some((prefix) => key.startsWith(prefix))) nodes[key] = override;
209
+ }
210
+ nextStore = { v: 1, sections, nodes };
211
+ }
212
+ return { state: nextState, store: nextStore, reapedIds, slotPrefixes, changed: true };
213
+ }
126
214
 
127
215
  // src/lib/brand-chrome.ts
128
216
  var BRAND_NAME_KEY = "__ohw_brand_name";
@@ -330,6 +418,12 @@ function styleSheetCss() {
330
418
  `[data-ohw-style-spacing="${spacing}"] { padding-top: ${px}px !important; padding-bottom: ${px}px !important; }`
331
419
  );
332
420
  }
421
+ rules.push(
422
+ `[data-ohw-style-corners="sharp"] :is(.card, [data-ohw-card]) { border-radius: 0 !important; }`
423
+ );
424
+ for (const align of ["left", "center", "right"]) {
425
+ rules.push(`[data-ohw-style-align="${align}"] { text-align: ${align} !important; }`);
426
+ }
333
427
  return rules.join("\n");
334
428
  }
335
429
  var STYLE_FONT_LINK_ID = "ohw-style-fonts";
@@ -356,10 +450,25 @@ var SECTION_ATTRS = {
356
450
  textDistribution: "data-ohw-style-distribution",
357
451
  headlineScale: "data-ohw-style-headline",
358
452
  imageAspect: "data-ohw-style-aspect",
359
- spacing: "data-ohw-style-spacing"
453
+ spacing: "data-ohw-style-spacing",
454
+ cornerStyle: "data-ohw-style-corners",
455
+ align: "data-ohw-style-align"
360
456
  };
361
457
  var NODE_WROTE_ATTR = "data-ohw-style-node";
362
- var NODE_PROPS = ["color", "font-family", "font-size", "background"];
458
+ var NODE_PROPS = [
459
+ "color",
460
+ "font-family",
461
+ "font-size",
462
+ "background",
463
+ "text-align",
464
+ "justify-content",
465
+ "align-items"
466
+ ];
467
+ var ALIGN_JUSTIFY = {
468
+ left: "flex-start",
469
+ center: "center",
470
+ right: "flex-end"
471
+ };
363
472
  function saveInline(el, prop) {
364
473
  const attr = `data-ohw-style-prev-${prop}`;
365
474
  if (el.hasAttribute(attr)) return;
@@ -403,6 +512,10 @@ function clearNodeProps(root) {
403
512
  function buttonSurfaceOf(el) {
404
513
  return el.closest("a, button") ?? el;
405
514
  }
515
+ function alignSubjectOf(el) {
516
+ const button = el.closest('[data-ohw-role="button"]');
517
+ return button?.parentElement ?? el;
518
+ }
406
519
  function applyStylesToDom(store) {
407
520
  ensureStyleSheet();
408
521
  clearSectionAttrs(document);
@@ -448,6 +561,18 @@ function applyStylesToDom(store) {
448
561
  el.style.setProperty("font-size", `${override.fontSize}px`, "important");
449
562
  el.setAttribute(NODE_WROTE_ATTR, "");
450
563
  }
564
+ if (override.align !== void 0) {
565
+ const subject = alignSubjectOf(el);
566
+ saveInline(subject, "text-align");
567
+ saveInline(subject, "justify-content");
568
+ subject.style.setProperty("text-align", override.align, "important");
569
+ subject.style.setProperty(
570
+ "justify-content",
571
+ ALIGN_JUSTIFY[override.align] ?? "flex-start",
572
+ "important"
573
+ );
574
+ subject.setAttribute(NODE_WROTE_ATTR, "");
575
+ }
451
576
  if (override.buttonBackground !== void 0 || override.buttonText !== void 0) {
452
577
  const surface = buttonSurfaceOf(el);
453
578
  if (override.buttonBackground !== void 0) {
@@ -468,9 +593,510 @@ function applyStylesToDom(store) {
468
593
  import { flushSync } from "react-dom";
469
594
  import { createRoot } from "react-dom/client";
470
595
 
596
+ // src/lib/sections.ts
597
+ var LINK_PICKER_EXCLUDED_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
598
+ function isChromeSection(el) {
599
+ return el.matches("header, nav, footer, aside");
600
+ }
601
+ function titleCaseSectionId(id) {
602
+ return id.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
603
+ }
604
+ function parseSectionsFromRoot(root) {
605
+ const seen = /* @__PURE__ */ new Set();
606
+ const sections = [];
607
+ for (const el of root.querySelectorAll("[data-ohw-section]")) {
608
+ const id = el.getAttribute("data-ohw-section") ?? "";
609
+ if (!id || seen.has(id) || LINK_PICKER_EXCLUDED_IDS.has(id)) continue;
610
+ if (el.parentElement?.closest("[data-ohw-section]")) continue;
611
+ if (el.hasAttribute("data-ohw-ai-template-hidden") || el.hasAttribute("data-ohw-ai-removed") || el.hasAttribute("data-ohw-ai-replaced-by"))
612
+ continue;
613
+ seen.add(id);
614
+ const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
615
+ sections.push({ id, label });
616
+ }
617
+ return sections;
618
+ }
619
+ function collectSectionsFromDom() {
620
+ if (typeof document === "undefined") return [];
621
+ return parseSectionsFromRoot(document);
622
+ }
623
+ function parseSectionsFromHtml(html) {
624
+ const doc = new DOMParser().parseFromString(html, "text/html");
625
+ return parseSectionsFromRoot(doc);
626
+ }
627
+
628
+ // src/lib/section-instances.ts
629
+ var SECTION_ORDER_KEY = "__ohw_section_order";
630
+ var REMOVED_ATTR = "data-ohw-section-removed";
631
+ function isRemovedSection(el) {
632
+ return el.hasAttribute(REMOVED_ATTR);
633
+ }
634
+ function topLevelSections() {
635
+ return Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
636
+ (el) => !el.parentElement?.closest("[data-ohw-section]") && !isChromeSection(el) && !isRemovedSection(el)
637
+ );
638
+ }
639
+ function instanceIdOf(el) {
640
+ return el.dataset.ohwInstance ?? el.dataset.ohwSection ?? "";
641
+ }
642
+ function findByInstanceId(instanceId) {
643
+ const escapedId = CSS.escape(instanceId);
644
+ return document.querySelector(`[data-ohw-instance="${escapedId}"]`) ?? document.querySelector(`[data-ohw-section="${escapedId}"]:not([data-ohw-instance])`);
645
+ }
646
+ function planSectionMove(instanceId, targetIndex, currentPath) {
647
+ const sections = topLevelSections();
648
+ const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
649
+ if (index === -1) return null;
650
+ const dragged = sections[index];
651
+ const others = sections.filter((_, i) => i !== index);
652
+ const clamped = Math.max(0, Math.min(targetIndex, others.length));
653
+ const reordered = [...others.slice(0, clamped), dragged, ...others.slice(clamped)];
654
+ return reordered.map((el, order) => ({
655
+ instanceId: instanceIdOf(el),
656
+ type: el.getAttribute("data-ohw-section") ?? "",
657
+ order,
658
+ pagePath: currentPath
659
+ }));
660
+ }
661
+ function moveSectionInstance(instanceId, direction, currentPath) {
662
+ const sections = topLevelSections();
663
+ const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
664
+ if (index === -1) return null;
665
+ const siblingIndex = direction === "up" ? index - 1 : index + 1;
666
+ if (siblingIndex < 0 || siblingIndex >= sections.length) return null;
667
+ const entries = planSectionMove(instanceId, siblingIndex, currentPath);
668
+ if (!entries) return null;
669
+ applyPersistedOrder(entries);
670
+ return entries;
671
+ }
672
+ function syncRemovedFlags(entries) {
673
+ const removedIds = new Set(entries.filter((e) => e.removed).map((e) => e.instanceId));
674
+ document.querySelectorAll(`[${REMOVED_ATTR}]`).forEach((el) => {
675
+ if (!removedIds.has(instanceIdOf(el))) {
676
+ el.style.removeProperty("display");
677
+ el.removeAttribute(REMOVED_ATTR);
678
+ }
679
+ });
680
+ for (const id of removedIds) {
681
+ const el = findByInstanceId(id);
682
+ if (el) {
683
+ el.style.display = "none";
684
+ el.setAttribute(REMOVED_ATTR, "");
685
+ }
686
+ }
687
+ }
688
+ function applyPersistedOrder(entries) {
689
+ syncRemovedFlags(entries);
690
+ if (entries.length === 0) return;
691
+ const sections = topLevelSections();
692
+ if (sections.length === 0) return;
693
+ const orderIndex = new Map(entries.map((e) => [e.instanceId, e.order]));
694
+ const ordered = [...sections].sort((a, b) => {
695
+ const aOrder = orderIndex.get(instanceIdOf(a));
696
+ const bOrder = orderIndex.get(instanceIdOf(b));
697
+ if (aOrder === void 0 && bOrder === void 0) return 0;
698
+ if (aOrder === void 0) return 1;
699
+ if (bOrder === void 0) return -1;
700
+ return aOrder - bOrder;
701
+ });
702
+ let prev = null;
703
+ for (const el of ordered) {
704
+ if (prev) prev.after(el);
705
+ prev = el;
706
+ }
707
+ }
708
+ function setSectionRemoved(instanceId, currentPath, existingEntries, removed) {
709
+ if (!findByInstanceId(instanceId)) return null;
710
+ const byId = new Map(existingEntries.map((e) => [e.instanceId, e]));
711
+ const allSections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
712
+ (el) => !el.parentElement?.closest("[data-ohw-section]") && !isChromeSection(el)
713
+ );
714
+ allSections.forEach((el, order) => {
715
+ const id = instanceIdOf(el);
716
+ if (!byId.has(id)) {
717
+ byId.set(id, { instanceId: id, type: el.getAttribute("data-ohw-section") ?? "", order, pagePath: currentPath });
718
+ }
719
+ });
720
+ const target = byId.get(instanceId);
721
+ if (!target) return null;
722
+ byId.set(instanceId, { ...target, removed });
723
+ const entries = Array.from(byId.values());
724
+ applyPersistedOrder(entries);
725
+ return entries;
726
+ }
727
+ function deleteSectionInstance(instanceId, currentPath, existingEntries) {
728
+ return setSectionRemoved(instanceId, currentPath, existingEntries, true);
729
+ }
730
+ function restoreSectionInstance(instanceId, currentPath, existingEntries) {
731
+ return setSectionRemoved(instanceId, currentPath, existingEntries, false);
732
+ }
733
+ function duplicateSectionInstance(instanceId, newId, currentPath, existingEntries) {
734
+ const original = findByInstanceId(instanceId);
735
+ if (!original) return null;
736
+ const clone = original.cloneNode(true);
737
+ clone.setAttribute("data-ohw-instance", newId);
738
+ const keyRekeys = rekeySectionSubtree(clone, newId);
739
+ original.insertAdjacentElement("afterend", clone);
740
+ const byId = new Map(existingEntries.map((e) => [e.instanceId, e]));
741
+ const entries = topLevelSections().map((el, order) => {
742
+ const id = instanceIdOf(el);
743
+ return {
744
+ instanceId: id,
745
+ type: el.getAttribute("data-ohw-section") ?? "",
746
+ order,
747
+ pagePath: currentPath,
748
+ ...byId.get(id)?.removed ? { removed: true } : {}
749
+ };
750
+ });
751
+ applyPersistedOrder(entries);
752
+ return { entries, keyRekeys };
753
+ }
754
+ function newInstanceId() {
755
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `instance-${Date.now()}-${Math.random().toString(36).slice(2)}`;
756
+ }
757
+ function getPageSectionOrderEntries(raw, currentPath) {
758
+ if (!raw) return [];
759
+ try {
760
+ const entries = JSON.parse(raw);
761
+ return entries.filter((e) => !e.pagePath || e.pagePath === currentPath);
762
+ } catch {
763
+ return [];
764
+ }
765
+ }
766
+ function mergePageSectionOrder(raw, currentPath, pageEntries) {
767
+ let all = [];
768
+ if (raw) {
769
+ try {
770
+ const parsed = JSON.parse(raw);
771
+ if (Array.isArray(parsed)) all = parsed;
772
+ } catch {
773
+ }
774
+ }
775
+ const otherPages = all.filter((e) => e && e.pagePath && e.pagePath !== currentPath);
776
+ const pageIds = new Set(pageEntries.map((e) => e.instanceId));
777
+ const removedHere = all.filter(
778
+ (e) => e && (!e.pagePath || e.pagePath === currentPath) && e.removed && !pageIds.has(e.instanceId)
779
+ );
780
+ return [...otherPages, ...removedHere, ...pageEntries];
781
+ }
782
+ function rekeySectionSubtree(root, instanceId) {
783
+ const suffix = `::${instanceId}`;
784
+ const pairs = [];
785
+ const rekey = (el, attr) => {
786
+ const current = el.getAttribute(attr);
787
+ if (!current) return;
788
+ const base = current.includes("::") ? current.slice(0, current.indexOf("::")) : current;
789
+ const next = `${base}${suffix}`;
790
+ el.setAttribute(attr, next);
791
+ pairs.push({ from: current, to: next });
792
+ };
793
+ if (root.hasAttribute("data-ohw-key")) rekey(root, "data-ohw-key");
794
+ if (root.hasAttribute("data-ohw-href-key")) rekey(root, "data-ohw-href-key");
795
+ root.querySelectorAll("[data-ohw-key]").forEach((el) => rekey(el, "data-ohw-key"));
796
+ root.querySelectorAll("[data-ohw-href-key]").forEach((el) => rekey(el, "data-ohw-href-key"));
797
+ return pairs;
798
+ }
799
+ function initSectionInstancesFromContent(content, currentPath) {
800
+ document.querySelectorAll("[data-ohw-section]:not([data-ohw-instance])").forEach((el) => {
801
+ el.setAttribute("data-ohw-instance", el.getAttribute("data-ohw-section") ?? "");
802
+ });
803
+ const entries = getPageSectionOrderEntries(content[SECTION_ORDER_KEY], currentPath);
804
+ for (const entry of entries) {
805
+ if (entry.instanceId === entry.type) continue;
806
+ if (document.querySelector(`[data-ohw-instance="${CSS.escape(entry.instanceId)}"]`)) continue;
807
+ const original = document.querySelector(
808
+ `[data-ohw-section="${CSS.escape(entry.type)}"][data-ohw-instance="${CSS.escape(entry.type)}"]`
809
+ );
810
+ if (!original) continue;
811
+ const clone = original.cloneNode(true);
812
+ clone.setAttribute("data-ohw-instance", entry.instanceId);
813
+ rekeySectionSubtree(clone, entry.instanceId);
814
+ original.insertAdjacentElement("afterend", clone);
815
+ }
816
+ applyPersistedOrder(entries);
817
+ }
818
+
471
819
  // src/ui/ai-tree/AiTreeRenderer.tsx
472
820
  import React from "react";
473
821
  import { ArrowLeft, ArrowRight, ChevronDown, icons as lucideIcons } from "lucide-react";
822
+
823
+ // src/lib/placeholder-imagery.ts
824
+ var U = (id) => `https://images.unsplash.com/photo-${id}?auto=format&fit=crop&w=1600&q=80`;
825
+ var GENERIC = [
826
+ U("1441986300917-64674bd600d8"),
827
+ U("1486406146926-c627a92ad1ab"),
828
+ U("1497032628192-86f99bcd76bc"),
829
+ U("1521737604893-d14cc237f11d"),
830
+ U("1522071820081-009f0129c71c"),
831
+ U("1519389950473-47ba0277781c"),
832
+ U("1460925895917-afdab827c52f"),
833
+ U("1504384308090-c894fdcc538d")
834
+ ];
835
+ var PEOPLE = [
836
+ U("1500648767791-00dcc994a43e"),
837
+ U("1494790108377-be9c29b29330"),
838
+ U("1507003211169-0a1dd7228f2d"),
839
+ U("1438761681033-6461ffad8d80"),
840
+ U("1544005313-94ddf0286df2"),
841
+ U("1472099645785-5658abf4ff4e"),
842
+ U("1519085360753-af0119f7cbe7"),
843
+ U("1534528741775-53994a69daeb")
844
+ ];
845
+ var THEMED = [
846
+ {
847
+ keywords: ["portrait", "headshot", "person", "people", "team", "staff", "avatar", "founder", "face"],
848
+ pool: PEOPLE
849
+ },
850
+ {
851
+ keywords: ["pet", "dog", "cat", "puppy", "kitten", "vet", "animal"],
852
+ pool: [
853
+ U("1548199973-03cce0bbc87b"),
854
+ U("1450778869180-41d0601e046e"),
855
+ U("1583511655857-d19b40a7a54e"),
856
+ U("1587300003388-59208cc962cb"),
857
+ U("1517849845537-4d257902454a"),
858
+ U("1601758228041-f3b2795255f1")
859
+ ]
860
+ },
861
+ {
862
+ keywords: [
863
+ "baker",
864
+ "bakery",
865
+ "cafe",
866
+ "coffee",
867
+ "latte",
868
+ "restaurant",
869
+ "pastr",
870
+ "bread",
871
+ "cake",
872
+ "cater",
873
+ "chef",
874
+ "kitchen",
875
+ "food",
876
+ "pizza",
877
+ "dessert",
878
+ "brunch",
879
+ "bistro",
880
+ "deli",
881
+ "dish",
882
+ "menu"
883
+ ],
884
+ pool: [
885
+ U("1509440159596-0249088772ff"),
886
+ U("1555507036-ab1f4038808a"),
887
+ U("1517433670267-08bbd4be890f"),
888
+ U("1486427944299-d1955d23e34d"),
889
+ U("1504754524776-8f4f37790ca0"),
890
+ U("1495474472287-4d71bcdd2085"),
891
+ U("1521017432531-fbd92d768814"),
892
+ U("1556909114-f6e7ad7d3136")
893
+ ]
894
+ },
895
+ {
896
+ keywords: [
897
+ "shop",
898
+ "store",
899
+ "boutique",
900
+ "retail",
901
+ "clothing",
902
+ "fashion",
903
+ "jewel",
904
+ "gift",
905
+ "florist",
906
+ "market",
907
+ "grocer",
908
+ "product",
909
+ "storefront"
910
+ ],
911
+ pool: [
912
+ U("1441984904996-e0b6ba687e04"),
913
+ U("1472851294608-062f824d29cc"),
914
+ U("1523381210434-271e8be1f52b"),
915
+ U("1534452203293-494d7ddbf7e0"),
916
+ U("1445205170230-053b83016050"),
917
+ U("1560243563-062bfc001d68")
918
+ ]
919
+ },
920
+ {
921
+ keywords: [
922
+ "yoga",
923
+ "pilates",
924
+ "fitness",
925
+ "gym",
926
+ "workout",
927
+ "trainer",
928
+ "wellness",
929
+ "meditat",
930
+ "massage",
931
+ "therap",
932
+ "physio",
933
+ "chiro",
934
+ "nutrition",
935
+ "spa",
936
+ "studio"
937
+ ],
938
+ pool: [
939
+ U("1544367567-0f2fcb009e0b"),
940
+ U("1506126613408-eca07ce68773"),
941
+ U("1545205597-3d9d02c29597"),
942
+ U("1552196563-55cd4e45efb3"),
943
+ U("1518611012118-696072aa579a"),
944
+ U("1571019613454-1cb2f99b2d8b"),
945
+ U("1540555700478-4be289fbecef"),
946
+ U("1519824145371-296894a0daa9")
947
+ ]
948
+ },
949
+ {
950
+ keywords: [
951
+ "salon",
952
+ "hairdress",
953
+ "haircut",
954
+ "barber",
955
+ "manicure",
956
+ "pedicure",
957
+ "nails",
958
+ "beauty",
959
+ "makeup",
960
+ "cosmetic",
961
+ "eyelash",
962
+ "eyebrow",
963
+ "skincare",
964
+ "esthetic",
965
+ "waxing",
966
+ "hair"
967
+ ],
968
+ pool: [
969
+ U("1560066984-138dadb4c035"),
970
+ U("1522337660859-02fbefca4702"),
971
+ U("1562322140-8baeececf3df"),
972
+ U("1521590832167-7bcbfaa6381f"),
973
+ U("1487412947147-5cebf100ffc2"),
974
+ U("1526045478516-99145907023c")
975
+ ]
976
+ },
977
+ {
978
+ keywords: [
979
+ "cleaning",
980
+ "plumb",
981
+ "electric",
982
+ "landscap",
983
+ "contractor",
984
+ "handyman",
985
+ "renov",
986
+ "hvac",
987
+ "roofing",
988
+ "painting",
989
+ "carpentry",
990
+ "flooring",
991
+ "movers",
992
+ "construction",
993
+ "tools"
994
+ ],
995
+ pool: [
996
+ U("1581578731548-c64695cc6952"),
997
+ U("1504307651254-35680f356dfd"),
998
+ U("1581092160562-40aa08e78837"),
999
+ U("1621905251189-08b45d6a269e"),
1000
+ U("1558618666-fcd25c85cd64"),
1001
+ U("1585128792020-803d29415281")
1002
+ ]
1003
+ },
1004
+ {
1005
+ keywords: [
1006
+ "legal",
1007
+ "attorney",
1008
+ "lawyer",
1009
+ "account",
1010
+ "bookkeep",
1011
+ "consult",
1012
+ "coaching",
1013
+ "financ",
1014
+ "insurance",
1015
+ "realtor",
1016
+ "estate",
1017
+ "marketing",
1018
+ "agency",
1019
+ "office",
1020
+ "business",
1021
+ "desk"
1022
+ ],
1023
+ pool: [
1024
+ U("1497366216548-37526070297c"),
1025
+ U("1497366811353-6870744d04b2"),
1026
+ U("1454165804606-c3d57bc86b40"),
1027
+ U("1521791136064-7986c2920216"),
1028
+ U("1556761175-b413da4baf72"),
1029
+ U("1542744173-8e7e53415bb0")
1030
+ ]
1031
+ },
1032
+ {
1033
+ keywords: [
1034
+ "wedding",
1035
+ "event",
1036
+ "party",
1037
+ "celebrat",
1038
+ "venue",
1039
+ "community",
1040
+ "nonprofit",
1041
+ "charity",
1042
+ "workshop",
1043
+ "photograph",
1044
+ "concert"
1045
+ ],
1046
+ pool: [
1047
+ U("1511578314322-379afb476865"),
1048
+ U("1501281668745-f7f57925c3b4"),
1049
+ U("1523580494863-6f3031224c94"),
1050
+ U("1540575467063-178a50c2df87"),
1051
+ U("1505236858219-8359eb29e329"),
1052
+ U("1528605248644-14dd04022da1")
1053
+ ]
1054
+ }
1055
+ ];
1056
+ function poolForSubject(subject) {
1057
+ for (const theme of THEMED) {
1058
+ if (theme.keywords.some((k) => subject.includes(k))) {
1059
+ return theme.pool;
1060
+ }
1061
+ }
1062
+ return GENERIC;
1063
+ }
1064
+ function mixedHash(text) {
1065
+ let hash = 2166136261;
1066
+ for (let i = 0; i < text.length; i++) {
1067
+ hash ^= text.charCodeAt(i);
1068
+ hash = Math.imul(hash, 16777619);
1069
+ }
1070
+ return hash >>> 16 & 65535;
1071
+ }
1072
+ function resolvePlaceholderRef(ref) {
1073
+ const match = /^placeholder:([a-z0-9-]+)$/.exec(ref);
1074
+ if (!match) return null;
1075
+ const subject = match[1];
1076
+ const pool = poolForSubject(subject.replace(/-\d+$/, ""));
1077
+ return pool[mixedHash(ref) % pool.length];
1078
+ }
1079
+ function collectPlaceholderRefs(tree) {
1080
+ const seen = /* @__PURE__ */ new Set();
1081
+ for (const match of JSON.stringify(tree ?? null).matchAll(/"(placeholder:[a-z0-9-]+)"/gu)) {
1082
+ seen.add(match[1]);
1083
+ }
1084
+ return [...seen];
1085
+ }
1086
+ function buildPlaceholderMap(tree) {
1087
+ const map = {};
1088
+ const cursor = /* @__PURE__ */ new Map();
1089
+ for (const ref of collectPlaceholderRefs(tree)) {
1090
+ const subject = ref.slice("placeholder:".length).replace(/-\d+$/, "");
1091
+ const pool = poolForSubject(subject);
1092
+ const start = cursor.get(pool) ?? mixedHash(ref) % pool.length;
1093
+ map[ref] = pool[start % pool.length];
1094
+ cursor.set(pool, start + 1);
1095
+ }
1096
+ return map;
1097
+ }
1098
+
1099
+ // src/ui/ai-tree/AiTreeRenderer.tsx
474
1100
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
475
1101
  function lucideByName(name) {
476
1102
  const pascal = name.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
@@ -484,6 +1110,7 @@ var typeStyle = (spec, font) => ({
484
1110
  fontWeight: spec.weight
485
1111
  });
486
1112
  var str = (value) => typeof value === "string" ? value : "";
1113
+ var cardRadius = (slots) => slots.cornerStyle === "sharp" ? 0 : AI_TREE_TOKENS.radiusCard;
487
1114
  var featureLines = (value) => value.split(/\n|<br\s*\/?>/i).map((line) => line.trim()).filter(Boolean);
488
1115
  var CHECK_MASK = `data:image/svg+xml,${encodeURIComponent(
489
1116
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>'
@@ -510,6 +1137,25 @@ var FEATURE_LINE_CSS = [
510
1137
  `background-color:currentColor;-webkit-mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px;`,
511
1138
  `mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px}`
512
1139
  ].join("");
1140
+ function buttonShellStyle(ctx, fullWidth) {
1141
+ const bs = ctx.buttonStyle;
1142
+ if (bs) {
1143
+ return {
1144
+ borderRadius: bs.radius,
1145
+ ...bs.padding ? { padding: bs.padding } : {},
1146
+ ...bs.fontFamily ? { fontFamily: bs.fontFamily } : { fontFamily: ctx.brand.fonts.body },
1147
+ ...bs.fontSize ? { fontSize: bs.fontSize } : {},
1148
+ ...bs.fontWeight ? { fontWeight: bs.fontWeight } : {},
1149
+ ...bs.letterSpacing && bs.letterSpacing !== "normal" ? { letterSpacing: bs.letterSpacing } : {},
1150
+ ...bs.textTransform && bs.textTransform !== "none" ? { textTransform: bs.textTransform } : {}
1151
+ };
1152
+ }
1153
+ return {
1154
+ borderRadius: AI_TREE_TOKENS.radiusButton,
1155
+ padding: fullWidth ? `${AI_TREE_TOKENS.spacing2}px ${AI_TREE_TOKENS.spacing4}px` : `${AI_TREE_TOKENS.spacing3}px ${AI_TREE_TOKENS.spacing6}px`,
1156
+ ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
1157
+ };
1158
+ }
513
1159
  function hexLuminance(color) {
514
1160
  const m = /^#([0-9a-f]{6})$/i.exec(color.trim());
515
1161
  if (!m) return null;
@@ -526,6 +1172,12 @@ function hexContrast(a, b) {
526
1172
  const [hi, lo] = la > lb ? [la, lb] : [lb, la];
527
1173
  return (hi + 0.05) / (lo + 0.05);
528
1174
  }
1175
+ function primaryButtonLabel(brand) {
1176
+ const darkC = hexContrast(brand.palette.primary, brand.palette.dark);
1177
+ const lightC = hexContrast(brand.palette.primary, AI_TREE_TOKENS.textPrimaryForeground);
1178
+ if (darkC === null || lightC === null) return AI_TREE_TOKENS.textPrimaryForeground;
1179
+ return darkC > lightC ? brand.palette.dark : AI_TREE_TOKENS.textPrimaryForeground;
1180
+ }
529
1181
  function accentBandContext(brand) {
530
1182
  const p = brand.palette;
531
1183
  const lightWins = (hexContrast(p.light, p.primary) ?? 99) >= (hexContrast(p.dark, p.primary) ?? 0);
@@ -543,6 +1195,22 @@ function accentBandContext(brand) {
543
1195
  function textAttrs(ctx, path) {
544
1196
  return ctx.keyFor ? { "data-ohw-key": ctx.keyFor(path), "data-ohw-editable": "text" } : {};
545
1197
  }
1198
+ var AI_RESPONSIVE_CSS = [
1199
+ "@media (max-width: 960px) {",
1200
+ " [data-ai-responsive] [data-ai-grid] { grid-template-columns: repeat(2, 1fr) !important; }",
1201
+ ' [data-ai-responsive] [data-ai-grid="1"] { grid-template-columns: 1fr !important; }',
1202
+ "}",
1203
+ "@media (max-width: 640px) {",
1204
+ " [data-ai-responsive] [data-ai-grid] { grid-template-columns: 1fr !important; }",
1205
+ " [data-ai-responsive] [data-ai-columns] > * { grid-column: 1 / -1 !important; }",
1206
+ // Group containers flatten to a column on phones; span placements come along for free.
1207
+ " [data-ai-responsive] [data-ai-group] { display: flex !important; flex-direction: column !important; }",
1208
+ " [data-ai-responsive] [data-ai-group] > * { grid-column: auto !important; }",
1209
+ " [data-ai-responsive] [data-ai-section-inner] { padding-left: 20px !important; padding-right: 20px !important; }",
1210
+ " [data-ai-responsive] { overflow-x: hidden; }",
1211
+ " [data-ai-responsive] img { max-width: 100%; }",
1212
+ "}"
1213
+ ].join("\n");
546
1214
  var TRANSPARENT_PX = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
547
1215
  function MediaBox({
548
1216
  refValue,
@@ -555,13 +1223,17 @@ function MediaBox({
555
1223
  const url = refValue ? ctx.resolveMedia(refValue) : null;
556
1224
  const isIcon = /^(lucide|simple):/.test(refValue);
557
1225
  const aspectRatio = aspect && /^\d+:\d+$/.test(aspect) ? aspect.replace(":", " / ") : void 0;
558
- const editAttrs = ctx.keyFor && editPath && !isIcon ? { "data-ohw-key": ctx.keyFor(editPath), "data-ohw-editable": "image" } : {};
1226
+ const editAttrs = ctx.keyFor && editPath ? {
1227
+ "data-ohw-key": ctx.keyFor(editPath),
1228
+ "data-ohw-editable": isIcon ? "icon" : "image"
1229
+ } : {};
559
1230
  if (isIcon) {
560
1231
  const Icon = refValue.startsWith("lucide:") ? lucideByName(refValue.slice(7)) : null;
561
1232
  return /* @__PURE__ */ jsx(
562
1233
  "span",
563
1234
  {
564
1235
  "data-ai-icon": refValue,
1236
+ ...editAttrs,
565
1237
  style: {
566
1238
  display: "inline-flex",
567
1239
  width: 48,
@@ -625,12 +1297,10 @@ function ButtonEl({
625
1297
  width: fullWidth ? "100%" : void 0,
626
1298
  alignItems: "center",
627
1299
  justifyContent: "center",
628
- padding: fullWidth ? `${AI_TREE_TOKENS.spacing2}px ${AI_TREE_TOKENS.spacing4}px` : `${AI_TREE_TOKENS.spacing3}px ${AI_TREE_TOKENS.spacing6}px`,
629
- borderRadius: AI_TREE_TOKENS.radiusButton,
630
1300
  textDecoration: "none",
631
1301
  cursor: "pointer",
632
- ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body),
633
- ...secondary ? { border: `1px solid ${ctx.brand.palette.dark}`, color: ctx.brand.palette.dark } : { background: ctx.brand.palette.primary, color: ctx.buttonLabel ?? AI_TREE_TOKENS.textPrimaryForeground }
1302
+ ...buttonShellStyle(ctx, fullWidth),
1303
+ ...secondary ? { border: `1px solid ${ctx.brand.palette.dark}`, color: ctx.brand.palette.dark } : { background: ctx.brand.palette.primary, color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand) }
634
1304
  },
635
1305
  children: /* @__PURE__ */ jsx("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
636
1306
  }
@@ -656,7 +1326,7 @@ function TextBlock({ slots, ctx, path }) {
656
1326
  }
657
1327
  function SectionHeaderBlock({ node, ctx, path }) {
658
1328
  const slots = node.slots ?? {};
659
- const align = slots.alignment === "center" ? "center" : "left";
1329
+ const align = node.align ?? (slots.alignment === "center" ? "center" : "left");
660
1330
  const children = node.children ?? [];
661
1331
  const buttonRowIdx = children.findIndex((c) => c.type === "button-row");
662
1332
  const buttonRow = buttonRowIdx >= 0 ? children[buttonRowIdx] : void 0;
@@ -700,7 +1370,7 @@ function SectionHeaderBlock({ node, ctx, path }) {
700
1370
  display: "flex",
701
1371
  gap: AI_TREE_TOKENS.spacing6,
702
1372
  marginTop: AI_TREE_TOKENS.spacing8,
703
- justifyContent: align === "center" ? "center" : "flex-start"
1373
+ justifyContent: align === "center" ? "center" : align === "right" ? "flex-end" : "flex-start"
704
1374
  },
705
1375
  children: (buttonRow.children ?? []).map((button, i) => /* @__PURE__ */ jsx(
706
1376
  ButtonEl,
@@ -806,10 +1476,11 @@ function PricingCard({ node, ctx, path }) {
806
1476
  return /* @__PURE__ */ jsxs(
807
1477
  "div",
808
1478
  {
1479
+ "data-ohw-card": "",
809
1480
  style: {
810
1481
  background: hasBg ? ctx.brand.palette.light : "transparent",
811
1482
  border: `1px solid ${dark}`,
812
- borderRadius: AI_TREE_TOKENS.radiusCard,
1483
+ borderRadius: cardRadius(slots),
813
1484
  padding: AI_TREE_TOKENS.paddingBlock,
814
1485
  display: "flex",
815
1486
  flexDirection: "column",
@@ -914,10 +1585,11 @@ function TestimonialCard({ node, ctx, path }) {
914
1585
  return /* @__PURE__ */ jsx(
915
1586
  "div",
916
1587
  {
1588
+ "data-ohw-card": "",
917
1589
  "data-ai-avatar-pos": avatarPos ?? void 0,
918
1590
  style: {
919
1591
  background: hasBg ? ctx.cardSurface : "transparent",
920
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1592
+ borderRadius: hasBg ? cardRadius(slots) : 0,
921
1593
  overflow: "hidden",
922
1594
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
923
1595
  minWidth: 0
@@ -952,10 +1624,11 @@ function TeamCard({ node, ctx, path }) {
952
1624
  return /* @__PURE__ */ jsxs(
953
1625
  "div",
954
1626
  {
1627
+ "data-ohw-card": "",
955
1628
  "data-ai-avatar-pos": avatarPos ?? void 0,
956
1629
  style: {
957
1630
  background: hasBg ? ctx.cardSurface : "transparent",
958
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1631
+ borderRadius: hasBg ? cardRadius(slots) : 0,
959
1632
  overflow: "hidden",
960
1633
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
961
1634
  minWidth: 0,
@@ -1033,7 +1706,7 @@ function CardBlock({ node, ctx, path }) {
1033
1706
  editPath: `${path}.media`
1034
1707
  }
1035
1708
  ) : null;
1036
- const centered = slots.alignment === "center";
1709
+ const centered = (node.align ?? slots.alignment) === "center";
1037
1710
  const content = /* @__PURE__ */ jsxs(
1038
1711
  "div",
1039
1712
  {
@@ -1126,9 +1799,10 @@ function CardBlock({ node, ctx, path }) {
1126
1799
  return /* @__PURE__ */ jsxs(
1127
1800
  "div",
1128
1801
  {
1802
+ "data-ohw-card": "",
1129
1803
  style: {
1130
1804
  background: hasBg ? ctx.cardSurface : "transparent",
1131
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1805
+ borderRadius: hasBg ? cardRadius(slots) : 0,
1132
1806
  overflow: "hidden",
1133
1807
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
1134
1808
  display: horizontal ? "flex" : "block",
@@ -1156,7 +1830,7 @@ function CardBlock({ node, ctx, path }) {
1156
1830
  ) : /* @__PURE__ */ jsx(
1157
1831
  "div",
1158
1832
  {
1159
- style: /^(lucide|simple):/.test(mediaRef) ? { padding: `${AI_TREE_TOKENS.paddingBlock}px ${AI_TREE_TOKENS.paddingBlock}px 0` } : hasBg ? void 0 : { borderRadius: AI_TREE_TOKENS.radiusCard, overflow: "hidden" },
1833
+ style: /^(lucide|simple):/.test(mediaRef) ? { padding: `${AI_TREE_TOKENS.paddingBlock}px ${AI_TREE_TOKENS.paddingBlock}px 0` } : hasBg ? void 0 : { borderRadius: cardRadius(slots), overflow: "hidden" },
1160
1834
  children: media
1161
1835
  }
1162
1836
  )),
@@ -1447,7 +2121,7 @@ function CollectionBlock({ node, ctx, path }) {
1447
2121
  return /* @__PURE__ */ jsx(
1448
2122
  "div",
1449
2123
  {
1450
- "data-ai-grid": "",
2124
+ "data-ai-grid": String(itemsPerRow),
1451
2125
  style: {
1452
2126
  display: "grid",
1453
2127
  gridTemplateColumns: `repeat(${itemsPerRow}, 1fr)`,
@@ -1567,6 +2241,32 @@ function renderNode(node, ctx, path) {
1567
2241
  if (child) {
1568
2242
  return renderNode(child, ctx, `${path}.c0`);
1569
2243
  }
2244
+ if (str(slots.provider) === "map" && str(slots.query)) {
2245
+ const query = str(slots.query);
2246
+ const mapAttrs = ctx.keyFor ? {
2247
+ "data-ohw-key": ctx.keyFor(`${path}.query`),
2248
+ "data-ohw-editable": "map",
2249
+ "data-ohw-map-query": query
2250
+ } : {};
2251
+ return /* @__PURE__ */ jsx(
2252
+ "iframe",
2253
+ {
2254
+ ...mapAttrs,
2255
+ "data-ai-embed": "map",
2256
+ title: str(slots.title) || "Map",
2257
+ src: `https://www.google.com/maps?q=${encodeURIComponent(query)}&output=embed`,
2258
+ loading: "lazy",
2259
+ referrerPolicy: "no-referrer-when-downgrade",
2260
+ style: {
2261
+ width: "100%",
2262
+ minHeight: 320,
2263
+ border: 0,
2264
+ borderRadius: AI_TREE_TOKENS.radiusCard,
2265
+ display: "block"
2266
+ }
2267
+ }
2268
+ );
2269
+ }
1570
2270
  return /* @__PURE__ */ jsx(
1571
2271
  "div",
1572
2272
  {
@@ -1670,15 +2370,12 @@ function renderNode(node, ctx, path) {
1670
2370
  alignSelf: submitAlign,
1671
2371
  border: "none",
1672
2372
  cursor: "pointer",
1673
- padding: "12px 24px",
1674
- // Corner radius follows the host template's own buttons (measured from a template
1675
- // CTA); 8px only when the page has no template button to match.
1676
- borderRadius: ctx.buttonRadius ?? 8,
2373
+ // Shape/padding/typography follow the host template's own buttons.
2374
+ ...buttonShellStyle(ctx),
1677
2375
  // Brand-styled: primary fill, brand-derived label colour (not a fixed token) so it
1678
2376
  // reads correctly on custom palettes.
1679
2377
  background: ctx.brand.palette.primary,
1680
- color: ctx.buttonLabel ?? ctx.brand.palette.light,
1681
- ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
2378
+ color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand)
1682
2379
  },
1683
2380
  children: /* @__PURE__ */ jsx("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
1684
2381
  },
@@ -1713,7 +2410,7 @@ function renderNode(node, ctx, path) {
1713
2410
  function AiTreeRenderer({
1714
2411
  tree,
1715
2412
  brand,
1716
- buttonRadius,
2413
+ buttonStyle,
1717
2414
  resolveMedia,
1718
2415
  editKeyPrefix
1719
2416
  }) {
@@ -1723,13 +2420,18 @@ function AiTreeRenderer({
1723
2420
  const resolvedBrand = brand ?? AI_DEFAULT_BRAND;
1724
2421
  const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
1725
2422
  const blockBrand = band?.brand ?? resolvedBrand;
2423
+ const placeholderMap = buildPlaceholderMap(tree);
1726
2424
  const ctx = {
1727
2425
  brand: blockBrand,
1728
- resolveMedia: resolveMedia ?? (() => null),
2426
+ // An owner/library ref resolves through the host resolver; a `placeholder:<subject>` ref the
2427
+ // host cannot resolve falls back to real stock photography (the per-section map first, then a
2428
+ // standalone resolve), so generated galleries, image rows, and overlay backgrounds arrive with
2429
+ // photos instead of grey boxes.
2430
+ resolveMedia: (ref) => resolveMedia?.(ref) ?? placeholderMap[ref] ?? resolvePlaceholderRef(ref),
1729
2431
  cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
1730
2432
  keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
1731
2433
  sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
1732
- buttonRadius,
2434
+ buttonStyle,
1733
2435
  ...band ? { buttonLabel: band.buttonLabel } : {}
1734
2436
  };
1735
2437
  const settings = tree.settings ?? {};
@@ -1752,11 +2454,25 @@ function AiTreeRenderer({
1752
2454
  }
1753
2455
  })();
1754
2456
  const distributed = !isOverlay && settings.textDistribution;
2457
+ const rowAlignItems = (rowAlign) => {
2458
+ if (rowAlign === "top") return "start";
2459
+ if (rowAlign === "bottom") return "end";
2460
+ if (rowAlign === "center" || rowAlign === "stretch") return rowAlign;
2461
+ if (distributed === "space-between") return "stretch";
2462
+ return (distributed ?? settings.verticalPosition) === "top" ? "start" : "center";
2463
+ };
2464
+ const cellAlignStyle = (blockAlign) => blockAlign ? {
2465
+ display: "flex",
2466
+ flexDirection: "column",
2467
+ alignItems: blockAlign === "left" ? "flex-start" : blockAlign === "right" ? "flex-end" : "center",
2468
+ textAlign: blockAlign
2469
+ } : {};
1755
2470
  return /* @__PURE__ */ jsxs(
1756
2471
  "section",
1757
2472
  {
1758
2473
  "data-ai-section": tree.tag ?? "",
1759
2474
  ...bgAttrs,
2475
+ "data-ai-responsive": "",
1760
2476
  style: {
1761
2477
  position: "relative",
1762
2478
  padding: `${pad}px 0`,
@@ -1767,12 +2483,13 @@ function AiTreeRenderer({
1767
2483
  color: settings.sectionBackground === "accent" ? blockBrand.palette.dark : void 0
1768
2484
  },
1769
2485
  children: [
1770
- isOverlay && backgroundUrl && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
2486
+ /* @__PURE__ */ jsx("style", { children: AI_RESPONSIVE_CSS }),
1771
2487
  /* @__PURE__ */ jsx("style", { children: AI_MOBILE_CSS }),
2488
+ isOverlay && backgroundUrl && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
1772
2489
  /* @__PURE__ */ jsx(
1773
2490
  "div",
1774
2491
  {
1775
- "data-ai-container": "",
2492
+ "data-ai-section-inner": "",
1776
2493
  style: {
1777
2494
  position: "relative",
1778
2495
  maxWidth: AI_TREE_TOKENS.sectionMaxWidth,
@@ -1783,12 +2500,12 @@ function AiTreeRenderer({
1783
2500
  children: tree.rows.map((row, r2) => /* @__PURE__ */ jsx(
1784
2501
  "div",
1785
2502
  {
1786
- "data-ai-row": "",
2503
+ "data-ai-columns": "",
1787
2504
  style: {
1788
2505
  display: "grid",
1789
2506
  gridTemplateColumns: "repeat(12, 1fr)",
1790
2507
  gap: AI_TREE_TOKENS.spacing6,
1791
- alignItems: distributed === "space-between" ? "stretch" : (distributed ?? settings.verticalPosition) === "top" ? "start" : "center",
2508
+ alignItems: rowAlignItems(row.align),
1792
2509
  marginTop: r2 > 0 ? AI_TREE_TOKENS.spacing8 : 0
1793
2510
  },
1794
2511
  children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ jsx(
@@ -1798,6 +2515,8 @@ function AiTreeRenderer({
1798
2515
  style: {
1799
2516
  gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`,
1800
2517
  minWidth: 0,
2518
+ // Horizontal placement of the block's content within its column.
2519
+ ...cellAlignStyle(block.align),
1801
2520
  // space-between: each column becomes a flex column whose content spreads over
1802
2521
  // the full row height instead of clumping at the top.
1803
2522
  ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
@@ -1820,7 +2539,7 @@ function AiTreeRenderer({
1820
2539
  import { jsx as jsx2 } from "react/jsx-runtime";
1821
2540
  var CONTAINER_ATTR = "data-ohw-ai-generated";
1822
2541
  var REPLACED_ATTR = "data-ohw-ai-replaced-by";
1823
- var REMOVED_ATTR = "data-ohw-ai-removed";
2542
+ var REMOVED_ATTR2 = "data-ohw-ai-removed";
1824
2543
  var TEMPLATE_HIDDEN_ATTR = "data-ohw-ai-template-hidden";
1825
2544
  var CHROME_SECTION_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
1826
2545
  function readRootVar(name) {
@@ -1844,13 +2563,13 @@ function deriveBrandOverride() {
1844
2563
  };
1845
2564
  }
1846
2565
  function deriveTemplateBrand() {
1847
- const dark = readRootVar("--color-dark");
1848
- const primary = readRootVar("--color-primary");
1849
- const light = readRootVar("--color-light");
2566
+ const primary = readRootVar("--brand-primary") || readRootVar("--color-primary");
2567
+ const dark = readRootVar("--brand-text") || readRootVar("--color-dark");
2568
+ const light = readRootVar("--brand-background") || readRootVar("--color-light");
1850
2569
  if (!dark || !primary || !light) return null;
1851
- const accent = readRootVar("--color-accent");
1852
- const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1853
- const body = readRootVar("--font-body");
2570
+ const accent = readRootVar("--brand-accent") || readRootVar("--color-accent");
2571
+ const heading = readRootVar("--brand-font-heading") || readRootVar("--font-heading") || readRootVar("--font-display");
2572
+ const body = readRootVar("--brand-font-body") || readRootVar("--font-body");
1854
2573
  return {
1855
2574
  palette: { dark, primary, accent: accent || dark, light },
1856
2575
  fonts: {
@@ -1859,12 +2578,32 @@ function deriveTemplateBrand() {
1859
2578
  }
1860
2579
  };
1861
2580
  }
1862
- function deriveTemplateButtonRadius() {
2581
+ function deriveTemplateButtonStyle() {
1863
2582
  if (typeof document === "undefined") return null;
1864
- const btn = document.querySelector('[data-ohw-role="button"]');
2583
+ const btn = Array.from(document.querySelectorAll('[data-ohw-role="button"]')).find(
2584
+ (el) => !el.closest(`[${CONTAINER_ATTR}]`)
2585
+ );
1865
2586
  if (!btn) return null;
1866
- const radius = getComputedStyle(btn).borderTopLeftRadius;
1867
- return radius || null;
2587
+ const cs = getComputedStyle(btn);
2588
+ const corners = [
2589
+ cs.borderTopLeftRadius,
2590
+ cs.borderTopRightRadius,
2591
+ cs.borderBottomRightRadius,
2592
+ cs.borderBottomLeftRadius
2593
+ ].map((v) => v || "0px");
2594
+ const radius = corners.every((v) => v === corners[0]) ? corners[0] : corners.join(" ");
2595
+ const px = (v) => parseFloat(v) || 0;
2596
+ const padY = Math.max(px(cs.paddingTop), px(cs.paddingBottom));
2597
+ const padX = Math.max(px(cs.paddingLeft), px(cs.paddingRight));
2598
+ return {
2599
+ radius: radius || "10px",
2600
+ padding: `${padY}px ${padX}px`,
2601
+ fontFamily: cs.fontFamily || "",
2602
+ fontSize: cs.fontSize || "",
2603
+ fontWeight: cs.fontWeight || "",
2604
+ letterSpacing: cs.letterSpacing || "",
2605
+ textTransform: cs.textTransform || ""
2606
+ };
1868
2607
  }
1869
2608
  var mounted = /* @__PURE__ */ new Map();
1870
2609
  function findTemplateSection(id) {
@@ -1914,18 +2653,18 @@ function placeContainer(container, entry) {
1914
2653
  }
1915
2654
  function syncRemovedSections(state) {
1916
2655
  const removed = new Set(state.removed ?? []);
1917
- for (const el of document.querySelectorAll(`[${REMOVED_ATTR}]`)) {
2656
+ for (const el of document.querySelectorAll(`[${REMOVED_ATTR2}]`)) {
1918
2657
  const id = el.getAttribute("data-ohw-section") ?? "";
1919
2658
  if (!removed.has(id)) {
1920
2659
  el.style.removeProperty("display");
1921
- el.removeAttribute(REMOVED_ATTR);
2660
+ el.removeAttribute(REMOVED_ATTR2);
1922
2661
  }
1923
2662
  }
1924
2663
  for (const id of removed) {
1925
2664
  const section = findTemplateSection(id);
1926
2665
  if (section && !section.hasAttribute(REPLACED_ATTR)) {
1927
2666
  section.style.display = "none";
1928
- section.setAttribute(REMOVED_ATTR, "");
2667
+ section.setAttribute(REMOVED_ATTR2, "");
1929
2668
  }
1930
2669
  }
1931
2670
  }
@@ -1942,7 +2681,7 @@ function syncTemplateHidden(state, pageHasSections) {
1942
2681
  if (el.hasAttribute(CONTAINER_ATTR)) continue;
1943
2682
  if (CHROME_SECTION_IDS.has(el.getAttribute("data-ohw-section") ?? "")) continue;
1944
2683
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
1945
- if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR)) continue;
2684
+ if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR2)) continue;
1946
2685
  el.style.display = "none";
1947
2686
  el.setAttribute(TEMPLATE_HIDDEN_ATTR, "");
1948
2687
  }
@@ -1966,18 +2705,23 @@ function syncReplacedOriginals(state) {
1966
2705
  }
1967
2706
  }
1968
2707
  var sectionOrderIndex = /* @__PURE__ */ new Map();
2708
+ var removedSectionIds = /* @__PURE__ */ new Set();
1969
2709
  function setAiSectionOrder(raw, currentPath) {
1970
2710
  const next = /* @__PURE__ */ new Map();
2711
+ const removed = /* @__PURE__ */ new Set();
1971
2712
  if (raw) {
1972
2713
  try {
1973
2714
  const entries = JSON.parse(raw);
1974
2715
  for (const entry of entries) {
1975
- if (!entry.pagePath || entry.pagePath === currentPath) next.set(entry.instanceId, entry.order);
2716
+ if (entry.pagePath && entry.pagePath !== currentPath) continue;
2717
+ next.set(entry.instanceId, entry.order);
2718
+ if (entry.removed) removed.add(entry.instanceId);
1976
2719
  }
1977
2720
  } catch {
1978
2721
  }
1979
2722
  }
1980
2723
  sectionOrderIndex = next;
2724
+ removedSectionIds = removed;
1981
2725
  }
1982
2726
  function applyExplicitOrder(entries) {
1983
2727
  if (sectionOrderIndex.size === 0) return entries;
@@ -2013,11 +2757,23 @@ function orderByChain(sections) {
2013
2757
  for (const root of roots) visit(root);
2014
2758
  return out.length === sections.length ? out : sections;
2015
2759
  }
2760
+ function syncSoftRemovedGenerated() {
2761
+ for (const [id, section] of mounted) {
2762
+ const el = section.container;
2763
+ if (removedSectionIds.has(id)) {
2764
+ el.style.display = "none";
2765
+ el.setAttribute(REMOVED_ATTR, "");
2766
+ } else if (el.hasAttribute(REMOVED_ATTR)) {
2767
+ el.style.removeProperty("display");
2768
+ el.removeAttribute(REMOVED_ATTR);
2769
+ }
2770
+ }
2771
+ }
2016
2772
  function applyAiSectionsToDom(state, options) {
2017
2773
  if (typeof document === "undefined") return;
2018
2774
  const brandOverride = deriveBrandOverride();
2019
2775
  const templateBrand = deriveTemplateBrand();
2020
- const templateButtonRadius = deriveTemplateButtonRadius();
2776
+ const templateButtonStyle = deriveTemplateButtonStyle();
2021
2777
  const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
2022
2778
  const pagePath = window.location.pathname;
2023
2779
  const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
@@ -2057,7 +2813,7 @@ function applyAiSectionsToDom(state, options) {
2057
2813
  {
2058
2814
  tree: entry.tree,
2059
2815
  brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2060
- buttonRadius: templateButtonRadius,
2816
+ buttonStyle: templateButtonStyle,
2061
2817
  resolveMedia,
2062
2818
  editKeyPrefix: `ai.${entry.id}`
2063
2819
  }
@@ -2080,6 +2836,7 @@ function applyAiSectionsToDom(state, options) {
2080
2836
  syncReplacedOriginals(state);
2081
2837
  syncRemovedSections(state);
2082
2838
  syncTemplateHidden(state, pageSections.length > 0);
2839
+ syncSoftRemovedGenerated();
2083
2840
  }
2084
2841
 
2085
2842
  // src/useLinkHrefGuardian.ts
@@ -7809,6 +8566,7 @@ function MediaOverlay({
7809
8566
  (prev) => prev && prev.fullWidth === width && prev.height === height ? prev : { fullWidth: width, height }
7810
8567
  );
7811
8568
  }, [isVideo]);
8569
+ const replaceLabel = isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image";
7812
8570
  const replaceMode = buttonSize ? replaceButtonMode({ width: rect.width, height: rect.height }, buttonSize) : "none";
7813
8571
  const box = {
7814
8572
  position: "fixed",
@@ -7938,17 +8696,17 @@ function MediaOverlay({
7938
8696
  },
7939
8697
  children: [
7940
8698
  isVideo ? /* @__PURE__ */ jsx15(Film, { size: 14 }) : /* @__PURE__ */ jsx15(ImageIcon, { size: 14 }),
7941
- isVideo ? "Replace video" : "Replace image"
8699
+ replaceLabel
7942
8700
  ]
7943
8701
  }
7944
8702
  ),
7945
- replaceMode === "none" ? null : /* @__PURE__ */ jsxs7(
8703
+ showChrome && replaceMode !== "none" && /* @__PURE__ */ jsxs7(
7946
8704
  Button,
7947
8705
  {
7948
8706
  "data-ohw-media-overlay": "",
7949
8707
  variant: "outline",
7950
8708
  size: "sm",
7951
- "aria-label": isVideo ? "Replace video" : "Replace image",
8709
+ "aria-label": replaceLabel,
7952
8710
  className: "gap-1.5 cursor-pointer hover:bg-background",
7953
8711
  style: {
7954
8712
  ...OVERLAY_BUTTON_STYLE,
@@ -7971,7 +8729,7 @@ function MediaOverlay({
7971
8729
  },
7972
8730
  children: [
7973
8731
  isVideo ? /* @__PURE__ */ jsx15(Film, { size: 14 }) : /* @__PURE__ */ jsx15(ImageIcon, { size: 14 }),
7974
- replaceMode === "full" ? isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image" : null
8732
+ replaceMode === "full" ? replaceLabel : null
7975
8733
  ]
7976
8734
  }
7977
8735
  )
@@ -8012,215 +8770,33 @@ function CarouselOverlay({
8012
8770
  boxShadow: "inset 0 0 0 1.5px var(--color-primary)",
8013
8771
  background: "color-mix(in srgb, var(--color-primary) 20%, transparent)"
8014
8772
  },
8015
- onClick: () => onEdit(hover.key),
8016
- children: /* @__PURE__ */ jsxs8(
8017
- Button,
8018
- {
8019
- "data-ohw-carousel-overlay": "",
8020
- variant: "outline",
8021
- size: "sm",
8022
- className: "cursor-pointer gap-1.5 hover:bg-background",
8023
- style: OVERLAY_BUTTON_STYLE2,
8024
- onMouseDown: (e) => e.preventDefault(),
8025
- onClick: (e) => {
8026
- e.stopPropagation();
8027
- onEdit(hover.key);
8028
- },
8029
- children: [
8030
- /* @__PURE__ */ jsx16(GalleryHorizontal, { size: 14 }),
8031
- "Edit gallery"
8032
- ]
8033
- }
8034
- )
8035
- }
8036
- );
8037
- }
8038
-
8039
- // src/ui/ai-section/AiSectionOverlay.tsx
8040
- import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
8041
- import { Check, X } from "lucide-react";
8042
-
8043
- // src/lib/sections.ts
8044
- var LINK_PICKER_EXCLUDED_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
8045
- function isChromeSection(el) {
8046
- return el.matches("header, nav, footer, aside");
8047
- }
8048
- function titleCaseSectionId(id) {
8049
- return id.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
8050
- }
8051
- function parseSectionsFromRoot(root) {
8052
- const seen = /* @__PURE__ */ new Set();
8053
- const sections = [];
8054
- for (const el of root.querySelectorAll("[data-ohw-section]")) {
8055
- const id = el.getAttribute("data-ohw-section") ?? "";
8056
- if (!id || seen.has(id) || LINK_PICKER_EXCLUDED_IDS.has(id)) continue;
8057
- if (el.parentElement?.closest("[data-ohw-section]")) continue;
8058
- if (el.hasAttribute("data-ohw-ai-template-hidden") || el.hasAttribute("data-ohw-ai-removed") || el.hasAttribute("data-ohw-ai-replaced-by"))
8059
- continue;
8060
- seen.add(id);
8061
- const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
8062
- sections.push({ id, label });
8063
- }
8064
- return sections;
8065
- }
8066
- function collectSectionsFromDom() {
8067
- if (typeof document === "undefined") return [];
8068
- return parseSectionsFromRoot(document);
8069
- }
8070
- function parseSectionsFromHtml(html) {
8071
- const doc = new DOMParser().parseFromString(html, "text/html");
8072
- return parseSectionsFromRoot(doc);
8073
- }
8074
-
8075
- // src/lib/section-instances.ts
8076
- var SECTION_ORDER_KEY = "__ohw_section_order";
8077
- var REMOVED_ATTR2 = "data-ohw-section-removed";
8078
- function isRemovedSection(el) {
8079
- return el.hasAttribute(REMOVED_ATTR2);
8080
- }
8081
- function topLevelSections() {
8082
- return Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
8083
- (el) => !el.parentElement?.closest("[data-ohw-section]") && !isChromeSection(el) && !isRemovedSection(el)
8084
- );
8085
- }
8086
- function instanceIdOf(el) {
8087
- return el.dataset.ohwInstance ?? el.dataset.ohwSection ?? "";
8088
- }
8089
- function findByInstanceId(instanceId) {
8090
- const escapedId = CSS.escape(instanceId);
8091
- return document.querySelector(`[data-ohw-instance="${escapedId}"]`) ?? document.querySelector(`[data-ohw-section="${escapedId}"]:not([data-ohw-instance])`);
8092
- }
8093
- function planSectionMove(instanceId, targetIndex, currentPath) {
8094
- const sections = topLevelSections();
8095
- const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
8096
- if (index === -1) return null;
8097
- const dragged = sections[index];
8098
- const others = sections.filter((_, i) => i !== index);
8099
- const clamped = Math.max(0, Math.min(targetIndex, others.length));
8100
- const reordered = [...others.slice(0, clamped), dragged, ...others.slice(clamped)];
8101
- return reordered.map((el, order) => ({
8102
- instanceId: instanceIdOf(el),
8103
- type: el.getAttribute("data-ohw-section") ?? "",
8104
- order,
8105
- pagePath: currentPath
8106
- }));
8107
- }
8108
- function moveSectionInstance(instanceId, direction, currentPath) {
8109
- const sections = topLevelSections();
8110
- const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
8111
- if (index === -1) return null;
8112
- const siblingIndex = direction === "up" ? index - 1 : index + 1;
8113
- if (siblingIndex < 0 || siblingIndex >= sections.length) return null;
8114
- const entries = planSectionMove(instanceId, siblingIndex, currentPath);
8115
- if (!entries) return null;
8116
- applyPersistedOrder(entries);
8117
- return entries;
8118
- }
8119
- function syncRemovedFlags(entries) {
8120
- const removedIds = new Set(entries.filter((e) => e.removed).map((e) => e.instanceId));
8121
- document.querySelectorAll(`[${REMOVED_ATTR2}]`).forEach((el) => {
8122
- if (!removedIds.has(instanceIdOf(el))) {
8123
- el.style.removeProperty("display");
8124
- el.removeAttribute(REMOVED_ATTR2);
8125
- }
8126
- });
8127
- for (const id of removedIds) {
8128
- const el = findByInstanceId(id);
8129
- if (el) {
8130
- el.style.display = "none";
8131
- el.setAttribute(REMOVED_ATTR2, "");
8773
+ onClick: () => onEdit(hover.key),
8774
+ children: /* @__PURE__ */ jsxs8(
8775
+ Button,
8776
+ {
8777
+ "data-ohw-carousel-overlay": "",
8778
+ variant: "outline",
8779
+ size: "sm",
8780
+ className: "cursor-pointer gap-1.5 hover:bg-background",
8781
+ style: OVERLAY_BUTTON_STYLE2,
8782
+ onMouseDown: (e) => e.preventDefault(),
8783
+ onClick: (e) => {
8784
+ e.stopPropagation();
8785
+ onEdit(hover.key);
8786
+ },
8787
+ children: [
8788
+ /* @__PURE__ */ jsx16(GalleryHorizontal, { size: 14 }),
8789
+ "Edit gallery"
8790
+ ]
8791
+ }
8792
+ )
8132
8793
  }
8133
- }
8134
- }
8135
- function applyPersistedOrder(entries) {
8136
- syncRemovedFlags(entries);
8137
- if (entries.length === 0) return;
8138
- const sections = topLevelSections();
8139
- if (sections.length === 0) return;
8140
- const orderIndex = new Map(entries.map((e) => [e.instanceId, e.order]));
8141
- const ordered = [...sections].sort((a, b) => {
8142
- const aOrder = orderIndex.get(instanceIdOf(a));
8143
- const bOrder = orderIndex.get(instanceIdOf(b));
8144
- if (aOrder === void 0 && bOrder === void 0) return 0;
8145
- if (aOrder === void 0) return 1;
8146
- if (bOrder === void 0) return -1;
8147
- return aOrder - bOrder;
8148
- });
8149
- let prev = null;
8150
- for (const el of ordered) {
8151
- if (prev) prev.after(el);
8152
- prev = el;
8153
- }
8154
- }
8155
- function setSectionRemoved(instanceId, currentPath, existingEntries, removed) {
8156
- if (!findByInstanceId(instanceId)) return null;
8157
- const byId = new Map(existingEntries.map((e) => [e.instanceId, e]));
8158
- const allSections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
8159
- (el) => !el.parentElement?.closest("[data-ohw-section]") && !isChromeSection(el)
8160
8794
  );
8161
- allSections.forEach((el, order) => {
8162
- const id = instanceIdOf(el);
8163
- if (!byId.has(id)) {
8164
- byId.set(id, { instanceId: id, type: el.getAttribute("data-ohw-section") ?? "", order, pagePath: currentPath });
8165
- }
8166
- });
8167
- const target = byId.get(instanceId);
8168
- if (!target) return null;
8169
- byId.set(instanceId, { ...target, removed });
8170
- const entries = Array.from(byId.values());
8171
- applyPersistedOrder(entries);
8172
- return entries;
8173
- }
8174
- function deleteSectionInstance(instanceId, currentPath, existingEntries) {
8175
- return setSectionRemoved(instanceId, currentPath, existingEntries, true);
8176
- }
8177
- function restoreSectionInstance(instanceId, currentPath, existingEntries) {
8178
- return setSectionRemoved(instanceId, currentPath, existingEntries, false);
8179
- }
8180
- function newInstanceId() {
8181
- return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `instance-${Date.now()}-${Math.random().toString(36).slice(2)}`;
8182
- }
8183
- function getPageSectionOrderEntries(raw, currentPath) {
8184
- if (!raw) return [];
8185
- try {
8186
- const entries = JSON.parse(raw);
8187
- return entries.filter((e) => !e.pagePath || e.pagePath === currentPath);
8188
- } catch {
8189
- return [];
8190
- }
8191
- }
8192
- function rekeySectionSubtree(root, instanceId) {
8193
- const suffix = `::${instanceId}`;
8194
- const rekey = (el, attr) => {
8195
- const current = el.getAttribute(attr);
8196
- if (current) el.setAttribute(attr, `${current}${suffix}`);
8197
- };
8198
- if (root.hasAttribute("data-ohw-key")) rekey(root, "data-ohw-key");
8199
- if (root.hasAttribute("data-ohw-href-key")) rekey(root, "data-ohw-href-key");
8200
- root.querySelectorAll("[data-ohw-key]").forEach((el) => rekey(el, "data-ohw-key"));
8201
- root.querySelectorAll("[data-ohw-href-key]").forEach((el) => rekey(el, "data-ohw-href-key"));
8202
- }
8203
- function initSectionInstancesFromContent(content, currentPath) {
8204
- document.querySelectorAll("[data-ohw-section]:not([data-ohw-instance])").forEach((el) => {
8205
- el.setAttribute("data-ohw-instance", el.getAttribute("data-ohw-section") ?? "");
8206
- });
8207
- const entries = getPageSectionOrderEntries(content[SECTION_ORDER_KEY], currentPath);
8208
- for (const entry of entries) {
8209
- if (entry.instanceId === entry.type) continue;
8210
- if (document.querySelector(`[data-ohw-instance="${CSS.escape(entry.instanceId)}"]`)) continue;
8211
- const original = document.querySelector(
8212
- `[data-ohw-section="${CSS.escape(entry.type)}"][data-ohw-instance="${CSS.escape(entry.type)}"]`
8213
- );
8214
- if (!original) continue;
8215
- const clone = original.cloneNode(true);
8216
- clone.setAttribute("data-ohw-instance", entry.instanceId);
8217
- rekeySectionSubtree(clone, entry.instanceId);
8218
- original.insertAdjacentElement("afterend", clone);
8219
- }
8220
- applyPersistedOrder(entries);
8221
8795
  }
8222
8796
 
8223
8797
  // src/ui/ai-section/AiSectionOverlay.tsx
8798
+ import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
8799
+ import { Check, X } from "lucide-react";
8224
8800
  import { Fragment as Fragment5, jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
8225
8801
  function findSectionElement(instanceId) {
8226
8802
  const escaped = CSS.escape(instanceId);
@@ -12936,6 +13512,7 @@ function readLogoSizeState(content, placement) {
12936
13512
  function getLogoElement(el) {
12937
13513
  const marked = el.closest('[data-ohw-role="logo"], [data-ohw-logo]');
12938
13514
  if (marked) return marked;
13515
+ if (el.closest('[data-ohw-editable="icon"]')) return null;
12939
13516
  const root = el.closest("nav, [data-ohw-nav-root], footer");
12940
13517
  if (!root) return null;
12941
13518
  const anchor = el.closest("a");
@@ -14008,15 +14585,17 @@ function useSectionDrag({
14008
14585
  clearSectionDragVisuals();
14009
14586
  return;
14010
14587
  }
14011
- const orderJson = JSON.stringify(entries);
14588
+ const orderJson = JSON.stringify(
14589
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, entries)
14590
+ );
14012
14591
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
14013
14592
  setAiSectionOrder(orderJson, window.location.pathname);
14014
14593
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
14015
- applyPersistedOrder(entries);
14594
+ applyPersistedOrder(getPageSectionOrderEntries(orderJson, window.location.pathname));
14016
14595
  clearSectionDragVisuals();
14017
14596
  requestAnimationFrame(() => {
14018
14597
  if (editContentRef.current[SECTION_ORDER_KEY] === orderJson) {
14019
- applyPersistedOrder(entries);
14598
+ applyPersistedOrder(getPageSectionOrderEntries(orderJson, window.location.pathname));
14020
14599
  }
14021
14600
  requestAnimationFrame(() => {
14022
14601
  window.dispatchEvent(new Event("resize"));
@@ -14317,6 +14896,9 @@ function collectEditableNodes(extraContent, root = document) {
14317
14896
  if (el.dataset.ohwEditable === "link") {
14318
14897
  return { key: el.dataset.ohwKey ?? "", type: "link", text: getLinkHref3(el) };
14319
14898
  }
14899
+ if (el.dataset.ohwEditable === "map") {
14900
+ return { key: el.dataset.ohwKey ?? "", type: "map", text: el.dataset.ohwMapQuery ?? "" };
14901
+ }
14320
14902
  return {
14321
14903
  key: el.dataset.ohwKey ?? "",
14322
14904
  type: el.dataset.ohwEditable ?? "text",
@@ -14870,21 +15452,10 @@ function parseSchedulingInsertAfter(insertAfter) {
14870
15452
  insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
14871
15453
  };
14872
15454
  }
14873
- function resolveSchedulingInsert(insertAfter, explicitInsertBefore) {
14874
- const parsed = parseSchedulingInsertAfter(insertAfter);
14875
- const insertBefore = explicitInsertBefore ?? parsed.insertBefore;
14876
- const effectiveInsertAfter = insertBefore && parsed.insertBefore === null ? `${insertAfter}${INSERT_BEFORE_MARKER}${insertBefore}` : insertAfter;
14877
- return { effectiveInsertAfter, insertBefore };
14878
- }
14879
- function getSchedulingMountPoint(insertAfter) {
14880
- const { anchor } = parseSchedulingInsertAfter(insertAfter);
14881
- let anchorEl = document.querySelector(`[data-ohw-section="${anchor}"]`);
14882
- if (!anchorEl && anchor === "scheduling") {
14883
- const widgets = Array.from(document.querySelectorAll('[data-ohw-section^="scheduling-"]'));
14884
- anchorEl = widgets.at(-1) ?? null;
14885
- }
14886
- if (!anchorEl) return null;
14887
- return anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
15455
+ function resolveEntryAnchor(entry) {
15456
+ if (entry.anchorId !== void 0) return { anchorId: entry.anchorId, beforeId: entry.beforeId ?? null };
15457
+ const parsed = parseSchedulingInsertAfter(entry.insertAfter);
15458
+ return { anchorId: parsed.anchor, beforeId: parsed.insertBefore };
14888
15459
  }
14889
15460
  function schedulingMountDepth(insertAfter) {
14890
15461
  if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
@@ -14901,8 +15472,7 @@ function getPageSchedulingEntries(raw) {
14901
15472
  }
14902
15473
  }
14903
15474
  function isSchedulingWidgetMissing(entry) {
14904
- const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
14905
- return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
15475
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(entry.insertAfter)}"]`);
14906
15476
  }
14907
15477
  function hasMissingSchedulingWidgets(entries) {
14908
15478
  return entries.some(isSchedulingWidgetMissing);
@@ -14932,16 +15502,17 @@ function initSectionsFromContent(content, removeExisting = false) {
14932
15502
  } catch {
14933
15503
  }
14934
15504
  }
14935
- function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
14936
- const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
14937
- const sectionId = schedulingSectionId(effectiveInsertAfter);
15505
+ function mountSchedulingWidget(anchorId, notifyOnConnect = false, scheduleId, beforeId, existingWidgetId) {
15506
+ const widgetId = existingWidgetId ?? (beforeId ? `${anchorId}${INSERT_BEFORE_MARKER}${beforeId}` : anchorId);
15507
+ const sectionId = schedulingSectionId(widgetId);
14938
15508
  if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
14939
- const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
14940
- if (!mountPoint) return false;
15509
+ const anchorEl = document.querySelector(`[data-ohw-section="${anchorId}"]`);
15510
+ if (!anchorEl) return false;
15511
+ const mountPoint = anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
14941
15512
  const container = document.createElement("div");
14942
15513
  container.dataset.ohwSectionContainer = "scheduling";
14943
- if (insertBefore) {
14944
- const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
15514
+ if (beforeId) {
15515
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${beforeId}"]`);
14945
15516
  const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
14946
15517
  if (!beforePoint) return false;
14947
15518
  beforePoint.insertAdjacentElement("beforebegin", container);
@@ -14952,19 +15523,25 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
14952
15523
  }
14953
15524
  tail.insertAdjacentElement("afterend", container);
14954
15525
  }
14955
- const root = createRoot2(container);
14956
- flushSync2(() => {
14957
- root.render(
14958
- /* @__PURE__ */ jsx33(
14959
- SchedulingWidget,
14960
- {
14961
- notifyOnConnect,
14962
- initialScheduleId: scheduleId,
14963
- insertAfter: effectiveInsertAfter
14964
- }
14965
- )
14966
- );
14967
- });
15526
+ try {
15527
+ const root = createRoot2(container);
15528
+ flushSync2(() => {
15529
+ root.render(
15530
+ /* @__PURE__ */ jsx33(
15531
+ SchedulingWidget,
15532
+ {
15533
+ notifyOnConnect,
15534
+ initialScheduleId: scheduleId,
15535
+ insertAfter: widgetId
15536
+ }
15537
+ )
15538
+ );
15539
+ });
15540
+ } catch (err) {
15541
+ console.error("[ow:scheduling] render threw", err);
15542
+ container.remove();
15543
+ return false;
15544
+ }
14968
15545
  const tracker = getSectionsTracker();
14969
15546
  let sections = [];
14970
15547
  try {
@@ -14972,10 +15549,12 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
14972
15549
  } catch {
14973
15550
  }
14974
15551
  const inEditor = typeof window !== "undefined" && window.self !== window.top;
14975
- if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
15552
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === widgetId)) {
14976
15553
  sections.push({
14977
15554
  type: "scheduling",
14978
- insertAfter: effectiveInsertAfter,
15555
+ insertAfter: widgetId,
15556
+ anchorId,
15557
+ beforeId: beforeId ?? null,
14979
15558
  pagePath: window.location.pathname,
14980
15559
  ...scheduleId ? { scheduleId } : {}
14981
15560
  });
@@ -14989,7 +15568,8 @@ function mountSchedulingEntries(entries, notifyOnConnect = false) {
14989
15568
  for (let i = pending.length - 1; i >= 0; i--) {
14990
15569
  const entry = pending[i];
14991
15570
  const shouldNotify = typeof notifyOnConnect === "function" ? notifyOnConnect(entry) : notifyOnConnect;
14992
- if (mountSchedulingWidget(entry.insertAfter, shouldNotify, entry.scheduleId ?? null)) {
15571
+ const { anchorId, beforeId } = resolveEntryAnchor(entry);
15572
+ if (mountSchedulingWidget(anchorId, shouldNotify, entry.scheduleId ?? null, beforeId, entry.insertAfter)) {
14993
15573
  pending.splice(i, 1);
14994
15574
  }
14995
15575
  }
@@ -15081,7 +15661,7 @@ var EDITOR_CHROME_SELECTOR = '[data-ohw-toolbar], [data-ohw-form-toolbar], [data
15081
15661
  function isOverEditorChrome(x, y) {
15082
15662
  return document.elementsFromPoint(x, y).some((el) => el instanceof HTMLElement && el.matches(EDITOR_CHROME_SELECTOR));
15083
15663
  }
15084
- var NON_MEDIA_SELECTOR = '[data-ohw-editable]:not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"]):not([data-ohw-editable="video"]):not([data-ohw-editable="icon"]):not([data-ohw-editable="form"])';
15664
+ var NON_MEDIA_SELECTOR = '[data-ohw-editable]:not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"]):not([data-ohw-editable="video"]):not([data-ohw-editable="icon"]):not([data-ohw-editable="form"]):not([data-ohw-editable="map"])';
15085
15665
  function getVideoEl2(el) {
15086
15666
  return el instanceof HTMLVideoElement ? el : el.querySelector("video");
15087
15667
  }
@@ -15137,6 +15717,12 @@ function applyVideoSettingNode(key, val) {
15137
15717
  });
15138
15718
  return true;
15139
15719
  }
15720
+ function applyMapQuery(el, val) {
15721
+ if (!(el instanceof HTMLIFrameElement)) return;
15722
+ const nextSrc = `https://www.google.com/maps?q=${encodeURIComponent(val)}&output=embed`;
15723
+ if (el.src !== nextSrc) el.src = nextSrc;
15724
+ el.setAttribute("data-ohw-map-query", val);
15725
+ }
15140
15726
  function applyLinkByKey(key, val) {
15141
15727
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
15142
15728
  if (el.dataset.ohwEditable === "link") applyLinkHref(el, val);
@@ -15147,6 +15733,11 @@ function applyLinkByKey(key, val) {
15147
15733
  hrefAnchors.forEach((el) => applyLinkHref(el, val));
15148
15734
  }
15149
15735
  }
15736
+ function isInsideLinkEditor(target) {
15737
+ return Boolean(
15738
+ target.closest("[data-ohw-link-popover-root]") || target.closest("[data-ohw-link-modal-root]") || target.closest("[data-ohw-link-page-dropdown]") || target.closest("[data-ohw-section-picker]") || target.closest("[data-ohw-navbar-container-chrome]") || target.closest("[data-ohw-navbar-add-button]") || target.closest("[data-ohw-footer-container-chrome]") || target.closest("[data-ohw-footer-add-button]") || target.closest("[data-ohw-more-menu]") || target.closest('[data-slot="dropdown-menu-content"]') || target.closest('[data-slot="popover-content"]') || target.closest('[data-slot="dialog-content"]') || target.closest('[data-slot="dialog-overlay"]')
15739
+ );
15740
+ }
15150
15741
  function isInsideFloatingPanel(target) {
15151
15742
  return Boolean(target.closest("[data-ohw-floating-panel]"));
15152
15743
  }
@@ -15154,11 +15745,6 @@ function isPointOverFloatingPanel(clientX, clientY) {
15154
15745
  const el = document.elementFromPoint(clientX, clientY);
15155
15746
  return Boolean(el instanceof Element && el.closest("[data-ohw-floating-panel]"));
15156
15747
  }
15157
- function isInsideLinkEditor(target) {
15158
- return Boolean(
15159
- target.closest("[data-ohw-link-popover-root]") || target.closest("[data-ohw-link-modal-root]") || target.closest("[data-ohw-link-page-dropdown]") || target.closest("[data-ohw-section-picker]") || target.closest("[data-ohw-navbar-container-chrome]") || target.closest("[data-ohw-navbar-add-button]") || target.closest("[data-ohw-footer-container-chrome]") || target.closest("[data-ohw-footer-add-button]") || target.closest("[data-ohw-more-menu]") || target.closest('[data-slot="dropdown-menu-content"]') || target.closest('[data-slot="popover-content"]') || target.closest('[data-slot="dialog-content"]') || target.closest('[data-slot="dialog-overlay"]')
15160
- );
15161
- }
15162
15748
  function getHrefKeyFromElement(el) {
15163
15749
  if (!el) return null;
15164
15750
  const anchor = el.closest("[data-ohw-href-key]");
@@ -15417,7 +16003,7 @@ function getNavigationSelectionParent(el) {
15417
16003
  if (!isFooterLinksContainer(el) && (el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isInferredFooterGroup2(el))) {
15418
16004
  return getFooterLinksContainer();
15419
16005
  }
15420
- if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || isFooterLinksContainer(el)) {
16006
+ if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isFooterLinksContainer(el) || isInferredFooterGroup2(el)) {
15421
16007
  return getNavigationRoot(el);
15422
16008
  }
15423
16009
  return null;
@@ -15632,7 +16218,6 @@ var ICONS = {
15632
16218
  insertUnorderedList: '<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>',
15633
16219
  insertOrderedList: '<line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/>'
15634
16220
  };
15635
- var HEIGHT_SETTLE_DELAYS = [150, 400, 800];
15636
16221
  var SELECTION_CHROME_GAP2 = 4;
15637
16222
  var TOOLBAR_STROKE_GAP2 = 4;
15638
16223
  var TOOLBAR_OFFSET_FROM_ELEMENT = SELECTION_CHROME_GAP2 + TOOLBAR_STROKE_GAP2;
@@ -16012,6 +16597,7 @@ function StateToggle({
16012
16597
  );
16013
16598
  }
16014
16599
  var contentCache = /* @__PURE__ */ new Map();
16600
+ var fetchedContentPaths = /* @__PURE__ */ new Set();
16015
16601
  var brandingCache = /* @__PURE__ */ new Map();
16016
16602
  var OHW_LOADER_STYLE = {
16017
16603
  position: "fixed",
@@ -16541,13 +17127,6 @@ function OhhwellsBridge() {
16541
17127
  const [isItemDragging, setIsItemDragging] = useState13(false);
16542
17128
  const [isFooterFrameSelection, setIsFooterFrameSelection] = useState13(false);
16543
17129
  isFooterFrameSelectionRef.current = isFooterFrameSelection;
16544
- const [floatingPanel, setFloatingPanel] = useState13(null);
16545
- const floatingPanelOpenRef = useRef10(false);
16546
- floatingPanelOpenRef.current = floatingPanel !== null;
16547
- const [floatingPanelPos, setFloatingPanelPos] = useState13(null);
16548
- const [logoSizeDraft, setLogoSizeDraft] = useState13(null);
16549
- const [editorViewport, setEditorViewport] = useState13("desktop");
16550
- const [parentScrollSnap, setParentScrollSnap] = useState13(null);
16551
17130
  const [navDropdownPreviewOpen, setNavDropdownPreviewOpen] = useState13(null);
16552
17131
  const [footerHeadingVisible, setFooterHeadingVisible] = useState13(null);
16553
17132
  const footerDragRef = useRef10(null);
@@ -16565,6 +17144,13 @@ function OhhwellsBridge() {
16565
17144
  const brandKitRef = useRef10("");
16566
17145
  const stylesRef = useRef10("");
16567
17146
  const pendingDeleteUndoRef = useRef10(null);
17147
+ const [floatingPanel, setFloatingPanel] = useState13(null);
17148
+ const floatingPanelOpenRef = useRef10(false);
17149
+ const setFloatingPanelRef = useRef10(setFloatingPanel);
17150
+ const [floatingPanelPos, setFloatingPanelPos] = useState13(null);
17151
+ const [logoSizeDraft, setLogoSizeDraft] = useState13(null);
17152
+ const [editorViewport, setEditorViewport] = useState13("desktop");
17153
+ const [parentScrollSnap, setParentScrollSnap] = useState13(null);
16568
17154
  const [sitePages, setSitePages] = useState13([]);
16569
17155
  const [sectionsByPath, setSectionsByPath] = useState13({});
16570
17156
  const sectionsPrefetchGenRef = useRef10(0);
@@ -16573,7 +17159,18 @@ function OhhwellsBridge() {
16573
17159
  const linkPopoverOpenRef = useRef10(false);
16574
17160
  const linkPopoverGraceUntilRef = useRef10(0);
16575
17161
  setLinkPopoverRef.current = setLinkPopover;
17162
+ setFloatingPanelRef.current = setFloatingPanel;
16576
17163
  linkPopoverSessionRef.current = linkPopover;
17164
+ floatingPanelOpenRef.current = Boolean(floatingPanel);
17165
+ useEffect13(() => {
17166
+ const syncViewport = () => {
17167
+ const next = window.innerWidth <= 480 ? "mobile" : "desktop";
17168
+ setEditorViewport((prev) => prev === next ? prev : next);
17169
+ };
17170
+ syncViewport();
17171
+ window.addEventListener("resize", syncViewport);
17172
+ return () => window.removeEventListener("resize", syncViewport);
17173
+ }, []);
16577
17174
  const {
16578
17175
  navDragRef,
16579
17176
  navDropSlots,
@@ -17898,17 +18495,19 @@ function OhhwellsBridge() {
17898
18495
  }
17899
18496
  if (typeof content[STYLE_STORE_KEY] === "string") {
17900
18497
  stylesRef.current = content[STYLE_STORE_KEY];
18498
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
17901
18499
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17902
18500
  }
17903
18501
  applyBrandChrome(content);
18502
+ initSectionInstancesFromContent(content, window.location.pathname);
17904
18503
  for (const [key, val] of Object.entries(content)) {
17905
18504
  if (key === "__ohw_sections") continue;
17906
18505
  if (key === AI_SECTIONS_KEY) continue;
18506
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
18507
+ if (LOGO_IMAGE_KEYS.includes(key)) continue;
17907
18508
  if (key === BRAND_KIT_KEY) continue;
17908
18509
  if (key === STYLE_STORE_KEY) continue;
17909
18510
  if (BRAND_CHROME_KEYS.has(key)) continue;
17910
- if (key === LOGO_PLACEHOLDER_KEY) continue;
17911
- if (LOGO_IMAGE_KEYS.includes(key)) continue;
17912
18511
  if (applyVideoSettingNode(key, val)) continue;
17913
18512
  if (applyCarouselNode(key, val)) continue;
17914
18513
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -17936,6 +18535,8 @@ function OhhwellsBridge() {
17936
18535
  }
17937
18536
  } else if (el.dataset.ohwEditable === "link") {
17938
18537
  applyLinkHref(el, val);
18538
+ } else if (el.dataset.ohwEditable === "map") {
18539
+ applyMapQuery(el, val);
17939
18540
  } else if (el.dataset.ohwEditable === "icon") {
17940
18541
  applyIconMarkup(el, val);
17941
18542
  } else if (el.dataset.ohwEditable === "form") {
@@ -17956,7 +18557,6 @@ function OhhwellsBridge() {
17956
18557
  if (isEditModeRef.current) requestMissingSocialIconsRef.current();
17957
18558
  enforceLinkHrefs();
17958
18559
  initSectionsFromContent(content, true);
17959
- initSectionInstancesFromContent(content, window.location.pathname);
17960
18560
  sectionsLoadedRef.current = true;
17961
18561
  pendingScheduleConfigRequests.current.splice(0).forEach(processConfigRequest);
17962
18562
  if (imageLoads.length === 0) return Promise.resolve();
@@ -17975,7 +18575,9 @@ function OhhwellsBridge() {
17975
18575
  let cancelled = false;
17976
18576
  setFetchState("loading");
17977
18577
  const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
17978
- fetch(`${apiUrl}/api/public/sites/${subdomain}/content`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18578
+ const initialPath = pathname;
18579
+ fetchedContentPaths.add(`${subdomain}::${initialPath}`);
18580
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(initialPath)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
17979
18581
  if (cancelled) return;
17980
18582
  const content = data?.content ?? {};
17981
18583
  const branding = Boolean(data?.showBranding);
@@ -18109,16 +18711,17 @@ function OhhwellsBridge() {
18109
18711
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
18110
18712
  }
18111
18713
  if (typeof content[STYLE_STORE_KEY] === "string") {
18714
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
18112
18715
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
18113
18716
  }
18114
18717
  for (const [key, val] of Object.entries(content)) {
18115
18718
  if (key === "__ohw_sections") continue;
18116
18719
  if (key === AI_SECTIONS_KEY) continue;
18720
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
18721
+ if (LOGO_IMAGE_KEYS.includes(key)) continue;
18117
18722
  if (key === BRAND_KIT_KEY) continue;
18118
18723
  if (key === STYLE_STORE_KEY) continue;
18119
18724
  if (BRAND_CHROME_KEYS.has(key)) continue;
18120
- if (key === LOGO_PLACEHOLDER_KEY) continue;
18121
- if (LOGO_IMAGE_KEYS.includes(key)) continue;
18122
18725
  if (applyVideoSettingNode(key, val)) continue;
18123
18726
  if (applyCarouselNode(key, val)) continue;
18124
18727
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -18133,6 +18736,8 @@ function OhhwellsBridge() {
18133
18736
  if (video && video.src !== val) applyVideoSrc(video, val);
18134
18737
  } else if (el.dataset.ohwEditable === "link") {
18135
18738
  applyLinkHref(el, val);
18739
+ } else if (el.dataset.ohwEditable === "map") {
18740
+ applyMapQuery(el, val);
18136
18741
  } else if (el.dataset.ohwEditable === "form") {
18137
18742
  } else if (isIconMarkupValue(val)) {
18138
18743
  } else if (el.innerHTML !== val) {
@@ -18164,6 +18769,17 @@ function OhhwellsBridge() {
18164
18769
  debounceTimer = setTimeout(applyFromCache, 150);
18165
18770
  };
18166
18771
  applyFromCache();
18772
+ const pathCacheKey = `${subdomain}::${pathname}`;
18773
+ if (!fetchedContentPaths.has(pathCacheKey)) {
18774
+ fetchedContentPaths.add(pathCacheKey);
18775
+ const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
18776
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(pathname)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18777
+ if (!data?.content) return;
18778
+ contentCache.set(subdomain, data.content);
18779
+ applyFromCache();
18780
+ }).catch(() => {
18781
+ });
18782
+ }
18167
18783
  observer = new MutationObserver(scheduleApply);
18168
18784
  observer.observe(document.body, { childList: true, subtree: true });
18169
18785
  return () => {
@@ -18279,26 +18895,11 @@ function OhhwellsBridge() {
18279
18895
  const t2 = setTimeout(measure, 500);
18280
18896
  const ro = new ResizeObserver(schedule);
18281
18897
  ro.observe(document.body);
18282
- let lastWidth = window.innerWidth;
18283
- let resizeTimers = [];
18284
- const clearResizeTimers = () => {
18285
- resizeTimers.forEach(clearTimeout);
18286
- resizeTimers = [];
18287
- };
18288
- const handleResize = () => {
18289
- if (window.innerWidth === lastWidth) return;
18290
- lastWidth = window.innerWidth;
18291
- clearResizeTimers();
18292
- resizeTimers = HEIGHT_SETTLE_DELAYS.map((delay) => setTimeout(measure, delay));
18293
- };
18294
- window.addEventListener("resize", handleResize);
18295
18898
  return () => {
18296
18899
  clearTimeout(t1);
18297
18900
  clearTimeout(t2);
18298
18901
  if (raf != null) cancelAnimationFrame(raf);
18299
18902
  ro.disconnect();
18300
- clearResizeTimers();
18301
- window.removeEventListener("resize", handleResize);
18302
18903
  };
18303
18904
  }, [pathname, isEditMode, postToParent2]);
18304
18905
  useEffect13(() => {
@@ -18342,15 +18943,6 @@ function OhhwellsBridge() {
18342
18943
  [style*="100vh"] { min-height: ${initialVh}px !important; height: ${initialVh}px !important; }
18343
18944
  [style*="100svh"] { min-height: ${initialVh}px !important; height: ${initialVh}px !important; }
18344
18945
  [style*="100dvh"] { min-height: ${initialVh}px !important; height: ${initialVh}px !important; }
18345
- /* A section written as min-height: var(--ohw-canvas-h, 100svh) \u2014 the documented way to
18346
- build a full-viewport section that still grows for its own content \u2014 matches the three
18347
- rules above on substring alone, since the fallback text contains "100svh" too. Forcing
18348
- a literal height on top of that turns "at least one screen" into "exactly one screen",
18349
- so content taller than one screen (a long mobile hero, say) overflows a centered flex
18350
- column upward, under whatever sits above it. Only the min-height half belongs to it. */
18351
- [style*="min-height"][style*="100vh"],
18352
- [style*="min-height"][style*="100svh"],
18353
- [style*="min-height"][style*="100dvh"] { height: auto !important; }
18354
18946
  /* Emptied text keeps somewhere to click. A label typed down to nothing collapses to a
18355
18947
  couple of pixels, and getting back into it meant hunting for the caret with the mouse.
18356
18948
  Edit mode only \u2014 the published page shows nothing where there is nothing (OHH-736). */
@@ -18553,9 +19145,6 @@ function OhhwellsBridge() {
18553
19145
  if (target.closest("[data-ohw-state-toggle]")) return;
18554
19146
  if (target.closest("[data-ohw-max-badge]")) return;
18555
19147
  if (isInsideLinkEditor(target)) return;
18556
- if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
18557
- clearMediaSelectionRef.current();
18558
- }
18559
19148
  if (isInsideFloatingPanel(target)) return;
18560
19149
  if (target.closest("[data-ohw-form-toolbar]")) return;
18561
19150
  if (target.closest(
@@ -18563,6 +19152,9 @@ function OhhwellsBridge() {
18563
19152
  )) {
18564
19153
  return;
18565
19154
  }
19155
+ if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
19156
+ clearMediaSelectionRef.current();
19157
+ }
18566
19158
  {
18567
19159
  const formEl = getFormElement(target);
18568
19160
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
@@ -18714,14 +19306,6 @@ function OhhwellsBridge() {
18714
19306
  }
18715
19307
  const clickedButton = findClosestButtonLike(target);
18716
19308
  const buttonOnMedia = Boolean(clickedButton && isMediaEditable(editable) && editable.contains(clickedButton));
18717
- console.log("[click-debug]", {
18718
- editableType: editable.dataset.ohwEditable,
18719
- editableTag: editable.tagName,
18720
- targetTag: target.tagName,
18721
- clickedButtonTag: clickedButton?.tagName ?? null,
18722
- buttonOnMedia,
18723
- isMediaEditableEditable: isMediaEditable(editable)
18724
- });
18725
19309
  if (isMediaEditable(editable) && !buttonOnMedia) {
18726
19310
  e.preventDefault();
18727
19311
  e.stopPropagation();
@@ -18748,11 +19332,6 @@ function OhhwellsBridge() {
18748
19332
  const hrefLookupTarget = buttonOnMedia ? clickedButton : editable;
18749
19333
  const hrefCtx = getHrefKeyFromElement(hrefLookupTarget);
18750
19334
  const navAnchor = hrefCtx ? getNavigationItemAnchor(hrefCtx.anchor) : null;
18751
- console.log("[click-debug 2]", {
18752
- hrefLookupTargetTag: hrefLookupTarget.tagName,
18753
- hrefCtx: hrefCtx ? { key: hrefCtx.key } : null,
18754
- navAnchorTag: navAnchor?.tagName ?? null
18755
- });
18756
19335
  if (navAnchor) {
18757
19336
  e.preventDefault();
18758
19337
  e.stopPropagation();
@@ -18922,6 +19501,9 @@ function OhhwellsBridge() {
18922
19501
  setHoveredItemRect(null);
18923
19502
  hoveredNavContainerRef.current = null;
18924
19503
  setHoveredNavContainerRect(null);
19504
+ siblingHintElRef.current = null;
19505
+ setSiblingHintRect(null);
19506
+ setSiblingHintRects([]);
18925
19507
  return;
18926
19508
  }
18927
19509
  {
@@ -19040,7 +19622,6 @@ function OhhwellsBridge() {
19040
19622
  hoveredNavContainerRef.current = null;
19041
19623
  setHoveredNavContainerRect(null);
19042
19624
  hoveredItemElRef.current = editable;
19043
- setHoveredItemRect(isSelectedForHoverRef.current(editable) ? null : editable.getBoundingClientRect());
19044
19625
  }
19045
19626
  }
19046
19627
  }
@@ -19337,7 +19918,7 @@ function OhhwellsBridge() {
19337
19918
  }
19338
19919
  };
19339
19920
  const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false) => {
19340
- if (linkPopoverOpenRef.current) {
19921
+ if (linkPopoverOpenRef.current || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
19341
19922
  if (hoveredImageRef.current) {
19342
19923
  hoveredImageRef.current = null;
19343
19924
  hoveredImageHasTextOverlapRef.current = false;
@@ -19702,8 +20283,7 @@ function OhhwellsBridge() {
19702
20283
  };
19703
20284
  const handleMouseMove = (e) => {
19704
20285
  const { clientX, clientY } = e;
19705
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
19706
- if (isOverEditorChrome(clientX, clientY)) {
20286
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY) || isOverEditorChrome(clientX, clientY)) {
19707
20287
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
19708
20288
  formHoverElRef.current = null;
19709
20289
  setFormHoverRect(null);
@@ -19711,6 +20291,12 @@ function OhhwellsBridge() {
19711
20291
  setHoveredItemRect(null);
19712
20292
  hoveredNavContainerRef.current = null;
19713
20293
  setHoveredNavContainerRect(null);
20294
+ siblingHintElRef.current = null;
20295
+ setSiblingHintRect(null);
20296
+ setSiblingHintRects([]);
20297
+ dismissImageHover();
20298
+ clearImageHover();
20299
+ setSectionGap(null);
19714
20300
  return;
19715
20301
  }
19716
20302
  if (probeSocialsRowAt(clientX, clientY)) return;
@@ -19722,7 +20308,11 @@ function OhhwellsBridge() {
19722
20308
  if (e.data?.type !== "ow:pointer-sync") return;
19723
20309
  const { clientX, clientY } = e.data;
19724
20310
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
19725
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
20311
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
20312
+ dismissImageHover();
20313
+ clearImageHover();
20314
+ return;
20315
+ }
19726
20316
  if (probeSocialsRowAt(clientX, clientY)) return;
19727
20317
  probeSectionGapAt(clientX, clientY);
19728
20318
  probeImageAt(clientX, clientY);
@@ -20001,6 +20591,44 @@ function OhhwellsBridge() {
20001
20591
  if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
20002
20592
  }, 400));
20003
20593
  };
20594
+ const reapCommittedAiSections = (excludeIds) => {
20595
+ const aiState = parseAiSectionsState(aiSectionsRef.current);
20596
+ if (aiState.sections.length === 0) return [];
20597
+ let orderEntries = [];
20598
+ try {
20599
+ const parsed = JSON.parse(editContentRef.current[SECTION_ORDER_KEY] ?? "[]");
20600
+ if (Array.isArray(parsed)) orderEntries = parsed;
20601
+ } catch {
20602
+ return [];
20603
+ }
20604
+ const removedIds = orderEntries.filter((entry) => entry && typeof entry.instanceId === "string" && entry.removed).map((entry) => entry.instanceId);
20605
+ if (removedIds.length === 0) return [];
20606
+ const result = reapRemovedAiSections(aiState, parseStyleStore(stylesRef.current), removedIds, excludeIds);
20607
+ if (!result.changed) return [];
20608
+ const nodes = [];
20609
+ aiSectionsRef.current = serializeAiSectionsState(result.state);
20610
+ nodes.push({ key: AI_SECTIONS_KEY, text: aiSectionsRef.current });
20611
+ const reaped = new Set(result.reapedIds);
20612
+ const nextOrderJson = JSON.stringify(orderEntries.filter((entry) => !reaped.has(entry.instanceId)));
20613
+ editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: nextOrderJson };
20614
+ setAiSectionOrder(nextOrderJson, window.location.pathname);
20615
+ nodes.push({ key: SECTION_ORDER_KEY, text: nextOrderJson });
20616
+ if (result.store) {
20617
+ stylesRef.current = JSON.stringify(result.store);
20618
+ nodes.push({ key: STYLE_STORE_KEY, text: stylesRef.current });
20619
+ }
20620
+ const nextContent = { ...editContentRef.current };
20621
+ for (const key of Object.keys(nextContent)) {
20622
+ if (key.startsWith(AI_SLOT_KEY_PREFIX) && result.slotPrefixes.some((prefix) => key.startsWith(prefix)) && nextContent[key] !== "") {
20623
+ nextContent[key] = "";
20624
+ nodes.push({ key, text: "" });
20625
+ }
20626
+ }
20627
+ editContentRef.current = nextContent;
20628
+ applyAiSectionsToDom(result.state);
20629
+ applyStylesToDom(parseStyleStore(stylesRef.current));
20630
+ return nodes;
20631
+ };
20004
20632
  const handleHydrate = (e) => {
20005
20633
  if (e.data?.type !== "ow:hydrate") return;
20006
20634
  const content = e.data.content;
@@ -20019,9 +20647,11 @@ function OhhwellsBridge() {
20019
20647
  }
20020
20648
  if (typeof content[STYLE_STORE_KEY] === "string") {
20021
20649
  stylesRef.current = content[STYLE_STORE_KEY];
20650
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
20022
20651
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
20023
20652
  }
20024
20653
  applyBrandChrome(content);
20654
+ initSectionInstancesFromContent(content, window.location.pathname);
20025
20655
  let sectionsJson = null;
20026
20656
  for (const [key, val] of Object.entries(content)) {
20027
20657
  if (key === "__ohw_sections") {
@@ -20029,11 +20659,11 @@ function OhhwellsBridge() {
20029
20659
  continue;
20030
20660
  }
20031
20661
  if (key === AI_SECTIONS_KEY) continue;
20662
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
20663
+ if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
20032
20664
  if (key === BRAND_KIT_KEY) continue;
20033
20665
  if (key === STYLE_STORE_KEY) continue;
20034
20666
  if (BRAND_CHROME_KEYS.has(key)) continue;
20035
- if (key === LOGO_PLACEHOLDER_KEY) continue;
20036
- if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
20037
20667
  if (applyVideoSettingNode(key, val)) continue;
20038
20668
  if (applyCarouselNode(key, val)) continue;
20039
20669
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -20047,6 +20677,8 @@ function OhhwellsBridge() {
20047
20677
  if (video && video.src !== val) applyVideoSrc(video, val);
20048
20678
  } else if (el.dataset.ohwEditable === "link") {
20049
20679
  applyLinkHref(el, val);
20680
+ } else if (el.dataset.ohwEditable === "map") {
20681
+ applyMapQuery(el, val);
20050
20682
  } else if (el.dataset.ohwEditable === "icon") {
20051
20683
  applyIconMarkup(el, val);
20052
20684
  } else if (isIconMarkupValue(val)) {
@@ -20063,12 +20695,16 @@ function OhhwellsBridge() {
20063
20695
  sectionsLoadedRef.current = true;
20064
20696
  pendingScheduleConfigRequests.current.splice(0).forEach(processConfigRequest);
20065
20697
  }
20066
- initSectionInstancesFromContent(content, window.location.pathname);
20067
20698
  editContentRef.current = { ...editContentRef.current, ...content };
20068
20699
  reconcileNavbarItemsFromContent(editContentRef.current);
20069
20700
  reconcileFooterOrderFromContent(editContentRef.current);
20070
20701
  syncNavigationDragCursorAttrs();
20071
20702
  enforceLinkHrefs();
20703
+ const hydrateReapExclude = /* @__PURE__ */ new Set();
20704
+ const hydratePendingUndo = pendingDeleteUndoRef.current;
20705
+ if (hydratePendingUndo?.sectionInstanceId) hydrateReapExclude.add(hydratePendingUndo.sectionInstanceId);
20706
+ const reapNodes = reapCommittedAiSections(hydrateReapExclude);
20707
+ if (reapNodes.length > 0) postToParentRef.current({ type: "ow:change", nodes: reapNodes });
20072
20708
  const hydratedHeight = document.body.scrollHeight;
20073
20709
  if (hydratedHeight > 50) postToParentRef.current({ type: "ow:height", height: hydratedHeight });
20074
20710
  postToParentRef.current({ type: "ow:hydrate-done" });
@@ -20208,12 +20844,35 @@ function OhhwellsBridge() {
20208
20844
  window.addEventListener("message", handleAiSetBrand);
20209
20845
  const handleAiSetStyles = (e) => {
20210
20846
  if (e.data?.type !== "ow:ai-set-styles") return;
20211
- const value = typeof e.data.value === "string" ? e.data.value : "";
20847
+ let value = typeof e.data.value === "string" ? e.data.value : "";
20212
20848
  const previous = stylesRef.current;
20849
+ let previousSections;
20850
+ const store = parseStyleStore(value);
20851
+ if (store) {
20852
+ const folded = foldAlignIntoTrees(parseAiSectionsState(aiSectionsRef.current), store);
20853
+ if (folded.changed) {
20854
+ const nextSections = serializeAiSectionsState(folded.state);
20855
+ if (nextSections !== aiSectionsRef.current) {
20856
+ previousSections = aiSectionsRef.current;
20857
+ aiSectionsRef.current = nextSections;
20858
+ applyAiSectionsToDom(folded.state);
20859
+ postToParentRef.current({
20860
+ type: "ow:change",
20861
+ nodes: [{ key: AI_SECTIONS_KEY, text: nextSections }]
20862
+ });
20863
+ }
20864
+ value = JSON.stringify(folded.store);
20865
+ }
20866
+ }
20213
20867
  stylesRef.current = value;
20214
20868
  applyStylesToDom(parseStyleStore(value));
20215
20869
  postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
20216
- postToParentRef.current({ type: "ow:ai-styles-applied", previous, value });
20870
+ postToParentRef.current({
20871
+ type: "ow:ai-styles-applied",
20872
+ previous,
20873
+ value,
20874
+ ...previousSections !== void 0 ? { previousSections } : {}
20875
+ });
20217
20876
  };
20218
20877
  window.addEventListener("message", handleAiSetStyles);
20219
20878
  const handleGetBrand = (e) => {
@@ -20230,8 +20889,11 @@ function OhhwellsBridge() {
20230
20889
  if (!instanceId || !direction) return;
20231
20890
  const entries = moveSectionInstance(instanceId, direction, window.location.pathname);
20232
20891
  if (!entries) return;
20233
- const orderJson = JSON.stringify(entries);
20892
+ const orderJson = JSON.stringify(
20893
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, entries)
20894
+ );
20234
20895
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20896
+ applyPersistedOrder(getPageSectionOrderEntries(orderJson, window.location.pathname));
20235
20897
  setAiSectionOrder(orderJson, window.location.pathname);
20236
20898
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
20237
20899
  window.dispatchEvent(new Event("resize"));
@@ -20250,8 +20912,11 @@ function OhhwellsBridge() {
20250
20912
  const currentEntries = getPageSectionOrderEntries(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname);
20251
20913
  const entries = deleteSectionInstance(instanceId, window.location.pathname, currentEntries);
20252
20914
  if (!entries) return;
20253
- const orderJson = JSON.stringify(entries);
20915
+ const orderJson = JSON.stringify(
20916
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, entries)
20917
+ );
20254
20918
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20919
+ setAiSectionOrder(orderJson, window.location.pathname);
20255
20920
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
20256
20921
  aiSectionApiRef.current?.clear();
20257
20922
  window.dispatchEvent(new Event("resize"));
@@ -20260,6 +20925,7 @@ function OhhwellsBridge() {
20260
20925
  const actionId = newInstanceId();
20261
20926
  pendingDeleteUndoRef.current = {
20262
20927
  actionId,
20928
+ sectionInstanceId: instanceId,
20263
20929
  restore: () => {
20264
20930
  const restoredEntries = getPageSectionOrderEntries(
20265
20931
  editContentRef.current[SECTION_ORDER_KEY],
@@ -20267,8 +20933,11 @@ function OhhwellsBridge() {
20267
20933
  );
20268
20934
  const restored = restoreSectionInstance(instanceId, window.location.pathname, restoredEntries);
20269
20935
  if (!restored) return;
20270
- const restoredJson = JSON.stringify(restored);
20936
+ const restoredJson = JSON.stringify(
20937
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, restored)
20938
+ );
20271
20939
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: restoredJson };
20940
+ setAiSectionOrder(restoredJson, window.location.pathname);
20272
20941
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: restoredJson }] });
20273
20942
  window.dispatchEvent(new Event("resize"));
20274
20943
  const restoreHeight = document.body.scrollHeight;
@@ -20285,6 +20954,37 @@ function OhhwellsBridge() {
20285
20954
  });
20286
20955
  };
20287
20956
  window.addEventListener("message", handleDeleteSection);
20957
+ const handleDuplicateSection = (e) => {
20958
+ if (e.data?.type !== "ow:duplicate-section") return;
20959
+ const instanceId = typeof e.data.instanceId === "string" ? e.data.instanceId : "";
20960
+ if (!instanceId) return;
20961
+ const newId = newInstanceId();
20962
+ const currentEntries = getPageSectionOrderEntries(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname);
20963
+ const result = duplicateSectionInstance(instanceId, newId, window.location.pathname, currentEntries);
20964
+ if (!result) return;
20965
+ const { entries, keyRekeys } = result;
20966
+ const orderJson = JSON.stringify(
20967
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, entries)
20968
+ );
20969
+ const nodes = [{ key: SECTION_ORDER_KEY, text: orderJson }];
20970
+ for (const { from, to } of keyRekeys) {
20971
+ const inherited = editContentRef.current[from];
20972
+ if (inherited !== void 0) nodes.push({ key: to, text: inherited });
20973
+ }
20974
+ editContentRef.current = {
20975
+ ...editContentRef.current,
20976
+ ...Object.fromEntries(nodes.map(({ key, text }) => [key, text]))
20977
+ };
20978
+ applyPersistedOrder(getPageSectionOrderEntries(orderJson, window.location.pathname));
20979
+ setAiSectionOrder(orderJson, window.location.pathname);
20980
+ postToParentRef.current({ type: "ow:change", nodes });
20981
+ window.dispatchEvent(new Event("resize"));
20982
+ const duplicateHeight = document.body.scrollHeight;
20983
+ if (duplicateHeight > 50) postToParentRef.current({ type: "ow:height", height: duplicateHeight });
20984
+ const clone = document.querySelector(`[data-ohw-instance="${CSS.escape(newId)}"]`);
20985
+ if (clone) aiSectionApiRef.current?.selectFromElement(clone);
20986
+ };
20987
+ window.addEventListener("message", handleDuplicateSection);
20288
20988
  const handleDeactivate = (e) => {
20289
20989
  if (e.data?.type !== "ow:deactivate") return;
20290
20990
  if (document.documentElement.hasAttribute("data-ohw-panel-dragging")) return;
@@ -20294,6 +20994,12 @@ function OhhwellsBridge() {
20294
20994
  closeLinkPopoverRef.current();
20295
20995
  return;
20296
20996
  }
20997
+ if (floatingPanelOpenRef.current) {
20998
+ setFloatingPanelRef.current(null);
20999
+ deselectRef.current();
21000
+ deactivateRef.current();
21001
+ return;
21002
+ }
20297
21003
  deselectRef.current();
20298
21004
  deactivateRef.current();
20299
21005
  clearMediaSelectionRef.current();
@@ -20539,6 +21245,10 @@ function OhhwellsBridge() {
20539
21245
  };
20540
21246
  const handleSave = (e) => {
20541
21247
  if (e.data?.type !== "ow:save") return;
21248
+ const pendingUndo = pendingDeleteUndoRef.current;
21249
+ const reapExclude = /* @__PURE__ */ new Set();
21250
+ if (pendingUndo?.sectionInstanceId) reapExclude.add(pendingUndo.sectionInstanceId);
21251
+ const reapNodes = reapCommittedAiSections(reapExclude);
20542
21252
  const nodes = collectEditableNodes(editContentRef.current);
20543
21253
  const tracker = document.querySelector("[data-ohw-sections-tracker]");
20544
21254
  if (tracker?.textContent) nodes.push({ key: "__ohw_sections", type: "sections", text: tracker.textContent });
@@ -20558,6 +21268,11 @@ function OhhwellsBridge() {
20558
21268
  const text = specs.length ? JSON.stringify(specs) : editContentRef.current[fieldsKey(formKey)];
20559
21269
  if (text) nodes.push({ key: fieldsKey(formKey), type: "text", text });
20560
21270
  });
21271
+ for (const reapNode of reapNodes) {
21272
+ if (reapNode.key.startsWith(AI_SLOT_KEY_PREFIX) && !nodes.some((n) => n.key === reapNode.key)) {
21273
+ nodes.push({ key: reapNode.key, type: "text", text: reapNode.text });
21274
+ }
21275
+ }
20561
21276
  postToParentRef.current({ type: "ow:save-result", nodes });
20562
21277
  };
20563
21278
  const handleInsertSection = (e) => {
@@ -20568,8 +21283,12 @@ function OhhwellsBridge() {
20568
21283
  if (inserted) {
20569
21284
  const tracker = getSectionsTracker();
20570
21285
  postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
20571
- const h = document.body.scrollHeight;
20572
- if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
21286
+ const reportHeight = () => {
21287
+ const h = document.body.scrollHeight;
21288
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
21289
+ };
21290
+ reportHeight();
21291
+ setTimeout(reportHeight, 500);
20573
21292
  }
20574
21293
  };
20575
21294
  const handleSwitchSchedule = (e) => {
@@ -20971,11 +21690,12 @@ function OhhwellsBridge() {
20971
21690
  window.removeEventListener("message", handleMoveSection);
20972
21691
  window.removeEventListener("message", handlePanelDragging);
20973
21692
  window.removeEventListener("message", handleDeleteSection);
21693
+ window.removeEventListener("message", handleDuplicateSection);
20974
21694
  window.removeEventListener("message", handleDeactivate);
20975
- document.documentElement.removeAttribute("data-ohw-panel-dragging");
20976
21695
  window.removeEventListener("message", handleToastAction);
20977
21696
  window.removeEventListener("message", handleFormCount);
20978
21697
  window.removeEventListener("message", handleUiEscape);
21698
+ document.documentElement.removeAttribute("data-ohw-panel-dragging");
20979
21699
  autoSaveTimers.current.forEach(clearTimeout);
20980
21700
  autoSaveTimers.current.clear();
20981
21701
  if (imageUnhoverTimerRef.current) clearTimeout(imageUnhoverTimerRef.current);
@@ -21178,7 +21898,7 @@ function OhhwellsBridge() {
21178
21898
  postToParent2({
21179
21899
  type: "ow:ready",
21180
21900
  version: "1",
21181
- bridgeVersion: "0.1.85",
21901
+ bridgeVersion: "0.1.87",
21182
21902
  path: pathname,
21183
21903
  nodes: collectEditableNodes(editContentRef.current),
21184
21904
  sections
@@ -22097,6 +22817,59 @@ function OhhwellsBridge() {
22097
22817
  ) : null
22098
22818
  ] });
22099
22819
  }
22820
+
22821
+ // src/ui/EmptySection.tsx
22822
+ import Link3 from "next/link";
22823
+ import { Fragment as Fragment9, jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
22824
+ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey }) {
22825
+ return /* @__PURE__ */ jsxs21(Fragment9, { children: [
22826
+ /* @__PURE__ */ jsx34(
22827
+ "p",
22828
+ {
22829
+ style: {
22830
+ fontFamily: "var(--brand-font-body)",
22831
+ fontSize: "0.75rem",
22832
+ fontWeight: 500,
22833
+ letterSpacing: "0.15em",
22834
+ textTransform: "uppercase",
22835
+ color: "var(--brand-accent)",
22836
+ marginBottom: "1.5rem"
22837
+ },
22838
+ children: /* @__PURE__ */ jsx34(Link3, { href: homeHref, style: { color: "inherit" }, children: /* @__PURE__ */ jsx34("span", { ...eyebrowKey ? { "data-ohw-editable": "text", "data-ohw-key": eyebrowKey } : {}, children: "Home" }) })
22839
+ }
22840
+ ),
22841
+ /* @__PURE__ */ jsx34(
22842
+ "h1",
22843
+ {
22844
+ style: {
22845
+ fontFamily: "var(--brand-font-heading)",
22846
+ fontSize: "clamp(2rem, 3.5vw, 2.75rem)",
22847
+ lineHeight: 1.1,
22848
+ letterSpacing: "-0.025em",
22849
+ color: "var(--brand-text)",
22850
+ marginBottom: "1rem"
22851
+ },
22852
+ ...titleKey ? { "data-ohw-editable": "text", "data-ohw-key": titleKey, "data-ohw-max-length": 80 } : {},
22853
+ children: title
22854
+ }
22855
+ ),
22856
+ /* @__PURE__ */ jsx34(
22857
+ "p",
22858
+ {
22859
+ style: {
22860
+ fontFamily: "var(--brand-font-body)",
22861
+ fontSize: "1rem",
22862
+ lineHeight: 1.7,
22863
+ fontWeight: 300,
22864
+ color: "var(--brand-text-muted)",
22865
+ maxWidth: "340px"
22866
+ },
22867
+ ...subtitleKey ? { "data-ohw-editable": "text", "data-ohw-key": subtitleKey, "data-ohw-max-length": 160 } : {},
22868
+ children: "This page doesn't have any content yet."
22869
+ }
22870
+ )
22871
+ ] });
22872
+ }
22100
22873
  export {
22101
22874
  AI_DEFAULT_BRAND,
22102
22875
  AI_TREE_SCHEMA_VERSIONS,
@@ -22113,6 +22886,7 @@ export {
22113
22886
  DropdownMenuItem,
22114
22887
  DropdownMenuSeparator,
22115
22888
  DropdownMenuTrigger,
22889
+ EmptySection,
22116
22890
  ItemActionToolbar,
22117
22891
  ItemInteractionLayer,
22118
22892
  LinkEditorPanel,