iykyka/mobile/lib/theme/app_theme.dart

258 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
/// Soft Neutral + surface hierarchy for 와카뷰.
/// Neutrals dominate; accent is thin (mine bubbles / small links only).
/// Radii: input/button 12, bubble 18. No glow, almost no shadow.
class AppTheme {
AppTheme._();
// Light neutrals
static const canvasLight = Color(0xFFFAFAF9);
static const surfaceLight = Color(0xFFFFFFFF);
static const inkLight = Color(0xFF171717);
static const mutedLight = Color(0xFF737373);
static const lineLight = Color(0xFFE5E5E5);
static const softFillLight = Color(0xFFF4F4F5);
// Dark neutrals
static const canvasDark = Color(0xFF0A0A0A);
static const surfaceDark = Color(0xFF141414);
static const inkDark = Color(0xFFFAFAFA);
static const mutedDark = Color(0xFFA3A3A3);
static const lineDark = Color(0xFF262626);
static const softFillDark = Color(0xFF1C1C1C);
/// Thin accent — used for mine bubbles & primary actions, not chrome.
static const accentLight = Color(0xFF3B6D9B);
static const accentDark = Color(0xFF8BB4D9);
static const accentSoftLight = Color(0xFFE8F0F7);
static const accentSoftDark = Color(0xFF1A2836);
static const twinAmberLight = Color(0xFF9A7B4F);
static const twinAmberDark = Color(0xFFC4A574);
/// 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(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;
static Color mineBubble(Brightness b) => b == Brightness.dark ? accentSoftDark : accentLight;
static Color mineBubbleFg(Brightness b) => b == Brightness.dark ? inkDark : Colors.white;
static Color peerBubble(Brightness b) => b == Brightness.dark ? softFillDark : softFillLight;
static const rInput = 12.0;
static const rButton = 12.0;
static const rBubble = 18.0;
static const rPanel = 12.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 = isDark ? canvasDark : Colors.white;
final ink = isDark ? inkDark : inkLight;
final muted = isDark ? mutedDark : mutedLight;
final line = isDark ? lineDark : lineLight;
final canvas = isDark ? canvasDark : canvasLight;
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(0xFFF87171) : const Color(0xFFB91C1C),
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: canvas.withValues(alpha: 0.92),
foregroundColor: ink,
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: false,
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),
),
margin: EdgeInsets.zero,
),
listTileTheme: ListTileThemeData(
iconColor: muted,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rPanel)),
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2),
minVerticalPadding: 8,
),
dividerTheme: DividerThemeData(color: line, space: 1, thickness: 1),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: surface,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(rInput),
borderSide: BorderSide(color: line),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(rInput),
borderSide: BorderSide(color: line),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(rInput),
borderSide: BorderSide(color: primary, width: 1.2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(rInput),
borderSide: BorderSide(color: scheme.error),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(rInput),
borderSide: BorderSide(color: scheme.error, width: 1.2),
),
hintStyle: TextStyle(color: muted.withValues(alpha: 0.8)),
labelStyle: TextStyle(color: muted, fontWeight: FontWeight.w500),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: isDark ? inkDark : inkLight,
foregroundColor: isDark ? canvasDark : Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
minimumSize: const Size(0, 46),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rButton)),
textStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15),
elevation: 0,
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: ink,
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
minimumSize: const Size(0, 46),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rButton)),
side: BorderSide(color: line),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: primary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
),
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(
foregroundColor: muted,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
),
chipTheme: ChipThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
side: BorderSide(color: line),
backgroundColor: surface,
labelStyle: TextStyle(color: muted, fontWeight: FontWeight.w500, fontSize: 12),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: isDark ? inkDark : inkLight,
foregroundColor: isDark ? canvasDark : Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rButton)),
),
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(16),
side: BorderSide(color: line),
),
),
progressIndicatorTheme: ProgressIndicatorThemeData(color: primary),
bannerTheme: MaterialBannerThemeData(backgroundColor: surface, padding: const EdgeInsets.all(16)),
segmentedButtonTheme: SegmentedButtonThemeData(
style: SegmentedButton.styleFrom(
backgroundColor: surface,
foregroundColor: muted,
selectedForegroundColor: ink,
selectedBackgroundColor: soft,
side: BorderSide(color: line),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rButton)),
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.w600, size: 36, ls: -1.0),
displaySmall: s(base.displaySmall, w: FontWeight.w600, size: 28, ls: -0.6),
headlineSmall: s(base.headlineSmall, w: FontWeight.w600, size: 20, ls: -0.2),
titleLarge: s(base.titleLarge, w: FontWeight.w600, size: 17, ls: -0.2),
titleMedium: s(base.titleMedium, w: FontWeight.w600, size: 15),
titleSmall: s(base.titleSmall, w: FontWeight.w600, size: 14),
bodyLarge: s(base.bodyLarge, w: FontWeight.w400, size: 15, 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.w500, size: 13),
labelSmall: s(base.labelSmall, w: FontWeight.w500, size: 11),
);
}
}