From a 7-agent design workflow (3 lenses judged + synthesised). Shipped the high-impact/low-risk tier: - message bubbles: self #1265ED (WCAG AA pass), incoming #1C2452 + hairline - deterministic per-user avatar colours (8-hue brand palette) - room tile: brighter unread preview, compact density, aligned timestamp - FAB -> New message (thumb zone), explore -> AppBar - login + welcome brand gradient, card outlines - input maxLines cap, bubble max-width viewport fraction, AppBar hairline All verified on production build in headless Chromium at mobile size. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
147 lines
5.0 KiB
Dart
147 lines
5.0 KiB
Dart
// Version: 2.0.0 | Created: 2026-04-01 | Updated: 2026-07-03
|
|
// M8Chat brand theme — dark and light variants.
|
|
// Palette from the m8chat.au Elementor kit (kit post 8):
|
|
// Accent #1 "#1265ED", Blue Element "#3B8BFF", Ancop Blue "#010B46".
|
|
// v2.0.0 replaces the old purple scheme, which did not match the website.
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// M8Chat brand colours.
|
|
abstract final class M8Colours {
|
|
static const Color brandBlue = Color(0xFF1265ED);
|
|
static const Color accentBlue = Color(0xFF3B8BFF);
|
|
// Self message bubble — deeper than accentBlue so white text passes WCAG AA
|
|
// (white on #1265ED = 5.11:1; on #3B8BFF = 3.32:1, fails).
|
|
static const Color selfBubble = Color(0xFF1265ED);
|
|
static const Color darkBackground = Color(0xFF0A0F2E);
|
|
static const Color darkSurface = Color(0xFF131A42);
|
|
static const Color darkSurfaceVariant = Color(0xFF1C2452);
|
|
static const Color onDarkSurface = Color(0xFFE8ECF5);
|
|
static const Color subtleText = Color(0xFF96A0C2);
|
|
static const Color errorRed = Color(0xFFCF6679);
|
|
static const Color unreadGreen = Color(0xFF4CAF50);
|
|
}
|
|
|
|
/// Dark theme — default for M8Chat.
|
|
ThemeData buildDarkTheme() {
|
|
const seed = M8Colours.brandBlue;
|
|
|
|
final scheme =
|
|
ColorScheme.fromSeed(
|
|
seedColor: seed,
|
|
brightness: Brightness.dark,
|
|
).copyWith(
|
|
surface: M8Colours.darkBackground,
|
|
surfaceContainerHighest: M8Colours.darkSurface,
|
|
primary: M8Colours.accentBlue,
|
|
onSurface: M8Colours.onDarkSurface,
|
|
error: M8Colours.errorRed,
|
|
);
|
|
|
|
return ThemeData(
|
|
useMaterial3: true,
|
|
colorScheme: scheme,
|
|
scaffoldBackgroundColor: M8Colours.darkBackground,
|
|
appBarTheme: AppBarTheme(
|
|
backgroundColor: M8Colours.darkSurface,
|
|
foregroundColor: M8Colours.onDarkSurface,
|
|
elevation: 0,
|
|
scrolledUnderElevation: 2,
|
|
surfaceTintColor: Colors.transparent,
|
|
shape: const Border(
|
|
bottom: BorderSide(color: M8Colours.darkSurfaceVariant),
|
|
),
|
|
),
|
|
navigationBarTheme: NavigationBarThemeData(
|
|
backgroundColor: M8Colours.darkSurface,
|
|
indicatorColor: M8Colours.accentBlue.withAlpha(51),
|
|
labelTextStyle: WidgetStateProperty.all(
|
|
const TextStyle(fontSize: 12, color: M8Colours.onDarkSurface),
|
|
),
|
|
),
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: M8Colours.darkSurfaceVariant,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: const BorderSide(color: M8Colours.accentBlue, width: 2),
|
|
),
|
|
labelStyle: const TextStyle(color: M8Colours.subtleText),
|
|
hintStyle: const TextStyle(color: M8Colours.subtleText),
|
|
),
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: M8Colours.accentBlue,
|
|
foregroundColor: Colors.white,
|
|
minimumSize: const Size.fromHeight(52),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
textStyle: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
),
|
|
cardTheme: CardThemeData(
|
|
color: M8Colours.darkSurface,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
),
|
|
dividerTheme: const DividerThemeData(
|
|
color: M8Colours.darkSurfaceVariant,
|
|
thickness: 1,
|
|
),
|
|
snackBarTheme: SnackBarThemeData(
|
|
backgroundColor: M8Colours.darkSurfaceVariant,
|
|
contentTextStyle: const TextStyle(color: M8Colours.onDarkSurface),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Light theme — follows system preference if user selects it.
|
|
ThemeData buildLightTheme() {
|
|
const seed = M8Colours.brandBlue;
|
|
|
|
final scheme = ColorScheme.fromSeed(
|
|
seedColor: seed,
|
|
brightness: Brightness.light,
|
|
);
|
|
|
|
return ThemeData(
|
|
useMaterial3: true,
|
|
colorScheme: scheme,
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
style: ElevatedButton.styleFrom(
|
|
minimumSize: const Size.fromHeight(52),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
textStyle: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
),
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(color: scheme.primary, width: 2),
|
|
),
|
|
),
|
|
snackBarTheme: SnackBarThemeData(
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
}
|