import 'package:flutter/material.dart'; /// Colors and shapes shared with the CH-J Server Manager desktop app /// (app/src/renderer/styles.css), so both products look like one family. abstract final class Brand { static const background = Color(0xff07111f); static const panel = Color(0xff102033); static const input = Color(0xff0b1929); static const navigation = Color(0xff050e19); static const terminal = Color(0xff050b12); static const line = Color(0x29b0c4dd); // rgba(151, 176, 205, .16) static const text = Color(0xffecf5ff); static const muted = Color(0xff8ea5bf); static const blue = Color(0xff42a5ff); static const green = Color(0xff43d6a0); static const violet = Color(0xff9d7bff); static const amber = Color(0xffffd082); static const red = Color(0xffff9a9a); static const lightBackground = Color(0xfff4f7fb); static const lightLine = Color(0xffccd8e4); static const lightMuted = Color(0xff53687d); static const cardRadius = 17.0, fieldRadius = 9.0, buttonRadius = 10.0; } ThemeData buildTheme(Brightness brightness) { final dark = brightness == Brightness.dark; final seeded = ColorScheme.fromSeed( seedColor: Brand.blue, brightness: brightness, ); final scheme = dark ? seeded.copyWith( primary: Brand.blue, // White on #42a5ff is 2.6:1; dark navy is 7.25:1 (WCAG AA). onPrimary: const Color(0xff04121f), secondary: Brand.green, tertiary: Brand.violet, surface: Brand.background, onSurface: Brand.text, onSurfaceVariant: Brand.muted, surfaceContainerLowest: Brand.terminal, surfaceContainerLow: Brand.input, surfaceContainer: Brand.panel, surfaceContainerHigh: Brand.panel, surfaceContainerHighest: const Color(0xff16304a), outline: const Color(0x52b0c4dd), outlineVariant: Brand.line, error: Brand.red, ) : seeded.copyWith( // 5.75:1 against white text (WCAG AA). primary: const Color(0xff1565c0), outlineVariant: Brand.lightLine, onSurfaceVariant: Brand.lightMuted, ); final line = dark ? Brand.line : Brand.lightLine; OutlineInputBorder field(Color color, [double width = 1]) => OutlineInputBorder( borderRadius: BorderRadius.circular(Brand.fieldRadius), borderSide: BorderSide(color: color, width: width), ); final buttonShape = RoundedRectangleBorder( borderRadius: BorderRadius.circular(Brand.buttonRadius), ); return ThemeData( useMaterial3: true, brightness: brightness, colorScheme: scheme, scaffoldBackgroundColor: dark ? Brand.background : Brand.lightBackground, dividerTheme: DividerThemeData(color: line, space: 1, thickness: 1), appBarTheme: AppBarTheme( backgroundColor: dark ? Brand.background : Brand.lightBackground, surfaceTintColor: Colors.transparent, scrolledUnderElevation: 0, shape: Border(bottom: BorderSide(color: line)), ), inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: dark ? Brand.input : Colors.white, border: field(line), enabledBorder: field(line), focusedBorder: field(scheme.primary.withValues(alpha: .65), 1.5), errorBorder: field(scheme.error), labelStyle: TextStyle(color: scheme.onSurfaceVariant), ), cardTheme: CardThemeData( margin: const EdgeInsets.symmetric(vertical: 5), elevation: 0, color: dark ? Brand.panel : Colors.white, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(Brand.cardRadius), side: BorderSide(color: line), ), ), filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( shape: buttonShape, minimumSize: const Size(48, 48), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( shape: buttonShape, minimumSize: const Size(48, 48), foregroundColor: dark ? const Color(0xffd9eaff) : null, backgroundColor: dark ? const Color(0x0bffffff) : null, side: BorderSide( color: dark ? const Color(0x387a9cbe) : Brand.lightLine, ), ), ), segmentedButtonTheme: SegmentedButtonThemeData( style: SegmentedButton.styleFrom( shape: buttonShape, side: BorderSide(color: line), selectedBackgroundColor: Brand.blue.withValues(alpha: .18), selectedForegroundColor: dark ? Colors.white : null, ), ), navigationBarTheme: NavigationBarThemeData( backgroundColor: dark ? Brand.navigation : null, indicatorColor: Brand.blue.withValues(alpha: .2), surfaceTintColor: Colors.transparent, ), navigationRailTheme: NavigationRailThemeData( backgroundColor: dark ? Brand.navigation : null, indicatorColor: Brand.blue.withValues(alpha: .2), selectedIconTheme: const IconThemeData(color: Brand.blue), ), bottomSheetTheme: BottomSheetThemeData( backgroundColor: dark ? Brand.panel : null, surfaceTintColor: Colors.transparent, ), dialogTheme: DialogThemeData( backgroundColor: dark ? Brand.panel : null, surfaceTintColor: Colors.transparent, ), popupMenuTheme: PopupMenuThemeData( color: dark ? Brand.panel : null, surfaceTintColor: Colors.transparent, ), progressIndicatorTheme: ProgressIndicatorThemeData( color: Brand.blue, linearTrackColor: line, ), ); } /// The desktop's page header: a small blue eyebrow above the title. class PageTitle extends StatelessWidget { final String eyebrow, title; const PageTitle(this.eyebrow, this.title, {super.key}); @override Widget build(BuildContext context) => Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( eyebrow.toUpperCase(), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: Theme.of(context).colorScheme.primary, fontSize: 10, fontWeight: FontWeight.w800, letterSpacing: 1.8, ), ), Text(title, maxLines: 1, overflow: TextOverflow.ellipsis), ], ); } /// The desktop's brand mark: a blue gradient tile with the product initials. class BrandMark extends StatelessWidget { final double size; const BrandMark({this.size = 32, super.key}); @override Widget build(BuildContext context) => ExcludeSemantics( child: Container( width: size, height: size, alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(size * .31), gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xff3ba7ff), Color(0xff315eff)], ), ), child: Text( 'SM', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w800, fontSize: size * .34, ), ), ), ); }