Items 16-22: Icon classification, accessibility, responsive design, API params

- Consolidate 5 weather icon methods into classifyWeatherIcon/applyWeatherIconClasses
- Add focus-visible styles, ARIA attributes, keyboard nav on config button/modal
- Add responsive breakpoints for departure cards, weather widget, config modal
- Simplify CSS selectors: replace :not(:is(...)) chains with body.normal
- Fix upsidedown layout margin assumption with transform-based centering
- Upgrade weather icons from @2x to @4x for high-DPI displays
- Add forecast window and transport filter params to SL departures API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 14:39:45 +01:00
parent 1fdb3e48c7
commit cdfd32dc69
6 changed files with 701 additions and 418 deletions

View File

@@ -195,43 +195,42 @@ class WeatherManager {
* Get weather icon URL from icon code
*/
getWeatherIconUrl(iconCode) {
return `https://openweathermap.org/img/wn/${iconCode}@2x.png`;
return `https://openweathermap.org/img/wn/${iconCode}@4x.png`;
}
/**
* Determine if icon represents sun (even behind clouds)
* Classify a weather icon and return the appropriate CSS classes
* @param {string} iconCode - OWM icon code (e.g. '01d', '13n')
* @param {string} condition - Weather condition text (e.g. 'Clear', 'Clouds')
* @returns {string[]} Array of CSS class names to apply
*/
isSunIcon(iconCode, condition) {
// Icon codes: 01d, 01n = clear, 02d, 02n = few clouds, 03d, 03n = scattered, 04d, 04n = broken clouds
const sunIconCodes = ['01d', '01n', '02d', '02n', '03d', '03n', '04d', '04n'];
return sunIconCodes.includes(iconCode) ||
condition.includes('Clear') ||
condition.includes('Clouds');
classifyWeatherIcon(iconCode, condition) {
const code = iconCode ? iconCode.replace(/[dn]$/, '') : '';
// Snow: icon 13x or condition contains 'Snow'
if (code === '13' || condition.includes('Snow')) {
return ['weather-snow'];
}
// Clear sun: icon 01x or condition is exactly 'Clear'
if (code === '01' || condition === 'Clear') {
return ['weather-sun', 'weather-clear-sun'];
}
// Sun behind clouds: icon 02-04x or cloudy condition
if (['02', '03', '04'].includes(code) || (condition.includes('Clouds') && !condition.includes('Clear'))) {
return ['weather-sun', 'weather-clouds-sun'];
}
return [];
}
/**
* Check if icon is clear sun (no clouds)
* Apply weather icon CSS classes to an element
*/
isClearSun(iconCode, condition) {
const clearIconCodes = ['01d', '01n'];
return clearIconCodes.includes(iconCode) || condition === 'Clear';
}
/**
* Check if icon is sun behind clouds
*/
isSunBehindClouds(iconCode, condition) {
const cloudIconCodes = ['02d', '02n', '03d', '03n', '04d', '04n'];
return cloudIconCodes.includes(iconCode) || (condition.includes('Clouds') && !condition.includes('Clear'));
}
/**
* Determine if icon represents snow
*/
isSnowIcon(iconCode, condition) {
// Icon code: 13d, 13n = snow
const snowIconCodes = ['13d', '13n'];
return snowIconCodes.includes(iconCode) || condition.includes('Snow');
applyWeatherIconClasses(element, iconCode, condition) {
element.classList.remove('weather-sun', 'weather-snow', 'weather-clear-sun', 'weather-clouds-sun');
const classes = this.classifyWeatherIcon(iconCode, condition);
if (classes.length > 0) {
element.classList.add(...classes);
}
}
/**
@@ -242,7 +241,7 @@ class WeatherManager {
temperature: 7.1,
condition: 'Clear',
description: 'clear sky',
icon: 'https://openweathermap.org/img/wn/01d@2x.png',
icon: 'https://openweathermap.org/img/wn/01d@4x.png',
iconCode: '01d',
wind: {
speed: 14.8,
@@ -273,7 +272,7 @@ class WeatherManager {
temperature: 7.1 - (i * 0.3), // Decrease temperature slightly each hour
condition: i < 2 ? 'Clear' : 'Clouds',
description: i < 2 ? 'clear sky' : 'few clouds',
icon: i < 2 ? 'https://openweathermap.org/img/wn/01n@2x.png' : 'https://openweathermap.org/img/wn/02n@2x.png',
icon: i < 2 ? 'https://openweathermap.org/img/wn/01n@4x.png' : 'https://openweathermap.org/img/wn/02n@4x.png',
iconCode: i < 2 ? '01n' : '02n',
timestamp: forecastTime,
precipitation: 0
@@ -305,18 +304,8 @@ class WeatherManager {
if (iconElement) {
iconElement.src = this.weatherData.icon;
iconElement.alt = this.weatherData.description;
// Add classes and data attributes for color filtering
iconElement.setAttribute('data-condition', this.weatherData.condition);
iconElement.classList.remove('weather-sun', 'weather-snow', 'weather-clear-sun', 'weather-clouds-sun');
if (this.isSnowIcon(this.weatherData.iconCode, this.weatherData.condition)) {
iconElement.classList.add('weather-snow');
} else if (this.isClearSun(this.weatherData.iconCode, this.weatherData.condition)) {
iconElement.classList.add('weather-sun', 'weather-clear-sun');
} else if (this.isSunBehindClouds(this.weatherData.iconCode, this.weatherData.condition)) {
iconElement.classList.add('weather-sun', 'weather-clouds-sun');
} else if (this.isSunIcon(this.weatherData.iconCode, this.weatherData.condition)) {
iconElement.classList.add('weather-sun');
}
this.applyWeatherIconClasses(iconElement, this.weatherData.iconCode, this.weatherData.condition);
}
const temperatureElement = document.querySelector('#custom-weather .temperature');
@@ -338,15 +327,7 @@ class WeatherManager {
nowIcon.alt = this.weatherData.description;
nowIcon.width = 56;
nowIcon.setAttribute('data-condition', this.weatherData.condition);
if (this.isSnowIcon(this.weatherData.iconCode, this.weatherData.condition)) {
nowIcon.classList.add('weather-snow');
} else if (this.isClearSun(this.weatherData.iconCode, this.weatherData.condition)) {
nowIcon.classList.add('weather-sun', 'weather-clear-sun');
} else if (this.isSunBehindClouds(this.weatherData.iconCode, this.weatherData.condition)) {
nowIcon.classList.add('weather-sun', 'weather-clouds-sun');
} else if (this.isSunIcon(this.weatherData.iconCode, this.weatherData.condition)) {
nowIcon.classList.add('weather-sun');
}
this.applyWeatherIconClasses(nowIcon, this.weatherData.iconCode, this.weatherData.condition);
nowElement.innerHTML = `
<div class="time">Nu</div>
<div class="icon"></div>
@@ -367,15 +348,7 @@ class WeatherManager {
forecastIcon.alt = forecast.description;
forecastIcon.width = 56;
forecastIcon.setAttribute('data-condition', forecast.condition);
if (this.isSnowIcon(forecast.iconCode, forecast.condition)) {
forecastIcon.classList.add('weather-snow');
} else if (this.isClearSun(forecast.iconCode, forecast.condition)) {
forecastIcon.classList.add('weather-sun', 'weather-clear-sun');
} else if (this.isSunBehindClouds(forecast.iconCode, forecast.condition)) {
forecastIcon.classList.add('weather-sun', 'weather-clouds-sun');
} else if (this.isSunIcon(forecast.iconCode, forecast.condition)) {
forecastIcon.classList.add('weather-sun');
}
this.applyWeatherIconClasses(forecastIcon, forecast.iconCode, forecast.condition);
forecastElement.innerHTML = `
<div class="time">${timeString}</div>
<div class="icon"></div>