Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Playwright MCP output (screenshots / snapshots)
.playwright-mcp/
6 changes: 4 additions & 2 deletions SWS_App/src/__tests__/weatherHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
37 changes: 35 additions & 2 deletions SWS_App/src/components/WeatherIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,41 @@ 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'
import thunderstorms from '@bybas/weather-icons/production/fill/all/thunderstorms.svg?raw'
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(/(?<!xlink:)href="#([^"]+)"/g, (_, id) => `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.
Expand Down Expand Up @@ -44,6 +72,7 @@ const SOURCES: Record<string, string> = {
'partly-cloudy-day': partlyCloudyDay,
'partly-cloudy-night': partlyCloudyNight,
fog,
'fog-night': fogNight,
drizzle,
rain,
snow,
Expand All @@ -52,11 +81,15 @@ const SOURCES: Record<string, string> = {
'not-available': notAvailable,
}

const MOON_ICONS = new Set(['clear-night', 'partly-cloudy-night'])

const ANIMATED_ICONS: Record<string, string> = {}
const STATIC_ICONS: Record<string, string> = {}
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 {
Expand Down
3 changes: 2 additions & 1 deletion SWS_App/src/types/weather.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading