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.

Signaler un bug

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.json

Source

"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.

Dependances

  • @vllnt/ui@^0.3.0