event-calendar
Event calendar with all views and live settings
Event calendar with all views and live settings
Загрузка превью…
src/event-calendar/c-event-calendar-1.vue
<!--
ПОРТ c-event-calendar-1 ("Event calendar with all views and live settings").
Апстрим держит `view` в React `useState` и меняет `viewSettings`/
`interactions`/`weekStartsOn`/`dayStartHour`/`dayEndHour`/`interval`/
`snapDuration`/`locale`/`timeZone` контролируемо (родитель хранит
`DemoSettings`, `EventCalendar` перерендеривается с новыми пропами).
Наш `EventCalendar.vue` v1 — только неуправляемый движок (`context.ts`,
`createEventCalendar` читает `options` один раз при создании), поэтому
такая живая перепривязка пропов в принципе не работает так же, как в
React. Настройки, для которых есть императивный сеттер на `api`
(`setViewSettings`/`setInteractions`), реализованы по-настоящему через
`calRef`. Остальные разделы демо-панели (Time grid: day start/end/
interval/snap; Region: language/timeZone; Week starts on; Day add button)
сеттера на движке не имеют — сознательно не включены в порт вместо
притворно интерактивных мёртвых контролов (правило 4a: не изображаем
работающее поведение, которого на самом деле нет). Popover закрыт по
умолчанию и не раскрывается диф-гейтом (см. docs/PORTING.md §10), поэтому
сокращение состава здесь не влияет на визуальное сравнение.
IconPlaceholder (Settings/Plus) заменён инлайновым `<svg>` (пути lucide
"settings-2"/"plus" v0.545.0), тем же приёмом, что и везде в порте.
`EventCalendarNav` — настоящий порт (без view switcher/date picker,
см. EventCalendarNav.vue), `EventCalendarToolbar` — простая обёртка
`flex items-center gap-2`, инлайнена ниже вместо отдельного компонента
(не экспортируется портом, см. комментарий в EventCalendarNav.vue).
`timeZone="UTC"` и фиксированный якорь `Date.UTC(2026, 5, 15)` — по тем же
причинам, что и в кейсе `ui-event-calendar` (стенд крутится в UTC,
"сегодня" недетерминировано). `renderEvent`/`eventTooltip`/`maxEventsPerCell`
(3 вместо дефолтного "auto" — ResizeObserver-измерение недетерминировано на
статичном скриншоте, тот же обход, что и в `ui-event-calendar`) переданы
через новый `view-config`-проп `EventCalendar.vue` (root-level оверрайд
view-layer конфига, добавлен этой задачей — см. комментарий в
`EventCalendar.vue`), а не отдельными плоскими пропами, как в оригинале.
-->
<script setup lang="ts">
import { h, reactive, ref } from "vue"
import {
addDays,
addMinutes,
setHours,
startOfDay,
startOfWeek,
} from "date-fns"
import { EventCalendar, EventCalendarContent, EventCalendarNav, type CalendarEvent, type EventCalendarApi, type EventCalendarResource, type EventCalendarRenderEventProps } from "@/components/reui/event-calendar"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Label } from "@/components/ui/label"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Switch } from "@/components/ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
const ANCHOR = new Date(Date.UTC(2026, 5, 15))
const TEAM: EventCalendarResource[] = [
{ id: "alex", title: "Alex", color: "var(--color-blue-500)" },
{ id: "mia", title: "Mia", color: "var(--color-violet-500)" },
{ id: "sam", title: "Sam", color: "var(--color-emerald-500)" },
]
function buildEvents(anchor: Date): CalendarEvent[] {
const week = startOfWeek(startOfDay(anchor), { weekStartsOn: 0 })
const at = (dayOffset: number, hour: number, minute = 0) =>
addMinutes(setHours(addDays(week, dayOffset), hour), minute)
const day = (dayOffset: number) => addDays(week, dayOffset)
return [
{ id: "team-sync", title: "Team sync", start: at(1, 9, 0), end: at(1, 9, 30), resourceId: "alex" },
{ id: "design-review", title: "Design review", start: at(2, 11, 0), end: at(2, 12, 0), resourceId: "mia", color: "var(--color-violet-500)" },
{ id: "product-demo", title: "Product demo", start: at(3, 15, 0), end: at(3, 16, 0), resourceId: "sam", color: "var(--color-emerald-500)" },
{ id: "roadmap-planning", title: "Roadmap planning", start: at(4, 10, 0), end: at(4, 11, 30), resourceId: "alex", color: "var(--color-indigo-500)" },
{ id: "client-call", title: "Client call", start: at(5, 14, 0), end: at(5, 15, 0), resourceId: "mia", color: "var(--color-amber-500)" },
{ id: "team-offsite", title: "Team offsite", start: day(4), end: day(6), allDay: true, color: "var(--color-rose-500)" },
{ id: "sprint-planning", title: "Sprint planning", start: at(9, 9, 30), end: at(9, 10, 30), resourceId: "sam", color: "var(--color-blue-500)" },
{ id: "quarterly-review", title: "Quarterly review", start: at(17, 13, 0), end: at(17, 14, 30), resourceId: "alex", color: "var(--color-cyan-500)" },
]
}
const events = buildEvents(ANCHOR)
function renderEventContent({ occurrence }: EventCalendarRenderEventProps) {
const event = occurrence.event
if (event.id === "design-review") {
return [
h("span", { class: "flex shrink-0 -space-x-1" }, [
h(Avatar, { class: "ring-background size-4 ring-1" }, () =>
h(AvatarFallback, { class: "bg-violet-500 text-[8px] font-semibold text-white" }, () => "MJ")
),
h(Avatar, { class: "ring-background size-4 ring-1" }, () =>
h(AvatarFallback, { class: "bg-sky-500 text-[8px] font-semibold text-white" }, () => "AL")
),
]),
h("span", { class: "truncate font-medium" }, event.title),
]
}
if (event.id === "client-call") {
return h("span", { class: "flex w-full min-w-0 items-center gap-1.5" }, [
h("span", { "aria-hidden": "true", class: "-me-0.5 size-1.5 shrink-0 rounded-full bg-(--ec-event-color)" }),
h("span", { class: "truncate font-medium" }, event.title),
h("span", { class: "ms-auto shrink-0 rounded bg-(--ec-event-color)/25 px-1 text-[10px] font-semibold" }, "30m"),
])
}
return undefined
}
const calRef = ref<{ api: EventCalendarApi } | null>(null)
let newEventCount = 0
const viewSettings = reactive({ weekends: true, weekNumbers: false, nowIndicator: true, offDays: false })
const interactions = reactive({ drag: true, resize: true, selectSlot: true })
const eventTooltip = ref(false)
function patchViewSettings(patch: Partial<typeof viewSettings>) {
Object.assign(viewSettings, patch)
calRef.value?.api.setViewSettings({ ...viewSettings })
}
function patchInteractions(patch: Partial<typeof interactions>) {
Object.assign(interactions, patch)
calRef.value?.api.setInteractions({ ...interactions })
}
function addEvent() {
const start = setHours(startOfDay(ANCHOR), 12)
const end = addMinutes(start, 60)
calRef.value?.api.addEvent({
id: `new-event-${newEventCount++}`,
title: "New event",
start,
end,
resourceId: "alex",
color: "var(--color-blue-500)",
})
calRef.value?.api.goTo(start)
}
</script>
<template>
<div class="w-full p-4">
<Card class="w-full py-0">
<CardContent class="p-0">
<EventCalendar
ref="calRef"
:events="events"
view="month"
:resources="TEAM"
:view-config="{ renderEvent: renderEventContent, eventTooltip, maxEventsPerCell: 3 }"
time-zone="UTC"
:view-settings="viewSettings"
:interactions="interactions"
off-days
class="h-[640px] w-full"
>
<div class="flex flex-wrap items-center gap-2 pe-2">
<EventCalendarNav class="min-w-0 flex-1" />
<div data-slot="event-calendar-toolbar" class="flex items-center gap-2">
<Popover>
<PopoverTrigger as-child>
<Button variant="outline" size="sm">
<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="size-4" aria-hidden="true"><path d="M14 17H5" /><path d="M19 7h-9" /><circle cx="17" cy="17" r="3" /><circle cx="7" cy="7" r="3" /></svg>
Settings
</Button>
</PopoverTrigger>
<PopoverContent align="end" :side-offset="8" class="w-80">
<Tabs default-value="view">
<TabsList class="w-full">
<TabsTrigger value="view" class="flex-1">View</TabsTrigger>
<TabsTrigger value="behavior" class="flex-1">Behavior</TabsTrigger>
</TabsList>
<TabsContent value="view" class="flex flex-col gap-3">
<div class="flex items-center justify-between gap-4">
<Label for="ec-set-weekends" class="font-normal">Weekends</Label>
<Switch id="ec-set-weekends" :model-value="viewSettings.weekends" @update:model-value="(v: boolean) => patchViewSettings({ weekends: v })" />
</div>
<div class="flex items-center justify-between gap-4">
<Label for="ec-set-week-numbers" class="font-normal">Week numbers</Label>
<Switch id="ec-set-week-numbers" :model-value="viewSettings.weekNumbers" @update:model-value="(v: boolean) => patchViewSettings({ weekNumbers: v })" />
</div>
<div class="flex items-center justify-between gap-4">
<Label for="ec-set-now" class="font-normal">Now indicator</Label>
<Switch id="ec-set-now" :model-value="viewSettings.nowIndicator" @update:model-value="(v: boolean) => patchViewSettings({ nowIndicator: v })" />
</div>
<div class="flex items-center justify-between gap-4">
<Label for="ec-set-off-days" class="font-normal">Mark off days</Label>
<Switch id="ec-set-off-days" :model-value="viewSettings.offDays" @update:model-value="(v: boolean) => patchViewSettings({ offDays: v })" />
</div>
</TabsContent>
<TabsContent value="behavior" class="flex flex-col gap-3">
<div class="flex items-center justify-between gap-4">
<Label for="ec-set-drag" class="font-normal">Drag to move</Label>
<Switch id="ec-set-drag" :model-value="interactions.drag" @update:model-value="(v: boolean) => patchInteractions({ drag: v })" />
</div>
<div class="flex items-center justify-between gap-4">
<Label for="ec-set-resize" class="font-normal">Drag to resize</Label>
<Switch id="ec-set-resize" :model-value="interactions.resize" @update:model-value="(v: boolean) => patchInteractions({ resize: v })" />
</div>
<div class="flex items-center justify-between gap-4">
<Label for="ec-set-select-slot" class="font-normal">Drag to create</Label>
<Switch id="ec-set-select-slot" :model-value="interactions.selectSlot" @update:model-value="(v: boolean) => patchInteractions({ selectSlot: v })" />
</div>
<div class="flex items-center justify-between gap-4">
<Label for="ec-set-tooltip" class="font-normal">Event tooltips</Label>
<Switch id="ec-set-tooltip" :model-value="eventTooltip" @update:model-value="(v: boolean) => (eventTooltip = v)" />
</div>
</TabsContent>
</Tabs>
</PopoverContent>
</Popover>
<Button size="sm" @click="addEvent">
<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="size-4" aria-hidden="true"><path d="M5 12h14" /><path d="M12 5v14" /></svg>
New event
</Button>
</div>
</div>
<EventCalendarContent />
</EventCalendar>
</CardContent>
</Card>
</div>
</template>