More shinier bubbles; the chat's top edge fades instead of clipping

Bubbles: ten -> eighteen (interleaved, 18px to 96px, 10-23s runs), with a
tighter specular hotspot, a reflected-light glint low on the sphere, a 1px
glass edge and a brighter inner rim on both the bubbles and the drifting
orbs. Bodies stay translucent with capped opacity, so wallpaper text stays
readable; bubble-perf shows no added frame cost.

Chat: the conversation scrolls up to the glass header, and a line clipped
exactly at that edge read as a rendering bug over the busy wallpaper - the
scroller now carries a 14px top mask in Aero, shorter than the column's
16px top padding so a fresh conversation shows no dimming.

Contrast 0 findings, axe clean, chat 101/101, mobile 123/123 (bubble count
pinned to 18), regression 44/44, i18n 13/13, native 33/33.
Version 0.3.7, SW -> freezmo-v66.
This commit is contained in:
Martin Dvořák 2026-10-13 13:19:12 +00:00
commit 4ba80a9bf2
7 changed files with 74 additions and 20 deletions

View file

@ -1411,6 +1411,19 @@ Things I chose while building, with the reasoning:
pre-blurred wallpaper — the scene is clearly visible through the glass, and the pre-blurred wallpaper — the scene is clearly visible through the glass, and the
pixel-contrast scan still passes. pixel-contrast scan still passes.
72. **More, shinier bubbles.** Aero's rising bubbles went from ten to eighteen (the extra eight
interleaved between the others, 18px to 96px, 10-23s runs) with a tighter specular hotspot, a
second reflected-light glint low on the sphere, a 1px glass edge and a brighter inner rim on
both the bubbles and the drifting orbs. The bodies stay translucent and their opacities are
capped (0.75-0.9), so text sitting on the wallpaper stays readable, and the bubble-perf
diagnostic shows no added frame cost (18 bubbles, same p95 as ten).
71. **The chat's top edge fades instead of hard-clipping.** The conversation scrolls right up to
the glass header, and over the busy wallpaper a line of text clipped exactly at that edge read
as a rendering bug ("the chat looks cut off"). The scroller now carries a 14px top mask in
Aero — shorter than the column's 16px top padding, so a fresh conversation at the top shows no
dimming at all.
70. **Chat answers are bubbles in Aero too.** The assistant answers are deliberately bubble-less 70. **Chat answers are bubbles in Aero too.** The assistant answers are deliberately bubble-less
(full-width text on the flat themes) and the user bubble's thin tint washed out over the bright (full-width text on the flat themes) and the user bubble's thin tint washed out over the bright
parts of the wallpaper, so the longest text in the app ended up sitting straight on the photo. parts of the wallpaper, so the longest text in the app ended up sitting straight on the photo.

View file

@ -276,14 +276,21 @@
/* The orbs become extra bubbles over the photo. */ /* The orbs become extra bubbles over the photo. */
.aero .ice-orb { .aero .ice-orb {
background: radial-gradient( background:
radial-gradient(circle at 30% 24%, rgb(255 255 255 / 0.95) 0%, rgb(255 255 255 / 0) 16%),
radial-gradient(circle at 72% 80%, rgb(255 255 255 / 0.4) 0%, rgb(255 255 255 / 0) 20%),
radial-gradient(
circle at 32% 28%, circle at 32% 28%,
rgb(255 255 255 / 0.95) 0%, rgb(255 255 255 / 0.9) 0%,
rgb(226 248 255 / 0.4) 22%, rgb(226 248 255 / 0.4) 22%,
rgb(125 211 252 / 0.14) 56%, rgb(125 211 252 / 0.14) 56%,
rgb(56 189 248 / 0.38) 100% rgb(56 189 248 / 0.38) 100%
); );
box-shadow: inset -8px -12px 20px -10px rgb(3 74 124 / 0.5), inset 8px 10px 16px -10px rgb(255 255 255 / 0.95), 0 12px 26px -16px rgb(3 74 124 / 0.55); box-shadow:
inset -8px -12px 20px -10px rgb(3 74 124 / 0.5),
inset 8px 10px 16px -10px rgb(255 255 255 / 1),
inset 0 0 0 1px rgb(255 255 255 / 0.35),
0 12px 26px -16px rgb(3 74 124 / 0.55);
} }
/* The Aero gloss for the floating surfaces (sheets, dropdowns and the sticky /* The Aero gloss for the floating surfaces (sheets, dropdowns and the sticky
@ -313,14 +320,22 @@
bottom: -14vh; bottom: -14vh;
border-radius: 50%; border-radius: 50%;
opacity: 0; opacity: 0;
background: radial-gradient( background:
radial-gradient(circle at 30% 24%, rgb(255 255 255 / 0.95) 0%, rgb(255 255 255 / 0) 16%),
radial-gradient(circle at 72% 80%, rgb(255 255 255 / 0.4) 0%, rgb(255 255 255 / 0) 20%),
radial-gradient(
circle at 32% 28%, circle at 32% 28%,
rgb(255 255 255 / 0.95) 0%, rgb(255 255 255 / 0.9) 0%,
rgb(226 248 255 / 0.38) 24%, rgb(226 248 255 / 0.38) 24%,
rgb(125 211 252 / 0.12) 58%, rgb(125 211 252 / 0.12) 58%,
rgb(56 189 248 / 0.34) 100% rgb(56 189 248 / 0.34) 100%
); );
box-shadow: inset -6px -10px 16px -8px rgb(3 74 124 / 0.45), inset 6px 8px 14px -8px rgb(255 255 255 / 0.9), 0 10px 22px -14px rgb(3 74 124 / 0.5); box-shadow:
inset -6px -10px 16px -8px rgb(3 74 124 / 0.5),
inset 6px 8px 14px -8px rgb(255 255 255 / 1),
inset 0 0 0 1px rgb(255 255 255 / 0.35),
0 10px 22px -14px rgb(3 74 124 / 0.5);
border: 1px solid rgb(255 255 255 / 0.25);
animation: aero-bubble-rise var(--bubble-duration, 20s) linear infinite; animation: aero-bubble-rise var(--bubble-duration, 20s) linear infinite;
animation-delay: var(--bubble-delay, 0s); animation-delay: var(--bubble-delay, 0s);
will-change: transform, opacity; will-change: transform, opacity;
@ -347,6 +362,14 @@
.aero .ice-bubble-8 { width: 46px; height: 46px; left: 78%; --bubble-duration: 16s; --bubble-delay: -13s; --bubble-sway: 4vw; } .aero .ice-bubble-8 { width: 46px; height: 46px; left: 78%; --bubble-duration: 16s; --bubble-delay: -13s; --bubble-sway: 4vw; }
.aero .ice-bubble-9 { width: 24px; height: 24px; left: 87%; --bubble-duration: 14s; --bubble-delay: -6s; --bubble-sway: -3vw; } .aero .ice-bubble-9 { width: 24px; height: 24px; left: 87%; --bubble-duration: 14s; --bubble-delay: -6s; --bubble-sway: -3vw; }
.aero .ice-bubble-10 { width: 120px; height: 120px; left: 92%; --bubble-duration: 27s; --bubble-delay: -20s; --bubble-sway: -5vw; --bubble-opacity: 0.75; } .aero .ice-bubble-10 { width: 120px; height: 120px; left: 92%; --bubble-duration: 27s; --bubble-delay: -20s; --bubble-sway: -5vw; --bubble-opacity: 0.75; }
.aero .ice-bubble-11 { width: 18px; height: 18px; left: 4%; --bubble-duration: 12s; --bubble-delay: -3s; --bubble-sway: 3vw; }
.aero .ice-bubble-12 { width: 58px; height: 58px; left: 12%; --bubble-duration: 18s; --bubble-delay: -11s; --bubble-sway: -3vw; }
.aero .ice-bubble-13 { width: 30px; height: 30px; left: 21%; --bubble-duration: 15s; --bubble-delay: -5s; --bubble-sway: -2vw; }
.aero .ice-bubble-14 { width: 96px; height: 96px; left: 34%; --bubble-duration: 23s; --bubble-delay: -19s; --bubble-sway: 4vw; --bubble-opacity: 0.8; }
.aero .ice-bubble-15 { width: 22px; height: 22px; left: 42%; --bubble-duration: 10s; --bubble-delay: -8s; --bubble-sway: -3vw; }
.aero .ice-bubble-16 { width: 44px; height: 44px; left: 54%; --bubble-duration: 16s; --bubble-delay: -2s; --bubble-sway: 2vw; }
.aero .ice-bubble-17 { width: 62px; height: 62px; left: 62%; --bubble-duration: 21s; --bubble-delay: -14s; --bubble-sway: -4vw; }
.aero .ice-bubble-18 { width: 26px; height: 26px; left: 74%; --bubble-duration: 13s; --bubble-delay: -6s; --bubble-sway: 3vw; }
/* Helper lines that sit straight on the wallpaper (the keyboard tip, the AI /* Helper lines that sit straight on the wallpaper (the keyboard tip, the AI
note) need their own surface in Aero: dark text on the grass measured note) need their own surface in Aero: dark text on the grass measured
@ -516,6 +539,16 @@
box-shadow: inset 0 2px 8px rgb(2 62 110 / 0.2), inset 0 1px 0 rgb(255 255 255 / 0.8); box-shadow: inset 0 2px 8px rgb(2 62 110 / 0.2), inset 0 1px 0 rgb(255 255 255 / 0.8);
} }
/* The conversation scrolls right up to the glass header; a hard-clipped line
of text at that edge reads as a rendering bug over the busy wallpaper, so
the top of the scroll area fades out instead (the mask is fixed to the
element, not the content — and stays inside the column's 16px top padding,
so at the very top nothing is dimmed). */
.aero .chat-scroll {
mask-image: linear-gradient(to bottom, transparent 0, black 14px);
-webkit-mask-image: linear-gradient(to bottom, transparent 0, black 14px);
}
/* Chat: the longest text in the app sits straight on the wallpaper. The /* Chat: the longest text in the app sits straight on the wallpaper. The
assistant answers are deliberately bubble-less in the flat themes, and the assistant answers are deliberately bubble-less in the flat themes, and the
user bubble's thin tint washes out over the bright parts of the scene — in user bubble's thin tint washes out over the bright parts of the scene — in

View file

@ -43,6 +43,14 @@ export function IceBackdrop() {
<span className="ice-bubble ice-bubble-8" /> <span className="ice-bubble ice-bubble-8" />
<span className="ice-bubble ice-bubble-9" /> <span className="ice-bubble ice-bubble-9" />
<span className="ice-bubble ice-bubble-10" /> <span className="ice-bubble ice-bubble-10" />
<span className="ice-bubble ice-bubble-11" />
<span className="ice-bubble ice-bubble-12" />
<span className="ice-bubble ice-bubble-13" />
<span className="ice-bubble ice-bubble-14" />
<span className="ice-bubble ice-bubble-15" />
<span className="ice-bubble ice-bubble-16" />
<span className="ice-bubble ice-bubble-17" />
<span className="ice-bubble ice-bubble-18" />
<span className="ice-grain" /> <span className="ice-grain" />
</div> </div>
); );

View file

@ -569,7 +569,7 @@ export function ChatScreen() {
</header> </header>
<div className="relative min-h-0 flex-1"> <div className="relative min-h-0 flex-1">
<div ref={scrollRef} onScroll={onScroll} className="h-full overflow-y-auto overscroll-contain"> <div ref={scrollRef} onScroll={onScroll} className="chat-scroll h-full overflow-y-auto overscroll-contain">
<div className="mx-auto w-full max-w-3xl space-y-4 px-4 py-4"> <div className="mx-auto w-full max-w-3xl space-y-4 px-4 py-4">
{messages.length === 0 ? ( {messages.length === 0 ? (
<div className="space-y-5 py-6"> <div className="space-y-5 py-6">

View file

@ -1,6 +1,6 @@
{ {
"name": "freezmo", "name": "freezmo",
"version": "0.3.5", "version": "0.3.7",
"private": true, "private": true,
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {

View file

@ -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-v65"; const CACHE_VERSION = "freezmo-v66";
const SHELL_CACHE = `${CACHE_VERSION}-shell`; const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const ASSET_CACHE = `${CACHE_VERSION}-assets`; const ASSET_CACHE = `${CACHE_VERSION}-assets`;

View file

@ -245,7 +245,7 @@ const AXE = axeSource();
); );
check( check(
"aero: bubbles rise", "aero: bubbles rise",
(await page.locator(".ice-bubble").count()) === 10 && (await page.locator(".ice-bubble").count()) === 18 &&
(await page.locator(".ice-bubble").first().evaluate((el) => getComputedStyle(el).animationName)) === (await page.locator(".ice-bubble").first().evaluate((el) => getComputedStyle(el).animationName)) ===
"aero-bubble-rise", "aero-bubble-rise",
); );