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
|
(measured: 268..1280 at a 1280 window) — a flat pane floating 40px inside the scene read as
|
||||||
"cut off next to the sidebar".
|
"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
|
92. **Study cards: where the answer came from, read aloud, and timed runs.** Three additions to
|
||||||
the study feed, all local and dependency-free.
|
the study feed, all local and dependency-free.
|
||||||
- **From your notes.** The generator now returns `source` with every activity: the verbatim
|
- **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;
|
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,
|
/* Chat chrome and the rail: one even frosted recipe. The pre-blurred,
|
||||||
extra-soft wash of the wallpaper (background-glass) sits under a uniform
|
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 —
|
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);
|
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. */
|
/* Page headings that sit straight on the wallpaper. */
|
||||||
.aero .page-heading {
|
.aero .page-heading {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,9 @@ export function StudyFeed({ deck }: { deck: Deck }) {
|
||||||
/* Timed runs ---------------------------------------------------------- */
|
/* Timed runs ---------------------------------------------------------- */
|
||||||
const timedActivity = index < total ? queue[index] : null;
|
const timedActivity = index < total ? queue[index] : null;
|
||||||
const timedAnswered = timedActivity ? Boolean(results[timedActivity.id]) : true;
|
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(() => {
|
useEffect(() => {
|
||||||
if (!timed || !timedActivity || timedAnswered) {
|
if (!timed || !timedActivity || timedAnswered) {
|
||||||
|
|
@ -188,6 +191,15 @@ export function StudyFeed({ deck }: { deck: Deck }) {
|
||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
}, [timed, timedActivity, timedAnswered, runId]);
|
}, [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
|
/* 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
|
history and the SRS schedule see it), the learner is told, and the feed
|
||||||
moves on. */
|
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
|
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. */}
|
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">
|
<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
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
aria-label={t("feed.back")}
|
aria-label={t("feed.back")}
|
||||||
|
|
@ -328,12 +357,14 @@ export function StudyFeed({ deck }: { deck: Deck }) {
|
||||||
aria-label={timed ? t("feed.timedOn") : t("feed.timed")}
|
aria-label={timed ? t("feed.timedOn") : t("feed.timed")}
|
||||||
title={timed ? t("feed.timedOn") : t("feed.timed")}
|
title={timed ? t("feed.timedOn") : t("feed.timed")}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-extrabold tabular-nums transition",
|
"inline-flex items-center gap-1 rounded-full border 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)]",
|
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" />
|
<ClockIcon className="size-3.5" />
|
||||||
{timed && remaining !== null ? `${remaining}s` : `${TIMED_CARD_SECONDS}s`}
|
{timedRemaining !== null ? `${timedRemaining}s` : `${TIMED_CARD_SECONDS}s`}
|
||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "freezmo",
|
"name": "freezmo",
|
||||||
"version": "0.6.1",
|
"version": "0.6.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
*
|
*
|
||||||
* Bumping CACHE_VERSION is how a new deploy invalidates old assets.
|
* 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 SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||||
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,9 @@ const DECK = {
|
||||||
await page.waitForTimeout(200);
|
await page.waitForTimeout(200);
|
||||||
const afterFive = Number.parseInt((await toggle.textContent()) ?? "", 10);
|
const afterFive = Number.parseInt((await toggle.textContent()) ?? "", 10);
|
||||||
check("timed: the countdown ticks down", afterFive <= 26 && afterFive > 0, String(afterFive));
|
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.clock.runFor(26_000);
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue