5924e565b1
- 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>
225 lines
5.5 KiB
Vue
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>
|