import { useEffect, useState } from 'react'; import { Command } from 'cmdk'; import { ArrowRight, Box, CornerDownRight, Database, Clock, Search, } from 'lucide-react'; export interface PaletteTarget { label: string; target: string; description?: string; } interface CommandItem { id: string; label: string; description?: string; icon: typeof Box; action: () => void; category?: string; } interface CommandPaletteProps { open: boolean; onClose: () => void; onAddService?: (type: string) => void; onNavigate?: (target: string) => void; /** Navigate-group items; falls back to the full app route map. */ navTargets?: PaletteTarget[]; /** Extra searchable "Open …" rows, e.g. projects. */ openTargets?: PaletteTarget[]; } const APP_NAV: PaletteTarget[] = [ { label: 'Dashboard', target: '/', description: 'Index — stats, nodes, deploys' }, { label: 'Projects', target: '/projects', description: 'All projects' }, { label: 'Templates', target: '/templates', description: 'Template catalog' }, { label: 'Builds', target: '/builds', description: 'Build pipeline' }, { label: 'Databases', target: '/databases', description: 'Managed databases' }, { label: 'Operations', target: '/operations', description: 'Backups, cron, maintenance' }, { label: 'Activity', target: '/activity', description: 'Platform event feed' }, { label: 'High Availability', target: '/ha', description: 'Failover & alerts' }, { label: 'Security', target: '/security', description: 'Access, sessions, secrets' }, { label: 'Usage', target: '/usage', description: 'Resource consumption' }, { label: 'People', target: '/people', description: 'Team & access control' }, { label: 'Audit Logs', target: '/settings/audit-logs', description: 'Recorded actions' }, { label: 'Webhooks', target: '/settings/webhooks', description: 'Signed event delivery' }, { label: 'Settings', target: '/settings', description: 'Platform preferences' }, { label: 'Docs', target: '/docs', description: 'Guides & reference' }, { label: 'Landing', target: '/landing', description: 'Public page' }, ]; export function CommandPalette({ open, onClose, onAddService, onNavigate, navTargets, openTargets, }: CommandPaletteProps) { if (!open) return null; return ( ); } function CommandPaletteContent({ onClose, onAddService, onNavigate, navTargets, openTargets, }: Omit) { const [search, setSearch] = useState(''); useEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); onClose(); } }; document.addEventListener('keydown', down); return () => document.removeEventListener('keydown', down); }, [onClose]); const commands: CommandItem[] = []; // Create items only exist where onAddService is wired — elsewhere the // entries would silently no-op. if (onAddService) { commands.push( { id: 'add-web', label: 'Add Web Service', description: 'Deploy a web application', icon: Box, category: 'Create', action: () => { onAddService('web'); onClose(); }, }, { id: 'add-worker', label: 'Add Worker Service', description: 'Deploy a background worker', icon: Clock, category: 'Create', action: () => { onAddService('worker'); onClose(); }, }, { id: 'add-database', label: 'Add Database', description: 'Provision a PostgreSQL database', icon: Database, category: 'Create', action: () => { onAddService('database'); onClose(); }, }, { id: 'add-cron', label: 'Add Cron Job', description: 'Schedule a recurring task', icon: Clock, category: 'Create', action: () => { onAddService('cron'); onClose(); }, }, ); } if (onNavigate) { (navTargets ?? APP_NAV).forEach((t, i) => commands.push({ id: `nav-${i}`, label: t.label, description: t.description, icon: CornerDownRight, category: 'Navigate', action: () => { onNavigate(t.target); onClose(); }, }), ); (openTargets ?? []).forEach((t, i) => commands.push({ id: `open-${i}`, label: t.label, description: t.description, icon: ArrowRight, category: 'Open', action: () => { onNavigate(t.target); onClose(); }, }), ); } const filteredCommands = commands.filter( (cmd) => cmd.label.toLowerCase().includes(search.toLowerCase()) || cmd.description?.toLowerCase().includes(search.toLowerCase()) || cmd.category?.toLowerCase().includes(search.toLowerCase()) ); const groupedCommands = filteredCommands.reduce( (acc, cmd) => { const category = cmd.category || 'Other'; if (!acc[category]) acc[category] = []; acc[category].push(cmd); return acc; }, {} as Record ); const placeholder = onNavigate ? 'Search pages, projects, actions…' : 'What would you like to create?'; return (
ESC
No results found. {Object.entries(groupedCommands).map(([category, items]) => (
{category}
{items.map((item) => { const Icon = item.icon; return (
{item.label}
{item.description && (
{item.description}
)}
); })}
))}
↑↓ Navigate
↵ Select
⌘K Toggle
); }