@ohhwells/bridge 0.1.152-next.414 → 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,
@@ -11740,6 +11742,172 @@ function useLinkHrefGuardian(...deps) {
11740
11742
  }, deps);
11741
11743
  }
11742
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
+
11743
11911
  // src/lib/sticky-search-params.ts
11744
11912
  var STICKY_EXACT = /* @__PURE__ */ new Set([
11745
11913
  // Site identity. Required in local dev and in the canvas editor's iframe, where there is no
@@ -17089,7 +17257,7 @@ function apiBase() {
17089
17257
  function bridgeVersion() {
17090
17258
  try {
17091
17259
  if (true) {
17092
- return "0.1.152-next.414";
17260
+ return "0.1.153-next.415";
17093
17261
  }
17094
17262
  } catch {
17095
17263
  }
@@ -17103,7 +17271,7 @@ function stampBridgeVersionOnDocument() {
17103
17271
  return;
17104
17272
  }
17105
17273
  try {
17106
- const v = true ? "0.1.152-next.414" : "";
17274
+ const v = true ? "0.1.153-next.415" : "";
17107
17275
  if (v) {
17108
17276
  document.documentElement.setAttribute("data-ohw-bridge-version", v);
17109
17277
  window.__OHW_BRIDGE_VERSION__ = v;
@@ -20888,141 +21056,6 @@ function isEditSessionActive() {
20888
21056
  return new URLSearchParams(q).get("mode") === "edit";
20889
21057
  }
20890
21058
 
20891
- // src/ui/link-modal/linkModal.utils.ts
20892
- function parseTarget(target) {
20893
- if (!target) return { pageRoute: "", sectionId: null };
20894
- const hashIndex = target.indexOf("#");
20895
- if (hashIndex === -1) return { pageRoute: target, sectionId: null };
20896
- return {
20897
- pageRoute: target.slice(0, hashIndex),
20898
- sectionId: target.slice(hashIndex + 1) || null
20899
- };
20900
- }
20901
- function normalizePath(path) {
20902
- const trimmed = path.trim();
20903
- if (!trimmed) return "/";
20904
- try {
20905
- if (/^https?:\/\//i.test(trimmed)) {
20906
- return new URL(trimmed).pathname || "/";
20907
- }
20908
- } catch {
20909
- }
20910
- const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
20911
- if (withSlash.length > 1 && withSlash.endsWith("/")) return withSlash.slice(0, -1);
20912
- return withSlash;
20913
- }
20914
- function findPageByPath(pages, path) {
20915
- const normalized = normalizePath(path);
20916
- return pages.find((p) => normalizePath(p.path) === normalized);
20917
- }
20918
- function inferPageTitleFromPath(path) {
20919
- const normalized = normalizePath(path);
20920
- if (normalized === "/") return "Home";
20921
- const segment = normalized.split("/").filter(Boolean).pop() ?? "";
20922
- if (!segment) return "Home";
20923
- return segment.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
20924
- }
20925
- function isInternalPath(route) {
20926
- const trimmed = route.trim();
20927
- if (!trimmed) return false;
20928
- if (/^https?:\/\//i.test(trimmed)) return false;
20929
- if (trimmed.startsWith("/")) return true;
20930
- if (trimmed.includes(" ") || trimmed.includes(".")) return false;
20931
- return true;
20932
- }
20933
- function resolvePage(route, pages) {
20934
- const found = findPageByPath(pages, route);
20935
- if (found) return { path: found.path, title: found.title };
20936
- return { path: normalizePath(route), title: inferPageTitleFromPath(route) };
20937
- }
20938
- function getSectionsForPath(sectionsByPath, pageRoute) {
20939
- if (!sectionsByPath) return [];
20940
- const normalized = normalizePath(pageRoute);
20941
- return sectionsByPath[normalized] ?? sectionsByPath[pageRoute] ?? [];
20942
- }
20943
- function buildTarget(page, section) {
20944
- if (section) return `${page.path}#${section.id}`;
20945
- return page.path;
20946
- }
20947
- function isValidUrl(urlString) {
20948
- try {
20949
- const url = new URL(urlString);
20950
- if (url.protocol !== "http:" && url.protocol !== "https:") return false;
20951
- const hostname = url.hostname;
20952
- if (!hostname || hostname.length === 0) return false;
20953
- if (hostname.includes(" ")) return false;
20954
- if (hostname === "localhost") return true;
20955
- if (!hostname.includes(".")) return false;
20956
- if (!/[a-z]/i.test(hostname)) return false;
20957
- return true;
20958
- } catch {
20959
- return false;
20960
- }
20961
- }
20962
- function normalizeUrl(value) {
20963
- const trimmed = value.trim();
20964
- if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
20965
- return `https://${trimmed}`;
20966
- }
20967
- function validateUrlInput(value, pages, allPages) {
20968
- const trimmed = value.trim();
20969
- if (!trimmed) return "";
20970
- const exactPage = allPages.find((p) => p.title.toLowerCase() === trimmed.toLowerCase());
20971
- if (exactPage) return "";
20972
- const hasPrefixMatch = pages.some((p) => p.title.toLowerCase().startsWith(trimmed.toLowerCase()));
20973
- if (hasPrefixMatch) {
20974
- return "Select a page from the list or enter a valid URL";
20975
- }
20976
- const hasScheme = /^https?:\/\//i.test(trimmed);
20977
- if (!hasScheme && !trimmed.includes(".")) {
20978
- return "Please enter a valid URL (e.g., https://example.com)";
20979
- }
20980
- if (!isValidUrl(normalizeUrl(trimmed))) {
20981
- return "Please enter a valid URL (e.g., https://example.com)";
20982
- }
20983
- return "";
20984
- }
20985
- function getEditModeInitialState(target, pages, sections) {
20986
- const empty = {
20987
- searchValue: "",
20988
- selectedPage: null,
20989
- selectedSection: null,
20990
- step: "input"
20991
- };
20992
- if (!target) return empty;
20993
- if (isValidUrl(normalizeUrl(target))) {
20994
- return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
20995
- }
20996
- const { pageRoute, sectionId } = parseTarget(target);
20997
- if (sectionId) {
20998
- if (isInternalPath(pageRoute)) {
20999
- const page = resolvePage(pageRoute, pages);
21000
- const sec = sections.find((s) => s.id === sectionId);
21001
- return {
21002
- searchValue: page.title,
21003
- selectedPage: page,
21004
- selectedSection: sec ? { id: sec.id, label: sec.label } : null,
21005
- step: "input"
21006
- };
21007
- }
21008
- return { searchValue: pageRoute || target, selectedPage: null, selectedSection: null, step: "input" };
21009
- }
21010
- if (isInternalPath(pageRoute)) {
21011
- const page = resolvePage(pageRoute, pages);
21012
- return {
21013
- searchValue: page.title,
21014
- selectedPage: page,
21015
- selectedSection: null,
21016
- step: "input"
21017
- };
21018
- }
21019
- return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
21020
- }
21021
- function filterAvailablePages(pages, existingTargets) {
21022
- const taken = new Set(existingTargets);
21023
- return pages.filter((p) => !taken.has(p.path));
21024
- }
21025
-
21026
21059
  // src/lib/prefetch-sections.ts
21027
21060
  function buildPageFetchUrl(path) {
21028
21061
  const normalized = normalizePath(path);
@@ -26074,6 +26107,7 @@ function OhhwellsBridge() {
26074
26107
  const [editorViewport, setEditorViewport] = (0, import_react21.useState)("desktop");
26075
26108
  const [parentScrollSnap, setParentScrollSnap] = (0, import_react21.useState)(null);
26076
26109
  const [sitePages, setSitePages] = (0, import_react21.useState)([]);
26110
+ const collectionItemsRef = (0, import_react21.useRef)(/* @__PURE__ */ new Map());
26077
26111
  const [sectionsByPath, setSectionsByPath] = (0, import_react21.useState)({});
26078
26112
  const sectionsPrefetchGenRef = (0, import_react21.useRef)(0);
26079
26113
  const setLinkPopoverRef = (0, import_react21.useRef)(setLinkPopover);
@@ -26228,10 +26262,12 @@ function OhhwellsBridge() {
26228
26262
  const onSitePages = (e) => {
26229
26263
  if (e.data?.type !== "ow:site-pages" || !Array.isArray(e.data.pages)) return;
26230
26264
  if (useFixtures) return;
26231
- const mapped = e.data.pages.map((p) => ({
26265
+ const incoming = e.data.pages;
26266
+ const mapped = incoming.map((p) => ({
26232
26267
  path: p.path,
26233
26268
  title: p.title
26234
26269
  }));
26270
+ collectionItemsRef.current = indexCollectionItems(incoming);
26235
26271
  setSitePages(mapped);
26236
26272
  runSectionsPrefetchRef.current(mapped);
26237
26273
  };
@@ -28643,6 +28679,17 @@ function OhhwellsBridge() {
28643
28679
  return;
28644
28680
  }
28645
28681
  }
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
+ });
28691
+ return;
28692
+ }
28646
28693
  {
28647
28694
  const formEl = getFormElement(target);
28648
28695
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
@@ -32084,7 +32131,7 @@ function OhhwellsBridge() {
32084
32131
  postToParent2({
32085
32132
  type: "ow:ready",
32086
32133
  version: "1",
32087
- bridgeVersion: "0.1.152-next.414",
32134
+ bridgeVersion: "0.1.153-next.415",
32088
32135
  path: pathname,
32089
32136
  nodes: collectEditableNodes(editContentRef.current),
32090
32137
  sections
@@ -33310,6 +33357,7 @@ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey
33310
33357
  foldAlignIntoTrees,
33311
33358
  getEditModeInitialState,
33312
33359
  hasMountedContentNodes,
33360
+ indexCollectionItems,
33313
33361
  insertFooterHours,
33314
33362
  isEditSessionActive,
33315
33363
  isPlainLeftClick,
@@ -33317,6 +33365,7 @@ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey
33317
33365
  isRenderableTree,
33318
33366
  isStickySearchParam,
33319
33367
  isValidUrl,
33368
+ matchCollectionItemClick,
33320
33369
  nextViableSlide,
33321
33370
  parseAiFooterState,
33322
33371
  parseAiSectionsState,