@ohhwells/bridge 0.1.85 → 0.1.86-next.256

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,494 @@ 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 rekeySectionSubtree(root, instanceId) {
841
+ const suffix = `::${instanceId}`;
842
+ const pairs = [];
843
+ const rekey = (el, attr) => {
844
+ const current = el.getAttribute(attr);
845
+ if (!current) return;
846
+ const base = current.includes("::") ? current.slice(0, current.indexOf("::")) : current;
847
+ const next = `${base}${suffix}`;
848
+ el.setAttribute(attr, next);
849
+ pairs.push({ from: current, to: next });
850
+ };
851
+ if (root.hasAttribute("data-ohw-key")) rekey(root, "data-ohw-key");
852
+ if (root.hasAttribute("data-ohw-href-key")) rekey(root, "data-ohw-href-key");
853
+ root.querySelectorAll("[data-ohw-key]").forEach((el) => rekey(el, "data-ohw-key"));
854
+ root.querySelectorAll("[data-ohw-href-key]").forEach((el) => rekey(el, "data-ohw-href-key"));
855
+ return pairs;
856
+ }
857
+ function initSectionInstancesFromContent(content, currentPath) {
858
+ document.querySelectorAll("[data-ohw-section]:not([data-ohw-instance])").forEach((el) => {
859
+ el.setAttribute("data-ohw-instance", el.getAttribute("data-ohw-section") ?? "");
860
+ });
861
+ const entries = getPageSectionOrderEntries(content[SECTION_ORDER_KEY], currentPath);
862
+ for (const entry of entries) {
863
+ if (entry.instanceId === entry.type) continue;
864
+ if (document.querySelector(`[data-ohw-instance="${CSS.escape(entry.instanceId)}"]`)) continue;
865
+ const original = document.querySelector(
866
+ `[data-ohw-section="${CSS.escape(entry.type)}"][data-ohw-instance="${CSS.escape(entry.type)}"]`
867
+ );
868
+ if (!original) continue;
869
+ const clone = original.cloneNode(true);
870
+ clone.setAttribute("data-ohw-instance", entry.instanceId);
871
+ rekeySectionSubtree(clone, entry.instanceId);
872
+ original.insertAdjacentElement("afterend", clone);
873
+ }
874
+ applyPersistedOrder(entries);
875
+ }
876
+
544
877
  // src/ui/ai-tree/AiTreeRenderer.tsx
545
878
  var import_react = __toESM(require("react"), 1);
546
879
  var import_lucide_react = require("lucide-react");
880
+
881
+ // src/lib/placeholder-imagery.ts
882
+ var U = (id) => `https://images.unsplash.com/photo-${id}?auto=format&fit=crop&w=1600&q=80`;
883
+ var GENERIC = [
884
+ U("1441986300917-64674bd600d8"),
885
+ U("1486406146926-c627a92ad1ab"),
886
+ U("1497032628192-86f99bcd76bc"),
887
+ U("1521737604893-d14cc237f11d"),
888
+ U("1522071820081-009f0129c71c"),
889
+ U("1519389950473-47ba0277781c"),
890
+ U("1460925895917-afdab827c52f"),
891
+ U("1504384308090-c894fdcc538d")
892
+ ];
893
+ var PEOPLE = [
894
+ U("1500648767791-00dcc994a43e"),
895
+ U("1494790108377-be9c29b29330"),
896
+ U("1507003211169-0a1dd7228f2d"),
897
+ U("1438761681033-6461ffad8d80"),
898
+ U("1544005313-94ddf0286df2"),
899
+ U("1472099645785-5658abf4ff4e"),
900
+ U("1519085360753-af0119f7cbe7"),
901
+ U("1534528741775-53994a69daeb")
902
+ ];
903
+ var THEMED = [
904
+ {
905
+ keywords: ["portrait", "headshot", "person", "people", "team", "staff", "avatar", "founder", "face"],
906
+ pool: PEOPLE
907
+ },
908
+ {
909
+ keywords: ["pet", "dog", "cat", "puppy", "kitten", "vet", "animal"],
910
+ pool: [
911
+ U("1548199973-03cce0bbc87b"),
912
+ U("1450778869180-41d0601e046e"),
913
+ U("1583511655857-d19b40a7a54e"),
914
+ U("1587300003388-59208cc962cb"),
915
+ U("1517849845537-4d257902454a"),
916
+ U("1601758228041-f3b2795255f1")
917
+ ]
918
+ },
919
+ {
920
+ keywords: [
921
+ "baker",
922
+ "bakery",
923
+ "cafe",
924
+ "coffee",
925
+ "latte",
926
+ "restaurant",
927
+ "pastr",
928
+ "bread",
929
+ "cake",
930
+ "cater",
931
+ "chef",
932
+ "kitchen",
933
+ "food",
934
+ "pizza",
935
+ "dessert",
936
+ "brunch",
937
+ "bistro",
938
+ "deli",
939
+ "dish",
940
+ "menu"
941
+ ],
942
+ pool: [
943
+ U("1509440159596-0249088772ff"),
944
+ U("1555507036-ab1f4038808a"),
945
+ U("1517433670267-08bbd4be890f"),
946
+ U("1486427944299-d1955d23e34d"),
947
+ U("1504754524776-8f4f37790ca0"),
948
+ U("1495474472287-4d71bcdd2085"),
949
+ U("1521017432531-fbd92d768814"),
950
+ U("1556909114-f6e7ad7d3136")
951
+ ]
952
+ },
953
+ {
954
+ keywords: [
955
+ "shop",
956
+ "store",
957
+ "boutique",
958
+ "retail",
959
+ "clothing",
960
+ "fashion",
961
+ "jewel",
962
+ "gift",
963
+ "florist",
964
+ "market",
965
+ "grocer",
966
+ "product",
967
+ "storefront"
968
+ ],
969
+ pool: [
970
+ U("1441984904996-e0b6ba687e04"),
971
+ U("1472851294608-062f824d29cc"),
972
+ U("1523381210434-271e8be1f52b"),
973
+ U("1534452203293-494d7ddbf7e0"),
974
+ U("1445205170230-053b83016050"),
975
+ U("1560243563-062bfc001d68")
976
+ ]
977
+ },
978
+ {
979
+ keywords: [
980
+ "yoga",
981
+ "pilates",
982
+ "fitness",
983
+ "gym",
984
+ "workout",
985
+ "trainer",
986
+ "wellness",
987
+ "meditat",
988
+ "massage",
989
+ "therap",
990
+ "physio",
991
+ "chiro",
992
+ "nutrition",
993
+ "spa",
994
+ "studio"
995
+ ],
996
+ pool: [
997
+ U("1544367567-0f2fcb009e0b"),
998
+ U("1506126613408-eca07ce68773"),
999
+ U("1545205597-3d9d02c29597"),
1000
+ U("1552196563-55cd4e45efb3"),
1001
+ U("1518611012118-696072aa579a"),
1002
+ U("1571019613454-1cb2f99b2d8b"),
1003
+ U("1540555700478-4be289fbecef"),
1004
+ U("1519824145371-296894a0daa9")
1005
+ ]
1006
+ },
1007
+ {
1008
+ keywords: [
1009
+ "salon",
1010
+ "hairdress",
1011
+ "haircut",
1012
+ "barber",
1013
+ "manicure",
1014
+ "pedicure",
1015
+ "nails",
1016
+ "beauty",
1017
+ "makeup",
1018
+ "cosmetic",
1019
+ "eyelash",
1020
+ "eyebrow",
1021
+ "skincare",
1022
+ "esthetic",
1023
+ "waxing",
1024
+ "hair"
1025
+ ],
1026
+ pool: [
1027
+ U("1560066984-138dadb4c035"),
1028
+ U("1522337660859-02fbefca4702"),
1029
+ U("1562322140-8baeececf3df"),
1030
+ U("1521590832167-7bcbfaa6381f"),
1031
+ U("1487412947147-5cebf100ffc2"),
1032
+ U("1526045478516-99145907023c")
1033
+ ]
1034
+ },
1035
+ {
1036
+ keywords: [
1037
+ "cleaning",
1038
+ "plumb",
1039
+ "electric",
1040
+ "landscap",
1041
+ "contractor",
1042
+ "handyman",
1043
+ "renov",
1044
+ "hvac",
1045
+ "roofing",
1046
+ "painting",
1047
+ "carpentry",
1048
+ "flooring",
1049
+ "movers",
1050
+ "construction",
1051
+ "tools"
1052
+ ],
1053
+ pool: [
1054
+ U("1581578731548-c64695cc6952"),
1055
+ U("1504307651254-35680f356dfd"),
1056
+ U("1581092160562-40aa08e78837"),
1057
+ U("1621905251189-08b45d6a269e"),
1058
+ U("1558618666-fcd25c85cd64"),
1059
+ U("1585128792020-803d29415281")
1060
+ ]
1061
+ },
1062
+ {
1063
+ keywords: [
1064
+ "legal",
1065
+ "attorney",
1066
+ "lawyer",
1067
+ "account",
1068
+ "bookkeep",
1069
+ "consult",
1070
+ "coaching",
1071
+ "financ",
1072
+ "insurance",
1073
+ "realtor",
1074
+ "estate",
1075
+ "marketing",
1076
+ "agency",
1077
+ "office",
1078
+ "business",
1079
+ "desk"
1080
+ ],
1081
+ pool: [
1082
+ U("1497366216548-37526070297c"),
1083
+ U("1497366811353-6870744d04b2"),
1084
+ U("1454165804606-c3d57bc86b40"),
1085
+ U("1521791136064-7986c2920216"),
1086
+ U("1556761175-b413da4baf72"),
1087
+ U("1542744173-8e7e53415bb0")
1088
+ ]
1089
+ },
1090
+ {
1091
+ keywords: [
1092
+ "wedding",
1093
+ "event",
1094
+ "party",
1095
+ "celebrat",
1096
+ "venue",
1097
+ "community",
1098
+ "nonprofit",
1099
+ "charity",
1100
+ "workshop",
1101
+ "photograph",
1102
+ "concert"
1103
+ ],
1104
+ pool: [
1105
+ U("1511578314322-379afb476865"),
1106
+ U("1501281668745-f7f57925c3b4"),
1107
+ U("1523580494863-6f3031224c94"),
1108
+ U("1540575467063-178a50c2df87"),
1109
+ U("1505236858219-8359eb29e329"),
1110
+ U("1528605248644-14dd04022da1")
1111
+ ]
1112
+ }
1113
+ ];
1114
+ function poolForSubject(subject) {
1115
+ for (const theme of THEMED) {
1116
+ if (theme.keywords.some((k) => subject.includes(k))) {
1117
+ return theme.pool;
1118
+ }
1119
+ }
1120
+ return GENERIC;
1121
+ }
1122
+ function mixedHash(text) {
1123
+ let hash = 2166136261;
1124
+ for (let i = 0; i < text.length; i++) {
1125
+ hash ^= text.charCodeAt(i);
1126
+ hash = Math.imul(hash, 16777619);
1127
+ }
1128
+ return hash >>> 16 & 65535;
1129
+ }
1130
+ function resolvePlaceholderRef(ref) {
1131
+ const match = /^placeholder:([a-z0-9-]+)$/.exec(ref);
1132
+ if (!match) return null;
1133
+ const subject = match[1];
1134
+ const pool = poolForSubject(subject.replace(/-\d+$/, ""));
1135
+ return pool[mixedHash(ref) % pool.length];
1136
+ }
1137
+ function collectPlaceholderRefs(tree) {
1138
+ const seen = /* @__PURE__ */ new Set();
1139
+ for (const match of JSON.stringify(tree ?? null).matchAll(/"(placeholder:[a-z0-9-]+)"/gu)) {
1140
+ seen.add(match[1]);
1141
+ }
1142
+ return [...seen];
1143
+ }
1144
+ function buildPlaceholderMap(tree) {
1145
+ const map = {};
1146
+ const cursor = /* @__PURE__ */ new Map();
1147
+ for (const ref of collectPlaceholderRefs(tree)) {
1148
+ const subject = ref.slice("placeholder:".length).replace(/-\d+$/, "");
1149
+ const pool = poolForSubject(subject);
1150
+ const start = cursor.get(pool) ?? mixedHash(ref) % pool.length;
1151
+ map[ref] = pool[start % pool.length];
1152
+ cursor.set(pool, start + 1);
1153
+ }
1154
+ return map;
1155
+ }
1156
+
1157
+ // src/ui/ai-tree/AiTreeRenderer.tsx
547
1158
  var import_jsx_runtime = require("react/jsx-runtime");
548
1159
  function lucideByName(name) {
549
1160
  const pascal = name.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
@@ -557,6 +1168,7 @@ var typeStyle = (spec, font) => ({
557
1168
  fontWeight: spec.weight
558
1169
  });
559
1170
  var str = (value) => typeof value === "string" ? value : "";
1171
+ var cardRadius = (slots) => slots.cornerStyle === "sharp" ? 0 : AI_TREE_TOKENS.radiusCard;
560
1172
  var featureLines = (value) => value.split(/\n|<br\s*\/?>/i).map((line) => line.trim()).filter(Boolean);
561
1173
  var CHECK_MASK = `data:image/svg+xml,${encodeURIComponent(
562
1174
  '<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 +1195,25 @@ var FEATURE_LINE_CSS = [
583
1195
  `background-color:currentColor;-webkit-mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px;`,
584
1196
  `mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px}`
585
1197
  ].join("");
1198
+ function buttonShellStyle(ctx, fullWidth) {
1199
+ const bs = ctx.buttonStyle;
1200
+ if (bs) {
1201
+ return {
1202
+ borderRadius: bs.radius,
1203
+ ...bs.padding ? { padding: bs.padding } : {},
1204
+ ...bs.fontFamily ? { fontFamily: bs.fontFamily } : { fontFamily: ctx.brand.fonts.body },
1205
+ ...bs.fontSize ? { fontSize: bs.fontSize } : {},
1206
+ ...bs.fontWeight ? { fontWeight: bs.fontWeight } : {},
1207
+ ...bs.letterSpacing && bs.letterSpacing !== "normal" ? { letterSpacing: bs.letterSpacing } : {},
1208
+ ...bs.textTransform && bs.textTransform !== "none" ? { textTransform: bs.textTransform } : {}
1209
+ };
1210
+ }
1211
+ return {
1212
+ borderRadius: AI_TREE_TOKENS.radiusButton,
1213
+ padding: fullWidth ? `${AI_TREE_TOKENS.spacing2}px ${AI_TREE_TOKENS.spacing4}px` : `${AI_TREE_TOKENS.spacing3}px ${AI_TREE_TOKENS.spacing6}px`,
1214
+ ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
1215
+ };
1216
+ }
586
1217
  function hexLuminance(color) {
587
1218
  const m = /^#([0-9a-f]{6})$/i.exec(color.trim());
588
1219
  if (!m) return null;
@@ -599,6 +1230,12 @@ function hexContrast(a, b) {
599
1230
  const [hi, lo] = la > lb ? [la, lb] : [lb, la];
600
1231
  return (hi + 0.05) / (lo + 0.05);
601
1232
  }
1233
+ function primaryButtonLabel(brand) {
1234
+ const darkC = hexContrast(brand.palette.primary, brand.palette.dark);
1235
+ const lightC = hexContrast(brand.palette.primary, AI_TREE_TOKENS.textPrimaryForeground);
1236
+ if (darkC === null || lightC === null) return AI_TREE_TOKENS.textPrimaryForeground;
1237
+ return darkC > lightC ? brand.palette.dark : AI_TREE_TOKENS.textPrimaryForeground;
1238
+ }
602
1239
  function accentBandContext(brand) {
603
1240
  const p = brand.palette;
604
1241
  const lightWins = (hexContrast(p.light, p.primary) ?? 99) >= (hexContrast(p.dark, p.primary) ?? 0);
@@ -616,6 +1253,22 @@ function accentBandContext(brand) {
616
1253
  function textAttrs(ctx, path) {
617
1254
  return ctx.keyFor ? { "data-ohw-key": ctx.keyFor(path), "data-ohw-editable": "text" } : {};
618
1255
  }
1256
+ var AI_RESPONSIVE_CSS = [
1257
+ "@media (max-width: 960px) {",
1258
+ " [data-ai-responsive] [data-ai-grid] { grid-template-columns: repeat(2, 1fr) !important; }",
1259
+ ' [data-ai-responsive] [data-ai-grid="1"] { grid-template-columns: 1fr !important; }',
1260
+ "}",
1261
+ "@media (max-width: 640px) {",
1262
+ " [data-ai-responsive] [data-ai-grid] { grid-template-columns: 1fr !important; }",
1263
+ " [data-ai-responsive] [data-ai-columns] > * { grid-column: 1 / -1 !important; }",
1264
+ // Group containers flatten to a column on phones; span placements come along for free.
1265
+ " [data-ai-responsive] [data-ai-group] { display: flex !important; flex-direction: column !important; }",
1266
+ " [data-ai-responsive] [data-ai-group] > * { grid-column: auto !important; }",
1267
+ " [data-ai-responsive] [data-ai-section-inner] { padding-left: 20px !important; padding-right: 20px !important; }",
1268
+ " [data-ai-responsive] { overflow-x: hidden; }",
1269
+ " [data-ai-responsive] img { max-width: 100%; }",
1270
+ "}"
1271
+ ].join("\n");
619
1272
  var TRANSPARENT_PX = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
620
1273
  function MediaBox({
621
1274
  refValue,
@@ -628,13 +1281,17 @@ function MediaBox({
628
1281
  const url = refValue ? ctx.resolveMedia(refValue) : null;
629
1282
  const isIcon = /^(lucide|simple):/.test(refValue);
630
1283
  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" } : {};
1284
+ const editAttrs = ctx.keyFor && editPath ? {
1285
+ "data-ohw-key": ctx.keyFor(editPath),
1286
+ "data-ohw-editable": isIcon ? "icon" : "image"
1287
+ } : {};
632
1288
  if (isIcon) {
633
1289
  const Icon = refValue.startsWith("lucide:") ? lucideByName(refValue.slice(7)) : null;
634
1290
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
635
1291
  "span",
636
1292
  {
637
1293
  "data-ai-icon": refValue,
1294
+ ...editAttrs,
638
1295
  style: {
639
1296
  display: "inline-flex",
640
1297
  width: 48,
@@ -698,12 +1355,10 @@ function ButtonEl({
698
1355
  width: fullWidth ? "100%" : void 0,
699
1356
  alignItems: "center",
700
1357
  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
1358
  textDecoration: "none",
704
1359
  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 }
1360
+ ...buttonShellStyle(ctx, fullWidth),
1361
+ ...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
1362
  },
708
1363
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
709
1364
  }
@@ -729,7 +1384,7 @@ function TextBlock({ slots, ctx, path }) {
729
1384
  }
730
1385
  function SectionHeaderBlock({ node, ctx, path }) {
731
1386
  const slots = node.slots ?? {};
732
- const align = slots.alignment === "center" ? "center" : "left";
1387
+ const align = node.align ?? (slots.alignment === "center" ? "center" : "left");
733
1388
  const children = node.children ?? [];
734
1389
  const buttonRowIdx = children.findIndex((c) => c.type === "button-row");
735
1390
  const buttonRow = buttonRowIdx >= 0 ? children[buttonRowIdx] : void 0;
@@ -773,7 +1428,7 @@ function SectionHeaderBlock({ node, ctx, path }) {
773
1428
  display: "flex",
774
1429
  gap: AI_TREE_TOKENS.spacing6,
775
1430
  marginTop: AI_TREE_TOKENS.spacing8,
776
- justifyContent: align === "center" ? "center" : "flex-start"
1431
+ justifyContent: align === "center" ? "center" : align === "right" ? "flex-end" : "flex-start"
777
1432
  },
778
1433
  children: (buttonRow.children ?? []).map((button, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
779
1434
  ButtonEl,
@@ -879,10 +1534,11 @@ function PricingCard({ node, ctx, path }) {
879
1534
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
880
1535
  "div",
881
1536
  {
1537
+ "data-ohw-card": "",
882
1538
  style: {
883
1539
  background: hasBg ? ctx.brand.palette.light : "transparent",
884
1540
  border: `1px solid ${dark}`,
885
- borderRadius: AI_TREE_TOKENS.radiusCard,
1541
+ borderRadius: cardRadius(slots),
886
1542
  padding: AI_TREE_TOKENS.paddingBlock,
887
1543
  display: "flex",
888
1544
  flexDirection: "column",
@@ -987,10 +1643,11 @@ function TestimonialCard({ node, ctx, path }) {
987
1643
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
988
1644
  "div",
989
1645
  {
1646
+ "data-ohw-card": "",
990
1647
  "data-ai-avatar-pos": avatarPos ?? void 0,
991
1648
  style: {
992
1649
  background: hasBg ? ctx.cardSurface : "transparent",
993
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1650
+ borderRadius: hasBg ? cardRadius(slots) : 0,
994
1651
  overflow: "hidden",
995
1652
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
996
1653
  minWidth: 0
@@ -1025,10 +1682,11 @@ function TeamCard({ node, ctx, path }) {
1025
1682
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1026
1683
  "div",
1027
1684
  {
1685
+ "data-ohw-card": "",
1028
1686
  "data-ai-avatar-pos": avatarPos ?? void 0,
1029
1687
  style: {
1030
1688
  background: hasBg ? ctx.cardSurface : "transparent",
1031
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1689
+ borderRadius: hasBg ? cardRadius(slots) : 0,
1032
1690
  overflow: "hidden",
1033
1691
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
1034
1692
  minWidth: 0,
@@ -1106,7 +1764,7 @@ function CardBlock({ node, ctx, path }) {
1106
1764
  editPath: `${path}.media`
1107
1765
  }
1108
1766
  ) : null;
1109
- const centered = slots.alignment === "center";
1767
+ const centered = (node.align ?? slots.alignment) === "center";
1110
1768
  const content = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1111
1769
  "div",
1112
1770
  {
@@ -1199,9 +1857,10 @@ function CardBlock({ node, ctx, path }) {
1199
1857
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1200
1858
  "div",
1201
1859
  {
1860
+ "data-ohw-card": "",
1202
1861
  style: {
1203
1862
  background: hasBg ? ctx.cardSurface : "transparent",
1204
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1863
+ borderRadius: hasBg ? cardRadius(slots) : 0,
1205
1864
  overflow: "hidden",
1206
1865
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
1207
1866
  display: horizontal ? "flex" : "block",
@@ -1229,7 +1888,7 @@ function CardBlock({ node, ctx, path }) {
1229
1888
  ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1230
1889
  "div",
1231
1890
  {
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" },
1891
+ 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
1892
  children: media
1234
1893
  }
1235
1894
  )),
@@ -1520,7 +2179,7 @@ function CollectionBlock({ node, ctx, path }) {
1520
2179
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1521
2180
  "div",
1522
2181
  {
1523
- "data-ai-grid": "",
2182
+ "data-ai-grid": String(itemsPerRow),
1524
2183
  style: {
1525
2184
  display: "grid",
1526
2185
  gridTemplateColumns: `repeat(${itemsPerRow}, 1fr)`,
@@ -1640,6 +2299,32 @@ function renderNode(node, ctx, path) {
1640
2299
  if (child) {
1641
2300
  return renderNode(child, ctx, `${path}.c0`);
1642
2301
  }
2302
+ if (str(slots.provider) === "map" && str(slots.query)) {
2303
+ const query = str(slots.query);
2304
+ const mapAttrs = ctx.keyFor ? {
2305
+ "data-ohw-key": ctx.keyFor(`${path}.query`),
2306
+ "data-ohw-editable": "map",
2307
+ "data-ohw-map-query": query
2308
+ } : {};
2309
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2310
+ "iframe",
2311
+ {
2312
+ ...mapAttrs,
2313
+ "data-ai-embed": "map",
2314
+ title: str(slots.title) || "Map",
2315
+ src: `https://www.google.com/maps?q=${encodeURIComponent(query)}&output=embed`,
2316
+ loading: "lazy",
2317
+ referrerPolicy: "no-referrer-when-downgrade",
2318
+ style: {
2319
+ width: "100%",
2320
+ minHeight: 320,
2321
+ border: 0,
2322
+ borderRadius: AI_TREE_TOKENS.radiusCard,
2323
+ display: "block"
2324
+ }
2325
+ }
2326
+ );
2327
+ }
1643
2328
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1644
2329
  "div",
1645
2330
  {
@@ -1743,15 +2428,12 @@ function renderNode(node, ctx, path) {
1743
2428
  alignSelf: submitAlign,
1744
2429
  border: "none",
1745
2430
  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,
2431
+ // Shape/padding/typography follow the host template's own buttons.
2432
+ ...buttonShellStyle(ctx),
1750
2433
  // Brand-styled: primary fill, brand-derived label colour (not a fixed token) so it
1751
2434
  // reads correctly on custom palettes.
1752
2435
  background: ctx.brand.palette.primary,
1753
- color: ctx.buttonLabel ?? ctx.brand.palette.light,
1754
- ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
2436
+ color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand)
1755
2437
  },
1756
2438
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
1757
2439
  },
@@ -1786,7 +2468,7 @@ function renderNode(node, ctx, path) {
1786
2468
  function AiTreeRenderer({
1787
2469
  tree,
1788
2470
  brand,
1789
- buttonRadius,
2471
+ buttonStyle,
1790
2472
  resolveMedia,
1791
2473
  editKeyPrefix
1792
2474
  }) {
@@ -1796,13 +2478,18 @@ function AiTreeRenderer({
1796
2478
  const resolvedBrand = brand ?? AI_DEFAULT_BRAND;
1797
2479
  const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
1798
2480
  const blockBrand = band?.brand ?? resolvedBrand;
2481
+ const placeholderMap = buildPlaceholderMap(tree);
1799
2482
  const ctx = {
1800
2483
  brand: blockBrand,
1801
- resolveMedia: resolveMedia ?? (() => null),
2484
+ // An owner/library ref resolves through the host resolver; a `placeholder:<subject>` ref the
2485
+ // host cannot resolve falls back to real stock photography (the per-section map first, then a
2486
+ // standalone resolve), so generated galleries, image rows, and overlay backgrounds arrive with
2487
+ // photos instead of grey boxes.
2488
+ resolveMedia: (ref) => resolveMedia?.(ref) ?? placeholderMap[ref] ?? resolvePlaceholderRef(ref),
1802
2489
  cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
1803
2490
  keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
1804
2491
  sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
1805
- buttonRadius,
2492
+ buttonStyle,
1806
2493
  ...band ? { buttonLabel: band.buttonLabel } : {}
1807
2494
  };
1808
2495
  const settings = tree.settings ?? {};
@@ -1825,11 +2512,25 @@ function AiTreeRenderer({
1825
2512
  }
1826
2513
  })();
1827
2514
  const distributed = !isOverlay && settings.textDistribution;
2515
+ const rowAlignItems = (rowAlign) => {
2516
+ if (rowAlign === "top") return "start";
2517
+ if (rowAlign === "bottom") return "end";
2518
+ if (rowAlign === "center" || rowAlign === "stretch") return rowAlign;
2519
+ if (distributed === "space-between") return "stretch";
2520
+ return (distributed ?? settings.verticalPosition) === "top" ? "start" : "center";
2521
+ };
2522
+ const cellAlignStyle = (blockAlign) => blockAlign ? {
2523
+ display: "flex",
2524
+ flexDirection: "column",
2525
+ alignItems: blockAlign === "left" ? "flex-start" : blockAlign === "right" ? "flex-end" : "center",
2526
+ textAlign: blockAlign
2527
+ } : {};
1828
2528
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1829
2529
  "section",
1830
2530
  {
1831
2531
  "data-ai-section": tree.tag ?? "",
1832
2532
  ...bgAttrs,
2533
+ "data-ai-responsive": "",
1833
2534
  style: {
1834
2535
  position: "relative",
1835
2536
  padding: `${pad}px 0`,
@@ -1840,12 +2541,13 @@ function AiTreeRenderer({
1840
2541
  color: settings.sectionBackground === "accent" ? blockBrand.palette.dark : void 0
1841
2542
  },
1842
2543
  children: [
1843
- isOverlay && backgroundUrl && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
2544
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: AI_RESPONSIVE_CSS }),
1844
2545
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: AI_MOBILE_CSS }),
2546
+ isOverlay && backgroundUrl && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
1845
2547
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1846
2548
  "div",
1847
2549
  {
1848
- "data-ai-container": "",
2550
+ "data-ai-section-inner": "",
1849
2551
  style: {
1850
2552
  position: "relative",
1851
2553
  maxWidth: AI_TREE_TOKENS.sectionMaxWidth,
@@ -1856,12 +2558,12 @@ function AiTreeRenderer({
1856
2558
  children: tree.rows.map((row, r2) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1857
2559
  "div",
1858
2560
  {
1859
- "data-ai-row": "",
2561
+ "data-ai-columns": "",
1860
2562
  style: {
1861
2563
  display: "grid",
1862
2564
  gridTemplateColumns: "repeat(12, 1fr)",
1863
2565
  gap: AI_TREE_TOKENS.spacing6,
1864
- alignItems: distributed === "space-between" ? "stretch" : (distributed ?? settings.verticalPosition) === "top" ? "start" : "center",
2566
+ alignItems: rowAlignItems(row.align),
1865
2567
  marginTop: r2 > 0 ? AI_TREE_TOKENS.spacing8 : 0
1866
2568
  },
1867
2569
  children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -1871,6 +2573,8 @@ function AiTreeRenderer({
1871
2573
  style: {
1872
2574
  gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`,
1873
2575
  minWidth: 0,
2576
+ // Horizontal placement of the block's content within its column.
2577
+ ...cellAlignStyle(block.align),
1874
2578
  // space-between: each column becomes a flex column whose content spreads over
1875
2579
  // the full row height instead of clumping at the top.
1876
2580
  ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
@@ -1893,7 +2597,7 @@ function AiTreeRenderer({
1893
2597
  var import_jsx_runtime2 = require("react/jsx-runtime");
1894
2598
  var CONTAINER_ATTR = "data-ohw-ai-generated";
1895
2599
  var REPLACED_ATTR = "data-ohw-ai-replaced-by";
1896
- var REMOVED_ATTR = "data-ohw-ai-removed";
2600
+ var REMOVED_ATTR2 = "data-ohw-ai-removed";
1897
2601
  var TEMPLATE_HIDDEN_ATTR = "data-ohw-ai-template-hidden";
1898
2602
  var CHROME_SECTION_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
1899
2603
  function readRootVar(name) {
@@ -1917,13 +2621,13 @@ function deriveBrandOverride() {
1917
2621
  };
1918
2622
  }
1919
2623
  function deriveTemplateBrand() {
1920
- const dark = readRootVar("--color-dark");
1921
- const primary = readRootVar("--color-primary");
1922
- const light = readRootVar("--color-light");
2624
+ const primary = readRootVar("--brand-primary") || readRootVar("--color-primary");
2625
+ const dark = readRootVar("--brand-text") || readRootVar("--color-dark");
2626
+ const light = readRootVar("--brand-background") || readRootVar("--color-light");
1923
2627
  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");
2628
+ const accent = readRootVar("--brand-accent") || readRootVar("--color-accent");
2629
+ const heading = readRootVar("--brand-font-heading") || readRootVar("--font-heading") || readRootVar("--font-display");
2630
+ const body = readRootVar("--brand-font-body") || readRootVar("--font-body");
1927
2631
  return {
1928
2632
  palette: { dark, primary, accent: accent || dark, light },
1929
2633
  fonts: {
@@ -1932,12 +2636,32 @@ function deriveTemplateBrand() {
1932
2636
  }
1933
2637
  };
1934
2638
  }
1935
- function deriveTemplateButtonRadius() {
2639
+ function deriveTemplateButtonStyle() {
1936
2640
  if (typeof document === "undefined") return null;
1937
- const btn = document.querySelector('[data-ohw-role="button"]');
2641
+ const btn = Array.from(document.querySelectorAll('[data-ohw-role="button"]')).find(
2642
+ (el) => !el.closest(`[${CONTAINER_ATTR}]`)
2643
+ );
1938
2644
  if (!btn) return null;
1939
- const radius = getComputedStyle(btn).borderTopLeftRadius;
1940
- return radius || null;
2645
+ const cs = getComputedStyle(btn);
2646
+ const corners = [
2647
+ cs.borderTopLeftRadius,
2648
+ cs.borderTopRightRadius,
2649
+ cs.borderBottomRightRadius,
2650
+ cs.borderBottomLeftRadius
2651
+ ].map((v) => v || "0px");
2652
+ const radius = corners.every((v) => v === corners[0]) ? corners[0] : corners.join(" ");
2653
+ const px = (v) => parseFloat(v) || 0;
2654
+ const padY = Math.max(px(cs.paddingTop), px(cs.paddingBottom));
2655
+ const padX = Math.max(px(cs.paddingLeft), px(cs.paddingRight));
2656
+ return {
2657
+ radius: radius || "10px",
2658
+ padding: `${padY}px ${padX}px`,
2659
+ fontFamily: cs.fontFamily || "",
2660
+ fontSize: cs.fontSize || "",
2661
+ fontWeight: cs.fontWeight || "",
2662
+ letterSpacing: cs.letterSpacing || "",
2663
+ textTransform: cs.textTransform || ""
2664
+ };
1941
2665
  }
1942
2666
  var mounted = /* @__PURE__ */ new Map();
1943
2667
  function findTemplateSection(id) {
@@ -1987,18 +2711,18 @@ function placeContainer(container, entry) {
1987
2711
  }
1988
2712
  function syncRemovedSections(state) {
1989
2713
  const removed = new Set(state.removed ?? []);
1990
- for (const el of document.querySelectorAll(`[${REMOVED_ATTR}]`)) {
2714
+ for (const el of document.querySelectorAll(`[${REMOVED_ATTR2}]`)) {
1991
2715
  const id = el.getAttribute("data-ohw-section") ?? "";
1992
2716
  if (!removed.has(id)) {
1993
2717
  el.style.removeProperty("display");
1994
- el.removeAttribute(REMOVED_ATTR);
2718
+ el.removeAttribute(REMOVED_ATTR2);
1995
2719
  }
1996
2720
  }
1997
2721
  for (const id of removed) {
1998
2722
  const section = findTemplateSection(id);
1999
2723
  if (section && !section.hasAttribute(REPLACED_ATTR)) {
2000
2724
  section.style.display = "none";
2001
- section.setAttribute(REMOVED_ATTR, "");
2725
+ section.setAttribute(REMOVED_ATTR2, "");
2002
2726
  }
2003
2727
  }
2004
2728
  }
@@ -2015,7 +2739,7 @@ function syncTemplateHidden(state, pageHasSections) {
2015
2739
  if (el.hasAttribute(CONTAINER_ATTR)) continue;
2016
2740
  if (CHROME_SECTION_IDS.has(el.getAttribute("data-ohw-section") ?? "")) continue;
2017
2741
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
2018
- if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR)) continue;
2742
+ if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR2)) continue;
2019
2743
  el.style.display = "none";
2020
2744
  el.setAttribute(TEMPLATE_HIDDEN_ATTR, "");
2021
2745
  }
@@ -2039,18 +2763,23 @@ function syncReplacedOriginals(state) {
2039
2763
  }
2040
2764
  }
2041
2765
  var sectionOrderIndex = /* @__PURE__ */ new Map();
2766
+ var removedSectionIds = /* @__PURE__ */ new Set();
2042
2767
  function setAiSectionOrder(raw, currentPath) {
2043
2768
  const next = /* @__PURE__ */ new Map();
2769
+ const removed = /* @__PURE__ */ new Set();
2044
2770
  if (raw) {
2045
2771
  try {
2046
2772
  const entries = JSON.parse(raw);
2047
2773
  for (const entry of entries) {
2048
- if (!entry.pagePath || entry.pagePath === currentPath) next.set(entry.instanceId, entry.order);
2774
+ if (entry.pagePath && entry.pagePath !== currentPath) continue;
2775
+ next.set(entry.instanceId, entry.order);
2776
+ if (entry.removed) removed.add(entry.instanceId);
2049
2777
  }
2050
2778
  } catch {
2051
2779
  }
2052
2780
  }
2053
2781
  sectionOrderIndex = next;
2782
+ removedSectionIds = removed;
2054
2783
  }
2055
2784
  function applyExplicitOrder(entries) {
2056
2785
  if (sectionOrderIndex.size === 0) return entries;
@@ -2086,11 +2815,23 @@ function orderByChain(sections) {
2086
2815
  for (const root of roots) visit(root);
2087
2816
  return out.length === sections.length ? out : sections;
2088
2817
  }
2818
+ function syncSoftRemovedGenerated() {
2819
+ for (const [id, section] of mounted) {
2820
+ const el = section.container;
2821
+ if (removedSectionIds.has(id)) {
2822
+ el.style.display = "none";
2823
+ el.setAttribute(REMOVED_ATTR, "");
2824
+ } else if (el.hasAttribute(REMOVED_ATTR)) {
2825
+ el.style.removeProperty("display");
2826
+ el.removeAttribute(REMOVED_ATTR);
2827
+ }
2828
+ }
2829
+ }
2089
2830
  function applyAiSectionsToDom(state, options) {
2090
2831
  if (typeof document === "undefined") return;
2091
2832
  const brandOverride = deriveBrandOverride();
2092
2833
  const templateBrand = deriveTemplateBrand();
2093
- const templateButtonRadius = deriveTemplateButtonRadius();
2834
+ const templateButtonStyle = deriveTemplateButtonStyle();
2094
2835
  const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
2095
2836
  const pagePath = window.location.pathname;
2096
2837
  const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
@@ -2130,7 +2871,7 @@ function applyAiSectionsToDom(state, options) {
2130
2871
  {
2131
2872
  tree: entry.tree,
2132
2873
  brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2133
- buttonRadius: templateButtonRadius,
2874
+ buttonStyle: templateButtonStyle,
2134
2875
  resolveMedia,
2135
2876
  editKeyPrefix: `ai.${entry.id}`
2136
2877
  }
@@ -2153,6 +2894,7 @@ function applyAiSectionsToDom(state, options) {
2153
2894
  syncReplacedOriginals(state);
2154
2895
  syncRemovedSections(state);
2155
2896
  syncTemplateHidden(state, pageSections.length > 0);
2897
+ syncSoftRemovedGenerated();
2156
2898
  }
2157
2899
 
2158
2900
  // src/useLinkHrefGuardian.ts
@@ -7882,6 +8624,7 @@ function MediaOverlay({
7882
8624
  (prev) => prev && prev.fullWidth === width && prev.height === height ? prev : { fullWidth: width, height }
7883
8625
  );
7884
8626
  }, [isVideo]);
8627
+ const replaceLabel = isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image";
7885
8628
  const replaceMode = buttonSize ? replaceButtonMode({ width: rect.width, height: rect.height }, buttonSize) : "none";
7886
8629
  const box = {
7887
8630
  position: "fixed",
@@ -8011,17 +8754,17 @@ function MediaOverlay({
8011
8754
  },
8012
8755
  children: [
8013
8756
  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"
8757
+ replaceLabel
8015
8758
  ]
8016
8759
  }
8017
8760
  ),
8018
- replaceMode === "none" ? null : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
8761
+ showChrome && replaceMode !== "none" && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
8019
8762
  Button,
8020
8763
  {
8021
8764
  "data-ohw-media-overlay": "",
8022
8765
  variant: "outline",
8023
8766
  size: "sm",
8024
- "aria-label": isVideo ? "Replace video" : "Replace image",
8767
+ "aria-label": replaceLabel,
8025
8768
  className: "gap-1.5 cursor-pointer hover:bg-background",
8026
8769
  style: {
8027
8770
  ...OVERLAY_BUTTON_STYLE,
@@ -8044,7 +8787,7 @@ function MediaOverlay({
8044
8787
  },
8045
8788
  children: [
8046
8789
  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
8790
+ replaceMode === "full" ? replaceLabel : null
8048
8791
  ]
8049
8792
  }
8050
8793
  )
@@ -8081,219 +8824,37 @@ function CarouselOverlay({
8081
8824
  width: rect.width,
8082
8825
  height: rect.height,
8083
8826
  zIndex: 2147483646,
8084
- pointerEvents: "auto",
8085
- boxShadow: "inset 0 0 0 1.5px var(--color-primary)",
8086
- background: "color-mix(in srgb, var(--color-primary) 20%, transparent)"
8087
- },
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, "");
8827
+ pointerEvents: "auto",
8828
+ boxShadow: "inset 0 0 0 1.5px var(--color-primary)",
8829
+ background: "color-mix(in srgb, var(--color-primary) 20%, transparent)"
8830
+ },
8831
+ onClick: () => onEdit(hover.key),
8832
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
8833
+ Button,
8834
+ {
8835
+ "data-ohw-carousel-overlay": "",
8836
+ variant: "outline",
8837
+ size: "sm",
8838
+ className: "cursor-pointer gap-1.5 hover:bg-background",
8839
+ style: OVERLAY_BUTTON_STYLE2,
8840
+ onMouseDown: (e) => e.preventDefault(),
8841
+ onClick: (e) => {
8842
+ e.stopPropagation();
8843
+ onEdit(hover.key);
8844
+ },
8845
+ children: [
8846
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react6.GalleryHorizontal, { size: 14 }),
8847
+ "Edit gallery"
8848
+ ]
8849
+ }
8850
+ )
8205
8851
  }
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
8852
  );
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
8853
  }
8295
8854
 
8296
8855
  // src/ui/ai-section/AiSectionOverlay.tsx
8856
+ var import_react8 = require("react");
8857
+ var import_lucide_react7 = require("lucide-react");
8297
8858
  var import_jsx_runtime17 = require("react/jsx-runtime");
8298
8859
  function findSectionElement(instanceId) {
8299
8860
  const escaped = CSS.escape(instanceId);
@@ -13009,6 +13570,7 @@ function readLogoSizeState(content, placement) {
13009
13570
  function getLogoElement(el) {
13010
13571
  const marked = el.closest('[data-ohw-role="logo"], [data-ohw-logo]');
13011
13572
  if (marked) return marked;
13573
+ if (el.closest('[data-ohw-editable="icon"]')) return null;
13012
13574
  const root = el.closest("nav, [data-ohw-nav-root], footer");
13013
13575
  if (!root) return null;
13014
13576
  const anchor = el.closest("a");
@@ -14384,6 +14946,9 @@ function collectEditableNodes(extraContent, root = document) {
14384
14946
  if (el.dataset.ohwEditable === "link") {
14385
14947
  return { key: el.dataset.ohwKey ?? "", type: "link", text: getLinkHref3(el) };
14386
14948
  }
14949
+ if (el.dataset.ohwEditable === "map") {
14950
+ return { key: el.dataset.ohwKey ?? "", type: "map", text: el.dataset.ohwMapQuery ?? "" };
14951
+ }
14387
14952
  return {
14388
14953
  key: el.dataset.ohwKey ?? "",
14389
14954
  type: el.dataset.ohwEditable ?? "text",
@@ -14937,21 +15502,10 @@ function parseSchedulingInsertAfter(insertAfter) {
14937
15502
  insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
14938
15503
  };
14939
15504
  }
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;
15505
+ function resolveEntryAnchor(entry) {
15506
+ if (entry.anchorId !== void 0) return { anchorId: entry.anchorId, beforeId: entry.beforeId ?? null };
15507
+ const parsed = parseSchedulingInsertAfter(entry.insertAfter);
15508
+ return { anchorId: parsed.anchor, beforeId: parsed.insertBefore };
14955
15509
  }
14956
15510
  function schedulingMountDepth(insertAfter) {
14957
15511
  if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
@@ -14968,8 +15522,7 @@ function getPageSchedulingEntries(raw) {
14968
15522
  }
14969
15523
  }
14970
15524
  function isSchedulingWidgetMissing(entry) {
14971
- const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
14972
- return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
15525
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(entry.insertAfter)}"]`);
14973
15526
  }
14974
15527
  function hasMissingSchedulingWidgets(entries) {
14975
15528
  return entries.some(isSchedulingWidgetMissing);
@@ -14999,16 +15552,17 @@ function initSectionsFromContent(content, removeExisting = false) {
14999
15552
  } catch {
15000
15553
  }
15001
15554
  }
15002
- function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
15003
- const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
15004
- const sectionId = schedulingSectionId(effectiveInsertAfter);
15555
+ function mountSchedulingWidget(anchorId, notifyOnConnect = false, scheduleId, beforeId, existingWidgetId) {
15556
+ const widgetId = existingWidgetId ?? (beforeId ? `${anchorId}${INSERT_BEFORE_MARKER}${beforeId}` : anchorId);
15557
+ const sectionId = schedulingSectionId(widgetId);
15005
15558
  if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
15006
- const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
15007
- if (!mountPoint) return false;
15559
+ const anchorEl = document.querySelector(`[data-ohw-section="${anchorId}"]`);
15560
+ if (!anchorEl) return false;
15561
+ const mountPoint = anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
15008
15562
  const container = document.createElement("div");
15009
15563
  container.dataset.ohwSectionContainer = "scheduling";
15010
- if (insertBefore) {
15011
- const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
15564
+ if (beforeId) {
15565
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${beforeId}"]`);
15012
15566
  const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
15013
15567
  if (!beforePoint) return false;
15014
15568
  beforePoint.insertAdjacentElement("beforebegin", container);
@@ -15019,19 +15573,25 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
15019
15573
  }
15020
15574
  tail.insertAdjacentElement("afterend", container);
15021
15575
  }
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
- });
15576
+ try {
15577
+ const root = (0, import_client2.createRoot)(container);
15578
+ (0, import_react_dom3.flushSync)(() => {
15579
+ root.render(
15580
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
15581
+ SchedulingWidget,
15582
+ {
15583
+ notifyOnConnect,
15584
+ initialScheduleId: scheduleId,
15585
+ insertAfter: widgetId
15586
+ }
15587
+ )
15588
+ );
15589
+ });
15590
+ } catch (err) {
15591
+ console.error("[ow:scheduling] render threw", err);
15592
+ container.remove();
15593
+ return false;
15594
+ }
15035
15595
  const tracker = getSectionsTracker();
15036
15596
  let sections = [];
15037
15597
  try {
@@ -15039,10 +15599,12 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
15039
15599
  } catch {
15040
15600
  }
15041
15601
  const inEditor = typeof window !== "undefined" && window.self !== window.top;
15042
- if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
15602
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === widgetId)) {
15043
15603
  sections.push({
15044
15604
  type: "scheduling",
15045
- insertAfter: effectiveInsertAfter,
15605
+ insertAfter: widgetId,
15606
+ anchorId,
15607
+ beforeId: beforeId ?? null,
15046
15608
  pagePath: window.location.pathname,
15047
15609
  ...scheduleId ? { scheduleId } : {}
15048
15610
  });
@@ -15056,7 +15618,8 @@ function mountSchedulingEntries(entries, notifyOnConnect = false) {
15056
15618
  for (let i = pending.length - 1; i >= 0; i--) {
15057
15619
  const entry = pending[i];
15058
15620
  const shouldNotify = typeof notifyOnConnect === "function" ? notifyOnConnect(entry) : notifyOnConnect;
15059
- if (mountSchedulingWidget(entry.insertAfter, shouldNotify, entry.scheduleId ?? null)) {
15621
+ const { anchorId, beforeId } = resolveEntryAnchor(entry);
15622
+ if (mountSchedulingWidget(anchorId, shouldNotify, entry.scheduleId ?? null, beforeId, entry.insertAfter)) {
15060
15623
  pending.splice(i, 1);
15061
15624
  }
15062
15625
  }
@@ -15148,7 +15711,7 @@ var EDITOR_CHROME_SELECTOR = '[data-ohw-toolbar], [data-ohw-form-toolbar], [data
15148
15711
  function isOverEditorChrome(x, y) {
15149
15712
  return document.elementsFromPoint(x, y).some((el) => el instanceof HTMLElement && el.matches(EDITOR_CHROME_SELECTOR));
15150
15713
  }
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"])';
15714
+ 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
15715
  function getVideoEl2(el) {
15153
15716
  return el instanceof HTMLVideoElement ? el : el.querySelector("video");
15154
15717
  }
@@ -15204,6 +15767,12 @@ function applyVideoSettingNode(key, val) {
15204
15767
  });
15205
15768
  return true;
15206
15769
  }
15770
+ function applyMapQuery(el, val) {
15771
+ if (!(el instanceof HTMLIFrameElement)) return;
15772
+ const nextSrc = `https://www.google.com/maps?q=${encodeURIComponent(val)}&output=embed`;
15773
+ if (el.src !== nextSrc) el.src = nextSrc;
15774
+ el.setAttribute("data-ohw-map-query", val);
15775
+ }
15207
15776
  function applyLinkByKey(key, val) {
15208
15777
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
15209
15778
  if (el.dataset.ohwEditable === "link") applyLinkHref(el, val);
@@ -15214,6 +15783,11 @@ function applyLinkByKey(key, val) {
15214
15783
  hrefAnchors.forEach((el) => applyLinkHref(el, val));
15215
15784
  }
15216
15785
  }
15786
+ function isInsideLinkEditor(target) {
15787
+ return Boolean(
15788
+ 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"]')
15789
+ );
15790
+ }
15217
15791
  function isInsideFloatingPanel(target) {
15218
15792
  return Boolean(target.closest("[data-ohw-floating-panel]"));
15219
15793
  }
@@ -15221,11 +15795,6 @@ function isPointOverFloatingPanel(clientX, clientY) {
15221
15795
  const el = document.elementFromPoint(clientX, clientY);
15222
15796
  return Boolean(el instanceof Element && el.closest("[data-ohw-floating-panel]"));
15223
15797
  }
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
15798
  function getHrefKeyFromElement(el) {
15230
15799
  if (!el) return null;
15231
15800
  const anchor = el.closest("[data-ohw-href-key]");
@@ -15484,7 +16053,7 @@ function getNavigationSelectionParent(el) {
15484
16053
  if (!isFooterLinksContainer(el) && (el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isInferredFooterGroup2(el))) {
15485
16054
  return getFooterLinksContainer();
15486
16055
  }
15487
- if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || isFooterLinksContainer(el)) {
16056
+ 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
16057
  return getNavigationRoot(el);
15489
16058
  }
15490
16059
  return null;
@@ -15699,7 +16268,6 @@ var ICONS = {
15699
16268
  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
16269
  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
16270
  };
15702
- var HEIGHT_SETTLE_DELAYS = [150, 400, 800];
15703
16271
  var SELECTION_CHROME_GAP2 = 4;
15704
16272
  var TOOLBAR_STROKE_GAP2 = 4;
15705
16273
  var TOOLBAR_OFFSET_FROM_ELEMENT = SELECTION_CHROME_GAP2 + TOOLBAR_STROKE_GAP2;
@@ -16079,6 +16647,7 @@ function StateToggle({
16079
16647
  );
16080
16648
  }
16081
16649
  var contentCache = /* @__PURE__ */ new Map();
16650
+ var fetchedContentPaths = /* @__PURE__ */ new Set();
16082
16651
  var brandingCache = /* @__PURE__ */ new Map();
16083
16652
  var OHW_LOADER_STYLE = {
16084
16653
  position: "fixed",
@@ -16608,13 +17177,6 @@ function OhhwellsBridge() {
16608
17177
  const [isItemDragging, setIsItemDragging] = (0, import_react17.useState)(false);
16609
17178
  const [isFooterFrameSelection, setIsFooterFrameSelection] = (0, import_react17.useState)(false);
16610
17179
  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
17180
  const [navDropdownPreviewOpen, setNavDropdownPreviewOpen] = (0, import_react17.useState)(null);
16619
17181
  const [footerHeadingVisible, setFooterHeadingVisible] = (0, import_react17.useState)(null);
16620
17182
  const footerDragRef = (0, import_react17.useRef)(null);
@@ -16632,6 +17194,13 @@ function OhhwellsBridge() {
16632
17194
  const brandKitRef = (0, import_react17.useRef)("");
16633
17195
  const stylesRef = (0, import_react17.useRef)("");
16634
17196
  const pendingDeleteUndoRef = (0, import_react17.useRef)(null);
17197
+ const [floatingPanel, setFloatingPanel] = (0, import_react17.useState)(null);
17198
+ const floatingPanelOpenRef = (0, import_react17.useRef)(false);
17199
+ const setFloatingPanelRef = (0, import_react17.useRef)(setFloatingPanel);
17200
+ const [floatingPanelPos, setFloatingPanelPos] = (0, import_react17.useState)(null);
17201
+ const [logoSizeDraft, setLogoSizeDraft] = (0, import_react17.useState)(null);
17202
+ const [editorViewport, setEditorViewport] = (0, import_react17.useState)("desktop");
17203
+ const [parentScrollSnap, setParentScrollSnap] = (0, import_react17.useState)(null);
16635
17204
  const [sitePages, setSitePages] = (0, import_react17.useState)([]);
16636
17205
  const [sectionsByPath, setSectionsByPath] = (0, import_react17.useState)({});
16637
17206
  const sectionsPrefetchGenRef = (0, import_react17.useRef)(0);
@@ -16640,7 +17209,18 @@ function OhhwellsBridge() {
16640
17209
  const linkPopoverOpenRef = (0, import_react17.useRef)(false);
16641
17210
  const linkPopoverGraceUntilRef = (0, import_react17.useRef)(0);
16642
17211
  setLinkPopoverRef.current = setLinkPopover;
17212
+ setFloatingPanelRef.current = setFloatingPanel;
16643
17213
  linkPopoverSessionRef.current = linkPopover;
17214
+ floatingPanelOpenRef.current = Boolean(floatingPanel);
17215
+ (0, import_react17.useEffect)(() => {
17216
+ const syncViewport = () => {
17217
+ const next = window.innerWidth <= 480 ? "mobile" : "desktop";
17218
+ setEditorViewport((prev) => prev === next ? prev : next);
17219
+ };
17220
+ syncViewport();
17221
+ window.addEventListener("resize", syncViewport);
17222
+ return () => window.removeEventListener("resize", syncViewport);
17223
+ }, []);
16644
17224
  const {
16645
17225
  navDragRef,
16646
17226
  navDropSlots,
@@ -17965,17 +18545,19 @@ function OhhwellsBridge() {
17965
18545
  }
17966
18546
  if (typeof content[STYLE_STORE_KEY] === "string") {
17967
18547
  stylesRef.current = content[STYLE_STORE_KEY];
18548
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
17968
18549
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17969
18550
  }
17970
18551
  applyBrandChrome(content);
18552
+ initSectionInstancesFromContent(content, window.location.pathname);
17971
18553
  for (const [key, val] of Object.entries(content)) {
17972
18554
  if (key === "__ohw_sections") continue;
17973
18555
  if (key === AI_SECTIONS_KEY) continue;
18556
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
18557
+ if (LOGO_IMAGE_KEYS.includes(key)) continue;
17974
18558
  if (key === BRAND_KIT_KEY) continue;
17975
18559
  if (key === STYLE_STORE_KEY) continue;
17976
18560
  if (BRAND_CHROME_KEYS.has(key)) continue;
17977
- if (key === LOGO_PLACEHOLDER_KEY) continue;
17978
- if (LOGO_IMAGE_KEYS.includes(key)) continue;
17979
18561
  if (applyVideoSettingNode(key, val)) continue;
17980
18562
  if (applyCarouselNode(key, val)) continue;
17981
18563
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -18003,6 +18585,8 @@ function OhhwellsBridge() {
18003
18585
  }
18004
18586
  } else if (el.dataset.ohwEditable === "link") {
18005
18587
  applyLinkHref(el, val);
18588
+ } else if (el.dataset.ohwEditable === "map") {
18589
+ applyMapQuery(el, val);
18006
18590
  } else if (el.dataset.ohwEditable === "icon") {
18007
18591
  applyIconMarkup(el, val);
18008
18592
  } else if (el.dataset.ohwEditable === "form") {
@@ -18023,7 +18607,6 @@ function OhhwellsBridge() {
18023
18607
  if (isEditModeRef.current) requestMissingSocialIconsRef.current();
18024
18608
  enforceLinkHrefs();
18025
18609
  initSectionsFromContent(content, true);
18026
- initSectionInstancesFromContent(content, window.location.pathname);
18027
18610
  sectionsLoadedRef.current = true;
18028
18611
  pendingScheduleConfigRequests.current.splice(0).forEach(processConfigRequest);
18029
18612
  if (imageLoads.length === 0) return Promise.resolve();
@@ -18042,7 +18625,9 @@ function OhhwellsBridge() {
18042
18625
  let cancelled = false;
18043
18626
  setFetchState("loading");
18044
18627
  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) => {
18628
+ const initialPath = pathname;
18629
+ fetchedContentPaths.add(`${subdomain}::${initialPath}`);
18630
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(initialPath)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18046
18631
  if (cancelled) return;
18047
18632
  const content = data?.content ?? {};
18048
18633
  const branding = Boolean(data?.showBranding);
@@ -18176,16 +18761,17 @@ function OhhwellsBridge() {
18176
18761
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
18177
18762
  }
18178
18763
  if (typeof content[STYLE_STORE_KEY] === "string") {
18764
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
18179
18765
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
18180
18766
  }
18181
18767
  for (const [key, val] of Object.entries(content)) {
18182
18768
  if (key === "__ohw_sections") continue;
18183
18769
  if (key === AI_SECTIONS_KEY) continue;
18770
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
18771
+ if (LOGO_IMAGE_KEYS.includes(key)) continue;
18184
18772
  if (key === BRAND_KIT_KEY) continue;
18185
18773
  if (key === STYLE_STORE_KEY) continue;
18186
18774
  if (BRAND_CHROME_KEYS.has(key)) continue;
18187
- if (key === LOGO_PLACEHOLDER_KEY) continue;
18188
- if (LOGO_IMAGE_KEYS.includes(key)) continue;
18189
18775
  if (applyVideoSettingNode(key, val)) continue;
18190
18776
  if (applyCarouselNode(key, val)) continue;
18191
18777
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -18200,6 +18786,8 @@ function OhhwellsBridge() {
18200
18786
  if (video && video.src !== val) applyVideoSrc(video, val);
18201
18787
  } else if (el.dataset.ohwEditable === "link") {
18202
18788
  applyLinkHref(el, val);
18789
+ } else if (el.dataset.ohwEditable === "map") {
18790
+ applyMapQuery(el, val);
18203
18791
  } else if (el.dataset.ohwEditable === "form") {
18204
18792
  } else if (isIconMarkupValue(val)) {
18205
18793
  } else if (el.innerHTML !== val) {
@@ -18231,6 +18819,17 @@ function OhhwellsBridge() {
18231
18819
  debounceTimer = setTimeout(applyFromCache, 150);
18232
18820
  };
18233
18821
  applyFromCache();
18822
+ const pathCacheKey = `${subdomain}::${pathname}`;
18823
+ if (!fetchedContentPaths.has(pathCacheKey)) {
18824
+ fetchedContentPaths.add(pathCacheKey);
18825
+ const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
18826
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(pathname)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18827
+ if (!data?.content) return;
18828
+ contentCache.set(subdomain, data.content);
18829
+ applyFromCache();
18830
+ }).catch(() => {
18831
+ });
18832
+ }
18234
18833
  observer = new MutationObserver(scheduleApply);
18235
18834
  observer.observe(document.body, { childList: true, subtree: true });
18236
18835
  return () => {
@@ -18346,26 +18945,11 @@ function OhhwellsBridge() {
18346
18945
  const t2 = setTimeout(measure, 500);
18347
18946
  const ro = new ResizeObserver(schedule);
18348
18947
  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
18948
  return () => {
18363
18949
  clearTimeout(t1);
18364
18950
  clearTimeout(t2);
18365
18951
  if (raf != null) cancelAnimationFrame(raf);
18366
18952
  ro.disconnect();
18367
- clearResizeTimers();
18368
- window.removeEventListener("resize", handleResize);
18369
18953
  };
18370
18954
  }, [pathname, isEditMode, postToParent2]);
18371
18955
  (0, import_react17.useEffect)(() => {
@@ -18611,9 +19195,6 @@ function OhhwellsBridge() {
18611
19195
  if (target.closest("[data-ohw-state-toggle]")) return;
18612
19196
  if (target.closest("[data-ohw-max-badge]")) return;
18613
19197
  if (isInsideLinkEditor(target)) return;
18614
- if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
18615
- clearMediaSelectionRef.current();
18616
- }
18617
19198
  if (isInsideFloatingPanel(target)) return;
18618
19199
  if (target.closest("[data-ohw-form-toolbar]")) return;
18619
19200
  if (target.closest(
@@ -18621,6 +19202,9 @@ function OhhwellsBridge() {
18621
19202
  )) {
18622
19203
  return;
18623
19204
  }
19205
+ if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
19206
+ clearMediaSelectionRef.current();
19207
+ }
18624
19208
  {
18625
19209
  const formEl = getFormElement(target);
18626
19210
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
@@ -18772,14 +19356,6 @@ function OhhwellsBridge() {
18772
19356
  }
18773
19357
  const clickedButton = findClosestButtonLike(target);
18774
19358
  const buttonOnMedia = Boolean(clickedButton && isMediaEditable(editable) && editable.contains(clickedButton));
18775
- console.log("[click-debug]", {
18776
- editableType: editable.dataset.ohwEditable,
18777
- editableTag: editable.tagName,
18778
- targetTag: target.tagName,
18779
- clickedButtonTag: clickedButton?.tagName ?? null,
18780
- buttonOnMedia,
18781
- isMediaEditableEditable: isMediaEditable(editable)
18782
- });
18783
19359
  if (isMediaEditable(editable) && !buttonOnMedia) {
18784
19360
  e.preventDefault();
18785
19361
  e.stopPropagation();
@@ -18806,11 +19382,6 @@ function OhhwellsBridge() {
18806
19382
  const hrefLookupTarget = buttonOnMedia ? clickedButton : editable;
18807
19383
  const hrefCtx = getHrefKeyFromElement(hrefLookupTarget);
18808
19384
  const navAnchor = hrefCtx ? getNavigationItemAnchor(hrefCtx.anchor) : null;
18809
- console.log("[click-debug 2]", {
18810
- hrefLookupTargetTag: hrefLookupTarget.tagName,
18811
- hrefCtx: hrefCtx ? { key: hrefCtx.key } : null,
18812
- navAnchorTag: navAnchor?.tagName ?? null
18813
- });
18814
19385
  if (navAnchor) {
18815
19386
  e.preventDefault();
18816
19387
  e.stopPropagation();
@@ -18980,6 +19551,9 @@ function OhhwellsBridge() {
18980
19551
  setHoveredItemRect(null);
18981
19552
  hoveredNavContainerRef.current = null;
18982
19553
  setHoveredNavContainerRect(null);
19554
+ siblingHintElRef.current = null;
19555
+ setSiblingHintRect(null);
19556
+ setSiblingHintRects([]);
18983
19557
  return;
18984
19558
  }
18985
19559
  {
@@ -19098,7 +19672,6 @@ function OhhwellsBridge() {
19098
19672
  hoveredNavContainerRef.current = null;
19099
19673
  setHoveredNavContainerRect(null);
19100
19674
  hoveredItemElRef.current = editable;
19101
- setHoveredItemRect(isSelectedForHoverRef.current(editable) ? null : editable.getBoundingClientRect());
19102
19675
  }
19103
19676
  }
19104
19677
  }
@@ -19395,7 +19968,7 @@ function OhhwellsBridge() {
19395
19968
  }
19396
19969
  };
19397
19970
  const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false) => {
19398
- if (linkPopoverOpenRef.current) {
19971
+ if (linkPopoverOpenRef.current || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
19399
19972
  if (hoveredImageRef.current) {
19400
19973
  hoveredImageRef.current = null;
19401
19974
  hoveredImageHasTextOverlapRef.current = false;
@@ -19760,8 +20333,7 @@ function OhhwellsBridge() {
19760
20333
  };
19761
20334
  const handleMouseMove = (e) => {
19762
20335
  const { clientX, clientY } = e;
19763
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
19764
- if (isOverEditorChrome(clientX, clientY)) {
20336
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY) || isOverEditorChrome(clientX, clientY)) {
19765
20337
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
19766
20338
  formHoverElRef.current = null;
19767
20339
  setFormHoverRect(null);
@@ -19769,6 +20341,12 @@ function OhhwellsBridge() {
19769
20341
  setHoveredItemRect(null);
19770
20342
  hoveredNavContainerRef.current = null;
19771
20343
  setHoveredNavContainerRect(null);
20344
+ siblingHintElRef.current = null;
20345
+ setSiblingHintRect(null);
20346
+ setSiblingHintRects([]);
20347
+ dismissImageHover();
20348
+ clearImageHover();
20349
+ setSectionGap(null);
19772
20350
  return;
19773
20351
  }
19774
20352
  if (probeSocialsRowAt(clientX, clientY)) return;
@@ -19780,7 +20358,11 @@ function OhhwellsBridge() {
19780
20358
  if (e.data?.type !== "ow:pointer-sync") return;
19781
20359
  const { clientX, clientY } = e.data;
19782
20360
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
19783
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
20361
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
20362
+ dismissImageHover();
20363
+ clearImageHover();
20364
+ return;
20365
+ }
19784
20366
  if (probeSocialsRowAt(clientX, clientY)) return;
19785
20367
  probeSectionGapAt(clientX, clientY);
19786
20368
  probeImageAt(clientX, clientY);
@@ -20059,6 +20641,44 @@ function OhhwellsBridge() {
20059
20641
  if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
20060
20642
  }, 400));
20061
20643
  };
20644
+ const reapCommittedAiSections = (excludeIds) => {
20645
+ const aiState = parseAiSectionsState(aiSectionsRef.current);
20646
+ if (aiState.sections.length === 0) return [];
20647
+ let orderEntries = [];
20648
+ try {
20649
+ const parsed = JSON.parse(editContentRef.current[SECTION_ORDER_KEY] ?? "[]");
20650
+ if (Array.isArray(parsed)) orderEntries = parsed;
20651
+ } catch {
20652
+ return [];
20653
+ }
20654
+ const removedIds = orderEntries.filter((entry) => entry && typeof entry.instanceId === "string" && entry.removed).map((entry) => entry.instanceId);
20655
+ if (removedIds.length === 0) return [];
20656
+ const result = reapRemovedAiSections(aiState, parseStyleStore(stylesRef.current), removedIds, excludeIds);
20657
+ if (!result.changed) return [];
20658
+ const nodes = [];
20659
+ aiSectionsRef.current = serializeAiSectionsState(result.state);
20660
+ nodes.push({ key: AI_SECTIONS_KEY, text: aiSectionsRef.current });
20661
+ const reaped = new Set(result.reapedIds);
20662
+ const nextOrderJson = JSON.stringify(orderEntries.filter((entry) => !reaped.has(entry.instanceId)));
20663
+ editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: nextOrderJson };
20664
+ setAiSectionOrder(nextOrderJson, window.location.pathname);
20665
+ nodes.push({ key: SECTION_ORDER_KEY, text: nextOrderJson });
20666
+ if (result.store) {
20667
+ stylesRef.current = JSON.stringify(result.store);
20668
+ nodes.push({ key: STYLE_STORE_KEY, text: stylesRef.current });
20669
+ }
20670
+ const nextContent = { ...editContentRef.current };
20671
+ for (const key of Object.keys(nextContent)) {
20672
+ if (key.startsWith(AI_SLOT_KEY_PREFIX) && result.slotPrefixes.some((prefix) => key.startsWith(prefix)) && nextContent[key] !== "") {
20673
+ nextContent[key] = "";
20674
+ nodes.push({ key, text: "" });
20675
+ }
20676
+ }
20677
+ editContentRef.current = nextContent;
20678
+ applyAiSectionsToDom(result.state);
20679
+ applyStylesToDom(parseStyleStore(stylesRef.current));
20680
+ return nodes;
20681
+ };
20062
20682
  const handleHydrate = (e) => {
20063
20683
  if (e.data?.type !== "ow:hydrate") return;
20064
20684
  const content = e.data.content;
@@ -20077,9 +20697,11 @@ function OhhwellsBridge() {
20077
20697
  }
20078
20698
  if (typeof content[STYLE_STORE_KEY] === "string") {
20079
20699
  stylesRef.current = content[STYLE_STORE_KEY];
20700
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
20080
20701
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
20081
20702
  }
20082
20703
  applyBrandChrome(content);
20704
+ initSectionInstancesFromContent(content, window.location.pathname);
20083
20705
  let sectionsJson = null;
20084
20706
  for (const [key, val] of Object.entries(content)) {
20085
20707
  if (key === "__ohw_sections") {
@@ -20087,11 +20709,11 @@ function OhhwellsBridge() {
20087
20709
  continue;
20088
20710
  }
20089
20711
  if (key === AI_SECTIONS_KEY) continue;
20712
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
20713
+ if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
20090
20714
  if (key === BRAND_KIT_KEY) continue;
20091
20715
  if (key === STYLE_STORE_KEY) continue;
20092
20716
  if (BRAND_CHROME_KEYS.has(key)) continue;
20093
- if (key === LOGO_PLACEHOLDER_KEY) continue;
20094
- if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
20095
20717
  if (applyVideoSettingNode(key, val)) continue;
20096
20718
  if (applyCarouselNode(key, val)) continue;
20097
20719
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -20105,6 +20727,8 @@ function OhhwellsBridge() {
20105
20727
  if (video && video.src !== val) applyVideoSrc(video, val);
20106
20728
  } else if (el.dataset.ohwEditable === "link") {
20107
20729
  applyLinkHref(el, val);
20730
+ } else if (el.dataset.ohwEditable === "map") {
20731
+ applyMapQuery(el, val);
20108
20732
  } else if (el.dataset.ohwEditable === "icon") {
20109
20733
  applyIconMarkup(el, val);
20110
20734
  } else if (isIconMarkupValue(val)) {
@@ -20121,12 +20745,16 @@ function OhhwellsBridge() {
20121
20745
  sectionsLoadedRef.current = true;
20122
20746
  pendingScheduleConfigRequests.current.splice(0).forEach(processConfigRequest);
20123
20747
  }
20124
- initSectionInstancesFromContent(content, window.location.pathname);
20125
20748
  editContentRef.current = { ...editContentRef.current, ...content };
20126
20749
  reconcileNavbarItemsFromContent(editContentRef.current);
20127
20750
  reconcileFooterOrderFromContent(editContentRef.current);
20128
20751
  syncNavigationDragCursorAttrs();
20129
20752
  enforceLinkHrefs();
20753
+ const hydrateReapExclude = /* @__PURE__ */ new Set();
20754
+ const hydratePendingUndo = pendingDeleteUndoRef.current;
20755
+ if (hydratePendingUndo?.sectionInstanceId) hydrateReapExclude.add(hydratePendingUndo.sectionInstanceId);
20756
+ const reapNodes = reapCommittedAiSections(hydrateReapExclude);
20757
+ if (reapNodes.length > 0) postToParentRef.current({ type: "ow:change", nodes: reapNodes });
20130
20758
  const hydratedHeight = document.body.scrollHeight;
20131
20759
  if (hydratedHeight > 50) postToParentRef.current({ type: "ow:height", height: hydratedHeight });
20132
20760
  postToParentRef.current({ type: "ow:hydrate-done" });
@@ -20266,12 +20894,35 @@ function OhhwellsBridge() {
20266
20894
  window.addEventListener("message", handleAiSetBrand);
20267
20895
  const handleAiSetStyles = (e) => {
20268
20896
  if (e.data?.type !== "ow:ai-set-styles") return;
20269
- const value = typeof e.data.value === "string" ? e.data.value : "";
20897
+ let value = typeof e.data.value === "string" ? e.data.value : "";
20270
20898
  const previous = stylesRef.current;
20899
+ let previousSections;
20900
+ const store = parseStyleStore(value);
20901
+ if (store) {
20902
+ const folded = foldAlignIntoTrees(parseAiSectionsState(aiSectionsRef.current), store);
20903
+ if (folded.changed) {
20904
+ const nextSections = serializeAiSectionsState(folded.state);
20905
+ if (nextSections !== aiSectionsRef.current) {
20906
+ previousSections = aiSectionsRef.current;
20907
+ aiSectionsRef.current = nextSections;
20908
+ applyAiSectionsToDom(folded.state);
20909
+ postToParentRef.current({
20910
+ type: "ow:change",
20911
+ nodes: [{ key: AI_SECTIONS_KEY, text: nextSections }]
20912
+ });
20913
+ }
20914
+ value = JSON.stringify(folded.store);
20915
+ }
20916
+ }
20271
20917
  stylesRef.current = value;
20272
20918
  applyStylesToDom(parseStyleStore(value));
20273
20919
  postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
20274
- postToParentRef.current({ type: "ow:ai-styles-applied", previous, value });
20920
+ postToParentRef.current({
20921
+ type: "ow:ai-styles-applied",
20922
+ previous,
20923
+ value,
20924
+ ...previousSections !== void 0 ? { previousSections } : {}
20925
+ });
20275
20926
  };
20276
20927
  window.addEventListener("message", handleAiSetStyles);
20277
20928
  const handleGetBrand = (e) => {
@@ -20310,6 +20961,7 @@ function OhhwellsBridge() {
20310
20961
  if (!entries) return;
20311
20962
  const orderJson = JSON.stringify(entries);
20312
20963
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20964
+ setAiSectionOrder(orderJson, window.location.pathname);
20313
20965
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
20314
20966
  aiSectionApiRef.current?.clear();
20315
20967
  window.dispatchEvent(new Event("resize"));
@@ -20318,6 +20970,7 @@ function OhhwellsBridge() {
20318
20970
  const actionId = newInstanceId();
20319
20971
  pendingDeleteUndoRef.current = {
20320
20972
  actionId,
20973
+ sectionInstanceId: instanceId,
20321
20974
  restore: () => {
20322
20975
  const restoredEntries = getPageSectionOrderEntries(
20323
20976
  editContentRef.current[SECTION_ORDER_KEY],
@@ -20327,6 +20980,7 @@ function OhhwellsBridge() {
20327
20980
  if (!restored) return;
20328
20981
  const restoredJson = JSON.stringify(restored);
20329
20982
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: restoredJson };
20983
+ setAiSectionOrder(restoredJson, window.location.pathname);
20330
20984
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: restoredJson }] });
20331
20985
  window.dispatchEvent(new Event("resize"));
20332
20986
  const restoreHeight = document.body.scrollHeight;
@@ -20343,6 +20997,34 @@ function OhhwellsBridge() {
20343
20997
  });
20344
20998
  };
20345
20999
  window.addEventListener("message", handleDeleteSection);
21000
+ const handleDuplicateSection = (e) => {
21001
+ if (e.data?.type !== "ow:duplicate-section") return;
21002
+ const instanceId = typeof e.data.instanceId === "string" ? e.data.instanceId : "";
21003
+ if (!instanceId) return;
21004
+ const newId = newInstanceId();
21005
+ const currentEntries = getPageSectionOrderEntries(editContentRef.current[SECTION_ORDER_KEY], window.location.pathname);
21006
+ const result = duplicateSectionInstance(instanceId, newId, window.location.pathname, currentEntries);
21007
+ if (!result) return;
21008
+ const { entries, keyRekeys } = result;
21009
+ const orderJson = JSON.stringify(entries);
21010
+ const nodes = [{ key: SECTION_ORDER_KEY, text: orderJson }];
21011
+ for (const { from, to } of keyRekeys) {
21012
+ const inherited = editContentRef.current[from];
21013
+ if (inherited !== void 0) nodes.push({ key: to, text: inherited });
21014
+ }
21015
+ editContentRef.current = {
21016
+ ...editContentRef.current,
21017
+ ...Object.fromEntries(nodes.map(({ key, text }) => [key, text]))
21018
+ };
21019
+ setAiSectionOrder(orderJson, window.location.pathname);
21020
+ postToParentRef.current({ type: "ow:change", nodes });
21021
+ window.dispatchEvent(new Event("resize"));
21022
+ const duplicateHeight = document.body.scrollHeight;
21023
+ if (duplicateHeight > 50) postToParentRef.current({ type: "ow:height", height: duplicateHeight });
21024
+ const clone = document.querySelector(`[data-ohw-instance="${CSS.escape(newId)}"]`);
21025
+ if (clone) aiSectionApiRef.current?.selectFromElement(clone);
21026
+ };
21027
+ window.addEventListener("message", handleDuplicateSection);
20346
21028
  const handleDeactivate = (e) => {
20347
21029
  if (e.data?.type !== "ow:deactivate") return;
20348
21030
  if (document.documentElement.hasAttribute("data-ohw-panel-dragging")) return;
@@ -20352,6 +21034,12 @@ function OhhwellsBridge() {
20352
21034
  closeLinkPopoverRef.current();
20353
21035
  return;
20354
21036
  }
21037
+ if (floatingPanelOpenRef.current) {
21038
+ setFloatingPanelRef.current(null);
21039
+ deselectRef.current();
21040
+ deactivateRef.current();
21041
+ return;
21042
+ }
20355
21043
  deselectRef.current();
20356
21044
  deactivateRef.current();
20357
21045
  clearMediaSelectionRef.current();
@@ -20597,6 +21285,10 @@ function OhhwellsBridge() {
20597
21285
  };
20598
21286
  const handleSave = (e) => {
20599
21287
  if (e.data?.type !== "ow:save") return;
21288
+ const pendingUndo = pendingDeleteUndoRef.current;
21289
+ const reapExclude = /* @__PURE__ */ new Set();
21290
+ if (pendingUndo?.sectionInstanceId) reapExclude.add(pendingUndo.sectionInstanceId);
21291
+ const reapNodes = reapCommittedAiSections(reapExclude);
20600
21292
  const nodes = collectEditableNodes(editContentRef.current);
20601
21293
  const tracker = document.querySelector("[data-ohw-sections-tracker]");
20602
21294
  if (tracker?.textContent) nodes.push({ key: "__ohw_sections", type: "sections", text: tracker.textContent });
@@ -20616,6 +21308,11 @@ function OhhwellsBridge() {
20616
21308
  const text = specs.length ? JSON.stringify(specs) : editContentRef.current[fieldsKey(formKey)];
20617
21309
  if (text) nodes.push({ key: fieldsKey(formKey), type: "text", text });
20618
21310
  });
21311
+ for (const reapNode of reapNodes) {
21312
+ if (reapNode.key.startsWith(AI_SLOT_KEY_PREFIX) && !nodes.some((n) => n.key === reapNode.key)) {
21313
+ nodes.push({ key: reapNode.key, type: "text", text: reapNode.text });
21314
+ }
21315
+ }
20619
21316
  postToParentRef.current({ type: "ow:save-result", nodes });
20620
21317
  };
20621
21318
  const handleInsertSection = (e) => {
@@ -20626,8 +21323,12 @@ function OhhwellsBridge() {
20626
21323
  if (inserted) {
20627
21324
  const tracker = getSectionsTracker();
20628
21325
  postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
20629
- const h = document.body.scrollHeight;
20630
- if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
21326
+ const reportHeight = () => {
21327
+ const h = document.body.scrollHeight;
21328
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
21329
+ };
21330
+ reportHeight();
21331
+ setTimeout(reportHeight, 500);
20631
21332
  }
20632
21333
  };
20633
21334
  const handleSwitchSchedule = (e) => {
@@ -21029,11 +21730,12 @@ function OhhwellsBridge() {
21029
21730
  window.removeEventListener("message", handleMoveSection);
21030
21731
  window.removeEventListener("message", handlePanelDragging);
21031
21732
  window.removeEventListener("message", handleDeleteSection);
21733
+ window.removeEventListener("message", handleDuplicateSection);
21032
21734
  window.removeEventListener("message", handleDeactivate);
21033
- document.documentElement.removeAttribute("data-ohw-panel-dragging");
21034
21735
  window.removeEventListener("message", handleToastAction);
21035
21736
  window.removeEventListener("message", handleFormCount);
21036
21737
  window.removeEventListener("message", handleUiEscape);
21738
+ document.documentElement.removeAttribute("data-ohw-panel-dragging");
21037
21739
  autoSaveTimers.current.forEach(clearTimeout);
21038
21740
  autoSaveTimers.current.clear();
21039
21741
  if (imageUnhoverTimerRef.current) clearTimeout(imageUnhoverTimerRef.current);
@@ -21236,7 +21938,7 @@ function OhhwellsBridge() {
21236
21938
  postToParent2({
21237
21939
  type: "ow:ready",
21238
21940
  version: "1",
21239
- bridgeVersion: "0.1.84",
21941
+ bridgeVersion: "0.1.86",
21240
21942
  path: pathname,
21241
21943
  nodes: collectEditableNodes(editContentRef.current),
21242
21944
  sections
@@ -22155,6 +22857,59 @@ function OhhwellsBridge() {
22155
22857
  ) : null
22156
22858
  ] });
22157
22859
  }
22860
+
22861
+ // src/ui/EmptySection.tsx
22862
+ var import_link = __toESM(require("next/link"), 1);
22863
+ var import_jsx_runtime34 = require("react/jsx-runtime");
22864
+ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey }) {
22865
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
22866
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
22867
+ "p",
22868
+ {
22869
+ style: {
22870
+ fontFamily: "var(--brand-font-body)",
22871
+ fontSize: "0.75rem",
22872
+ fontWeight: 500,
22873
+ letterSpacing: "0.15em",
22874
+ textTransform: "uppercase",
22875
+ color: "var(--brand-accent)",
22876
+ marginBottom: "1.5rem"
22877
+ },
22878
+ 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" }) })
22879
+ }
22880
+ ),
22881
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
22882
+ "h1",
22883
+ {
22884
+ style: {
22885
+ fontFamily: "var(--brand-font-heading)",
22886
+ fontSize: "clamp(2rem, 3.5vw, 2.75rem)",
22887
+ lineHeight: 1.1,
22888
+ letterSpacing: "-0.025em",
22889
+ color: "var(--brand-text)",
22890
+ marginBottom: "1rem"
22891
+ },
22892
+ ...titleKey ? { "data-ohw-editable": "text", "data-ohw-key": titleKey, "data-ohw-max-length": 80 } : {},
22893
+ children: title
22894
+ }
22895
+ ),
22896
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
22897
+ "p",
22898
+ {
22899
+ style: {
22900
+ fontFamily: "var(--brand-font-body)",
22901
+ fontSize: "1rem",
22902
+ lineHeight: 1.7,
22903
+ fontWeight: 300,
22904
+ color: "var(--brand-text-muted)",
22905
+ maxWidth: "340px"
22906
+ },
22907
+ ...subtitleKey ? { "data-ohw-editable": "text", "data-ohw-key": subtitleKey, "data-ohw-max-length": 160 } : {},
22908
+ children: "This page doesn't have any content yet."
22909
+ }
22910
+ )
22911
+ ] });
22912
+ }
22158
22913
  // Annotate the CommonJS export names for ESM import in node:
22159
22914
  0 && (module.exports = {
22160
22915
  AI_DEFAULT_BRAND,
@@ -22172,6 +22927,7 @@ function OhhwellsBridge() {
22172
22927
  DropdownMenuItem,
22173
22928
  DropdownMenuSeparator,
22174
22929
  DropdownMenuTrigger,
22930
+ EmptySection,
22175
22931
  ItemActionToolbar,
22176
22932
  ItemInteractionLayer,
22177
22933
  LinkEditorPanel,