// pangolin_theme.dart // 穿山甲 VPN · Pangolin VPN — Flutter design tokens // // Single Dart source mirroring colors_and_type.css. // Fonts are loaded at runtime via the google_fonts package (no local ttf needed). // // Usage: // MaterialApp( // theme: PangolinTheme.light, // darkTheme: PangolinTheme.dark, // themeMode: ThemeMode.system, // ); // // tokens: PangolinColors.clay500, PangolinSpacing.x4, PangolinRadius.lg ... import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; /// ── Primitive color ramps ─────────────────────────────────────────── class PangolinColors { PangolinColors._(); // Clay / Copper — pangolin-armor primary (warm earth) static const clay50 = Color(0xFFFAF3ED); static const clay100 = Color(0xFFF2E2D4); static const clay200 = Color(0xFFE6C7AC); static const clay300 = Color(0xFFD9A982); static const clay400 = Color(0xFFCC8B5C); static const clay500 = Color(0xFFB96A3D); // brand primary static const clay600 = Color(0xFF9E5630); static const clay700 = Color(0xFF7E4426); static const clay800 = Color(0xFF5E331D); static const clay900 = Color(0xFF3D2213); // Sand / Taupe — warm neutral ramp static const sand50 = Color(0xFFFAF8F4); static const sand100 = Color(0xFFF2EEE7); static const sand200 = Color(0xFFE6DFD3); static const sand300 = Color(0xFFD2C8B8); static const sand400 = Color(0xFFB0A491); static const sand500 = Color(0xFF8C8270); static const sand600 = Color(0xFF6B6253); static const sand700 = Color(0xFF4E4940); static const sand800 = Color(0xFF2E2A24); static const sand900 = Color(0xFF1F1C18); static const sand950 = Color(0xFF14110E); // warm espresso near-black // Semantic hues (earth-tuned) static const green400 = Color(0xFF7FB07A); static const green500 = Color(0xFF5B8C5A); // connected / secure static const green600 = Color(0xFF467046); static const amber400 = Color(0xFFE2B05A); static const amber500 = Color(0xFFD69A3C); // connecting / warning static const amber600 = Color(0xFFB47E29); static const red400 = Color(0xFFD4715A); static const red500 = Color(0xFFC0533B); // error / disconnect static const red600 = Color(0xFF9E4230); static const white = Color(0xFFFFFFFF); } /// ── Semantic tokens, resolved per brightness ─────────────────────── @immutable class PangolinScheme extends ThemeExtension { const PangolinScheme({ required this.bg, required this.bgSubtle, required this.surface, required this.surface2, required this.overlay, required this.fg1, required this.fg2, required this.fg3, required this.fgOnAccent, required this.accent, required this.accentHover, required this.accentPress, required this.accentSubtle, required this.accentBorder, required this.border, required this.borderStrong, required this.ring, required this.success, required this.successSubtle, required this.warning, required this.warningSubtle, required this.danger, required this.dangerSubtle, }); final Color bg, bgSubtle, surface, surface2, overlay; final Color fg1, fg2, fg3, fgOnAccent; final Color accent, accentHover, accentPress, accentSubtle, accentBorder; final Color border, borderStrong, ring; final Color success, successSubtle, warning, warningSubtle, danger, dangerSubtle; static const light = PangolinScheme( bg: PangolinColors.sand50, bgSubtle: PangolinColors.sand100, surface: PangolinColors.white, surface2: PangolinColors.sand50, overlay: Color(0x731F1C18), fg1: PangolinColors.sand900, fg2: PangolinColors.sand600, fg3: PangolinColors.sand500, fgOnAccent: PangolinColors.white, accent: PangolinColors.clay500, accentHover: PangolinColors.clay600, accentPress: PangolinColors.clay700, accentSubtle: PangolinColors.clay50, accentBorder: PangolinColors.clay200, border: PangolinColors.sand200, borderStrong: PangolinColors.sand300, ring: Color(0x59B96A3D), success: PangolinColors.green500, successSubtle: Color(0xFFE9F0E6), warning: PangolinColors.amber500, warningSubtle: Color(0xFFF8EED6), danger: PangolinColors.red500, dangerSubtle: Color(0xFFF6E1DA), ); static const dark = PangolinScheme( bg: PangolinColors.sand950, bgSubtle: PangolinColors.sand900, surface: Color(0xFF221E19), surface2: Color(0xFF2A251F), overlay: Color(0x99000000), fg1: Color(0xFFF4EFE8), fg2: Color(0xFFB6AC9C), fg3: Color(0xFF897F6F), fgOnAccent: PangolinColors.sand900, accent: PangolinColors.clay400, accentHover: PangolinColors.clay300, accentPress: PangolinColors.clay500, accentSubtle: Color(0x24CC8B5C), accentBorder: Color(0x4DCC8B5C), border: Color(0x1AF2EEE7), borderStrong: Color(0x2EF2EEE7), ring: Color(0x73CC8B5C), success: PangolinColors.green400, successSubtle: Color(0x297FB07A), warning: PangolinColors.amber400, warningSubtle: Color(0x29E2B05A), danger: PangolinColors.red400, dangerSubtle: Color(0x29D4715A), ); @override PangolinScheme copyWith({Color? bg, Color? fg1, Color? accent}) => PangolinScheme( bg: bg ?? this.bg, bgSubtle: bgSubtle, surface: surface, surface2: surface2, overlay: overlay, fg1: fg1 ?? this.fg1, fg2: fg2, fg3: fg3, fgOnAccent: fgOnAccent, accent: accent ?? this.accent, accentHover: accentHover, accentPress: accentPress, accentSubtle: accentSubtle, accentBorder: accentBorder, border: border, borderStrong: borderStrong, ring: ring, success: success, successSubtle: successSubtle, warning: warning, warningSubtle: warningSubtle, danger: danger, dangerSubtle: dangerSubtle, ); @override PangolinScheme lerp(ThemeExtension? other, double t) { if (other is! PangolinScheme) return this; Color c(Color a, Color b) => Color.lerp(a, b, t)!; return PangolinScheme( bg: c(bg, other.bg), bgSubtle: c(bgSubtle, other.bgSubtle), surface: c(surface, other.surface), surface2: c(surface2, other.surface2), overlay: c(overlay, other.overlay), fg1: c(fg1, other.fg1), fg2: c(fg2, other.fg2), fg3: c(fg3, other.fg3), fgOnAccent: c(fgOnAccent, other.fgOnAccent), accent: c(accent, other.accent), accentHover: c(accentHover, other.accentHover), accentPress: c(accentPress, other.accentPress), accentSubtle: c(accentSubtle, other.accentSubtle), accentBorder: c(accentBorder, other.accentBorder), border: c(border, other.border), borderStrong: c(borderStrong, other.borderStrong), ring: c(ring, other.ring), success: c(success, other.success), successSubtle: c(successSubtle, other.successSubtle), warning: c(warning, other.warning), warningSubtle: c(warningSubtle, other.warningSubtle), danger: c(danger, other.danger), dangerSubtle: c(dangerSubtle, other.dangerSubtle), ); } } extension PangolinContext on BuildContext { PangolinScheme get pangolin => Theme.of(this).extension()!; } /// ── Spacing (4px base) ────────────────────────────────────────────── class PangolinSpacing { PangolinSpacing._(); static const double x1 = 4, x2 = 8, x3 = 12, x4 = 16, x5 = 20, x6 = 24, x8 = 32, x10 = 40, x12 = 48, x16 = 64; } /// ── Radii ─────────────────────────────────────────────────────────── class PangolinRadius { PangolinRadius._(); static const double sm = 6, md = 10, lg = 14, xl = 20, xxl = 28, full = 999; static const rSm = Radius.circular(sm); static const rMd = Radius.circular(md); static const rLg = Radius.circular(lg); static const rXl = Radius.circular(xl); static const rXxl = Radius.circular(xxl); } /// ── Motion ────────────────────────────────────────────────────────── class PangolinMotion { PangolinMotion._(); static const fast = Duration(milliseconds: 140); static const base = Duration(milliseconds: 220); static const slow = Duration(milliseconds: 360); static const easeOut = Cubic(0.22, 1, 0.36, 1); static const easeInOut = Cubic(0.65, 0, 0.35, 1); } /// ── Font families (via google_fonts at runtime) ───────────────────── class PangolinFonts { PangolinFonts._(); // These string constants are used as display labels only. // Actual font loading is done via google_fonts in PangolinText getters. static const display = 'Sora'; static const sans = 'Manrope'; static const cjk = 'Noto Sans SC'; static const mono = 'JetBrains Mono'; } /// ── Type scale ────────────────────────────────────────────────────── /// Uses google_fonts for runtime font loading (no local ttf required). /// All PangolinText members are static getters (not const) so they can /// reference google_fonts, which registers fonts dynamically. class PangolinText { PangolinText._(); static TextStyle get displayXl => GoogleFonts.sora( fontSize: 48, fontWeight: FontWeight.w700, height: 1.15, letterSpacing: -0.96); static TextStyle get display => GoogleFonts.sora( fontSize: 36, fontWeight: FontWeight.w700, height: 1.15, letterSpacing: -0.72); static TextStyle get h1 => GoogleFonts.sora( fontSize: 30, fontWeight: FontWeight.w600, height: 1.3, letterSpacing: -0.3); static TextStyle get h2 => GoogleFonts.sora( fontSize: 24, fontWeight: FontWeight.w600, height: 1.3, letterSpacing: -0.24); static TextStyle get h3 => GoogleFonts.manrope( fontSize: 20, fontWeight: FontWeight.w600, height: 1.3); static TextStyle get bodyLg => GoogleFonts.manrope( fontSize: 18, fontWeight: FontWeight.w400, height: 1.65); static TextStyle get body => GoogleFonts.manrope( fontSize: 16, fontWeight: FontWeight.w400, height: 1.5); static TextStyle get sm => GoogleFonts.manrope( fontSize: 14, fontWeight: FontWeight.w400, height: 1.5); static TextStyle get caption => GoogleFonts.manrope( fontSize: 12, fontWeight: FontWeight.w500, height: 1.5); static TextStyle get overline => GoogleFonts.manrope( fontSize: 12, fontWeight: FontWeight.w600, letterSpacing: 0.96); static TextStyle get mono => GoogleFonts.jetBrainsMono( fontSize: 14, fontWeight: FontWeight.w400, fontFeatures: const [FontFeature.tabularFigures()]); } /// ── Soft warm-tinted shadows ──────────────────────────────────────── class PangolinShadow { PangolinShadow._(); static const _tint = Color(0xFF2D1E14); static List sm = [BoxShadow(color: _tint.withOpacity(0.06), blurRadius: 2, offset: const Offset(0, 1))]; static List md = [BoxShadow(color: _tint.withOpacity(0.08), blurRadius: 14, offset: const Offset(0, 4))]; static List lg = [BoxShadow(color: _tint.withOpacity(0.12), blurRadius: 32, offset: const Offset(0, 12))]; static List xl = [BoxShadow(color: _tint.withOpacity(0.16), blurRadius: 60, offset: const Offset(0, 24))]; } /// ── Ready-to-use ThemeData ────────────────────────────────────────── class PangolinTheme { PangolinTheme._(); static ThemeData get light => _build(PangolinScheme.light, Brightness.light); static ThemeData get dark => _build(PangolinScheme.dark, Brightness.dark); static ThemeData _build(PangolinScheme s, Brightness b) { final scheme = ColorScheme( brightness: b, primary: s.accent, onPrimary: s.fgOnAccent, secondary: s.accent, onSecondary: s.fgOnAccent, error: s.danger, onError: PangolinColors.white, surface: s.surface, onSurface: s.fg1, ); return ThemeData( useMaterial3: true, brightness: b, colorScheme: scheme, scaffoldBackgroundColor: s.bg, // Use google_fonts Manrope as app-wide base font textTheme: GoogleFonts.manropeTextTheme( ThemeData(brightness: b).textTheme, ), extensions: [s], filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( backgroundColor: s.accent, foregroundColor: s.fgOnAccent, textStyle: GoogleFonts.manrope(fontWeight: FontWeight.w600, fontSize: 15), shape: const StadiumBorder(), padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 14), ), ), cardTheme: CardThemeData( color: s.surface, elevation: 0, shape: RoundedRectangleBorder( side: BorderSide(color: s.border), borderRadius: BorderRadius.circular(PangolinRadius.lg), ), ), inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: s.bg, border: OutlineInputBorder( borderRadius: BorderRadius.circular(PangolinRadius.md), borderSide: BorderSide(color: s.borderStrong, width: 1.5), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(PangolinRadius.md), borderSide: BorderSide(color: s.accent, width: 1.5), ), ), ); } }