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:
@@ -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],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user