// CodeBlock — pretty wrapper for a `
` fragment with an
// optional filename header, language label, and a one-click copy
// button. Three rendering modes, in priority order:
// 1. `html` set → render pre-highlighted Shiki HTML verbatim.
// (The docs app's MDX pipeline pre-renders code
// fences at build time and feeds them here.)
// 2. `code` + `language` matching SHIKI_LANGS → lazy-load Shiki and
// highlight at runtime via .
// Until the WASM lands, the body falls back to
// plain escaped code so layout doesn't jump.
// 3. `code` only → plain , no highlighting.
//
// Browser-only behaviour (copy button) is opt-in: when `interactive:
// 'islands'` strips the rest of the JS, the button stays a static
// that doesn't do anything until JS lands; visually still
// looks right, just non-functional. That's the same compromise the
// rest of the kit makes.
//
// For Fumadocs-style tabs around a CodeBlock, use the `Tabs` primitive
// in this folder.
import { useState, type ReactNode } from 'react'
import { cn } from '../cn'
import { HighlightedCode, SHIKI_LANGS, type ShikiLang } from './highlightedCode'
const SHIKI_LANG_SET = SHIKI_LANGS as ReadonlyArray
const asShikiLang = (s: string | undefined): ShikiLang | null =>
s && SHIKI_LANG_SET.includes(s) ? (s as ShikiLang) : null
interface CodeBlockProps {
/** Filename label shown in the header. Optional. */
readonly filename?: string
/** Language label shown in the top-right. Optional. */
readonly language?: string
/** Pre-highlighted HTML (e.g. from Shiki) — rendered via
* dangerouslySetInnerHTML. Pick this OR `code`, not both. */
readonly html?: string
/** Plain code text — renders inside a styled . */
readonly code?: string
readonly className?: string
/** Copy-button caption + `aria-label` before a copy. Default `'Copy'` /
* `'Copy code'`. */
readonly copyLabel?: string
readonly copyAriaLabel?: string
/** Copy-button caption + `aria-label` after a successful copy. Default
* `'Copied'`. */
readonly copiedLabel?: string
readonly copiedAriaLabel?: string
}
export const CodeBlock = ({
filename, language, html, code, className,
copyLabel = 'Copy', copyAriaLabel = 'Copy code',
copiedLabel = 'Copied', copiedAriaLabel = 'Copied',
}: CodeBlockProps): ReactNode => {
const [copied, setCopied] = useState(false)
// Text used by the copy button: prefer the raw `code`; when only
// pre-rendered HTML is available, fall back to stripping tags at
// copy time.
const copyText = (): string => {
if (code) return code
if (html) {
const tmp = typeof document !== 'undefined' ? document.createElement('div') : null
if (!tmp) return ''
tmp.innerHTML = html
return tmp.textContent ?? ''
}
return ''
}
const doCopy = (): void => {
const text = copyText()
if (!text || typeof navigator === 'undefined' || !navigator.clipboard) return
navigator.clipboard.writeText(text).then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 1400)
}).catch(() => { /* clipboard denied; ignore */ })
}
return (
{(filename || language) ? (
{filename ? (
{filename}
) : }
{language ? (
{language}
) : null}
) : null}
{copied ? (
<>
{copiedLabel}
>
) : (
<>
{copyLabel}
>
)}
{html ? (
) : code && asShikiLang(language) ? (
) : (
{code}
)}
)
}