@ohhwells/bridge 0.1.87 → 0.1.88-next.262

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