Fix sidebar rail overflow on the chat page
The content offset was fine — the rail's glass panel was the problem. As a
flex-1 item in the row-flex aside, the panel's automatic minimum size
(min-width: auto) is its content's min-content width. The chat list's
search input (an <input> defaults to size=20) has a ~192px intrinsic
width, which with the New chat button stretched the panel to 345px — 89px
past the 268px rail — where it painted over the chat page: the header
title, the composer placeholder ("sk about your notes...") and the
context chip ("ts").
- min-w-0 on the panel so the rail width always wins and wide content
truncates instead (the input and list rows already shrink/truncate).
- Chat suite: assert the panel stays inside the rail and the main content
starts after it, expanded and collapsed (the old checks counted
elements, never their boxes — this is what slipped through).
- README: history bullet + decision 49. SW -> freezmo-v36.
Verified on the dev instance and a production build: panel 244px inside
the 268px rail (60px in the 84px collapsed rail) at 1024-1920, content
starts after the rail, all sidebar pages unchanged, dark + light, and
the full e2e (12/12 suites, 516 checks).
This commit is contained in:
parent
8a3bef0add
commit
05e0fdcc86
4 changed files with 55 additions and 9 deletions
22
README.md
22
README.md
|
|
@ -765,11 +765,12 @@ have a step explained without leaving the app.
|
||||||
allowlist and never accepts a model name from the client.
|
allowlist and never accepts a model name from the client.
|
||||||
- **Attachments.** Photos, PDFs and documents go through the existing vision/extraction step first
|
- **Attachments.** Photos, PDFs and documents go through the existing vision/extraction step first
|
||||||
(plain text), so any chat model works — text-only included.
|
(plain text), so any chat model works — text-only included.
|
||||||
- **History.** Conversations live in a local IndexedDB store: a list on the left on desktop, a
|
- **History.** Conversations live in a local IndexedDB store: the list sits in the main sidebar on
|
||||||
sheet on phones, with auto titles from your first question, rename, delete (with Undo) and a
|
desktop (it takes the deck navigation's place on `/chat`), a sheet on phones, with auto titles
|
||||||
search across titles and message text. Conversations remember their scope and model, reopen from
|
from your first question, rename, delete (with Undo) and a search across titles and message
|
||||||
`?c=<id>` (so a reload continues where you left off), stay readable offline, and are included in
|
text. Conversations remember their scope and model, reopen from `?c=<id>` (so a reload continues
|
||||||
the backup export and the encrypted sync — still never the key.
|
where you left off), stay readable offline, and are included in the backup export and the
|
||||||
|
encrypted sync — still never the key.
|
||||||
- **From an answer.** Every answer can become a deck (**Turn into flashcards** opens New deck with
|
- **From an answer.** Every answer can become a deck (**Turn into flashcards** opens New deck with
|
||||||
the answer as notes, on the options step) or be appended to the scoped deck's notes (**Save to
|
the answer as notes, on the options step) or be appended to the scoped deck's notes (**Save to
|
||||||
notes**, only offered when exactly one deck is in scope).
|
notes**, only offered when exactly one deck is in scope).
|
||||||
|
|
@ -1258,11 +1259,20 @@ Things I chose while building, with the reasoning:
|
||||||
Assistant answers also offer **Turn into flashcards** (opens New deck with the answer as notes,
|
Assistant answers also offer **Turn into flashcards** (opens New deck with the answer as notes,
|
||||||
landing on the options step) and **Save to notes** (appends to the single scoped deck, trimming
|
landing on the options step) and **Save to notes** (appends to the single scoped deck, trimming
|
||||||
at 40k characters). Motion comes from `lib/motion.ts`: message fade-up, sheet springs, a 150 ms
|
at 40k characters). Motion comes from `lib/motion.ts`: message fade-up, sheet springs, a 150 ms
|
||||||
composer height transition — all skipped under reduced motion. Verified by the chat suite (89
|
composer height transition — all skipped under reduced motion. Verified by the chat suite (92
|
||||||
checks: everything from phase 1 plus history persistence, reload, rename/search/delete/undo,
|
checks: everything from phase 1 plus history persistence, reload, rename/search/delete/undo,
|
||||||
both actions, the export payload, a two-device sync round-trip of a chat, offline readability
|
both actions, the export payload, a two-device sync round-trip of a chat, offline readability
|
||||||
and the desktop panel).
|
and the desktop panel).
|
||||||
|
|
||||||
|
49. **The sidebar rail never lets its content widen it.** The desktop rail is a fixed-width `flex`
|
||||||
|
aside and its glass panel is a `flex-1` item, so the panel's automatic minimum size
|
||||||
|
(`min-width: auto`) is its content's min-content width. When the chat list moved into the
|
||||||
|
sidebar, its search input's intrinsic width (an `<input>` defaults to `size=20`) stretched the
|
||||||
|
panel 89 px past the rail, where it painted over the page — the content offset itself was
|
||||||
|
correct, which is what made it look like the page had lost its left margin. The panel now
|
||||||
|
carries `min-w-0` so wide content truncates instead, and the chat suite asserts that the panel
|
||||||
|
stays inside the rail and the main content starts after it, expanded and collapsed.
|
||||||
|
|
||||||
36. **The mobile declutter pass reshapes, it doesn't fork.** Phones (≤639px) get a 3-step deck
|
36. **The mobile declutter pass reshapes, it doesn't fork.** Phones (≤639px) get a 3-step deck
|
||||||
flow and a 2-step homework flow that share the exact state and handlers with the desktop
|
flow and a 2-step homework flow that share the exact state and handlers with the desktop
|
||||||
cards (one `UploadScreen` / one `HomeworkInput`, two layouts switched by `useIsPhone()`);
|
cards (one `UploadScreen` / one `HomeworkInput`, two layouts switched by `useIsPhone()`);
|
||||||
|
|
|
||||||
|
|
@ -373,8 +373,12 @@ export function Sidebar() {
|
||||||
isCollapsed ? "w-[84px]" : "w-[268px]",
|
isCollapsed ? "w-[84px]" : "w-[268px]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* One tall inset glass panel, so the ice scene shows around it. */}
|
{/* One tall inset glass panel, so the ice scene shows around it. The
|
||||||
<div className="sidebar-surface flex min-h-0 flex-1 flex-col px-2.5 py-3">
|
min-w-0 keeps it inside the rail: as a flex item its automatic
|
||||||
|
minimum size is the content's min-content width, and wide content
|
||||||
|
(e.g. the chat list's search input) would otherwise stretch the
|
||||||
|
panel past the rail and paint over the page. */}
|
||||||
|
<div className="sidebar-surface flex min-h-0 min-w-0 flex-1 flex-col px-2.5 py-3">
|
||||||
<NavContent collapsed={isCollapsed} />
|
<NavContent collapsed={isCollapsed} />
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
||||||
|
|
@ -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-v35";
|
const CACHE_VERSION = "freezmo-v36";
|
||||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||||
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,38 @@ try {
|
||||||
await desktop.page.waitForTimeout(1200);
|
await desktop.page.waitForTimeout(1200);
|
||||||
check("desktop: chat route opens with the sidebar", desktop.page.url().endsWith("/chat") && (await desktop.page.locator("aside").count()) >= 1);
|
check("desktop: chat route opens with the sidebar", desktop.page.url().endsWith("/chat") && (await desktop.page.locator("aside").count()) >= 1);
|
||||||
check("desktop: no tab bar on chat", (await desktop.page.locator("nav.tabbar").count()) === 0);
|
check("desktop: no tab bar on chat", (await desktop.page.locator("nav.tabbar").count()) === 0);
|
||||||
|
|
||||||
|
/* The rail must not paint over the page: its glass panel stays inside the
|
||||||
|
rail and the chat content starts after it. Regression: a wide chat list
|
||||||
|
stretched the panel (flex item min-width: auto) past the rail and
|
||||||
|
covered the header title, composer and context chip. */
|
||||||
|
const railMetrics = () =>
|
||||||
|
desktop.page.evaluate(() => {
|
||||||
|
const aside = document.querySelector("aside[aria-label]");
|
||||||
|
const panel = aside?.firstElementChild;
|
||||||
|
const main = document.querySelector("main");
|
||||||
|
if (!aside || !panel || !main) return null;
|
||||||
|
const a = aside.getBoundingClientRect();
|
||||||
|
const p = panel.getBoundingClientRect();
|
||||||
|
const m = main.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
panelInside: p.right <= a.right - 1,
|
||||||
|
contentClear: m.left >= a.right - 1,
|
||||||
|
gap: Math.round(m.left - p.right),
|
||||||
|
overflowing: [...panel.querySelectorAll("*")].some((el) => el.getBoundingClientRect().right > p.right + 1),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandedRail = await railMetrics();
|
||||||
|
check("desktop: sidebar panel stays inside the rail", expandedRail !== null && expandedRail.panelInside && !expandedRail.overflowing);
|
||||||
|
check("desktop: chat content starts after the sidebar", expandedRail !== null && expandedRail.contentClear && expandedRail.gap > 0);
|
||||||
|
|
||||||
|
await desktop.page.getByRole("button", { name: "Collapse sidebar" }).click();
|
||||||
|
await desktop.page.waitForTimeout(400);
|
||||||
|
const collapsedRail = await railMetrics();
|
||||||
|
check("desktop: collapsed rail also stays inside and clear", collapsedRail !== null && collapsedRail.panelInside && !collapsedRail.overflowing && collapsedRail.contentClear);
|
||||||
|
await desktop.page.getByRole("button", { name: "Expand sidebar" }).click();
|
||||||
|
await desktop.page.waitForTimeout(400);
|
||||||
await desktop.context.close();
|
await desktop.context.close();
|
||||||
|
|
||||||
const phone = await makePage(browser);
|
const phone = await makePage(browser);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue