UI-Politur: Kontrast, Abstaende, Typografie, Bewegung

Kein Redesign — Schwarz/Rot, 4 Tabs und Lieder/Kategorie bleiben. Grundlage
sind die Design-Skills: ui-ux-pro-max (Barrierefreiheit, Touch, Typografie,
Bewegung), ui-design/mobile-android (Material 3) und die Token-Disziplin
aus Hue.

Behobene Maengel (messbar, nicht Geschmack):
- Text in Colors.white38 an 17 Stellen = 3,4:1 Kontrast, WCAG verlangt
  4,5:1. Ersetzt durch drei benannte Stufen (text1 15,9:1 / text2 8,8:1 /
  text3 4,9:1). theme_kontrast_test.dart rechnet die Verhaeltnisse bei
  jedem Lauf nach, statt sie zu behaupten.
- SubTabs waren ~38 dp hoch (Material 3 verlangt 48), ohne Ripple und ohne
  Uebergang. Jetzt 48 dp, InkWell, AnimatedContainer, und die Auswahl wird
  Vorlesehilfen als Zustand gemeldet (Semantics.selected).
- mini_player.dart: Expanded UM eine feste Hoehe herum — zwei
  widerspruechliche Angaben. Entschaerft.
- Emoji in Bedienelementen der Einstellungen (jeweils neben einem echten
  Icon) entfernt.

Neu in shared/theme.dart: MeloSpace (8er-Raster), MeloRadius, MeloMotion,
Farbrollen text1/2/3 + border + hairline, minTouchTarget, vollstaendiges
textTheme (7 Stufen) und Themes fuer Listen, Sheets, Snackbars,
Fortschritt, Trenner.

318 Tests gruen (23 neue, 1 uebersprungen), flutter analyze ohne Befund.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
This commit is contained in:
Hermes (Server)
2026-08-21 12:40:25 +02:00
co-authored by Claude Opus 5
parent 2df85e650e
commit bc861f6087
23 changed files with 773 additions and 184 deletions
+69 -20
View File
@@ -1,7 +1,12 @@
import 'package:flutter/material.dart';
import 'theme.dart';
/// Unterreiter im Stil der Referenz-UI: der aktive Reiter ist eine helle
/// Pille, die übrigen sind nur Text.
///
/// Die Pille wechselt weich statt zu springen — so ist sichtbar, dass es
/// derselbe Reiter ist, der sich bewegt, und nicht ein neuer Bildschirm.
class SubTabs extends StatelessWidget {
const SubTabs({
super.key,
@@ -18,31 +23,17 @@ class SubTabs extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 4),
padding: const EdgeInsets.fromLTRB(
MeloSpace.md, MeloSpace.sm, MeloSpace.md, MeloSpace.xs),
child: Row(
children: [
for (var i = 0; i < labels.length; i++)
Padding(
padding: const EdgeInsets.only(right: 8),
child: GestureDetector(
padding: const EdgeInsets.only(right: MeloSpace.sm),
child: _Reiter(
label: labels[i],
aktiv: i == index,
onTap: () => onChanged(i),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 18, vertical: 9),
decoration: BoxDecoration(
color: i == index ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(20),
),
child: Text(
labels[i],
style: TextStyle(
color: i == index ? Colors.black : Colors.white70,
fontSize: 15,
fontWeight:
i == index ? FontWeight.w600 : FontWeight.w400,
),
),
),
),
),
],
@@ -50,3 +41,61 @@ class SubTabs extends StatelessWidget {
);
}
}
class _Reiter extends StatelessWidget {
const _Reiter({
required this.label,
required this.aktiv,
required this.onTap,
});
final String label;
final bool aktiv;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Semantics(
button: true,
selected: aktiv,
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(MeloRadius.pill),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: AnimatedContainer(
duration: MeloMotion.fast,
curve: MeloMotion.curve,
// Volle 48 dp hoch: darunter trifft man den Reiter im Gehen nicht
// zuverlässig (Material 3, Mindestgröße für Bedienelemente).
constraints: const BoxConstraints(
minHeight: MeloTheme.minTouchTarget,
minWidth: MeloTheme.minTouchTarget,
),
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: MeloSpace.md),
decoration: BoxDecoration(
color: aktiv ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(MeloRadius.pill),
// Der inaktive Reiter bleibt als Fläche erkennbar, ohne sich
// vorzudrängen — sonst schweben die Wörter im Nichts.
border: aktiv ? null : MeloTheme.hairline,
),
child: AnimatedDefaultTextStyle(
duration: MeloMotion.fast,
curve: MeloMotion.curve,
style: TextStyle(
color: aktiv ? MeloTheme.black : MeloTheme.text2,
fontSize: 15,
height: 1.2,
fontWeight: aktiv ? FontWeight.w600 : FontWeight.w500,
),
child: Text(label),
),
),
),
),
);
}
}