import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/theme/app_typography.dart'; import '../../../../core/config/dev_config.dart'; import '../../../../core/constants/enums.dart'; import '../../../../core/constants/route_constants.dart'; import '../../../../core/constants/storage_keys.dart'; import '../../../../core/theme/theme_provider.dart'; import '../../../../core/utils/responsive_utils.dart'; import '../../../../core/utils/validators.dart'; import '../../../../shared/models/user_model.dart'; import '../../../../shared/providers/auth_provider.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/sidebar_logo.dart'; import '../../../settings/presentation/providers/settings_provider.dart'; import '../../data/repositories/auth_repository_impl.dart'; import '../widgets/login_colors.dart'; import '../widgets/login_hero_panel.dart'; class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key, this.resetToken}); /// Token from email deep link (`/reset-password?token=` or `/login?token=`). final String? resetToken; @override ConsumerState createState() => _LoginScreenState(); } enum _AuthBackFace { forgot, reset } class _LoginScreenState extends ConsumerState with SingleTickerProviderStateMixin { final _loginFormKey = GlobalKey(); final _forgotFormKey = GlobalKey(); final _resetFormKey = GlobalKey(); final _cardMeasureKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _forgotEmailController = TextEditingController(); final _newPasswordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); late final AnimationController _flipController; late final Animation _flipAnimation; bool _isLoading = false; bool _isForgotLoading = false; bool _isResetLoading = false; bool _obscurePassword = true; bool _obscureNewPassword = true; bool _obscureConfirmPassword = true; bool _rememberMe = false; String? _forgotSuccessMessage; String? _forgotErrorMessage; String? _resetErrorMessage; double? _cardHeight; _AuthBackFace _backFace = _AuthBackFace.forgot; String? _resetToken; bool get _showingBack => _flipAnimation.value > 0.5; @override void initState() { super.initState(); _flipController = AnimationController( vsync: this, duration: const Duration(milliseconds: 700), ); _flipAnimation = CurvedAnimation( parent: _flipController, curve: Curves.easeInOutCubic, reverseCurve: Curves.easeInOutCubic, ); final token = widget.resetToken?.trim(); if (token != null && token.isNotEmpty) { _resetToken = token; _backFace = _AuthBackFace.reset; _flipController.value = 1.0; } WidgetsBinding.instance.addPostFrameCallback((_) => _loadRememberedEmail()); } Future _loadRememberedEmail() async { final prefs = ref.read(sharedPreferencesProvider); final remember = prefs.getBool(StorageKeys.rememberMe) ?? false; final email = prefs.getString(StorageKeys.rememberedEmail); if (!mounted) return; setState(() { _rememberMe = remember; if (remember && email != null) { _emailController.text = email; } }); } Future _persistRememberMe() async { final prefs = ref.read(sharedPreferencesProvider); if (_rememberMe) { await prefs.setBool(StorageKeys.rememberMe, true); await prefs.setString( StorageKeys.rememberedEmail, _emailController.text.trim(), ); } else { await prefs.remove(StorageKeys.rememberMe); await prefs.remove(StorageKeys.rememberedEmail); } } @override void dispose() { _flipController.dispose(); _emailController.dispose(); _passwordController.dispose(); _forgotEmailController.dispose(); _newPasswordController.dispose(); _confirmPasswordController.dispose(); super.dispose(); } Future _login() async { if (!_loginFormKey.currentState!.validate()) return; setState(() => _isLoading = true); await _persistRememberMe(); final success = await ref.read(authStateProvider.notifier).login( LoginRequest( email: _emailController.text.trim(), password: _passwordController.text, ), ); setState(() => _isLoading = false); if (!mounted) return; if (success) { context.go(RouteConstants.dashboard); } else { final error = ref.read(authStateProvider).error; showAppToastFromSnackBar( context, SnackBar(content: Text(error ?? 'Invalid credentials')), ); } } Future _submitForgotPassword() async { if (!_forgotFormKey.currentState!.validate()) return; setState(() { _isForgotLoading = true; _forgotSuccessMessage = null; _forgotErrorMessage = null; }); final repository = ref.read(authRepositoryProvider); final result = await repository.forgotPassword( ForgotPasswordRequest(email: _forgotEmailController.text.trim()), ); if (!mounted) return; setState(() => _isForgotLoading = false); if (result.failure != null) { setState( () => _forgotErrorMessage = result.failure?.message ?? 'Unable to send reset email.', ); return; } setState( () => _forgotSuccessMessage = 'Password reset instructions have been sent to your email.', ); } Future _submitResetPassword() async { if (!_resetFormKey.currentState!.validate()) return; final token = _resetToken?.trim() ?? ''; if (token.isEmpty) { setState( () => _resetErrorMessage = 'This reset link is invalid or has expired. Request a new one.', ); return; } setState(() { _isResetLoading = true; _resetErrorMessage = null; }); final repository = ref.read(authRepositoryProvider); final result = await repository.resetPassword( ResetPasswordRequest( token: token, newPassword: _newPasswordController.text, confirmPassword: _confirmPasswordController.text, ), ); if (!mounted) return; setState(() => _isResetLoading = false); if (result.failure != null) { setState( () => _resetErrorMessage = result.failure?.message ?? 'Unable to reset password.', ); return; } // Clear reset form + token, then leave the deep-link route for a clean login. _newPasswordController.clear(); _confirmPasswordController.clear(); _resetToken = null; _resetFormKey.currentState?.reset(); setState(() { _resetErrorMessage = null; _backFace = _AuthBackFace.forgot; _obscureNewPassword = true; _obscureConfirmPassword = true; }); showAppToastFromSnackBar( context, const SnackBar( content: Text('Password reset successfully. Please sign in.'), ), ); if (!mounted) return; context.go(RouteConstants.login); } Future _ensureCardHeightLocked() async { if (_cardHeight != null) return; await Future.delayed(Duration.zero); if (!mounted) return; final box = _cardMeasureKey.currentContext?.findRenderObject() as RenderBox?; if (box != null && box.hasSize) { setState(() => _cardHeight = box.size.height); } } Future _flipToForgot() async { if (_flipController.isAnimating || _isLoading || _showingBack) return; if (_forgotEmailController.text.isEmpty && _emailController.text.trim().isNotEmpty) { _forgotEmailController.text = _emailController.text.trim(); } setState(() { _backFace = _AuthBackFace.forgot; _forgotSuccessMessage = null; _forgotErrorMessage = null; }); await _ensureCardHeightLocked(); await _flipController.forward(); } Future _flipToSignIn() async { if (_flipController.isAnimating || _isForgotLoading || _isResetLoading || !_showingBack) { return; } await _flipController.reverse(); if (!mounted) return; setState(() { _backFace = _AuthBackFace.forgot; _resetErrorMessage = null; }); if (widget.resetToken != null) { context.go(RouteConstants.login); } } InputDecoration _fieldDecoration({ required LoginColors colors, required String label, required IconData icon, Widget? suffix, }) { final radius = BorderRadius.circular(12); return InputDecoration( labelText: label, floatingLabelBehavior: FloatingLabelBehavior.auto, labelStyle: AppTypography.label2(color: colors.onSurfaceVariant), floatingLabelStyle: AppTypography.label3( weight: AppTypography.semiBold, color: colors.primary, ), prefixIcon: Icon(icon, color: colors.iconMuted, size: 18), suffixIcon: suffix, filled: true, fillColor: colors.fieldFillColor, contentPadding: const EdgeInsets.fromLTRB(12, 18, 12, 14), border: OutlineInputBorder( borderRadius: radius, borderSide: BorderSide(color: colors.outlineSoft, width: 1.5), ), enabledBorder: OutlineInputBorder( borderRadius: radius, borderSide: BorderSide(color: colors.outlineSoft, width: 1.5), ), focusedBorder: OutlineInputBorder( borderRadius: radius, borderSide: BorderSide(color: colors.primary, width: 1.5), ), errorBorder: OutlineInputBorder( borderRadius: radius, borderSide: BorderSide(color: colors.colorScheme.error, width: 1.5), ), focusedErrorBorder: OutlineInputBorder( borderRadius: radius, borderSide: BorderSide(color: colors.colorScheme.error, width: 1.5), ), ); } Widget _cardShell({ required LoginColors colors, required Widget child, }) { return Container( decoration: BoxDecoration( color: colors.surface.withValues(alpha: 0.92), borderRadius: BorderRadius.circular(28), border: Border.all(color: colors.outlineSoft), boxShadow: [ BoxShadow( color: colors.cardShadowColor, blurRadius: 32, offset: const Offset(0, 12), ), ], ), padding: const EdgeInsets.fromLTRB(36, 40, 36, 32), child: child, ); } Widget _buildLogo(String logoUrl, LoginColors colors) { return Center( child: DecoratedBox( decoration: BoxDecoration( // Light: blend into the card. Dark: keep white so the logo stays readable. color: colors.isDark ? Colors.white : colors.cardBackground, borderRadius: BorderRadius.circular(12), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), child: SidebarLogo( logoUrl: logoUrl, height: 64, width: 220, fit: BoxFit.contain, showBackground: false, ), ), ), ); } Widget _secureAccessFooter(LoginColors colors) { return Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Expanded(child: Divider(color: colors.outlineSoft, height: 1)), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Text( 'SECURE ACCESS', style: AppTypography.caption1(color: colors.onSurfaceVariant) .copyWith(letterSpacing: 0.6), ), ), Expanded(child: Divider(color: colors.outlineSoft, height: 1)), ], ), const SizedBox(height: 14), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.verified_user_outlined, size: 14, color: colors.success, ), const SizedBox(width: 6), Text( 'Your data is protected and encrypted', style: AppTypography.body4(color: colors.onSurfaceVariant), ), ], ), ], ); } Widget _backToSignInButton(LoginColors colors) { return Center( child: TextButton( onPressed: _flipToSignIn, style: TextButton.styleFrom( foregroundColor: colors.linkColor, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), ), child: Text( 'Back to Sign In', style: AppTypography.body3(weight: AppTypography.semiBold), ), ), ); } Widget _primaryButtonTheme({ required LoginColors colors, required Widget child, }) { return Theme( data: Theme.of(context).copyWith( elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: colors.primary, foregroundColor: colors.onPrimary, disabledBackgroundColor: colors.primary.withValues(alpha: 0.75), minimumSize: const Size(double.infinity, 52), elevation: 0, shadowColor: colors.primary.withValues(alpha: 0.35), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), textStyle: AppTypography.label1(weight: AppTypography.bold), ), ), ), child: child, ); } Widget _buildSignInFace({ required LoginColors colors, required String logoUrl, }) { return Form( key: _loginFormKey, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildLogo(logoUrl, colors), const SizedBox(height: 20), Text( 'Welcome', style: AppTypography.heading5( weight: AppTypography.bold, color: colors.headingColor, ), ), const SizedBox(height: 5), Text( 'Sign in to access your BCPL workspace.', style: AppTypography.body3(color: colors.subtitleColor), ), const SizedBox(height: 26), TextFormField( controller: _emailController, keyboardType: TextInputType.emailAddress, autofillHints: const [AutofillHints.email], validator: Validators.email, style: AppTypography.body3( weight: AppTypography.medium, color: colors.headingColor, ), decoration: _fieldDecoration( colors: colors, label: 'Email address', icon: Icons.mail_outline_rounded, ), ), const SizedBox(height: 18), TextFormField( controller: _passwordController, obscureText: _obscurePassword, autofillHints: const [AutofillHints.password], validator: (v) => Validators.required(v, fieldName: 'Password'), style: AppTypography.body3( weight: AppTypography.medium, color: colors.headingColor, ), decoration: _fieldDecoration( colors: colors, label: 'Password', icon: Icons.lock_outline_rounded, suffix: IconButton( tooltip: _obscurePassword ? 'Show password' : 'Hide password', icon: Icon( _obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: colors.iconMuted, size: 18, ), onPressed: () => setState(() => _obscurePassword = !_obscurePassword), ), ), ), const SizedBox(height: 10), Row( children: [ SizedBox( height: 34, width: 34, child: Checkbox( value: _rememberMe, activeColor: colors.primary, checkColor: colors.onPrimary, side: BorderSide(color: colors.outline, width: 1.5), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), onChanged: (v) => setState(() => _rememberMe = v ?? false), ), ), Text( 'Remember me', style: AppTypography.body3(color: colors.labelColor), ), const Spacer(), TextButton( onPressed: _flipToForgot, style: TextButton.styleFrom( foregroundColor: colors.linkColor, padding: const EdgeInsets.symmetric(horizontal: 4), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: Text( 'Forgot password?', style: AppTypography.body3( weight: AppTypography.semiBold, ), ), ), ], ), const SizedBox(height: 14), _primaryButtonTheme( colors: colors, child: AppButton( label: _isLoading ? 'Signing in…' : 'Sign in', isLoading: _isLoading, onPressed: _login, ), ), const SizedBox(height: 26), _secureAccessFooter(colors), if (DevConfig.screenPreviewEnabled) ...[ const SizedBox(height: 20), Divider(color: colors.outlineSoft), const SizedBox(height: 14), Text( 'Login API unavailable? Browse all screens without signing in:', textAlign: TextAlign.center, style: AppTypography.body4(color: colors.subtitleColor), ), const SizedBox(height: 12), Theme( data: Theme.of(context).copyWith( outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( foregroundColor: colors.primary, side: BorderSide(color: colors.outlineSoft), minimumSize: const Size(double.infinity, 46), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), child: AppButton( label: 'Explore All Screens', isOutlined: true, onPressed: () { ref.read(authStateProvider.notifier).loginAsDemo(); context.go(RouteConstants.screenGallery); }, ), ), ], ], ), ), ); } Widget _buildForgotFace({ required LoginColors colors, required String logoUrl, }) { final fillHeight = _cardHeight != null; return Form( key: _forgotFormKey, child: Column( mainAxisSize: fillHeight ? MainAxisSize.max : MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildLogo(logoUrl, colors), const SizedBox(height: 20), Text( 'Forgot Password', style: AppTypography.heading5( weight: AppTypography.bold, color: colors.headingColor, ), ), const SizedBox(height: 5), Text( 'Enter your registered email address to receive password reset instructions.', style: AppTypography.body3(color: colors.subtitleColor) .copyWith(height: 1.45), ), const SizedBox(height: 26), TextFormField( controller: _forgotEmailController, keyboardType: TextInputType.emailAddress, autofillHints: const [AutofillHints.email], validator: Validators.email, style: AppTypography.body3( weight: AppTypography.medium, color: colors.headingColor, ), decoration: _fieldDecoration( colors: colors, label: 'Email address', icon: Icons.mail_outline_rounded, ), ), if (_forgotErrorMessage != null) ...[ const SizedBox(height: 12), Text( _forgotErrorMessage!, style: AppTypography.body3(color: colors.colorScheme.error), ), ], if (_forgotSuccessMessage != null) ...[ const SizedBox(height: 12), Text( _forgotSuccessMessage!, style: AppTypography.body3(color: colors.primary), ), ], const SizedBox(height: 18), _primaryButtonTheme( colors: colors, child: AppButton( label: _isForgotLoading ? 'Sending…' : 'Send Reset Link', isLoading: _isForgotLoading, onPressed: _submitForgotPassword, ), ), if (fillHeight) const Spacer() else const SizedBox(height: 22), _backToSignInButton(colors), const SizedBox(height: 18), _secureAccessFooter(colors), ], ), ); } Widget _buildResetFace({ required LoginColors colors, required String logoUrl, }) { final fillHeight = _cardHeight != null; return Form( key: _resetFormKey, child: Column( mainAxisSize: fillHeight ? MainAxisSize.max : MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildLogo(logoUrl, colors), const SizedBox(height: 20), Text( 'Reset Password', style: AppTypography.heading5( weight: AppTypography.bold, color: colors.headingColor, ), ), const SizedBox(height: 5), Text( 'Choose a new password for your account.', style: AppTypography.body3(color: colors.subtitleColor) .copyWith(height: 1.45), ), const SizedBox(height: 26), TextFormField( controller: _newPasswordController, obscureText: _obscureNewPassword, autofillHints: const [AutofillHints.newPassword], validator: Validators.password, style: AppTypography.body3( weight: AppTypography.medium, color: colors.headingColor, ), decoration: _fieldDecoration( colors: colors, label: 'New password', icon: Icons.lock_outline_rounded, suffix: IconButton( tooltip: _obscureNewPassword ? 'Show password' : 'Hide password', icon: Icon( _obscureNewPassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: colors.iconMuted, size: 18, ), onPressed: () => setState( () => _obscureNewPassword = !_obscureNewPassword, ), ), ), ), const SizedBox(height: 18), TextFormField( controller: _confirmPasswordController, obscureText: _obscureConfirmPassword, autofillHints: const [AutofillHints.newPassword], validator: (v) { if (v != _newPasswordController.text) { return 'Passwords do not match'; } return Validators.password(v); }, style: AppTypography.body3( weight: AppTypography.medium, color: colors.headingColor, ), decoration: _fieldDecoration( colors: colors, label: 'Confirm password', icon: Icons.lock_outline_rounded, suffix: IconButton( tooltip: _obscureConfirmPassword ? 'Show password' : 'Hide password', icon: Icon( _obscureConfirmPassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: colors.iconMuted, size: 18, ), onPressed: () => setState( () => _obscureConfirmPassword = !_obscureConfirmPassword, ), ), ), ), if (_resetErrorMessage != null) ...[ const SizedBox(height: 12), Text( _resetErrorMessage!, style: AppTypography.body3(color: colors.colorScheme.error), ), ], if ((_resetToken ?? '').isEmpty) ...[ const SizedBox(height: 12), Text( 'This reset link is invalid or has expired. Request a new one from Forgot Password.', style: AppTypography.body3(color: colors.colorScheme.error), ), ], const SizedBox(height: 18), _primaryButtonTheme( colors: colors, child: AppButton( label: _isResetLoading ? 'Updating…' : 'Reset Password', isLoading: _isResetLoading, onPressed: _submitResetPassword, ), ), if (fillHeight) const Spacer() else const SizedBox(height: 22), _backToSignInButton(colors), const SizedBox(height: 18), _secureAccessFooter(colors), ], ), ); } void _scheduleCardHeightCapture() { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted || _flipController.value > 0) return; final box = _cardMeasureKey.currentContext?.findRenderObject() as RenderBox?; if (box == null || !box.hasSize) return; final height = box.size.height; if (_cardHeight == null || (_cardHeight! - height).abs() > 1) { setState(() => _cardHeight = height); } }); } Widget _buildAuthCard(LoginColors colors) { final companyProfile = ref.watch(appSettingsProvider).companyProfile; final branding = ref.watch(brandingProvider); final logoUrl = resolveSidebarLogoUrl( companyProfileLogo: companyProfile.logoUrl, brandingLogo: branding.logoUrl, ); if (_flipController.isDismissed) { _scheduleCardHeightCapture(); } return ConstrainedBox( constraints: const BoxConstraints(maxWidth: 420), child: AnimatedBuilder( animation: _flipAnimation, builder: (context, _) { final t = _flipAnimation.value; final angle = t * math.pi; final showFront = angle <= math.pi / 2; // Soft mid-flip fade for a premium feel. final fade = 0.82 + 0.18 * math.cos(angle * 2).abs(); final front = KeyedSubtree( key: _cardMeasureKey, child: _cardShell( colors: colors, child: _buildSignInFace(colors: colors, logoUrl: logoUrl), ), ); final backChild = _backFace == _AuthBackFace.reset ? _buildResetFace(colors: colors, logoUrl: logoUrl) : _buildForgotFace(colors: colors, logoUrl: logoUrl); final back = _cardShell( colors: colors, child: _cardHeight != null ? SizedBox.expand(child: backChild) : backChild, ); final face = showFront ? front : Transform( alignment: Alignment.center, transform: Matrix4.identity()..rotateY(math.pi), child: back, ); final flipped = Opacity( opacity: fade.clamp(0.82, 1.0), child: Transform( alignment: Alignment.center, transform: Matrix4.identity() ..setEntry(3, 2, 0.00115) ..rotateY(angle), child: face, ), ); if (_cardHeight == null) return flipped; // Lock height only while flipping / on the back face so the two // faces match. Keep the front face unconstrained so validation // errors can expand without overflowing. final lockHeight = _flipController.isAnimating || !_flipController.isDismissed; if (!lockHeight) return flipped; return SizedBox( height: _cardHeight, width: double.infinity, child: flipped, ); }, ), ); } Widget _buildThemeToggle(LoginColors colors) { final mode = ref.watch(themeModeProvider); final isDarkActive = Theme.of(context).brightness == Brightness.dark || mode == ThemeModeOption.dark; return Material( color: colors.surface, shape: StadiumBorder( side: BorderSide(color: colors.outlineSoft), ), elevation: 1, shadowColor: colors.cardShadowColor, child: Padding( padding: const EdgeInsets.all(6), child: Row( mainAxisSize: MainAxisSize.min, children: [ _ThemeChip( icon: Icons.light_mode_outlined, active: !isDarkActive, colors: colors, onTap: () => ref .read(themeModeProvider.notifier) .setThemeMode(ThemeModeOption.light), ), const SizedBox(width: 4), _ThemeChip( icon: Icons.dark_mode_outlined, active: isDarkActive, colors: colors, onTap: () => ref .read(themeModeProvider.notifier) .setThemeMode(ThemeModeOption.dark), ), ], ), ), ); } Widget _buildAmbient(LoginColors colors) { return Stack( children: [ Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [colors.bgGradA, colors.bg], ), ), ), ), Positioned( top: -120, right: -100, child: _Blob(color: colors.primaryContainer, size: 420), ), Positioned( bottom: -140, left: -120, child: _Blob(color: colors.tertiaryContainer, size: 360), ), ], ); } @override Widget build(BuildContext context) { final isWide = context.isDesktop; final colors = LoginColors.of(context); return Scaffold( backgroundColor: colors.bg, body: Stack( children: [ if (isWide) Row( children: [ const Expanded(flex: 105, child: LoginHeroPanel()), Expanded( flex: 100, child: Stack( children: [ Positioned.fill(child: _buildAmbient(colors)), Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric( horizontal: 32, vertical: 40, ), child: _buildAuthCard(colors), ), ), ], ), ), ], ) else Stack( children: [ Positioned.fill(child: _buildAmbient(colors)), SingleChildScrollView( child: Column( children: [ const SizedBox( height: 340, width: double.infinity, child: LoginHeroPanel(compact: true), ), Padding( padding: const EdgeInsets.fromLTRB(16, 24, 16, 40), child: Center(child: _buildAuthCard(colors)), ), ], ), ), ], ), Positioned( top: 22, right: 22, child: SafeArea(child: _buildThemeToggle(colors)), ), ], ), ); } } class _ThemeChip extends StatelessWidget { const _ThemeChip({ required this.icon, required this.active, required this.colors, required this.onTap, }); final IconData icon; final bool active; final LoginColors colors; final VoidCallback onTap; @override Widget build(BuildContext context) { return Material( color: active ? colors.primary : Colors.transparent, shape: const CircleBorder(), child: InkWell( customBorder: const CircleBorder(), onTap: onTap, child: SizedBox( width: 34, height: 34, child: Icon( icon, size: 17, color: active ? colors.onPrimary : colors.onSurfaceVariant, ), ), ), ); } } class _Blob extends StatelessWidget { const _Blob({required this.color, required this.size}); final Color color; final double size; @override Widget build(BuildContext context) { return IgnorePointer( child: Container( width: size, height: size, decoration: BoxDecoration( shape: BoxShape.circle, color: color.withValues(alpha: 0.35), ), ), ); } }