feat(chat): pin the composer to the viewport bottom — Stop is always reachable while reading

This commit is contained in:
2026-08-30 14:59:40 -04:00
parent 619bf2187a
commit 820753948e
4 changed files with 897 additions and 0 deletions
+23
View File
@@ -1130,10 +1130,33 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
.suggestion-chip:active { transform: scale(0.98); }
/* ---------- Composer ---------- */
/* Phase 52 (2026-08-30, TODO.md L3): the composer is PINNED to the
viewport bottom. The page scrolls at the document level and
`.chat-shell` (the centered 46rem column) is the composer's sticky
containing block, so the box sticks to the bottom edge of the
viewport at every scroll position and settles back into its normal
flow position (above the footer) once the document bottom is
reached. Before this, a long conversation put the composer below the
fold while phase 42 forbids auto-scrolling a streaming reply — the
Stop control could literally not be reached ("it runs away from the
user as they try to click stop").
CSS-only by design: no DOM change, no JS, and NO new scroll call
site (the phase-42 never-auto-scroll contract stays intact —
`scrollReveal` is still the one page scroll in app.js). The offset
is the notch-aware safe-area inset (`env()` resolves to 0 on
desktop, so the box sits flush with the viewport bottom; ≤640px keeps
its own `main { padding-bottom: env(safe-area-inset-bottom) }`).
The solid `--surface` background, border, radius and shadow are kept
so messages scrolling behind the pinned box never show through it,
and no z-index is added: DOM order already paints the composer over
`.messages`, it never overlaps the sticky header (z 20) and stays
under the z-1000 document modal. */
.composer {
display: flex;
align-items: flex-end;
gap: 0.6rem;
position: sticky;
bottom: env(safe-area-inset-bottom);
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);