@flamingo-stack/openframe-frontend-core 0.0.685 → 0.0.686-2464.7467.1

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.
Files changed (188) hide show
  1. package/dist/{chunk-T6KWNJDW.js → chunk-22RSJ57Z.js} +470 -465
  2. package/dist/chunk-22RSJ57Z.js.map +1 -0
  3. package/dist/{chunk-K2HZG4RM.cjs → chunk-2OD2PPNO.cjs} +32 -32
  4. package/dist/{chunk-K2HZG4RM.cjs.map → chunk-2OD2PPNO.cjs.map} +1 -1
  5. package/dist/{chunk-WJK2EETW.js → chunk-3NBKFWHG.js} +3 -3
  6. package/dist/{chunk-B25CF3PO.cjs → chunk-3XZHZ4LD.cjs} +43 -43
  7. package/dist/{chunk-B25CF3PO.cjs.map → chunk-3XZHZ4LD.cjs.map} +1 -1
  8. package/dist/{chunk-7XF63THA.js → chunk-637VBBHC.js} +8 -8
  9. package/dist/{chunk-O7LOZW4R.js → chunk-6MDNGGCJ.js} +7 -7
  10. package/dist/{chunk-GXV6UEVY.js → chunk-74TJJQMT.js} +3 -3
  11. package/dist/{chunk-PSQKD6ZN.js → chunk-BOIYZ2L6.js} +5 -5
  12. package/dist/{chunk-YCTVLN47.cjs → chunk-CCLO5R5A.cjs} +17 -17
  13. package/dist/{chunk-YCTVLN47.cjs.map → chunk-CCLO5R5A.cjs.map} +1 -1
  14. package/dist/{chunk-QHLJSB2J.cjs → chunk-CJNYQ6FK.cjs} +14 -14
  15. package/dist/{chunk-QHLJSB2J.cjs.map → chunk-CJNYQ6FK.cjs.map} +1 -1
  16. package/dist/{chunk-IJUQQTMV.cjs → chunk-D2XLLWIU.cjs} +4 -2
  17. package/dist/chunk-D2XLLWIU.cjs.map +1 -0
  18. package/dist/{chunk-U3IXG7TR.cjs → chunk-GQ4FKLFC.cjs} +43 -43
  19. package/dist/{chunk-U3IXG7TR.cjs.map → chunk-GQ4FKLFC.cjs.map} +1 -1
  20. package/dist/{chunk-TUG7IT6H.js → chunk-HLEXYTOE.js} +2 -2
  21. package/dist/{chunk-EFBMNVSH.cjs → chunk-HSWEUDKR.cjs} +15 -15
  22. package/dist/{chunk-EFBMNVSH.cjs.map → chunk-HSWEUDKR.cjs.map} +1 -1
  23. package/dist/{chunk-C3B36OYZ.cjs → chunk-IQDUMR3K.cjs} +5 -5
  24. package/dist/{chunk-C3B36OYZ.cjs.map → chunk-IQDUMR3K.cjs.map} +1 -1
  25. package/dist/{chunk-XF5TJ4P2.cjs → chunk-IWJEJBML.cjs} +12 -12
  26. package/dist/{chunk-XF5TJ4P2.cjs.map → chunk-IWJEJBML.cjs.map} +1 -1
  27. package/dist/{chunk-XGX6JSEU.js → chunk-IXLO6K3T.js} +2087 -1645
  28. package/dist/chunk-IXLO6K3T.js.map +1 -0
  29. package/dist/{chunk-CMQXPD5C.cjs → chunk-J5O5KIHL.cjs} +28 -28
  30. package/dist/{chunk-CMQXPD5C.cjs.map → chunk-J5O5KIHL.cjs.map} +1 -1
  31. package/dist/{chunk-UT333EOT.js → chunk-JA6NYUF3.js} +6 -6
  32. package/dist/{chunk-65KTXRBY.js → chunk-JTC4RPS3.js} +6 -6
  33. package/dist/{chunk-DWXWEGGO.cjs → chunk-KBH7EWFW.cjs} +26 -26
  34. package/dist/{chunk-DWXWEGGO.cjs.map → chunk-KBH7EWFW.cjs.map} +1 -1
  35. package/dist/{chunk-B32YLOYQ.js → chunk-LMZW2KZF.js} +5 -5
  36. package/dist/{chunk-GHLGXFNU.js → chunk-NMFBC7CC.js} +3 -3
  37. package/dist/{chunk-VDPCK437.js → chunk-NRH4IHI4.js} +23 -7
  38. package/dist/{chunk-VDPCK437.js.map → chunk-NRH4IHI4.js.map} +1 -1
  39. package/dist/{chunk-B5N3AHPG.cjs → chunk-OWSFP7LX.cjs} +90 -90
  40. package/dist/{chunk-B5N3AHPG.cjs.map → chunk-OWSFP7LX.cjs.map} +1 -1
  41. package/dist/{chunk-5LJZPJHX.cjs → chunk-P2AE46BQ.cjs} +64 -64
  42. package/dist/{chunk-5LJZPJHX.cjs.map → chunk-P2AE46BQ.cjs.map} +1 -1
  43. package/dist/{chunk-Y7ZYOQNB.js → chunk-PLLY62XF.js} +3 -3
  44. package/dist/{chunk-HMXUOLZR.cjs → chunk-QIYQMEP3.cjs} +25 -25
  45. package/dist/{chunk-HMXUOLZR.cjs.map → chunk-QIYQMEP3.cjs.map} +1 -1
  46. package/dist/{chunk-DQOQ3KTP.cjs → chunk-QNWQQ4RT.cjs} +78 -73
  47. package/dist/chunk-QNWQQ4RT.cjs.map +1 -0
  48. package/dist/{chunk-P2ASEPA2.cjs → chunk-RHBJIFAJ.cjs} +9 -9
  49. package/dist/{chunk-P2ASEPA2.cjs.map → chunk-RHBJIFAJ.cjs.map} +1 -1
  50. package/dist/{chunk-T4DUTP5C.cjs → chunk-RMNWN5RC.cjs} +5 -5
  51. package/dist/{chunk-T4DUTP5C.cjs.map → chunk-RMNWN5RC.cjs.map} +1 -1
  52. package/dist/{chunk-XW7MUIW4.js → chunk-S6C5GBQ4.js} +4 -4
  53. package/dist/{chunk-BZ2XOEHW.js → chunk-SFYBD456.js} +4 -4
  54. package/dist/{chunk-A3CSQB4A.cjs → chunk-SIAGFCKP.cjs} +1299 -857
  55. package/dist/chunk-SIAGFCKP.cjs.map +1 -0
  56. package/dist/{chunk-TJI5V5BR.cjs → chunk-TV6YNAVH.cjs} +7 -7
  57. package/dist/{chunk-TJI5V5BR.cjs.map → chunk-TV6YNAVH.cjs.map} +1 -1
  58. package/dist/{chunk-ZF4Z36G4.js → chunk-U5LNGS5P.js} +9 -9
  59. package/dist/{chunk-N4Z6X5BV.js → chunk-VAHO2DTJ.js} +6 -6
  60. package/dist/{chunk-JNUL3ZWK.js → chunk-WYB27NKC.js} +5 -3
  61. package/dist/chunk-WYB27NKC.js.map +1 -0
  62. package/dist/{chunk-M4HYIXP6.cjs → chunk-XML4CLL3.cjs} +471 -455
  63. package/dist/chunk-XML4CLL3.cjs.map +1 -0
  64. package/dist/{chunk-V5REIMSE.js → chunk-ZQDJPLRS.js} +2 -2
  65. package/dist/components/case-studies/index.cjs +19 -19
  66. package/dist/components/case-studies/index.js +6 -6
  67. package/dist/components/chat/ai-assistant-info.d.ts.map +1 -1
  68. package/dist/components/chat/approval-request-message.d.ts.map +1 -1
  69. package/dist/components/chat/chat-appearance-context.d.ts +9 -0
  70. package/dist/components/chat/chat-appearance-context.d.ts.map +1 -0
  71. package/dist/components/chat/chat-input.d.ts.map +1 -1
  72. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  73. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  74. package/dist/components/chat/chat-nav-sidebar.d.ts +50 -0
  75. package/dist/components/chat/chat-nav-sidebar.d.ts.map +1 -0
  76. package/dist/components/chat/chat-top-navigation.d.ts +29 -0
  77. package/dist/components/chat/chat-top-navigation.d.ts.map +1 -0
  78. package/dist/components/chat/error-message-display.d.ts.map +1 -1
  79. package/dist/components/chat/index.cjs +22 -4
  80. package/dist/components/chat/index.cjs.map +1 -1
  81. package/dist/components/chat/index.d.ts +6 -0
  82. package/dist/components/chat/index.d.ts.map +1 -1
  83. package/dist/components/chat/index.js +21 -3
  84. package/dist/components/chat/msp-organization-card.d.ts +7 -1
  85. package/dist/components/chat/msp-organization-card.d.ts.map +1 -1
  86. package/dist/components/chat/onboarding-carousel.d.ts +29 -0
  87. package/dist/components/chat/onboarding-carousel.d.ts.map +1 -0
  88. package/dist/components/chat/system-event-message.d.ts +14 -0
  89. package/dist/components/chat/system-event-message.d.ts.map +1 -0
  90. package/dist/components/chat/types/chat.types.d.ts +13 -0
  91. package/dist/components/chat/types/chat.types.d.ts.map +1 -1
  92. package/dist/components/chat/types/component.types.d.ts +20 -1
  93. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  94. package/dist/components/contact/index.cjs +7 -7
  95. package/dist/components/contact/index.js +6 -6
  96. package/dist/components/docs/index.cjs +10 -10
  97. package/dist/components/docs/index.js +9 -9
  98. package/dist/components/embeds/index.cjs +7 -7
  99. package/dist/components/embeds/index.js +6 -6
  100. package/dist/components/faq/index.cjs +7 -7
  101. package/dist/components/faq/index.js +6 -6
  102. package/dist/components/features/index.cjs +6 -6
  103. package/dist/components/features/index.js +5 -5
  104. package/dist/components/help-center-pages/index.cjs +100 -100
  105. package/dist/components/help-center-pages/index.js +17 -17
  106. package/dist/components/index.cjs +79 -61
  107. package/dist/components/index.cjs.map +1 -1
  108. package/dist/components/index.js +35 -17
  109. package/dist/components/index.js.map +1 -1
  110. package/dist/components/meeting-scheduler/index.cjs +48 -48
  111. package/dist/components/meeting-scheduler/index.js +5 -5
  112. package/dist/components/navigation/index.cjs +9 -9
  113. package/dist/components/navigation/index.js +8 -8
  114. package/dist/components/onboarding-guides/index.cjs +9 -9
  115. package/dist/components/onboarding-guides/index.js +8 -8
  116. package/dist/components/related-content/index.cjs +3 -3
  117. package/dist/components/related-content/index.js +2 -2
  118. package/dist/components/tickets/index.cjs +10 -10
  119. package/dist/components/tickets/index.js +9 -9
  120. package/dist/components/tool-icon.d.ts.map +1 -1
  121. package/dist/components/ui/avatar-stack.d.ts +5 -1
  122. package/dist/components/ui/avatar-stack.d.ts.map +1 -1
  123. package/dist/components/ui/index.cjs +24 -6
  124. package/dist/components/ui/index.cjs.map +1 -1
  125. package/dist/components/ui/index.js +23 -5
  126. package/dist/components/ui/reopen-ticket-request-modal.d.ts +4 -1
  127. package/dist/components/ui/reopen-ticket-request-modal.d.ts.map +1 -1
  128. package/dist/components/ui/tab-navigation.d.ts +7 -0
  129. package/dist/components/ui/tab-navigation.d.ts.map +1 -1
  130. package/dist/hooks/index.cjs +3 -3
  131. package/dist/hooks/index.js +2 -2
  132. package/dist/index.cjs +30 -12
  133. package/dist/index.cjs.map +1 -1
  134. package/dist/index.js +27 -9
  135. package/dist/index.js.map +1 -1
  136. package/package.json +1 -1
  137. package/src/components/chat/__tests__/chat-appearance.test.tsx +79 -0
  138. package/src/components/chat/__tests__/onboarding-carousel.test.tsx +56 -0
  139. package/src/components/chat/ai-assistant-info.tsx +7 -1
  140. package/src/components/chat/approval-request-message.tsx +6 -1
  141. package/src/components/chat/chat-appearance-context.tsx +15 -0
  142. package/src/components/chat/chat-input.tsx +26 -1
  143. package/src/components/chat/chat-message-enhanced.tsx +180 -142
  144. package/src/components/chat/chat-message-list.tsx +169 -164
  145. package/src/components/chat/chat-nav-sidebar.tsx +163 -0
  146. package/src/components/chat/chat-top-navigation.tsx +109 -0
  147. package/src/components/chat/error-message-display.tsx +21 -3
  148. package/src/components/chat/index.ts +6 -0
  149. package/src/components/chat/msp-organization-card.tsx +35 -0
  150. package/src/components/chat/onboarding-carousel.tsx +162 -0
  151. package/src/components/chat/system-event-message.tsx +54 -0
  152. package/src/components/chat/types/chat.types.ts +15 -0
  153. package/src/components/chat/types/component.types.ts +20 -0
  154. package/src/components/tool-icon.tsx +4 -2
  155. package/src/components/ui/__tests__/avatar-stack.test.tsx +25 -0
  156. package/src/components/ui/__tests__/tab-navigation.test.tsx +56 -2
  157. package/src/components/ui/avatar-stack.tsx +15 -5
  158. package/src/components/ui/reopen-ticket-request-modal.tsx +6 -2
  159. package/src/components/ui/tab-navigation.tsx +23 -0
  160. package/src/stories/FaeLayoutV2.stories.tsx +150 -0
  161. package/src/stories/FaeOnboardingV2.stories.tsx +68 -0
  162. package/src/stories/FaeThreadV2.stories.tsx +192 -0
  163. package/src/stories/fae-brand-decorator.tsx +28 -0
  164. package/src/styles/ods-colors.css +10 -0
  165. package/src/styles/ods-effects.css +13 -0
  166. package/dist/chunk-A3CSQB4A.cjs.map +0 -1
  167. package/dist/chunk-DQOQ3KTP.cjs.map +0 -1
  168. package/dist/chunk-IJUQQTMV.cjs.map +0 -1
  169. package/dist/chunk-JNUL3ZWK.js.map +0 -1
  170. package/dist/chunk-M4HYIXP6.cjs.map +0 -1
  171. package/dist/chunk-T6KWNJDW.js.map +0 -1
  172. package/dist/chunk-XGX6JSEU.js.map +0 -1
  173. /package/dist/{chunk-WJK2EETW.js.map → chunk-3NBKFWHG.js.map} +0 -0
  174. /package/dist/{chunk-7XF63THA.js.map → chunk-637VBBHC.js.map} +0 -0
  175. /package/dist/{chunk-O7LOZW4R.js.map → chunk-6MDNGGCJ.js.map} +0 -0
  176. /package/dist/{chunk-GXV6UEVY.js.map → chunk-74TJJQMT.js.map} +0 -0
  177. /package/dist/{chunk-PSQKD6ZN.js.map → chunk-BOIYZ2L6.js.map} +0 -0
  178. /package/dist/{chunk-TUG7IT6H.js.map → chunk-HLEXYTOE.js.map} +0 -0
  179. /package/dist/{chunk-UT333EOT.js.map → chunk-JA6NYUF3.js.map} +0 -0
  180. /package/dist/{chunk-65KTXRBY.js.map → chunk-JTC4RPS3.js.map} +0 -0
  181. /package/dist/{chunk-B32YLOYQ.js.map → chunk-LMZW2KZF.js.map} +0 -0
  182. /package/dist/{chunk-GHLGXFNU.js.map → chunk-NMFBC7CC.js.map} +0 -0
  183. /package/dist/{chunk-Y7ZYOQNB.js.map → chunk-PLLY62XF.js.map} +0 -0
  184. /package/dist/{chunk-XW7MUIW4.js.map → chunk-S6C5GBQ4.js.map} +0 -0
  185. /package/dist/{chunk-BZ2XOEHW.js.map → chunk-SFYBD456.js.map} +0 -0
  186. /package/dist/{chunk-ZF4Z36G4.js.map → chunk-U5LNGS5P.js.map} +0 -0
  187. /package/dist/{chunk-N4Z6X5BV.js.map → chunk-VAHO2DTJ.js.map} +0 -0
  188. /package/dist/{chunk-V5REIMSE.js.map → chunk-ZQDJPLRS.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-QHLJSB2J.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-QHLJSB2J.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 { recordCountLabel } from '../../../utils/source-grouping';\nimport { getSourceLabel } from '../../../utils/source-icons';\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 * A SEARCH rule, deliberately narrower than the chat strip's `groupsByTable`:\n * a search result is a destination, so only rows that share one are folded.\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\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 results.push({\n id: `group-${entry.repo}`,\n // Same table label and count as the chat's grouped chip: one owner each.\n title: `${getSourceLabel(entry.repo)} ${recordCountLabel(rows.length)}`,\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"]}
1
+ {"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-CJNYQ6FK.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-CJNYQ6FK.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 { recordCountLabel } from '../../../utils/source-grouping';\nimport { getSourceLabel } from '../../../utils/source-icons';\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 * A SEARCH rule, deliberately narrower than the chat strip's `groupsByTable`:\n * a search result is a destination, so only rows that share one are folded.\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\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 results.push({\n id: `group-${entry.repo}`,\n // Same table label and count as the chat's grouped chip: one owner each.\n title: `${getSourceLabel(entry.repo)} ${recordCountLabel(rows.length)}`,\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"]}
@@ -282,7 +282,9 @@ var toolIconMap = {
282
282
  [ToolTypeValues.AUTHENTIK]: (size, className) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkYNL2NHXHcjs.AuthentikLogoGreyIcon, { size, className }),
283
283
  [ToolTypeValues.OSQUERY]: (size, className) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkYNL2NHXHcjs.OsqueryLogoGreyIcon, { size, className }),
284
284
  [ToolTypeValues.SYSTEM]: () => null,
285
- [ToolTypeValues.MICROSOFT_365]: (size, className) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkYNL2NHXHcjs.MicrosoftLogoGreyIcon, { size, className }),
285
+ // The Microsoft 365 tool wears the Office 365 mark, not the Microsoft company logo: the product's
286
+ // own brand is what the logs and the directory pickers name.
287
+ [ToolTypeValues.MICROSOFT_365]: (size, className) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkYNL2NHXHcjs.Office365LogoGreyIcon, { size, className }),
286
288
  [ToolTypeValues.GOOGLE_WORKSPACE]: (size, className) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkYNL2NHXHcjs.GoogleIcon, { size, className })
287
289
  };
288
290
  var ToolIcon = ({ toolType, size = 16, className }) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _jsxruntime.Fragment, { children: _nullishCoalesce(_optionalChain([toolIconMap, 'access', _7 => _7[toolType], 'optionalCall', _8 => _8(size, className)]), () => ( null)) });
@@ -784,4 +786,4 @@ function navigateSamePageHash(target, options = {}) {
784
786
 
785
787
 
786
788
  exports.useIsTruncated = useIsTruncated; exports.useDebounce = useDebounce; exports.useIsomorphicLayoutEffect = useIsomorphicLayoutEffect; exports.useMediaQuery = useMediaQuery; exports.breakpoints = breakpoints; exports.useSmUp = useSmUp; exports.useMdUp = useMdUp; exports.useLgUp = useLgUp; exports.useHeaderHeight = useHeaderHeight; exports.useMarqueeEngine = useMarqueeEngine; exports.useResetOnPageHidden = useResetOnPageHidden; exports.useSuppressCloneFocus = useSuppressCloneFocus; exports.ToolTypeValues = ToolTypeValues; exports.toolLabels = toolLabels; exports.ToolIcon = ToolIcon; exports.cn = cn; exports.errorMessage = errorMessage; exports.apiErrorMessage = apiErrorMessage; exports.delay = delay; exports.generateRandomString = generateRandomString; exports.truncateString = truncateString; exports.serializeJsonLd = serializeJsonLd; exports.deepClone = deepClone; exports.getSlackCommunityJoinUrl = getSlackCommunityJoinUrl; exports.clamp = clamp; exports.pick = pick; exports.NO_CLIENT_CACHE = NO_CLIENT_CACHE; exports.NEAR_VIEWPORT_ROOT_MARGIN = NEAR_VIEWPORT_ROOT_MARGIN; exports.useNearViewport = useNearViewport; exports.isBlankLine = isBlankLine; exports.createFenceTracker = createFenceTracker; exports.HEADING_EMOJI_RE = HEADING_EMOJI_RE; exports.stripHeadingEmojis = stripHeadingEmojis; exports.slugifyHeadingBase = slugifyHeadingBase; exports.slugifyHeadingText = slugifyHeadingText; exports.scanHeadings = scanHeadings; exports.createHeadingIdDeduper = createHeadingIdDeduper; exports.normalizeAnchorId = normalizeAnchorId; exports.findAnchorElementByNormalizedId = findAnchorElementByNormalizedId; exports.getScrollableAncestor = getScrollableAncestor; exports.scrollElementIntoView = scrollElementIntoView; exports.STICKY_HEADER_OFFSET_PX = STICKY_HEADER_OFFSET_PX; exports.HUB_HEADER_OFFSET_PX = HUB_HEADER_OFFSET_PX; exports.normalizeHashFragment = normalizeHashFragment; exports.getHashTargetElement = getHashTargetElement; exports.navigateSamePageHash = navigateSamePageHash;
787
- //# sourceMappingURL=chunk-IJUQQTMV.cjs.map
789
+ //# sourceMappingURL=chunk-D2XLLWIU.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-D2XLLWIU.cjs","../src/hooks/ui/use-is-truncated.ts","../src/hooks/ui/use-debounce.ts","../src/hooks/ui/use-isomorphic-layout-effect.ts","../src/hooks/ui/use-media-query.ts","../src/hooks/ui/use-header-height.ts","../src/hooks/ui/use-marquee-engine.ts","../src/hooks/ui/use-reset-on-page-hidden.ts","../src/hooks/ui/use-suppress-clone-focus.ts","../src/types/tool.types.ts","../src/components/tool-icon.tsx","../src/utils/common.ts","../src/hooks/use-near-viewport.ts","../src/utils/markdown-fences.ts","../src/utils/markdown-heading-id.ts","../src/utils/anchor-id.ts","../src/utils/scroll-into-view.ts","../src/utils/same-page-hash-nav.ts"],"names":["delay","useState","useEffect","header","bar","useCallback","useRef"],"mappings":"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AACN,EAAA;AAEA,EAAA;AACM,IAAA;AACN,EAAA;AAIO,EAAA;AACT;AfueU;AACA;AgB5gBN;AACA;AAEK;AACH,EAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACI,EAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEM;AAWU;AACd,EAAA;AACQ,IAAA;AAEH,IAAA;AAGD,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAOgB;AAIV,EAAA;AACI,EAAA;AAKF,EAAA;AACA,EAAA;AACA,EAAA;AACA,IAAA;AACC,IAAA;AACP,EAAA;AAKM,EAAA;AACE,IAAA;AAKA,IAAA;AACA,IAAA;AAGN,IAAA;AACF,EAAA;AAGA,EAAA;AAEM,EAAA;AAIF,EAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AAGI,EAAA;AACA,EAAA;AAKE,EAAA;AACC,EAAA;AACA,EAAA;AACP,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AAEM,EAAA;AACA,IAAA;AACF,MAAA;AACA,MAAA;AACF,IAAA;AACM,IAAA;AACA,IAAA;AACA,IAAA;AACN,IAAA;AACI,IAAA;AACF,MAAA;AACF,IAAA;AAEE,MAAA;AACA,MAAA;AACI,MAAA;AACF,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AhB0dU;AACA;AiBtpBG;AAIA;AAUG;AACT,EAAA;AACC,EAAA;AACC,EAAA;AACT;AAqBgB;AACL,EAAA;AACH,EAAA;AACF,EAAA;AAEA,EAAA;AACA,EAAA;AACF,IAAA;AACI,IAAA;AACF,MAAA;AACI,MAAA;AACN,IAAA;AACM,EAAA;AAEN,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AAsBgB;AACV,EAAA;AACI,EAAA;AACF,EAAA;AAKF,EAAA;AACA,EAAA;AACI,IAAA;AACA,EAAA;AACN,IAAA;AACF,EAAA;AAEM,EAAA;AAIJ,IAAA;AACF,EAAA;AACM,EAAA;AAGA,EAAA;AACF,EAAA;AACF,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACM,EAAA;AACA,EAAA;AAED,EAAA;AACD,EAAA;AACI,IAAA;AACF,IAAA;AACF,MAAA;AACF,IAAA;AACE,MAAA;AACF,IAAA;AAGA,IAAA;AACM,MAAA;AACF,QAAA;AACA,QAAA;AACD,MAAA;AACH,IAAA;AACF,EAAA;AACM,EAAA;AACF,EAAA;AACF,IAAA;AACF,EAAA;AAGA,EAAA;AACE,IAAA;AACA,IAAA;AACD,EAAA;AACM,EAAA;AACT;AjBilBU;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-D2XLLWIU.cjs","sourcesContent":[null,"'use client';\n\nimport { useCallback, useEffect, useState } from 'react';\n\nexport interface UseIsTruncatedOptions {\n /** Measure vertical clipping (`line-clamp-N`) instead of the single-line ellipsis. */\n multiline?: boolean;\n}\n\n/**\n * Tracks whether an element's text is ACTUALLY clipped — the condition for\n * offering a \"full value\" tooltip. Text that fits needs no tooltip, and showing\n * one anyway just repeats what the user is already reading.\n *\n * Three things can change the answer, and all three are watched: the box\n * resizing (`ResizeObserver`), the text itself changing, and the measured\n * ELEMENT changing. None of them implies the others — new text inside a\n * width-constrained box leaves the box exactly as it was, so the observer never\n * fires; and a caller that swaps between render branches (adornment vs plain)\n * mounts a different node under the same text, which a `useRef` would leave\n * measured against the old, detached node. Pass the rendered value as `value`.\n *\n * `ref` is a CALLBACK ref, not a `RefObject` — that is what makes the element\n * itself a dependency. Attach it exactly like a normal ref; there is no\n * `.current` to read.\n *\n * ```tsx\n * const { ref, isTruncated } = useIsTruncated<HTMLHeadingElement>(title)\n * <FloatingTooltip content={title} disabled={!isTruncated}>\n * <h1 ref={ref} className=\"truncate\">{title}</h1>\n * </FloatingTooltip>\n * ```\n */\nexport function useIsTruncated<T extends HTMLElement>(value: unknown, options?: UseIsTruncatedOptions) {\n const multiline = options?.multiline ?? false;\n const [element, setElement] = useState<T | null>(null);\n const [isTruncated, setIsTruncated] = useState(false);\n\n // Wrapped so the identity stays stable across renders: passing the raw\n // `useState` setter as a ref would be stable too, but this keeps the public\n // shape a plain `RefCallback<T>` instead of a state dispatcher.\n const ref = useCallback((node: T | null) => setElement(node), []);\n\n useEffect(() => {\n if (!element) return undefined;\n // The 1px tolerance absorbs sub-pixel layout rounding, which otherwise\n // reports a permanent fractional \"overflow\" at some zoom levels / DPRs and\n // leaves a tooltip armed on text that is fully visible.\n const check = () => {\n setIsTruncated(\n multiline ? element.scrollHeight > element.clientHeight + 1 : element.scrollWidth > element.clientWidth + 1,\n );\n };\n check();\n const observer = new ResizeObserver(check);\n observer.observe(element);\n return () => observer.disconnect();\n }, [element, value, multiline]);\n\n // Masked rather than reset from the effect: with no element there is nothing\n // to clip, and deriving that during render avoids a second render pass every\n // time the measured node unmounts.\n return { ref, isTruncated: element ? isTruncated : false };\n}\n","'use client';\n\nimport { useState, useEffect } from 'react';\n\n/**\n * Hook to debounce a value\n * @param value - Value to debounce\n * @param delay - Delay in milliseconds\n * @returns Debounced value\n */\nexport function useDebounce<T>(value: T, delay = 500): T {\n const [debouncedValue, setDebouncedValue] = useState<T>(value);\n\n useEffect(() => {\n // Set up timeout to update debounced value\n const timer = setTimeout(() => {\n setDebouncedValue(value);\n }, delay);\n\n // Clean up timeout on value change or unmount\n return () => {\n clearTimeout(timer);\n };\n }, [value, delay]);\n\n return debouncedValue;\n}\n","'use client';\n\nimport { useEffect, useLayoutEffect } from 'react';\n\n/**\n * `useLayoutEffect` in the browser, `useEffect` on the server.\n *\n * `useLayoutEffect` never runs during server rendering — React cannot measure or\n * mutate a layout that has not been laid out — so calling it in a component that\n * SSRs logs a warning on every render pass. Client components still render on the\n * server in Next.js, so `'use client'` alone does not avoid it.\n *\n * Use this wherever the effect writes something the FIRST paint depends on (a CSS\n * custom property, a scroll position, a measured size). A plain `useEffect` there\n * runs after paint, which shows one frame of the pre-effect value; a plain\n * `useLayoutEffect` gets the timing right and pays for it with the warning. This\n * gets both: paint-blocking where there is a paint, a no-op where there is not.\n */\nexport const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;\n","'use client';\n\nimport { useState } from 'react';\nimport { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';\n\n/**\n * Hook to check if a media query matches\n * @param query - Media query to check\n * @returns Whether the media query matches, or undefined during SSR/initial render\n */\nexport function useMediaQuery(query: string): boolean | undefined {\n const [matches, setMatches] = useState<boolean | undefined>(undefined);\n\n useIsomorphicLayoutEffect(() => {\n const matchMedia = window.matchMedia(query);\n\n const handleChange = () => {\n setMatches(matchMedia.matches);\n };\n\n // Set initial value\n handleChange();\n\n matchMedia.addEventListener('change', handleChange);\n\n return () => {\n matchMedia.removeEventListener('change', handleChange);\n };\n }, [query]);\n\n return matches;\n}\n\n/**\n * Predefined breakpoints for common screen sizes\n */\nexport const breakpoints = {\n md: '(min-width: 800px)', // Tablet: 50rem\n lg: '(min-width: 1280px)', // Desktop: 80rem\n};\n\n/** @deprecated Use useMdUp instead */\nexport function useSmUp(): boolean | undefined {\n return useMdUp();\n}\n\nexport function useMdUp(): boolean | undefined {\n const matches = useMediaQuery(breakpoints.md);\n return matches === undefined ? undefined : matches;\n}\n\nexport function useLgUp(): boolean | undefined {\n const matches = useMediaQuery(breakpoints.lg);\n return matches === undefined ? undefined : matches;\n}\n","'use client';\n\nimport { useEffect, useState } from 'react';\n\n/**\n * Returns the combined height (px) of the sticky page header and announcement\n * bar (if present), updated live via ResizeObserver / MutationObserver.\n * Useful for offsetting fixed/absolute-positioned panels so they don't\n * overlap the header.\n */\nexport function useHeaderHeight(\n // First-paint fallback = the unified top-navigation `big` bar (72px), the\n // size every hub/marketing site renders; replaced by the live measurement.\n defaultHeight = 72,\n options: {\n /**\n * When false, no observers are attached and the default height is\n * returned — for consumers that stay mounted on every page but only need\n * the measurement while an overlay is open (avoids an always-on\n * document-wide MutationObserver + forced reflow).\n * @default true\n */\n enabled?: boolean;\n } = {},\n): number {\n const { enabled = true } = options;\n const [height, setHeight] = useState(defaultHeight);\n\n useEffect(() => {\n if (!enabled) return undefined;\n\n const measure = () => {\n let total = 0;\n const header = document.querySelector('header');\n if (header) total += header.offsetHeight;\n const bar = document.querySelector('[data-announcement-bar]');\n if (bar instanceof HTMLElement) total += bar.offsetHeight;\n setHeight(total > 0 ? total : defaultHeight);\n };\n\n measure();\n\n const resizeObserver = new ResizeObserver(measure);\n const header = document.querySelector('header');\n if (header) resizeObserver.observe(header);\n const bar = document.querySelector('[data-announcement-bar]');\n if (bar) resizeObserver.observe(bar);\n\n const mutationObserver = new MutationObserver(mutations => {\n for (const mutation of mutations) {\n if (mutation.type === 'childList' || mutation.type === 'attributes') {\n measure();\n const newBar = document.querySelector('[data-announcement-bar]');\n if (newBar) resizeObserver.observe(newBar);\n }\n }\n });\n mutationObserver.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['data-announcement-bar'],\n });\n\n return () => {\n resizeObserver.disconnect();\n mutationObserver.disconnect();\n };\n }, [defaultHeight, enabled]);\n\n return height;\n}\n","'use client';\n\nimport { useCallback, useEffect, useRef, type MutableRefObject } from 'react';\n\n/**\n * THE marquee animation core (single source of truth) — extracted verbatim\n * from <CardsStrip> so every endless-scroll surface (card strips, logo strips,\n * quick-action walls, chip marquees) shares ONE rAF position engine (\"unify\n * means delete variations\").\n *\n * The engine is axis- and driver-agnostic: it owns a FLOAT position ref, a\n * velocity envelope, seam wrapping, and an optional chevron-style glide; the\n * caller decides what a position \"means\" via `apply` — writing\n * `scroller.scrollLeft` (CardsStrip: a real scroller the user can also drag)\n * or a `translate` transform (MarqueeWall: decorative tracks, incl. vertical —\n * mandatory inside surfaces that forbid inner scrollers, e.g. the deck's\n * mobile single-fold panels).\n *\n * Engine semantics (do not \"simplify\"):\n * - Position lives in a FLOAT ref, never read-modify-write from the DOM:\n * scrollLeft reads back rounded values, so sub-pixel increments\n * (speed/fps < 1px) get eaten by rounding — stutter + measurably slower\n * than the configured speed.\n * - Velocity envelope (~250ms time constant) eases between 0 and `speed`\n * instead of binary stop/start — pause decelerates, resume accelerates from\n * the current position (GSAP-marquee behavior; hard cuts read as jank).\n * - Glide consumes a signed remaining-distance, wrapping modulo the copy size\n * each frame, so seam warps are invisible to it (browser smooth-scroll\n * animates toward an ABSOLUTE target and lunges after a warp).\n * - `isPaused` is evaluated every frame — the caller owns the pause-reason\n * set (hover, viewport, tab visibility, suppress windows); the engine only\n * turns it into a smooth velocity target.\n */\nexport interface MarqueeEngineOptions {\n /** Master switch — the rAF runs only while true. On (re)activation the\n * position resyncs from `readBack` when provided. */\n active: boolean;\n /** Cruise speed in px/s. */\n speed: number;\n /** Per-frame pause verdict (caller-owned reason set). `now` is the rAF\n * timestamp, for suppress-until comparisons. */\n isPaused: (now: number) => boolean;\n /** Single-copy size in px (the wrap modulus). Return <= 0 to disable\n * wrapping (position advances unwrapped — callers should size tracks so\n * this never happens while active). */\n getWrapSize: () => number;\n /** Lower bound of the wrap range: positions stay in\n * [wrapMin, wrapMin + wrapSize). Default 0. Scroller drivers with USER\n * scrolling pass their seam-warp buffer here so engine-written positions\n * never enter the re-center zones — an exact-edge warp check is racy\n * against the rAF (the engine nudges the scroller off the edge before the\n * scroll handler samples it → \"stuck at the left edge\"). */\n getWrapMin?: () => number;\n /** Write the position to the DOM (scrollLeft assignment or transform). */\n apply: (pos: number) => void;\n /** Read the externally-mutated position back (real scrollers: user drag /\n * native smooth scroll move scrollLeft under the engine). Omit for\n * transform drivers — nothing external can move them. */\n readBack?: () => number;\n /** Called at the end of every frame (e.g. hover re-resolution after the\n * track moved under a stationary pointer). */\n onAfterFrame?: () => void;\n}\n\nexport interface MarqueeEngineHandle {\n /** The engine's float position (px along the axis, 0 ≤ pos < wrap while\n * wrapping). Callers may write it together with their own DOM write (seam\n * warp on manual scroll). */\n posRef: MutableRefObject<number>;\n /** Accumulate a signed glide distance (chevron navigation). Runs through\n * the engine's own tween — never native smooth scroll while wrapping. */\n glideBy: (px: number) => void;\n /** Wrap an arbitrary position into [0, wrapSize). */\n wrap: (pos: number) => number;\n}\n\n// Resync threshold (px): external movement beyond this adopts the DOM value.\nconst RESYNC_EPSILON = 1.5;\n\nexport function useMarqueeEngine(options: MarqueeEngineOptions): MarqueeEngineHandle {\n const posRef = useRef(0);\n const glideRemainingRef = useRef(0);\n const speedEnvRef = useRef(0);\n\n // Latest-options ref: the rAF loop always reads current values without the\n // effect restarting on every render. Refreshed in an unconditional effect,\n // declared before every effect that starts the loop, rather than in the\n // render body — a render attempt React discards must not be able to hand the\n // loop options that were never committed.\n const optsRef = useRef(options);\n useEffect(() => {\n optsRef.current = options;\n });\n\n const wrap = useCallback((pos: number) => {\n const half = optsRef.current.getWrapSize();\n if (half <= 0) return pos;\n const min = optsRef.current.getWrapMin?.() ?? 0;\n let wrapped = pos;\n while (wrapped >= min + half) wrapped -= half;\n while (wrapped < min) wrapped += half;\n return wrapped;\n }, []);\n\n const glideBy = useCallback((px: number) => {\n glideRemainingRef.current += px;\n }, []);\n\n const active = options.active;\n useEffect(() => {\n if (!active) return undefined;\n let raf = 0;\n let last = performance.now();\n const readBack = optsRef.current.readBack;\n if (readBack) posRef.current = readBack();\n\n const tick = (now: number) => {\n const opts = optsRef.current;\n const dt = Math.min(now - last, 100) / 1000; // clamp tab-wake jumps\n last = now;\n\n const paused = opts.isPaused(now);\n // Ease the velocity toward its target (0 when paused) — smooth decel on\n // pause, smooth accel on resume, always from the CURRENT position.\n const targetSpeed = paused ? 0 : opts.speed;\n speedEnvRef.current += (targetSpeed - speedEnvRef.current) * Math.min(1, dt / 0.25);\n if (targetSpeed === 0 && speedEnvRef.current < 0.5) speedEnvRef.current = 0;\n\n const external = opts.readBack ? opts.readBack() : null;\n const resync = () => {\n if (external !== null && Math.abs(external - posRef.current) > RESYNC_EPSILON) {\n posRef.current = external;\n }\n };\n\n const glide = glideRemainingRef.current;\n if (glide !== 0) {\n // Ease-out toward the glide target; runs even while \"paused\" (the\n // interaction that queued it sets its own suppress window).\n resync();\n const speed = Math.max(Math.abs(glide) * 6, 240); // px/s, proportional\n const step = Math.sign(glide) * Math.min(Math.abs(glide), speed * dt);\n glideRemainingRef.current = Math.abs(glide - step) < 0.5 ? 0 : glide - step;\n posRef.current = wrap(posRef.current + step);\n opts.apply(posRef.current);\n } else if (speedEnvRef.current > 0) {\n // Resync after external movement (user scroll, seam warp).\n resync();\n posRef.current = wrap(posRef.current + speedEnvRef.current * dt);\n opts.apply(posRef.current);\n } else if (external !== null) {\n // Idle: follow external movement so resumes start from where the\n // user left the scroller.\n posRef.current = external;\n }\n opts.onAfterFrame?.();\n raf = requestAnimationFrame(tick);\n };\n raf = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(raf);\n }, [active, wrap]);\n\n return { posRef, glideBy, wrap };\n}\n","import { useEffect } from 'react';\n\n/**\n * Runs `reset` whenever the tab is hidden or the window loses focus — exactly the\n * moments a `pointerleave` / `pointercancel` can be SWALLOWED (tab blur, an\n * overlay mounting under the cursor, an interrupted pointer), which otherwise\n * leaves a hover/\"inside\" flag stuck `true` and freezes a marquee FOREVER. The\n * next real pointer/hover re-arms the flag.\n *\n * SSOT self-heal for every marquee surface (MarqueeWall, CardsStrip) so the\n * blur + `visibilitychange` wiring lives in one place. `reset` almost always\n * only UN-sticks state. The one edge: on `blur` WITHOUT the tab hiding (a second\n * window focused beside this one), clearing the hover-pause can let the marquee\n * resume under a still-stationary cursor in the now-unfocused window — it\n * re-pauses on the next `pointermove`/`pointerenter`. We keep the `blur` reset\n * anyway because it's the only signal that catches a tab-blur that swallows the\n * `pointerleave` without a `visibilitychange`. Pass a STABLE `reset` (memoize\n * with `useCallback`) — the listeners re-attach whenever its identity changes.\n */\nexport function useResetOnPageHidden(reset: () => void): void {\n useEffect(() => {\n const onVisibility = () => {\n if (document.visibilityState === 'hidden') reset();\n };\n window.addEventListener('blur', reset);\n document.addEventListener('visibilitychange', onVisibility);\n return () => {\n window.removeEventListener('blur', reset);\n document.removeEventListener('visibilitychange', onVisibility);\n };\n }, [reset]);\n}\n","'use client';\n\nimport { useEffect, useRef } from 'react';\n\n// Every focusable a clone might contain — matched so it can be pulled out of the\n// keyboard tab order (see useSuppressCloneFocus). `[contenteditable]:not(\n// [contenteditable=\"false\"])` catches the empty/`\"true\"`/`\"plaintext-only\"`\n// editable forms; `summary` is focusable inside a <details>.\nconst CLONE_FOCUSABLE_SELECTOR =\n 'a[href],button,input,select,textarea,iframe,summary,audio[controls],video[controls],[tabindex],[contenteditable]:not([contenteditable=\"false\"])';\n\n/**\n * Keep a marquee clone's content pointer-CLICKABLE while taking it out of the\n * accessibility tree and the keyboard tab order. Shared by every endless-loop\n * surface that renders a second track copy (<CardsStrip> managed cells,\n * <MarqueeWall> clone copies).\n *\n * `inert` (the previous approach) does all three at once: a11y-hidden,\n * un-focusable AND un-clickable. That third effect was the \"news marquee stops\n * being clickable after you scroll it for a while\" bug — the 2-copy endless\n * loop ALWAYS paints clone cards inside the viewport near the copy seam, so a\n * user who manually scrolls and parks there is looking at real-looking cards\n * that silently swallow every click. Clones must therefore stay clickable (a\n * click on ANY visible card must open its link — mouse, ⌘-click, middle-click\n * and context-menu all handled natively by the clone's own anchor).\n *\n * The a11y contract is still met without `inert`: the caller keeps\n * `aria-hidden` on the clone wrapper (out of the a11y tree) and this hook\n * forces every focusable descendant to `tabindex=\"-1\"` (out of the tab order →\n * no duplicate tab stops, no aria-hidden-focus violation). Re-applied on\n * subtree mutation. This is the standard loop-clone treatment used by\n * Swiper/Splide.\n */\nexport function useSuppressCloneFocus(active: boolean) {\n const ref = useRef<HTMLDivElement | null>(null);\n useEffect(() => {\n const root = ref.current;\n if (!root || !active) return undefined;\n const suppress = () => {\n root.querySelectorAll<HTMLElement>(CLONE_FOCUSABLE_SELECTOR).forEach(el => {\n // Only stamp the ones not already stamped — avoids touching the DOM (and\n // clobbering an intentional tabindex) on every mutation.\n if (el.getAttribute('tabindex') !== '-1') el.setAttribute('tabindex', '-1');\n });\n };\n suppress();\n // Watch structure AND the attributes that can turn a descendant tabbable\n // (an anchor gaining `href`, React restoring `tabindex=\"0\"`, an element\n // becoming editable, media gaining `controls`). The `!== '-1'` guard above\n // makes our own tabindex writes a no-op, so the observer never loops.\n const mo = new MutationObserver(suppress);\n mo.observe(root, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['href', 'tabindex', 'contenteditable', 'controls'],\n });\n return () => mo.disconnect();\n }, [active]);\n return ref;\n}\n","/**\n * Centralized Tool Types\n *\n * Single source of truth for all tool-related types across the entire platform.\n * Used by ToolBadge, ToolIcon, and any component that needs tool type information.\n */\n\nexport const ToolTypeValues = {\n FLEET_MDM: 'FLEET_MDM',\n MESHCENTRAL: 'MESHCENTRAL',\n AUTHENTIK: 'AUTHENTIK',\n OPENFRAME: 'OPENFRAME',\n OPENFRAME_CHAT: 'OPENFRAME_CHAT',\n OPENFRAME_CLIENT: 'OPENFRAME_CLIENT',\n OPENFRAME_RMM: 'OPENFRAME_RMM',\n OSQUERY: 'OSQUERY',\n SYSTEM: 'SYSTEM',\n // Directory tenants (Tenant Management): the same identifiers the gateway's\n // DirectoryProvider enum uses, so a directory sync's log rows resolve here.\n MICROSOFT_365: 'MICROSOFT_365',\n GOOGLE_WORKSPACE: 'GOOGLE_WORKSPACE',\n} as const;\n\nexport type ToolType = (typeof ToolTypeValues)[keyof typeof ToolTypeValues];\n\n/**\n * Maps tool types to display labels\n */\nexport const toolLabels: Record<ToolType, string> = {\n FLEET_MDM: 'Fleet',\n MESHCENTRAL: 'MeshCentral',\n AUTHENTIK: 'Authentik',\n OPENFRAME: 'OpenFrame',\n OPENFRAME_CHAT: 'OpenFrame Chat',\n OPENFRAME_CLIENT: 'OpenFrame Client',\n OPENFRAME_RMM: 'RMM',\n OSQUERY: 'Osquery',\n SYSTEM: 'System',\n MICROSOFT_365: 'Microsoft 365',\n GOOGLE_WORKSPACE: 'Google Workspace',\n};\n","import type { FC, ReactNode } from 'react';\nimport { type ToolType, ToolTypeValues } from '../types/tool.types';\nimport {\n OsqueryLogoGreyIcon,\n MeshcentralLogoGreyIcon,\n FleetMdmLogoGreyIcon,\n AuthentikLogoGreyIcon,\n Office365LogoGreyIcon,\n GoogleIcon,\n OpenframeLogoGreyIcon,\n} from './icons-v2-generated';\n\ntype ToolIconRenderer = (size: number, className?: string) => ReactNode;\n\n// Every mark is the grey (`currentColor`) cut of the brand logo, the OpenFrame one included, so a\n// row of tool icons reads as one set and takes the text colour of wherever it sits.\nconst renderOpenFrameLogo: ToolIconRenderer = (size, className) => (\n <OpenframeLogoGreyIcon size={size} className={className} />\n);\n\nconst toolIconMap: Record<ToolType, ToolIconRenderer> = {\n [ToolTypeValues.FLEET_MDM]: (size, className) => <FleetMdmLogoGreyIcon size={size} className={className} />,\n [ToolTypeValues.MESHCENTRAL]: (size, className) => <MeshcentralLogoGreyIcon size={size} className={className} />,\n [ToolTypeValues.OPENFRAME]: renderOpenFrameLogo,\n [ToolTypeValues.OPENFRAME_CHAT]: renderOpenFrameLogo,\n [ToolTypeValues.OPENFRAME_CLIENT]: renderOpenFrameLogo,\n [ToolTypeValues.OPENFRAME_RMM]: renderOpenFrameLogo,\n [ToolTypeValues.AUTHENTIK]: (size, className) => <AuthentikLogoGreyIcon size={size} className={className} />,\n [ToolTypeValues.OSQUERY]: (size, className) => <OsqueryLogoGreyIcon size={size} className={className} />,\n [ToolTypeValues.SYSTEM]: () => null,\n // The Microsoft 365 tool wears the Office 365 mark, not the Microsoft company logo: the product's\n // own brand is what the logs and the directory pickers name.\n [ToolTypeValues.MICROSOFT_365]: (size, className) => <Office365LogoGreyIcon size={size} className={className} />,\n [ToolTypeValues.GOOGLE_WORKSPACE]: (size, className) => <GoogleIcon size={size} className={className} />,\n};\n\nexport interface ToolIconProps {\n toolType: ToolType;\n size?: number;\n className?: string;\n}\n\nexport const ToolIcon: FC<ToolIconProps> = ({ toolType, size = 16, className }) => (\n <>{toolIconMap[toolType]?.(size, className) ?? null}</>\n);\n\nToolIcon.displayName = 'ToolIcon';\n","import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/**\n * Utility functions for common operations\n */\n\n/**\n * Merge class names with Tailwind CSS\n * @param inputs - Class names to merge\n * @returns Merged class names\n */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\n/**\n * Human-readable message for a caught value.\n *\n * `catch (e)` binds `unknown` under this tsconfig, and a thrown value is not\n * guaranteed to be an `Error`. Use this instead of `catch (e: any)` +\n * `e.message`, which crashes on a thrown string/object with no `message`.\n *\n * @param error - The caught value\n * @param fallback - Returned when the value carries no usable message\n */\nexport function errorMessage(error: unknown, fallback = 'Something went wrong'): string {\n if (error instanceof Error && error.message) return error.message;\n if (typeof error === 'string' && error) return error;\n if (typeof error === 'object' && error !== null) {\n const message = (error as { message?: unknown }).message;\n if (typeof message === 'string' && message) return message;\n }\n return fallback;\n}\n\n/**\n * Human-readable message for a FAILED API response body.\n *\n * `Response.json()` is typed `any`, so `body.error` used to be read off an\n * `any` at every call site. The platform's route helpers answer errors with\n * `{ error, code, details }` (see `errorResponse` in the hub's\n * `lib/api/route-base.ts`); a few endpoints answer `{ message }` instead, so\n * both are accepted before giving up.\n *\n * @param payload - The parsed response body (or whatever came back)\n * @param fallback - Returned when the body carries no usable message\n */\nexport function apiErrorMessage(payload: unknown, fallback: string): string {\n if (typeof payload === 'object' && payload !== null && 'error' in payload) {\n const { error } = payload;\n if (typeof error === 'string' && error) return error;\n }\n return errorMessage(payload, fallback);\n}\n\n/**\n * Delay execution for a specified time\n * @param ms - Milliseconds to delay\n * @returns Promise that resolves after the delay\n */\nexport function delay(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/**\n * Generate a random string of specified length\n * @param length - Length of the string\n * @returns Random string\n */\nexport function generateRandomString(length = 8): string {\n const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n let result = '';\n for (let i = 0; i < length; i++) {\n result += chars.charAt(Math.floor(Math.random() * chars.length));\n }\n return result;\n}\n\n/**\n * Truncate a string to a specified length\n * @param str - String to truncate\n * @param maxLength - Maximum length\n * @param suffix - Suffix to add to truncated string\n * @returns Truncated string\n */\nexport function truncateString(str: string, maxLength: number, suffix = '...'): string {\n if (str.length <= maxLength) return str;\n return str.substring(0, maxLength - suffix.length) + suffix;\n}\n\n/**\n * Serialize a JSON-LD schema for a `<script type=\"application/ld+json\">`\n * block — THE one home of the escape rule (the hub re-exports this from\n * lib/utils/breadcrumbs). JSON.stringify does NOT HTML-escape: a stored\n * \"</script>\" inside any admin/user-entered field would terminate the tag\n * early (stored XSS). Every \"<\" becomes the JSON escape sequence\n * backslash-u003c — still valid JSON, inert in HTML.\n */\nexport function serializeJsonLd(schema: unknown): string {\n return JSON.stringify(schema).replace(/</g, '\\\\u003c');\n}\n\n/**\n * Deep clone an object through a JSON round-trip.\n *\n * Lossy by construction, and deliberately so — this is the JSON projection of\n * `T`: `undefined` members and functions are dropped, `Date` becomes an ISO\n * string, `Map`/`Set` become `{}`, and a cyclic graph throws. Reach for\n * `structuredClone` when any of that matters.\n *\n * `JSON.parse` is typed `any`; the assertion states the contract above, which\n * is the closest a JSON clone can get to \"still a `T`\".\n *\n * @param obj - Object to clone (must be JSON-serializable)\n * @returns Cloned object\n */\nexport function deepClone<T>(obj: T): T {\n return JSON.parse(JSON.stringify(obj)) as T;\n}\n\n/**\n * Get the Slack community join URL from environment variables\n * @returns Slack community join URL or fallback URL\n */\nexport function getSlackCommunityJoinUrl(): string {\n const url = process.env.NEXT_PUBLIC_SLACK_COMMUNITY_JOIN_URL;\n if (!url) {\n console.warn('NEXT_PUBLIC_SLACK_COMMUNITY_JOIN_URL is not defined in environment variables');\n return 'https://join.slack.com/t/openmsp/shared_invite/zt-36bl7mx0h-3~U2nFH6nqHqoTPXMaHEHA';\n }\n return url;\n}\n\n/**\n * Clamp a number into an inclusive range. THE one clamp for the lib and the hub\n * (step/page/contrast math all bottom out here).\n */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\n/**\n * Typed key-subset of an object. Keys whose value is `undefined` on the source\n * are still copied when present, so `pick(params, dropped)` writes explicit\n * `null`s (the URL-state writer's \"remove this key\" signal).\n */\nexport function pick<T extends object, K extends keyof T>(source: T, keys: readonly K[]): Pick<T, K> {\n const out = {} as Pick<T, K>;\n for (const key of keys) {\n if (key in source) out[key] = source[key];\n }\n return out;\n}\n\n/**\n * React Query options for data that MUST NOT be served from the client cache.\n *\n * The rule (hub memory `feedback_no_client_query_cache_for_mutable_entities`):\n * anything a user can mutate, or a cron can rewrite under them, is refetched on\n * mount. Spread this instead of retyping `staleTime: 0` — it is also the hub\n * QueryClient's default, so a hook that spreads it is explicit rather than\n * silently inheriting.\n */\nexport const NO_CLIENT_CACHE = { staleTime: 0 } as const;\n","/**\n * useNearViewport — module-level shared IntersectionObserver in hook form.\n *\n * Single IO instance per `rootMargin` value, shared across every component\n * that mounts the hook. Reduces overhead vs. one IO per component on\n * grid/list pages where many subscribers observe the viewport with the same\n * margin. Promoted from the inline singleton at\n * `multi-platform-hub/components/shared/video-bites-display.tsx:21-43`,\n * which is the only IO pattern in either repo today.\n *\n * Usage:\n * ```tsx\n * function MyCard() {\n * const { ref, isNear } = useNearViewport('500px');\n * return <div ref={ref}>{isNear ? <HeavyChild /> : <Placeholder />}</div>;\n * }\n * ```\n *\n * StrictMode safety: cleanup uses an identity check on the registered\n * callback so React's dev double-mount (mount → cleanup → re-mount) does\n * not drop the second mount's freshly-set subscription. The IO callback\n * also checks `subscribers.get(target)` before invoking so a fire that\n * races with unmount cannot crash on a torn-down component.\n *\n * The hook fires once — on first intersection it sets `isNear=true` and\n * unobserves the element. Callers that need re-observation should\n * unmount and remount (or fork the hook for two-way behavior).\n */\n\nimport { useEffect, useRef, useState, useCallback } from 'react';\n\n// Per-(rootMargin, threshold) IO map. Multiple call sites with different\n// margins/thresholds each get their own singleton observer.\nconst observers = new Map<string, IntersectionObserver>();\nconst subscribers = new WeakMap<Element, () => void>();\n\n// Slack for the explicit ratio check: an element sized to exactly meet the\n// threshold can report a ratio a hair under it (sub-pixel / float rounding), so\n// accept `ratio >= threshold - EPSILON`. With `threshold: 0` this is a no-op\n// (ratio is always ≥ 0 > -EPSILON) — the `isIntersecting` check alone gates.\nconst THRESHOLD_EPSILON = 0.01;\n\n/** Stable map key for an observer config. */\nfunction observerKey(rootMargin: string, threshold: number): string {\n return `${rootMargin}|${threshold}`;\n}\n\nfunction getObserverFor(rootMargin: string, threshold: number): IntersectionObserver {\n const key = observerKey(rootMargin, threshold);\n const existing = observers.get(key);\n if (existing) return existing;\n\n const io = new IntersectionObserver(\n entries => {\n entries.forEach(entry => {\n // Enforce the threshold EXPLICITLY. `isIntersecting` is spec-defined as\n // `intersectionRatio > 0` (any overlap), so the initial `observe()`\n // firing can report `true` at, say, 20% visible even with\n // `threshold: 0.5` — which would prematurely pass the gate. Require both\n // an intersection AND the ratio to actually meet the threshold (minus a\n // small epsilon for the float rounding where the ratio lands a hair\n // under the exact value).\n if (!entry.isIntersecting) return;\n if (entry.intersectionRatio < threshold - THRESHOLD_EPSILON) return;\n // Race-safe: re-read the callback at fire time. A late IO firing\n // after cleanup must not invoke a stale callback.\n const cb = subscribers.get(entry.target);\n if (cb) {\n cb();\n io.unobserve(entry.target);\n subscribers.delete(entry.target);\n }\n });\n },\n { rootMargin, threshold },\n );\n observers.set(key, io);\n return io;\n}\n\n/**\n * Default near-viewport lookahead — the SSOT for \"mount media on approach\".\n * Shared by this hook's default AND the raw two-way IntersectionObservers in\n * `cards-strip.tsx` / `video-bites-strip.tsx` (which need mount+unmount, not\n * this hook's fire-once semantics, but must agree on the distance).\n */\nexport const NEAR_VIEWPORT_ROOT_MARGIN = '500px';\n\nexport interface UseNearViewportResult<T extends Element = HTMLElement> {\n /** Ref to attach to the element you want to gate on visibility. */\n ref: (node: T | null) => void;\n /** Flips to `true` once the element enters within `rootMargin` of the viewport. Never flips back. */\n isNear: boolean;\n}\n\n/**\n * @param rootMargin Margin around the viewport (CSS-style string).\n * '500px' = element starts mounting 500px before scroll-in.\n * '1000px' = a full viewport's worth of lookahead.\n * '0px' = strict on-screen detection.\n * @param threshold Fraction of the element that must be visible before firing\n * (0 = any pixel — the default; 0.5 = at least half on-screen).\n */\nexport function useNearViewport<T extends Element = HTMLElement>(\n rootMargin: string = NEAR_VIEWPORT_ROOT_MARGIN,\n threshold: number = 0,\n): UseNearViewportResult<T> {\n const [isNear, setIsNear] = useState(false);\n const elRef = useRef<T | null>(null);\n const key = observerKey(rootMargin, threshold);\n\n // Subscribe/unsubscribe on element change.\n const ref = useCallback(\n (node: T | null) => {\n const prev = elRef.current;\n\n // Unsubscribe previous, if any. Identity-check the callback so a\n // StrictMode re-mount that has already re-registered keeps its sub.\n if (prev) {\n const stillOurs = subscribers.get(prev);\n if (stillOurs) {\n subscribers.delete(prev);\n observers.get(key)?.unobserve(prev);\n }\n }\n\n elRef.current = node;\n if (!node) return;\n\n const cb = () => setIsNear(true);\n subscribers.set(node, cb);\n getObserverFor(rootMargin, threshold).observe(node);\n },\n [key, rootMargin, threshold],\n );\n\n // Unsubscribe on unmount. Identity check guards the StrictMode race.\n useEffect(() => {\n return () => {\n const el = elRef.current;\n if (!el) return;\n if (subscribers.get(el)) {\n subscribers.delete(el);\n observers.get(key)?.unobserve(el);\n }\n };\n }, [key]);\n\n return { ref, isNear };\n}\n","/**\n * Fenced-code tracker — THE definition of \"am I inside a code fence\".\n * Server-safe: no React, no DOM.\n *\n * Lives in its own module because it is a GENERAL CommonMark fence machine\n * with two unrelated consumers: the heading scanner\n * (`utils/markdown-heading-id`) and the streaming block splitter\n * (`components/ui/markdown/streaming`). It used to live inside\n * `markdown-heading-id`, which meant a streaming renderer imported its fence\n * state machine from a module named after heading slugs. `markdown-heading-id`\n * re-exports these names so the public `utils` surface is unchanged.\n */\n\n/**\n * CommonMark fenced-code opener/closer. Fence indent is capped at 3 spaces\n * (4+ is an indented code block, whose backticks are literal content), and the\n * run length is captured so a closer can be required to be at least as long as\n * its opener. Matched on the RAW line — `trimStart()` would let a fence inside\n * 4-space-indented code toggle the state.\n *\n * `\\r`-TOLERANT BY CONSTRUCTION. The previous spelling ended `(.*)$`, and `.`\n * excludes `\\r` while `$` (no `m` flag) anchors at end of input — so on a CRLF\n * document, split on `\\n`, NO line ever matched and the tracker was completely\n * fence-blind: `['```js\\r','const a = 1\\r','```\\r','after\\r']` classified as\n * four `text` lines where the LF spelling gives `open/inside/close/text`. The\n * sanitizer's closer haystack then never blanked the fenced region, so a\n * `</textarea>` written as a CRLF code sample satisfied `hasLaterCloser` and a\n * prose `<textarea>` above it stayed LIVE (reproduced in the DOM; the LF twin\n * of the same fixture escapes correctly). The `\\r` is absorbed by `\\r?$` rather\n * than normalized out of the text, because the sanitizer's mask is\n * LENGTH-PRESERVING and every offset in it is an index into the original\n * string — stripping `\\r` would shift them all.\n *\n * THE ONLY COPY. This constant plus its open/close state machine used to be\n * duplicated verbatim in `components/ui/markdown/streaming.ts`, kept in sync\n * by a code comment — which is exactly how the `~~~`-blind extractor drifted\n * in the first place. Both consumers now instantiate `createFenceTracker()`.\n */\nconst FENCE_RE = /^ {0,3}(`{3,}|~{3,})([^\\n]*?)\\r?$/;\n\n/**\n * CommonMark's BLANK LINE — spaces and tabs only. The single definition, shared\n * by every consumer of this module (the sanitizer's mask passes, the heading\n * scanner, the HTML-block range walk).\n *\n * NOT `String.prototype.trim()`. JS `trim()` strips the full Unicode\n * White_Space set — U+00A0 NBSP, U+000B VT, U+000C FF, U+2028/9, U+FEFF BOM —\n * none of which CommonMark (or remark) treats as blank. Every place that meant\n * \"is this a CommonMark blank line?\" and asked `trim() === ''` therefore\n * TERMINATED a construct one line early while remark kept it open. Verified\n * end-to-end: `<div>\\n\\u00A0\\n\\`<textarea>\\`\\n\\nrest\\n` left a LIVE `<textarea>`\n * in the DOM (`escapeUnknownHtmlTags` returned the input byte-identical),\n * because the tracked HTML-block range ended at the NBSP filler line and the\n * inline-code shelter below it stopped being inside a tracked block. Same for\n * VT / FF / BOM, for `<pre>` / `<details>` wrappers, and for a live third-party\n * `<iframe>`.\n *\n * `\\r` IS accepted: a CRLF document split on `\\n` spells its blank lines\n * `\"\\r\"`, and the mask is length-preserving so stripping `\\r` from the source\n * is not an option (see `FENCE_RE`'s `\\r?$`).\n */\nconst BLANK_LINE_RE = /^[ \\t\\r]*$/;\n\n/** True when `line` is a CommonMark blank line (spaces/tabs only, `\\r`-tolerant). */\nexport function isBlankLine(line: string): boolean {\n return BLANK_LINE_RE.test(line);\n}\n\n/**\n * What one source line is, relative to fenced code:\n * - `open` — this line opened a fence\n * - `close` — this line closed the open fence\n * - `inside`— this line is fence content (including a would-be delimiter that\n * does NOT close the open fence: wrong marker char, too short, or\n * carrying an info string)\n * - `text` — this line is ordinary markdown outside any fence\n *\n * NOTE: no consumer discriminates the three non-`text` roles today — both the\n * heading scanner and the streaming splitter only test `!== 'text'`, and the\n * closer-haystack mask below only tests `=== 'open'` / `=== 'close'`. The\n * four-value union is kept because the state machine already computes the\n * distinction for free and a future consumer (e.g. fence-aware syntax\n * post-processing) would otherwise have to re-derive it.\n */\nexport type FenceLineRole = 'open' | 'close' | 'inside' | 'text';\n\nexport interface FenceTracker {\n /** Advance across one raw source line (mutates) and classify it. */\n push(line: string): FenceLineRole;\n /** Marker CHARACTER of the currently open fence (`` ` `` / `~`), or null. */\n openChar(): string | null;\n /** Run length of the currently open fence (a closer must be ≥ this). */\n openLength(): number;\n}\n\n/** A fresh fenced-code state machine. Server-safe: no React, no DOM. */\nexport function createFenceTracker(): FenceTracker {\n let fenceChar: string | null = null;\n let fenceLength = 0;\n return {\n push(line: string): FenceLineRole {\n const fence = FENCE_RE.exec(line);\n if (fence) {\n const run = fence[1];\n if (fenceChar === null) {\n fenceChar = run[0];\n fenceLength = run.length;\n return 'open';\n }\n // A closer must use the SAME marker char, be at least as long as the\n // opener, and (per CommonMark) carry no info string. \"No info string\"\n // means spaces/tabs only — `trim()` would accept an NBSP tail and close\n // a fence CommonMark keeps open, i.e. blank LESS of the haystack\n // (fail-OPEN). Hence `isBlankLine`, not `trim()`.\n if (run[0] === fenceChar && run.length >= fenceLength && isBlankLine(fence[2])) {\n fenceChar = null;\n fenceLength = 0;\n return 'close';\n }\n return 'inside';\n }\n return fenceChar === null ? 'text' : 'inside';\n },\n openChar: () => fenceChar,\n openLength: () => fenceLength,\n };\n}\n","/**\n * Markdown heading-id slug SSOT (server-safe: no React, no DOM).\n *\n * This algorithm previously existed in THREE byte-identical copies:\n * - `components/ui/simple-markdown-renderer.tsx` (generateHeadingId)\n * - `components/ui/rich-markdown-renderer.tsx` (generateHeadingId)\n * - `utils/markdown-section-extractor.ts` (extractSections)\n *\n * The extractor is the PRODUCER of `sectionIds` and the renderers are the\n * CONSUMERS — if the two ever drift, deep-link anchors and scroll-spy\n * targets silently diverge. All three now call these helpers; the parity\n * test `components/ui/__tests__/markdown-parity.test.tsx` asserts\n * extractor-vs-renderer ID agreement over the fixture corpus.\n */\n\n/** Emoji ranges stripped from heading text before slugification. */\nexport const HEADING_EMOJI_RE = /[\\u{1F300}-\\u{1F9FF}]|[\\u{2600}-\\u{26FF}]|[\\u{2700}-\\u{27BF}]/gu;\n\n/** Remove emoji characters and trim. */\nexport function stripHeadingEmojis(text: string): string {\n return text.replace(HEADING_EMOJI_RE, '').trim();\n}\n\n/**\n * Core slug chain WITHOUT the emoji strip (the extractor exposes emoji\n * stripping as an option, so the two steps are kept separable):\n * lowercase → drop non-word/space/hyphen chars → spaces to hyphens →\n * trim leading/trailing hyphens. May return `''` for symbol-only input —\n * callers apply their own fallback (`section-N`).\n */\nexport function slugifyHeadingBase(text: string): string {\n return text\n .trim()\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/** The full default chain used by the renderers: emoji strip + slugify. */\nexport function slugifyHeadingText(text: string): string {\n return slugifyHeadingBase(stripHeadingEmojis(text));\n}\n\n// ---------------------------------------------------------------------------\n// Heading SCANNER + DEDUPER (the other half of the producer/consumer contract)\n// ---------------------------------------------------------------------------\n/**\n * Sharing the slug chain alone was not enough. The PRODUCER\n * (`utils/markdown-section-extractor`) and the CONSUMER\n * (`components/ui/markdown/heading-ids`) each carried their own copy of\n * \"which lines are headings\" and \"how do duplicates get suffixed\", and the\n * copies drifted:\n * - the extractor toggled its code-block state on a bare\n * `line.startsWith('```')`, so a `~~~`-fenced (or wider-backtick, or\n * indented) block containing `## Setup` produced a SECTION from the\n * extractor and NO id from the renderer — `sectionIdMap` then missed\n * silently and the deep-link anchor pointed nowhere;\n * - the extractor anchored ATX at column 0 while the renderer allowed the\n * CommonMark 0..3-space indent;\n * - the dedupe counter was hand-copied in both files, each with a comment\n * saying \"the two must agree\".\n * Both now call `scanHeadings` + `createHeadingIdDeduper`. Server-safe: no\n * React, no DOM.\n */\n\n// The CommonMark fence machine used to live in THIS file, which meant a\n// streaming renderer imported its fence state from a module named after\n// heading slugs. It now lives in `./markdown-fences`; re-exported here so the\n// public `utils` surface (and every existing import) is unchanged.\nexport { createFenceTracker, isBlankLine, type FenceTracker, type FenceLineRole } from './markdown-fences';\nimport { createFenceTracker, isBlankLine } from './markdown-fences';\n\n/** A heading the renderer will emit, located in the source. */\nexport interface ScannedHeading {\n /** 1-based line of the heading's FIRST line (setext: the title line). */\n line: number;\n level: number;\n /** Raw title text, before `stripInlineMarkdown` / slugification. */\n text: string;\n}\n\nexport interface ScanHeadingsOptions {\n /**\n * Skip a leading YAML frontmatter block (`---` on line 1 through the next\n * `---`). Only a DOCUMENT-LEADING block counts: a bare `---` mid-document\n * is a thematic break or a setext underline, and treating it as a\n * frontmatter toggle (what the extractor used to do) silently swallowed\n * every heading until the next one.\n */\n skipFrontmatter?: boolean;\n /** Include raw-HTML headings (`<h2>Title</h2>`). Default true. */\n includeRawHtml?: boolean;\n /**\n * Skip fenced-code blocks. Default true — a `##` inside a code fence is\n * code, not a heading, and the renderer emits no id for it.\n */\n skipFences?: boolean;\n}\n\n/**\n * EVERY line-anchored regex in this module ends `\\r?$`, not `$`.\n *\n * Lines here come from `content.split('\\n')`, so on a CRLF document each one\n * carries a trailing `\\r`. `$` (no `m` flag) anchors at end of string and `\\r`\n * is not in `[ \\t]`, so a `$`-anchored pattern matches NOTHING on such a\n * document. That was a live REGRESSION: `main`'s looser `^(#{1,6})\\s+(.+)`\n * had no end anchor at all and tolerated the `\\r` (the title was `.trim()`ed\n * afterwards), so `'# Real\\r\\n\\r\\nbody\\r\\n\\r\\n## Second\\r\\n'` yielded\n * `[\"Real\",\"Second\"]` on `main` and `[]` here — every CRLF-stored doc / blog /\n * release body silently lost its TOC, its in-page anchors and its doc-SEO\n * heading links.\n *\n * The `\\r` is ABSORBED, not normalized away: `scanHeadings` reports 1-based\n * LINE numbers that the renderer's id map is keyed by, and the sanitizer's mask\n * (which shares `markdown-fences`) is length-preserving — neither may be fed a\n * rewritten source.\n */\n/** CommonMark ATX heading (closing `#` run is not part of the title). */\nconst ATX_HEADING_RE = /^ {0,3}(#{1,6})[ \\t]+(.*?)(?:[ \\t]+#+)?[ \\t]*\\r?$/;\n/** ATX with no title at all (`###`, `## ###`) — still a heading, empty text. */\nconst ATX_EMPTY_RE = /^ {0,3}(#{1,6})[ \\t]*#*[ \\t]*\\r?$/;\n/**\n * Raw-HTML heading (`<h2 id=\"x\">Title</h2>`) — rehype-raw renders these too.\n * GLOBAL: two `<h3>`s on one line are two headings, and a non-global scan\n * silently dropped the second (it then fell through to the renderer's\n * suffix-free fallback and emitted a DUPLICATE DOM id).\n */\nconst RAW_HEADING_RE = /<h([1-6])\\b[^>]*>([\\s\\S]*?)(?:<\\/h\\1>|$)/gi;\n/**\n * Blockquote markers and list-item markers preceding an ATX heading\n * (`> ## Setup`, `- ## Setup`, `1. ## Setup`). mdast emits a REAL `<h2>` for\n * these; scanning only column-0..3 missed them entirely, so two identical\n * `> ## Setup` headings both hit the suffix-free fallback and emitted the\n * duplicate DOM ids this whole module exists to prevent.\n */\nconst CONTAINER_PREFIX_RE = /^ {0,3}(?:(?:>[ \\t]?)+|(?:[-+*]|\\d{1,9}[.)])[ \\t]+)+/;\n/** Setext underline (`===` → h1, `---` → h2), AFTER container-prefix strip. */\nconst SETEXT_UNDERLINE_RE = /^ {0,3}(=+|-+)[ \\t]*\\r?$/;\n/** Thematic break — ends a paragraph run, never underlines it. */\nconst THEMATIC_BREAK_RE = /^ {0,3}(\\*|_){3,}[ \\t]*\\r?$/;\n/**\n * CommonMark HTML-block type 6 tag names — these open a raw HTML block no\n * matter what follows them on the line.\n */\nconst HTML_BLOCK_TAGS =\n 'address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h1|h2|h3|h4|h5|h6|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul';\n\n/**\n * A line that OPENS a raw HTML block. mdast hands the whole block to the HTML\n * parser, so a `---` inside it is not a setext underline and the lines above\n * it are not a paragraph: `<div>\\nText\\n---\\n</div>` emits NO heading. The\n * scanner used to publish a phantom `Text` h2 there.\n *\n * The naive `^ {0,3}<` this replaced disqualified any run whose first line\n * merely STARTED with a tag, so `<span>x</span> more text` + `---` (a plain\n * paragraph in mdast, yielding an h2) was missed. The alternatives are spelled\n * out per CommonMark:\n * - types 1-5: `<script|pre|style|textarea`, `<!--`, `<?`, `<!X`, `<![CDATA[`\n * - type 6: a `HTML_BLOCK_TAGS` name, whatever follows it\n * - type 7: ANY complete open/closing tag ALONE on the line (trailing\n * whitespace only) — this is the clause the old regex got wrong\n * Attribute soup inside a type-7 tag is approximated with `[^>]*` rather than\n * a full attribute grammar; the error bias is unchanged (a missed run costs a\n * TOC entry, never a wrong id — the same direction as the documented\n * list-item-indent gap in `SetextRun.prefix`).\n */\nconst HTML_BLOCK_OPENER_RE = new RegExp(\n '^ {0,3}(?:' +\n // types 1-5\n '<(?:script|pre|style|textarea)(?:[\\\\s>]|$)|<!--|<\\\\?|<![a-zA-Z]|<!\\\\[CDATA\\\\[' +\n // type 6\n `|</?(?:${HTML_BLOCK_TAGS})(?:[\\\\s/>]|$)` +\n // type 7 — a complete tag standing alone on the line\n '|<[a-zA-Z][a-zA-Z0-9-]*(?:\\\\s[^>]*)?/?>[ \\\\t]*\\\\r?$' +\n '|</[a-zA-Z][a-zA-Z0-9-]*[ \\\\t]*>[ \\\\t]*\\\\r?$' +\n ')',\n 'i',\n);\n\n/**\n * The paragraph RUN that a setext underline would convert into a heading.\n *\n * mdast positions a setext heading at the FIRST line of the paragraph and its\n * text is the WHOLE run (`Foo\\nbar\\n===` → `{line:1, text:\"Foo\\nbar\"}`). The\n * scanner used to remember only the LAST line, so the renderer's line-keyed\n * lookup missed the heading entirely (falling back to a slug of the rendered\n * children, `foo-bar`) while the extractor published `bar` — a dead deep\n * link, plus a phantom TOC entry at the wrong line.\n */\ninterface SetextRun {\n /** 1-based line of the run's FIRST line — where mdast puts the heading. */\n line: number;\n /**\n * Container prefix (`> `, `- `, …) every line of the run shares. The\n * underline must carry the IDENTICAL prefix, which is why `> Quote` +\n * `> ---` scans as an h2 while `> Quote` + a bare `---` does not.\n *\n * KNOWN, DELIBERATE GAP: a setext underline indented to a LIST ITEM's\n * content column (`- item` / ` ---`) is a heading in mdast but has no\n * container prefix at all, so this equality test misses it. Closing that\n * would mean re-deriving list-item content columns here — the scanner\n * simply emits nothing (the pre-existing behavior), which costs a TOC entry\n * and never produces a WRONG id.\n */\n prefix: string;\n /** Run lines with the container prefix stripped, in order. */\n lines: string[];\n /** Run opened with a raw-HTML block line — mdast emits no heading for it. */\n disqualified: boolean;\n}\n\n/**\n * Collect every heading the markdown renderer will emit, in document order.\n * THE single definition of \"which lines are headings\" for both the extractor\n * and the renderer's id map.\n */\nexport function scanHeadings(content: string, options: ScanHeadingsOptions = {}): ScannedHeading[] {\n const { skipFrontmatter = true, includeRawHtml = true, skipFences = true } = options;\n const out: ScannedHeading[] = [];\n const lines = content.split('\\n');\n // ONE fenced-code state machine (utils/markdown-fences), shared with\n // markdown/streaming.ts.\n const fences = skipFences ? createFenceTracker() : null;\n\n let start = 0;\n if (skipFrontmatter && lines[0]?.trim() === '---') {\n for (let i = 1; i < lines.length; i++) {\n if (lines[i].trim() === '---' || lines[i].trim() === '...') {\n start = i + 1;\n break;\n }\n }\n }\n\n // The paragraph run a setext underline would convert (see SetextRun).\n let setextRun: SetextRun | null = null;\n\n for (let i = start; i < lines.length; i++) {\n const line = lines[i];\n if (fences) {\n const role = fences.push(line);\n if (role !== 'text') {\n setextRun = null;\n continue;\n }\n }\n\n // Container prefix (`> `, `- `, `1. `) is stripped from EVERY construct\n // below, so a blockquote/list-nested heading is scanned in both its ATX\n // (`> ## Setup`) and setext (`> Quote title` / `> ---`) spellings.\n const prefix = CONTAINER_PREFIX_RE.exec(line)?.[0] ?? '';\n const body = prefix ? line.slice(prefix.length) : line;\n\n // --- setext (`Title` / `===`): a REAL h1/h2 in mdast, positioned at the\n // run's FIRST line — which is the line the heading-id map is keyed by.\n // The underline must carry the SAME container prefix as the run it\n // closes; `> Quote` followed by a bare `---` is a quote and a thematic\n // break, not a heading.\n const setext = SETEXT_UNDERLINE_RE.exec(body);\n if (setext) {\n if (setextRun && !setextRun.disqualified && setextRun.prefix === prefix) {\n out.push({\n line: setextRun.line,\n level: setext[1][0] === '=' ? 1 : 2,\n text: setextRun.lines.join('\\n'),\n });\n }\n setextRun = null;\n continue;\n }\n\n // --- ATX, including inside a blockquote / list item.\n const atx = ATX_HEADING_RE.exec(body) ?? ATX_EMPTY_RE.exec(body);\n if (atx) {\n out.push({ line: i + 1, level: atx[1].length, text: (atx[2] ?? '').trim() });\n setextRun = null;\n continue;\n }\n\n if (includeRawHtml && line.indexOf('<') !== -1) {\n RAW_HEADING_RE.lastIndex = 0;\n let raw: RegExpExecArray | null;\n let matched = false;\n while ((raw = RAW_HEADING_RE.exec(line)) !== null) {\n matched = true;\n out.push({\n line: i + 1,\n level: Number(raw[1]),\n text: raw[2].replace(/<[^>]*>/g, '').trim(),\n });\n }\n if (matched) {\n setextRun = null;\n continue;\n }\n }\n\n // Paragraph accumulation. A blank line or a thematic break ends the run;\n // a change of container prefix starts a new block (a list or blockquote\n // interrupts a paragraph in CommonMark).\n // CommonMark's blank line (spaces/tabs), NOT `trim()` — an NBSP-only line\n // is paragraph CONTENT to remark, so ending the run there would move a\n // setext heading's reported line off the run's first line.\n if (isBlankLine(body) || THEMATIC_BREAK_RE.test(body)) {\n setextRun = null;\n continue;\n }\n if (setextRun && setextRun.prefix === prefix) {\n setextRun.lines.push(body.trim());\n } else {\n setextRun = {\n line: i + 1,\n prefix,\n lines: [body.trim()],\n disqualified: HTML_BLOCK_OPENER_RE.test(body),\n };\n }\n }\n\n return out;\n}\n\n/**\n * The duplicate-id suffix counter, as ONE closure both sides instantiate.\n * First occurrence keeps the bare slug; the Nth gets `-N`. Extractor and\n * renderer MUST use the same counter or `#anchor` deep links diverge.\n */\nexport function createHeadingIdDeduper(): (cleanId: string) => string {\n const counts: Record<string, number> = {};\n return (cleanId: string): string => {\n if (counts[cleanId]) {\n counts[cleanId]++;\n return `${cleanId}-${counts[cleanId]}`;\n }\n counts[cleanId] = 1;\n return cleanId;\n };\n}\n","/**\n * GitHub-compatible anchor resolution for in-page `#hash` links.\n *\n * ## The mismatch this exists to bridge\n *\n * Doc authors write their table of contents against GitHub's slugger, which\n * keeps the separator left behind by a stripped emoji:\n *\n * `## 📚 Table of Contents` → GitHub anchor `#-table-of-contents`\n *\n * Our heading ids come from the shared slug SSOT (`markdown-heading-id`),\n * whose `slugifyHeadingBase` trims leading and trailing hyphens — so the same\n * heading renders as `id=\"table-of-contents\"`. The \"On this page\" rail\n * navigates by those ids and works; the in-body TOC link points at an id that\n * does not exist and the browser silently does nothing.\n *\n * Rather than change the id shape (it is already baked into shared URLs and\n * the rail), resolution is made tolerant: compare a *normalized* form of both\n * sides. This module owns ONLY that tolerance — the slug chain itself is\n * imported, never re-implemented, so a change to the id shape lands in one\n * place and both sides move together.\n *\n * Callers do not reach for this directly: `getHashTargetElement`\n * (`utils/same-page-hash-nav`) is the ONE resolver every hash consumer goes\n * through, and it applies this as its last pass.\n */\nimport { slugifyHeadingText } from './markdown-heading-id';\n\n/**\n * Reduce an anchor id (ours or GitHub's) to a comparable form.\n *\n * Everything but the final hyphen-run collapse is the shared heading-slug\n * chain: emoji strip → lowercase → drop non-word chars → spaces to hyphens →\n * trim edge hyphens. The collapse is this module's own addition, covering\n * GitHub's *interior* divergence — `## 💬 Community & Support` slugs to\n * `#-community--support` (a double hyphen around the dropped `&`) against our\n * `community-support`.\n *\n * `'-getting-started'` → `'getting-started'`\n * `'-community--support'` → `'community-support'`\n * `'Getting Started'` → `'getting-started'`\n */\nexport function normalizeAnchorId(raw: string): string {\n return slugifyHeadingText(raw)\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/** Heading elements carrying an id — the candidate set for the fuzzy pass. */\nconst HEADING_SELECTOR = 'h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]';\n\n/** A `base-N` id, split into its base and ordinal. */\nconst DUPLICATE_SUFFIX_RE = /^(.+)-(\\d+)$/;\n\n/**\n * The two sides also disagree on where duplicate-heading numbering STARTS.\n * For a document with two `## Setup` headings GitHub emits `setup` and\n * `setup-1`, while `createHeadingIdDeduper` emits `setup` and `setup-2` (its\n * suffix is the occurrence COUNT, not the offset). So a GitHub `#setup-1`\n * needs our `setup-2` — every duplicate is off by exactly one.\n *\n * Runs LAST, after the exact lookup and the heading scan have both missed: a\n * heading that genuinely slugs to `step-2` is resolved long before this, and\n * the shift is the one pass that can match an id it was not asked for.\n */\nfunction findDuplicateShifted(normalized: string, doc: Document): HTMLElement | null {\n const parts = DUPLICATE_SUFFIX_RE.exec(normalized);\n if (!parts) return null;\n const ordinal = Number(parts[2]);\n if (!Number.isSafeInteger(ordinal)) return null;\n return doc.getElementById(`${parts[1]}-${ordinal + 1}`);\n}\n\n/**\n * Resolve a `#hash` id that no exact lookup matched, by comparing normalized\n * forms. Returns `null` when nothing in the document matches, so the caller\n * can leave the browser's default behavior alone rather than hijacking a\n * click that was never ours to handle.\n *\n * Three passes, in order of how much they assume:\n * 1. a single `getElementById` on the normalized id — catches the whole\n * emoji-hyphen family without walking the DOM;\n * 2. a heading scan — catches ids whose own raw form normalizes differently\n * (e.g. `a---b`, from a title containing a literal ` - `);\n * 3. the duplicate-heading shift (see `findDuplicateShifted`) — last,\n * because it is the only pass that resolves to an id the fragment did\n * not name.\n */\nexport function findAnchorElementByNormalizedId(rawId: string, doc: Document): HTMLElement | null {\n const normalized = normalizeAnchorId(rawId);\n if (!normalized) return null;\n\n if (normalized !== rawId) {\n const direct = doc.getElementById(normalized);\n if (direct) return direct;\n }\n\n for (const heading of Array.from(doc.querySelectorAll<HTMLElement>(HEADING_SELECTOR))) {\n if (normalizeAnchorId(heading.id) === normalized) return heading;\n }\n\n // Last: the shift would happily match an unrelated `x-3` for a `x-2` that\n // has no heading at all, so it runs only once the scan has come up empty.\n return findDuplicateShifted(normalized, doc);\n}\n","/**\n * `scrollElementIntoView` — canonical \"scroll an element to the top of the\n * viewport, account for sticky chrome, survive layout shifts\" helper.\n *\n * One shared implementation so every caller (the ticket drawer expand, the\n * hub's `useUnifiedNav` / `use-nav-link` hash scroll, doc-tree, delivery\n * `?focus=`, sticky-section-nav, …) inherits the SAME cancellation-proof\n * motion.\n *\n * WHY A SELF-DRIVEN rAF TWEEN INSTEAD OF `window.scrollTo({behavior:'smooth'})`:\n * the native smooth scroll is CANCELLABLE, and in real pages it gets cancelled\n * constantly:\n *\n * - Browser SCROLL ANCHORING: when content is inserted/removed above or\n * around the target (a collapsible drawer expanding, an async image\n * loading, a list re-rendering) the browser issues a synchronous scrollTop\n * correction to keep the anchored element stable. Per CSSOM-View \"perform a\n * scroll\" step 1 (\"abort any ongoing smooth scroll\"), that correction\n * ABORTS an in-flight native smooth scroll — so it lands as an instant jump.\n * Anchoring is suppressed when the scroll offset is 0, which is exactly why\n * a native smooth scroll appears to work the FIRST time (page at top) and\n * jumps on every repeat (page already scrolled). This was a multi-day\n * \"smooth only works once\" bug on the /tickets drawer.\n * - A second programmatic scroll on the same frame, or a `focus()` without\n * `{preventScroll:true}`, cancels it the same way.\n *\n * A tween that re-asserts the position with INSTANT writes every frame is\n * immune: there is no \"ongoing native smooth scroll\" for anchoring/focus to\n * abort, and any correction that lands between our frames is overwritten on the\n * next frame. We also RECOMPUTE the target each frame, so an element whose\n * final position is still settling (drawer still expanding, images loading)\n * is tracked to its resting place instead of animating to a stale pixel.\n *\n * Honors `prefers-reduced-motion` (jumps instantly) and cancels on genuine user\n * scroll intent (wheel / touch) so we never fight the user.\n *\n * WINDOW *OR* A SCROLLABLE ANCESTOR: the helper is not hard-wired to the window\n * scroller. It walks up from the target to the nearest ancestor that is an\n * actual scroll container (`overflow-y: auto | scroll | overlay` AND\n * `scrollHeight > clientHeight`) and drives THAT element; only when none exists\n * does it fall back to `window`. This is what makes it work inside app shells\n * that put page content in a fixed-height `<main class=\"overflow-y-auto\">`\n * (e.g. OpenFrame's `AppLayout`) where the document/window never scrolls — the\n * old window-only version was a silent no-op there. Note `overflow: clip` /\n * `hidden` are deliberately NOT treated as scroll containers, so a list wrapper\n * that uses `overflow-clip` only to round its corners still bubbles the scroll\n * up to the real container (matches the `<HelpCenterCard>` list intent).\n */\n\nexport interface ScrollElementIntoViewOptions {\n /** Pixels to subtract from the target element's `top` so it lands BELOW\n * sticky chrome. Defaults to 0. Pass `96` for the standard hub header. */\n headerOffset?: number;\n /** `'smooth'` (default) runs the self-driven tween; `'instant'` / `'auto'`\n * jump in one synchronous write (deep-link land, programmatic focus moves). */\n behavior?: ScrollBehavior;\n /** Optional adjustment applied to the computed pixel target each frame. The\n * callback receives the \"raw\" Y (`element.top + scrollY - headerOffset`) and\n * returns the FINAL target. Use when the caller knows about a layout shift\n * (e.g. a sibling drawer collapsing) the geometry can't yet reflect. */\n adjustTargetY?: (rawTargetY: number) => number;\n /** Tween duration in ms (smooth only). Default 320. */\n durationMs?: number;\n}\n\n/** Module-level handle to the in-flight tween so a new call (or a user\n * gesture) cancels the previous one — only ever one page-scroll animation at\n * a time. */\nlet activeRaf = 0;\nlet teardownActive: (() => void) | null = null;\n\nfunction cancelActiveScroll(): void {\n if (activeRaf) {\n cancelAnimationFrame(activeRaf);\n activeRaf = 0;\n }\n if (teardownActive) {\n teardownActive();\n teardownActive = null;\n }\n}\n\nconst easeOutCubic = (t: number): number => 1 - Math.pow(1 - t, 3);\n\n/** Nearest ancestor that is a *real* scroll container, or `null` when the\n * window/document is the scroller. Only `auto | scroll | overlay` count —\n * `clip` / `hidden` are intentionally excluded (a wrapper using `overflow-clip`\n * purely to round corners must let the scroll bubble to the page).\n *\n * THE one rule for \"what scrolls this element\": `useScrollSpy` resolves the\n * scroller it listens to with it, so the section a spy highlights is the\n * section this helper scrolls to — inside a `<main overflow-y-auto>` shell as\n * much as on a window-scrolled page. */\nexport function getScrollableAncestor(el: HTMLElement): HTMLElement | null {\n for (let node = el.parentElement; node; node = node.parentElement) {\n const overflowY = getComputedStyle(node).overflowY;\n if (\n (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') &&\n node.scrollHeight > node.clientHeight\n ) {\n return node;\n }\n }\n return null;\n}\n\n/**\n * Scroll the page so `target` lands at the top of the viewport (below sticky\n * chrome via `headerOffset`). SSR-safe; `null`/`undefined` target is a no-op so\n * callers can pass refs without defensive branching.\n */\nexport function scrollElementIntoView(\n target: HTMLElement | null | undefined,\n options: ScrollElementIntoViewOptions = {},\n): void {\n if (typeof window === 'undefined' || !target) return;\n const { headerOffset = 0, behavior = 'smooth', adjustTargetY, durationMs = 320 } = options;\n\n // Pick the scroller ONCE: a fixed-height `<main overflow-y-auto>` shell scrolls\n // the element, a plain document scrolls the window. The choice can't change\n // mid-tween, so resolve it up front and route every read/write through it.\n const container = getScrollableAncestor(target);\n const readCurrent = (): number => (container ? container.scrollTop : window.scrollY);\n const writeTo = (y: number): void => {\n if (container) container.scrollTop = y;\n else window.scrollTo(0, y);\n };\n\n // Target is recomputed every frame: the row's absolute position can move as\n // the page reflows (a sibling drawer collapsing) and the reachable max grows\n // as the just-opened drawer expands. Clamp to the LIVE max each frame.\n const computeTarget = (): number => {\n const raw = container\n ? container.scrollTop +\n (target.getBoundingClientRect().top - container.getBoundingClientRect().top) -\n headerOffset\n : target.getBoundingClientRect().top + window.scrollY - headerOffset;\n const adjusted = adjustTargetY ? adjustTargetY(raw) : raw;\n const maxScroll = container\n ? Math.max(0, container.scrollHeight - container.clientHeight)\n : Math.max(0, document.documentElement.scrollHeight - window.innerHeight);\n return Math.min(Math.max(0, adjusted), maxScroll);\n };\n\n // Any prior animation loses — one page scroll at a time.\n cancelActiveScroll();\n\n const prefersReduced =\n typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n // Instant paths: a single synchronous write. No tween, no anchoring race.\n if (behavior === 'instant' || behavior === 'auto' || prefersReduced) {\n writeTo(computeTarget());\n return;\n }\n\n // Smooth: self-driven tween with instant per-frame writes (anchoring-proof).\n let startY: number | null = null;\n let startTime = 0;\n\n // Bail the moment the user takes over with a real scroll gesture — we must\n // never fight them. (Not keydown: the ticket composer auto-focuses on open,\n // and typing there should not abort the scroll.)\n const onUserGesture = () => cancelActiveScroll();\n window.addEventListener('wheel', onUserGesture, { passive: true });\n window.addEventListener('touchmove', onUserGesture, { passive: true });\n teardownActive = () => {\n window.removeEventListener('wheel', onUserGesture);\n window.removeEventListener('touchmove', onUserGesture);\n };\n\n const step = (now: number) => {\n if (startY === null) {\n startY = readCurrent();\n startTime = now;\n }\n const targetY = computeTarget();\n const t = Math.min(1, (now - startTime) / durationMs);\n const y = startY + (targetY - startY) * easeOutCubic(t);\n writeTo(y);\n if (t < 1) {\n activeRaf = requestAnimationFrame(step);\n } else {\n // Final exact write in case easing left a sub-pixel gap, then teardown.\n writeTo(computeTarget());\n activeRaf = 0;\n if (teardownActive) {\n teardownActive();\n teardownActive = null;\n }\n }\n };\n activeRaf = requestAnimationFrame(step);\n}\n","import { findAnchorElementByNormalizedId } from './anchor-id';\nimport { scrollElementIntoView } from './scroll-into-view';\n\n/** Pages with a section-nav STRIP on top of the global hub header\n * (dev-center roadmap/delivery/tickets, FAQ category-pill nav).\n * Anchor lands BELOW both layers. */\nexport const STICKY_HEADER_OFFSET_PX = 96;\n\n/** Pages with only the global hub header (docs, blog, vendor detail).\n * Anchor lands BELOW the header bar. */\nexport const HUB_HEADER_OFFSET_PX = 80;\n\n/**\n * Take only the FIRST hash segment from a fragment that may contain extra\n * `#` characters. `'' → ''`, `'#a' → '#a'`, `'#a#b' → '#a'`.\n *\n * No real DOM id contains `#`, so a multi-fragment hash is always a bug at\n * the composer site; `navigateSamePageHash` + `useScrollToHash` both call\n * this so URL bar and `getElementById` stay in sync.\n */\nexport function normalizeHashFragment(hash: string): string {\n if (!hash) return '';\n const second = hash.indexOf('#', 1);\n return second < 0 ? hash : hash.slice(0, second);\n}\n\n/**\n * Resolve the DOM element a hash fragment id points at — THE hash resolver;\n * `navigateSamePageHash`, `useScrollToHash` and the markdown link renderer\n * all go through this one, so a fragment that resolves for a deep link\n * resolves for an in-body click too.\n *\n * Three passes, cheapest first:\n * 1. the raw id (ordinary anchors — the overwhelming common case);\n * 2. the percent-decoded form — href composers like `buildTicketOpenHref`\n * encode the id into the fragment while rows render the raw id, so\n * `#ticket-a%2Fb` must still find `id=\"ticket-a/b\"`. A malformed escape\n * sequence just falls through;\n * 3. a NORMALIZED match against the document's headings — doc bodies are\n * authored on GitHub, whose slugger keeps the hyphen left behind by a\n * stripped emoji (`## 📚 Table of Contents` → `#-table-of-contents`)\n * while our ids trim it. Without this every in-body TOC link in every\n * GitHub-authored doc resolves to nothing and clicking one does nothing.\n * See `utils/anchor-id`.\n */\nexport function getHashTargetElement(id: string): HTMLElement | null {\n if (!id) return null;\n const direct = document.getElementById(id);\n if (direct) return direct;\n\n let decoded = id;\n try {\n decoded = decodeURIComponent(id);\n if (decoded !== id) {\n const decodedEl = document.getElementById(decoded);\n if (decodedEl) return decodedEl;\n }\n } catch {\n // malformed escape — the raw lookup above already covered it\n decoded = id;\n }\n\n return findAnchorElementByNormalizedId(decoded, document);\n}\n\nexport interface NavigateSamePageHashOptions {\n /** Pixels to subtract for sticky chrome. */\n headerOffset?: number;\n /** `'push'` (default) — new history entry; `'replace'` — overwrite\n * current entry (use for TOC-style in-page navigators). */\n history?: 'push' | 'replace';\n}\n\n/**\n * Same-page hash navigation primitive: pushState + synthetic `hashchange`\n * + anchoring-proof smooth scroll. Replaces `router.push` for hash CTAs\n * (Next.js suppresses smooth-scroll during navigation; `router.push` on\n * an exact-URL match is a no-op). Returns `true` when the helper claimed\n * the nav (same pathname + search); `false` for cross-page targets so\n * callers fall through to `router.push`.\n *\n * `target` accepts an origin-stripped path (`/x#anchor`) or a bare hash\n * (`#anchor`); bare-hash callers don't need to reconstruct `pathname +\n * search` themselves.\n */\nexport function navigateSamePageHash(target: string, options: NavigateSamePageHashOptions = {}): boolean {\n if (typeof window === 'undefined') return false;\n const { headerOffset = 0, history: historyMode = 'push' } = options;\n const normalizedTarget = target.startsWith('#') ? window.location.pathname + window.location.search + target : target;\n // `new URL(absoluteUrl, base)` ignores `base` per RFC 3986; an absolute\n // cross-origin target sharing pathname/search would otherwise pass the\n // check below and trip pushState's same-origin enforcement. Parse with\n // an explicit base so malformed inputs cleanly fall through.\n let url: URL;\n try {\n url = new URL(normalizedTarget, window.location.href);\n } catch {\n return false;\n }\n if (\n url.origin !== window.location.origin ||\n url.pathname !== window.location.pathname ||\n url.search !== window.location.search\n ) {\n return false;\n }\n const current = window.location.pathname + window.location.search + window.location.hash;\n // Heal a malformed multi-fragment hash so the URL bar is clean and\n // `getElementById` resolves. Dev-warn fingers the upstream composer.\n const normalizedHash = normalizeHashFragment(url.hash);\n if (process.env.NODE_ENV === 'development' && normalizedHash !== url.hash) {\n console.warn(\n `[navigateSamePageHash] malformed fragment \"${url.hash}\" → normalizing to \"${normalizedHash}\". Fix the upstream composer.`,\n );\n }\n const next = url.pathname + url.search + normalizedHash;\n const id = normalizedHash && normalizedHash !== '#' ? normalizedHash.slice(1) : '';\n // Hash-less targets are only ours on an EXACT URL re-click.\n if (!id && next !== current) return false;\n if (next !== current) {\n const oldURL = window.location.href;\n if (historyMode === 'replace') {\n window.history.replaceState(null, '', next);\n } else {\n window.history.pushState(null, '', next);\n }\n // Synthetic `hashchange` — `pushState` doesn't fire it (HTML spec),\n // so URL-hash-bound listeners (FAQ auto-expand, etc.) wouldn't react.\n window.dispatchEvent(\n new HashChangeEvent('hashchange', {\n oldURL,\n newURL: window.location.href,\n }),\n );\n }\n const el = id ? getHashTargetElement(id) : null;\n if (id && !el && process.env.NODE_ENV === 'development') {\n console.warn(`[navigateSamePageHash] anchor \"#${id}\" not found — scrolling to top.`);\n }\n // Missing anchor → tween to page top. `documentElement` is at 0 by\n // definition, so one tween covers both branches.\n scrollElementIntoView(el ?? document.documentElement, {\n behavior: 'smooth',\n headerOffset,\n });\n return true;\n}\n"]}