Calendar

Decade Picker

A year picker by decades: a 4×3 grid shows ten years plus one neighbour on each edge, and the arrows page ten years at a time.

  • calendar
  • year
  • picker
  • decade

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

20202029
Выбран год 202619702040
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-012" (Decade Picker) 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-012.json

Registry item: https://vibeui.ru/r/calendar-012.json
Installs to: components/vibeui/calendar-012.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 year picker by decades: a 4×3 grid shows ten years plus one neighbour on each edge, and the arrows page ten years at a time.

A decade-based year picker: a 4×3 grid, arrows stepping by ten years, out-of-range years switched off. Client-side, one file, zero dependencies.

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

<Calendar012 defaultValue={2026} min={1970} max={2040} />

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-012-* palette — with someone else's theme tokens the component stops matching its preview
- twelve cells instead of ten: the edge neighbours remove an extra arrow press at a decade boundary
- aria-pressed on the year buttons: without it the chosen year is visible to sighted users only
- disabled plus strike-through for years outside min and max: dimming alone blurs "not allowed" and "another decade"
- the arrows locked at the range boundaries: there is nowhere to page when the next decade lies fully outside min and max
- the component's own light surface: dark digits are invisible on the dark catalogue card

## 6. You may change
- the initial year through defaultValue
- the lower bound through min and the upper one through max
- the highlight colour through accent
- the selection handler through onChange

## 7. Rules
- The component knows nothing about months and days: it is a year-selection step, not a full calendar.
- min and max limit pressing only, not paging inside the visible grid — there the years are simply switched off.
- The 4×3 grid keeps the height constant at any width: do not move it to auto-fill or the block starts jumping.
- A defaultValue outside the min–max range will show as selected but cannot be pressed again — validate the input.

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

Клиентский компонент: useState держит выбранный год и текущее десятилетие, поэтому нужен «use client». Сетка из двенадцати кнопок — это десять лет десятилетия и по одному году с каждой стороны: переход через границу (1999 из сетки 2000-х) делается одним нажатием, без похода стрелкой назад. Соседние годы помечаются data-outside и приглушаются, но остаются нажимаемыми. Выход за min/max выключает кнопку через disabled и перечёркивает подпись, а стрелки блокируются, когда следующее десятилетие целиком вне диапазона. Выбранный год отмечен aria-pressed, поэтому скринридер сообщает состояние без дополнительной подписи.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

"use client"

import { useState } from "react"
import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Calendar012Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children" | "onChange" | "defaultValue"
> & {
  defaultValue?: number
  min?: number
  max?: number
  onChange?: (year: number) => void
  accent?: string
}

// Идея компонента: год выбирается десятилетием, а не прокруткой списка.
// Сетка 4×3 показывает десять лет десятилетия плюс по одному соседнему с
// краёв — переход через границу (1999 → 2000) не требует нажатия стрелки.
const STYLES = `
:where([data-vibeui-block="calendar-012"]){
--vibeui-calendar-012-bg:oklch(1 0 0);
--vibeui-calendar-012-fg:oklch(0.24 0.014 265);
--vibeui-calendar-012-muted:oklch(0.62 0.014 265);
--vibeui-calendar-012-border:oklch(0.91 0.006 265);
--vibeui-calendar-012-hover:oklch(0.96 0.004 265);
--vibeui-calendar-012-accent:oklch(0.54 0.16 285);
--vibeui-calendar-012-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="calendar-012"]{
display:flex;flex-direction:column;gap:0.75rem;
width:100%;max-width:19rem;box-sizing:border-box;padding:0.875rem;
background:var(--vibeui-calendar-012-bg);
border:1px solid var(--vibeui-calendar-012-border);border-radius:0.875rem;
color:var(--vibeui-calendar-012-fg);font-family:var(--vibeui-calendar-012-font);
}
[data-vibeui-block="calendar-012"] [data-part="head"]{
display:flex;align-items:center;justify-content:space-between;gap:0.5rem;
}
[data-vibeui-block="calendar-012"] [data-part="range"]{
font-size:0.9375rem;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:-0.01em;
}
[data-vibeui-block="calendar-012"] [data-part="nav"] button{
appearance:none;cursor:pointer;
display:inline-flex;align-items:center;justify-content:center;
width:1.75rem;height:1.75rem;margin-left:0.25rem;padding:0;
border:1px solid var(--vibeui-calendar-012-border);border-radius:0.5rem;
background:transparent;color:inherit;
}
[data-vibeui-block="calendar-012"] [data-part="nav"] button:hover:not(:disabled){background:var(--vibeui-calendar-012-hover)}
[data-vibeui-block="calendar-012"] [data-part="nav"] button:disabled{opacity:.35;cursor:not-allowed}
[data-vibeui-block="calendar-012"] [data-part="nav"] button:focus-visible{outline:2px solid var(--vibeui-calendar-012-accent);outline-offset:2px}
[data-vibeui-block="calendar-012"] [data-part="arrow"]{
width:0.375rem;height:0.375rem;
border-left:1.5px solid currentColor;border-bottom:1.5px solid currentColor;
transform:rotate(45deg) translate(0.0625rem,-0.0625rem);
}
[data-vibeui-block="calendar-012"] [data-part="arrow"][data-dir="next"]{transform:rotate(-135deg) translate(0.0625rem,-0.0625rem)}
[data-vibeui-block="calendar-012"] [data-part="grid"]{
display:grid;grid-template-columns:repeat(4,1fr);gap:0.25rem;
}
[data-vibeui-block="calendar-012"] [data-part="grid"] button{
appearance:none;cursor:pointer;
height:2.5rem;padding:0;border:0;border-radius:0.5rem;
background:transparent;color:inherit;
font:inherit;font-size:0.875rem;font-variant-numeric:tabular-nums;
transition:background-color .14s ease;
}
[data-vibeui-block="calendar-012"] [data-part="grid"] button:hover:not(:disabled){background:var(--vibeui-calendar-012-hover)}
[data-vibeui-block="calendar-012"] [data-part="grid"] button:focus-visible{outline:2px solid var(--vibeui-calendar-012-accent);outline-offset:-2px}
/* Соседние десятилетия приглушены, но нажимаются: 1999 из сетки 2000-х
   выбирается одним нажатием, без похода стрелкой назад. */
[data-vibeui-block="calendar-012"] [data-part="grid"] button[data-outside="true"]{color:var(--vibeui-calendar-012-muted);opacity:.6}
[data-vibeui-block="calendar-012"] [data-part="grid"] button:disabled{opacity:.25;cursor:not-allowed;text-decoration:line-through}
[data-vibeui-block="calendar-012"] [data-part="grid"] button[aria-pressed="true"]{
background:var(--vibeui-calendar-012-accent);color:oklch(0.99 0.01 285);font-weight:700;opacity:1;
}
[data-vibeui-block="calendar-012"] [data-part="foot"]{
display:flex;align-items:center;justify-content:space-between;gap:0.5rem;
font-size:0.75rem;color:var(--vibeui-calendar-012-muted);
}
[data-vibeui-block="calendar-012"] [data-part="foot"] strong{
color:var(--vibeui-calendar-012-fg);font-variant-numeric:tabular-nums;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="calendar-012"] *{animation:none!important;transition:none!important}}
`

function decadeStart(year: number) {
  return Math.floor(year / 10) * 10
}

/**
 * Выбор года десятилетиями: сетка 4×3 и стрелки по десять лет.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Calendar012({
  defaultValue = 2026,
  min = 1970,
  max = 2040,
  onChange,
  accent,
  className,
  style,
  ...props
}: Calendar012Props) {
  const [selected, setSelected] = useState(defaultValue)
  const [decade, setDecade] = useState(() => decadeStart(defaultValue))

  const years = Array.from({ length: 12 }, (_, index) => decade - 1 + index)

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

  const pick = (year: number) => {
    setSelected(year)
    setDecade(decadeStart(year))
    onChange?.(year)
  }

  return (
    <>
      <style href="vibeui-calendar-012" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="calendar-012"
        className={className}
        style={palette}
      >
        <div data-part="head">
          <span data-part="range">
            {decade} — {decade + 9}
          </span>
          <span data-part="nav">
            <button
              type="button"
              aria-label="Предыдущее десятилетие"
              disabled={decade - 10 + 9 < min}
              onClick={() => setDecade(decade - 10)}
            >
              <span data-part="arrow" aria-hidden="true" />
            </button>
            <button
              type="button"
              aria-label="Следующее десятилетие"
              disabled={decade + 10 > max}
              onClick={() => setDecade(decade + 10)}
            >
              <span data-part="arrow" data-dir="next" aria-hidden="true" />
            </button>
          </span>
        </div>
        <div data-part="grid" role="group" aria-label="Годы десятилетия">
          {years.map((year) => (
            <button
              key={year}
              type="button"
              aria-pressed={year === selected}
              data-outside={year < decade || year > decade + 9}
              disabled={year < min || year > max}
              onClick={() => pick(year)}
            >
              {year}
            </button>
          ))}
        </div>
        <div data-part="foot">
          <span>
            Выбран год <strong>{selected}</strong>
          </span>
          <span>
            {min}—{max}
          </span>
        </div>
      </div>
    </>
  )
}