@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.d.cts CHANGED
@@ -926,6 +926,36 @@ declare function shouldFetchContentForPath(subdomain: string, path: string): boo
926
926
  /** Test seam: the cache is module state that would otherwise leak between cases. */
927
927
  declare function resetOverriddenPathsCache(): void;
928
928
 
929
+ /** One entry of the parent's `ow:site-pages` payload (title is unused here). */
930
+ type SitePageEntry = {
931
+ path: string;
932
+ kind?: string | null;
933
+ collection?: string | null;
934
+ };
935
+ type CollectionItemHit = {
936
+ /** The page path as the editor knows it, not as the anchor spelled it. */
937
+ path: string;
938
+ /** Route segment of the owning collection, e.g. `blog`. */
939
+ collection: string;
940
+ };
941
+ /** Normalized item path → the hit to post. Empty on a site with no collections. */
942
+ type CollectionItemIndex = Map<string, CollectionItemHit>;
943
+ declare function indexCollectionItems(pages: readonly SitePageEntry[]): CollectionItemIndex;
944
+ /**
945
+ * Resolve an edit-mode click to the collection item it belongs to, or null to leave the click
946
+ * alone.
947
+ *
948
+ * Only clicks on *editable text* are intercepted. Media inside a post card stays replaceable
949
+ * from the listing page, so this removes exactly the inline text editing the Post panel
950
+ * replaces and nothing else.
951
+ *
952
+ * A CTA link is left alone too. `professional-services` links each `/team/${slug}` from a
953
+ * `data-ohw-role="button"` "View profile →" anchor rather than wrapping the whole card, and that
954
+ * label is the listing page's own copy — editing it has nothing to do with the item it points at.
955
+ * The role is the site tagger's CTA marker (see `cta-button.ts`), so this holds on any template.
956
+ */
957
+ declare function matchCollectionItemClick(target: Element | null, index: CollectionItemIndex): CollectionItemHit | null;
958
+
929
959
  declare function isStickySearchParam(name: string): boolean;
930
960
  /**
931
961
  * Copy the sticky params of `currentSearch` onto `target`, and report whether anything changed.
@@ -970,4 +1000,4 @@ declare function useOhwCarousel(key: string, initial: CarouselSlide[]): {
970
1000
  bind: Record<string, string>;
971
1001
  };
972
1002
 
973
- export { AI_DEFAULT_BRAND, AI_FOOTER_KEY, AI_TREE_SCHEMA_VERSIONS, AI_TREE_TOKENS, type AiBlockNode, type AiBrandKit, type AiFooterBottomBar, type AiFooterColumn, type AiFooterLayoutTree, AiFooterRenderer, type AiFooterRendererProps, type AiLayoutTree, AiTreeRenderer, type AiTreeRendererProps, BRAND_TITLE_KEY, type CarouselSlide, CustomToolbar, CustomToolbarButton, CustomToolbarDivider, DragHandle, DropIndicator, type DropIndicatorProps, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, EmptySection, type EmptySectionProps, FOOTER_COLUMN_BLOCKS, FOOTER_COLUMN_SPANS, FOOTER_SOCIAL_PLATFORMS, ItemActionToolbar, type ItemActionToolbarProps, ItemInteractionLayer, type ItemInteractionLayerProps, type ItemInteractionState, LinkEditorPanel, type LinkModalMode, type LinkModalProps, type LinkPage, LinkPopover, type LinkSection, OHW_LOADER_PREHYDRATE_SCRIPT, OHW_LOADER_STYLE, OhhwellsBridge, OhwLoaderSpinner, OhwLoaderSurface, SITE_OG_IMAGE_KEY, SchedulingWidget, type SectionScaffold, type SiteLegalLinks, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, addFooterLinkColumn, appendFooterSocial, applyBrandChrome, applyStickySearchParams, buildDefaultFooterTree, buildTarget, dropIndicatorVariants, ensureFooterTree, filterAvailablePages, foldAlignIntoTrees, getEditModeInitialState, hasMountedContentNodes, insertFooterHours, isEditSessionActive, isPlainLeftClick, isRenderableFooterTree, isRenderableTree, isStickySearchParam, isValidUrl, nextViableSlide, parseAiFooterState, parseAiSectionsState, parseTarget, recordOverriddenPaths, removeFooterBlock, resetOverriddenPathsCache, resolveFooterCopyright, resolveStickyNavigation, serializeAiFooterState, shouldFetchContentForPath, stepViableSlide, stripTextAlignCss, toggleVariants, useOhwCarousel, validateUrlInput, viableSlides };
1003
+ export { AI_DEFAULT_BRAND, AI_FOOTER_KEY, AI_TREE_SCHEMA_VERSIONS, AI_TREE_TOKENS, type AiBlockNode, type AiBrandKit, type AiFooterBottomBar, type AiFooterColumn, type AiFooterLayoutTree, AiFooterRenderer, type AiFooterRendererProps, type AiLayoutTree, AiTreeRenderer, type AiTreeRendererProps, BRAND_TITLE_KEY, type CarouselSlide, type CollectionItemHit, type CollectionItemIndex, CustomToolbar, CustomToolbarButton, CustomToolbarDivider, DragHandle, DropIndicator, type DropIndicatorProps, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, EmptySection, type EmptySectionProps, FOOTER_COLUMN_BLOCKS, FOOTER_COLUMN_SPANS, FOOTER_SOCIAL_PLATFORMS, ItemActionToolbar, type ItemActionToolbarProps, ItemInteractionLayer, type ItemInteractionLayerProps, type ItemInteractionState, LinkEditorPanel, type LinkModalMode, type LinkModalProps, type LinkPage, LinkPopover, type LinkSection, OHW_LOADER_PREHYDRATE_SCRIPT, OHW_LOADER_STYLE, OhhwellsBridge, OhwLoaderSpinner, OhwLoaderSurface, SITE_OG_IMAGE_KEY, SchedulingWidget, type SectionScaffold, type SiteLegalLinks, type SitePageEntry, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, addFooterLinkColumn, appendFooterSocial, applyBrandChrome, applyStickySearchParams, buildDefaultFooterTree, buildTarget, dropIndicatorVariants, ensureFooterTree, filterAvailablePages, foldAlignIntoTrees, getEditModeInitialState, hasMountedContentNodes, indexCollectionItems, insertFooterHours, isEditSessionActive, isPlainLeftClick, isRenderableFooterTree, isRenderableTree, isStickySearchParam, isValidUrl, matchCollectionItemClick, nextViableSlide, parseAiFooterState, parseAiSectionsState, parseTarget, recordOverriddenPaths, removeFooterBlock, resetOverriddenPathsCache, resolveFooterCopyright, resolveStickyNavigation, serializeAiFooterState, shouldFetchContentForPath, stepViableSlide, stripTextAlignCss, toggleVariants, useOhwCarousel, validateUrlInput, viableSlides };
package/dist/index.d.ts CHANGED
@@ -926,6 +926,36 @@ declare function shouldFetchContentForPath(subdomain: string, path: string): boo
926
926
  /** Test seam: the cache is module state that would otherwise leak between cases. */
927
927
  declare function resetOverriddenPathsCache(): void;
928
928
 
929
+ /** One entry of the parent's `ow:site-pages` payload (title is unused here). */
930
+ type SitePageEntry = {
931
+ path: string;
932
+ kind?: string | null;
933
+ collection?: string | null;
934
+ };
935
+ type CollectionItemHit = {
936
+ /** The page path as the editor knows it, not as the anchor spelled it. */
937
+ path: string;
938
+ /** Route segment of the owning collection, e.g. `blog`. */
939
+ collection: string;
940
+ };
941
+ /** Normalized item path → the hit to post. Empty on a site with no collections. */
942
+ type CollectionItemIndex = Map<string, CollectionItemHit>;
943
+ declare function indexCollectionItems(pages: readonly SitePageEntry[]): CollectionItemIndex;
944
+ /**
945
+ * Resolve an edit-mode click to the collection item it belongs to, or null to leave the click
946
+ * alone.
947
+ *
948
+ * Only clicks on *editable text* are intercepted. Media inside a post card stays replaceable
949
+ * from the listing page, so this removes exactly the inline text editing the Post panel
950
+ * replaces and nothing else.
951
+ *
952
+ * A CTA link is left alone too. `professional-services` links each `/team/${slug}` from a
953
+ * `data-ohw-role="button"` "View profile →" anchor rather than wrapping the whole card, and that
954
+ * label is the listing page's own copy — editing it has nothing to do with the item it points at.
955
+ * The role is the site tagger's CTA marker (see `cta-button.ts`), so this holds on any template.
956
+ */
957
+ declare function matchCollectionItemClick(target: Element | null, index: CollectionItemIndex): CollectionItemHit | null;
958
+
929
959
  declare function isStickySearchParam(name: string): boolean;
930
960
  /**
931
961
  * Copy the sticky params of `currentSearch` onto `target`, and report whether anything changed.
@@ -970,4 +1000,4 @@ declare function useOhwCarousel(key: string, initial: CarouselSlide[]): {
970
1000
  bind: Record<string, string>;
971
1001
  };
972
1002
 
973
- export { AI_DEFAULT_BRAND, AI_FOOTER_KEY, AI_TREE_SCHEMA_VERSIONS, AI_TREE_TOKENS, type AiBlockNode, type AiBrandKit, type AiFooterBottomBar, type AiFooterColumn, type AiFooterLayoutTree, AiFooterRenderer, type AiFooterRendererProps, type AiLayoutTree, AiTreeRenderer, type AiTreeRendererProps, BRAND_TITLE_KEY, type CarouselSlide, CustomToolbar, CustomToolbarButton, CustomToolbarDivider, DragHandle, DropIndicator, type DropIndicatorProps, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, EmptySection, type EmptySectionProps, FOOTER_COLUMN_BLOCKS, FOOTER_COLUMN_SPANS, FOOTER_SOCIAL_PLATFORMS, ItemActionToolbar, type ItemActionToolbarProps, ItemInteractionLayer, type ItemInteractionLayerProps, type ItemInteractionState, LinkEditorPanel, type LinkModalMode, type LinkModalProps, type LinkPage, LinkPopover, type LinkSection, OHW_LOADER_PREHYDRATE_SCRIPT, OHW_LOADER_STYLE, OhhwellsBridge, OhwLoaderSpinner, OhwLoaderSurface, SITE_OG_IMAGE_KEY, SchedulingWidget, type SectionScaffold, type SiteLegalLinks, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, addFooterLinkColumn, appendFooterSocial, applyBrandChrome, applyStickySearchParams, buildDefaultFooterTree, buildTarget, dropIndicatorVariants, ensureFooterTree, filterAvailablePages, foldAlignIntoTrees, getEditModeInitialState, hasMountedContentNodes, insertFooterHours, isEditSessionActive, isPlainLeftClick, isRenderableFooterTree, isRenderableTree, isStickySearchParam, isValidUrl, nextViableSlide, parseAiFooterState, parseAiSectionsState, parseTarget, recordOverriddenPaths, removeFooterBlock, resetOverriddenPathsCache, resolveFooterCopyright, resolveStickyNavigation, serializeAiFooterState, shouldFetchContentForPath, stepViableSlide, stripTextAlignCss, toggleVariants, useOhwCarousel, validateUrlInput, viableSlides };
1003
+ export { AI_DEFAULT_BRAND, AI_FOOTER_KEY, AI_TREE_SCHEMA_VERSIONS, AI_TREE_TOKENS, type AiBlockNode, type AiBrandKit, type AiFooterBottomBar, type AiFooterColumn, type AiFooterLayoutTree, AiFooterRenderer, type AiFooterRendererProps, type AiLayoutTree, AiTreeRenderer, type AiTreeRendererProps, BRAND_TITLE_KEY, type CarouselSlide, type CollectionItemHit, type CollectionItemIndex, CustomToolbar, CustomToolbarButton, CustomToolbarDivider, DragHandle, DropIndicator, type DropIndicatorProps, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, EmptySection, type EmptySectionProps, FOOTER_COLUMN_BLOCKS, FOOTER_COLUMN_SPANS, FOOTER_SOCIAL_PLATFORMS, ItemActionToolbar, type ItemActionToolbarProps, ItemInteractionLayer, type ItemInteractionLayerProps, type ItemInteractionState, LinkEditorPanel, type LinkModalMode, type LinkModalProps, type LinkPage, LinkPopover, type LinkSection, OHW_LOADER_PREHYDRATE_SCRIPT, OHW_LOADER_STYLE, OhhwellsBridge, OhwLoaderSpinner, OhwLoaderSurface, SITE_OG_IMAGE_KEY, SchedulingWidget, type SectionScaffold, type SiteLegalLinks, type SitePageEntry, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, addFooterLinkColumn, appendFooterSocial, applyBrandChrome, applyStickySearchParams, buildDefaultFooterTree, buildTarget, dropIndicatorVariants, ensureFooterTree, filterAvailablePages, foldAlignIntoTrees, getEditModeInitialState, hasMountedContentNodes, indexCollectionItems, insertFooterHours, isEditSessionActive, isPlainLeftClick, isRenderableFooterTree, isRenderableTree, isStickySearchParam, isValidUrl, matchCollectionItemClick, nextViableSlide, parseAiFooterState, parseAiSectionsState, parseTarget, recordOverriddenPaths, removeFooterBlock, resetOverriddenPathsCache, resolveFooterCopyright, resolveStickyNavigation, serializeAiFooterState, shouldFetchContentForPath, stepViableSlide, stripTextAlignCss, toggleVariants, useOhwCarousel, validateUrlInput, viableSlides };
package/dist/index.js CHANGED
@@ -11630,6 +11630,172 @@ function useLinkHrefGuardian(...deps) {
11630
11630
  }, deps);
11631
11631
  }
11632
11632
 
11633
+ // src/ui/link-modal/linkModal.utils.ts
11634
+ function parseTarget(target) {
11635
+ if (!target) return { pageRoute: "", sectionId: null };
11636
+ const hashIndex = target.indexOf("#");
11637
+ if (hashIndex === -1) return { pageRoute: target, sectionId: null };
11638
+ return {
11639
+ pageRoute: target.slice(0, hashIndex),
11640
+ sectionId: target.slice(hashIndex + 1) || null
11641
+ };
11642
+ }
11643
+ function normalizePath(path) {
11644
+ const trimmed = path.trim();
11645
+ if (!trimmed) return "/";
11646
+ try {
11647
+ if (/^https?:\/\//i.test(trimmed)) {
11648
+ return new URL(trimmed).pathname || "/";
11649
+ }
11650
+ } catch {
11651
+ }
11652
+ const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
11653
+ if (withSlash.length > 1 && withSlash.endsWith("/")) return withSlash.slice(0, -1);
11654
+ return withSlash;
11655
+ }
11656
+ function findPageByPath(pages, path) {
11657
+ const normalized = normalizePath(path);
11658
+ return pages.find((p) => normalizePath(p.path) === normalized);
11659
+ }
11660
+ function inferPageTitleFromPath(path) {
11661
+ const normalized = normalizePath(path);
11662
+ if (normalized === "/") return "Home";
11663
+ const segment = normalized.split("/").filter(Boolean).pop() ?? "";
11664
+ if (!segment) return "Home";
11665
+ return segment.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
11666
+ }
11667
+ function isInternalPath(route) {
11668
+ const trimmed = route.trim();
11669
+ if (!trimmed) return false;
11670
+ if (/^https?:\/\//i.test(trimmed)) return false;
11671
+ if (trimmed.startsWith("/")) return true;
11672
+ if (trimmed.includes(" ") || trimmed.includes(".")) return false;
11673
+ return true;
11674
+ }
11675
+ function resolvePage(route, pages) {
11676
+ const found = findPageByPath(pages, route);
11677
+ if (found) return { path: found.path, title: found.title };
11678
+ return { path: normalizePath(route), title: inferPageTitleFromPath(route) };
11679
+ }
11680
+ function getSectionsForPath(sectionsByPath, pageRoute) {
11681
+ if (!sectionsByPath) return [];
11682
+ const normalized = normalizePath(pageRoute);
11683
+ return sectionsByPath[normalized] ?? sectionsByPath[pageRoute] ?? [];
11684
+ }
11685
+ function buildTarget(page, section) {
11686
+ if (section) return `${page.path}#${section.id}`;
11687
+ return page.path;
11688
+ }
11689
+ function isValidUrl(urlString) {
11690
+ try {
11691
+ const url = new URL(urlString);
11692
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
11693
+ const hostname = url.hostname;
11694
+ if (!hostname || hostname.length === 0) return false;
11695
+ if (hostname.includes(" ")) return false;
11696
+ if (hostname === "localhost") return true;
11697
+ if (!hostname.includes(".")) return false;
11698
+ if (!/[a-z]/i.test(hostname)) return false;
11699
+ return true;
11700
+ } catch {
11701
+ return false;
11702
+ }
11703
+ }
11704
+ function normalizeUrl(value) {
11705
+ const trimmed = value.trim();
11706
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
11707
+ return `https://${trimmed}`;
11708
+ }
11709
+ function validateUrlInput(value, pages, allPages) {
11710
+ const trimmed = value.trim();
11711
+ if (!trimmed) return "";
11712
+ const exactPage = allPages.find((p) => p.title.toLowerCase() === trimmed.toLowerCase());
11713
+ if (exactPage) return "";
11714
+ const hasPrefixMatch = pages.some((p) => p.title.toLowerCase().startsWith(trimmed.toLowerCase()));
11715
+ if (hasPrefixMatch) {
11716
+ return "Select a page from the list or enter a valid URL";
11717
+ }
11718
+ const hasScheme = /^https?:\/\//i.test(trimmed);
11719
+ if (!hasScheme && !trimmed.includes(".")) {
11720
+ return "Please enter a valid URL (e.g., https://example.com)";
11721
+ }
11722
+ if (!isValidUrl(normalizeUrl(trimmed))) {
11723
+ return "Please enter a valid URL (e.g., https://example.com)";
11724
+ }
11725
+ return "";
11726
+ }
11727
+ function getEditModeInitialState(target, pages, sections) {
11728
+ const empty = {
11729
+ searchValue: "",
11730
+ selectedPage: null,
11731
+ selectedSection: null,
11732
+ step: "input"
11733
+ };
11734
+ if (!target) return empty;
11735
+ if (isValidUrl(normalizeUrl(target))) {
11736
+ return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
11737
+ }
11738
+ const { pageRoute, sectionId } = parseTarget(target);
11739
+ if (sectionId) {
11740
+ if (isInternalPath(pageRoute)) {
11741
+ const page = resolvePage(pageRoute, pages);
11742
+ const sec = sections.find((s) => s.id === sectionId);
11743
+ return {
11744
+ searchValue: page.title,
11745
+ selectedPage: page,
11746
+ selectedSection: sec ? { id: sec.id, label: sec.label } : null,
11747
+ step: "input"
11748
+ };
11749
+ }
11750
+ return { searchValue: pageRoute || target, selectedPage: null, selectedSection: null, step: "input" };
11751
+ }
11752
+ if (isInternalPath(pageRoute)) {
11753
+ const page = resolvePage(pageRoute, pages);
11754
+ return {
11755
+ searchValue: page.title,
11756
+ selectedPage: page,
11757
+ selectedSection: null,
11758
+ step: "input"
11759
+ };
11760
+ }
11761
+ return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
11762
+ }
11763
+ function filterAvailablePages(pages, existingTargets) {
11764
+ const taken = new Set(existingTargets);
11765
+ return pages.filter((p) => !taken.has(p.path));
11766
+ }
11767
+
11768
+ // src/lib/collection-item-click.ts
11769
+ function indexCollectionItems(pages) {
11770
+ const index = /* @__PURE__ */ new Map();
11771
+ for (const page of pages) {
11772
+ if (page.kind !== "COLLECTION_ITEM" || !page.collection || !page.path) continue;
11773
+ index.set(normalizePath(page.path), { path: page.path, collection: page.collection });
11774
+ }
11775
+ return index;
11776
+ }
11777
+ function pathOfHref(href) {
11778
+ const trimmed = href.trim();
11779
+ if (!trimmed || trimmed.startsWith("#")) return null;
11780
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed) && !/^https?:/i.test(trimmed)) return null;
11781
+ const withoutHash = trimmed.split("#")[0];
11782
+ const withoutQuery = withoutHash.split("?")[0];
11783
+ if (!withoutQuery) return null;
11784
+ return normalizePath(withoutQuery);
11785
+ }
11786
+ function matchCollectionItemClick(target, index) {
11787
+ if (!target || index.size === 0) return null;
11788
+ const text = target.closest('[data-ohw-editable="text"], [data-ohw-editable="plain"]');
11789
+ if (!text) return null;
11790
+ const anchor = text.closest("a");
11791
+ if (!anchor || anchor.closest(CTA_BUTTON_SELECTOR)) return null;
11792
+ const href = anchor.getAttribute("href");
11793
+ if (!href) return null;
11794
+ const path = pathOfHref(href);
11795
+ if (!path) return null;
11796
+ return index.get(path) ?? null;
11797
+ }
11798
+
11633
11799
  // src/lib/sticky-search-params.ts
11634
11800
  var STICKY_EXACT = /* @__PURE__ */ new Set([
11635
11801
  // Site identity. Required in local dev and in the canvas editor's iframe, where there is no
@@ -16979,7 +17145,7 @@ function apiBase() {
16979
17145
  function bridgeVersion() {
16980
17146
  try {
16981
17147
  if (true) {
16982
- return "0.1.152-next.414";
17148
+ return "0.1.153-next.415";
16983
17149
  }
16984
17150
  } catch {
16985
17151
  }
@@ -16993,7 +17159,7 @@ function stampBridgeVersionOnDocument() {
16993
17159
  return;
16994
17160
  }
16995
17161
  try {
16996
- const v = true ? "0.1.152-next.414" : "";
17162
+ const v = true ? "0.1.153-next.415" : "";
16997
17163
  if (v) {
16998
17164
  document.documentElement.setAttribute("data-ohw-bridge-version", v);
16999
17165
  window.__OHW_BRIDGE_VERSION__ = v;
@@ -20794,141 +20960,6 @@ function isEditSessionActive() {
20794
20960
  return new URLSearchParams(q).get("mode") === "edit";
20795
20961
  }
20796
20962
 
20797
- // src/ui/link-modal/linkModal.utils.ts
20798
- function parseTarget(target) {
20799
- if (!target) return { pageRoute: "", sectionId: null };
20800
- const hashIndex = target.indexOf("#");
20801
- if (hashIndex === -1) return { pageRoute: target, sectionId: null };
20802
- return {
20803
- pageRoute: target.slice(0, hashIndex),
20804
- sectionId: target.slice(hashIndex + 1) || null
20805
- };
20806
- }
20807
- function normalizePath(path) {
20808
- const trimmed = path.trim();
20809
- if (!trimmed) return "/";
20810
- try {
20811
- if (/^https?:\/\//i.test(trimmed)) {
20812
- return new URL(trimmed).pathname || "/";
20813
- }
20814
- } catch {
20815
- }
20816
- const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
20817
- if (withSlash.length > 1 && withSlash.endsWith("/")) return withSlash.slice(0, -1);
20818
- return withSlash;
20819
- }
20820
- function findPageByPath(pages, path) {
20821
- const normalized = normalizePath(path);
20822
- return pages.find((p) => normalizePath(p.path) === normalized);
20823
- }
20824
- function inferPageTitleFromPath(path) {
20825
- const normalized = normalizePath(path);
20826
- if (normalized === "/") return "Home";
20827
- const segment = normalized.split("/").filter(Boolean).pop() ?? "";
20828
- if (!segment) return "Home";
20829
- return segment.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
20830
- }
20831
- function isInternalPath(route) {
20832
- const trimmed = route.trim();
20833
- if (!trimmed) return false;
20834
- if (/^https?:\/\//i.test(trimmed)) return false;
20835
- if (trimmed.startsWith("/")) return true;
20836
- if (trimmed.includes(" ") || trimmed.includes(".")) return false;
20837
- return true;
20838
- }
20839
- function resolvePage(route, pages) {
20840
- const found = findPageByPath(pages, route);
20841
- if (found) return { path: found.path, title: found.title };
20842
- return { path: normalizePath(route), title: inferPageTitleFromPath(route) };
20843
- }
20844
- function getSectionsForPath(sectionsByPath, pageRoute) {
20845
- if (!sectionsByPath) return [];
20846
- const normalized = normalizePath(pageRoute);
20847
- return sectionsByPath[normalized] ?? sectionsByPath[pageRoute] ?? [];
20848
- }
20849
- function buildTarget(page, section) {
20850
- if (section) return `${page.path}#${section.id}`;
20851
- return page.path;
20852
- }
20853
- function isValidUrl(urlString) {
20854
- try {
20855
- const url = new URL(urlString);
20856
- if (url.protocol !== "http:" && url.protocol !== "https:") return false;
20857
- const hostname = url.hostname;
20858
- if (!hostname || hostname.length === 0) return false;
20859
- if (hostname.includes(" ")) return false;
20860
- if (hostname === "localhost") return true;
20861
- if (!hostname.includes(".")) return false;
20862
- if (!/[a-z]/i.test(hostname)) return false;
20863
- return true;
20864
- } catch {
20865
- return false;
20866
- }
20867
- }
20868
- function normalizeUrl(value) {
20869
- const trimmed = value.trim();
20870
- if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
20871
- return `https://${trimmed}`;
20872
- }
20873
- function validateUrlInput(value, pages, allPages) {
20874
- const trimmed = value.trim();
20875
- if (!trimmed) return "";
20876
- const exactPage = allPages.find((p) => p.title.toLowerCase() === trimmed.toLowerCase());
20877
- if (exactPage) return "";
20878
- const hasPrefixMatch = pages.some((p) => p.title.toLowerCase().startsWith(trimmed.toLowerCase()));
20879
- if (hasPrefixMatch) {
20880
- return "Select a page from the list or enter a valid URL";
20881
- }
20882
- const hasScheme = /^https?:\/\//i.test(trimmed);
20883
- if (!hasScheme && !trimmed.includes(".")) {
20884
- return "Please enter a valid URL (e.g., https://example.com)";
20885
- }
20886
- if (!isValidUrl(normalizeUrl(trimmed))) {
20887
- return "Please enter a valid URL (e.g., https://example.com)";
20888
- }
20889
- return "";
20890
- }
20891
- function getEditModeInitialState(target, pages, sections) {
20892
- const empty = {
20893
- searchValue: "",
20894
- selectedPage: null,
20895
- selectedSection: null,
20896
- step: "input"
20897
- };
20898
- if (!target) return empty;
20899
- if (isValidUrl(normalizeUrl(target))) {
20900
- return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
20901
- }
20902
- const { pageRoute, sectionId } = parseTarget(target);
20903
- if (sectionId) {
20904
- if (isInternalPath(pageRoute)) {
20905
- const page = resolvePage(pageRoute, pages);
20906
- const sec = sections.find((s) => s.id === sectionId);
20907
- return {
20908
- searchValue: page.title,
20909
- selectedPage: page,
20910
- selectedSection: sec ? { id: sec.id, label: sec.label } : null,
20911
- step: "input"
20912
- };
20913
- }
20914
- return { searchValue: pageRoute || target, selectedPage: null, selectedSection: null, step: "input" };
20915
- }
20916
- if (isInternalPath(pageRoute)) {
20917
- const page = resolvePage(pageRoute, pages);
20918
- return {
20919
- searchValue: page.title,
20920
- selectedPage: page,
20921
- selectedSection: null,
20922
- step: "input"
20923
- };
20924
- }
20925
- return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
20926
- }
20927
- function filterAvailablePages(pages, existingTargets) {
20928
- const taken = new Set(existingTargets);
20929
- return pages.filter((p) => !taken.has(p.path));
20930
- }
20931
-
20932
20963
  // src/lib/prefetch-sections.ts
20933
20964
  function buildPageFetchUrl(path) {
20934
20965
  const normalized = normalizePath(path);
@@ -25986,6 +26017,7 @@ function OhhwellsBridge() {
25986
26017
  const [editorViewport, setEditorViewport] = useState18("desktop");
25987
26018
  const [parentScrollSnap, setParentScrollSnap] = useState18(null);
25988
26019
  const [sitePages, setSitePages] = useState18([]);
26020
+ const collectionItemsRef = useRef14(/* @__PURE__ */ new Map());
25989
26021
  const [sectionsByPath, setSectionsByPath] = useState18({});
25990
26022
  const sectionsPrefetchGenRef = useRef14(0);
25991
26023
  const setLinkPopoverRef = useRef14(setLinkPopover);
@@ -26140,10 +26172,12 @@ function OhhwellsBridge() {
26140
26172
  const onSitePages = (e) => {
26141
26173
  if (e.data?.type !== "ow:site-pages" || !Array.isArray(e.data.pages)) return;
26142
26174
  if (useFixtures) return;
26143
- const mapped = e.data.pages.map((p) => ({
26175
+ const incoming = e.data.pages;
26176
+ const mapped = incoming.map((p) => ({
26144
26177
  path: p.path,
26145
26178
  title: p.title
26146
26179
  }));
26180
+ collectionItemsRef.current = indexCollectionItems(incoming);
26147
26181
  setSitePages(mapped);
26148
26182
  runSectionsPrefetchRef.current(mapped);
26149
26183
  };
@@ -28555,6 +28589,17 @@ function OhhwellsBridge() {
28555
28589
  return;
28556
28590
  }
28557
28591
  }
28592
+ const collectionItem = matchCollectionItemClick(target, collectionItemsRef.current);
28593
+ if (collectionItem) {
28594
+ e.preventDefault();
28595
+ e.stopPropagation();
28596
+ postToParentRef.current({
28597
+ type: "ow:collection-item-selected",
28598
+ path: collectionItem.path,
28599
+ collection: collectionItem.collection
28600
+ });
28601
+ return;
28602
+ }
28558
28603
  {
28559
28604
  const formEl = getFormElement(target);
28560
28605
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
@@ -31996,7 +32041,7 @@ function OhhwellsBridge() {
31996
32041
  postToParent2({
31997
32042
  type: "ow:ready",
31998
32043
  version: "1",
31999
- bridgeVersion: "0.1.152-next.414",
32044
+ bridgeVersion: "0.1.153-next.415",
32000
32045
  path: pathname,
32001
32046
  nodes: collectEditableNodes(editContentRef.current),
32002
32047
  sections
@@ -33221,6 +33266,7 @@ export {
33221
33266
  foldAlignIntoTrees,
33222
33267
  getEditModeInitialState,
33223
33268
  hasMountedContentNodes,
33269
+ indexCollectionItems,
33224
33270
  insertFooterHours,
33225
33271
  isEditSessionActive,
33226
33272
  isPlainLeftClick,
@@ -33228,6 +33274,7 @@ export {
33228
33274
  isRenderableTree,
33229
33275
  isStickySearchParam,
33230
33276
  isValidUrl,
33277
+ matchCollectionItemClick,
33231
33278
  nextViableSlide,
33232
33279
  parseAiFooterState,
33233
33280
  parseAiSectionsState,