@ohhwells/bridge 0.1.87 → 0.1.88-next.262

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