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
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-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.jsonhttps://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>
</>
)
}