fix(wetter): design improvements — scroll chips, hierarchy, dedup names

- LocationPicker: horizontal scroll instead of wrapping chips, action
  buttons now show text labels (Standort, Hinzufügen, Verwalten)
- CurrentConditions: temperature is now the dominant hero element (4rem),
  deduplicate "Berlin, Berlin" → "Berlin", added last-updated timestamp
- Fix "Uebersicht" → "Übersicht" umlaut in tab labels
- Better visual hierarchy: temperature dominates, details recede

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-17 14:24:10 +02:00
parent ef91b61e20
commit fef71dd571
4 changed files with 166 additions and 120 deletions

View file

@ -85,7 +85,7 @@
class:active={activeTab === 'overview'}
onclick={() => (activeTab = 'overview')}
>
Uebersicht
Übersicht
</button>
<button
class="tab"
@ -108,7 +108,11 @@
{:else if weatherStore.weatherData}
{@const data = weatherStore.weatherData}
<CurrentConditions current={data.current} locationName={data.location.name} />
<CurrentConditions
current={data.current}
locationName={data.location.name}
fetchedAt={data.fetchedAt}
/>
<WeatherAlerts alerts={data.alerts} />

View file

@ -1,6 +1,6 @@
<!--
Current weather conditions card — shows temperature, conditions,
wind, humidity, pressure, UV index.
Current weather conditions — hero card with dominant temperature,
conditions, wind, humidity, pressure, UV index, and last-updated time.
-->
<script lang="ts">
import type { CurrentWeather } from '../types';
@ -9,43 +9,62 @@
interface Props {
current: CurrentWeather;
locationName: string;
fetchedAt?: number;
}
let { current, locationName }: Props = $props();
let { current, locationName, fetchedAt }: Props = $props();
/** Deduplicate "Berlin, Berlin" → "Berlin" */
let displayName = $derived(() => {
const parts = locationName.split(', ');
if (parts.length === 2 && parts[0].trim() === parts[1].trim()) return parts[0];
return locationName;
});
let lastUpdated = $derived(() => {
if (!fetchedAt) return '';
return new Date(fetchedAt).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
});
</script>
<div class="current-card">
<div class="location-name">{locationName}</div>
<div class="main-row">
<span class="weather-icon">{getWeatherIcon(current.weatherCode, current.isDay)}</span>
<span class="temperature">{Math.round(current.temperature)}°</span>
<div class="condition-info">
<span class="condition-label">{getWeatherLabel(current.weatherCode)}</span>
<span class="feels-like">Gefuehlt {Math.round(current.feelsLike)}°</span>
<!-- Hero: temperature as dominant element -->
<div class="hero">
<div class="hero-temp-block">
<span class="hero-temp">{Math.round(current.temperature)}°</span>
<span class="hero-icon">{getWeatherIcon(current.weatherCode, current.isDay)}</span>
</div>
<div class="hero-meta">
<span class="hero-condition">{getWeatherLabel(current.weatherCode)}</span>
<span class="hero-location">{displayName()}</span>
<span class="hero-feels">Gefühlt {Math.round(current.feelsLike)}°</span>
</div>
</div>
<!-- Detail grid -->
<div class="detail-grid">
<div class="detail">
<span class="detail-icon">💨</span>
<span class="detail-val">{Math.round(current.windSpeed)} km/h</span>
<span class="detail-lbl">{windDirectionLabel(current.windDirection)}</span>
<span class="detail-lbl">Wind {windDirectionLabel(current.windDirection)}</span>
</div>
<div class="detail">
<span class="detail-icon">💧</span>
<span class="detail-val">{current.humidity}%</span>
<span class="detail-lbl">Feuchtigkeit</span>
</div>
<div class="detail">
<span class="detail-icon">🌡</span>
<span class="detail-val">{Math.round(current.pressure)} hPa</span>
<span class="detail-lbl">Druck</span>
<span class="detail-lbl">Luftdruck</span>
</div>
<div class="detail">
<span class="detail-icon">☀️</span>
<span class="detail-val">{current.uvIndex}</span>
<span class="detail-lbl">UV-Index</span>
</div>
</div>
<!-- Timestamp -->
{#if lastUpdated()}
<div class="timestamp">Aktualisiert {lastUpdated()}</div>
{/if}
</div>
<style>
@ -55,44 +74,55 @@
border-radius: 16px;
padding: 20px;
}
.location-name {
font-size: 0.85rem;
color: var(--text-secondary, #9ca3af);
margin-bottom: 8px;
}
.main-row {
/* Hero — temperature is the dominant element */
.hero {
display: flex;
align-items: center;
align-items: flex-start;
gap: 12px;
margin-bottom: 16px;
}
.weather-icon {
font-size: 3rem;
line-height: 1;
.hero-temp-block {
display: flex;
align-items: flex-start;
gap: 4px;
}
.temperature {
font-size: 3.5rem;
font-weight: 300;
.hero-temp {
font-size: 4rem;
font-weight: 200;
line-height: 1;
color: var(--text-primary, #f3f4f6);
letter-spacing: -2px;
}
.condition-info {
.hero-icon {
font-size: 1.8rem;
margin-top: 4px;
}
.hero-meta {
display: flex;
flex-direction: column;
gap: 2px;
padding-top: 8px;
}
.condition-label {
font-size: 1rem;
.hero-condition {
font-size: 1.1rem;
font-weight: 500;
color: var(--text-primary, #f3f4f6);
}
.feels-like {
font-size: 0.85rem;
.hero-location {
font-size: 0.8rem;
color: var(--text-secondary, #9ca3af);
}
.hero-feels {
font-size: 0.8rem;
color: var(--text-tertiary, #6b7280);
}
/* Detail grid */
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 8px;
gap: 6px;
}
.detail {
display: flex;
@ -103,16 +133,22 @@
border-radius: 8px;
background: var(--card-bg-hover, rgba(255, 255, 255, 0.04));
}
.detail-icon {
font-size: 1.1rem;
}
.detail-val {
font-size: 0.85rem;
font-size: 0.82rem;
font-weight: 500;
color: var(--text-primary, #f3f4f6);
}
.detail-lbl {
font-size: 0.7rem;
color: var(--text-secondary, #9ca3af);
font-size: 0.65rem;
color: var(--text-tertiary, #6b7280);
text-align: center;
}
/* Timestamp */
.timestamp {
margin-top: 10px;
font-size: 0.65rem;
color: var(--text-tertiary, #6b7280);
text-align: right;
}
</style>

View file

@ -1,6 +1,6 @@
<!--
Location picker — saved location chips with delete, GPS button,
search with instant-save, and a manage mode to reorder/remove.
Location picker — horizontal scrolling saved location chips,
GPS/search/manage buttons with labels, manage panel for default/remove.
-->
<script lang="ts">
import type { WeatherLocation, GeocodingResult } from '../types';
@ -81,54 +81,52 @@
</script>
<div class="location-picker">
<!-- Saved location chips -->
<div class="picker-row">
<!-- Saved location chips — horizontal scroll -->
{#if locations.length > 0}
<div class="chips-scroll">
{#each locations as loc (loc.id)}
<button
class="loc-chip"
class:active={isSelected(loc)}
onclick={() => onSelect(loc.lat, loc.lon, loc.name)}
>
{#if loc.isDefault}<span class="default-dot"></span>{/if}
{loc.name}
</button>
{/each}
</div>
{/if}
<!-- Action buttons with labels -->
<div class="actions-row">
<button class="action-btn" onclick={useGps} disabled={locating}>
<span class="action-icon">{locating ? '...' : '📍'}</span>
<span class="action-label">Standort</span>
</button>
<button
class="action-btn"
class:active={showSearch}
onclick={() => {
showSearch = !showSearch;
showManage = false;
}}
>
<span class="action-icon">+</span>
<span class="action-label">Hinzufügen</span>
</button>
{#if locations.length > 0}
<div class="location-chips">
{#each locations as loc (loc.id)}
<button
class="loc-chip"
class:active={isSelected(loc)}
class:is-default={loc.isDefault}
onclick={() => onSelect(loc.lat, loc.lon, loc.name)}
>
{#if loc.isDefault}<span class="default-dot"></span>{/if}
{loc.name}
</button>
{/each}
</div>
{:else}
<span class="no-locations">Keine Orte gespeichert</span>
{/if}
<div class="picker-actions">
<button class="action-btn" onclick={useGps} disabled={locating} title="Aktueller Standort">
{locating ? '...' : '📍'}
</button>
<button
class="action-btn"
class:active={showSearch}
class:active={showManage}
onclick={() => {
showSearch = !showSearch;
showManage = false;
showManage = !showManage;
showSearch = false;
}}
title="Stadt hinzufuegen"
>
+
<span class="action-icon"></span>
<span class="action-label">Verwalten</span>
</button>
{#if locations.length > 0}
<button
class="action-btn manage-btn"
class:active={showManage}
onclick={() => {
showManage = !showManage;
showSearch = false;
}}
title="Orte verwalten"
>
</button>
{/if}
</div>
{/if}
</div>
<!-- Search panel -->
@ -138,7 +136,7 @@
<input
class="search-input"
type="text"
placeholder="Stadt suchen und speichern..."
placeholder="Stadt suchen..."
bind:value={searchQuery}
/>
<button class="search-btn" type="submit" disabled={searching}>
@ -172,7 +170,7 @@
{#if saved}
<span class="already-saved">Gespeichert</span>
{:else}
<span class="save-label">Speichern</span>
<span class="save-label">+ Speichern</span>
{/if}
</div>
{/each}
@ -208,7 +206,7 @@
</div>
</div>
{/each}
<p class="manage-hint">★ = Standard-Ort beim Oeffnen</p>
<p class="manage-hint">★ = Standard-Ort beim Öffnen</p>
</div>
{/if}
</div>
@ -219,37 +217,37 @@
flex-direction: column;
gap: 8px;
}
.picker-row {
/* Horizontal scrolling chips */
.chips-scroll {
display: flex;
align-items: center;
gap: 8px;
gap: 6px;
overflow-x: auto;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
padding: 2px 0;
}
.location-chips {
display: flex;
gap: 4px;
flex-wrap: wrap;
flex: 1;
}
.no-locations {
font-size: 0.8rem;
color: var(--text-tertiary, #6b7280);
flex: 1;
.chips-scroll::-webkit-scrollbar {
height: 0;
}
.loc-chip {
scroll-snap-align: start;
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.8rem;
gap: 5px;
padding: 5px 12px;
border-radius: 16px;
font-size: 0.78rem;
border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.1));
background: var(--card-bg, rgba(255, 255, 255, 0.06));
color: var(--text-secondary, #9ca3af);
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
flex-shrink: 0;
}
.loc-chip.active {
background: var(--accent-subtle, rgba(56, 189, 248, 0.15));
background: rgba(56, 189, 248, 0.15);
color: #38bdf8;
border-color: rgba(56, 189, 248, 0.3);
}
@ -260,31 +258,35 @@
background: #38bdf8;
flex-shrink: 0;
}
.picker-actions {
/* Action buttons with text labels */
.actions-row {
display: flex;
gap: 4px;
flex-shrink: 0;
gap: 6px;
}
.action-btn {
width: 36px;
height: 36px;
border-radius: 18px;
display: flex;
align-items: center;
gap: 4px;
padding: 5px 10px;
border-radius: 8px;
border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.1));
background: var(--card-bg, rgba(255, 255, 255, 0.06));
cursor: pointer;
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary, #9ca3af);
transition: all 0.15s ease;
}
.action-btn:hover,
.action-btn.active {
background: var(--card-bg-hover, rgba(255, 255, 255, 0.1));
color: var(--text-primary, #f3f4f6);
}
.manage-btn {
.action-icon {
font-size: 0.85rem;
line-height: 1;
}
.action-label {
font-size: 0.72rem;
}
/* Panels */

View file

@ -91,7 +91,7 @@
class:active={activeTab === 'overview'}
onclick={() => (activeTab = 'overview')}
>
Uebersicht
Übersicht
</button>
<button
class="tab"
@ -124,7 +124,11 @@
{:else if weatherStore.weatherData}
{@const data = weatherStore.weatherData}
<CurrentConditions current={data.current} locationName={data.location.name} />
<CurrentConditions
current={data.current}
locationName={data.location.name}
fetchedAt={data.fetchedAt}
/>
<WeatherAlerts alerts={data.alerts} />