GrCalendar
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- the calendar is the screen — a booking page, a planner, a day-selection panel: the grid has to be open rather than opened by a click in a field;
- the cells have content of their own — the
dayslot draws the load, the price, the number of events over a day; - the grid is needed next to something else — a panel of your own, buttons of your own, a footer
of your own through
headerandfooter; - the choice of a day is not related to a form — the value goes into the state of the screen rather than into an input field.
When to take something else
| Need | Take |
|---|---|
| A date as the value of a form field | GrDatePicker |
| A “from — to” period | GrDateRangePicker |
| A date with a time | GrDateTimePicker |
| A time only | GrTimePicker |
| Events in time as a feed rather than as a grid | GrTimeline |
There is no event calendar here. The grid shows the days rather than what happens in them: a
week and a day layout, dragging events, overlaps and a feed of resources are a separate domain, and
a separate package will cover it. The day slot covers only the marks on a day — a counter, a dot,
a price.
The model is a `PlainDate` rather than a `Date`
The only component of the package whose internal { y, m, d } tuple goes outside rather than a
Date through a valueAdapter. The reason is that a calendar is not a form control: it has nothing
to serialise, and a Date at the boundary would mean midnight in some zone — exactly the ambiguity
for the sake of avoiding which the package computes on tuples
(../model.md).
If the value is needed as a Date, it is assembled on your own side; the pickers do the same, only
inside themselves.
The first day of the week comes from the locale rather than from a prop
Monday in Russia and Sunday in the USA is Intl rather than a setting of the component. The
locale prop changes both the names and the order of the days at once; changing them separately is
not allowed, otherwise a calendar comes out that does not exist in any country.
When the locale does have to be overridden — for instance the working week starts on Sunday
regardless of the language of the interface — that is a decision of the application rather than of a
screen. Such a thing is set once through GrConfigProvider:
<GrConfigProvider :component-defaults="{ GrCalendar: { weekStart: 7, showWeekNumbers: true } }">
The setting goes under the GrCalendar key and reaches all of the pickers: their panel is shared,
and GrDatePicker and its neighbours deliberately have no key of their own. The resolution order is
the usual one — the prop in place is stronger than the config, and the config is stronger than the
locale; if nothing is set, Intl decides.
The grid draws the selection but does not compute it
A range arrives in the grid as two props (rangeStart, rangeEnd) and a set as one
(selectedDates), and the grid changes neither of them: adding, removing, sorting and checking the
length are the business of the picker. The calendar answers one question: how to colour a cell.
Because of that it serves equally well GrDatePicker with its set, GrDateRangePicker with its
segment, and an application that runs the selection itself.
Membership of the set is computed with a Set assembled once per change of the set: walking the
array for each of the forty-two cells would turn the highlighting into a quadratic.
Five modes, and the week among them is not a grid of periods
mode | What it shows | What it puts into the model |
|---|---|---|
day (the default) | a grid of days | the selected day |
week | the same grid of days | the beginning of the selected week |
month | twelve months | the first day of the month |
quarter | four quarters | the first day of the quarter |
year | the twelve years of a decade | the first of January |
A week is drawn as a grid of days rather than as a grid of periods. Twelve cells in three columns are twelve weeks on the screen, a quarter of a year without a single month label: there is nothing to choose in such a grid. Instead a click on any day selects the week it fell into, and the row is highlighted as a whole.
The beginning of the week is taken from the locale (see “The first day of the week” above) rather
than nailed to Monday: in the USA the same date falls into a week starting on Sunday. One date is
put into the model — the beginning of the week — and the shape of the value stays shared across all
of the modes: valueAdapter works as it worked.
The package does not show week numbers: ISO and the USA count them differently, and Intl does not
provide it — the package does not undertake to invent something locale-dependent past Intl.
There are four quarters, and their grid has two columns — three would leave a lonely cell in the
second row. The label is taken from a string of the package locale (Q1, 1 qtr): Intl does not
name quarters at all, and this is interface text rather than locale-dependent data.
A deterministic render requires `today`
The clock of the environment is read once per instance. Where the render is obliged to match a past
one — snapshots, tests, server rendering — “today” is set with the today prop: otherwise the first
client render will diverge from the server one exactly at midnight. The details —
../ssr.md.
Install
npm i @feugene/granularity-chronoImport
import { GrCalendar } from '@feugene/granularity-chrono/components/GrCalendar'API
The API for this component has not been generated yet: the showcase generator only covers the core so far. Until it does, the reference lives in the package documentation.
Examples 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>