From 0b91798c965d0a962b2f4b413a06b0c2b3e3e7f8 Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Thu, 22 Jan 2026 10:23:36 +0100 Subject: [PATCH 1/2] update layout for accessibility --- index.html | 47 ++++++++++-------- script.js | 77 ++++++++++++----------------- styles.css | 139 +++++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 190 insertions(+), 73 deletions(-) diff --git a/index.html b/index.html index 7e5dd88..a5d6b45 100644 --- a/index.html +++ b/index.html @@ -10,13 +10,15 @@ +

Rolling Stone Magazine's

Top 500 Albums of All Time

The Greatest Albums Ever Made - 2023 Edition

- @@ -34,9 +36,10 @@
+ -
+
+ - +
-
-
+
+
-
+ +
-
+ +
-
+ +
+
-
+ +
+ +
-
-
+
+

Loading albums...

- diff --git a/script.js b/script.js index 9121da2..236c197 100644 --- a/script.js +++ b/script.js @@ -73,34 +73,14 @@ function setupEventListeners() { // Setup click handlers for stats cards function setupStatsClickHandlers() { - const totalAlbumsCard = document.querySelector('.stat-item:has(#totalAlbums)') || - document.querySelector('#totalAlbums').closest('.stat-item'); - const newAlbumsCard = document.querySelector('.stat-item:has(#newAlbums)') || - document.querySelector('#newAlbums').closest('.stat-item'); - const improvedAlbumsCard = document.querySelector('.stat-item:has(#improvedAlbums)') || - document.querySelector('#improvedAlbums').closest('.stat-item'); - const droppedAlbumsCard = document.querySelector('.stat-item:has(#droppedAlbums)') || - document.querySelector('#droppedAlbums').closest('.stat-item'); - - if (totalAlbumsCard) { - totalAlbumsCard.addEventListener('click', () => handleStatsCardClick('')); - totalAlbumsCard.style.cursor = 'pointer'; - } - - if (newAlbumsCard) { - newAlbumsCard.addEventListener('click', () => handleStatsCardClick('New in 2023')); - newAlbumsCard.style.cursor = 'pointer'; - } - - if (improvedAlbumsCard) { - improvedAlbumsCard.addEventListener('click', () => handleStatsCardClick('improved')); - improvedAlbumsCard.style.cursor = 'pointer'; - } - - if (droppedAlbumsCard) { - droppedAlbumsCard.addEventListener('click', () => handleStatsCardClick('dropped')); - droppedAlbumsCard.style.cursor = 'pointer'; - } + // Stats cards are now buttons with data-filter attributes + const statButtons = document.querySelectorAll('.stat-item[data-filter]'); + statButtons.forEach(button => { + button.addEventListener('click', () => { + const filterValue = button.getAttribute('data-filter'); + handleStatsCardClick(filterValue); + }); + }); } // Handle stats card clicks @@ -235,6 +215,11 @@ function renderAlbums() { if (currentPage === 1) { albumsGrid.innerHTML = ''; + // Announce results to screen readers + const announcement = document.getElementById('resultsAnnouncement'); + if (announcement) { + announcement.textContent = `Showing ${filteredData.length} albums`; + } } albumsToShow.slice(startIndex).forEach(album => { @@ -288,34 +273,34 @@ function createAlbumCard(album) { const nextAlbum = hasNext ? filteredData[currentIndex + 1] : null; card.innerHTML = ` -
-
#${album.Rank}
+
+
#${album.Rank}
-
${escapeHtml(album.Album)}
-
-
${escapeHtml(album.Artist)}
- ${album.Info ? `
${escapeHtml(album.Info)}
` : ''} +

${escapeHtml(album.Artist)}

+ ${album.Info ? `

${escapeHtml(album.Info)}

` : ''}
-
+
${coverImagePath ? `${escapeHtml(album.Album)} by ${escapeHtml(album.Artist)} - ` : - `
🎵
` + ` : + `` }
${statusText}
- ` : ''}
@@ -466,8 +451,9 @@ function handleSort(event) { function handleReverse() { isReversed = !isReversed; - // Update button appearance + // Update button appearance and accessibility state reverseButton.classList.toggle('reversed', isReversed); + reverseButton.setAttribute('aria-pressed', isReversed.toString()); // Reverse the current filtered data filteredData.reverse(); @@ -481,6 +467,7 @@ function resetReverse() { if (isReversed) { isReversed = false; reverseButton.classList.remove('reversed'); + reverseButton.setAttribute('aria-pressed', 'false'); } } diff --git a/styles.css b/styles.css index 7fe683f..df34c50 100644 --- a/styles.css +++ b/styles.css @@ -5,6 +5,40 @@ box-sizing: border-box; } +/* Screen reader only utility class */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Skip link for keyboard navigation */ +.skip-link { + position: absolute; + top: -40px; + left: 0; + padding: 8px 16px; + background: var(--primary-color); + color: var(--text-light); + text-decoration: none; + font-weight: 500; + z-index: 1000; + border-radius: 0 0 4px 0; + transition: top 0.2s ease; +} + +.skip-link:focus { + top: 0; + outline: 2px solid var(--text-light); + outline-offset: 2px; +} + /* Theme Variables */ :root { /* Default theme - Gruvbox */ @@ -228,11 +262,17 @@ body { } .theme-select-header:focus { - outline: none; + outline: 2px solid var(--text-light); + outline-offset: 2px; border-color: rgba(255, 255, 255, 0.6); background: rgba(255, 255, 255, 0.2); } +.theme-select-header:focus-visible { + outline: 2px solid var(--text-light); + outline-offset: 2px; +} + .theme-select-header option { background: var(--card-background); color: var(--text-primary); @@ -269,12 +309,18 @@ body { border: 2px solid #e1e5e9; border-radius: 8px 0 0 8px; font-size: 1rem; - outline: none; transition: border-color 0.3s ease; } .search-input:focus { - border-color: #667eea; + outline: 2px solid var(--primary-color); + outline-offset: -2px; + border-color: var(--primary-color); +} + +.search-input:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: -2px; } .search-button { @@ -299,6 +345,11 @@ body { background: var(--secondary-color); } +.search-button:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + .filters { display: flex; gap: 1rem; @@ -316,7 +367,6 @@ body { border: 2px solid #e1e5e9; border-radius: 8px; font-size: 1rem; - outline: none; transition: border-color 0.3s ease; cursor: pointer; background: var(--card-background); @@ -325,9 +375,16 @@ body { } .filter-select:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; border-color: var(--primary-color); } +.filter-select:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + .filter-select option { background: var(--card-background); color: var(--text-primary); @@ -345,7 +402,11 @@ body { align-items: center; gap: 0.5rem; transition: all 0.3s ease; - outline: none; +} + +.reverse-button:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; } .reverse-button:hover { @@ -401,14 +462,20 @@ body { border: 2px solid #e1e5e9; border-radius: 8px; font-size: 1rem; - outline: none; transition: border-color 0.3s ease; } .rank-input:focus { + outline: 2px solid var(--primary-color); + outline-offset: -2px; border-color: var(--primary-color); } +.rank-input:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: -2px; +} + .theme-selector { margin-left: 1rem; } @@ -418,7 +485,6 @@ body { border: 2px solid #e1e5e9; border-radius: 8px; font-size: 1rem; - outline: none; transition: border-color 0.3s ease; cursor: pointer; background: var(--card-background); @@ -427,9 +493,16 @@ body { } .theme-select:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; border-color: var(--primary-color); } +.theme-select:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + .theme-select option { background: var(--card-background); color: var(--text-primary); @@ -444,7 +517,11 @@ body { font-size: 1rem; cursor: pointer; transition: all 0.3s ease; - outline: none; +} + +.jump-button:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; } .jump-button:hover { @@ -473,6 +550,9 @@ body { box-shadow: 0 4px 20px rgba(0,0,0,0.1); transition: all 0.3s ease; cursor: pointer; + border: none; + font-family: inherit; + width: 100%; } .stat-item:hover { @@ -481,6 +561,11 @@ body { background: var(--hover-background); } +.stat-item:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + .stat-number { display: block; font-size: 2.5rem; @@ -622,12 +707,16 @@ body { transform: scale(0.95); } +.album-share:focus-visible { + outline: 2px solid var(--text-light); + outline-offset: 2px; +} + .album-share.copied { background: var(--secondary-color); } .album-card:hover { - transform: translateY(-4px); box-shadow: 0 8px 32px rgba(0,0,0,0.15); } @@ -782,6 +871,11 @@ body { transform: translateY(0); } +.wikipedia-link:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + .spotify-link { display: inline-block; color: #1db954; /* Spotify green */ @@ -804,6 +898,11 @@ body { transform: translateY(0); } +.spotify-link:focus-visible { + outline: 2px solid #1db954; + outline-offset: 2px; +} + /* Dark theme adjustments for Spotify link */ [data-theme="dark"] .spotify-link, [data-theme="gruvbox"] .spotify-link, @@ -846,6 +945,11 @@ body { transform: translateY(0); } +.next-album-link:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + .next-album-link svg { color: inherit; } @@ -1060,4 +1164,21 @@ body { flex-direction: column; gap: 0.5rem; } +} + +/* Reduced motion preference */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + .loading-spinner { + animation: none; + border: 3px solid var(--primary-color); + } } \ No newline at end of file From 21042ec1338e2e27df582477251d6e13897db6b9 Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Thu, 22 Jan 2026 10:25:07 +0100 Subject: [PATCH 2/2] added AGENTS.md --- AGENTS.md | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..51a7a94 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,187 @@ +# AGENTS.md + +This file provides guidance for AI coding agents working in this repository. + +## Project Overview + +Top 500 Albums analysis with data from Rolling Stone (2020) and Wikipedia (2023). Includes Python data processing scripts, an interactive web interface (vanilla HTML/CSS/JS), and album cover artwork from the iTunes API. + +## Build/Lint/Test Commands + +**No formal build system.** All Python scripts use only the standard library. + +```bash +# Run Python scripts directly +python scripts/download_all_covers.py +python scripts/compare_top500_albums.py + +# Local dev server (required for CORS) +python -m http.server 8000 +``` + +**No tests or linting configured.** Manual testing only - run scripts and verify output, open website and check functionality. + +## Code Style Guidelines + +### Python + +```python +#!/usr/bin/env python3 +"""Module docstring explaining the script's purpose.""" + +import csv +import json +import os +import re +from typing import Dict, List, Optional, Tuple + +# Constants: UPPER_SNAKE_CASE +MAX_RETRIES = 3 + +# Functions/variables: snake_case +def normalize_text(text: str) -> str: + """Normalize text for comparison.""" + pass + +albums_2020 = {} +``` + +**Key patterns:** +- Always use shebang: `#!/usr/bin/env python3` +- Include module-level docstring +- Standard library only - no external dependencies +- Type hints for function signatures +- Use `encoding='utf-8'` and `newline=''` for CSV operations + +**Error handling:** +```python +try: + with urllib.request.urlopen(url, timeout=15) as response: + data = json.loads(response.read().decode()) +except Exception as e: + print(f"Error: {e}") + return None +``` + +### JavaScript + +```javascript +// Global variables at top +let albumsData = []; +const itemsPerPage = 50; + +// DOM refs cached on load +const albumsGrid = document.getElementById('albumsGrid'); + +// Initialize on DOMContentLoaded +document.addEventListener('DOMContentLoaded', function() { + loadData(); +}); + +// Async/await for fetch +async function loadAlbumsData() { + try { + const response = await fetch('data.csv'); + if (!response.ok) throw new Error('Failed'); + // ... + } catch (err) { + console.error('Error:', err); + } +} +``` + +**Key patterns:** +- Variables/functions: camelCase +- Use `console.warn()` for non-critical failures +- Vanilla JS only - no frameworks + +### HTML/CSS + +- Semantic HTML5 elements +- CSS custom properties for theming +- Responsive design with Grid/Flexbox + +## Accessibility Requirements + +All UI changes must follow WCAG 2.1 AA guidelines: + +**Keyboard Navigation:** +- All interactive elements must be keyboard accessible +- Use `tabindex` appropriately (0 for focusable, -1 for programmatic focus) +- Implement visible focus indicators (`:focus-visible` with `outline`) +- Support skip links for main content + +**Screen Readers:** +- Use semantic HTML (`