Leave, maintenance and other time a lane is not available: hatched behind the work, a finding where work covers it, and closed to a drag.
import { useSchedule, findings } from "@umriss-ui/schedule";
Blocked time is data beside the work: one list, each interval naming its lane, as a leave table or a maintenance plan holds it. findings takes the same list as its third argument.
Work the data already put into blocked time is never locked there: it may be moved within it and out of it, and the finding stays until it is.
A person on leave, a server in maintenance: BlockedTimes hatches that time on the lane, and work planned into it is a finding.
import { Stack, Text } from "@umriss-ui/core";
import { findings, useSchedule } from "@umriss-ui/schedule";
import type { BlockedTime, Subtask, Task } from "@umriss-ui/schedule";
/* Blocked time is data beside the work: one list, each interval naming its
lane, as a leave table or a maintenance plan holds it. The schedule draws it
behind everything on the lane - hatched, in the muted ink, in forced colours
too - and a subtask that covers some of it is marked like an overlap, named
in its tooltip and counted in what a screen reader hears.
`findings` takes the same list as its third argument and returns every
subtask in blocked time with the interval it covers. */
const day = (d: number, hours = 0) => new Date(2026, 2, 16 + d, hours).getTime();
const PEOPLE = [
{ id: "anna", label: "Anna Berg" },
{ id: "ben", label: "Ben Ortiz" },
{ id: "chloe", label: "Chloe Lind" },
{ id: "build", label: "Build server" },
];
const PROJECTS: readonly Task[] = [
{ id: "portal", name: "Customer portal", color: "light-dark(#2563eb, #6b9bff)" },
{ id: "billing", name: "Billing rework", color: "light-dark(#0d9488, #3cc7b8)" },
];
const WORK: readonly Subtask[] = [
{ id: "portal-design", task: "portal", lane: "anna", from: day(0, 9), to: day(1, 17), name: "Design" },
/* Planned into Anna's leave - the finding. */
{ id: "portal-review", task: "portal", lane: "anna", from: day(2, 13), to: day(3, 12), name: "Design review" },
{ id: "billing-api", task: "billing", lane: "ben", from: day(0, 9), to: day(2, 17), name: "Invoice API" },
{ id: "portal-front", task: "portal", lane: "chloe", from: day(1, 9), to: day(4, 17), name: "Front end" },
{ id: "billing-ci", task: "billing", lane: "build", from: day(3, 9), to: day(3, 15), name: "Release build" },
];
const BLOCKED: readonly BlockedTime[] = [
{ id: "anna-leave", lane: "anna", from: day(3, 0), to: day(5, 0), label: "Leave" },
{ id: "build-maintenance", lane: "build", from: day(1, 18), to: day(2, 12), label: "Maintenance" },
];
const time = (instant: number) =>
new Date(instant).toLocaleString("en-GB", { weekday: "short", hour: "2-digit", minute: "2-digit" });
export default function Leave() {
const { Schedule, Lane, Subtasks, BlockedTimes } = useSchedule({ initialView: { domain: [day(0, 6), day(5, 0)] } });
const found = findings(WORK, [], BLOCKED);
return (
<Stack gap={3}>
<Schedule ariaLabel="Team plan, week of 16 March" height={240}>
{PEOPLE.map((person) => (
<Lane key={person.id} id={person.id} label={person.label} />
))}
<BlockedTimes data={BLOCKED} />
<Subtasks data={WORK} tasks={PROJECTS} />
</Schedule>
<ul style={{ margin: 0, paddingLeft: 18 }} data-blocked-findings>
{found.inBlockedTime.map((f) => (
<li key={`${f.subtask}:${f.blocked}`}>
<Text size="sm" mono>
{`${f.subtask} in ${f.blocked}, ${time(f.from)} to ${time(f.to)}`}
</Text>
</li>
))}
</ul>
</Stack>
);
}
A drag does not put work into blocked time: the ghost stays where it was allowed until the pointer is past it.
import { useState } from "react";
import { Stack, Text } from "@umriss-ui/core";
import { applyIntent, useSchedule } from "@umriss-ui/schedule";
import type { BlockedTime, Intent, Subtask, Task } from "@umriss-ui/schedule";
/* Blocked time is part of where a subtask may go. Drag the review towards
Anna's leave and the ghost stops short of it, the cursor says no and a line
ties the ghost to the pointer; carry on past the leave, or onto Ben's lane,
and it follows again. Alt with an arrow key steps over the leave instead of
stopping in front of it.
Work the data already put into blocked time is never locked there: it may be
moved within that blocked time, and out of it. The finding stays until it
is. */
const day = (d: number, hours = 0) => new Date(2026, 2, 16 + d, hours).getTime();
const PROJECTS: readonly Task[] = [{ id: "portal", name: "Customer portal", color: "light-dark(#2563eb, #6b9bff)" }];
const START: readonly Subtask[] = [
{ id: "review", task: "portal", lane: "anna", from: day(0, 9), to: day(1, 12), name: "Design review" },
{ id: "tests", task: "portal", lane: "ben", from: day(2, 9), to: day(3, 17), name: "Tests" },
];
const BLOCKED: readonly BlockedTime[] = [{ id: "anna-leave", lane: "anna", from: day(2, 0), to: day(4, 0), label: "Leave" }];
export default function DragAround() {
const { Schedule, Lane, Subtasks, BlockedTimes } = useSchedule({ initialView: { domain: [day(0, 6), day(5, 0)] } });
const [work, setWork] = useState<readonly Subtask[]>(START);
const [last, setLast] = useState("Drag the design review into Anna's leave");
const onIntent = (intent: Intent) => {
if (intent.kind === "place") return;
setWork((current) => current.map((s) => applyIntent(s, intent)));
setLast(`${intent.subtask}: ${intent.kind}`);
};
return (
<Stack gap={3}>
<Schedule
ariaLabel="Two people, one on leave"
height={150}
intents={["move", "lane"]}
onIntent={onIntent}
>
<Lane id="anna" label="Anna Berg" />
<Lane id="ben" label="Ben Ortiz" />
<BlockedTimes data={BLOCKED} />
<Subtasks data={work} tasks={PROJECTS} />
</Schedule>
<Text size="sm" mono tone="secondary" data-last-move>
{last}
</Text>
</Stack>
);
}
The team's holidays, sick days and training become BlockedTimes, and findings confirms that no work item runs into one.
import { Stack, Text } from "@umriss-ui/core";
import { findings, useSchedule } from "@umriss-ui/schedule";
import type { BlockedTime } 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"),
];
interface Leave {
id: string;
person: string;
from: number;
to: number;
reason: "Holiday" | "Sick" | "Training";
}
/** When people are away: blocked time on their lane. */
const LEAVE: readonly Leave[] = [
{ id: "l-1", person: "noah", from: at(13, 0, 0), to: at(14, 0, 0), reason: "Sick" },
{ id: "l-2", person: "arjun", from: at(18, 0, 0), to: at(21, 0, 0), reason: "Holiday" },
{ id: "l-3", person: "kofi", from: at(13, 0, 0), to: at(14, 0, 0), reason: "Training" },
{ id: "l-4", person: "freya", from: at(16, 0, 0), to: at(18, 0, 0), reason: "Holiday" },
{ id: "l-5", person: "david", from: at(9, 0, 0), to: at(14, 0, 0), reason: "Holiday" },
{ id: "l-6", person: "maya", from: at(19, 13, 0), to: at(20, 17, 0), reason: "Training" },
];
const BLOCKED: BlockedTime[] = LEAVE.map((leave) => ({ id: leave.id, lane: leave.person, from: leave.from, to: leave.to, label: leave.reason }));
const SPRINT_14: readonly [number, number] = [new Date(2026, 2, 9, 6).getTime(), new Date(2026, 2, 21).getTime()];
const NAME = new Map(WORK.map((item) => [item.id, item.name]));
const PERSON = new Map(PEOPLE.map((person) => [person.id, person.name]));
export default function TeamLeave() {
const { Schedule, Lane, Subtasks, BlockedTimes } = useSchedule({ initialView: { domain: SPRINT_14 } });
const clashes = findings(WORK, [], BLOCKED).inBlockedTime;
return (
<Stack gap={3}>
<Schedule ariaLabel="Sprint 14 with the team's leave" height={500}>
{PEOPLE.map((person) => (
<Lane key={person.id} id={person.id} label={person.name} />
))}
<BlockedTimes data={BLOCKED} />
<Subtasks data={WORK} tasks={PROJECTS} />
</Schedule>
<Text size="sm" tone="secondary">
{clashes.length === 0
? "No work falls into anyone's leave."
: clashes.map((clash) => `${NAME.get(clash.subtask)} (${PERSON.get(WORK.find((w) => w.id === clash.subtask)?.lane ?? "")})`).join(" · ")}
</Text>
</Stack>
);
}
The keys of First schedule apply here.
BlockedTimesProps| Name | Type | Default | Description |
|---|---|---|---|
datarequired | readonly BlockedTime[] | — | The blocked time of the lanes - leave, maintenance, unavailability -, as
the caller holds it: one list, each interval naming its lane. Shown in: Block out leave and maintenance, Plan work around someone's leave, Check a sprint against the leave calendar and 5 more |
BlockedTime| Name | Type | Default | Description |
|---|---|---|---|
idrequired | string | — | The caller's identity of the interval - what a finding names. Shown in: Block out leave and maintenance, Plan work around someone's leave, Check a sprint against the leave calendar and 5 more |
lanerequired | string | — | The id of the lane it blocks. Shown in: Block out leave and maintenance, Plan work around someone's leave, Check a sprint against the leave calendar and 5 more |
fromrequired | number | — | Where it begins. Shown in: Block out leave and maintenance, Plan work around someone's leave, Check a sprint against the leave calendar and 5 more |
torequired | number | — | Where it ends. Shown in: Block out leave and maintenance, Plan work around someone's leave, Check a sprint against the leave calendar and 5 more |
label | string | — | Why the lane is blocked - "Leave", "Maintenance" -, where the schedule
names it. Shown in: Block out leave and maintenance, Plan work around someone's leave, Check a sprint against the leave calendar and 5 more |