feat(focus): the game chips answer the pointer, as the dailies do

They are the same object twice — a pill carrying a game's colour, pressed
or not, that you tap — so they get the same hover rather than a second
one: a ring in the game's hue, a one-pixel lift, and the label coming up
to full ink while the chip is not the pressed one.

Sharing it meant `.tick-chip` becoming `.hue-chip`, and dropping the
`data-done` attribute it keyed off. Both chips were already saying the
same thing in `aria-pressed`, and a second attribute repeating it is one
more thing to keep in step — a rule that reads the announced state cannot
disagree with it.

The focus strip needed a pixel of headroom for that lift. `scroll-x` sets
`overflow-x: auto`, which computes the vertical axis to `auto` as well,
so a chip that rises loses that pixel and its ring to the scroller's
edge; `pt-1` is the room, and `mt` gives the same total gap back.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-19 06:13:48 +02:00
co-authored by Claude Opus 5
parent 858551ac98
commit d3066aefdc
3 changed files with 41 additions and 31 deletions
+6 -6
View File
@@ -194,11 +194,11 @@ function TickChip({
aria-pressed={isDone}
aria-label={`${ariaLabel}${isDone ? ", done today" : ", not done today"}`}
title={title}
// Done-ness for the stylesheet: the chip's own colours are the game's
// hue and arrive inline, which no rule can override, so the hover has to
// be drawn from what inline does not own — see `.tick-chip`.
data-done={isDone}
className="tick-chip flex max-w-[15rem] items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-medium"
// `.hue-chip` is the shared hover: the chip's own colours are the game's
// hue and arrive inline, which no rule can override, so it is drawn from
// what inline does not own. `--hue` below is the only thing it needs;
// pressed-ness it reads off `aria-pressed`, which is already up there.
className="hue-chip flex max-w-[15rem] items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-medium"
style={{
["--hue" as string]: hue,
borderColor: isDone ? hue : `color-mix(in srgb, ${hue} 20%, transparent)`,
@@ -222,7 +222,7 @@ function TickChip({
opacity={isDone ? 1 : 0.25}
/>
</svg>
<span className="tick-label truncate">{label}</span>
<span className="hue-chip-label truncate">{label}</span>
{streak > 1 && (
<span className="tnum shrink-0 text-[0.625rem] opacity-70">{streak}d</span>
)}
+12 -3
View File
@@ -61,7 +61,12 @@ export function GameFocus({
<div
role="group"
aria-label="Focus on one game"
className="scroll-x -mx-4 mt-2 flex gap-1.5 px-4 pb-1 lg:flex-wrap"
// `pt-1` is the lift's headroom, not spacing: `scroll-x` sets
// `overflow-x: auto`, which computes the vertical axis to `auto` as
// well, so a chip that rises a pixel under the cursor loses that pixel
// and its ring to the scroller's edge. `mt` gives the same total gap
// back.
className="scroll-x -mx-4 mt-1 flex gap-1.5 px-4 pb-1 pt-1 lg:flex-wrap"
>
<Chip
label="All"
@@ -120,8 +125,12 @@ function Chip({
onClick={onClick}
aria-pressed={on}
aria-label={`${ariaLabel ?? label}, ${count} outstanding`}
className="flex shrink-0 items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors"
// The dailies strip's hover, and for the same reason: these colours are a
// game's hue, so they arrive inline where no rule can reach them, and
// `.hue-chip` works off `--hue` and the `aria-pressed` above instead.
className="hue-chip flex shrink-0 items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium"
style={{
["--hue" as string]: hue,
borderColor: on ? hue : `color-mix(in srgb, ${hue} 30%, transparent)`,
color: on ? hue : "var(--color-muted)",
background: on
@@ -131,7 +140,7 @@ function Chip({
opacity: count === 0 && !on ? 0.5 : 1,
}}
>
{label}
<span className="hue-chip-label">{label}</span>
<span className="tnum text-[0.625rem] opacity-70">{count}</span>
</button>
);