@flamingo-stack/openframe-frontend-core 0.0.303 → 0.0.304

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 (116) hide show
  1. package/dist/{chunk-R2NVJUOB.js → chunk-2X4HTRQ4.js} +10 -22
  2. package/dist/chunk-2X4HTRQ4.js.map +1 -0
  3. package/dist/{chunk-UMYWG5C3.js → chunk-45DC5AJC.js} +2 -2
  4. package/dist/{chunk-KZGSVTRP.cjs → chunk-7KTSRZI4.cjs} +475 -543
  5. package/dist/chunk-7KTSRZI4.cjs.map +1 -0
  6. package/dist/{chunk-6D53QFBF.cjs → chunk-A25ZI7HO.cjs} +12 -12
  7. package/dist/{chunk-6D53QFBF.cjs.map → chunk-A25ZI7HO.cjs.map} +1 -1
  8. package/dist/{chunk-4JK3ZFMD.cjs → chunk-BMNGBMSN.cjs} +26 -26
  9. package/dist/{chunk-4JK3ZFMD.cjs.map → chunk-BMNGBMSN.cjs.map} +1 -1
  10. package/dist/{chunk-VAH46QQO.js → chunk-DOYOOBP4.js} +2 -2
  11. package/dist/{chunk-YKP7UXWT.cjs → chunk-FVLEE7YZ.cjs} +23 -35
  12. package/dist/chunk-FVLEE7YZ.cjs.map +1 -0
  13. package/dist/{chunk-FG4XA6NA.js → chunk-INZOAK77.js} +2 -2
  14. package/dist/{chunk-OZSU6S6U.js → chunk-JO6EUJGU.js} +21 -27
  15. package/dist/chunk-JO6EUJGU.js.map +1 -0
  16. package/dist/{chunk-UNWVMS3E.cjs → chunk-MZRNARMO.cjs} +37 -37
  17. package/dist/{chunk-UNWVMS3E.cjs.map → chunk-MZRNARMO.cjs.map} +1 -1
  18. package/dist/{chunk-SYTHAQRP.cjs → chunk-O4TIFKDG.cjs} +7 -7
  19. package/dist/{chunk-SYTHAQRP.cjs.map → chunk-O4TIFKDG.cjs.map} +1 -1
  20. package/dist/{chunk-3GYV6RP7.cjs → chunk-RNF2E736.cjs} +11 -10
  21. package/dist/chunk-RNF2E736.cjs.map +1 -0
  22. package/dist/{chunk-LTDGGKOW.cjs → chunk-UAJAJFI6.cjs} +44 -50
  23. package/dist/chunk-UAJAJFI6.cjs.map +1 -0
  24. package/dist/{chunk-JEHWEKWA.js → chunk-X5N6ANEO.js} +4 -3
  25. package/dist/{chunk-JEHWEKWA.js.map → chunk-X5N6ANEO.js.map} +1 -1
  26. package/dist/{chunk-SLRDPGGS.js → chunk-Y2D2RJQX.js} +2694 -2762
  27. package/dist/chunk-Y2D2RJQX.js.map +1 -0
  28. package/dist/{chunk-OFLTDHC2.js → chunk-YV73VRRY.js} +2 -2
  29. package/dist/{chunk-ZCXABON3.cjs → chunk-Z7322A4A.cjs} +5 -5
  30. package/dist/{chunk-ZCXABON3.cjs.map → chunk-Z7322A4A.cjs.map} +1 -1
  31. package/dist/{chunk-ZRSS67EY.js → chunk-ZXIM2DJM.js} +2 -2
  32. package/dist/components/case-studies/index.cjs +8 -8
  33. package/dist/components/case-studies/index.js +2 -2
  34. package/dist/components/chat/hooks/use-chat-identity.d.ts +7 -1
  35. package/dist/components/chat/hooks/use-chat-identity.d.ts.map +1 -1
  36. package/dist/components/chat/hooks/use-empty-state-config.d.ts.map +1 -1
  37. package/dist/components/chat/hooks/use-slash-commands.d.ts.map +1 -1
  38. package/dist/components/chat/index.cjs +2 -2
  39. package/dist/components/chat/index.js +1 -1
  40. package/dist/components/contact/index.cjs +3 -3
  41. package/dist/components/contact/index.js +2 -2
  42. package/dist/components/docs/doc-viewer.d.ts +3 -4
  43. package/dist/components/docs/doc-viewer.d.ts.map +1 -1
  44. package/dist/components/docs/index.cjs +5 -5
  45. package/dist/components/docs/index.js +4 -4
  46. package/dist/components/docs/use-docs-resolve-link.d.ts.map +1 -1
  47. package/dist/components/docs/use-document-tree.d.ts.map +1 -1
  48. package/dist/components/embeds/index.cjs +3 -3
  49. package/dist/components/embeds/index.js +2 -2
  50. package/dist/components/faq/index.cjs +3 -3
  51. package/dist/components/faq/index.js +2 -2
  52. package/dist/components/features/index.cjs +2 -2
  53. package/dist/components/features/index.js +1 -1
  54. package/dist/components/index.cjs +172 -178
  55. package/dist/components/index.cjs.map +1 -1
  56. package/dist/components/index.js +8 -14
  57. package/dist/components/index.js.map +1 -1
  58. package/dist/components/layout/page-layout.d.ts +1 -10
  59. package/dist/components/layout/page-layout.d.ts.map +1 -1
  60. package/dist/components/layout/title-block.d.ts +1 -17
  61. package/dist/components/layout/title-block.d.ts.map +1 -1
  62. package/dist/components/navigation/index.cjs +2 -2
  63. package/dist/components/navigation/index.js +1 -1
  64. package/dist/components/onboarding-guides/index.cjs +23 -23
  65. package/dist/components/onboarding-guides/index.js +3 -3
  66. package/dist/components/related-content/index.cjs +3 -3
  67. package/dist/components/related-content/index.js +2 -2
  68. package/dist/components/shared/dev-section/dev-section-page.d.ts +0 -9
  69. package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
  70. package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -1
  71. package/dist/components/shared/dev-section/index.d.ts +1 -1
  72. package/dist/components/shared/dev-section/index.d.ts.map +1 -1
  73. package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -1
  74. package/dist/components/tickets/index.cjs +60 -60
  75. package/dist/components/tickets/index.js +3 -3
  76. package/dist/components/ui/index.cjs +2 -6
  77. package/dist/components/ui/index.cjs.map +1 -1
  78. package/dist/components/ui/index.d.ts +0 -1
  79. package/dist/components/ui/index.d.ts.map +1 -1
  80. package/dist/components/ui/index.js +1 -5
  81. package/dist/index.cjs +2 -6
  82. package/dist/index.cjs.map +1 -1
  83. package/dist/index.js +1 -5
  84. package/package.json +1 -1
  85. package/src/components/chat/embeddable-chat.tsx +25 -3
  86. package/src/components/chat/hooks/use-chat-identity.ts +13 -2
  87. package/src/components/chat/hooks/use-empty-state-config.ts +30 -16
  88. package/src/components/chat/hooks/use-slash-commands.ts +24 -8
  89. package/src/components/docs/doc-viewer.tsx +25 -22
  90. package/src/components/docs/use-docs-resolve-link.ts +2 -1
  91. package/src/components/docs/use-document-tree.ts +3 -2
  92. package/src/components/layout/page-layout.tsx +28 -14
  93. package/src/components/layout/title-block.tsx +86 -40
  94. package/src/components/shared/dev-section/dev-section-page.tsx +1 -9
  95. package/src/components/shared/dev-section/dev-section-view.tsx +9 -14
  96. package/src/components/shared/dev-section/index.ts +1 -1
  97. package/src/components/shared/doc-search/use-doc-search.ts +2 -1
  98. package/src/components/ui/index.ts +0 -1
  99. package/dist/chunk-3GYV6RP7.cjs.map +0 -1
  100. package/dist/chunk-KZGSVTRP.cjs.map +0 -1
  101. package/dist/chunk-LTDGGKOW.cjs.map +0 -1
  102. package/dist/chunk-OZSU6S6U.js.map +0 -1
  103. package/dist/chunk-R2NVJUOB.js.map +0 -1
  104. package/dist/chunk-SLRDPGGS.js.map +0 -1
  105. package/dist/chunk-YKP7UXWT.cjs.map +0 -1
  106. package/dist/components/layout/page-header.d.ts +0 -78
  107. package/dist/components/layout/page-header.d.ts.map +0 -1
  108. package/dist/components/layout/page-with-header.d.ts +0 -67
  109. package/dist/components/layout/page-with-header.d.ts.map +0 -1
  110. package/src/components/layout/page-header.tsx +0 -182
  111. package/src/components/layout/page-with-header.tsx +0 -110
  112. /package/dist/{chunk-UMYWG5C3.js.map → chunk-45DC5AJC.js.map} +0 -0
  113. /package/dist/{chunk-VAH46QQO.js.map → chunk-DOYOOBP4.js.map} +0 -0
  114. /package/dist/{chunk-FG4XA6NA.js.map → chunk-INZOAK77.js.map} +0 -0
  115. /package/dist/{chunk-OFLTDHC2.js.map → chunk-YV73VRRY.js.map} +0 -0
  116. /package/dist/{chunk-ZRSS67EY.js.map → chunk-ZXIM2DJM.js.map} +0 -0
@@ -62,14 +62,25 @@ export async function fetchSlashCommands(
62
62
  signal: AbortSignal | undefined,
63
63
  commandsUrl: string,
64
64
  ): Promise<SlashCommandSummary[]> {
65
- const url = new URL(commandsUrl, window.location.origin);
66
- if (prefix) url.searchParams.set("q", prefix);
67
- // `headers: {}` opts out of the default `Content-Type: application/json`
68
- // this is a bare GET with no body, so no content-type is needed.
69
- const res = await embedAuthedFetch(url.toString(), { signal, headers: {} });
70
- if (!res.ok) return [];
71
- const data = (await res.json()) as { commands?: SlashCommandSummary[] };
72
- return data.commands ?? [];
65
+ try {
66
+ const url = new URL(commandsUrl, window.location.origin);
67
+ if (prefix) url.searchParams.set("q", prefix);
68
+ // `headers: {}` opts out of the default `Content-Type: application/json`
69
+ // this is a bare GET with no body, so no content-type is needed.
70
+ const res = await embedAuthedFetch(url.toString(), { signal, headers: {} });
71
+ if (!res.ok) return [];
72
+ const data = (await res.json()) as { commands?: SlashCommandSummary[] };
73
+ return data.commands ?? [];
74
+ } catch (err) {
75
+ // Cancellation (unmount / dep change) MUST propagate so react-query treats
76
+ // it as cancelled, not as a successful empty result. Every OTHER failure
77
+ // (network down, proxy reject, non-JSON body) degrades to "no commands" so
78
+ // a flaky commands endpoint can NEVER break the chat — the autocomplete /
79
+ // onboarding list just renders empty.
80
+ if ((err as Error)?.name === "AbortError") throw err;
81
+ console.warn("[chat] slash-commands fetch failed, showing none:", err);
82
+ return [];
83
+ }
73
84
  }
74
85
 
75
86
  /**
@@ -144,6 +155,11 @@ export function useSlashCommandRegistry(
144
155
  enabled: options?.enabled ?? true,
145
156
  staleTime: Infinity,
146
157
  gcTime: Infinity,
158
+ // The commands registry is non-critical chrome — a failure degrades to an
159
+ // empty registry (handled in `fetchSlashCommands`). Don't retry: settle to
160
+ // the neutral empty state immediately so a flaky endpoint never holds the
161
+ // welcome UI in a loading spinner.
162
+ retry: false,
147
163
  });
148
164
  return {
149
165
  commands: query.data ?? [],
@@ -2,7 +2,6 @@
2
2
 
3
3
  import React, { useMemo } from "react"
4
4
  import { MultiLevelNavigation, MobileNavigationDropdown } from "../navigation/multi-level-navigation"
5
- import { PageHeader } from "../layout/page-header"
6
5
  import { PageLayout } from "../layout/page-layout"
7
6
  import { PageShell } from "../layout/article-detail-layout"
8
7
  import { useRouter } from "../../embed-shims/next-navigation"
@@ -59,10 +58,9 @@ export interface DocViewerProps {
59
58
  */
60
59
  chatSource: string
61
60
 
62
- /** Page title — rendered via the shared `<PageHeader>` primitive
63
- * so the doc-viewer chrome matches every other lib page
64
- * (DevSectionPage / LegalDocumentPage / OnboardingGuideDetailView)
65
- * pixel-for-pixel. ReactNode is intentionally not supported here —
61
+ /** Page title — rendered as the inline hero `<h1>` (same DOM
62
+ * `<DevSectionView>`'s hero uses) so the doc-viewer chrome matches the
63
+ * dev-section pages. ReactNode is intentionally not supported here —
66
64
  * every consumer renders the same typography. */
67
65
  title?: string
68
66
  /** Optional icon rendered inline before the title text — same slot
@@ -259,15 +257,10 @@ function DocViewerContent({
259
257
  const resolvedEmptyText = emptyStateText || defaultEmptyText
260
258
 
261
259
  return (
262
- // STRUCTURAL UNIFICATION: render through the IDENTICAL wrapper chain
263
- // `<DevSectionPage>` uses (PageShell → PageLayout → `gap-10 flex-col`),
264
- // not a hand-rolled custom container with similar-looking spacing.
265
- // PageLayout owns the back-button row; the inner `gap-10` div +
266
- // `<PageHeader noTopPadding noBottomMargin>` renders the title section
267
- // the same way `<DevSectionView>`'s hero does. This is the only way to
268
- // guarantee /knowledge-base sits at pixel-identical vertical rhythm to
269
- // /roadmap / /releases / /onboarding-guides — same components, same DOM,
270
- // not "same CSS classes that look similar on paper."
260
+ // Render through the shared wrapper chain (PageShell → PageLayout →
261
+ // `gap-10 flex-col`). PageLayout owns the back-button row; the inner
262
+ // `gap-10` div holds an inline title hero (same DOM `<DevSectionView>`'s
263
+ // hero renders) followed by the search bar + content grid.
271
264
  //
272
265
  // `colorPalette` / `className` / `bgStyle` flow through PageShell's
273
266
  // contentClassName + an inner style-passthrough wrapper so legacy
@@ -277,14 +270,24 @@ function DocViewerContent({
277
270
  <div style={{ ...bgStyle, ...containerBgStyle }}>
278
271
  <PageLayout backButton={backCfg ?? undefined}>
279
272
  <div className="w-full flex flex-col gap-10">
280
- <PageHeader
281
- title={title}
282
- titleIcon={titleIcon}
283
- subtitle={subtitle}
284
- accentDot={accentDot}
285
- noTopPadding
286
- noBottomMargin
287
- />
273
+ {(title || titleIcon || subtitle) && (
274
+ <div className="space-y-4">
275
+ {(title || titleIcon) && (
276
+ <h1 className="text-h1 tracking-[-1.12px] text-ods-text-primary flex items-center gap-3">
277
+ {titleIcon}
278
+ {title && (
279
+ <span>
280
+ {title}
281
+ {accentDot && <span className="text-ods-accent">.</span>}
282
+ </span>
283
+ )}
284
+ </h1>
285
+ )}
286
+ <p className="font-['DM_Sans'] font-medium text-[18px] leading-[28px] text-ods-text-secondary max-w-3xl line-clamp-2 min-h-[56px]">
287
+ {subtitle || ' '}
288
+ </p>
289
+ </div>
290
+ )}
288
291
 
289
292
  {showAIChat && (
290
293
  <DocSearchBar
@@ -1,6 +1,7 @@
1
1
  import { useCallback } from 'react'
2
2
  import { useChatRuntime } from '../../contexts/chat-runtime-context'
3
3
  import type { ResolveLinkResult } from '../../types/doc-source'
4
+ import { contentFetch } from '../../utils/embed-content-fetch'
4
5
 
5
6
  /**
6
7
  * `useDocsResolveLink(sourceId, override?)` — POST `/api/docs/resolve-link`
@@ -30,7 +31,7 @@ export function useDocsResolveLink(
30
31
  return useCallback(
31
32
  async (href: string, currentPath: string): Promise<ResolveLinkResult> => {
32
33
  try {
33
- const response = await fetch(resolvedResolveLinkEndpoint, {
34
+ const response = await contentFetch(resolvedResolveLinkEndpoint, {
34
35
  method: 'POST',
35
36
  headers: { 'Content-Type': 'application/json' },
36
37
  body: JSON.stringify({ link: href, currentPath, source: sourceId }),
@@ -9,6 +9,7 @@ import {
9
9
  DEFAULT_FOLDER_INDEX_FILE,
10
10
  } from '../../utils/doc-tree-nav'
11
11
  import { useDocNavigation } from './doc-navigation-context'
12
+ import { contentFetch } from '../../utils/embed-content-fetch'
12
13
  import { scrollElementIntoView } from '../../utils/scroll-into-view'
13
14
  import { navigateSamePageHash, HUB_HEADER_OFFSET_PX } from '../../utils/same-page-hash-nav'
14
15
 
@@ -179,7 +180,7 @@ export function useDocumentTree(
179
180
  setIsLoadingStructure(true)
180
181
  setError(null)
181
182
 
182
- const response = await fetch(structureEndpoint)
183
+ const response = await contentFetch(structureEndpoint)
183
184
 
184
185
  if (!response.ok) {
185
186
  throw new Error('Failed to load documentation structure')
@@ -232,7 +233,7 @@ export function useDocumentTree(
232
233
  // returns early without writing to state. Clearing error here would
233
234
  // briefly flicker the user-visible error message.
234
235
 
235
- const response = await fetch(`${contentEndpoint}?path=${encodeURIComponent(path)}`)
236
+ const response = await contentFetch(`${contentEndpoint}?path=${encodeURIComponent(path)}`)
236
237
 
237
238
  // Request-id guard: between awaits, `lastFetchedPath.current` may have
238
239
  // been bumped by a newer fetch (the structure-arrives auto-select issues
@@ -1,5 +1,32 @@
1
1
  'use client'
2
2
 
3
+ /* ============================================================================
4
+ * ⛔️ FROZEN — DO NOT MODIFY (AI agents & contributors, read this first)
5
+ * ----------------------------------------------------------------------------
6
+ * `PageLayout` and its `TitleBlock` are a FINALIZED, locked component. They are
7
+ * the canonical, stable page chrome for OpenFrame surfaces and their visual +
8
+ * behavioral contract is intentionally complete. Treat this file as read-only.
9
+ *
10
+ * Do NOT: change the markup/CSS, swap the title typography (`text-h2`) or
11
+ * subtitle (`text-h6`), re-architect this to delegate to another primitive,
12
+ * add/rename props, or "unify"/"refactor"/"simplify" it. Do NOT restyle to
13
+ * match some other surface.
14
+ *
15
+ * Why this rule exists (the incident it prevents): a refactor once re-styled
16
+ * this layout (bumped the title to `text-h1`, rerouted it through a new
17
+ * `PageHeader`/`PageWithHeader` chain) to "unify" page chrome. That silently
18
+ * changed the look of every page rendered through `PageLayout` and had to be
19
+ * fully reverted. The current code IS the reverted, correct baseline.
20
+ *
21
+ * MANY consumers depend on the EXACT current output — not only OpenFrame pages
22
+ * but also `DevSectionPage`, `DocViewer`, and the multi-platform hub (through
23
+ * its own local `PageWithHeader`). A change here ripples across all of them.
24
+ *
25
+ * If a new design genuinely needs different chrome: build a SEPARATE new
26
+ * component for it. Never mutate this one. If you believe an edit here is
27
+ * unavoidable, STOP and get explicit human sign-off first.
28
+ * ========================================================================== */
29
+
3
30
  import React from 'react'
4
31
  import { cn } from '../../utils/cn'
5
32
  import type { ActionsMenuGroup } from '../ui/actions-menu'
@@ -10,11 +37,6 @@ export interface PageLayoutProps {
10
37
  children: React.ReactNode
11
38
  title?: string
12
39
  subtitle?: string
13
- /** Inline icon rendered before the title text — forwarded to
14
- * TitleBlock/PageHeader. Same shape as DevSectionPage's hero icon. */
15
- titleIcon?: React.ReactNode
16
- /** Yellow accent dot after the title — forwarded to TitleBlock/PageHeader. */
17
- accentDot?: boolean
18
40
  image?: { src: string; alt?: string }
19
41
  backButton?: { label?: string; onClick: () => void }
20
42
  actions?: PageActionButton[]
@@ -38,8 +60,6 @@ export function PageLayout({
38
60
  children,
39
61
  title,
40
62
  subtitle,
41
- titleIcon,
42
- accentDot,
43
63
  image,
44
64
  backButton,
45
65
  actions,
@@ -53,16 +73,14 @@ export function PageLayout({
53
73
  }: PageLayoutProps) {
54
74
  const hasActions = actions && actions.length > 0
55
75
  const needsBottomPadding = hasActions && actionsVariant === 'primary-buttons'
56
- const hasHeader = showHeader && (title || subtitle || titleIcon || image || backButton || hasActions || selector)
76
+ const hasHeader = showHeader && (title || subtitle || image || backButton || hasActions || selector)
57
77
 
58
78
  return (
59
79
  <div className={cn('flex flex-col w-full', className)}>
60
80
  {hasHeader && (
61
81
  <TitleBlock
62
82
  title={title}
63
- titleIcon={titleIcon}
64
83
  subtitle={subtitle}
65
- accentDot={accentDot}
66
84
  image={image}
67
85
  backButton={backButton}
68
86
  actions={actions}
@@ -83,8 +101,4 @@ export function PageLayout({
83
101
  export type { PageActionButton } from '../ui/page-actions'
84
102
  export { TitleBlock } from './title-block'
85
103
  export type { TitleBlockProps } from './title-block'
86
- export { PageHeader } from './page-header'
87
- export type { PageHeaderProps } from './page-header'
88
- export { PageWithHeader } from './page-with-header'
89
- export type { PageWithHeaderProps } from './page-with-header'
90
104
  export default PageLayout
@@ -1,29 +1,39 @@
1
1
  'use client'
2
2
 
3
+ /* ============================================================================
4
+ * ⛔️ FROZEN — DO NOT MODIFY (AI agents & contributors, read this first)
5
+ * ----------------------------------------------------------------------------
6
+ * `TitleBlock` is the FINALIZED title/subtitle/back-button/actions chrome used
7
+ * by `PageLayout`. It is a locked, complete component — treat it as read-only.
8
+ *
9
+ * Do NOT: change the markup/CSS, alter the title typography (`text-h2`) or
10
+ * subtitle (`text-h6`), change the image/title 2-column layout, re-route this
11
+ * through a `PageHeader`/`PageWithHeader` primitive, or add/rename props. Do
12
+ * NOT "unify"/"refactor"/"simplify" it or restyle it to match another surface.
13
+ *
14
+ * Why this rule exists (the incident it prevents): a refactor once rewrote
15
+ * this to delegate to a new `PageHeader` (title bumped to `text-h1`, new
16
+ * subtitle styling) to "unify" page chrome — it silently changed every page
17
+ * using `PageLayout` and had to be fully reverted. This code IS that reverted,
18
+ * correct baseline.
19
+ *
20
+ * Downstream consumers (OpenFrame pages, `DevSectionPage`, `DocViewer`, and the
21
+ * multi-platform hub via its local `PageWithHeader`) depend on the EXACT
22
+ * current output. If a new design needs different chrome, build a SEPARATE new
23
+ * component — never mutate this one. If an edit here seems unavoidable, STOP
24
+ * and get explicit human sign-off first.
25
+ * ========================================================================== */
26
+
3
27
  import React from 'react'
28
+ import { cn } from '../../utils/cn'
4
29
  import type { ActionsMenuGroup } from '../ui/actions-menu'
30
+ import { EntityImage } from '../ui/entity-image'
5
31
  import { PageActions, type PageActionButton } from '../ui/page-actions'
6
- import { PageHeader } from './page-header'
32
+ import { BackButton } from './back-button'
7
33
 
8
- /**
9
- * `<TitleBlock>` — thin adapter over `<PageHeader>` that turns the
10
- * `actions: PageActionButton[]` / `menuActions` / `selector` API into
11
- * a `ReactNode` slot that PageHeader can render. Kept as a separate
12
- * component for backwards compatibility (`PageLayout` consumes it,
13
- * external callers may too) — all the DOM/CSS lives in PageHeader.
14
- *
15
- * If a new consumer doesn't need the `PageActions` wiring, prefer
16
- * `<PageHeader>` directly.
17
- */
18
34
  export interface TitleBlockProps {
19
35
  title?: string
20
36
  subtitle?: string
21
- /** Inline icon rendered before the title text (e.g. HelpCircle on /faqs,
22
- * BookOpen on /knowledge-base, Map on /roadmap). Forwarded verbatim to
23
- * `<PageHeader>`. */
24
- titleIcon?: React.ReactNode
25
- /** Yellow accent dot after the title — same flag as PageHeader. */
26
- accentDot?: boolean
27
37
  image?: { src: string; alt?: string }
28
38
  backButton?: { label?: string; onClick: () => void }
29
39
  actions?: PageActionButton[]
@@ -43,8 +53,6 @@ export interface TitleBlockProps {
43
53
  export function TitleBlock({
44
54
  title,
45
55
  subtitle,
46
- titleIcon,
47
- accentDot,
48
56
  image,
49
57
  backButton,
50
58
  actions,
@@ -56,29 +64,67 @@ export function TitleBlock({
56
64
  }: TitleBlockProps) {
57
65
  const hasActions = actions && actions.length > 0
58
66
  const hasMenuActions = !!menuActions && menuActions.some(g => g.items.length > 0)
59
- const hasActionsSlot = hasActions || hasMenuActions || !!selector
60
-
61
- const actionsNode = hasActionsSlot ? (
62
- <PageActions
63
- variant={actionsVariant}
64
- actions={actions ?? []}
65
- menuActions={menuActions}
66
- selector={selector}
67
- />
68
- ) : undefined
69
67
 
70
68
  return (
71
- <PageHeader
72
- title={title}
73
- titleIcon={titleIcon}
74
- subtitle={subtitle}
75
- accentDot={accentDot}
76
- image={image}
77
- backButton={backButton}
78
- actions={actionsNode}
79
- variant={variant}
80
- className={className}
81
- />
69
+ <div
70
+ className={cn(
71
+ 'flex items-end justify-between gap-[var(--spacing-system-m)]',
72
+ 'md:flex-col md:items-start md:justify-start lg:flex-row lg:items-end lg:justify-between',
73
+ 'pt-[var(--spacing-system-l)]',
74
+ variant === 'card'
75
+ ? cn(
76
+ 'bg-ods-card border-b border-ods-border',
77
+ 'px-[var(--spacing-system-l)] pb-[var(--spacing-system-l)]',
78
+ 'md:bg-transparent md:border-b-0',
79
+ 'md:px-0 md:pb-0',
80
+ 'md:mb-[var(--spacing-system-l)]',
81
+ )
82
+ : 'mb-[var(--spacing-system-l)]',
83
+ className,
84
+ )}
85
+ >
86
+ <div className="flex flex-col gap-[var(--spacing-system-xs)] flex-1 min-w-0">
87
+ {backButton && (
88
+ <BackButton
89
+ onClick={backButton.onClick}
90
+ label={backButton.label}
91
+ className="hidden md:inline-flex"
92
+ />
93
+ )}
94
+ {(image || subtitle) ? (
95
+ <div className="flex items-center gap-[var(--spacing-system-m)] min-w-0 w-full">
96
+ {image && (
97
+ <EntityImage
98
+ src={image.src}
99
+ alt={image.alt}
100
+ fallbackText={image.alt || title}
101
+ />
102
+ )}
103
+ <div className="flex flex-col justify-center min-w-0 flex-1">
104
+ {title && (
105
+ <h1 className="text-h2 text-ods-text-primary truncate" title={title}>{title}</h1>
106
+ )}
107
+ {subtitle && (
108
+ <p className="text-h6 text-ods-text-secondary truncate" title={subtitle}>{subtitle}</p>
109
+ )}
110
+ </div>
111
+ </div>
112
+ ) : (
113
+ title && <h1 className="text-h2 text-ods-text-primary">{title}</h1>
114
+ )}
115
+ </div>
116
+
117
+ {(hasActions || hasMenuActions || selector) && (
118
+ <div className="flex gap-2 items-center shrink-0">
119
+ <PageActions
120
+ variant={actionsVariant}
121
+ actions={actions ?? []}
122
+ menuActions={menuActions}
123
+ selector={selector}
124
+ />
125
+ </div>
126
+ )}
127
+ </div>
82
128
  )
83
129
  }
84
130
 
@@ -25,15 +25,7 @@ import {
25
25
  type OpenframeDevSectionKey,
26
26
  } from '../../../utils/dev-sections/openframe-dev-sections';
27
27
 
28
- /** Re-export the constant so existing dev-section call sites keep their
29
- * old import path. The canonical home is `src/utils/page-header-constants.ts`
30
- * (NOT a `'use client'` module) so server modules can import it without
31
- * Next.js turning it into a client reference proxy — that proxy is what
32
- * blew up lucide's `mergeClasses().trim()` when used as
33
- * `<Icon className={SECTION_HERO_ICON_CLASS} />` inside a hub
34
- * server-component preset. */
35
- import { SECTION_HERO_ICON_CLASS } from '../../../utils/page-header-constants';
36
- export { SECTION_HERO_ICON_CLASS };
28
+ const SECTION_HERO_ICON_CLASS = 'h-10 w-10 text-ods-accent';
37
29
 
38
30
  export interface DevSectionPageProps {
39
31
  sectionKey: OpenframeDevSectionKey;
@@ -19,7 +19,6 @@ import { useState, useEffect } from 'react';
19
19
  import { useRouter, useSearchParams, usePathname } from '../../../embed-shims';
20
20
  import { SearchInput } from '../../ui';
21
21
  import { StatusFilterComponent } from '../../features';
22
- import { PageHeader } from '../../layout/page-header';
23
22
  import {
24
23
  OPENFRAME_DEV_SECTIONS,
25
24
  type OpenframeDevSectionKey,
@@ -96,19 +95,15 @@ export function DevSectionView({ sectionKey, hero, preControls, children }: DevS
96
95
  return (
97
96
  <div className="w-full flex flex-col gap-10">
98
97
  {hero ? (
99
- // Render through the shared `<PageHeader>` primitive so the dev-
100
- // section hero (Releases, Roadmap, Onboarding catalog) and the
101
- // docs-hub hero (Knowledge Hub, Data Room) are LITERALLY the same
102
- // component rendering the same DOM/CSS. `noBottomMargin` because
103
- // the parent `gap-10` already supplies the spacing to the next
104
- // sibling (preControls / search / filter).
105
- <PageHeader
106
- title={hero.title ?? section.hero.title}
107
- titleIcon={hero.icon}
108
- subtitle={hero.description}
109
- noBottomMargin
110
- noTopPadding
111
- />
98
+ <div className="space-y-4">
99
+ <h1 className="text-h1 tracking-[-1.12px] text-ods-text-primary flex items-center gap-3">
100
+ {hero.icon}
101
+ {hero.title ?? section.hero.title}
102
+ </h1>
103
+ <p className="font-['DM_Sans'] font-medium text-[18px] leading-[28px] text-ods-text-secondary max-w-3xl">
104
+ {hero.description}
105
+ </p>
106
+ </div>
112
107
  ) : (
113
108
  <div className="flex items-center justify-between w-full">
114
109
  <h2 className="font-['Azeret_Mono'] font-semibold text-[32px] md:text-[40px] lg:text-[48px] leading-[40px] md:leading-[48px] lg:leading-[56px] text-ods-text-primary tracking-[-0.64px] md:tracking-[-0.8px] lg:tracking-[-0.96px]">
@@ -1,5 +1,5 @@
1
1
  export { DevSectionView, type DevSectionViewProps } from './dev-section-view';
2
- export { DevSectionPage, type DevSectionPageProps, SECTION_HERO_ICON_CLASS } from './dev-section-page';
2
+ export { DevSectionPage, type DevSectionPageProps } from './dev-section-page';
3
3
  export {
4
4
  DevCardRowContent,
5
5
  DevCardRowSkeleton,
@@ -32,6 +32,7 @@ import { useState, useEffect, useCallback } from 'react'
32
32
  import { useRouter } from '../../../embed-shims'
33
33
  import { useDebounce } from '../../../hooks/ui/use-debounce'
34
34
  import { useChatRuntime } from '../../../contexts/chat-runtime-context'
35
+ import { contentFetch } from '../../../utils/embed-content-fetch'
35
36
  import type { SearchResult } from '../../ui/search-input'
36
37
  import {
37
38
  resolveExternalNavigation,
@@ -118,7 +119,7 @@ export function useDocSearch(config: UseDocSearchConfig) {
118
119
  })
119
120
  if (tableIdsKey) params.set('tableIds', tableIdsKey)
120
121
 
121
- const response = await fetch(`${resolvedSearchEndpoint}?${params.toString()}`)
122
+ const response = await contentFetch(`${resolvedSearchEndpoint}?${params.toString()}`)
122
123
  if (!response.ok) {
123
124
  throw new Error(`Search request failed: ${response.status}`)
124
125
  }
@@ -61,7 +61,6 @@ export * from './hover-dropdown'
61
61
  export * from '../chat'
62
62
  export * from '../layout/list-page-layout'
63
63
  export * from '../layout/page-container'
64
- export * from '../layout/page-header'
65
64
  export * from '../layout/page-heading'
66
65
  export * from '../layout/page-layout'
67
66
  export * from '../layout/article-detail-layout'
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-3GYV6RP7.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-3GYV6RP7.cjs","sourcesContent":[null,"/**\n * Format a full document path as a breadcrumb trail.\n * Shows parent folders only (excludes the last segment / filename).\n *\n * @example\n * formatRelativePath('openframe-oss-tenant/architecture/api-controllers.md')\n * // → 'Openframe oss tenant / Architecture'\n */\nexport function formatRelativePath(fullPath: string): string {\n if (!fullPath) return ''\n const segments = fullPath.replace(/\\.md$/, '').split('/')\n // Show only parent path (exclude the filename itself since the title already shows it)\n const parentSegments = segments.length > 1 ? segments.slice(0, -1) : segments\n return parentSegments\n .map((seg) => seg.charAt(0).toUpperCase() + seg.slice(1).replace(/-/g, ' '))\n .join(' / ')\n}\n","'use client'\n\n/**\n * Single row in the `<SearchInput>` dropdown — the standard layout\n * used by every doc-search-backed surface (company-hub data-room\n * search bar, onboarding-guide catalog search, …). Single source of\n * truth for the row appearance so search dropdowns are visually\n * identical everywhere.\n *\n * Resolves the source icon via the same `resolveSourceIcon()`\n * registry the inline chat-card refs use, so a row pointing at e.g.\n * an onboarding-guide surfaces the SAME `<GraduationCap>` glyph the\n * chat card surfaces — no cross-surface drift.\n */\n\nimport { resolveSourceIcon } from '../../chat/utils/source-row-cta'\nimport { formatRelativePath } from './format-relative-path'\n\n/**\n * Minimal result shape this row renders. Compatible with any\n * doc-search hook whose result type exposes `{ title?, path?,\n * metadata? }`. The two hub consumers (onboarding-guide catalog,\n * data-room sidebar) both satisfy this shape via their\n * `useDocSearch` hook result.\n */\nexport interface DocSearchResultRowEntry {\n title?: string\n path?: string\n metadata?: Record<string, unknown>\n}\n\nexport interface DocSearchResultRowProps {\n result: DocSearchResultRowEntry\n isHighlighted: boolean\n}\n\nexport function DocSearchResultRow({\n result,\n isHighlighted,\n}: DocSearchResultRowProps) {\n const docType = (result.metadata?.documentType as string) || undefined\n const sourceRepo = (result.metadata?.sourceRepo as string) || undefined\n const { Icon: SourceIcon, label: iconLabel } = resolveSourceIcon({\n sourceRepo,\n documentType: docType,\n })\n const isGroup = result.metadata?.isGroup as boolean | undefined\n\n return (\n <div className=\"flex items-center gap-3 w-full min-w-0\">\n <span\n className=\"flex-shrink-0 text-ods-text-secondary\"\n title={iconLabel}\n >\n <SourceIcon className=\"size-4\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <div\n className={`text-sm font-medium leading-5 truncate ${\n isHighlighted ? 'text-ods-accent' : 'text-ods-text-primary'\n }`}\n >\n {result.title || result.path}\n </div>\n {!isGroup && result.path?.includes('/') && (\n <div className=\"text-xs leading-4 text-ods-text-secondary truncate mt-0.5\">\n {formatRelativePath(result.path)}\n </div>\n )}\n </div>\n </div>\n )\n}\n","'use client'\n\n/**\n * `<DocSearchBar>` — the canonical RAG-search dropdown surface.\n *\n * Mounted by every doc-search consumer (data-room sidebar, onboarding-\n * guide catalog, and any future surface that needs typeahead against\n * `/api/docs/search`). Wraps `<SearchInput>` with the lib's standard\n * `<DocSearchResultRow>` so the dropdown looks identical everywhere.\n *\n * ## Why a presentation component, not a \"search bar that owns its\n * own hook\"\n *\n * The data-fetching hook (`useDocSearch`) lives hub-side because it\n * depends on hub-only context (`useDocNavigation`, the rag-table-\n * config registry, the hub's `decideNewTab` helper). Moving the hook\n * would cascade ~5 more file migrations into the lib.\n *\n * Instead, the hook stays hub-side and callers pass its result into\n * this component as plain props. Both consumers shrink to ~5 lines.\n */\n\nimport type { ReactNode } from 'react'\nimport { SearchInput, type SearchResult } from '../../ui/search-input'\nimport { DocSearchResultRow } from './doc-search-result-row'\n\nexport interface DocSearchBarProps {\n placeholder: string\n query: string\n onQueryChange: (value: string) => void\n /** Hook-fetched results. Reuses the lib's `<SearchInput>` `SearchResult`\n * shape directly so callers don't translate. */\n results: SearchResult[]\n isLoading: boolean\n /** Result selection handler. Mirrors `<SearchInput>` — the second\n * `modifiers` argument is preserved so cmd-click / shift-click on\n * a result row still forces new-tab behavior. Hub `useDocSearch`\n * reads these to short-circuit to `window.open()`. */\n onResultSelect: (\n result: SearchResult,\n modifiers?: {\n metaKey?: boolean\n ctrlKey?: boolean\n shiftKey?: boolean\n altKey?: boolean\n button?: number\n },\n ) => void\n /** Lets the caller's hook force the dropdown open after a recent\n * internal action (e.g. result navigation). `undefined` falls back\n * to `<SearchInput>`'s built-in focus/hover heuristics. */\n showDropdown?: boolean\n /** Defaults to 2 — matches the existing data-room and onboarding-\n * guide consumers. Override only if a surface needs different\n * typeahead semantics. */\n minQueryLength?: number\n /** Defaults to 0 — both existing consumers debounce inside the\n * hook, not the input. */\n debounceMs?: number\n className?: string\n /** Optional row-renderer override. Defaults to the lib's standard\n * `<DocSearchResultRow>` (source icon + title + path breadcrumb).\n * Override only when a surface needs custom row chrome. */\n renderResult?: (result: SearchResult, isHighlighted: boolean) => ReactNode\n}\n\nexport function DocSearchBar({\n placeholder,\n query,\n onQueryChange,\n results,\n isLoading,\n onResultSelect,\n showDropdown,\n minQueryLength = 2,\n debounceMs = 0,\n className = 'w-full',\n renderResult,\n}: DocSearchBarProps) {\n return (\n <SearchInput\n placeholder={placeholder}\n value={query}\n onChange={onQueryChange}\n results={results}\n isLoading={isLoading}\n onResultSelect={onResultSelect}\n showDropdown={showDropdown || undefined}\n debounceMs={debounceMs}\n minQueryLength={minQueryLength}\n className={className}\n renderResult={\n renderResult ??\n ((result, isHighlighted) => (\n <DocSearchResultRow result={result} isHighlighted={isHighlighted} />\n ))\n }\n />\n )\n}\n","/**\n * Map RAG `/api/docs/search` wire results into the `<DocSearchBar>`\n * dropdown's row shape, collapsing entity-table rows into grouped\n * results so the dropdown lists ONE \"Cap Table (12 records)\" row\n * instead of 12 individual rows.\n *\n * Pure transform — no telemetry, no navigation, no React deps. Lifted\n * from the hub's `hooks/use-docs.ts:mapDocSearchResults` (the hub's\n * `traceCompose` call was hub-only telemetry and is intentionally\n * dropped — callers that want logging can wrap this helper).\n */\n\nimport type { SearchResult } from '../../ui/search-input'\nimport type { DocSearchResult } from './types'\n\n/** Source repos that should be collapsed into grouped results in the search bar.\n * Only financial tables (all rows link to the same admin page).\n * Content tables (blog, webinar, podcast, etc.) stay individual since each has a unique URL. */\nconst SEARCH_GROUP_REPOS = new Set([\n 'financial-cap-table',\n 'financial-kpis',\n 'financial-pnl',\n 'financial-balance-sheet',\n 'financial-cash-flow',\n])\n\nconst ENTITY_LABELS: Record<string, string> = {\n 'financial-cap-table': 'Cap Table',\n 'financial-kpis': 'Financial KPIs',\n 'financial-pnl': 'Profit & Loss',\n 'financial-balance-sheet': 'Balance Sheets',\n 'financial-cash-flow': 'Cash Flow',\n 'blog-posts': 'Blog Posts',\n 'product-releases': 'Product Releases',\n 'case-studies': 'Case Studies',\n webinars: 'Webinars',\n events: 'Events',\n podcasts: 'Podcasts',\n}\n\nexport function mapDocSearchResults(docs: DocSearchResult[]): SearchResult[] {\n const entityGroups = new Map<string, DocSearchResult[]>()\n // Track insertion order — groups appear where the FIRST row of that\n // repo appeared in the response.\n const order: Array<\n { type: 'entity'; repo: string } | { type: 'doc'; doc: DocSearchResult }\n > = []\n const seenRepos = new Set<string>()\n\n for (const doc of docs) {\n if (doc.sourceRepo && SEARCH_GROUP_REPOS.has(doc.sourceRepo)) {\n const group = entityGroups.get(doc.sourceRepo) || []\n group.push(doc)\n entityGroups.set(doc.sourceRepo, group)\n if (!seenRepos.has(doc.sourceRepo)) {\n seenRepos.add(doc.sourceRepo)\n order.push({ type: 'entity', repo: doc.sourceRepo })\n }\n } else {\n order.push({ type: 'doc', doc })\n }\n }\n\n const results: SearchResult[] = []\n for (const entry of order) {\n if (entry.type === 'entity') {\n const rows = entityGroups.get(entry.repo)!\n const label = ENTITY_LABELS[entry.repo] || entry.repo\n results.push({\n id: `group-${entry.repo}`,\n title: `${label} (${rows.length} ${rows.length === 1 ? 'record' : 'records'})`,\n path: rows[0].path,\n type: 'file',\n metadata: {\n documentType: rows[0].documentType,\n externalUrl: rows[0].externalUrl,\n sourceRepo: entry.repo,\n id: rows[0].entityId,\n isGroup: true,\n items: rows.map((r) => ({\n name: r.name,\n externalUrl: r.externalUrl,\n id: r.entityId,\n sourceRepo: r.sourceRepo,\n documentType: r.documentType,\n })),\n },\n })\n } else {\n const doc = entry.doc\n const isNonMarkdown = doc.documentType && doc.documentType !== 'markdown'\n results.push({\n id: doc.path,\n title: doc.name,\n description: isNonMarkdown ? doc.name : doc.snippet,\n path: doc.path,\n type: doc.type,\n metadata: {\n matchType: doc.matchType,\n ...(doc.documentType ? { documentType: doc.documentType } : {}),\n ...(doc.externalUrl ? { externalUrl: doc.externalUrl } : {}),\n ...(doc.targetPlatform != null\n ? { targetPlatform: doc.targetPlatform }\n : {}),\n ...(doc.sourceRepo ? { sourceRepo: doc.sourceRepo } : {}),\n ...(doc.entityId ? { id: doc.entityId } : {}),\n },\n })\n }\n }\n\n return results\n}\n","/**\n * Resolve what should happen when the user picks a search result.\n * Returns one of five typed actions so the caller is a single switch.\n *\n * Resolution order:\n * 1. `externalUrl` present → use `decideNewTab` to choose same-tab vs\n * new-tab against the row's `targetPlatform`.\n * 2. Row has `id` + `sourceRepo` + `documentType` → synth an Ask-AI\n * action (entity drill-in via primary key, no URL).\n * 3. Row has only `path` → legacy navigation fallback.\n * 4. Nothing actionable → noop.\n *\n * Lifted from the hub's `hooks/use-docs.ts:resolveSearchResultAction`.\n * Pure — no React, no telemetry.\n */\n\nimport type { SearchResult } from '../../ui/search-input'\nimport type { ChatRef } from '../../chat/chat-ref.types'\nimport { decideNewTab } from '../../chat/utils/decide-new-tab'\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): SearchResultAction {\n const meta = result.metadata ?? {}\n const externalUrl = meta.externalUrl as string | undefined\n if (externalUrl) {\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 targetPlatform = meta.targetPlatform as string | null | undefined\n const isNewTab = decideNewTab({\n href: externalUrl,\n targetPlatform,\n surface: 'useUnifiedNav',\n runtimeMode,\n currentSource: source,\n })\n return isNewTab\n ? { kind: 'navigate-new-tab', href: externalUrl }\n : { kind: 'navigate-same-tab', href: externalUrl }\n }\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 (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\nimport { useState, useEffect, useCallback } from 'react'\nimport { useRouter } from '../../../embed-shims'\nimport { useDebounce } from '../../../hooks/ui/use-debounce'\nimport { useChatRuntime } from '../../../contexts/chat-runtime-context'\nimport type { SearchResult } from '../../ui/search-input'\nimport {\n resolveExternalNavigation,\n stripSameOriginToPath,\n NEW_TAB_FEATURES,\n} from '../../chat/utils/chat-nav-resolution'\nimport type { DocSearchResult } from './types'\nimport { mapDocSearchResults } from './map-doc-search-results'\nimport { resolveSearchResultAction } from './resolve-search-result-action'\n\nexport interface UseDocSearchConfig {\n /** Discriminator passed to `/api/docs/search?source=` (e.g.\n * `'openframe'`). Embedders set it to whatever discriminator their\n * reverse-proxy expects. */\n source: string\n /** Base route prefix this search lives under (e.g. `'/onboarding-guides'`).\n * When a result's href starts with `${baseRoute}/`, the hook\n * attempts the optional in-page swap path before falling through\n * to a full nav. */\n baseRoute: string\n /** Imperative navigation fallback. Called when no override\n * (in-page swap, new-tab) applies. Hub callers pass\n * `(path) => router.push(path)`; embedders pass an equivalent. */\n onNavigate: (path: string) => void\n /** Optional `RagTableConfig.id` list to narrow the search to specific\n * tables (e.g. `['onboarding-guides']`). Forwarded to\n * `/api/docs/search?tableIds=…` which intersects with the source's\n * standing set. */\n tableIds?: string[]\n /** Optional in-page swap callback. When the result's href is under\n * `baseRoute` AND this callback returns true, the hook treats the\n * click as handled in-page (no router push). Hub's\n * `<DocumentationSection>` wires this to\n * `useDocNavigation().navigate(path)`. */\n onInPageSwap?: (path: string) => boolean\n /** Optional endpoint override. Defaults to `'/api/docs/search'`\n * (the hub's reverse-proxy route). Embedders with a different\n * path can override. */\n searchEndpoint?: string\n}\n\nexport function useDocSearch(config: UseDocSearchConfig) {\n const {\n source,\n baseRoute,\n onNavigate,\n tableIds,\n onInPageSwap,\n searchEndpoint,\n } = config\n const tableIdsKey = tableIds && tableIds.length > 0 ? tableIds.join(',') : ''\n\n const router = useRouter()\n // Optional chat-runtime read — when present and mode='embed' the\n // search-result row click short-circuits to a new-tab open against\n // the absolutized URL. Null/host preserves today's behavior.\n // Also used as the proxy-prefix fallback for `searchEndpoint`, matching\n // how tickets resolves `findTicketUrl`.\n const runtime = useChatRuntime()\n const resolvedSearchEndpoint =\n searchEndpoint ?? runtime?.endpoints.docsSearchUrl ?? '/api/docs/search'\n\n const [query, setQuery] = useState('')\n const [results, setResults] = useState<SearchResult[]>([])\n const [isFetching, setIsFetching] = useState(false)\n const debouncedQuery = useDebounce(query, 300)\n\n useEffect(() => {\n if (!debouncedQuery || debouncedQuery.trim().length < 2) {\n setResults([])\n setIsFetching(false)\n return\n }\n\n let cancelled = false\n\n async function fetchResults() {\n setIsFetching(true)\n try {\n const params = new URLSearchParams({\n q: debouncedQuery,\n source,\n limit: '10',\n })\n if (tableIdsKey) params.set('tableIds', tableIdsKey)\n\n const response = await fetch(`${resolvedSearchEndpoint}?${params.toString()}`)\n if (!response.ok) {\n throw new Error(`Search request failed: ${response.status}`)\n }\n\n const json = await response.json()\n\n if (!cancelled && json.success && Array.isArray(json.data)) {\n const mapped = mapDocSearchResults(json.data as DocSearchResult[])\n setResults(mapped)\n }\n } catch (error) {\n console.error('Doc search error:', error)\n if (!cancelled) {\n setResults([])\n }\n } finally {\n if (!cancelled) {\n setIsFetching(false)\n }\n }\n }\n\n fetchResults()\n\n return () => {\n cancelled = true\n }\n }, [debouncedQuery, source, tableIdsKey, resolvedSearchEndpoint])\n\n // Derived loading state — single source of truth for \"should the\n // dropdown show 'Loading...' instead of 'No results found'\":\n const isLoading =\n query.trim().length >= 2 && (query !== debouncedQuery || isFetching)\n\n // Track whether dropdown should stay open (external link opened in new tab).\n const [keepOpen, setKeepOpen] = useState(false)\n\n const handleResultSelect = useCallback(\n (\n result: SearchResult,\n modifiers?: {\n metaKey?: boolean\n ctrlKey?: boolean\n shiftKey?: boolean\n altKey?: boolean\n button?: number\n },\n ) => {\n const action = resolveSearchResultAction(\n result,\n source,\n runtime?.navigation.mode,\n )\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 // Embed-mode short-circuit — autocomplete row clicked while\n // the chat panel is hosted inside an embedding app.\n if (runtime?.navigation.mode === 'embed') {\n setKeepOpen(true)\n const targetPlatform =\n (result.metadata?.targetPlatform as string | null | undefined) ?? null\n resolveExternalNavigation({\n href: action.href,\n targetPlatform,\n runtime,\n }).open()\n return\n }\n if (wantsNewTab) {\n setKeepOpen(true)\n window.open(action.href, '_blank', NEW_TAB_FEATURES)\n return\n }\n // Same-origin click:\n // 1. If the href is under the current doc-tree's baseRoute AND\n // an `onInPageSwap` callback is wired AND returns true →\n // consider in-page swap handled.\n // 2. Otherwise → embed-shim `router.push()` (soft RSC nav on\n // Next.js hosts, window.location.assign on bare hosts).\n setKeepOpen(false)\n const path =\n baseRoute && action.href.startsWith(`${baseRoute}/`)\n ? action.href.slice(baseRoute.length + 1)\n : null\n if (path && onInPageSwap?.(path)) return\n router.push(stripSameOriginToPath(action.href))\n return\n }\n case 'navigate-new-tab':\n // Cross-origin (e.g. clicking a flamingo.run release from\n // product-hub) — open in a new tab. Keep dropdown open so the\n // user can pick another result without re-searching.\n setKeepOpen(true)\n window.open(action.href, '_blank', NEW_TAB_FEATURES)\n return\n case 'ask-ai':\n // Row is searchable-but-not-openable (cap_table positions,\n // financial-kpi snapshots, anything backed by\n // `resolveUrl: () => null`). Dispatch a CustomEvent that\n // GlobalAskAI listens for — opens chat + drills via\n // `entityIdFilter` (primary-key only, same as inline-card Ask).\n setKeepOpen(false)\n window.dispatchEvent(\n new CustomEvent('ask-ai:open-with-ref', { detail: action.detail }),\n )\n return\n case 'route':\n // Final fallback: legacy navigation by path. Hits when a row\n // has neither URL nor pk metadata — a mapper/API regression.\n setKeepOpen(false)\n onNavigate(action.path)\n return\n case 'noop':\n return\n }\n },\n [onNavigate, source, baseRoute, router, onInPageSwap, runtime],\n )\n\n // Reset keepOpen when query changes.\n useEffect(() => {\n setKeepOpen(false)\n }, [query])\n\n return {\n query,\n setQuery,\n results,\n isLoading,\n handleResultSelect,\n keepDropdownOpen: keepOpen,\n }\n}\n"]}