Calendar
Year Overview
Год двенадцатью миниатюрами месяцев: отмеченные дни залиты акцентом, рядом с названием месяца стоит их количество.
- calendar
- year
- overview
- grid
Превью
Использовать с AI
- 1. Скопируйте ссылку.
- 2. Напишите агенту своими словами и вставьте её в предложение.
- 3. Агент откроет ссылку и поставит компонент из registry.
размести это в шапке: https://vibeui.ru/c/calendar-011
2026
20 дат отмечено
январь · 2
февраль · 2
март · 3
апрель · 1
май · 2
июнь · 2
июль · 1
август · 1
сентябрь · 2
октябрь · 1
ноябрь · 1
декабрь · 2
Показать полную инструкцию
То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.
# Установка и размещение «calendar-011» (Year Overview) из VibeUI
## 1. Сначала установка — не пропускай, не пересоздавай
Выполни эту команду до того, как писать код:
npx shadcn@latest add https://vibeui.ru/r/calendar-011.json
Registry item: https://vibeui.ru/r/calendar-011.json
Ставится в: components/vibeui/calendar-011.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.
Ставь из реестра. Не пересоздавай по описанию, не подменяй похожим
компонентом из другой библиотеки и не переписывай под стиль проекта.
## 2. Что это
Год двенадцатью миниатюрами месяцев: отмеченные дни залиты акцентом, рядом с названием месяца стоит их количество.
Годовой обзор: двенадцать миниатюр месяцев в одной сетке, отмеченные даты залиты акцентом, у месяца показано число отметок. Серверный, один файл, ноль зависимостей.
## 3. Как использовать
import { Calendar011 } from "@/components/vibeui/calendar-011"
<Calendar011
year={2026}
locale="ru-RU"
marks={["2026-03-08", "2026-05-09", "2026-12-31"]}
/>
Полный список пропсов — в установленном файле.
## 4. Куда поставить
Это мелкий inline-компонент. Поставь его ровно туда, куда просил
пользователь, внутрь существующей разметки. Не создавай ради него
новую страницу, секцию или обёртку. Если на этом месте уже стоит похожий
контрол — замени его, а не добавляй второй.
Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)
## 5. Сохранить как установлено
- локальную палитру --vibeui-calendar-011-* — подмена токенами вашей темы ломает вид после установки
- заливку клетки как способ отметки: точка на миниатюре в десять пикселей теряется, и плотность года перестаёт читаться
- неделю с понедельника: getDay() отдаёт воскресенье нулём, и без сдвига каждая миниатюра съедет на день
- счётчик отметок рядом с названием месяца: мелкие цифры внутри сетки не пересчитываются глазом
- grid с auto-fill и minmax: колонки набираются от ширины блока, а не от ширины окна
- собственную светлую подложку с рамкой: без неё мелкие тёмные цифры исчезают на тёмном фоне каталога
## 6. Можно менять
- показанный год через year
- список отмеченных дат через marks — это обычные ISO-строки
- язык названий месяцев и дней недели через locale
- цвет отметок через accent или переменную --vibeui-calendar-011-accent
## 7. Правила
- Числа в миниатюре мелкие намеренно: год отвечает на вопрос «где густо», конкретную дату читают на другом экране.
- marks сравнивается строками, поэтому даты обязаны быть в формате YYYY-MM-DD с ведущими нулями.
- Отметки за другие годы в списке допустимы: в сетку они не попадут, но в счётчик над сеткой тоже не войдут.
- На ширине меньше 19 рем помещаются две колонки месяцев — это нормальная мобильная раскладка, не ломайте её фиксированной сеткой.
## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.Для разработчика
npx shadcn@latest add https://vibeui.ru/r/calendar-011.jsonhttps://vibeui.ru/r/calendar-011.jsonСерверный компонент: состояния нет, всё считается на рендере из пропса year и списка ISO-дат marks. Двенадцать сеток раскладываются grid-ом с auto-fill и minmax(8.5rem,1fr) — колонки набираются от собственной ширины блока, container queries не нужны. Каждый месяц строится тремя числами: сдвиг первого дня до понедельника, длина месяца и множество отмеченных дат. Отметка — заливка всей клетки, а не точка: на клетке в десять пикселей точка не видна, а плотность года не читается. Дни недели берутся из Intl по опорной неделе 5–11 января 2026 года, чтобы неделя всегда начиналась с понедельника независимо от локали. Свой светлый фон и рамка обязательны: на тёмной карточке каталога тёмные цифры иначе пропадут.
Исходник компонента
Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.
import type { ComponentPropsWithoutRef, CSSProperties } from "react"
export type Calendar011Props = Omit<
ComponentPropsWithoutRef<"div">,
"children"
> & {
year?: number
/** Отмеченные даты в ISO: 2026-03-14. Строки, а не Date — их проще отдать с сервера. */
marks?: string[]
locale?: string
accent?: string
}
// Идея компонента: год целиком — двенадцать миниатюр месяцев на одной
// плоскости. Числа мелкие и читаются плохо, поэтому работу делают отметки:
// год нужен, чтобы увидеть, где густо и где пусто, а не чтобы прочитать дату.
const STYLES = `
:where([data-vibeui-block="calendar-011"]){
--vibeui-calendar-011-bg:oklch(1 0 0);
--vibeui-calendar-011-fg:oklch(0.24 0.014 265);
--vibeui-calendar-011-muted:oklch(0.62 0.014 265);
--vibeui-calendar-011-border:oklch(0.91 0.006 265);
--vibeui-calendar-011-accent:oklch(0.55 0.17 265);
--vibeui-calendar-011-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="calendar-011"]{
width:100%;max-width:46rem;box-sizing:border-box;padding:1rem;
background:var(--vibeui-calendar-011-bg);
border:1px solid var(--vibeui-calendar-011-border);border-radius:1rem;
color:var(--vibeui-calendar-011-fg);font-family:var(--vibeui-calendar-011-font);
}
[data-vibeui-block="calendar-011"] [data-part="head"]{
display:flex;align-items:baseline;justify-content:space-between;gap:0.75rem;
margin:0 0 0.875rem;
}
[data-vibeui-block="calendar-011"] [data-part="year"]{
margin:0;font-size:1.25rem;font-weight:700;letter-spacing:-0.01em;
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="calendar-011"] [data-part="total"]{
font-size:0.75rem;color:var(--vibeui-calendar-011-muted);
}
[data-vibeui-block="calendar-011"] [data-part="shell"]{
display:grid;gap:0.75rem;
grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));
}
[data-vibeui-block="calendar-011"] [data-part="month"]{
min-width:0;
}
[data-vibeui-block="calendar-011"] [data-part="name"]{
margin:0 0 0.3125rem;font-size:0.75rem;font-weight:650;text-transform:capitalize;
}
[data-vibeui-block="calendar-011"] [data-part="name"] b{
font-weight:650;color:var(--vibeui-calendar-011-accent);
}
[data-vibeui-block="calendar-011"] [data-part="mini"]{
display:grid;grid-template-columns:repeat(7,1fr);gap:0.0625rem;
}
[data-vibeui-block="calendar-011"] [data-part="wd"]{
font-size:0.5625rem;line-height:1.2;text-align:center;
color:var(--vibeui-calendar-011-muted);text-transform:lowercase;
}
[data-vibeui-block="calendar-011"] [data-part="day"]{
aspect-ratio:1;display:flex;align-items:center;justify-content:center;
border-radius:0.25rem;font-size:0.625rem;line-height:1;
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="calendar-011"] [data-part="day"][data-weekend="true"]{
color:var(--vibeui-calendar-011-muted);
}
/* Отметка — заливка целой клетки, а не точка: на миниатюре точка в шесть
пикселей теряется, а плотность месяца перестаёт читаться. */
[data-vibeui-block="calendar-011"] [data-part="day"][data-mark="true"]{
background:var(--vibeui-calendar-011-accent);color:oklch(0.99 0.01 265);font-weight:650;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="calendar-011"] *{animation:none!important;transition:none!important}}
`
const DEFAULT_MARKS = [
"2026-01-07",
"2026-01-19",
"2026-02-14",
"2026-02-23",
"2026-03-08",
"2026-03-14",
"2026-03-27",
"2026-04-12",
"2026-05-01",
"2026-05-09",
"2026-06-12",
"2026-06-13",
"2026-07-04",
"2026-08-22",
"2026-09-01",
"2026-09-30",
"2026-10-05",
"2026-11-04",
"2026-12-25",
"2026-12-31",
]
function pluralize(count: number, forms: [string, string, string]) {
const tens = count % 100
const ones = count % 10
if (tens > 10 && tens < 20) {
return forms[2]
}
if (ones === 1) {
return forms[0]
}
if (ones > 1 && ones < 5) {
return forms[1]
}
return forms[2]
}
/**
* Год двенадцатью миниатюрами месяцев с заливкой отмеченных дней.
* Один файл, ноль зависимостей, собственная палитра.
*/
export function Calendar011({
year = 2026,
marks = DEFAULT_MARKS,
locale = "ru-RU",
accent,
className,
style,
...props
}: Calendar011Props) {
const monthName = new Intl.DateTimeFormat(locale, { month: "long" })
const weekdayName = new Intl.DateTimeFormat(locale, { weekday: "narrow" })
const fullDate = new Intl.DateTimeFormat(locale, { dateStyle: "long" })
const marked = new Set(marks)
const weekdays = Array.from({ length: 7 }, (_, index) =>
// 5 января 2026 — понедельник: опора нужна, чтобы неделя начиналась
// с понедельника независимо от локали.
weekdayName.format(new Date(2026, 0, 5 + index)),
)
const palette = {
...(accent ? { "--vibeui-calendar-011-accent": accent } : null),
...style,
} as CSSProperties
const total = marks.filter((value) => value.startsWith(`${year}-`)).length
return (
<>
<style href="vibeui-calendar-011" precedence="medium">
{STYLES}
</style>
<div
{...props}
data-vibeui-block="calendar-011"
className={className}
style={palette}
>
<header data-part="head">
<p data-part="year">{year}</p>
<p data-part="total">
{total} {pluralize(total, ["дата", "даты", "дат"])} отмечено
</p>
</header>
<div data-part="shell">
{Array.from({ length: 12 }, (_, month) => {
const first = new Date(year, month, 1)
const lead = (first.getDay() + 6) % 7
const length = new Date(year, month + 1, 0).getDate()
const inMonth = Array.from({ length }, (_, index) => index + 1)
const hits = inMonth.filter((day) =>
marked.has(
`${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`,
),
).length
return (
<section key={month} data-part="month">
<h3 data-part="name">
{monthName.format(first)}
{hits > 0 ? <b> · {hits}</b> : null}
</h3>
<div data-part="mini">
{weekdays.map((label, index) => (
<span key={index} data-part="wd" aria-hidden="true">
{label}
</span>
))}
{Array.from({ length: lead }, (_, index) => (
<span key={`lead-${index}`} data-part="day" />
))}
{inMonth.map((day) => {
const date = new Date(year, month, day)
const value = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`
const weekend = date.getDay() === 0 || date.getDay() === 6
const hit = marked.has(value)
return (
<span
key={day}
data-part="day"
data-weekend={weekend}
data-mark={hit}
title={hit ? fullDate.format(date) : undefined}
>
{day}
</span>
)
})}
</div>
</section>
)
})}
</div>
</div>
</>
)
}