also looking at this
fix(ui): bound the mobile menu to the visible viewport
#53
2 files
+26
-0
| @@ -186,6 +186,9 @@ | ||
| 186 | 186 | gap: 0.25rem; |
| 187 | 187 | min-width: 232px; |
| 188 | 188 | padding: 0.6rem; |
| 189 | max-height: calc(100dvh - 92px); | |
| 190 | overflow-y: auto; | |
| 191 | overscroll-behavior: contain; | |
| 189 | 192 | background: var(--surface-2); |
| 190 | 193 | border: 1px solid var(--line-strong); |
| 191 | 194 | border-radius: var(--r); |
| @@ -695,6 +698,7 @@ | ||
| 695 | 698 | .footer-brand { grid-column: 1 / -1; } |
| 696 | 699 | } |
| 697 | 700 | @media (max-width: 860px) { |
| 701 | body:has(.nav-menu[open]) { overflow: hidden; } | |
| 698 | 702 | .nav-links { display: none; } |
| 699 | 703 | .nav-cta { display: none; } |
| 700 | 704 | .nav-menu { display: block; } |
| @@ -0,0 +1,22 @@ | ||
| 1 | import { describe, expect, test } from 'bun:test' | |
| 2 | import { readFileSync } from 'node:fs' | |
| 3 | import { join } from 'node:path' | |
| 4 | ||
| 5 | const ROOT = join(import.meta.dir, '../..') | |
| 6 | const marketing = readFileSync(join(ROOT, 'public/marketing.css'), 'utf8') | |
| 7 | ||
| 8 | // The phone menu lists every marketing destination in one column, which is | |
| 9 | // taller than an 812px viewport. The panel has to scroll inside the viewport | |
| 10 | // and the page behind it has to stay put, or the last links are unreachable. | |
| 11 | describe('mobile layout contracts', () => { | |
| 12 | test('marketing menu is bounded by the visible mobile viewport', () => { | |
| 13 | const panel = marketing.match(/\.nav-menu-panel\s*\{[^}]*\}/)?.[0] ?? '' | |
| 14 | expect(panel).toContain('max-height: calc(100dvh - 92px)') | |
| 15 | expect(panel).toContain('overflow-y: auto') | |
| 16 | expect(panel).toContain('overscroll-behavior: contain') | |
| 17 | }) | |
| 18 | ||
| 19 | test('opening the marketing menu locks the page behind it', () => { | |
| 20 | expect(marketing).toContain('body:has(.nav-menu[open]) { overflow: hidden; }') | |
| 21 | }) | |
| 22 | }) | |