Files
pangolin/client/lib/pangolin_theme.dart
T
wangjia 526e7f2f33 feat(tsk_iRa5iBC4v7Tu): Flutter 客户端工程搭建(演示态)
基于 design/flutter/ 起步包在 client/ 下建立 Flutter 工程:

- pubspec.yaml:flutter_svg / lucide_icons / google_fonts 依赖;
  SVG 资产注册;字体由 google_fonts 运行时加载,无需本地 ttf
- lib/pangolin_theme.dart:完整设计令牌(色阶 / 间距 / 圆角 /
  动效 / 文字阶),PangolinText 改用 GoogleFonts getter,
  PangolinTheme.light / .dark 开箱即用
- lib/main.dart:runApp + 明暗主题 + 登录 → 引导 → 主框架 RootFlow
- lib/widgets/:全套组件雏形
    pangolin_icons    Lucide 图标映射
    pangolin_button   胶囊按钮四态
    country_code      国家码块 + 信号条
    status_pill       色点胶囊
    connect_button    核心连接键三态(off/connecting/on + 动画)
    server_tile       服务器列表行
    plan_card         套餐卡(专业版渐变高亮)
    auth_screen       登录 / 注册(邮箱验证码 + 设密码)
    onboarding_screen 首次引导 PageView(3 屏可滑动 + 进度点)
    home_shell        底部 4 Tab(连接/节点/统计/账户 + 状态机)
    account_screens   套餐选择 / 设备管理 / 兑换 / 联系我们
    pangolin_logo     PangolinMark / BrandLockup / AppIcon(SVG)
- assets/:logo-mark / logo-mark-white / logo-wordmark / app-icon(SVG)
- android/:Kotlin 主 Activity + Manifest + Gradle 配置
- ios/:Runner.xcodeproj + xcscheme + Podfile + AppDelegate + storyboard

运行方式(cd client/):
  flutter pub get && flutter run

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 02:15:42 +08:00

322 lines
13 KiB
Dart

// 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<PangolinScheme> {
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<PangolinScheme>? 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<PangolinScheme>()!;
}
/// ── 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<BoxShadow> sm = [BoxShadow(color: _tint.withOpacity(0.06), blurRadius: 2, offset: const Offset(0, 1))];
static List<BoxShadow> md = [BoxShadow(color: _tint.withOpacity(0.08), blurRadius: 14, offset: const Offset(0, 4))];
static List<BoxShadow> lg = [BoxShadow(color: _tint.withOpacity(0.12), blurRadius: 32, offset: const Offset(0, 12))];
static List<BoxShadow> 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),
),
),
);
}
}