Files
FabledScribe/frontend/src/components/WeatherCard.vue
T
bvandeusen 30dfbce426 fix(typography): strip chrome italic; reserve italic for emphasis
Per the design rule "italic is for emphasis, not for design", removed
every chrome `font-style: italic` declaration across the frontend.
42 declarations gone across 24 files: empty-state placeholder copy,
loading messages, "no results" hints, ghost text, voice/role labels,
field placeholder text, brand wordmark, et al.

Markdown content emphasis is unaffected — `<em>` and `<i>` tags from
`*emphasis*` markup still render italic via browser default styling
(prose.css doesn't override em behavior). User-typed emphasis in
notes, journal entries, and chat messages keeps its italic.

Specific spots that lost the decorative italic:

- KnowledgeView .filter-label, .empty-narrator
- NoteEditorView .ef-label, link-suggest related
- ChatPanel .empty-msg, .empty-greeting, .role-label
- ChatMessage .role-assistant .role-label (the "Fable" voice tag —
  was italic per the doc's Illuminated Transcript spec, but per the
  new typography rule the speaker tag stays regular and lets the
  border-left + glow do the bubble framing on their own)
- AppHeader .brand-text ("Fabled" wordmark)
- editor-shared.css .title-input::placeholder
- ProjectView .project-title-input::placeholder
- HomeView .urgency-loading
- WorkspaceTaskPanel .empty-group
- WeatherCard .weather-unavailable
- SharedWithMeView .empty-msg
- DiffView empty-state spans
- ToolCallCard .tool-event-more
- SettingsView 7 spots (.you-label, .geo-pending, hint text, etc.)
- + several other empty-state / hint text spots

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 07:54:26 -04:00

278 lines
7.0 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
precip_summary?: string
precip_peak_hour?: string
}
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
precip_summary?: string | null
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 ''
}
})
function hasPrecip(day: ForecastDay): boolean {
return (day.precip_probability != null && day.precip_probability > 0) ||
(day.precip_mm != null && day.precip_mm > 0)
}
</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 v-if="weather.precip_summary" class="weather-precip-summary">
💧 {{ weather.precip_summary }}
</div>
<table class="weather-forecast" v-if="weather.forecast.length">
<thead>
<tr>
<th></th>
<th></th>
<th>Hi / Lo</th>
<th>💧</th>
<th>💨 {{ weather.wind_unit ?? 'km/h' }}</th>
</tr>
</thead>
<tbody>
<tr v-for="day in weather.forecast" :key="day.day">
<td class="forecast-day-name">{{ day.day }}</td>
<td class="forecast-icon">{{ weatherIcon(day.condition) }}</td>
<td class="forecast-temps">{{ day.high }}° / {{ day.low }}°</td>
<td class="forecast-precip" :class="{ 'forecast-precip--dry': !hasPrecip(day) }">
<template v-if="day.precip_summary">
<span class="precip-detail" :title="day.precip_summary">
{{ day.precip_probability }}%
<span v-if="day.precip_peak_hour" class="precip-peak">{{ day.precip_peak_hour }}</span>
</span>
</template>
<template v-else-if="day.precip_probability != null && day.precip_probability > 0">{{ day.precip_probability }}%</template>
<template v-else-if="day.precip_mm != null && day.precip_mm > 0">{{ day.precip_mm.toFixed(1) }}mm</template>
<template v-else>&mdash;</template>
</td>
<td class="forecast-wind">{{ day.windspeed_max }}</td>
</tr>
</tbody>
</table>
</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;
container-type: inline-size;
}
.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: clamp(1.5rem, 5cqi, 2.5rem);
line-height: 1;
}
.weather-temp {
font-size: clamp(1.5rem, 5cqi, 2.5rem);
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.25rem;
font-size: 0.85rem;
}
.weather-delta {
color: var(--color-text-muted);
font-size: 0.82rem;
}
.weather-precip-summary {
color: var(--color-text-secondary);
font-size: 0.83rem;
margin-bottom: 0.75rem;
padding: 0.35rem 0.5rem;
background: color-mix(in srgb, var(--color-primary) 8%, transparent);
border-radius: var(--radius-sm, 6px);
border-left: 2px solid color-mix(in srgb, var(--color-primary) 40%, transparent);
}
.weather-forecast {
width: 100%;
border-collapse: collapse;
margin-top: 0.75rem;
border-top: 1px solid var(--color-border);
font-size: 0.8rem;
}
.weather-forecast thead th {
font-size: 0.68rem;
font-weight: 600;
color: var(--color-text-muted);
text-align: right;
padding: 0.5rem 0.4rem 0.25rem;
white-space: nowrap;
}
.weather-forecast thead th:first-child,
.weather-forecast thead th:nth-child(2) {
text-align: left;
}
.weather-forecast tbody td {
padding: 0.3rem 0.4rem;
white-space: nowrap;
vertical-align: middle;
}
.forecast-day-name {
font-weight: 600;
}
.forecast-icon {
font-size: clamp(0.9rem, 3cqi, 1.3rem);
line-height: 1;
}
.forecast-temps {
text-align: right;
}
.forecast-precip {
text-align: right;
color: var(--color-text-muted);
}
.forecast-precip--dry {
opacity: 0.35;
}
.precip-detail {
display: inline-flex;
align-items: baseline;
gap: 0.3em;
}
.precip-peak {
font-size: 0.7rem;
color: var(--color-text-muted);
opacity: 0.8;
}
.forecast-wind {
text-align: right;
color: var(--color-text-muted);
}
.weather-unavailable {
color: var(--color-text-muted);
}
</style>