A point in time on a lane - a handover, a release, a deadline - drawn as a diamond above the work, reached by the keys and named in the tooltip.
import { useSchedule } from "@umriss-ui/schedule";
A milestone is data beside the work: one list, each point naming its lane and the instant it stands at ({ id, lane, at, label? }). It has no duration, no task and no tone; it is not dragged and reports no intent, and work beside it is never a finding.
The keys walk to it in time order among the subtasks of its lane, the tooltip names it by its label and time, and the live region reads its lane, label and time. It is named by its label, or its id without one.
A point in time on a lane is a mark, not a bar: Milestones draws a diamond at at, above the work.
import { Stack } from "@umriss-ui/core";
import { useSchedule } from "@umriss-ui/schedule";
import type { Milestone, Subtask, Task } from "@umriss-ui/schedule";
/* A milestone is data beside the work: one list, each point naming its lane
and the instant it stands at, as a release calendar holds it. It is drawn
above the bars - the design handover below stands on the end of the design
bar and is still seen - and it has no duration, no task and no colour of
its own.
Hover it and the tooltip names it and says when; the keys reach it as they
reach the work, with the arrows in time order along the lane, and the live
region reads its lane, its label and its time. It is not dragged and
reports no intent. */
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)" },
{ 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" },
{ id: "portal-front", task: "portal", lane: "chloe", from: day(1, 9), to: day(3, 17), name: "Front end" },
{ id: "billing-api", task: "billing", lane: "ben", from: day(0, 9), to: day(2, 17), name: "Invoice API" },
];
const MILESTONES: readonly Milestone[] = [
{ id: "design-handover", lane: "anna", at: day(1, 17), label: "Design handover" },
{ id: "api-freeze", lane: "ben", at: day(2, 17), label: "API freeze" },
{ id: "release", lane: "chloe", at: day(4, 12), label: "Release 2.0" },
];
export default function HandoversAndReleases() {
const { Schedule, Lane, Subtasks, Milestones } = useSchedule({ initialView: { domain: [day(0, 6), day(5, 0)] } });
return (
<Stack gap={3}>
<Schedule ariaLabel="Portal and billing plan, week of 16 March" height={188}>
<Lane id="anna" label="Anna Berg" />
<Lane id="ben" label="Ben Ortiz" />
<Lane id="chloe" label="Chloe Lind" />
<Subtasks data={WORK} tasks={PROJECTS} />
<Milestones data={MILESTONES} />
</Schedule>
</Stack>
);
}
Over a milestone the target is milestone: the point itself, with its label and at, for a tooltip of your own.
import { Stack, Text } from "@umriss-ui/core";
import { useSchedule } from "@umriss-ui/schedule";
import type { Milestone, ScheduleTooltipTarget, Subtask, Task } from "@umriss-ui/schedule";
const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const PROJECTS: readonly Task[] = [{ id: "portal", name: "Member portal", color: "light-dark(#2563eb, #6b9bff)" }];
const WORK: readonly Subtask[] = [
{ id: "build", task: "portal", lane: "arjun", from: at(7), to: at(12), name: "Build" },
{ id: "test", task: "portal", lane: "eva", from: at(12, 30), to: at(15), name: "Test" },
];
const MILESTONES: readonly Milestone[] = [{ id: "code-freeze", lane: "arjun", at: at(12), label: "Code freeze" }];
const time = (instant: number) => new Date(instant).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
function Own({ target }: { target: ScheduleTooltipTarget }) {
if (target.kind === "dependency") return null;
if (target.kind === "subtask") return <Text size="sm">{target.subtask.name}</Text>;
return (
<Stack gap={1}>
<Text size="sm" weight="semibold">
{target.milestone.label ?? target.milestone.id}
</Text>
<Text size="xs" tone="secondary">
Test may begin from {time(target.milestone.at)}
</Text>
</Stack>
);
}
export default function SayWhatAMilestoneIs() {
const { Schedule, Lane, Subtasks, Milestones } = useSchedule({ initialView: { domain: [at(6, 30), at(15, 30)] } });
return (
<Schedule ariaLabel="Build and test around the code freeze" height={150} tooltip={(target) => <Own target={target} />}>
<Lane id="arjun" label="Arjun Mehta" />
<Lane id="eva" label="Eva Novak" />
<Subtasks data={WORK} tasks={PROJECTS} />
<Milestones data={MILESTONES} />
</Schedule>
);
}
The keys of First schedule apply here.
MilestonesProps| Name | Type | Default | Description |
|---|---|---|---|
datarequired | readonly Milestone[] | — | The milestones of the lanes, as the caller holds them: one list, each
naming its lane and the instant it stands at. Shown in: Mark a handover and a release, Say what a milestone is in your own tooltip |
Milestone| Name | Type | Default | Description |
|---|---|---|---|
idrequired | string | — | The caller's identity of the milestone. Shown in: Mark a handover and a release, Say what a milestone is in your own tooltip |
lanerequired | string | — | The id of the lane it stands on. Shown in: Mark a handover and a release, Say what a milestone is in your own tooltip |
atrequired | number | — | The instant it stands at. Shown in: Mark a handover and a release, Say what a milestone is in your own tooltip |
label | string | — | What it is called, where the schedule names it; without it, its id. Shown in: Mark a handover and a release, Say what a milestone is in your own tooltip |