Eventos en directoAurora Live (Negocio ficticio)
Planificación de escenarios de un festival
Una 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.
- Escala temporal
- Demo con datos sintéticos
- minutos / horas
- Capacidades Pro
- DependenciasZoom continuoEscala por minutosPanel de detalleTemas
Carga SuperScheduler Pro solo cuando lo pides.
La situación
Aurora Live es un festival ficticio de un día con tres escenarios, dos camerinos, una sala de prensa y cuatro equipos técnicos. El orden de actuaciones se publica semanas antes; lo que producción sigue planificando el mismo día son las pruebas de sonido, encajadas entre actuaciones y con un margen antes de que el grupo salga al escenario.
En una hoja de cálculo cinco minutos desaparecen: una prueba que acaba diez minutos antes de una actuación parece correcta con resolución horaria. El tablero tiene que ampliar hasta el minuto, mantener cada prueba unida a su actuación, rechazar los movimientos sin sentido (una prueba en otro escenario, una actuación ya publicada) y mostrar qué salas y equipos implica cada artista.
Qué vas a hacerAbrirás la hoja de un artista en el panel lateral, ampliarás a celdas de cinco minutos y recortarás una prueba de sonido hasta que su vínculo con la actuación deje de estar en rojo.
Dónde termina la librería y empieza tu aplicación
SuperScheduler aporta
- Línea de tiempo intradía con la noche oculta (horario laborable, sábado incluido) y niveles de zoom de horas a celdas de 15 y 5 minutos, animados con control.zoom.
- Filas agrupadas de escenarios, salas y equipos, y la API de selección de filas que las resalta.
- Vínculos fin-inicio curvos entre cada prueba de sonido y su actuación.
- Arrastre y redimensionado ajustados a la celda activa, sin solapes por escenario, mensajes de rechazo en la tarjeta de arrastre, deshacer/rehacer y movimiento con teclado.
- Personalización visual con propiedades CSS y reglas sin capa sobre la raíz del control (festival.css).
El código de este ejemplo decide
- Qué bloques se mueven (solo las pruebas de sonido), en qué escenario y cuánto pueden acortarse.
- El margen de 20 minutos entre una prueba y su actuación, cuándo su vínculo se vuelve rojo y qué vínculos se dibujan (márgenes cortos y el artista seleccionado, o todos).
- Qué salas y equipos dependen de cada artista, y el panel lateral que salta a su prueba o a su actuación.
- Las etiquetas de cabecera a cinco minutos, la misión y todos los textos de esta página.
El código de esta demo
Son los archivos que ejecuta esta página, no una copia simplificada. Compilan contra los exports públicos de SuperScheduler Pro.
La integración: tres niveles de zoom, horas nocturnas ocultas, las etiquetas de cabecera a cinco minutos, el resaltado de filas con rows.selection y los callbacks donde las reglas rechazan un movimiento o un cambio de duración.
/**
* Aurora Live running order: stages, rooms and crews over one festival day, down to five minutes.
*
* Library: timeline with hidden night, zoom levels from hours to 5-minute cells, tree rows, row
* selection (used to highlight related rows), dependency links, drag/resize with overlap
* prevention, undo history, keyboard, and theming through CSS custom properties (./festival.css).
* Application: which blocks can move, the line-check buffer before each set, the shortest check,
* and which rows relate to an act. Those rules live in ./rules and run inside the callbacks below.
*/
import { useEffect, useMemo, useState, type RefObject } from 'react'
import { SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import type {
SchedulerBeforeTimeHeaderRenderArgs,
SchedulerEventsChangeArgs,
SchedulerProps,
} from 'super-scheduler'
import type { SchedulerHistory } from 'super-scheduler/history'
import { formatCivil } from '../../scheduler/civil'
import { headerLabel, usesTwelveHours } from '../../scheduler/header-format'
import { createHeaderHook } from '../../scheduler/live-headers'
import { toneClass } from '../../scheduler/tones'
import { KIND_TONES, readSlotFields, slotLabel, toResources, type SlotEvent } from './adapter'
import {
FESTIVAL_DAY,
INITIAL_VIEW_START,
OPEN_HOURS,
type ActId,
type RowId,
type Slot,
} from './model'
import { checkTooShort, moveProblem, type MoveProblem } from './rules'
import type { AuroraText, ZoomId } from './text'
import './festival.css'
export interface AuroraSchedulerProps {
readonly events: SlotEvent[]
readonly slots: readonly Slot[]
/** Acts whose line check ends too close to their set. */
readonly shortBuffers: readonly ActId[]
/** Rows that relate to the act in the side panel (highlighted). */
readonly relatedRows: readonly RowId[]
readonly text: AuroraText
/** BCP 47 tag for labels, and the scheduler locale id. */
readonly tag: string
readonly schedulerLocale: string
/** Level on mount. Later changes go through control.zoom so the view animates from where it is. */
readonly initialZoom: ZoomId
/** Every check-to-set link, or only those of the selected act and of short buffers. */
readonly showAllLinks: boolean
readonly selectedAct: ActId | null
readonly history: SchedulerHistory
readonly controlRef: RefObject<SuperScheduler.Scheduler | null>
readonly onEventsChange: (args: SchedulerEventsChangeArgs) => void
readonly onSelect: (id: string) => void
readonly onMoved: (id: string) => void
readonly onResized: (id: string) => void
readonly onRefused: (message: string) => void
readonly onZoomLevel: (level: ZoomId) => void
}
const ZOOM_LEVELS: SuperScheduler.ZoomLevel[] = [
{
id: 'hours',
properties: {
scale: 'Hour',
cellWidth: 80,
timeHeaders: [{ groupBy: 'Day' }, { groupBy: 'Hour' }],
},
},
{
id: 'quarter',
properties: {
scale: 'CellDuration',
cellDuration: 15,
cellWidth: 64,
timeHeaders: [{ groupBy: 'Day' }, { groupBy: 'Cell' }],
},
},
{
id: 'five',
properties: {
scale: 'CellDuration',
cellDuration: 5,
cellWidth: 26,
timeHeaders: [{ groupBy: 'Hour' }, { groupBy: 'Cell' }],
},
},
]
const ZOOM_IDS: readonly ZoomId[] = ['hours', 'quarter', 'five']
/**
* Time headers: the shared Intl labels, except at five minutes where the hour sits above and the
* minutes (":05") below, so 26 px cells stay readable.
*/
function festivalHeaders(tag: string): (args: SchedulerBeforeTimeHeaderRenderArgs) => void {
const shared = createHeaderHook(tag)
return (args) => {
if (args.control.cellDuration !== 5) {
shared(args)
return
}
const ticks = args.header.start.getTime()
const minutes = String(args.header.start.getMinutes()).padStart(2, '0')
args.header.html =
args.header.level === 0
? SuperScheduler.Util.escapeHtml(headerLabel(ticks, 'hour', tag, 'group'))
: `:${minutes}`
}
}
export function AuroraScheduler(props: AuroraSchedulerProps) {
const {
events,
slots,
shortBuffers,
relatedRows,
selectedAct,
text,
tag,
schedulerLocale,
initialZoom,
showAllLinks,
history,
controlRef,
} = props
const { onEventsChange, onSelect, onMoved, onResized, onRefused, onZoomLevel } = props
const resources = useMemo(() => toResources(text), [text])
// Narrower row headers on phones, chosen once on mount.
const rowHeaderWidth = useMemo(
() => (window.matchMedia('(max-width: 639px)').matches ? 116 : 176),
[],
)
// Header labels follow the active level. An animated zoom renders the new level's headers before
// control.cellDuration reports it, so the hook is rebuilt (headers repainted) once a zoom lands.
const [headerHook, setHeaderHook] = useState(() => festivalHeaders(tag))
// Each act's line check leads to its set: a finish-to-start link, red when the buffer is short.
// Morning checks would cross a whole stage row, so by default only the selected act and the
// short buffers show theirs.
const links = useMemo<SuperScheduler.LinkData[]>(() => {
const short = new Set(shortBuffers)
return slots.flatMap((check) => {
const set =
check.kind === 'check'
? slots.find((slot) => slot.kind === 'set' && slot.act === check.act)
: undefined
if (set === undefined || check.act === undefined) return []
if (!showAllLinks && !short.has(check.act) && check.act !== selectedAct) return []
return [
{
id: `${check.act}-buffer`,
from: check.id,
to: set.id,
type: 'FinishToStart' as const,
...(short.has(check.act) ? { cssClass: 'ss-link--late' } : {}),
},
]
})
}, [slots, shortBuffers, showAllLinks, selectedAct])
const config = useMemo<SchedulerProps>(() => {
const short = new Set<string>(shortBuffers)
const related = new Set<string>(relatedRows)
const pattern = usesTwelveHours(tag) ? 'h:mm tt' : 'HH:mm'
const clock = (value: string) => formatCivil(value, tag, { hour: 'numeric', minute: '2-digit' })
const refusal = (problem: MoveProblem) =>
problem === 'published'
? text.rules.published
: problem === 'follows'
? text.rules.follows
: text.rules.wrongStage
return {
locale: schedulerLocale,
startDate: FESTIVAL_DAY,
days: 1,
zoomLevels: ZOOM_LEVELS,
zoom: initialZoom,
zoomPosition: 'left',
// The site opens at noon: the morning stays hidden. A Saturday is a working day here.
businessBeginsHour: OPEN_HOURS.begin,
businessEndsHour: OPEN_HOURS.end,
businessWeekends: true,
showNonBusiness: false,
useEventBoxes: 'Never',
height: '100%',
cssClass: 'al-board',
treeEnabled: true,
treePreventParentUsage: true,
rowHeaderWidth,
rowHeaderWidthAutoFit: false,
eventHeight: 32,
rowMarginTop: 4,
rowMarginBottom: 4,
allowEventOverlap: false,
linkLayer: 'Below',
timeRangeSelectedHandling: 'Disabled',
// Minute-level work: the drag card and header marker show clock times, not dates.
dragCard: {
duration: false,
dateFormat: pattern,
movingDateFormat: pattern,
markerFormat: pattern,
},
durationBarVisible: false,
showToolTip: false,
eventHoverHandling: 'Disabled',
keyboardEnabled: true,
keyboardTarget: 'component',
keyboardMode: 'Full',
history,
controlRef,
onEventsChange,
onBeforeTimeHeaderRender: headerHook,
onBeforeRowHeaderRender: (args) => {
args.row.cssClass = args.row.data.children !== undefined ? 'ss-rh--group' : ''
},
// The selected act's rows are also tinted across the grid (headers come from rows.selection).
onBeforeCellRender: (args) => {
if (related.has(String(args.cell.resource))) args.cell.properties.cssClass = 'al-related'
},
onBeforeEventRender: (args) => {
const fields = readSlotFields(args.data)
if (fields === null) return
const label = SuperScheduler.Util.escapeHtml(slotLabel(fields, text))
const time =
fields.kind === 'set'
? `<span class="ss-evt__meta">${SuperScheduler.Util.escapeHtml(clock(args.data.start.value))}</span>`
: ''
args.data.html = `<span class="ss-evt"><span class="ss-evt__title">${label}</span>${time}</span>`
const classes = ['ss-ev', toneClass(KIND_TONES[fields.kind])]
if (fields.kind === 'set') classes.push('al-set')
if (fields.kind === 'check') classes.push('ss-ev--ghost')
if (fields.kind === 'changeover') classes.push('ss-ev--hatched')
if (fields.kind === 'set' && fields.act !== undefined && short.has(fields.act))
classes.push('ss-ev--late')
args.data.cssClass = classes.join(' ')
},
// Only line checks move, and only on their act's stage; overlap is refused by the library.
onEventMoving: (args) => {
const fields = readSlotFields(args.e.data)
const problem = fields === null ? null : moveProblem(fields, String(args.resource))
if (problem !== null) {
args.allowed = false
args.message = refusal(problem)
}
},
// Final check before the commit (also covers keyboard moves).
onEventMove: (args) => {
const fields = readSlotFields(args.e.data)
const problem = fields === null ? null : moveProblem(fields, String(args.newResource))
if (problem !== null) {
args.preventDefault()
onRefused(refusal(problem))
}
},
onEventResizing: (args) => {
if (checkTooShort(args.start.value, args.end.value)) {
args.allowed = false
args.message = text.rules.tooShort
}
},
onEventResize: (args) => {
if (checkTooShort(args.newStart.value, args.newEnd.value)) {
args.preventDefault()
onRefused(text.rules.tooShort)
}
},
onEventMoved: (args) => onMoved(String(args.e.id())),
onEventResized: (args) => onResized(String(args.e.id())),
onEventClick: (args) => onSelect(String(args.e.id())),
onZoom: (args) => {
if (args.phase !== 'end') return
setHeaderHook(() => festivalHeaders(tag))
const level = ZOOM_IDS[args.level]
if (level !== undefined) onZoomLevel(level)
},
}
}, [
history,
controlRef,
shortBuffers,
relatedRows,
rowHeaderWidth,
onEventsChange,
onMoved,
onRefused,
onResized,
onSelect,
onZoomLevel,
schedulerLocale,
headerHook,
tag,
text,
initialZoom,
])
// Highlighted rows are the control's row selection, driven by the act in the side panel.
useEffect(() => {
const control = controlRef.current
if (control === null || control.disposed()) return
control.rows.selection.clear()
for (const id of relatedRows) {
const row = control.rows.find(id)
if (row !== null) control.rows.selection.add(row)
}
}, [controlRef, relatedRows])
// Imperative and once: a scroll prop would be re-applied on every update.
useEffect(() => {
controlRef.current?.scrollTo(INITIAL_VIEW_START, false, 'left')
}, [controlRef])
return <SuperSchedulerComponent {...config} resources={resources} events={events} links={links} />
}Comparte este escenario
El enlace abre este ejemplo con la vista actual (escala y opciones). Nunca contiene tus cambios ni datos personales.