diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7151991 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Playwright MCP output (screenshots / snapshots) +.playwright-mcp/ diff --git a/SWS_App/src/__tests__/weatherHelpers.test.ts b/SWS_App/src/__tests__/weatherHelpers.test.ts index 5f980a7..bfb1d09 100644 --- a/SWS_App/src/__tests__/weatherHelpers.test.ts +++ b/SWS_App/src/__tests__/weatherHelpers.test.ts @@ -67,9 +67,11 @@ describe('getWeatherIconName', () => { expect(getWeatherIconName(3, false)).toBe('partly-cloudy-night') }) - it('returns a day/night-agnostic fog icon for codes 45 and 48', () => { + it('returns fog for code 45 and the icy fog-night variant for code 48', () => { expect(getWeatherIconName(45, true)).toBe('fog') - expect(getWeatherIconName(48, false)).toBe('fog') + expect(getWeatherIconName(45, false)).toBe('fog') + expect(getWeatherIconName(48, true)).toBe('fog-night') + expect(getWeatherIconName(48, false)).toBe('fog-night') }) it('returns rain for codes 61-67 regardless of day/night', () => { diff --git a/SWS_App/src/components/WeatherIcon.tsx b/SWS_App/src/components/WeatherIcon.tsx index 4334feb..bf2cb8d 100644 --- a/SWS_App/src/components/WeatherIcon.tsx +++ b/SWS_App/src/components/WeatherIcon.tsx @@ -10,6 +10,7 @@ import clearNight from '@bybas/weather-icons/production/fill/all/clear-night.svg import partlyCloudyDay from '@bybas/weather-icons/production/fill/all/partly-cloudy-day.svg?raw' import partlyCloudyNight from '@bybas/weather-icons/production/fill/all/partly-cloudy-night.svg?raw' import fog from '@bybas/weather-icons/production/fill/all/fog.svg?raw' +import fogNight from '@bybas/weather-icons/production/fill/all/fog-night.svg?raw' import drizzle from '@bybas/weather-icons/production/fill/all/drizzle.svg?raw' import rain from '@bybas/weather-icons/production/fill/all/rain.svg?raw' import snow from '@bybas/weather-icons/production/fill/all/snow.svg?raw' @@ -17,6 +18,33 @@ import thunderstorms from '@bybas/weather-icons/production/fill/all/thunderstorm import thunderstormsRain from '@bybas/weather-icons/production/fill/all/thunderstorms-rain.svg?raw' import notAvailable from '@bybas/weather-icons/production/fill/all/not-available.svg?raw' +// All Meteocons SVGs use short generic gradient IDs (a, b, c…). When multiple +// icons are inlined on the same page those IDs collide and the wrong gradients +// are applied. We prefix every id and every reference to it with the icon name +// so each icon has a private namespace. +// +// IMPORTANT: xlink:href must be handled before the bare href rule, and the +// bare href rule uses a negative lookbehind to avoid double-prefixing the +// "href" fragment that already appears inside "xlink:href". +function prefixIds(svg: string, prefix: string): string { + return svg + .replace(/\bid="([^"]+)"/g, (_, id) => `id="${prefix}-${id}"`) + .replace(/url\(#([^)]+)\)/g, (_, id) => `url(#${prefix}-${id})`) + .replace(/xlink:href="#([^"]+)"/g, (_, id) => `xlink:href="#${prefix}-${id}"`) + .replace(/(? `href="#${prefix}-${id}"`) +} + +// Meteocons colours the moon in night icons with an icy blue (#86c3db / #5eafcf). +// We remap those to a neutral white/silver so the crescent reads as a moon +// rather than an ice crystal. Applied only to icons that have a moon element, +// not to fog-night which intentionally keeps the icy palette. +function neutralMoon(svg: string): string { + return svg + .replace(/#86c3db/gi, '#f0ecd8') + .replace(/#5eafcf/gi, '#d4c98e') + .replace(/#72b9d5/gi, '#c2b87a') +} + // Let the SVG scale to the wrapper instead of its intrinsic size. Strip any // existing width/height first so we never emit duplicate attributes if the // icon set ever ships intrinsic dimensions. @@ -44,6 +72,7 @@ const SOURCES: Record = { 'partly-cloudy-day': partlyCloudyDay, 'partly-cloudy-night': partlyCloudyNight, fog, + 'fog-night': fogNight, drizzle, rain, snow, @@ -52,11 +81,15 @@ const SOURCES: Record = { 'not-available': notAvailable, } +const MOON_ICONS = new Set(['clear-night', 'partly-cloudy-night']) + const ANIMATED_ICONS: Record = {} const STATIC_ICONS: Record = {} for (const [name, svg] of Object.entries(SOURCES)) { - ANIMATED_ICONS[name] = sized(svg) - STATIC_ICONS[name] = toStatic(svg) + const adjusted = MOON_ICONS.has(name) ? neutralMoon(svg) : svg + const prefixed = prefixIds(adjusted, name) + ANIMATED_ICONS[name] = sized(prefixed) + STATIC_ICONS[name] = toStatic(prefixed) } interface WeatherIconProps { diff --git a/SWS_App/src/types/weather.ts b/SWS_App/src/types/weather.ts index 70140e5..0e3c502 100644 --- a/SWS_App/src/types/weather.ts +++ b/SWS_App/src/types/weather.ts @@ -80,7 +80,8 @@ export function getWeatherIconName(code: number, isDay: boolean): string { const dayNight = isDay ? 'day' : 'night' if (code === 0) return `clear-${dayNight}` if (code <= 3) return `partly-cloudy-${dayNight}` - if (code === 45 || code === 48) return 'fog' + if (code === 45) return 'fog' + if (code === 48) return 'fog-night' if (code >= 51 && code <= 57) return 'drizzle' if (code >= 61 && code <= 67) return 'rain' if (code >= 71 && code <= 77) return 'snow'