Saltar al contenido
SuperScheduler

PersonalizaciónSe aplica aSuperScheduler Pro

Slots de renderizado React y tarjetas emergentes

Importa SuperSchedulerComponent desde super-scheduler/react-render en lugar de desde la raíz del paquete y pasa renderEvent, renderRowHeader, renderTimeHeader, renderCorner, renderCell o renderArea; cada una devuelve el contenido React de un tipo de slot. El HTML o el texto de reserva se pinta primero y React lo sustituye en pequeños lotes cuando el navegador está libre, así que el scroll nunca espera a React. Añade eventHover para tener tarjetas emergentes que los usuarios pueden fijar.

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

SuperScheduler pinta su cuadrícula con su propio código DOM, y eso es lo que mantiene fluido el scroll con miles de filas y eventos. Cuando el contenido de un evento o de una cabecera debe salir de tus componentes React (tu sistema de diseño, iconos, avatares, valores formateados), el punto de entrada super-scheduler/react-render monta contenido React en los slots del scheduler sin cederle a React el control de la cuadrícula.

Los slots de renderizado React y las tarjetas emergentes necesitan SuperScheduler Pro.

Cambiar al componente react-render

super-scheduler/react-render exporta su propio SuperSchedulerComponent. Acepta todas las props del componente principal, expone los mismos ref.current.control y controlRef, y añade las props render*, eventHover, renderOptions y los handlers onBefore*DomAdd / onBefore*DomRemove.

El componente de la raíz del paquete también acepta estas props, pero solo avisa una vez (needs the component from "super-scheduler/react-render") y no renderiza nada a partir de ellas. Tener la maquinaria de React en su propio punto de entrada hace que las páginas que no la usan no la carguen.

Los slots

PropArgumentosSustituye
renderEventcontrol, e, data, row, width, lodEl contenido de la caja de un evento
renderRowHeadercontrol, row, columnEl contenido de una celda de la cabecera de fila (column es el índice de la columna, 0 si no hay columnas)
renderTimeHeadercontrol, header (start, end, level)El contenido de una celda de la cabecera de tiempo
renderCornercontrolLa esquina superior izquierda
renderCellcontrol, cellEl contenido de una celda de la cuadrícula
renderAreacontrol, area, sourceUn área declarada con render: true

El motor conserva las partes que le pertenecen: la caja del evento y su posición, la barra de duración, los tiradores de redimensionado, las áreas normales, el botón de plegar del árbol y las líneas de la cuadrícula. Un slot es el contenido de dentro.

Renderizar el contenido de los eventos

src/CampaignBoard.tsxtsx
import { memo, useMemo } from 'react'
import { SuperScheduler } from 'super-scheduler'
import type { SchedulerProps } from 'super-scheduler'
import { SuperSchedulerComponent } from 'super-scheduler/react-render'
import 'super-scheduler/styles.css'

type Campaign = { client: string; channel: 'social' | 'print' | 'video'; progress: number }

const CHANNEL_LABEL: Record<Campaign['channel'], string> = {
  social: 'Social',
  print: 'Print',
  video: 'Video',
}

const CampaignContent = memo(function CampaignContent(props: {
  title: string
  campaign: Campaign
  compact: boolean
}) {
  const { title, campaign, compact } = props
  if (compact) return <strong className="campaign__title">{title}</strong>
  return (
    <span className="campaign">
      <strong className="campaign__title">{title}</strong>
      <span className="campaign__meta">
        {campaign.client} · {CHANNEL_LABEL[campaign.channel]} ·{' '}
        {Math.round(campaign.progress * 100)}%
      </span>
    </span>
  )
})

// Module-level functions keep their identity: a new function re-renders every slot.
const renderEvent: NonNullable<SchedulerProps['renderEvent']> = ({ e, data, width, lod }) => {
  // `data` is the event after onBeforeEventRender; custom fields need a cast.
  const campaign = data as SuperScheduler.EventRenderData<Campaign>
  // `width` comes in 8 px steps and changes only when a gesture ends.
  return (
    <CampaignContent title={e.text()} campaign={campaign} compact={width < 160 || lod !== 'full'} />
  )
}

// The HTML fallback paints first and stays if the React content fails.
const onBeforeEventRender: NonNullable<SchedulerProps['onBeforeEventRender']> = (args) => {
  args.data.html = SuperScheduler.Util.escapeHtml(args.data.text)
}

export function CampaignBoard(props: {
  resources: SuperScheduler.ResourceData[]
  campaigns: SuperScheduler.EventData<Campaign>[]
}) {
  const events = useMemo(() => props.campaigns.slice(), [props.campaigns])
  return (
    <SuperSchedulerComponent
      startDate="2026-10-01"
      days={61}
      scale="Day"
      cellWidth={36}
      eventHeight={44}
      resources={props.resources}
      events={events}
      onBeforeEventRender={onBeforeEventRender}
      renderEvent={renderEvent}
    />
  )
}

Deberías ver cada campaña con su cliente, su canal y su progreso, y solo el título cuando el evento mide menos de 160 px o el zoom está alejado.

Los argumentos, en detalle:

  • e es el envoltorio del evento: e.id(), e.text(), e.start(), e.end(), y e.data para el objeto almacenado.
  • data es el evento tal como lo dejó onBeforeEventRender, con start y end como valores SuperScheduler.Date. Los campos propios necesitan un cast, como en el snippet.
  • width es el ancho renderizado en pasos de 8 px, que se actualiza al terminar un gesto y no en cada fotograma.
  • lod es el nivel de detalle ('full', 'compact' u 'overview') en el momento en que se renderizó el contenido.

Cabeceras, esquina, celdas y áreas

src/TeamBoard.tsxtsx
import { useMemo } from 'react'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
import { SuperSchedulerComponent } from 'super-scheduler/react-render'

const TIME_HEADERS: SuperScheduler.TimeHeaderData[] = [{ groupBy: 'Month' }, { groupBy: 'Day' }]

// Scheduler dates are civil values: format their native Date in UTC to keep the wall clock.
const WEEKDAY = new Intl.DateTimeFormat('en-US', { weekday: 'short', timeZone: 'UTC' })

// Every slot of a kind gets the function's result: return content for each case
// (a null result leaves that slot empty rather than showing the fallback).
const SLOTS: SchedulerProps = {
  renderRowHeader: ({ row }) => {
    const role = typeof row.data.role === 'string' ? row.data.role : ''
    return (
      <span className="person">
        <span className="person__initials" aria-hidden="true">
          {row.name.slice(0, 1)}
        </span>
        <span className="person__name">{row.name}</span>
        {role !== '' && <span className="person__role">{role}</span>}
      </span>
    )
  },
  renderTimeHeader: ({ header }) =>
    header.level === 0 ? (
      <span>{header.start.toString('MMMM yyyy')}</span>
    ) : (
      <span className="day">
        <small>{WEEKDAY.format(header.start.toDate())}</small> {header.start.toString('d')}
      </span>
    ),
  renderCorner: () => <span className="corner">Team</span>,
  // Only areas declared with `render: true` reach renderArea.
  renderArea: ({ area }) =>
    area.id === 'approval' ? <span className="badge">Needs approval</span> : null,
  onBeforeEventRender: (args) => {
    if (args.data.status === 'draft') {
      args.data.areas = [
        { id: 'approval', render: true, right: 4, top: 4, width: 96, height: 16, action: 'None' },
      ]
    }
  },
}

export function TeamBoard(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
}) {
  const events = useMemo(() => props.events.slice(), [props.events])
  return (
    <SuperSchedulerComponent
      {...SLOTS}
      startDate="2026-10-01"
      days={31}
      scale="Day"
      timeHeaders={TIME_HEADERS}
      rowHeaderWidth={200}
      resources={props.resources}
      events={events}
      // Keeps React work bounded on large boards (defaults shown).
      renderOptions={{ sliceMs: 8 }}
    />
  )
}

Una función de render es dueña de todos los slots de su tipo. Devuelve contenido para cada caso: un resultado null deja ese slot vacío en lugar de mostrar el contenido de reserva. En la práctica, renderArea es la excepción, porque solo le llegan las áreas que declaraste con render: true.

Notas por slot:

  • Cabeceras de fila. El botón de plegar del árbol se queda en su sitio. Con rowHeaderColumns, la función se ejecuta una vez por columna y recibe su índice en column.
  • Cabeceras de tiempo. header.level es el índice en timeHeaders (0 es la fila superior). Las fechas del scheduler son valores civiles: para formatearlas con Intl, pasa date.toDate() y timeZone: 'UTC', como hace el snippet.
  • Celdas. renderCell monta una raíz de React por cada celda montada, y ninguna mientras las celdas miden menos de 24 px. Una vista de 40 filas por 30 días ya monta 1200: para disponibilidad, precios o sombreado, usa en su lugar html, cssClass o backColor en onBeforeCellRender.
  • Áreas. Declara el área en el evento (o en la fila, la celda o la cabecera) con render: true y su posición; source te dice a qué elemento pertenece el área.

Contenido de reserva, lotes y ciclo de vida

El contenido React nunca bloquea el pintado:

  1. El scheduler pinta primero el HTML o el texto de reserva: el html o el text que producen tus datos y tus hooks onBefore*Render.
  2. Cuando el navegador está libre, el contenido React se confirma en lotes que apuntan a renderOptions.sliceMs (8 ms por defecto). Cada slot oculta su contenido de reserva en cuanto su contenido está listo.
  3. Durante el scroll, el zoom y el arrastre, el contenido existente se mueve con la cuadrícula. Los slots nuevos y los cambios de renderer esperan a que termine el gesto.
  4. Si una función de render lanza un error, ese slot conserva su contenido de reserva y el error se comunica una vez por slot mediante reportError del navegador (un evento global error que tu herramienta de seguimiento de errores puede capturar).

El contenido que sale de la vista con el scroll se conserva desconectado para que pueda volver sin renderizarse de nuevo: hasta renderOptions.retain elementos, por defecto el doble de los montados con un máximo de 2000. El estado local de un elemento conservado sobrevive; un elemento descartado empieza de cero. retain: 0 desactiva la conservación.

El contenido de los slots se renderiza mediante portales, así que ve tus providers: tema, traducciones, router, clientes de datos. El CSS puede apuntar a [data-super-scheduler-slot], [data-super-scheduler-slot-ready] y [data-super-scheduler-fallback].

En el servidor, el componente renderiza un <div> vacío; los slots aparecen después de que el scheduler se monte en el cliente. Consulta renderizado en servidor y prerenderizado.

Tarjetas emergentes

eventHover muestra una tarjeta React junto a un evento cuando el puntero se detiene sobre él. Sin eventHover, no aparece ninguna tarjeta.

OpciónPor defectoEfecto
render(args)obligatoriaContenido de la tarjeta; args tiene control, e, row, anchor (la caja del evento), pinned y close()
delay350Milisegundos que el puntero debe estar quieto antes de que se abra la tarjeta
leaveGrace180Milisegundos antes de cerrarla cuando el puntero sale del evento o de la tarjeta
placement'auto''auto', 'above', 'below', 'start' o 'end'
pinfalse'click' o 'dblclick' fija la tarjeta para que los usuarios puedan interactuar con ella
glidetruePasar a otro evento desplaza la tarjeta abierta en lugar de volver a abrirla
src/BookingsWithCards.tsxtsx
import { useMemo } from 'react'
import type { SchedulerEventHoverOptions, SuperScheduler } from 'super-scheduler'
import { SuperSchedulerComponent } from 'super-scheduler/react-render'

// Module-level: the options object keeps its identity across renders.
const BOOKING_CARD: SchedulerEventHoverOptions = {
  delay: 350,
  leaveGrace: 180,
  placement: 'auto',
  // A click pins the card as a non-modal dialog; on touch screens a tap does it.
  pin: 'click',
  render: ({ e, row, pinned, close }) => (
    <article className="booking-card">
      <h3>{e.text()}</h3>
      <p>{row.name}</p>
      <p>
        {e.start().toString('d MMM, HH:mm')} to {e.end().toString('d MMM, HH:mm')}
      </p>
      {pinned && (
        <button type="button" onClick={close}>
          Close
        </button>
      )}
    </article>
  ),
}

export function BookingsWithCards(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
}) {
  const events = useMemo(() => props.events.slice(), [props.events])
  return (
    <SuperSchedulerComponent
      startDate="2026-10-01"
      days={14}
      scale="Day"
      resources={props.resources}
      events={events}
      eventHover={BOOKING_CARD}
    />
  )
}

Cómo se comporta la tarjeta:

  • Una tarjeta sin fijar es un role="tooltip"; una tarjeta fijada es un role="dialog" no modal que toma el foco. Esc o un clic fuera cierra una tarjeta fijada y devuelve el foco al evento.
  • Llevar el puntero a la tarjeta la mantiene abierta. El scroll, el zoom, el arrastre y la selección la ocultan de inmediato.
  • La tarjeta se coloca al abrirse, se voltea o se encoge para caber en la ventana y respeta la preferencia de movimiento reducido.
  • Vive en document.body y lleva el tema del scheduler. Dale estilo con --super-scheduler-hover-padding, -hover-border, -hover-radius, -hover-bg, -hover-color, -hover-shadow y --super-scheduler-z-hover.
  • Las pantallas táctiles no tienen hover: con pin: 'click', un toque abre una tarjeta fijada.

Las tarjetas emergentes son independientes de las burbujas HTML (bubble, bubbleHtml). Las burbujas no pueden contener contenido React; para eso, usa eventHover.

Rendimiento

  • Funciones estables. Define las funciones de render y los objetos de opciones a nivel de módulo, o memorízalos. Una función con identidad nueva vuelve a renderizar todos los slots de ese tipo.
  • Renders baratos. sliceMs es un objetivo para los lotes, no un límite para tu código: una función de render lenta retrasa su lote. No leas el layout ni midas el DOM dentro de las funciones de render; usa width y lod.
  • Componentes memorizados. Envuelve los componentes de los slots en memo y pásales props primitivas, como en el snippet de eventos.
  • Contexto. Un valor de contexto que cambia a menudo vuelve a renderizar todos los slots que lo leen. Mantén el estado que cambia rápido (posición del puntero, temporizadores) fuera de los contextos que consumen los slots.
  • Celdas. En cuadrículas grandes, usa preferentemente las cadenas de onBeforeCellRender en lugar de renderCell.
  • Nada de estado por fotograma. No actualices el estado de React desde onScroll ni desde los handlers de arrastre; la librería hace su trabajo de cada fotograma sin renders de React.

Mide tu propio contenido con el React Profiler: la librería no puede abaratar un componente caro.

Planificación de recursos en una agencia creativaUna diseñadora tiene doble reserva. Pasa dos tareas a un compañero con un solo arrastre, revisa qué desbloquean y deshazlo. Reserva de instrumentos de laboratorioReserva un instrumento y la calibración viene incluida. Saca una sesión del servicio técnico y alarga tu medición.