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
+4 -3
View File
@@ -9,6 +9,7 @@ import '../providers/attendance_provider.dart';
import '../providers/profile_provider.dart';
import '../services/face_verification.dart' as face;
import '../services/image_compress_service.dart';
import '../theme/app_status_colors.dart';
import '../theme/m3_motion.dart';
import '../widgets/qr_verification_dialog.dart';
@@ -302,7 +303,7 @@ class _FaceVerificationOverlayState
textAlign: TextAlign.center,
style: textTheme.titleMedium?.copyWith(
color: _phase == _Phase.success
? Colors.green
? AppStatusColors.of(context).success.color
: _phase == _Phase.failed
? colors.error
: _phase == _Phase.saving
@@ -337,7 +338,7 @@ class _FaceVerificationOverlayState
final Color ringColor;
if (isSuccess) {
ringColor = Colors.green;
ringColor = AppStatusColors.of(context).success.color;
} else if (isFailed) {
ringColor = colors.error;
} else if (isSaving) {
@@ -461,7 +462,7 @@ class _FaceVerificationOverlayState
Color dotColor;
if (isSuccess_) {
dotColor = Colors.green;
dotColor = AppStatusColors.of(context).success.color;
} else if (isDone) {
dotColor = colors.error.withValues(alpha: 0.5);
} else if (isCurrent) {
+52 -43
View File
@@ -1,26 +1,27 @@
import 'package:flutter/material.dart';
/// M3 Expressive Card variants.
import '../theme/app_surfaces.dart';
enum _M3CardVariant { elevated, filled, outlined }
/// Cyber/IT Card variants (Material 3).
///
/// Use these factory constructors to build semantically correct cards:
/// - [M3Card.elevated] — default, uses tonal surface tint (no hard shadow).
/// - [M3Card.filled] — uses surfaceContainerHighest for emphasis.
/// - [M3Card.outlined] — transparent fill with a subtle outline border.
/// - [M3Card.elevated] — default panel: tonal fill + hairline outline (flat).
/// - [M3Card.filled] — high-emphasis fill (caller passes [color]); no border.
/// - [M3Card.outlined] — transparent fill with a hairline outline border.
class M3Card extends StatelessWidget {
const M3Card._({
const M3Card._(
this._variant, {
required this.child,
this.color,
this.elevation,
this.shadowColor,
this.surfaceTintColor,
this.shape,
this.margin,
this.clipBehavior = Clip.none,
this.onTap,
});
/// Elevated card — tonal surface tint, minimal shadow.
/// Elevated card — default panel with a tonal fill + hairline outline.
/// Best for primary content surfaces (metric cards, detail panels).
factory M3Card.elevated({
required Widget child,
@@ -31,9 +32,8 @@ class M3Card extends StatelessWidget {
VoidCallback? onTap,
}) {
return M3Card._(
_M3CardVariant.elevated,
color: color,
elevation: 1,
shadowColor: Colors.transparent,
shape: shape,
margin: margin,
clipBehavior: clipBehavior,
@@ -42,8 +42,8 @@ class M3Card extends StatelessWidget {
);
}
/// Filled card — uses surfaceContainerHighest for high emphasis.
/// Best for summary cards, status counts, callout panels.
/// Filled card — a high-emphasis fill (caller passes a semantic [color] or a
/// surfaceContainer tone). No border. Best for summary/callout panels.
factory M3Card.filled({
required Widget child,
Color? color,
@@ -53,10 +53,8 @@ class M3Card extends StatelessWidget {
VoidCallback? onTap,
}) {
return M3Card._(
color: color, // caller passes surfaceContainerHighest or semantic color
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
_M3CardVariant.filled,
color: color,
shape: shape,
margin: margin,
clipBehavior: clipBehavior,
@@ -65,7 +63,7 @@ class M3Card extends StatelessWidget {
);
}
/// Outlined card — transparent fill with outline border.
/// Outlined card — transparent fill with a hairline outline border.
/// Best for list items, form sections, grouped content.
factory M3Card.outlined({
required Widget child,
@@ -76,10 +74,8 @@ class M3Card extends StatelessWidget {
VoidCallback? onTap,
}) {
return M3Card._(
_M3CardVariant.outlined,
color: color,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
shape: shape,
margin: margin,
clipBehavior: clipBehavior,
@@ -88,11 +84,9 @@ class M3Card extends StatelessWidget {
);
}
final _M3CardVariant _variant;
final Widget child;
final Color? color;
final double? elevation;
final Color? shadowColor;
final Color? surfaceTintColor;
final ShapeBorder? shape;
final EdgeInsetsGeometry? margin;
final Clip clipBehavior;
@@ -101,36 +95,51 @@ class M3Card extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final radius = AppSurfaces.of(context).cardRadius;
final isDark = cs.brightness == Brightness.dark;
final Color resolvedColor;
final bool bordered;
switch (_variant) {
case _M3CardVariant.elevated:
resolvedColor =
color ??
(isDark ? cs.surfaceContainer : cs.surfaceContainerLowest);
bordered = true;
case _M3CardVariant.filled:
resolvedColor = color ?? cs.surfaceContainerHighest;
bordered = false;
case _M3CardVariant.outlined:
resolvedColor = color ?? Colors.transparent;
bordered = true;
}
// For outlined, we need the border side
final resolvedShape =
shape ??
(elevation == 0 && surfaceTintColor == Colors.transparent
? RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: (shadowColor == Colors.transparent && color == null)
? BorderSide(color: cs.outlineVariant)
: BorderSide.none,
)
: null);
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radius),
side: bordered
? BorderSide(color: cs.outlineVariant)
: BorderSide.none,
);
final card = Card(
color: color,
elevation: elevation,
shadowColor: shadowColor,
surfaceTintColor: surfaceTintColor,
return Card(
color: resolvedColor,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
shape: resolvedShape,
margin: margin ?? EdgeInsets.zero,
clipBehavior: clipBehavior,
clipBehavior: onTap != null && clipBehavior == Clip.none
? Clip.antiAlias
: clipBehavior,
child: onTap != null
? InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(radius),
child: child,
)
: child,
);
return card;
}
}
+41 -13
View File
@@ -1,32 +1,60 @@
import 'package:flutter/material.dart';
/// M3 Expressive status pill — uses tonal container colors with a
/// smooth, spring-physics-inspired animation.
import '../theme/app_status_colors.dart';
import '../theme/app_surfaces.dart';
/// Cyber/IT status pill — a compact, uppercase, mono-friendly badge whose color
/// carries meaning via [AppStatusColors].
///
/// Two constructors:
/// * [StatusPill.kind] (preferred) — pass a semantic [StatusKind] and it pulls
/// the correct tonal container/foreground from the theme, consistent in both
/// light and dark.
/// * The default constructor keeps the older `isEmphasized` behaviour for
/// back-compat (primaryContainer vs. tertiaryContainer).
class StatusPill extends StatelessWidget {
const StatusPill({super.key, required this.label, this.isEmphasized = false});
const StatusPill({super.key, required this.label, this.isEmphasized = false})
: kind = null;
/// Semantic variant — the recommended way to build a status pill.
const StatusPill.kind(
this.kind, {
super.key,
required this.label,
}) : isEmphasized = false;
final String label;
final bool isEmphasized;
final StatusKind? kind;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
// isEmphasized uses primaryContainer (higher tonal weight) to visually
// distinguish high-priority statuses from routine ones.
final background = isEmphasized
? scheme.primaryContainer
: scheme.tertiaryContainer;
final foreground = isEmphasized
? scheme.onPrimaryContainer
: scheme.onTertiaryContainer;
final radius = AppSurfaces.of(context).chipRadius;
late final Color background;
late final Color foreground;
if (kind != null) {
final swatch = AppStatusColors.of(context).byKind(kind!);
background = swatch.container;
foreground = swatch.onContainer;
} else {
// Legacy path: emphasis maps to primary vs. tertiary containers.
background = isEmphasized
? scheme.primaryContainer
: scheme.tertiaryContainer;
foreground = isEmphasized
? scheme.onPrimaryContainer
: scheme.onTertiaryContainer;
}
return AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeOutCubic,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(28),
borderRadius: BorderRadius.circular(radius),
),
child: Text(
label.toUpperCase(),
+5 -3
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/connectivity_provider.dart';
import '../theme/app_status_colors.dart';
/// Two-state "this item is not yet synced" indicator.
///
@@ -38,6 +39,7 @@ class SyncPendingBadge extends ConsumerWidget {
final isOnline = ref.watch(isOnlineProvider);
final scheme = Theme.of(context).colorScheme;
final status = AppStatusColors.of(context);
final Color bg;
final Color fg;
@@ -53,9 +55,9 @@ class SyncPendingBadge extends ConsumerWidget {
text = label ?? 'Syncing…';
spin = true;
} else {
// Queued offline: amber, static.
bg = Colors.amber.shade100;
fg = Colors.amber.shade900;
// Queued offline: warning tone, static.
bg = status.warning.container;
fg = status.warning.onContainer;
icon = Icons.sync;
text = label ?? 'Pending sync';
spin = false;
+92
View File
@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import '../theme/app_status_colors.dart';
import '../theme/app_surfaces.dart';
import '../theme/app_typography.dart';
/// A compact, monospace "code" chip for technical identifiers — ticket/task
/// numbers, IP/MAC addresses, ports, VLAN ids, device codes, short timestamps.
///
/// Reinforces the Cyber/IT identity by rendering machine data in Roboto Mono on
/// a subtle tonal surface with a hairline outline. Optionally tint it with a
/// semantic [StatusKind] for at-a-glance meaning.
class TechChip extends StatelessWidget {
const TechChip(
this.text, {
super.key,
this.icon,
this.status,
this.onTap,
this.dense = false,
});
final String text;
/// Optional leading glyph (e.g. `Icons.tag`, `Icons.lan_outlined`).
final IconData? icon;
/// When set, tints the chip with the matching [AppStatusColors] swatch.
final StatusKind? status;
final VoidCallback? onTap;
/// Tighter padding + smaller glyph for inline use within dense rows.
final bool dense;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final radius = AppSurfaces.of(context).chipRadius;
final swatch = status == null
? null
: AppStatusColors.of(context).byKind(status!);
final background = swatch?.container ?? cs.surfaceContainerHighest;
final foreground = swatch?.onContainer ?? cs.onSurfaceVariant;
final border = swatch == null ? cs.outlineVariant : Colors.transparent;
final labelStyle = AppMonoText.of(context).label.copyWith(
color: foreground,
fontWeight: FontWeight.w600,
height: 1.0,
);
final content = Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: dense ? 12 : 14, color: foreground),
SizedBox(width: dense ? 4 : 6),
],
Flexible(
child: Text(
text,
style: labelStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
);
final chip = Container(
padding: EdgeInsets.symmetric(
horizontal: dense ? 6 : 8,
vertical: dense ? 2 : 4,
),
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(radius),
border: Border.all(color: border),
),
child: content,
);
if (onTap == null) return chip;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(radius),
child: chip,
);
}
}