264 lines
11 KiB
Dart
264 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);
|
|
/// Dark accent — muted teal (avoid neon cyan against charcoal).
|
|
static const tealBright = Color(0xFF5EBBB0);
|
|
static const tealSoftDark = Color(0xFF1A3331);
|
|
static const twinAmberDark = Color(0xFFE0A84A);
|
|
|
|
/// 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 ? const Color(0xFF244844) : 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),
|
|
);
|
|
}
|
|
}
|