GrSidebar
Берут, когда разделов много и они постоянны.
Обзор
Когда брать
- разделов много и они постоянны — админка, почта, панель управления: список всегда под рукой;
- места на экране мало —
collapsedсворачивает панель до иконочной ширины, не убирая навигацию; - разделы сгруппированы —
GrSidebarGroupдаёт заголовки внутри списка; - нужен лендмарк —
landmarkобъявляет панель ориентиромnavigationилиcomplementary.
Когда взять другое
| Нужно | Берите |
|---|---|
| Панель выезжает по требованию и закрывается | GrDrawer |
| Верхняя панель приложения | GrNavbar |
| Нижняя панель на мобильном | GrBottomNav |
| Разделы внутри одной страницы | GrTabs |
| Список действий по кнопке | GrDropdownMenu |
Playground контролы выводятся из API, а не пишутся руками
Этот компонент живёт поверх экрана, а не внутри холста: его открывает экран, а конструктору открывать его неоткуда. Примеры ниже показывают его в работе.
0 изменено· состояние — в адресе
<GrSidebar>Текст</GrSidebar>Примеры 4 у каждого — живое демо и его код
Навигация бэк-офиса биллинга
Панель действительно навигация, поэтому
landmark="navigation"делает корень<nav>, аaria-labelотличает его от других ориентиров. Группы называют наборы пунктов; «Invoices» держит подпункты и становится веткой — кнопкой, которая раскрывает поддерево, а не ссылкой, — сv-model:expanded. Свёрнутая ветка показывает себя активной, когда открыта одна из её страниц; интеграция не из этого тарифа —disabled, и Tab её пропускает.Overdue invoices4 invoices, €3,912.40 — the oldest 23 days lateРейка, которая отдаёт место
Агент работает в переписке, а не в меню:
v-model:collapsedсворачивает панель доcollapsed-width, аshow-toggle-buttonставит переключатель в шапку со своимtoggle-label. Имя остаётся у каждого пункта — вaria-labelи подсказкой при наведении и фокусе; у «Refunds» нет иконки, и он показывает первую букву. Заголовки групп уступают место разделителям.2 conversationsExport stops at 50,000 rowsAdd a second billing contactФильтры отчёта справа
Фильтры — не навигация: корень остаётся
<aside>, ориентиромcomplementaryпо умолчанию.position="right"переносит границу на внутреннюю сторону, слотtitleпоказывает число сужающих фильтров, а слотsubtitleнад ним называет отчёт. Содержимое — обычная разметка, группы флажков, — прокручивается само.MRR by plan€36.0k across 3 segmentsTeam · EU€18.9kTeam · UK€7.3kBusiness · EU€9.8k
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
title | string | undefined | undefined | Заголовок шапки — название продукта или раздела. В свёрнутой панели скрыт; слот `#title` сильнее. |
openv-model | boolean | undefined | false | Открыт ли слой. Поддерживает `v-model:open`; без `overlay` не значит ничего. |
ariaLabel | string | undefined | undefined | Имя лендмарка: без него две панели на странице неразличимы. |
position | "right" | "left" | undefined | "left" | Сторона экрана: граница и направление шеврона зеркалятся. |
landmark | "complementary" | "navigation" | undefined | "complementary" | Лендмарк корня. `complementary` (по умолчанию) — `<aside>`; `navigation` — `<nav>` для панели, которая действительно является навигацией. Вложенный `<nav>` внутрь `<aside>` не заводим: два лендмарка на одну панель засоряют обзор, а панель фильтров навигацией не является вовсе. |
width | string | undefined | "240px" | Ширина в развёрнутом состоянии. |
subtitle | string | undefined | undefined | Приглушённая строка над заголовком — например, рабочее пространство. Слот `#subtitle` сильнее. |
collapsedv-model | boolean | undefined | false | Свёрнутое состояние. Поддерживает `v-model:collapsed`. |
showToggleButton | boolean | undefined | false | Показать кнопку сворачивания/разворачивания в хедере. |
collapsedWidth | string | undefined | "64px" | Ширина в свёрнутом состоянии. |
toggleLabel | string | undefined | undefined | A11y-лейбл кнопки тогла. Не задан — берётся из локали (`gr.sidebar.*`). |
overlay | boolean | undefined | false | Панель как модальный слой поверх страницы: подложка, `Esc`, ловушка фокуса, блокировка прокрутки. Для узкого экрана, где колонке места нет. Когда включать — решает приложение: своей системы брейкпоинтов у пакета нет, а спрашивать среду в `setup` нельзя — первый клиентский рендер разошёлся бы с серверным и сломал гидрацию. |
Слоты
| Slot | Type | Описание |
|---|---|---|
default | any | Содержимое панели: навигация, группы, произвольная разметка. |
title | any | Заголовок шапки вместо пропа `title`. |
subtitle | any | Подзаголовок под заголовком. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:open | (event: "update:open", value: boolean): void | — |
update:collapsed | (event: "update:collapsed", value: boolean): void | — |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrSidebarProps, GrSidebarEmits, GrSidebarGroupProps } from '@feugene/granularity/components/GrSidebar'Файлdist/types/components/GrSidebar/GrSidebar.vue.d.ts
interface GrSidebarProps {
/** Заголовок шапки — название продукта или раздела. В свёрнутой панели скрыт; слот `#title` сильнее. */
title?: string;
/** Приглушённая строка над заголовком — например, рабочее пространство. Слот `#subtitle` сильнее. */
subtitle?: string;
/** Свёрнутое состояние. Поддерживает `v-model:collapsed`. */
collapsed?: boolean;
/** Показать кнопку сворачивания/разворачивания в хедере. */
showToggleButton?: boolean;
/** Ширина в развёрнутом состоянии. */
width?: string;
/** Ширина в свёрнутом состоянии. */
collapsedWidth?: string;
/** A11y-лейбл кнопки тогла. Не задан — берётся из локали (`gr.sidebar.*`). */
toggleLabel?: string;
/** Сторона экрана: граница и направление шеврона зеркалятся. */
position?: GrSidebarPosition;
/**
* Лендмарк корня. `complementary` (по умолчанию) — `<aside>`; `navigation` —
* `<nav>` для панели, которая действительно является навигацией. Вложенный
* `<nav>` внутрь `<aside>` не заводим: два лендмарка на одну панель засоряют
* обзор, а панель фильтров навигацией не является вовсе.
*/
landmark?: GrSidebarLandmark;
/** Имя лендмарка: без него две панели на странице неразличимы. */
ariaLabel?: string;
/**
* Панель как модальный слой поверх страницы: подложка, `Esc`, ловушка фокуса,
* блокировка прокрутки. Для узкого экрана, где колонке места нет.
*
* Когда включать — решает приложение: своей системы брейкпоинтов у пакета нет,
* а спрашивать среду в `setup` нельзя — первый клиентский рендер разошёлся бы
* с серверным и сломал гидрацию.
*/
overlay?: boolean;
/** Открыт ли слой. Поддерживает `v-model:open`; без `overlay` не значит ничего. */
open?: boolean;
}interface GrSidebarEmits {
/**
* Панель свернули или развернули (`v-model:collapsed`): кнопкой в шапке или
* нажатием на ветку в свёрнутом рейле, которое возвращает панели ширину.
*/
(e: 'update:collapsed', value: boolean): void;
/**
* Слой `overlay` закрыт — `Esc`, подложкой или кнопкой в шапке (`v-model:open`).
* Приходит только `false`.
*/
(e: 'update:open', value: boolean): void;
}Файлdist/types/components/GrSidebar/GrSidebarGroup.vue.d.ts
interface GrSidebarGroupProps {
/** Заголовок секции. Слот `#label` сильнее. */
label?: string;
}Файлdist/types/components/GrSidebar/config.d.ts
const grSidebarConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrSidebar">;Файлdist/types/components/GrSidebar/grSidebarStyles.d.ts
type GrSidebarPosition = typeof GR_SIDEBAR_POSITIONS[number];type GrSidebarLandmark = typeof GR_SIDEBAR_LANDMARKS[number];Файлdist/types/components/GrSidebar/safelist.d.ts
const grSidebarSafelist: string[];Файлdist/types/components/GrSidebar/GrSidebarItem.vue.d.ts
interface GrSidebarItemProps {
label: string;
/**
* Иконка: Vue-компонент либо класс иконки (`'i-lucide-home'` — правило для
* класса заводит приложение: фабрике движка или провайдером, см.
* `docs/installation.md#иконки`).
*/
icon?: string | Component;
href?: string;
as?: string | Component;
active?: boolean;
disabled?: boolean;
badge?: string | number;
/**
* Раскрыто ли поддерево. Поддерживает `v-model:expanded`; без пропа пункт
* ведёт себя сам.
*/
expanded?: boolean;
/** Начальное состояние поддерева, когда `expanded` не контролируется. */
defaultExpanded?: boolean;
}interface GrSidebarItemEmits {
(e: 'update:expanded', value: boolean): void;
}Доступность
Паттерн APG
—
| Клавиша | Действие |
|---|---|
| — | кнопка сворачивания — обычная кнопка (Enter/Space), фокус на ней остаётся, поэтому повторное нажатие возвращает панель. Содержимое сайдбара — скроллер и потому таб-стоп (tabindex="0"). Свёрнутый пункт под фокусом показывает подпись подсказкой: у нативного title этого не было, и рейл из одних иконок читался только мышью. Пункт с подпунктами — обычная кнопка-раскрывашка (Enter/Space |
| — | aria-expanded); свёрнутое поддерево не отрисовано, поэтому в таб-порядок не попадает |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Лендмарк и имя
<GrSidebar landmark="navigation" aria-label="Основная навигация">
…
</GrSidebar>Корень — <aside> (landmark="complementary", по умолчанию) или <nav>
(landmark="navigation"). Вложенного <nav> внутрь <aside> компонент не
рисует: два лендмарка на одну панель засоряют обзор диктора, а панель фильтров
навигацией не является вовсе — сторона потребителя решает, чем панель является.
ariaLabel обязателен там, где панелей на странице больше одной: без имени
лендмарки в обзоре неразличимы.
Шапка
Шапка рисуется, когда в ней есть что показать: заголовок или подзаголовок —
пропом title/subtitle или слотом #title/#subtitle, — либо кнопка
showToggleButton. Слот сильнее пропа, и одного слота достаточно:
<template>
<GrSidebar position="right" aria-label="Report filters">
<template #title>
Filters <GrBadge>2</GrBadge>
</template>
<template #subtitle>
Applied to every chart
</template>
</GrSidebar>
</template>Группы секций
<GrSidebarGroup label="Администрирование">
<GrSidebarItem label="Оплата" icon="i-lucide-credit-card" />
<GrSidebarItem label="Настройки" icon="i-lucide-settings" />
</GrSidebarGroup>Группа объявлена role="group" и связана со своим заголовком через
aria-labelledby. В свёрнутой панели заголовку негде поместиться: он скрывается,
а секции начинает разделять линия — без неё иконки соседних групп сливаются в
один столбец. Имя группы при этом не берётся из пустоты: aria-labelledby
снимается вместе с заголовком.
Сворачивание
collapsed поддерживает v-model:collapsed, панель работает и как
управляемая, и как самостоятельная. show-toggle-button добавляет кнопку;
её подпись приходит из локали (gr.sidebar.expand / gr.sidebar.collapse) и
переопределяется пропом toggleLabel.
Шеврон всегда указывает туда, куда уедет панель: у правой панели «свернуть» — это стрелка вправо.
В свёрнутом виде GrSidebarItem показывает иконку, а без неё — первую букву
метки. Имя пункту даёт aria-label, а подпись показывает GrTooltip — сбоку
от панели, в её сторону: у левой вправо, у правой влево.
Подсказка появляется и по фокусу, а не только по наведению: пройти свёрнутую
панель табом и понять, где ты, иначе было нельзя — нативный title клавиатуре
не отвечает вовсе.
Модальный слой на узком экране
overlay превращает панель в модальный слой: подложка, Esc, ловушка фокуса,
блокировка прокрутки, возврат фокуса на открывший элемент. Всё это берётся из
общего useModalOverlay, которым живут GrDrawer и GrPopover, — своей копии
у панели нет.
<GrSidebar v-model:open="menu" :overlay="isNarrow" aria-label="Разделы">
…
</GrSidebar>Когда включать — решает приложение. Своей системы брейкпоинтов у пакета нет,
а спрашивать среду в setup нельзя: matchMedia даёт на сервере и на первом
клиентском рендере разные ответы, и гидрация расходится. Приложение знает свои
точки перелома и передаёт готовый ответ пропом.
Разметка панели одна на оба режима. Обёртка слоя рисуется всегда, но вне
overlay она display: contents и из раскладки исчезает. Продублируй мы панель
на два режима, копии разошлись бы молча. Отсюда следствие: корень компонента —
эта обёртка, поэтому атрибуты потребителя уходят на саму панель явным
v-bind, а не наследованием.
Что меняется в слое:
- свёрнутость игнорируется — рейл шириной в иконку внутри модального окна это
половина экрана, потраченная на пустоту; панель держит полную
widthи заголовок; - кнопка в шапке закрывает, а не сворачивает: сворачивать там нечего, а
закрыть иначе можно только
Escили подложкой. И называется она так же — «Close sidebar» (gr.sidebar.close), а не «Collapse sidebar»;toggleLabelсильнее; - до монтирования слой не рисуется вовсе. Телепорт включается только после монтирования (иначе первый клиентский рендер разойдётся с серверным), и панель в этом кадре не должна мелькать в потоке страницы.
Вложенные пункты
Подпункты кладутся в сам пункт — и он становится раскрывающейся веткой:
<GrSidebarItem label="Оплата" icon="i-lucide-credit-card" default-expanded>
<GrSidebarItem label="Счета" />
<GrSidebarItem label="Тариф" />
</GrSidebarItem>Глубина считается разметкой, а не пропом. Каждый пункт объявляет своим детям
уровень на единицу больше своего, поэтому переставленную ветку не нужно
перенумеровывать: уровни пересчитаются сами. Отступ задаётся строке целиком, а не
её тексту, — сдвинь мы только подпись, активный фон уезжал бы под соседний
уровень. Шаг настраивается токеном --gr-sidebar-nest-indent.
Раскрытие живёт локально (default-expanded задаёт начальное), а v-model:expanded
забирает его наружу.
Ветка не может быть одновременно ссылкой. Одно нажатие не в состоянии и
увести на страницу, и раскрыть поддерево, поэтому подпункты сильнее: пункт с
ними становится <button aria-expanded>, а href и as не используются. В
dev-сборке об этом предупреждает консоль — тихо снятая навигация по поведению
неотличима от забытой.
Свёрнутое поддерево не рисуется, а не прячется: невидимая ветка ловила бы
Tab и читалась бы диктором вопреки aria-expanded="false".
В свёрнутом рейле подпунктов нет
В шестьдесят четыре пикселя они не влезают, а всплывающее подменю — другая, куда более крупная история. Поэтому рейл показывает только верхний уровень, а нажатие на ветку сперва возвращает панели ширину и раскрывает её: иначе это была бы кнопка, от которой ничего не происходит.
Отсюда следствие для потребителя: пока панель свёрнута, активный подпункт ничем
себя не показывает. Если это важно, помечайте active и его ветку.
Сторона и ширина
| Проп | Что делает |
|---|---|
position | left (по умолчанию) или right: граница переезжает на внутреннюю сторону, шевроны зеркалятся |
width / collapsedWidth | ширины развёрнутого и свёрнутого состояния |
Клавиатура
Контейнер контента прокручивается и потому является остановкой Tab с видимым
фокус-кольцом: иначе панель с текстовым содержимым не проскроллить без мыши.
Недоступный пункт (disabled) перестаёт быть кнопкой — он рендерится <span>,
не ловит фокус и гасится токеном --gr-disabled-fg, а не прозрачностью.
Пропы этого примера