iykyka/mobile/lib/theme/app_theme.dart

262 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
/// Messenger UI for 와카뷰 — iMessage-inspired light default + soft charcoal dark.
/// Light: white stage, blue mine bubbles, gray peer bubbles.
/// Dark: elevated charcoal (not pure black), softer blue accents.
class AppTheme {
AppTheme._();
// Light — iOS Messages / white messenger
static const canvasLight = Color(0xFFF2F2F7);
static const surfaceLight = Color(0xFFFFFFFF);
static const inkLight = Color(0xFF1C1C1E);
static const mutedLight = Color(0xFF8E8E93);
static const lineLight = Color(0xFFD1D1D6);
static const softFillLight = Color(0xFFE9E9EB);
// Dark — soft charcoal (not #000)
static const canvasDark = Color(0xFF141418);
static const surfaceDark = Color(0xFF1C1C22);
static const inkDark = Color(0xFFF2F2F7);
static const mutedDark = Color(0xFF98989F);
static const lineDark = Color(0xFF2C2C34);
static const softFillDark = Color(0xFF2C2C34);
/// iMessage-like blue for mine bubbles & primary actions.
static const accentLight = Color(0xFF007AFF);
static const accentDark = Color(0xFF0A84FF);
static const accentSoftLight = Color(0xFFD6E8FF);
static const accentSoftDark = Color(0xFF163A66);
static const twinAmberLight = Color(0xFFB08A4A);
static const twinAmberDark = Color(0xFFD4B06A);
/// Compat for older call sites.
static const teal = accentLight;
static const tealBright = accentDark;
static const canvasDarkCompat = canvasDark;
static const brandGradient = LinearGradient(
colors: [accentLight, accentLight],
);
static Color twinAccent(Brightness b) => b == Brightness.dark ? twinAmberDark : twinAmberLight;
static LinearGradient backgroundGradient(Brightness b) {
final c = b == Brightness.dark ? canvasDark : canvasLight;
return LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [c, c],
);
}
static Color glassFill(Brightness b) => b == Brightness.dark ? surfaceDark : surfaceLight;
static Color glassBorder(Brightness b) => b == Brightness.dark ? lineDark : lineLight;
static Color glassShadow(Brightness b) => Colors.transparent;
/// Mine = solid messenger blue (both modes).
static Color mineBubble(Brightness b) => b == Brightness.dark ? accentDark : accentLight;
static Color mineBubbleFg(Brightness b) => Colors.white;
static Color peerBubble(Brightness b) => b == Brightness.dark ? softFillDark : softFillLight;
static const rInput = 20.0;
static const rButton = 14.0;
static const rBubble = 20.0;
static const rPanel = 14.0;
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 ? accentDark : accentLight;
final onPrimary = Colors.white;
final ink = isDark ? inkDark : inkLight;
final muted = isDark ? mutedDark : mutedLight;
final line = isDark ? lineDark : lineLight;
final surface = isDark ? surfaceDark : surfaceLight;
final soft = isDark ? softFillDark : softFillLight;
final scheme = ColorScheme(
brightness: brightness,
primary: primary,
onPrimary: onPrimary,
primaryContainer: isDark ? accentSoftDark : accentSoftLight,
onPrimaryContainer: primary,
secondary: isDark ? twinAmberDark : twinAmberLight,
onSecondary: isDark ? canvasDark : Colors.white,
secondaryContainer: soft,
onSecondaryContainer: muted,
tertiary: primary,
onTertiary: onPrimary,
error: isDark ? const Color(0xFFFF6961) : const Color(0xFFFF3B30),
onError: Colors.white,
surface: surface,
onSurface: ink,
onSurfaceVariant: muted,
outline: line,
outlineVariant: line,
surfaceContainerHighest: soft,
surfaceContainerHigh: soft,
surfaceContainer: surface,
);
final textTheme = _textTheme(GoogleFonts.plusJakartaSansTextTheme(), ink);
return ThemeData(
useMaterial3: true,
brightness: brightness,
colorScheme: scheme,
scaffoldBackgroundColor: Colors.transparent,
visualDensity: VisualDensity.standard,
textTheme: textTheme,
primaryTextTheme: textTheme,
appBarTheme: AppBarTheme(
backgroundColor: surface.withValues(alpha: isDark ? 0.94 : 0.88),
foregroundColor: ink,
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
titleTextStyle: textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600, letterSpacing: -0.2),
),
cardTheme: CardThemeData(
elevation: 0,
color: surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(rPanel),
side: BorderSide(color: line.withValues(alpha: isDark ? 0.9 : 0.65)),
),
margin: EdgeInsets.zero,
),
listTileTheme: ListTileThemeData(
iconColor: muted,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rPanel)),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
minVerticalPadding: 10,
),
dividerTheme: DividerThemeData(color: line, space: 1, thickness: 0.5),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: isDark ? soft : surface,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(rInput),
borderSide: BorderSide(color: line.withValues(alpha: 0.7)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(rInput),
borderSide: BorderSide(color: line.withValues(alpha: 0.7)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(rInput),
borderSide: BorderSide(color: primary, width: 1.4),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(rInput),
borderSide: BorderSide(color: scheme.error),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(rInput),
borderSide: BorderSide(color: scheme.error, width: 1.4),
),
hintStyle: TextStyle(color: muted.withValues(alpha: 0.85)),
labelStyle: TextStyle(color: muted, fontWeight: FontWeight.w500),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: primary,
foregroundColor: onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rButton)),
textStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
elevation: 0,
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: primary,
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rButton)),
side: BorderSide(color: primary.withValues(alpha: 0.45)),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: primary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
),
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(
foregroundColor: primary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
chipTheme: ChipThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
side: BorderSide(color: line),
backgroundColor: soft,
labelStyle: TextStyle(color: ink, fontWeight: FontWeight.w500, fontSize: 12),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: primary,
foregroundColor: onPrimary,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
backgroundColor: isDark ? softFillDark : inkLight,
contentTextStyle: TextStyle(color: isDark ? inkDark : Colors.white),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rPanel)),
elevation: 0,
),
dialogTheme: DialogThemeData(
backgroundColor: surface,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
side: BorderSide(color: line.withValues(alpha: 0.8)),
),
),
progressIndicatorTheme: ProgressIndicatorThemeData(color: primary),
bannerTheme: MaterialBannerThemeData(backgroundColor: surface, padding: const EdgeInsets.all(16)),
segmentedButtonTheme: SegmentedButtonThemeData(
style: SegmentedButton.styleFrom(
backgroundColor: soft,
foregroundColor: muted,
selectedForegroundColor: primary,
selectedBackgroundColor: isDark ? accentSoftDark : accentSoftLight,
side: BorderSide.none,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
),
),
);
}
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: 34, ls: -0.8),
displaySmall: s(base.displaySmall, w: FontWeight.w700, size: 28, ls: -0.5),
headlineSmall: s(base.headlineSmall, w: FontWeight.w600, size: 22, ls: -0.3),
titleLarge: s(base.titleLarge, w: FontWeight.w600, size: 17, ls: -0.2),
titleMedium: s(base.titleMedium, w: FontWeight.w600, size: 16),
titleSmall: s(base.titleSmall, w: FontWeight.w600, size: 15),
bodyLarge: s(base.bodyLarge, w: FontWeight.w400, size: 17, h: 1.35),
bodyMedium: s(base.bodyMedium, w: FontWeight.w400, size: 16, h: 1.35),
bodySmall: s(base.bodySmall, w: FontWeight.w400, size: 13, h: 1.35),
labelLarge: s(base.labelLarge, w: FontWeight.w500, size: 15),
labelSmall: s(base.labelSmall, w: FontWeight.w500, size: 11),
);
}
}