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 ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import rehypeSlug from "rehype-slug";
|
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 [copied, setCopied] = useState(false);
|
||||||
const textContent = String(children).replace(/\n$/, "");
|
const textContent = extractText(children).replace(/\n$/, "");
|
||||||
const match = /language-(\w+)/.exec(className || "");
|
|
||||||
const language = match ? match[1] : "";
|
|
||||||
|
|
||||||
// If inline code
|
let language = "text";
|
||||||
if (!className && typeof children === "string" && !children.includes("\n")) {
|
if (React.isValidElement(children)) {
|
||||||
return (
|
const className = (children.props as { className?: string })?.className || "";
|
||||||
<code className={className} {...props}>
|
const match = /language-(\w+)/.exec(className);
|
||||||
{children}
|
if (match) {
|
||||||
</code>
|
language = match[1];
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCopy() {
|
function handleCopy() {
|
||||||
@@ -27,37 +42,46 @@ function CodeBlock({ children, className, ...props }: React.HTMLAttributes<HTMLE
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* 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">
|
<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">
|
||||||
<span>{language || "text"}</span>
|
<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
|
<button
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
type="button"
|
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 ? (
|
{copied ? (
|
||||||
<>
|
<>
|
||||||
<svg className="w-3 h-3 text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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 4 L19 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
</svg>
|
</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" />
|
<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>
|
</svg>
|
||||||
<span className="font-sans">Copy</span>
|
<span>Copy</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pre className="!m-0 !bg-transparent p-4 overflow-x-auto text-sm text-[var(--pre-text)] font-mono leading-relaxed">
|
<pre className="!m-0 !bg-transparent p-4 overflow-x-auto text-[13.5px] text-[var(--pre-text)] font-mono leading-relaxed" {...props}>
|
||||||
<code className={className} {...props}>
|
{children}
|
||||||
{children}
|
|
||||||
</code>
|
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -67,31 +91,47 @@ export default function Markdown({ source }: { source: string }) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="prose max-w-none
|
className="prose max-w-none
|
||||||
prose-headings:text-[var(--text-primary)] prose-headings:font-medium prose-headings:tracking-tight
|
prose-headings:text-[var(--text-primary)] prose-headings:font-semibold 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-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 prose-h2:mb-4 prose-h2:mt-8
|
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-base prose-h3:mb-3 prose-h3:mt-6
|
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-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-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-3 prose-li:my-1
|
prose-ul:text-[var(--text-secondary)] prose-ul:my-4 prose-li:my-1.5
|
||||||
prose-ol:text-[var(--text-secondary)] prose-ol:my-3
|
prose-ol:text-[var(--text-secondary)] prose-ol:my-4
|
||||||
prose-strong:text-[var(--text-primary)] prose-strong:font-semibold
|
prose-strong:text-[var(--text-primary)] prose-strong:font-semibold
|
||||||
prose-hr:border-[var(--border-color)] prose-hr:my-8"
|
prose-hr:border-[var(--border-color)] prose-hr:my-8"
|
||||||
>
|
>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
rehypePlugins={[rehypeSlug]}
|
rehypePlugins={[rehypeSlug, rehypeHighlight]}
|
||||||
components={{
|
components={{
|
||||||
code: CodeBlock as any,
|
pre: PreBlock as any,
|
||||||
pre: ({ children }) => <>{children}</>,
|
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 }) => (
|
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} />
|
<table className="w-full border-collapse" {...props} />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
blockquote: ({ children }) => {
|
blockquote: ({ children }) => {
|
||||||
return (
|
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}
|
{children}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import Markdown from "@/components/Markdown";
|
||||||
|
|
||||||
const CONFIGS = [
|
const CONFIGS = [
|
||||||
{
|
{
|
||||||
id: "claude",
|
id: "claude",
|
||||||
label: "Claude Desktop",
|
label: "Claude Desktop",
|
||||||
|
lang: "json",
|
||||||
filename: "~/Library/Application Support/Claude/claude_desktop_config.json",
|
filename: "~/Library/Application Support/Claude/claude_desktop_config.json",
|
||||||
content: `{
|
content: `{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
@@ -23,6 +25,7 @@ const CONFIGS = [
|
|||||||
{
|
{
|
||||||
id: "http",
|
id: "http",
|
||||||
label: "Remote HTTP (SSE)",
|
label: "Remote HTTP (SSE)",
|
||||||
|
lang: "json",
|
||||||
filename: "Streamable HTTP Endpoint",
|
filename: "Streamable HTTP Endpoint",
|
||||||
content: `{
|
content: `{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
@@ -38,6 +41,7 @@ const CONFIGS = [
|
|||||||
{
|
{
|
||||||
id: "cli",
|
id: "cli",
|
||||||
label: "CLI Client",
|
label: "CLI Client",
|
||||||
|
lang: "bash",
|
||||||
filename: "Terminal One-Shot / REPL",
|
filename: "Terminal One-Shot / REPL",
|
||||||
content: `# One-shot question
|
content: `# One-shot question
|
||||||
bun run apps/mcp-client/src/ask.ts "How does ret2win stack alignment work?"
|
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() {
|
export default function McpConfigSnippet() {
|
||||||
const [activeTab, setActiveTab] = useState("claude");
|
const [activeTab, setActiveTab] = useState("claude");
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
|
|
||||||
const current = CONFIGS.find((c) => c.id === activeTab) || CONFIGS[0];
|
const current = CONFIGS.find((c) => c.id === activeTab) || CONFIGS[0];
|
||||||
|
|
||||||
function handleCopy() {
|
const markdownSnippet = `\`\`\`${current.lang}
|
||||||
navigator.clipboard.writeText(current.content);
|
${current.content}
|
||||||
setCopied(true);
|
\`\`\``;
|
||||||
setTimeout(() => setCopied(false), 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* 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 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">
|
<div className="flex items-center gap-1.5">
|
||||||
@@ -68,9 +69,9 @@ export default function McpConfigSnippet() {
|
|||||||
<button
|
<button
|
||||||
key={c.id}
|
key={c.id}
|
||||||
onClick={() => setActiveTab(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
|
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)]"
|
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -79,37 +80,25 @@ export default function McpConfigSnippet() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div className="text-[11px] text-[var(--text-dim)] font-mono hidden sm:block">
|
||||||
onClick={handleCopy}
|
{current.filename}
|
||||||
type="button"
|
</div>
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
{/* Code preview */}
|
{/* Code preview via Markdown syntax highlighter */}
|
||||||
<pre className="p-4 text-xs font-mono text-[var(--pre-text)] bg-[var(--code-block-bg)] overflow-x-auto leading-relaxed">
|
<div className="p-4 [&>.prose>.group]:my-0 [&>.prose]:max-w-none">
|
||||||
<code>{current.content}</code>
|
<Markdown source={markdownSnippet} />
|
||||||
</pre>
|
</div>
|
||||||
|
|
||||||
<div className="px-4 py-2 bg-[var(--bg-elevated)] border-t border-[var(--border-color)] text-[11px] text-[var(--text-dim)]">
|
<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">
|
||||||
Config location: <span className="font-mono text-[var(--text-muted)]">{current.filename}</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+150
-15
@@ -18,19 +18,41 @@
|
|||||||
--text-muted: #64748b;
|
--text-muted: #64748b;
|
||||||
--text-dim: #94a3b8;
|
--text-dim: #94a3b8;
|
||||||
|
|
||||||
--brand: #5e6ad2;
|
--brand: #4f46e5;
|
||||||
--brand-hover: #6a75e0; /* lighter for hover in light mode */
|
--brand-hover: #4338ca;
|
||||||
--accent: #4f46e5;
|
--accent: #4f46e5;
|
||||||
--accent-hover: #4338ca;
|
--accent-hover: #4338ca;
|
||||||
|
|
||||||
--nav-bg: rgba(255, 255, 255, 0.88);
|
--nav-bg: rgba(255, 255, 255, 0.88);
|
||||||
|
|
||||||
|
/* Code & Syntax variables (Light Mode) */
|
||||||
--code-bg: #f1f5f9;
|
--code-bg: #f1f5f9;
|
||||||
--code-block-bg: #0f172a;
|
--code-block-bg: #f8fafc;
|
||||||
--pre-text: #f8fafc;
|
--code-header-bg: #f1f5f9;
|
||||||
|
--border-code: #e2e8f0;
|
||||||
|
--code-button-bg: #ffffff;
|
||||||
|
--code-button-hover-bg: #e2e8f0;
|
||||||
|
--pre-text: #1e293b;
|
||||||
|
--code-inline-bg: #f1f5f9;
|
||||||
|
--code-inline-text: #0f172a;
|
||||||
|
--code-inline-border: #e2e8f0;
|
||||||
|
|
||||||
--table-zebra: rgba(241, 245, 249, 0.6);
|
--table-zebra: rgba(241, 245, 249, 0.6);
|
||||||
--scrollbar-thumb: #cbd5e1;
|
--scrollbar-thumb: #cbd5e1;
|
||||||
--scrollbar-thumb-hover: #94a3b8;
|
--scrollbar-thumb-hover: #94a3b8;
|
||||||
--shadow-card: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px -1px rgba(0, 0, 0, 0.05);
|
--shadow-card: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px -1px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
/* Syntax Highlighting (Light Mode) */
|
||||||
|
--hl-keyword: #cf222e;
|
||||||
|
--hl-string: #0a3069;
|
||||||
|
--hl-title: #8250df;
|
||||||
|
--hl-attr: #953800;
|
||||||
|
--hl-number: #0550ae;
|
||||||
|
--hl-comment: #6e7781;
|
||||||
|
--hl-builtin: #0969da;
|
||||||
|
--hl-operator: #57606a;
|
||||||
|
--hl-variable: #953800;
|
||||||
|
--hl-meta: #6e7781;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode overrides (html.dark) */
|
/* Dark mode overrides (html.dark) */
|
||||||
@@ -51,18 +73,40 @@ html.dark {
|
|||||||
--text-dim: #62666d;
|
--text-dim: #62666d;
|
||||||
|
|
||||||
--brand: #5e6ad2;
|
--brand: #5e6ad2;
|
||||||
--brand-hover: #7170ff; /* lighter for hover in dark mode */
|
--brand-hover: #7170ff;
|
||||||
--accent: #7170ff;
|
--accent: #7170ff;
|
||||||
--accent-hover: #828fff;
|
--accent-hover: #828fff;
|
||||||
|
|
||||||
--nav-bg: rgba(8, 9, 10, 0.85);
|
--nav-bg: rgba(8, 9, 10, 0.85);
|
||||||
|
|
||||||
|
/* Code & Syntax variables (Dark Mode) */
|
||||||
--code-bg: #141517;
|
--code-bg: #141517;
|
||||||
--code-block-bg: #0c0d0e;
|
--code-block-bg: #0d1117;
|
||||||
--pre-text: #e2e4e7;
|
--code-header-bg: #161b22;
|
||||||
|
--border-code: #30363d;
|
||||||
|
--code-button-bg: #21262d;
|
||||||
|
--code-button-hover-bg: #30363d;
|
||||||
|
--pre-text: #e6edf3;
|
||||||
|
--code-inline-bg: #161b22;
|
||||||
|
--code-inline-text: #e6edf3;
|
||||||
|
--code-inline-border: #30363d;
|
||||||
|
|
||||||
--table-zebra: rgba(255, 255, 255, 0.02);
|
--table-zebra: rgba(255, 255, 255, 0.02);
|
||||||
--scrollbar-thumb: #23252a;
|
--scrollbar-thumb: #23252a;
|
||||||
--scrollbar-thumb-hover: #383b42;
|
--scrollbar-thumb-hover: #383b42;
|
||||||
--shadow-card: none;
|
--shadow-card: none;
|
||||||
|
|
||||||
|
/* Syntax Highlighting (Dark Mode) */
|
||||||
|
--hl-keyword: #ff7b72;
|
||||||
|
--hl-string: #a5d6ff;
|
||||||
|
--hl-title: #d2a8ff;
|
||||||
|
--hl-attr: #ffa657;
|
||||||
|
--hl-number: #79c0ff;
|
||||||
|
--hl-comment: #8b949e;
|
||||||
|
--hl-builtin: #7ee787;
|
||||||
|
--hl-operator: #8b949e;
|
||||||
|
--hl-variable: #ffa657;
|
||||||
|
--hl-meta: #8b949e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
@@ -75,6 +119,7 @@ html.dark {
|
|||||||
--color-elevated: var(--bg-elevated);
|
--color-elevated: var(--bg-elevated);
|
||||||
--color-elevated-hover: var(--bg-elevated-hover);
|
--color-elevated-hover: var(--bg-elevated-hover);
|
||||||
--color-border: var(--border-color);
|
--color-border: var(--border-color);
|
||||||
|
--border-code: var(--border-code);
|
||||||
--color-border-subtle: var(--border-subtle);
|
--color-border-subtle: var(--border-subtle);
|
||||||
|
|
||||||
--color-text-primary: var(--text-primary);
|
--color-text-primary: var(--text-primary);
|
||||||
@@ -189,24 +234,26 @@ body {
|
|||||||
|
|
||||||
.prose pre {
|
.prose pre {
|
||||||
background: var(--code-block-bg) !important;
|
background: var(--code-block-bg) !important;
|
||||||
border: 1px solid var(--border-color);
|
color: var(--pre-text) !important;
|
||||||
border-radius: 0.625rem;
|
border: 1px solid var(--border-code);
|
||||||
|
border-radius: 0.75rem;
|
||||||
padding: 1rem 1.25rem;
|
padding: 1rem 1.25rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose code {
|
.prose code {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 0.85em;
|
font-size: 0.875em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose :not(pre) > code {
|
.prose :not(pre) > code {
|
||||||
background: var(--code-bg);
|
background: var(--code-inline-bg);
|
||||||
color: var(--text-primary);
|
color: var(--code-inline-text);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--code-inline-border);
|
||||||
padding: 0.15rem 0.35rem;
|
padding: 0.15rem 0.375rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.375rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +277,94 @@ body {
|
|||||||
margin: 2.5rem 0;
|
margin: 2.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Syntax Highlighting Tokens (Highlight.js) */
|
||||||
|
.hljs {
|
||||||
|
color: var(--pre-text);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-selector-tag,
|
||||||
|
.hljs-subst,
|
||||||
|
.hljs-doctag {
|
||||||
|
color: var(--hl-keyword);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-link {
|
||||||
|
color: var(--hl-string);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-title,
|
||||||
|
.hljs-title.function_,
|
||||||
|
.hljs-title.class_,
|
||||||
|
.hljs-section {
|
||||||
|
color: var(--hl-title);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-attr,
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-variable,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-type {
|
||||||
|
color: var(--hl-attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-literal {
|
||||||
|
color: var(--hl-number);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote {
|
||||||
|
color: var(--hl-comment);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-symbol,
|
||||||
|
.hljs-bullet {
|
||||||
|
color: var(--hl-builtin);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-meta,
|
||||||
|
.hljs-meta .hljs-keyword,
|
||||||
|
.hljs-meta .hljs-string {
|
||||||
|
color: var(--hl-meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-params {
|
||||||
|
color: var(--pre-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-punctuation,
|
||||||
|
.hljs-operator {
|
||||||
|
color: var(--hl-operator);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-addition {
|
||||||
|
color: #1a7f37;
|
||||||
|
background-color: rgba(46, 160, 67, 0.15);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-deletion {
|
||||||
|
color: #cf222e;
|
||||||
|
background-color: rgba(248, 81, 73, 0.15);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
/* Animations */
|
/* Animations */
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; transform: translateY(4px); }
|
from { opacity: 0; transform: translateY(4px); }
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8",
|
"react-dom": "19.2.8",
|
||||||
"react-markdown": "^9.0.0",
|
"react-markdown": "^9.0.0",
|
||||||
|
"rehype-highlight": "^7.0.2",
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
"remark-gfm": "^4.0.1"
|
"remark-gfm": "^4.0.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
|
||||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
|
||||||
<Image
|
|
||||||
className="dark:invert h-5 w-[100px]"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
|
||||||
width={100}
|
|
||||||
height={20}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
||||||
To get started, edit the{" "}
|
|
||||||
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
|
|
||||||
page.tsx
|
|
||||||
</code>{" "}
|
|
||||||
file.
|
|
||||||
</h1>
|
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Templates
|
|
||||||
</a>{" "}
|
|
||||||
or the{" "}
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Learning
|
|
||||||
</a>{" "}
|
|
||||||
center.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert h-[14px] w-4"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={14}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user