262 lines
11 KiB
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),
|
|
);
|
|
}
|
|
}
|