@ohhwells/bridge 0.1.152 → 0.1.153-next.415

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
@@ -83,6 +83,7 @@ __export(index_exports, {
83
83
  foldAlignIntoTrees: () => foldAlignIntoTrees,
84
84
  getEditModeInitialState: () => getEditModeInitialState,
85
85
  hasMountedContentNodes: () => hasMountedContentNodes,
86
+ indexCollectionItems: () => indexCollectionItems,
86
87
  insertFooterHours: () => insertFooterHours,
87
88
  isEditSessionActive: () => isEditSessionActive,
88
89
  isPlainLeftClick: () => isPlainLeftClick,
@@ -90,6 +91,7 @@ __export(index_exports, {
90
91
  isRenderableTree: () => isRenderableTree,
91
92
  isStickySearchParam: () => isStickySearchParam,
92
93
  isValidUrl: () => isValidUrl,
94
+ matchCollectionItemClick: () => matchCollectionItemClick,
93
95
  nextViableSlide: () => nextViableSlide,
94
96
  parseAiFooterState: () => parseAiFooterState,
95
97
  parseAiSectionsState: () => parseAiSectionsState,
@@ -6652,6 +6654,21 @@ function pricingCardFeatureCount(tree, index) {
6652
6654
  const target = findPricingCollection(tree)?.children?.[index];
6653
6655
  return target ? pricingFeatureList(target).length : 0;
6654
6656
  }
6657
+ function prevailingFeatureIcon(features) {
6658
+ const counts = /* @__PURE__ */ new Map();
6659
+ for (const row of features) {
6660
+ const key = `${row.icon ?? ""}|${row.iconColor ?? ""}`;
6661
+ const entry = counts.get(key) ?? { count: 0, ...row.icon ? { icon: row.icon } : {}, ...row.iconColor ? { iconColor: row.iconColor } : {} };
6662
+ entry.count += 1;
6663
+ counts.set(key, entry);
6664
+ }
6665
+ let best = null;
6666
+ for (const entry of counts.values()) {
6667
+ if (!best || entry.count > best.count) best = entry;
6668
+ }
6669
+ if (!best) return {};
6670
+ return { ...best.icon ? { icon: best.icon } : {}, ...best.iconColor ? { iconColor: best.iconColor } : {} };
6671
+ }
6655
6672
  function addPricingCardFeature(tree, index, text) {
6656
6673
  return withPricingCollection(tree, (collection) => {
6657
6674
  const children = collection.children;
@@ -6660,7 +6677,7 @@ function addPricingCardFeature(tree, index, text) {
6660
6677
  const features = pricingFeatureList(target);
6661
6678
  if (features.length >= PRICING_FEATURES_MAX) return false;
6662
6679
  target.slots = target.slots ?? {};
6663
- target.slots.features = [...features, { text }];
6680
+ target.slots.features = [...features, { text, ...prevailingFeatureIcon(features) }];
6664
6681
  return true;
6665
6682
  });
6666
6683
  }
@@ -7271,6 +7288,21 @@ function applyCustomCodeToHead(content) {
7271
7288
  if (beforeNodes.length > 0) document.head.append(...beforeNodes);
7272
7289
  }
7273
7290
 
7291
+ // src/lib/selectNavContainer.ts
7292
+ function isOnScreen(el) {
7293
+ const rect = el.getBoundingClientRect();
7294
+ if (rect.width <= 0 || rect.height <= 0) return false;
7295
+ if (el.offsetParent !== null) return true;
7296
+ return getComputedStyle(el).position === "fixed";
7297
+ }
7298
+ function findSelectableNavContainer(doc = document) {
7299
+ const visible = Array.from(doc.querySelectorAll("[data-ohw-nav-container]")).filter(isOnScreen);
7300
+ if (visible.length === 0) return null;
7301
+ return visible.reduce(
7302
+ (best, el) => el.getBoundingClientRect().top < best.getBoundingClientRect().top ? el : best
7303
+ );
7304
+ }
7305
+
7274
7306
  // src/ui/ai-tree/aiSectionsManager.tsx
7275
7307
  var import_react_dom = require("react-dom");
7276
7308
  var import_client = require("react-dom/client");
@@ -7844,7 +7876,7 @@ var AI_MOBILE_CSS = [
7844
7876
  // page's first section. Top only (bottom needs no nav clearance); ~96px approximates the
7845
7877
  // template nav's own height (measured ~94px) — there's no shared `--ohw-header-h` var to read
7846
7878
  // an exact figure from (no live measurement exists), so this is a safe approximation, not a
7847
- // pixel-exact fit. Horizontal spacing already comes from `data-ai-container`.
7879
+ // pixel-exact fit. Horizontal spacing already comes from `data-ai-section-inner`.
7848
7880
  '[data-ai-overlay-clearance] [data-ai-overlap-layer="overlay"]{padding-top:96px}',
7849
7881
  // A full-bleed carousel's height is pure arithmetic: it spans the whole window, so 16/9 turns
7850
7882
  // window width straight into height. Measured at 16/9 it came out ~90% of the viewport at EVERY
@@ -7860,8 +7892,10 @@ var AI_MOBILE_CSS = [
7860
7892
  "[data-ai-section]{overflow-x:hidden}",
7861
7893
  // `:not([data-ai-full-bleed])`: this `!important` gutter beat the inline `padding: 0` that a
7862
7894
  // full-bleed section's container sets, so a carousel/media hero that is edge-to-edge on desktop
7863
- // still got a 20px band of section background down both sides on a phone.
7864
- "[data-ai-container]:not([data-ai-full-bleed]){padding:0 20px !important}",
7895
+ // still got a 20px band of section background down both sides on a phone. The footer's own
7896
+ // container keeps the unguarded rule below — `data-ai-full-bleed` can never appear on it.
7897
+ "[data-ai-section-inner]:not([data-ai-full-bleed]){padding:0 20px !important}",
7898
+ "[data-ai-container]{padding:0 20px !important}",
7865
7899
  "[data-ai-row]{display:flex !important;flex-direction:column !important;align-items:stretch !important}",
7866
7900
  "[data-ai-cell]{width:100%;min-width:0}",
7867
7901
  // The rule above turns every row into a COLUMN flex container on phones, which silently breaks
@@ -8066,6 +8100,31 @@ function pricingCardEmphasis(featured, ctx, hasBg) {
8066
8100
  function textAttrs(ctx, path) {
8067
8101
  return ctx.keyFor ? { "data-ohw-key": ctx.keyFor(path), "data-ohw-editable": "text" } : {};
8068
8102
  }
8103
+ var AI_RESPONSIVE_CSS = [
8104
+ "@media (max-width: 960px) {",
8105
+ " [data-ai-responsive] [data-ai-grid] { grid-template-columns: repeat(2, 1fr) !important; }",
8106
+ ' [data-ai-responsive] [data-ai-grid="1"] { grid-template-columns: 1fr !important; }',
8107
+ "}",
8108
+ "@media (max-width: 640px) {",
8109
+ " [data-ai-responsive] [data-ai-grid] { grid-template-columns: 1fr !important; }",
8110
+ " [data-ai-responsive] [data-ai-columns] > * { grid-column: 1 / -1 !important; }",
8111
+ // Group containers flatten to a column on phones; span placements come along for free.
8112
+ // `:not([data-ai-group="overlap"])` for the same reason as the twin pair in AI_MOBILE_CSS — see
8113
+ // the comment there. This sheet's copy is the narrower selector, so without the exemption it
8114
+ // flattens an `overlap` hero below 640px even though the other rule stands down: measured a 390px
8115
+ // viewport with the heading/CTA in flow *under* the image, 219px of section background where the
8116
+ // layered text used to be.
8117
+ ' [data-ai-responsive] [data-ai-group]:not([data-ai-group="overlap"]) { display: flex !important; flex-direction: column !important; }',
8118
+ ' [data-ai-responsive] [data-ai-group]:not([data-ai-group="overlap"]) > * { grid-column: auto !important; }',
8119
+ // `:not([data-ai-full-bleed])` for the same reason as the twin rule in AI_MOBILE_CSS: this one
8120
+ // is narrower (0,2,0 vs 0,1,0) and also `!important`, so without the guard it wins outright and
8121
+ // a full-bleed hero gets its 20px side band back below 640px only — the one width where the
8122
+ // other rule had already been taught to stand down.
8123
+ " [data-ai-responsive] [data-ai-section-inner]:not([data-ai-full-bleed]) { padding-left: 20px !important; padding-right: 20px !important; }",
8124
+ " [data-ai-responsive] { overflow-x: hidden; }",
8125
+ " [data-ai-responsive] img { max-width: 100%; }",
8126
+ "}"
8127
+ ].join("\n");
8069
8128
  var TRANSPARENT_PX = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
8070
8129
  function MediaBox({
8071
8130
  refValue,
@@ -8120,7 +8179,7 @@ function MediaBox({
8120
8179
  position: fill ? "absolute" : void 0,
8121
8180
  inset: fill ? 0 : void 0,
8122
8181
  width: fill ? "100%" : fixedSize ? "auto" : "100%",
8123
- height: fill ? "100%" : void 0,
8182
+ height: fill ? "100%" : fixedSize ? height : void 0,
8124
8183
  aspectRatio: fill ? void 0 : aspectRatio,
8125
8184
  minHeight: fill || fixedSize ? void 0 : height ?? (aspectRatio ? void 0 : 180),
8126
8185
  borderRadius: radius,
@@ -9616,7 +9675,7 @@ function CollectionBlock({ node, ctx, path }) {
9616
9675
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
9617
9676
  "div",
9618
9677
  {
9619
- "data-ai-grid": "",
9678
+ "data-ai-grid": String(itemsPerRow),
9620
9679
  "data-ohw-pricing-collection": isPricing ? "" : void 0,
9621
9680
  style: {
9622
9681
  display: "grid",
@@ -9918,7 +9977,7 @@ function renderNode(node, ctx, path, ambientAlign) {
9918
9977
  },
9919
9978
  children: [
9920
9979
  i > 0 && ctx.heroOverlapFullBleed ? (
9921
- // The section's `data-ai-container` gave up its max-width/gutter so the base
9980
+ // The section's `data-ai-section-inner` gave up its max-width/gutter so the base
9922
9981
  // layer (this group's i===0 sibling) can span edge-to-edge — reintroduced here,
9923
9982
  // scoped to just the overlay text, so a heading/copy/CTA stack doesn't stretch
9924
9983
  // the full section width instead of reading at the usual content width.
@@ -10337,6 +10396,7 @@ function AiTreeRenderer({
10337
10396
  ...fillHeight ? { "data-ai-fill-height": "" } : {},
10338
10397
  ...needsOverlayClearance ? { "data-ai-overlay-clearance": "" } : {},
10339
10398
  ...bgAttrs,
10399
+ "data-ai-responsive": "",
10340
10400
  style: {
10341
10401
  position: "relative",
10342
10402
  padding: `${padCss} 0`,
@@ -10350,12 +10410,13 @@ function AiTreeRenderer({
10350
10410
  ...fillHeight ? { display: "grid" } : {}
10351
10411
  },
10352
10412
  children: [
10353
- isOverlay && backgroundUrl && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
10413
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: AI_RESPONSIVE_CSS }),
10354
10414
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: AI_MOBILE_CSS }),
10415
+ isOverlay && backgroundUrl && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
10355
10416
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
10356
10417
  "div",
10357
10418
  {
10358
- "data-ai-container": "",
10419
+ "data-ai-section-inner": "",
10359
10420
  ...fullBleedMedia ? { "data-ai-full-bleed": "" } : {},
10360
10421
  style: {
10361
10422
  position: "relative",
@@ -10372,7 +10433,7 @@ function AiTreeRenderer({
10372
10433
  children: tree.rows.map((row, r2) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
10373
10434
  "div",
10374
10435
  {
10375
- "data-ai-row": "",
10436
+ "data-ai-columns": "",
10376
10437
  style: {
10377
10438
  display: "grid",
10378
10439
  gridTemplateColumns: "repeat(12, 1fr)",
@@ -11034,9 +11095,10 @@ function applyAiSectionsToDom(state, options) {
11034
11095
  if (existing && existing.serialized === serialized && existing.container.isConnected) {
11035
11096
  continue;
11036
11097
  }
11037
- const isNewMount = !existing || !existing.container.isConnected;
11038
11098
  let container = existing?.container;
11099
+ let isNewMount = false;
11039
11100
  if (!container || !container.isConnected) {
11101
+ isNewMount = true;
11040
11102
  existing?.root.unmount();
11041
11103
  container?.remove();
11042
11104
  container = document.createElement("div");
@@ -11680,6 +11742,172 @@ function useLinkHrefGuardian(...deps) {
11680
11742
  }, deps);
11681
11743
  }
11682
11744
 
11745
+ // src/ui/link-modal/linkModal.utils.ts
11746
+ function parseTarget(target) {
11747
+ if (!target) return { pageRoute: "", sectionId: null };
11748
+ const hashIndex = target.indexOf("#");
11749
+ if (hashIndex === -1) return { pageRoute: target, sectionId: null };
11750
+ return {
11751
+ pageRoute: target.slice(0, hashIndex),
11752
+ sectionId: target.slice(hashIndex + 1) || null
11753
+ };
11754
+ }
11755
+ function normalizePath(path) {
11756
+ const trimmed = path.trim();
11757
+ if (!trimmed) return "/";
11758
+ try {
11759
+ if (/^https?:\/\//i.test(trimmed)) {
11760
+ return new URL(trimmed).pathname || "/";
11761
+ }
11762
+ } catch {
11763
+ }
11764
+ const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
11765
+ if (withSlash.length > 1 && withSlash.endsWith("/")) return withSlash.slice(0, -1);
11766
+ return withSlash;
11767
+ }
11768
+ function findPageByPath(pages, path) {
11769
+ const normalized = normalizePath(path);
11770
+ return pages.find((p) => normalizePath(p.path) === normalized);
11771
+ }
11772
+ function inferPageTitleFromPath(path) {
11773
+ const normalized = normalizePath(path);
11774
+ if (normalized === "/") return "Home";
11775
+ const segment = normalized.split("/").filter(Boolean).pop() ?? "";
11776
+ if (!segment) return "Home";
11777
+ return segment.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
11778
+ }
11779
+ function isInternalPath(route) {
11780
+ const trimmed = route.trim();
11781
+ if (!trimmed) return false;
11782
+ if (/^https?:\/\//i.test(trimmed)) return false;
11783
+ if (trimmed.startsWith("/")) return true;
11784
+ if (trimmed.includes(" ") || trimmed.includes(".")) return false;
11785
+ return true;
11786
+ }
11787
+ function resolvePage(route, pages) {
11788
+ const found = findPageByPath(pages, route);
11789
+ if (found) return { path: found.path, title: found.title };
11790
+ return { path: normalizePath(route), title: inferPageTitleFromPath(route) };
11791
+ }
11792
+ function getSectionsForPath(sectionsByPath, pageRoute) {
11793
+ if (!sectionsByPath) return [];
11794
+ const normalized = normalizePath(pageRoute);
11795
+ return sectionsByPath[normalized] ?? sectionsByPath[pageRoute] ?? [];
11796
+ }
11797
+ function buildTarget(page, section) {
11798
+ if (section) return `${page.path}#${section.id}`;
11799
+ return page.path;
11800
+ }
11801
+ function isValidUrl(urlString) {
11802
+ try {
11803
+ const url = new URL(urlString);
11804
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
11805
+ const hostname = url.hostname;
11806
+ if (!hostname || hostname.length === 0) return false;
11807
+ if (hostname.includes(" ")) return false;
11808
+ if (hostname === "localhost") return true;
11809
+ if (!hostname.includes(".")) return false;
11810
+ if (!/[a-z]/i.test(hostname)) return false;
11811
+ return true;
11812
+ } catch {
11813
+ return false;
11814
+ }
11815
+ }
11816
+ function normalizeUrl(value) {
11817
+ const trimmed = value.trim();
11818
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
11819
+ return `https://${trimmed}`;
11820
+ }
11821
+ function validateUrlInput(value, pages, allPages) {
11822
+ const trimmed = value.trim();
11823
+ if (!trimmed) return "";
11824
+ const exactPage = allPages.find((p) => p.title.toLowerCase() === trimmed.toLowerCase());
11825
+ if (exactPage) return "";
11826
+ const hasPrefixMatch = pages.some((p) => p.title.toLowerCase().startsWith(trimmed.toLowerCase()));
11827
+ if (hasPrefixMatch) {
11828
+ return "Select a page from the list or enter a valid URL";
11829
+ }
11830
+ const hasScheme = /^https?:\/\//i.test(trimmed);
11831
+ if (!hasScheme && !trimmed.includes(".")) {
11832
+ return "Please enter a valid URL (e.g., https://example.com)";
11833
+ }
11834
+ if (!isValidUrl(normalizeUrl(trimmed))) {
11835
+ return "Please enter a valid URL (e.g., https://example.com)";
11836
+ }
11837
+ return "";
11838
+ }
11839
+ function getEditModeInitialState(target, pages, sections) {
11840
+ const empty = {
11841
+ searchValue: "",
11842
+ selectedPage: null,
11843
+ selectedSection: null,
11844
+ step: "input"
11845
+ };
11846
+ if (!target) return empty;
11847
+ if (isValidUrl(normalizeUrl(target))) {
11848
+ return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
11849
+ }
11850
+ const { pageRoute, sectionId } = parseTarget(target);
11851
+ if (sectionId) {
11852
+ if (isInternalPath(pageRoute)) {
11853
+ const page = resolvePage(pageRoute, pages);
11854
+ const sec = sections.find((s) => s.id === sectionId);
11855
+ return {
11856
+ searchValue: page.title,
11857
+ selectedPage: page,
11858
+ selectedSection: sec ? { id: sec.id, label: sec.label } : null,
11859
+ step: "input"
11860
+ };
11861
+ }
11862
+ return { searchValue: pageRoute || target, selectedPage: null, selectedSection: null, step: "input" };
11863
+ }
11864
+ if (isInternalPath(pageRoute)) {
11865
+ const page = resolvePage(pageRoute, pages);
11866
+ return {
11867
+ searchValue: page.title,
11868
+ selectedPage: page,
11869
+ selectedSection: null,
11870
+ step: "input"
11871
+ };
11872
+ }
11873
+ return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
11874
+ }
11875
+ function filterAvailablePages(pages, existingTargets) {
11876
+ const taken = new Set(existingTargets);
11877
+ return pages.filter((p) => !taken.has(p.path));
11878
+ }
11879
+
11880
+ // src/lib/collection-item-click.ts
11881
+ function indexCollectionItems(pages) {
11882
+ const index = /* @__PURE__ */ new Map();
11883
+ for (const page of pages) {
11884
+ if (page.kind !== "COLLECTION_ITEM" || !page.collection || !page.path) continue;
11885
+ index.set(normalizePath(page.path), { path: page.path, collection: page.collection });
11886
+ }
11887
+ return index;
11888
+ }
11889
+ function pathOfHref(href) {
11890
+ const trimmed = href.trim();
11891
+ if (!trimmed || trimmed.startsWith("#")) return null;
11892
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed) && !/^https?:/i.test(trimmed)) return null;
11893
+ const withoutHash = trimmed.split("#")[0];
11894
+ const withoutQuery = withoutHash.split("?")[0];
11895
+ if (!withoutQuery) return null;
11896
+ return normalizePath(withoutQuery);
11897
+ }
11898
+ function matchCollectionItemClick(target, index) {
11899
+ if (!target || index.size === 0) return null;
11900
+ const text = target.closest('[data-ohw-editable="text"], [data-ohw-editable="plain"]');
11901
+ if (!text) return null;
11902
+ const anchor = text.closest("a");
11903
+ if (!anchor || anchor.closest(CTA_BUTTON_SELECTOR)) return null;
11904
+ const href = anchor.getAttribute("href");
11905
+ if (!href) return null;
11906
+ const path = pathOfHref(href);
11907
+ if (!path) return null;
11908
+ return index.get(path) ?? null;
11909
+ }
11910
+
11683
11911
  // src/lib/sticky-search-params.ts
11684
11912
  var STICKY_EXACT = /* @__PURE__ */ new Set([
11685
11913
  // Site identity. Required in local dev and in the canvas editor's iframe, where there is no
@@ -17020,6 +17248,103 @@ function bindPublishedForms(apiUrl, subdomain, content = {}) {
17020
17248
  );
17021
17249
  }
17022
17250
 
17251
+ // src/lib/outage-beacon.ts
17252
+ var DEDUP_MS = 5 * 60 * 1e3;
17253
+ var lastSent = /* @__PURE__ */ new Map();
17254
+ function apiBase() {
17255
+ return process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
17256
+ }
17257
+ function bridgeVersion() {
17258
+ try {
17259
+ if (true) {
17260
+ return "0.1.153-next.415";
17261
+ }
17262
+ } catch {
17263
+ }
17264
+ if (typeof document !== "undefined") {
17265
+ return document.documentElement.getAttribute("data-ohw-bridge-version") || void 0;
17266
+ }
17267
+ return void 0;
17268
+ }
17269
+ function stampBridgeVersionOnDocument() {
17270
+ if (typeof document === "undefined") {
17271
+ return;
17272
+ }
17273
+ try {
17274
+ const v = true ? "0.1.153-next.415" : "";
17275
+ if (v) {
17276
+ document.documentElement.setAttribute("data-ohw-bridge-version", v);
17277
+ window.__OHW_BRIDGE_VERSION__ = v;
17278
+ }
17279
+ } catch {
17280
+ }
17281
+ }
17282
+ var RECOVERABLE_PAGE_ERROR = /Minified React error #(?:418|419|421|422|423|425)\b|Hydration failed because the initial UI does not match|Text content does not match server-rendered HTML|There was an error while hydrating|did not match\. Server:/i;
17283
+ function isVercelPreviewHost() {
17284
+ if (typeof location === "undefined") {
17285
+ return false;
17286
+ }
17287
+ return location.hostname.toLowerCase().endsWith(".vercel.ohhwells.site");
17288
+ }
17289
+ function reportOutageBeacon(args) {
17290
+ if (typeof window === "undefined" || !args.subdomain) {
17291
+ return;
17292
+ }
17293
+ if (isVercelPreviewHost()) {
17294
+ return;
17295
+ }
17296
+ if (args.reason === "page_error" && args.detail && RECOVERABLE_PAGE_ERROR.test(args.detail)) {
17297
+ return;
17298
+ }
17299
+ const key = `${args.subdomain}|${args.reason}`;
17300
+ const now = Date.now();
17301
+ const prev = lastSent.get(key) || 0;
17302
+ if (now - prev < DEDUP_MS) {
17303
+ return;
17304
+ }
17305
+ lastSent.set(key, now);
17306
+ const body = JSON.stringify({
17307
+ subdomain: args.subdomain,
17308
+ reason: args.reason,
17309
+ detail: args.detail?.slice(0, 300),
17310
+ templateId: args.templateId,
17311
+ bridgeVersion: bridgeVersion(),
17312
+ href: typeof location !== "undefined" ? location.href : void 0
17313
+ });
17314
+ const url = `${apiBase()}/api/public/sites/outage-beacon`;
17315
+ try {
17316
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
17317
+ const blob = new Blob([body], { type: "application/json" });
17318
+ if (navigator.sendBeacon(url, blob)) {
17319
+ return;
17320
+ }
17321
+ }
17322
+ void fetch(url, {
17323
+ method: "POST",
17324
+ headers: { "Content-Type": "application/json" },
17325
+ body,
17326
+ keepalive: true,
17327
+ cache: "no-store"
17328
+ }).catch(() => {
17329
+ });
17330
+ } catch {
17331
+ }
17332
+ }
17333
+ function armLoaderHangBeacon(subdomain, fetchState, hangMs = 15e3) {
17334
+ if (!subdomain || fetchState !== "loading") {
17335
+ return () => {
17336
+ };
17337
+ }
17338
+ const timer = window.setTimeout(() => {
17339
+ reportOutageBeacon({
17340
+ subdomain,
17341
+ reason: "loader_stuck",
17342
+ detail: `fetchState still loading after ${hangMs}ms`
17343
+ });
17344
+ }, hangMs);
17345
+ return () => window.clearTimeout(timer);
17346
+ }
17347
+
17023
17348
  // src/lib/form-fields.ts
17024
17349
  var FIELD_DEFAULTS = {
17025
17350
  "short-text": { label: "Short text", placeholder: "Type placeholder text..." },
@@ -17620,6 +17945,7 @@ function MediaOverlay({
17620
17945
  (prev) => prev && prev.fullWidth === width && prev.height === height ? prev : { fullWidth: width, height }
17621
17946
  );
17622
17947
  }, [isVideo]);
17948
+ const replaceLabel = isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image";
17623
17949
  const replaceMode = buttonSize ? replaceButtonMode({ width: rect.width, height: rect.height }, buttonSize) : "none";
17624
17950
  const box = {
17625
17951
  position: "fixed",
@@ -17718,11 +18044,10 @@ function MediaOverlay({
17718
18044
  className: "flex items-center justify-center cursor-pointer",
17719
18045
  style: {
17720
18046
  ...box,
17721
- // When text overlays this media, let clicks fall THROUGH to the text underneath. In the
17722
- // parent this needed a whole `ow:click-at` round-trip to re-hit-test inside the iframe;
17723
- // in-document, pointer-events does it natively. The button below opts back in, so
17724
- // Replace still works.
17725
- pointerEvents: hover.hasTextOverlap || cueOnly ? "none" : "auto",
18047
+ // Visual only. `pointer-events: auto` on this full-bleed rect covered the navbar
18048
+ // (hero bg includes the absolute TopNav) and swallowed CTA press-drag. Text overlay
18049
+ // and cueOnly also need clicks to fall through — Replace / video controls opt back in.
18050
+ pointerEvents: "none",
17726
18051
  // Selected: a firm component ring with no wash, so the image reads as chosen rather
17727
18052
  // than hovered. Hover keeps the existing tinted preview.
17728
18053
  boxShadow: selected ? "inset 0 0 0 2px var(--color-bridge-primary)" : "inset 0 0 0 1.5px var(--color-bridge-primary)",
@@ -17749,17 +18074,17 @@ function MediaOverlay({
17749
18074
  },
17750
18075
  children: [
17751
18076
  isVideo ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react5.Film, { size: 14 }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react5.ImageIcon, { size: 14 }),
17752
- isVideo ? "Replace video" : "Replace image"
18077
+ replaceLabel
17753
18078
  ]
17754
18079
  }
17755
18080
  ),
17756
- replaceMode === "none" || cueOnly ? null : /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
18081
+ showChrome && replaceMode !== "none" && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
17757
18082
  Button,
17758
18083
  {
17759
18084
  "data-ohw-media-overlay": "",
17760
18085
  variant: "outline",
17761
18086
  size: "sm",
17762
- "aria-label": isVideo ? "Replace video" : "Replace image",
18087
+ "aria-label": replaceLabel,
17763
18088
  className: "gap-1.5 cursor-pointer hover:bg-background",
17764
18089
  style: {
17765
18090
  ...OVERLAY_BUTTON_STYLE,
@@ -17782,7 +18107,7 @@ function MediaOverlay({
17782
18107
  },
17783
18108
  children: [
17784
18109
  isVideo ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react5.Film, { size: 14 }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react5.ImageIcon, { size: 14 }),
17785
- replaceMode === "full" ? isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image" : null
18110
+ replaceMode === "full" ? replaceLabel : null
17786
18111
  ]
17787
18112
  }
17788
18113
  )
@@ -18292,7 +18617,7 @@ function AiSectionOverlay({
18292
18617
 
18293
18618
  // src/ui/ai-section/AiMediaStripOverlay.tsx
18294
18619
  var import_react10 = require("react");
18295
- var import_lucide_react8 = require("lucide-react");
18620
+ var import_lucide_react9 = require("lucide-react");
18296
18621
 
18297
18622
  // src/lib/ai-media-strip-tree.ts
18298
18623
  var MEDIA_STRIP_ITEM_MIN = 1;
@@ -18406,14 +18731,6 @@ function setMediaStripItemAsset(tree, index, assetId) {
18406
18731
  return true;
18407
18732
  });
18408
18733
  }
18409
- function setMediaStripItemAlt(tree, index, alt) {
18410
- return withMediaStrip(tree, (block) => {
18411
- const items = block.items;
18412
- if (index < 0 || index >= items.length) return false;
18413
- items[index] = { ...items[index], alt };
18414
- return true;
18415
- });
18416
- }
18417
18734
  function setMediaStripItemLink(tree, index, link) {
18418
18735
  return withMediaStrip(tree, (block) => {
18419
18736
  const items = block.items;
@@ -18431,40 +18748,167 @@ function setMediaStripItemLink(tree, index, link) {
18431
18748
  });
18432
18749
  }
18433
18750
 
18434
- // src/ui/ai-section/AiMediaStripOverlay.tsx
18751
+ // src/ui/dialog.tsx
18752
+ var React9 = __toESM(require("react"), 1);
18753
+ var import_radix_ui6 = require("radix-ui");
18754
+ var import_lucide_react8 = require("lucide-react");
18435
18755
  var import_jsx_runtime20 = require("react/jsx-runtime");
18436
- var PRIMARY3 = "#0885FE";
18437
- var OHW_BLUE = `var(--ohw-primary, ${PRIMARY3})`;
18438
- var OHW_BLUE_DARK = `color-mix(in srgb, var(--ohw-primary, ${PRIMARY3}) 60%, #000000)`;
18439
- function findMediaStripItemElement(sectionId, index) {
18440
- const sectionEl = findByInstanceId(sectionId);
18441
- if (!sectionEl) return null;
18442
- const items = sectionEl.querySelectorAll("[data-ohw-media-strip-item]");
18443
- for (const el of Array.from(items)) {
18444
- if (Number(el.dataset.ohwMediaStripIndex) === index) return el;
18445
- }
18446
- return null;
18756
+ function Dialog2({
18757
+ ...props
18758
+ }) {
18759
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_radix_ui6.Dialog.Root, { "data-slot": "dialog", ...props });
18447
18760
  }
18448
- function readRect2(ref) {
18449
- const el = ref && findMediaStripItemElement(ref.sectionId, ref.index);
18450
- if (!el) return null;
18451
- const r2 = el.getBoundingClientRect();
18452
- if (r2.width <= 0 || r2.height <= 0) return null;
18453
- return { top: r2.top, left: r2.left, width: r2.width, height: r2.height };
18761
+ function DialogPortal({
18762
+ ...props
18763
+ }) {
18764
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_radix_ui6.Dialog.Portal, { ...props });
18454
18765
  }
18455
- function useLiveItemRect(ref) {
18456
- const [rect, setRect] = (0, import_react10.useState)(null);
18457
- (0, import_react10.useEffect)(() => {
18458
- if (!ref) {
18459
- setRect(null);
18460
- return;
18766
+ function DialogOverlay({
18767
+ className,
18768
+ ...props
18769
+ }) {
18770
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18771
+ import_radix_ui6.Dialog.Overlay,
18772
+ {
18773
+ "data-slot": "dialog-overlay",
18774
+ "data-ohw-link-modal-root": "",
18775
+ className: cn("fixed inset-0 z-[2147483646] bg-black/50", className),
18776
+ ...props
18461
18777
  }
18462
- const update = () => {
18463
- const next = readRect2(ref);
18464
- setRect(
18465
- (prev) => prev && next && prev.top === next.top && prev.left === next.left && prev.width === next.width && prev.height === next.height ? prev : next
18466
- );
18467
- };
18778
+ );
18779
+ }
18780
+ var DialogContent = React9.forwardRef(
18781
+ ({ className, children, showCloseButton = true, container, ...props }, ref) => {
18782
+ const positionMode = container ? "absolute" : "fixed";
18783
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(DialogPortal, { container: container ?? void 0, children: [
18784
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(DialogOverlay, { className: cn(positionMode, "inset-0") }),
18785
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
18786
+ import_radix_ui6.Dialog.Content,
18787
+ {
18788
+ ref,
18789
+ "data-slot": "dialog-content",
18790
+ className: cn(
18791
+ positionMode,
18792
+ "left-1/2 top-1/2 z-[2147483647] flex w-full max-w-[448px] -translate-x-1/2 -translate-y-1/2 flex-col overflow-visible",
18793
+ "rounded-xl border border-border bg-background font-sans shadow-lg outline-none",
18794
+ container ? "max-h-[calc(100%-32px)]" : "max-h-[calc(100vh-32px)]",
18795
+ className
18796
+ ),
18797
+ onMouseDown: (e) => e.stopPropagation(),
18798
+ ...props,
18799
+ children: [
18800
+ children,
18801
+ showCloseButton ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18802
+ import_radix_ui6.Dialog.Close,
18803
+ {
18804
+ type: "button",
18805
+ className: "absolute right-[9px] top-[9px] rounded-sm p-1.5 text-foreground hover:bg-muted/50",
18806
+ "aria-label": "Close",
18807
+ children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react8.X, { size: 16, "aria-hidden": true })
18808
+ }
18809
+ ) : null
18810
+ ]
18811
+ }
18812
+ )
18813
+ ] });
18814
+ }
18815
+ );
18816
+ DialogContent.displayName = import_radix_ui6.Dialog.Content.displayName;
18817
+ function DialogHeader({
18818
+ className,
18819
+ ...props
18820
+ }) {
18821
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: cn("flex flex-col gap-1.5", className), ...props });
18822
+ }
18823
+ function DialogFooter({
18824
+ className,
18825
+ ...props
18826
+ }) {
18827
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18828
+ "div",
18829
+ {
18830
+ className: cn("flex items-center justify-end gap-2", className),
18831
+ ...props
18832
+ }
18833
+ );
18834
+ }
18835
+ var DialogTitle = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18836
+ import_radix_ui6.Dialog.Title,
18837
+ {
18838
+ ref,
18839
+ className: cn(
18840
+ "text-lg font-semibold leading-none tracking-tight text-card-foreground",
18841
+ className
18842
+ ),
18843
+ ...props
18844
+ }
18845
+ ));
18846
+ DialogTitle.displayName = import_radix_ui6.Dialog.Title.displayName;
18847
+ var DialogDescription = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18848
+ import_radix_ui6.Dialog.Description,
18849
+ {
18850
+ ref,
18851
+ className: cn("text-sm text-muted-foreground", className),
18852
+ ...props
18853
+ }
18854
+ ));
18855
+ DialogDescription.displayName = import_radix_ui6.Dialog.Description.displayName;
18856
+ var DialogClose = import_radix_ui6.Dialog.Close;
18857
+
18858
+ // src/ui/ai-section/AiMediaStripOverlay.tsx
18859
+ var import_jsx_runtime21 = require("react/jsx-runtime");
18860
+ var PRIMARY3 = "var(--ohw-primary, #0885FE)";
18861
+ var CHROME_SELECTOR = "[data-ohw-toolbar], [data-ohw-edit-chrome], [data-ohw-item-interaction], [data-ohw-link-modal-root], [data-ohw-section-picker], [data-ohw-ai-review], [data-ohw-more-menu]";
18862
+ function findMediaStripItemElement(sectionId, index) {
18863
+ const sectionEl = findByInstanceId(sectionId);
18864
+ if (!sectionEl) return null;
18865
+ const items = sectionEl.querySelectorAll("[data-ohw-media-strip-item]");
18866
+ for (const el of Array.from(items)) {
18867
+ if (Number(el.dataset.ohwMediaStripIndex) === index) return el;
18868
+ }
18869
+ return null;
18870
+ }
18871
+ function itemRefFromTarget(target) {
18872
+ if (!(target instanceof Element)) return null;
18873
+ if (target.closest(CHROME_SELECTOR)) return null;
18874
+ const itemEl = target.closest("[data-ohw-media-strip-item]");
18875
+ if (!itemEl) return null;
18876
+ const sectionEl = itemEl.closest("[data-ohw-section]");
18877
+ const unit = sectionEl ? movableUnit(sectionEl) : null;
18878
+ const sectionId = unit ? instanceIdOf(unit) || null : null;
18879
+ const index = Number(itemEl.dataset.ohwMediaStripIndex);
18880
+ if (!sectionId || Number.isNaN(index)) return null;
18881
+ return { sectionId, index };
18882
+ }
18883
+ function sameRef(a, b) {
18884
+ return a?.sectionId === b?.sectionId && a?.index === b?.index;
18885
+ }
18886
+ function rectOf(el) {
18887
+ if (!el) return null;
18888
+ const r2 = el.getBoundingClientRect();
18889
+ if (r2.width <= 0 || r2.height <= 0) return null;
18890
+ return { top: r2.top, left: r2.left, width: r2.width, height: r2.height };
18891
+ }
18892
+ function readRect2(ref) {
18893
+ return rectOf(ref && findMediaStripItemElement(ref.sectionId, ref.index));
18894
+ }
18895
+ function readStripRect(ref) {
18896
+ const item = ref && findMediaStripItemElement(ref.sectionId, ref.index);
18897
+ return rectOf(item?.closest("[data-ohw-media-strip]") ?? item);
18898
+ }
18899
+ function useLiveItemRect(ref, strip = false) {
18900
+ const [rect, setRect] = (0, import_react10.useState)(null);
18901
+ (0, import_react10.useEffect)(() => {
18902
+ if (!ref) {
18903
+ setRect(null);
18904
+ return;
18905
+ }
18906
+ const update = () => {
18907
+ const next = strip ? readStripRect(ref) : readRect2(ref);
18908
+ setRect(
18909
+ (prev) => prev && next && prev.top === next.top && prev.left === next.left && prev.width === next.width && prev.height === next.height ? prev : next
18910
+ );
18911
+ };
18468
18912
  update();
18469
18913
  const opts = { capture: true, passive: true };
18470
18914
  window.addEventListener("scroll", update, opts);
@@ -18475,9 +18919,49 @@ function useLiveItemRect(ref) {
18475
18919
  window.removeEventListener("resize", update);
18476
18920
  clearInterval(interval);
18477
18921
  };
18478
- }, [ref?.sectionId, ref?.index]);
18922
+ }, [ref?.sectionId, ref?.index, strip]);
18479
18923
  return rect;
18480
18924
  }
18925
+ var TOOLBAR_STRIP_GAP = 52;
18926
+ var TOOLBAR_STRIP_GAP_MOBILE = 42;
18927
+ var TOOLBAR_EDGE_MARGIN2 = 4;
18928
+ function StripToolbarSlot({ stripRect, children }) {
18929
+ const slotRef = (0, import_react10.useRef)(null);
18930
+ const [shift, setShift] = (0, import_react10.useState)(0);
18931
+ const centerX = stripRect.left + stripRect.width / 2;
18932
+ (0, import_react10.useLayoutEffect)(() => {
18933
+ const slot = slotRef.current;
18934
+ if (!slot) return;
18935
+ const clamp = () => {
18936
+ const w = slot.offsetWidth;
18937
+ if (w <= 0) return;
18938
+ const half = w / 2;
18939
+ const clamped = Math.max(TOOLBAR_EDGE_MARGIN2 + half, Math.min(centerX, window.innerWidth - TOOLBAR_EDGE_MARGIN2 - half));
18940
+ setShift((prev) => Math.abs(prev - (clamped - centerX)) < 0.5 ? prev : clamped - centerX);
18941
+ };
18942
+ clamp();
18943
+ const ro = new ResizeObserver(clamp);
18944
+ ro.observe(slot);
18945
+ return () => ro.disconnect();
18946
+ }, [centerX, children]);
18947
+ const mobile = window.innerWidth <= 480;
18948
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
18949
+ "div",
18950
+ {
18951
+ ref: slotRef,
18952
+ "data-ohw-bridge": "",
18953
+ "data-ohw-item-interaction": "",
18954
+ className: "pointer-events-auto fixed",
18955
+ style: {
18956
+ top: stripRect.top - (mobile ? TOOLBAR_STRIP_GAP_MOBILE : TOOLBAR_STRIP_GAP),
18957
+ left: centerX + shift,
18958
+ transform: "translate(-50%, -100%)",
18959
+ zIndex: 2147483645
18960
+ },
18961
+ children
18962
+ }
18963
+ );
18964
+ }
18481
18965
  function requestValidateTree(postToParent2, tree) {
18482
18966
  return new Promise((resolve) => {
18483
18967
  const id = `ai-validate-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -18514,116 +18998,72 @@ function requestAssetPick(postToParent2) {
18514
18998
  postToParent2({ type: "ow:asset-pick", id });
18515
18999
  });
18516
19000
  }
18517
- function Pill({
19001
+ function ToolbarAction({
18518
19002
  label,
18519
19003
  onClick,
18520
- disabled,
18521
- active
19004
+ disabled = false,
19005
+ active = false,
19006
+ children
18522
19007
  }) {
18523
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18524
- "button",
18525
- {
18526
- type: "button",
18527
- onClick,
18528
- disabled,
18529
- style: {
18530
- height: 28,
18531
- padding: "0 10px",
18532
- display: "inline-flex",
18533
- alignItems: "center",
18534
- gap: 4,
18535
- borderRadius: 999,
18536
- border: "none",
18537
- cursor: disabled ? "default" : "pointer",
18538
- background: active ? OHW_BLUE_DARK : "#ffffff",
18539
- color: active ? "#ffffff" : OHW_BLUE_DARK,
18540
- opacity: disabled ? 0.4 : 1,
18541
- fontSize: 12,
18542
- lineHeight: "16px",
18543
- fontFamily: "Figtree, ui-sans-serif, system-ui, sans-serif",
18544
- whiteSpace: "nowrap",
18545
- flex: "0 0 auto"
18546
- },
18547
- children: label
18548
- }
18549
- );
19008
+ const button = /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(CustomToolbarButton, { type: "button", "aria-label": label, disabled, active, onClick, className: "size-8", children });
19009
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Tooltip, { children: [
19010
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(TooltipTrigger, { asChild: true, children: disabled ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "inline-flex", children: button }) : button }),
19011
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(TooltipContent, { side: "bottom", sideOffset: 8, children: label })
19012
+ ] });
18550
19013
  }
18551
- function AltInput({ value, onCommit }) {
18552
- const [draft, setDraft] = (0, import_react10.useState)(value);
18553
- (0, import_react10.useEffect)(() => setDraft(value), [value]);
18554
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
18555
- "div",
18556
- {
18557
- style: {
18558
- display: "flex",
18559
- alignItems: "center",
18560
- gap: 6,
18561
- height: 28,
18562
- padding: "0 12px",
18563
- borderRadius: 999,
18564
- background: "#ffffff",
18565
- width: "100%",
18566
- maxWidth: 260,
18567
- boxSizing: "border-box"
18568
- },
18569
- children: [
18570
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18571
- "span",
18572
- {
18573
- style: {
18574
- flex: "0 0 auto",
18575
- color: OHW_BLUE_DARK,
18576
- fontSize: 10,
18577
- fontWeight: 700,
18578
- textTransform: "uppercase",
18579
- letterSpacing: 0.4
18580
- },
18581
- children: "Alt"
18582
- }
18583
- ),
18584
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18585
- "input",
19014
+ function ToolbarSeparator() {
19015
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { "aria-hidden": true, className: "mx-0.5 block h-6 w-px shrink-0 bg-border" });
19016
+ }
19017
+ function OptionMenu({
19018
+ label,
19019
+ value,
19020
+ options,
19021
+ onChange
19022
+ }) {
19023
+ const current = options.find((o) => o.value === value) ?? options[0];
19024
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-1.5 pl-1.5", children: [
19025
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "text-xs text-muted-foreground", children: label }),
19026
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(DropdownMenu, { modal: false, children: [
19027
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
19028
+ "button",
19029
+ {
19030
+ type: "button",
19031
+ "aria-label": label,
19032
+ className: "inline-flex h-8 min-w-[76px] items-center gap-[7px] rounded-[6px] bg-transparent px-2 text-[13px] font-medium text-foreground transition-colors hover:bg-muted data-[state=open]:bg-muted",
19033
+ children: [
19034
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "flex-1 text-left", children: current.label }),
19035
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.ChevronDown, { size: 16, style: { pointerEvents: "none" } })
19036
+ ]
19037
+ }
19038
+ ) }),
19039
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(DropdownMenuContent, { align: "end", sideOffset: 8, className: "w-[132px] min-w-[132px] p-1", children: options.map((option) => {
19040
+ const selected = option.value === value;
19041
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
19042
+ DropdownMenuItem,
18586
19043
  {
18587
- type: "text",
18588
- value: draft,
18589
- onChange: (e) => setDraft(e.target.value),
18590
- onBlur: () => {
18591
- if (draft !== value) onCommit(draft);
18592
- },
18593
- onKeyDown: (e) => {
18594
- if (e.key === "Enter") e.currentTarget.blur();
18595
- if (e.key === "Escape") {
18596
- setDraft(value);
18597
- e.currentTarget.blur();
18598
- }
18599
- },
18600
- placeholder: "Describe this image",
18601
- "aria-label": "Image alt text",
18602
- style: {
18603
- flex: 1,
18604
- minWidth: 0,
18605
- border: "none",
18606
- outline: "none",
18607
- background: "transparent",
18608
- fontSize: 12,
18609
- lineHeight: "16px",
18610
- fontFamily: "Figtree, ui-sans-serif, system-ui, sans-serif",
18611
- color: "#1f2937"
18612
- }
18613
- }
18614
- )
18615
- ]
18616
- }
18617
- );
19044
+ role: "menuitemradio",
19045
+ "aria-checked": selected,
19046
+ onSelect: () => onChange(option.value),
19047
+ className: cn("grid grid-cols-[1fr_16px] gap-2 py-2 pl-2 pr-2 text-[13px]", selected && "bg-muted"),
19048
+ children: [
19049
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: option.label }),
19050
+ selected && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Check, { size: 16, className: "text-bridge-primary" })
19051
+ ]
19052
+ },
19053
+ option.value
19054
+ );
19055
+ }) })
19056
+ ] })
19057
+ ] });
18618
19058
  }
18619
19059
  var VARIANTS = [
18620
19060
  { value: "logos", label: "Logos" },
18621
19061
  { value: "reel", label: "Reel" }
18622
19062
  ];
18623
19063
  var ALIGNS = [
18624
- { value: "left", label: "Left" },
18625
- { value: "center", label: "Center" },
18626
- { value: "right", label: "Right" }
19064
+ { value: "left", label: "Align left", Icon: import_lucide_react9.AlignLeft },
19065
+ { value: "center", label: "Align center", Icon: import_lucide_react9.AlignCenter },
19066
+ { value: "right", label: "Align right", Icon: import_lucide_react9.AlignRight }
18627
19067
  ];
18628
19068
  var HOVERS = [
18629
19069
  { value: "none", label: "None" },
@@ -18631,104 +19071,104 @@ var HOVERS = [
18631
19071
  { value: "zoom", label: "Zoom" },
18632
19072
  { value: "fade", label: "Fade" }
18633
19073
  ];
18634
- var TREATMENTS = [
18635
- { value: "color", label: "Color" },
18636
- { value: "grayscale", label: "Grayscale" }
18637
- ];
18638
19074
  var HTTP_URL_RE = /^https?:\/\/\S+$/i;
18639
- function LinkInput({ value, onCommit }) {
18640
- const [draft, setDraft] = (0, import_react10.useState)(value);
19075
+ function LinkModal({
19076
+ href,
19077
+ linkTarget,
19078
+ onClose,
19079
+ onSave
19080
+ }) {
19081
+ const [draft, setDraft] = (0, import_react10.useState)(href);
19082
+ const [newTab, setNewTab] = (0, import_react10.useState)(linkTarget !== "same-tab");
18641
19083
  const [invalid, setInvalid] = (0, import_react10.useState)(false);
18642
- (0, import_react10.useEffect)(() => {
18643
- setDraft(value);
18644
- setInvalid(false);
18645
- }, [value]);
18646
- const commit = () => {
19084
+ const save = () => {
18647
19085
  const next = draft.trim();
18648
- if (next === value) return setInvalid(false);
18649
- if (next && !HTTP_URL_RE.test(next)) return setInvalid(true);
18650
- setInvalid(false);
18651
- onCommit(next);
18652
- };
18653
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
18654
- "div",
18655
- {
18656
- style: {
18657
- display: "flex",
18658
- alignItems: "center",
18659
- gap: 6,
18660
- height: 28,
18661
- padding: "0 12px",
18662
- borderRadius: 999,
18663
- background: "#ffffff",
18664
- width: "100%",
18665
- maxWidth: 260,
18666
- boxSizing: "border-box",
18667
- boxShadow: invalid ? "inset 0 0 0 1.5px #EF4444" : "none"
18668
- },
18669
- children: [
18670
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18671
- "span",
18672
- {
18673
- style: {
18674
- flex: "0 0 auto",
18675
- color: OHW_BLUE_DARK,
18676
- fontSize: 10,
18677
- fontWeight: 700,
18678
- textTransform: "uppercase",
18679
- letterSpacing: 0.4
18680
- },
18681
- children: "Link"
18682
- }
18683
- ),
18684
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18685
- "input",
18686
- {
18687
- type: "text",
18688
- inputMode: "url",
18689
- value: draft,
18690
- onChange: (e) => setDraft(e.target.value),
18691
- onBlur: commit,
18692
- onKeyDown: (e) => {
18693
- if (e.key === "Enter") e.currentTarget.blur();
18694
- if (e.key === "Escape") {
18695
- setDraft(value);
18696
- setInvalid(false);
18697
- e.currentTarget.blur();
18698
- }
18699
- },
18700
- placeholder: "https://example.com",
18701
- "aria-label": "Image link URL",
18702
- "aria-invalid": invalid,
18703
- style: {
18704
- flex: 1,
18705
- minWidth: 0,
18706
- border: "none",
18707
- outline: "none",
18708
- background: "transparent",
18709
- fontSize: 12,
18710
- lineHeight: "16px",
18711
- fontFamily: "Figtree, ui-sans-serif, system-ui, sans-serif",
18712
- color: "#1f2937"
18713
- }
18714
- }
18715
- )
18716
- ]
19086
+ if (next && !HTTP_URL_RE.test(next)) {
19087
+ setInvalid(true);
19088
+ return;
18717
19089
  }
18718
- );
19090
+ onSave(next, newTab ? "new-tab" : "same-tab");
19091
+ onClose();
19092
+ };
19093
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Dialog2, { open: true, onOpenChange: (open) => !open && onClose(), children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(DialogContent, { "data-ohw-link-modal-root": "", className: "max-w-[400px] gap-4 p-5", children: [
19094
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(DialogHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(DialogTitle, { className: "text-base", children: "Image link" }) }),
19095
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex flex-col gap-3", children: [
19096
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19097
+ "input",
19098
+ {
19099
+ autoFocus: true,
19100
+ type: "text",
19101
+ inputMode: "url",
19102
+ value: draft,
19103
+ onChange: (e) => {
19104
+ setDraft(e.target.value);
19105
+ setInvalid(false);
19106
+ },
19107
+ onKeyDown: (e) => {
19108
+ if (e.key === "Enter") save();
19109
+ },
19110
+ placeholder: "https://example.com",
19111
+ "aria-label": "Image link URL",
19112
+ "aria-invalid": invalid,
19113
+ className: cn(
19114
+ "h-9 w-full rounded-[6px] border border-border bg-background px-3 text-[13px] text-foreground outline-none placeholder:text-muted-foreground/70 focus:border-bridge-primary",
19115
+ invalid && "border-destructive focus:border-destructive"
19116
+ )
19117
+ }
19118
+ ),
19119
+ invalid && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { className: "text-xs text-destructive", children: "Enter a full URL starting with http:// or https://" }),
19120
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("label", { className: "flex items-center gap-2 text-[13px] text-foreground", children: [
19121
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("input", { type: "checkbox", checked: newTab, onChange: (e) => setNewTab(e.target.checked) }),
19122
+ "Open in new tab"
19123
+ ] })
19124
+ ] }),
19125
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(DialogFooter, { children: [
19126
+ href && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19127
+ "button",
19128
+ {
19129
+ type: "button",
19130
+ onClick: () => {
19131
+ onSave("", "new-tab");
19132
+ onClose();
19133
+ },
19134
+ className: "mr-auto rounded-[6px] px-3 py-2 text-[13px] font-medium text-destructive transition-colors hover:bg-muted",
19135
+ children: "Remove link"
19136
+ }
19137
+ ),
19138
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19139
+ "button",
19140
+ {
19141
+ type: "button",
19142
+ onClick: onClose,
19143
+ className: "rounded-[6px] px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-muted",
19144
+ children: "Cancel"
19145
+ }
19146
+ ),
19147
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19148
+ "button",
19149
+ {
19150
+ type: "button",
19151
+ onClick: save,
19152
+ className: "rounded-[6px] bg-bridge-primary px-3 py-2 text-[13px] font-medium text-primary-foreground transition-opacity hover:opacity-90",
19153
+ children: "Save"
19154
+ }
19155
+ )
19156
+ ] })
19157
+ ] }) });
18719
19158
  }
18720
19159
  function MediaStripToolbar({
18721
19160
  entry,
18722
19161
  index,
18723
19162
  pending,
18724
19163
  error,
19164
+ itemEditOn,
19165
+ onToggleItemEdit,
18725
19166
  onSetVariant,
18726
19167
  onSetAlign,
18727
19168
  onSetHover,
18728
19169
  onSetTreatment,
18729
19170
  onResize,
18730
- onSetAlt,
18731
- onSetLink,
19171
+ onOpenLink,
18732
19172
  onAddItem,
18733
19173
  onRemoveItem,
18734
19174
  onChangeImage,
@@ -18737,81 +19177,102 @@ function MediaStripToolbar({
18737
19177
  }) {
18738
19178
  const items = mediaStripItems(entry.tree);
18739
19179
  const itemCount = items.length;
18740
- const currentAlt = items[index]?.alt ?? "";
18741
- const currentHref = items[index]?.href ?? "";
18742
- const currentTarget = items[index]?.linkTarget === "same-tab" ? "same-tab" : "new-tab";
18743
19180
  const slots = findMediaStripSlots(entry.tree);
18744
19181
  const variant = slots?.variant === "reel" ? "reel" : "logos";
18745
19182
  const align = slots?.align === "left" || slots?.align === "right" ? slots.align : "center";
18746
19183
  const hover = slots?.hover === "none" || slots?.hover === "lift" || slots?.hover === "zoom" || slots?.hover === "fade" ? slots.hover : "lift";
18747
19184
  const treatment = slots?.treatment === "color" || slots?.treatment === "grayscale" ? slots.treatment : variant === "logos" ? "grayscale" : "color";
18748
19185
  const sizeValue = variant === "logos" ? typeof slots?.logoMaxHeight === "number" ? slots.logoMaxHeight : 48 : typeof slots?.cardWidth === "number" ? slots.cardWidth : 240;
18749
- const isMobile = window.innerWidth <= 480;
18750
- const rowStyle = isMobile ? {
18751
- display: "flex",
18752
- gap: 6,
18753
- flexWrap: "nowrap",
18754
- justifyContent: "center",
18755
- overflowX: "auto",
18756
- WebkitOverflowScrolling: "touch",
18757
- width: "100%",
18758
- scrollbarWidth: "none"
18759
- } : { display: "flex", gap: 6, flexWrap: "nowrap", justifyContent: "center" };
18760
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
19186
+ const mobile = window.innerWidth <= 480;
19187
+ const gatedLabel = (label) => itemEditOn ? label : "Turn on the star to edit this image";
19188
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(TooltipProvider, { delayDuration: 0, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
18761
19189
  "div",
18762
19190
  {
18763
19191
  "data-ohw-bridge": "",
18764
19192
  "data-ohw-toolbar": "",
18765
19193
  onMouseDown: (e) => e.stopPropagation(),
18766
- style: {
18767
- display: "flex",
18768
- flexDirection: "column",
18769
- gap: 6,
18770
- alignItems: "center",
18771
- background: OHW_BLUE,
18772
- padding: 8,
18773
- borderRadius: 12,
18774
- boxShadow: "0 8px 24px rgba(0,0,0,0.25)",
18775
- pointerEvents: pending ? "none" : "auto",
18776
- opacity: pending ? 0.6 : 1,
18777
- maxWidth: isMobile ? "calc(100vw - 24px)" : "none"
18778
- },
19194
+ className: cn("flex flex-col items-center gap-1.5 font-sans", pending && "pointer-events-none opacity-60"),
19195
+ style: { maxWidth: mobile ? "calc(100vw - 24px)" : void 0 },
18779
19196
  children: [
18780
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { style: rowStyle, children: [
18781
- VARIANTS.map((v) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Pill, { label: v.label, active: variant === v.value, onClick: () => onSetVariant(v.value) }, v.value)),
18782
- ALIGNS.map((a) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Pill, { label: a.label, active: align === a.value, onClick: () => onSetAlign(a.value) }, a.value))
18783
- ] }),
18784
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { style: rowStyle, children: [
18785
- HOVERS.map((h) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Pill, { label: h.label, active: hover === h.value, onClick: () => onSetHover(h.value) }, h.value)),
18786
- TREATMENTS.map((t) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Pill, { label: t.label, active: treatment === t.value, onClick: () => onSetTreatment(t.value) }, t.value))
18787
- ] }),
18788
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [
18789
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Pill, { label: "\u2212", onClick: () => onResize(-8) }),
18790
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("span", { style: { color: "#ffffff", fontSize: 12, minWidth: 34, textAlign: "center" }, children: [
18791
- sizeValue,
18792
- "px"
18793
- ] }),
18794
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Pill, { label: "+", onClick: () => onResize(8) })
18795
- ] }),
18796
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { style: { display: "flex", justifyContent: "center", width: "100%" }, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(AltInput, { value: currentAlt, onCommit: onSetAlt }) }),
18797
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { style: { display: "flex", justifyContent: "center", width: "100%" }, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(LinkInput, { value: currentHref, onCommit: (href) => onSetLink(href, currentTarget) }) }),
18798
- currentHref && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { style: rowStyle, children: [
18799
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Pill, { label: "New tab", active: currentTarget === "new-tab", onClick: () => onSetLink(currentHref, "new-tab") }),
18800
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Pill, { label: "Same tab", active: currentTarget === "same-tab", onClick: () => onSetLink(currentHref, "same-tab") })
18801
- ] }),
18802
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { style: rowStyle, children: [
18803
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Pill, { label: "Change Image", onClick: onChangeImage }),
18804
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Pill, { label: "+ Item", onClick: onAddItem }),
18805
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Pill, { label: "\u2212 Item", onClick: onRemoveItem, disabled: itemCount <= 1 })
18806
- ] }),
18807
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { style: rowStyle, children: [
18808
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Pill, { label: "\u2190 Move", onClick: onMoveLeft, disabled: index <= 0 }),
18809
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Pill, { label: "\u2192 Move", onClick: onMoveRight, disabled: index >= itemCount - 1 })
18810
- ] }),
18811
- error && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { style: { color: "#FCA5A5", fontSize: 11, maxWidth: 220, textAlign: "center" }, children: error })
19197
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
19198
+ "div",
19199
+ {
19200
+ className: cn(
19201
+ "flex items-center gap-1 rounded-[10px] border border-border bg-background p-1 whitespace-nowrap",
19202
+ "shadow-[0px_10px_15px_-3px_rgba(0,0,0,0.1),0px_4px_6px_-4px_rgba(0,0,0,0.1)]",
19203
+ mobile ? "flex-wrap justify-center" : "flex-nowrap"
19204
+ ),
19205
+ children: [
19206
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Tooltip, { children: [
19207
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19208
+ CustomToolbarButton,
19209
+ {
19210
+ type: "button",
19211
+ "aria-label": itemEditOn ? "Done editing this image" : "Edit this image",
19212
+ "aria-pressed": itemEditOn,
19213
+ onClick: onToggleItemEdit,
19214
+ className: "group size-8",
19215
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19216
+ import_lucide_react9.Star,
19217
+ {
19218
+ size: 16,
19219
+ fill: "currentColor",
19220
+ strokeWidth: 0,
19221
+ className: cn(
19222
+ "transition-colors",
19223
+ itemEditOn ? "text-[#f59e0b] group-hover:text-[#d97706]" : "text-[#d6d3d1] group-hover:text-[#fbbf24]"
19224
+ ),
19225
+ style: { pointerEvents: "none" }
19226
+ }
19227
+ )
19228
+ }
19229
+ ) }),
19230
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(TooltipContent, { side: "bottom", sideOffset: 8, children: itemEditOn ? "Done editing this image" : "Edit this image" })
19231
+ ] }),
19232
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarSeparator, {}),
19233
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarAction, { label: gatedLabel("Change image"), onClick: onChangeImage, disabled: !itemEditOn, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Image, { size: 16, style: { pointerEvents: "none" } }) }),
19234
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarAction, { label: gatedLabel("Link"), onClick: onOpenLink, disabled: !itemEditOn, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Link, { size: 16, style: { pointerEvents: "none" } }) }),
19235
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarAction, { label: gatedLabel("Move left"), onClick: onMoveLeft, disabled: !itemEditOn || index <= 0, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.ArrowLeft, { size: 16, style: { pointerEvents: "none" } }) }),
19236
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19237
+ ToolbarAction,
19238
+ {
19239
+ label: gatedLabel("Move right"),
19240
+ onClick: onMoveRight,
19241
+ disabled: !itemEditOn || index >= itemCount - 1,
19242
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.ArrowRight, { size: 16, style: { pointerEvents: "none" } })
19243
+ }
19244
+ ),
19245
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarAction, { label: gatedLabel("Remove image"), onClick: onRemoveItem, disabled: !itemEditOn || itemCount <= 1, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.ImageMinus, { size: 16, style: { pointerEvents: "none" } }) }),
19246
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarSeparator, {}),
19247
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(OptionMenu, { label: "Layout", value: variant, options: VARIANTS, onChange: onSetVariant }),
19248
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(OptionMenu, { label: "Hover", value: hover, options: HOVERS, onChange: onSetHover }),
19249
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19250
+ ToolbarAction,
19251
+ {
19252
+ label: treatment === "grayscale" ? "Show in color" : "Show grayscale",
19253
+ active: treatment === "grayscale",
19254
+ onClick: () => onSetTreatment(treatment === "grayscale" ? "color" : "grayscale"),
19255
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Palette, { size: 16, style: { pointerEvents: "none" } })
19256
+ }
19257
+ ),
19258
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarSeparator, {}),
19259
+ ALIGNS.map(({ value, label, Icon }) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarAction, { label, active: align === value, onClick: () => onSetAlign(value), children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Icon, { size: 16, style: { pointerEvents: "none" } }) }, value)),
19260
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarSeparator, {}),
19261
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarAction, { label: "Smaller", onClick: () => onResize(-8), children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Minus, { size: 16, style: { pointerEvents: "none" } }) }),
19262
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("span", { className: "min-w-[40px] text-center text-xs tabular-nums text-muted-foreground", children: [
19263
+ sizeValue,
19264
+ "px"
19265
+ ] }),
19266
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarAction, { label: "Larger", onClick: () => onResize(8), children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Plus, { size: 16, style: { pointerEvents: "none" } }) }),
19267
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarSeparator, {}),
19268
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarAction, { label: "Add image", onClick: onAddItem, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.ImagePlus, { size: 16, style: { pointerEvents: "none" } }) })
19269
+ ]
19270
+ }
19271
+ ),
19272
+ error && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "max-w-[260px] rounded-md bg-background px-2 py-1 text-center text-[11px] text-destructive shadow-sm", children: error })
18812
19273
  ]
18813
19274
  }
18814
- );
19275
+ ) });
18815
19276
  }
18816
19277
  function findMediaStripSlots(tree) {
18817
19278
  const stack = tree.rows.flatMap((row) => row.blocks);
@@ -18830,31 +19291,23 @@ function AiMediaStripOverlay({
18830
19291
  }) {
18831
19292
  const [hover, setHover] = (0, import_react10.useState)(null);
18832
19293
  const [active, setActive] = (0, import_react10.useState)(null);
19294
+ const [itemEditOn, setItemEditOn] = (0, import_react10.useState)(false);
19295
+ const [linkOpen, setLinkOpen] = (0, import_react10.useState)(false);
18833
19296
  const [pending, setPending] = (0, import_react10.useState)(false);
18834
19297
  const [error, setError] = (0, import_react10.useState)(null);
18835
19298
  const activeRef = (0, import_react10.useRef)(null);
18836
19299
  activeRef.current = active;
19300
+ const linkOpenRef = (0, import_react10.useRef)(false);
19301
+ linkOpenRef.current = linkOpen;
18837
19302
  (0, import_react10.useEffect)(() => {
18838
19303
  const onMove = (e) => {
19304
+ const ref = itemRefFromTarget(e.target);
18839
19305
  const t = e.target;
18840
- if (!(t instanceof HTMLElement)) return;
18841
- if (t.closest("[data-ohw-toolbar], [data-ohw-edit-chrome], [data-ohw-item-interaction], [data-ohw-link-modal-root], [data-ohw-section-picker], [data-ohw-ai-review]")) {
18842
- return;
18843
- }
18844
- const itemEl = t.closest("[data-ohw-media-strip-item]");
18845
- if (!itemEl) {
18846
- setHover(null);
18847
- return;
18848
- }
18849
- const sectionEl = itemEl.closest("[data-ohw-section]");
18850
- const unit = sectionEl ? movableUnit(sectionEl) : null;
18851
- const sectionId = unit ? instanceIdOf(unit) || null : null;
18852
- const index = Number(itemEl.dataset.ohwMediaStripIndex);
18853
- if (!sectionId || Number.isNaN(index)) {
18854
- setHover(null);
18855
- return;
18856
- }
18857
- setHover({ sectionId, index });
19306
+ const overEditable = Boolean(t?.closest?.("[data-ohw-editable]"));
19307
+ setHover((prev) => {
19308
+ const next = ref && !overEditable ? ref : null;
19309
+ return sameRef(prev, next) ? prev : next;
19310
+ });
18858
19311
  };
18859
19312
  const onLeave = () => setHover(null);
18860
19313
  document.addEventListener("mousemove", onMove, { passive: true });
@@ -18866,23 +19319,25 @@ function AiMediaStripOverlay({
18866
19319
  }, []);
18867
19320
  (0, import_react10.useEffect)(() => {
18868
19321
  const onClick = (e) => {
18869
- const current = activeRef.current;
18870
- if (!current) return;
18871
19322
  const t = e.target;
18872
- if (!(t instanceof HTMLElement)) return;
18873
- if (t.closest("[data-ohw-item-interaction]")) return;
18874
- const itemEl = findMediaStripItemElement(current.sectionId, current.index);
18875
- if (itemEl && itemEl.contains(t)) return;
18876
- setActive(null);
19323
+ if (!(t instanceof Element)) return;
19324
+ if (t.closest(CHROME_SELECTOR)) return;
19325
+ const ref = itemRefFromTarget(t);
19326
+ if (sameRef(activeRef.current, ref)) return;
19327
+ if (ref) clearInteractionChromeRef.current();
19328
+ setItemEditOn(false);
18877
19329
  setError(null);
19330
+ setActive(ref);
18878
19331
  };
18879
19332
  document.addEventListener("click", onClick, true);
18880
19333
  return () => document.removeEventListener("click", onClick, true);
18881
- }, []);
19334
+ }, [clearInteractionChromeRef]);
18882
19335
  (0, import_react10.useEffect)(() => {
18883
19336
  const onBlur = () => {
18884
19337
  if (!activeRef.current) return;
18885
19338
  setActive(null);
19339
+ setItemEditOn(false);
19340
+ setLinkOpen(false);
18886
19341
  setError(null);
18887
19342
  };
18888
19343
  window.addEventListener("blur", onBlur);
@@ -18892,7 +19347,10 @@ function AiMediaStripOverlay({
18892
19347
  const onKeyDown = (e) => {
18893
19348
  if (e.key !== "Escape") return;
18894
19349
  if (!activeRef.current) return;
19350
+ if (linkOpenRef.current) return;
19351
+ if (document.querySelector('[data-ohw-more-menu][data-state="open"]')) return;
18895
19352
  setActive(null);
19353
+ setItemEditOn(false);
18896
19354
  setError(null);
18897
19355
  };
18898
19356
  document.addEventListener("keydown", onKeyDown);
@@ -18926,137 +19384,133 @@ function AiMediaStripOverlay({
18926
19384
  tree: nextTree,
18927
19385
  label: entry.label,
18928
19386
  media: extraMedia ? { ...entry.media, ...extraMedia } : entry.media,
18929
- ...entry.brand ? { brand: entry.brand } : {}
19387
+ ...entry.brand ? { brand: entry.brand } : {},
19388
+ // The user is already looking at the strip they just edited — recentering the viewport
19389
+ // on every edit reads as the section jumping around.
19390
+ skipScrollIntoView: true
18930
19391
  });
18931
19392
  return true;
18932
19393
  },
18933
19394
  [findEntry, postToParent2, applyTreeLocalRef]
18934
19395
  );
18935
19396
  const activeEntry = active ? findEntry(active.sectionId) : null;
18936
- const hoverHandleRect = useLiveItemRect(!active && hover ? hover : null);
18937
19397
  const activeItemRect = useLiveItemRect(active);
18938
- const TOOLBAR_APPROX_H = 170;
18939
- const toolbarState = activeItemRect && activeItemRect.top < TOOLBAR_APPROX_H ? "active-bottom" : "active-top";
18940
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
18941
- hoverHandleRect && hover && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18942
- "button",
18943
- {
18944
- type: "button",
18945
- "data-ohw-toolbar": "",
18946
- onClick: () => {
18947
- clearInteractionChromeRef.current();
18948
- setActive(hover);
18949
- },
18950
- "aria-label": "Configure media item",
18951
- style: {
18952
- position: "fixed",
18953
- top: hoverHandleRect.top + 8,
18954
- left: hoverHandleRect.left + hoverHandleRect.width - 36,
18955
- width: 28,
18956
- height: 28,
18957
- borderRadius: 8,
18958
- border: `1.5px dashed ${PRIMARY3}`,
18959
- background: "#ffffff",
18960
- color: PRIMARY3,
18961
- display: "flex",
18962
- alignItems: "center",
18963
- justifyContent: "center",
18964
- cursor: "pointer",
18965
- zIndex: 2147483643,
18966
- fontSize: 14,
18967
- fontWeight: 700
18968
- },
18969
- children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react8.Star, { size: 14, style: { pointerEvents: "none" } })
18970
- }
18971
- ),
18972
- activeItemRect && active && activeEntry && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18973
- ItemInteractionLayer,
18974
- {
18975
- rect: activeItemRect,
18976
- state: toolbarState,
18977
- toolbar: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18978
- MediaStripToolbar,
18979
- {
18980
- entry: activeEntry,
18981
- index: active.index,
18982
- pending,
18983
- error,
18984
- onSetVariant: (variant) => runMutation(active.sectionId, (tree) => setMediaStripVariant(tree, variant)),
18985
- onSetAlign: (align) => runMutation(active.sectionId, (tree) => setMediaStripAlign(tree, align)),
18986
- onSetHover: (hoverPreset) => runMutation(active.sectionId, (tree) => setMediaStripHover(tree, hoverPreset)),
18987
- onSetTreatment: (treatment) => runMutation(active.sectionId, (tree) => setMediaStripTreatment(tree, treatment)),
18988
- onResize: (delta) => {
18989
- const slots = findMediaStripSlots(activeEntry.tree);
18990
- const variant = slots?.variant === "reel" ? "reel" : "logos";
18991
- const current = variant === "logos" ? typeof slots?.logoMaxHeight === "number" ? slots.logoMaxHeight : 48 : typeof slots?.cardWidth === "number" ? slots.cardWidth : 240;
18992
- void runMutation(active.sectionId, (tree) => setMediaStripSizing(tree, current + delta));
18993
- },
18994
- onSetAlt: (alt) => {
18995
- void runMutation(active.sectionId, (tree) => setMediaStripItemAlt(tree, active.index, alt));
18996
- },
18997
- onSetLink: (href, linkTarget) => {
19398
+ const activeStripRect = useLiveItemRect(active, true);
19399
+ const hoverRect = useLiveItemRect(hover && !sameRef(hover, active) ? hover : null);
19400
+ const activeItems = activeEntry ? mediaStripItems(activeEntry.tree) : [];
19401
+ const activeItem = active ? activeItems[active.index] : void 0;
19402
+ const activeHref = activeItem?.href ?? "";
19403
+ const activeTarget = activeItem?.linkTarget === "same-tab" ? "same-tab" : "new-tab";
19404
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
19405
+ hoverRect && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ItemInteractionLayer, { rect: hoverRect, state: "hover" }),
19406
+ activeItemRect && activeStripRect && active && activeEntry && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
19407
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ItemInteractionLayer, { rect: activeItemRect, state: "active-top" }),
19408
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(StripToolbarSlot, { stripRect: activeStripRect, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19409
+ MediaStripToolbar,
19410
+ {
19411
+ entry: activeEntry,
19412
+ index: active.index,
19413
+ pending,
19414
+ error,
19415
+ itemEditOn,
19416
+ onToggleItemEdit: () => setItemEditOn((on) => !on),
19417
+ onSetVariant: (variant) => runMutation(active.sectionId, (tree) => setMediaStripVariant(tree, variant)),
19418
+ onSetAlign: (align) => runMutation(active.sectionId, (tree) => setMediaStripAlign(tree, align)),
19419
+ onSetHover: (hoverPreset) => runMutation(active.sectionId, (tree) => setMediaStripHover(tree, hoverPreset)),
19420
+ onSetTreatment: (treatment) => runMutation(active.sectionId, (tree) => setMediaStripTreatment(tree, treatment)),
19421
+ onResize: (delta) => {
19422
+ const slots = findMediaStripSlots(activeEntry.tree);
19423
+ const variant = slots?.variant === "reel" ? "reel" : "logos";
19424
+ const current = variant === "logos" ? typeof slots?.logoMaxHeight === "number" ? slots.logoMaxHeight : 48 : typeof slots?.cardWidth === "number" ? slots.cardWidth : 240;
19425
+ void runMutation(active.sectionId, (tree) => setMediaStripSizing(tree, current + delta));
19426
+ },
19427
+ onOpenLink: () => setLinkOpen(true),
19428
+ onAddItem: () => {
19429
+ void requestAssetPick(postToParent2).then((picked) => {
19430
+ if (!picked) return;
19431
+ const item = { assetId: picked.assetId, alt: "Logo" };
18998
19432
  void runMutation(
18999
19433
  active.sectionId,
19000
- (tree) => setMediaStripItemLink(tree, active.index, href ? { href, linkTarget } : {})
19001
- );
19002
- },
19003
- onAddItem: () => {
19004
- void requestAssetPick(postToParent2).then((picked) => {
19005
- if (!picked) return;
19006
- const item = { assetId: picked.assetId, alt: "Logo" };
19007
- void runMutation(
19008
- active.sectionId,
19009
- (tree) => addMediaStripItem(tree, item, active.index),
19010
- { [picked.assetId]: picked.url }
19011
- ).then((applied) => {
19012
- if (applied) setActive({ sectionId: active.sectionId, index: active.index + 1 });
19013
- });
19014
- });
19015
- },
19016
- onChangeImage: () => {
19017
- void requestAssetPick(postToParent2).then((picked) => {
19018
- if (!picked) return;
19019
- void runMutation(
19020
- active.sectionId,
19021
- (tree) => setMediaStripItemAsset(tree, active.index, picked.assetId),
19022
- { [picked.assetId]: picked.url }
19023
- );
19434
+ (tree) => addMediaStripItem(tree, item, active.index),
19435
+ { [picked.assetId]: picked.url }
19436
+ ).then((applied) => {
19437
+ if (applied) setActive({ sectionId: active.sectionId, index: active.index + 1 });
19024
19438
  });
19025
- },
19026
- onRemoveItem: () => {
19027
- void runMutation(active.sectionId, (tree) => removeMediaStripItem(tree, active.index)).then(
19028
- (applied) => {
19029
- if (applied) setActive(null);
19030
- }
19031
- );
19032
- },
19033
- onMoveLeft: () => {
19034
- void runMutation(active.sectionId, (tree) => reorderMediaStripItem(tree, active.index, -1)).then(
19035
- (applied) => {
19036
- if (applied) setActive({ sectionId: active.sectionId, index: active.index - 1 });
19037
- }
19038
- );
19039
- },
19040
- onMoveRight: () => {
19041
- void runMutation(active.sectionId, (tree) => reorderMediaStripItem(tree, active.index, 1)).then(
19042
- (applied) => {
19043
- if (applied) setActive({ sectionId: active.sectionId, index: active.index + 1 });
19044
- }
19439
+ });
19440
+ },
19441
+ onChangeImage: () => {
19442
+ void requestAssetPick(postToParent2).then((picked) => {
19443
+ if (!picked) return;
19444
+ void runMutation(
19445
+ active.sectionId,
19446
+ (tree) => setMediaStripItemAsset(tree, active.index, picked.assetId),
19447
+ { [picked.assetId]: picked.url }
19045
19448
  );
19046
- }
19449
+ });
19450
+ },
19451
+ onRemoveItem: () => {
19452
+ void runMutation(active.sectionId, (tree) => removeMediaStripItem(tree, active.index)).then(
19453
+ (applied) => {
19454
+ if (applied) setActive(null);
19455
+ }
19456
+ );
19457
+ },
19458
+ onMoveLeft: () => {
19459
+ void runMutation(active.sectionId, (tree) => reorderMediaStripItem(tree, active.index, -1)).then(
19460
+ (applied) => {
19461
+ if (applied) setActive({ sectionId: active.sectionId, index: active.index - 1 });
19462
+ }
19463
+ );
19464
+ },
19465
+ onMoveRight: () => {
19466
+ void runMutation(active.sectionId, (tree) => reorderMediaStripItem(tree, active.index, 1)).then(
19467
+ (applied) => {
19468
+ if (applied) setActive({ sectionId: active.sectionId, index: active.index + 1 });
19469
+ }
19470
+ );
19047
19471
  }
19048
- )
19049
- }
19050
- )
19472
+ }
19473
+ ) }),
19474
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19475
+ "div",
19476
+ {
19477
+ "data-ohw-bridge": "",
19478
+ "data-ohw-item-interaction": "",
19479
+ "aria-hidden": true,
19480
+ className: "pointer-events-none fixed rounded-[4px] px-[7px] py-[3px] font-sans text-[11px] font-medium leading-none text-primary-foreground whitespace-nowrap",
19481
+ style: {
19482
+ top: activeItemRect.top + activeItemRect.height + 10,
19483
+ left: activeItemRect.left - 5,
19484
+ background: PRIMARY3,
19485
+ zIndex: 2147483644
19486
+ },
19487
+ children: "Media item"
19488
+ }
19489
+ ),
19490
+ linkOpen && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19491
+ LinkModal,
19492
+ {
19493
+ href: activeHref,
19494
+ linkTarget: activeTarget,
19495
+ onClose: () => setLinkOpen(false),
19496
+ onSave: (href, linkTarget) => {
19497
+ void runMutation(
19498
+ active.sectionId,
19499
+ (tree) => setMediaStripItemLink(tree, active.index, href ? { href, linkTarget } : {})
19500
+ );
19501
+ }
19502
+ }
19503
+ )
19504
+ ] })
19051
19505
  ] });
19052
19506
  }
19053
19507
 
19054
19508
  // src/ui/ai-section/AiPricingCardOverlay.tsx
19055
19509
  var import_react11 = require("react");
19056
- var import_lucide_react9 = require("lucide-react");
19057
- var import_jsx_runtime21 = require("react/jsx-runtime");
19510
+ var import_lucide_react10 = require("lucide-react");
19511
+ var import_jsx_runtime22 = require("react/jsx-runtime");
19058
19512
  var PRIMARY4 = "var(--ohw-primary, #0885FE)";
19059
- var CHROME_SELECTOR = "[data-ohw-toolbar], [data-ohw-edit-chrome], [data-ohw-item-interaction], [data-ohw-link-modal-root], [data-ohw-section-picker], [data-ohw-ai-review], [data-ohw-more-menu]";
19513
+ var CHROME_SELECTOR2 = "[data-ohw-toolbar], [data-ohw-edit-chrome], [data-ohw-item-interaction], [data-ohw-link-modal-root], [data-ohw-section-picker], [data-ohw-ai-review], [data-ohw-more-menu]";
19060
19514
  function findPricingCardElement(sectionId, index) {
19061
19515
  const sectionEl = findByInstanceId(sectionId);
19062
19516
  if (!sectionEl) return null;
@@ -19068,7 +19522,7 @@ function findPricingCardElement(sectionId, index) {
19068
19522
  }
19069
19523
  function cardRefFromTarget(target) {
19070
19524
  if (!(target instanceof Element)) return null;
19071
- if (target.closest(CHROME_SELECTOR)) return null;
19525
+ if (target.closest(CHROME_SELECTOR2)) return null;
19072
19526
  const cardEl = target.closest("[data-ohw-pricing-card]");
19073
19527
  if (!cardEl) return null;
19074
19528
  const sectionEl = cardEl.closest("[data-ohw-section]");
@@ -19078,21 +19532,21 @@ function cardRefFromTarget(target) {
19078
19532
  if (!sectionId || Number.isNaN(index)) return null;
19079
19533
  return { sectionId, index };
19080
19534
  }
19081
- function sameRef(a, b) {
19535
+ function sameRef2(a, b) {
19082
19536
  return a?.sectionId === b?.sectionId && a?.index === b?.index;
19083
19537
  }
19084
- function rectOf(el) {
19538
+ function rectOf2(el) {
19085
19539
  if (!el) return null;
19086
19540
  const r2 = el.getBoundingClientRect();
19087
19541
  if (r2.width <= 0 || r2.height <= 0) return null;
19088
19542
  return { top: r2.top, left: r2.left, width: r2.width, height: r2.height };
19089
19543
  }
19090
19544
  function readRect3(ref) {
19091
- return rectOf(ref && findPricingCardElement(ref.sectionId, ref.index));
19545
+ return rectOf2(ref && findPricingCardElement(ref.sectionId, ref.index));
19092
19546
  }
19093
19547
  function readGridRect(ref) {
19094
19548
  const card = ref && findPricingCardElement(ref.sectionId, ref.index);
19095
- return rectOf(card?.closest("[data-ohw-pricing-collection]") ?? card);
19549
+ return rectOf2(card?.closest("[data-ohw-pricing-collection]") ?? card);
19096
19550
  }
19097
19551
  function useLiveCardRect(ref, grid = false) {
19098
19552
  const [rect, setRect] = (0, import_react11.useState)(null);
@@ -19122,7 +19576,7 @@ function useLiveCardRect(ref, grid = false) {
19122
19576
  }
19123
19577
  var TOOLBAR_GRID_GAP = 52;
19124
19578
  var TOOLBAR_GRID_GAP_MOBILE = 42;
19125
- var TOOLBAR_EDGE_MARGIN2 = 4;
19579
+ var TOOLBAR_EDGE_MARGIN3 = 4;
19126
19580
  function GridToolbarSlot({ gridRect, children }) {
19127
19581
  const slotRef = (0, import_react11.useRef)(null);
19128
19582
  const [shift, setShift] = (0, import_react11.useState)(0);
@@ -19134,7 +19588,7 @@ function GridToolbarSlot({ gridRect, children }) {
19134
19588
  const w = slot.offsetWidth;
19135
19589
  if (w <= 0) return;
19136
19590
  const half = w / 2;
19137
- const clamped = Math.max(TOOLBAR_EDGE_MARGIN2 + half, Math.min(centerX, window.innerWidth - TOOLBAR_EDGE_MARGIN2 - half));
19591
+ const clamped = Math.max(TOOLBAR_EDGE_MARGIN3 + half, Math.min(centerX, window.innerWidth - TOOLBAR_EDGE_MARGIN3 - half));
19138
19592
  setShift((prev) => Math.abs(prev - (clamped - centerX)) < 0.5 ? prev : clamped - centerX);
19139
19593
  };
19140
19594
  clamp();
@@ -19143,7 +19597,7 @@ function GridToolbarSlot({ gridRect, children }) {
19143
19597
  return () => ro.disconnect();
19144
19598
  }, [centerX, children]);
19145
19599
  const mobile = window.innerWidth <= 480;
19146
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19600
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
19147
19601
  "div",
19148
19602
  {
19149
19603
  ref: slotRef,
@@ -19178,7 +19632,7 @@ function requestValidateTree2(postToParent2, tree) {
19178
19632
  });
19179
19633
  }
19180
19634
  function StyleSwatch({ style }) {
19181
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19635
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
19182
19636
  "span",
19183
19637
  {
19184
19638
  "aria-hidden": true,
@@ -19192,35 +19646,35 @@ function StyleSwatch({ style }) {
19192
19646
  );
19193
19647
  }
19194
19648
  function ButtonGlyph() {
19195
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("svg", { width: "18", height: "12", viewBox: "0 0 18 12", fill: "none", "aria-hidden": true, style: { pointerEvents: "none" }, children: [
19196
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("rect", { x: "0.75", y: "0.75", width: "16.5", height: "10.5", rx: "3", stroke: "currentColor", strokeWidth: "1.5" }),
19197
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("path", { d: "M5.5 6H12.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })
19649
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("svg", { width: "18", height: "12", viewBox: "0 0 18 12", fill: "none", "aria-hidden": true, style: { pointerEvents: "none" }, children: [
19650
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("rect", { x: "0.75", y: "0.75", width: "16.5", height: "10.5", rx: "3", stroke: "currentColor", strokeWidth: "1.5" }),
19651
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("path", { d: "M5.5 6H12.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })
19198
19652
  ] });
19199
19653
  }
19200
19654
  function FeatureGlyph({ mode }) {
19201
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none", "aria-hidden": true, style: { pointerEvents: "none" }, children: [
19202
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("circle", { cx: "8", cy: "8", r: "7.25", stroke: "currentColor", strokeWidth: "1.5" }),
19203
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("path", { d: "M5 8.2L7.2 10.3L11 6", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }),
19204
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("circle", { cx: "13.5", cy: "13.5", r: "4.25", fill: "var(--ohw-background, #ffffff)", stroke: "var(--ohw-background, #ffffff)", strokeWidth: "1" }),
19205
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("circle", { cx: "13.5", cy: "13.5", r: "3.75", fill: "var(--ohw-background, #ffffff)", stroke: PRIMARY4, strokeWidth: "1" }),
19206
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("path", { d: "M11.5 13.5H15.5", stroke: PRIMARY4, strokeWidth: "1.5", strokeLinecap: "round" }),
19207
- mode === "add" && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("path", { d: "M13.5 11.5V15.5", stroke: PRIMARY4, strokeWidth: "1.5", strokeLinecap: "round" })
19655
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none", "aria-hidden": true, style: { pointerEvents: "none" }, children: [
19656
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("circle", { cx: "8", cy: "8", r: "7.25", stroke: "currentColor", strokeWidth: "1.5" }),
19657
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("path", { d: "M5 8.2L7.2 10.3L11 6", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }),
19658
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("circle", { cx: "13.5", cy: "13.5", r: "4.25", fill: "var(--ohw-background, #ffffff)", stroke: "var(--ohw-background, #ffffff)", strokeWidth: "1" }),
19659
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("circle", { cx: "13.5", cy: "13.5", r: "3.75", fill: "var(--ohw-background, #ffffff)", stroke: PRIMARY4, strokeWidth: "1" }),
19660
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("path", { d: "M11.5 13.5H15.5", stroke: PRIMARY4, strokeWidth: "1.5", strokeLinecap: "round" }),
19661
+ mode === "add" && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("path", { d: "M13.5 11.5V15.5", stroke: PRIMARY4, strokeWidth: "1.5", strokeLinecap: "round" })
19208
19662
  ] });
19209
19663
  }
19210
- function ToolbarAction({
19664
+ function ToolbarAction2({
19211
19665
  label,
19212
19666
  onClick,
19213
19667
  disabled = false,
19214
19668
  children
19215
19669
  }) {
19216
- const button = /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(CustomToolbarButton, { type: "button", "aria-label": label, disabled, onClick, className: "size-8", children });
19217
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Tooltip, { children: [
19218
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(TooltipTrigger, { asChild: true, children: disabled ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "inline-flex", children: button }) : button }),
19219
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(TooltipContent, { side: "bottom", sideOffset: 8, children: label })
19670
+ const button = /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(CustomToolbarButton, { type: "button", "aria-label": label, disabled, onClick, className: "size-8", children });
19671
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(Tooltip, { children: [
19672
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TooltipTrigger, { asChild: true, children: disabled ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "inline-flex", children: button }) : button }),
19673
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TooltipContent, { side: "bottom", sideOffset: 8, children: label })
19220
19674
  ] });
19221
19675
  }
19222
- function ToolbarSeparator() {
19223
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { "aria-hidden": true, className: "mx-0.5 block h-6 w-px shrink-0 bg-border" });
19676
+ function ToolbarSeparator2() {
19677
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { "aria-hidden": true, className: "mx-0.5 block h-6 w-px shrink-0 bg-border" });
19224
19678
  }
19225
19679
  function CardStyleMenu({
19226
19680
  value,
@@ -19228,7 +19682,7 @@ function CardStyleMenu({
19228
19682
  disabled = false
19229
19683
  }) {
19230
19684
  const current = PRICING_CARD_STYLES.find((s) => s.value === value) ?? PRICING_CARD_STYLES[0];
19231
- const trigger = /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
19685
+ const trigger = /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
19232
19686
  "button",
19233
19687
  {
19234
19688
  type: "button",
@@ -19239,28 +19693,28 @@ function CardStyleMenu({
19239
19693
  disabled && "cursor-not-allowed opacity-40 hover:bg-transparent"
19240
19694
  ),
19241
19695
  children: [
19242
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(StyleSwatch, { style: current.value }),
19243
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "flex-1 text-left", children: current.label }),
19244
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.ChevronDown, { size: 16, style: { pointerEvents: "none" } })
19696
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(StyleSwatch, { style: current.value }),
19697
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "flex-1 text-left", children: current.label }),
19698
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react10.ChevronDown, { size: 16, style: { pointerEvents: "none" } })
19245
19699
  ]
19246
19700
  }
19247
19701
  );
19248
19702
  if (disabled) {
19249
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-1.5 pl-1.5", children: [
19250
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "text-xs text-muted-foreground opacity-50", children: "Card style" }),
19251
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Tooltip, { children: [
19252
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "inline-flex", children: trigger }) }),
19253
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(TooltipContent, { side: "bottom", sideOffset: 8, children: "Mark as featured to edit card style" })
19703
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex items-center gap-1.5 pl-1.5", children: [
19704
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "text-xs text-muted-foreground opacity-50", children: "Card style" }),
19705
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(Tooltip, { children: [
19706
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "inline-flex", children: trigger }) }),
19707
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TooltipContent, { side: "bottom", sideOffset: 8, children: "Mark as featured to edit card style" })
19254
19708
  ] })
19255
19709
  ] });
19256
19710
  }
19257
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-1.5 pl-1.5", children: [
19258
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "text-xs text-muted-foreground", children: "Card style" }),
19259
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(DropdownMenu, { modal: false, children: [
19260
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(DropdownMenuTrigger, { asChild: true, children: trigger }),
19261
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(DropdownMenuContent, { align: "end", sideOffset: 8, className: "w-[152px] min-w-[152px] p-1", children: PRICING_CARD_STYLES.map((style) => {
19711
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex items-center gap-1.5 pl-1.5", children: [
19712
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "text-xs text-muted-foreground", children: "Card style" }),
19713
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(DropdownMenu, { modal: false, children: [
19714
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(DropdownMenuTrigger, { asChild: true, children: trigger }),
19715
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(DropdownMenuContent, { align: "end", sideOffset: 8, className: "w-[152px] min-w-[152px] p-1", children: PRICING_CARD_STYLES.map((style) => {
19262
19716
  const selected = style.value === value;
19263
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
19717
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
19264
19718
  DropdownMenuItem,
19265
19719
  {
19266
19720
  role: "menuitemradio",
@@ -19268,9 +19722,9 @@ function CardStyleMenu({
19268
19722
  onSelect: () => onChange(style.value),
19269
19723
  className: cn("grid grid-cols-[18px_1fr_16px] gap-2 py-2 pl-2 pr-2 text-[13px]", selected && "bg-muted"),
19270
19724
  children: [
19271
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(StyleSwatch, { style: style.value }),
19272
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: style.label }),
19273
- selected && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Check, { size: 16, className: "text-bridge-primary" })
19725
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(StyleSwatch, { style: style.value }),
19726
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { children: style.label }),
19727
+ selected && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react10.Check, { size: 16, className: "text-bridge-primary" })
19274
19728
  ]
19275
19729
  },
19276
19730
  style.value
@@ -19300,7 +19754,7 @@ function PricingCardToolbar({
19300
19754
  const style = pricingCardStyle(entry.tree, index);
19301
19755
  const featuredTag = pricingCardFeaturedTag(entry.tree, index);
19302
19756
  const mobile = window.innerWidth <= 480;
19303
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(TooltipProvider, { delayDuration: 0, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
19757
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TooltipProvider, { delayDuration: 0, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
19304
19758
  "div",
19305
19759
  {
19306
19760
  "data-ohw-bridge": "",
@@ -19312,7 +19766,7 @@ function PricingCardToolbar({
19312
19766
  ),
19313
19767
  style: { maxWidth: mobile ? "calc(100vw - 24px)" : void 0 },
19314
19768
  children: [
19315
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
19769
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
19316
19770
  "div",
19317
19771
  {
19318
19772
  className: cn(
@@ -19321,8 +19775,8 @@ function PricingCardToolbar({
19321
19775
  mobile ? "flex-wrap justify-center" : "flex-nowrap"
19322
19776
  ),
19323
19777
  children: [
19324
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Tooltip, { children: [
19325
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19778
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(Tooltip, { children: [
19779
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
19326
19780
  CustomToolbarButton,
19327
19781
  {
19328
19782
  type: "button",
@@ -19330,8 +19784,8 @@ function PricingCardToolbar({
19330
19784
  "aria-pressed": featuredTag,
19331
19785
  onClick: () => onSetFeaturedTag(!featuredTag),
19332
19786
  className: "group size-8",
19333
- children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19334
- import_lucide_react9.Star,
19787
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
19788
+ import_lucide_react10.Star,
19335
19789
  {
19336
19790
  size: 16,
19337
19791
  fill: "currentColor",
@@ -19345,25 +19799,25 @@ function PricingCardToolbar({
19345
19799
  )
19346
19800
  }
19347
19801
  ) }),
19348
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(TooltipContent, { side: "bottom", sideOffset: 8, children: featuredTag ? "Unmark featured" : "Mark as featured" })
19802
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TooltipContent, { side: "bottom", sideOffset: 8, children: featuredTag ? "Unmark featured" : "Mark as featured" })
19349
19803
  ] }),
19350
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarSeparator, {}),
19351
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(CardStyleMenu, { value: style, onChange: onSetStyle, disabled: !featuredTag }),
19352
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarSeparator, {}),
19353
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarAction, { label: "Add button", onClick: onAddButton, disabled: buttonCount >= 2, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ButtonGlyph, {}) }),
19354
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarAction, { label: "Remove button", onClick: onRemoveButton, disabled: buttonCount === 0, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("span", { className: "relative inline-flex", children: [
19355
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ButtonGlyph, {}),
19356
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Minus, { size: 10, strokeWidth: 2.5, className: "absolute -right-1.5 -top-1.5 rounded-full bg-background text-bridge-primary" })
19804
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ToolbarSeparator2, {}),
19805
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(CardStyleMenu, { value: style, onChange: onSetStyle, disabled: !featuredTag }),
19806
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ToolbarSeparator2, {}),
19807
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ToolbarAction2, { label: "Add button", onClick: onAddButton, disabled: buttonCount >= 2, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ButtonGlyph, {}) }),
19808
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ToolbarAction2, { label: "Remove button", onClick: onRemoveButton, disabled: buttonCount === 0, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("span", { className: "relative inline-flex", children: [
19809
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ButtonGlyph, {}),
19810
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react10.Minus, { size: 10, strokeWidth: 2.5, className: "absolute -right-1.5 -top-1.5 rounded-full bg-background text-bridge-primary" })
19357
19811
  ] }) }),
19358
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarAction, { label: "Add feature", onClick: onAddFeature, disabled: featureCount >= 10, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(FeatureGlyph, { mode: "add" }) }),
19359
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarAction, { label: "Remove feature", onClick: onRemoveFeature, disabled: featureCount <= 1, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(FeatureGlyph, { mode: "remove" }) }),
19360
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarSeparator, {}),
19361
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarAction, { label: "Add tier", onClick: onAddTier, disabled: collectionCount >= 4, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Plus, { size: 16, style: { pointerEvents: "none" } }) }),
19362
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ToolbarAction, { label: "Remove tier", onClick: onRemoveTier, disabled: collectionCount <= 1, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Minus, { size: 16, style: { pointerEvents: "none" } }) })
19812
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ToolbarAction2, { label: "Add feature", onClick: onAddFeature, disabled: featureCount >= 10, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(FeatureGlyph, { mode: "add" }) }),
19813
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ToolbarAction2, { label: "Remove feature", onClick: onRemoveFeature, disabled: featureCount <= 1, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(FeatureGlyph, { mode: "remove" }) }),
19814
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ToolbarSeparator2, {}),
19815
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ToolbarAction2, { label: "Add tier", onClick: onAddTier, disabled: collectionCount >= 4, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react10.Plus, { size: 16, style: { pointerEvents: "none" } }) }),
19816
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ToolbarAction2, { label: "Remove tier", onClick: onRemoveTier, disabled: collectionCount <= 1, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react10.Minus, { size: 16, style: { pointerEvents: "none" } }) })
19363
19817
  ]
19364
19818
  }
19365
19819
  ),
19366
- error && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "max-w-[260px] rounded-md bg-background px-2 py-1 text-center text-[11px] text-destructive shadow-sm", children: error })
19820
+ error && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "max-w-[260px] rounded-md bg-background px-2 py-1 text-center text-[11px] text-destructive shadow-sm", children: error })
19367
19821
  ]
19368
19822
  }
19369
19823
  ) });
@@ -19397,7 +19851,7 @@ function AiPricingCardOverlay({
19397
19851
  const overEditable = Boolean(t?.closest?.("[data-ohw-editable], [data-ohw-role], a, button"));
19398
19852
  setHover((prev) => {
19399
19853
  const next = ref && !overEditable ? ref : null;
19400
- return sameRef(prev, next) ? prev : next;
19854
+ return sameRef2(prev, next) ? prev : next;
19401
19855
  });
19402
19856
  };
19403
19857
  const onLeave = () => setHover(null);
@@ -19412,10 +19866,10 @@ function AiPricingCardOverlay({
19412
19866
  const onClick = (e) => {
19413
19867
  const t = e.target;
19414
19868
  if (!(t instanceof Element)) return;
19415
- if (t.closest(CHROME_SELECTOR)) return;
19869
+ if (t.closest(CHROME_SELECTOR2)) return;
19416
19870
  const ref = cardRefFromTarget(t);
19417
- setActive((prev) => sameRef(prev, ref) ? prev : ref);
19418
- if (!ref || !sameRef(activeRef.current, ref)) setError(null);
19871
+ setActive((prev) => sameRef2(prev, ref) ? prev : ref);
19872
+ if (!ref || !sameRef2(activeRef.current, ref)) setError(null);
19419
19873
  };
19420
19874
  document.addEventListener("click", onClick, true);
19421
19875
  return () => document.removeEventListener("click", onClick, true);
@@ -19480,12 +19934,12 @@ function AiPricingCardOverlay({
19480
19934
  const activeEntry = active ? findEntry(active.sectionId) : null;
19481
19935
  const activeCardRect = useLiveCardRect(active);
19482
19936
  const activeGridRect = useLiveCardRect(active, true);
19483
- const hoverRect = useLiveCardRect(hover && !sameRef(hover, active) ? hover : null);
19484
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
19485
- hoverRect && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ItemInteractionLayer, { rect: hoverRect, state: "hover" }),
19486
- activeCardRect && activeGridRect && active && activeEntry && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
19487
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ItemInteractionLayer, { rect: activeCardRect, state: "active-top" }),
19488
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(GridToolbarSlot, { gridRect: activeGridRect, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19937
+ const hoverRect = useLiveCardRect(hover && !sameRef2(hover, active) ? hover : null);
19938
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
19939
+ hoverRect && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ItemInteractionLayer, { rect: hoverRect, state: "hover" }),
19940
+ activeCardRect && activeGridRect && active && activeEntry && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
19941
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ItemInteractionLayer, { rect: activeCardRect, state: "active-top" }),
19942
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(GridToolbarSlot, { gridRect: activeGridRect, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
19489
19943
  PricingCardToolbar,
19490
19944
  {
19491
19945
  entry: activeEntry,
@@ -19513,7 +19967,7 @@ function AiPricingCardOverlay({
19513
19967
  onRemoveFeature: () => runMutation(active.sectionId, (tree) => removePricingCardFeature(tree, active.index))
19514
19968
  }
19515
19969
  ) }),
19516
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
19970
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
19517
19971
  "div",
19518
19972
  {
19519
19973
  "data-ohw-bridge": "",
@@ -19535,7 +19989,7 @@ function AiPricingCardOverlay({
19535
19989
 
19536
19990
  // src/ui/ai-section/AiVideoOverlay.tsx
19537
19991
  var import_react12 = require("react");
19538
- var import_lucide_react10 = require("lucide-react");
19992
+ var import_lucide_react11 = require("lucide-react");
19539
19993
 
19540
19994
  // src/lib/ai-video-tree.ts
19541
19995
  function resolveVideoBlock(tree, path) {
@@ -19606,10 +20060,10 @@ function extractYouTubeId(input) {
19606
20060
  }
19607
20061
 
19608
20062
  // src/ui/ai-section/AiVideoOverlay.tsx
19609
- var import_jsx_runtime22 = require("react/jsx-runtime");
20063
+ var import_jsx_runtime23 = require("react/jsx-runtime");
19610
20064
  var PRIMARY5 = "#0885FE";
19611
- var OHW_BLUE2 = `var(--ohw-primary, ${PRIMARY5})`;
19612
- var OHW_BLUE_DARK2 = `color-mix(in srgb, var(--ohw-primary, ${PRIMARY5}) 60%, #000000)`;
20065
+ var OHW_BLUE = `var(--ohw-primary, ${PRIMARY5})`;
20066
+ var OHW_BLUE_DARK = `color-mix(in srgb, var(--ohw-primary, ${PRIMARY5}) 60%, #000000)`;
19613
20067
  function findVideoBlockElement(sectionId, path) {
19614
20068
  const sectionEl = findByInstanceId(sectionId);
19615
20069
  if (!sectionEl) return null;
@@ -19669,12 +20123,12 @@ function requestValidateTree3(postToParent2, tree) {
19669
20123
  postToParent2({ type: "ow:ai-validate-tree-request", id, tree });
19670
20124
  });
19671
20125
  }
19672
- function Pill2({
20126
+ function Pill({
19673
20127
  label,
19674
20128
  onClick,
19675
20129
  disabled
19676
20130
  }) {
19677
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
20131
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
19678
20132
  "button",
19679
20133
  {
19680
20134
  type: "button",
@@ -19690,7 +20144,7 @@ function Pill2({
19690
20144
  border: "none",
19691
20145
  cursor: disabled ? "default" : "pointer",
19692
20146
  background: "#ffffff",
19693
- color: OHW_BLUE_DARK2,
20147
+ color: OHW_BLUE_DARK,
19694
20148
  opacity: disabled ? 0.4 : 1,
19695
20149
  fontSize: 12,
19696
20150
  lineHeight: "16px",
@@ -19733,8 +20187,8 @@ function VideoUrlInput({ value, onCommit }) {
19733
20187
  onCommit(id);
19734
20188
  }
19735
20189
  };
19736
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 4, width: "100%", maxWidth: 280 }, children: [
19737
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
20190
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 4, width: "100%", maxWidth: 280 }, children: [
20191
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
19738
20192
  "div",
19739
20193
  {
19740
20194
  style: {
@@ -19750,12 +20204,12 @@ function VideoUrlInput({ value, onCommit }) {
19750
20204
  boxShadow: invalid ? "inset 0 0 0 1.5px #EF4444" : "none"
19751
20205
  },
19752
20206
  children: [
19753
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
20207
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
19754
20208
  "span",
19755
20209
  {
19756
20210
  style: {
19757
20211
  flex: "0 0 auto",
19758
- color: OHW_BLUE_DARK2,
20212
+ color: OHW_BLUE_DARK,
19759
20213
  fontSize: 10,
19760
20214
  lineHeight: "16px",
19761
20215
  fontWeight: 700,
@@ -19765,7 +20219,7 @@ function VideoUrlInput({ value, onCommit }) {
19765
20219
  children: "Video"
19766
20220
  }
19767
20221
  ),
19768
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
20222
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
19769
20223
  "input",
19770
20224
  {
19771
20225
  type: "text",
@@ -19802,7 +20256,7 @@ function VideoUrlInput({ value, onCommit }) {
19802
20256
  ]
19803
20257
  }
19804
20258
  ),
19805
- invalid && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { style: { color: "#FCA5A5", fontSize: 11, textAlign: "center" }, children: "That doesn't look like a YouTube link \u2014 try pasting the video URL again." })
20259
+ invalid && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { style: { color: "#FCA5A5", fontSize: 11, textAlign: "center" }, children: "That doesn't look like a YouTube link \u2014 try pasting the video URL again." })
19806
20260
  ] });
19807
20261
  }
19808
20262
  function VideoToolbar({
@@ -19812,7 +20266,7 @@ function VideoToolbar({
19812
20266
  onSetVideoId,
19813
20267
  onRemove
19814
20268
  }) {
19815
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
20269
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
19816
20270
  "div",
19817
20271
  {
19818
20272
  "data-ohw-bridge": "",
@@ -19824,7 +20278,7 @@ function VideoToolbar({
19824
20278
  gap: 6,
19825
20279
  alignItems: "center",
19826
20280
  width: 280,
19827
- background: OHW_BLUE2,
20281
+ background: OHW_BLUE,
19828
20282
  padding: 8,
19829
20283
  borderRadius: 12,
19830
20284
  boxShadow: "0 8px 24px rgba(0,0,0,0.25)",
@@ -19832,9 +20286,9 @@ function VideoToolbar({
19832
20286
  opacity: pending ? 0.6 : 1
19833
20287
  },
19834
20288
  children: [
19835
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(VideoUrlInput, { value: videoId, onCommit: onSetVideoId }),
19836
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Pill2, { label: "Remove video", onClick: onRemove, disabled: !videoId }),
19837
- error && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { style: { color: "#FCA5A5", fontSize: 11, maxWidth: 220, textAlign: "center" }, children: error })
20289
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(VideoUrlInput, { value: videoId, onCommit: onSetVideoId }),
20290
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Pill, { label: "Remove video", onClick: onRemove, disabled: !videoId }),
20291
+ error && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { style: { color: "#FCA5A5", fontSize: 11, maxWidth: 220, textAlign: "center" }, children: error })
19838
20292
  ]
19839
20293
  }
19840
20294
  );
@@ -19954,8 +20408,8 @@ function AiVideoOverlay({
19954
20408
  const activeBlockRect = useLiveBlockRect(active);
19955
20409
  const TOOLBAR_APPROX_H = 90;
19956
20410
  const toolbarState = activeBlockRect && activeBlockRect.top < TOOLBAR_APPROX_H ? "active-bottom" : "active-top";
19957
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
19958
- hoverHandleRect && hover && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
20411
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
20412
+ hoverHandleRect && hover && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
19959
20413
  "button",
19960
20414
  {
19961
20415
  type: "button",
@@ -19983,15 +20437,15 @@ function AiVideoOverlay({
19983
20437
  fontSize: 14,
19984
20438
  fontWeight: 700
19985
20439
  },
19986
- children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react10.Star, { size: 14, style: { pointerEvents: "none" } })
20440
+ children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_lucide_react11.Star, { size: 14, style: { pointerEvents: "none" } })
19987
20441
  }
19988
20442
  ),
19989
- activeBlockRect && active && activeEntry && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
20443
+ activeBlockRect && active && activeEntry && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
19990
20444
  ItemInteractionLayer,
19991
20445
  {
19992
20446
  rect: activeBlockRect,
19993
20447
  state: toolbarState,
19994
- toolbar: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
20448
+ toolbar: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
19995
20449
  VideoToolbar,
19996
20450
  {
19997
20451
  videoId: videoIdAt(activeEntry.tree, active.path),
@@ -20015,7 +20469,7 @@ function AiVideoOverlay({
20015
20469
 
20016
20470
  // src/ui/ai-section/AiFadeCarouselOverlay.tsx
20017
20471
  var import_react13 = require("react");
20018
- var import_lucide_react11 = require("lucide-react");
20472
+ var import_lucide_react12 = require("lucide-react");
20019
20473
 
20020
20474
  // src/lib/ai-carousel-tree.ts
20021
20475
  function isCarouselBlock(node) {
@@ -20066,10 +20520,10 @@ function setCarouselAutoplay(tree, on) {
20066
20520
  }
20067
20521
 
20068
20522
  // src/ui/ai-section/AiFadeCarouselOverlay.tsx
20069
- var import_jsx_runtime23 = require("react/jsx-runtime");
20523
+ var import_jsx_runtime24 = require("react/jsx-runtime");
20070
20524
  var PRIMARY6 = "#0885FE";
20071
- var OHW_BLUE3 = `var(--ohw-primary, ${PRIMARY6})`;
20072
- var OHW_BLUE_DARK3 = `color-mix(in srgb, var(--ohw-primary, ${PRIMARY6}) 60%, #000000)`;
20525
+ var OHW_BLUE2 = `var(--ohw-primary, ${PRIMARY6})`;
20526
+ var OHW_BLUE_DARK2 = `color-mix(in srgb, var(--ohw-primary, ${PRIMARY6}) 60%, #000000)`;
20073
20527
  function findCarouselItemElement(sectionId, index) {
20074
20528
  const sectionEl = findByInstanceId(sectionId);
20075
20529
  if (!sectionEl) return null;
@@ -20148,8 +20602,8 @@ function requestAssetPick2(postToParent2) {
20148
20602
  postToParent2({ type: "ow:asset-pick", id });
20149
20603
  });
20150
20604
  }
20151
- function Pill3({ label, onClick, disabled }) {
20152
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
20605
+ function Pill2({ label, onClick, disabled }) {
20606
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
20153
20607
  "button",
20154
20608
  {
20155
20609
  type: "button",
@@ -20165,7 +20619,7 @@ function Pill3({ label, onClick, disabled }) {
20165
20619
  border: "none",
20166
20620
  cursor: disabled ? "default" : "pointer",
20167
20621
  background: "#ffffff",
20168
- color: OHW_BLUE_DARK3,
20622
+ color: OHW_BLUE_DARK2,
20169
20623
  opacity: disabled ? 0.4 : 1,
20170
20624
  fontSize: 12,
20171
20625
  lineHeight: "16px",
@@ -20183,7 +20637,7 @@ function TogglePill({
20183
20637
  onToggle,
20184
20638
  disabled
20185
20639
  }) {
20186
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
20640
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
20187
20641
  "button",
20188
20642
  {
20189
20643
  type: "button",
@@ -20202,7 +20656,7 @@ function TogglePill({
20202
20656
  border: "none",
20203
20657
  cursor: disabled ? "default" : "pointer",
20204
20658
  background: "#ffffff",
20205
- color: OHW_BLUE_DARK3,
20659
+ color: OHW_BLUE_DARK2,
20206
20660
  opacity: disabled ? 0.4 : 1,
20207
20661
  fontSize: 12,
20208
20662
  lineHeight: "16px",
@@ -20212,7 +20666,7 @@ function TogglePill({
20212
20666
  },
20213
20667
  children: [
20214
20668
  label,
20215
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
20669
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
20216
20670
  "span",
20217
20671
  {
20218
20672
  "aria-hidden": true,
@@ -20225,10 +20679,10 @@ function TogglePill({
20225
20679
  // Off is a grey track, not a washed-out blue: the pill's own white background already
20226
20680
  // carries the brand colour in the text, and a pale blue track next to it read as
20227
20681
  // "disabled control" rather than "switch that is off".
20228
- background: on ? OHW_BLUE_DARK3 : "#CBD5E1",
20682
+ background: on ? OHW_BLUE_DARK2 : "#CBD5E1",
20229
20683
  transition: "background 120ms ease"
20230
20684
  },
20231
- children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
20685
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
20232
20686
  "span",
20233
20687
  {
20234
20688
  style: {
@@ -20257,7 +20711,7 @@ function CarouselSlideToolbar({
20257
20711
  onChangeImage,
20258
20712
  onToggleAutoplay
20259
20713
  }) {
20260
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
20714
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
20261
20715
  "div",
20262
20716
  {
20263
20717
  "data-ohw-bridge": "",
@@ -20268,7 +20722,7 @@ function CarouselSlideToolbar({
20268
20722
  flexDirection: "column",
20269
20723
  gap: 6,
20270
20724
  alignItems: "center",
20271
- background: OHW_BLUE3,
20725
+ background: OHW_BLUE2,
20272
20726
  padding: 8,
20273
20727
  borderRadius: 12,
20274
20728
  boxShadow: "0 8px 24px rgba(0,0,0,0.25)",
@@ -20280,11 +20734,11 @@ function CarouselSlideToolbar({
20280
20734
  pointerEvents: pending ? "none" : "auto"
20281
20735
  },
20282
20736
  children: [
20283
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [
20284
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Pill3, { label: "Change Image", onClick: onChangeImage }),
20285
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(TogglePill, { label: "Autoplay", on: autoplay, onToggle: onToggleAutoplay })
20737
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [
20738
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Pill2, { label: "Change Image", onClick: onChangeImage }),
20739
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(TogglePill, { label: "Autoplay", on: autoplay, onToggle: onToggleAutoplay })
20286
20740
  ] }),
20287
- error && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { style: { color: "#FCA5A5", fontSize: 11, maxWidth: 220, textAlign: "center" }, children: error })
20741
+ error && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { style: { color: "#FCA5A5", fontSize: 11, maxWidth: 220, textAlign: "center" }, children: error })
20288
20742
  ]
20289
20743
  }
20290
20744
  );
@@ -20424,8 +20878,8 @@ function AiFadeCarouselOverlay({
20424
20878
  const activeItemRect = useLiveItemRect2(active);
20425
20879
  const TOOLBAR_APPROX_H = 90;
20426
20880
  const toolbarState = activeItemRect && activeItemRect.top < TOOLBAR_APPROX_H ? "active-bottom" : "active-top";
20427
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
20428
- hoverHandleRect && hover && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
20881
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
20882
+ hoverHandleRect && hover && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
20429
20883
  "button",
20430
20884
  {
20431
20885
  type: "button",
@@ -20454,10 +20908,10 @@ function AiFadeCarouselOverlay({
20454
20908
  fontSize: 14,
20455
20909
  fontWeight: 700
20456
20910
  },
20457
- children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_lucide_react11.Star, { size: 14, style: { pointerEvents: "none" } })
20911
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_lucide_react12.Star, { size: 14, style: { pointerEvents: "none" } })
20458
20912
  }
20459
20913
  ),
20460
- swapping && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
20914
+ swapping && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
20461
20915
  "div",
20462
20916
  {
20463
20917
  ref: skeletonRef,
@@ -20475,15 +20929,15 @@ function AiFadeCarouselOverlay({
20475
20929
  zIndex: 2147483646,
20476
20930
  pointerEvents: "none"
20477
20931
  },
20478
- children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("style", { children: MEDIA_SKELETON_CSS })
20932
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("style", { children: MEDIA_SKELETON_CSS })
20479
20933
  }
20480
20934
  ),
20481
- activeItemRect && active && activeEntry && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
20935
+ activeItemRect && active && activeEntry && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
20482
20936
  ItemInteractionLayer,
20483
20937
  {
20484
20938
  rect: activeItemRect,
20485
20939
  state: toolbarState,
20486
- toolbar: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
20940
+ toolbar: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
20487
20941
  CarouselSlideToolbar,
20488
20942
  {
20489
20943
  pending,
@@ -20576,165 +21030,30 @@ function findButtonAtPoint(x, y, media) {
20576
21030
  if (hits.length === 0) return null;
20577
21031
  return hits.reduce((best, el) => best.contains(el) ? el : best);
20578
21032
  }
20579
- function findNavbarCtaAtPoint(x, y) {
20580
- const hits = Array.from(
20581
- document.querySelectorAll('[data-ohw-role="navbar-button"]')
20582
- ).filter((el) => containsPoint(el, x, y) && !el.closest("[data-ohw-bridge]"));
20583
- if (hits.length === 0) return null;
20584
- return hits.reduce((best, el) => best.contains(el) ? el : best);
20585
- }
20586
-
20587
- // src/lib/session-search.ts
20588
- var STORAGE_KEY = "ohw-preserved-search";
20589
- function readPreservedSearch() {
20590
- if (typeof window === "undefined") return "";
20591
- const fromUrl = window.location.search;
20592
- if (fromUrl && fromUrl !== "?") {
20593
- sessionStorage.setItem(STORAGE_KEY, fromUrl);
20594
- return fromUrl;
20595
- }
20596
- return sessionStorage.getItem(STORAGE_KEY) ?? "";
20597
- }
20598
- function isEditSessionActive() {
20599
- const search = readPreservedSearch();
20600
- if (!search || search === "?") return false;
20601
- const q = search.startsWith("?") ? search.slice(1) : search;
20602
- return new URLSearchParams(q).get("mode") === "edit";
20603
- }
20604
-
20605
- // src/ui/link-modal/linkModal.utils.ts
20606
- function parseTarget(target) {
20607
- if (!target) return { pageRoute: "", sectionId: null };
20608
- const hashIndex = target.indexOf("#");
20609
- if (hashIndex === -1) return { pageRoute: target, sectionId: null };
20610
- return {
20611
- pageRoute: target.slice(0, hashIndex),
20612
- sectionId: target.slice(hashIndex + 1) || null
20613
- };
20614
- }
20615
- function normalizePath(path) {
20616
- const trimmed = path.trim();
20617
- if (!trimmed) return "/";
20618
- try {
20619
- if (/^https?:\/\//i.test(trimmed)) {
20620
- return new URL(trimmed).pathname || "/";
20621
- }
20622
- } catch {
20623
- }
20624
- const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
20625
- if (withSlash.length > 1 && withSlash.endsWith("/")) return withSlash.slice(0, -1);
20626
- return withSlash;
20627
- }
20628
- function findPageByPath(pages, path) {
20629
- const normalized = normalizePath(path);
20630
- return pages.find((p) => normalizePath(p.path) === normalized);
20631
- }
20632
- function inferPageTitleFromPath(path) {
20633
- const normalized = normalizePath(path);
20634
- if (normalized === "/") return "Home";
20635
- const segment = normalized.split("/").filter(Boolean).pop() ?? "";
20636
- if (!segment) return "Home";
20637
- return segment.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
20638
- }
20639
- function isInternalPath(route) {
20640
- const trimmed = route.trim();
20641
- if (!trimmed) return false;
20642
- if (/^https?:\/\//i.test(trimmed)) return false;
20643
- if (trimmed.startsWith("/")) return true;
20644
- if (trimmed.includes(" ") || trimmed.includes(".")) return false;
20645
- return true;
20646
- }
20647
- function resolvePage(route, pages) {
20648
- const found = findPageByPath(pages, route);
20649
- if (found) return { path: found.path, title: found.title };
20650
- return { path: normalizePath(route), title: inferPageTitleFromPath(route) };
20651
- }
20652
- function getSectionsForPath(sectionsByPath, pageRoute) {
20653
- if (!sectionsByPath) return [];
20654
- const normalized = normalizePath(pageRoute);
20655
- return sectionsByPath[normalized] ?? sectionsByPath[pageRoute] ?? [];
20656
- }
20657
- function buildTarget(page, section) {
20658
- if (section) return `${page.path}#${section.id}`;
20659
- return page.path;
20660
- }
20661
- function isValidUrl(urlString) {
20662
- try {
20663
- const url = new URL(urlString);
20664
- if (url.protocol !== "http:" && url.protocol !== "https:") return false;
20665
- const hostname = url.hostname;
20666
- if (!hostname || hostname.length === 0) return false;
20667
- if (hostname.includes(" ")) return false;
20668
- if (hostname === "localhost") return true;
20669
- if (!hostname.includes(".")) return false;
20670
- if (!/[a-z]/i.test(hostname)) return false;
20671
- return true;
20672
- } catch {
20673
- return false;
20674
- }
20675
- }
20676
- function normalizeUrl(value) {
20677
- const trimmed = value.trim();
20678
- if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
20679
- return `https://${trimmed}`;
20680
- }
20681
- function validateUrlInput(value, pages, allPages) {
20682
- const trimmed = value.trim();
20683
- if (!trimmed) return "";
20684
- const exactPage = allPages.find((p) => p.title.toLowerCase() === trimmed.toLowerCase());
20685
- if (exactPage) return "";
20686
- const hasPrefixMatch = pages.some((p) => p.title.toLowerCase().startsWith(trimmed.toLowerCase()));
20687
- if (hasPrefixMatch) {
20688
- return "Select a page from the list or enter a valid URL";
20689
- }
20690
- const hasScheme = /^https?:\/\//i.test(trimmed);
20691
- if (!hasScheme && !trimmed.includes(".")) {
20692
- return "Please enter a valid URL (e.g., https://example.com)";
20693
- }
20694
- if (!isValidUrl(normalizeUrl(trimmed))) {
20695
- return "Please enter a valid URL (e.g., https://example.com)";
20696
- }
20697
- return "";
20698
- }
20699
- function getEditModeInitialState(target, pages, sections) {
20700
- const empty = {
20701
- searchValue: "",
20702
- selectedPage: null,
20703
- selectedSection: null,
20704
- step: "input"
20705
- };
20706
- if (!target) return empty;
20707
- if (isValidUrl(normalizeUrl(target))) {
20708
- return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
20709
- }
20710
- const { pageRoute, sectionId } = parseTarget(target);
20711
- if (sectionId) {
20712
- if (isInternalPath(pageRoute)) {
20713
- const page = resolvePage(pageRoute, pages);
20714
- const sec = sections.find((s) => s.id === sectionId);
20715
- return {
20716
- searchValue: page.title,
20717
- selectedPage: page,
20718
- selectedSection: sec ? { id: sec.id, label: sec.label } : null,
20719
- step: "input"
20720
- };
20721
- }
20722
- return { searchValue: pageRoute || target, selectedPage: null, selectedSection: null, step: "input" };
20723
- }
20724
- if (isInternalPath(pageRoute)) {
20725
- const page = resolvePage(pageRoute, pages);
20726
- return {
20727
- searchValue: page.title,
20728
- selectedPage: page,
20729
- selectedSection: null,
20730
- step: "input"
20731
- };
21033
+ function findNavbarCtaAtPoint(x, y) {
21034
+ const hits = Array.from(
21035
+ document.querySelectorAll('[data-ohw-role="navbar-button"]')
21036
+ ).filter((el) => containsPoint(el, x, y) && !el.closest("[data-ohw-bridge]"));
21037
+ if (hits.length === 0) return null;
21038
+ return hits.reduce((best, el) => best.contains(el) ? el : best);
21039
+ }
21040
+
21041
+ // src/lib/session-search.ts
21042
+ var STORAGE_KEY = "ohw-preserved-search";
21043
+ function readPreservedSearch() {
21044
+ if (typeof window === "undefined") return "";
21045
+ const fromUrl = window.location.search;
21046
+ if (fromUrl && fromUrl !== "?") {
21047
+ sessionStorage.setItem(STORAGE_KEY, fromUrl);
21048
+ return fromUrl;
20732
21049
  }
20733
- return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
21050
+ return sessionStorage.getItem(STORAGE_KEY) ?? "";
20734
21051
  }
20735
- function filterAvailablePages(pages, existingTargets) {
20736
- const taken = new Set(existingTargets);
20737
- return pages.filter((p) => !taken.has(p.path));
21052
+ function isEditSessionActive() {
21053
+ const search = readPreservedSearch();
21054
+ if (!search || search === "?") return false;
21055
+ const q = search.startsWith("?") ? search.slice(1) : search;
21056
+ return new URLSearchParams(q).get("mode") === "edit";
20738
21057
  }
20739
21058
 
20740
21059
  // src/lib/prefetch-sections.ts
@@ -20836,113 +21155,6 @@ function scrollToHashSectionWhenReady(behavior = "smooth") {
20836
21155
  // src/ui/link-modal/LinkPopover.tsx
20837
21156
  var import_react17 = require("react");
20838
21157
 
20839
- // src/ui/dialog.tsx
20840
- var React9 = __toESM(require("react"), 1);
20841
- var import_radix_ui6 = require("radix-ui");
20842
- var import_lucide_react12 = require("lucide-react");
20843
- var import_jsx_runtime24 = require("react/jsx-runtime");
20844
- function Dialog2({
20845
- ...props
20846
- }) {
20847
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_radix_ui6.Dialog.Root, { "data-slot": "dialog", ...props });
20848
- }
20849
- function DialogPortal({
20850
- ...props
20851
- }) {
20852
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_radix_ui6.Dialog.Portal, { ...props });
20853
- }
20854
- function DialogOverlay({
20855
- className,
20856
- ...props
20857
- }) {
20858
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
20859
- import_radix_ui6.Dialog.Overlay,
20860
- {
20861
- "data-slot": "dialog-overlay",
20862
- "data-ohw-link-modal-root": "",
20863
- className: cn("fixed inset-0 z-[2147483646] bg-black/50", className),
20864
- ...props
20865
- }
20866
- );
20867
- }
20868
- var DialogContent = React9.forwardRef(
20869
- ({ className, children, showCloseButton = true, container, ...props }, ref) => {
20870
- const positionMode = container ? "absolute" : "fixed";
20871
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(DialogPortal, { container: container ?? void 0, children: [
20872
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(DialogOverlay, { className: cn(positionMode, "inset-0") }),
20873
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
20874
- import_radix_ui6.Dialog.Content,
20875
- {
20876
- ref,
20877
- "data-slot": "dialog-content",
20878
- className: cn(
20879
- positionMode,
20880
- "left-1/2 top-1/2 z-[2147483647] flex w-full max-w-[448px] -translate-x-1/2 -translate-y-1/2 flex-col overflow-visible",
20881
- "rounded-xl border border-border bg-background font-sans shadow-lg outline-none",
20882
- container ? "max-h-[calc(100%-32px)]" : "max-h-[calc(100vh-32px)]",
20883
- className
20884
- ),
20885
- onMouseDown: (e) => e.stopPropagation(),
20886
- ...props,
20887
- children: [
20888
- children,
20889
- showCloseButton ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
20890
- import_radix_ui6.Dialog.Close,
20891
- {
20892
- type: "button",
20893
- className: "absolute right-[9px] top-[9px] rounded-sm p-1.5 text-foreground hover:bg-muted/50",
20894
- "aria-label": "Close",
20895
- children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_lucide_react12.X, { size: 16, "aria-hidden": true })
20896
- }
20897
- ) : null
20898
- ]
20899
- }
20900
- )
20901
- ] });
20902
- }
20903
- );
20904
- DialogContent.displayName = import_radix_ui6.Dialog.Content.displayName;
20905
- function DialogHeader({
20906
- className,
20907
- ...props
20908
- }) {
20909
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: cn("flex flex-col gap-1.5", className), ...props });
20910
- }
20911
- function DialogFooter({
20912
- className,
20913
- ...props
20914
- }) {
20915
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
20916
- "div",
20917
- {
20918
- className: cn("flex items-center justify-end gap-2", className),
20919
- ...props
20920
- }
20921
- );
20922
- }
20923
- var DialogTitle = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
20924
- import_radix_ui6.Dialog.Title,
20925
- {
20926
- ref,
20927
- className: cn(
20928
- "text-lg font-semibold leading-none tracking-tight text-card-foreground",
20929
- className
20930
- ),
20931
- ...props
20932
- }
20933
- ));
20934
- DialogTitle.displayName = import_radix_ui6.Dialog.Title.displayName;
20935
- var DialogDescription = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
20936
- import_radix_ui6.Dialog.Description,
20937
- {
20938
- ref,
20939
- className: cn("text-sm text-muted-foreground", className),
20940
- ...props
20941
- }
20942
- ));
20943
- DialogDescription.displayName = import_radix_ui6.Dialog.Description.displayName;
20944
- var DialogClose = import_radix_ui6.Dialog.Close;
20945
-
20946
21158
  // src/ui/link-modal/LinkEditorPanel.tsx
20947
21159
  var import_lucide_react16 = require("lucide-react");
20948
21160
 
@@ -22046,6 +22258,7 @@ function shouldUseDevFixtures() {
22046
22258
  function getLogoElement(el) {
22047
22259
  const marked = el.closest('[data-ohw-role="logo"], [data-ohw-logo]');
22048
22260
  if (marked) return marked;
22261
+ if (el.closest('[data-ohw-editable="icon"]')) return null;
22049
22262
  const root = el.closest("nav, [data-ohw-nav-root], footer");
22050
22263
  if (!root) return null;
22051
22264
  const anchor = el.closest("a");
@@ -24626,7 +24839,7 @@ function getNavigationSelectionParent(el) {
24626
24839
  if (!isFooterLinksContainer(el) && (el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isInferredFooterGroup2(el))) {
24627
24840
  return resolveFooterLinksContainerFrom(el);
24628
24841
  }
24629
- if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || isFooterLinksContainer(el)) {
24842
+ 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)) {
24630
24843
  return getNavigationRoot(el);
24631
24844
  }
24632
24845
  return null;
@@ -24848,7 +25061,6 @@ var ICONS = {
24848
25061
  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"/>',
24849
25062
  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"/>'
24850
25063
  };
24851
- var HEIGHT_SETTLE_DELAYS = [150, 400, 800];
24852
25064
  var SELECTION_CHROME_GAP2 = 4;
24853
25065
  var TOOLBAR_STROKE_GAP2 = 4;
24854
25066
  var TOOLBAR_OFFSET_FROM_ELEMENT = SELECTION_CHROME_GAP2 + TOOLBAR_STROKE_GAP2;
@@ -24946,7 +25158,7 @@ function applyVisibleViewport(el, parentScroll) {
24946
25158
  el.style.top = `${top}px`;
24947
25159
  el.style.height = `${height}px`;
24948
25160
  }
24949
- var TOOLBAR_EDGE_MARGIN3 = 4;
25161
+ var TOOLBAR_EDGE_MARGIN4 = 4;
24950
25162
  function clampToolbarToClip(top, transform, rect, clip, approxH, gap) {
24951
25163
  const isAbove = transform.includes("translateY(-100%)");
24952
25164
  let visualTop = isAbove ? top - approxH : top;
@@ -24973,8 +25185,8 @@ function clampToolbarToClip(top, transform, rect, clip, approxH, gap) {
24973
25185
  function clampToolbarLeft(centerX, toolbarWidth) {
24974
25186
  const half = toolbarWidth / 2;
24975
25187
  return Math.max(
24976
- TOOLBAR_EDGE_MARGIN3 + half,
24977
- Math.min(centerX, window.innerWidth - TOOLBAR_EDGE_MARGIN3 - half)
25188
+ TOOLBAR_EDGE_MARGIN4 + half,
25189
+ Math.min(centerX, window.innerWidth - TOOLBAR_EDGE_MARGIN4 - half)
24978
25190
  );
24979
25191
  }
24980
25192
  function calcToolbarPos(rect, parentScroll, toolbarW = 306) {
@@ -25405,6 +25617,34 @@ function OhhwellsBridge() {
25405
25617
  const [fetchState, setFetchState] = (0, import_react21.useState)("idle");
25406
25618
  const [showBranding, setShowBranding] = (0, import_react21.useState)(false);
25407
25619
  const autoSaveTimers = (0, import_react21.useRef)(/* @__PURE__ */ new Map());
25620
+ (0, import_react21.useEffect)(() => {
25621
+ stampBridgeVersionOnDocument();
25622
+ }, []);
25623
+ (0, import_react21.useEffect)(() => {
25624
+ if (isEditMode) return;
25625
+ return armLoaderHangBeacon(subdomain, fetchState);
25626
+ }, [isEditMode, subdomain, fetchState]);
25627
+ (0, import_react21.useEffect)(() => {
25628
+ if (isEditMode || !subdomain) return;
25629
+ const onError = (event) => {
25630
+ reportOutageBeacon({
25631
+ subdomain,
25632
+ reason: "page_error",
25633
+ detail: event.message || "window error"
25634
+ });
25635
+ };
25636
+ const onRejection = (event) => {
25637
+ const reason = event.reason;
25638
+ const detail = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : "unhandledrejection";
25639
+ reportOutageBeacon({ subdomain, reason: "page_error", detail });
25640
+ };
25641
+ window.addEventListener("error", onError);
25642
+ window.addEventListener("unhandledrejection", onRejection);
25643
+ return () => {
25644
+ window.removeEventListener("error", onError);
25645
+ window.removeEventListener("unhandledrejection", onRejection);
25646
+ };
25647
+ }, [isEditMode, subdomain]);
25408
25648
  const activeElRef = (0, import_react21.useRef)(null);
25409
25649
  const pointerHeldRef = (0, import_react21.useRef)(false);
25410
25650
  const selectedElRef = (0, import_react21.useRef)(null);
@@ -25826,21 +26066,6 @@ function OhhwellsBridge() {
25826
26066
  const [isItemDragging, setIsItemDragging] = (0, import_react21.useState)(false);
25827
26067
  const [isFooterFrameSelection, setIsFooterFrameSelection] = (0, import_react21.useState)(false);
25828
26068
  isFooterFrameSelectionRef.current = isFooterFrameSelection;
25829
- const [floatingPanel, setFloatingPanel] = (0, import_react21.useState)(null);
25830
- const floatingPanelOpenRef = (0, import_react21.useRef)(false);
25831
- floatingPanelOpenRef.current = floatingPanel !== null;
25832
- const floatingPanelRef = (0, import_react21.useRef)(null);
25833
- floatingPanelRef.current = floatingPanel;
25834
- const [floatingPanelPos, setFloatingPanelPos] = (0, import_react21.useState)(null);
25835
- const buttonCollectionDragRef = (0, import_react21.useRef)(null);
25836
- const buttonDropInsertIndexRef = (0, import_react21.useRef)(null);
25837
- const [buttonDropSlots, setButtonDropSlots] = (0, import_react21.useState)([]);
25838
- const [activeButtonDropIndex, setActiveButtonDropIndex] = (0, import_react21.useState)(null);
25839
- const buttonPointerDragRef = (0, import_react21.useRef)(null);
25840
- const [buttonStyleTick, setButtonStyleTick] = (0, import_react21.useState)(0);
25841
- const [logoSizeDraft, setLogoSizeDraft] = (0, import_react21.useState)(null);
25842
- const [editorViewport, setEditorViewport] = (0, import_react21.useState)("desktop");
25843
- const [parentScrollSnap, setParentScrollSnap] = (0, import_react21.useState)(null);
25844
26069
  const [navDropdownPreviewOpen, setNavDropdownPreviewOpen] = (0, import_react21.useState)(null);
25845
26070
  const [footerHeadingVisible, setFooterHeadingVisible] = (0, import_react21.useState)(null);
25846
26071
  const footerDragRef = (0, import_react21.useRef)(null);
@@ -25866,7 +26091,23 @@ function OhhwellsBridge() {
25866
26091
  const stylesRef = (0, import_react21.useRef)("");
25867
26092
  const appliedDrawerRef = (0, import_react21.useRef)(null);
25868
26093
  const pendingDeleteUndoRef = (0, import_react21.useRef)(null);
26094
+ const [floatingPanel, setFloatingPanel] = (0, import_react21.useState)(null);
26095
+ const floatingPanelOpenRef = (0, import_react21.useRef)(false);
26096
+ const floatingPanelRef = (0, import_react21.useRef)(null);
26097
+ floatingPanelRef.current = floatingPanel;
26098
+ const setFloatingPanelRef = (0, import_react21.useRef)(setFloatingPanel);
26099
+ const [floatingPanelPos, setFloatingPanelPos] = (0, import_react21.useState)(null);
26100
+ const buttonCollectionDragRef = (0, import_react21.useRef)(null);
26101
+ const buttonDropInsertIndexRef = (0, import_react21.useRef)(null);
26102
+ const [buttonDropSlots, setButtonDropSlots] = (0, import_react21.useState)([]);
26103
+ const [activeButtonDropIndex, setActiveButtonDropIndex] = (0, import_react21.useState)(null);
26104
+ const buttonPointerDragRef = (0, import_react21.useRef)(null);
26105
+ const [buttonStyleTick, setButtonStyleTick] = (0, import_react21.useState)(0);
26106
+ const [logoSizeDraft, setLogoSizeDraft] = (0, import_react21.useState)(null);
26107
+ const [editorViewport, setEditorViewport] = (0, import_react21.useState)("desktop");
26108
+ const [parentScrollSnap, setParentScrollSnap] = (0, import_react21.useState)(null);
25869
26109
  const [sitePages, setSitePages] = (0, import_react21.useState)([]);
26110
+ const collectionItemsRef = (0, import_react21.useRef)(/* @__PURE__ */ new Map());
25870
26111
  const [sectionsByPath, setSectionsByPath] = (0, import_react21.useState)({});
25871
26112
  const sectionsPrefetchGenRef = (0, import_react21.useRef)(0);
25872
26113
  const setLinkPopoverRef = (0, import_react21.useRef)(setLinkPopover);
@@ -25874,7 +26115,18 @@ function OhhwellsBridge() {
25874
26115
  const linkPopoverOpenRef = (0, import_react21.useRef)(false);
25875
26116
  const linkPopoverGraceUntilRef = (0, import_react21.useRef)(0);
25876
26117
  setLinkPopoverRef.current = setLinkPopover;
26118
+ setFloatingPanelRef.current = setFloatingPanel;
25877
26119
  linkPopoverSessionRef.current = linkPopover;
26120
+ floatingPanelOpenRef.current = Boolean(floatingPanel);
26121
+ (0, import_react21.useEffect)(() => {
26122
+ const syncViewport = () => {
26123
+ const next = window.innerWidth <= 480 ? "mobile" : "desktop";
26124
+ setEditorViewport((prev) => prev === next ? prev : next);
26125
+ };
26126
+ syncViewport();
26127
+ window.addEventListener("resize", syncViewport);
26128
+ return () => window.removeEventListener("resize", syncViewport);
26129
+ }, []);
25878
26130
  (0, import_react21.useEffect)(() => {
25879
26131
  setSectionOrderPersistHandler((entries) => {
25880
26132
  const currentPath = window.location.pathname;
@@ -26010,10 +26262,12 @@ function OhhwellsBridge() {
26010
26262
  const onSitePages = (e) => {
26011
26263
  if (e.data?.type !== "ow:site-pages" || !Array.isArray(e.data.pages)) return;
26012
26264
  if (useFixtures) return;
26013
- const mapped = e.data.pages.map((p) => ({
26265
+ const incoming = e.data.pages;
26266
+ const mapped = incoming.map((p) => ({
26014
26267
  path: p.path,
26015
26268
  title: p.title
26016
26269
  }));
26270
+ collectionItemsRef.current = indexCollectionItems(incoming);
26017
26271
  setSitePages(mapped);
26018
26272
  runSectionsPrefetchRef.current(mapped);
26019
26273
  };
@@ -27569,6 +27823,7 @@ function OhhwellsBridge() {
27569
27823
  applyAiFooterFromContent(content);
27570
27824
  if (typeof content[STYLE_STORE_KEY] === "string") {
27571
27825
  stylesRef.current = content[STYLE_STORE_KEY];
27826
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
27572
27827
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
27573
27828
  }
27574
27829
  applyBrandChrome(content);
@@ -27579,12 +27834,12 @@ function OhhwellsBridge() {
27579
27834
  if (key === "__ohw_sections") continue;
27580
27835
  if (key === AI_SECTIONS_KEY) continue;
27581
27836
  if (key === AI_FOOTER_KEY) continue;
27837
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
27838
+ if (LOGO_IMAGE_KEYS.includes(key)) continue;
27582
27839
  if (key === BRAND_KIT_KEY) continue;
27583
27840
  if (key === STYLE_STORE_KEY) continue;
27584
27841
  if (BRAND_CHROME_KEYS.has(key)) continue;
27585
- if (key === LOGO_PLACEHOLDER_KEY) continue;
27586
27842
  if (key === CUSTOM_CODE_AFTER_HEAD_KEY || key === CUSTOM_CODE_BEFORE_HEAD_KEY) continue;
27587
- if (LOGO_IMAGE_KEYS.includes(key)) continue;
27588
27843
  if (typeof content[AI_FOOTER_KEY] === "string" && content[AI_FOOTER_KEY] && isAiFooterOwnedContentKey(key)) {
27589
27844
  continue;
27590
27845
  }
@@ -27661,7 +27916,17 @@ function OhhwellsBridge() {
27661
27916
  const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
27662
27917
  const initialPath = pathname;
27663
27918
  fetchedContentPaths.add(`${subdomain}::${initialPath}`);
27664
- fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(initialPath)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
27919
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(initialPath)}`, { cache: "no-store" }).then((r2) => {
27920
+ if (!r2.ok) {
27921
+ reportOutageBeacon({
27922
+ subdomain,
27923
+ reason: "content_fetch_failed",
27924
+ detail: `HTTP ${r2.status}`
27925
+ });
27926
+ return null;
27927
+ }
27928
+ return r2.json();
27929
+ }).then((data) => {
27665
27930
  if (cancelled) return;
27666
27931
  const content = data?.content ?? {};
27667
27932
  const branding = Boolean(data?.showBranding);
@@ -27670,7 +27935,12 @@ function OhhwellsBridge() {
27670
27935
  recordOverriddenPaths(subdomain, data?.overriddenPaths);
27671
27936
  setShowBranding(branding);
27672
27937
  return applyContent(content);
27673
- }).catch(() => {
27938
+ }).catch((err) => {
27939
+ reportOutageBeacon({
27940
+ subdomain,
27941
+ reason: "content_fetch_failed",
27942
+ detail: err instanceof Error ? err.message : "fetch failed"
27943
+ });
27674
27944
  }).finally(() => {
27675
27945
  if (!cancelled) setFetchState("done");
27676
27946
  });
@@ -27800,17 +28070,18 @@ function OhhwellsBridge() {
27800
28070
  }
27801
28071
  applyAiFooterFromContent(content);
27802
28072
  if (typeof content[STYLE_STORE_KEY] === "string") {
28073
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
27803
28074
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
27804
28075
  }
27805
28076
  for (const [key, val] of Object.entries(content)) {
27806
28077
  if (key === "__ohw_sections") continue;
27807
28078
  if (key === AI_SECTIONS_KEY) continue;
27808
28079
  if (key === AI_FOOTER_KEY) continue;
28080
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
28081
+ if (LOGO_IMAGE_KEYS.includes(key)) continue;
27809
28082
  if (key === BRAND_KIT_KEY) continue;
27810
28083
  if (key === STYLE_STORE_KEY) continue;
27811
28084
  if (BRAND_CHROME_KEYS.has(key)) continue;
27812
- if (key === LOGO_PLACEHOLDER_KEY) continue;
27813
- if (LOGO_IMAGE_KEYS.includes(key)) continue;
27814
28085
  if (typeof content[AI_FOOTER_KEY] === "string" && content[AI_FOOTER_KEY] && isAiFooterOwnedContentKey(key)) {
27815
28086
  continue;
27816
28087
  }
@@ -28077,26 +28348,11 @@ function OhhwellsBridge() {
28077
28348
  const t2 = setTimeout(measure, 500);
28078
28349
  const ro = new ResizeObserver(schedule);
28079
28350
  ro.observe(document.body);
28080
- let lastWidth = window.innerWidth;
28081
- let resizeTimers = [];
28082
- const clearResizeTimers = () => {
28083
- resizeTimers.forEach(clearTimeout);
28084
- resizeTimers = [];
28085
- };
28086
- const handleResize = () => {
28087
- if (window.innerWidth === lastWidth) return;
28088
- lastWidth = window.innerWidth;
28089
- clearResizeTimers();
28090
- resizeTimers = HEIGHT_SETTLE_DELAYS.map((delay) => setTimeout(measure, delay));
28091
- };
28092
- window.addEventListener("resize", handleResize);
28093
28351
  return () => {
28094
28352
  clearTimeout(t1);
28095
28353
  clearTimeout(t2);
28096
28354
  if (raf != null) cancelAnimationFrame(raf);
28097
28355
  ro.disconnect();
28098
- clearResizeTimers();
28099
- window.removeEventListener("resize", handleResize);
28100
28356
  };
28101
28357
  }, [pathname, isEditMode, postToParent2]);
28102
28358
  (0, import_react21.useEffect)(() => {
@@ -28393,6 +28649,13 @@ function OhhwellsBridge() {
28393
28649
  if (target.closest("[data-ohw-state-toggle]")) return;
28394
28650
  if (target.closest("[data-ohw-max-badge]")) return;
28395
28651
  if (isInsideLinkEditor(target)) return;
28652
+ if (isInsideFloatingPanel(target)) return;
28653
+ if (target.closest("[data-ohw-form-toolbar]")) return;
28654
+ if (target.closest(
28655
+ '[data-ohw-field-toolbar], [data-ohw-field-type-picker], [data-radix-popper-content-wrapper], [role="menu"], [data-slot="dropdown-menu-content"]'
28656
+ )) {
28657
+ return;
28658
+ }
28396
28659
  if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
28397
28660
  clearMediaSelectionRef.current();
28398
28661
  }
@@ -28416,11 +28679,15 @@ function OhhwellsBridge() {
28416
28679
  return;
28417
28680
  }
28418
28681
  }
28419
- if (isInsideFloatingPanel(target)) return;
28420
- if (target.closest("[data-ohw-form-toolbar]")) return;
28421
- if (target.closest(
28422
- '[data-ohw-field-toolbar], [data-ohw-field-type-picker], [data-radix-popper-content-wrapper], [role="menu"], [data-slot="dropdown-menu-content"]'
28423
- )) {
28682
+ const collectionItem = matchCollectionItemClick(target, collectionItemsRef.current);
28683
+ if (collectionItem) {
28684
+ e.preventDefault();
28685
+ e.stopPropagation();
28686
+ postToParentRef.current({
28687
+ type: "ow:collection-item-selected",
28688
+ path: collectionItem.path,
28689
+ collection: collectionItem.collection
28690
+ });
28424
28691
  return;
28425
28692
  }
28426
28693
  {
@@ -28576,9 +28843,13 @@ function OhhwellsBridge() {
28576
28843
  if (isIconEditable(editable) && !getSocialItem(editable)) {
28577
28844
  e.preventDefault();
28578
28845
  e.stopPropagation();
28579
- selectIconElementRef.current(editable);
28580
28846
  const aiIcon = editable.hasAttribute("data-ai-icon");
28581
28847
  const featureIcon = aiIcon && /\.features\.\d+\.icon$/.test(editable.dataset.ohwKey ?? "");
28848
+ if (aiIcon) {
28849
+ aiSectionApiRef.current?.selectFromElement(editable);
28850
+ } else {
28851
+ selectIconElementRef.current(editable);
28852
+ }
28582
28853
  postToParentRef.current({
28583
28854
  type: "ow:icon-pick",
28584
28855
  key: editable.dataset.ohwKey ?? "",
@@ -28806,6 +29077,9 @@ function OhhwellsBridge() {
28806
29077
  setHoveredItemRect(null);
28807
29078
  hoveredNavContainerRef.current = null;
28808
29079
  setHoveredNavContainerRect(null);
29080
+ siblingHintElRef.current = null;
29081
+ setSiblingHintRect(null);
29082
+ setSiblingHintRects([]);
28809
29083
  return;
28810
29084
  }
28811
29085
  {
@@ -28926,7 +29200,6 @@ function OhhwellsBridge() {
28926
29200
  hoveredNavContainerRef.current = null;
28927
29201
  setHoveredNavContainerRect(null);
28928
29202
  hoveredItemElRef.current = editable;
28929
- setHoveredItemRect(isSelectedForHoverRef.current(editable) ? null : editable.getBoundingClientRect());
28930
29203
  }
28931
29204
  }
28932
29205
  }
@@ -29636,7 +29909,11 @@ function OhhwellsBridge() {
29636
29909
  };
29637
29910
  const handleMouseMove = (e) => {
29638
29911
  const { clientX, clientY } = e;
29639
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
29912
+ if (pointOwnedByFloatingPanel(clientX, clientY)) {
29913
+ dismissImageHover();
29914
+ clearImageHover();
29915
+ return;
29916
+ }
29640
29917
  if (isNavDrawerOpen() && isPointOutsideOpenDrawer(clientX, clientY)) {
29641
29918
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
29642
29919
  formHoverElRef.current = null;
@@ -29665,7 +29942,12 @@ function OhhwellsBridge() {
29665
29942
  setHoveredItemRect(null);
29666
29943
  hoveredNavContainerRef.current = null;
29667
29944
  setHoveredNavContainerRect(null);
29945
+ siblingHintElRef.current = null;
29946
+ setSiblingHintRect(null);
29947
+ setSiblingHintRects([]);
29668
29948
  dismissImageHover();
29949
+ clearImageHover();
29950
+ setSectionGap(null);
29669
29951
  return;
29670
29952
  }
29671
29953
  if (probeSocialsRowAt(clientX, clientY)) return;
@@ -29677,7 +29959,11 @@ function OhhwellsBridge() {
29677
29959
  if (e.data?.type !== "ow:pointer-sync") return;
29678
29960
  const { clientX, clientY } = e.data;
29679
29961
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
29680
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
29962
+ if (pointOwnedByFloatingPanel(clientX, clientY)) {
29963
+ dismissImageHover();
29964
+ clearImageHover();
29965
+ return;
29966
+ }
29681
29967
  if (isNavDrawerOpen() && isPointOutsideOpenDrawer(clientX, clientY)) return;
29682
29968
  if (probeSocialsRowAt(clientX, clientY)) return;
29683
29969
  probeSectionGapAt(clientX, clientY);
@@ -30057,6 +30343,7 @@ function OhhwellsBridge() {
30057
30343
  applyAiFooterFromContent(content);
30058
30344
  if (typeof content[STYLE_STORE_KEY] === "string") {
30059
30345
  stylesRef.current = content[STYLE_STORE_KEY];
30346
+ document.querySelectorAll('[data-ohw-editable="form"]').forEach((f) => markFormFields(f));
30060
30347
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
30061
30348
  }
30062
30349
  applyBrandChrome(content);
@@ -30071,12 +30358,12 @@ function OhhwellsBridge() {
30071
30358
  }
30072
30359
  if (key === AI_SECTIONS_KEY) continue;
30073
30360
  if (key === AI_FOOTER_KEY) continue;
30361
+ if (key === LOGO_PLACEHOLDER_KEY) continue;
30362
+ if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
30074
30363
  if (key === BRAND_KIT_KEY) continue;
30075
30364
  if (key === STYLE_STORE_KEY) continue;
30076
30365
  if (BRAND_CHROME_KEYS.has(key)) continue;
30077
- if (key === LOGO_PLACEHOLDER_KEY) continue;
30078
30366
  if (key === CUSTOM_CODE_AFTER_HEAD_KEY || key === CUSTOM_CODE_BEFORE_HEAD_KEY) continue;
30079
- if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
30080
30367
  if (typeof content[AI_FOOTER_KEY] === "string" && content[AI_FOOTER_KEY] && isAiFooterOwnedContentKey(key)) {
30081
30368
  continue;
30082
30369
  }
@@ -30712,12 +30999,24 @@ function OhhwellsBridge() {
30712
30999
  closeLinkPopoverRef.current();
30713
31000
  return;
30714
31001
  }
31002
+ if (floatingPanelOpenRef.current) {
31003
+ setFloatingPanelRef.current(null);
31004
+ deselectRef.current();
31005
+ deactivateRef.current();
31006
+ return;
31007
+ }
30715
31008
  deselectRef.current();
30716
31009
  deactivateRef.current();
30717
31010
  clearMediaSelectionRef.current();
30718
31011
  clearIconSelectionRef.current();
30719
31012
  };
30720
31013
  window.addEventListener("message", handleDeactivate);
31014
+ const handleSelectNav = (e) => {
31015
+ if (e.data?.type !== "ow:select-nav") return;
31016
+ const navContainer = findSelectableNavContainer();
31017
+ if (navContainer) selectFrameRef.current(navContainer);
31018
+ };
31019
+ window.addEventListener("message", handleSelectNav);
30721
31020
  const handleToastAction = (e) => {
30722
31021
  if (e.data?.type !== "ow:toast-action") return;
30723
31022
  const actionId = typeof e.data.actionId === "string" ? e.data.actionId : null;
@@ -31534,10 +31833,11 @@ function OhhwellsBridge() {
31534
31833
  window.removeEventListener("message", handleDeleteSection);
31535
31834
  window.removeEventListener("message", handleDuplicateSection);
31536
31835
  window.removeEventListener("message", handleDeactivate);
31537
- document.documentElement.removeAttribute("data-ohw-panel-dragging");
31836
+ window.removeEventListener("message", handleSelectNav);
31538
31837
  window.removeEventListener("message", handleToastAction);
31539
31838
  window.removeEventListener("message", handleFormCount);
31540
31839
  window.removeEventListener("message", handleUiEscape);
31840
+ document.documentElement.removeAttribute("data-ohw-panel-dragging");
31541
31841
  autoSaveTimers.current.forEach(clearTimeout);
31542
31842
  autoSaveTimers.current.clear();
31543
31843
  if (imageUnhoverTimerRef.current) clearTimeout(imageUnhoverTimerRef.current);
@@ -31831,7 +32131,7 @@ function OhhwellsBridge() {
31831
32131
  postToParent2({
31832
32132
  type: "ow:ready",
31833
32133
  version: "1",
31834
- bridgeVersion: "0.1.152",
32134
+ bridgeVersion: "0.1.153-next.415",
31835
32135
  path: pathname,
31836
32136
  nodes: collectEditableNodes(editContentRef.current),
31837
32137
  sections
@@ -33057,6 +33357,7 @@ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey
33057
33357
  foldAlignIntoTrees,
33058
33358
  getEditModeInitialState,
33059
33359
  hasMountedContentNodes,
33360
+ indexCollectionItems,
33060
33361
  insertFooterHours,
33061
33362
  isEditSessionActive,
33062
33363
  isPlainLeftClick,
@@ -33064,6 +33365,7 @@ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey
33064
33365
  isRenderableTree,
33065
33366
  isStickySearchParam,
33066
33367
  isValidUrl,
33368
+ matchCollectionItemClick,
33067
33369
  nextViableSlide,
33068
33370
  parseAiFooterState,
33069
33371
  parseAiSectionsState,