@ohhwells/bridge 0.1.85 → 0.1.86-next.256

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>'
@@ -510,6 +1121,25 @@ var FEATURE_LINE_CSS = [
510
1121
  `background-color:currentColor;-webkit-mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px;`,
511
1122
  `mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px}`
512
1123
  ].join("");
1124
+ function buttonShellStyle(ctx, fullWidth) {
1125
+ const bs = ctx.buttonStyle;
1126
+ if (bs) {
1127
+ return {
1128
+ borderRadius: bs.radius,
1129
+ ...bs.padding ? { padding: bs.padding } : {},
1130
+ ...bs.fontFamily ? { fontFamily: bs.fontFamily } : { fontFamily: ctx.brand.fonts.body },
1131
+ ...bs.fontSize ? { fontSize: bs.fontSize } : {},
1132
+ ...bs.fontWeight ? { fontWeight: bs.fontWeight } : {},
1133
+ ...bs.letterSpacing && bs.letterSpacing !== "normal" ? { letterSpacing: bs.letterSpacing } : {},
1134
+ ...bs.textTransform && bs.textTransform !== "none" ? { textTransform: bs.textTransform } : {}
1135
+ };
1136
+ }
1137
+ return {
1138
+ borderRadius: AI_TREE_TOKENS.radiusButton,
1139
+ padding: fullWidth ? `${AI_TREE_TOKENS.spacing2}px ${AI_TREE_TOKENS.spacing4}px` : `${AI_TREE_TOKENS.spacing3}px ${AI_TREE_TOKENS.spacing6}px`,
1140
+ ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
1141
+ };
1142
+ }
513
1143
  function hexLuminance(color) {
514
1144
  const m = /^#([0-9a-f]{6})$/i.exec(color.trim());
515
1145
  if (!m) return null;
@@ -526,6 +1156,12 @@ function hexContrast(a, b) {
526
1156
  const [hi, lo] = la > lb ? [la, lb] : [lb, la];
527
1157
  return (hi + 0.05) / (lo + 0.05);
528
1158
  }
1159
+ function primaryButtonLabel(brand) {
1160
+ const darkC = hexContrast(brand.palette.primary, brand.palette.dark);
1161
+ const lightC = hexContrast(brand.palette.primary, AI_TREE_TOKENS.textPrimaryForeground);
1162
+ if (darkC === null || lightC === null) return AI_TREE_TOKENS.textPrimaryForeground;
1163
+ return darkC > lightC ? brand.palette.dark : AI_TREE_TOKENS.textPrimaryForeground;
1164
+ }
529
1165
  function accentBandContext(brand) {
530
1166
  const p = brand.palette;
531
1167
  const lightWins = (hexContrast(p.light, p.primary) ?? 99) >= (hexContrast(p.dark, p.primary) ?? 0);
@@ -543,6 +1179,22 @@ function accentBandContext(brand) {
543
1179
  function textAttrs(ctx, path) {
544
1180
  return ctx.keyFor ? { "data-ohw-key": ctx.keyFor(path), "data-ohw-editable": "text" } : {};
545
1181
  }
1182
+ var AI_RESPONSIVE_CSS = [
1183
+ "@media (max-width: 960px) {",
1184
+ " [data-ai-responsive] [data-ai-grid] { grid-template-columns: repeat(2, 1fr) !important; }",
1185
+ ' [data-ai-responsive] [data-ai-grid="1"] { grid-template-columns: 1fr !important; }',
1186
+ "}",
1187
+ "@media (max-width: 640px) {",
1188
+ " [data-ai-responsive] [data-ai-grid] { grid-template-columns: 1fr !important; }",
1189
+ " [data-ai-responsive] [data-ai-columns] > * { grid-column: 1 / -1 !important; }",
1190
+ // Group containers flatten to a column on phones; span placements come along for free.
1191
+ " [data-ai-responsive] [data-ai-group] { display: flex !important; flex-direction: column !important; }",
1192
+ " [data-ai-responsive] [data-ai-group] > * { grid-column: auto !important; }",
1193
+ " [data-ai-responsive] [data-ai-section-inner] { padding-left: 20px !important; padding-right: 20px !important; }",
1194
+ " [data-ai-responsive] { overflow-x: hidden; }",
1195
+ " [data-ai-responsive] img { max-width: 100%; }",
1196
+ "}"
1197
+ ].join("\n");
546
1198
  var TRANSPARENT_PX = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
547
1199
  function MediaBox({
548
1200
  refValue,
@@ -555,13 +1207,17 @@ function MediaBox({
555
1207
  const url = refValue ? ctx.resolveMedia(refValue) : null;
556
1208
  const isIcon = /^(lucide|simple):/.test(refValue);
557
1209
  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" } : {};
1210
+ const editAttrs = ctx.keyFor && editPath ? {
1211
+ "data-ohw-key": ctx.keyFor(editPath),
1212
+ "data-ohw-editable": isIcon ? "icon" : "image"
1213
+ } : {};
559
1214
  if (isIcon) {
560
1215
  const Icon = refValue.startsWith("lucide:") ? lucideByName(refValue.slice(7)) : null;
561
1216
  return /* @__PURE__ */ jsx(
562
1217
  "span",
563
1218
  {
564
1219
  "data-ai-icon": refValue,
1220
+ ...editAttrs,
565
1221
  style: {
566
1222
  display: "inline-flex",
567
1223
  width: 48,
@@ -625,12 +1281,10 @@ function ButtonEl({
625
1281
  width: fullWidth ? "100%" : void 0,
626
1282
  alignItems: "center",
627
1283
  justifyContent: "center",
628
- padding: fullWidth ? `${AI_TREE_TOKENS.spacing2}px ${AI_TREE_TOKENS.spacing4}px` : `${AI_TREE_TOKENS.spacing3}px ${AI_TREE_TOKENS.spacing6}px`,
629
- borderRadius: AI_TREE_TOKENS.radiusButton,
630
1284
  textDecoration: "none",
631
1285
  cursor: "pointer",
632
- ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body),
633
- ...secondary ? { border: `1px solid ${ctx.brand.palette.dark}`, color: ctx.brand.palette.dark } : { background: ctx.brand.palette.primary, color: ctx.buttonLabel ?? AI_TREE_TOKENS.textPrimaryForeground }
1286
+ ...buttonShellStyle(ctx, fullWidth),
1287
+ ...secondary ? { border: `1px solid ${ctx.brand.palette.dark}`, color: ctx.brand.palette.dark } : { background: ctx.brand.palette.primary, color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand) }
634
1288
  },
635
1289
  children: /* @__PURE__ */ jsx("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
636
1290
  }
@@ -656,7 +1310,7 @@ function TextBlock({ slots, ctx, path }) {
656
1310
  }
657
1311
  function SectionHeaderBlock({ node, ctx, path }) {
658
1312
  const slots = node.slots ?? {};
659
- const align = slots.alignment === "center" ? "center" : "left";
1313
+ const align = node.align ?? (slots.alignment === "center" ? "center" : "left");
660
1314
  const children = node.children ?? [];
661
1315
  const buttonRowIdx = children.findIndex((c) => c.type === "button-row");
662
1316
  const buttonRow = buttonRowIdx >= 0 ? children[buttonRowIdx] : void 0;
@@ -700,7 +1354,7 @@ function SectionHeaderBlock({ node, ctx, path }) {
700
1354
  display: "flex",
701
1355
  gap: AI_TREE_TOKENS.spacing6,
702
1356
  marginTop: AI_TREE_TOKENS.spacing8,
703
- justifyContent: align === "center" ? "center" : "flex-start"
1357
+ justifyContent: align === "center" ? "center" : align === "right" ? "flex-end" : "flex-start"
704
1358
  },
705
1359
  children: (buttonRow.children ?? []).map((button, i) => /* @__PURE__ */ jsx(
706
1360
  ButtonEl,
@@ -806,10 +1460,11 @@ function PricingCard({ node, ctx, path }) {
806
1460
  return /* @__PURE__ */ jsxs(
807
1461
  "div",
808
1462
  {
1463
+ "data-ohw-card": "",
809
1464
  style: {
810
1465
  background: hasBg ? ctx.brand.palette.light : "transparent",
811
1466
  border: `1px solid ${dark}`,
812
- borderRadius: AI_TREE_TOKENS.radiusCard,
1467
+ borderRadius: cardRadius(slots),
813
1468
  padding: AI_TREE_TOKENS.paddingBlock,
814
1469
  display: "flex",
815
1470
  flexDirection: "column",
@@ -914,10 +1569,11 @@ function TestimonialCard({ node, ctx, path }) {
914
1569
  return /* @__PURE__ */ jsx(
915
1570
  "div",
916
1571
  {
1572
+ "data-ohw-card": "",
917
1573
  "data-ai-avatar-pos": avatarPos ?? void 0,
918
1574
  style: {
919
1575
  background: hasBg ? ctx.cardSurface : "transparent",
920
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1576
+ borderRadius: hasBg ? cardRadius(slots) : 0,
921
1577
  overflow: "hidden",
922
1578
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
923
1579
  minWidth: 0
@@ -952,10 +1608,11 @@ function TeamCard({ node, ctx, path }) {
952
1608
  return /* @__PURE__ */ jsxs(
953
1609
  "div",
954
1610
  {
1611
+ "data-ohw-card": "",
955
1612
  "data-ai-avatar-pos": avatarPos ?? void 0,
956
1613
  style: {
957
1614
  background: hasBg ? ctx.cardSurface : "transparent",
958
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1615
+ borderRadius: hasBg ? cardRadius(slots) : 0,
959
1616
  overflow: "hidden",
960
1617
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
961
1618
  minWidth: 0,
@@ -1033,7 +1690,7 @@ function CardBlock({ node, ctx, path }) {
1033
1690
  editPath: `${path}.media`
1034
1691
  }
1035
1692
  ) : null;
1036
- const centered = slots.alignment === "center";
1693
+ const centered = (node.align ?? slots.alignment) === "center";
1037
1694
  const content = /* @__PURE__ */ jsxs(
1038
1695
  "div",
1039
1696
  {
@@ -1126,9 +1783,10 @@ function CardBlock({ node, ctx, path }) {
1126
1783
  return /* @__PURE__ */ jsxs(
1127
1784
  "div",
1128
1785
  {
1786
+ "data-ohw-card": "",
1129
1787
  style: {
1130
1788
  background: hasBg ? ctx.cardSurface : "transparent",
1131
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1789
+ borderRadius: hasBg ? cardRadius(slots) : 0,
1132
1790
  overflow: "hidden",
1133
1791
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
1134
1792
  display: horizontal ? "flex" : "block",
@@ -1156,7 +1814,7 @@ function CardBlock({ node, ctx, path }) {
1156
1814
  ) : /* @__PURE__ */ jsx(
1157
1815
  "div",
1158
1816
  {
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" },
1817
+ 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
1818
  children: media
1161
1819
  }
1162
1820
  )),
@@ -1447,7 +2105,7 @@ function CollectionBlock({ node, ctx, path }) {
1447
2105
  return /* @__PURE__ */ jsx(
1448
2106
  "div",
1449
2107
  {
1450
- "data-ai-grid": "",
2108
+ "data-ai-grid": String(itemsPerRow),
1451
2109
  style: {
1452
2110
  display: "grid",
1453
2111
  gridTemplateColumns: `repeat(${itemsPerRow}, 1fr)`,
@@ -1567,6 +2225,32 @@ function renderNode(node, ctx, path) {
1567
2225
  if (child) {
1568
2226
  return renderNode(child, ctx, `${path}.c0`);
1569
2227
  }
2228
+ if (str(slots.provider) === "map" && str(slots.query)) {
2229
+ const query = str(slots.query);
2230
+ const mapAttrs = ctx.keyFor ? {
2231
+ "data-ohw-key": ctx.keyFor(`${path}.query`),
2232
+ "data-ohw-editable": "map",
2233
+ "data-ohw-map-query": query
2234
+ } : {};
2235
+ return /* @__PURE__ */ jsx(
2236
+ "iframe",
2237
+ {
2238
+ ...mapAttrs,
2239
+ "data-ai-embed": "map",
2240
+ title: str(slots.title) || "Map",
2241
+ src: `https://www.google.com/maps?q=${encodeURIComponent(query)}&output=embed`,
2242
+ loading: "lazy",
2243
+ referrerPolicy: "no-referrer-when-downgrade",
2244
+ style: {
2245
+ width: "100%",
2246
+ minHeight: 320,
2247
+ border: 0,
2248
+ borderRadius: AI_TREE_TOKENS.radiusCard,
2249
+ display: "block"
2250
+ }
2251
+ }
2252
+ );
2253
+ }
1570
2254
  return /* @__PURE__ */ jsx(
1571
2255
  "div",
1572
2256
  {
@@ -1670,15 +2354,12 @@ function renderNode(node, ctx, path) {
1670
2354
  alignSelf: submitAlign,
1671
2355
  border: "none",
1672
2356
  cursor: "pointer",
1673
- padding: "12px 24px",
1674
- // Corner radius follows the host template's own buttons (measured from a template
1675
- // CTA); 8px only when the page has no template button to match.
1676
- borderRadius: ctx.buttonRadius ?? 8,
2357
+ // Shape/padding/typography follow the host template's own buttons.
2358
+ ...buttonShellStyle(ctx),
1677
2359
  // Brand-styled: primary fill, brand-derived label colour (not a fixed token) so it
1678
2360
  // reads correctly on custom palettes.
1679
2361
  background: ctx.brand.palette.primary,
1680
- color: ctx.buttonLabel ?? ctx.brand.palette.light,
1681
- ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
2362
+ color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand)
1682
2363
  },
1683
2364
  children: /* @__PURE__ */ jsx("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
1684
2365
  },
@@ -1713,7 +2394,7 @@ function renderNode(node, ctx, path) {
1713
2394
  function AiTreeRenderer({
1714
2395
  tree,
1715
2396
  brand,
1716
- buttonRadius,
2397
+ buttonStyle,
1717
2398
  resolveMedia,
1718
2399
  editKeyPrefix
1719
2400
  }) {
@@ -1723,13 +2404,18 @@ function AiTreeRenderer({
1723
2404
  const resolvedBrand = brand ?? AI_DEFAULT_BRAND;
1724
2405
  const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
1725
2406
  const blockBrand = band?.brand ?? resolvedBrand;
2407
+ const placeholderMap = buildPlaceholderMap(tree);
1726
2408
  const ctx = {
1727
2409
  brand: blockBrand,
1728
- resolveMedia: resolveMedia ?? (() => null),
2410
+ // An owner/library ref resolves through the host resolver; a `placeholder:<subject>` ref the
2411
+ // host cannot resolve falls back to real stock photography (the per-section map first, then a
2412
+ // standalone resolve), so generated galleries, image rows, and overlay backgrounds arrive with
2413
+ // photos instead of grey boxes.
2414
+ resolveMedia: (ref) => resolveMedia?.(ref) ?? placeholderMap[ref] ?? resolvePlaceholderRef(ref),
1729
2415
  cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
1730
2416
  keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
1731
2417
  sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
1732
- buttonRadius,
2418
+ buttonStyle,
1733
2419
  ...band ? { buttonLabel: band.buttonLabel } : {}
1734
2420
  };
1735
2421
  const settings = tree.settings ?? {};
@@ -1752,11 +2438,25 @@ function AiTreeRenderer({
1752
2438
  }
1753
2439
  })();
1754
2440
  const distributed = !isOverlay && settings.textDistribution;
2441
+ const rowAlignItems = (rowAlign) => {
2442
+ if (rowAlign === "top") return "start";
2443
+ if (rowAlign === "bottom") return "end";
2444
+ if (rowAlign === "center" || rowAlign === "stretch") return rowAlign;
2445
+ if (distributed === "space-between") return "stretch";
2446
+ return (distributed ?? settings.verticalPosition) === "top" ? "start" : "center";
2447
+ };
2448
+ const cellAlignStyle = (blockAlign) => blockAlign ? {
2449
+ display: "flex",
2450
+ flexDirection: "column",
2451
+ alignItems: blockAlign === "left" ? "flex-start" : blockAlign === "right" ? "flex-end" : "center",
2452
+ textAlign: blockAlign
2453
+ } : {};
1755
2454
  return /* @__PURE__ */ jsxs(
1756
2455
  "section",
1757
2456
  {
1758
2457
  "data-ai-section": tree.tag ?? "",
1759
2458
  ...bgAttrs,
2459
+ "data-ai-responsive": "",
1760
2460
  style: {
1761
2461
  position: "relative",
1762
2462
  padding: `${pad}px 0`,
@@ -1767,12 +2467,13 @@ function AiTreeRenderer({
1767
2467
  color: settings.sectionBackground === "accent" ? blockBrand.palette.dark : void 0
1768
2468
  },
1769
2469
  children: [
1770
- isOverlay && backgroundUrl && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
2470
+ /* @__PURE__ */ jsx("style", { children: AI_RESPONSIVE_CSS }),
1771
2471
  /* @__PURE__ */ jsx("style", { children: AI_MOBILE_CSS }),
2472
+ isOverlay && backgroundUrl && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
1772
2473
  /* @__PURE__ */ jsx(
1773
2474
  "div",
1774
2475
  {
1775
- "data-ai-container": "",
2476
+ "data-ai-section-inner": "",
1776
2477
  style: {
1777
2478
  position: "relative",
1778
2479
  maxWidth: AI_TREE_TOKENS.sectionMaxWidth,
@@ -1783,12 +2484,12 @@ function AiTreeRenderer({
1783
2484
  children: tree.rows.map((row, r2) => /* @__PURE__ */ jsx(
1784
2485
  "div",
1785
2486
  {
1786
- "data-ai-row": "",
2487
+ "data-ai-columns": "",
1787
2488
  style: {
1788
2489
  display: "grid",
1789
2490
  gridTemplateColumns: "repeat(12, 1fr)",
1790
2491
  gap: AI_TREE_TOKENS.spacing6,
1791
- alignItems: distributed === "space-between" ? "stretch" : (distributed ?? settings.verticalPosition) === "top" ? "start" : "center",
2492
+ alignItems: rowAlignItems(row.align),
1792
2493
  marginTop: r2 > 0 ? AI_TREE_TOKENS.spacing8 : 0
1793
2494
  },
1794
2495
  children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ jsx(
@@ -1798,6 +2499,8 @@ function AiTreeRenderer({
1798
2499
  style: {
1799
2500
  gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`,
1800
2501
  minWidth: 0,
2502
+ // Horizontal placement of the block's content within its column.
2503
+ ...cellAlignStyle(block.align),
1801
2504
  // space-between: each column becomes a flex column whose content spreads over
1802
2505
  // the full row height instead of clumping at the top.
1803
2506
  ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
@@ -1820,7 +2523,7 @@ function AiTreeRenderer({
1820
2523
  import { jsx as jsx2 } from "react/jsx-runtime";
1821
2524
  var CONTAINER_ATTR = "data-ohw-ai-generated";
1822
2525
  var REPLACED_ATTR = "data-ohw-ai-replaced-by";
1823
- var REMOVED_ATTR = "data-ohw-ai-removed";
2526
+ var REMOVED_ATTR2 = "data-ohw-ai-removed";
1824
2527
  var TEMPLATE_HIDDEN_ATTR = "data-ohw-ai-template-hidden";
1825
2528
  var CHROME_SECTION_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
1826
2529
  function readRootVar(name) {
@@ -1844,13 +2547,13 @@ function deriveBrandOverride() {
1844
2547
  };
1845
2548
  }
1846
2549
  function deriveTemplateBrand() {
1847
- const dark = readRootVar("--color-dark");
1848
- const primary = readRootVar("--color-primary");
1849
- const light = readRootVar("--color-light");
2550
+ const primary = readRootVar("--brand-primary") || readRootVar("--color-primary");
2551
+ const dark = readRootVar("--brand-text") || readRootVar("--color-dark");
2552
+ const light = readRootVar("--brand-background") || readRootVar("--color-light");
1850
2553
  if (!dark || !primary || !light) return null;
1851
- const accent = readRootVar("--color-accent");
1852
- const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1853
- const body = readRootVar("--font-body");
2554
+ const accent = readRootVar("--brand-accent") || readRootVar("--color-accent");
2555
+ const heading = readRootVar("--brand-font-heading") || readRootVar("--font-heading") || readRootVar("--font-display");
2556
+ const body = readRootVar("--brand-font-body") || readRootVar("--font-body");
1854
2557
  return {
1855
2558
  palette: { dark, primary, accent: accent || dark, light },
1856
2559
  fonts: {
@@ -1859,12 +2562,32 @@ function deriveTemplateBrand() {
1859
2562
  }
1860
2563
  };
1861
2564
  }
1862
- function deriveTemplateButtonRadius() {
2565
+ function deriveTemplateButtonStyle() {
1863
2566
  if (typeof document === "undefined") return null;
1864
- const btn = document.querySelector('[data-ohw-role="button"]');
2567
+ const btn = Array.from(document.querySelectorAll('[data-ohw-role="button"]')).find(
2568
+ (el) => !el.closest(`[${CONTAINER_ATTR}]`)
2569
+ );
1865
2570
  if (!btn) return null;
1866
- const radius = getComputedStyle(btn).borderTopLeftRadius;
1867
- return radius || null;
2571
+ const cs = getComputedStyle(btn);
2572
+ const corners = [
2573
+ cs.borderTopLeftRadius,
2574
+ cs.borderTopRightRadius,
2575
+ cs.borderBottomRightRadius,
2576
+ cs.borderBottomLeftRadius
2577
+ ].map((v) => v || "0px");
2578
+ const radius = corners.every((v) => v === corners[0]) ? corners[0] : corners.join(" ");
2579
+ const px = (v) => parseFloat(v) || 0;
2580
+ const padY = Math.max(px(cs.paddingTop), px(cs.paddingBottom));
2581
+ const padX = Math.max(px(cs.paddingLeft), px(cs.paddingRight));
2582
+ return {
2583
+ radius: radius || "10px",
2584
+ padding: `${padY}px ${padX}px`,
2585
+ fontFamily: cs.fontFamily || "",
2586
+ fontSize: cs.fontSize || "",
2587
+ fontWeight: cs.fontWeight || "",
2588
+ letterSpacing: cs.letterSpacing || "",
2589
+ textTransform: cs.textTransform || ""
2590
+ };
1868
2591
  }
1869
2592
  var mounted = /* @__PURE__ */ new Map();
1870
2593
  function findTemplateSection(id) {
@@ -1914,18 +2637,18 @@ function placeContainer(container, entry) {
1914
2637
  }
1915
2638
  function syncRemovedSections(state) {
1916
2639
  const removed = new Set(state.removed ?? []);
1917
- for (const el of document.querySelectorAll(`[${REMOVED_ATTR}]`)) {
2640
+ for (const el of document.querySelectorAll(`[${REMOVED_ATTR2}]`)) {
1918
2641
  const id = el.getAttribute("data-ohw-section") ?? "";
1919
2642
  if (!removed.has(id)) {
1920
2643
  el.style.removeProperty("display");
1921
- el.removeAttribute(REMOVED_ATTR);
2644
+ el.removeAttribute(REMOVED_ATTR2);
1922
2645
  }
1923
2646
  }
1924
2647
  for (const id of removed) {
1925
2648
  const section = findTemplateSection(id);
1926
2649
  if (section && !section.hasAttribute(REPLACED_ATTR)) {
1927
2650
  section.style.display = "none";
1928
- section.setAttribute(REMOVED_ATTR, "");
2651
+ section.setAttribute(REMOVED_ATTR2, "");
1929
2652
  }
1930
2653
  }
1931
2654
  }
@@ -1942,7 +2665,7 @@ function syncTemplateHidden(state, pageHasSections) {
1942
2665
  if (el.hasAttribute(CONTAINER_ATTR)) continue;
1943
2666
  if (CHROME_SECTION_IDS.has(el.getAttribute("data-ohw-section") ?? "")) continue;
1944
2667
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
1945
- if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR)) continue;
2668
+ if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR2)) continue;
1946
2669
  el.style.display = "none";
1947
2670
  el.setAttribute(TEMPLATE_HIDDEN_ATTR, "");
1948
2671
  }
@@ -1966,18 +2689,23 @@ function syncReplacedOriginals(state) {
1966
2689
  }
1967
2690
  }
1968
2691
  var sectionOrderIndex = /* @__PURE__ */ new Map();
2692
+ var removedSectionIds = /* @__PURE__ */ new Set();
1969
2693
  function setAiSectionOrder(raw, currentPath) {
1970
2694
  const next = /* @__PURE__ */ new Map();
2695
+ const removed = /* @__PURE__ */ new Set();
1971
2696
  if (raw) {
1972
2697
  try {
1973
2698
  const entries = JSON.parse(raw);
1974
2699
  for (const entry of entries) {
1975
- if (!entry.pagePath || entry.pagePath === currentPath) next.set(entry.instanceId, entry.order);
2700
+ if (entry.pagePath && entry.pagePath !== currentPath) continue;
2701
+ next.set(entry.instanceId, entry.order);
2702
+ if (entry.removed) removed.add(entry.instanceId);
1976
2703
  }
1977
2704
  } catch {
1978
2705
  }
1979
2706
  }
1980
2707
  sectionOrderIndex = next;
2708
+ removedSectionIds = removed;
1981
2709
  }
1982
2710
  function applyExplicitOrder(entries) {
1983
2711
  if (sectionOrderIndex.size === 0) return entries;
@@ -2013,11 +2741,23 @@ function orderByChain(sections) {
2013
2741
  for (const root of roots) visit(root);
2014
2742
  return out.length === sections.length ? out : sections;
2015
2743
  }
2744
+ function syncSoftRemovedGenerated() {
2745
+ for (const [id, section] of mounted) {
2746
+ const el = section.container;
2747
+ if (removedSectionIds.has(id)) {
2748
+ el.style.display = "none";
2749
+ el.setAttribute(REMOVED_ATTR, "");
2750
+ } else if (el.hasAttribute(REMOVED_ATTR)) {
2751
+ el.style.removeProperty("display");
2752
+ el.removeAttribute(REMOVED_ATTR);
2753
+ }
2754
+ }
2755
+ }
2016
2756
  function applyAiSectionsToDom(state, options) {
2017
2757
  if (typeof document === "undefined") return;
2018
2758
  const brandOverride = deriveBrandOverride();
2019
2759
  const templateBrand = deriveTemplateBrand();
2020
- const templateButtonRadius = deriveTemplateButtonRadius();
2760
+ const templateButtonStyle = deriveTemplateButtonStyle();
2021
2761
  const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
2022
2762
  const pagePath = window.location.pathname;
2023
2763
  const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
@@ -2057,7 +2797,7 @@ function applyAiSectionsToDom(state, options) {
2057
2797
  {
2058
2798
  tree: entry.tree,
2059
2799
  brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2060
- buttonRadius: templateButtonRadius,
2800
+ buttonStyle: templateButtonStyle,
2061
2801
  resolveMedia,
2062
2802
  editKeyPrefix: `ai.${entry.id}`
2063
2803
  }
@@ -2080,6 +2820,7 @@ function applyAiSectionsToDom(state, options) {
2080
2820
  syncReplacedOriginals(state);
2081
2821
  syncRemovedSections(state);
2082
2822
  syncTemplateHidden(state, pageSections.length > 0);
2823
+ syncSoftRemovedGenerated();
2083
2824
  }
2084
2825
 
2085
2826
  // src/useLinkHrefGuardian.ts
@@ -7809,6 +8550,7 @@ function MediaOverlay({
7809
8550
  (prev) => prev && prev.fullWidth === width && prev.height === height ? prev : { fullWidth: width, height }
7810
8551
  );
7811
8552
  }, [isVideo]);
8553
+ const replaceLabel = isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image";
7812
8554
  const replaceMode = buttonSize ? replaceButtonMode({ width: rect.width, height: rect.height }, buttonSize) : "none";
7813
8555
  const box = {
7814
8556
  position: "fixed",
@@ -7938,17 +8680,17 @@ function MediaOverlay({
7938
8680
  },
7939
8681
  children: [
7940
8682
  isVideo ? /* @__PURE__ */ jsx15(Film, { size: 14 }) : /* @__PURE__ */ jsx15(ImageIcon, { size: 14 }),
7941
- isVideo ? "Replace video" : "Replace image"
8683
+ replaceLabel
7942
8684
  ]
7943
8685
  }
7944
8686
  ),
7945
- replaceMode === "none" ? null : /* @__PURE__ */ jsxs7(
8687
+ showChrome && replaceMode !== "none" && /* @__PURE__ */ jsxs7(
7946
8688
  Button,
7947
8689
  {
7948
8690
  "data-ohw-media-overlay": "",
7949
8691
  variant: "outline",
7950
8692
  size: "sm",
7951
- "aria-label": isVideo ? "Replace video" : "Replace image",
8693
+ "aria-label": replaceLabel,
7952
8694
  className: "gap-1.5 cursor-pointer hover:bg-background",
7953
8695
  style: {
7954
8696
  ...OVERLAY_BUTTON_STYLE,
@@ -7971,7 +8713,7 @@ function MediaOverlay({
7971
8713
  },
7972
8714
  children: [
7973
8715
  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
8716
+ replaceMode === "full" ? replaceLabel : null
7975
8717
  ]
7976
8718
  }
7977
8719
  )
@@ -8008,219 +8750,37 @@ function CarouselOverlay({
8008
8750
  width: rect.width,
8009
8751
  height: rect.height,
8010
8752
  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, "");
8753
+ pointerEvents: "auto",
8754
+ boxShadow: "inset 0 0 0 1.5px var(--color-primary)",
8755
+ background: "color-mix(in srgb, var(--color-primary) 20%, transparent)"
8756
+ },
8757
+ onClick: () => onEdit(hover.key),
8758
+ children: /* @__PURE__ */ jsxs8(
8759
+ Button,
8760
+ {
8761
+ "data-ohw-carousel-overlay": "",
8762
+ variant: "outline",
8763
+ size: "sm",
8764
+ className: "cursor-pointer gap-1.5 hover:bg-background",
8765
+ style: OVERLAY_BUTTON_STYLE2,
8766
+ onMouseDown: (e) => e.preventDefault(),
8767
+ onClick: (e) => {
8768
+ e.stopPropagation();
8769
+ onEdit(hover.key);
8770
+ },
8771
+ children: [
8772
+ /* @__PURE__ */ jsx16(GalleryHorizontal, { size: 14 }),
8773
+ "Edit gallery"
8774
+ ]
8775
+ }
8776
+ )
8132
8777
  }
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
8778
  );
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
8779
  }
8222
8780
 
8223
8781
  // src/ui/ai-section/AiSectionOverlay.tsx
8782
+ import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
8783
+ import { Check, X } from "lucide-react";
8224
8784
  import { Fragment as Fragment5, jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
8225
8785
  function findSectionElement(instanceId) {
8226
8786
  const escaped = CSS.escape(instanceId);
@@ -12936,6 +13496,7 @@ function readLogoSizeState(content, placement) {
12936
13496
  function getLogoElement(el) {
12937
13497
  const marked = el.closest('[data-ohw-role="logo"], [data-ohw-logo]');
12938
13498
  if (marked) return marked;
13499
+ if (el.closest('[data-ohw-editable="icon"]')) return null;
12939
13500
  const root = el.closest("nav, [data-ohw-nav-root], footer");
12940
13501
  if (!root) return null;
12941
13502
  const anchor = el.closest("a");
@@ -14317,6 +14878,9 @@ function collectEditableNodes(extraContent, root = document) {
14317
14878
  if (el.dataset.ohwEditable === "link") {
14318
14879
  return { key: el.dataset.ohwKey ?? "", type: "link", text: getLinkHref3(el) };
14319
14880
  }
14881
+ if (el.dataset.ohwEditable === "map") {
14882
+ return { key: el.dataset.ohwKey ?? "", type: "map", text: el.dataset.ohwMapQuery ?? "" };
14883
+ }
14320
14884
  return {
14321
14885
  key: el.dataset.ohwKey ?? "",
14322
14886
  type: el.dataset.ohwEditable ?? "text",
@@ -14870,21 +15434,10 @@ function parseSchedulingInsertAfter(insertAfter) {
14870
15434
  insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
14871
15435
  };
14872
15436
  }
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;
15437
+ function resolveEntryAnchor(entry) {
15438
+ if (entry.anchorId !== void 0) return { anchorId: entry.anchorId, beforeId: entry.beforeId ?? null };
15439
+ const parsed = parseSchedulingInsertAfter(entry.insertAfter);
15440
+ return { anchorId: parsed.anchor, beforeId: parsed.insertBefore };
14888
15441
  }
14889
15442
  function schedulingMountDepth(insertAfter) {
14890
15443
  if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
@@ -14901,8 +15454,7 @@ function getPageSchedulingEntries(raw) {
14901
15454
  }
14902
15455
  }
14903
15456
  function isSchedulingWidgetMissing(entry) {
14904
- const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
14905
- return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
15457
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(entry.insertAfter)}"]`);
14906
15458
  }
14907
15459
  function hasMissingSchedulingWidgets(entries) {
14908
15460
  return entries.some(isSchedulingWidgetMissing);
@@ -14932,16 +15484,17 @@ function initSectionsFromContent(content, removeExisting = false) {
14932
15484
  } catch {
14933
15485
  }
14934
15486
  }
14935
- function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
14936
- const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
14937
- const sectionId = schedulingSectionId(effectiveInsertAfter);
15487
+ function mountSchedulingWidget(anchorId, notifyOnConnect = false, scheduleId, beforeId, existingWidgetId) {
15488
+ const widgetId = existingWidgetId ?? (beforeId ? `${anchorId}${INSERT_BEFORE_MARKER}${beforeId}` : anchorId);
15489
+ const sectionId = schedulingSectionId(widgetId);
14938
15490
  if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
14939
- const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
14940
- if (!mountPoint) return false;
15491
+ const anchorEl = document.querySelector(`[data-ohw-section="${anchorId}"]`);
15492
+ if (!anchorEl) return false;
15493
+ const mountPoint = anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
14941
15494
  const container = document.createElement("div");
14942
15495
  container.dataset.ohwSectionContainer = "scheduling";
14943
- if (insertBefore) {
14944
- const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
15496
+ if (beforeId) {
15497
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${beforeId}"]`);
14945
15498
  const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
14946
15499
  if (!beforePoint) return false;
14947
15500
  beforePoint.insertAdjacentElement("beforebegin", container);
@@ -14952,19 +15505,25 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
14952
15505
  }
14953
15506
  tail.insertAdjacentElement("afterend", container);
14954
15507
  }
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
- });
15508
+ try {
15509
+ const root = createRoot2(container);
15510
+ flushSync2(() => {
15511
+ root.render(
15512
+ /* @__PURE__ */ jsx33(
15513
+ SchedulingWidget,
15514
+ {
15515
+ notifyOnConnect,
15516
+ initialScheduleId: scheduleId,
15517
+ insertAfter: widgetId
15518
+ }
15519
+ )
15520
+ );
15521
+ });
15522
+ } catch (err) {
15523
+ console.error("[ow:scheduling] render threw", err);
15524
+ container.remove();
15525
+ return false;
15526
+ }
14968
15527
  const tracker = getSectionsTracker();
14969
15528
  let sections = [];
14970
15529
  try {
@@ -14972,10 +15531,12 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
14972
15531
  } catch {
14973
15532
  }
14974
15533
  const inEditor = typeof window !== "undefined" && window.self !== window.top;
14975
- if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
15534
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === widgetId)) {
14976
15535
  sections.push({
14977
15536
  type: "scheduling",
14978
- insertAfter: effectiveInsertAfter,
15537
+ insertAfter: widgetId,
15538
+ anchorId,
15539
+ beforeId: beforeId ?? null,
14979
15540
  pagePath: window.location.pathname,
14980
15541
  ...scheduleId ? { scheduleId } : {}
14981
15542
  });
@@ -14989,7 +15550,8 @@ function mountSchedulingEntries(entries, notifyOnConnect = false) {
14989
15550
  for (let i = pending.length - 1; i >= 0; i--) {
14990
15551
  const entry = pending[i];
14991
15552
  const shouldNotify = typeof notifyOnConnect === "function" ? notifyOnConnect(entry) : notifyOnConnect;
14992
- if (mountSchedulingWidget(entry.insertAfter, shouldNotify, entry.scheduleId ?? null)) {
15553
+ const { anchorId, beforeId } = resolveEntryAnchor(entry);
15554
+ if (mountSchedulingWidget(anchorId, shouldNotify, entry.scheduleId ?? null, beforeId, entry.insertAfter)) {
14993
15555
  pending.splice(i, 1);
14994
15556
  }
14995
15557
  }
@@ -15081,7 +15643,7 @@ var EDITOR_CHROME_SELECTOR = '[data-ohw-toolbar], [data-ohw-form-toolbar], [data
15081
15643
  function isOverEditorChrome(x, y) {
15082
15644
  return document.elementsFromPoint(x, y).some((el) => el instanceof HTMLElement && el.matches(EDITOR_CHROME_SELECTOR));
15083
15645
  }
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"])';
15646
+ 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
15647
  function getVideoEl2(el) {
15086
15648
  return el instanceof HTMLVideoElement ? el : el.querySelector("video");
15087
15649
  }
@@ -15137,6 +15699,12 @@ function applyVideoSettingNode(key, val) {
15137
15699
  });
15138
15700
  return true;
15139
15701
  }
15702
+ function applyMapQuery(el, val) {
15703
+ if (!(el instanceof HTMLIFrameElement)) return;
15704
+ const nextSrc = `https://www.google.com/maps?q=${encodeURIComponent(val)}&output=embed`;
15705
+ if (el.src !== nextSrc) el.src = nextSrc;
15706
+ el.setAttribute("data-ohw-map-query", val);
15707
+ }
15140
15708
  function applyLinkByKey(key, val) {
15141
15709
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
15142
15710
  if (el.dataset.ohwEditable === "link") applyLinkHref(el, val);
@@ -15147,6 +15715,11 @@ function applyLinkByKey(key, val) {
15147
15715
  hrefAnchors.forEach((el) => applyLinkHref(el, val));
15148
15716
  }
15149
15717
  }
15718
+ function isInsideLinkEditor(target) {
15719
+ return Boolean(
15720
+ 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"]')
15721
+ );
15722
+ }
15150
15723
  function isInsideFloatingPanel(target) {
15151
15724
  return Boolean(target.closest("[data-ohw-floating-panel]"));
15152
15725
  }
@@ -15154,11 +15727,6 @@ function isPointOverFloatingPanel(clientX, clientY) {
15154
15727
  const el = document.elementFromPoint(clientX, clientY);
15155
15728
  return Boolean(el instanceof Element && el.closest("[data-ohw-floating-panel]"));
15156
15729
  }
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
15730
  function getHrefKeyFromElement(el) {
15163
15731
  if (!el) return null;
15164
15732
  const anchor = el.closest("[data-ohw-href-key]");
@@ -15417,7 +15985,7 @@ function getNavigationSelectionParent(el) {
15417
15985
  if (!isFooterLinksContainer(el) && (el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isInferredFooterGroup2(el))) {
15418
15986
  return getFooterLinksContainer();
15419
15987
  }
15420
- if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || isFooterLinksContainer(el)) {
15988
+ 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
15989
  return getNavigationRoot(el);
15422
15990
  }
15423
15991
  return null;
@@ -15632,7 +16200,6 @@ var ICONS = {
15632
16200
  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
16201
  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
16202
  };
15635
- var HEIGHT_SETTLE_DELAYS = [150, 400, 800];
15636
16203
  var SELECTION_CHROME_GAP2 = 4;
15637
16204
  var TOOLBAR_STROKE_GAP2 = 4;
15638
16205
  var TOOLBAR_OFFSET_FROM_ELEMENT = SELECTION_CHROME_GAP2 + TOOLBAR_STROKE_GAP2;
@@ -16012,6 +16579,7 @@ function StateToggle({
16012
16579
  );
16013
16580
  }
16014
16581
  var contentCache = /* @__PURE__ */ new Map();
16582
+ var fetchedContentPaths = /* @__PURE__ */ new Set();
16015
16583
  var brandingCache = /* @__PURE__ */ new Map();
16016
16584
  var OHW_LOADER_STYLE = {
16017
16585
  position: "fixed",
@@ -16541,13 +17109,6 @@ function OhhwellsBridge() {
16541
17109
  const [isItemDragging, setIsItemDragging] = useState13(false);
16542
17110
  const [isFooterFrameSelection, setIsFooterFrameSelection] = useState13(false);
16543
17111
  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
17112
  const [navDropdownPreviewOpen, setNavDropdownPreviewOpen] = useState13(null);
16552
17113
  const [footerHeadingVisible, setFooterHeadingVisible] = useState13(null);
16553
17114
  const footerDragRef = useRef10(null);
@@ -16565,6 +17126,13 @@ function OhhwellsBridge() {
16565
17126
  const brandKitRef = useRef10("");
16566
17127
  const stylesRef = useRef10("");
16567
17128
  const pendingDeleteUndoRef = useRef10(null);
17129
+ const [floatingPanel, setFloatingPanel] = useState13(null);
17130
+ const floatingPanelOpenRef = useRef10(false);
17131
+ const setFloatingPanelRef = useRef10(setFloatingPanel);
17132
+ const [floatingPanelPos, setFloatingPanelPos] = useState13(null);
17133
+ const [logoSizeDraft, setLogoSizeDraft] = useState13(null);
17134
+ const [editorViewport, setEditorViewport] = useState13("desktop");
17135
+ const [parentScrollSnap, setParentScrollSnap] = useState13(null);
16568
17136
  const [sitePages, setSitePages] = useState13([]);
16569
17137
  const [sectionsByPath, setSectionsByPath] = useState13({});
16570
17138
  const sectionsPrefetchGenRef = useRef10(0);
@@ -16573,7 +17141,18 @@ function OhhwellsBridge() {
16573
17141
  const linkPopoverOpenRef = useRef10(false);
16574
17142
  const linkPopoverGraceUntilRef = useRef10(0);
16575
17143
  setLinkPopoverRef.current = setLinkPopover;
17144
+ setFloatingPanelRef.current = setFloatingPanel;
16576
17145
  linkPopoverSessionRef.current = linkPopover;
17146
+ floatingPanelOpenRef.current = Boolean(floatingPanel);
17147
+ useEffect13(() => {
17148
+ const syncViewport = () => {
17149
+ const next = window.innerWidth <= 480 ? "mobile" : "desktop";
17150
+ setEditorViewport((prev) => prev === next ? prev : next);
17151
+ };
17152
+ syncViewport();
17153
+ window.addEventListener("resize", syncViewport);
17154
+ return () => window.removeEventListener("resize", syncViewport);
17155
+ }, []);
16577
17156
  const {
16578
17157
  navDragRef,
16579
17158
  navDropSlots,
@@ -17898,17 +18477,19 @@ function OhhwellsBridge() {
17898
18477
  }
17899
18478
  if (typeof content[STYLE_STORE_KEY] === "string") {
17900
18479
  stylesRef.current = content[STYLE_STORE_KEY];
18480
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
17901
18481
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17902
18482
  }
17903
18483
  applyBrandChrome(content);
18484
+ initSectionInstancesFromContent(content, window.location.pathname);
17904
18485
  for (const [key, val] of Object.entries(content)) {
17905
18486
  if (key === "__ohw_sections") continue;
17906
18487
  if (key === AI_SECTIONS_KEY) continue;
18488
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
18489
+ if (LOGO_IMAGE_KEYS.includes(key)) continue;
17907
18490
  if (key === BRAND_KIT_KEY) continue;
17908
18491
  if (key === STYLE_STORE_KEY) continue;
17909
18492
  if (BRAND_CHROME_KEYS.has(key)) continue;
17910
- if (key === LOGO_PLACEHOLDER_KEY) continue;
17911
- if (LOGO_IMAGE_KEYS.includes(key)) continue;
17912
18493
  if (applyVideoSettingNode(key, val)) continue;
17913
18494
  if (applyCarouselNode(key, val)) continue;
17914
18495
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -17936,6 +18517,8 @@ function OhhwellsBridge() {
17936
18517
  }
17937
18518
  } else if (el.dataset.ohwEditable === "link") {
17938
18519
  applyLinkHref(el, val);
18520
+ } else if (el.dataset.ohwEditable === "map") {
18521
+ applyMapQuery(el, val);
17939
18522
  } else if (el.dataset.ohwEditable === "icon") {
17940
18523
  applyIconMarkup(el, val);
17941
18524
  } else if (el.dataset.ohwEditable === "form") {
@@ -17956,7 +18539,6 @@ function OhhwellsBridge() {
17956
18539
  if (isEditModeRef.current) requestMissingSocialIconsRef.current();
17957
18540
  enforceLinkHrefs();
17958
18541
  initSectionsFromContent(content, true);
17959
- initSectionInstancesFromContent(content, window.location.pathname);
17960
18542
  sectionsLoadedRef.current = true;
17961
18543
  pendingScheduleConfigRequests.current.splice(0).forEach(processConfigRequest);
17962
18544
  if (imageLoads.length === 0) return Promise.resolve();
@@ -17975,7 +18557,9 @@ function OhhwellsBridge() {
17975
18557
  let cancelled = false;
17976
18558
  setFetchState("loading");
17977
18559
  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) => {
18560
+ const initialPath = pathname;
18561
+ fetchedContentPaths.add(`${subdomain}::${initialPath}`);
18562
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(initialPath)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
17979
18563
  if (cancelled) return;
17980
18564
  const content = data?.content ?? {};
17981
18565
  const branding = Boolean(data?.showBranding);
@@ -18109,16 +18693,17 @@ function OhhwellsBridge() {
18109
18693
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
18110
18694
  }
18111
18695
  if (typeof content[STYLE_STORE_KEY] === "string") {
18696
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
18112
18697
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
18113
18698
  }
18114
18699
  for (const [key, val] of Object.entries(content)) {
18115
18700
  if (key === "__ohw_sections") continue;
18116
18701
  if (key === AI_SECTIONS_KEY) continue;
18702
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
18703
+ if (LOGO_IMAGE_KEYS.includes(key)) continue;
18117
18704
  if (key === BRAND_KIT_KEY) continue;
18118
18705
  if (key === STYLE_STORE_KEY) continue;
18119
18706
  if (BRAND_CHROME_KEYS.has(key)) continue;
18120
- if (key === LOGO_PLACEHOLDER_KEY) continue;
18121
- if (LOGO_IMAGE_KEYS.includes(key)) continue;
18122
18707
  if (applyVideoSettingNode(key, val)) continue;
18123
18708
  if (applyCarouselNode(key, val)) continue;
18124
18709
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -18133,6 +18718,8 @@ function OhhwellsBridge() {
18133
18718
  if (video && video.src !== val) applyVideoSrc(video, val);
18134
18719
  } else if (el.dataset.ohwEditable === "link") {
18135
18720
  applyLinkHref(el, val);
18721
+ } else if (el.dataset.ohwEditable === "map") {
18722
+ applyMapQuery(el, val);
18136
18723
  } else if (el.dataset.ohwEditable === "form") {
18137
18724
  } else if (isIconMarkupValue(val)) {
18138
18725
  } else if (el.innerHTML !== val) {
@@ -18164,6 +18751,17 @@ function OhhwellsBridge() {
18164
18751
  debounceTimer = setTimeout(applyFromCache, 150);
18165
18752
  };
18166
18753
  applyFromCache();
18754
+ const pathCacheKey = `${subdomain}::${pathname}`;
18755
+ if (!fetchedContentPaths.has(pathCacheKey)) {
18756
+ fetchedContentPaths.add(pathCacheKey);
18757
+ const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
18758
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(pathname)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18759
+ if (!data?.content) return;
18760
+ contentCache.set(subdomain, data.content);
18761
+ applyFromCache();
18762
+ }).catch(() => {
18763
+ });
18764
+ }
18167
18765
  observer = new MutationObserver(scheduleApply);
18168
18766
  observer.observe(document.body, { childList: true, subtree: true });
18169
18767
  return () => {
@@ -18279,26 +18877,11 @@ function OhhwellsBridge() {
18279
18877
  const t2 = setTimeout(measure, 500);
18280
18878
  const ro = new ResizeObserver(schedule);
18281
18879
  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
18880
  return () => {
18296
18881
  clearTimeout(t1);
18297
18882
  clearTimeout(t2);
18298
18883
  if (raf != null) cancelAnimationFrame(raf);
18299
18884
  ro.disconnect();
18300
- clearResizeTimers();
18301
- window.removeEventListener("resize", handleResize);
18302
18885
  };
18303
18886
  }, [pathname, isEditMode, postToParent2]);
18304
18887
  useEffect13(() => {
@@ -18544,9 +19127,6 @@ function OhhwellsBridge() {
18544
19127
  if (target.closest("[data-ohw-state-toggle]")) return;
18545
19128
  if (target.closest("[data-ohw-max-badge]")) return;
18546
19129
  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
19130
  if (isInsideFloatingPanel(target)) return;
18551
19131
  if (target.closest("[data-ohw-form-toolbar]")) return;
18552
19132
  if (target.closest(
@@ -18554,6 +19134,9 @@ function OhhwellsBridge() {
18554
19134
  )) {
18555
19135
  return;
18556
19136
  }
19137
+ if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
19138
+ clearMediaSelectionRef.current();
19139
+ }
18557
19140
  {
18558
19141
  const formEl = getFormElement(target);
18559
19142
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
@@ -18705,14 +19288,6 @@ function OhhwellsBridge() {
18705
19288
  }
18706
19289
  const clickedButton = findClosestButtonLike(target);
18707
19290
  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
19291
  if (isMediaEditable(editable) && !buttonOnMedia) {
18717
19292
  e.preventDefault();
18718
19293
  e.stopPropagation();
@@ -18739,11 +19314,6 @@ function OhhwellsBridge() {
18739
19314
  const hrefLookupTarget = buttonOnMedia ? clickedButton : editable;
18740
19315
  const hrefCtx = getHrefKeyFromElement(hrefLookupTarget);
18741
19316
  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
19317
  if (navAnchor) {
18748
19318
  e.preventDefault();
18749
19319
  e.stopPropagation();
@@ -18913,6 +19483,9 @@ function OhhwellsBridge() {
18913
19483
  setHoveredItemRect(null);
18914
19484
  hoveredNavContainerRef.current = null;
18915
19485
  setHoveredNavContainerRect(null);
19486
+ siblingHintElRef.current = null;
19487
+ setSiblingHintRect(null);
19488
+ setSiblingHintRects([]);
18916
19489
  return;
18917
19490
  }
18918
19491
  {
@@ -19031,7 +19604,6 @@ function OhhwellsBridge() {
19031
19604
  hoveredNavContainerRef.current = null;
19032
19605
  setHoveredNavContainerRect(null);
19033
19606
  hoveredItemElRef.current = editable;
19034
- setHoveredItemRect(isSelectedForHoverRef.current(editable) ? null : editable.getBoundingClientRect());
19035
19607
  }
19036
19608
  }
19037
19609
  }
@@ -19328,7 +19900,7 @@ function OhhwellsBridge() {
19328
19900
  }
19329
19901
  };
19330
19902
  const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false) => {
19331
- if (linkPopoverOpenRef.current) {
19903
+ if (linkPopoverOpenRef.current || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
19332
19904
  if (hoveredImageRef.current) {
19333
19905
  hoveredImageRef.current = null;
19334
19906
  hoveredImageHasTextOverlapRef.current = false;
@@ -19693,8 +20265,7 @@ function OhhwellsBridge() {
19693
20265
  };
19694
20266
  const handleMouseMove = (e) => {
19695
20267
  const { clientX, clientY } = e;
19696
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
19697
- if (isOverEditorChrome(clientX, clientY)) {
20268
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY) || isOverEditorChrome(clientX, clientY)) {
19698
20269
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
19699
20270
  formHoverElRef.current = null;
19700
20271
  setFormHoverRect(null);
@@ -19702,6 +20273,12 @@ function OhhwellsBridge() {
19702
20273
  setHoveredItemRect(null);
19703
20274
  hoveredNavContainerRef.current = null;
19704
20275
  setHoveredNavContainerRect(null);
20276
+ siblingHintElRef.current = null;
20277
+ setSiblingHintRect(null);
20278
+ setSiblingHintRects([]);
20279
+ dismissImageHover();
20280
+ clearImageHover();
20281
+ setSectionGap(null);
19705
20282
  return;
19706
20283
  }
19707
20284
  if (probeSocialsRowAt(clientX, clientY)) return;
@@ -19713,7 +20290,11 @@ function OhhwellsBridge() {
19713
20290
  if (e.data?.type !== "ow:pointer-sync") return;
19714
20291
  const { clientX, clientY } = e.data;
19715
20292
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
19716
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
20293
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
20294
+ dismissImageHover();
20295
+ clearImageHover();
20296
+ return;
20297
+ }
19717
20298
  if (probeSocialsRowAt(clientX, clientY)) return;
19718
20299
  probeSectionGapAt(clientX, clientY);
19719
20300
  probeImageAt(clientX, clientY);
@@ -19992,6 +20573,44 @@ function OhhwellsBridge() {
19992
20573
  if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
19993
20574
  }, 400));
19994
20575
  };
20576
+ const reapCommittedAiSections = (excludeIds) => {
20577
+ const aiState = parseAiSectionsState(aiSectionsRef.current);
20578
+ if (aiState.sections.length === 0) return [];
20579
+ let orderEntries = [];
20580
+ try {
20581
+ const parsed = JSON.parse(editContentRef.current[SECTION_ORDER_KEY] ?? "[]");
20582
+ if (Array.isArray(parsed)) orderEntries = parsed;
20583
+ } catch {
20584
+ return [];
20585
+ }
20586
+ const removedIds = orderEntries.filter((entry) => entry && typeof entry.instanceId === "string" && entry.removed).map((entry) => entry.instanceId);
20587
+ if (removedIds.length === 0) return [];
20588
+ const result = reapRemovedAiSections(aiState, parseStyleStore(stylesRef.current), removedIds, excludeIds);
20589
+ if (!result.changed) return [];
20590
+ const nodes = [];
20591
+ aiSectionsRef.current = serializeAiSectionsState(result.state);
20592
+ nodes.push({ key: AI_SECTIONS_KEY, text: aiSectionsRef.current });
20593
+ const reaped = new Set(result.reapedIds);
20594
+ const nextOrderJson = JSON.stringify(orderEntries.filter((entry) => !reaped.has(entry.instanceId)));
20595
+ editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: nextOrderJson };
20596
+ setAiSectionOrder(nextOrderJson, window.location.pathname);
20597
+ nodes.push({ key: SECTION_ORDER_KEY, text: nextOrderJson });
20598
+ if (result.store) {
20599
+ stylesRef.current = JSON.stringify(result.store);
20600
+ nodes.push({ key: STYLE_STORE_KEY, text: stylesRef.current });
20601
+ }
20602
+ const nextContent = { ...editContentRef.current };
20603
+ for (const key of Object.keys(nextContent)) {
20604
+ if (key.startsWith(AI_SLOT_KEY_PREFIX) && result.slotPrefixes.some((prefix) => key.startsWith(prefix)) && nextContent[key] !== "") {
20605
+ nextContent[key] = "";
20606
+ nodes.push({ key, text: "" });
20607
+ }
20608
+ }
20609
+ editContentRef.current = nextContent;
20610
+ applyAiSectionsToDom(result.state);
20611
+ applyStylesToDom(parseStyleStore(stylesRef.current));
20612
+ return nodes;
20613
+ };
19995
20614
  const handleHydrate = (e) => {
19996
20615
  if (e.data?.type !== "ow:hydrate") return;
19997
20616
  const content = e.data.content;
@@ -20010,9 +20629,11 @@ function OhhwellsBridge() {
20010
20629
  }
20011
20630
  if (typeof content[STYLE_STORE_KEY] === "string") {
20012
20631
  stylesRef.current = content[STYLE_STORE_KEY];
20632
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
20013
20633
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
20014
20634
  }
20015
20635
  applyBrandChrome(content);
20636
+ initSectionInstancesFromContent(content, window.location.pathname);
20016
20637
  let sectionsJson = null;
20017
20638
  for (const [key, val] of Object.entries(content)) {
20018
20639
  if (key === "__ohw_sections") {
@@ -20020,11 +20641,11 @@ function OhhwellsBridge() {
20020
20641
  continue;
20021
20642
  }
20022
20643
  if (key === AI_SECTIONS_KEY) continue;
20644
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
20645
+ if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
20023
20646
  if (key === BRAND_KIT_KEY) continue;
20024
20647
  if (key === STYLE_STORE_KEY) continue;
20025
20648
  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
20649
  if (applyVideoSettingNode(key, val)) continue;
20029
20650
  if (applyCarouselNode(key, val)) continue;
20030
20651
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -20038,6 +20659,8 @@ function OhhwellsBridge() {
20038
20659
  if (video && video.src !== val) applyVideoSrc(video, val);
20039
20660
  } else if (el.dataset.ohwEditable === "link") {
20040
20661
  applyLinkHref(el, val);
20662
+ } else if (el.dataset.ohwEditable === "map") {
20663
+ applyMapQuery(el, val);
20041
20664
  } else if (el.dataset.ohwEditable === "icon") {
20042
20665
  applyIconMarkup(el, val);
20043
20666
  } else if (isIconMarkupValue(val)) {
@@ -20054,12 +20677,16 @@ function OhhwellsBridge() {
20054
20677
  sectionsLoadedRef.current = true;
20055
20678
  pendingScheduleConfigRequests.current.splice(0).forEach(processConfigRequest);
20056
20679
  }
20057
- initSectionInstancesFromContent(content, window.location.pathname);
20058
20680
  editContentRef.current = { ...editContentRef.current, ...content };
20059
20681
  reconcileNavbarItemsFromContent(editContentRef.current);
20060
20682
  reconcileFooterOrderFromContent(editContentRef.current);
20061
20683
  syncNavigationDragCursorAttrs();
20062
20684
  enforceLinkHrefs();
20685
+ const hydrateReapExclude = /* @__PURE__ */ new Set();
20686
+ const hydratePendingUndo = pendingDeleteUndoRef.current;
20687
+ if (hydratePendingUndo?.sectionInstanceId) hydrateReapExclude.add(hydratePendingUndo.sectionInstanceId);
20688
+ const reapNodes = reapCommittedAiSections(hydrateReapExclude);
20689
+ if (reapNodes.length > 0) postToParentRef.current({ type: "ow:change", nodes: reapNodes });
20063
20690
  const hydratedHeight = document.body.scrollHeight;
20064
20691
  if (hydratedHeight > 50) postToParentRef.current({ type: "ow:height", height: hydratedHeight });
20065
20692
  postToParentRef.current({ type: "ow:hydrate-done" });
@@ -20199,12 +20826,35 @@ function OhhwellsBridge() {
20199
20826
  window.addEventListener("message", handleAiSetBrand);
20200
20827
  const handleAiSetStyles = (e) => {
20201
20828
  if (e.data?.type !== "ow:ai-set-styles") return;
20202
- const value = typeof e.data.value === "string" ? e.data.value : "";
20829
+ let value = typeof e.data.value === "string" ? e.data.value : "";
20203
20830
  const previous = stylesRef.current;
20831
+ let previousSections;
20832
+ const store = parseStyleStore(value);
20833
+ if (store) {
20834
+ const folded = foldAlignIntoTrees(parseAiSectionsState(aiSectionsRef.current), store);
20835
+ if (folded.changed) {
20836
+ const nextSections = serializeAiSectionsState(folded.state);
20837
+ if (nextSections !== aiSectionsRef.current) {
20838
+ previousSections = aiSectionsRef.current;
20839
+ aiSectionsRef.current = nextSections;
20840
+ applyAiSectionsToDom(folded.state);
20841
+ postToParentRef.current({
20842
+ type: "ow:change",
20843
+ nodes: [{ key: AI_SECTIONS_KEY, text: nextSections }]
20844
+ });
20845
+ }
20846
+ value = JSON.stringify(folded.store);
20847
+ }
20848
+ }
20204
20849
  stylesRef.current = value;
20205
20850
  applyStylesToDom(parseStyleStore(value));
20206
20851
  postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
20207
- postToParentRef.current({ type: "ow:ai-styles-applied", previous, value });
20852
+ postToParentRef.current({
20853
+ type: "ow:ai-styles-applied",
20854
+ previous,
20855
+ value,
20856
+ ...previousSections !== void 0 ? { previousSections } : {}
20857
+ });
20208
20858
  };
20209
20859
  window.addEventListener("message", handleAiSetStyles);
20210
20860
  const handleGetBrand = (e) => {
@@ -20243,6 +20893,7 @@ function OhhwellsBridge() {
20243
20893
  if (!entries) return;
20244
20894
  const orderJson = JSON.stringify(entries);
20245
20895
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20896
+ setAiSectionOrder(orderJson, window.location.pathname);
20246
20897
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
20247
20898
  aiSectionApiRef.current?.clear();
20248
20899
  window.dispatchEvent(new Event("resize"));
@@ -20251,6 +20902,7 @@ function OhhwellsBridge() {
20251
20902
  const actionId = newInstanceId();
20252
20903
  pendingDeleteUndoRef.current = {
20253
20904
  actionId,
20905
+ sectionInstanceId: instanceId,
20254
20906
  restore: () => {
20255
20907
  const restoredEntries = getPageSectionOrderEntries(
20256
20908
  editContentRef.current[SECTION_ORDER_KEY],
@@ -20260,6 +20912,7 @@ function OhhwellsBridge() {
20260
20912
  if (!restored) return;
20261
20913
  const restoredJson = JSON.stringify(restored);
20262
20914
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: restoredJson };
20915
+ setAiSectionOrder(restoredJson, window.location.pathname);
20263
20916
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: restoredJson }] });
20264
20917
  window.dispatchEvent(new Event("resize"));
20265
20918
  const restoreHeight = document.body.scrollHeight;
@@ -20276,6 +20929,34 @@ function OhhwellsBridge() {
20276
20929
  });
20277
20930
  };
20278
20931
  window.addEventListener("message", handleDeleteSection);
20932
+ const handleDuplicateSection = (e) => {
20933
+ if (e.data?.type !== "ow:duplicate-section") return;
20934
+ const instanceId = typeof e.data.instanceId === "string" ? e.data.instanceId : "";
20935
+ if (!instanceId) return;
20936
+ const newId = newInstanceId();
20937
+ const currentEntries = getPageSectionOrderEntries(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname);
20938
+ const result = duplicateSectionInstance(instanceId, newId, window.location.pathname, currentEntries);
20939
+ if (!result) return;
20940
+ const { entries, keyRekeys } = result;
20941
+ const orderJson = JSON.stringify(entries);
20942
+ const nodes = [{ key: SECTION_ORDER_KEY, text: orderJson }];
20943
+ for (const { from, to } of keyRekeys) {
20944
+ const inherited = editContentRef.current[from];
20945
+ if (inherited !== void 0) nodes.push({ key: to, text: inherited });
20946
+ }
20947
+ editContentRef.current = {
20948
+ ...editContentRef.current,
20949
+ ...Object.fromEntries(nodes.map(({ key, text }) => [key, text]))
20950
+ };
20951
+ setAiSectionOrder(orderJson, window.location.pathname);
20952
+ postToParentRef.current({ type: "ow:change", nodes });
20953
+ window.dispatchEvent(new Event("resize"));
20954
+ const duplicateHeight = document.body.scrollHeight;
20955
+ if (duplicateHeight > 50) postToParentRef.current({ type: "ow:height", height: duplicateHeight });
20956
+ const clone = document.querySelector(`[data-ohw-instance="${CSS.escape(newId)}"]`);
20957
+ if (clone) aiSectionApiRef.current?.selectFromElement(clone);
20958
+ };
20959
+ window.addEventListener("message", handleDuplicateSection);
20279
20960
  const handleDeactivate = (e) => {
20280
20961
  if (e.data?.type !== "ow:deactivate") return;
20281
20962
  if (document.documentElement.hasAttribute("data-ohw-panel-dragging")) return;
@@ -20285,6 +20966,12 @@ function OhhwellsBridge() {
20285
20966
  closeLinkPopoverRef.current();
20286
20967
  return;
20287
20968
  }
20969
+ if (floatingPanelOpenRef.current) {
20970
+ setFloatingPanelRef.current(null);
20971
+ deselectRef.current();
20972
+ deactivateRef.current();
20973
+ return;
20974
+ }
20288
20975
  deselectRef.current();
20289
20976
  deactivateRef.current();
20290
20977
  clearMediaSelectionRef.current();
@@ -20530,6 +21217,10 @@ function OhhwellsBridge() {
20530
21217
  };
20531
21218
  const handleSave = (e) => {
20532
21219
  if (e.data?.type !== "ow:save") return;
21220
+ const pendingUndo = pendingDeleteUndoRef.current;
21221
+ const reapExclude = /* @__PURE__ */ new Set();
21222
+ if (pendingUndo?.sectionInstanceId) reapExclude.add(pendingUndo.sectionInstanceId);
21223
+ const reapNodes = reapCommittedAiSections(reapExclude);
20533
21224
  const nodes = collectEditableNodes(editContentRef.current);
20534
21225
  const tracker = document.querySelector("[data-ohw-sections-tracker]");
20535
21226
  if (tracker?.textContent) nodes.push({ key: "__ohw_sections", type: "sections", text: tracker.textContent });
@@ -20549,6 +21240,11 @@ function OhhwellsBridge() {
20549
21240
  const text = specs.length ? JSON.stringify(specs) : editContentRef.current[fieldsKey(formKey)];
20550
21241
  if (text) nodes.push({ key: fieldsKey(formKey), type: "text", text });
20551
21242
  });
21243
+ for (const reapNode of reapNodes) {
21244
+ if (reapNode.key.startsWith(AI_SLOT_KEY_PREFIX) && !nodes.some((n) => n.key === reapNode.key)) {
21245
+ nodes.push({ key: reapNode.key, type: "text", text: reapNode.text });
21246
+ }
21247
+ }
20552
21248
  postToParentRef.current({ type: "ow:save-result", nodes });
20553
21249
  };
20554
21250
  const handleInsertSection = (e) => {
@@ -20559,8 +21255,12 @@ function OhhwellsBridge() {
20559
21255
  if (inserted) {
20560
21256
  const tracker = getSectionsTracker();
20561
21257
  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 });
21258
+ const reportHeight = () => {
21259
+ const h = document.body.scrollHeight;
21260
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
21261
+ };
21262
+ reportHeight();
21263
+ setTimeout(reportHeight, 500);
20564
21264
  }
20565
21265
  };
20566
21266
  const handleSwitchSchedule = (e) => {
@@ -20962,11 +21662,12 @@ function OhhwellsBridge() {
20962
21662
  window.removeEventListener("message", handleMoveSection);
20963
21663
  window.removeEventListener("message", handlePanelDragging);
20964
21664
  window.removeEventListener("message", handleDeleteSection);
21665
+ window.removeEventListener("message", handleDuplicateSection);
20965
21666
  window.removeEventListener("message", handleDeactivate);
20966
- document.documentElement.removeAttribute("data-ohw-panel-dragging");
20967
21667
  window.removeEventListener("message", handleToastAction);
20968
21668
  window.removeEventListener("message", handleFormCount);
20969
21669
  window.removeEventListener("message", handleUiEscape);
21670
+ document.documentElement.removeAttribute("data-ohw-panel-dragging");
20970
21671
  autoSaveTimers.current.forEach(clearTimeout);
20971
21672
  autoSaveTimers.current.clear();
20972
21673
  if (imageUnhoverTimerRef.current) clearTimeout(imageUnhoverTimerRef.current);
@@ -21169,7 +21870,7 @@ function OhhwellsBridge() {
21169
21870
  postToParent2({
21170
21871
  type: "ow:ready",
21171
21872
  version: "1",
21172
- bridgeVersion: "0.1.84",
21873
+ bridgeVersion: "0.1.86",
21173
21874
  path: pathname,
21174
21875
  nodes: collectEditableNodes(editContentRef.current),
21175
21876
  sections
@@ -22088,6 +22789,59 @@ function OhhwellsBridge() {
22088
22789
  ) : null
22089
22790
  ] });
22090
22791
  }
22792
+
22793
+ // src/ui/EmptySection.tsx
22794
+ import Link3 from "next/link";
22795
+ import { Fragment as Fragment9, jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
22796
+ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey }) {
22797
+ return /* @__PURE__ */ jsxs21(Fragment9, { children: [
22798
+ /* @__PURE__ */ jsx34(
22799
+ "p",
22800
+ {
22801
+ style: {
22802
+ fontFamily: "var(--brand-font-body)",
22803
+ fontSize: "0.75rem",
22804
+ fontWeight: 500,
22805
+ letterSpacing: "0.15em",
22806
+ textTransform: "uppercase",
22807
+ color: "var(--brand-accent)",
22808
+ marginBottom: "1.5rem"
22809
+ },
22810
+ children: /* @__PURE__ */ jsx34(Link3, { href: homeHref, style: { color: "inherit" }, children: /* @__PURE__ */ jsx34("span", { ...eyebrowKey ? { "data-ohw-editable": "text", "data-ohw-key": eyebrowKey } : {}, children: "Home" }) })
22811
+ }
22812
+ ),
22813
+ /* @__PURE__ */ jsx34(
22814
+ "h1",
22815
+ {
22816
+ style: {
22817
+ fontFamily: "var(--brand-font-heading)",
22818
+ fontSize: "clamp(2rem, 3.5vw, 2.75rem)",
22819
+ lineHeight: 1.1,
22820
+ letterSpacing: "-0.025em",
22821
+ color: "var(--brand-text)",
22822
+ marginBottom: "1rem"
22823
+ },
22824
+ ...titleKey ? { "data-ohw-editable": "text", "data-ohw-key": titleKey, "data-ohw-max-length": 80 } : {},
22825
+ children: title
22826
+ }
22827
+ ),
22828
+ /* @__PURE__ */ jsx34(
22829
+ "p",
22830
+ {
22831
+ style: {
22832
+ fontFamily: "var(--brand-font-body)",
22833
+ fontSize: "1rem",
22834
+ lineHeight: 1.7,
22835
+ fontWeight: 300,
22836
+ color: "var(--brand-text-muted)",
22837
+ maxWidth: "340px"
22838
+ },
22839
+ ...subtitleKey ? { "data-ohw-editable": "text", "data-ohw-key": subtitleKey, "data-ohw-max-length": 160 } : {},
22840
+ children: "This page doesn't have any content yet."
22841
+ }
22842
+ )
22843
+ ] });
22844
+ }
22091
22845
  export {
22092
22846
  AI_DEFAULT_BRAND,
22093
22847
  AI_TREE_SCHEMA_VERSIONS,
@@ -22104,6 +22858,7 @@ export {
22104
22858
  DropdownMenuItem,
22105
22859
  DropdownMenuSeparator,
22106
22860
  DropdownMenuTrigger,
22861
+ EmptySection,
22107
22862
  ItemActionToolbar,
22108
22863
  ItemInteractionLayer,
22109
22864
  LinkEditorPanel,