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:
parent
7da308fa1e
commit
a5d30a171c
8 changed files with 124 additions and 20 deletions
17
README.md
17
README.md
|
|
@ -1419,9 +1419,20 @@ Things I chose while building, with the reasoning:
|
||||||
chips, input, chips, note — shares one left edge.
|
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
|
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
|
stretches to the content column's edges — flush under the header and above the composer
|
||||||
the chrome's edges) — a flat pane floating 40px inside the scene read as "cut off next to the
|
(measured: 268..1280 at a 1280 window) — a flat pane floating 40px inside the scene read as
|
||||||
sidebar".
|
"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
|
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
|
`MotionConfig reducedMotion="always"`, which only strips *transform* animations — while every
|
||||||
|
|
|
||||||
|
|
@ -690,11 +690,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chat header: the same glass as the cards, a touch more fill — its title and
|
/* 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 {
|
.aero .chat-header {
|
||||||
background-color: rgb(232 248 255 / 0.4);
|
background-color: rgb(232 248 255 / 0.4);
|
||||||
border-bottom: 1px solid rgb(255 255 255 / 0.6);
|
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
|
/* The chat composer: one flat frosted panel for the input, the chips and the
|
||||||
note — instead of each of them floating on the wallpaper. */
|
note — instead of each of them floating on the wallpaper. */
|
||||||
|
|
@ -703,6 +707,56 @@
|
||||||
border-top: 1px solid rgb(255 255 255 / 0.65);
|
border-top: 1px solid rgb(255 255 255 / 0.65);
|
||||||
box-shadow: 0 -16px 36px -26px rgb(2 62 110 / 0.5);
|
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
|
/* The chat welcome (subtitle + suggestions): the same frosted glass as the
|
||||||
cards (the shared recipe above), stretched flush with the header and the
|
cards (the shared recipe above), stretched flush with the header and the
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
|
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
import { Composer, type PendingAttachment } from "./Composer";
|
import { Composer, type PendingAttachment } from "./Composer";
|
||||||
import { ContextPicker } from "./ContextPicker";
|
import { ContextPicker } from "./ContextPicker";
|
||||||
|
|
@ -769,14 +768,6 @@ export function ChatScreen() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<HistorySheet
|
<HistorySheet
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export function Composer({
|
||||||
const canSend = online && !busy && value.trim().length > 0;
|
const canSend = online && !busy && value.trim().length > 0;
|
||||||
|
|
||||||
return (
|
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 ? (
|
{attachments.length > 0 ? (
|
||||||
<ul className="mb-2 flex flex-wrap gap-1.5">
|
<ul className="mb-2 flex flex-wrap gap-1.5">
|
||||||
{attachments.map((entry, index) => (
|
{attachments.map((entry, index) => (
|
||||||
|
|
|
||||||
|
|
@ -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.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.oldDecksNote": "Older decks only have the generated study content. Re-add the notes for better answers.",
|
||||||
"chat.fromLabel": "From",
|
"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.attach": "Attach photos or documents",
|
||||||
"chat.attachPhoto": "photo",
|
"chat.attachPhoto": "photo",
|
||||||
"chat.attachDoc": "document",
|
"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.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.oldDecksNote": "Starší sady mají jen vygenerovaný obsah. Přidej znovu poznámky a odpovědi budou lepší.",
|
||||||
"chat.fromLabel": "Z",
|
"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.attach": "Přiložit fotky nebo dokumenty",
|
||||||
"chat.attachPhoto": "fotka",
|
"chat.attachPhoto": "fotka",
|
||||||
"chat.attachDoc": "dokument",
|
"chat.attachDoc": "dokument",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "freezmo",
|
"name": "freezmo",
|
||||||
"version": "0.5.4",
|
"version": "0.5.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-v76";
|
const CACHE_VERSION = "freezmo-v77";
|
||||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||||
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -291,6 +291,58 @@ try {
|
||||||
await desktop.page.waitForTimeout(400);
|
await desktop.page.waitForTimeout(400);
|
||||||
check("desktop: AI disclaimer under the composer", await desktop.page.getByText("Freezmo uses AI and can make mistakes").isVisible());
|
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 ---------------
|
/* 6b. desktop pickers: the chip that opens also closes ---------------
|
||||||
(It used to only ever open, so the only way out was a choice.) */
|
(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();
|
const modelChip = () => desktop.page.getByRole("button", { name: /Stub (Text|Vision)|Chat model/ }).first();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue