Módulos ProSe aplica aSuperScheduler Pro
Paneles coordinados y vistas guardadas
Sustituye SuperSchedulerComponent por SchedulerPanes de super-scheduler/panes y describe cada panel con un id y resources o un rowFilter; los paneles comparten el scroll horizontal, el zoom y el ancho de la cabecera de fila, se desplazan en vertical cada uno por su cuenta, y los eventos se pueden arrastrar de uno a otro. Para las vistas guardadas, getViewState(control) devuelve un objeto apto para JSON con el zoom, la posición de scroll, la densidad, las filas plegadas y las columnas, y applyViewState lo restaura; dónde se guarda lo decide tu aplicación.
En cualquier pantalla de planificación grande aparecen dos necesidades. La primera es mantener parte de las filas a la vista mientras el resto se desplaza: una bandeja de «sin asignar» bajo las habitaciones, un equipo encima de sus máquinas. La segunda es volver más tarde a la misma vista: el zoom, la fecha y las filas que el usuario estaba mirando. super-scheduler/panes y super-scheduler/views las cubren, y los dos requieren SuperScheduler Pro.
Dividir una línea de tiempo en paneles
SchedulerPanes renderiza varios schedulers apilados sobre una misma línea de tiempo. Comparten la posición de scroll horizontal, el zoom y el ancho de la cabecera de fila; cada panel se desplaza en vertical por su cuenta y tiene su propia altura. Los divisores entre paneles permiten cambiar su tamaño.
Sustituye a SuperSchedulerComponent: pasas una sola vez las mismas props del scheduler, más un array panes y una altura total height.
import { useCallback, useMemo, useRef, useState } from 'react'
import type { SchedulerEventsChangeArgs, SchedulerProps, SuperScheduler } from 'super-scheduler'
import { SchedulerPanes } from 'super-scheduler/panes'
import type { SchedulerPane, SchedulerPanesHandle } from 'super-scheduler/panes'
import 'super-scheduler/styles.css'
const isTray = (resource: SuperScheduler.ResourceData) => resource.kind === 'tray'
// Module-level (or memoized): a new `panes` array resets the sizes the user dragged.
const PANES: SchedulerPane[] = [
{ id: 'rooms', rowFilter: (resource) => !isTray(resource), minSize: 200 },
{
id: 'tray',
rowFilter: isTray,
size: 140,
minSize: 96,
// Per-pane overrides: smaller events in the unassigned tray.
props: { eventHeight: 28 },
},
]
export function RoomsWithTray(props: {
resources: SuperScheduler.ResourceData[]
initial: SuperScheduler.EventData[]
}) {
const [events, setEvents] = useState(props.initial)
const panesRef = useRef<SchedulerPanesHandle>(null)
// One list for every pane: each event appears in the pane that holds its resource.
const onEventsChange = useCallback(
(args: SchedulerEventsChangeArgs) => setEvents([...args.events]),
[],
)
const shared = useMemo<Partial<SchedulerProps>>(
() => ({
onEventMove: (args) => {
// `pane` is where the event lands; `sourcePane` is set only for a move between panes.
if (args.sourcePane !== 'rooms' || args.pane !== 'tray') return
// Unassigning a booking asks first; the drop waits for the answer.
args.async = true
void confirmWithUser(`Unassign ${args.e.text()}?`).then((ok) => {
if (!ok) args.preventDefault()
args.loaded()
})
},
}),
[],
)
return (
<>
<button type="button" onClick={() => panesRef.current?.scrollTo('2026-10-01', 'left')}>
Go to 1 October
</button>
<SchedulerPanes
{...shared}
panesRef={panesRef}
panes={PANES}
// Total height of every pane, the splitter and the shared header.
height={640}
resources={props.resources}
events={events}
onEventsChange={onEventsChange}
splitter={{ size: 6, step: 8 }}
startDate="2026-10-01"
days={60}
scale="Day"
cellWidth={44}
/>
</>
)
}Deberías ver las habitaciones arriba y, debajo, una bandeja de «sin asignar» de 140 px, con una sola cabecera de tiempo en la parte superior. Desplaza cualquiera de los paneles en horizontal y el otro lo sigue. Arrastra una reserva de la bandeja a una habitación y se mueve allí; arrastra una de una habitación a la bandeja y la aplicación pregunta antes.
Opciones de cada panel
| Campo | Por defecto | Efecto |
|---|---|---|
id | obligatorio | Identifica el panel en los handlers (args.pane), en panesRef y en el DOM (data-pane) |
resources | Las filas de este panel | |
rowFilter | Elige las filas de este panel entre los resources compartidos; usa esto o resources, no ambos | |
size | 'auto' | Píxeles, un porcentaje de la altura libre ('30%') o 'auto' para una parte de lo que queda |
minSize | 48 | Altura mínima en píxeles; los mínimos prevalecen cuando el total es demasiado pequeño |
hidden | false | Oculta el panel pero lo mantiene montado, así que volver a mostrarlo no cuesta nada |
props | Props solo para este panel; los handlers definidos aquí sustituyen a los compartidos |
Las filas se asignan por recurso de nivel superior: un padre se lleva a sus hijos a su panel. El primer panel sin resources ni rowFilter recibe todos los recursos de nivel superior que no se han llevado los demás paneles.
Disposición y divisor
| Prop | Por defecto | Efecto |
|---|---|---|
height | obligatorio | Altura total en píxeles: todos los paneles, los divisores y la cabecera compartida |
timeHeader | 'first' | 'first' muestra la cabecera de tiempo solo en el primer panel visible; 'all', en todos los paneles |
scrollbar | 'last' | Barra de scroll horizontal solo en el último panel, o 'all' |
splitter | true | { size, step } fija su grosor (6 px) y su paso con el teclado (8 px); false lo elimina |
onPaneResize | { sizes } por id de panel, después de confirmar un cambio de tamaño |
El divisor es enfocable y tiene role="separator"; su valor es la altura del panel que queda debajo. Flecha arriba y Flecha abajo lo mueven step píxeles, Mayús+Flecha arriba y Mayús+Flecha abajo, 40 px; Inicio y Fin lo llevan a los límites, e Intro o un doble clic restauran los tamaños declarados. Mientras arrastras, se muestra una vista previa de los paneles; sus alturas cambian al soltar. En 0.1.0, su nombre accesible es el inglés «Pane size», sin ninguna opción para traducirlo.
Eventos en paneles
Pasa todos los eventos una sola vez. Cada panel muestra los eventos cuyo resource es una de sus filas, y un evento pasa a otro panel cuando su recurso lo hace.
- Controlados:
eventsmásonEventsChange. El handler recibe la lista completa y combinada enargs.events, yargs.panecon el panel donde se produjo el cambio. Adóptala como en estado controlado. - No controlados:
defaultEvents, y los paneles mantienen la lista por su cuenta.
Cambiar directamente el control.events.list de un panel no se comparte con los demás paneles; hazlo a través del estado o de la API control.events.
Movimientos entre paneles
Arrastrar entre paneles está activado por defecto (crossPaneMove: true); false mantiene cada evento en su panel. Con el valor por defecto eventMoveHandling: 'Update', un movimiento entre paneles se comunica una sola vez, como un cambio 'move' en onEventsChange.
Todos los handlers compartidos reciben args.pane. En un movimiento entre paneles, onEventMove y onEventMoved reciben además args.sourcePane, así que una regla puede depender de la dirección: el fragmento pide confirmación solo para los movimientos de las habitaciones a la bandeja, con args.async y args.loaded(). Un movimiento cancelado o rechazado deja los datos sin cambios.
Acceder al control de cada panel
SchedulerPanes crea los schedulers, así que te da sus controles a través de panesRef:
controls: un mapa del id de panel al control, ycontrol(id)para obtener uno de ellos;forEach(run)para llamar a algo en todos los paneles;scrollTo(date, position)para desplazarlos juntos;update(options)para aplicar opciones a todos los paneles.
Para usar los slots de renderizado React dentro de los paneles, pasa el componente de ese punto de entrada: component={SuperSchedulerComponent} importado de super-scheduler/react-render. El módulo de paneles no lo importa a menos que lo hagas tú.
Vincular schedulers que colocas tú
Cuando los schedulers no están apilados (un plan de personal en la parte superior de la página y un plan de salas más abajo), conserva tus propios componentes y vincula sus controles con linkPanes:
import { useEffect, useMemo } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { linkPanes } from 'super-scheduler/panes'
// Two schedulers placed by your own layout (here, a page section apart) that move together.
export function LinkedBoards(props: {
staff: SuperScheduler.ResourceData[]
rooms: SuperScheduler.ResourceData[]
shifts: SuperScheduler.EventData[]
bookings: SuperScheduler.EventData[]
}) {
const staff = useSchedulerControl()
const rooms = useSchedulerControl()
const shifts = useMemo(() => props.shifts.slice(), [props.shifts])
const bookings = useMemo(() => props.bookings.slice(), [props.bookings])
useEffect(() => {
if (staff.control === null || rooms.control === null) return
// Horizontal scroll always; zoom and row header width too unless turned off.
const link = linkPanes([staff.control, rooms.control], { zoom: true, rowHeaderWidth: true })
return () => link.dispose()
}, [staff.control, rooms.control])
return (
<>
<h2>Staff</h2>
<SuperSchedulerComponent
controlRef={staff.controlRef}
startDate="2026-10-01"
days={30}
scale="Day"
resources={props.staff}
events={shifts}
/>
<h2>Rooms</h2>
<SuperSchedulerComponent
controlRef={rooms.controlRef}
startDate="2026-10-01"
days={30}
scale="Day"
resources={props.rooms}
events={bookings}
/>
</>
)
}El scroll horizontal siempre se comparte. El zoom y el ancho de la cabecera de fila se comparten salvo que pases zoom: false o rowHeaderWidth: false. Llama a dispose() para desvincularlos.
Guardar y restaurar una vista
Una vista es cómo el usuario está mirando los datos, no los datos en sí. getViewState(control, include?) la captura como un objeto pequeño y apto para JSON; applyViewState(control, state, options?) la restaura.
Clave en include | Campos guardados | Notas |
|---|---|---|
'zoom' | cellWidth, zoomLevel | zoomLevel es el índice del nivel activo en zoomLevels, así que mantén estable su orden |
'scroll' | anchorDate, topRowId, topOffset | La fecha del borde izquierdo y la fila superior, por id, con el desplazamiento dentro de ella |
'density' | density | Solo cuando defines la prop density |
'collapsed' | collapsed | Ids de los padres del árbol que están plegados |
'columns' | columnWidths, columnOrder | Ancho y orden de las columnas de la cabecera de fila |
Todos los estados tienen v: 1. Sin include, se capturan las cinco claves.
import type { SuperScheduler } from 'super-scheduler'
import { applyViewState, getViewState } from 'super-scheduler/views'
import type { SchedulerViewState, ViewStateKey } from 'super-scheduler/views'
// What this application restores from the view. Density and columns stay in React state here.
const KEYS: readonly ViewStateKey[] = ['zoom', 'scroll', 'collapsed']
const storageKey = (user: string, view: string) => `planning-view:${user}:${view}`
/** Saves the current view. The application owns storage: here localStorage, per user. */
export function saveView(control: SuperScheduler.Scheduler, user: string, view: string): void {
const state = getViewState(control, KEYS)
try {
localStorage.setItem(storageKey(user, view), JSON.stringify(state))
} catch {
// Storage can be full or disabled; a view is a convenience, not data.
}
}
/** Stored values are untrusted input: check the shape before using them. */
function isViewState(value: unknown): value is SchedulerViewState {
return typeof value === 'object' && value !== null && (value as { v?: unknown }).v === 1
}
/** Restores a saved view. Resolves false when nothing was saved or the rows never appeared. */
export async function restoreView(
control: SuperScheduler.Scheduler,
user: string,
view: string,
): Promise<boolean> {
let saved: unknown = null
try {
saved = JSON.parse(localStorage.getItem(storageKey(user, view)) ?? 'null')
} catch {
return false
}
if (!isViewState(saved)) return false
// Waits (up to 5 s) for the rows and the saved top row, for data that loads after mount.
return applyViewState(control, saved, { when: 'rows', timeout: 5000 })
}import { useEffect, useMemo } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { restoreView, saveView } from './saved-view'
const ZOOM_LEVELS: SuperScheduler.ZoomLevel[] = [
{ id: 'weeks', properties: { scale: 'Week', cellWidth: 120 } },
{ id: 'days', properties: { scale: 'Day', cellWidth: 44 } },
]
export function PlannerWithViews(props: {
user: string
resources: SuperScheduler.ResourceData[]
events: SuperScheduler.EventData[]
}) {
const { controlRef, control } = useSchedulerControl()
const events = useMemo(() => props.events.slice(), [props.events])
// Restore once the control exists; keep row ids stable so the top row can be found again.
useEffect(() => {
if (control !== null) void restoreView(control, props.user, 'default')
}, [control, props.user])
return (
<>
<button
type="button"
disabled={control === null}
onClick={() => control && saveView(control, props.user, 'default')}
>
Save this view
</button>
<SuperSchedulerComponent
controlRef={controlRef}
startDate="2026-01-01"
days={365}
zoomLevels={ZOOM_LEVELS}
zoom="days"
treeEnabled
resources={props.resources}
events={events}
/>
</>
)
}Desplázate hasta una fecha, pliega una planta, pulsa «Save this view» y recarga: el planificador vuelve a la misma fecha y fila, con la planta plegada.
Cómo funciona la restauración:
when: 'rows'(por defecto) espera a que existan las filas y la fila superior guardada; eso cubre los datos que llegan después del montaje. Si no aparecen dentro detimeout(5.000 ms por defecto), la promesa se resuelve confalse.when: 'now'aplica el estado de inmediato; si la fila superior guardada ya no existe, el desplazamiento guardado se usa como posición de scroll absoluta.animate: trueanima el cambio de zoom.- Los padres que figuran en
collapsedse pliegan, y todos los demás padres se despliegan. - Si las columnas guardadas ya no coinciden con
rowHeaderColumns(otro número de columnas), no se aplica nada y la promesa se resuelve confalse. Un estado con una versión distinta de 1 también se resuelve confalse. - Restaurar no mueve el foco del teclado.
Con paneles, guarda y restaura a través del control de un panel (panesRef.current?.control('rooms')): el zoom y el scroll horizontal son compartidos, mientras que el scroll vertical y las filas plegadas pertenecen a ese panel.
Qué le corresponde a tu aplicación
- El almacenamiento.
localStoragepara un solo navegador, o tu backend para seguir al usuario entre dispositivos. La librería nunca guarda nada. - Nombres y uso compartido. Vistas con nombre, vistas por defecto por equipo, enlaces que abren una vista.
- La validación. Las vistas guardadas son entrada no fiable: comprueba la forma y
vantes de aplicarlas, y descarta las que no pasen. - Ids estables. Los ids de fila tienen que corresponder a las mismas filas de una sesión a otra para que
topRowIdycollapsedfuncionen. - Tamaños de panel y selecciones. Ninguno de los dos forma parte de una vista; guarda los tamaños de panel desde
onPaneResizesi quieres recuperarlos.
Relacionado
- Árboles de recursos, columnas y selección para las filas plegadas y las columnas que guarda una vista.
- Escalas de tiempo y zoom para los niveles de zoom que restaura una vista.