a642bf16a2
design/ 同步自最新设计导出,新增 ui_kits/tablet/ 平板分栏布局;todo/ 录入 18 个并行实施任务。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
308 lines
14 KiB
Dart
308 lines
14 KiB
Dart
// 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<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), // 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<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);
|
|
// 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<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,
|
|
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),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|