diff --git a/README.md b/README.md index 6c619c4..8059642 100644 --- a/README.md +++ b/README.md @@ -1434,6 +1434,25 @@ Things I chose while building, with the reasoning: (measured: 268..1280 at a 1280 window) — a flat pane floating 40px inside the scene read as "cut off next to the sidebar". +93. **Phone Aero glass, and a timed run you can actually see.** Two follow-ups from a phone. + - *The chat read as a white pane.* On touch devices the chat header, welcome card and composer + carried a 55% flat white veil while every other surface in the app used the thin 32% + Ask-pill tint — over a bright sky that is "fully white with no transparency", nothing like + the rest of the theme. They now use the same flat glass as the cards (the baked wallpaper + layer stays desktop-only, exactly as on every other surface), the answer and user bubbles + keep a touch more body for long reading (42%), and the composer's field is a lighter 42% + tile of the same family. Ordering matters and is now pinned by a comment: the shared wash + block sits *before* the touch media query, otherwise its later `background-image` wins and + the bake (and its late repaint) comes back on phones. The chat screen's pixel-contrast scan + is clean in all three themes at 390px — the one earlier finding was the Next.js dev-tools + badge sitting over the disclaimer, not the panel. + - *The timer looked dead.* It worked (30 → 0, marked as a miss, toast, next card) but the only + cue was a small chip among the streak/XP counters, so turning it on seemed to do nothing. + The chrome pill now draws the countdown along its lower edge (`[data-timed-bar]`), the chip + has a border so it reads as a control, it turns rose together with the bar in the last ten + seconds, and a soft haptic tick fires at ten seconds. The activities suite pins the bar as + well as the countdown. + 92. **Study cards: where the answer came from, read aloud, and timed runs.** Three additions to the study feed, all local and dependency-free. - **From your notes.** The generator now returns `source` with every activity: the verbatim diff --git a/app/globals.css b/app/globals.css index 7d348ea..f7f6c22 100644 --- a/app/globals.css +++ b/app/globals.css @@ -604,104 +604,6 @@ background-repeat: no-repeat, no-repeat; } -/* Touch devices: flat glass, no bake and no live blur. The baked layer is - `background-attachment: fixed`, which mobile engines repaint late (iOS - treats it as scroll) — panes showed a stale crop until a tap forced a - repaint. The honest alternative (a real backdrop-filter) is worse: every - pane re-blurs on every frame while the bubbles animate, and phones choke — - measured as input lag, "buttons take ages to respond". So on touch the - panes keep the thin Ask-pill tint over the sharp scene: transparent glass - at zero per-frame cost. Desktop keeps the baked blur. */ -@media (max-width: 639px), (pointer: coarse) { - .aero .card, - .aero .glass-solid, - .aero .sidebar-surface, - .aero .gate-card, - .aero .list-group, - .aero .nav-action, - .aero .nav-row[aria-current="page"], - .aero .nav-row[aria-pressed="true"], - .aero .nav-row[data-active="true"], - .aero .glass-chip, - .aero .glass-pill, - .aero .btn-secondary, - .aero .btn-ghost, - .aero .chat-header, - .aero .chat-empty, - .aero .segmented { - background-image: none; - background-color: rgb(255 255 255 / 0.32); - } - .aero .chat-bubble, - .aero .chat-answer { - background-image: none; - background-color: rgb(255 255 255 / 0.55); - } - /* The chat chrome and the rail keep one even, a little stronger tint on - touch: without the baked blur the flat fill has to carry the surface on - its own, or the photo underneath tints it unevenly again. */ - .aero .chat-header, - .aero .chat-empty, - .aero .chat-composer-band { - background-image: none; - background-color: rgb(255 255 255 / 0.55); - } - .aero .tabbar { - background-image: none; - } - /* The hand-rolled frost fills drop the bake too and keep their own tints. */ - .aero [class*="bg-white/35"], - .aero [class*="bg-white/40"], - .aero [class*="bg-white/45"], - .aero [class*="bg-white/50"], - .aero [class*="bg-white/55"], - .aero [class*="bg-white/60"], - .aero [class*="bg-white/70"], - .aero [class*="bg-[var(--surface)]"], - .aero [class*="bg-[var(--surface-strong)]"], - .aero [class*="bg-[var(--surface-sunken)]"], - .aero [class*="bg-frost-50"] { - background-image: none; - } -} - -/* Buttons need more presence than the cards: a stronger veil so a button reads - as pale glass over grass, sky or water — the "Ask pill" look — instead of - blending into the blurred scene. */ -.aero .btn-secondary, -.aero .glass-pill { - background-image: - linear-gradient( - 180deg, - rgb(255 255 255 / 0.52) 0%, - rgb(255 255 255 / 0.36) 46%, - rgb(255 255 255 / 0.32) 53%, - rgb(255 255 255 / 0.44) 100% - ), - url("/aero/background-blur.webp?v=4"); - background-size: 100% 100%, cover; - background-position: 0 0, center 42%; - background-attachment: scroll, fixed; - background-repeat: no-repeat, no-repeat; -} -.aero .btn-ghost { - background-image: - linear-gradient( - 180deg, - rgb(255 255 255 / 0.52) 0%, - rgb(255 255 255 / 0.36) 46%, - rgb(255 255 255 / 0.32) 53%, - rgb(255 255 255 / 0.44) 100% - ), - url("/aero/background-blur.webp?v=4"); - background-size: 100% 100%, cover; - background-position: 0 0, center 42%; - background-attachment: scroll, fixed; - background-repeat: no-repeat, no-repeat; - border: 1px solid rgb(255 255 255 / 0.6); - color: var(--text); -} - /* Chat chrome and the rail: one even frosted recipe. The pre-blurred, extra-soft wash of the wallpaper (background-glass) sits under a uniform white veil, so the surface colour cannot drift with the photo behind it — @@ -763,6 +665,105 @@ border-color: rgb(255 255 255 / 0.9); } +/* Touch devices: flat glass, no bake and no live blur. The baked layer is + `background-attachment: fixed`, which mobile engines repaint late (iOS + treats it as scroll) — panes showed a stale crop until a tap forced a + repaint. The honest alternative (a real backdrop-filter) is worse: every + pane re-blurs on every frame while the bubbles animate, and phones choke — + measured as input lag, "buttons take ages to respond". So on touch the + panes keep the thin Ask-pill tint over the sharp scene: transparent glass + at zero per-frame cost. Desktop keeps the baked blur. */ +@media (max-width: 639px), (pointer: coarse) { + .aero .card, + .aero .glass-solid, + .aero .sidebar-surface, + .aero .gate-card, + .aero .list-group, + .aero .nav-action, + .aero .nav-row[aria-current="page"], + .aero .nav-row[aria-pressed="true"], + .aero .nav-row[data-active="true"], + .aero .glass-chip, + .aero .glass-pill, + .aero .btn-secondary, + .aero .btn-ghost, + .aero .chat-header, + .aero .chat-empty, + .aero .chat-composer-band, + .aero .segmented { + background-image: none; + background-color: rgb(255 255 255 / 0.32); + } + /* Reading surfaces keep a touch more body than the chrome — long answers + have to stay comfortable over grass and water — but they are still glass, + not white panes. */ + .aero .chat-bubble, + .aero .chat-answer { + background-image: none; + background-color: rgb(255 255 255 / 0.42); + } + /* The composer's field: a lighter, even tile inside the panel. */ + .aero .chat-composer-band textarea, + .aero .chat-composer-band input[type="search"] { + background-image: none; + background-color: rgb(255 255 255 / 0.42); + } + .aero .tabbar { + background-image: none; + } + /* The hand-rolled frost fills drop the bake too and keep their own tints. */ + .aero [class*="bg-white/35"], + .aero [class*="bg-white/40"], + .aero [class*="bg-white/45"], + .aero [class*="bg-white/50"], + .aero [class*="bg-white/55"], + .aero [class*="bg-white/60"], + .aero [class*="bg-white/70"], + .aero [class*="bg-[var(--surface)]"], + .aero [class*="bg-[var(--surface-strong)]"], + .aero [class*="bg-[var(--surface-sunken)]"], + .aero [class*="bg-frost-50"] { + background-image: none; + } +} + +/* Buttons need more presence than the cards: a stronger veil so a button reads + as pale glass over grass, sky or water — the "Ask pill" look — instead of + blending into the blurred scene. */ +.aero .btn-secondary, +.aero .glass-pill { + background-image: + linear-gradient( + 180deg, + rgb(255 255 255 / 0.52) 0%, + rgb(255 255 255 / 0.36) 46%, + rgb(255 255 255 / 0.32) 53%, + rgb(255 255 255 / 0.44) 100% + ), + url("/aero/background-blur.webp?v=4"); + background-size: 100% 100%, cover; + background-position: 0 0, center 42%; + background-attachment: scroll, fixed; + background-repeat: no-repeat, no-repeat; +} +.aero .btn-ghost { + background-image: + linear-gradient( + 180deg, + rgb(255 255 255 / 0.52) 0%, + rgb(255 255 255 / 0.36) 46%, + rgb(255 255 255 / 0.32) 53%, + rgb(255 255 255 / 0.44) 100% + ), + url("/aero/background-blur.webp?v=4"); + background-size: 100% 100%, cover; + background-position: 0 0, center 42%; + background-attachment: scroll, fixed; + background-repeat: no-repeat, no-repeat; + border: 1px solid rgb(255 255 255 / 0.6); + color: var(--text); +} + /* Page headings that sit straight on the wallpaper. */ .aero .page-heading { display: inline-block; diff --git a/components/StudyFeed.tsx b/components/StudyFeed.tsx index 830ac40..d00ee8f 100644 --- a/components/StudyFeed.tsx +++ b/components/StudyFeed.tsx @@ -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(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. */}
-
+
+ {/* The countdown, drawn along the pill's lower edge: the chip alone + was easy to miss mid-card. */} + {timedRemaining !== null ? ( + + + + ) : null} - {timed && remaining !== null ? `${remaining}s` : `${TIMED_CARD_SECONDS}s`} + {timedRemaining !== null ? `${timedRemaining}s` : `${TIMED_CARD_SECONDS}s`} 0, String(afterFive)); + const bar = await page.locator("[data-timed-bar]").evaluate((element) => element.getBoundingClientRect().width); + const track = await page.locator("[data-timed-bar]").evaluate((element) => element.parentElement.getBoundingClientRect().width); + check("timed: the countdown is drawn along the chrome", bar > 10 && bar < track, `${Math.round(bar)}/${Math.round(track)}`); await page.clock.runFor(26_000); await page.waitForTimeout(300);