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
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; |