Chat chrome docks into the rail; drop the notes-off banner

The chat header and composer are full-bleed bands inside the main
pane, which starts right at the sidebar on desktop - so both ended in
a hard vertical edge beside the floating rail card (the 12px gutter
showed the raw scene and the bands' fill + backdrop blur read as a
color step at the seam), with their hairlines starting in mid-air at
that edge. Both bands now reach the card's right edge (the rail's own
12px padding, expanded or collapsed) and their hairlines are inset to
the content box; phones keep the plain full-width borders. The chat
suite pins the dock. The "Chat is set to ignore notes text" banner
went away - the scope chip already says what the chat may read.

Version 0.5.5, SW -> freezmo-v77. Chat 104/104 (2 new), mobile
126/126, wizard 95/95, phase3 31/31, bubbles 34/34, i18n 13/13,
native 33/33, regression 44/44, contrast 0 findings, axe clean.
Full e2e: 14/14 suites.
This commit is contained in:
Martin Dvořák 2026-10-14 11:46:34 +00:00
commit a5d30a171c
8 changed files with 124 additions and 20 deletions

View file

@ -1419,9 +1419,20 @@ Things I chose while building, with the reasoning:
chips, input, chips, note — shares one left edge.
In Aero the welcome panel is the same frosted glass as the cards (blurred, not a flat tint) and
stretches flush with the header and the composer (measured: 268..1280 at a 1280 window, exactly
the chrome's edges) — a flat pane floating 40px inside the scene read as "cut off next to the
sidebar".
stretches to the content column's edges — flush under the header and above the composer
(measured: 268..1280 at a 1280 window) — a flat pane floating 40px inside the scene read as
"cut off next to the sidebar".
87. **Chat's chrome docks into the rail (the sidebar seam).** The chat header and composer are
full-bleed bands inside the main pane; at desktop widths that pane starts right at the sidebar,
so both bands ended in a hard vertical edge beside the floating rail card — the 12px gutter
showed the raw scene, and the bands' fill + backdrop blur made it read as a color step at the
seam, with their hairlines starting in mid-air at that edge. Both bands now reach the card's
right edge (the rail's own 12px padding, expanded or collapsed) and their hairlines are inset
to the content box instead of touching the seam; phones keep the plain full-width borders. The
chat suite pins the dock (band edge === card edge, zeroed band borders, inset `::after`
hairlines). The "Chat is set to ignore notes text" banner went away with it — the scope chip
already says what the chat may read.
86. **Reduced motion no longer ghosts the sheets.** The in-app toggle fed framer through
`MotionConfig reducedMotion="always"`, which only strips *transform* animations — while every

View file

@ -690,11 +690,15 @@
}
/* Chat header: the same glass as the cards, a touch more fill — its title and
scope line sit directly over the sky. */
scope line sit directly over the sky. At lg its divider is the inset hairline
the desktop docking block below draws (::after), so it never hits the seam. */
.aero .chat-header {
background-color: rgb(232 248 255 / 0.4);
border-bottom: 1px solid rgb(255 255 255 / 0.6);
}
.aero .chat-header::after {
background: rgb(255 255 255 / 0.6);
}
/* The chat composer: one flat frosted panel for the input, the chips and the
note — instead of each of them floating on the wallpaper. */
@ -703,6 +707,56 @@
border-top: 1px solid rgb(255 255 255 / 0.65);
box-shadow: 0 -16px 36px -26px rgb(2 62 110 / 0.5);
}
.aero .chat-composer::after {
background: rgb(255 255 255 / 0.65);
}
/* Desktop: the chat chrome docks into the rail. The header and the composer
are full-bleed bands inside the main pane; at lg that pane starts exactly
at the sidebar, so both bands used to stop in a hard vertical edge beside
the floating rail card — the scene showed through the 12px gutter and the
bands' own fill/blur made it read as a color step at the seam. They now
reach the card's right edge (the rail's own 12px padding, in the expanded
and the collapsed state alike) and their hairlines span the content box
instead of starting in mid-air at the seam. */
@media (min-width: 1024px) {
.chat-header {
margin-left: -0.75rem;
padding-left: 1.75rem;
border-bottom: 0;
}
.aero .chat-header {
border-bottom: 0;
}
.chat-header::after {
content: "";
position: absolute;
left: 1.75rem;
right: 1rem;
bottom: 0;
height: 1px;
background: var(--glass-border);
}
.chat-composer {
margin-left: -0.75rem;
}
.chat-composer-band {
padding-left: 3.25rem;
border-top: 0;
}
.aero .chat-composer {
border-top: 0;
}
.chat-composer::after {
content: "";
position: absolute;
left: 3.25rem;
right: 2.5rem;
top: 0;
height: 1px;
background: var(--glass-border);
}
}
/* The chat welcome (subtitle + suggestions): the same frosted glass as the
cards (the shared recipe above), stretched flush with the header and the

View file

@ -3,7 +3,6 @@
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import Link from "next/link";
import { Composer, type PendingAttachment } from "./Composer";
import { ContextPicker } from "./ContextPicker";
@ -769,14 +768,6 @@ export function ChatScreen() {
/>
</div>
{settings.chatNotes === false && scope.kind !== "none" ? (
<p className="border-t border-[var(--hairline-soft)] bg-[var(--surface-sunken)] px-4 py-2 text-[11.5px] text-[var(--text-subtle)]">
{t("chat.notesOffNote")}{" "}
<Link href="/" className="underline decoration-dotted">
{t("chat.notesOffLink")}
</Link>
</p>
) : null}
</div>
<HistorySheet

View file

@ -70,7 +70,7 @@ export function Composer({
const canSend = online && !busy && value.trim().length > 0;
return (
<div className="border-t border-[var(--glass-border)] bg-[var(--glass-bg)] px-4 pb-safe pt-2 backdrop-blur-xl lg:px-10">
<div className="chat-composer-band border-t border-[var(--glass-border)] bg-[var(--glass-bg)] px-4 pb-safe pt-2 backdrop-blur-xl lg:px-10">
{attachments.length > 0 ? (
<ul className="mb-2 flex flex-wrap gap-1.5">
{attachments.map((entry, index) => (

View file

@ -945,8 +945,6 @@ const en: Record<string, string> = {
"chat.emptyHint": "Answers come from your own decks. Pick what the chat may read with the context chip below.",
"chat.oldDecksNote": "Older decks only have the generated study content. Re-add the notes for better answers.",
"chat.fromLabel": "From",
"chat.notesOffNote": "Chat is set to ignore notes text.",
"chat.notesOffLink": "Turn it back on in Settings",
"chat.attach": "Attach photos or documents",
"chat.attachPhoto": "photo",
"chat.attachDoc": "document",
@ -1931,8 +1929,6 @@ const cs: Record<string, string> = {
"chat.emptyHint": "Odpovědi vycházejí z tvých sad. Pomocí chipu s kontextem níže vyber, co smí chat číst.",
"chat.oldDecksNote": "Starší sady mají jen vygenerovaný obsah. Přidej znovu poznámky a odpovědi budou lepší.",
"chat.fromLabel": "Z",
"chat.notesOffNote": "Chat má vypnuté používání textu poznámek.",
"chat.notesOffLink": "Zapnout v nastavení",
"chat.attach": "Přiložit fotky nebo dokumenty",
"chat.attachPhoto": "fotka",
"chat.attachDoc": "dokument",

View file

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

View file

@ -9,7 +9,7 @@
*
* Bumping CACHE_VERSION is how a new deploy invalidates old assets.
*/
const CACHE_VERSION = "freezmo-v76";
const CACHE_VERSION = "freezmo-v77";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const ASSET_CACHE = `${CACHE_VERSION}-assets`;

View file

@ -291,6 +291,58 @@ try {
await desktop.page.waitForTimeout(400);
check("desktop: AI disclaimer under the composer", await desktop.page.getByText("Freezmo uses AI and can make mistakes").isVisible());
/* The chat chrome docks into the rail: the header and the composer used to
end in a hard vertical edge beside the floating rail card (the scene
showed through the gutter and their fill/blur made it read as a color
step at the seam), and their hairlines started right at that edge. Both
bands now reach the card's right edge and their hairlines are inset to
the content box. */
const seam = await desktop.page.evaluate(() => {
const card = document.querySelector(".sidebar-surface");
const header = document.querySelector(".chat-header");
const band = document.querySelector(".chat-composer-band");
if (!card || !header || !band) return null;
const line = (el) => {
const cs = getComputedStyle(el, "::after");
return { content: cs.content, left: parseFloat(cs.left), right: parseFloat(cs.right) };
};
return {
cardRight: Math.round(card.getBoundingClientRect().right),
headerLeft: Math.round(header.getBoundingClientRect().left),
bandLeft: Math.round(band.getBoundingClientRect().left),
headerBorder: getComputedStyle(header).borderBottomWidth,
bandBorder: getComputedStyle(band).borderTopWidth,
headerLine: line(header),
bandLine: line(band.parentElement),
};
});
check(
"desktop: chat chrome docks into the rail (no seam)",
seam !== null &&
seam.headerLeft === seam.cardRight &&
seam.bandLeft === seam.cardRight &&
seam.headerBorder === "0px" &&
seam.bandBorder === "0px" &&
seam.headerLine.content !== "none" &&
seam.headerLine.left > 0 &&
seam.bandLine.content !== "none" &&
seam.bandLine.left > 0,
JSON.stringify(seam),
);
/* The notes-off banner is gone: with "keep notes text" off, chat no longer
nags at the bottom — the scope chip already says what it reads. */
const quiet = await makePage(browser, { width: 1280, height: 900, deviceScaleFactor: 1 });
await seed(quiet.page, {
decks: [DECK_NOTES],
extra: { "freezmo.settings.v1": JSON.stringify({ apiKey: "test-key", chatNotes: false }) },
});
await quiet.page.goto(`${APP}/chat`, { waitUntil: "networkidle" });
await quiet.page.waitForTimeout(900);
const quietText = await quiet.page.evaluate(() => document.body.innerText);
check("desktop: no notes-off banner with chat notes off", !/ignore notes text|ignoruje/i.test(quietText));
await quiet.context.close();
/* 6b. desktop pickers: the chip that opens also closes ---------------
(It used to only ever open, so the only way out was a choice.) */
const modelChip = () => desktop.page.getByRole("button", { name: /Stub (Text|Vision)|Chat model/ }).first();