import 'package:flutter/material.dart'; // ───────────────────────────────────────────────────────────────────────────── // Named palette. Every colour used anywhere in the app is named here — never // write a hex literal at a call site. Brightness-aware surfaces still come from // [JpColors] via `context.jp`; these constants are the fixed brand tones and // the defaults used where no context is available. // ───────────────────────────────────────────────────────────────────────────── var colorWhite = const Color(0xFFFFFFFF); var colorPrimary = const Color(0xFFE8451F); // vermilion — brand primary var colorPrimaryDark = const Color(0xFF111823); // near-black ink var colorPrimaryDark2 = const Color(0xFF0E1521); var colorPrimaryLight = const Color(0xFFF3F4F7); var colorPrimaryLight2 = const Color(0xFFF5F6F9); var colorAccent = const Color(0xFF3B82F6); // blue — web/deploy differentiation var colorPositive = const Color(0xFF17C79A); var colorNegative = const Color(0xFFE5484D); var colorWarmYellow = const Color(0xFFF5A623); var colorViolet = const Color(0xFF8B7CFF); var colorCyan = const Color(0xFF38BDF8); var colorGrey = const Color(0xFF94A3B8); var colorGrey2 = const Color(0xFF586372); var colorGrey3 = const Color(0xFF9AA3B0); var colorBorder = const Color(0xFFE5E8ED); var colorConsole = const Color(0xFF070A10); /// The terminal palette. Console surfaces (log output, unit-file previews, /// command blocks) stay dark in **both** app themes — like a code block, they /// don't invert — so these are fixed tones rather than [JpColors] entries. /// The background itself is still `jp.consoleBg`, which the light theme lifts /// slightly. class ConsoleColors { const ConsoleColors._(); /// Hairline frame and dividers inside a console panel. static const frame = Color(0xFF232B36); /// Slightly lifted frame, for controls sitting on the console. static const frameRaised = Color(0xFF2E3947); /// Primary console body copy — the monospace output itself. static const body = Color(0xFFD3D8E0); /// Header/label copy and enabled control glyphs. static const header = Color(0xFFB9C1CE); /// Secondary counters and captions. static const muted = Color(0xFF6B7482); /// Disabled controls and placeholder copy. static const disabled = Color(0xFF5A6472); // Log severity tones, tuned for legibility on [JpColors.consoleBg]. static const error = Color(0xFFFF8B8B); static const warn = Color(0xFFFFD066); static const success = Color(0xFF3DE0AE); static const step = Color(0xFF8AD0FF); static const info = Color(0xFF9AA4B2); /// The pulsing "LIVE" indicator on a streaming console. static const live = Color(0xFFFF6B6B); } /// Builds the 50→900 swatch Material needs from a single brand colour. MaterialColor createMaterialColor(Color color) { final strengths = [.05]; final swatch = {}; final int r = (color.r * 255).round(), g = (color.g * 255).round(), b = (color.b * 255).round(); for (int i = 1; i < 10; i++) { strengths.add(0.1 * i); } for (final strength in strengths) { final double ds = 0.5 - strength; swatch[(strength * 1000).round()] = Color.fromRGBO( r + ((ds < 0 ? r : (255 - r)) * ds).round(), g + ((ds < 0 ? g : (255 - g)) * ds).round(), b + ((ds < 0 ? b : (255 - b)) * ds).round(), 1, ); } return MaterialColor(color.toARGB32(), swatch); } /// `#RRGGBB` (or `#AARRGGBB`) → [Color]. Color getColorFromHex(String hex) { var value = hex.replaceFirst('#', ''); if (value.length == 6) value = 'FF$value'; return Color(int.parse(value, radix: 16)); } /// [Color] → `#AARRGGBB`. String colorToHex(Color color) => '#${color.toARGB32().toRadixString(16).padLeft(8, '0').toUpperCase()}'; /// Brightness-aware neutral tokens (surfaces, borders, text). Brand accents /// live on [PeachTheme] and read well on both light and dark. @immutable class JpColors extends ThemeExtension { final Color bg; final Color surface; final Color surface2; final Color border; final Color text; final Color textDim; final Color textFaint; final Color consoleBg; const JpColors({ required this.bg, required this.surface, required this.surface2, required this.border, required this.text, required this.textDim, required this.textFaint, required this.consoleBg, }); // Deep, cool-neutral ink. More separation between each step so panels read // crisply and hairline borders stay visible — the Linear/Vercel register. static const dark = JpColors( bg: Color(0xFF0A0E14), surface: Color(0xFF121821), surface2: Color(0xFF1B222D), border: Color(0xFF283040), text: Color(0xFFF3F5F8), textDim: Color(0xFFAEB7C4), textFaint: Color(0xFF6B7585), consoleBg: Color(0xFF070A10), ); // Clean, soft, premium light — off-white canvas, pure-white cards, crisp // hairlines. Avoids the flat grey that reads "template". static const light = JpColors( bg: Color(0xFFF3F4F7), surface: Color(0xFFFFFFFF), surface2: Color(0xFFF5F6F9), border: Color(0xFFE5E8ED), text: Color(0xFF0E1521), textDim: Color(0xFF586372), textFaint: Color(0xFF9AA3B0), consoleBg: Color(0xFF0E1521), ); @override JpColors copyWith({ Color? bg, Color? surface, Color? surface2, Color? border, Color? text, Color? textDim, Color? textFaint, Color? consoleBg, }) => JpColors( bg: bg ?? this.bg, surface: surface ?? this.surface, surface2: surface2 ?? this.surface2, border: border ?? this.border, text: text ?? this.text, textDim: textDim ?? this.textDim, textFaint: textFaint ?? this.textFaint, consoleBg: consoleBg ?? this.consoleBg, ); @override JpColors lerp(ThemeExtension? other, double t) { if (other is! JpColors) return this; return JpColors( bg: Color.lerp(bg, other.bg, t)!, surface: Color.lerp(surface, other.surface, t)!, surface2: Color.lerp(surface2, other.surface2, t)!, border: Color.lerp(border, other.border, t)!, text: Color.lerp(text, other.text, t)!, textDim: Color.lerp(textDim, other.textDim, t)!, textFaint: Color.lerp(textFaint, other.textFaint, t)!, consoleBg: Color.lerp(consoleBg, other.consoleBg, t)!, ); } } extension JpColorsContext on BuildContext { JpColors get jp => Theme.of(this).extension()!; } /// PeachDeployments theme: modern rounded "cockpit", light + dark. class PeachTheme { // "expand" brand palette: vermilion red-orange primary, ink dark, warm gray. static const seed = Color(0xFFE8451F); // vermilion red-orange static const ink = Color(0xFF111823); // near-black static const accent = Color(0xFF3B82F6); // blue (web differentiation) static const danger = Color(0xFFE5484D); static const warn = Color(0xFFF5A623); static const slate = Color(0xFF94A3B8); // neutral chart tone static const violet = Color(0xFF8B7CFF); static const mint = Color(0xFF17C79A); // fresh positive tone static const cyan = Color(0xFF38BDF8); static const pink = Color(0xFFEC4899); // completes the categorical set static const _display = 'Sora'; static const _body = 'Urbanist'; // Warm brand gradient for CTAs, rings and progress. static const brandGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFFF6A3D), Color(0xFFE23C16)], ); static const peachGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFFFE0D0), Color(0xFFFFC1A3)], ); static ThemeData light() => _build(Brightness.light, JpColors.light); static ThemeData dark() => _build(Brightness.dark, JpColors.dark); /// A colored glow (for accent elements). static List glow( Color c, { double blur = 26, double spread = -2, double opacity = 0.45, }) => [ BoxShadow( color: c.withValues(alpha: opacity), blurRadius: blur, spreadRadius: spread, offset: Offset(0, blur * 0.35), ), ]; /// Soft, layered depth shadow — barely-there but present, the premium /// register (not the heavy drop shadow of template UIs). static List softShadow(Brightness b) => b == Brightness.dark ? const [ BoxShadow( color: Color(0x4D000000), blurRadius: 28, spreadRadius: -8, offset: Offset(0, 18), ), BoxShadow( color: Color(0x26000000), blurRadius: 4, offset: Offset(0, 1), ), ] : const [ BoxShadow( color: Color(0x141B2A44), blurRadius: 24, spreadRadius: -6, offset: Offset(0, 12), ), BoxShadow( color: Color(0x0A1B2A44), blurRadius: 2, offset: Offset(0, 1), ), ]; /// A hair-lighter tint of [surface] for the top edge of a card, giving an /// opaque "lit from above" bevel without any translucency. static Color _topSheen(Color surface, Brightness b) => b == Brightness.dark ? Color.alphaBlend(Colors.white.withValues(alpha: 0.05), surface) : surface; /// Elevated SOLID panel decoration used by the custom cards. Opaque, with a /// subtle top-to-bottom sheen and a crisp hairline — no translucency. static BoxDecoration panel( BuildContext context, { Color? glowColor, double radius = 20, }) { final jp = context.jp; final b = Theme.of(context).brightness; return BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [_topSheen(jp.surface, b), jp.surface], ), borderRadius: BorderRadius.circular(radius), border: Border.all(color: jp.border), boxShadow: [ ...softShadow(b), if (glowColor != null) ...glow(glowColor, opacity: 0.16, blur: 30), ], ); } static ThemeData _build(Brightness b, JpColors jp) { final base = ThemeData(brightness: b, useMaterial3: true); final scheme = ColorScheme.fromSeed(seedColor: seed, brightness: b) .copyWith( surface: jp.surface, secondary: accent, primary: seed, onPrimary: Colors.white, error: danger, ); final textTheme = base.textTheme .apply(fontFamily: _body, bodyColor: jp.text, displayColor: jp.text) .copyWith( // Tight, confident display tracking — the hallmark of premium type. displaySmall: base.textTheme.displaySmall?.copyWith( fontFamily: _display, fontWeight: FontWeight.w800, letterSpacing: -0.8, color: jp.text, ), headlineMedium: base.textTheme.headlineMedium?.copyWith( fontFamily: _display, fontWeight: FontWeight.w800, letterSpacing: -0.6, color: jp.text, ), headlineSmall: base.textTheme.headlineSmall?.copyWith( fontFamily: _display, fontWeight: FontWeight.w700, letterSpacing: -0.4, color: jp.text, ), titleLarge: base.textTheme.titleLarge?.copyWith( fontFamily: _display, fontWeight: FontWeight.w700, letterSpacing: -0.2, color: jp.text, ), titleMedium: base.textTheme.titleMedium?.copyWith( fontFamily: _display, fontWeight: FontWeight.w700, color: jp.text, ), bodyLarge: base.textTheme.bodyLarge?.copyWith( height: 1.45, color: jp.text, ), bodyMedium: base.textTheme.bodyMedium?.copyWith( height: 1.45, color: jp.textDim, ), labelLarge: base.textTheme.labelLarge?.copyWith( fontWeight: FontWeight.w700, letterSpacing: 0.2, ), ); return base.copyWith( colorScheme: scheme, extensions: [jp], // Transparent so the glow backdrop shows through app-wide. scaffoldBackgroundColor: Colors.transparent, canvasColor: jp.bg, textTheme: textTheme, splashFactory: InkSparkle.splashFactory, appBarTheme: AppBarTheme( backgroundColor: jp.bg, foregroundColor: jp.text, surfaceTintColor: Colors.transparent, elevation: 0, scrolledUnderElevation: 0, centerTitle: false, titleTextStyle: TextStyle( fontFamily: _display, fontSize: 20, fontWeight: FontWeight.w700, letterSpacing: -0.3, color: jp.text, ), iconTheme: IconThemeData(color: jp.textDim), ), iconTheme: IconThemeData(color: jp.textDim), dividerTheme: DividerThemeData(color: jp.border, thickness: 1), listTileTheme: ListTileThemeData( iconColor: jp.textDim, textColor: jp.text, ), tabBarTheme: TabBarThemeData( labelColor: seed, unselectedLabelColor: jp.textFaint, indicatorColor: seed, indicatorSize: TabBarIndicatorSize.label, dividerColor: jp.border, overlayColor: WidgetStateProperty.all(seed.withValues(alpha: 0.06)), ), popupMenuTheme: PopupMenuThemeData( color: jp.surface, surfaceTintColor: Colors.transparent, elevation: 12, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: jp.border), ), ), dialogTheme: DialogThemeData( backgroundColor: jp.surface, surfaceTintColor: Colors.transparent, elevation: 24, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), side: BorderSide(color: jp.border), ), ), snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, backgroundColor: b == Brightness.dark ? jp.surface2 : const Color(0xFF161D28), contentTextStyle: const TextStyle( color: Colors.white, fontFamily: _body, ), elevation: 12, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide( color: b == Brightness.dark ? jp.border : Colors.transparent, ), ), ), cardTheme: CardThemeData( color: jp.surface, elevation: 0, shadowColor: Colors.transparent, surfaceTintColor: Colors.transparent, margin: EdgeInsets.zero, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), side: BorderSide(color: jp.border), ), ), inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: jp.surface2, isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 14, vertical: 14, ), hintStyle: TextStyle(color: jp.textFaint), labelStyle: TextStyle(color: jp.textDim), floatingLabelStyle: const TextStyle( color: seed, fontWeight: FontWeight.w700, ), prefixIconColor: jp.textFaint, border: OutlineInputBorder( borderRadius: BorderRadius.circular(13), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(13), borderSide: BorderSide(color: jp.border), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(13), borderSide: const BorderSide(color: seed, width: 1.6), ), ), filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( backgroundColor: seed, foregroundColor: Colors.white, disabledBackgroundColor: seed.withValues(alpha: 0.32), disabledForegroundColor: Colors.white70, elevation: 0, shadowColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(13), ), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), textStyle: const TextStyle( fontFamily: _body, fontWeight: FontWeight.w700, fontSize: 14, letterSpacing: 0.2, ), ).copyWith( // Subtle brand shadow only when the button is actually enabled. elevation: WidgetStateProperty.resolveWith( (s) => s.contains(WidgetState.disabled) ? 0 : 6, ), shadowColor: WidgetStateProperty.all( seed.withValues(alpha: 0.45), ), overlayColor: WidgetStateProperty.all( Colors.white.withValues(alpha: 0.10), ), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( foregroundColor: jp.text, side: BorderSide(color: jp.border), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(13), ), padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 15), textStyle: const TextStyle( fontFamily: _body, fontWeight: FontWeight.w700, fontSize: 14, ), ), ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( foregroundColor: seed, textStyle: const TextStyle( fontFamily: _body, fontWeight: FontWeight.w700, ), ), ), floatingActionButtonTheme: FloatingActionButtonThemeData( backgroundColor: seed, foregroundColor: Colors.white, elevation: 8, highlightElevation: 10, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), ), chipTheme: base.chipTheme.copyWith( backgroundColor: jp.surface2, side: BorderSide(color: jp.border), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(9)), labelStyle: TextStyle(color: jp.textDim, fontWeight: FontWeight.w600), ), switchTheme: SwitchThemeData( thumbColor: WidgetStateProperty.resolveWith( (s) => s.contains(WidgetState.selected) ? seed : null, ), trackColor: WidgetStateProperty.resolveWith( (s) => s.contains(WidgetState.selected) ? seed.withValues(alpha: 0.4) : null, ), ), progressIndicatorTheme: ProgressIndicatorThemeData( color: seed, linearTrackColor: jp.surface2, circularTrackColor: Colors.transparent, ), tooltipTheme: TooltipThemeData( decoration: BoxDecoration( color: b == Brightness.dark ? jp.surface2 : const Color(0xFF161D28), borderRadius: BorderRadius.circular(9), border: Border.all( color: b == Brightness.dark ? jp.border : Colors.transparent, ), ), textStyle: const TextStyle( color: Colors.white, fontFamily: _body, fontSize: 12, ), ), ); } }