GrCommandPalette

  • ядро
  • Навигация
  • API · 16 props
  • ⌨ клавиши

Берут, когда команд много и они разбросаны по интерфейсу.

import { GrCommandPalette } from '@feugene/granularity/components/GrCommandPalette'

Обзор

Когда брать

  • команд много и они разбросаны по интерфейсу — палитра даёт к ним один вход вместо поиска по меню;
  • пользователь работает клавиатурой — hotkey открывает палитру откуда угодно, дальше всё делается стрелками;
  • команды сгруппированы — разделы, недавние (recentIds) и подсказки сочетаний уже есть;
  • источник асинхронный — source подгружает элементы по запросу, virtual держит длинный список.

Когда взять другое

НужноБерите
Выбирается значение поля формыGrSelect
Ищется объект с подгрузкойGrAutocomplete
Действия относятся к одному объектуGrDropdownMenu
Каталог виджетов дашбордаGrDashboardPalette
Поиск по содержимому страницы, а не по командамGrInput

Палитра не заменяет навигацию: она ускоряет работу тому, кто уже знает, что ищет. Единственный способ добраться до раздела ею быть не может — новый пользователь не догадается нажать сочетание.

Playground контролы выводятся из API, а не пишутся руками

Сцена

Этот компонент живёт поверх экрана, а не внутри холста: его открывает экран, а конструктору открывать его неоткуда. Примеры ниже показывают его в работе.

0 изменено· состояние — в адресе

Примеры 3 у каждого — живое демо и его код

  • Панель команд приложения

    Откройте кнопкой или ⌘J — hotkey вешает глобальное сочетание, и кнопка о нём говорит. Список плоский: group собирает разделы, shortcut рисует клавиши, description даёт контекст, keywords позволяют найти настройки по «vat», а disabled-команда остаётся видна вместе с причиной. recent-ids поднимают последние команды наверх; историю держит страница.

    Acme Billing
    INV-0418 · Northwind Traders€1,284.50 · due 30 Sep · 6 days overdue
  • Клиенты, которых ищет сервер

    Двенадцать тысяч клиентов в браузер не помещаются. filterable="false" отдаёт фильтрацию владельцу: запрос уходит событием search, loading показывает ожидание, а палитра рисует то, что пришло. Слот item рисует свою строку, а слот empty превращает «ничего не найдено» в «создать».

    Customers12,408 accounts · searched on the server
    Try «north» or «contoso».
  • Переключатель пространств со своим фильтром

    Свой filter находит «wwi» по инициалам, а не только по части названия. recent-ids держат два последних пространства наверху, пока запрос пуст, size="sm" делает палитру компактной, а слот footer держит клавиши там, где руки.

    WorkspaceNorthwind Traders

API порождается из пакета · разойтись не может

Props

PropTypeпо умолчаниюОписание
filterGrCommandFilter | undefinedundefinedКастомный матчер локальной фильтрации.
size"sm" | "md" | "lg" | "xl" | "full" | undefinedundefinedШирина окна по шкале панелей `GrModal`. По умолчанию `lg`; берётся из `GrConfigProvider`.
placeholderstring | undefinedundefinedПодсказка в поле поиска. По умолчанию — строка локали.
ariaLabelstring | undefinedundefinedДоступное имя палитры и поля поиска. По умолчанию — строка локали.
loadingboolean | undefinedfalseВнешне управляемое состояние загрузки (для remote-поиска).
filterableboolean | undefinedtrueЛокальная фильтрация по запросу. `false` — фильтрует владелец по событию `search`.
closeOnSelectboolean | undefinedtrueЗакрывать палитру после выбора команды.
virtualboolean | undefinedfalseВиртуализация списка: в DOM живёт только окно вокруг вьюпорта. Высоту окна задаёт `maxHeight`. Включается осознанно: на сотне команд выигрыша нет, а в разметке остаётся только окно — вместе с ним меняется и то, что находит `querySelector` потребителя. Профильный сценарий — палитра приложения на тысячи команд.
itemsGrCommandItem[] | undefinedundefinedПлоский список команд; группировка — по полю `group` самой команды.
emptyTextstring | undefinedundefinedТекст пустой выдачи. Слот `#empty` сильнее; по умолчанию — строка локали.
recentIdsstring[] | undefinedundefinedНедавние команды: пока запрос пуст, они поднимаются отдельной группой наверх в этом порядке и не дублируются ниже.
hotkeystring | null | undefined"mod+k"Глобальное сочетание открытия. `null` — не вешать слушатель.
hotkeyIdstring | undefinedundefinedИдентификатор записи в реестре (`granularityHotkeysPlugin`) вместо сочетания строкой: подсказка и привязка берутся из одного источника и не могут разойтись. Сильнее `hotkey`.
maxHeightnumber | undefined360Максимальная высота списка, px.

Слоты

SlotTypeОписание
item{ item: GrCommandItem; active: boolean; }Строка списка вместо стандартной.
empty{ query: string; }Пустое состояние: получает запрос, чтобы предложить действие по нему.
footeranyПодвал палитры: подсказки по клавишам, счётчик.

События

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

grCommandPaletteConfigconst
const grCommandPaletteConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrCommandPalette">;

Файлdist/types/components/GrCommandPalette/filtering.d.ts

GrCommandItemtype

Команда палитры. 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;
};
GrCommandGrouptype

Команды, сгруппированные для рендера: порядок групп — порядок первого вхождения.

type GrCommandGroup = {
    /** `undefined` — команды без группы (рендерятся без заголовка). */
    name: string | undefined;
    items: GrCommandItem[];
};
GrCommandFiltertype
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

GrCommandPalettePropsinterface

Публичный 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;
}
GrCommandPaletteEmitsinterface
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

GrCommandPaletteSizetype
type GrCommandPaletteSize = GrOverlaySize;

Файлdist/types/components/GrCommandPalette/safelist.d.ts

grCommandPaletteSafelistconst
const grCommandPaletteSafelist: string[];

Файлdist/types/components/GrCommandPalette/index.d.ts

GrCommandPaletteInstancetype
type GrCommandPaletteInstance = ComponentExposed<typeof GrCommandPaletteComponent>;

Доступность

Паттерн APG

dialog + listbox

Клавиши

mod+K↑ / ↓Home / EndEnter

КлавишаДействие
mod+Kоткрыть
↑ / ↓по результатам
Home / Endк краям
Enterвыполнить
Escзакрыть

Использование из прозы библиотеки

Команды

Модель плоская: 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 поля. Чтобы окно не схлопывалось между «ищем» и «нашлось», резервируйте высоту области результатов, а не списка:

css
[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. Профильный сценарий — палитра приложения на тысячи команд.

vue
<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).