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
);
}