Event Calendar

Month Overflow

A month with events inside the cells: each cell keeps a fixed height and everything that does not fit folds into an expandable «more N» built on native <details>.

  • calendar
  • events
  • month
  • overflow

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/eventcalendar-001?lang=en

март 2026 г.

15 событий в месяце

пнвтсрчтптсбвс
23
24
25
26
27
28
1
2
310:00Планёрка
4
514:00Ревью дизайна16:30Созвон с Кимом
6
7
8
9
10
1112:00Релиз 4.2
1207:30Бассейн11:00Спринт-обзор
ещё 2
  • 15:00Интервью
  • 17:00Ретро
13
14
15
16
17
1809:00Отчёт за квартал
1913:00Обед с ВеройПравки бюджета
20
21
22
23
2411:30Демо клиенту19:00Тренировка
ещё 1
  • Черновик плана
25
26
2710:00Выезд команды
28
29
30
31
1
2
3
4
5
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 "eventcalendar-001" (Month Overflow) 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/eventcalendar-001.json

Registry item: https://vibeui.ru/r/eventcalendar-001.json
Installs to: components/vibeui/eventcalendar-001.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 month with events inside the cells: each cell keeps a fixed height and everything that does not fit folds into an expandable «more N» built on native <details>.

A month of events: up to `visible` chips per cell, the rest folded into an expandable «more N». Zero dependencies, one file, no client JS.

## 3. How to use it
import { Eventcalendar001 } from "@/components/vibeui/eventcalendar-001"

<Eventcalendar001
  month="2026-03-01"
  visible={2}
  events={[{ day: 5, time: "14:00", title: "Review", tone: "work" }]}
/>

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-eventcalendar-001-* palette — do not swap it for your theme tokens
- the fixed min-height of a cell: without it one busy day stretches the whole month row
- the overflow handled by <details> rather than overflow:hidden — a hidden event must stay reachable from the keyboard
- the popup flipping upwards in the bottom rows: otherwise it escapes the card
- colour together with border style on an event: one hue alone dies in print and for colour blindness
- the card's own light surface: dark text disappears on a dark showcase background

## 6. You may change
- the events array and the month through month
- how many chips a cell shows through visible
- the accent through accent and the rest of the palette
- the label language through locale and the caption through heading

## 7. Rules
- Multi-day events are not supported: a bar across several cells needs a different layout.
- Open «more N» popups do not close each other — that is the price of having no client JS.
- Events are matched to cells by day number, so the array belongs to a single month.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.

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

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-eventcalendar-001-*. Серверный: хуков нет. Сетка 7×6 строится от первого понедельника, названия дней и месяца берутся из Intl. Клетка имеет min-height, поэтому загруженный день не растягивает строку месяца; события сверх visible уходят в <details>/<summary>, а всплывающий список позиционируется абсолютно и в нижних строках разворачивается вверх правилом :nth-child(n+29). Тип события различается цветом левой границы и её стилем.

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 Eventcalendar001Event = {
  /** День месяца, 1–31. */
  day: number
  title: string
  time?: string
  tone?: "work" | "personal" | "hold"
}

export type Eventcalendar001Props = Omit<
  ComponentPropsWithoutRef<"section">,
  "children" | "title"
> & {
  month?: string
  events?: Eventcalendar001Event[]
  visible?: number
  heading?: string
  locale?: string
  accent?: string
}

// Идея компонента: месяц показывает не даты, а события в датах. Клетка
// держит фиксированную высоту, поэтому загруженный день не растягивает
// строку и сетка остаётся сеткой; всё, что не влезло, сворачивается в
// «ещё N» и раскрывается нативным <details> — без единой строки клиентского
// кода. Переполнение считается заранее, а не обрезается overflow: скрытое
// событие обязано быть достижимо, иначе календарь врёт.
const STYLES = `
:where([data-vibeui-block="eventcalendar-001"]){
--vibeui-eventcalendar-001-bg:oklch(1 0 0);
--vibeui-eventcalendar-001-fg:oklch(0.24 0.014 265);
--vibeui-eventcalendar-001-muted:oklch(0.6 0.014 265);
--vibeui-eventcalendar-001-border:oklch(0.91 0.006 265);
--vibeui-eventcalendar-001-line:oklch(0.95 0.004 265);
--vibeui-eventcalendar-001-accent:oklch(0.55 0.16 262);
--vibeui-eventcalendar-001-personal:oklch(0.58 0.14 152);
--vibeui-eventcalendar-001-hold:oklch(0.6 0.02 265);
--vibeui-eventcalendar-001-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="eventcalendar-001"]{
width:100%;max-width:46rem;box-sizing:border-box;padding:1rem;
background:var(--vibeui-eventcalendar-001-bg);
border:1px solid var(--vibeui-eventcalendar-001-border);border-radius:1rem;
color:var(--vibeui-eventcalendar-001-fg);
font-family:var(--vibeui-eventcalendar-001-font);
}
[data-vibeui-block="eventcalendar-001"] *{box-sizing:border-box}
[data-vibeui-block="eventcalendar-001"] [data-part="head"]{
display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;
gap:0.5rem;margin:0 0 0.75rem;
}
[data-vibeui-block="eventcalendar-001"] [data-part="heading"]{
margin:0;font-size:0.9375rem;font-weight:700;letter-spacing:-0.01em;
}
/* Заглавная только первая буква: capitalize поднимает и «г.» в «март 2026 г.». */
[data-vibeui-block="eventcalendar-001"] [data-part="heading"]::first-letter{text-transform:uppercase}
[data-vibeui-block="eventcalendar-001"] [data-part="count"]{
font-size:0.75rem;color:var(--vibeui-eventcalendar-001-muted);
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="eventcalendar-001"] [data-part="grid"]{
display:grid;grid-template-columns:repeat(7,minmax(0,1fr));
border:1px solid var(--vibeui-eventcalendar-001-line);border-radius:0.625rem;
}
[data-vibeui-block="eventcalendar-001"] [data-part="weekday"]{
padding:0.375rem 0.5rem;text-align:center;
font-size:0.6875rem;font-weight:600;text-transform:capitalize;
color:var(--vibeui-eventcalendar-001-muted);
border-bottom:1px solid var(--vibeui-eventcalendar-001-line);
}
/* Клетка держит высоту сама: иначе один загруженный день растянет всю
   строку месяца и сетка перестанет читаться как сетка. */
[data-vibeui-block="eventcalendar-001"] [data-part="cell"]{
position:relative;display:flex;flex-direction:column;gap:0.1875rem;
min-height:5.25rem;padding:0.3125rem;
border-top:1px solid var(--vibeui-eventcalendar-001-line);
border-left:1px solid var(--vibeui-eventcalendar-001-line);
}
[data-vibeui-block="eventcalendar-001"] [data-part="cell"]:nth-child(7n+1){border-left:0}
[data-vibeui-block="eventcalendar-001"] [data-part="cell"][data-outside="true"]{
background:var(--vibeui-eventcalendar-001-line);
}
[data-vibeui-block="eventcalendar-001"] [data-part="daynum"]{
font-size:0.6875rem;font-weight:650;font-variant-numeric:tabular-nums;
color:var(--vibeui-eventcalendar-001-muted);
}
[data-vibeui-block="eventcalendar-001"] [data-part="cell"][data-outside="false"] [data-part="daynum"]{
color:var(--vibeui-eventcalendar-001-fg);
}
[data-vibeui-block="eventcalendar-001"] [data-part="chip"]{
display:flex;align-items:center;gap:0.25rem;
padding:0.125rem 0.3125rem;border-radius:0.3125rem;
border-left:3px solid var(--vibeui-eventcalendar-001-accent);
background:color-mix(in oklab,var(--vibeui-eventcalendar-001-accent) 12%,transparent);
font-size:0.625rem;line-height:1.3;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
[data-vibeui-block="eventcalendar-001"] [data-part="chip"] b{
font-weight:600;font-variant-numeric:tabular-nums;
color:var(--vibeui-eventcalendar-001-muted);
}
[data-vibeui-block="eventcalendar-001"] [data-tone="personal"]{
border-left-color:var(--vibeui-eventcalendar-001-personal);
background:color-mix(in oklab,var(--vibeui-eventcalendar-001-personal) 14%,transparent);
}
[data-vibeui-block="eventcalendar-001"] [data-tone="hold"]{
border-left-color:var(--vibeui-eventcalendar-001-hold);border-left-style:dashed;
background:color-mix(in oklab,var(--vibeui-eventcalendar-001-hold) 10%,transparent);
}
/* «Ещё N» — обычный <details>: раскрытие списка не требует состояния,
   а скрытое событие обязано оставаться достижимым с клавиатуры. */
[data-vibeui-block="eventcalendar-001"] [data-part="more"]{margin-top:auto}
[data-vibeui-block="eventcalendar-001"] summary{
list-style:none;cursor:pointer;border-radius:0.3125rem;padding:0.0625rem 0.25rem;
font-size:0.625rem;font-weight:600;
color:var(--vibeui-eventcalendar-001-accent);
}
[data-vibeui-block="eventcalendar-001"] summary::-webkit-details-marker{display:none}
[data-vibeui-block="eventcalendar-001"] summary:focus-visible{
outline:2px solid var(--vibeui-eventcalendar-001-accent);outline-offset:1px;
}
[data-vibeui-block="eventcalendar-001"] [data-part="overflow"]{
position:absolute;left:0.25rem;right:0.25rem;top:100%;z-index:2;
display:flex;flex-direction:column;gap:0.1875rem;
margin:0.1875rem 0 0;padding:0.375rem;list-style:none;
background:var(--vibeui-eventcalendar-001-bg);
border:1px solid var(--vibeui-eventcalendar-001-border);border-radius:0.5rem;
box-shadow:0 10px 24px oklch(0.24 0.014 265 / 14%);
}
[data-vibeui-block="eventcalendar-001"] [data-part="cell"]:nth-child(n+29) [data-part="overflow"]{
top:auto;bottom:100%;margin:0 0 0.1875rem;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="eventcalendar-001"] *{animation:none!important;transition:none!important}}
`

const DAY = 86400000

const DEFAULT_EVENTS: Eventcalendar001Event[] = [
  { day: 3, title: "Планёрка", time: "10:00" },
  { day: 5, title: "Ревью дизайна", time: "14:00" },
  { day: 5, title: "Созвон с Кимом", time: "16:30", tone: "personal" },
  { day: 11, title: "Релиз 4.2", time: "12:00" },
  { day: 12, title: "Бассейн", time: "07:30", tone: "personal" },
  { day: 12, title: "Спринт-обзор", time: "11:00" },
  { day: 12, title: "Интервью", time: "15:00" },
  { day: 12, title: "Ретро", time: "17:00", tone: "hold" },
  { day: 18, title: "Отчёт за квартал", time: "09:00" },
  { day: 19, title: "Обед с Верой", time: "13:00", tone: "personal" },
  { day: 19, title: "Правки бюджета", tone: "hold" },
  { day: 24, title: "Демо клиенту", time: "11:30" },
  { day: 24, title: "Тренировка", time: "19:00", tone: "personal" },
  { day: 24, title: "Черновик плана", tone: "hold" },
  { day: 27, title: "Выезд команды", time: "10:00" },
]

/**
 * Месяц с событиями в клетках: лишние сворачиваются в «ещё N» и
 * раскрываются нативным <details>. Один файл, ноль зависимостей.
 */
export function Eventcalendar001({
  month = "2026-03-01",
  events = DEFAULT_EVENTS,
  visible = 2,
  heading,
  locale = "ru-RU",
  accent,
  className,
  style,
  ...props
}: Eventcalendar001Props) {
  const anchor = new Date(`${month}T00:00:00Z`)
  const year = anchor.getUTCFullYear()
  const monthIndex = anchor.getUTCMonth()
  const first = Date.UTC(year, monthIndex, 1)
  const shift = (new Date(first).getUTCDay() + 6) % 7
  const start = first - shift * DAY

  const monthName = new Intl.DateTimeFormat(locale, {
    month: "long",
    year: "numeric",
    timeZone: "UTC",
  }).format(anchor)

  const weekdays = Array.from({ length: 7 }, (_, index) =>
    new Intl.DateTimeFormat(locale, {
      weekday: "short",
      timeZone: "UTC",
    }).format(new Date(start + index * DAY)),
  )

  const cells = Array.from({ length: 42 }, (_, index) => {
    const date = new Date(start + index * DAY)
    const outside = date.getUTCMonth() !== monthIndex
    const day = date.getUTCDate()

    return {
      key: date.toISOString().slice(0, 10),
      day,
      outside,
      events: outside ? [] : events.filter((event) => event.day === day),
    }
  })

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

  return (
    <>
      <style href="vibeui-eventcalendar-001" precedence="medium">
        {STYLES}
      </style>
      <section
        {...props}
        data-vibeui-block="eventcalendar-001"
        aria-label={heading ?? monthName}
        className={className}
        style={palette}
      >
        <header data-part="head">
          <h3 data-part="heading">{heading ?? monthName}</h3>
          <p data-part="count">{events.length} событий в месяце</p>
        </header>

        <div data-part="grid">
          {weekdays.map((weekday) => (
            <span key={weekday} data-part="weekday">
              {weekday}
            </span>
          ))}

          {cells.map((cell) => {
            const shown = cell.events.slice(0, visible)
            const hidden = cell.events.slice(visible)

            return (
              <div
                key={cell.key}
                data-part="cell"
                data-outside={String(cell.outside)}
              >
                <span data-part="daynum">{cell.day}</span>

                {shown.map((event) => (
                  <span
                    key={event.title}
                    data-part="chip"
                    data-tone={event.tone ?? "work"}
                  >
                    {event.time ? <b>{event.time}</b> : null}
                    {event.title}
                  </span>
                ))}

                {hidden.length > 0 ? (
                  <details data-part="more">
                    <summary>ещё {hidden.length}</summary>
                    <ul data-part="overflow">
                      {hidden.map((event) => (
                        <li
                          key={event.title}
                          data-part="chip"
                          data-tone={event.tone ?? "work"}
                        >
                          {event.time ? <b>{event.time}</b> : null}
                          {event.title}
                        </li>
                      ))}
                    </ul>
                  </details>
                ) : null}
              </div>
            )
          })}
        </div>
      </section>
    </>
  )
}