Code Block
code-block and preview of the code
Component Preview - Preview
app/api/mcp/route.ts
1import { z } from 'zod';2import { createMcpHandler } from 'mcp-handler';3 4const handler = createMcpHandler(5(server) => {6 server.tool(7 'roll_dice',8 'Rolls an N-sided die',9 { sides: z.number().int().min(2) },10 async ({ sides }) => {11 const value = 1 + Math.floor(Math.random() * sides);12 return {13 content: [{ type: 'text', text: `🎲 You rolled a 10!` }],14 };15 },16 );17},18{},19{ basePath: '/api' },20);pages/index.tsx
1import { useChat } from '@ai-sdk/react';2import { useState } from 'react';3 4export default function Chat() {5const [input, setInput] = useState('');6const { messages, sendMessage } = useChat();7return (8 <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">9 {messages.map(message => (10 <div key={message.id} className="whitespace-pre-wrap">11 {message.role === 'user' ? 'User: ' : 'AI: '}12 {message.parts.map((part, i) => {13 switch (part.type) {14 case 'text':15 return <div>{part.text}</div>;16 }17 })}18 </div>19 ))}20 <form21 onSubmit={e => {22 e.preventDefault();23 sendMessage({ text: input });24 setInput('');25 }}26 >27 <input28 className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl"29 value={input}30 placeholder="Say something..."31 onChange={e => setInput(e.currentTarget.value)}32 />33 </form>34 </div>35);36}Manual Installation
1
Install dependencies
Terminal
bash
npm i clsx tailwind-merge react-icons shaki2
Add the util file
lib/utils.ts
ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}3
Copy the source code
components/lokalhost_io/code-blocks/component-preview.tsx
tsx
"use client";
import React, { useState, useEffect, CSSProperties } from "react";
import { IoCheckmarkOutline } from "react-icons/io5";
import { BiLogoTypescript } from "react-icons/bi";
import { codeToHtml } from "shiki";
import { useTheme } from "next-themes";
import { cn } from "@/lib/utils";
import { PiCopyDuotone } from "react-icons/pi";
interface CodeBlockProps {
code: string;
fileName?: string;
language?: string;
showLineNumbers?: boolean;
lightTheme?: string; // e.g. "github-light"
darkTheme?: string; // e.g. "github-dark"
// layout
width?: string | number; // e.g. 600, "100%", "40rem"
height?: string | number; // e.g. 400, "auto"
borderRadius?: string | number; // e.g. 8, "0.5rem", "9999px"
// colors (override the neutral defaults)
bgColor?: string; // light mode background
darkBgColor?: string; // dark mode background
borderColor?: string; // light mode border
darkBorderColor?: string; // dark mode border
headerBgColor?: string; // optional separate header bg (defaults to bgColor)
darkHeaderBgColor?: string;
className?: string; // escape hatch for anything else
}
function CodeBlock({
code,
fileName = "app/api/mcp/route.ts",
language = "tsx",
showLineNumbers = false,
lightTheme = "github-light",
darkTheme = "github-dark",
width = 600,
height = 400,
borderRadius = 0,
bgColor,
darkBgColor,
borderColor,
darkBorderColor,
headerBgColor,
darkHeaderBgColor,
className,
}: CodeBlockProps) {
const { resolvedTheme } = useTheme();
const [copied, setCopied] = useState(false);
const isDark = resolvedTheme === "dark";
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch (err) {
console.error("Failed to copy code:", err);
}
};
const containerStyle: CSSProperties = {
width: typeof width === "number" ? `widthpx` : width,
height: typeof height === "number" ? `heightpx` : height,
borderRadius: typeof borderRadius === "number" ? `borderRadiuspx` : borderRadius,
backgroundColor: isDark ? darkBgColor ?? bgColor : bgColor,
borderColor: isDark ? darkBorderColor ?? borderColor : borderColor,
};
const headerStyle: CSSProperties = {
backgroundColor: isDark
? darkHeaderBgColor ?? headerBgColor
: headerBgColor,
};
return (
<div className="w-full flex justify-center items-center">
<div
style={containerStyle}
className={cn(
"flex flex-col border overflow-hidden",
// fall back to defaults only where no override color was given
!bgColor && "bg-neutral-100",
!darkBgColor && "dark:bg-black",
!borderColor && "border-neutral-400",
!darkBorderColor && "dark:border-neutral-700",
className
)}
>
{/* Header */}
<div
style={headerStyle}
className={cn(
"flex w-full justify-between items-center h-[50px] shrink-0 border-b",
!borderColor && "border-neutral-400",
!darkBorderColor && "dark:border-neutral-700"
)}
>
<div className="flex items-center gap-2 px-4">
<div>
<BiLogoTypescript size={20} />
</div>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
{fileName}
</p>
</div>
<div className="flex items-center gap-2 px-4">
<button
onClick={handleCopy}
aria-label="Copy code"
className="cursor-pointer flex items-center justify-center p-1 rounded-sm bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700 transition-colors"
>
{copied ? (
<IoCheckmarkOutline size={16} className="text-neutral-600 dark:text-neutral-500" />
) : (
<PiCopyDuotone size={16} />
)}
</button>
</div>
</div>
{/* Code block part */}
<div className="flex-1 min-h-0 overflow-auto">
<CodeHighlight
code={code}
language={language}
shikiTheme={isDark ? darkTheme : lightTheme}
showLineNumbers={showLineNumbers}
/>
</div>
</div>
</div>
);
}
function CodeHighlight({
code,
language = "tsx",
shikiTheme,
showLineNumbers,
}: {
code: string;
language?: string;
shikiTheme: string;
showLineNumbers: boolean;
}) {
const [html, setHtml] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
codeToHtml(code, {
lang: language,
theme: shikiTheme,
}).then((h) => {
if (!cancelled) setHtml(h);
});
return () => {
cancelled = true;
};
}, [code, language, shikiTheme]);
if (!html) {
return <CodeFallback code={code} showLineNumbers={showLineNumbers} />;
}
return (
<>
{showLineNumbers && (
<style>{`
.shiki-lines code { counter-reset: line; }
.shiki-lines .line { counter-increment: line; display: inline-block; width: 100%; }
.shiki-lines .line::before {
content: counter(line);
display: inline-block;
width: 2rem;
margin-right: 0.50rem;
text-align: right;
color: hsl(var(--muted-foreground));
opacity: 0.4;
user-select: none;
}
`}</style>
)}
<div
style={{ fontFamily: "'Geist Mono', 'Geist Mono Fallback', monospace" }}
className={cn(
showLineNumbers && "shiki-lines",
"[&>pre]:overflow-x-auto [&>pre]:text-[12px] [&>pre]:leading-relaxed [&>pre]:py-4",
showLineNumbers ? "[&>pre]:pl-0 [&>pre]:pr-4" : "[&>pre]:px-4",
// force transparent bg so the container's own bg shows through
"[&>pre]:!bg-transparent [&_code]:!bg-transparent [&>pre]:![font-family:inherit]"
)}
dangerouslySetInnerHTML={{ __html: html }}
/>
</>
);
}
function CodeFallback({
code,
showLineNumbers,
}: {
code: string;
showLineNumbers: boolean;
}) {
const lines = code.split("
");
return (
<pre className="overflow-x-auto text-[12px] leading-relaxed font-mono text-foreground px-4 py-4">
<code
className={showLineNumbers ? "grid" : "block"}
style={showLineNumbers ? { gridTemplateColumns: "auto 1fr" } : undefined}
>
{lines.map((line, i) =>
showLineNumbers ? (
<span key={i} className="contents">
<span className="select-none pr-2 text-right text-muted-foreground/40">
{i + 1}
</span>
<span>{line || " "}</span>
</span>
) : (
<span key={i} className="block">
{line || " "}
</span>
)
)}
</code>
</pre>
);
}
export default CodeBlock;Props
| Prop | Type | Description |
|---|---|---|
| code* | string | The raw source code to display and highlight. |
| fileName | string | File path or name shown in the header next to the language icon. |
| language | string | Shiki-supported language id used for syntax highlighting (e.g. "ts", "tsx", "bash", "json"). |
| showLineNumbers | boolean | Toggles a line-number gutter alongside the code. |
| lightTheme | string | Shiki theme id used when resolved app theme is light. |
| darkTheme | string | Shiki theme id used when resolved app theme is dark. |
| width | string | number | Width of the component. Numbers are treated as pixels; strings are used as-is (e.g. "100%", "40rem"). |
| height | string | number | Height of the component. Numbers are treated as pixels; strings are used as-is (e.g. "auto", "60vh"). |
| borderRadius | string | number | Corner radius of the outer card. Numbers are treated as pixels. |
| bgColor | string | Background color in light mode. Falls back to the default neutral background if omitted. |
| darkBgColor | string | Background color in dark mode. Falls back to the default neutral background if omitted. |
| borderColor | string | Border color in light mode. Falls back to the default neutral border if omitted. |
| darkBorderColor | string | Border color in dark mode. Falls back to the default neutral border if omitted. |
| headerBgColor | string | Background color for the header row in light mode. Defaults to bgColor if omitted. |
| darkHeaderBgColor | string | Background color for the header row in dark mode. Defaults to darkBgColor if omitted. |
| className | string | Additional classes applied to the outer card for one-off style overrides. |