mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +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'}
|
||||
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} />
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue