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'} class:active={activeTab === 'overview'}
onclick={() => (activeTab = 'overview')} onclick={() => (activeTab = 'overview')}
> >
Uebersicht Übersicht
</button> </button>
<button <button
class="tab" class="tab"
@ -108,7 +108,11 @@
{:else if weatherStore.weatherData} {:else if weatherStore.weatherData}
{@const data = 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} /> <WeatherAlerts alerts={data.alerts} />

View file

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

View file

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

View file

@ -91,7 +91,7 @@
class:active={activeTab === 'overview'} class:active={activeTab === 'overview'}
onclick={() => (activeTab = 'overview')} onclick={() => (activeTab = 'overview')}
> >
Uebersicht Übersicht
</button> </button>
<button <button
class="tab" class="tab"
@ -124,7 +124,11 @@
{:else if weatherStore.weatherData} {:else if weatherStore.weatherData}
{@const data = 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} /> <WeatherAlerts alerts={data.alerts} />