fix(bienvenida): bottom-anchor content and add the blurred backdrop

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).
This commit is contained in:
2026-07-29 23:41:35 +02:00
parent 684e8e8ff9
commit e94e64faca
2 changed files with 200 additions and 74 deletions
+80 -4
View File
@@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -47,14 +49,31 @@ class PantallaBienvenida extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
return Scaffold( return Scaffold(
body: SafeArea( 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( child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(24, 48, 24, 28), padding: const EdgeInsets.only(top: 48),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Image.asset( Image.asset(
'assets/icons/pluriwave_app_mark.png', 'assets/icons/pluriwave_app_mark.png',
key: const ValueKey('welcome-logo'),
width: 76, width: 76,
height: 76, height: 76,
errorBuilder: errorBuilder:
@@ -77,7 +96,9 @@ class PantallaBienvenida extends StatelessWidget {
Text( Text(
l10n.welcomeBody, l10n.welcomeBody,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.72), color: theme.colorScheme.onSurface.withValues(
alpha: 0.72,
),
height: 1.5, height: 1.5,
), ),
), ),
@@ -102,7 +123,11 @@ class PantallaBienvenida extends StatelessWidget {
titulo: l10n.welcomeBullet3Title, titulo: l10n.welcomeBullet3Title,
subtitulo: l10n.welcomeBullet3Subtitle, subtitulo: l10n.welcomeBullet3Subtitle,
), ),
const SizedBox(height: 32), ],
),
),
),
const SizedBox(height: 24),
SizedBox( SizedBox(
height: 58, height: 58,
child: FilledButton( child: FilledButton(
@@ -120,6 +145,8 @@ class PantallaBienvenida extends StatelessWidget {
), ),
), ),
), ),
],
),
); );
} }
@@ -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],
),
),
),
],
),
);
}
}
+56 -6
View File
@@ -39,7 +39,13 @@ void main() {
expect(find.byType(AlertDialog), findsNothing); expect(find.byType(AlertDialog), findsNothing);
expect(find.byType(Scaffold), findsOneWidget); 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.text('Tu mundo, en directo'), findsOneWidget);
expect( expect(
find.textContaining('53.412 emisoras de 238 países'), find.textContaining('53.412 emisoras de 238 países'),
@@ -77,9 +83,7 @@ void main() {
expect(textoCompleto.contains(''), isFalse); expect(textoCompleto.contains(''), isFalse);
expect(textoCompleto.contains(r'$'), isFalse); expect(textoCompleto.contains(r'$'), isFalse);
expect( expect(
RegExp(r'\d+\s*d[ií]as?\b', caseSensitive: false).hasMatch( RegExp(r'\d+\s*d[ií]as?\b', caseSensitive: false).hasMatch(textoCompleto),
textoCompleto,
),
isFalse, isFalse,
reason: 'no day-count trial phrase (e.g. "14 días")', reason: 'no day-count trial phrase (e.g. "14 días")',
); );
@@ -103,8 +107,7 @@ void main() {
(tester) async { (tester) async {
// Start on a DIFFERENT tab so a pass actually proves irA() ran, // Start on a DIFFERENT tab so a pass actually proves irA() ran,
// rather than coasting on EstadoNavegacionRaiz's own escuchar default. // rather than coasting on EstadoNavegacionRaiz's own escuchar default.
final navegacion = EstadoNavegacionRaiz() final navegacion = EstadoNavegacionRaiz()..irA(RaizPluriWave.ajustes);
..irA(RaizPluriWave.ajustes);
addTearDown(navegacion.dispose); addTearDown(navegacion.dispose);
final navigatorKey = GlobalKey<NavigatorState>(); final navigatorKey = GlobalKey<NavigatorState>();
@@ -141,4 +144,51 @@ void main() {
expect(find.text('home stand-in'), findsOneWidget); 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',
);
},
);
} }