A code block with a language label and one-click copy button.
export const sum = (a: number, b: number) => a + b; export const clamp = (n: number, min: number, max: number) => Math.min(Math.max(n, min), max);import { useState } from "react";
export function CodeBlock({ lang, code }: { lang: string; code: string }) {
const [copied, setCopied] = useState(false);
const lines = code.split("\n");
const copy = () => {
navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 1200);
};
return (
<div className="cb">
<div className="cb-head">
<span className="cb-file">
<svg className="cb-icon" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="m8 6-6 6 6 6M16 6l6 6-6 6" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" /></svg>
<span className="cb-lang">{lang}</span>
</span>
<button className="cb-copy" onClick={copy} aria-label="Copy code">
{copied ? "Copied" : "Copy"}
</button>
</div>
<div className="cb-body">
{lines.map((line, i) => (
<div className="cb-row" key={i}>
<span className="cb-ln">{i + 1}</span>
<code className="cb-code">{line || "\u00A0"}</code>
</div>
))}
</div>
</div>
);
}
/* CodeBlock.css */
.cb { border-radius: 12px; background: #fff; box-shadow: 0 0 0 1px #e6e8ec; padding: 12px 16px 0; overflow: hidden; }
.cb-head { display: flex; align-items: center; gap: 8px; margin: -12px -16px 0; padding: 10px 12px 10px 16px; background: #fafafa; border-bottom: 0.5px solid #e6e8ec; }
.cb-file { display: inline-flex; align-items: center; gap: 7px; }
.cb-icon { display: block; width: 15px; height: 15px; color: #a1a1a1; flex: none; transform: translateY(-1px); }
.cb-lang { font-family: ui-monospace, monospace; font-size: 12.5px; line-height: 1; color: #1a1a1a; transform: translateY(-1px); }
.cb-copy { margin-left: auto; font-size: 12px; color: #a1a1a1; border: 0; background: none; padding: 5px 7px; border-radius: 7px; cursor: pointer; }
.cb-copy:hover { color: #1a1a1a; background: #f4f5f7; }
.cb-body { position: relative; margin: 0 -16px; padding: 10px 0; font-family: ui-monospace, monospace; font-size: 12.5px; line-height: 20px; }
.cb-body::before { content: ""; position: absolute; top: 0; bottom: 0; left: 32px; width: 0.5px; background: #e6e8ec; }
.cb-row { display: grid; grid-template-columns: 32px 1fr; }
.cb-ln { user-select: none; text-align: right; padding: 0 7px; color: #a1a1a1; font-size: 11px; }
.cb-code { white-space: pre; padding: 0 12px 0 8px; color: #1a1a1a; overflow-x: auto; scrollbar-width: none; }
.cb-code::-webkit-scrollbar { display: none; }
@media (prefers-color-scheme: dark) {
.cb { background: #1a1a1a; box-shadow: 0 0 0 1px #303030; }
.cb-head { background: #242424; border-bottom-color: #303030; }
.cb-lang { color: #f5f5f5; }
.cb-copy:hover { color: #f5f5f5; background: #242424; }
.cb-body::before { background: #303030; }
.cb-code { color: #f5f5f5; }
}