feat(school-detail): mobile section nav becomes a menu; Compare shrinks to an icon
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 12s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 48s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s

Fixes the cramped mobile bar where Back, the carried Compare pill and All ▾ all
competed with the scrolling section links:

- On mobile the horizontal swipe strip collapses into a single "Section: <current> ▾"
  button that opens the existing jump-to-section sheet — no fragile horizontal
  swipe, and it doubles as a "you are here" indicator.
- The carried Compare CTA becomes a compact 38px icon on mobile (compare-arrows +
  "add" badge; flips to a teal check when in the comparison), so it no longer
  crowds the section control. Desktop keeps the labelled pill.
- Back is icon-only on mobile (label hidden), text on desktop.
- Desktop is unchanged: horizontal links + All ▾, full CTA in the hero.

Both the mobile section menu and the desktop All ▾ open the same sheet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lh7Js5xSetKNzLVr9ArXLF
This commit is contained in:
Tudor
2026-06-30 23:27:03 +01:00
co-authored by Claude Opus 4.8
parent 758f902b66
commit 2fb7faf659
2 changed files with 202 additions and 44 deletions
@@ -205,6 +205,7 @@
flex: none;
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.3rem 0.625rem;
font-size: 0.75rem;
font-weight: 600;
@@ -294,6 +295,119 @@
color: white;
}
/* ── Mobile: the scrolling links collapse into one "section" menu button ──
(hidden on desktop, where the links fit). */
.sectionNavMenu {
display: none; /* shown only ≤640px */
flex: 1;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
min-height: 38px;
padding: 0.34rem 0.7rem;
background: var(--bg-secondary, #f3ede4);
border: 1px solid var(--border-color, #e5dfd5);
border-radius: 8px;
cursor: pointer;
font-family: var(--font-dm-sans), "DM Sans", sans-serif;
color: var(--text-primary, #1a1612);
}
.sectionNavMenuCur {
display: flex;
align-items: center;
gap: 0.45rem;
min-width: 0;
}
.sectionNavMenuEyebrow {
flex: none;
font-size: 0.64rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text-muted, #6d685f);
}
.sectionNavMenuNow {
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sectionNavMenuChev {
flex: none;
color: var(--text-muted, #6d685f);
font-size: 0.7rem;
}
/* Compact icon version of the Compare CTA, used on mobile. */
.sectionNavCompareIcon {
display: none; /* shown only ≤640px */
position: relative;
flex: none;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border-radius: 9px;
border: 1px solid var(--accent-coral, #e07256);
background: var(--accent-coral, #e07256);
color: white;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease;
}
.sectionNavCompareIcon svg {
width: 19px;
height: 19px;
}
.sectionNavCompareBadge {
position: absolute;
top: -5px;
right: -5px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--bg-card, white);
color: var(--accent-coral-dark, #c45a3f);
border: 1.5px solid var(--accent-coral, #e07256);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 800;
line-height: 1;
}
.sectionNavCompareIconIn {
background: var(--bg-card, white);
border-color: var(--accent-teal, #2d7d7d);
color: var(--accent-teal, #2d7d7d);
}
/* Swap which controls show at the mobile breakpoint. */
@media (max-width: 640px) {
.sectionNavLinks,
.sectionNavCompare,
.sectionNavAll,
.sectionNavBackLabel {
display: none;
}
.sectionNavMenu {
display: flex;
}
.sectionNavCompareIcon {
display: inline-flex;
}
}
/* Compare CTA carried into the bar once the hero's button scrolls away. */
.sectionNavCompare {
flex: none;
@@ -333,13 +447,9 @@
}
}
/* "All ▾" jump menu. */
.sectionNavAllWrap {
flex: none;
position: relative;
}
/* "All ▾" jump menu (desktop). */
.sectionNavAll {
flex: none;
display: inline-flex;
align-items: center;
gap: 0.25rem;
+86 -38
View File
@@ -291,6 +291,9 @@ export function SchoolDetailView({
const heroAcademicYear = latestResults ? formatAcademicYear(latestResults.year) : '';
// Label shown in the mobile "section" menu button — the section in view.
const activeNavLabel = (navItems.find((n) => n.id === activeSection) ?? navItems[0])?.label ?? '';
return (
<div className={styles.container}>
{/* Header */}
@@ -450,8 +453,12 @@ export function SchoolDetailView({
{/* Sticky Section Navigation — docks under the global header */}
<nav className={styles.sectionNav} aria-label="Page sections">
<button onClick={handleBack} className={styles.sectionNavBack}> Back</button>
<button onClick={handleBack} className={styles.sectionNavBack} aria-label="Back">
<span aria-hidden="true"></span>
<span className={styles.sectionNavBackLabel}>Back</span>
</button>
{/* Desktop: scrolling section links */}
<div
ref={sectionLinksRef}
className={`${styles.sectionNavLinks}${sectionNavAtEnd ? ` ${styles.atEnd}` : ''}`}
@@ -468,51 +475,92 @@ export function SchoolDetailView({
))}
</div>
{/* The hero's Compare CTA, carried in once it scrolls out of view */}
{!heroCtaVisible && (
{/* Mobile: a single "section" menu button that opens the jump sheet */}
{navItems.length > 0 && (
<button
onClick={handleComparisonToggle}
className={`${styles.sectionNavCompare}${isInComparison ? ` ${styles.sectionNavCompareIn}` : ''}`}
type="button"
className={styles.sectionNavMenu}
aria-haspopup="menu"
aria-expanded={sectionsOpen}
onClick={() => setSectionsOpen((o) => !o)}
>
{isInComparison ? '✓ Comparing' : '+ Compare'}
<span className={styles.sectionNavMenuCur}>
<span className={styles.sectionNavMenuEyebrow}>Section</span>
<span className={styles.sectionNavMenuNow}>{activeNavLabel}</span>
</span>
<span className={styles.sectionNavMenuChev} aria-hidden="true"></span>
</button>
)}
{navItems.length > 0 && (
<div className={styles.sectionNavAllWrap}>
{/* The hero's Compare CTA, carried in once it scrolls out of view.
Desktop shows a labelled pill; mobile a compact icon. */}
{!heroCtaVisible && (
<>
<button
type="button"
className={styles.sectionNavAll}
aria-haspopup="menu"
aria-expanded={sectionsOpen}
onClick={() => setSectionsOpen((o) => !o)}
onClick={handleComparisonToggle}
className={`${styles.sectionNavCompare}${isInComparison ? ` ${styles.sectionNavCompareIn}` : ''}`}
>
All <span aria-hidden="true"></span>
{isInComparison ? '✓ Comparing' : '+ Compare'}
</button>
{sectionsOpen && (
<>
<div className={styles.sectionsBackdrop} onClick={() => setSectionsOpen(false)} />
<div className={styles.sectionsPanel} role="menu" aria-label="Jump to section">
<div className={styles.sectionsPanelHead}>On this page</div>
{navItems.map(({ id, label }) => (
<a
key={id}
href={`#${id}`}
role="menuitem"
className={`${styles.sectionsItem}${activeSection === id ? ` ${styles.sectionsItemActive}` : ''}`}
onClick={() => {
setSectionsOpen(false);
track('section_nav_used', { section: id, via: 'all_menu' });
}}
>
<span>{label}</span>
{activeSection === id && <span className={styles.sectionsTick} aria-hidden="true"></span>}
</a>
))}
</div>
</>
)}
</div>
<button
onClick={handleComparisonToggle}
className={`${styles.sectionNavCompareIcon}${isInComparison ? ` ${styles.sectionNavCompareIconIn}` : ''}`}
aria-label={isInComparison ? 'In comparison' : 'Add to compare'}
title={isInComparison ? 'In comparison' : 'Add to compare'}
>
{isInComparison ? (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="m5 12 5 5 9-11" />
</svg>
) : (
<>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
<path d="M4 7h13l-3-3" />
<path d="M20 17H7l3 3" />
</svg>
<span className={styles.sectionNavCompareBadge} aria-hidden="true">+</span>
</>
)}
</button>
</>
)}
{/* Desktop: "All ▾" trigger (same sheet as the mobile section menu) */}
{navItems.length > 0 && (
<button
type="button"
className={styles.sectionNavAll}
aria-haspopup="menu"
aria-expanded={sectionsOpen}
onClick={() => setSectionsOpen((o) => !o)}
>
All <span aria-hidden="true"></span>
</button>
)}
{/* Shared jump-to-section sheet (dropdown on desktop, bottom sheet on mobile) */}
{sectionsOpen && (
<>
<div className={styles.sectionsBackdrop} onClick={() => setSectionsOpen(false)} />
<div className={styles.sectionsPanel} role="menu" aria-label="Jump to section">
<div className={styles.sectionsPanelHead}>Jump to section</div>
{navItems.map(({ id, label }) => (
<a
key={id}
href={`#${id}`}
role="menuitem"
className={`${styles.sectionsItem}${activeSection === id ? ` ${styles.sectionsItemActive}` : ''}`}
onClick={() => {
setSectionsOpen(false);
track('section_nav_used', { section: id, via: 'all_menu' });
}}
>
<span>{label}</span>
{activeSection === id && <span className={styles.sectionsTick} aria-hidden="true"></span>}
</a>
))}
</div>
</>
)}
</nav>