MDX Content

Affiche du contenu MDX avec correspondance de composants. Installez MDX Content depuis le registre VLLNT UI avec la CLI shadcn.

Signaler un bug

Affiche du contenu MDX avec correspondance de composants. 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 MDX Content 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/mdx-content.json

Source

import { evaluate } from "@mdx-js/mdx"; import type React from "react"; import * as runtime from "react/jsx-runtime"; import ReactMarkdown, { type Components } from "react-markdown"; import remarkGfm from "remark-gfm"; import { CodeBlock } from "../code-block/code-block"; import { StaticCode } from "../static-code/static-code"; type MDXContentProps = { // eslint-disable-next-line @typescript-eslint/no-explicit-any components?: Record<string, React.ComponentType<any>>; content: string; enableMDX?: boolean; }; function extractCodeText(node: React.ReactNode): string { return typeof node === "string" ? node.replace(/\n$/, "") : String(node ?? ""); } const MDXComponents: Components = { a: ({ children, href, ...props }: React.ComponentProps<"a">) => ( <a className="text-primary underline underline-offset-4 hover:text-primary/80 font-medium" href={href} {...props} > {children} </a> ), blockquote: ({ children, ...props }: React.ComponentProps<"blockquote">) => ( <blockquote className="border-l border-primary pl-4 italic text-muted-foreground my-6 py-2 text-sm" {...props} > {children} </blockquote> ), code: ({ children, className, ...props }: React.ComponentProps<"code">) => { if (typeof className === "string" && className.startsWith("language-")) { const language = className.replace(/^language-/, ""); const text = extractCodeText(children); return <StaticCode code={text} language={language} />; } return ( <code className="bg-muted px-1 py-0.5 rounded text-sm font-mono" {...props} > {children} </code> ); }, em: ({ children, ...props }: React.ComponentProps<"em">) => ( <em className="italic" {...props}> {children} </em> ), h1: ({ children, ...props }: React.ComponentProps<"h1">) => ( <h1 className="text-2xl font-semibold mt-8 mb-4" {...props}> {children} </h1> ), h2: ({ children, ...props }: React.ComponentProps<"h2">) => ( <h2 className="text-xl font-semibold mt-6 mb-3" {...props}> {children} </h2> ), h3: ({ children, ...props }: React.ComponentProps<"h3">) => ( <h3 className="text-lg font-semibold mt-4 mb-2" {...props}> {children} </h3> ), hr: ({ ...props }: React.ComponentProps<"hr">) => ( <hr className="my-8 border-border" {...props} /> ), li: ({ children, ...props }: React.ComponentProps<"li">) => ( <li className="mb-2 leading-relaxed text-muted-foreground text-sm pl-2" {...props} > {children} </li> ), ol: ({ children, ...props }: React.ComponentProps<"ol">) => ( <ol className="list-decimal list-outside mb-6 space-y-2 ml-6 text-muted-foreground text-sm" {...props} > {children} </ol> ), p: ({ children, ...props }: React.ComponentProps<"p">) => ( <p className="mb-6 leading-relaxed text-muted-foreground text-sm max-w-none" {...props} > {children} </p> ), pre: ({ children }: React.ComponentProps<"pre">) => ( <div className="contents">{children}</div> ), strong: ({ children, ...props }: React.ComponentProps<"strong">) => ( <strong className="font-semibold" {...props}> {children} </strong> ), ul: ({ children, ...props }: React.ComponentProps<"ul">) => ( <ul className="list-disc list-outside mb-6 space-y-2 ml-6 text-muted-foreground text-sm" {...props} > {children} </ul> ), }; const proseClasses = [ "prose prose-lg dark:prose-invert max-w-none", "prose-headings:font-bold prose-headings:tracking-tight", "prose-h1:text-2xl prose-h1:mt-8 prose-h1:mb-4", "prose-h2:text-xl prose-h2:mt-6 prose-h2:mb-3", "prose-h3:text-lg prose-h3:mt-4 prose-h3:mb-2", "prose-p:leading-relaxed prose-p:mb-6 prose-p:text-muted-foreground prose-p:text-sm prose-p:max-w-none", "prose-ul:my-6 prose-ul:ml-6 prose-ul:list-disc prose-ul:list-outside", "prose-ol:my-6 prose-ol:ml-6 prose-ol:list-decimal prose-ol:list-outside", "prose-li:mb-2 prose-li:leading-relaxed prose-li:pl-2", "prose-strong:font-semibold prose-em:italic", "prose-a:text-primary prose-a:underline prose-a:underline-offset-4 hover:prose-a:text-primary/80", "prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:font-mono", "prose-pre:my-6 prose-pre:overflow-x-auto prose-pre:rounded-lg prose-pre:border prose-pre:bg-zinc-950 prose-pre:py-4 prose-pre:font-mono prose-pre:text-sm prose-pre:text-white prose-pre:shadow-lg dark:prose-pre:bg-zinc-900", "prose-blockquote:border-l prose-blockquote:border-primary prose-blockquote:pl-4 prose-blockquote:italic prose-blockquote:text-muted-foreground prose-blockquote:my-6 prose-blockquote:py-2", "prose-hr:my-8 prose-hr:border-border", "prose-table:w-full prose-table:border-collapse prose-table:border prose-table:border-border", "prose-th:border prose-th:border-border prose-th:bg-muted prose-th:p-2 prose-th:text-left prose-th:font-medium", "prose-td:border prose-td:border-border prose-td:p-2", "prose-img:rounded-lg prose-img:border prose-img:border-border prose-img:shadow-lg", ].join(" "); const markdownPlugins = [remarkGfm]; function removeImportStatements( content: string, componentNames: string[], ): string { let processed = content.replaceAll(/^import\s+.*CodeBlock.*from.*$/gm, ""); componentNames.forEach((name) => { processed = processed.replaceAll( new RegExp(`^import\\s+.*${name}.*from.*$`, "gm"), "", ); }); return processed; } function buildCustomComponents( // eslint-disable-next-line @typescript-eslint/no-explicit-any injectedComponents: Record<string, React.ComponentType<any>>, // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Record<string, React.ComponentType<any>> { return { // eslint-disable-next-line @typescript-eslint/no-explicit-any CodeBlock: CodeBlock as React.ComponentType<any>, ...injectedComponents, }; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const EMPTY_MDX_COMPONENTS: Record<string, React.ComponentType<any>> = {}; export async function MDXContent({ components = EMPTY_MDX_COMPONENTS, content, enableMDX = true, }: MDXContentProps) { const componentNames = Object.keys(components); const processedContent = removeImportStatements(content, componentNames); const contentWithoutCodeBlocks = processedContent.replaceAll( /```[\S\s]*?```/g, "", ); const hasJSX = /<[A-Z][A-Za-z]*/.test(contentWithoutCodeBlocks); const customComponents = buildCustomComponents(components); const allComponents = { ...MDXComponents, ...customComponents }; if (enableMDX && hasJSX) { // eslint-disable-next-line @typescript-eslint/no-explicit-any let Component: React.ComponentType<{ components: any }> | undefined; try { const result = await evaluate(processedContent, { ...runtime, baseUrl: import.meta.url, }); Component = result.default; } catch (error) { console.error("Error rendering MDX:", error); } if (Component) { return ( <div className={proseClasses}> <Component components={allComponents} /> </div> ); } return ( <div className={proseClasses}> <ReactMarkdown components={allComponents} remarkPlugins={markdownPlugins} > {content} </ReactMarkdown> </div> ); } return ( <div className={proseClasses}> <ReactMarkdown components={allComponents} remarkPlugins={markdownPlugins}> {content} </ReactMarkdown> </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
  • @mdx-js/mdx@^3.1.1
  • react-markdown@^10.1.0
  • remark-gfm@^4.0.1