feat(theme): 接入 Obsidian Flare 设计系统

This commit is contained in:
Zhou Weidong committed 2026-10-19 15:01:41 +08:00
1 parent b1c69e22af
commit eae1c533ce
17 files changed
+411 -49

No files matched your search

+46 -27
View File
@@ -2,26 +2,31 @@
import 'package:flutter/material.dart';
/// Evening palette for the Indonesia launch.
/// Obsidian Flare palette from `docs/design/README.md` and screens 01/02.
///
/// Chili is the action color. Gold marks coins, rewards, and playback
/// progress. Cocoa surfaces keep the app in night-market light instead of
/// a cold utility screen.
/// Existing semantic names (`blue`, `blueSoft`, `ink`, ...) are kept so page
/// code does not need a mass rename; their values now follow the design tokens.
abstract final class AppColors {
/// Chili fill for buttons, selected chips, and brand marks.
static const blue = Color(0xFFC4322A);
/// Primary CTA, selected navigation, playback progress.
static const blue = Color(0xFFFF5B3D);
static const primary = blue;
/// Pressed chili.
static const bluePressed = Color(0xFF8E221C);
/// Pressed state for the primary CTA.
static const bluePressed = Color(0xFFFF4321);
/// Coral for text and icons sitting on dark surfaces.
static const blueSoft = Color(0xFFFF8A72);
/// Secondary accent for tags, locks, and highlighted icons.
static const blueSoft = Color(0xFF9278EA);
static const secondary = blueSoft;
static const lavender = blueSoft;
/// Coins, rewards, and the playback progress line.
static const gold = Color(0xFFF6C453);
/// Coins and rewards.
static const gold = Color(0xFFFBBF24);
/// Favorite/like heart without competing with the chili primary action.
static const favorite = Color(0xFFD74A7D);
/// Success and free-state accents.
static const success = Color(0xFF34D399);
/// Liked hearts use the primary accent.
static const favorite = blue;
/// Text placed on [gold].
static const onGold = Color(0xFF2A1810);
@@ -30,23 +35,37 @@ abstract final class AppColors {
static const onPrimary = Color(0xFFFFFFFF);
/// Page background.
static const ink = Color(0xFF100C0A);
static const ink = Color(0xFF0B0C10);
/// Bottom bar.
static const bar = Color(0xFF1A1412);
/// Bottom navigation background (rendered at 95% with blur).
static const bar = Color(0xFF0B0C10);
/// Cards lifted off [ink].
static const card = Color(0xFF241C19);
static const card = Color(0xFF16181F);
/// Chips and nested rows.
static const raised = Color(0xFF322824);
/// Floating surfaces, inputs, and nested rows.
static const raised = Color(0xFF1D1E28);
/// Primary text on dark surfaces.
static const cream = Color(0xFFF8F1EA);
/// Body text on dark surfaces.
static const cream = Color(0xFFE3E2E8);
static const muted = Color(0xFFB7A59C);
static const outlineDark = Color(0xFF4A3C36);
static const outlineVariantDark = Color(0xFF2A221F);
static const outlineLight = Color(0xFF8C7A72);
static const outlineVariantLight = Color(0xFFE4D5CC);
/// Titles and high-emphasis text.
static const onSurface = Color(0xFFFFFFFF);
/// Secondary text.
static const muted = Color(0xFFA1A1AA);
/// Weak hints and timestamps.
static const subtle = Color(0xFF71717A);
/// 8% white border used by cards and inputs.
static const border = Color(0x14FFFFFF);
/// 14% white border used by glass controls.
static const glassBorder = Color(0x24FFFFFF);
static const outlineDark = Color(0xFF2A2C36);
static const outlineVariantDark = Color(0xFF242630);
static const outlineLight = Color(0xFF8E8E93);
static const outlineVariantLight = Color(0xFFE4E4E7);
}
+9 -1
View File
@@ -1,6 +1,6 @@
// Author: fruit.com.cn
/// K20 cross-page spacing, type, radius, and control sizes.
/// Cross-page spacing, type, radius, and control sizes.
abstract final class AppDimens {
static const double spaceXs = 4;
static const double spaceSm = 8;
@@ -15,6 +15,9 @@ abstract final class AppDimens {
static const double innerRadius = 12;
static const double innerPadding = 12;
static const double pillRadius = 20;
static const double glassSmallRadius = 16;
static const double glassRadius = 24;
static const double glassCardRadius = 26;
static const double caption = 12;
static const double emptyStateSubtitle = 12.5;
@@ -34,6 +37,11 @@ abstract final class AppDimens {
static const double navIcon = 22;
static const double progressHeight = 4;
static const double progressRadius = 4;
static const double progressLineHeight = 1.5;
static const double searchHeight = 44;
static const double navDot = 3;
static const double glassBlur = 24;
static const double accountCardHeight = 64;
static const double accountIcon = 18;
+37 -13
View File
@@ -6,9 +6,10 @@ import 'package:flutter/services.dart';
import 'app_colors.dart';
import 'app_dimens.dart';
/// Light paper and warm-cocoa themes for the Indonesia evening palette.
/// Obsidian Flare themes. Dark is the product default; light mirrors the same
/// orange primary for system-light devices.
abstract final class AppTheme {
static const _lightBackground = Color(0xFFFFF6F1);
static const _lightBackground = Color(0xFFF7F7F8);
static const _darkBackground = AppColors.ink;
/// Light theme for system light mode.
@@ -22,7 +23,7 @@ abstract final class AppTheme {
static ThemeData dark() => _build(
brightness: Brightness.dark,
surface: _darkBackground,
accent: AppColors.blueSoft,
accent: AppColors.blue,
);
static ThemeData _build({
@@ -35,27 +36,27 @@ abstract final class AppTheme {
? const ColorScheme.dark(
primary: AppColors.blue,
onPrimary: AppColors.onPrimary,
secondary: AppColors.blueSoft,
onSecondary: AppColors.ink,
secondary: AppColors.secondary,
onSecondary: AppColors.onPrimary,
primaryContainer: AppColors.bluePressed,
onPrimaryContainer: AppColors.onPrimary,
secondaryContainer: AppColors.bluePressed,
secondaryContainer: AppColors.raised,
onSecondaryContainer: AppColors.onPrimary,
surface: _darkBackground,
onSurface: AppColors.cream,
onSurface: AppColors.onSurface,
outline: AppColors.outlineDark,
outlineVariant: AppColors.outlineVariantDark,
surfaceTint: AppColors.blueSoft,
surfaceTint: AppColors.blue,
inversePrimary: AppColors.blueSoft,
)
: ColorScheme.light(
primary: AppColors.blue,
onPrimary: AppColors.onPrimary,
secondary: AppColors.blue,
secondary: AppColors.secondary,
onSecondary: AppColors.onPrimary,
primaryContainer: AppColors.blueSoft.withValues(alpha: 0.22),
primaryContainer: AppColors.blue.withValues(alpha: 0.14),
onPrimaryContainer: AppColors.ink,
secondaryContainer: AppColors.blueSoft.withValues(alpha: 0.22),
secondaryContainer: AppColors.blueSoft.withValues(alpha: 0.14),
onSecondaryContainer: AppColors.ink,
surface: _lightBackground,
onSurface: AppColors.ink,
@@ -64,11 +65,12 @@ abstract final class AppTheme {
surfaceTint: AppColors.blue,
inversePrimary: AppColors.blueSoft,
);
return ThemeData(
final base = ThemeData(
useMaterial3: true,
brightness: brightness,
colorScheme: scheme,
scaffoldBackgroundColor: surface,
fontFamily: 'PlusJakartaSans',
progressIndicatorTheme: ProgressIndicatorThemeData(color: accent),
sliderTheme: SliderThemeData(
activeTrackColor: accent,
@@ -105,7 +107,8 @@ abstract final class AppTheme {
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle.light,
titleTextStyle: TextStyle(
color: AppColors.cream,
color: AppColors.onSurface,
fontFamily: 'Outfit',
fontSize: AppDimens.pageTitle,
fontWeight: FontWeight.w800,
),
@@ -125,8 +128,29 @@ abstract final class AppTheme {
contentTextStyle: const TextStyle(color: AppColors.onPrimary),
),
);
final textTheme = base.textTheme
.apply(
fontFamily: 'PlusJakartaSans',
bodyColor: dark ? AppColors.cream : AppColors.ink,
displayColor: dark ? AppColors.onSurface : AppColors.ink,
)
.copyWith(
displayLarge: _display(base.textTheme.displayLarge, dark),
displayMedium: _display(base.textTheme.displayMedium, dark),
displaySmall: _display(base.textTheme.displaySmall, dark),
headlineLarge: _display(base.textTheme.headlineLarge, dark),
headlineMedium: _display(base.textTheme.headlineMedium, dark),
headlineSmall: _display(base.textTheme.headlineSmall, dark),
titleLarge: _display(base.textTheme.titleLarge, dark),
);
return base.copyWith(textTheme: textTheme);
}
static TextStyle? _display(TextStyle? style, bool dark) => style?.copyWith(
fontFamily: 'Outfit',
color: dark ? AppColors.onSurface : AppColors.ink,
);
static Color _buttonColor(Set<WidgetState> states) {
if (states.contains(WidgetState.pressed) ||
states.contains(WidgetState.hovered) ||
+54
View File
@@ -0,0 +1,54 @@
// Author: fruit.com.cn
import 'dart:ui';
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_dimens.dart';
/// Dark glass surface used by the home info card and floating controls.
class GlassPanel extends StatelessWidget {
const GlassPanel({
super.key,
required this.child,
this.radius = AppDimens.glassRadius,
this.blur = AppDimens.glassBlur,
this.padding,
this.borderColor = AppColors.glassBorder,
this.gradient,
});
final Widget child;
final double radius;
final double blur;
final EdgeInsetsGeometry? padding;
final Color borderColor;
final Gradient? gradient;
@override
Widget build(BuildContext context) {
final borderRadius = BorderRadius.circular(radius);
final surface = DecoratedBox(
decoration: BoxDecoration(
borderRadius: borderRadius,
border: Border.all(color: borderColor),
gradient:
gradient ??
const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0x24FFFFFF), Color(0x0DFFFFFF)],
),
),
child: Padding(padding: padding ?? EdgeInsets.zero, child: child),
);
return ClipRRect(
borderRadius: borderRadius,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
child: surface,
),
);
}
}