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) {