From 8aca0a7a53c45aaedba465cfeb325b2c326110db Mon Sep 17 00:00:00 2001 From: Tudor Date: Wed, 25 Mar 2026 20:28:03 +0000 Subject: [PATCH] =?UTF-8?q?feat(ui):=20site-wide=20UX/UI=20audit=20?= =?UTF-8?q?=E2=80=94=20unified=20buttons,=20tokens,=20accessibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add shared button system (.btn-primary/secondary/tertiary/active) in globals.css - Replace 40+ hardcoded rgba() values with design tokens across all CSS modules - Add skip link, :focus-visible indicators, and ARIA landmarks - Standardise button labels ("+ Compare" / "✓ Comparing") across all components - Add collapse/minimize toggle to ComparisonToast - Fix heading hierarchy (h3→h2 in ComparisonView) - Add aria-live on search results, aria-label on trend SVGs - Add "Search" nav link, fix footer empty section, unify max-widths - Darken --text-muted for WCAG AA compliance (4.6:1 contrast ratio) - Net reduction of ~180 lines through button style deduplication Co-Authored-By: Claude Opus 4.6 --- nextjs-app/app/globals.css | 114 +++++++++++++++++- nextjs-app/app/layout.tsx | 3 +- .../components/ComparisonToast.module.css | 36 +++--- nextjs-app/components/ComparisonToast.tsx | 54 ++++++--- .../components/ComparisonView.module.css | 43 +------ nextjs-app/components/ComparisonView.tsx | 10 +- nextjs-app/components/FilterBar.module.css | 34 +----- nextjs-app/components/FilterBar.tsx | 4 +- nextjs-app/components/Footer.module.css | 4 +- nextjs-app/components/Footer.tsx | 8 -- nextjs-app/components/HomeView.module.css | 50 +------- nextjs-app/components/HomeView.tsx | 10 +- nextjs-app/components/Navigation.module.css | 4 +- nextjs-app/components/Navigation.tsx | 8 +- nextjs-app/components/RankingsView.module.css | 47 +------- nextjs-app/components/RankingsView.tsx | 6 +- nextjs-app/components/SchoolCard.module.css | 75 +----------- nextjs-app/components/SchoolCard.tsx | 12 +- .../components/SchoolDetailView.module.css | 22 ++-- nextjs-app/components/SchoolRow.module.css | 67 +--------- nextjs-app/components/SchoolRow.tsx | 12 +- .../components/SchoolSearchModal.module.css | 28 +---- nextjs-app/components/SchoolSearchModal.tsx | 4 +- 23 files changed, 237 insertions(+), 418 deletions(-) diff --git a/nextjs-app/app/globals.css b/nextjs-app/app/globals.css index bf97e41..19cca8a 100644 --- a/nextjs-app/app/globals.css +++ b/nextjs-app/app/globals.css @@ -12,7 +12,7 @@ --text-primary: #1a1612; --text-secondary: #5c564d; - --text-muted: #8a847a; + --text-muted: #6d685f; /* Darkened for WCAG AA (4.6:1 on cream) */ --text-inverse: #faf7f2; --accent-coral: #e07256; @@ -20,8 +20,19 @@ --accent-teal: #2d7d7d; --accent-teal-light: #3a9e9e; --accent-gold: #c9a227; + --accent-gold-text: #7a6800; /* WCAG AA safe for text on white/cream */ --accent-navy: #2c3e50; + /* Semantic background tints (replaces hardcoded rgba values) */ + --accent-coral-bg: rgba(224, 114, 86, 0.12); + --accent-teal-bg: rgba(45, 125, 125, 0.12); + --accent-gold-bg: rgba(201, 162, 39, 0.12); + + /* Trend colours */ + --trend-up: #16a34a; + --trend-down: var(--accent-coral); + --trend-stable: var(--text-muted); + /* Button/Action colors */ --primary: #e07256; --primary-dark: #c45a3f; @@ -67,6 +78,107 @@ body { min-height: 100vh; } +/* Skip link — visible only on focus for keyboard users */ +.skip-link { + position: absolute; + top: -100px; + left: 1rem; + z-index: 10000; + padding: 0.5rem 1rem; + background: var(--bg-accent); + color: var(--text-inverse); + font-size: 0.875rem; + font-weight: 600; + border-radius: var(--radius-md); + text-decoration: none; + transition: top 0.15s ease; +} +.skip-link:focus { + top: 0.5rem; +} + +/* Focus indicators — branded and visible on cream background */ +:focus-visible { + outline: 2px solid var(--accent-coral); + outline-offset: 2px; + border-radius: var(--radius-sm); +} + +/* ================================================================ + Shared button classes — use these across all components + ================================================================ */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.375rem; + padding: 0.5rem 1rem; + font-family: inherit; + font-size: 0.875rem; + font-weight: 600; + line-height: 1; + border-radius: 6px; + border: 1px solid transparent; + cursor: pointer; + transition: all var(--transition); + text-decoration: none; + white-space: nowrap; +} +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Primary: coral background — main CTAs (Search, Compare Now) */ +.btn-primary { + background: var(--accent-coral); + color: white; + border-color: var(--accent-coral); +} +.btn-primary:hover:not(:disabled) { + background: var(--accent-coral-dark); + border-color: var(--accent-coral-dark); +} + +/* Secondary: teal outline — supporting actions (+ Compare) */ +.btn-secondary { + background: transparent; + color: var(--accent-teal); + border-color: var(--accent-teal); +} +.btn-secondary:hover:not(:disabled) { + background: var(--accent-teal-bg); +} + +/* Tertiary: subtle gray — low-emphasis (View, Clear) */ +.btn-tertiary { + background: var(--bg-secondary); + color: var(--text-secondary); + border-color: var(--border-color); +} +.btn-tertiary:hover:not(:disabled) { + background: var(--border-color); + color: var(--text-primary); +} + +/* Danger/active: for remove/destructive actions or active toggle state */ +.btn-active { + background: var(--accent-teal-bg); + color: var(--accent-teal); + border-color: var(--accent-teal); +} +.btn-active:hover:not(:disabled) { + background: transparent; + color: var(--accent-coral); + border-color: var(--accent-coral); +} + +/* Small variant */ +.btn-sm { + padding: 0.3rem 0.625rem; + font-size: 0.8125rem; +} + /* Subtle noise texture overlay - editorial paper feel */ .noise-overlay { position: fixed; diff --git a/nextjs-app/app/layout.tsx b/nextjs-app/app/layout.tsx index 264b450..7ba5770 100644 --- a/nextjs-app/app/layout.tsx +++ b/nextjs-app/app/layout.tsx @@ -67,8 +67,9 @@ export default function RootLayout({
+ Skip to main content -
+
{children}
diff --git a/nextjs-app/components/ComparisonToast.module.css b/nextjs-app/components/ComparisonToast.module.css index ac14f1f..0cf4a6a 100644 --- a/nextjs-app/components/ComparisonToast.module.css +++ b/nextjs-app/components/ComparisonToast.module.css @@ -64,20 +64,6 @@ border-top: 1px solid rgba(255, 255, 255, 0.1); } -.btnClear { - background: transparent; - border: none; - color: rgba(250, 247, 242, 0.7); - font-size: 0.85rem; - font-weight: 500; - cursor: pointer; - padding: 0.5rem; - transition: color 0.2s ease; -} - -.btnClear:hover { - color: var(--text-inverse, #faf7f2); -} .btnCompare { background: white; @@ -99,9 +85,31 @@ .toastHeader { display: flex; align-items: center; + justify-content: space-between; margin-bottom: 0.5rem; } +.toastCollapsed .toastHeader { + margin-bottom: 0; +} + +.collapseBtn { + background: none; + border: none; + color: rgba(250, 247, 242, 0.6); + cursor: pointer; + padding: 0.25rem; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.2s ease; +} + +.collapseBtn:hover { + color: var(--text-inverse, #faf7f2); +} + .toastTitle { display: flex; align-items: center; diff --git a/nextjs-app/components/ComparisonToast.tsx b/nextjs-app/components/ComparisonToast.tsx index 5e55d8e..b7325d9 100644 --- a/nextjs-app/components/ComparisonToast.tsx +++ b/nextjs-app/components/ComparisonToast.tsx @@ -9,6 +9,7 @@ import styles from './ComparisonToast.module.css'; export function ComparisonToast() { const { selectedSchools, clearAll, removeSchool } = useComparison(); const [mounted, setMounted] = useState(false); + const [collapsed, setCollapsed] = useState(false); const pathname = usePathname(); useEffect(() => { @@ -24,31 +25,48 @@ export function ComparisonToast() { return (
-
+
{selectedSchools.length} {selectedSchools.length === 1 ? 'school' : 'schools'} selected +
-
- {selectedSchools.map(school => ( -
- - {school.school_name.length > 28 ? school.school_name.slice(0, 28) + '…' : school.school_name} - - + {!collapsed && ( + <> +
+ {selectedSchools.map(school => ( +
+ + {school.school_name.length > 28 ? school.school_name.slice(0, 28) + '…' : school.school_name} + + +
+ ))}
- ))} -
-
- - Compare Now -
+
+ + Compare Now +
+ + )}
); diff --git a/nextjs-app/components/ComparisonView.module.css b/nextjs-app/components/ComparisonView.module.css index 442638a..7e32309 100644 --- a/nextjs-app/components/ComparisonView.module.css +++ b/nextjs-app/components/ComparisonView.module.css @@ -30,24 +30,6 @@ line-height: 1.6; } -.addButton { - padding: 0.625rem 1.25rem; - font-size: 0.9rem; - font-weight: 600; - background: var(--accent-coral, #e07256); - color: white; - border: none; - border-radius: 8px; - cursor: pointer; - transition: all 0.2s ease; - white-space: nowrap; -} - -.addButton:hover { - background: var(--accent-coral-dark, #c45a3f); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(224, 114, 86, 0.3); -} /* Metric Selector */ .metricSelector { @@ -90,7 +72,7 @@ .metricSelect:focus { outline: none; border-color: var(--accent-coral, #e07256); - box-shadow: 0 0 0 3px rgba(224, 114, 86, 0.15); + box-shadow: 0 0 0 3px var(--accent-coral-bg); } .metricSelect optgroup { @@ -366,25 +348,6 @@ border-radius: 0 var(--radius-sm) var(--radius-sm) 0; } -.shareButton { - display: inline-flex; - align-items: center; - gap: 0.375rem; - padding: 0.625rem 1.25rem; - background: var(--bg-secondary); - border: 1px solid var(--border-color, #e0ddd8); - border-radius: 8px; - color: var(--text-secondary); - font-size: 0.9rem; - font-weight: 600; - cursor: pointer; - transition: all var(--transition); -} - -.shareButton:hover { - background: var(--bg-primary); - color: var(--text-primary); -} /* Responsive Design */ @media (max-width: 768px) { @@ -393,10 +356,6 @@ align-items: stretch; } - .addButton { - width: 100%; - } - .header h1 { font-size: 1.75rem; } diff --git a/nextjs-app/components/ComparisonView.tsx b/nextjs-app/components/ComparisonView.tsx index ae55b79..a5dc667 100644 --- a/nextjs-app/components/ComparisonView.tsx +++ b/nextjs-app/components/ComparisonView.tsx @@ -147,10 +147,10 @@ export function ComparisonView({

- - @@ -234,14 +234,14 @@ export function ComparisonView({ -

+

{school.school_name} -

+
{school.local_authority && ( {school.local_authority} diff --git a/nextjs-app/components/FilterBar.module.css b/nextjs-app/components/FilterBar.module.css index faf7758..fb1a7df 100644 --- a/nextjs-app/components/FilterBar.module.css +++ b/nextjs-app/components/FilterBar.module.css @@ -55,7 +55,7 @@ .omniInput:focus { border-color: var(--accent-coral, #e07256); - box-shadow: 0 0 0 3px rgba(224, 114, 86, 0.15); + box-shadow: 0 0 0 3px var(--accent-coral-bg); } .omniInput::placeholder { @@ -65,30 +65,10 @@ .searchButton { padding: 0.875rem 2rem; font-size: 1.05rem; - font-weight: 600; - background: var(--accent-coral, #e07256); - color: white; - border: none; border-radius: 8px; - cursor: pointer; - transition: all 0.2s ease; - display: flex; - align-items: center; - justify-content: center; min-width: 120px; } -.searchButton:hover:not(:disabled) { - background: var(--accent-coral-dark, #c45a3f); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(224, 114, 86, 0.3); -} - -.searchButton:disabled { - opacity: 0.8; - cursor: not-allowed; -} - .spinner { width: 20px; height: 20px; @@ -128,18 +108,6 @@ .clearButton { padding: 0.75rem 1.25rem; font-size: 0.95rem; - font-weight: 500; - background: var(--bg-secondary, #f3ede4); - color: var(--text-secondary, #5c564d); - border: 1px solid var(--border-color, #e5dfd5); - border-radius: 6px; - cursor: pointer; - transition: all 0.2s ease; -} - -.clearButton:hover:not(:disabled) { - background: var(--border-color, #e5dfd5); - color: var(--text-primary, #1a1612); } @media (max-width: 768px) { diff --git a/nextjs-app/components/FilterBar.tsx b/nextjs-app/components/FilterBar.tsx index 6c08b94..401525e 100644 --- a/nextjs-app/components/FilterBar.tsx +++ b/nextjs-app/components/FilterBar.tsx @@ -100,7 +100,7 @@ export function FilterBar({ filters, isHero }: FilterBarProps) { placeholder="Search by school name or postcode (e.g., SW1A 1AA)..." className={styles.omniInput} /> -
@@ -153,7 +153,7 @@ export function FilterBar({ filters, isHero }: FilterBarProps) { {hasActiveFilters && ( - )} diff --git a/nextjs-app/components/Footer.module.css b/nextjs-app/components/Footer.module.css index 9411417..2386e91 100644 --- a/nextjs-app/components/Footer.module.css +++ b/nextjs-app/components/Footer.module.css @@ -5,14 +5,14 @@ } .container { - max-width: 1280px; + max-width: 1400px; margin: 0 auto; padding: 3rem 1.5rem 2rem; } .content { display: grid; - grid-template-columns: 2fr 1fr 1fr; + grid-template-columns: 2fr 1fr; gap: 3rem; margin-bottom: 3rem; } diff --git a/nextjs-app/components/Footer.tsx b/nextjs-app/components/Footer.tsx index f0713b3..666cad6 100644 --- a/nextjs-app/components/Footer.tsx +++ b/nextjs-app/components/Footer.tsx @@ -19,14 +19,6 @@ export function Footer() {

-
-

About

-
    -
  • -
  • -
-
-

Resources

    diff --git a/nextjs-app/components/HomeView.module.css b/nextjs-app/components/HomeView.module.css index 49e518e..7a25735 100644 --- a/nextjs-app/components/HomeView.module.css +++ b/nextjs-app/components/HomeView.module.css @@ -47,8 +47,8 @@ align-items: center; gap: 0.5rem; padding: 0.5rem 1rem; - background: rgba(45, 125, 125, 0.1); - border: 1px solid rgba(45, 125, 125, 0.3); + background: var(--accent-teal-bg); + border: 1px solid rgba(45, 125, 125, 0.25); border-radius: 8px; font-size: 0.875rem; color: var(--accent-teal, #2d7d7d); @@ -234,52 +234,6 @@ flex-shrink: 0; } -.compactBtn { - padding: 0.25rem 0.5rem; - font-size: 0.6875rem; - font-weight: 600; - background: var(--accent-coral, #e07256); - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; - text-align: center; -} - -.compactBtn:hover { - background: var(--accent-coral-dark, #c45a3f); -} - -.compactBtn.compactBtnActive { - background: var(--bg-secondary, #f3ede4); - color: var(--text-secondary, #5c564d); - border: 1px solid var(--border-color, #e5dfd5); -} - -.compactBtn.compactBtnActive:hover { - background: var(--border-color, #e5dfd5); - color: var(--text-primary, #1a1612); -} - -.compactBtnSecondary { - padding: 0.25rem 0.5rem; - font-size: 0.6875rem; - font-weight: 500; - background: transparent; - color: var(--text-secondary, #5c564d); - border: 1px solid var(--border-color, #e5dfd5); - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; - text-decoration: none; - text-align: center; -} - -.compactBtnSecondary:hover { - background: var(--bg-secondary, #f3ede4); - color: var(--text-primary, #1a1612); -} .sectionHeader { margin-bottom: 1rem; diff --git a/nextjs-app/components/HomeView.tsx b/nextjs-app/components/HomeView.tsx index e0e9040..5166e73 100644 --- a/nextjs-app/components/HomeView.tsx +++ b/nextjs-app/components/HomeView.tsx @@ -128,7 +128,7 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp {hasSearch && resultsView === 'list' && (
    -

    +

    {initialSchools.total.toLocaleString()} school {initialSchools.total !== 1 ? 's' : ''} found

    @@ -267,13 +267,13 @@ function CompactSchoolItem({ school, onAddToCompare, isInCompare }: CompactSchoo
    - - Details + + View
diff --git a/nextjs-app/components/Navigation.module.css b/nextjs-app/components/Navigation.module.css index c7941cf..104fd0f 100644 --- a/nextjs-app/components/Navigation.module.css +++ b/nextjs-app/components/Navigation.module.css @@ -8,7 +8,7 @@ } .container { - max-width: 1280px; + max-width: 1400px; margin: 0 auto; padding: 0 1.5rem; display: flex; @@ -89,7 +89,7 @@ .navLink.active { color: var(--accent-coral, #e07256); - background: rgba(224, 114, 86, 0.1); + background: var(--accent-coral-bg); } .navLink.active::after { diff --git a/nextjs-app/components/Navigation.tsx b/nextjs-app/components/Navigation.tsx index 46a9737..4c33a8c 100644 --- a/nextjs-app/components/Navigation.tsx +++ b/nextjs-app/components/Navigation.tsx @@ -35,7 +35,13 @@ export function Navigation() { SchoolCompare -
);