{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-TDA355L7.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-TDA355L7.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.map(seg => seg.charAt(0).toUpperCase() + seg.slice(1).replace(/-/g, ' ')).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({ result, isHighlighted }: 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 w-full min-w-0 items-center gap-3\">\n      <span className=\"flex-shrink-0 text-ods-text-secondary\" title={iconLabel}>\n        <SourceIcon className=\"size-4\" />\n      </span>\n      <div className=\"min-w-0 flex-1\">\n        <div className={`truncate text-h6 ${isHighlighted ? 'text-ods-accent' : 'text-ods-text-primary'}`}>\n          {result.title || result.path}\n        </div>\n        {!isGroup && result.path?.includes('/') && (\n          <div className=\"mt-0.5 truncate text-ods-text-secondary text-h6\">{formatRelativePath(result.path)}</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) => <DocSearchResultRow result={result} isHighlighted={isHighlighted} />)\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<{ type: 'entity'; repo: string } | { type: 'doc'; doc: DocSearchResult }> = [];\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      // An 'entity' entry is only pushed AFTER its group lands in the map, so\n      // a miss is unreachable — but dropping one group beats throwing away the\n      // whole search dropdown if that ever stops holding.\n      const rows = entityGroups.get(entry.repo);\n      if (!rows || rows.length === 0) continue;\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 ? { targetPlatform: doc.targetPlatform } : {}),\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 { ComposeContentUrl } from '../../../utils/content-href';\nimport type { ChatRef } from '../../chat/chat-ref.types';\nimport { decideNewTab } from '../../chat/utils/decide-new-tab';\nimport type { SearchResult } from '../../ui/search-input';\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 ? { kind: 'navigate-new-tab', href } : { 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 { useChatRuntime } from '../../../contexts/chat-runtime-context';\nimport { useRouter } from '../../../embed-shims';\nimport { useDebounce } from '../../../hooks/ui/use-debounce';\nimport type { ComposeContentUrl } from '../../../utils/content-href';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\nimport {\n  resolveExternalNavigation,\n  stripSameOriginToPath,\n  NEW_TAB_FEATURES,\n} from '../../chat/utils/chat-nav-resolution';\nimport type { SearchResult } from '../../ui/search-input';\nimport { mapDocSearchResults } from './map-doc-search-results';\nimport { resolveSearchResultAction } from './resolve-search-result-action';\nimport type { DocSearchResult } from './types';\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\n/** One shared empty array, so \"no matches\" keeps a stable identity. */\nconst NO_RESULTS: SearchResult[] = [];\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null;\n}\n\n/** `Array.isArray` narrows `unknown` to `any[]`; this keeps the elements `unknown`. */\nfunction isUnknownArray(value: unknown): value is readonly unknown[] {\n  return Array.isArray(value);\n}\n\nfunction readString(source: Record<string, unknown>, key: string): string | undefined {\n  const value = source[key];\n  return typeof value === 'string' ? value : undefined;\n}\n\n/**\n * Decode `/api/docs/search`'s `{ success, data }` envelope.\n *\n * `Response.json()` is typed `any`, so the rows used to reach\n * `mapDocSearchResults` through an `as DocSearchResult[]` that asserted a\n * shape nothing had checked. `path` and `name` are the two that bite: the\n * mapper uses `path` as the row's React `id` and `name` as its title, so a row\n * missing either renders as a blank, key-colliding entry in the dropdown.\n * Such rows are dropped; the rest of the result set still lists.\n *\n * Returns null when the envelope itself is not a successful result set.\n */\nfunction toDocSearchResults(payload: unknown): DocSearchResult[] | null {\n  if (!isRecord(payload) || !payload.success || !isUnknownArray(payload.data)) return null;\n\n  const rows: DocSearchResult[] = [];\n  for (const entry of payload.data) {\n    if (!isRecord(entry)) continue;\n    const path = readString(entry, 'path');\n    const name = readString(entry, 'name');\n    const type = entry.type;\n    if (path === undefined || name === undefined) continue;\n\n    const targetPlatform = entry.targetPlatform;\n    rows.push({\n      path,\n      name,\n      type: type === 'folder' ? 'folder' : 'file',\n      snippet: readString(entry, 'snippet') ?? '',\n      matchType: entry.matchType === 'title' ? 'title' : 'content',\n      documentType: readString(entry, 'documentType'),\n      externalUrl: readString(entry, 'externalUrl'),\n      targetPlatform: typeof targetPlatform === 'string' ? targetPlatform : null,\n      sourceRepo: readString(entry, 'sourceRepo'),\n      entityId: readString(entry, 'entityId'),\n    });\n  }\n  return rows;\n}\n\nexport function useDocSearch(config: UseDocSearchConfig) {\n  const { source, baseRoute, onNavigate, tableIds, onInPageSwap, searchEndpoint, composeContentUrl } = 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 = 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[]>(NO_RESULTS);\n  const [isFetching, setIsFetching] = useState(false);\n  const debouncedQuery = useDebounce(query, 300);\n\n  // Falling below the two-character floor empties the dropdown. Adjusted while\n  // rendering (React's documented pattern for a reset the render already has\n  // the answer to) rather than from an effect, which committed one more frame\n  // of the previous matches under a query that no longer searches for them.\n  const searchable = debouncedQuery.trim().length >= 2;\n  const [wasSearchable, setWasSearchable] = useState(searchable);\n  if (wasSearchable !== searchable) {\n    setWasSearchable(searchable);\n    if (!searchable) {\n      setResults(NO_RESULTS);\n      setIsFetching(false);\n    }\n  }\n\n  useEffect(() => {\n    if (!searchable) return undefined;\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: unknown = await response.json();\n\n        if (!cancelled) {\n          // An envelope that is not a successful result set now clears the\n          // dropdown instead of leaving it untouched. Previously a 200 that\n          // carried `success: false` (or a non-array `data`) fell through\n          // every branch, so the PREVIOUS query's matches stayed on screen\n          // under the new query — the throw path already reset them.\n          const rows = toDocSearchResults(json);\n          setResults(rows ? mapDocSearchResults(rows) : NO_RESULTS);\n        }\n      } catch (error) {\n        console.error('Doc search error:', error);\n        if (!cancelled) {\n          setResults(NO_RESULTS);\n        }\n      } finally {\n        if (!cancelled) {\n          setIsFetching(false);\n        }\n      }\n    }\n\n    // Never rejects — try/catch/finally, every write gated on `cancelled`.\n    void fetchResults();\n\n    return () => {\n      cancelled = true;\n    };\n  }, [debouncedQuery, searchable, 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 = 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(result, source, runtime?.navigation.mode, resolvedComposeContentUrl);\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}/`) ? action.href.slice(baseRoute.length + 1) : 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 = (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(new CustomEvent('ask-ai:open-with-ref', { detail: action.detail }));\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    [onNavigate, source, baseRoute, router, onInPageSwap, runtime, resolvedComposeContentUrl],\n  );\n\n  // Reset keepOpen when query changes. Adjusted while rendering, not from an\n  // effect: the dropdown's open state is read from this in the same render the\n  // keystroke produced, so an effect held it pinned open for one extra frame.\n  const [keepOpenForQuery, setKeepOpenForQuery] = useState(query);\n  if (keepOpenForQuery !== query) {\n    setKeepOpenForQuery(query);\n    setKeepOpen(false);\n  }\n\n  return {\n    query,\n    setQuery,\n    results,\n    isLoading,\n    handleResultSelect,\n    keepDropdownOpen: keepOpen,\n  };\n}\n"]}