@umriss-ui/schedule 0.3.12 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/llms-full.md CHANGED
@@ -1,4 +1,4 @@
1
- # @umriss-ui/schedule 0.3.12
1
+ # @umriss-ui/schedule 0.3.14
2
2
 
3
3
  > React Gantt-style schedule for data-dense applications - work on lanes over time, with dependencies, blocked time, findings and controlled editing.
4
4
 
@@ -24,7 +24,51 @@ import { useState } from "react";
24
24
  import { Alert, Badge, Stack, Text } from "@umriss-ui/core";
25
25
  import { BlockedTimes, Lane, Schedule, Subtasks, applyIntent, findings } from "@umriss-ui/schedule";
26
26
  import type { BlockedTime, Intent, Subtask, Task } from "@umriss-ui/schedule";
27
- import { ENGINEERS, NOW, ONCALL } from "./operations";
27
+
28
+ /* Data from the operations world, written out here so the example runs on its own. */
29
+
30
+ const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
31
+
32
+ /** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
33
+ const NOW = at(17, 10, 30);
34
+
35
+ interface Engineer {
36
+ id: string;
37
+ name: string;
38
+ team: string;
39
+ }
40
+
41
+ const ENGINEERS: readonly Engineer[] = [
42
+ { id: "priya", name: "Priya Raman", team: "Payments" },
43
+ { id: "jonas", name: "Jonas Keller", team: "Payments" },
44
+ { id: "ada", name: "Ada Mwangi", team: "Identity" },
45
+ { id: "tomasz", name: "Tomasz Nowak", team: "Discovery" },
46
+ { id: "leila", name: "Leila Haddad", team: "Discovery" },
47
+ { id: "sam", name: "Sam Okafor", team: "Messaging" },
48
+ { id: "ines", name: "Ines Duarte", team: "Integrations" },
49
+ { id: "felix", name: "Felix Brandt", team: "Insights" },
50
+ ];
51
+
52
+ interface OnCall {
53
+ id: string;
54
+ engineer: string;
55
+ rotation: "primary" | "secondary";
56
+ from: number;
57
+ to: number;
58
+ }
59
+
60
+ /** Monday 16 to Monday 23 March, handed over every morning at 09:00: the
61
+ secondary of one day is the primary of the next. */
62
+ const ONCALL: readonly OnCall[] = Array.from({ length: 7 }, (_, day) => [
63
+ { rotation: "primary" as const, engineer: ENGINEERS[day % ENGINEERS.length]!.id },
64
+ { rotation: "secondary" as const, engineer: ENGINEERS[(day + 1) % ENGINEERS.length]!.id },
65
+ ].map(({ rotation, engineer }) => ({
66
+ id: `${rotation}-${16 + day}`,
67
+ engineer,
68
+ rotation,
69
+ from: at(16 + day, 9),
70
+ to: at(17 + day, 9),
71
+ }))).flat();
28
72
 
29
73
  const day = (d: number, hours = 0) => new Date(2026, 2, d, hours).getTime();
30
74
 
@@ -137,8 +181,6 @@ export default function HandOver() {
137
181
  }
138
182
  ```
139
183
 
140
- It imports `operations.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
141
-
142
184
  ### Plan tomorrow's tours
143
185
 
144
186
  A dispatcher lays out tomorrow's delivery tours van by van and moves them until every stop arrives inside the window its customer was promised.
@@ -155,15 +197,143 @@ import { useState } from "react";
155
197
  import { Badge, Button, Stack, Text } from "@umriss-ui/core";
156
198
  import { Dependencies, Lane, LaneGroup, Schedule, Subtasks, applyIntent, ripple, shiftTask } from "@umriss-ui/schedule";
157
199
  import type { Dependency, Intent, Subtask, Task } from "@umriss-ui/schedule";
158
- import { DEPOTS, DRIVERS, TOURS, VEHICLES } from "./logistics";
159
200
 
201
+ /* Data from the logistics world, written out here so the example runs on its own. */
202
+
203
+ /** A small LCG - the same numbers on every computer. */
204
+ function random(seed: number): () => number {
205
+ let s = seed >>> 0;
206
+ return () => {
207
+ s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
208
+ return s / 4294967296;
209
+ };
210
+ }
211
+
212
+ const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
160
213
  const MINUTE = 60_000;
214
+
215
+ const DEPOTS = [
216
+ { id: "north", name: "North depot" },
217
+ { id: "river", name: "Riverside depot" },
218
+ { id: "east", name: "East Gate depot" },
219
+ ] as const;
220
+
221
+ interface Vehicle {
222
+ id: string;
223
+ plate: string;
224
+ type: "van" | "e-van" | "truck";
225
+ /** Payload, in kg. */
226
+ capacity: number;
227
+ depot: string;
228
+ }
229
+
230
+ const VEHICLES: readonly Vehicle[] = [
231
+ { id: "v1", plate: "FP 214 K", type: "van", capacity: 1200, depot: "north" },
232
+ { id: "v2", plate: "FP 377 K", type: "e-van", capacity: 900, depot: "north" },
233
+ { id: "v3", plate: "FP 118 R", type: "truck", capacity: 7500, depot: "north" },
234
+ { id: "v4", plate: "FP 402 R", type: "van", capacity: 1200, depot: "river" },
235
+ { id: "v5", plate: "FP 455 R", type: "e-van", capacity: 900, depot: "river" },
236
+ { id: "v6", plate: "FP 290 E", type: "van", capacity: 1200, depot: "east" },
237
+ { id: "v7", plate: "FP 311 E", type: "e-van", capacity: 900, depot: "east" },
238
+ { id: "v8", plate: "FP 520 E", type: "truck", capacity: 7500, depot: "east" },
239
+ ];
240
+
241
+ const DRIVERS = [
242
+ { id: "d1", name: "Martin Hale", depot: "north" },
243
+ { id: "d2", name: "Nadia Petrova", depot: "north" },
244
+ { id: "d3", name: "Owen Carter", depot: "north" },
245
+ { id: "d4", name: "Lucia Romero", depot: "river" },
246
+ { id: "d5", name: "Ben Adeyemi", depot: "river" },
247
+ { id: "d6", name: "Hanna Berg", depot: "east" },
248
+ { id: "d7", name: "Yusuf Demir", depot: "east" },
249
+ { id: "d8", name: "Clara Wendt", depot: "east" },
250
+ ] as const;
251
+
252
+ const CUSTOMERS = [
253
+ "Holloway Garden Supplies",
254
+ "Marlow & Finch Books",
255
+ "Oakridge Pharmacy",
256
+ "Brixley Cycles",
257
+ "Tamsin's Bakery",
258
+ "Northfold Office",
259
+ "Pellham Hardware",
260
+ "Greywick Studio",
261
+ "Juniper Lane Florist",
262
+ "Ashcombe Dental",
263
+ ] as const;
264
+
265
+ interface Stop {
266
+ id: string;
267
+ shipment: string;
268
+ customer: string;
269
+ /** The window the customer was promised, `[from, to]`. */
270
+ window: readonly [number, number];
271
+ /** When the tour plans to be there. */
272
+ arrival: number;
273
+ }
274
+
275
+ interface Tour {
276
+ id: string;
277
+ vehicle: string;
278
+ driver: string;
279
+ depot: string;
280
+ /** Leaves the depot, loaded; back at the depot. */
281
+ from: number;
282
+ to: number;
283
+ /** Minutes of loading before `from`. */
284
+ loading: number;
285
+ km: number;
286
+ stops: readonly Stop[];
287
+ }
288
+
289
+ /** One tour per vehicle. Stops every half hour or so, each with a two-hour
290
+ window; every seventh arrival misses its window - the late ones a
291
+ dispatcher looks for. */
292
+ function tour(index: number): Tour {
293
+ const r = random(700 + index);
294
+ const vehicle = VEHICLES[index]!;
295
+ const truck = vehicle.type === "truck";
296
+ const from = at(truck ? 6 : 7, index % 2 === 0 ? 0 : 30);
297
+ const count = truck ? 5 : 8 + Math.floor(r() * 4);
298
+ const stops: Stop[] = [];
299
+ let arrival = from + (20 + Math.floor(r() * 15)) * MINUTE;
300
+ for (let i = 0; i < count; i++) {
301
+ const n = index * 20 + i;
302
+ const slot = Math.floor((arrival - at(0)) / (2 * 60 * MINUTE)) * 2;
303
+ /* A late stop's window closed half an hour or more before the arrival. */
304
+ const late = i >= 2 && n % 7 === 3;
305
+ const halfHour = 30 * MINUTE;
306
+ const windowFrom = late ? Math.floor(arrival / halfHour) * halfHour - 5 * halfHour : at(slot);
307
+ stops.push({
308
+ id: `s-${index + 1}-${i + 1}`,
309
+ shipment: `FP-${(1_004_210 + n * 13).toString()}`,
310
+ customer: CUSTOMERS[(n * 3) % CUSTOMERS.length]!,
311
+ window: [windowFrom, windowFrom + 2 * 60 * MINUTE],
312
+ arrival,
313
+ });
314
+ arrival += (truck ? 45 : 22) * MINUTE + Math.floor(r() * 18) * MINUTE;
315
+ }
316
+ return {
317
+ id: `T-${String(index + 1).padStart(2, "0")}`,
318
+ vehicle: vehicle.id,
319
+ driver: DRIVERS[index]!.id,
320
+ depot: vehicle.depot,
321
+ from,
322
+ to: arrival + 30 * MINUTE,
323
+ loading: truck ? 45 : 30,
324
+ km: Math.round((truck ? 140 : 70) + r() * 40),
325
+ stops,
326
+ };
327
+ }
328
+
329
+ const TOURS: readonly Tour[] = VEHICLES.map((_, index) => tour(index));
330
+
161
331
  const DAY = 24 * 60 * MINUTE;
162
332
  /* Today's tours in the world, laid on tomorrow. */
163
333
  const DWELL = 10 * MINUTE;
164
334
  const QUARTER = 15 * MINUTE;
165
335
 
166
- const at = (hours: number, minutes = 0) => new Date(2026, 2, 18, hours, minutes).getTime();
336
+ const tomorrow = (hours: number, minutes = 0) => new Date(2026, 2, 18, hours, minutes).getTime();
167
337
 
168
338
  const COLORS = [
169
339
  "light-dark(#2563eb, #6b9bff)",
@@ -248,7 +418,7 @@ export default function TourPlan() {
248
418
  <div data-callout="1">
249
419
  <Schedule
250
420
  ariaLabel="Tours of Wednesday, 18 March"
251
- initialDomain={[at(5, 30), at(15)]}
421
+ initialDomain={[tomorrow(5, 30), tomorrow(15)]}
252
422
  height={470}
253
423
  intents={["move"]}
254
424
  onIntent={apply}
@@ -295,8 +465,6 @@ export default function TourPlan() {
295
465
  }
296
466
  ```
297
467
 
298
- It imports `logistics.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
299
-
300
468
  ### Plan the sprint around leave
301
469
 
302
470
  A team lead checks the running sprint against holidays, sick days and training, and moves the work that lands on someone who is away.
@@ -313,56 +481,169 @@ import { useState } from "react";
313
481
  import { Alert, Badge, Stack, Text } from "@umriss-ui/core";
314
482
  import { BlockedTimes, Lane, LaneGroup, Schedule, Subtasks, applyIntent, findings } from "@umriss-ui/schedule";
315
483
  import type { BlockedTime, Intent, Subtask } from "@umriss-ui/schedule";
316
- import { LEAVE, NOW, PEOPLE, PROJECTS, SPRINTS, WORK } from "./planning";
317
484
 
318
- const at = (day: number, hours = 9) => new Date(2026, 2, day, hours).getTime();
485
+ /* Data from the planning world, written out here so the example runs on its own. */
319
486
 
320
- const SPRINT = SPRINTS.find((one) => one.id === "sprint-14")!;
487
+ const at = (day: number, hours = 9, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
321
488
 
322
- /* The working days of the sprint: the axis shows these and nothing else. */
323
- const WORKING_DAYS = [9, 10, 11, 12, 13, 16, 17, 18, 19, 20].map((day) => ({
324
- from: at(day, 8),
325
- to: at(day, 18),
326
- }));
489
+ /** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
490
+ const NOW = at(17, 10, 30);
327
491
 
328
- /* Added after planning, before anyone looked at the leave table. */
329
- const START: readonly Subtask[] = [
330
- ...WORK,
331
- {
332
- id: "w-117",
333
- task: "portal",
334
- lane: "arjun",
335
- from: at(18),
336
- to: at(19, 17),
337
- name: "Account deletion",
338
- },
492
+ interface Person {
493
+ id: string;
494
+ name: string;
495
+ role: "Developer" | "Designer" | "Product manager" | "QA engineer";
496
+ team: "Web" | "Apps";
497
+ /** Hours a week they can be planned for. */
498
+ capacity: number;
499
+ }
500
+
501
+ const PEOPLE: readonly Person[] = [
502
+ { id: "maya", name: "Maya Lindgren", role: "Product manager", team: "Web", capacity: 32 },
503
+ { id: "arjun", name: "Arjun Mehta", role: "Developer", team: "Web", capacity: 40 },
504
+ { id: "chloe", name: "Chloe Durand", role: "Developer", team: "Web", capacity: 40 },
505
+ { id: "noah", name: "Noah Fischer", role: "Designer", team: "Web", capacity: 24 },
506
+ { id: "eva", name: "Eva Novak", role: "QA engineer", team: "Web", capacity: 40 },
507
+ { id: "luis", name: "Luis Moreno", role: "Product manager", team: "Apps", capacity: 40 },
508
+ { id: "hana", name: "Hana Sato", role: "Developer", team: "Apps", capacity: 40 },
509
+ { id: "kofi", name: "Kofi Mensah", role: "Developer", team: "Apps", capacity: 32 },
510
+ { id: "freya", name: "Freya Olsen", role: "Designer", team: "Apps", capacity: 40 },
511
+ { id: "david", name: "David Kowalski", role: "QA engineer", team: "Apps", capacity: 20 },
339
512
  ];
340
513
 
341
- const BLOCKED: readonly BlockedTime[] = LEAVE.map((leave) => ({
342
- id: leave.id,
343
- lane: leave.person,
344
- from: leave.from,
345
- to: leave.to,
346
- label: leave.reason,
347
- }));
514
+ interface Project {
515
+ id: string;
516
+ name: string;
517
+ client: string;
518
+ color: string;
519
+ }
348
520
 
349
- const teamOf = (person: string) => PEOPLE.find((one) => one.id === person)?.team;
350
- const nameOf = (person: string) => PEOPLE.find((one) => one.id === person)?.name ?? person;
521
+ const PROJECTS: readonly Project[] = [
522
+ { id: "shop", name: "Online shop relaunch", client: "Pembury Home", color: "light-dark(#2563eb, #6b9bff)" },
523
+ { id: "booking", name: "Booking app", client: "Saltmarsh Clinics", color: "light-dark(#0d9488, #3cc7b8)" },
524
+ { id: "portal", name: "Member portal", client: "Rowan Credit Union", color: "light-dark(#7c3aed, #a98bfa)" },
525
+ { id: "intranet", name: "Intranet", client: "Tidewell (internal)", color: "light-dark(#c2410c, #f08a52)" },
526
+ ];
351
527
 
352
- export default function SprintAroundLeave() {
353
- const [work, setWork] = useState<readonly Subtask[]>(START);
528
+ interface Sprint {
529
+ id: string;
530
+ name: string;
531
+ from: number;
532
+ to: number;
533
+ goal: string;
534
+ }
354
535
 
355
- const onIntent = (intent: Intent) => {
356
- if (intent.kind === "place") return;
357
- setWork((current) => current.map((item) => applyIntent(item, intent)));
358
- };
536
+ /** Two weeks each, Monday 09:00 to the second Friday 17:00. */
537
+ const SPRINTS: readonly Sprint[] = [
538
+ { name: "Sprint 12", day: 9 - 28, goal: "Shop checkout on the new design" },
539
+ { name: "Sprint 13", day: 9 - 14, goal: "Booking app in the stores' beta" },
540
+ { name: "Sprint 14", day: 9, goal: "Member portal sign-in and profile" },
541
+ { name: "Sprint 15", day: 23, goal: "Shop search and filters" },
542
+ { name: "Sprint 16", day: 37, goal: "Booking reminders" },
543
+ ].map(({ name, day, goal }) => ({ id: name.toLowerCase().replace(" ", "-"), name, from: at(day), to: at(day + 11, 17), goal }));
359
544
 
360
- const clashes = findings(work, [], BLOCKED).inBlockedTime;
545
+ /** A work item, shaped as `@umriss-ui/schedule`'s `Subtask`. */
546
+ interface WorkItem {
547
+ id: string;
548
+ /** The project. */
549
+ task: string;
550
+ /** The person. */
551
+ lane: string;
552
+ from: number;
553
+ to: number;
554
+ name: string;
555
+ sprint: string;
556
+ /** Hours, as estimated at planning. */
557
+ estimate: number;
558
+ status: "to do" | "in progress" | "in review" | "done";
559
+ }
361
560
 
362
- return (
363
- <Stack gap={3}>
364
- <Stack direction="row" gap={2} align="center" wrap data-callout="1">
365
- <Badge tone="accent">{SPRINT.name}</Badge>
561
+ const item = (id: string, task: string, lane: string, from: number, to: number, name: string, estimate: number, status: WorkItem["status"]): WorkItem =>
562
+ ({ id, task, lane, from, to, name, sprint: "sprint-14", estimate, status });
563
+
564
+ const WORK: readonly WorkItem[] = [
565
+ item("w-101", "portal", "arjun", at(9), at(11, 17), "Sign-in with e-mail code", 18, "done"),
566
+ item("w-102", "portal", "arjun", at(12), at(17, 17), "Profile page", 26, "in progress"),
567
+ item("w-103", "portal", "chloe", at(9), at(10, 17), "Session handling", 12, "done"),
568
+ item("w-104", "shop", "chloe", at(11), at(13, 17), "Basket keeps items across devices", 20, "in review"),
569
+ item("w-105", "portal", "chloe", at(16), at(19, 17), "Change of address form", 24, "in progress"),
570
+ item("w-106", "portal", "noah", at(9), at(12, 13), "Profile page design", 14, "done"),
571
+ item("w-107", "shop", "noah", at(16), at(18, 17), "Search results layout", 12, "to do"),
572
+ item("w-108", "portal", "eva", at(12), at(13, 17), "Test plan for sign-in", 10, "done"),
573
+ item("w-109", "portal", "eva", at(17), at(20, 17), "Regression run", 20, "to do"),
574
+ item("w-110", "booking", "hana", at(9), at(13, 17), "Reminder scheduling service", 32, "done"),
575
+ item("w-111", "booking", "hana", at(16), at(20, 17), "Push notifications", 30, "in progress"),
576
+ item("w-112", "booking", "kofi", at(10), at(12, 17), "Calendar sync", 18, "done"),
577
+ item("w-113", "intranet", "kofi", at(16), at(18, 17), "News feed", 16, "in progress"),
578
+ item("w-114", "booking", "freya", at(9), at(11, 17), "Reminder settings screen", 16, "done"),
579
+ item("w-115", "booking", "freya", at(18), at(20, 17), "Store screenshots", 12, "to do"),
580
+ item("w-116", "booking", "david", at(16), at(19, 13), "Device test matrix", 14, "to do"),
581
+ ];
582
+
583
+ interface Leave {
584
+ id: string;
585
+ person: string;
586
+ from: number;
587
+ to: number;
588
+ reason: "Holiday" | "Sick" | "Training";
589
+ }
590
+
591
+ /** When people are away: blocked time on their lane. */
592
+ const LEAVE: readonly Leave[] = [
593
+ { id: "l-1", person: "noah", from: at(13, 0, 0), to: at(14, 0, 0), reason: "Sick" },
594
+ { id: "l-2", person: "arjun", from: at(18, 0, 0), to: at(21, 0, 0), reason: "Holiday" },
595
+ { id: "l-3", person: "kofi", from: at(13, 0, 0), to: at(14, 0, 0), reason: "Training" },
596
+ { id: "l-4", person: "freya", from: at(16, 0, 0), to: at(18, 0, 0), reason: "Holiday" },
597
+ { id: "l-5", person: "david", from: at(9, 0, 0), to: at(14, 0, 0), reason: "Holiday" },
598
+ { id: "l-6", person: "maya", from: at(19, 13, 0), to: at(20, 17, 0), reason: "Training" },
599
+ ];
600
+
601
+ const SPRINT = SPRINTS.find((one) => one.id === "sprint-14")!;
602
+
603
+ /* The working days of the sprint: the axis shows these and nothing else. */
604
+ const WORKING_DAYS = [9, 10, 11, 12, 13, 16, 17, 18, 19, 20].map((day) => ({
605
+ from: at(day, 8),
606
+ to: at(day, 18),
607
+ }));
608
+
609
+ /* Added after planning, before anyone looked at the leave table. */
610
+ const START: readonly Subtask[] = [
611
+ ...WORK,
612
+ {
613
+ id: "w-117",
614
+ task: "portal",
615
+ lane: "arjun",
616
+ from: at(18),
617
+ to: at(19, 17),
618
+ name: "Account deletion",
619
+ },
620
+ ];
621
+
622
+ const BLOCKED: readonly BlockedTime[] = LEAVE.map((leave) => ({
623
+ id: leave.id,
624
+ lane: leave.person,
625
+ from: leave.from,
626
+ to: leave.to,
627
+ label: leave.reason,
628
+ }));
629
+
630
+ const teamOf = (person: string) => PEOPLE.find((one) => one.id === person)?.team;
631
+ const nameOf = (person: string) => PEOPLE.find((one) => one.id === person)?.name ?? person;
632
+
633
+ export default function SprintAroundLeave() {
634
+ const [work, setWork] = useState<readonly Subtask[]>(START);
635
+
636
+ const onIntent = (intent: Intent) => {
637
+ if (intent.kind === "place") return;
638
+ setWork((current) => current.map((item) => applyIntent(item, intent)));
639
+ };
640
+
641
+ const clashes = findings(work, [], BLOCKED).inBlockedTime;
642
+
643
+ return (
644
+ <Stack gap={3}>
645
+ <Stack direction="row" gap={2} align="center" wrap data-callout="1">
646
+ <Badge tone="accent">{SPRINT.name}</Badge>
366
647
  <Text size="sm">{SPRINT.goal}</Text>
367
648
  <Text size="sm" tone="secondary">
368
649
  9 to 20 March
@@ -421,8 +702,6 @@ export default function SprintAroundLeave() {
421
702
  }
422
703
  ```
423
704
 
424
- It imports `planning.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
425
-
426
705
  ### Replan the day on the line
427
706
 
428
707
  A production planner at the tile works moves orders between machines during the day, lets the cascade follow and reads the findings before committing.
@@ -438,8 +717,77 @@ Built from: First schedule, Dependencies, Move and lane, Stretch, lead-in, lead-
438
717
  import { useState } from "react";
439
718
  import { Checkbox, ContextMenu, MenuItem, MenuSeparator, Stack, Text } from "@umriss-ui/core";
440
719
  import { Lane, Schedule, Subtasks, Dependencies, applyIntent, findings, ripple, shiftTask } from "@umriss-ui/schedule";
441
- import type { Intent, ScheduleInteraction, Subtask } from "@umriss-ui/schedule";
442
- import { DAY_OF_PLAN, MOVES, ORDERS, STATIONS, STEPS } from "./plant";
720
+ import type { Dependency, Intent, ScheduleInteraction, Subtask, Task } from "@umriss-ui/schedule";
721
+
722
+ /* Data from the plant world, written out here so the example runs on its own. */
723
+
724
+ const planAt = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
725
+ const min = (n: number) => n * 60_000;
726
+
727
+ const DAY_OF_PLAN: readonly [number, number] = [planAt(5, 30), planAt(18)];
728
+
729
+ const STATIONS = [
730
+ { id: "saw", label: "Saw 1" },
731
+ { id: "lathe-1", label: "Lathe 1" },
732
+ { id: "lathe-2", label: "Lathe 2" },
733
+ { id: "mill", label: "Mill" },
734
+ { id: "press", label: "Press 2" },
735
+ { id: "paint", label: "Paint shop" },
736
+ { id: "qa", label: "Inspection" },
737
+ ] as const;
738
+
739
+ /* Colours in both schemes, as the application's own tokens would give them. */
740
+ const ORDERS: readonly Task[] = [
741
+ { id: "a-2041", name: "A-2041 Housing", color: "light-dark(#2563eb, #6b9bff)" },
742
+ { id: "a-2042", name: "A-2042 Shaft", color: "light-dark(#0d9488, #3cc7b8)" },
743
+ { id: "a-2043", name: "A-2043 Bracket", color: "light-dark(#c2410c, #f08a52)" },
744
+ { id: "a-2044", name: "A-2044 Flange", color: "light-dark(#7c3aed, #a98bfa)" },
745
+ { id: "a-2045", name: "A-2045 Cover", color: "light-dark(#be185d, #f06aa6)" },
746
+ { id: "a-2046", name: "A-2046 Axle", color: "light-dark(#4d7c0f, #8fc43e)" },
747
+ ];
748
+
749
+ const STEPS: readonly Subtask[] = [
750
+ { id: "a-2041-1", task: "a-2041", lane: "saw", from: planAt(6), to: planAt(7), leadIn: min(15), leadOut: min(10) },
751
+ { id: "a-2041-2", task: "a-2041", lane: "mill", from: planAt(8), to: planAt(10, 30), leadIn: min(30), leadOut: min(15) },
752
+ { id: "a-2041-3", task: "a-2041", lane: "qa", from: planAt(11, 30), to: planAt(12, 15) },
753
+
754
+ { id: "a-2042-1", task: "a-2042", lane: "saw", from: planAt(7, 30), to: planAt(8, 15), leadIn: min(10) },
755
+ { id: "a-2042-2", task: "a-2042", lane: "lathe-1", from: planAt(9), to: planAt(11), leadIn: min(20), leadOut: min(15) },
756
+ { id: "a-2042-3", task: "a-2042", lane: "qa", from: planAt(13), to: planAt(13, 30) },
757
+
758
+ { id: "a-2043-1", task: "a-2043", lane: "press", from: planAt(6, 30), to: planAt(8), leadIn: min(30), leadOut: min(15) },
759
+ { id: "a-2043-2", task: "a-2043", lane: "mill", from: planAt(10), to: planAt(11, 30), leadIn: min(15) },
760
+ { id: "a-2043-3", task: "a-2043", lane: "paint", from: planAt(12), to: planAt(14), leadIn: min(20), leadOut: min(20) },
761
+
762
+ { id: "a-2044-1", task: "a-2044", lane: "lathe-2", from: planAt(6), to: planAt(8, 30), leadIn: min(20), leadOut: min(10) },
763
+ { id: "a-2044-2", task: "a-2044", lane: "press", from: planAt(9, 30), to: planAt(10, 45), leadIn: min(25) },
764
+ { id: "a-2044-3", task: "a-2044", lane: "paint", from: planAt(14, 45), to: planAt(16), leadIn: min(15), leadOut: min(15) },
765
+
766
+ { id: "a-2045-1", task: "a-2045", lane: "lathe-1", from: planAt(12), to: planAt(13, 30), leadIn: min(15), leadOut: min(10) },
767
+ { id: "a-2045-2", task: "a-2045", lane: "press", from: planAt(14, 15), to: planAt(15), leadIn: min(20) },
768
+ { id: "a-2045-3", task: "a-2045", lane: "qa", from: planAt(15, 45), to: planAt(16, 30) },
769
+
770
+ { id: "a-2046-1", task: "a-2046", lane: "lathe-2", from: planAt(10), to: planAt(12), leadIn: min(20), leadOut: min(15) },
771
+ { id: "a-2046-2", task: "a-2046", lane: "mill", from: planAt(13), to: planAt(14, 30), leadIn: min(20), leadOut: min(10) },
772
+ { id: "a-2046-3", task: "a-2046", lane: "qa", from: planAt(15), to: planAt(15, 30) },
773
+ ];
774
+
775
+ const MOVES: readonly Dependency[] = [
776
+ { id: "t-2041-1", from: "a-2041-1", to: "a-2041-2", lag: min(10) },
777
+ { id: "t-2041-2", from: "a-2041-2", to: "a-2041-3", lag: min(20) },
778
+ { id: "t-2042-1", from: "a-2042-1", to: "a-2042-2", lag: min(15) },
779
+ { id: "t-2042-2", from: "a-2042-2", to: "a-2042-3", lag: min(30), leaves: "main" },
780
+ { id: "t-2043-1", from: "a-2043-1", to: "a-2043-2", lag: min(45) },
781
+ /* Leaves the mill at 11:30 and has 10 minutes to reach the paint shop's
782
+ lead-in at 11:40 - it takes 25: a violated dependency, on purpose. */
783
+ { id: "t-2043-2", from: "a-2043-2", to: "a-2043-3", lag: min(25) },
784
+ { id: "t-2044-1", from: "a-2044-1", to: "a-2044-2", lag: min(20) },
785
+ { id: "t-2044-2", from: "a-2044-2", to: "a-2044-3", lag: min(60), arrives: "main" },
786
+ { id: "t-2045-1", from: "a-2045-1", to: "a-2045-2", lag: min(15) },
787
+ { id: "t-2045-2", from: "a-2045-2", to: "a-2045-3", lag: min(15) },
788
+ { id: "t-2046-1", from: "a-2046-1", to: "a-2046-2", lag: min(20) },
789
+ { id: "t-2046-2", from: "a-2046-2", to: "a-2046-3", lag: min(10), leaves: "main" },
790
+ ];
443
791
 
444
792
  /* The whole recipe, as an application writes it.
445
793
 
@@ -549,8 +897,6 @@ export default function ReplanTheDay() {
549
897
  }
550
898
  ```
551
899
 
552
- It imports `plant.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
553
-
554
900
  ## Getting started
555
901
 
556
902
  What an application installs and hands over before the first plan.
@@ -725,7 +1071,77 @@ The full case: seven stations, six orders and their transports, the present mark
725
1071
 
726
1072
  ```tsx
727
1073
  import { Dependencies, Lane, Schedule, Subtasks } from "@umriss-ui/schedule";
728
- import { DAY_OF_PLAN, MOVES, ORDERS, STATIONS, STEPS } from "./plant";
1074
+ import type { Dependency, Subtask, Task } from "@umriss-ui/schedule";
1075
+
1076
+ /* Data from the plant world, written out here so the example runs on its own. */
1077
+
1078
+ const planAt = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
1079
+ const min = (n: number) => n * 60_000;
1080
+
1081
+ const DAY_OF_PLAN: readonly [number, number] = [planAt(5, 30), planAt(18)];
1082
+
1083
+ const STATIONS = [
1084
+ { id: "saw", label: "Saw 1" },
1085
+ { id: "lathe-1", label: "Lathe 1" },
1086
+ { id: "lathe-2", label: "Lathe 2" },
1087
+ { id: "mill", label: "Mill" },
1088
+ { id: "press", label: "Press 2" },
1089
+ { id: "paint", label: "Paint shop" },
1090
+ { id: "qa", label: "Inspection" },
1091
+ ] as const;
1092
+
1093
+ /* Colours in both schemes, as the application's own tokens would give them. */
1094
+ const ORDERS: readonly Task[] = [
1095
+ { id: "a-2041", name: "A-2041 Housing", color: "light-dark(#2563eb, #6b9bff)" },
1096
+ { id: "a-2042", name: "A-2042 Shaft", color: "light-dark(#0d9488, #3cc7b8)" },
1097
+ { id: "a-2043", name: "A-2043 Bracket", color: "light-dark(#c2410c, #f08a52)" },
1098
+ { id: "a-2044", name: "A-2044 Flange", color: "light-dark(#7c3aed, #a98bfa)" },
1099
+ { id: "a-2045", name: "A-2045 Cover", color: "light-dark(#be185d, #f06aa6)" },
1100
+ { id: "a-2046", name: "A-2046 Axle", color: "light-dark(#4d7c0f, #8fc43e)" },
1101
+ ];
1102
+
1103
+ const STEPS: readonly Subtask[] = [
1104
+ { id: "a-2041-1", task: "a-2041", lane: "saw", from: planAt(6), to: planAt(7), leadIn: min(15), leadOut: min(10) },
1105
+ { id: "a-2041-2", task: "a-2041", lane: "mill", from: planAt(8), to: planAt(10, 30), leadIn: min(30), leadOut: min(15) },
1106
+ { id: "a-2041-3", task: "a-2041", lane: "qa", from: planAt(11, 30), to: planAt(12, 15) },
1107
+
1108
+ { id: "a-2042-1", task: "a-2042", lane: "saw", from: planAt(7, 30), to: planAt(8, 15), leadIn: min(10) },
1109
+ { id: "a-2042-2", task: "a-2042", lane: "lathe-1", from: planAt(9), to: planAt(11), leadIn: min(20), leadOut: min(15) },
1110
+ { id: "a-2042-3", task: "a-2042", lane: "qa", from: planAt(13), to: planAt(13, 30) },
1111
+
1112
+ { id: "a-2043-1", task: "a-2043", lane: "press", from: planAt(6, 30), to: planAt(8), leadIn: min(30), leadOut: min(15) },
1113
+ { id: "a-2043-2", task: "a-2043", lane: "mill", from: planAt(10), to: planAt(11, 30), leadIn: min(15) },
1114
+ { id: "a-2043-3", task: "a-2043", lane: "paint", from: planAt(12), to: planAt(14), leadIn: min(20), leadOut: min(20) },
1115
+
1116
+ { id: "a-2044-1", task: "a-2044", lane: "lathe-2", from: planAt(6), to: planAt(8, 30), leadIn: min(20), leadOut: min(10) },
1117
+ { id: "a-2044-2", task: "a-2044", lane: "press", from: planAt(9, 30), to: planAt(10, 45), leadIn: min(25) },
1118
+ { id: "a-2044-3", task: "a-2044", lane: "paint", from: planAt(14, 45), to: planAt(16), leadIn: min(15), leadOut: min(15) },
1119
+
1120
+ { id: "a-2045-1", task: "a-2045", lane: "lathe-1", from: planAt(12), to: planAt(13, 30), leadIn: min(15), leadOut: min(10) },
1121
+ { id: "a-2045-2", task: "a-2045", lane: "press", from: planAt(14, 15), to: planAt(15), leadIn: min(20) },
1122
+ { id: "a-2045-3", task: "a-2045", lane: "qa", from: planAt(15, 45), to: planAt(16, 30) },
1123
+
1124
+ { id: "a-2046-1", task: "a-2046", lane: "lathe-2", from: planAt(10), to: planAt(12), leadIn: min(20), leadOut: min(15) },
1125
+ { id: "a-2046-2", task: "a-2046", lane: "mill", from: planAt(13), to: planAt(14, 30), leadIn: min(20), leadOut: min(10) },
1126
+ { id: "a-2046-3", task: "a-2046", lane: "qa", from: planAt(15), to: planAt(15, 30) },
1127
+ ];
1128
+
1129
+ const MOVES: readonly Dependency[] = [
1130
+ { id: "t-2041-1", from: "a-2041-1", to: "a-2041-2", lag: min(10) },
1131
+ { id: "t-2041-2", from: "a-2041-2", to: "a-2041-3", lag: min(20) },
1132
+ { id: "t-2042-1", from: "a-2042-1", to: "a-2042-2", lag: min(15) },
1133
+ { id: "t-2042-2", from: "a-2042-2", to: "a-2042-3", lag: min(30), leaves: "main" },
1134
+ { id: "t-2043-1", from: "a-2043-1", to: "a-2043-2", lag: min(45) },
1135
+ /* Leaves the mill at 11:30 and has 10 minutes to reach the paint shop's
1136
+ lead-in at 11:40 - it takes 25: a violated dependency, on purpose. */
1137
+ { id: "t-2043-2", from: "a-2043-2", to: "a-2043-3", lag: min(25) },
1138
+ { id: "t-2044-1", from: "a-2044-1", to: "a-2044-2", lag: min(20) },
1139
+ { id: "t-2044-2", from: "a-2044-2", to: "a-2044-3", lag: min(60), arrives: "main" },
1140
+ { id: "t-2045-1", from: "a-2045-1", to: "a-2045-2", lag: min(15) },
1141
+ { id: "t-2045-2", from: "a-2045-2", to: "a-2045-3", lag: min(15) },
1142
+ { id: "t-2046-1", from: "a-2046-1", to: "a-2046-2", lag: min(20) },
1143
+ { id: "t-2046-2", from: "a-2046-2", to: "a-2046-3", lag: min(10), leaves: "main" },
1144
+ ];
729
1145
 
730
1146
  const NAMES = new Map(ORDERS.map((order) => [order.id, order.name ?? order.id]));
731
1147
  const HALF_PAST_TEN = new Date(2026, 2, 17, 10, 30).getTime();
@@ -749,8 +1165,6 @@ export default function MachinePlan() {
749
1165
  }
750
1166
  ```
751
1167
 
752
- It imports `plant.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
753
-
754
1168
  #### Keyboard
755
1169
 
756
1170
  | Key | Action |
@@ -1302,33 +1716,108 @@ Work items straight from the team's tracker: the person is the lane, the project
1302
1716
  ```tsx
1303
1717
  import { Lane, Schedule, Subtasks } from "@umriss-ui/schedule";
1304
1718
  import type { Subtask } from "@umriss-ui/schedule";
1305
- import { PEOPLE, PROJECTS, WORK } from "./planning";
1306
1719
 
1307
- const SUBTASKS: Subtask[] = WORK.map((item): Subtask => ({
1308
- ...item,
1309
- /* Not started yet: drawn hollow, as planned but not begun. */
1310
- ...(item.status === "to do" ? { appearance: ["provisional"] } : {}),
1311
- }));
1720
+ /* Data from the planning world, written out here so the example runs on its own. */
1312
1721
 
1313
- const SPRINT_14: readonly [number, number] = [new Date(2026, 2, 9, 6).getTime(), new Date(2026, 2, 21).getTime()];
1722
+ const at = (day: number, hours = 9, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
1314
1723
 
1315
- export default function ASprint() {
1316
- return (
1317
- <Schedule ariaLabel="Sprint 14, 9 to 20 March" initialDomain={SPRINT_14} height={500} label={(s) => s.name ?? s.id}>
1318
- {PEOPLE.map((person) => (
1319
- <Lane key={person.id} id={person.id} label={person.name} />
1320
- ))}
1321
- <Subtasks data={SUBTASKS} tasks={PROJECTS} />
1322
- </Schedule>
1323
- );
1724
+ interface Person {
1725
+ id: string;
1726
+ name: string;
1727
+ role: "Developer" | "Designer" | "Product manager" | "QA engineer";
1728
+ team: "Web" | "Apps";
1729
+ /** Hours a week they can be planned for. */
1730
+ capacity: number;
1324
1731
  }
1325
- ```
1326
1732
 
1327
- It imports `planning.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
1733
+ const PEOPLE: readonly Person[] = [
1734
+ { id: "maya", name: "Maya Lindgren", role: "Product manager", team: "Web", capacity: 32 },
1735
+ { id: "arjun", name: "Arjun Mehta", role: "Developer", team: "Web", capacity: 40 },
1736
+ { id: "chloe", name: "Chloe Durand", role: "Developer", team: "Web", capacity: 40 },
1737
+ { id: "noah", name: "Noah Fischer", role: "Designer", team: "Web", capacity: 24 },
1738
+ { id: "eva", name: "Eva Novak", role: "QA engineer", team: "Web", capacity: 40 },
1739
+ { id: "luis", name: "Luis Moreno", role: "Product manager", team: "Apps", capacity: 40 },
1740
+ { id: "hana", name: "Hana Sato", role: "Developer", team: "Apps", capacity: 40 },
1741
+ { id: "kofi", name: "Kofi Mensah", role: "Developer", team: "Apps", capacity: 32 },
1742
+ { id: "freya", name: "Freya Olsen", role: "Designer", team: "Apps", capacity: 40 },
1743
+ { id: "david", name: "David Kowalski", role: "QA engineer", team: "Apps", capacity: 20 },
1744
+ ];
1328
1745
 
1329
- #### API
1746
+ interface Project {
1747
+ id: string;
1748
+ name: string;
1749
+ client: string;
1750
+ color: string;
1751
+ }
1330
1752
 
1331
- ##### `SubtasksProps`
1753
+ const PROJECTS: readonly Project[] = [
1754
+ { id: "shop", name: "Online shop relaunch", client: "Pembury Home", color: "light-dark(#2563eb, #6b9bff)" },
1755
+ { id: "booking", name: "Booking app", client: "Saltmarsh Clinics", color: "light-dark(#0d9488, #3cc7b8)" },
1756
+ { id: "portal", name: "Member portal", client: "Rowan Credit Union", color: "light-dark(#7c3aed, #a98bfa)" },
1757
+ { id: "intranet", name: "Intranet", client: "Tidewell (internal)", color: "light-dark(#c2410c, #f08a52)" },
1758
+ ];
1759
+
1760
+ /** A work item, shaped as `@umriss-ui/schedule`'s `Subtask`. */
1761
+ interface WorkItem {
1762
+ id: string;
1763
+ /** The project. */
1764
+ task: string;
1765
+ /** The person. */
1766
+ lane: string;
1767
+ from: number;
1768
+ to: number;
1769
+ name: string;
1770
+ sprint: string;
1771
+ /** Hours, as estimated at planning. */
1772
+ estimate: number;
1773
+ status: "to do" | "in progress" | "in review" | "done";
1774
+ }
1775
+
1776
+ const item = (id: string, task: string, lane: string, from: number, to: number, name: string, estimate: number, status: WorkItem["status"]): WorkItem =>
1777
+ ({ id, task, lane, from, to, name, sprint: "sprint-14", estimate, status });
1778
+
1779
+ const WORK: readonly WorkItem[] = [
1780
+ item("w-101", "portal", "arjun", at(9), at(11, 17), "Sign-in with e-mail code", 18, "done"),
1781
+ item("w-102", "portal", "arjun", at(12), at(17, 17), "Profile page", 26, "in progress"),
1782
+ item("w-103", "portal", "chloe", at(9), at(10, 17), "Session handling", 12, "done"),
1783
+ item("w-104", "shop", "chloe", at(11), at(13, 17), "Basket keeps items across devices", 20, "in review"),
1784
+ item("w-105", "portal", "chloe", at(16), at(19, 17), "Change of address form", 24, "in progress"),
1785
+ item("w-106", "portal", "noah", at(9), at(12, 13), "Profile page design", 14, "done"),
1786
+ item("w-107", "shop", "noah", at(16), at(18, 17), "Search results layout", 12, "to do"),
1787
+ item("w-108", "portal", "eva", at(12), at(13, 17), "Test plan for sign-in", 10, "done"),
1788
+ item("w-109", "portal", "eva", at(17), at(20, 17), "Regression run", 20, "to do"),
1789
+ item("w-110", "booking", "hana", at(9), at(13, 17), "Reminder scheduling service", 32, "done"),
1790
+ item("w-111", "booking", "hana", at(16), at(20, 17), "Push notifications", 30, "in progress"),
1791
+ item("w-112", "booking", "kofi", at(10), at(12, 17), "Calendar sync", 18, "done"),
1792
+ item("w-113", "intranet", "kofi", at(16), at(18, 17), "News feed", 16, "in progress"),
1793
+ item("w-114", "booking", "freya", at(9), at(11, 17), "Reminder settings screen", 16, "done"),
1794
+ item("w-115", "booking", "freya", at(18), at(20, 17), "Store screenshots", 12, "to do"),
1795
+ item("w-116", "booking", "david", at(16), at(19, 13), "Device test matrix", 14, "to do"),
1796
+ ];
1797
+
1798
+ const SUBTASKS: Subtask[] = WORK.map((item): Subtask => ({
1799
+ ...item,
1800
+ /* Not started yet: drawn hollow, as planned but not begun. */
1801
+ ...(item.status === "to do" ? { appearance: ["provisional"] } : {}),
1802
+ }));
1803
+
1804
+ const SPRINT_14: readonly [number, number] = [new Date(2026, 2, 9, 6).getTime(), new Date(2026, 2, 21).getTime()];
1805
+
1806
+ export default function ASprint() {
1807
+ return (
1808
+ <Schedule ariaLabel="Sprint 14, 9 to 20 March" initialDomain={SPRINT_14} height={500} label={(s) => s.name ?? s.id}>
1809
+ {PEOPLE.map((person) => (
1810
+ <Lane key={person.id} id={person.id} label={person.name} />
1811
+ ))}
1812
+ <Subtasks data={SUBTASKS} tasks={PROJECTS} />
1813
+ </Schedule>
1814
+ );
1815
+ }
1816
+ ```
1817
+
1818
+ #### API
1819
+
1820
+ ##### `SubtasksProps`
1332
1821
 
1333
1822
  | Prop | Type | Default | Description |
1334
1823
  |---|---|---|---|
@@ -2263,7 +2752,48 @@ A week of the on-call rota: the day band names each day, and the time band's ste
2263
2752
  ```tsx
2264
2753
  import { Lane, Schedule, Subtasks } from "@umriss-ui/schedule";
2265
2754
  import type { Subtask, Task } from "@umriss-ui/schedule";
2266
- import { ENGINEERS, ONCALL } from "./operations";
2755
+
2756
+ /* Data from the operations world, written out here so the example runs on its own. */
2757
+
2758
+ const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
2759
+
2760
+ interface Engineer {
2761
+ id: string;
2762
+ name: string;
2763
+ team: string;
2764
+ }
2765
+
2766
+ const ENGINEERS: readonly Engineer[] = [
2767
+ { id: "priya", name: "Priya Raman", team: "Payments" },
2768
+ { id: "jonas", name: "Jonas Keller", team: "Payments" },
2769
+ { id: "ada", name: "Ada Mwangi", team: "Identity" },
2770
+ { id: "tomasz", name: "Tomasz Nowak", team: "Discovery" },
2771
+ { id: "leila", name: "Leila Haddad", team: "Discovery" },
2772
+ { id: "sam", name: "Sam Okafor", team: "Messaging" },
2773
+ { id: "ines", name: "Ines Duarte", team: "Integrations" },
2774
+ { id: "felix", name: "Felix Brandt", team: "Insights" },
2775
+ ];
2776
+
2777
+ interface OnCall {
2778
+ id: string;
2779
+ engineer: string;
2780
+ rotation: "primary" | "secondary";
2781
+ from: number;
2782
+ to: number;
2783
+ }
2784
+
2785
+ /** Monday 16 to Monday 23 March, handed over every morning at 09:00: the
2786
+ secondary of one day is the primary of the next. */
2787
+ const ONCALL: readonly OnCall[] = Array.from({ length: 7 }, (_, day) => [
2788
+ { rotation: "primary" as const, engineer: ENGINEERS[day % ENGINEERS.length]!.id },
2789
+ { rotation: "secondary" as const, engineer: ENGINEERS[(day + 1) % ENGINEERS.length]!.id },
2790
+ ].map(({ rotation, engineer }) => ({
2791
+ id: `${rotation}-${16 + day}`,
2792
+ engineer,
2793
+ rotation,
2794
+ from: at(16 + day, 9),
2795
+ to: at(17 + day, 9),
2796
+ }))).flat();
2267
2797
 
2268
2798
  const ROTATIONS: Task[] = [
2269
2799
  { id: "primary", name: "Primary", color: "light-dark(#2563eb, #6b9bff)" },
@@ -2286,8 +2816,6 @@ export default function AWeek() {
2286
2816
  }
2287
2817
  ```
2288
2818
 
2289
- It imports `operations.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
2290
-
2291
2819
  ##### Cut the nights out of the axis
2292
2820
 
2293
2821
  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.
@@ -2344,9 +2872,84 @@ Weekdays 09:00 to 17:00 as the `calendar`: a two-week sprint reads as ten workin
2344
2872
 
2345
2873
  ```tsx
2346
2874
  import { Lane, Schedule, Subtasks } from "@umriss-ui/schedule";
2347
- import { PEOPLE, PROJECTS, WORK } from "./planning";
2348
2875
 
2349
- const at = (day: number, hours: number) => new Date(2026, 2, day, hours).getTime();
2876
+ /* Data from the planning world, written out here so the example runs on its own. */
2877
+
2878
+ const at = (day: number, hours = 9, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
2879
+
2880
+ interface Person {
2881
+ id: string;
2882
+ name: string;
2883
+ role: "Developer" | "Designer" | "Product manager" | "QA engineer";
2884
+ team: "Web" | "Apps";
2885
+ /** Hours a week they can be planned for. */
2886
+ capacity: number;
2887
+ }
2888
+
2889
+ const PEOPLE: readonly Person[] = [
2890
+ { id: "maya", name: "Maya Lindgren", role: "Product manager", team: "Web", capacity: 32 },
2891
+ { id: "arjun", name: "Arjun Mehta", role: "Developer", team: "Web", capacity: 40 },
2892
+ { id: "chloe", name: "Chloe Durand", role: "Developer", team: "Web", capacity: 40 },
2893
+ { id: "noah", name: "Noah Fischer", role: "Designer", team: "Web", capacity: 24 },
2894
+ { id: "eva", name: "Eva Novak", role: "QA engineer", team: "Web", capacity: 40 },
2895
+ { id: "luis", name: "Luis Moreno", role: "Product manager", team: "Apps", capacity: 40 },
2896
+ { id: "hana", name: "Hana Sato", role: "Developer", team: "Apps", capacity: 40 },
2897
+ { id: "kofi", name: "Kofi Mensah", role: "Developer", team: "Apps", capacity: 32 },
2898
+ { id: "freya", name: "Freya Olsen", role: "Designer", team: "Apps", capacity: 40 },
2899
+ { id: "david", name: "David Kowalski", role: "QA engineer", team: "Apps", capacity: 20 },
2900
+ ];
2901
+
2902
+ interface Project {
2903
+ id: string;
2904
+ name: string;
2905
+ client: string;
2906
+ color: string;
2907
+ }
2908
+
2909
+ const PROJECTS: readonly Project[] = [
2910
+ { id: "shop", name: "Online shop relaunch", client: "Pembury Home", color: "light-dark(#2563eb, #6b9bff)" },
2911
+ { id: "booking", name: "Booking app", client: "Saltmarsh Clinics", color: "light-dark(#0d9488, #3cc7b8)" },
2912
+ { id: "portal", name: "Member portal", client: "Rowan Credit Union", color: "light-dark(#7c3aed, #a98bfa)" },
2913
+ { id: "intranet", name: "Intranet", client: "Tidewell (internal)", color: "light-dark(#c2410c, #f08a52)" },
2914
+ ];
2915
+
2916
+ /** A work item, shaped as `@umriss-ui/schedule`'s `Subtask`. */
2917
+ interface WorkItem {
2918
+ id: string;
2919
+ /** The project. */
2920
+ task: string;
2921
+ /** The person. */
2922
+ lane: string;
2923
+ from: number;
2924
+ to: number;
2925
+ name: string;
2926
+ sprint: string;
2927
+ /** Hours, as estimated at planning. */
2928
+ estimate: number;
2929
+ status: "to do" | "in progress" | "in review" | "done";
2930
+ }
2931
+
2932
+ const item = (id: string, task: string, lane: string, from: number, to: number, name: string, estimate: number, status: WorkItem["status"]): WorkItem =>
2933
+ ({ id, task, lane, from, to, name, sprint: "sprint-14", estimate, status });
2934
+
2935
+ const WORK: readonly WorkItem[] = [
2936
+ item("w-101", "portal", "arjun", at(9), at(11, 17), "Sign-in with e-mail code", 18, "done"),
2937
+ item("w-102", "portal", "arjun", at(12), at(17, 17), "Profile page", 26, "in progress"),
2938
+ item("w-103", "portal", "chloe", at(9), at(10, 17), "Session handling", 12, "done"),
2939
+ item("w-104", "shop", "chloe", at(11), at(13, 17), "Basket keeps items across devices", 20, "in review"),
2940
+ item("w-105", "portal", "chloe", at(16), at(19, 17), "Change of address form", 24, "in progress"),
2941
+ item("w-106", "portal", "noah", at(9), at(12, 13), "Profile page design", 14, "done"),
2942
+ item("w-107", "shop", "noah", at(16), at(18, 17), "Search results layout", 12, "to do"),
2943
+ item("w-108", "portal", "eva", at(12), at(13, 17), "Test plan for sign-in", 10, "done"),
2944
+ item("w-109", "portal", "eva", at(17), at(20, 17), "Regression run", 20, "to do"),
2945
+ item("w-110", "booking", "hana", at(9), at(13, 17), "Reminder scheduling service", 32, "done"),
2946
+ item("w-111", "booking", "hana", at(16), at(20, 17), "Push notifications", 30, "in progress"),
2947
+ item("w-112", "booking", "kofi", at(10), at(12, 17), "Calendar sync", 18, "done"),
2948
+ item("w-113", "intranet", "kofi", at(16), at(18, 17), "News feed", 16, "in progress"),
2949
+ item("w-114", "booking", "freya", at(9), at(11, 17), "Reminder settings screen", 16, "done"),
2950
+ item("w-115", "booking", "freya", at(18), at(20, 17), "Store screenshots", 12, "to do"),
2951
+ item("w-116", "booking", "david", at(16), at(19, 13), "Device test matrix", 14, "to do"),
2952
+ ];
2350
2953
 
2351
2954
  const WORKING_DAYS = [9, 10, 11, 12, 13, 16, 17, 18, 19, 20];
2352
2955
  const OFFICE_HOURS = WORKING_DAYS.map((day) => ({ from: at(day, 9), to: at(day, 17) }));
@@ -2372,8 +2975,6 @@ export default function OfficeHours() {
2372
2975
  }
2373
2976
  ```
2374
2977
 
2375
- It imports `planning.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
2376
-
2377
2978
  #### When to use something else
2378
2979
 
2379
2980
  - Time a single lane is not available → Blocked time
@@ -2540,55 +3141,148 @@ The team's holidays, sick days and training become `BlockedTimes`, and `findings
2540
3141
  import { Stack, Text } from "@umriss-ui/core";
2541
3142
  import { BlockedTimes, Lane, Schedule, Subtasks, findings } from "@umriss-ui/schedule";
2542
3143
  import type { BlockedTime } from "@umriss-ui/schedule";
2543
- import { LEAVE, PEOPLE, PROJECTS, WORK } from "./planning";
2544
-
2545
- const BLOCKED: BlockedTime[] = LEAVE.map((leave) => ({ id: leave.id, lane: leave.person, from: leave.from, to: leave.to, label: leave.reason }));
2546
3144
 
2547
- const SPRINT_14: readonly [number, number] = [new Date(2026, 2, 9, 6).getTime(), new Date(2026, 2, 21).getTime()];
3145
+ /* Data from the planning world, written out here so the example runs on its own. */
2548
3146
 
2549
- const NAME = new Map(WORK.map((item) => [item.id, item.name]));
2550
- const PERSON = new Map(PEOPLE.map((person) => [person.id, person.name]));
3147
+ const at = (day: number, hours = 9, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
2551
3148
 
2552
- export default function TeamLeave() {
2553
- const clashes = findings(WORK, [], BLOCKED).inBlockedTime;
2554
- return (
2555
- <Stack gap={3}>
2556
- <Schedule ariaLabel="Sprint 14 with the team's leave" initialDomain={SPRINT_14} height={500}>
2557
- {PEOPLE.map((person) => (
2558
- <Lane key={person.id} id={person.id} label={person.name} />
2559
- ))}
2560
- <BlockedTimes data={BLOCKED} />
2561
- <Subtasks data={WORK} tasks={PROJECTS} />
2562
- </Schedule>
2563
- <Text size="sm" tone="secondary">
2564
- {clashes.length === 0
2565
- ? "No work falls into anyone's leave."
2566
- : clashes.map((clash) => `${NAME.get(clash.subtask)} (${PERSON.get(WORK.find((w) => w.id === clash.subtask)?.lane ?? "")})`).join(" · ")}
2567
- </Text>
2568
- </Stack>
2569
- );
3149
+ interface Person {
3150
+ id: string;
3151
+ name: string;
3152
+ role: "Developer" | "Designer" | "Product manager" | "QA engineer";
3153
+ team: "Web" | "Apps";
3154
+ /** Hours a week they can be planned for. */
3155
+ capacity: number;
2570
3156
  }
2571
- ```
2572
-
2573
- It imports `planning.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
2574
-
2575
- #### When to use something else
2576
3157
 
2577
- - Hours nobody works, on every lane → Time axis and calendar
3158
+ const PEOPLE: readonly Person[] = [
3159
+ { id: "maya", name: "Maya Lindgren", role: "Product manager", team: "Web", capacity: 32 },
3160
+ { id: "arjun", name: "Arjun Mehta", role: "Developer", team: "Web", capacity: 40 },
3161
+ { id: "chloe", name: "Chloe Durand", role: "Developer", team: "Web", capacity: 40 },
3162
+ { id: "noah", name: "Noah Fischer", role: "Designer", team: "Web", capacity: 24 },
3163
+ { id: "eva", name: "Eva Novak", role: "QA engineer", team: "Web", capacity: 40 },
3164
+ { id: "luis", name: "Luis Moreno", role: "Product manager", team: "Apps", capacity: 40 },
3165
+ { id: "hana", name: "Hana Sato", role: "Developer", team: "Apps", capacity: 40 },
3166
+ { id: "kofi", name: "Kofi Mensah", role: "Developer", team: "Apps", capacity: 32 },
3167
+ { id: "freya", name: "Freya Olsen", role: "Designer", team: "Apps", capacity: 40 },
3168
+ { id: "david", name: "David Kowalski", role: "QA engineer", team: "Apps", capacity: 20 },
3169
+ ];
2578
3170
 
2579
- #### API
3171
+ interface Project {
3172
+ id: string;
3173
+ name: string;
3174
+ client: string;
3175
+ color: string;
3176
+ }
2580
3177
 
2581
- ##### `BlockedTimesProps`
3178
+ const PROJECTS: readonly Project[] = [
3179
+ { id: "shop", name: "Online shop relaunch", client: "Pembury Home", color: "light-dark(#2563eb, #6b9bff)" },
3180
+ { id: "booking", name: "Booking app", client: "Saltmarsh Clinics", color: "light-dark(#0d9488, #3cc7b8)" },
3181
+ { id: "portal", name: "Member portal", client: "Rowan Credit Union", color: "light-dark(#7c3aed, #a98bfa)" },
3182
+ { id: "intranet", name: "Intranet", client: "Tidewell (internal)", color: "light-dark(#c2410c, #f08a52)" },
3183
+ ];
2582
3184
 
2583
- | Prop | Type | Default | Description |
2584
- |---|---|---|---|
2585
- | `data` (required) | `readonly BlockedTime[]` | — | The blocked time of the lanes - leave, maintenance, unavailability -, as the caller holds it: one list, each interval naming its lane. |
3185
+ /** A work item, shaped as `@umriss-ui/schedule`'s `Subtask`. */
3186
+ interface WorkItem {
3187
+ id: string;
3188
+ /** The project. */
3189
+ task: string;
3190
+ /** The person. */
3191
+ lane: string;
3192
+ from: number;
3193
+ to: number;
3194
+ name: string;
3195
+ sprint: string;
3196
+ /** Hours, as estimated at planning. */
3197
+ estimate: number;
3198
+ status: "to do" | "in progress" | "in review" | "done";
3199
+ }
2586
3200
 
2587
- ##### `BlockedTime`
3201
+ const item = (id: string, task: string, lane: string, from: number, to: number, name: string, estimate: number, status: WorkItem["status"]): WorkItem =>
3202
+ ({ id, task, lane, from, to, name, sprint: "sprint-14", estimate, status });
2588
3203
 
2589
- | Prop | Type | Default | Description |
2590
- |---|---|---|---|
2591
- | `id` (required) | `string` | — | The caller's identity of the interval - what a finding names. |
3204
+ const WORK: readonly WorkItem[] = [
3205
+ item("w-101", "portal", "arjun", at(9), at(11, 17), "Sign-in with e-mail code", 18, "done"),
3206
+ item("w-102", "portal", "arjun", at(12), at(17, 17), "Profile page", 26, "in progress"),
3207
+ item("w-103", "portal", "chloe", at(9), at(10, 17), "Session handling", 12, "done"),
3208
+ item("w-104", "shop", "chloe", at(11), at(13, 17), "Basket keeps items across devices", 20, "in review"),
3209
+ item("w-105", "portal", "chloe", at(16), at(19, 17), "Change of address form", 24, "in progress"),
3210
+ item("w-106", "portal", "noah", at(9), at(12, 13), "Profile page design", 14, "done"),
3211
+ item("w-107", "shop", "noah", at(16), at(18, 17), "Search results layout", 12, "to do"),
3212
+ item("w-108", "portal", "eva", at(12), at(13, 17), "Test plan for sign-in", 10, "done"),
3213
+ item("w-109", "portal", "eva", at(17), at(20, 17), "Regression run", 20, "to do"),
3214
+ item("w-110", "booking", "hana", at(9), at(13, 17), "Reminder scheduling service", 32, "done"),
3215
+ item("w-111", "booking", "hana", at(16), at(20, 17), "Push notifications", 30, "in progress"),
3216
+ item("w-112", "booking", "kofi", at(10), at(12, 17), "Calendar sync", 18, "done"),
3217
+ item("w-113", "intranet", "kofi", at(16), at(18, 17), "News feed", 16, "in progress"),
3218
+ item("w-114", "booking", "freya", at(9), at(11, 17), "Reminder settings screen", 16, "done"),
3219
+ item("w-115", "booking", "freya", at(18), at(20, 17), "Store screenshots", 12, "to do"),
3220
+ item("w-116", "booking", "david", at(16), at(19, 13), "Device test matrix", 14, "to do"),
3221
+ ];
3222
+
3223
+ interface Leave {
3224
+ id: string;
3225
+ person: string;
3226
+ from: number;
3227
+ to: number;
3228
+ reason: "Holiday" | "Sick" | "Training";
3229
+ }
3230
+
3231
+ /** When people are away: blocked time on their lane. */
3232
+ const LEAVE: readonly Leave[] = [
3233
+ { id: "l-1", person: "noah", from: at(13, 0, 0), to: at(14, 0, 0), reason: "Sick" },
3234
+ { id: "l-2", person: "arjun", from: at(18, 0, 0), to: at(21, 0, 0), reason: "Holiday" },
3235
+ { id: "l-3", person: "kofi", from: at(13, 0, 0), to: at(14, 0, 0), reason: "Training" },
3236
+ { id: "l-4", person: "freya", from: at(16, 0, 0), to: at(18, 0, 0), reason: "Holiday" },
3237
+ { id: "l-5", person: "david", from: at(9, 0, 0), to: at(14, 0, 0), reason: "Holiday" },
3238
+ { id: "l-6", person: "maya", from: at(19, 13, 0), to: at(20, 17, 0), reason: "Training" },
3239
+ ];
3240
+
3241
+ const BLOCKED: BlockedTime[] = LEAVE.map((leave) => ({ id: leave.id, lane: leave.person, from: leave.from, to: leave.to, label: leave.reason }));
3242
+
3243
+ const SPRINT_14: readonly [number, number] = [new Date(2026, 2, 9, 6).getTime(), new Date(2026, 2, 21).getTime()];
3244
+
3245
+ const NAME = new Map(WORK.map((item) => [item.id, item.name]));
3246
+ const PERSON = new Map(PEOPLE.map((person) => [person.id, person.name]));
3247
+
3248
+ export default function TeamLeave() {
3249
+ const clashes = findings(WORK, [], BLOCKED).inBlockedTime;
3250
+ return (
3251
+ <Stack gap={3}>
3252
+ <Schedule ariaLabel="Sprint 14 with the team's leave" initialDomain={SPRINT_14} height={500}>
3253
+ {PEOPLE.map((person) => (
3254
+ <Lane key={person.id} id={person.id} label={person.name} />
3255
+ ))}
3256
+ <BlockedTimes data={BLOCKED} />
3257
+ <Subtasks data={WORK} tasks={PROJECTS} />
3258
+ </Schedule>
3259
+ <Text size="sm" tone="secondary">
3260
+ {clashes.length === 0
3261
+ ? "No work falls into anyone's leave."
3262
+ : clashes.map((clash) => `${NAME.get(clash.subtask)} (${PERSON.get(WORK.find((w) => w.id === clash.subtask)?.lane ?? "")})`).join(" · ")}
3263
+ </Text>
3264
+ </Stack>
3265
+ );
3266
+ }
3267
+ ```
3268
+
3269
+ #### When to use something else
3270
+
3271
+ - Hours nobody works, on every lane → Time axis and calendar
3272
+
3273
+ #### API
3274
+
3275
+ ##### `BlockedTimesProps`
3276
+
3277
+ | Prop | Type | Default | Description |
3278
+ |---|---|---|---|
3279
+ | `data` (required) | `readonly BlockedTime[]` | — | The blocked time of the lanes - leave, maintenance, unavailability -, as the caller holds it: one list, each interval naming its lane. |
3280
+
3281
+ ##### `BlockedTime`
3282
+
3283
+ | Prop | Type | Default | Description |
3284
+ |---|---|---|---|
3285
+ | `id` (required) | `string` | — | The caller's identity of the interval - what a finding names. |
2592
3286
  | `lane` (required) | `string` | — | The id of the lane it blocks. |
2593
3287
  | `from` (required) | `number` | — | Where it begins. |
2594
3288
  | `to` (required) | `number` | — | Where it ends. |
@@ -2613,12 +3307,136 @@ Pass an instant to `now` and a line crosses the lanes there: a dispatcher at 10:
2613
3307
  ```tsx
2614
3308
  import { Lane, Schedule, Subtasks } from "@umriss-ui/schedule";
2615
3309
  import type { Subtask, Task } from "@umriss-ui/schedule";
2616
- import { NOW, TOURS, VEHICLES } from "./logistics";
2617
3310
 
2618
- /* A fixed instant suits a replay and a picture that must hold still; in a
2619
- live application `now` alone follows the clock - the next example. */
3311
+ /* Data from the logistics world, written out here so the example runs on its own. */
3312
+
3313
+ /** A small LCG - the same numbers on every computer. */
3314
+ function random(seed: number): () => number {
3315
+ let s = seed >>> 0;
3316
+ return () => {
3317
+ s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
3318
+ return s / 4294967296;
3319
+ };
3320
+ }
2620
3321
 
2621
3322
  const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
3323
+ const MINUTE = 60_000;
3324
+
3325
+ /** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
3326
+ const NOW = at(10, 30);
3327
+
3328
+ interface Vehicle {
3329
+ id: string;
3330
+ plate: string;
3331
+ type: "van" | "e-van" | "truck";
3332
+ /** Payload, in kg. */
3333
+ capacity: number;
3334
+ depot: string;
3335
+ }
3336
+
3337
+ const VEHICLES: readonly Vehicle[] = [
3338
+ { id: "v1", plate: "FP 214 K", type: "van", capacity: 1200, depot: "north" },
3339
+ { id: "v2", plate: "FP 377 K", type: "e-van", capacity: 900, depot: "north" },
3340
+ { id: "v3", plate: "FP 118 R", type: "truck", capacity: 7500, depot: "north" },
3341
+ { id: "v4", plate: "FP 402 R", type: "van", capacity: 1200, depot: "river" },
3342
+ { id: "v5", plate: "FP 455 R", type: "e-van", capacity: 900, depot: "river" },
3343
+ { id: "v6", plate: "FP 290 E", type: "van", capacity: 1200, depot: "east" },
3344
+ { id: "v7", plate: "FP 311 E", type: "e-van", capacity: 900, depot: "east" },
3345
+ { id: "v8", plate: "FP 520 E", type: "truck", capacity: 7500, depot: "east" },
3346
+ ];
3347
+
3348
+ const DRIVERS = [
3349
+ { id: "d1", name: "Martin Hale", depot: "north" },
3350
+ { id: "d2", name: "Nadia Petrova", depot: "north" },
3351
+ { id: "d3", name: "Owen Carter", depot: "north" },
3352
+ { id: "d4", name: "Lucia Romero", depot: "river" },
3353
+ { id: "d5", name: "Ben Adeyemi", depot: "river" },
3354
+ { id: "d6", name: "Hanna Berg", depot: "east" },
3355
+ { id: "d7", name: "Yusuf Demir", depot: "east" },
3356
+ { id: "d8", name: "Clara Wendt", depot: "east" },
3357
+ ] as const;
3358
+
3359
+ const CUSTOMERS = [
3360
+ "Holloway Garden Supplies",
3361
+ "Marlow & Finch Books",
3362
+ "Oakridge Pharmacy",
3363
+ "Brixley Cycles",
3364
+ "Tamsin's Bakery",
3365
+ "Northfold Office",
3366
+ "Pellham Hardware",
3367
+ "Greywick Studio",
3368
+ "Juniper Lane Florist",
3369
+ "Ashcombe Dental",
3370
+ ] as const;
3371
+
3372
+ interface Stop {
3373
+ id: string;
3374
+ shipment: string;
3375
+ customer: string;
3376
+ /** The window the customer was promised, `[from, to]`. */
3377
+ window: readonly [number, number];
3378
+ /** When the tour plans to be there. */
3379
+ arrival: number;
3380
+ }
3381
+
3382
+ interface Tour {
3383
+ id: string;
3384
+ vehicle: string;
3385
+ driver: string;
3386
+ depot: string;
3387
+ /** Leaves the depot, loaded; back at the depot. */
3388
+ from: number;
3389
+ to: number;
3390
+ /** Minutes of loading before `from`. */
3391
+ loading: number;
3392
+ km: number;
3393
+ stops: readonly Stop[];
3394
+ }
3395
+
3396
+ /** One tour per vehicle. Stops every half hour or so, each with a two-hour
3397
+ window; every seventh arrival misses its window - the late ones a
3398
+ dispatcher looks for. */
3399
+ function tour(index: number): Tour {
3400
+ const r = random(700 + index);
3401
+ const vehicle = VEHICLES[index]!;
3402
+ const truck = vehicle.type === "truck";
3403
+ const from = at(truck ? 6 : 7, index % 2 === 0 ? 0 : 30);
3404
+ const count = truck ? 5 : 8 + Math.floor(r() * 4);
3405
+ const stops: Stop[] = [];
3406
+ let arrival = from + (20 + Math.floor(r() * 15)) * MINUTE;
3407
+ for (let i = 0; i < count; i++) {
3408
+ const n = index * 20 + i;
3409
+ const slot = Math.floor((arrival - at(0)) / (2 * 60 * MINUTE)) * 2;
3410
+ /* A late stop's window closed half an hour or more before the arrival. */
3411
+ const late = i >= 2 && n % 7 === 3;
3412
+ const halfHour = 30 * MINUTE;
3413
+ const windowFrom = late ? Math.floor(arrival / halfHour) * halfHour - 5 * halfHour : at(slot);
3414
+ stops.push({
3415
+ id: `s-${index + 1}-${i + 1}`,
3416
+ shipment: `FP-${(1_004_210 + n * 13).toString()}`,
3417
+ customer: CUSTOMERS[(n * 3) % CUSTOMERS.length]!,
3418
+ window: [windowFrom, windowFrom + 2 * 60 * MINUTE],
3419
+ arrival,
3420
+ });
3421
+ arrival += (truck ? 45 : 22) * MINUTE + Math.floor(r() * 18) * MINUTE;
3422
+ }
3423
+ return {
3424
+ id: `T-${String(index + 1).padStart(2, "0")}`,
3425
+ vehicle: vehicle.id,
3426
+ driver: DRIVERS[index]!.id,
3427
+ depot: vehicle.depot,
3428
+ from,
3429
+ to: arrival + 30 * MINUTE,
3430
+ loading: truck ? 45 : 30,
3431
+ km: Math.round((truck ? 140 : 70) + r() * 40),
3432
+ stops,
3433
+ };
3434
+ }
3435
+
3436
+ const TOURS: readonly Tour[] = VEHICLES.map((_, index) => tour(index));
3437
+
3438
+ /* A fixed instant suits a replay and a picture that must hold still; in a
3439
+ live application `now` alone follows the clock - the next example. */
2622
3440
 
2623
3441
  const COLORS = ["light-dark(#2563eb, #6b9bff)", "light-dark(#0d9488, #3cc7b8)", "light-dark(#c2410c, #f08a52)", "light-dark(#7c3aed, #a98bfa)"];
2624
3442
 
@@ -2645,8 +3463,6 @@ export default function NowLine() {
2645
3463
  }
2646
3464
  ```
2647
3465
 
2648
- It imports `logistics.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
2649
-
2650
3466
  ##### Follow the clock
2651
3467
 
2652
3468
  `now` on its own reads the clock and moves on by the minute, so a plan left open on a wall screen stays true.
@@ -2999,7 +3815,48 @@ import { useState } from "react";
2999
3815
  import { ContextMenu, MenuItem } from "@umriss-ui/core";
3000
3816
  import { Lane, Schedule, Subtasks, applyIntent } from "@umriss-ui/schedule";
3001
3817
  import type { ScheduleInteraction, Subtask, Task } from "@umriss-ui/schedule";
3002
- import { ENGINEERS, ONCALL } from "./operations";
3818
+
3819
+ /* Data from the operations world, written out here so the example runs on its own. */
3820
+
3821
+ const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
3822
+
3823
+ interface Engineer {
3824
+ id: string;
3825
+ name: string;
3826
+ team: string;
3827
+ }
3828
+
3829
+ const ENGINEERS: readonly Engineer[] = [
3830
+ { id: "priya", name: "Priya Raman", team: "Payments" },
3831
+ { id: "jonas", name: "Jonas Keller", team: "Payments" },
3832
+ { id: "ada", name: "Ada Mwangi", team: "Identity" },
3833
+ { id: "tomasz", name: "Tomasz Nowak", team: "Discovery" },
3834
+ { id: "leila", name: "Leila Haddad", team: "Discovery" },
3835
+ { id: "sam", name: "Sam Okafor", team: "Messaging" },
3836
+ { id: "ines", name: "Ines Duarte", team: "Integrations" },
3837
+ { id: "felix", name: "Felix Brandt", team: "Insights" },
3838
+ ];
3839
+
3840
+ interface OnCall {
3841
+ id: string;
3842
+ engineer: string;
3843
+ rotation: "primary" | "secondary";
3844
+ from: number;
3845
+ to: number;
3846
+ }
3847
+
3848
+ /** Monday 16 to Monday 23 March, handed over every morning at 09:00: the
3849
+ secondary of one day is the primary of the next. */
3850
+ const ONCALL: readonly OnCall[] = Array.from({ length: 7 }, (_, day) => [
3851
+ { rotation: "primary" as const, engineer: ENGINEERS[day % ENGINEERS.length]!.id },
3852
+ { rotation: "secondary" as const, engineer: ENGINEERS[(day + 1) % ENGINEERS.length]!.id },
3853
+ ].map(({ rotation, engineer }) => ({
3854
+ id: `${rotation}-${16 + day}`,
3855
+ engineer,
3856
+ rotation,
3857
+ from: at(16 + day, 9),
3858
+ to: at(17 + day, 9),
3859
+ }))).flat();
3003
3860
 
3004
3861
  const day = (d: number, hours = 0) => new Date(2026, 2, d, hours).getTime();
3005
3862
 
@@ -3067,8 +3924,6 @@ export default function OpenAContextMenu() {
3067
3924
  }
3068
3925
  ```
3069
3926
 
3070
- It imports `operations.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
3071
-
3072
3927
  #### When to use something else
3073
3928
 
3074
3929
  - You only need to know which task was picked → Selection
@@ -3236,21 +4091,97 @@ import { useState } from "react";
3236
4091
  import { Badge, Card, CardBody, Stack, Text } from "@umriss-ui/core";
3237
4092
  import { Lane, Schedule, Subtasks } from "@umriss-ui/schedule";
3238
4093
  import type { ScheduleInteraction } from "@umriss-ui/schedule";
3239
- import { PEOPLE, PROJECTS, WORK } from "./planning";
3240
- import type { WorkItem } from "./planning";
3241
4094
 
3242
- const day = (d: number, hours = 0) => new Date(2026, 2, d, hours).getTime();
4095
+ /* Data from the planning world, written out here so the example runs on its own. */
3243
4096
 
3244
- const WEB = PEOPLE.filter((person) => person.team === "Web");
3245
- const WEB_WORK = WORK.filter((item) => WEB.some((person) => person.id === item.lane));
4097
+ const at = (day: number, hours = 9, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
3246
4098
 
3247
- export default function DetailPanelInstead() {
3248
- const [item, setItem] = useState<WorkItem | null>(null);
4099
+ interface Person {
4100
+ id: string;
4101
+ name: string;
4102
+ role: "Developer" | "Designer" | "Product manager" | "QA engineer";
4103
+ team: "Web" | "Apps";
4104
+ /** Hours a week they can be planned for. */
4105
+ capacity: number;
4106
+ }
3249
4107
 
3250
- const onInteraction = (interaction: ScheduleInteraction) => {
3251
- if (interaction.type !== "hover") return;
3252
- const hit = interaction.hit;
3253
- setItem(hit.kind === "subtask" ? (WEB_WORK.find((one) => one.id === hit.subtask.id) ?? null) : null);
4108
+ const PEOPLE: readonly Person[] = [
4109
+ { id: "maya", name: "Maya Lindgren", role: "Product manager", team: "Web", capacity: 32 },
4110
+ { id: "arjun", name: "Arjun Mehta", role: "Developer", team: "Web", capacity: 40 },
4111
+ { id: "chloe", name: "Chloe Durand", role: "Developer", team: "Web", capacity: 40 },
4112
+ { id: "noah", name: "Noah Fischer", role: "Designer", team: "Web", capacity: 24 },
4113
+ { id: "eva", name: "Eva Novak", role: "QA engineer", team: "Web", capacity: 40 },
4114
+ { id: "luis", name: "Luis Moreno", role: "Product manager", team: "Apps", capacity: 40 },
4115
+ { id: "hana", name: "Hana Sato", role: "Developer", team: "Apps", capacity: 40 },
4116
+ { id: "kofi", name: "Kofi Mensah", role: "Developer", team: "Apps", capacity: 32 },
4117
+ { id: "freya", name: "Freya Olsen", role: "Designer", team: "Apps", capacity: 40 },
4118
+ { id: "david", name: "David Kowalski", role: "QA engineer", team: "Apps", capacity: 20 },
4119
+ ];
4120
+
4121
+ interface Project {
4122
+ id: string;
4123
+ name: string;
4124
+ client: string;
4125
+ color: string;
4126
+ }
4127
+
4128
+ const PROJECTS: readonly Project[] = [
4129
+ { id: "shop", name: "Online shop relaunch", client: "Pembury Home", color: "light-dark(#2563eb, #6b9bff)" },
4130
+ { id: "booking", name: "Booking app", client: "Saltmarsh Clinics", color: "light-dark(#0d9488, #3cc7b8)" },
4131
+ { id: "portal", name: "Member portal", client: "Rowan Credit Union", color: "light-dark(#7c3aed, #a98bfa)" },
4132
+ { id: "intranet", name: "Intranet", client: "Tidewell (internal)", color: "light-dark(#c2410c, #f08a52)" },
4133
+ ];
4134
+
4135
+ /** A work item, shaped as `@umriss-ui/schedule`'s `Subtask`. */
4136
+ interface WorkItem {
4137
+ id: string;
4138
+ /** The project. */
4139
+ task: string;
4140
+ /** The person. */
4141
+ lane: string;
4142
+ from: number;
4143
+ to: number;
4144
+ name: string;
4145
+ sprint: string;
4146
+ /** Hours, as estimated at planning. */
4147
+ estimate: number;
4148
+ status: "to do" | "in progress" | "in review" | "done";
4149
+ }
4150
+
4151
+ const item = (id: string, task: string, lane: string, from: number, to: number, name: string, estimate: number, status: WorkItem["status"]): WorkItem =>
4152
+ ({ id, task, lane, from, to, name, sprint: "sprint-14", estimate, status });
4153
+
4154
+ const WORK: readonly WorkItem[] = [
4155
+ item("w-101", "portal", "arjun", at(9), at(11, 17), "Sign-in with e-mail code", 18, "done"),
4156
+ item("w-102", "portal", "arjun", at(12), at(17, 17), "Profile page", 26, "in progress"),
4157
+ item("w-103", "portal", "chloe", at(9), at(10, 17), "Session handling", 12, "done"),
4158
+ item("w-104", "shop", "chloe", at(11), at(13, 17), "Basket keeps items across devices", 20, "in review"),
4159
+ item("w-105", "portal", "chloe", at(16), at(19, 17), "Change of address form", 24, "in progress"),
4160
+ item("w-106", "portal", "noah", at(9), at(12, 13), "Profile page design", 14, "done"),
4161
+ item("w-107", "shop", "noah", at(16), at(18, 17), "Search results layout", 12, "to do"),
4162
+ item("w-108", "portal", "eva", at(12), at(13, 17), "Test plan for sign-in", 10, "done"),
4163
+ item("w-109", "portal", "eva", at(17), at(20, 17), "Regression run", 20, "to do"),
4164
+ item("w-110", "booking", "hana", at(9), at(13, 17), "Reminder scheduling service", 32, "done"),
4165
+ item("w-111", "booking", "hana", at(16), at(20, 17), "Push notifications", 30, "in progress"),
4166
+ item("w-112", "booking", "kofi", at(10), at(12, 17), "Calendar sync", 18, "done"),
4167
+ item("w-113", "intranet", "kofi", at(16), at(18, 17), "News feed", 16, "in progress"),
4168
+ item("w-114", "booking", "freya", at(9), at(11, 17), "Reminder settings screen", 16, "done"),
4169
+ item("w-115", "booking", "freya", at(18), at(20, 17), "Store screenshots", 12, "to do"),
4170
+ item("w-116", "booking", "david", at(16), at(19, 13), "Device test matrix", 14, "to do"),
4171
+ ];
4172
+
4173
+ const day = (d: number, hours = 0) => new Date(2026, 2, d, hours).getTime();
4174
+
4175
+ const WEB = PEOPLE.filter((person) => person.team === "Web");
4176
+ const WEB_WORK = WORK.filter((item) => WEB.some((person) => person.id === item.lane));
4177
+
4178
+ export default function DetailPanelInstead() {
4179
+ const [item, setItem] = useState<WorkItem | null>(null);
4180
+
4181
+ const onInteraction = (interaction: ScheduleInteraction) => {
4182
+ if (interaction.type !== "hover") return;
4183
+ const hit = interaction.hit;
4184
+ setItem(hit.kind === "subtask" ? (WEB_WORK.find((one) => one.id === hit.subtask.id) ?? null) : null);
3254
4185
  };
3255
4186
 
3256
4187
  return (
@@ -3294,8 +4225,6 @@ export default function DetailPanelInstead() {
3294
4225
  }
3295
4226
  ```
3296
4227
 
3297
- It imports `planning.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
3298
-
3299
4228
  #### API
3300
4229
 
3301
4230
  ##### `ScheduleTooltipTarget`
@@ -3408,7 +4337,97 @@ import { useState } from "react";
3408
4337
  import { Stack } from "@umriss-ui/core";
3409
4338
  import { Lane, Schedule, Subtasks } from "@umriss-ui/schedule";
3410
4339
  import type { Subtask, Task } from "@umriss-ui/schedule";
3411
- import { ENGINEERS, INCIDENTS, NOW, ONCALL, SERVICES } from "./operations";
4340
+
4341
+ /* Data from the operations world, written out here so the example runs on its own. */
4342
+
4343
+ const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
4344
+
4345
+ /** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
4346
+ const NOW = at(17, 10, 30);
4347
+
4348
+ interface Service {
4349
+ id: string;
4350
+ name: string;
4351
+ team: string;
4352
+ /** 1 is customer-facing and pages at night; 3 waits for the morning. */
4353
+ tier: 1 | 2 | 3;
4354
+ /** The latency objective: the 95th percentile stays below this, in ms. */
4355
+ latencySlo: number;
4356
+ /** The availability promised for a month, in per cent. */
4357
+ availabilityTarget: number;
4358
+ }
4359
+
4360
+ const SERVICES: readonly Service[] = [
4361
+ { id: "checkout", name: "Checkout", team: "Payments", tier: 1, latencySlo: 300, availabilityTarget: 99.95 },
4362
+ { id: "billing", name: "Billing", team: "Payments", tier: 1, latencySlo: 400, availabilityTarget: 99.9 },
4363
+ { id: "sign-in", name: "Sign-in", team: "Identity", tier: 1, latencySlo: 200, availabilityTarget: 99.95 },
4364
+ { id: "search", name: "Search", team: "Discovery", tier: 1, latencySlo: 250, availabilityTarget: 99.9 },
4365
+ { id: "images", name: "Image service", team: "Discovery", tier: 2, latencySlo: 500, availabilityTarget: 99.5 },
4366
+ { id: "notifications", name: "Notifications", team: "Messaging", tier: 2, latencySlo: 800, availabilityTarget: 99.5 },
4367
+ { id: "webhooks", name: "Webhooks", team: "Integrations", tier: 2, latencySlo: 1000, availabilityTarget: 99.5 },
4368
+ { id: "reports", name: "Reporting", team: "Insights", tier: 3, latencySlo: 2000, availabilityTarget: 99 },
4369
+ ];
4370
+
4371
+ interface Engineer {
4372
+ id: string;
4373
+ name: string;
4374
+ team: string;
4375
+ }
4376
+
4377
+ const ENGINEERS: readonly Engineer[] = [
4378
+ { id: "priya", name: "Priya Raman", team: "Payments" },
4379
+ { id: "jonas", name: "Jonas Keller", team: "Payments" },
4380
+ { id: "ada", name: "Ada Mwangi", team: "Identity" },
4381
+ { id: "tomasz", name: "Tomasz Nowak", team: "Discovery" },
4382
+ { id: "leila", name: "Leila Haddad", team: "Discovery" },
4383
+ { id: "sam", name: "Sam Okafor", team: "Messaging" },
4384
+ { id: "ines", name: "Ines Duarte", team: "Integrations" },
4385
+ { id: "felix", name: "Felix Brandt", team: "Insights" },
4386
+ ];
4387
+
4388
+ interface OnCall {
4389
+ id: string;
4390
+ engineer: string;
4391
+ rotation: "primary" | "secondary";
4392
+ from: number;
4393
+ to: number;
4394
+ }
4395
+
4396
+ /** Monday 16 to Monday 23 March, handed over every morning at 09:00: the
4397
+ secondary of one day is the primary of the next. */
4398
+ const ONCALL: readonly OnCall[] = Array.from({ length: 7 }, (_, day) => [
4399
+ { rotation: "primary" as const, engineer: ENGINEERS[day % ENGINEERS.length]!.id },
4400
+ { rotation: "secondary" as const, engineer: ENGINEERS[(day + 1) % ENGINEERS.length]!.id },
4401
+ ].map(({ rotation, engineer }) => ({
4402
+ id: `${rotation}-${16 + day}`,
4403
+ engineer,
4404
+ rotation,
4405
+ from: at(16 + day, 9),
4406
+ to: at(17 + day, 9),
4407
+ }))).flat();
4408
+
4409
+ interface Incident {
4410
+ id: string;
4411
+ title: string;
4412
+ service: string;
4413
+ severity: "SEV1" | "SEV2" | "SEV3";
4414
+ opened: number;
4415
+ acknowledged?: number;
4416
+ resolved?: number;
4417
+ /** An engineer's id. */
4418
+ assignee: string;
4419
+ }
4420
+
4421
+ const INCIDENTS: readonly Incident[] = [
4422
+ { id: "INC-1048", title: "Checkout slow, card payments time out", service: "checkout", severity: "SEV1", opened: at(17, 9, 42), acknowledged: at(17, 9, 46), assignee: "jonas" },
4423
+ { id: "INC-1047", title: "Webhook deliveries delayed", service: "webhooks", severity: "SEV3", opened: at(17, 7, 15), acknowledged: at(17, 8, 2), assignee: "ines" },
4424
+ { id: "INC-1046", title: "Thumbnails missing for new uploads", service: "images", severity: "SEV2", opened: at(16, 22, 5), acknowledged: at(16, 22, 11), resolved: at(17, 0, 40), assignee: "tomasz" },
4425
+ { id: "INC-1045", title: "Sign-in codes arrive late", service: "sign-in", severity: "SEV2", opened: at(16, 14, 20), acknowledged: at(16, 14, 24), resolved: at(16, 15, 5), assignee: "ada" },
4426
+ { id: "INC-1044", title: "Monthly report export fails", service: "reports", severity: "SEV3", opened: at(15, 10, 0), acknowledged: at(16, 9, 12), resolved: at(16, 11, 30), assignee: "felix" },
4427
+ { id: "INC-1043", title: "Search returns no results for some regions", service: "search", severity: "SEV1", opened: at(14, 18, 30), acknowledged: at(14, 18, 33), resolved: at(14, 19, 55), assignee: "leila" },
4428
+ { id: "INC-1042", title: "Duplicate reminder e-mails", service: "notifications", severity: "SEV3", opened: at(13, 8, 45), acknowledged: at(13, 9, 30), resolved: at(13, 13, 0), assignee: "sam" },
4429
+ { id: "INC-1041", title: "Invoices generated twice", service: "billing", severity: "SEV2", opened: at(12, 16, 10), acknowledged: at(12, 16, 18), resolved: at(12, 18, 40), assignee: "priya" },
4430
+ ];
3412
4431
 
3413
4432
  const day = (d: number, hours = 0) => new Date(2026, 2, d, hours).getTime();
3414
4433
 
@@ -3463,8 +4482,6 @@ export default function RotaBesideIncidents() {
3463
4482
  }
3464
4483
  ```
3465
4484
 
3466
- It imports `operations.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
3467
-
3468
4485
  #### When to use something else
3469
4486
 
3470
4487
  - The lanes belong together in one plan → Lane groups
@@ -3719,7 +4736,84 @@ import { useRef, useState } from "react";
3719
4736
  import { Stack, Text } from "@umriss-ui/core";
3720
4737
  import { Lane, Schedule, Subtasks, applyIntent, overlaps } from "@umriss-ui/schedule";
3721
4738
  import type { Intent, Subtask } from "@umriss-ui/schedule";
3722
- import { PEOPLE, PROJECTS, WORK } from "./planning";
4739
+
4740
+ /* Data from the planning world, written out here so the example runs on its own. */
4741
+
4742
+ const at = (day: number, hours = 9, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
4743
+
4744
+ interface Person {
4745
+ id: string;
4746
+ name: string;
4747
+ role: "Developer" | "Designer" | "Product manager" | "QA engineer";
4748
+ team: "Web" | "Apps";
4749
+ /** Hours a week they can be planned for. */
4750
+ capacity: number;
4751
+ }
4752
+
4753
+ const PEOPLE: readonly Person[] = [
4754
+ { id: "maya", name: "Maya Lindgren", role: "Product manager", team: "Web", capacity: 32 },
4755
+ { id: "arjun", name: "Arjun Mehta", role: "Developer", team: "Web", capacity: 40 },
4756
+ { id: "chloe", name: "Chloe Durand", role: "Developer", team: "Web", capacity: 40 },
4757
+ { id: "noah", name: "Noah Fischer", role: "Designer", team: "Web", capacity: 24 },
4758
+ { id: "eva", name: "Eva Novak", role: "QA engineer", team: "Web", capacity: 40 },
4759
+ { id: "luis", name: "Luis Moreno", role: "Product manager", team: "Apps", capacity: 40 },
4760
+ { id: "hana", name: "Hana Sato", role: "Developer", team: "Apps", capacity: 40 },
4761
+ { id: "kofi", name: "Kofi Mensah", role: "Developer", team: "Apps", capacity: 32 },
4762
+ { id: "freya", name: "Freya Olsen", role: "Designer", team: "Apps", capacity: 40 },
4763
+ { id: "david", name: "David Kowalski", role: "QA engineer", team: "Apps", capacity: 20 },
4764
+ ];
4765
+
4766
+ interface Project {
4767
+ id: string;
4768
+ name: string;
4769
+ client: string;
4770
+ color: string;
4771
+ }
4772
+
4773
+ const PROJECTS: readonly Project[] = [
4774
+ { id: "shop", name: "Online shop relaunch", client: "Pembury Home", color: "light-dark(#2563eb, #6b9bff)" },
4775
+ { id: "booking", name: "Booking app", client: "Saltmarsh Clinics", color: "light-dark(#0d9488, #3cc7b8)" },
4776
+ { id: "portal", name: "Member portal", client: "Rowan Credit Union", color: "light-dark(#7c3aed, #a98bfa)" },
4777
+ { id: "intranet", name: "Intranet", client: "Tidewell (internal)", color: "light-dark(#c2410c, #f08a52)" },
4778
+ ];
4779
+
4780
+ /** A work item, shaped as `@umriss-ui/schedule`'s `Subtask`. */
4781
+ interface WorkItem {
4782
+ id: string;
4783
+ /** The project. */
4784
+ task: string;
4785
+ /** The person. */
4786
+ lane: string;
4787
+ from: number;
4788
+ to: number;
4789
+ name: string;
4790
+ sprint: string;
4791
+ /** Hours, as estimated at planning. */
4792
+ estimate: number;
4793
+ status: "to do" | "in progress" | "in review" | "done";
4794
+ }
4795
+
4796
+ const item = (id: string, task: string, lane: string, from: number, to: number, name: string, estimate: number, status: WorkItem["status"]): WorkItem =>
4797
+ ({ id, task, lane, from, to, name, sprint: "sprint-14", estimate, status });
4798
+
4799
+ const WORK: readonly WorkItem[] = [
4800
+ item("w-101", "portal", "arjun", at(9), at(11, 17), "Sign-in with e-mail code", 18, "done"),
4801
+ item("w-102", "portal", "arjun", at(12), at(17, 17), "Profile page", 26, "in progress"),
4802
+ item("w-103", "portal", "chloe", at(9), at(10, 17), "Session handling", 12, "done"),
4803
+ item("w-104", "shop", "chloe", at(11), at(13, 17), "Basket keeps items across devices", 20, "in review"),
4804
+ item("w-105", "portal", "chloe", at(16), at(19, 17), "Change of address form", 24, "in progress"),
4805
+ item("w-106", "portal", "noah", at(9), at(12, 13), "Profile page design", 14, "done"),
4806
+ item("w-107", "shop", "noah", at(16), at(18, 17), "Search results layout", 12, "to do"),
4807
+ item("w-108", "portal", "eva", at(12), at(13, 17), "Test plan for sign-in", 10, "done"),
4808
+ item("w-109", "portal", "eva", at(17), at(20, 17), "Regression run", 20, "to do"),
4809
+ item("w-110", "booking", "hana", at(9), at(13, 17), "Reminder scheduling service", 32, "done"),
4810
+ item("w-111", "booking", "hana", at(16), at(20, 17), "Push notifications", 30, "in progress"),
4811
+ item("w-112", "booking", "kofi", at(10), at(12, 17), "Calendar sync", 18, "done"),
4812
+ item("w-113", "intranet", "kofi", at(16), at(18, 17), "News feed", 16, "in progress"),
4813
+ item("w-114", "booking", "freya", at(9), at(11, 17), "Reminder settings screen", 16, "done"),
4814
+ item("w-115", "booking", "freya", at(18), at(20, 17), "Store screenshots", 12, "to do"),
4815
+ item("w-116", "booking", "david", at(16), at(19, 13), "Device test matrix", 14, "to do"),
4816
+ ];
3723
4817
 
3724
4818
  const day = (d: number, hours = 0) => new Date(2026, 2, d, hours).getTime();
3725
4819
 
@@ -3769,8 +4863,6 @@ export default function RefuseADoubleBooking() {
3769
4863
  }
3770
4864
  ```
3771
4865
 
3772
- It imports `planning.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
3773
-
3774
4866
  #### When to use something else
3775
4867
 
3776
4868
  - The work is not on the plan yet → Placing from outside
@@ -4354,32 +5446,127 @@ import { useState } from "react";
4354
5446
  import { Stack, Text } from "@umriss-ui/core";
4355
5447
  import { BlockedTimes, Lane, Schedule, Subtasks, applyIntent } from "@umriss-ui/schedule";
4356
5448
  import type { BlockedTime, Intent, Subtask } from "@umriss-ui/schedule";
4357
- import { LEAVE, PEOPLE, PROJECTS, WORK } from "./planning";
4358
-
4359
- const day = (d: number, hours = 0) => new Date(2026, 2, d, hours).getTime();
4360
5449
 
4361
- const APPS = PEOPLE.filter((person) => person.team === "Apps" && person.role !== "Product manager");
4362
- const START: readonly Subtask[] = WORK.filter((item) => APPS.some((person) => person.id === item.lane));
4363
- const AWAY: readonly BlockedTime[] = LEAVE.filter((leave) => APPS.some((person) => person.id === leave.person)).map((leave) => ({
4364
- id: leave.id,
4365
- lane: leave.person,
4366
- from: leave.from,
4367
- to: leave.to,
4368
- label: leave.reason,
4369
- }));
5450
+ /* Data from the planning world, written out here so the example runs on its own. */
4370
5451
 
4371
- /* Work stays with people of the role it was planned for. */
4372
- const roleOf = (lane: string) => PEOPLE.find((person) => person.id === lane)?.role;
4373
- const START_ROLE = new Map(START.map((item) => [item.id, roleOf(item.lane)]));
4374
- const mayGo = (item: Subtask, lane: string) => START_ROLE.get(item.id) === roleOf(lane);
5452
+ const at = (day: number, hours = 9, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
4375
5453
 
4376
- export default function AroundLeave() {
4377
- const [work, setWork] = useState<readonly Subtask[]>(START);
4378
- const [last, setLast] = useState("Drag Freya's store screenshots into her holiday");
5454
+ interface Person {
5455
+ id: string;
5456
+ name: string;
5457
+ role: "Developer" | "Designer" | "Product manager" | "QA engineer";
5458
+ team: "Web" | "Apps";
5459
+ /** Hours a week they can be planned for. */
5460
+ capacity: number;
5461
+ }
4379
5462
 
4380
- const onIntent = (intent: Intent) => {
4381
- if (intent.kind === "place") return;
4382
- setWork((current) => current.map((item) => applyIntent(item, intent)));
5463
+ const PEOPLE: readonly Person[] = [
5464
+ { id: "maya", name: "Maya Lindgren", role: "Product manager", team: "Web", capacity: 32 },
5465
+ { id: "arjun", name: "Arjun Mehta", role: "Developer", team: "Web", capacity: 40 },
5466
+ { id: "chloe", name: "Chloe Durand", role: "Developer", team: "Web", capacity: 40 },
5467
+ { id: "noah", name: "Noah Fischer", role: "Designer", team: "Web", capacity: 24 },
5468
+ { id: "eva", name: "Eva Novak", role: "QA engineer", team: "Web", capacity: 40 },
5469
+ { id: "luis", name: "Luis Moreno", role: "Product manager", team: "Apps", capacity: 40 },
5470
+ { id: "hana", name: "Hana Sato", role: "Developer", team: "Apps", capacity: 40 },
5471
+ { id: "kofi", name: "Kofi Mensah", role: "Developer", team: "Apps", capacity: 32 },
5472
+ { id: "freya", name: "Freya Olsen", role: "Designer", team: "Apps", capacity: 40 },
5473
+ { id: "david", name: "David Kowalski", role: "QA engineer", team: "Apps", capacity: 20 },
5474
+ ];
5475
+
5476
+ interface Project {
5477
+ id: string;
5478
+ name: string;
5479
+ client: string;
5480
+ color: string;
5481
+ }
5482
+
5483
+ const PROJECTS: readonly Project[] = [
5484
+ { id: "shop", name: "Online shop relaunch", client: "Pembury Home", color: "light-dark(#2563eb, #6b9bff)" },
5485
+ { id: "booking", name: "Booking app", client: "Saltmarsh Clinics", color: "light-dark(#0d9488, #3cc7b8)" },
5486
+ { id: "portal", name: "Member portal", client: "Rowan Credit Union", color: "light-dark(#7c3aed, #a98bfa)" },
5487
+ { id: "intranet", name: "Intranet", client: "Tidewell (internal)", color: "light-dark(#c2410c, #f08a52)" },
5488
+ ];
5489
+
5490
+ /** A work item, shaped as `@umriss-ui/schedule`'s `Subtask`. */
5491
+ interface WorkItem {
5492
+ id: string;
5493
+ /** The project. */
5494
+ task: string;
5495
+ /** The person. */
5496
+ lane: string;
5497
+ from: number;
5498
+ to: number;
5499
+ name: string;
5500
+ sprint: string;
5501
+ /** Hours, as estimated at planning. */
5502
+ estimate: number;
5503
+ status: "to do" | "in progress" | "in review" | "done";
5504
+ }
5505
+
5506
+ const item = (id: string, task: string, lane: string, from: number, to: number, name: string, estimate: number, status: WorkItem["status"]): WorkItem =>
5507
+ ({ id, task, lane, from, to, name, sprint: "sprint-14", estimate, status });
5508
+
5509
+ const WORK: readonly WorkItem[] = [
5510
+ item("w-101", "portal", "arjun", at(9), at(11, 17), "Sign-in with e-mail code", 18, "done"),
5511
+ item("w-102", "portal", "arjun", at(12), at(17, 17), "Profile page", 26, "in progress"),
5512
+ item("w-103", "portal", "chloe", at(9), at(10, 17), "Session handling", 12, "done"),
5513
+ item("w-104", "shop", "chloe", at(11), at(13, 17), "Basket keeps items across devices", 20, "in review"),
5514
+ item("w-105", "portal", "chloe", at(16), at(19, 17), "Change of address form", 24, "in progress"),
5515
+ item("w-106", "portal", "noah", at(9), at(12, 13), "Profile page design", 14, "done"),
5516
+ item("w-107", "shop", "noah", at(16), at(18, 17), "Search results layout", 12, "to do"),
5517
+ item("w-108", "portal", "eva", at(12), at(13, 17), "Test plan for sign-in", 10, "done"),
5518
+ item("w-109", "portal", "eva", at(17), at(20, 17), "Regression run", 20, "to do"),
5519
+ item("w-110", "booking", "hana", at(9), at(13, 17), "Reminder scheduling service", 32, "done"),
5520
+ item("w-111", "booking", "hana", at(16), at(20, 17), "Push notifications", 30, "in progress"),
5521
+ item("w-112", "booking", "kofi", at(10), at(12, 17), "Calendar sync", 18, "done"),
5522
+ item("w-113", "intranet", "kofi", at(16), at(18, 17), "News feed", 16, "in progress"),
5523
+ item("w-114", "booking", "freya", at(9), at(11, 17), "Reminder settings screen", 16, "done"),
5524
+ item("w-115", "booking", "freya", at(18), at(20, 17), "Store screenshots", 12, "to do"),
5525
+ item("w-116", "booking", "david", at(16), at(19, 13), "Device test matrix", 14, "to do"),
5526
+ ];
5527
+
5528
+ interface Leave {
5529
+ id: string;
5530
+ person: string;
5531
+ from: number;
5532
+ to: number;
5533
+ reason: "Holiday" | "Sick" | "Training";
5534
+ }
5535
+
5536
+ /** When people are away: blocked time on their lane. */
5537
+ const LEAVE: readonly Leave[] = [
5538
+ { id: "l-1", person: "noah", from: at(13, 0, 0), to: at(14, 0, 0), reason: "Sick" },
5539
+ { id: "l-2", person: "arjun", from: at(18, 0, 0), to: at(21, 0, 0), reason: "Holiday" },
5540
+ { id: "l-3", person: "kofi", from: at(13, 0, 0), to: at(14, 0, 0), reason: "Training" },
5541
+ { id: "l-4", person: "freya", from: at(16, 0, 0), to: at(18, 0, 0), reason: "Holiday" },
5542
+ { id: "l-5", person: "david", from: at(9, 0, 0), to: at(14, 0, 0), reason: "Holiday" },
5543
+ { id: "l-6", person: "maya", from: at(19, 13, 0), to: at(20, 17, 0), reason: "Training" },
5544
+ ];
5545
+
5546
+ const day = (d: number, hours = 0) => new Date(2026, 2, d, hours).getTime();
5547
+
5548
+ const APPS = PEOPLE.filter((person) => person.team === "Apps" && person.role !== "Product manager");
5549
+ const START: readonly Subtask[] = WORK.filter((item) => APPS.some((person) => person.id === item.lane));
5550
+ const AWAY: readonly BlockedTime[] = LEAVE.filter((leave) => APPS.some((person) => person.id === leave.person)).map((leave) => ({
5551
+ id: leave.id,
5552
+ lane: leave.person,
5553
+ from: leave.from,
5554
+ to: leave.to,
5555
+ label: leave.reason,
5556
+ }));
5557
+
5558
+ /* Work stays with people of the role it was planned for. */
5559
+ const roleOf = (lane: string) => PEOPLE.find((person) => person.id === lane)?.role;
5560
+ const START_ROLE = new Map(START.map((item) => [item.id, roleOf(item.lane)]));
5561
+ const mayGo = (item: Subtask, lane: string) => START_ROLE.get(item.id) === roleOf(lane);
5562
+
5563
+ export default function AroundLeave() {
5564
+ const [work, setWork] = useState<readonly Subtask[]>(START);
5565
+ const [last, setLast] = useState("Drag Freya's store screenshots into her holiday");
5566
+
5567
+ const onIntent = (intent: Intent) => {
5568
+ if (intent.kind === "place") return;
5569
+ setWork((current) => current.map((item) => applyIntent(item, intent)));
4383
5570
  setLast(`${intent.subtask}: ${intent.kind}`);
4384
5571
  };
4385
5572
 
@@ -4407,8 +5594,6 @@ export default function AroundLeave() {
4407
5594
  }
4408
5595
  ```
4409
5596
 
4410
- It imports `planning.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
4411
-
4412
5597
  #### When to use something else
4413
5598
 
4414
5599
  - The rule depends on the result, such as no double booking → Move and lane
@@ -4967,1363 +6152,3 @@ interface ResolvedAppearance {
4967
6152
  readonly open: boolean;
4968
6153
  }
4969
6154
  ```
4970
-
4971
- ## Files the examples show
4972
-
4973
- The demos' shared data, which some examples import from beside themselves. Copied with the example, it runs.
4974
-
4975
- ### `logistics.ts`
4976
-
4977
- ```ts
4978
- /**
4979
- * The logistics world: Ferrow Parcel, a regional parcel carrier with three
4980
- * depots.
4981
- *
4982
- * The standing cast:
4983
- * - `DEPOTS`, `VEHICLES` (plate, type, payload, depot) and `DRIVERS`;
4984
- * - `TOURS` - today's eight delivery tours, each with its vehicle, driver,
4985
- * start, end, distance and stops; a stop has a delivery window and the
4986
- * planned arrival, and a few arrive outside their window;
4987
- * - `SHIPMENTS` - one per stop, with customer, weight, status and window;
4988
- * - `VEHICLE_STATES` and `vehicleDay(vehicle)` - what a vehicle did through
4989
- * the day, quarter hour by quarter hour, for a state band;
4990
- * - `COST_RATES` - what a kilometre, an hour and a vehicle day cost, the
4991
- * inputs of a cost per tour.
4992
- *
4993
- * "Now" is Tuesday, 17 March 2026, 10:30 local time. Plain data and small pure
4994
- * functions, no imports: copy the file beside an example and it runs.
4995
- */
4996
-
4997
- /** A small LCG - the same numbers on every computer. */
4998
- function random(seed: number): () => number {
4999
- let s = seed >>> 0;
5000
- return () => {
5001
- s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
5002
- return s / 4294967296;
5003
- };
5004
- }
5005
-
5006
- const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
5007
- const MINUTE = 60_000;
5008
-
5009
- /** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
5010
- export const NOW = at(10, 30);
5011
-
5012
- export const DEPOTS = [
5013
- { id: "north", name: "North depot" },
5014
- { id: "river", name: "Riverside depot" },
5015
- { id: "east", name: "East Gate depot" },
5016
- ] as const;
5017
-
5018
- export interface Vehicle {
5019
- id: string;
5020
- plate: string;
5021
- type: "van" | "e-van" | "truck";
5022
- /** Payload, in kg. */
5023
- capacity: number;
5024
- depot: string;
5025
- }
5026
-
5027
- export const VEHICLES: readonly Vehicle[] = [
5028
- { id: "v1", plate: "FP 214 K", type: "van", capacity: 1200, depot: "north" },
5029
- { id: "v2", plate: "FP 377 K", type: "e-van", capacity: 900, depot: "north" },
5030
- { id: "v3", plate: "FP 118 R", type: "truck", capacity: 7500, depot: "north" },
5031
- { id: "v4", plate: "FP 402 R", type: "van", capacity: 1200, depot: "river" },
5032
- { id: "v5", plate: "FP 455 R", type: "e-van", capacity: 900, depot: "river" },
5033
- { id: "v6", plate: "FP 290 E", type: "van", capacity: 1200, depot: "east" },
5034
- { id: "v7", plate: "FP 311 E", type: "e-van", capacity: 900, depot: "east" },
5035
- { id: "v8", plate: "FP 520 E", type: "truck", capacity: 7500, depot: "east" },
5036
- ];
5037
-
5038
- export const DRIVERS = [
5039
- { id: "d1", name: "Martin Hale", depot: "north" },
5040
- { id: "d2", name: "Nadia Petrova", depot: "north" },
5041
- { id: "d3", name: "Owen Carter", depot: "north" },
5042
- { id: "d4", name: "Lucia Romero", depot: "river" },
5043
- { id: "d5", name: "Ben Adeyemi", depot: "river" },
5044
- { id: "d6", name: "Hanna Berg", depot: "east" },
5045
- { id: "d7", name: "Yusuf Demir", depot: "east" },
5046
- { id: "d8", name: "Clara Wendt", depot: "east" },
5047
- ] as const;
5048
-
5049
- const CUSTOMERS = [
5050
- "Holloway Garden Supplies",
5051
- "Marlow & Finch Books",
5052
- "Oakridge Pharmacy",
5053
- "Brixley Cycles",
5054
- "Tamsin's Bakery",
5055
- "Northfold Office",
5056
- "Pellham Hardware",
5057
- "Greywick Studio",
5058
- "Juniper Lane Florist",
5059
- "Ashcombe Dental",
5060
- ] as const;
5061
-
5062
- export interface Stop {
5063
- id: string;
5064
- shipment: string;
5065
- customer: string;
5066
- /** The window the customer was promised, `[from, to]`. */
5067
- window: readonly [number, number];
5068
- /** When the tour plans to be there. */
5069
- arrival: number;
5070
- }
5071
-
5072
- export interface Tour {
5073
- id: string;
5074
- vehicle: string;
5075
- driver: string;
5076
- depot: string;
5077
- /** Leaves the depot, loaded; back at the depot. */
5078
- from: number;
5079
- to: number;
5080
- /** Minutes of loading before `from`. */
5081
- loading: number;
5082
- km: number;
5083
- stops: readonly Stop[];
5084
- }
5085
-
5086
- /** One tour per vehicle. Stops every half hour or so, each with a two-hour
5087
- window; every seventh arrival misses its window - the late ones a
5088
- dispatcher looks for. */
5089
- function tour(index: number): Tour {
5090
- const r = random(700 + index);
5091
- const vehicle = VEHICLES[index]!;
5092
- const truck = vehicle.type === "truck";
5093
- const from = at(truck ? 6 : 7, index % 2 === 0 ? 0 : 30);
5094
- const count = truck ? 5 : 8 + Math.floor(r() * 4);
5095
- const stops: Stop[] = [];
5096
- let arrival = from + (20 + Math.floor(r() * 15)) * MINUTE;
5097
- for (let i = 0; i < count; i++) {
5098
- const n = index * 20 + i;
5099
- const slot = Math.floor((arrival - at(0)) / (2 * 60 * MINUTE)) * 2;
5100
- /* A late stop's window closed half an hour or more before the arrival. */
5101
- const late = i >= 2 && n % 7 === 3;
5102
- const halfHour = 30 * MINUTE;
5103
- const windowFrom = late ? Math.floor(arrival / halfHour) * halfHour - 5 * halfHour : at(slot);
5104
- stops.push({
5105
- id: `s-${index + 1}-${i + 1}`,
5106
- shipment: `FP-${(1_004_210 + n * 13).toString()}`,
5107
- customer: CUSTOMERS[(n * 3) % CUSTOMERS.length]!,
5108
- window: [windowFrom, windowFrom + 2 * 60 * MINUTE],
5109
- arrival,
5110
- });
5111
- arrival += (truck ? 45 : 22) * MINUTE + Math.floor(r() * 18) * MINUTE;
5112
- }
5113
- return {
5114
- id: `T-${String(index + 1).padStart(2, "0")}`,
5115
- vehicle: vehicle.id,
5116
- driver: DRIVERS[index]!.id,
5117
- depot: vehicle.depot,
5118
- from,
5119
- to: arrival + 30 * MINUTE,
5120
- loading: truck ? 45 : 30,
5121
- km: Math.round((truck ? 140 : 70) + r() * 40),
5122
- stops,
5123
- };
5124
- }
5125
-
5126
- export const TOURS: readonly Tour[] = VEHICLES.map((_, index) => tour(index));
5127
-
5128
- export interface Shipment {
5129
- id: string;
5130
- customer: string;
5131
- /** In kg. */
5132
- weight: number;
5133
- status: "delivered" | "out for delivery" | "failed attempt";
5134
- window: readonly [number, number];
5135
- tour: string;
5136
- }
5137
-
5138
- /** One shipment per stop. Before now, delivered - or a failed attempt, where
5139
- nobody was in; after now, out for delivery. */
5140
- export const SHIPMENTS: readonly Shipment[] = TOURS.flatMap((one) =>
5141
- one.stops.map((stop) => {
5142
- const n = Number(stop.shipment.slice(3));
5143
- return {
5144
- id: stop.shipment,
5145
- customer: stop.customer,
5146
- weight: Math.round((VEHICLES.find((v) => v.id === one.vehicle)!.type === "truck" ? 180 : 2) + ((n * 7919) % 230) / 10),
5147
- status: stop.arrival > NOW ? "out for delivery" : n % 11 === 0 ? "failed attempt" : "delivered",
5148
- window: stop.window,
5149
- tour: one.id,
5150
- } satisfies Shipment;
5151
- }),
5152
- );
5153
-
5154
- /** The closed set of vehicle states. The order is the code. */
5155
- export const VEHICLE_STATES = [
5156
- { label: "Driving", color: "#2f6fb4" },
5157
- { label: "Loading", color: "#c08a2e" },
5158
- { label: "Idle", color: "#8a8f98" },
5159
- { label: "Break", color: "#6b8e5a" },
5160
- ] as const;
5161
-
5162
- export interface StatePoint {
5163
- t: number;
5164
- /** A code in `VEHICLE_STATES`; `null` before the tour and after it. */
5165
- state: number | null;
5166
- }
5167
-
5168
- /** What a vehicle did from 05:00 to 19:00, quarter hour by quarter hour:
5169
- loading before its tour, driving between stops, idle at them now and then,
5170
- and a break around noon. */
5171
- export function vehicleDay(vehicleId: string): StatePoint[] {
5172
- const one = TOURS.find((t) => t.vehicle === vehicleId);
5173
- if (one === undefined) throw new Error(`No vehicle "${vehicleId}".`);
5174
- const r = random(900 + TOURS.indexOf(one));
5175
- const points: StatePoint[] = [];
5176
- for (let t = at(5); t <= at(19); t += 15 * MINUTE) {
5177
- let state: number | null = null;
5178
- if (t >= one.from - one.loading * MINUTE && t < one.from) state = 1;
5179
- else if (t >= one.from && t < one.to) state = t >= at(12) && t < at(12, 30) ? 3 : r() < 0.18 ? 2 : 0;
5180
- points.push({ t, state });
5181
- }
5182
- return points;
5183
- }
5184
-
5185
- /** What a tour costs, per unit - the inputs of a cost per tour. */
5186
- export const COST_RATES = {
5187
- /** Diesel, per litre; electricity, per kWh. */
5188
- dieselPerLitre: 1.68,
5189
- electricityPerKwh: 0.31,
5190
- /** Consumption per 100 km. */
5191
- per100Km: { van: 9.5, truck: 24, "e-van": 22 },
5192
- /** A driver's hour, all in. */
5193
- driverPerHour: 34.5,
5194
- /** Lease, insurance and upkeep, per day. */
5195
- vehiclePerDay: { van: 58, truck: 145, "e-van": 72 },
5196
- } as const;
5197
-
5198
- /* ---------------------------------------------------------------------------
5199
- For @umriss-ui/charts: an e-van's battery, the North depot's week and
5200
- fortnight, the delays by cause and the parcels per depot and hour.
5201
- --------------------------------------------------------------------------- */
5202
-
5203
- const on = (month: number, day: number, hours = 0) => new Date(2026, month - 1, day, hours).getTime();
5204
- const HOUR = 60 * MINUTE;
5205
-
5206
- export interface BatteryPoint {
5207
- t: number;
5208
- /** State of charge, in per cent; `null` before the tour and after it. */
5209
- charge: number | null;
5210
- }
5211
-
5212
- /** An e-van's battery through the day, with `vehicleDay`'s quarter hours:
5213
- full when the tour leaves, draining while it drives, a little while it
5214
- idles, and topped up at the depot's charger over the noon break. */
5215
- export function batteryDay(vehicleId: string): BatteryPoint[] {
5216
- let charge = 100;
5217
- return vehicleDay(vehicleId).map(({ t, state }) => {
5218
- if (state === null) return { t, charge: null };
5219
- const now = charge;
5220
- charge = Math.max(0, Math.min(100, charge + [-4.8, 0, -0.6, 3][state]!));
5221
- return { t, charge: Math.round(now * 10) / 10 };
5222
- });
5223
- }
5224
-
5225
- /** The North depot's sorting hours: Monday to Friday, 06:00 to 22:00. */
5226
- export const SORTING_HOURS = Array.from({ length: 5 }, (_, day) => ({ from: on(3, 9 + day, 6), to: on(3, 9 + day, 22) }));
5227
-
5228
- export interface SortedPoint {
5229
- t: number;
5230
- /** Parcels sorted per hour. */
5231
- parcels: number;
5232
- }
5233
-
5234
- /** Last week's sorting at the North depot, quarter hour by quarter hour -
5235
- only in its sorting hours. */
5236
- export const SORTED: readonly SortedPoint[] = (() => {
5237
- const r = random(1963);
5238
- const points: SortedPoint[] = [];
5239
- let parcels = 420;
5240
- for (const { from } of SORTING_HOURS) {
5241
- for (let quarter = 0; quarter < 4 * 16; quarter++) {
5242
- parcels += (r() - 0.5) * 26;
5243
- points.push({ t: from + quarter * 15 * MINUTE, parcels: Math.round(parcels) });
5244
- }
5245
- }
5246
- return points;
5247
- })();
5248
-
5249
- export interface DepotDay {
5250
- /** Day of March. */
5251
- day: number;
5252
- /** Pallets that arrived, and that left on the tours. */
5253
- arrived: number;
5254
- dispatched: number;
5255
- /** Pallets on hand at the end of the day. */
5256
- onHand: number;
5257
- /** The range the stock is meant to stay in; `null` while it was reset
5258
- after the stocktake. */
5259
- targetLow: number | null;
5260
- targetHigh: number | null;
5261
- /** A pallet count by hand - it never quite agrees with the books. */
5262
- counted: number;
5263
- }
5264
-
5265
- /** The North depot's last fourteen days, 2 to 15 March. */
5266
- export const DEPOT_DAYS: readonly DepotDay[] = (() => {
5267
- const r = random(2026);
5268
- let onHand = 62;
5269
- return Array.from({ length: 14 }, (_, i) => {
5270
- const arrived = 30 + r() * 25;
5271
- const dispatched = 22 + r() * 20;
5272
- onHand += (arrived - dispatched) * 0.35;
5273
- const reset = i >= 6 && i <= 8;
5274
- return {
5275
- day: i + 2,
5276
- arrived: Math.round(arrived),
5277
- dispatched: Math.round(dispatched),
5278
- onHand: Math.round(onHand),
5279
- targetLow: reset ? null : Math.round(onHand - 9 - r() * 3),
5280
- targetHigh: reset ? null : Math.round(onHand + 9 + r() * 3),
5281
- counted: Math.round(onHand + (r() - 0.5) * 26),
5282
- };
5283
- });
5284
- })();
5285
-
5286
- export interface DayDelays {
5287
- /** The position on the x axis: the working day's index. */
5288
- day: number;
5289
- name: string;
5290
- /** Minutes late across all tours, by cause; `null` where it was not logged. */
5291
- traffic: number;
5292
- loading: number | null;
5293
- access: number;
5294
- }
5295
-
5296
- /** Two working weeks, 2 to 13 March. On the first Wednesday the loading log
5297
- was not kept - a gap, not a zero - and on the second Thursday a closed
5298
- bridge takes the day. */
5299
- export const DELAYS: readonly DayDelays[] = (() => {
5300
- const r = random(1204);
5301
- const names = ["Mon", "Tue", "Wed", "Thu", "Fri"];
5302
- return Array.from({ length: 10 }, (_, day) => ({
5303
- day,
5304
- name: `${names[day % 5]} ${2 + day + 2 * Math.floor(day / 5)}`,
5305
- traffic: Math.round((day === 8 ? 150 : 40) + r() * 30),
5306
- loading: day === 2 ? null : Math.round(10 + r() * 45),
5307
- access: Math.round(5 + r() * 25),
5308
- }));
5309
- })();
5310
-
5311
- export interface DepotHour {
5312
- t: number;
5313
- /** Parcels loaded per hour at each depot. */
5314
- north: number;
5315
- river: number;
5316
- east: number;
5317
- }
5318
-
5319
- /** Monday 06:00 to Tuesday 06:00, hour by hour. East Gate closes at night and
5320
- loads nothing - 0, not nothing. */
5321
- export const PARCELS_PER_HOUR: readonly DepotHour[] = (() => {
5322
- const r = random(906);
5323
- return Array.from({ length: 25 }, (_, i) => {
5324
- const hour = (6 + i) % 24;
5325
- const night = hour >= 22 || hour < 6;
5326
- return {
5327
- t: on(3, 16, 6) + i * HOUR,
5328
- north: Math.round(120 + r() * 30),
5329
- river: Math.round((night ? 60 : 90) + r() * 25),
5330
- east: night ? 0 : Math.round(70 + r() * 30),
5331
- };
5332
- });
5333
- })();
5334
- ```
5335
-
5336
- ### `operations.ts`
5337
-
5338
- ```ts
5339
- /**
5340
- * The operations world: Quillmere, a platform on which small businesses take
5341
- * online bookings, run as a set of services by six teams.
5342
- *
5343
- * The standing cast:
5344
- * - `SERVICES` - eight services with their team, tier, latency objective and
5345
- * availability target;
5346
- * - `ENGINEERS` - the people on call, and `ONCALL`, who is primary and
5347
- * secondary this week;
5348
- * - `metrics(service)` - latency and error rate every five minutes of today up
5349
- * to now; Checkout has a bad half hour from 09:40;
5350
- * - `INCIDENTS` - the last week's incidents, one of them still open;
5351
- * - `ALERT_TYPES` and `ALERTS` - what the alert list shows at 10:30;
5352
- * - `DOWNTIME_MINUTES` - each service's downtime this month, for its
5353
- * availability.
5354
- *
5355
- * "Now" is Tuesday, 17 March 2026, 10:30 local time. Plain data and small pure
5356
- * functions, no imports: copy the file beside an example and it runs.
5357
- */
5358
-
5359
- /** A small LCG - the same numbers on every computer. */
5360
- function random(seed: number): () => number {
5361
- let s = seed >>> 0;
5362
- return () => {
5363
- s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
5364
- return s / 4294967296;
5365
- };
5366
- }
5367
-
5368
- const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
5369
- const MINUTE = 60_000;
5370
-
5371
- /** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
5372
- export const NOW = at(17, 10, 30);
5373
-
5374
- export interface Service {
5375
- id: string;
5376
- name: string;
5377
- team: string;
5378
- /** 1 is customer-facing and pages at night; 3 waits for the morning. */
5379
- tier: 1 | 2 | 3;
5380
- /** The latency objective: the 95th percentile stays below this, in ms. */
5381
- latencySlo: number;
5382
- /** The availability promised for a month, in per cent. */
5383
- availabilityTarget: number;
5384
- }
5385
-
5386
- export const SERVICES: readonly Service[] = [
5387
- { id: "checkout", name: "Checkout", team: "Payments", tier: 1, latencySlo: 300, availabilityTarget: 99.95 },
5388
- { id: "billing", name: "Billing", team: "Payments", tier: 1, latencySlo: 400, availabilityTarget: 99.9 },
5389
- { id: "sign-in", name: "Sign-in", team: "Identity", tier: 1, latencySlo: 200, availabilityTarget: 99.95 },
5390
- { id: "search", name: "Search", team: "Discovery", tier: 1, latencySlo: 250, availabilityTarget: 99.9 },
5391
- { id: "images", name: "Image service", team: "Discovery", tier: 2, latencySlo: 500, availabilityTarget: 99.5 },
5392
- { id: "notifications", name: "Notifications", team: "Messaging", tier: 2, latencySlo: 800, availabilityTarget: 99.5 },
5393
- { id: "webhooks", name: "Webhooks", team: "Integrations", tier: 2, latencySlo: 1000, availabilityTarget: 99.5 },
5394
- { id: "reports", name: "Reporting", team: "Insights", tier: 3, latencySlo: 2000, availabilityTarget: 99 },
5395
- ];
5396
-
5397
- export interface Engineer {
5398
- id: string;
5399
- name: string;
5400
- team: string;
5401
- }
5402
-
5403
- export const ENGINEERS: readonly Engineer[] = [
5404
- { id: "priya", name: "Priya Raman", team: "Payments" },
5405
- { id: "jonas", name: "Jonas Keller", team: "Payments" },
5406
- { id: "ada", name: "Ada Mwangi", team: "Identity" },
5407
- { id: "tomasz", name: "Tomasz Nowak", team: "Discovery" },
5408
- { id: "leila", name: "Leila Haddad", team: "Discovery" },
5409
- { id: "sam", name: "Sam Okafor", team: "Messaging" },
5410
- { id: "ines", name: "Ines Duarte", team: "Integrations" },
5411
- { id: "felix", name: "Felix Brandt", team: "Insights" },
5412
- ];
5413
-
5414
- export interface OnCall {
5415
- id: string;
5416
- engineer: string;
5417
- rotation: "primary" | "secondary";
5418
- from: number;
5419
- to: number;
5420
- }
5421
-
5422
- /** Monday 16 to Monday 23 March, handed over every morning at 09:00: the
5423
- secondary of one day is the primary of the next. */
5424
- export const ONCALL: readonly OnCall[] = Array.from({ length: 7 }, (_, day) => [
5425
- { rotation: "primary" as const, engineer: ENGINEERS[day % ENGINEERS.length]!.id },
5426
- { rotation: "secondary" as const, engineer: ENGINEERS[(day + 1) % ENGINEERS.length]!.id },
5427
- ].map(({ rotation, engineer }) => ({
5428
- id: `${rotation}-${16 + day}`,
5429
- engineer,
5430
- rotation,
5431
- from: at(16 + day, 9),
5432
- to: at(17 + day, 9),
5433
- }))).flat();
5434
-
5435
- export interface MetricPoint {
5436
- t: number;
5437
- /** Median and 95th percentile latency, in ms. */
5438
- p50: number;
5439
- p95: number;
5440
- /** Failed requests, in per cent. */
5441
- errorRate: number;
5442
- /** Requests per minute. */
5443
- requests: number;
5444
- }
5445
-
5446
- /** Requests per minute at the busiest hour, by tier. */
5447
- const VOLUME = { 1: 900, 2: 400, 3: 120 } as const;
5448
-
5449
- /** A service's latency and error rate every five minutes of today, from
5450
- midnight up to now. Busy by day, quiet at night; Checkout's 95th percentile
5451
- climbs well past its objective from 09:40 to 10:10, and its errors with it. */
5452
- export function metrics(serviceId: string): MetricPoint[] {
5453
- const index = SERVICES.findIndex((one) => one.id === serviceId);
5454
- const service = SERVICES[index];
5455
- if (service === undefined) throw new Error(`No service "${serviceId}".`);
5456
- const r = random(1000 + index * 37);
5457
- const points: MetricPoint[] = [];
5458
- let drift = 0;
5459
- for (let t = at(17, 0); t <= NOW; t += 5 * MINUTE) {
5460
- const hour = (t - at(17, 0)) / (60 * MINUTE);
5461
- const load = 0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16));
5462
- drift = 0.8 * drift + (r() - 0.5) * 0.08;
5463
- const base = service.latencySlo * (0.45 + 0.2 * load + drift);
5464
- const surge = serviceId === "checkout" && t >= at(17, 9, 40) && t < at(17, 10, 10) ? 1.9 : 1;
5465
- const p95 = Math.round(base * surge * (1 + r() * 0.08));
5466
- points.push({
5467
- t,
5468
- p50: Math.round(p95 * (0.42 + r() * 0.06)),
5469
- p95,
5470
- errorRate: Math.round((0.05 + r() * 0.12 + (surge > 1 ? 2.4 + r() : 0)) * 100) / 100,
5471
- requests: Math.round(VOLUME[service.tier] * load * (1 + (r() - 0.5) * 0.1)),
5472
- });
5473
- }
5474
- return points;
5475
- }
5476
-
5477
- export interface Incident {
5478
- id: string;
5479
- title: string;
5480
- service: string;
5481
- severity: "SEV1" | "SEV2" | "SEV3";
5482
- opened: number;
5483
- acknowledged?: number;
5484
- resolved?: number;
5485
- /** An engineer's id. */
5486
- assignee: string;
5487
- }
5488
-
5489
- export const INCIDENTS: readonly Incident[] = [
5490
- { id: "INC-1048", title: "Checkout slow, card payments time out", service: "checkout", severity: "SEV1", opened: at(17, 9, 42), acknowledged: at(17, 9, 46), assignee: "jonas" },
5491
- { id: "INC-1047", title: "Webhook deliveries delayed", service: "webhooks", severity: "SEV3", opened: at(17, 7, 15), acknowledged: at(17, 8, 2), assignee: "ines" },
5492
- { id: "INC-1046", title: "Thumbnails missing for new uploads", service: "images", severity: "SEV2", opened: at(16, 22, 5), acknowledged: at(16, 22, 11), resolved: at(17, 0, 40), assignee: "tomasz" },
5493
- { id: "INC-1045", title: "Sign-in codes arrive late", service: "sign-in", severity: "SEV2", opened: at(16, 14, 20), acknowledged: at(16, 14, 24), resolved: at(16, 15, 5), assignee: "ada" },
5494
- { id: "INC-1044", title: "Monthly report export fails", service: "reports", severity: "SEV3", opened: at(15, 10, 0), acknowledged: at(16, 9, 12), resolved: at(16, 11, 30), assignee: "felix" },
5495
- { id: "INC-1043", title: "Search returns no results for some regions", service: "search", severity: "SEV1", opened: at(14, 18, 30), acknowledged: at(14, 18, 33), resolved: at(14, 19, 55), assignee: "leila" },
5496
- { id: "INC-1042", title: "Duplicate reminder e-mails", service: "notifications", severity: "SEV3", opened: at(13, 8, 45), acknowledged: at(13, 9, 30), resolved: at(13, 13, 0), assignee: "sam" },
5497
- { id: "INC-1041", title: "Invoices generated twice", service: "billing", severity: "SEV2", opened: at(12, 16, 10), acknowledged: at(12, 16, 18), resolved: at(12, 18, 40), assignee: "priya" },
5498
- ];
5499
-
5500
- /** A kind of alert, shaped as `@umriss-ui/table`'s alarm list reads it. */
5501
- export interface AlertType {
5502
- id: string;
5503
- label: string;
5504
- priority: "high" | "medium" | "low";
5505
- }
5506
-
5507
- export const ALERT_TYPES: readonly AlertType[] = [
5508
- { id: "checkout-latency", label: "Checkout · p95 latency above 300 ms", priority: "high" },
5509
- { id: "checkout-errors", label: "Checkout · error rate above 2\u00a0%", priority: "high" },
5510
- { id: "webhooks-queue", label: "Webhooks · queue older than 5 min", priority: "medium" },
5511
- { id: "search-latency", label: "Search · p95 latency above 250 ms", priority: "medium" },
5512
- { id: "images-disk", label: "Image service · disk 85 % full", priority: "low" },
5513
- { id: "reports-job", label: "Reporting · nightly job late", priority: "low" },
5514
- ];
5515
-
5516
- /** One alert, shaped as `@umriss-ui/table`'s alarm list reads it. */
5517
- export type Alert = {
5518
- id: string;
5519
- type: string;
5520
- lifecycle: "active-unacknowledged" | "active-acknowledged" | "resolved-unacknowledged" | "resolved-acknowledged";
5521
- raised: number;
5522
- resolved?: number;
5523
- acknowledgedAt?: number;
5524
- } & (
5525
- | { availability?: "in-service" | "suppressed" | "disabled"; snooze?: undefined }
5526
- | { availability: "snoozed"; snooze: { until: number; by: string } }
5527
- );
5528
-
5529
- /** The alerts as they stand at 10:30. */
5530
- export const ALERTS: readonly Alert[] = [
5531
- { id: "a-1", type: "checkout-latency", lifecycle: "active-acknowledged", raised: at(17, 9, 41), acknowledgedAt: at(17, 9, 46) },
5532
- { id: "a-2", type: "checkout-errors", lifecycle: "resolved-unacknowledged", raised: at(17, 9, 43), resolved: at(17, 10, 11) },
5533
- { id: "a-3", type: "webhooks-queue", lifecycle: "active-acknowledged", raised: at(17, 7, 12), acknowledgedAt: at(17, 8, 2) },
5534
- { id: "a-4", type: "search-latency", lifecycle: "active-unacknowledged", raised: at(17, 10, 24) },
5535
- { id: "a-5", type: "search-latency", lifecycle: "resolved-acknowledged", raised: at(17, 8, 5), resolved: at(17, 8, 9), acknowledgedAt: at(17, 8, 6) },
5536
- { id: "a-6", type: "images-disk", lifecycle: "active-unacknowledged", raised: at(17, 6, 0), availability: "snoozed", snooze: { until: at(17, 14), by: "Tomasz Nowak" } },
5537
- { id: "a-7", type: "reports-job", lifecycle: "active-unacknowledged", raised: at(17, 4, 30), availability: "disabled" },
5538
- ];
5539
-
5540
- /** The month so far: 1 March 00:00 to now, in minutes. */
5541
- export const MONTH_MINUTES = (NOW - at(1, 0)) / MINUTE;
5542
-
5543
- /** Minutes each service was down this month. With `MONTH_MINUTES` and the
5544
- service's target, what an availability is worked out from. */
5545
- export const DOWNTIME_MINUTES: Readonly<Record<string, number>> = {
5546
- checkout: 34,
5547
- billing: 18,
5548
- "sign-in": 41,
5549
- search: 85,
5550
- images: 120,
5551
- notifications: 12,
5552
- webhooks: 210,
5553
- reports: 95,
5554
- };
5555
-
5556
- /* ---------------------------------------------------------------------------
5557
- For @umriss-ui/charts: last week at any resolution, a load test, single
5558
- traced requests, the autoscaler's log, the expected range and yesterday's
5559
- success rates.
5560
- --------------------------------------------------------------------------- */
5561
-
5562
- const HOUR = 60 * MINUTE;
5563
- const DAY = 24 * HOUR;
5564
-
5565
- /** Monday, 9 March 2026, 00:00 - the start of last week. */
5566
- export const LAST_WEEK = at(9, 0);
5567
-
5568
- /** Search returned nothing for some regions (INC-1043): Saturday, 18:30 to 19:55. */
5569
- const SEARCH_OUTAGE = [at(14, 18, 30) - LAST_WEEK, at(14, 19, 55) - LAST_WEEK] as const;
5570
-
5571
- /** A service's last week, Monday to Monday, one point every `step`
5572
- milliseconds: busy by day, quieter at the weekend. During INC-1043 Search's
5573
- 95th percentile more than doubles and its errors climb. The hour is counted,
5574
- not asked of a Date - a week of seconds is 604,800 points. */
5575
- export function week(serviceId: string, step: number): MetricPoint[] {
5576
- const index = SERVICES.findIndex((one) => one.id === serviceId);
5577
- const service = SERVICES[index];
5578
- if (service === undefined) throw new Error(`No service "${serviceId}".`);
5579
- const r = random(2000 + index * 37);
5580
- const n = Math.floor((7 * DAY) / step);
5581
- const points = new Array<MetricPoint>(n);
5582
- /* The drift wanders at the same pace whatever the step. */
5583
- const keep = Math.pow(0.8, step / (5 * MINUTE));
5584
- const kick = 0.08 * Math.sqrt(1 - keep * keep) / 0.6;
5585
- let drift = 0;
5586
- for (let i = 0; i < n; i++) {
5587
- const offset = i * step;
5588
- const day = Math.floor(offset / DAY);
5589
- const hour = (offset % DAY) / HOUR;
5590
- const load = (day >= 5 ? 0.85 : 1) * (0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16)));
5591
- drift = keep * drift + (r() - 0.5) * kick;
5592
- const outage = serviceId === "search" && offset >= SEARCH_OUTAGE[0] && offset < SEARCH_OUTAGE[1];
5593
- const p95 = Math.round(service.latencySlo * (0.45 + 0.2 * load + drift) * (outage ? 2.3 : 1) * (1 + r() * 0.08));
5594
- points[i] = {
5595
- t: LAST_WEEK + offset,
5596
- p50: Math.round(p95 * (0.42 + r() * 0.06)),
5597
- p95,
5598
- errorRate: Math.round((0.05 + r() * 0.12 + (outage ? 3 : 0)) * 100) / 100,
5599
- requests: Math.round(VOLUME[service.tier] * load * (1 + (r() - 0.5) * 0.1)),
5600
- };
5601
- }
5602
- return points;
5603
- }
5604
-
5605
- /** When this morning's load test of Search began. */
5606
- export interface LoadTestPoint {
5607
- /** Minutes since the test began. */
5608
- minute: number;
5609
- /** CPU load of the busiest node, in per cent. */
5610
- cpu: number;
5611
- requestsPerHour: number;
5612
- /** 95th percentile latency, in ms. */
5613
- p95: number;
5614
- }
5615
-
5616
- /** An hour of load test, minute by minute: three magnitudes far apart. */
5617
- export const LOAD_TEST: readonly LoadTestPoint[] = (() => {
5618
- const r = random(99);
5619
- let cpu = 21;
5620
- let requestsPerHour = 128_000;
5621
- let p95 = 640;
5622
- return Array.from({ length: 60 }, (_, minute) => {
5623
- cpu += (r() - 0.5) * 0.9;
5624
- requestsPerHour += (r() - 0.5) * 9000;
5625
- p95 += (r() - 0.5) * 40;
5626
- return { minute, cpu, requestsPerHour, p95 };
5627
- });
5628
- })();
5629
-
5630
- export interface Trace {
5631
- t: number;
5632
- /** How long the request took, end to end, in ms. */
5633
- ms: number;
5634
- }
5635
-
5636
- /** Single Checkout requests traced end to end this morning, one every few
5637
- minutes from 06:00 - individual measurements, nothing in between. Slow
5638
- during the bad half hour, and now and then on their own. */
5639
- export const TRACES: readonly Trace[] = (() => {
5640
- const r = random(4040);
5641
- const traces: Trace[] = [];
5642
- for (let t = at(17, 6, 2); t <= NOW; t += Math.round(1 + r() * 4) * MINUTE) {
5643
- const bad = t >= at(17, 9, 40) && t < at(17, 10, 10);
5644
- const ms = 150 + r() * 90 + (bad ? 110 + r() * 120 : r() < 0.05 ? 150 : 0);
5645
- traces.push({ t, ms: Math.round(ms) });
5646
- }
5647
- return traces;
5648
- })();
5649
-
5650
- export interface ReplicaChange {
5651
- t: number;
5652
- /** Checkout's instances from here on; `null` while the autoscaler was paused. */
5653
- replicas: number | null;
5654
- }
5655
-
5656
- /** Checkout's instances today, logged only when the autoscaler changes them.
5657
- It was paused for a database upgrade from 03:00 to 04:00; the last entry,
5658
- at now, closes the hold. */
5659
- export const REPLICAS: readonly ReplicaChange[] = [
5660
- { t: at(17, 0), replicas: 4 },
5661
- { t: at(17, 3), replicas: null },
5662
- { t: at(17, 4), replicas: 3 },
5663
- { t: at(17, 6, 30), replicas: 5 },
5664
- { t: at(17, 8), replicas: 8 },
5665
- { t: at(17, 9, 45), replicas: 12 },
5666
- { t: at(17, 10, 15), replicas: 9 },
5667
- { t: NOW, replicas: 9 },
5668
- ];
5669
-
5670
- export interface ExpectedPoint {
5671
- t: number;
5672
- /** The range the requests per minute are expected in; `null` where the
5673
- job that computes it did not run. */
5674
- low: number | null;
5675
- high: number | null;
5676
- }
5677
-
5678
- /** The range a service's requests per minute are expected in today, every
5679
- five minutes - the day's usual curve, give or take 15 %. The job that
5680
- computes it skipped 03:00 to 04:00. */
5681
- export function expected(serviceId: string): ExpectedPoint[] {
5682
- const service = SERVICES.find((one) => one.id === serviceId);
5683
- if (service === undefined) throw new Error(`No service "${serviceId}".`);
5684
- const points: ExpectedPoint[] = [];
5685
- for (let t = at(17, 0); t <= NOW; t += 5 * MINUTE) {
5686
- const hour = (t - at(17, 0)) / HOUR;
5687
- const usual = VOLUME[service.tier] * (0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16)));
5688
- const skipped = hour >= 3 && hour < 4;
5689
- points.push({ t, low: skipped ? null : Math.round(usual * 0.85), high: skipped ? null : Math.round(usual * 1.15) });
5690
- }
5691
- return points;
5692
- }
5693
-
5694
- export interface SuccessCell {
5695
- hour: number;
5696
- /** The row: the service's place in `SERVICES`. */
5697
- service: number;
5698
- /** Requests that succeeded, in per cent; `null` where there were none. */
5699
- success: number | null;
5700
- }
5701
-
5702
- /** Yesterday, per service and hour: the share of requests that succeeded.
5703
- Webhooks has a bad late morning, 03:00 is bad everywhere (a DNS change),
5704
- and Reporting takes no requests before 06:00 - no requests, no share. */
5705
- export const SUCCESS_BY_HOUR: readonly SuccessCell[] = (() => {
5706
- const r = random(23);
5707
- const cells: SuccessCell[] = [];
5708
- SERVICES.forEach((one, service) => {
5709
- for (let hour = 0; hour < 24; hour++) {
5710
- let success: number | null = 99.2 + r() * 0.8;
5711
- if (one.id === "webhooks" && hour >= 9 && hour < 14) success = 96 + r() * 1.5;
5712
- if (hour === 3) success = 97.4 + r() * 1;
5713
- if (one.id === "reports" && hour < 6) success = null;
5714
- cells.push({ hour, service, success: success === null ? null : Math.round(success * 100) / 100 });
5715
- }
5716
- });
5717
- return cells;
5718
- })();
5719
- ```
5720
-
5721
- ### `planning.ts`
5722
-
5723
- ```ts
5724
- /**
5725
- * The planning world: Tidewell, a small studio that builds web and mobile
5726
- * apps for its clients, in two teams.
5727
- *
5728
- * The standing cast:
5729
- * - `PEOPLE` - ten people with their role, team and weekly capacity in hours;
5730
- * - `PROJECTS` - four client projects, each with a colour;
5731
- * - `SPRINTS` - two-week sprints; sprint 14 runs now, 9 to 20 March;
5732
- * - `WORK` - the current sprint's work items with an estimate and a status,
5733
- * placed on a person over time; structurally a `Subtask` of
5734
- * `@umriss-ui/schedule` (`task` is the project, `lane` the person);
5735
- * - `LEAVE` - holidays, sick days and training, to become blocked time;
5736
- * - `BURNDOWN` - sprint 14's remaining hours at the end of each working day,
5737
- * up to yesterday.
5738
- *
5739
- * "Now" is Tuesday, 17 March 2026, 10:30 local time. Plain data and small pure
5740
- * functions, no imports: copy the file beside an example and it runs.
5741
- */
5742
-
5743
- const at = (day: number, hours = 9, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
5744
-
5745
- /** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
5746
- export const NOW = at(17, 10, 30);
5747
-
5748
- export interface Person {
5749
- id: string;
5750
- name: string;
5751
- role: "Developer" | "Designer" | "Product manager" | "QA engineer";
5752
- team: "Web" | "Apps";
5753
- /** Hours a week they can be planned for. */
5754
- capacity: number;
5755
- }
5756
-
5757
- export const PEOPLE: readonly Person[] = [
5758
- { id: "maya", name: "Maya Lindgren", role: "Product manager", team: "Web", capacity: 32 },
5759
- { id: "arjun", name: "Arjun Mehta", role: "Developer", team: "Web", capacity: 40 },
5760
- { id: "chloe", name: "Chloe Durand", role: "Developer", team: "Web", capacity: 40 },
5761
- { id: "noah", name: "Noah Fischer", role: "Designer", team: "Web", capacity: 24 },
5762
- { id: "eva", name: "Eva Novak", role: "QA engineer", team: "Web", capacity: 40 },
5763
- { id: "luis", name: "Luis Moreno", role: "Product manager", team: "Apps", capacity: 40 },
5764
- { id: "hana", name: "Hana Sato", role: "Developer", team: "Apps", capacity: 40 },
5765
- { id: "kofi", name: "Kofi Mensah", role: "Developer", team: "Apps", capacity: 32 },
5766
- { id: "freya", name: "Freya Olsen", role: "Designer", team: "Apps", capacity: 40 },
5767
- { id: "david", name: "David Kowalski", role: "QA engineer", team: "Apps", capacity: 20 },
5768
- ];
5769
-
5770
- /** A project, shaped as `@umriss-ui/schedule`'s `Task`: an id, a name and a
5771
- colour in both schemes. */
5772
- export interface Project {
5773
- id: string;
5774
- name: string;
5775
- client: string;
5776
- color: string;
5777
- }
5778
-
5779
- export const PROJECTS: readonly Project[] = [
5780
- { id: "shop", name: "Online shop relaunch", client: "Pembury Home", color: "light-dark(#2563eb, #6b9bff)" },
5781
- { id: "booking", name: "Booking app", client: "Saltmarsh Clinics", color: "light-dark(#0d9488, #3cc7b8)" },
5782
- { id: "portal", name: "Member portal", client: "Rowan Credit Union", color: "light-dark(#7c3aed, #a98bfa)" },
5783
- { id: "intranet", name: "Intranet", client: "Tidewell (internal)", color: "light-dark(#c2410c, #f08a52)" },
5784
- ];
5785
-
5786
- export interface Sprint {
5787
- id: string;
5788
- name: string;
5789
- from: number;
5790
- to: number;
5791
- goal: string;
5792
- }
5793
-
5794
- /** Two weeks each, Monday 09:00 to the second Friday 17:00. */
5795
- export const SPRINTS: readonly Sprint[] = [
5796
- { name: "Sprint 12", day: 9 - 28, goal: "Shop checkout on the new design" },
5797
- { name: "Sprint 13", day: 9 - 14, goal: "Booking app in the stores' beta" },
5798
- { name: "Sprint 14", day: 9, goal: "Member portal sign-in and profile" },
5799
- { name: "Sprint 15", day: 23, goal: "Shop search and filters" },
5800
- { name: "Sprint 16", day: 37, goal: "Booking reminders" },
5801
- ].map(({ name, day, goal }) => ({ id: name.toLowerCase().replace(" ", "-"), name, from: at(day), to: at(day + 11, 17), goal }));
5802
-
5803
- /** A work item, shaped as `@umriss-ui/schedule`'s `Subtask`. */
5804
- export interface WorkItem {
5805
- id: string;
5806
- /** The project. */
5807
- task: string;
5808
- /** The person. */
5809
- lane: string;
5810
- from: number;
5811
- to: number;
5812
- name: string;
5813
- sprint: string;
5814
- /** Hours, as estimated at planning. */
5815
- estimate: number;
5816
- status: "to do" | "in progress" | "in review" | "done";
5817
- }
5818
-
5819
- const item = (id: string, task: string, lane: string, from: number, to: number, name: string, estimate: number, status: WorkItem["status"]): WorkItem =>
5820
- ({ id, task, lane, from, to, name, sprint: "sprint-14", estimate, status });
5821
-
5822
- export const WORK: readonly WorkItem[] = [
5823
- item("w-101", "portal", "arjun", at(9), at(11, 17), "Sign-in with e-mail code", 18, "done"),
5824
- item("w-102", "portal", "arjun", at(12), at(17, 17), "Profile page", 26, "in progress"),
5825
- item("w-103", "portal", "chloe", at(9), at(10, 17), "Session handling", 12, "done"),
5826
- item("w-104", "shop", "chloe", at(11), at(13, 17), "Basket keeps items across devices", 20, "in review"),
5827
- item("w-105", "portal", "chloe", at(16), at(19, 17), "Change of address form", 24, "in progress"),
5828
- item("w-106", "portal", "noah", at(9), at(12, 13), "Profile page design", 14, "done"),
5829
- item("w-107", "shop", "noah", at(16), at(18, 17), "Search results layout", 12, "to do"),
5830
- item("w-108", "portal", "eva", at(12), at(13, 17), "Test plan for sign-in", 10, "done"),
5831
- item("w-109", "portal", "eva", at(17), at(20, 17), "Regression run", 20, "to do"),
5832
- item("w-110", "booking", "hana", at(9), at(13, 17), "Reminder scheduling service", 32, "done"),
5833
- item("w-111", "booking", "hana", at(16), at(20, 17), "Push notifications", 30, "in progress"),
5834
- item("w-112", "booking", "kofi", at(10), at(12, 17), "Calendar sync", 18, "done"),
5835
- item("w-113", "intranet", "kofi", at(16), at(18, 17), "News feed", 16, "in progress"),
5836
- item("w-114", "booking", "freya", at(9), at(11, 17), "Reminder settings screen", 16, "done"),
5837
- item("w-115", "booking", "freya", at(18), at(20, 17), "Store screenshots", 12, "to do"),
5838
- item("w-116", "booking", "david", at(16), at(19, 13), "Device test matrix", 14, "to do"),
5839
- ];
5840
-
5841
- export interface Leave {
5842
- id: string;
5843
- person: string;
5844
- from: number;
5845
- to: number;
5846
- reason: "Holiday" | "Sick" | "Training";
5847
- }
5848
-
5849
- /** When people are away: blocked time on their lane. */
5850
- export const LEAVE: readonly Leave[] = [
5851
- { id: "l-1", person: "noah", from: at(13, 0, 0), to: at(14, 0, 0), reason: "Sick" },
5852
- { id: "l-2", person: "arjun", from: at(18, 0, 0), to: at(21, 0, 0), reason: "Holiday" },
5853
- { id: "l-3", person: "kofi", from: at(13, 0, 0), to: at(14, 0, 0), reason: "Training" },
5854
- { id: "l-4", person: "freya", from: at(16, 0, 0), to: at(18, 0, 0), reason: "Holiday" },
5855
- { id: "l-5", person: "david", from: at(9, 0, 0), to: at(14, 0, 0), reason: "Holiday" },
5856
- { id: "l-6", person: "maya", from: at(19, 13, 0), to: at(20, 17, 0), reason: "Training" },
5857
- ];
5858
-
5859
- export interface BurndownPoint {
5860
- /** The working day, at 17:00. */
5861
- t: number;
5862
- /** Where the line would be, burning evenly. */
5863
- ideal: number;
5864
- /** Hours left, `null` for the days still to come. */
5865
- remaining: number | null;
5866
- }
5867
-
5868
- /** Sprint 14's ten working days. The team fell behind in the first week, when
5869
- two were away, and is catching up. */
5870
- export const BURNDOWN: readonly BurndownPoint[] = (() => {
5871
- const total = WORK.reduce((sum, one) => sum + one.estimate, 0);
5872
- const days = [9, 10, 11, 12, 13, 16, 17, 18, 19, 20];
5873
- const burnt = [0, 22, 38, 60, 71, 98];
5874
- return days.map((day, i) => ({
5875
- t: at(day, 17),
5876
- ideal: Math.round(total * (1 - i / (days.length - 1))),
5877
- remaining: burnt[i] === undefined ? null : total - burnt[i]!,
5878
- }));
5879
- })();
5880
- ```
5881
-
5882
- ### `plant.ts`
5883
-
5884
- ```ts
5885
- /**
5886
- * The plant world: Brenholt Tile Works, a tile maker with one kiln line, and the
5887
- * machine shop next door.
5888
- *
5889
- * The standing cast:
5890
- * - the kiln line - press, dryer D1 and kiln K1 - over one early shift, minute
5891
- * by minute: readings, samples, batches and the alarms they raise (`plant`);
5892
- * - the machine shop's plan for Tuesday, 17 March 2026: seven stations, six
5893
- * orders, their steps and the dependencies between them (`STATIONS`,
5894
- * `ORDERS`, `STEPS`, `MOVES`).
5895
- *
5896
- * Plain data and small pure functions, no imports: copy the file beside an
5897
- * example and it runs. Seeded, so the same seed gives the same numbers on
5898
- * every machine; nothing here reads a clock.
5899
- */
5900
-
5901
- /* The few shapes the umriss packages expect, written out so this file needs
5902
- no import. The data is structurally what the packages take. */
5903
-
5904
- /** A limit set as `@umriss-ui/core` reads it. */
5905
- interface LimitSet {
5906
- target?: number;
5907
- limits?: readonly { value: number; side: "upper" | "lower"; severity: "warning" | "alarm" }[];
5908
- }
5909
-
5910
- /** An alarm's dead band, as `@umriss-ui/table` reads it. */
5911
- interface ReturnBand {
5912
- direction: "upper" | "lower";
5913
- limit: number;
5914
- returnTo: number;
5915
- }
5916
-
5917
- /** A kind of alarm, as `@umriss-ui/table` reads it. */
5918
- interface AlarmType {
5919
- id: string;
5920
- label: string;
5921
- priority: "high" | "medium" | "low";
5922
- returnBand?: ReturnBand;
5923
- }
5924
-
5925
- /** One alarm, as `@umriss-ui/table` reads it. */
5926
- interface Alarm {
5927
- id: string;
5928
- type: string;
5929
- lifecycle: "active-unacknowledged" | "active-acknowledged" | "resolved-unacknowledged" | "resolved-acknowledged";
5930
- raised: number;
5931
- resolved?: number;
5932
- acknowledgedAt?: number;
5933
- availability?: "in-service" | "suppressed" | "disabled";
5934
- }
5935
-
5936
- /** A task, a subtask and a dependency, as `@umriss-ui/schedule` reads them. */
5937
- interface Task {
5938
- readonly id: string;
5939
- readonly name?: string;
5940
- readonly color: string;
5941
- }
5942
- interface Subtask {
5943
- readonly id: string;
5944
- readonly task: string;
5945
- readonly lane: string;
5946
- readonly from: number;
5947
- readonly to: number;
5948
- readonly leadIn?: number;
5949
- readonly leadOut?: number;
5950
- }
5951
- interface Dependency {
5952
- readonly id: string;
5953
- readonly from: string;
5954
- readonly to: string;
5955
- readonly lag: number;
5956
- readonly leaves?: "main" | "leadOut";
5957
- readonly arrives?: "main" | "leadIn";
5958
- }
5959
-
5960
- /** A small LCG - reproducible across runs and platforms. */
5961
- export function random(seed: number): () => number {
5962
- let s = seed >>> 0;
5963
- return () => {
5964
- s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
5965
- return s / 4294967296;
5966
- };
5967
- }
5968
-
5969
- /* ---------------------------------------------------------------------------
5970
- The kiln line over an early shift.
5971
-
5972
- Every part of a control room reads from this one shift - the trend, the
5973
- tiles, the alarm list, the control chart, the plan and the OEE. The limit
5974
- the kiln crosses in the trend is the alarm in the list, the verdict on the
5975
- tile and the scrap in the OEE, because all four are read off the same
5976
- minute of the same readings. The line counts in minutes from the start of
5977
- the shift, and whoever draws it adds the instant the shift began.
5978
-
5979
- The line generates alarms, which is exactly what the library does not do
5980
- (ADR-0009): it decides that a reading above the limit is an alarm, and when
5981
- it has come back far enough to clear. The table's model takes it from
5982
- there.
5983
- --------------------------------------------------------------------------- */
5984
-
5985
- /** The early shift, 06:00 to 14:00. */
5986
- export const SHIFT_MINUTES = 480;
5987
-
5988
- /** The kiln's zone 3, in °C: the numbers every part reads it against, once.
5989
- Between the two warnings lies the tolerance - a tile fired outside it is
5990
- scrap; `returnTo` is the alarm's dead band. */
5991
- export const KILN = { target: 1200, tolerance: [1185, 1215], alarm: 1230, returnTo: 1222 } as const;
5992
-
5993
- /** The same numbers as the limit set the tile and the verdict read. */
5994
- export const KILN_LIMITS: LimitSet = {
5995
- target: KILN.target,
5996
- limits: [
5997
- { value: KILN.tolerance[1], side: "upper", severity: "warning" },
5998
- { value: KILN.alarm, side: "upper", severity: "alarm" },
5999
- { value: KILN.tolerance[0], side: "lower", severity: "warning" },
6000
- ],
6001
- };
6002
-
6003
- /** The kiln fires one tile every quarter of a minute when nothing holds it up -
6004
- the ideal cycle time of the OEE's performance. */
6005
- export const IDEAL_CYCLE_MINUTES = 0.25;
6006
-
6007
- /** The kiln alarm's condition and its dead band, as the table's model reads them. */
6008
- const KILN_RETURN: ReturnBand = { direction: "upper", limit: KILN.alarm, returnTo: KILN.returnTo };
6009
-
6010
- /** What the four measuring points of the line can report. */
6011
- export const ALARM_TYPES: readonly AlarmType[] = [
6012
- {
6013
- id: "kiln-high",
6014
- label: "Kiln K1 · zone 3 above alarm limit",
6015
- priority: "high",
6016
- /* The dead band: back below `returnTo`, not merely below the limit, or a
6017
- reading riding on the limit would raise an alarm a minute. */
6018
- returnBand: KILN_RETURN,
6019
- },
6020
- { id: "exit-silent", label: "Kiln K1 · exit pyrometer sends nothing", priority: "medium" },
6021
- { id: "belt-empty", label: "Kiln K1 · belt empty", priority: "medium" },
6022
- { id: "dryer-fan", label: "Dryer D1 · fan vibration high", priority: "low" },
6023
- ];
6024
-
6025
- /** One minute of the line. */
6026
- export interface Reading {
6027
- /** Minutes since the start of the shift. */
6028
- minute: number;
6029
- /** Zone 3 of the kiln, in °C. */
6030
- kiln: number;
6031
- /** The exit pyrometer, in °C - `null` while it sends nothing. */
6032
- exit: number | null;
6033
- /** Whether the belt runs. */
6034
- running: boolean;
6035
- /** Tiles out of the kiln in this minute. */
6036
- fired: number;
6037
- /** Of them, the ones fired inside the tolerance. */
6038
- good: number;
6039
- }
6040
-
6041
- /** A tile taken off the belt and measured - one every ten minutes while it runs. */
6042
- export interface Sample {
6043
- minute: number;
6044
- /** The tile's length after firing, in mm (nominal 600). */
6045
- length: number;
6046
- }
6047
-
6048
- /** A batch of tiles through the three stations. */
6049
- export interface Batch {
6050
- id: string;
6051
- name: string;
6052
- /** How many tiles the batch is planned for. */
6053
- tiles: number;
6054
- /** Minutes since the start of the shift, per station. */
6055
- press: readonly [number, number];
6056
- dryer: readonly [number, number];
6057
- kiln: readonly [number, number];
6058
- }
6059
-
6060
- export interface Plant {
6061
- readings: readonly Reading[];
6062
- samples: readonly Sample[];
6063
- batches: readonly Batch[];
6064
- }
6065
-
6066
- /* When the exit pyrometer goes silent, and for how long. Fixed rather than
6067
- seeded, so that a running control room shows its reading turn stale, then
6068
- lost, and come back. */
6069
- const SILENT_FROM = 288;
6070
- const SILENT_FOR = 50;
6071
-
6072
- /* The dryer's vibration sensor is under maintenance all shift: its alarm is
6073
- active and disabled. */
6074
- const DRYER_FAN_RAISED = 20;
6075
-
6076
- /** The shift, from a seed. */
6077
- export function plant(seed: number): Plant {
6078
- const r = random(seed);
6079
-
6080
- /* The one excursion: a burner overshoots somewhere between 02:30 and 03:30
6081
- into the shift, for half an hour, peaking near 1242 °C. The operator stops
6082
- the feed twenty-two minutes in, for eighteen minutes. The noise around it
6083
- is too small to reach a warning on its own, so this is the only crossing -
6084
- which is what the tests hold. */
6085
- const surgeFrom = 150 + Math.floor(r() * 60);
6086
- const surgeFor = 30;
6087
- const stopFrom = surgeFrom + 22;
6088
- const stopFor = 18;
6089
-
6090
- /* The plan: batches through the kiln one after another, each pressed an
6091
- hour and dried half an hour before - the first ones during the night
6092
- shift. Tiles come out of the kiln only while a batch is in it, so the
6093
- OEE's count and the batches' counts are the same tiles. The plan is a
6094
- plan: a stop of the belt does not move it. */
6095
- const batches: Batch[] = [];
6096
- for (let at = 0, i = 0; at < SHIFT_MINUTES; i++) {
6097
- const length = 70 + Math.floor(r() * 25);
6098
- batches.push({
6099
- id: `b-${4121 + i}`,
6100
- name: `B-${4121 + i}`,
6101
- tiles: Math.round(length / IDEAL_CYCLE_MINUTES / 10) * 10,
6102
- press: [at - 60, at - 60 + length],
6103
- dryer: [at - 30, at - 30 + length],
6104
- kiln: [at, at + length],
6105
- });
6106
- at += length + 5;
6107
- }
6108
-
6109
- const readings: Reading[] = [];
6110
- const samples: Sample[] = [];
6111
- let drift = 0;
6112
- for (let minute = 0; minute < SHIFT_MINUTES; minute++) {
6113
- drift = 0.9 * drift + (r() - 0.5) * 3;
6114
- const into = minute - surgeFrom;
6115
- const surge = into >= 0 && into <= surgeFor ? 42 * Math.sin((Math.PI * into) / surgeFor) : 0;
6116
- const kiln = Math.round((KILN.target + drift + surge) * 10) / 10;
6117
-
6118
- const running = !(minute >= stopFrom && minute < stopFrom + stopFor);
6119
- const loaded = batches.some((batch) => batch.kiln[0] <= minute && minute < batch.kiln[1]);
6120
- const jam = r() < 0.12;
6121
- const fired = running && loaded ? (jam ? 3 : 4) : 0;
6122
- const inTolerance = kiln >= KILN.tolerance[0] && kiln <= KILN.tolerance[1];
6123
- const silent = minute >= SILENT_FROM && minute < SILENT_FROM + SILENT_FOR;
6124
- const exit = silent ? null : Math.round((kiln - 380 + (r() - 0.5) * 4) * 10) / 10;
6125
-
6126
- readings.push({ minute, kiln, exit, running, fired, good: inTolerance ? fired : 0 });
6127
-
6128
- if (running && minute % 10 === 0) {
6129
- /* Hotter shrinks more: the length follows the kiln, so the control
6130
- chart sees the excursion the trend sees. */
6131
- const length = 600 + (KILN.target - kiln) * 0.05 + (r() - 0.5) * 0.6;
6132
- samples.push({ minute, length: Math.round(length * 100) / 100 });
6133
- }
6134
- }
6135
-
6136
- return { readings, samples, batches };
6137
- }
6138
-
6139
- /** The readings up to and including the minute. */
6140
- export function upTo<T extends { minute: number }>(series: readonly T[], minute: number): readonly T[] {
6141
- return series.filter((one) => one.minute <= minute);
6142
- }
6143
-
6144
- /** How long ago the exit pyrometer last reported, in minutes - `0` while it
6145
- reports. What the tile's freshness is told. */
6146
- export function exitSilence(p: Plant, minute: number): number {
6147
- for (let m = minute; m >= 0; m--) {
6148
- if (p.readings[m]?.exit !== null) return minute - m;
6149
- }
6150
- return minute;
6151
- }
6152
-
6153
- /** When a condition held, as spans of minutes: `[from, to)`, `to` absent while
6154
- it still holds at the minute. */
6155
- function spans(
6156
- readings: readonly Reading[],
6157
- minute: number,
6158
- raise: (one: Reading) => boolean,
6159
- clear: (one: Reading) => boolean,
6160
- ): { from: number; to?: number }[] {
6161
- const found: { from: number; to?: number }[] = [];
6162
- let open: { from: number; to?: number } | null = null;
6163
- for (const one of readings) {
6164
- if (one.minute > minute) break;
6165
- if (open === null && raise(one)) {
6166
- open = { from: one.minute };
6167
- found.push(open);
6168
- } else if (open !== null && clear(one)) {
6169
- open.to = one.minute;
6170
- open = null;
6171
- }
6172
- }
6173
- return found;
6174
- }
6175
-
6176
- /**
6177
- * The alarms of the shift as they stand at the minute: raised, resolved - and
6178
- * acknowledged where the operator did, at the instant he did.
6179
- *
6180
- * `start` is the instant the shift began; the alarms carry instants because the
6181
- * table's model does.
6182
- */
6183
- export function alarmsAt(
6184
- p: Plant,
6185
- minute: number,
6186
- start: number,
6187
- acknowledged: ReadonlyMap<string, number> = new Map(),
6188
- ): Alarm[] {
6189
- const instant = (m: number) => start + m * 60_000;
6190
- const alarms: Alarm[] = [];
6191
-
6192
- const add = (
6193
- type: string,
6194
- span: { from: number; to?: number },
6195
- extra: { availability?: "suppressed" | "disabled" } = {},
6196
- ) => {
6197
- alarms.push({
6198
- id: `${type}-${span.from}`,
6199
- type,
6200
- lifecycle: span.to === undefined ? "active-unacknowledged" : "resolved-unacknowledged",
6201
- raised: instant(span.from),
6202
- ...(span.to === undefined ? {} : { resolved: instant(span.to) }),
6203
- ...extra,
6204
- });
6205
- };
6206
-
6207
- for (const span of spans(p.readings, minute, (one) => one.kiln > KILN_RETURN.limit, (one) => one.kiln <= KILN_RETURN.returnTo)) {
6208
- add("kiln-high", span);
6209
- }
6210
- for (const span of spans(p.readings, minute, (one) => one.exit === null, (one) => one.exit !== null)) {
6211
- add("exit-silent", span);
6212
- }
6213
- /* The belt runs empty because the operator stopped the feed: the plant's
6214
- logic knows it, so the alarm is suppressed - there, and neutral. */
6215
- for (const span of spans(p.readings, minute, (one) => !one.running, (one) => one.running)) {
6216
- add("belt-empty", span, { availability: "suppressed" });
6217
- }
6218
- if (minute >= DRYER_FAN_RAISED) {
6219
- add("dryer-fan", { from: DRYER_FAN_RAISED }, { availability: "disabled" });
6220
- }
6221
-
6222
- return alarms.map((alarm) => {
6223
- const at = acknowledged.get(alarm.id);
6224
- if (at === undefined) return alarm;
6225
- const lifecycle = alarm.resolved === undefined ? "active-acknowledged" : "resolved-acknowledged";
6226
- return { ...alarm, lifecycle, acknowledgedAt: at };
6227
- });
6228
- }
6229
-
6230
- /** The shift's figures for the OEE, up to and including the minute. */
6231
- export interface ShiftCount {
6232
- /** Minutes of the shift so far. */
6233
- planned: number;
6234
- /** Minutes the belt stood. */
6235
- downtime: number;
6236
- /** Tiles out of the kiln. */
6237
- total: number;
6238
- /** Of them, inside the tolerance. */
6239
- good: number;
6240
- }
6241
-
6242
- export function countAt(p: Plant, minute: number): ShiftCount {
6243
- const so = upTo(p.readings, minute);
6244
- return {
6245
- planned: so.length,
6246
- downtime: so.filter((one) => !one.running).length,
6247
- total: so.reduce((sum, one) => sum + one.fired, 0),
6248
- good: so.reduce((sum, one) => sum + one.good, 0),
6249
- };
6250
- }
6251
-
6252
- /* ---------------------------------------------------------------------------
6253
- The machine shop's plan: seven stations, six orders on their way through
6254
- them on Tuesday, 17 March 2026 - the day the browser suites freeze the
6255
- clock on.
6256
-
6257
- Written out, not generated from a seed: a planner reads these as a day's
6258
- plan, and a finding is placed where it is on purpose - the overlap on the
6259
- mill, the violated dependency into the paint shop.
6260
- --------------------------------------------------------------------------- */
6261
-
6262
- const planAt = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
6263
- const min = (n: number) => n * 60_000;
6264
-
6265
- export const DAY_OF_PLAN: readonly [number, number] = [planAt(5, 30), planAt(18)];
6266
-
6267
- export const STATIONS = [
6268
- { id: "saw", label: "Saw 1" },
6269
- { id: "lathe-1", label: "Lathe 1" },
6270
- { id: "lathe-2", label: "Lathe 2" },
6271
- { id: "mill", label: "Mill" },
6272
- { id: "press", label: "Press 2" },
6273
- { id: "paint", label: "Paint shop" },
6274
- { id: "qa", label: "Inspection" },
6275
- ] as const;
6276
-
6277
- /* Colours in both schemes, as the application's own tokens would give them. */
6278
- export const ORDERS: readonly Task[] = [
6279
- { id: "a-2041", name: "A-2041 Housing", color: "light-dark(#2563eb, #6b9bff)" },
6280
- { id: "a-2042", name: "A-2042 Shaft", color: "light-dark(#0d9488, #3cc7b8)" },
6281
- { id: "a-2043", name: "A-2043 Bracket", color: "light-dark(#c2410c, #f08a52)" },
6282
- { id: "a-2044", name: "A-2044 Flange", color: "light-dark(#7c3aed, #a98bfa)" },
6283
- { id: "a-2045", name: "A-2045 Cover", color: "light-dark(#be185d, #f06aa6)" },
6284
- { id: "a-2046", name: "A-2046 Axle", color: "light-dark(#4d7c0f, #8fc43e)" },
6285
- ];
6286
-
6287
- export const STEPS: readonly Subtask[] = [
6288
- { id: "a-2041-1", task: "a-2041", lane: "saw", from: planAt(6), to: planAt(7), leadIn: min(15), leadOut: min(10) },
6289
- { id: "a-2041-2", task: "a-2041", lane: "mill", from: planAt(8), to: planAt(10, 30), leadIn: min(30), leadOut: min(15) },
6290
- { id: "a-2041-3", task: "a-2041", lane: "qa", from: planAt(11, 30), to: planAt(12, 15) },
6291
-
6292
- { id: "a-2042-1", task: "a-2042", lane: "saw", from: planAt(7, 30), to: planAt(8, 15), leadIn: min(10) },
6293
- { id: "a-2042-2", task: "a-2042", lane: "lathe-1", from: planAt(9), to: planAt(11), leadIn: min(20), leadOut: min(15) },
6294
- { id: "a-2042-3", task: "a-2042", lane: "qa", from: planAt(13), to: planAt(13, 30) },
6295
-
6296
- { id: "a-2043-1", task: "a-2043", lane: "press", from: planAt(6, 30), to: planAt(8), leadIn: min(30), leadOut: min(15) },
6297
- { id: "a-2043-2", task: "a-2043", lane: "mill", from: planAt(10), to: planAt(11, 30), leadIn: min(15) },
6298
- { id: "a-2043-3", task: "a-2043", lane: "paint", from: planAt(12), to: planAt(14), leadIn: min(20), leadOut: min(20) },
6299
-
6300
- { id: "a-2044-1", task: "a-2044", lane: "lathe-2", from: planAt(6), to: planAt(8, 30), leadIn: min(20), leadOut: min(10) },
6301
- { id: "a-2044-2", task: "a-2044", lane: "press", from: planAt(9, 30), to: planAt(10, 45), leadIn: min(25) },
6302
- { id: "a-2044-3", task: "a-2044", lane: "paint", from: planAt(14, 45), to: planAt(16), leadIn: min(15), leadOut: min(15) },
6303
-
6304
- { id: "a-2045-1", task: "a-2045", lane: "lathe-1", from: planAt(12), to: planAt(13, 30), leadIn: min(15), leadOut: min(10) },
6305
- { id: "a-2045-2", task: "a-2045", lane: "press", from: planAt(14, 15), to: planAt(15), leadIn: min(20) },
6306
- { id: "a-2045-3", task: "a-2045", lane: "qa", from: planAt(15, 45), to: planAt(16, 30) },
6307
-
6308
- { id: "a-2046-1", task: "a-2046", lane: "lathe-2", from: planAt(10), to: planAt(12), leadIn: min(20), leadOut: min(15) },
6309
- { id: "a-2046-2", task: "a-2046", lane: "mill", from: planAt(13), to: planAt(14, 30), leadIn: min(20), leadOut: min(10) },
6310
- { id: "a-2046-3", task: "a-2046", lane: "qa", from: planAt(15), to: planAt(15, 30) },
6311
- ];
6312
-
6313
- export const MOVES: readonly Dependency[] = [
6314
- { id: "t-2041-1", from: "a-2041-1", to: "a-2041-2", lag: min(10) },
6315
- { id: "t-2041-2", from: "a-2041-2", to: "a-2041-3", lag: min(20) },
6316
- { id: "t-2042-1", from: "a-2042-1", to: "a-2042-2", lag: min(15) },
6317
- { id: "t-2042-2", from: "a-2042-2", to: "a-2042-3", lag: min(30), leaves: "main" },
6318
- { id: "t-2043-1", from: "a-2043-1", to: "a-2043-2", lag: min(45) },
6319
- /* Leaves the mill at 11:30 and has 10 minutes to reach the paint shop's
6320
- lead-in at 11:40 - it takes 25: a violated dependency, on purpose. */
6321
- { id: "t-2043-2", from: "a-2043-2", to: "a-2043-3", lag: min(25) },
6322
- { id: "t-2044-1", from: "a-2044-1", to: "a-2044-2", lag: min(20) },
6323
- { id: "t-2044-2", from: "a-2044-2", to: "a-2044-3", lag: min(60), arrives: "main" },
6324
- { id: "t-2045-1", from: "a-2045-1", to: "a-2045-2", lag: min(15) },
6325
- { id: "t-2045-2", from: "a-2045-2", to: "a-2045-3", lag: min(15) },
6326
- { id: "t-2046-1", from: "a-2046-1", to: "a-2046-2", lag: min(20) },
6327
- { id: "t-2046-2", from: "a-2046-2", to: "a-2046-3", lag: min(10), leaves: "main" },
6328
- ];
6329
- ```