57377b1b21
Re-theme TasQ with a refined enterprise "Cyber/IT" identity on Material 3. Functionality is unchanged — only color/shape tokens and a few state widgets. Foundation: - Hand-tuned light + dark ColorSchemes (deep-slate surfaces, azure/teal/violet accents), tightened 12-16dp shape language, flat hairline-outlined cards. - New AppStatusColors theme extension (success/warning/danger/info/neutral, per-brightness) replacing scattered raw Colors.green/orange/red/grey so status color is consistent and legible in dark mode. - New TechChip mono primitive; StatusPill.kind; M3Card hairline variants; tightened AppSurfaces radii. Per-screen: - Tokenized status colors + radii across dashboard, tickets, tasks, IT service requests, attendance, whereabouts, network map, reports charts, auth, and shared/admin screens. Added _ticketStatusKind/_taskStatusKind/_isrStatusKind mappers so status carries meaning consistently. - Fixed white pie-slice borders for dark mode; dropped ad-hoc dialog radius overrides in favor of the theme default. Categorical palettes (leave-type icons, team color picker, distribution charts) intentionally preserved. Verified: flutter analyze lib clean; flutter build web green; visual QA in light and dark (dashboard, tickets, attendance). Co-Authored-By: claude-flow <ruv@ruv.net>
429 lines
18 KiB
Dart
429 lines
18 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:google_fonts/google_fonts.dart';
|
||
|
||
import 'app_typography.dart';
|
||
import 'app_surfaces.dart';
|
||
import 'app_status_colors.dart';
|
||
|
||
/// Refined enterprise **Cyber/IT** theme for TasQ, built on Material 3.
|
||
///
|
||
/// Identity:
|
||
/// * Deep slate surfaces (dark) / cool near-white (light) with an electric
|
||
/// **azure** primary, **teal** secondary and **violet** tertiary.
|
||
/// * Cards use a flat tonal fill plus a hairline outline (no drop shadow) for a
|
||
/// precise, technical feel.
|
||
/// * A tightened 12–16 dp shape language (see [AppSurfaces]).
|
||
/// * Monospace ([AppMonoText]) reserved for technical data — IDs, IPs, ports,
|
||
/// timestamps — and chip labels.
|
||
/// * Semantic status tokens ([AppStatusColors]) drive all status/priority color.
|
||
class AppTheme {
|
||
const AppTheme._();
|
||
|
||
/// Brand azure — anchors M3's neutral tonal ramp generation.
|
||
static const Color _seed = Color(0xFF2D7FF9);
|
||
|
||
static const AppSurfaces _surfaces = AppSurfaces(
|
||
cardRadius: 12,
|
||
compactCardRadius: 10,
|
||
containerRadius: 16,
|
||
dialogRadius: 20,
|
||
chipRadius: 8,
|
||
);
|
||
|
||
// ────────────────────────────────────────────────────────────
|
||
// COLOR SCHEMES (hand-tuned cyber palette)
|
||
// ────────────────────────────────────────────────────────────
|
||
static ColorScheme _darkScheme() {
|
||
return ColorScheme.fromSeed(
|
||
seedColor: _seed,
|
||
brightness: Brightness.dark,
|
||
).copyWith(
|
||
primary: const Color(0xFF4EA8FF),
|
||
onPrimary: const Color(0xFF04263F),
|
||
primaryContainer: const Color(0xFF0E3A5F),
|
||
onPrimaryContainer: const Color(0xFFCFE7FF),
|
||
secondary: const Color(0xFF2DD4BF),
|
||
onSecondary: const Color(0xFF00201C),
|
||
secondaryContainer: const Color(0xFF0C4B45),
|
||
onSecondaryContainer: const Color(0xFFB8F5EC),
|
||
tertiary: const Color(0xFFA78BFA),
|
||
onTertiary: const Color(0xFF1C0E3F),
|
||
tertiaryContainer: const Color(0xFF3A2A6B),
|
||
onTertiaryContainer: const Color(0xFFE7DDFF),
|
||
error: const Color(0xFFF87171),
|
||
onError: const Color(0xFF2A0A0A),
|
||
errorContainer: const Color(0xFF5A1A1A),
|
||
onErrorContainer: const Color(0xFFFFC9C4),
|
||
surface: const Color(0xFF0E141B),
|
||
onSurface: const Color(0xFFE3E9F0),
|
||
onSurfaceVariant: const Color(0xFFA7B4C2),
|
||
surfaceContainerLowest: const Color(0xFF080C11),
|
||
surfaceContainerLow: const Color(0xFF141B23),
|
||
surfaceContainer: const Color(0xFF17202A),
|
||
surfaceContainerHigh: const Color(0xFF1E2833),
|
||
surfaceContainerHighest: const Color(0xFF26313D),
|
||
surfaceDim: const Color(0xFF0B0F14),
|
||
surfaceBright: const Color(0xFF2A3542),
|
||
outline: const Color(0xFF3A4A5A),
|
||
outlineVariant: const Color(0xFF283440),
|
||
surfaceTint: const Color(0xFF4EA8FF),
|
||
inverseSurface: const Color(0xFFE3E9F0),
|
||
onInverseSurface: const Color(0xFF111820),
|
||
inversePrimary: const Color(0xFF0B6BCB),
|
||
);
|
||
}
|
||
|
||
static ColorScheme _lightScheme() {
|
||
return ColorScheme.fromSeed(
|
||
seedColor: _seed,
|
||
brightness: Brightness.light,
|
||
).copyWith(
|
||
primary: const Color(0xFF0B6BCB),
|
||
onPrimary: const Color(0xFFFFFFFF),
|
||
primaryContainer: const Color(0xFFD3E7FF),
|
||
onPrimaryContainer: const Color(0xFF00294A),
|
||
secondary: const Color(0xFF0F766E),
|
||
onSecondary: const Color(0xFFFFFFFF),
|
||
secondaryContainer: const Color(0xFFB8F1E8),
|
||
onSecondaryContainer: const Color(0xFF00201C),
|
||
tertiary: const Color(0xFF6D28D9),
|
||
onTertiary: const Color(0xFFFFFFFF),
|
||
tertiaryContainer: const Color(0xFFE9DDFF),
|
||
onTertiaryContainer: const Color(0xFF23005C),
|
||
error: const Color(0xFFDC2626),
|
||
onError: const Color(0xFFFFFFFF),
|
||
errorContainer: const Color(0xFFFBD9D6),
|
||
onErrorContainer: const Color(0xFF450A0A),
|
||
surface: const Color(0xFFF6F8FB),
|
||
onSurface: const Color(0xFF111820),
|
||
onSurfaceVariant: const Color(0xFF42505F),
|
||
surfaceContainerLowest: const Color(0xFFFFFFFF),
|
||
surfaceContainerLow: const Color(0xFFF0F4F9),
|
||
surfaceContainer: const Color(0xFFEDF1F6),
|
||
surfaceContainerHigh: const Color(0xFFE4EAF2),
|
||
surfaceContainerHighest: const Color(0xFFDCE4EE),
|
||
surfaceDim: const Color(0xFFD7DEE8),
|
||
surfaceBright: const Color(0xFFF6F8FB),
|
||
outline: const Color(0xFF73808F),
|
||
outlineVariant: const Color(0xFFC3CDDA),
|
||
surfaceTint: const Color(0xFF0B6BCB),
|
||
inverseSurface: const Color(0xFF111820),
|
||
onInverseSurface: const Color(0xFFEFF3F8),
|
||
inversePrimary: const Color(0xFFA6C8FF),
|
||
);
|
||
}
|
||
|
||
// ────────────────────────────────────────────────────────────
|
||
// LIGHT
|
||
// ────────────────────────────────────────────────────────────
|
||
static ThemeData light() {
|
||
return _apply(
|
||
ThemeData(colorScheme: _lightScheme(), useMaterial3: true),
|
||
AppStatusColors.light,
|
||
);
|
||
}
|
||
|
||
// ────────────────────────────────────────────────────────────
|
||
// DARK
|
||
// ────────────────────────────────────────────────────────────
|
||
static ThemeData dark() {
|
||
return _apply(
|
||
ThemeData(colorScheme: _darkScheme(), useMaterial3: true),
|
||
AppStatusColors.dark,
|
||
);
|
||
}
|
||
|
||
// ────────────────────────────────────────────────────────────
|
||
// SHARED BUILDER
|
||
// ────────────────────────────────────────────────────────────
|
||
static ThemeData _apply(ThemeData base, AppStatusColors status) {
|
||
final cs = base.colorScheme;
|
||
final isDark = cs.brightness == Brightness.dark;
|
||
|
||
final textTheme = GoogleFonts.spaceGroteskTextTheme(base.textTheme);
|
||
final monoTheme = GoogleFonts.robotoMonoTextTheme(base.textTheme);
|
||
final mono = AppMonoText(
|
||
label:
|
||
monoTheme.labelMedium?.copyWith(letterSpacing: 0.3) ??
|
||
const TextStyle(letterSpacing: 0.3),
|
||
body:
|
||
monoTheme.bodyMedium?.copyWith(letterSpacing: 0.2) ??
|
||
const TextStyle(letterSpacing: 0.2),
|
||
);
|
||
|
||
final surfaces = _surfaces;
|
||
|
||
// Card fill: flat tonal surface + hairline outline (cyber "panel" look).
|
||
final cardColor = isDark ? cs.surfaceContainer : cs.surfaceContainerLowest;
|
||
|
||
return base.copyWith(
|
||
textTheme: textTheme,
|
||
scaffoldBackgroundColor: isDark
|
||
? const Color(0xFF0B0F14)
|
||
: cs.surfaceContainerLowest,
|
||
extensions: [mono, surfaces, status],
|
||
|
||
// ── AppBar ──────────────────────────────────────────────
|
||
appBarTheme: AppBarTheme(
|
||
backgroundColor: cs.surface,
|
||
foregroundColor: cs.onSurface,
|
||
elevation: 0,
|
||
scrolledUnderElevation: 2,
|
||
surfaceTintColor: cs.surfaceTint,
|
||
centerTitle: false,
|
||
toolbarHeight: 64,
|
||
titleSpacing: 16.0,
|
||
iconTheme: IconThemeData(color: cs.onSurfaceVariant, size: 24),
|
||
actionsIconTheme: IconThemeData(color: cs.onSurfaceVariant, size: 24),
|
||
titleTextStyle: textTheme.titleLarge?.copyWith(
|
||
fontWeight: FontWeight.w700,
|
||
letterSpacing: 0.2,
|
||
),
|
||
),
|
||
|
||
// ── Cards — flat tonal fill + hairline outline (no hard shadow) ──
|
||
cardTheme: CardThemeData(
|
||
color: cardColor,
|
||
elevation: 0,
|
||
margin: EdgeInsets.zero,
|
||
shadowColor: Colors.transparent,
|
||
surfaceTintColor: Colors.transparent,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.cardRadius),
|
||
side: BorderSide(color: cs.outlineVariant),
|
||
),
|
||
),
|
||
|
||
// ── Chips — mono label, tight radius ────────────────────
|
||
chipTheme: ChipThemeData(
|
||
backgroundColor: cs.surfaceContainerHighest,
|
||
side: BorderSide(color: cs.outlineVariant),
|
||
labelStyle: mono.label,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.chipRadius),
|
||
),
|
||
),
|
||
|
||
// ── Dividers ────────────────────────────────────────────
|
||
dividerTheme: DividerThemeData(color: cs.outlineVariant, thickness: 1),
|
||
|
||
// ── Input Fields ────────────────────────────────────────
|
||
inputDecorationTheme: InputDecorationTheme(
|
||
filled: true,
|
||
fillColor: isDark ? cs.surfaceContainerLow : cs.surfaceContainer,
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.cardRadius),
|
||
borderSide: BorderSide(color: cs.outlineVariant),
|
||
),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.cardRadius),
|
||
borderSide: BorderSide(color: cs.outlineVariant),
|
||
),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.cardRadius),
|
||
borderSide: BorderSide(color: cs.primary, width: 2),
|
||
),
|
||
contentPadding: const EdgeInsets.symmetric(
|
||
horizontal: 16,
|
||
vertical: 14,
|
||
),
|
||
),
|
||
|
||
// ── Buttons — M3 hierarchy, tightened shapes ────────────
|
||
filledButtonTheme: FilledButtonThemeData(
|
||
style: FilledButton.styleFrom(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.cardRadius),
|
||
),
|
||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||
textStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600),
|
||
),
|
||
),
|
||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: cs.surfaceContainerHigh,
|
||
foregroundColor: cs.primary,
|
||
elevation: 0,
|
||
shadowColor: Colors.transparent,
|
||
surfaceTintColor: Colors.transparent,
|
||
side: BorderSide(color: cs.outlineVariant),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.cardRadius),
|
||
),
|
||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||
textStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600),
|
||
),
|
||
),
|
||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||
style: OutlinedButton.styleFrom(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.cardRadius),
|
||
),
|
||
side: BorderSide(color: cs.outline),
|
||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||
textStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600),
|
||
),
|
||
),
|
||
textButtonTheme: TextButtonThemeData(
|
||
style: TextButton.styleFrom(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.cardRadius),
|
||
),
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||
textStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600),
|
||
),
|
||
),
|
||
iconButtonTheme: IconButtonThemeData(
|
||
style: IconButton.styleFrom(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.compactCardRadius),
|
||
),
|
||
),
|
||
),
|
||
segmentedButtonTheme: SegmentedButtonThemeData(
|
||
style: ButtonStyle(
|
||
shape: WidgetStateProperty.all(
|
||
RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.cardRadius),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
|
||
// ── FAB ──────────────────────────────────────────────────
|
||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||
backgroundColor: cs.primaryContainer,
|
||
foregroundColor: cs.onPrimaryContainer,
|
||
elevation: 3,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.containerRadius),
|
||
),
|
||
),
|
||
|
||
// ── Navigation — pill indicators ────────────────────────
|
||
navigationDrawerTheme: NavigationDrawerThemeData(
|
||
backgroundColor: cs.surface,
|
||
indicatorColor: cs.secondaryContainer,
|
||
tileHeight: 56,
|
||
indicatorShape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.containerRadius),
|
||
),
|
||
),
|
||
navigationRailTheme: NavigationRailThemeData(
|
||
backgroundColor: cs.surface,
|
||
selectedIconTheme: IconThemeData(color: cs.onSecondaryContainer),
|
||
unselectedIconTheme: IconThemeData(color: cs.onSurfaceVariant),
|
||
selectedLabelTextStyle: textTheme.labelMedium?.copyWith(
|
||
fontWeight: FontWeight.w700,
|
||
color: cs.onSurface,
|
||
),
|
||
unselectedLabelTextStyle: textTheme.labelMedium?.copyWith(
|
||
color: cs.onSurfaceVariant,
|
||
),
|
||
indicatorColor: cs.secondaryContainer,
|
||
indicatorShape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.containerRadius),
|
||
),
|
||
),
|
||
navigationBarTheme: NavigationBarThemeData(
|
||
backgroundColor: cs.surfaceContainer,
|
||
indicatorColor: cs.secondaryContainer,
|
||
indicatorShape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.cardRadius),
|
||
),
|
||
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
||
final selected = states.contains(WidgetState.selected);
|
||
return textTheme.labelMedium?.copyWith(
|
||
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
||
color: selected ? cs.onSurface : cs.onSurfaceVariant,
|
||
);
|
||
}),
|
||
elevation: 2,
|
||
surfaceTintColor: cs.surfaceTint,
|
||
),
|
||
|
||
// ── List Tiles ──────────────────────────────────────────
|
||
listTileTheme: ListTileThemeData(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.cardRadius),
|
||
),
|
||
tileColor: Colors.transparent,
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||
),
|
||
|
||
// ── Dialogs ─────────────────────────────────────────────
|
||
dialogTheme: DialogThemeData(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.dialogRadius),
|
||
),
|
||
surfaceTintColor: Colors.transparent,
|
||
backgroundColor: isDark
|
||
? cs.surfaceContainerHigh
|
||
: cs.surfaceContainerLowest,
|
||
),
|
||
|
||
// ── Bottom Sheets ───────────────────────────────────────
|
||
bottomSheetTheme: BottomSheetThemeData(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(
|
||
top: Radius.circular(surfaces.dialogRadius),
|
||
),
|
||
),
|
||
backgroundColor: isDark
|
||
? cs.surfaceContainerHigh
|
||
: cs.surfaceContainerLowest,
|
||
surfaceTintColor: Colors.transparent,
|
||
showDragHandle: true,
|
||
),
|
||
|
||
// ── Snackbar ────────────────────────────────────────────
|
||
snackBarTheme: SnackBarThemeData(
|
||
behavior: SnackBarBehavior.floating,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.compactCardRadius),
|
||
),
|
||
),
|
||
|
||
// ── Search Bar ──────────────────────────────────────────
|
||
searchBarTheme: SearchBarThemeData(
|
||
elevation: WidgetStateProperty.all(0),
|
||
backgroundColor: WidgetStateProperty.all(cs.surfaceContainerHigh),
|
||
shape: WidgetStateProperty.all(
|
||
RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.containerRadius),
|
||
),
|
||
),
|
||
),
|
||
|
||
// ── Tooltips ────────────────────────────────────────────
|
||
tooltipTheme: TooltipThemeData(
|
||
decoration: BoxDecoration(
|
||
color: cs.inverseSurface,
|
||
borderRadius: BorderRadius.circular(surfaces.chipRadius),
|
||
),
|
||
textStyle: textTheme.bodySmall?.copyWith(color: cs.onInverseSurface),
|
||
),
|
||
|
||
// ── Tab Bar ─────────────────────────────────────────────
|
||
tabBarTheme: TabBarThemeData(
|
||
labelStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700),
|
||
unselectedLabelStyle: textTheme.labelLarge,
|
||
indicatorColor: cs.primary,
|
||
labelColor: cs.primary,
|
||
unselectedLabelColor: cs.onSurfaceVariant,
|
||
indicatorSize: TabBarIndicatorSize.label,
|
||
dividerColor: cs.outlineVariant,
|
||
),
|
||
|
||
// ── PopupMenu ───────────────────────────────────────────
|
||
popupMenuTheme: PopupMenuThemeData(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(surfaces.compactCardRadius),
|
||
),
|
||
surfaceTintColor: Colors.transparent,
|
||
color: isDark ? cs.surfaceContainerHigh : cs.surfaceContainerLowest,
|
||
),
|
||
);
|
||
}
|
||
}
|