-

Code Editor

PreviousNext

A lightweight code editing surface for ZGI workflow code, JSON, SQL, and transform steps.

Installation

pnpm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/code-editor.json
npm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/code-editor.json
yarn
yarn dlx shadcn@latest add https://ui.zgi.ai/r/code-editor.json

Component

components/ui/code-editor.tsx
"use client"

import * as React from "react"
import { CheckIcon, CopyIcon } from "lucide-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

export type CodeEditorLanguage =
  | "javascript"
  | "typescript"
  | "python"
  | "json"
  | "sql"

export interface CodeEditorProps
  extends Omit<
    React.ComponentProps<"textarea">,
    "onChange" | "title" | "value"
  > {
  value: string
  onValueChange?: (value: string) => void
  language?: CodeEditorLanguage
  onLanguageChange?: (language: CodeEditorLanguage) => void
  languages?: CodeEditorLanguage[]
  title?: React.ReactNode
  showCopyButton?: boolean
  showLanguageSelector?: boolean
  editorClassName?: string
}

const languageLabels: Record<CodeEditorLanguage, string> = {
  javascript: "JavaScript",
  typescript: "TypeScript",
  python: "Python",
  json: "JSON",
  sql: "SQL",
}

export function CodeEditor({
  value,
  onValueChange,
  language = "typescript",
  onLanguageChange,
  languages = ["typescript", "javascript", "python", "json", "sql"],
  title = "Code",
  showCopyButton = true,
  showLanguageSelector = true,
  readOnly,
  disabled,
  className,
  editorClassName,
  ...props
}: CodeEditorProps) {
  const [copied, setCopied] = React.useState(false)
  const lines = React.useMemo(() => value.split("\n"), [value])

  async function copyCode() {
    await navigator.clipboard.writeText(value)
    setCopied(true)
    window.setTimeout(() => setCopied(false), 1200)
  }

  return (
    <div
      className={cn(
        "border-border bg-card text-card-foreground overflow-hidden rounded-lg border shadow-sm",
        className
      )}
    >
      <div className="border-border bg-muted/30 flex min-h-11 items-center justify-between gap-3 border-b px-3">
        <div className="min-w-0 text-sm font-medium">{title}</div>
        <div className="flex shrink-0 items-center gap-2">
          {showLanguageSelector ? (
            <Select
              value={language}
              onValueChange={(next) =>
                onLanguageChange?.(next as CodeEditorLanguage)
              }
              disabled={disabled || readOnly}
            >
              <SelectTrigger className="h-8 w-34 rounded-md text-xs">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                {languages.map((item) => (
                  <SelectItem key={item} value={item}>
                    {languageLabels[item]}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          ) : null}
          {showCopyButton ? (
            <Button
              type="button"
              variant="ghost"
              size="icon"
              className="size-8 rounded-md"
              onClick={copyCode}
              aria-label="Copy code"
            >
              {copied ? (
                <CheckIcon className="size-4" />
              ) : (
                <CopyIcon className="size-4" />
              )}
            </Button>
          ) : null}
        </div>
      </div>
      <div className="grid grid-cols-[auto_minmax(0,1fr)]">
        <pre
          aria-hidden="true"
          className="text-muted-foreground border-border bg-muted/20 min-w-10 border-r py-3 pr-3 pl-4 text-right font-mono text-xs leading-6 select-none"
        >
          {lines.map((_, index) => (
            <span key={index} className="block">
              {index + 1}
            </span>
          ))}
        </pre>
        <textarea
          value={value}
          onChange={(event) => onValueChange?.(event.target.value)}
          readOnly={readOnly}
          disabled={disabled}
          spellCheck={false}
          className={cn(
            "text-foreground min-h-72 w-full resize-y bg-transparent p-3 font-mono text-sm leading-6 outline-none",
            "placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
            editorClassName
          )}
          {...props}
        />
      </div>
    </div>
  )
}

Usage

import { CodeEditor } from "@/components/ui/code-editor"
 
export function Example() {
  return (
    <CodeEditor
      value="const result = input"
      language="typescript"
      onValueChange={console.log}
    />
  )
}

Props

PropTypeDefault
valuestringrequired
onValueChange(value: string) => void-
languageCodeEditorLanguage"typescript"
onLanguageChange(language) => void-
showCopyButtonbooleantrue
showLanguageSelectorbooleantrue