diff --git a/src/client/App.tsx b/src/client/App.tsx
index 0a639ee..26d53dc 100644
--- a/src/client/App.tsx
+++ b/src/client/App.tsx
@@ -286,6 +286,15 @@ export function App() {
const live = visible.filter((r) => r.clock.live);
const upcoming = visible.filter((r) => r.clock.upcoming);
+ /**
+ * The unstarted events the checklist actually lists.
+ *
+ * `upcoming` stays the full count either way, because the page header states
+ * it — a section that is simply absent is indistinguishable from a quiet
+ * fortnight, and this app does not leave a reader to infer what it is not
+ * showing them.
+ */
+ const listedUpcoming = prefs.showUpcoming ? upcoming : [];
/**
* What the page is telling the reader to *do*, as opposed to what it is
@@ -507,13 +516,13 @@ export function App() {
)}
- {upcoming.length > 0 && (
+ {listedUpcoming.length > 0 && (
- {focus !== null - ? `Nothing running in ${gameMeta(focus).name}. Try another game, or show all of them.` - : "Nothing to show. Every game is switched off, or you've finished everything and hidden completed events."} + {upcoming.length > 0 + ? `Nothing running right now. ${ + upcoming.length === 1 + ? "One event has" + : `${upcoming.length} events have` + } not started yet — switch on “Show events that haven't started” below to list them.` + : focus !== null + ? `Nothing running in ${gameMeta(focus).name}. Try another game, or show all of them.` + : "Nothing to show. Every game is switched off, or you've finished everything and hidden completed events."}
)} @@ -551,7 +569,7 @@ export function App() { // The board holds these back itself rather than being handed a // shorter list, so it can say how many are waiting when there is // nothing else left to draw. The switch is in settings. - showUpcoming={prefs.timelineUpcoming} + showUpcoming={prefs.showUpcoming} splitUpcoming={prefs.timelineSplitUpcoming} onOpen={setOpenId} isDone={isDone} diff --git a/src/client/components/Controls.tsx b/src/client/components/Controls.tsx index f43635e..efbdfc4 100644 --- a/src/client/components/Controls.tsx +++ b/src/client/components/Controls.tsx @@ -34,7 +34,7 @@ const SPLITS: Array<{ split: boolean; label: string; hint: string }> = [ { split: true, label: "In their own group", - hint: "Each lane runs out, then a \u201cNot started yet\u201d heading and what is queued behind it.", + hint: "Each lane runs out, then a \u201cNot started yet\u201d heading and what is queued behind it \u2014 the shape the checklist has either way.", }, { split: false, @@ -156,28 +156,24 @@ export function Controls({ Show events I've finished - {/* Sits with the other two "what am I allowed to look at" rows - because that is the question it answers — but unlike them it - is answered for the board only, and a row that did not say so - would read as a promise about the whole app. The checklist - keeps its own "Not started yet" section either way, which is - why switching this off costs a reader nothing they cannot - still go and read. */} + {/* One of the three "what am I allowed to look at" rows, and it + reaches both views: the checklist's "Not started yet" section + and the board's future bars are the same events answering the + same question. Off is the default because this app answers + *what expires next* — see PRD F1. */} @@ -187,7 +183,7 @@ export function Controls({ are on it, and offering it anyway is a control that does nothing — the stored answer is kept either way, so switching the row above back on restores it rather than a default. */} - {prefs.timelineUpcoming && ( + {prefs.showUpcoming && (+ On the timeline.{" "} {SPLITS.find((s) => s.split === prefs.timelineSplitUpcoming) ?.hint}
diff --git a/src/client/components/Timeline.tsx b/src/client/components/Timeline.tsx index 8d0d838..dd84800 100644 --- a/src/client/components/Timeline.tsx +++ b/src/client/components/Timeline.tsx @@ -113,10 +113,11 @@ export function Timeline({ /** * Whether events that have not started yet are plotted. * - * Off by default (`prefs.timelineUpcoming`). The board is asked "how does the - * time I am in lay out?", and every lane has a next patch queued behind it — - * plotting those unasked stretches the window weeks past today and squeezes - * the running bars the reader came for. + * Off by default (`prefs.showUpcoming`, which governs the checklist's own + * "Not started yet" section too). The board is asked "how does the time I am + * in lay out?", and every lane has a next patch queued behind it — plotting + * those unasked stretches the window weeks past today and squeezes the + * running bars the reader came for. * * Read-only here: the switch lives in settings with the other two answers to * "what am I allowed to look at" (`Controls`), not in the board's own header diff --git a/src/client/state/usePrefs.ts b/src/client/state/usePrefs.ts index 690574d..6db629d 100644 --- a/src/client/state/usePrefs.ts +++ b/src/client/state/usePrefs.ts @@ -87,18 +87,21 @@ export interface Prefs { */ timelineGroup: TimelineGroup; /** - * Whether the board plots events that have not started yet. + * Whether events that have not started yet are shown at all — the board + * plots them, and the checklist keeps its "Not started yet" section. * - * Off by default, and that is a claim about what the board is *for*: it - * answers "how does the time I am in lay out?", and a reader with fourteen - * lanes has a next patch queued behind every one of them. Plotting those by - * default pushes the right edge of the board weeks past today, shrinks every - * running bar to make room, and fills the space with things nobody can do - * yet. Nothing is lost by leaving them off — the checklist's "Not started - * yet" section has listed them all along, and the toggle in the board's - * header says how many are being held back. + * Off by default, and that is a claim about what this app is for: it answers + * *what expires next*, and a reader with fourteen lanes has a next patch + * queued behind every one of them. On the board it is also structural, since + * the window is drawn from what is plotted — showing the future pushes the + * right edge weeks past today and shrinks every running bar to make room. + * + * It sits with `showCompleted` and `showIgnored` because it is the same + * question: what is the reader allowed to look at. It was called + * `timelineUpcoming` while it governed only the board — see `adoptRenamed`, + * which carries a reader's stored answer across rather than resetting it. */ - timelineUpcoming: boolean; + showUpcoming: boolean; /** * Whether those unstarted events keep to their own block on the board, under * a "Not started yet" heading, or sit in one deadline order with everything @@ -111,9 +114,12 @@ export interface Prefs { * Sunday is a nearer deadline than one running now until October, and the * split order can never show that. * + * The board only: the checklist splits them structurally, into a section with + * a heading of its own, and always has. + * * Defaults to `true`, the board as it was before this existed. Only read when - * `timelineUpcoming` is on — with nothing unstarted plotted there is no block - * to keep apart — but stored either way, so switching the parent back on + * `showUpcoming` is on — with nothing unstarted plotted there is no block to + * keep apart — but stored either way, so switching the parent back on * restores the answer they gave rather than a default. */ timelineSplitUpcoming: boolean; @@ -155,7 +161,7 @@ function defaults(): Prefs { view: "soon", timelineDayWidth: DEFAULT_DAY_WIDTH, timelineGroup: "game", - timelineUpcoming: false, + showUpcoming: false, timelineSplitUpcoming: true, detectDaily: false, showCompleted: true, @@ -204,10 +210,34 @@ export function adoptNewLanes( }; } +/** + * A stored `prefs` object with the one renamed field carried across. + * + * `timelineUpcoming` became `showUpcoming` when it stopped being about the + * board alone. Dropping the old name would not lose data — this is one blob + * under one key, not a key space — but it would silently reset the answer of + * every reader who had switched the future on, and they would have to find the + * setting again to say a thing they already said. + * + * A stored new name always wins, so this can never overwrite a fresher answer + * with a stale one; and once written back under the new name the old one is + * simply an unread leftover. Pure and exported so that is a test rather than a + * claim. + */ +export function adoptRenamed( + stored: Partial