nino

Command Palette

Search for a command to run...

メニュー

Codes

2025年01月15日

2025年01月15日

複数のコードスニペットをタブ形式で表示し、グループ化やコピー機能を備えた composable で themeable なコードコンポーネントです。

const Demo = () => {
  return <div>Demo</div>;
}

インストール

pnpx shadcn@latest add https://www.dninomiya.com/r/codes.json

CodeProvider の設置

グループの選択状態を記憶し、アプリケーション全体で同期するには、CodeProvider をレイアウトに設置する必要があります。

import { CodeProvider } from "@/registry/blocks/codes";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body>
        <CodeProvider>{children}</CodeProvider>
      </body>
    </html>
  );
}

CodeProvider は以下の機能を提供します:

  • グループ選択の永続化 - localStorage を使用してユーザーの選択を保存します。
  • ページ間の同期 - すべてのページで同じグループが選択された状態を維持します。
  • 複数のコードブロック間の同期 - 1つのコードブロックでグループを変更すると、他のすべてのコードブロックも同じグループに切り替わります。

例えば、npm/yarn/pnpm のようなパッケージマネージャーをグループで管理している場合、ユーザーが一度 pnpm を選択すれば、そのサイト内のすべてのインストールコマンドが pnpm で表示されるようになります。

Props

NameTypeDescription
defaultActiveGroupsstring[]デフォルトで選択されるグループの配列(オプション)

構造

Codes コンポーネントは以下のパーツで構成されています:

  • Codes - メインのコンテナコンポーネント。タブの状態を管理します。
  • CodeHeader - ヘッダー部分。タブやボタンを配置します。
  • CodeList - タブのリスト部分。
  • CodeTrigger - 個別のタブトリガー。
  • CodeContent - コードコンテンツのラッパー。
  • CodeDisplay - シンタックスハイライトされたコードを表示。
  • CodeCopyButton - コピーボタン。
  • CodeGroupSelector - グループセレクター(オプション)。
  • CodeGroupOption - グループオプション。

使い方

基本的な使い方

import {
  Codes,
  CodeHeader,
  CodeList,
  CodeTrigger,
  CodeContent,
  CodeDisplay,
  CodeCopyButton,
} from "@/registry/blocks/codes";
import { generateCodeHtml } from "@/lib/code-to-html";

export async function MyComponent() {
  const code = `const hello = "world";`;
  const html = await generateCodeHtml(code, "tsx");

  return (
    <Codes defaultValue="code-1">
      <CodeHeader>
        <CodeList>
          <CodeTrigger value="code-1">
            <span>demo.tsx</span>
          </CodeTrigger>
        </CodeList>
        <CodeCopyButton />
      </CodeHeader>
      <CodeContent value="code-1" code={code}>
        <CodeDisplay html={html} />
      </CodeContent>
    </Codes>
  );
}

複数のコードタブ

const codes = [
  {
    lang: "tsx",
    code: `const Demo = () => <div>Demo</div>;`,
    title: "demo.tsx",
  },
  {
    lang: "css",
    code: `.demo { color: red; }`,
    title: "demo.css",
  },
];

export async function MultipleCodesExample() {
  const codesWithValue = await Promise.all(
    codes.map(async (item, i) => {
      const html = await generateCodeHtml(item.code, item.lang);
      return { ...item, html, value: `${i}` };
    })
  );

  return (
    <Codes defaultValue={codesWithValue[0]?.value}>
      <CodeHeader>
        <CodeList>
          {codesWithValue.map((item, i) => (
            <CodeTrigger key={i} value={item.value}>
              <span>{item.title}</span>
            </CodeTrigger>
          ))}
        </CodeList>
        <CodeCopyButton />
      </CodeHeader>
      {codesWithValue.map((item, i) => (
        <CodeContent key={i} value={item.value} code={item.code}>
          <CodeDisplay html={item.html} />
        </CodeContent>
      ))}
    </Codes>
  );
}

グループセレクター付き

パッケージマネージャーや言語の切り替えなど、グループごとにコードを切り替える場合に使用します。

import {
  CodeGroupSelector,
  CodeGroupOption,
} from "@/registry/blocks/codes";

const codes = [
  {
    lang: "sh",
    code: `npm install package`,
    title: "npm",
    group: "npm",
  },
  {
    lang: "sh",
    code: `yarn add package`,
    title: "yarn",
    group: "yarn",
  },
  {
    lang: "sh",
    code: `pnpm add package`,
    title: "pnpm",
    group: "pnpm",
  },
];

const groups = ["npm", "yarn", "pnpm"];

export async function GroupedCodesExample() {
  const codesWithValue = await Promise.all(
    codes.map(async (item, i) => {
      const html = await generateCodeHtml(item.code, item.lang);
      return {
        ...item,
        html,
        value: `${item.group}-${i}`,
      };
    })
  );

  return (
    <Codes defaultValue={codesWithValue[0]?.value} groups={groups}>
      <CodeHeader>
        <CodeList>
          {codesWithValue.map((item, i) => (
            <CodeTrigger key={i} value={item.value} group={item.group}>
              <span>{item.title}</span>
            </CodeTrigger>
          ))}
        </CodeList>
        <span className="flex-1" />
        <CodeGroupSelector>
          {groups.map((group) => (
            <CodeGroupOption key={group} value={group}>
              <span>{group}</span>
            </CodeGroupOption>
          ))}
        </CodeGroupSelector>
        <CodeCopyButton />
      </CodeHeader>
      {codesWithValue.map((item, i) => (
        <CodeContent key={i} value={item.value} code={item.code}>
          <CodeDisplay html={item.html} />
        </CodeContent>
      ))}
    </Codes>
  );
}

アイコン付き

import { SiTypescript, SiJavascript } from "@icons-pack/react-simple-icons";

const icons = {
  ts: SiTypescript,
  js: SiJavascript,
};

// CodeTrigger 内でアイコンを使用
<CodeTrigger value="code-1">
  <SiTypescript className="size-3.5" />
  <span>demo.tsx</span>
</CodeTrigger>

コンポーネント

Codes

メインのコンテナコンポーネント。タブの状態管理を行います。

Props

NameTypeDescription
defaultValuestringデフォルトで選択されるタブの値
groupsstring[]グループの配列(オプション)

CodeHeader

ヘッダー部分のコンテナ。タブリストやボタンを配置します。

CodeList

タブのリストコンテナ。スクロール可能で、複数の CodeTrigger を含みます。

CodeTrigger

個別のタブトリガー。クリックでコードコンテンツを切り替えます。

Props

NameTypeDescription
valuestringタブの一意な値
groupstringグループ名(オプション)

CodeContent

コードコンテンツのラッパー。選択されたタブに対応するコードを表示します。

Props

NameTypeDescription
valuestringコンテンツの値(CodeTrigger の value と対応)
codestringコピー用の生のコード文字列

CodeDisplay

シンタックスハイライトされたコードを表示します。

Props

NameTypeDescription
htmlstringgenerateCodeHtml で生成された HTML 文字列

CodeCopyButton

現在選択されているコードをクリップボードにコピーするボタン。

CodeGroupSelector

グループを切り替えるセレクター。

CodeGroupOption

グループの選択肢。

Props

NameTypeDescription
valuestringグループの値

実装のヒント

CodeProvider の重要性

グループ機能を使用する場合、CodeProvider の設置は必須です。設置しないと以下の問題が発生します:

  • グループの選択がページ遷移で失われる
  • 複数のコードブロック間で選択が同期されない
  • ユーザーの選択が記憶されない
// ❌ 悪い例: CodeProvider なし
export default function Layout({ children }) {
  return <>{children}</>;
}

// ✅ 良い例: CodeProvider あり
export default function Layout({ children }) {
  return <CodeProvider>{children}</CodeProvider>;
}

サーバーコンポーネントでの使用

generateCodeHtml は非同期関数なので、React Server Components で使用するのが最適です。

export async function MyServerComponent() {
  const html = await generateCodeHtml(code, "tsx");
  // ...
}

コードの取得

コードは外部ファイルから読み込むこともできます:

import { readFile } from "fs/promises";
import { join } from "path";

export async function CodeFromFile() {
  const code = await readFile(
    join(process.cwd(), "examples/demo.tsx"),
    "utf-8"
  );
  const html = await generateCodeHtml(code, "tsx");
  
  return (
    <Codes defaultValue="demo">
      {/* ... */}
    </Codes>
  );
}

スタイリング

コンポーネントは Tailwind CSS でスタイリングされており、カスタマイズ可能です:

<CodeHeader className="bg-muted">
  {/* ... */}
</CodeHeader>

<CodeDisplay className="text-sm" html={html} />

使用例

  • ドキュメント内でのコード例の表示
  • チュートリアルでの複数言語対応
  • パッケージマネージャーごとのインストールコマンド
  • ファイル構成の表示
  • API レスポンスの例示

関連ツール

このコンポーネントを最大限活用するために、以下のツールを推奨します:

MDX のコード解析

MDX ファイル内のコードブロックを解析するには、remark-code-to-slot を使用することを推奨します。

このプラグインは、MDX 内のコードブロックをスロットに変換し、データ属性として解析しやすい形式で提供します。

// next.config.mjs
import createMDX from '@next/mdx';

const withMDX = createMDX({
  options: {
    remarkPlugins: ['remark-code-to-slot'],
    rehypePlugins: [],
  },
});

export default withMDX(nextConfig);

詳細は remark-code-to-slot のドキュメントをご覧ください。

シンタックスハイライト

コードのシンタックスハイライトには、Shiki を使用することを推奨します。

Shiki は VS Code と同じ TextMate 文法エンジンを使用しており、以下の特徴があります:

  • 正確で美しい - VS Code と同じエンジンで一貫性のあるハイライト
  • ゼロランタイム - ビルド時に処理され、JavaScript を一切配信しない
  • 高度なカスタマイズ - HAST ベースで、トランスフォーマーや装飾が可能
  • ユニバーサル - ブラウザ、Node.js、Cloudflare Workers など、あらゆる環境で動作
import { codeToHtml } from 'shiki';

export async function generateCodeHtml(code: string, lang: string) {
  const html = await codeToHtml(code, {
    lang,
    theme: 'github-dark',
  });
  return html;
}

詳細は Shiki のドキュメントをご覧ください。