Blocked time

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.

Examples

Block out leave and maintenance

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>
  );
}

Plan work around someone's leave

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>
  );
}

Check a sprint against the leave calendar

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>
  );
}

When to use something else

Keyboard

The keys of First schedule apply here.

API

BlockedTimesProps

NameTypeDefaultDescription
datarequiredreadonly 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

NameTypeDefaultDescription
idrequiredstring—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

lanerequiredstring—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

fromrequirednumber—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

torequirednumber—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

labelstring—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

Every page of @umriss-ui/schedule

Getting started

Plan

Time

Reading

Editing

Measuring

API index