Saltar al contenido
SuperScheduler

InteracciónSe aplica aSuperScheduler Pro

Horas, minutos, días y zoom

Elige el tamaño de celda con scale ('Hour', 'Day', 'Week', 'Month', 'Year', o 'CellDuration' con cellDuration en minutos), fija cellWidth en píxeles por celda y days para la longitud, y describe las filas de cabecera con timeHeaders. Oculta noches y fines de semana con businessBeginsHour, businessEndsHour y showNonBusiness={false}. Para el zoom, enumera zoomLevels y cambia entre ellos con control.zoom.setActive, animateTo o step; los gestos de pellizco y Ctrl/Cmd + rueda están activados por defecto.

Verificado con v0.1.0 · revisado el 7 de octubre de 2026.md

El eje de tiempo de SuperScheduler Pro se define con unas pocas opciones: qué representa una celda (scale), cuánto mide de ancho (cellWidth), dónde empieza la línea de tiempo y cuánto dura (startDate, days), y cómo la etiquetan las filas de cabecera (timeHeaders). El zoom es una lista de configuraciones de ese tipo, zoomLevels, a la que los usuarios llegan con gestos y tu código mediante control.zoom.

Esta guía va de las escalas fijas al zoom continuo. Lite tiene un eje de días fijo; todo lo demás que aparece aquí requiere Pro.

Escala, duración y ancho de celda

scaleUna celda esUso típico
'Minute'1 minutoEscaletas de emisión, ejecuciones de laboratorio
'CellDuration'cellDuration minutos (60 por defecto)Franjas de 5, 15 o 30 minutos; turnos de 240 minutos
'Hour'1 horaTalleres, salas de reuniones, cuadrillas
'Day'1 día naturalHoteles, alquileres, asignación de personal
'Week'1 semana natural, que empieza en weekStartsProyectos, campañas
'Month'1 mes naturalAsignaciones largas, planes de capacidad
'Year'1 año naturalVistas generales de varios años
'Manual'Las celdas que enumeras en timelinePeriodos irregulares

cellWidth está en píxeles por celda de la escala actual (40 por defecto): 44 significa 44 píxeles por día en un eje de días, pero 44 píxeles por hora en un eje de horas. startDate (hoy por defecto, truncado a medianoche) y days fijan la longitud de la línea de tiempo.

Algunas configuraciones típicas:

src/scales.tsts
import type { SchedulerProps } from 'super-scheduler'

// A month of day cells: the classic booking chart.
export const monthOfDays = {
  scale: 'Day',
  startDate: '2026-10-01',
  days: 31,
  cellWidth: 44,
  timeHeaders: [{ groupBy: 'Month' }, { groupBy: 'Day', format: 'ddd d' }],
} satisfies SchedulerProps

// One working day in 15-minute cells; nights are removed from the axis.
export const quarterHours = {
  scale: 'CellDuration',
  cellDuration: 15,
  startDate: '2026-10-12',
  days: 1,
  cellWidth: 36,
  businessBeginsHour: 7,
  businessEndsHour: 19,
  showNonBusiness: false,
  timeHeaders: [
    { groupBy: 'Hour', format: 'HH:mm' },
    { groupBy: 'Cell', format: 'mm' },
  ],
} satisfies SchedulerProps

// A work week of hours, Monday to Friday, with 12-hour labels.
export const workWeekOfHours = {
  scale: 'Hour',
  startDate: '2026-10-12',
  days: 5,
  cellWidth: 48,
  timeFormat: 'Clock12Hours',
  businessBeginsHour: 8,
  businessEndsHour: 18,
  showNonBusiness: false,
  timeHeaders: [{ groupBy: 'Day', format: 'dddd d MMMM' }, { groupBy: 'Hour' }],
} satisfies SchedulerProps

// A year in month cells, for long-running assignments.
export const yearOfMonths = {
  scale: 'Month',
  startDate: '2026-01-01',
  days: 365,
  cellWidth: 90,
  timeHeaders: [{ groupBy: 'Year' }, { groupBy: 'Month', format: 'MMM' }],
} satisfies SchedulerProps

cellDuration también fija el ajuste a la cuadrícula por defecto: con celdas de 15 minutos, los movimientos, redimensionados y selecciones se ajustan a cuartos de hora. La familia de opciones snapToGrid desactiva el ajuste gesto a gesto. En un eje de días, los eventos se dibujan por defecto como celdas completas (useEventBoxes: 'Always'); pon useEventBoxes="Never" para dibujarlos en su hora exacta, de modo que una estancia de 14:00 a 11:00 empiece y termine dentro de sus celdas de día.

Cabeceras de tiempo

timeHeaders enumera las filas de cabecera de arriba abajo. Cada fila agrupa el tiempo por una unidad y puede fijar un format de etiqueta y una height:

groupByAgrupa por
'Year', 'Quarter', 'Month', 'Week', 'Day', 'Hour', 'Minute'Esa unidad del calendario
'Cell'Una etiqueta por celda
'Default'cellGroupBy ('Day' por defecto)
'None'Una etiqueta para toda la fila

El valor por defecto es [{ groupBy: 'Default' }, { groupBy: 'Cell' }]: días encima de las celdas. Cada fila de cabecera mide headerHeight píxeles de alto (30 por defecto) salvo que fije su propia height.

Tokens de formato

Los formatos usan estos tokens; cualquier otro carácter se imprime tal cual. Los ejemplos formatean 2026-10-05T14:30:00 con el locale en-us.

TokenResultadoTokenResultado
yyyy2026HH14
yy26H14
MMMMOctoberhh02
MMMOcth2
MM10mm30
M10m30
ddddMondayss, s00, 0
dddMottPM
dd, d05, 5%d5

Los nombres siguen el locale del scheduler ('en-us' por defecto): 'dddd d MMMM' da «lunes 5 octubre» con locale="es-es". En varios locales, ddd es una abreviatura de una o dos letras («Mo», «L»); usa dddd, o escribe tu propia etiqueta en onBeforeTimeHeaderRender, cuando quieras tres letras. En ese hook se pueden personalizar las etiquetas, los estilos, los tooltips y las áreas de la cabecera.

Etiquetas de 12 o de 24 horas

timeFormat controla las etiquetas de hora por defecto: 'Auto' (por defecto) sigue el locale (12 horas para en-us, 24 horas para la mayoría de los locales europeos), y 'Clock12Hours' y 'Clock24Hours' fuerzan uno de los dos. Un format explícito en una fila de cabecera siempre tiene prioridad: 'h:mm tt' para etiquetas de 12 horas, 'HH:mm' para etiquetas de 24 horas. Cambiar el formato de reloj solo cambia las etiquetas; las horas de los eventos nunca se mueven.

Horario laboral y tiempo oculto

El tiempo laborable se define con businessBeginsHour (9 por defecto), businessEndsHour (18 por defecto; 0 significa la medianoche al final del día) y businessWeekends (false por defecto). Con showNonBusiness en su valor por defecto, true, las celdas de tiempo no laborable se sombrean. Con showNonBusiness={false} se eliminan del eje:

  • en un eje de días, desaparecen los fines de semana (14 días se convierten en 10 columnas);
  • en un eje intradía, desaparecen las horas fuera del horario laboral, así que una semana laboral en horas muestra solo de 08:00 a 18:00 cada día.

Para algo más específico, onIncludeTimeCell se llama para cada celda candidata mientras se construye la línea de tiempo: pon args.cell.visible = false para quitar una celda, o args.cell.width para cambiar su ancho. scale: 'Manual' con un array timeline de celdas { start, end, width } da control total.

Niveles de zoom

Un nivel de zoom es un conjunto con nombre de opciones que se aplican juntas: normalmente scale, cellDuration, cellWidth y timeHeaders. Define la escalera una sola vez, a nivel de módulo:

src/zoom-levels.tsts
import type { SuperScheduler } from 'super-scheduler'

/**
 * From the most detailed view to the widest. Each level is a set of options applied together;
 * cellWidth is in px per cell of that level (per 15 minutes, per hour, per day, per week).
 */
export const ZOOM_LEVELS: SuperScheduler.ZoomLevel[] = [
  {
    id: 'quarter-hours',
    properties: {
      scale: 'CellDuration',
      cellDuration: 15,
      cellWidth: 40,
      timeHeaders: [
        { groupBy: 'Day', format: 'dddd d MMMM' },
        { groupBy: 'Cell', format: 'HH:mm' },
      ],
    },
  },
  {
    id: 'hours',
    properties: {
      scale: 'Hour',
      cellWidth: 56,
      timeHeaders: [
        { groupBy: 'Day', format: 'dddd d MMMM' },
        { groupBy: 'Hour', format: 'HH:mm' },
      ],
    },
  },
  {
    id: 'days',
    properties: {
      scale: 'Day',
      cellWidth: 80,
      timeHeaders: [{ groupBy: 'Month' }, { groupBy: 'Day', format: 'ddd d' }],
    },
  },
  {
    id: 'weeks',
    properties: {
      scale: 'Week',
      cellWidth: 120,
      timeHeaders: [{ groupBy: 'Month' }, { groupBy: 'Week' }],
    },
  },
]

export type ZoomLevelId = 'quarter-hours' | 'hours' | 'days' | 'weeks'
export const ZOOM_LEVEL_IDS: readonly ZoomLevelId[] = ['quarter-hours', 'hours', 'days', 'weeks']

Pásala como zoomLevels y elige el nivel inicial con zoom (un índice o un id). zoomPosition ('left' por defecto, o 'middle', 'right') decide qué parte del área visible se queda en su sitio cuando cambia el nivel.

Una propiedad también puede ser una función de la fecha de anclaje, ({ date, level }) => value, por ejemplo para mostrar el año en la cabecera de meses solo en torno al Año Nuevo.

Durante un gesto continuo, el scheduler elige el nivel más cercano al tiempo por píxel actual y aplica su eje y sus cabeceras cuando el usuario entra en él. Las propiedades que reiniciarían la ventana, como days y startDate, solo se aplican cuando tu código selecciona un nivel de forma explícita. El orden del array no importa a los gestos, que miden todos los niveles.

Cambiar el zoom desde código

control.zoom tiene tres métodos y una propiedad:

MiembroQué hace
setActive(level, position?, anchorDate?)Aplica un nivel (índice o id) de inmediato, incluidos days y startDate
animateTo(target, options?)Anima hasta { level } o hasta un { cellWidth } libre; devuelve una promesa que se resuelve cuando termina
step(delta, options?)Avanza delta posiciones por zoomLevels, en el orden del array y sin salirse de los extremos; sin zoomLevels, multiplica el ancho de celda por 1,6 en cada paso
activeÍndice del nivel activo; -1 antes de aplicar ningún nivel

animateTo y step aceptan { duration, position, anchorDate }: duration en milisegundos (300 por defecto, 0 para no animar) y anchorDate como una fecha, 'center' o 'today' para mantener ese momento en su sitio. Las animaciones son instantáneas cuando el usuario prefiere movimiento reducido, y no hacen nada mientras hay un gesto de zoom en curso. Un id de nivel desconocido lanza un error.

src/ZoomablePlanning.tsxtsx
import { useCallback, useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { ZOOM_LEVELS, ZOOM_LEVEL_IDS, type ZoomLevelId } from './zoom-levels'

// Stable objects: a new one per render would be re-applied on every render.
const ZOOM_GESTURE: SuperScheduler.ZoomGestureOptions = {
  // The default maximum (400 px per cell) is too narrow to cross from days into hours.
  max: 1024,
}

interface Props {
  readonly rooms: SuperScheduler.ResourceData[]
  readonly bookings: SuperScheduler.EventData[]
}

export function ZoomablePlanning({ rooms, bookings }: Props) {
  const { controlRef, control } = useSchedulerControl()
  const [level, setLevel] = useState<ZoomLevelId>('days')
  const owned = useMemo(() => bookings.slice(), [bookings])

  // One React update when a gesture or an animation settles, never one per frame.
  const onZoom = useCallback((args: SuperScheduler.ZoomArgs) => {
    if (args.phase !== 'end') return
    const id = ZOOM_LEVEL_IDS[args.level]
    if (id !== undefined) setLevel(id)
  }, [])

  const show = (id: ZoomLevelId) =>
    void control?.zoom.animateTo({ level: id }, { anchorDate: 'center' })
  // step() walks the zoomLevels array in its order: here -1 is more detail, +1 a wider view.
  const zoomIn = () => void control?.zoom.step(-1)
  const zoomOut = () => void control?.zoom.step(1)

  return (
    <>
      <div role="toolbar" aria-label="Zoom">
        {ZOOM_LEVEL_IDS.map((id) => (
          <button key={id} type="button" aria-pressed={level === id} onClick={() => show(id)}>
            {id}
          </button>
        ))}
        <button type="button" aria-label="Zoom in" onClick={zoomIn}>
          +
        </button>
        <button type="button" aria-label="Zoom out" onClick={zoomOut}>
          −
        </button>
      </div>
      <SuperSchedulerComponent
        controlRef={controlRef}
        startDate="2026-10-12"
        days={14}
        zoomLevels={ZOOM_LEVELS}
        zoom="days"
        zoomPosition="middle"
        zoomGesture={ZOOM_GESTURE}
        onZoom={onZoom}
        resources={rooms}
        events={owned}
      />
    </>
  )
}

Deberías ver cuatro botones de nivel y botones de más y menos encima de la planificación. Pulsar «hours» anima el eje de días a horas en torno al centro de la vista, y el estado pulsado también sigue a los gestos de pellizco, porque onZoom comunica el nivel al terminar cada zoom.

onZoom recibe phase ('start', 'change', 'end'), origin ('gesture' o 'api'), level, cellWidth, scale, cellDuration, la fecha de anclaje, el inicio del área visible y el nivel de detalle. Los gestos y animateTo informan en cada fotograma; actúa en phase === 'end' salvo que actualices el DOM directamente, y nunca actualices el estado de React en cada fotograma.

Gestos

Los gestos de zoom están activados por defecto: Ctrl o Cmd con la rueda del ratón (que también cubre el pellizco en el trackpad en Chrome, Edge y Firefox), el pellizco en el trackpad en Safari y el pellizco con dos dedos en pantallas táctiles. El zoom es continuo y se ancla bajo el puntero. Ajústalo con zoomGesture:

OpciónPor defectoSignificado
mincellWidthMin (como mínimo 1)Ancho de celda mínimo en píxeles
max400Ancho de celda máximo en píxeles
wheel'ctrl''always' hace zoom con cualquier giro vertical de la rueda (Mayús + rueda hace scroll); false nunca hace zoom con la rueda
pinchtruePellizco en el trackpad de Safari y en pantallas táctiles
sensitivity1Multiplicador de velocidad
scales'zoomLevels'Pasa de uno a otro de tus niveles; 'auto' usa una escalera de hora, día, semana y mes; false mantiene la escala actual
linkningunoLos schedulers con el mismo id de enlace hacen zoom juntos

zoomGesture={false} elimina todos los listeners de gestos. Como cellWidth es por celda, pasar de un nivel de días a uno de horas necesita margen: un día a 400 píxeles son solo unos 17 píxeles por hora, así que sube max (el ejemplo usa 1024) cuando tu escalera va de días a horas.

keyboardOptions={{ zoomKeys: true }}, junto con keyboardEnabled, añade Ctrl/Cmd con = o + (step(1)), - (step(-1)) y 0 (vuelta al nivel inicial) mientras el foco está dentro del scheduler.

Widgets de zoom

super-scheduler/zoom-ui ofrece tres widgets DOM opcionales que se actualizan sin renders de React:

  • createZoomHud(control, options): un indicador dentro de la cuadrícula que se muestra durante los gestos y durante 700 ms después de un zoom por API; format fija su texto.
  • createZoomSlider(control, container, options): un input de rango nativo con soporte de teclado, escala logarítmica o lineal y topes en tus niveles de zoom o en anchos explícitos. Su valor son píxeles por celda, así que encaja mejor con un eje de una sola escala.
  • createLodBadge(control, container, labels): muestra si la vista está en modo detalle, compacto o vista general.
src/PlanningWithZoomWidgets.tsxtsx
import { useEffect, useRef } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { createLodBadge, createZoomHud, createZoomSlider } from 'super-scheduler/zoom-ui'

export function PlanningWithZoomWidgets({ rooms }: { rooms: SuperScheduler.ResourceData[] }) {
  const { controlRef, control } = useSchedulerControl()
  const toolbar = useRef<HTMLDivElement>(null)

  // Widgets need a mounted control. The cleanup matters: Strict Mode mounts twice in development.
  useEffect(() => {
    const host = toolbar.current
    if (control === null || host === null) return
    // A pill inside the grid while zooming (no container needed).
    const hud = createZoomHud(control, {
      format: ({ cellWidth }) => `${Math.round(cellWidth)} px per day`,
    })
    // A native range input; its value is px per cell, so it suits a single-scale axis like this one.
    const slider = createZoomSlider(control, host, {
      min: 4,
      max: 160,
      scale: 'log',
      label: 'Day width',
    })
    // Detail, Compact or Overview, following the level of detail.
    const badge = createLodBadge(control, host)
    return () => {
      hud.dispose()
      slider.dispose()
      badge.dispose()
    }
  }, [control])

  return (
    <>
      <div ref={toolbar} role="toolbar" aria-label="Zoom" />
      <SuperSchedulerComponent
        controlRef={controlRef}
        startDate="2026-10-01"
        days={120}
        scale="Day"
        cellWidth={44}
        resources={rooms}
      />
    </>
  )
}

Cada widget tiene element y dispose(). Créalos en un efecto que dependa del control y libéralos en su función de limpieza; liberar el control también los elimina.

Nivel de detalle

Cuando los usuarios alejan mucho el zoom, dibujar cada etiqueta a tamaño completo sería ilegible y lento. El nivel de detalle (lod, activado por defecto) adapta el render al espacio en pantalla y no cambia nada a partir de 40 píxeles por día:

  • Los eventos, por debajo de 40 píxeles por día, se adaptan a su propio ancho: contenido completo a partir de 80 píxeles, una línea de texto a partir de 66 píxeles y un bloque simple por debajo. Por debajo de 8 píxeles por día, los bloques pasan a ser rellenos sólidos y los eventos estrechos, barras finas.
  • Las celdas muestran su contenido (HTML, texto, áreas) a partir de 24 píxeles por celda. Por debajo de 2 píxeles por celda no hay ningún elemento de celda y no se llama a onBeforeCellRender.
  • Las líneas de la cuadrícula mantienen al menos 8 píxeles de separación, el sombreado de fines de semana y de tiempo no laborable necesita 6 píxeles por día, y las etiquetas de cabecera se acortan o pasan a una unidad mayor cuando ya no caben.

Todos los umbrales se pueden cambiar mediante lod={{ ... }} (zoomedOut, eventFull, eventText, eventSolid, cellContent, cellBackground, gridLines, shading, dayLabel, dayNumber, weekLabel, hysteresis), y lod={false} renderiza literalmente con cualquier zoom. El estado actual está en control.levelOfDetail (level es 'full', 'compact' u 'overview') y se escribe como atributos data-lod en la raíz, para tu CSS.

Citas de clínica de fisioterapiaUn paciente no puede venir a las 10:00. Encuentra el siguiente hueco que respete pausas y limpiezas. Planificación de escenarios de un festivalUna prueba de sonido invade el margen de una actuación. Amplía a cinco minutos, recórtala y mira qué salas y equipos dependen del artista. Planificación de una flota de alquilerUn compacto queda inmovilizado el día de la recogida. Pasa su alquiler a otro coche, respeta la limpieza y mira cuándo se queda sin flota la oficina.

Siguientes pasos