Phone Aero glass + a timed run you can see

The phone (touch) chat chrome carried a 55% flat white veil while every other
Aero surface uses the thin 32% Ask-pill tint, so the header, welcome card and
composer read as plain white on a bright sky. They now use the same flat glass
as the cards, the answer/user bubbles keep 42% for long reading, and the
composer's field is a lighter 42% tile. The shared Aero wash block moved above
the touch media query so its background-image can no longer resurrect the bake
on phones.

The timed run worked but was invisible: the only cue was a small chip among the
streak/XP counters. The chrome pill now draws the countdown along its lower
edge, the chip is bordered (a control, not a stat), turns rose with the bar in
the last ten seconds, and fires one soft haptic tick at ten seconds.

Contrast scan clean on the phone chat in all three themes; activities suite
pins the countdown bar (25 checks). Version 0.6.2, SW v84.
This commit is contained in:
Martin Dvořák 2026-10-15 08:11:52 +00:00
commit a8b3dfd0e3
6 changed files with 129 additions and 75 deletions

View file

@ -175,6 +175,9 @@ export function StudyFeed({ deck }: { deck: Deck }) {
/* Timed runs ---------------------------------------------------------- */
const timedActivity = index < total ? queue[index] : null;
const timedAnswered = timedActivity ? Boolean(results[timedActivity.id]) : true;
/** Seconds left on the card in view, or null when no countdown runs. */
const timedRemaining = timed && remaining !== null && timedActivity && !timedAnswered ? remaining : null;
const timedDanger = timedRemaining !== null && timedRemaining <= 10;
useEffect(() => {
if (!timed || !timedActivity || timedAnswered) {
@ -188,6 +191,15 @@ export function StudyFeed({ deck }: { deck: Deck }) {
return () => window.clearInterval(timer);
}, [timed, timedActivity, timedAnswered, runId]);
/* Last ten seconds: one soft tick per card, like a phone timer. */
const warnedRef = useRef<string | null>(null);
useEffect(() => {
if (!timed || timedRemaining === null || timedRemaining > 10 || !timedActivity) return;
if (warnedRef.current === timedActivity.id) return;
warnedRef.current = timedActivity.id;
haptic("toggle");
}, [timed, timedRemaining, timedActivity]);
/* Ran out: the card counts as a miss (the same path as a wrong answer, so
history and the SRS schedule see it), the learner is told, and the feed
moves on. */
@ -290,7 +302,24 @@ export function StudyFeed({ deck }: { deck: Deck }) {
the streak/xp chips at the other end. Everything sits on the pill's
centre line with even padding, so nothing crowds the rounded ends. */}
<header className="pt-safe absolute inset-x-0 top-0 z-30 px-3">
<div className="study-chrome mx-auto flex max-w-xl items-center gap-2.5 rounded-full px-2.5 py-2">
<div className="study-chrome relative mx-auto flex max-w-xl items-center gap-2.5 rounded-full px-2.5 py-2">
{/* The countdown, drawn along the pill's lower edge: the chip alone
was easy to miss mid-card. */}
{timedRemaining !== null ? (
<span
aria-hidden
className="pointer-events-none absolute inset-x-4 bottom-[0.3rem] h-[3px] overflow-hidden rounded-full bg-[color-mix(in_oklab,var(--text)_14%,transparent)]"
>
<span
data-timed-bar
className={cn(
"block h-full rounded-full transition-[width] duration-1000 ease-linear motion-reduce:transition-none",
timedDanger ? "bg-rose-500" : "grad-ice",
)}
style={{ width: `${Math.round((timedRemaining / TIMED_CARD_SECONDS) * 100)}%` }}
/>
</span>
) : null}
<Link
href="/"
aria-label={t("feed.back")}
@ -328,12 +357,14 @@ export function StudyFeed({ deck }: { deck: Deck }) {
aria-label={timed ? t("feed.timedOn") : t("feed.timed")}
title={timed ? t("feed.timedOn") : t("feed.timed")}
className={cn(
"inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-extrabold tabular-nums transition",
timed ? "tint-peach" : "bg-[var(--surface-sunken)] text-[var(--text-muted)] hover:text-[var(--text)]",
"inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-xs font-extrabold tabular-nums transition",
timed
? cn("border-transparent tint-peach", timedDanger && "tint-rose")
: "border-[var(--glass-border)] bg-[var(--surface-strong)] text-[var(--text-muted)] hover:text-[var(--text)]",
)}
>
<ClockIcon className="size-3.5" />
{timed && remaining !== null ? `${remaining}s` : `${TIMED_CARD_SECONDS}s`}
{timedRemaining !== null ? `${timedRemaining}s` : `${TIMED_CARD_SECONDS}s`}
</button>
<span
className={cn(