mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 11:23:38 +02:00
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:
parent
ef91b61e20
commit
fef71dd571
4 changed files with 166 additions and 120 deletions
|
|
@ -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} />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue