@flamingo-stack/openframe-frontend-core 0.0.295 → 0.0.296-snapshot.20260621021605

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 (291) 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-7KXD7CWD.js → chunk-3JIQVE7T.js} +9 -15
  5. package/dist/{chunk-7KXD7CWD.js.map → chunk-3JIQVE7T.js.map} +1 -1
  6. package/dist/{chunk-FT4FCV7L.cjs → chunk-4PSQS3SW.cjs} +7 -9
  7. package/dist/chunk-4PSQS3SW.cjs.map +1 -0
  8. package/dist/{chunk-OOKKGOPQ.js → chunk-4TLE6VLU.js} +30 -24
  9. package/dist/chunk-4TLE6VLU.js.map +1 -0
  10. package/dist/{chunk-6IBA2MQV.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-5O6N3BKR.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-XXI7BNB6.cjs → chunk-FQOTC3UU.cjs} +321 -18
  27. package/dist/chunk-FQOTC3UU.cjs.map +1 -0
  28. package/dist/{chunk-INDQMNP6.cjs → chunk-GUTS7HGA.cjs} +11658 -2146
  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-HOVJGXF7.js → chunk-IL47XWV5.js} +8 -14
  33. package/dist/{chunk-HOVJGXF7.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-5IJ46KAV.js → chunk-JALO4TAZ.js} +360 -57
  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-J3RDKZ32.js → chunk-L7ULJKG7.js} +6 -10
  41. package/dist/{chunk-J3RDKZ32.js.map → chunk-L7ULJKG7.js.map} +1 -1
  42. package/dist/{chunk-6BZEAPNT.js → chunk-PC746XCO.js} +15120 -5608
  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-ETACGX2A.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-EJXHZX2E.js → chunk-X4DOXQRT.js} +4 -6
  54. package/dist/{chunk-EJXHZX2E.js.map → chunk-X4DOXQRT.js.map} +1 -1
  55. package/dist/{chunk-A2YL7QRX.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/chat-container.d.ts.map +1 -1
  66. package/dist/components/chat/error-message-display.d.ts.map +1 -1
  67. package/dist/components/chat/index.cjs +8 -18
  68. package/dist/components/chat/index.cjs.map +1 -1
  69. package/dist/components/chat/index.js +75 -85
  70. package/dist/components/chat/types/component.types.d.ts +2 -0
  71. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  72. package/dist/components/contact/index.cjs +8 -15
  73. package/dist/components/contact/index.cjs.map +1 -1
  74. package/dist/components/contact/index.js +7 -14
  75. package/dist/components/docs/doc-viewer.d.ts +39 -2
  76. package/dist/components/docs/doc-viewer.d.ts.map +1 -1
  77. package/dist/components/docs/docs-hub-page.d.ts +46 -0
  78. package/dist/components/docs/docs-hub-page.d.ts.map +1 -0
  79. package/dist/components/docs/index.cjs +17 -9
  80. package/dist/components/docs/index.cjs.map +1 -1
  81. package/dist/components/docs/index.d.ts +4 -0
  82. package/dist/components/docs/index.d.ts.map +1 -1
  83. package/dist/components/docs/index.js +16 -8
  84. package/dist/components/docs/skeletons.d.ts +32 -0
  85. package/dist/components/docs/skeletons.d.ts.map +1 -0
  86. package/dist/components/docs/use-docs-resolve-link.d.ts +20 -0
  87. package/dist/components/docs/use-docs-resolve-link.d.ts.map +1 -0
  88. package/dist/components/docs/use-document-tree.d.ts.map +1 -1
  89. package/dist/components/embeds/embed-container.d.ts +37 -0
  90. package/dist/components/embeds/embed-container.d.ts.map +1 -0
  91. package/dist/components/embeds/embed-iframe.d.ts.map +1 -1
  92. package/dist/components/embeds/file-download-card.d.ts +18 -0
  93. package/dist/components/embeds/file-download-card.d.ts.map +1 -0
  94. package/dist/components/embeds/index.cjs +38 -15
  95. package/dist/components/embeds/index.cjs.map +1 -1
  96. package/dist/components/embeds/index.d.ts +8 -0
  97. package/dist/components/embeds/index.d.ts.map +1 -1
  98. package/dist/components/embeds/index.js +40 -17
  99. package/dist/components/embeds/linkedin-embed-client.d.ts +8 -0
  100. package/dist/components/embeds/linkedin-embed-client.d.ts.map +1 -0
  101. package/dist/components/embeds/markdown-image.d.ts +5 -0
  102. package/dist/components/embeds/markdown-image.d.ts.map +1 -0
  103. package/dist/components/embeds/reddit-embed-client.d.ts +7 -0
  104. package/dist/components/embeds/reddit-embed-client.d.ts.map +1 -0
  105. package/dist/components/embeds/rich-markdown-runtime.d.ts +46 -0
  106. package/dist/components/embeds/rich-markdown-runtime.d.ts.map +1 -0
  107. package/dist/components/embeds/twitter-embed-client.d.ts +8 -0
  108. package/dist/components/embeds/twitter-embed-client.d.ts.map +1 -0
  109. package/dist/components/faq/index.cjs +9 -16
  110. package/dist/components/faq/index.cjs.map +1 -1
  111. package/dist/components/faq/index.js +8 -15
  112. package/dist/components/features/index.cjs +8 -16
  113. package/dist/components/features/index.cjs.map +1 -1
  114. package/dist/components/features/index.js +24 -32
  115. package/dist/components/features/notifications/notification-drawer.d.ts.map +1 -1
  116. package/dist/components/features/notifications/notifications-context.d.ts +5 -1
  117. package/dist/components/features/notifications/notifications-context.d.ts.map +1 -1
  118. package/dist/components/index.cjs +257 -452
  119. package/dist/components/index.cjs.map +1 -1
  120. package/dist/components/index.js +781 -976
  121. package/dist/components/index.js.map +1 -1
  122. package/dist/components/layout/page-header.d.ts +78 -0
  123. package/dist/components/layout/page-header.d.ts.map +1 -0
  124. package/dist/components/layout/page-layout.d.ts +10 -1
  125. package/dist/components/layout/page-layout.d.ts.map +1 -1
  126. package/dist/components/layout/page-with-header.d.ts +67 -0
  127. package/dist/components/layout/page-with-header.d.ts.map +1 -0
  128. package/dist/components/layout/title-block.d.ts +17 -1
  129. package/dist/components/layout/title-block.d.ts.map +1 -1
  130. package/dist/components/navigation/index.cjs +7 -15
  131. package/dist/components/navigation/index.cjs.map +1 -1
  132. package/dist/components/navigation/index.js +9 -17
  133. package/dist/components/onboarding-guides/index.cjs +35 -36
  134. package/dist/components/onboarding-guides/index.cjs.map +1 -1
  135. package/dist/components/onboarding-guides/index.js +13 -14
  136. package/dist/components/onboarding-guides/index.js.map +1 -1
  137. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +1 -1
  138. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  139. package/dist/components/related-content/index.cjs +9 -16
  140. package/dist/components/related-content/index.cjs.map +1 -1
  141. package/dist/components/related-content/index.js +8 -15
  142. package/dist/components/shared/dev-section/dev-section-page.d.ts +9 -0
  143. package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
  144. package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -1
  145. package/dist/components/shared/dev-section/index.d.ts +1 -1
  146. package/dist/components/shared/dev-section/index.d.ts.map +1 -1
  147. package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -1
  148. package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
  149. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  150. package/dist/components/tickets/index.cjs +100 -112
  151. package/dist/components/tickets/index.cjs.map +1 -1
  152. package/dist/components/tickets/index.js +20 -32
  153. package/dist/components/tickets/index.js.map +1 -1
  154. package/dist/components/ui/button/split-button.d.ts.map +1 -1
  155. package/dist/components/ui/file-manager/index.cjs +50 -52
  156. package/dist/components/ui/file-manager/index.cjs.map +1 -1
  157. package/dist/components/ui/file-manager/index.js +4 -6
  158. package/dist/components/ui/file-manager/index.js.map +1 -1
  159. package/dist/components/ui/index.cjs +13 -19
  160. package/dist/components/ui/index.cjs.map +1 -1
  161. package/dist/components/ui/index.d.ts +2 -0
  162. package/dist/components/ui/index.d.ts.map +1 -1
  163. package/dist/components/ui/index.js +133 -139
  164. package/dist/components/ui/release-changelog-section.d.ts +6 -2
  165. package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
  166. package/dist/components/ui/rich-markdown-renderer.d.ts +34 -0
  167. package/dist/components/ui/rich-markdown-renderer.d.ts.map +1 -0
  168. package/dist/components/ui/simple-markdown-renderer.d.ts +2 -8
  169. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
  170. package/dist/contexts/chat-runtime-context.d.ts +14 -0
  171. package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
  172. package/dist/contexts/index.cjs +3 -3
  173. package/dist/contexts/index.js +5 -5
  174. package/dist/embed-shims/index.cjs +3 -3
  175. package/dist/embed-shims/index.cjs.map +1 -1
  176. package/dist/embed-shims/index.js +4 -4
  177. package/dist/hooks/index.cjs +4 -9
  178. package/dist/hooks/index.cjs.map +1 -1
  179. package/dist/hooks/index.js +6 -11
  180. package/dist/index.cjs +14 -20
  181. package/dist/index.cjs.map +1 -1
  182. package/dist/index.js +362 -368
  183. package/dist/types/doc-source.d.ts +31 -1
  184. package/dist/types/doc-source.d.ts.map +1 -1
  185. package/dist/utils/index.cjs +4 -0
  186. package/dist/utils/index.cjs.map +1 -1
  187. package/dist/utils/index.d.ts +1 -0
  188. package/dist/utils/index.d.ts.map +1 -1
  189. package/dist/utils/index.js +4 -1
  190. package/dist/utils/index.js.map +1 -1
  191. package/dist/utils/page-header-constants.d.ts +15 -0
  192. package/dist/utils/page-header-constants.d.ts.map +1 -0
  193. package/dist/utils/social-embed-cache.d.ts +29 -0
  194. package/dist/utils/social-embed-cache.d.ts.map +1 -0
  195. package/package.json +7 -1
  196. package/src/components/case-studies/index.ts +4 -0
  197. package/src/components/case-studies/share-experience-section.tsx +185 -0
  198. package/src/components/chat/chat-container.tsx +5 -7
  199. package/src/components/chat/embeddable-chat.tsx +1 -1
  200. package/src/components/chat/error-message-display.tsx +49 -31
  201. package/src/components/chat/types/component.types.ts +2 -0
  202. package/src/components/docs/doc-viewer.tsx +111 -19
  203. package/src/components/docs/docs-hub-page.tsx +149 -0
  204. package/src/components/docs/index.ts +17 -0
  205. package/src/components/docs/skeletons.tsx +138 -0
  206. package/src/components/docs/use-docs-resolve-link.ts +52 -0
  207. package/src/components/docs/use-document-tree.ts +21 -0
  208. package/src/components/embeds/embed-container.tsx +80 -0
  209. package/src/components/embeds/embed-iframe.tsx +7 -9
  210. package/src/components/embeds/file-download-card.tsx +54 -0
  211. package/src/components/embeds/index.ts +30 -0
  212. package/src/components/embeds/linkedin-embed-client.tsx +100 -0
  213. package/src/components/embeds/markdown-image.tsx +88 -0
  214. package/src/components/embeds/og-link-preview.tsx +13 -13
  215. package/src/components/embeds/reddit-embed-client.tsx +550 -0
  216. package/src/components/embeds/rich-markdown-runtime.tsx +79 -0
  217. package/src/components/embeds/twitter-embed-client.tsx +308 -0
  218. package/src/components/features/notifications/notification-drawer.tsx +18 -7
  219. package/src/components/features/notifications/notifications-context.tsx +7 -0
  220. package/src/components/layout/page-header.tsx +182 -0
  221. package/src/components/layout/page-layout.tsx +14 -1
  222. package/src/components/layout/page-with-header.tsx +110 -0
  223. package/src/components/layout/title-block.tsx +40 -62
  224. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +3 -3
  225. package/src/components/shared/dev-section/dev-section-page.tsx +9 -1
  226. package/src/components/shared/dev-section/dev-section-view.tsx +14 -9
  227. package/src/components/shared/dev-section/index.ts +1 -1
  228. package/src/components/shared/doc-search/use-doc-search.ts +7 -3
  229. package/src/components/shared/legal-document/legal-document-page.tsx +2 -2
  230. package/src/components/shared/product-release/release-detail-page.tsx +6 -4
  231. package/src/components/ui/button/split-button.tsx +5 -2
  232. package/src/components/ui/index.ts +2 -0
  233. package/src/components/ui/release-changelog-section.tsx +7 -2
  234. package/src/components/ui/rich-markdown-renderer.tsx +1203 -0
  235. package/src/components/ui/simple-markdown-renderer.tsx +7 -11
  236. package/src/contexts/chat-runtime-context.tsx +14 -0
  237. package/src/stories/NotificationDrawer.stories.tsx +2 -0
  238. package/src/types/doc-source.ts +33 -1
  239. package/src/utils/index.ts +1 -0
  240. package/src/utils/page-header-constants.ts +15 -0
  241. package/src/utils/social-embed-cache.ts +391 -0
  242. package/dist/chunk-26PKDALD.js +0 -2379
  243. package/dist/chunk-26PKDALD.js.map +0 -1
  244. package/dist/chunk-3MCHAFHB.js +0 -89
  245. package/dist/chunk-3MCHAFHB.js.map +0 -1
  246. package/dist/chunk-3XIB4VKS.cjs +0 -619
  247. package/dist/chunk-3XIB4VKS.cjs.map +0 -1
  248. package/dist/chunk-4W7NYJ3B.cjs +0 -3009
  249. package/dist/chunk-4W7NYJ3B.cjs.map +0 -1
  250. package/dist/chunk-5E2HOSSH.cjs.map +0 -1
  251. package/dist/chunk-5IJ46KAV.js.map +0 -1
  252. package/dist/chunk-5O6N3BKR.cjs.map +0 -1
  253. package/dist/chunk-6BZEAPNT.js.map +0 -1
  254. package/dist/chunk-6IBA2MQV.cjs.map +0 -1
  255. package/dist/chunk-6JINAOI7.cjs +0 -311
  256. package/dist/chunk-6JINAOI7.cjs.map +0 -1
  257. package/dist/chunk-7RIYT7ZH.js.map +0 -1
  258. package/dist/chunk-A2YL7QRX.cjs.map +0 -1
  259. package/dist/chunk-AQOWFSMB.cjs.map +0 -1
  260. package/dist/chunk-E4XABBSU.js.map +0 -1
  261. package/dist/chunk-ETACGX2A.cjs.map +0 -1
  262. package/dist/chunk-EYEW6PTA.cjs.map +0 -1
  263. package/dist/chunk-FQJK446R.js +0 -1606
  264. package/dist/chunk-FQJK446R.js.map +0 -1
  265. package/dist/chunk-FT4FCV7L.cjs.map +0 -1
  266. package/dist/chunk-INDQMNP6.cjs.map +0 -1
  267. package/dist/chunk-J54Z3OCR.cjs +0 -1606
  268. package/dist/chunk-J54Z3OCR.cjs.map +0 -1
  269. package/dist/chunk-KXCRGTRN.cjs +0 -2379
  270. package/dist/chunk-KXCRGTRN.cjs.map +0 -1
  271. package/dist/chunk-LCNMR277.js.map +0 -1
  272. package/dist/chunk-LFGGF7OT.cjs +0 -449
  273. package/dist/chunk-LFGGF7OT.cjs.map +0 -1
  274. package/dist/chunk-M2OCXTNT.js +0 -311
  275. package/dist/chunk-M2OCXTNT.js.map +0 -1
  276. package/dist/chunk-NSPOYUBH.js +0 -3009
  277. package/dist/chunk-NSPOYUBH.js.map +0 -1
  278. package/dist/chunk-OOKKGOPQ.js.map +0 -1
  279. package/dist/chunk-OQ6X7ZOC.js +0 -449
  280. package/dist/chunk-OQ6X7ZOC.js.map +0 -1
  281. package/dist/chunk-POKKCWKF.js +0 -354
  282. package/dist/chunk-POKKCWKF.js.map +0 -1
  283. package/dist/chunk-TFSYSWPS.cjs +0 -89
  284. package/dist/chunk-TFSYSWPS.cjs.map +0 -1
  285. package/dist/chunk-XXI7BNB6.cjs.map +0 -1
  286. package/dist/chunk-YD43AKI5.js +0 -619
  287. package/dist/chunk-YD43AKI5.js.map +0 -1
  288. package/dist/chunk-YETA25JW.cjs +0 -354
  289. package/dist/chunk-YETA25JW.cjs.map +0 -1
  290. package/dist/chunk-YIGPRLQY.cjs.map +0 -1
  291. /package/dist/{chunk-3ZXUQQL4.js.map → chunk-PI4WSYQV.js.map} +0 -0
@@ -0,0 +1,1203 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react';
4
+ import ReactMarkdown from 'react-markdown';
5
+ import remarkGfm from 'remark-gfm';
6
+ import remarkBreaks from 'remark-breaks';
7
+ import rehypeHighlight from 'rehype-highlight';
8
+ import rehypeRaw from 'rehype-raw';
9
+ // Theme removed - using fixed dark mode for platform consistency
10
+ // Using rehype-highlight instead of SyntaxHighlighter for better integration
11
+ import { RedditEmbedClient } from '../embeds/reddit-embed-client';
12
+ import { TwitterEmbedClient } from '../embeds/twitter-embed-client';
13
+ import { LinkedInEmbedClient } from '../embeds/linkedin-embed-client';
14
+ import { Video } from '../features/video';
15
+ import { ErrorIcon } from '../icons/error-icon';
16
+ import { OGLinkPreview, OGLinkErrorBoundary } from '../embeds/og-link-preview';
17
+ import { FigmaEmbed } from '../embeds/figma-embed';
18
+ import { MarkdownImage } from '../embeds/markdown-image';
19
+ import {
20
+ RichMarkdownRuntimeProvider,
21
+ useRichMarkdownRuntime,
22
+ type RichMarkdownRuntime,
23
+ } from '../embeds/rich-markdown-runtime';
24
+
25
+ // Import highlight.js styles only - rehype-highlight handles the actual highlighting
26
+ // No manual language imports needed
27
+
28
+
29
+
30
+ // Global styles for Mermaid diagrams
31
+ const mermaidStyles = `
32
+ .mermaid-svg-container svg {
33
+ max-width: 100% !important;
34
+ height: auto !important;
35
+ min-height: 200px;
36
+ font-family: 'DM Sans', sans-serif !important;
37
+ font-size: 14px !important;
38
+ }
39
+
40
+ /* Desktop sizing - larger and more prominent */
41
+ @media (min-width: 1520px) {
42
+ .mermaid-svg-container svg {
43
+ max-width: 900px !important;
44
+ max-height: 700px !important;
45
+ min-height: 300px;
46
+ font-size: 16px !important;
47
+ }
48
+ }
49
+
50
+ /* Medium screens (tablets/laptops) */
51
+ @media (min-width: 768px) and (max-width: 1519px) {
52
+ .mermaid-svg-container svg {
53
+ max-width: 700px !important;
54
+ max-height: 600px !important;
55
+ min-height: 250px;
56
+ font-size: 15px !important;
57
+ }
58
+ }
59
+
60
+ /* Mobile responsiveness */
61
+ @media (max-width: 767px) {
62
+ .mermaid-svg-container svg {
63
+ max-width: 90vw !important;
64
+ max-height: 400px !important;
65
+ min-height: 200px;
66
+ font-size: 13px !important;
67
+ }
68
+ }
69
+
70
+ /* Responsive pie chart and flowchart sizing */
71
+ .mermaid-svg-container svg[width] {
72
+ width: 100% !important;
73
+ }
74
+
75
+ .mermaid-svg-container .node rect,
76
+ .mermaid-svg-container .node circle,
77
+ .mermaid-svg-container .node ellipse,
78
+ .mermaid-svg-container .node polygon {
79
+ stroke-width: 2px !important;
80
+ }
81
+ .mermaid-svg-container .edgePath path {
82
+ stroke-width: 2px !important;
83
+ }
84
+
85
+ /* Enhance readability on larger screens */
86
+ @media (min-width: 768px) {
87
+ .mermaid-svg-container .node text,
88
+ .mermaid-svg-container .edgeLabel text {
89
+ font-size: 14px !important;
90
+ }
91
+ }
92
+
93
+ @media (min-width: 1520px) {
94
+ .mermaid-svg-container .node text,
95
+ .mermaid-svg-container .edgeLabel text {
96
+ font-size: 16px !important;
97
+ }
98
+ }
99
+ `;
100
+
101
+ // Interface definition moved above the component
102
+
103
+ // <Video> is the single source of truth for every video surface — it
104
+ // handles YouTube facade + Mux HLS + MP4 fallback behind one component.
105
+
106
+ // Mermaid Diagram Component
107
+ const MermaidDiagram: React.FC<{ chart: string }> = ({ chart }) => {
108
+ const [svg, setSvg] = useState<string>('');
109
+ const [error, setError] = useState<string>('');
110
+ const [isLoading, setIsLoading] = useState<boolean>(true);
111
+ const [mounted, setMounted] = useState(false);
112
+
113
+ useEffect(() => {
114
+ setMounted(true);
115
+ }, []);
116
+
117
+ // Fixed dark mode for platform consistency
118
+ const isDarkMode = true;
119
+
120
+ useEffect(() => {
121
+ const renderMermaid = async () => {
122
+ try {
123
+ setIsLoading(true);
124
+ const { default: mermaid } = await import('mermaid');
125
+
126
+ // Configure theme based on detected mode
127
+ const themeConfig = isDarkMode ? {
128
+ theme: 'dark' as const,
129
+ themeVariables: {
130
+ primaryColor: '#FFC008',
131
+ primaryTextColor: '#FAFAFA',
132
+ primaryBorderColor: '#3A3A3A',
133
+ lineColor: '#888888',
134
+ secondaryColor: '#212121',
135
+ tertiaryColor: '#2A2A2A',
136
+ background: 'transparent',
137
+ mainBkg: 'transparent',
138
+ secondBkg: 'transparent',
139
+ tertiaryBkg: 'transparent',
140
+ cScale0: '#FFC008',
141
+ cScale1: '#4ECDC4',
142
+ cScale2: '#45B7D1',
143
+ cScale3: '#96CEB4',
144
+ cScale4: '#FFEAA7',
145
+ cScale5: '#DDA0DD',
146
+ cScale6: '#98D8C8',
147
+ cScale7: '#F7DC6F',
148
+ cScale8: '#BB8FCE',
149
+ cScale9: '#85C1E9',
150
+ taskTextColor: '#FAFAFA',
151
+ taskTextOutsideColor: '#FAFAFA',
152
+ activeTaskTextColor: '#1A1A1A',
153
+ nodeTextColor: '#FAFAFA'
154
+ }
155
+ } : {
156
+ theme: 'base' as const,
157
+ themeVariables: {
158
+ primaryColor: '#FFC008',
159
+ primaryTextColor: '#1A1A1A',
160
+ primaryBorderColor: '#D1D5DB',
161
+ lineColor: '#6B7280',
162
+ secondaryColor: '#F3F4F6',
163
+ tertiaryColor: '#E5E7EB',
164
+ background: 'transparent',
165
+ mainBkg: 'transparent',
166
+ secondBkg: 'transparent',
167
+ tertiaryBkg: 'transparent',
168
+ cScale0: '#F59E0B',
169
+ cScale1: '#10B981',
170
+ cScale2: '#3B82F6',
171
+ cScale3: '#8B5CF6',
172
+ cScale4: '#EF4444',
173
+ cScale5: '#F97316',
174
+ cScale6: '#06B6D4',
175
+ cScale7: '#84CC16',
176
+ cScale8: '#EC4899',
177
+ cScale9: '#6366F1',
178
+ taskTextColor: '#1A1A1A',
179
+ taskTextOutsideColor: '#1A1A1A',
180
+ activeTaskTextColor: '#FFFFFF',
181
+ nodeTextColor: '#1A1A1A',
182
+ textColor: '#1A1A1A',
183
+ labelTextColor: '#1A1A1A'
184
+ }
185
+ };
186
+
187
+ mermaid.initialize({
188
+ startOnLoad: false,
189
+ ...themeConfig,
190
+ // Ensure proper sizing
191
+ flowchart: {
192
+ useMaxWidth: true,
193
+ htmlLabels: true,
194
+ rankSpacing: 50,
195
+ nodeSpacing: 30,
196
+ curve: 'basis'
197
+ },
198
+ sequence: {
199
+ useMaxWidth: true,
200
+ width: 150
201
+ },
202
+ pie: {
203
+ useMaxWidth: true,
204
+ useWidth: undefined
205
+ },
206
+ // Global font settings
207
+ fontFamily: 'DM Sans, sans-serif',
208
+ fontSize: 14,
209
+ // More lenient parsing
210
+ securityLevel: 'loose'
211
+ });
212
+
213
+ const { svg: renderedSvg } = await mermaid.render(`mermaid-${Date.now()}`, chart);
214
+ setSvg(renderedSvg);
215
+ setIsLoading(false);
216
+ } catch (err) {
217
+ console.error('Mermaid rendering error:', err);
218
+ setError(`Failed to render diagram: ${err instanceof Error ? err.message : 'Unknown error'}`);
219
+ setIsLoading(false);
220
+ }
221
+ };
222
+
223
+ if (mounted) {
224
+ renderMermaid();
225
+ }
226
+ }, [chart, isDarkMode, mounted]);
227
+
228
+ if (error) {
229
+ return (
230
+ <div className="error-state bg-ods-card border border-ods-border rounded-lg p-6 my-6">
231
+ <div className="error-icon flex justify-center mb-4">
232
+ <ErrorIcon className="w-12 h-12 text-ods-error" />
233
+ </div>
234
+ <div className="error-title text-center font-sans font-semibold text-lg text-ods-error mb-2">
235
+ Diagram Error
236
+ </div>
237
+ <div className="error-description text-center font-sans text-sm text-ods-text-secondary mb-4 break-words overflow-hidden max-w-full">
238
+ <div className="overflow-x-auto">
239
+ <pre className="whitespace-pre-wrap break-words text-xs">{error}</pre>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ );
244
+ }
245
+
246
+ if (isLoading || !svg) {
247
+ return (
248
+ <div className="skeleton-code bg-ods-card border border-ods-border rounded-lg p-6 min-h-[120px] flex items-center justify-center">
249
+ <div className="animate-pulse text-ods-text-tertiary font-sans">
250
+ {isLoading ? 'Loading diagram renderer...' : 'Rendering diagram...'}
251
+ </div>
252
+ </div>
253
+ );
254
+ }
255
+
256
+ const containerClasses = isDarkMode
257
+ ? 'mermaid-container rounded-lg p-4 md:p-6 lg:p-8 my-6 overflow-x-auto bg-ods-card border border-ods-border'
258
+ : 'mermaid-container rounded-lg p-4 md:p-6 lg:p-8 my-6 overflow-x-auto bg-white border border-ods-border';
259
+
260
+ return (
261
+ <div className={containerClasses}>
262
+ <div className="flex justify-center items-center w-full min-h-[200px] md:min-h-[250px] lg:min-h-[300px]">
263
+ <div
264
+ className="mermaid-svg-container w-full flex justify-center max-w-full"
265
+ style={{
266
+ fontSize: '14px'
267
+ }}
268
+ dangerouslySetInnerHTML={{
269
+ __html: svg.replace(
270
+ /<svg[^>]*>/,
271
+ (match) => {
272
+ // Force responsive sizing for all diagrams, especially pie charts
273
+ return match
274
+ .replace(/width="[^"]*"/, 'width="100%"')
275
+ .replace(/height="[^"]*"/, 'height="auto"')
276
+ .replace(/viewBox="[^"]*"/, (viewBoxMatch) => {
277
+ // Preserve viewBox for proper scaling
278
+ return viewBoxMatch;
279
+ });
280
+ }
281
+ )
282
+ }}
283
+ />
284
+ </div>
285
+ </div>
286
+ );
287
+ };
288
+
289
+ // Process shortcodes AND auto-detect URLs before passing to react-markdown
290
+ const processShortcodes = (content: string): string => {
291
+ let processedContent = content;
292
+
293
+ // Escape values interpolated into the raw HTML `data-*` attributes generated below.
294
+ // With rehypeRaw enabled, an unescaped `"`/`<`/`>` in a URL or id could break out of
295
+ // the attribute and inject markup, so every interpolated embed value goes through this.
296
+ const escapeAttr = (value: string) =>
297
+ value
298
+ .replace(/&/g, '&amp;')
299
+ .replace(/"/g, '&quot;')
300
+ .replace(/</g, '&lt;')
301
+ .replace(/>/g, '&gt;');
302
+
303
+ // First, process explicit shortcodes
304
+ processedContent = processedContent
305
+ // YouTube embeds: {{youtube:VIDEO_ID}}
306
+ .replace(/\{\{youtube:([^}]+)\}\}/g, (match, videoId) => {
307
+ return `\n\n<div class="youtube-embed" data-video-id="${escapeAttr(videoId.trim())}"></div>\n\n`;
308
+ })
309
+ // Markdoc-style YouTube: {% youtube id="VIDEO_ID" /%} or {% youtube id="VIDEO_ID" title="..." /%}
310
+ .replace(/\{%\s*youtube\s+id="([^"]+)"(?:\s+title="[^"]*")?\s*\/?%\}/g, (match, videoId) => {
311
+ return `\n\n<div class="youtube-embed" data-video-id="${escapeAttr(videoId.trim())}"></div>\n\n`;
312
+ })
313
+ /**
314
+ * SHORTCODE: YouTube Thumbnail Link (RECOMMENDED - GitHub + Flamingo Compatible)
315
+ *
316
+ * This is a SHORTCODE pattern processed in processShortcodes(), NOT auto-detection.
317
+ * It is the PREFERRED format because it works on BOTH GitHub AND Flamingo:
318
+ * - On GitHub: Renders as a clickable thumbnail image linking to YouTube
319
+ * - On Flamingo: Converts to a full embedded YouTube player
320
+ *
321
+ * SYNTAX: [![Title](https://img.youtube.com/vi/VIDEO_ID/QUALITY.jpg)](https://www.youtube.com/watch?v=VIDEO_ID)
322
+ *
323
+ * HOW TO CREATE:
324
+ * 1. Get your VIDEO_ID from: youtube.com/watch?v=VIDEO_ID or youtu.be/VIDEO_ID
325
+ * 2. Choose thumbnail quality: maxresdefault.jpg (HD), hqdefault.jpg, or 0.jpg
326
+ * 3. Build the pattern: [![Your Title](https://img.youtube.com/vi/YOUR_VIDEO_ID/maxresdefault.jpg)](https://www.youtube.com/watch?v=YOUR_VIDEO_ID)
327
+ *
328
+ * THUMBNAIL QUALITY OPTIONS:
329
+ * - maxresdefault.jpg - HD 1280x720 (may not exist for all videos)
330
+ * - hqdefault.jpg - High quality 480x360
331
+ * - 0.jpg - Standard quality 480x360
332
+ *
333
+ * COMPLETE EXAMPLE (video ID: awc-yAnkhIo):
334
+ * [![OpenFrame Demo](https://img.youtube.com/vi/awc-yAnkhIo/maxresdefault.jpg)](https://www.youtube.com/watch?v=awc-yAnkhIo)
335
+ *
336
+ * USE THIS FORMAT for all documentation that needs to work on both GitHub and Flamingo.
337
+ * Only use {{youtube:ID}} or {% youtube id="ID" /%} for Flamingo-only content.
338
+ */
339
+ .replace(/\[!\[([^\]]*)\]\(https?:\/\/img\.youtube\.com\/vi\/([a-zA-Z0-9_-]+)\/[^)]+\)\]\(https?:\/\/(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+)[^)]*\)/g,
340
+ (match, altText, thumbId, videoId) => {
341
+ return `\n\n<div class="youtube-embed" data-video-id="${videoId}"></div>\n\n`;
342
+ })
343
+ // Reddit embeds: {{reddit:POST_URL}}
344
+ .replace(/\{\{reddit:([^}]+)\}\}/g, (match, urlOrId) => {
345
+ const postUrl = urlOrId.trim();
346
+ // Handle both full URLs and relative paths
347
+ const fullUrl = postUrl.startsWith('http') ? postUrl : `https://reddit.com/r/${postUrl}`;
348
+ return `\n\n<div class="reddit-embed" data-post-url="${escapeAttr(fullUrl)}"></div>\n\n`;
349
+ })
350
+ // Twitter/X embeds: {{tweet:TWEET_URL}} or {{twitter:TWEET_URL}}
351
+ .replace(/\{\{(?:tweet|twitter):([^}]+)\}\}/g, (match, urlOrId) => {
352
+ const tweetInput = urlOrId.trim();
353
+ // Handle both full URLs and tweet IDs
354
+ const tweetUrl = tweetInput.startsWith('http')
355
+ ? tweetInput
356
+ : `https://twitter.com/twitter/status/${tweetInput}`;
357
+ return `\n\n<div class="tweet-embed" data-tweet-url="${escapeAttr(tweetUrl)}"></div>\n\n`;
358
+ })
359
+ // Figma embeds: {{figma:URL}}
360
+ .replace(/\{\{figma:([^}]+)\}\}/g, (match, url) => {
361
+ return `\n\n<div class="figma-embed" data-figma-url="${escapeAttr(url.trim())}"></div>\n\n`;
362
+ })
363
+ // LinkedIn embeds: {{linkedin:POST_URL}}
364
+ .replace(/\{\{linkedin:([^}]+)\}\}/g, (match, url) => {
365
+ return `\n\n<div class="linkedin-embed" data-post-url="${escapeAttr(url.trim())}"></div>\n\n`;
366
+ })
367
+ // Link previews: {{link:URL}}
368
+ .replace(/\{\{link:([^}]+)\}\}/g, (match, url) => {
369
+ return `\n\n<div class="link-preview" data-url="${escapeAttr(url.trim())}"></div>\n\n`;
370
+ });
371
+
372
+ // Next, auto-detect standalone URLs (but NOT those already in markdown links or code blocks)
373
+
374
+ // Step 1: Temporarily replace code blocks to protect them
375
+ const codeBlocks: string[] = [];
376
+ processedContent = processedContent.replace(/```[\s\S]*?```|`[^`]+`/g, (match) => {
377
+ const placeholder = `__CODE_BLOCK_${codeBlocks.length}__`;
378
+ codeBlocks.push(match);
379
+ return placeholder;
380
+ });
381
+
382
+ // Step 2: Temporarily replace markdown links to protect them
383
+ const markdownLinks: string[] = [];
384
+ processedContent = processedContent.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match) => {
385
+ const placeholder = `__MARKDOWN_LINK_${markdownLinks.length}__`;
386
+ markdownLinks.push(match);
387
+ return placeholder;
388
+ });
389
+
390
+ // Step 2.5: Temporarily replace table rows to protect URLs inside tables
391
+ const tableRows: string[] = [];
392
+ processedContent = processedContent.replace(/^\|.+\|$/gm, (match) => {
393
+ const placeholder = `__TABLE_ROW_${tableRows.length}__`;
394
+ tableRows.push(match);
395
+ return placeholder;
396
+ });
397
+
398
+ // Step 3: Auto-detect standalone URLs and convert to appropriate embeds
399
+ processedContent = processedContent
400
+ // YouTube URLs (standalone only)
401
+ .replace(/(?:^|\s)(https?:\/\/(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+))(?:\s|$)/g,
402
+ (match, fullUrl, videoId, offset, string) => {
403
+ return match.replace(fullUrl, `\n\n<div class="youtube-embed" data-video-id="${videoId}"></div>\n\n`);
404
+ })
405
+ // Reddit URLs (standalone only) - matches any reddit.com URL pattern
406
+ .replace(/(?:^|\s)(https?:\/\/(?:www\.)?reddit\.com\/[^\s]+)(?:\s|$)/g,
407
+ (match, redditUrl) => {
408
+ return match.replace(redditUrl, `\n\n<div class="reddit-embed" data-post-url="${escapeAttr(redditUrl)}"></div>\n\n`);
409
+ })
410
+ // Twitter/X URLs (standalone only)
411
+ .replace(/(?:^|\s)(https?:\/\/(?:www\.)?(?:twitter\.com|x\.com)\/[^\/\s]+\/status\/\d+)(?:\s|$)/g,
412
+ (match, tweetUrl) => {
413
+ return match.replace(tweetUrl, `\n\n<div class="tweet-embed" data-tweet-url="${escapeAttr(tweetUrl)}"></div>\n\n`);
414
+ })
415
+ // Figma URLs (standalone only) - design/file/proto/board/deck/slides → interactive embed
416
+ .replace(/(?:^|\s)(https?:\/\/(?:www\.|embed\.)?figma\.com\/(?:design|file|proto|board|deck|slides)\/[^\s]+)(?:\s|$)/g,
417
+ (match, figmaUrl) => {
418
+ return match.replace(figmaUrl, `\n\n<div class="figma-embed" data-figma-url="${escapeAttr(figmaUrl)}"></div>\n\n`);
419
+ })
420
+ // LinkedIn post URLs (standalone only) → native post embed (like reddit/twitter)
421
+ .replace(/(?:^|\s)(https?:\/\/(?:www\.)?linkedin\.com\/(?:posts|feed\/update|embed\/feed\/update)\/[^\s]+)(?:\s|$)/g,
422
+ (match, liUrl) => {
423
+ return match.replace(liUrl, `\n\n<div class="linkedin-embed" data-post-url="${escapeAttr(liUrl)}"></div>\n\n`);
424
+ })
425
+ // Other external URLs (standalone only) - convert to link previews
426
+ .replace(/(?:^|\s)(https?:\/\/[^\s]+)(?:\s|$)/g,
427
+ (match, url) => {
428
+ try {
429
+ // Skip if already processed as a specific embed above
430
+ // Use more precise domain matching to avoid false positives like "zabbix.com" containing "x.com"
431
+ const urlObj = new URL(url);
432
+ const hostname = urlObj.hostname.toLowerCase();
433
+
434
+ // Skip URLs already handled by specific embed handlers above (videos, posts, tweets).
435
+ // Exact host match (or a subdomain of it) — substring checks like
436
+ // `hostname.includes('x.com')` false-positive on "zabbix.com", and
437
+ // `includes('figma.com')` would match a hostile "evil-figma.com".
438
+ const hostIs = (domain: string) =>
439
+ hostname === domain || hostname.endsWith(`.${domain}`);
440
+ // Allow non-video YouTube URLs (channels, playlists, `@handle`) to fall
441
+ // through to the og-scraper — the scraper now sends a YouTube consent
442
+ // cookie so the channel OG metadata (title, channel avatar) comes back
443
+ // rich. Only video URLs (`?v=` / youtu.be) become inline player embeds.
444
+ const isYouTubeVideo =
445
+ (hostIs('youtube.com') && urlObj.searchParams.has('v')) || hostIs('youtu.be');
446
+ // Allow LinkedIn non-post URLs (profile `/in/`, company `/company/`,
447
+ // etc.) to fall through to the og-scraper. LinkedIn returns full OG for
448
+ // signed-out preview crawlers — og:title with name + employer,
449
+ // og:description with bio + experience, og:image with profile photo.
450
+ // Only interactive post embeds keep their dedicated handler above.
451
+ if (isYouTubeVideo ||
452
+ hostIs('reddit.com') || hostIs('twitter.com') || hostIs('x.com') ||
453
+ hostIs('figma.com')) {
454
+ return match;
455
+ }
456
+
457
+ return match.replace(url, `\n\n<div class="link-preview" data-url="${escapeAttr(url)}"></div>\n\n`);
458
+ } catch (e) {
459
+ // If URL parsing fails, just return the original match without processing
460
+ console.warn('Failed to parse URL for link preview:', url, e);
461
+ return match;
462
+ }
463
+ });
464
+
465
+ // Step 3.5: Restore table rows
466
+ tableRows.forEach((row, index) => {
467
+ processedContent = processedContent.replace(`__TABLE_ROW_${index}__`, row);
468
+ });
469
+
470
+ // Step 4: Restore markdown links
471
+ markdownLinks.forEach((link, index) => {
472
+ processedContent = processedContent.replace(`__MARKDOWN_LINK_${index}__`, link);
473
+ });
474
+
475
+ // Step 5: Restore code blocks (MUST be last to prevent link preview in code)
476
+ codeBlocks.forEach((block, index) => {
477
+ processedContent = processedContent.replace(`__CODE_BLOCK_${index}__`, block);
478
+ });
479
+
480
+ return processedContent;
481
+ };
482
+
483
+ /**
484
+ * Props for `<RichMarkdownRenderer>`. Aside from the four runtime knobs
485
+ * lifted from {@link RichMarkdownRuntime}, this is the same shape the hub's
486
+ * `SimpleMarkdownRenderer` ever had — every legacy call site can be moved
487
+ * over without other changes.
488
+ */
489
+ export interface RichMarkdownRendererProps extends Partial<RichMarkdownRuntime> {
490
+ content: string;
491
+ className?: string;
492
+ sectionIds?: Array<{ id: string; title: string; level: number }>;
493
+ /** Callback for internal navigation (called after the resolver returns) */
494
+ onInternalLinkClick?: (path: string, options?: { expandFolder?: boolean; fromInternalLink?: boolean }) => void;
495
+ /** List of broken links detected server-side */
496
+ brokenLinks?: string[];
497
+ /** Current documentation path for resolving relative links */
498
+ currentPath?: string;
499
+ /** Source for resolving internal links (default: 'openframe-docs'). Registry id from DOC_SOURCES. */
500
+ resolveSource?: string;
501
+ /** Path of the internal link-resolver endpoint. Default '/api/docs/resolve-link'. */
502
+ resolveLinkEndpointUrl?: string;
503
+ /** When the page already has an H1, render markdown `#` as `h2` (e.g. legal pages). */
504
+ demoteMarkdownH1ToH2?: boolean;
505
+ }
506
+
507
+ export const RichMarkdownRenderer: React.FC<RichMarkdownRendererProps> = ({
508
+ content,
509
+ className = "",
510
+ sectionIds,
511
+ onInternalLinkClick,
512
+ brokenLinks = [],
513
+ currentPath: propCurrentPath,
514
+ resolveSource = 'openframe-docs',
515
+ resolveLinkEndpointUrl = '/api/docs/resolve-link',
516
+ demoteMarkdownH1ToH2 = false,
517
+ // Runtime overrides; provider fills the defaults
518
+ redditProxyUrl,
519
+ twitterProxyUrl,
520
+ ogScraperUrl,
521
+ transformImageSrc,
522
+ }) => {
523
+ return (
524
+ <RichMarkdownRuntimeProvider
525
+ redditProxyUrl={redditProxyUrl}
526
+ twitterProxyUrl={twitterProxyUrl}
527
+ ogScraperUrl={ogScraperUrl}
528
+ transformImageSrc={transformImageSrc}
529
+ >
530
+ <RichMarkdownInner
531
+ content={content}
532
+ className={className}
533
+ sectionIds={sectionIds}
534
+ onInternalLinkClick={onInternalLinkClick}
535
+ brokenLinks={brokenLinks}
536
+ currentPath={propCurrentPath}
537
+ resolveSource={resolveSource}
538
+ resolveLinkEndpointUrl={resolveLinkEndpointUrl}
539
+ demoteMarkdownH1ToH2={demoteMarkdownH1ToH2}
540
+ />
541
+ </RichMarkdownRuntimeProvider>
542
+ );
543
+ };
544
+
545
+ interface InnerProps {
546
+ content: string;
547
+ className?: string;
548
+ sectionIds?: Array<{ id: string; title: string; level: number }>;
549
+ onInternalLinkClick?: (path: string, options?: { expandFolder?: boolean; fromInternalLink?: boolean }) => void;
550
+ brokenLinks?: string[];
551
+ currentPath?: string;
552
+ resolveSource: string;
553
+ resolveLinkEndpointUrl: string;
554
+ demoteMarkdownH1ToH2: boolean;
555
+ }
556
+
557
+ const RichMarkdownInner: React.FC<InnerProps> = ({
558
+ content,
559
+ className = "",
560
+ sectionIds,
561
+ onInternalLinkClick,
562
+ brokenLinks = [],
563
+ currentPath: propCurrentPath,
564
+ resolveSource,
565
+ resolveLinkEndpointUrl,
566
+ demoteMarkdownH1ToH2,
567
+ }) => {
568
+ const idCountsRef = useRef<Record<string, number>>({});
569
+ const { ogScraperUrl } = useRichMarkdownRuntime();
570
+
571
+ // The OG link-preview endpoint is `${apiBaseUrl}${ogEndpointPath}` —
572
+ // split the runtime URL once so we can pass both parts into the lib's
573
+ // existing `OGLinkPreview`. For full URLs (`https://hub.example.com/api/...`)
574
+ // we route through the cross-origin proxy; for path-only values we use
575
+ // them as the path with an empty base.
576
+ const { ogApiBaseUrl, ogEndpointPath } = useMemo(() => {
577
+ try {
578
+ const u = new URL(ogScraperUrl);
579
+ return {
580
+ ogApiBaseUrl: `${u.protocol}//${u.host}`,
581
+ ogEndpointPath: u.pathname,
582
+ };
583
+ } catch {
584
+ // Not a full URL — treat as a path on the same origin.
585
+ return { ogApiBaseUrl: '', ogEndpointPath: ogScraperUrl };
586
+ }
587
+ }, [ogScraperUrl]);
588
+
589
+ // Build section ID map synchronously so it's available during the first render
590
+ // (useEffect would run after render, causing heading ID mismatches)
591
+ const sectionIdMap = useMemo(() => {
592
+ const map = new Map<string, string>();
593
+ if (sectionIds) {
594
+ sectionIds.forEach(section => {
595
+ const cleanTitle = section.title
596
+ .replace(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '')
597
+ .trim()
598
+ .toLowerCase();
599
+ map.set(section.title.toLowerCase(), section.id);
600
+ map.set(cleanTitle, section.id);
601
+ map.set(section.title, section.id);
602
+ });
603
+ }
604
+ return map;
605
+ }, [sectionIds]);
606
+
607
+ // Fixed dark mode - no theme detection needed
608
+ const isDarkMode = true;
609
+
610
+ // Function to generate unique IDs for headings
611
+ const generateHeadingId = useCallback((text: string, level: number): string => {
612
+ // If we have sectionIds from backend and this is H1 or H2, use those
613
+ if (sectionIds && (level === 1 || level === 2)) {
614
+ // Try multiple variations for matching
615
+ const variations = [
616
+ text,
617
+ text.toLowerCase(),
618
+ text.replace(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '').trim(),
619
+ text.replace(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '').trim().toLowerCase()
620
+ ];
621
+
622
+ for (const variation of variations) {
623
+ const backendId = sectionIdMap.get(variation);
624
+ if (backendId) {
625
+ return backendId;
626
+ }
627
+ }
628
+ }
629
+
630
+ // Otherwise generate ID normally
631
+ const baseId = text
632
+ .replace(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '') // Remove emojis
633
+ .trim()
634
+ .toLowerCase()
635
+ .replace(/[^\w\s-]/g, '') // Remove remaining special chars
636
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
637
+ .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
638
+
639
+ // Fallback if baseId is empty after cleaning
640
+ const cleanId = baseId || `section-${Object.keys(idCountsRef.current).length + 1}`;
641
+
642
+ // Handle duplicate IDs
643
+ if (idCountsRef.current[cleanId]) {
644
+ idCountsRef.current[cleanId]++;
645
+ return `${cleanId}-${idCountsRef.current[cleanId]}`;
646
+ } else {
647
+ idCountsRef.current[cleanId] = 1;
648
+ return cleanId;
649
+ }
650
+ }, [sectionIds, sectionIdMap]);
651
+
652
+ // Process content early - before any conditional returns
653
+ const processedContent = processShortcodes(content);
654
+
655
+ // Memoize components to prevent React from losing event handlers
656
+ // This MUST be before any conditional returns to satisfy React's Rules of Hooks
657
+ const components = useMemo(() => ({
658
+ // Custom code renderer
659
+ code: ({ node, inline, className, children, ...props }: any) => {
660
+ const match = /language-(\w+)/.exec(className || '');
661
+ const language = match ? match[1] : '';
662
+
663
+ if (!inline && language === 'mermaid') {
664
+ return <MermaidDiagram chart={String(children).replace(/\n$/, '')} />;
665
+ }
666
+
667
+ if (!inline && language === 'youtube-embed') {
668
+ const videoId = String(children).replace(/\n$/, '').trim();
669
+ return <Video kind="youtube" url={videoId} />;
670
+ }
671
+
672
+ if (!inline && language === 'reddit-embed') {
673
+ const postUrl = String(children).replace(/\n$/, '').trim();
674
+ return <RedditEmbedClient url={postUrl} />;
675
+ }
676
+
677
+ if (!inline && language === 'tweet-embed') {
678
+ const tweetUrl = String(children).replace(/\n$/, '').trim();
679
+ return <TwitterEmbedClient url={tweetUrl} />;
680
+ }
681
+
682
+
683
+
684
+ if (!inline && language === 'link-preview') {
685
+ const url = String(children).replace(/\n$/, '').trim();
686
+ return (
687
+ <OGLinkPreview
688
+ url={url}
689
+ variant="compact"
690
+ enablePlaceholder={false}
691
+ apiBaseUrl={ogApiBaseUrl}
692
+ ogEndpointPath={ogEndpointPath}
693
+ />
694
+ );
695
+ }
696
+
697
+ if (!inline && language === 'figma-embed') {
698
+ const url = String(children).replace(/\n$/, '').trim();
699
+ return <FigmaEmbed url={url} height="70vh" />;
700
+ }
701
+
702
+ if (!inline && language === 'linkedin-embed') {
703
+ const postUrl = String(children).replace(/\n$/, '').trim();
704
+ return <LinkedInEmbedClient url={postUrl} />;
705
+ }
706
+
707
+
708
+
709
+ if (!inline && match) {
710
+ // Let rehype-highlight handle the syntax highlighting automatically
711
+ // Just provide the styled container
712
+ return (
713
+ <div className={`code-block-container border rounded-lg my-6 overflow-hidden ${
714
+ isDarkMode
715
+ ? 'bg-ods-card border-ods-border'
716
+ : 'bg-ods-bg-secondary border-ods-border'
717
+ }`}>
718
+ <div className={`code-header border-b px-4 py-2 ${
719
+ isDarkMode
720
+ ? 'bg-ods-card border-ods-border'
721
+ : 'bg-[#E5E7EB] border-[#D1D5DB]'
722
+ }`}>
723
+ <span className={`font-sans text-xs uppercase tracking-wide ${
724
+ isDarkMode ? 'text-ods-text-tertiary' : 'text-ods-text-tertiary'
725
+ }`}>
726
+ {language || 'code'}
727
+ </span>
728
+ </div>
729
+ <div className="p-4">
730
+ <pre className="overflow-x-auto">
731
+ <code
732
+ className={`language-${language} hljs`}
733
+ style={{
734
+ fontSize: '14px',
735
+ fontFamily: "'JetBrains Mono', 'SF Mono', Consolas, monospace",
736
+ background: 'transparent',
737
+ color: isDarkMode ? 'var(--ods-text-primary)' : 'var(--ods-text-primary)'
738
+ }}
739
+ {...props}
740
+ >
741
+ {children}
742
+ </code>
743
+ </pre>
744
+ </div>
745
+ </div>
746
+ );
747
+ }
748
+
749
+ return (
750
+ <code
751
+ className={`font-mono text-[0.9em] px-1.5 py-0.5 rounded border ${
752
+ isDarkMode
753
+ ? 'bg-ods-card text-ods-text-primary border-ods-border'
754
+ : 'bg-ods-bg-secondary text-ods-text-primary border-ods-border'
755
+ }`}
756
+ {...props}
757
+ >
758
+ {children}
759
+ </code>
760
+ );
761
+ },
762
+
763
+ // Custom HTML element renderer for our processed shortcodes
764
+ div: ({ node, className, children, ...props }: any) => {
765
+ if (className === 'youtube-embed') {
766
+ const videoId = props['data-video-id'];
767
+ return <Video kind="youtube" url={videoId} />;
768
+ }
769
+
770
+ if (className === 'reddit-embed') {
771
+ const postUrl = props['data-post-url'];
772
+ return <RedditEmbedClient url={postUrl} />;
773
+ }
774
+
775
+ if (className === 'tweet-embed') {
776
+ const tweetUrl = props['data-tweet-url'];
777
+ return <TwitterEmbedClient url={tweetUrl} />;
778
+ }
779
+
780
+
781
+
782
+ if (className === 'link-preview') {
783
+ const url = props['data-url'];
784
+
785
+ // Validate URL before rendering component
786
+ if (!url || typeof url !== 'string') {
787
+ console.warn('Invalid URL for link preview:', url);
788
+ return <div className="text-ods-text-secondary text-sm">Invalid link</div>;
789
+ }
790
+
791
+ try {
792
+ new URL(url); // Validate URL format
793
+ // Wrap in error boundary to catch any runtime errors
794
+ return (
795
+ <OGLinkErrorBoundary fallback={<div className="text-ods-text-secondary text-sm">Link preview unavailable</div>}>
796
+ <OGLinkPreview
797
+ url={url}
798
+ variant="compact"
799
+ enablePlaceholder={false}
800
+ apiBaseUrl={ogApiBaseUrl}
801
+ ogEndpointPath={ogEndpointPath}
802
+ />
803
+ </OGLinkErrorBoundary>
804
+ );
805
+ } catch (e) {
806
+ console.warn('Malformed URL for link preview:', url, e);
807
+ return <div className="text-ods-text-secondary text-sm">Malformed URL: {url}</div>;
808
+ }
809
+ }
810
+
811
+ if (className === 'figma-embed') {
812
+ return <FigmaEmbed url={props['data-figma-url']} height="70vh" />;
813
+ }
814
+
815
+ if (className === 'linkedin-embed') {
816
+ return <LinkedInEmbedClient url={props['data-post-url']} />;
817
+ }
818
+
819
+ return <div className={className} {...props}>{children}</div>;
820
+ },
821
+
822
+ // Style blockquotes
823
+ blockquote: ({ children }: any) => (
824
+ <blockquote className={`border-l-4 border-[#FFC008] ml-0 pl-6 my-8 py-4 rounded-r-lg ${
825
+ isDarkMode
826
+ ? 'bg-[#1F1F1F]'
827
+ : 'bg-[#F8F9FA]'
828
+ }`}>
829
+ <div className={`font-sans text-[1.125em] leading-relaxed ${
830
+ isDarkMode
831
+ ? 'text-ods-text-secondary'
832
+ : 'text-ods-text-primary'
833
+ }`}>
834
+ {children}
835
+ </div>
836
+ </blockquote>
837
+ ),
838
+
839
+ // Style headings - SIMPLIFIED: No complex logic in react-markdown
840
+ h1: ({ children }: any) => {
841
+ // Extract text from children (could be string or React elements)
842
+ const extractText = (node: any): string => {
843
+ if (typeof node === 'string') return node;
844
+ if (Array.isArray(node)) return node.map(extractText).join('');
845
+ if (node?.props?.children) return extractText(node.props.children);
846
+ return '';
847
+ };
848
+
849
+ const text = extractText(children);
850
+ const level = demoteMarkdownH1ToH2 ? 2 : 1;
851
+ const id = generateHeadingId(text, level);
852
+
853
+ const h1VisualClassName = `font-sans font-bold text-[32px] md:text-[40px] lg:text-[48px] leading-[1.25] mt-8 mb-4 first:mt-0 ${
854
+ isDarkMode ? 'text-ods-text-primary' : 'text-[#111827]'
855
+ }`;
856
+
857
+ if (demoteMarkdownH1ToH2) {
858
+ return (
859
+ <h2 id={id} className={h1VisualClassName}>
860
+ {children}
861
+ </h2>
862
+ );
863
+ }
864
+
865
+ return (
866
+ <h1 id={id} className={h1VisualClassName}>
867
+ {children}
868
+ </h1>
869
+ );
870
+ },
871
+ h2: ({ children }: any) => {
872
+ // Extract text from children (could be string or React elements)
873
+ const extractText = (node: any): string => {
874
+ if (typeof node === 'string') return node;
875
+ if (Array.isArray(node)) return node.map(extractText).join('');
876
+ if (node?.props?.children) return extractText(node.props.children);
877
+ return '';
878
+ };
879
+
880
+ const text = extractText(children);
881
+ const id = generateHeadingId(text, 2);
882
+
883
+ return (
884
+ <h2
885
+ id={id}
886
+ className={`font-sans font-semibold text-[28px] md:text-[32px] mt-8 mb-4 pb-2 border-b ${
887
+ isDarkMode
888
+ ? 'text-ods-text-primary border-ods-border'
889
+ : 'text-[#111827] border-[#E5E7EB]'
890
+ }`}
891
+ >
892
+ {children}
893
+ </h2>
894
+ );
895
+ },
896
+ h3: ({ children }: any) => {
897
+ // Extract text from children (could be string or React elements)
898
+ const extractText = (node: any): string => {
899
+ if (typeof node === 'string') return node;
900
+ if (Array.isArray(node)) return node.map(extractText).join('');
901
+ if (node?.props?.children) return extractText(node.props.children);
902
+ return '';
903
+ };
904
+
905
+ const text = extractText(children);
906
+ const id = generateHeadingId(text, 3);
907
+
908
+ return (
909
+ <h3
910
+ id={id}
911
+ className={`font-sans font-semibold text-[24px] md:text-[28px] mt-6 mb-3 ${
912
+ isDarkMode ? 'text-ods-text-primary' : 'text-[#111827]'
913
+ }`}
914
+ >
915
+ {children}
916
+ </h3>
917
+ );
918
+ },
919
+ h4: ({ children }: any) => {
920
+ // Extract text from children (could be string or React elements)
921
+ const extractText = (node: any): string => {
922
+ if (typeof node === 'string') return node;
923
+ if (Array.isArray(node)) return node.map(extractText).join('');
924
+ if (node?.props?.children) return extractText(node.props.children);
925
+ return '';
926
+ };
927
+
928
+ const text = extractText(children);
929
+ const id = generateHeadingId(text, 4);
930
+
931
+ return (
932
+ <h4
933
+ id={id}
934
+ className={`font-sans font-semibold text-[20px] md:text-[22px] mt-4 mb-2 ${
935
+ isDarkMode ? 'text-ods-text-primary' : 'text-[#111827]'
936
+ }`}
937
+ >
938
+ {children}
939
+ </h4>
940
+ );
941
+ },
942
+ h5: ({ children }: any) => {
943
+ // Extract text from children (could be string or React elements)
944
+ const extractText = (node: any): string => {
945
+ if (typeof node === 'string') return node;
946
+ if (Array.isArray(node)) return node.map(extractText).join('');
947
+ if (node?.props?.children) return extractText(node.props.children);
948
+ return '';
949
+ };
950
+
951
+ const text = extractText(children);
952
+ const id = generateHeadingId(text, 5);
953
+
954
+ return (
955
+ <h5
956
+ id={id}
957
+ className={`font-sans font-semibold text-[18px] md:text-[20px] mt-3 mb-2 ${
958
+ isDarkMode ? 'text-ods-text-primary' : 'text-[#111827]'
959
+ }`}
960
+ >
961
+ {children}
962
+ </h5>
963
+ );
964
+ },
965
+ h6: ({ children }: any) => {
966
+ // Extract text from children (could be string or React elements)
967
+ const extractText = (node: any): string => {
968
+ if (typeof node === 'string') return node;
969
+ if (Array.isArray(node)) return node.map(extractText).join('');
970
+ if (node?.props?.children) return extractText(node.props.children);
971
+ return '';
972
+ };
973
+
974
+ const text = extractText(children);
975
+ const id = generateHeadingId(text, 6);
976
+
977
+ return (
978
+ <h6
979
+ id={id}
980
+ className={`font-sans font-semibold text-[16px] md:text-[18px] mt-3 mb-1 ${
981
+ isDarkMode ? 'text-ods-text-primary' : 'text-[#111827]'
982
+ }`}
983
+ >
984
+ {children}
985
+ </h6>
986
+ );
987
+ },
988
+
989
+ // Style paragraphs
990
+ p: ({ children }: any) => (
991
+ <p className={`font-sans text-[16px] md:text-[18px] lg:text-[20px] leading-[1.6] my-4 ${
992
+ isDarkMode ? 'text-ods-text-primary' : 'text-[#374151]'
993
+ }`}>
994
+ {children}
995
+ </p>
996
+ ),
997
+
998
+ // Style links - use SPAN for internal docs to avoid browser navigation
999
+ a: ({ href, children, className }: any) => {
1000
+ // Check if this link is broken
1001
+ const isBroken = brokenLinks.includes(href);
1002
+
1003
+ // Internal doc link: only DocumentationSection (knowledge-base) passes currentPath.
1004
+ // Using prop instead of window.location keeps server/client output identical (no hydration mismatch).
1005
+ const isInternalDocLink =
1006
+ (propCurrentPath !== undefined && propCurrentPath !== null) &&
1007
+ href &&
1008
+ !href.startsWith('http') &&
1009
+ !href.startsWith('#');
1010
+
1011
+ // For broken links, show as non-clickable but keep original color
1012
+ if (isBroken) {
1013
+ return (
1014
+ <span className="text-ods-accent cursor-not-allowed">
1015
+ {children}
1016
+ <sup className="ml-1 text-xs font-bold text-red-500">[BROKEN]</sup>
1017
+ </span>
1018
+ );
1019
+ }
1020
+
1021
+ // For internal doc links, use span to avoid ANY default navigation
1022
+ if (isInternalDocLink) {
1023
+ const currentPath = propCurrentPath ?? '';
1024
+ return (
1025
+ <span
1026
+ className="text-ods-accent no-underline relative transition-colors duration-200 hover:after:w-full after:content-[''] after:absolute after:w-0 after:h-0.5 after:-bottom-0.5 after:left-0 after:bg-ods-accent after:transition-all after:duration-300 cursor-pointer"
1027
+ onClick={async (e) => {
1028
+ e.preventDefault();
1029
+ e.stopPropagation();
1030
+
1031
+
1032
+ if (!onInternalLinkClick) {
1033
+ console.error('🔗 No onInternalLinkClick callback provided!');
1034
+ return;
1035
+ }
1036
+
1037
+ try {
1038
+ // Call server to resolve the link
1039
+ const response = await fetch(resolveLinkEndpointUrl, {
1040
+ method: 'POST',
1041
+ headers: { 'Content-Type': 'application/json' },
1042
+ body: JSON.stringify({ link: href, currentPath, source: resolveSource })
1043
+ });
1044
+
1045
+ const result = await response.json();
1046
+
1047
+ if (result.type === 'folder-no-readme' && result.action === 'expand_folder') {
1048
+ // Folder without README - expand it and scroll to sidebar
1049
+ onInternalLinkClick(result.resolvedPath, { expandFolder: true, fromInternalLink: true });
1050
+ } else if (result.type === 'not-found') {
1051
+ // Link points to non-existent path - this shouldn't happen since broken links are pre-detected
1052
+ console.warn(`🔗 Link points to non-existent path: ${result.resolvedPath}`);
1053
+ // Don't navigate
1054
+ return;
1055
+ } else if (result.success && result.resolvedPath) {
1056
+ // Normal navigation - ALWAYS pass fromInternalLink for consistent behavior
1057
+ onInternalLinkClick(result.resolvedPath, { fromInternalLink: true });
1058
+ } else {
1059
+ console.error('Failed to resolve link:', result.error || result.message);
1060
+ }
1061
+ } catch (error) {
1062
+ console.error('Error resolving link:', error);
1063
+ }
1064
+ }}
1065
+ role="link"
1066
+ tabIndex={0}
1067
+ onKeyDown={(e) => {
1068
+ if (e.key === 'Enter' || e.key === ' ') {
1069
+ (e.currentTarget as HTMLElement).click();
1070
+ }
1071
+ }}
1072
+ >
1073
+ {children}
1074
+ </span>
1075
+ );
1076
+ }
1077
+
1078
+ // Regular external links and anchors
1079
+ return (
1080
+ <a
1081
+ href={href}
1082
+ className={`text-ods-accent no-underline relative transition-colors duration-200 hover:after:w-full after:content-[''] after:absolute after:w-0 after:h-0.5 after:-bottom-0.5 after:left-0 after:bg-ods-accent after:transition-all after:duration-300 ${className || ''}`}
1083
+ target={href?.startsWith('http') ? '_blank' : undefined}
1084
+ rel={href?.startsWith('http') ? 'noopener noreferrer' : undefined}
1085
+ >
1086
+ {children}
1087
+ </a>
1088
+ );
1089
+ },
1090
+
1091
+ // In-article images. Used everywhere RichMarkdownRenderer is (blog, case studies,
1092
+ // interviews, docs, legal, admin preview). Markdown images have unknown intrinsic
1093
+ // dimensions, so <MarkdownImage> reads `transformImageSrc` from the runtime context
1094
+ // to optimize Supabase URLs (hub) or fall through identity (embedders).
1095
+ // Guard against empty/undefined sources (e.g. `![]()` in markdown).
1096
+ img: ({ src, alt }: any) => {
1097
+ if (!src || typeof src !== 'string' || src.trim() === '') {
1098
+ return null;
1099
+ }
1100
+ return <MarkdownImage src={src.trim()} alt={alt} />;
1101
+ },
1102
+
1103
+ // Style lists
1104
+ ul: ({ children }: any) => (
1105
+ <ul className={`list-disc list-outside my-4 ml-8 space-y-2 ${
1106
+ isDarkMode ? 'text-ods-text-primary' : 'text-[#374151]'
1107
+ }`}>
1108
+ {children}
1109
+ </ul>
1110
+ ),
1111
+ ol: ({ children }: any) => (
1112
+ <ol className={`list-decimal list-outside my-4 ml-8 space-y-2 ${
1113
+ isDarkMode ? 'text-ods-text-primary' : 'text-[#374151]'
1114
+ }`}>
1115
+ {children}
1116
+ </ol>
1117
+ ),
1118
+ li: ({ children }: any) => (
1119
+ <li className="text-[16px] md:text-[18px] leading-relaxed pl-2">
1120
+ {children}
1121
+ </li>
1122
+ ),
1123
+
1124
+ // Style tables
1125
+ table: ({ children }: any) => (
1126
+ <div className="table-container my-6 overflow-x-auto">
1127
+ <div className={`min-w-full border rounded-lg ${
1128
+ isDarkMode
1129
+ ? 'border-ods-border bg-ods-card'
1130
+ : 'border-[#E5E7EB] bg-white'
1131
+ }`}>
1132
+ <table className="w-full table-fixed md:table-auto">
1133
+ {children}
1134
+ </table>
1135
+ </div>
1136
+ </div>
1137
+ ),
1138
+ thead: ({ children }: any) => (
1139
+ <thead className={isDarkMode ? 'bg-ods-bg-secondary' : 'bg-ods-bg-secondary'}>
1140
+ {children}
1141
+ </thead>
1142
+ ),
1143
+ th: ({ children }: any) => (
1144
+ <th className={`px-2 md:px-4 py-3 text-left text-xs md:text-sm font-semibold text-ods-accent border-r last:border-r-0 break-words ${
1145
+ isDarkMode ? 'border-ods-border' : 'border-[#E5E7EB]'
1146
+ }`}>
1147
+ {children}
1148
+ </th>
1149
+ ),
1150
+ td: ({ children }: any) => (
1151
+ <td className={`px-2 md:px-4 py-3 text-xs md:text-sm border-r last:border-r-0 border-b break-words whitespace-normal ${
1152
+ isDarkMode
1153
+ ? 'text-ods-text-primary border-ods-border'
1154
+ : 'text-[#374151] border-[#E5E7EB]'
1155
+ }`}>
1156
+ {children}
1157
+ </td>
1158
+ ),
1159
+
1160
+ // Style horizontal rules
1161
+ hr: () => (
1162
+ <hr className={`border-0 border-t my-8 ${
1163
+ isDarkMode ? 'border-ods-border' : 'border-[#E5E7EB]'
1164
+ }`} />
1165
+ ),
1166
+ }), [
1167
+ isDarkMode,
1168
+ generateHeadingId,
1169
+ onInternalLinkClick,
1170
+ brokenLinks,
1171
+ propCurrentPath,
1172
+ demoteMarkdownH1ToH2,
1173
+ resolveSource,
1174
+ resolveLinkEndpointUrl,
1175
+ ogApiBaseUrl,
1176
+ ogEndpointPath,
1177
+ ]);
1178
+
1179
+ // Render markdown on both server and client so article content is in initial HTML (SSR).
1180
+ return (
1181
+ <div className={`simple-markdown-renderer ${className}`}>
1182
+ {/* Inject Mermaid styles */}
1183
+ <style dangerouslySetInnerHTML={{ __html: mermaidStyles }} />
1184
+ <div className="content-wrapper max-w-none">
1185
+ <article className="prose prose-lg max-w-none">
1186
+ <ReactMarkdown
1187
+ remarkPlugins={[remarkGfm, remarkBreaks]}
1188
+ rehypePlugins={[
1189
+ rehypeRaw,
1190
+ [rehypeHighlight, {
1191
+ detect: true,
1192
+ ignoreMissing: true
1193
+ }]
1194
+ ]}
1195
+ components={components}
1196
+ >
1197
+ {processedContent}
1198
+ </ReactMarkdown>
1199
+ </article>
1200
+ </div>
1201
+ </div>
1202
+ );
1203
+ };