Справочник
Тип Command
interface Command<T = unknown> {
id: string // уникальный идентификатор
label: string // отображаемый текст, ищется движком нечёткого поиска
description?: string // подзаголовок под label
icon?: Component | string // Vue-компонент или эмодзи / строка
keywords?: string[] // дополнительные термины для поиска
aliases?: string[] // альтернативные label (тот же счёт, что и совпадение по label)
shortcut?: string[] // подсказка только для отображения: ['$mod', 'k']
disabled?: boolean // постоянно недоступна
enabled?: () => boolean // динамическое отключение — вычисляется на каждом рендере
disabledReason?: string // тултип, показываемый когда команда отключена
badge?: string | { text: string; color?: string } // небольшая метка (например, "New", "Pro")
confirm?: string // подсказка перед выполнением
perform: () => void | Promise<void> // действие; может быть асинхронным
subCommands?: Command[] // открывает вложенную палитру при выборе
page?: CommandPage // открывает страницу со своим вводом/асинхронным поиском
actions?: CommandAction[] // дополнительные действия, открываются по Tab
info?: string // текст/HTML, показываемый в панели предпросмотра (v-html)
data?: T // типобезопасный payload (см. «Типизированные данные команды»)
}
interface CommandAction {
id: string
label: string
icon?: Component | string
shortcut?: string[] // подсказка только для отображения
perform: () => void | Promise<void>
}
interface CommandPage {
placeholder?: string // плейсхолдер ввода на странице
items?: Command[] // статичные элементы (пустой запрос)
onSearch?: (query: string) => Command[] | Promise<Command[]> // результаты по запросу (с debounce)
}Поле icon
Принимает строку-эмодзи, обычную текстовую строку или любой Vue-компонент:
import MyIcon from './MyIcon.vue'
{
icon: '🏠'
} // строка-эмодзи
{
icon: '⌘'
} // строка-символ
{
icon: MyIcon
} // Vue-компонент — рендерится как <MyIcon />Типизированные данные команды
Прикрепите произвольный типобезопасный payload к командам через дженерик Command<T> и его поле data. useRegisterCommands<T> / useRegisterGroup<T>, fuzzySearch<T>, SearchResult<T> и SearchFn<T> — все пробрасывают тип дальше, так что вы получаете полный вывод типов (и ошибки при несовпадении). По умолчанию используется unknown, поэтому существующий нетипизированный код не затрагивается.
interface UserData {
id: number
email: string
}
useRegisterCommands<UserData>([
{
id: 'user-ada',
label: 'Ada Lovelace',
data: { id: 1, email: 'ada@example.com' }, // проверяется по UserData
perform: () => {},
},
])
// Автономный поиск сохраняет тип:
const results = fuzzySearch<UserData>('ada', commands)
results[0].command.data?.email // string | undefined// @ts-expect-error — data должен соответствовать UserData
const bad: Command<UserData> = { id: 'x', label: 'X', data: { wrong: true }, perform: () => {} }Внутри слотов
#item/#previewcommandтипизирован какCommand(data: unknown), поскольку палитра хранит команды смешанных типов — сужайте тип через приведение или type guard, когда там нужен payload.
Типы TypeScript
Все публичные типы экспортируются из корня пакета:
import type {
Command,
CommandGroupType, // определение группы — НЕ компонент CommandGroup
CommandAction, // дополнительное действие команды
CommandPage, // страница, открываемая командой (плейсхолдер + асинхронный onSearch)
SearchResult, // { command, score, matches, groupId?, parents?, matchedField? }
SearchFn, // сигнатура кастомной стратегии поиска
PaletteMode, // активируемая префиксом область
CommandUsage, // статистика frecency { count, lastUsed }
PaletteOptions,
PaletteLabels, // настраиваемые строки UI (i18n)
PaletteContext,
PaletteState,
CommandStore,
KeyboardManager,
} from '@macrulez/vue-command-palette'Примечание: именованный экспорт
CommandGroup— это Vue-компонент. Интерфейс определения группы экспортируется какCommandGroupType, чтобы избежать конфликта.
SearchResult
interface SearchResult {
command: Command
score: number
matches: Array<[start: number, end: number]>
groupId?: string
parents?: Command[] // цепочка предков, когда результат — вложенная под-команда
matchedField?: 'label' | 'description' | 'keyword' | 'alias' // какое поле выиграло счёт
matchedText?: string // текст совпавшего ключевого слова/алиаса
}PaletteContext
Полный инжектируемый контекст, доступный в кастомных composables через inject(PALETTE_INJECT_KEY):
interface PaletteContext {
store: CommandStore
keyboard: KeyboardManager
isOpen: Ref<boolean>
query: Ref<string>
activeIndex: Ref<number>
history: Ref<HistoryEntry[]>
recentIds: Ref<string[]>
loadingCommandId: Ref<string | null>
results: ComputedRef<SearchResult[]>
persistRecent: boolean
maxRecent: number
maxRecentPerGroup: number
localStorageKey: string
onOpen?: () => void
onClose?: () => void
onError?: (err: unknown, command: Command) => void
}Доступность
| Функция | Реализация |
|---|---|
role="dialog" + aria-modal="true" | Применяется к элементу диалога палитры |
role="combobox" | Применяется к <input> поиска |
aria-expanded="true" | Устанавливается на input, пока палитра открыта |
aria-controls | Input указывает на список результатов с role="listbox" |
aria-activedescendant | Обновляется при смене активного по клавиатуре пункта |
role="listbox" | Применяется к контейнеру списка результатов |
role="option" | Применяется к каждому CommandItem |
aria-selected | Устанавливается в true для текущего активного пункта |
aria-disabled | Устанавливается, когда disabled: true или enabled() возвращает false |
aria-live="polite" | Хлебные крошки — скринридеры объявляют навигацию по под-палитре; визуально скрытая область также объявляет количество результатов (labels.resultsCount) |
| Focus trap | Tab перехватывается, чтобы удерживать фокус внутри диалога |
| Блокировка скролла | document.body.style.overflow устанавливается в hidden, пока диалог открыт |
| Уменьшенное движение | @media (prefers-reduced-motion: reduce) отключает fade-переход |
Совместимость с SSR
Все браузерные API защищены перед использованием:
// KeyboardManager — пропускает addEventListener на сервере
if (typeof document === 'undefined') return
// Недавние команды — пропускает localStorage на сервере
if (typeof localStorage === 'undefined') return
// CommandItem — определение платформы для метки ⌘ или Ctrl
typeof navigator !== 'undefined' && navigator.platform.includes('Mac')VirtualList (используется для наборов результатов > 50 элементов) рендерит пустой плейсхолдер на сервере и гидрируется на клиенте. Весь контент слотов и регистрация команд полностью SSR-безопасны.
Размер бандла
| Точка входа | Peer-зависимости | Заметки |
|---|---|---|
@macrulez/vue-command-palette | vue ^3.3 | Компоненты, composables, движок нечёткого поиска, менеджер клавиатуры |
@macrulez/vue-command-palette/style.css | — | Стандартные стили; ~3 КБ |
@macrulez/vue-command-palette/testing | vue ^3.3 | createPaletteContext + PaletteProvider; только для dev/тестов |
@macrulez/vue-command-palette/nuxt | nuxt ^3, vue ^3.3 | Авто-плагин для Nuxt |
Поставляется как tree-shakeable ESM (dist/@macrulez/vue-command-palette.js) + CJS (dist/@macrulez/vue-command-palette.cjs). Базовый бандл без стилей — ~11 КБ gzip; стилевой файл — ~2 КБ gzip.
Лицензия
MIT