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:
parent
796e2e9fe9
commit
a8b3dfd0e3
6 changed files with 129 additions and 75 deletions
19
README.md
19
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
|
||||
|
|
|
|||
197
app/globals.css
197
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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "freezmo",
|
||||
"version": "0.6.1",
|
||||
"version": "0.6.2",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
*
|
||||
* Bumping CACHE_VERSION is how a new deploy invalidates old assets.
|
||||
*/
|
||||
const CACHE_VERSION = "freezmo-v83";
|
||||
const CACHE_VERSION = "freezmo-v84";
|
||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
||||
|
||||
|
|
|
|||
|
|
@ -174,6 +174,9 @@ const DECK = {
|
|||
await page.waitForTimeout(200);
|
||||
const afterFive = Number.parseInt((await toggle.textContent()) ?? "", 10);
|
||||
check("timed: the countdown ticks down", afterFive <= 26 && afterFive > 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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue