Code Block
Affichage de code avec coloration syntaxique et prise en charge de la copie. Installez Code Block depuis le registre VLLNT UI avec la CLI shadcn.
Affichage de code avec coloration syntaxique et prise en charge de la copie. Fait partie de la famille content dans VLLNT UI, il est publie comme entree de registre lisible par machine — copiez la source directement dans votre application avec la CLI shadcn et possedez-la, sans dependance d'execution a une bibliotheque de composants.
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.
Installation
Ajoutez Code Block a votre projet avec la CLI shadcn. La source arrive dans votre code, prete a etre adaptee :
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/code-block.jsonSource
"use client";
import {
type ComponentType,
type ReactNode,
useEffect,
useRef,
useState,
} from "react";
import { Check, Copy } from "lucide-react";
import { useTheme } from "next-themes";
import type { SyntaxHighlighterProps } from "react-syntax-highlighter";
import { cn } from "../../lib/utils";
import { Button } from "../button/button";
import { useCopyToClipboard } from "../copy-button/copy-button";
type PrismStyle = SyntaxHighlighterProps["style"];
type LoadedHighlighter = {
oneDark: PrismStyle;
oneLight: PrismStyle;
SyntaxHighlighter: ComponentType<SyntaxHighlighterProps>;
};
type CodeBlockProps = {
children: ReactNode;
className?: string;
language?: string;
showLanguage?: boolean;
};
function extractTextFromChildren(node: ReactNode): string {
if (typeof node === "string") {
return node;
}
if (typeof node === "number") {
return String(node);
}
if (Array.isArray(node)) {
return node.map(extractTextFromChildren).join("");
}
if (
node &&
typeof node === "object" &&
"props" in node &&
node.props &&
typeof node.props === "object" &&
"children" in node.props
) {
return extractTextFromChildren(node.props.children as ReactNode);
}
return String(node ?? "");
}
function findScrollableParent(
element: HTMLElement | null,
): HTMLElement | undefined {
if (!element) return undefined;
if (element.scrollHeight > element.clientHeight) return element;
return findScrollableParent(element.parentElement);
}
export function CodeBlock({
children,
className,
language = "typescript",
showLanguage = false,
}: CodeBlockProps) {
const { copied, copy } = useCopyToClipboard();
// react-syntax-highlighter (~10MB) is dynamic-imported on mount so the
// @vllnt/ui barrel's static graph never reaches it — barrel consumers that
// never render a CodeBlock ship zero bytes of it. Null until the chunk loads.
const [highlighter, setHighlighter] = useState<LoadedHighlighter | null>(
null,
);
const { systemTheme, theme } = useTheme();
const resolvedTheme = theme === "system" ? systemTheme : theme;
const isDark = resolvedTheme !== "light";
const code = extractTextFromChildren(children);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let active = true;
void Promise.all([
import("react-syntax-highlighter"),
import("react-syntax-highlighter/dist/esm/styles/prism"),
]).then(([module_, styles]) => {
if (!active) return;
setHighlighter({
oneDark: styles.oneDark,
oneLight: styles.oneLight,
SyntaxHighlighter: module_.Prism,
});
});
return () => {
active = false;
};
}, []);
useEffect(() => {
const element = scrollRef.current;
if (!element) return;
const onWheel = (event: WheelEvent) => {
if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return;
const scrollable = findScrollableParent(element);
if (scrollable) {
scrollable.scrollTop += event.deltaY;
event.preventDefault();
}
};
element.addEventListener("wheel", onWheel, { passive: false });
return () => {
element.removeEventListener("wheel", onWheel);
};
}, []);
const handleCopy = () => {
void copy(code);
};
const SyntaxHighlighter = highlighter?.SyntaxHighlighter;
const codeStyle = isDark ? highlighter?.oneDark : highlighter?.oneLight;
return (
<div
className={cn(
"relative w-full overflow-hidden rounded-md border bg-background",
className,
)}
>
<div
className="relative overflow-x-auto overflow-y-hidden touch-pan-y"
ref={scrollRef}
>
{SyntaxHighlighter ? (
<SyntaxHighlighter
codeTagProps={{
className: "font-mono text-sm",
style: {
background: "transparent",
display: "block",
},
}}
customStyle={{
background: "oklch(var(--background))",
fontSize: "0.875rem",
margin: 0,
minWidth: "fit-content",
overflowY: "hidden",
padding: "1rem",
}}
language={language}
style={codeStyle}
>
{code}
</SyntaxHighlighter>
) : (
<pre className="m-0 min-w-fit overflow-y-hidden p-4 font-mono text-sm">
<code className="block bg-transparent">{code}</code>
</pre>
)}
<div className="absolute right-2 top-2 flex items-center gap-2">
{showLanguage ? (
<span className="text-xs font-mono text-muted-foreground uppercase tracking-wider">
{language}
</span>
) : null}
<Button
aria-label={copied ? "Copied" : "Copy code"}
className="size-8"
onClick={handleCopy}
size="icon"
variant="ghost"
>
{copied ? (
<Check className="size-3" />
) : (
<Copy className="size-3" />
)}
</Button>
</div>
</div>
</div>
);
}
Stories
Explorez chaque variante et etat dans le Storybook interactif :
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.