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>
This commit is contained in:
2026-09-26 15:13:47 +08:00
parent 3cb980b629
commit 57377b1b21
28 changed files with 986 additions and 470 deletions
+187 -91
View File
@@ -3,57 +3,140 @@ import 'package:google_fonts/google_fonts.dart';
import 'app_typography.dart';
import 'app_surfaces.dart';
import 'app_status_colors.dart';
/// M3 Expressive theme for TasQ.
/// Refined enterprise **Cyber/IT** theme for TasQ, built on Material 3.
///
/// Key differences from the previous Hybrid M2/M3 theme:
/// * Cards use **tonal elevation** (surfaceTint color overlays) instead of
/// drop-shadows, giving surfaces an organic, seed-tinted look.
/// * Large containers use the M3 standard **28 dp** corner radius.
/// * Buttons follow the M3 hierarchy: FilledButton (primary), Tonal, Elevated,
/// Outlined, and Text.
/// * NavigationBar / NavigationRail use pill-shaped indicators with the
/// secondary-container tonal color.
/// * Spring-physics inspired durations: transitions default to 400 ms with an
/// emphasized easing curve.
/// 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 {
/// The seed color drives M3's entire tonal palette generation.
static const Color _seed = Color(0xFF4A6FA5);
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() {
final base = ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: _seed,
brightness: Brightness.light,
),
useMaterial3: true,
return _apply(
ThemeData(colorScheme: _lightScheme(), useMaterial3: true),
AppStatusColors.light,
);
return _apply(base);
}
// ────────────────────────────────────────────────────────────
// DARK
// ────────────────────────────────────────────────────────────
static ThemeData dark() {
final base = ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: _seed,
brightness: Brightness.dark,
),
useMaterial3: true,
return _apply(
ThemeData(colorScheme: _darkScheme(), useMaterial3: true),
AppStatusColors.dark,
);
return _apply(base);
}
// ────────────────────────────────────────────────────────────
// SHARED BUILDER
// ────────────────────────────────────────────────────────────
static ThemeData _apply(ThemeData base) {
static ThemeData _apply(ThemeData base, AppStatusColors status) {
final cs = base.colorScheme;
final isDark = cs.brightness == Brightness.dark;
@@ -68,18 +151,17 @@ class AppTheme {
const TextStyle(letterSpacing: 0.2),
);
const surfaces = AppSurfaces(
cardRadius: 16,
compactCardRadius: 12,
containerRadius: 28,
dialogRadius: 28,
chipRadius: 12,
);
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 ? cs.surface : cs.surfaceContainerLowest,
extensions: [mono, surfaces],
scaffoldBackgroundColor: isDark
? const Color(0xFF0B0F14)
: cs.surfaceContainerLowest,
extensions: [mono, surfaces, status],
// ── AppBar ──────────────────────────────────────────────
appBarTheme: AppBarTheme(
@@ -99,22 +181,27 @@ class AppTheme {
),
),
// ── Cards — M3 Elevated (tonal surface tint, no hard shadow) ──
// ── Cards — flat tonal fill + hairline outline (no hard shadow) ──
cardTheme: CardThemeData(
color: isDark ? cs.surfaceContainer : cs.surfaceContainerLow,
elevation: 1,
color: cardColor,
elevation: 0,
margin: EdgeInsets.zero,
shadowColor: Colors.transparent,
surfaceTintColor: cs.surfaceTint,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(surfaces.cardRadius),
side: BorderSide(color: cs.outlineVariant),
),
),
// ── Chips ───────────────────────────────────────────────
// ── Chips — mono label, tight radius ────────────────────
chipTheme: ChipThemeData(
backgroundColor: cs.surfaceContainerHighest,
side: BorderSide.none,
labelStyle: textTheme.labelSmall,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
side: BorderSide(color: cs.outlineVariant),
labelStyle: mono.label,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(surfaces.chipRadius),
),
),
// ── Dividers ────────────────────────────────────────────
@@ -123,17 +210,17 @@ class AppTheme {
// ── Input Fields ────────────────────────────────────────
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: cs.surfaceContainerLow,
fillColor: isDark ? cs.surfaceContainerLow : cs.surfaceContainer,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(surfaces.cardRadius),
borderSide: BorderSide(color: cs.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(surfaces.cardRadius),
borderSide: BorderSide(color: cs.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(surfaces.cardRadius),
borderSide: BorderSide(color: cs.primary, width: 2),
),
contentPadding: const EdgeInsets.symmetric(
@@ -142,87 +229,84 @@ class AppTheme {
),
),
// ── Buttons — M3 Expressive hierarchy ───────────────────
// ── Buttons — M3 hierarchy, tightened shapes ────────────
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(surfaces.cardRadius),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
textStyle: textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
),
textStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: cs.surfaceContainerLow,
backgroundColor: cs.surfaceContainerHigh,
foregroundColor: cs.primary,
elevation: 1,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: cs.surfaceTint,
surfaceTintColor: Colors.transparent,
side: BorderSide(color: cs.outlineVariant),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(surfaces.cardRadius),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
textStyle: textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
),
textStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
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,
),
textStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(surfaces.cardRadius),
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
textStyle: textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
),
textStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600),
),
),
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(surfaces.compactCardRadius),
),
),
),
segmentedButtonTheme: SegmentedButtonThemeData(
style: ButtonStyle(
shape: WidgetStateProperty.all(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(surfaces.cardRadius),
),
),
),
),
// ── FAB — M3 Expressive ──────────────────────────────────
// ── FAB ──────────────────────────────────────────────────
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: cs.primaryContainer,
foregroundColor: cs.onPrimaryContainer,
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(surfaces.containerRadius),
),
),
// ── Navigation — M3 Expressive pill indicators ──────────
// ── Navigation — pill indicators ────────────────────────
navigationDrawerTheme: NavigationDrawerThemeData(
backgroundColor: cs.surface,
indicatorColor: cs.secondaryContainer,
tileHeight: 56,
indicatorShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
borderRadius: BorderRadius.circular(surfaces.containerRadius),
),
),
navigationRailTheme: NavigationRailThemeData(
@@ -238,14 +322,14 @@ class AppTheme {
),
indicatorColor: cs.secondaryContainer,
indicatorShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
borderRadius: BorderRadius.circular(surfaces.containerRadius),
),
),
navigationBarTheme: NavigationBarThemeData(
backgroundColor: cs.surfaceContainer,
indicatorColor: cs.secondaryContainer,
indicatorShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(surfaces.cardRadius),
),
labelTextStyle: WidgetStateProperty.resolveWith((states) {
final selected = states.contains(WidgetState.selected);
@@ -260,15 +344,19 @@ class AppTheme {
// ── List Tiles ──────────────────────────────────────────
listTileTheme: ListTileThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
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(28)),
surfaceTintColor: cs.surfaceTint,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(surfaces.dialogRadius),
),
surfaceTintColor: Colors.transparent,
backgroundColor: isDark
? cs.surfaceContainerHigh
: cs.surfaceContainerLowest,
@@ -276,20 +364,24 @@ class AppTheme {
// ── Bottom Sheets ───────────────────────────────────────
bottomSheetTheme: BottomSheetThemeData(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(surfaces.dialogRadius),
),
),
backgroundColor: isDark
? cs.surfaceContainerHigh
: cs.surfaceContainerLowest,
surfaceTintColor: cs.surfaceTint,
surfaceTintColor: Colors.transparent,
showDragHandle: true,
),
// ── Snackbar ────────────────────────────────────────────
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(surfaces.compactCardRadius),
),
),
// ── Search Bar ──────────────────────────────────────────
@@ -297,7 +389,9 @@ class AppTheme {
elevation: WidgetStateProperty.all(0),
backgroundColor: WidgetStateProperty.all(cs.surfaceContainerHigh),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(surfaces.containerRadius),
),
),
),
@@ -305,7 +399,7 @@ class AppTheme {
tooltipTheme: TooltipThemeData(
decoration: BoxDecoration(
color: cs.inverseSurface,
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(surfaces.chipRadius),
),
textStyle: textTheme.bodySmall?.copyWith(color: cs.onInverseSurface),
),
@@ -323,8 +417,10 @@ class AppTheme {
// ── PopupMenu ───────────────────────────────────────────
popupMenuTheme: PopupMenuThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
surfaceTintColor: cs.surfaceTint,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(surfaces.compactCardRadius),
),
surfaceTintColor: Colors.transparent,
color: isDark ? cs.surfaceContainerHigh : cs.surfaceContainerLowest,
),
);