PraxaLing / mobile /lib /theme /app_theme.dart
Reubencf's picture
Deploy PraxaLing: component refactor, bug fixes, unified neo-brutal design, Qwen3.5 everywhere
2992997
Raw
History Blame Contribute Delete
9.83 kB
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
// --- Neo-brutalism palette (mirrors web app/globals.css + Tailwind usage) ---
const kBrutalBlack = Color(0xFF000000); // borders, shadows, body text
const kBrutalYellow = Color(0xFFFFD21E); // warning / accent
const kBrutalWhite = Color(0xFFFFFFFF); // card / unselected button
const kBrutalCream = Color(0xFFF8FBFF); // page background — matches web #F8FBFF
const kBrutalRed = Color(0xFFF43F5E); // error / wrong answer — matches web #F43F5E
const kBrutalGreen = Color(0xFF22C55E); // correct answer
const kBrutalBlue = Color(0xFF3B82F6); // secondary accent (web blue)
const kBrutalTeal = Color(0xFF0EA5A4); // primary brand (web teal)
const kBrutalMuted = Color(0xFF777777); // secondary text — matches web #777777
const kBrutalSlate = Color(0xFF3C3C3C); // body text — matches web #3C3C3C
// --- Back-compat aliases (old screens still reference these names) ---
const kPrimary = kBrutalTeal; // teal is the primary brand color (web)
const kPrimaryShadow = kBrutalBlack;
const kSecondary = kBrutalBlue;
const kSecondaryShadow = kBrutalBlack;
const kWarning = kBrutalYellow;
const kDanger = kBrutalRed;
const kDangerShadow = kBrutalBlack;
const kForeground = kBrutalSlate;
const kMuted = kBrutalMuted;
const kBackground = kBrutalCream;
const kCard = kBrutalWhite;
const kCardFeature = kBrutalWhite;
const kBorder = kBrutalBlack;
// Universal corner radius — neo-brutal: small, never soft.
const double kBrutalRadius = 4;
TextStyle brutalFont({
double? fontSize,
FontWeight fontWeight = FontWeight.w800,
Color? color,
double? height,
double? letterSpacing,
TextDecoration? decoration,
FontStyle? fontStyle,
}) {
return GoogleFonts.almarai(
fontSize: fontSize,
fontWeight: fontWeight,
color: color,
height: height,
letterSpacing: letterSpacing,
decoration: decoration,
fontStyle: fontStyle,
);
}
ThemeData buildTheme() {
final textTheme = GoogleFonts.almaraiTextTheme(
ThemeData.light().textTheme.apply(
bodyColor: kBrutalSlate,
displayColor: kBrutalBlack,
),
);
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
scaffoldBackgroundColor: kBrutalCream,
colorScheme: const ColorScheme.light(
primary: kBrutalTeal,
onPrimary: kBrutalWhite,
secondary: kBrutalBlue,
onSecondary: kBrutalWhite,
surface: kBrutalWhite,
onSurface: kBrutalSlate,
outline: kBrutalBlack,
error: kBrutalRed,
onError: kBrutalWhite,
),
textTheme: textTheme,
appBarTheme: AppBarTheme(
backgroundColor: kBrutalCream,
foregroundColor: kBrutalBlack,
elevation: 0,
centerTitle: false,
surfaceTintColor: Colors.transparent,
systemOverlayStyle: SystemUiOverlayStyle.dark,
titleTextStyle: GoogleFonts.almarai(
color: kBrutalBlack,
fontWeight: FontWeight.w900,
fontSize: 20,
),
iconTheme: const IconThemeData(color: kBrutalBlack),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: kBrutalTeal,
foregroundColor: kBrutalWhite,
disabledBackgroundColor: kBrutalWhite,
disabledForegroundColor: kBrutalMuted,
minimumSize: const Size(0, 52),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(kBrutalRadius),
side: const BorderSide(color: kBrutalBlack, width: 2),
),
textStyle: GoogleFonts.almarai(fontWeight: FontWeight.w900, fontSize: 15),
elevation: 0,
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: kBrutalBlack,
backgroundColor: kBrutalWhite,
minimumSize: const Size(0, 52),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(kBrutalRadius),
side: const BorderSide(color: kBrutalBlack, width: 2),
),
textStyle: GoogleFonts.almarai(fontWeight: FontWeight.w900, fontSize: 15),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: kBrutalBlack,
textStyle: GoogleFonts.almarai(fontWeight: FontWeight.w900),
),
),
cardTheme: CardThemeData(
color: kBrutalWhite,
elevation: 0,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(kBrutalRadius),
side: const BorderSide(color: kBrutalBlack, width: 2),
),
),
navigationBarTheme: NavigationBarThemeData(
height: 72,
backgroundColor: kBrutalWhite,
indicatorColor: kBrutalTeal,
surfaceTintColor: Colors.transparent,
indicatorShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(kBrutalRadius),
side: const BorderSide(color: kBrutalBlack, width: 2),
),
iconTheme: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return const IconThemeData(color: kBrutalWhite);
}
return const IconThemeData(color: kBrutalBlack);
}),
labelTextStyle: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return GoogleFonts.almarai(color: kBrutalBlack, fontSize: 12, fontWeight: FontWeight.w900);
}
return GoogleFonts.almarai(color: kBrutalMuted, fontSize: 12, fontWeight: FontWeight.w800);
}),
),
tabBarTheme: TabBarThemeData(
labelColor: kBrutalBlack,
unselectedLabelColor: kBrutalMuted,
indicatorSize: TabBarIndicatorSize.tab,
labelStyle: GoogleFonts.almarai(fontWeight: FontWeight.w900),
unselectedLabelStyle: GoogleFonts.almarai(fontWeight: FontWeight.w800),
),
dividerColor: kBrutalBlack,
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: kBrutalWhite,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(kBrutalRadius),
borderSide: const BorderSide(color: kBrutalBlack, width: 2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(kBrutalRadius),
borderSide: const BorderSide(color: kBrutalBlack, width: 2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(kBrutalRadius),
borderSide: const BorderSide(color: kBrutalBlue, width: 3),
),
hintStyle: GoogleFonts.almarai(color: kBrutalMuted, fontWeight: FontWeight.w700),
labelStyle: GoogleFonts.almarai(color: kBrutalMuted, fontWeight: FontWeight.w800),
),
chipTheme: ChipThemeData(
backgroundColor: kBrutalWhite,
selectedColor: kBrutalTeal,
disabledColor: kBrutalWhite,
labelStyle: GoogleFonts.almarai(color: kBrutalBlack, fontWeight: FontWeight.w900),
secondaryLabelStyle: GoogleFonts.almarai(color: kBrutalWhite, fontWeight: FontWeight.w900),
side: const BorderSide(color: kBrutalBlack, width: 2),
shape: const StadiumBorder(side: BorderSide(color: kBrutalBlack, width: 2)),
),
snackBarTheme: SnackBarThemeData(
backgroundColor: kBrutalBlack,
contentTextStyle: GoogleFonts.almarai(color: kBrutalWhite, fontWeight: FontWeight.w800),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(kBrutalRadius),
side: const BorderSide(color: kBrutalBlack, width: 2),
),
),
dialogTheme: DialogThemeData(
backgroundColor: kBrutalWhite,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(kBrutalRadius),
side: const BorderSide(color: kBrutalBlack, width: 3),
),
titleTextStyle: GoogleFonts.almarai(color: kBrutalBlack, fontSize: 20, fontWeight: FontWeight.w900),
contentTextStyle: GoogleFonts.almarai(color: kBrutalSlate, fontWeight: FontWeight.w800),
),
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: kBrutalWhite,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(kBrutalRadius)),
side: BorderSide(color: kBrutalBlack, width: 3),
),
),
);
}
// --- Decorations ---
/// Neo-brutal card: thick black border + hard black offset shadow.
/// [offset] shrinks to indicate a pressed state (web: translate-1).
BoxDecoration brutalCard({
Color color = kBrutalWhite,
double offset = 4,
double radius = kBrutalRadius,
Color borderColor = kBrutalBlack,
double borderWidth = 2,
}) {
return BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(radius),
border: Border.all(color: borderColor, width: borderWidth),
boxShadow: [
BoxShadow(color: kBrutalBlack, offset: Offset(offset, offset), blurRadius: 0),
],
);
}
/// Pressed/active variant — collapses to a 1px shadow, yellow fill by default.
BoxDecoration brutalCardPressed({
Color color = kBrutalYellow,
double radius = kBrutalRadius,
}) =>
brutalCard(color: color, offset: 1, radius: radius);
/// Back-compat alias — existing screens call this. Maps to the brutal version.
BoxDecoration duoCardDecoration({Color color = kBrutalWhite}) =>
brutalCard(color: color);