A day band that names the days and a time band whose ticks follow the zoom, from quarter hours to days. A working calendar cuts the hours nobody works out of the axis.
import { useSchedule } from "@umriss-ui/schedule";
The calendar is a list of intervals in which time counts, nothing more; deriving it from opening hours and holidays is the application's business. A dotted line marks each seam, and a drag that would end in removed time stops at the seam where time counts again.
A week of the on-call rota: the day band names each day, and the time band's step follows the zoom.
import { useSchedule } from "@umriss-ui/schedule";
import type { Subtask, Task } from "@umriss-ui/schedule";
/* Data from the operations world, written out here so the example runs on its own. */
const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
interface Engineer {
id: string;
name: string;
team: string;
}
const ENGINEERS: readonly Engineer[] = [
{ id: "priya", name: "Priya Raman", team: "Payments" },
{ id: "jonas", name: "Jonas Keller", team: "Payments" },
{ id: "ada", name: "Ada Mwangi", team: "Identity" },
{ id: "tomasz", name: "Tomasz Nowak", team: "Discovery" },
{ id: "leila", name: "Leila Haddad", team: "Discovery" },
{ id: "sam", name: "Sam Okafor", team: "Messaging" },
{ id: "ines", name: "Ines Duarte", team: "Integrations" },
{ id: "felix", name: "Felix Brandt", team: "Insights" },
];
interface OnCall {
id: string;
engineer: string;
rotation: "primary" | "secondary";
from: number;
to: number;
}
/** Monday 16 to Monday 23 March, handed over every morning at 09:00: the
secondary of one day is the primary of the next. */
const ONCALL: readonly OnCall[] = Array.from({ length: 7 }, (_, day) => [
{ rotation: "primary" as const, engineer: ENGINEERS[day % ENGINEERS.length]!.id },
{ rotation: "secondary" as const, engineer: ENGINEERS[(day + 1) % ENGINEERS.length]!.id },
].map(({ rotation, engineer }) => ({
id: `${rotation}-${16 + day}`,
engineer,
rotation,
from: at(16 + day, 9),
to: at(17 + day, 9),
}))).flat();
const ROTATIONS: Task[] = [
{ id: "primary", name: "Primary", color: "light-dark(#2563eb, #6b9bff)" },
{ id: "secondary", name: "Secondary", color: "light-dark(#0d9488, #3cc7b8)" },
];
const DUTIES: Subtask[] = ONCALL.map((duty) => ({ id: duty.id, task: duty.rotation, lane: duty.engineer, from: duty.from, to: duty.to }));
const WEEK: readonly [number, number] = [new Date(2026, 2, 16).getTime(), new Date(2026, 2, 23, 12).getTime()];
export default function AWeek() {
const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: WEEK } });
return (
<Schedule ariaLabel="On-call rota, 16 to 23 March" height={420}>
{ENGINEERS.map((engineer) => (
<Lane key={engineer.id} id={engineer.id} label={engineer.name} />
))}
<Subtasks data={DUTIES} tasks={ROTATIONS} />
</Schedule>
);
}
The depot works 06:00 to 22:00: calendar lists those hours, the nights leave the axis, and a drag into one stops at the next morning.
import { useState } from "react";
import { applyIntent, useSchedule } from "@umriss-ui/schedule";
import type { Subtask, Task, Dependency } from "@umriss-ui/schedule";
/* Three days in the width of two; a dotted line marks each seam. Deriving the
list from opening hours and holidays is the application's business. */
const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, 16 + day, hours, minutes).getTime();
const OPENING_HOURS = [0, 1, 2].map((day) => ({ from: at(day, 6), to: at(day, 22) }));
const TOURS: Task[] = [{ id: "t-09", name: "T-09 Line haul East", color: "light-dark(#c2410c, #f08a52)" }];
const LEGS: Subtask[] = [
{ id: "line-haul", task: "t-09", lane: "truck", from: at(0, 18), to: at(0, 21, 30), leadIn: 30 * 60_000 },
{ id: "round", task: "t-09", lane: "van-1", from: at(1, 6, 30), to: at(1, 11), leadIn: 15 * 60_000 },
{ id: "returns", task: "t-09", lane: "van-2", from: at(1, 20), to: at(2, 9), leadOut: 30 * 60_000 },
];
const HANDOVERS: Dependency[] = [
{ id: "unload", from: "line-haul", to: "round", lag: 20 * 60_000 },
{ id: "collect", from: "round", to: "returns", lag: 45 * 60_000 },
];
export default function WorkingCalendar() {
const { Schedule, Lane, Subtasks, Dependencies } = useSchedule({ initialView: { domain: [at(0, 6), at(2, 22)] } });
const [legs, setLegs] = useState<readonly Subtask[]>(LEGS);
return (
<Schedule
ariaLabel="Three days at East Gate depot, nights removed"
calendar={OPENING_HOURS}
height={220}
intents={["move"]}
onIntent={(intent) => setLegs((current) => current.map((leg) => applyIntent(leg, intent)))}
>
<Lane id="truck" label="Truck FP 520 E" />
<Lane id="van-1" label="Van FP 290 E" />
<Lane id="van-2" label="E-van FP 311 E" />
<Dependencies data={HANDOVERS} />
<Subtasks data={legs} tasks={TOURS} />
</Schedule>
);
}
Weekdays 09:00 to 17:00 as the calendar: a two-week sprint reads as ten working days, with the evenings and the weekend gone.
import { useSchedule } from "@umriss-ui/schedule";
/* Data from the planning world, written out here so the example runs on its own. */
const at = (day: number, hours = 9, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
interface Person {
id: string;
name: string;
role: "Developer" | "Designer" | "Product manager" | "QA engineer";
team: "Web" | "Apps";
/** Hours a week they can be planned for. */
capacity: number;
}
const PEOPLE: readonly Person[] = [
{ id: "maya", name: "Maya Lindgren", role: "Product manager", team: "Web", capacity: 32 },
{ id: "arjun", name: "Arjun Mehta", role: "Developer", team: "Web", capacity: 40 },
{ id: "chloe", name: "Chloe Durand", role: "Developer", team: "Web", capacity: 40 },
{ id: "noah", name: "Noah Fischer", role: "Designer", team: "Web", capacity: 24 },
{ id: "eva", name: "Eva Novak", role: "QA engineer", team: "Web", capacity: 40 },
{ id: "luis", name: "Luis Moreno", role: "Product manager", team: "Apps", capacity: 40 },
{ id: "hana", name: "Hana Sato", role: "Developer", team: "Apps", capacity: 40 },
{ id: "kofi", name: "Kofi Mensah", role: "Developer", team: "Apps", capacity: 32 },
{ id: "freya", name: "Freya Olsen", role: "Designer", team: "Apps", capacity: 40 },
{ id: "david", name: "David Kowalski", role: "QA engineer", team: "Apps", capacity: 20 },
];
interface Project {
id: string;
name: string;
client: string;
color: string;
}
const PROJECTS: readonly Project[] = [
{ id: "shop", name: "Online shop relaunch", client: "Pembury Home", color: "light-dark(#2563eb, #6b9bff)" },
{ id: "booking", name: "Booking app", client: "Saltmarsh Clinics", color: "light-dark(#0d9488, #3cc7b8)" },
{ id: "portal", name: "Member portal", client: "Rowan Credit Union", color: "light-dark(#7c3aed, #a98bfa)" },
{ id: "intranet", name: "Intranet", client: "Tidewell (internal)", color: "light-dark(#c2410c, #f08a52)" },
];
/** A work item, shaped as `@umriss-ui/schedule`'s `Subtask`. */
interface WorkItem {
id: string;
/** The project. */
task: string;
/** The person. */
lane: string;
from: number;
to: number;
name: string;
sprint: string;
/** Hours, as estimated at planning. */
estimate: number;
status: "to do" | "in progress" | "in review" | "done";
}
const item = (id: string, task: string, lane: string, from: number, to: number, name: string, estimate: number, status: WorkItem["status"]): WorkItem =>
({ id, task, lane, from, to, name, sprint: "sprint-14", estimate, status });
const WORK: readonly WorkItem[] = [
item("w-101", "portal", "arjun", at(9), at(11, 17), "Sign-in with e-mail code", 18, "done"),
item("w-102", "portal", "arjun", at(12), at(17, 17), "Profile page", 26, "in progress"),
item("w-103", "portal", "chloe", at(9), at(10, 17), "Session handling", 12, "done"),
item("w-104", "shop", "chloe", at(11), at(13, 17), "Basket keeps items across devices", 20, "in review"),
item("w-105", "portal", "chloe", at(16), at(19, 17), "Change of address form", 24, "in progress"),
item("w-106", "portal", "noah", at(9), at(12, 13), "Profile page design", 14, "done"),
item("w-107", "shop", "noah", at(16), at(18, 17), "Search results layout", 12, "to do"),
item("w-108", "portal", "eva", at(12), at(13, 17), "Test plan for sign-in", 10, "done"),
item("w-109", "portal", "eva", at(17), at(20, 17), "Regression run", 20, "to do"),
item("w-110", "booking", "hana", at(9), at(13, 17), "Reminder scheduling service", 32, "done"),
item("w-111", "booking", "hana", at(16), at(20, 17), "Push notifications", 30, "in progress"),
item("w-112", "booking", "kofi", at(10), at(12, 17), "Calendar sync", 18, "done"),
item("w-113", "intranet", "kofi", at(16), at(18, 17), "News feed", 16, "in progress"),
item("w-114", "booking", "freya", at(9), at(11, 17), "Reminder settings screen", 16, "done"),
item("w-115", "booking", "freya", at(18), at(20, 17), "Store screenshots", 12, "to do"),
item("w-116", "booking", "david", at(16), at(19, 13), "Device test matrix", 14, "to do"),
];
const WORKING_DAYS = [9, 10, 11, 12, 13, 16, 17, 18, 19, 20];
const OFFICE_HOURS = WORKING_DAYS.map((day) => ({ from: at(day, 9), to: at(day, 17) }));
const WEB_TEAM = PEOPLE.filter((person) => person.team === "Web");
const ON_THE_WEB_TEAM = new Set(WEB_TEAM.map((person) => person.id));
export default function OfficeHours() {
const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(9, 9), at(20, 17)] } });
return (
<Schedule
ariaLabel="Sprint 14 of the web team, office hours only"
calendar={OFFICE_HOURS}
height={280}
label={(item) => item.name ?? item.id}
>
{WEB_TEAM.map((person) => (
<Lane key={person.id} id={person.id} label={person.name} />
))}
<Subtasks data={WORK.filter((item) => ON_THE_WEB_TEAM.has(item.lane))} tasks={PROJECTS} />
</Schedule>
);
}
The keys of First schedule apply here.
From ScheduleProps — the full table stands on First schedule.
| Name | Type | Default | Description |
|---|---|---|---|
height | number | 400 | Height in CSS pixels, both bands included. The width is the host's. |
calendar | CalendarInput | [] | A working calendar: the intervals in which time counts. Nights and weekends outside them are cut out of the axis. Without intervals every hour counts, as on the wall clock. |
intents | readonly IntentKind[] | [] | The intents the caller handles. Each one enables its interaction; none leaves a read-only schedule (ADR-0023). |
label | (subtask: Subtask) => string | — | What stands written in a bar: a function from a subtask to a line of text, or nothing for bars without text. The text is cut off with an ellipsis where the bar is too narrow for it and left out where even that would say nothing; a bar that began before the view keeps its text at the view's edge. |
children | ReactNode | — | Lane, Dependencies and Subtasks, in the order they are to stand and
be drawn. |
| Name | Type | Default | Description |
|---|---|---|---|
onIntent | (intent: Intent) => void | — | Called once per intent when a drag ends. The data changes only if the caller changes it. |
| 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. |
name | string | — | What the subtask is called, where the schedule names it. |
From Task — the full table stands on Subtasks.
From DependenciesProps — the full table stands on Dependencies.
| Name | Type | Default | Description |
|---|---|---|---|
datarequired | readonly Dependency[] | — | The dependencies, as the caller holds them. |
From Dependency — the full table stands on Dependencies.
| Name | Type | Default | Description |
|---|---|---|---|
idrequired | string | — | The caller's identity of the dependency. |
fromrequired | string | — | The id of the subtask it leaves. |
torequired | string | — | The id of the subtask it arrives at. |
lagrequired | number | — | The least time between the two anchors; zero where the successor may begin as soon as the predecessor ends. |
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. |