bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
137 lines (130 loc) • 6.86 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { CheckIcon, CopyIcon } from "@radix-ui/react-icons";
import { useEffect, useRef, useState } from "react";
import { bundledLanguages, createHighlighter } from "shiki";
import { cn } from "../../lib/utils.js";
import { Button } from "../ui/button.js";
import { Card, CardContent, CardHeader } from "../ui/card.js";
export function CodeBlock({ language = "text", code, showCopy = true, title, filename, showLineNumbers = false, highlightLines = [], terminal = false, variant = "default", size = "default", theme = "github-dark", }) {
const [copied, setCopied] = useState(false);
const [html, setHtml] = useState("");
const codeRef = useRef(null);
// Highlight code using Shiki
useEffect(() => {
async function highlightCode() {
try {
const highlighter = await createHighlighter({
themes: [theme],
langs: Object.keys(bundledLanguages),
});
// Build decorations for line highlighting
const decorations = highlightLines.map((lineNumber) => {
const lineIndex = lineNumber - 1;
return {
start: { line: lineIndex, character: 0 },
end: { line: lineIndex + 1, character: 0 }, // Start of next line
properties: { class: "highlighted-line" },
tagName: "span",
};
});
const highlighted = highlighter.codeToHtml(code || "", {
lang: language,
theme,
decorations: decorations.length > 0 ? decorations : undefined,
});
setHtml(highlighted);
}
catch (error) {
console.error("Failed to highlight code:", error);
// Fallback to plain text
const safeCode = code || "";
setHtml(`<pre><code>${safeCode.replace(/</g, "<").replace(/>/g, ">")}</code></pre>`);
}
}
highlightCode();
}, [code, language, theme, highlightLines]);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(code || "");
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
catch (err) {
console.error("Failed to copy code:", err);
}
};
// Size variants for padding
const sizeClasses = {
sm: "p-3",
default: "p-4",
lg: "p-6",
};
// Custom styles for Shiki output
const shikiStyles = `
.shiki {
margin: 0;
padding: 0.75rem;
border-radius: ${terminal ? "0" : "0.375rem"};
overflow-x: auto;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
}
.shiki code {
display: block;
width: fit-content;
min-width: 100%;
}
.shiki .line {
display: inline-block;
width: 100%;
}
.shiki .line.highlighted {
background-color: hsl(var(--muted) / 0.5);
margin: 0 -0.75rem;
padding: 0 0.75rem;
}
${showLineNumbers
? `
.shiki code {
counter-reset: line;
}
.shiki .line::before {
counter-increment: line;
content: counter(line);
display: inline-block;
width: 3ch;
margin-right: 1.5ch;
text-align: right;
color: hsl(var(--muted-foreground));
user-select: none;
}
`
: ""}
/* Highlighted line styles */
.highlighted-line {
background-color: hsl(var(--accent));
border-left: 2px solid hsl(var(--primary));
margin: 0 -1rem;
padding: 0 1rem;
display: block;
}
`;
// Terminal mode rendering
if (terminal) {
return (_jsxs("div", { className: "relative rounded-lg overflow-hidden border bg-card", children: [_jsx("style", { dangerouslySetInnerHTML: { __html: shikiStyles } }), _jsxs("div", { className: "flex items-center justify-between px-4 py-3 bg-muted border-b", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "w-3 h-3 rounded-full bg-destructive" }), _jsx("div", { className: "w-3 h-3 rounded-full bg-secondary" }), _jsx("div", { className: "w-3 h-3 rounded-full bg-primary" })] }), (filename || title) && (_jsx("div", { className: "flex items-center gap-2", children: _jsx("span", { className: "text-sm text-foreground", children: filename || title }) }))] }), showCopy && (_jsx(Button, { size: "sm", variant: "ghost", onClick: handleCopy, className: "h-7 px-2 opacity-70 hover:opacity-100 transition-opacity", children: copied ? (_jsxs(_Fragment, { children: [_jsx(CheckIcon, { className: "h-3.5 w-3.5 mr-1" }), "Copied"] })) : (_jsxs(_Fragment, { children: [_jsx(CopyIcon, { className: "h-3.5 w-3.5 mr-1" }), "Copy"] })) }))] }), _jsx("div", { ref: codeRef, className: "p-4",
// biome-ignore lint/security/noDangerouslySetInnerHtml: Required for Shiki syntax highlighting
dangerouslySetInnerHTML: { __html: html } })] }));
}
// Regular mode rendering
const cardVariantClasses = {
default: "",
ghost: "border-0 shadow-none",
outline: "shadow-none",
};
return (_jsxs(Card, { className: cn("overflow-hidden", cardVariantClasses[variant]), children: [_jsx("style", { dangerouslySetInnerHTML: { __html: shikiStyles } }), (title || filename || showCopy) && (_jsx(CardHeader, { className: cn("pb-3", sizeClasses[size]), children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [title && _jsx("h3", { className: "text-base font-medium", children: title }), filename && (_jsx("p", { className: "text-sm text-muted-foreground", children: filename }))] }), showCopy && (_jsx(Button, { size: "sm", variant: "ghost", onClick: handleCopy, className: "h-8 px-2", children: copied ? (_jsxs(_Fragment, { children: [_jsx(CheckIcon, { className: "h-3.5 w-3.5 mr-1" }), "Copied"] })) : (_jsxs(_Fragment, { children: [_jsx(CopyIcon, { className: "h-3.5 w-3.5 mr-1" }), "Copy"] })) }))] }) })), _jsx(CardContent, { className: cn("p-0", !title && !filename && !showCopy && sizeClasses[size]), children: _jsx("div", { ref: codeRef,
// biome-ignore lint/security/noDangerouslySetInnerHtml: Required for Shiki syntax highlighting
dangerouslySetInnerHTML: { __html: html } }) })] }));
}
// Re-export for convenience
export default CodeBlock;