58 lines · 2.2 KB
import { useCopy } from "~/hooks/use-copy";
import { highlightLine, type SnippetLanguage, type TokenKind } from "~/lib/snippet-highlight";
import { cn } from "~/utils/cn";

type Props = { title?: string; source: string; language?: SnippetLanguage; className?: string };

const TOKEN_COLOR: Record<TokenKind, string | undefined> = {
  comment: "var(--sh-comment)",
  string: "var(--sh-string)",
  keyword: "var(--sh-keyword)",
  property: "var(--sh-property)",
  entity: "var(--sh-entity)",
  sign: "var(--sh-sign)",
  plain: undefined,
};

function Line({ line, language }: { line: string; language?: SnippetLanguage }) {
  if (!line) return <span className="block"> </span>;
  if (!language) return <span className="block">{line}</span>;
  return (
    <span className="block">
      {highlightLine(line, language).map((token, index) => (
        // oxlint-disable-next-line react/no-array-index-key -- tokens are static and may repeat
        <span key={index} style={{ color: TOKEN_COLOR[token.kind] }}>
          {token.text}
        </span>
      ))}
    </span>
  );
}

/** Read-only snippet with a title bar and copy button, highlighted when `language` is set. */
export function CodeBlock({ title, source, language, className }: Props) {
  const { copied, copy } = useCopy();
  return (
    <figure className={cn("overflow-hidden rounded-lg border border-kumo-hairline bg-kumo-canvas", className)}>
      <figcaption className="flex items-center justify-between border-b border-kumo-hairline bg-kumo-base/60 px-4 py-2">
        <span className="font-mono text-xs text-kumo-subtle">{title}</span>
        <button
          type="button"