calendar

Custom navigation with year view

Custom navigation with year view

Загрузка превью…

src/calendar/c-calendar-13.vue

<!--
  АДАПТАЦИЯ (docs/PORTING.md §4a/§5): оригинал подменяет внутренние
  `react-day-picker`-компоненты `CaptionLabel`/`MonthGrid`, накладывая
  поверх сетки дней отдельный слой выбора года/месяца. `reka-ui`
  `CalendarRoot` не даёт способа подменить `MonthGrid` изнутри — здесь тот
  же оверлей (год → месяц, `ScrollArea`, кнопки) собран вокруг `CCalendar`
  как обычный `position: absolute` слой поверх неё, тем же приёмом, что и
  в оригинале (`bg-background absolute inset-0 z-20`). Логика переключения
  (клик по подписи месяца → список лет → список месяцев) перенесена без
  изменений; месяц/сегодня зафиксированы явно, без `new Date()`.
-->
<script setup lang="ts">
import { ref, computed } from "vue"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Calendar as CCalendar } from "@/components/ui/calendar"

const today = new Date(2026, 0, 15)
const month = ref<Date>(today)
const date = ref<Date | undefined>(today)
const isYearView = ref(false)
const selectedYear = ref<number | null>(null)

const startYear = today.getFullYear() - 10
const endYear = today.getFullYear() + 10
const years = Array.from({ length: endYear - startYear + 1 }, (_, i) => startYear + i)
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]

const caption = computed(() =>
  // en-US явно: react-day-picker форматирует подписи на английском
  // независимо от локали окружения (см. Calendar.vue (@revueui/ui)/effectiveLocale).
  new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric" }).format(month.value)
)

function toggleYearView() {
  isYearView.value = !isYearView.value
  if (!isYearView.value) selectedYear.value = null
}

function pickYear(y: number) {
  selectedYear.value = y
}

function pickMonth(mIndex: number) {
  if (selectedYear.value === null) return
  month.value = new Date(selectedYear.value, mIndex, 1)
  isYearView.value = false
  selectedYear.value = null
}

function isMonthDisabled(y: number, mIndex: number): boolean {
  const d = new Date(y, mIndex, 1)
  return d < new Date(startYear, 0, 1) || d > new Date(endYear, 11, 31)
}
</script>

<template>
  <Card class="p-0">
    <CardContent class="p-0">
      <div class="relative">
        <CCalendar
          mode="single"
          v-model="date"
          :month="month"
          @update:month="month = $event"
          caption-class="ms-2.5 justify-start"
          nav-class="flex items-center w-full absolute inset-x-0 justify-end pointer-events-none [&>button]:pointer-events-auto"
        >
          <template #caption>
            <div class="flex h-(--cell-size) w-full items-center px-(--cell-size) ms-2.5 justify-start">
              <Button
                class="-ms-2 flex items-center gap-2 text-sm font-medium hover:bg-transparent"
                :data-state="isYearView ? 'open' : 'closed'"
                size="sm"
                variant="ghost"
                @click="toggleYearView"
              >
                {{ caption }}
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" :class="['text-muted-foreground/80 shrink-0 transition-transform duration-200', isYearView && 'rotate-180']">
                  <path d="m6 9 6 6 6-6" />
                </svg>
              </Button>
            </div>
          </template>
        </CCalendar>
        <div v-if="isYearView" class="bg-background absolute inset-0 z-20 -m-2">
          <div class="h-full">
            <ScrollArea class="h-full">
              <div class="px-3 pt-1 pb-3">
                <div v-if="selectedYear === null" class="grid grid-cols-4 gap-2">
                  <Button
                    v-for="y in years"
                    :key="y"
                    :variant="y === month.getFullYear() ? 'default' : 'outline'"
                    size="sm"
                    class="h-8"
                    @click="pickYear(y)"
                  >
                    {{ y }}
                  </Button>
                </div>
                <div v-else class="flex flex-col gap-2">
                  <div class="flex items-center gap-2">
                    <Button variant="ghost" size="sm" class="px-2" @click="selectedYear = null">
                      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1 size-4 rotate-90">
                        <path d="m6 9 6 6 6-6" />
                      </svg>
                      {{ selectedYear }}
                    </Button>
                  </div>
                  <div class="grid grid-cols-3 gap-2">
                    <Button
                      v-for="(m, mi) in monthNames"
                      :key="m"
                      :variant="mi === month.getMonth() && selectedYear === month.getFullYear() ? 'default' : 'outline'"
                      size="sm"
                      class="h-8"
                      :disabled="isMonthDisabled(selectedYear, mi)"
                      @click="pickMonth(mi)"
                    >
                      {{ m }}
                    </Button>
                  </div>
                </div>
              </div>
            </ScrollArea>
          </div>
        </div>
      </div>
    </CardContent>
  </Card>
</template>

Установка

npx shadcn-vue@latest add https://revueui.rootapi.dev/r/c-calendar-13.json

Зависимости реестра

Источник: порт из ReUI (Keenthemes, MIT)