Raccourci clavier : Ctrl + K
Commencer

Calendrier

Un agenda à lire et à réorganiser : vues jour, semaine, mois et année, avec des événements que l'on peut déplacer et étirer. Ouvrir l'un d'eux pour l'éditer vous revient.

Utilisation

vue

5–11 janvier 2026

lun.5
mar.6
mer.7
jeu.8
ven.9
sam.10
dim.11
Appuyez sur Entrée pour ouvrir cet évènement. Appuyez sur Espace pour le saisir, puis sur les flèches pour le déplacer et sur Maj avec les flèches pour changer son heure de fin.
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { VCalendar, type CalendarEvent } from 'vectis-ui'

/*
 * This page is prerendered, so the clock cannot be read while the markup is being built:
 * the build date would be written into the HTML and the browser would disagree with it.
 * The schedule below therefore starts on a fixed week and moves onto the current one once
 * the page is in the browser, which is what the theme does here for the same reason.
 */
const REFERENCE_MONDAY = '2026-01-05'

const anchor = ref(REFERENCE_MONDAY)
const events = ref<CalendarEvent[]>(scheduleFor(REFERENCE_MONDAY))

onMounted(() => {
  anchor.value = mondayOf(new Date())
  events.value = scheduleFor(anchor.value)
})

/** A date as the local ISO day the calendar reads, never through UTC. */
function isoOf(date: Date): string {
  const two = (part: number) => String(part).padStart(2, '0')
  return `${date.getFullYear()}-${two(date.getMonth() + 1)}-${two(date.getDate())}`
}

/** The Monday of the week a date falls in. */
function mondayOf(date: Date): string {
  const monday = new Date(date)
  monday.setDate(date.getDate() - ((date.getDay() + 6) % 7))
  return isoOf(monday)
}

/** The same three appointments, on whichever week starts on the Monday given. */
function scheduleFor(monday: string): CalendarEvent[] {
  const day = (offset: number) => {
    // The time keeps the parse local: a bare `YYYY-MM-DD` is read as UTC.
    const date = new Date(`${monday}T00:00:00`)
    date.setDate(date.getDate() + offset)
    return isoOf(date)
  }

  return [
    {
      id: 'standup',
      title: 'Standup',
      start: day(0),
      end: day(0),
      startTime: '09:00',
      endTime: '09:15',
    },
    {
      id: 'review',
      title: 'Design review',
      start: day(1),
      end: day(1),
      startTime: '14:00',
      endTime: '15:30',
      description: 'With Anna and Ravi',
    },
    {
      id: 'lunch',
      title: 'Team lunch',
      start: day(3),
      end: day(3),
      startTime: '12:30',
      endTime: '13:30',
    },
  ]
}
</script>

<template>
  <VCalendar v-model:date="anchor" v-model:events="events" label="Team schedule" class="calendar" />
</template>

<style scoped>
/* A calendar has no height of its own, and collapses without one. */
.calendar {
  block-size: 34rem;
}
</style>

Exemples

Mois

La vue mois dessine chaque jour comme une case contenant ses événements en puces. monthEventLimit limite le nombre affiché avant que le reste soit compté.

vue

juin 2026

Appuyez sur Entrée pour ouvrir cet évènement. Appuyez sur Espace pour le saisir, puis sur les flèches pour le déplacer et sur Maj avec les flèches pour changer son heure de fin.
<script setup lang="ts">
import { ref } from 'vue'
import { VCalendar, type CalendarEvent } from 'vectis-ui'

const events = ref<CalendarEvent[]>([
  {
    id: 'standup',
    title: 'Standup',
    start: '2026-06-08',
    end: '2026-06-08',
    startTime: '09:00',
    endTime: '09:15',
  },
  {
    id: 'quarterly',
    title: 'Quarterly plan',
    start: '2026-06-10',
    end: '2026-06-10',
    startTime: '08:00',
    endTime: '08:30',
  },
  {
    id: 'lunch',
    title: 'Team lunch',
    start: '2026-06-10',
    end: '2026-06-10',
    startTime: '12:30',
    endTime: '13:30',
  },
  {
    id: 'one-to-one',
    title: 'One to one',
    start: '2026-06-10',
    end: '2026-06-10',
    startTime: '15:00',
    endTime: '15:45',
  },
  {
    id: 'interview',
    title: 'Interview',
    start: '2026-06-10',
    end: '2026-06-10',
    startTime: '17:00',
    endTime: '18:00',
  },
  {
    id: 'workshop',
    title: 'Workshop',
    start: '2026-06-11',
    end: '2026-06-11',
    startTime: '09:30',
    endTime: '12:00',
  },
  {
    id: 'retro',
    title: 'Retrospective',
    start: '2026-06-19',
    end: '2026-06-19',
    startTime: '16:00',
    endTime: '17:00',
  },
  {
    id: 'release',
    title: 'Release window',
    start: '2026-06-16',
    end: '2026-06-18',
    startTime: '00:00',
    endTime: '23:59',
  },
])
</script>

<template>
  <VCalendar
    v-model:events="events"
    view="month"
    :views="['month', 'week', 'day']"
    date="2026-06-10"
    label="Month"
    class="calendar"
  />
</template>

<style scoped>
.calendar {
  block-size: 38rem;
}
</style>

Année

La vue année affiche douze petits mois pour se repérer. Les jours chargés sont cerclés et choisir un mois l'ouvre.

vue

2026

Appuyez sur Entrée pour ouvrir cet évènement. Appuyez sur Espace pour le saisir, puis sur les flèches pour le déplacer et sur Maj avec les flèches pour changer son heure de fin.
<script setup lang="ts">
import { ref } from 'vue'
import { VCalendar, type CalendarEvent } from 'vectis-ui'

/* A year is read for its shape rather than its detail, so the events below are spread over
   it: a few single days, and the two stretches that make a run of days busy at once. */
const events = ref<CalendarEvent[]>([
  {
    id: 'kickoff',
    title: 'Kickoff',
    start: '2026-01-13',
    end: '2026-01-13',
    startTime: '10:00',
    endTime: '11:00',
  },
  {
    id: 'audit',
    title: 'Accessibility audit',
    start: '2026-03-02',
    end: '2026-03-06',
    startTime: '00:00',
    endTime: '23:59',
  },
  {
    id: 'review',
    title: 'Design review',
    start: '2026-04-21',
    end: '2026-04-21',
    startTime: '14:00',
    endTime: '15:30',
  },
  {
    id: 'quarterly',
    title: 'Quarterly plan',
    start: '2026-06-10',
    end: '2026-06-10',
    startTime: '08:00',
    endTime: '09:00',
  },
  {
    id: 'summer',
    title: 'Summer break',
    start: '2026-08-03',
    end: '2026-08-14',
    startTime: '00:00',
    endTime: '23:59',
  },
  {
    id: 'conference',
    title: 'Conference',
    start: '2026-09-22',
    end: '2026-09-24',
    startTime: '00:00',
    endTime: '23:59',
  },
  {
    id: 'retro',
    title: 'Retrospective',
    start: '2026-11-05',
    end: '2026-11-05',
    startTime: '16:00',
    endTime: '17:00',
  },
])
</script>

<template>
  <VCalendar
    v-model:events="events"
    view="year"
    :views="['year', 'month', 'week']"
    date="2026-06-10"
    label="Year"
    class="calendar"
  />
</template>

<style scoped>
.calendar {
  block-size: 38rem;
}
</style>

Vue personnalisée

La vue custom vous en laisse la longueur : customDays dit combien de jours elle montre, et de combien avancent Précédent et Suivant.

vue

10–14 juin 2026

mer.10
jeu.11
ven.12
sam.13
dim.14
Appuyez sur Entrée pour ouvrir cet évènement. Appuyez sur Espace pour le saisir, puis sur les flèches pour le déplacer et sur Maj avec les flèches pour changer son heure de fin.
<script setup lang="ts">
import { ref } from 'vue'
import { VCalendar, type CalendarEvent } from 'vectis-ui'

/* The span below runs from Wednesday to Sunday, which is what shows what the custom view
   is for: a week would have stopped at the Sunday boundary and started again on Monday. */
const events = ref<CalendarEvent[]>([
  {
    id: 'standup',
    title: 'Standup',
    start: '2026-06-10',
    end: '2026-06-10',
    startTime: '09:00',
    endTime: '09:15',
  },
  {
    id: 'review',
    title: 'Design review',
    start: '2026-06-10',
    end: '2026-06-10',
    startTime: '14:00',
    endTime: '15:30',
  },
  {
    id: 'workshop',
    title: 'Workshop',
    start: '2026-06-11',
    end: '2026-06-11',
    startTime: '09:30',
    endTime: '12:00',
  },
  {
    id: 'retro',
    title: 'Retrospective',
    start: '2026-06-12',
    end: '2026-06-12',
    startTime: '16:00',
    endTime: '17:00',
  },
  {
    id: 'conference',
    title: 'Conference',
    start: '2026-06-13',
    end: '2026-06-13',
    startTime: '10:00',
    endTime: '16:00',
  },
  {
    id: 'handover',
    title: 'Handover',
    start: '2026-06-14',
    end: '2026-06-14',
    startTime: '11:00',
    endTime: '12:00',
  },
])
</script>

<template>
  <VCalendar
    v-model:events="events"
    view="custom"
    :custom-days="5"
    :views="['day', 'custom', 'week']"
    :day-start="8"
    :day-end="18"
    date="2026-06-10"
    label="Custom span"
    class="calendar"
  />
</template>

<style scoped>
.calendar {
  block-size: 34rem;
}
</style>

Les jours affichés

weekdays décide quels jours apparaissent, en nombres à partir de 0 pour dimanche, sa première entrée étant le jour où commence la semaine. Sans lui, firstDayOfWeek fixe ce premier jour à la place de la locale. dayStart et dayEnd rognent les heures affichées dans les grilles horaires.

vue

8–12 juin 2026

lun.8
mar.9
mer.10
jeu.11
ven.12
Appuyez sur Entrée pour ouvrir cet évènement. Appuyez sur Espace pour le saisir, puis sur les flèches pour le déplacer et sur Maj avec les flèches pour changer son heure de fin.
<script setup lang="ts">
import { ref } from 'vue'
import { VCalendar, type CalendarEvent } from 'vectis-ui'

const events = ref<CalendarEvent[]>([
  {
    id: 'standup',
    title: 'Standup',
    start: '2026-06-08',
    end: '2026-06-08',
    startTime: '09:00',
    endTime: '09:15',
  },
  {
    id: 'review',
    title: 'Design review',
    start: '2026-06-09',
    end: '2026-06-09',
    startTime: '10:00',
    endTime: '11:30',
  },
  {
    id: 'lunch',
    title: 'Team lunch',
    start: '2026-06-10',
    end: '2026-06-10',
    startTime: '12:30',
    endTime: '13:30',
  },
  {
    id: 'workshop',
    title: 'Workshop',
    start: '2026-06-11',
    end: '2026-06-11',
    startTime: '09:30',
    endTime: '12:00',
  },
  {
    id: 'retro',
    title: 'Retrospective',
    start: '2026-06-12',
    end: '2026-06-12',
    startTime: '16:00',
    endTime: '17:00',
  },
])
</script>

<template>
  <!-- Monday to Friday, eight in the morning to seven in the evening. Every view is on the
       menu, since the weekend is missing from all five and not only from the columns. -->
  <VCalendar
    v-model:events="events"
    :weekdays="[1, 2, 3, 4, 5]"
    :day-start="8"
    :day-end="19"
    :custom-days="3"
    :views="['day', 'custom', 'week', 'month', 'year']"
    date="2026-06-10"
    label="Working week"
    class="calendar"
  />
</template>

<style scoped>
.calendar {
  block-size: 34rem;
}
</style>

Événements sur la journée

allDay place un événement dans le bandeau au-dessus de la grille, où va déjà un événement de 24 heures ou plus. Celui qui passe minuit en durant moins reste dans la grille, avec une carte dans chacun de ses deux jours.

vue

8–14 juin 2026

lun.8
mar.9
mer.10
jeu.11
ven.12
sam.13
dim.14
Journée
Appuyez sur Entrée pour ouvrir cet évènement. Appuyez sur Espace pour le saisir, puis sur les flèches pour le déplacer et sur Maj avec les flèches pour changer son heure de fin.
<script setup lang="ts">
import { ref } from 'vue'
import { VCalendar, type CalendarEvent } from 'vectis-ui'

const events = ref<CalendarEvent[]>([
  // Marked `allDay`, so it goes to the band above the grid although it sits on one day.
  {
    id: 'holiday',
    title: 'Bank holiday',
    start: '2026-06-08',
    end: '2026-06-08',
    startTime: '00:00',
    endTime: '23:59',
    allDay: true,
  },
  // These two run from one day to the next, which puts them in the band on their own.
  {
    id: 'workshop',
    title: 'Workshop',
    start: '2026-06-09',
    end: '2026-06-11',
    startTime: '00:00',
    endTime: '23:59',
  },
  {
    id: 'release',
    title: 'Release window',
    start: '2026-06-10',
    end: '2026-06-12',
    startTime: '00:00',
    endTime: '23:59',
  },
  {
    id: 'review',
    title: 'Design review',
    start: '2026-06-10',
    end: '2026-06-10',
    startTime: '10:00',
    endTime: '11:30',
  },
  {
    id: 'retro',
    title: 'Retrospective',
    start: '2026-06-11',
    end: '2026-06-11',
    startTime: '16:00',
    endTime: '17:00',
  },
])
</script>

<template>
  <VCalendar
    v-model:events="events"
    :day-start="8"
    :day-end="18"
    date="2026-06-10"
    label="All-day events"
    class="calendar"
  />
</template>

<style scoped>
.calendar {
  block-size: 34rem;
}
</style>

Événements qui se chevauchent

Les événements simultanés se partagent la largeur de leur journée, groupés en grappes pour qu'une matinée chargée ne rétrécisse pas un rendez-vous isolé de l'après-midi.

vue

mercredi 10 juin 2026

mer.10
Appuyez sur Entrée pour ouvrir cet évènement. Appuyez sur Espace pour le saisir, puis sur les flèches pour le déplacer et sur Maj avec les flèches pour changer son heure de fin.
<script setup lang="ts">
import { ref } from 'vue'
import { VCalendar, type CalendarEvent } from 'vectis-ui'

const DAY = '2026-06-10'

/* Four events crowd the morning and one stands alone after it, which is what shows the
   clustering: the lunch keeps the full width the morning has to share. */
const events = ref<CalendarEvent[]>([
  {
    id: 'standup',
    title: 'Standup',
    start: DAY,
    end: DAY,
    startTime: '09:00',
    endTime: '10:00',
  },
  {
    id: 'review',
    title: 'Design review',
    start: DAY,
    end: DAY,
    startTime: '09:30',
    endTime: '11:00',
  },
  {
    id: 'interview',
    title: 'Interview',
    start: DAY,
    end: DAY,
    startTime: '10:00',
    endTime: '10:30',
  },
  {
    id: 'workshop',
    title: 'Workshop',
    start: DAY,
    end: DAY,
    startTime: '09:00',
    endTime: '09:30',
  },
  {
    id: 'lunch',
    title: 'Team lunch',
    start: DAY,
    end: DAY,
    startTime: '12:30',
    endTime: '13:30',
  },
])
</script>

<template>
  <VCalendar
    v-model:events="events"
    view="day"
    :views="['day', 'week']"
    :day-start="8"
    :day-end="18"
    :date="DAY"
    label="Overlapping events"
    class="calendar"
  />
</template>

<style scoped>
.calendar {
  block-size: 34rem;
}
</style>

Couleurs

Un événement sans color prend une teinte dérivée de son identifiant. Celui qui nomme sa propre couleur l'emploie pour son bord d'attaque et un voile sur sa face.

vue

mercredi 10 juin 2026

mer.10
Appuyez sur Entrée pour ouvrir cet évènement. Appuyez sur Espace pour le saisir, puis sur les flèches pour le déplacer et sur Maj avec les flèches pour changer son heure de fin.
<script setup lang="ts">
import { ref } from 'vue'
import { VCalendar, type CalendarEvent } from 'vectis-ui'

const DAY = '2026-06-10'

/* The first six name no colour, so each takes the hue its id derives. The last two name
   one of their own, in two of the notations any CSS colour may be written in. */
const events = ref<CalendarEvent[]>([
  { id: 'hue-1', title: 'Standup', start: DAY, end: DAY, startTime: '08:00', endTime: '09:00' },
  {
    id: 'hue-2',
    title: 'Design review',
    start: DAY,
    end: DAY,
    startTime: '09:00',
    endTime: '10:00',
  },
  { id: 'hue-3', title: 'Workshop', start: DAY, end: DAY, startTime: '10:00', endTime: '11:00' },
  {
    id: 'hue-4',
    title: 'Retrospective',
    start: DAY,
    end: DAY,
    startTime: '11:00',
    endTime: '12:00',
  },
  { id: 'hue-5', title: 'Team lunch', start: DAY, end: DAY, startTime: '12:00', endTime: '13:00' },
  { id: 'hue-6', title: 'Interview', start: DAY, end: DAY, startTime: '13:00', endTime: '14:00' },
  {
    id: 'own-1',
    title: 'One to one',
    start: DAY,
    end: DAY,
    startTime: '14:00',
    endTime: '15:00',
    color: '#7c3aed',
  },
  {
    id: 'own-2',
    title: 'Release window',
    start: DAY,
    end: DAY,
    startTime: '16:00',
    endTime: '18:00',
    color: 'oklch(0.65 0.15 200)',
  },
])
</script>

<template>
  <VCalendar
    v-model:events="events"
    view="day"
    :views="['day', 'week']"
    :day-start="8"
    :day-end="20"
    :date="DAY"
    label="Event colours"
    class="calendar"
  />
</template>

<style scoped>
.calendar {
  block-size: 34rem;
}
</style>

Contenu d'événement personnalisé

Le slot #event remplace le contenu d'une carte et reçoit l'événement, son timeText formaté, le layout dans lequel il est dessiné, s'il se poursuit avant ou après la journée, et s'il est en train d'être glissé ou saisi au clavier.

vue

mercredi 10 juin 2026

mer.10
Appuyez sur Entrée pour ouvrir cet évènement. Appuyez sur Espace pour le saisir, puis sur les flèches pour le déplacer et sur Maj avec les flèches pour changer son heure de fin.
<script setup lang="ts">
import { ref } from 'vue'
import { VCalendar, VIcon, type CalendarEvent } from 'vectis-ui'
import { schedule as scheduleIcon } from 'vectis-ui/icons'

/* An interface of your own extending the contract: the extra fields reach the slot typed,
   rather than as something to cast there. */
interface Booking extends CalendarEvent {
  room: string
  attendees: number
}

const DAY = '2026-06-10'

const events = ref<Booking[]>([
  {
    id: 'review',
    title: 'Design review',
    start: DAY,
    end: DAY,
    startTime: '10:00',
    endTime: '11:30',
    room: 'B2',
    attendees: 6,
  },
  {
    id: 'workshop',
    title: 'Workshop',
    start: DAY,
    end: DAY,
    startTime: '14:00',
    endTime: '16:00',
    room: 'A1',
    attendees: 12,
  },
])
</script>

<template>
  <VCalendar
    v-model:events="events"
    view="day"
    :views="['day', 'week']"
    :day-start="8"
    :day-end="18"
    :date="DAY"
    label="Custom event content"
    class="calendar"
  >
    <template #event="{ event, timeText }">
      <span class="title">{{ event.title }}</span>
      <span class="line">
        <VIcon :name="scheduleIcon" :size="14" aria-hidden="true" />
        {{ timeText }}
      </span>
      <span class="line">Room {{ event.room }}, {{ event.attendees }} people</span>
    </template>
  </VCalendar>
</template>

<style scoped>
.calendar {
  block-size: 34rem;
}

.title {
  font-weight: var(--vectis-font-weight-semibold);
}

.line {
  display: flex;
  align-items: center;
  gap: var(--vectis-space-1);
  font-size: var(--vectis-text-caption-size);
}
</style>

Créer et éditer des événements

Le calendrier signale ce que le lecteur a fait et vous laisse la liste : créer et éditer se résument à trois événements et une boîte de dialogue. Tracer un créneau avec creatable déclenche event-create avec ses deux bornes, cliquer une case vide déclenche cell-activate avec un début seulement, et un bouton placé dans le slot #actions ouvre un formulaire vierge. Cliquer une carte déclenche event-activate, qui ouvre le même formulaire prérempli. Enregistrer écrit un nouveau tableau dans v-model:events, le modèle où le glisser et l'étirement écrivent déjà.

vue

8–14 juin 2026

lun.8
mar.9
mer.10
jeu.11
ven.12
sam.13
dim.14
Appuyez sur Entrée pour ouvrir cet évènement. Appuyez sur Espace pour le saisir, puis sur les flèches pour le déplacer et sur Maj avec les flèches pour changer son heure de fin.
<script setup lang="ts">
import { computed, reactive, ref, useId } from 'vue'
import {
  VButton,
  VCalendar,
  VDateInput,
  VDialog,
  VInput,
  VSwitch,
  VTextarea,
  VTimeInput,
  type CalendarCell,
  type CalendarEvent,
  type CalendarEventId,
  type CalendarEventTimes,
  type CalendarView,
} from 'vectis-ui'

const view = ref<CalendarView>('week')
const anchor = ref('2026-06-10')

const events = ref<CalendarEvent[]>([
  {
    id: 'standup',
    title: 'Standup',
    start: '2026-06-08',
    end: '2026-06-08',
    startTime: '09:00',
    endTime: '09:15',
  },
  {
    id: 'review',
    title: 'Design review',
    start: '2026-06-10',
    end: '2026-06-10',
    startTime: '14:00',
    endTime: '15:30',
    description: 'With Anna and Ravi',
  },
])

let nextId = 1

/** What the form edits: a copy, so Cancel leaves the calendar as it was. */
interface Draft {
  title: string
  allDay: boolean
  start: string | null
  startTime: string | null
  end: string | null
  endTime: string | null
  description: string
}

const open = ref(false)
const formId = useId()
/** The event being edited, or null while a new one is being written. */
const editingId = ref<CalendarEventId | null>(null)
const draft = reactive<Draft>(emptyDraft())

function emptyDraft(): Draft {
  return {
    title: '',
    allDay: false,
    start: null,
    startTime: null,
    end: null,
    endTime: null,
    description: '',
  }
}

/** A local date and time moved by some minutes, crossing midnight when it has to. */
function addMinutes(date: string, time: string, minutes: number): { date: string; time: string } {
  const moved = new Date(`${date}T${time}:00`)
  moved.setMinutes(moved.getMinutes() + minutes)
  const two = (part: number) => String(part).padStart(2, '0')
  return {
    date: `${moved.getFullYear()}-${two(moved.getMonth() + 1)}-${two(moved.getDate())}`,
    time: `${two(moved.getHours())}:${two(moved.getMinutes())}`,
  }
}

function openNew(times: CalendarEventTimes) {
  Object.assign(draft, emptyDraft(), times)
  editingId.value = null
  open.value = true
}

/* A drawn-out slot already has both ends. */
function onEventCreate(times: CalendarEventTimes) {
  openNew(times)
}

/* A clicked cell only has a start, so the event is given an hour. A day of the month view
   reports the hour the grid starts at, midnight here, which is a poor default for a meeting. */
function onCellActivate(cell: CalendarCell) {
  const startTime = view.value === 'month' ? '09:00' : cell.time
  const end = addMinutes(cell.date, startTime, 60)
  openNew({ start: cell.date, startTime, end: end.date, endTime: end.time })
}

/* The toolbar button has neither, and starts on the day the calendar is showing. */
function onNewClick() {
  openNew({ start: anchor.value, startTime: '09:00', end: anchor.value, endTime: '10:00' })
}

function onEventActivate(event: CalendarEvent) {
  Object.assign(draft, {
    title: event.title,
    allDay: event.allDay ?? false,
    start: event.start,
    startTime: event.startTime,
    end: event.end,
    endTime: event.endTime,
    description: event.description ?? '',
  })
  editingId.value = event.id
  open.value = true
}

/* ISO dates and 24-hour times compare correctly as strings, so no Date is needed here. */
const endsBeforeStart = computed(() => {
  if (!draft.start || !draft.end) return false
  if (draft.allDay) return draft.end < draft.start
  if (!draft.startTime || !draft.endTime) return false
  return `${draft.end} ${draft.endTime}` <= `${draft.start} ${draft.startTime}`
})

function save() {
  // `required` has the browser refuse empty fields before this runs. What it cannot know is
  // whether the end comes after the start, or that a title made of spaces is no title.
  const title = draft.title.trim()
  // The contract always wants times: an all-day event spans the whole of its days.
  const startTime = draft.allDay ? '00:00' : draft.startTime
  const endTime = draft.allDay ? '23:59' : draft.endTime
  if (!title || !draft.start || !draft.end || !startTime || !endTime) return
  if (endsBeforeStart.value) return

  const saved: CalendarEvent = {
    id: editingId.value ?? `event-${nextId++}`,
    title,
    start: draft.start,
    end: draft.end,
    startTime,
    endTime,
    allDay: draft.allDay || undefined,
    description: draft.description.trim() || undefined,
  }

  // A new array rather than a push: the calendar is handed a list and never mutates it.
  events.value =
    editingId.value === null
      ? [...events.value, saved]
      : events.value.map((event) => (event.id === saved.id ? saved : event))
  open.value = false
}

function remove() {
  events.value = events.value.filter((event) => event.id !== editingId.value)
  open.value = false
}
</script>

<template>
  <VCalendar
    v-model:view="view"
    v-model:date="anchor"
    v-model:events="events"
    creatable
    :views="['day', 'week', 'month']"
    :day-start="7"
    :day-end="20"
    label="Editable schedule"
    class="calendar"
    @event-create="onEventCreate"
    @cell-activate="onCellActivate"
    @event-activate="onEventActivate"
  >
    <template #actions>
      <VButton size="sm" @click="onNewClick">New event</VButton>
    </template>
  </VCalendar>

  <!-- Closing the dialog hands the focus back to whatever opened it: the card, the cell or
       the button. The form lives in the body and its buttons in the footer, so the submit
       button reaches it through the `form` attribute. -->
  <VDialog
    v-model:open="open"
    :title="editingId === null ? 'New event' : 'Edit event'"
    width="32rem"
  >
    <form :id="formId" class="form" @submit.prevent="save">
      <VInput v-model="draft.title" label="Title" required />

      <VSwitch v-model="draft.allDay" label="All day" class="all-day" />

      <div class="row">
        <VDateInput v-model="draft.start" label="Start date" show-picker required />
        <VTimeInput
          v-if="!draft.allDay"
          v-model="draft.startTime"
          label="Start time"
          :minute-step="15"
          required
        />
      </div>

      <div class="row">
        <VDateInput
          v-model="draft.end"
          label="End date"
          show-picker
          required
          :invalid="endsBeforeStart"
          :hint="endsBeforeStart ? 'The event has to end after it starts.' : undefined"
        />
        <VTimeInput
          v-if="!draft.allDay"
          v-model="draft.endTime"
          label="End time"
          :minute-step="15"
          required
          :invalid="endsBeforeStart"
        />
      </div>

      <VTextarea v-model="draft.description" label="Description" :rows="3" />
    </form>

    <template #footer>
      <VButton
        v-if="editingId !== null"
        class="delete"
        variant="ghost"
        tone="danger"
        @click="remove"
      >
        Delete
      </VButton>
      <VButton variant="ghost" tone="neutral" @click="open = false">Cancel</VButton>
      <VButton type="submit" :form="formId">Save</VButton>
    </template>
  </VDialog>
</template>

<style scoped>
.calendar {
  block-size: 34rem;
}

.form {
  display: grid;
  gap: var(--vectis-space-4);
}

/* A grid item stretches by default, which would push the label away from its switch. */
.all-day {
  justify-self: start;
}

.row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: var(--vectis-space-3);
  align-items: start;
}

/* Pushed to the other end of the footer, away from the button that saves. */
.delete {
  margin-inline-end: auto;
}
</style>

API

Props

PropTypeDéfaut
viewsCalendarView[]['day', '4days', 'week']
Quelles vues le menu propose, dans l'ordre où il les liste. Le restreindre est la façon dont un calendrier qui ne montre que des semaines cesse d'en proposer d'autres.
customDaysnumber4
Combien de jours la vue personnalisée montre, et de combien Précédent et Suivant y avancent.
weekdaysnumber[]aucune
Quels jours de la semaine sont montrés, en nombres à partir de 0 pour dimanche. L'ORDRE compte aussi : la première entrée est le jour où commence une semaine, et elle l'emporte sur firstDayOfWeek. Omis, les sept jours à partir de firstDayOfWeek.
firstDayOfWeeknumberaucune
Le jour où commence une semaine, à partir de 0 pour dimanche, quand weekdays n'est pas donné. Omis, la locale décide.
localestringaucune
La langue dans laquelle les jours, les mois et les heures sont écrits. Elle retombe sur la locale globale.
formatCalendarFormat'12h' | '24h'aucune
Si les heures sont montrées sur une horloge de 12 ou de 24 heures. Suit la locale.
dayStartnumber0
L'heure à laquelle la grille commence, à partir de 0.
dayEndnumber24
L'heure à laquelle elle se termine, jusqu'à 24.
slotDurationnumber15
Le pas sur lequel tout s'aligne, en minutes : de combien un coup de pouce déplace un événement, et l'unité dans laquelle un créneau se trace.
scrollTimestring'08:00'
Où la grille est défilée à sa première apparition, pour que la journée de travail soit en vue.
hideCurrentTimebooleanfalse
Retire la ligne tracée en travers de la colonne du jour à l'heure qu'il est, et le point sur son bord avant. Laissée en place, elle avance d'une minute par minute tant que le calendrier est à l'écran.
monthEventLimitnumber3
Combien d'événements un jour de la vue mois montre avant de compter le reste.
readonlybooleanfalse
Empêche de déplacer et d'étirer les événements, à la souris comme au clavier. Ils restent lisibles et cliquables, et rien de plus.
disabledbooleanfalse
Fige tout le calendrier : plus rien ne se déplace, ne se crée ni ne s'ouvre, et aucune autre période n'est atteignable. Les cartes sortent du parcours de tabulation, la grille garde le sien, si bien que l'agenda reste lisible. C'est ce qui le distingue de readonly, qui n'arrête que l'édition.
creatablebooleanfalse
Permet de tracer au pointeur une plage vide d'une grille horaire, vers le haut ou vers le bas depuis le créneau pressé. Au relâchement, ses horaires sont transmis par event-create et rien n'est ajouté à events : mettre l'événement au calendrier vous revient. Un clic, ou Entrée sur une cellule, transmet cell-activate avec ou sans cette prop.
edgeStepDelaynumber800
Combien de temps un événement déplacé doit reposer contre le bord du calendrier avant que la vue passe à la période précédente ou suivante, en millisecondes. Zéro le désactive. L'attente est tout l'intérêt : tourner la page à l'instant où le pointeur touche le bord rendrait le dernier jour d'une semaine impossible à viser.
noEdgeScrollbooleanfalse
Empêche un glissement près du haut ou du bas d'une grille horaire de la faire défiler.
labelstringaucune
Le nom du calendrier, pour qui ne peut pas le voir.
v-model:viewCalendarView'day' | '4days' | 'week' | 'month' | 'year' | 'custom''week'
Quelle étendue le calendrier affiche. Il ouvre sur la semaine.
v-model:datestringtoday
Le jour sur lequel la vue est ancrée, en chaîne ISO. Il ouvre sur aujourd'hui.
v-model:eventsE[][]
Ce qui est au calendrier. C'est un modèle plutôt qu'une simple prop parce que déplacer et redimensionner y réécrivent : le calendrier réorganise ce qu'on lui donne et rend la nouvelle liste, sans jamais muter celle qu'il a reçue.

Événements

ÉvénementType
event-activate[event: E]
Une carte a été cliquée ou activée, le signal pour ouvrir un éditeur à vous.
cell-activate[cell: CalendarCell]
Une partie vide de la grille a été activée, à ce jour et à cette heure. Un jour de la vue mois n'a pas d'heure à lui, et donne celle à laquelle commencent les grilles horaires.
event-move[event: E, previous: CalendarEventTimes]
Un événement a été déplacé ou poussé ailleurs. Il porte l'événement tel qu'il est désormais et d'où il vient, si bien qu'annuler ne demande aucune copie de votre part.
event-resize[event: E, previous: CalendarEventTimes]
La fin d'un événement a été déplacée ou poussée, en ces deux mêmes parties.
event-create[times: CalendarEventTimes]
Une plage vide d'une journée a été tracée, et voici ses horaires. Rien n'a été ajouté à la liste : c'est le signal pour créer l'événement, dans votre propre modèle ou par votre propre formulaire.

Slots

SlotType
actions{}
Des contrôles supplémentaires dans la barre d'outils, entre la plage et le menu des vues.
eventCalendarEventSlotProps<E>
Le contenu de la carte d'un événement, qui remplace le titre et les heures.
day-header{ iso: string; weekday: string; dayText: string; today: boolean; }
La tête d'une colonne de jour, qui remplace le jour de la semaine et le numéro.
all-day-label{}
Le libellé à côté de la bande des événements sur la journée entière.

Types

Les types que les tables ci-dessus nomment, écrits comme la librairie les déclare. Ceux qui portent export s'importent depuis vectis-ui pour typer votre propre code ; les autres décrivent la forme de ce qu'un slot fournit.

export interface CalendarCell {
  date: string
  time: string
}
export interface CalendarEvent {
  id: CalendarEventId
  title: string
  start: string
  end: string
  startTime: string
  endTime: string
  description?: string
  color?: string
  timezone?: string
  allDay?: boolean
}
export type CalendarEventId = string | number
export type CalendarEventLayout = 'block' | 'chip'
export interface CalendarEventSlotProps<E extends CalendarEvent = CalendarEvent> {
  event: E
  layout: CalendarEventLayout
  timeText: string
  continuesBefore: boolean
  continuesAfter: boolean
  dragging: boolean
  grabbed: boolean
}
export interface CalendarEventTimes {
  start: string
  end: string
  startTime: string
  endTime: string
}
export type CalendarView = 'day' | '4days' | 'week' | 'month' | 'year' | 'custom'

Variables CSS

TokenValeur
--vectis-control-size-calendar-hour4rem
--vectis-control-size-calendar-gutter4.5rem
--vectis-control-size-calendar-tick0.375rem
--vectis-control-size-calendar-edge3rem
--vectis-control-size-calendar-day-min5rem
--vectis-control-size-calendar-handle0.5rem
--vectis-control-size-calendar-allday-lane1.5rem
--vectis-control-size-calendar-allday-max7rem
--vectis-control-size-calendar-now-dot0.625rem
--vectis-control-size-calendar-month-cell8.5rem
--vectis-control-size-calendar-year-cell1.5rem