@flamingo-stack/openframe-frontend-core 0.0.296-snapshot.20260621021605 → 0.0.296
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.
- package/README.md +0 -9
- package/dist/chunk-26PKDALD.js +2379 -0
- package/dist/chunk-26PKDALD.js.map +1 -0
- package/dist/chunk-3MCHAFHB.js +89 -0
- package/dist/chunk-3MCHAFHB.js.map +1 -0
- package/dist/{chunk-PI4WSYQV.js → chunk-3ZXUQQL4.js} +2 -2
- package/dist/{chunk-WMSTJAZT.cjs → chunk-5E2HOSSH.cjs} +51 -913
- package/dist/chunk-5E2HOSSH.cjs.map +1 -0
- package/dist/{chunk-IL47XWV5.js → chunk-5P3B2LZW.js} +14 -8
- package/dist/{chunk-IL47XWV5.js.map → chunk-5P3B2LZW.js.map} +1 -1
- package/dist/chunk-66AANIOC.cjs +619 -0
- package/dist/chunk-66AANIOC.cjs.map +1 -0
- package/dist/{chunk-AD6C23QY.js → chunk-6GCI7JOE.js} +7 -8
- package/dist/{chunk-AD6C23QY.js.map → chunk-6GCI7JOE.js.map} +1 -1
- package/dist/chunk-6JINAOI7.cjs +311 -0
- package/dist/chunk-6JINAOI7.cjs.map +1 -0
- package/dist/{chunk-2QG57XOJ.js → chunk-7RIYT7ZH.js} +205 -1067
- package/dist/chunk-7RIYT7ZH.js.map +1 -0
- package/dist/{chunk-L6PSSIUQ.cjs → chunk-AQOWFSMB.cjs} +1 -1
- package/dist/chunk-AQOWFSMB.cjs.map +1 -0
- package/dist/chunk-BOCFIKYS.cjs +3009 -0
- package/dist/chunk-BOCFIKYS.cjs.map +1 -0
- package/dist/{chunk-54KNMC2R.cjs → chunk-D3LEFMOA.cjs} +3 -3
- package/dist/{chunk-54KNMC2R.cjs.map → chunk-D3LEFMOA.cjs.map} +1 -1
- package/dist/chunk-D652TJBQ.js +3009 -0
- package/dist/chunk-D652TJBQ.js.map +1 -0
- package/dist/{chunk-PWQUAVA3.js → chunk-E4XABBSU.js} +98 -338
- package/dist/chunk-E4XABBSU.js.map +1 -0
- package/dist/{chunk-JALO4TAZ.js → chunk-EL6QLAWX.js} +55 -357
- package/dist/chunk-EL6QLAWX.js.map +1 -0
- package/dist/{chunk-6C526VNN.cjs → chunk-EYEW6PTA.cjs} +118 -358
- package/dist/chunk-EYEW6PTA.cjs.map +1 -0
- package/dist/chunk-FQJK446R.js +1606 -0
- package/dist/chunk-FQJK446R.js.map +1 -0
- package/dist/{chunk-4PSQS3SW.cjs → chunk-GLLDTKZK.cjs} +9 -7
- package/dist/chunk-GLLDTKZK.cjs.map +1 -0
- package/dist/{chunk-FQOTC3UU.cjs → chunk-IE6OU3WQ.cjs} +16 -318
- package/dist/chunk-IE6OU3WQ.cjs.map +1 -0
- package/dist/chunk-J54Z3OCR.cjs +1606 -0
- package/dist/chunk-J54Z3OCR.cjs.map +1 -0
- package/dist/{chunk-PC746XCO.js → chunk-K2PFPBMF.js} +5563 -15048
- package/dist/chunk-K2PFPBMF.js.map +1 -0
- package/dist/chunk-KXCRGTRN.cjs +2379 -0
- package/dist/chunk-KXCRGTRN.cjs.map +1 -0
- package/dist/{chunk-IZ7JSBFP.js → chunk-LCNMR277.js} +1 -1
- package/dist/chunk-LCNMR277.js.map +1 -0
- package/dist/chunk-LFGGF7OT.cjs +449 -0
- package/dist/chunk-LFGGF7OT.cjs.map +1 -0
- package/dist/chunk-M2OCXTNT.js +311 -0
- package/dist/chunk-M2OCXTNT.js.map +1 -0
- package/dist/{chunk-L7ULJKG7.js → chunk-MBFWU2EM.js} +10 -6
- package/dist/{chunk-L7ULJKG7.js.map → chunk-MBFWU2EM.js.map} +1 -1
- package/dist/chunk-ME4EVDFP.js +619 -0
- package/dist/chunk-ME4EVDFP.js.map +1 -0
- package/dist/chunk-OQ6X7ZOC.js +449 -0
- package/dist/chunk-OQ6X7ZOC.js.map +1 -0
- package/dist/{chunk-4TLE6VLU.js → chunk-OY7OF7E7.js} +24 -30
- package/dist/chunk-OY7OF7E7.js.map +1 -0
- package/dist/chunk-POKKCWKF.js +354 -0
- package/dist/chunk-POKKCWKF.js.map +1 -0
- package/dist/{chunk-GUTS7HGA.cjs → chunk-QHIXS3W2.cjs} +2514 -11999
- package/dist/chunk-QHIXS3W2.cjs.map +1 -0
- package/dist/chunk-TFSYSWPS.cjs +89 -0
- package/dist/chunk-TFSYSWPS.cjs.map +1 -0
- package/dist/{chunk-53FUMSZ5.cjs → chunk-W6M2FLLT.cjs} +46 -40
- package/dist/chunk-W6M2FLLT.cjs.map +1 -0
- package/dist/{chunk-3JIQVE7T.js → chunk-WHMATDVP.js} +15 -9
- package/dist/{chunk-3JIQVE7T.js.map → chunk-WHMATDVP.js.map} +1 -1
- package/dist/{chunk-YBYI62OE.cjs → chunk-X647HY3F.cjs} +37 -33
- package/dist/chunk-X647HY3F.cjs.map +1 -0
- package/dist/{chunk-UNVE2SDJ.cjs → chunk-X6BV7MB7.cjs} +31 -37
- package/dist/chunk-X6BV7MB7.cjs.map +1 -0
- package/dist/{chunk-7OVGB2DQ.cjs → chunk-XREEV72C.cjs} +25 -19
- package/dist/chunk-XREEV72C.cjs.map +1 -0
- package/dist/chunk-YETA25JW.cjs +354 -0
- package/dist/chunk-YETA25JW.cjs.map +1 -0
- package/dist/{chunk-FCDQNTDG.cjs → chunk-YIGPRLQY.cjs} +20 -21
- package/dist/chunk-YIGPRLQY.cjs.map +1 -0
- package/dist/{chunk-X4DOXQRT.js → chunk-ZP4AVIZP.js} +6 -4
- package/dist/{chunk-X4DOXQRT.js.map → chunk-ZP4AVIZP.js.map} +1 -1
- package/dist/components/chat/index.cjs +18 -8
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +85 -75
- package/dist/components/contact/index.cjs +15 -8
- package/dist/components/contact/index.cjs.map +1 -1
- package/dist/components/contact/index.js +14 -7
- package/dist/components/docs/doc-viewer.d.ts +2 -39
- package/dist/components/docs/doc-viewer.d.ts.map +1 -1
- package/dist/components/docs/index.cjs +9 -17
- package/dist/components/docs/index.cjs.map +1 -1
- package/dist/components/docs/index.d.ts +0 -4
- package/dist/components/docs/index.d.ts.map +1 -1
- package/dist/components/docs/index.js +8 -16
- package/dist/components/docs/use-document-tree.d.ts.map +1 -1
- package/dist/components/embeds/embed-iframe.d.ts.map +1 -1
- package/dist/components/embeds/index.cjs +15 -38
- package/dist/components/embeds/index.cjs.map +1 -1
- package/dist/components/embeds/index.d.ts +0 -8
- package/dist/components/embeds/index.d.ts.map +1 -1
- package/dist/components/embeds/index.js +17 -40
- package/dist/components/faq/index.cjs +16 -9
- package/dist/components/faq/index.cjs.map +1 -1
- package/dist/components/faq/index.js +15 -8
- package/dist/components/features/index.cjs +16 -8
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +32 -24
- package/dist/components/index.cjs +452 -257
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +976 -781
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/page-layout.d.ts +1 -10
- package/dist/components/layout/page-layout.d.ts.map +1 -1
- package/dist/components/layout/title-block.d.ts +1 -17
- package/dist/components/layout/title-block.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +15 -7
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.js +17 -9
- package/dist/components/onboarding-guides/index.cjs +36 -35
- package/dist/components/onboarding-guides/index.cjs.map +1 -1
- package/dist/components/onboarding-guides/index.js +14 -13
- package/dist/components/onboarding-guides/index.js.map +1 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +1 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +16 -9
- package/dist/components/related-content/index.cjs.map +1 -1
- package/dist/components/related-content/index.js +15 -8
- package/dist/components/shared/dev-section/dev-section-page.d.ts +0 -9
- package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
- package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -1
- package/dist/components/shared/dev-section/index.d.ts +1 -1
- package/dist/components/shared/dev-section/index.d.ts.map +1 -1
- package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -1
- package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +112 -100
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +32 -20
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +52 -50
- package/dist/components/ui/file-manager/index.cjs.map +1 -1
- package/dist/components/ui/file-manager/index.js +6 -4
- package/dist/components/ui/file-manager/index.js.map +1 -1
- package/dist/components/ui/index.cjs +19 -13
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +0 -2
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +139 -133
- package/dist/components/ui/release-changelog-section.d.ts +2 -6
- package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
- package/dist/components/ui/simple-markdown-renderer.d.ts +8 -2
- package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
- package/dist/contexts/chat-runtime-context.d.ts +0 -14
- package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
- package/dist/contexts/index.cjs +3 -3
- package/dist/contexts/index.js +5 -5
- package/dist/embed-shims/index.cjs +3 -3
- package/dist/embed-shims/index.cjs.map +1 -1
- package/dist/embed-shims/index.js +4 -4
- package/dist/hooks/index.cjs +9 -4
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +11 -6
- package/dist/index.cjs +20 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +364 -358
- package/dist/types/doc-source.d.ts +1 -31
- package/dist/types/doc-source.d.ts.map +1 -1
- package/dist/utils/index.cjs +0 -4
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -4
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -7
- package/src/components/chat/embeddable-chat.tsx +1 -1
- package/src/components/docs/doc-viewer.tsx +19 -111
- package/src/components/docs/index.ts +0 -17
- package/src/components/docs/use-document-tree.ts +0 -21
- package/src/components/embeds/embed-iframe.tsx +9 -7
- package/src/components/embeds/index.ts +0 -30
- package/src/components/embeds/og-link-preview.tsx +13 -13
- package/src/components/layout/page-layout.tsx +1 -14
- package/src/components/layout/title-block.tsx +62 -40
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +3 -3
- package/src/components/shared/dev-section/dev-section-page.tsx +1 -9
- package/src/components/shared/dev-section/dev-section-view.tsx +9 -14
- package/src/components/shared/dev-section/index.ts +1 -1
- package/src/components/shared/doc-search/use-doc-search.ts +3 -7
- package/src/components/shared/legal-document/legal-document-page.tsx +2 -2
- package/src/components/shared/product-release/release-detail-page.tsx +4 -6
- package/src/components/ui/index.ts +0 -2
- package/src/components/ui/release-changelog-section.tsx +2 -7
- package/src/components/ui/simple-markdown-renderer.tsx +11 -7
- package/src/contexts/chat-runtime-context.tsx +0 -14
- package/src/types/doc-source.ts +1 -33
- package/src/utils/index.ts +0 -1
- package/dist/chunk-2QG57XOJ.js.map +0 -1
- package/dist/chunk-4PSQS3SW.cjs.map +0 -1
- package/dist/chunk-4TLE6VLU.js.map +0 -1
- package/dist/chunk-53FUMSZ5.cjs.map +0 -1
- package/dist/chunk-6C526VNN.cjs.map +0 -1
- package/dist/chunk-7OVGB2DQ.cjs.map +0 -1
- package/dist/chunk-F5OB2YAL.cjs +0 -144
- package/dist/chunk-F5OB2YAL.cjs.map +0 -1
- package/dist/chunk-FBWXMMRB.cjs +0 -2
- package/dist/chunk-FBWXMMRB.cjs.map +0 -1
- package/dist/chunk-FCDQNTDG.cjs.map +0 -1
- package/dist/chunk-FQOTC3UU.cjs.map +0 -1
- package/dist/chunk-GUTS7HGA.cjs.map +0 -1
- package/dist/chunk-GZ4C3XW6.js +0 -2
- package/dist/chunk-GZ4C3XW6.js.map +0 -1
- package/dist/chunk-IZ7JSBFP.js.map +0 -1
- package/dist/chunk-JALO4TAZ.js.map +0 -1
- package/dist/chunk-L6PSSIUQ.cjs.map +0 -1
- package/dist/chunk-PC746XCO.js.map +0 -1
- package/dist/chunk-PWQUAVA3.js.map +0 -1
- package/dist/chunk-SA2WPJVO.js +0 -144
- package/dist/chunk-SA2WPJVO.js.map +0 -1
- package/dist/chunk-UNVE2SDJ.cjs.map +0 -1
- package/dist/chunk-WMSTJAZT.cjs.map +0 -1
- package/dist/chunk-YBYI62OE.cjs.map +0 -1
- package/dist/components/case-studies/index.cjs +0 -126
- package/dist/components/case-studies/index.cjs.map +0 -1
- package/dist/components/case-studies/index.d.ts +0 -2
- package/dist/components/case-studies/index.d.ts.map +0 -1
- package/dist/components/case-studies/index.js +0 -126
- package/dist/components/case-studies/index.js.map +0 -1
- package/dist/components/case-studies/share-experience-section.d.ts +0 -48
- package/dist/components/case-studies/share-experience-section.d.ts.map +0 -1
- package/dist/components/docs/docs-hub-page.d.ts +0 -46
- package/dist/components/docs/docs-hub-page.d.ts.map +0 -1
- package/dist/components/docs/skeletons.d.ts +0 -32
- package/dist/components/docs/skeletons.d.ts.map +0 -1
- package/dist/components/docs/use-docs-resolve-link.d.ts +0 -20
- package/dist/components/docs/use-docs-resolve-link.d.ts.map +0 -1
- package/dist/components/embeds/embed-container.d.ts +0 -37
- package/dist/components/embeds/embed-container.d.ts.map +0 -1
- package/dist/components/embeds/file-download-card.d.ts +0 -18
- package/dist/components/embeds/file-download-card.d.ts.map +0 -1
- package/dist/components/embeds/linkedin-embed-client.d.ts +0 -8
- package/dist/components/embeds/linkedin-embed-client.d.ts.map +0 -1
- package/dist/components/embeds/markdown-image.d.ts +0 -5
- package/dist/components/embeds/markdown-image.d.ts.map +0 -1
- package/dist/components/embeds/reddit-embed-client.d.ts +0 -7
- package/dist/components/embeds/reddit-embed-client.d.ts.map +0 -1
- package/dist/components/embeds/rich-markdown-runtime.d.ts +0 -46
- package/dist/components/embeds/rich-markdown-runtime.d.ts.map +0 -1
- package/dist/components/embeds/twitter-embed-client.d.ts +0 -8
- package/dist/components/embeds/twitter-embed-client.d.ts.map +0 -1
- package/dist/components/layout/page-header.d.ts +0 -78
- package/dist/components/layout/page-header.d.ts.map +0 -1
- package/dist/components/layout/page-with-header.d.ts +0 -67
- package/dist/components/layout/page-with-header.d.ts.map +0 -1
- package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
- package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
- package/dist/utils/page-header-constants.d.ts +0 -15
- package/dist/utils/page-header-constants.d.ts.map +0 -1
- package/dist/utils/social-embed-cache.d.ts +0 -29
- package/dist/utils/social-embed-cache.d.ts.map +0 -1
- package/src/components/case-studies/index.ts +0 -4
- package/src/components/case-studies/share-experience-section.tsx +0 -185
- package/src/components/docs/docs-hub-page.tsx +0 -149
- package/src/components/docs/skeletons.tsx +0 -138
- package/src/components/docs/use-docs-resolve-link.ts +0 -52
- package/src/components/embeds/embed-container.tsx +0 -80
- package/src/components/embeds/file-download-card.tsx +0 -54
- package/src/components/embeds/linkedin-embed-client.tsx +0 -100
- package/src/components/embeds/markdown-image.tsx +0 -88
- package/src/components/embeds/reddit-embed-client.tsx +0 -550
- package/src/components/embeds/rich-markdown-runtime.tsx +0 -79
- package/src/components/embeds/twitter-embed-client.tsx +0 -308
- package/src/components/layout/page-header.tsx +0 -182
- package/src/components/layout/page-with-header.tsx +0 -110
- package/src/components/ui/rich-markdown-renderer.tsx +0 -1203
- package/src/utils/page-header-constants.ts +0 -15
- package/src/utils/social-embed-cache.ts +0 -391
- /package/dist/{chunk-PI4WSYQV.js.map → chunk-3ZXUQQL4.js.map} +0 -0
|
@@ -1,1203 +0,0 @@
|
|
|
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, '&')
|
|
299
|
-
.replace(/"/g, '"')
|
|
300
|
-
.replace(/</g, '<')
|
|
301
|
-
.replace(/>/g, '>');
|
|
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: [](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: [](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
|
-
* [](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
|
-
};
|