Doc Code
A code block with a searchable picker of 32 languages, line numbers you can turn off, line wrap, copy and a monospace editor. In the editor, ``` plus a space (or ```typescript) makes one, /code adds one, and code pasted from VS Code keeps its language.
npx shadcn@latest add @velaui/doc-codepnpm dlx velaui add @velaui/doc-codeyarn dlx shadcn@latest add @velaui/doc-codebunx --bun shadcn@latest add @velaui/doc-codeveni add doc-codeInstalls the copy of vela-ui that veni-builder serves to Venizia apps; veni-builder serves the Radix UI copy only, so this tab exists for that base alone.
npx shadcn@latest add @velaui/doc-codepnpm dlx velaui add @velaui/doc-codeyarn dlx shadcn@latest add @velaui/doc-codebunx --bun shadcn@latest add @velaui/doc-codeExamples
- Default
- Wrap
- Read only
Usage
Props
LanguageIcon
| Prop | Type | Default |
|---|---|---|
languagerequired | string | — |
Takes these and every prop of { language: string }.
DocCode
| Prop | Type | Default |
|---|---|---|
className | string | — |
language | string | "plain" |
languages | string[] | DOC_CODE_LANGUAGES |
lineNumbers | boolean | — |
onChange | (value: string) => void | — |
onLanguageChange | (language: string) => void | — |
onLineNumbersChange | (lineNumbers: boolean) => void | — |
onWrapChange | (wrap: boolean) => void | — |
readOnly | boolean | — |
textareaRef | ((instance: HTMLTextAreaElement | null) => void | (() => void | { [UNDEFINED_VOID_ONLY]: never; }) | React.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES[keyof React.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES]) | React.RefObject<HTMLTextAreaElement | null> | — |
valuerequired | string | — |
wrap | boolean | — |
Takes these and every prop of DocCodeProps.
Dependencies
- npm
- none besides React
- Installs with it
- Combobox20 icons
Source
components/ui/doc-code.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Box } from "@/components/icons/box";
import { Code04 } from "@/components/icons/code-04";
import { Data01 } from "@/components/icons/data-01";
import { FileCode } from "@/components/icons/file-code";
import { GitBranch01 } from "@/components/icons/git-branch-01";
import { Hash01 } from "@/components/icons/hash-01";
import { Hash02 } from "@/components/icons/hash-02";
import { Layers01 } from "@/components/icons/layers-01";
import { Package01 } from "@/components/icons/package-01";
import { Palette } from "@/components/icons/palette";
import { Server01 } from "@/components/icons/server-01";
import { AlignLeft01 } from "@/components/icons/align-left-01";
import { Brackets } from "@/components/icons/brackets";
import { BrowserCode } from "@/components/icons/browser-code";
import { Code01 } from "@/components/icons/code-01";
import { Code02 } from "@/components/icons/code-02";
import { Code03 } from "@/components/icons/code-03";
import { File } from "@/components/icons/file";
import { Layout01 } from "@/components/icons/layout-01";
import { Terminal } from "@/components/icons/terminal";
import {
Combobox,
ComboboxCollection,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@/components/ui/combobox";
import { cn } from "@/lib/utils";
export const DOC_CODE_LANGUAGES = [
"plain",
"javascript",
"typescript",
"jsx",
"tsx",
"python",
"java",
"c",
"cpp",
"csharp",
"go",
"rust",
"ruby",
"php",
"swift",
"kotlin",
"dart",
"lua",
"r",
"sql",
"html",
"css",
"scss",
"json",
"yaml",
"toml",
"xml",
"markdown",
"graphql",
"bash",
"powershell",
"dockerfile",
];
/** Words people type after ``` that are not the language's id. */
const LANGUAGE_ALIAS: Record<string, string> = {
js: "javascript",
ts: "typescript",
py: "python",
rb: "ruby",
rs: "rust",
golang: "go",
"c++": "cpp",
"c#": "csharp",
cs: "csharp",
kt: "kotlin",
sh: "bash",
shell: "bash",
zsh: "bash",
ps1: "powershell",
yml: "yaml",
md: "markdown",
text: "plain",
txt: "plain",
};
/** The language a typed word (``` fence) names, or undefined when it is not one of the block's languages. */
export function languageFromWord(word: string, languages = DOC_CODE_LANGUAGES) {
const w = word.toLowerCase();
const id = LANGUAGE_ALIAS[w] ?? w;
return languages.includes(id) ? id : undefined;
}
/** VS Code language ids that differ from ours; plaintext means VS Code detected nothing. */
const VSCODE_LANGUAGE: Record<string, string | undefined> = {
javascriptreact: "jsx",
typescriptreact: "tsx",
jsonc: "json",
shellscript: "bash",
plaintext: undefined,
};
/**
* The language of code pasted from VS Code and editors built on it, which put `vscode-editor-data` on the clipboard.
* Undefined when the clipboard has none or its language is not one the block offers.
*/
export function languageFromClipboard(data: DataTransfer, languages = DOC_CODE_LANGUAGES) {
try {
const mode = (JSON.parse(data.getData("vscode-editor-data")) as { mode?: string }).mode ?? "";
return languageFromWord(mode in VSCODE_LANGUAGE ? (VSCODE_LANGUAGE[mode] ?? "") : mode, languages);
} catch {
return undefined;
}
}
/** A glyph per language from the icon set (it has no brand logos); unknown languages get the plain file. */
const LANGUAGE_ICON: Record<string, React.ComponentType<{ className?: string }>> = {
plain: File,
javascript: Code02,
jsx: Code02,
typescript: Code01,
tsx: Code01,
python: Code03,
java: Code04,
kotlin: Code04,
swift: Code04,
dart: Code04,
c: Hash01,
cpp: Hash01,
csharp: Hash02,
go: Server01,
rust: Box,
ruby: FileCode,
php: FileCode,
lua: FileCode,
r: FileCode,
sql: Data01,
html: BrowserCode,
css: Layout01,
scss: Palette,
json: Brackets,
yaml: Layers01,
toml: Layers01,
xml: FileCode,
markdown: AlignLeft01,
graphql: GitBranch01,
bash: Terminal,
powershell: Terminal,
dockerfile: Package01,
mermaid: GitBranch01,
};
export interface DocCodeProps {
value: string;
language?: string;
readOnly?: boolean;
onChange?: (value: string) => void;
onLanguageChange?: (language: string) => void;
languages?: string[];
/** Wrap long lines instead of scrolling sideways. Line numbers are hidden while wrapping, as they cannot follow. */
wrap?: boolean;
onWrapChange?: (wrap: boolean) => void;
/** Number the lines (default on). */
lineNumbers?: boolean;
onLineNumbersChange?: (lineNumbers: boolean) => void;
/** The textarea, for a host that moves focus into the block. */
textareaRef?: React.Ref<HTMLTextAreaElement>;
className?: string;
}
export function LanguageIcon({ language }: { language: string }) {
const Icon = LANGUAGE_ICON[language] ?? File;
return <Icon className="size-4" />;
}
const toggle =
"rounded px-1.5 py-0.5 hover:bg-bg-surface-2 aria-pressed:bg-bg-surface-2 aria-pressed:text-text-strong pointer-coarse:min-h-11 pointer-coarse:min-w-11";
/** A code block: language picker, line numbers, copy, and a monospace editor that scrolls sideways. */
function DocCode({
value,
language = "plain",
readOnly,
onChange,
onLanguageChange,
languages = DOC_CODE_LANGUAGES,
wrap,
onWrapChange,
lineNumbers,
onLineNumbersChange,
textareaRef,
className,
}: DocCodeProps) {
const [copied, setCopied] = React.useState(false);
const [ownWrap, setOwnWrap] = React.useState(false);
const [ownNumbers, setOwnNumbers] = React.useState(true);
const wrapped = wrap ?? ownWrap;
const numbered = (lineNumbers ?? ownNumbers) && !wrapped;
const lines = value.split("\n").length;
React.useEffect(() => {
if (!copied) return;
const t = setTimeout(() => setCopied(false), 1500);
return () => clearTimeout(t);
}, [copied]);
const copy = () => {
void navigator.clipboard?.writeText(value);
setCopied(true);
};
return (
<div className={cn("my-2 overflow-hidden rounded-md border border-border-default", className)}>
<div className="flex items-center gap-2 px-2 py-1 text-xs text-text-muted">
{readOnly || !onLanguageChange ? (
<span>{language}</span>
) : (
<Combobox autoHighlight items={languages} value={language} onValueChange={(v) => onLanguageChange(v ?? "plain")}>
<ComboboxInput
variant="fill"
className="h-6 w-32 text-xs"
placeholder="Search language"
aria-label="Code language"
/>
<ComboboxContent>
<ComboboxEmpty>No language found.</ComboboxEmpty>
<ComboboxList>
<ComboboxCollection>
{(l: string) => (
<ComboboxItem key={l} value={l}>
<LanguageIcon language={l} />
{l}
</ComboboxItem>
)}
</ComboboxCollection>
</ComboboxList>
</ComboboxContent>
</Combobox>
)}
<button
type="button"
aria-pressed={wrapped}
className={cn(toggle, "ml-auto")}
onClick={() => (setOwnWrap(!wrapped), onWrapChange?.(!wrapped))}
>
Wrap
</button>
<button
type="button"
aria-pressed={numbered}
disabled={wrapped}
className={cn(toggle, "disabled:opacity-40")}
onClick={() => (setOwnNumbers(!numbered), onLineNumbersChange?.(!numbered))}
>
Numbers
</button>
<button type="button" className={toggle} onClick={copy}>
{copied ? "Copied" : "Copy"}
</button>
<span role="status" className="sr-only">
{copied && "Copied to clipboard"}
</span>
</div>
<div className="flex font-mono text-sm leading-[1.6]">
{numbered && (
<div aria-hidden className="select-none px-2 py-2 text-right text-text-muted">
{Array.from({ length: lines }, (_, i) => (
<div key={i}>{i + 1}</div>
))}
</div>
)}
<textarea
ref={textareaRef}
wrap={wrapped ? "soft" : "off"}
rows={lines}
spellCheck={false}
aria-label={`Code block - ${language}`}
value={value}
readOnly={readOnly}
onChange={(e) => onChange?.(e.target.value)}
className={cn(
"min-w-0 flex-1 resize-none bg-transparent py-2 outline-none field-sizing-content",
numbered ? "pr-2" : "px-3",
wrapped ? "whitespace-pre-wrap" : "overflow-x-auto whitespace-pre",
)}
/>
</div>
</div>
);
}
export { DocCode };