InteractionS’applique àSuperScheduler Pro
Heures, minutes, jours et zoom
Choisissez la taille des cellules avec scale ('Hour', 'Day', 'Week', 'Month', 'Year', ou 'CellDuration' avec cellDuration en minutes), définissez cellWidth en pixels par cellule et days pour la longueur, et décrivez les lignes d’en-tête avec timeHeaders. Masquez les nuits et les week-ends avec businessBeginsHour, businessEndsHour et showNonBusiness={false}. Pour le zoom, listez des zoomLevels et passez de l’un à l’autre avec control.zoom.setActive, animateTo ou step ; les gestes de pincement et Ctrl/Cmd + molette sont activés par défaut.
L’axe du temps de SuperScheduler Pro est défini par une poignée d’options : ce que représente une cellule (scale), sa largeur (cellWidth), le début et la longueur de la frise (startDate, days), et la façon dont les lignes d’en-tête l’étiquettent (timeHeaders). Le zoom est une liste de telles configurations, zoomLevels, que les utilisateurs atteignent par des gestes et votre code via control.zoom.
Ce guide va des échelles fixes au zoom continu. Lite a un axe fixe en jours ; tout le reste de cette page nécessite Pro.
Échelle, durée et largeur de cellule
scale | Une cellule vaut | Usage typique |
|---|---|---|
'Minute' | 1 minute | Conducteurs d’antenne, séries d’analyses en laboratoire |
'CellDuration' | cellDuration minutes (60 par défaut) | Créneaux de 5, 15 ou 30 minutes ; postes de 240 minutes |
'Hour' | 1 heure | Ateliers, salles de réunion, équipes |
'Day' | 1 jour calendaire | Hôtels, locations, gestion du personnel |
'Week' | 1 semaine calendaire, commençant le jour weekStarts | Projets, campagnes |
'Month' | 1 mois calendaire | Affectations longues, plans de capacité |
'Year' | 1 année calendaire | Vues d’ensemble pluriannuelles |
'Manual' | Les cellules que vous listez dans timeline | Périodes irrégulières |
cellWidth s’exprime en pixels par cellule de l’échelle en cours (40 par défaut) : 44 signifie 44 pixels par jour sur un axe en jours, mais 44 pixels par heure sur un axe en heures. startDate (aujourd’hui par défaut, tronqué à minuit) et days fixent la longueur de la frise.
Quelques configurations typiques :
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 SchedulerPropscellDuration fixe aussi le magnétisme par défaut : avec des cellules de 15 minutes, déplacements, redimensionnements et sélections s’alignent sur les quarts d’heure. La famille d’options snapToGrid désactive le magnétisme geste par geste. Sur un axe en jours, les événements sont dessinés par défaut en cellules entières (useEventBoxes: 'Always') ; définissez useEventBoxes="Never" pour les dessiner à leurs horaires exacts, de sorte qu’un séjour de 14:00 à 11:00 commence et se termine à l’intérieur de ses cellules de jour.
En-têtes de temps
timeHeaders liste les lignes d’en-tête de haut en bas. Chaque ligne groupe le temps selon une unité et peut définir un format de libellé et une height :
groupBy | Groupe par |
|---|---|
'Year', 'Quarter', 'Month', 'Week', 'Day', 'Hour', 'Minute' | Cette unité calendaire |
'Cell' | Un libellé par cellule |
'Default' | cellGroupBy ('Day' par défaut) |
'None' | Un seul libellé pour toute la ligne |
La valeur par défaut est [{ groupBy: 'Default' }, { groupBy: 'Cell' }] : les jours au-dessus des cellules. Chaque ligne d’en-tête mesure headerHeight pixels de haut (30 par défaut), sauf si elle définit sa propre height.
Jetons de format
Les formats utilisent ces jetons ; tout autre caractère est affiché tel quel. Les exemples formatent 2026-10-05T14:30:00 avec la locale en-us.
| Jeton | Résultat | Jeton | Résultat |
|---|---|---|---|
yyyy | 2026 | HH | 14 |
yy | 26 | H | 14 |
MMMM | October | hh | 02 |
MMM | Oct | h | 2 |
MM | 10 | mm | 30 |
M | 10 | m | 30 |
dddd | Monday | ss, s | 00, 0 |
ddd | Mo | tt | PM |
dd, d | 05, 5 | %d | 5 |
Les noms suivent la locale du planificateur ('en-us' par défaut) : 'dddd d MMMM' donne « lunes 5 octubre » avec locale="es-es". Dans plusieurs locales, ddd est une abréviation d’une ou deux lettres (« Mo », « L ») ; utilisez dddd, ou écrivez votre propre libellé dans onBeforeTimeHeaderRender, si vous voulez trois lettres. Libellés, styles, infobulles et zones des en-têtes peuvent tous être personnalisés dans ce hook.
Libellés sur 12 ou 24 heures
timeFormat contrôle les libellés d’heure par défaut : 'Auto' (par défaut) suit la locale (12 heures pour en-us, 24 heures pour la plupart des locales européennes), 'Clock12Hours' et 'Clock24Hours' imposent l’un ou l’autre. Un format explicite sur une ligne d’en-tête l’emporte toujours : 'h:mm tt' pour des libellés sur 12 heures, 'HH:mm' pour des libellés sur 24 heures. Changer le format d’horloge ne change que les libellés ; les horaires des événements ne bougent jamais.
Heures ouvrées et temps masqué
Le temps ouvré est défini par businessBeginsHour (9 par défaut), businessEndsHour (18 par défaut ; 0 signifie minuit à la fin de la journée) et businessWeekends (false par défaut). Avec showNonBusiness à sa valeur par défaut true, les cellules de temps non ouvré sont grisées. Avec showNonBusiness={false}, elles sont retirées de l’axe :
- sur un axe en jours, les jours de week-end disparaissent (14 jours deviennent 10 colonnes) ;
- sur un axe intrajournalier, les heures hors de la plage ouvrée disparaissent : une semaine de travail en heures n’affiche alors que 08:00 à 18:00 chaque jour.
Pour des besoins plus spécifiques, onIncludeTimeCell est appelé pour chaque cellule candidate pendant la construction de la frise : définissez args.cell.visible = false pour supprimer une cellule, ou args.cell.width pour la redimensionner. scale: 'Manual' avec un tableau timeline de cellules { start, end, width } donne un contrôle total.
Niveaux de zoom
Un niveau de zoom est un ensemble nommé d’options appliquées en bloc : en général scale, cellDuration, cellWidth et timeHeaders. Définissez l’échelle des niveaux une seule fois, au niveau du module :
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']Passez-la dans zoomLevels et choisissez le niveau initial avec zoom (un index ou un id). zoomPosition ('left' par défaut, ou 'middle', 'right') détermine quelle partie de la zone visible reste en place quand le niveau change.
Une propriété peut aussi être une fonction de la date d’ancrage, ({ date, level }) => value, par exemple pour n’afficher l’année dans l’en-tête des mois qu’autour du Nouvel An.
Pendant un geste continu, le planificateur choisit le niveau le plus proche du temps par pixel en cours et applique son axe et ses en-têtes dès que l’utilisateur entre dans ce niveau. Les propriétés qui réinitialiseraient la fenêtre, comme days et startDate, ne s’appliquent que lorsque votre code sélectionne explicitement un niveau. L’ordre du tableau n’a pas d’importance pour les gestes, qui mesurent tous les niveaux.
Changer le zoom depuis le code
control.zoom comporte trois méthodes et une propriété :
| Membre | Rôle |
|---|---|
setActive(level, position?, anchorDate?) | Applique un niveau (index ou id) immédiatement, days et startDate compris |
animateTo(target, options?) | Anime vers { level } ou vers une largeur libre { cellWidth } ; renvoie une promesse résolue une fois l’animation stabilisée |
step(delta, options?) | Avance de delta positions dans zoomLevels, dans l’ordre du tableau et avec butée ; sans zoomLevels, multiplie la largeur de cellule par 1,6 à chaque pas |
active | Index du niveau actif, -1 tant qu’aucun niveau n’a été appliqué |
animateTo et step acceptent { duration, position, anchorDate } : duration en millisecondes (300 par défaut, 0 pour aucune animation), et anchorDate sous forme de date, de 'center' ou de 'today' pour garder ce moment en place. Les animations sont instantanées quand l’utilisateur préfère réduire les animations, et ne font rien pendant qu’un geste de zoom est en cours. Un id de niveau inconnu lève une erreur.
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}
/>
</>
)
}Vous devriez voir quatre boutons de niveau ainsi que des boutons plus et moins au-dessus du planning. Appuyer sur « hours » anime l’axe des jours vers les heures autour du centre de la vue, et l’état enfoncé suit aussi les gestes de pincement, car onZoom signale le niveau à la fin de chaque zoom.
onZoom reçoit phase ('start', 'change', 'end'), origin ('gesture' ou 'api'), level, cellWidth, scale, cellDuration, la date d’ancrage, le début de la zone visible et le niveau de détail. Les gestes et animateTo signalent chaque frame ; agissez sur phase === 'end', sauf si vous mettez à jour le DOM directement, et ne mettez jamais à jour le state React à chaque frame.
Gestes
Les gestes de zoom sont activés par défaut : Ctrl ou Cmd avec la molette de la souris (ce qui couvre aussi le pincement au trackpad dans Chrome, Edge et Firefox), le pincement au trackpad dans Safari et le pincement à deux doigts sur écran tactile. Le zoom est continu et ancré sous le pointeur. Réglez-le avec zoomGesture :
| Option | Défaut | Signification |
|---|---|---|
min | cellWidthMin (au moins 1) | Plus petite largeur de cellule en pixels |
max | 400 | Plus grande largeur de cellule en pixels |
wheel | 'ctrl' | 'always' zoome à chaque mouvement vertical de la molette (Maj + molette fait défiler) ; false ne zoome jamais avec la molette |
pinch | true | Pincement au trackpad dans Safari et sur écran tactile |
sensitivity | 1 | Multiplicateur de vitesse |
scales | 'zoomLevels' | Passe d’un de vos niveaux à l’autre ; 'auto' utilise une échelle heure, jour, semaine, mois ; false conserve l’échelle en cours |
link | aucun | Les planificateurs partageant le même identifiant de lien zooment ensemble |
zoomGesture={false} supprime tous les écouteurs de gestes. Comme cellWidth s’entend par cellule, passer d’un niveau en jours à un niveau en heures demande de la place : un jour à 400 pixels ne fait qu’environ 17 pixels par heure. Augmentez donc max (l’exemple utilise 1024) quand votre échelle de niveaux va des jours aux heures.
keyboardOptions={{ zoomKeys: true }}, avec keyboardEnabled, ajoute Ctrl/Cmd avec = ou + (step(1)), - (step(-1)) et 0 (retour au niveau initial) lorsque le focus est dans le planificateur.
Widgets de zoom
super-scheduler/zoom-ui fournit trois widgets DOM facultatifs qui se mettent à jour sans rendu React :
createZoomHud(control, options): un indicateur dans la grille, affiché pendant les gestes et 700 ms après un zoom par l’API ;formatdéfinit son texte.createZoomSlider(control, container, options): un input range natif avec prise en charge du clavier, une échelle logarithmique ou linéaire, et des crans à vos niveaux de zoom ou à des largeurs explicites. Sa valeur s’exprime en pixels par cellule : il convient donc surtout à un axe à échelle unique.createLodBadge(control, container, labels): indique si la vue est en mode détail, compact ou vue d’ensemble.
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}
/>
</>
)
}Chaque widget possède element et dispose(). Créez-les dans un effet qui dépend du contrôle et libérez-les dans sa fonction de nettoyage ; libérer le contrôle les supprime aussi.
Niveau de détail
Quand les utilisateurs dézooment fortement, dessiner chaque libellé en taille réelle serait illisible et lent. Le niveau de détail (lod, activé par défaut) adapte le rendu à l’espace disponible à l’écran et ne change rien à partir de 40 pixels par jour :
- Les événements, en dessous de 40 pixels par jour, s’adaptent à leur propre largeur : contenu complet à partir de 80 pixels, une ligne de texte à partir de 66 pixels, un simple bloc en dessous. Sous 8 pixels par jour, les blocs deviennent des aplats et les événements étroits de fines barres.
- Les cellules affichent leur contenu (HTML, texte, zones) à partir de 24 pixels par cellule. Sous 2 pixels par cellule, il n’y a plus du tout d’éléments de cellule, et
onBeforeCellRendern’est pas appelé. - Les lignes de grille restent espacées d’au moins 8 pixels, le grisé des week-ends et du temps non ouvré nécessite 6 pixels par jour, et les libellés d’en-tête se raccourcissent ou passent à une unité plus grossière quand ils ne tiennent plus.
Chaque seuil peut être modifié via lod={{ ... }} (zoomedOut, eventFull, eventText, eventSolid, cellContent, cellBackground, gridLines, shading, dayLabel, dayNumber, weekLabel, hysteresis), et lod={false} affiche tout littéralement à tous les niveaux de zoom. L’état courant est control.levelOfDetail (level vaut 'full', 'compact' ou 'overview') et il est écrit sous forme d’attributs data-lod sur la racine, pour votre CSS.
Rendez-vous d’un cabinet de kinésithérapieUn patient ne peut pas venir à 10 h. Trouvez le prochain créneau qui respecte pauses et nettoyages. Planification des scènes d’un festivalUne balance mord sur la marge d’un concert. Zoomez à cinq minutes, raccourcissez-la et voyez les salles et équipes dont dépend l’artiste. Planification d’une flotte de locationUne citadine est immobilisée le jour du départ. Confiez son contrat à une autre voiture, gardez le temps de préparation et voyez où la flotte manque.
Étapes suivantes
- Montrez où se trouve la zone visible sur une longue frise : Minimap et métriques.
- Enregistrez le zoom et la position de défilement par utilisateur : Volets et vues enregistrées.
- Gardez le défilement et le zoom rapides avec de gros volumes de données : Performances et virtualisation.
Exemples liés
- FormaRendez-vous d’un cabinet de kinésithérapieUn patient ne peut pas venir à 10 h. Trouvez le prochain créneau qui respecte pauses et nettoyages.
- Aurora LivePlanification des scènes d’un festivalUne balance mord sur la marge d’un concert. Zoomez à cinq minutes, raccourcissez-la et voyez les salles et équipes dont dépend l’artiste.
- FleetlinePlanification d’une flotte de locationUne citadine est immobilisée le jour du départ. Confiez son contrat à une autre voiture, gardez le temps de préparation et voyez où la flotte manque.
- Court ClubRéservation de courts dans un club sportifEn pleine matinée, le filet d’un court de padel lâche. Déplacez le stage, fermez le court et gardez chaque coach dans son service.