GrSidebar

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

Берут, когда разделов много и они постоянны.

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

Обзор

Когда брать

  • разделов много и они постоянны — админка, почта, панель управления: список всегда под рукой;
  • места на экране мало — collapsed сворачивает панель до иконочной ширины, не убирая навигацию;
  • разделы сгруппированы — GrSidebarGroup даёт заголовки внутри списка;
  • нужен лендмарк — landmark объявляет панель ориентиром navigation или complementary.

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

НужноБерите
Панель выезжает по требованию и закрываетсяGrDrawer
Верхняя панель приложенияGrNavbar
Нижняя панель на мобильномGrBottomNav
Разделы внутри одной страницыGrTabs
Список действий по кнопкеGrDropdownMenu

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

Сцена

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

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

Примеры 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 conversations
    Export stops at 50,000 rows
    Add a second billing contact
  • Фильтры отчёта справа

    Фильтры — не навигация: корень остаётся <aside>, ориентиром complementary по умолчанию. position="right" переносит границу на внутреннюю сторону, слот title показывает число сужающих фильтров, а слот subtitle над ним называет отчёт. Содержимое — обычная разметка, группы флажков, — прокручивается само.

    MRR by plan€36.0k across 3 segments
    Team · EU€18.9k
    Team · UK€7.3k
    Business · EU€9.8k
  • То же меню на телефоне

    Колонке негде поместиться, и overlay превращает панель в модальный слой: подложка, Esc, ловушка фокуса, заблокированная прокрутка страницы и фокус обратно на «Menu». Когда переключаться — решает приложение: брейкпоинтов у пакета нет, а matchMedia в setup разошёлся бы с гидрацией. В слое кнопка шапки закрывает панель, а не сворачивает.

    Overdue invoices
    4 invoices, €3,912.40 — the oldest 23 days late

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

Props

PropTypeпо умолчаниюОписание
titlestring | undefinedundefinedЗаголовок шапки — название продукта или раздела. В свёрнутой панели скрыт; слот `#title` сильнее.
openv-modelboolean | undefinedfalseОткрыт ли слой. Поддерживает `v-model:open`; без `overlay` не значит ничего.
ariaLabelstring | undefinedundefinedИмя лендмарка: без него две панели на странице неразличимы.
position"right" | "left" | undefined"left"Сторона экрана: граница и направление шеврона зеркалятся.
landmark"complementary" | "navigation" | undefined"complementary"Лендмарк корня. `complementary` (по умолчанию) — `<aside>`; `navigation` — `<nav>` для панели, которая действительно является навигацией. Вложенный `<nav>` внутрь `<aside>` не заводим: два лендмарка на одну панель засоряют обзор, а панель фильтров навигацией не является вовсе.
widthstring | undefined"240px"Ширина в развёрнутом состоянии.
subtitlestring | undefinedundefinedПриглушённая строка над заголовком — например, рабочее пространство. Слот `#subtitle` сильнее.
collapsedv-modelboolean | undefinedfalseСвёрнутое состояние. Поддерживает `v-model:collapsed`.
showToggleButtonboolean | undefinedfalseПоказать кнопку сворачивания/разворачивания в хедере.
collapsedWidthstring | undefined"64px"Ширина в свёрнутом состоянии.
toggleLabelstring | undefinedundefinedA11y-лейбл кнопки тогла. Не задан — берётся из локали (`gr.sidebar.*`).
overlayboolean | undefinedfalseПанель как модальный слой поверх страницы: подложка, `Esc`, ловушка фокуса, блокировка прокрутки. Для узкого экрана, где колонке места нет. Когда включать — решает приложение: своей системы брейкпоинтов у пакета нет, а спрашивать среду в `setup` нельзя — первый клиентский рендер разошёлся бы с серверным и сломал гидрацию.

Слоты

SlotTypeОписание
defaultanyСодержимое панели: навигация, группы, произвольная разметка.
titleanyЗаголовок шапки вместо пропа `title`.
subtitleanyПодзаголовок под заголовком.

События

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

GrSidebarPropsinterface
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;
}
GrSidebarEmitsinterface
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

GrSidebarGroupPropsinterface
interface GrSidebarGroupProps {
    /** Заголовок секции. Слот `#label` сильнее. */
    label?: string;
}

Файлdist/types/components/GrSidebar/config.d.ts

grSidebarConfigconst
const grSidebarConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrSidebar">;

Файлdist/types/components/GrSidebar/grSidebarStyles.d.ts

GrSidebarPositiontype
type GrSidebarPosition = typeof GR_SIDEBAR_POSITIONS[number];
GrSidebarLandmarktype
type GrSidebarLandmark = typeof GR_SIDEBAR_LANDMARKS[number];

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

grSidebarSafelistconst
const grSidebarSafelist: string[];

Файлdist/types/components/GrSidebar/GrSidebarItem.vue.d.ts

GrSidebarItemPropsinterface
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;
}
GrSidebarItemEmitsinterface
interface GrSidebarItemEmits {
    (e: 'update:expanded', value: boolean): void;
}

Доступность

Паттерн APG

—

КлавишаДействие
—кнопка сворачивания — обычная кнопка (Enter/Space), фокус на ней остаётся, поэтому повторное нажатие возвращает панель. Содержимое сайдбара — скроллер и потому таб-стоп (tabindex="0"). Свёрнутый пункт под фокусом показывает подпись подсказкой: у нативного title этого не было, и рейл из одних иконок читался только мышью. Пункт с подпунктами — обычная кнопка-раскрывашка (Enter/Space
—aria-expanded); свёрнутое поддерево не отрисовано, поэтому в таб-порядок не попадает

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

Лендмарк и имя
vue
<GrSidebar landmark="navigation" aria-label="Основная навигация">
  …
</GrSidebar>

Корень — <aside> (landmark="complementary", по умолчанию) или <nav> (landmark="navigation"). Вложенного <nav> внутрь <aside> компонент не рисует: два лендмарка на одну панель засоряют обзор диктора, а панель фильтров навигацией не является вовсе — сторона потребителя решает, чем панель является.

ariaLabel обязателен там, где панелей на странице больше одной: без имени лендмарки в обзоре неразличимы.

Шапка

Шапка рисуется, когда в ней есть что показать: заголовок или подзаголовок — пропом title/subtitle или слотом #title/#subtitle, — либо кнопка showToggleButton. Слот сильнее пропа, и одного слота достаточно:

vue
<template>
  <GrSidebar position="right" aria-label="Report filters">
    <template #title>
      Filters <GrBadge>2</GrBadge>
    </template>
    <template #subtitle>
      Applied to every chart
    </template>
  </GrSidebar>
</template>
Группы секций
vue
<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, — своей копии у панели нет.

vue
<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 сильнее;
  • до монтирования слой не рисуется вовсе. Телепорт включается только после монтирования (иначе первый клиентский рендер разойдётся с серверным), и панель в этом кадре не должна мелькать в потоке страницы.
Вложенные пункты

Подпункты кладутся в сам пункт — и он становится раскрывающейся веткой:

vue
<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 и его ветку.

Сторона и ширина
ПропЧто делает
positionleft (по умолчанию) или right: граница переезжает на внутреннюю сторону, шевроны зеркалятся
width / collapsedWidthширины развёрнутого и свёрнутого состояния
Клавиатура

Контейнер контента прокручивается и потому является остановкой Tab с видимым фокус-кольцом: иначе панель с текстовым содержимым не проскроллить без мыши.

Недоступный пункт (disabled) перестаёт быть кнопкой — он рендерится <span>, не ловит фокус и гасится токеном --gr-disabled-fg, а не прозрачностью.