219 lines
5.1 KiB
TypeScript
219 lines
5.1 KiB
TypeScript
// src/renderer/components/meeting/MarkdownRenderer.tsx
|
|
// Phase 14.5: react-markdown + remark-gfm 래퍼 — d3roPalette 기반 스타일
|
|
|
|
import ReactMarkdown from 'react-markdown'
|
|
import remarkGfm from 'remark-gfm'
|
|
import { Box } from '@mui/material'
|
|
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
|
import type { Components } from 'react-markdown'
|
|
|
|
interface MarkdownRendererProps {
|
|
content: string
|
|
}
|
|
|
|
/**
|
|
* 링크는 앱 창 안에서 이동하지 않는다 — 메인 창이 원격 페이지로 바뀌면 preload API가 그 페이지에 노출된다.
|
|
* 클릭은 main의 openExternal(허용 scheme만)로 넘겨 OS 브라우저에서 연다.
|
|
*/
|
|
function openLinkExternally(event: React.MouseEvent<HTMLAnchorElement>, href: string | undefined): void {
|
|
event.preventDefault()
|
|
if (!href) return
|
|
void window.electronAPI.system.openExternal({ url: href })
|
|
}
|
|
|
|
const components: Components = {
|
|
a: ({ children, href }) => (
|
|
<Box
|
|
component="a"
|
|
href={href}
|
|
rel="noopener noreferrer"
|
|
onClick={(event: React.MouseEvent<HTMLAnchorElement>) => openLinkExternally(event, href)}
|
|
sx={{ color: d3roPalette.accent.main, textDecoration: 'underline', cursor: 'pointer' }}
|
|
>
|
|
{children}
|
|
</Box>
|
|
),
|
|
h1: ({ children }) => (
|
|
<Box
|
|
component="h1"
|
|
sx={{
|
|
fontSize: d3roTypo.value.size,
|
|
fontWeight: 600,
|
|
color: d3roPalette.accent.main,
|
|
mt: 2,
|
|
mb: 1,
|
|
borderBottom: `1px solid ${d3roPalette.border.default}`,
|
|
pb: 0.5,
|
|
}}
|
|
>
|
|
{children}
|
|
</Box>
|
|
),
|
|
h2: ({ children }) => (
|
|
<Box
|
|
component="h2"
|
|
sx={{
|
|
fontSize: d3roTypo.heading.size,
|
|
fontWeight: 600,
|
|
color: d3roPalette.text.primary,
|
|
mt: 1.5,
|
|
mb: 0.75,
|
|
}}
|
|
>
|
|
{children}
|
|
</Box>
|
|
),
|
|
h3: ({ children }) => (
|
|
<Box
|
|
component="h3"
|
|
sx={{
|
|
fontSize: d3roTypo.body.size,
|
|
fontWeight: 600,
|
|
color: d3roPalette.text.secondary,
|
|
mt: 1,
|
|
mb: 0.5,
|
|
}}
|
|
>
|
|
{children}
|
|
</Box>
|
|
),
|
|
p: ({ children }) => (
|
|
<Box
|
|
component="p"
|
|
sx={{
|
|
fontSize: d3roTypo.body.size,
|
|
color: d3roPalette.text.primary,
|
|
lineHeight: 1.7,
|
|
mb: 0.75,
|
|
mt: 0,
|
|
}}
|
|
>
|
|
{children}
|
|
</Box>
|
|
),
|
|
code: ({ children, className }) => {
|
|
const isBlock = className?.startsWith('language-') ?? false
|
|
if (isBlock) {
|
|
return (
|
|
<Box
|
|
component="pre"
|
|
sx={{
|
|
bgcolor: d3roPalette.bg.inset,
|
|
borderRadius: d3roRadius.small,
|
|
p: 1.5,
|
|
overflowX: 'auto',
|
|
my: 1,
|
|
border: `1px solid ${d3roPalette.border.subtle}`,
|
|
}}
|
|
>
|
|
<Box
|
|
component="code"
|
|
sx={{
|
|
fontFamily: d3roFontMono,
|
|
fontSize: d3roTypo.compact.size,
|
|
color: d3roPalette.accent.main,
|
|
}}
|
|
>
|
|
{children}
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|
|
return (
|
|
<Box
|
|
component="code"
|
|
sx={{
|
|
fontFamily: d3roFontMono,
|
|
fontSize: d3roTypo.compact.size,
|
|
color: d3roPalette.accent.main,
|
|
bgcolor: d3roPalette.bg.inset,
|
|
px: 0.5,
|
|
py: 0.25,
|
|
borderRadius: d3roRadius.xs,
|
|
}}
|
|
>
|
|
{children}
|
|
</Box>
|
|
)
|
|
},
|
|
table: ({ children }) => (
|
|
<Box
|
|
component="table"
|
|
sx={{
|
|
width: '100%',
|
|
borderCollapse: 'collapse',
|
|
fontSize: d3roTypo.compact.size,
|
|
mb: 1,
|
|
'& th, & td': {
|
|
border: `1px solid ${d3roPalette.border.default}`,
|
|
p: 0.75,
|
|
textAlign: 'left',
|
|
},
|
|
'& th': {
|
|
bgcolor: d3roPalette.bg.chassis,
|
|
color: d3roPalette.text.secondary,
|
|
fontWeight: 600,
|
|
},
|
|
'& tr:nth-of-type(even)': {
|
|
bgcolor: d3roPalette.bg.inset,
|
|
},
|
|
}}
|
|
>
|
|
{children}
|
|
</Box>
|
|
),
|
|
li: ({ children }) => (
|
|
<Box
|
|
component="li"
|
|
sx={{
|
|
fontSize: d3roTypo.body.size,
|
|
color: d3roPalette.text.primary,
|
|
lineHeight: 1.7,
|
|
mb: 0.25,
|
|
}}
|
|
>
|
|
{children}
|
|
</Box>
|
|
),
|
|
ul: ({ children }) => (
|
|
<Box
|
|
component="ul"
|
|
sx={{ pl: 2.5, mb: 0.75, mt: 0 }}
|
|
>
|
|
{children}
|
|
</Box>
|
|
),
|
|
ol: ({ children }) => (
|
|
<Box
|
|
component="ol"
|
|
sx={{ pl: 2.5, mb: 0.75, mt: 0 }}
|
|
>
|
|
{children}
|
|
</Box>
|
|
),
|
|
blockquote: ({ children }) => (
|
|
<Box
|
|
component="blockquote"
|
|
sx={{
|
|
borderLeft: `3px solid ${d3roPalette.accent.main}`,
|
|
pl: 1.5,
|
|
ml: 0,
|
|
my: 0.75,
|
|
color: d3roPalette.text.secondary,
|
|
fontStyle: 'italic',
|
|
}}
|
|
>
|
|
{children}
|
|
</Box>
|
|
),
|
|
}
|
|
|
|
export function MarkdownRenderer({ content }: MarkdownRendererProps): React.ReactElement {
|
|
return (
|
|
<Box sx={{ color: d3roPalette.text.primary, fontSize: d3roTypo.body.size }}>
|
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
|
|
{content}
|
|
</ReactMarkdown>
|
|
</Box>
|
|
)
|
|
}
|