rise-deploy 0.18.0

A simple and powerful CLI for deploying containerized applications
import { useEffect, useMemo, useState } from 'react';

export type CommandItem = {
    id: string;
    label: string;
    keywords?: string[];
    run: () => void;
};

export function CommandPalette({
    isOpen,
    onClose,
    items,
}: {
    isOpen: boolean;
    onClose: () => void;
    items: CommandItem[];
}) {
    const [query, setQuery] = useState('');
    const [activeIndex, setActiveIndex] = useState(0);

    useEffect(() => {
        if (isOpen) {
            setQuery('');
            setActiveIndex(0);
        }
    }, [isOpen]);

    const filteredItems = useMemo(() => {
        const q = query.trim().toLowerCase();
        if (!q) return items;
        return items.filter((item) => {
            const haystack = [item.label, ...(item.keywords || [])].join(' ').toLowerCase();
            return haystack.includes(q);
        });
    }, [items, query]);

    useEffect(() => {
        if (!isOpen) return;

        const onKey = (e: KeyboardEvent) => {
            if (e.key === 'Escape') {
                onClose();
                return;
            }
            if (e.key === 'ArrowDown') {
                e.preventDefault();
                setActiveIndex((prev) => Math.min(prev + 1, Math.max(0, filteredItems.length - 1)));
                return;
            }
            if (e.key === 'ArrowUp') {
                e.preventDefault();
                setActiveIndex((prev) => Math.max(prev - 1, 0));
                return;
            }
            if (e.key === 'Enter' && filteredItems[activeIndex]) {
                e.preventDefault();
                filteredItems[activeIndex].run();
                onClose();
            }
        };

        window.addEventListener('keydown', onKey);
        return () => window.removeEventListener('keydown', onKey);
    }, [isOpen, activeIndex, filteredItems, onClose]);

    if (!isOpen) return null;

    return (
        <div className="mono-palette-backdrop" onClick={onClose}>
            <div className="mono-palette" onClick={(e) => e.stopPropagation()}>
                <input
                    autoFocus
                    aria-label="Command palette"
                    value={query}
                    onChange={(e) => {
                        setQuery(e.target.value);
                        setActiveIndex(0);
                    }}
                    placeholder="Type a command..."
                    className="mono-palette-input"
                />
                <div className="mono-palette-list">
                    {filteredItems.length === 0 ? (
                        <p className="mono-palette-empty">No commands found</p>
                    ) : (
                        filteredItems.map((item, idx) => (
                            <button
                                key={item.id}
                                className={`mono-palette-item ${idx === activeIndex ? 'active' : ''}`}
                                onMouseEnter={() => setActiveIndex(idx)}
                                onClick={() => {
                                    item.run();
                                    onClose();
                                }}
                            >
                                {item.label}
                            </button>
                        ))
                    )}
                </div>
            </div>
        </div>
    );
}