event-calendar
Event calendar with custom event chips
Event calendar with custom event chips
Загрузка превью…
src/event-calendar/c-event-calendar-4.vue
<!--
ПОРТ c-event-calendar-4 ("Event calendar with custom event chips").
IconPlaceholder (Palette/Code/Megaphone/Compass в чипах, Flag/Plus в
тулбаре) заменён инлайновым `<svg>` (пути lucide "palette"/"code"/
"megaphone"/"compass"/"flag"/"plus" v0.545.0). `renderEvent`/
`renderEventTooltip`/`eventTooltip`/`maxEventsPerCell` переданы через
`view-config`-проп `EventCalendar` (см. c-event-calendar-1.vue и
комментарий в `EventCalendar.vue`). `EventCalendarNav` — настоящий порт,
уже без view switcher (совпадает с `showViewSwitcher={false}` оригинала
буквально). `timeZone="UTC"` и фиксированный якорь `Date.UTC(2026, 5, 15)` —
те же причины, что и в `ui-event-calendar`. `renderEventTooltip` не
раскрывается диф-гейтом (Tooltip — плавающий слой, см.
docs/PORTING.md §10), поэтому не адаптировался отдельно.
-->
<script setup lang="ts">
import { h, ref } from "vue"
import { addDays, startOfDay, startOfWeek } from "date-fns"
import { EventCalendar, EventCalendarContent, EventCalendarNav, type CalendarEvent, type EventCalendarApi, type EventCalendarRenderEventProps } from "@/components/reui/event-calendar"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
const ANCHOR = new Date(Date.UTC(2026, 5, 15))
type Category = "design" | "engineering" | "marketing" | "product"
const CATEGORY_ICON: Record<Category, () => ReturnType<typeof h>> = {
design: () =>
h("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-3.5", "aria-hidden": "true" }, [
h("path", { d: "M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z" }),
h("circle", { cx: "13.5", cy: "6.5", r: ".5", fill: "currentColor" }),
h("circle", { cx: "17.5", cy: "10.5", r: ".5", fill: "currentColor" }),
h("circle", { cx: "6.5", cy: "12.5", r: ".5", fill: "currentColor" }),
h("circle", { cx: "8.5", cy: "7.5", r: ".5", fill: "currentColor" }),
]),
engineering: () =>
h("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-3.5", "aria-hidden": "true" }, [
h("path", { d: "m16 18 6-6-6-6" }),
h("path", { d: "m8 6-6 6 6 6" }),
]),
marketing: () =>
h("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-3.5", "aria-hidden": "true" }, [
h("path", { d: "M11 6a13 13 0 0 0 8.4-2.8A1 1 0 0 1 21 4v12a1 1 0 0 1-1.6.8A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" }),
h("path", { d: "M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14" }),
h("path", { d: "M8 6v8" }),
]),
product: () =>
h("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-3.5", "aria-hidden": "true" }, [
h("path", { d: "m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z" }),
h("circle", { cx: "12", cy: "12", r: "10" }),
]),
}
const CATEGORY: Record<Category, { label: string; color: string }> = {
design: { label: "Design", color: "var(--color-violet-500)" },
engineering: { label: "Engineering", color: "var(--color-sky-500)" },
marketing: { label: "Marketing", color: "var(--color-orange-500)" },
product: { label: "Product", color: "var(--color-teal-500)" },
}
interface EventData {
category: Category
}
function buildEvents(anchor: Date): CalendarEvent<EventData>[] {
const week = startOfWeek(startOfDay(anchor), { weekStartsOn: 1 })
const day = (offset: number) => addDays(week, offset)
const span = (id: string, title: string, startOffset: number, endOffset: number, category: Category): CalendarEvent<EventData> => ({
id,
title,
start: day(startOffset),
end: day(endOffset),
allDay: true,
color: CATEGORY[category].color,
data: { category },
})
return [
span("onboarding", "Onboarding revamp", -2, 1, "product"),
span("design-sprint", "Design sprint", 1, 4, "design"),
span("api-migration", "API migration", 2, 6, "engineering"),
span("user-research", "User research", 3, 5, "product"),
span("brand-refresh", "Brand refresh", 8, 10, "design"),
span("launch", "Launch campaign", 9, 13, "marketing"),
span("roadmap", "Roadmap review", 15, 16, "engineering"),
]
}
const events = buildEvents(ANCHOR)
function renderChip({ occurrence }: EventCalendarRenderEventProps<EventData>) {
const category = occurrence.event.data?.category
if (!category) return undefined
return h("span", { class: "flex w-full min-w-0 items-center gap-1.5" }, [
h("span", { class: "flex shrink-0 text-(--ec-event-color)" }, [CATEGORY_ICON[category]()]),
h("span", { class: "truncate font-medium" }, occurrence.event.title),
])
}
function renderTooltip({ occurrence }: { occurrence: { event: CalendarEvent<EventData> } }) {
const category = occurrence.event.data?.category
if (!category) return undefined
return h("div", { class: "space-y-0.5" }, [
h("p", { class: "font-medium" }, occurrence.event.title),
h("p", { class: "text-muted-foreground text-xs" }, CATEGORY[category].label),
])
}
const calRef = ref<{ api: EventCalendarApi<EventData> } | null>(null)
let counter = 0
function addProject() {
const start = startOfDay(ANCHOR)
calRef.value?.api.addEvent({
id: `project-${counter++}`,
title: "New project",
start,
end: addDays(start, 3),
allDay: true,
color: CATEGORY.design.color,
data: { category: "design" },
})
calRef.value?.api.goTo(start)
}
function addMilestone() {
const start = startOfDay(ANCHOR)
calRef.value?.api.addEvent({
id: `milestone-${counter++}`,
title: "Milestone",
start,
end: addDays(start, 1),
allDay: true,
color: CATEGORY.product.color,
data: { category: "product" },
})
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"
:week-starts-on="1"
:interactions="{ drag: true, resize: true, selectSlot: false }"
:view-config="{ renderEvent: renderChip, renderEventTooltip: renderTooltip, eventTooltip: { side: 'top' }, maxEventsPerCell: 3 }"
time-zone="UTC"
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">
<Button variant="outline" size="sm" @click="addMilestone">
<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="M4 22V4a1 1 0 0 1 .4-.8A6 6 0 0 1 8 2c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10a1 1 0 0 1-.4.8A6 6 0 0 1 16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528" /></svg>
Milestone
</Button>
<Button size="sm" @click="addProject">
<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 project
</Button>
</div>
</div>
<EventCalendarContent />
</EventCalendar>
<div class="text-muted-foreground flex flex-wrap items-center gap-x-4 gap-y-2 border-t px-4 py-3 text-xs"><span v-for="key in (Object.keys(CATEGORY) as Category[])" :key="key" class="flex items-center gap-1.5"><span aria-hidden="true" class="size-2 rounded-full" :style="{ backgroundColor: CATEGORY[key].color }" />{{ CATEGORY[key].label }}</span></div>
</CardContent>
</Card>
</div>
</template>
Установка
npx shadcn-vue@latest add https://revueui.rootapi.dev/r/c-event-calendar-4.jsonЗависимости реестра
npm-зависимости
- date-fns
Источник: порт из ReUI (Keenthemes, MIT)