GrSidebar

Пакет: @feugene/granularityядроГруппа: Навигация

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

Когда брать

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

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

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

Лендмарк и имя

<GrSidebar landmark="navigation" aria-label="Основная навигация">

</GrSidebar>

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

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

Группы секций

<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 показывает иконку, а без неё — первую букву метки; метка уходит в title и aria-label.

Сторона и ширина

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

Клавиатура

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

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

Playground 10

Загружается…

Код
<GrSidebar />

Установка

npm i @feugene/granularity

Импорт

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

API

Props

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

Slots

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

Events

EventTypeОписание
update:collapsed[value: boolean]

Примеры 3

Базовый ряд разделов

Базовый desktop-shell: GrSidebar с v-model:collapsed и show-toggle-button, а навигация собрана из GrSidebarItem (иконка, badge, active-состояние). В свёрнутом виде пункты сохраняют иконку, а «Billing» без иконки показывает первую букву.

overview
Expanded

Toggle the sidebar: collapsed items keep their icon, and «Billing» (no icon) falls back to its first letter.

Basic Sections
<script setup lang="ts">
import { ref } from 'vue'

import { GrBadge, GrSidebar, GrSidebarGroup, GrSidebarItem } from '@feugene/granularity'

const currentSection = ref('overview')
const collapsed = ref(false)

// icon — класс UnoCSS-иконки; у «Billing» иконки нет — в свёрнутом виде покажется буква «B».
const groups = [
  {
    label: 'Workspace',
    items: [
      { label: 'Overview', value: 'overview', icon: 'i-lucide-layout-dashboard', badge: undefined as number | undefined },
      { label: 'Team', value: 'team', icon: 'i-lucide-users', badge: 4 },
    ],
  },
  {
    label: 'Administration',
    items: [
      { label: 'Billing', value: 'billing', icon: undefined, badge: undefined },
      { label: 'Settings', value: 'settings', icon: 'i-lucide-settings', badge: undefined },
    ],
  },
]
</script>

<template>
  <div class="flex min-h-[240px] gap-3">
    <GrSidebar
      v-model:collapsed="collapsed"
      title="Workspace"
      subtitle="Navigation"
      show-toggle-button
      landmark="navigation"
      aria-label="Workspace sections"
      class="rounded-xl"
    >
      <GrSidebarGroup v-for="group in groups" :key="group.label" :label="group.label">
        <div class="grid gap-1">
          <GrSidebarItem
            v-for="section in group.items"
            :key="section.value"
            :label="section.label"
            :icon="section.icon"
            :badge="section.badge"
            :active="section.value === currentSection"
            @click="currentSection = section.value"
          />
        </div>
      </GrSidebarGroup>
    </GrSidebar>

    <div class="flex-1 rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-bg)] p-4">
      <div class="flex items-center justify-between gap-3">
        <div class="text-base font-semibold capitalize">
          {{ currentSection }}
        </div>
        <GrBadge tone="neutral">
          {{ collapsed ? 'Collapsed' : 'Expanded' }}
        </GrBadge>
      </div>
      <p class="mt-2 text-sm text-[var(--gr-muted-fg)]">
        Toggle the sidebar: collapsed items keep their icon, and «Billing» (no icon) falls back to its first letter.
      </p>
    </div>
  </div>
</template>

Якоря документации

Sidebar как rail для doc anchors: кастомные <button>-пункты с active-подсветкой через --gr-sidebar-* токены и badge-маркером якоря.

Current anchor target
API

Documentation Nav
<script setup lang="ts">
import { computed, ref } from 'vue'

import { GrBadge, GrSidebar } from '@feugene/granularity'

const currentSection = ref('api')
const sections = [
  { label: 'Overview', value: 'overview' },
  { label: 'Examples', value: 'examples' },
  { label: 'API', value: 'api' },
  { label: 'Notes', value: 'notes' },
]

const activeSection = computed(() => {
  return sections.find(section => section.value === currentSection.value)?.label ?? 'API'
})
</script>

<template>
  <div class="grid gap-3 sm:grid-cols-[260px_minmax(0,1fr)]">
    <GrSidebar title="Doc sections" subtitle="Anchored navigation">
      <div class="grid gap-2">
        <button
          v-for="section in sections"
          :key="section.value"
          type="button"
          class="flex items-center justify-between rounded-md px-3 py-2 text-left text-sm transition-colors"
          :class="section.value === currentSection ? 'bg-[var(--gr-sidebar-primary)] text-[var(--gr-sidebar-primary-fg)]' : 'hover:bg-[var(--gr-sidebar-accent)] hover:text-[var(--gr-sidebar-accent-fg)]'"
          @click="currentSection = section.value"
        >
          <span>{{ section.label }}</span>
          <GrBadge size="sm" tone="neutral">
            #
          </GrBadge>
        </button>
      </div>
    </GrSidebar>

    <div class="rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-bg)] p-4">
      <div class="text-sm text-[var(--gr-muted-fg)]">
        Current anchor target
      </div>
      <div class="mt-1 text-base font-semibold">
        {{ activeSection }}
      </div>
    </div>
  </div>
</template>

Композиция панели фильтров

Sidebar как persistent container для фильтров и переключателей: набор GrSwitch, чьё состояние отражается справа через GrBadge.

Active: on Assigned: off Overdue: on

Filter Rail
<script setup lang="ts">
import { reactive } from 'vue'

import { GrBadge, GrSidebar, GrSwitch } from '@feugene/granularity'

const filters = reactive({
  active: true,
  assigned: false,
  overdue: true,
})
</script>

<template>
  <div class="grid gap-3 sm:grid-cols-[260px_minmax(0,1fr)]">
    <GrSidebar title="Filters" subtitle="Sticky control rail">
      <div class="grid gap-3">
        <label class="flex items-center justify-between gap-3 text-sm">
          Active only
          <GrSwitch v-model="filters.active" />
        </label>
        <label class="flex items-center justify-between gap-3 text-sm">
          Assigned to me
          <GrSwitch v-model="filters.assigned" />
        </label>
        <label class="flex items-center justify-between gap-3 text-sm">
          Overdue
          <GrSwitch v-model="filters.overdue" />
        </label>
      </div>
    </GrSidebar>

    <div class="rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-bg)] p-4">
      <div class="flex flex-wrap gap-2">
        <GrBadge :tone="filters.active ? 'primary' : 'neutral'">
          Active: {{ filters.active ? 'on' : 'off' }}
        </GrBadge>
        <GrBadge :tone="filters.assigned ? 'primary' : 'neutral'">
          Assigned: {{ filters.assigned ? 'on' : 'off' }}
        </GrBadge>
        <GrBadge :tone="filters.overdue ? 'primary' : 'neutral'">
          Overdue: {{ filters.overdue ? 'on' : 'off' }}
        </GrBadge>
      </div>
    </div>
  </div>
</template>

Доступность

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

Полный клавиатурный контракт пакета

Документация компонентаВсе компоненты