GrCommandPalette
Берут, когда команд много и они разбросаны по интерфейсу.
Обзор
Когда брать
- команд много и они разбросаны по интерфейсу — палитра даёт к ним один вход вместо поиска по меню;
- пользователь работает клавиатурой —
hotkeyоткрывает палитру откуда угодно, дальше всё делается стрелками; - команды сгруппированы — разделы, недавние (
recentIds) и подсказки сочетаний уже есть; - источник асинхронный —
sourceподгружает элементы по запросу,virtualдержит длинный список.
Когда взять другое
| Нужно | Берите |
|---|---|
| Выбирается значение поля формы | GrSelect |
| Ищется объект с подгрузкой | GrAutocomplete |
| Действия относятся к одному объекту | GrDropdownMenu |
| Каталог виджетов дашборда | GrDashboardPalette |
| Поиск по содержимому страницы, а не по командам | GrInput |
Палитра не заменяет навигацию: она ускоряет работу тому, кто уже знает, что ищет. Единственный способ добраться до раздела ею быть не может — новый пользователь не догадается нажать сочетание.
Playground контролы выводятся из API, а не пишутся руками
Этот компонент живёт поверх экрана, а не внутри холста: его открывает экран, а конструктору открывать его неоткуда. Примеры ниже показывают его в работе.
0 изменено· состояние — в адресе
<GrCommandPalette>Текст</GrCommandPalette>Примеры 3 у каждого — живое демо и его код
Панель команд приложения
Откройте кнопкой или ⌘J —
hotkeyвешает глобальное сочетание, и кнопка о нём говорит. Список плоский:groupсобирает разделы,shortcutрисует клавиши,descriptionдаёт контекст,keywordsпозволяют найти настройки по «vat», аdisabled-команда остаётся видна вместе с причиной.recent-idsподнимают последние команды наверх; историю держит страница.Acme BillingINV-0418 · Northwind Traders€1,284.50 · due 30 Sep · 6 days overdueКлиенты, которых ищет сервер
Двенадцать тысяч клиентов в браузер не помещаются.
filterable="false"отдаёт фильтрацию владельцу: запрос уходит событиемsearch,loadingпоказывает ожидание, а палитра рисует то, что пришло. Слотitemрисует свою строку, а слотemptyпревращает «ничего не найдено» в «создать».Customers12,408 accounts · searched on the serverTry «north» or «contoso».Переключатель пространств со своим фильтром
Свой
filterнаходит «wwi» по инициалам, а не только по части названия.recent-idsдержат два последних пространства наверху, пока запрос пуст,size="sm"делает палитру компактной, а слотfooterдержит клавиши там, где руки.WorkspaceNorthwind Traders
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
filter | GrCommandFilter | undefined | undefined | Кастомный матчер локальной фильтрации. |
size | "sm" | "md" | "lg" | "xl" | "full" | undefined | undefined | Ширина окна по шкале панелей `GrModal`. По умолчанию `lg`; берётся из `GrConfigProvider`. |
placeholder | string | undefined | undefined | Подсказка в поле поиска. По умолчанию — строка локали. |
ariaLabel | string | undefined | undefined | Доступное имя палитры и поля поиска. По умолчанию — строка локали. |
loading | boolean | undefined | false | Внешне управляемое состояние загрузки (для remote-поиска). |
filterable | boolean | undefined | true | Локальная фильтрация по запросу. `false` — фильтрует владелец по событию `search`. |
closeOnSelect | boolean | undefined | true | Закрывать палитру после выбора команды. |
virtual | boolean | undefined | false | Виртуализация списка: в DOM живёт только окно вокруг вьюпорта. Высоту окна задаёт `maxHeight`. Включается осознанно: на сотне команд выигрыша нет, а в разметке остаётся только окно — вместе с ним меняется и то, что находит `querySelector` потребителя. Профильный сценарий — палитра приложения на тысячи команд. |
items | GrCommandItem[] | undefined | undefined | Плоский список команд; группировка — по полю `group` самой команды. |
emptyText | string | undefined | undefined | Текст пустой выдачи. Слот `#empty` сильнее; по умолчанию — строка локали. |
recentIds | string[] | undefined | undefined | Недавние команды: пока запрос пуст, они поднимаются отдельной группой наверх в этом порядке и не дублируются ниже. |
hotkey | string | null | undefined | "mod+k" | Глобальное сочетание открытия. `null` — не вешать слушатель. |
hotkeyId | string | undefined | undefined | Идентификатор записи в реестре (`granularityHotkeysPlugin`) вместо сочетания строкой: подсказка и привязка берутся из одного источника и не могут разойтись. Сильнее `hotkey`. |
maxHeight | number | undefined | 360 | Максимальная высота списка, px. |
showHotkeyHint | boolean | undefined | true | Показывать подсказку сочетания в поле ввода. |
modelValueобязательныйv-model | boolean | — | Открыта ли палитра. |
Слоты
| Slot | Type | Описание |
|---|---|---|
item | { item: GrCommandItem; active: boolean; } | Строка списка вместо стандартной. |
empty | { query: string; } | Пустое состояние: получает запрос, чтобы предложить действие по нему. |
footer | any | Подвал палитры: подсказки по клавишам, счётчик. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: boolean): void | — |
search | (event: "search", query: string): void | — |
select | (event: "select", item: GrCommandItem): void | — |
Методы
| Methods | Сигнатура | Описание |
|---|---|---|
open | () => void | Открыть палитру — для кнопки в шапке или пункта меню рядом с сочетанием. |
close | () => void | Закрыть палитру. |
toggle | () => void | Переключить палитру — то же, что глобальное сочетание. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrCommandItem, GrCommandGroup, GrCommandFilter } from '@feugene/granularity/components/GrCommandPalette'Файлdist/types/components/GrCommandPalette/config.d.ts
const grCommandPaletteConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrCommandPalette">;Файлdist/types/components/GrCommandPalette/filtering.d.ts
Команда палитры. id обязателен — он же ключ рендера и цель aria-activedescendant.
type GrCommandItem = {
id: string;
label: string;
/** Пояснение под меткой. */
description?: string;
/**
* Иконка команды: Vue-компонент либо класс иконки (`'i-lucide-file-plus'` —
* правило для класса заводит приложение: фабрике движка или провайдером,
* см. `docs/installation.md#иконки`).
*/
icon?: string | Component;
/** Подсказка сочетания клавиш — массив клавиш, каждая рисуется отдельным `GrKbd`. */
shortcut?: string[];
/** Заголовок группы, под которой команда показывается в списке. */
group?: string;
/** Дополнительные слова для поиска (синонимы, транслитерация). */
keywords?: string[];
disabled?: boolean;
};Команды, сгруппированные для рендера: порядок групп — порядок первого вхождения.
type GrCommandGroup = {
/** `undefined` — команды без группы (рендерятся без заголовка). */
name: string | undefined;
items: GrCommandItem[];
};type GrCommandFilter = (item: GrCommandItem, query: string) => boolean;
/**
* Матчер по умолчанию: подстрока (без учёта регистра) в метке, описании,
* названии группы или ключевых словах.
*
* Набор полей у команды шире, чем у опции списка, а сам подстрочный матч — тот
* же самый на весь пакет (`shared/optionFilter.ts`).
*/
export declare function matchCommandItem(item: GrCommandItem, query: string): boolean;
export declare function filterCommandItems(items: GrCommandItem[], query: string, filter?: GrCommandFilter): GrCommandItem[];
/**
* Раскладывает плоский список по группам, сохраняя порядок первого появления
* группы. Команды без `group` собираются в единственную безымянную группу,
* которая остаётся на своём месте в общем порядке.
*/
export declare function groupCommandItems(items: GrCommandItem[]): GrCommandGroup[];
/** Кусок текста для подсветки: `match` — попал в запрос. */
export type GrCommandMatchSegment = {Файлdist/types/components/GrCommandPalette/GrCommandPalette.vue.d.ts
Публичный GR-примитив «Command palette» (⌘K) — модальный поиск по командам
приложения с группировкой, подсказками сочетаний и клавиатурной навигацией.
A11y: WAI-ARIA combobox поверх модалки — role="combobox" на поле ввода,
role="listbox" на списке, aria-activedescendant на активной команде
(фокус остаётся в поле). Esc, фокус-ловушка и блокировка скролла приходят
из GrModal.
Открытие по глобальному сочетанию (hotkey, по умолчанию mod+k — Cmd на
macOS, Ctrl на прочих) компонент берёт на себя; передайте hotkey: null,
чтобы управлять открытием только через v-model.
interface GrCommandPaletteProps {
/** Открыта ли палитра. */
modelValue: boolean;
/** Плоский список команд; группировка — по полю `group` самой команды. */
items?: GrCommandItem[];
/**
* Недавние команды: пока запрос пуст, они поднимаются отдельной группой
* наверх в этом порядке и не дублируются ниже.
*/
recentIds?: string[];
/** Подсказка в поле поиска. По умолчанию — строка локали. */
placeholder?: string;
/** Ширина окна по шкале панелей `GrModal`. По умолчанию `lg`; берётся из `GrConfigProvider`. */
size?: GrCommandPaletteSize;
/** Глобальное сочетание открытия. `null` — не вешать слушатель. */
hotkey?: string | null;
/**
* Идентификатор записи в реестре (`granularityHotkeysPlugin`) вместо
* сочетания строкой: подсказка и привязка берутся из одного источника и не
* могут разойтись. Сильнее `hotkey`.
*/
hotkeyId?: string;
/** Локальная фильтрация по запросу. `false` — фильтрует владелец по событию `search`. */
filterable?: boolean;
/** Кастомный матчер локальной фильтрации. */
filter?: GrCommandFilter;
/** Внешне управляемое состояние загрузки (для remote-поиска). */
loading?: boolean;
/** Закрывать палитру после выбора команды. */
closeOnSelect?: boolean;
/** Максимальная высота списка, px. */
maxHeight?: number;
/**
* Виртуализация списка: в DOM живёт только окно вокруг вьюпорта. Высоту окна
* задаёт `maxHeight`.
*
* Включается осознанно: на сотне команд выигрыша нет, а в разметке остаётся
* только окно — вместе с ним меняется и то, что находит `querySelector`
* потребителя. Профильный сценарий — палитра приложения на тысячи команд.
*/
virtual?: boolean;
/** Показывать подсказку сочетания в поле ввода. */
showHotkeyHint?: boolean;
/** Текст пустой выдачи. Слот `#empty` сильнее; по умолчанию — строка локали. */
emptyText?: string;
/** Доступное имя палитры и поля поиска. По умолчанию — строка локали. */
ariaLabel?: string;
}interface GrCommandPaletteEmits {
/** Палитра открылась или закрылась (`v-model`): сочетанием, по `Esc` или после выбора команды. */
(e: 'update:modelValue', value: boolean): void;
/** Команда выбрана (клик или Enter). */
(e: 'select', item: GrCommandItem): void;
/** Поисковый запрос изменился — точка входа для remote-поиска. */
(e: 'search', query: string): void;
}Файлdist/types/components/GrCommandPalette/grCommandPaletteStyles.d.ts
type GrCommandPaletteSize = GrOverlaySize;Файлdist/types/components/GrCommandPalette/safelist.d.ts
const grCommandPaletteSafelist: string[];Файлdist/types/components/GrCommandPalette/index.d.ts
type GrCommandPaletteInstance = ComponentExposed<typeof GrCommandPaletteComponent>;Доступность
Паттерн APG
dialog + listbox
Клавиши
mod+K↑ / ↓Home / EndEnter
| Клавиша | Действие |
|---|---|
| mod+K | открыть |
| ↑ / ↓ | по результатам |
| Home / End | к краям |
| Enter | выполнить |
| Esc | закрыть |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Команды
Модель плоская: id, label, опциональные description, icon, shortcut,
group, keywords, disabled. icon — Vue-компонент либо класс иконки
(i-lucide-* — правило для него заводит приложение: фабрике движка или
провайдером, иначе — Vue-компонент; см. «Иконки»). Группы собираются из group в порядке первого
появления; команды без группы остаются безымянной группой на своём месте.
id обязателен и обязан быть уникальным: он же ключ рендера и цель
aria-activedescendant. Дубли дают одинаковые DOM-id, и фокус уезжает не на ту
команду, — в dev-сборке компонент об этом предупреждает.
Фильтр и подсветка
По умолчанию матчится подстрока (без учёта регистра) в метке, описании, имени
группы и keywords. Совпавший фрагмент метки и описания подсвечивается
<mark>; цвет настраивается переменной --gr-command-match-bg.
Свой filter может матчить по чему угодно — например только по keywords.
Тогда в метке совпадения нет, и подсветка не появляется: подсвечивать нечего.
filterable="false" отдаёт фильтрацию наружу (remote-поиск): компонент
показывает то, что пришло, а запрос отдаёт событием search.
Недавние
recentIds поднимает команды отдельной группой наверх — в порядке самого
массива, а не списка команд. Из остального списка они убираются: одна команда
не может встречаться дважды, у неё один id.
Секция живёт, только пока запрос пуст. С непустым запросом правит релевантность: история увела бы взгляд не туда с первой же буквы.
Состояния
Загрузка и «ничего не найдено» показываются одним живым регионом
(role="status", aria-live="polite") вне списка. Иконка спиннера при этом
декоративна: aria-label на элементе без роли большинство AT игнорируют, и
раньше состояние загрузки не объявлялось никак.
Пустой список места не занимает: сообщение встаёт сразу под полем, а сам
role="listbox" остаётся в DOM — на него ссылается aria-controls поля. Чтобы
окно не схлопывалось между «ищем» и «нашлось», резервируйте высоту области
результатов, а не списка:
[data-gr-command-palette] {
--gr-command-results-min-height: 14rem;
}Тогда «Ничего не найдено», загрузка и слот #empty встают по центру резерва.
Резерв на самом списке (min-height у [data-gr-command-palette-list]) уводил бы
сообщение под пустую полосу.
Внутри role="listbox" прямыми потомками остаются только role="group" —
заголовок группы лежит внутри неё и объявлен презентационным, имя группе он
даёт через aria-labelledby. Иначе ломается aria-required-children, а
панель здесь развёрнута всегда, то есть это основное состояние.
Виртуализация
virtual оставляет в DOM только окно вокруг вьюпорта; высоту окна задаёт
maxHeight. Профильный сценарий — палитра приложения на тысячи команд.
<GrCommandPalette v-model="open" :items="commands" virtual :max-height="360" />Группы переживают окно. Если список прокручен внутрь группы, её заголовка в
разметке уже нет — обёртка role="group" всё равно создаётся и берёт имя через
aria-label вместо aria-labelledby.
Набор считается по группе. При virtual команды несут
aria-setsize/aria-posinset, и это размер их группы, а не всей палитры — так
того требует ARIA. В обычном режиме атрибутов нет: там набор виден по DOM.
Активная команда всегда смонтирована. Стрелки прокручивают список до неё
прежде, чем перевести aria-activedescendant. Устройство примитива —
virtual-list.md.
Клавиатура и хоткей
↑/↓ ходят по командам (disabled пропускаются, обход зациклен),
Home/End — к краям, Enter выполняет, Esc закрывает. Порядок обхода
совпадает с экранным, включая «недавние».
hotkey (по умолчанию mod+k) вешает глобальное сочетание; mod — это ⌘ на
Apple и Ctrl везде ещё. Платформа определяется после монтирования: на
сервере navigator нет, и первый клиентский рендер обязан совпасть с
серверным, иначе подсказка разъезжается при гидрации.
Императивный API
open(), close() и toggle() через ref на компоненте — тот же состав, что
у остальных оверлеев. Нужны там, где палитру открывает не её хоткей: пункт меню
«Найти команду», кнопка в шапке, ссылка из онбординга.
Палитра управляемая, поэтому методы эмитят update:modelValue — состояние
остаётся в v-model родителя (подробнее — GrModal.md).
Пропы этого примера