Planification des rendez-vous

@dayflow-pro/appointment-schedule fait deux choses :

  1. il permet à un organisateur de définir quand on peut le réserver, directement dans la vue Semaine de DayFlow ;
  2. il transforme cette définition en créneaux sélectionnables pour un participant, sans qu'un CalendarApp soit nécessaire.

Ce n'est pas une plateforme de réservation. Il n'y a ni backend, ni cycle de vie de réservation, ni service de notification, ni système de paiement. Sélectionner un créneau déclenche un callback, et votre application prend le relais.

Installation

npm install @dayflow-pro/appointment-schedule
pnpm add @dayflow-pro/appointment-schedule
yarn add @dayflow-pro/appointment-schedule
bun add @dayflow-pro/appointment-schedule

Consultez le guide d'installation de Pro pour les étapes d'installation.

temporal-polyfill est obligatoire. @dayflow/core n'est requis que par le plugin organisateur et la mise en page en superposition hebdomadaire. React, Vue, Svelte et Angular sont des dépendances peer facultatives : n'installez que le framework de l'adaptateur que vous importez.

import '@dayflow-pro/appointment-schedule/styles.css';
// Alternatively, for Tailwind projects that already load the core theme:
import '@dayflow-pro/appointment-schedule/styles.components.css';

Plugin organisateur

appointmentPlugin.ts
import { createAppointmentSchedulePlugin } from '@dayflow-pro/appointment-schedule/plugin';
import type { AppointmentSchedule } from '@dayflow-pro/appointment-schedule/engine';
import '@dayflow-pro/appointment-schedule/styles.css';

let schedules: AppointmentSchedule[] = [];

const upsertSchedule = (schedule: AppointmentSchedule) => {
  schedules = [
    ...schedules.filter(current => current.id !== schedule.id),
    schedule,
  ];
  appointmentPlugin.updateConfig?.({ schedules });
};

export const appointmentPlugin = createAppointmentSchedulePlugin({
  schedules,
  drawerPlacement: 'calendar',
  drawerWidth: 420,
  onCreateSchedule: upsertSchedule,
  onUpdateSchedule: upsertSchedule,
});

Ajoutez la même instance du plugin à une vue Semaine dans votre framework :

import { createWeekView } from '@dayflow/core';
import { DayFlowCalendar, useCalendarApp } from '@dayflow/react';
import { appointmentPlugin } from './appointmentPlugin';

export function App() {
  const calendar = useCalendarApp({
    views: [createWeekView()],
    plugins: [appointmentPlugin],
  });

  return (
    <>
      <button onClick={() => appointmentPlugin.api.openCreate()}>
        New appointment schedule
      </button>
      <DayFlowCalendar calendar={calendar} />
    </>
  );
}
<script setup lang="ts">
import { createWeekView } from '@dayflow/core';
import { DayFlowCalendar, useCalendarApp } from '@dayflow/vue';
import { appointmentPlugin } from './appointmentPlugin';

const calendar = useCalendarApp({
  views: [createWeekView()],
  plugins: [appointmentPlugin],
});
</script>

<template>
  <button @click="appointmentPlugin.api.openCreate()">
    New appointment schedule
  </button>
  <DayFlowCalendar :calendar="calendar" />
</template>
import { Component } from '@angular/core';
import { DayFlowCalendarModule } from '@dayflow/angular';
import { createWeekView } from '@dayflow/core';
import { appointmentPlugin } from './appointmentPlugin';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [DayFlowCalendarModule],
  template: `
    <button (click)="appointmentPlugin.api.openCreate()">
      New appointment schedule
    </button>
    <dayflow-calendar [calendar]="calendar"></dayflow-calendar>
  `,
})
export class AppComponent {
  readonly appointmentPlugin = appointmentPlugin;
  readonly calendar = {
    views: [createWeekView()],
    plugins: [appointmentPlugin],
  };
}
<script lang="ts">
  import { createWeekView } from '@dayflow/core';
  import { DayFlowCalendar, useCalendarApp } from '@dayflow/svelte';
  import { appointmentPlugin } from './appointmentPlugin';

  const calendar = useCalendarApp({
    views: [createWeekView()],
    plugins: [appointmentPlugin],
  });
</script>

<button onclick={() => appointmentPlugin.api.openCreate()}>
  New appointment schedule
</button>
<DayFlowCalendar {calendar} />

schedules est une donnée contrôlée : le plugin ne persiste jamais rien. La disponibilité est dessinée comme une couche de fond sous les événements, elle n'entre donc jamais dans getEvents(), la recherche, l'impression ou la synchronisation distante.

Modèle de planning

AppointmentSchedule est le contrat de données partagé par l'organisateur, les composants de réservation et le moteur sans interface.

PropriétéTypeRôle
idstringIdentifiant stable du planning.
titlestringNom affiché côté organisateur et côté réservation.
durationMinutesnumberDurée de chaque rendez-vous.
slotIntervalMinutes?numberÉcart entre les heures de début des créneaux. Par défaut, la durée.
beforeBufferMinutes?numberMarge occupée avant chaque réservation. Par défaut 0.
afterBufferMinutes?numberMarge occupée après chaque réservation. Par défaut 0.
timeZonestringFuseau IANA dans lequel la disponibilité est définie.
calendarId?stringCalendrier hôte, utilisé pour la couleur et les événements créés.
recurrence?AppointmentRecurrenceRègle hebdomadaire, ponctuelle ou toutes les N semaines.
availabilityWeeklyAvailability[]Plages réservables récurrentes, groupées par jour de la semaine. Obligatoire.
unavailableIntervals?WeeklyAvailability[]Pauses récurrentes qui restent visibles mais ne produisent jamais de créneau.
dateOverrides?DateAvailabilityOverride[]Plages par date qui remplacent la règle hebdomadaire.
location?AppointmentLocationConfigLien personnalisé, fournisseur de visioconférence, adresse ou téléphone.
meta?Record<string, unknown>Métadonnées sérialisables appartenant à l'application.

WeeklyAvailability contient dayOfWeek (0 dimanche à 6 samedi) et un tableau intervals. Chaque AvailabilityInterval possède id, startTime et endTime au format HH:mm en heure locale. Un DateAvailabilityOverride comporte une date ISO et des intervals de remplacement ; un tableau vide ferme cette date.

Ouvrez l'éditeur depuis votre propre interface :

appointmentPlugin.api.openCreate();
appointmentPlugin.api.openEdit('product-demo');

Le plugin ajoute également une entrée au popup de création rapide du calendrier. Les calendriers sans ce plugin conservent leur création rapide habituelle.

Options courantes

Ces champs constituent AppointmentSchedulePluginConfig.

OptionPar défautRôle
schedulesObligatoireListe contrôlée des plannings de rendez-vous.
activeScheduleIdAucunOuvre l'éditeur avec un planning donné déjà sélectionné.
availabilitySnapMinutes15Pas en minutes utilisé lors de l'édition des disponibilités.
drawerPlacement'viewport'Ancre l'éditeur à la fenêtre ou au calendrier.
drawerWidth420Largeur du panneau en pixels ou toute longueur CSS.
drawerTargetPremier calendrierÉlément ou sélecteur utilisé par l'ancrage calendar.
drawerRendererPanneau intégréRemplace entièrement l'éditeur de l'organisateur.
timeFormatFormat de la vue activeUtilise le format 12 h ou 24 h.
conferenceProviders[]Ajoute des fournisseurs de visioconférence au sélecteur de lieu.
onCreateScheduleAucunPersiste dans votre application un planning nouvellement créé.
onUpdateScheduleAucunPersiste les modifications d'un planning existant.
onDeleteScheduleRequestAucunDemande à l'application hôte de confirmer et de supprimer un planning.
onExternalUpdateConflictAucunSignale une mise à jour des données contrôlées reçue pendant l'édition.

API du plugin

Le plugin expose une AppointmentScheduleApi via appointmentPlugin.api.

MéthodeRôle
openCreate(initial?)Ouvre un nouveau brouillon, avec des champs initiaux facultatifs.
openEdit(scheduleId)Ouvre un planning contrôlé existant.
closeEditor() / cancelDraft()Abandonne le brouillon en cours et ferme le panneau.
saveDraft()Exécute le callback de création ou de mise à jour, puis ferme en cas de succès.
setActiveSchedule(scheduleId)Change le planning actif sans ouvrir l'éditeur.
getActiveSchedule()Renvoie le planning contrôlé actif.
getDraft()Renvoie le brouillon modifiable en cours, s'il y a édition.
draftManagerFournit les opérations d'édition des champs et des disponibilités.
subscribeDraft(listener)S'abonne aux mises à jour du brouillon et renvoie une fonction de désabonnement.

Personnaliser l'éditeur de l'organisateur

Utilisez drawerRenderer pour remplacer entièrement le panneau de l'organisateur. Le plugin continue de gérer l'ancrage, le brouillon actif, l'édition des disponibilités dans le calendrier ainsi que l'enregistrement et l'annulation. Votre application affiche un composant classique de votre framework dans l'hôte fourni.

La fonction de rendu reçoit AppointmentScheduleDrawerRenderArgs :

PropriétéRôle
draftBrouillon AppointmentSchedule en cours.
isCreatingDistingue un nouveau planning d'une édition.
draftManagerMet à jour les champs et fournit toggleDay, addInterval, updateInterval, removeInterval ainsi que des fonctions de copie.
calendarsCalendriers disponibles, sous la forme { id, name, color? }.
conferenceProvidersFournisseurs enregistrés, sous la forme { id, name, icon? }.
placementAncrage résolu : 'calendar' ou 'viewport'.
drawerWidthLargeur CSS résolue, sous forme de chaîne.
timeFormat / localePréférences d'affichage héritées de la configuration et du calendrier.
translateRecherche une traduction du paquet, avec valeur de repli.
save()Exécute le callback de création ou de mise à jour de l'hôte, puis ferme en cas de succès.
cancel()Abandonne le brouillon et ferme l'éditeur.

Ce callback est une frontière de montage pour votre framework, non une raison de construire le formulaire à coups de manipulations DOM manuelles. Les exemples suivants affichent les mêmes contrôles de titre, d'enregistrement et d'annulation sous forme de composants natifs :

organiserDrawer.tsx
import { createRoot } from 'react-dom/client';
import type {
  AppointmentScheduleDrawerRenderArgs,
  AppointmentScheduleDrawerRenderer,
} from '@dayflow-pro/appointment-schedule/plugin';

function OrganiserDrawer({
  args,
}: {
  args: AppointmentScheduleDrawerRenderArgs;
}) {
  return (
    <form onSubmit={event => { event.preventDefault(); void args.save(); }}>
      <input
        value={args.draft.title}
        onChange={event =>
          args.draftManager.updateDraft({ title: event.target.value })
        }
      />
      <button type="submit">Save</button>
      <button type="button" onClick={args.cancel}>Cancel</button>
    </form>
  );
}

export const drawerRenderer: AppointmentScheduleDrawerRenderer =
  (initial, host) => {
    const root = createRoot(host);
    const render = (args: AppointmentScheduleDrawerRenderArgs) =>
      root.render(<OrganiserDrawer args={args} />);

    render(initial);
    // Closing the drawer from a React effect — flipping `drawerRenderer`,
    // for instance — reaches `destroy` while React is still rendering, and
    // unmounting a root there races that render. Defer it by a microtask.
    return {
      update: render,
      destroy: () => queueMicrotask(() => root.unmount()),
    };
  };
OrganiserDrawer.vue
<script setup lang="ts">
import type { AppointmentScheduleDrawerRenderArgs } from '@dayflow-pro/appointment-schedule/plugin';

const props = defineProps<{ args: AppointmentScheduleDrawerRenderArgs }>();
const updateTitle = (event: Event) => {
  props.args.draftManager.updateDraft({
    title: (event.target as HTMLInputElement).value,
  });
};
</script>

<template>
  <form @submit.prevent="args.save()">
    <input :value="args.draft.title" @input="updateTitle" />
    <button type="submit">Save</button>
    <button type="button" @click="args.cancel()">Cancel</button>
  </form>
</template>
organiserDrawerRenderer.ts
import { createApp, h, reactive } from 'vue';
import type {
  AppointmentScheduleDrawerRenderArgs,
  AppointmentScheduleDrawerRenderer,
} from '@dayflow-pro/appointment-schedule/plugin';
import OrganiserDrawer from './OrganiserDrawer.vue';

export const drawerRenderer: AppointmentScheduleDrawerRenderer =
  (initial, host) => {
    const state = reactive({ args: initial });
    const app = createApp({
      render: () => h(OrganiserDrawer, { args: state.args }),
    });
    app.mount(host);

    return {
      update: (args: AppointmentScheduleDrawerRenderArgs) => {
        state.args = args;
      },
      destroy: () => app.unmount(),
    };
  };
organiser-drawer.ts
import {
  ApplicationRef,
  Component,
  EnvironmentInjector,
  Input,
  createComponent,
} from '@angular/core';
import type {
  AppointmentScheduleDrawerRenderArgs,
  AppointmentScheduleDrawerRenderer,
} from '@dayflow-pro/appointment-schedule/plugin';

@Component({
  selector: 'app-organiser-drawer',
  standalone: true,
  template: `
    <input [value]="args.draft.title" (input)="updateTitle($event)" />
    <button type="button" (click)="args.save()">Save</button>
    <button type="button" (click)="args.cancel()">Cancel</button>
  `,
})
export class OrganiserDrawerComponent {
  @Input({ required: true }) args!: AppointmentScheduleDrawerRenderArgs;

  updateTitle(event: Event) {
    this.args.draftManager.updateDraft({
      title: (event.target as HTMLInputElement).value,
    });
  }
}

export const createDrawerRenderer = (
  appRef: ApplicationRef,
  environmentInjector: EnvironmentInjector
): AppointmentScheduleDrawerRenderer => (initial, host) => {
  const component = createComponent(OrganiserDrawerComponent, {
    hostElement: host,
    environmentInjector,
  });
  appRef.attachView(component.hostView);
  const update = (args: AppointmentScheduleDrawerRenderArgs) => {
    component.setInput('args', args);
    component.changeDetectorRef.detectChanges();
  };
  update(initial);

  return {
    update,
    destroy: () => {
      appRef.detachView(component.hostView);
      component.destroy();
    },
  };
};
OrganiserDrawer.svelte
<script lang="ts">
  import type { Readable } from 'svelte/store';
  import type { AppointmentScheduleDrawerRenderArgs } from '@dayflow-pro/appointment-schedule/plugin';

  let { state }: { state: Readable<AppointmentScheduleDrawerRenderArgs> } = $props();
  const updateTitle = (event: Event) => {
    $state.draftManager.updateDraft({
      title: (event.target as HTMLInputElement).value,
    });
  };
</script>

<form onsubmit={(event) => { event.preventDefault(); void $state.save(); }}>
  <input value={$state.draft.title} oninput={updateTitle} />
  <button type="submit">Save</button>
  <button type="button" onclick={$state.cancel}>Cancel</button>
</form>
organiserDrawerRenderer.ts
import { mount, unmount } from 'svelte';
import { writable } from 'svelte/store';
import type { AppointmentScheduleDrawerRenderer } from '@dayflow-pro/appointment-schedule/plugin';
import OrganiserDrawer from './OrganiserDrawer.svelte';

export const drawerRenderer: AppointmentScheduleDrawerRenderer =
  (initial, host) => {
    const state = writable(initial);
    const component = mount(OrganiserDrawer, {
      target: host,
      props: { state },
    });

    return {
      update: next => state.set(next),
      destroy: () => void unmount(component),
    };
  };

Passez la fonction de rendu obtenue à createAppointmentSchedulePlugin({ drawerRenderer }). Les modifications du brouillon appellent update ; fermer le panneau ou remplacer la fonction de rendu appelle destroy. Les arguments de rendu fournissent aussi les métadonnées de calendrier et de visioconférence, la locale, timeFormat, translate, isCreating, save() et cancel().

Hôte du panneau, animation et superposition

Le plugin est propriétaire de l'élément hôte qu'il transmet à la fonction de rendu. Cet hôte porte la classe df-appointment-custom-drawer-host ainsi qu'un modificateur --calendar ou --viewport, et le plugin définit sa position, sa largeur et son empilement en style inline. Stylez-le à partir de ces classes ; ne le déplacez pas dans le DOM et ne changez pas sa position, car le plugin réapplique les deux à chaque rendu.

Les panneaux de remplacement apparaissent et disparaissent comme le panneau intégré. À la fermeture, le plugin marque l'hôte avec data-df-drawer-exiting, attend la fin de ses animations de keyframes, puis seulement appelle destroy et retire l'hôte — le panneau s'anime donc en sortie avec son contenu encore monté, au lieu de disparaître brutalement :

/* Defaults shipped by the package; override to change the motion. */
.df-appointment-custom-drawer-host {
  animation: df-slide-in-left 200ms cubic-bezier(0.16, 1, 0.3, 1);
}

.df-appointment-custom-drawer-host[data-df-drawer-exiting] {
  animation: df-slide-out-left 180ms cubic-bezier(0.7, 0, 0.84, 0) forwards;
}

Trois détails méritent d'être connus. Seules les animations de keyframes sont attendues : les transitions CSS qu'un framework attache aux survols et aux anneaux de focus ne retardent donc jamais le panneau. Les animations infinies sont ignorées, et un minuteur borné par la durée de l'animation elle-même sécurise l'attente : un spinner à l'intérieur du panneau — ou un onglet en arrière-plan, où les frames d'animation s'arrêtent — ne peut donc jamais laisser l'hôte bloqué dans le document. Enfin, si l'animation est retirée ou si prefers-reduced-motion: reduce s'applique, l'hôte est supprimé dans la frame même de la fermeture, ce que le paquet fait déjà pour les personnes qui demandent moins d'animations.

Le panneau se situe à z-index: 900, sous le popup de création rapide et les boîtes de dialogue du calendrier placés à 1000 : ouvrir le menu d'ajout n'est donc jamais masqué par un éditeur ouvert. Faire passer le panneau au-dessus de 1000 inverse ce rapport. Si le panneau doit recouvrir ces surfaces, remontez-les elles aussi plutôt que le seul panneau.

Récurrence

La disponibilité se répète chaque semaine par défaut. Le panneau propose également un planning ponctuel et une règle personnalisée « toutes les N semaines » :

type AppointmentRecurrence = {
  frequency: 'weekly' | 'none' | 'custom';
  startDate?: string; // YYYY-MM-DD anchor
  intervalWeeks?: number; // custom: every N weeks
  endsOnDate?: string;
  endsAfterOccurrences?: number;
};

frequency: 'none' applique les heures hebdomadaires choisies à la seule semaine du lundi au dimanche contenant la date d'ancrage ; il ne réduit pas la disponibilité à cette seule date.

Les règles plus riches — mensuelles, n-ième jour de la semaine ou RRULE — relèvent de l'application. Exprimez-les via dateOverrides, qui l'emportent toujours sur la règle de récurrence.

Lieu et visioconférence

Un planning peut indiquer se déroule le rendez-vous. Il s'agit d'une configuration, pas d'une réunion déjà réservée. Un même planning pouvant être réservé de nombreuses fois, il ne stocke jamais d'URL de connexion propre à une réservation :

type AppointmentLocationConfig =
  | { type: 'custom-link'; url: string; label?: string }
  | { type: 'conference'; providerId: string }
  | { type: 'in-person'; address: string }
  | { type: 'phone'; phone?: string };

Le sélecteur Lieu du panneau propose toujours « Lien de réunion personnalisé », « En personne » et « Appel téléphonique ». Les applications de visioconférence nommées n'apparaissent qu'une fois enregistrées :

createAppointmentSchedulePlugin({
  schedules,
  conferenceProviders: [googleMeet, zoom], // ← the picker lists these first
});

Fournisseurs de visioconférence

DayFlow ne dialogue jamais avec Google, Zoom ou Microsoft. Il définit une interface à une seule méthode et l'appelle ; le token OAuth, le secret d'API et le SDK du fournisseur restent dans votre backend :

import type { ConferenceProvider } from '@dayflow-pro/appointment-schedule/engine';

const googleMeet: ConferenceProvider = {
  id: 'google-meet',
  name: 'Google Meet',
  icon: '/icons/meet.svg',
  createConference: input =>
    fetch('/api/dayflow/google-meet', {
      method: 'POST',
      body: JSON.stringify({ ...input, start: input.start.toString() }),
    }).then(response => response.json()),
};
// → { provider, joinUrl, meetingId?, hostUrl?, password?, meta? }

createConference reçoit un CreateConferenceInput : scheduleId, title, start et end Temporal, timeZone, plus éventuellement host et attendees. Il renvoie une Conference avec provider et joinUrl ; meetingId, hostUrl, password et meta sont facultatifs.

Déclarer les mêmes fournisseurs sur le composant de réservation libelle la ligne de lieu et affiche l'icône du fournisseur :

<AppointmentBooking schedule={schedule} conferenceProviders={[googleMeet]} />

Créer la réunion

Créez la visioconférence lorsque le participant confirme, pas lorsqu'il met un créneau en évidence. Quelqu'un qui clique sur 10 h 00 puis s'en va ne devrait pas laisser une réunion derrière lui.

import {
  createBookingEvent,
  createConferenceForBooking,
} from '@dayflow-pro/appointment-schedule/engine';

const conference = await createConferenceForBooking({
  slot,
  schedule,
  providers: [googleMeet],
  attendees: [{ name: 'Ada Lovelace', email: 'ada@example.com' }],
});

const draft = createBookingEvent({ slot, schedule, conference });
// draft.location  → 'https://meet.google.com/abc-defg-hij'
// draft.conference → { provider: 'google-meet', joinUrl, meetingId }

Un planning custom-link se résout vers la salle permanente de l'organisateur sans appel réseau, et les plannings in-person / phone se résolvent vers undefined. Un providerId sans fournisseur enregistré lève une erreur, car une réservation ne devrait pas perdre silencieusement son lien de réunion.

Pourquoi ne pas toujours réutiliser le même lien ?

Pour Google Meet, Zoom et Teams, préférez conference à custom-link. Google recommande une nouvelle visioconférence par événement plutôt qu'une réunion réutilisée, car partager les données de visioconférence entre événements pose des problèmes d'accès et de confidentialité.

Pour afficher un lieu ailleurs, par exemple dans un e-mail ou une page de confirmation, utilisez le même résolveur pur que la ligne d'informations de la réunion :

import { resolveLocation } from '@dayflow-pro/appointment-schedule/engine';

resolveLocation(schedule.location, { providers: [googleMeet] });
// → { kind: 'video', label: 'Google Meet', icon: '/icons/meet.svg', config }

Le résolveur renvoie un ResolvedLocation lorsqu'un lieu est configuré.

Composants de réservation côté participant

import { AppointmentBooking } from '@dayflow-pro/appointment-schedule/booking';

<AppointmentBooking
  schedule={schedule}
  presentation={{ organiserName: 'Alex Morgan', locationLabel: 'Zoom meeting' }}
  busyIntervals={busyIntervals}
  layout='calendar-day-slots'
  onSelectSlot={slot => console.log(slot.start.toString())}
/>;

presentation contient un contenu facultatif, purement visuel, destiné au panneau d'informations de la réunion. La disponibilité et le comportement de réservation continuent de venir de schedule.

PropriétéTypeRôle
organiserNamestringAffiche le nom de l'organisateur et fournit l'initiale de repli en l'absence d'avatar.
organiserAvatarstringURL de l'image d'avatar de l'organisateur.
organiserUrlstringURL de profil ouverte depuis l'avatar de l'organisateur.
titlestringTitre affiché au-dessus de la durée, du lieu et du fuseau horaire. Passez schedule.title pour réutiliser son titre.
descriptionstringTexte complémentaire affiché sous les métadonnées de la réunion.
locationLabelstringRemplace le texte de lieu déduit de schedule.location. Préférez normalement le champ du planning.

Options de réservation

Tous les adaptateurs acceptent au final AppointmentBookingProps ; Vue, Angular et Svelte nomment le type de montage équivalent dans leurs propres points d'entrée.

Groupe de propriétésRôle
scheduleAppointmentSchedule obligatoire, utilisé pour générer les créneaux.
calendarAppLit les événements DayFlow — et s'y abonne — comme temps occupé de l'organisateur.
presentationContenu AppointmentPresentation facultatif décrit ci-dessus.
conferenceProvidersRésout les noms et icônes de fournisseur pour les lieux en visioconférence.
busyIntervals / attendeeBusyIntervalsPlages Temporal qui retirent des créneaux de l'organisateur ou superposent les conflits du participant.
layoutUn BookingLayout : calendar-day-slots, multi-day-slots ou week-overlay.
renderWeekOverlay / weekOverlayOptionsUtilisent WeekOverlayRenderArgs et WeekOverlayOptions pour activer et régler la vue semaine.
availableLayouts / onLayoutChangeContrôlent les mises en page proposées par le sélecteur intégré.
displayTimeZone / timeZoneOptions / onDisplayTimeZoneChangeContrôlent le fuseau affiché au participant et son sélecteur.
timeFormat / onTimeFormatChangeContrôlent la valeur de TimeFormat, 12h ou 24h.
theme / locale / startOfWeekDéfinissent le thème de la semaine, la locale et la valeur de StartOfWeek (0, 1 ou 6).
multiDayCount / skipEmptyDaysAjustent la mise en page multi-jours.
now / rangeStart / rangeEndRemplacent l'heure courante et la plage de génération des créneaux.
selectedDate / onSelectDateDate focalisée, en mode contrôlé.
selectedSlotId / onSelectSlotSélection de créneau contrôlée et callback de sélection.
loading / disabledAffichent l'état de chargement ou désactivent les interactions.
onError / onRetryIntègrent votre propre remontée d'erreurs et comportement de reprise.
className / styleAjoutent des styles à la racine.
labelsRemplacent tout sous-ensemble des textes visibles.
slotsRemplacent ou complètent les régions listées ci-dessous.
onAnalyticsEventReçoit un AppointmentBookingAnalyticsEvent et sa charge utile non personnelle.

Choisissez la mise en page selon la quantité de disponibilité à présenter :

Mise en pageIdéale pour
calendar-day-slotsUn sélecteur mensuel avec les heures du jour choisi.
multi-day-slotsComparer les heures disponibles de plusieurs jours en colonnes.
week-overlayAfficher les heures réservables sur une chronologie hebdomadaire. Nécessite la fonction de rendu ci-dessous.

La mise en page semaine est optionnelle, car c'est la seule à nécessiter @dayflow/core :

import { renderWeekOverlay } from '@dayflow-pro/appointment-schedule/booking/week-overlay';

<AppointmentBooking
  layout='week-overlay'
  renderWeekOverlay={renderWeekOverlay}
/>;

Adaptateurs de framework

Utilisez l'adaptateur du framework qui porte votre page de réservation. Tous acceptent les mêmes options et gèrent le montage, les mises à jour réactives et le nettoyage. Importer un adaptateur n'embarque pas les runtimes des autres frameworks dans votre bundle.

Créez un planning sérialisable, partageable par n'importe quel adaptateur :

schedule.ts
import type { AppointmentSchedule } from '@dayflow-pro/appointment-schedule/engine';

export const schedule: AppointmentSchedule = {
  id: 'product-demo',
  title: 'Product demo',
  durationMinutes: 30,
  slotIntervalMinutes: 30,
  timeZone: 'Europe/London',
  recurrence: { frequency: 'weekly' },
  availability: [
    {
      dayOfWeek: 1,
      intervals: [{ id: 'monday', startTime: '09:00', endTime: '17:00' }],
    },
    {
      dayOfWeek: 2,
      intervals: [{ id: 'tuesday', startTime: '09:00', endTime: '17:00' }],
    },
    {
      dayOfWeek: 3,
      intervals: [{ id: 'wednesday', startTime: '09:00', endTime: '17:00' }],
    },
    {
      dayOfWeek: 4,
      intervals: [{ id: 'thursday', startTime: '09:00', endTime: '17:00' }],
    },
    {
      dayOfWeek: 5,
      intervals: [{ id: 'friday', startTime: '09:00', endTime: '17:00' }],
    },
  ],
  location: {
    type: 'custom-link',
    url: 'https://zoom.us/j/1234567890',
    label: 'Zoom meeting',
  },
};

Importez styles.components.css une seule fois depuis le point d'entrée de votre application, puis utilisez l'adaptateur correspondant :

import { AppointmentBooking } from '@dayflow-pro/appointment-schedule/react';
import '@dayflow-pro/appointment-schedule/styles.components.css';
import { schedule } from './schedule';

export function BookingPage() {
  return (
    <AppointmentBooking
      schedule={schedule}
      presentation={{
        organiserName: 'Alex Morgan',
        locationLabel: 'Zoom meeting',
      }}
      layout='calendar-day-slots'
      onSelectSlot={slot => console.log(slot.start.toString())}
    />
  );
}
<script setup lang="ts">
import { AppointmentBooking } from '@dayflow-pro/appointment-schedule/vue';
import type { MountAppointmentBookingProps } from '@dayflow-pro/appointment-schedule/vue';
import '@dayflow-pro/appointment-schedule/styles.components.css';
import { schedule } from './schedule';

const bookingOptions: MountAppointmentBookingProps = {
  schedule,
  presentation: {
    organiserName: 'Alex Morgan',
    locationLabel: 'Zoom meeting',
  },
  layout: 'calendar-day-slots',
  onSelectSlot: slot => console.log(slot.start.toString()),
};
</script>

<template>
  <AppointmentBooking :options="bookingOptions" />
</template>
import { Component } from '@angular/core';
import {
  AppointmentBookingDirective,
  type MountAppointmentBookingProps,
} from '@dayflow-pro/appointment-schedule/angular';
import '@dayflow-pro/appointment-schedule/styles.components.css';
import { schedule } from './schedule';

@Component({
  standalone: true,
  imports: [AppointmentBookingDirective],
  template: '<div [dfAppointmentBooking]="bookingOptions"></div>',
})
export class BookingPage {
  readonly bookingOptions: MountAppointmentBookingProps = {
    schedule,
    presentation: {
      organiserName: 'Alex Morgan',
      locationLabel: 'Zoom meeting',
    },
    layout: 'calendar-day-slots',
    onSelectSlot: slot => console.log(slot.start.toString()),
  };
}
<script lang="ts">
  import { appointmentBooking } from '@dayflow-pro/appointment-schedule/svelte';
  import type { SvelteAppointmentBookingOptions } from '@dayflow-pro/appointment-schedule/svelte';
  import '@dayflow-pro/appointment-schedule/styles.components.css';
  import { schedule } from './schedule';

  const options: SvelteAppointmentBookingOptions = {
    schedule,
    presentation: {
      organiserName: 'Alex Morgan',
      locationLabel: 'Zoom meeting',
    },
    layout: 'calendar-day-slots',
    onSelectSlot: slot => console.log(slot.start.toString()),
  };
</script>

<div use:appointmentBooking={options}></div>

Les adaptateurs qui acceptent un objet options exposent les régions personnalisées via options.slots ; les composants à props exposent directement le même contrat slots. Remplacez l'objet d'options pour mettre à jour la réservation. Changer la collection de fonctions de rendu remonte proprement, et démonter l'hôte détruit l'instance de réservation.

API d'intégration

API de montage direct

Utilisez createAppointmentBooking lorsque vous voulez monter l'interface de réservation vous-même, depuis un autre framework ou une page sans framework :

import { createAppointmentBooking } from '@dayflow-pro/appointment-schedule/booking';

const booking = createAppointmentBooking('#booking', {
  schedule,
  onSelectSlot: slot => console.log(slot.start.toString()),
});

booking.update({ disabled: true });
booking.destroy();

Les enfants existants de la cible ne sont pas touchés. L'API de montage crée et gère un unique élément enfant, qu'elle retire lors de destroy().

Les régions DOM reçoivent leurs arguments courants et un conteneur appartenant à la fonction de rendu :

createAppointmentBooking('#booking', {
  schedule,
  slots: {
    slotButton: (args, host) => {
      host.textContent = `${args.formattedTime} · ${price(args.slot)}`;
    },
  },
});

Une fonction de rendu peut ne rien renvoyer, renvoyer une fonction de nettoyage, ou renvoyer { update, destroy }. Cette dernière forme est destinée aux API de montage de frameworks qui ajoutent au lieu de patcher.

Contrôleur de réservation sans interface

Utilisez createBookingController lorsque vous voulez le comportement de réservation fourni, mais avec votre propre balisage. Il gère la date et le créneau sélectionnés, le fuseau et le format horaires, la visibilité du temps occupé du participant et le regroupement des créneaux, mais ne touche jamais au DOM :

import { createBookingController } from '@dayflow-pro/appointment-schedule/controller';

const booking = createBookingController({ schedule });
const unsubscribe = booking.subscribe(() => render(booking.getState()));

booking.getState().selectSlot(slot);
booking.setOptions({ schedule, disabled: true });

unsubscribe();
booking.destroy();

Personnalisation

Tokens de design

.df-appointment-booking est la portée de thème du composant participant, pas son unique token. Le paquet prend en charge les propriétés personnalisées suivantes. Chargez vos surcharges après la feuille de style du paquet et définissez-les sur .df-appointment-booking, ou passez un className personnalisé et ciblez les deux classes.

.df-appointment-booking.my-booking-theme {
  --df-ap-accent: #2563eb;
  --df-ap-radius: 6px;
  --df-ap-sidebar-width: 320px;
}
TokenPar défautContrôle
--df-ap-surfaceFond du cœurSurfaces principales
--df-ap-surface-sunkenTeinte atténuée du cœurSurfaces en creux
--df-ap-fill#e8eaefRemplissages neutres
--df-ap-borderBordure du cœurBordures et séparateurs
--df-ap-fgPremier plan du cœurTexte principal
--df-ap-fg-mutedPremier plan atténué du cœurTexte secondaire
--df-ap-fg-subtle#9ca3afTexte discret
--df-ap-accentCouleur primaire du cœurContrôles sélectionnés
--df-ap-accent-fgPremier plan sur la primaireTexte sur surfaces accentuées
--df-ap-accent-softDérivé de l'accentFonds doux au survol
--df-ap-accent-ringDérivé de l'accentAnneaux de focus et de sélection
--df-ap-available#22c55eIndicateur de disponibilité
--df-ap-radius14pxRayon des cartes
--df-ap-radius-md9pxRayon des contrôles
--df-ap-radius-sm7pxRayon des éléments compacts
--df-ap-max-width1440pxLargeur maximale de la réservation
--df-ap-sidebar-width296pxLargeur de la barre latérale
--df-ap-slots-width320pxLargeur de la colonne de créneaux du jour
--df-ap-week-height620pxHauteur de la chronologie hebdomadaire
--df-ap-slot-scroll-heightnoneHauteur de la liste de créneaux du jour
--df-ap-pad1.5remEspacement interne principal
--df-ap-fontPile de polices systèmeTypographie de la réservation

Les couleurs de survol, de sélection et de focus sont dérivées de --df-ap-accent. Les thèmes avancés peuvent aussi surcharger directement --df-ap-accent-soft et --df-ap-accent-ring.

Le panneau de l'organisateur fait partie du calendrier DayFlow et utilise le thème du cœur, non les tokens du participant :

:root {
  --df-color-background: #ffffff;
  --df-color-card: #ffffff;
  --df-color-foreground: #172033;
  --df-color-muted: #f4f6f8;
  --df-color-muted-foreground: #667085;
  --df-color-border: #d0d5dd;
  --df-color-primary: #7c3aed;
  --df-color-primary-foreground: #ffffff;
  --df-color-destructive: #dc2626;
  --df-color-ring: #7c3aed;
}

Utilisez :root lorsque drawerPlacement vaut viewport, car le panneau est monté hors de l'élément calendrier. Avec l'ancrage calendar, les variables peuvent être posées sur le conteneur du calendrier.

Régions personnalisables

Tous les adaptateurs gèrent les régions, mais la signature de leurs fonctions de rendu diffère :

  • Les régions React renvoient du contenu React. slotButton et dayCell reçoivent en plus defaultContent, ce qui leur permet d'envelopper le contenu intégré.
  • Vue, Angular et Svelte utilisent options.slots. Chaque fonction de rendu reçoit (args, host) et écrit dans l'élément DOM fourni. Elle peut renvoyer une fonction de nettoyage ou un handle { update, destroy } lorsqu'elle monte un composant du framework.

Les exemples suivants personnalisent correctement la même région slotButton dans chaque framework :

<AppointmentBooking
  schedule={schedule}
  slots={{
    slotButton: ({ defaultContent }) => (
      <>
        {defaultContent}
        <span>$120</span>
      </>
    ),
  }}
/>
import type { DomBookingSlots } from '@dayflow-pro/appointment-schedule/vue';

const slots: DomBookingSlots = {
  slotButton: ({ formattedTime }, host) => {
    host.textContent = `${formattedTime} · $120`;
  },
};

const bookingOptions = { schedule, slots };
import type { DomBookingSlots } from '@dayflow-pro/appointment-schedule/angular';

const slots: DomBookingSlots = {
  slotButton: ({ formattedTime }, host) => {
    host.textContent = `${formattedTime} · $120`;
  },
};

export class BookingPage {
  readonly bookingOptions = { schedule, slots };
}
<script lang="ts">
  import type { DomBookingSlots } from '@dayflow-pro/appointment-schedule/svelte';

  const slots: DomBookingSlots = {
    slotButton: ({ formattedTime }, host) => {
      host.textContent = `${formattedTime} · $120`;
    },
  };

  const options = { schedule, slots };
</script>

<div use:appointmentBooking={options}></div>

AppointmentBookingSlots définit les régions ci-dessous. Les types d'arguments exportés incluent SidebarSlotArgs, MeetingInfoSlotArgs, MonthPickerSlotArgs, SlotListSlotArgs, ToolbarSlotArgs, SlotButtonSlotArgs, DayCellSlotArgs et SelectedSummarySlotArgs.

RégionRôleArguments importants
sidebarRemplace toute la colonne latérale, avec les détails de la réunion et le mini-calendrier.schedule, presentation, location, layout, defaultContent
meetingInfoRemplace le panneau d'informations de la réunion.schedule, presentation, location, timeZone, defaultContent
meetingInfoFooterAjoute du contenu sous le panneau d'informations de la réunion.schedule, presentation, location, timeZone
monthPickerRemplace le sélecteur de mois dans la zone principale et la barre latérale.selectedDate, availableDates, onSelectDate, compact, defaultContent
slotListRemplace la liste des heures du jour choisi dans calendar-day-slots.date, slots, selectedSlotId, onSelectSlot, defaultContent
toolbarRemplace la barre d'outils dans multi-day-slots et week-overlay.layout, timeZone, timeFormat, rangeLabel, defaultContent
toolbarExtraAjoute du contenu à droite des contrôles de la barre d'outils.layout, timeZone, timeFormat, rangeLabel
slotButtonRemplace le contenu de chaque bouton d'heure réservable.slot, formattedTime, isSelected, disabled, defaultContent
dayCellRemplace le contenu de chaque cellule de jour du sélecteur de mois.date, mois, état de disponibilité et de sélection, defaultContent
emptyDayS'affiche lorsque le jour choisi n'a aucune heure disponible.date
emptyRangeS'affiche lorsque la plage de dates courante n'a aucune heure réservable.Aucun
loadingRemplace l'état de chargement.Aucun
errorRemplace l'état d'erreur et reçoit l'erreur ainsi qu'une action de reprise facultative.error, retry
selectedSummaryAjoute un récapitulatif sous le contenu une fois un créneau sélectionné.slot, formattedDate, formattedRange, timeZone

Sans interface imposée : créez votre propre UI

Si aucune mise en page ne vous convient, laissez complètement de côté les composants. Le moteur est une fonction pure qui reçoit un planning et renvoie des créneaux. Rien d'autre du paquet n'atteint votre bundle.

import { generateSlots } from '@dayflow-pro/appointment-schedule/engine';
import { Temporal } from 'temporal-polyfill';

const slots = generateSlots({
  schedule,
  rangeStart: Temporal.PlainDate.from('2026-08-03'),
  rangeEnd: Temporal.PlainDate.from('2026-08-09'),
  busyIntervals,
  displayTimeZone: 'Europe/London',
  now: Temporal.Now.zonedDateTimeISO('Australia/Sydney'),
});
// slots: { id, scheduleId, start, end, displayTimeZone }[]

L'entrée est un SlotQuery ; rangeStart et rangeEnd sont des Temporal.PlainDate inclusifs. busyIntervals et attendeeBusyIntervals contiennent des BusyInterval avec start et end Temporal. Le résultat est un AppointmentSlot[].

Le moteur ne touche jamais à window, document ni au fuseau horaire système : il peut donc être importé côté serveur sans risque. Il exporte aussi ses briques : expandAvailability, recurrenceAppliesOn, sortBusyIntervals, mergeBusyIntervals, hasConflict et eventsToBusyIntervals. Vous pouvez les assembler pour composer votre propre pipeline.

Transformer une réservation en événement

Le module n'écrit jamais d'événement. Il vous donne à la place une fonction de mapping pure :

import { createBookingEvent } from '@dayflow-pro/appointment-schedule/engine';

const draft = createBookingEvent({
  slot,
  schedule, // schedule.calendarId decides which calendar (and colour)
  attendee: { name: 'Ada Lovelace' }, // you collect it, the module never stores it
  conference, // optional; see Location and conferencing
  // titleTemplate: ({ attendee }) => `1:1 · ${attendee?.name}`,
});
// → { id, title: 'Meeting with Ada Lovelace', start, end, calendarId, location?, conference?, meta }

calendar.addEvent(draft);

meta transporte appointmentScheduleId et appointmentSlotId, ce qui permet de remonter d'un événement au créneau dont il provient, ainsi que appointmentLocation et appointmentConference lorsque le planning a un lieu.

Accessibilité

Vise le niveau WCAG 2.2 AA. Le sélecteur de mois est un véritable role="grid", avec navigation par flèches, Origine, Fin, Page précédente et Page suivante ; les boutons de créneau exposent aria-pressed et un nom accessible contenant la date, l'heure et le fuseau horaire ; enfin, les états disponible, indisponible et sélectionné ne sont jamais signalés par la seule couleur.

Dans cette page