iykyka/mobile/lib/theme/app_theme.dart

263 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
/// Quiet Ink — calm messenger direction for 와카뷰.
/// Light: warm off-white canvas, charcoal ink, single deep-teal accent.
/// Dark: charcoal surfaces, same teal (brighter), twin stays warm amber.
/// Avoids aurora/purple glass so chat reads as a daily messenger, not a demo.
class AppTheme {
AppTheme._();
// Light
static const canvasLight = Color(0xFFF7F6F3);
static const surfaceLight = Color(0xFFFFFFFF);
static const inkLight = Color(0xFF1C1B1A);
static const mutedLight = Color(0xFF6B6864);
static const lineLight = Color(0xFFE8E6E1);
static const teal = Color(0xFF0F766E);
static const tealSoft = Color(0xFFD9F3F0);
static const twinAmberLight = Color(0xFFB45309);
// Dark
static const canvasDark = Color(0xFF0E0F12);
static const surfaceDark = Color(0xFF1A1C22);
static const inkDark = Color(0xFFF2F1EF);
static const mutedDark = Color(0xFF9B9893);
static const lineDark = Color(0xFF2A2D36);
static const tealBright = Color(0xFF2DD4BF);
static const tealSoftDark = Color(0xFF143D3A);
static const twinAmberDark = Color(0xFFFBBF24);
/// Kept for call sites that still reference a "brand" fill — solid teal, not a rainbow.
static const brandGradient = LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [teal, Color(0xFF0D9488)],
);
static Color twinAccent(Brightness brightness) =>
brightness == Brightness.dark ? twinAmberDark : twinAmberLight;
/// Soft near-flat wash (not aurora). Used by [GradientBackdrop].
static LinearGradient backgroundGradient(Brightness brightness) {
if (brightness == Brightness.dark) {
return const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF12141A), canvasDark],
);
}
return const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFF3F2EE), canvasLight],
);
}
static Color glowPrimary(Brightness brightness) =>
(brightness == Brightness.dark ? tealBright : teal).withValues(alpha: 0.06);
static Color glowSecondary(Brightness brightness) => Colors.transparent;
/// Panel fill (replaces glassmorphism).
static Color glassFill(Brightness brightness) =>
brightness == Brightness.dark ? surfaceDark : surfaceLight;
static Color glassBorder(Brightness brightness) =>
brightness == Brightness.dark ? lineDark : lineLight;
static Color glassShadow(Brightness brightness) =>
brightness == Brightness.dark ? Colors.black.withValues(alpha: 0.35) : const Color(0xFF1C1B1A).withValues(alpha: 0.06);
static Color mineBubble(Brightness brightness) =>
brightness == Brightness.dark ? tealSoftDark : teal;
static Color mineBubbleFg(Brightness brightness) =>
brightness == Brightness.dark ? inkDark : Colors.white;
static Color peerBubble(Brightness brightness) =>
brightness == Brightness.dark ? const Color(0xFF24272F) : const Color(0xFFEFEEEA);
static ThemeData light() => _build(Brightness.light);
static ThemeData dark() => _build(Brightness.dark);
static ThemeData _build(Brightness brightness) {
final isDark = brightness == Brightness.dark;
final primary = isDark ? tealBright : teal;
final onPrimary = isDark ? canvasDark : Colors.white;
final scheme = ColorScheme(
brightness: brightness,
primary: primary,
onPrimary: onPrimary,
primaryContainer: isDark ? tealSoftDark : tealSoft,
onPrimaryContainer: isDark ? tealBright : const Color(0xFF0A4F4A),
secondary: isDark ? twinAmberDark : twinAmberLight,
onSecondary: isDark ? canvasDark : Colors.white,
secondaryContainer: isDark ? const Color(0xFF3D2E14) : const Color(0xFFFFE8C8),
onSecondaryContainer: isDark ? twinAmberDark : twinAmberLight,
tertiary: primary,
onTertiary: onPrimary,
error: isDark ? const Color(0xFFF87171) : const Color(0xFFB91C1C),
onError: Colors.white,
surface: isDark ? surfaceDark : surfaceLight,
onSurface: isDark ? inkDark : inkLight,
onSurfaceVariant: isDark ? mutedDark : mutedLight,
outline: isDark ? lineDark : lineLight,
outlineVariant: isDark ? lineDark : lineLight,
surfaceContainerHighest: isDark ? const Color(0xFF24272F) : const Color(0xFFEFEEEA),
surfaceContainerHigh: isDark ? const Color(0xFF20232A) : const Color(0xFFF3F2EE),
surfaceContainer: isDark ? surfaceDark : surfaceLight,
);
final baseText = GoogleFonts.plusJakartaSansTextTheme();
final textTheme = _textTheme(baseText, scheme.onSurface);
final panel = glassFill(brightness);
final edge = glassBorder(brightness);
final shadow = glassShadow(brightness);
final shape = RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: edge),
);
return ThemeData(
useMaterial3: true,
brightness: brightness,
colorScheme: scheme,
scaffoldBackgroundColor: Colors.transparent,
visualDensity: VisualDensity.standard,
textTheme: textTheme,
primaryTextTheme: textTheme,
appBarTheme: AppBarTheme(
backgroundColor: Colors.transparent,
foregroundColor: scheme.onSurface,
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: false,
titleTextStyle: textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700, letterSpacing: -0.3),
),
cardTheme: CardThemeData(
elevation: 0,
shadowColor: shadow,
color: panel,
shape: shape,
margin: EdgeInsets.zero,
),
listTileTheme: ListTileThemeData(
iconColor: scheme.onSurfaceVariant,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4),
minVerticalPadding: 10,
),
dividerTheme: DividerThemeData(color: edge, space: 1, thickness: 1),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: isDark ? const Color(0xFF14161C) : const Color(0xFFF0EFEC),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: edge),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: edge),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: primary, width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: scheme.error),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: scheme.error, width: 1.5),
),
hintStyle: TextStyle(color: scheme.onSurfaceVariant.withValues(alpha: 0.75)),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: primary,
foregroundColor: onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
textStyle: const TextStyle(fontWeight: FontWeight.w700),
elevation: 0,
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: scheme.onSurface,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
side: BorderSide(color: edge),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: primary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
),
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(
foregroundColor: scheme.onSurfaceVariant,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
chipTheme: ChipThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
side: BorderSide(color: edge),
backgroundColor: panel,
labelStyle: TextStyle(color: scheme.onSurfaceVariant, fontWeight: FontWeight.w600, fontSize: 12),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: primary,
foregroundColor: onPrimary,
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
backgroundColor: isDark ? surfaceDark : inkLight,
contentTextStyle: TextStyle(color: isDark ? inkDark : Colors.white),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
dialogTheme: DialogThemeData(
backgroundColor: panel,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
),
progressIndicatorTheme: ProgressIndicatorThemeData(color: primary),
bannerTheme: MaterialBannerThemeData(backgroundColor: panel, padding: const EdgeInsets.all(16)),
);
}
static TextTheme _textTheme(TextTheme base, Color ink) {
TextStyle s(TextStyle? t, {FontWeight? w, double? size, double? ls, double? h}) =>
(t ?? const TextStyle()).copyWith(
color: ink,
fontWeight: w,
fontSize: size,
letterSpacing: ls,
height: h,
);
return base.copyWith(
displayLarge: s(base.displayLarge, w: FontWeight.w700, size: 40, ls: -1.2),
displaySmall: s(base.displaySmall, w: FontWeight.w700, size: 32, ls: -0.8),
headlineSmall: s(base.headlineSmall, w: FontWeight.w700, size: 22, ls: -0.3),
titleLarge: s(base.titleLarge, w: FontWeight.w700, size: 18, ls: -0.2),
titleMedium: s(base.titleMedium, w: FontWeight.w600, size: 16),
titleSmall: s(base.titleSmall, w: FontWeight.w600, size: 14),
bodyLarge: s(base.bodyLarge, w: FontWeight.w400, size: 16, h: 1.45),
bodyMedium: s(base.bodyMedium, w: FontWeight.w400, size: 14, h: 1.45),
bodySmall: s(base.bodySmall, w: FontWeight.w400, size: 12, h: 1.4),
labelLarge: s(base.labelLarge, w: FontWeight.w600, size: 14),
labelSmall: s(base.labelSmall, w: FontWeight.w600, size: 11, ls: 0.1),
);
}
}