GrCalendar
Берут, когда календарь и есть экран.
Когда брать
- календарь и есть экран — страница брони, планировщик, панель выбора дня: сетка нужна открытой, а не по клику в поле;
- в ячейках своё содержимое — слот
dayрисует загрузку, цену, число событий поверх дня; - сетка нужна рядом с чем-то ещё — своя панель, свои кнопки, свой подвал
через
headerиfooter; - выбор дня не связан с формой — значение уходит в состояние экрана, а не в поле ввода.
Когда взять другое
| Нужно | Берите |
|---|---|
| Дата как значение поля формы | GrDatePicker |
| Период «с — по» | GrDateRangePicker |
| Дата со временем | GrDateTimePicker |
| Только время | GrTimePicker |
| События во времени лентой, а не сеткой | GrTimeline |
Календаря событий здесь нет. Сетка показывает дни, а не то, что в них
происходит: недельная и дневная раскладка, перетаскивание событий, пересечения и
лента ресурсов — это отдельный домен, и закрывать его будет отдельный пакет.
Слот day закрывает только пометки на дне — счётчик, точку, цену.
Модель — `PlainDate`, а не `Date`
Единственный компонент пакета, у которого наружу выходит внутренний кортеж
{ y, m, d }, а не Date через valueAdapter. Причина в том, что календарь не
форм-контрол: сериализовать ему нечего, а Date на границе означал бы полночь
в какой-то зоне — ровно ту неоднозначность, ради ухода от которой пакет и
считает на кортежах (../model.md).
Значение нужно в виде Date — его собирают на своей стороне; пикеры делают то
же самое, только внутри себя.
Первый день недели — из локали, а не из пропа
Понедельник в России и воскресенье в США — это Intl, а не настройка
компонента. Проп locale меняет и названия, и порядок дней разом; порознь их
менять нельзя, иначе получится календарь, которого не существует ни в одной
стране.
Когда локаль всё же нужно перебить — например, рабочая неделя начинается
с воскресенья независимо от языка интерфейса, — это решение приложения, а не
экрана. Такое ставится один раз через GrConfigProvider:
<GrConfigProvider :component-defaults="{ GrCalendar: { weekStart: 7, showWeekNumbers: true } }">
Настройка идёт под ключом GrCalendar и доезжает до всех пикеров: панель у них
общая, своего ключа у GrDatePicker и соседей нет намеренно. Порядок разрешения
обычный — проп на месте сильнее конфига, конфиг сильнее локали; не задано
ничего — решает Intl.
Сетка рисует выбор, но не считает его
Диапазон приходит в сетку двумя пропами (rangeStart, rangeEnd), набор — одним
(selectedDates), и ни один из них сетка не изменяет: складывать, снимать, сортировать и проверять
длину — дело пикера. Календарь отвечает на один вопрос: как покрасить ячейку.
Из-за этого он одинаково служит и GrDatePicker с его набором, и GrDateRangePicker с отрезком, и
приложению, которое ведёт выбор само.
Вхождение в набор считается по Set, собранному один раз на смену набора: обход массива на каждую
из сорока двух ячеек превратил бы подсветку в квадрат.
Пять режимов, и неделя среди них — не сетка периодов
mode | Что показывает | Что кладёт в модель |
|---|---|---|
day (по умолчанию) | сетку дней | выбранный день |
week | ту же сетку дней | начало выбранной недели |
month | двенадцать месяцев | первое число месяца |
quarter | четыре квартала | первое число квартала |
year | двенадцать лет десятилетия | первое января |
Неделя рисуется сеткой дней, а не сеткой периодов. Двенадцать ячеек в три колонки — это двенадцать недель на экране, четверть года без единой подписи месяца: выбирать в такой сетке нечего. Вместо этого клик по любому дню выбирает неделю, в которую он попал, и строка подсвечивается целиком.
Начало недели берётся из локали (см. «Первый день недели» выше), а не прибивается к понедельнику: в
США та же дата попадает в неделю, начинающуюся воскресеньем. В модель кладётся одна дата —
начало недели, — и форма значения остаётся общей для всех режимов: valueAdapter работает как
работал.
Номера недели пакет не показывает: у ISO и у США он считается по-разному, а Intl его не даёт —
выдумывать локале-зависимое мимо Intl пакет не берётся.
Кварталов четыре, и сетка у них в две колонки — три оставили бы одинокую ячейку во второй
строке. Подпись берётся строкой локали пакета (Q1, 1 кв.): Intl кварталы не именует вовсе, и
это интерфейсный текст, а не локале-зависимые данные.
Детерминированный рендер требует `today`
Часы среды читаются один раз на экземпляр. Там, где рендер обязан совпасть
с прошлым — снимки, тесты, серверная отрисовка, — «сегодня» задаётся пропом
today: иначе первый клиентский рендер разойдётся с серверным ровно в полночь.
Подробности — ../ssr.md.
Установка
npm i @feugene/granularity-chronoИмпорт
import { GrCalendar } from '@feugene/granularity-chrono/components/GrCalendar'API
API этого компонента ещё не посчитан: генератор витрины пока обходит только ядро. Пока его нет, справочник — в документации пакета.
Примеры 3
Basic
<script setup lang="ts">
import { ref } from 'vue'
// `GrCalendar` подставляется авто-импортом (`unplugin-vue-components`).
import type { PlainDate } from '@feugene/granularity-chrono'
import { plainDateKey } from '@feugene/granularity-chrono'
// Дата — кортеж `{ y, m, d }`, где `m` считается с нуля. Ни `Date`, ни
// таймзоны здесь нет: сетка про календарь, а не про момент времени.
const value = ref<PlainDate | null>({ y: 2026, m: 7, d: 12 })
const min: PlainDate = { y: 2026, m: 7, d: 3 }
const max: PlainDate = { y: 2026, m: 8, d: 18 }
</script>
<template>
<div class="grid gap-4 justify-items-start">
<GrCalendar
v-model="value"
:min="min"
:max="max"
show-week-numbers
aria-label="Delivery date"
/>
<p class="showcase-demo-text text-sm">
<span class="opacity-70">value=</span>
<code>{{ value ? plainDateKey(value) : '—' }}</code>
</p>
</div>
</template>Day Slot
<script setup lang="ts">
import { ref } from 'vue'
// `GrCalendar` подставляется авто-импортом (`unplugin-vue-components`).
import type { PlainDate } from '@feugene/granularity-chrono'
import { plainDateKey } from '@feugene/granularity-chrono'
const value = ref<PlainDate | null>(null)
/** Нагрузка дня: ключ `2026-08-12` совпадает с ключом ячейки сетки. */
const eventsByDay: Record<string, number> = {
'2026-08-04': 1,
'2026-08-12': 3,
'2026-08-13': 2,
'2026-08-21': 1,
}
function eventsOn(date: PlainDate): number {
return eventsByDay[plainDateKey(date)] ?? 0
}
</script>
<template>
<div class="grid gap-4 justify-items-start">
<GrCalendar
v-model="value"
:view-date="{ y: 2026, m: 7, d: 1 }"
aria-label="Schedule"
>
<!-- Слот отдаёт саму ячейку: число рисуем сами и дописываем метки. -->
<template #day="{ cell, selected }">
<span class="relative inline-flex flex-col items-center leading-none">
<span>{{ cell.date.d }}</span>
<span
v-if="eventsOn(cell.date) && !selected"
class="mt-0.5 h-1 w-1 rounded-[var(--gr-radius-full)] bg-[var(--gr-primary)]"
:aria-label="`${eventsOn(cell.date)} events`"
/>
</span>
</template>
</GrCalendar>
<p class="showcase-demo-text text-sm">
<span class="opacity-70">busy days=</span><code>{{ Object.keys(eventsByDay).length }}</code>
</p>
</div>
</template>Modes
<script setup lang="ts">
import { ref } from 'vue'
// `GrCalendar` и `GrSegmented` подставляются авто-импортом.
import type { PlainDate } from '@feugene/granularity-chrono'
import { plainDateKey } from '@feugene/granularity-chrono'
type CalendarMode = 'day' | 'week' | 'month' | 'quarter' | 'year'
const mode = ref<CalendarMode>('day')
// В режимах периода значением становится первое число: месяц — это 1 августа,
// квартал — 1 июля, год — 1 января. Неделя кладёт своё начало, и оно зависит
// от локали: у `en-US` неделя начинается с воскресенья. Показываем значение
// ISO-ключом — он не зависит от языка витрины, а сетка рядом и так показывает язык.
const value = ref<PlainDate | null>({ y: 2026, m: 7, d: 12 })
const modeOptions = [
{ value: 'day', label: 'Day' },
{ value: 'week', label: 'Week' },
{ value: 'month', label: 'Month' },
{ value: 'quarter', label: 'Quarter' },
{ value: 'year', label: 'Year' },
] satisfies Array<{ value: CalendarMode, label: string }>
</script>
<template>
<div class="grid gap-4 justify-items-start">
<GrSegmented v-model="mode" :options="modeOptions" size="sm" />
<GrCalendar
v-model="value"
:mode="mode"
aria-label="Reporting period"
/>
<p class="showcase-demo-text text-sm">
<span class="opacity-70">value=</span>
<code>{{ value ? plainDateKey(value) : '—' }}</code>
</p>
<p class="showcase-demo-text text-sm opacity-70">
Неделя рисуется той же сеткой дней: клик по любому дню выбирает его неделю, и подсвечивается
вся строка. Двенадцать недель в сетке периодов были бы четвертью года без единой подписи
месяца — выбирать там нечего. Кварталов четыре, и сетка у них в две колонки.
</p>
</div>
</template>