Delta Components
uicodesource: ok · refreshed
Code
Syntax-highlighted code with a copy button, a filename bar, and a package-manager tab strip.
npx shadcn add "https://deltacomponents.dev/r/code.json"free
Visual style
terminal-mono
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightprism-react-rendererprismjs
Registry dependencies
https://www.deltacomponents.dev/r/utils.jsonhttps://www.deltacomponents.dev/r/button.jsonhttps://www.deltacomponents.dev/r/tokens.jsonhttps://www.deltacomponents.dev/r/clipboard.json
Files3 · source cached 10/7/2026, 3:18:49 PM
registry/ui/code.tsx
"use client"
import * as React from "react"
import { useEffect, useState } from "react"
import { Highlight } from "prism-react-renderer"
import type { PrismTheme } from "prism-react-renderer"
// Side-effect import: registers extra Prism grammars (bash, docker,
// java, ruby, php, …) on the shared instance. Must come before any
// ``<Highlight>`` usage in this module.
import "@/lib/prism-languages"
import { cn } from "@/lib/utils"
import { copyToClipboard } from "@/lib/clipboard"
import { getIconForFile } from "@/components/ui/code-icons"
import { Button } from "@/components/ui/button"
type PackageManager = "npm" | "yarn" | "pnpm" | "bun"
/** Every group the two built-in Prism palettes used to carry as hardcoded hex,
* now a named slot in the CSS palette (`--code-token-<key>`, declared in
* globals.css §8). One list drives two things: the built-in theme below
* (each group's color is just `var(--code-token-<key>)`, resolved by the
* `.dark` class with no JS), and `resolveGroupColor` — which walks a
* consumer-supplied `adaptiveTheme` palette to find the color it assigns
* each group, so that palette can be resolved the same CSS-only way. */
const tokenGroups: { key: string; types: string[]; fontStyle?: "italic" }[] = [
{ key: "comment", types: ["comment"], fontStyle: "italic" },
{ key: "keyword", types: ["keyword"] },
{ key: "property", types: ["property", "property-access", "attr-name"] },
{ key: "tag", types: ["tag"] },
{ key: "punctuation", types: ["punctuation", "symbol", "dom", "operator"] },
{ key: "definition", types: ["definition"] },
{ key: "function", types: ["function"] },
{ key: "string", types: ["string", "char", "attr-value"] },
{ key: "static", types: ["static", "number"] },
{ key: "variable", types: ["variable", "parameter"] },
{ key: "builtin", types: ["builtin", "function-definition"] },
{ key: "class-name", types: ["class-name"] },
]
/** The built-in syntax palette. Every color is a CSS variable reference —
* light values on `:root`, dark overrides on `.dark` (globals.css §8) — so
* the right palette is already in effect before this ever renders. Merging
* the old separate dark/light `PrismTheme`s into one is what makes that
* possible: previously the choice of *which* theme object to use lived in
* React state, guaranteeing a light-palette frame on every mount. */
const builtinTheme: PrismTheme = {
plain: { color: "var(--code-fg)", backgroundColor: "var(--code-bg)" },
styles: tokenGroups.map(({ key, types, fontStyle }) => ({
types,
style: {
color: `var(--code-token-${key})`,
...(fontStyle ? { fontStyle } : {}),
},
})),
}
/** The color a `PrismTheme` assigns a token group, mirroring how
* prism-react-renderer resolves overlapping style entries: the last one
* naming the type wins. Falls back to the theme's own plain color for a
* group it doesn't style at all — used to project a consumer's
* `adaptiveTheme` onto the same `--code-token-*` slots the built-in palette
* uses. */
function resolveGroupColor(theme: PrismTheme, types: string[]): string {
for (let i = theme.styles.length - 1; i >= 0; i--) {
const entry = theme.styles[i]
if (types.some((t) => entry.types.includes(t)))
return entry.style.color ?? theme.plain?.color ?? ""
}
return theme.plain?.color ?? ""
}
interface CodeProps extends Omit<React.ComponentProps<"div">, "children"> {
npm?: string
yarn?: string
pnpm?: string
bun?: string
defaultPackageManager?: PackageManager
code?: string
css?: string
language?: string
defaultLanguage?: string
filename?: string
showLineNumbers?: boolean
theme?: PrismTheme
adaptiveTheme?: {
light: PrismTheme
dark: PrismTheme
}
/** Paint the header and code surface from the Prism theme's own background
* instead of the page's card token. Defaults to true whenever `theme` or
* `adaptiveTheme` is supplied — a palette brought from elsewhere carries its
* own ground, and pairing its colours with the page's card is what makes a
* dark theme read washed out on a light page. */
useThemeBackground?: boolean
scrollbar?: boolean
expandable?: boolean
defaultExpanded?: boolean
collapsedHeight?: string
/** Label on the affordance that opens a collapsed block. Docs previews say
* "View code"; a bare code block says "Expand". Expanding is one-way —
* there is no collapse control, so a reader never has to re-open what they
* just opened. A page load starts collapsed again. */
expandLabel?: string
}
/** The code's own type size, set on the block's ROOT rather than on the
* `<pre>`, so every part of the block inherits it — the highlighted source,
* the line-number gutter, the filename bar and the package-manager tabs. That
* is what makes a single `text-sm` in `className` resize the whole block:
* tailwind-merge drops this default in favour of the caller's utility, where
* a size declared further down would have won on specificity and left the
* parts at different sizes. */
const CODE_TEXT_SIZE = "text-[14px]"
const monoFontFamily =
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
function parseMarkdownCodeBlock(input: string): {
code: string
language: string | null
} {
const markdownRegex = /^```(\w+)?\n?([\s\S]*?)```$/
const match = input.trim().match(markdownRegex)
if (match) {
return {
language: match[1] || null,
code: match[2]?.trim() || "",
}
}
return { code: input, language: null }
}
const languageAliases: Record<string, string> = {
py: "python",
js: "javascript",
ts: "typescript",
tsx: "tsx",
jsx: "jsx",
rb: "ruby",
rs: "rust",
sh: "bash",
shell: "bash",
zsh: "bash",
console: "bash",
yml: "yaml",
md: "markdown",
dockerfile: "docker",
Dockerfile: "docker",
ps1: "powershell",
ps: "powershell",
}
const packageManagerCommands: Record<string, PackageManager> = {
npx: "npm",
npm: "npm",
yarn: "yarn",
pnpm: "pnpm",
bunx: "bun",
bun: "bun",
}
function resolveLanguage(lang: string): string {
return languageAliases[lang.toLowerCase()] || lang.toLowerCase()
}
function detectPackageManagerFromMarkdown(input: string): {
isPackageManager: boolean
command: string
manager: PackageManager
} | null {
const parsed = parseMarkdownCodeBlock(input)
if (!parsed.language) return null
const lang = parsed.language.toLowerCase()
const manager = packageManagerCommands[lang]
if (manager) {
return {
isPackageManager: true,
command: parsed.code,
manager,
}
}
return null
}
function convertNpxToPackageManagers(npxCommand: string): {
npm: string
yarn: string
pnpm: string
bun: string
} {
// Handle patterns like "npx shadcn@latest add button" or just "shadcn@latest add button"
let command = npxCommand.trim()
// Remove leading "npx " if present
if (command.startsWith("npx ")) {
command = command.slice(4)
}
return {
npm: `npx ${command}`,
yarn: `yarn dlx ${command}`,
pnpm: `pnpm dlx ${command}`,
bun: `bunx ${command}`,
}
}
/* ------------------------------------------------------------------
* Icons
* ------------------------------------------------------------------
* Inlined rather than imported from lucide-react. Three glyphs are not worth
* an install-time dependency on a consumer's project: the registry item lists
* what `shadcn add` installs, and every package there is one the reader did
* not choose. The paths are lucide's own (ISC), drawn at its 24px grid so they
* sit correctly beside icons a consumer does bring from it.
*/
interface IconProps {
className?: string
style?: React.CSSProperties
}
function iconProps({ className, style }: IconProps) {
return {
className,
style,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 2,
strokeLinecap: "round" as const,
strokeLinejoin: "round" as const,
"aria-hidden": true,
}
}
function CopyIcon(props: IconProps) {
return (
<svg {...iconProps(props)}>
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
)
}
function CheckIcon(props: IconProps) {
return (
<svg {...iconProps(props)}>
<path d="M20 6 9 17l-5-5" />
</svg>
)
}
function TerminalIcon(props: IconProps) {
return (
<svg {...iconProps(props)}>
<polyline points="4 17 10 11 4 5" />
<line x1="12" x2="20" y1="19" y2="19" />
</svg>
)
}
/** The tone every piece of chrome sitting ON the code surface shares — the
* copy glyph, the filename and its file badge, the package-manager tabs — so
* they read as one set rather than several separately-tuned greys.
*
* Opacity on the INHERITED colour, never a colour of its own. 60% of a
* surface's own foreground IS the muted token: measured against both
* palettes it lands within 1/255 of `--muted-foreground` over `--card`, in
* light and in dark alike. This used to be two branches saying the same
* thing twice — the app's muted token when the block sat on the page's card,
* an opacity when it painted from its own palette — and only the opacity
* form follows the surface underneath it. Riding the inherited colour means
* a palette brought from elsewhere, AND a consumer recolouring the header
* with a className, both carry the chrome with them rather than stranding it
* on the page's grey.
*
* Hover is the only thing that brightens it — deliberately not touch. A phone
* never hovers, so pinning it to full strength there (which is what this used
* to do) left the copy glyph permanently darker than the filename it shares a
* row with.
*
* Both are complete literal class strings. Tailwind generates utilities by
* scanning source text, so a class assembled as `${modifier}:opacity-100`
* never reaches the stylesheet — a hover written that way here once did
* nothing at all. Keep them literal. */
const CHROME_REST = "opacity-60"
const CHROME_HOVER = "group-hover:opacity-100"
/** Copy ⇄ check swap: both glyphs stay mounted, stacked, and cross-morph —
* the leaver shrinks to a blurred point while the arriver grows out of one
* (same choreography as MP3Player's play/pause). The leaver takes the exit
* tier so the arriver is what the eye lands on. Spans, not the svgs, carry
* the transition: Button already owns `[&_svg]:transition` for its press dip. */
const ICON_SWAP =
"absolute inset-0 flex items-center justify-center opacity-0 scale-25 blur-[4px] transition-[opacity,scale,filter] duration-(--motion-moderate-exit) ease-spring data-[shown=true]:opacity-100 data-[shown=true]:scale-100 data-[shown=true]:blur-none data-[shown=true]:duration-(--motion-moderate)"
/** Copy-to-clipboard control for a code surface.
*
* Transparent at rest so it doesn't sit on the code as a visible chip; the
* ground arrives on hover, mixed against the block's own `--code-fg`/
* `--code-bg` (globals.css §8) rather than an app token, because a block
* carries its own light/dark palette independent of the page. Both vars are
* set once on the block's outer wrapper and inherited here, so this needs no
* colour prop of its own.
*
* A floating button (no filename bar to sit in) also gets that ground at
* rest on a touch device — there's no hover to reveal it otherwise, so
* without a resting ground the glyph lands on top of whatever the first
* line happens to be. The two in-header buttons already sit on a bar and
* stay transparent until touched or hovered. */
function CopyButton({
value,
className,
floating = false,
}: {
value: string
className?: string
/** Floats over the code with no header/figcaption underneath it. */
floating?: boolean
}) {
const [hasCopied, setHasCopied] = useState(false)
useEffect(() => {
if (!hasCopied) return
const timer = setTimeout(() => setHasCopied(false), 2000)
return () => clearTimeout(timer)
}, [hasCopied])
return (
<Button
data-slot="copy-button"
type="button"
variant="ghost"
size="icon-compact"
aria-label={hasCopied ? "Copied" : "Copy code"}
className={cn(
"group size-7 border-none shadow-none transition-colors",
"bg-transparent hover:bg-[var(--copy-button-hover-bg)] active:bg-[var(--copy-button-hover-bg)]",
// Opt out of the ghost variant's own muted/foreground pair. The glyph
// takes its colour from the bar it sits in, so recolouring the header
// carries it too — tailwind-merge drops the variant's classes for
// these, and the hover half has to be named or it snaps back on hover.
"text-inherit hover:text-inherit",
// No hover on a phone, so only the floating button — the one with no
// ground under it already — gets a resting chip there.
floating && "[@media(hover:none)]:bg-[var(--copy-button-rest-bg)]",
className
)}
style={
// Only the floating button, which has no header to take a colour
// from. In a header bar it inherits — which is what lets a caller
// recolour the bar and carry this glyph along with it.
floating ? ({ color: "var(--code-fg)" } as React.CSSProperties) : undefined
}
onClick={async () => {
// Only claim success when the copy actually landed.
if (await copyToClipboard(value)) setHasCopied(true)
}}
>
<span
className={cn(
"relative block size-3.5 transition-opacity",
CHROME_REST,
CHROME_HOVER,
hasCopied && "opacity-100"
)}
>
<span data-shown={!hasCopied} className={ICON_SWAP}>
<CopyIcon className="size-3.5" />
</span>
<span data-shown={hasCopied} className={ICON_SWAP}>
<CheckIcon className="size-3.5" />
</span>
</span>
</Button>
)
}
/** The highlighted source itself, with optional gutter line numbers. Shared by
* the expandable and plain wrappers so the two can't drift. */
function HighlightedCode({
code,
language,
theme,
showLineNumbers,
}: {
code: string
language: string
theme: PrismTheme
showLineNumbers: boolean
}) {
const lineNumberColor = theme.plain?.color
? `color-mix(in srgb, ${theme.plain.color} 35%, transparent)`
: "rgba(128, 128, 128, 0.35)"
return (
<Highlight theme={theme} code={code.trim()} language={language}>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre
data-slot="code-pre"
className={cn(
className,
// w-max so the block grows to the longest line, min-w-full so it
// still fills the scroller when every line is short. Together they
// give the rows below a width to stretch to.
"relative w-max min-w-full py-3.5 outline-none",
// Unitless leading rather than `leading-6`: it lands on the same
// 24px at the default 14px, but a `text-*` override on the root
// now scales the rows with the type instead of leaving them
// pinned to a height the larger text overflows.
"leading-[1.7143] font-normal"
)}
style={
{
...style,
fontFamily: monoFontFamily,
backgroundColor: "transparent",
MozOsxFontSmoothing: "grayscale",
"--line-number-color": lineNumberColor,
} as React.CSSProperties
}
>
{tokens.map((line, i) => (
<div
key={i}
{...getLineProps({ line })}
// A sticky child can only stay put inside its own containing
// block. Left to size on its content, a short line's row ended
// before the scroll did and its number slid away with it; at the
// full block width every gutter survives the whole scroll.
className="relative flex min-h-[1.7143em] w-max min-w-full"
>
{showLineNumbers && (
// Sticky so the gutter survives a horizontal scroll; it paints
// the block background so code scrolls *under* it, not through.
<span
data-slot="code-gutter"
className="sticky left-0 z-10 w-16 flex-shrink-0 pr-6 pl-6 text-right font-medium tabular-nums select-none"
style={{
fontFamily: monoFontFamily,
color: lineNumberColor,
backgroundColor: "var(--code-bg)",
}}
>
{i + 1}
</span>
)}
<span
className={cn(
"flex-1 pr-6 whitespace-pre",
!showLineNumbers && "pl-6"
)}
>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token })} />
))}
</span>
</div>
))}
</pre>
)}
</Highlight>
)
}
const COLLAPSED_ROW_CAP = 40
export function Code({
npm,
yarn,
pnpm,
bun,
defaultPackageManager = "npm",
code,
css,
language = "typescript",
defaultLanguage,
filename,
showLineNumbers = true,
theme,
adaptiveTheme,
useThemeBackground,
className,
scrollbar = true,
expandable = false,
defaultExpanded = false,
collapsedHeight = "12rem",
expandLabel = "Expand",
style,
...props
}: CodeProps) {
const [packageManager, setPackageManager] = React.useState<PackageManager>(
defaultPackageManager
)
const [isExpanded, setIsExpanded] = useState(defaultExpanded)
// A caller-supplied palette brings its own background; the built-in pair is
// designed to sit flush with --card, so only that one defaults to the token.
const paintFromTheme = useThemeBackground ?? Boolean(theme || adaptiveTheme)
const pageSurfaceVars = { color: "var(--foreground)", backgroundColor: "var(--card)" }
// `adaptiveTheme` is the one case that still varies by resolved theme, but
// the choice is made by the `.dark` class (see `wrapperVars` below), not by
// reading which theme is on screen — so `selectedTheme` itself never
// branches on light vs. dark.
//
// Without the theme's ground, its plain colour can't be trusted against the
// page surface either — a dark palette's near-white body text would land on
// a light card. Tokens keep their colours; unstyled text falls back.
const selectedTheme =
theme && !paintFromTheme
? { ...theme, plain: { ...theme.plain, ...pageSurfaceVars } }
: theme || builtinTheme
// The palette's foreground/background, set once on the outer wrapper so the
// copy button's resting/hover ground (globals.css §8) can mix against a
// block's real surface instead of `transparent`. `adaptiveTheme` sets both
// halves of every pair here too, and `.code-adaptive-theme` (also in §8)
// is what lets `.dark` pick between them without JS.
const wrapperVars: Record<string, string> = adaptiveTheme
? {
"--code-fg-l": adaptiveTheme.light.plain?.color ?? "",
"--code-fg-d": adaptiveTheme.dark.plain?.color ?? "",
"--code-bg-l": adaptiveTheme.light.plain?.backgroundColor ?? "",
"--code-bg-d": adaptiveTheme.dark.plain?.backgroundColor ?? "",
...Object.fromEntries(
tokenGroups.flatMap(({ key, types }) => [
[`--code-token-${key}-l`, resolveGroupColor(adaptiveTheme.light, types)],
[`--code-token-${key}-d`, resolveGroupColor(adaptiveTheme.dark, types)],
])
),
}
: {
"--code-fg": theme?.plain?.color ?? "var(--foreground)",
"--code-bg": theme?.plain?.backgroundColor ?? "var(--card)",
}
// The code surface paints straight from `--code-bg`, so turning the theme
// ground off has to move the variable itself — the header alone would leave
// the two halves of the block on different surfaces. Inline vars outrank the
// `.code-adaptive-theme` class rules, so this covers the adaptive pair too.
if (!paintFromTheme) {
wrapperVars["--code-fg"] = pageSurfaceVars.color
wrapperVars["--code-bg"] = pageSurfaceVars.backgroundColor
}
const packageManagerFromMarkdown = code
? detectPackageManagerFromMarkdown(code)
: null
const parsedMarkdown = code ? parseMarkdownCodeBlock(code) : null
const actualCode = css || (parsedMarkdown?.code ?? code)
const actualLanguage = css
? "css"
: parsedMarkdown?.language
? resolveLanguage(parsedMarkdown.language)
: defaultLanguage || language
// Two ways to get a package-manager block: an explicit npm/yarn/pnpm/bun
// prop set, or a ```npx fenced command we expand into all four. They render
// the same tab strip, so they resolve to one shape here rather than forking
// the JSX.
const explicit: Partial<Record<PackageManager, string>> = {
npm,
yarn,
pnpm,
bun,
}
const hasExplicit = Object.values(explicit).some(Boolean)
const pmCommands = packageManagerFromMarkdown
? convertNpxToPackageManagers(packageManagerFromMarkdown.command)
: hasExplicit
? explicit
: null
if (pmCommands) {
const keys = (["npm", "yarn", "pnpm", "bun"] as PackageManager[]).filter(
(key) => pmCommands[key]
)
const active = pmCommands[packageManager] ? packageManager : keys[0]
const surfaceStyle = paintFromTheme
? {
backgroundColor: selectedTheme.plain?.backgroundColor,
color: selectedTheme.plain?.color,
}
: undefined
return (
<div
data-slot="code"
className={cn(
"bg-card text-card-foreground border-border/60 overflow-hidden rounded-[var(--radius-bg,var(--radius,0.5rem))] border [background-clip:padding-box]",
CODE_TEXT_SIZE,
adaptiveTheme && "code-adaptive-theme",
className
)}
style={{ ...(wrapperVars as React.CSSProperties), ...style }}
{...props}
>
<div
data-slot="code-header"
className={cn(
"border-border/60 flex items-center justify-between border-b px-3 py-2",
!paintFromTheme && "bg-background"
)}
style={surfaceStyle}
>
<div className="flex items-center gap-2.5">
<TerminalIcon className={cn("size-4", CHROME_REST)} />
<div
role="tablist"
aria-label="Package manager"
data-slot="code-tablist"
className="flex items-center"
>
{keys.map((key) => (
<button
key={key}
type="button"
role="tab"
aria-selected={active === key}
data-state={active === key ? "active" : "inactive"}
data-slot="code-tab"
onClick={() => setPackageManager(key)}
className={cn(
"h-7 rounded-[var(--radius-item,var(--radius,0.5rem))] px-2.5 font-medium transition-opacity",
CHROME_REST,
"data-[state=active]:opacity-100",
"outline-none focus-visible:ring-1 focus-visible:ring-[color:var(--focus-ring,#6B97FF)]"
)}
style={{ fontFamily: monoFontFamily }}
>
{key}
</button>
))}
</div>
</div>
<CopyButton value={pmCommands[active] || ""} className="size-7" />
</div>
<div
data-slot="code-body"
className={cn("overflow-x-auto", !scrollbar && "scrollbar-hide")}
>
<div
className={cn("relative w-max min-w-full", !paintFromTheme && "bg-card")}
style={
paintFromTheme
? { backgroundColor: selectedTheme.plain?.backgroundColor }
: undefined
}
>
<Highlight
theme={selectedTheme}
code={pmCommands[active] || ""}
language="bash"
>
{({
className: highlightClassName,
style,
tokens,
getLineProps,
getTokenProps,
}) => (
<pre
data-slot="code-pre"
className={cn(
highlightClassName,
"w-max min-w-full px-4 py-4 leading-relaxed font-normal"
)}
style={{
...style,
fontFamily: monoFontFamily,
backgroundColor: paintFromTheme
? selectedTheme.plain?.backgroundColor
: "transparent",
WebkitFontSmoothing: "antialiased",
MozOsxFontSmoothing: "grayscale",
}}
>
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ line })}>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token })} />
))}
</div>
))}
</pre>
)}
</Highlight>
</div>
</div>
</div>
)
}
if (!actualCode) return null
// A collapsed block shows a handful of rows under `collapsedHeight`, and
// tokenising and mounting the other hundreds — several hundred milliseconds
// for a long source file — is what froze a tab switch that revealed one.
// Until it is expanded only the rows that can be seen are rendered; the
// copy button still carries the whole thing.
// ponytail: 40 rows covers a collapsedHeight of ~4× the default; derive
// the count from the height if a caller ever collapses taller than that.
const collapsed = expandable && !isExpanded
const visibleCode = collapsed
? actualCode.split("\n").slice(0, COLLAPSED_ROW_CAP).join("\n")
: actualCode
const highlighted = (
<HighlightedCode
code={visibleCode}
language={actualLanguage}
theme={selectedTheme}
showLineNumbers={showLineNumbers}
/>
)
return (
<div
data-slot="code"
className={cn(
"pointer-events-auto w-full max-w-full overflow-hidden",
"border-border/60 border [background-clip:padding-box]",
"rounded-[var(--radius-bg,var(--radius,0.5rem))]",
CODE_TEXT_SIZE,
adaptiveTheme && "code-adaptive-theme",
className
)}
style={{ ...(wrapperVars as React.CSSProperties), ...style }}
{...props}
>
{filename && (
<figcaption
data-slot="code-header"
className={cn(
"border-border/60 flex items-center justify-between border-b px-4 py-2.5",
!paintFromTheme && "bg-background"
)}
style={{
fontFamily: monoFontFamily,
...(paintFromTheme
? {
backgroundColor: selectedTheme.plain?.backgroundColor,
color: selectedTheme.plain?.color,
}
: {}),
}}
>
<div
data-slot="code-filename"
className={cn("flex items-center gap-2", CHROME_REST)}
>
{/* The file badge is a solid shape where the name beside it is
13px text, so at one shared colour it out-weighs the name it
belongs to and pulls the eye off both the label and the copy
button. The token stays the same; a notch of transparency is
what evens the three optically. */}
<span className="flex items-center opacity-70 [&_svg]:size-3.5">
{getIconForFile(filename)}
</span>
<span className="font-medium tracking-tight">{filename}</span>
</div>
<div className="flex items-center gap-2">
<CopyButton value={actualCode} />
</div>
</figcaption>
)}
<div className="relative">
{/* With no filename bar to live in, the controls float over the code.
The row is pointer-transparent so it doesn't eat text selection;
only the controls themselves take pointer events back. */}
{!filename && (
<div className="pointer-events-none absolute inset-x-0 top-0 z-10 flex justify-end">
<div className="pointer-events-auto flex items-center gap-2 p-3">
<CopyButton value={actualCode} floating />
</div>
</div>
)}
{expandable ? (
<div
data-slot="code-body"
className={cn(
"relative w-full transition-all",
isExpanded ? "overflow-auto" : "overflow-hidden",
!scrollbar && "scrollbar-hide"
)}
style={{
backgroundColor: "var(--code-bg)",
maxHeight: isExpanded ? "none" : collapsedHeight,
}}
>
{highlighted}
</div>
) : (
<div
data-slot="code-body"
className={cn(
"relative max-h-[450px] w-full overflow-auto",
!scrollbar && "scrollbar-hide"
)}
style={{ backgroundColor: "var(--code-bg)" }}
>
{highlighted}
</div>
)}
{expandable && !isExpanded && (
<div
data-slot="code-expand-overlay"
className="absolute inset-x-0 bottom-0 z-20 flex h-24 items-end justify-center rounded-b-[var(--radius-bg,var(--radius,0.5rem))] pb-4"
style={{
background: `linear-gradient(to top, ${
paintFromTheme && selectedTheme.plain?.backgroundColor
? selectedTheme.plain.backgroundColor
: "var(--card)"
} 40%, color-mix(in oklab, ${
paintFromTheme && selectedTheme.plain?.backgroundColor
? selectedTheme.plain.backgroundColor
: "var(--card)"
} 60%, transparent) 75%, transparent 100%)`,
}}
>
<Button
type="button"
data-slot="code-expand"
variant="tertiary"
size="compact"
onClick={() => setIsExpanded(true)}
className="relative z-10 shadow-none"
>
{expandLabel}
</Button>
</div>
)}
</div>
</div>
)
}
registry/ui/code-icons.tsx
import type React from "react"
type IconProps = React.HTMLAttributes<SVGElement>
export const Icons = {
ts: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75c.612 0 1.154.037 1.627.111a6.38 6.38 0 0 1 1.306.34v2.458a3.95 3.95 0 0 0-.643-.361 5.093 5.093 0 0 0-.717-.26 5.453 5.453 0 0 0-1.426-.2c-.3 0-.573.028-.819.086a2.1 2.1 0 0 0-.623.242c-.17.104-.3.229-.393.374a.888.888 0 0 0-.14.49c0 .196.053.373.156.529.104.156.252.304.443.444s.423.276.696.41c.273.135.582.274.926.416.47.197.892.407 1.266.628.374.222.695.473.963.753.268.279.472.598.614.957.142.359.214.776.214 1.253 0 .657-.125 1.21-.373 1.656a3.033 3.033 0 0 1-1.012 1.085 4.38 4.38 0 0 1-1.487.596c-.566.12-1.163.18-1.79.18a9.916 9.916 0 0 1-1.84-.164 5.544 5.544 0 0 1-1.512-.493v-2.63a5.033 5.033 0 0 0 3.237 1.2c.333 0 .624-.03.872-.09.249-.06.456-.144.623-.25.166-.108.29-.234.373-.38a1.023 1.023 0 0 0-.074-1.089 2.12 2.12 0 0 0-.537-.5 5.597 5.597 0 0 0-.807-.444 27.72 27.72 0 0 0-1.007-.436c-.918-.383-1.602-.852-2.053-1.405-.45-.553-.676-1.222-.676-2.005 0-.614.123-1.141.369-1.582.246-.441.58-.804 1.004-1.089a4.494 4.494 0 0 1 1.47-.629 7.536 7.536 0 0 1 1.77-.201zm-15.113.188h9.563v2.166H9.506v9.646H6.789v-9.646H3.375z"
/>
</svg>
),
css: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M1.5 0h21l-1.91 21.563L11.977 24l-8.565-2.438L1.5 0zm17.09 4.413L5.41 4.41l.213 2.622 10.125.002-.255 2.716h-6.64l.24 2.573h6.182l-.366 3.523-2.91.804-2.956-.81-.188-2.11h-2.61l.29 3.855L12 19.288l5.373-1.53L18.59 4.414z"
/>
</svg>
),
json: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M5.759 3.975h1.783V5.76H5.759v4.458A1.783 1.783 0 0 1 3.975 12a1.783 1.783 0 0 1 1.784 1.783v4.459h1.783v1.783H5.759c-.954-.24-1.784-.803-1.784-1.783v-3.567a1.783 1.783 0 0 0-1.783-1.783H.975v-1.783h1.217a1.783 1.783 0 0 0 1.783-1.784V5.76c0-.98.83-1.543 1.784-1.784m12.482 0c.954.24 1.784.803 1.784 1.783v3.567a1.783 1.783 0 0 0 1.783 1.783h1.217v1.784h-1.217a1.783 1.783 0 0 0-1.783 1.783v3.567c0 .98-.83 1.543-1.784 1.783h-1.783v-1.783h1.783v-4.459A1.783 1.783 0 0 1 20.025 12a1.783 1.783 0 0 1-1.784-1.783V5.759h-1.783V3.975z"
/>
</svg>
),
python: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M11.914 0C5.82 0 6.2 2.656 6.2 2.656l.007 2.752h5.814v.826H3.9S0 5.789 0 11.969c0 6.18 3.403 5.96 3.403 5.96h2.03v-2.867s-.109-3.42 3.35-3.42h5.766s3.24.052 3.24-3.148V3.202S18.28 0 11.914 0M8.708 1.85c.578 0 1.046.47 1.046 1.052 0 .581-.468 1.051-1.046 1.051-.579 0-1.046-.47-1.046-1.051 0-.582.467-1.052 1.046-1.052"
/>
<path
fill="currentColor"
d="M12.086 24c6.094 0 5.714-2.656 5.714-2.656l-.007-2.752h-5.814v-.826h8.121s3.9.445 3.9-5.735c0-6.18-3.403-5.96-3.403-5.96h-2.03v2.867s.109 3.42-3.35 3.42H9.451s-3.24-.052-3.24 3.148v5.292S5.72 24 12.086 24m3.206-1.85c-.578 0-1.046-.47-1.046-1.052 0-.581.468-1.051 1.046-1.051.579 0 1.046.47 1.046 1.051 0 .582-.467 1.052-1.046 1.052"
/>
</svg>
),
rust: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="22 22 100 100" {...props}>
<path
fill="currentColor"
d="m71.05 23.68c-26.06 0-47.27 21.22-47.27 47.27s21.22 47.27 47.27 47.27 47.27-21.22 47.27-47.27-21.22-47.27-47.27-47.27zm-.07 4.2a3.1 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11zm7.12 5.12a38.27 38.27 0 0 1 26.2 18.66l-3.67 8.28c-.63 1.43.02 3.11 1.44 3.75l7.06 3.13a38.27 38.27 0 0 1 .08 6.64h-3.93c-.39 0-.55.26-.55.64v1.8c0 4.24-2.39 5.17-4.49 5.4-2 .23-4.21-.84-4.49-2.06-1.18-6.63-3.14-8.04-6.24-10.49 3.85-2.44 7.85-6.05 7.85-10.87 0-5.21-3.57-8.49-6-10.1-3.42-2.25-7.2-2.7-8.22-2.7h-40.6a38.27 38.27 0 0 1 21.41-12.08l4.79 5.02c1.08 1.13 2.87 1.18 4 .09zm-44.2 23.02a3.11 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11zm74.15.14a3.11 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11zm-68.29.5h5.42v24.44h-10.94a38.27 38.27 0 0 1 -1.24-14.61l6.7-2.98c1.43-.64 2.08-2.31 1.44-3.74zm22.62.26h12.91c.67 0 4.71.77 4.71 3.8 0 2.51-3.1 3.41-5.65 3.41h-11.98zm0 17.56h9.89c.9 0 4.83.26 6.08 5.28.39 1.54 1.26 6.56 1.85 8.17.59 1.8 2.98 5.4 5.53 5.4h16.14a38.27 38.27 0 0 1 -3.54 4.1l-6.57-1.41c-1.53-.33-3.04.65-3.37 2.18l-1.56 7.28a38.27 38.27 0 0 1 -31.91-.15l-1.56-7.28c-.33-1.53-1.83-2.51-3.36-2.18l-6.43 1.38a38.27 38.27 0 0 1 -3.32-3.92h31.27c.35 0 .59-.06.59-.39v-11.06c0-.32-.24-.39-.59-.39h-9.15zm-14.43 25.33a3.11 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11zm46.05.14a3.11 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11z"
/>
<path
fill="currentColor"
fillRule="evenodd"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="3"
d="m115.68 70.95a44.63 44.63 0 0 1 -44.63 44.63 44.63 44.63 0 0 1 -44.63-44.63 44.63 44.63 0 0 1 44.63-44.63 44.63 44.63 0 0 1 44.63 44.63zm-.84-4.31 6.96 4.31-6.96 4.31 5.98 5.59-7.66 2.87 4.78 6.65-8.09 1.32 3.4 7.46-8.19-.29 1.88 7.98-7.98-1.88.29 8.19-7.46-3.4-1.32 8.09-6.65-4.78-2.87 7.66-5.59-5.98-4.31 6.96-4.31-6.96-5.59 5.98-2.87-7.66-6.65 4.78-1.32-8.09-7.46 3.4.29-8.19-7.98 1.88 1.88-7.98-8.19.29 3.4-7.46-8.09-1.32 4.78-6.65-7.66-2.87 5.98-5.59-6.96-4.31 6.96-4.31-5.98-5.59 7.66-2.87-4.78-6.65 8.09-1.32-3.4-7.46 8.19.29-1.88-7.98 7.98 1.88-.29-8.19 7.46 3.4 1.32-8.09 6.65 4.78 2.87-7.66 5.59 5.98 4.31-6.96 4.31 6.96 5.59-5.98 2.87 7.66 6.65-4.78 1.32 8.09 7.46-3.4-.29 8.19 7.98-1.88-1.88 7.98 8.19-.29-3.4 7.46 8.09 1.32-4.78 6.65 7.66 2.87z"
/>
</svg>
),
bash: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M21.038 4.9 13.461.402a2.86 2.86 0 0 0-2.923.001L2.961 4.9A3.023 3.023 0 0 0 1.5 7.503v8.995c0 1.073.557 2.066 1.462 2.603l7.577 4.497a2.86 2.86 0 0 0 2.922 0l7.577-4.497a3.023 3.023 0 0 0 1.462-2.603V7.503A3.021 3.021 0 0 0 21.038 4.9z"
/>
</svg>
),
html: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M1.5 0h21l-1.91 21.563L11.977 24l-8.564-2.438L1.5 0Zm7.031 9.75-.232-2.718 10.059.003.23-2.622L5.412 4.41l.698 8.01h9.126l-.326 3.426-2.91.804-2.955-.81-.188-2.11H6.248l.33 4.171L12 19.351l5.379-1.443.744-8.157H8.531Z"
/>
</svg>
),
react: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38a2.167 2.167 0 0 0-1.092-.278z"
/>
</svg>
),
go: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M1.811 10.715c-.047 0-.058-.024-.035-.059l.246-.316c.024-.035.078-.059.129-.059h4.17c.047 0 .058.035.035.07l-.199.305c-.02.035-.082.07-.117.07zm-1.35 1.073c-.047 0-.058-.023-.035-.058l.246-.316c.023-.035.082-.059.129-.059h5.328c.047 0 .07.035.059.07l-.094.28c-.012.046-.059.07-.106.07zm2.156 1.074c-.047 0-.058-.035-.035-.07l.164-.293c.023-.035.07-.07.117-.07h2.337c.047 0 .07.035.07.082l-.023.281c0 .047-.047.082-.082.082zM15.004 10.505c-.735.188-1.239.328-1.963.516-.176.047-.188.059-.34-.117-.176-.199-.305-.328-.551-.445-.738-.363-1.452-.258-2.115.176-.797.516-1.207 1.276-1.195 2.22.012.938.657 1.708 1.582 1.836.797.106 1.464-.164 1.992-.773.106-.129.2-.27.317-.434h-2.258c-.246 0-.305-.152-.223-.352.152-.363.434-.973.598-1.276.047-.082.14-.188.293-.188h4.253c-.024.317-.024.633-.07.95-.118.809-.4 1.558-.856 2.226-.703 1.027-1.64 1.734-2.844 2.033-1.002.246-1.968.176-2.868-.293-.856-.446-1.441-1.123-1.758-2.015-.363-1.027-.317-2.033.129-3.001.504-1.095 1.301-1.898 2.396-2.391 1.002-.445 2.045-.527 3.118-.258.797.199 1.476.61 2.01 1.229.164.199.305.41.48.656h.012zM18.989 16.847c-.925-.012-1.78-.27-2.52-.833-.633-.48-1.038-1.1-1.16-1.874-.176-1.12.152-2.092.867-2.938.668-.797 1.523-1.289 2.555-1.464 1.003-.164 1.945-.023 2.808.539.75.492 1.254 1.158 1.43 2.057.222 1.27-.118 2.32-.949 3.236-.609.68-1.37 1.135-2.25 1.265-.422.07-.845.07-1.278.082z"
/>
</svg>
),
markdown: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" {...props}>
<path
fill="currentColor"
d="M102 366C88 366 76 354 76 340V170c0-14 12-26 26-26h307c14 0 26 12 26 26v170c0 14-11 26-25 26zM102 162c-4 0-9 4-9 9v170c0 5 4 9 9 9h307c5 0 9-4 9-9V170c0-5-4-9-9-9H102zm26 153V196h34l34 43 34-43h35v119h-34v-68l-34 43-34-43v68zm216 0-52-57h34v-61h34v60h34z"
/>
</svg>
),
file: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"
/>
<polyline
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
points="14 2 14 8 20 8"
/>
</svg>
),
}
export function getIconForFile(filename: string) {
const extension = filename.split(".").pop()?.toLowerCase()
switch (extension) {
case "json":
return <Icons.json className="size-4 fill-current" />
case "css":
return <Icons.css className="size-4 fill-current" />
case "ts":
case "tsx":
case "typescript":
return <Icons.ts className="size-4 fill-current" />
case "js":
case "jsx":
return <Icons.react className="size-4 fill-current" />
case "py":
case "python":
return <Icons.python className="size-4 fill-current" />
case "rs":
case "rust":
return <Icons.rust className="size-4 fill-current" />
case "sh":
case "bash":
case "zsh":
return <Icons.bash className="size-4 fill-current" />
case "html":
case "htm":
return <Icons.html className="size-4 fill-current" />
case "go":
return <Icons.go className="size-4 fill-current" />
case "md":
case "mdx":
return <Icons.markdown className="size-4 fill-current" />
default:
return <Icons.file className="size-4" />
}
}
registry/lib/prism-languages.ts
/**
* Register additional Prism grammars on the shared instance used by
* ``prism-react-renderer``.
*
* ``prism-react-renderer`` ships with a small built-in set (JS/TS,
* Python, Go, Rust, JSON, YAML, CSS/HTML, SQL, Swift, Kotlin, etc.)
* and silently falls back to a plain-text render for anything not on
* that list. Each language-component file from ``prismjs/components``
* self-registers on ``globalThis.Prism`` as a side effect of being
* imported — so we expose the renderer's Prism instance there once,
* then ``require`` each grammar we want to support.
*
* Importing this module anywhere in the app is enough; the
* registrations are global and idempotent. Keep this file
* side-effect-only: no exports, no React, no SSR-unsafe globals.
*/
import { Prism } from "prism-react-renderer";
// ``prismjs`` grammars read from ``global.Prism`` (Node) or
// ``window.Prism`` (browser). ``globalThis`` resolves to whichever is
// available, so this works on both the server and the client.
(globalThis as typeof globalThis & { Prism?: typeof Prism }).Prism = Prism;
// Each ``require`` mutates ``globalThis.Prism.languages`` and returns
// nothing useful — the side effect is the point. Using ``require``
// rather than ``import`` because the grammar files are CommonJS and
// expect a synchronous registration order.
/* eslint-disable @typescript-eslint/no-require-imports */
// ``prism-markup-templating`` defines ``Prism.languages.markup-templating``
// and helpers (``tokenizePlaceholders``) that PHP / Smarty / Twig /
// EJS / ERB grammars look up at registration time. It must load
// **before** any of those.
require("prismjs/components/prism-markup-templating");
require("prismjs/components/prism-bash");
require("prismjs/components/prism-diff");
require("prismjs/components/prism-docker");
require("prismjs/components/prism-ini");
require("prismjs/components/prism-java");
require("prismjs/components/prism-nginx");
require("prismjs/components/prism-php");
require("prismjs/components/prism-powershell");
require("prismjs/components/prism-ruby");
require("prismjs/components/prism-toml");
/* eslint-enable @typescript-eslint/no-require-imports */