# CalendarBase: Руководства

## Паттерны

### Общий заголовок календаря

Когда использовать:

- пикеру нужен заголовок со своим листанием, например страница лет с подписью диапазона

Заметки о композиции:

- задавайте размер и вариант на корне
- рендерите кнопки управления и пикера внутри `CalendarBaseHeader`, каждой кнопке-иконке — `aria-label`
- страницей управляет приложение: меняйте `year` (или `start`) и `active` вместе, чтобы в порядке табуляции оставалась ячейка сетки

Компромиссы:

- единообразное оформление семейства
- логика встроенного пикера всё равно нужна отдельно

```tsx
import {
  CalendarBase,
  CalendarBaseControlIconButton,
  CalendarBaseHeader,
  Icon,
  Text,
  YearPicker,
  YearPickerCell,
  YearPickerLayout,
} from '@vega-ui/react';
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';
import { useState } from 'react';

const PAGE_SIZE = 12;

function toYearKey(year: number) {
  return `${year}-01-01`;
}

export default function SharedCalendarHeader() {
  const [firstYear, setFirstYear] = useState(2016);
  const [active, setActive] = useState(toYearKey(2016));
  const [selected, setSelected] = useState<string | null>(null);

  const lastYear = firstYear + PAGE_SIZE - 1;

  const showPage = (amount: number) => {
    const next = firstYear + amount * PAGE_SIZE;
    setFirstYear(next);
    setActive(toYearKey(next));
  };

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-6)', justifyItems: 'start', width: '100%' }}>
      <CalendarBase style={{ width: '100%' }}>
        <CalendarBaseHeader>
          <CalendarBaseControlIconButton aria-label='Previous 12 years' onClick={() => showPage(-1)}>
            <Icon><ChevronLeft /></Icon>
          </CalendarBaseControlIconButton>
          <Text fontWeight={600} style={{ alignSelf: 'center' }}>{firstYear}–{lastYear}</Text>
          <CalendarBaseControlIconButton aria-label='Next 12 years' onClick={() => showPage(1)}>
            <Icon><ChevronRight /></Icon>
          </CalendarBaseControlIconButton>
        </CalendarBaseHeader>
        <YearPicker
          aria-label={`Graduation year, ${firstYear} to ${lastYear}`}
          locale='en-US'
          year={toYearKey(firstYear)}
          active={active}
          onChangeActive={setActive}
          selected={selected}
          onSelectCell={setSelected}
        >
          <YearPickerLayout start={toYearKey(firstYear)} offset={0}>
            <YearPickerCell />
          </YearPickerLayout>
        </YearPicker>
      </CalendarBase>
      <Text aria-live='polite' style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums' }}>Graduation year: {selected ? selected.slice(0, 4) : 'not set'}</Text>
    </div>
  );
}
```

### Общая строка дней недели

Когда использовать:

- производное представление сетки дней, например полоса из одной недели, должно повторно использовать стандартное отображение дней недели

Заметки о композиции:

- оставляйте ровно семь подписей и размещайте их внутри пикера, потому что строка подписей — строка сетки
- выравнивайте их по встроенной сетке дней: здесь и подписи, и ячейки строятся из одних и тех же семи ISO-дней

Компромиссы:

- переиспользуемо и единообразно
- выравнивание по-прежнему зависит от логики нижележащей сетки

```tsx
import {
  CalendarBase,
  CalendarBaseControlIconButton,
  CalendarBaseHeader,
  CalendarBaseWeekLabel,
  CalendarBaseWeekLabels,
  DayPicker,
  DayPickerItem,
  DayPickerRow,
  DayPickerRowGroup,
  Icon,
  Text,
} from '@vega-ui/react';
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';
import { useState } from 'react';

const LOCALE = 'en-GB';
const DAY = 86_400_000;
const weekdayFormat = new Intl.DateTimeFormat(LOCALE, { weekday: 'short', timeZone: 'UTC' });
const dayFormat = new Intl.DateTimeFormat(LOCALE, { day: 'numeric', timeZone: 'UTC' });
const rangeFormat = new Intl.DateTimeFormat(LOCALE, { day: 'numeric', month: 'short', timeZone: 'UTC' });
const fullyBooked = ['2026-11-04', '2026-11-12'];

function getWeek(monday: string) {
  return Array.from({ length: 7 }, (_, index) => new Date(Date.parse(monday) + index * DAY).toISOString().slice(0, 10));
}

function addWeeks(monday: string, amount: number) {
  return new Date(Date.parse(monday) + amount * 7 * DAY).toISOString().slice(0, 10);
}

export default function SharedWeekdayRow() {
  const [monday, setMonday] = useState('2026-11-02');
  const [active, setActive] = useState('2026-11-02');
  const [selected, setSelected] = useState<string | null>(null);

  const week = getWeek(monday);

  const showWeek = (amount: number) => {
    const next = addWeeks(monday, amount);
    setMonday(next);
    setActive(next);
  };

  return (
    <CalendarBase compact>
      <CalendarBaseHeader>
        <CalendarBaseControlIconButton aria-label='Previous week' onClick={() => showWeek(-1)}>
          <Icon><ChevronLeft /></Icon>
        </CalendarBaseControlIconButton>
        <Text fontWeight={600} style={{ alignSelf: 'center' }}>
          {rangeFormat.format(new Date(week[0]))} – {rangeFormat.format(new Date(week[6]))}
        </Text>
        <CalendarBaseControlIconButton aria-label='Next week' onClick={() => showWeek(1)}>
          <Icon><ChevronRight /></Icon>
        </CalendarBaseControlIconButton>
      </CalendarBaseHeader>
      <DayPicker
        aria-label='Appointment day'
        locale={LOCALE}
        month={monday}
        active={active}
        onChangeActive={setActive}
        selected={selected}
        onSelectCell={setSelected}
        disabled={fullyBooked}
      >
        <CalendarBaseWeekLabels>
          {week.map((day) => (
            <CalendarBaseWeekLabel key={day}>{weekdayFormat.format(new Date(day))}</CalendarBaseWeekLabel>
          ))}
        </CalendarBaseWeekLabels>
        <DayPickerRowGroup>
          <DayPickerRow row={0}>
            {week.map((day, col) => (
              <DayPickerItem key={day} col={col} value={day}>{dayFormat.format(new Date(day))}</DayPickerItem>
            ))}
          </DayPickerRow>
        </DayPickerRowGroup>
      </DayPicker>
    </CalendarBase>
  );
}
```

### Компактная встроенная оболочка

Когда использовать:

- поверхности календаря нужна меньшая ширина, например в боковой панели или карточке

Заметки о композиции:

- используйте `compact` с маленьким `size`, узкие названия дней недели с полными названиями в `aria-label` и короткий формат месяца
- проверьте, что встроенный пикер по-прежнему помещается без обрезки

Компромиссы:

- более плотное отображение
- крупное содержимое пикера легче «зажать»

```tsx
import {
  CalendarBase,
  CalendarBaseControlIconButton,
  CalendarBaseHeader,
  CalendarBaseWeekLabel,
  CalendarBaseWeekLabels,
  Card,
  DayPicker,
  DayPickerCell,
  DayPickerLayout,
  Heading,
  Icon,
  Text,
} from '@vega-ui/react';
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';
import { useState } from 'react';

const LOCALE = 'en-US';
const WEEK_START = 0;
const weekdayFormat = new Intl.DateTimeFormat(LOCALE, { weekday: 'narrow', timeZone: 'UTC' });
const longWeekdayFormat = new Intl.DateTimeFormat(LOCALE, { weekday: 'long', timeZone: 'UTC' });
const monthFormat = new Intl.DateTimeFormat(LOCALE, { month: 'short', year: 'numeric', timeZone: 'UTC' });
const weekdays = Array.from({ length: 7 }, (_, index) => Date.UTC(2026, 10, 1 + WEEK_START + index));

function addMonths(month: string, amount: number) {
  const date = new Date(month);
  date.setUTCMonth(date.getUTCMonth() + amount);

  return date.toISOString().slice(0, 10);
}

export default function CompactEmbeddedShell() {
  const [month, setMonth] = useState('2026-11-01');
  const [active, setActive] = useState('2026-11-01');

  const showMonth = (amount: number) => {
    const next = addMonths(month, amount);
    setMonth(next);
    setActive(next);
  };

  return (
    <Card style={{ display: 'grid', gap: 'var(--spacing-6)', inlineSize: 'fit-content' }}>
      <Heading as='h3' size={3}>Team calendar</Heading>
      <CalendarBase compact size='xs'>
        <CalendarBaseHeader>
          <CalendarBaseControlIconButton aria-label='Previous month' onClick={() => showMonth(-1)}>
            <Icon size='xs'><ChevronLeft /></Icon>
          </CalendarBaseControlIconButton>
          <Text size={2} fontWeight={600} style={{ alignSelf: 'center' }}>{monthFormat.format(new Date(month))}</Text>
          <CalendarBaseControlIconButton aria-label='Next month' onClick={() => showMonth(1)}>
            <Icon size='xs'><ChevronRight /></Icon>
          </CalendarBaseControlIconButton>
        </CalendarBaseHeader>
        <DayPicker aria-label='Team calendar' locale={LOCALE} month={month} active={active} onChangeActive={setActive}>
          <CalendarBaseWeekLabels>
            {weekdays.map((date) => (
              <CalendarBaseWeekLabel key={date} aria-label={longWeekdayFormat.format(date)}>
                {weekdayFormat.format(date)}
              </CalendarBaseWeekLabel>
            ))}
          </CalendarBaseWeekLabels>
          <DayPickerLayout weekStartsOn={WEEK_START}>
            <DayPickerCell />
          </DayPickerLayout>
        </DayPicker>
      </CalendarBase>
    </Card>
  );
}
```

## Сравнение

### Быстрое правило выбора

Используйте `CalendarBase`, когда расширяете саму подсистему календаря. Используйте `Calendar`, `DayPicker`, `MonthPicker` или `YearPicker`, когда нужно реальное поведение выбора даты.

### `CalendarBase` или `Calendar`

- Используйте `CalendarBase` для композиции общей оболочки.
- Используйте `Calendar` для подсистемы календаря, ориентированной на пользователей.

### `CalendarBase` или компоненты-пикеры

- Используйте `CalendarBase` для общего оформления.
- Используйте `DayPicker`, `MonthPicker` или `YearPicker` для реального выбора в сетке.

### `CalendarBase` или универсальные контейнеры раскладки

- Используйте `CalendarBase`, когда оболочка должна оставаться согласованной с контекстом размера и варианта семейства календаря.
- Используйте `Card`, `Sheet` или другой примитив раскладки, когда содержимое на самом деле не относится к семейству календаря.

### Выбирайте этот компонент, когда

- вы расширяете внутренности семейства календаря
- нужно единообразное оформление заголовка и подписей календаря

### Не выбирайте этот компонент, когда

- нужен готовый пикер
- нужна лишь универсальная панель или раскладка карточки

## Решение проблем

### Оформление календаря выглядит правильно, но поведение пикера кажется неполным

#### Симптом

Заголовок и подписи рендерятся, но календарь ведёт себя не как полноценный пикер.

#### Вероятная причина

`CalendarBase` использовали как готовый календарь, а не как оболочку.

#### Как проверить

- проверьте, встроен ли в оболочку реальный компонент-пикер

#### Исправление

Скомпонуйте `CalendarBase` с `DayPicker`, `MonthPicker`, `YearPicker` или календарём более высокого уровня.

### Общие элементы управления не совпадают со встроенным пикером

#### Симптом

Кнопки или подписи заголовка визуально не сочетаются с содержимым пикера ниже.

#### Вероятная причина

Контекст размера или варианта разошёлся между оболочкой и встроенным пикером.

#### Как проверить

- проверьте `size` и `variant`, переданные всей композиции

#### Исправление

Рассматривайте оболочку и встроенный пикер как одну поверхность семейства календаря и согласуйте их общий визуальный контекст.

### Компактный режим обрезает содержимое

#### Симптом

Оболочка становится слишком узкой, и встроенному содержимому тесно.

#### Вероятная причина

`compact` включён для содержимого, которому по-прежнему нужно больше ширины.

#### Как проверить

- сравните одну и ту же композицию с `compact` и без него

#### Исправление

Отключите `compact` или соответствующим образом уменьшите плотность встроенного пикера.
