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 `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). + * + * 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/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", }} 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/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)} /> )} -
+ ); } 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 ( -
+