import 'package:flutter/material.dart'; /// Farbschema der App (Einstellungen → Erscheinungsbild → Farbmodus). enum ThemeVariant { /// „Weiches Dunkel“ – Dunkelgrau statt Fast-Schwarz, mehr Kontrast bei /// Text. Standard seit 2026-10-07 (Bernds Entscheidung). soft, /// Das bisherige Fast-Schwarz (spart auf OLED-Displays etwas Akku). black, /// Hell. light; bool get isDark => this != ThemeVariant.light; String get label => switch (this) { ThemeVariant.soft => 'Dunkel', ThemeVariant.black => 'Schwarz', ThemeVariant.light => 'Hell', }; static ThemeVariant fromName(String? name) => ThemeVariant.values.firstWhere((v) => v.name == name, orElse: () => ThemeVariant.soft); } /// Die Flächen- und Textfarben eines [ThemeVariant]. class PyramidPalette { const PyramidPalette({ required this.bg0, required this.bg1, required this.bg2, required this.bg3, required this.bgHover, required this.bgActive, required this.border, required this.borderStrong, required this.fg, required this.fgMuted, required this.fgDim, }); final Color bg0, bg1, bg2, bg3, bgHover, bgActive, border, borderStrong; final Color fg, fgMuted, fgDim; /// Weiches Dunkel: alle Flächen ~10 % heller als „Schwarz“, Nebentexte /// deutlich kontrastreicher (fgDim auf bg1 ≈ 5:1 statt ≈ 3,8:1). static const soft = PyramidPalette( bg0: Color(0xFF15161A), bg1: Color(0xFF1B1C21), bg2: Color(0xFF212228), bg3: Color(0xFF282A31), bgHover: Color(0xFF2F3139), bgActive: Color(0xFF373945), border: Color(0xFF2C2E36), borderStrong: Color(0xFF3A3C46), fg: Color(0xFFF2F2F5), fgMuted: Color(0xFFB7B7C2), fgDim: Color(0xFF8B8B99), ); static const black = PyramidPalette( bg0: PyramidColors.bg0, bg1: PyramidColors.bg1, bg2: PyramidColors.bg2, bg3: PyramidColors.bg3, bgHover: PyramidColors.bgHover, bgActive: PyramidColors.bgActive, border: PyramidColors.border, borderStrong: PyramidColors.borderStrong, fg: PyramidColors.fg, fgMuted: PyramidColors.fgMuted, fgDim: PyramidColors.fgDim, ); static const light = PyramidPalette( bg0: PyramidColors.bg0Light, bg1: PyramidColors.bg1Light, bg2: PyramidColors.bg2Light, bg3: PyramidColors.bg3Light, bgHover: PyramidColors.bgHoverLight, bgActive: PyramidColors.bgActiveLight, border: PyramidColors.borderLight, borderStrong: PyramidColors.borderStrongLight, fg: PyramidColors.fgLight, fgMuted: PyramidColors.fgMutedLight, fgDim: PyramidColors.fgDimLight, ); static PyramidPalette of(ThemeVariant variant) => switch (variant) { ThemeVariant.soft => soft, ThemeVariant.black => black, ThemeVariant.light => light, }; } // Pyramid design tokens — matching mockup CSS variables exactly. // Die dunklen Werte hier sind die Variante „Schwarz“; Widgets sollten die // Farben über PyramidTheme.of(context) holen (folgt dem gewählten Schema). class PyramidColors { const PyramidColors._(); // Dark theme static const bg0 = Color(0xFF0A0A0C); static const bg1 = Color(0xFF111114); static const bg2 = Color(0xFF17171C); static const bg3 = Color(0xFF1E1E25); static const bgHover = Color(0xFF232330); static const bgActive = Color(0xFF2A2A38); static const border = Color(0xFF25252E); static const borderStrong = Color(0xFF32323D); static const fg = Color(0xFFECECF0); static const fgMuted = Color(0xFFA4A4B0); static const fgDim = Color(0xFF6F6F7D); // Light theme static const bg0Light = Color(0xFFF5F4F0); static const bg1Light = Color(0xFFFFFFFF); static const bg2Light = Color(0xFFFAF9F5); static const bg3Light = Color(0xFFF0EEEA); static const bgHoverLight = Color(0xFFE9E7E1); static const bgActiveLight = Color(0xFFDFDCD3); static const borderLight = Color(0xFFE5E2DC); static const borderStrongLight = Color(0xFFD3CFC6); static const fgLight = Color(0xFF1A1A1F); static const fgMutedLight = Color(0xFF54545E); static const fgDimLight = Color(0xFF8A8A92); // Semantic colors (same in both themes) static const danger = Color(0xFFE5484D); static const success = Color(0xFF30A46C); static const online = Color(0xFF4ADE80); static const away = Color(0xFFFACC15); static const busy = Color(0xFFF87171); // Accent presets — default: amber hsl(42 95% 58%) static const accentAmber = Color(0xFFF5A614); static const accentCoral = Color(0xFFED6951); static const accentMint = Color(0xFF3DB88A); static const accentViolet = Color(0xFFB67EEA); static const accentCobalt = Color(0xFF5BA3F5); static const accentLime = Color(0xFF8EC63F); static const accentFg = Color(0xFF0B0B0D); } class PyramidTheme extends InheritedWidget { final ThemeVariant variant; final Color accent; final double radius; final double density; final double motion; const PyramidTheme({ super.key, required super.child, this.variant = ThemeVariant.soft, this.accent = PyramidColors.accentAmber, this.radius = 12, this.density = 1, this.motion = 1, }); static PyramidTheme of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType()!; } bool get isDark => variant.isDark; PyramidPalette get palette => PyramidPalette.of(variant); Color get bg0 => palette.bg0; Color get bg1 => palette.bg1; Color get bg2 => palette.bg2; Color get bg3 => palette.bg3; Color get bgHover => palette.bgHover; Color get bgActive => palette.bgActive; Color get border => palette.border; Color get borderStrong => palette.borderStrong; Color get fg => palette.fg; Color get fgMuted => palette.fgMuted; Color get fgDim => palette.fgDim; Color get danger => PyramidColors.danger; Color get online => PyramidColors.online; Color get away => PyramidColors.away; Color get busy => PyramidColors.busy; Color get accentSoft => accent.withAlpha(36); // ~14% Color get accentGlow => accent.withAlpha(89); // ~35% Color get accentFg => PyramidColors.accentFg; double get rSm => radius * 0.6; double get rBase => radius; double get rLg => radius * 1.5; double get rXl => radius * 2; double get rowPadY => 10 * density; double get rowPadX => 12 * density; double get msgGap => 16 * density; Duration get durationFast => const Duration(milliseconds: 150); Duration get durationNormal => const Duration(milliseconds: 220); Curve get springBounce => Curves.easeOutBack; Curve get springSoft => Curves.easeOutCubic; @override bool updateShouldNotify(PyramidTheme oldWidget) => variant != oldWidget.variant || accent != oldWidget.accent || radius != oldWidget.radius || density != oldWidget.density || motion != oldWidget.motion; } // Inter ist als Asset gebündelt (pubspec `fonts:`) — kein Laufzeit-Download. TextStyle _base(Color color) => TextStyle(fontFamily: 'Inter', color: color); ThemeData buildPyramidThemeData(ThemeVariant variant) { final palette = PyramidPalette.of(variant); final isDark = variant.isDark; final fg = palette.fg; final bg1 = palette.bg1; final accent = PyramidColors.accentAmber; return ThemeData( useMaterial3: true, fontFamily: 'Inter', brightness: isDark ? Brightness.dark : Brightness.light, scaffoldBackgroundColor: bg1, colorScheme: ColorScheme( brightness: isDark ? Brightness.dark : Brightness.light, primary: accent, onPrimary: PyramidColors.accentFg, secondary: accent, onSecondary: PyramidColors.accentFg, error: PyramidColors.danger, onError: Colors.white, surface: bg1, onSurface: fg, ), textTheme: TextTheme( bodyMedium: _base(fg).copyWith(fontSize: 14), bodySmall: _base(fg).copyWith(fontSize: 12, color: palette.fgMuted), labelSmall: _base(fg).copyWith(fontSize: 11, color: palette.fgDim), ), dividerTheme: DividerThemeData( color: palette.border, thickness: 1, space: 1, ), splashFactory: NoSplash.splashFactory, highlightColor: Colors.transparent, ); }