From 8311cbfe3320250becf4c0c75642019692e52def Mon Sep 17 00:00:00 2001 From: Amaury Date: Wed, 19 Nov 2025 12:10:51 -0300 Subject: [PATCH 1/2] improve theme colors and text styles --- app/lib/presentation/navigation/routers.dart | 11 +- .../locale/generated/intl/messages_en.dart | 19 ++ .../locale/generated/intl/messages_es.dart | 19 ++ .../resources/locale/generated/l10n.dart | 95 +++++++++ .../presentation/resources/locale/intl_en.arb | 15 +- .../presentation/resources/locale/intl_es.arb | 13 +- .../ui/pages/auth/login/login_form.dart | 9 +- .../ui/pages/onboarding/onboarding_page.dart | 189 ++++++++++++++++++ modules/domain/devtools_options.yaml | 3 + 9 files changed, 361 insertions(+), 12 deletions(-) create mode 100644 app/lib/presentation/ui/pages/onboarding/onboarding_page.dart create mode 100644 modules/domain/devtools_options.yaml diff --git a/app/lib/presentation/navigation/routers.dart b/app/lib/presentation/navigation/routers.dart index cde504c..44bd1a0 100644 --- a/app/lib/presentation/navigation/routers.dart +++ b/app/lib/presentation/navigation/routers.dart @@ -3,6 +3,7 @@ import 'package:app/presentation/ui/custom/debug_banner.dart'; import 'package:app/presentation/ui/pages/main/home/home_page.dart'; import 'package:app/presentation/ui/pages/auth/login/login_page.dart'; import 'package:app/presentation/ui/pages/auth/sign_up/sign_up_page.dart'; +import 'package:app/presentation/ui/pages/onboarding/onboarding_page.dart'; import 'package:app/presentation/ui/pages/splash/splash_page.dart'; import 'package:common/core/resource.dart'; import 'package:domain/bloc/auth/auth_cubit.dart'; @@ -14,6 +15,7 @@ import 'package:go_router/go_router.dart'; enum Routes { auth, + onboarding, login, signup, app, @@ -50,7 +52,7 @@ class Routers { initialLocation: initialLocation ?? (getIt().isLoggedIn() ? Routes.app.path - : Routes.auth.path), + : Routes.onboarding.path), routes: [ GoRoute( path: '/', @@ -79,7 +81,7 @@ class Routers { return; } debugPrint('Navigating to auth route'); - Routes.auth.go(context); + Routes.onboarding.go(context); break; case _: } @@ -93,6 +95,11 @@ class Routers { builder: (context, state, child) => kDebugMode ? DebugBanner(child: child) : child, routes: [ + GoRoute( + name: Routes.onboarding.name, + path: Routes.onboarding.path, + builder: (context, state) => const OnboardingPage(), + ), GoRoute( name: Routes.auth.name, path: Routes.auth.path, diff --git a/app/lib/presentation/resources/locale/generated/intl/messages_en.dart b/app/lib/presentation/resources/locale/generated/intl/messages_en.dart index e99d512..0aae7e2 100644 --- a/app/lib/presentation/resources/locale/generated/intl/messages_en.dart +++ b/app/lib/presentation/resources/locale/generated/intl/messages_en.dart @@ -70,6 +70,25 @@ class MessageLookup extends MessageLookupByLibrary { "Invalid email or password.", ), "noConnection": MessageLookupByLibrary.simpleMessage("No connection"), + "onboardingBack": MessageLookupByLibrary.simpleMessage("Back"), + "onboardingNext": MessageLookupByLibrary.simpleMessage("Next"), + "onboardingPage1Description": MessageLookupByLibrary.simpleMessage( + "This is the first page of the onboarding flow", + ), + "onboardingPage1Title": MessageLookupByLibrary.simpleMessage("Welcome"), + "onboardingPage2Description": MessageLookupByLibrary.simpleMessage( + "This is the second page of the onboarding flow", + ), + "onboardingPage2Title": MessageLookupByLibrary.simpleMessage("Discover"), + "onboardingPage3Description": MessageLookupByLibrary.simpleMessage( + "This is the third page of the onboarding flow", + ), + "onboardingPage3Title": MessageLookupByLibrary.simpleMessage("Connect"), + "onboardingPage4Description": MessageLookupByLibrary.simpleMessage( + "This is the fourth page of the onboarding flow", + ), + "onboardingPage4Title": MessageLookupByLibrary.simpleMessage("Get Started"), + "onboardingStart": MessageLookupByLibrary.simpleMessage("Start"), "passwordInstructions": MessageLookupByLibrary.simpleMessage( "Min 8 characters long: 1 uppercase letter, 1 lowercase letter, 1 number, and 1 special character.", ), diff --git a/app/lib/presentation/resources/locale/generated/intl/messages_es.dart b/app/lib/presentation/resources/locale/generated/intl/messages_es.dart index 76be69b..fa4996d 100644 --- a/app/lib/presentation/resources/locale/generated/intl/messages_es.dart +++ b/app/lib/presentation/resources/locale/generated/intl/messages_es.dart @@ -72,6 +72,25 @@ class MessageLookup extends MessageLookupByLibrary { "Correo electrónico o contraseña incorrectos.", ), "noConnection": MessageLookupByLibrary.simpleMessage("Sin conexión"), + "onboardingBack": MessageLookupByLibrary.simpleMessage("Atrás"), + "onboardingNext": MessageLookupByLibrary.simpleMessage("Siguiente"), + "onboardingPage1Description": MessageLookupByLibrary.simpleMessage( + "Esta es la primera página del flujo de incorporación", + ), + "onboardingPage1Title": MessageLookupByLibrary.simpleMessage("Bienvenido"), + "onboardingPage2Description": MessageLookupByLibrary.simpleMessage( + "Esta es la segunda página del flujo de incorporación", + ), + "onboardingPage2Title": MessageLookupByLibrary.simpleMessage("Descubrir"), + "onboardingPage3Description": MessageLookupByLibrary.simpleMessage( + "Esta es la tercera página del flujo de incorporación", + ), + "onboardingPage3Title": MessageLookupByLibrary.simpleMessage("Conectar"), + "onboardingPage4Description": MessageLookupByLibrary.simpleMessage( + "Esta es la cuarta página del flujo de incorporación", + ), + "onboardingPage4Title": MessageLookupByLibrary.simpleMessage("Comenzar"), + "onboardingStart": MessageLookupByLibrary.simpleMessage("Comenzar"), "passwordInstructions": MessageLookupByLibrary.simpleMessage( "Mínimo 8 caracteres: 1 mayúscula, 1 minúscula, 1 número y 1 carácter especial.", ), diff --git a/app/lib/presentation/resources/locale/generated/l10n.dart b/app/lib/presentation/resources/locale/generated/l10n.dart index c6fbafc..91cf316 100644 --- a/app/lib/presentation/resources/locale/generated/l10n.dart +++ b/app/lib/presentation/resources/locale/generated/l10n.dart @@ -314,6 +314,101 @@ class S { args: [], ); } + + /// `Back` + String get onboardingBack { + return Intl.message('Back', name: 'onboardingBack', desc: '', args: []); + } + + /// `Next` + String get onboardingNext { + return Intl.message('Next', name: 'onboardingNext', desc: '', args: []); + } + + /// `Start` + String get onboardingStart { + return Intl.message('Start', name: 'onboardingStart', desc: '', args: []); + } + + /// `Welcome` + String get onboardingPage1Title { + return Intl.message( + 'Welcome', + name: 'onboardingPage1Title', + desc: '', + args: [], + ); + } + + /// `This is the first page of the onboarding flow` + String get onboardingPage1Description { + return Intl.message( + 'This is the first page of the onboarding flow', + name: 'onboardingPage1Description', + desc: '', + args: [], + ); + } + + /// `Discover` + String get onboardingPage2Title { + return Intl.message( + 'Discover', + name: 'onboardingPage2Title', + desc: '', + args: [], + ); + } + + /// `This is the second page of the onboarding flow` + String get onboardingPage2Description { + return Intl.message( + 'This is the second page of the onboarding flow', + name: 'onboardingPage2Description', + desc: '', + args: [], + ); + } + + /// `Connect` + String get onboardingPage3Title { + return Intl.message( + 'Connect', + name: 'onboardingPage3Title', + desc: '', + args: [], + ); + } + + /// `This is the third page of the onboarding flow` + String get onboardingPage3Description { + return Intl.message( + 'This is the third page of the onboarding flow', + name: 'onboardingPage3Description', + desc: '', + args: [], + ); + } + + /// `Get Started` + String get onboardingPage4Title { + return Intl.message( + 'Get Started', + name: 'onboardingPage4Title', + desc: '', + args: [], + ); + } + + /// `This is the fourth page of the onboarding flow` + String get onboardingPage4Description { + return Intl.message( + 'This is the fourth page of the onboarding flow', + name: 'onboardingPage4Description', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/app/lib/presentation/resources/locale/intl_en.arb b/app/lib/presentation/resources/locale/intl_en.arb index f0b4445..051a824 100644 --- a/app/lib/presentation/resources/locale/intl_en.arb +++ b/app/lib/presentation/resources/locale/intl_en.arb @@ -30,5 +30,16 @@ "debugModeResetAppTitle": "Reset App", "debugModeResetAppMessage": "Are you sure you want to reset the app?", "debugModeCancel": "Cancel", - "debugModeConfirm": "Confirm" -} + "debugModeConfirm": "Confirm", + "onboardingBack": "Back", + "onboardingNext": "Next", + "onboardingStart": "Start", + "onboardingPage1Title": "Welcome", + "onboardingPage1Description": "This is the first page of the onboarding flow", + "onboardingPage2Title": "Discover", + "onboardingPage2Description": "This is the second page of the onboarding flow", + "onboardingPage3Title": "Connect", + "onboardingPage3Description": "This is the third page of the onboarding flow", + "onboardingPage4Title": "Get Started", + "onboardingPage4Description": "This is the fourth page of the onboarding flow" +} \ No newline at end of file diff --git a/app/lib/presentation/resources/locale/intl_es.arb b/app/lib/presentation/resources/locale/intl_es.arb index 9a95d34..745f046 100644 --- a/app/lib/presentation/resources/locale/intl_es.arb +++ b/app/lib/presentation/resources/locale/intl_es.arb @@ -30,5 +30,16 @@ "debugModeResetAppTitle": "Resetear App", "debugModeResetAppMessage": "¿Estás seguro de que deseas resetear la aplicación?", "debugModeCancel": "Cancelar", - "debugModeConfirm": "Confirmar" + "debugModeConfirm": "Confirmar", + "onboardingBack": "Atrás", + "onboardingNext": "Siguiente", + "onboardingStart": "Comenzar", + "onboardingPage1Title": "Bienvenido", + "onboardingPage1Description": "Esta es la primera página del flujo de incorporación", + "onboardingPage2Title": "Descubrir", + "onboardingPage2Description": "Esta es la segunda página del flujo de incorporación", + "onboardingPage3Title": "Conectar", + "onboardingPage3Description": "Esta es la tercera página del flujo de incorporación", + "onboardingPage4Title": "Comenzar", + "onboardingPage4Description": "Esta es la cuarta página del flujo de incorporación" } \ No newline at end of file diff --git a/app/lib/presentation/ui/pages/auth/login/login_form.dart b/app/lib/presentation/ui/pages/auth/login/login_form.dart index 0eb85f6..25be1bd 100644 --- a/app/lib/presentation/ui/pages/auth/login/login_form.dart +++ b/app/lib/presentation/ui/pages/auth/login/login_form.dart @@ -72,18 +72,13 @@ class _LoginFormState extends State { obscureText: true, controller: passwordController, validator: (value) { - if (!FormValidator.isStrongPassword(value)) { - return S.of(context).errorPasswordWeak; + if (value?.trim().isEmpty ?? true) { + return S.of(context).errorPasswordRequired; } return null; }, ), const Gap(Dimen.spacingM), - Text( - S.of(context).passwordInstructions, - style: Theme.of(context).textTheme.bodySmall, - ), - const Gap(Dimen.spacingM), TermsServicesCheck( agreeToTerms: agreeToTerms, onChanged: (value) { diff --git a/app/lib/presentation/ui/pages/onboarding/onboarding_page.dart b/app/lib/presentation/ui/pages/onboarding/onboarding_page.dart new file mode 100644 index 0000000..a4f4a88 --- /dev/null +++ b/app/lib/presentation/ui/pages/onboarding/onboarding_page.dart @@ -0,0 +1,189 @@ +import 'package:app/presentation/navigation/routers.dart'; +import 'package:app/presentation/resources/locale/generated/l10n.dart'; +import 'package:app/presentation/resources/resources.dart'; +import 'package:flutter/material.dart'; + +class OnboardingPage extends StatefulWidget { + const OnboardingPage({super.key}); + + @override + State createState() => _OnboardingPageState(); +} + +class _OnboardingPageState extends State { + final PageController _pageController = PageController(); + int _currentPage = 0; + final int _totalPages = 4; + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + void _onPageChanged(int page) { + setState(() { + _currentPage = page; + }); + } + + void _nextPage() { + if (_currentPage < _totalPages - 1) { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.bounceInOut, + ); + } else { + // On the last page, handle start action + _onStart(); + } + } + + void _previousPage() { + if (_currentPage > 0) { + _pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } + + void _onStart() { + Routes.auth.go(context); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Column( + children: [ + // Page Indicator + Padding( + padding: const EdgeInsets.all(Dimen.spacingM), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + _totalPages, + (index) => Container( + margin: + const EdgeInsets.symmetric(horizontal: Dimen.spacingXs), + width: + _currentPage == index ? Dimen.spacingL : Dimen.spacingS, + height: Dimen.spacingS, + decoration: BoxDecoration( + color: _currentPage == index + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outline, + borderRadius: BorderRadius.circular(Dimen.spacingXs), + ), + ), + ), + ), + ), + + // PageView + Expanded( + child: PageView( + controller: _pageController, + onPageChanged: _onPageChanged, + children: [ + _buildPage( + title: S.of(context).onboardingPage1Title, + description: S.of(context).onboardingPage1Description, + icon: Icons.waving_hand, + color: Theme.of(context).colorScheme.primary, + ), + _buildPage( + title: S.of(context).onboardingPage2Title, + description: S.of(context).onboardingPage2Description, + icon: Icons.explore, + color: Theme.of(context).colorScheme.secondary, + ), + _buildPage( + title: S.of(context).onboardingPage3Title, + description: S.of(context).onboardingPage3Description, + icon: Icons.people, + color: Theme.of(context).colorScheme.tertiary, + ), + _buildPage( + title: S.of(context).onboardingPage4Title, + description: S.of(context).onboardingPage4Description, + icon: Icons.rocket_launch, + color: Theme.of(context).colorScheme.error, + ), + ], + ), + ), + + // Navigation Buttons + Padding( + padding: const EdgeInsets.all(Dimen.spacingL), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Back Button (hidden on first page) + if (_currentPage > 0) + TextButton( + onPressed: _previousPage, + child: Text(S.of(context).onboardingBack), + ) + else + const SizedBox(width: 80), // Placeholder for alignment + + // Next/Start Button + ElevatedButton( + onPressed: _nextPage, + child: Text( + _currentPage == _totalPages - 1 + ? S.of(context).onboardingStart + : S.of(context).onboardingNext, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildPage({ + required String title, + required String description, + required IconData icon, + required Color color, + }) { + return Padding( + padding: const EdgeInsets.all(Dimen.spacingXl), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: 120, + color: color, + ), + const SizedBox(height: Dimen.spacingXl), + Text( + title, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: Dimen.spacingM), + Text( + description, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: + Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} diff --git a/modules/domain/devtools_options.yaml b/modules/domain/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/modules/domain/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: From 2977e68104b4033dcc909a6843b97fff4dec5ac6 Mon Sep 17 00:00:00 2001 From: Amaury Date: Wed, 19 Nov 2025 13:01:45 -0300 Subject: [PATCH 2/2] add focus node to allow nav with keyboard --- app/lib/presentation/resources/dim.dart | 1 + .../locale/generated/intl/messages_en.dart | 4 + .../locale/generated/intl/messages_es.dart | 4 + .../resources/locale/generated/l10n.dart | 10 + .../presentation/resources/locale/intl_en.arb | 3 +- .../presentation/resources/locale/intl_es.arb | 3 +- .../ui/pages/onboarding/onboarding_page.dart | 189 ++++++++++-------- 7 files changed, 129 insertions(+), 85 deletions(-) diff --git a/app/lib/presentation/resources/dim.dart b/app/lib/presentation/resources/dim.dart index 027ea22..ed1f904 100644 --- a/app/lib/presentation/resources/dim.dart +++ b/app/lib/presentation/resources/dim.dart @@ -19,4 +19,5 @@ class Dimen { static const spacingXxxl = 48.0; static const double buttonHeightM = 48.0; + static const double onboardingIconSize = 120.0; } diff --git a/app/lib/presentation/resources/locale/generated/intl/messages_en.dart b/app/lib/presentation/resources/locale/generated/intl/messages_en.dart index 0aae7e2..f7ead05 100644 --- a/app/lib/presentation/resources/locale/generated/intl/messages_en.dart +++ b/app/lib/presentation/resources/locale/generated/intl/messages_en.dart @@ -20,6 +20,9 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'en'; + static String m0(currentPage, totalPages) => + "Page ${currentPage} of ${totalPages}"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "appName": MessageLookupByLibrary.simpleMessage("Flutter Target"), @@ -88,6 +91,7 @@ class MessageLookup extends MessageLookupByLibrary { "This is the fourth page of the onboarding flow", ), "onboardingPage4Title": MessageLookupByLibrary.simpleMessage("Get Started"), + "onboardingPageIndicator": m0, "onboardingStart": MessageLookupByLibrary.simpleMessage("Start"), "passwordInstructions": MessageLookupByLibrary.simpleMessage( "Min 8 characters long: 1 uppercase letter, 1 lowercase letter, 1 number, and 1 special character.", diff --git a/app/lib/presentation/resources/locale/generated/intl/messages_es.dart b/app/lib/presentation/resources/locale/generated/intl/messages_es.dart index fa4996d..6d8e904 100644 --- a/app/lib/presentation/resources/locale/generated/intl/messages_es.dart +++ b/app/lib/presentation/resources/locale/generated/intl/messages_es.dart @@ -20,6 +20,9 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'es'; + static String m0(currentPage, totalPages) => + "Página ${currentPage} de ${totalPages}"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "appName": MessageLookupByLibrary.simpleMessage("Flutter Target"), @@ -90,6 +93,7 @@ class MessageLookup extends MessageLookupByLibrary { "Esta es la cuarta página del flujo de incorporación", ), "onboardingPage4Title": MessageLookupByLibrary.simpleMessage("Comenzar"), + "onboardingPageIndicator": m0, "onboardingStart": MessageLookupByLibrary.simpleMessage("Comenzar"), "passwordInstructions": MessageLookupByLibrary.simpleMessage( "Mínimo 8 caracteres: 1 mayúscula, 1 minúscula, 1 número y 1 carácter especial.", diff --git a/app/lib/presentation/resources/locale/generated/l10n.dart b/app/lib/presentation/resources/locale/generated/l10n.dart index 91cf316..d3696f8 100644 --- a/app/lib/presentation/resources/locale/generated/l10n.dart +++ b/app/lib/presentation/resources/locale/generated/l10n.dart @@ -409,6 +409,16 @@ class S { args: [], ); } + + /// `Page {currentPage} of {totalPages}` + String onboardingPageIndicator(Object currentPage, Object totalPages) { + return Intl.message( + 'Page $currentPage of $totalPages', + name: 'onboardingPageIndicator', + desc: '', + args: [currentPage, totalPages], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/app/lib/presentation/resources/locale/intl_en.arb b/app/lib/presentation/resources/locale/intl_en.arb index 051a824..0a541e4 100644 --- a/app/lib/presentation/resources/locale/intl_en.arb +++ b/app/lib/presentation/resources/locale/intl_en.arb @@ -41,5 +41,6 @@ "onboardingPage3Title": "Connect", "onboardingPage3Description": "This is the third page of the onboarding flow", "onboardingPage4Title": "Get Started", - "onboardingPage4Description": "This is the fourth page of the onboarding flow" + "onboardingPage4Description": "This is the fourth page of the onboarding flow", + "onboardingPageIndicator": "Page {currentPage} of {totalPages}" } \ No newline at end of file diff --git a/app/lib/presentation/resources/locale/intl_es.arb b/app/lib/presentation/resources/locale/intl_es.arb index 745f046..cd3eea8 100644 --- a/app/lib/presentation/resources/locale/intl_es.arb +++ b/app/lib/presentation/resources/locale/intl_es.arb @@ -41,5 +41,6 @@ "onboardingPage3Title": "Conectar", "onboardingPage3Description": "Esta es la tercera página del flujo de incorporación", "onboardingPage4Title": "Comenzar", - "onboardingPage4Description": "Esta es la cuarta página del flujo de incorporación" + "onboardingPage4Description": "Esta es la cuarta página del flujo de incorporación", + "onboardingPageIndicator": "Página {currentPage} de {totalPages}" } \ No newline at end of file diff --git a/app/lib/presentation/ui/pages/onboarding/onboarding_page.dart b/app/lib/presentation/ui/pages/onboarding/onboarding_page.dart index a4f4a88..cdb131b 100644 --- a/app/lib/presentation/ui/pages/onboarding/onboarding_page.dart +++ b/app/lib/presentation/ui/pages/onboarding/onboarding_page.dart @@ -2,6 +2,7 @@ import 'package:app/presentation/navigation/routers.dart'; import 'package:app/presentation/resources/locale/generated/l10n.dart'; import 'package:app/presentation/resources/resources.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class OnboardingPage extends StatefulWidget { const OnboardingPage({super.key}); @@ -12,12 +13,14 @@ class OnboardingPage extends StatefulWidget { class _OnboardingPageState extends State { final PageController _pageController = PageController(); + final FocusNode _focusNode = FocusNode(); int _currentPage = 0; final int _totalPages = 4; @override void dispose() { _pageController.dispose(); + _focusNode.dispose(); super.dispose(); } @@ -55,95 +58,115 @@ class _OnboardingPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: SafeArea( - child: Column( - children: [ - // Page Indicator - Padding( - padding: const EdgeInsets.all(Dimen.spacingM), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate( - _totalPages, - (index) => Container( - margin: - const EdgeInsets.symmetric(horizontal: Dimen.spacingXs), - width: - _currentPage == index ? Dimen.spacingL : Dimen.spacingS, - height: Dimen.spacingS, - decoration: BoxDecoration( - color: _currentPage == index - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline, - borderRadius: BorderRadius.circular(Dimen.spacingXs), + body: Focus( + focusNode: _focusNode, + autofocus: true, + onKeyEvent: (node, event) { + if (event is KeyDownEvent) { + if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + _nextPage(); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + _previousPage(); + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + }, + child: SafeArea( + child: Column( + children: [ + // Page Indicator + Padding( + padding: const EdgeInsets.all(Dimen.spacingM), + child: Semantics( + label: S.of(context).onboardingPageIndicator( + _currentPage + 1, + _totalPages, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + _totalPages, + (index) => Container( + margin: const EdgeInsets.symmetric( + horizontal: Dimen.spacingXs), + width: _currentPage == index + ? Dimen.spacingL + : Dimen.spacingS, + height: Dimen.spacingS, + decoration: BoxDecoration( + color: _currentPage == index + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outline, + borderRadius: BorderRadius.circular(Dimen.spacingXs), + ), + ), ), ), ), ), - ), - - // PageView - Expanded( - child: PageView( - controller: _pageController, - onPageChanged: _onPageChanged, - children: [ - _buildPage( - title: S.of(context).onboardingPage1Title, - description: S.of(context).onboardingPage1Description, - icon: Icons.waving_hand, - color: Theme.of(context).colorScheme.primary, - ), - _buildPage( - title: S.of(context).onboardingPage2Title, - description: S.of(context).onboardingPage2Description, - icon: Icons.explore, - color: Theme.of(context).colorScheme.secondary, - ), - _buildPage( - title: S.of(context).onboardingPage3Title, - description: S.of(context).onboardingPage3Description, - icon: Icons.people, - color: Theme.of(context).colorScheme.tertiary, - ), - _buildPage( - title: S.of(context).onboardingPage4Title, - description: S.of(context).onboardingPage4Description, - icon: Icons.rocket_launch, - color: Theme.of(context).colorScheme.error, - ), - ], + // PageView + Expanded( + child: PageView( + controller: _pageController, + onPageChanged: _onPageChanged, + children: [ + _buildPage( + title: S.of(context).onboardingPage1Title, + description: S.of(context).onboardingPage1Description, + icon: Icons.waving_hand, + color: Theme.of(context).colorScheme.primary, + ), + _buildPage( + title: S.of(context).onboardingPage2Title, + description: S.of(context).onboardingPage2Description, + icon: Icons.explore, + color: Theme.of(context).colorScheme.secondary, + ), + _buildPage( + title: S.of(context).onboardingPage3Title, + description: S.of(context).onboardingPage3Description, + icon: Icons.people, + color: Theme.of(context).colorScheme.tertiary, + ), + _buildPage( + title: S.of(context).onboardingPage4Title, + description: S.of(context).onboardingPage4Description, + icon: Icons.rocket_launch, + color: Theme.of(context).colorScheme.error, + ), + ], + ), ), - ), - - // Navigation Buttons - Padding( - padding: const EdgeInsets.all(Dimen.spacingL), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // Back Button (hidden on first page) - if (_currentPage > 0) - TextButton( - onPressed: _previousPage, - child: Text(S.of(context).onboardingBack), - ) - else - const SizedBox(width: 80), // Placeholder for alignment - - // Next/Start Button - ElevatedButton( - onPressed: _nextPage, - child: Text( - _currentPage == _totalPages - 1 - ? S.of(context).onboardingStart - : S.of(context).onboardingNext, + // Navigation Buttons + Padding( + padding: const EdgeInsets.all(Dimen.spacingL), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Back Button (hidden on first page) + if (_currentPage > 0) + TextButton( + onPressed: _previousPage, + child: Text(S.of(context).onboardingBack), + ) + else + const SizedBox(width: Dimen.spacingL), + // Next/Start Button + ElevatedButton( + onPressed: _nextPage, + child: Text( + _currentPage == _totalPages - 1 + ? S.of(context).onboardingStart + : S.of(context).onboardingNext, + ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), ); @@ -162,7 +185,7 @@ class _OnboardingPageState extends State { children: [ Icon( icon, - size: 120, + size: Dimen.onboardingIconSize, color: color, ), const SizedBox(height: Dimen.spacingXl),