Command+K палитра для Vue 3 — нечёткий поиск, группировка команд, вложенные под-палитры, асинхронный поиск и полная headless-кастомизация через слоты.

Cmd+K давно стало ожидаемым паттерном в рабочих инструментах — vue-command-palette даёт готовую, полностью кастомизируемую реализацию вместо того, чтобы собирать поиск, клавиатурную навигацию и подсветку совпадений с нуля.
Настройки — в одном углу, экспорт — в другом, переключение темы — в третьем; палитра команд собирает всё это в одном месте, доступном с клавиатуры без похода по меню.
«Назначить исполнителя» не может просто выполниться — сначала нужно найти человека; вложенная страница со своим асинхронным поиском решает это без отдельного модального окна.
«Удалить проект» не должна нажиматься так же легко, как «Открыть настройки» — встроенный шаг подтверждения страхует от случайного клика.
Вместо того чтобы каждый раз печатать заново, недавние и закреплённые команды поднимаются наверх при пустом запросе, а те, что вы используете чаще всего, со временем поднимаются в списке ещё выше.

Мгновенный поиск с ранжированием (точное совпадение, префикс, подстрока, нечёткое), подсветкой совпадений и нормализацией диакритики. Группировка команд с заголовками, вложенные подпалитры любой глубины и страницы с собственным асинхронным поиском.

Встроенный менеджер клавиатуры поддерживает модификаторы ($mod, shift, alt, ctrl, meta) и последовательности из двух клавиш. Привязывайте шорткаты к командам или используйте их отдельно. Команды с shortcut автоматически регистрируются как глобальные хоткеи.

Группы и страницы могут загружать команды по запросу через onSearch с дебаунсом 200 мс. Встроенная поддержка состояний загрузки, динамическое включение/отключение команд через enabled(), асинхронное выполнение с индикатором прогресса и обработка ошибок.

История выполненных команд сохраняется в localStorage и показывается при пустом запросе. Закрепляйте избранные команды, а frecency (частота + свежесть) поднимает часто используемые команды в результатах поиска. История запросов доступна через Alt+↑/↓.

Headless-слоты для полного контроля над UI, 20+ CSS-переменных для темизации, встроенная светлая/тёмная тема. Готовые хуки для Vue 3 и Nuxt-модуль. Все API безопасны для SSR, а утилиты тестирования упрощают юнит-тесты. Единственная зависимость — Vue 3.

Список результатов рендерится через виртуальный скроллинг — даже палитра с сотнями команд остаётся плавной. Создавайте несколько независимых экземпляров через createCommandPalette(), а для деструктивных команд используйте встроенный шаг подтверждения.
Две клавиши подряд без модификаторов — g, затем h — в окне 500 мс. registerShortcut сам разбирает такие последовательности.
import { createKeyboardManager } from '@macrulez/vue-command-palette'
const km = createKeyboardManager()
km.start()
km.registerShortcut(['g', 'h'], () => router.push('/home'))
km.registerShortcut(['g', 'p'], () => router.push('/projects'))
km.registerShortcut(['g', 's'], () => router.push('/settings'))
// Two bare keys, no modifier, pressed within a 500ms window — the same
// "g then h" navigation pattern as GitHub's own keyboard shortcuts.subCommands открывает дочернюю палитру по выбору, а запрос "light" сразу находит команду внутри — с хлебной крошкой «Change Theme › Light», без захода в подменю.
const changeTheme = {
id: 'change-theme',
label: 'Change Theme',
icon: '🎨',
subCommands: [
{ id: 'theme-light', label: 'Light', icon: '☀️', perform: () => (theme.value = 'light') },
{ id: 'theme-dark', label: 'Dark', icon: '🌙', perform: () => (theme.value = 'dark') },
{ id: 'theme-system', label: 'System', icon: '💻', perform: () => (theme.value = 'system') },
],
}
// With searchNested (on by default), typing "light" finds the command
// inside the sub-palette directly — shown with a breadcrumb, "Change
// Theme › Light" — no need to open "Change Theme" first.onSearch у группы дёргает любой API — документацию, базу, поиск — с дебаунсом 200 мс, а результаты сливаются с обычными и пересортировываются по релевантности.
import { useRegisterGroup } from '@macrulez/vue-command-palette'
useRegisterGroup({
id: 'docs-search',
label: 'Documentation',
commands: [],
onSearch: async (query) => {
const results = await searchDocs(query)
return results.slice(0, 5).map((doc) => ({
id: `doc-${doc.slug}`,
label: doc.title,
description: doc.excerpt,
icon: '📄',
perform: () => window.open(doc.url, '_blank'),
}))
},
})
// Debounced 200ms, merged with the synchronous results and re-sorted by
// score — already-shown results stay visible while the request is in flight.