feat: enhance Markdown component with syntax highlighting and improve code snippet display
This commit is contained in:
@@ -4,20 +4,35 @@ import React, { useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import rehypeSlug from "rehype-slug";
|
||||
import rehypeHighlight from "rehype-highlight";
|
||||
|
||||
function CodeBlock({ children, className, ...props }: React.HTMLAttributes<HTMLElement>) {
|
||||
function extractText(node: React.ReactNode): string {
|
||||
if (node === null || node === undefined || typeof node === "boolean") {
|
||||
return "";
|
||||
}
|
||||
if (typeof node === "string" || typeof node === "number") {
|
||||
return String(node);
|
||||
}
|
||||
if (Array.isArray(node)) {
|
||||
return node.map(extractText).join("");
|
||||
}
|
||||
if (React.isValidElement(node)) {
|
||||
return extractText((node.props as { children?: React.ReactNode })?.children);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function PreBlock({ children, ...props }: React.HTMLAttributes<HTMLPreElement>) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const textContent = String(children).replace(/\n$/, "");
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
const language = match ? match[1] : "";
|
||||
const textContent = extractText(children).replace(/\n$/, "");
|
||||
|
||||
// If inline code
|
||||
if (!className && typeof children === "string" && !children.includes("\n")) {
|
||||
return (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
let language = "text";
|
||||
if (React.isValidElement(children)) {
|
||||
const className = (children.props as { className?: string })?.className || "";
|
||||
const match = /language-(\w+)/.exec(className);
|
||||
if (match) {
|
||||
language = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
function handleCopy() {
|
||||
@@ -27,37 +42,46 @@ function CodeBlock({ children, className, ...props }: React.HTMLAttributes<HTMLE
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative group my-4 rounded-lg overflow-hidden border border-[var(--border-color)] bg-[var(--code-block-bg)]">
|
||||
<div className="relative group my-5 rounded-xl overflow-hidden border border-[var(--border-code)] bg-[var(--code-block-bg)] shadow-xs">
|
||||
{/* Code block header bar */}
|
||||
<div className="flex items-center justify-between px-3.5 py-1.5 bg-[var(--bg-elevated)] border-b border-[var(--border-color)] text-[11px] text-[var(--text-muted)] font-mono">
|
||||
<span>{language || "text"}</span>
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-[var(--code-header-bg)] border-b border-[var(--border-code)] text-xs font-mono select-none">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex items-center gap-1.5 opacity-75 group-hover:opacity-100 transition-opacity">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-rose-400/80 dark:bg-rose-500/60 inline-block" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-amber-400/80 dark:bg-amber-500/60 inline-block" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-emerald-400/80 dark:bg-emerald-500/60 inline-block" />
|
||||
</div>
|
||||
<span className="text-[11px] font-medium uppercase tracking-wider text-[var(--text-muted)] group-hover:text-[var(--text-secondary)] transition-colors">
|
||||
{language}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
type="button"
|
||||
className="flex items-center gap-1 text-[var(--text-muted)] hover:text-[var(--text-primary)] bg-[var(--bg-elevated-hover)] hover:bg-[var(--border-subtle)] px-2 py-0.5 rounded border border-[var(--border-color)] transition-all"
|
||||
aria-label="Copy code to clipboard"
|
||||
className="flex items-center gap-1.5 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] bg-[var(--code-button-bg)] hover:bg-[var(--code-button-hover-bg)] px-2.5 py-1 rounded-md border border-[var(--border-code)] transition-all font-sans shadow-xs cursor-pointer"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<svg className="w-3 h-3 text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4 L19 7" />
|
||||
<svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-emerald-400 font-sans">Copied!</span>
|
||||
<span className="text-emerald-500 font-medium">Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="font-sans">Copy</span>
|
||||
<span>Copy</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<pre className="!m-0 !bg-transparent p-4 overflow-x-auto text-sm text-[var(--pre-text)] font-mono leading-relaxed">
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
<pre className="!m-0 !bg-transparent p-4 overflow-x-auto text-[13.5px] text-[var(--pre-text)] font-mono leading-relaxed" {...props}>
|
||||
{children}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
@@ -67,31 +91,47 @@ export default function Markdown({ source }: { source: string }) {
|
||||
return (
|
||||
<div
|
||||
className="prose max-w-none
|
||||
prose-headings:text-[var(--text-primary)] prose-headings:font-medium prose-headings:tracking-tight
|
||||
prose-h1:text-2xl prose-h1:mb-6 prose-h1:mt-8 prose-h1:pb-2 prose-h1:border-b prose-h1:border-[var(--border-color)]
|
||||
prose-h2:text-xl prose-h2:mb-4 prose-h2:mt-8
|
||||
prose-h3:text-base prose-h3:mb-3 prose-h3:mt-6
|
||||
prose-headings:text-[var(--text-primary)] prose-headings:font-semibold prose-headings:tracking-tight
|
||||
prose-h1:text-2xl sm:prose-h1:text-3xl prose-h1:mb-6 prose-h1:mt-8 prose-h1:pb-3 prose-h1:border-b prose-h1:border-[var(--border-color)]
|
||||
prose-h2:text-xl sm:prose-h2:text-2xl prose-h2:mb-4 prose-h2:mt-8 prose-h2:pb-2 prose-h2:border-b prose-h2:border-[var(--border-subtle)]
|
||||
prose-h3:text-lg prose-h3:mb-3 prose-h3:mt-6
|
||||
prose-p:text-[var(--text-secondary)] prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-a:text-[var(--accent)] prose-a:no-underline hover:prose-a:underline hover:prose-a:text-[var(--accent-hover)]
|
||||
prose-ul:text-[var(--text-secondary)] prose-ul:my-3 prose-li:my-1
|
||||
prose-ol:text-[var(--text-secondary)] prose-ol:my-3
|
||||
prose-a:text-[var(--accent)] prose-a:font-medium prose-a:no-underline hover:prose-a:underline hover:prose-a:text-[var(--accent-hover)]
|
||||
prose-ul:text-[var(--text-secondary)] prose-ul:my-4 prose-li:my-1.5
|
||||
prose-ol:text-[var(--text-secondary)] prose-ol:my-4
|
||||
prose-strong:text-[var(--text-primary)] prose-strong:font-semibold
|
||||
prose-hr:border-[var(--border-color)] prose-hr:my-8"
|
||||
>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeSlug]}
|
||||
rehypePlugins={[rehypeSlug, rehypeHighlight]}
|
||||
components={{
|
||||
code: CodeBlock as any,
|
||||
pre: ({ children }) => <>{children}</>,
|
||||
pre: PreBlock as any,
|
||||
code: ({ children, className, ...props }: React.HTMLAttributes<HTMLElement>) => {
|
||||
if (className?.includes("hljs") || className?.includes("language-")) {
|
||||
return (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<code
|
||||
className="px-1.5 py-0.5 rounded-md bg-[var(--code-inline-bg)] text-[var(--code-inline-text)] border border-[var(--code-inline-border)] font-mono text-[0.875em] font-medium"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
table: ({ node, ...props }) => (
|
||||
<div className="overflow-x-auto my-6 border border-[var(--border-color)] rounded-lg">
|
||||
<div className="overflow-x-auto my-6 border border-[var(--border-color)] rounded-xl shadow-xs">
|
||||
<table className="w-full border-collapse" {...props} />
|
||||
</div>
|
||||
),
|
||||
blockquote: ({ children }) => {
|
||||
return (
|
||||
<blockquote className="border-l-4 border-[var(--brand)] bg-[var(--bg-elevated)]/60 pl-4 py-2 my-4 rounded-r text-[var(--text-muted)] not-italic">
|
||||
<blockquote className="border-l-4 border-[var(--brand)] bg-[var(--brand)]/5 dark:bg-[var(--brand)]/10 pl-4 py-2.5 my-5 rounded-r-lg text-[var(--text-secondary)] not-italic">
|
||||
{children}
|
||||
</blockquote>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Markdown from "@/components/Markdown";
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
id: "claude",
|
||||
label: "Claude Desktop",
|
||||
lang: "json",
|
||||
filename: "~/Library/Application Support/Claude/claude_desktop_config.json",
|
||||
content: `{
|
||||
"mcpServers": {
|
||||
@@ -23,6 +25,7 @@ const CONFIGS = [
|
||||
{
|
||||
id: "http",
|
||||
label: "Remote HTTP (SSE)",
|
||||
lang: "json",
|
||||
filename: "Streamable HTTP Endpoint",
|
||||
content: `{
|
||||
"mcpServers": {
|
||||
@@ -38,6 +41,7 @@ const CONFIGS = [
|
||||
{
|
||||
id: "cli",
|
||||
label: "CLI Client",
|
||||
lang: "bash",
|
||||
filename: "Terminal One-Shot / REPL",
|
||||
content: `# One-shot question
|
||||
bun run apps/mcp-client/src/ask.ts "How does ret2win stack alignment work?"
|
||||
@@ -49,18 +53,15 @@ bun run apps/mcp-client/src/index.ts`,
|
||||
|
||||
export default function McpConfigSnippet() {
|
||||
const [activeTab, setActiveTab] = useState("claude");
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const current = CONFIGS.find((c) => c.id === activeTab) || CONFIGS[0];
|
||||
|
||||
function handleCopy() {
|
||||
navigator.clipboard.writeText(current.content);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
const markdownSnippet = `\`\`\`${current.lang}
|
||||
${current.content}
|
||||
\`\`\``;
|
||||
|
||||
return (
|
||||
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl overflow-hidden shadow-md">
|
||||
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl overflow-hidden shadow-sm">
|
||||
{/* Header bar with tabs */}
|
||||
<div className="flex items-center justify-between px-4 py-2.5 bg-[var(--bg-elevated)] border-b border-[var(--border-color)] flex-wrap gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -68,9 +69,9 @@ export default function McpConfigSnippet() {
|
||||
<button
|
||||
key={c.id}
|
||||
onClick={() => setActiveTab(c.id)}
|
||||
className={`px-3 py-1 text-xs rounded-md font-medium transition-all ${
|
||||
className={`px-3 py-1.5 text-xs rounded-lg font-medium transition-all cursor-pointer ${
|
||||
activeTab === c.id
|
||||
? "bg-[var(--brand)] text-white shadow-sm"
|
||||
? "bg-[var(--brand)] text-white shadow-xs"
|
||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface)]"
|
||||
}`}
|
||||
>
|
||||
@@ -79,37 +80,25 @@ export default function McpConfigSnippet() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
type="button"
|
||||
className="flex items-center gap-1.5 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated-hover)] px-2.5 py-1 rounded border border-[var(--border-color)] transition-all shadow-sm"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-emerald-500 font-medium">Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span>Copy Config</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<div className="text-[11px] text-[var(--text-dim)] font-mono hidden sm:block">
|
||||
{current.filename}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Code preview */}
|
||||
<pre className="p-4 text-xs font-mono text-[var(--pre-text)] bg-[var(--code-block-bg)] overflow-x-auto leading-relaxed">
|
||||
<code>{current.content}</code>
|
||||
</pre>
|
||||
{/* Code preview via Markdown syntax highlighter */}
|
||||
<div className="p-4 [&>.prose>.group]:my-0 [&>.prose]:max-w-none">
|
||||
<Markdown source={markdownSnippet} />
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-2 bg-[var(--bg-elevated)] border-t border-[var(--border-color)] text-[11px] text-[var(--text-dim)]">
|
||||
Config location: <span className="font-mono text-[var(--text-muted)]">{current.filename}</span>
|
||||
<div className="px-4 py-2 bg-[var(--bg-elevated)] border-t border-[var(--border-color)] text-[11px] text-[var(--text-dim)] flex items-center justify-between flex-wrap gap-2">
|
||||
<span>
|
||||
Config target: <span className="font-mono text-[var(--text-secondary)]">{current.filename}</span>
|
||||
</span>
|
||||
<span className="text-[10px] text-[var(--text-muted)]">
|
||||
Format: <span className="uppercase font-mono font-semibold">{current.lang}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user