redesign: duolingo-pattern onboarding, no gradient, pressable button

- Nunito font replacing Plus Jakarta Sans
- SButton: Stack-based 3D pressable (dark shadow layer + top layer translates on press)
- SCard: flat border-based selection, no shadows
- SProgressBar: 4px thin bar, animated fill
- All screens: white bg, no gradients
- Duolingo layout: illustration top zone, content below, sticky button
This commit is contained in:
mav
2026-05-28 16:34:04 +02:00
parent 30cee5a486
commit 6311088256
9 changed files with 1168 additions and 638 deletions

View File

@@ -1,114 +1,171 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:google_fonts/google_fonts.dart';
import '../theme.dart';
import '../widgets/primary_button.dart';
import '../widgets/s_button.dart';
import 'quiz/quiz_flow.dart';
class IntroScreen extends StatelessWidget {
const IntroScreen({super.key});
void _start(BuildContext context) {
Navigator.of(context).push(PageRouteBuilder(
pageBuilder: (_, a, __) => const QuizFlow(),
transitionsBuilder: (_, a, __, child) => SlideTransition(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(CurvedAnimation(parent: a, curve: Curves.easeOut)),
child: child,
),
transitionDuration: const Duration(milliseconds: 300),
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: SColors.bgPage,
backgroundColor: SColors.bgSurface,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Illustration zone (top 42%)
Expanded(
flex: 42,
child: _illustrationZone()
.animate()
.fadeIn(duration: 500.ms),
),
// Content zone (bottom 58%)
Expanded(
flex: 58,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
// Trust badge
_badge()
.animate()
.fadeIn(duration: 400.ms, delay: 150.ms),
const SizedBox(height: 16),
// Headline
Text(
'Skin that glows,\nroutine that works.',
style: SText.headline2(SColors.textHead),
)
.animate()
.fadeIn(duration: 400.ms, delay: 200.ms)
.slideY(begin: 0.06, end: 0, duration: 400.ms),
const SizedBox(height: 10),
// Sub
Text(
'Answer 5 quick questions and get your\npersonalised skincare plan in 60 seconds.',
style: SText.subtitle(SColors.textSub),
)
.animate()
.fadeIn(duration: 400.ms, delay: 280.ms),
const Spacer(),
// Stats row
_statsRow()
.animate()
.fadeIn(duration: 400.ms, delay: 360.ms),
const SizedBox(height: 24),
// CTA button
SButton(
label: 'GET STARTED',
onTap: () => _start(context),
)
.animate()
.fadeIn(duration: 400.ms, delay: 440.ms)
.slideY(begin: 0.1, end: 0, duration: 400.ms),
const SizedBox(height: 16),
// Sign in link (plain text, no button)
Center(
child: GestureDetector(
onTap: () {},
child: Text(
'Already have an account? Sign in',
style: SText.caption(SColors.textSub),
),
),
)
.animate()
.fadeIn(duration: 400.ms, delay: 500.ms),
const SizedBox(height: 20),
],
),
),
),
],
),
),
);
}
Widget _illustrationZone() {
return Stack(
children: [
// Light tinted background for art area
Container(color: const Color(0xFFF7F5FF)),
// Stats visual — abstract feature cards
Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 24),
_badge()
.animate()
.fadeIn(duration: 400.ms),
const SizedBox(height: 20),
Text(
'Skin that glows,\nroutine that works.',
style: GoogleFonts.plusJakartaSans(
fontSize: 32,
fontWeight: FontWeight.w800,
color: SColors.textPrimary,
height: 1.2,
),
)
.animate()
.fadeIn(duration: 500.ms, delay: 100.ms)
.slideY(begin: 0.1, end: 0, duration: 500.ms),
const SizedBox(height: 12),
Text(
'Answer 5 quick questions and get your\npersonalised skincare plan — in 60 seconds.',
style: GoogleFonts.plusJakartaSans(
fontSize: 15,
color: SColors.textSecondary,
height: 1.5,
),
)
.animate()
.fadeIn(duration: 500.ms, delay: 200.ms),
const SizedBox(height: 32),
_statsRow()
.animate()
.fadeIn(duration: 500.ms, delay: 300.ms),
const SizedBox(height: 32),
_featureList()
.animate()
.fadeIn(duration: 500.ms, delay: 400.ms),
const Spacer(),
PrimaryButton(
label: 'Build my routine',
icon: Icons.arrow_forward_rounded,
onTap: () => Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (_, a, __) => const QuizFlow(),
transitionsBuilder: (_, a, __, child) =>
SlideTransition(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(CurvedAnimation(parent: a, curve: Curves.easeOut)),
child: child,
),
transitionDuration: const Duration(milliseconds: 350),
),
),
)
.animate()
.fadeIn(duration: 400.ms, delay: 600.ms)
.slideY(begin: 0.2, end: 0, duration: 400.ms),
const SizedBox(height: 12),
Center(
child: Text(
'Free · No credit card · 1 min setup',
style: GoogleFonts.plusJakartaSans(
fontSize: 12,
color: SColors.textSecondary,
),
),
)
.animate()
.fadeIn(duration: 400.ms, delay: 700.ms),
const SizedBox(height: 24),
_featureChip(Icons.auto_awesome_rounded, 'AI skin analysis'),
const SizedBox(height: 10),
_featureChip(Icons.science_rounded, 'Ingredient science'),
const SizedBox(height: 10),
_featureChip(Icons.lock_outline_rounded, 'Private & secure'),
],
),
),
],
);
}
Widget _featureChip(IconData icon, String label) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: SColors.bgSurface,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: SColors.borderLight, width: 1.5),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: SColors.primary, size: 20),
const SizedBox(width: 10),
Text(label, style: SText.cardLabel(SColors.textHead)),
],
),
);
}
Widget _badge() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: SColors.primary.withAlpha(18),
color: SColors.primaryLight,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
width: 7,
height: 7,
decoration: const BoxDecoration(
color: SColors.accent,
shape: BoxShape.circle,
@@ -117,11 +174,7 @@ class IntroScreen extends StatelessWidget {
const SizedBox(width: 6),
Text(
'Trusted by 50,000+ women',
style: GoogleFonts.plusJakartaSans(
fontSize: 12,
fontWeight: FontWeight.w600,
color: SColors.primary,
),
style: SText.caption(SColors.primary).copyWith(fontWeight: FontWeight.w700),
),
],
),
@@ -131,96 +184,32 @@ class IntroScreen extends StatelessWidget {
Widget _statsRow() {
return Row(
children: [
_statCard('50K+', 'Users'),
const SizedBox(width: 12),
_statCard('4.9★', 'Rating'),
const SizedBox(width: 12),
_statCard('94%', 'See results'),
_statBox('50K+', 'Users'),
const SizedBox(width: 10),
_statBox('4.9★', 'Rating'),
const SizedBox(width: 10),
_statBox('94%', 'Results'),
],
);
}
Widget _statCard(String value, String label) {
Widget _statBox(String value, String label) {
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: SColors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: SColors.border),
color: SColors.bgSurface,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: SColors.borderLight, width: 1.5),
),
child: Column(
children: [
Text(
value,
style: GoogleFonts.plusJakartaSans(
fontSize: 20,
fontWeight: FontWeight.w800,
color: SColors.primary,
),
),
Text(value, style: SText.title(SColors.primary)),
const SizedBox(height: 2),
Text(
label,
style: GoogleFonts.plusJakartaSans(
fontSize: 12,
color: SColors.textSecondary,
),
),
Text(label, style: SText.caption(SColors.textSub)),
],
),
),
);
}
Widget _featureList() {
const items = [
(Icons.auto_awesome_rounded, 'AI-powered analysis', 'Personalised to your skin'),
(Icons.science_rounded, 'Ingredient science', 'Evidence-based recommendations'),
(Icons.lock_outline_rounded, 'Private & secure', 'Your data stays with you'),
];
return Column(
children: items.map((item) => _featureRow(item.$1, item.$2, item.$3)).toList(),
);
}
Widget _featureRow(IconData icon, String title, String sub) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: SColors.primary.withAlpha(18),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: SColors.primary, size: 20),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.plusJakartaSans(
fontSize: 14,
fontWeight: FontWeight.w600,
color: SColors.textPrimary,
),
),
Text(
sub,
style: GoogleFonts.plusJakartaSans(
fontSize: 12,
color: SColors.textSecondary,
),
),
],
),
],
),
);
}
}