@ohhwells/bridge 0.1.86 → 0.1.87-next.261

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.cjs CHANGED
@@ -46,6 +46,7 @@ __export(index_exports, {
46
46
  DropdownMenuItem: () => DropdownMenuItem,
47
47
  DropdownMenuSeparator: () => DropdownMenuSeparator,
48
48
  DropdownMenuTrigger: () => DropdownMenuTrigger,
49
+ EmptySection: () => EmptySection,
49
50
  ItemActionToolbar: () => ItemActionToolbar,
50
51
  ItemInteractionLayer: () => ItemInteractionLayer,
51
52
  LinkEditorPanel: () => LinkEditorPanel,
@@ -142,6 +143,10 @@ function isRenderableTree(value) {
142
143
 
143
144
  // src/lib/ai-sections-store.ts
144
145
  var AI_SECTIONS_KEY = "__ohw_ai_sections";
146
+ var AI_SLOT_KEY_PREFIX = "ai.";
147
+ function aiSlotKeyPrefixFor(sectionId) {
148
+ return `${AI_SLOT_KEY_PREFIX}${sectionId}.`;
149
+ }
145
150
  var EMPTY_AI_SECTIONS = { v: 1, sections: [] };
146
151
  function parseAiSectionsState(raw) {
147
152
  if (!raw) return EMPTY_AI_SECTIONS;
@@ -185,6 +190,63 @@ function applyTreeToState(state, payload) {
185
190
  const others = state.sections.filter((existing) => existing.id !== entry.id);
186
191
  return { ...state, v: 1, sections: [...others, entry] };
187
192
  }
193
+ function foldAlignIntoTrees(state, store) {
194
+ const byId = new Map(state.sections.map((entry) => [entry.id, entry]));
195
+ const nextTrees = /* @__PURE__ */ new Map();
196
+ const treeFor = (id) => {
197
+ const cloned = nextTrees.get(id);
198
+ if (cloned) return cloned;
199
+ const entry = byId.get(id);
200
+ if (!entry) return void 0;
201
+ const fresh = {
202
+ ...entry.tree,
203
+ rows: entry.tree.rows.map((row) => ({ ...row, blocks: row.blocks.map((block) => ({ ...block })) }))
204
+ };
205
+ nextTrees.set(id, fresh);
206
+ return fresh;
207
+ };
208
+ const nodes = {};
209
+ for (const [key, override] of Object.entries(store.nodes)) {
210
+ const match = override.align !== void 0 && key.startsWith(AI_SLOT_KEY_PREFIX) ? key.match(/^ai\.(.+?)\.r(\d+)\.b(\d+)(?:\.|$)/) : null;
211
+ const tree = match ? treeFor(match[1]) : void 0;
212
+ const block = match && tree ? tree.rows[Number(match[2])]?.blocks[Number(match[3])] : void 0;
213
+ if (!block) {
214
+ nodes[key] = override;
215
+ continue;
216
+ }
217
+ block.align = override.align;
218
+ const rest = { ...override };
219
+ delete rest.align;
220
+ if (Object.keys(rest).length > 0) nodes[key] = rest;
221
+ }
222
+ const sections = {};
223
+ for (const [sectionId, override] of Object.entries(store.sections)) {
224
+ const tree = override.align !== void 0 ? treeFor(sectionId) : void 0;
225
+ if (!tree) {
226
+ sections[sectionId] = override;
227
+ continue;
228
+ }
229
+ for (const row of tree.rows) {
230
+ for (const block of row.blocks) block.align = override.align;
231
+ }
232
+ const rest = { ...override };
233
+ delete rest.align;
234
+ if (Object.keys(rest).length > 0) sections[sectionId] = rest;
235
+ }
236
+ if (nextTrees.size === 0) return { state, store, changed: false };
237
+ return {
238
+ state: {
239
+ ...state,
240
+ v: 1,
241
+ sections: state.sections.map((entry) => {
242
+ const tree = nextTrees.get(entry.id);
243
+ return tree ? { ...entry, tree } : entry;
244
+ })
245
+ },
246
+ store: { v: 1, sections, nodes },
247
+ changed: true
248
+ };
249
+ }
188
250
  function removeFromState(state, id) {
189
251
  return { ...state, v: 1, sections: state.sections.filter((entry) => entry.id !== id) };
190
252
  }
@@ -196,6 +258,33 @@ function deleteSectionFromState(state, sectionId) {
196
258
  if (removed.includes(sectionId)) return state;
197
259
  return { ...state, v: 1, sections: state.sections, removed: [...removed, sectionId] };
198
260
  }
261
+ function reapRemovedAiSections(state, store, removedIds, excludeIds = /* @__PURE__ */ new Set()) {
262
+ const generated = new Set(state.sections.map((entry) => entry.id));
263
+ const reapedIds = [...new Set(removedIds)].filter((id) => generated.has(id) && !excludeIds.has(id));
264
+ if (reapedIds.length === 0) {
265
+ return { state, store, reapedIds: [], slotPrefixes: [], changed: false };
266
+ }
267
+ const reaped = new Set(reapedIds);
268
+ const slotPrefixes = reapedIds.map(aiSlotKeyPrefixFor);
269
+ const nextState = {
270
+ ...state,
271
+ v: 1,
272
+ sections: state.sections.filter((entry) => !reaped.has(entry.id))
273
+ };
274
+ let nextStore = store;
275
+ if (store) {
276
+ const sections = {};
277
+ for (const [key, override] of Object.entries(store.sections)) {
278
+ if (!reaped.has(key)) sections[key] = override;
279
+ }
280
+ const nodes = {};
281
+ for (const [key, override] of Object.entries(store.nodes)) {
282
+ if (!slotPrefixes.some((prefix) => key.startsWith(prefix))) nodes[key] = override;
283
+ }
284
+ nextStore = { v: 1, sections, nodes };
285
+ }
286
+ return { state: nextState, store: nextStore, reapedIds, slotPrefixes, changed: true };
287
+ }
199
288
 
200
289
  // src/lib/brand-chrome.ts
201
290
  var BRAND_NAME_KEY = "__ohw_brand_name";
@@ -403,6 +492,12 @@ function styleSheetCss() {
403
492
  `[data-ohw-style-spacing="${spacing}"] { padding-top: ${px}px !important; padding-bottom: ${px}px !important; }`
404
493
  );
405
494
  }
495
+ rules.push(
496
+ `[data-ohw-style-corners="sharp"] :is(.card, [data-ohw-card]) { border-radius: 0 !important; }`
497
+ );
498
+ for (const align of ["left", "center", "right"]) {
499
+ rules.push(`[data-ohw-style-align="${align}"] { text-align: ${align} !important; }`);
500
+ }
406
501
  return rules.join("\n");
407
502
  }
408
503
  var STYLE_FONT_LINK_ID = "ohw-style-fonts";
@@ -429,10 +524,25 @@ var SECTION_ATTRS = {
429
524
  textDistribution: "data-ohw-style-distribution",
430
525
  headlineScale: "data-ohw-style-headline",
431
526
  imageAspect: "data-ohw-style-aspect",
432
- spacing: "data-ohw-style-spacing"
527
+ spacing: "data-ohw-style-spacing",
528
+ cornerStyle: "data-ohw-style-corners",
529
+ align: "data-ohw-style-align"
433
530
  };
434
531
  var NODE_WROTE_ATTR = "data-ohw-style-node";
435
- var NODE_PROPS = ["color", "font-family", "font-size", "background"];
532
+ var NODE_PROPS = [
533
+ "color",
534
+ "font-family",
535
+ "font-size",
536
+ "background",
537
+ "text-align",
538
+ "justify-content",
539
+ "align-items"
540
+ ];
541
+ var ALIGN_JUSTIFY = {
542
+ left: "flex-start",
543
+ center: "center",
544
+ right: "flex-end"
545
+ };
436
546
  function saveInline(el, prop) {
437
547
  const attr = `data-ohw-style-prev-${prop}`;
438
548
  if (el.hasAttribute(attr)) return;
@@ -476,6 +586,10 @@ function clearNodeProps(root) {
476
586
  function buttonSurfaceOf(el) {
477
587
  return el.closest("a, button") ?? el;
478
588
  }
589
+ function alignSubjectOf(el) {
590
+ const button = el.closest('[data-ohw-role="button"]');
591
+ return button?.parentElement ?? el;
592
+ }
479
593
  function applyStylesToDom(store) {
480
594
  ensureStyleSheet();
481
595
  clearSectionAttrs(document);
@@ -521,6 +635,18 @@ function applyStylesToDom(store) {
521
635
  el.style.setProperty("font-size", `${override.fontSize}px`, "important");
522
636
  el.setAttribute(NODE_WROTE_ATTR, "");
523
637
  }
638
+ if (override.align !== void 0) {
639
+ const subject = alignSubjectOf(el);
640
+ saveInline(subject, "text-align");
641
+ saveInline(subject, "justify-content");
642
+ subject.style.setProperty("text-align", override.align, "important");
643
+ subject.style.setProperty(
644
+ "justify-content",
645
+ ALIGN_JUSTIFY[override.align] ?? "flex-start",
646
+ "important"
647
+ );
648
+ subject.setAttribute(NODE_WROTE_ATTR, "");
649
+ }
524
650
  if (override.buttonBackground !== void 0 || override.buttonText !== void 0) {
525
651
  const surface = buttonSurfaceOf(el);
526
652
  if (override.buttonBackground !== void 0) {
@@ -541,9 +667,510 @@ function applyStylesToDom(store) {
541
667
  var import_react_dom = require("react-dom");
542
668
  var import_client = require("react-dom/client");
543
669
 
670
+ // src/lib/sections.ts
671
+ var LINK_PICKER_EXCLUDED_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
672
+ function isChromeSection(el) {
673
+ return el.matches("header, nav, footer, aside");
674
+ }
675
+ function titleCaseSectionId(id) {
676
+ return id.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
677
+ }
678
+ function parseSectionsFromRoot(root) {
679
+ const seen = /* @__PURE__ */ new Set();
680
+ const sections = [];
681
+ for (const el of root.querySelectorAll("[data-ohw-section]")) {
682
+ const id = el.getAttribute("data-ohw-section") ?? "";
683
+ if (!id || seen.has(id) || LINK_PICKER_EXCLUDED_IDS.has(id)) continue;
684
+ if (el.parentElement?.closest("[data-ohw-section]")) continue;
685
+ if (el.hasAttribute("data-ohw-ai-template-hidden") || el.hasAttribute("data-ohw-ai-removed") || el.hasAttribute("data-ohw-ai-replaced-by"))
686
+ continue;
687
+ seen.add(id);
688
+ const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
689
+ sections.push({ id, label });
690
+ }
691
+ return sections;
692
+ }
693
+ function collectSectionsFromDom() {
694
+ if (typeof document === "undefined") return [];
695
+ return parseSectionsFromRoot(document);
696
+ }
697
+ function parseSectionsFromHtml(html) {
698
+ const doc = new DOMParser().parseFromString(html, "text/html");
699
+ return parseSectionsFromRoot(doc);
700
+ }
701
+
702
+ // src/lib/section-instances.ts
703
+ var SECTION_ORDER_KEY = "__ohw_section_order";
704
+ var REMOVED_ATTR = "data-ohw-section-removed";
705
+ function isRemovedSection(el) {
706
+ return el.hasAttribute(REMOVED_ATTR);
707
+ }
708
+ function topLevelSections() {
709
+ return Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
710
+ (el) => !el.parentElement?.closest("[data-ohw-section]") && !isChromeSection(el) && !isRemovedSection(el)
711
+ );
712
+ }
713
+ function instanceIdOf(el) {
714
+ return el.dataset.ohwInstance ?? el.dataset.ohwSection ?? "";
715
+ }
716
+ function findByInstanceId(instanceId) {
717
+ const escapedId = CSS.escape(instanceId);
718
+ return document.querySelector(`[data-ohw-instance="${escapedId}"]`) ?? document.querySelector(`[data-ohw-section="${escapedId}"]:not([data-ohw-instance])`);
719
+ }
720
+ function planSectionMove(instanceId, targetIndex, currentPath) {
721
+ const sections = topLevelSections();
722
+ const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
723
+ if (index === -1) return null;
724
+ const dragged = sections[index];
725
+ const others = sections.filter((_, i) => i !== index);
726
+ const clamped = Math.max(0, Math.min(targetIndex, others.length));
727
+ const reordered = [...others.slice(0, clamped), dragged, ...others.slice(clamped)];
728
+ return reordered.map((el, order) => ({
729
+ instanceId: instanceIdOf(el),
730
+ type: el.getAttribute("data-ohw-section") ?? "",
731
+ order,
732
+ pagePath: currentPath
733
+ }));
734
+ }
735
+ function moveSectionInstance(instanceId, direction, currentPath) {
736
+ const sections = topLevelSections();
737
+ const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
738
+ if (index === -1) return null;
739
+ const siblingIndex = direction === "up" ? index - 1 : index + 1;
740
+ if (siblingIndex < 0 || siblingIndex >= sections.length) return null;
741
+ const entries = planSectionMove(instanceId, siblingIndex, currentPath);
742
+ if (!entries) return null;
743
+ applyPersistedOrder(entries);
744
+ return entries;
745
+ }
746
+ function syncRemovedFlags(entries) {
747
+ const removedIds = new Set(entries.filter((e) => e.removed).map((e) => e.instanceId));
748
+ document.querySelectorAll(`[${REMOVED_ATTR}]`).forEach((el) => {
749
+ if (!removedIds.has(instanceIdOf(el))) {
750
+ el.style.removeProperty("display");
751
+ el.removeAttribute(REMOVED_ATTR);
752
+ }
753
+ });
754
+ for (const id of removedIds) {
755
+ const el = findByInstanceId(id);
756
+ if (el) {
757
+ el.style.display = "none";
758
+ el.setAttribute(REMOVED_ATTR, "");
759
+ }
760
+ }
761
+ }
762
+ function applyPersistedOrder(entries) {
763
+ syncRemovedFlags(entries);
764
+ if (entries.length === 0) return;
765
+ const sections = topLevelSections();
766
+ if (sections.length === 0) return;
767
+ const orderIndex = new Map(entries.map((e) => [e.instanceId, e.order]));
768
+ const ordered = [...sections].sort((a, b) => {
769
+ const aOrder = orderIndex.get(instanceIdOf(a));
770
+ const bOrder = orderIndex.get(instanceIdOf(b));
771
+ if (aOrder === void 0 && bOrder === void 0) return 0;
772
+ if (aOrder === void 0) return 1;
773
+ if (bOrder === void 0) return -1;
774
+ return aOrder - bOrder;
775
+ });
776
+ let prev = null;
777
+ for (const el of ordered) {
778
+ if (prev) prev.after(el);
779
+ prev = el;
780
+ }
781
+ }
782
+ function setSectionRemoved(instanceId, currentPath, existingEntries, removed) {
783
+ if (!findByInstanceId(instanceId)) return null;
784
+ const byId = new Map(existingEntries.map((e) => [e.instanceId, e]));
785
+ const allSections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
786
+ (el) => !el.parentElement?.closest("[data-ohw-section]") && !isChromeSection(el)
787
+ );
788
+ allSections.forEach((el, order) => {
789
+ const id = instanceIdOf(el);
790
+ if (!byId.has(id)) {
791
+ byId.set(id, { instanceId: id, type: el.getAttribute("data-ohw-section") ?? "", order, pagePath: currentPath });
792
+ }
793
+ });
794
+ const target = byId.get(instanceId);
795
+ if (!target) return null;
796
+ byId.set(instanceId, { ...target, removed });
797
+ const entries = Array.from(byId.values());
798
+ applyPersistedOrder(entries);
799
+ return entries;
800
+ }
801
+ function deleteSectionInstance(instanceId, currentPath, existingEntries) {
802
+ return setSectionRemoved(instanceId, currentPath, existingEntries, true);
803
+ }
804
+ function restoreSectionInstance(instanceId, currentPath, existingEntries) {
805
+ return setSectionRemoved(instanceId, currentPath, existingEntries, false);
806
+ }
807
+ function duplicateSectionInstance(instanceId, newId, currentPath, existingEntries) {
808
+ const original = findByInstanceId(instanceId);
809
+ if (!original) return null;
810
+ const clone = original.cloneNode(true);
811
+ clone.setAttribute("data-ohw-instance", newId);
812
+ const keyRekeys = rekeySectionSubtree(clone, newId);
813
+ original.insertAdjacentElement("afterend", clone);
814
+ const byId = new Map(existingEntries.map((e) => [e.instanceId, e]));
815
+ const entries = topLevelSections().map((el, order) => {
816
+ const id = instanceIdOf(el);
817
+ return {
818
+ instanceId: id,
819
+ type: el.getAttribute("data-ohw-section") ?? "",
820
+ order,
821
+ pagePath: currentPath,
822
+ ...byId.get(id)?.removed ? { removed: true } : {}
823
+ };
824
+ });
825
+ applyPersistedOrder(entries);
826
+ return { entries, keyRekeys };
827
+ }
828
+ function newInstanceId() {
829
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `instance-${Date.now()}-${Math.random().toString(36).slice(2)}`;
830
+ }
831
+ function getPageSectionOrderEntries(raw, currentPath) {
832
+ if (!raw) return [];
833
+ try {
834
+ const entries = JSON.parse(raw);
835
+ return entries.filter((e) => !e.pagePath || e.pagePath === currentPath);
836
+ } catch {
837
+ return [];
838
+ }
839
+ }
840
+ function mergePageSectionOrder(raw, currentPath, pageEntries) {
841
+ let all = [];
842
+ if (raw) {
843
+ try {
844
+ const parsed = JSON.parse(raw);
845
+ if (Array.isArray(parsed)) all = parsed;
846
+ } catch {
847
+ }
848
+ }
849
+ const otherPages = all.filter((e) => e && e.pagePath && e.pagePath !== currentPath);
850
+ const pageIds = new Set(pageEntries.map((e) => e.instanceId));
851
+ const removedHere = all.filter(
852
+ (e) => e && (!e.pagePath || e.pagePath === currentPath) && e.removed && !pageIds.has(e.instanceId)
853
+ );
854
+ return [...otherPages, ...removedHere, ...pageEntries];
855
+ }
856
+ function rekeySectionSubtree(root, instanceId) {
857
+ const suffix = `::${instanceId}`;
858
+ const pairs = [];
859
+ const rekey = (el, attr) => {
860
+ const current = el.getAttribute(attr);
861
+ if (!current) return;
862
+ const base = current.includes("::") ? current.slice(0, current.indexOf("::")) : current;
863
+ const next = `${base}${suffix}`;
864
+ el.setAttribute(attr, next);
865
+ pairs.push({ from: current, to: next });
866
+ };
867
+ if (root.hasAttribute("data-ohw-key")) rekey(root, "data-ohw-key");
868
+ if (root.hasAttribute("data-ohw-href-key")) rekey(root, "data-ohw-href-key");
869
+ root.querySelectorAll("[data-ohw-key]").forEach((el) => rekey(el, "data-ohw-key"));
870
+ root.querySelectorAll("[data-ohw-href-key]").forEach((el) => rekey(el, "data-ohw-href-key"));
871
+ return pairs;
872
+ }
873
+ function initSectionInstancesFromContent(content, currentPath) {
874
+ document.querySelectorAll("[data-ohw-section]:not([data-ohw-instance])").forEach((el) => {
875
+ el.setAttribute("data-ohw-instance", el.getAttribute("data-ohw-section") ?? "");
876
+ });
877
+ const entries = getPageSectionOrderEntries(content[SECTION_ORDER_KEY], currentPath);
878
+ for (const entry of entries) {
879
+ if (entry.instanceId === entry.type) continue;
880
+ if (document.querySelector(`[data-ohw-instance="${CSS.escape(entry.instanceId)}"]`)) continue;
881
+ const original = document.querySelector(
882
+ `[data-ohw-section="${CSS.escape(entry.type)}"][data-ohw-instance="${CSS.escape(entry.type)}"]`
883
+ );
884
+ if (!original) continue;
885
+ const clone = original.cloneNode(true);
886
+ clone.setAttribute("data-ohw-instance", entry.instanceId);
887
+ rekeySectionSubtree(clone, entry.instanceId);
888
+ original.insertAdjacentElement("afterend", clone);
889
+ }
890
+ applyPersistedOrder(entries);
891
+ }
892
+
544
893
  // src/ui/ai-tree/AiTreeRenderer.tsx
545
894
  var import_react = __toESM(require("react"), 1);
546
895
  var import_lucide_react = require("lucide-react");
896
+
897
+ // src/lib/placeholder-imagery.ts
898
+ var U = (id) => `https://images.unsplash.com/photo-${id}?auto=format&fit=crop&w=1600&q=80`;
899
+ var GENERIC = [
900
+ U("1441986300917-64674bd600d8"),
901
+ U("1486406146926-c627a92ad1ab"),
902
+ U("1497032628192-86f99bcd76bc"),
903
+ U("1521737604893-d14cc237f11d"),
904
+ U("1522071820081-009f0129c71c"),
905
+ U("1519389950473-47ba0277781c"),
906
+ U("1460925895917-afdab827c52f"),
907
+ U("1504384308090-c894fdcc538d")
908
+ ];
909
+ var PEOPLE = [
910
+ U("1500648767791-00dcc994a43e"),
911
+ U("1494790108377-be9c29b29330"),
912
+ U("1507003211169-0a1dd7228f2d"),
913
+ U("1438761681033-6461ffad8d80"),
914
+ U("1544005313-94ddf0286df2"),
915
+ U("1472099645785-5658abf4ff4e"),
916
+ U("1519085360753-af0119f7cbe7"),
917
+ U("1534528741775-53994a69daeb")
918
+ ];
919
+ var THEMED = [
920
+ {
921
+ keywords: ["portrait", "headshot", "person", "people", "team", "staff", "avatar", "founder", "face"],
922
+ pool: PEOPLE
923
+ },
924
+ {
925
+ keywords: ["pet", "dog", "cat", "puppy", "kitten", "vet", "animal"],
926
+ pool: [
927
+ U("1548199973-03cce0bbc87b"),
928
+ U("1450778869180-41d0601e046e"),
929
+ U("1583511655857-d19b40a7a54e"),
930
+ U("1587300003388-59208cc962cb"),
931
+ U("1517849845537-4d257902454a"),
932
+ U("1601758228041-f3b2795255f1")
933
+ ]
934
+ },
935
+ {
936
+ keywords: [
937
+ "baker",
938
+ "bakery",
939
+ "cafe",
940
+ "coffee",
941
+ "latte",
942
+ "restaurant",
943
+ "pastr",
944
+ "bread",
945
+ "cake",
946
+ "cater",
947
+ "chef",
948
+ "kitchen",
949
+ "food",
950
+ "pizza",
951
+ "dessert",
952
+ "brunch",
953
+ "bistro",
954
+ "deli",
955
+ "dish",
956
+ "menu"
957
+ ],
958
+ pool: [
959
+ U("1509440159596-0249088772ff"),
960
+ U("1555507036-ab1f4038808a"),
961
+ U("1517433670267-08bbd4be890f"),
962
+ U("1486427944299-d1955d23e34d"),
963
+ U("1504754524776-8f4f37790ca0"),
964
+ U("1495474472287-4d71bcdd2085"),
965
+ U("1521017432531-fbd92d768814"),
966
+ U("1556909114-f6e7ad7d3136")
967
+ ]
968
+ },
969
+ {
970
+ keywords: [
971
+ "shop",
972
+ "store",
973
+ "boutique",
974
+ "retail",
975
+ "clothing",
976
+ "fashion",
977
+ "jewel",
978
+ "gift",
979
+ "florist",
980
+ "market",
981
+ "grocer",
982
+ "product",
983
+ "storefront"
984
+ ],
985
+ pool: [
986
+ U("1441984904996-e0b6ba687e04"),
987
+ U("1472851294608-062f824d29cc"),
988
+ U("1523381210434-271e8be1f52b"),
989
+ U("1534452203293-494d7ddbf7e0"),
990
+ U("1445205170230-053b83016050"),
991
+ U("1560243563-062bfc001d68")
992
+ ]
993
+ },
994
+ {
995
+ keywords: [
996
+ "yoga",
997
+ "pilates",
998
+ "fitness",
999
+ "gym",
1000
+ "workout",
1001
+ "trainer",
1002
+ "wellness",
1003
+ "meditat",
1004
+ "massage",
1005
+ "therap",
1006
+ "physio",
1007
+ "chiro",
1008
+ "nutrition",
1009
+ "spa",
1010
+ "studio"
1011
+ ],
1012
+ pool: [
1013
+ U("1544367567-0f2fcb009e0b"),
1014
+ U("1506126613408-eca07ce68773"),
1015
+ U("1545205597-3d9d02c29597"),
1016
+ U("1552196563-55cd4e45efb3"),
1017
+ U("1518611012118-696072aa579a"),
1018
+ U("1571019613454-1cb2f99b2d8b"),
1019
+ U("1540555700478-4be289fbecef"),
1020
+ U("1519824145371-296894a0daa9")
1021
+ ]
1022
+ },
1023
+ {
1024
+ keywords: [
1025
+ "salon",
1026
+ "hairdress",
1027
+ "haircut",
1028
+ "barber",
1029
+ "manicure",
1030
+ "pedicure",
1031
+ "nails",
1032
+ "beauty",
1033
+ "makeup",
1034
+ "cosmetic",
1035
+ "eyelash",
1036
+ "eyebrow",
1037
+ "skincare",
1038
+ "esthetic",
1039
+ "waxing",
1040
+ "hair"
1041
+ ],
1042
+ pool: [
1043
+ U("1560066984-138dadb4c035"),
1044
+ U("1522337660859-02fbefca4702"),
1045
+ U("1562322140-8baeececf3df"),
1046
+ U("1521590832167-7bcbfaa6381f"),
1047
+ U("1487412947147-5cebf100ffc2"),
1048
+ U("1526045478516-99145907023c")
1049
+ ]
1050
+ },
1051
+ {
1052
+ keywords: [
1053
+ "cleaning",
1054
+ "plumb",
1055
+ "electric",
1056
+ "landscap",
1057
+ "contractor",
1058
+ "handyman",
1059
+ "renov",
1060
+ "hvac",
1061
+ "roofing",
1062
+ "painting",
1063
+ "carpentry",
1064
+ "flooring",
1065
+ "movers",
1066
+ "construction",
1067
+ "tools"
1068
+ ],
1069
+ pool: [
1070
+ U("1581578731548-c64695cc6952"),
1071
+ U("1504307651254-35680f356dfd"),
1072
+ U("1581092160562-40aa08e78837"),
1073
+ U("1621905251189-08b45d6a269e"),
1074
+ U("1558618666-fcd25c85cd64"),
1075
+ U("1585128792020-803d29415281")
1076
+ ]
1077
+ },
1078
+ {
1079
+ keywords: [
1080
+ "legal",
1081
+ "attorney",
1082
+ "lawyer",
1083
+ "account",
1084
+ "bookkeep",
1085
+ "consult",
1086
+ "coaching",
1087
+ "financ",
1088
+ "insurance",
1089
+ "realtor",
1090
+ "estate",
1091
+ "marketing",
1092
+ "agency",
1093
+ "office",
1094
+ "business",
1095
+ "desk"
1096
+ ],
1097
+ pool: [
1098
+ U("1497366216548-37526070297c"),
1099
+ U("1497366811353-6870744d04b2"),
1100
+ U("1454165804606-c3d57bc86b40"),
1101
+ U("1521791136064-7986c2920216"),
1102
+ U("1556761175-b413da4baf72"),
1103
+ U("1542744173-8e7e53415bb0")
1104
+ ]
1105
+ },
1106
+ {
1107
+ keywords: [
1108
+ "wedding",
1109
+ "event",
1110
+ "party",
1111
+ "celebrat",
1112
+ "venue",
1113
+ "community",
1114
+ "nonprofit",
1115
+ "charity",
1116
+ "workshop",
1117
+ "photograph",
1118
+ "concert"
1119
+ ],
1120
+ pool: [
1121
+ U("1511578314322-379afb476865"),
1122
+ U("1501281668745-f7f57925c3b4"),
1123
+ U("1523580494863-6f3031224c94"),
1124
+ U("1540575467063-178a50c2df87"),
1125
+ U("1505236858219-8359eb29e329"),
1126
+ U("1528605248644-14dd04022da1")
1127
+ ]
1128
+ }
1129
+ ];
1130
+ function poolForSubject(subject) {
1131
+ for (const theme of THEMED) {
1132
+ if (theme.keywords.some((k) => subject.includes(k))) {
1133
+ return theme.pool;
1134
+ }
1135
+ }
1136
+ return GENERIC;
1137
+ }
1138
+ function mixedHash(text) {
1139
+ let hash = 2166136261;
1140
+ for (let i = 0; i < text.length; i++) {
1141
+ hash ^= text.charCodeAt(i);
1142
+ hash = Math.imul(hash, 16777619);
1143
+ }
1144
+ return hash >>> 16 & 65535;
1145
+ }
1146
+ function resolvePlaceholderRef(ref) {
1147
+ const match = /^placeholder:([a-z0-9-]+)$/.exec(ref);
1148
+ if (!match) return null;
1149
+ const subject = match[1];
1150
+ const pool = poolForSubject(subject.replace(/-\d+$/, ""));
1151
+ return pool[mixedHash(ref) % pool.length];
1152
+ }
1153
+ function collectPlaceholderRefs(tree) {
1154
+ const seen = /* @__PURE__ */ new Set();
1155
+ for (const match of JSON.stringify(tree ?? null).matchAll(/"(placeholder:[a-z0-9-]+)"/gu)) {
1156
+ seen.add(match[1]);
1157
+ }
1158
+ return [...seen];
1159
+ }
1160
+ function buildPlaceholderMap(tree) {
1161
+ const map = {};
1162
+ const cursor = /* @__PURE__ */ new Map();
1163
+ for (const ref of collectPlaceholderRefs(tree)) {
1164
+ const subject = ref.slice("placeholder:".length).replace(/-\d+$/, "");
1165
+ const pool = poolForSubject(subject);
1166
+ const start = cursor.get(pool) ?? mixedHash(ref) % pool.length;
1167
+ map[ref] = pool[start % pool.length];
1168
+ cursor.set(pool, start + 1);
1169
+ }
1170
+ return map;
1171
+ }
1172
+
1173
+ // src/ui/ai-tree/AiTreeRenderer.tsx
547
1174
  var import_jsx_runtime = require("react/jsx-runtime");
548
1175
  function lucideByName(name) {
549
1176
  const pascal = name.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
@@ -557,6 +1184,7 @@ var typeStyle = (spec, font) => ({
557
1184
  fontWeight: spec.weight
558
1185
  });
559
1186
  var str = (value) => typeof value === "string" ? value : "";
1187
+ var cardRadius = (slots) => slots.cornerStyle === "sharp" ? 0 : AI_TREE_TOKENS.radiusCard;
560
1188
  var featureLines = (value) => value.split(/\n|<br\s*\/?>/i).map((line) => line.trim()).filter(Boolean);
561
1189
  var CHECK_MASK = `data:image/svg+xml,${encodeURIComponent(
562
1190
  '<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>'
@@ -583,6 +1211,25 @@ var FEATURE_LINE_CSS = [
583
1211
  `background-color:currentColor;-webkit-mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px;`,
584
1212
  `mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px}`
585
1213
  ].join("");
1214
+ function buttonShellStyle(ctx, fullWidth) {
1215
+ const bs = ctx.buttonStyle;
1216
+ if (bs) {
1217
+ return {
1218
+ borderRadius: bs.radius,
1219
+ ...bs.padding ? { padding: bs.padding } : {},
1220
+ ...bs.fontFamily ? { fontFamily: bs.fontFamily } : { fontFamily: ctx.brand.fonts.body },
1221
+ ...bs.fontSize ? { fontSize: bs.fontSize } : {},
1222
+ ...bs.fontWeight ? { fontWeight: bs.fontWeight } : {},
1223
+ ...bs.letterSpacing && bs.letterSpacing !== "normal" ? { letterSpacing: bs.letterSpacing } : {},
1224
+ ...bs.textTransform && bs.textTransform !== "none" ? { textTransform: bs.textTransform } : {}
1225
+ };
1226
+ }
1227
+ return {
1228
+ borderRadius: AI_TREE_TOKENS.radiusButton,
1229
+ padding: fullWidth ? `${AI_TREE_TOKENS.spacing2}px ${AI_TREE_TOKENS.spacing4}px` : `${AI_TREE_TOKENS.spacing3}px ${AI_TREE_TOKENS.spacing6}px`,
1230
+ ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
1231
+ };
1232
+ }
586
1233
  function hexLuminance(color) {
587
1234
  const m = /^#([0-9a-f]{6})$/i.exec(color.trim());
588
1235
  if (!m) return null;
@@ -599,6 +1246,12 @@ function hexContrast(a, b) {
599
1246
  const [hi, lo] = la > lb ? [la, lb] : [lb, la];
600
1247
  return (hi + 0.05) / (lo + 0.05);
601
1248
  }
1249
+ function primaryButtonLabel(brand) {
1250
+ const darkC = hexContrast(brand.palette.primary, brand.palette.dark);
1251
+ const lightC = hexContrast(brand.palette.primary, AI_TREE_TOKENS.textPrimaryForeground);
1252
+ if (darkC === null || lightC === null) return AI_TREE_TOKENS.textPrimaryForeground;
1253
+ return darkC > lightC ? brand.palette.dark : AI_TREE_TOKENS.textPrimaryForeground;
1254
+ }
602
1255
  function accentBandContext(brand) {
603
1256
  const p = brand.palette;
604
1257
  const lightWins = (hexContrast(p.light, p.primary) ?? 99) >= (hexContrast(p.dark, p.primary) ?? 0);
@@ -616,6 +1269,22 @@ function accentBandContext(brand) {
616
1269
  function textAttrs(ctx, path) {
617
1270
  return ctx.keyFor ? { "data-ohw-key": ctx.keyFor(path), "data-ohw-editable": "text" } : {};
618
1271
  }
1272
+ var AI_RESPONSIVE_CSS = [
1273
+ "@media (max-width: 960px) {",
1274
+ " [data-ai-responsive] [data-ai-grid] { grid-template-columns: repeat(2, 1fr) !important; }",
1275
+ ' [data-ai-responsive] [data-ai-grid="1"] { grid-template-columns: 1fr !important; }',
1276
+ "}",
1277
+ "@media (max-width: 640px) {",
1278
+ " [data-ai-responsive] [data-ai-grid] { grid-template-columns: 1fr !important; }",
1279
+ " [data-ai-responsive] [data-ai-columns] > * { grid-column: 1 / -1 !important; }",
1280
+ // Group containers flatten to a column on phones; span placements come along for free.
1281
+ " [data-ai-responsive] [data-ai-group] { display: flex !important; flex-direction: column !important; }",
1282
+ " [data-ai-responsive] [data-ai-group] > * { grid-column: auto !important; }",
1283
+ " [data-ai-responsive] [data-ai-section-inner] { padding-left: 20px !important; padding-right: 20px !important; }",
1284
+ " [data-ai-responsive] { overflow-x: hidden; }",
1285
+ " [data-ai-responsive] img { max-width: 100%; }",
1286
+ "}"
1287
+ ].join("\n");
619
1288
  var TRANSPARENT_PX = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
620
1289
  function MediaBox({
621
1290
  refValue,
@@ -628,13 +1297,17 @@ function MediaBox({
628
1297
  const url = refValue ? ctx.resolveMedia(refValue) : null;
629
1298
  const isIcon = /^(lucide|simple):/.test(refValue);
630
1299
  const aspectRatio = aspect && /^\d+:\d+$/.test(aspect) ? aspect.replace(":", " / ") : void 0;
631
- const editAttrs = ctx.keyFor && editPath && !isIcon ? { "data-ohw-key": ctx.keyFor(editPath), "data-ohw-editable": "image" } : {};
1300
+ const editAttrs = ctx.keyFor && editPath ? {
1301
+ "data-ohw-key": ctx.keyFor(editPath),
1302
+ "data-ohw-editable": isIcon ? "icon" : "image"
1303
+ } : {};
632
1304
  if (isIcon) {
633
1305
  const Icon = refValue.startsWith("lucide:") ? lucideByName(refValue.slice(7)) : null;
634
1306
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
635
1307
  "span",
636
1308
  {
637
1309
  "data-ai-icon": refValue,
1310
+ ...editAttrs,
638
1311
  style: {
639
1312
  display: "inline-flex",
640
1313
  width: 48,
@@ -698,12 +1371,10 @@ function ButtonEl({
698
1371
  width: fullWidth ? "100%" : void 0,
699
1372
  alignItems: "center",
700
1373
  justifyContent: "center",
701
- padding: fullWidth ? `${AI_TREE_TOKENS.spacing2}px ${AI_TREE_TOKENS.spacing4}px` : `${AI_TREE_TOKENS.spacing3}px ${AI_TREE_TOKENS.spacing6}px`,
702
- borderRadius: AI_TREE_TOKENS.radiusButton,
703
1374
  textDecoration: "none",
704
1375
  cursor: "pointer",
705
- ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body),
706
- ...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 }
1376
+ ...buttonShellStyle(ctx, fullWidth),
1377
+ ...secondary ? { border: `1px solid ${ctx.brand.palette.dark}`, color: ctx.brand.palette.dark } : { background: ctx.brand.palette.primary, color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand) }
707
1378
  },
708
1379
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
709
1380
  }
@@ -729,7 +1400,7 @@ function TextBlock({ slots, ctx, path }) {
729
1400
  }
730
1401
  function SectionHeaderBlock({ node, ctx, path }) {
731
1402
  const slots = node.slots ?? {};
732
- const align = slots.alignment === "center" ? "center" : "left";
1403
+ const align = node.align ?? (slots.alignment === "center" ? "center" : "left");
733
1404
  const children = node.children ?? [];
734
1405
  const buttonRowIdx = children.findIndex((c) => c.type === "button-row");
735
1406
  const buttonRow = buttonRowIdx >= 0 ? children[buttonRowIdx] : void 0;
@@ -773,7 +1444,7 @@ function SectionHeaderBlock({ node, ctx, path }) {
773
1444
  display: "flex",
774
1445
  gap: AI_TREE_TOKENS.spacing6,
775
1446
  marginTop: AI_TREE_TOKENS.spacing8,
776
- justifyContent: align === "center" ? "center" : "flex-start"
1447
+ justifyContent: align === "center" ? "center" : align === "right" ? "flex-end" : "flex-start"
777
1448
  },
778
1449
  children: (buttonRow.children ?? []).map((button, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
779
1450
  ButtonEl,
@@ -879,10 +1550,11 @@ function PricingCard({ node, ctx, path }) {
879
1550
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
880
1551
  "div",
881
1552
  {
1553
+ "data-ohw-card": "",
882
1554
  style: {
883
1555
  background: hasBg ? ctx.brand.palette.light : "transparent",
884
1556
  border: `1px solid ${dark}`,
885
- borderRadius: AI_TREE_TOKENS.radiusCard,
1557
+ borderRadius: cardRadius(slots),
886
1558
  padding: AI_TREE_TOKENS.paddingBlock,
887
1559
  display: "flex",
888
1560
  flexDirection: "column",
@@ -987,10 +1659,11 @@ function TestimonialCard({ node, ctx, path }) {
987
1659
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
988
1660
  "div",
989
1661
  {
1662
+ "data-ohw-card": "",
990
1663
  "data-ai-avatar-pos": avatarPos ?? void 0,
991
1664
  style: {
992
1665
  background: hasBg ? ctx.cardSurface : "transparent",
993
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1666
+ borderRadius: hasBg ? cardRadius(slots) : 0,
994
1667
  overflow: "hidden",
995
1668
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
996
1669
  minWidth: 0
@@ -1025,10 +1698,11 @@ function TeamCard({ node, ctx, path }) {
1025
1698
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1026
1699
  "div",
1027
1700
  {
1701
+ "data-ohw-card": "",
1028
1702
  "data-ai-avatar-pos": avatarPos ?? void 0,
1029
1703
  style: {
1030
1704
  background: hasBg ? ctx.cardSurface : "transparent",
1031
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1705
+ borderRadius: hasBg ? cardRadius(slots) : 0,
1032
1706
  overflow: "hidden",
1033
1707
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
1034
1708
  minWidth: 0,
@@ -1106,7 +1780,7 @@ function CardBlock({ node, ctx, path }) {
1106
1780
  editPath: `${path}.media`
1107
1781
  }
1108
1782
  ) : null;
1109
- const centered = slots.alignment === "center";
1783
+ const centered = (node.align ?? slots.alignment) === "center";
1110
1784
  const content = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1111
1785
  "div",
1112
1786
  {
@@ -1199,9 +1873,10 @@ function CardBlock({ node, ctx, path }) {
1199
1873
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1200
1874
  "div",
1201
1875
  {
1876
+ "data-ohw-card": "",
1202
1877
  style: {
1203
1878
  background: hasBg ? ctx.cardSurface : "transparent",
1204
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1879
+ borderRadius: hasBg ? cardRadius(slots) : 0,
1205
1880
  overflow: "hidden",
1206
1881
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
1207
1882
  display: horizontal ? "flex" : "block",
@@ -1229,7 +1904,7 @@ function CardBlock({ node, ctx, path }) {
1229
1904
  ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1230
1905
  "div",
1231
1906
  {
1232
- 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" },
1907
+ 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" },
1233
1908
  children: media
1234
1909
  }
1235
1910
  )),
@@ -1520,7 +2195,7 @@ function CollectionBlock({ node, ctx, path }) {
1520
2195
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1521
2196
  "div",
1522
2197
  {
1523
- "data-ai-grid": "",
2198
+ "data-ai-grid": String(itemsPerRow),
1524
2199
  style: {
1525
2200
  display: "grid",
1526
2201
  gridTemplateColumns: `repeat(${itemsPerRow}, 1fr)`,
@@ -1640,6 +2315,32 @@ function renderNode(node, ctx, path) {
1640
2315
  if (child) {
1641
2316
  return renderNode(child, ctx, `${path}.c0`);
1642
2317
  }
2318
+ if (str(slots.provider) === "map" && str(slots.query)) {
2319
+ const query = str(slots.query);
2320
+ const mapAttrs = ctx.keyFor ? {
2321
+ "data-ohw-key": ctx.keyFor(`${path}.query`),
2322
+ "data-ohw-editable": "map",
2323
+ "data-ohw-map-query": query
2324
+ } : {};
2325
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2326
+ "iframe",
2327
+ {
2328
+ ...mapAttrs,
2329
+ "data-ai-embed": "map",
2330
+ title: str(slots.title) || "Map",
2331
+ src: `https://www.google.com/maps?q=${encodeURIComponent(query)}&output=embed`,
2332
+ loading: "lazy",
2333
+ referrerPolicy: "no-referrer-when-downgrade",
2334
+ style: {
2335
+ width: "100%",
2336
+ minHeight: 320,
2337
+ border: 0,
2338
+ borderRadius: AI_TREE_TOKENS.radiusCard,
2339
+ display: "block"
2340
+ }
2341
+ }
2342
+ );
2343
+ }
1643
2344
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1644
2345
  "div",
1645
2346
  {
@@ -1743,15 +2444,12 @@ function renderNode(node, ctx, path) {
1743
2444
  alignSelf: submitAlign,
1744
2445
  border: "none",
1745
2446
  cursor: "pointer",
1746
- padding: "12px 24px",
1747
- // Corner radius follows the host template's own buttons (measured from a template
1748
- // CTA); 8px only when the page has no template button to match.
1749
- borderRadius: ctx.buttonRadius ?? 8,
2447
+ // Shape/padding/typography follow the host template's own buttons.
2448
+ ...buttonShellStyle(ctx),
1750
2449
  // Brand-styled: primary fill, brand-derived label colour (not a fixed token) so it
1751
2450
  // reads correctly on custom palettes.
1752
2451
  background: ctx.brand.palette.primary,
1753
- color: ctx.buttonLabel ?? ctx.brand.palette.light,
1754
- ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
2452
+ color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand)
1755
2453
  },
1756
2454
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
1757
2455
  },
@@ -1786,7 +2484,7 @@ function renderNode(node, ctx, path) {
1786
2484
  function AiTreeRenderer({
1787
2485
  tree,
1788
2486
  brand,
1789
- buttonRadius,
2487
+ buttonStyle,
1790
2488
  resolveMedia,
1791
2489
  editKeyPrefix
1792
2490
  }) {
@@ -1796,13 +2494,18 @@ function AiTreeRenderer({
1796
2494
  const resolvedBrand = brand ?? AI_DEFAULT_BRAND;
1797
2495
  const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
1798
2496
  const blockBrand = band?.brand ?? resolvedBrand;
2497
+ const placeholderMap = buildPlaceholderMap(tree);
1799
2498
  const ctx = {
1800
2499
  brand: blockBrand,
1801
- resolveMedia: resolveMedia ?? (() => null),
2500
+ // An owner/library ref resolves through the host resolver; a `placeholder:<subject>` ref the
2501
+ // host cannot resolve falls back to real stock photography (the per-section map first, then a
2502
+ // standalone resolve), so generated galleries, image rows, and overlay backgrounds arrive with
2503
+ // photos instead of grey boxes.
2504
+ resolveMedia: (ref) => resolveMedia?.(ref) ?? placeholderMap[ref] ?? resolvePlaceholderRef(ref),
1802
2505
  cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
1803
2506
  keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
1804
2507
  sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
1805
- buttonRadius,
2508
+ buttonStyle,
1806
2509
  ...band ? { buttonLabel: band.buttonLabel } : {}
1807
2510
  };
1808
2511
  const settings = tree.settings ?? {};
@@ -1825,11 +2528,25 @@ function AiTreeRenderer({
1825
2528
  }
1826
2529
  })();
1827
2530
  const distributed = !isOverlay && settings.textDistribution;
2531
+ const rowAlignItems = (rowAlign) => {
2532
+ if (rowAlign === "top") return "start";
2533
+ if (rowAlign === "bottom") return "end";
2534
+ if (rowAlign === "center" || rowAlign === "stretch") return rowAlign;
2535
+ if (distributed === "space-between") return "stretch";
2536
+ return (distributed ?? settings.verticalPosition) === "top" ? "start" : "center";
2537
+ };
2538
+ const cellAlignStyle = (blockAlign) => blockAlign ? {
2539
+ display: "flex",
2540
+ flexDirection: "column",
2541
+ alignItems: blockAlign === "left" ? "flex-start" : blockAlign === "right" ? "flex-end" : "center",
2542
+ textAlign: blockAlign
2543
+ } : {};
1828
2544
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1829
2545
  "section",
1830
2546
  {
1831
2547
  "data-ai-section": tree.tag ?? "",
1832
2548
  ...bgAttrs,
2549
+ "data-ai-responsive": "",
1833
2550
  style: {
1834
2551
  position: "relative",
1835
2552
  padding: `${pad}px 0`,
@@ -1840,12 +2557,13 @@ function AiTreeRenderer({
1840
2557
  color: settings.sectionBackground === "accent" ? blockBrand.palette.dark : void 0
1841
2558
  },
1842
2559
  children: [
1843
- isOverlay && backgroundUrl && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
2560
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: AI_RESPONSIVE_CSS }),
1844
2561
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: AI_MOBILE_CSS }),
2562
+ isOverlay && backgroundUrl && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
1845
2563
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1846
2564
  "div",
1847
2565
  {
1848
- "data-ai-container": "",
2566
+ "data-ai-section-inner": "",
1849
2567
  style: {
1850
2568
  position: "relative",
1851
2569
  maxWidth: AI_TREE_TOKENS.sectionMaxWidth,
@@ -1856,12 +2574,12 @@ function AiTreeRenderer({
1856
2574
  children: tree.rows.map((row, r2) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1857
2575
  "div",
1858
2576
  {
1859
- "data-ai-row": "",
2577
+ "data-ai-columns": "",
1860
2578
  style: {
1861
2579
  display: "grid",
1862
2580
  gridTemplateColumns: "repeat(12, 1fr)",
1863
2581
  gap: AI_TREE_TOKENS.spacing6,
1864
- alignItems: distributed === "space-between" ? "stretch" : (distributed ?? settings.verticalPosition) === "top" ? "start" : "center",
2582
+ alignItems: rowAlignItems(row.align),
1865
2583
  marginTop: r2 > 0 ? AI_TREE_TOKENS.spacing8 : 0
1866
2584
  },
1867
2585
  children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -1871,6 +2589,8 @@ function AiTreeRenderer({
1871
2589
  style: {
1872
2590
  gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`,
1873
2591
  minWidth: 0,
2592
+ // Horizontal placement of the block's content within its column.
2593
+ ...cellAlignStyle(block.align),
1874
2594
  // space-between: each column becomes a flex column whose content spreads over
1875
2595
  // the full row height instead of clumping at the top.
1876
2596
  ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
@@ -1893,7 +2613,7 @@ function AiTreeRenderer({
1893
2613
  var import_jsx_runtime2 = require("react/jsx-runtime");
1894
2614
  var CONTAINER_ATTR = "data-ohw-ai-generated";
1895
2615
  var REPLACED_ATTR = "data-ohw-ai-replaced-by";
1896
- var REMOVED_ATTR = "data-ohw-ai-removed";
2616
+ var REMOVED_ATTR2 = "data-ohw-ai-removed";
1897
2617
  var TEMPLATE_HIDDEN_ATTR = "data-ohw-ai-template-hidden";
1898
2618
  var CHROME_SECTION_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
1899
2619
  function readRootVar(name) {
@@ -1917,13 +2637,13 @@ function deriveBrandOverride() {
1917
2637
  };
1918
2638
  }
1919
2639
  function deriveTemplateBrand() {
1920
- const dark = readRootVar("--color-dark");
1921
- const primary = readRootVar("--color-primary");
1922
- const light = readRootVar("--color-light");
2640
+ const primary = readRootVar("--brand-primary") || readRootVar("--color-primary");
2641
+ const dark = readRootVar("--brand-text") || readRootVar("--color-dark");
2642
+ const light = readRootVar("--brand-background") || readRootVar("--color-light");
1923
2643
  if (!dark || !primary || !light) return null;
1924
- const accent = readRootVar("--color-accent");
1925
- const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1926
- const body = readRootVar("--font-body");
2644
+ const accent = readRootVar("--brand-accent") || readRootVar("--color-accent");
2645
+ const heading = readRootVar("--brand-font-heading") || readRootVar("--font-heading") || readRootVar("--font-display");
2646
+ const body = readRootVar("--brand-font-body") || readRootVar("--font-body");
1927
2647
  return {
1928
2648
  palette: { dark, primary, accent: accent || dark, light },
1929
2649
  fonts: {
@@ -1932,12 +2652,32 @@ function deriveTemplateBrand() {
1932
2652
  }
1933
2653
  };
1934
2654
  }
1935
- function deriveTemplateButtonRadius() {
2655
+ function deriveTemplateButtonStyle() {
1936
2656
  if (typeof document === "undefined") return null;
1937
- const btn = document.querySelector('[data-ohw-role="button"]');
2657
+ const btn = Array.from(document.querySelectorAll('[data-ohw-role="button"]')).find(
2658
+ (el) => !el.closest(`[${CONTAINER_ATTR}]`)
2659
+ );
1938
2660
  if (!btn) return null;
1939
- const radius = getComputedStyle(btn).borderTopLeftRadius;
1940
- return radius || null;
2661
+ const cs = getComputedStyle(btn);
2662
+ const corners = [
2663
+ cs.borderTopLeftRadius,
2664
+ cs.borderTopRightRadius,
2665
+ cs.borderBottomRightRadius,
2666
+ cs.borderBottomLeftRadius
2667
+ ].map((v) => v || "0px");
2668
+ const radius = corners.every((v) => v === corners[0]) ? corners[0] : corners.join(" ");
2669
+ const px = (v) => parseFloat(v) || 0;
2670
+ const padY = Math.max(px(cs.paddingTop), px(cs.paddingBottom));
2671
+ const padX = Math.max(px(cs.paddingLeft), px(cs.paddingRight));
2672
+ return {
2673
+ radius: radius || "10px",
2674
+ padding: `${padY}px ${padX}px`,
2675
+ fontFamily: cs.fontFamily || "",
2676
+ fontSize: cs.fontSize || "",
2677
+ fontWeight: cs.fontWeight || "",
2678
+ letterSpacing: cs.letterSpacing || "",
2679
+ textTransform: cs.textTransform || ""
2680
+ };
1941
2681
  }
1942
2682
  var mounted = /* @__PURE__ */ new Map();
1943
2683
  function findTemplateSection(id) {
@@ -1987,18 +2727,18 @@ function placeContainer(container, entry) {
1987
2727
  }
1988
2728
  function syncRemovedSections(state) {
1989
2729
  const removed = new Set(state.removed ?? []);
1990
- for (const el of document.querySelectorAll(`[${REMOVED_ATTR}]`)) {
2730
+ for (const el of document.querySelectorAll(`[${REMOVED_ATTR2}]`)) {
1991
2731
  const id = el.getAttribute("data-ohw-section") ?? "";
1992
2732
  if (!removed.has(id)) {
1993
2733
  el.style.removeProperty("display");
1994
- el.removeAttribute(REMOVED_ATTR);
2734
+ el.removeAttribute(REMOVED_ATTR2);
1995
2735
  }
1996
2736
  }
1997
2737
  for (const id of removed) {
1998
2738
  const section = findTemplateSection(id);
1999
2739
  if (section && !section.hasAttribute(REPLACED_ATTR)) {
2000
2740
  section.style.display = "none";
2001
- section.setAttribute(REMOVED_ATTR, "");
2741
+ section.setAttribute(REMOVED_ATTR2, "");
2002
2742
  }
2003
2743
  }
2004
2744
  }
@@ -2015,7 +2755,7 @@ function syncTemplateHidden(state, pageHasSections) {
2015
2755
  if (el.hasAttribute(CONTAINER_ATTR)) continue;
2016
2756
  if (CHROME_SECTION_IDS.has(el.getAttribute("data-ohw-section") ?? "")) continue;
2017
2757
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
2018
- if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR)) continue;
2758
+ if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR2)) continue;
2019
2759
  el.style.display = "none";
2020
2760
  el.setAttribute(TEMPLATE_HIDDEN_ATTR, "");
2021
2761
  }
@@ -2039,18 +2779,23 @@ function syncReplacedOriginals(state) {
2039
2779
  }
2040
2780
  }
2041
2781
  var sectionOrderIndex = /* @__PURE__ */ new Map();
2782
+ var removedSectionIds = /* @__PURE__ */ new Set();
2042
2783
  function setAiSectionOrder(raw, currentPath) {
2043
2784
  const next = /* @__PURE__ */ new Map();
2785
+ const removed = /* @__PURE__ */ new Set();
2044
2786
  if (raw) {
2045
2787
  try {
2046
2788
  const entries = JSON.parse(raw);
2047
2789
  for (const entry of entries) {
2048
- if (!entry.pagePath || entry.pagePath === currentPath) next.set(entry.instanceId, entry.order);
2790
+ if (entry.pagePath && entry.pagePath !== currentPath) continue;
2791
+ next.set(entry.instanceId, entry.order);
2792
+ if (entry.removed) removed.add(entry.instanceId);
2049
2793
  }
2050
2794
  } catch {
2051
2795
  }
2052
2796
  }
2053
2797
  sectionOrderIndex = next;
2798
+ removedSectionIds = removed;
2054
2799
  }
2055
2800
  function applyExplicitOrder(entries) {
2056
2801
  if (sectionOrderIndex.size === 0) return entries;
@@ -2086,11 +2831,23 @@ function orderByChain(sections) {
2086
2831
  for (const root of roots) visit(root);
2087
2832
  return out.length === sections.length ? out : sections;
2088
2833
  }
2834
+ function syncSoftRemovedGenerated() {
2835
+ for (const [id, section] of mounted) {
2836
+ const el = section.container;
2837
+ if (removedSectionIds.has(id)) {
2838
+ el.style.display = "none";
2839
+ el.setAttribute(REMOVED_ATTR, "");
2840
+ } else if (el.hasAttribute(REMOVED_ATTR)) {
2841
+ el.style.removeProperty("display");
2842
+ el.removeAttribute(REMOVED_ATTR);
2843
+ }
2844
+ }
2845
+ }
2089
2846
  function applyAiSectionsToDom(state, options) {
2090
2847
  if (typeof document === "undefined") return;
2091
2848
  const brandOverride = deriveBrandOverride();
2092
2849
  const templateBrand = deriveTemplateBrand();
2093
- const templateButtonRadius = deriveTemplateButtonRadius();
2850
+ const templateButtonStyle = deriveTemplateButtonStyle();
2094
2851
  const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
2095
2852
  const pagePath = window.location.pathname;
2096
2853
  const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
@@ -2130,7 +2887,7 @@ function applyAiSectionsToDom(state, options) {
2130
2887
  {
2131
2888
  tree: entry.tree,
2132
2889
  brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2133
- buttonRadius: templateButtonRadius,
2890
+ buttonStyle: templateButtonStyle,
2134
2891
  resolveMedia,
2135
2892
  editKeyPrefix: `ai.${entry.id}`
2136
2893
  }
@@ -2153,6 +2910,7 @@ function applyAiSectionsToDom(state, options) {
2153
2910
  syncReplacedOriginals(state);
2154
2911
  syncRemovedSections(state);
2155
2912
  syncTemplateHidden(state, pageSections.length > 0);
2913
+ syncSoftRemovedGenerated();
2156
2914
  }
2157
2915
 
2158
2916
  // src/useLinkHrefGuardian.ts
@@ -7882,6 +8640,7 @@ function MediaOverlay({
7882
8640
  (prev) => prev && prev.fullWidth === width && prev.height === height ? prev : { fullWidth: width, height }
7883
8641
  );
7884
8642
  }, [isVideo]);
8643
+ const replaceLabel = isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image";
7885
8644
  const replaceMode = buttonSize ? replaceButtonMode({ width: rect.width, height: rect.height }, buttonSize) : "none";
7886
8645
  const box = {
7887
8646
  position: "fixed",
@@ -8011,17 +8770,17 @@ function MediaOverlay({
8011
8770
  },
8012
8771
  children: [
8013
8772
  isVideo ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react5.Film, { size: 14 }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react5.ImageIcon, { size: 14 }),
8014
- isVideo ? "Replace video" : "Replace image"
8773
+ replaceLabel
8015
8774
  ]
8016
8775
  }
8017
8776
  ),
8018
- replaceMode === "none" ? null : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
8777
+ showChrome && replaceMode !== "none" && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
8019
8778
  Button,
8020
8779
  {
8021
8780
  "data-ohw-media-overlay": "",
8022
8781
  variant: "outline",
8023
8782
  size: "sm",
8024
- "aria-label": isVideo ? "Replace video" : "Replace image",
8783
+ "aria-label": replaceLabel,
8025
8784
  className: "gap-1.5 cursor-pointer hover:bg-background",
8026
8785
  style: {
8027
8786
  ...OVERLAY_BUTTON_STYLE,
@@ -8044,7 +8803,7 @@ function MediaOverlay({
8044
8803
  },
8045
8804
  children: [
8046
8805
  isVideo ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react5.Film, { size: 14 }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react5.ImageIcon, { size: 14 }),
8047
- replaceMode === "full" ? isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image" : null
8806
+ replaceMode === "full" ? replaceLabel : null
8048
8807
  ]
8049
8808
  }
8050
8809
  )
@@ -8085,215 +8844,33 @@ function CarouselOverlay({
8085
8844
  boxShadow: "inset 0 0 0 1.5px var(--color-primary)",
8086
8845
  background: "color-mix(in srgb, var(--color-primary) 20%, transparent)"
8087
8846
  },
8088
- onClick: () => onEdit(hover.key),
8089
- children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
8090
- Button,
8091
- {
8092
- "data-ohw-carousel-overlay": "",
8093
- variant: "outline",
8094
- size: "sm",
8095
- className: "cursor-pointer gap-1.5 hover:bg-background",
8096
- style: OVERLAY_BUTTON_STYLE2,
8097
- onMouseDown: (e) => e.preventDefault(),
8098
- onClick: (e) => {
8099
- e.stopPropagation();
8100
- onEdit(hover.key);
8101
- },
8102
- children: [
8103
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react6.GalleryHorizontal, { size: 14 }),
8104
- "Edit gallery"
8105
- ]
8106
- }
8107
- )
8108
- }
8109
- );
8110
- }
8111
-
8112
- // src/ui/ai-section/AiSectionOverlay.tsx
8113
- var import_react8 = require("react");
8114
- var import_lucide_react7 = require("lucide-react");
8115
-
8116
- // src/lib/sections.ts
8117
- var LINK_PICKER_EXCLUDED_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
8118
- function isChromeSection(el) {
8119
- return el.matches("header, nav, footer, aside");
8120
- }
8121
- function titleCaseSectionId(id) {
8122
- return id.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
8123
- }
8124
- function parseSectionsFromRoot(root) {
8125
- const seen = /* @__PURE__ */ new Set();
8126
- const sections = [];
8127
- for (const el of root.querySelectorAll("[data-ohw-section]")) {
8128
- const id = el.getAttribute("data-ohw-section") ?? "";
8129
- if (!id || seen.has(id) || LINK_PICKER_EXCLUDED_IDS.has(id)) continue;
8130
- if (el.parentElement?.closest("[data-ohw-section]")) continue;
8131
- if (el.hasAttribute("data-ohw-ai-template-hidden") || el.hasAttribute("data-ohw-ai-removed") || el.hasAttribute("data-ohw-ai-replaced-by"))
8132
- continue;
8133
- seen.add(id);
8134
- const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
8135
- sections.push({ id, label });
8136
- }
8137
- return sections;
8138
- }
8139
- function collectSectionsFromDom() {
8140
- if (typeof document === "undefined") return [];
8141
- return parseSectionsFromRoot(document);
8142
- }
8143
- function parseSectionsFromHtml(html) {
8144
- const doc = new DOMParser().parseFromString(html, "text/html");
8145
- return parseSectionsFromRoot(doc);
8146
- }
8147
-
8148
- // src/lib/section-instances.ts
8149
- var SECTION_ORDER_KEY = "__ohw_section_order";
8150
- var REMOVED_ATTR2 = "data-ohw-section-removed";
8151
- function isRemovedSection(el) {
8152
- return el.hasAttribute(REMOVED_ATTR2);
8153
- }
8154
- function topLevelSections() {
8155
- return Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
8156
- (el) => !el.parentElement?.closest("[data-ohw-section]") && !isChromeSection(el) && !isRemovedSection(el)
8157
- );
8158
- }
8159
- function instanceIdOf(el) {
8160
- return el.dataset.ohwInstance ?? el.dataset.ohwSection ?? "";
8161
- }
8162
- function findByInstanceId(instanceId) {
8163
- const escapedId = CSS.escape(instanceId);
8164
- return document.querySelector(`[data-ohw-instance="${escapedId}"]`) ?? document.querySelector(`[data-ohw-section="${escapedId}"]:not([data-ohw-instance])`);
8165
- }
8166
- function planSectionMove(instanceId, targetIndex, currentPath) {
8167
- const sections = topLevelSections();
8168
- const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
8169
- if (index === -1) return null;
8170
- const dragged = sections[index];
8171
- const others = sections.filter((_, i) => i !== index);
8172
- const clamped = Math.max(0, Math.min(targetIndex, others.length));
8173
- const reordered = [...others.slice(0, clamped), dragged, ...others.slice(clamped)];
8174
- return reordered.map((el, order) => ({
8175
- instanceId: instanceIdOf(el),
8176
- type: el.getAttribute("data-ohw-section") ?? "",
8177
- order,
8178
- pagePath: currentPath
8179
- }));
8180
- }
8181
- function moveSectionInstance(instanceId, direction, currentPath) {
8182
- const sections = topLevelSections();
8183
- const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
8184
- if (index === -1) return null;
8185
- const siblingIndex = direction === "up" ? index - 1 : index + 1;
8186
- if (siblingIndex < 0 || siblingIndex >= sections.length) return null;
8187
- const entries = planSectionMove(instanceId, siblingIndex, currentPath);
8188
- if (!entries) return null;
8189
- applyPersistedOrder(entries);
8190
- return entries;
8191
- }
8192
- function syncRemovedFlags(entries) {
8193
- const removedIds = new Set(entries.filter((e) => e.removed).map((e) => e.instanceId));
8194
- document.querySelectorAll(`[${REMOVED_ATTR2}]`).forEach((el) => {
8195
- if (!removedIds.has(instanceIdOf(el))) {
8196
- el.style.removeProperty("display");
8197
- el.removeAttribute(REMOVED_ATTR2);
8198
- }
8199
- });
8200
- for (const id of removedIds) {
8201
- const el = findByInstanceId(id);
8202
- if (el) {
8203
- el.style.display = "none";
8204
- el.setAttribute(REMOVED_ATTR2, "");
8847
+ onClick: () => onEdit(hover.key),
8848
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
8849
+ Button,
8850
+ {
8851
+ "data-ohw-carousel-overlay": "",
8852
+ variant: "outline",
8853
+ size: "sm",
8854
+ className: "cursor-pointer gap-1.5 hover:bg-background",
8855
+ style: OVERLAY_BUTTON_STYLE2,
8856
+ onMouseDown: (e) => e.preventDefault(),
8857
+ onClick: (e) => {
8858
+ e.stopPropagation();
8859
+ onEdit(hover.key);
8860
+ },
8861
+ children: [
8862
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react6.GalleryHorizontal, { size: 14 }),
8863
+ "Edit gallery"
8864
+ ]
8865
+ }
8866
+ )
8205
8867
  }
8206
- }
8207
- }
8208
- function applyPersistedOrder(entries) {
8209
- syncRemovedFlags(entries);
8210
- if (entries.length === 0) return;
8211
- const sections = topLevelSections();
8212
- if (sections.length === 0) return;
8213
- const orderIndex = new Map(entries.map((e) => [e.instanceId, e.order]));
8214
- const ordered = [...sections].sort((a, b) => {
8215
- const aOrder = orderIndex.get(instanceIdOf(a));
8216
- const bOrder = orderIndex.get(instanceIdOf(b));
8217
- if (aOrder === void 0 && bOrder === void 0) return 0;
8218
- if (aOrder === void 0) return 1;
8219
- if (bOrder === void 0) return -1;
8220
- return aOrder - bOrder;
8221
- });
8222
- let prev = null;
8223
- for (const el of ordered) {
8224
- if (prev) prev.after(el);
8225
- prev = el;
8226
- }
8227
- }
8228
- function setSectionRemoved(instanceId, currentPath, existingEntries, removed) {
8229
- if (!findByInstanceId(instanceId)) return null;
8230
- const byId = new Map(existingEntries.map((e) => [e.instanceId, e]));
8231
- const allSections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
8232
- (el) => !el.parentElement?.closest("[data-ohw-section]") && !isChromeSection(el)
8233
8868
  );
8234
- allSections.forEach((el, order) => {
8235
- const id = instanceIdOf(el);
8236
- if (!byId.has(id)) {
8237
- byId.set(id, { instanceId: id, type: el.getAttribute("data-ohw-section") ?? "", order, pagePath: currentPath });
8238
- }
8239
- });
8240
- const target = byId.get(instanceId);
8241
- if (!target) return null;
8242
- byId.set(instanceId, { ...target, removed });
8243
- const entries = Array.from(byId.values());
8244
- applyPersistedOrder(entries);
8245
- return entries;
8246
- }
8247
- function deleteSectionInstance(instanceId, currentPath, existingEntries) {
8248
- return setSectionRemoved(instanceId, currentPath, existingEntries, true);
8249
- }
8250
- function restoreSectionInstance(instanceId, currentPath, existingEntries) {
8251
- return setSectionRemoved(instanceId, currentPath, existingEntries, false);
8252
- }
8253
- function newInstanceId() {
8254
- return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `instance-${Date.now()}-${Math.random().toString(36).slice(2)}`;
8255
- }
8256
- function getPageSectionOrderEntries(raw, currentPath) {
8257
- if (!raw) return [];
8258
- try {
8259
- const entries = JSON.parse(raw);
8260
- return entries.filter((e) => !e.pagePath || e.pagePath === currentPath);
8261
- } catch {
8262
- return [];
8263
- }
8264
- }
8265
- function rekeySectionSubtree(root, instanceId) {
8266
- const suffix = `::${instanceId}`;
8267
- const rekey = (el, attr) => {
8268
- const current = el.getAttribute(attr);
8269
- if (current) el.setAttribute(attr, `${current}${suffix}`);
8270
- };
8271
- if (root.hasAttribute("data-ohw-key")) rekey(root, "data-ohw-key");
8272
- if (root.hasAttribute("data-ohw-href-key")) rekey(root, "data-ohw-href-key");
8273
- root.querySelectorAll("[data-ohw-key]").forEach((el) => rekey(el, "data-ohw-key"));
8274
- root.querySelectorAll("[data-ohw-href-key]").forEach((el) => rekey(el, "data-ohw-href-key"));
8275
- }
8276
- function initSectionInstancesFromContent(content, currentPath) {
8277
- document.querySelectorAll("[data-ohw-section]:not([data-ohw-instance])").forEach((el) => {
8278
- el.setAttribute("data-ohw-instance", el.getAttribute("data-ohw-section") ?? "");
8279
- });
8280
- const entries = getPageSectionOrderEntries(content[SECTION_ORDER_KEY], currentPath);
8281
- for (const entry of entries) {
8282
- if (entry.instanceId === entry.type) continue;
8283
- if (document.querySelector(`[data-ohw-instance="${CSS.escape(entry.instanceId)}"]`)) continue;
8284
- const original = document.querySelector(
8285
- `[data-ohw-section="${CSS.escape(entry.type)}"][data-ohw-instance="${CSS.escape(entry.type)}"]`
8286
- );
8287
- if (!original) continue;
8288
- const clone = original.cloneNode(true);
8289
- clone.setAttribute("data-ohw-instance", entry.instanceId);
8290
- rekeySectionSubtree(clone, entry.instanceId);
8291
- original.insertAdjacentElement("afterend", clone);
8292
- }
8293
- applyPersistedOrder(entries);
8294
8869
  }
8295
8870
 
8296
8871
  // src/ui/ai-section/AiSectionOverlay.tsx
8872
+ var import_react8 = require("react");
8873
+ var import_lucide_react7 = require("lucide-react");
8297
8874
  var import_jsx_runtime17 = require("react/jsx-runtime");
8298
8875
  function findSectionElement(instanceId) {
8299
8876
  const escaped = CSS.escape(instanceId);
@@ -13009,6 +13586,7 @@ function readLogoSizeState(content, placement) {
13009
13586
  function getLogoElement(el) {
13010
13587
  const marked = el.closest('[data-ohw-role="logo"], [data-ohw-logo]');
13011
13588
  if (marked) return marked;
13589
+ if (el.closest('[data-ohw-editable="icon"]')) return null;
13012
13590
  const root = el.closest("nav, [data-ohw-nav-root], footer");
13013
13591
  if (!root) return null;
13014
13592
  const anchor = el.closest("a");
@@ -14075,15 +14653,17 @@ function useSectionDrag({
14075
14653
  clearSectionDragVisuals();
14076
14654
  return;
14077
14655
  }
14078
- const orderJson = JSON.stringify(entries);
14656
+ const orderJson = JSON.stringify(
14657
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, entries)
14658
+ );
14079
14659
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
14080
14660
  setAiSectionOrder(orderJson, window.location.pathname);
14081
14661
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
14082
- applyPersistedOrder(entries);
14662
+ applyPersistedOrder(getPageSectionOrderEntries(orderJson, window.location.pathname));
14083
14663
  clearSectionDragVisuals();
14084
14664
  requestAnimationFrame(() => {
14085
14665
  if (editContentRef.current[SECTION_ORDER_KEY] === orderJson) {
14086
- applyPersistedOrder(entries);
14666
+ applyPersistedOrder(getPageSectionOrderEntries(orderJson, window.location.pathname));
14087
14667
  }
14088
14668
  requestAnimationFrame(() => {
14089
14669
  window.dispatchEvent(new Event("resize"));
@@ -14384,6 +14964,9 @@ function collectEditableNodes(extraContent, root = document) {
14384
14964
  if (el.dataset.ohwEditable === "link") {
14385
14965
  return { key: el.dataset.ohwKey ?? "", type: "link", text: getLinkHref3(el) };
14386
14966
  }
14967
+ if (el.dataset.ohwEditable === "map") {
14968
+ return { key: el.dataset.ohwKey ?? "", type: "map", text: el.dataset.ohwMapQuery ?? "" };
14969
+ }
14387
14970
  return {
14388
14971
  key: el.dataset.ohwKey ?? "",
14389
14972
  type: el.dataset.ohwEditable ?? "text",
@@ -14937,21 +15520,10 @@ function parseSchedulingInsertAfter(insertAfter) {
14937
15520
  insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
14938
15521
  };
14939
15522
  }
14940
- function resolveSchedulingInsert(insertAfter, explicitInsertBefore) {
14941
- const parsed = parseSchedulingInsertAfter(insertAfter);
14942
- const insertBefore = explicitInsertBefore ?? parsed.insertBefore;
14943
- const effectiveInsertAfter = insertBefore && parsed.insertBefore === null ? `${insertAfter}${INSERT_BEFORE_MARKER}${insertBefore}` : insertAfter;
14944
- return { effectiveInsertAfter, insertBefore };
14945
- }
14946
- function getSchedulingMountPoint(insertAfter) {
14947
- const { anchor } = parseSchedulingInsertAfter(insertAfter);
14948
- let anchorEl = document.querySelector(`[data-ohw-section="${anchor}"]`);
14949
- if (!anchorEl && anchor === "scheduling") {
14950
- const widgets = Array.from(document.querySelectorAll('[data-ohw-section^="scheduling-"]'));
14951
- anchorEl = widgets.at(-1) ?? null;
14952
- }
14953
- if (!anchorEl) return null;
14954
- return anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
15523
+ function resolveEntryAnchor(entry) {
15524
+ if (entry.anchorId !== void 0) return { anchorId: entry.anchorId, beforeId: entry.beforeId ?? null };
15525
+ const parsed = parseSchedulingInsertAfter(entry.insertAfter);
15526
+ return { anchorId: parsed.anchor, beforeId: parsed.insertBefore };
14955
15527
  }
14956
15528
  function schedulingMountDepth(insertAfter) {
14957
15529
  if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
@@ -14968,8 +15540,7 @@ function getPageSchedulingEntries(raw) {
14968
15540
  }
14969
15541
  }
14970
15542
  function isSchedulingWidgetMissing(entry) {
14971
- const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
14972
- return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
15543
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(entry.insertAfter)}"]`);
14973
15544
  }
14974
15545
  function hasMissingSchedulingWidgets(entries) {
14975
15546
  return entries.some(isSchedulingWidgetMissing);
@@ -14999,16 +15570,17 @@ function initSectionsFromContent(content, removeExisting = false) {
14999
15570
  } catch {
15000
15571
  }
15001
15572
  }
15002
- function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
15003
- const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
15004
- const sectionId = schedulingSectionId(effectiveInsertAfter);
15573
+ function mountSchedulingWidget(anchorId, notifyOnConnect = false, scheduleId, beforeId, existingWidgetId) {
15574
+ const widgetId = existingWidgetId ?? (beforeId ? `${anchorId}${INSERT_BEFORE_MARKER}${beforeId}` : anchorId);
15575
+ const sectionId = schedulingSectionId(widgetId);
15005
15576
  if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
15006
- const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
15007
- if (!mountPoint) return false;
15577
+ const anchorEl = document.querySelector(`[data-ohw-section="${anchorId}"]`);
15578
+ if (!anchorEl) return false;
15579
+ const mountPoint = anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
15008
15580
  const container = document.createElement("div");
15009
15581
  container.dataset.ohwSectionContainer = "scheduling";
15010
- if (insertBefore) {
15011
- const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
15582
+ if (beforeId) {
15583
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${beforeId}"]`);
15012
15584
  const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
15013
15585
  if (!beforePoint) return false;
15014
15586
  beforePoint.insertAdjacentElement("beforebegin", container);
@@ -15019,19 +15591,25 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
15019
15591
  }
15020
15592
  tail.insertAdjacentElement("afterend", container);
15021
15593
  }
15022
- const root = (0, import_client2.createRoot)(container);
15023
- (0, import_react_dom3.flushSync)(() => {
15024
- root.render(
15025
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
15026
- SchedulingWidget,
15027
- {
15028
- notifyOnConnect,
15029
- initialScheduleId: scheduleId,
15030
- insertAfter: effectiveInsertAfter
15031
- }
15032
- )
15033
- );
15034
- });
15594
+ try {
15595
+ const root = (0, import_client2.createRoot)(container);
15596
+ (0, import_react_dom3.flushSync)(() => {
15597
+ root.render(
15598
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
15599
+ SchedulingWidget,
15600
+ {
15601
+ notifyOnConnect,
15602
+ initialScheduleId: scheduleId,
15603
+ insertAfter: widgetId
15604
+ }
15605
+ )
15606
+ );
15607
+ });
15608
+ } catch (err) {
15609
+ console.error("[ow:scheduling] render threw", err);
15610
+ container.remove();
15611
+ return false;
15612
+ }
15035
15613
  const tracker = getSectionsTracker();
15036
15614
  let sections = [];
15037
15615
  try {
@@ -15039,10 +15617,12 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
15039
15617
  } catch {
15040
15618
  }
15041
15619
  const inEditor = typeof window !== "undefined" && window.self !== window.top;
15042
- if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
15620
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === widgetId)) {
15043
15621
  sections.push({
15044
15622
  type: "scheduling",
15045
- insertAfter: effectiveInsertAfter,
15623
+ insertAfter: widgetId,
15624
+ anchorId,
15625
+ beforeId: beforeId ?? null,
15046
15626
  pagePath: window.location.pathname,
15047
15627
  ...scheduleId ? { scheduleId } : {}
15048
15628
  });
@@ -15056,7 +15636,8 @@ function mountSchedulingEntries(entries, notifyOnConnect = false) {
15056
15636
  for (let i = pending.length - 1; i >= 0; i--) {
15057
15637
  const entry = pending[i];
15058
15638
  const shouldNotify = typeof notifyOnConnect === "function" ? notifyOnConnect(entry) : notifyOnConnect;
15059
- if (mountSchedulingWidget(entry.insertAfter, shouldNotify, entry.scheduleId ?? null)) {
15639
+ const { anchorId, beforeId } = resolveEntryAnchor(entry);
15640
+ if (mountSchedulingWidget(anchorId, shouldNotify, entry.scheduleId ?? null, beforeId, entry.insertAfter)) {
15060
15641
  pending.splice(i, 1);
15061
15642
  }
15062
15643
  }
@@ -15148,7 +15729,7 @@ var EDITOR_CHROME_SELECTOR = '[data-ohw-toolbar], [data-ohw-form-toolbar], [data
15148
15729
  function isOverEditorChrome(x, y) {
15149
15730
  return document.elementsFromPoint(x, y).some((el) => el instanceof HTMLElement && el.matches(EDITOR_CHROME_SELECTOR));
15150
15731
  }
15151
- var NON_MEDIA_SELECTOR = '[data-ohw-editable]:not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"]):not([data-ohw-editable="video"]):not([data-ohw-editable="icon"]):not([data-ohw-editable="form"])';
15732
+ var NON_MEDIA_SELECTOR = '[data-ohw-editable]:not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"]):not([data-ohw-editable="video"]):not([data-ohw-editable="icon"]):not([data-ohw-editable="form"]):not([data-ohw-editable="map"])';
15152
15733
  function getVideoEl2(el) {
15153
15734
  return el instanceof HTMLVideoElement ? el : el.querySelector("video");
15154
15735
  }
@@ -15204,6 +15785,12 @@ function applyVideoSettingNode(key, val) {
15204
15785
  });
15205
15786
  return true;
15206
15787
  }
15788
+ function applyMapQuery(el, val) {
15789
+ if (!(el instanceof HTMLIFrameElement)) return;
15790
+ const nextSrc = `https://www.google.com/maps?q=${encodeURIComponent(val)}&output=embed`;
15791
+ if (el.src !== nextSrc) el.src = nextSrc;
15792
+ el.setAttribute("data-ohw-map-query", val);
15793
+ }
15207
15794
  function applyLinkByKey(key, val) {
15208
15795
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
15209
15796
  if (el.dataset.ohwEditable === "link") applyLinkHref(el, val);
@@ -15214,6 +15801,11 @@ function applyLinkByKey(key, val) {
15214
15801
  hrefAnchors.forEach((el) => applyLinkHref(el, val));
15215
15802
  }
15216
15803
  }
15804
+ function isInsideLinkEditor(target) {
15805
+ return Boolean(
15806
+ target.closest("[data-ohw-link-popover-root]") || target.closest("[data-ohw-link-modal-root]") || target.closest("[data-ohw-link-page-dropdown]") || target.closest("[data-ohw-section-picker]") || target.closest("[data-ohw-navbar-container-chrome]") || target.closest("[data-ohw-navbar-add-button]") || target.closest("[data-ohw-footer-container-chrome]") || target.closest("[data-ohw-footer-add-button]") || target.closest("[data-ohw-more-menu]") || target.closest('[data-slot="dropdown-menu-content"]') || target.closest('[data-slot="popover-content"]') || target.closest('[data-slot="dialog-content"]') || target.closest('[data-slot="dialog-overlay"]')
15807
+ );
15808
+ }
15217
15809
  function isInsideFloatingPanel(target) {
15218
15810
  return Boolean(target.closest("[data-ohw-floating-panel]"));
15219
15811
  }
@@ -15221,11 +15813,6 @@ function isPointOverFloatingPanel(clientX, clientY) {
15221
15813
  const el = document.elementFromPoint(clientX, clientY);
15222
15814
  return Boolean(el instanceof Element && el.closest("[data-ohw-floating-panel]"));
15223
15815
  }
15224
- function isInsideLinkEditor(target) {
15225
- return Boolean(
15226
- 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"]')
15227
- );
15228
- }
15229
15816
  function getHrefKeyFromElement(el) {
15230
15817
  if (!el) return null;
15231
15818
  const anchor = el.closest("[data-ohw-href-key]");
@@ -15484,7 +16071,7 @@ function getNavigationSelectionParent(el) {
15484
16071
  if (!isFooterLinksContainer(el) && (el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isInferredFooterGroup2(el))) {
15485
16072
  return getFooterLinksContainer();
15486
16073
  }
15487
- if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || isFooterLinksContainer(el)) {
16074
+ if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isFooterLinksContainer(el) || isInferredFooterGroup2(el)) {
15488
16075
  return getNavigationRoot(el);
15489
16076
  }
15490
16077
  return null;
@@ -15699,7 +16286,6 @@ var ICONS = {
15699
16286
  insertUnorderedList: '<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>',
15700
16287
  insertOrderedList: '<line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/>'
15701
16288
  };
15702
- var HEIGHT_SETTLE_DELAYS = [150, 400, 800];
15703
16289
  var SELECTION_CHROME_GAP2 = 4;
15704
16290
  var TOOLBAR_STROKE_GAP2 = 4;
15705
16291
  var TOOLBAR_OFFSET_FROM_ELEMENT = SELECTION_CHROME_GAP2 + TOOLBAR_STROKE_GAP2;
@@ -16079,6 +16665,7 @@ function StateToggle({
16079
16665
  );
16080
16666
  }
16081
16667
  var contentCache = /* @__PURE__ */ new Map();
16668
+ var fetchedContentPaths = /* @__PURE__ */ new Set();
16082
16669
  var brandingCache = /* @__PURE__ */ new Map();
16083
16670
  var OHW_LOADER_STYLE = {
16084
16671
  position: "fixed",
@@ -16608,13 +17195,6 @@ function OhhwellsBridge() {
16608
17195
  const [isItemDragging, setIsItemDragging] = (0, import_react17.useState)(false);
16609
17196
  const [isFooterFrameSelection, setIsFooterFrameSelection] = (0, import_react17.useState)(false);
16610
17197
  isFooterFrameSelectionRef.current = isFooterFrameSelection;
16611
- const [floatingPanel, setFloatingPanel] = (0, import_react17.useState)(null);
16612
- const floatingPanelOpenRef = (0, import_react17.useRef)(false);
16613
- floatingPanelOpenRef.current = floatingPanel !== null;
16614
- const [floatingPanelPos, setFloatingPanelPos] = (0, import_react17.useState)(null);
16615
- const [logoSizeDraft, setLogoSizeDraft] = (0, import_react17.useState)(null);
16616
- const [editorViewport, setEditorViewport] = (0, import_react17.useState)("desktop");
16617
- const [parentScrollSnap, setParentScrollSnap] = (0, import_react17.useState)(null);
16618
17198
  const [navDropdownPreviewOpen, setNavDropdownPreviewOpen] = (0, import_react17.useState)(null);
16619
17199
  const [footerHeadingVisible, setFooterHeadingVisible] = (0, import_react17.useState)(null);
16620
17200
  const footerDragRef = (0, import_react17.useRef)(null);
@@ -16632,6 +17212,13 @@ function OhhwellsBridge() {
16632
17212
  const brandKitRef = (0, import_react17.useRef)("");
16633
17213
  const stylesRef = (0, import_react17.useRef)("");
16634
17214
  const pendingDeleteUndoRef = (0, import_react17.useRef)(null);
17215
+ const [floatingPanel, setFloatingPanel] = (0, import_react17.useState)(null);
17216
+ const floatingPanelOpenRef = (0, import_react17.useRef)(false);
17217
+ const setFloatingPanelRef = (0, import_react17.useRef)(setFloatingPanel);
17218
+ const [floatingPanelPos, setFloatingPanelPos] = (0, import_react17.useState)(null);
17219
+ const [logoSizeDraft, setLogoSizeDraft] = (0, import_react17.useState)(null);
17220
+ const [editorViewport, setEditorViewport] = (0, import_react17.useState)("desktop");
17221
+ const [parentScrollSnap, setParentScrollSnap] = (0, import_react17.useState)(null);
16635
17222
  const [sitePages, setSitePages] = (0, import_react17.useState)([]);
16636
17223
  const [sectionsByPath, setSectionsByPath] = (0, import_react17.useState)({});
16637
17224
  const sectionsPrefetchGenRef = (0, import_react17.useRef)(0);
@@ -16640,7 +17227,18 @@ function OhhwellsBridge() {
16640
17227
  const linkPopoverOpenRef = (0, import_react17.useRef)(false);
16641
17228
  const linkPopoverGraceUntilRef = (0, import_react17.useRef)(0);
16642
17229
  setLinkPopoverRef.current = setLinkPopover;
17230
+ setFloatingPanelRef.current = setFloatingPanel;
16643
17231
  linkPopoverSessionRef.current = linkPopover;
17232
+ floatingPanelOpenRef.current = Boolean(floatingPanel);
17233
+ (0, import_react17.useEffect)(() => {
17234
+ const syncViewport = () => {
17235
+ const next = window.innerWidth <= 480 ? "mobile" : "desktop";
17236
+ setEditorViewport((prev) => prev === next ? prev : next);
17237
+ };
17238
+ syncViewport();
17239
+ window.addEventListener("resize", syncViewport);
17240
+ return () => window.removeEventListener("resize", syncViewport);
17241
+ }, []);
16644
17242
  const {
16645
17243
  navDragRef,
16646
17244
  navDropSlots,
@@ -17965,17 +18563,19 @@ function OhhwellsBridge() {
17965
18563
  }
17966
18564
  if (typeof content[STYLE_STORE_KEY] === "string") {
17967
18565
  stylesRef.current = content[STYLE_STORE_KEY];
18566
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
17968
18567
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17969
18568
  }
17970
18569
  applyBrandChrome(content);
18570
+ initSectionInstancesFromContent(content, window.location.pathname);
17971
18571
  for (const [key, val] of Object.entries(content)) {
17972
18572
  if (key === "__ohw_sections") continue;
17973
18573
  if (key === AI_SECTIONS_KEY) continue;
18574
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
18575
+ if (LOGO_IMAGE_KEYS.includes(key)) continue;
17974
18576
  if (key === BRAND_KIT_KEY) continue;
17975
18577
  if (key === STYLE_STORE_KEY) continue;
17976
18578
  if (BRAND_CHROME_KEYS.has(key)) continue;
17977
- if (key === LOGO_PLACEHOLDER_KEY) continue;
17978
- if (LOGO_IMAGE_KEYS.includes(key)) continue;
17979
18579
  if (applyVideoSettingNode(key, val)) continue;
17980
18580
  if (applyCarouselNode(key, val)) continue;
17981
18581
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -18003,6 +18603,8 @@ function OhhwellsBridge() {
18003
18603
  }
18004
18604
  } else if (el.dataset.ohwEditable === "link") {
18005
18605
  applyLinkHref(el, val);
18606
+ } else if (el.dataset.ohwEditable === "map") {
18607
+ applyMapQuery(el, val);
18006
18608
  } else if (el.dataset.ohwEditable === "icon") {
18007
18609
  applyIconMarkup(el, val);
18008
18610
  } else if (el.dataset.ohwEditable === "form") {
@@ -18023,7 +18625,6 @@ function OhhwellsBridge() {
18023
18625
  if (isEditModeRef.current) requestMissingSocialIconsRef.current();
18024
18626
  enforceLinkHrefs();
18025
18627
  initSectionsFromContent(content, true);
18026
- initSectionInstancesFromContent(content, window.location.pathname);
18027
18628
  sectionsLoadedRef.current = true;
18028
18629
  pendingScheduleConfigRequests.current.splice(0).forEach(processConfigRequest);
18029
18630
  if (imageLoads.length === 0) return Promise.resolve();
@@ -18042,7 +18643,9 @@ function OhhwellsBridge() {
18042
18643
  let cancelled = false;
18043
18644
  setFetchState("loading");
18044
18645
  const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
18045
- fetch(`${apiUrl}/api/public/sites/${subdomain}/content`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18646
+ const initialPath = pathname;
18647
+ fetchedContentPaths.add(`${subdomain}::${initialPath}`);
18648
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(initialPath)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18046
18649
  if (cancelled) return;
18047
18650
  const content = data?.content ?? {};
18048
18651
  const branding = Boolean(data?.showBranding);
@@ -18176,16 +18779,17 @@ function OhhwellsBridge() {
18176
18779
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
18177
18780
  }
18178
18781
  if (typeof content[STYLE_STORE_KEY] === "string") {
18782
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
18179
18783
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
18180
18784
  }
18181
18785
  for (const [key, val] of Object.entries(content)) {
18182
18786
  if (key === "__ohw_sections") continue;
18183
18787
  if (key === AI_SECTIONS_KEY) continue;
18788
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
18789
+ if (LOGO_IMAGE_KEYS.includes(key)) continue;
18184
18790
  if (key === BRAND_KIT_KEY) continue;
18185
18791
  if (key === STYLE_STORE_KEY) continue;
18186
18792
  if (BRAND_CHROME_KEYS.has(key)) continue;
18187
- if (key === LOGO_PLACEHOLDER_KEY) continue;
18188
- if (LOGO_IMAGE_KEYS.includes(key)) continue;
18189
18793
  if (applyVideoSettingNode(key, val)) continue;
18190
18794
  if (applyCarouselNode(key, val)) continue;
18191
18795
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -18200,6 +18804,8 @@ function OhhwellsBridge() {
18200
18804
  if (video && video.src !== val) applyVideoSrc(video, val);
18201
18805
  } else if (el.dataset.ohwEditable === "link") {
18202
18806
  applyLinkHref(el, val);
18807
+ } else if (el.dataset.ohwEditable === "map") {
18808
+ applyMapQuery(el, val);
18203
18809
  } else if (el.dataset.ohwEditable === "form") {
18204
18810
  } else if (isIconMarkupValue(val)) {
18205
18811
  } else if (el.innerHTML !== val) {
@@ -18231,6 +18837,17 @@ function OhhwellsBridge() {
18231
18837
  debounceTimer = setTimeout(applyFromCache, 150);
18232
18838
  };
18233
18839
  applyFromCache();
18840
+ const pathCacheKey = `${subdomain}::${pathname}`;
18841
+ if (!fetchedContentPaths.has(pathCacheKey)) {
18842
+ fetchedContentPaths.add(pathCacheKey);
18843
+ const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
18844
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(pathname)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18845
+ if (!data?.content) return;
18846
+ contentCache.set(subdomain, data.content);
18847
+ applyFromCache();
18848
+ }).catch(() => {
18849
+ });
18850
+ }
18234
18851
  observer = new MutationObserver(scheduleApply);
18235
18852
  observer.observe(document.body, { childList: true, subtree: true });
18236
18853
  return () => {
@@ -18346,26 +18963,11 @@ function OhhwellsBridge() {
18346
18963
  const t2 = setTimeout(measure, 500);
18347
18964
  const ro = new ResizeObserver(schedule);
18348
18965
  ro.observe(document.body);
18349
- let lastWidth = window.innerWidth;
18350
- let resizeTimers = [];
18351
- const clearResizeTimers = () => {
18352
- resizeTimers.forEach(clearTimeout);
18353
- resizeTimers = [];
18354
- };
18355
- const handleResize = () => {
18356
- if (window.innerWidth === lastWidth) return;
18357
- lastWidth = window.innerWidth;
18358
- clearResizeTimers();
18359
- resizeTimers = HEIGHT_SETTLE_DELAYS.map((delay) => setTimeout(measure, delay));
18360
- };
18361
- window.addEventListener("resize", handleResize);
18362
18966
  return () => {
18363
18967
  clearTimeout(t1);
18364
18968
  clearTimeout(t2);
18365
18969
  if (raf != null) cancelAnimationFrame(raf);
18366
18970
  ro.disconnect();
18367
- clearResizeTimers();
18368
- window.removeEventListener("resize", handleResize);
18369
18971
  };
18370
18972
  }, [pathname, isEditMode, postToParent2]);
18371
18973
  (0, import_react17.useEffect)(() => {
@@ -18409,15 +19011,6 @@ function OhhwellsBridge() {
18409
19011
  [style*="100vh"] { min-height: ${initialVh}px !important; height: ${initialVh}px !important; }
18410
19012
  [style*="100svh"] { min-height: ${initialVh}px !important; height: ${initialVh}px !important; }
18411
19013
  [style*="100dvh"] { min-height: ${initialVh}px !important; height: ${initialVh}px !important; }
18412
- /* A section written as min-height: var(--ohw-canvas-h, 100svh) \u2014 the documented way to
18413
- build a full-viewport section that still grows for its own content \u2014 matches the three
18414
- rules above on substring alone, since the fallback text contains "100svh" too. Forcing
18415
- a literal height on top of that turns "at least one screen" into "exactly one screen",
18416
- so content taller than one screen (a long mobile hero, say) overflows a centered flex
18417
- column upward, under whatever sits above it. Only the min-height half belongs to it. */
18418
- [style*="min-height"][style*="100vh"],
18419
- [style*="min-height"][style*="100svh"],
18420
- [style*="min-height"][style*="100dvh"] { height: auto !important; }
18421
19014
  /* Emptied text keeps somewhere to click. A label typed down to nothing collapses to a
18422
19015
  couple of pixels, and getting back into it meant hunting for the caret with the mouse.
18423
19016
  Edit mode only \u2014 the published page shows nothing where there is nothing (OHH-736). */
@@ -18620,9 +19213,6 @@ function OhhwellsBridge() {
18620
19213
  if (target.closest("[data-ohw-state-toggle]")) return;
18621
19214
  if (target.closest("[data-ohw-max-badge]")) return;
18622
19215
  if (isInsideLinkEditor(target)) return;
18623
- if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
18624
- clearMediaSelectionRef.current();
18625
- }
18626
19216
  if (isInsideFloatingPanel(target)) return;
18627
19217
  if (target.closest("[data-ohw-form-toolbar]")) return;
18628
19218
  if (target.closest(
@@ -18630,6 +19220,9 @@ function OhhwellsBridge() {
18630
19220
  )) {
18631
19221
  return;
18632
19222
  }
19223
+ if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
19224
+ clearMediaSelectionRef.current();
19225
+ }
18633
19226
  {
18634
19227
  const formEl = getFormElement(target);
18635
19228
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
@@ -18781,14 +19374,6 @@ function OhhwellsBridge() {
18781
19374
  }
18782
19375
  const clickedButton = findClosestButtonLike(target);
18783
19376
  const buttonOnMedia = Boolean(clickedButton && isMediaEditable(editable) && editable.contains(clickedButton));
18784
- console.log("[click-debug]", {
18785
- editableType: editable.dataset.ohwEditable,
18786
- editableTag: editable.tagName,
18787
- targetTag: target.tagName,
18788
- clickedButtonTag: clickedButton?.tagName ?? null,
18789
- buttonOnMedia,
18790
- isMediaEditableEditable: isMediaEditable(editable)
18791
- });
18792
19377
  if (isMediaEditable(editable) && !buttonOnMedia) {
18793
19378
  e.preventDefault();
18794
19379
  e.stopPropagation();
@@ -18815,11 +19400,6 @@ function OhhwellsBridge() {
18815
19400
  const hrefLookupTarget = buttonOnMedia ? clickedButton : editable;
18816
19401
  const hrefCtx = getHrefKeyFromElement(hrefLookupTarget);
18817
19402
  const navAnchor = hrefCtx ? getNavigationItemAnchor(hrefCtx.anchor) : null;
18818
- console.log("[click-debug 2]", {
18819
- hrefLookupTargetTag: hrefLookupTarget.tagName,
18820
- hrefCtx: hrefCtx ? { key: hrefCtx.key } : null,
18821
- navAnchorTag: navAnchor?.tagName ?? null
18822
- });
18823
19403
  if (navAnchor) {
18824
19404
  e.preventDefault();
18825
19405
  e.stopPropagation();
@@ -18989,6 +19569,9 @@ function OhhwellsBridge() {
18989
19569
  setHoveredItemRect(null);
18990
19570
  hoveredNavContainerRef.current = null;
18991
19571
  setHoveredNavContainerRect(null);
19572
+ siblingHintElRef.current = null;
19573
+ setSiblingHintRect(null);
19574
+ setSiblingHintRects([]);
18992
19575
  return;
18993
19576
  }
18994
19577
  {
@@ -19107,7 +19690,6 @@ function OhhwellsBridge() {
19107
19690
  hoveredNavContainerRef.current = null;
19108
19691
  setHoveredNavContainerRect(null);
19109
19692
  hoveredItemElRef.current = editable;
19110
- setHoveredItemRect(isSelectedForHoverRef.current(editable) ? null : editable.getBoundingClientRect());
19111
19693
  }
19112
19694
  }
19113
19695
  }
@@ -19404,7 +19986,7 @@ function OhhwellsBridge() {
19404
19986
  }
19405
19987
  };
19406
19988
  const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false) => {
19407
- if (linkPopoverOpenRef.current) {
19989
+ if (linkPopoverOpenRef.current || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
19408
19990
  if (hoveredImageRef.current) {
19409
19991
  hoveredImageRef.current = null;
19410
19992
  hoveredImageHasTextOverlapRef.current = false;
@@ -19769,8 +20351,7 @@ function OhhwellsBridge() {
19769
20351
  };
19770
20352
  const handleMouseMove = (e) => {
19771
20353
  const { clientX, clientY } = e;
19772
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
19773
- if (isOverEditorChrome(clientX, clientY)) {
20354
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY) || isOverEditorChrome(clientX, clientY)) {
19774
20355
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
19775
20356
  formHoverElRef.current = null;
19776
20357
  setFormHoverRect(null);
@@ -19778,6 +20359,12 @@ function OhhwellsBridge() {
19778
20359
  setHoveredItemRect(null);
19779
20360
  hoveredNavContainerRef.current = null;
19780
20361
  setHoveredNavContainerRect(null);
20362
+ siblingHintElRef.current = null;
20363
+ setSiblingHintRect(null);
20364
+ setSiblingHintRects([]);
20365
+ dismissImageHover();
20366
+ clearImageHover();
20367
+ setSectionGap(null);
19781
20368
  return;
19782
20369
  }
19783
20370
  if (probeSocialsRowAt(clientX, clientY)) return;
@@ -19789,7 +20376,11 @@ function OhhwellsBridge() {
19789
20376
  if (e.data?.type !== "ow:pointer-sync") return;
19790
20377
  const { clientX, clientY } = e.data;
19791
20378
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
19792
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
20379
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
20380
+ dismissImageHover();
20381
+ clearImageHover();
20382
+ return;
20383
+ }
19793
20384
  if (probeSocialsRowAt(clientX, clientY)) return;
19794
20385
  probeSectionGapAt(clientX, clientY);
19795
20386
  probeImageAt(clientX, clientY);
@@ -20068,6 +20659,44 @@ function OhhwellsBridge() {
20068
20659
  if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
20069
20660
  }, 400));
20070
20661
  };
20662
+ const reapCommittedAiSections = (excludeIds) => {
20663
+ const aiState = parseAiSectionsState(aiSectionsRef.current);
20664
+ if (aiState.sections.length === 0) return [];
20665
+ let orderEntries = [];
20666
+ try {
20667
+ const parsed = JSON.parse(editContentRef.current[SECTION_ORDER_KEY] ?? "[]");
20668
+ if (Array.isArray(parsed)) orderEntries = parsed;
20669
+ } catch {
20670
+ return [];
20671
+ }
20672
+ const removedIds = orderEntries.filter((entry) => entry && typeof entry.instanceId === "string" && entry.removed).map((entry) => entry.instanceId);
20673
+ if (removedIds.length === 0) return [];
20674
+ const result = reapRemovedAiSections(aiState, parseStyleStore(stylesRef.current), removedIds, excludeIds);
20675
+ if (!result.changed) return [];
20676
+ const nodes = [];
20677
+ aiSectionsRef.current = serializeAiSectionsState(result.state);
20678
+ nodes.push({ key: AI_SECTIONS_KEY, text: aiSectionsRef.current });
20679
+ const reaped = new Set(result.reapedIds);
20680
+ const nextOrderJson = JSON.stringify(orderEntries.filter((entry) => !reaped.has(entry.instanceId)));
20681
+ editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: nextOrderJson };
20682
+ setAiSectionOrder(nextOrderJson, window.location.pathname);
20683
+ nodes.push({ key: SECTION_ORDER_KEY, text: nextOrderJson });
20684
+ if (result.store) {
20685
+ stylesRef.current = JSON.stringify(result.store);
20686
+ nodes.push({ key: STYLE_STORE_KEY, text: stylesRef.current });
20687
+ }
20688
+ const nextContent = { ...editContentRef.current };
20689
+ for (const key of Object.keys(nextContent)) {
20690
+ if (key.startsWith(AI_SLOT_KEY_PREFIX) && result.slotPrefixes.some((prefix) => key.startsWith(prefix)) && nextContent[key] !== "") {
20691
+ nextContent[key] = "";
20692
+ nodes.push({ key, text: "" });
20693
+ }
20694
+ }
20695
+ editContentRef.current = nextContent;
20696
+ applyAiSectionsToDom(result.state);
20697
+ applyStylesToDom(parseStyleStore(stylesRef.current));
20698
+ return nodes;
20699
+ };
20071
20700
  const handleHydrate = (e) => {
20072
20701
  if (e.data?.type !== "ow:hydrate") return;
20073
20702
  const content = e.data.content;
@@ -20086,9 +20715,11 @@ function OhhwellsBridge() {
20086
20715
  }
20087
20716
  if (typeof content[STYLE_STORE_KEY] === "string") {
20088
20717
  stylesRef.current = content[STYLE_STORE_KEY];
20718
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
20089
20719
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
20090
20720
  }
20091
20721
  applyBrandChrome(content);
20722
+ initSectionInstancesFromContent(content, window.location.pathname);
20092
20723
  let sectionsJson = null;
20093
20724
  for (const [key, val] of Object.entries(content)) {
20094
20725
  if (key === "__ohw_sections") {
@@ -20096,11 +20727,11 @@ function OhhwellsBridge() {
20096
20727
  continue;
20097
20728
  }
20098
20729
  if (key === AI_SECTIONS_KEY) continue;
20730
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
20731
+ if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
20099
20732
  if (key === BRAND_KIT_KEY) continue;
20100
20733
  if (key === STYLE_STORE_KEY) continue;
20101
20734
  if (BRAND_CHROME_KEYS.has(key)) continue;
20102
- if (key === LOGO_PLACEHOLDER_KEY) continue;
20103
- if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
20104
20735
  if (applyVideoSettingNode(key, val)) continue;
20105
20736
  if (applyCarouselNode(key, val)) continue;
20106
20737
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -20114,6 +20745,8 @@ function OhhwellsBridge() {
20114
20745
  if (video && video.src !== val) applyVideoSrc(video, val);
20115
20746
  } else if (el.dataset.ohwEditable === "link") {
20116
20747
  applyLinkHref(el, val);
20748
+ } else if (el.dataset.ohwEditable === "map") {
20749
+ applyMapQuery(el, val);
20117
20750
  } else if (el.dataset.ohwEditable === "icon") {
20118
20751
  applyIconMarkup(el, val);
20119
20752
  } else if (isIconMarkupValue(val)) {
@@ -20130,12 +20763,16 @@ function OhhwellsBridge() {
20130
20763
  sectionsLoadedRef.current = true;
20131
20764
  pendingScheduleConfigRequests.current.splice(0).forEach(processConfigRequest);
20132
20765
  }
20133
- initSectionInstancesFromContent(content, window.location.pathname);
20134
20766
  editContentRef.current = { ...editContentRef.current, ...content };
20135
20767
  reconcileNavbarItemsFromContent(editContentRef.current);
20136
20768
  reconcileFooterOrderFromContent(editContentRef.current);
20137
20769
  syncNavigationDragCursorAttrs();
20138
20770
  enforceLinkHrefs();
20771
+ const hydrateReapExclude = /* @__PURE__ */ new Set();
20772
+ const hydratePendingUndo = pendingDeleteUndoRef.current;
20773
+ if (hydratePendingUndo?.sectionInstanceId) hydrateReapExclude.add(hydratePendingUndo.sectionInstanceId);
20774
+ const reapNodes = reapCommittedAiSections(hydrateReapExclude);
20775
+ if (reapNodes.length > 0) postToParentRef.current({ type: "ow:change", nodes: reapNodes });
20139
20776
  const hydratedHeight = document.body.scrollHeight;
20140
20777
  if (hydratedHeight > 50) postToParentRef.current({ type: "ow:height", height: hydratedHeight });
20141
20778
  postToParentRef.current({ type: "ow:hydrate-done" });
@@ -20275,12 +20912,35 @@ function OhhwellsBridge() {
20275
20912
  window.addEventListener("message", handleAiSetBrand);
20276
20913
  const handleAiSetStyles = (e) => {
20277
20914
  if (e.data?.type !== "ow:ai-set-styles") return;
20278
- const value = typeof e.data.value === "string" ? e.data.value : "";
20915
+ let value = typeof e.data.value === "string" ? e.data.value : "";
20279
20916
  const previous = stylesRef.current;
20917
+ let previousSections;
20918
+ const store = parseStyleStore(value);
20919
+ if (store) {
20920
+ const folded = foldAlignIntoTrees(parseAiSectionsState(aiSectionsRef.current), store);
20921
+ if (folded.changed) {
20922
+ const nextSections = serializeAiSectionsState(folded.state);
20923
+ if (nextSections !== aiSectionsRef.current) {
20924
+ previousSections = aiSectionsRef.current;
20925
+ aiSectionsRef.current = nextSections;
20926
+ applyAiSectionsToDom(folded.state);
20927
+ postToParentRef.current({
20928
+ type: "ow:change",
20929
+ nodes: [{ key: AI_SECTIONS_KEY, text: nextSections }]
20930
+ });
20931
+ }
20932
+ value = JSON.stringify(folded.store);
20933
+ }
20934
+ }
20280
20935
  stylesRef.current = value;
20281
20936
  applyStylesToDom(parseStyleStore(value));
20282
20937
  postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
20283
- postToParentRef.current({ type: "ow:ai-styles-applied", previous, value });
20938
+ postToParentRef.current({
20939
+ type: "ow:ai-styles-applied",
20940
+ previous,
20941
+ value,
20942
+ ...previousSections !== void 0 ? { previousSections } : {}
20943
+ });
20284
20944
  };
20285
20945
  window.addEventListener("message", handleAiSetStyles);
20286
20946
  const handleGetBrand = (e) => {
@@ -20297,8 +20957,11 @@ function OhhwellsBridge() {
20297
20957
  if (!instanceId || !direction) return;
20298
20958
  const entries = moveSectionInstance(instanceId, direction, window.location.pathname);
20299
20959
  if (!entries) return;
20300
- const orderJson = JSON.stringify(entries);
20960
+ const orderJson = JSON.stringify(
20961
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, entries)
20962
+ );
20301
20963
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20964
+ applyPersistedOrder(getPageSectionOrderEntries(orderJson, window.location.pathname));
20302
20965
  setAiSectionOrder(orderJson, window.location.pathname);
20303
20966
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
20304
20967
  window.dispatchEvent(new Event("resize"));
@@ -20317,8 +20980,11 @@ function OhhwellsBridge() {
20317
20980
  const currentEntries = getPageSectionOrderEntries(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname);
20318
20981
  const entries = deleteSectionInstance(instanceId, window.location.pathname, currentEntries);
20319
20982
  if (!entries) return;
20320
- const orderJson = JSON.stringify(entries);
20983
+ const orderJson = JSON.stringify(
20984
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, entries)
20985
+ );
20321
20986
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20987
+ setAiSectionOrder(orderJson, window.location.pathname);
20322
20988
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
20323
20989
  aiSectionApiRef.current?.clear();
20324
20990
  window.dispatchEvent(new Event("resize"));
@@ -20327,6 +20993,7 @@ function OhhwellsBridge() {
20327
20993
  const actionId = newInstanceId();
20328
20994
  pendingDeleteUndoRef.current = {
20329
20995
  actionId,
20996
+ sectionInstanceId: instanceId,
20330
20997
  restore: () => {
20331
20998
  const restoredEntries = getPageSectionOrderEntries(
20332
20999
  editContentRef.current[SECTION_ORDER_KEY],
@@ -20334,8 +21001,11 @@ function OhhwellsBridge() {
20334
21001
  );
20335
21002
  const restored = restoreSectionInstance(instanceId, window.location.pathname, restoredEntries);
20336
21003
  if (!restored) return;
20337
- const restoredJson = JSON.stringify(restored);
21004
+ const restoredJson = JSON.stringify(
21005
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, restored)
21006
+ );
20338
21007
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: restoredJson };
21008
+ setAiSectionOrder(restoredJson, window.location.pathname);
20339
21009
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: restoredJson }] });
20340
21010
  window.dispatchEvent(new Event("resize"));
20341
21011
  const restoreHeight = document.body.scrollHeight;
@@ -20352,6 +21022,37 @@ function OhhwellsBridge() {
20352
21022
  });
20353
21023
  };
20354
21024
  window.addEventListener("message", handleDeleteSection);
21025
+ const handleDuplicateSection = (e) => {
21026
+ if (e.data?.type !== "ow:duplicate-section") return;
21027
+ const instanceId = typeof e.data.instanceId === "string" ? e.data.instanceId : "";
21028
+ if (!instanceId) return;
21029
+ const newId = newInstanceId();
21030
+ const currentEntries = getPageSectionOrderEntries(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname);
21031
+ const result = duplicateSectionInstance(instanceId, newId, window.location.pathname, currentEntries);
21032
+ if (!result) return;
21033
+ const { entries, keyRekeys } = result;
21034
+ const orderJson = JSON.stringify(
21035
+ mergePageSectionOrder(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname, entries)
21036
+ );
21037
+ const nodes = [{ key: SECTION_ORDER_KEY, text: orderJson }];
21038
+ for (const { from, to } of keyRekeys) {
21039
+ const inherited = editContentRef.current[from];
21040
+ if (inherited !== void 0) nodes.push({ key: to, text: inherited });
21041
+ }
21042
+ editContentRef.current = {
21043
+ ...editContentRef.current,
21044
+ ...Object.fromEntries(nodes.map(({ key, text }) => [key, text]))
21045
+ };
21046
+ applyPersistedOrder(getPageSectionOrderEntries(orderJson, window.location.pathname));
21047
+ setAiSectionOrder(orderJson, window.location.pathname);
21048
+ postToParentRef.current({ type: "ow:change", nodes });
21049
+ window.dispatchEvent(new Event("resize"));
21050
+ const duplicateHeight = document.body.scrollHeight;
21051
+ if (duplicateHeight > 50) postToParentRef.current({ type: "ow:height", height: duplicateHeight });
21052
+ const clone = document.querySelector(`[data-ohw-instance="${CSS.escape(newId)}"]`);
21053
+ if (clone) aiSectionApiRef.current?.selectFromElement(clone);
21054
+ };
21055
+ window.addEventListener("message", handleDuplicateSection);
20355
21056
  const handleDeactivate = (e) => {
20356
21057
  if (e.data?.type !== "ow:deactivate") return;
20357
21058
  if (document.documentElement.hasAttribute("data-ohw-panel-dragging")) return;
@@ -20361,6 +21062,12 @@ function OhhwellsBridge() {
20361
21062
  closeLinkPopoverRef.current();
20362
21063
  return;
20363
21064
  }
21065
+ if (floatingPanelOpenRef.current) {
21066
+ setFloatingPanelRef.current(null);
21067
+ deselectRef.current();
21068
+ deactivateRef.current();
21069
+ return;
21070
+ }
20364
21071
  deselectRef.current();
20365
21072
  deactivateRef.current();
20366
21073
  clearMediaSelectionRef.current();
@@ -20606,6 +21313,10 @@ function OhhwellsBridge() {
20606
21313
  };
20607
21314
  const handleSave = (e) => {
20608
21315
  if (e.data?.type !== "ow:save") return;
21316
+ const pendingUndo = pendingDeleteUndoRef.current;
21317
+ const reapExclude = /* @__PURE__ */ new Set();
21318
+ if (pendingUndo?.sectionInstanceId) reapExclude.add(pendingUndo.sectionInstanceId);
21319
+ const reapNodes = reapCommittedAiSections(reapExclude);
20609
21320
  const nodes = collectEditableNodes(editContentRef.current);
20610
21321
  const tracker = document.querySelector("[data-ohw-sections-tracker]");
20611
21322
  if (tracker?.textContent) nodes.push({ key: "__ohw_sections", type: "sections", text: tracker.textContent });
@@ -20625,6 +21336,11 @@ function OhhwellsBridge() {
20625
21336
  const text = specs.length ? JSON.stringify(specs) : editContentRef.current[fieldsKey(formKey)];
20626
21337
  if (text) nodes.push({ key: fieldsKey(formKey), type: "text", text });
20627
21338
  });
21339
+ for (const reapNode of reapNodes) {
21340
+ if (reapNode.key.startsWith(AI_SLOT_KEY_PREFIX) && !nodes.some((n) => n.key === reapNode.key)) {
21341
+ nodes.push({ key: reapNode.key, type: "text", text: reapNode.text });
21342
+ }
21343
+ }
20628
21344
  postToParentRef.current({ type: "ow:save-result", nodes });
20629
21345
  };
20630
21346
  const handleInsertSection = (e) => {
@@ -20635,8 +21351,12 @@ function OhhwellsBridge() {
20635
21351
  if (inserted) {
20636
21352
  const tracker = getSectionsTracker();
20637
21353
  postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
20638
- const h = document.body.scrollHeight;
20639
- if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
21354
+ const reportHeight = () => {
21355
+ const h = document.body.scrollHeight;
21356
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
21357
+ };
21358
+ reportHeight();
21359
+ setTimeout(reportHeight, 500);
20640
21360
  }
20641
21361
  };
20642
21362
  const handleSwitchSchedule = (e) => {
@@ -21038,11 +21758,12 @@ function OhhwellsBridge() {
21038
21758
  window.removeEventListener("message", handleMoveSection);
21039
21759
  window.removeEventListener("message", handlePanelDragging);
21040
21760
  window.removeEventListener("message", handleDeleteSection);
21761
+ window.removeEventListener("message", handleDuplicateSection);
21041
21762
  window.removeEventListener("message", handleDeactivate);
21042
- document.documentElement.removeAttribute("data-ohw-panel-dragging");
21043
21763
  window.removeEventListener("message", handleToastAction);
21044
21764
  window.removeEventListener("message", handleFormCount);
21045
21765
  window.removeEventListener("message", handleUiEscape);
21766
+ document.documentElement.removeAttribute("data-ohw-panel-dragging");
21046
21767
  autoSaveTimers.current.forEach(clearTimeout);
21047
21768
  autoSaveTimers.current.clear();
21048
21769
  if (imageUnhoverTimerRef.current) clearTimeout(imageUnhoverTimerRef.current);
@@ -21245,7 +21966,7 @@ function OhhwellsBridge() {
21245
21966
  postToParent2({
21246
21967
  type: "ow:ready",
21247
21968
  version: "1",
21248
- bridgeVersion: "0.1.85",
21969
+ bridgeVersion: "0.1.87",
21249
21970
  path: pathname,
21250
21971
  nodes: collectEditableNodes(editContentRef.current),
21251
21972
  sections
@@ -22164,6 +22885,59 @@ function OhhwellsBridge() {
22164
22885
  ) : null
22165
22886
  ] });
22166
22887
  }
22888
+
22889
+ // src/ui/EmptySection.tsx
22890
+ var import_link = __toESM(require("next/link"), 1);
22891
+ var import_jsx_runtime34 = require("react/jsx-runtime");
22892
+ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey }) {
22893
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
22894
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
22895
+ "p",
22896
+ {
22897
+ style: {
22898
+ fontFamily: "var(--brand-font-body)",
22899
+ fontSize: "0.75rem",
22900
+ fontWeight: 500,
22901
+ letterSpacing: "0.15em",
22902
+ textTransform: "uppercase",
22903
+ color: "var(--brand-accent)",
22904
+ marginBottom: "1.5rem"
22905
+ },
22906
+ children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_link.default, { href: homeHref, style: { color: "inherit" }, children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { ...eyebrowKey ? { "data-ohw-editable": "text", "data-ohw-key": eyebrowKey } : {}, children: "Home" }) })
22907
+ }
22908
+ ),
22909
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
22910
+ "h1",
22911
+ {
22912
+ style: {
22913
+ fontFamily: "var(--brand-font-heading)",
22914
+ fontSize: "clamp(2rem, 3.5vw, 2.75rem)",
22915
+ lineHeight: 1.1,
22916
+ letterSpacing: "-0.025em",
22917
+ color: "var(--brand-text)",
22918
+ marginBottom: "1rem"
22919
+ },
22920
+ ...titleKey ? { "data-ohw-editable": "text", "data-ohw-key": titleKey, "data-ohw-max-length": 80 } : {},
22921
+ children: title
22922
+ }
22923
+ ),
22924
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
22925
+ "p",
22926
+ {
22927
+ style: {
22928
+ fontFamily: "var(--brand-font-body)",
22929
+ fontSize: "1rem",
22930
+ lineHeight: 1.7,
22931
+ fontWeight: 300,
22932
+ color: "var(--brand-text-muted)",
22933
+ maxWidth: "340px"
22934
+ },
22935
+ ...subtitleKey ? { "data-ohw-editable": "text", "data-ohw-key": subtitleKey, "data-ohw-max-length": 160 } : {},
22936
+ children: "This page doesn't have any content yet."
22937
+ }
22938
+ )
22939
+ ] });
22940
+ }
22167
22941
  // Annotate the CommonJS export names for ESM import in node:
22168
22942
  0 && (module.exports = {
22169
22943
  AI_DEFAULT_BRAND,
@@ -22181,6 +22955,7 @@ function OhhwellsBridge() {
22181
22955
  DropdownMenuItem,
22182
22956
  DropdownMenuSeparator,
22183
22957
  DropdownMenuTrigger,
22958
+ EmptySection,
22184
22959
  ItemActionToolbar,
22185
22960
  ItemInteractionLayer,
22186
22961
  LinkEditorPanel,