@ohhwells/bridge 0.1.154 → 0.1.156

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
@@ -11585,6 +11585,172 @@ function useLinkHrefGuardian(...deps) {
11585
11585
  }, deps);
11586
11586
  }
11587
11587
 
11588
+ // src/ui/link-modal/linkModal.utils.ts
11589
+ function parseTarget(target) {
11590
+ if (!target) return { pageRoute: "", sectionId: null };
11591
+ const hashIndex = target.indexOf("#");
11592
+ if (hashIndex === -1) return { pageRoute: target, sectionId: null };
11593
+ return {
11594
+ pageRoute: target.slice(0, hashIndex),
11595
+ sectionId: target.slice(hashIndex + 1) || null
11596
+ };
11597
+ }
11598
+ function normalizePath(path) {
11599
+ const trimmed = path.trim();
11600
+ if (!trimmed) return "/";
11601
+ try {
11602
+ if (/^https?:\/\//i.test(trimmed)) {
11603
+ return new URL(trimmed).pathname || "/";
11604
+ }
11605
+ } catch {
11606
+ }
11607
+ const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
11608
+ if (withSlash.length > 1 && withSlash.endsWith("/")) return withSlash.slice(0, -1);
11609
+ return withSlash;
11610
+ }
11611
+ function findPageByPath(pages, path) {
11612
+ const normalized = normalizePath(path);
11613
+ return pages.find((p) => normalizePath(p.path) === normalized);
11614
+ }
11615
+ function inferPageTitleFromPath(path) {
11616
+ const normalized = normalizePath(path);
11617
+ if (normalized === "/") return "Home";
11618
+ const segment = normalized.split("/").filter(Boolean).pop() ?? "";
11619
+ if (!segment) return "Home";
11620
+ return segment.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
11621
+ }
11622
+ function isInternalPath(route) {
11623
+ const trimmed = route.trim();
11624
+ if (!trimmed) return false;
11625
+ if (/^https?:\/\//i.test(trimmed)) return false;
11626
+ if (trimmed.startsWith("/")) return true;
11627
+ if (trimmed.includes(" ") || trimmed.includes(".")) return false;
11628
+ return true;
11629
+ }
11630
+ function resolvePage(route, pages) {
11631
+ const found = findPageByPath(pages, route);
11632
+ if (found) return { path: found.path, title: found.title };
11633
+ return { path: normalizePath(route), title: inferPageTitleFromPath(route) };
11634
+ }
11635
+ function getSectionsForPath(sectionsByPath, pageRoute) {
11636
+ if (!sectionsByPath) return [];
11637
+ const normalized = normalizePath(pageRoute);
11638
+ return sectionsByPath[normalized] ?? sectionsByPath[pageRoute] ?? [];
11639
+ }
11640
+ function buildTarget(page, section) {
11641
+ if (section) return `${page.path}#${section.id}`;
11642
+ return page.path;
11643
+ }
11644
+ function isValidUrl(urlString) {
11645
+ try {
11646
+ const url = new URL(urlString);
11647
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
11648
+ const hostname = url.hostname;
11649
+ if (!hostname || hostname.length === 0) return false;
11650
+ if (hostname.includes(" ")) return false;
11651
+ if (hostname === "localhost") return true;
11652
+ if (!hostname.includes(".")) return false;
11653
+ if (!/[a-z]/i.test(hostname)) return false;
11654
+ return true;
11655
+ } catch {
11656
+ return false;
11657
+ }
11658
+ }
11659
+ function normalizeUrl(value) {
11660
+ const trimmed = value.trim();
11661
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
11662
+ return `https://${trimmed}`;
11663
+ }
11664
+ function validateUrlInput(value, pages, allPages) {
11665
+ const trimmed = value.trim();
11666
+ if (!trimmed) return "";
11667
+ const exactPage = allPages.find((p) => p.title.toLowerCase() === trimmed.toLowerCase());
11668
+ if (exactPage) return "";
11669
+ const hasPrefixMatch = pages.some((p) => p.title.toLowerCase().startsWith(trimmed.toLowerCase()));
11670
+ if (hasPrefixMatch) {
11671
+ return "Select a page from the list or enter a valid URL";
11672
+ }
11673
+ const hasScheme = /^https?:\/\//i.test(trimmed);
11674
+ if (!hasScheme && !trimmed.includes(".")) {
11675
+ return "Please enter a valid URL (e.g., https://example.com)";
11676
+ }
11677
+ if (!isValidUrl(normalizeUrl(trimmed))) {
11678
+ return "Please enter a valid URL (e.g., https://example.com)";
11679
+ }
11680
+ return "";
11681
+ }
11682
+ function getEditModeInitialState(target, pages, sections) {
11683
+ const empty = {
11684
+ searchValue: "",
11685
+ selectedPage: null,
11686
+ selectedSection: null,
11687
+ step: "input"
11688
+ };
11689
+ if (!target) return empty;
11690
+ if (isValidUrl(normalizeUrl(target))) {
11691
+ return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
11692
+ }
11693
+ const { pageRoute, sectionId } = parseTarget(target);
11694
+ if (sectionId) {
11695
+ if (isInternalPath(pageRoute)) {
11696
+ const page = resolvePage(pageRoute, pages);
11697
+ const sec = sections.find((s) => s.id === sectionId);
11698
+ return {
11699
+ searchValue: page.title,
11700
+ selectedPage: page,
11701
+ selectedSection: sec ? { id: sec.id, label: sec.label } : null,
11702
+ step: "input"
11703
+ };
11704
+ }
11705
+ return { searchValue: pageRoute || target, selectedPage: null, selectedSection: null, step: "input" };
11706
+ }
11707
+ if (isInternalPath(pageRoute)) {
11708
+ const page = resolvePage(pageRoute, pages);
11709
+ return {
11710
+ searchValue: page.title,
11711
+ selectedPage: page,
11712
+ selectedSection: null,
11713
+ step: "input"
11714
+ };
11715
+ }
11716
+ return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
11717
+ }
11718
+ function filterAvailablePages(pages, existingTargets) {
11719
+ const taken = new Set(existingTargets);
11720
+ return pages.filter((p) => !taken.has(p.path));
11721
+ }
11722
+
11723
+ // src/lib/collection-item-click.ts
11724
+ function indexCollectionItems(pages) {
11725
+ const index = /* @__PURE__ */ new Map();
11726
+ for (const page of pages) {
11727
+ if (page.kind !== "COLLECTION_ITEM" || !page.collection || !page.path) continue;
11728
+ index.set(normalizePath(page.path), { path: page.path, collection: page.collection });
11729
+ }
11730
+ return index;
11731
+ }
11732
+ function pathOfHref(href) {
11733
+ const trimmed = href.trim();
11734
+ if (!trimmed || trimmed.startsWith("#")) return null;
11735
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed) && !/^https?:/i.test(trimmed)) return null;
11736
+ const withoutHash = trimmed.split("#")[0];
11737
+ const withoutQuery = withoutHash.split("?")[0];
11738
+ if (!withoutQuery) return null;
11739
+ return normalizePath(withoutQuery);
11740
+ }
11741
+ function matchCollectionItemClick(target, index) {
11742
+ if (!target || index.size === 0) return null;
11743
+ const text = target.closest('[data-ohw-editable="text"], [data-ohw-editable="plain"]');
11744
+ if (!text) return null;
11745
+ const anchor = text.closest("a");
11746
+ if (!anchor || anchor.closest(CTA_BUTTON_SELECTOR)) return null;
11747
+ const href = anchor.getAttribute("href");
11748
+ if (!href) return null;
11749
+ const path = pathOfHref(href);
11750
+ if (!path) return null;
11751
+ return index.get(path) ?? null;
11752
+ }
11753
+
11588
11754
  // src/lib/sticky-search-params.ts
11589
11755
  var STICKY_EXACT = /* @__PURE__ */ new Set([
11590
11756
  // Site identity. Required in local dev and in the canvas editor's iframe, where there is no
@@ -16934,7 +17100,7 @@ function apiBase() {
16934
17100
  function bridgeVersion() {
16935
17101
  try {
16936
17102
  if (true) {
16937
- return "0.1.154";
17103
+ return "0.1.156";
16938
17104
  }
16939
17105
  } catch {
16940
17106
  }
@@ -16948,7 +17114,7 @@ function stampBridgeVersionOnDocument() {
16948
17114
  return;
16949
17115
  }
16950
17116
  try {
16951
- const v = true ? "0.1.154" : "";
17117
+ const v = true ? "0.1.156" : "";
16952
17118
  if (v) {
16953
17119
  document.documentElement.setAttribute("data-ohw-bridge-version", v);
16954
17120
  window.__OHW_BRIDGE_VERSION__ = v;
@@ -16957,10 +17123,19 @@ function stampBridgeVersionOnDocument() {
16957
17123
  }
16958
17124
  }
16959
17125
  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;
17126
+ function isVercelPreviewHost() {
17127
+ if (typeof location === "undefined") {
17128
+ return false;
17129
+ }
17130
+ return location.hostname.toLowerCase().endsWith(".vercel.ohhwells.site");
17131
+ }
16960
17132
  function reportOutageBeacon(args) {
16961
17133
  if (typeof window === "undefined" || !args.subdomain) {
16962
17134
  return;
16963
17135
  }
17136
+ if (isVercelPreviewHost()) {
17137
+ return;
17138
+ }
16964
17139
  if (args.reason === "page_error" && args.detail && RECOVERABLE_PAGE_ERROR.test(args.detail)) {
16965
17140
  return;
16966
17141
  }
@@ -20595,141 +20770,6 @@ function isEditSessionActive() {
20595
20770
  return new URLSearchParams(q).get("mode") === "edit";
20596
20771
  }
20597
20772
 
20598
- // src/ui/link-modal/linkModal.utils.ts
20599
- function parseTarget(target) {
20600
- if (!target) return { pageRoute: "", sectionId: null };
20601
- const hashIndex = target.indexOf("#");
20602
- if (hashIndex === -1) return { pageRoute: target, sectionId: null };
20603
- return {
20604
- pageRoute: target.slice(0, hashIndex),
20605
- sectionId: target.slice(hashIndex + 1) || null
20606
- };
20607
- }
20608
- function normalizePath(path) {
20609
- const trimmed = path.trim();
20610
- if (!trimmed) return "/";
20611
- try {
20612
- if (/^https?:\/\//i.test(trimmed)) {
20613
- return new URL(trimmed).pathname || "/";
20614
- }
20615
- } catch {
20616
- }
20617
- const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
20618
- if (withSlash.length > 1 && withSlash.endsWith("/")) return withSlash.slice(0, -1);
20619
- return withSlash;
20620
- }
20621
- function findPageByPath(pages, path) {
20622
- const normalized = normalizePath(path);
20623
- return pages.find((p) => normalizePath(p.path) === normalized);
20624
- }
20625
- function inferPageTitleFromPath(path) {
20626
- const normalized = normalizePath(path);
20627
- if (normalized === "/") return "Home";
20628
- const segment = normalized.split("/").filter(Boolean).pop() ?? "";
20629
- if (!segment) return "Home";
20630
- return segment.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
20631
- }
20632
- function isInternalPath(route) {
20633
- const trimmed = route.trim();
20634
- if (!trimmed) return false;
20635
- if (/^https?:\/\//i.test(trimmed)) return false;
20636
- if (trimmed.startsWith("/")) return true;
20637
- if (trimmed.includes(" ") || trimmed.includes(".")) return false;
20638
- return true;
20639
- }
20640
- function resolvePage(route, pages) {
20641
- const found = findPageByPath(pages, route);
20642
- if (found) return { path: found.path, title: found.title };
20643
- return { path: normalizePath(route), title: inferPageTitleFromPath(route) };
20644
- }
20645
- function getSectionsForPath(sectionsByPath, pageRoute) {
20646
- if (!sectionsByPath) return [];
20647
- const normalized = normalizePath(pageRoute);
20648
- return sectionsByPath[normalized] ?? sectionsByPath[pageRoute] ?? [];
20649
- }
20650
- function buildTarget(page, section) {
20651
- if (section) return `${page.path}#${section.id}`;
20652
- return page.path;
20653
- }
20654
- function isValidUrl(urlString) {
20655
- try {
20656
- const url = new URL(urlString);
20657
- if (url.protocol !== "http:" && url.protocol !== "https:") return false;
20658
- const hostname = url.hostname;
20659
- if (!hostname || hostname.length === 0) return false;
20660
- if (hostname.includes(" ")) return false;
20661
- if (hostname === "localhost") return true;
20662
- if (!hostname.includes(".")) return false;
20663
- if (!/[a-z]/i.test(hostname)) return false;
20664
- return true;
20665
- } catch {
20666
- return false;
20667
- }
20668
- }
20669
- function normalizeUrl(value) {
20670
- const trimmed = value.trim();
20671
- if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
20672
- return `https://${trimmed}`;
20673
- }
20674
- function validateUrlInput(value, pages, allPages) {
20675
- const trimmed = value.trim();
20676
- if (!trimmed) return "";
20677
- const exactPage = allPages.find((p) => p.title.toLowerCase() === trimmed.toLowerCase());
20678
- if (exactPage) return "";
20679
- const hasPrefixMatch = pages.some((p) => p.title.toLowerCase().startsWith(trimmed.toLowerCase()));
20680
- if (hasPrefixMatch) {
20681
- return "Select a page from the list or enter a valid URL";
20682
- }
20683
- const hasScheme = /^https?:\/\//i.test(trimmed);
20684
- if (!hasScheme && !trimmed.includes(".")) {
20685
- return "Please enter a valid URL (e.g., https://example.com)";
20686
- }
20687
- if (!isValidUrl(normalizeUrl(trimmed))) {
20688
- return "Please enter a valid URL (e.g., https://example.com)";
20689
- }
20690
- return "";
20691
- }
20692
- function getEditModeInitialState(target, pages, sections) {
20693
- const empty = {
20694
- searchValue: "",
20695
- selectedPage: null,
20696
- selectedSection: null,
20697
- step: "input"
20698
- };
20699
- if (!target) return empty;
20700
- if (isValidUrl(normalizeUrl(target))) {
20701
- return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
20702
- }
20703
- const { pageRoute, sectionId } = parseTarget(target);
20704
- if (sectionId) {
20705
- if (isInternalPath(pageRoute)) {
20706
- const page = resolvePage(pageRoute, pages);
20707
- const sec = sections.find((s) => s.id === sectionId);
20708
- return {
20709
- searchValue: page.title,
20710
- selectedPage: page,
20711
- selectedSection: sec ? { id: sec.id, label: sec.label } : null,
20712
- step: "input"
20713
- };
20714
- }
20715
- return { searchValue: pageRoute || target, selectedPage: null, selectedSection: null, step: "input" };
20716
- }
20717
- if (isInternalPath(pageRoute)) {
20718
- const page = resolvePage(pageRoute, pages);
20719
- return {
20720
- searchValue: page.title,
20721
- selectedPage: page,
20722
- selectedSection: null,
20723
- step: "input"
20724
- };
20725
- }
20726
- return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
20727
- }
20728
- function filterAvailablePages(pages, existingTargets) {
20729
- const taken = new Set(existingTargets);
20730
- return pages.filter((p) => !taken.has(p.path));
20731
- }
20732
-
20733
20773
  // src/lib/prefetch-sections.ts
20734
20774
  function buildPageFetchUrl(path) {
20735
20775
  const normalized = normalizePath(path);
@@ -25894,6 +25934,7 @@ function OhhwellsBridge() {
25894
25934
  const appliedDrawerRef = useRef14(null);
25895
25935
  const pendingDeleteUndoRef = useRef14(null);
25896
25936
  const [sitePages, setSitePages] = useState18([]);
25937
+ const collectionItemsRef = useRef14(/* @__PURE__ */ new Map());
25897
25938
  const [sectionsByPath, setSectionsByPath] = useState18({});
25898
25939
  const sectionsPrefetchGenRef = useRef14(0);
25899
25940
  const setLinkPopoverRef = useRef14(setLinkPopover);
@@ -26037,10 +26078,12 @@ function OhhwellsBridge() {
26037
26078
  const onSitePages = (e) => {
26038
26079
  if (e.data?.type !== "ow:site-pages" || !Array.isArray(e.data.pages)) return;
26039
26080
  if (useFixtures) return;
26040
- const mapped = e.data.pages.map((p) => ({
26081
+ const incoming = e.data.pages;
26082
+ const mapped = incoming.map((p) => ({
26041
26083
  path: p.path,
26042
26084
  title: p.title
26043
26085
  }));
26086
+ collectionItemsRef.current = indexCollectionItems(incoming);
26044
26087
  setSitePages(mapped);
26045
26088
  runSectionsPrefetchRef.current(mapped);
26046
26089
  };
@@ -28460,6 +28503,17 @@ function OhhwellsBridge() {
28460
28503
  }
28461
28504
  if (isInsideFloatingPanel(target)) return;
28462
28505
  if (target.closest("[data-ohw-form-toolbar]")) return;
28506
+ const collectionItem = matchCollectionItemClick(target, collectionItemsRef.current);
28507
+ if (collectionItem) {
28508
+ e.preventDefault();
28509
+ e.stopPropagation();
28510
+ postToParentRef.current({
28511
+ type: "ow:collection-item-selected",
28512
+ path: collectionItem.path,
28513
+ collection: collectionItem.collection
28514
+ });
28515
+ return;
28516
+ }
28463
28517
  if (target.closest(
28464
28518
  '[data-ohw-field-toolbar], [data-ohw-field-type-picker], [data-radix-popper-content-wrapper], [role="menu"], [data-slot="dropdown-menu-content"]'
28465
28519
  )) {
@@ -31880,7 +31934,7 @@ function OhhwellsBridge() {
31880
31934
  postToParent2({
31881
31935
  type: "ow:ready",
31882
31936
  version: "1",
31883
- bridgeVersion: "0.1.154",
31937
+ bridgeVersion: "0.1.156",
31884
31938
  path: pathname,
31885
31939
  nodes: collectEditableNodes(editContentRef.current),
31886
31940
  sections
@@ -33105,6 +33159,7 @@ export {
33105
33159
  foldAlignIntoTrees,
33106
33160
  getEditModeInitialState,
33107
33161
  hasMountedContentNodes,
33162
+ indexCollectionItems,
33108
33163
  insertFooterHours,
33109
33164
  isEditSessionActive,
33110
33165
  isPlainLeftClick,
@@ -33112,6 +33167,7 @@ export {
33112
33167
  isRenderableTree,
33113
33168
  isStickySearchParam,
33114
33169
  isValidUrl,
33170
+ matchCollectionItemClick,
33115
33171
  nextViableSlide,
33116
33172
  parseAiFooterState,
33117
33173
  parseAiSectionsState,