391 lines
11 KiB
TypeScript
391 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { X, Maximize2, Minimize2, Folder, Tag } from 'lucide-react';
|
|
import ReactMarkdown from 'react-markdown';
|
|
import remarkGfm from 'remark-gfm';
|
|
import { useState } from 'react';
|
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
|
|
interface MarkdownPreviewDialogProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
title: string;
|
|
content: string;
|
|
folder?: string;
|
|
tags?: string[];
|
|
}
|
|
|
|
export function MarkdownPreviewDialog({
|
|
isOpen,
|
|
onClose,
|
|
title,
|
|
content,
|
|
folder,
|
|
tags,
|
|
}: MarkdownPreviewDialogProps) {
|
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
|
<DialogContent
|
|
className={`
|
|
${isFullscreen
|
|
? 'fixed inset-0 w-screen h-screen max-w-none rounded-none border-0 p-0'
|
|
: 'w-[95vw] max-w-6xl h-[90vh]'
|
|
}
|
|
p-0 gap-0 overflow-hidden bg-slate-950 border-slate-800 flex flex-col
|
|
`}
|
|
>
|
|
{/* Header */}
|
|
<DialogHeader className="px-6 py-4 border-b border-slate-800 bg-slate-900/50 flex-shrink-0">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="flex items-center gap-3 min-w-0 flex-1">
|
|
<DialogTitle className="text-xl font-semibold text-slate-100 truncate">
|
|
{title}
|
|
</DialogTitle>
|
|
</div>
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setIsFullscreen(!isFullscreen)}
|
|
className="h-9 w-9 text-slate-400 hover:text-slate-100 hover:bg-slate-800"
|
|
title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
|
>
|
|
{isFullscreen ? (
|
|
<Minimize2 className="w-5 h-5" />
|
|
) : (
|
|
<Maximize2 className="w-5 h-5" />
|
|
)}
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={onClose}
|
|
className="h-9 w-9 text-slate-400 hover:text-slate-100 hover:bg-slate-800"
|
|
title="Close"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Metadata row: Folder and Tags */}
|
|
<div className="flex flex-wrap items-center gap-3 mt-3">
|
|
{folder && (
|
|
<div className="flex items-center gap-1.5 text-sm text-slate-400">
|
|
<Folder className="w-3.5 h-3.5 text-slate-500" />
|
|
<span className="bg-slate-800/70 px-2 py-0.5 rounded text-slate-300">
|
|
{folder}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{tags && tags.length > 0 && (
|
|
<div className="flex items-center gap-2">
|
|
<Tag className="w-3.5 h-3.5 text-slate-500" />
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{tags.map((tag) => (
|
|
<span
|
|
key={tag}
|
|
className="text-xs text-slate-400 bg-slate-800/50 px-2 py-0.5 rounded border border-slate-700/50"
|
|
>
|
|
#{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</DialogHeader>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 overflow-auto bg-slate-950">
|
|
<div className="max-w-4xl mx-auto p-8">
|
|
<article className="markdown-body">
|
|
<ReactMarkdown
|
|
remarkPlugins={[remarkGfm]}
|
|
components={{
|
|
code({ node, inline, className, children, ...props }: any) {
|
|
const match = /language-(\w+)/.exec(className || '');
|
|
return !inline && match ? (
|
|
<div className="code-block-wrapper">
|
|
<div className="code-block-header">
|
|
<span className="code-language">{match[1]}</span>
|
|
</div>
|
|
<SyntaxHighlighter
|
|
style={vscDarkPlus}
|
|
language={match[1]}
|
|
PreTag="div"
|
|
customStyle={{
|
|
margin: 0,
|
|
borderRadius: '0 0 8px 8px',
|
|
background: '#020617',
|
|
}}
|
|
{...props}
|
|
>
|
|
{String(children).replace(/\n$/, '')}
|
|
</SyntaxHighlighter>
|
|
</div>
|
|
) : (
|
|
<code className={className} {...props}>
|
|
{children}
|
|
</code>
|
|
);
|
|
},
|
|
pre({ children }) {
|
|
return <>{children}</>;
|
|
},
|
|
}}
|
|
>
|
|
{content}
|
|
</ReactMarkdown>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer with line count */}
|
|
<div className="px-6 py-2 border-t border-slate-800 bg-slate-900/30 flex-shrink-0">
|
|
<div className="text-xs text-slate-500 text-right">
|
|
{content.split(/\r?\n/).length} lines
|
|
</div>
|
|
</div>
|
|
|
|
{/* Custom styles for markdown content */}
|
|
<style jsx global>{`
|
|
.markdown-body {
|
|
color: #e2e8f0;
|
|
line-height: 1.75;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.markdown-body > *:first-child {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.markdown-body h1,
|
|
.markdown-body h2,
|
|
.markdown-body h3,
|
|
.markdown-body h4,
|
|
.markdown-body h5,
|
|
.markdown-body h6 {
|
|
color: #f8fafc;
|
|
margin-top: 2em;
|
|
margin-bottom: 0.75em;
|
|
font-weight: 600;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.markdown-body h1 {
|
|
font-size: 2em;
|
|
border-bottom: 1px solid #334155;
|
|
padding-bottom: 0.3em;
|
|
}
|
|
|
|
.markdown-body h2 {
|
|
font-size: 1.5em;
|
|
border-bottom: 1px solid #334155;
|
|
padding-bottom: 0.3em;
|
|
}
|
|
|
|
.markdown-body h3 {
|
|
font-size: 1.25em;
|
|
}
|
|
|
|
.markdown-body h4 {
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.markdown-body p {
|
|
margin-top: 0;
|
|
margin-bottom: 1em;
|
|
color: #cbd5e1;
|
|
}
|
|
|
|
.markdown-body a {
|
|
color: #60a5fa;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.markdown-body a:hover {
|
|
text-decoration: underline;
|
|
color: #93c5fd;
|
|
}
|
|
|
|
.markdown-body strong {
|
|
color: #f1f5f9;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.markdown-body em {
|
|
color: #e2e8f0;
|
|
}
|
|
|
|
.markdown-body code {
|
|
background: #1e293b;
|
|
border: 1px solid #334155;
|
|
border-radius: 6px;
|
|
padding: 0.2em 0.4em;
|
|
font-size: 0.875em;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
|
color: #e2e8f0;
|
|
}
|
|
|
|
.markdown-body pre code {
|
|
background: transparent;
|
|
border: 0;
|
|
padding: 0;
|
|
font-size: 0.875em;
|
|
color: inherit;
|
|
}
|
|
|
|
.code-block-wrapper {
|
|
margin: 1.5em 0;
|
|
border: 1px solid #1e293b;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.code-block-header {
|
|
background: #0f172a;
|
|
padding: 8px 16px;
|
|
border-bottom: 1px solid #1e293b;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.code-language {
|
|
font-size: 12px;
|
|
color: #94a3b8;
|
|
text-transform: uppercase;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.markdown-body blockquote {
|
|
border-left: 4px solid #475569;
|
|
margin: 1.5em 0;
|
|
padding: 0.5em 1em;
|
|
color: #94a3b8;
|
|
background: #0f172a;
|
|
border-radius: 0 8px 8px 0;
|
|
}
|
|
|
|
.markdown-body blockquote > :first-child {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.markdown-body blockquote > :last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.markdown-body ul,
|
|
.markdown-body ol {
|
|
margin-top: 0;
|
|
margin-bottom: 1em;
|
|
padding-left: 2em;
|
|
}
|
|
|
|
.markdown-body ul {
|
|
list-style-type: disc;
|
|
}
|
|
|
|
.markdown-body ol {
|
|
list-style-type: decimal;
|
|
}
|
|
|
|
.markdown-body li {
|
|
margin: 0.25em 0;
|
|
}
|
|
|
|
.markdown-body li > p {
|
|
margin-bottom: 0.5em;
|
|
}
|
|
|
|
.markdown-body table {
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
margin: 1.5em 0;
|
|
font-size: 0.875em;
|
|
}
|
|
|
|
.markdown-body table th,
|
|
.markdown-body table td {
|
|
border: 1px solid #334155;
|
|
padding: 8px 12px;
|
|
text-align: left;
|
|
}
|
|
|
|
.markdown-body table th {
|
|
background: #0f172a;
|
|
color: #f8fafc;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.markdown-body table tr:nth-child(even) {
|
|
background: #0f172a;
|
|
}
|
|
|
|
.markdown-body table tr:hover {
|
|
background: #1e293b;
|
|
}
|
|
|
|
.markdown-body hr {
|
|
border: 0;
|
|
border-top: 1px solid #334155;
|
|
margin: 2em 0;
|
|
}
|
|
|
|
.markdown-body img {
|
|
max-width: 100%;
|
|
height: auto;
|
|
border-radius: 8px;
|
|
margin: 1em 0;
|
|
}
|
|
|
|
.markdown-body input[type='checkbox'] {
|
|
margin-right: 0.5em;
|
|
}
|
|
|
|
/* Task lists */
|
|
.markdown-body ul.contains-task-list {
|
|
list-style-type: none;
|
|
padding-left: 0;
|
|
}
|
|
|
|
.markdown-body ul.contains-task-list li {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0.5em;
|
|
}
|
|
|
|
.markdown-body ul.contains-task-list li input[type='checkbox'] {
|
|
margin-top: 0.3em;
|
|
}
|
|
|
|
/* Scrollbar styling for dark theme */
|
|
.markdown-body ::-webkit-scrollbar {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
|
|
.markdown-body ::-webkit-scrollbar-track {
|
|
background: #0f172a;
|
|
}
|
|
|
|
.markdown-body ::-webkit-scrollbar-thumb {
|
|
background: #334155;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.markdown-body ::-webkit-scrollbar-thumb:hover {
|
|
background: #475569;
|
|
}
|
|
`}</style>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|