// pangolin_theme.dart // 穿山甲 VPN · Pangolin VPN — Flutter design tokens // // Single Dart source mirroring colors_and_type.css. Generated by hand to be // Flutter-friendly: const color ramps, spacing/radii/durations, text styles, // and ready-to-use light + dark ThemeData. // // Fonts: add Sora, Manrope, Noto Sans SC, JetBrains Mono to pubspec.yaml // (google_fonts package or bundled assets). Family names below assume those. // // Usage: // MaterialApp( // theme: PangolinTheme.light, // darkTheme: PangolinTheme.dark, // themeMode: ThemeMode.system, // ); // // tokens: PangolinColors.clay500, PangolinSpacing.x4, PangolinRadius.lg ... import 'package:flutter/material.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 ─────────────────────── /// Read these off the active theme: `context.pangolin` (see extension below), /// or build directly with PangolinScheme.light / .dark. @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), // rgba(31,28,24,0.45) 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), // rgba(185,106,61,0.35) 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), // rgba(0,0,0,0.6) fg1: Color(0xFFF4EFE8), fg2: Color(0xFFB6AC9C), fg3: Color(0xFF897F6F), fgOnAccent: PangolinColors.sand900, accent: PangolinColors.clay400, accentHover: PangolinColors.clay300, accentPress: PangolinColors.clay500, accentSubtle: Color(0x24CC8B5C), // rgba(204,139,92,0.14) accentBorder: Color(0x4DCC8B5C), // rgba(204,139,92,0.30) border: Color(0x1AF2EEE7), // rgba(242,238,231,0.10) borderStrong: Color(0x2EF2EEE7), // rgba(242,238,231,0.18) ring: Color(0x73CC8B5C), // rgba(204,139,92,0.45) 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); // easeOut cubic-bezier(0.22, 1, 0.36, 1) static const easeOut = Cubic(0.22, 1, 0.36, 1); static const easeInOut = Cubic(0.65, 0, 0.35, 1); } /// ── Font families ─────────────────────────────────────────────────── class PangolinFonts { PangolinFonts._(); static const display = 'Sora'; static const sans = 'Manrope'; static const cjk = 'Noto Sans SC'; static const mono = 'JetBrains Mono'; } /// ── Type scale (sizes mirror the CSS rem scale at 16px root) ──────── class PangolinText { PangolinText._(); // Build with a color at call site: PangolinText.h1.copyWith(color: ctx.pangolin.fg1) static const displayXl = TextStyle(fontFamily: PangolinFonts.display, fontSize: 48, fontWeight: FontWeight.w700, height: 1.15, letterSpacing: -0.96); static const display = TextStyle(fontFamily: PangolinFonts.display, fontSize: 36, fontWeight: FontWeight.w700, height: 1.15, letterSpacing: -0.72); static const h1 = TextStyle(fontFamily: PangolinFonts.display, fontSize: 30, fontWeight: FontWeight.w600, height: 1.3, letterSpacing: -0.3); static const h2 = TextStyle(fontFamily: PangolinFonts.display, fontSize: 24, fontWeight: FontWeight.w600, height: 1.3, letterSpacing: -0.24); static const h3 = TextStyle(fontFamily: PangolinFonts.sans, fontSize: 20, fontWeight: FontWeight.w600, height: 1.3); static const bodyLg = TextStyle(fontFamily: PangolinFonts.sans, fontSize: 18, fontWeight: FontWeight.w400, height: 1.65); static const body = TextStyle(fontFamily: PangolinFonts.sans, fontSize: 16, fontWeight: FontWeight.w400, height: 1.5); static const sm = TextStyle(fontFamily: PangolinFonts.sans, fontSize: 14, fontWeight: FontWeight.w400, height: 1.5); static const caption = TextStyle(fontFamily: PangolinFonts.sans, fontSize: 12, fontWeight: FontWeight.w500, height: 1.5); static const overline = TextStyle(fontFamily: PangolinFonts.sans, fontSize: 12, fontWeight: FontWeight.w600, letterSpacing: 0.96); static const mono = TextStyle(fontFamily: PangolinFonts.mono, fontSize: 14, fontWeight: FontWeight.w400, fontFeatures: [FontFeature.tabularFigures()]); } /// ── Soft warm-tinted shadows ──────────────────────────────────────── class PangolinShadow { PangolinShadow._(); static const _tint = Color(0xFF2D1E14); // warm 45,30,20 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, fontFamily: PangolinFonts.sans, extensions: [s], filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( backgroundColor: s.accent, foregroundColor: s.fgOnAccent, textStyle: const TextStyle(fontFamily: PangolinFonts.sans, 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), ), ), ); } }