feat(timeline): let a lane be one deadline queue, started or not
`endingSoonestFirst` holds every unstarted event behind every running one. That is right for a checklist — you cannot do a thing that has not opened — and it is one answer of two for a board, which is asked what runs out first. An event opening on Friday and closing on Sunday is a nearer deadline than one running now until October, and that order can never show it. So `byDeadline` is the same comparator with that clause removed, and `timelineLanes` takes a `split` flag choosing between them. It defaults to the old behaviour, so nothing moves yet. The flag is also the one case where lane mode reorders inside a lane, which the module otherwise refuses to do. Deliberate: the given order is live-first whatever the reader chose in the list, so honouring it would draw exactly the block this was asked not to draw. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c0e523bdc4
commit
6029e9ef10
@@ -1,5 +1,5 @@
|
|||||||
import type { LaneId } from "../../shared/custom.ts";
|
import type { LaneId } from "../../shared/custom.ts";
|
||||||
import { endingSoonestFirst, type EventClock } from "../../shared/time.ts";
|
import { byDeadline, endingSoonestFirst, type EventClock } from "../../shared/time.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How the timeline is stacked: a lane per game, or every game together in
|
* How the timeline is stacked: a lane per game, or every game together in
|
||||||
@@ -65,19 +65,35 @@ export interface Lane<T> {
|
|||||||
* Lane mode leaves the order it was given alone. The rows arrive sorted by
|
* Lane mode leaves the order it was given alone. The rows arrive sorted by
|
||||||
* whatever the reader chose in the list, and grouping them by game is not a
|
* whatever the reader chose in the list, and grouping them by game is not a
|
||||||
* licence to re-sort inside a game.
|
* licence to re-sort inside a game.
|
||||||
|
*
|
||||||
|
* `split` is the exception to that last sentence, and deliberately so. Both
|
||||||
|
* orders above hold every unstarted event behind every running one, which is
|
||||||
|
* the segregation the board's "Not started yet" heading names — so a reader who
|
||||||
|
* asks for them mixed in is asking for exactly that clause to be dropped, and
|
||||||
|
* `byDeadline` is the same comparator with it gone. It applies in **both**
|
||||||
|
* modes, lane mode included: leaving a lane's given order alone there would
|
||||||
|
* produce the block it was told not to draw, minus the heading that explained
|
||||||
|
* it, which is the worst of both answers.
|
||||||
*/
|
*/
|
||||||
export function timelineLanes<T extends Row>(
|
export function timelineLanes<T extends Row>(
|
||||||
rows: readonly T[],
|
rows: readonly T[],
|
||||||
mode: TimelineGroup,
|
mode: TimelineGroup,
|
||||||
|
split = true,
|
||||||
): Array<Lane<T>> {
|
): Array<Lane<T>> {
|
||||||
|
const order = split ? endingSoonestFirst : byDeadline;
|
||||||
|
|
||||||
if (mode === "ending") {
|
if (mode === "ending") {
|
||||||
if (rows.length === 0) return [];
|
if (rows.length === 0) return [];
|
||||||
return [{ id: "all", game: null, rows: [...rows].sort(endingSoonestFirst) }];
|
return [{ id: "all", game: null, rows: [...rows].sort(order) }];
|
||||||
}
|
}
|
||||||
|
|
||||||
const byGame = new Map<LaneId, T[]>();
|
const byGame = new Map<LaneId, T[]>();
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
byGame.set(row.event.game, [...(byGame.get(row.event.game) ?? []), row]);
|
byGame.set(row.event.game, [...(byGame.get(row.event.game) ?? []), row]);
|
||||||
}
|
}
|
||||||
return [...byGame].map(([game, laneRows]) => ({ id: game, game, rows: laneRows }));
|
return [...byGame].map(([game, laneRows]) => ({
|
||||||
|
id: game,
|
||||||
|
game,
|
||||||
|
rows: split ? laneRows : [...laneRows].sort(byDeadline),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,6 +344,30 @@ export function endingSoonestFirst(
|
|||||||
return a.clock.msRemaining - b.clock.msRemaining;
|
return a.clock.msRemaining - b.clock.msRemaining;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort key: soonest deadline first, and nothing else.
|
||||||
|
*
|
||||||
|
* The other half of the timeline's "put them all together" (PRD F1). Where
|
||||||
|
* `endingSoonestFirst` holds every unstarted event behind every running one —
|
||||||
|
* which is what a *checklist* wants, since you cannot do a thing that has not
|
||||||
|
* opened — this asks the one question a Gantt board is for: what runs out
|
||||||
|
* first. An event opening on Friday and closing on Sunday is a nearer deadline
|
||||||
|
* than one running now until October, and a reader looking at a calendar rather
|
||||||
|
* than a to-do list is entitled to see it in that order.
|
||||||
|
*
|
||||||
|
* The `endsAt: null` rule is the same in both: an unannounced end is real but is
|
||||||
|
* not a deadline, so it sorts behind every dated row rather than claiming a
|
||||||
|
* place in the queue.
|
||||||
|
*/
|
||||||
|
export function byDeadline(
|
||||||
|
a: { clock: EventClock },
|
||||||
|
b: { clock: EventClock },
|
||||||
|
): number {
|
||||||
|
if (a.clock.msRemaining === null) return b.clock.msRemaining === null ? 0 : 1;
|
||||||
|
if (b.clock.msRemaining === null) return -1;
|
||||||
|
return a.clock.msRemaining - b.clock.msRemaining;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A plain-language caption for an event's window.
|
* A plain-language caption for an event's window.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user