feat(mobile): promote filter toggle when active + document 360px baseline
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 12s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 47s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 0s
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 12s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 47s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 0s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -199,10 +199,11 @@ export function FilterBar({ filters, isHero, resultFilters }: FilterBarProps) {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={styles.advancedToggle}
|
||||
className={`${styles.advancedToggle}${hasActiveDropdownFilters ? ` ${styles.advancedToggleActive}` : ''}`}
|
||||
onClick={() => setFiltersOpen((v) => !v)}
|
||||
aria-expanded={filtersOpen}
|
||||
>
|
||||
Advanced
|
||||
{hasActiveDropdownFilters ? 'Filters' : 'Advanced'}
|
||||
{hasActiveDropdownFilters
|
||||
? ` (${activeDropdownFilters.length})`
|
||||
: ""}
|
||||
|
||||
Reference in New Issue
Block a user