# Appearances

> Package `@umriss-ui/schedule`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/schedule/appearances/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/schedule/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/schedule/llms-full.txt).

What a bar says besides its colour: provisional, fixed, muted, running on past the view, and how far the work has got. Each takes one channel of the drawing, so several on one bar stay readable.

```ts
import { useSchedule, resolveAppearance } from "@umriss-ui/schedule";
```

Demo page: https://romanhaendler.github.io/umriss-ui/schedule/appearances/

`"provisional"` owns the fill (none at all), `"fixed"` the ends (a cap inside each), `"muted"` the saturation, `"open"` a fade at the edge of the view, and `progress` a rail inside the main time. The faint, outlined fill belongs to a lead-in and a lead-out and to nothing else.

`"provisional"` and `"fixed"` contradict, and the later one in the list wins. [`resolveAppearance`](https://romanhaendler.github.io/umriss-ui/schedule/api/#resolveAppearance) is that rule, exported for the application to read as well.

## Examples

### Provisional

Work pencilled in but not agreed: `"provisional"` leaves the bar hollow with a dashed outline, never pale like a lead-in.

```tsx
import { useSchedule } from "@umriss-ui/schedule";
import type { 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 PROJECTS: Task[] = [{ id: "portal", name: "Member portal", color: "light-dark(#2563eb, #6b9bff)" }];

const WORK: Subtask[] = [
  { id: "agreed", task: "portal", lane: "released", from: at(7), to: at(10), leadIn: min(30), leadOut: min(30) },
  { id: "pencilled", task: "portal", lane: "draft", from: at(7), to: at(10), appearance: ["provisional"] },
];

export default function Provisional() {
  const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(6, 30), at(11)] } });
  return (
    <Schedule ariaLabel="Agreed work above, provisional work below" height={144}>
      <Lane id="released" label="Agreed, with lead-in" />
      <Lane id="draft" label="Provisional" />
      <Subtasks data={WORK} tasks={PROJECTS} />
    </Schedule>
  );
}
```

### Fixed

A delivery slot the customer booked may not move: `"fixed"` sets a cap inside each end and keeps the face clear for the label.

```tsx
import { useSchedule } from "@umriss-ui/schedule";
import type { Subtask, Task } from "@umriss-ui/schedule";

const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();

const TOURS: Task[] = [{ id: "t-05", name: "T-05 Riverside", color: "light-dark(#2563eb, #6b9bff)" }];

const LEGS: Subtask[] = [
  { id: "movable", task: "t-05", lane: "movable", from: at(7), to: at(10) },
  { id: "nailed", task: "t-05", lane: "nailed", from: at(7), to: at(10), appearance: ["fixed"] },
];

export default function Fixed() {
  const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(6, 30), at(11)] } });
  return (
    <Schedule
      ariaLabel="A round that may move above, a booked slot below"
      height={144}
      label={(leg) => (leg.id === "nailed" ? "T-05 · booked slot" : "T-05")}
    >
      <Lane id="movable" label="May move" />
      <Lane id="nailed" label="Fixed" />
      <Subtasks data={LEGS} tasks={TOURS} />
    </Schedule>
  );
}
```

### Muted

Another team's work, shown for context: `"muted"` mixes the colour into the surface at full height, so it cannot pass for a lead-in.

```tsx
import { useSchedule } from "@umriss-ui/schedule";
import type { 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 TASKS: Task[] = [{ id: "migration", name: "Checkout migration", color: "light-dark(#2563eb, #6b9bff)" }];

const WORK: Subtask[] = [
  { id: "ours", task: "migration", lane: "ours", from: at(7), to: at(10), leadIn: min(30) },
  { id: "theirs", task: "migration", lane: "theirs", from: at(7), to: at(10), leadIn: min(30), appearance: ["muted"] },
];

export default function Muted() {
  const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(6, 30), at(11)] } });
  return (
    <Schedule
      ariaLabel="Our team's work above, another team's below"
      height={144}
      label={() => "Checkout migration"}
    >
      <Lane id="ours" label="Payments" />
      <Lane id="theirs" label="Identity" />
      <Subtasks data={WORK} tasks={TASKS} />
    </Schedule>
  );
}
```

### Progress

`progress` from 0 to 1 draws a rail inside the main time; left out, the bar claims nothing - which is not zero per cent.

```tsx
import { useSchedule } from "@umriss-ui/schedule";
import type { 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 PROJECTS: Task[] = [{ id: "portal", name: "Member portal", color: "light-dark(#2563eb, #6b9bff)" }];

const WORK: Subtask[] = [
  { id: "unclaimed", task: "portal", lane: "unclaimed", from: at(7), to: at(10) },
  { id: "started", task: "portal", lane: "started", from: at(7), to: at(10), progress: 0.65, leadOut: min(30) },
];

export default function Progress() {
  const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(6, 30), at(11)] } });
  return (
    <Schedule ariaLabel="Work claiming nothing above, work at 65 per cent below" height={144}>
      <Lane id="unclaimed" label="Says nothing" />
      <Lane id="started" label="65 per cent done" />
      <Subtasks data={WORK} tasks={PROJECTS} />
    </Schedule>
  );
}
```

### Open

An incident still being worked runs past the view: `"open"` fades the bar at whichever edge it passes, and only there.

```tsx
import { useSchedule } from "@umriss-ui/schedule";
import type { Subtask, Task } from "@umriss-ui/schedule";

/* The middle bar is wholly in view and carries no fade; zoom out until it
   passes an edge and the fade appears. */

const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();

const TASKS: Task[] = [{ id: "inc-1048", name: "INC-1048", color: "light-dark(#2563eb, #6b9bff)" }];

const WORK: Subtask[] = [
  { id: "runs-on", task: "inc-1048", lane: "runs-on", from: at(7), to: at(13), appearance: ["open"] },
  { id: "whole", task: "inc-1048", lane: "whole", from: at(7), to: at(10), appearance: ["open"] },
  { id: "began", task: "inc-1048", lane: "began", from: at(4), to: at(10), appearance: ["open"] },
];

export default function Open() {
  const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(6, 30), at(11)] } });
  return (
    <Schedule ariaLabel="Work running past the view at either edge" height={188}>
      <Lane id="runs-on" label="Runs on" />
      <Lane id="whole" label="Wholly in view" />
      <Lane id="began" label="Began before" />
      <Subtasks data={WORK} tasks={TASKS} />
    </Schedule>
  );
}
```

### Several at once

Muted, fixed and open on one bar stay three statements, each in its own channel; of `"provisional"` and `"fixed"` the later one wins.

```tsx
import { useSchedule } from "@umriss-ui/schedule";
import type { Subtask, Task } from "@umriss-ui/schedule";

/* A hollow bar lies on the surface, so its label takes the page's text
   colour; a muted one takes whatever reads on its mix. */

const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();

const TOURS: Task[] = [
  { id: "own", name: "North depot", color: "light-dark(#2563eb, #6b9bff)" },
  { id: "other", name: "Riverside depot", color: "light-dark(#c2410c, #f08a52)" },
];

const LEGS: Subtask[] = [
  /* Paler colour, capped ends, faded edge. */
  {
    id: "all-three",
    task: "other",
    lane: "all-three",
    from: at(7),
    to: at(13),
    appearance: ["fixed", "muted", "open"],
    leadIn: 30 * 60_000,
  },
  /* The rail lies on the surface the hollow bar leaves. */
  { id: "planned", task: "own", lane: "planned", from: at(7), to: at(10), appearance: ["provisional"], progress: 0.4 },
  /* `"fixed"` is the later word, so this bar is fixed. */
  { id: "settled", task: "own", lane: "settled", from: at(7), to: at(10), appearance: ["provisional", "fixed"] },
  /* And the other way round, which makes it provisional. */
  { id: "reopened", task: "own", lane: "reopened", from: at(7), to: at(10), appearance: ["fixed", "provisional"] },
];

/** What stands in each bar. */
const NAMES: Record<string, string> = {
  "all-three": "T-06 · Riverside",
  planned: "T-02 · draft",
  settled: "T-02 · released",
  reopened: "T-02 · reopened",
};

const LANES = [
  { id: "all-three", label: "Other depot, fixed, runs on" },
  { id: "planned", label: "Provisional, 40 per cent" },
  { id: "settled", label: "provisional then fixed" },
  { id: "reopened", label: "fixed then provisional" },
];

export default function Combinations() {
  const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(6, 30), at(11)] } });
  return (
    <Schedule
      ariaLabel="Bars carrying several appearances at once"
      height={230}
      label={(leg) => NAMES[leg.id] ?? leg.id}
    >
      {LANES.map((lane) => (
        <Lane key={lane.id} id={lane.id} label={lane.label} />
      ))}
      <Subtasks data={LEGS} tasks={TOURS} />
    </Schedule>
  );
}
```

## Keyboard

The keys of [First schedule](https://romanhaendler.github.io/umriss-ui/schedule/schedule/#keyboard-schedule) apply here.

## Props on this page

From `ScheduleProps` — the full table stands on [First schedule](https://romanhaendler.github.io/umriss-ui/schedule/schedule/#type-ScheduleProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`height`](https://romanhaendler.github.io/umriss-ui/schedule/schedule/#ScheduleProps-height) | `number` | `400` | Height in CSS pixels, both bands included. The width is the host's. |
| [`label`](https://romanhaendler.github.io/umriss-ui/schedule/schedule/#ScheduleProps-label) | `(subtask: `[`Subtask`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#type-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`](https://romanhaendler.github.io/umriss-ui/schedule/schedule/#ScheduleProps-children) | `ReactNode` | — | `Lane`, `Dependencies` and `Subtasks`, in the order they are to stand and be drawn. |

#### Accessibility

| Prop | Type | Default | Description |
|---|---|---|---|
| [`ariaLabel`](https://romanhaendler.github.io/umriss-ui/schedule/schedule/#ScheduleProps-ariaLabel) *required* | `string` | — | What the schedule shows, for a screen reader: "Plan of week 38". |

From `LaneProps` — the full table stands on [Lanes](https://romanhaendler.github.io/umriss-ui/schedule/lane/#type-LaneProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`id`](https://romanhaendler.github.io/umriss-ui/schedule/lane/#LaneProps-id) *required* | `string` | — | The identity subtasks name in their `lane`. |
| [`label`](https://romanhaendler.github.io/umriss-ui/schedule/lane/#LaneProps-label) | `ReactNode` | `id` | What the lane header says - the person, vehicle or room. Real text, read by a screen reader. |

From `SubtasksProps` — the full table stands on [Subtasks](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#type-SubtasksProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`data`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#SubtasksProps-data) *required* | `readonly `[`Subtask`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#type-Subtask)`[]` | — | The subtasks, as the caller holds them. The schedule never changes one. |
| [`tasks`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#SubtasksProps-tasks) *required* | `readonly `[`Task`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#type-Task)`[]` | — | The tasks the subtasks belong to - their identity and their colour. A subtask whose task is not among them is drawn in a muted colour. |

From `Subtask` — the full table stands on [Subtasks](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#type-Subtask).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`id`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#Subtask-id) *required* | `string` | — | The caller's identity of the subtask. |
| [`task`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#Subtask-task) *required* | `string` | — | The id of the task it belongs to. |
| [`lane`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#Subtask-lane) *required* | `string` | — | The id of the lane it sits on. |
| [`from`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#Subtask-from) *required* | `number` | — | Start of the main time. |
| [`to`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#Subtask-to) *required* | `number` | — | End of the main time. |
| [`leadIn`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#Subtask-leadIn) | `number` | — | Duration of the lead-in before the main time; none is zero. |
| [`leadOut`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#Subtask-leadOut) | `number` | — | Duration of the lead-out after the main time; none is zero. |
| [`appearance`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#Subtask-appearance) | `readonly `[`SubtaskAppearance`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#type-SubtaskAppearance)`[]` | — | What the bar says besides its colour: `"provisional"`, `"fixed"`, `"muted"`, `"open"`. Several hold at once; `"provisional"` and `"fixed"` contradict, and the later one in the list wins. |
| [`progress`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#Subtask-progress) | `number` | — | The share of the work that is done, from 0 to 1, drawn as a filled part of the bar. Without it nothing is claimed - an empty bar and a bar at zero per cent are two different statements. |

From `Task` — the full table stands on [Subtasks](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#type-Task).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`id`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#Task-id) *required* | `string` | — | The caller's identity of the task. |
| [`color`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#Task-color) *required* | `string` | — | Any CSS colour - a literal, a token, `light-dark(…)`. The colour means what the caller's domain needs it to mean. |
| [`name`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#Task-name) | `string` | — | What the task is called, where the schedule names it. |

From `ScheduleOptions` — the full table stands on [View](https://romanhaendler.github.io/umriss-ui/schedule/view/#type-ScheduleOptions).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`initialView`](https://romanhaendler.github.io/umriss-ui/schedule/view/#ScheduleOptions-initialView) | [`ScheduleView`](https://romanhaendler.github.io/umriss-ui/schedule/view/#type-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. |

From `ScheduleView` — the full table stands on [View](https://romanhaendler.github.io/umriss-ui/schedule/view/#type-ScheduleView).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`domain`](https://romanhaendler.github.io/umriss-ui/schedule/view/#ScheduleView-domain) | `readonly [number, number]` | — | The span in view, as two wall-clock instants. Absent, the schedule shows the extent of its subtasks, within `zoomLimits`. |
