Vela UI Docsvenizia.ai

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-code
npx shadcn@latest add @velaui/doc-code

Examples

  • Default
  • Wrap
  • Read only

View code

Doc Code · Default

Installation

pnpm dlx velaui add @velaui/doc-code
pnpm dlx velaui add @velaui/doc-code

Code

doc-code-default.tsx
import * as React from "react";

import { DocCode } from "@/components/ui/doc-code";

const SAMPLES = [
  {
    language: "typescript",
    code: [
      "type Job<T> = () => Promise<T>;",
      "",
      "/** Run a job, retrying with exponential backoff. */",
      "export async function retry<T>(job: Job<T>, attempts = 4): Promise<T> {",
      "  let delay = 200;",
      "  for (let attempt = 1; ; attempt++) {",
      "    try {",
      "      return await job();",
      "    } catch (error) {",
      "      if (attempt >= attempts) throw error;",
      "      await new Promise(resolve => setTimeout(resolve, delay));",
      "      delay *= 2;",
      "    }",
      "  }",
      "}",
    ].join("\n"),
  },
  {
    language: "python",
    code: [
      "from collections import Counter",
      "from dataclasses import dataclass",
      "",
      "",
      "@dataclass(frozen=True)",
      "class Order:",
      "    customer: str",
      "    total: float",
      "",
      "",
      "def top_customers(orders: list[Order], n: int = 3) -> list[tuple[str, float]]:",
      '    """Customers with the highest spend, largest first."""',
      "    spend: Counter[str] = Counter()",
      "    for order in orders:",
      "        spend[order.customer] += order.total",
      "    return spend.most_common(n)",
    ].join("\n"),
  },
  {
    language: "go",
    code: [
      "package main",
      "",
      'import "sync"',
      "",
      "// Squares computes each square in its own goroutine.",
      "func Squares(nums []int) []int {",
      "\tout := make([]int, len(nums))",
      "\tvar wg sync.WaitGroup",
      "\tfor i, n := range nums {",
      "\t\twg.Add(1)",
      "\t\tgo func() {",
      "\t\t\tdefer wg.Done()",
      "\t\t\tout[i] = n * n",
      "\t\t}()",
      "\t}",
      "\twg.Wait()",
      "\treturn out",
      "}",
    ].join("\n"),
  },
];

export default function Example() {
  const [samples, setSamples] = React.useState(SAMPLES);
  const update = (index: number, change: Partial<(typeof SAMPLES)[number]>) =>
    setSamples(samples.map((s, i) => (i === index ? { ...s, ...change } : s)));
  return (
    <div className="w-full max-w-xl space-y-2 p-6">
      {samples.map((sample, i) => (
        <DocCode
          key={i}
          value={sample.code}
          language={sample.language}
          onChange={code => update(i, { code })}
          onLanguageChange={language => update(i, { language })}
        />
      ))}
    </div>
  );
}

View code

Doc Code · Wrap

Installation

pnpm dlx velaui add @velaui/doc-code
pnpm dlx velaui add @velaui/doc-code

Code

doc-code-wrap.tsx
import * as React from "react";

import { DocCode } from "@/components/ui/doc-code";

export default function Example() {
  const [value, setValue] = React.useState(
    "const message = 'A long line that needs sideways scrolling unless Wrap is on: ' + new Date().toISOString() + ' - and it keeps going';\nconsole.log(message);",
  );
  return (
    <div className="w-full max-w-xl p-6">
      <DocCode value={value} language="typescript" onChange={setValue} />
    </div>
  );
}

View code

Doc Code · Read only

Installation

pnpm dlx velaui add @velaui/doc-code
pnpm dlx velaui add @velaui/doc-code

Code

doc-code-read-only.tsx
import { DocCode } from "@/components/ui/doc-code";

const GO = [
  "package main",
  "",
  'import "sync"',
  "",
  "// Squares computes each square in its own goroutine.",
  "func Squares(nums []int) []int {",
  "\tout := make([]int, len(nums))",
  "\tvar wg sync.WaitGroup",
  "\tfor i, n := range nums {",
  "\t\twg.Add(1)",
  "\t\tgo func() {",
  "\t\t\tdefer wg.Done()",
  "\t\t\tout[i] = n * n",
  "\t\t}()",
  "\t}",
  "\twg.Wait()",
  "\treturn out",
  "}",
].join("\n");

export default function Example() {
  return (
    <div className="w-full max-w-xl p-6">
      <DocCode readOnly language="go" value={GO} />
    </div>
  );
}

Usage

import * as React from "react";

import { DocCode } from "@/components/ui/doc-code";

const SAMPLES = [
  {
    language: "typescript",
    code: [
      "type Job<T> = () => Promise<T>;",
      "",
      "/** Run a job, retrying with exponential backoff. */",
      "export async function retry<T>(job: Job<T>, attempts = 4): Promise<T> {",
      "  let delay = 200;",
      "  for (let attempt = 1; ; attempt++) {",
      "    try {",
      "      return await job();",
      "    } catch (error) {",
      "      if (attempt >= attempts) throw error;",
      "      await new Promise(resolve => setTimeout(resolve, delay));",
      "      delay *= 2;",
      "    }",
      "  }",
      "}",
    ].join("\n"),
  },
  {
    language: "python",
    code: [
      "from collections import Counter",
      "from dataclasses import dataclass",
      "",
      "",
      "@dataclass(frozen=True)",
      "class Order:",
      "    customer: str",
      "    total: float",
      "",
      "",
      "def top_customers(orders: list[Order], n: int = 3) -> list[tuple[str, float]]:",
      '    """Customers with the highest spend, largest first."""',
      "    spend: Counter[str] = Counter()",
      "    for order in orders:",
      "        spend[order.customer] += order.total",
      "    return spend.most_common(n)",
    ].join("\n"),
  },
  {
    language: "go",
    code: [
      "package main",
      "",
      'import "sync"',
      "",
      "// Squares computes each square in its own goroutine.",
      "func Squares(nums []int) []int {",
      "\tout := make([]int, len(nums))",
      "\tvar wg sync.WaitGroup",
      "\tfor i, n := range nums {",
      "\t\twg.Add(1)",
      "\t\tgo func() {",
      "\t\t\tdefer wg.Done()",
      "\t\t\tout[i] = n * n",
      "\t\t}()",
      "\t}",
      "\twg.Wait()",
      "\treturn out",
      "}",
    ].join("\n"),
  },
];

export default function Example() {
  const [samples, setSamples] = React.useState(SAMPLES);
  const update = (index: number, change: Partial<(typeof SAMPLES)[number]>) =>
    setSamples(samples.map((s, i) => (i === index ? { ...s, ...change } : s)));
  return (
    <div className="w-full max-w-xl space-y-2 p-6">
      {samples.map((sample, i) => (
        <DocCode
          key={i}
          value={sample.code}
          language={sample.language}
          onChange={code => update(i, { code })}
          onLanguageChange={language => update(i, { language })}
        />
      ))}
    </div>
  );
}

Props

LanguageIcon

PropTypeDefault
languagerequiredstring—

Takes these and every prop of { language: string }.

DocCode

PropTypeDefault
classNamestring—
languagestring"plain"
languagesstring[]DOC_CODE_LANGUAGES
lineNumbersboolean—
onChange(value: string) => void—
onLanguageChange(language: string) => void—
onLineNumbersChange(lineNumbers: boolean) => void—
onWrapChange(wrap: boolean) => void—
readOnlyboolean—
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>—
valuerequiredstring—
wrapboolean—

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 };