コマンドパレット(⌘K)
:LiTarget: 用途
Cmd+K で開く Spotlight 風コマンドパレット。アクション一覧 + 絞込検索。
:LiSparkle: 特徴
- ⌘K ショートカット
- 絞込検索
- 上下キー選択
- Enter で実行
:LiCode: コード(コピペ用)
'use client';
import { useEffect, useState, useMemo } from 'react';
type Cmd = { id: string; label: string; action: () => void; keywords?: string };
export function CommandPalette({ commands }: { commands: Cmd[] }) {
const [open, setOpen] = useState(false);
const [q, setQ] = useState('');
const [idx, setIdx] = useState(0);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); setOpen(o => !o); setQ(''); setIdx(0); }
if (e.key === 'Escape') setOpen(false);
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
const filtered = useMemo(() => {
const ql = q.toLowerCase();
return commands.filter(c =>
c.label.toLowerCase().includes(ql) || (c.keywords?.toLowerCase().includes(ql) ?? false)
);
}, [q, commands]);
if (!open) return null;
return (
<div onClick={() => setOpen(false)} style={{
position: 'fixed', inset: 0, background: 'rgba(0,0,0,.6)', zIndex: 9999,
display: 'flex', alignItems: 'flex-start', justifyContent: 'center', paddingTop: '15vh'
}}>
<div onClick={e => e.stopPropagation()} style={{
background: '#0a0a0c', border: '1px solid rgba(255,255,255,.07)',
width: '90%', maxWidth: 600, borderRadius: 8, overflow: 'hidden',
}}>
<input autoFocus value={q} onChange={e => { setQ(e.target.value); setIdx(0); }}
onKeyDown={e => {
if (e.key === 'ArrowDown') { e.preventDefault(); setIdx(i => Math.min(filtered.length - 1, i + 1)); }
if (e.key === 'ArrowUp') { e.preventDefault(); setIdx(i => Math.max(0, i - 1)); }
if (e.key === 'Enter') { filtered[idx]?.action(); setOpen(false); }
}}
placeholder="検索..." style={{
width: '100%', padding: '1rem 1.25rem', background: 'transparent',
border: 'none', color: '#fff', fontSize: '.95rem', outline: 'none',
}} />
<div style={{ borderTop: '1px solid rgba(255,255,255,.07)', maxHeight: 400, overflowY: 'auto' }}>
{filtered.map((c, i) => (
<div key={c.id} onClick={() => { c.action(); setOpen(false); }}
style={{
padding: '.75rem 1.25rem', cursor: 'pointer',
background: i === idx ? 'rgba(165,180,252,.1)' : 'transparent',
color: i === idx ? '#fff' : 'rgba(255,255,255,.85)',
}}>
{c.label}
</div>
))}
</div>
</div>
</div>
);
}
:LiHandPointer: 使い方
対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。
:LiAlertCircle: 注意事項
- 依存パッケージを忘れず追加