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:
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user