From 1ca957499a3dacd93578d8fe4c1ab50a33e66b45 Mon Sep 17 00:00:00 2001 From: Tudor Sitaru Date: Tue, 19 May 2026 09:52:17 +0100 Subject: [PATCH] feat(mobile): promote filter toggle when active + document 360px baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MOB-08: Search list pagination is already implemented (page_size 50, "Load more schools" button + count) — no change needed; ticket closed. MOB-10: Rather than build a full bottom-sheet filter modal (large change, modal/focus-trap/scroll-lock infra), promote the existing "Advanced" toggle to a coral pill labeled "Filters (n)" whenever dropdown filters are applied. Users now see at a glance that the list is being narrowed; the inline accordion remains the disclosure mechanism. Adds aria-expanded for screen readers. MOB-23: Add MOBILE.md at the repo root with the 360 px design baseline, acceptance checks for any UI PR (no horizontal overflow, ≥44px tap targets, no <11px visible text, iOS Chrome parity, safe-area-inset, dvh), and the established component patterns. Playwright regression test deferred — adding the dep for one test is heavier than the current value warrants; documented as a future option. Co-Authored-By: Claude Opus 4.7 --- MOBILE.md | 71 ++++++++++++++++++++++ nextjs-app/components/FilterBar.module.css | 15 +++++ nextjs-app/components/FilterBar.tsx | 5 +- 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 MOBILE.md diff --git a/MOBILE.md b/MOBILE.md new file mode 100644 index 0000000..dd5595b --- /dev/null +++ b/MOBILE.md @@ -0,0 +1,71 @@ +# Mobile design baseline + +Mobile (≥55% of traffic) is the primary target for this app. Any new +screen or component must be designed at the **360 px** viewport first +and verified at three reference widths before merge. + +## Reference viewports + +| Width | Device class | Purpose | +| --- | --- | --- | +| 360 px | Low-end Android (Samsung A-series, older Pixels) | Hard floor — if it doesn't fit here it isn't shipping | +| 390 px | iPhone 14 / 15 / 16 (38% of mobile traffic) | Primary iOS target | +| 430 px | iPhone 16 Pro Max, large Android | Upper mobile bound | + +## Acceptance checks for any screen change + +Before raising a PR that touches user-visible UI, confirm at each +reference width: + +1. **No horizontal overflow.** `document.documentElement.scrollWidth === + window.innerWidth`. The most reliable check: in DevTools console run + ```js + document.documentElement.scrollWidth - innerWidth + ``` + It must read `0`. Any positive number means something is bleeding + past the right edge — usually a fixed-width element, an inline-block + that didn't wrap, or a flex row missing `flex-wrap: wrap`. +2. **Tap targets ≥ 44 × 44 px** on every interactive element (iOS Human + Interface Guidelines minimum). Probe with: + ```js + Array.from(document.querySelectorAll('a, button, [role=button], input, select')) + .filter(el => el.offsetParent) + .map(el => ({ t: el.innerText?.trim().slice(0,30), r: el.getBoundingClientRect() })) + .filter(o => o.r.width < 44 || o.r.height < 44) + ``` +3. **No text below 11 px** in any visible-by-default block. Decorative + demo content (illustrations, mocked previews) should either scale up + or be hidden under the `640 px` breakpoint — see `MOB-04` for the + pattern used on the home page's "What you'll see" section. +4. **iOS Chrome bottom-bar parity.** The fixed `Navigation` bottom tab + bar already compensates for the auto-hiding URL bar via the Visual + Viewport API (`Navigation.tsx`). New fixed-bottom elements must + either use the same offset (read `var(--mobile-bar-offset)`) or sit + inside the existing tab-bar container. +5. **Safe-area insets** on any new sticky/fixed chrome: + `padding-bottom: env(safe-area-inset-bottom)` for bottom-pinned UI, + `padding-inline: env(safe-area-inset-left/right)` for header-class + chrome that runs full bleed. +6. **`dvh`, not `vh`.** iOS Safari's collapsing toolbar makes raw `vh` + units jump. Prefer `100dvh` (with a `100vh` fallback if you support + older engines) for any height that needs to track the visible + viewport. + +## Component patterns + +- **Hide-on-mobile decoration:** wrap with `@media (max-width: 640px) { + .x { display: none; } }` — examples in `HomeView.module.css` + (`.hiwVisual`), `MetricTooltip.module.css` (`.wrapper`). +- **Right-edge scroll-fade for horizontal scrollers:** + `mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);` + Drop the fade when scrolled to the end with a JS-toggled class — see + `SchoolDetailView.tsx`'s `sectionNavAtEnd` state for the pattern. + +## Automation (future) + +A Playwright regression test that asserts `docW === vw` at the three +reference widths on `/`, `/rankings`, `/admissions`, `/compare`, and a +representative `/school/:urn` page would catch overflow regressions +immediately. Not added yet — Playwright isn't currently in the project +dependency set, and the existing Jest setup doesn't compute layout. +Worth adding if mobile overflow regressions recur. diff --git a/nextjs-app/components/FilterBar.module.css b/nextjs-app/components/FilterBar.module.css index 8888402..485ba3a 100644 --- a/nextjs-app/components/FilterBar.module.css +++ b/nextjs-app/components/FilterBar.module.css @@ -261,6 +261,21 @@ color: var(--text-primary, #1a1612); } +/* When filters are applied, promote the toggle to a coral pill so users + can see at a glance that the result list is being narrowed. */ +.advancedToggleActive { + border-color: var(--accent-coral, #e07256); + background: var(--accent-coral-bg, rgba(224, 114, 86, 0.12)); + color: var(--accent-coral, #e07256); + font-weight: 600; +} + +.advancedToggleActive:hover { + border-color: var(--accent-coral-dark, #c45a3f); + background: var(--accent-coral-bg, rgba(224, 114, 86, 0.18)); + color: var(--accent-coral-dark, #c45a3f); +} + .chevronDown, .chevronUp { display: inline-block; diff --git a/nextjs-app/components/FilterBar.tsx b/nextjs-app/components/FilterBar.tsx index 4535f57..708468b 100644 --- a/nextjs-app/components/FilterBar.tsx +++ b/nextjs-app/components/FilterBar.tsx @@ -199,10 +199,11 @@ export function FilterBar({ filters, isHero, resultFilters }: FilterBarProps) {