Initial commit
This commit is contained in:
30
lib/core/constants/api_constants.dart
Normal file
30
lib/core/constants/api_constants.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
class ApiConstants {
|
||||
static const String baseUrl = 'https://api.doormile.com/api/v1';
|
||||
|
||||
// ---- Auth ----
|
||||
static const String register = '$baseUrl/customer/register';
|
||||
static const String login = '$baseUrl/customer/login';
|
||||
static const String verifyPin = '$baseUrl/customer/verify-pin';
|
||||
static const String resetPin = '$baseUrl/customer/reset-pin';
|
||||
static const String logout = '$baseUrl/customer/logout';
|
||||
|
||||
// ---- Profile ----
|
||||
static const String profile = '$baseUrl/customer/profile';
|
||||
|
||||
// ---- Pickup Locations ----
|
||||
static const String locations = '$baseUrl/customer/locations';
|
||||
static String updateLocation(String id) => '$baseUrl/customer/locations/$id';
|
||||
static String deleteLocation(String id) => '$baseUrl/customer/locations/$id';
|
||||
|
||||
// ---- Pickups (Bookings) ----
|
||||
static const String createPickup = '$baseUrl/customer/bookings';
|
||||
static const String getPickups = '$baseUrl/customer/bookings';
|
||||
static String getPickupDetails(String bookingId) => '$baseUrl/customer/bookings/$bookingId';
|
||||
static String cancelPickup(String bookingId) => '$baseUrl/customer/bookings/$bookingId/cancel';
|
||||
|
||||
// ---- Pricing ----
|
||||
static const String checkPrice = '$baseUrl/pricing/check';
|
||||
|
||||
// ---- Tracking ----
|
||||
static String trackPickup(String trackingNo) => '$baseUrl/customer/track/$trackingNo';
|
||||
}
|
||||
56
lib/core/theme/app_colors.dart
Normal file
56
lib/core/theme/app_colors.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppColors {
|
||||
AppColors._();
|
||||
|
||||
// Backgrounds & Surfaces
|
||||
static const surface = Color(0xFFF7F4F2);
|
||||
static const surfaceDim = Color(0xFFE8DED8);
|
||||
static const surfaceContainerLowest = Color(0xFFFFFFFF);
|
||||
static const surfaceContainerLow = Color(0xFFFCFAF8);
|
||||
static const surfaceContainer = Color(0xFFF0E8E3);
|
||||
static const surfaceContainerHigh = Color(0xFFE6DAD3);
|
||||
static const surfaceContainerHighest = Color(0xFFD8C8BF);
|
||||
static const surfaceVariant = Color(0xFFEDE3DD);
|
||||
|
||||
// Text Colors
|
||||
static const onSurface = Color(0xFF271D1B);
|
||||
static const onSurfaceVariant = Color(0xFF756762);
|
||||
static const outline = Color(0xFFA9958D);
|
||||
static const outlineVariant = Color(0xFFD8C8BF);
|
||||
static const inverseSurface = Color(0xFF271D1B);
|
||||
|
||||
// Primary (brand red)
|
||||
static const primary = Color(0xFFC31723);
|
||||
static const primaryContainer = Color(0xFF7D1017);
|
||||
static const onPrimary = Color(0xFFFFFFFF);
|
||||
static const onPrimaryContainer = Color(0xFFFFEDEE);
|
||||
static const primaryFixed = Color(0xFFFFE9E7);
|
||||
static const primaryFixedDim = Color(0xFFFFCFCB);
|
||||
|
||||
// Secondary (blue)
|
||||
static const secondary = Color(0xFF126E82);
|
||||
static const secondaryContainer = Color(0xFFDDF4F4);
|
||||
static const secondaryFixed = Color(0xFFC6ECEC);
|
||||
static const secondaryFixedDim = Color(0xFF9FDADB);
|
||||
static const onSecondaryFixedVariant = Color(0xFF064A5A);
|
||||
|
||||
// Tertiary (eco green)
|
||||
static const tertiary = Color(0xFF16824E);
|
||||
static const tertiaryContainer = Color(0xFFE7F7ED);
|
||||
static const tertiaryFixed = Color(0xFFDCFCE7);
|
||||
static const tertiaryFixedDim = Color(0xFFBBF7D0);
|
||||
static const onTertiaryFixedVariant = Color(0xFF14532D);
|
||||
|
||||
// Status / Warning
|
||||
static const error = Color(0xFFDC2626);
|
||||
static const errorContainer = Color(0xFFFEE2E2);
|
||||
static const onErrorContainer = Color(0xFF7F1D1D);
|
||||
static const warning = Color(0xFFFF7A00); // Orange
|
||||
static const success = Color(0xFF16A34A); // Green
|
||||
|
||||
// Journey accents (brand narrative)
|
||||
static const firstMile = Color(0xFFEA580C); // orange — pickup
|
||||
static const midMile = Color(0xFF0E7C7B); // teal — transit
|
||||
static const lastMile = Color(0xFF1D4EDB); // blue — delivery
|
||||
}
|
||||
97
lib/core/theme/app_theme.dart
Normal file
97
lib/core/theme/app_theme.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:doormile/core/theme/app_colors.dart';
|
||||
|
||||
/// Central theme + reusable text styles based on the Doormile design system.
|
||||
class AppTheme {
|
||||
AppTheme._();
|
||||
|
||||
static const fontFamily = 'Public Sans';
|
||||
|
||||
static ThemeData get light {
|
||||
final base = ThemeData.light(useMaterial3: true);
|
||||
|
||||
return base.copyWith(
|
||||
scaffoldBackgroundColor: AppColors.surface,
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: AppColors.primary,
|
||||
onPrimary: AppColors.onPrimary,
|
||||
primaryContainer: AppColors.primaryContainer,
|
||||
onPrimaryContainer: AppColors.onPrimaryContainer,
|
||||
secondary: AppColors.secondary,
|
||||
onSecondary: Colors.white,
|
||||
tertiary: AppColors.tertiary,
|
||||
onTertiary: Colors.white,
|
||||
error: AppColors.error,
|
||||
surface: AppColors.surface,
|
||||
onSurface: AppColors.onSurface,
|
||||
surfaceContainerHighest: AppColors.surfaceContainerHighest,
|
||||
outline: AppColors.outline,
|
||||
outlineVariant: AppColors.outlineVariant,
|
||||
),
|
||||
textTheme: base.textTheme.apply(
|
||||
fontFamily: fontFamily,
|
||||
bodyColor: AppColors.onSurface,
|
||||
displayColor: AppColors.onSurface,
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
foregroundColor: AppColors.primary,
|
||||
centerTitle: false,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: AppColors.surfaceContainerLowest,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
|
||||
hintStyle: bodyMd.copyWith(color: AppColors.outline),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(color: AppColors.outlineVariant),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(color: AppColors.outlineVariant),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(color: AppColors.primary, width: 1.4),
|
||||
),
|
||||
),
|
||||
splashFactory: InkRipple.splashFactory,
|
||||
);
|
||||
}
|
||||
|
||||
static const _f = fontFamily;
|
||||
|
||||
/// Builds a style backed by the variable font's true `wght` axis so weights
|
||||
/// render at their real glyph widths (no synthetic-bold widening).
|
||||
static TextStyle _ts(double size, int wght, double lineHeight, [Color? color, double? letterSpacing]) {
|
||||
return TextStyle(
|
||||
fontFamily: _f,
|
||||
fontSize: size,
|
||||
height: lineHeight / size,
|
||||
letterSpacing: letterSpacing,
|
||||
color: color ?? AppColors.onSurface,
|
||||
fontWeight: FontWeight.values[(wght ~/ 100) - 1],
|
||||
fontVariations: [FontVariation('wght', wght.toDouble())],
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Typography helpers (mirror DESIGN.md scale) ----
|
||||
static final TextStyle headlineLg = _ts(22, 800, 28, null, -0.5);
|
||||
static final TextStyle headlineMd = _ts(17, 700, 24, null, -0.3);
|
||||
static final TextStyle headlineSm = _ts(15, 700, 22, null, -0.2);
|
||||
static final TextStyle bodyLg = _ts(15, 400, 22);
|
||||
static final TextStyle bodyMd = _ts(13, 400, 20);
|
||||
static final TextStyle labelMd = _ts(13, 700, 20);
|
||||
static final TextStyle caption = _ts(11, 500, 16, AppColors.onSurfaceVariant);
|
||||
static final TextStyle button = _ts(14, 700, 20, null, 0.2);
|
||||
|
||||
/// Ambient card shadow (Level 1 elevation from the design brief).
|
||||
static List<BoxShadow> get cardShadow => const [
|
||||
BoxShadow(color: Color(0x12000000), blurRadius: 28, offset: Offset(0, 12)),
|
||||
BoxShadow(color: Color(0x08C31723), blurRadius: 8, offset: Offset(0, 2)),
|
||||
];
|
||||
}
|
||||
434
lib/core/widgets/common.dart
Normal file
434
lib/core/widgets/common.dart
Normal file
@@ -0,0 +1,434 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:doormile/core/theme/app_colors.dart';
|
||||
import 'package:doormile/core/theme/app_theme.dart';
|
||||
|
||||
/// White rounded card with the design-system ambient shadow.
|
||||
class AppCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final VoidCallback? onTap;
|
||||
final Color color;
|
||||
final BorderRadius? radius;
|
||||
final Border? border;
|
||||
|
||||
const AppCard({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.padding = const EdgeInsets.all(16),
|
||||
this.onTap,
|
||||
this.color = AppColors.surfaceContainerLowest,
|
||||
this.radius,
|
||||
this.border,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final br = radius ?? BorderRadius.circular(18);
|
||||
return Material(
|
||||
color: color,
|
||||
borderRadius: br,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: br,
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: br,
|
||||
border: border ?? Border.all(color: AppColors.surfaceContainer.withValues(alpha: 0.7)),
|
||||
boxShadow: AppTheme.cardShadow,
|
||||
),
|
||||
child: Padding(padding: padding, child: child),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Primary filled action button (min height 52, 12px radius).
|
||||
class PrimaryButton extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
final IconData? trailingIcon;
|
||||
final bool loading;
|
||||
final Color color;
|
||||
final double height;
|
||||
|
||||
const PrimaryButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.trailingIcon,
|
||||
this.loading = false,
|
||||
this.color = AppColors.primary,
|
||||
this.height = 52,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: height,
|
||||
child: ElevatedButton(
|
||||
onPressed: loading ? null : onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: color,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: color.withValues(alpha: 0.6),
|
||||
disabledForegroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shadowColor: AppColors.primary.withValues(alpha: 0.2),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
child: loading
|
||||
? const SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: CircularProgressIndicator(strokeWidth: 2.4, color: Colors.white),
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(label,
|
||||
style: AppTheme.button,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1),
|
||||
),
|
||||
if (trailingIcon != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Icon(trailingIcon, size: 20),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Secondary tinted button (pink bg, red text).
|
||||
class SecondaryButton extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
const SecondaryButton({super.key, required this.label, this.onPressed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 44,
|
||||
child: OutlinedButton(
|
||||
onPressed: onPressed,
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: AppColors.surfaceContainerLowest,
|
||||
foregroundColor: AppColors.primary,
|
||||
side: const BorderSide(color: AppColors.outlineVariant),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
),
|
||||
child: Text(label, style: AppTheme.labelMd.copyWith(color: AppColors.primary)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pill-shaped status / category chip.
|
||||
class StatusChip extends StatelessWidget {
|
||||
final String label;
|
||||
final Color color;
|
||||
final Color bg;
|
||||
final IconData? icon;
|
||||
const StatusChip({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.color,
|
||||
required this.bg,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(color: color.withValues(alpha: 0.12)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[Icon(icon, size: 14, color: color), const SizedBox(width: 4)],
|
||||
Text(label,
|
||||
style: AppTheme.caption.copyWith(color: color, fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A horizontal stepper progress bar with a fraction filled.
|
||||
class JourneyProgressBar extends StatelessWidget {
|
||||
final double value; // 0..1
|
||||
final bool gradient;
|
||||
const JourneyProgressBar({super.key, required this.value, this.gradient = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(height: 6, color: AppColors.surfaceContainer),
|
||||
FractionallySizedBox(
|
||||
widthFactor: value.clamp(0, 1),
|
||||
child: Container(
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: gradient ? null : AppColors.primary,
|
||||
gradient: gradient
|
||||
? const LinearGradient(colors: [
|
||||
AppColors.firstMile,
|
||||
AppColors.midMile,
|
||||
AppColors.lastMile,
|
||||
])
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Eco / EV reassurance banner used across screens.
|
||||
class EcoBanner extends StatelessWidget {
|
||||
final String text;
|
||||
const EcoBanner({super.key, required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.tertiaryFixed.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.tertiary.withValues(alpha: 0.1)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.eco, color: AppColors.tertiary, size: 22),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(text,
|
||||
style: AppTheme.bodyMd.copyWith(color: AppColors.tertiary, height: 1.35)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Warm page backdrop with restrained route/map texture.
|
||||
class AppBackground extends StatelessWidget {
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry padding;
|
||||
const AppBackground({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.padding = EdgeInsets.zero,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFFFFFBF8), AppColors.surface],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
const Positioned(
|
||||
top: 240,
|
||||
left: -70,
|
||||
child: _SoftCircle(size: 180, color: Color(0x1A126E82)),
|
||||
),
|
||||
Padding(padding: padding, child: child),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SoftCircle extends StatelessWidget {
|
||||
final double size;
|
||||
final Color color;
|
||||
const _SoftCircle({required this.size, required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reusable immersive red header used by primary tabs.
|
||||
class ImmersiveHeader extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
final Widget? trailing;
|
||||
final bool showBack;
|
||||
|
||||
const ImmersiveHeader({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
this.trailing,
|
||||
this.showBack = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColors.primaryContainer, AppColors.primary],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(24)),
|
||||
child: AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.light,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
right: -20,
|
||||
bottom: -30,
|
||||
child: Icon(icon, size: 140, color: Colors.white.withValues(alpha: 0.04)),
|
||||
),
|
||||
Positioned(
|
||||
right: 100,
|
||||
top: -10,
|
||||
child: Transform.rotate(
|
||||
angle: 0.2,
|
||||
child: Icon(Icons.auto_awesome_rounded, size: 60, color: Colors.white.withValues(alpha: 0.03)),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
if (showBack) ...[
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
padding: EdgeInsets.zero,
|
||||
alignment: Alignment.centerLeft,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppTheme.headlineLg.copyWith(color: Colors.white)),
|
||||
const SizedBox(height: 4),
|
||||
Text(subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppTheme.bodyMd.copyWith(color: Colors.white.withValues(alpha: 0.8))),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trailing != null) trailing!,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A small circular icon avatar with a tinted background.
|
||||
class IconBadge extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color bg;
|
||||
final Color color;
|
||||
final double size;
|
||||
final double iconSize;
|
||||
final bool circle;
|
||||
const IconBadge({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.bg,
|
||||
required this.color,
|
||||
this.size = 48,
|
||||
this.iconSize = 22,
|
||||
this.circle = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(circle ? size : 12),
|
||||
),
|
||||
child: Icon(icon, color: color, size: iconSize),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Title row with an optional trailing action ("View All").
|
||||
class SectionHeader extends StatelessWidget {
|
||||
final String title;
|
||||
final String? action;
|
||||
final VoidCallback? onAction;
|
||||
const SectionHeader({super.key, required this.title, this.action, this.onAction});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(title,
|
||||
style: AppTheme.headlineSm, overflow: TextOverflow.ellipsis, maxLines: 1),
|
||||
),
|
||||
if (action != null)
|
||||
GestureDetector(
|
||||
onTap: onAction,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Text(action!,
|
||||
style: AppTheme.labelMd.copyWith(color: AppColors.primary)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
69
lib/core/widgets/logo.dart
Normal file
69
lib/core/widgets/logo.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:doormile/core/theme/app_colors.dart';
|
||||
import 'package:doormile/core/theme/app_theme.dart';
|
||||
|
||||
/// Stylised Doormile badge mark — a rounded pin holding a package glyph.
|
||||
class DoormileMark extends StatelessWidget {
|
||||
final double size;
|
||||
final Color color;
|
||||
final Color glyphColor;
|
||||
const DoormileMark({
|
||||
super.key,
|
||||
this.size = 48,
|
||||
this.color = AppColors.primary,
|
||||
this.glyphColor = Colors.white,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(size * 0.28),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withValues(alpha: 0.25),
|
||||
blurRadius: size * 0.3,
|
||||
offset: Offset(0, size * 0.12),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(Icons.local_shipping_rounded, color: glyphColor, size: size * 0.55),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Full wordmark: badge + "Doormile" text.
|
||||
class DoormileWordmark extends StatelessWidget {
|
||||
final double markSize;
|
||||
final double fontSize;
|
||||
final Color textColor;
|
||||
final Color markColor;
|
||||
final Color glyphColor;
|
||||
const DoormileWordmark({
|
||||
super.key,
|
||||
this.markSize = 40,
|
||||
this.fontSize = 24,
|
||||
this.textColor = AppColors.primary,
|
||||
this.markColor = AppColors.primary,
|
||||
this.glyphColor = Colors.white,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DoormileMark(size: markSize, color: markColor, glyphColor: glyphColor),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'Doormile',
|
||||
style: AppTheme.headlineLg.copyWith(color: textColor, fontSize: fontSize),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user