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

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 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

12345678910111213141516171819202122232425262728293031

февраль · 2

12345678910111213141516171819202122232425262728

март · 3

12345678910111213141516171819202122232425262728293031

апрель · 1

123456789101112131415161718192021222324252627282930

май · 2

12345678910111213141516171819202122232425262728293031

июнь · 2

123456789101112131415161718192021222324252627282930

июль · 1

12345678910111213141516171819202122232425262728293031

август · 1

12345678910111213141516171819202122232425262728293031

сентябрь · 2

123456789101112131415161718192021222324252627282930

октябрь · 1

12345678910111213141516171819202122232425262728293031

ноябрь · 1

123456789101112131415161718192021222324252627282930

декабрь · 2

12345678910111213141516171819202122232425262728293031
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.json
https://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>
    </>
  )
}