Files
tasq/lib/theme/app_theme.dart
T
redz1029 57377b1b21 UI: Cyber/IT design overhaul with semantic status tokens (Material 3)
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>
2026-09-26 15:13:47 +08:00

429 lines
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
),
);
}
}