Programación de citas

@dayflow-pro/appointment-schedule hace dos cosas:

  1. permite al organizador definir cuándo se le puede reservar, editándolo directamente en la vista de semana de DayFlow;
  2. convierte esa definición en franjas seleccionables para el asistente, sin necesidad de un CalendarApp.

No es una plataforma de reservas. No hay backend, ni ciclo de vida de la reserva, ni servicio de notificaciones, ni pasarela de pago. Al seleccionar una franja se dispara un callback y tu aplicación se encarga del resto.

Instalación

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

Consulta la guía de instalación de Pro para conocer los pasos de instalación.

temporal-polyfill es obligatorio. @dayflow/core solo hace falta para el plugin del organizador y el diseño de superposición semanal. React, Vue, Svelte y Angular son dependencias peer opcionales: instala únicamente el framework del adaptador que vayas a importar.

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 del organizador

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,
});

Añade la misma instancia del plugin a una vista de semana en tu 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 son datos controlados: el plugin nunca persiste nada. La disponibilidad se dibuja como una capa de fondo bajo los eventos, así que nunca entra en getEvents(), ni en la búsqueda, ni en la impresión, ni en la sincronización remota.

Modelo de programación

AppointmentSchedule es el contrato de datos compartido entre el organizador, los componentes de reserva y el motor headless.

PropiedadTipoPara qué sirve
idstringIdentificador estable de la agenda.
titlestringNombre que se muestra en las pantallas del organizador y de reserva.
durationMinutesnumberDuración de cada cita.
slotIntervalMinutes?numberSeparación entre horas de inicio. Por defecto, la duración.
beforeBufferMinutes?numberMargen ocupado antes de cada reserva. Por defecto, 0.
afterBufferMinutes?numberMargen ocupado después de cada reserva. Por defecto, 0.
timeZonestringZona IANA en la que se define la disponibilidad.
calendarId?stringCalendario anfitrión, usado para el color y los eventos creados.
recurrence?AppointmentRecurrenceRegla semanal, puntual o cada N semanas.
availabilityWeeklyAvailability[]Rangos reservables recurrentes agrupados por día de la semana. Obligatorio.
unavailableIntervals?WeeklyAvailability[]Pausas recurrentes que siguen siendo visibles pero nunca generan franjas.
dateOverrides?DateAvailabilityOverride[]Rangos por fecha que sustituyen la regla semanal.
location?AppointmentLocationConfigEnlace propio, proveedor de videollamada, dirección o teléfono.
meta?Record<string, unknown>Metadatos serializables que pertenecen a tu aplicación.

WeeklyAvailability contiene dayOfWeek (0 domingo a 6 sábado) y un array intervals. Cada AvailabilityInterval tiene id, startTime y endTime en formato HH:mm de reloj de pared. Un DateAvailabilityOverride tiene una date ISO y unos intervals de sustitución; un array vacío cierra esa fecha.

Abre el editor desde tu propia interfaz:

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

El plugin también añade una entrada al popup de creación rápida del calendario. Los calendarios sin este plugin conservan su creación rápida habitual.

Opciones habituales

Estos campos forman AppointmentSchedulePluginConfig.

OpciónValor por defectoPara qué sirve
schedulesObligatorioLista controlada de agendas de citas.
activeScheduleIdNingunoAbre el editor con una agenda concreta seleccionada.
availabilitySnapMinutes15Intervalo en minutos al editar la disponibilidad.
drawerPlacement'viewport'Ancla el editor a la ventana o al calendario.
drawerWidth420Anchura del panel en píxeles o cualquier longitud CSS.
drawerTargetPrimer calendarioElemento o selector usado por el anclaje calendar.
drawerRendererPanel integradoSustituye por completo el editor del organizador.
timeFormatFormato de la vista activaUsa formato de 12 o de 24 horas.
conferenceProviders[]Añade proveedores de videollamada al selector de ubicación.
onCreateScheduleNingunoPersiste en tu aplicación una agenda recién creada.
onUpdateScheduleNingunoPersiste los cambios de una agenda existente.
onDeleteScheduleRequestNingunoPide a la aplicación anfitriona que confirme y elimine una agenda.
onExternalUpdateConflictNingunoInforma de una actualización de datos controlados recibida durante la edición.

API del plugin

El plugin expone una AppointmentScheduleApi en appointmentPlugin.api.

MétodoPara qué sirve
openCreate(initial?)Abre un borrador nuevo con campos iniciales opcionales.
openEdit(scheduleId)Abre una agenda controlada existente.
closeEditor() / cancelDraft()Descarta el borrador actual y cierra el panel.
saveDraft()Ejecuta el callback de creación o actualización y cierra si tiene éxito.
setActiveSchedule(scheduleId)Cambia la agenda activa sin abrir el editor.
getActiveSchedule()Devuelve la agenda controlada activa.
getDraft()Devuelve el borrador mutable actual, si se está editando.
draftManagerOfrece operaciones de edición de campos y de disponibilidad.
subscribeDraft(listener)Se suscribe a los cambios del borrador y devuelve una función para cancelar la suscripción.

Personalizar el editor del organizador

Usa drawerRenderer para sustituir por completo el panel del organizador. El plugin sigue encargándose del anclaje, del borrador activo, de la edición de disponibilidad en el calendario y del comportamiento de guardar y cancelar. Tu aplicación renderiza un componente normal de tu framework dentro del contenedor que se le proporciona.

El renderizador recibe AppointmentScheduleDrawerRenderArgs:

PropiedadPara qué sirve
draftBorrador AppointmentSchedule actual.
isCreatingDistingue una agenda nueva de una edición.
draftManagerActualiza campos y aporta toggleDay, addInterval, updateInterval, removeInterval y funciones de copia.
calendarsCalendarios disponibles con la forma { id, name, color? }.
conferenceProvidersProveedores registrados con la forma { id, name, icon? }.
placementAnclaje resuelto: 'calendar' o 'viewport'.
drawerWidthAnchura CSS ya resuelta, como cadena.
timeFormat / localePreferencias de visualización heredadas de la configuración y del calendario.
translateBusca una traducción del paquete, con valor de reserva.
save()Ejecuta el callback de creación o actualización de la aplicación y cierra si tiene éxito.
cancel()Descarta el borrador y cierra el editor.

El callback es una frontera de montaje para tu framework, no una razón para construir el formulario manipulando el DOM a mano. Los ejemplos siguientes renderizan los mismos controles de título, guardar y cancelar como componentes nativos:

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),
    };
  };

Pasa el renderizador resultante a createAppointmentSchedulePlugin({ drawerRenderer }). Los cambios en el borrador llaman a update; cerrar el panel o sustituir el renderizador llama a destroy. Los argumentos de renderizado también aportan metadatos de calendario y videollamada, la locale, timeFormat, translate, isCreating, save() y cancel().

Contenedor, animación y capas del panel

El plugin es el dueño del elemento contenedor que entrega al renderizador. Lleva la clase df-appointment-custom-drawer-host más un modificador --calendar o --viewport, y el plugin fija su posición, anchura y apilamiento en línea. Aplica estilos desde esas clases; no lo muevas en el DOM ni cambies su posición, porque el plugin vuelve a aplicar ambas cosas en cada render.

Los paneles de sustitución entran y salen con la misma animación que el integrado. Al cerrarse, el plugin marca el contenedor con data-df-drawer-exiting, espera a que terminen sus animaciones de keyframes y solo entonces llama a destroy y lo elimina, de modo que el panel se anima al salir con su contenido aún montado en lugar de desaparecer de golpe:

/* 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;
}

Conviene conocer tres detalles. Solo se esperan las animaciones de keyframes, así que las transiciones CSS que un framework asocia a los hovers y a los anillos de foco nunca retrasan el panel. Las animaciones infinitas se omiten, y un temporizador acotado por la propia duración de la animación respalda la espera, de modo que un spinner dentro del panel —o una pestaña en segundo plano, donde se detienen los fotogramas— nunca puede dejar el contenedor atascado en el documento. Y si se elimina la animación, o si se cumple prefers-reduced-motion: reduce, el contenedor se retira en el mismo fotograma en que se cerró, que es justo lo que el paquete ya hace para quienes prefieren menos movimiento.

El panel se sitúa en z-index: 900, por debajo del popup de creación rápida y de los diálogos del calendario, que están en 1000, de modo que abrir el menú de añadir nunca queda oculto tras un editor abierto. Subir el panel por encima de 1000 invierte esa relación. Si el panel necesita cubrir esas superficies, súbelas también a ellas en lugar de subir solo el panel.

Recurrencia

La disponibilidad se repite semanalmente por defecto. El panel ofrece además una agenda puntual y una regla propia de cada N semanas:

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

frequency: 'none' aplica las horas semanales seleccionadas solo a la semana de lunes a domingo de la fecha ancla; no reduce la disponibilidad únicamente a esa fecha.

Las reglas más complejas —mensuales, por enésimo día de la semana o con RRULE— corresponden a tu aplicación. Exprésalas mediante dateOverrides, que siempre prevalecen sobre la regla de recurrencia.

Ubicación y videollamadas

Una agenda puede indicar dónde ocurre la cita. Esto es configuración, no una reunión ya reservada. Una misma agenda puede reservarse muchas veces, así que nunca guarda una URL de acceso por reserva:

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

El selector Ubicación del panel siempre ofrece «Enlace de reunión propio», «Presencial» y «Llamada telefónica». Las aplicaciones de videollamada con nombre solo aparecen una vez las registras:

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

Proveedores de videollamada

DayFlow nunca se comunica con Google, Zoom ni Microsoft. Define una interfaz de un solo método y la invoca; el token de OAuth, el secreto de la API y el SDK del proveedor se quedan en tu 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 recibe un CreateConferenceInput: scheduleId, title, start y end de Temporal, timeZone, y opcionalmente host y attendees. Devuelve una Conference con provider y joinUrl; meetingId, hostUrl, password y meta son opcionales.

Nombrar los mismos proveedores en el componente de reserva etiqueta la fila de ubicación y muestra el icono del proveedor:

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

Crear la reunión

Crea la videollamada cuando el asistente confirma, no cuando resalta una franja. Quien hace clic en las 10:00 y se marcha no debería dejar una reunión creada.

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 }

Una programación de tipo custom-link se resuelve en la sala permanente del organizador sin llamadas de red, mientras que las de tipo in-person o phone se resuelven en undefined. Un providerId sin proveedor registrado genera un error: una reserva nunca debería perder su enlace de reunión sin avisar.

¿Por qué no usar siempre el mismo enlace?

Para Google Meet, Zoom y Teams es preferible conference a custom-link. Google recomienda crear una videollamada nueva por evento en lugar de reutilizar una, porque compartir los datos de la videollamada entre eventos causa problemas de acceso y de privacidad.

Para renderizar una ubicación en otro sitio, como en un correo o en una página de confirmación, usa el mismo resolvedor puro que la fila de información de la reunión:

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

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

El resolvedor devuelve un ResolvedLocation cuando hay una ubicación configurada.

Componentes de reserva para asistentes

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 contiene contenido opcional, solo de presentación, para el panel de información de la reunión. La disponibilidad y el comportamiento de reserva siguen viniendo de schedule.

PropiedadTipoPara qué sirve
organiserNamestringMuestra el nombre del organizador y aporta la inicial de reserva cuando no hay avatar.
organiserAvatarstringURL de la imagen del avatar del organizador.
organiserUrlstringURL de perfil que se abre desde el avatar del organizador.
titlestringEncabezado sobre la duración, la ubicación y la zona horaria. Pasa schedule.title para reutilizar su título.
descriptionstringTexto de apoyo bajo los metadatos de la reunión.
locationLabelstringSustituye el texto de ubicación derivado de schedule.location. Normalmente es preferible usar el campo de la agenda.

Opciones de reserva

Todos los adaptadores de framework acaban aceptando AppointmentBookingProps; Vue, Angular y Svelte nombran el tipo de montaje equivalente en sus propios puntos de entrada.

Grupo de propiedadesPara qué sirve
scheduleAppointmentSchedule obligatorio con el que se generan las franjas.
calendarAppLee los eventos de DayFlow (y se suscribe a ellos) como tiempo ocupado del organizador.
presentationContenido opcional AppointmentPresentation descrito más arriba.
conferenceProvidersResuelve nombres e iconos de proveedor para las ubicaciones de videollamada.
busyIntervals / attendeeBusyIntervalsRangos Temporal que eliminan franjas del organizador o superponen conflictos del asistente.
layoutUn BookingLayout: calendar-day-slots, multi-day-slots o week-overlay.
renderWeekOverlay / weekOverlayOptionsUsan WeekOverlayRenderArgs y WeekOverlayOptions para activar y ajustar la vista semanal.
availableLayouts / onLayoutChangeControlan los diseños que ofrece el selector integrado.
displayTimeZone / timeZoneOptions / onDisplayTimeZoneChangeControlan la zona horaria mostrada al asistente y su selector.
timeFormat / onTimeFormatChangeControlan el valor de TimeFormat, 12h o 24h.
theme / locale / startOfWeekDefinen el tema de la semana, la locale y el valor de StartOfWeek (0, 1 o 6).
multiDayCount / skipEmptyDaysAjustan el diseño de varios días.
now / rangeStart / rangeEndSustituyen la hora actual y el rango de generación de franjas.
selectedDate / onSelectDateFecha con foco, en modo controlado.
selectedSlotId / onSelectSlotSelección de franja controlada y callback de selección.
loading / disabledMuestran la interfaz de carga o desactivan la interacción.
onError / onRetryIntegran tu propio registro de errores y comportamiento de reintento.
className / styleAñaden estilos al elemento raíz.
labelsSustituye cualquier subconjunto de los textos visibles.
slotsSustituye o amplía las regiones enumeradas más abajo.
onAnalyticsEventRecibe un AppointmentBookingAnalyticsEvent y su carga útil no personal.

Elige el diseño según la cantidad de disponibilidad que necesites mostrar:

DiseñoRecomendado para
calendar-day-slotsUn selector mensual con las horas del día elegido.
multi-day-slotsComparar horas disponibles de varios días en columnas.
week-overlayMostrar las horas reservables sobre una línea temporal semanal. Requiere el renderizador de abajo.

El diseño semanal es opcional, porque es el único que necesita @dayflow/core:

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

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

Adaptadores de framework

Usa el adaptador del framework en el que esté tu página de reservas. Todos aceptan las mismas opciones y se encargan del montaje, de las actualizaciones reactivas y de la limpieza. Importar un adaptador no incluye en tu bundle los runtimes de los demás frameworks.

Crea una agenda serializable que pueda compartir cualquier adaptador:

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',
  },
};

Importa styles.components.css una sola vez desde el punto de entrada de tu aplicación y usa después el adaptador correspondiente:

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>

Los adaptadores que aceptan un objeto options exponen las regiones propias como options.slots; los componentes con props exponen directamente el mismo contrato slots. Sustituye el objeto de opciones para actualizar la reserva. Cambiar la colección de renderizadores de región vuelve a montar de forma segura, y desmontar el contenedor destruye la instancia de reserva.

APIs de integración

API de montaje directo

Usa createAppointmentBooking cuando quieras montar tú mismo la interfaz de reserva, ya sea desde otro framework o desde una página sin 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();

Los hijos que ya existan en el destino se dejan intactos. La API de montaje crea y gestiona un único elemento hijo, y lo elimina al llamar a destroy().

Las regiones del DOM reciben sus argumentos actuales y un contenedor que pertenece al renderizador:

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

Un renderizador puede no devolver nada, devolver una función de limpieza o devolver { update, destroy }. La forma con handle está pensada para las APIs de montaje de frameworks que añaden en lugar de parchear.

Controlador de reservas headless

Usa createBookingController cuando quieras el comportamiento de reserva que trae el paquete pero con tu propio marcado. Gestiona la fecha y la franja seleccionadas, la zona horaria y el formato, la visibilidad del tiempo ocupado del asistente y la agrupación de franjas, pero nunca toca el 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();

Personalización

Tokens de diseño

.df-appointment-booking es el ámbito de tema del componente del asistente, no su único token. El paquete admite las propiedades personalizadas siguientes. Carga tus sustituciones después de la hoja de estilos del paquete y defínelas en .df-appointment-booking, o pasa un className propio y apunta a ambas clases.

.df-appointment-booking.my-booking-theme {
  --df-ap-accent: #2563eb;
  --df-ap-radius: 6px;
  --df-ap-sidebar-width: 320px;
}
TokenValor por defectoQué controla
--df-ap-surfaceFondo del núcleoSuperficies principales
--df-ap-surface-sunkenTono apagado del núcleoSuperficies hundidas
--df-ap-fill#e8eaefRellenos neutros
--df-ap-borderBorde del núcleoBordes y separadores
--df-ap-fgPrimer plano del núcleoTexto principal
--df-ap-fg-mutedPrimer plano apagado del núcleoTexto secundario
--df-ap-fg-subtle#9ca3afTexto sutil
--df-ap-accentColor primario del núcleoControles seleccionados
--df-ap-accent-fgPrimer plano sobre el primarioTexto sobre superficies de acento
--df-ap-accent-softDerivado del acentoFondos suaves al pasar el ratón
--df-ap-accent-ringDerivado del acentoAnillos de foco y selección
--df-ap-available#22c55eIndicador de disponibilidad
--df-ap-radius14pxRadio de las tarjetas
--df-ap-radius-md9pxRadio de los controles
--df-ap-radius-sm7pxRadio de los elementos compactos
--df-ap-max-width1440pxAnchura máxima de la reserva
--df-ap-sidebar-width296pxAnchura de la barra lateral
--df-ap-slots-width320pxAnchura de la columna de franjas del día
--df-ap-week-height620pxAltura de la línea temporal semanal
--df-ap-slot-scroll-heightnoneAltura de la lista de franjas del día
--df-ap-pad1.5remEspaciado interno principal
--df-ap-fontPila de fuentes del sistemaTipografía de la reserva

Los colores de hover, selección y foco se derivan de --df-ap-accent. Los temas avanzados también pueden sustituir directamente --df-ap-accent-soft y --df-ap-accent-ring.

El panel del organizador forma parte del calendario de DayFlow y usa el tema del núcleo, no los tokens del asistente:

: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;
}

Usa :root cuando drawerPlacement sea viewport, porque el panel se monta fuera del elemento del calendario. Con el anclaje calendar, las variables pueden definirse en el contenedor del calendario.

Regiones personalizables

Todos los adaptadores admiten regiones, pero la firma de sus renderizadores varía:

  • Las regiones de React devuelven contenido de React. slotButton y dayCell reciben además defaultContent, de modo que pueden envolver el contenido integrado.
  • Vue, Angular y Svelte usan options.slots. Cada renderizador recibe (args, host) y escribe en el elemento del DOM que se le pasa. Puede devolver una función de limpieza o un handle { update, destroy } cuando monta un componente del framework.

Los ejemplos siguientes personalizan correctamente la misma región slotButton en cada 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 define las regiones de abajo. Entre los tipos de argumento exportados están SidebarSlotArgs, MeetingInfoSlotArgs, MonthPickerSlotArgs, SlotListSlotArgs, ToolbarSlotArgs, SlotButtonSlotArgs, DayCellSlotArgs y SelectedSummarySlotArgs.

RegiónPara qué sirveArgumentos importantes
sidebarSustituye toda la columna lateral con los detalles de la reunión y el minicalendario.schedule, presentation, location, layout, defaultContent
meetingInfoSustituye el panel de información de la reunión.schedule, presentation, location, timeZone, defaultContent
meetingInfoFooterAñade contenido bajo el panel de información de la reunión.schedule, presentation, location, timeZone
monthPickerSustituye el selector de mes del área principal y de la barra lateral.selectedDate, availableDates, onSelectDate, compact, defaultContent
slotListSustituye la lista de horas del día elegido en calendar-day-slots.date, slots, selectedSlotId, onSelectSlot, defaultContent
toolbarSustituye la barra de herramientas en multi-day-slots y week-overlay.layout, timeZone, timeFormat, rangeLabel, defaultContent
toolbarExtraAñade contenido a la derecha de los controles de la barra de herramientas.layout, timeZone, timeFormat, rangeLabel
slotButtonSustituye el contenido de cada botón de hora reservable.slot, formattedTime, isSelected, disabled, defaultContent
dayCellSustituye el contenido de cada celda de día del selector de mes.date, mes, estado de disponibilidad y selección, defaultContent
emptyDaySe renderiza cuando el día elegido no tiene horas disponibles.date
emptyRangeSe renderiza cuando el rango de fechas actual no tiene horas reservables.Ninguno
loadingSustituye el estado de carga.Ninguno
errorSustituye el estado de error y recibe el error y una acción de reintento opcional.error, retry
selectedSummaryAñade un resumen bajo el contenido cuando se ha seleccionado una franja.slot, formattedDate, formattedRange, timeZone

Headless: construye tu propia interfaz

Si ninguno de los diseños te encaja, prescinde por completo de los componentes. El motor es una función pura que recibe una agenda y devuelve franjas. Nada más del paquete llega a tu 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 }[]

La entrada es un SlotQuery; rangeStart y rangeEnd son valores Temporal.PlainDate inclusivos. busyIntervals y attendeeBusyIntervals contienen valores BusyInterval con start y end de Temporal. El resultado es un AppointmentSlot[].

El motor nunca toca window, document ni la zona horaria del sistema, así que es seguro importarlo en un servidor. También exporta sus piezas: expandAvailability, recurrenceAppliesOn, sortBusyIntervals, mergeBusyIntervals, hasConflict y eventsToBusyIntervals. Puedes usarlas para componer tu propia canalización.

Convertir una reserva en un evento

El módulo nunca escribe eventos. En su lugar te da una función de mapeo pura:

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 lleva appointmentScheduleId y appointmentSlotId, de modo que un evento puede rastrearse hasta la franja de la que salió, y además appointmentLocation y appointmentConference cuando la agenda tiene ubicación.

Accesibilidad

Apunta a WCAG 2.2 AA. El selector de mes es un role="grid" real, con navegación mediante flechas, Inicio, Fin, RePág y AvPág; los botones de franja exponen aria-pressed y un nombre accesible que incluye fecha, hora y zona horaria; y los estados disponible, no disponible y seleccionado nunca se indican solo con el color.

En esta página