# CalendarBase

## Кратко

`CalendarBase` — общая основа раскладки и контекста для семейства календарей VegaUI. Он передаёт размер, вариант и компактность общим частям: заголовку, кнопкам пикера, кнопкам управления и подписям дней недели, — но сам по себе не является полноценным виджетом выбора даты.

## Импорт

```tsx
import {
  CalendarBase,
  CalendarBaseControlIconButton,
  CalendarBaseHeader,
  CalendarBasePickerButton,
  CalendarBaseWeekLabel,
  CalendarBaseWeekLabels,
  type CalendarBaseControlIconButtonProps,
  type CalendarBaseHeaderProps,
  type CalendarBasePickerButtonProps,
  type CalendarBaseProps,
  type CalendarBaseSize,
  type CalendarBaseVariant,
  type CalendarBaseWeekLabelProps,
  type CalendarBaseWeekLabelsProps,
} from '@vega-ui/react';
```

## Экспортируемые типы

- `CalendarBaseProps`
- `CalendarBaseHeaderProps`
- `CalendarBaseWeekLabelsProps`
- `CalendarBaseWeekLabelProps`
- `CalendarBasePickerButtonProps`
- `CalendarBaseControlIconButtonProps`
- `CalendarBaseVariant`
- `CalendarBaseSize`

## Минимальная композиция

```tsx
import {
  CalendarBase,
  CalendarBaseHeader,
  CalendarBaseWeekLabel,
  CalendarBaseWeekLabels,
  DayPicker,
  DayPickerCell,
  DayPickerLayout,
  Text,
} from '@vega-ui/react';

const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

export default function MinimalComposition() {
  return (
    <CalendarBase compact>
      <CalendarBaseHeader>
        <Text fontWeight={600}>November 2026</Text>
      </CalendarBaseHeader>
      <DayPicker aria-label='November 2026' locale='en-US' month='2026-11-01'>
        <CalendarBaseWeekLabels>
          {weekdays.map((weekday) => (
            <CalendarBaseWeekLabel key={weekday}>{weekday}</CalendarBaseWeekLabel>
          ))}
        </CalendarBaseWeekLabels>
        <DayPickerLayout weekStartsOn={0}>
          <DayPickerCell />
        </DayPickerLayout>
      </DayPicker>
    </CalendarBase>
  );
}
```

## Обязательные части

- `CalendarBase`: корневая раскладка и общий контекст календаря

## Необязательные части

- `CalendarBaseHeader`
- `CalendarBasePickerButton`
- `CalendarBaseControlIconButton`
- `CalendarBaseWeekLabels`
- `CalendarBaseWeekLabel`
- встроенное содержимое пикера из `DayPicker`, `MonthPicker`, `YearPicker` или собственных обёрток семейства календаря

## Порядок композиции

1. `CalendarBase`
2. необязательный `CalendarBaseHeader`
3. необязательная строка подписей дней недели
4. встроенное содержимое пикера/сетки

## Варианты

- Вариант: `primary` или `secondary`
- Размер: `xs`, `sm`, `md`, `lg`, `xl`
- Плотность: по умолчанию или `compact`

## Пограничные случаи

- `CalendarBase` — прежде всего примитив подсистемы, а не основная поверхность календаря для пользователей.
- Размер и вариант должны совпадать у заголовка и встроенного содержимого пикера.
- `compact` меняет плотность раскладки, но не логику работы с датами.
- Он должен визуально соответствовать слоям пикера, а не становиться отдельной темой или системой раскладки.

## Частые ошибки

- Считать `CalendarBase` полноценным виджетом календаря.
- Вручную пересобирать общие части заголовка календаря вместо экспортированных.
- Допускать расхождение собственных вариантов с остальным семейством календаря.

## Примеры

### Базовый

#### Базовый: календарь дней из базовых частей

`CalendarBase` — оболочка для календаря, который приложение собирает само: она передаёт `size` и `variant` кнопкам заголовка и подписям дней недели, а месяцем управляет приложение. Строка дней недели находится внутри `DayPicker`, потому что это строка сетки, и её порядок совпадает с `weekStartsOn` раскладки. `DayPicker` сохраняет активную ячейку при смене `month`, поэтому пример переносит `active` вместе с месяцем, чтобы в порядке табуляции оставалась ячейка.

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

const LOCALE = 'en-GB';
const WEEK_START = 1;
const weekdayFormat = new Intl.DateTimeFormat(LOCALE, { weekday: 'short', timeZone: 'UTC' });
const monthFormat = new Intl.DateTimeFormat(LOCALE, { month: 'long', year: 'numeric', timeZone: 'UTC' });
const weekdays = Array.from({ length: 7 }, (_, index) => weekdayFormat.format(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 DayCalendarFromBaseParts() {
  const [month, setMonth] = useState('2026-11-01');
  const [active, setActive] = useState('2026-11-01');
  const [selected, setSelected] = useState<string | null>(null);

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

  return (
    <CalendarBase compact>
      <CalendarBaseHeader>
        <CalendarBaseControlIconButton aria-label='Previous month' onClick={() => showMonth(-1)}>
          <Icon><ChevronLeft /></Icon>
        </CalendarBaseControlIconButton>
        <Text fontWeight={600} aria-live='polite' style={{ alignSelf: 'center' }}>
          {monthFormat.format(new Date(month))}
        </Text>
        <CalendarBaseControlIconButton aria-label='Next month' onClick={() => showMonth(1)}>
          <Icon><ChevronRight /></Icon>
        </CalendarBaseControlIconButton>
      </CalendarBaseHeader>
      <DayPicker
        aria-label={monthFormat.format(new Date(month))}
        locale={LOCALE}
        month={month}
        active={active}
        onChangeActive={setActive}
        selected={selected}
        onSelectCell={setSelected}
      >
        <CalendarBaseWeekLabels>
          {weekdays.map((weekday) => (
            <CalendarBaseWeekLabel key={weekday}>{weekday}</CalendarBaseWeekLabel>
          ))}
        </CalendarBaseWeekLabels>
        <DayPickerLayout weekStartsOn={WEEK_START}>
          <DayPickerCell />
        </DayPickerLayout>
      </DayPicker>
    </CalendarBase>
  );
}
```

### Контролируемый/с состоянием

#### Контролируемый/с состоянием: заголовок с динамическими подписями

Одна `CalendarBasePickerButton` переключает дни и месяцы и сообщает состояние через `aria-pressed`; подписи кнопок «назад» / «вперёд» меняются вместе с видом, потому что те же кнопки шагают на месяц или на год.

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

export const locales = ['en-US', 'ru-RU', 'ar-EG', 'he-IL'];
const WEEK_START = 0;

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 HeaderWithDynamicLabels({ locale = 'en-US' }: { locale?: string }) {
  const weekdayFormat = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
  const monthFormat = new Intl.DateTimeFormat(locale, { month: 'long', timeZone: 'UTC' });
  const yearFormat = new Intl.DateTimeFormat(locale, { year: 'numeric', timeZone: 'UTC' });
  const weekdays = Array.from({ length: 7 }, (_, index) => weekdayFormat.format(Date.UTC(2026, 10, 1 + WEEK_START + index)));

  const [view, setView] = useState<'day' | 'month'>('day');
  const [month, setMonth] = useState('2026-11-01');
  const [active, setActive] = useState('2026-11-01');

  const step = view === 'day' ? 1 : 12;

  const showMonth = (next: string) => {
    setMonth(next);
    setActive(next);
  };

  const pickMonth = (next: string) => {
    showMonth(next);
    setView('day');
  };

  const toggleView = () => {
    setActive(month);
    setView(view === 'day' ? 'month' : 'day');
  };

  return (
    <CalendarBase compact>
      <CalendarBaseHeader>
        <CalendarBaseControlIconButton
          aria-label={view === 'day' ? 'Previous month' : 'Previous year'}
          onClick={() => showMonth(addMonths(month, -step))}
        >
          <Icon><ChevronLeft /></Icon>
        </CalendarBaseControlIconButton>
        <CalendarBasePickerButton aria-pressed={view === 'month'} onClick={toggleView}>
          {monthFormat.format(new Date(month))} {yearFormat.format(new Date(month))}
        </CalendarBasePickerButton>
        <CalendarBaseControlIconButton
          aria-label={view === 'day' ? 'Next month' : 'Next year'}
          onClick={() => showMonth(addMonths(month, step))}
        >
          <Icon><ChevronRight /></Icon>
        </CalendarBaseControlIconButton>
      </CalendarBaseHeader>
      {view === 'day' && (
        <DayPicker aria-label='Days' locale={locale} month={month} active={active} onChangeActive={setActive}>
          <CalendarBaseWeekLabels>
            {weekdays.map((weekday) => (
              <CalendarBaseWeekLabel key={weekday}>{weekday}</CalendarBaseWeekLabel>
            ))}
          </CalendarBaseWeekLabels>
          <DayPickerLayout weekStartsOn={WEEK_START}>
            <DayPickerCell />
          </DayPickerLayout>
        </DayPicker>
      )}
      {view === 'month' && (
        <MonthPicker
          aria-label='Months'
          locale={locale}
          year={month}
          active={active}
          onChangeActive={setActive}
          selected={month}
          onSelectCell={pickMonth}
        >
          <MonthPickerLayout format='short'>
            <MonthPickerCell />
          </MonthPickerLayout>
        </MonthPicker>
      )}
    </CalendarBase>
  );
}
```

### Раскладка/оверлей

#### Раскладка/оверлей: два месяца в одной оболочке

Две `DayPickerLayout` внутри одного `DayPicker` позволяют диапазону пересечь границу месяца; разные `offset` сохраняют индексы строк уникальными. У каждого месяца своя строка дней недели и `aria-label` на группе строк, поэтому видимые заголовки месяцев можно скрыть от скринридера.

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

export const locales = ['en-US', 'ru-RU', 'ar-EG', 'he-IL'];
const WEEK_START = 0;

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 TwoMonthRange({ locale = 'en-US' }: { locale?: string }) {
  const weekdayFormat = new Intl.DateTimeFormat(locale, { weekday: 'narrow', timeZone: 'UTC' });
  const monthFormat = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric', timeZone: 'UTC' });
  const rangeFormat = new Intl.DateTimeFormat(locale, { month: 'short', day: 'numeric', timeZone: 'UTC' });
  const weekdays = Array.from({ length: 7 }, (_, index) => weekdayFormat.format(Date.UTC(2026, 10, 1 + WEEK_START + index)));

  const [month, setMonth] = useState('2026-11-01');
  const [active, setActive] = useState('2026-11-01');
  const [range, setRange] = useState<string[]>([]);

  const nextMonth = addMonths(month, 1);
  const [start, end] = range;

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

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-6)', justifyItems: 'start', width: '100%' }}>
      <CalendarBase>
        <CalendarBaseHeader>
          <CalendarBaseControlIconButton aria-label='Previous month' onClick={() => showMonth(-1)}>
            <Icon><ChevronLeft /></Icon>
          </CalendarBaseControlIconButton>
          <CalendarBaseControlIconButton aria-label='Next month' onClick={() => showMonth(1)}>
            <Icon><ChevronRight /></Icon>
          </CalendarBaseControlIconButton>
        </CalendarBaseHeader>
        <DayPicker
          aria-label='Trip dates'
          locale={locale}
          selection='range'
          month={month}
          active={active}
          onChangeActive={setActive}
          selected={range}
          onSelectCell={setRange}
          style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 'var(--spacing-16)', width: '100%' }}
        >
          {[month, nextMonth].map((visible, index) => (
            <div key={visible} role='presentation' style={{ display: 'grid', gap: 'var(--spacing-4)' }}>
              <Text aria-hidden fontWeight={600} style={{ textAlign: 'center' }}>{monthFormat.format(new Date(visible))}</Text>
              <CalendarBaseWeekLabels>
                {weekdays.map((weekday, col) => (
                  <CalendarBaseWeekLabel key={col}>{weekday}</CalendarBaseWeekLabel>
                ))}
              </CalendarBaseWeekLabels>
              <DayPickerLayout
                aria-label={monthFormat.format(new Date(visible))}
                month={visible}
                offset={index}
                weekStartsOn={WEEK_START}
              >
                <DayPickerCell />
              </DayPickerLayout>
            </div>
          ))}
        </DayPicker>
      </CalendarBase>
      <Text aria-live='polite' style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums' }}>
        {start && end ? rangeFormat.formatRange(new Date(start), new Date(end)) : 'Pick departure and return'}
      </Text>
    </div>
  );
}
```

### Отключённый

#### Отключённый: навигация внутри окна

`from` и `to` у `DayPicker` отключают дни вне окна, а приложение отключает кнопки «назад» / «вперёд» на первом и последнем месяце. Правило написано рядом с сеткой и связано через `aria-describedby`.

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

const LOCALE = 'en-US';
const WEEK_START = 0;
const FIRST_DAY = '2026-11-16';
const LAST_DAY = '2027-01-15';
const weekdayFormat = new Intl.DateTimeFormat(LOCALE, { weekday: 'short', timeZone: 'UTC' });
const monthFormat = new Intl.DateTimeFormat(LOCALE, { month: 'long', year: 'numeric', timeZone: 'UTC' });
const weekdays = Array.from({ length: 7 }, (_, index) => weekdayFormat.format(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);
}

function getMonthStart(day: string) {
  return `${day.slice(0, 7)}-01`;
}

export default function NavigationWithinAWindow() {
  const [month, setMonth] = useState(getMonthStart(FIRST_DAY));
  const [active, setActive] = useState(FIRST_DAY);
  const hintId = useId();

  const isFirstMonth = month === getMonthStart(FIRST_DAY);
  const isLastMonth = month === getMonthStart(LAST_DAY);

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

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', justifyItems: 'start', width: '100%' }}>
      <CalendarBase compact>
        <CalendarBaseHeader>
          <CalendarBaseControlIconButton aria-label='Previous month' disabled={isFirstMonth} onClick={() => showMonth(-1)}>
            <Icon><ChevronLeft /></Icon>
          </CalendarBaseControlIconButton>
          <Text fontWeight={600} style={{ alignSelf: 'center' }}>{monthFormat.format(new Date(month))}</Text>
          <CalendarBaseControlIconButton aria-label='Next month' disabled={isLastMonth} onClick={() => showMonth(1)}>
            <Icon><ChevronRight /></Icon>
          </CalendarBaseControlIconButton>
        </CalendarBaseHeader>
        <DayPicker
          aria-label='Return date'
          aria-describedby={hintId}
          locale={LOCALE}
          month={month}
          from={FIRST_DAY}
          to={LAST_DAY}
          active={active}
          onChangeActive={setActive}
        >
          <CalendarBaseWeekLabels>
            {weekdays.map((weekday) => (
              <CalendarBaseWeekLabel key={weekday}>{weekday}</CalendarBaseWeekLabel>
            ))}
          </CalendarBaseWeekLabels>
          <DayPickerLayout weekStartsOn={WEEK_START}>
            <DayPickerCell />
          </DayPickerLayout>
        </DayPicker>
      </CalendarBase>
      <HelperText id={hintId}>Returns are accepted from November 16 to January 15.</HelperText>
    </div>
  );
}
```

### Пограничный случай

#### Пограничный случай: компактные подписи дней недели

Узкие названия дней недели во многих локалях повторяют буквы (S, M, T, W, T, F, S), поэтому каждая подпись получает полное название в `aria-label`. `compact` с `size='xs'` держит оболочку не шире сетки.

```tsx
import {
  CalendarBase,
  CalendarBaseWeekLabel,
  CalendarBaseWeekLabels,
  DayPicker,
  DayPickerCell,
  DayPickerLayout,
} from '@vega-ui/react';

const LOCALE = 'en-US';
const WEEK_START = 0;
const narrowFormat = new Intl.DateTimeFormat(LOCALE, { weekday: 'narrow', timeZone: 'UTC' });
const longFormat = new Intl.DateTimeFormat(LOCALE, { weekday: 'long', timeZone: 'UTC' });

const weekdays = Array.from({ length: 7 }, (_, index) => {
  const date = Date.UTC(2026, 10, 1 + WEEK_START + index);

  return { narrow: narrowFormat.format(date), long: longFormat.format(date) };
});

export default function CompactWeekdayLabels() {
  return (
    <CalendarBase compact size='xs'>
      <DayPicker aria-label='November 2026' locale={LOCALE} month='2026-11-01'>
        <CalendarBaseWeekLabels>
          {weekdays.map(({ narrow, long }) => (
            <CalendarBaseWeekLabel key={long} aria-label={long} title={long}>{narrow}</CalendarBaseWeekLabel>
          ))}
        </CalendarBaseWeekLabels>
        <DayPickerLayout weekStartsOn={WEEK_START}>
          <DayPickerCell />
        </DayPickerLayout>
      </DayPicker>
    </CalendarBase>
  );
}
```

#### Пограничный случай: оболочка справа налево

`dir='rtl'` на `CalendarBase` отражает заголовок и сетку, но базовые части не меняют иконки местами: кнопке «назад» нужен шеврон вправо, а кнопке «вперёд» — влево. Кнопки навигации `Calendar` разворачивают шевроны сами.

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

const LOCALE = 'he-IL';
const WEEK_START = 0;
const weekdayFormat = new Intl.DateTimeFormat(LOCALE, { weekday: 'narrow', timeZone: 'UTC' });
const monthFormat = new Intl.DateTimeFormat(LOCALE, { month: 'long', year: 'numeric', timeZone: 'UTC' });
const weekdays = Array.from({ length: 7 }, (_, index) => weekdayFormat.format(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 RightToLeftShell() {
  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 (
    <CalendarBase compact dir='rtl' lang={LOCALE}>
      <CalendarBaseHeader>
        <CalendarBaseControlIconButton aria-label='החודש הקודם' onClick={() => showMonth(-1)}>
          <Icon><ChevronRight /></Icon>
        </CalendarBaseControlIconButton>
        <Text fontWeight={600} style={{ alignSelf: 'center' }}>{monthFormat.format(new Date(month))}</Text>
        <CalendarBaseControlIconButton aria-label='החודש הבא' onClick={() => showMonth(1)}>
          <Icon><ChevronLeft /></Icon>
        </CalendarBaseControlIconButton>
      </CalendarBaseHeader>
      <DayPicker aria-label='ימים' locale={LOCALE} month={month} active={active} onChangeActive={setActive}>
        <CalendarBaseWeekLabels>
          {weekdays.map((weekday, col) => (
            <CalendarBaseWeekLabel key={col}>{weekday}</CalendarBaseWeekLabel>
          ))}
        </CalendarBaseWeekLabels>
        <DayPickerLayout weekStartsOn={WEEK_START}>
          <DayPickerCell />
        </DayPickerLayout>
      </DayPicker>
    </CalendarBase>
  );
}
```

## Анатомия

### Обзор

`CalendarBase` даёт общую визуальную оболочку для компонентов семейства календаря. Он передаёт контекст общему заголовку, кнопкам управления, кнопкам пикера и подписям дней недели.

### Обязательные части

#### `CalendarBase`

Обязательна. Отвечает за общий размер, вариант и компактность.

### Необязательные части

#### `CalendarBaseHeader`

Необязательная строка заголовка для навигации и триггеров пикера.

#### `CalendarBasePickerButton`

Необязательная кнопка для переключения пикера месяца/года.

#### `CalendarBaseControlIconButton`

Необязательная кнопка «назад»/«вперёд».

#### `CalendarBaseWeekLabels`

Необязательная обёртка-строка для подписей дней недели.

#### `CalendarBaseWeekLabel`

Необязательная ячейка заголовка дня недели.

### Порядок композиции

1. `CalendarBase`
2. необязательный `CalendarBaseHeader`
3. встроенное содержимое пикера
4. необязательный `CalendarBaseWeekLabels` внутри пикера перед его строками: у строки подписей `role='row'`, и она принадлежит сетке

### Допустимые паттерны композиции

Рабочий календарь дней: заголовок с кнопками-иконками и подписью месяца, затем `DayPicker`, в котором строка дней недели и раскладка. Видимый месяц и активный день хранит приложение.

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

const LOCALE = 'en-GB';
const WEEK_START = 1;
const weekdayFormat = new Intl.DateTimeFormat(LOCALE, { weekday: 'short', timeZone: 'UTC' });
const monthFormat = new Intl.DateTimeFormat(LOCALE, { month: 'long', year: 'numeric', timeZone: 'UTC' });
const weekdays = Array.from({ length: 7 }, (_, index) => weekdayFormat.format(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 DayCalendarFromBaseParts() {
  const [month, setMonth] = useState('2026-11-01');
  const [active, setActive] = useState('2026-11-01');
  const [selected, setSelected] = useState<string | null>(null);

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

  return (
    <CalendarBase compact>
      <CalendarBaseHeader>
        <CalendarBaseControlIconButton aria-label='Previous month' onClick={() => showMonth(-1)}>
          <Icon><ChevronLeft /></Icon>
        </CalendarBaseControlIconButton>
        <Text fontWeight={600} aria-live='polite' style={{ alignSelf: 'center' }}>
          {monthFormat.format(new Date(month))}
        </Text>
        <CalendarBaseControlIconButton aria-label='Next month' onClick={() => showMonth(1)}>
          <Icon><ChevronRight /></Icon>
        </CalendarBaseControlIconButton>
      </CalendarBaseHeader>
      <DayPicker
        aria-label={monthFormat.format(new Date(month))}
        locale={LOCALE}
        month={month}
        active={active}
        onChangeActive={setActive}
        selected={selected}
        onSelectCell={setSelected}
      >
        <CalendarBaseWeekLabels>
          {weekdays.map((weekday) => (
            <CalendarBaseWeekLabel key={weekday}>{weekday}</CalendarBaseWeekLabel>
          ))}
        </CalendarBaseWeekLabels>
        <DayPickerLayout weekStartsOn={WEEK_START}>
          <DayPickerCell />
        </DayPickerLayout>
      </DayPicker>
    </CalendarBase>
  );
}
```

### Недопустимые паттерны композиции

#### Общие части календаря отрендерены вне `CalendarBase`

Они теряют контекст размера и варианта, который сохраняет единообразие семейства.

#### Календарь для пользователей собран из базовых частей без повторного использования поведения семейства

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