Converts between a point on the screen and a time on a lane, so marks of your own - a cut-off, a delivery window, a pin - stand at the right place beside the plan.
import { useSchedule } from "@umriss-ui/schedule";
The handle holds three functions and nothing else: clientPointOf(time, lane?), positionAt(clientX, clientY) and visibleDomain(). Everything else the schedule does is props.
Place your marks again whenever view.domain changes.
clientPointOf turns a time into a point for your own mark; positionAt turns a pointer back into a time and a lane.
import { useEffect, useRef, useState } from "react";
import { Stack, Text } from "@umriss-ui/core";
import { useSchedule } from "@umriss-ui/schedule";
import type { ScheduleHandle, Subtask, Task } from "@umriss-ui/schedule";
const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const min = (n: number) => n * 60_000;
const VEHICLES = [
{ id: "truck-118", label: "Truck FP 118 R" },
{ id: "van-402", label: "Van FP 402 R" },
{ id: "truck-520", label: "Truck FP 520 E" },
];
const CONSIGNMENTS: readonly Task[] = [
{ id: "c-2041", name: "C-2041 Holloway Garden Supplies", color: "light-dark(#2563eb, #6b9bff)" },
{ id: "c-2043", name: "C-2043 Oakridge Pharmacy", color: "light-dark(#c2410c, #f08a52)" },
];
const LEGS: readonly Subtask[] = [
{ id: "c-2041-1", task: "c-2041", lane: "truck-118", from: at(6), to: at(7), leadIn: min(15), leadOut: min(10) },
{ id: "c-2041-2", task: "c-2041", lane: "van-402", from: at(8), to: at(10, 30), leadIn: min(30), leadOut: min(15) },
{ id: "c-2043-1", task: "c-2043", lane: "truck-520", from: at(6, 30), to: at(8), leadIn: min(30), leadOut: min(15) },
{ id: "c-2043-2", task: "c-2043", lane: "van-402", from: at(12), to: at(13, 30), leadIn: min(15) },
];
/** The last moment a parcel is taken for delivery today. */
const CUT_OFF = at(14);
const clock = (time: number) => new Date(time).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
export default function PlaceAMark() {
const { Schedule, Lane, Subtasks, view } = useSchedule({ initialView: { domain: [at(5, 30), at(18)] } });
const plan = useRef<ScheduleHandle>(null);
const host = useRef<HTMLDivElement>(null);
const [pin, setPin] = useState<number | null>(null);
const [under, setUnder] = useState("Move the pointer over the plan");
/* The client point, turned into one inside this example - anew whenever the span moves. */
useEffect(() => {
const point = plan.current?.clientPointOf(CUT_OFF);
const left = host.current?.getBoundingClientRect().left;
setPin(point == null || left === undefined ? null : point.x - left);
}, [view.domain]);
return (
<Stack gap={2}>
<div ref={host} style={{ position: "relative", height: 12 }}>
{pin !== null && (
<Text size="xs" tone="muted" style={{ position: "absolute", top: -4, left: pin, transform: "translateX(-50%)", whiteSpace: "nowrap" }} data-pin>
Same-day cut-off
</Text>
)}
</div>
<Schedule
ref={plan}
ariaLabel="Three vehicles with a cut-off of the application's own"
height={196}
onInteraction={(interaction) => {
/* The interaction carries time and lane already; `positionAt` is for
listeners of your own, which have only a point. */
const there = plan.current?.positionAt(interaction.clientX, interaction.clientY);
setUnder(there == null ? "off the plan" : `${clock(there.time)} on ${there.lane ?? "no lane"}`);
}}
>
{VEHICLES.map((vehicle) => (
<Lane key={vehicle.id} id={vehicle.id} label={vehicle.label} />
))}
<Subtasks data={LEGS} tasks={CONSIGNMENTS} />
</Schedule>
<Text size="sm" mono tone="secondary" data-under-pointer>
{under}
</Text>
</Stack>
);
}
The keys of First schedule apply here.
From ScheduleProps — the full table stands on First schedule.
| Name | Type | Default | Description |
|---|---|---|---|
ref | Ref<ScheduleHandle> | — | Receives the ScheduleHandle - not an element. |
height | number | 400 | Height in CSS pixels, both bands included. The width is the host's. |
children | ReactNode | — | Lane, Dependencies and Subtasks, in the order they are to stand and
be drawn. |
| Name | Type | Default | Description |
|---|---|---|---|
onInteraction | (interaction: ScheduleInteraction) => void | — | Click, context menu and hover, with what the pointer is on and where. While it is set, a right-click opens no browser menu. |
| Name | Type | Default | Description |
|---|---|---|---|
ariaLabelrequired | string | — | What the schedule shows, for a screen reader: "Plan of week 38". |
From LaneProps — the full table stands on Lanes.
From SubtasksProps — the full table stands on Subtasks.
From Subtask — the full table stands on Subtasks.
| Name | Type | Default | Description |
|---|---|---|---|
idrequired | string | — | The caller's identity of the subtask. |
taskrequired | string | — | The id of the task it belongs to. |
lanerequired | string | — | The id of the lane it sits on. |
fromrequired | number | — | Start of the main time. |
torequired | number | — | End of the main time. |
leadIn | number | — | Duration of the lead-in before the main time; none is zero. |
leadOut | number | — | Duration of the lead-out after the main time; none is zero. |
From Task — the full table stands on Subtasks.
From ScheduleInteraction — the full table stands on Interactions.
From ScheduleOptions — the full table stands on View.
| Name | Type | Default | Description |
|---|---|---|---|
initialView | ScheduleView | — | The view to start from, read once when the hook first runs; a different
value on a later render is ignored, as React ignores a changed
defaultValue. To go to a view later, call setView. The schedule
remembers none: where a view is kept is the application's decision. |