@flamingo-stack/openframe-frontend-core 0.0.296 → 0.0.297

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 (276) hide show
  1. package/README.md +9 -0
  2. package/dist/{chunk-7RIYT7ZH.js → chunk-2QG57XOJ.js} +1067 -205
  3. package/dist/chunk-2QG57XOJ.js.map +1 -0
  4. package/dist/{chunk-WHMATDVP.js → chunk-3JIQVE7T.js} +9 -15
  5. package/dist/{chunk-WHMATDVP.js.map → chunk-3JIQVE7T.js.map} +1 -1
  6. package/dist/{chunk-GLLDTKZK.cjs → chunk-4PSQS3SW.cjs} +7 -9
  7. package/dist/chunk-4PSQS3SW.cjs.map +1 -0
  8. package/dist/{chunk-OY7OF7E7.js → chunk-4TLE6VLU.js} +30 -24
  9. package/dist/chunk-4TLE6VLU.js.map +1 -0
  10. package/dist/{chunk-W6M2FLLT.cjs → chunk-53FUMSZ5.cjs} +40 -46
  11. package/dist/chunk-53FUMSZ5.cjs.map +1 -0
  12. package/dist/{chunk-D3LEFMOA.cjs → chunk-54KNMC2R.cjs} +3 -3
  13. package/dist/{chunk-D3LEFMOA.cjs.map → chunk-54KNMC2R.cjs.map} +1 -1
  14. package/dist/{chunk-EYEW6PTA.cjs → chunk-6C526VNN.cjs} +358 -118
  15. package/dist/chunk-6C526VNN.cjs.map +1 -0
  16. package/dist/{chunk-XREEV72C.cjs → chunk-7OVGB2DQ.cjs} +19 -25
  17. package/dist/chunk-7OVGB2DQ.cjs.map +1 -0
  18. package/dist/{chunk-6GCI7JOE.js → chunk-AD6C23QY.js} +8 -7
  19. package/dist/{chunk-6GCI7JOE.js.map → chunk-AD6C23QY.js.map} +1 -1
  20. package/dist/chunk-F5OB2YAL.cjs +144 -0
  21. package/dist/chunk-F5OB2YAL.cjs.map +1 -0
  22. package/dist/chunk-FBWXMMRB.cjs +2 -0
  23. package/dist/chunk-FBWXMMRB.cjs.map +1 -0
  24. package/dist/{chunk-YIGPRLQY.cjs → chunk-FCDQNTDG.cjs} +21 -20
  25. package/dist/chunk-FCDQNTDG.cjs.map +1 -0
  26. package/dist/{chunk-IE6OU3WQ.cjs → chunk-FQOTC3UU.cjs} +318 -16
  27. package/dist/chunk-FQOTC3UU.cjs.map +1 -0
  28. package/dist/{chunk-QHIXS3W2.cjs → chunk-GUTS7HGA.cjs} +11590 -2105
  29. package/dist/chunk-GUTS7HGA.cjs.map +1 -0
  30. package/dist/chunk-GZ4C3XW6.js +2 -0
  31. package/dist/chunk-GZ4C3XW6.js.map +1 -0
  32. package/dist/{chunk-5P3B2LZW.js → chunk-IL47XWV5.js} +8 -14
  33. package/dist/{chunk-5P3B2LZW.js.map → chunk-IL47XWV5.js.map} +1 -1
  34. package/dist/{chunk-LCNMR277.js → chunk-IZ7JSBFP.js} +1 -1
  35. package/dist/chunk-IZ7JSBFP.js.map +1 -0
  36. package/dist/{chunk-EL6QLAWX.js → chunk-JALO4TAZ.js} +357 -55
  37. package/dist/chunk-JALO4TAZ.js.map +1 -0
  38. package/dist/{chunk-AQOWFSMB.cjs → chunk-L6PSSIUQ.cjs} +1 -1
  39. package/dist/chunk-L6PSSIUQ.cjs.map +1 -0
  40. package/dist/{chunk-MBFWU2EM.js → chunk-L7ULJKG7.js} +6 -10
  41. package/dist/{chunk-MBFWU2EM.js.map → chunk-L7ULJKG7.js.map} +1 -1
  42. package/dist/{chunk-K2PFPBMF.js → chunk-PC746XCO.js} +15050 -5565
  43. package/dist/chunk-PC746XCO.js.map +1 -0
  44. package/dist/{chunk-3ZXUQQL4.js → chunk-PI4WSYQV.js} +2 -2
  45. package/dist/{chunk-E4XABBSU.js → chunk-PWQUAVA3.js} +338 -98
  46. package/dist/chunk-PWQUAVA3.js.map +1 -0
  47. package/dist/chunk-SA2WPJVO.js +144 -0
  48. package/dist/chunk-SA2WPJVO.js.map +1 -0
  49. package/dist/{chunk-X6BV7MB7.cjs → chunk-UNVE2SDJ.cjs} +37 -31
  50. package/dist/chunk-UNVE2SDJ.cjs.map +1 -0
  51. package/dist/{chunk-5E2HOSSH.cjs → chunk-WMSTJAZT.cjs} +913 -51
  52. package/dist/chunk-WMSTJAZT.cjs.map +1 -0
  53. package/dist/{chunk-ZP4AVIZP.js → chunk-X4DOXQRT.js} +4 -6
  54. package/dist/{chunk-ZP4AVIZP.js.map → chunk-X4DOXQRT.js.map} +1 -1
  55. package/dist/{chunk-X647HY3F.cjs → chunk-YBYI62OE.cjs} +33 -37
  56. package/dist/chunk-YBYI62OE.cjs.map +1 -0
  57. package/dist/components/case-studies/index.cjs +126 -0
  58. package/dist/components/case-studies/index.cjs.map +1 -0
  59. package/dist/components/case-studies/index.d.ts +2 -0
  60. package/dist/components/case-studies/index.d.ts.map +1 -0
  61. package/dist/components/case-studies/index.js +126 -0
  62. package/dist/components/case-studies/index.js.map +1 -0
  63. package/dist/components/case-studies/share-experience-section.d.ts +48 -0
  64. package/dist/components/case-studies/share-experience-section.d.ts.map +1 -0
  65. package/dist/components/chat/index.cjs +8 -18
  66. package/dist/components/chat/index.cjs.map +1 -1
  67. package/dist/components/chat/index.js +75 -85
  68. package/dist/components/contact/index.cjs +8 -15
  69. package/dist/components/contact/index.cjs.map +1 -1
  70. package/dist/components/contact/index.js +7 -14
  71. package/dist/components/docs/doc-viewer.d.ts +39 -2
  72. package/dist/components/docs/doc-viewer.d.ts.map +1 -1
  73. package/dist/components/docs/docs-hub-page.d.ts +46 -0
  74. package/dist/components/docs/docs-hub-page.d.ts.map +1 -0
  75. package/dist/components/docs/index.cjs +17 -9
  76. package/dist/components/docs/index.cjs.map +1 -1
  77. package/dist/components/docs/index.d.ts +4 -0
  78. package/dist/components/docs/index.d.ts.map +1 -1
  79. package/dist/components/docs/index.js +16 -8
  80. package/dist/components/docs/skeletons.d.ts +32 -0
  81. package/dist/components/docs/skeletons.d.ts.map +1 -0
  82. package/dist/components/docs/use-docs-resolve-link.d.ts +20 -0
  83. package/dist/components/docs/use-docs-resolve-link.d.ts.map +1 -0
  84. package/dist/components/docs/use-document-tree.d.ts.map +1 -1
  85. package/dist/components/embeds/embed-container.d.ts +37 -0
  86. package/dist/components/embeds/embed-container.d.ts.map +1 -0
  87. package/dist/components/embeds/embed-iframe.d.ts.map +1 -1
  88. package/dist/components/embeds/file-download-card.d.ts +18 -0
  89. package/dist/components/embeds/file-download-card.d.ts.map +1 -0
  90. package/dist/components/embeds/index.cjs +38 -15
  91. package/dist/components/embeds/index.cjs.map +1 -1
  92. package/dist/components/embeds/index.d.ts +8 -0
  93. package/dist/components/embeds/index.d.ts.map +1 -1
  94. package/dist/components/embeds/index.js +40 -17
  95. package/dist/components/embeds/linkedin-embed-client.d.ts +8 -0
  96. package/dist/components/embeds/linkedin-embed-client.d.ts.map +1 -0
  97. package/dist/components/embeds/markdown-image.d.ts +5 -0
  98. package/dist/components/embeds/markdown-image.d.ts.map +1 -0
  99. package/dist/components/embeds/reddit-embed-client.d.ts +7 -0
  100. package/dist/components/embeds/reddit-embed-client.d.ts.map +1 -0
  101. package/dist/components/embeds/rich-markdown-runtime.d.ts +46 -0
  102. package/dist/components/embeds/rich-markdown-runtime.d.ts.map +1 -0
  103. package/dist/components/embeds/twitter-embed-client.d.ts +8 -0
  104. package/dist/components/embeds/twitter-embed-client.d.ts.map +1 -0
  105. package/dist/components/faq/index.cjs +9 -16
  106. package/dist/components/faq/index.cjs.map +1 -1
  107. package/dist/components/faq/index.js +8 -15
  108. package/dist/components/features/index.cjs +8 -16
  109. package/dist/components/features/index.cjs.map +1 -1
  110. package/dist/components/features/index.js +24 -32
  111. package/dist/components/index.cjs +257 -452
  112. package/dist/components/index.cjs.map +1 -1
  113. package/dist/components/index.js +781 -976
  114. package/dist/components/index.js.map +1 -1
  115. package/dist/components/layout/page-header.d.ts +78 -0
  116. package/dist/components/layout/page-header.d.ts.map +1 -0
  117. package/dist/components/layout/page-layout.d.ts +10 -1
  118. package/dist/components/layout/page-layout.d.ts.map +1 -1
  119. package/dist/components/layout/page-with-header.d.ts +67 -0
  120. package/dist/components/layout/page-with-header.d.ts.map +1 -0
  121. package/dist/components/layout/title-block.d.ts +17 -1
  122. package/dist/components/layout/title-block.d.ts.map +1 -1
  123. package/dist/components/navigation/index.cjs +7 -15
  124. package/dist/components/navigation/index.cjs.map +1 -1
  125. package/dist/components/navigation/index.js +9 -17
  126. package/dist/components/onboarding-guides/index.cjs +35 -36
  127. package/dist/components/onboarding-guides/index.cjs.map +1 -1
  128. package/dist/components/onboarding-guides/index.js +13 -14
  129. package/dist/components/onboarding-guides/index.js.map +1 -1
  130. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +1 -1
  131. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  132. package/dist/components/related-content/index.cjs +9 -16
  133. package/dist/components/related-content/index.cjs.map +1 -1
  134. package/dist/components/related-content/index.js +8 -15
  135. package/dist/components/shared/dev-section/dev-section-page.d.ts +9 -0
  136. package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
  137. package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -1
  138. package/dist/components/shared/dev-section/index.d.ts +1 -1
  139. package/dist/components/shared/dev-section/index.d.ts.map +1 -1
  140. package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -1
  141. package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
  142. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  143. package/dist/components/tickets/index.cjs +100 -112
  144. package/dist/components/tickets/index.cjs.map +1 -1
  145. package/dist/components/tickets/index.js +20 -32
  146. package/dist/components/tickets/index.js.map +1 -1
  147. package/dist/components/ui/file-manager/index.cjs +50 -52
  148. package/dist/components/ui/file-manager/index.cjs.map +1 -1
  149. package/dist/components/ui/file-manager/index.js +4 -6
  150. package/dist/components/ui/file-manager/index.js.map +1 -1
  151. package/dist/components/ui/index.cjs +13 -19
  152. package/dist/components/ui/index.cjs.map +1 -1
  153. package/dist/components/ui/index.d.ts +2 -0
  154. package/dist/components/ui/index.d.ts.map +1 -1
  155. package/dist/components/ui/index.js +133 -139
  156. package/dist/components/ui/release-changelog-section.d.ts +6 -2
  157. package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
  158. package/dist/components/ui/rich-markdown-renderer.d.ts +34 -0
  159. package/dist/components/ui/rich-markdown-renderer.d.ts.map +1 -0
  160. package/dist/components/ui/simple-markdown-renderer.d.ts +2 -8
  161. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
  162. package/dist/contexts/chat-runtime-context.d.ts +14 -0
  163. package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
  164. package/dist/contexts/index.cjs +3 -3
  165. package/dist/contexts/index.js +5 -5
  166. package/dist/embed-shims/index.cjs +3 -3
  167. package/dist/embed-shims/index.cjs.map +1 -1
  168. package/dist/embed-shims/index.js +4 -4
  169. package/dist/hooks/index.cjs +4 -9
  170. package/dist/hooks/index.cjs.map +1 -1
  171. package/dist/hooks/index.js +6 -11
  172. package/dist/index.cjs +14 -20
  173. package/dist/index.cjs.map +1 -1
  174. package/dist/index.js +362 -368
  175. package/dist/types/doc-source.d.ts +31 -1
  176. package/dist/types/doc-source.d.ts.map +1 -1
  177. package/dist/utils/index.cjs +4 -0
  178. package/dist/utils/index.cjs.map +1 -1
  179. package/dist/utils/index.d.ts +1 -0
  180. package/dist/utils/index.d.ts.map +1 -1
  181. package/dist/utils/index.js +4 -1
  182. package/dist/utils/index.js.map +1 -1
  183. package/dist/utils/page-header-constants.d.ts +15 -0
  184. package/dist/utils/page-header-constants.d.ts.map +1 -0
  185. package/dist/utils/social-embed-cache.d.ts +29 -0
  186. package/dist/utils/social-embed-cache.d.ts.map +1 -0
  187. package/package.json +7 -1
  188. package/src/components/case-studies/index.ts +4 -0
  189. package/src/components/case-studies/share-experience-section.tsx +185 -0
  190. package/src/components/chat/embeddable-chat.tsx +1 -1
  191. package/src/components/docs/doc-viewer.tsx +111 -19
  192. package/src/components/docs/docs-hub-page.tsx +149 -0
  193. package/src/components/docs/index.ts +17 -0
  194. package/src/components/docs/skeletons.tsx +138 -0
  195. package/src/components/docs/use-docs-resolve-link.ts +52 -0
  196. package/src/components/docs/use-document-tree.ts +21 -0
  197. package/src/components/embeds/embed-container.tsx +80 -0
  198. package/src/components/embeds/embed-iframe.tsx +7 -9
  199. package/src/components/embeds/file-download-card.tsx +54 -0
  200. package/src/components/embeds/index.ts +30 -0
  201. package/src/components/embeds/linkedin-embed-client.tsx +100 -0
  202. package/src/components/embeds/markdown-image.tsx +88 -0
  203. package/src/components/embeds/og-link-preview.tsx +13 -13
  204. package/src/components/embeds/reddit-embed-client.tsx +550 -0
  205. package/src/components/embeds/rich-markdown-runtime.tsx +79 -0
  206. package/src/components/embeds/twitter-embed-client.tsx +308 -0
  207. package/src/components/layout/page-header.tsx +182 -0
  208. package/src/components/layout/page-layout.tsx +14 -1
  209. package/src/components/layout/page-with-header.tsx +110 -0
  210. package/src/components/layout/title-block.tsx +40 -62
  211. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +3 -3
  212. package/src/components/shared/dev-section/dev-section-page.tsx +9 -1
  213. package/src/components/shared/dev-section/dev-section-view.tsx +14 -9
  214. package/src/components/shared/dev-section/index.ts +1 -1
  215. package/src/components/shared/doc-search/use-doc-search.ts +7 -3
  216. package/src/components/shared/legal-document/legal-document-page.tsx +2 -2
  217. package/src/components/shared/product-release/release-detail-page.tsx +6 -4
  218. package/src/components/ui/index.ts +2 -0
  219. package/src/components/ui/release-changelog-section.tsx +7 -2
  220. package/src/components/ui/rich-markdown-renderer.tsx +1203 -0
  221. package/src/components/ui/simple-markdown-renderer.tsx +7 -11
  222. package/src/contexts/chat-runtime-context.tsx +14 -0
  223. package/src/types/doc-source.ts +33 -1
  224. package/src/utils/index.ts +1 -0
  225. package/src/utils/page-header-constants.ts +15 -0
  226. package/src/utils/social-embed-cache.ts +391 -0
  227. package/dist/chunk-26PKDALD.js +0 -2379
  228. package/dist/chunk-26PKDALD.js.map +0 -1
  229. package/dist/chunk-3MCHAFHB.js +0 -89
  230. package/dist/chunk-3MCHAFHB.js.map +0 -1
  231. package/dist/chunk-5E2HOSSH.cjs.map +0 -1
  232. package/dist/chunk-66AANIOC.cjs +0 -619
  233. package/dist/chunk-66AANIOC.cjs.map +0 -1
  234. package/dist/chunk-6JINAOI7.cjs +0 -311
  235. package/dist/chunk-6JINAOI7.cjs.map +0 -1
  236. package/dist/chunk-7RIYT7ZH.js.map +0 -1
  237. package/dist/chunk-AQOWFSMB.cjs.map +0 -1
  238. package/dist/chunk-BOCFIKYS.cjs +0 -3009
  239. package/dist/chunk-BOCFIKYS.cjs.map +0 -1
  240. package/dist/chunk-D652TJBQ.js +0 -3009
  241. package/dist/chunk-D652TJBQ.js.map +0 -1
  242. package/dist/chunk-E4XABBSU.js.map +0 -1
  243. package/dist/chunk-EL6QLAWX.js.map +0 -1
  244. package/dist/chunk-EYEW6PTA.cjs.map +0 -1
  245. package/dist/chunk-FQJK446R.js +0 -1606
  246. package/dist/chunk-FQJK446R.js.map +0 -1
  247. package/dist/chunk-GLLDTKZK.cjs.map +0 -1
  248. package/dist/chunk-IE6OU3WQ.cjs.map +0 -1
  249. package/dist/chunk-J54Z3OCR.cjs +0 -1606
  250. package/dist/chunk-J54Z3OCR.cjs.map +0 -1
  251. package/dist/chunk-K2PFPBMF.js.map +0 -1
  252. package/dist/chunk-KXCRGTRN.cjs +0 -2379
  253. package/dist/chunk-KXCRGTRN.cjs.map +0 -1
  254. package/dist/chunk-LCNMR277.js.map +0 -1
  255. package/dist/chunk-LFGGF7OT.cjs +0 -449
  256. package/dist/chunk-LFGGF7OT.cjs.map +0 -1
  257. package/dist/chunk-M2OCXTNT.js +0 -311
  258. package/dist/chunk-M2OCXTNT.js.map +0 -1
  259. package/dist/chunk-ME4EVDFP.js +0 -619
  260. package/dist/chunk-ME4EVDFP.js.map +0 -1
  261. package/dist/chunk-OQ6X7ZOC.js +0 -449
  262. package/dist/chunk-OQ6X7ZOC.js.map +0 -1
  263. package/dist/chunk-OY7OF7E7.js.map +0 -1
  264. package/dist/chunk-POKKCWKF.js +0 -354
  265. package/dist/chunk-POKKCWKF.js.map +0 -1
  266. package/dist/chunk-QHIXS3W2.cjs.map +0 -1
  267. package/dist/chunk-TFSYSWPS.cjs +0 -89
  268. package/dist/chunk-TFSYSWPS.cjs.map +0 -1
  269. package/dist/chunk-W6M2FLLT.cjs.map +0 -1
  270. package/dist/chunk-X647HY3F.cjs.map +0 -1
  271. package/dist/chunk-X6BV7MB7.cjs.map +0 -1
  272. package/dist/chunk-XREEV72C.cjs.map +0 -1
  273. package/dist/chunk-YETA25JW.cjs +0 -354
  274. package/dist/chunk-YETA25JW.cjs.map +0 -1
  275. package/dist/chunk-YIGPRLQY.cjs.map +0 -1
  276. /package/dist/{chunk-3ZXUQQL4.js.map → chunk-PI4WSYQV.js.map} +0 -0
@@ -11,6 +11,9 @@ import { visit } from 'unist-util-visit';
11
11
  import Image from '../../embed-shims/next-image';
12
12
  import { AlertCircleIcon } from '../icons-v2-generated';
13
13
  import { cn } from '../../utils/cn';
14
+ import type { ResolveLinkResult } from '../../types/doc-source';
15
+
16
+ export type { ResolveLinkResult };
14
17
 
15
18
  // ---------------------------------------------------------------------------
16
19
  // rehype HAST sanitizer — runs AFTER rehype-raw to strip XSS vectors
@@ -523,17 +526,10 @@ function resolveTextSizeConfig(config?: TextSizeConfig): Record<TextSizeElement,
523
526
  return { ...defaultSizes, ...config };
524
527
  }
525
528
 
526
- // ---------------------------------------------------------------------------
527
- // Resolved link result used by onResolveLink callback
528
- // ---------------------------------------------------------------------------
529
- export interface ResolveLinkResult {
530
- success: boolean;
531
- resolvedPath?: string;
532
- type?: string;
533
- action?: string;
534
- error?: string;
535
- message?: string;
536
- }
529
+ // `ResolveLinkResult` lives in `../../types/doc-source` so it's the single
530
+ // canonical home shared by `DocRenderHandlers.onResolveLink`, `DocViewer`'s
531
+ // resolver, and this renderer's `onResolveLink` prop. Re-exported at the top
532
+ // of this file for back-compat with callers that import from here.
537
533
 
538
534
  // ---------------------------------------------------------------------------
539
535
  // Props
@@ -57,6 +57,20 @@ export interface ChatRuntime {
57
57
  listEngagementsUrl?: string
58
58
  /** GET slash-command catalog. Hub: '/api/docs/commands'. */
59
59
  commandsUrl: string
60
+ /** GET RAG-search endpoint behind `<DocSearchBar>` (the in-source search
61
+ * bar mounted by `<DocViewer>` / `<DocsHubPage>` when `showAIChat` is on).
62
+ * Hub: '/api/docs/search'. OPTIONAL — falls back to the hub path so
63
+ * same-origin Next.js hosts don't need to set it. Cross-origin embedders
64
+ * set their proxied path so the search bar routes through the same
65
+ * reverse proxy as everything else. Same pattern as `findTicketUrl`. */
66
+ docsSearchUrl?: string
67
+ /** POST internal-link resolver. The in-source markdown renderer (lib or
68
+ * custom) calls `<DocViewer>`'s `handlers.onResolveLink(href, currentPath)`
69
+ * for relative hrefs like `./getting-started/intro.md` — that callback
70
+ * posts to this URL with `{ link, currentPath, source }` and expects a
71
+ * `ResolveLinkResult` back. Hub: '/api/docs/resolve-link'. OPTIONAL — same
72
+ * fall-back chain as `docsSearchUrl`: prop override → runtime → default. */
73
+ docsResolveLinkUrl?: string
60
74
  /** GET per-platform empty-state config (admin-edited in
61
75
  * `/admin/chat-config`): `{ greeting, enabledRagTableIds, suggestedQueries }`.
62
76
  * Hub: '/api/docs/empty-state'. OPTIONAL — the in-app (host-mode) chat
@@ -25,6 +25,13 @@ export interface DocNode {
25
25
  children?: DocNode[]
26
26
  }
27
27
 
28
+ /**
29
+ * Named alias for `DocNode['documentType']` (non-nullable). Use this as the
30
+ * key type when building per-document-type renderer maps — keeps consumers
31
+ * from re-declaring the union or having to dig into `DocNode`.
32
+ */
33
+ export type DocumentType = NonNullable<DocNode['documentType']>
34
+
28
35
  /**
29
36
  * Content payload returned by a doc-source DAL's `getContent` call. Carries
30
37
  * everything any consumer's renderer might need; markdown-only fields and
@@ -71,6 +78,21 @@ export interface DocSourceDal<Client = unknown> {
71
78
  */
72
79
  export type DocSourceId = 'openframe-docs' | 'data-room-docs'
73
80
 
81
+ /**
82
+ * Result returned by the resolve-link endpoint. Discriminator-style: `success`
83
+ * + `type` jointly decide what the renderer does (navigate, expand folder,
84
+ * show broken-link badge). Backed by hub `/api/docs/resolve-link` and any
85
+ * embedder-proxied equivalent.
86
+ */
87
+ export interface ResolveLinkResult {
88
+ success: boolean
89
+ resolvedPath?: string
90
+ type?: string
91
+ action?: string
92
+ error?: string
93
+ message?: string
94
+ }
95
+
74
96
  /**
75
97
  * Handlers the viewer passes to a consumer's `renderContent` callback.
76
98
  * The page shell wires `renderContent` directly — no `DocContentRenderer`
@@ -83,6 +105,16 @@ export interface DocRenderHandlers {
83
105
  ) => void
84
106
  currentPath: string
85
107
  /** Registry source id (e.g. `'openframe-docs'`, `'data-room-docs'`) — used by
86
- * the consumer's `/api/resolve-link` POST to disambiguate the doc source. */
108
+ * the consumer's `/api/docs/resolve-link` POST to disambiguate the doc source. */
87
109
  sourceId: DocSourceId
110
+ /**
111
+ * Async link resolver — POSTs the raw markdown href + the current doc's path
112
+ * to the embedder's resolve-link endpoint and returns the resolved tree path.
113
+ * The lib auto-wires this when `DocViewer` knows the resolve-link endpoint
114
+ * (via `resolveLinkEndpoint` prop, `ChatRuntime.endpoints.docsResolveLinkUrl`,
115
+ * or the `/api/docs/resolve-link` default). Consumers thread it directly into
116
+ * their markdown renderer's `onResolveLink` prop — without it, relative
117
+ * hrefs like `./intro.md` end up fetched verbatim and 404.
118
+ */
119
+ onResolveLink?: (href: string, currentPath: string) => Promise<ResolveLinkResult>
88
120
  }
@@ -317,3 +317,4 @@ export * from './tree-builder'
317
317
  export * from './markdown-section-extractor'
318
318
  export * from './markdown-to-plain'
319
319
  export * from './embed-url-converters'
320
+ export * from './page-header-constants'
@@ -0,0 +1,15 @@
1
+ /**
2
+ * String constants shared by page-header consumers (`<PageHeader>`,
3
+ * `<DevSectionPage>`, `<DocsHubPage>` callers). Lives in `utils/` —
4
+ * NOT `'use client'` — so server modules (e.g. the hub's
5
+ * `lib/docs/hub-docs-presets.tsx` that builds preset JSX with
6
+ * `<Icon className={SECTION_HERO_ICON_CLASS} />`) can import the raw
7
+ * string. Importing the constant from a `'use client'` module turns it
8
+ * into a Next.js client-reference proxy in server contexts, which
9
+ * lucide's internal `mergeClasses` then calls `.trim()` on and crashes.
10
+ */
11
+
12
+ /** Tailwind class applied uniformly to every section-hero / page-header
13
+ * icon across the lib (Roadmap Map, Releases Rocket, Knowledge Hub
14
+ * BookOpen, Data Room Building2, …). Yellow accent color, 40x40. */
15
+ export const SECTION_HERO_ICON_CLASS = 'h-10 w-10 text-ods-accent'
@@ -0,0 +1,391 @@
1
+ // Unified Social Media Embed Caching System
2
+ // Shared between Reddit and Twitter embeds for consistent behavior.
3
+ //
4
+ // Lifted from hub `lib/utils/social-embed-cache.ts` so the lib's
5
+ // RichMarkdownRenderer satellites (reddit/twitter embed clients) can
6
+ // share it without an `@/lib/...` hub dependency. Pure-TS; no React.
7
+ //
8
+ // Endpoint paths are passed in via `apiEndpoint` on each call so embedders
9
+ // can route through their own reverse proxy (e.g. `/content/api/blog/reddit-proxy`).
10
+
11
+ // Global request deduplication to prevent duplicate caching requests
12
+ const cachingRequests = new Map<string, Promise<void>>();
13
+
14
+ // Global data cache to share fetched data between component instances
15
+ const dataCache = new Map<string, any>();
16
+
17
+ // Global refresh tracking to prevent duplicate background refreshes
18
+ const refreshTimestamps = new Map<string, number>();
19
+
20
+ interface CacheOptions {
21
+ platform: 'reddit' | 'twitter';
22
+ url: string;
23
+ apiEndpoint: string;
24
+ }
25
+
26
+ export class SocialEmbedCache {
27
+ private static instance: SocialEmbedCache;
28
+
29
+ static getInstance(): SocialEmbedCache {
30
+ if (!SocialEmbedCache.instance) {
31
+ SocialEmbedCache.instance = new SocialEmbedCache();
32
+ }
33
+ return SocialEmbedCache.instance;
34
+ }
35
+
36
+ // Check if data exists in memory cache
37
+ getFromMemory(url: string): any | null {
38
+ return dataCache.get(url) || null;
39
+ }
40
+
41
+ // Store data in memory cache
42
+ setInMemory(url: string, data: any): void {
43
+ dataCache.set(url, data);
44
+ }
45
+
46
+ // Check server cache (cache-only mode)
47
+ async getFromServer(options: CacheOptions): Promise<any | null> {
48
+ try {
49
+ console.log(`🔍 [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Cache] Checking server cache for: ${options.url}`);
50
+ const response = await fetch(`${options.apiEndpoint}?url=${encodeURIComponent(options.url)}&cache-only=true`);
51
+
52
+ if (response.ok) {
53
+ const data = await response.json();
54
+ console.log(`✅ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Cache] Server cache hit: ${options.url}`);
55
+ return data;
56
+ } else if (response.status === 404) {
57
+ console.log(`💨 [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Cache] No server cache available for: ${options.url}`);
58
+ return null;
59
+ }
60
+ } catch (error) {
61
+ console.log(`❌ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Cache] Server cache check failed for: ${options.url}`, error);
62
+ }
63
+ return null;
64
+ }
65
+
66
+ // Helper method to update server cache asynchronously
67
+ private async updateServerCache(apiEndpoint: string, url: string, data: any): Promise<void> {
68
+ try {
69
+ console.log(`💾 [Cache] Updating server cache for: ${url}`);
70
+ await fetch(`${apiEndpoint}?url=${encodeURIComponent(url)}`, {
71
+ method: 'POST',
72
+ headers: { 'Content-Type': 'application/json' },
73
+ body: JSON.stringify(data)
74
+ });
75
+ console.log(`✅ [Cache] Server cache updated for: ${url}`);
76
+ } catch (error) {
77
+ console.log(`❌ [Cache] Server cache update failed for: ${url}`, error);
78
+ }
79
+ }
80
+
81
+ // Store data in server cache (with deduplication)
82
+ async setInServer(options: CacheOptions, data: any): Promise<void> {
83
+ const cacheKey = `${options.platform}-${options.url}`;
84
+
85
+ // Prevent duplicate caching requests
86
+ if (cachingRequests.has(cacheKey)) {
87
+ console.log(`⏭️ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Cache] Server cache update skipped (already in progress): ${options.url}`);
88
+ return;
89
+ }
90
+
91
+ console.log(`💾 [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Cache] Updating server cache for: ${options.url}`);
92
+ const cachePromise = fetch(`${options.apiEndpoint}?url=${encodeURIComponent(options.url)}`, {
93
+ method: 'POST',
94
+ headers: { 'Content-Type': 'application/json' },
95
+ body: JSON.stringify(data)
96
+ }).then(() => {
97
+ console.log(`✅ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Cache] Server cache updated successfully: ${options.url}`);
98
+ cachingRequests.delete(cacheKey);
99
+ }).catch((error) => {
100
+ console.log(`❌ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Cache] Server cache update failed: ${options.url}`, error);
101
+ cachingRequests.delete(cacheKey);
102
+ });
103
+
104
+ cachingRequests.set(cacheKey, cachePromise);
105
+ await cachePromise;
106
+ }
107
+
108
+ // Direct fetch with platform-specific logic
109
+ async fetchDirect(options: CacheOptions & {
110
+ directFetcher: () => Promise<any>
111
+ }): Promise<any | null> {
112
+ try {
113
+ console.log(`🔄 [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Direct] Fetching content: ${options.url}`);
114
+ const result = await options.directFetcher();
115
+ console.log(`✅ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Direct] Fetch successful: ${options.url}`);
116
+ return result;
117
+ } catch (error) {
118
+ console.log(`❌ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Direct] Fetch failed: ${options.url}`, error);
119
+ return null;
120
+ }
121
+ }
122
+
123
+ // Unified cache hierarchy: Memory → Server → Direct
124
+ async fetchWithHierarchy(options: CacheOptions & {
125
+ dataValidator: (data: any) => boolean;
126
+ onDataUpdate: (data: any) => void;
127
+ onError: (error: string) => void;
128
+ onLoading: (loading: boolean) => void;
129
+ }): Promise<void> {
130
+ const { url, platform, onDataUpdate, onError, onLoading, dataValidator } = options;
131
+ const platformName = platform.charAt(0).toUpperCase() + platform.slice(1);
132
+
133
+ // Schedule background refresh for ALL loads (not just cache hits)
134
+ this.scheduleAsyncRefresh(platform, url, dataValidator, options.apiEndpoint);
135
+
136
+ // Step 1: Check memory cache first
137
+ console.log(`🔍 [${platformName} Cache] Checking memory cache for: ${url}`);
138
+ const memoryData = this.getFromMemory(url);
139
+ if (memoryData && dataValidator(memoryData)) {
140
+ console.log(`💎 [${platformName} Cache] Memory cache hit for: ${url}`);
141
+ onDataUpdate(memoryData);
142
+ onLoading(false);
143
+ return;
144
+ }
145
+
146
+ // Step 2: Check server cache (file system cache)
147
+ console.log(`🔍 [${platformName} Cache] No memory cache, checking server cache for: ${url}`);
148
+ const serverCacheData = await this.getFromServer(options);
149
+ if (serverCacheData && dataValidator(serverCacheData)) {
150
+ console.log(`🎯 [${platformName} Cache] Server cache hit (cached data) for: ${url}`);
151
+ onDataUpdate(serverCacheData);
152
+ this.setInMemory(url, serverCacheData); // Update memory cache
153
+ onLoading(false);
154
+ return;
155
+ }
156
+
157
+ // Step 3: Direct fetch from browser (bypasses server proxy)
158
+ console.log(`🔍 [${platformName} Cache] No server cache, attempting direct fetch for: ${url}`);
159
+
160
+ // For Reddit, try direct browser fetch first (CORS enabled). Use
161
+ // `AbortSignal.timeout(...)` to bound the wait — without it a hung
162
+ // upstream (Reddit dropping the connection without RST, or a captive-
163
+ // portal proxy stalling) would block this code path indefinitely and
164
+ // never fall through to the server proxy.
165
+ if (platform === 'reddit') {
166
+ try {
167
+ const directResponse = await fetch(url, {
168
+ method: 'GET',
169
+ headers: {
170
+ 'Accept': 'application/json',
171
+ },
172
+ signal: AbortSignal.timeout(10_000),
173
+ });
174
+
175
+ if (directResponse.ok) {
176
+ const directData = await directResponse.json();
177
+ if (directData && dataValidator(directData)) {
178
+ console.log(`✅ [${platformName} Direct] Browser direct fetch successful for: ${url}`);
179
+ onDataUpdate(directData);
180
+ this.setInMemory(url, directData);
181
+ onLoading(false);
182
+
183
+ // Async server cache update
184
+ setTimeout(() => {
185
+ this.updateServerCache(options.apiEndpoint, url, directData);
186
+ }, 0);
187
+ return;
188
+ }
189
+ }
190
+ console.log(`💨 [${platformName} Direct] Browser direct fetch failed (${directResponse.status}), trying server proxy...`);
191
+ } catch (directError) {
192
+ console.log(`💨 [${platformName} Direct] Browser direct fetch failed, trying server proxy...`, directError);
193
+ }
194
+ }
195
+
196
+ // Step 4: Server proxy as fallback
197
+ console.log(`🔍 [${platformName} Cache] Trying server proxy for: ${url}`);
198
+ try {
199
+ const response = await fetch(`${options.apiEndpoint}?url=${encodeURIComponent(url)}`);
200
+
201
+ if (response.ok) {
202
+ const serverData = await response.json();
203
+ if (serverData && dataValidator(serverData)) {
204
+ // Check X-Cache header to determine if this was a true cache hit or fresh fetch
205
+ const cacheStatus = response.headers.get('x-cache') || 'UNKNOWN';
206
+ if (cacheStatus === 'HIT') {
207
+ console.log(`🎯 [${platformName} Proxy] Server cache hit (cached data) for: ${url}`);
208
+ } else if (cacheStatus === 'MISS-FETCHED') {
209
+ console.log(`🔄 [${platformName} Proxy] Server processed fresh fetch (now cached) for: ${url}`);
210
+ } else {
211
+ console.log(`✅ [${platformName} Proxy] Server returned data (${cacheStatus}) for: ${url}`);
212
+ }
213
+ onDataUpdate(serverData);
214
+ this.setInMemory(url, serverData);
215
+ onLoading(false);
216
+ return;
217
+ } else {
218
+ // 200 with a body that fails our shape check — throw so the
219
+ // outer catch surfaces `onError` instead of silently dropping.
220
+ console.log(`⚠️ [${platformName} Proxy] Server returned data but validation failed for: ${url}`);
221
+ throw new Error('Server proxy returned invalid data shape');
222
+ }
223
+ } else {
224
+ // The Response body is a one-shot stream — read it ONCE and branch
225
+ // on shape afterwards. The previous code parsed 404s, fell through
226
+ // to the generic-error path, and then called `response.json()` a
227
+ // second time, which throws `TypeError: body stream already read`
228
+ // and masks the real status with a confusing error.
229
+ let errorData: { error?: string; message?: string } | null = null;
230
+ try {
231
+ errorData = await response.json();
232
+ } catch {
233
+ // Body wasn't JSON; errorData stays null and we fall through.
234
+ }
235
+ if (response.status === 404 && errorData?.error && errorData?.message) {
236
+ // Structured 404 (e.g. an unavailable Reddit post). Surface
237
+ // the proxy's message directly to the consumer.
238
+ console.log(`🚫 [${platformName} Proxy] Content unavailable: ${errorData.message}`);
239
+ onError(errorData.message);
240
+ onLoading(false);
241
+ return;
242
+ }
243
+ if (errorData?.error) {
244
+ throw new Error(`Server error: ${errorData.error}`);
245
+ }
246
+ throw new Error(`Server proxy failed: ${response.status}`);
247
+ }
248
+ } catch (error) {
249
+ console.log(`❌ [${platformName} Cache] All fetch attempts failed for: ${url}`, error);
250
+ onError(`Unable to load ${platform} content`);
251
+ onLoading(false);
252
+ }
253
+ }
254
+
255
+ // Schedule async background refresh (runs once, 1 second after load)
256
+ //
257
+ // The hub's old version hard-coded `/api/blog/{reddit,twitter}-proxy`. We
258
+ // now require `apiEndpoint` so embedders can route through their own
259
+ // reverse proxy (e.g. `/content/api/blog/...`).
260
+ private scheduleAsyncRefresh(
261
+ platform: string,
262
+ url: string,
263
+ dataValidator: (data: any) => boolean,
264
+ apiEndpoint: string,
265
+ ): void {
266
+ setTimeout(() => {
267
+ const platformName = platform.charAt(0).toUpperCase() + platform.slice(1);
268
+ console.log(`🔄 [${platformName} Async] Background refresh starting for: ${url}`);
269
+
270
+ fetch(`${apiEndpoint}?url=${encodeURIComponent(url)}`)
271
+ .then(response => {
272
+ if (response.ok) {
273
+ return response.json();
274
+ } else {
275
+ console.log(`❌ [${platformName} Async] Background refresh failed (${response.status}) for: ${url}`);
276
+ return null;
277
+ }
278
+ })
279
+ .then(data => {
280
+ if (data && dataValidator(data)) {
281
+ this.setInMemory(url, data);
282
+ console.log(`✅ [${platformName} Async] Background refresh completed for: ${url}`);
283
+ } else if (data) {
284
+ console.log(`❌ [${platformName} Async] Background refresh failed (invalid data) for: ${url}`);
285
+ }
286
+ })
287
+ .catch((error) => console.log(`❌ [${platformName} Async] Background refresh failed for: ${url}`, error));
288
+ }, 1000); // Runs exactly once, 1 second after load
289
+ }
290
+
291
+ // Background refresh to keep cache fresh (with request deduplication)
292
+ private async backgroundRefresh(options: CacheOptions & {
293
+ directFetcher: () => Promise<any>;
294
+ dataValidator: (data: any) => boolean;
295
+ onDataUpdate: (data: any) => void;
296
+ }, currentData: any): Promise<void> {
297
+ const refreshKey = `refresh-${options.platform}-${options.url}`;
298
+ const now = Date.now();
299
+
300
+ // Check if we've refreshed this URL recently (within 30 seconds)
301
+ const lastRefresh = refreshTimestamps.get(refreshKey);
302
+ if (lastRefresh && (now - lastRefresh) < 30000) {
303
+ console.log(`⏭️ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Background] Refresh skipped (recent refresh): ${options.url}`);
304
+ return;
305
+ }
306
+
307
+ // Prevent multiple background refresh requests for the same URL
308
+ if (cachingRequests.has(refreshKey)) {
309
+ console.log(`⏭️ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Background] Refresh skipped (already in progress): ${options.url}`);
310
+ return;
311
+ }
312
+
313
+ const refreshPromise = (async () => {
314
+ try {
315
+ console.log(`🔄 [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Background] Starting background refresh for: ${options.url}`);
316
+ refreshTimestamps.set(refreshKey, now);
317
+
318
+ const freshData = await this.fetchDirect(options);
319
+
320
+ if (freshData && options.dataValidator(freshData)) {
321
+ // Update cache silently without triggering UI re-render
322
+ this.setInMemory(options.url, freshData);
323
+ await this.setInServer(options, freshData);
324
+
325
+ console.log(`✅ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Background] Background refresh completed for: ${options.url}`);
326
+ } else {
327
+ console.log(`❌ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Background] Background refresh failed (invalid data) for: ${options.url}`);
328
+ }
329
+ } catch (error) {
330
+ console.log(`❌ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Background] Background refresh failed for: ${options.url}`, error);
331
+ } finally {
332
+ cachingRequests.delete(refreshKey);
333
+ }
334
+ })();
335
+
336
+ cachingRequests.set(refreshKey, refreshPromise);
337
+ await refreshPromise;
338
+ }
339
+
340
+ // Silent background refresh for UI-sensitive components
341
+ private silentBackgroundRefresh(options: CacheOptions & {
342
+ directFetcher: () => Promise<any>;
343
+ dataValidator: (data: any) => boolean;
344
+ }): void {
345
+ const refreshKey = `silent-refresh-${options.platform}-${options.url}`;
346
+ const now = Date.now();
347
+
348
+ // Check if we've refreshed this URL recently (within 5 minutes for silent refresh)
349
+ const lastRefresh = refreshTimestamps.get(refreshKey);
350
+ if (lastRefresh && (now - lastRefresh) < 300000) {
351
+ console.log(`⏭️ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Silent] Silent refresh skipped (recent refresh): ${options.url}`);
352
+ return; // Skip if refreshed within 5 minutes
353
+ }
354
+
355
+ // Prevent multiple silent refresh requests for the same URL
356
+ if (cachingRequests.has(refreshKey)) {
357
+ console.log(`⏭️ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Silent] Silent refresh skipped (already in progress): ${options.url}`);
358
+ return;
359
+ }
360
+
361
+ // Run in next tick to avoid blocking UI thread
362
+ setTimeout(() => {
363
+ const refreshPromise = (async () => {
364
+ try {
365
+ console.log(`🔄 [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Silent] Starting silent background refresh for: ${options.url}`);
366
+ refreshTimestamps.set(refreshKey, now);
367
+
368
+ // Fetch fresh data silently
369
+ const freshData = await this.fetchDirect(options);
370
+
371
+ if (freshData && options.dataValidator(freshData)) {
372
+ // Update cache silently - no UI notifications
373
+ this.setInMemory(options.url, freshData);
374
+ await this.setInServer(options, freshData);
375
+ console.log(`✅ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Silent] Silent refresh completed for: ${options.url}`);
376
+ } else {
377
+ console.log(`❌ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Silent] Silent refresh failed (invalid data) for: ${options.url}`);
378
+ }
379
+ } catch (error) {
380
+ console.log(`❌ [${options.platform.charAt(0).toUpperCase() + options.platform.slice(1)} Silent] Silent refresh failed for: ${options.url}`, error);
381
+ } finally {
382
+ cachingRequests.delete(refreshKey);
383
+ }
384
+ })();
385
+
386
+ cachingRequests.set(refreshKey, refreshPromise);
387
+ }, 0); // Defer to next event loop tick
388
+ }
389
+ }
390
+
391
+ export const socialCache = SocialEmbedCache.getInstance();