Skip to content

Commit fc77d46

Browse files
aksOpsclaude
andcommitted
refactor(ui): restore pinned top-bar + scrollable session list
Reverse course on the previous commit's body-level scroll: the user prefers the classic app layout where the ctm header + QuotaStrip stay pinned at the top, the Live-feed link stays pinned at the bottom, and the session list scrolls in between. The actual fix that was doing the heavy lifting sticks: - Root is `h-dvh` (not `h-screen`) so the footer doesn't get shoved below the collapsible browser chrome on mobile — that was the real cause of "had to scroll twice to reach Live feed". - `html, body, #root` uses `min-height: 100%` (not `height: 100%`) so nothing clips internal scroll containers. Added `overscroll-contain` on the list's scroll div so momentum flings don't chain into the body / pull-to-refresh on touch devices — tighter app feel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 38254c1 commit fc77d46

2 files changed

Lines changed: 26 additions & 23 deletions

File tree

ui/src/components/SessionListPanel.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,10 @@ export function SessionListPanel({
3636
<aside
3737
aria-label="Sessions"
3838
className={cn(
39-
// On desktop the aside is a fixed-height flex column so the
40-
// middle region gets its own scroll; on mobile the page body
41-
// is the scroll container, so the aside flows naturally and
42-
// the footer sits at the end of the document (reachable in
43-
// one continuous scroll, letting the browser URL bar collapse
44-
// as the user drags).
45-
"flex flex-col border-r border-border bg-bg md:h-full md:min-h-0",
39+
// Fixed-height flex column: header + scrollable list + footer.
40+
// The inner div is the scroll container so the Live-feed link
41+
// stays pinned at the viewport bottom at every width.
42+
"flex h-full min-h-0 flex-col border-r border-border bg-bg",
4643
className,
4744
)}
4845
>
@@ -61,7 +58,15 @@ export function SessionListPanel({
6158
</label>
6259
</header>
6360

64-
<div className="md:min-h-0 md:flex-1 md:overflow-y-auto">
61+
<div
62+
className={cn(
63+
"min-h-0 flex-1 overflow-y-auto",
64+
// overscroll-contain stops the inner scroll from chaining
65+
// into the body when the user flings past either end —
66+
// gives a tighter, more app-like feel on touch devices.
67+
"overscroll-contain",
68+
)}
69+
>
6570
{isLoading && (
6671
<div className="space-y-2 p-4">
6772
<Skeleton className="h-16 w-full" />

ui/src/routes/Dashboard.tsx

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,18 @@ import { cn } from "@/lib/utils";
2121
* >=768px list (300px) | SessionDetail (auto-selects latest)
2222
* <768px list-only when no name; detail-only when name is set
2323
*
24-
* Height model differs by breakpoint:
24+
* Height model: root is `h-dvh` at every breakpoint so the header +
25+
* QuotaStrip stay pinned at top and the Live-feed footer stays pinned
26+
* at bottom, with the session list scrolling inside. `h-dvh` (dynamic
27+
* viewport) instead of `h-screen` (100vh) is critical on mobile —
28+
* 100vh includes the collapsible browser chrome, which would push
29+
* the footer below the visible area until the user scrolls once and
30+
* the address bar collapses. `dvh` tracks the actual visible viewport.
2531
*
26-
* < md — the *document body* is the scroll container. Root is
27-
* `min-h-dvh`, header/QuotaStrip flow naturally, list and
28-
* detail panes grow to content height. Browser URL bar
29-
* collapses on scroll the way users expect on mobile, and
30-
* the "Live feed" footer sits at the bottom of the document
31-
* so reaching it is a single continuous scroll.
32-
*
33-
* ≥ md — root is `h-dvh` (exact dynamic viewport). The middle flex
34-
* row is `flex-1 min-h-0 overflow-hidden` so list + detail
35-
* panes each own their own scroll container. Without the
36-
* min-h-0 the flex → overflow chain breaks and children
37-
* can't scroll.
32+
* The middle flex row is `flex-1 min-h-0 overflow-hidden` so the
33+
* list pane and the detail pane each own their own scroll container
34+
* and never push the page height. Without `min-h-0` the flex →
35+
* overflow chain breaks and children can't scroll.
3836
*/
3937
export function Dashboard() {
4038
const { name } = useParams<{ name?: string }>();
@@ -66,7 +64,7 @@ export function Dashboard() {
6664
const detailVisible = Boolean(name);
6765

6866
return (
69-
<div className="flex min-h-dvh flex-col bg-bg text-fg md:h-dvh">
67+
<div className="flex h-dvh flex-col bg-bg text-fg">
7068
<header className="flex shrink-0 items-center justify-between border-b border-border px-4 py-3">
7169
<h1 className="font-serif text-xl font-bold tracking-tight">ctm</h1>
7270
<div className="flex items-center gap-1">
@@ -117,7 +115,7 @@ export function Dashboard() {
117115
<CostChart />
118116
</div>
119117

120-
<div className="flex flex-1 md:min-h-0 md:overflow-hidden">
118+
<div className="flex min-h-0 flex-1 overflow-hidden">
121119
<SessionListPanel
122120
activeName={name}
123121
className={cn(

0 commit comments

Comments
 (0)