Thinner glass everywhere, chat answers as bubbles in Aero
Two asks. "Slightly more transparent": the veils drop another notch (recipe 0.15 -> 0.12, tokens ~0.03 less, tab bar / wizard bar / gloss likewise). "Some buttons aren't themed": the recipe now catches the flows' hand-rolled frost by attribute selectors - the study activities' bg-white/xx chips (multiple choice, matching, ordering, fill blank), the exam rows that used bg-[var(--surface)] (an opaque white in Aero), and everything on the surface tokens (chat chips, homework chips, inputs, dropzones). Inside sheets the same shapes stay flat; the dropzones lost their press-scale (a transform re-anchors the fixed backdrop) and shade inward instead. And the chat: the assistant answers are bubble-less by design and the user bubble's thin tint washed out over the bright wallpaper, so the longest text in the app sat straight on the photo. In Aero both roles get a real pane now - chat-answer wraps the markdown in a bubble, the user turn carries chat-bubble, with a stronger veil than the shared glass for comfortable reading. Light and dark keep the bubble-less assistant design (styled only under .aero). Contrast: 0 below-minimum findings, 0 axe violations. Chat 101/101, mobile 123/123, regression 44/44, i18n 13/13, native 33/33. Version 0.3.5, SW -> freezmo-v65.
This commit is contained in:
parent
70ddc5e753
commit
5eec310e16
5 changed files with 102 additions and 25 deletions
20
README.md
20
README.md
|
|
@ -1411,6 +1411,26 @@ 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.
|
||||||
|
|
||||||
|
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
|
||||||
|
parts of the wallpaper, so the longest text in the app ended up sitting straight on the photo.
|
||||||
|
In Aero both get a real pane: `.chat-answer` wraps the markdown in a bubble (plus a
|
||||||
|
`chat-bubble` class on the user turn), with a stronger veil than the shared glass so a wall of
|
||||||
|
text still reads comfortably over the same blurred scene, and the bright rim. Light and dark
|
||||||
|
keep the bubble-less assistant design — these classes are styled only under `.aero`.
|
||||||
|
|
||||||
|
69. **The glass got thinner and reaches the whole app.** Two asks in one pass: "slightly more
|
||||||
|
transparent" and "some buttons aren't themed" while doing decks/homework. The veils drop
|
||||||
|
another notch (recipe 0.15 -> 0.12, the tokens ~0.03 less, the tab bar / wizard bar / gloss
|
||||||
|
likewise), and the recipe now catches the flows' hand-rolled frost: the study activities'
|
||||||
|
`bg-white/xx` chips (multiple-choice options, matching terms, ordering cards, fill-in-blank
|
||||||
|
chips), the exam deck rows and pickers that used `bg-[var(--surface)]` — an *opaque* white in
|
||||||
|
Aero — and everything using the surface tokens (chat action chips, homework chips, inputs,
|
||||||
|
dropzones); attribute selectors match them, so no component needed a new class. The same shapes
|
||||||
|
inside sheets stay flat (the exclusion list covers the new selectors — there the sheet itself is
|
||||||
|
the glass), and the dropzones lost their press-scale (a transform on a fixed backdrop
|
||||||
|
re-anchors it) and shade inward instead. `tests/contrast.mjs`: 0 findings.
|
||||||
|
|
||||||
68. **The Ask pill joins the glass, and Aero's hovers light the pane instead of moving it.** Two
|
68. **The Ask pill joins the glass, and Aero's hovers light the pane instead of moving it.** Two
|
||||||
leftovers from the previous pass. The Ask button (and the two phone icon pills that share its
|
leftovers from the previous pass. The Ask button (and the two phone icon pills that share its
|
||||||
styling) was a hand-styled `bg-[var(--surface-strong)]` pill that never picked up the frosted
|
styling) was a hand-styled `bg-[var(--surface-strong)]` pill that never picked up the frosted
|
||||||
|
|
|
||||||
|
|
@ -295,10 +295,10 @@
|
||||||
.aero .glass {
|
.aero .glass {
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
rgb(255 255 255 / 0.32) 0%,
|
rgb(255 255 255 / 0.28) 0%,
|
||||||
rgb(255 255 255 / 0.1) 46%,
|
rgb(255 255 255 / 0.08) 46%,
|
||||||
rgb(255 255 255 / 0.03) 47%,
|
rgb(255 255 255 / 0.03) 47%,
|
||||||
rgb(255 255 255 / 0.1) 100%
|
rgb(255 255 255 / 0.08) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -386,12 +386,12 @@
|
||||||
stay thin too. Contrast is pixel-scanned in all three themes, so the tints
|
stay thin too. Contrast is pixel-scanned in all three themes, so the tints
|
||||||
cannot drop below what the text needs. */
|
cannot drop below what the text needs. */
|
||||||
.aero {
|
.aero {
|
||||||
--glass-bg: rgb(228 246 255 / 0.26);
|
--glass-bg: rgb(228 246 255 / 0.22);
|
||||||
--glass-solid-bg: rgb(232 248 255 / 0.34);
|
--glass-solid-bg: rgb(232 248 255 / 0.3);
|
||||||
--glass-sheet-bg: rgb(232 248 255 / 0.68);
|
--glass-sheet-bg: rgb(232 248 255 / 0.64);
|
||||||
--surface-strong: rgb(255 255 255 / 0.26);
|
--surface-strong: rgb(255 255 255 / 0.23);
|
||||||
--surface-sunken: rgb(255 255 255 / 0.2);
|
--surface-sunken: rgb(255 255 255 / 0.17);
|
||||||
--nav-active-bg: rgb(255 255 255 / 0.3);
|
--nav-active-bg: rgb(255 255 255 / 0.27);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Liquid glass — one recipe for every surface, so the rail, the cards, the
|
/* Liquid glass — one recipe for every surface, so the rail, the cards, the
|
||||||
|
|
@ -418,16 +418,29 @@
|
||||||
.aero .grad-ice,
|
.aero .grad-ice,
|
||||||
.aero .segmented,
|
.aero .segmented,
|
||||||
.aero .segmented > button[aria-pressed="true"],
|
.aero .segmented > button[aria-pressed="true"],
|
||||||
.aero .segmented > a[aria-current="page"] {
|
.aero .segmented > a[aria-current="page"],
|
||||||
|
/* Hand-rolled frost fills in the study, exam, chat and homework flows:
|
||||||
|
white/xx utilities and the surface tokens — without this they render as
|
||||||
|
flat white chips next to the glass. */
|
||||||
|
.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-frost-50"] {
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
rgb(255 255 255 / 0.16) 0%,
|
rgb(255 255 255 / 0.13) 0%,
|
||||||
rgb(255 255 255 / 0.05) 46%,
|
rgb(255 255 255 / 0.04) 46%,
|
||||||
rgb(255 255 255 / 0.04) 53%,
|
rgb(255 255 255 / 0.03) 53%,
|
||||||
rgb(255 255 255 / 0.09) 100%
|
rgb(255 255 255 / 0.07) 100%
|
||||||
),
|
),
|
||||||
linear-gradient(rgb(255 255 255 / 0.15), rgb(255 255 255 / 0.15)),
|
linear-gradient(rgb(255 255 255 / 0.12), rgb(255 255 255 / 0.12)),
|
||||||
url("/aero/background-blur.webp?v=3");
|
url("/aero/background-blur.webp?v=3");
|
||||||
background-size: 100% 100%, 100% 100%, cover;
|
background-size: 100% 100%, 100% 100%, cover;
|
||||||
background-position: 0 0, 0 0, center 42%;
|
background-position: 0 0, 0 0, center 42%;
|
||||||
|
|
@ -439,7 +452,7 @@
|
||||||
blue, which read as a heavy slab over the photo — give it the same thin
|
blue, which read as a heavy slab over the photo — give it the same thin
|
||||||
glass as everything else (it already has a real backdrop blur). */
|
glass as everything else (it already has a real backdrop blur). */
|
||||||
.aero [data-wizard-bar] {
|
.aero [data-wizard-bar] {
|
||||||
background: rgb(255 255 255 / 0.24);
|
background: rgb(255 255 255 / 0.2);
|
||||||
border-top-color: rgb(255 255 255 / 0.6);
|
border-top-color: rgb(255 255 255 / 0.6);
|
||||||
box-shadow: 0 -14px 34px -22px rgb(2 62 110 / 0.5);
|
box-shadow: 0 -14px 34px -22px rgb(2 62 110 / 0.5);
|
||||||
}
|
}
|
||||||
|
|
@ -484,6 +497,7 @@
|
||||||
}
|
}
|
||||||
.aero .card-hover:active,
|
.aero .card-hover:active,
|
||||||
.aero .card-hover:hover,
|
.aero .card-hover:hover,
|
||||||
|
.aero .press:active,
|
||||||
.aero .btn-primary:active,
|
.aero .btn-primary:active,
|
||||||
.aero .segmented > button:active,
|
.aero .segmented > button:active,
|
||||||
.aero .segmented > a:active,
|
.aero .segmented > a:active,
|
||||||
|
|
@ -494,6 +508,7 @@
|
||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
.aero .card-hover:active,
|
.aero .card-hover:active,
|
||||||
|
.aero .press:active,
|
||||||
.aero .btn-primary:active,
|
.aero .btn-primary:active,
|
||||||
.aero .nav-action:active,
|
.aero .nav-action:active,
|
||||||
.aero .glass-pill:active,
|
.aero .glass-pill:active,
|
||||||
|
|
@ -501,16 +516,46 @@
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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
|
||||||
|
user bubble's thin tint washes out over the bright parts of the scene — in
|
||||||
|
Aero both get a real pane (a stronger veil than the shared glass, so an
|
||||||
|
answer reads as its own bubble). */
|
||||||
|
.aero .chat-bubble,
|
||||||
|
.aero .chat-answer {
|
||||||
|
border-color: rgb(255 255 255 / 0.7);
|
||||||
|
background-image:
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgb(255 255 255 / 0.6) 0%,
|
||||||
|
rgb(255 255 255 / 0.44) 46%,
|
||||||
|
rgb(255 255 255 / 0.4) 53%,
|
||||||
|
rgb(255 255 255 / 0.52) 100%
|
||||||
|
),
|
||||||
|
url("/aero/background-blur.webp?v=3");
|
||||||
|
background-size: 100% 100%, cover;
|
||||||
|
background-position: 0 0, center 42%;
|
||||||
|
background-attachment: scroll, fixed;
|
||||||
|
background-repeat: no-repeat, no-repeat;
|
||||||
|
box-shadow: 0 16px 34px -22px rgb(2 62 110 / 0.45), inset 0 1px 0 rgb(255 255 255 / 0.9);
|
||||||
|
}
|
||||||
|
.aero .chat-answer {
|
||||||
|
border: 1px solid rgb(255 255 255 / 0.7);
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
border-bottom-left-radius: 0.6rem;
|
||||||
|
padding: 0.7rem 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* The phone tab bar carries its labels right at the bottom of the water, so
|
/* The phone tab bar carries its labels right at the bottom of the water, so
|
||||||
it keeps a little more veil than the cards. */
|
it keeps a little more veil than the cards. */
|
||||||
.aero .tabbar {
|
.aero .tabbar {
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
rgb(255 255 255 / 0.26) 0%,
|
rgb(255 255 255 / 0.22) 0%,
|
||||||
rgb(255 255 255 / 0.12) 46%,
|
rgb(255 255 255 / 0.1) 46%,
|
||||||
rgb(255 255 255 / 0.1) 53%,
|
rgb(255 255 255 / 0.08) 53%,
|
||||||
rgb(255 255 255 / 0.16) 100%
|
rgb(255 255 255 / 0.13) 100%
|
||||||
),
|
),
|
||||||
url("/aero/background-blur.webp?v=3");
|
url("/aero/background-blur.webp?v=3");
|
||||||
background-size: 100% 100%, cover;
|
background-size: 100% 100%, cover;
|
||||||
|
|
@ -533,7 +578,17 @@
|
||||||
.aero .glass-sheet .segmented > a[aria-current="page"],
|
.aero .glass-sheet .segmented > a[aria-current="page"],
|
||||||
.aero .glass-sheet .btn-secondary,
|
.aero .glass-sheet .btn-secondary,
|
||||||
.aero .glass-sheet .btn-primary,
|
.aero .glass-sheet .btn-primary,
|
||||||
.aero .glass-sheet .grad-ice {
|
.aero .glass-sheet .grad-ice,
|
||||||
|
.aero .glass-sheet [class*="bg-white/35"],
|
||||||
|
.aero .glass-sheet [class*="bg-white/40"],
|
||||||
|
.aero .glass-sheet [class*="bg-white/45"],
|
||||||
|
.aero .glass-sheet [class*="bg-white/50"],
|
||||||
|
.aero .glass-sheet [class*="bg-white/55"],
|
||||||
|
.aero .glass-sheet [class*="bg-white/60"],
|
||||||
|
.aero .glass-sheet [class*="bg-white/70"],
|
||||||
|
.aero .glass-sheet [class*="bg-[var(--surface)]"],
|
||||||
|
.aero .glass-sheet [class*="bg-[var(--surface-strong)]"],
|
||||||
|
.aero .glass-sheet [class*="bg-frost-50"] {
|
||||||
background-image: none;
|
background-image: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export function MessageRow({
|
||||||
if (message.role === "user") {
|
if (message.role === "user") {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-end gap-1">
|
<div className="flex flex-col items-end gap-1">
|
||||||
<div className="max-w-[88%] rounded-3xl rounded-br-lg border border-[var(--glass-border)] bg-[var(--surface-strong)] px-3.5 py-2.5">
|
<div className="chat-bubble max-w-[88%] rounded-3xl rounded-br-lg border border-[var(--glass-border)] bg-[var(--surface-strong)] px-3.5 py-2.5">
|
||||||
<p className="whitespace-pre-wrap break-words text-[15px] leading-relaxed text-[var(--text)]">{message.content}</p>
|
<p className="whitespace-pre-wrap break-words text-[15px] leading-relaxed text-[var(--text)]">{message.content}</p>
|
||||||
{message.attachments && message.attachments.length > 0 ? (
|
{message.attachments && message.attachments.length > 0 ? (
|
||||||
<p className="mt-1.5 text-[11.5px] text-[var(--text-subtle)]">
|
<p className="mt-1.5 text-[11.5px] text-[var(--text-subtle)]">
|
||||||
|
|
@ -69,7 +69,9 @@ export function MessageRow({
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{message.thinking ? <ThinkingRow thinking={message.thinking} streaming={streaming && !message.content} /> : null}
|
{message.thinking ? <ThinkingRow thinking={message.thinking} streaming={streaming && !message.content} /> : null}
|
||||||
{message.content ? (
|
{message.content ? (
|
||||||
|
<div className="chat-answer">
|
||||||
<Markdown text={message.content} />
|
<Markdown text={message.content} />
|
||||||
|
</div>
|
||||||
) : streaming ? (
|
) : streaming ? (
|
||||||
<span className="flex items-center gap-1 py-1" role="status" aria-label={t("chat.thinking")}>
|
<span className="flex items-center gap-1 py-1" role="status" aria-label={t("chat.thinking")}>
|
||||||
{[0, 1, 2].map((dot) => (
|
{[0, 1, 2].map((dot) => (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "freezmo",
|
"name": "freezmo",
|
||||||
"version": "0.3.3",
|
"version": "0.3.5",
|
||||||
"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-v64";
|
const CACHE_VERSION = "freezmo-v65";
|
||||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||||
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue