{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-WFEVKHPP.cjs","../src/components/shared/doc-search/format-relative-path.ts","../src/components/shared/doc-search/doc-search-result-row.tsx","../src/components/shared/doc-search/doc-search-bar.tsx","../src/components/shared/doc-search/map-doc-search-results.ts","../src/components/shared/doc-search/resolve-search-result-action.ts","../src/components/shared/doc-search/use-doc-search.ts"],"names":["jsx"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-WFEVKHPP.cjs","sourcesContent":[null,"/**\n * Format a full document path as a breadcrumb trail.\n * Shows parent folders only (excludes the last segment / filename).\n *\n * @example\n *   formatRelativePath('openframe-oss-tenant/architecture/api-controllers.md')\n *   // → 'Openframe oss tenant / Architecture'\n */\nexport function formatRelativePath(fullPath: string): string {\n  if (!fullPath) return ''\n  const segments = fullPath.replace(/\\.md$/, '').split('/')\n  // Show only parent path (exclude the filename itself since the title already shows it)\n  const parentSegments = segments.length > 1 ? segments.slice(0, -1) : segments\n  return parentSegments\n    .map((seg) => seg.charAt(0).toUpperCase() + seg.slice(1).replace(/-/g, ' '))\n    .join(' / ')\n}\n","'use client'\n\n/**\n * Single row in the `<SearchInput>` dropdown — the standard layout\n * used by every doc-search-backed surface (company-hub data-room\n * search bar, onboarding-guide catalog search, …). Single source of\n * truth for the row appearance so search dropdowns are visually\n * identical everywhere.\n *\n * Resolves the source icon via the same `resolveSourceIcon()`\n * registry the inline chat-card refs use, so a row pointing at e.g.\n * an onboarding-guide surfaces the SAME `<GraduationCap>` glyph the\n * chat card surfaces — no cross-surface drift.\n */\n\nimport { resolveSourceIcon } from '../../chat/utils/source-row-cta'\nimport { formatRelativePath } from './format-relative-path'\n\n/**\n * Minimal result shape this row renders. Compatible with any\n * doc-search hook whose result type exposes `{ title?, path?,\n * metadata? }`. The two hub consumers (onboarding-guide catalog,\n * data-room sidebar) both satisfy this shape via their\n * `useDocSearch` hook result.\n */\nexport interface DocSearchResultRowEntry {\n  title?: string\n  path?: string\n  metadata?: Record<string, unknown>\n}\n\nexport interface DocSearchResultRowProps {\n  result: DocSearchResultRowEntry\n  isHighlighted: boolean\n}\n\nexport function DocSearchResultRow({\n  result,\n  isHighlighted,\n}: DocSearchResultRowProps) {\n  const docType = (result.metadata?.documentType as string) || undefined\n  const sourceRepo = (result.metadata?.sourceRepo as string) || undefined\n  const { Icon: SourceIcon, label: iconLabel } = resolveSourceIcon({\n    sourceRepo,\n    documentType: docType,\n  })\n  const isGroup = result.metadata?.isGroup as boolean | undefined\n\n  return (\n    <div className=\"flex items-center gap-3 w-full min-w-0\">\n      <span\n        className=\"flex-shrink-0 text-ods-text-secondary\"\n        title={iconLabel}\n      >\n        <SourceIcon className=\"size-4\" />\n      </span>\n      <div className=\"min-w-0 flex-1\">\n        <div\n          className={`text-h6 truncate ${\n            isHighlighted ? 'text-ods-accent' : 'text-ods-text-primary'\n          }`}\n        >\n          {result.title || result.path}\n        </div>\n        {!isGroup && result.path?.includes('/') && (\n          <div className=\"text-h6 text-ods-text-secondary truncate mt-0.5\">\n            {formatRelativePath(result.path)}\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n","'use client'\n\n/**\n * `<DocSearchBar>` — the canonical RAG-search dropdown surface.\n *\n * Mounted by every doc-search consumer (data-room sidebar, onboarding-\n * guide catalog, and any future surface that needs typeahead against\n * `/api/docs/search`). Wraps `<SearchInput>` with the lib's standard\n * `<DocSearchResultRow>` so the dropdown looks identical everywhere.\n *\n * ## Why a presentation component, not a \"search bar that owns its\n *    own hook\"\n *\n * The data-fetching hook (`useDocSearch`) lives hub-side because it\n * depends on hub-only context (`useDocNavigation`, the rag-table-\n * config registry, the hub's `decideNewTab` helper). Moving the hook\n * would cascade ~5 more file migrations into the lib.\n *\n * Instead, the hook stays hub-side and callers pass its result into\n * this component as plain props. Both consumers shrink to ~5 lines.\n */\n\nimport type { ReactNode } from 'react'\nimport { SearchInput, type SearchResult } from '../../ui/search-input'\nimport { DocSearchResultRow } from './doc-search-result-row'\n\nexport interface DocSearchBarProps {\n  placeholder: string\n  query: string\n  onQueryChange: (value: string) => void\n  /** Hook-fetched results. Reuses the lib's `<SearchInput>` `SearchResult`\n   *  shape directly so callers don't translate. */\n  results: SearchResult[]\n  isLoading: boolean\n  /** Result selection handler. Mirrors `<SearchInput>` — the second\n   *  `modifiers` argument is preserved so cmd-click / shift-click on\n   *  a result row still forces new-tab behavior. Hub `useDocSearch`\n   *  reads these to short-circuit to `window.open()`. */\n  onResultSelect: (\n    result: SearchResult,\n    modifiers?: {\n      metaKey?: boolean\n      ctrlKey?: boolean\n      shiftKey?: boolean\n      altKey?: boolean\n      button?: number\n    },\n  ) => void\n  /** Lets the caller's hook force the dropdown open after a recent\n   *  internal action (e.g. result navigation). `undefined` falls back\n   *  to `<SearchInput>`'s built-in focus/hover heuristics. */\n  showDropdown?: boolean\n  /** Defaults to 2 — matches the existing data-room and onboarding-\n   *  guide consumers. Override only if a surface needs different\n   *  typeahead semantics. */\n  minQueryLength?: number\n  /** Defaults to 0 — both existing consumers debounce inside the\n   *  hook, not the input. */\n  debounceMs?: number\n  className?: string\n  /** Optional row-renderer override. Defaults to the lib's standard\n   *  `<DocSearchResultRow>` (source icon + title + path breadcrumb).\n   *  Override only when a surface needs custom row chrome. */\n  renderResult?: (result: SearchResult, isHighlighted: boolean) => ReactNode\n}\n\nexport function DocSearchBar({\n  placeholder,\n  query,\n  onQueryChange,\n  results,\n  isLoading,\n  onResultSelect,\n  showDropdown,\n  minQueryLength = 2,\n  debounceMs = 0,\n  className = 'w-full',\n  renderResult,\n}: DocSearchBarProps) {\n  return (\n    <SearchInput\n      placeholder={placeholder}\n      value={query}\n      onChange={onQueryChange}\n      results={results}\n      isLoading={isLoading}\n      onResultSelect={onResultSelect}\n      showDropdown={showDropdown || undefined}\n      debounceMs={debounceMs}\n      minQueryLength={minQueryLength}\n      className={className}\n      renderResult={\n        renderResult ??\n        ((result, isHighlighted) => (\n          <DocSearchResultRow result={result} isHighlighted={isHighlighted} />\n        ))\n      }\n    />\n  )\n}\n","/**\n * Map RAG `/api/docs/search` wire results into the `<DocSearchBar>`\n * dropdown's row shape, collapsing entity-table rows into grouped\n * results so the dropdown lists ONE \"Cap Table (12 records)\" row\n * instead of 12 individual rows.\n *\n * Pure transform — no telemetry, no navigation, no React deps. Lifted\n * from the hub's `hooks/use-docs.ts:mapDocSearchResults` (the hub's\n * `traceCompose` call was hub-only telemetry and is intentionally\n * dropped — callers that want logging can wrap this helper).\n */\n\nimport type { SearchResult } from '../../ui/search-input'\nimport type { DocSearchResult } from './types'\n\n/** Source repos that should be collapsed into grouped results in the search bar.\n *  Only financial tables (all rows link to the same admin page).\n *  Content tables (blog, webinar, podcast, etc.) stay individual since each has a unique URL. */\nconst SEARCH_GROUP_REPOS = new Set([\n  'financial-cap-table',\n  'financial-kpis',\n  'financial-pnl',\n  'financial-balance-sheet',\n  'financial-cash-flow',\n])\n\nconst ENTITY_LABELS: Record<string, string> = {\n  'financial-cap-table': 'Cap Table',\n  'financial-kpis': 'Financial KPIs',\n  'financial-pnl': 'Profit & Loss',\n  'financial-balance-sheet': 'Balance Sheets',\n  'financial-cash-flow': 'Cash Flow',\n  'blog-posts': 'Blog Posts',\n  'product-releases': 'Product Releases',\n  'case-studies': 'Case Studies',\n  webinars: 'Webinars',\n  events: 'Events',\n  podcasts: 'Podcasts',\n}\n\nexport function mapDocSearchResults(docs: DocSearchResult[]): SearchResult[] {\n  const entityGroups = new Map<string, DocSearchResult[]>()\n  // Track insertion order — groups appear where the FIRST row of that\n  // repo appeared in the response.\n  const order: Array<\n    { type: 'entity'; repo: string } | { type: 'doc'; doc: DocSearchResult }\n  > = []\n  const seenRepos = new Set<string>()\n\n  for (const doc of docs) {\n    if (doc.sourceRepo && SEARCH_GROUP_REPOS.has(doc.sourceRepo)) {\n      const group = entityGroups.get(doc.sourceRepo) || []\n      group.push(doc)\n      entityGroups.set(doc.sourceRepo, group)\n      if (!seenRepos.has(doc.sourceRepo)) {\n        seenRepos.add(doc.sourceRepo)\n        order.push({ type: 'entity', repo: doc.sourceRepo })\n      }\n    } else {\n      order.push({ type: 'doc', doc })\n    }\n  }\n\n  const results: SearchResult[] = []\n  for (const entry of order) {\n    if (entry.type === 'entity') {\n      const rows = entityGroups.get(entry.repo)!\n      const label = ENTITY_LABELS[entry.repo] || entry.repo\n      results.push({\n        id: `group-${entry.repo}`,\n        title: `${label} (${rows.length} ${rows.length === 1 ? 'record' : 'records'})`,\n        path: rows[0].path,\n        type: 'file',\n        metadata: {\n          documentType: rows[0].documentType,\n          externalUrl: rows[0].externalUrl,\n          sourceRepo: entry.repo,\n          id: rows[0].entityId,\n          isGroup: true,\n          items: rows.map((r) => ({\n            name: r.name,\n            externalUrl: r.externalUrl,\n            id: r.entityId,\n            sourceRepo: r.sourceRepo,\n            documentType: r.documentType,\n          })),\n        },\n      })\n    } else {\n      const doc = entry.doc\n      const isNonMarkdown = doc.documentType && doc.documentType !== 'markdown'\n      results.push({\n        id: doc.path,\n        title: doc.name,\n        description: isNonMarkdown ? doc.name : doc.snippet,\n        path: doc.path,\n        type: doc.type,\n        metadata: {\n          matchType: doc.matchType,\n          ...(doc.documentType ? { documentType: doc.documentType } : {}),\n          ...(doc.externalUrl ? { externalUrl: doc.externalUrl } : {}),\n          ...(doc.targetPlatform != null\n            ? { targetPlatform: doc.targetPlatform }\n            : {}),\n          ...(doc.sourceRepo ? { sourceRepo: doc.sourceRepo } : {}),\n          ...(doc.entityId ? { id: doc.entityId } : {}),\n        },\n      })\n    }\n  }\n\n  return results\n}\n","/**\n * Resolve what should happen when the user picks a search result.\n * Returns one of five typed actions so the caller is a single switch.\n *\n * Resolution order:\n *  1. `externalUrl` present → resolve the href through the host's\n *     `composeContentUrl` seam (when wired), then use `decideNewTab` to\n *     choose same-tab vs new-tab against the resolved `targetPlatform`.\n *  2. Row has `id` + `sourceRepo` + `documentType` → synth an Ask-AI\n *     action (entity drill-in via primary key, no URL).\n *  3. Row has only `path` → legacy navigation fallback.\n *  4. Nothing actionable → noop.\n *\n * Lifted from the hub's `hooks/use-docs.ts:resolveSearchResultAction`.\n * Pure — no React, no telemetry.\n */\n\nimport type { SearchResult } from '../../ui/search-input'\nimport type { ChatRef } from '../../chat/chat-ref.types'\nimport type { ComposeContentUrl } from '../../../utils/content-href'\nimport { decideNewTab } from '../../chat/utils/decide-new-tab'\n\n/** True for `https://…` and protocol-relative `//…`. A composed href that is\n *  NOT absolute is an in-app route the host claimed via `hostedTypes` /\n *  `overrides`. */\nfunction isAbsoluteHref(href: string): boolean {\n  return /^(?:[a-z][a-z0-9+.-]*:)?\\/\\//i.test(href)\n}\n\nexport type SearchResultAction =\n  | { kind: 'navigate-same-tab'; href: string }\n  | { kind: 'navigate-new-tab'; href: string }\n  | { kind: 'ask-ai'; detail: { source: string; ref: ChatRef } }\n  | { kind: 'route'; path: string }\n  | { kind: 'noop' }\n\nexport function resolveSearchResultAction(\n  result: SearchResult,\n  source: string,\n  runtimeMode?: 'host' | 'embed',\n  /** The host's unified content-href seam (`runtime.composeContentUrl`).\n   *  When wired, an entity row's RAG `externalUrl` is re-resolved through it,\n   *  so a type the host serves in-app (`hostedTypes`) or deep-links itself\n   *  (`overrides`) lands on the SAME href the catalog cards and chat cards\n   *  use. Omitted → the legacy verbatim-`externalUrl` behavior. */\n  composeContentUrl?: ComposeContentUrl,\n): SearchResultAction {\n  const meta = result.metadata ?? {}\n  const externalUrl = meta.externalUrl as string | undefined\n  const rowId = meta.id as string | undefined\n  const sourceRepo = meta.sourceRepo as string | undefined\n  const documentType = meta.documentType as string | undefined\n  if (externalUrl) {\n    const targetPlatform = meta.targetPlatform as string | null | undefined\n    // Unified content-href seam — the same call `resolveSourceRowCTA` makes\n    // for chat rows. Hosted types relativize the slug out of `externalUrl`;\n    // everything else comes back verbatim, so an unwired host (or a row with\n    // no `documentType`) keeps today's behavior byte-for-byte.\n    const composed =\n      composeContentUrl && documentType\n        ? composeContentUrl({\n            type: documentType,\n            identifier: rowId ?? '',\n            externalUrl,\n            targetPlatform: targetPlatform ?? null,\n          })\n        : null\n    const href = composed?.href || externalUrl\n    // A RELATIVE composed href means the host claimed this row as in-app.\n    // Short-circuit BEFORE `decideNewTab`: in embed mode that helper forces\n    // new-tab unconditionally, which would fire `window.open()` on a path\n    // that only resolves inside this app.\n    if (composed && !isAbsoluteHref(href)) {\n      return { kind: 'navigate-same-tab', href }\n    }\n    // Same pure helper `useNavLink` and `useUnifiedNav` call — single\n    // decision rule across cards, chips, and autocomplete rows. Thread\n    // the caller's `source` as `currentSource` so the platform-vs-\n    // platform comparison matches the hub's pre-migration behavior.\n    const isNewTab = decideNewTab({\n      href,\n      targetPlatform: composed ? composed.targetPlatform : targetPlatform,\n      surface: 'useUnifiedNav',\n      runtimeMode,\n      currentSource: source,\n    })\n    return isNewTab\n      ? { kind: 'navigate-new-tab', href }\n      : { kind: 'navigate-same-tab', href }\n  }\n  if (rowId && sourceRepo && documentType) {\n    return {\n      kind: 'ask-ai',\n      detail: {\n        source,\n        ref: { type: documentType, id: rowId, title: result.title, url: null },\n      },\n    }\n  }\n  if (result.path) {\n    return { kind: 'route', path: result.path }\n  }\n  return { kind: 'noop' }\n}\n","'use client'\n\n/**\n * `useDocSearch` — debounced RAG-search hook against `/api/docs/search`.\n *\n * Pure fetch + navigation glue. Embedders can mount this directly\n * (any host with a reverse-proxy that exposes `/api/docs/search` will\n * work). Hub callers wire it into the lib `<DocSearchBar>` for the\n * canonical typeahead dropdown.\n *\n * ## What moved from hub to lib\n *\n * Lifted from `multi-platform-hub/hooks/use-docs.ts:useDocSearch`. Two\n * hub-only concerns are now optional injection points instead of\n * direct imports:\n *\n *   - `useDocNavigation()` (hub's in-page doc-tree swap) → optional\n *     `onInPageSwap?: (path: string) => boolean` config callback. When\n *     present and returns true, the hook treats a same-origin result\n *     click as \"handled in-page\"; when absent or returns false, the\n *     hook falls back to `onNavigate(path)` (`router.push` on hub,\n *     `window.location.assign` on bare embedders).\n *   - `traceCompose` (hub-only telemetry) → dropped. The lib has no\n *     equivalent runtime-context yet; bring it back when there is one.\n *\n * Everything else (debounce, `useChatRuntime` for embed-mode short-\n * circuit, embed-shim router, the action-resolver + result-mapper) is\n * now lib-resident.\n *\n * ## Result hrefs go through the content-href seam\n *\n * A row's href is NOT the RAG `externalUrl` verbatim any more: when the\n * host wires `runtime.composeContentUrl` (or passes `composeContentUrl`\n * here), the row is re-resolved through it — the SAME seam the catalog\n * cards and the chat entity cards use. So a host that serves onboarding\n * guides at `/help-center/onboarding-guides/<slug>` gets that path from\n * the dropdown too, instead of bouncing the user to the canonical hub\n * URL. Unwired hosts keep the verbatim-`externalUrl` behavior.\n */\n\nimport { useState, useEffect, useCallback } from 'react'\nimport { useRouter } from '../../../embed-shims'\nimport { useDebounce } from '../../../hooks/ui/use-debounce'\nimport { useChatRuntime } from '../../../contexts/chat-runtime-context'\nimport { contentFetch } from '../../../utils/embed-content-fetch'\nimport type { SearchResult } from '../../ui/search-input'\nimport {\n  resolveExternalNavigation,\n  stripSameOriginToPath,\n  NEW_TAB_FEATURES,\n} from '../../chat/utils/chat-nav-resolution'\nimport type { ComposeContentUrl } from '../../../utils/content-href'\nimport type { DocSearchResult } from './types'\nimport { mapDocSearchResults } from './map-doc-search-results'\nimport { resolveSearchResultAction } from './resolve-search-result-action'\n\nexport interface UseDocSearchConfig {\n  /** Discriminator passed to `/api/docs/search?source=` (e.g.\n   *  `'openframe'`). Embedders set it to whatever discriminator their\n   *  reverse-proxy expects. */\n  source: string\n  /** Base route prefix this search lives under (e.g. `'/onboarding-guides'`).\n   *  When a result's href starts with `${baseRoute}/`, the hook\n   *  attempts the optional in-page swap path before falling through\n   *  to a full nav. */\n  baseRoute: string\n  /** Imperative navigation fallback. Called when no override\n   *  (in-page swap, new-tab) applies. Hub callers pass\n   *  `(path) => router.push(path)`; embedders pass an equivalent. */\n  onNavigate: (path: string) => void\n  /** Optional `RagTableConfig.id` list to narrow the search to specific\n   *  tables (e.g. `['onboarding-guides']`). Forwarded to\n   *  `/api/docs/search?tableIds=…` which intersects with the source's\n   *  standing set. */\n  tableIds?: string[]\n  /** Optional in-page swap callback. When the result's href is under\n   *  `baseRoute` AND this callback returns true, the hook treats the\n   *  click as handled in-page (no router push). Hub's\n   *  `<DocumentationSection>` wires this to\n   *  `useDocNavigation().navigate(path)`. */\n  onInPageSwap?: (path: string) => boolean\n  /** Optional endpoint override. Defaults to `'/api/docs/search'`\n   *  (the hub's reverse-proxy route). Embedders with a different\n   *  path can override. */\n  searchEndpoint?: string\n  /** Optional content-href seam override. Same fall-back chain as\n   *  `searchEndpoint`: prop → `runtime.composeContentUrl` → none. Wired,\n   *  it makes a result row honor the host's `hostedTypes` / `overrides`\n   *  instead of navigating to the RAG's canonical hub URL — so a row\n   *  lands where the catalog card for the same entity lands. */\n  composeContentUrl?: ComposeContentUrl\n}\n\nexport function useDocSearch(config: UseDocSearchConfig) {\n  const {\n    source,\n    baseRoute,\n    onNavigate,\n    tableIds,\n    onInPageSwap,\n    searchEndpoint,\n    composeContentUrl,\n  } = config\n  const tableIdsKey = tableIds && tableIds.length > 0 ? tableIds.join(',') : ''\n\n  const router = useRouter()\n  // Optional chat-runtime read — when present and mode='embed' the\n  // search-result row click short-circuits to a new-tab open against\n  // the absolutized URL. Null/host preserves today's behavior.\n  // Also used as the proxy-prefix fallback for `searchEndpoint`, matching\n  // how tickets resolves `findTicketUrl`.\n  const runtime = useChatRuntime()\n  const resolvedSearchEndpoint =\n    searchEndpoint ?? runtime?.endpoints.docsSearchUrl ?? '/api/docs/search'\n  // Content-href seam, same prop → runtime → none chain. Threading it into\n  // `resolveSearchResultAction` is what makes the dropdown obey the host's\n  // route map; without it the row navigates to the RAG's canonical hub URL\n  // even when the host serves that content in-app under a different path.\n  const resolvedComposeContentUrl = composeContentUrl ?? runtime?.composeContentUrl\n\n  const [query, setQuery] = useState('')\n  const [results, setResults] = useState<SearchResult[]>([])\n  const [isFetching, setIsFetching] = useState(false)\n  const debouncedQuery = useDebounce(query, 300)\n\n  useEffect(() => {\n    if (!debouncedQuery || debouncedQuery.trim().length < 2) {\n      setResults([])\n      setIsFetching(false)\n      return\n    }\n\n    let cancelled = false\n\n    async function fetchResults() {\n      setIsFetching(true)\n      try {\n        const params = new URLSearchParams({\n          q: debouncedQuery,\n          source,\n          limit: '10',\n        })\n        if (tableIdsKey) params.set('tableIds', tableIdsKey)\n\n        const response = await contentFetch(`${resolvedSearchEndpoint}?${params.toString()}`)\n        if (!response.ok) {\n          throw new Error(`Search request failed: ${response.status}`)\n        }\n\n        const json = await response.json()\n\n        if (!cancelled && json.success && Array.isArray(json.data)) {\n          const mapped = mapDocSearchResults(json.data as DocSearchResult[])\n          setResults(mapped)\n        }\n      } catch (error) {\n        console.error('Doc search error:', error)\n        if (!cancelled) {\n          setResults([])\n        }\n      } finally {\n        if (!cancelled) {\n          setIsFetching(false)\n        }\n      }\n    }\n\n    fetchResults()\n\n    return () => {\n      cancelled = true\n    }\n  }, [debouncedQuery, source, tableIdsKey, resolvedSearchEndpoint])\n\n  // Derived loading state — single source of truth for \"should the\n  // dropdown show 'Loading...' instead of 'No results found'\":\n  const isLoading =\n    query.trim().length >= 2 && (query !== debouncedQuery || isFetching)\n\n  // Track whether dropdown should stay open (external link opened in new tab).\n  const [keepOpen, setKeepOpen] = useState(false)\n\n  const handleResultSelect = useCallback(\n    (\n      result: SearchResult,\n      modifiers?: {\n        metaKey?: boolean\n        ctrlKey?: boolean\n        shiftKey?: boolean\n        altKey?: boolean\n        button?: number\n      },\n    ) => {\n      const action = resolveSearchResultAction(\n        result,\n        source,\n        runtime?.navigation.mode,\n        resolvedComposeContentUrl,\n      )\n      // Modifier / non-primary mouse click → force new tab regardless of\n      // same-tab/new-tab decision. The dropdown row is a `<div>`, not an\n      // `<a target=\"_blank\">`, so the browser doesn't background-tab\n      // natively on cmd-click. Honor it explicitly here for parity with\n      // the anchor-based surfaces (cards, chips, related-content). Plain\n      // Enter from the keyboard passes `modifiers === undefined`.\n      const wantsNewTab =\n        modifiers &&\n        (modifiers.metaKey ||\n          modifiers.ctrlKey ||\n          modifiers.shiftKey ||\n          modifiers.altKey ||\n          (typeof modifiers.button === 'number' && modifiers.button !== 0))\n      switch (action.kind) {\n        case 'navigate-same-tab': {\n          if (wantsNewTab) {\n            setKeepOpen(true)\n            window.open(action.href, '_blank', NEW_TAB_FEATURES)\n            return\n          }\n          // Same-origin click:\n          //   1. If the href is under the current doc-tree's baseRoute AND\n          //      an `onInPageSwap` callback is wired AND returns true →\n          //      consider in-page swap handled.\n          //   2. Otherwise → embed-shim `router.push()` (soft RSC nav on\n          //      Next.js hosts, window.location.assign on bare hosts).\n          setKeepOpen(false)\n          const path =\n            baseRoute && action.href.startsWith(`${baseRoute}/`)\n              ? action.href.slice(baseRoute.length + 1)\n              : null\n          if (path && onInPageSwap?.(path)) return\n          router.push(stripSameOriginToPath(action.href))\n          return\n        }\n        case 'navigate-new-tab':\n          // Cross-origin (e.g. clicking a flamingo.run release from\n          // product-hub) — open in a new tab. Keep dropdown open so the\n          // user can pick another result without re-searching.\n          setKeepOpen(true)\n          // Embed-mode short-circuit — the row was clicked while the panel is\n          // hosted inside an embedding app, so absolutize against the\n          // embedder-supplied content origin and honor its `openExternal`\n          // override. Not the only path an embed host takes: a composed\n          // RELATIVE href resolves to `navigate-same-tab` above and never\n          // reaches `decideNewTab`, which is what lets an embedder keep its\n          // own in-app routes in-app.\n          if (runtime?.navigation.mode === 'embed') {\n            const targetPlatform =\n              (result.metadata?.targetPlatform as string | null | undefined) ?? null\n            resolveExternalNavigation({\n              href: action.href,\n              targetPlatform,\n              runtime,\n            }).open()\n            return\n          }\n          window.open(action.href, '_blank', NEW_TAB_FEATURES)\n          return\n        case 'ask-ai':\n          // Row is searchable-but-not-openable (cap_table positions,\n          // financial-kpi snapshots, anything backed by\n          // `resolveUrl: () => null`). Dispatch a CustomEvent that\n          // GlobalAskAI listens for — opens chat + drills via\n          // `entityIdFilter` (primary-key only, same as inline-card Ask).\n          setKeepOpen(false)\n          window.dispatchEvent(\n            new CustomEvent('ask-ai:open-with-ref', { detail: action.detail }),\n          )\n          return\n        case 'route':\n          // Final fallback: legacy navigation by path. Hits when a row\n          // has neither URL nor pk metadata — a mapper/API regression.\n          setKeepOpen(false)\n          onNavigate(action.path)\n          return\n        case 'noop':\n          return\n      }\n    },\n    [\n      onNavigate,\n      source,\n      baseRoute,\n      router,\n      onInPageSwap,\n      runtime,\n      resolvedComposeContentUrl,\n    ],\n  )\n\n  // Reset keepOpen when query changes.\n  useEffect(() => {\n    setKeepOpen(false)\n  }, [query])\n\n  return {\n    query,\n    setQuery,\n    results,\n    isLoading,\n    handleResultSelect,\n    keepDropdownOpen: keepOpen,\n  }\n}\n"]}