@ohhwells/bridge 0.1.91 → 0.1.92-next.269

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -69,6 +69,10 @@ function isRenderableTree(value) {
69
69
 
70
70
  // src/lib/ai-sections-store.ts
71
71
  var AI_SECTIONS_KEY = "__ohw_ai_sections";
72
+ var AI_SLOT_KEY_PREFIX = "ai.";
73
+ function aiSlotKeyPrefixFor(sectionId) {
74
+ return `${AI_SLOT_KEY_PREFIX}${sectionId}.`;
75
+ }
72
76
  var EMPTY_AI_SECTIONS = { v: 1, sections: [] };
73
77
  function parseAiSectionsState(raw) {
74
78
  if (!raw) return EMPTY_AI_SECTIONS;
@@ -112,6 +116,63 @@ function applyTreeToState(state, payload) {
112
116
  const others = state.sections.filter((existing) => existing.id !== entry.id);
113
117
  return { ...state, v: 1, sections: [...others, entry] };
114
118
  }
119
+ function foldAlignIntoTrees(state, store) {
120
+ const byId = new Map(state.sections.map((entry) => [entry.id, entry]));
121
+ const nextTrees = /* @__PURE__ */ new Map();
122
+ const treeFor = (id) => {
123
+ const cloned = nextTrees.get(id);
124
+ if (cloned) return cloned;
125
+ const entry = byId.get(id);
126
+ if (!entry) return void 0;
127
+ const fresh = {
128
+ ...entry.tree,
129
+ rows: entry.tree.rows.map((row) => ({ ...row, blocks: row.blocks.map((block) => ({ ...block })) }))
130
+ };
131
+ nextTrees.set(id, fresh);
132
+ return fresh;
133
+ };
134
+ const nodes = {};
135
+ for (const [key, override] of Object.entries(store.nodes)) {
136
+ const match = override.align !== void 0 && key.startsWith(AI_SLOT_KEY_PREFIX) ? key.match(/^ai\.(.+?)\.r(\d+)\.b(\d+)(?:\.|$)/) : null;
137
+ const tree = match ? treeFor(match[1]) : void 0;
138
+ const block = match && tree ? tree.rows[Number(match[2])]?.blocks[Number(match[3])] : void 0;
139
+ if (!block) {
140
+ nodes[key] = override;
141
+ continue;
142
+ }
143
+ block.align = override.align;
144
+ const rest = { ...override };
145
+ delete rest.align;
146
+ if (Object.keys(rest).length > 0) nodes[key] = rest;
147
+ }
148
+ const sections = {};
149
+ for (const [sectionId, override] of Object.entries(store.sections)) {
150
+ const tree = override.align !== void 0 ? treeFor(sectionId) : void 0;
151
+ if (!tree) {
152
+ sections[sectionId] = override;
153
+ continue;
154
+ }
155
+ for (const row of tree.rows) {
156
+ for (const block of row.blocks) block.align = override.align;
157
+ }
158
+ const rest = { ...override };
159
+ delete rest.align;
160
+ if (Object.keys(rest).length > 0) sections[sectionId] = rest;
161
+ }
162
+ if (nextTrees.size === 0) return { state, store, changed: false };
163
+ return {
164
+ state: {
165
+ ...state,
166
+ v: 1,
167
+ sections: state.sections.map((entry) => {
168
+ const tree = nextTrees.get(entry.id);
169
+ return tree ? { ...entry, tree } : entry;
170
+ })
171
+ },
172
+ store: { v: 1, sections, nodes },
173
+ changed: true
174
+ };
175
+ }
115
176
  function removeFromState(state, id) {
116
177
  return { ...state, v: 1, sections: state.sections.filter((entry) => entry.id !== id) };
117
178
  }
@@ -123,6 +184,33 @@ function deleteSectionFromState(state, sectionId) {
123
184
  if (removed.includes(sectionId)) return state;
124
185
  return { ...state, v: 1, sections: state.sections, removed: [...removed, sectionId] };
125
186
  }
187
+ function reapRemovedAiSections(state, store, removedIds, excludeIds = /* @__PURE__ */ new Set()) {
188
+ const generated = new Set(state.sections.map((entry) => entry.id));
189
+ const reapedIds = [...new Set(removedIds)].filter((id) => generated.has(id) && !excludeIds.has(id));
190
+ if (reapedIds.length === 0) {
191
+ return { state, store, reapedIds: [], slotPrefixes: [], changed: false };
192
+ }
193
+ const reaped = new Set(reapedIds);
194
+ const slotPrefixes = reapedIds.map(aiSlotKeyPrefixFor);
195
+ const nextState = {
196
+ ...state,
197
+ v: 1,
198
+ sections: state.sections.filter((entry) => !reaped.has(entry.id))
199
+ };
200
+ let nextStore = store;
201
+ if (store) {
202
+ const sections = {};
203
+ for (const [key, override] of Object.entries(store.sections)) {
204
+ if (!reaped.has(key)) sections[key] = override;
205
+ }
206
+ const nodes = {};
207
+ for (const [key, override] of Object.entries(store.nodes)) {
208
+ if (!slotPrefixes.some((prefix) => key.startsWith(prefix))) nodes[key] = override;
209
+ }
210
+ nextStore = { v: 1, sections, nodes };
211
+ }
212
+ return { state: nextState, store: nextStore, reapedIds, slotPrefixes, changed: true };
213
+ }
126
214
 
127
215
  // src/lib/brand-chrome.ts
128
216
  var BRAND_NAME_KEY = "__ohw_brand_name";
@@ -330,6 +418,13 @@ function styleSheetCss() {
330
418
  `[data-ohw-style-spacing="${spacing}"] { padding-top: ${px}px !important; padding-bottom: ${px}px !important; }`
331
419
  );
332
420
  }
421
+ rules.push(
422
+ `[data-ohw-style-corners="sharp"] :is(.card, [data-ohw-card], img, picture, figure, [data-ohw-editable="bg-image"]) { border-radius: 0 !important; }`,
423
+ `[data-ohw-style-corners="sharp"] :has(> img) { border-radius: 0 !important; }`
424
+ );
425
+ for (const align of ["left", "center", "right"]) {
426
+ rules.push(`[data-ohw-style-align="${align}"] { text-align: ${align} !important; }`);
427
+ }
333
428
  return rules.join("\n");
334
429
  }
335
430
  var STYLE_FONT_LINK_ID = "ohw-style-fonts";
@@ -356,10 +451,25 @@ var SECTION_ATTRS = {
356
451
  textDistribution: "data-ohw-style-distribution",
357
452
  headlineScale: "data-ohw-style-headline",
358
453
  imageAspect: "data-ohw-style-aspect",
359
- spacing: "data-ohw-style-spacing"
454
+ spacing: "data-ohw-style-spacing",
455
+ cornerStyle: "data-ohw-style-corners",
456
+ align: "data-ohw-style-align"
360
457
  };
361
458
  var NODE_WROTE_ATTR = "data-ohw-style-node";
362
- var NODE_PROPS = ["color", "font-family", "font-size", "background"];
459
+ var NODE_PROPS = [
460
+ "color",
461
+ "font-family",
462
+ "font-size",
463
+ "background",
464
+ "text-align",
465
+ "justify-content",
466
+ "align-items"
467
+ ];
468
+ var ALIGN_JUSTIFY = {
469
+ left: "flex-start",
470
+ center: "center",
471
+ right: "flex-end"
472
+ };
363
473
  function saveInline(el, prop) {
364
474
  const attr = `data-ohw-style-prev-${prop}`;
365
475
  if (el.hasAttribute(attr)) return;
@@ -403,6 +513,10 @@ function clearNodeProps(root) {
403
513
  function buttonSurfaceOf(el) {
404
514
  return el.closest("a, button") ?? el;
405
515
  }
516
+ function alignSubjectOf(el) {
517
+ const button = el.closest('[data-ohw-role="button"]');
518
+ return button?.parentElement ?? el;
519
+ }
406
520
  function applyStylesToDom(store) {
407
521
  ensureStyleSheet();
408
522
  clearSectionAttrs(document);
@@ -448,6 +562,18 @@ function applyStylesToDom(store) {
448
562
  el.style.setProperty("font-size", `${override.fontSize}px`, "important");
449
563
  el.setAttribute(NODE_WROTE_ATTR, "");
450
564
  }
565
+ if (override.align !== void 0) {
566
+ const subject = alignSubjectOf(el);
567
+ saveInline(subject, "text-align");
568
+ saveInline(subject, "justify-content");
569
+ subject.style.setProperty("text-align", override.align, "important");
570
+ subject.style.setProperty(
571
+ "justify-content",
572
+ ALIGN_JUSTIFY[override.align] ?? "flex-start",
573
+ "important"
574
+ );
575
+ subject.setAttribute(NODE_WROTE_ATTR, "");
576
+ }
451
577
  if (override.buttonBackground !== void 0 || override.buttonText !== void 0) {
452
578
  const surface = buttonSurfaceOf(el);
453
579
  if (override.buttonBackground !== void 0) {
@@ -468,9 +594,535 @@ function applyStylesToDom(store) {
468
594
  import { flushSync } from "react-dom";
469
595
  import { createRoot } from "react-dom/client";
470
596
 
597
+ // src/lib/sections.ts
598
+ var LINK_PICKER_EXCLUDED_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
599
+ function isChromeSection(el) {
600
+ return el.matches("header, nav, footer, aside");
601
+ }
602
+ function titleCaseSectionId(id) {
603
+ return id.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
604
+ }
605
+ function parseSectionsFromRoot(root) {
606
+ const seen = /* @__PURE__ */ new Set();
607
+ const sections = [];
608
+ for (const el of root.querySelectorAll("[data-ohw-section]")) {
609
+ const id = el.getAttribute("data-ohw-section") ?? "";
610
+ if (!id || seen.has(id) || LINK_PICKER_EXCLUDED_IDS.has(id)) continue;
611
+ if (el.parentElement?.closest("[data-ohw-section]")) continue;
612
+ if (el.hasAttribute("data-ohw-ai-template-hidden") || el.hasAttribute("data-ohw-ai-removed") || el.hasAttribute("data-ohw-ai-replaced-by"))
613
+ continue;
614
+ seen.add(id);
615
+ const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
616
+ sections.push({ id, label });
617
+ }
618
+ return sections;
619
+ }
620
+ function collectSectionsFromDom() {
621
+ if (typeof document === "undefined") return [];
622
+ return parseSectionsFromRoot(document);
623
+ }
624
+ function parseSectionsFromHtml(html) {
625
+ const doc = new DOMParser().parseFromString(html, "text/html");
626
+ return parseSectionsFromRoot(doc);
627
+ }
628
+
629
+ // src/lib/section-instances.ts
630
+ var SECTION_ORDER_KEY = "__ohw_section_order";
631
+ var REMOVED_ATTR = "data-ohw-section-removed";
632
+ function isRemovedSection(el) {
633
+ return el.hasAttribute(REMOVED_ATTR);
634
+ }
635
+ function movableUnit(el) {
636
+ return el.closest("[data-ohw-section-container]") ?? el;
637
+ }
638
+ function sectionTypeOf(el) {
639
+ return el.getAttribute("data-ohw-section") ?? el.querySelector("[data-ohw-section]")?.getAttribute("data-ohw-section") ?? "";
640
+ }
641
+ function sectionElementOf(el) {
642
+ return el.hasAttribute("data-ohw-section") ? el : el.querySelector("[data-ohw-section]") ?? el;
643
+ }
644
+ function collectTopLevelUnits(predicate) {
645
+ const seen = /* @__PURE__ */ new Set();
646
+ const result = [];
647
+ document.querySelectorAll("[data-ohw-section]").forEach((el) => {
648
+ if (!predicate(el)) return;
649
+ const unit = movableUnit(el);
650
+ if (unit.parentElement?.closest("[data-ohw-section],[data-ohw-section-container]")) return;
651
+ if (seen.has(unit)) return;
652
+ seen.add(unit);
653
+ result.push(unit);
654
+ });
655
+ return result;
656
+ }
657
+ function topLevelSections() {
658
+ return collectTopLevelUnits((el) => !isChromeSection(el) && !isRemovedSection(movableUnit(el)));
659
+ }
660
+ function instanceIdOf(el) {
661
+ return el.dataset.ohwInstance ?? el.dataset.ohwSection ?? "";
662
+ }
663
+ function findByInstanceId(instanceId) {
664
+ const escapedId = CSS.escape(instanceId);
665
+ const direct = document.querySelector(`[data-ohw-instance="${escapedId}"]`);
666
+ if (direct) return movableUnit(direct);
667
+ const bare = document.querySelector(`[data-ohw-section="${escapedId}"]:not([data-ohw-instance])`);
668
+ return bare ? movableUnit(bare) : null;
669
+ }
670
+ function planSectionMove(instanceId, targetIndex, currentPath) {
671
+ const sections = topLevelSections();
672
+ const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
673
+ if (index === -1) return null;
674
+ const dragged = sections[index];
675
+ const others = sections.filter((_, i) => i !== index);
676
+ const clamped = Math.max(0, Math.min(targetIndex, others.length));
677
+ const reordered = [...others.slice(0, clamped), dragged, ...others.slice(clamped)];
678
+ return reordered.map((el, order) => ({
679
+ instanceId: instanceIdOf(el),
680
+ type: sectionTypeOf(el),
681
+ order,
682
+ pagePath: currentPath
683
+ }));
684
+ }
685
+ function moveSectionInstance(instanceId, direction, currentPath) {
686
+ const sections = topLevelSections();
687
+ const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
688
+ if (index === -1) return null;
689
+ const siblingIndex = direction === "up" ? index - 1 : index + 1;
690
+ if (siblingIndex < 0 || siblingIndex >= sections.length) return null;
691
+ const entries = planSectionMove(instanceId, siblingIndex, currentPath);
692
+ if (!entries) return null;
693
+ applyPersistedOrder(entries);
694
+ return entries;
695
+ }
696
+ function syncRemovedFlags(entries) {
697
+ const removedIds = new Set(entries.filter((e) => e.removed).map((e) => e.instanceId));
698
+ document.querySelectorAll(`[${REMOVED_ATTR}]`).forEach((el) => {
699
+ if (!removedIds.has(instanceIdOf(el))) {
700
+ el.style.removeProperty("display");
701
+ el.removeAttribute(REMOVED_ATTR);
702
+ }
703
+ });
704
+ for (const id of removedIds) {
705
+ const el = findByInstanceId(id);
706
+ if (el) {
707
+ el.style.display = "none";
708
+ el.setAttribute(REMOVED_ATTR, "");
709
+ }
710
+ }
711
+ }
712
+ function applyPersistedOrder(entries) {
713
+ syncRemovedFlags(entries);
714
+ if (entries.length === 0) return;
715
+ const sections = topLevelSections();
716
+ if (sections.length === 0) return;
717
+ const orderIndex = new Map(entries.map((e) => [e.instanceId, e.order]));
718
+ const ordered = [...sections].sort((a, b) => {
719
+ const aOrder = orderIndex.get(instanceIdOf(a));
720
+ const bOrder = orderIndex.get(instanceIdOf(b));
721
+ if (aOrder === void 0 && bOrder === void 0) return 0;
722
+ if (aOrder === void 0) return 1;
723
+ if (bOrder === void 0) return -1;
724
+ return aOrder - bOrder;
725
+ });
726
+ let prev = null;
727
+ for (const el of ordered) {
728
+ if (prev) prev.after(el);
729
+ prev = el;
730
+ }
731
+ }
732
+ function setSectionRemoved(instanceId, currentPath, existingEntries, removed) {
733
+ if (!findByInstanceId(instanceId)) return null;
734
+ const byId = new Map(existingEntries.map((e) => [e.instanceId, e]));
735
+ const allSections = collectTopLevelUnits((el) => !isChromeSection(el));
736
+ allSections.forEach((el, order) => {
737
+ const id = instanceIdOf(el);
738
+ if (!byId.has(id)) {
739
+ byId.set(id, { instanceId: id, type: sectionTypeOf(el), order, pagePath: currentPath });
740
+ }
741
+ });
742
+ const target = byId.get(instanceId);
743
+ if (!target) return null;
744
+ byId.set(instanceId, { ...target, removed });
745
+ const entries = Array.from(byId.values());
746
+ applyPersistedOrder(entries);
747
+ return entries;
748
+ }
749
+ function deleteSectionInstance(instanceId, currentPath, existingEntries) {
750
+ return setSectionRemoved(instanceId, currentPath, existingEntries, true);
751
+ }
752
+ function restoreSectionInstance(instanceId, currentPath, existingEntries) {
753
+ return setSectionRemoved(instanceId, currentPath, existingEntries, false);
754
+ }
755
+ function duplicateSectionInstance(instanceId, newId, currentPath, existingEntries) {
756
+ const original = findByInstanceId(instanceId);
757
+ if (!original) return null;
758
+ const clone = original.cloneNode(true);
759
+ clone.setAttribute("data-ohw-instance", newId);
760
+ const keyRekeys = rekeySectionSubtree(clone, newId);
761
+ original.insertAdjacentElement("afterend", clone);
762
+ const byId = new Map(existingEntries.map((e) => [e.instanceId, e]));
763
+ const entries = topLevelSections().map((el, order) => {
764
+ const id = instanceIdOf(el);
765
+ return {
766
+ instanceId: id,
767
+ type: sectionTypeOf(el),
768
+ order,
769
+ pagePath: currentPath,
770
+ ...byId.get(id)?.removed ? { removed: true } : {}
771
+ };
772
+ });
773
+ applyPersistedOrder(entries);
774
+ return { entries, keyRekeys };
775
+ }
776
+ function newInstanceId() {
777
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `instance-${Date.now()}-${Math.random().toString(36).slice(2)}`;
778
+ }
779
+ function getPageSectionOrderEntries(raw, currentPath) {
780
+ if (!raw) return [];
781
+ try {
782
+ const entries = JSON.parse(raw);
783
+ return entries.filter((e) => !e.pagePath || e.pagePath === currentPath);
784
+ } catch {
785
+ return [];
786
+ }
787
+ }
788
+ function mergePageSectionOrder(raw, currentPath, pageEntries) {
789
+ let all = [];
790
+ if (raw) {
791
+ try {
792
+ const parsed = JSON.parse(raw);
793
+ if (Array.isArray(parsed)) all = parsed;
794
+ } catch {
795
+ }
796
+ }
797
+ const otherPages = all.filter((e) => e && e.pagePath && e.pagePath !== currentPath);
798
+ const pageIds = new Set(pageEntries.map((e) => e.instanceId));
799
+ const removedHere = all.filter(
800
+ (e) => e && (!e.pagePath || e.pagePath === currentPath) && e.removed && !pageIds.has(e.instanceId)
801
+ );
802
+ return [...otherPages, ...removedHere, ...pageEntries];
803
+ }
804
+ function rekeySectionSubtree(root, instanceId) {
805
+ const suffix = `::${instanceId}`;
806
+ const pairs = [];
807
+ const rekey = (el, attr) => {
808
+ const current = el.getAttribute(attr);
809
+ if (!current) return;
810
+ const base = current.includes("::") ? current.slice(0, current.indexOf("::")) : current;
811
+ const next = `${base}${suffix}`;
812
+ el.setAttribute(attr, next);
813
+ pairs.push({ from: current, to: next });
814
+ };
815
+ if (root.hasAttribute("data-ohw-key")) rekey(root, "data-ohw-key");
816
+ if (root.hasAttribute("data-ohw-href-key")) rekey(root, "data-ohw-href-key");
817
+ root.querySelectorAll("[data-ohw-key]").forEach((el) => rekey(el, "data-ohw-key"));
818
+ root.querySelectorAll("[data-ohw-href-key]").forEach((el) => rekey(el, "data-ohw-href-key"));
819
+ return pairs;
820
+ }
821
+ function initSectionInstancesFromContent(content, currentPath) {
822
+ document.querySelectorAll("[data-ohw-section]:not([data-ohw-instance])").forEach((el) => {
823
+ el.setAttribute("data-ohw-instance", el.getAttribute("data-ohw-section") ?? "");
824
+ });
825
+ document.querySelectorAll("[data-ohw-section-container]:not([data-ohw-instance])").forEach((el) => {
826
+ const type = sectionTypeOf(el);
827
+ if (type) el.setAttribute("data-ohw-instance", type);
828
+ });
829
+ const entries = getPageSectionOrderEntries(content[SECTION_ORDER_KEY], currentPath);
830
+ for (const entry of entries) {
831
+ if (entry.instanceId === entry.type) continue;
832
+ if (document.querySelector(`[data-ohw-instance="${CSS.escape(entry.instanceId)}"]`)) continue;
833
+ const original = document.querySelector(
834
+ `[data-ohw-section="${CSS.escape(entry.type)}"][data-ohw-instance="${CSS.escape(entry.type)}"]`
835
+ );
836
+ if (!original) continue;
837
+ const clone = original.cloneNode(true);
838
+ clone.setAttribute("data-ohw-instance", entry.instanceId);
839
+ rekeySectionSubtree(clone, entry.instanceId);
840
+ original.insertAdjacentElement("afterend", clone);
841
+ }
842
+ applyPersistedOrder(entries);
843
+ }
844
+
471
845
  // src/ui/ai-tree/AiTreeRenderer.tsx
472
846
  import React from "react";
473
847
  import { ArrowLeft, ArrowRight, ChevronDown, icons as lucideIcons } from "lucide-react";
848
+
849
+ // src/lib/placeholder-imagery.ts
850
+ var U = (id) => `https://images.unsplash.com/photo-${id}?auto=format&fit=crop&w=1600&q=80`;
851
+ var GENERIC = [
852
+ U("1441986300917-64674bd600d8"),
853
+ U("1486406146926-c627a92ad1ab"),
854
+ U("1497032628192-86f99bcd76bc"),
855
+ U("1521737604893-d14cc237f11d"),
856
+ U("1522071820081-009f0129c71c"),
857
+ U("1519389950473-47ba0277781c"),
858
+ U("1460925895917-afdab827c52f"),
859
+ U("1504384308090-c894fdcc538d")
860
+ ];
861
+ var PEOPLE = [
862
+ U("1500648767791-00dcc994a43e"),
863
+ U("1494790108377-be9c29b29330"),
864
+ U("1507003211169-0a1dd7228f2d"),
865
+ U("1438761681033-6461ffad8d80"),
866
+ U("1544005313-94ddf0286df2"),
867
+ U("1472099645785-5658abf4ff4e"),
868
+ U("1519085360753-af0119f7cbe7"),
869
+ U("1534528741775-53994a69daeb")
870
+ ];
871
+ var THEMED = [
872
+ {
873
+ keywords: ["portrait", "headshot", "person", "people", "team", "staff", "avatar", "founder", "face"],
874
+ pool: PEOPLE
875
+ },
876
+ {
877
+ keywords: ["pet", "dog", "cat", "puppy", "kitten", "vet", "animal"],
878
+ pool: [
879
+ U("1548199973-03cce0bbc87b"),
880
+ U("1450778869180-41d0601e046e"),
881
+ U("1583511655857-d19b40a7a54e"),
882
+ U("1587300003388-59208cc962cb"),
883
+ U("1517849845537-4d257902454a"),
884
+ U("1601758228041-f3b2795255f1")
885
+ ]
886
+ },
887
+ {
888
+ keywords: [
889
+ "baker",
890
+ "bakery",
891
+ "cafe",
892
+ "coffee",
893
+ "latte",
894
+ "restaurant",
895
+ "pastr",
896
+ "bread",
897
+ "cake",
898
+ "cater",
899
+ "chef",
900
+ "kitchen",
901
+ "food",
902
+ "pizza",
903
+ "dessert",
904
+ "brunch",
905
+ "bistro",
906
+ "deli",
907
+ "dish",
908
+ "menu"
909
+ ],
910
+ pool: [
911
+ U("1509440159596-0249088772ff"),
912
+ U("1555507036-ab1f4038808a"),
913
+ U("1517433670267-08bbd4be890f"),
914
+ U("1486427944299-d1955d23e34d"),
915
+ U("1504754524776-8f4f37790ca0"),
916
+ U("1495474472287-4d71bcdd2085"),
917
+ U("1521017432531-fbd92d768814"),
918
+ U("1556909114-f6e7ad7d3136")
919
+ ]
920
+ },
921
+ {
922
+ keywords: [
923
+ "shop",
924
+ "store",
925
+ "boutique",
926
+ "retail",
927
+ "clothing",
928
+ "fashion",
929
+ "jewel",
930
+ "gift",
931
+ "florist",
932
+ "market",
933
+ "grocer",
934
+ "product",
935
+ "storefront"
936
+ ],
937
+ pool: [
938
+ U("1441984904996-e0b6ba687e04"),
939
+ U("1472851294608-062f824d29cc"),
940
+ U("1523381210434-271e8be1f52b"),
941
+ U("1534452203293-494d7ddbf7e0"),
942
+ U("1445205170230-053b83016050"),
943
+ U("1560243563-062bfc001d68")
944
+ ]
945
+ },
946
+ {
947
+ keywords: [
948
+ "yoga",
949
+ "pilates",
950
+ "fitness",
951
+ "gym",
952
+ "workout",
953
+ "trainer",
954
+ "wellness",
955
+ "meditat",
956
+ "massage",
957
+ "therap",
958
+ "physio",
959
+ "chiro",
960
+ "nutrition",
961
+ "spa",
962
+ "studio"
963
+ ],
964
+ pool: [
965
+ U("1544367567-0f2fcb009e0b"),
966
+ U("1506126613408-eca07ce68773"),
967
+ U("1545205597-3d9d02c29597"),
968
+ U("1552196563-55cd4e45efb3"),
969
+ U("1518611012118-696072aa579a"),
970
+ U("1571019613454-1cb2f99b2d8b"),
971
+ U("1540555700478-4be289fbecef"),
972
+ U("1519824145371-296894a0daa9")
973
+ ]
974
+ },
975
+ {
976
+ keywords: [
977
+ "salon",
978
+ "hairdress",
979
+ "haircut",
980
+ "barber",
981
+ "manicure",
982
+ "pedicure",
983
+ "nails",
984
+ "beauty",
985
+ "makeup",
986
+ "cosmetic",
987
+ "eyelash",
988
+ "eyebrow",
989
+ "skincare",
990
+ "esthetic",
991
+ "waxing",
992
+ "hair"
993
+ ],
994
+ pool: [
995
+ U("1560066984-138dadb4c035"),
996
+ U("1522337660859-02fbefca4702"),
997
+ U("1562322140-8baeececf3df"),
998
+ U("1521590832167-7bcbfaa6381f"),
999
+ U("1487412947147-5cebf100ffc2"),
1000
+ U("1526045478516-99145907023c")
1001
+ ]
1002
+ },
1003
+ {
1004
+ keywords: [
1005
+ "cleaning",
1006
+ "plumb",
1007
+ "electric",
1008
+ "landscap",
1009
+ "contractor",
1010
+ "handyman",
1011
+ "renov",
1012
+ "hvac",
1013
+ "roofing",
1014
+ "painting",
1015
+ "carpentry",
1016
+ "flooring",
1017
+ "movers",
1018
+ "construction",
1019
+ "tools"
1020
+ ],
1021
+ pool: [
1022
+ U("1581578731548-c64695cc6952"),
1023
+ U("1504307651254-35680f356dfd"),
1024
+ U("1581092160562-40aa08e78837"),
1025
+ U("1621905251189-08b45d6a269e"),
1026
+ U("1558618666-fcd25c85cd64"),
1027
+ U("1585128792020-803d29415281")
1028
+ ]
1029
+ },
1030
+ {
1031
+ keywords: [
1032
+ "legal",
1033
+ "attorney",
1034
+ "lawyer",
1035
+ "account",
1036
+ "bookkeep",
1037
+ "consult",
1038
+ "coaching",
1039
+ "financ",
1040
+ "insurance",
1041
+ "realtor",
1042
+ "estate",
1043
+ "marketing",
1044
+ "agency",
1045
+ "office",
1046
+ "business",
1047
+ "desk"
1048
+ ],
1049
+ pool: [
1050
+ U("1497366216548-37526070297c"),
1051
+ U("1497366811353-6870744d04b2"),
1052
+ U("1454165804606-c3d57bc86b40"),
1053
+ U("1521791136064-7986c2920216"),
1054
+ U("1556761175-b413da4baf72"),
1055
+ U("1542744173-8e7e53415bb0")
1056
+ ]
1057
+ },
1058
+ {
1059
+ keywords: [
1060
+ "wedding",
1061
+ "event",
1062
+ "party",
1063
+ "celebrat",
1064
+ "venue",
1065
+ "community",
1066
+ "nonprofit",
1067
+ "charity",
1068
+ "workshop",
1069
+ "photograph",
1070
+ "concert"
1071
+ ],
1072
+ pool: [
1073
+ U("1511578314322-379afb476865"),
1074
+ U("1501281668745-f7f57925c3b4"),
1075
+ U("1523580494863-6f3031224c94"),
1076
+ U("1540575467063-178a50c2df87"),
1077
+ U("1505236858219-8359eb29e329"),
1078
+ U("1528605248644-14dd04022da1")
1079
+ ]
1080
+ }
1081
+ ];
1082
+ function poolForSubject(subject) {
1083
+ for (const theme of THEMED) {
1084
+ if (theme.keywords.some((k) => subject.includes(k))) {
1085
+ return theme.pool;
1086
+ }
1087
+ }
1088
+ return GENERIC;
1089
+ }
1090
+ function mixedHash(text) {
1091
+ let hash = 2166136261;
1092
+ for (let i = 0; i < text.length; i++) {
1093
+ hash ^= text.charCodeAt(i);
1094
+ hash = Math.imul(hash, 16777619);
1095
+ }
1096
+ return hash >>> 16 & 65535;
1097
+ }
1098
+ function resolvePlaceholderRef(ref) {
1099
+ const match = /^placeholder:([a-z0-9-]+)$/.exec(ref);
1100
+ if (!match) return null;
1101
+ const subject = match[1];
1102
+ const pool = poolForSubject(subject.replace(/-\d+$/, ""));
1103
+ return pool[mixedHash(ref) % pool.length];
1104
+ }
1105
+ function collectPlaceholderRefs(tree) {
1106
+ const seen = /* @__PURE__ */ new Set();
1107
+ for (const match of JSON.stringify(tree ?? null).matchAll(/"(placeholder:[a-z0-9-]+)"/gu)) {
1108
+ seen.add(match[1]);
1109
+ }
1110
+ return [...seen];
1111
+ }
1112
+ function buildPlaceholderMap(tree) {
1113
+ const map = {};
1114
+ const cursor = /* @__PURE__ */ new Map();
1115
+ for (const ref of collectPlaceholderRefs(tree)) {
1116
+ const subject = ref.slice("placeholder:".length).replace(/-\d+$/, "");
1117
+ const pool = poolForSubject(subject);
1118
+ const start = cursor.get(pool) ?? mixedHash(ref) % pool.length;
1119
+ map[ref] = pool[start % pool.length];
1120
+ cursor.set(pool, start + 1);
1121
+ }
1122
+ return map;
1123
+ }
1124
+
1125
+ // src/ui/ai-tree/AiTreeRenderer.tsx
474
1126
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
475
1127
  function lucideByName(name) {
476
1128
  const pascal = name.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
@@ -484,6 +1136,7 @@ var typeStyle = (spec, font) => ({
484
1136
  fontWeight: spec.weight
485
1137
  });
486
1138
  var str = (value) => typeof value === "string" ? value : "";
1139
+ var cardRadius = (slots) => slots.cornerStyle === "sharp" ? 0 : AI_TREE_TOKENS.radiusCard;
487
1140
  var featureLines = (value) => value.split(/\n|<br\s*\/?>/i).map((line) => line.trim()).filter(Boolean);
488
1141
  var CHECK_MASK = `data:image/svg+xml,${encodeURIComponent(
489
1142
  '<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 +1163,25 @@ var FEATURE_LINE_CSS = [
510
1163
  `background-color:currentColor;-webkit-mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px;`,
511
1164
  `mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px}`
512
1165
  ].join("");
1166
+ function buttonShellStyle(ctx, fullWidth) {
1167
+ const bs = ctx.buttonStyle;
1168
+ if (bs) {
1169
+ return {
1170
+ borderRadius: bs.radius,
1171
+ ...bs.padding ? { padding: bs.padding } : {},
1172
+ ...bs.fontFamily ? { fontFamily: bs.fontFamily } : { fontFamily: ctx.brand.fonts.body },
1173
+ ...bs.fontSize ? { fontSize: bs.fontSize } : {},
1174
+ ...bs.fontWeight ? { fontWeight: bs.fontWeight } : {},
1175
+ ...bs.letterSpacing && bs.letterSpacing !== "normal" ? { letterSpacing: bs.letterSpacing } : {},
1176
+ ...bs.textTransform && bs.textTransform !== "none" ? { textTransform: bs.textTransform } : {}
1177
+ };
1178
+ }
1179
+ return {
1180
+ borderRadius: AI_TREE_TOKENS.radiusButton,
1181
+ padding: fullWidth ? `${AI_TREE_TOKENS.spacing2}px ${AI_TREE_TOKENS.spacing4}px` : `${AI_TREE_TOKENS.spacing3}px ${AI_TREE_TOKENS.spacing6}px`,
1182
+ ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
1183
+ };
1184
+ }
513
1185
  function hexLuminance(color) {
514
1186
  const m = /^#([0-9a-f]{6})$/i.exec(color.trim());
515
1187
  if (!m) return null;
@@ -526,6 +1198,12 @@ function hexContrast(a, b) {
526
1198
  const [hi, lo] = la > lb ? [la, lb] : [lb, la];
527
1199
  return (hi + 0.05) / (lo + 0.05);
528
1200
  }
1201
+ function primaryButtonLabel(brand) {
1202
+ const darkC = hexContrast(brand.palette.primary, brand.palette.dark);
1203
+ const lightC = hexContrast(brand.palette.primary, AI_TREE_TOKENS.textPrimaryForeground);
1204
+ if (darkC === null || lightC === null) return AI_TREE_TOKENS.textPrimaryForeground;
1205
+ return darkC > lightC ? brand.palette.dark : AI_TREE_TOKENS.textPrimaryForeground;
1206
+ }
529
1207
  function accentBandContext(brand) {
530
1208
  const p = brand.palette;
531
1209
  const lightWins = (hexContrast(p.light, p.primary) ?? 99) >= (hexContrast(p.dark, p.primary) ?? 0);
@@ -543,6 +1221,22 @@ function accentBandContext(brand) {
543
1221
  function textAttrs(ctx, path) {
544
1222
  return ctx.keyFor ? { "data-ohw-key": ctx.keyFor(path), "data-ohw-editable": "text" } : {};
545
1223
  }
1224
+ var AI_RESPONSIVE_CSS = [
1225
+ "@media (max-width: 960px) {",
1226
+ " [data-ai-responsive] [data-ai-grid] { grid-template-columns: repeat(2, 1fr) !important; }",
1227
+ ' [data-ai-responsive] [data-ai-grid="1"] { grid-template-columns: 1fr !important; }',
1228
+ "}",
1229
+ "@media (max-width: 640px) {",
1230
+ " [data-ai-responsive] [data-ai-grid] { grid-template-columns: 1fr !important; }",
1231
+ " [data-ai-responsive] [data-ai-columns] > * { grid-column: 1 / -1 !important; }",
1232
+ // Group containers flatten to a column on phones; span placements come along for free.
1233
+ " [data-ai-responsive] [data-ai-group] { display: flex !important; flex-direction: column !important; }",
1234
+ " [data-ai-responsive] [data-ai-group] > * { grid-column: auto !important; }",
1235
+ " [data-ai-responsive] [data-ai-section-inner] { padding-left: 20px !important; padding-right: 20px !important; }",
1236
+ " [data-ai-responsive] { overflow-x: hidden; }",
1237
+ " [data-ai-responsive] img { max-width: 100%; }",
1238
+ "}"
1239
+ ].join("\n");
546
1240
  var TRANSPARENT_PX = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
547
1241
  function MediaBox({
548
1242
  refValue,
@@ -555,13 +1249,17 @@ function MediaBox({
555
1249
  const url = refValue ? ctx.resolveMedia(refValue) : null;
556
1250
  const isIcon = /^(lucide|simple):/.test(refValue);
557
1251
  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" } : {};
1252
+ const editAttrs = ctx.keyFor && editPath ? {
1253
+ "data-ohw-key": ctx.keyFor(editPath),
1254
+ "data-ohw-editable": isIcon ? "icon" : "image"
1255
+ } : {};
559
1256
  if (isIcon) {
560
1257
  const Icon = refValue.startsWith("lucide:") ? lucideByName(refValue.slice(7)) : null;
561
1258
  return /* @__PURE__ */ jsx(
562
1259
  "span",
563
1260
  {
564
1261
  "data-ai-icon": refValue,
1262
+ ...editAttrs,
565
1263
  style: {
566
1264
  display: "inline-flex",
567
1265
  width: 48,
@@ -625,12 +1323,10 @@ function ButtonEl({
625
1323
  width: fullWidth ? "100%" : void 0,
626
1324
  alignItems: "center",
627
1325
  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
1326
  textDecoration: "none",
631
1327
  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 }
1328
+ ...buttonShellStyle(ctx, fullWidth),
1329
+ ...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
1330
  },
635
1331
  children: /* @__PURE__ */ jsx("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
636
1332
  }
@@ -656,7 +1352,7 @@ function TextBlock({ slots, ctx, path }) {
656
1352
  }
657
1353
  function SectionHeaderBlock({ node, ctx, path }) {
658
1354
  const slots = node.slots ?? {};
659
- const align = slots.alignment === "center" ? "center" : "left";
1355
+ const align = node.align ?? (slots.alignment === "center" ? "center" : "left");
660
1356
  const children = node.children ?? [];
661
1357
  const buttonRowIdx = children.findIndex((c) => c.type === "button-row");
662
1358
  const buttonRow = buttonRowIdx >= 0 ? children[buttonRowIdx] : void 0;
@@ -700,7 +1396,7 @@ function SectionHeaderBlock({ node, ctx, path }) {
700
1396
  display: "flex",
701
1397
  gap: AI_TREE_TOKENS.spacing6,
702
1398
  marginTop: AI_TREE_TOKENS.spacing8,
703
- justifyContent: align === "center" ? "center" : "flex-start"
1399
+ justifyContent: align === "center" ? "center" : align === "right" ? "flex-end" : "flex-start"
704
1400
  },
705
1401
  children: (buttonRow.children ?? []).map((button, i) => /* @__PURE__ */ jsx(
706
1402
  ButtonEl,
@@ -806,10 +1502,11 @@ function PricingCard({ node, ctx, path }) {
806
1502
  return /* @__PURE__ */ jsxs(
807
1503
  "div",
808
1504
  {
1505
+ "data-ohw-card": "",
809
1506
  style: {
810
1507
  background: hasBg ? ctx.brand.palette.light : "transparent",
811
1508
  border: `1px solid ${dark}`,
812
- borderRadius: AI_TREE_TOKENS.radiusCard,
1509
+ borderRadius: cardRadius(slots),
813
1510
  padding: AI_TREE_TOKENS.paddingBlock,
814
1511
  display: "flex",
815
1512
  flexDirection: "column",
@@ -914,10 +1611,11 @@ function TestimonialCard({ node, ctx, path }) {
914
1611
  return /* @__PURE__ */ jsx(
915
1612
  "div",
916
1613
  {
1614
+ "data-ohw-card": "",
917
1615
  "data-ai-avatar-pos": avatarPos ?? void 0,
918
1616
  style: {
919
1617
  background: hasBg ? ctx.cardSurface : "transparent",
920
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1618
+ borderRadius: hasBg ? cardRadius(slots) : 0,
921
1619
  overflow: "hidden",
922
1620
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
923
1621
  minWidth: 0
@@ -952,10 +1650,11 @@ function TeamCard({ node, ctx, path }) {
952
1650
  return /* @__PURE__ */ jsxs(
953
1651
  "div",
954
1652
  {
1653
+ "data-ohw-card": "",
955
1654
  "data-ai-avatar-pos": avatarPos ?? void 0,
956
1655
  style: {
957
1656
  background: hasBg ? ctx.cardSurface : "transparent",
958
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1657
+ borderRadius: hasBg ? cardRadius(slots) : 0,
959
1658
  overflow: "hidden",
960
1659
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
961
1660
  minWidth: 0,
@@ -1033,7 +1732,7 @@ function CardBlock({ node, ctx, path }) {
1033
1732
  editPath: `${path}.media`
1034
1733
  }
1035
1734
  ) : null;
1036
- const centered = slots.alignment === "center";
1735
+ const centered = (node.align ?? slots.alignment) === "center";
1037
1736
  const content = /* @__PURE__ */ jsxs(
1038
1737
  "div",
1039
1738
  {
@@ -1126,9 +1825,10 @@ function CardBlock({ node, ctx, path }) {
1126
1825
  return /* @__PURE__ */ jsxs(
1127
1826
  "div",
1128
1827
  {
1828
+ "data-ohw-card": "",
1129
1829
  style: {
1130
1830
  background: hasBg ? ctx.cardSurface : "transparent",
1131
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1831
+ borderRadius: hasBg ? cardRadius(slots) : 0,
1132
1832
  overflow: "hidden",
1133
1833
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
1134
1834
  display: horizontal ? "flex" : "block",
@@ -1156,7 +1856,7 @@ function CardBlock({ node, ctx, path }) {
1156
1856
  ) : /* @__PURE__ */ jsx(
1157
1857
  "div",
1158
1858
  {
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" },
1859
+ 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
1860
  children: media
1161
1861
  }
1162
1862
  )),
@@ -1447,7 +2147,7 @@ function CollectionBlock({ node, ctx, path }) {
1447
2147
  return /* @__PURE__ */ jsx(
1448
2148
  "div",
1449
2149
  {
1450
- "data-ai-grid": "",
2150
+ "data-ai-grid": String(itemsPerRow),
1451
2151
  style: {
1452
2152
  display: "grid",
1453
2153
  gridTemplateColumns: `repeat(${itemsPerRow}, 1fr)`,
@@ -1567,6 +2267,32 @@ function renderNode(node, ctx, path) {
1567
2267
  if (child) {
1568
2268
  return renderNode(child, ctx, `${path}.c0`);
1569
2269
  }
2270
+ if (str(slots.provider) === "map" && str(slots.query)) {
2271
+ const query = str(slots.query);
2272
+ const mapAttrs = ctx.keyFor ? {
2273
+ "data-ohw-key": ctx.keyFor(`${path}.query`),
2274
+ "data-ohw-editable": "map",
2275
+ "data-ohw-map-query": query
2276
+ } : {};
2277
+ return /* @__PURE__ */ jsx(
2278
+ "iframe",
2279
+ {
2280
+ ...mapAttrs,
2281
+ "data-ai-embed": "map",
2282
+ title: str(slots.title) || "Map",
2283
+ src: `https://www.google.com/maps?q=${encodeURIComponent(query)}&output=embed`,
2284
+ loading: "lazy",
2285
+ referrerPolicy: "no-referrer-when-downgrade",
2286
+ style: {
2287
+ width: "100%",
2288
+ minHeight: 320,
2289
+ border: 0,
2290
+ borderRadius: AI_TREE_TOKENS.radiusCard,
2291
+ display: "block"
2292
+ }
2293
+ }
2294
+ );
2295
+ }
1570
2296
  return /* @__PURE__ */ jsx(
1571
2297
  "div",
1572
2298
  {
@@ -1670,15 +2396,12 @@ function renderNode(node, ctx, path) {
1670
2396
  alignSelf: submitAlign,
1671
2397
  border: "none",
1672
2398
  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,
2399
+ // Shape/padding/typography follow the host template's own buttons.
2400
+ ...buttonShellStyle(ctx),
1677
2401
  // Brand-styled: primary fill, brand-derived label colour (not a fixed token) so it
1678
2402
  // reads correctly on custom palettes.
1679
2403
  background: ctx.brand.palette.primary,
1680
- color: ctx.buttonLabel ?? ctx.brand.palette.light,
1681
- ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
2404
+ color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand)
1682
2405
  },
1683
2406
  children: /* @__PURE__ */ jsx("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
1684
2407
  },
@@ -1713,7 +2436,7 @@ function renderNode(node, ctx, path) {
1713
2436
  function AiTreeRenderer({
1714
2437
  tree,
1715
2438
  brand,
1716
- buttonRadius,
2439
+ buttonStyle,
1717
2440
  resolveMedia,
1718
2441
  editKeyPrefix
1719
2442
  }) {
@@ -1723,13 +2446,18 @@ function AiTreeRenderer({
1723
2446
  const resolvedBrand = brand ?? AI_DEFAULT_BRAND;
1724
2447
  const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
1725
2448
  const blockBrand = band?.brand ?? resolvedBrand;
2449
+ const placeholderMap = buildPlaceholderMap(tree);
1726
2450
  const ctx = {
1727
2451
  brand: blockBrand,
1728
- resolveMedia: resolveMedia ?? (() => null),
2452
+ // An owner/library ref resolves through the host resolver; a `placeholder:<subject>` ref the
2453
+ // host cannot resolve falls back to real stock photography (the per-section map first, then a
2454
+ // standalone resolve), so generated galleries, image rows, and overlay backgrounds arrive with
2455
+ // photos instead of grey boxes.
2456
+ resolveMedia: (ref) => resolveMedia?.(ref) ?? placeholderMap[ref] ?? resolvePlaceholderRef(ref),
1729
2457
  cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
1730
2458
  keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
1731
2459
  sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
1732
- buttonRadius,
2460
+ buttonStyle,
1733
2461
  ...band ? { buttonLabel: band.buttonLabel } : {}
1734
2462
  };
1735
2463
  const settings = tree.settings ?? {};
@@ -1752,11 +2480,25 @@ function AiTreeRenderer({
1752
2480
  }
1753
2481
  })();
1754
2482
  const distributed = !isOverlay && settings.textDistribution;
2483
+ const rowAlignItems = (rowAlign) => {
2484
+ if (rowAlign === "top") return "start";
2485
+ if (rowAlign === "bottom") return "end";
2486
+ if (rowAlign === "center" || rowAlign === "stretch") return rowAlign;
2487
+ if (distributed === "space-between") return "stretch";
2488
+ return (distributed ?? settings.verticalPosition) === "top" ? "start" : "center";
2489
+ };
2490
+ const cellAlignStyle = (blockAlign) => blockAlign ? {
2491
+ display: "flex",
2492
+ flexDirection: "column",
2493
+ alignItems: blockAlign === "left" ? "flex-start" : blockAlign === "right" ? "flex-end" : "center",
2494
+ textAlign: blockAlign
2495
+ } : {};
1755
2496
  return /* @__PURE__ */ jsxs(
1756
2497
  "section",
1757
2498
  {
1758
2499
  "data-ai-section": tree.tag ?? "",
1759
2500
  ...bgAttrs,
2501
+ "data-ai-responsive": "",
1760
2502
  style: {
1761
2503
  position: "relative",
1762
2504
  padding: `${pad}px 0`,
@@ -1767,12 +2509,13 @@ function AiTreeRenderer({
1767
2509
  color: settings.sectionBackground === "accent" ? blockBrand.palette.dark : void 0
1768
2510
  },
1769
2511
  children: [
1770
- isOverlay && backgroundUrl && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
2512
+ /* @__PURE__ */ jsx("style", { children: AI_RESPONSIVE_CSS }),
1771
2513
  /* @__PURE__ */ jsx("style", { children: AI_MOBILE_CSS }),
2514
+ isOverlay && backgroundUrl && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
1772
2515
  /* @__PURE__ */ jsx(
1773
2516
  "div",
1774
2517
  {
1775
- "data-ai-container": "",
2518
+ "data-ai-section-inner": "",
1776
2519
  style: {
1777
2520
  position: "relative",
1778
2521
  maxWidth: AI_TREE_TOKENS.sectionMaxWidth,
@@ -1783,12 +2526,12 @@ function AiTreeRenderer({
1783
2526
  children: tree.rows.map((row, r2) => /* @__PURE__ */ jsx(
1784
2527
  "div",
1785
2528
  {
1786
- "data-ai-row": "",
2529
+ "data-ai-columns": "",
1787
2530
  style: {
1788
2531
  display: "grid",
1789
2532
  gridTemplateColumns: "repeat(12, 1fr)",
1790
2533
  gap: AI_TREE_TOKENS.spacing6,
1791
- alignItems: distributed === "space-between" ? "stretch" : (distributed ?? settings.verticalPosition) === "top" ? "start" : "center",
2534
+ alignItems: rowAlignItems(row.align),
1792
2535
  marginTop: r2 > 0 ? AI_TREE_TOKENS.spacing8 : 0
1793
2536
  },
1794
2537
  children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ jsx(
@@ -1798,6 +2541,8 @@ function AiTreeRenderer({
1798
2541
  style: {
1799
2542
  gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`,
1800
2543
  minWidth: 0,
2544
+ // Horizontal placement of the block's content within its column.
2545
+ ...cellAlignStyle(block.align),
1801
2546
  // space-between: each column becomes a flex column whose content spreads over
1802
2547
  // the full row height instead of clumping at the top.
1803
2548
  ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
@@ -1820,9 +2565,13 @@ function AiTreeRenderer({
1820
2565
  import { jsx as jsx2 } from "react/jsx-runtime";
1821
2566
  var CONTAINER_ATTR = "data-ohw-ai-generated";
1822
2567
  var REPLACED_ATTR = "data-ohw-ai-replaced-by";
1823
- var REMOVED_ATTR = "data-ohw-ai-removed";
2568
+ var REMOVED_ATTR2 = "data-ohw-ai-removed";
1824
2569
  var TEMPLATE_HIDDEN_ATTR = "data-ohw-ai-template-hidden";
1825
2570
  var CHROME_SECTION_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
2571
+ function isChromeSection2(el) {
2572
+ if (CHROME_SECTION_IDS.has(el.getAttribute("data-ohw-section") ?? "")) return true;
2573
+ return el.tagName === "HEADER" || el.tagName === "FOOTER";
2574
+ }
1826
2575
  function readRootVar(name) {
1827
2576
  if (typeof document === "undefined") return "";
1828
2577
  return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
@@ -1844,13 +2593,13 @@ function deriveBrandOverride() {
1844
2593
  };
1845
2594
  }
1846
2595
  function deriveTemplateBrand() {
1847
- const dark = readRootVar("--color-dark");
1848
- const primary = readRootVar("--color-primary");
1849
- const light = readRootVar("--color-light");
2596
+ const primary = readRootVar("--brand-primary") || readRootVar("--color-primary");
2597
+ const dark = readRootVar("--brand-text") || readRootVar("--color-dark");
2598
+ const light = readRootVar("--brand-background") || readRootVar("--color-light");
1850
2599
  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");
2600
+ const accent = readRootVar("--brand-accent") || readRootVar("--color-accent");
2601
+ const heading = readRootVar("--brand-font-heading") || readRootVar("--font-heading") || readRootVar("--font-display");
2602
+ const body = readRootVar("--brand-font-body") || readRootVar("--font-body");
1854
2603
  return {
1855
2604
  palette: { dark, primary, accent: accent || dark, light },
1856
2605
  fonts: {
@@ -1859,12 +2608,35 @@ function deriveTemplateBrand() {
1859
2608
  }
1860
2609
  };
1861
2610
  }
1862
- function deriveTemplateButtonRadius() {
2611
+ function deriveTemplateButtonStyle() {
1863
2612
  if (typeof document === "undefined") return null;
1864
- const btn = document.querySelector('[data-ohw-role="button"]');
2613
+ const btn = Array.from(document.querySelectorAll('[data-ohw-role="button"]')).find(
2614
+ (el) => !el.closest(`[${CONTAINER_ATTR}]`)
2615
+ );
1865
2616
  if (!btn) return null;
1866
- const radius = getComputedStyle(btn).borderTopLeftRadius;
1867
- return radius || null;
2617
+ const cs = getComputedStyle(btn);
2618
+ const corners = [
2619
+ cs.borderTopLeftRadius,
2620
+ cs.borderTopRightRadius,
2621
+ cs.borderBottomRightRadius,
2622
+ cs.borderBottomLeftRadius
2623
+ ].map((v) => v || "0px");
2624
+ const radius = corners.every((v) => v === corners[0]) ? corners[0] : corners.join(" ");
2625
+ const px = (v) => parseFloat(v) || 0;
2626
+ const lineHeight = px(cs.lineHeight) || px(cs.fontSize) * 1.2;
2627
+ const contentH = btn.getBoundingClientRect().height - px(cs.borderTopWidth) - px(cs.borderBottomWidth);
2628
+ const impliedY = Math.round(Math.max(0, (contentH - lineHeight) / 2));
2629
+ const padY = Math.max(px(cs.paddingTop), px(cs.paddingBottom), impliedY);
2630
+ const padX = Math.max(px(cs.paddingLeft), px(cs.paddingRight));
2631
+ return {
2632
+ radius: radius || "10px",
2633
+ padding: `${padY}px ${padX}px`,
2634
+ fontFamily: cs.fontFamily || "",
2635
+ fontSize: cs.fontSize || "",
2636
+ fontWeight: cs.fontWeight || "",
2637
+ letterSpacing: cs.letterSpacing || "",
2638
+ textTransform: cs.textTransform || ""
2639
+ };
1868
2640
  }
1869
2641
  var mounted = /* @__PURE__ */ new Map();
1870
2642
  function findTemplateSection(id) {
@@ -1880,6 +2652,19 @@ function findPlacementAnchor(id, exclude) {
1880
2652
  for (const el of document.querySelectorAll(`[data-ohw-section="${CSS.escape(id)}"]`)) {
1881
2653
  if (el === exclude) continue;
1882
2654
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
2655
+ if (isChromeSection2(el)) return null;
2656
+ return el;
2657
+ }
2658
+ return null;
2659
+ }
2660
+ function findFooterSection() {
2661
+ const byId = findTemplateSection("footer");
2662
+ if (byId) return byId;
2663
+ for (const el of Array.from(
2664
+ document.querySelectorAll("footer[data-ohw-section]")
2665
+ ).reverse()) {
2666
+ if (el.hasAttribute(CONTAINER_ATTR)) continue;
2667
+ if (el.parentElement?.closest("[data-ohw-section]")) continue;
1883
2668
  return el;
1884
2669
  }
1885
2670
  return null;
@@ -1906,7 +2691,7 @@ function placeContainer(container, entry) {
1906
2691
  return;
1907
2692
  }
1908
2693
  }
1909
- const footer = findTemplateSection("footer");
2694
+ const footer = findFooterSection();
1910
2695
  if (footer) {
1911
2696
  footer.insertAdjacentElement("beforebegin", container);
1912
2697
  } else {
@@ -1915,18 +2700,18 @@ function placeContainer(container, entry) {
1915
2700
  }
1916
2701
  function syncRemovedSections(state) {
1917
2702
  const removed = new Set(state.removed ?? []);
1918
- for (const el of document.querySelectorAll(`[${REMOVED_ATTR}]`)) {
2703
+ for (const el of document.querySelectorAll(`[${REMOVED_ATTR2}]`)) {
1919
2704
  const id = el.getAttribute("data-ohw-section") ?? "";
1920
2705
  if (!removed.has(id)) {
1921
2706
  el.style.removeProperty("display");
1922
- el.removeAttribute(REMOVED_ATTR);
2707
+ el.removeAttribute(REMOVED_ATTR2);
1923
2708
  }
1924
2709
  }
1925
2710
  for (const id of removed) {
1926
2711
  const section = findTemplateSection(id);
1927
2712
  if (section && !section.hasAttribute(REPLACED_ATTR)) {
1928
2713
  section.style.display = "none";
1929
- section.setAttribute(REMOVED_ATTR, "");
2714
+ section.setAttribute(REMOVED_ATTR2, "");
1930
2715
  }
1931
2716
  }
1932
2717
  }
@@ -1941,9 +2726,9 @@ function syncTemplateHidden(state, pageHasSections) {
1941
2726
  if (!hide) return;
1942
2727
  for (const el of Array.from(document.querySelectorAll("[data-ohw-section]"))) {
1943
2728
  if (el.hasAttribute(CONTAINER_ATTR)) continue;
1944
- if (CHROME_SECTION_IDS.has(el.getAttribute("data-ohw-section") ?? "")) continue;
2729
+ if (isChromeSection2(el)) continue;
1945
2730
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
1946
- if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR)) continue;
2731
+ if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR2)) continue;
1947
2732
  el.style.display = "none";
1948
2733
  el.setAttribute(TEMPLATE_HIDDEN_ATTR, "");
1949
2734
  }
@@ -1967,18 +2752,23 @@ function syncReplacedOriginals(state) {
1967
2752
  }
1968
2753
  }
1969
2754
  var sectionOrderIndex = /* @__PURE__ */ new Map();
2755
+ var removedSectionIds = /* @__PURE__ */ new Set();
1970
2756
  function setAiSectionOrder(raw, currentPath) {
1971
2757
  const next = /* @__PURE__ */ new Map();
2758
+ const removed = /* @__PURE__ */ new Set();
1972
2759
  if (raw) {
1973
2760
  try {
1974
2761
  const entries = JSON.parse(raw);
1975
2762
  for (const entry of entries) {
1976
- if (!entry.pagePath || entry.pagePath === currentPath) next.set(entry.instanceId, entry.order);
2763
+ if (entry.pagePath && entry.pagePath !== currentPath) continue;
2764
+ next.set(entry.instanceId, entry.order);
2765
+ if (entry.removed) removed.add(entry.instanceId);
1977
2766
  }
1978
2767
  } catch {
1979
2768
  }
1980
2769
  }
1981
2770
  sectionOrderIndex = next;
2771
+ removedSectionIds = removed;
1982
2772
  }
1983
2773
  function applyExplicitOrder(entries) {
1984
2774
  if (sectionOrderIndex.size === 0) return entries;
@@ -2014,11 +2804,23 @@ function orderByChain(sections) {
2014
2804
  for (const root of roots) visit(root);
2015
2805
  return out.length === sections.length ? out : sections;
2016
2806
  }
2807
+ function syncSoftRemovedGenerated() {
2808
+ for (const [id, section] of mounted) {
2809
+ const el = section.container;
2810
+ if (removedSectionIds.has(id)) {
2811
+ el.style.display = "none";
2812
+ el.setAttribute(REMOVED_ATTR, "");
2813
+ } else if (el.hasAttribute(REMOVED_ATTR)) {
2814
+ el.style.removeProperty("display");
2815
+ el.removeAttribute(REMOVED_ATTR);
2816
+ }
2817
+ }
2818
+ }
2017
2819
  function applyAiSectionsToDom(state, options) {
2018
2820
  if (typeof document === "undefined") return;
2019
2821
  const brandOverride = deriveBrandOverride();
2020
2822
  const templateBrand = deriveTemplateBrand();
2021
- const templateButtonRadius = deriveTemplateButtonRadius();
2823
+ const templateButtonStyle = deriveTemplateButtonStyle();
2022
2824
  const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
2023
2825
  const pagePath = window.location.pathname;
2024
2826
  const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
@@ -2058,7 +2860,7 @@ function applyAiSectionsToDom(state, options) {
2058
2860
  {
2059
2861
  tree: entry.tree,
2060
2862
  brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2061
- buttonRadius: templateButtonRadius,
2863
+ buttonStyle: templateButtonStyle,
2062
2864
  resolveMedia,
2063
2865
  editKeyPrefix: `ai.${entry.id}`
2064
2866
  }
@@ -2081,6 +2883,7 @@ function applyAiSectionsToDom(state, options) {
2081
2883
  syncReplacedOriginals(state);
2082
2884
  syncRemovedSections(state);
2083
2885
  syncTemplateHidden(state, pageSections.length > 0);
2886
+ syncSoftRemovedGenerated();
2084
2887
  }
2085
2888
 
2086
2889
  // src/useLinkHrefGuardian.ts
@@ -7810,6 +8613,7 @@ function MediaOverlay({
7810
8613
  (prev) => prev && prev.fullWidth === width && prev.height === height ? prev : { fullWidth: width, height }
7811
8614
  );
7812
8615
  }, [isVideo]);
8616
+ const replaceLabel = isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image";
7813
8617
  const replaceMode = buttonSize ? replaceButtonMode({ width: rect.width, height: rect.height }, buttonSize) : "none";
7814
8618
  const box = {
7815
8619
  position: "fixed",
@@ -7939,17 +8743,17 @@ function MediaOverlay({
7939
8743
  },
7940
8744
  children: [
7941
8745
  isVideo ? /* @__PURE__ */ jsx15(Film, { size: 14 }) : /* @__PURE__ */ jsx15(ImageIcon, { size: 14 }),
7942
- isVideo ? "Replace video" : "Replace image"
8746
+ replaceLabel
7943
8747
  ]
7944
8748
  }
7945
8749
  ),
7946
- replaceMode === "none" ? null : /* @__PURE__ */ jsxs7(
8750
+ showChrome && replaceMode !== "none" && /* @__PURE__ */ jsxs7(
7947
8751
  Button,
7948
8752
  {
7949
8753
  "data-ohw-media-overlay": "",
7950
8754
  variant: "outline",
7951
8755
  size: "sm",
7952
- "aria-label": isVideo ? "Replace video" : "Replace image",
8756
+ "aria-label": replaceLabel,
7953
8757
  className: "gap-1.5 cursor-pointer hover:bg-background",
7954
8758
  style: {
7955
8759
  ...OVERLAY_BUTTON_STYLE,
@@ -7972,7 +8776,7 @@ function MediaOverlay({
7972
8776
  },
7973
8777
  children: [
7974
8778
  isVideo ? /* @__PURE__ */ jsx15(Film, { size: 14 }) : /* @__PURE__ */ jsx15(ImageIcon, { size: 14 }),
7975
- replaceMode === "full" ? isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image" : null
8779
+ replaceMode === "full" ? replaceLabel : null
7976
8780
  ]
7977
8781
  }
7978
8782
  )
@@ -8026,234 +8830,22 @@ function CarouselOverlay({
8026
8830
  onClick: (e) => {
8027
8831
  e.stopPropagation();
8028
8832
  onEdit(hover.key);
8029
- },
8030
- children: [
8031
- /* @__PURE__ */ jsx16(GalleryHorizontal, { size: 14 }),
8032
- "Edit gallery"
8033
- ]
8034
- }
8035
- )
8036
- }
8037
- );
8038
- }
8039
-
8040
- // src/ui/ai-section/AiSectionOverlay.tsx
8041
- import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
8042
- import { Check, X } from "lucide-react";
8043
-
8044
- // src/lib/sections.ts
8045
- var LINK_PICKER_EXCLUDED_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
8046
- function isChromeSection(el) {
8047
- return el.matches("header, nav, footer, aside");
8048
- }
8049
- function titleCaseSectionId(id) {
8050
- return id.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
8051
- }
8052
- function parseSectionsFromRoot(root) {
8053
- const seen = /* @__PURE__ */ new Set();
8054
- const sections = [];
8055
- for (const el of root.querySelectorAll("[data-ohw-section]")) {
8056
- const id = el.getAttribute("data-ohw-section") ?? "";
8057
- if (!id || seen.has(id) || LINK_PICKER_EXCLUDED_IDS.has(id)) continue;
8058
- if (el.parentElement?.closest("[data-ohw-section]")) continue;
8059
- if (el.hasAttribute("data-ohw-ai-template-hidden") || el.hasAttribute("data-ohw-ai-removed") || el.hasAttribute("data-ohw-ai-replaced-by"))
8060
- continue;
8061
- seen.add(id);
8062
- const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
8063
- sections.push({ id, label });
8064
- }
8065
- return sections;
8066
- }
8067
- function collectSectionsFromDom() {
8068
- if (typeof document === "undefined") return [];
8069
- return parseSectionsFromRoot(document);
8070
- }
8071
- function parseSectionsFromHtml(html) {
8072
- const doc = new DOMParser().parseFromString(html, "text/html");
8073
- return parseSectionsFromRoot(doc);
8074
- }
8075
-
8076
- // src/lib/section-instances.ts
8077
- var SECTION_ORDER_KEY = "__ohw_section_order";
8078
- var REMOVED_ATTR2 = "data-ohw-section-removed";
8079
- function isRemovedSection(el) {
8080
- return el.hasAttribute(REMOVED_ATTR2);
8081
- }
8082
- function topLevelSections() {
8083
- return Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
8084
- (el) => !el.parentElement?.closest("[data-ohw-section]") && !isChromeSection(el) && !isRemovedSection(el)
8085
- );
8086
- }
8087
- function instanceIdOf(el) {
8088
- return el.dataset.ohwInstance ?? el.dataset.ohwSection ?? "";
8089
- }
8090
- function findByInstanceId(instanceId) {
8091
- const escapedId = CSS.escape(instanceId);
8092
- return document.querySelector(`[data-ohw-instance="${escapedId}"]`) ?? document.querySelector(`[data-ohw-section="${escapedId}"]:not([data-ohw-instance])`);
8093
- }
8094
- function planSectionMove(instanceId, targetIndex, currentPath) {
8095
- const sections = topLevelSections();
8096
- const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
8097
- if (index === -1) return null;
8098
- const dragged = sections[index];
8099
- const others = sections.filter((_, i) => i !== index);
8100
- const clamped = Math.max(0, Math.min(targetIndex, others.length));
8101
- const reordered = [...others.slice(0, clamped), dragged, ...others.slice(clamped)];
8102
- return reordered.map((el, order) => ({
8103
- instanceId: instanceIdOf(el),
8104
- type: el.getAttribute("data-ohw-section") ?? "",
8105
- order,
8106
- pagePath: currentPath
8107
- }));
8108
- }
8109
- function moveSectionInstance(instanceId, direction, currentPath) {
8110
- const sections = topLevelSections();
8111
- const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
8112
- if (index === -1) return null;
8113
- const siblingIndex = direction === "up" ? index - 1 : index + 1;
8114
- if (siblingIndex < 0 || siblingIndex >= sections.length) return null;
8115
- const entries = planSectionMove(instanceId, siblingIndex, currentPath);
8116
- if (!entries) return null;
8117
- applyPersistedOrder(entries);
8118
- return entries;
8119
- }
8120
- function syncRemovedFlags(entries) {
8121
- const removedIds = new Set(entries.filter((e) => e.removed).map((e) => e.instanceId));
8122
- document.querySelectorAll(`[${REMOVED_ATTR2}]`).forEach((el) => {
8123
- if (!removedIds.has(instanceIdOf(el))) {
8124
- el.style.removeProperty("display");
8125
- el.removeAttribute(REMOVED_ATTR2);
8126
- }
8127
- });
8128
- for (const id of removedIds) {
8129
- const el = findByInstanceId(id);
8130
- if (el) {
8131
- el.style.display = "none";
8132
- el.setAttribute(REMOVED_ATTR2, "");
8833
+ },
8834
+ children: [
8835
+ /* @__PURE__ */ jsx16(GalleryHorizontal, { size: 14 }),
8836
+ "Edit gallery"
8837
+ ]
8838
+ }
8839
+ )
8133
8840
  }
8134
- }
8135
- }
8136
- function applyPersistedOrder(entries) {
8137
- syncRemovedFlags(entries);
8138
- if (entries.length === 0) return;
8139
- const sections = topLevelSections();
8140
- if (sections.length === 0) return;
8141
- const orderIndex = new Map(entries.map((e) => [e.instanceId, e.order]));
8142
- const ordered = [...sections].sort((a, b) => {
8143
- const aOrder = orderIndex.get(instanceIdOf(a));
8144
- const bOrder = orderIndex.get(instanceIdOf(b));
8145
- if (aOrder === void 0 && bOrder === void 0) return 0;
8146
- if (aOrder === void 0) return 1;
8147
- if (bOrder === void 0) return -1;
8148
- return aOrder - bOrder;
8149
- });
8150
- let prev = null;
8151
- for (const el of ordered) {
8152
- if (prev) prev.after(el);
8153
- prev = el;
8154
- }
8155
- }
8156
- function setSectionRemoved(instanceId, currentPath, existingEntries, removed) {
8157
- if (!findByInstanceId(instanceId)) return null;
8158
- const byId = new Map(existingEntries.map((e) => [e.instanceId, e]));
8159
- const allSections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
8160
- (el) => !el.parentElement?.closest("[data-ohw-section]") && !isChromeSection(el)
8161
8841
  );
8162
- allSections.forEach((el, order) => {
8163
- const id = instanceIdOf(el);
8164
- if (!byId.has(id)) {
8165
- byId.set(id, { instanceId: id, type: el.getAttribute("data-ohw-section") ?? "", order, pagePath: currentPath });
8166
- }
8167
- });
8168
- const target = byId.get(instanceId);
8169
- if (!target) return null;
8170
- byId.set(instanceId, { ...target, removed });
8171
- const entries = Array.from(byId.values());
8172
- applyPersistedOrder(entries);
8173
- return entries;
8174
- }
8175
- function deleteSectionInstance(instanceId, currentPath, existingEntries) {
8176
- return setSectionRemoved(instanceId, currentPath, existingEntries, true);
8177
- }
8178
- function restoreSectionInstance(instanceId, currentPath, existingEntries) {
8179
- return setSectionRemoved(instanceId, currentPath, existingEntries, false);
8180
- }
8181
- function duplicateSectionInstance(instanceId, newId, currentPath, existingEntries) {
8182
- const original = findByInstanceId(instanceId);
8183
- if (!original) return null;
8184
- const clone = original.cloneNode(true);
8185
- clone.setAttribute("data-ohw-instance", newId);
8186
- const keyRekeys = rekeySectionSubtree(clone, newId);
8187
- original.insertAdjacentElement("afterend", clone);
8188
- const byId = new Map(existingEntries.map((e) => [e.instanceId, e]));
8189
- const entries = topLevelSections().map((el, order) => {
8190
- const id = instanceIdOf(el);
8191
- return {
8192
- instanceId: id,
8193
- type: el.getAttribute("data-ohw-section") ?? "",
8194
- order,
8195
- pagePath: currentPath,
8196
- ...byId.get(id)?.removed ? { removed: true } : {}
8197
- };
8198
- });
8199
- applyPersistedOrder(entries);
8200
- return { entries, keyRekeys };
8201
- }
8202
- function newInstanceId() {
8203
- return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `instance-${Date.now()}-${Math.random().toString(36).slice(2)}`;
8204
- }
8205
- function getPageSectionOrderEntries(raw, currentPath) {
8206
- if (!raw) return [];
8207
- try {
8208
- const entries = JSON.parse(raw);
8209
- return entries.filter((e) => !e.pagePath || e.pagePath === currentPath);
8210
- } catch {
8211
- return [];
8212
- }
8213
- }
8214
- function rekeySectionSubtree(root, instanceId) {
8215
- const suffix = `::${instanceId}`;
8216
- const pairs = [];
8217
- const rekey = (el, attr) => {
8218
- const current = el.getAttribute(attr);
8219
- if (!current) return;
8220
- const base = current.includes("::") ? current.slice(0, current.indexOf("::")) : current;
8221
- const next = `${base}${suffix}`;
8222
- el.setAttribute(attr, next);
8223
- pairs.push({ from: current, to: next });
8224
- };
8225
- if (root.hasAttribute("data-ohw-key")) rekey(root, "data-ohw-key");
8226
- if (root.hasAttribute("data-ohw-href-key")) rekey(root, "data-ohw-href-key");
8227
- root.querySelectorAll("[data-ohw-key]").forEach((el) => rekey(el, "data-ohw-key"));
8228
- root.querySelectorAll("[data-ohw-href-key]").forEach((el) => rekey(el, "data-ohw-href-key"));
8229
- return pairs;
8230
- }
8231
- function initSectionInstancesFromContent(content, currentPath) {
8232
- document.querySelectorAll("[data-ohw-section]:not([data-ohw-instance])").forEach((el) => {
8233
- el.setAttribute("data-ohw-instance", el.getAttribute("data-ohw-section") ?? "");
8234
- });
8235
- const entries = getPageSectionOrderEntries(content[SECTION_ORDER_KEY], currentPath);
8236
- for (const entry of entries) {
8237
- if (entry.instanceId === entry.type) continue;
8238
- if (document.querySelector(`[data-ohw-instance="${CSS.escape(entry.instanceId)}"]`)) continue;
8239
- const original = document.querySelector(
8240
- `[data-ohw-section="${CSS.escape(entry.type)}"][data-ohw-instance="${CSS.escape(entry.type)}"]`
8241
- );
8242
- if (!original) continue;
8243
- const clone = original.cloneNode(true);
8244
- clone.setAttribute("data-ohw-instance", entry.instanceId);
8245
- rekeySectionSubtree(clone, entry.instanceId);
8246
- original.insertAdjacentElement("afterend", clone);
8247
- }
8248
- applyPersistedOrder(entries);
8249
8842
  }
8250
8843
 
8251
8844
  // src/ui/ai-section/AiSectionOverlay.tsx
8845
+ import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
8846
+ import { Check, X } from "lucide-react";
8252
8847
  import { Fragment as Fragment5, jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
8253
- function findSectionElement(instanceId) {
8254
- const escaped = CSS.escape(instanceId);
8255
- return document.querySelector(`[data-ohw-instance="${escaped}"]`) ?? document.querySelector(`[data-ohw-section="${escaped}"]:not([data-ohw-instance])`);
8256
- }
8848
+ var findSectionElement = findByInstanceId;
8257
8849
  function readRect(instanceId) {
8258
8850
  const el = findSectionElement(instanceId);
8259
8851
  if (!el) return null;
@@ -8293,7 +8885,7 @@ function useLiveSectionRect(sectionId) {
8293
8885
  }
8294
8886
  function computeSectionBoundaryFlags(instanceId) {
8295
8887
  const topLevel = topLevelSections();
8296
- const index = topLevel.findIndex((el) => (el.dataset.ohwInstance ?? el.dataset.ohwSection) === instanceId);
8888
+ const index = topLevel.findIndex((el) => instanceIdOf(el) === instanceId);
8297
8889
  if (index === -1) return { isFirst: true, isLast: true };
8298
8890
  return { isFirst: index === 0, isLast: index === topLevel.length - 1 };
8299
8891
  }
@@ -8377,18 +8969,20 @@ function AiSectionOverlay({
8377
8969
  selectedIdRef.current = selectedId;
8378
8970
  const report = useCallback2(
8379
8971
  (el) => {
8972
+ const labelSrc = el ? sectionElementOf(el) : null;
8380
8973
  postToParent2({
8381
8974
  type: "ow:section-selected",
8382
- sectionId: el ? el.dataset.ohwInstance ?? el.dataset.ohwSection ?? null : null,
8383
- sectionLabel: el ? el.dataset.ohwSectionLabel ?? titleCaseSectionId(el.dataset.ohwSection ?? "") : null
8975
+ sectionId: el ? instanceIdOf(el) || null : null,
8976
+ sectionLabel: labelSrc ? labelSrc.dataset.ohwSectionLabel ?? titleCaseSectionId(labelSrc.dataset.ohwSection ?? "") : null
8384
8977
  });
8385
8978
  },
8386
8979
  [postToParent2]
8387
8980
  );
8388
8981
  const selectFromElement = useCallback2(
8389
8982
  (el, options) => {
8390
- const sectionEl = el?.closest("[data-ohw-section]") ?? null;
8391
- const id = sectionEl ? sectionEl.dataset.ohwInstance ?? sectionEl.dataset.ohwSection ?? null : null;
8983
+ const inner = el?.closest("[data-ohw-section]") ?? null;
8984
+ const sectionEl = inner ? movableUnit(inner) : null;
8985
+ const id = sectionEl ? instanceIdOf(sectionEl) || null : null;
8392
8986
  if (id === selectedIdRef.current) return;
8393
8987
  setSelectedId(id);
8394
8988
  if (options?.report !== false) report(sectionEl);
@@ -8454,7 +9048,8 @@ function AiSectionOverlay({
8454
9048
  return;
8455
9049
  }
8456
9050
  const sec = t.closest("[data-ohw-section]");
8457
- setHoveredId(sec ? sec.dataset.ohwInstance ?? sec.dataset.ohwSection ?? null : null);
9051
+ const unit = sec ? movableUnit(sec) : null;
9052
+ setHoveredId(unit ? instanceIdOf(unit) || null : null);
8458
9053
  };
8459
9054
  const onLeave = () => setHoveredId(null);
8460
9055
  document.addEventListener("mousemove", onMove, { passive: true });
@@ -12965,6 +13560,7 @@ function readLogoSizeState(content, placement) {
12965
13560
  function getLogoElement(el) {
12966
13561
  const marked = el.closest('[data-ohw-role="logo"], [data-ohw-logo]');
12967
13562
  if (marked) return marked;
13563
+ if (el.closest('[data-ohw-editable="icon"]')) return null;
12968
13564
  const root = el.closest("nav, [data-ohw-nav-root], footer");
12969
13565
  if (!root) return null;
12970
13566
  const anchor = el.closest("a");
@@ -14037,15 +14633,17 @@ function useSectionDrag({
14037
14633
  clearSectionDragVisuals();
14038
14634
  return;
14039
14635
  }
14040
- const orderJson = JSON.stringify(entries);
14636
+ const orderJson = JSON.stringify(
14637
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, entries)
14638
+ );
14041
14639
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
14042
14640
  setAiSectionOrder(orderJson, window.location.pathname);
14043
14641
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
14044
- applyPersistedOrder(entries);
14642
+ applyPersistedOrder(getPageSectionOrderEntries(orderJson, window.location.pathname));
14045
14643
  clearSectionDragVisuals();
14046
14644
  requestAnimationFrame(() => {
14047
14645
  if (editContentRef.current[SECTION_ORDER_KEY] === orderJson) {
14048
- applyPersistedOrder(entries);
14646
+ applyPersistedOrder(getPageSectionOrderEntries(orderJson, window.location.pathname));
14049
14647
  }
14050
14648
  requestAnimationFrame(() => {
14051
14649
  window.dispatchEvent(new Event("resize"));
@@ -14078,8 +14676,9 @@ function useSectionDrag({
14078
14676
  const target = e.target;
14079
14677
  if (!(target instanceof HTMLElement)) return;
14080
14678
  if (target.closest(SECTION_DRAG_EXCLUDED_SELECTOR)) return;
14081
- const sectionEl = target.closest("[data-ohw-section]");
14082
- if (!sectionEl || isChromeSection(sectionEl) || sectionEl.dataset.ohwSection === "footer") return;
14679
+ const inner = target.closest("[data-ohw-section]");
14680
+ if (!inner || isChromeSection(inner) || inner.dataset.ohwSection === "footer") return;
14681
+ const sectionEl = movableUnit(inner);
14083
14682
  if (!topLevelSections().includes(sectionEl)) return;
14084
14683
  startSectionPressDrag(sectionEl, e.clientX, e.clientY, e.pointerId);
14085
14684
  };
@@ -14346,6 +14945,9 @@ function collectEditableNodes(extraContent, root = document) {
14346
14945
  if (el.dataset.ohwEditable === "link") {
14347
14946
  return { key: el.dataset.ohwKey ?? "", type: "link", text: getLinkHref3(el) };
14348
14947
  }
14948
+ if (el.dataset.ohwEditable === "map") {
14949
+ return { key: el.dataset.ohwKey ?? "", type: "map", text: el.dataset.ohwMapQuery ?? "" };
14950
+ }
14349
14951
  return {
14350
14952
  key: el.dataset.ohwKey ?? "",
14351
14953
  type: el.dataset.ohwEditable ?? "text",
@@ -14908,21 +15510,10 @@ function parseSchedulingInsertAfter(insertAfter) {
14908
15510
  insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
14909
15511
  };
14910
15512
  }
14911
- function resolveSchedulingInsert(insertAfter, explicitInsertBefore) {
14912
- const parsed = parseSchedulingInsertAfter(insertAfter);
14913
- const insertBefore = explicitInsertBefore ?? parsed.insertBefore;
14914
- const effectiveInsertAfter = insertBefore && parsed.insertBefore === null ? `${insertAfter}${INSERT_BEFORE_MARKER}${insertBefore}` : insertAfter;
14915
- return { effectiveInsertAfter, insertBefore };
14916
- }
14917
- function getSchedulingMountPoint(insertAfter) {
14918
- const { anchor } = parseSchedulingInsertAfter(insertAfter);
14919
- let anchorEl = document.querySelector(`[data-ohw-section="${anchor}"]`);
14920
- if (!anchorEl && anchor === "scheduling") {
14921
- const widgets = Array.from(document.querySelectorAll('[data-ohw-section^="scheduling-"]'));
14922
- anchorEl = widgets.at(-1) ?? null;
14923
- }
14924
- if (!anchorEl) return null;
14925
- return anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
15513
+ function resolveEntryAnchor(entry) {
15514
+ if (entry.anchorId !== void 0) return { anchorId: entry.anchorId, beforeId: entry.beforeId ?? null };
15515
+ const parsed = parseSchedulingInsertAfter(entry.insertAfter);
15516
+ return { anchorId: parsed.anchor, beforeId: parsed.insertBefore };
14926
15517
  }
14927
15518
  function schedulingMountDepth(insertAfter) {
14928
15519
  if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
@@ -14939,8 +15530,7 @@ function getPageSchedulingEntries(raw) {
14939
15530
  }
14940
15531
  }
14941
15532
  function isSchedulingWidgetMissing(entry) {
14942
- const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
14943
- return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
15533
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(entry.insertAfter)}"]`);
14944
15534
  }
14945
15535
  function hasMissingSchedulingWidgets(entries) {
14946
15536
  return entries.some(isSchedulingWidgetMissing);
@@ -14978,18 +15568,18 @@ function initSectionsFromContent(content, removeExisting = false, currentPath =
14978
15568
  } catch {
14979
15569
  }
14980
15570
  }
14981
- function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
14982
- const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
14983
- const sectionId = schedulingSectionId(effectiveInsertAfter);
15571
+ function mountSchedulingWidget(anchorId, notifyOnConnect = false, scheduleId, beforeId, existingWidgetId) {
15572
+ const widgetId = existingWidgetId ?? (beforeId ? `${anchorId}${INSERT_BEFORE_MARKER}${beforeId}` : anchorId);
15573
+ const sectionId = schedulingSectionId(widgetId);
14984
15574
  if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
14985
- const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
14986
- if (!mountPoint) return false;
15575
+ const anchorEl = document.querySelector(`[data-ohw-section="${anchorId}"]`);
15576
+ if (!anchorEl) return false;
15577
+ const mountPoint = anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
14987
15578
  const container = document.createElement("div");
14988
15579
  container.dataset.ohwSectionContainer = "scheduling";
14989
- container.dataset.ohwSection = sectionId;
14990
15580
  container.dataset.ohwInstance = sectionId;
14991
- if (insertBefore) {
14992
- const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
15581
+ if (beforeId) {
15582
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${beforeId}"]`);
14993
15583
  const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
14994
15584
  if (!beforePoint) return false;
14995
15585
  beforePoint.insertAdjacentElement("beforebegin", container);
@@ -15000,20 +15590,26 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
15000
15590
  }
15001
15591
  tail.insertAdjacentElement("afterend", container);
15002
15592
  }
15003
- const root = createRoot2(container);
15004
- schedulingRoots.set(container, root);
15005
- flushSync2(() => {
15006
- root.render(
15007
- /* @__PURE__ */ jsx33(
15008
- SchedulingWidget,
15009
- {
15010
- notifyOnConnect,
15011
- initialScheduleId: scheduleId,
15012
- insertAfter: effectiveInsertAfter
15013
- }
15014
- )
15015
- );
15016
- });
15593
+ try {
15594
+ const root = createRoot2(container);
15595
+ schedulingRoots.set(container, root);
15596
+ flushSync2(() => {
15597
+ root.render(
15598
+ /* @__PURE__ */ jsx33(
15599
+ SchedulingWidget,
15600
+ {
15601
+ notifyOnConnect,
15602
+ initialScheduleId: scheduleId,
15603
+ insertAfter: widgetId
15604
+ }
15605
+ )
15606
+ );
15607
+ });
15608
+ } catch (err) {
15609
+ console.error("[ow:scheduling] render threw", err);
15610
+ container.remove();
15611
+ return false;
15612
+ }
15017
15613
  const tracker = getSectionsTracker();
15018
15614
  let sections = [];
15019
15615
  try {
@@ -15021,10 +15617,12 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
15021
15617
  } catch {
15022
15618
  }
15023
15619
  const inEditor = typeof window !== "undefined" && window.self !== window.top;
15024
- if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
15620
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === widgetId)) {
15025
15621
  sections.push({
15026
15622
  type: "scheduling",
15027
- insertAfter: effectiveInsertAfter,
15623
+ insertAfter: widgetId,
15624
+ anchorId,
15625
+ beforeId: beforeId ?? null,
15028
15626
  pagePath: window.location.pathname,
15029
15627
  ...scheduleId ? { scheduleId } : {}
15030
15628
  });
@@ -15038,7 +15636,8 @@ function mountSchedulingEntries(entries, notifyOnConnect = false) {
15038
15636
  for (let i = pending.length - 1; i >= 0; i--) {
15039
15637
  const entry = pending[i];
15040
15638
  const shouldNotify = typeof notifyOnConnect === "function" ? notifyOnConnect(entry) : notifyOnConnect;
15041
- if (mountSchedulingWidget(entry.insertAfter, shouldNotify, entry.scheduleId ?? null)) {
15639
+ const { anchorId, beforeId } = resolveEntryAnchor(entry);
15640
+ if (mountSchedulingWidget(anchorId, shouldNotify, entry.scheduleId ?? null, beforeId, entry.insertAfter)) {
15042
15641
  pending.splice(i, 1);
15043
15642
  }
15044
15643
  }
@@ -15130,7 +15729,7 @@ var EDITOR_CHROME_SELECTOR = '[data-ohw-toolbar], [data-ohw-form-toolbar], [data
15130
15729
  function isOverEditorChrome(x, y) {
15131
15730
  return document.elementsFromPoint(x, y).some((el) => el instanceof HTMLElement && el.matches(EDITOR_CHROME_SELECTOR));
15132
15731
  }
15133
- 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"])';
15732
+ 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"])';
15134
15733
  function getVideoEl2(el) {
15135
15734
  return el instanceof HTMLVideoElement ? el : el.querySelector("video");
15136
15735
  }
@@ -15186,6 +15785,12 @@ function applyVideoSettingNode(key, val) {
15186
15785
  });
15187
15786
  return true;
15188
15787
  }
15788
+ function applyMapQuery(el, val) {
15789
+ if (!(el instanceof HTMLIFrameElement)) return;
15790
+ const nextSrc = `https://www.google.com/maps?q=${encodeURIComponent(val)}&output=embed`;
15791
+ if (el.src !== nextSrc) el.src = nextSrc;
15792
+ el.setAttribute("data-ohw-map-query", val);
15793
+ }
15189
15794
  function applyLinkByKey(key, val) {
15190
15795
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
15191
15796
  if (el.dataset.ohwEditable === "link") applyLinkHref(el, val);
@@ -15196,6 +15801,11 @@ function applyLinkByKey(key, val) {
15196
15801
  hrefAnchors.forEach((el) => applyLinkHref(el, val));
15197
15802
  }
15198
15803
  }
15804
+ function isInsideLinkEditor(target) {
15805
+ return Boolean(
15806
+ 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"]')
15807
+ );
15808
+ }
15199
15809
  function isInsideFloatingPanel(target) {
15200
15810
  return Boolean(target.closest("[data-ohw-floating-panel]"));
15201
15811
  }
@@ -15203,11 +15813,6 @@ function isPointOverFloatingPanel(clientX, clientY) {
15203
15813
  const el = document.elementFromPoint(clientX, clientY);
15204
15814
  return Boolean(el instanceof Element && el.closest("[data-ohw-floating-panel]"));
15205
15815
  }
15206
- function isInsideLinkEditor(target) {
15207
- return Boolean(
15208
- 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"]')
15209
- );
15210
- }
15211
15816
  function getHrefKeyFromElement(el) {
15212
15817
  if (!el) return null;
15213
15818
  const anchor = el.closest("[data-ohw-href-key]");
@@ -15466,7 +16071,7 @@ function getNavigationSelectionParent(el) {
15466
16071
  if (!isFooterLinksContainer(el) && (el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isInferredFooterGroup2(el))) {
15467
16072
  return getFooterLinksContainer();
15468
16073
  }
15469
- if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || isFooterLinksContainer(el)) {
16074
+ 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)) {
15470
16075
  return getNavigationRoot(el);
15471
16076
  }
15472
16077
  return null;
@@ -15681,7 +16286,6 @@ var ICONS = {
15681
16286
  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"/>',
15682
16287
  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"/>'
15683
16288
  };
15684
- var HEIGHT_SETTLE_DELAYS = [150, 400, 800];
15685
16289
  var SELECTION_CHROME_GAP2 = 4;
15686
16290
  var TOOLBAR_STROKE_GAP2 = 4;
15687
16291
  var TOOLBAR_OFFSET_FROM_ELEMENT = SELECTION_CHROME_GAP2 + TOOLBAR_STROKE_GAP2;
@@ -16061,6 +16665,7 @@ function StateToggle({
16061
16665
  );
16062
16666
  }
16063
16667
  var contentCache = /* @__PURE__ */ new Map();
16668
+ var fetchedContentPaths = /* @__PURE__ */ new Set();
16064
16669
  var brandingCache = /* @__PURE__ */ new Map();
16065
16670
  var OHW_LOADER_STYLE = {
16066
16671
  position: "fixed",
@@ -16590,13 +17195,6 @@ function OhhwellsBridge() {
16590
17195
  const [isItemDragging, setIsItemDragging] = useState13(false);
16591
17196
  const [isFooterFrameSelection, setIsFooterFrameSelection] = useState13(false);
16592
17197
  isFooterFrameSelectionRef.current = isFooterFrameSelection;
16593
- const [floatingPanel, setFloatingPanel] = useState13(null);
16594
- const floatingPanelOpenRef = useRef10(false);
16595
- floatingPanelOpenRef.current = floatingPanel !== null;
16596
- const [floatingPanelPos, setFloatingPanelPos] = useState13(null);
16597
- const [logoSizeDraft, setLogoSizeDraft] = useState13(null);
16598
- const [editorViewport, setEditorViewport] = useState13("desktop");
16599
- const [parentScrollSnap, setParentScrollSnap] = useState13(null);
16600
17198
  const [navDropdownPreviewOpen, setNavDropdownPreviewOpen] = useState13(null);
16601
17199
  const [footerHeadingVisible, setFooterHeadingVisible] = useState13(null);
16602
17200
  const footerDragRef = useRef10(null);
@@ -16614,6 +17212,13 @@ function OhhwellsBridge() {
16614
17212
  const brandKitRef = useRef10("");
16615
17213
  const stylesRef = useRef10("");
16616
17214
  const pendingDeleteUndoRef = useRef10(null);
17215
+ const [floatingPanel, setFloatingPanel] = useState13(null);
17216
+ const floatingPanelOpenRef = useRef10(false);
17217
+ const setFloatingPanelRef = useRef10(setFloatingPanel);
17218
+ const [floatingPanelPos, setFloatingPanelPos] = useState13(null);
17219
+ const [logoSizeDraft, setLogoSizeDraft] = useState13(null);
17220
+ const [editorViewport, setEditorViewport] = useState13("desktop");
17221
+ const [parentScrollSnap, setParentScrollSnap] = useState13(null);
16617
17222
  const [sitePages, setSitePages] = useState13([]);
16618
17223
  const [sectionsByPath, setSectionsByPath] = useState13({});
16619
17224
  const sectionsPrefetchGenRef = useRef10(0);
@@ -16622,7 +17227,18 @@ function OhhwellsBridge() {
16622
17227
  const linkPopoverOpenRef = useRef10(false);
16623
17228
  const linkPopoverGraceUntilRef = useRef10(0);
16624
17229
  setLinkPopoverRef.current = setLinkPopover;
17230
+ setFloatingPanelRef.current = setFloatingPanel;
16625
17231
  linkPopoverSessionRef.current = linkPopover;
17232
+ floatingPanelOpenRef.current = Boolean(floatingPanel);
17233
+ useEffect13(() => {
17234
+ const syncViewport = () => {
17235
+ const next = window.innerWidth <= 480 ? "mobile" : "desktop";
17236
+ setEditorViewport((prev) => prev === next ? prev : next);
17237
+ };
17238
+ syncViewport();
17239
+ window.addEventListener("resize", syncViewport);
17240
+ return () => window.removeEventListener("resize", syncViewport);
17241
+ }, []);
16626
17242
  const {
16627
17243
  navDragRef,
16628
17244
  navDropSlots,
@@ -17947,6 +18563,7 @@ function OhhwellsBridge() {
17947
18563
  }
17948
18564
  if (typeof content[STYLE_STORE_KEY] === "string") {
17949
18565
  stylesRef.current = content[STYLE_STORE_KEY];
18566
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
17950
18567
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17951
18568
  }
17952
18569
  applyBrandChrome(content);
@@ -17954,11 +18571,11 @@ function OhhwellsBridge() {
17954
18571
  for (const [key, val] of Object.entries(content)) {
17955
18572
  if (key === "__ohw_sections") continue;
17956
18573
  if (key === AI_SECTIONS_KEY) continue;
18574
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
18575
+ if (LOGO_IMAGE_KEYS.includes(key)) continue;
17957
18576
  if (key === BRAND_KIT_KEY) continue;
17958
18577
  if (key === STYLE_STORE_KEY) continue;
17959
18578
  if (BRAND_CHROME_KEYS.has(key)) continue;
17960
- if (key === LOGO_PLACEHOLDER_KEY) continue;
17961
- if (LOGO_IMAGE_KEYS.includes(key)) continue;
17962
18579
  if (applyVideoSettingNode(key, val)) continue;
17963
18580
  if (applyCarouselNode(key, val)) continue;
17964
18581
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -17986,6 +18603,8 @@ function OhhwellsBridge() {
17986
18603
  }
17987
18604
  } else if (el.dataset.ohwEditable === "link") {
17988
18605
  applyLinkHref(el, val);
18606
+ } else if (el.dataset.ohwEditable === "map") {
18607
+ applyMapQuery(el, val);
17989
18608
  } else if (el.dataset.ohwEditable === "icon") {
17990
18609
  applyIconMarkup(el, val);
17991
18610
  } else if (el.dataset.ohwEditable === "form") {
@@ -18024,7 +18643,9 @@ function OhhwellsBridge() {
18024
18643
  let cancelled = false;
18025
18644
  setFetchState("loading");
18026
18645
  const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
18027
- fetch(`${apiUrl}/api/public/sites/${subdomain}/content`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18646
+ const initialPath = pathname;
18647
+ fetchedContentPaths.add(`${subdomain}::${initialPath}`);
18648
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(initialPath)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18028
18649
  if (cancelled) return;
18029
18650
  const content = data?.content ?? {};
18030
18651
  const branding = Boolean(data?.showBranding);
@@ -18143,10 +18764,10 @@ function OhhwellsBridge() {
18143
18764
  const applyFromCache = () => {
18144
18765
  const content = contentCache.get(subdomain);
18145
18766
  if (!content) return;
18146
- retryMissingSchedulingMounts(getPageSchedulingEntries(content["__ohw_sections"]), false);
18147
- initSectionInstancesFromContent(content, window.location.pathname);
18148
18767
  observer?.disconnect();
18149
18768
  try {
18769
+ retryMissingSchedulingMounts(getPageSchedulingEntries(content["__ohw_sections"]), false);
18770
+ initSectionInstancesFromContent(content, window.location.pathname);
18150
18771
  applyBrandChrome(content);
18151
18772
  if (typeof content[BRAND_KIT_KEY] === "string") {
18152
18773
  applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
@@ -18158,16 +18779,17 @@ function OhhwellsBridge() {
18158
18779
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
18159
18780
  }
18160
18781
  if (typeof content[STYLE_STORE_KEY] === "string") {
18782
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
18161
18783
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
18162
18784
  }
18163
18785
  for (const [key, val] of Object.entries(content)) {
18164
18786
  if (key === "__ohw_sections") continue;
18165
18787
  if (key === AI_SECTIONS_KEY) continue;
18788
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
18789
+ if (LOGO_IMAGE_KEYS.includes(key)) continue;
18166
18790
  if (key === BRAND_KIT_KEY) continue;
18167
18791
  if (key === STYLE_STORE_KEY) continue;
18168
18792
  if (BRAND_CHROME_KEYS.has(key)) continue;
18169
- if (key === LOGO_PLACEHOLDER_KEY) continue;
18170
- if (LOGO_IMAGE_KEYS.includes(key)) continue;
18171
18793
  if (applyVideoSettingNode(key, val)) continue;
18172
18794
  if (applyCarouselNode(key, val)) continue;
18173
18795
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -18182,6 +18804,8 @@ function OhhwellsBridge() {
18182
18804
  if (video && video.src !== val) applyVideoSrc(video, val);
18183
18805
  } else if (el.dataset.ohwEditable === "link") {
18184
18806
  applyLinkHref(el, val);
18807
+ } else if (el.dataset.ohwEditable === "map") {
18808
+ applyMapQuery(el, val);
18185
18809
  } else if (el.dataset.ohwEditable === "form") {
18186
18810
  } else if (isIconMarkupValue(val)) {
18187
18811
  } else if (el.innerHTML !== val) {
@@ -18213,6 +18837,17 @@ function OhhwellsBridge() {
18213
18837
  debounceTimer = setTimeout(applyFromCache, 150);
18214
18838
  };
18215
18839
  applyFromCache();
18840
+ const pathCacheKey = `${subdomain}::${pathname}`;
18841
+ if (!fetchedContentPaths.has(pathCacheKey)) {
18842
+ fetchedContentPaths.add(pathCacheKey);
18843
+ const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
18844
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(pathname)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18845
+ if (!data?.content) return;
18846
+ contentCache.set(subdomain, data.content);
18847
+ applyFromCache();
18848
+ }).catch(() => {
18849
+ });
18850
+ }
18216
18851
  observer = new MutationObserver(scheduleApply);
18217
18852
  observer.observe(document.body, { childList: true, subtree: true });
18218
18853
  return () => {
@@ -18237,6 +18872,10 @@ function OhhwellsBridge() {
18237
18872
  deselectRef.current();
18238
18873
  deactivateRef.current();
18239
18874
  }, [pathname, isEditMode]);
18875
+ useEffect13(() => {
18876
+ if (!isEditMode) return;
18877
+ initSectionInstancesFromContent(editContentRef.current, pathname);
18878
+ }, [pathname, isEditMode]);
18240
18879
  useEffect13(() => {
18241
18880
  const contentForNav = () => {
18242
18881
  if (isEditMode) return editContentRef.current;
@@ -18328,26 +18967,11 @@ function OhhwellsBridge() {
18328
18967
  const t2 = setTimeout(measure, 500);
18329
18968
  const ro = new ResizeObserver(schedule);
18330
18969
  ro.observe(document.body);
18331
- let lastWidth = window.innerWidth;
18332
- let resizeTimers = [];
18333
- const clearResizeTimers = () => {
18334
- resizeTimers.forEach(clearTimeout);
18335
- resizeTimers = [];
18336
- };
18337
- const handleResize = () => {
18338
- if (window.innerWidth === lastWidth) return;
18339
- lastWidth = window.innerWidth;
18340
- clearResizeTimers();
18341
- resizeTimers = HEIGHT_SETTLE_DELAYS.map((delay) => setTimeout(measure, delay));
18342
- };
18343
- window.addEventListener("resize", handleResize);
18344
18970
  return () => {
18345
18971
  clearTimeout(t1);
18346
18972
  clearTimeout(t2);
18347
18973
  if (raf != null) cancelAnimationFrame(raf);
18348
18974
  ro.disconnect();
18349
- clearResizeTimers();
18350
- window.removeEventListener("resize", handleResize);
18351
18975
  };
18352
18976
  }, [pathname, isEditMode, postToParent2]);
18353
18977
  useEffect13(() => {
@@ -18602,9 +19226,6 @@ function OhhwellsBridge() {
18602
19226
  if (target.closest("[data-ohw-state-toggle]")) return;
18603
19227
  if (target.closest("[data-ohw-max-badge]")) return;
18604
19228
  if (isInsideLinkEditor(target)) return;
18605
- if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
18606
- clearMediaSelectionRef.current();
18607
- }
18608
19229
  if (isInsideFloatingPanel(target)) return;
18609
19230
  if (target.closest("[data-ohw-form-toolbar]")) return;
18610
19231
  if (target.closest(
@@ -18612,6 +19233,9 @@ function OhhwellsBridge() {
18612
19233
  )) {
18613
19234
  return;
18614
19235
  }
19236
+ if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
19237
+ clearMediaSelectionRef.current();
19238
+ }
18615
19239
  {
18616
19240
  const formEl = getFormElement(target);
18617
19241
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
@@ -18763,14 +19387,6 @@ function OhhwellsBridge() {
18763
19387
  }
18764
19388
  const clickedButton = findClosestButtonLike(target);
18765
19389
  const buttonOnMedia = Boolean(clickedButton && isMediaEditable(editable) && editable.contains(clickedButton));
18766
- console.log("[click-debug]", {
18767
- editableType: editable.dataset.ohwEditable,
18768
- editableTag: editable.tagName,
18769
- targetTag: target.tagName,
18770
- clickedButtonTag: clickedButton?.tagName ?? null,
18771
- buttonOnMedia,
18772
- isMediaEditableEditable: isMediaEditable(editable)
18773
- });
18774
19390
  if (isMediaEditable(editable) && !buttonOnMedia) {
18775
19391
  e.preventDefault();
18776
19392
  e.stopPropagation();
@@ -18797,11 +19413,6 @@ function OhhwellsBridge() {
18797
19413
  const hrefLookupTarget = buttonOnMedia ? clickedButton : editable;
18798
19414
  const hrefCtx = getHrefKeyFromElement(hrefLookupTarget);
18799
19415
  const navAnchor = hrefCtx ? getNavigationItemAnchor(hrefCtx.anchor) : null;
18800
- console.log("[click-debug 2]", {
18801
- hrefLookupTargetTag: hrefLookupTarget.tagName,
18802
- hrefCtx: hrefCtx ? { key: hrefCtx.key } : null,
18803
- navAnchorTag: navAnchor?.tagName ?? null
18804
- });
18805
19416
  if (navAnchor) {
18806
19417
  e.preventDefault();
18807
19418
  e.stopPropagation();
@@ -18971,6 +19582,9 @@ function OhhwellsBridge() {
18971
19582
  setHoveredItemRect(null);
18972
19583
  hoveredNavContainerRef.current = null;
18973
19584
  setHoveredNavContainerRect(null);
19585
+ siblingHintElRef.current = null;
19586
+ setSiblingHintRect(null);
19587
+ setSiblingHintRects([]);
18974
19588
  return;
18975
19589
  }
18976
19590
  {
@@ -19089,7 +19703,6 @@ function OhhwellsBridge() {
19089
19703
  hoveredNavContainerRef.current = null;
19090
19704
  setHoveredNavContainerRect(null);
19091
19705
  hoveredItemElRef.current = editable;
19092
- setHoveredItemRect(isSelectedForHoverRef.current(editable) ? null : editable.getBoundingClientRect());
19093
19706
  }
19094
19707
  }
19095
19708
  }
@@ -19386,7 +19999,7 @@ function OhhwellsBridge() {
19386
19999
  }
19387
20000
  };
19388
20001
  const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false) => {
19389
- if (linkPopoverOpenRef.current) {
20002
+ if (linkPopoverOpenRef.current || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
19390
20003
  if (hoveredImageRef.current) {
19391
20004
  hoveredImageRef.current = null;
19392
20005
  hoveredImageHasTextOverlapRef.current = false;
@@ -19751,8 +20364,7 @@ function OhhwellsBridge() {
19751
20364
  };
19752
20365
  const handleMouseMove = (e) => {
19753
20366
  const { clientX, clientY } = e;
19754
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
19755
- if (isOverEditorChrome(clientX, clientY)) {
20367
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY) || isOverEditorChrome(clientX, clientY)) {
19756
20368
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
19757
20369
  formHoverElRef.current = null;
19758
20370
  setFormHoverRect(null);
@@ -19760,6 +20372,12 @@ function OhhwellsBridge() {
19760
20372
  setHoveredItemRect(null);
19761
20373
  hoveredNavContainerRef.current = null;
19762
20374
  setHoveredNavContainerRect(null);
20375
+ siblingHintElRef.current = null;
20376
+ setSiblingHintRect(null);
20377
+ setSiblingHintRects([]);
20378
+ dismissImageHover();
20379
+ clearImageHover();
20380
+ setSectionGap(null);
19763
20381
  return;
19764
20382
  }
19765
20383
  if (probeSocialsRowAt(clientX, clientY)) return;
@@ -19771,7 +20389,11 @@ function OhhwellsBridge() {
19771
20389
  if (e.data?.type !== "ow:pointer-sync") return;
19772
20390
  const { clientX, clientY } = e.data;
19773
20391
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
19774
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
20392
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
20393
+ dismissImageHover();
20394
+ clearImageHover();
20395
+ return;
20396
+ }
19775
20397
  if (probeSocialsRowAt(clientX, clientY)) return;
19776
20398
  probeSectionGapAt(clientX, clientY);
19777
20399
  probeImageAt(clientX, clientY);
@@ -20050,6 +20672,44 @@ function OhhwellsBridge() {
20050
20672
  if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
20051
20673
  }, 400));
20052
20674
  };
20675
+ const reapCommittedAiSections = (excludeIds) => {
20676
+ const aiState = parseAiSectionsState(aiSectionsRef.current);
20677
+ if (aiState.sections.length === 0) return [];
20678
+ let orderEntries = [];
20679
+ try {
20680
+ const parsed = JSON.parse(editContentRef.current[SECTION_ORDER_KEY] ?? "[]");
20681
+ if (Array.isArray(parsed)) orderEntries = parsed;
20682
+ } catch {
20683
+ return [];
20684
+ }
20685
+ const removedIds = orderEntries.filter((entry) => entry && typeof entry.instanceId === "string" && entry.removed).map((entry) => entry.instanceId);
20686
+ if (removedIds.length === 0) return [];
20687
+ const result = reapRemovedAiSections(aiState, parseStyleStore(stylesRef.current), removedIds, excludeIds);
20688
+ if (!result.changed) return [];
20689
+ const nodes = [];
20690
+ aiSectionsRef.current = serializeAiSectionsState(result.state);
20691
+ nodes.push({ key: AI_SECTIONS_KEY, text: aiSectionsRef.current });
20692
+ const reaped = new Set(result.reapedIds);
20693
+ const nextOrderJson = JSON.stringify(orderEntries.filter((entry) => !reaped.has(entry.instanceId)));
20694
+ editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: nextOrderJson };
20695
+ setAiSectionOrder(nextOrderJson, window.location.pathname);
20696
+ nodes.push({ key: SECTION_ORDER_KEY, text: nextOrderJson });
20697
+ if (result.store) {
20698
+ stylesRef.current = JSON.stringify(result.store);
20699
+ nodes.push({ key: STYLE_STORE_KEY, text: stylesRef.current });
20700
+ }
20701
+ const nextContent = { ...editContentRef.current };
20702
+ for (const key of Object.keys(nextContent)) {
20703
+ if (key.startsWith(AI_SLOT_KEY_PREFIX) && result.slotPrefixes.some((prefix) => key.startsWith(prefix)) && nextContent[key] !== "") {
20704
+ nextContent[key] = "";
20705
+ nodes.push({ key, text: "" });
20706
+ }
20707
+ }
20708
+ editContentRef.current = nextContent;
20709
+ applyAiSectionsToDom(result.state);
20710
+ applyStylesToDom(parseStyleStore(stylesRef.current));
20711
+ return nodes;
20712
+ };
20053
20713
  const handleHydrate = (e) => {
20054
20714
  if (e.data?.type !== "ow:hydrate") return;
20055
20715
  const content = e.data.content;
@@ -20068,6 +20728,7 @@ function OhhwellsBridge() {
20068
20728
  }
20069
20729
  if (typeof content[STYLE_STORE_KEY] === "string") {
20070
20730
  stylesRef.current = content[STYLE_STORE_KEY];
20731
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
20071
20732
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
20072
20733
  }
20073
20734
  applyBrandChrome(content);
@@ -20079,11 +20740,11 @@ function OhhwellsBridge() {
20079
20740
  continue;
20080
20741
  }
20081
20742
  if (key === AI_SECTIONS_KEY) continue;
20743
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
20744
+ if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
20082
20745
  if (key === BRAND_KIT_KEY) continue;
20083
20746
  if (key === STYLE_STORE_KEY) continue;
20084
20747
  if (BRAND_CHROME_KEYS.has(key)) continue;
20085
- if (key === LOGO_PLACEHOLDER_KEY) continue;
20086
- if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
20087
20748
  if (applyVideoSettingNode(key, val)) continue;
20088
20749
  if (applyCarouselNode(key, val)) continue;
20089
20750
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -20097,6 +20758,8 @@ function OhhwellsBridge() {
20097
20758
  if (video && video.src !== val) applyVideoSrc(video, val);
20098
20759
  } else if (el.dataset.ohwEditable === "link") {
20099
20760
  applyLinkHref(el, val);
20761
+ } else if (el.dataset.ohwEditable === "map") {
20762
+ applyMapQuery(el, val);
20100
20763
  } else if (el.dataset.ohwEditable === "icon") {
20101
20764
  applyIconMarkup(el, val);
20102
20765
  } else if (isIconMarkupValue(val)) {
@@ -20118,8 +20781,16 @@ function OhhwellsBridge() {
20118
20781
  reconcileFooterOrderFromContent(editContentRef.current);
20119
20782
  syncNavigationDragCursorAttrs();
20120
20783
  enforceLinkHrefs();
20784
+ const hydrateReapExclude = /* @__PURE__ */ new Set();
20785
+ const hydratePendingUndo = pendingDeleteUndoRef.current;
20786
+ if (hydratePendingUndo?.sectionInstanceId) hydrateReapExclude.add(hydratePendingUndo.sectionInstanceId);
20787
+ const reapNodes = reapCommittedAiSections(hydrateReapExclude);
20788
+ if (reapNodes.length > 0) postToParentRef.current({ type: "ow:change", nodes: reapNodes });
20121
20789
  const hydratedHeight = document.body.scrollHeight;
20122
20790
  if (hydratedHeight > 50) postToParentRef.current({ type: "ow:height", height: hydratedHeight });
20791
+ if (parseAiSectionsState(aiSectionsRef.current).sections.length > 0) {
20792
+ postAiSectionsChanged();
20793
+ }
20123
20794
  postToParentRef.current({ type: "ow:hydrate-done" });
20124
20795
  };
20125
20796
  const handleUpdateLogoIdentity = (e) => {
@@ -20257,12 +20928,35 @@ function OhhwellsBridge() {
20257
20928
  window.addEventListener("message", handleAiSetBrand);
20258
20929
  const handleAiSetStyles = (e) => {
20259
20930
  if (e.data?.type !== "ow:ai-set-styles") return;
20260
- const value = typeof e.data.value === "string" ? e.data.value : "";
20931
+ let value = typeof e.data.value === "string" ? e.data.value : "";
20261
20932
  const previous = stylesRef.current;
20933
+ let previousSections;
20934
+ const store = parseStyleStore(value);
20935
+ if (store) {
20936
+ const folded = foldAlignIntoTrees(parseAiSectionsState(aiSectionsRef.current), store);
20937
+ if (folded.changed) {
20938
+ const nextSections = serializeAiSectionsState(folded.state);
20939
+ if (nextSections !== aiSectionsRef.current) {
20940
+ previousSections = aiSectionsRef.current;
20941
+ aiSectionsRef.current = nextSections;
20942
+ applyAiSectionsToDom(folded.state);
20943
+ postToParentRef.current({
20944
+ type: "ow:change",
20945
+ nodes: [{ key: AI_SECTIONS_KEY, text: nextSections }]
20946
+ });
20947
+ }
20948
+ value = JSON.stringify(folded.store);
20949
+ }
20950
+ }
20262
20951
  stylesRef.current = value;
20263
20952
  applyStylesToDom(parseStyleStore(value));
20264
20953
  postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
20265
- postToParentRef.current({ type: "ow:ai-styles-applied", previous, value });
20954
+ postToParentRef.current({
20955
+ type: "ow:ai-styles-applied",
20956
+ previous,
20957
+ value,
20958
+ ...previousSections !== void 0 ? { previousSections } : {}
20959
+ });
20266
20960
  };
20267
20961
  window.addEventListener("message", handleAiSetStyles);
20268
20962
  const handleGetBrand = (e) => {
@@ -20279,8 +20973,11 @@ function OhhwellsBridge() {
20279
20973
  if (!instanceId || !direction) return;
20280
20974
  const entries = moveSectionInstance(instanceId, direction, window.location.pathname);
20281
20975
  if (!entries) return;
20282
- const orderJson = JSON.stringify(entries);
20976
+ const orderJson = JSON.stringify(
20977
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, entries)
20978
+ );
20283
20979
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20980
+ applyPersistedOrder(getPageSectionOrderEntries(orderJson, window.location.pathname));
20284
20981
  setAiSectionOrder(orderJson, window.location.pathname);
20285
20982
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
20286
20983
  window.dispatchEvent(new Event("resize"));
@@ -20326,8 +21023,11 @@ function OhhwellsBridge() {
20326
21023
  const currentEntries = getPageSectionOrderEntries(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname);
20327
21024
  const entries = deleteSectionInstance(instanceId, window.location.pathname, currentEntries);
20328
21025
  if (!entries) return;
20329
- const orderJson = JSON.stringify(entries);
21026
+ const orderJson = JSON.stringify(
21027
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, entries)
21028
+ );
20330
21029
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
21030
+ setAiSectionOrder(orderJson, window.location.pathname);
20331
21031
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
20332
21032
  aiSectionApiRef.current?.clear();
20333
21033
  window.dispatchEvent(new Event("resize"));
@@ -20336,6 +21036,7 @@ function OhhwellsBridge() {
20336
21036
  const actionId = newInstanceId();
20337
21037
  pendingDeleteUndoRef.current = {
20338
21038
  actionId,
21039
+ sectionInstanceId: instanceId,
20339
21040
  restore: () => {
20340
21041
  const restoredEntries = getPageSectionOrderEntries(
20341
21042
  editContentRef.current[SECTION_ORDER_KEY],
@@ -20343,8 +21044,11 @@ function OhhwellsBridge() {
20343
21044
  );
20344
21045
  const restored = restoreSectionInstance(instanceId, window.location.pathname, restoredEntries);
20345
21046
  if (!restored) return;
20346
- const restoredJson = JSON.stringify(restored);
21047
+ const restoredJson = JSON.stringify(
21048
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, restored)
21049
+ );
20347
21050
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: restoredJson };
21051
+ setAiSectionOrder(restoredJson, window.location.pathname);
20348
21052
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: restoredJson }] });
20349
21053
  window.dispatchEvent(new Event("resize"));
20350
21054
  const restoreHeight = document.body.scrollHeight;
@@ -20370,7 +21074,9 @@ function OhhwellsBridge() {
20370
21074
  const result = duplicateSectionInstance(instanceId, newId, window.location.pathname, currentEntries);
20371
21075
  if (!result) return;
20372
21076
  const { entries, keyRekeys } = result;
20373
- const orderJson = JSON.stringify(entries);
21077
+ const orderJson = JSON.stringify(
21078
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, entries)
21079
+ );
20374
21080
  const nodes = [{ key: SECTION_ORDER_KEY, text: orderJson }];
20375
21081
  for (const { from, to } of keyRekeys) {
20376
21082
  const inherited = editContentRef.current[from];
@@ -20380,6 +21086,7 @@ function OhhwellsBridge() {
20380
21086
  ...editContentRef.current,
20381
21087
  ...Object.fromEntries(nodes.map(({ key, text }) => [key, text]))
20382
21088
  };
21089
+ applyPersistedOrder(getPageSectionOrderEntries(orderJson, window.location.pathname));
20383
21090
  setAiSectionOrder(orderJson, window.location.pathname);
20384
21091
  postToParentRef.current({ type: "ow:change", nodes });
20385
21092
  window.dispatchEvent(new Event("resize"));
@@ -20398,6 +21105,12 @@ function OhhwellsBridge() {
20398
21105
  closeLinkPopoverRef.current();
20399
21106
  return;
20400
21107
  }
21108
+ if (floatingPanelOpenRef.current) {
21109
+ setFloatingPanelRef.current(null);
21110
+ deselectRef.current();
21111
+ deactivateRef.current();
21112
+ return;
21113
+ }
20401
21114
  deselectRef.current();
20402
21115
  deactivateRef.current();
20403
21116
  clearMediaSelectionRef.current();
@@ -20643,6 +21356,10 @@ function OhhwellsBridge() {
20643
21356
  };
20644
21357
  const handleSave = (e) => {
20645
21358
  if (e.data?.type !== "ow:save") return;
21359
+ const pendingUndo = pendingDeleteUndoRef.current;
21360
+ const reapExclude = /* @__PURE__ */ new Set();
21361
+ if (pendingUndo?.sectionInstanceId) reapExclude.add(pendingUndo.sectionInstanceId);
21362
+ const reapNodes = reapCommittedAiSections(reapExclude);
20646
21363
  const nodes = collectEditableNodes(editContentRef.current);
20647
21364
  const tracker = document.querySelector("[data-ohw-sections-tracker]");
20648
21365
  if (tracker?.textContent) nodes.push({ key: "__ohw_sections", type: "sections", text: tracker.textContent });
@@ -20662,6 +21379,11 @@ function OhhwellsBridge() {
20662
21379
  const text = specs.length ? JSON.stringify(specs) : editContentRef.current[fieldsKey(formKey)];
20663
21380
  if (text) nodes.push({ key: fieldsKey(formKey), type: "text", text });
20664
21381
  });
21382
+ for (const reapNode of reapNodes) {
21383
+ if (reapNode.key.startsWith(AI_SLOT_KEY_PREFIX) && !nodes.some((n) => n.key === reapNode.key)) {
21384
+ nodes.push({ key: reapNode.key, type: "text", text: reapNode.text });
21385
+ }
21386
+ }
20665
21387
  postToParentRef.current({ type: "ow:save-result", nodes });
20666
21388
  };
20667
21389
  const handleInsertSection = (e) => {
@@ -20672,8 +21394,12 @@ function OhhwellsBridge() {
20672
21394
  if (inserted) {
20673
21395
  const tracker = getSectionsTracker();
20674
21396
  postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
20675
- const h = document.body.scrollHeight;
20676
- if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
21397
+ const reportHeight = () => {
21398
+ const h = document.body.scrollHeight;
21399
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
21400
+ };
21401
+ reportHeight();
21402
+ setTimeout(reportHeight, 500);
20677
21403
  }
20678
21404
  };
20679
21405
  const handleSwitchSchedule = (e) => {
@@ -21077,10 +21803,10 @@ function OhhwellsBridge() {
21077
21803
  window.removeEventListener("message", handleDeleteSection);
21078
21804
  window.removeEventListener("message", handleDuplicateSection);
21079
21805
  window.removeEventListener("message", handleDeactivate);
21080
- document.documentElement.removeAttribute("data-ohw-panel-dragging");
21081
21806
  window.removeEventListener("message", handleToastAction);
21082
21807
  window.removeEventListener("message", handleFormCount);
21083
21808
  window.removeEventListener("message", handleUiEscape);
21809
+ document.documentElement.removeAttribute("data-ohw-panel-dragging");
21084
21810
  autoSaveTimers.current.forEach(clearTimeout);
21085
21811
  autoSaveTimers.current.clear();
21086
21812
  if (imageUnhoverTimerRef.current) clearTimeout(imageUnhoverTimerRef.current);
@@ -21283,7 +22009,7 @@ function OhhwellsBridge() {
21283
22009
  postToParent2({
21284
22010
  type: "ow:ready",
21285
22011
  version: "1",
21286
- bridgeVersion: "0.1.90",
22012
+ bridgeVersion: "0.1.92",
21287
22013
  path: pathname,
21288
22014
  nodes: collectEditableNodes(editContentRef.current),
21289
22015
  sections
@@ -22202,6 +22928,59 @@ function OhhwellsBridge() {
22202
22928
  ) : null
22203
22929
  ] });
22204
22930
  }
22931
+
22932
+ // src/ui/EmptySection.tsx
22933
+ import Link3 from "next/link";
22934
+ import { Fragment as Fragment9, jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
22935
+ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey }) {
22936
+ return /* @__PURE__ */ jsxs21(Fragment9, { children: [
22937
+ /* @__PURE__ */ jsx34(
22938
+ "p",
22939
+ {
22940
+ style: {
22941
+ fontFamily: "var(--brand-font-body)",
22942
+ fontSize: "0.75rem",
22943
+ fontWeight: 500,
22944
+ letterSpacing: "0.15em",
22945
+ textTransform: "uppercase",
22946
+ color: "var(--brand-accent)",
22947
+ marginBottom: "1.5rem"
22948
+ },
22949
+ children: /* @__PURE__ */ jsx34(Link3, { href: homeHref, style: { color: "inherit" }, children: /* @__PURE__ */ jsx34("span", { ...eyebrowKey ? { "data-ohw-editable": "text", "data-ohw-key": eyebrowKey } : {}, children: "Home" }) })
22950
+ }
22951
+ ),
22952
+ /* @__PURE__ */ jsx34(
22953
+ "h1",
22954
+ {
22955
+ style: {
22956
+ fontFamily: "var(--brand-font-heading)",
22957
+ fontSize: "clamp(2rem, 3.5vw, 2.75rem)",
22958
+ lineHeight: 1.1,
22959
+ letterSpacing: "-0.025em",
22960
+ color: "var(--brand-text)",
22961
+ marginBottom: "1rem"
22962
+ },
22963
+ ...titleKey ? { "data-ohw-editable": "text", "data-ohw-key": titleKey, "data-ohw-max-length": 80 } : {},
22964
+ children: title
22965
+ }
22966
+ ),
22967
+ /* @__PURE__ */ jsx34(
22968
+ "p",
22969
+ {
22970
+ style: {
22971
+ fontFamily: "var(--brand-font-body)",
22972
+ fontSize: "1rem",
22973
+ lineHeight: 1.7,
22974
+ fontWeight: 300,
22975
+ color: "var(--brand-text-muted)",
22976
+ maxWidth: "340px"
22977
+ },
22978
+ ...subtitleKey ? { "data-ohw-editable": "text", "data-ohw-key": subtitleKey, "data-ohw-max-length": 160 } : {},
22979
+ children: "This page doesn't have any content yet."
22980
+ }
22981
+ )
22982
+ ] });
22983
+ }
22205
22984
  export {
22206
22985
  AI_DEFAULT_BRAND,
22207
22986
  AI_TREE_SCHEMA_VERSIONS,
@@ -22218,6 +22997,7 @@ export {
22218
22997
  DropdownMenuItem,
22219
22998
  DropdownMenuSeparator,
22220
22999
  DropdownMenuTrigger,
23000
+ EmptySection,
22221
23001
  ItemActionToolbar,
22222
23002
  ItemInteractionLayer,
22223
23003
  LinkEditorPanel,