diff --git a/apps/web/app/components/Markdown.tsx b/apps/web/app/components/Markdown.tsx index 8765691..2dc95ef 100644 --- a/apps/web/app/components/Markdown.tsx +++ b/apps/web/app/components/Markdown.tsx @@ -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) { +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) { 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 ( - - {children} - - ); + 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 +
{/* Code block header bar */} -
- {language || "text"} +
+
+
+ + + +
+ + {language} + +
+
-
-        
-          {children}
-        
+      
+        {children}
       
); @@ -67,31 +91,47 @@ export default function Markdown({ source }: { source: string }) { return (
<>{children}, + pre: PreBlock as any, + code: ({ children, className, ...props }: React.HTMLAttributes) => { + if (className?.includes("hljs") || className?.includes("language-")) { + return ( + + {children} + + ); + } + return ( + + {children} + + ); + }, table: ({ node, ...props }) => ( -
+
), blockquote: ({ children }) => { return ( -
+
{children}
); diff --git a/apps/web/app/components/McpConfigSnippet.tsx b/apps/web/app/components/McpConfigSnippet.tsx index 19f2191..ff04c59 100644 --- a/apps/web/app/components/McpConfigSnippet.tsx +++ b/apps/web/app/components/McpConfigSnippet.tsx @@ -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 ( -
+
{/* Header bar with tabs */}
@@ -68,9 +69,9 @@ export default function McpConfigSnippet() {
- +
+ {current.filename} +
- {/* Code preview */} -
-        {current.content}
-      
+ {/* Code preview via Markdown syntax highlighter */} +
+ +
-
- Config location: {current.filename} +
+ + Config target: {current.filename} + + + Format: {current.lang} +
); } + diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 8faf55a..48c94f0 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -18,19 +18,41 @@ --text-muted: #64748b; --text-dim: #94a3b8; - --brand: #5e6ad2; - --brand-hover: #6a75e0; /* lighter for hover in light mode */ + --brand: #4f46e5; + --brand-hover: #4338ca; --accent: #4f46e5; --accent-hover: #4338ca; --nav-bg: rgba(255, 255, 255, 0.88); + + /* Code & Syntax variables (Light Mode) */ --code-bg: #f1f5f9; - --code-block-bg: #0f172a; - --pre-text: #f8fafc; + --code-block-bg: #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); --scrollbar-thumb: #cbd5e1; --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); + + /* 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) */ @@ -51,18 +73,40 @@ html.dark { --text-dim: #62666d; --brand: #5e6ad2; - --brand-hover: #7170ff; /* lighter for hover in dark mode */ + --brand-hover: #7170ff; --accent: #7170ff; --accent-hover: #828fff; --nav-bg: rgba(8, 9, 10, 0.85); + + /* Code & Syntax variables (Dark Mode) */ --code-bg: #141517; - --code-block-bg: #0c0d0e; - --pre-text: #e2e4e7; + --code-block-bg: #0d1117; + --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); --scrollbar-thumb: #23252a; --scrollbar-thumb-hover: #383b42; --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 { @@ -75,6 +119,7 @@ html.dark { --color-elevated: var(--bg-elevated); --color-elevated-hover: var(--bg-elevated-hover); --color-border: var(--border-color); + --border-code: var(--border-code); --color-border-subtle: var(--border-subtle); --color-text-primary: var(--text-primary); @@ -189,24 +234,26 @@ body { .prose pre { background: var(--code-block-bg) !important; - border: 1px solid var(--border-color); - border-radius: 0.625rem; + color: var(--pre-text) !important; + border: 1px solid var(--border-code); + border-radius: 0.75rem; padding: 1rem 1.25rem; overflow-x: auto; position: relative; + font-family: var(--font-mono); } .prose code { font-family: var(--font-mono); - font-size: 0.85em; + font-size: 0.875em; } .prose :not(pre) > code { - background: var(--code-bg); - color: var(--text-primary); - border: 1px solid var(--border-color); - padding: 0.15rem 0.35rem; - border-radius: 0.25rem; + background: var(--code-inline-bg); + color: var(--code-inline-text); + border: 1px solid var(--code-inline-border); + padding: 0.15rem 0.375rem; + border-radius: 0.375rem; font-weight: 500; } @@ -230,6 +277,94 @@ body { 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 */ @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } diff --git a/apps/web/package.json b/apps/web/package.json index 3b29b17..9a69582 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -18,6 +18,7 @@ "react": "19.2.8", "react-dom": "19.2.8", "react-markdown": "^9.0.0", + "rehype-highlight": "^7.0.2", "rehype-slug": "^6.0.0", "remark-gfm": "^4.0.1" }, diff --git a/apps/web/page.tsx b/apps/web/page.tsx deleted file mode 100644 index c887311..0000000 --- a/apps/web/page.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import Image from "next/image"; - -export default function Home() { - return ( -
-
- -
-

- To get started, edit the{" "} - - page.tsx - {" "} - file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
- -
-
- ); -} diff --git a/bun.lock b/bun.lock index 9be0941..288e175 100644 --- a/bun.lock +++ b/bun.lock @@ -76,6 +76,7 @@ "react": "19.2.8", "react-dom": "19.2.8", "react-markdown": "^9.0.0", + "rehype-highlight": "^7.0.2", "rehype-slug": "^6.0.0", "remark-gfm": "^4.0.1", }, @@ -935,16 +936,22 @@ "hast-util-heading-rank": ["hast-util-heading-rank@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA=="], + "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], + "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + "highlight.js": ["highlight.js@11.11.2", "", {}, "sha512-oaXMACAU0kzOMXBjWpNcX+vlwSBCIAiZ9BHa7gA15NOTtT2L/l8OSZDuqS2XppOhZBPJ7hm4o8ep2kyuip2uEQ=="], + "hono": ["hono@4.13.3", "", {}, "sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw=="], "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], @@ -1109,6 +1116,8 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "lowlight": ["lowlight@3.3.0", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], @@ -1335,6 +1344,8 @@ "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + "rehype-highlight": ["rehype-highlight@7.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-text": "^4.0.0", "lowlight": "^3.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA=="], + "rehype-slug": ["rehype-slug@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "github-slugger": "^2.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-to-string": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A=="], "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], @@ -1493,6 +1504,8 @@ "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],