diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 51a7a94..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,187 +0,0 @@ -# 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 (` + -
- -
+
192 New in 2023 - -
+
164 Improved - -
+
113 Dropped - +
- -
- -
+
-
- +
+

Loading albums...

- diff --git a/script.js b/script.js index 236c197..9121da2 100644 --- a/script.js +++ b/script.js @@ -73,14 +73,34 @@ function setupEventListeners() { // Setup click handlers for stats cards function setupStatsClickHandlers() { - // 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); - }); - }); + 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'; + } } // Handle stats card clicks @@ -215,11 +235,6 @@ 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 => { @@ -273,34 +288,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}
- ` : ''}
@@ -451,9 +466,8 @@ function handleSort(event) { function handleReverse() { isReversed = !isReversed; - // Update button appearance and accessibility state + // Update button appearance reverseButton.classList.toggle('reversed', isReversed); - reverseButton.setAttribute('aria-pressed', isReversed.toString()); // Reverse the current filtered data filteredData.reverse(); @@ -467,7 +481,6 @@ function resetReverse() { if (isReversed) { isReversed = false; reverseButton.classList.remove('reversed'); - reverseButton.setAttribute('aria-pressed', 'false'); } } diff --git a/styles.css b/styles.css index df34c50..7fe683f 100644 --- a/styles.css +++ b/styles.css @@ -5,40 +5,6 @@ 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 */ @@ -262,17 +228,11 @@ body { } .theme-select-header:focus { - outline: 2px solid var(--text-light); - outline-offset: 2px; + outline: none; 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); @@ -309,18 +269,12 @@ 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 { - 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; + border-color: #667eea; } .search-button { @@ -345,11 +299,6 @@ body { background: var(--secondary-color); } -.search-button:focus-visible { - outline: 2px solid var(--primary-color); - outline-offset: 2px; -} - .filters { display: flex; gap: 1rem; @@ -367,6 +316,7 @@ 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); @@ -375,16 +325,9 @@ 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); @@ -402,11 +345,7 @@ body { align-items: center; gap: 0.5rem; transition: all 0.3s ease; -} - -.reverse-button:focus-visible { - outline: 2px solid var(--primary-color); - outline-offset: 2px; + outline: none; } .reverse-button:hover { @@ -462,20 +401,14 @@ 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; } @@ -485,6 +418,7 @@ 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); @@ -493,16 +427,9 @@ 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); @@ -517,11 +444,7 @@ body { font-size: 1rem; cursor: pointer; transition: all 0.3s ease; -} - -.jump-button:focus-visible { - outline: 2px solid var(--primary-color); - outline-offset: 2px; + outline: none; } .jump-button:hover { @@ -550,9 +473,6 @@ 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 { @@ -561,11 +481,6 @@ 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; @@ -707,16 +622,12 @@ 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); } @@ -871,11 +782,6 @@ 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 */ @@ -898,11 +804,6 @@ 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, @@ -945,11 +846,6 @@ body { transform: translateY(0); } -.next-album-link:focus-visible { - outline: 2px solid var(--primary-color); - outline-offset: 2px; -} - .next-album-link svg { color: inherit; } @@ -1164,21 +1060,4 @@ 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