diff --git a/lib/pantallas/pantalla_inicio.dart b/lib/pantallas/pantalla_inicio.dart index cca7819..679a315 100644 --- a/lib/pantallas/pantalla_inicio.dart +++ b/lib/pantallas/pantalla_inicio.dart @@ -226,6 +226,9 @@ class _EscucharHero extends StatelessWidget { 0, ), child: PluriGlassSurface( + // S3 (Tier 1 visual fidelity): the active/now-playing card — the + // system rule's other named exception to the opaque default. + glass: true, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/tema/pluriwave_tokens.dart b/lib/tema/pluriwave_tokens.dart index 963193d..7cae4cd 100644 --- a/lib/tema/pluriwave_tokens.dart +++ b/lib/tema/pluriwave_tokens.dart @@ -54,6 +54,11 @@ class PluriWaveTokens extends ThemeExtension { final Color balloonSurface; final double radiusSm; + + /// S4 (Tier 1 visual fidelity): the prototype's dominant CARD radius — + /// `t4` lines 512, 613, 715, 133 (Ajustes group, storage card, welcome + /// CTA, tray tiles). Was 22, a systematic +4px drift versus every card + /// that defaults to this token (`PluriGlassSurface.borderRadius`). final double radiusMd; final double radiusLg; @@ -84,7 +89,7 @@ class PluriWaveTokens extends ThemeExtension { offlineAccent: Color(0xFFE8879A), balloonSurface: Color(0xFF0A1B24), radiusSm: 14, - radiusMd: 22, + radiusMd: 18, radiusLg: 30, spacingXs: 4, spacingSm: 8, @@ -154,8 +159,7 @@ class PluriWaveTokens extends ThemeExtension { offlineAccent: Color.lerp(offlineAccent, other.offlineAccent, t) ?? offlineAccent, balloonSurface: - Color.lerp(balloonSurface, other.balloonSurface, t) ?? - balloonSurface, + Color.lerp(balloonSurface, other.balloonSurface, t) ?? balloonSurface, radiusSm: lerpDouble(radiusSm, other.radiusSm, t) ?? radiusSm, radiusMd: lerpDouble(radiusMd, other.radiusMd, t) ?? radiusMd, radiusLg: lerpDouble(radiusLg, other.radiusLg, t) ?? radiusLg, diff --git a/lib/widgets/mini_reproductor.dart b/lib/widgets/mini_reproductor.dart index b9feac6..eeaac5d 100644 --- a/lib/widgets/mini_reproductor.dart +++ b/lib/widgets/mini_reproductor.dart @@ -72,6 +72,9 @@ class _MiniReproductorState extends State { t.spacingSm, ), child: PluriGlassSurface( + // S3 (Tier 1 visual fidelity): chrome — one of the system rule's + // two named exceptions to the opaque list-surface default. + glass: true, padding: EdgeInsets.symmetric( horizontal: t.spacingSm, vertical: t.spacingXs, diff --git a/lib/widgets/pluri_glass_surface.dart b/lib/widgets/pluri_glass_surface.dart index 6465a2e..6151d7d 100644 --- a/lib/widgets/pluri_glass_surface.dart +++ b/lib/widgets/pluri_glass_surface.dart @@ -4,6 +4,15 @@ import 'package:flutter/material.dart'; import '../tema/pluriwave_theme.dart'; +/// S3+S4 (Tier 1 visual fidelity): the prototype's own system rule (`t4` +/// line 40, verbatim) — "Superficie de lista opaca #102532, cristal solo en +/// el cromo y en la tarjeta activa" ("Opaque list surface #102532, glass +/// only in the chrome and in the active card"). This primitive backs nearly +/// every card/row in the app, so [glass] defaults to `false`: an OPAQUE +/// [PluriWaveTokens.listSurface] fill, no blur. Pass `glass: true` for the +/// rule's two named exceptions — chrome (e.g. `MiniReproductor`) and the +/// active/now-playing card (Escuchar's `_EscucharHero`) — to keep the +/// original translucent, blurred look there. class PluriGlassSurface extends StatelessWidget { const PluriGlassSurface({ super.key, @@ -12,6 +21,7 @@ class PluriGlassSurface extends StatelessWidget { this.borderRadius, this.blurSigma = 18, this.glowColor, + this.glass = false, }); final Widget child; @@ -20,31 +30,47 @@ class PluriGlassSurface extends StatelessWidget { final double blurSigma; final Color? glowColor; + /// See class doc — `false` (opaque `listSurface`) is the system default. + final bool glass; + @override Widget build(BuildContext context) { final t = context.pluriTokens; final radius = borderRadius ?? BorderRadius.circular(t.radiusMd); + final decoratedChild = DecoratedBox( + decoration: BoxDecoration( + color: glass ? t.glassSurface : t.listSurface, + borderRadius: radius, + border: Border.all( + color: glass ? t.glassBorder : Colors.white.withValues(alpha: 0.07), + ), + boxShadow: + glass + ? [ + BoxShadow( + color: glowColor ?? t.glowColor.withValues(alpha: 0.12), + blurRadius: 30, + spreadRadius: -14, + offset: const Offset(0, 18), + ), + ] + : null, + ), + child: Padding(padding: padding, child: child), + ); + + if (!glass) { + return RepaintBoundary( + child: ClipRRect(borderRadius: radius, child: decoratedChild), + ); + } + return RepaintBoundary( child: ClipRRect( borderRadius: radius, child: BackdropFilter( filter: ImageFilter.blur(sigmaX: blurSigma, sigmaY: blurSigma), - child: DecoratedBox( - decoration: BoxDecoration( - color: t.glassSurface, - borderRadius: radius, - border: Border.all(color: t.glassBorder), - boxShadow: [ - BoxShadow( - color: glowColor ?? t.glowColor.withValues(alpha: 0.12), - blurRadius: 30, - spreadRadius: -14, - offset: const Offset(0, 18), - ), - ], - ), - child: Padding(padding: padding, child: child), - ), + child: decoratedChild, ), ), ); diff --git a/lib/widgets/tarjeta_emisora.dart b/lib/widgets/tarjeta_emisora.dart index 1e3e04f..9e26236 100644 --- a/lib/widgets/tarjeta_emisora.dart +++ b/lib/widgets/tarjeta_emisora.dart @@ -65,8 +65,11 @@ class _TarjetaEmisoraState extends State { label: l10n.stationSemanticLabel(stationName), child: PluriGlassSurface( padding: EdgeInsets.zero, + // S4 (Tier 1 visual fidelity): compact (esCompacta) is a flat-list + // ROW — prototype dominant row radius 14 (t4 lines 175, 227, 302). + // The full grid card keeps the dominant CARD radius, 18. borderRadius: BorderRadius.circular( - widget.esCompacta ? t.radiusMd : t.radiusLg, + widget.esCompacta ? t.radiusSm : t.radiusMd, ), child: Material( color: Colors.transparent, diff --git a/test/tema/pluriwave_tokens_test.dart b/test/tema/pluriwave_tokens_test.dart index b7ed267..82b2156 100644 --- a/test/tema/pluriwave_tokens_test.dart +++ b/test/tema/pluriwave_tokens_test.dart @@ -12,7 +12,10 @@ void main() { group('PluriWaveTokens — redesign colours', () { test('dark keeps the pre-existing base palette', () { expect(PluriWaveTokens.dark.deepViolet, const Color(0xFF07121A)); - expect(PluriWaveTokens.dark.radiusMd, 22); + // S4 (Tier 1 visual fidelity): the prototype's dominant card radius + // is 18 (t4 lines 512, 613, 715, 133) — radiusMd used to be 22, a + // systematic +4px drift across every card that defaults to it. + expect(PluriWaveTokens.dark.radiusMd, 18); expect(PluriWaveTokens.dark.spacingMd, 16); }); diff --git a/test/widgets/pluri_glass_surface_test.dart b/test/widgets/pluri_glass_surface_test.dart new file mode 100644 index 0000000..e1220f1 --- /dev/null +++ b/test/widgets/pluri_glass_surface_test.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pluriwave/tema/pluriwave_theme.dart'; +import 'package:pluriwave/tema/pluriwave_tokens.dart'; +import 'package:pluriwave/widgets/pluri_glass_surface.dart'; + +/// S3+S4 (Tier 1 visual fidelity): the prototype's own system rule (`t4` +/// line 40, verbatim) — "Superficie de lista opaca #102532, cristal solo en +/// el cromo y en la tarjeta activa" ("Opaque list surface #102532, glass +/// only in the chrome and in the active card"). [PluriGlassSurface] is used +/// for nearly every card/row in the app, so its OWN default must be the +/// opaque fill; only callers that are chrome or the active/now-playing card +/// opt into the old blurred look via `glass: true`. +void main() { + Widget host(Widget child) { + return MaterialApp( + theme: PluriWaveTheme.dark(), + home: Scaffold(body: child), + ); + } + + BoxDecoration decorationOf(WidgetTester tester) { + final box = tester.widget(find.byType(DecoratedBox).first); + return box.decoration as BoxDecoration; + } + + testWidgets('defaults to an OPAQUE listSurface fill with no backdrop blur', ( + tester, + ) async { + await tester.pumpWidget(host(const PluriGlassSurface(child: Text('row')))); + + final decoration = decorationOf(tester); + expect(decoration.color, PluriWaveTokens.dark.listSurface); + expect(decoration.color!.a, 1.0, reason: 'must be fully opaque'); + expect(find.byType(BackdropFilter), findsNothing); + }); + + testWidgets('defaults its border radius to radiusMd (18 — the ' + "prototype's dominant card radius)", (tester) async { + await tester.pumpWidget(host(const PluriGlassSurface(child: Text('row')))); + + final clip = tester.widget(find.byType(ClipRRect).first); + expect( + clip.borderRadius, + BorderRadius.circular(PluriWaveTokens.dark.radiusMd), + ); + expect(PluriWaveTokens.dark.radiusMd, 18); + }); + + testWidgets('glass:true keeps the old translucent, blurred chrome look', ( + tester, + ) async { + await tester.pumpWidget( + host(const PluriGlassSurface(glass: true, child: Text('chrome'))), + ); + + final decoration = decorationOf(tester); + expect(decoration.color, PluriWaveTokens.dark.glassSurface); + expect(find.byType(BackdropFilter), findsOneWidget); + }); +} diff --git a/test/widgets/pluriwave_foundations_test.dart b/test/widgets/pluriwave_foundations_test.dart index 4b05fbf..08a545f 100644 --- a/test/widgets/pluriwave_foundations_test.dart +++ b/test/widgets/pluriwave_foundations_test.dart @@ -9,7 +9,10 @@ import 'package:pluriwave/widgets/pluri_wave_scaffold.dart'; void main() { test('PluriWaveTokens.dark mantiene valores base esperados', () { expect(PluriWaveTokens.dark.deepViolet, const Color(0xFF07121A)); - expect(PluriWaveTokens.dark.radiusMd, 22); + // S4 (Tier 1 visual fidelity): the prototype's dominant card radius is + // 18 (t4 lines 512, 613, 715, 133) — radiusMd used to be 22, a + // systematic +4px drift across every card that defaults to it. + expect(PluriWaveTokens.dark.radiusMd, 18); expect(PluriWaveTokens.dark.spacingMd, 16); }); diff --git a/test/widgets/tarjeta_emisora_surface_test.dart b/test/widgets/tarjeta_emisora_surface_test.dart new file mode 100644 index 0000000..fc595d8 --- /dev/null +++ b/test/widgets/tarjeta_emisora_surface_test.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pluriwave/estado/estado_radio.dart'; +import 'package:pluriwave/l10n/gen/app_localizations.dart'; +import 'package:pluriwave/tema/pluriwave_tokens.dart'; +import 'package:pluriwave/widgets/pluri_glass_surface.dart'; +import 'package:pluriwave/widgets/tarjeta_emisora.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../helpers/fakes.dart'; + +/// S4 (Tier 1 visual fidelity): the prototype's dominant ROW radius is 14 +/// (t4 lines 175, 227, 302 — Favoritos/Buscar/results rows, all rendered by +/// `TarjetaEmisora(esCompacta: true)`), its dominant CARD radius is 18 +/// (lines 155-160 — "Cerca de ti", rendered by the non-compact grid card). +void main() { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + Widget host(Widget child) { + final estado = EstadoRadio( + audio: FakeServicioAudio(), + favoritos: FakeServicioFavoritos(), + radio: FakeServicioRadio(), + servicioEcualizador: FakeServicioEcualizador(), + iniciarAutomaticamente: false, + ); + addTearDown(estado.dispose); + return ChangeNotifierProvider.value( + value: estado, + child: MaterialApp( + locale: const Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold(body: Center(child: child)), + ), + ); + } + + BorderRadius radiusOf(WidgetTester tester) { + final surface = tester.widget( + find.byType(PluriGlassSurface), + ); + return surface.borderRadius as BorderRadius; + } + + testWidgets('esCompacta:true (row) uses radiusSm — 14', (tester) async { + await tester.pumpWidget( + host( + SizedBox( + width: 320, + child: TarjetaEmisora( + emisora: emisoraDemo(uuid: 'row', nombre: 'Row FM'), + esCompacta: true, + ), + ), + ), + ); + await tester.pump(); + + expect( + radiusOf(tester), + BorderRadius.circular(PluriWaveTokens.dark.radiusSm), + ); + expect(PluriWaveTokens.dark.radiusSm, 14); + }); + + testWidgets('esCompacta:false (card) uses radiusMd — 18', (tester) async { + await tester.pumpWidget( + host( + SizedBox( + width: 220, + height: 320, + child: TarjetaEmisora( + emisora: emisoraDemo(uuid: 'card', nombre: 'Card FM'), + ), + ), + ), + ); + await tester.pump(); + + expect( + radiusOf(tester), + BorderRadius.circular(PluriWaveTokens.dark.radiusMd), + ); + expect(PluriWaveTokens.dark.radiusMd, 18); + }); +}