production
This commit is contained in:
@@ -1,55 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:miler/views/onboardscreens/Sign_in.dart';
|
||||
import 'package:miler/views/onboardscreens/auth_scaffold.dart';
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:miler/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:miler/views/helpers/widgets/miler_app_bar.dart';
|
||||
import 'package:miler/views/helpers/constants/design_constants.dart';
|
||||
|
||||
/// ═════════════════════════════════════════════════════════════════════════
|
||||
/// FIRST RUN — three slides, then sign in.
|
||||
/// FIRST RUN — three full-bleed brand slides, then sign in.
|
||||
///
|
||||
/// ── The copy described a different app ──
|
||||
/// ── The artwork is the slide ──
|
||||
///
|
||||
/// This is the most important change here, and it is not a visual one. The old
|
||||
/// slides said "Find pickups near you — see nearby pickup requests the moment
|
||||
/// they arrive, accept in a single tap" and "Navigate the smart way". That is a
|
||||
/// gig-marketplace pitch, and Miler is not a marketplace: the **hub assigns a
|
||||
/// slot**, the route comes with its stops and its order already set, it is a
|
||||
/// **round trip** that starts and ends at the hub, and a stop that is not ready
|
||||
/// gets **skipped, never reordered**. A rider onboarded on the old copy would
|
||||
/// open the app looking for a feed of nearby jobs to claim and not find one.
|
||||
/// Each slide is a finished poster: headline, sub-copy, product shot and the
|
||||
/// MILER by DOORMILE lockup are all baked into the image. So this screen draws
|
||||
/// no title or body text of its own — a Flutter caption under the picture would
|
||||
/// simply say the same thing twice, in a second typeface, at a second size.
|
||||
/// The screen's whole job is to present the image edge to edge and put the
|
||||
/// controls somewhere they stay readable.
|
||||
///
|
||||
/// The third slide also carried a "Powered by AI" chip next to a claim about
|
||||
/// real-time bonuses. Neither was backed by anything in the app, and a promise
|
||||
/// made during onboarding is the one a rider remembers when it does not hold.
|
||||
/// ── Controls that survive three different backdrops ──
|
||||
///
|
||||
/// ── The illustrations show the actual product ──
|
||||
///
|
||||
/// Each slide used to draw a 320×320 rounded panel, containing a 156px tinted
|
||||
/// circle, containing an 84px Material icon — three nested shapes to present one
|
||||
/// glyph, on a `repeat(reverse: true)` pulse that never stopped. A location pin
|
||||
/// does not teach anyone what a slot route is.
|
||||
///
|
||||
/// So the panels now hold small, honest mock-ups built from the app's own
|
||||
/// vocabulary — the same slot chip, stop rows and hub markers the rider will see
|
||||
/// on Home an hour later. Onboarding that previews the real interface is doing
|
||||
/// two jobs at once: it explains the model *and* it makes the real screen
|
||||
/// familiar on arrival.
|
||||
/// Slides 1 and 3 end in the brand's red wave; slide 2 ends on dark tarmac.
|
||||
/// [ColorConstants.primary] would vanish into the first and get lost in the
|
||||
/// second, so the CTA inverts: a white pill carrying the red label, which holds
|
||||
/// its contrast on red, on grey and on black. A short scrim under the controls
|
||||
/// does the rest without dulling the wave. `Skip` sits top-right, where all
|
||||
/// three images are near-white, so it stays dark type on a light plate.
|
||||
/// ═════════════════════════════════════════════════════════════════════════
|
||||
class _Slide {
|
||||
final String title;
|
||||
final String body;
|
||||
final Widget art;
|
||||
const _Slide({required this.title, required this.body, required this.art});
|
||||
}
|
||||
|
||||
class Introscreen extends StatefulWidget {
|
||||
const Introscreen({super.key});
|
||||
|
||||
@@ -60,32 +41,27 @@ class Introscreen extends StatefulWidget {
|
||||
class _IntroscreenState extends State<Introscreen> {
|
||||
final PageController _controller = PageController();
|
||||
int _index = 0;
|
||||
bool _precached = false;
|
||||
|
||||
static final _slides = <_Slide>[
|
||||
_Slide(
|
||||
title: 'Your route arrives ready',
|
||||
body:
|
||||
'Your hub assigns each shift as a time slot, with the stops and '
|
||||
'their order already set. Take the whole trip, or accept it stop by '
|
||||
'stop.',
|
||||
art: _SlotArt(),
|
||||
),
|
||||
_Slide(
|
||||
title: 'Work the stops in order',
|
||||
body:
|
||||
'Follow the list as it stands. If a stop is not ready, skip it and '
|
||||
'keep moving — the order is never rearranged underneath you.',
|
||||
art: _StopsArt(),
|
||||
),
|
||||
_Slide(
|
||||
title: 'Collect, then close the loop',
|
||||
body:
|
||||
'Take the parcels and any cash due, capture the proof, and finish '
|
||||
'back at the hub. Every stop is logged as you go.',
|
||||
art: _HubArt(),
|
||||
),
|
||||
/// Order is the order the rider sees: trust → the work → the pay.
|
||||
static const _slides = <String>[
|
||||
'assets/images/onboard_1.png',
|
||||
'assets/images/onboard_2.png',
|
||||
'assets/images/onboard_3.png',
|
||||
];
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
// Decode all three up front. Paging onto an undecoded 940×1672 PNG shows a
|
||||
// blank frame first, which on the very first run reads as a broken app.
|
||||
if (_precached) return;
|
||||
_precached = true;
|
||||
for (final path in _slides) {
|
||||
precacheImage(AssetImage(path), context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _finish() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -120,520 +96,194 @@ class _IntroscreenState extends State<Introscreen> {
|
||||
value: SystemUiOverlayStyle.dark.copyWith(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
systemNavigationBarColor: Colors.black,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: ColorConstants.surface,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// ── Top bar: mark, and a way out ──
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(20.w, 8.h, 12.w, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
const MilerBrandMark(),
|
||||
const Spacer(),
|
||||
// Skip stays mounted on the last slide instead of fading to
|
||||
// `opacity: 0` — an invisible-but-present control is a trap
|
||||
// for screen readers, and on the last slide the primary
|
||||
// button already does what Skip would.
|
||||
if (!isLast)
|
||||
AuthTextAction(label: 'Skip', onPressed: _finish),
|
||||
],
|
||||
),
|
||||
backgroundColor: ColorConstants.pureSurface,
|
||||
// The image runs under the status bar; only the controls are inset.
|
||||
extendBodyBehindAppBar: true,
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// ── The slide itself ──
|
||||
PageView.builder(
|
||||
controller: _controller,
|
||||
onPageChanged: (i) => setState(() => _index = i),
|
||||
itemCount: _slides.length,
|
||||
itemBuilder: (_, i) => Image.asset(
|
||||
_slides[i],
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
filterQuality: FilterQuality.medium,
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: PageView.builder(
|
||||
controller: _controller,
|
||||
onPageChanged: (i) => setState(() => _index = i),
|
||||
itemCount: _slides.length,
|
||||
itemBuilder: (_, i) => _SlideView(
|
||||
slide: _slides[i],
|
||||
controller: _controller,
|
||||
index: i,
|
||||
// ── Scrim: just enough to seat the dots and the CTA ──
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
height: 210.h,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0x00000000), Color(0x73000000)],
|
||||
stops: [0.0, 0.85],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── Position ──
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(_slides.length, (i) {
|
||||
final active = i == _index;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOutCubic,
|
||||
margin: EdgeInsets.symmetric(horizontal: 3.w),
|
||||
height: 6.h,
|
||||
width: active ? 26.w : 6.h,
|
||||
decoration: BoxDecoration(
|
||||
color: active
|
||||
? ColorConstants.primary
|
||||
: ColorConstants.borderStrong,
|
||||
borderRadius: BorderRadius.circular(
|
||||
DesignConstants.radiusFull,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(24.w, 22.h, 24.w, 14.h),
|
||||
child: AuthPrimaryButton(
|
||||
label: isLast ? 'Get started' : 'Next',
|
||||
onPressed: _next,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// One slide. Parallax is driven by an [AnimatedBuilder] on the page controller
|
||||
/// rather than by a listener calling `setState` on the whole screen, which is
|
||||
/// what the old version did on every pixel of every drag.
|
||||
class _SlideView extends StatelessWidget {
|
||||
final _Slide slide;
|
||||
final PageController controller;
|
||||
final int index;
|
||||
|
||||
const _SlideView({
|
||||
required this.slide,
|
||||
required this.controller,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, _) {
|
||||
// `page` is null until the viewport has been laid out once.
|
||||
final page = controller.hasClients && controller.position.haveDimensions
|
||||
? (controller.page ?? controller.initialPage.toDouble())
|
||||
: controller.initialPage.toDouble();
|
||||
final delta = (page - index).abs().clamp(0.0, 1.0);
|
||||
|
||||
return Opacity(
|
||||
opacity: 1 - (delta * 0.55),
|
||||
child: Transform.scale(
|
||||
scale: 1 - (delta * 0.08),
|
||||
child: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 8.h),
|
||||
SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
slide.art,
|
||||
SizedBox(height: 34.h),
|
||||
Text(
|
||||
slide.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 25.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.6,
|
||||
height: 1.2,
|
||||
color: ColorConstants.slateText,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
// ── A way out ──
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(20.w, 8.h, 16.w, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
// Skip stays unmounted on the last slide rather than
|
||||
// fading to `opacity: 0` — an invisible-but-present
|
||||
// control is a trap for screen readers, and by then the
|
||||
// CTA already does what Skip would.
|
||||
if (!isLast) _SkipAction(onPressed: _finish),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
Text(
|
||||
slide.body,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14.5.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.5,
|
||||
color: ColorConstants.secondaryText,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// ── Position ──
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(_slides.length, (i) {
|
||||
final active = i == _index;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOutCubic,
|
||||
margin: EdgeInsets.symmetric(horizontal: 3.w),
|
||||
height: 6.h,
|
||||
width: active ? 26.w : 6.h,
|
||||
decoration: BoxDecoration(
|
||||
color: active
|
||||
? Colors.white
|
||||
: Colors.white.withValues(alpha: 0.45),
|
||||
borderRadius: BorderRadius.circular(
|
||||
DesignConstants.radiusFull,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(24.w, 20.h, 24.w, 16.h),
|
||||
child: _OnboardButton(
|
||||
label: isLast ? 'Get started' : 'Next',
|
||||
onPressed: _next,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// ILLUSTRATIONS
|
||||
//
|
||||
// Built from the app's real vocabulary — white surfaces, 1.5px `borderStrong`
|
||||
// edges, slate type, the hub teal — so the first thing the rider sees is the
|
||||
// thing he will be using. Static: no controllers, nothing to dispose, nothing
|
||||
// left repainting behind the keyboard.
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Shared frame: the "screen" each mock sits on.
|
||||
class _ArtFrame extends StatelessWidget {
|
||||
final Widget child;
|
||||
const _ArtFrame({required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 300.w),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(16.w),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.pureSurface,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
border: Border.all(color: ColorConstants.borderStrong, width: 1.5),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0F000000),
|
||||
blurRadius: 20,
|
||||
offset: Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Color _hubTeal = Color(0xFF0E7C7B);
|
||||
/// The CTA, inverted. White plate, red label — the one pairing that holds on
|
||||
/// the red wave of slides 1 and 3 *and* on the tarmac of slide 2.
|
||||
class _OnboardButton extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
TextStyle _artTitle(double size) => TextStyle(
|
||||
fontSize: size.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.3,
|
||||
color: ColorConstants.slateText,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
);
|
||||
const _OnboardButton({required this.label, required this.onPressed});
|
||||
|
||||
TextStyle _artMeta(double size) => TextStyle(
|
||||
fontSize: size.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ColorConstants.secondaryText,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
);
|
||||
|
||||
/// Slide 1 — a slot, its load, and the shape of the trip.
|
||||
class _SlotArt extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _ArtFrame(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('12:00 PM – 3:00 PM', style: _artTitle(16)),
|
||||
SizedBox(height: 3.h),
|
||||
Text('6 stops', style: _artMeta(11.5)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 9.w, vertical: 5.h),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.neutralLight,
|
||||
borderRadius: BorderRadius.circular(
|
||||
DesignConstants.radiusFull,
|
||||
),
|
||||
border: Border.all(color: ColorConstants.borderStrong),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.package,
|
||||
size: 11.sp,
|
||||
color: ColorConstants.slateText,
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Text('15', style: _artTitle(12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: ButtonSizes.primary,
|
||||
child: ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.pureSurface,
|
||||
foregroundColor: ColorConstants.primary,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ButtonSizes.radius.r),
|
||||
),
|
||||
SizedBox(height: 14.h),
|
||||
// The metric strip, in miniature.
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.h),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
|
||||
border: Border.all(
|
||||
color: ColorConstants.borderStrong,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_cell('≈3h', 'Duration', ColorConstants.primary),
|
||||
_divider(),
|
||||
_cell('50 km', 'Round trip', ColorConstants.slateText),
|
||||
_divider(),
|
||||
_cell('₹1,030', 'To collect', ColorConstants.acceptGreen),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 14.h),
|
||||
Row(
|
||||
children: [
|
||||
Icon(LucideIcons.warehouse, size: 13.sp, color: _hubTeal),
|
||||
SizedBox(width: 6.w),
|
||||
Text(
|
||||
'HUB',
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 10.sp,
|
||||
color: ColorConstants.primary,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.6,
|
||||
color: _hubTeal,
|
||||
letterSpacing: -0.2,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 7.w),
|
||||
for (var i = 0; i < 4; i++) ...[
|
||||
Container(
|
||||
width: 14.w,
|
||||
height: 2,
|
||||
color: ColorConstants.borderStrong,
|
||||
),
|
||||
Container(
|
||||
width: 7.w,
|
||||
height: 7.w,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
Container(
|
||||
width: 14.w,
|
||||
height: 2,
|
||||
color: ColorConstants.borderStrong,
|
||||
),
|
||||
Icon(LucideIcons.warehouse, size: 13.sp, color: _hubTeal),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _cell(String value, String label, Color accent) => Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 12.5.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.3,
|
||||
color: accent,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 1.h),
|
||||
Text(label, style: _artMeta(9)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _divider() =>
|
||||
Container(width: 1, height: 24.h, color: ColorConstants.borderSubtle);
|
||||
}
|
||||
|
||||
/// Slide 2 — the list, in order, with one done and one skipped.
|
||||
class _StopsArt extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _ArtFrame(
|
||||
child: Column(
|
||||
children: [
|
||||
_row(
|
||||
1,
|
||||
'Sri Balaji Stores',
|
||||
'Done',
|
||||
ColorConstants.acceptGreen,
|
||||
LucideIcons.check,
|
||||
),
|
||||
SizedBox(height: 9.h),
|
||||
_row(
|
||||
2,
|
||||
'Anand Traders',
|
||||
'Skipped',
|
||||
ColorConstants.warning,
|
||||
LucideIcons.redo2,
|
||||
),
|
||||
SizedBox(height: 9.h),
|
||||
_row(3, 'Kumar Textiles', 'Next', ColorConstants.primary, null),
|
||||
SizedBox(height: 9.h),
|
||||
_row(4, 'MS Enterprises', null, ColorConstants.borderStrong, null),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _row(
|
||||
int n,
|
||||
String name,
|
||||
String? status,
|
||||
Color accent,
|
||||
IconData? badge,
|
||||
) {
|
||||
final pending = status == null;
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 20.w,
|
||||
height: 20.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: pending ? ColorConstants.pureSurface : accent,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: accent, width: 1.8),
|
||||
),
|
||||
child: badge != null
|
||||
? Icon(badge, size: 11.sp, color: Colors.white)
|
||||
: Text(
|
||||
'$n',
|
||||
style: TextStyle(
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: pending
|
||||
? ColorConstants.secondaryText
|
||||
: Colors.white,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.2,
|
||||
color: pending
|
||||
? ColorConstants.secondaryText
|
||||
: ColorConstants.slateText,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (status != null)
|
||||
Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 10.5.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: accent,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
SizedBox(width: 8.w),
|
||||
Icon(
|
||||
LucideIcons.arrowRight,
|
||||
size: 18.sp,
|
||||
color: ColorConstants.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Slide 3 — what leaves the stop with you, and where it ends up.
|
||||
class _HubArt extends StatelessWidget {
|
||||
/// Skip, on a translucent plate. The top of every slide is near-white, so the
|
||||
/// label is dark type; the plate keeps it legible if artwork is ever swapped
|
||||
/// for something busier.
|
||||
class _SkipAction extends StatelessWidget {
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _SkipAction({required this.onPressed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _ArtFrame(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'COLLECT AT THIS STOP',
|
||||
style: TextStyle(
|
||||
fontSize: 9.5.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.9,
|
||||
color: ColorConstants.secondaryText,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 11.h),
|
||||
_line(LucideIcons.package, '3 parcels', ColorConstants.pickupAccent),
|
||||
SizedBox(height: 9.h),
|
||||
_line(LucideIcons.banknote, '₹340 cash', ColorConstants.acceptGreen),
|
||||
SizedBox(height: 9.h),
|
||||
_line(LucideIcons.camera, 'Photo proof', ColorConstants.slateText),
|
||||
SizedBox(height: 14.h),
|
||||
Container(height: 1, color: ColorConstants.borderSubtle),
|
||||
SizedBox(height: 13.h),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 22.w,
|
||||
height: 22.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: _hubTeal.withValues(alpha: 0.14),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: _hubTeal, width: 1.5),
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.warehouse,
|
||||
size: 12.sp,
|
||||
color: _hubTeal,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 9.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'RETURN · HUB',
|
||||
style: TextStyle(
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.7,
|
||||
color: _hubTeal,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2.h),
|
||||
Text('Hand everything over', style: _artMeta(10.5)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
return TextButton(
|
||||
onPressed: onPressed,
|
||||
style: TextButton.styleFrom(
|
||||
minimumSize: Size(0, ButtonSizes.minTapTarget),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
|
||||
backgroundColor: Colors.white.withValues(alpha: 0.72),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusFull),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _line(IconData icon, String text, Color accent) => Row(
|
||||
children: [
|
||||
Icon(icon, size: 15.sp, color: accent),
|
||||
SizedBox(width: 9.w),
|
||||
Text(
|
||||
text,
|
||||
child: Text(
|
||||
'Skip',
|
||||
style: TextStyle(
|
||||
fontSize: 13.sp,
|
||||
fontSize: 13.5.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.2,
|
||||
color: ColorConstants.slateText,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user