@ohhwells/bridge 0.1.85 → 0.1.86-next.255

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -69,6 +69,10 @@ function isRenderableTree(value) {
69
69
 
70
70
  // src/lib/ai-sections-store.ts
71
71
  var AI_SECTIONS_KEY = "__ohw_ai_sections";
72
+ var AI_SLOT_KEY_PREFIX = "ai.";
73
+ function aiSlotKeyPrefixFor(sectionId) {
74
+ return `${AI_SLOT_KEY_PREFIX}${sectionId}.`;
75
+ }
72
76
  var EMPTY_AI_SECTIONS = { v: 1, sections: [] };
73
77
  function parseAiSectionsState(raw) {
74
78
  if (!raw) return EMPTY_AI_SECTIONS;
@@ -112,6 +116,63 @@ function applyTreeToState(state, payload) {
112
116
  const others = state.sections.filter((existing) => existing.id !== entry.id);
113
117
  return { ...state, v: 1, sections: [...others, entry] };
114
118
  }
119
+ function foldAlignIntoTrees(state, store) {
120
+ const byId = new Map(state.sections.map((entry) => [entry.id, entry]));
121
+ const nextTrees = /* @__PURE__ */ new Map();
122
+ const treeFor = (id) => {
123
+ const cloned = nextTrees.get(id);
124
+ if (cloned) return cloned;
125
+ const entry = byId.get(id);
126
+ if (!entry) return void 0;
127
+ const fresh = {
128
+ ...entry.tree,
129
+ rows: entry.tree.rows.map((row) => ({ ...row, blocks: row.blocks.map((block) => ({ ...block })) }))
130
+ };
131
+ nextTrees.set(id, fresh);
132
+ return fresh;
133
+ };
134
+ const nodes = {};
135
+ for (const [key, override] of Object.entries(store.nodes)) {
136
+ const match = override.align !== void 0 && key.startsWith(AI_SLOT_KEY_PREFIX) ? key.match(/^ai\.(.+?)\.r(\d+)\.b(\d+)(?:\.|$)/) : null;
137
+ const tree = match ? treeFor(match[1]) : void 0;
138
+ const block = match && tree ? tree.rows[Number(match[2])]?.blocks[Number(match[3])] : void 0;
139
+ if (!block) {
140
+ nodes[key] = override;
141
+ continue;
142
+ }
143
+ block.align = override.align;
144
+ const rest = { ...override };
145
+ delete rest.align;
146
+ if (Object.keys(rest).length > 0) nodes[key] = rest;
147
+ }
148
+ const sections = {};
149
+ for (const [sectionId, override] of Object.entries(store.sections)) {
150
+ const tree = override.align !== void 0 ? treeFor(sectionId) : void 0;
151
+ if (!tree) {
152
+ sections[sectionId] = override;
153
+ continue;
154
+ }
155
+ for (const row of tree.rows) {
156
+ for (const block of row.blocks) block.align = override.align;
157
+ }
158
+ const rest = { ...override };
159
+ delete rest.align;
160
+ if (Object.keys(rest).length > 0) sections[sectionId] = rest;
161
+ }
162
+ if (nextTrees.size === 0) return { state, store, changed: false };
163
+ return {
164
+ state: {
165
+ ...state,
166
+ v: 1,
167
+ sections: state.sections.map((entry) => {
168
+ const tree = nextTrees.get(entry.id);
169
+ return tree ? { ...entry, tree } : entry;
170
+ })
171
+ },
172
+ store: { v: 1, sections, nodes },
173
+ changed: true
174
+ };
175
+ }
115
176
  function removeFromState(state, id) {
116
177
  return { ...state, v: 1, sections: state.sections.filter((entry) => entry.id !== id) };
117
178
  }
@@ -123,6 +184,33 @@ function deleteSectionFromState(state, sectionId) {
123
184
  if (removed.includes(sectionId)) return state;
124
185
  return { ...state, v: 1, sections: state.sections, removed: [...removed, sectionId] };
125
186
  }
187
+ function reapRemovedAiSections(state, store, removedIds, excludeIds = /* @__PURE__ */ new Set()) {
188
+ const generated = new Set(state.sections.map((entry) => entry.id));
189
+ const reapedIds = [...new Set(removedIds)].filter((id) => generated.has(id) && !excludeIds.has(id));
190
+ if (reapedIds.length === 0) {
191
+ return { state, store, reapedIds: [], slotPrefixes: [], changed: false };
192
+ }
193
+ const reaped = new Set(reapedIds);
194
+ const slotPrefixes = reapedIds.map(aiSlotKeyPrefixFor);
195
+ const nextState = {
196
+ ...state,
197
+ v: 1,
198
+ sections: state.sections.filter((entry) => !reaped.has(entry.id))
199
+ };
200
+ let nextStore = store;
201
+ if (store) {
202
+ const sections = {};
203
+ for (const [key, override] of Object.entries(store.sections)) {
204
+ if (!reaped.has(key)) sections[key] = override;
205
+ }
206
+ const nodes = {};
207
+ for (const [key, override] of Object.entries(store.nodes)) {
208
+ if (!slotPrefixes.some((prefix) => key.startsWith(prefix))) nodes[key] = override;
209
+ }
210
+ nextStore = { v: 1, sections, nodes };
211
+ }
212
+ return { state: nextState, store: nextStore, reapedIds, slotPrefixes, changed: true };
213
+ }
126
214
 
127
215
  // src/lib/brand-chrome.ts
128
216
  var BRAND_NAME_KEY = "__ohw_brand_name";
@@ -330,6 +418,12 @@ function styleSheetCss() {
330
418
  `[data-ohw-style-spacing="${spacing}"] { padding-top: ${px}px !important; padding-bottom: ${px}px !important; }`
331
419
  );
332
420
  }
421
+ rules.push(
422
+ `[data-ohw-style-corners="sharp"] :is(.card, [data-ohw-card]) { border-radius: 0 !important; }`
423
+ );
424
+ for (const align of ["left", "center", "right"]) {
425
+ rules.push(`[data-ohw-style-align="${align}"] { text-align: ${align} !important; }`);
426
+ }
333
427
  return rules.join("\n");
334
428
  }
335
429
  var STYLE_FONT_LINK_ID = "ohw-style-fonts";
@@ -356,10 +450,25 @@ var SECTION_ATTRS = {
356
450
  textDistribution: "data-ohw-style-distribution",
357
451
  headlineScale: "data-ohw-style-headline",
358
452
  imageAspect: "data-ohw-style-aspect",
359
- spacing: "data-ohw-style-spacing"
453
+ spacing: "data-ohw-style-spacing",
454
+ cornerStyle: "data-ohw-style-corners",
455
+ align: "data-ohw-style-align"
360
456
  };
361
457
  var NODE_WROTE_ATTR = "data-ohw-style-node";
362
- var NODE_PROPS = ["color", "font-family", "font-size", "background"];
458
+ var NODE_PROPS = [
459
+ "color",
460
+ "font-family",
461
+ "font-size",
462
+ "background",
463
+ "text-align",
464
+ "justify-content",
465
+ "align-items"
466
+ ];
467
+ var ALIGN_JUSTIFY = {
468
+ left: "flex-start",
469
+ center: "center",
470
+ right: "flex-end"
471
+ };
363
472
  function saveInline(el, prop) {
364
473
  const attr = `data-ohw-style-prev-${prop}`;
365
474
  if (el.hasAttribute(attr)) return;
@@ -403,6 +512,10 @@ function clearNodeProps(root) {
403
512
  function buttonSurfaceOf(el) {
404
513
  return el.closest("a, button") ?? el;
405
514
  }
515
+ function alignSubjectOf(el) {
516
+ const button = el.closest('[data-ohw-role="button"]');
517
+ return button?.parentElement ?? el;
518
+ }
406
519
  function applyStylesToDom(store) {
407
520
  ensureStyleSheet();
408
521
  clearSectionAttrs(document);
@@ -448,6 +561,18 @@ function applyStylesToDom(store) {
448
561
  el.style.setProperty("font-size", `${override.fontSize}px`, "important");
449
562
  el.setAttribute(NODE_WROTE_ATTR, "");
450
563
  }
564
+ if (override.align !== void 0) {
565
+ const subject = alignSubjectOf(el);
566
+ saveInline(subject, "text-align");
567
+ saveInline(subject, "justify-content");
568
+ subject.style.setProperty("text-align", override.align, "important");
569
+ subject.style.setProperty(
570
+ "justify-content",
571
+ ALIGN_JUSTIFY[override.align] ?? "flex-start",
572
+ "important"
573
+ );
574
+ subject.setAttribute(NODE_WROTE_ATTR, "");
575
+ }
451
576
  if (override.buttonBackground !== void 0 || override.buttonText !== void 0) {
452
577
  const surface = buttonSurfaceOf(el);
453
578
  if (override.buttonBackground !== void 0) {
@@ -468,9 +593,494 @@ function applyStylesToDom(store) {
468
593
  import { flushSync } from "react-dom";
469
594
  import { createRoot } from "react-dom/client";
470
595
 
596
+ // src/lib/sections.ts
597
+ var LINK_PICKER_EXCLUDED_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
598
+ function isChromeSection(el) {
599
+ return el.matches("header, nav, footer, aside");
600
+ }
601
+ function titleCaseSectionId(id) {
602
+ return id.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
603
+ }
604
+ function parseSectionsFromRoot(root) {
605
+ const seen = /* @__PURE__ */ new Set();
606
+ const sections = [];
607
+ for (const el of root.querySelectorAll("[data-ohw-section]")) {
608
+ const id = el.getAttribute("data-ohw-section") ?? "";
609
+ if (!id || seen.has(id) || LINK_PICKER_EXCLUDED_IDS.has(id)) continue;
610
+ if (el.parentElement?.closest("[data-ohw-section]")) continue;
611
+ if (el.hasAttribute("data-ohw-ai-template-hidden") || el.hasAttribute("data-ohw-ai-removed") || el.hasAttribute("data-ohw-ai-replaced-by"))
612
+ continue;
613
+ seen.add(id);
614
+ const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
615
+ sections.push({ id, label });
616
+ }
617
+ return sections;
618
+ }
619
+ function collectSectionsFromDom() {
620
+ if (typeof document === "undefined") return [];
621
+ return parseSectionsFromRoot(document);
622
+ }
623
+ function parseSectionsFromHtml(html) {
624
+ const doc = new DOMParser().parseFromString(html, "text/html");
625
+ return parseSectionsFromRoot(doc);
626
+ }
627
+
628
+ // src/lib/section-instances.ts
629
+ var SECTION_ORDER_KEY = "__ohw_section_order";
630
+ var REMOVED_ATTR = "data-ohw-section-removed";
631
+ function isRemovedSection(el) {
632
+ return el.hasAttribute(REMOVED_ATTR);
633
+ }
634
+ function topLevelSections() {
635
+ return Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
636
+ (el) => !el.parentElement?.closest("[data-ohw-section]") && !isChromeSection(el) && !isRemovedSection(el)
637
+ );
638
+ }
639
+ function instanceIdOf(el) {
640
+ return el.dataset.ohwInstance ?? el.dataset.ohwSection ?? "";
641
+ }
642
+ function findByInstanceId(instanceId) {
643
+ const escapedId = CSS.escape(instanceId);
644
+ return document.querySelector(`[data-ohw-instance="${escapedId}"]`) ?? document.querySelector(`[data-ohw-section="${escapedId}"]:not([data-ohw-instance])`);
645
+ }
646
+ function planSectionMove(instanceId, targetIndex, currentPath) {
647
+ const sections = topLevelSections();
648
+ const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
649
+ if (index === -1) return null;
650
+ const dragged = sections[index];
651
+ const others = sections.filter((_, i) => i !== index);
652
+ const clamped = Math.max(0, Math.min(targetIndex, others.length));
653
+ const reordered = [...others.slice(0, clamped), dragged, ...others.slice(clamped)];
654
+ return reordered.map((el, order) => ({
655
+ instanceId: instanceIdOf(el),
656
+ type: el.getAttribute("data-ohw-section") ?? "",
657
+ order,
658
+ pagePath: currentPath
659
+ }));
660
+ }
661
+ function moveSectionInstance(instanceId, direction, currentPath) {
662
+ const sections = topLevelSections();
663
+ const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
664
+ if (index === -1) return null;
665
+ const siblingIndex = direction === "up" ? index - 1 : index + 1;
666
+ if (siblingIndex < 0 || siblingIndex >= sections.length) return null;
667
+ const entries = planSectionMove(instanceId, siblingIndex, currentPath);
668
+ if (!entries) return null;
669
+ applyPersistedOrder(entries);
670
+ return entries;
671
+ }
672
+ function syncRemovedFlags(entries) {
673
+ const removedIds = new Set(entries.filter((e) => e.removed).map((e) => e.instanceId));
674
+ document.querySelectorAll(`[${REMOVED_ATTR}]`).forEach((el) => {
675
+ if (!removedIds.has(instanceIdOf(el))) {
676
+ el.style.removeProperty("display");
677
+ el.removeAttribute(REMOVED_ATTR);
678
+ }
679
+ });
680
+ for (const id of removedIds) {
681
+ const el = findByInstanceId(id);
682
+ if (el) {
683
+ el.style.display = "none";
684
+ el.setAttribute(REMOVED_ATTR, "");
685
+ }
686
+ }
687
+ }
688
+ function applyPersistedOrder(entries) {
689
+ syncRemovedFlags(entries);
690
+ if (entries.length === 0) return;
691
+ const sections = topLevelSections();
692
+ if (sections.length === 0) return;
693
+ const orderIndex = new Map(entries.map((e) => [e.instanceId, e.order]));
694
+ const ordered = [...sections].sort((a, b) => {
695
+ const aOrder = orderIndex.get(instanceIdOf(a));
696
+ const bOrder = orderIndex.get(instanceIdOf(b));
697
+ if (aOrder === void 0 && bOrder === void 0) return 0;
698
+ if (aOrder === void 0) return 1;
699
+ if (bOrder === void 0) return -1;
700
+ return aOrder - bOrder;
701
+ });
702
+ let prev = null;
703
+ for (const el of ordered) {
704
+ if (prev) prev.after(el);
705
+ prev = el;
706
+ }
707
+ }
708
+ function setSectionRemoved(instanceId, currentPath, existingEntries, removed) {
709
+ if (!findByInstanceId(instanceId)) return null;
710
+ const byId = new Map(existingEntries.map((e) => [e.instanceId, e]));
711
+ const allSections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
712
+ (el) => !el.parentElement?.closest("[data-ohw-section]") && !isChromeSection(el)
713
+ );
714
+ allSections.forEach((el, order) => {
715
+ const id = instanceIdOf(el);
716
+ if (!byId.has(id)) {
717
+ byId.set(id, { instanceId: id, type: el.getAttribute("data-ohw-section") ?? "", order, pagePath: currentPath });
718
+ }
719
+ });
720
+ const target = byId.get(instanceId);
721
+ if (!target) return null;
722
+ byId.set(instanceId, { ...target, removed });
723
+ const entries = Array.from(byId.values());
724
+ applyPersistedOrder(entries);
725
+ return entries;
726
+ }
727
+ function deleteSectionInstance(instanceId, currentPath, existingEntries) {
728
+ return setSectionRemoved(instanceId, currentPath, existingEntries, true);
729
+ }
730
+ function restoreSectionInstance(instanceId, currentPath, existingEntries) {
731
+ return setSectionRemoved(instanceId, currentPath, existingEntries, false);
732
+ }
733
+ function duplicateSectionInstance(instanceId, newId, currentPath, existingEntries) {
734
+ const original = findByInstanceId(instanceId);
735
+ if (!original) return null;
736
+ const clone = original.cloneNode(true);
737
+ clone.setAttribute("data-ohw-instance", newId);
738
+ const keyRekeys = rekeySectionSubtree(clone, newId);
739
+ original.insertAdjacentElement("afterend", clone);
740
+ const byId = new Map(existingEntries.map((e) => [e.instanceId, e]));
741
+ const entries = topLevelSections().map((el, order) => {
742
+ const id = instanceIdOf(el);
743
+ return {
744
+ instanceId: id,
745
+ type: el.getAttribute("data-ohw-section") ?? "",
746
+ order,
747
+ pagePath: currentPath,
748
+ ...byId.get(id)?.removed ? { removed: true } : {}
749
+ };
750
+ });
751
+ applyPersistedOrder(entries);
752
+ return { entries, keyRekeys };
753
+ }
754
+ function newInstanceId() {
755
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `instance-${Date.now()}-${Math.random().toString(36).slice(2)}`;
756
+ }
757
+ function getPageSectionOrderEntries(raw, currentPath) {
758
+ if (!raw) return [];
759
+ try {
760
+ const entries = JSON.parse(raw);
761
+ return entries.filter((e) => !e.pagePath || e.pagePath === currentPath);
762
+ } catch {
763
+ return [];
764
+ }
765
+ }
766
+ function rekeySectionSubtree(root, instanceId) {
767
+ const suffix = `::${instanceId}`;
768
+ const pairs = [];
769
+ const rekey = (el, attr) => {
770
+ const current = el.getAttribute(attr);
771
+ if (!current) return;
772
+ const base = current.includes("::") ? current.slice(0, current.indexOf("::")) : current;
773
+ const next = `${base}${suffix}`;
774
+ el.setAttribute(attr, next);
775
+ pairs.push({ from: current, to: next });
776
+ };
777
+ if (root.hasAttribute("data-ohw-key")) rekey(root, "data-ohw-key");
778
+ if (root.hasAttribute("data-ohw-href-key")) rekey(root, "data-ohw-href-key");
779
+ root.querySelectorAll("[data-ohw-key]").forEach((el) => rekey(el, "data-ohw-key"));
780
+ root.querySelectorAll("[data-ohw-href-key]").forEach((el) => rekey(el, "data-ohw-href-key"));
781
+ return pairs;
782
+ }
783
+ function initSectionInstancesFromContent(content, currentPath) {
784
+ document.querySelectorAll("[data-ohw-section]:not([data-ohw-instance])").forEach((el) => {
785
+ el.setAttribute("data-ohw-instance", el.getAttribute("data-ohw-section") ?? "");
786
+ });
787
+ const entries = getPageSectionOrderEntries(content[SECTION_ORDER_KEY], currentPath);
788
+ for (const entry of entries) {
789
+ if (entry.instanceId === entry.type) continue;
790
+ if (document.querySelector(`[data-ohw-instance="${CSS.escape(entry.instanceId)}"]`)) continue;
791
+ const original = document.querySelector(
792
+ `[data-ohw-section="${CSS.escape(entry.type)}"][data-ohw-instance="${CSS.escape(entry.type)}"]`
793
+ );
794
+ if (!original) continue;
795
+ const clone = original.cloneNode(true);
796
+ clone.setAttribute("data-ohw-instance", entry.instanceId);
797
+ rekeySectionSubtree(clone, entry.instanceId);
798
+ original.insertAdjacentElement("afterend", clone);
799
+ }
800
+ applyPersistedOrder(entries);
801
+ }
802
+
471
803
  // src/ui/ai-tree/AiTreeRenderer.tsx
472
804
  import React from "react";
473
805
  import { ArrowLeft, ArrowRight, ChevronDown, icons as lucideIcons } from "lucide-react";
806
+
807
+ // src/lib/placeholder-imagery.ts
808
+ var U = (id) => `https://images.unsplash.com/photo-${id}?auto=format&fit=crop&w=1600&q=80`;
809
+ var GENERIC = [
810
+ U("1441986300917-64674bd600d8"),
811
+ U("1486406146926-c627a92ad1ab"),
812
+ U("1497032628192-86f99bcd76bc"),
813
+ U("1521737604893-d14cc237f11d"),
814
+ U("1522071820081-009f0129c71c"),
815
+ U("1519389950473-47ba0277781c"),
816
+ U("1460925895917-afdab827c52f"),
817
+ U("1504384308090-c894fdcc538d")
818
+ ];
819
+ var PEOPLE = [
820
+ U("1500648767791-00dcc994a43e"),
821
+ U("1494790108377-be9c29b29330"),
822
+ U("1507003211169-0a1dd7228f2d"),
823
+ U("1438761681033-6461ffad8d80"),
824
+ U("1544005313-94ddf0286df2"),
825
+ U("1472099645785-5658abf4ff4e"),
826
+ U("1519085360753-af0119f7cbe7"),
827
+ U("1534528741775-53994a69daeb")
828
+ ];
829
+ var THEMED = [
830
+ {
831
+ keywords: ["portrait", "headshot", "person", "people", "team", "staff", "avatar", "founder", "face"],
832
+ pool: PEOPLE
833
+ },
834
+ {
835
+ keywords: ["pet", "dog", "cat", "puppy", "kitten", "vet", "animal"],
836
+ pool: [
837
+ U("1548199973-03cce0bbc87b"),
838
+ U("1450778869180-41d0601e046e"),
839
+ U("1583511655857-d19b40a7a54e"),
840
+ U("1587300003388-59208cc962cb"),
841
+ U("1517849845537-4d257902454a"),
842
+ U("1601758228041-f3b2795255f1")
843
+ ]
844
+ },
845
+ {
846
+ keywords: [
847
+ "baker",
848
+ "bakery",
849
+ "cafe",
850
+ "coffee",
851
+ "latte",
852
+ "restaurant",
853
+ "pastr",
854
+ "bread",
855
+ "cake",
856
+ "cater",
857
+ "chef",
858
+ "kitchen",
859
+ "food",
860
+ "pizza",
861
+ "dessert",
862
+ "brunch",
863
+ "bistro",
864
+ "deli",
865
+ "dish",
866
+ "menu"
867
+ ],
868
+ pool: [
869
+ U("1509440159596-0249088772ff"),
870
+ U("1555507036-ab1f4038808a"),
871
+ U("1517433670267-08bbd4be890f"),
872
+ U("1486427944299-d1955d23e34d"),
873
+ U("1504754524776-8f4f37790ca0"),
874
+ U("1495474472287-4d71bcdd2085"),
875
+ U("1521017432531-fbd92d768814"),
876
+ U("1556909114-f6e7ad7d3136")
877
+ ]
878
+ },
879
+ {
880
+ keywords: [
881
+ "shop",
882
+ "store",
883
+ "boutique",
884
+ "retail",
885
+ "clothing",
886
+ "fashion",
887
+ "jewel",
888
+ "gift",
889
+ "florist",
890
+ "market",
891
+ "grocer",
892
+ "product",
893
+ "storefront"
894
+ ],
895
+ pool: [
896
+ U("1441984904996-e0b6ba687e04"),
897
+ U("1472851294608-062f824d29cc"),
898
+ U("1523381210434-271e8be1f52b"),
899
+ U("1534452203293-494d7ddbf7e0"),
900
+ U("1445205170230-053b83016050"),
901
+ U("1560243563-062bfc001d68")
902
+ ]
903
+ },
904
+ {
905
+ keywords: [
906
+ "yoga",
907
+ "pilates",
908
+ "fitness",
909
+ "gym",
910
+ "workout",
911
+ "trainer",
912
+ "wellness",
913
+ "meditat",
914
+ "massage",
915
+ "therap",
916
+ "physio",
917
+ "chiro",
918
+ "nutrition",
919
+ "spa",
920
+ "studio"
921
+ ],
922
+ pool: [
923
+ U("1544367567-0f2fcb009e0b"),
924
+ U("1506126613408-eca07ce68773"),
925
+ U("1545205597-3d9d02c29597"),
926
+ U("1552196563-55cd4e45efb3"),
927
+ U("1518611012118-696072aa579a"),
928
+ U("1571019613454-1cb2f99b2d8b"),
929
+ U("1540555700478-4be289fbecef"),
930
+ U("1519824145371-296894a0daa9")
931
+ ]
932
+ },
933
+ {
934
+ keywords: [
935
+ "salon",
936
+ "hairdress",
937
+ "haircut",
938
+ "barber",
939
+ "manicure",
940
+ "pedicure",
941
+ "nails",
942
+ "beauty",
943
+ "makeup",
944
+ "cosmetic",
945
+ "eyelash",
946
+ "eyebrow",
947
+ "skincare",
948
+ "esthetic",
949
+ "waxing",
950
+ "hair"
951
+ ],
952
+ pool: [
953
+ U("1560066984-138dadb4c035"),
954
+ U("1522337660859-02fbefca4702"),
955
+ U("1562322140-8baeececf3df"),
956
+ U("1521590832167-7bcbfaa6381f"),
957
+ U("1487412947147-5cebf100ffc2"),
958
+ U("1526045478516-99145907023c")
959
+ ]
960
+ },
961
+ {
962
+ keywords: [
963
+ "cleaning",
964
+ "plumb",
965
+ "electric",
966
+ "landscap",
967
+ "contractor",
968
+ "handyman",
969
+ "renov",
970
+ "hvac",
971
+ "roofing",
972
+ "painting",
973
+ "carpentry",
974
+ "flooring",
975
+ "movers",
976
+ "construction",
977
+ "tools"
978
+ ],
979
+ pool: [
980
+ U("1581578731548-c64695cc6952"),
981
+ U("1504307651254-35680f356dfd"),
982
+ U("1581092160562-40aa08e78837"),
983
+ U("1621905251189-08b45d6a269e"),
984
+ U("1558618666-fcd25c85cd64"),
985
+ U("1585128792020-803d29415281")
986
+ ]
987
+ },
988
+ {
989
+ keywords: [
990
+ "legal",
991
+ "attorney",
992
+ "lawyer",
993
+ "account",
994
+ "bookkeep",
995
+ "consult",
996
+ "coaching",
997
+ "financ",
998
+ "insurance",
999
+ "realtor",
1000
+ "estate",
1001
+ "marketing",
1002
+ "agency",
1003
+ "office",
1004
+ "business",
1005
+ "desk"
1006
+ ],
1007
+ pool: [
1008
+ U("1497366216548-37526070297c"),
1009
+ U("1497366811353-6870744d04b2"),
1010
+ U("1454165804606-c3d57bc86b40"),
1011
+ U("1521791136064-7986c2920216"),
1012
+ U("1556761175-b413da4baf72"),
1013
+ U("1542744173-8e7e53415bb0")
1014
+ ]
1015
+ },
1016
+ {
1017
+ keywords: [
1018
+ "wedding",
1019
+ "event",
1020
+ "party",
1021
+ "celebrat",
1022
+ "venue",
1023
+ "community",
1024
+ "nonprofit",
1025
+ "charity",
1026
+ "workshop",
1027
+ "photograph",
1028
+ "concert"
1029
+ ],
1030
+ pool: [
1031
+ U("1511578314322-379afb476865"),
1032
+ U("1501281668745-f7f57925c3b4"),
1033
+ U("1523580494863-6f3031224c94"),
1034
+ U("1540575467063-178a50c2df87"),
1035
+ U("1505236858219-8359eb29e329"),
1036
+ U("1528605248644-14dd04022da1")
1037
+ ]
1038
+ }
1039
+ ];
1040
+ function poolForSubject(subject) {
1041
+ for (const theme of THEMED) {
1042
+ if (theme.keywords.some((k) => subject.includes(k))) {
1043
+ return theme.pool;
1044
+ }
1045
+ }
1046
+ return GENERIC;
1047
+ }
1048
+ function mixedHash(text) {
1049
+ let hash = 2166136261;
1050
+ for (let i = 0; i < text.length; i++) {
1051
+ hash ^= text.charCodeAt(i);
1052
+ hash = Math.imul(hash, 16777619);
1053
+ }
1054
+ return hash >>> 16 & 65535;
1055
+ }
1056
+ function resolvePlaceholderRef(ref) {
1057
+ const match = /^placeholder:([a-z0-9-]+)$/.exec(ref);
1058
+ if (!match) return null;
1059
+ const subject = match[1];
1060
+ const pool = poolForSubject(subject.replace(/-\d+$/, ""));
1061
+ return pool[mixedHash(ref) % pool.length];
1062
+ }
1063
+ function collectPlaceholderRefs(tree) {
1064
+ const seen = /* @__PURE__ */ new Set();
1065
+ for (const match of JSON.stringify(tree ?? null).matchAll(/"(placeholder:[a-z0-9-]+)"/gu)) {
1066
+ seen.add(match[1]);
1067
+ }
1068
+ return [...seen];
1069
+ }
1070
+ function buildPlaceholderMap(tree) {
1071
+ const map = {};
1072
+ const cursor = /* @__PURE__ */ new Map();
1073
+ for (const ref of collectPlaceholderRefs(tree)) {
1074
+ const subject = ref.slice("placeholder:".length).replace(/-\d+$/, "");
1075
+ const pool = poolForSubject(subject);
1076
+ const start = cursor.get(pool) ?? mixedHash(ref) % pool.length;
1077
+ map[ref] = pool[start % pool.length];
1078
+ cursor.set(pool, start + 1);
1079
+ }
1080
+ return map;
1081
+ }
1082
+
1083
+ // src/ui/ai-tree/AiTreeRenderer.tsx
474
1084
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
475
1085
  function lucideByName(name) {
476
1086
  const pascal = name.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
@@ -484,6 +1094,7 @@ var typeStyle = (spec, font) => ({
484
1094
  fontWeight: spec.weight
485
1095
  });
486
1096
  var str = (value) => typeof value === "string" ? value : "";
1097
+ var cardRadius = (slots) => slots.cornerStyle === "sharp" ? 0 : AI_TREE_TOKENS.radiusCard;
487
1098
  var featureLines = (value) => value.split(/\n|<br\s*\/?>/i).map((line) => line.trim()).filter(Boolean);
488
1099
  var CHECK_MASK = `data:image/svg+xml,${encodeURIComponent(
489
1100
  '<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>'
@@ -543,6 +1154,22 @@ function accentBandContext(brand) {
543
1154
  function textAttrs(ctx, path) {
544
1155
  return ctx.keyFor ? { "data-ohw-key": ctx.keyFor(path), "data-ohw-editable": "text" } : {};
545
1156
  }
1157
+ var AI_RESPONSIVE_CSS = [
1158
+ "@media (max-width: 960px) {",
1159
+ " [data-ai-responsive] [data-ai-grid] { grid-template-columns: repeat(2, 1fr) !important; }",
1160
+ ' [data-ai-responsive] [data-ai-grid="1"] { grid-template-columns: 1fr !important; }',
1161
+ "}",
1162
+ "@media (max-width: 640px) {",
1163
+ " [data-ai-responsive] [data-ai-grid] { grid-template-columns: 1fr !important; }",
1164
+ " [data-ai-responsive] [data-ai-columns] > * { grid-column: 1 / -1 !important; }",
1165
+ // Group containers flatten to a column on phones; span placements come along for free.
1166
+ " [data-ai-responsive] [data-ai-group] { display: flex !important; flex-direction: column !important; }",
1167
+ " [data-ai-responsive] [data-ai-group] > * { grid-column: auto !important; }",
1168
+ " [data-ai-responsive] [data-ai-section-inner] { padding-left: 20px !important; padding-right: 20px !important; }",
1169
+ " [data-ai-responsive] { overflow-x: hidden; }",
1170
+ " [data-ai-responsive] img { max-width: 100%; }",
1171
+ "}"
1172
+ ].join("\n");
546
1173
  var TRANSPARENT_PX = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
547
1174
  function MediaBox({
548
1175
  refValue,
@@ -555,13 +1182,17 @@ function MediaBox({
555
1182
  const url = refValue ? ctx.resolveMedia(refValue) : null;
556
1183
  const isIcon = /^(lucide|simple):/.test(refValue);
557
1184
  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" } : {};
1185
+ const editAttrs = ctx.keyFor && editPath ? {
1186
+ "data-ohw-key": ctx.keyFor(editPath),
1187
+ "data-ohw-editable": isIcon ? "icon" : "image"
1188
+ } : {};
559
1189
  if (isIcon) {
560
1190
  const Icon = refValue.startsWith("lucide:") ? lucideByName(refValue.slice(7)) : null;
561
1191
  return /* @__PURE__ */ jsx(
562
1192
  "span",
563
1193
  {
564
1194
  "data-ai-icon": refValue,
1195
+ ...editAttrs,
565
1196
  style: {
566
1197
  display: "inline-flex",
567
1198
  width: 48,
@@ -656,7 +1287,7 @@ function TextBlock({ slots, ctx, path }) {
656
1287
  }
657
1288
  function SectionHeaderBlock({ node, ctx, path }) {
658
1289
  const slots = node.slots ?? {};
659
- const align = slots.alignment === "center" ? "center" : "left";
1290
+ const align = node.align ?? (slots.alignment === "center" ? "center" : "left");
660
1291
  const children = node.children ?? [];
661
1292
  const buttonRowIdx = children.findIndex((c) => c.type === "button-row");
662
1293
  const buttonRow = buttonRowIdx >= 0 ? children[buttonRowIdx] : void 0;
@@ -700,7 +1331,7 @@ function SectionHeaderBlock({ node, ctx, path }) {
700
1331
  display: "flex",
701
1332
  gap: AI_TREE_TOKENS.spacing6,
702
1333
  marginTop: AI_TREE_TOKENS.spacing8,
703
- justifyContent: align === "center" ? "center" : "flex-start"
1334
+ justifyContent: align === "center" ? "center" : align === "right" ? "flex-end" : "flex-start"
704
1335
  },
705
1336
  children: (buttonRow.children ?? []).map((button, i) => /* @__PURE__ */ jsx(
706
1337
  ButtonEl,
@@ -806,10 +1437,11 @@ function PricingCard({ node, ctx, path }) {
806
1437
  return /* @__PURE__ */ jsxs(
807
1438
  "div",
808
1439
  {
1440
+ "data-ohw-card": "",
809
1441
  style: {
810
1442
  background: hasBg ? ctx.brand.palette.light : "transparent",
811
1443
  border: `1px solid ${dark}`,
812
- borderRadius: AI_TREE_TOKENS.radiusCard,
1444
+ borderRadius: cardRadius(slots),
813
1445
  padding: AI_TREE_TOKENS.paddingBlock,
814
1446
  display: "flex",
815
1447
  flexDirection: "column",
@@ -914,10 +1546,11 @@ function TestimonialCard({ node, ctx, path }) {
914
1546
  return /* @__PURE__ */ jsx(
915
1547
  "div",
916
1548
  {
1549
+ "data-ohw-card": "",
917
1550
  "data-ai-avatar-pos": avatarPos ?? void 0,
918
1551
  style: {
919
1552
  background: hasBg ? ctx.cardSurface : "transparent",
920
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1553
+ borderRadius: hasBg ? cardRadius(slots) : 0,
921
1554
  overflow: "hidden",
922
1555
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
923
1556
  minWidth: 0
@@ -952,10 +1585,11 @@ function TeamCard({ node, ctx, path }) {
952
1585
  return /* @__PURE__ */ jsxs(
953
1586
  "div",
954
1587
  {
1588
+ "data-ohw-card": "",
955
1589
  "data-ai-avatar-pos": avatarPos ?? void 0,
956
1590
  style: {
957
1591
  background: hasBg ? ctx.cardSurface : "transparent",
958
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1592
+ borderRadius: hasBg ? cardRadius(slots) : 0,
959
1593
  overflow: "hidden",
960
1594
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
961
1595
  minWidth: 0,
@@ -1033,7 +1667,7 @@ function CardBlock({ node, ctx, path }) {
1033
1667
  editPath: `${path}.media`
1034
1668
  }
1035
1669
  ) : null;
1036
- const centered = slots.alignment === "center";
1670
+ const centered = (node.align ?? slots.alignment) === "center";
1037
1671
  const content = /* @__PURE__ */ jsxs(
1038
1672
  "div",
1039
1673
  {
@@ -1126,9 +1760,10 @@ function CardBlock({ node, ctx, path }) {
1126
1760
  return /* @__PURE__ */ jsxs(
1127
1761
  "div",
1128
1762
  {
1763
+ "data-ohw-card": "",
1129
1764
  style: {
1130
1765
  background: hasBg ? ctx.cardSurface : "transparent",
1131
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1766
+ borderRadius: hasBg ? cardRadius(slots) : 0,
1132
1767
  overflow: "hidden",
1133
1768
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
1134
1769
  display: horizontal ? "flex" : "block",
@@ -1156,7 +1791,7 @@ function CardBlock({ node, ctx, path }) {
1156
1791
  ) : /* @__PURE__ */ jsx(
1157
1792
  "div",
1158
1793
  {
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" },
1794
+ 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
1795
  children: media
1161
1796
  }
1162
1797
  )),
@@ -1447,7 +2082,7 @@ function CollectionBlock({ node, ctx, path }) {
1447
2082
  return /* @__PURE__ */ jsx(
1448
2083
  "div",
1449
2084
  {
1450
- "data-ai-grid": "",
2085
+ "data-ai-grid": String(itemsPerRow),
1451
2086
  style: {
1452
2087
  display: "grid",
1453
2088
  gridTemplateColumns: `repeat(${itemsPerRow}, 1fr)`,
@@ -1567,6 +2202,32 @@ function renderNode(node, ctx, path) {
1567
2202
  if (child) {
1568
2203
  return renderNode(child, ctx, `${path}.c0`);
1569
2204
  }
2205
+ if (str(slots.provider) === "map" && str(slots.query)) {
2206
+ const query = str(slots.query);
2207
+ const mapAttrs = ctx.keyFor ? {
2208
+ "data-ohw-key": ctx.keyFor(`${path}.query`),
2209
+ "data-ohw-editable": "map",
2210
+ "data-ohw-map-query": query
2211
+ } : {};
2212
+ return /* @__PURE__ */ jsx(
2213
+ "iframe",
2214
+ {
2215
+ ...mapAttrs,
2216
+ "data-ai-embed": "map",
2217
+ title: str(slots.title) || "Map",
2218
+ src: `https://www.google.com/maps?q=${encodeURIComponent(query)}&output=embed`,
2219
+ loading: "lazy",
2220
+ referrerPolicy: "no-referrer-when-downgrade",
2221
+ style: {
2222
+ width: "100%",
2223
+ minHeight: 320,
2224
+ border: 0,
2225
+ borderRadius: AI_TREE_TOKENS.radiusCard,
2226
+ display: "block"
2227
+ }
2228
+ }
2229
+ );
2230
+ }
1570
2231
  return /* @__PURE__ */ jsx(
1571
2232
  "div",
1572
2233
  {
@@ -1723,9 +2384,14 @@ function AiTreeRenderer({
1723
2384
  const resolvedBrand = brand ?? AI_DEFAULT_BRAND;
1724
2385
  const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
1725
2386
  const blockBrand = band?.brand ?? resolvedBrand;
2387
+ const placeholderMap = buildPlaceholderMap(tree);
1726
2388
  const ctx = {
1727
2389
  brand: blockBrand,
1728
- resolveMedia: resolveMedia ?? (() => null),
2390
+ // An owner/library ref resolves through the host resolver; a `placeholder:<subject>` ref the
2391
+ // host cannot resolve falls back to real stock photography (the per-section map first, then a
2392
+ // standalone resolve), so generated galleries, image rows, and overlay backgrounds arrive with
2393
+ // photos instead of grey boxes.
2394
+ resolveMedia: (ref) => resolveMedia?.(ref) ?? placeholderMap[ref] ?? resolvePlaceholderRef(ref),
1729
2395
  cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
1730
2396
  keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
1731
2397
  sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
@@ -1752,11 +2418,25 @@ function AiTreeRenderer({
1752
2418
  }
1753
2419
  })();
1754
2420
  const distributed = !isOverlay && settings.textDistribution;
2421
+ const rowAlignItems = (rowAlign) => {
2422
+ if (rowAlign === "top") return "start";
2423
+ if (rowAlign === "bottom") return "end";
2424
+ if (rowAlign === "center" || rowAlign === "stretch") return rowAlign;
2425
+ if (distributed === "space-between") return "stretch";
2426
+ return (distributed ?? settings.verticalPosition) === "top" ? "start" : "center";
2427
+ };
2428
+ const cellAlignStyle = (blockAlign) => blockAlign ? {
2429
+ display: "flex",
2430
+ flexDirection: "column",
2431
+ alignItems: blockAlign === "left" ? "flex-start" : blockAlign === "right" ? "flex-end" : "center",
2432
+ textAlign: blockAlign
2433
+ } : {};
1755
2434
  return /* @__PURE__ */ jsxs(
1756
2435
  "section",
1757
2436
  {
1758
2437
  "data-ai-section": tree.tag ?? "",
1759
2438
  ...bgAttrs,
2439
+ "data-ai-responsive": "",
1760
2440
  style: {
1761
2441
  position: "relative",
1762
2442
  padding: `${pad}px 0`,
@@ -1767,12 +2447,13 @@ function AiTreeRenderer({
1767
2447
  color: settings.sectionBackground === "accent" ? blockBrand.palette.dark : void 0
1768
2448
  },
1769
2449
  children: [
1770
- isOverlay && backgroundUrl && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
2450
+ /* @__PURE__ */ jsx("style", { children: AI_RESPONSIVE_CSS }),
1771
2451
  /* @__PURE__ */ jsx("style", { children: AI_MOBILE_CSS }),
2452
+ isOverlay && backgroundUrl && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
1772
2453
  /* @__PURE__ */ jsx(
1773
2454
  "div",
1774
2455
  {
1775
- "data-ai-container": "",
2456
+ "data-ai-section-inner": "",
1776
2457
  style: {
1777
2458
  position: "relative",
1778
2459
  maxWidth: AI_TREE_TOKENS.sectionMaxWidth,
@@ -1783,12 +2464,12 @@ function AiTreeRenderer({
1783
2464
  children: tree.rows.map((row, r2) => /* @__PURE__ */ jsx(
1784
2465
  "div",
1785
2466
  {
1786
- "data-ai-row": "",
2467
+ "data-ai-columns": "",
1787
2468
  style: {
1788
2469
  display: "grid",
1789
2470
  gridTemplateColumns: "repeat(12, 1fr)",
1790
2471
  gap: AI_TREE_TOKENS.spacing6,
1791
- alignItems: distributed === "space-between" ? "stretch" : (distributed ?? settings.verticalPosition) === "top" ? "start" : "center",
2472
+ alignItems: rowAlignItems(row.align),
1792
2473
  marginTop: r2 > 0 ? AI_TREE_TOKENS.spacing8 : 0
1793
2474
  },
1794
2475
  children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ jsx(
@@ -1798,6 +2479,8 @@ function AiTreeRenderer({
1798
2479
  style: {
1799
2480
  gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`,
1800
2481
  minWidth: 0,
2482
+ // Horizontal placement of the block's content within its column.
2483
+ ...cellAlignStyle(block.align),
1801
2484
  // space-between: each column becomes a flex column whose content spreads over
1802
2485
  // the full row height instead of clumping at the top.
1803
2486
  ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
@@ -1820,7 +2503,7 @@ function AiTreeRenderer({
1820
2503
  import { jsx as jsx2 } from "react/jsx-runtime";
1821
2504
  var CONTAINER_ATTR = "data-ohw-ai-generated";
1822
2505
  var REPLACED_ATTR = "data-ohw-ai-replaced-by";
1823
- var REMOVED_ATTR = "data-ohw-ai-removed";
2506
+ var REMOVED_ATTR2 = "data-ohw-ai-removed";
1824
2507
  var TEMPLATE_HIDDEN_ATTR = "data-ohw-ai-template-hidden";
1825
2508
  var CHROME_SECTION_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
1826
2509
  function readRootVar(name) {
@@ -1914,18 +2597,18 @@ function placeContainer(container, entry) {
1914
2597
  }
1915
2598
  function syncRemovedSections(state) {
1916
2599
  const removed = new Set(state.removed ?? []);
1917
- for (const el of document.querySelectorAll(`[${REMOVED_ATTR}]`)) {
2600
+ for (const el of document.querySelectorAll(`[${REMOVED_ATTR2}]`)) {
1918
2601
  const id = el.getAttribute("data-ohw-section") ?? "";
1919
2602
  if (!removed.has(id)) {
1920
2603
  el.style.removeProperty("display");
1921
- el.removeAttribute(REMOVED_ATTR);
2604
+ el.removeAttribute(REMOVED_ATTR2);
1922
2605
  }
1923
2606
  }
1924
2607
  for (const id of removed) {
1925
2608
  const section = findTemplateSection(id);
1926
2609
  if (section && !section.hasAttribute(REPLACED_ATTR)) {
1927
2610
  section.style.display = "none";
1928
- section.setAttribute(REMOVED_ATTR, "");
2611
+ section.setAttribute(REMOVED_ATTR2, "");
1929
2612
  }
1930
2613
  }
1931
2614
  }
@@ -1942,7 +2625,7 @@ function syncTemplateHidden(state, pageHasSections) {
1942
2625
  if (el.hasAttribute(CONTAINER_ATTR)) continue;
1943
2626
  if (CHROME_SECTION_IDS.has(el.getAttribute("data-ohw-section") ?? "")) continue;
1944
2627
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
1945
- if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR)) continue;
2628
+ if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR2)) continue;
1946
2629
  el.style.display = "none";
1947
2630
  el.setAttribute(TEMPLATE_HIDDEN_ATTR, "");
1948
2631
  }
@@ -1966,18 +2649,23 @@ function syncReplacedOriginals(state) {
1966
2649
  }
1967
2650
  }
1968
2651
  var sectionOrderIndex = /* @__PURE__ */ new Map();
2652
+ var removedSectionIds = /* @__PURE__ */ new Set();
1969
2653
  function setAiSectionOrder(raw, currentPath) {
1970
2654
  const next = /* @__PURE__ */ new Map();
2655
+ const removed = /* @__PURE__ */ new Set();
1971
2656
  if (raw) {
1972
2657
  try {
1973
2658
  const entries = JSON.parse(raw);
1974
2659
  for (const entry of entries) {
1975
- if (!entry.pagePath || entry.pagePath === currentPath) next.set(entry.instanceId, entry.order);
2660
+ if (entry.pagePath && entry.pagePath !== currentPath) continue;
2661
+ next.set(entry.instanceId, entry.order);
2662
+ if (entry.removed) removed.add(entry.instanceId);
1976
2663
  }
1977
2664
  } catch {
1978
2665
  }
1979
2666
  }
1980
2667
  sectionOrderIndex = next;
2668
+ removedSectionIds = removed;
1981
2669
  }
1982
2670
  function applyExplicitOrder(entries) {
1983
2671
  if (sectionOrderIndex.size === 0) return entries;
@@ -2013,6 +2701,18 @@ function orderByChain(sections) {
2013
2701
  for (const root of roots) visit(root);
2014
2702
  return out.length === sections.length ? out : sections;
2015
2703
  }
2704
+ function syncSoftRemovedGenerated() {
2705
+ for (const [id, section] of mounted) {
2706
+ const el = section.container;
2707
+ if (removedSectionIds.has(id)) {
2708
+ el.style.display = "none";
2709
+ el.setAttribute(REMOVED_ATTR, "");
2710
+ } else if (el.hasAttribute(REMOVED_ATTR)) {
2711
+ el.style.removeProperty("display");
2712
+ el.removeAttribute(REMOVED_ATTR);
2713
+ }
2714
+ }
2715
+ }
2016
2716
  function applyAiSectionsToDom(state, options) {
2017
2717
  if (typeof document === "undefined") return;
2018
2718
  const brandOverride = deriveBrandOverride();
@@ -2080,6 +2780,7 @@ function applyAiSectionsToDom(state, options) {
2080
2780
  syncReplacedOriginals(state);
2081
2781
  syncRemovedSections(state);
2082
2782
  syncTemplateHidden(state, pageSections.length > 0);
2783
+ syncSoftRemovedGenerated();
2083
2784
  }
2084
2785
 
2085
2786
  // src/useLinkHrefGuardian.ts
@@ -7809,6 +8510,7 @@ function MediaOverlay({
7809
8510
  (prev) => prev && prev.fullWidth === width && prev.height === height ? prev : { fullWidth: width, height }
7810
8511
  );
7811
8512
  }, [isVideo]);
8513
+ const replaceLabel = isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image";
7812
8514
  const replaceMode = buttonSize ? replaceButtonMode({ width: rect.width, height: rect.height }, buttonSize) : "none";
7813
8515
  const box = {
7814
8516
  position: "fixed",
@@ -7938,17 +8640,17 @@ function MediaOverlay({
7938
8640
  },
7939
8641
  children: [
7940
8642
  isVideo ? /* @__PURE__ */ jsx15(Film, { size: 14 }) : /* @__PURE__ */ jsx15(ImageIcon, { size: 14 }),
7941
- isVideo ? "Replace video" : "Replace image"
8643
+ replaceLabel
7942
8644
  ]
7943
8645
  }
7944
8646
  ),
7945
- replaceMode === "none" ? null : /* @__PURE__ */ jsxs7(
8647
+ showChrome && replaceMode !== "none" && /* @__PURE__ */ jsxs7(
7946
8648
  Button,
7947
8649
  {
7948
8650
  "data-ohw-media-overlay": "",
7949
8651
  variant: "outline",
7950
8652
  size: "sm",
7951
- "aria-label": isVideo ? "Replace video" : "Replace image",
8653
+ "aria-label": replaceLabel,
7952
8654
  className: "gap-1.5 cursor-pointer hover:bg-background",
7953
8655
  style: {
7954
8656
  ...OVERLAY_BUTTON_STYLE,
@@ -7971,7 +8673,7 @@ function MediaOverlay({
7971
8673
  },
7972
8674
  children: [
7973
8675
  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
8676
+ replaceMode === "full" ? replaceLabel : null
7975
8677
  ]
7976
8678
  }
7977
8679
  )
@@ -8008,219 +8710,37 @@ function CarouselOverlay({
8008
8710
  width: rect.width,
8009
8711
  height: rect.height,
8010
8712
  zIndex: 2147483646,
8011
- pointerEvents: "auto",
8012
- boxShadow: "inset 0 0 0 1.5px var(--color-primary)",
8013
- background: "color-mix(in srgb, var(--color-primary) 20%, transparent)"
8014
- },
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, "");
8713
+ pointerEvents: "auto",
8714
+ boxShadow: "inset 0 0 0 1.5px var(--color-primary)",
8715
+ background: "color-mix(in srgb, var(--color-primary) 20%, transparent)"
8716
+ },
8717
+ onClick: () => onEdit(hover.key),
8718
+ children: /* @__PURE__ */ jsxs8(
8719
+ Button,
8720
+ {
8721
+ "data-ohw-carousel-overlay": "",
8722
+ variant: "outline",
8723
+ size: "sm",
8724
+ className: "cursor-pointer gap-1.5 hover:bg-background",
8725
+ style: OVERLAY_BUTTON_STYLE2,
8726
+ onMouseDown: (e) => e.preventDefault(),
8727
+ onClick: (e) => {
8728
+ e.stopPropagation();
8729
+ onEdit(hover.key);
8730
+ },
8731
+ children: [
8732
+ /* @__PURE__ */ jsx16(GalleryHorizontal, { size: 14 }),
8733
+ "Edit gallery"
8734
+ ]
8735
+ }
8736
+ )
8132
8737
  }
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
8738
  );
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
8739
  }
8222
8740
 
8223
8741
  // src/ui/ai-section/AiSectionOverlay.tsx
8742
+ import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
8743
+ import { Check, X } from "lucide-react";
8224
8744
  import { Fragment as Fragment5, jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
8225
8745
  function findSectionElement(instanceId) {
8226
8746
  const escaped = CSS.escape(instanceId);
@@ -12936,6 +13456,7 @@ function readLogoSizeState(content, placement) {
12936
13456
  function getLogoElement(el) {
12937
13457
  const marked = el.closest('[data-ohw-role="logo"], [data-ohw-logo]');
12938
13458
  if (marked) return marked;
13459
+ if (el.closest('[data-ohw-editable="icon"]')) return null;
12939
13460
  const root = el.closest("nav, [data-ohw-nav-root], footer");
12940
13461
  if (!root) return null;
12941
13462
  const anchor = el.closest("a");
@@ -14317,6 +14838,9 @@ function collectEditableNodes(extraContent, root = document) {
14317
14838
  if (el.dataset.ohwEditable === "link") {
14318
14839
  return { key: el.dataset.ohwKey ?? "", type: "link", text: getLinkHref3(el) };
14319
14840
  }
14841
+ if (el.dataset.ohwEditable === "map") {
14842
+ return { key: el.dataset.ohwKey ?? "", type: "map", text: el.dataset.ohwMapQuery ?? "" };
14843
+ }
14320
14844
  return {
14321
14845
  key: el.dataset.ohwKey ?? "",
14322
14846
  type: el.dataset.ohwEditable ?? "text",
@@ -14870,21 +15394,10 @@ function parseSchedulingInsertAfter(insertAfter) {
14870
15394
  insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
14871
15395
  };
14872
15396
  }
14873
- function resolveSchedulingInsert(insertAfter, explicitInsertBefore) {
14874
- const parsed = parseSchedulingInsertAfter(insertAfter);
14875
- const insertBefore = explicitInsertBefore ?? parsed.insertBefore;
14876
- const effectiveInsertAfter = insertBefore && parsed.insertBefore === null ? `${insertAfter}${INSERT_BEFORE_MARKER}${insertBefore}` : insertAfter;
14877
- return { effectiveInsertAfter, insertBefore };
14878
- }
14879
- function getSchedulingMountPoint(insertAfter) {
14880
- const { anchor } = parseSchedulingInsertAfter(insertAfter);
14881
- let anchorEl = document.querySelector(`[data-ohw-section="${anchor}"]`);
14882
- if (!anchorEl && anchor === "scheduling") {
14883
- const widgets = Array.from(document.querySelectorAll('[data-ohw-section^="scheduling-"]'));
14884
- anchorEl = widgets.at(-1) ?? null;
14885
- }
14886
- if (!anchorEl) return null;
14887
- return anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
15397
+ function resolveEntryAnchor(entry) {
15398
+ if (entry.anchorId !== void 0) return { anchorId: entry.anchorId, beforeId: entry.beforeId ?? null };
15399
+ const parsed = parseSchedulingInsertAfter(entry.insertAfter);
15400
+ return { anchorId: parsed.anchor, beforeId: parsed.insertBefore };
14888
15401
  }
14889
15402
  function schedulingMountDepth(insertAfter) {
14890
15403
  if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
@@ -14901,8 +15414,7 @@ function getPageSchedulingEntries(raw) {
14901
15414
  }
14902
15415
  }
14903
15416
  function isSchedulingWidgetMissing(entry) {
14904
- const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
14905
- return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
15417
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(entry.insertAfter)}"]`);
14906
15418
  }
14907
15419
  function hasMissingSchedulingWidgets(entries) {
14908
15420
  return entries.some(isSchedulingWidgetMissing);
@@ -14932,16 +15444,17 @@ function initSectionsFromContent(content, removeExisting = false) {
14932
15444
  } catch {
14933
15445
  }
14934
15446
  }
14935
- function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
14936
- const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
14937
- const sectionId = schedulingSectionId(effectiveInsertAfter);
15447
+ function mountSchedulingWidget(anchorId, notifyOnConnect = false, scheduleId, beforeId, existingWidgetId) {
15448
+ const widgetId = existingWidgetId ?? (beforeId ? `${anchorId}${INSERT_BEFORE_MARKER}${beforeId}` : anchorId);
15449
+ const sectionId = schedulingSectionId(widgetId);
14938
15450
  if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
14939
- const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
14940
- if (!mountPoint) return false;
15451
+ const anchorEl = document.querySelector(`[data-ohw-section="${anchorId}"]`);
15452
+ if (!anchorEl) return false;
15453
+ const mountPoint = anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
14941
15454
  const container = document.createElement("div");
14942
15455
  container.dataset.ohwSectionContainer = "scheduling";
14943
- if (insertBefore) {
14944
- const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
15456
+ if (beforeId) {
15457
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${beforeId}"]`);
14945
15458
  const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
14946
15459
  if (!beforePoint) return false;
14947
15460
  beforePoint.insertAdjacentElement("beforebegin", container);
@@ -14952,19 +15465,25 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
14952
15465
  }
14953
15466
  tail.insertAdjacentElement("afterend", container);
14954
15467
  }
14955
- const root = createRoot2(container);
14956
- flushSync2(() => {
14957
- root.render(
14958
- /* @__PURE__ */ jsx33(
14959
- SchedulingWidget,
14960
- {
14961
- notifyOnConnect,
14962
- initialScheduleId: scheduleId,
14963
- insertAfter: effectiveInsertAfter
14964
- }
14965
- )
14966
- );
14967
- });
15468
+ try {
15469
+ const root = createRoot2(container);
15470
+ flushSync2(() => {
15471
+ root.render(
15472
+ /* @__PURE__ */ jsx33(
15473
+ SchedulingWidget,
15474
+ {
15475
+ notifyOnConnect,
15476
+ initialScheduleId: scheduleId,
15477
+ insertAfter: widgetId
15478
+ }
15479
+ )
15480
+ );
15481
+ });
15482
+ } catch (err) {
15483
+ console.error("[ow:scheduling] render threw", err);
15484
+ container.remove();
15485
+ return false;
15486
+ }
14968
15487
  const tracker = getSectionsTracker();
14969
15488
  let sections = [];
14970
15489
  try {
@@ -14972,10 +15491,12 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
14972
15491
  } catch {
14973
15492
  }
14974
15493
  const inEditor = typeof window !== "undefined" && window.self !== window.top;
14975
- if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
15494
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === widgetId)) {
14976
15495
  sections.push({
14977
15496
  type: "scheduling",
14978
- insertAfter: effectiveInsertAfter,
15497
+ insertAfter: widgetId,
15498
+ anchorId,
15499
+ beforeId: beforeId ?? null,
14979
15500
  pagePath: window.location.pathname,
14980
15501
  ...scheduleId ? { scheduleId } : {}
14981
15502
  });
@@ -14989,7 +15510,8 @@ function mountSchedulingEntries(entries, notifyOnConnect = false) {
14989
15510
  for (let i = pending.length - 1; i >= 0; i--) {
14990
15511
  const entry = pending[i];
14991
15512
  const shouldNotify = typeof notifyOnConnect === "function" ? notifyOnConnect(entry) : notifyOnConnect;
14992
- if (mountSchedulingWidget(entry.insertAfter, shouldNotify, entry.scheduleId ?? null)) {
15513
+ const { anchorId, beforeId } = resolveEntryAnchor(entry);
15514
+ if (mountSchedulingWidget(anchorId, shouldNotify, entry.scheduleId ?? null, beforeId, entry.insertAfter)) {
14993
15515
  pending.splice(i, 1);
14994
15516
  }
14995
15517
  }
@@ -15081,7 +15603,7 @@ var EDITOR_CHROME_SELECTOR = '[data-ohw-toolbar], [data-ohw-form-toolbar], [data
15081
15603
  function isOverEditorChrome(x, y) {
15082
15604
  return document.elementsFromPoint(x, y).some((el) => el instanceof HTMLElement && el.matches(EDITOR_CHROME_SELECTOR));
15083
15605
  }
15084
- var NON_MEDIA_SELECTOR = '[data-ohw-editable]:not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"]):not([data-ohw-editable="video"]):not([data-ohw-editable="icon"]):not([data-ohw-editable="form"])';
15606
+ var NON_MEDIA_SELECTOR = '[data-ohw-editable]:not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"]):not([data-ohw-editable="video"]):not([data-ohw-editable="icon"]):not([data-ohw-editable="form"]):not([data-ohw-editable="map"])';
15085
15607
  function getVideoEl2(el) {
15086
15608
  return el instanceof HTMLVideoElement ? el : el.querySelector("video");
15087
15609
  }
@@ -15137,6 +15659,12 @@ function applyVideoSettingNode(key, val) {
15137
15659
  });
15138
15660
  return true;
15139
15661
  }
15662
+ function applyMapQuery(el, val) {
15663
+ if (!(el instanceof HTMLIFrameElement)) return;
15664
+ const nextSrc = `https://www.google.com/maps?q=${encodeURIComponent(val)}&output=embed`;
15665
+ if (el.src !== nextSrc) el.src = nextSrc;
15666
+ el.setAttribute("data-ohw-map-query", val);
15667
+ }
15140
15668
  function applyLinkByKey(key, val) {
15141
15669
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
15142
15670
  if (el.dataset.ohwEditable === "link") applyLinkHref(el, val);
@@ -15147,6 +15675,11 @@ function applyLinkByKey(key, val) {
15147
15675
  hrefAnchors.forEach((el) => applyLinkHref(el, val));
15148
15676
  }
15149
15677
  }
15678
+ function isInsideLinkEditor(target) {
15679
+ return Boolean(
15680
+ 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"]')
15681
+ );
15682
+ }
15150
15683
  function isInsideFloatingPanel(target) {
15151
15684
  return Boolean(target.closest("[data-ohw-floating-panel]"));
15152
15685
  }
@@ -15154,11 +15687,6 @@ function isPointOverFloatingPanel(clientX, clientY) {
15154
15687
  const el = document.elementFromPoint(clientX, clientY);
15155
15688
  return Boolean(el instanceof Element && el.closest("[data-ohw-floating-panel]"));
15156
15689
  }
15157
- function isInsideLinkEditor(target) {
15158
- return Boolean(
15159
- target.closest("[data-ohw-link-popover-root]") || target.closest("[data-ohw-link-modal-root]") || target.closest("[data-ohw-link-page-dropdown]") || target.closest("[data-ohw-section-picker]") || target.closest("[data-ohw-navbar-container-chrome]") || target.closest("[data-ohw-navbar-add-button]") || target.closest("[data-ohw-footer-container-chrome]") || target.closest("[data-ohw-footer-add-button]") || target.closest("[data-ohw-more-menu]") || target.closest('[data-slot="dropdown-menu-content"]') || target.closest('[data-slot="popover-content"]') || target.closest('[data-slot="dialog-content"]') || target.closest('[data-slot="dialog-overlay"]')
15160
- );
15161
- }
15162
15690
  function getHrefKeyFromElement(el) {
15163
15691
  if (!el) return null;
15164
15692
  const anchor = el.closest("[data-ohw-href-key]");
@@ -15417,7 +15945,7 @@ function getNavigationSelectionParent(el) {
15417
15945
  if (!isFooterLinksContainer(el) && (el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isInferredFooterGroup2(el))) {
15418
15946
  return getFooterLinksContainer();
15419
15947
  }
15420
- if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || isFooterLinksContainer(el)) {
15948
+ if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isFooterLinksContainer(el) || isInferredFooterGroup2(el)) {
15421
15949
  return getNavigationRoot(el);
15422
15950
  }
15423
15951
  return null;
@@ -15632,7 +16160,6 @@ var ICONS = {
15632
16160
  insertUnorderedList: '<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>',
15633
16161
  insertOrderedList: '<line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/>'
15634
16162
  };
15635
- var HEIGHT_SETTLE_DELAYS = [150, 400, 800];
15636
16163
  var SELECTION_CHROME_GAP2 = 4;
15637
16164
  var TOOLBAR_STROKE_GAP2 = 4;
15638
16165
  var TOOLBAR_OFFSET_FROM_ELEMENT = SELECTION_CHROME_GAP2 + TOOLBAR_STROKE_GAP2;
@@ -16012,6 +16539,7 @@ function StateToggle({
16012
16539
  );
16013
16540
  }
16014
16541
  var contentCache = /* @__PURE__ */ new Map();
16542
+ var fetchedContentPaths = /* @__PURE__ */ new Set();
16015
16543
  var brandingCache = /* @__PURE__ */ new Map();
16016
16544
  var OHW_LOADER_STYLE = {
16017
16545
  position: "fixed",
@@ -16541,13 +17069,6 @@ function OhhwellsBridge() {
16541
17069
  const [isItemDragging, setIsItemDragging] = useState13(false);
16542
17070
  const [isFooterFrameSelection, setIsFooterFrameSelection] = useState13(false);
16543
17071
  isFooterFrameSelectionRef.current = isFooterFrameSelection;
16544
- const [floatingPanel, setFloatingPanel] = useState13(null);
16545
- const floatingPanelOpenRef = useRef10(false);
16546
- floatingPanelOpenRef.current = floatingPanel !== null;
16547
- const [floatingPanelPos, setFloatingPanelPos] = useState13(null);
16548
- const [logoSizeDraft, setLogoSizeDraft] = useState13(null);
16549
- const [editorViewport, setEditorViewport] = useState13("desktop");
16550
- const [parentScrollSnap, setParentScrollSnap] = useState13(null);
16551
17072
  const [navDropdownPreviewOpen, setNavDropdownPreviewOpen] = useState13(null);
16552
17073
  const [footerHeadingVisible, setFooterHeadingVisible] = useState13(null);
16553
17074
  const footerDragRef = useRef10(null);
@@ -16565,6 +17086,13 @@ function OhhwellsBridge() {
16565
17086
  const brandKitRef = useRef10("");
16566
17087
  const stylesRef = useRef10("");
16567
17088
  const pendingDeleteUndoRef = useRef10(null);
17089
+ const [floatingPanel, setFloatingPanel] = useState13(null);
17090
+ const floatingPanelOpenRef = useRef10(false);
17091
+ const setFloatingPanelRef = useRef10(setFloatingPanel);
17092
+ const [floatingPanelPos, setFloatingPanelPos] = useState13(null);
17093
+ const [logoSizeDraft, setLogoSizeDraft] = useState13(null);
17094
+ const [editorViewport, setEditorViewport] = useState13("desktop");
17095
+ const [parentScrollSnap, setParentScrollSnap] = useState13(null);
16568
17096
  const [sitePages, setSitePages] = useState13([]);
16569
17097
  const [sectionsByPath, setSectionsByPath] = useState13({});
16570
17098
  const sectionsPrefetchGenRef = useRef10(0);
@@ -16573,7 +17101,18 @@ function OhhwellsBridge() {
16573
17101
  const linkPopoverOpenRef = useRef10(false);
16574
17102
  const linkPopoverGraceUntilRef = useRef10(0);
16575
17103
  setLinkPopoverRef.current = setLinkPopover;
17104
+ setFloatingPanelRef.current = setFloatingPanel;
16576
17105
  linkPopoverSessionRef.current = linkPopover;
17106
+ floatingPanelOpenRef.current = Boolean(floatingPanel);
17107
+ useEffect13(() => {
17108
+ const syncViewport = () => {
17109
+ const next = window.innerWidth <= 480 ? "mobile" : "desktop";
17110
+ setEditorViewport((prev) => prev === next ? prev : next);
17111
+ };
17112
+ syncViewport();
17113
+ window.addEventListener("resize", syncViewport);
17114
+ return () => window.removeEventListener("resize", syncViewport);
17115
+ }, []);
16577
17116
  const {
16578
17117
  navDragRef,
16579
17118
  navDropSlots,
@@ -17898,17 +18437,19 @@ function OhhwellsBridge() {
17898
18437
  }
17899
18438
  if (typeof content[STYLE_STORE_KEY] === "string") {
17900
18439
  stylesRef.current = content[STYLE_STORE_KEY];
18440
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
17901
18441
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17902
18442
  }
17903
18443
  applyBrandChrome(content);
18444
+ initSectionInstancesFromContent(content, window.location.pathname);
17904
18445
  for (const [key, val] of Object.entries(content)) {
17905
18446
  if (key === "__ohw_sections") continue;
17906
18447
  if (key === AI_SECTIONS_KEY) continue;
18448
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
18449
+ if (LOGO_IMAGE_KEYS.includes(key)) continue;
17907
18450
  if (key === BRAND_KIT_KEY) continue;
17908
18451
  if (key === STYLE_STORE_KEY) continue;
17909
18452
  if (BRAND_CHROME_KEYS.has(key)) continue;
17910
- if (key === LOGO_PLACEHOLDER_KEY) continue;
17911
- if (LOGO_IMAGE_KEYS.includes(key)) continue;
17912
18453
  if (applyVideoSettingNode(key, val)) continue;
17913
18454
  if (applyCarouselNode(key, val)) continue;
17914
18455
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -17936,6 +18477,8 @@ function OhhwellsBridge() {
17936
18477
  }
17937
18478
  } else if (el.dataset.ohwEditable === "link") {
17938
18479
  applyLinkHref(el, val);
18480
+ } else if (el.dataset.ohwEditable === "map") {
18481
+ applyMapQuery(el, val);
17939
18482
  } else if (el.dataset.ohwEditable === "icon") {
17940
18483
  applyIconMarkup(el, val);
17941
18484
  } else if (el.dataset.ohwEditable === "form") {
@@ -17956,7 +18499,6 @@ function OhhwellsBridge() {
17956
18499
  if (isEditModeRef.current) requestMissingSocialIconsRef.current();
17957
18500
  enforceLinkHrefs();
17958
18501
  initSectionsFromContent(content, true);
17959
- initSectionInstancesFromContent(content, window.location.pathname);
17960
18502
  sectionsLoadedRef.current = true;
17961
18503
  pendingScheduleConfigRequests.current.splice(0).forEach(processConfigRequest);
17962
18504
  if (imageLoads.length === 0) return Promise.resolve();
@@ -17975,7 +18517,9 @@ function OhhwellsBridge() {
17975
18517
  let cancelled = false;
17976
18518
  setFetchState("loading");
17977
18519
  const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
17978
- fetch(`${apiUrl}/api/public/sites/${subdomain}/content`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18520
+ const initialPath = pathname;
18521
+ fetchedContentPaths.add(`${subdomain}::${initialPath}`);
18522
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(initialPath)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
17979
18523
  if (cancelled) return;
17980
18524
  const content = data?.content ?? {};
17981
18525
  const branding = Boolean(data?.showBranding);
@@ -18109,16 +18653,17 @@ function OhhwellsBridge() {
18109
18653
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
18110
18654
  }
18111
18655
  if (typeof content[STYLE_STORE_KEY] === "string") {
18656
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
18112
18657
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
18113
18658
  }
18114
18659
  for (const [key, val] of Object.entries(content)) {
18115
18660
  if (key === "__ohw_sections") continue;
18116
18661
  if (key === AI_SECTIONS_KEY) continue;
18662
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
18663
+ if (LOGO_IMAGE_KEYS.includes(key)) continue;
18117
18664
  if (key === BRAND_KIT_KEY) continue;
18118
18665
  if (key === STYLE_STORE_KEY) continue;
18119
18666
  if (BRAND_CHROME_KEYS.has(key)) continue;
18120
- if (key === LOGO_PLACEHOLDER_KEY) continue;
18121
- if (LOGO_IMAGE_KEYS.includes(key)) continue;
18122
18667
  if (applyVideoSettingNode(key, val)) continue;
18123
18668
  if (applyCarouselNode(key, val)) continue;
18124
18669
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -18133,6 +18678,8 @@ function OhhwellsBridge() {
18133
18678
  if (video && video.src !== val) applyVideoSrc(video, val);
18134
18679
  } else if (el.dataset.ohwEditable === "link") {
18135
18680
  applyLinkHref(el, val);
18681
+ } else if (el.dataset.ohwEditable === "map") {
18682
+ applyMapQuery(el, val);
18136
18683
  } else if (el.dataset.ohwEditable === "form") {
18137
18684
  } else if (isIconMarkupValue(val)) {
18138
18685
  } else if (el.innerHTML !== val) {
@@ -18164,6 +18711,17 @@ function OhhwellsBridge() {
18164
18711
  debounceTimer = setTimeout(applyFromCache, 150);
18165
18712
  };
18166
18713
  applyFromCache();
18714
+ const pathCacheKey = `${subdomain}::${pathname}`;
18715
+ if (!fetchedContentPaths.has(pathCacheKey)) {
18716
+ fetchedContentPaths.add(pathCacheKey);
18717
+ const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
18718
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(pathname)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18719
+ if (!data?.content) return;
18720
+ contentCache.set(subdomain, data.content);
18721
+ applyFromCache();
18722
+ }).catch(() => {
18723
+ });
18724
+ }
18167
18725
  observer = new MutationObserver(scheduleApply);
18168
18726
  observer.observe(document.body, { childList: true, subtree: true });
18169
18727
  return () => {
@@ -18279,26 +18837,11 @@ function OhhwellsBridge() {
18279
18837
  const t2 = setTimeout(measure, 500);
18280
18838
  const ro = new ResizeObserver(schedule);
18281
18839
  ro.observe(document.body);
18282
- let lastWidth = window.innerWidth;
18283
- let resizeTimers = [];
18284
- const clearResizeTimers = () => {
18285
- resizeTimers.forEach(clearTimeout);
18286
- resizeTimers = [];
18287
- };
18288
- const handleResize = () => {
18289
- if (window.innerWidth === lastWidth) return;
18290
- lastWidth = window.innerWidth;
18291
- clearResizeTimers();
18292
- resizeTimers = HEIGHT_SETTLE_DELAYS.map((delay) => setTimeout(measure, delay));
18293
- };
18294
- window.addEventListener("resize", handleResize);
18295
18840
  return () => {
18296
18841
  clearTimeout(t1);
18297
18842
  clearTimeout(t2);
18298
18843
  if (raf != null) cancelAnimationFrame(raf);
18299
18844
  ro.disconnect();
18300
- clearResizeTimers();
18301
- window.removeEventListener("resize", handleResize);
18302
18845
  };
18303
18846
  }, [pathname, isEditMode, postToParent2]);
18304
18847
  useEffect13(() => {
@@ -18544,9 +19087,6 @@ function OhhwellsBridge() {
18544
19087
  if (target.closest("[data-ohw-state-toggle]")) return;
18545
19088
  if (target.closest("[data-ohw-max-badge]")) return;
18546
19089
  if (isInsideLinkEditor(target)) return;
18547
- if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
18548
- clearMediaSelectionRef.current();
18549
- }
18550
19090
  if (isInsideFloatingPanel(target)) return;
18551
19091
  if (target.closest("[data-ohw-form-toolbar]")) return;
18552
19092
  if (target.closest(
@@ -18554,6 +19094,9 @@ function OhhwellsBridge() {
18554
19094
  )) {
18555
19095
  return;
18556
19096
  }
19097
+ if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
19098
+ clearMediaSelectionRef.current();
19099
+ }
18557
19100
  {
18558
19101
  const formEl = getFormElement(target);
18559
19102
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
@@ -18705,14 +19248,6 @@ function OhhwellsBridge() {
18705
19248
  }
18706
19249
  const clickedButton = findClosestButtonLike(target);
18707
19250
  const buttonOnMedia = Boolean(clickedButton && isMediaEditable(editable) && editable.contains(clickedButton));
18708
- console.log("[click-debug]", {
18709
- editableType: editable.dataset.ohwEditable,
18710
- editableTag: editable.tagName,
18711
- targetTag: target.tagName,
18712
- clickedButtonTag: clickedButton?.tagName ?? null,
18713
- buttonOnMedia,
18714
- isMediaEditableEditable: isMediaEditable(editable)
18715
- });
18716
19251
  if (isMediaEditable(editable) && !buttonOnMedia) {
18717
19252
  e.preventDefault();
18718
19253
  e.stopPropagation();
@@ -18739,11 +19274,6 @@ function OhhwellsBridge() {
18739
19274
  const hrefLookupTarget = buttonOnMedia ? clickedButton : editable;
18740
19275
  const hrefCtx = getHrefKeyFromElement(hrefLookupTarget);
18741
19276
  const navAnchor = hrefCtx ? getNavigationItemAnchor(hrefCtx.anchor) : null;
18742
- console.log("[click-debug 2]", {
18743
- hrefLookupTargetTag: hrefLookupTarget.tagName,
18744
- hrefCtx: hrefCtx ? { key: hrefCtx.key } : null,
18745
- navAnchorTag: navAnchor?.tagName ?? null
18746
- });
18747
19277
  if (navAnchor) {
18748
19278
  e.preventDefault();
18749
19279
  e.stopPropagation();
@@ -18913,6 +19443,9 @@ function OhhwellsBridge() {
18913
19443
  setHoveredItemRect(null);
18914
19444
  hoveredNavContainerRef.current = null;
18915
19445
  setHoveredNavContainerRect(null);
19446
+ siblingHintElRef.current = null;
19447
+ setSiblingHintRect(null);
19448
+ setSiblingHintRects([]);
18916
19449
  return;
18917
19450
  }
18918
19451
  {
@@ -19031,7 +19564,6 @@ function OhhwellsBridge() {
19031
19564
  hoveredNavContainerRef.current = null;
19032
19565
  setHoveredNavContainerRect(null);
19033
19566
  hoveredItemElRef.current = editable;
19034
- setHoveredItemRect(isSelectedForHoverRef.current(editable) ? null : editable.getBoundingClientRect());
19035
19567
  }
19036
19568
  }
19037
19569
  }
@@ -19328,7 +19860,7 @@ function OhhwellsBridge() {
19328
19860
  }
19329
19861
  };
19330
19862
  const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false) => {
19331
- if (linkPopoverOpenRef.current) {
19863
+ if (linkPopoverOpenRef.current || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
19332
19864
  if (hoveredImageRef.current) {
19333
19865
  hoveredImageRef.current = null;
19334
19866
  hoveredImageHasTextOverlapRef.current = false;
@@ -19693,8 +20225,7 @@ function OhhwellsBridge() {
19693
20225
  };
19694
20226
  const handleMouseMove = (e) => {
19695
20227
  const { clientX, clientY } = e;
19696
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
19697
- if (isOverEditorChrome(clientX, clientY)) {
20228
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY) || isOverEditorChrome(clientX, clientY)) {
19698
20229
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
19699
20230
  formHoverElRef.current = null;
19700
20231
  setFormHoverRect(null);
@@ -19702,6 +20233,12 @@ function OhhwellsBridge() {
19702
20233
  setHoveredItemRect(null);
19703
20234
  hoveredNavContainerRef.current = null;
19704
20235
  setHoveredNavContainerRect(null);
20236
+ siblingHintElRef.current = null;
20237
+ setSiblingHintRect(null);
20238
+ setSiblingHintRects([]);
20239
+ dismissImageHover();
20240
+ clearImageHover();
20241
+ setSectionGap(null);
19705
20242
  return;
19706
20243
  }
19707
20244
  if (probeSocialsRowAt(clientX, clientY)) return;
@@ -19713,7 +20250,11 @@ function OhhwellsBridge() {
19713
20250
  if (e.data?.type !== "ow:pointer-sync") return;
19714
20251
  const { clientX, clientY } = e.data;
19715
20252
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
19716
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
20253
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
20254
+ dismissImageHover();
20255
+ clearImageHover();
20256
+ return;
20257
+ }
19717
20258
  if (probeSocialsRowAt(clientX, clientY)) return;
19718
20259
  probeSectionGapAt(clientX, clientY);
19719
20260
  probeImageAt(clientX, clientY);
@@ -19992,6 +20533,44 @@ function OhhwellsBridge() {
19992
20533
  if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
19993
20534
  }, 400));
19994
20535
  };
20536
+ const reapCommittedAiSections = (excludeIds) => {
20537
+ const aiState = parseAiSectionsState(aiSectionsRef.current);
20538
+ if (aiState.sections.length === 0) return [];
20539
+ let orderEntries = [];
20540
+ try {
20541
+ const parsed = JSON.parse(editContentRef.current[SECTION_ORDER_KEY] ?? "[]");
20542
+ if (Array.isArray(parsed)) orderEntries = parsed;
20543
+ } catch {
20544
+ return [];
20545
+ }
20546
+ const removedIds = orderEntries.filter((entry) => entry && typeof entry.instanceId === "string" && entry.removed).map((entry) => entry.instanceId);
20547
+ if (removedIds.length === 0) return [];
20548
+ const result = reapRemovedAiSections(aiState, parseStyleStore(stylesRef.current), removedIds, excludeIds);
20549
+ if (!result.changed) return [];
20550
+ const nodes = [];
20551
+ aiSectionsRef.current = serializeAiSectionsState(result.state);
20552
+ nodes.push({ key: AI_SECTIONS_KEY, text: aiSectionsRef.current });
20553
+ const reaped = new Set(result.reapedIds);
20554
+ const nextOrderJson = JSON.stringify(orderEntries.filter((entry) => !reaped.has(entry.instanceId)));
20555
+ editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: nextOrderJson };
20556
+ setAiSectionOrder(nextOrderJson, window.location.pathname);
20557
+ nodes.push({ key: SECTION_ORDER_KEY, text: nextOrderJson });
20558
+ if (result.store) {
20559
+ stylesRef.current = JSON.stringify(result.store);
20560
+ nodes.push({ key: STYLE_STORE_KEY, text: stylesRef.current });
20561
+ }
20562
+ const nextContent = { ...editContentRef.current };
20563
+ for (const key of Object.keys(nextContent)) {
20564
+ if (key.startsWith(AI_SLOT_KEY_PREFIX) && result.slotPrefixes.some((prefix) => key.startsWith(prefix)) && nextContent[key] !== "") {
20565
+ nextContent[key] = "";
20566
+ nodes.push({ key, text: "" });
20567
+ }
20568
+ }
20569
+ editContentRef.current = nextContent;
20570
+ applyAiSectionsToDom(result.state);
20571
+ applyStylesToDom(parseStyleStore(stylesRef.current));
20572
+ return nodes;
20573
+ };
19995
20574
  const handleHydrate = (e) => {
19996
20575
  if (e.data?.type !== "ow:hydrate") return;
19997
20576
  const content = e.data.content;
@@ -20010,9 +20589,11 @@ function OhhwellsBridge() {
20010
20589
  }
20011
20590
  if (typeof content[STYLE_STORE_KEY] === "string") {
20012
20591
  stylesRef.current = content[STYLE_STORE_KEY];
20592
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
20013
20593
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
20014
20594
  }
20015
20595
  applyBrandChrome(content);
20596
+ initSectionInstancesFromContent(content, window.location.pathname);
20016
20597
  let sectionsJson = null;
20017
20598
  for (const [key, val] of Object.entries(content)) {
20018
20599
  if (key === "__ohw_sections") {
@@ -20020,11 +20601,11 @@ function OhhwellsBridge() {
20020
20601
  continue;
20021
20602
  }
20022
20603
  if (key === AI_SECTIONS_KEY) continue;
20604
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
20605
+ if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
20023
20606
  if (key === BRAND_KIT_KEY) continue;
20024
20607
  if (key === STYLE_STORE_KEY) continue;
20025
20608
  if (BRAND_CHROME_KEYS.has(key)) continue;
20026
- if (key === LOGO_PLACEHOLDER_KEY) continue;
20027
- if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
20028
20609
  if (applyVideoSettingNode(key, val)) continue;
20029
20610
  if (applyCarouselNode(key, val)) continue;
20030
20611
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -20038,6 +20619,8 @@ function OhhwellsBridge() {
20038
20619
  if (video && video.src !== val) applyVideoSrc(video, val);
20039
20620
  } else if (el.dataset.ohwEditable === "link") {
20040
20621
  applyLinkHref(el, val);
20622
+ } else if (el.dataset.ohwEditable === "map") {
20623
+ applyMapQuery(el, val);
20041
20624
  } else if (el.dataset.ohwEditable === "icon") {
20042
20625
  applyIconMarkup(el, val);
20043
20626
  } else if (isIconMarkupValue(val)) {
@@ -20054,12 +20637,16 @@ function OhhwellsBridge() {
20054
20637
  sectionsLoadedRef.current = true;
20055
20638
  pendingScheduleConfigRequests.current.splice(0).forEach(processConfigRequest);
20056
20639
  }
20057
- initSectionInstancesFromContent(content, window.location.pathname);
20058
20640
  editContentRef.current = { ...editContentRef.current, ...content };
20059
20641
  reconcileNavbarItemsFromContent(editContentRef.current);
20060
20642
  reconcileFooterOrderFromContent(editContentRef.current);
20061
20643
  syncNavigationDragCursorAttrs();
20062
20644
  enforceLinkHrefs();
20645
+ const hydrateReapExclude = /* @__PURE__ */ new Set();
20646
+ const hydratePendingUndo = pendingDeleteUndoRef.current;
20647
+ if (hydratePendingUndo?.sectionInstanceId) hydrateReapExclude.add(hydratePendingUndo.sectionInstanceId);
20648
+ const reapNodes = reapCommittedAiSections(hydrateReapExclude);
20649
+ if (reapNodes.length > 0) postToParentRef.current({ type: "ow:change", nodes: reapNodes });
20063
20650
  const hydratedHeight = document.body.scrollHeight;
20064
20651
  if (hydratedHeight > 50) postToParentRef.current({ type: "ow:height", height: hydratedHeight });
20065
20652
  postToParentRef.current({ type: "ow:hydrate-done" });
@@ -20199,12 +20786,35 @@ function OhhwellsBridge() {
20199
20786
  window.addEventListener("message", handleAiSetBrand);
20200
20787
  const handleAiSetStyles = (e) => {
20201
20788
  if (e.data?.type !== "ow:ai-set-styles") return;
20202
- const value = typeof e.data.value === "string" ? e.data.value : "";
20789
+ let value = typeof e.data.value === "string" ? e.data.value : "";
20203
20790
  const previous = stylesRef.current;
20791
+ let previousSections;
20792
+ const store = parseStyleStore(value);
20793
+ if (store) {
20794
+ const folded = foldAlignIntoTrees(parseAiSectionsState(aiSectionsRef.current), store);
20795
+ if (folded.changed) {
20796
+ const nextSections = serializeAiSectionsState(folded.state);
20797
+ if (nextSections !== aiSectionsRef.current) {
20798
+ previousSections = aiSectionsRef.current;
20799
+ aiSectionsRef.current = nextSections;
20800
+ applyAiSectionsToDom(folded.state);
20801
+ postToParentRef.current({
20802
+ type: "ow:change",
20803
+ nodes: [{ key: AI_SECTIONS_KEY, text: nextSections }]
20804
+ });
20805
+ }
20806
+ value = JSON.stringify(folded.store);
20807
+ }
20808
+ }
20204
20809
  stylesRef.current = value;
20205
20810
  applyStylesToDom(parseStyleStore(value));
20206
20811
  postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
20207
- postToParentRef.current({ type: "ow:ai-styles-applied", previous, value });
20812
+ postToParentRef.current({
20813
+ type: "ow:ai-styles-applied",
20814
+ previous,
20815
+ value,
20816
+ ...previousSections !== void 0 ? { previousSections } : {}
20817
+ });
20208
20818
  };
20209
20819
  window.addEventListener("message", handleAiSetStyles);
20210
20820
  const handleGetBrand = (e) => {
@@ -20243,6 +20853,7 @@ function OhhwellsBridge() {
20243
20853
  if (!entries) return;
20244
20854
  const orderJson = JSON.stringify(entries);
20245
20855
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20856
+ setAiSectionOrder(orderJson, window.location.pathname);
20246
20857
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
20247
20858
  aiSectionApiRef.current?.clear();
20248
20859
  window.dispatchEvent(new Event("resize"));
@@ -20251,6 +20862,7 @@ function OhhwellsBridge() {
20251
20862
  const actionId = newInstanceId();
20252
20863
  pendingDeleteUndoRef.current = {
20253
20864
  actionId,
20865
+ sectionInstanceId: instanceId,
20254
20866
  restore: () => {
20255
20867
  const restoredEntries = getPageSectionOrderEntries(
20256
20868
  editContentRef.current[SECTION_ORDER_KEY],
@@ -20260,6 +20872,7 @@ function OhhwellsBridge() {
20260
20872
  if (!restored) return;
20261
20873
  const restoredJson = JSON.stringify(restored);
20262
20874
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: restoredJson };
20875
+ setAiSectionOrder(restoredJson, window.location.pathname);
20263
20876
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: restoredJson }] });
20264
20877
  window.dispatchEvent(new Event("resize"));
20265
20878
  const restoreHeight = document.body.scrollHeight;
@@ -20276,6 +20889,34 @@ function OhhwellsBridge() {
20276
20889
  });
20277
20890
  };
20278
20891
  window.addEventListener("message", handleDeleteSection);
20892
+ const handleDuplicateSection = (e) => {
20893
+ if (e.data?.type !== "ow:duplicate-section") return;
20894
+ const instanceId = typeof e.data.instanceId === "string" ? e.data.instanceId : "";
20895
+ if (!instanceId) return;
20896
+ const newId = newInstanceId();
20897
+ const currentEntries = getPageSectionOrderEntries(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname);
20898
+ const result = duplicateSectionInstance(instanceId, newId, window.location.pathname, currentEntries);
20899
+ if (!result) return;
20900
+ const { entries, keyRekeys } = result;
20901
+ const orderJson = JSON.stringify(entries);
20902
+ const nodes = [{ key: SECTION_ORDER_KEY, text: orderJson }];
20903
+ for (const { from, to } of keyRekeys) {
20904
+ const inherited = editContentRef.current[from];
20905
+ if (inherited !== void 0) nodes.push({ key: to, text: inherited });
20906
+ }
20907
+ editContentRef.current = {
20908
+ ...editContentRef.current,
20909
+ ...Object.fromEntries(nodes.map(({ key, text }) => [key, text]))
20910
+ };
20911
+ setAiSectionOrder(orderJson, window.location.pathname);
20912
+ postToParentRef.current({ type: "ow:change", nodes });
20913
+ window.dispatchEvent(new Event("resize"));
20914
+ const duplicateHeight = document.body.scrollHeight;
20915
+ if (duplicateHeight > 50) postToParentRef.current({ type: "ow:height", height: duplicateHeight });
20916
+ const clone = document.querySelector(`[data-ohw-instance="${CSS.escape(newId)}"]`);
20917
+ if (clone) aiSectionApiRef.current?.selectFromElement(clone);
20918
+ };
20919
+ window.addEventListener("message", handleDuplicateSection);
20279
20920
  const handleDeactivate = (e) => {
20280
20921
  if (e.data?.type !== "ow:deactivate") return;
20281
20922
  if (document.documentElement.hasAttribute("data-ohw-panel-dragging")) return;
@@ -20285,6 +20926,12 @@ function OhhwellsBridge() {
20285
20926
  closeLinkPopoverRef.current();
20286
20927
  return;
20287
20928
  }
20929
+ if (floatingPanelOpenRef.current) {
20930
+ setFloatingPanelRef.current(null);
20931
+ deselectRef.current();
20932
+ deactivateRef.current();
20933
+ return;
20934
+ }
20288
20935
  deselectRef.current();
20289
20936
  deactivateRef.current();
20290
20937
  clearMediaSelectionRef.current();
@@ -20530,6 +21177,10 @@ function OhhwellsBridge() {
20530
21177
  };
20531
21178
  const handleSave = (e) => {
20532
21179
  if (e.data?.type !== "ow:save") return;
21180
+ const pendingUndo = pendingDeleteUndoRef.current;
21181
+ const reapExclude = /* @__PURE__ */ new Set();
21182
+ if (pendingUndo?.sectionInstanceId) reapExclude.add(pendingUndo.sectionInstanceId);
21183
+ const reapNodes = reapCommittedAiSections(reapExclude);
20533
21184
  const nodes = collectEditableNodes(editContentRef.current);
20534
21185
  const tracker = document.querySelector("[data-ohw-sections-tracker]");
20535
21186
  if (tracker?.textContent) nodes.push({ key: "__ohw_sections", type: "sections", text: tracker.textContent });
@@ -20549,6 +21200,11 @@ function OhhwellsBridge() {
20549
21200
  const text = specs.length ? JSON.stringify(specs) : editContentRef.current[fieldsKey(formKey)];
20550
21201
  if (text) nodes.push({ key: fieldsKey(formKey), type: "text", text });
20551
21202
  });
21203
+ for (const reapNode of reapNodes) {
21204
+ if (reapNode.key.startsWith(AI_SLOT_KEY_PREFIX) && !nodes.some((n) => n.key === reapNode.key)) {
21205
+ nodes.push({ key: reapNode.key, type: "text", text: reapNode.text });
21206
+ }
21207
+ }
20552
21208
  postToParentRef.current({ type: "ow:save-result", nodes });
20553
21209
  };
20554
21210
  const handleInsertSection = (e) => {
@@ -20559,8 +21215,12 @@ function OhhwellsBridge() {
20559
21215
  if (inserted) {
20560
21216
  const tracker = getSectionsTracker();
20561
21217
  postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
20562
- const h = document.body.scrollHeight;
20563
- if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
21218
+ const reportHeight = () => {
21219
+ const h = document.body.scrollHeight;
21220
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
21221
+ };
21222
+ reportHeight();
21223
+ setTimeout(reportHeight, 500);
20564
21224
  }
20565
21225
  };
20566
21226
  const handleSwitchSchedule = (e) => {
@@ -20962,11 +21622,12 @@ function OhhwellsBridge() {
20962
21622
  window.removeEventListener("message", handleMoveSection);
20963
21623
  window.removeEventListener("message", handlePanelDragging);
20964
21624
  window.removeEventListener("message", handleDeleteSection);
21625
+ window.removeEventListener("message", handleDuplicateSection);
20965
21626
  window.removeEventListener("message", handleDeactivate);
20966
- document.documentElement.removeAttribute("data-ohw-panel-dragging");
20967
21627
  window.removeEventListener("message", handleToastAction);
20968
21628
  window.removeEventListener("message", handleFormCount);
20969
21629
  window.removeEventListener("message", handleUiEscape);
21630
+ document.documentElement.removeAttribute("data-ohw-panel-dragging");
20970
21631
  autoSaveTimers.current.forEach(clearTimeout);
20971
21632
  autoSaveTimers.current.clear();
20972
21633
  if (imageUnhoverTimerRef.current) clearTimeout(imageUnhoverTimerRef.current);
@@ -21169,7 +21830,7 @@ function OhhwellsBridge() {
21169
21830
  postToParent2({
21170
21831
  type: "ow:ready",
21171
21832
  version: "1",
21172
- bridgeVersion: "0.1.84",
21833
+ bridgeVersion: "0.1.86",
21173
21834
  path: pathname,
21174
21835
  nodes: collectEditableNodes(editContentRef.current),
21175
21836
  sections
@@ -22088,6 +22749,59 @@ function OhhwellsBridge() {
22088
22749
  ) : null
22089
22750
  ] });
22090
22751
  }
22752
+
22753
+ // src/ui/EmptySection.tsx
22754
+ import Link3 from "next/link";
22755
+ import { Fragment as Fragment9, jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
22756
+ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey }) {
22757
+ return /* @__PURE__ */ jsxs21(Fragment9, { children: [
22758
+ /* @__PURE__ */ jsx34(
22759
+ "p",
22760
+ {
22761
+ style: {
22762
+ fontFamily: "var(--brand-font-body)",
22763
+ fontSize: "0.75rem",
22764
+ fontWeight: 500,
22765
+ letterSpacing: "0.15em",
22766
+ textTransform: "uppercase",
22767
+ color: "var(--brand-accent)",
22768
+ marginBottom: "1.5rem"
22769
+ },
22770
+ children: /* @__PURE__ */ jsx34(Link3, { href: homeHref, style: { color: "inherit" }, children: /* @__PURE__ */ jsx34("span", { ...eyebrowKey ? { "data-ohw-editable": "text", "data-ohw-key": eyebrowKey } : {}, children: "Home" }) })
22771
+ }
22772
+ ),
22773
+ /* @__PURE__ */ jsx34(
22774
+ "h1",
22775
+ {
22776
+ style: {
22777
+ fontFamily: "var(--brand-font-heading)",
22778
+ fontSize: "clamp(2rem, 3.5vw, 2.75rem)",
22779
+ lineHeight: 1.1,
22780
+ letterSpacing: "-0.025em",
22781
+ color: "var(--brand-text)",
22782
+ marginBottom: "1rem"
22783
+ },
22784
+ ...titleKey ? { "data-ohw-editable": "text", "data-ohw-key": titleKey, "data-ohw-max-length": 80 } : {},
22785
+ children: title
22786
+ }
22787
+ ),
22788
+ /* @__PURE__ */ jsx34(
22789
+ "p",
22790
+ {
22791
+ style: {
22792
+ fontFamily: "var(--brand-font-body)",
22793
+ fontSize: "1rem",
22794
+ lineHeight: 1.7,
22795
+ fontWeight: 300,
22796
+ color: "var(--brand-text-muted)",
22797
+ maxWidth: "340px"
22798
+ },
22799
+ ...subtitleKey ? { "data-ohw-editable": "text", "data-ohw-key": subtitleKey, "data-ohw-max-length": 160 } : {},
22800
+ children: "This page doesn't have any content yet."
22801
+ }
22802
+ )
22803
+ ] });
22804
+ }
22091
22805
  export {
22092
22806
  AI_DEFAULT_BRAND,
22093
22807
  AI_TREE_SCHEMA_VERSIONS,
@@ -22104,6 +22818,7 @@ export {
22104
22818
  DropdownMenuItem,
22105
22819
  DropdownMenuSeparator,
22106
22820
  DropdownMenuTrigger,
22821
+ EmptySection,
22107
22822
  ItemActionToolbar,
22108
22823
  ItemInteractionLayer,
22109
22824
  LinkEditorPanel,