UNPKG

bigblocks

Version:

Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React

137 lines (130 loc) 6.86 kB
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, "&lt;").replace(/>/g, "&gt;")}</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;