From 2a8ff29ccdbbed0345693063789ecc6a60f2015a Mon Sep 17 00:00:00 2001 From: Tudor Sitaru Date: Mon, 18 May 2026 15:26:01 +0100 Subject: [PATCH] =?UTF-8?q?feat(nav):=20mobile=20bottom=20tab=20bar=20and?= =?UTF-8?q?=20=E2=89=A544px=20logo=20tap=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MOB-01: At ≤640px the top inline nav is hidden and replaced by a fixed bottom tab bar (Search / Compare / Rankings / Admissions) with icon + label, 56px tap targets, env(safe-area-inset-bottom) padding, and the Compare count rendered as a badge on the icon. Eliminates horizontal page overflow on every route (docW was 401–429 at vw 390; now docW === vw). MOB-02: Logo link gains a padded hit area so the touch target is ≥44×44 (was 36×36), without resizing the visual mark. ComparisonToast lifted above the new bottom bar on mobile so the two do not stack on top of each other. body gets a bottom padding equal to the bar height + safe-area inset so page content is never hidden. Co-Authored-By: Claude Opus 4.7 --- nextjs-app/app/globals.css | 7 + .../components/ComparisonToast.module.css | 5 +- nextjs-app/components/Navigation.module.css | 103 +++++++++++- nextjs-app/components/Navigation.tsx | 149 ++++++++++++------ 4 files changed, 208 insertions(+), 56 deletions(-) diff --git a/nextjs-app/app/globals.css b/nextjs-app/app/globals.css index ca6c3d4..2c25287 100644 --- a/nextjs-app/app/globals.css +++ b/nextjs-app/app/globals.css @@ -95,6 +95,13 @@ body { min-height: 100vh; } +/* Reserve space for the fixed mobile bottom tab bar (56px + safe-area inset). */ +@media (max-width: 640px) { + body { + padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px)); + } +} + /* Skip link — visible only on focus for keyboard users */ .skip-link { position: absolute; diff --git a/nextjs-app/components/ComparisonToast.module.css b/nextjs-app/components/ComparisonToast.module.css index 6223860..85dce02 100644 --- a/nextjs-app/components/ComparisonToast.module.css +++ b/nextjs-app/components/ComparisonToast.module.css @@ -169,8 +169,9 @@ @media (max-width: 640px) { .toastContainer { - bottom: 1.5rem; - width: calc(100% - 3rem); + /* Sit above the fixed bottom tab bar (56px + safe-area inset) */ + bottom: calc(56px + env(safe-area-inset-bottom, 0px) + 0.75rem); + width: calc(100% - 2rem); } .toastContent { diff --git a/nextjs-app/components/Navigation.module.css b/nextjs-app/components/Navigation.module.css index 104fd0f..d4e4b15 100644 --- a/nextjs-app/components/Navigation.module.css +++ b/nextjs-app/components/Navigation.module.css @@ -21,11 +21,15 @@ display: flex; align-items: center; gap: 0.75rem; + /* Padded hit area so the logo link is ≥44×44 on touch */ + margin: -0.375rem -0.5rem; + padding: 0.375rem 0.5rem; text-decoration: none; color: var(--text-primary, #1a1612); font-size: 1.25rem; font-weight: 700; transition: color 0.2s ease; + -webkit-tap-highlight-color: transparent; } .logo:hover { @@ -33,11 +37,17 @@ } .logoIcon { + display: inline-flex; width: 36px; height: 36px; color: var(--accent-coral, #e07256); } +.logoIcon svg { + width: 100%; + height: 100%; +} + .logoText { font-family: var(--font-playfair), 'Playfair Display', serif; font-weight: 700; @@ -126,21 +136,102 @@ } } +/* ─── Bottom tab bar (mobile only) ──────────────────────────────── */ + +.bottomBar { + display: none; +} + +.tab { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.2rem; + flex: 1; + min-height: 56px; + padding: 0.375rem 0.25rem; + color: var(--text-secondary, #5c564d); + text-decoration: none; + font-size: 0.6875rem; + font-weight: 500; + letter-spacing: 0.01em; + transition: color 0.15s ease, background-color 0.15s ease; + -webkit-tap-highlight-color: transparent; +} + +.tab:active { + background: var(--bg-secondary, #f3ede4); +} + +.tabActive { + color: var(--accent-coral, #e07256); +} + +.tabIconWrap { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; +} + +.tabIcon { + width: 22px; + height: 22px; +} + +.tabLabel { + line-height: 1; +} + +.tabBadge { + position: absolute; + top: -6px; + right: -10px; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + font-size: 0.6875rem; + font-weight: 700; + color: white; + background: var(--accent-coral, #e07256); + border: 2px solid var(--bg-card, white); + border-radius: 9999px; + animation: badgePop 0.3s ease-out; +} + @media (max-width: 640px) { .container { padding: 0 1rem; + height: 56px; } - .logoText { + /* Hide the top text nav; the bottom bar takes over */ + .nav { display: none; } - .nav { - gap: 0.25rem; + .logoIcon { + width: 32px; + height: 32px; } - .navLink { - padding: 0.5rem 0.75rem; - font-size: 0.875rem; + .bottomBar { + display: flex; + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 1000; + background: var(--bg-card, white); + border-top: 1px solid var(--border-color, #e5dfd5); + box-shadow: 0 -2px 12px rgba(26, 22, 18, 0.06); + /* Respect iPhone home-indicator inset */ + padding-bottom: env(safe-area-inset-bottom, 0); } } diff --git a/nextjs-app/components/Navigation.tsx b/nextjs-app/components/Navigation.tsx index 0c4a18d..73c38a8 100644 --- a/nextjs-app/components/Navigation.tsx +++ b/nextjs-app/components/Navigation.tsx @@ -1,6 +1,6 @@ /** * Navigation Component - * Main navigation header with active link highlighting + * Top header nav for desktop; bottom tab bar for mobile (≤640px). */ 'use client'; @@ -10,63 +10,116 @@ import { usePathname } from 'next/navigation'; import { useComparison } from '@/hooks/useComparison'; import styles from './Navigation.module.css'; +type IconProps = { className?: string }; + +const SearchIcon = ({ className }: IconProps) => ( + +); + +const CompareIcon = ({ className }: IconProps) => ( + +); + +const RankingsIcon = ({ className }: IconProps) => ( + +); + +const AdmissionsIcon = ({ className }: IconProps) => ( + +); + export function Navigation() { const pathname = usePathname(); const { selectedSchools } = useComparison(); const isActive = (path: string) => { - if (path === '/') { - return pathname === '/'; - } + if (path === '/') return pathname === '/'; return pathname.startsWith(path); }; - return ( -
-
- -
- - - - - -
- SchoolCompare - + const items = [ + { href: '/', label: 'Search', Icon: SearchIcon }, + { href: '/compare', label: 'Compare', Icon: CompareIcon }, + { href: '/rankings', label: 'Rankings', Icon: RankingsIcon }, + { href: '/admissions', label: 'Admissions', Icon: AdmissionsIcon }, + ] as const; -
-
+ {label} + + ); + })} + + ); }