feat(timeline): offer both readings of a board with the future on it
"In their own group" is the shape of a patch — what is on now, and what is queued behind it. "Mixed in" is the question a Gantt chart exists for: what runs out first, whether or not it has opened. On real data that is not a cosmetic difference — mixed, five events currently running sort below an Arknights rerun that has not started, because it closes before they do. Which is why it is a pair of pills and not a checkbox: neither answer is the absence of the other, and "mixed in" is a different order rather than the heading switched off. It sits under the row that puts the future on the board at all, and only while that row is ticked — a choice about arranging unstarted events is unanswerable with none of them on screen. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9dde82801a
commit
3db93a1ccf
@@ -552,6 +552,7 @@ export function App() {
|
||||
// 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}
|
||||
splitUpcoming={prefs.timelineSplitUpcoming}
|
||||
onOpen={setOpenId}
|
||||
isDone={isDone}
|
||||
/>
|
||||
|
||||
@@ -21,6 +21,28 @@ const THEMES: Array<{ id: ThemeChoice; label: string }> = [
|
||||
{ id: "system", label: "System" },
|
||||
];
|
||||
|
||||
/**
|
||||
* The two readings of a board with the future on it, and both are right for
|
||||
* somebody — see PRD F1.
|
||||
*
|
||||
* Kept as a pair of pills rather than a second checkbox because neither answer
|
||||
* is the absence of the other: "mixed in" is a different order, not a heading
|
||||
* switched off. A checkbox would name one of them and leave the other as
|
||||
* whatever is left over.
|
||||
*/
|
||||
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.",
|
||||
},
|
||||
{
|
||||
split: false,
|
||||
label: "Mixed in",
|
||||
hint: "One deadline order, started or not — so something opening Friday and closing Sunday sits above an event running until October.",
|
||||
},
|
||||
];
|
||||
|
||||
export function Controls({
|
||||
games,
|
||||
prefs,
|
||||
@@ -160,6 +182,39 @@ export function Controls({
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{/* Only while there is something to arrange. A choice about how
|
||||
unstarted events sit on the board is unanswerable when none
|
||||
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 && (
|
||||
<div className="ml-6 flex flex-col gap-1.5">
|
||||
<div className="flex gap-1.5">
|
||||
{SPLITS.map((s) => (
|
||||
<button
|
||||
key={String(s.split)}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onUpdate({ timelineSplitUpcoming: s.split })
|
||||
}
|
||||
aria-pressed={prefs.timelineSplitUpcoming === s.split}
|
||||
className={`rounded-full border px-3 py-1 text-[0.6875rem] font-medium transition-colors ${
|
||||
prefs.timelineSplitUpcoming === s.split
|
||||
? "border-ink/70 text-ink"
|
||||
: "border-hairline text-faint hover:text-muted"
|
||||
}`}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="max-w-xs text-xs leading-relaxed text-faint">
|
||||
{SPLITS.find((s) => s.split === prefs.timelineSplitUpcoming)
|
||||
?.hint}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Detection reads the source's wording and is wrong in both
|
||||
directions, so it ships off and says so. Off leaves only the
|
||||
events the reader marked, and discards nothing — every mark and
|
||||
|
||||
@@ -98,6 +98,7 @@ export function Timeline({
|
||||
group,
|
||||
onGroup,
|
||||
showUpcoming,
|
||||
splitUpcoming,
|
||||
onOpen,
|
||||
isDone,
|
||||
}: {
|
||||
@@ -124,6 +125,18 @@ export function Timeline({
|
||||
* decides what is on it at all.
|
||||
*/
|
||||
showUpcoming: boolean;
|
||||
/**
|
||||
* Whether those unstarted events keep to their own block under a heading, or
|
||||
* sit in one deadline order with the running ones
|
||||
* (`prefs.timelineSplitUpcoming`, and the switch is in settings beside the
|
||||
* one above).
|
||||
*
|
||||
* Mixed is not merely the heading switched off: the orders this board is
|
||||
* given all hold unstarted rows behind running ones, so dropping the label
|
||||
* alone would leave the same block with nothing explaining it. `lanes.ts`
|
||||
* re-sorts instead, and this only decides whether the heading is drawn.
|
||||
*/
|
||||
splitUpcoming: boolean;
|
||||
onOpen: (id: string) => void;
|
||||
/**
|
||||
* Asked rather than derived from the progress store: an entry exists there
|
||||
@@ -201,7 +214,7 @@ export function Timeline({
|
||||
);
|
||||
}
|
||||
|
||||
const lanes = timelineLanes(plotted, group);
|
||||
const lanes = timelineLanes(plotted, group, splitUpcoming);
|
||||
const marks = startMarkers(plotted, x);
|
||||
|
||||
const months = monthBoundaries(min, max);
|
||||
@@ -351,7 +364,9 @@ export function Timeline({
|
||||
{lanes.map((lane) => {
|
||||
const heading = lane.game === null ? null : gameMeta(lane.game);
|
||||
// Where this lane stops running and starts being scheduled.
|
||||
const breakAt = splitAt(lane.rows);
|
||||
// Mixed in, there is no such place — the rows are one deadline
|
||||
// queue and a heading would be pointing at the middle of it.
|
||||
const breakAt = splitUpcoming ? splitAt(lane.rows) : -1;
|
||||
return (
|
||||
<div key={lane.id}>
|
||||
{/* On its own line and pinned to the left edge, so the lane
|
||||
|
||||
Reference in New Issue
Block a user