From f04c18ca4fbc1e70f02656a7ee09001a736d7494 Mon Sep 17 00:00:00 2001 From: Darkest-Teddy Date: Sun, 12 Jul 2026 12:58:17 -0400 Subject: [PATCH 1/4] fix(ui): pin full-height shell screens so only the body scrolls (#331) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the horizontal top-nav layout, ShellFrame stacks the 56px TopNav above
in a 100vh flex column, so
is only `100vh - 56px` tall. Screens that hardcoded `height: 100vh` were therefore taller than their container, overflowing
and scrolling the whole page — dragging chat headers/inputs out of view instead of scrolling just the message body. The sidebar layout was unaffected because SideNav sits beside
, giving it the full 100vh. Fix: fill the parent (`height: 100%`) instead of the viewport. Introduce a shared wrapper and adopt it across the full-height shell screens so the pattern is consistent and can't regress: - new: components/FullHeightScreen.tsx (height:100%, min-height:0) - Learn, Quiz, Library, Social, course-planner: use FullHeightScreen - notetaker: height 100vh -> 100% (root + loading/empty states) - Study: min-height 100vh -> 100% (stays scrollable, now bounded to
) Percentage height resolves in both layouts because
has a definite height in each, so the sidebar layout remains correct. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/app/(shell)/course-planner/page.tsx | 5 +- frontend/src/app/(shell)/notetaker/page.tsx | 6 +-- frontend/src/components/FullHeightScreen.tsx | 47 +++++++++++++++++++ frontend/src/components/screens/Learn.tsx | 9 ++-- frontend/src/components/screens/Library.tsx | 5 +- frontend/src/components/screens/Quiz.tsx | 5 +- frontend/src/components/screens/Social.tsx | 5 +- frontend/src/components/screens/Study.tsx | 2 +- 8 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 frontend/src/components/FullHeightScreen.tsx diff --git a/frontend/src/app/(shell)/course-planner/page.tsx b/frontend/src/app/(shell)/course-planner/page.tsx index 351edccf..9d523af6 100644 --- a/frontend/src/app/(shell)/course-planner/page.tsx +++ b/frontend/src/app/(shell)/course-planner/page.tsx @@ -1,10 +1,11 @@ "use client"; import { TopBar } from "@/components/TopBar"; import { Icon } from "@/components/Icon"; +import { FullHeightScreen } from "@/components/FullHeightScreen"; export default function CoursePlannerPage() { return ( -
+
-
+ ); } diff --git a/frontend/src/app/(shell)/notetaker/page.tsx b/frontend/src/app/(shell)/notetaker/page.tsx index 745330dd..7083f0e1 100644 --- a/frontend/src/app/(shell)/notetaker/page.tsx +++ b/frontend/src/app/(shell)/notetaker/page.tsx @@ -480,7 +480,7 @@ export default function NotetakerPage() { display: "flex", alignItems: "center", justifyContent: "center", - height: "100vh", + height: "100%", color: "var(--text-muted)", fontSize: 13, }} @@ -499,7 +499,7 @@ export default function NotetakerPage() { flexDirection: "column", alignItems: "center", justifyContent: "center", - height: "100vh", + height: "100%", padding: 32, textAlign: "center", gap: 12, @@ -556,7 +556,7 @@ export default function NotetakerPage() { } return ( -
+
`. + * + * ShellFrame's horizontal-nav layout stacks the 56px `TopNav` above `
` + * in a `100vh` flex column, so `
` is only `100vh - 56px` tall. Screens + * that hardcoded `height: 100vh` were therefore taller than their container, + * which overflowed `
` and made the whole page scroll — dragging chat + * headers/inputs out of view instead of scrolling just the body (issue #331). + * + * Filling the parent with `height: 100%` pins the screen to `
` exactly, + * so inner regions scroll internally. `
` has a definite height in both + * the sidebar and top-nav layouts, so the percentage resolves correctly in + * each — keeping the sidebar layout unaffected. + * + * Use this instead of `height: 100vh` for any full-height shell screen. + */ +export function FullHeightScreen({ + children, + direction = "column", + className, + style, +}: { + children: React.ReactNode; + /** Flex direction of the root. Defaults to "column". */ + direction?: "row" | "column"; + className?: string; + style?: React.CSSProperties; +}) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/src/components/screens/Learn.tsx b/frontend/src/components/screens/Learn.tsx index 46ca8b96..954a8d64 100644 --- a/frontend/src/components/screens/Learn.tsx +++ b/frontend/src/components/screens/Learn.tsx @@ -7,6 +7,7 @@ import { TopBar } from "../TopBar"; import { Icon } from "../Icon"; import { CustomSelect } from "../CustomSelect"; import { ChatPanel, type ChatMsg } from "../ChatPanel"; +import { FullHeightScreen } from "../FullHeightScreen"; import { SessionSummary } from "../SessionSummary"; import { SharedContextToggle, useSharedContext } from "../SharedContextToggle"; import { ModelToggle, useModelPref } from "../ModelToggle"; @@ -430,7 +431,7 @@ function LearnInner() { // ────────── Entry screen (no active session) ────────── if (!sessionId && !starting) { return ( -
+
-
+ ); } // ────────── Active session ────────── return ( -
+ {isMobile && ( @@ -650,7 +651,7 @@ function LearnInner() { onStartNext={startNextFromSummary} /> )} -
+ ); } diff --git a/frontend/src/components/screens/Library.tsx b/frontend/src/components/screens/Library.tsx index f5b96d7f..e00359f9 100644 --- a/frontend/src/components/screens/Library.tsx +++ b/frontend/src/components/screens/Library.tsx @@ -3,6 +3,7 @@ import React from "react"; import dynamic from "next/dynamic"; import { createPortal } from "react-dom"; import { TopBar } from "../TopBar"; +import { FullHeightScreen } from "../FullHeightScreen"; import { Icon } from "../Icon"; import { Pill } from "../Pill"; import { DocumentUploadModal } from "../DocumentUploadModal"; @@ -137,7 +138,7 @@ export function Library() { }, [documents]); return ( -
+
-
+
); } diff --git a/frontend/src/components/screens/Quiz.tsx b/frontend/src/components/screens/Quiz.tsx index 81a52772..178bc3c9 100644 --- a/frontend/src/components/screens/Quiz.tsx +++ b/frontend/src/components/screens/Quiz.tsx @@ -3,6 +3,7 @@ import React, { Suspense, useEffect, useMemo, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { TopBar } from "../TopBar"; +import { FullHeightScreen } from "../FullHeightScreen"; import { AIDisclaimerChip } from "../AIDisclaimerChip"; import { DisclaimerModal } from "../DisclaimerModal"; import { QuizPanel } from "../QuizPanel"; @@ -72,7 +73,7 @@ function QuizInner() { }, [conceptParam, topicParam, concepts]); return ( -
+ ) : null}
-
+ ); } diff --git a/frontend/src/components/screens/Social.tsx b/frontend/src/components/screens/Social.tsx index d5cd9404..45753b65 100644 --- a/frontend/src/components/screens/Social.tsx +++ b/frontend/src/components/screens/Social.tsx @@ -3,6 +3,7 @@ import React from "react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { Icon } from "../Icon"; +import { FullHeightScreen } from "../FullHeightScreen"; import { Avatar } from "../Avatar"; import { CustomSelect } from "../CustomSelect"; import { SocialRoomsSkeleton } from "../Skeleton"; @@ -1043,7 +1044,7 @@ export function Social() { const members = (overview?.members || []).map(m => ({ user_id: m.user_id, name: m.name })); return ( -
+
{tab === "directory" ? : active ? ( <> @@ -1124,7 +1125,7 @@ export function Social() { ))}
-
+ ); } diff --git a/frontend/src/components/screens/Study.tsx b/frontend/src/components/screens/Study.tsx index d0aa6f41..b94c83b3 100644 --- a/frontend/src/components/screens/Study.tsx +++ b/frontend/src/components/screens/Study.tsx @@ -116,7 +116,7 @@ export function Study() { ); return ( -
+
Date: Wed, 15 Jul 2026 01:41:50 -0400 Subject: [PATCH 2/4] fix(ui): pin the settings screen to
instead of calc(100vh - 112px) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings was the screen #331 missed. Its content row hardcoded `height: calc(100vh - 112px)` — the TopBar height subtracted from the viewport, which only held under the sidebar layout where `
` is the full viewport. Under the top-nav layout `
` is `100vh - 56px`, so the row overflowed it by exactly the 56px TopNav: /settings got a second scrollbar and pushed its bottom 56px below the fold, including the account-deletion controls on the `data` tab. Root the screen in FullHeightScreen and let the content row flex into whatever `
` leaves below the TopBar, matching how the other five screens were fixed. This also drops the 112px magic number, so the row no longer silently breaks if the TopBar is ever resized. PreviewModal is `position: fixed`, so it stays out of flow and is unaffected by the root becoming a flex column. --- frontend/src/components/screens/Settings.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/screens/Settings.tsx b/frontend/src/components/screens/Settings.tsx index 2254ce43..96211740 100644 --- a/frontend/src/components/screens/Settings.tsx +++ b/frontend/src/components/screens/Settings.tsx @@ -2,6 +2,7 @@ import React from "react"; import Link from "next/link"; import { TopBar } from "../TopBar"; +import { FullHeightScreen } from "../FullHeightScreen"; import { Icon } from "../Icon"; import { Avatar } from "../Avatar"; import { CustomSelect } from "../CustomSelect"; @@ -216,7 +217,7 @@ export function Settings() { const tabs: Tab[] = ["profile", "preferences", "notifications", "data"]; return ( -
+ } /> -
+ {/* Fills whatever `
` leaves below TopBar. `minHeight: 0` lets this + shrink past its content so the panes below scroll internally rather + than pushing the screen taller than `
`. */} +
{!settings && tab !== "cosmetics" && } {tab === "profile" && settings && ( @@ -635,7 +639,7 @@ export function Settings() { {previewOpen && preview && ( setPreviewOpen(false)} /> )} -
+ ); } From 1113035e347ee2d313ed110cc705c1d417e94f84 Mon Sep 17 00:00:00 2001 From: AndresL230 <190146319+AndresL230@users.noreply.github.com> Date: Wed, 15 Jul 2026 01:42:03 -0400 Subject: [PATCH 3/4] fix(ui): size the app shell with 100dvh so the iOS toolbar can't clip it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On iOS Safari `100vh` resolves to the *large* viewport — the height with the toolbar collapsed — so a `100vh` shell always extends past the visual viewport while the toolbar is expanded, hiding its own bottom edge. Until now the full-height screens overflowed `
` by 56px, and that incidental scroll slack let you drag the clipped edge back into view. #331 removed the slack: screens fit `
` exactly, so anything under the toolbar is now unreachable — most visibly the /learn composer. `100dvh` tracks the visual viewport, so the shell ends where the toolbar begins. The usual dvh objection (the value changes as the toolbar collapses, resizing the layout mid-scroll) does not apply here: the shell root is `overflow: hidden` and scrolling happens in inner panes, so the document never scrolls and the toolbar never collapses. No `100vh` fallback: expressing one needs two declarations of the same property, which a React inline style object cannot hold, so it would mean moving the shell root to a CSS class. It would buy nothing — dvh has been Baseline since 2022 (Safari 15.4 / Chrome 108 / Firefox 101), and globals.css already leans unguarded on `color-mix()`, `:has()`, and `overflow-x: clip`, all of which are equal or narrower support. Any browser needing the fallback is already broken by the stylesheet. --- frontend/src/components/FullHeightScreen.tsx | 2 +- frontend/src/components/ShellFrame.tsx | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/FullHeightScreen.tsx b/frontend/src/components/FullHeightScreen.tsx index 195e1754..ff70bbd9 100644 --- a/frontend/src/components/FullHeightScreen.tsx +++ b/frontend/src/components/FullHeightScreen.tsx @@ -6,7 +6,7 @@ import React from "react"; * Full-height root for screens rendered inside `ShellFrame`'s `
`. * * ShellFrame's horizontal-nav layout stacks the 56px `TopNav` above `
` - * in a `100vh` flex column, so `
` is only `100vh - 56px` tall. Screens + * in a `100dvh` flex column, so `
` is only `100dvh - 56px` tall. Screens * that hardcoded `height: 100vh` were therefore taller than their container, * which overflowed `
` and made the whole page scroll — dragging chat * headers/inputs out of view instead of scrolling just the body (issue #331). diff --git a/frontend/src/components/ShellFrame.tsx b/frontend/src/components/ShellFrame.tsx index 1f089754..fa9807a4 100644 --- a/frontend/src/components/ShellFrame.tsx +++ b/frontend/src/components/ShellFrame.tsx @@ -11,6 +11,16 @@ import { AchievementUnlockWatcher } from "./AchievementUnlockWatcher"; import { useLayoutPref } from "@/lib/useLayoutPref"; import { useIsMobile } from "@/lib/useIsMobile"; +/** + * `100dvh`, not `100vh`: on iOS Safari `100vh` is the *large* viewport (the + * height with the toolbar collapsed), so with the toolbar expanded a `100vh` + * shell runs past the visual viewport and hides its own bottom edge — the + * `/learn` composer, most visibly. Screens now fill `
` exactly (#331), + * so there is no leftover scroll slack to drag that edge back into view. + * `100dvh` tracks the visual viewport instead, and since the shell itself + * never scrolls (`overflow: hidden`, inner panes scroll), the toolbar never + * collapses and the value stays put — no resize thrash. + */ export function ShellFrame({ children }: { children: React.ReactNode }) { const [pref] = useLayoutPref(); const isMobile = useIsMobile(); @@ -22,7 +32,7 @@ export function ShellFrame({ children }: { children: React.ReactNode }) { style={{ display: "flex", flexDirection: "row", - height: "100vh", + height: "100dvh", overflow: "hidden", position: "relative", }} @@ -58,7 +68,7 @@ export function ShellFrame({ children }: { children: React.ReactNode }) { style={{ display: "flex", flexDirection: "column", - height: "100vh", + height: "100dvh", overflow: "hidden", position: "relative", }} From 40fdc9131d772a71b8235e47ca332baed09ad9bf Mon Sep 17 00:00:00 2001 From: AndresL230 <190146319+AndresL230@users.noreply.github.com> Date: Wed, 15 Jul 2026 01:42:14 -0400 Subject: [PATCH 4/4] fix(ui): drop inert minHeight from FullHeightScreen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `min-height: 0` only does anything on a flex item, where it overrides the `auto` automatic-minimum-size that would otherwise refuse to shrink below the content. FullHeightScreen's documented parent is ShellFrame's `
`, a block container, so the root is a block-level box and `min-height: auto` already computes to 0 — the declaration is a no-op at every one of its six call sites. Remove it rather than comment it. Inert CSS on a shared primitive reads as load-bearing and gets copied into places where the author has not checked whether it matters. Callers that genuinely need it can pass it via `style`, and screens that need it on their own inner rows still set it there (see Settings). --- frontend/src/components/FullHeightScreen.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/components/FullHeightScreen.tsx b/frontend/src/components/FullHeightScreen.tsx index ff70bbd9..8c16316c 100644 --- a/frontend/src/components/FullHeightScreen.tsx +++ b/frontend/src/components/FullHeightScreen.tsx @@ -37,7 +37,6 @@ export function FullHeightScreen({ display: "flex", flexDirection: direction, height: "100%", - minHeight: 0, ...style, }} >