feat: add the depletion meter
A discrete tick-strip rather than a smooth progress bar, borrowed from the stamina meters these games all use — ticks read as a finite resource visibly draining, which is the right metaphor for a limited-time event. An event with no announced end gets a hatched strip, never a full meter. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ac165f0df7
commit
ec8cc97c1a
@@ -0,0 +1,65 @@
|
|||||||
|
import type { Urgency } from "../../shared/time.ts";
|
||||||
|
|
||||||
|
const TICKS = 24;
|
||||||
|
|
||||||
|
export const URGENCY_COLOR: Record<Urgency, string> = {
|
||||||
|
expired: "var(--color-faint)",
|
||||||
|
critical: "var(--color-critical)",
|
||||||
|
soon: "var(--color-soon)",
|
||||||
|
near: "var(--color-near)",
|
||||||
|
calm: "var(--color-calm)",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MeterProps {
|
||||||
|
/** 0–1 through the event's own window. Null when the end is unannounced. */
|
||||||
|
progress: number | null;
|
||||||
|
urgency: Urgency;
|
||||||
|
/** Suppresses the entry animation for long lists. */
|
||||||
|
animate?: boolean | undefined;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The depletion meter — this interface's signature element.
|
||||||
|
*
|
||||||
|
* A discrete tick-strip rather than a smooth bar, borrowed from the stamina
|
||||||
|
* meters these games all use. Ticks read as a countable resource in a way a
|
||||||
|
* continuous bar does not, which is the right metaphor: what is left is
|
||||||
|
* finite and visibly draining.
|
||||||
|
*
|
||||||
|
* An event with no announced end gets a hatched strip instead of a filled one.
|
||||||
|
* It must not look like a full meter — "we don't know" and "loads of time" are
|
||||||
|
* different facts and conflating them is the failure this product avoids.
|
||||||
|
*/
|
||||||
|
export function Meter({ progress, urgency, animate = true, label }: MeterProps) {
|
||||||
|
const unknown = progress === null;
|
||||||
|
const remainingTicks = unknown
|
||||||
|
? 0
|
||||||
|
: Math.max(0, Math.round((1 - progress) * TICKS));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="meter"
|
||||||
|
role="img"
|
||||||
|
aria-label={label}
|
||||||
|
style={{ ["--tick" as string]: URGENCY_COLOR[urgency] }}
|
||||||
|
>
|
||||||
|
{Array.from({ length: TICKS }, (_, i) => {
|
||||||
|
// Ticks drain from the right, so the surviving run sits at the left
|
||||||
|
// edge and rows align into a readable ramp down the list.
|
||||||
|
const live = i < remainingTicks;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className="meter-tick"
|
||||||
|
data-live={live}
|
||||||
|
data-unknown={unknown}
|
||||||
|
style={
|
||||||
|
animate ? { animationDelay: `${Math.min(i, 12) * 14}ms` } : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user