feat(lens): order the deadlines, not just the closest one

The headline panel asked for one row and got one row, so nothing else could
ask this question. `nextToExpire` answers it for any count and `firstToExpire`
is now that function asked for one — one definition, so a big countdown and the
lines under it can never disagree about which deadline is next.

Unannounced ends still sort behind every dated one however long they have been
running: a panel of deadlines that leads with "unknown" is not a panel of
deadlines. It sorts a copy, because the array it is handed is the one the list
on screen is rendering from.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-18 03:25:46 +02:00
co-authored by Claude Opus 5
parent c2179e29e0
commit 6e95e54ec6
2 changed files with 81 additions and 18 deletions
+26 -18
View File
@@ -39,28 +39,36 @@ export function outstanding<T extends Row>(
}
/**
* The single row closest to expiring.
* The rows closest to expiring, soonest first.
*
* Reads the minimum rather than taking the first row, because the list it is
* given is sorted by whatever mode the reader chose — under "doing first" the
* head of the list is what they are partway through, which is not what a panel
* headed "next to expire" is claiming to show.
* Ordered here rather than taken off the top of the list, because the list it
* is given is sorted by whatever mode the reader chose — under "doing first"
* the head of the list is what they are partway through, which is not what a
* panel headed "next to expire" is claiming to show.
*
* An event with no announced end can only ever be the answer when nothing else
* is running: it is real, but it is not a deadline.
* An event with no announced end sorts behind every dated one however long it
* has been running: it is real, but it is not a deadline, and it can only
* surface here once the deadlines run out.
*/
export function nextToExpire<T extends Row>(
rows: readonly T[],
count: number,
): T[] {
const dated = rows
.filter((r) => r.clock.msRemaining !== null)
.sort((a, b) => (a.clock.msRemaining ?? 0) - (b.clock.msRemaining ?? 0));
const undated = rows.filter((r) => r.clock.msRemaining === null);
return [...dated, ...undated].slice(0, Math.max(0, count));
}
/**
* The single row closest to expiring — the headline's own event.
*
* One definition, asked for one row, so the big countdown and the lines under
* it can never disagree about which deadline is next.
*/
export function firstToExpire<T extends Row>(rows: readonly T[]): T | null {
let best: T | null = null;
let bestMs = Infinity;
for (const row of rows) {
const ms = row.clock.msRemaining;
if (ms === null) continue;
if (ms < bestMs) {
best = row;
bestMs = ms;
}
}
return best ?? rows[0] ?? null;
return nextToExpire(rows, 1)[0] ?? null;
}
/**