fix: say what the bars and colours mean

The meter encoded two things at once and neither was guessable from looking
at it — obvious to whoever built it, opaque to everyone else.

Every row now carries a plain caption under the bar ("Aug 10 – Aug 17 · 3 of
7 days left", "started Apr 22 · no end date announced"), which explains the
bar and doubles as the exact dates people wanted anyway. A legend above the
list explains the colour ramp, which no per-row text can — shown once,
because a legend repeated fifty times is noise.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-15 01:09:54 +02:00
co-authored by Claude Opus 5
parent ad45b52c84
commit f4b133274a
4 changed files with 109 additions and 2 deletions
+5
View File
@@ -7,6 +7,7 @@ import { NextUp } from "./components/NextUp.tsx";
import { Timeline } from "./components/Timeline.tsx";
import { Welcome } from "./components/Welcome.tsx";
import { Colophon } from "./components/Colophon.tsx";
import { Legend } from "./components/Legend.tsx";
import { useCompletions } from "./state/useCompletions.ts";
import { usePrefs } from "./state/usePrefs.ts";
import { clockFor, DAY, endingSoonestFirst, formatRemaining } from "../shared/time.ts";
@@ -189,6 +190,7 @@ export function App() {
{live.length > 0 && (
<Section
legend
title="Running now"
hint={
live.length > 1
@@ -284,10 +286,12 @@ function Shell({ children }: { children: React.ReactNode }) {
function Section({
title,
hint,
legend,
children,
}: {
title: string;
hint?: string | undefined;
legend?: boolean | undefined;
children: React.ReactNode;
}) {
return (
@@ -296,6 +300,7 @@ function Section({
<h2 className="eyebrow">{title}</h2>
{hint !== undefined && <p className="text-xs text-faint">{hint}</p>}
</div>
{legend === true && <Legend />}
<ul className="border-t border-hairline">{children}</ul>
</section>
);
+13 -2
View File
@@ -1,6 +1,10 @@
import { gameMeta } from "../../shared/games.ts";
import type { GachaEvent } from "../../shared/schema.ts";
import { formatRemaining, type EventClock } from "../../shared/time.ts";
import {
formatRemaining,
windowCaption,
type EventClock,
} from "../../shared/time.ts";
import { Meter, URGENCY_COLOR } from "./Meter.tsx";
export interface RowEvent {
@@ -20,6 +24,8 @@ export function EventRow({ row, completed, onToggle, onOpen }: EventRowProps) {
const game = gameMeta(event.game);
const heat = URGENCY_COLOR[clock.urgency];
const caption = windowCaption(clock, Date.now());
const countdown = clock.upcoming
? `starts in ${formatRemaining(clock.startsMs - Date.now())}`
: clock.msRemaining === null
@@ -84,8 +90,13 @@ export function EventRow({ row, completed, onToggle, onOpen }: EventRowProps) {
<Meter
progress={clock.upcoming ? 1 : clock.progress}
urgency={clock.urgency}
label={`${event.title}: ${countdown}`}
label={`${event.title}: ${caption}`}
/>
{/* Says what the bar is a proportion of. Without this the ticks
are a shape the reader has to guess the meaning of. */}
<p className="mt-1.5 text-[0.6875rem] leading-none text-faint">
{caption}
</p>
</div>
</div>
+55
View File
@@ -0,0 +1,55 @@
import type { Urgency } from "../../shared/time.ts";
import { URGENCY_COLOR } from "./Meter.tsx";
const STEPS: Array<{ urgency: Urgency; label: string }> = [
{ urgency: "calm", label: "a week+" },
{ urgency: "near", label: "under a week" },
{ urgency: "soon", label: "under 3 days" },
{ urgency: "critical", label: "under a day" },
];
/**
* What the bars and colours mean, said once.
*
* The meter encodes two things at once, and neither is guessable from looking
* at it. Every row now carries a plain caption ("9 of 12 days left"), which
* explains the bar; this explains the colour, which no per-row text can.
*
* Shown once above the list rather than repeated per row — a legend that
* appears 50 times is noise, not help.
*/
export function Legend() {
return (
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 px-4 pb-3">
<p className="flex items-center gap-2 text-[0.6875rem] leading-none text-faint">
<span aria-hidden className="flex gap-[2px]">
{Array.from({ length: 8 }, (_, i) => (
<span
key={i}
className="h-2.5 w-[3px] rounded-[1px]"
style={{
background:
i < 5 ? URGENCY_COLOR.near : "var(--color-hairline)",
}}
/>
))}
</span>
bars = time left
</p>
<p className="flex flex-wrap items-center gap-x-2.5 gap-y-1 text-[0.6875rem] leading-none text-faint">
<span>colour = ends in</span>
{STEPS.map((step) => (
<span key={step.urgency} className="flex items-center gap-1">
<span
aria-hidden
className="size-2 rounded-[1px]"
style={{ background: URGENCY_COLOR[step.urgency] }}
/>
{step.label}
</span>
))}
</p>
</div>
);
}