Calendar

Sidebar Mini

A fifteen-rem mini calendar for a sidebar: event dots sit under the numbers, and the fourth one and beyond collapse into a plus sign.

  • calendar
  • sidebar
  • mini
  • events

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-014?lang=en

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-014" (Sidebar Mini) 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-014.json

Registry item: https://vibeui.ru/r/calendar-014.json
Installs to: components/vibeui/calendar-014.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 fifteen-rem mini calendar for a sidebar: event dots sit under the numbers, and the fourth one and beyond collapse into a plus sign.

A sidebar mini calendar: event dots under the day numbers, a month total and a link to the full list. Server-side, one file, zero dependencies.

## 3. How to use it
import { Calendar014 } from "@/components/vibeui/calendar-014"

<Calendar014
  year={2026}
  month={3}
  today={14}
  events={{ 5: 2, 14: 3, 23: 5 }}
  caption="All events"
/>

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-014-* palette — swapping it for theme tokens breaks the match with the preview
- dots under the number rather than behind it: in a thirty-pixel cell a fill would swallow the digit itself
- the three-dot limit with a plus: four dots in a row merge into a bar at this width
- the aria-label carrying the event count: dots are visual information a screen reader cannot reach
- the Monday-first week: without the getDay() shift the grid slides by one day
- the component's own light surface: another project's sidebar may be dark, and the digits would disappear

## 6. You may change
- the month and the year through month and year
- the events through events — a map of day to count
- the highlighted day through today
- the footer link caption through caption and its address in the markup

## 7. Rules
- The component does not page months: in a sidebar that is one control too many, the month usually comes from the page.
- today is a day number, not a date: the calendar already knows its month, there is no point duplicating it in the prop.
- events accepts any count, but only the first three are distinguishable in a cell — beyond that a plus is shown.
- Below twelve rem the cells get tighter than the weekday row: that is the layout limit, below it use a different component.

## 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-014.json
https://vibeui.ru/r/calendar-014.json

Серверный компонент: состояния нет, всё считается из пропсов year, month и словаря events. Точки стоят под числом, а не поверх: подложка под цифрой в клетке высотой тридцать пикселей съела бы саму цифру. Точек максимум три, дальше ставится плюс — четыре точки в ряд шириной в двадцать пикселей сливаются в полосу. Дни недели берутся из Intl в узком варианте по опорной неделе 5–11 января 2026 года, чтобы неделя начиналась с понедельника. Каждая клетка с событиями несёт aria-label с количеством и склонением, поэтому точки не остаются чисто визуальной информацией. Свой светлый фон обязателен: боковая колонка чужого сайта может быть любой.

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 Calendar014Props = Omit<
  ComponentPropsWithoutRef<"aside">,
  "children" | "title"
> & {
  year?: number
  month?: number
  /** Число событий по дням месяца: ключ — день, значение — сколько событий. */
  events?: Record<number, number>
  today?: number
  caption?: string
  locale?: string
  accent?: string
}

// Идея компонента: мини-календарь для боковой колонки. Ширина — пятнадцать
// рем, поэтому событий в клетке нет: под числом стоят точки, максимум три,
// а четвёртое и дальше сворачиваются в плюс. Точки под числом, а не поверх:
// подложка под цифрой съела бы саму цифру.
const STYLES = `
:where([data-vibeui-block="calendar-014"]){
--vibeui-calendar-014-bg:oklch(1 0 0);
--vibeui-calendar-014-fg:oklch(0.24 0.014 265);
--vibeui-calendar-014-muted:oklch(0.63 0.014 265);
--vibeui-calendar-014-border:oklch(0.91 0.006 265);
--vibeui-calendar-014-accent:oklch(0.56 0.16 25);
--vibeui-calendar-014-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="calendar-014"]{
display:flex;flex-direction:column;gap:0.5rem;
width:100%;max-width:15rem;box-sizing:border-box;padding:0.75rem;
background:var(--vibeui-calendar-014-bg);
border:1px solid var(--vibeui-calendar-014-border);border-radius:0.75rem;
color:var(--vibeui-calendar-014-fg);font-family:var(--vibeui-calendar-014-font);
}
[data-vibeui-block="calendar-014"] [data-part="head"]{
display:flex;align-items:baseline;justify-content:space-between;gap:0.5rem;
}
[data-vibeui-block="calendar-014"] [data-part="month"]{
margin:0;font-size:0.8125rem;font-weight:650;
}
/* Заглавная только первая буква: capitalize поднимает и «г.» в «январь 2026 г.». */
[data-vibeui-block="calendar-014"] [data-part="month"]::first-letter{text-transform:uppercase}
[data-vibeui-block="calendar-014"] [data-part="year"]{
font-size:0.6875rem;color:var(--vibeui-calendar-014-muted);font-variant-numeric:tabular-nums;
}
[data-vibeui-block="calendar-014"] [data-part="grid"]{
display:grid;grid-template-columns:repeat(7,1fr);gap:0.0625rem;
}
[data-vibeui-block="calendar-014"] [data-part="wd"]{
padding-bottom:0.1875rem;text-align:center;
font-size:0.625rem;font-weight:600;text-transform:lowercase;
color:var(--vibeui-calendar-014-muted);
}
[data-vibeui-block="calendar-014"] [data-part="cell"]{
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.125rem;
height:1.875rem;border-radius:0.375rem;
font-size:0.6875rem;line-height:1;font-variant-numeric:tabular-nums;
}
[data-vibeui-block="calendar-014"] [data-part="cell"][data-weekend="true"]{color:var(--vibeui-calendar-014-muted)}
[data-vibeui-block="calendar-014"] [data-part="cell"][data-today="true"]{
background:var(--vibeui-calendar-014-accent);color:oklch(0.99 0.01 25);font-weight:700;
}
[data-vibeui-block="calendar-014"] [data-part="dots"]{
display:flex;align-items:center;gap:0.09375rem;height:0.25rem;
}
[data-vibeui-block="calendar-014"] [data-part="dots"] i{
width:0.1875rem;height:0.1875rem;border-radius:50%;
background:var(--vibeui-calendar-014-accent);
}
[data-vibeui-block="calendar-014"] [data-part="cell"][data-today="true"] [data-part="dots"] i{background:oklch(0.99 0.01 25)}
[data-vibeui-block="calendar-014"] [data-part="more"]{
font-size:0.5rem;line-height:0.25rem;font-weight:700;
color:var(--vibeui-calendar-014-accent);
}
[data-vibeui-block="calendar-014"] [data-part="cell"][data-today="true"] [data-part="more"]{color:oklch(0.99 0.01 25)}
[data-vibeui-block="calendar-014"] [data-part="foot"]{
display:flex;align-items:center;justify-content:space-between;gap:0.5rem;
padding-top:0.375rem;border-top:1px solid var(--vibeui-calendar-014-border);
font-size:0.6875rem;color:var(--vibeui-calendar-014-muted);
}
[data-vibeui-block="calendar-014"] [data-part="foot"] a{
color:var(--vibeui-calendar-014-accent);font-weight:600;text-decoration:none;border-radius:0.25rem;
}
[data-vibeui-block="calendar-014"] [data-part="foot"] a:hover{text-decoration:underline}
[data-vibeui-block="calendar-014"] [data-part="foot"] a:focus-visible{outline:2px solid var(--vibeui-calendar-014-accent);outline-offset:2px}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="calendar-014"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_EVENTS: Record<number, number> = {
  3: 1,
  5: 2,
  9: 4,
  12: 1,
  14: 3,
  17: 2,
  20: 1,
  23: 5,
  27: 2,
  30: 1,
}

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 Calendar014({
  year = 2026,
  month = 3,
  events = DEFAULT_EVENTS,
  today = 14,
  caption = "Все события",
  locale = "ru-RU",
  accent,
  className,
  style,
  ...props
}: Calendar014Props) {
  const first = new Date(year, month - 1, 1)
  const lead = (first.getDay() + 6) % 7
  const length = new Date(year, month, 0).getDate()

  const weekdayName = new Intl.DateTimeFormat(locale, { weekday: "narrow" })
  const weekdays = Array.from({ length: 7 }, (_, index) =>
    weekdayName.format(new Date(2026, 0, 5 + index)),
  )
  const monthName = new Intl.DateTimeFormat(locale, { month: "long" }).format(
    first,
  )

  const total = Object.values(events).reduce((sum, count) => sum + count, 0)

  const palette = {
    ...(accent ? { "--vibeui-calendar-014-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-calendar-014" precedence="medium">
        {STYLES}
      </style>
      <aside
        {...props}
        data-vibeui-block="calendar-014"
        className={className}
        style={palette}
        aria-label={`Календарь: ${monthName} ${year}`}
      >
        <header data-part="head">
          <h3 data-part="month">{monthName}</h3>
          <span data-part="year">{year}</span>
        </header>
        <div data-part="grid">
          {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="cell" />
          ))}
          {Array.from({ length }, (_, index) => {
            const day = index + 1
            const date = new Date(year, month - 1, day)
            const weekend = date.getDay() === 0 || date.getDay() === 6
            const count = events[day] ?? 0

            return (
              <span
                key={day}
                data-part="cell"
                data-weekend={weekend}
                data-today={day === today}
                aria-label={
                  count > 0
                    ? `${day} ${monthName}: ${count} ${pluralize(count, ["событие", "события", "событий"])}`
                    : undefined
                }
              >
                <span>{day}</span>
                <span data-part="dots" aria-hidden="true">
                  {Array.from({ length: Math.min(count, 3) }, (_, dot) => (
                    <i key={dot} />
                  ))}
                  {count > 3 ? <b data-part="more">+</b> : null}
                </span>
              </span>
            )
          })}
        </div>
        <footer data-part="foot">
          <span>
            {total} {pluralize(total, ["событие", "события", "событий"])}
          </span>
          <a href="#events">{caption}</a>
        </footer>
      </aside>
    </>
  )
}