You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

256 lines
8.8 KiB
Dart

import 'package:flutter/material.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// "Futuristic Hospital — Dr. Sulaiman Al Habib (HMG)" design tokens.
///
/// LIGHT MODE variant: clean white clinical surfaces with HMG red (#D02127)
/// as the single brand accent — used for primary actions, focus states,
/// active scanner glow, and status highlights. Panels are white cards with
/// soft red-tinted shadows/borders instead of the previous dark glass look,
/// keeping a crisp, futuristic-kiosk feel while staying bright and readable.
///
/// Drop this file in lib/theme/app_theme.dart and wrap MaterialApp with
/// `theme: AppTheme.light()`.
/// ─────────────────────────────────────────────────────────────────────────
class AppTheme {
AppTheme._();
// Core brand colors — HMG red & white
static const Color red = Color(0xFFD02127); // HMG primary brand red
static const Color redDark = Color(0xFFA8191F); // pressed / deep variant
static const Color redTint = Color(0xFFFCE9EA); // faint red surface tint
static const Color background = Color(0xFFFFFFFF); // page background
static const Color surface = Color(0xFFFFFFFF); // card surfaces
static const Color surfaceMuted = Color(0xFFF7F4F4); // sunken fields, chips
static const Color saudiGreen = Color(0xFF1E9E6B); // success / verified (semantic only)
static const Color amberAlert = Color(0xFFC8821A); // pending / connecting
static const Color redAlert = Color(0xFFD02127); // error reuses brand red
static const Color textPrimary = Color(0xFF1A1213);
static const Color textMuted = Color(0xFF7A6C6D);
static const Color hairline = Color(0xFFE7DCDD);
// Backward-compatible aliases — anywhere older screen code references
// AppTheme.cyan / AppTheme.gold / AppTheme.ink*, it now resolves to the
// light-mode equivalents so existing screens compile unchanged.
static const Color cyan = red;
static const Color gold = red;
static const Color ink = background;
static const Color inkRaised = surface;
static const Color inkRaised2 = surfaceMuted;
static ThemeData light() {
final scheme = ColorScheme.fromSeed(
seedColor: red,
brightness: Brightness.light,
primary: red,
secondary: redDark,
error: redAlert,
surface: surface,
).copyWith(
surfaceContainerLowest: background,
surfaceContainerLow: surface,
surfaceContainer: surfaceMuted,
surfaceContainerHigh: surfaceMuted,
outline: hairline,
outlineVariant: hairline,
onSurface: textPrimary,
onSurfaceVariant: textMuted,
onPrimary: Colors.white,
);
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme: scheme,
scaffoldBackgroundColor: background,
fontFamily: 'Roboto',
textTheme: const TextTheme().apply(
bodyColor: textPrimary,
displayColor: textPrimary,
),
appBarTheme: const AppBarTheme(
backgroundColor: background,
surfaceTintColor: Colors.transparent,
elevation: 0,
foregroundColor: textPrimary,
titleTextStyle: TextStyle(
color: textPrimary,
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
),
),
cardTheme: CardThemeData(
color: surface,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: const BorderSide(color: hairline),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: surfaceMuted,
labelStyle: const TextStyle(color: textMuted),
hintStyle: const TextStyle(color: textMuted),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: hairline),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: hairline),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: red, width: 1.4),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: redAlert),
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: red,
foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(54),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
textStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: surfaceMuted,
foregroundColor: textPrimary,
elevation: 0,
minimumSize: const Size.fromHeight(54),
side: const BorderSide(color: hairline),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
textStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15),
),
),
dividerTheme: const DividerThemeData(color: hairline, thickness: 1),
);
}
}
/// A reusable card-style panel with a soft red-tinted shadow/border — the
/// signature surface treatment of this light theme. Replaces the old dark
/// "glow panel"; same API so existing screens compile unchanged.
class GlowPanel extends StatelessWidget {
final Widget child;
final Color glowColor;
final EdgeInsetsGeometry padding;
final double borderRadius;
const GlowPanel({
super.key,
required this.child,
this.glowColor = AppTheme.red,
this.padding = const EdgeInsets.all(20),
this.borderRadius = 20,
});
@override
Widget build(BuildContext context) {
return Container(
padding: padding,
decoration: BoxDecoration(
color: AppTheme.surface,
borderRadius: BorderRadius.circular(borderRadius),
border: Border.all(color: glowColor.withValues(alpha: 0.22)),
boxShadow: [
BoxShadow(
color: glowColor.withValues(alpha: 0.10),
blurRadius: 20,
offset: const Offset(0, 6),
),
const BoxShadow(
color: Color(0x0A1A1213),
blurRadius: 12,
offset: Offset(0, 2),
),
],
),
child: child,
);
}
}
/// Subtle hex/grid backdrop to reinforce the "med-tech kiosk" feel without
/// hurting readability. Cheap to render — pure CustomPaint, no images.
/// In light mode this is a very faint red grid on white.
class TechGridBackground extends StatelessWidget {
final Widget child;
const TechGridBackground({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: RadialGradient(
center: const Alignment(0.9, -0.9),
radius: 1.4,
colors: [
AppTheme.red.withValues(alpha: 0.05),
AppTheme.background,
],
),
),
),
),
Positioned.fill(child: CustomPaint(painter: _GridPainter())),
child,
],
);
}
}
class _GridPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = AppTheme.red.withValues(alpha: 0.025)
..strokeWidth = 1;
const step = 36.0;
for (double x = 0; x < size.width; x += step) {
canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint);
}
for (double y = 0; y < size.height; y += step) {
canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
/// Centers content and clamps width on tablets/desktop so forms don't
/// stretch edge-to-edge on large screens.
class ResponsiveCenter extends StatelessWidget {
final Widget child;
final double maxWidth;
const ResponsiveCenter({super.key, required this.child, this.maxWidth = 760});
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: child,
),
);
}
}
/// Breakpoint helper.
bool isTablet(BuildContext context) =>
MediaQuery.sizeOf(context).shortestSide >= 600;