/** Command palette (Ctrl/Cmd+K): fuzzy list of tools, panels and actions. */ export function createCommandPalette({ commands = [], onRun = null } = {}) { const overlay = document.createElement('div'); overlay.className = 'ge-command-palette'; overlay.hidden = true; overlay.setAttribute('role', 'dialog'); overlay.setAttribute('aria-modal', 'true'); overlay.setAttribute('aria-label', 'Command palette'); overlay.innerHTML = `
`; document.body.appendChild(overlay); const input = overlay.querySelector('.ge-command-input'); const list = overlay.querySelector('.ge-command-list'); let filtered = []; let activeIndex = 0; function render() { const query = input.value.trim().toLowerCase(); filtered = commands.filter(cmd => !query || String(cmd.label).toLowerCase().includes(query)); list.innerHTML = ''; if (!filtered.length) { const empty = document.createElement('p'); empty.className = 'ge-command-empty'; empty.textContent = 'No matching commands'; list.appendChild(empty); return; } if (activeIndex >= filtered.length) activeIndex = 0; filtered.forEach((cmd, index) => { const row = document.createElement('button'); row.type = 'button'; row.className = `ge-command-item${index === activeIndex ? ' active' : ''}`; row.setAttribute('role', 'option'); row.setAttribute('aria-selected', index === activeIndex ? 'true' : 'false'); row.textContent = cmd.label; row.addEventListener('click', () => run(cmd)); list.appendChild(row); }); } function run(cmd) { close(); if (typeof cmd.run === 'function') cmd.run(); else if (typeof onRun === 'function') onRun(cmd); } function open() { overlay.hidden = false; input.value = ''; activeIndex = 0; render(); input.focus(); } function close() { overlay.hidden = true; } input.addEventListener('input', () => { activeIndex = 0; render(); }); input.addEventListener('keydown', (event) => { if (event.key === 'Escape') { event.preventDefault(); close(); } else if (event.key === 'ArrowDown') { event.preventDefault(); activeIndex = Math.min(activeIndex + 1, filtered.length - 1); render(); } else if (event.key === 'ArrowUp') { event.preventDefault(); activeIndex = Math.max(activeIndex - 1, 0); render(); } else if (event.key === 'Enter') { event.preventDefault(); if (filtered[activeIndex]) run(filtered[activeIndex]); } }); overlay.addEventListener('pointerdown', (event) => { if (event.target === overlay) close(); }); return { element: overlay, open, close, toggle: () => (overlay.hidden ? open() : close()), isOpen: () => !overlay.hidden, }; }