Use Cases

Implementation with Claude Code

Learn about implementation using Claude Code. Practical tips and code examples included.

コマンドパレットでユーザー体験を劇的に改善

VSCodeやSlackで見かけるコマンドパレットは、キーボードだけで素早く操作できるパワーユーザー向けのUI要素です。Claude Codeを使えば、検索・フィルタリング・キーボードナビゲーションを備えたコマンドパレットを効率的に実装できます。

基本的なコマンドパレットコンポーネント

> Cmd+K(Mac)/ Ctrl+K(Windows)で開くコマンドパレットを実装して。
> 検索、キーボード操作、カテゴリ別表示に対応して。
// components/CommandPalette.tsx
import { useState, useEffect, useRef, useCallback } from 'react';

interface Command {
  id: string;
  label: string;
  category: string;
  icon?: string;
  shortcut?: string;
  action: () => void;
}

interface CommandPaletteProps {
  commands: Command[];
}

export function CommandPalette({ commands }: CommandPaletteProps) {
  const [isOpen, setIsOpen] = useState(false);
  const [query, setQuery] = useState('');
  const [selectedIndex, setSelectedIndex] = useState(0);
  const inputRef = useRef<HTMLInputElement>(null);

  const filtered = commands.filter(cmd =>
    cmd.label.toLowerCase().includes(query.toLowerCase()) ||
    cmd.category.toLowerCase().includes(query.toLowerCase())
  );

  // グローバルショートカット
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
        e.preventDefault();
        setIsOpen(prev => !prev);
      }
      if (e.key === 'Escape') setIsOpen(false);
    };
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, []);

  // フォーカス制御
  useEffect(() => {
    if (isOpen) {
      inputRef.current?.focus();
      setQuery('');
      setSelectedIndex(0);
    }
  }, [isOpen]);

  const handleKeyNav = useCallback((e: React.KeyboardEvent) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setSelectedIndex(i => Math.min(i + 1, filtered.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setSelectedIndex(i => Math.max(i - 1, 0));
        break;
      case 'Enter':
        e.preventDefault();
        if (filtered[selectedIndex]) {
          filtered[selectedIndex].action();
          setIsOpen(false);
        }
        break;
    }
  }, [filtered, selectedIndex]);

  if (!isOpen) return null;

  return (
    <div className="fixed inset-0 z-50 flex items-start justify-center pt-[20vh]">
      <div className="fixed inset-0 bg-black/50" onClick={() => setIsOpen(false)} />
      <div className="relative w-full max-w-lg rounded-xl bg-white shadow-2xl dark:bg-gray-800">
        <input
          ref={inputRef}
          type="text"
          value={query}
          onChange={e => { setQuery(e.target.value); setSelectedIndex(0); }}
          onKeyDown={handleKeyNav}
          placeholder="コマンドを検索..."
          className="w-full border-b px-4 py-3 text-lg outline-none dark:bg-gray-800"
        />
        <ul className="max-h-80 overflow-y-auto p-2" role="listbox">
          {filtered.map((cmd, index) => (
            <li
              key={cmd.id}
              role="option"
              aria-selected={index === selectedIndex}
              onClick={() => { cmd.action(); setIsOpen(false); }}
              className={`flex cursor-pointer items-center justify-between rounded-lg px-3 py-2
                ${index === selectedIndex ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50'}`}
            >
              <div className="flex items-center gap-3">
                {cmd.icon && <span className="text-lg">{cmd.icon}</span>}
                <div>
                  <div className="font-medium">{cmd.label}</div>
                  <div className="text-xs text-gray-500">{cmd.category}</div>
                </div>
              </div>
              {cmd.shortcut && (
                <kbd className="rounded bg-gray-100 px-2 py-1 text-xs dark:bg-gray-700">
                  {cmd.shortcut}
                </kbd>
              )}
            </li>
          ))}
          {filtered.length === 0 && (
            <li className="px-3 py-8 text-center text-gray-500">
              該当するコマンドがありません
            </li>
          )}
        </ul>
      </div>
    </div>
  );
}

コマンドの定義と登録

// hooks/useCommands.ts
import { useRouter } from 'next/navigation';

export function useCommands(): Command[] {
  const router = useRouter();

  return [
    {
      id: 'home', label: 'ホームに移動', category: 'ナビゲーション',
      icon: '🏠', shortcut: 'G H',
      action: () => router.push('/'),
    },
    {
      id: 'blog', label: 'ブログ一覧', category: 'ナビゲーション',
      icon: '📝', shortcut: 'G B',
      action: () => router.push('/blog'),
    },
    {
      id: 'theme', label: 'テーマ切替', category: '設定',
      icon: '🌙', shortcut: 'T T',
      action: () => document.documentElement.classList.toggle('dark'),
    },
    {
      id: 'search', label: '記事を検索', category: '検索',
      icon: '🔍', shortcut: '/',
      action: () => router.push('/search'),
    },
  ];
}

アクセシビリティの対応

// WAI-ARIAの適用
<div
  role="dialog"
  aria-modal="true"
  aria-label="コマンドパレット"
>
  <input
    role="combobox"
    aria-expanded="true"
    aria-controls="command-list"
    aria-activedescendant={`command-${filtered[selectedIndex]?.id}`}
  />
  <ul id="command-list" role="listbox">
    {/* ... */}
  </ul>
</div>

Summary

コマンドパレットはキーボードショートカットと組み合わせることで、パワーユーザーの生産性を大幅に向上させます。Claude Codeを使えば、検索・フィルタリング・キーボードナビゲーションを備えた本格的なコマンドパレットを短時間で実装できます。アクセシビリティにも配慮して、すべてのユーザーが使いやすいUIを目指しましょう。実装の参考としてcmdkライブラリも検討してみてください。

#Claude Code #コマンドパレット #UI #キーボード操作 #React