import 'package:flutter/material.dart'; /// Warp's design tokens (apps/ui/src/styles/tokens.css) as a Flutter theme. /// /// The token names are kept so a colour can be traced back to the brand /// package: [WarpColors.light] and [WarpColors.dark] are `:root` and /// `[data-theme="dark"]`. @immutable class WarpColors extends ThemeExtension { const WarpColors({ required this.surface, required this.surfaceSunken, required this.surfaceRaised, required this.line, required this.lineStrong, required this.ink, required this.inkMuted, required this.teal, required this.tealStrong, required this.onTeal, required this.tealText, required this.tealSoft, required this.bubbleOther, required this.danger, required this.dangerSoft, required this.warning, required this.warningSoft, required this.info, required this.infoSoft, required this.presenceOnline, required this.presenceAway, required this.presenceOffline, }); final Color surface; final Color surfaceSunken; final Color surfaceRaised; final Color line; final Color lineStrong; final Color ink; final Color inkMuted; final Color teal; final Color tealStrong; final Color onTeal; final Color tealText; final Color tealSoft; final Color bubbleOther; final Color danger; final Color dangerSoft; final Color warning; final Color warningSoft; final Color info; final Color infoSoft; final Color presenceOnline; final Color presenceAway; final Color presenceOffline; static const light = WarpColors( surface: Color(0xFFFFFFFF), surfaceSunken: Color(0xFFF3F5F4), surfaceRaised: Color(0xFFFFFFFF), line: Color(0xFFE1E6E4), lineStrong: Color(0xFF7B8682), ink: Color(0xFF111114), inkMuted: Color(0xFF5B6461), teal: Color(0xFF00A389), tealStrong: Color(0xFF00806A), onTeal: Color(0xFFFFFFFF), tealText: Color(0xFF00735F), tealSoft: Color(0xFFDDF4EE), bubbleOther: Color(0xFFEFF2F1), danger: Color(0xFFB42318), dangerSoft: Color(0xFFFDE8E6), warning: Color(0xFF8A5300), warningSoft: Color(0xFFFFF2DB), info: Color(0xFF1D5FD1), infoSoft: Color(0xFFE3EDFF), presenceOnline: Color(0xFF00A389), presenceAway: Color(0xFFD99A00), presenceOffline: Color(0xFF8A9490), ); static const dark = WarpColors( surface: Color(0xFF0F1213), surfaceSunken: Color(0xFF161A1C), surfaceRaised: Color(0xFF1B2022), line: Color(0xFF2A3134), lineStrong: Color(0xFF6E7976), ink: Color(0xFFECEEEE), inkMuted: Color(0xFF9AA5A1), teal: Color(0xFF22C3A6), tealStrong: Color(0xFF22C3A6), onTeal: Color(0xFF062A23), tealText: Color(0xFF3ED6B8), tealSoft: Color(0xFF123A33), bubbleOther: Color(0xFF1E2427), danger: Color(0xFFF97066), dangerSoft: Color(0xFF3A1512), warning: Color(0xFFF5B547), warningSoft: Color(0xFF33260B), info: Color(0xFF7EB1FF), infoSoft: Color(0xFF14233F), presenceOnline: Color(0xFF22C3A6), presenceAway: Color(0xFFF5B547), presenceOffline: Color(0xFF6E7976), ); @override WarpColors copyWith() => this; @override WarpColors lerp(WarpColors? other, double t) => other == null || t < 0.5 ? this : other; } extension WarpTheme on BuildContext { WarpColors get warp => Theme.of(this).extension()!; } const fontDisplay = 'Outfit'; const fontSans = 'Figtree'; const fontMono = 'JetBrains Mono'; ThemeData warpTheme(Brightness brightness) { final c = brightness == Brightness.dark ? WarpColors.dark : WarpColors.light; final scheme = ColorScheme( brightness: brightness, primary: c.tealStrong, onPrimary: c.onTeal, primaryContainer: c.tealSoft, onPrimaryContainer: c.tealText, secondary: c.teal, onSecondary: c.onTeal, error: c.danger, onError: brightness == Brightness.dark ? c.surface : Colors.white, errorContainer: c.dangerSoft, onErrorContainer: c.danger, surface: c.surface, onSurface: c.ink, onSurfaceVariant: c.inkMuted, surfaceContainerLowest: c.surface, surfaceContainerLow: c.surfaceSunken, surfaceContainer: c.surfaceSunken, surfaceContainerHigh: c.surfaceRaised, surfaceContainerHighest: c.bubbleOther, outline: c.lineStrong, outlineVariant: c.line, ); TextStyle display(double size, double height, FontWeight w) => TextStyle( fontFamily: fontDisplay, fontSize: size, height: height / size, fontWeight: w, color: c.ink, ); TextStyle sans(double size, double height, FontWeight w, [Color? color]) => TextStyle( fontFamily: fontSans, fontSize: size, height: height / size, fontWeight: w, color: color ?? c.ink, ); final text = TextTheme( displaySmall: display(40, 44, FontWeight.w700), headlineMedium: display(28, 34, FontWeight.w600), headlineSmall: display(24, 30, FontWeight.w600), titleLarge: display(20, 26, FontWeight.w600), titleMedium: display(16, 22, FontWeight.w600), titleSmall: sans(14, 20, FontWeight.w600), bodyLarge: sans(15, 22, FontWeight.w400), bodyMedium: sans(14, 20, FontWeight.w400), bodySmall: sans(13, 18, FontWeight.w400, c.inkMuted), labelLarge: sans(14, 20, FontWeight.w600), labelMedium: sans(12, 16, FontWeight.w500), labelSmall: sans(11, 14, FontWeight.w500, c.inkMuted), ); final radius = BorderRadius.circular(12); return ThemeData( useMaterial3: true, brightness: brightness, colorScheme: scheme, fontFamily: fontSans, textTheme: text, scaffoldBackgroundColor: c.surface, extensions: [c], dividerTheme: DividerThemeData(color: c.line, space: 1, thickness: 1), appBarTheme: AppBarTheme( backgroundColor: c.surface, foregroundColor: c.ink, surfaceTintColor: Colors.transparent, elevation: 0, scrolledUnderElevation: 0, titleTextStyle: display(18, 24, FontWeight.w600), shape: Border(bottom: BorderSide(color: c.line)), ), inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: c.surfaceSunken, contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), border: OutlineInputBorder( borderRadius: radius, borderSide: BorderSide(color: c.line), ), enabledBorder: OutlineInputBorder( borderRadius: radius, borderSide: BorderSide(color: c.line), ), focusedBorder: OutlineInputBorder( borderRadius: radius, borderSide: BorderSide(color: c.tealStrong, width: 2), ), errorBorder: OutlineInputBorder( borderRadius: radius, borderSide: BorderSide(color: c.danger), ), labelStyle: sans(14, 20, FontWeight.w500, c.inkMuted), hintStyle: sans(14, 20, FontWeight.w400, c.inkMuted), ), filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( backgroundColor: c.tealStrong, foregroundColor: c.onTeal, minimumSize: const Size(64, 44), textStyle: sans(14, 20, FontWeight.w600), shape: RoundedRectangleBorder(borderRadius: radius), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( foregroundColor: c.ink, minimumSize: const Size(64, 44), side: BorderSide(color: c.line), textStyle: sans(14, 20, FontWeight.w600), shape: RoundedRectangleBorder(borderRadius: radius), ), ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( foregroundColor: c.tealText, textStyle: sans(14, 20, FontWeight.w600), ), ), floatingActionButtonTheme: FloatingActionButtonThemeData( backgroundColor: c.tealStrong, foregroundColor: c.onTeal, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), ), dialogTheme: DialogThemeData( backgroundColor: c.surfaceRaised, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), titleTextStyle: display(20, 26, FontWeight.w600), ), bottomSheetTheme: BottomSheetThemeData( backgroundColor: c.surfaceRaised, surfaceTintColor: Colors.transparent, showDragHandle: true, ), snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, backgroundColor: c.ink, contentTextStyle: sans(14, 20, FontWeight.w500, c.surface), shape: RoundedRectangleBorder(borderRadius: radius), ), listTileTheme: ListTileThemeData( iconColor: c.inkMuted, titleTextStyle: sans(15, 22, FontWeight.w500), subtitleTextStyle: sans(13, 18, FontWeight.w400, c.inkMuted), ), switchTheme: SwitchThemeData( thumbColor: WidgetStateProperty.resolveWith( (s) => s.contains(WidgetState.selected) ? c.onTeal : c.lineStrong, ), trackColor: WidgetStateProperty.resolveWith( (s) => s.contains(WidgetState.selected) ? c.tealStrong : c.line, ), ), progressIndicatorTheme: ProgressIndicatorThemeData(color: c.teal), ); }