fix(surfaces): make list/card surfaces opaque by default, fix card radius

The prototype states an explicit system rule (t4 line 40): "opaque list
surface #102532, glass only in the chrome and in the active card."
PluriGlassSurface backs nearly every card/row in the app, but always
rendered translucent + blurred (glassSurface, blur 18) — the listSurface
token (#102532) existed since WU1 but was only ever used at reduced
alpha, never opaquely.

Add a `glass` flag to PluriGlassSurface, defaulting to false: an opaque
listSurface fill with no BackdropFilter. Chrome (MiniReproductor) and
the active/now-playing card (Escuchar's hero, when a station is
playing) opt into the old translucent look via `glass: true` — every
other of the ~26 call sites needs no change and now renders opaquely.

Also fix the card radius: the prototype's dominant card radius is 18
(t4 lines 512, 613, 715, 133); radiusMd was 22, a systematic +4px drift
across every card that defaults to it. Retune TarjetaEmisora's own
radius ternary so its compact (row) variant uses the dominant row
radius, 14, instead of inheriting the card radius.

S3+S4, Tier 1 visual-fidelity pass (audit id 2521).
This commit is contained in:
2026-07-29 20:23:55 +02:00
parent 78a7415dd8
commit 5a5f1655a9
9 changed files with 218 additions and 22 deletions
@@ -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<DecoratedBox>(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<ClipRRect>(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);
});
}
+4 -1
View File
@@ -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);
});
@@ -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<EstadoRadio>.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<PluriGlassSurface>(
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);
});
}