Files
FabledScribe/frontend/src/components/WeatherCard.vue
T
bvandeusen 5924e565b1 fix: floating mini-chat overlay style + weather precip fallback
- KnowledgeView mini-chat: replace harsh border-top with upward box-shadow
  and rounded top corners (16px); remove padding-bottom from content area
  so widget truly overlays cards without pushing layout; add collapse
  toggle (chevron) that hides messages without closing the conversation
- WeatherCard: show precip_mm as fallback when precipitation_probability_max
  is null but actual rainfall is expected (Open-Meteo omits probability for
  some forecast days even when rain is shown)
- Pass precip_mm through weather service → frontend type definitions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 09:04:47 -04:00

225 lines
5.5 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue'
interface ForecastDay {
day: string
condition: string
high: number
low: number
precip_probability: number | null
precip_mm: number | null
windspeed_max: number
}
interface WeatherData {
location: string
fetched_at: string
current_temp: number
condition: string
today_high: number | null
today_low: number | null
yesterday_high: number | null
yesterday_low: number | null
wind_unit?: string
forecast: ForecastDay[]
}
const props = defineProps<{
weather: WeatherData | null
tempUnit?: string
}>()
function weatherIcon(condition: string): string {
const c = condition.toLowerCase()
if (c.includes('thunderstorm')) return '⛈️'
if (c.includes('hail')) return '🌨️'
if (c.includes('snow showers')) return '🌨️'
if (c.includes('snow')) return '❄️'
if (c.includes('rain showers: violent')) return '⛈️'
if (c.includes('rain showers')) return '🌦️'
if (c.includes('drizzle') || c.includes('rain')) return '🌧️'
if (c.includes('fog')) return '🌫️'
if (c.includes('overcast')) return '☁️'
if (c.includes('partly cloudy')) return '⛅'
if (c.includes('mainly clear')) return '🌤️'
if (c.includes('clear')) return '☀️'
return '🌡️'
}
const unit = computed(() => props.tempUnit ?? 'C')
const tempDelta = computed(() => {
const w = props.weather
if (!w || w.today_high == null || w.yesterday_high == null) return null
const diff = w.today_high - w.yesterday_high
if (Math.abs(diff) < 1) return 'Same as yesterday'
const dir = diff > 0 ? 'warmer' : 'cooler'
return `${Math.abs(diff)}° ${dir} than yesterday`
})
const fetchedAtLabel = computed(() => {
if (!props.weather?.fetched_at) return ''
try {
return new Date(props.weather.fetched_at).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})
} catch {
return ''
}
})
</script>
<template>
<div v-if="weather" class="weather-card">
<div class="weather-header">
<span class="weather-location">{{ weather.location }}</span>
<span class="weather-fetched-at">as of {{ fetchedAtLabel }}</span>
</div>
<div class="weather-current">
<span class="weather-icon">{{ weatherIcon(weather.condition) }}</span>
<span class="weather-temp">{{ weather.current_temp }}°{{ unit }}</span>
<span class="weather-condition">{{ weather.condition }}</span>
</div>
<div class="weather-today" v-if="weather.today_high != null">
Today: {{ weather.today_high }}° / {{ weather.today_low }}°
<span v-if="tempDelta" class="weather-delta"> · {{ tempDelta }}</span>
</div>
<div class="weather-forecast" v-if="weather.forecast.length">
<div v-for="day in weather.forecast" :key="day.day" class="weather-forecast-day">
<span class="forecast-day-name">{{ day.day }}</span>
<span class="forecast-icon">{{ weatherIcon(day.condition) }}</span>
<span class="forecast-condition">{{ day.condition }}</span>
<span class="forecast-temps">{{ day.high }}° / {{ day.low }}°</span>
<span v-if="day.precip_probability != null && day.precip_probability > 0" class="forecast-precip">
💧 {{ day.precip_probability }}%
</span>
<span v-else-if="day.precip_mm != null && day.precip_mm > 0" class="forecast-precip">
💧 {{ day.precip_mm.toFixed(1) }}mm
</span>
<span v-else class="forecast-precip forecast-precip--dry">💧 </span>
<span class="forecast-wind">💨 {{ day.windspeed_max }} {{ weather.wind_unit ?? 'km/h' }}</span>
</div>
</div>
</div>
<div v-else class="weather-card weather-unavailable">
Weather data unavailable will retry at next slot.
</div>
</template>
<style scoped>
.weather-card {
background: color-mix(in srgb, var(--color-surface) 80%, transparent);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 1rem 1.25rem;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.weather-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.5rem;
}
.weather-location {
font-weight: 600;
font-size: 0.95rem;
}
.weather-fetched-at {
color: var(--color-text-muted);
font-size: 0.78rem;
}
.weather-current {
display: flex;
align-items: baseline;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.weather-icon {
font-size: 2rem;
line-height: 1;
}
.weather-temp {
font-size: 2rem;
font-weight: 700;
line-height: 1;
}
.weather-condition {
color: var(--color-text-muted);
font-size: 0.9rem;
}
.weather-today {
color: var(--color-text-secondary);
margin-bottom: 0.75rem;
font-size: 0.85rem;
}
.weather-delta {
color: var(--color-text-muted);
font-size: 0.82rem;
}
.weather-forecast {
display: flex;
gap: 0.75rem;
overflow-x: auto;
padding-top: 0.75rem;
border-top: 1px solid var(--color-border);
}
.weather-forecast-day {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
min-width: 4.5rem;
font-size: 0.8rem;
}
.forecast-day-name {
font-weight: 600;
}
.forecast-icon {
font-size: 1.2rem;
line-height: 1;
}
.forecast-condition {
font-size: 0.7rem;
color: var(--color-text-muted);
text-align: center;
line-height: 1.2;
word-break: break-word;
}
.forecast-temps {
white-space: nowrap;
}
.forecast-precip,
.forecast-wind {
font-size: 0.72rem;
white-space: nowrap;
color: var(--color-text-muted);
}
.forecast-precip--dry {
opacity: 0.35;
}
.weather-unavailable {
color: var(--color-text-muted);
font-style: italic;
}
</style>