feat(mobile): section-nav scroll affordance and drop illegible home previews
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 14s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 52s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 14s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 0s
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 14s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 52s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 14s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 0s
MOB-03: The school-detail section nav (SATs / Admissions / Pupils / Location / History) overflows on phones (scrollWidth 462 vs clientWidth 356) with no signal that more tabs exist past the right edge. Add a right-edge mask-image fade at ≤640px, scroll-snap on each link, and bigger tap targets (min-height 36px, padding bumped). A scroll/resize listener toggles an .atEnd class that removes the fade once the user has scrolled to the last tab. MOB-04: The "What you'll see on every school" cards rendered preview visuals (mini cascade chart, Ofsted badge, compare table) with text scaled down to 6–10px — unreadable on a phone. Hide .hiwVisual at ≤640px and let the explanatory text carry each card; tiny visible text count on the home dropped from 51 to 8. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1003,6 +1003,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* On phones, hide the scaled-down preview visuals (text becomes illegible
|
||||||
|
at this size) and let the explanatory text carry each card. */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.hiwVisual {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.hiwCard {
|
||||||
|
padding: 1rem 1.1rem;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
.hiwTitle {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
.hiwDesc {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Editorial section ───────────────────────────────── */
|
/* ── Editorial section ───────────────────────────────── */
|
||||||
|
|
||||||
.editorial {
|
.editorial {
|
||||||
|
|||||||
@@ -190,6 +190,8 @@
|
|||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
|
||||||
|
scroll-snap-type: x proximity;
|
||||||
|
scroll-padding-inline: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionNav::-webkit-scrollbar {
|
.sectionNav::-webkit-scrollbar {
|
||||||
@@ -202,6 +204,21 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Right-edge fade so users see there's more to scroll to. */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.sectionNav {
|
||||||
|
-webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
|
||||||
|
mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
|
||||||
|
padding-right: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When scrolled to the end, drop the fade so the last item isn't dimmed. */
|
||||||
|
.sectionNav.atEnd {
|
||||||
|
-webkit-mask-image: none;
|
||||||
|
mask-image: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sectionNavBack {
|
.sectionNavBack {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -232,7 +249,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sectionNavLink {
|
.sectionNavLink {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
padding: 0.3rem 0.625rem;
|
padding: 0.3rem 0.625rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -241,6 +259,16 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.sectionNavLink,
|
||||||
|
.sectionNavBack {
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionNavLink:hover {
|
.sectionNavLink:hover {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useComparison } from '@/hooks/useComparison';
|
import { useComparison } from '@/hooks/useComparison';
|
||||||
import { PerformanceChart } from './PerformanceChart';
|
import { PerformanceChart } from './PerformanceChart';
|
||||||
@@ -75,6 +75,29 @@ export function SchoolDetailView({
|
|||||||
const isInComparison = isSelected(schoolInfo.urn);
|
const isInComparison = isSelected(schoolInfo.urn);
|
||||||
|
|
||||||
const [activeSection, setActiveSection] = useState<string>('');
|
const [activeSection, setActiveSection] = useState<string>('');
|
||||||
|
const sectionNavRef = useRef<HTMLElement | null>(null);
|
||||||
|
const [sectionNavAtEnd, setSectionNavAtEnd] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = sectionNavRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const update = () => {
|
||||||
|
const overflow = el.scrollWidth - el.clientWidth;
|
||||||
|
// No overflow → treat as "at end" so the fade is hidden.
|
||||||
|
if (overflow <= 1) {
|
||||||
|
setSectionNavAtEnd(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSectionNavAtEnd(el.scrollLeft >= overflow - 2);
|
||||||
|
};
|
||||||
|
update();
|
||||||
|
el.addEventListener('scroll', update, { passive: true });
|
||||||
|
window.addEventListener('resize', update);
|
||||||
|
return () => {
|
||||||
|
el.removeEventListener('scroll', update);
|
||||||
|
window.removeEventListener('resize', update);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null;
|
const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null;
|
||||||
|
|
||||||
@@ -356,7 +379,11 @@ export function SchoolDetailView({
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Sticky Section Navigation */}
|
{/* Sticky Section Navigation */}
|
||||||
<nav className={styles.sectionNav} aria-label="Page sections">
|
<nav
|
||||||
|
ref={sectionNavRef}
|
||||||
|
className={`${styles.sectionNav}${sectionNavAtEnd ? ` ${styles.atEnd}` : ''}`}
|
||||||
|
aria-label="Page sections"
|
||||||
|
>
|
||||||
<div className={styles.sectionNavInner}>
|
<div className={styles.sectionNavInner}>
|
||||||
<button onClick={() => router.back()} className={styles.sectionNavBack}>← Back</button>
|
<button onClick={() => router.back()} className={styles.sectionNavBack}>← Back</button>
|
||||||
{navItems.length > 0 && <div className={styles.sectionNavDivider} />}
|
{navItems.length > 0 && <div className={styles.sectionNavDivider} />}
|
||||||
|
|||||||
Reference in New Issue
Block a user