Improve onboarding with title and progress bar

- Added "Add Months" header with subtitle
- Numbered step indicators (1, 2, 3) with progress lines
- Checkmarks for completed steps
- "Step X of 3" badge on each slide

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-02-20 22:50:04 -08:00
parent c92ecf5774
commit 615e18d03a

View file

@ -43,28 +43,46 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
body: SafeArea( body: SafeArea(
child: Column( child: Column(
children: [ children: [
// Header
Padding(
padding: const EdgeInsets.fromLTRB(32, 24, 32, 0),
child: Column(
children: [
Text(
'Add Months',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
'Evidence-based lifespan optimization',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
const SizedBox(height: 16),
// Progress bar
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: _buildProgressBar(),
),
// Slides
Expanded( Expanded(
child: PageView.builder( child: PageView.builder(
controller: _controller, controller: _controller,
itemCount: _slides.length, itemCount: _slides.length,
onPageChanged: (index) => setState(() => _currentPage = index), onPageChanged: (index) => setState(() => _currentPage = index),
itemBuilder: (context, index) => _buildSlide(_slides[index]), itemBuilder: (context, index) =>
_buildSlide(index, _slides[index]),
), ),
), ),
// Bottom section
Padding( Padding(
padding: const EdgeInsets.all(32), padding: const EdgeInsets.fromLTRB(32, 0, 32, 32),
child: Column( child: Column(
children: [ children: [
// Page indicators
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
_slides.length,
(index) => _buildDot(index),
),
),
const SizedBox(height: 32),
// Button
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
@ -75,7 +93,6 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Skip button (not on last page)
if (_currentPage < _slides.length - 1) if (_currentPage < _slides.length - 1)
TextButton( TextButton(
onPressed: _skip, onPressed: _skip,
@ -95,32 +112,112 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
); );
} }
Widget _buildSlide(_SlideData slide) { Widget _buildProgressBar() {
return Row(
children: List.generate(_slides.length, (index) {
final isCompleted = index < _currentPage;
final isCurrent = index == _currentPage;
return Expanded(
child: Container(
margin: EdgeInsets.only(right: index < _slides.length - 1 ? 8 : 0),
child: Column(
children: [
// Step number row
Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: isCompleted || isCurrent
? AppColors.primary
: AppColors.divider,
shape: BoxShape.circle,
),
child: Center(
child: isCompleted
? const Icon(Icons.check,
size: 14, color: Colors.white)
: Text(
'${index + 1}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: isCurrent
? Colors.white
: AppColors.textTertiary,
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Container(
height: 3,
decoration: BoxDecoration(
color: isCompleted
? AppColors.primary
: AppColors.divider,
borderRadius: BorderRadius.circular(2),
),
),
),
],
),
],
),
),
);
}),
);
}
Widget _buildSlide(int index, _SlideData slide) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40), padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Step indicator
Container( Container(
width: 120, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
height: 120, decoration: BoxDecoration(
color: AppColors.primary.withAlpha(26),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Step ${index + 1} of ${_slides.length}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
),
),
const SizedBox(height: 32),
// Icon
Container(
width: 100,
height: 100,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.primary.withAlpha(26), color: AppColors.primary.withAlpha(26),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon( child: Icon(
slide.icon, slide.icon,
size: 56, size: 48,
color: AppColors.primary, color: AppColors.primary,
), ),
), ),
const SizedBox(height: 48), const SizedBox(height: 32),
// Title
Text( Text(
slide.title, slide.title,
style: Theme.of(context).textTheme.headlineLarge, style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
// Description
Text( Text(
slide.description, slide.description,
style: Theme.of(context).textTheme.bodyLarge?.copyWith( style: Theme.of(context).textTheme.bodyLarge?.copyWith(
@ -133,20 +230,6 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
); );
} }
Widget _buildDot(int index) {
final isActive = index == _currentPage;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(horizontal: 4),
width: isActive ? 24 : 8,
height: 8,
decoration: BoxDecoration(
color: isActive ? AppColors.primary : AppColors.divider,
borderRadius: BorderRadius.circular(4),
),
);
}
void _onButtonPressed() { void _onButtonPressed() {
if (_currentPage < _slides.length - 1) { if (_currentPage < _slides.length - 1) {
_controller.nextPage( _controller.nextPage(