From e94e64faca0582aa82efb85f83f8948a3ca61a51 Mon Sep 17 00:00:00 2001 From: freetlab Date: Wed, 29 Jul 2026 23:41:35 +0200 Subject: [PATCH] fix(bienvenida): bottom-anchor content and add the blurred backdrop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit 14.2 (t4 lines 693/706): content was top-anchored in a plain SingleChildScrollView; the prototype anchors it to the bottom with two flexible spacers, so the CTA sits at the screen edge. Restructured as a scrollable Expanded region (safe on short screens / large text scale) with the CTA pinned below it, always at the bottom. Audit 14.1 (t4 lines 687-689): added the full-bleed blurred banner backdrop, reusing aurora_wave_banner.png (the same asset the deleted PluriScreenHeader used) since this app has no bundled equivalent of the prototype's mockup-only banner.jpg. No monetization content touched (PRO pill, "14 días", pricing card, free-version link all remain absent per the binding no-monetization decision). --- lib/pantallas/pantalla_bienvenida.dart | 212 +++++++++++++------ test/pantallas/pantalla_bienvenida_test.dart | 62 +++++- 2 files changed, 200 insertions(+), 74 deletions(-) diff --git a/lib/pantallas/pantalla_bienvenida.dart b/lib/pantallas/pantalla_bienvenida.dart index c95ff21..9a83e62 100644 --- a/lib/pantallas/pantalla_bienvenida.dart +++ b/lib/pantallas/pantalla_bienvenida.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -47,78 +49,103 @@ class PantallaBienvenida extends StatelessWidget { final theme = Theme.of(context); return Scaffold( - body: SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(24, 48, 24, 28), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Image.asset( - 'assets/icons/pluriwave_app_mark.png', - width: 76, - height: 76, - errorBuilder: - (_, __, ___) => Icon( - Icons.graphic_eq_rounded, - size: 76, - color: t.electricMagenta, - ), - ), - const SizedBox(height: 22), - Text( - l10n.welcomeHeadline, - style: theme.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.w800, - height: 1.05, - letterSpacing: -1.0, - ), - ), - const SizedBox(height: 12), - Text( - l10n.welcomeBody, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withValues(alpha: 0.72), - height: 1.5, - ), - ), - const SizedBox(height: 28), - FilaCaracteristicaBienvenida( - icon: Icons.equalizer_rounded, - iconColor: t.electricMagenta, - titulo: l10n.welcomeBullet1Title, - subtitulo: l10n.welcomeBullet1Subtitle, - ), - const SizedBox(height: 16), - FilaCaracteristicaBienvenida( - icon: Icons.directions_car_rounded, - iconColor: t.liveGreen, - titulo: l10n.welcomeBullet2Title, - subtitulo: l10n.welcomeBullet2Subtitle, - ), - const SizedBox(height: 16), - FilaCaracteristicaBienvenida( - icon: Icons.alarm_rounded, - iconColor: t.warmCoral, - titulo: l10n.welcomeBullet3Title, - subtitulo: l10n.welcomeBullet3Subtitle, - ), - const SizedBox(height: 32), - SizedBox( - height: 58, - child: FilledButton( - onPressed: () => _empezar(context), - child: Text( - l10n.welcomeCtaLabel, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w800, + body: Stack( + fit: StackFit.expand, + children: [ + const _FondoBienvenida(), + SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 28), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Audit 14.2 (t4 lines 693/706): the prototype anchors + // this content to the BOTTOM with two flexible spacers, + // so the CTA sits at the screen edge. The scrollable + // Expanded region above the CTA keeps this safe on short + // screens / large text scale instead of a rigid Spacer + // that could overflow. + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.only(top: 48), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Image.asset( + 'assets/icons/pluriwave_app_mark.png', + key: const ValueKey('welcome-logo'), + width: 76, + height: 76, + errorBuilder: + (_, __, ___) => Icon( + Icons.graphic_eq_rounded, + size: 76, + color: t.electricMagenta, + ), + ), + const SizedBox(height: 22), + Text( + l10n.welcomeHeadline, + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w800, + height: 1.05, + letterSpacing: -1.0, + ), + ), + const SizedBox(height: 12), + Text( + l10n.welcomeBody, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withValues( + alpha: 0.72, + ), + height: 1.5, + ), + ), + const SizedBox(height: 28), + FilaCaracteristicaBienvenida( + icon: Icons.equalizer_rounded, + iconColor: t.electricMagenta, + titulo: l10n.welcomeBullet1Title, + subtitulo: l10n.welcomeBullet1Subtitle, + ), + const SizedBox(height: 16), + FilaCaracteristicaBienvenida( + icon: Icons.directions_car_rounded, + iconColor: t.liveGreen, + titulo: l10n.welcomeBullet2Title, + subtitulo: l10n.welcomeBullet2Subtitle, + ), + const SizedBox(height: 16), + FilaCaracteristicaBienvenida( + icon: Icons.alarm_rounded, + iconColor: t.warmCoral, + titulo: l10n.welcomeBullet3Title, + subtitulo: l10n.welcomeBullet3Subtitle, + ), + ], + ), ), ), - ), + const SizedBox(height: 24), + SizedBox( + height: 58, + child: FilledButton( + onPressed: () => _empezar(context), + child: Text( + l10n.welcomeCtaLabel, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + ), + ), + ), + ), + ], ), - ], + ), ), - ), + ], ), ); } @@ -186,3 +213,52 @@ class FilaCaracteristicaBienvenida extends StatelessWidget { ); } } + +/// Full-bleed blurred backdrop (audit 14.1, t4 lines 687-689). Reuses the +/// bundled `aurora_wave_banner.png` — the same asset the now-deleted +/// `PluriScreenHeader` used (Tier 1, S2) — rather than the prototype's own +/// mockup-only `assets/banner.jpg`, which this app does not bundle. Mirrors +/// `_FondoArteDifuminado` in `pantalla_alarma_sonando.dart`: blur + a +/// deepViolet gradient, no noise layer, purely decorative and not +/// spec-tested beyond its own presence. +class _FondoBienvenida extends StatelessWidget { + const _FondoBienvenida(); + + @override + Widget build(BuildContext context) { + final tokens = context.pluriTokens; + return Positioned.fill( + key: const ValueKey('welcome-background-art'), + child: Stack( + fit: StackFit.expand, + children: [ + ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), + child: Opacity( + opacity: 0.4, + child: Image.asset( + 'assets/images/aurora_wave_banner.png', + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const SizedBox.shrink(), + ), + ), + ), + DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + tokens.deepViolet.withValues(alpha: 0.45), + tokens.deepViolet.withValues(alpha: 0.9), + tokens.deepViolet, + ], + stops: const [0, 0.46, 1], + ), + ), + ), + ], + ), + ); + } +} diff --git a/test/pantallas/pantalla_bienvenida_test.dart b/test/pantallas/pantalla_bienvenida_test.dart index e005310..1bbaa3d 100644 --- a/test/pantallas/pantalla_bienvenida_test.dart +++ b/test/pantallas/pantalla_bienvenida_test.dart @@ -39,7 +39,13 @@ void main() { expect(find.byType(AlertDialog), findsNothing); expect(find.byType(Scaffold), findsOneWidget); - expect(find.byType(Image), findsOneWidget, reason: 'the logo mark'); + // Audit 14.1 adds a second Image (the blurred backdrop), so "the + // logo mark" is now asserted by key rather than "the only Image". + expect( + find.byKey(const ValueKey('welcome-logo')), + findsOneWidget, + reason: 'the logo mark', + ); expect(find.text('Tu mundo, en directo'), findsOneWidget); expect( find.textContaining('53.412 emisoras de 238 países'), @@ -77,9 +83,7 @@ void main() { expect(textoCompleto.contains('€'), isFalse); expect(textoCompleto.contains(r'$'), isFalse); expect( - RegExp(r'\d+\s*d[ií]as?\b', caseSensitive: false).hasMatch( - textoCompleto, - ), + RegExp(r'\d+\s*d[ií]as?\b', caseSensitive: false).hasMatch(textoCompleto), isFalse, reason: 'no day-count trial phrase (e.g. "14 días")', ); @@ -103,8 +107,7 @@ void main() { (tester) async { // Start on a DIFFERENT tab so a pass actually proves irA() ran, // rather than coasting on EstadoNavegacionRaiz's own escuchar default. - final navegacion = EstadoNavegacionRaiz() - ..irA(RaizPluriWave.ajustes); + final navegacion = EstadoNavegacionRaiz()..irA(RaizPluriWave.ajustes); addTearDown(navegacion.dispose); final navigatorKey = GlobalKey(); @@ -141,4 +144,51 @@ void main() { expect(find.text('home stand-in'), findsOneWidget); }, ); + + testWidgets('visual fidelity (audit 14.1, proto t4 lines 687-689): a blurred ' + 'banner backdrop is present behind the content', (tester) async { + final navegacion = EstadoNavegacionRaiz(); + addTearDown(navegacion.dispose); + + await tester.pumpWidget(_app(navegacion: navegacion)); + await tester.pumpAndSettle(); + + expect( + find.byKey(const ValueKey('welcome-background-art')), + findsOneWidget, + reason: + 'the prototype draws a blurred, 40%-opacity banner image ' + 'full-bleed behind the content — this screen used to be a ' + 'plain Scaffold with no backdrop at all', + ); + }); + + testWidgets( + 'visual fidelity (audit 14.2, proto t4 lines 693/706): the primary CTA ' + 'is anchored to the bottom edge, not immediately after the bullets', + (tester) async { + final navegacion = EstadoNavegacionRaiz(); + addTearDown(navegacion.dispose); + tester.view.physicalSize = const Size(1440, 3200); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget(_app(navegacion: navegacion)); + await tester.pumpAndSettle(); + + final alturaPantalla = tester.view.physicalSize.height; + final ctaAbajo = tester.getBottomLeft(find.byType(FilledButton)).dy; + + expect( + ctaAbajo, + greaterThan(alturaPantalla - 100), + reason: + 'the prototype anchors content to the bottom with two ' + 'flexible spacers (lines 693, 706) so the CTA sits at the ' + 'screen edge — it used to render immediately after the top ' + 'padding instead', + ); + }, + ); }