Calendar
Year Overview
A whole year as twelve month thumbnails: marked days are filled with the accent colour and each month name carries the count of its marks.
- calendar
- year
- overview
- grid
Preview
Use it with AI
- 1. Copy the link.
- 2. Write to your agent in your own words and drop the link into the sentence.
- 3. The agent opens the link and installs the component from the registry.
put this in the header: https://vibeui.ru/c/calendar-011?lang=en
2026
20 дат отмечено
январь · 2
февраль · 2
март · 3
апрель · 1
май · 2
июнь · 2
июль · 1
август · 1
сентябрь · 2
октябрь · 1
ноябрь · 1
декабрь · 2
Show the full instructions
What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.
# Install and place "calendar-011" (Year Overview) from VibeUI
## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/calendar-011.json
Registry item: https://vibeui.ru/r/calendar-011.json
Installs to: components/vibeui/calendar-011.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.
Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.
## 2. What it is
A whole year as twelve month thumbnails: marked days are filled with the accent colour and each month name carries the count of its marks.
A year overview: twelve month thumbnails in one grid, marked dates filled with the accent, each month showing how many marks it holds. Server-side, one file, zero dependencies.
## 3. How to use it
import { Calendar011 } from "@/components/vibeui/calendar-011"
<Calendar011
year={2026}
locale="en-US"
marks={["2026-03-08", "2026-05-09", "2026-12-31"]}
/>
Read the installed file for the full prop list.
## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.
Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)
## 5. Keep exactly as installed
- the local --vibeui-calendar-011-* palette — swapping it for your theme tokens breaks the look after install
- the filled cell as the mark: a dot on a ten-pixel thumbnail disappears and the density of the year stops reading
- the Monday-first week: getDay() returns Sunday as zero, and without the shift every thumbnail slides by one day
- the mark counter next to the month name: tiny digits inside the grid are not recounted by eye
- the grid with auto-fill and minmax: columns are packed from the block width, not from the viewport
- the component's own light surface with a border: without it small dark digits vanish on the dark catalogue background
## 6. You may change
- the year shown through year
- the list of marked dates through marks — plain ISO strings
- the language of month and weekday names through locale
- the mark colour through accent or the --vibeui-calendar-011-accent variable
## 7. Rules
- The digits in a thumbnail are deliberately small: a year answers where it is crowded, an exact date is read on another screen.
- marks is compared as strings, so dates must use the YYYY-MM-DD format with leading zeros.
- Marks from other years are allowed in the list: they never reach the grid, but they do not reach the counter above it either.
- Below 19rem two month columns fit — that is the normal mobile layout, do not break it with a fixed grid.
## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.For developers
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 года, чтобы неделя всегда начиналась с понедельника независимо от локали. Свой светлый фон и рамка обязательны: на тёмной карточке каталога тёмные цифры иначе пропадут.
Component source
The same file your agent installs. Here in case you would rather copy it by hand.
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>
</>
)
}