@ohhwells/bridge 0.1.91 → 0.1.92-next.267

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,7 +2565,7 @@ 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"]);
1826
2571
  function readRootVar(name) {
@@ -1844,13 +2589,13 @@ function deriveBrandOverride() {
1844
2589
  };
1845
2590
  }
1846
2591
  function deriveTemplateBrand() {
1847
- const dark = readRootVar("--color-dark");
1848
- const primary = readRootVar("--color-primary");
1849
- const light = readRootVar("--color-light");
2592
+ const primary = readRootVar("--brand-primary") || readRootVar("--color-primary");
2593
+ const dark = readRootVar("--brand-text") || readRootVar("--color-dark");
2594
+ const light = readRootVar("--brand-background") || readRootVar("--color-light");
1850
2595
  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");
2596
+ const accent = readRootVar("--brand-accent") || readRootVar("--color-accent");
2597
+ const heading = readRootVar("--brand-font-heading") || readRootVar("--font-heading") || readRootVar("--font-display");
2598
+ const body = readRootVar("--brand-font-body") || readRootVar("--font-body");
1854
2599
  return {
1855
2600
  palette: { dark, primary, accent: accent || dark, light },
1856
2601
  fonts: {
@@ -1859,12 +2604,32 @@ function deriveTemplateBrand() {
1859
2604
  }
1860
2605
  };
1861
2606
  }
1862
- function deriveTemplateButtonRadius() {
2607
+ function deriveTemplateButtonStyle() {
1863
2608
  if (typeof document === "undefined") return null;
1864
- const btn = document.querySelector('[data-ohw-role="button"]');
2609
+ const btn = Array.from(document.querySelectorAll('[data-ohw-role="button"]')).find(
2610
+ (el) => !el.closest(`[${CONTAINER_ATTR}]`)
2611
+ );
1865
2612
  if (!btn) return null;
1866
- const radius = getComputedStyle(btn).borderTopLeftRadius;
1867
- return radius || null;
2613
+ const cs = getComputedStyle(btn);
2614
+ const corners = [
2615
+ cs.borderTopLeftRadius,
2616
+ cs.borderTopRightRadius,
2617
+ cs.borderBottomRightRadius,
2618
+ cs.borderBottomLeftRadius
2619
+ ].map((v) => v || "0px");
2620
+ const radius = corners.every((v) => v === corners[0]) ? corners[0] : corners.join(" ");
2621
+ const px = (v) => parseFloat(v) || 0;
2622
+ const padY = Math.max(px(cs.paddingTop), px(cs.paddingBottom));
2623
+ const padX = Math.max(px(cs.paddingLeft), px(cs.paddingRight));
2624
+ return {
2625
+ radius: radius || "10px",
2626
+ padding: `${padY}px ${padX}px`,
2627
+ fontFamily: cs.fontFamily || "",
2628
+ fontSize: cs.fontSize || "",
2629
+ fontWeight: cs.fontWeight || "",
2630
+ letterSpacing: cs.letterSpacing || "",
2631
+ textTransform: cs.textTransform || ""
2632
+ };
1868
2633
  }
1869
2634
  var mounted = /* @__PURE__ */ new Map();
1870
2635
  function findTemplateSection(id) {
@@ -1915,18 +2680,18 @@ function placeContainer(container, entry) {
1915
2680
  }
1916
2681
  function syncRemovedSections(state) {
1917
2682
  const removed = new Set(state.removed ?? []);
1918
- for (const el of document.querySelectorAll(`[${REMOVED_ATTR}]`)) {
2683
+ for (const el of document.querySelectorAll(`[${REMOVED_ATTR2}]`)) {
1919
2684
  const id = el.getAttribute("data-ohw-section") ?? "";
1920
2685
  if (!removed.has(id)) {
1921
2686
  el.style.removeProperty("display");
1922
- el.removeAttribute(REMOVED_ATTR);
2687
+ el.removeAttribute(REMOVED_ATTR2);
1923
2688
  }
1924
2689
  }
1925
2690
  for (const id of removed) {
1926
2691
  const section = findTemplateSection(id);
1927
2692
  if (section && !section.hasAttribute(REPLACED_ATTR)) {
1928
2693
  section.style.display = "none";
1929
- section.setAttribute(REMOVED_ATTR, "");
2694
+ section.setAttribute(REMOVED_ATTR2, "");
1930
2695
  }
1931
2696
  }
1932
2697
  }
@@ -1943,7 +2708,7 @@ function syncTemplateHidden(state, pageHasSections) {
1943
2708
  if (el.hasAttribute(CONTAINER_ATTR)) continue;
1944
2709
  if (CHROME_SECTION_IDS.has(el.getAttribute("data-ohw-section") ?? "")) continue;
1945
2710
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
1946
- if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR)) continue;
2711
+ if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR2)) continue;
1947
2712
  el.style.display = "none";
1948
2713
  el.setAttribute(TEMPLATE_HIDDEN_ATTR, "");
1949
2714
  }
@@ -1967,18 +2732,23 @@ function syncReplacedOriginals(state) {
1967
2732
  }
1968
2733
  }
1969
2734
  var sectionOrderIndex = /* @__PURE__ */ new Map();
2735
+ var removedSectionIds = /* @__PURE__ */ new Set();
1970
2736
  function setAiSectionOrder(raw, currentPath) {
1971
2737
  const next = /* @__PURE__ */ new Map();
2738
+ const removed = /* @__PURE__ */ new Set();
1972
2739
  if (raw) {
1973
2740
  try {
1974
2741
  const entries = JSON.parse(raw);
1975
2742
  for (const entry of entries) {
1976
- if (!entry.pagePath || entry.pagePath === currentPath) next.set(entry.instanceId, entry.order);
2743
+ if (entry.pagePath && entry.pagePath !== currentPath) continue;
2744
+ next.set(entry.instanceId, entry.order);
2745
+ if (entry.removed) removed.add(entry.instanceId);
1977
2746
  }
1978
2747
  } catch {
1979
2748
  }
1980
2749
  }
1981
2750
  sectionOrderIndex = next;
2751
+ removedSectionIds = removed;
1982
2752
  }
1983
2753
  function applyExplicitOrder(entries) {
1984
2754
  if (sectionOrderIndex.size === 0) return entries;
@@ -2014,11 +2784,23 @@ function orderByChain(sections) {
2014
2784
  for (const root of roots) visit(root);
2015
2785
  return out.length === sections.length ? out : sections;
2016
2786
  }
2787
+ function syncSoftRemovedGenerated() {
2788
+ for (const [id, section] of mounted) {
2789
+ const el = section.container;
2790
+ if (removedSectionIds.has(id)) {
2791
+ el.style.display = "none";
2792
+ el.setAttribute(REMOVED_ATTR, "");
2793
+ } else if (el.hasAttribute(REMOVED_ATTR)) {
2794
+ el.style.removeProperty("display");
2795
+ el.removeAttribute(REMOVED_ATTR);
2796
+ }
2797
+ }
2798
+ }
2017
2799
  function applyAiSectionsToDom(state, options) {
2018
2800
  if (typeof document === "undefined") return;
2019
2801
  const brandOverride = deriveBrandOverride();
2020
2802
  const templateBrand = deriveTemplateBrand();
2021
- const templateButtonRadius = deriveTemplateButtonRadius();
2803
+ const templateButtonStyle = deriveTemplateButtonStyle();
2022
2804
  const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
2023
2805
  const pagePath = window.location.pathname;
2024
2806
  const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
@@ -2058,7 +2840,7 @@ function applyAiSectionsToDom(state, options) {
2058
2840
  {
2059
2841
  tree: entry.tree,
2060
2842
  brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2061
- buttonRadius: templateButtonRadius,
2843
+ buttonStyle: templateButtonStyle,
2062
2844
  resolveMedia,
2063
2845
  editKeyPrefix: `ai.${entry.id}`
2064
2846
  }
@@ -2081,6 +2863,7 @@ function applyAiSectionsToDom(state, options) {
2081
2863
  syncReplacedOriginals(state);
2082
2864
  syncRemovedSections(state);
2083
2865
  syncTemplateHidden(state, pageSections.length > 0);
2866
+ syncSoftRemovedGenerated();
2084
2867
  }
2085
2868
 
2086
2869
  // src/useLinkHrefGuardian.ts
@@ -7810,6 +8593,7 @@ function MediaOverlay({
7810
8593
  (prev) => prev && prev.fullWidth === width && prev.height === height ? prev : { fullWidth: width, height }
7811
8594
  );
7812
8595
  }, [isVideo]);
8596
+ const replaceLabel = isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image";
7813
8597
  const replaceMode = buttonSize ? replaceButtonMode({ width: rect.width, height: rect.height }, buttonSize) : "none";
7814
8598
  const box = {
7815
8599
  position: "fixed",
@@ -7939,17 +8723,17 @@ function MediaOverlay({
7939
8723
  },
7940
8724
  children: [
7941
8725
  isVideo ? /* @__PURE__ */ jsx15(Film, { size: 14 }) : /* @__PURE__ */ jsx15(ImageIcon, { size: 14 }),
7942
- isVideo ? "Replace video" : "Replace image"
8726
+ replaceLabel
7943
8727
  ]
7944
8728
  }
7945
8729
  ),
7946
- replaceMode === "none" ? null : /* @__PURE__ */ jsxs7(
8730
+ showChrome && replaceMode !== "none" && /* @__PURE__ */ jsxs7(
7947
8731
  Button,
7948
8732
  {
7949
8733
  "data-ohw-media-overlay": "",
7950
8734
  variant: "outline",
7951
8735
  size: "sm",
7952
- "aria-label": isVideo ? "Replace video" : "Replace image",
8736
+ "aria-label": replaceLabel,
7953
8737
  className: "gap-1.5 cursor-pointer hover:bg-background",
7954
8738
  style: {
7955
8739
  ...OVERLAY_BUTTON_STYLE,
@@ -7972,7 +8756,7 @@ function MediaOverlay({
7972
8756
  },
7973
8757
  children: [
7974
8758
  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
8759
+ replaceMode === "full" ? replaceLabel : null
7976
8760
  ]
7977
8761
  }
7978
8762
  )
@@ -8025,235 +8809,23 @@ function CarouselOverlay({
8025
8809
  onMouseDown: (e) => e.preventDefault(),
8026
8810
  onClick: (e) => {
8027
8811
  e.stopPropagation();
8028
- 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, "");
8812
+ onEdit(hover.key);
8813
+ },
8814
+ children: [
8815
+ /* @__PURE__ */ jsx16(GalleryHorizontal, { size: 14 }),
8816
+ "Edit gallery"
8817
+ ]
8818
+ }
8819
+ )
8133
8820
  }
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
8821
  );
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
8822
  }
8250
8823
 
8251
8824
  // src/ui/ai-section/AiSectionOverlay.tsx
8825
+ import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
8826
+ import { Check, X } from "lucide-react";
8252
8827
  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
- }
8828
+ var findSectionElement = findByInstanceId;
8257
8829
  function readRect(instanceId) {
8258
8830
  const el = findSectionElement(instanceId);
8259
8831
  if (!el) return null;
@@ -8293,7 +8865,7 @@ function useLiveSectionRect(sectionId) {
8293
8865
  }
8294
8866
  function computeSectionBoundaryFlags(instanceId) {
8295
8867
  const topLevel = topLevelSections();
8296
- const index = topLevel.findIndex((el) => (el.dataset.ohwInstance ?? el.dataset.ohwSection) === instanceId);
8868
+ const index = topLevel.findIndex((el) => instanceIdOf(el) === instanceId);
8297
8869
  if (index === -1) return { isFirst: true, isLast: true };
8298
8870
  return { isFirst: index === 0, isLast: index === topLevel.length - 1 };
8299
8871
  }
@@ -8377,18 +8949,20 @@ function AiSectionOverlay({
8377
8949
  selectedIdRef.current = selectedId;
8378
8950
  const report = useCallback2(
8379
8951
  (el) => {
8952
+ const labelSrc = el ? sectionElementOf(el) : null;
8380
8953
  postToParent2({
8381
8954
  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
8955
+ sectionId: el ? instanceIdOf(el) || null : null,
8956
+ sectionLabel: labelSrc ? labelSrc.dataset.ohwSectionLabel ?? titleCaseSectionId(labelSrc.dataset.ohwSection ?? "") : null
8384
8957
  });
8385
8958
  },
8386
8959
  [postToParent2]
8387
8960
  );
8388
8961
  const selectFromElement = useCallback2(
8389
8962
  (el, options) => {
8390
- const sectionEl = el?.closest("[data-ohw-section]") ?? null;
8391
- const id = sectionEl ? sectionEl.dataset.ohwInstance ?? sectionEl.dataset.ohwSection ?? null : null;
8963
+ const inner = el?.closest("[data-ohw-section]") ?? null;
8964
+ const sectionEl = inner ? movableUnit(inner) : null;
8965
+ const id = sectionEl ? instanceIdOf(sectionEl) || null : null;
8392
8966
  if (id === selectedIdRef.current) return;
8393
8967
  setSelectedId(id);
8394
8968
  if (options?.report !== false) report(sectionEl);
@@ -8454,7 +9028,8 @@ function AiSectionOverlay({
8454
9028
  return;
8455
9029
  }
8456
9030
  const sec = t.closest("[data-ohw-section]");
8457
- setHoveredId(sec ? sec.dataset.ohwInstance ?? sec.dataset.ohwSection ?? null : null);
9031
+ const unit = sec ? movableUnit(sec) : null;
9032
+ setHoveredId(unit ? instanceIdOf(unit) || null : null);
8458
9033
  };
8459
9034
  const onLeave = () => setHoveredId(null);
8460
9035
  document.addEventListener("mousemove", onMove, { passive: true });
@@ -12965,6 +13540,7 @@ function readLogoSizeState(content, placement) {
12965
13540
  function getLogoElement(el) {
12966
13541
  const marked = el.closest('[data-ohw-role="logo"], [data-ohw-logo]');
12967
13542
  if (marked) return marked;
13543
+ if (el.closest('[data-ohw-editable="icon"]')) return null;
12968
13544
  const root = el.closest("nav, [data-ohw-nav-root], footer");
12969
13545
  if (!root) return null;
12970
13546
  const anchor = el.closest("a");
@@ -14037,15 +14613,17 @@ function useSectionDrag({
14037
14613
  clearSectionDragVisuals();
14038
14614
  return;
14039
14615
  }
14040
- const orderJson = JSON.stringify(entries);
14616
+ const orderJson = JSON.stringify(
14617
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, entries)
14618
+ );
14041
14619
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
14042
14620
  setAiSectionOrder(orderJson, window.location.pathname);
14043
14621
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
14044
- applyPersistedOrder(entries);
14622
+ applyPersistedOrder(getPageSectionOrderEntries(orderJson, window.location.pathname));
14045
14623
  clearSectionDragVisuals();
14046
14624
  requestAnimationFrame(() => {
14047
14625
  if (editContentRef.current[SECTION_ORDER_KEY] === orderJson) {
14048
- applyPersistedOrder(entries);
14626
+ applyPersistedOrder(getPageSectionOrderEntries(orderJson, window.location.pathname));
14049
14627
  }
14050
14628
  requestAnimationFrame(() => {
14051
14629
  window.dispatchEvent(new Event("resize"));
@@ -14078,8 +14656,9 @@ function useSectionDrag({
14078
14656
  const target = e.target;
14079
14657
  if (!(target instanceof HTMLElement)) return;
14080
14658
  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;
14659
+ const inner = target.closest("[data-ohw-section]");
14660
+ if (!inner || isChromeSection(inner) || inner.dataset.ohwSection === "footer") return;
14661
+ const sectionEl = movableUnit(inner);
14083
14662
  if (!topLevelSections().includes(sectionEl)) return;
14084
14663
  startSectionPressDrag(sectionEl, e.clientX, e.clientY, e.pointerId);
14085
14664
  };
@@ -14346,6 +14925,9 @@ function collectEditableNodes(extraContent, root = document) {
14346
14925
  if (el.dataset.ohwEditable === "link") {
14347
14926
  return { key: el.dataset.ohwKey ?? "", type: "link", text: getLinkHref3(el) };
14348
14927
  }
14928
+ if (el.dataset.ohwEditable === "map") {
14929
+ return { key: el.dataset.ohwKey ?? "", type: "map", text: el.dataset.ohwMapQuery ?? "" };
14930
+ }
14349
14931
  return {
14350
14932
  key: el.dataset.ohwKey ?? "",
14351
14933
  type: el.dataset.ohwEditable ?? "text",
@@ -14908,21 +15490,10 @@ function parseSchedulingInsertAfter(insertAfter) {
14908
15490
  insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
14909
15491
  };
14910
15492
  }
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;
15493
+ function resolveEntryAnchor(entry) {
15494
+ if (entry.anchorId !== void 0) return { anchorId: entry.anchorId, beforeId: entry.beforeId ?? null };
15495
+ const parsed = parseSchedulingInsertAfter(entry.insertAfter);
15496
+ return { anchorId: parsed.anchor, beforeId: parsed.insertBefore };
14926
15497
  }
14927
15498
  function schedulingMountDepth(insertAfter) {
14928
15499
  if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
@@ -14939,8 +15510,7 @@ function getPageSchedulingEntries(raw) {
14939
15510
  }
14940
15511
  }
14941
15512
  function isSchedulingWidgetMissing(entry) {
14942
- const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
14943
- return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
15513
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(entry.insertAfter)}"]`);
14944
15514
  }
14945
15515
  function hasMissingSchedulingWidgets(entries) {
14946
15516
  return entries.some(isSchedulingWidgetMissing);
@@ -14978,18 +15548,18 @@ function initSectionsFromContent(content, removeExisting = false, currentPath =
14978
15548
  } catch {
14979
15549
  }
14980
15550
  }
14981
- function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
14982
- const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
14983
- const sectionId = schedulingSectionId(effectiveInsertAfter);
15551
+ function mountSchedulingWidget(anchorId, notifyOnConnect = false, scheduleId, beforeId, existingWidgetId) {
15552
+ const widgetId = existingWidgetId ?? (beforeId ? `${anchorId}${INSERT_BEFORE_MARKER}${beforeId}` : anchorId);
15553
+ const sectionId = schedulingSectionId(widgetId);
14984
15554
  if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
14985
- const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
14986
- if (!mountPoint) return false;
15555
+ const anchorEl = document.querySelector(`[data-ohw-section="${anchorId}"]`);
15556
+ if (!anchorEl) return false;
15557
+ const mountPoint = anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
14987
15558
  const container = document.createElement("div");
14988
15559
  container.dataset.ohwSectionContainer = "scheduling";
14989
- container.dataset.ohwSection = sectionId;
14990
15560
  container.dataset.ohwInstance = sectionId;
14991
- if (insertBefore) {
14992
- const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
15561
+ if (beforeId) {
15562
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${beforeId}"]`);
14993
15563
  const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
14994
15564
  if (!beforePoint) return false;
14995
15565
  beforePoint.insertAdjacentElement("beforebegin", container);
@@ -15000,20 +15570,26 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
15000
15570
  }
15001
15571
  tail.insertAdjacentElement("afterend", container);
15002
15572
  }
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
- });
15573
+ try {
15574
+ const root = createRoot2(container);
15575
+ schedulingRoots.set(container, root);
15576
+ flushSync2(() => {
15577
+ root.render(
15578
+ /* @__PURE__ */ jsx33(
15579
+ SchedulingWidget,
15580
+ {
15581
+ notifyOnConnect,
15582
+ initialScheduleId: scheduleId,
15583
+ insertAfter: widgetId
15584
+ }
15585
+ )
15586
+ );
15587
+ });
15588
+ } catch (err) {
15589
+ console.error("[ow:scheduling] render threw", err);
15590
+ container.remove();
15591
+ return false;
15592
+ }
15017
15593
  const tracker = getSectionsTracker();
15018
15594
  let sections = [];
15019
15595
  try {
@@ -15021,10 +15597,12 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
15021
15597
  } catch {
15022
15598
  }
15023
15599
  const inEditor = typeof window !== "undefined" && window.self !== window.top;
15024
- if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
15600
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === widgetId)) {
15025
15601
  sections.push({
15026
15602
  type: "scheduling",
15027
- insertAfter: effectiveInsertAfter,
15603
+ insertAfter: widgetId,
15604
+ anchorId,
15605
+ beforeId: beforeId ?? null,
15028
15606
  pagePath: window.location.pathname,
15029
15607
  ...scheduleId ? { scheduleId } : {}
15030
15608
  });
@@ -15038,7 +15616,8 @@ function mountSchedulingEntries(entries, notifyOnConnect = false) {
15038
15616
  for (let i = pending.length - 1; i >= 0; i--) {
15039
15617
  const entry = pending[i];
15040
15618
  const shouldNotify = typeof notifyOnConnect === "function" ? notifyOnConnect(entry) : notifyOnConnect;
15041
- if (mountSchedulingWidget(entry.insertAfter, shouldNotify, entry.scheduleId ?? null)) {
15619
+ const { anchorId, beforeId } = resolveEntryAnchor(entry);
15620
+ if (mountSchedulingWidget(anchorId, shouldNotify, entry.scheduleId ?? null, beforeId, entry.insertAfter)) {
15042
15621
  pending.splice(i, 1);
15043
15622
  }
15044
15623
  }
@@ -15130,7 +15709,7 @@ var EDITOR_CHROME_SELECTOR = '[data-ohw-toolbar], [data-ohw-form-toolbar], [data
15130
15709
  function isOverEditorChrome(x, y) {
15131
15710
  return document.elementsFromPoint(x, y).some((el) => el instanceof HTMLElement && el.matches(EDITOR_CHROME_SELECTOR));
15132
15711
  }
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"])';
15712
+ 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
15713
  function getVideoEl2(el) {
15135
15714
  return el instanceof HTMLVideoElement ? el : el.querySelector("video");
15136
15715
  }
@@ -15186,6 +15765,12 @@ function applyVideoSettingNode(key, val) {
15186
15765
  });
15187
15766
  return true;
15188
15767
  }
15768
+ function applyMapQuery(el, val) {
15769
+ if (!(el instanceof HTMLIFrameElement)) return;
15770
+ const nextSrc = `https://www.google.com/maps?q=${encodeURIComponent(val)}&output=embed`;
15771
+ if (el.src !== nextSrc) el.src = nextSrc;
15772
+ el.setAttribute("data-ohw-map-query", val);
15773
+ }
15189
15774
  function applyLinkByKey(key, val) {
15190
15775
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
15191
15776
  if (el.dataset.ohwEditable === "link") applyLinkHref(el, val);
@@ -15196,6 +15781,11 @@ function applyLinkByKey(key, val) {
15196
15781
  hrefAnchors.forEach((el) => applyLinkHref(el, val));
15197
15782
  }
15198
15783
  }
15784
+ function isInsideLinkEditor(target) {
15785
+ return Boolean(
15786
+ 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"]')
15787
+ );
15788
+ }
15199
15789
  function isInsideFloatingPanel(target) {
15200
15790
  return Boolean(target.closest("[data-ohw-floating-panel]"));
15201
15791
  }
@@ -15203,11 +15793,6 @@ function isPointOverFloatingPanel(clientX, clientY) {
15203
15793
  const el = document.elementFromPoint(clientX, clientY);
15204
15794
  return Boolean(el instanceof Element && el.closest("[data-ohw-floating-panel]"));
15205
15795
  }
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
15796
  function getHrefKeyFromElement(el) {
15212
15797
  if (!el) return null;
15213
15798
  const anchor = el.closest("[data-ohw-href-key]");
@@ -15466,7 +16051,7 @@ function getNavigationSelectionParent(el) {
15466
16051
  if (!isFooterLinksContainer(el) && (el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isInferredFooterGroup2(el))) {
15467
16052
  return getFooterLinksContainer();
15468
16053
  }
15469
- if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || isFooterLinksContainer(el)) {
16054
+ 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
16055
  return getNavigationRoot(el);
15471
16056
  }
15472
16057
  return null;
@@ -15681,7 +16266,6 @@ var ICONS = {
15681
16266
  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
16267
  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
16268
  };
15684
- var HEIGHT_SETTLE_DELAYS = [150, 400, 800];
15685
16269
  var SELECTION_CHROME_GAP2 = 4;
15686
16270
  var TOOLBAR_STROKE_GAP2 = 4;
15687
16271
  var TOOLBAR_OFFSET_FROM_ELEMENT = SELECTION_CHROME_GAP2 + TOOLBAR_STROKE_GAP2;
@@ -16061,6 +16645,7 @@ function StateToggle({
16061
16645
  );
16062
16646
  }
16063
16647
  var contentCache = /* @__PURE__ */ new Map();
16648
+ var fetchedContentPaths = /* @__PURE__ */ new Set();
16064
16649
  var brandingCache = /* @__PURE__ */ new Map();
16065
16650
  var OHW_LOADER_STYLE = {
16066
16651
  position: "fixed",
@@ -16590,13 +17175,6 @@ function OhhwellsBridge() {
16590
17175
  const [isItemDragging, setIsItemDragging] = useState13(false);
16591
17176
  const [isFooterFrameSelection, setIsFooterFrameSelection] = useState13(false);
16592
17177
  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
17178
  const [navDropdownPreviewOpen, setNavDropdownPreviewOpen] = useState13(null);
16601
17179
  const [footerHeadingVisible, setFooterHeadingVisible] = useState13(null);
16602
17180
  const footerDragRef = useRef10(null);
@@ -16614,6 +17192,13 @@ function OhhwellsBridge() {
16614
17192
  const brandKitRef = useRef10("");
16615
17193
  const stylesRef = useRef10("");
16616
17194
  const pendingDeleteUndoRef = useRef10(null);
17195
+ const [floatingPanel, setFloatingPanel] = useState13(null);
17196
+ const floatingPanelOpenRef = useRef10(false);
17197
+ const setFloatingPanelRef = useRef10(setFloatingPanel);
17198
+ const [floatingPanelPos, setFloatingPanelPos] = useState13(null);
17199
+ const [logoSizeDraft, setLogoSizeDraft] = useState13(null);
17200
+ const [editorViewport, setEditorViewport] = useState13("desktop");
17201
+ const [parentScrollSnap, setParentScrollSnap] = useState13(null);
16617
17202
  const [sitePages, setSitePages] = useState13([]);
16618
17203
  const [sectionsByPath, setSectionsByPath] = useState13({});
16619
17204
  const sectionsPrefetchGenRef = useRef10(0);
@@ -16622,7 +17207,18 @@ function OhhwellsBridge() {
16622
17207
  const linkPopoverOpenRef = useRef10(false);
16623
17208
  const linkPopoverGraceUntilRef = useRef10(0);
16624
17209
  setLinkPopoverRef.current = setLinkPopover;
17210
+ setFloatingPanelRef.current = setFloatingPanel;
16625
17211
  linkPopoverSessionRef.current = linkPopover;
17212
+ floatingPanelOpenRef.current = Boolean(floatingPanel);
17213
+ useEffect13(() => {
17214
+ const syncViewport = () => {
17215
+ const next = window.innerWidth <= 480 ? "mobile" : "desktop";
17216
+ setEditorViewport((prev) => prev === next ? prev : next);
17217
+ };
17218
+ syncViewport();
17219
+ window.addEventListener("resize", syncViewport);
17220
+ return () => window.removeEventListener("resize", syncViewport);
17221
+ }, []);
16626
17222
  const {
16627
17223
  navDragRef,
16628
17224
  navDropSlots,
@@ -17947,6 +18543,7 @@ function OhhwellsBridge() {
17947
18543
  }
17948
18544
  if (typeof content[STYLE_STORE_KEY] === "string") {
17949
18545
  stylesRef.current = content[STYLE_STORE_KEY];
18546
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
17950
18547
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17951
18548
  }
17952
18549
  applyBrandChrome(content);
@@ -17954,11 +18551,11 @@ function OhhwellsBridge() {
17954
18551
  for (const [key, val] of Object.entries(content)) {
17955
18552
  if (key === "__ohw_sections") continue;
17956
18553
  if (key === AI_SECTIONS_KEY) continue;
18554
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
18555
+ if (LOGO_IMAGE_KEYS.includes(key)) continue;
17957
18556
  if (key === BRAND_KIT_KEY) continue;
17958
18557
  if (key === STYLE_STORE_KEY) continue;
17959
18558
  if (BRAND_CHROME_KEYS.has(key)) continue;
17960
- if (key === LOGO_PLACEHOLDER_KEY) continue;
17961
- if (LOGO_IMAGE_KEYS.includes(key)) continue;
17962
18559
  if (applyVideoSettingNode(key, val)) continue;
17963
18560
  if (applyCarouselNode(key, val)) continue;
17964
18561
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -17986,6 +18583,8 @@ function OhhwellsBridge() {
17986
18583
  }
17987
18584
  } else if (el.dataset.ohwEditable === "link") {
17988
18585
  applyLinkHref(el, val);
18586
+ } else if (el.dataset.ohwEditable === "map") {
18587
+ applyMapQuery(el, val);
17989
18588
  } else if (el.dataset.ohwEditable === "icon") {
17990
18589
  applyIconMarkup(el, val);
17991
18590
  } else if (el.dataset.ohwEditable === "form") {
@@ -18024,7 +18623,9 @@ function OhhwellsBridge() {
18024
18623
  let cancelled = false;
18025
18624
  setFetchState("loading");
18026
18625
  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) => {
18626
+ const initialPath = pathname;
18627
+ fetchedContentPaths.add(`${subdomain}::${initialPath}`);
18628
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(initialPath)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18028
18629
  if (cancelled) return;
18029
18630
  const content = data?.content ?? {};
18030
18631
  const branding = Boolean(data?.showBranding);
@@ -18143,10 +18744,10 @@ function OhhwellsBridge() {
18143
18744
  const applyFromCache = () => {
18144
18745
  const content = contentCache.get(subdomain);
18145
18746
  if (!content) return;
18146
- retryMissingSchedulingMounts(getPageSchedulingEntries(content["__ohw_sections"]), false);
18147
- initSectionInstancesFromContent(content, window.location.pathname);
18148
18747
  observer?.disconnect();
18149
18748
  try {
18749
+ retryMissingSchedulingMounts(getPageSchedulingEntries(content["__ohw_sections"]), false);
18750
+ initSectionInstancesFromContent(content, window.location.pathname);
18150
18751
  applyBrandChrome(content);
18151
18752
  if (typeof content[BRAND_KIT_KEY] === "string") {
18152
18753
  applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
@@ -18158,16 +18759,17 @@ function OhhwellsBridge() {
18158
18759
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
18159
18760
  }
18160
18761
  if (typeof content[STYLE_STORE_KEY] === "string") {
18762
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
18161
18763
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
18162
18764
  }
18163
18765
  for (const [key, val] of Object.entries(content)) {
18164
18766
  if (key === "__ohw_sections") continue;
18165
18767
  if (key === AI_SECTIONS_KEY) continue;
18768
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
18769
+ if (LOGO_IMAGE_KEYS.includes(key)) continue;
18166
18770
  if (key === BRAND_KIT_KEY) continue;
18167
18771
  if (key === STYLE_STORE_KEY) continue;
18168
18772
  if (BRAND_CHROME_KEYS.has(key)) continue;
18169
- if (key === LOGO_PLACEHOLDER_KEY) continue;
18170
- if (LOGO_IMAGE_KEYS.includes(key)) continue;
18171
18773
  if (applyVideoSettingNode(key, val)) continue;
18172
18774
  if (applyCarouselNode(key, val)) continue;
18173
18775
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -18182,6 +18784,8 @@ function OhhwellsBridge() {
18182
18784
  if (video && video.src !== val) applyVideoSrc(video, val);
18183
18785
  } else if (el.dataset.ohwEditable === "link") {
18184
18786
  applyLinkHref(el, val);
18787
+ } else if (el.dataset.ohwEditable === "map") {
18788
+ applyMapQuery(el, val);
18185
18789
  } else if (el.dataset.ohwEditable === "form") {
18186
18790
  } else if (isIconMarkupValue(val)) {
18187
18791
  } else if (el.innerHTML !== val) {
@@ -18213,6 +18817,17 @@ function OhhwellsBridge() {
18213
18817
  debounceTimer = setTimeout(applyFromCache, 150);
18214
18818
  };
18215
18819
  applyFromCache();
18820
+ const pathCacheKey = `${subdomain}::${pathname}`;
18821
+ if (!fetchedContentPaths.has(pathCacheKey)) {
18822
+ fetchedContentPaths.add(pathCacheKey);
18823
+ const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
18824
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(pathname)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18825
+ if (!data?.content) return;
18826
+ contentCache.set(subdomain, data.content);
18827
+ applyFromCache();
18828
+ }).catch(() => {
18829
+ });
18830
+ }
18216
18831
  observer = new MutationObserver(scheduleApply);
18217
18832
  observer.observe(document.body, { childList: true, subtree: true });
18218
18833
  return () => {
@@ -18237,6 +18852,10 @@ function OhhwellsBridge() {
18237
18852
  deselectRef.current();
18238
18853
  deactivateRef.current();
18239
18854
  }, [pathname, isEditMode]);
18855
+ useEffect13(() => {
18856
+ if (!isEditMode) return;
18857
+ initSectionInstancesFromContent(editContentRef.current, pathname);
18858
+ }, [pathname, isEditMode]);
18240
18859
  useEffect13(() => {
18241
18860
  const contentForNav = () => {
18242
18861
  if (isEditMode) return editContentRef.current;
@@ -18328,26 +18947,11 @@ function OhhwellsBridge() {
18328
18947
  const t2 = setTimeout(measure, 500);
18329
18948
  const ro = new ResizeObserver(schedule);
18330
18949
  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
18950
  return () => {
18345
18951
  clearTimeout(t1);
18346
18952
  clearTimeout(t2);
18347
18953
  if (raf != null) cancelAnimationFrame(raf);
18348
18954
  ro.disconnect();
18349
- clearResizeTimers();
18350
- window.removeEventListener("resize", handleResize);
18351
18955
  };
18352
18956
  }, [pathname, isEditMode, postToParent2]);
18353
18957
  useEffect13(() => {
@@ -18602,9 +19206,6 @@ function OhhwellsBridge() {
18602
19206
  if (target.closest("[data-ohw-state-toggle]")) return;
18603
19207
  if (target.closest("[data-ohw-max-badge]")) return;
18604
19208
  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
19209
  if (isInsideFloatingPanel(target)) return;
18609
19210
  if (target.closest("[data-ohw-form-toolbar]")) return;
18610
19211
  if (target.closest(
@@ -18612,6 +19213,9 @@ function OhhwellsBridge() {
18612
19213
  )) {
18613
19214
  return;
18614
19215
  }
19216
+ if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
19217
+ clearMediaSelectionRef.current();
19218
+ }
18615
19219
  {
18616
19220
  const formEl = getFormElement(target);
18617
19221
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
@@ -18763,14 +19367,6 @@ function OhhwellsBridge() {
18763
19367
  }
18764
19368
  const clickedButton = findClosestButtonLike(target);
18765
19369
  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
19370
  if (isMediaEditable(editable) && !buttonOnMedia) {
18775
19371
  e.preventDefault();
18776
19372
  e.stopPropagation();
@@ -18797,11 +19393,6 @@ function OhhwellsBridge() {
18797
19393
  const hrefLookupTarget = buttonOnMedia ? clickedButton : editable;
18798
19394
  const hrefCtx = getHrefKeyFromElement(hrefLookupTarget);
18799
19395
  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
19396
  if (navAnchor) {
18806
19397
  e.preventDefault();
18807
19398
  e.stopPropagation();
@@ -18971,6 +19562,9 @@ function OhhwellsBridge() {
18971
19562
  setHoveredItemRect(null);
18972
19563
  hoveredNavContainerRef.current = null;
18973
19564
  setHoveredNavContainerRect(null);
19565
+ siblingHintElRef.current = null;
19566
+ setSiblingHintRect(null);
19567
+ setSiblingHintRects([]);
18974
19568
  return;
18975
19569
  }
18976
19570
  {
@@ -19089,7 +19683,6 @@ function OhhwellsBridge() {
19089
19683
  hoveredNavContainerRef.current = null;
19090
19684
  setHoveredNavContainerRect(null);
19091
19685
  hoveredItemElRef.current = editable;
19092
- setHoveredItemRect(isSelectedForHoverRef.current(editable) ? null : editable.getBoundingClientRect());
19093
19686
  }
19094
19687
  }
19095
19688
  }
@@ -19386,7 +19979,7 @@ function OhhwellsBridge() {
19386
19979
  }
19387
19980
  };
19388
19981
  const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false) => {
19389
- if (linkPopoverOpenRef.current) {
19982
+ if (linkPopoverOpenRef.current || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
19390
19983
  if (hoveredImageRef.current) {
19391
19984
  hoveredImageRef.current = null;
19392
19985
  hoveredImageHasTextOverlapRef.current = false;
@@ -19751,8 +20344,7 @@ function OhhwellsBridge() {
19751
20344
  };
19752
20345
  const handleMouseMove = (e) => {
19753
20346
  const { clientX, clientY } = e;
19754
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
19755
- if (isOverEditorChrome(clientX, clientY)) {
20347
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY) || isOverEditorChrome(clientX, clientY)) {
19756
20348
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
19757
20349
  formHoverElRef.current = null;
19758
20350
  setFormHoverRect(null);
@@ -19760,6 +20352,12 @@ function OhhwellsBridge() {
19760
20352
  setHoveredItemRect(null);
19761
20353
  hoveredNavContainerRef.current = null;
19762
20354
  setHoveredNavContainerRect(null);
20355
+ siblingHintElRef.current = null;
20356
+ setSiblingHintRect(null);
20357
+ setSiblingHintRects([]);
20358
+ dismissImageHover();
20359
+ clearImageHover();
20360
+ setSectionGap(null);
19763
20361
  return;
19764
20362
  }
19765
20363
  if (probeSocialsRowAt(clientX, clientY)) return;
@@ -19771,7 +20369,11 @@ function OhhwellsBridge() {
19771
20369
  if (e.data?.type !== "ow:pointer-sync") return;
19772
20370
  const { clientX, clientY } = e.data;
19773
20371
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
19774
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
20372
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
20373
+ dismissImageHover();
20374
+ clearImageHover();
20375
+ return;
20376
+ }
19775
20377
  if (probeSocialsRowAt(clientX, clientY)) return;
19776
20378
  probeSectionGapAt(clientX, clientY);
19777
20379
  probeImageAt(clientX, clientY);
@@ -20050,6 +20652,44 @@ function OhhwellsBridge() {
20050
20652
  if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
20051
20653
  }, 400));
20052
20654
  };
20655
+ const reapCommittedAiSections = (excludeIds) => {
20656
+ const aiState = parseAiSectionsState(aiSectionsRef.current);
20657
+ if (aiState.sections.length === 0) return [];
20658
+ let orderEntries = [];
20659
+ try {
20660
+ const parsed = JSON.parse(editContentRef.current[SECTION_ORDER_KEY] ?? "[]");
20661
+ if (Array.isArray(parsed)) orderEntries = parsed;
20662
+ } catch {
20663
+ return [];
20664
+ }
20665
+ const removedIds = orderEntries.filter((entry) => entry && typeof entry.instanceId === "string" && entry.removed).map((entry) => entry.instanceId);
20666
+ if (removedIds.length === 0) return [];
20667
+ const result = reapRemovedAiSections(aiState, parseStyleStore(stylesRef.current), removedIds, excludeIds);
20668
+ if (!result.changed) return [];
20669
+ const nodes = [];
20670
+ aiSectionsRef.current = serializeAiSectionsState(result.state);
20671
+ nodes.push({ key: AI_SECTIONS_KEY, text: aiSectionsRef.current });
20672
+ const reaped = new Set(result.reapedIds);
20673
+ const nextOrderJson = JSON.stringify(orderEntries.filter((entry) => !reaped.has(entry.instanceId)));
20674
+ editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: nextOrderJson };
20675
+ setAiSectionOrder(nextOrderJson, window.location.pathname);
20676
+ nodes.push({ key: SECTION_ORDER_KEY, text: nextOrderJson });
20677
+ if (result.store) {
20678
+ stylesRef.current = JSON.stringify(result.store);
20679
+ nodes.push({ key: STYLE_STORE_KEY, text: stylesRef.current });
20680
+ }
20681
+ const nextContent = { ...editContentRef.current };
20682
+ for (const key of Object.keys(nextContent)) {
20683
+ if (key.startsWith(AI_SLOT_KEY_PREFIX) && result.slotPrefixes.some((prefix) => key.startsWith(prefix)) && nextContent[key] !== "") {
20684
+ nextContent[key] = "";
20685
+ nodes.push({ key, text: "" });
20686
+ }
20687
+ }
20688
+ editContentRef.current = nextContent;
20689
+ applyAiSectionsToDom(result.state);
20690
+ applyStylesToDom(parseStyleStore(stylesRef.current));
20691
+ return nodes;
20692
+ };
20053
20693
  const handleHydrate = (e) => {
20054
20694
  if (e.data?.type !== "ow:hydrate") return;
20055
20695
  const content = e.data.content;
@@ -20068,6 +20708,7 @@ function OhhwellsBridge() {
20068
20708
  }
20069
20709
  if (typeof content[STYLE_STORE_KEY] === "string") {
20070
20710
  stylesRef.current = content[STYLE_STORE_KEY];
20711
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
20071
20712
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
20072
20713
  }
20073
20714
  applyBrandChrome(content);
@@ -20079,11 +20720,11 @@ function OhhwellsBridge() {
20079
20720
  continue;
20080
20721
  }
20081
20722
  if (key === AI_SECTIONS_KEY) continue;
20723
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
20724
+ if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
20082
20725
  if (key === BRAND_KIT_KEY) continue;
20083
20726
  if (key === STYLE_STORE_KEY) continue;
20084
20727
  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
20728
  if (applyVideoSettingNode(key, val)) continue;
20088
20729
  if (applyCarouselNode(key, val)) continue;
20089
20730
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -20097,6 +20738,8 @@ function OhhwellsBridge() {
20097
20738
  if (video && video.src !== val) applyVideoSrc(video, val);
20098
20739
  } else if (el.dataset.ohwEditable === "link") {
20099
20740
  applyLinkHref(el, val);
20741
+ } else if (el.dataset.ohwEditable === "map") {
20742
+ applyMapQuery(el, val);
20100
20743
  } else if (el.dataset.ohwEditable === "icon") {
20101
20744
  applyIconMarkup(el, val);
20102
20745
  } else if (isIconMarkupValue(val)) {
@@ -20118,6 +20761,11 @@ function OhhwellsBridge() {
20118
20761
  reconcileFooterOrderFromContent(editContentRef.current);
20119
20762
  syncNavigationDragCursorAttrs();
20120
20763
  enforceLinkHrefs();
20764
+ const hydrateReapExclude = /* @__PURE__ */ new Set();
20765
+ const hydratePendingUndo = pendingDeleteUndoRef.current;
20766
+ if (hydratePendingUndo?.sectionInstanceId) hydrateReapExclude.add(hydratePendingUndo.sectionInstanceId);
20767
+ const reapNodes = reapCommittedAiSections(hydrateReapExclude);
20768
+ if (reapNodes.length > 0) postToParentRef.current({ type: "ow:change", nodes: reapNodes });
20121
20769
  const hydratedHeight = document.body.scrollHeight;
20122
20770
  if (hydratedHeight > 50) postToParentRef.current({ type: "ow:height", height: hydratedHeight });
20123
20771
  postToParentRef.current({ type: "ow:hydrate-done" });
@@ -20257,12 +20905,35 @@ function OhhwellsBridge() {
20257
20905
  window.addEventListener("message", handleAiSetBrand);
20258
20906
  const handleAiSetStyles = (e) => {
20259
20907
  if (e.data?.type !== "ow:ai-set-styles") return;
20260
- const value = typeof e.data.value === "string" ? e.data.value : "";
20908
+ let value = typeof e.data.value === "string" ? e.data.value : "";
20261
20909
  const previous = stylesRef.current;
20910
+ let previousSections;
20911
+ const store = parseStyleStore(value);
20912
+ if (store) {
20913
+ const folded = foldAlignIntoTrees(parseAiSectionsState(aiSectionsRef.current), store);
20914
+ if (folded.changed) {
20915
+ const nextSections = serializeAiSectionsState(folded.state);
20916
+ if (nextSections !== aiSectionsRef.current) {
20917
+ previousSections = aiSectionsRef.current;
20918
+ aiSectionsRef.current = nextSections;
20919
+ applyAiSectionsToDom(folded.state);
20920
+ postToParentRef.current({
20921
+ type: "ow:change",
20922
+ nodes: [{ key: AI_SECTIONS_KEY, text: nextSections }]
20923
+ });
20924
+ }
20925
+ value = JSON.stringify(folded.store);
20926
+ }
20927
+ }
20262
20928
  stylesRef.current = value;
20263
20929
  applyStylesToDom(parseStyleStore(value));
20264
20930
  postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
20265
- postToParentRef.current({ type: "ow:ai-styles-applied", previous, value });
20931
+ postToParentRef.current({
20932
+ type: "ow:ai-styles-applied",
20933
+ previous,
20934
+ value,
20935
+ ...previousSections !== void 0 ? { previousSections } : {}
20936
+ });
20266
20937
  };
20267
20938
  window.addEventListener("message", handleAiSetStyles);
20268
20939
  const handleGetBrand = (e) => {
@@ -20279,8 +20950,11 @@ function OhhwellsBridge() {
20279
20950
  if (!instanceId || !direction) return;
20280
20951
  const entries = moveSectionInstance(instanceId, direction, window.location.pathname);
20281
20952
  if (!entries) return;
20282
- const orderJson = JSON.stringify(entries);
20953
+ const orderJson = JSON.stringify(
20954
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, entries)
20955
+ );
20283
20956
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20957
+ applyPersistedOrder(getPageSectionOrderEntries(orderJson, window.location.pathname));
20284
20958
  setAiSectionOrder(orderJson, window.location.pathname);
20285
20959
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
20286
20960
  window.dispatchEvent(new Event("resize"));
@@ -20326,8 +21000,11 @@ function OhhwellsBridge() {
20326
21000
  const currentEntries = getPageSectionOrderEntries(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname);
20327
21001
  const entries = deleteSectionInstance(instanceId, window.location.pathname, currentEntries);
20328
21002
  if (!entries) return;
20329
- const orderJson = JSON.stringify(entries);
21003
+ const orderJson = JSON.stringify(
21004
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, entries)
21005
+ );
20330
21006
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
21007
+ setAiSectionOrder(orderJson, window.location.pathname);
20331
21008
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
20332
21009
  aiSectionApiRef.current?.clear();
20333
21010
  window.dispatchEvent(new Event("resize"));
@@ -20336,6 +21013,7 @@ function OhhwellsBridge() {
20336
21013
  const actionId = newInstanceId();
20337
21014
  pendingDeleteUndoRef.current = {
20338
21015
  actionId,
21016
+ sectionInstanceId: instanceId,
20339
21017
  restore: () => {
20340
21018
  const restoredEntries = getPageSectionOrderEntries(
20341
21019
  editContentRef.current[SECTION_ORDER_KEY],
@@ -20343,8 +21021,11 @@ function OhhwellsBridge() {
20343
21021
  );
20344
21022
  const restored = restoreSectionInstance(instanceId, window.location.pathname, restoredEntries);
20345
21023
  if (!restored) return;
20346
- const restoredJson = JSON.stringify(restored);
21024
+ const restoredJson = JSON.stringify(
21025
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, restored)
21026
+ );
20347
21027
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: restoredJson };
21028
+ setAiSectionOrder(restoredJson, window.location.pathname);
20348
21029
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: restoredJson }] });
20349
21030
  window.dispatchEvent(new Event("resize"));
20350
21031
  const restoreHeight = document.body.scrollHeight;
@@ -20370,7 +21051,9 @@ function OhhwellsBridge() {
20370
21051
  const result = duplicateSectionInstance(instanceId, newId, window.location.pathname, currentEntries);
20371
21052
  if (!result) return;
20372
21053
  const { entries, keyRekeys } = result;
20373
- const orderJson = JSON.stringify(entries);
21054
+ const orderJson = JSON.stringify(
21055
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, entries)
21056
+ );
20374
21057
  const nodes = [{ key: SECTION_ORDER_KEY, text: orderJson }];
20375
21058
  for (const { from, to } of keyRekeys) {
20376
21059
  const inherited = editContentRef.current[from];
@@ -20380,6 +21063,7 @@ function OhhwellsBridge() {
20380
21063
  ...editContentRef.current,
20381
21064
  ...Object.fromEntries(nodes.map(({ key, text }) => [key, text]))
20382
21065
  };
21066
+ applyPersistedOrder(getPageSectionOrderEntries(orderJson, window.location.pathname));
20383
21067
  setAiSectionOrder(orderJson, window.location.pathname);
20384
21068
  postToParentRef.current({ type: "ow:change", nodes });
20385
21069
  window.dispatchEvent(new Event("resize"));
@@ -20398,6 +21082,12 @@ function OhhwellsBridge() {
20398
21082
  closeLinkPopoverRef.current();
20399
21083
  return;
20400
21084
  }
21085
+ if (floatingPanelOpenRef.current) {
21086
+ setFloatingPanelRef.current(null);
21087
+ deselectRef.current();
21088
+ deactivateRef.current();
21089
+ return;
21090
+ }
20401
21091
  deselectRef.current();
20402
21092
  deactivateRef.current();
20403
21093
  clearMediaSelectionRef.current();
@@ -20643,6 +21333,10 @@ function OhhwellsBridge() {
20643
21333
  };
20644
21334
  const handleSave = (e) => {
20645
21335
  if (e.data?.type !== "ow:save") return;
21336
+ const pendingUndo = pendingDeleteUndoRef.current;
21337
+ const reapExclude = /* @__PURE__ */ new Set();
21338
+ if (pendingUndo?.sectionInstanceId) reapExclude.add(pendingUndo.sectionInstanceId);
21339
+ const reapNodes = reapCommittedAiSections(reapExclude);
20646
21340
  const nodes = collectEditableNodes(editContentRef.current);
20647
21341
  const tracker = document.querySelector("[data-ohw-sections-tracker]");
20648
21342
  if (tracker?.textContent) nodes.push({ key: "__ohw_sections", type: "sections", text: tracker.textContent });
@@ -20662,6 +21356,11 @@ function OhhwellsBridge() {
20662
21356
  const text = specs.length ? JSON.stringify(specs) : editContentRef.current[fieldsKey(formKey)];
20663
21357
  if (text) nodes.push({ key: fieldsKey(formKey), type: "text", text });
20664
21358
  });
21359
+ for (const reapNode of reapNodes) {
21360
+ if (reapNode.key.startsWith(AI_SLOT_KEY_PREFIX) && !nodes.some((n) => n.key === reapNode.key)) {
21361
+ nodes.push({ key: reapNode.key, type: "text", text: reapNode.text });
21362
+ }
21363
+ }
20665
21364
  postToParentRef.current({ type: "ow:save-result", nodes });
20666
21365
  };
20667
21366
  const handleInsertSection = (e) => {
@@ -20672,8 +21371,12 @@ function OhhwellsBridge() {
20672
21371
  if (inserted) {
20673
21372
  const tracker = getSectionsTracker();
20674
21373
  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 });
21374
+ const reportHeight = () => {
21375
+ const h = document.body.scrollHeight;
21376
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
21377
+ };
21378
+ reportHeight();
21379
+ setTimeout(reportHeight, 500);
20677
21380
  }
20678
21381
  };
20679
21382
  const handleSwitchSchedule = (e) => {
@@ -21077,10 +21780,10 @@ function OhhwellsBridge() {
21077
21780
  window.removeEventListener("message", handleDeleteSection);
21078
21781
  window.removeEventListener("message", handleDuplicateSection);
21079
21782
  window.removeEventListener("message", handleDeactivate);
21080
- document.documentElement.removeAttribute("data-ohw-panel-dragging");
21081
21783
  window.removeEventListener("message", handleToastAction);
21082
21784
  window.removeEventListener("message", handleFormCount);
21083
21785
  window.removeEventListener("message", handleUiEscape);
21786
+ document.documentElement.removeAttribute("data-ohw-panel-dragging");
21084
21787
  autoSaveTimers.current.forEach(clearTimeout);
21085
21788
  autoSaveTimers.current.clear();
21086
21789
  if (imageUnhoverTimerRef.current) clearTimeout(imageUnhoverTimerRef.current);
@@ -21283,7 +21986,7 @@ function OhhwellsBridge() {
21283
21986
  postToParent2({
21284
21987
  type: "ow:ready",
21285
21988
  version: "1",
21286
- bridgeVersion: "0.1.90",
21989
+ bridgeVersion: "0.1.92",
21287
21990
  path: pathname,
21288
21991
  nodes: collectEditableNodes(editContentRef.current),
21289
21992
  sections
@@ -22202,6 +22905,59 @@ function OhhwellsBridge() {
22202
22905
  ) : null
22203
22906
  ] });
22204
22907
  }
22908
+
22909
+ // src/ui/EmptySection.tsx
22910
+ import Link3 from "next/link";
22911
+ import { Fragment as Fragment9, jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
22912
+ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey }) {
22913
+ return /* @__PURE__ */ jsxs21(Fragment9, { children: [
22914
+ /* @__PURE__ */ jsx34(
22915
+ "p",
22916
+ {
22917
+ style: {
22918
+ fontFamily: "var(--brand-font-body)",
22919
+ fontSize: "0.75rem",
22920
+ fontWeight: 500,
22921
+ letterSpacing: "0.15em",
22922
+ textTransform: "uppercase",
22923
+ color: "var(--brand-accent)",
22924
+ marginBottom: "1.5rem"
22925
+ },
22926
+ children: /* @__PURE__ */ jsx34(Link3, { href: homeHref, style: { color: "inherit" }, children: /* @__PURE__ */ jsx34("span", { ...eyebrowKey ? { "data-ohw-editable": "text", "data-ohw-key": eyebrowKey } : {}, children: "Home" }) })
22927
+ }
22928
+ ),
22929
+ /* @__PURE__ */ jsx34(
22930
+ "h1",
22931
+ {
22932
+ style: {
22933
+ fontFamily: "var(--brand-font-heading)",
22934
+ fontSize: "clamp(2rem, 3.5vw, 2.75rem)",
22935
+ lineHeight: 1.1,
22936
+ letterSpacing: "-0.025em",
22937
+ color: "var(--brand-text)",
22938
+ marginBottom: "1rem"
22939
+ },
22940
+ ...titleKey ? { "data-ohw-editable": "text", "data-ohw-key": titleKey, "data-ohw-max-length": 80 } : {},
22941
+ children: title
22942
+ }
22943
+ ),
22944
+ /* @__PURE__ */ jsx34(
22945
+ "p",
22946
+ {
22947
+ style: {
22948
+ fontFamily: "var(--brand-font-body)",
22949
+ fontSize: "1rem",
22950
+ lineHeight: 1.7,
22951
+ fontWeight: 300,
22952
+ color: "var(--brand-text-muted)",
22953
+ maxWidth: "340px"
22954
+ },
22955
+ ...subtitleKey ? { "data-ohw-editable": "text", "data-ohw-key": subtitleKey, "data-ohw-max-length": 160 } : {},
22956
+ children: "This page doesn't have any content yet."
22957
+ }
22958
+ )
22959
+ ] });
22960
+ }
22205
22961
  export {
22206
22962
  AI_DEFAULT_BRAND,
22207
22963
  AI_TREE_SCHEMA_VERSIONS,
@@ -22218,6 +22974,7 @@ export {
22218
22974
  DropdownMenuItem,
22219
22975
  DropdownMenuSeparator,
22220
22976
  DropdownMenuTrigger,
22977
+ EmptySection,
22221
22978
  ItemActionToolbar,
22222
22979
  ItemInteractionLayer,
22223
22980
  LinkEditorPanel,