replica: recon, architecture, design tokens, build log, reference and clone screenshots
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
// Generated from tokens.json by gen_theme.py. Do not edit by hand.
|
||||
package app.wetter.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
object WColor {
|
||||
val bg = Color(0xFFF8F9FF)
|
||||
val surface = Color(0xFFFFFFFF)
|
||||
val surfaceContainer = Color(0xFFECEEF4)
|
||||
val surfaceContainerHigh = Color(0xFFE1E3EA)
|
||||
val border = Color(0xFFC3C6CF)
|
||||
val borderInput = Color(0xFF73777F)
|
||||
val text = Color(0xFF191C20)
|
||||
val textMuted = Color(0xFF43474E)
|
||||
val accent = Color(0xFF2A5EA8)
|
||||
val onAccent = Color(0xFFFFFFFF)
|
||||
val accentContainer = Color(0xFFD6E3FF)
|
||||
val onAccentContainer = Color(0xFF001B3E)
|
||||
val danger = Color(0xFFBA1A1A)
|
||||
val warning = Color(0xFF8A5100)
|
||||
val success = Color(0xFF2E6B30)
|
||||
val darkBg = Color(0xFF111318)
|
||||
val darkSurface = Color(0xFF1D2024)
|
||||
val darkSurfaceContainer = Color(0xFF272A2F)
|
||||
val darkBorder = Color(0xFF43474E)
|
||||
val darkBorderInput = Color(0xFF8D9199)
|
||||
val darkText = Color(0xFFE2E2E9)
|
||||
val darkTextMuted = Color(0xFFC3C6CF)
|
||||
val darkAccent = Color(0xFFA9C7FF)
|
||||
val darkOnAccent = Color(0xFF003063)
|
||||
val darkDanger = Color(0xFFFFB4AB)
|
||||
val darkWarning = Color(0xFFFFB870)
|
||||
val sceneClearDayTop = Color(0xFF7CC4EF)
|
||||
val sceneClearDayBottom = Color(0xFFBFE3F7)
|
||||
val sceneClearNightTop = Color(0xFF3D4F9A)
|
||||
val sceneClearNightBottom = Color(0xFF5B4F8F)
|
||||
val sceneCloudyDayTop = Color(0xFFC9CDD2)
|
||||
val sceneCloudyDayBottom = Color(0xFFDFE2E5)
|
||||
val sceneRainTop = Color(0xFF2E3D4F)
|
||||
val sceneRainBottom = Color(0xFF5A6B7D)
|
||||
val sceneSnowTop = Color(0xFF7C8EA3)
|
||||
val sceneSnowBottom = Color(0xFFD4DDE6)
|
||||
val sceneStormTop = Color(0xFF1F1A33)
|
||||
val sceneStormBottom = Color(0xFF4A3F63)
|
||||
val sceneFogTop = Color(0xFF8A929B)
|
||||
val sceneFogBottom = Color(0xFFC4C9CE)
|
||||
val onScene = Color(0xFFFFFFFF)
|
||||
val onSceneMuted = Color(0xFFE8EDF3)
|
||||
val onSceneDark = Color(0xFF14181D)
|
||||
val sceneCard = Color(0xFF2A3644)
|
||||
val sceneCardLight = Color(0xFFF2F5F8)
|
||||
val alertMinor = Color(0xFFFFF2B8)
|
||||
val onAlertMinor = Color(0xFF3D2F00)
|
||||
val alertSevere = Color(0xFFFFDBC8)
|
||||
val onAlertSevere = Color(0xFF4A1A00)
|
||||
val alertExtreme = Color(0xFF93000A)
|
||||
val onAlertExtreme = Color(0xFFFFFFFF)
|
||||
val aqiGood = Color(0xFF2E7D32)
|
||||
val aqiModerate = Color(0xFFA06A00)
|
||||
val aqiBad = Color(0xFFC62828)
|
||||
val uvHigh = Color(0xFFB34700)
|
||||
val card = Color(0xFFFDF8F3)
|
||||
val onCard = Color(0xFF1F1B17)
|
||||
val onCardMuted = Color(0xFF5A524B)
|
||||
val darkCard = Color(0xFF1F1714)
|
||||
val darkOnCard = Color(0xFFEDE0DB)
|
||||
val darkOnCardMuted = Color(0xFFC4B6B0)
|
||||
val fillPrimary = Color(0xFFD3E3FD)
|
||||
val onFillPrimary = Color(0xFF0B2A55)
|
||||
val fillSecondary = Color(0xFFE9DDFF)
|
||||
val onFillSecondary = Color(0xFF2A1A4D)
|
||||
val fillTertiary = Color(0xFFC8E6C2)
|
||||
val onFillTertiary = Color(0xFF0F3A12)
|
||||
val darkFillPrimary = Color(0xFFA8C7FA)
|
||||
val darkOnFillPrimary = Color(0xFF062E6F)
|
||||
val darkFillTertiary = Color(0xFF2D5A27)
|
||||
val darkOnFillTertiary = Color(0xFFB7F0AC)
|
||||
val darkFillOlive = Color(0xFF3E4A1E)
|
||||
val pill = Color(0xFFF1EBE5)
|
||||
val darkPill = Color(0xFF2B221E)
|
||||
val pillSelected = Color(0xFFA8C7FA)
|
||||
val onPillSelected = Color(0xFF0B2A55)
|
||||
val nowChip = Color(0xFF4E4038)
|
||||
val onNowChip = Color(0xFFFFFFFF)
|
||||
val precipText = Color(0xFF2A5EA8)
|
||||
val darkPrecipText = Color(0xFFA8C7FA)
|
||||
val nowcastDot = Color(0xFF2E7D32)
|
||||
val iconSun = Color(0xFFFFB02E)
|
||||
val iconSunCore = Color(0xFFFF9A1F)
|
||||
val iconMoon = Color(0xFFA9C4FF)
|
||||
val iconCloud = Color(0xFFF4F6F9)
|
||||
val iconCloudShade = Color(0xFFCFD6DF)
|
||||
val iconCloudDark = Color(0xFF9AA5B3)
|
||||
val iconDrop = Color(0xFF3D7BF0)
|
||||
val iconSnow = Color(0xFFFFFFFF)
|
||||
val iconSnowOutline = Color(0xFF9FB3CC)
|
||||
val iconBolt = Color(0xFFFFC93C)
|
||||
val iconFog = Color(0xFFC3CAD3)
|
||||
val sceneGlowSun = Color(0xFFFFF3C4)
|
||||
val sceneCloud = Color(0xFFFFFFFF)
|
||||
val sceneHill = Color(0xFF9FC5E6)
|
||||
val darkSceneHill = Color(0xFF2F3F78)
|
||||
val rangeCold = Color(0xFF6FA8FF)
|
||||
val rangeWarm = Color(0xFFFFB02E)
|
||||
val aqiBar1 = Color(0xFF4FD1C5)
|
||||
val aqiBar2 = Color(0xFF9CCC65)
|
||||
val aqiBar3 = Color(0xFFFFD54F)
|
||||
val aqiBar4 = Color(0xFFFF8A65)
|
||||
val aqiBar5 = Color(0xFFE53935)
|
||||
val aqiBar6 = Color(0xFF8E24AA)
|
||||
val uvDotLow = Color(0xFF5CCF8A)
|
||||
val uvDotOff = Color(0xFF6B5A52)
|
||||
}
|
||||
|
||||
object WType {
|
||||
val labelSm = TextStyle(fontSize = 11.sp, lineHeight = 16.sp, fontWeight = FontWeight.W500)
|
||||
val label = TextStyle(fontSize = 12.sp, lineHeight = 16.sp, fontWeight = FontWeight.W500)
|
||||
val bodySm = TextStyle(fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight.W400)
|
||||
val body = TextStyle(fontSize = 16.sp, lineHeight = 24.sp, fontWeight = FontWeight.W400)
|
||||
val title = TextStyle(fontSize = 18.sp, lineHeight = 24.sp, fontWeight = FontWeight.W500)
|
||||
val headline = TextStyle(fontSize = 24.sp, lineHeight = 32.sp, fontWeight = FontWeight.W500)
|
||||
val metric = TextStyle(fontSize = 32.sp, lineHeight = 40.sp, fontWeight = FontWeight.W500)
|
||||
val display = TextStyle(fontSize = 112.sp, lineHeight = 112.sp, fontWeight = FontWeight.W700)
|
||||
val displayWidget = TextStyle(fontSize = 44.sp, lineHeight = 48.sp, fontWeight = FontWeight.W400)
|
||||
val dayRange = TextStyle(fontSize = 56.sp, lineHeight = 60.sp, fontWeight = FontWeight.W600)
|
||||
val heroCondition = TextStyle(fontSize = 22.sp, lineHeight = 28.sp, fontWeight = FontWeight.W400)
|
||||
val cardTitle = TextStyle(fontSize = 16.sp, lineHeight = 24.sp, fontWeight = FontWeight.W500)
|
||||
}
|
||||
|
||||
object WSpace {
|
||||
val s0 = 0.dp
|
||||
val s2 = 2.dp
|
||||
val s4 = 4.dp
|
||||
val s8 = 8.dp
|
||||
val s12 = 12.dp
|
||||
val s16 = 16.dp
|
||||
val s20 = 20.dp
|
||||
val s24 = 24.dp
|
||||
val s32 = 32.dp
|
||||
val s48 = 48.dp
|
||||
}
|
||||
|
||||
object WRadius {
|
||||
val xs = 8.dp
|
||||
val sm = 12.dp
|
||||
val md = 16.dp
|
||||
val lg = 24.dp
|
||||
val xl = 28.dp
|
||||
val widget = 24.dp
|
||||
val pill = 999.dp
|
||||
val card = 28.dp
|
||||
val dayPill = 24.dp
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
# Komponenten: Wetter-Klon (Compose, Material 3 Expressive)
|
||||
|
||||
Tokens: `tokens.json` → `Tokens.kt` (über `gen_theme.py`). Komponenten verwenden nur Rollen (`WColor.onScene`), nie rohe Hex-Werte.
|
||||
Icons: Material Symbols Rounded (Apache 2.0) für die UI. Die Wettericons sind eigene oder [Meteocons](https://github.com/basmilius/weather-icons) (MIT), animiert über Lottie. Kein Google-Artwork.
|
||||
Schrift: Roboto Flex (OFL). Texte sind alle neu geschrieben, auf Deutsch.
|
||||
|
||||
## Grundregel Kontrast über Szenen
|
||||
|
||||
Text auf dem animierten Himmel nutzt `on-scene`, bei hellen Szenen (Schnee, Nebel, unterer Tagesverlauf) `on-scene-dark`. Die Szene meldet ihre Helligkeit (`scene.isLight`), und das Theme wählt danach.
|
||||
Beim klaren Tageshimmel oben besteht weißer Text nur als Großtext (3,9:1). Kleiner Text dort braucht deshalb einen Scrim, einen Verlauf von 0 auf 25 % Schwarz hinter dem Hero-Kopf.
|
||||
Karten auf der Szene: `scene-card` mit 85 % Deckkraft plus Blur (API 31+, `RenderEffect`). Ohne Blur wird `scene-card` voll deckend gezeigt.
|
||||
|
||||
---
|
||||
|
||||
```
|
||||
WeatherScene (animierter Hintergrund)
|
||||
variants clear, partly-cloudy, cloudy, rain, drizzle, snow, sleet, storm, fog × day/night; intensity 0..1
|
||||
layers sky gradient (scene-*-top/bottom) → sun/moon+stars → clouds (AGSL noise) → precipitation particles → lightning flash → fog
|
||||
states playing, paused (off-screen / lifecycle STOPPED), reduced (battery saver, "Animationen entfernen", setting off → static gradient), transitioning (crossfade 600ms between conditions)
|
||||
motion parallax 0.3× on scroll, wind direction tilts rain, haptic tick on lightning (if vibrations on, never during severe alert)
|
||||
a11y decorative: no semantics; condition always also stated in text
|
||||
perf adaptive quality (particle count by frame time), 30fps cap on low-end
|
||||
used on S01, S02 (row backgrounds, static), S12 (pre-rendered bitmap)
|
||||
|
||||
HeroBlock
|
||||
content city name (title) + location pin if current, temperature (display 96sp, wdth 125), condition (headline), "Gefühlt 12°" + H/T (body)
|
||||
states loading (shimmer on numbers, scene neutral), filled, stale ("Stand: vor 2 Std" chip), offline (cloud-off icon chip), no-permission (CTA "Standort erlauben")
|
||||
tokens on-scene / on-scene-dark, space 24 top
|
||||
motion temperature counts up on first load (300ms), collapses into app bar on scroll (large → small top app bar)
|
||||
a11y one merged semantics node: "Berlin, 14 Grad, leicht bewölkt, gefühlt 12, Höchstwert 17, Tiefstwert 9"
|
||||
used on S01
|
||||
|
||||
AlertBanner
|
||||
variants minor (alert-minor), moderate/severe (alert-severe), extreme (alert-extreme)
|
||||
content icon, event ("Sturmböen"), agency + validity ("DWD · bis 18:00"), chevron
|
||||
states single, stacked (n more), expanded (inline first lines), expired (removed with fade)
|
||||
tokens radius lg, padding 16
|
||||
a11y liveRegion polite on appear; whole banner is one button "Warnung: Sturmböen, Deutscher Wetterdienst, bis 18 Uhr. Details"
|
||||
used on S01 (top of cards, replaces Brief), S11
|
||||
|
||||
BriefCard
|
||||
content 1–2 sentences, generated (e.g. "Ab 15 Uhr Regen, abends trocken. Morgen 6° kühler.")
|
||||
states loading (two shimmer lines), filled, hidden (alert active or setting off)
|
||||
tokens scene-card, body, radius xl
|
||||
used on S01
|
||||
|
||||
HourlyStrip
|
||||
content per hour: time label, icon, temp, precip % if ≥ 20 %; sunrise/sunset inserted as items; "Jetzt" first
|
||||
states loading, filled, scroll; selected-hour (tap shows detail tooltip)
|
||||
extra temperature line chart behind items (Canvas, smooth cubic)
|
||||
a11y LazyRow, each item "15 Uhr, 14 Grad, Regen 40 Prozent"
|
||||
used on S01 (fixed position), S04, S12 (widget variant)
|
||||
|
||||
DayRow / DailyList
|
||||
content weekday ("Heute", "Mo"), icon, precip %, low temp, range bar (gradient cold→warm over the 10-day min..max, dot = current temp today), high temp
|
||||
states default, pressed (ripple), expanded (inline hourly, optional)
|
||||
a11y "Montag, Regen 60 Prozent, 8 bis 15 Grad"
|
||||
used on S01, S12
|
||||
|
||||
MetricCard (base for all detail cards)
|
||||
variants small 1×1, wide 2×1; shapes per tokens.shape (squircle, cookie, arch, pill)
|
||||
slots header (icon + label), value (metric 32sp), visual (gauge/chart), footer (one-line explanation)
|
||||
states default, pressed (morph radius→md, spring-fast), dragging (elevation card-drag, scale 1.04, haptic), placeholder (dashed border while dragging), loading, unavailable ("In deiner Region nicht verfügbar")
|
||||
tokens scene-card / scene-card-light, on-scene, radius lg
|
||||
a11y button → opens S05; reorder via accessibility actions "Nach oben/unten verschieben"
|
||||
used on S01, S10
|
||||
|
||||
instances
|
||||
Precip next-hour bar chart (minutely_15) + "Regen in ~20 Min" / total today
|
||||
Wind compass dial with arrow, speed, gusts, Beaufort label
|
||||
UV cookie shape, arc 0–11+, colour by band (uv-high etc.), "Sonnenschutz 11–16 Uhr"
|
||||
Humidity value + dew point, vertical fill
|
||||
Pressure dial with trend arrow (rising/falling over 3h)
|
||||
Visibility value + description
|
||||
Sun arch with sun position dot, sunrise/sunset times, day length
|
||||
AQI pill scale (aqi-good..aqi-bad), value, main pollutant
|
||||
Pollen top 3 types with level dots (Europe only, else hidden)
|
||||
Map static radar preview thumbnail → opens S06
|
||||
|
||||
RadarMap (S06)
|
||||
content MapLibre + OSM base (light/dark style), RainViewer overlay, location dot, attribution
|
||||
controls timeline scrubber (past 2h … +30min), play/pause FAB, time label, legend
|
||||
states loading tiles, playing, paused, no-coverage, offline
|
||||
a11y scrubber is a Slider with "13:40, Vorhersage" values
|
||||
used on S06
|
||||
|
||||
CityRow
|
||||
content name, admin area, local time, temp, condition icon, H/T; current-location row with pin and not deletable
|
||||
states default, pressed, swiping (red bg + trash icon reveal), deleted (snackbar "Gelöscht · Rückgängig"), dragging (reorder)
|
||||
bg static mini scene gradient for the city's condition
|
||||
motion shared element (name + temp) into S01
|
||||
used on S02
|
||||
|
||||
SearchField + ResultList
|
||||
states empty (recents), typing (debounced 300ms, progress), results, no results ("Kein Ort gefunden"), error (retry)
|
||||
a11y SearchBar (M3), results announce count
|
||||
used on S03
|
||||
|
||||
TopAppBar
|
||||
variants transparent over scene (S01), surface (S02, S08)
|
||||
content menu/list button, city title (collapsed), settings icon (no account avatar)
|
||||
used on S01, S02, S08
|
||||
|
||||
PagerIndicator
|
||||
dots for saved cities, current one wide (expressive pill); swipe between cities
|
||||
used on S01
|
||||
|
||||
Settings rows
|
||||
variants radio group, switch, link row, section header
|
||||
states default, disabled (with reason text), focused
|
||||
used on S08, S09
|
||||
|
||||
Snackbar, Dialog (permission rationale), Shimmer placeholder: M3 standard with tokens.
|
||||
```
|
||||
|
||||
## Widgets (Glance)
|
||||
|
||||
```
|
||||
Widget shell
|
||||
radius widget (24dp, follows system launcher radius on Android 12+)
|
||||
styles dynamic_color (surface-container from wallpaper), condition (pre-rendered scene bitmap + on-scene text), transparent (text with shadow)
|
||||
states loading ("…"), filled, stale (small clock icon), no location ("Ort wählen"), error (tap to retry)
|
||||
tap opens S01 for that city; refresh icon only on ≥ 4×2
|
||||
|
||||
NowWidget 2×1: icon + temp | 2×2: + city, condition, H/T
|
||||
HourlyWidget 4×2: now block + 6 hours (icon, temp, time)
|
||||
DailyWidget 4×2: now + 5 days | 4×3: + range bars
|
||||
FullWidget 4×4: now, 5 hours, 5 days, alert line if active
|
||||
GlanceWidget 4×1: "Fr., 17. Okt. · ☀ 14°" one line, alert replaces date (two lines: event, agency)
|
||||
|
||||
type display-widget 44sp for temps, label for times; size by SizeMode.Responsive breakpoints
|
||||
a11y contentDescription on root summarising the widget
|
||||
config S12-config activity: location picker, style, live preview, "Fertig"
|
||||
```
|
||||
|
||||
## Screens → Komponenten (Status)
|
||||
|
||||
| Screen | Komponenten |
|
||||
| --- | --- |
|
||||
| S01 | WeatherScene, TopAppBar, HeroBlock, AlertBanner, BriefCard, HourlyStrip, DailyList, MetricCard×n, PagerIndicator |
|
||||
| S02 | TopAppBar, CityRow, SearchField |
|
||||
| S03 | SearchField, ResultList |
|
||||
| S04/S05 | HourlyStrip, chart, MetricCard expanded |
|
||||
| S06 | RadarMap |
|
||||
| S08/S09 | Settings rows |
|
||||
| S10 | MetricCard (dragging/placeholder) |
|
||||
| S11 | AlertBanner expanded, text |
|
||||
| S12 | Widgets |
|
||||
|
||||
## Abgleich mit Screenshots (ref-01..04), überschreibt Abweichungen oben
|
||||
|
||||
Gemessen an `replica/screens/ref-*.png`. Das Layout-Muster übernehmen wir, die Grafik zeichnen wir selbst.
|
||||
|
||||
- **HeroBlock:** zentrierte Spalte. Oben die Wetterlage (22sp), darunter die Temperatur sehr groß und **fett** (112sp, 700, nicht dünn) mit dem Wettericon direkt rechts daneben, etwa 40 % der Ziffernhöhe, oben ausgerichtet. Dann „Gefühlt 19°“ und „Höchstwert 22° · Tiefstwert 14°“. Am Tag ist der Text dunkel auf hellem Himmel, nachts weiß. Es gibt keinen Scrim, die hellen Szenen sind entsprechend hell gewählt.
|
||||
- **Szene:** ruhiger, weicher Verlauf mit großen, unscharfen, langsam treibenden Formen (Wolkenschemen, Hügel). Keine fotorealistische Darstellung. Bei Niederschlag fallen dezente Partikel.
|
||||
- **Karten-Container:** deckend (`card` bzw. `dark-card`, warm getöntes Neutral), Radius 28, Padding 20. Die Kopfzeile besteht aus einem Outline-Icon und dem Titel (card-title). Zwischen den Karten 12 dp Abstand, nach außen 16 dp Rand.
|
||||
- **HourlyStrip:** Die Spalten zeigen Temperatur, darunter Icon, darunter Niederschlag in % (`precip-text`, klein), darunter die Uhrzeit. Die aktuelle Stunde trägt einen **Cookie-Chip** (`now-chip`) hinter der Temperatur, das Label heißt „Jetzt“.
|
||||
- **DailyList:** **horizontal** scrollende Reihe vertikaler **Kapseln** (`pill`, Radius 24, ca. 56×190 dp). In jeder Kapsel stehen Höchstwert, Tiefstwert (muted), Icon, Niederschlag in % und Tag plus Datum. Ein Tipp auf die Kapsel öffnet S04.
|
||||
- **DayDetail (S04, bestätigt):** oben eine Kapselreihe, die gewählte Kapsel ist hervorgehoben (`pill-selected`). Darunter Datum, Ort, „24°/14°“ (day-range 56sp) mit Icon, Wetterlage, die stündliche Vorhersage für diesen Tag und darunter dieselben Detailkarten.
|
||||
- **Detailkarten:** Raster mit 2 Spalten aus quadratischen Kacheln, **Formen statt einheitlicher Karten**:
|
||||
- Wind: Kreis, darin ein abgerundetes Dreieck als Blob (`fill-*`), gedreht in die Windrichtung, mittig Wert und Einheit, darunter „Aus NW“
|
||||
- UV: Cookie mit 12 Wellen, Wert und Stufe, Punkte auf einem unteren Bogen, der aktive Punkt farbig
|
||||
- Sichtweite: Kreis mit gewellter Innenfläche (`fill-tertiary`), Wert und Einheit
|
||||
- Luftfeuchte: Rechteck, Füllstand mit gewellter Oberkante, Höhe = %, unten ein runder Chip mit dem Taupunkt
|
||||
- Luftdruck: Kreis mit 270°-Bogenanzeige, Wert groß, Einheit darunter
|
||||
- Sonne: Rechteck mit gefülltem Sinus-Hügel über der Horizontlinie, der Nachtteil dunkler, Sonnenpunkt auf der Kurve, Aufgang und Untergang als Zeiten
|
||||
- Luftqualität: Rechteck mit Verlaufsbalken und Knopf, Wert, Einstufung („Gute Luftqualität“); ohne Daten wird „– –“ gezeigt
|
||||
- **NowcastCard (neu):** „Regen ab 09:15“ als Titel, Untertitel „Leichter Regen von 09:15 bis 10:45“. Ein Punkt-Balkendiagramm zeigt die nächsten 2–3 Stunden in 15-Minuten-Schritten, die Y-Achse in drei Stufen (leicht, mäßig, stark). Ein Marker-Chip am Beginn, die X-Achse mit Uhrzeiten. Die Karte erscheint nur, wenn in den nächsten 3 Stunden Niederschlag kommt. Daten: Open-Meteo `minutely_15`.
|
||||
- **BriefCard:** wie eine normale Karte mit Icon, Titel „Kurz gesagt“ und einem Chevron zum Auf- und Zuklappen. Darunter 2–3 Sätze Text in body.
|
||||
- **Vorschau eines Orts (S15, neu):** Ein gesuchter Ort öffnet die volle Ansicht mit einem Button „Speichern“ oben rechts (gefüllter Pill-Button, Tonal).
|
||||
- **Abweichungen, die bewusst eigen bleiben:** eigene Icons und Szenen, ein eigener Akzent erst in `/replica-brand`, eigene Texte. Die Kartenformen sind M3-Expressive-Standardformen (`MaterialShapes`: Cookie, Circle, Triangle, Wavy), damit ist nichts vom Original nachgezeichnet.
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""tokens.json -> Tokens.kt (Compose). Re-run after every token change."""
|
||||
import json, re, sys, pathlib
|
||||
src = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "tokens.json")
|
||||
out = pathlib.Path(sys.argv[2] if len(sys.argv) > 2 else src.with_name("Tokens.kt"))
|
||||
t = json.loads(src.read_text())
|
||||
camel = lambda s: re.sub(r"-(\w)", lambda m: m.group(1).upper(), s)
|
||||
L = ["// Generated from tokens.json by gen_theme.py. Do not edit by hand.",
|
||||
"package app.wetter.ui.theme", "",
|
||||
"import androidx.compose.ui.graphics.Color",
|
||||
"import androidx.compose.ui.text.TextStyle",
|
||||
"import androidx.compose.ui.text.font.FontWeight",
|
||||
"import androidx.compose.ui.unit.dp",
|
||||
"import androidx.compose.ui.unit.sp", "",
|
||||
"object WColor {"]
|
||||
L += [f" val {camel(k)} = Color(0xFF{v.lstrip('#').upper()})" for k, v in t["color"].items()]
|
||||
L += ["}", "", "object WType {"]
|
||||
for k, v in t["type"].items():
|
||||
L.append(f" val {camel(k)} = TextStyle(fontSize = {v['size']}.sp, lineHeight = {v['line']}.sp, fontWeight = FontWeight.W{v['weight']})")
|
||||
L += ["}", "", "object WSpace {"]
|
||||
L += [f" val s{v} = {v}.dp" for v in t["space"]]
|
||||
L += ["}", "", "object WRadius {"]
|
||||
L += [f" val {camel(k)} = {v}.dp" for k, v in t["radius"].items()]
|
||||
L += ["}", ""]
|
||||
out.write_text("\n".join(L))
|
||||
print("wrote", out)
|
||||
@@ -0,0 +1,528 @@
|
||||
{
|
||||
"_about": "Wetter-Klon tokens. Roles measured by eye from replica/screens/ref-01..04 (Pixel Weather, 2024-25). Values are own approximations, no brand colour copied. Names stay, values change in /replica-brand. Check: python3 ~/.claude/skills/replica-design/contrast.py replica/design/tokens.json",
|
||||
"color": {
|
||||
"bg": "#f8f9ff",
|
||||
"surface": "#ffffff",
|
||||
"surface-container": "#eceef4",
|
||||
"surface-container-high": "#e1e3ea",
|
||||
"border": "#c3c6cf",
|
||||
"border-input": "#73777f",
|
||||
"text": "#191c20",
|
||||
"text-muted": "#43474e",
|
||||
"accent": "#2a5ea8",
|
||||
"on-accent": "#ffffff",
|
||||
"accent-container": "#d6e3ff",
|
||||
"on-accent-container": "#001b3e",
|
||||
"danger": "#ba1a1a",
|
||||
"warning": "#8a5100",
|
||||
"success": "#2e6b30",
|
||||
"dark-bg": "#111318",
|
||||
"dark-surface": "#1d2024",
|
||||
"dark-surface-container": "#272a2f",
|
||||
"dark-border": "#43474e",
|
||||
"dark-border-input": "#8d9199",
|
||||
"dark-text": "#e2e2e9",
|
||||
"dark-text-muted": "#c3c6cf",
|
||||
"dark-accent": "#a9c7ff",
|
||||
"dark-on-accent": "#003063",
|
||||
"dark-danger": "#ffb4ab",
|
||||
"dark-warning": "#ffb870",
|
||||
"scene-clear-day-top": "#7cc4ef",
|
||||
"scene-clear-day-bottom": "#bfe3f7",
|
||||
"scene-clear-night-top": "#3d4f9a",
|
||||
"scene-clear-night-bottom": "#5b4f8f",
|
||||
"scene-cloudy-day-top": "#c9cdd2",
|
||||
"scene-cloudy-day-bottom": "#dfe2e5",
|
||||
"scene-rain-top": "#2e3d4f",
|
||||
"scene-rain-bottom": "#5a6b7d",
|
||||
"scene-snow-top": "#7c8ea3",
|
||||
"scene-snow-bottom": "#d4dde6",
|
||||
"scene-storm-top": "#1f1a33",
|
||||
"scene-storm-bottom": "#4a3f63",
|
||||
"scene-fog-top": "#8a929b",
|
||||
"scene-fog-bottom": "#c4c9ce",
|
||||
"on-scene": "#ffffff",
|
||||
"on-scene-muted": "#e8edf3",
|
||||
"on-scene-dark": "#14181d",
|
||||
"scene-card": "#2a3644",
|
||||
"scene-card-light": "#f2f5f8",
|
||||
"alert-minor": "#fff2b8",
|
||||
"on-alert-minor": "#3d2f00",
|
||||
"alert-severe": "#ffdbc8",
|
||||
"on-alert-severe": "#4a1a00",
|
||||
"alert-extreme": "#93000a",
|
||||
"on-alert-extreme": "#ffffff",
|
||||
"aqi-good": "#2e7d32",
|
||||
"aqi-moderate": "#a06a00",
|
||||
"aqi-bad": "#c62828",
|
||||
"uv-high": "#b34700",
|
||||
"card": "#fdf8f3",
|
||||
"on-card": "#1f1b17",
|
||||
"on-card-muted": "#5a524b",
|
||||
"dark-card": "#1f1714",
|
||||
"dark-on-card": "#ede0db",
|
||||
"dark-on-card-muted": "#c4b6b0",
|
||||
"fill-primary": "#d3e3fd",
|
||||
"on-fill-primary": "#0b2a55",
|
||||
"fill-secondary": "#e9ddff",
|
||||
"on-fill-secondary": "#2a1a4d",
|
||||
"fill-tertiary": "#c8e6c2",
|
||||
"on-fill-tertiary": "#0f3a12",
|
||||
"dark-fill-primary": "#a8c7fa",
|
||||
"dark-on-fill-primary": "#062e6f",
|
||||
"dark-fill-tertiary": "#2d5a27",
|
||||
"dark-on-fill-tertiary": "#b7f0ac",
|
||||
"dark-fill-olive": "#3e4a1e",
|
||||
"pill": "#f1ebe5",
|
||||
"dark-pill": "#2b221e",
|
||||
"pill-selected": "#a8c7fa",
|
||||
"on-pill-selected": "#0b2a55",
|
||||
"now-chip": "#4e4038",
|
||||
"on-now-chip": "#ffffff",
|
||||
"precip-text": "#2a5ea8",
|
||||
"dark-precip-text": "#a8c7fa",
|
||||
"nowcast-dot": "#2e7d32",
|
||||
"icon-sun": "#ffb02e",
|
||||
"icon-sun-core": "#ff9a1f",
|
||||
"icon-moon": "#a9c4ff",
|
||||
"icon-cloud": "#f4f6f9",
|
||||
"icon-cloud-shade": "#cfd6df",
|
||||
"icon-cloud-dark": "#9aa5b3",
|
||||
"icon-drop": "#3d7bf0",
|
||||
"icon-snow": "#ffffff",
|
||||
"icon-snow-outline": "#9fb3cc",
|
||||
"icon-bolt": "#ffc93c",
|
||||
"icon-fog": "#c3cad3",
|
||||
"scene-glow-sun": "#fff3c4",
|
||||
"scene-cloud": "#ffffff",
|
||||
"scene-hill": "#9fc5e6",
|
||||
"dark-scene-hill": "#2f3f78",
|
||||
"range-cold": "#6fa8ff",
|
||||
"range-warm": "#ffb02e",
|
||||
"aqi-bar-1": "#4fd1c5",
|
||||
"aqi-bar-2": "#9ccc65",
|
||||
"aqi-bar-3": "#ffd54f",
|
||||
"aqi-bar-4": "#ff8a65",
|
||||
"aqi-bar-5": "#e53935",
|
||||
"aqi-bar-6": "#8e24aa",
|
||||
"uv-dot-low": "#5ccf8a",
|
||||
"uv-dot-off": "#6b5a52"
|
||||
},
|
||||
"pairs": [
|
||||
[
|
||||
"text",
|
||||
"bg"
|
||||
],
|
||||
[
|
||||
"text",
|
||||
"surface"
|
||||
],
|
||||
[
|
||||
"text",
|
||||
"surface-container"
|
||||
],
|
||||
[
|
||||
"text",
|
||||
"surface-container-high"
|
||||
],
|
||||
[
|
||||
"text-muted",
|
||||
"bg"
|
||||
],
|
||||
[
|
||||
"text-muted",
|
||||
"surface"
|
||||
],
|
||||
[
|
||||
"text-muted",
|
||||
"surface-container"
|
||||
],
|
||||
[
|
||||
"on-accent",
|
||||
"accent"
|
||||
],
|
||||
[
|
||||
"on-accent-container",
|
||||
"accent-container"
|
||||
],
|
||||
[
|
||||
"accent",
|
||||
"bg"
|
||||
],
|
||||
[
|
||||
"accent",
|
||||
"surface"
|
||||
],
|
||||
[
|
||||
"danger",
|
||||
"surface"
|
||||
],
|
||||
[
|
||||
"warning",
|
||||
"surface"
|
||||
],
|
||||
[
|
||||
"success",
|
||||
"surface"
|
||||
],
|
||||
[
|
||||
"border-input",
|
||||
"surface",
|
||||
"ui"
|
||||
],
|
||||
[
|
||||
"accent",
|
||||
"surface",
|
||||
"ui"
|
||||
],
|
||||
[
|
||||
"dark-text",
|
||||
"dark-bg"
|
||||
],
|
||||
[
|
||||
"dark-text",
|
||||
"dark-surface"
|
||||
],
|
||||
[
|
||||
"dark-text",
|
||||
"dark-surface-container"
|
||||
],
|
||||
[
|
||||
"dark-text-muted",
|
||||
"dark-bg"
|
||||
],
|
||||
[
|
||||
"dark-text-muted",
|
||||
"dark-surface"
|
||||
],
|
||||
[
|
||||
"dark-text-muted",
|
||||
"dark-surface-container"
|
||||
],
|
||||
[
|
||||
"dark-on-accent",
|
||||
"dark-accent"
|
||||
],
|
||||
[
|
||||
"dark-accent",
|
||||
"dark-surface"
|
||||
],
|
||||
[
|
||||
"dark-danger",
|
||||
"dark-surface"
|
||||
],
|
||||
[
|
||||
"dark-warning",
|
||||
"dark-surface"
|
||||
],
|
||||
[
|
||||
"dark-border-input",
|
||||
"dark-surface",
|
||||
"ui"
|
||||
],
|
||||
[
|
||||
"on-scene",
|
||||
"scene-clear-night-top"
|
||||
],
|
||||
[
|
||||
"on-scene",
|
||||
"scene-clear-night-bottom"
|
||||
],
|
||||
[
|
||||
"on-scene",
|
||||
"scene-rain-top"
|
||||
],
|
||||
[
|
||||
"on-scene",
|
||||
"scene-rain-bottom"
|
||||
],
|
||||
[
|
||||
"on-scene",
|
||||
"scene-storm-top"
|
||||
],
|
||||
[
|
||||
"on-scene",
|
||||
"scene-storm-bottom"
|
||||
],
|
||||
[
|
||||
"on-scene-dark",
|
||||
"scene-clear-day-bottom"
|
||||
],
|
||||
[
|
||||
"on-scene-dark",
|
||||
"scene-cloudy-day-bottom"
|
||||
],
|
||||
[
|
||||
"on-scene-dark",
|
||||
"scene-snow-top"
|
||||
],
|
||||
[
|
||||
"on-scene-dark",
|
||||
"scene-snow-bottom"
|
||||
],
|
||||
[
|
||||
"on-scene-dark",
|
||||
"scene-fog-top"
|
||||
],
|
||||
[
|
||||
"on-scene-dark",
|
||||
"scene-fog-bottom"
|
||||
],
|
||||
[
|
||||
"on-scene",
|
||||
"scene-card"
|
||||
],
|
||||
[
|
||||
"on-scene-muted",
|
||||
"scene-card"
|
||||
],
|
||||
[
|
||||
"on-scene-dark",
|
||||
"scene-card-light"
|
||||
],
|
||||
[
|
||||
"on-alert-minor",
|
||||
"alert-minor"
|
||||
],
|
||||
[
|
||||
"on-alert-severe",
|
||||
"alert-severe"
|
||||
],
|
||||
[
|
||||
"on-alert-extreme",
|
||||
"alert-extreme"
|
||||
],
|
||||
[
|
||||
"aqi-good",
|
||||
"surface",
|
||||
"ui"
|
||||
],
|
||||
[
|
||||
"aqi-moderate",
|
||||
"surface",
|
||||
"ui"
|
||||
],
|
||||
[
|
||||
"aqi-bad",
|
||||
"surface",
|
||||
"ui"
|
||||
],
|
||||
[
|
||||
"uv-high",
|
||||
"surface",
|
||||
"ui"
|
||||
],
|
||||
[
|
||||
"on-scene-dark",
|
||||
"scene-clear-day-top"
|
||||
],
|
||||
[
|
||||
"on-scene-dark",
|
||||
"scene-cloudy-day-top"
|
||||
],
|
||||
[
|
||||
"on-card",
|
||||
"card"
|
||||
],
|
||||
[
|
||||
"on-card-muted",
|
||||
"card"
|
||||
],
|
||||
[
|
||||
"dark-on-card",
|
||||
"dark-card"
|
||||
],
|
||||
[
|
||||
"dark-on-card-muted",
|
||||
"dark-card"
|
||||
],
|
||||
[
|
||||
"on-fill-primary",
|
||||
"fill-primary"
|
||||
],
|
||||
[
|
||||
"on-fill-secondary",
|
||||
"fill-secondary"
|
||||
],
|
||||
[
|
||||
"on-fill-tertiary",
|
||||
"fill-tertiary"
|
||||
],
|
||||
[
|
||||
"dark-on-fill-primary",
|
||||
"dark-fill-primary"
|
||||
],
|
||||
[
|
||||
"dark-on-fill-tertiary",
|
||||
"dark-fill-tertiary"
|
||||
],
|
||||
[
|
||||
"dark-on-card",
|
||||
"dark-fill-olive"
|
||||
],
|
||||
[
|
||||
"on-card",
|
||||
"pill"
|
||||
],
|
||||
[
|
||||
"dark-on-card",
|
||||
"dark-pill"
|
||||
],
|
||||
[
|
||||
"on-pill-selected",
|
||||
"pill-selected"
|
||||
],
|
||||
[
|
||||
"on-now-chip",
|
||||
"now-chip"
|
||||
],
|
||||
[
|
||||
"precip-text",
|
||||
"card"
|
||||
],
|
||||
[
|
||||
"precip-text",
|
||||
"pill"
|
||||
],
|
||||
[
|
||||
"dark-precip-text",
|
||||
"dark-card"
|
||||
],
|
||||
[
|
||||
"dark-precip-text",
|
||||
"dark-pill"
|
||||
],
|
||||
[
|
||||
"nowcast-dot",
|
||||
"card",
|
||||
"ui"
|
||||
]
|
||||
],
|
||||
"font": {
|
||||
"sans": "Roboto Flex (OFL), system default fallback",
|
||||
"display": "Roboto Flex, weight 700-800, tight tracking (-2%) for the hero temperature",
|
||||
"mono": "Roboto Mono"
|
||||
},
|
||||
"type": {
|
||||
"label-sm": {
|
||||
"size": 11,
|
||||
"line": 16,
|
||||
"weight": 500
|
||||
},
|
||||
"label": {
|
||||
"size": 12,
|
||||
"line": 16,
|
||||
"weight": 500
|
||||
},
|
||||
"body-sm": {
|
||||
"size": 14,
|
||||
"line": 20,
|
||||
"weight": 400
|
||||
},
|
||||
"body": {
|
||||
"size": 16,
|
||||
"line": 24,
|
||||
"weight": 400
|
||||
},
|
||||
"title": {
|
||||
"size": 18,
|
||||
"line": 24,
|
||||
"weight": 500
|
||||
},
|
||||
"headline": {
|
||||
"size": 24,
|
||||
"line": 32,
|
||||
"weight": 500
|
||||
},
|
||||
"metric": {
|
||||
"size": 32,
|
||||
"line": 40,
|
||||
"weight": 500
|
||||
},
|
||||
"display": {
|
||||
"size": 112,
|
||||
"line": 112,
|
||||
"weight": 700
|
||||
},
|
||||
"display-widget": {
|
||||
"size": 44,
|
||||
"line": 48,
|
||||
"weight": 400
|
||||
},
|
||||
"day-range": {
|
||||
"size": 56,
|
||||
"line": 60,
|
||||
"weight": 600
|
||||
},
|
||||
"hero-condition": {
|
||||
"size": 22,
|
||||
"line": 28,
|
||||
"weight": 400
|
||||
},
|
||||
"card-title": {
|
||||
"size": 16,
|
||||
"line": 24,
|
||||
"weight": 500
|
||||
}
|
||||
},
|
||||
"space": [
|
||||
0,
|
||||
2,
|
||||
4,
|
||||
8,
|
||||
12,
|
||||
16,
|
||||
20,
|
||||
24,
|
||||
32,
|
||||
48
|
||||
],
|
||||
"radius": {
|
||||
"xs": 8,
|
||||
"sm": 12,
|
||||
"md": 16,
|
||||
"lg": 24,
|
||||
"xl": 28,
|
||||
"widget": 24,
|
||||
"pill": 999,
|
||||
"card": 28,
|
||||
"day-pill": 24
|
||||
},
|
||||
"shape": {
|
||||
"wind": "circle container, inner rounded-triangle blob rotated to wind direction",
|
||||
"uv": "scalloped 12-wave cookie, dots on a lower arc show the UV band",
|
||||
"visibility": "circle container, inner wavy-edged circle fill",
|
||||
"humidity": "rounded rect, wave-topped fill whose height = humidity",
|
||||
"pressure": "circle container, 270° arc gauge",
|
||||
"sun": "rounded rect, filled sine hill above a horizon line, night part darker",
|
||||
"aqi": "rounded rect, gradient bar with knob",
|
||||
"now-chip": "8-point soft cookie behind current-hour temp",
|
||||
"day": "vertical capsule per day (10-day row scrolls horizontally)"
|
||||
},
|
||||
"elevation": {
|
||||
"card": 0,
|
||||
"card-drag": 6,
|
||||
"sheet": 1,
|
||||
"fab": 3
|
||||
},
|
||||
"motion": {
|
||||
"spring-fast": "dampingRatio 0.9, stiffness 1400",
|
||||
"spring-default": "dampingRatio 0.8, stiffness 380",
|
||||
"spring-bouncy": "dampingRatio 0.6, stiffness 300",
|
||||
"scene-crossfade": "600ms",
|
||||
"fast": "120ms",
|
||||
"base": "250ms"
|
||||
},
|
||||
"layout": {
|
||||
"gutter": 16,
|
||||
"card-gap": 12,
|
||||
"grid-compact": 2,
|
||||
"grid-expanded": 4,
|
||||
"two-pane-from": "600dp",
|
||||
"hero-height": "~45% of viewport",
|
||||
"app-bar": 64,
|
||||
"hero": "centered column: condition, temp + inline icon, feels like, high · low",
|
||||
"card-padding": 20,
|
||||
"detail-grid": "2 columns, circles left / rects right, equal squares"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user