Skip to content

Commit b961579

Browse files
fix(dashboard): stop the shell scrolling sideways between 768px and ~890px (#588)
* fix(dashboard): stop the shell scrolling sideways between 768px and ~890px Every `/dashboard/*` page could only be read by panning horizontally at viewports between 768px and roughly 890px. Measured on the student dashboard, the document was 87px wider than the viewport at 768px, 55px at 800px and 5px at 850px. Two things collided, and both needed fixing. `SidebarInset` was `w-full flex-1` with no `min-w-0`. As a flex item its min-width resolved to `auto`, so it refused to shrink below its min-content width and widened the document instead of letting the offending content clip or scroll inside itself. That is a latent trap for any wide child — tables, code blocks, charts — not only for the case found here. The content that tripped it was the gamification header card, gated on `hidden md:block`. The desktop sidebar also appears at `md` and takes 256px, so the card arrived at exactly the moment the column it lives in got 256px narrower. Teacher and admin dashboards, which render the same shell without that card, never overflowed at any width — which is what isolated it. The card now waits until `lg`. Between 768px and 1023px it is still reachable in the avatar dropdown, whose gate moved from `md:hidden` to `lg:hidden` in lockstep; the two breakpoints are a pair, and a test now asserts the card is visible in exactly one of the two places at every width rather than in neither. Losing the always-visible chips in that band is a deliberate trade against a page that cannot be read without panning. Also matched the card's loading skeleton to its loaded state (`max-w-full overflow-hidden`), since the skeleton was briefly wider than the thing it stood in for. `tests/playwright/specs/overflow-586.spec.ts` walks student, teacher and admin dashboards across 700/768/800/850/900/1024/1280 and asserts `scrollWidth === clientWidth` everywhere. It fails on the parent commit with exactly the numbers above. Closes #586 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01183C2p5ed7youHQAUdPfR1 * docs(qa): before/after evidence for #586 Screenshots at 768px and 820px on the student dashboard and courses list, plus a scrolled-fully-right frame showing what was unreachable before, and a pair of GIFs sweeping the viewport from 700px to 1024px. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01183C2p5ed7youHQAUdPfR1 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 14a7832 commit b961579

16 files changed

Lines changed: 318 additions & 9 deletions

app/[locale]/dashboard/layout.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,19 @@ export default async function DashboardLayout({
2727
<SidebarProvider>
2828
<AppSidebar userRole={role} />
2929
<SidebarInset>
30-
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
30+
<header className="flex h-16 min-w-0 shrink-0 items-center gap-2 border-b px-4">
3131
<SidebarTrigger className="-ml-1" />
3232
<Separator orientation="vertical" className="mr-2 h-4" />
33-
<div className="ml-auto flex items-center gap-4">
33+
<div className="ml-auto flex min-w-0 items-center gap-4">
34+
{/* #586: gated at `lg`, not `md`. The desktop sidebar
35+
arrives at `md` and takes 256px, which leaves the
36+
inset too narrow for these chips — the page ended up
37+
scrolling sideways between 768px and ~890px. Below
38+
`lg` the same card is reachable in the avatar
39+
dropdown (see components/user-nav.tsx), so the two
40+
breakpoints must stay in lockstep. */}
3441
{role === 'student' && (
35-
<div className="hidden md:block">
42+
<div className="hidden lg:block">
3643
<GamificationHeaderCard />
3744
</div>
3845
)}

components/gamification/gamification-header-card.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ export function GamificationHeaderCard() {
1919

2020
if (loading) {
2121
return (
22-
<div className="flex items-center gap-4 px-3 py-1.5 bg-muted/50 rounded-xl border border-border">
22+
// #586: same `max-w-full overflow-hidden` as the loaded state below,
23+
// so the skeleton cannot be wider than the thing it stands in for.
24+
<div className="flex items-center gap-4 px-3 py-1.5 bg-muted/50 rounded-xl border border-border max-w-full overflow-hidden">
2325
<Skeleton className="h-6 w-20 rounded-md" />
2426
<Skeleton className="h-6 w-16 rounded-md" />
2527
<Skeleton className="h-6 w-16 rounded-md" />
@@ -30,7 +32,10 @@ export function GamificationHeaderCard() {
3032
if (!summary) return null;
3133

3234
return (
33-
<div className="flex items-center gap-1 md:gap-2 bg-muted/50 border border-border rounded-xl p-1 md:p-1.5 max-w-full overflow-hidden">
35+
<div
36+
data-slot="gamification-header-card"
37+
className="flex items-center gap-1 md:gap-2 bg-muted/50 border border-border rounded-xl p-1 md:p-1.5 max-w-full overflow-hidden"
38+
>
3439
{/* Level & XP */}
3540
<div className="flex items-center gap-1.5 md:gap-2 px-2 md:px-2.5 py-1 rounded-lg hover:bg-accent/50 transition-colors shrink-0">
3641
<div className="relative flex items-center justify-center">

components/ui/sidebar.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,11 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
307307
<main
308308
data-slot="sidebar-inset"
309309
className={cn(
310-
"relative flex w-full flex-1 flex-col bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
310+
// #586: `min-w-0` is load-bearing. Without it this flex item keeps its
311+
// `auto` min-width and refuses to shrink below its min-content width, so
312+
// a wide child widens the whole document instead of clipping or
313+
// scrolling inside the inset.
314+
"relative flex w-full min-w-0 flex-1 flex-col bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
311315
className
312316
)}
313317
{...props}

components/user-nav.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function UserNav({ user }: UserNavProps) {
3232
<DropdownMenu>
3333
<DropdownMenuTrigger
3434
render={
35-
<Button variant="ghost" className="relative h-8 w-8 rounded-full border border-border/50 overflow-hidden transition-colors">
35+
<Button data-testid="user-nav-trigger" variant="ghost" className="relative h-8 w-8 rounded-full border border-border/50 overflow-hidden transition-colors">
3636
<CurrentUserAvatar />
3737
</Button>
3838
}
@@ -81,8 +81,12 @@ export function UserNav({ user }: UserNavProps) {
8181
<span>{t('logout')}</span>
8282
</DropdownMenuItem>
8383
</DropdownMenuGroup>
84-
<DropdownMenuSeparator className="flex md:hidden" />
85-
<DropdownMenuGroup className="flex md:hidden p-2">
84+
{/* #586: `lg`, matching the header's gate in
85+
app/[locale]/dashboard/layout.tsx. These two breakpoints are a pair —
86+
whenever the header hides the card, this has to show it, or the
87+
streak and coins become unreachable between 768px and 1023px. */}
88+
<DropdownMenuSeparator className="flex lg:hidden" />
89+
<DropdownMenuGroup className="flex lg:hidden p-2">
8690
<GamificationHeaderCard />
8791
</DropdownMenuGroup>
8892
</DropdownMenuContent>
115 KB
Loading
76.7 KB
Loading
78.4 KB
Loading

docs/qa/586-after.gif

63.3 KB
Loading
113 KB
Loading
75.7 KB
Loading

0 commit comments

Comments
 (0)