fix(ui): replace viewport-math residuals with FullHeightScreen (#341) - #487

Merged
AndresL230 merged 6 commits into
mainfrom
feat/341-viewport-math
Jul 31, 2026
Merged

fix(ui): replace viewport-math residuals with FullHeightScreen (#341)#487
AndresL230 merged 6 commits into
mainfrom
feat/341-viewport-math

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Replaces the three surviving calc(100vh - …) residuals with FullHeightScreen + flex, and adds the journey that proves the one with a user-visible consequence.

Why these were wrong

ShellFrame renders two layouts: a sidebar (its <main> is the full 100dvh) or a horizontal TopNav above <main> (so <main> is 100dvh - 56px). Any constant subtracted from 100vh can only be calibrated for one of them — and none of them accounted for the density preference, which retunes the padding tokens of the chrome being subtracted.

The consequential one — Tree.tsx

height: calc(100vh - 240px) wrapped the element a ResizeObserver watches; its contentRect is passed straight through as <KnowledgeGraph width height>. So this was not a stray scrollbar — the graph canvas itself rendered at the wrong size in whichever layout the constant wasn't tuned for.

Now: FullHeightScreen root + flex: 1 / minHeight: 0 on the graph row, so it absorbs exactly what <main> has left below the TopBar and filter row. Correct in both layouts, at any density, by construction rather than by calibration.

The semantic ones — Gradebook Landing + Course

minHeight: calc(100vh - var(--row-h)) subtracted a density token (40/34/48px per globals.css) as if it were a nav height. Both now sit in a FullHeightScreen with flex: 1 0 auto on their <main> — fill the remaining space, keep growing when content is taller (the minHeight semantic, preserved).

Test

frontend/e2e/tree-viewport.spec.ts asserts the invariant that was violated: on /tree the graph canvas fits inside the shell scrollport — no overshoot, no scrollable overflow — in both layouts, driven off the sapling_layout localStorage pref. It is a new spec rather than an addition to graph.spec.ts, which declares itself data-only and deliberately reads no x/y.

FullscreenGraph (the overlay variant) already used flex: 1 and is untouched.

Gates

  • tsc --noEmit — clean
  • npm run lint — 0 errors (36 pre-existing warnings)
  • npx vitest run — 55 files, 399 tests passed
  • Full local e2e cycle — recorded in a comment below

part of #341

Summary by CodeRabbit

  • Bug Fixes

    • Improved screen sizing across sidebar and top-navigation layouts.
    • Fixed Tree and Gradebook views to fill available space without unwanted overflow.
    • Preserved legitimate scrolling for content that exceeds the available height.
  • Tests

    • Added end-to-end coverage validating viewport fit, shell sizing, canvas settling, and content scrolling.

Three screens still sized themselves by subtracting a magic constant from
100vh. That subtraction can only be correct for ONE of ShellFrame's two
layouts — the sidebar variant gives `<main>` the full 100dvh, the TopNav
variant gives it 100dvh - 56px — and it is blind to the density preference
that retunes the padding tokens above it.
Tree.tsx is the one with a user-visible consequence, not just a stray
scrollbar: the row sized `calc(100vh - 240px)` wraps the element a
ResizeObserver watches, and that contentRect is passed straight through as
`<KnowledgeGraph width height>`. A mis-measured row therefore renders the
graph CANVAS at the wrong size. It now takes `flex: 1` + `minHeight: 0`
inside a FullHeightScreen root, so it absorbs exactly what `<main>` has
left after the TopBar and filter row, in either layout and at any density.
Gradebook Landing/Course used `calc(100vh - var(--row-h))` — subtracting a
DENSITY token (40/34/48px) as though it were a nav height. Both now sit in
a FullHeightScreen with `flex: 1 0 auto` on their `<main>`: fill the
remaining space, keep growing when the content is taller.
Adds frontend/e2e/tree-viewport.spec.ts, which asserts the invariant that
was silently violated — on /tree the graph canvas fits inside the shell
scrollport — in BOTH layouts, driven off the localStorage layout pref. It
lives in its own spec because graph.spec.ts declares itself data-only and
reads no geometry by design.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabaseBot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5fd64ba9-fab4-4f6b-9590-eb106f11a53c

📥 Commits

Reviewing files that changed from the base of the PR and between e700c62 and caf04b0.

📒 Files selected for processing (1)
  • frontend/e2e/viewport-fit.spec.ts
📝 Walkthrough

Walkthrough

The PR replaces viewport-based sizing with FullHeightScreen layouts for Tree and Gradebook screens. It adds Playwright tests for shell fit, graph sizing, and Gradebook content height in sidebar and top-navigation layouts.

Changes

Viewport-fit shell layout

Layer / File(s)Summary
Full-height screen integration
frontend/src/components/screens/Tree.tsx, frontend/src/components/screens/Gradebook/Course.tsx, frontend/src/components/screens/Gradebook/Landing.tsx
Tree and Gradebook screens now use FullHeightScreen. Tree uses flexible graph sizing and a non-shrinking filter row. Gradebook content uses flex growth instead of viewport-based minimum heights.
Viewport-fit end-to-end validation
frontend/e2e/viewport-fit.spec.ts
Playwright coverage validates shell geometry, settled graph height, overflow, and Gradebook content sizing across both shell layouts.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers:darkest-teddy, jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly identifies the main UI change: replacing viewport calculations with FullHeightScreen.
Description check✅ PassedThe description explains the motivation, implementation, affected screens, tests, and validation results in sufficient detail.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/341-viewport-math

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingcaf04b0Commit Preview URL

Branch Preview URL
Jul 31 2026, 02:59 AM

AndresL230and others added 3 commits July 30, 2026 19:13
Review follow-ups on the journey added with this PR.
Two-sided fit: the spec only guarded overshoot, so a future regression that
broke `flex: 1` and left the canvas a few pixels tall would have passed —
`canvasHeight > 0` is not a floor. It now asserts the canvas settles FLUSH
with the scrollport's bottom edge, which is the actual stated invariant, and
polls for it because the canvas size is ResizeObserver-driven and starts at
Tree.tsx's placeholder 900x600.
Gradebook coverage: Landing/Course got the same class of change as Tree with
no geometry assertion behind it — the gap that let the identical
`calc(100vh - N)` bug survive review on Settings.tsx during #335, since jsdom
has no layout engine and cannot catch it. Adds a second test asserting the
seeded gradebook does not manufacture scroll it does not need, in both
layouts.
Renames tree-viewport.spec.ts -> viewport-fit.spec.ts now that it covers two
screens.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ever scrolls (#341)
The first version of this test asserted the gradebook page must not scroll
at all. That is wrong: under the topnav layout the scrollport is 56px
shorter, and the seeded gradebook's content genuinely needs more than it —
scrolling there is the correct answer, and the assertion failed the FIXED
build for a legitimate reason (16px at topnav, 0 at sidebar).
What the fix actually guarantees is narrower: the container contributes no
height of its own beyond the space it was given or the height its content
needs. That is now what the test measures — <main> must be no taller than
max(available, content), and no shorter than the space available (which
would mean flex-grow is broken).
The unfixed build overshot by exactly 40px at the default density, which is
`--row-h` to the pixel — the density-token-as-nav-height bug, measured.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ght (#341)
The previous revision of this assertion passed on the UNFIXED build, which
makes it worthless as a regression test. Caught by running the spec against
the base sha before trusting it green.
Cause: it derived the content height from `main.scrollHeight`, which is
floored at the element's own client height. Any phantom height the box gave
itself was therefore mirrored into the "content" it was being compared
against, so `mainHeight <= max(available, content)` held by construction and
could never fail.
It now measures the content extent from the in-flow children (skipping
AmbientOrbs, which is position:fixed) plus the bottom padding — a number the
box's own sizing cannot influence.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Local e2e cycle — red at base, green at fix

Run as two phases inside one flock of the stack lock, at 3ee666c vs base 432c207, with SAPLING_MODEL_MODE=function.

Both phases matter: a journey that passes on unfixed code proves nothing, so phase 1 runs the new spec against the base build and requires it to fail.

Phase 1 — base 432c207 + the new spec: 2 failed (expected)

layout "topnav": canvas 480px vs scrollport 664px — overshoots by 27px
layout "sidebar": <main> is 686px but needs only 647px
(647px available, 606px of content) — 39px is phantom

Two things worth noting in those numbers:

  • The tree failure is topnav only — sidebar passed at base, which is exactly what a 240px constant calibrated for the sidebar layout would do. 27px measured against the issue's estimate of ~29px.
  • The gradebook phantom is 39px, which is --row-h (40px at the default density) to within rounding — the density-token-as-nav-height bug, measured rather than argued.

Phase 2 — fix 3ee666c: 35/35 journeys, oracles clean

35 passed (1.3m)
0 finding(s), 1 suppressed (allowlisted)

Two corrections this caught

The gradebook assertion took two revisions before it was worth keeping, both caught by the base run rather than by review:

  1. First version asserted the page must not scroll at all. Wrong — under topnav the scrollport is 56px shorter and the seeded content genuinely needs more than it, so scrolling there is correct. It failed the fixed build for a legitimate reason.
  2. Second version derived content height from main.scrollHeight, which is floored at the element's own client height — so any phantom height was mirrored into the number it was compared against, and it passed at base. Vacuous.

It now measures the content extent from the in-flow children (skipping AmbientOrbs, which is position: fixed), which the box's own sizing cannot influence.

Gates

tsc --noEmit clean · npm run lint 0 errors · npx vitest run 55 files / 399 tests passed

Follow-ups from the review of the spec delta.
Gate on the LOADED gradebook grid, not the chrome. `gradebook-transcript-open`
renders as soon as there is a user, while `loading` is still swapping a
six-card skeleton in for the real grid — measuring that transient made the
phantom-height assertion depend on fetch timing. Now waits on
role="grid"/"Courses", which only exists once the fetch resolved.
Count trailing child margins in the content measurement. A margin-bottom on
a direct in-flow child raises <main>'s height without appearing in any
child's bounding rect, which would have understated the content and turned
a real regression into a pass. Latent today; cheap to close.
Assert on the reading that settled. The tree test polled for convergence and
then measured AGAIN, reopening the window the poll existed to close. A small
settledFit() helper now returns the very reading that satisfied the
condition, and every assertion runs on that one. It also returns the last
reading on timeout, so a failure reports real numbers instead of a bare
"timed out".
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
frontend/e2e/viewport-fit.spec.ts (1)

120-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add Gradebook Course screen coverage for the same phantom-height fix.

frontend/e2e/viewport-fit.spec.ts only runs this geometry check on /gradebook. No other e2e test exercises GradebookCourseScreen path with the same <main> content-vs-container height assertion, so add a course-route test or sibling coverage to cover this regression directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/e2e/viewport-fit.spec.ts` around lines 120 - 189, Extend the
viewport-fit geometry coverage to navigate to a GradebookCourseScreen course
route in addition to /gradebook. Reuse the existing layout loop and <main>
content-versus-available-height assertions, ensuring the course screen’s route
and seeded data are loaded before measuring so the phantom-height regression is
tested directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@frontend/e2e/viewport-fit.spec.ts`:
- Around line 120-189: Extend the viewport-fit geometry coverage to navigate to
a GradebookCourseScreen course route in addition to /gradebook. Reuse the
existing layout loop and <main> content-versus-available-height assertions,
ensuring the course screen’s route and seeded data are loaded before measuring
so the phantom-height regression is tested directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 99a37da4-caf8-470e-ab54-4b0b4d647048

📥 Commits

Reviewing files that changed from the base of the PR and between 432c207 and e700c62.

📒 Files selected for processing (4)
  • frontend/e2e/viewport-fit.spec.ts
  • frontend/src/components/screens/Gradebook/Course.tsx
  • frontend/src/components/screens/Gradebook/Landing.tsx
  • frontend/src/components/screens/Tree.tsx

#341)
CI's react-hooks/rules-of-hooks rejected the helper: anything named use* is
treated as a Hook, and the spec calls it inside a for-loop over the two shell
layouts. Renamed to switchLayout — behaviour identical.
Local `npm run lint` reported 0 errors on the same code; the installed
eslint-plugin-react-hooks is older than CI's, so this was only visible there.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 162078a into mainJul 31, 2026
7 checks passed
@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Member

Automated post-merge review pass (Claude Code) on caf04b0 (the review ran while this merged — treat as follow-up input). The core verdict is positive: all three residuals from #341 are properly root-caused, the flex: 1 0 auto replacement expresses what the old minHeight: calc(...) was failing to say, the residual sweep is genuinely complete for the bug class, and the red-at-base regression journey (both layouts, overshoot and undershoot, real scrollport measurements) is exemplary.

One real regression worth a follow-up fix:

  1. Sticky TopBar travel is now clipped on the two gradebook screens (Gradebook/Landing.tsx:229, Course.tsx:519). At base, TopBar's sticky containing block was the shell <main>'s full scrollable content, so it stayed pinned for the whole scroll. At head its parent is FullHeightScreen's fixed height: 100% box — exactly one scrollport tall — so once page content exceeds ~2× the scrollport, the pinned header (breadcrumb, Curve/Letter Scale actions) detaches and scrolls away mid-page. These are precisely the screens that deliberately still page-scroll, and long assignment tables plausibly cross the threshold. Verified: FullHeightScreen.tsx sets height: "100%" with the ...style spread after it, so passing style={{ height: "auto", minHeight: "100%" }} on just these two screens restores full sticky travel while keeping the fill/phantom-height behavior the journey asserts (a min-height flex column still distributes free space to a flex: 1 0 auto child). The pinned-chat screens must keep fixed height — a grow prop on FullHeightScreen would make the two modes explicit.

Minor follow-ups:

  1. One same-class residual survives: (shell)/notetaker/page.tsx:1028minHeight: calc(100vh - 280px) on the fullscreen editor textarea (defanged by the enclosing scroll pane, but the same blind-constant pattern).
  2. viewport-fit.spec.ts measures /gradebook Landing only; Course.tsx got the identical fix but no measurement — extending the loop to the seeded /gradebook/[courseId] is cheap, and a scroll-then-assert that TopBar is still pinned on a tall Course page is the assertion that would have caught Add Personal Client-Side Definitive Study Guides #1.
  3. Tree.tsx:280-300 — the filter row got flexShrink: 0 but the TopBar header keeps default shrink; at very short viewports the TopBar is now the only compressible child and squashes (previously the page scrolled). flexShrink: 0 on the header keeps the chrome intact.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@AndresL230@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(ui): replace viewport-math residuals with FullHeightScreen (#341) - #487

Merged
AndresL230 merged 6 commits into
mainfrom
feat/341-viewport-math
Jul 31, 2026
Merged

fix(ui): replace viewport-math residuals with FullHeightScreen (#341)#487
AndresL230 merged 6 commits into
mainfrom
feat/341-viewport-math

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Replaces the three surviving calc(100vh - …) residuals with FullHeightScreen + flex, and adds the journey that proves the one with a user-visible consequence.

Why these were wrong

ShellFrame renders two layouts: a sidebar (its <main> is the full 100dvh) or a horizontal TopNav above <main> (so <main> is 100dvh - 56px). Any constant subtracted from 100vh can only be calibrated for one of them — and none of them accounted for the density preference, which retunes the padding tokens of the chrome being subtracted.

The consequential one — Tree.tsx

height: calc(100vh - 240px) wrapped the element a ResizeObserver watches; its contentRect is passed straight through as <KnowledgeGraph width height>. So this was not a stray scrollbar — the graph canvas itself rendered at the wrong size in whichever layout the constant wasn't tuned for.

Now: FullHeightScreen root + flex: 1 / minHeight: 0 on the graph row, so it absorbs exactly what <main> has left below the TopBar and filter row. Correct in both layouts, at any density, by construction rather than by calibration.

The semantic ones — Gradebook Landing + Course

minHeight: calc(100vh - var(--row-h)) subtracted a density token (40/34/48px per globals.css) as if it were a nav height. Both now sit in a FullHeightScreen with flex: 1 0 auto on their <main> — fill the remaining space, keep growing when content is taller (the minHeight semantic, preserved).

Test

frontend/e2e/tree-viewport.spec.ts asserts the invariant that was violated: on /tree the graph canvas fits inside the shell scrollport — no overshoot, no scrollable overflow — in both layouts, driven off the sapling_layout localStorage pref. It is a new spec rather than an addition to graph.spec.ts, which declares itself data-only and deliberately reads no x/y.

FullscreenGraph (the overlay variant) already used flex: 1 and is untouched.

Gates

  • tsc --noEmit — clean
  • npm run lint — 0 errors (36 pre-existing warnings)
  • npx vitest run — 55 files, 399 tests passed
  • Full local e2e cycle — recorded in a comment below

part of #341

Summary by CodeRabbit

  • Bug Fixes

    • Improved screen sizing across sidebar and top-navigation layouts.
    • Fixed Tree and Gradebook views to fill available space without unwanted overflow.
    • Preserved legitimate scrolling for content that exceeds the available height.
  • Tests

    • Added end-to-end coverage validating viewport fit, shell sizing, canvas settling, and content scrolling.

Three screens still sized themselves by subtracting a magic constant from
100vh. That subtraction can only be correct for ONE of ShellFrame's two
layouts — the sidebar variant gives `<main>` the full 100dvh, the TopNav
variant gives it 100dvh - 56px — and it is blind to the density preference
that retunes the padding tokens above it.
Tree.tsx is the one with a user-visible consequence, not just a stray
scrollbar: the row sized `calc(100vh - 240px)` wraps the element a
ResizeObserver watches, and that contentRect is passed straight through as
`<KnowledgeGraph width height>`. A mis-measured row therefore renders the
graph CANVAS at the wrong size. It now takes `flex: 1` + `minHeight: 0`
inside a FullHeightScreen root, so it absorbs exactly what `<main>` has
left after the TopBar and filter row, in either layout and at any density.
Gradebook Landing/Course used `calc(100vh - var(--row-h))` — subtracting a
DENSITY token (40/34/48px) as though it were a nav height. Both now sit in
a FullHeightScreen with `flex: 1 0 auto` on their `<main>`: fill the
remaining space, keep growing when the content is taller.
Adds frontend/e2e/tree-viewport.spec.ts, which asserts the invariant that
was silently violated — on /tree the graph canvas fits inside the shell
scrollport — in BOTH layouts, driven off the localStorage layout pref. It
lives in its own spec because graph.spec.ts declares itself data-only and
reads no geometry by design.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabaseBot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5fd64ba9-fab4-4f6b-9590-eb106f11a53c

📥 Commits

Reviewing files that changed from the base of the PR and between e700c62 and caf04b0.

📒 Files selected for processing (1)
  • frontend/e2e/viewport-fit.spec.ts
📝 Walkthrough

Walkthrough

The PR replaces viewport-based sizing with FullHeightScreen layouts for Tree and Gradebook screens. It adds Playwright tests for shell fit, graph sizing, and Gradebook content height in sidebar and top-navigation layouts.

Changes

Viewport-fit shell layout

Layer / File(s)Summary
Full-height screen integration
frontend/src/components/screens/Tree.tsx, frontend/src/components/screens/Gradebook/Course.tsx, frontend/src/components/screens/Gradebook/Landing.tsx
Tree and Gradebook screens now use FullHeightScreen. Tree uses flexible graph sizing and a non-shrinking filter row. Gradebook content uses flex growth instead of viewport-based minimum heights.
Viewport-fit end-to-end validation
frontend/e2e/viewport-fit.spec.ts
Playwright coverage validates shell geometry, settled graph height, overflow, and Gradebook content sizing across both shell layouts.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers:darkest-teddy, jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly identifies the main UI change: replacing viewport calculations with FullHeightScreen.
Description check✅ PassedThe description explains the motivation, implementation, affected screens, tests, and validation results in sufficient detail.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/341-viewport-math

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingcaf04b0Commit Preview URL

Branch Preview URL
Jul 31 2026, 02:59 AM

AndresL230and others added 3 commits July 30, 2026 19:13
Review follow-ups on the journey added with this PR.
Two-sided fit: the spec only guarded overshoot, so a future regression that
broke `flex: 1` and left the canvas a few pixels tall would have passed —
`canvasHeight > 0` is not a floor. It now asserts the canvas settles FLUSH
with the scrollport's bottom edge, which is the actual stated invariant, and
polls for it because the canvas size is ResizeObserver-driven and starts at
Tree.tsx's placeholder 900x600.
Gradebook coverage: Landing/Course got the same class of change as Tree with
no geometry assertion behind it — the gap that let the identical
`calc(100vh - N)` bug survive review on Settings.tsx during #335, since jsdom
has no layout engine and cannot catch it. Adds a second test asserting the
seeded gradebook does not manufacture scroll it does not need, in both
layouts.
Renames tree-viewport.spec.ts -> viewport-fit.spec.ts now that it covers two
screens.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ever scrolls (#341)
The first version of this test asserted the gradebook page must not scroll
at all. That is wrong: under the topnav layout the scrollport is 56px
shorter, and the seeded gradebook's content genuinely needs more than it —
scrolling there is the correct answer, and the assertion failed the FIXED
build for a legitimate reason (16px at topnav, 0 at sidebar).
What the fix actually guarantees is narrower: the container contributes no
height of its own beyond the space it was given or the height its content
needs. That is now what the test measures — <main> must be no taller than
max(available, content), and no shorter than the space available (which
would mean flex-grow is broken).
The unfixed build overshot by exactly 40px at the default density, which is
`--row-h` to the pixel — the density-token-as-nav-height bug, measured.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ght (#341)
The previous revision of this assertion passed on the UNFIXED build, which
makes it worthless as a regression test. Caught by running the spec against
the base sha before trusting it green.
Cause: it derived the content height from `main.scrollHeight`, which is
floored at the element's own client height. Any phantom height the box gave
itself was therefore mirrored into the "content" it was being compared
against, so `mainHeight <= max(available, content)` held by construction and
could never fail.
It now measures the content extent from the in-flow children (skipping
AmbientOrbs, which is position:fixed) plus the bottom padding — a number the
box's own sizing cannot influence.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Local e2e cycle — red at base, green at fix

Run as two phases inside one flock of the stack lock, at 3ee666c vs base 432c207, with SAPLING_MODEL_MODE=function.

Both phases matter: a journey that passes on unfixed code proves nothing, so phase 1 runs the new spec against the base build and requires it to fail.

Phase 1 — base 432c207 + the new spec: 2 failed (expected)

layout "topnav": canvas 480px vs scrollport 664px — overshoots by 27px
layout "sidebar": <main> is 686px but needs only 647px
(647px available, 606px of content) — 39px is phantom

Two things worth noting in those numbers:

  • The tree failure is topnav only — sidebar passed at base, which is exactly what a 240px constant calibrated for the sidebar layout would do. 27px measured against the issue's estimate of ~29px.
  • The gradebook phantom is 39px, which is --row-h (40px at the default density) to within rounding — the density-token-as-nav-height bug, measured rather than argued.

Phase 2 — fix 3ee666c: 35/35 journeys, oracles clean

35 passed (1.3m)
0 finding(s), 1 suppressed (allowlisted)

Two corrections this caught

The gradebook assertion took two revisions before it was worth keeping, both caught by the base run rather than by review:

  1. First version asserted the page must not scroll at all. Wrong — under topnav the scrollport is 56px shorter and the seeded content genuinely needs more than it, so scrolling there is correct. It failed the fixed build for a legitimate reason.
  2. Second version derived content height from main.scrollHeight, which is floored at the element's own client height — so any phantom height was mirrored into the number it was compared against, and it passed at base. Vacuous.

It now measures the content extent from the in-flow children (skipping AmbientOrbs, which is position: fixed), which the box's own sizing cannot influence.

Gates

tsc --noEmit clean · npm run lint 0 errors · npx vitest run 55 files / 399 tests passed

Follow-ups from the review of the spec delta.
Gate on the LOADED gradebook grid, not the chrome. `gradebook-transcript-open`
renders as soon as there is a user, while `loading` is still swapping a
six-card skeleton in for the real grid — measuring that transient made the
phantom-height assertion depend on fetch timing. Now waits on
role="grid"/"Courses", which only exists once the fetch resolved.
Count trailing child margins in the content measurement. A margin-bottom on
a direct in-flow child raises <main>'s height without appearing in any
child's bounding rect, which would have understated the content and turned
a real regression into a pass. Latent today; cheap to close.
Assert on the reading that settled. The tree test polled for convergence and
then measured AGAIN, reopening the window the poll existed to close. A small
settledFit() helper now returns the very reading that satisfied the
condition, and every assertion runs on that one. It also returns the last
reading on timeout, so a failure reports real numbers instead of a bare
"timed out".
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
frontend/e2e/viewport-fit.spec.ts (1)

120-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add Gradebook Course screen coverage for the same phantom-height fix.

frontend/e2e/viewport-fit.spec.ts only runs this geometry check on /gradebook. No other e2e test exercises GradebookCourseScreen path with the same <main> content-vs-container height assertion, so add a course-route test or sibling coverage to cover this regression directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/e2e/viewport-fit.spec.ts` around lines 120 - 189, Extend the
viewport-fit geometry coverage to navigate to a GradebookCourseScreen course
route in addition to /gradebook. Reuse the existing layout loop and <main>
content-versus-available-height assertions, ensuring the course screen’s route
and seeded data are loaded before measuring so the phantom-height regression is
tested directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@frontend/e2e/viewport-fit.spec.ts`:
- Around line 120-189: Extend the viewport-fit geometry coverage to navigate to
a GradebookCourseScreen course route in addition to /gradebook. Reuse the
existing layout loop and <main> content-versus-available-height assertions,
ensuring the course screen’s route and seeded data are loaded before measuring
so the phantom-height regression is tested directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 99a37da4-caf8-470e-ab54-4b0b4d647048

📥 Commits

Reviewing files that changed from the base of the PR and between 432c207 and e700c62.

📒 Files selected for processing (4)
  • frontend/e2e/viewport-fit.spec.ts
  • frontend/src/components/screens/Gradebook/Course.tsx
  • frontend/src/components/screens/Gradebook/Landing.tsx
  • frontend/src/components/screens/Tree.tsx

#341)
CI's react-hooks/rules-of-hooks rejected the helper: anything named use* is
treated as a Hook, and the spec calls it inside a for-loop over the two shell
layouts. Renamed to switchLayout — behaviour identical.
Local `npm run lint` reported 0 errors on the same code; the installed
eslint-plugin-react-hooks is older than CI's, so this was only visible there.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 162078a into mainJul 31, 2026
7 checks passed
@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Member

Automated post-merge review pass (Claude Code) on caf04b0 (the review ran while this merged — treat as follow-up input). The core verdict is positive: all three residuals from #341 are properly root-caused, the flex: 1 0 auto replacement expresses what the old minHeight: calc(...) was failing to say, the residual sweep is genuinely complete for the bug class, and the red-at-base regression journey (both layouts, overshoot and undershoot, real scrollport measurements) is exemplary.

One real regression worth a follow-up fix:

  1. Sticky TopBar travel is now clipped on the two gradebook screens (Gradebook/Landing.tsx:229, Course.tsx:519). At base, TopBar's sticky containing block was the shell <main>'s full scrollable content, so it stayed pinned for the whole scroll. At head its parent is FullHeightScreen's fixed height: 100% box — exactly one scrollport tall — so once page content exceeds ~2× the scrollport, the pinned header (breadcrumb, Curve/Letter Scale actions) detaches and scrolls away mid-page. These are precisely the screens that deliberately still page-scroll, and long assignment tables plausibly cross the threshold. Verified: FullHeightScreen.tsx sets height: "100%" with the ...style spread after it, so passing style={{ height: "auto", minHeight: "100%" }} on just these two screens restores full sticky travel while keeping the fill/phantom-height behavior the journey asserts (a min-height flex column still distributes free space to a flex: 1 0 auto child). The pinned-chat screens must keep fixed height — a grow prop on FullHeightScreen would make the two modes explicit.

Minor follow-ups:

  1. One same-class residual survives: (shell)/notetaker/page.tsx:1028minHeight: calc(100vh - 280px) on the fullscreen editor textarea (defanged by the enclosing scroll pane, but the same blind-constant pattern).
  2. viewport-fit.spec.ts measures /gradebook Landing only; Course.tsx got the identical fix but no measurement — extending the loop to the seeded /gradebook/[courseId] is cheap, and a scroll-then-assert that TopBar is still pinned on a tall Course page is the assertion that would have caught Add Personal Client-Side Definitive Study Guides #1.
  3. Tree.tsx:280-300 — the filter row got flexShrink: 0 but the TopBar header keeps default shrink; at very short viewports the TopBar is now the only compressible child and squashes (previously the page scrolled). flexShrink: 0 on the header keeps the chrome intact.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@AndresL230@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(ui): replace viewport-math residuals with FullHeightScreen (#341) - #487

Merged
AndresL230 merged 6 commits into
mainfrom
feat/341-viewport-math
Jul 31, 2026
Merged

fix(ui): replace viewport-math residuals with FullHeightScreen (#341)#487
AndresL230 merged 6 commits into
mainfrom
feat/341-viewport-math

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Replaces the three surviving calc(100vh - …) residuals with FullHeightScreen + flex, and adds the journey that proves the one with a user-visible consequence.

Why these were wrong

ShellFrame renders two layouts: a sidebar (its <main> is the full 100dvh) or a horizontal TopNav above <main> (so <main> is 100dvh - 56px). Any constant subtracted from 100vh can only be calibrated for one of them — and none of them accounted for the density preference, which retunes the padding tokens of the chrome being subtracted.

The consequential one — Tree.tsx

height: calc(100vh - 240px) wrapped the element a ResizeObserver watches; its contentRect is passed straight through as <KnowledgeGraph width height>. So this was not a stray scrollbar — the graph canvas itself rendered at the wrong size in whichever layout the constant wasn't tuned for.

Now: FullHeightScreen root + flex: 1 / minHeight: 0 on the graph row, so it absorbs exactly what <main> has left below the TopBar and filter row. Correct in both layouts, at any density, by construction rather than by calibration.

The semantic ones — Gradebook Landing + Course

minHeight: calc(100vh - var(--row-h)) subtracted a density token (40/34/48px per globals.css) as if it were a nav height. Both now sit in a FullHeightScreen with flex: 1 0 auto on their <main> — fill the remaining space, keep growing when content is taller (the minHeight semantic, preserved).

Test

frontend/e2e/tree-viewport.spec.ts asserts the invariant that was violated: on /tree the graph canvas fits inside the shell scrollport — no overshoot, no scrollable overflow — in both layouts, driven off the sapling_layout localStorage pref. It is a new spec rather than an addition to graph.spec.ts, which declares itself data-only and deliberately reads no x/y.

FullscreenGraph (the overlay variant) already used flex: 1 and is untouched.

Gates

  • tsc --noEmit — clean
  • npm run lint — 0 errors (36 pre-existing warnings)
  • npx vitest run — 55 files, 399 tests passed
  • Full local e2e cycle — recorded in a comment below

part of #341

Summary by CodeRabbit

  • Bug Fixes

    • Improved screen sizing across sidebar and top-navigation layouts.
    • Fixed Tree and Gradebook views to fill available space without unwanted overflow.
    • Preserved legitimate scrolling for content that exceeds the available height.
  • Tests

    • Added end-to-end coverage validating viewport fit, shell sizing, canvas settling, and content scrolling.

Three screens still sized themselves by subtracting a magic constant from
100vh. That subtraction can only be correct for ONE of ShellFrame's two
layouts — the sidebar variant gives `<main>` the full 100dvh, the TopNav
variant gives it 100dvh - 56px — and it is blind to the density preference
that retunes the padding tokens above it.
Tree.tsx is the one with a user-visible consequence, not just a stray
scrollbar: the row sized `calc(100vh - 240px)` wraps the element a
ResizeObserver watches, and that contentRect is passed straight through as
`<KnowledgeGraph width height>`. A mis-measured row therefore renders the
graph CANVAS at the wrong size. It now takes `flex: 1` + `minHeight: 0`
inside a FullHeightScreen root, so it absorbs exactly what `<main>` has
left after the TopBar and filter row, in either layout and at any density.
Gradebook Landing/Course used `calc(100vh - var(--row-h))` — subtracting a
DENSITY token (40/34/48px) as though it were a nav height. Both now sit in
a FullHeightScreen with `flex: 1 0 auto` on their `<main>`: fill the
remaining space, keep growing when the content is taller.
Adds frontend/e2e/tree-viewport.spec.ts, which asserts the invariant that
was silently violated — on /tree the graph canvas fits inside the shell
scrollport — in BOTH layouts, driven off the localStorage layout pref. It
lives in its own spec because graph.spec.ts declares itself data-only and
reads no geometry by design.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabaseBot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5fd64ba9-fab4-4f6b-9590-eb106f11a53c

📥 Commits

Reviewing files that changed from the base of the PR and between e700c62 and caf04b0.

📒 Files selected for processing (1)
  • frontend/e2e/viewport-fit.spec.ts
📝 Walkthrough

Walkthrough

The PR replaces viewport-based sizing with FullHeightScreen layouts for Tree and Gradebook screens. It adds Playwright tests for shell fit, graph sizing, and Gradebook content height in sidebar and top-navigation layouts.

Changes

Viewport-fit shell layout

Layer / File(s)Summary
Full-height screen integration
frontend/src/components/screens/Tree.tsx, frontend/src/components/screens/Gradebook/Course.tsx, frontend/src/components/screens/Gradebook/Landing.tsx
Tree and Gradebook screens now use FullHeightScreen. Tree uses flexible graph sizing and a non-shrinking filter row. Gradebook content uses flex growth instead of viewport-based minimum heights.
Viewport-fit end-to-end validation
frontend/e2e/viewport-fit.spec.ts
Playwright coverage validates shell geometry, settled graph height, overflow, and Gradebook content sizing across both shell layouts.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers:darkest-teddy, jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly identifies the main UI change: replacing viewport calculations with FullHeightScreen.
Description check✅ PassedThe description explains the motivation, implementation, affected screens, tests, and validation results in sufficient detail.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/341-viewport-math

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingcaf04b0Commit Preview URL

Branch Preview URL
Jul 31 2026, 02:59 AM

AndresL230and others added 3 commits July 30, 2026 19:13
Review follow-ups on the journey added with this PR.
Two-sided fit: the spec only guarded overshoot, so a future regression that
broke `flex: 1` and left the canvas a few pixels tall would have passed —
`canvasHeight > 0` is not a floor. It now asserts the canvas settles FLUSH
with the scrollport's bottom edge, which is the actual stated invariant, and
polls for it because the canvas size is ResizeObserver-driven and starts at
Tree.tsx's placeholder 900x600.
Gradebook coverage: Landing/Course got the same class of change as Tree with
no geometry assertion behind it — the gap that let the identical
`calc(100vh - N)` bug survive review on Settings.tsx during #335, since jsdom
has no layout engine and cannot catch it. Adds a second test asserting the
seeded gradebook does not manufacture scroll it does not need, in both
layouts.
Renames tree-viewport.spec.ts -> viewport-fit.spec.ts now that it covers two
screens.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ever scrolls (#341)
The first version of this test asserted the gradebook page must not scroll
at all. That is wrong: under the topnav layout the scrollport is 56px
shorter, and the seeded gradebook's content genuinely needs more than it —
scrolling there is the correct answer, and the assertion failed the FIXED
build for a legitimate reason (16px at topnav, 0 at sidebar).
What the fix actually guarantees is narrower: the container contributes no
height of its own beyond the space it was given or the height its content
needs. That is now what the test measures — <main> must be no taller than
max(available, content), and no shorter than the space available (which
would mean flex-grow is broken).
The unfixed build overshot by exactly 40px at the default density, which is
`--row-h` to the pixel — the density-token-as-nav-height bug, measured.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ght (#341)
The previous revision of this assertion passed on the UNFIXED build, which
makes it worthless as a regression test. Caught by running the spec against
the base sha before trusting it green.
Cause: it derived the content height from `main.scrollHeight`, which is
floored at the element's own client height. Any phantom height the box gave
itself was therefore mirrored into the "content" it was being compared
against, so `mainHeight <= max(available, content)` held by construction and
could never fail.
It now measures the content extent from the in-flow children (skipping
AmbientOrbs, which is position:fixed) plus the bottom padding — a number the
box's own sizing cannot influence.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Local e2e cycle — red at base, green at fix

Run as two phases inside one flock of the stack lock, at 3ee666c vs base 432c207, with SAPLING_MODEL_MODE=function.

Both phases matter: a journey that passes on unfixed code proves nothing, so phase 1 runs the new spec against the base build and requires it to fail.

Phase 1 — base 432c207 + the new spec: 2 failed (expected)

layout "topnav": canvas 480px vs scrollport 664px — overshoots by 27px
layout "sidebar": <main> is 686px but needs only 647px
(647px available, 606px of content) — 39px is phantom

Two things worth noting in those numbers:

  • The tree failure is topnav only — sidebar passed at base, which is exactly what a 240px constant calibrated for the sidebar layout would do. 27px measured against the issue's estimate of ~29px.
  • The gradebook phantom is 39px, which is --row-h (40px at the default density) to within rounding — the density-token-as-nav-height bug, measured rather than argued.

Phase 2 — fix 3ee666c: 35/35 journeys, oracles clean

35 passed (1.3m)
0 finding(s), 1 suppressed (allowlisted)

Two corrections this caught

The gradebook assertion took two revisions before it was worth keeping, both caught by the base run rather than by review:

  1. First version asserted the page must not scroll at all. Wrong — under topnav the scrollport is 56px shorter and the seeded content genuinely needs more than it, so scrolling there is correct. It failed the fixed build for a legitimate reason.
  2. Second version derived content height from main.scrollHeight, which is floored at the element's own client height — so any phantom height was mirrored into the number it was compared against, and it passed at base. Vacuous.

It now measures the content extent from the in-flow children (skipping AmbientOrbs, which is position: fixed), which the box's own sizing cannot influence.

Gates

tsc --noEmit clean · npm run lint 0 errors · npx vitest run 55 files / 399 tests passed

Follow-ups from the review of the spec delta.
Gate on the LOADED gradebook grid, not the chrome. `gradebook-transcript-open`
renders as soon as there is a user, while `loading` is still swapping a
six-card skeleton in for the real grid — measuring that transient made the
phantom-height assertion depend on fetch timing. Now waits on
role="grid"/"Courses", which only exists once the fetch resolved.
Count trailing child margins in the content measurement. A margin-bottom on
a direct in-flow child raises <main>'s height without appearing in any
child's bounding rect, which would have understated the content and turned
a real regression into a pass. Latent today; cheap to close.
Assert on the reading that settled. The tree test polled for convergence and
then measured AGAIN, reopening the window the poll existed to close. A small
settledFit() helper now returns the very reading that satisfied the
condition, and every assertion runs on that one. It also returns the last
reading on timeout, so a failure reports real numbers instead of a bare
"timed out".
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
frontend/e2e/viewport-fit.spec.ts (1)

120-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add Gradebook Course screen coverage for the same phantom-height fix.

frontend/e2e/viewport-fit.spec.ts only runs this geometry check on /gradebook. No other e2e test exercises GradebookCourseScreen path with the same <main> content-vs-container height assertion, so add a course-route test or sibling coverage to cover this regression directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/e2e/viewport-fit.spec.ts` around lines 120 - 189, Extend the
viewport-fit geometry coverage to navigate to a GradebookCourseScreen course
route in addition to /gradebook. Reuse the existing layout loop and <main>
content-versus-available-height assertions, ensuring the course screen’s route
and seeded data are loaded before measuring so the phantom-height regression is
tested directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@frontend/e2e/viewport-fit.spec.ts`:
- Around line 120-189: Extend the viewport-fit geometry coverage to navigate to
a GradebookCourseScreen course route in addition to /gradebook. Reuse the
existing layout loop and <main> content-versus-available-height assertions,
ensuring the course screen’s route and seeded data are loaded before measuring
so the phantom-height regression is tested directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 99a37da4-caf8-470e-ab54-4b0b4d647048

📥 Commits

Reviewing files that changed from the base of the PR and between 432c207 and e700c62.

📒 Files selected for processing (4)
  • frontend/e2e/viewport-fit.spec.ts
  • frontend/src/components/screens/Gradebook/Course.tsx
  • frontend/src/components/screens/Gradebook/Landing.tsx
  • frontend/src/components/screens/Tree.tsx

#341)
CI's react-hooks/rules-of-hooks rejected the helper: anything named use* is
treated as a Hook, and the spec calls it inside a for-loop over the two shell
layouts. Renamed to switchLayout — behaviour identical.
Local `npm run lint` reported 0 errors on the same code; the installed
eslint-plugin-react-hooks is older than CI's, so this was only visible there.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 162078a into mainJul 31, 2026
7 checks passed
@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Member

Automated post-merge review pass (Claude Code) on caf04b0 (the review ran while this merged — treat as follow-up input). The core verdict is positive: all three residuals from #341 are properly root-caused, the flex: 1 0 auto replacement expresses what the old minHeight: calc(...) was failing to say, the residual sweep is genuinely complete for the bug class, and the red-at-base regression journey (both layouts, overshoot and undershoot, real scrollport measurements) is exemplary.

One real regression worth a follow-up fix:

  1. Sticky TopBar travel is now clipped on the two gradebook screens (Gradebook/Landing.tsx:229, Course.tsx:519). At base, TopBar's sticky containing block was the shell <main>'s full scrollable content, so it stayed pinned for the whole scroll. At head its parent is FullHeightScreen's fixed height: 100% box — exactly one scrollport tall — so once page content exceeds ~2× the scrollport, the pinned header (breadcrumb, Curve/Letter Scale actions) detaches and scrolls away mid-page. These are precisely the screens that deliberately still page-scroll, and long assignment tables plausibly cross the threshold. Verified: FullHeightScreen.tsx sets height: "100%" with the ...style spread after it, so passing style={{ height: "auto", minHeight: "100%" }} on just these two screens restores full sticky travel while keeping the fill/phantom-height behavior the journey asserts (a min-height flex column still distributes free space to a flex: 1 0 auto child). The pinned-chat screens must keep fixed height — a grow prop on FullHeightScreen would make the two modes explicit.

Minor follow-ups:

  1. One same-class residual survives: (shell)/notetaker/page.tsx:1028minHeight: calc(100vh - 280px) on the fullscreen editor textarea (defanged by the enclosing scroll pane, but the same blind-constant pattern).
  2. viewport-fit.spec.ts measures /gradebook Landing only; Course.tsx got the identical fix but no measurement — extending the loop to the seeded /gradebook/[courseId] is cheap, and a scroll-then-assert that TopBar is still pinned on a tall Course page is the assertion that would have caught Add Personal Client-Side Definitive Study Guides #1.
  3. Tree.tsx:280-300 — the filter row got flexShrink: 0 but the TopBar header keeps default shrink; at very short viewports the TopBar is now the only compressible child and squashes (previously the page scrolled). flexShrink: 0 on the header keeps the chrome intact.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@AndresL230@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(ui): replace viewport-math residuals with FullHeightScreen (#341) - #487

Merged
AndresL230 merged 6 commits into
mainfrom
feat/341-viewport-math
Jul 31, 2026
Merged

fix(ui): replace viewport-math residuals with FullHeightScreen (#341)#487
AndresL230 merged 6 commits into
mainfrom
feat/341-viewport-math

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Replaces the three surviving calc(100vh - …) residuals with FullHeightScreen + flex, and adds the journey that proves the one with a user-visible consequence.

Why these were wrong

ShellFrame renders two layouts: a sidebar (its <main> is the full 100dvh) or a horizontal TopNav above <main> (so <main> is 100dvh - 56px). Any constant subtracted from 100vh can only be calibrated for one of them — and none of them accounted for the density preference, which retunes the padding tokens of the chrome being subtracted.

The consequential one — Tree.tsx

height: calc(100vh - 240px) wrapped the element a ResizeObserver watches; its contentRect is passed straight through as <KnowledgeGraph width height>. So this was not a stray scrollbar — the graph canvas itself rendered at the wrong size in whichever layout the constant wasn't tuned for.

Now: FullHeightScreen root + flex: 1 / minHeight: 0 on the graph row, so it absorbs exactly what <main> has left below the TopBar and filter row. Correct in both layouts, at any density, by construction rather than by calibration.

The semantic ones — Gradebook Landing + Course

minHeight: calc(100vh - var(--row-h)) subtracted a density token (40/34/48px per globals.css) as if it were a nav height. Both now sit in a FullHeightScreen with flex: 1 0 auto on their <main> — fill the remaining space, keep growing when content is taller (the minHeight semantic, preserved).

Test

frontend/e2e/tree-viewport.spec.ts asserts the invariant that was violated: on /tree the graph canvas fits inside the shell scrollport — no overshoot, no scrollable overflow — in both layouts, driven off the sapling_layout localStorage pref. It is a new spec rather than an addition to graph.spec.ts, which declares itself data-only and deliberately reads no x/y.

FullscreenGraph (the overlay variant) already used flex: 1 and is untouched.

Gates

  • tsc --noEmit — clean
  • npm run lint — 0 errors (36 pre-existing warnings)
  • npx vitest run — 55 files, 399 tests passed
  • Full local e2e cycle — recorded in a comment below

part of #341

Summary by CodeRabbit

  • Bug Fixes

    • Improved screen sizing across sidebar and top-navigation layouts.
    • Fixed Tree and Gradebook views to fill available space without unwanted overflow.
    • Preserved legitimate scrolling for content that exceeds the available height.
  • Tests

    • Added end-to-end coverage validating viewport fit, shell sizing, canvas settling, and content scrolling.

Three screens still sized themselves by subtracting a magic constant from
100vh. That subtraction can only be correct for ONE of ShellFrame's two
layouts — the sidebar variant gives `<main>` the full 100dvh, the TopNav
variant gives it 100dvh - 56px — and it is blind to the density preference
that retunes the padding tokens above it.
Tree.tsx is the one with a user-visible consequence, not just a stray
scrollbar: the row sized `calc(100vh - 240px)` wraps the element a
ResizeObserver watches, and that contentRect is passed straight through as
`<KnowledgeGraph width height>`. A mis-measured row therefore renders the
graph CANVAS at the wrong size. It now takes `flex: 1` + `minHeight: 0`
inside a FullHeightScreen root, so it absorbs exactly what `<main>` has
left after the TopBar and filter row, in either layout and at any density.
Gradebook Landing/Course used `calc(100vh - var(--row-h))` — subtracting a
DENSITY token (40/34/48px) as though it were a nav height. Both now sit in
a FullHeightScreen with `flex: 1 0 auto` on their `<main>`: fill the
remaining space, keep growing when the content is taller.
Adds frontend/e2e/tree-viewport.spec.ts, which asserts the invariant that
was silently violated — on /tree the graph canvas fits inside the shell
scrollport — in BOTH layouts, driven off the localStorage layout pref. It
lives in its own spec because graph.spec.ts declares itself data-only and
reads no geometry by design.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabaseBot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5fd64ba9-fab4-4f6b-9590-eb106f11a53c

📥 Commits

Reviewing files that changed from the base of the PR and between e700c62 and caf04b0.

📒 Files selected for processing (1)
  • frontend/e2e/viewport-fit.spec.ts
📝 Walkthrough

Walkthrough

The PR replaces viewport-based sizing with FullHeightScreen layouts for Tree and Gradebook screens. It adds Playwright tests for shell fit, graph sizing, and Gradebook content height in sidebar and top-navigation layouts.

Changes

Viewport-fit shell layout

Layer / File(s)Summary
Full-height screen integration
frontend/src/components/screens/Tree.tsx, frontend/src/components/screens/Gradebook/Course.tsx, frontend/src/components/screens/Gradebook/Landing.tsx
Tree and Gradebook screens now use FullHeightScreen. Tree uses flexible graph sizing and a non-shrinking filter row. Gradebook content uses flex growth instead of viewport-based minimum heights.
Viewport-fit end-to-end validation
frontend/e2e/viewport-fit.spec.ts
Playwright coverage validates shell geometry, settled graph height, overflow, and Gradebook content sizing across both shell layouts.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers:darkest-teddy, jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly identifies the main UI change: replacing viewport calculations with FullHeightScreen.
Description check✅ PassedThe description explains the motivation, implementation, affected screens, tests, and validation results in sufficient detail.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/341-viewport-math

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingcaf04b0Commit Preview URL

Branch Preview URL
Jul 31 2026, 02:59 AM

AndresL230and others added 3 commits July 30, 2026 19:13
Review follow-ups on the journey added with this PR.
Two-sided fit: the spec only guarded overshoot, so a future regression that
broke `flex: 1` and left the canvas a few pixels tall would have passed —
`canvasHeight > 0` is not a floor. It now asserts the canvas settles FLUSH
with the scrollport's bottom edge, which is the actual stated invariant, and
polls for it because the canvas size is ResizeObserver-driven and starts at
Tree.tsx's placeholder 900x600.
Gradebook coverage: Landing/Course got the same class of change as Tree with
no geometry assertion behind it — the gap that let the identical
`calc(100vh - N)` bug survive review on Settings.tsx during #335, since jsdom
has no layout engine and cannot catch it. Adds a second test asserting the
seeded gradebook does not manufacture scroll it does not need, in both
layouts.
Renames tree-viewport.spec.ts -> viewport-fit.spec.ts now that it covers two
screens.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ever scrolls (#341)
The first version of this test asserted the gradebook page must not scroll
at all. That is wrong: under the topnav layout the scrollport is 56px
shorter, and the seeded gradebook's content genuinely needs more than it —
scrolling there is the correct answer, and the assertion failed the FIXED
build for a legitimate reason (16px at topnav, 0 at sidebar).
What the fix actually guarantees is narrower: the container contributes no
height of its own beyond the space it was given or the height its content
needs. That is now what the test measures — <main> must be no taller than
max(available, content), and no shorter than the space available (which
would mean flex-grow is broken).
The unfixed build overshot by exactly 40px at the default density, which is
`--row-h` to the pixel — the density-token-as-nav-height bug, measured.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ght (#341)
The previous revision of this assertion passed on the UNFIXED build, which
makes it worthless as a regression test. Caught by running the spec against
the base sha before trusting it green.
Cause: it derived the content height from `main.scrollHeight`, which is
floored at the element's own client height. Any phantom height the box gave
itself was therefore mirrored into the "content" it was being compared
against, so `mainHeight <= max(available, content)` held by construction and
could never fail.
It now measures the content extent from the in-flow children (skipping
AmbientOrbs, which is position:fixed) plus the bottom padding — a number the
box's own sizing cannot influence.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Local e2e cycle — red at base, green at fix

Run as two phases inside one flock of the stack lock, at 3ee666c vs base 432c207, with SAPLING_MODEL_MODE=function.

Both phases matter: a journey that passes on unfixed code proves nothing, so phase 1 runs the new spec against the base build and requires it to fail.

Phase 1 — base 432c207 + the new spec: 2 failed (expected)

layout "topnav": canvas 480px vs scrollport 664px — overshoots by 27px
layout "sidebar": <main> is 686px but needs only 647px
(647px available, 606px of content) — 39px is phantom

Two things worth noting in those numbers:

  • The tree failure is topnav only — sidebar passed at base, which is exactly what a 240px constant calibrated for the sidebar layout would do. 27px measured against the issue's estimate of ~29px.
  • The gradebook phantom is 39px, which is --row-h (40px at the default density) to within rounding — the density-token-as-nav-height bug, measured rather than argued.

Phase 2 — fix 3ee666c: 35/35 journeys, oracles clean

35 passed (1.3m)
0 finding(s), 1 suppressed (allowlisted)

Two corrections this caught

The gradebook assertion took two revisions before it was worth keeping, both caught by the base run rather than by review:

  1. First version asserted the page must not scroll at all. Wrong — under topnav the scrollport is 56px shorter and the seeded content genuinely needs more than it, so scrolling there is correct. It failed the fixed build for a legitimate reason.
  2. Second version derived content height from main.scrollHeight, which is floored at the element's own client height — so any phantom height was mirrored into the number it was compared against, and it passed at base. Vacuous.

It now measures the content extent from the in-flow children (skipping AmbientOrbs, which is position: fixed), which the box's own sizing cannot influence.

Gates

tsc --noEmit clean · npm run lint 0 errors · npx vitest run 55 files / 399 tests passed

Follow-ups from the review of the spec delta.
Gate on the LOADED gradebook grid, not the chrome. `gradebook-transcript-open`
renders as soon as there is a user, while `loading` is still swapping a
six-card skeleton in for the real grid — measuring that transient made the
phantom-height assertion depend on fetch timing. Now waits on
role="grid"/"Courses", which only exists once the fetch resolved.
Count trailing child margins in the content measurement. A margin-bottom on
a direct in-flow child raises <main>'s height without appearing in any
child's bounding rect, which would have understated the content and turned
a real regression into a pass. Latent today; cheap to close.
Assert on the reading that settled. The tree test polled for convergence and
then measured AGAIN, reopening the window the poll existed to close. A small
settledFit() helper now returns the very reading that satisfied the
condition, and every assertion runs on that one. It also returns the last
reading on timeout, so a failure reports real numbers instead of a bare
"timed out".
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
frontend/e2e/viewport-fit.spec.ts (1)

120-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add Gradebook Course screen coverage for the same phantom-height fix.

frontend/e2e/viewport-fit.spec.ts only runs this geometry check on /gradebook. No other e2e test exercises GradebookCourseScreen path with the same <main> content-vs-container height assertion, so add a course-route test or sibling coverage to cover this regression directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/e2e/viewport-fit.spec.ts` around lines 120 - 189, Extend the
viewport-fit geometry coverage to navigate to a GradebookCourseScreen course
route in addition to /gradebook. Reuse the existing layout loop and <main>
content-versus-available-height assertions, ensuring the course screen’s route
and seeded data are loaded before measuring so the phantom-height regression is
tested directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@frontend/e2e/viewport-fit.spec.ts`:
- Around line 120-189: Extend the viewport-fit geometry coverage to navigate to
a GradebookCourseScreen course route in addition to /gradebook. Reuse the
existing layout loop and <main> content-versus-available-height assertions,
ensuring the course screen’s route and seeded data are loaded before measuring
so the phantom-height regression is tested directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 99a37da4-caf8-470e-ab54-4b0b4d647048

📥 Commits

Reviewing files that changed from the base of the PR and between 432c207 and e700c62.

📒 Files selected for processing (4)
  • frontend/e2e/viewport-fit.spec.ts
  • frontend/src/components/screens/Gradebook/Course.tsx
  • frontend/src/components/screens/Gradebook/Landing.tsx
  • frontend/src/components/screens/Tree.tsx

#341)
CI's react-hooks/rules-of-hooks rejected the helper: anything named use* is
treated as a Hook, and the spec calls it inside a for-loop over the two shell
layouts. Renamed to switchLayout — behaviour identical.
Local `npm run lint` reported 0 errors on the same code; the installed
eslint-plugin-react-hooks is older than CI's, so this was only visible there.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 162078a into mainJul 31, 2026
7 checks passed
@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Member

Automated post-merge review pass (Claude Code) on caf04b0 (the review ran while this merged — treat as follow-up input). The core verdict is positive: all three residuals from #341 are properly root-caused, the flex: 1 0 auto replacement expresses what the old minHeight: calc(...) was failing to say, the residual sweep is genuinely complete for the bug class, and the red-at-base regression journey (both layouts, overshoot and undershoot, real scrollport measurements) is exemplary.

One real regression worth a follow-up fix:

  1. Sticky TopBar travel is now clipped on the two gradebook screens (Gradebook/Landing.tsx:229, Course.tsx:519). At base, TopBar's sticky containing block was the shell <main>'s full scrollable content, so it stayed pinned for the whole scroll. At head its parent is FullHeightScreen's fixed height: 100% box — exactly one scrollport tall — so once page content exceeds ~2× the scrollport, the pinned header (breadcrumb, Curve/Letter Scale actions) detaches and scrolls away mid-page. These are precisely the screens that deliberately still page-scroll, and long assignment tables plausibly cross the threshold. Verified: FullHeightScreen.tsx sets height: "100%" with the ...style spread after it, so passing style={{ height: "auto", minHeight: "100%" }} on just these two screens restores full sticky travel while keeping the fill/phantom-height behavior the journey asserts (a min-height flex column still distributes free space to a flex: 1 0 auto child). The pinned-chat screens must keep fixed height — a grow prop on FullHeightScreen would make the two modes explicit.

Minor follow-ups:

  1. One same-class residual survives: (shell)/notetaker/page.tsx:1028minHeight: calc(100vh - 280px) on the fullscreen editor textarea (defanged by the enclosing scroll pane, but the same blind-constant pattern).
  2. viewport-fit.spec.ts measures /gradebook Landing only; Course.tsx got the identical fix but no measurement — extending the loop to the seeded /gradebook/[courseId] is cheap, and a scroll-then-assert that TopBar is still pinned on a tall Course page is the assertion that would have caught Add Personal Client-Side Definitive Study Guides #1.
  3. Tree.tsx:280-300 — the filter row got flexShrink: 0 but the TopBar header keeps default shrink; at very short viewports the TopBar is now the only compressible child and squashes (previously the page scrolled). flexShrink: 0 on the header keeps the chrome intact.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@AndresL230@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(ui): replace viewport-math residuals with FullHeightScreen (#341) - #487

Merged
AndresL230 merged 6 commits into
mainfrom
feat/341-viewport-math
Jul 31, 2026
Merged

fix(ui): replace viewport-math residuals with FullHeightScreen (#341)#487
AndresL230 merged 6 commits into
mainfrom
feat/341-viewport-math

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Replaces the three surviving calc(100vh - …) residuals with FullHeightScreen + flex, and adds the journey that proves the one with a user-visible consequence.

Why these were wrong

ShellFrame renders two layouts: a sidebar (its <main> is the full 100dvh) or a horizontal TopNav above <main> (so <main> is 100dvh - 56px). Any constant subtracted from 100vh can only be calibrated for one of them — and none of them accounted for the density preference, which retunes the padding tokens of the chrome being subtracted.

The consequential one — Tree.tsx

height: calc(100vh - 240px) wrapped the element a ResizeObserver watches; its contentRect is passed straight through as <KnowledgeGraph width height>. So this was not a stray scrollbar — the graph canvas itself rendered at the wrong size in whichever layout the constant wasn't tuned for.

Now: FullHeightScreen root + flex: 1 / minHeight: 0 on the graph row, so it absorbs exactly what <main> has left below the TopBar and filter row. Correct in both layouts, at any density, by construction rather than by calibration.

The semantic ones — Gradebook Landing + Course

minHeight: calc(100vh - var(--row-h)) subtracted a density token (40/34/48px per globals.css) as if it were a nav height. Both now sit in a FullHeightScreen with flex: 1 0 auto on their <main> — fill the remaining space, keep growing when content is taller (the minHeight semantic, preserved).

Test

frontend/e2e/tree-viewport.spec.ts asserts the invariant that was violated: on /tree the graph canvas fits inside the shell scrollport — no overshoot, no scrollable overflow — in both layouts, driven off the sapling_layout localStorage pref. It is a new spec rather than an addition to graph.spec.ts, which declares itself data-only and deliberately reads no x/y.

FullscreenGraph (the overlay variant) already used flex: 1 and is untouched.

Gates

  • tsc --noEmit — clean
  • npm run lint — 0 errors (36 pre-existing warnings)
  • npx vitest run — 55 files, 399 tests passed
  • Full local e2e cycle — recorded in a comment below

part of #341

Summary by CodeRabbit

  • Bug Fixes

    • Improved screen sizing across sidebar and top-navigation layouts.
    • Fixed Tree and Gradebook views to fill available space without unwanted overflow.
    • Preserved legitimate scrolling for content that exceeds the available height.
  • Tests

    • Added end-to-end coverage validating viewport fit, shell sizing, canvas settling, and content scrolling.

Three screens still sized themselves by subtracting a magic constant from
100vh. That subtraction can only be correct for ONE of ShellFrame's two
layouts — the sidebar variant gives `<main>` the full 100dvh, the TopNav
variant gives it 100dvh - 56px — and it is blind to the density preference
that retunes the padding tokens above it.
Tree.tsx is the one with a user-visible consequence, not just a stray
scrollbar: the row sized `calc(100vh - 240px)` wraps the element a
ResizeObserver watches, and that contentRect is passed straight through as
`<KnowledgeGraph width height>`. A mis-measured row therefore renders the
graph CANVAS at the wrong size. It now takes `flex: 1` + `minHeight: 0`
inside a FullHeightScreen root, so it absorbs exactly what `<main>` has
left after the TopBar and filter row, in either layout and at any density.
Gradebook Landing/Course used `calc(100vh - var(--row-h))` — subtracting a
DENSITY token (40/34/48px) as though it were a nav height. Both now sit in
a FullHeightScreen with `flex: 1 0 auto` on their `<main>`: fill the
remaining space, keep growing when the content is taller.
Adds frontend/e2e/tree-viewport.spec.ts, which asserts the invariant that
was silently violated — on /tree the graph canvas fits inside the shell
scrollport — in BOTH layouts, driven off the localStorage layout pref. It
lives in its own spec because graph.spec.ts declares itself data-only and
reads no geometry by design.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabaseBot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5fd64ba9-fab4-4f6b-9590-eb106f11a53c

📥 Commits

Reviewing files that changed from the base of the PR and between e700c62 and caf04b0.

📒 Files selected for processing (1)
  • frontend/e2e/viewport-fit.spec.ts
📝 Walkthrough

Walkthrough

The PR replaces viewport-based sizing with FullHeightScreen layouts for Tree and Gradebook screens. It adds Playwright tests for shell fit, graph sizing, and Gradebook content height in sidebar and top-navigation layouts.

Changes

Viewport-fit shell layout

Layer / File(s)Summary
Full-height screen integration
frontend/src/components/screens/Tree.tsx, frontend/src/components/screens/Gradebook/Course.tsx, frontend/src/components/screens/Gradebook/Landing.tsx
Tree and Gradebook screens now use FullHeightScreen. Tree uses flexible graph sizing and a non-shrinking filter row. Gradebook content uses flex growth instead of viewport-based minimum heights.
Viewport-fit end-to-end validation
frontend/e2e/viewport-fit.spec.ts
Playwright coverage validates shell geometry, settled graph height, overflow, and Gradebook content sizing across both shell layouts.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers:darkest-teddy, jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly identifies the main UI change: replacing viewport calculations with FullHeightScreen.
Description check✅ PassedThe description explains the motivation, implementation, affected screens, tests, and validation results in sufficient detail.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/341-viewport-math

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingcaf04b0Commit Preview URL

Branch Preview URL
Jul 31 2026, 02:59 AM

AndresL230and others added 3 commits July 30, 2026 19:13
Review follow-ups on the journey added with this PR.
Two-sided fit: the spec only guarded overshoot, so a future regression that
broke `flex: 1` and left the canvas a few pixels tall would have passed —
`canvasHeight > 0` is not a floor. It now asserts the canvas settles FLUSH
with the scrollport's bottom edge, which is the actual stated invariant, and
polls for it because the canvas size is ResizeObserver-driven and starts at
Tree.tsx's placeholder 900x600.
Gradebook coverage: Landing/Course got the same class of change as Tree with
no geometry assertion behind it — the gap that let the identical
`calc(100vh - N)` bug survive review on Settings.tsx during #335, since jsdom
has no layout engine and cannot catch it. Adds a second test asserting the
seeded gradebook does not manufacture scroll it does not need, in both
layouts.
Renames tree-viewport.spec.ts -> viewport-fit.spec.ts now that it covers two
screens.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ever scrolls (#341)
The first version of this test asserted the gradebook page must not scroll
at all. That is wrong: under the topnav layout the scrollport is 56px
shorter, and the seeded gradebook's content genuinely needs more than it —
scrolling there is the correct answer, and the assertion failed the FIXED
build for a legitimate reason (16px at topnav, 0 at sidebar).
What the fix actually guarantees is narrower: the container contributes no
height of its own beyond the space it was given or the height its content
needs. That is now what the test measures — <main> must be no taller than
max(available, content), and no shorter than the space available (which
would mean flex-grow is broken).
The unfixed build overshot by exactly 40px at the default density, which is
`--row-h` to the pixel — the density-token-as-nav-height bug, measured.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ght (#341)
The previous revision of this assertion passed on the UNFIXED build, which
makes it worthless as a regression test. Caught by running the spec against
the base sha before trusting it green.
Cause: it derived the content height from `main.scrollHeight`, which is
floored at the element's own client height. Any phantom height the box gave
itself was therefore mirrored into the "content" it was being compared
against, so `mainHeight <= max(available, content)` held by construction and
could never fail.
It now measures the content extent from the in-flow children (skipping
AmbientOrbs, which is position:fixed) plus the bottom padding — a number the
box's own sizing cannot influence.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Local e2e cycle — red at base, green at fix

Run as two phases inside one flock of the stack lock, at 3ee666c vs base 432c207, with SAPLING_MODEL_MODE=function.

Both phases matter: a journey that passes on unfixed code proves nothing, so phase 1 runs the new spec against the base build and requires it to fail.

Phase 1 — base 432c207 + the new spec: 2 failed (expected)

layout "topnav": canvas 480px vs scrollport 664px — overshoots by 27px
layout "sidebar": <main> is 686px but needs only 647px
(647px available, 606px of content) — 39px is phantom

Two things worth noting in those numbers:

  • The tree failure is topnav only — sidebar passed at base, which is exactly what a 240px constant calibrated for the sidebar layout would do. 27px measured against the issue's estimate of ~29px.
  • The gradebook phantom is 39px, which is --row-h (40px at the default density) to within rounding — the density-token-as-nav-height bug, measured rather than argued.

Phase 2 — fix 3ee666c: 35/35 journeys, oracles clean

35 passed (1.3m)
0 finding(s), 1 suppressed (allowlisted)

Two corrections this caught

The gradebook assertion took two revisions before it was worth keeping, both caught by the base run rather than by review:

  1. First version asserted the page must not scroll at all. Wrong — under topnav the scrollport is 56px shorter and the seeded content genuinely needs more than it, so scrolling there is correct. It failed the fixed build for a legitimate reason.
  2. Second version derived content height from main.scrollHeight, which is floored at the element's own client height — so any phantom height was mirrored into the number it was compared against, and it passed at base. Vacuous.

It now measures the content extent from the in-flow children (skipping AmbientOrbs, which is position: fixed), which the box's own sizing cannot influence.

Gates

tsc --noEmit clean · npm run lint 0 errors · npx vitest run 55 files / 399 tests passed

Follow-ups from the review of the spec delta.
Gate on the LOADED gradebook grid, not the chrome. `gradebook-transcript-open`
renders as soon as there is a user, while `loading` is still swapping a
six-card skeleton in for the real grid — measuring that transient made the
phantom-height assertion depend on fetch timing. Now waits on
role="grid"/"Courses", which only exists once the fetch resolved.
Count trailing child margins in the content measurement. A margin-bottom on
a direct in-flow child raises <main>'s height without appearing in any
child's bounding rect, which would have understated the content and turned
a real regression into a pass. Latent today; cheap to close.
Assert on the reading that settled. The tree test polled for convergence and
then measured AGAIN, reopening the window the poll existed to close. A small
settledFit() helper now returns the very reading that satisfied the
condition, and every assertion runs on that one. It also returns the last
reading on timeout, so a failure reports real numbers instead of a bare
"timed out".
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
frontend/e2e/viewport-fit.spec.ts (1)

120-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add Gradebook Course screen coverage for the same phantom-height fix.

frontend/e2e/viewport-fit.spec.ts only runs this geometry check on /gradebook. No other e2e test exercises GradebookCourseScreen path with the same <main> content-vs-container height assertion, so add a course-route test or sibling coverage to cover this regression directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/e2e/viewport-fit.spec.ts` around lines 120 - 189, Extend the
viewport-fit geometry coverage to navigate to a GradebookCourseScreen course
route in addition to /gradebook. Reuse the existing layout loop and <main>
content-versus-available-height assertions, ensuring the course screen’s route
and seeded data are loaded before measuring so the phantom-height regression is
tested directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@frontend/e2e/viewport-fit.spec.ts`:
- Around line 120-189: Extend the viewport-fit geometry coverage to navigate to
a GradebookCourseScreen course route in addition to /gradebook. Reuse the
existing layout loop and <main> content-versus-available-height assertions,
ensuring the course screen’s route and seeded data are loaded before measuring
so the phantom-height regression is tested directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 99a37da4-caf8-470e-ab54-4b0b4d647048

📥 Commits

Reviewing files that changed from the base of the PR and between 432c207 and e700c62.

📒 Files selected for processing (4)
  • frontend/e2e/viewport-fit.spec.ts
  • frontend/src/components/screens/Gradebook/Course.tsx
  • frontend/src/components/screens/Gradebook/Landing.tsx
  • frontend/src/components/screens/Tree.tsx

#341)
CI's react-hooks/rules-of-hooks rejected the helper: anything named use* is
treated as a Hook, and the spec calls it inside a for-loop over the two shell
layouts. Renamed to switchLayout — behaviour identical.
Local `npm run lint` reported 0 errors on the same code; the installed
eslint-plugin-react-hooks is older than CI's, so this was only visible there.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 162078a into mainJul 31, 2026
7 checks passed
@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Member

Automated post-merge review pass (Claude Code) on caf04b0 (the review ran while this merged — treat as follow-up input). The core verdict is positive: all three residuals from #341 are properly root-caused, the flex: 1 0 auto replacement expresses what the old minHeight: calc(...) was failing to say, the residual sweep is genuinely complete for the bug class, and the red-at-base regression journey (both layouts, overshoot and undershoot, real scrollport measurements) is exemplary.

One real regression worth a follow-up fix:

  1. Sticky TopBar travel is now clipped on the two gradebook screens (Gradebook/Landing.tsx:229, Course.tsx:519). At base, TopBar's sticky containing block was the shell <main>'s full scrollable content, so it stayed pinned for the whole scroll. At head its parent is FullHeightScreen's fixed height: 100% box — exactly one scrollport tall — so once page content exceeds ~2× the scrollport, the pinned header (breadcrumb, Curve/Letter Scale actions) detaches and scrolls away mid-page. These are precisely the screens that deliberately still page-scroll, and long assignment tables plausibly cross the threshold. Verified: FullHeightScreen.tsx sets height: "100%" with the ...style spread after it, so passing style={{ height: "auto", minHeight: "100%" }} on just these two screens restores full sticky travel while keeping the fill/phantom-height behavior the journey asserts (a min-height flex column still distributes free space to a flex: 1 0 auto child). The pinned-chat screens must keep fixed height — a grow prop on FullHeightScreen would make the two modes explicit.

Minor follow-ups:

  1. One same-class residual survives: (shell)/notetaker/page.tsx:1028minHeight: calc(100vh - 280px) on the fullscreen editor textarea (defanged by the enclosing scroll pane, but the same blind-constant pattern).
  2. viewport-fit.spec.ts measures /gradebook Landing only; Course.tsx got the identical fix but no measurement — extending the loop to the seeded /gradebook/[courseId] is cheap, and a scroll-then-assert that TopBar is still pinned on a tall Course page is the assertion that would have caught Add Personal Client-Side Definitive Study Guides #1.
  3. Tree.tsx:280-300 — the filter row got flexShrink: 0 but the TopBar header keeps default shrink; at very short viewports the TopBar is now the only compressible child and squashes (previously the page scrolled). flexShrink: 0 on the header keeps the chrome intact.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@AndresL230@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(ui): replace viewport-math residuals with FullHeightScreen (#341) - #487

Merged
AndresL230 merged 6 commits into
mainfrom
feat/341-viewport-math
Jul 31, 2026
Merged

fix(ui): replace viewport-math residuals with FullHeightScreen (#341)#487
AndresL230 merged 6 commits into
mainfrom
feat/341-viewport-math

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Replaces the three surviving calc(100vh - …) residuals with FullHeightScreen + flex, and adds the journey that proves the one with a user-visible consequence.

Why these were wrong

ShellFrame renders two layouts: a sidebar (its <main> is the full 100dvh) or a horizontal TopNav above <main> (so <main> is 100dvh - 56px). Any constant subtracted from 100vh can only be calibrated for one of them — and none of them accounted for the density preference, which retunes the padding tokens of the chrome being subtracted.

The consequential one — Tree.tsx

height: calc(100vh - 240px) wrapped the element a ResizeObserver watches; its contentRect is passed straight through as <KnowledgeGraph width height>. So this was not a stray scrollbar — the graph canvas itself rendered at the wrong size in whichever layout the constant wasn't tuned for.

Now: FullHeightScreen root + flex: 1 / minHeight: 0 on the graph row, so it absorbs exactly what <main> has left below the TopBar and filter row. Correct in both layouts, at any density, by construction rather than by calibration.

The semantic ones — Gradebook Landing + Course

minHeight: calc(100vh - var(--row-h)) subtracted a density token (40/34/48px per globals.css) as if it were a nav height. Both now sit in a FullHeightScreen with flex: 1 0 auto on their <main> — fill the remaining space, keep growing when content is taller (the minHeight semantic, preserved).

Test

frontend/e2e/tree-viewport.spec.ts asserts the invariant that was violated: on /tree the graph canvas fits inside the shell scrollport — no overshoot, no scrollable overflow — in both layouts, driven off the sapling_layout localStorage pref. It is a new spec rather than an addition to graph.spec.ts, which declares itself data-only and deliberately reads no x/y.

FullscreenGraph (the overlay variant) already used flex: 1 and is untouched.

Gates

  • tsc --noEmit — clean
  • npm run lint — 0 errors (36 pre-existing warnings)
  • npx vitest run — 55 files, 399 tests passed
  • Full local e2e cycle — recorded in a comment below

part of #341

Summary by CodeRabbit

  • Bug Fixes

    • Improved screen sizing across sidebar and top-navigation layouts.
    • Fixed Tree and Gradebook views to fill available space without unwanted overflow.
    • Preserved legitimate scrolling for content that exceeds the available height.
  • Tests

    • Added end-to-end coverage validating viewport fit, shell sizing, canvas settling, and content scrolling.

Three screens still sized themselves by subtracting a magic constant from
100vh. That subtraction can only be correct for ONE of ShellFrame's two
layouts — the sidebar variant gives `<main>` the full 100dvh, the TopNav
variant gives it 100dvh - 56px — and it is blind to the density preference
that retunes the padding tokens above it.
Tree.tsx is the one with a user-visible consequence, not just a stray
scrollbar: the row sized `calc(100vh - 240px)` wraps the element a
ResizeObserver watches, and that contentRect is passed straight through as
`<KnowledgeGraph width height>`. A mis-measured row therefore renders the
graph CANVAS at the wrong size. It now takes `flex: 1` + `minHeight: 0`
inside a FullHeightScreen root, so it absorbs exactly what `<main>` has
left after the TopBar and filter row, in either layout and at any density.
Gradebook Landing/Course used `calc(100vh - var(--row-h))` — subtracting a
DENSITY token (40/34/48px) as though it were a nav height. Both now sit in
a FullHeightScreen with `flex: 1 0 auto` on their `<main>`: fill the
remaining space, keep growing when the content is taller.
Adds frontend/e2e/tree-viewport.spec.ts, which asserts the invariant that
was silently violated — on /tree the graph canvas fits inside the shell
scrollport — in BOTH layouts, driven off the localStorage layout pref. It
lives in its own spec because graph.spec.ts declares itself data-only and
reads no geometry by design.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabaseBot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5fd64ba9-fab4-4f6b-9590-eb106f11a53c

📥 Commits

Reviewing files that changed from the base of the PR and between e700c62 and caf04b0.

📒 Files selected for processing (1)
  • frontend/e2e/viewport-fit.spec.ts
📝 Walkthrough

Walkthrough

The PR replaces viewport-based sizing with FullHeightScreen layouts for Tree and Gradebook screens. It adds Playwright tests for shell fit, graph sizing, and Gradebook content height in sidebar and top-navigation layouts.

Changes

Viewport-fit shell layout

Layer / File(s)Summary
Full-height screen integration
frontend/src/components/screens/Tree.tsx, frontend/src/components/screens/Gradebook/Course.tsx, frontend/src/components/screens/Gradebook/Landing.tsx
Tree and Gradebook screens now use FullHeightScreen. Tree uses flexible graph sizing and a non-shrinking filter row. Gradebook content uses flex growth instead of viewport-based minimum heights.
Viewport-fit end-to-end validation
frontend/e2e/viewport-fit.spec.ts
Playwright coverage validates shell geometry, settled graph height, overflow, and Gradebook content sizing across both shell layouts.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers:darkest-teddy, jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly identifies the main UI change: replacing viewport calculations with FullHeightScreen.
Description check✅ PassedThe description explains the motivation, implementation, affected screens, tests, and validation results in sufficient detail.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/341-viewport-math

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingcaf04b0Commit Preview URL

Branch Preview URL
Jul 31 2026, 02:59 AM

AndresL230and others added 3 commits July 30, 2026 19:13
Review follow-ups on the journey added with this PR.
Two-sided fit: the spec only guarded overshoot, so a future regression that
broke `flex: 1` and left the canvas a few pixels tall would have passed —
`canvasHeight > 0` is not a floor. It now asserts the canvas settles FLUSH
with the scrollport's bottom edge, which is the actual stated invariant, and
polls for it because the canvas size is ResizeObserver-driven and starts at
Tree.tsx's placeholder 900x600.
Gradebook coverage: Landing/Course got the same class of change as Tree with
no geometry assertion behind it — the gap that let the identical
`calc(100vh - N)` bug survive review on Settings.tsx during #335, since jsdom
has no layout engine and cannot catch it. Adds a second test asserting the
seeded gradebook does not manufacture scroll it does not need, in both
layouts.
Renames tree-viewport.spec.ts -> viewport-fit.spec.ts now that it covers two
screens.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ever scrolls (#341)
The first version of this test asserted the gradebook page must not scroll
at all. That is wrong: under the topnav layout the scrollport is 56px
shorter, and the seeded gradebook's content genuinely needs more than it —
scrolling there is the correct answer, and the assertion failed the FIXED
build for a legitimate reason (16px at topnav, 0 at sidebar).
What the fix actually guarantees is narrower: the container contributes no
height of its own beyond the space it was given or the height its content
needs. That is now what the test measures — <main> must be no taller than
max(available, content), and no shorter than the space available (which
would mean flex-grow is broken).
The unfixed build overshot by exactly 40px at the default density, which is
`--row-h` to the pixel — the density-token-as-nav-height bug, measured.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ght (#341)
The previous revision of this assertion passed on the UNFIXED build, which
makes it worthless as a regression test. Caught by running the spec against
the base sha before trusting it green.
Cause: it derived the content height from `main.scrollHeight`, which is
floored at the element's own client height. Any phantom height the box gave
itself was therefore mirrored into the "content" it was being compared
against, so `mainHeight <= max(available, content)` held by construction and
could never fail.
It now measures the content extent from the in-flow children (skipping
AmbientOrbs, which is position:fixed) plus the bottom padding — a number the
box's own sizing cannot influence.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Local e2e cycle — red at base, green at fix

Run as two phases inside one flock of the stack lock, at 3ee666c vs base 432c207, with SAPLING_MODEL_MODE=function.

Both phases matter: a journey that passes on unfixed code proves nothing, so phase 1 runs the new spec against the base build and requires it to fail.

Phase 1 — base 432c207 + the new spec: 2 failed (expected)

layout "topnav": canvas 480px vs scrollport 664px — overshoots by 27px
layout "sidebar": <main> is 686px but needs only 647px
(647px available, 606px of content) — 39px is phantom

Two things worth noting in those numbers:

  • The tree failure is topnav only — sidebar passed at base, which is exactly what a 240px constant calibrated for the sidebar layout would do. 27px measured against the issue's estimate of ~29px.
  • The gradebook phantom is 39px, which is --row-h (40px at the default density) to within rounding — the density-token-as-nav-height bug, measured rather than argued.

Phase 2 — fix 3ee666c: 35/35 journeys, oracles clean

35 passed (1.3m)
0 finding(s), 1 suppressed (allowlisted)

Two corrections this caught

The gradebook assertion took two revisions before it was worth keeping, both caught by the base run rather than by review:

  1. First version asserted the page must not scroll at all. Wrong — under topnav the scrollport is 56px shorter and the seeded content genuinely needs more than it, so scrolling there is correct. It failed the fixed build for a legitimate reason.
  2. Second version derived content height from main.scrollHeight, which is floored at the element's own client height — so any phantom height was mirrored into the number it was compared against, and it passed at base. Vacuous.

It now measures the content extent from the in-flow children (skipping AmbientOrbs, which is position: fixed), which the box's own sizing cannot influence.

Gates

tsc --noEmit clean · npm run lint 0 errors · npx vitest run 55 files / 399 tests passed

Follow-ups from the review of the spec delta.
Gate on the LOADED gradebook grid, not the chrome. `gradebook-transcript-open`
renders as soon as there is a user, while `loading` is still swapping a
six-card skeleton in for the real grid — measuring that transient made the
phantom-height assertion depend on fetch timing. Now waits on
role="grid"/"Courses", which only exists once the fetch resolved.
Count trailing child margins in the content measurement. A margin-bottom on
a direct in-flow child raises <main>'s height without appearing in any
child's bounding rect, which would have understated the content and turned
a real regression into a pass. Latent today; cheap to close.
Assert on the reading that settled. The tree test polled for convergence and
then measured AGAIN, reopening the window the poll existed to close. A small
settledFit() helper now returns the very reading that satisfied the
condition, and every assertion runs on that one. It also returns the last
reading on timeout, so a failure reports real numbers instead of a bare
"timed out".
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
frontend/e2e/viewport-fit.spec.ts (1)

120-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add Gradebook Course screen coverage for the same phantom-height fix.

frontend/e2e/viewport-fit.spec.ts only runs this geometry check on /gradebook. No other e2e test exercises GradebookCourseScreen path with the same <main> content-vs-container height assertion, so add a course-route test or sibling coverage to cover this regression directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/e2e/viewport-fit.spec.ts` around lines 120 - 189, Extend the
viewport-fit geometry coverage to navigate to a GradebookCourseScreen course
route in addition to /gradebook. Reuse the existing layout loop and <main>
content-versus-available-height assertions, ensuring the course screen’s route
and seeded data are loaded before measuring so the phantom-height regression is
tested directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@frontend/e2e/viewport-fit.spec.ts`:
- Around line 120-189: Extend the viewport-fit geometry coverage to navigate to
a GradebookCourseScreen course route in addition to /gradebook. Reuse the
existing layout loop and <main> content-versus-available-height assertions,
ensuring the course screen’s route and seeded data are loaded before measuring
so the phantom-height regression is tested directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 99a37da4-caf8-470e-ab54-4b0b4d647048

📥 Commits

Reviewing files that changed from the base of the PR and between 432c207 and e700c62.

📒 Files selected for processing (4)
  • frontend/e2e/viewport-fit.spec.ts
  • frontend/src/components/screens/Gradebook/Course.tsx
  • frontend/src/components/screens/Gradebook/Landing.tsx
  • frontend/src/components/screens/Tree.tsx

#341)
CI's react-hooks/rules-of-hooks rejected the helper: anything named use* is
treated as a Hook, and the spec calls it inside a for-loop over the two shell
layouts. Renamed to switchLayout — behaviour identical.
Local `npm run lint` reported 0 errors on the same code; the installed
eslint-plugin-react-hooks is older than CI's, so this was only visible there.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 162078a into mainJul 31, 2026
7 checks passed
@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Member

Automated post-merge review pass (Claude Code) on caf04b0 (the review ran while this merged — treat as follow-up input). The core verdict is positive: all three residuals from #341 are properly root-caused, the flex: 1 0 auto replacement expresses what the old minHeight: calc(...) was failing to say, the residual sweep is genuinely complete for the bug class, and the red-at-base regression journey (both layouts, overshoot and undershoot, real scrollport measurements) is exemplary.

One real regression worth a follow-up fix:

  1. Sticky TopBar travel is now clipped on the two gradebook screens (Gradebook/Landing.tsx:229, Course.tsx:519). At base, TopBar's sticky containing block was the shell <main>'s full scrollable content, so it stayed pinned for the whole scroll. At head its parent is FullHeightScreen's fixed height: 100% box — exactly one scrollport tall — so once page content exceeds ~2× the scrollport, the pinned header (breadcrumb, Curve/Letter Scale actions) detaches and scrolls away mid-page. These are precisely the screens that deliberately still page-scroll, and long assignment tables plausibly cross the threshold. Verified: FullHeightScreen.tsx sets height: "100%" with the ...style spread after it, so passing style={{ height: "auto", minHeight: "100%" }} on just these two screens restores full sticky travel while keeping the fill/phantom-height behavior the journey asserts (a min-height flex column still distributes free space to a flex: 1 0 auto child). The pinned-chat screens must keep fixed height — a grow prop on FullHeightScreen would make the two modes explicit.

Minor follow-ups:

  1. One same-class residual survives: (shell)/notetaker/page.tsx:1028minHeight: calc(100vh - 280px) on the fullscreen editor textarea (defanged by the enclosing scroll pane, but the same blind-constant pattern).
  2. viewport-fit.spec.ts measures /gradebook Landing only; Course.tsx got the identical fix but no measurement — extending the loop to the seeded /gradebook/[courseId] is cheap, and a scroll-then-assert that TopBar is still pinned on a tall Course page is the assertion that would have caught Add Personal Client-Side Definitive Study Guides #1.
  3. Tree.tsx:280-300 — the filter row got flexShrink: 0 but the TopBar header keeps default shrink; at very short viewports the TopBar is now the only compressible child and squashes (previously the page scrolled). flexShrink: 0 on the header keeps the chrome intact.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@AndresL230@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(ui): replace viewport-math residuals with FullHeightScreen (#341) - #487

Merged
AndresL230 merged 6 commits into
mainfrom
feat/341-viewport-math
Jul 31, 2026
Merged

fix(ui): replace viewport-math residuals with FullHeightScreen (#341)#487
AndresL230 merged 6 commits into
mainfrom
feat/341-viewport-math

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Replaces the three surviving calc(100vh - …) residuals with FullHeightScreen + flex, and adds the journey that proves the one with a user-visible consequence.

Why these were wrong

ShellFrame renders two layouts: a sidebar (its <main> is the full 100dvh) or a horizontal TopNav above <main> (so <main> is 100dvh - 56px). Any constant subtracted from 100vh can only be calibrated for one of them — and none of them accounted for the density preference, which retunes the padding tokens of the chrome being subtracted.

The consequential one — Tree.tsx

height: calc(100vh - 240px) wrapped the element a ResizeObserver watches; its contentRect is passed straight through as <KnowledgeGraph width height>. So this was not a stray scrollbar — the graph canvas itself rendered at the wrong size in whichever layout the constant wasn't tuned for.

Now: FullHeightScreen root + flex: 1 / minHeight: 0 on the graph row, so it absorbs exactly what <main> has left below the TopBar and filter row. Correct in both layouts, at any density, by construction rather than by calibration.

The semantic ones — Gradebook Landing + Course

minHeight: calc(100vh - var(--row-h)) subtracted a density token (40/34/48px per globals.css) as if it were a nav height. Both now sit in a FullHeightScreen with flex: 1 0 auto on their <main> — fill the remaining space, keep growing when content is taller (the minHeight semantic, preserved).

Test

frontend/e2e/tree-viewport.spec.ts asserts the invariant that was violated: on /tree the graph canvas fits inside the shell scrollport — no overshoot, no scrollable overflow — in both layouts, driven off the sapling_layout localStorage pref. It is a new spec rather than an addition to graph.spec.ts, which declares itself data-only and deliberately reads no x/y.

FullscreenGraph (the overlay variant) already used flex: 1 and is untouched.

Gates

  • tsc --noEmit — clean
  • npm run lint — 0 errors (36 pre-existing warnings)
  • npx vitest run — 55 files, 399 tests passed
  • Full local e2e cycle — recorded in a comment below

part of #341

Summary by CodeRabbit

  • Bug Fixes

    • Improved screen sizing across sidebar and top-navigation layouts.
    • Fixed Tree and Gradebook views to fill available space without unwanted overflow.
    • Preserved legitimate scrolling for content that exceeds the available height.
  • Tests

    • Added end-to-end coverage validating viewport fit, shell sizing, canvas settling, and content scrolling.

Three screens still sized themselves by subtracting a magic constant from
100vh. That subtraction can only be correct for ONE of ShellFrame's two
layouts — the sidebar variant gives `<main>` the full 100dvh, the TopNav
variant gives it 100dvh - 56px — and it is blind to the density preference
that retunes the padding tokens above it.
Tree.tsx is the one with a user-visible consequence, not just a stray
scrollbar: the row sized `calc(100vh - 240px)` wraps the element a
ResizeObserver watches, and that contentRect is passed straight through as
`<KnowledgeGraph width height>`. A mis-measured row therefore renders the
graph CANVAS at the wrong size. It now takes `flex: 1` + `minHeight: 0`
inside a FullHeightScreen root, so it absorbs exactly what `<main>` has
left after the TopBar and filter row, in either layout and at any density.
Gradebook Landing/Course used `calc(100vh - var(--row-h))` — subtracting a
DENSITY token (40/34/48px) as though it were a nav height. Both now sit in
a FullHeightScreen with `flex: 1 0 auto` on their `<main>`: fill the
remaining space, keep growing when the content is taller.
Adds frontend/e2e/tree-viewport.spec.ts, which asserts the invariant that
was silently violated — on /tree the graph canvas fits inside the shell
scrollport — in BOTH layouts, driven off the localStorage layout pref. It
lives in its own spec because graph.spec.ts declares itself data-only and
reads no geometry by design.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabaseBot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5fd64ba9-fab4-4f6b-9590-eb106f11a53c

📥 Commits

Reviewing files that changed from the base of the PR and between e700c62 and caf04b0.

📒 Files selected for processing (1)
  • frontend/e2e/viewport-fit.spec.ts
📝 Walkthrough

Walkthrough

The PR replaces viewport-based sizing with FullHeightScreen layouts for Tree and Gradebook screens. It adds Playwright tests for shell fit, graph sizing, and Gradebook content height in sidebar and top-navigation layouts.

Changes

Viewport-fit shell layout

Layer / File(s)Summary
Full-height screen integration
frontend/src/components/screens/Tree.tsx, frontend/src/components/screens/Gradebook/Course.tsx, frontend/src/components/screens/Gradebook/Landing.tsx
Tree and Gradebook screens now use FullHeightScreen. Tree uses flexible graph sizing and a non-shrinking filter row. Gradebook content uses flex growth instead of viewport-based minimum heights.
Viewport-fit end-to-end validation
frontend/e2e/viewport-fit.spec.ts
Playwright coverage validates shell geometry, settled graph height, overflow, and Gradebook content sizing across both shell layouts.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers:darkest-teddy, jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly identifies the main UI change: replacing viewport calculations with FullHeightScreen.
Description check✅ PassedThe description explains the motivation, implementation, affected screens, tests, and validation results in sufficient detail.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/341-viewport-math

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingcaf04b0Commit Preview URL

Branch Preview URL
Jul 31 2026, 02:59 AM

AndresL230and others added 3 commits July 30, 2026 19:13
Review follow-ups on the journey added with this PR.
Two-sided fit: the spec only guarded overshoot, so a future regression that
broke `flex: 1` and left the canvas a few pixels tall would have passed —
`canvasHeight > 0` is not a floor. It now asserts the canvas settles FLUSH
with the scrollport's bottom edge, which is the actual stated invariant, and
polls for it because the canvas size is ResizeObserver-driven and starts at
Tree.tsx's placeholder 900x600.
Gradebook coverage: Landing/Course got the same class of change as Tree with
no geometry assertion behind it — the gap that let the identical
`calc(100vh - N)` bug survive review on Settings.tsx during #335, since jsdom
has no layout engine and cannot catch it. Adds a second test asserting the
seeded gradebook does not manufacture scroll it does not need, in both
layouts.
Renames tree-viewport.spec.ts -> viewport-fit.spec.ts now that it covers two
screens.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ever scrolls (#341)
The first version of this test asserted the gradebook page must not scroll
at all. That is wrong: under the topnav layout the scrollport is 56px
shorter, and the seeded gradebook's content genuinely needs more than it —
scrolling there is the correct answer, and the assertion failed the FIXED
build for a legitimate reason (16px at topnav, 0 at sidebar).
What the fix actually guarantees is narrower: the container contributes no
height of its own beyond the space it was given or the height its content
needs. That is now what the test measures — <main> must be no taller than
max(available, content), and no shorter than the space available (which
would mean flex-grow is broken).
The unfixed build overshot by exactly 40px at the default density, which is
`--row-h` to the pixel — the density-token-as-nav-height bug, measured.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ght (#341)
The previous revision of this assertion passed on the UNFIXED build, which
makes it worthless as a regression test. Caught by running the spec against
the base sha before trusting it green.
Cause: it derived the content height from `main.scrollHeight`, which is
floored at the element's own client height. Any phantom height the box gave
itself was therefore mirrored into the "content" it was being compared
against, so `mainHeight <= max(available, content)` held by construction and
could never fail.
It now measures the content extent from the in-flow children (skipping
AmbientOrbs, which is position:fixed) plus the bottom padding — a number the
box's own sizing cannot influence.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Local e2e cycle — red at base, green at fix

Run as two phases inside one flock of the stack lock, at 3ee666c vs base 432c207, with SAPLING_MODEL_MODE=function.

Both phases matter: a journey that passes on unfixed code proves nothing, so phase 1 runs the new spec against the base build and requires it to fail.

Phase 1 — base 432c207 + the new spec: 2 failed (expected)

layout "topnav": canvas 480px vs scrollport 664px — overshoots by 27px
layout "sidebar": <main> is 686px but needs only 647px
(647px available, 606px of content) — 39px is phantom

Two things worth noting in those numbers:

  • The tree failure is topnav only — sidebar passed at base, which is exactly what a 240px constant calibrated for the sidebar layout would do. 27px measured against the issue's estimate of ~29px.
  • The gradebook phantom is 39px, which is --row-h (40px at the default density) to within rounding — the density-token-as-nav-height bug, measured rather than argued.

Phase 2 — fix 3ee666c: 35/35 journeys, oracles clean

35 passed (1.3m)
0 finding(s), 1 suppressed (allowlisted)

Two corrections this caught

The gradebook assertion took two revisions before it was worth keeping, both caught by the base run rather than by review:

  1. First version asserted the page must not scroll at all. Wrong — under topnav the scrollport is 56px shorter and the seeded content genuinely needs more than it, so scrolling there is correct. It failed the fixed build for a legitimate reason.
  2. Second version derived content height from main.scrollHeight, which is floored at the element's own client height — so any phantom height was mirrored into the number it was compared against, and it passed at base. Vacuous.

It now measures the content extent from the in-flow children (skipping AmbientOrbs, which is position: fixed), which the box's own sizing cannot influence.

Gates

tsc --noEmit clean · npm run lint 0 errors · npx vitest run 55 files / 399 tests passed

Follow-ups from the review of the spec delta.
Gate on the LOADED gradebook grid, not the chrome. `gradebook-transcript-open`
renders as soon as there is a user, while `loading` is still swapping a
six-card skeleton in for the real grid — measuring that transient made the
phantom-height assertion depend on fetch timing. Now waits on
role="grid"/"Courses", which only exists once the fetch resolved.
Count trailing child margins in the content measurement. A margin-bottom on
a direct in-flow child raises <main>'s height without appearing in any
child's bounding rect, which would have understated the content and turned
a real regression into a pass. Latent today; cheap to close.
Assert on the reading that settled. The tree test polled for convergence and
then measured AGAIN, reopening the window the poll existed to close. A small
settledFit() helper now returns the very reading that satisfied the
condition, and every assertion runs on that one. It also returns the last
reading on timeout, so a failure reports real numbers instead of a bare
"timed out".
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
frontend/e2e/viewport-fit.spec.ts (1)

120-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add Gradebook Course screen coverage for the same phantom-height fix.

frontend/e2e/viewport-fit.spec.ts only runs this geometry check on /gradebook. No other e2e test exercises GradebookCourseScreen path with the same <main> content-vs-container height assertion, so add a course-route test or sibling coverage to cover this regression directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/e2e/viewport-fit.spec.ts` around lines 120 - 189, Extend the
viewport-fit geometry coverage to navigate to a GradebookCourseScreen course
route in addition to /gradebook. Reuse the existing layout loop and <main>
content-versus-available-height assertions, ensuring the course screen’s route
and seeded data are loaded before measuring so the phantom-height regression is
tested directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@frontend/e2e/viewport-fit.spec.ts`:
- Around line 120-189: Extend the viewport-fit geometry coverage to navigate to
a GradebookCourseScreen course route in addition to /gradebook. Reuse the
existing layout loop and <main> content-versus-available-height assertions,
ensuring the course screen’s route and seeded data are loaded before measuring
so the phantom-height regression is tested directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 99a37da4-caf8-470e-ab54-4b0b4d647048

📥 Commits

Reviewing files that changed from the base of the PR and between 432c207 and e700c62.

📒 Files selected for processing (4)
  • frontend/e2e/viewport-fit.spec.ts
  • frontend/src/components/screens/Gradebook/Course.tsx
  • frontend/src/components/screens/Gradebook/Landing.tsx
  • frontend/src/components/screens/Tree.tsx

#341)
CI's react-hooks/rules-of-hooks rejected the helper: anything named use* is
treated as a Hook, and the spec calls it inside a for-loop over the two shell
layouts. Renamed to switchLayout — behaviour identical.
Local `npm run lint` reported 0 errors on the same code; the installed
eslint-plugin-react-hooks is older than CI's, so this was only visible there.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 162078a into mainJul 31, 2026
7 checks passed
@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Member

Automated post-merge review pass (Claude Code) on caf04b0 (the review ran while this merged — treat as follow-up input). The core verdict is positive: all three residuals from #341 are properly root-caused, the flex: 1 0 auto replacement expresses what the old minHeight: calc(...) was failing to say, the residual sweep is genuinely complete for the bug class, and the red-at-base regression journey (both layouts, overshoot and undershoot, real scrollport measurements) is exemplary.

One real regression worth a follow-up fix:

  1. Sticky TopBar travel is now clipped on the two gradebook screens (Gradebook/Landing.tsx:229, Course.tsx:519). At base, TopBar's sticky containing block was the shell <main>'s full scrollable content, so it stayed pinned for the whole scroll. At head its parent is FullHeightScreen's fixed height: 100% box — exactly one scrollport tall — so once page content exceeds ~2× the scrollport, the pinned header (breadcrumb, Curve/Letter Scale actions) detaches and scrolls away mid-page. These are precisely the screens that deliberately still page-scroll, and long assignment tables plausibly cross the threshold. Verified: FullHeightScreen.tsx sets height: "100%" with the ...style spread after it, so passing style={{ height: "auto", minHeight: "100%" }} on just these two screens restores full sticky travel while keeping the fill/phantom-height behavior the journey asserts (a min-height flex column still distributes free space to a flex: 1 0 auto child). The pinned-chat screens must keep fixed height — a grow prop on FullHeightScreen would make the two modes explicit.

Minor follow-ups:

  1. One same-class residual survives: (shell)/notetaker/page.tsx:1028minHeight: calc(100vh - 280px) on the fullscreen editor textarea (defanged by the enclosing scroll pane, but the same blind-constant pattern).
  2. viewport-fit.spec.ts measures /gradebook Landing only; Course.tsx got the identical fix but no measurement — extending the loop to the seeded /gradebook/[courseId] is cheap, and a scroll-then-assert that TopBar is still pinned on a tall Course page is the assertion that would have caught Add Personal Client-Side Definitive Study Guides #1.
  3. Tree.tsx:280-300 — the filter row got flexShrink: 0 but the TopBar header keeps default shrink; at very short viewports the TopBar is now the only compressible child and squashes (previously the page scrolled). flexShrink: 0 on the header keeps the chrome intact.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@AndresL230@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(ui): replace viewport-math residuals with FullHeightScreen (#341) - #487

Merged
AndresL230 merged 6 commits into
mainfrom
feat/341-viewport-math
Jul 31, 2026
Merged

fix(ui): replace viewport-math residuals with FullHeightScreen (#341)#487
AndresL230 merged 6 commits into
mainfrom
feat/341-viewport-math

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Replaces the three surviving calc(100vh - …) residuals with FullHeightScreen + flex, and adds the journey that proves the one with a user-visible consequence.

Why these were wrong

ShellFrame renders two layouts: a sidebar (its <main> is the full 100dvh) or a horizontal TopNav above <main> (so <main> is 100dvh - 56px). Any constant subtracted from 100vh can only be calibrated for one of them — and none of them accounted for the density preference, which retunes the padding tokens of the chrome being subtracted.

The consequential one — Tree.tsx

height: calc(100vh - 240px) wrapped the element a ResizeObserver watches; its contentRect is passed straight through as <KnowledgeGraph width height>. So this was not a stray scrollbar — the graph canvas itself rendered at the wrong size in whichever layout the constant wasn't tuned for.

Now: FullHeightScreen root + flex: 1 / minHeight: 0 on the graph row, so it absorbs exactly what <main> has left below the TopBar and filter row. Correct in both layouts, at any density, by construction rather than by calibration.

The semantic ones — Gradebook Landing + Course

minHeight: calc(100vh - var(--row-h)) subtracted a density token (40/34/48px per globals.css) as if it were a nav height. Both now sit in a FullHeightScreen with flex: 1 0 auto on their <main> — fill the remaining space, keep growing when content is taller (the minHeight semantic, preserved).

Test

frontend/e2e/tree-viewport.spec.ts asserts the invariant that was violated: on /tree the graph canvas fits inside the shell scrollport — no overshoot, no scrollable overflow — in both layouts, driven off the sapling_layout localStorage pref. It is a new spec rather than an addition to graph.spec.ts, which declares itself data-only and deliberately reads no x/y.

FullscreenGraph (the overlay variant) already used flex: 1 and is untouched.

Gates

  • tsc --noEmit — clean
  • npm run lint — 0 errors (36 pre-existing warnings)
  • npx vitest run — 55 files, 399 tests passed
  • Full local e2e cycle — recorded in a comment below

part of #341

Summary by CodeRabbit

  • Bug Fixes

    • Improved screen sizing across sidebar and top-navigation layouts.
    • Fixed Tree and Gradebook views to fill available space without unwanted overflow.
    • Preserved legitimate scrolling for content that exceeds the available height.
  • Tests

    • Added end-to-end coverage validating viewport fit, shell sizing, canvas settling, and content scrolling.

Three screens still sized themselves by subtracting a magic constant from
100vh. That subtraction can only be correct for ONE of ShellFrame's two
layouts — the sidebar variant gives `<main>` the full 100dvh, the TopNav
variant gives it 100dvh - 56px — and it is blind to the density preference
that retunes the padding tokens above it.
Tree.tsx is the one with a user-visible consequence, not just a stray
scrollbar: the row sized `calc(100vh - 240px)` wraps the element a
ResizeObserver watches, and that contentRect is passed straight through as
`<KnowledgeGraph width height>`. A mis-measured row therefore renders the
graph CANVAS at the wrong size. It now takes `flex: 1` + `minHeight: 0`
inside a FullHeightScreen root, so it absorbs exactly what `<main>` has
left after the TopBar and filter row, in either layout and at any density.
Gradebook Landing/Course used `calc(100vh - var(--row-h))` — subtracting a
DENSITY token (40/34/48px) as though it were a nav height. Both now sit in
a FullHeightScreen with `flex: 1 0 auto` on their `<main>`: fill the
remaining space, keep growing when the content is taller.
Adds frontend/e2e/tree-viewport.spec.ts, which asserts the invariant that
was silently violated — on /tree the graph canvas fits inside the shell
scrollport — in BOTH layouts, driven off the localStorage layout pref. It
lives in its own spec because graph.spec.ts declares itself data-only and
reads no geometry by design.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabaseBot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5fd64ba9-fab4-4f6b-9590-eb106f11a53c

📥 Commits

Reviewing files that changed from the base of the PR and between e700c62 and caf04b0.

📒 Files selected for processing (1)
  • frontend/e2e/viewport-fit.spec.ts
📝 Walkthrough

Walkthrough

The PR replaces viewport-based sizing with FullHeightScreen layouts for Tree and Gradebook screens. It adds Playwright tests for shell fit, graph sizing, and Gradebook content height in sidebar and top-navigation layouts.

Changes

Viewport-fit shell layout

Layer / File(s)Summary
Full-height screen integration
frontend/src/components/screens/Tree.tsx, frontend/src/components/screens/Gradebook/Course.tsx, frontend/src/components/screens/Gradebook/Landing.tsx
Tree and Gradebook screens now use FullHeightScreen. Tree uses flexible graph sizing and a non-shrinking filter row. Gradebook content uses flex growth instead of viewport-based minimum heights.
Viewport-fit end-to-end validation
frontend/e2e/viewport-fit.spec.ts
Playwright coverage validates shell geometry, settled graph height, overflow, and Gradebook content sizing across both shell layouts.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers:darkest-teddy, jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly identifies the main UI change: replacing viewport calculations with FullHeightScreen.
Description check✅ PassedThe description explains the motivation, implementation, affected screens, tests, and validation results in sufficient detail.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/341-viewport-math

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingcaf04b0Commit Preview URL

Branch Preview URL
Jul 31 2026, 02:59 AM

AndresL230and others added 3 commits July 30, 2026 19:13
Review follow-ups on the journey added with this PR.
Two-sided fit: the spec only guarded overshoot, so a future regression that
broke `flex: 1` and left the canvas a few pixels tall would have passed —
`canvasHeight > 0` is not a floor. It now asserts the canvas settles FLUSH
with the scrollport's bottom edge, which is the actual stated invariant, and
polls for it because the canvas size is ResizeObserver-driven and starts at
Tree.tsx's placeholder 900x600.
Gradebook coverage: Landing/Course got the same class of change as Tree with
no geometry assertion behind it — the gap that let the identical
`calc(100vh - N)` bug survive review on Settings.tsx during #335, since jsdom
has no layout engine and cannot catch it. Adds a second test asserting the
seeded gradebook does not manufacture scroll it does not need, in both
layouts.
Renames tree-viewport.spec.ts -> viewport-fit.spec.ts now that it covers two
screens.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ever scrolls (#341)
The first version of this test asserted the gradebook page must not scroll
at all. That is wrong: under the topnav layout the scrollport is 56px
shorter, and the seeded gradebook's content genuinely needs more than it —
scrolling there is the correct answer, and the assertion failed the FIXED
build for a legitimate reason (16px at topnav, 0 at sidebar).
What the fix actually guarantees is narrower: the container contributes no
height of its own beyond the space it was given or the height its content
needs. That is now what the test measures — <main> must be no taller than
max(available, content), and no shorter than the space available (which
would mean flex-grow is broken).
The unfixed build overshot by exactly 40px at the default density, which is
`--row-h` to the pixel — the density-token-as-nav-height bug, measured.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ght (#341)
The previous revision of this assertion passed on the UNFIXED build, which
makes it worthless as a regression test. Caught by running the spec against
the base sha before trusting it green.
Cause: it derived the content height from `main.scrollHeight`, which is
floored at the element's own client height. Any phantom height the box gave
itself was therefore mirrored into the "content" it was being compared
against, so `mainHeight <= max(available, content)` held by construction and
could never fail.
It now measures the content extent from the in-flow children (skipping
AmbientOrbs, which is position:fixed) plus the bottom padding — a number the
box's own sizing cannot influence.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Local e2e cycle — red at base, green at fix

Run as two phases inside one flock of the stack lock, at 3ee666c vs base 432c207, with SAPLING_MODEL_MODE=function.

Both phases matter: a journey that passes on unfixed code proves nothing, so phase 1 runs the new spec against the base build and requires it to fail.

Phase 1 — base 432c207 + the new spec: 2 failed (expected)

layout "topnav": canvas 480px vs scrollport 664px — overshoots by 27px
layout "sidebar": <main> is 686px but needs only 647px
(647px available, 606px of content) — 39px is phantom

Two things worth noting in those numbers:

  • The tree failure is topnav only — sidebar passed at base, which is exactly what a 240px constant calibrated for the sidebar layout would do. 27px measured against the issue's estimate of ~29px.
  • The gradebook phantom is 39px, which is --row-h (40px at the default density) to within rounding — the density-token-as-nav-height bug, measured rather than argued.

Phase 2 — fix 3ee666c: 35/35 journeys, oracles clean

35 passed (1.3m)
0 finding(s), 1 suppressed (allowlisted)

Two corrections this caught

The gradebook assertion took two revisions before it was worth keeping, both caught by the base run rather than by review:

  1. First version asserted the page must not scroll at all. Wrong — under topnav the scrollport is 56px shorter and the seeded content genuinely needs more than it, so scrolling there is correct. It failed the fixed build for a legitimate reason.
  2. Second version derived content height from main.scrollHeight, which is floored at the element's own client height — so any phantom height was mirrored into the number it was compared against, and it passed at base. Vacuous.

It now measures the content extent from the in-flow children (skipping AmbientOrbs, which is position: fixed), which the box's own sizing cannot influence.

Gates

tsc --noEmit clean · npm run lint 0 errors · npx vitest run 55 files / 399 tests passed

Follow-ups from the review of the spec delta.
Gate on the LOADED gradebook grid, not the chrome. `gradebook-transcript-open`
renders as soon as there is a user, while `loading` is still swapping a
six-card skeleton in for the real grid — measuring that transient made the
phantom-height assertion depend on fetch timing. Now waits on
role="grid"/"Courses", which only exists once the fetch resolved.
Count trailing child margins in the content measurement. A margin-bottom on
a direct in-flow child raises <main>'s height without appearing in any
child's bounding rect, which would have understated the content and turned
a real regression into a pass. Latent today; cheap to close.
Assert on the reading that settled. The tree test polled for convergence and
then measured AGAIN, reopening the window the poll existed to close. A small
settledFit() helper now returns the very reading that satisfied the
condition, and every assertion runs on that one. It also returns the last
reading on timeout, so a failure reports real numbers instead of a bare
"timed out".
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
frontend/e2e/viewport-fit.spec.ts (1)

120-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add Gradebook Course screen coverage for the same phantom-height fix.

frontend/e2e/viewport-fit.spec.ts only runs this geometry check on /gradebook. No other e2e test exercises GradebookCourseScreen path with the same <main> content-vs-container height assertion, so add a course-route test or sibling coverage to cover this regression directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/e2e/viewport-fit.spec.ts` around lines 120 - 189, Extend the
viewport-fit geometry coverage to navigate to a GradebookCourseScreen course
route in addition to /gradebook. Reuse the existing layout loop and <main>
content-versus-available-height assertions, ensuring the course screen’s route
and seeded data are loaded before measuring so the phantom-height regression is
tested directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@frontend/e2e/viewport-fit.spec.ts`:
- Around line 120-189: Extend the viewport-fit geometry coverage to navigate to
a GradebookCourseScreen course route in addition to /gradebook. Reuse the
existing layout loop and <main> content-versus-available-height assertions,
ensuring the course screen’s route and seeded data are loaded before measuring
so the phantom-height regression is tested directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 99a37da4-caf8-470e-ab54-4b0b4d647048

📥 Commits

Reviewing files that changed from the base of the PR and between 432c207 and e700c62.

📒 Files selected for processing (4)
  • frontend/e2e/viewport-fit.spec.ts
  • frontend/src/components/screens/Gradebook/Course.tsx
  • frontend/src/components/screens/Gradebook/Landing.tsx
  • frontend/src/components/screens/Tree.tsx

#341)
CI's react-hooks/rules-of-hooks rejected the helper: anything named use* is
treated as a Hook, and the spec calls it inside a for-loop over the two shell
layouts. Renamed to switchLayout — behaviour identical.
Local `npm run lint` reported 0 errors on the same code; the installed
eslint-plugin-react-hooks is older than CI's, so this was only visible there.
part of #341
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit 162078a into mainJul 31, 2026
7 checks passed
@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Member

Automated post-merge review pass (Claude Code) on caf04b0 (the review ran while this merged — treat as follow-up input). The core verdict is positive: all three residuals from #341 are properly root-caused, the flex: 1 0 auto replacement expresses what the old minHeight: calc(...) was failing to say, the residual sweep is genuinely complete for the bug class, and the red-at-base regression journey (both layouts, overshoot and undershoot, real scrollport measurements) is exemplary.

One real regression worth a follow-up fix:

  1. Sticky TopBar travel is now clipped on the two gradebook screens (Gradebook/Landing.tsx:229, Course.tsx:519). At base, TopBar's sticky containing block was the shell <main>'s full scrollable content, so it stayed pinned for the whole scroll. At head its parent is FullHeightScreen's fixed height: 100% box — exactly one scrollport tall — so once page content exceeds ~2× the scrollport, the pinned header (breadcrumb, Curve/Letter Scale actions) detaches and scrolls away mid-page. These are precisely the screens that deliberately still page-scroll, and long assignment tables plausibly cross the threshold. Verified: FullHeightScreen.tsx sets height: "100%" with the ...style spread after it, so passing style={{ height: "auto", minHeight: "100%" }} on just these two screens restores full sticky travel while keeping the fill/phantom-height behavior the journey asserts (a min-height flex column still distributes free space to a flex: 1 0 auto child). The pinned-chat screens must keep fixed height — a grow prop on FullHeightScreen would make the two modes explicit.

Minor follow-ups:

  1. One same-class residual survives: (shell)/notetaker/page.tsx:1028minHeight: calc(100vh - 280px) on the fullscreen editor textarea (defanged by the enclosing scroll pane, but the same blind-constant pattern).
  2. viewport-fit.spec.ts measures /gradebook Landing only; Course.tsx got the identical fix but no measurement — extending the loop to the seeded /gradebook/[courseId] is cheap, and a scroll-then-assert that TopBar is still pinned on a tall Course page is the assertion that would have caught Add Personal Client-Side Definitive Study Guides #1.
  3. Tree.tsx:280-300 — the filter row got flexShrink: 0 but the TopBar header keeps default shrink; at very short viewports the TopBar is now the only compressible child and squashes (previously the page scrolled). flexShrink: 0 on the header keeps the chrome intact.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@AndresL230@Jose-Gael-Cruz-Lopez