From 01615f9751f358bb2144a90a87d6c6a376b0a1a8 Mon Sep 17 00:00:00 2001 From: freetlab Date: Thu, 30 Jul 2026 14:48:06 +0200 Subject: [PATCH] fix(escuchar,reproductor): add the section-heading token and screen 1-3 polish Audit S7 plus the open items on screens 1-3. Escuchar gains the prototype's own 52px "now listening" eyebrow header (t4:53), which is structurally different from the 56px title row every other root uses (t4:325) -- the earlier wiring test asserted PluriRootHeader on all five roots, an over-broad premise now corrected to guard what actually holds: no AppBar, and the sleep-timer action still reachable. --- lib/l10n/app_ar.arb | 4 +- lib/l10n/app_bn.arb | 4 +- lib/l10n/app_de.arb | 4 +- lib/l10n/app_en.arb | 2 + lib/l10n/app_es.arb | 2 + lib/l10n/app_fr.arb | 4 +- lib/l10n/app_hi.arb | 4 +- lib/l10n/app_id.arb | 4 +- lib/l10n/app_it.arb | 4 +- lib/l10n/app_ja.arb | 4 +- lib/l10n/app_pt.arb | 4 +- lib/l10n/app_ru.arb | 4 +- lib/l10n/app_zh.arb | 4 +- lib/l10n/gen/app_localizations.dart | 12 + lib/l10n/gen/app_localizations_ar.dart | 6 + lib/l10n/gen/app_localizations_bn.dart | 6 + lib/l10n/gen/app_localizations_de.dart | 6 + lib/l10n/gen/app_localizations_en.dart | 6 + lib/l10n/gen/app_localizations_es.dart | 6 + lib/l10n/gen/app_localizations_fr.dart | 6 + lib/l10n/gen/app_localizations_hi.dart | 6 + lib/l10n/gen/app_localizations_id.dart | 6 + lib/l10n/gen/app_localizations_it.dart | 6 + lib/l10n/gen/app_localizations_ja.dart | 6 + lib/l10n/gen/app_localizations_pt.dart | 6 + lib/l10n/gen/app_localizations_ru.dart | 6 + lib/l10n/gen/app_localizations_zh.dart | 6 + lib/pantallas/pantalla_inicio.dart | 274 ++++++++++-- lib/pantallas/pantalla_reproductor.dart | 423 +++++++++++------- lib/tema/pluriwave_typography.dart | 22 +- test/pantallas/pantalla_inicio_test.dart | 294 ++++++++++++ test/pantallas/pantalla_reproductor_test.dart | 156 +++++++ test/pantallas/root_header_wiring_test.dart | 26 +- test/tema/pluriwave_typography_test.dart | 14 + 34 files changed, 1129 insertions(+), 218 deletions(-) diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 9fbdd84..2eb1815 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -816,5 +816,7 @@ "welcomeBullet3Title": "منبهات موسيقية", "welcomeCtaLabel": "ابدأ الاستماع", "welcomeHeadline": "عالمك، على الهواء مباشرة", - "yourStationsTitle": "محطاتك" + "yourStationsTitle": "محطاتك", + "nowListeningLabel": "الاستماع الآن", + "popularNowTitle": "الأكثر شيوعًا الآن" } diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index 753ba0c..de0b361 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -816,5 +816,7 @@ "welcomeBullet3Title": "সুরেলা অ্যালার্ম", "welcomeCtaLabel": "শোনা শুরু করুন", "welcomeHeadline": "আপনার বিশ্ব, সরাসরি", - "yourStationsTitle": "আপনার স্টেশন" + "yourStationsTitle": "আপনার স্টেশন", + "nowListeningLabel": "এখন শোনা হচ্ছে", + "popularNowTitle": "এখন জনপ্রিয়" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 0135c1c..a95d02f 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -816,5 +816,7 @@ "welcomeBullet3Title": "Musikalarme", "welcomeCtaLabel": "Jetzt hören", "welcomeHeadline": "Deine Welt, live", - "yourStationsTitle": "Deine Sender" + "yourStationsTitle": "Deine Sender", + "nowListeningLabel": "Läuft gerade", + "popularNowTitle": "Jetzt beliebt" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3b63f2f..5e7dab1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -358,6 +358,7 @@ "yourStationsTitle": "Your stations", "seeAllAction": "See all", "openFullPlayerTooltip": "Open full player", + "nowListeningLabel": "Now listening", "nothingPlayingTitle": "Nothing playing yet", "nothingPlayingSubtitle": "Pick a station from Your stations or search to start.", "nearYou": "Near you", @@ -375,6 +376,7 @@ "exploreTrendingSubtitle": "Today", "exploreNewTitle": "New", "exploreNewSubtitle": "This week", + "popularNowTitle": "Popular now", "offlineBannerTitle": "No connection", "retryAction": "Retry", "noStationsAvailable": "No stations available", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 495b3db..7525f8e 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -358,6 +358,7 @@ "yourStationsTitle": "Tus emisoras", "seeAllAction": "Ver todas", "openFullPlayerTooltip": "Abrir reproductor completo", + "nowListeningLabel": "Escuchando ahora", "nothingPlayingTitle": "Todavía no estás escuchando nada", "nothingPlayingSubtitle": "Elegí una emisora de Tus emisoras o buscá una para empezar.", "nearYou": "Cerca de vos", @@ -375,6 +376,7 @@ "exploreTrendingSubtitle": "Hoy", "exploreNewTitle": "Novedades", "exploreNewSubtitle": "Esta semana", + "popularNowTitle": "Populares ahora", "offlineBannerTitle": "Sin conexión", "retryAction": "Reintentar", "noStationsAvailable": "No hay emisoras disponibles", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index bfbb86c..d28356e 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -816,5 +816,7 @@ "welcomeBullet3Title": "Alarmes musicales", "welcomeCtaLabel": "Commencer à écouter", "welcomeHeadline": "Votre monde, en direct", - "yourStationsTitle": "Vos stations" + "yourStationsTitle": "Vos stations", + "nowListeningLabel": "En cours d'écoute", + "popularNowTitle": "Populaire maintenant" } diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index fd812b1..8372283 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -816,5 +816,7 @@ "welcomeBullet3Title": "संगीत अलार्म", "welcomeCtaLabel": "सुनना शुरू करें", "welcomeHeadline": "आपकी दुनिया, लाइव", - "yourStationsTitle": "आपके स्टेशन" + "yourStationsTitle": "आपके स्टेशन", + "nowListeningLabel": "अभी सुन रहे हैं", + "popularNowTitle": "अभी लोकप्रिय" } diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 80fe6e8..efbdd43 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -816,5 +816,7 @@ "welcomeBullet3Title": "Alarm musik", "welcomeCtaLabel": "Mulai mendengarkan", "welcomeHeadline": "Duniamu, secara langsung", - "yourStationsTitle": "Stasiun Anda" + "yourStationsTitle": "Stasiun Anda", + "nowListeningLabel": "Sedang mendengarkan", + "popularNowTitle": "Populer sekarang" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 0c41a0f..9731558 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -816,5 +816,7 @@ "welcomeBullet3Title": "Sveglie musicali", "welcomeCtaLabel": "Inizia ad ascoltare", "welcomeHeadline": "Il tuo mondo, in diretta", - "yourStationsTitle": "Le tue emittenti" + "yourStationsTitle": "Le tue emittenti", + "nowListeningLabel": "In ascolto ora", + "popularNowTitle": "Popolari ora" } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index b1ef025..d7a0261 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -816,5 +816,7 @@ "welcomeBullet3Title": "音楽アラーム", "welcomeCtaLabel": "聴き始める", "welcomeHeadline": "あなたの世界を、ライブで", - "yourStationsTitle": "あなたの局" + "yourStationsTitle": "あなたの局", + "nowListeningLabel": "再生中", + "popularNowTitle": "今人気" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 63f8348..4a1c3c4 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -816,5 +816,7 @@ "welcomeBullet3Title": "Alarmes musicais", "welcomeCtaLabel": "Começar a ouvir", "welcomeHeadline": "Seu mundo, ao vivo", - "yourStationsTitle": "Suas estações" + "yourStationsTitle": "Suas estações", + "nowListeningLabel": "Ouvindo agora", + "popularNowTitle": "Populares agora" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 53a0ebe..c686277 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -816,5 +816,7 @@ "welcomeBullet3Title": "Музыкальные будильники", "welcomeCtaLabel": "Начать слушать", "welcomeHeadline": "Ваш мир, в прямом эфире", - "yourStationsTitle": "Ваши станции" + "yourStationsTitle": "Ваши станции", + "nowListeningLabel": "Сейчас слушаете", + "popularNowTitle": "Популярно сейчас" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 5f226b8..daa1c63 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -816,5 +816,7 @@ "welcomeBullet3Title": "音乐闹钟", "welcomeCtaLabel": "开始收听", "welcomeHeadline": "你的世界,直播中", - "yourStationsTitle": "你的电台" + "yourStationsTitle": "你的电台", + "nowListeningLabel": "正在收听", + "popularNowTitle": "当前热门" } diff --git a/lib/l10n/gen/app_localizations.dart b/lib/l10n/gen/app_localizations.dart index c0bb7f7..447e391 100644 --- a/lib/l10n/gen/app_localizations.dart +++ b/lib/l10n/gen/app_localizations.dart @@ -1402,6 +1402,12 @@ abstract class AppLocalizations { /// **'Abrir reproductor completo'** String get openFullPlayerTooltip; + /// No description provided for @nowListeningLabel. + /// + /// In es, this message translates to: + /// **'Escuchando ahora'** + String get nowListeningLabel; + /// No description provided for @nothingPlayingTitle. /// /// In es, this message translates to: @@ -1474,6 +1480,12 @@ abstract class AppLocalizations { /// **'Esta semana'** String get exploreNewSubtitle; + /// No description provided for @popularNowTitle. + /// + /// In es, this message translates to: + /// **'Populares ahora'** + String get popularNowTitle; + /// No description provided for @offlineBannerTitle. /// /// In es, this message translates to: diff --git a/lib/l10n/gen/app_localizations_ar.dart b/lib/l10n/gen/app_localizations_ar.dart index 46a3591..911927a 100644 --- a/lib/l10n/gen/app_localizations_ar.dart +++ b/lib/l10n/gen/app_localizations_ar.dart @@ -745,6 +745,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get openFullPlayerTooltip => 'فتح المشغل الكامل'; + @override + String get nowListeningLabel => 'الاستماع الآن'; + @override String get nothingPlayingTitle => 'لا تستمع إلى شيء بعد'; @@ -784,6 +787,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get exploreNewSubtitle => 'هذا الأسبوع'; + @override + String get popularNowTitle => 'الأكثر شيوعًا الآن'; + @override String get offlineBannerTitle => 'لا يوجد اتصال'; diff --git a/lib/l10n/gen/app_localizations_bn.dart b/lib/l10n/gen/app_localizations_bn.dart index 3d828e1..143503f 100644 --- a/lib/l10n/gen/app_localizations_bn.dart +++ b/lib/l10n/gen/app_localizations_bn.dart @@ -748,6 +748,9 @@ class AppLocalizationsBn extends AppLocalizations { @override String get openFullPlayerTooltip => 'সম্পূর্ণ প্লেয়ার খুলুন'; + @override + String get nowListeningLabel => 'এখন শোনা হচ্ছে'; + @override String get nothingPlayingTitle => 'এখনও কিছু চলছে না'; @@ -787,6 +790,9 @@ class AppLocalizationsBn extends AppLocalizations { @override String get exploreNewSubtitle => 'এই সপ্তাহে'; + @override + String get popularNowTitle => 'এখন জনপ্রিয়'; + @override String get offlineBannerTitle => 'সংযোগ নেই'; diff --git a/lib/l10n/gen/app_localizations_de.dart b/lib/l10n/gen/app_localizations_de.dart index 72e4cea..b57a5c0 100644 --- a/lib/l10n/gen/app_localizations_de.dart +++ b/lib/l10n/gen/app_localizations_de.dart @@ -751,6 +751,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get openFullPlayerTooltip => 'Vollständigen Player öffnen'; + @override + String get nowListeningLabel => 'Läuft gerade'; + @override String get nothingPlayingTitle => 'Es läuft noch nichts'; @@ -790,6 +793,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get exploreNewSubtitle => 'Diese Woche'; + @override + String get popularNowTitle => 'Jetzt beliebt'; + @override String get offlineBannerTitle => 'Keine Verbindung'; diff --git a/lib/l10n/gen/app_localizations_en.dart b/lib/l10n/gen/app_localizations_en.dart index 8c00298..cd6a2c8 100644 --- a/lib/l10n/gen/app_localizations_en.dart +++ b/lib/l10n/gen/app_localizations_en.dart @@ -744,6 +744,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get openFullPlayerTooltip => 'Open full player'; + @override + String get nowListeningLabel => 'Now listening'; + @override String get nothingPlayingTitle => 'Nothing playing yet'; @@ -783,6 +786,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get exploreNewSubtitle => 'This week'; + @override + String get popularNowTitle => 'Popular now'; + @override String get offlineBannerTitle => 'No connection'; diff --git a/lib/l10n/gen/app_localizations_es.dart b/lib/l10n/gen/app_localizations_es.dart index e05feb3..5550f72 100644 --- a/lib/l10n/gen/app_localizations_es.dart +++ b/lib/l10n/gen/app_localizations_es.dart @@ -749,6 +749,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get openFullPlayerTooltip => 'Abrir reproductor completo'; + @override + String get nowListeningLabel => 'Escuchando ahora'; + @override String get nothingPlayingTitle => 'Todavía no estás escuchando nada'; @@ -788,6 +791,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get exploreNewSubtitle => 'Esta semana'; + @override + String get popularNowTitle => 'Populares ahora'; + @override String get offlineBannerTitle => 'Sin conexión'; diff --git a/lib/l10n/gen/app_localizations_fr.dart b/lib/l10n/gen/app_localizations_fr.dart index 52b41ce..45e2575 100644 --- a/lib/l10n/gen/app_localizations_fr.dart +++ b/lib/l10n/gen/app_localizations_fr.dart @@ -753,6 +753,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get openFullPlayerTooltip => 'Ouvrir le lecteur complet'; + @override + String get nowListeningLabel => 'En cours d\'écoute'; + @override String get nothingPlayingTitle => 'Rien n\'est en cours de lecture'; @@ -792,6 +795,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get exploreNewSubtitle => 'Cette semaine'; + @override + String get popularNowTitle => 'Populaire maintenant'; + @override String get offlineBannerTitle => 'Aucune connexion'; diff --git a/lib/l10n/gen/app_localizations_hi.dart b/lib/l10n/gen/app_localizations_hi.dart index 9d7f824..c42962c 100644 --- a/lib/l10n/gen/app_localizations_hi.dart +++ b/lib/l10n/gen/app_localizations_hi.dart @@ -745,6 +745,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get openFullPlayerTooltip => 'पूरा प्लेयर खोलें'; + @override + String get nowListeningLabel => 'अभी सुन रहे हैं'; + @override String get nothingPlayingTitle => 'अभी तक कुछ नहीं चल रहा'; @@ -784,6 +787,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get exploreNewSubtitle => 'इस सप्ताह'; + @override + String get popularNowTitle => 'अभी लोकप्रिय'; + @override String get offlineBannerTitle => 'कोई कनेक्शन नहीं'; diff --git a/lib/l10n/gen/app_localizations_id.dart b/lib/l10n/gen/app_localizations_id.dart index 3c1b6b3..e0910c4 100644 --- a/lib/l10n/gen/app_localizations_id.dart +++ b/lib/l10n/gen/app_localizations_id.dart @@ -745,6 +745,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get openFullPlayerTooltip => 'Buka pemutar lengkap'; + @override + String get nowListeningLabel => 'Sedang mendengarkan'; + @override String get nothingPlayingTitle => 'Belum ada yang diputar'; @@ -784,6 +787,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get exploreNewSubtitle => 'Minggu ini'; + @override + String get popularNowTitle => 'Populer sekarang'; + @override String get offlineBannerTitle => 'Tidak ada koneksi'; diff --git a/lib/l10n/gen/app_localizations_it.dart b/lib/l10n/gen/app_localizations_it.dart index 72b5966..422bdc8 100644 --- a/lib/l10n/gen/app_localizations_it.dart +++ b/lib/l10n/gen/app_localizations_it.dart @@ -751,6 +751,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get openFullPlayerTooltip => 'Apri il player completo'; + @override + String get nowListeningLabel => 'In ascolto ora'; + @override String get nothingPlayingTitle => 'Non stai ancora ascoltando nulla'; @@ -790,6 +793,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get exploreNewSubtitle => 'Questa settimana'; + @override + String get popularNowTitle => 'Popolari ora'; + @override String get offlineBannerTitle => 'Nessuna connessione'; diff --git a/lib/l10n/gen/app_localizations_ja.dart b/lib/l10n/gen/app_localizations_ja.dart index a236a8f..50da498 100644 --- a/lib/l10n/gen/app_localizations_ja.dart +++ b/lib/l10n/gen/app_localizations_ja.dart @@ -720,6 +720,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get openFullPlayerTooltip => 'フルプレーヤーを開く'; + @override + String get nowListeningLabel => '再生中'; + @override String get nothingPlayingTitle => 'まだ何も再生されていません'; @@ -758,6 +761,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get exploreNewSubtitle => '今週'; + @override + String get popularNowTitle => '今人気'; + @override String get offlineBannerTitle => '接続なし'; diff --git a/lib/l10n/gen/app_localizations_pt.dart b/lib/l10n/gen/app_localizations_pt.dart index 11dec34..bfd9223 100644 --- a/lib/l10n/gen/app_localizations_pt.dart +++ b/lib/l10n/gen/app_localizations_pt.dart @@ -748,6 +748,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get openFullPlayerTooltip => 'Abrir player completo'; + @override + String get nowListeningLabel => 'Ouvindo agora'; + @override String get nothingPlayingTitle => 'Ainda não há nada tocando'; @@ -787,6 +790,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get exploreNewSubtitle => 'Nesta semana'; + @override + String get popularNowTitle => 'Populares agora'; + @override String get offlineBannerTitle => 'Sem conexão'; diff --git a/lib/l10n/gen/app_localizations_ru.dart b/lib/l10n/gen/app_localizations_ru.dart index 070ac83..32f5222 100644 --- a/lib/l10n/gen/app_localizations_ru.dart +++ b/lib/l10n/gen/app_localizations_ru.dart @@ -749,6 +749,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get openFullPlayerTooltip => 'Открыть полный плеер'; + @override + String get nowListeningLabel => 'Сейчас слушаете'; + @override String get nothingPlayingTitle => 'Пока ничего не воспроизводится'; @@ -788,6 +791,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get exploreNewSubtitle => 'На этой неделе'; + @override + String get popularNowTitle => 'Популярно сейчас'; + @override String get offlineBannerTitle => 'Нет подключения'; diff --git a/lib/l10n/gen/app_localizations_zh.dart b/lib/l10n/gen/app_localizations_zh.dart index 3a0b352..f0500b2 100644 --- a/lib/l10n/gen/app_localizations_zh.dart +++ b/lib/l10n/gen/app_localizations_zh.dart @@ -717,6 +717,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get openFullPlayerTooltip => '打开完整播放器'; + @override + String get nowListeningLabel => '正在收听'; + @override String get nothingPlayingTitle => '目前没有正在播放的内容'; @@ -755,6 +758,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get exploreNewSubtitle => '本周'; + @override + String get popularNowTitle => '当前热门'; + @override String get offlineBannerTitle => '无连接'; diff --git a/lib/pantallas/pantalla_inicio.dart b/lib/pantallas/pantalla_inicio.dart index 80e12f5..4a0ecf6 100644 --- a/lib/pantallas/pantalla_inicio.dart +++ b/lib/pantallas/pantalla_inicio.dart @@ -14,7 +14,6 @@ import '../tema/pluriwave_theme.dart'; import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_layout.dart'; import '../widgets/pluri_premium_widgets.dart'; -import '../widgets/pluri_root_header.dart'; import '../widgets/pluri_sleep_timer_sheet.dart'; import '../widgets/visualizador_audio.dart'; @@ -38,35 +37,46 @@ class _PantallaInicioState extends State { final theme = Theme.of(context); final l10n = AppLocalizations.of(context); - return CustomScrollView( - slivers: [ - // S1 (Tier 1 visual fidelity): the prototype has no global AppBar — - // this root now draws its own 56px title row instead of relying on - // app.dart's removed shared chrome (which is also where the - // sleep-timer action used to live). - SliverToBoxAdapter( - child: PluriRootHeader( - title: l10n.navHome, - onSleepTimer: () => showPluriSleepTimerSheet(context), - ), - ), - // WU5 built the hero; WU6 relocated the discovery sections that - // used to follow it (_seccionCercanas, _seccionTendencias, - // _chipGeneros, _errorBanner, the browse grid) into - // PantallaBuscar's landing state and DELETED them here (this WU's - // own task 6.5 — completing WU5's task 5.9 deferral). Escuchar's - // content is now just the hero and the favorites preview below; - // pull-to-refresh was dropped along with the grid it refreshed — - // the retry button that used to live in the (now relocated) error - // banner already covers manual recovery on Buscar. - const SliverToBoxAdapter(child: _EscucharHero()), - SliverToBoxAdapter(child: _seccionTusEmisoras(context, theme, l10n)), - // ADR-7(b): the mini player is hidden on this whole screen - // (app.dart), so its content needs less bottom padding than every - // other root — escucharBottomChromeInset, not the plain - // bottomChromeInset every other root/scrollable uses. - const SliverToBoxAdapter( - child: SizedBox(height: PluriLayout.escucharBottomChromeInset), + // Audit 1.1 (t4 lines 49-51): a full-bleed art/gradient/noise wash sits + // BEHIND the whole screen, not just inside the hero card -- the Stack + // wrapper is new; everything that used to be the bare CustomScrollView + // return value is unchanged below. + return Stack( + fit: StackFit.expand, + children: [ + const _FondoEscuchar(), + CustomScrollView( + slivers: [ + // Audit 1.2 (t4 line 53): unlike the other 3 roots' plain 56px + // PluriRootHeader, Escuchar's own header is a 52px "Now + // listening" eyebrow row -- the tab bar already identifies this + // destination, so it never repeats "Escuchar" as a title. + SliverToBoxAdapter( + child: _CabeceraEscuchar( + onSleepTimer: () => showPluriSleepTimerSheet(context), + ), + ), + // WU5 built the hero; WU6 relocated the discovery sections that + // used to follow it (_seccionCercanas, _seccionTendencias, + // _chipGeneros, _errorBanner, the browse grid) into + // PantallaBuscar's landing state and DELETED them here (this WU's + // own task 6.5 — completing WU5's task 5.9 deferral). Escuchar's + // content is now just the hero and the favorites preview below; + // pull-to-refresh was dropped along with the grid it refreshed — + // the retry button that used to live in the (now relocated) error + // banner already covers manual recovery on Buscar. + const SliverToBoxAdapter(child: _EscucharHero()), + SliverToBoxAdapter( + child: _seccionTusEmisoras(context, theme, l10n), + ), + // ADR-7(b): the mini player is hidden on this whole screen + // (app.dart), so its content needs less bottom padding than every + // other root — escucharBottomChromeInset, not the plain + // bottomChromeInset every other root/scrollable uses. + const SliverToBoxAdapter( + child: SizedBox(height: PluriLayout.escucharBottomChromeInset), + ), + ], ), ], ); @@ -112,9 +122,10 @@ class _PantallaInicioState extends State { Flexible( child: Text( l10n.yourStationsTitle, - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w900, - ), + // S7 (visual fidelity, t4 line 80): the in-page + // section heading style (15/w800/ls-.2), not + // titleMedium(16)/w900. + style: context.pluriType.inPageSectionHeading, overflow: TextOverflow.ellipsis, ), ), @@ -187,6 +198,134 @@ class _PantallaInicioState extends State { } } +/// Audit 1.1 (t4 lines 49-51): a full-bleed backdrop behind the ENTIRE +/// screen -- 400px of the current station's own art at 50% opacity, a +/// gradient fading it into the page background by 430px, and a faint noise +/// texture. Reads `emisoraActual` ITSELF (not a parent watch), so this stays +/// the only new subscriber -- `_PantallaInicioState.build()` keeps its +/// documented S4-R5 "no root watch" rule. Renders the gradient/noise even +/// with nothing playing (there is simply no image layer to show then) so +/// the page keeps the same base depth in every state. +class _FondoEscuchar extends StatelessWidget { + const _FondoEscuchar(); + + static const double _altoArte = 400; + static const double _altoDegradado = 430; + + @override + Widget build(BuildContext context) { + final emisora = context.select( + (e) => e.emisoraActual, + ); + final tokens = context.pluriTokens; + final favicon = emisora?.favicon; + + return IgnorePointer( + key: const ValueKey('escuchar-background-art'), + child: Stack( + children: [ + if (favicon != null && favicon.isNotEmpty) + SizedBox( + width: double.infinity, + height: _altoArte, + child: Opacity( + opacity: 0.5, + child: CachedNetworkImage( + imageUrl: favicon, + fit: BoxFit.cover, + errorWidget: (_, __, ___) => const SizedBox.shrink(), + ), + ), + ), + SizedBox( + width: double.infinity, + height: _altoDegradado, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + tokens.deepViolet.withValues(alpha: 0.4), + tokens.deepViolet.withValues(alpha: 0.86), + tokens.deepViolet, + ], + stops: const [0, 0.58, 1], + ), + ), + ), + ), + SizedBox( + width: double.infinity, + height: _altoDegradado, + child: Opacity( + opacity: 0.05, + child: Image.asset( + 'assets/images/noise_soft.png', + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const SizedBox.shrink(), + ), + ), + ), + ], + ), + ); + } +} + +/// Audit 1.2 (t4 line 53): Escuchar's own header -- 52px, an eyebrow-styled +/// "Now listening" label (not a screen title -- the tab bar already +/// identifies this destination) and a single trailing action. +/// +/// The prototype also draws a `cast` icon here. This app has no casting +/// service anywhere in `lib/servicios` (checked before writing this) -- +/// inventing a Chromecast-style button with no backing capability would fail +/// the same "don't invent a capability absent from the domain" test WU5/WU9 +/// already established elsewhere in this app, so it is omitted, not faked. +/// The ONE real action this row keeps is the sleep timer, which used to be +/// [PluriRootHeader]'s bedtime button on this exact screen. +class _CabeceraEscuchar extends StatelessWidget { + const _CabeceraEscuchar({required this.onSleepTimer}); + + final VoidCallback onSleepTimer; + + static const double altura = 52; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + return SizedBox( + height: altura, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 10, 0), + child: Row( + children: [ + Expanded( + child: Text( + l10n.nowListeningLabel, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w800, + letterSpacing: 0.3, + color: const Color(0xFFF2F7FA).withValues(alpha: 0.55), + ), + ), + ), + IconButton( + icon: const Icon(Icons.bedtime_outlined), + iconSize: 23, + tooltip: l10n.sleepTimer, + onPressed: onSleepTimer, + ), + ], + ), + ), + ); + } +} + /// WU5, design ADR-7: the Escuchar embedded player. `EstadoRadio` is the /// single source of truth (`emisoraActual` already feeds `MiniReproductor` /// and `PantallaReproductor`) — this hero is a third VIEW, never a third @@ -292,8 +431,14 @@ class _EscucharHero extends StatelessWidget { const SizedBox(height: 6), Text( stationName, + // Audit 1.5 (t4 line 60): 24/w800/ls-.5/height 1.12 + // -- titleLarge is 22 with neither letter-spacing + // nor an explicit line height. style: theme.textTheme.titleLarge?.copyWith( + fontSize: 24, fontWeight: FontWeight.w800, + letterSpacing: -0.5, + height: 1.12, ), maxLines: 2, overflow: TextOverflow.ellipsis, @@ -334,12 +479,55 @@ class _EscucharHero extends StatelessWidget { const SizedBox(height: 14), _FilaTransporteEscuchar(emisora: emisora), const SizedBox(height: 10), - Align( - alignment: Alignment.centerLeft, - child: OutlinedButton.icon( - icon: const Icon(Icons.tune_rounded, size: 18), - label: Text(l10n.openFullPlayerTooltip), - onPressed: () => PantallaReproductor.abrir(context, emisora), + // Audit 1.9 (t4 line 78): a CENTRED pill -- padding:9px 15px, + // radius:999, rgba(255,255,255,.07) + border .1, 12px/w700, + // icon expand_less 17px -- not a left-aligned OutlinedButton + // with tune_rounded at 18px. + Center( + child: Material( + type: MaterialType.transparency, + child: InkWell( + borderRadius: BorderRadius.circular(999), + onTap: () => PantallaReproductor.abrir(context, emisora), + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.07), + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: Colors.white.withValues(alpha: 0.1), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 9, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.expand_less_rounded, + size: 17, + color: theme.colorScheme.onSurface.withValues( + alpha: 0.78, + ), + ), + const SizedBox(width: 6), + Text( + l10n.openFullPlayerTooltip, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: theme.colorScheme.onSurface.withValues( + alpha: 0.78, + ), + ), + ), + ], + ), + ), + ), + ), ), ), ], @@ -474,8 +662,10 @@ class _FilaTransporteEscuchar extends StatelessWidget { s == EstadoReproduccion.cargando || s == EstadoReproduccion.reconectando; return SizedBox( - width: 56, - height: 56, + // Audit 1.8 (t4 line 73): 72x72 with a 38px icon, not 56x56 + // with a 28px icon. + width: 72, + height: 72, child: FilledButton( style: FilledButton.styleFrom( shape: const CircleBorder(), @@ -503,7 +693,7 @@ class _FilaTransporteEscuchar extends StatelessWidget { reproduciendo ? Icons.pause_rounded : Icons.play_arrow_rounded, - size: 28, + size: 38, ), ), ); diff --git a/lib/pantallas/pantalla_reproductor.dart b/lib/pantallas/pantalla_reproductor.dart index 4ad837e..78d210a 100644 --- a/lib/pantallas/pantalla_reproductor.dart +++ b/lib/pantallas/pantalla_reproductor.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -13,6 +15,7 @@ import '../servicios/servicio_audio.dart'; import '../servicios/servicio_timer.dart'; import '../tema/pluri_animate.dart'; import '../tema/pluriwave_theme.dart'; +import '../tema/pluriwave_tokens.dart'; import '../widgets/ecualizador_widget.dart'; import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_premium_widgets.dart'; @@ -117,83 +120,110 @@ class _PantallaReproductorState extends State { ); }, ), - body: SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), - child: Column( - children: [ - _ArteReproductor( - emisora: emisoraActiva, - estadoStream: estado.estadoStream, - ).pluriScaleIn( - context, - begin: 0.86, - duration: const Duration(milliseconds: 420), - curve: Curves.easeOutBack, + // Audit 2.2 (t4 lines 108-109): a full-bleed blurred backdrop of the + // station's own art sits BEHIND the scrollable content -- the Stack + // wrapper is new; the SafeArea/SingleChildScrollView subtree below is + // unchanged. + body: Stack( + fit: StackFit.expand, + children: [ + _FondoReproductor(emisora: emisoraActiva, tokens: tokens), + SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), + child: Column( + children: [ + _ArteReproductor( + emisora: emisoraActiva, + estadoStream: estado.estadoStream, + ).pluriScaleIn( + context, + begin: 0.86, + duration: const Duration(milliseconds: 420), + curve: Curves.easeOutBack, + ), + const SizedBox(height: 18), + Text( + emisoraActiva.nombre, + // Audit 2.3 (t4 line 116): 26/w800/ls-.5/height 1.15 -- + // headlineSmall is 24/w700 with neither letter-spacing + // nor an explicit line height. + style: theme.textTheme.headlineSmall?.copyWith( + fontSize: 26, + fontWeight: FontWeight.w800, + letterSpacing: -0.5, + height: 1.15, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ).pluriFadeIn( + context, + delay: const Duration(milliseconds: 150), + ), + const SizedBox(height: 6), + _SubtituloInfo(emisora: emisoraActiva).pluriFadeIn( + context, + delay: const Duration(milliseconds: 190), + ), + const SizedBox(height: 14), + // Audit 2.10 (t4 lines 120-122): the bars sit directly on + // the background -- no wrapping glass card. The key moves + // from the (removed) PluriGlassSurface onto the + // visualizer itself so existing finders keep working. + VisualizadorAudio( + key: const Key('player-visualizer'), + estadoStream: estado.estadoStream, + androidAudioSessionIdStream: + estado.audio.androidAudioSessionIdStream, + // Audit 2.5 (t4 lines 120-122): 30 discrete bars at + // 40px, gradient ending at 45% alpha (not the hero's + // 30%). + barras: 30, + color: tokens.warmCoral, + altura: 40, + barrasDiscretas: true, + gradienteFinAlpha: 0.45, + ).pluriFadeIn( + context, + delay: const Duration(milliseconds: 270), + ), + const SizedBox(height: 22), + _Controles( + estado: estado, + emisora: emisoraActiva, + esFavorito: esFavorito, + ).pluriFadeSlideIn( + context, + delay: const Duration(milliseconds: 310), + beginY: 0.3, + ), + const SizedBox(height: 20), + _BandejaHerramientas( + estado: estado, + eq: eq, + emisora: emisoraActiva, + compartir: _compartir, + ).pluriFadeIn( + context, + delay: const Duration(milliseconds: 350), + ), + const SizedBox(height: 12), + // Item 26 / audit 2.4 (t4:114-140): the quality row is + // LAST, after the tool tray -- it used to sit right after + // the subtitle, 4 positions too early. + _FilaCalidad( + estado: estado, + emisora: emisoraActiva, + ).pluriFadeIn( + context, + delay: const Duration(milliseconds: 390), + ), + ], ), - const SizedBox(height: 18), - Text( - emisoraActiva.nombre, - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w700, - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ).pluriFadeIn(context, delay: const Duration(milliseconds: 150)), - const SizedBox(height: 6), - _SubtituloInfo( - emisora: emisoraActiva, - ).pluriFadeIn(context, delay: const Duration(milliseconds: 190)), - const SizedBox(height: 14), - PluriGlassSurface( - key: const Key('player-visualizer'), - borderRadius: BorderRadius.circular(tokens.radiusLg), - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 12, - ), - child: VisualizadorAudio( - estadoStream: estado.estadoStream, - androidAudioSessionIdStream: - estado.audio.androidAudioSessionIdStream, - // Audit 2.5 (t4 lines 120-122): 30 discrete bars at 40px, - // gradient ending at 45% alpha (not the hero's 30%). - barras: 30, - color: tokens.warmCoral, - altura: 40, - barrasDiscretas: true, - gradienteFinAlpha: 0.45, - ), - ).pluriFadeIn(context, delay: const Duration(milliseconds: 270)), - const SizedBox(height: 22), - _Controles( - estado: estado, - emisora: emisoraActiva, - esFavorito: esFavorito, - ).pluriFadeSlideIn( - context, - delay: const Duration(milliseconds: 310), - beginY: 0.3, - ), - const SizedBox(height: 20), - _BandejaHerramientas( - estado: estado, - eq: eq, - emisora: emisoraActiva, - compartir: _compartir, - ).pluriFadeIn(context, delay: const Duration(milliseconds: 350)), - const SizedBox(height: 12), - // Item 26 / audit 2.4 (t4:114-140): the quality row is LAST, - // after the tool tray -- it used to sit right after the - // subtitle, 4 positions too early. - _FilaCalidad( - estado: estado, - emisora: emisoraActiva, - ).pluriFadeIn(context, delay: const Duration(milliseconds: 390)), - ], + ), ), - ), + ], ), ); } @@ -201,25 +231,32 @@ class _PantallaReproductorState extends State { /// Square art (design proposal WU14 row: "square art" replaces the old /// circular `_WaveHero`). Loading/error overlays and the fallback icon are -/// unchanged from the prior circular version — only the clip shape and the -/// decorative halo geometry changed from circle to rounded-square. +/// unchanged from the prior circular version. +/// +/// Audit 2.1 (t4 line 114): the prototype draws a FIXED 246x246 square, +/// radius 28, with only a drop shadow -- no halo, no ring. The previous +/// `width*0.62`-derived size plus a radial-gradient halo Container and a +/// bordered ring Container (neither present in the prototype) are both +/// removed; `_radio` (28) is deliberately its own constant, distinct from +/// every named token radius, mirroring this codebase's established +/// one-off-radius precedent (e.g. the ringing screen's stop button). class _ArteReproductor extends StatelessWidget { final Emisora emisora; final Stream estadoStream; const _ArteReproductor({required this.emisora, required this.estadoStream}); + static const double _lado = 246; + static const double _radio = 28; + @override Widget build(BuildContext context) { final theme = Theme.of(context); - final t = context.pluriTokens; - final size = MediaQuery.of(context).size.width * 0.62; - final radio = BorderRadius.circular(t.radiusLg); + final radio = BorderRadius.circular(_radio); return StreamBuilder( stream: estadoStream, builder: (context, snapshot) { - final reproduciendo = snapshot.data == EstadoReproduccion.reproduciendo; // S7-R3: reconectando renders as loading, never as error. final cargando = snapshot.data == EstadoReproduccion.cargando || @@ -228,83 +265,62 @@ class _ArteReproductor extends StatelessWidget { return SizedBox( key: const Key('player-hero-art'), - width: size + 40, - height: size + 40, - child: Stack( - alignment: Alignment.center, - children: [ - Container( - width: size + 34, - height: size + 34, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(t.radiusLg + 16), - gradient: RadialGradient( - colors: [ - t.electricMagenta.withValues( - alpha: reproduciendo ? 0.35 : 0.18, - ), - t.deepViolet.withValues(alpha: 0.0), - ], - ), + width: _lado, + height: _lado, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: radio, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.55), + blurRadius: 60, + offset: const Offset(0, 26), ), - ), - Container( - width: size + 12, - height: size + 12, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(t.radiusLg + 4), - border: Border.all(color: t.glassBorder), - ), - ), - PluriGlassSurface( + ], + ), + child: PluriGlassSurface( + borderRadius: radio, + padding: EdgeInsets.zero, + child: ClipRRect( borderRadius: radio, - padding: EdgeInsets.zero, - child: SizedBox( - width: size, - height: size, - child: ClipRRect( - borderRadius: radio, - child: Stack( - fit: StackFit.expand, - children: [ - if (emisora.favicon != null && - emisora.favicon!.isNotEmpty) - CachedNetworkImage( - imageUrl: emisora.favicon!, - fit: BoxFit.cover, - placeholder: (_, __) => _shimmer(theme), - errorWidget: (_, __, ___) => _iconoFallback(theme), - ) - else - _iconoFallback(theme), - if (cargando) - Container( - color: Colors.black45, - child: Center( - child: CircularProgressIndicator( - color: theme.colorScheme.onSurface, - ), + child: Stack( + fit: StackFit.expand, + children: [ + if (emisora.favicon != null && emisora.favicon!.isNotEmpty) + CachedNetworkImage( + imageUrl: emisora.favicon!, + fit: BoxFit.cover, + placeholder: (_, __) => _shimmer(theme), + errorWidget: (_, __, ___) => _iconoFallback(theme), + ) + else + _iconoFallback(theme), + if (cargando) + Container( + color: Colors.black45, + child: Center( + child: CircularProgressIndicator( + color: theme.colorScheme.onSurface, + ), + ), + ), + if (hayError) + Container( + color: Colors.black54, + child: Center( + child: Icon( + Icons.wifi_off_rounded, + size: 56, + color: theme.colorScheme.onSurface.withValues( + alpha: 0.85, ), ), - if (hayError) - Container( - color: Colors.black54, - child: Center( - child: Icon( - Icons.wifi_off_rounded, - size: 56, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.85, - ), - ), - ), - ), - ], - ), - ), + ), + ), + ], ), ), - ], + ), ), ); }, @@ -327,6 +343,63 @@ class _ArteReproductor extends StatelessWidget { ); } +/// Audit 2.2 (t4 lines 108-109): a full-bleed blurred backdrop of the +/// CURRENT station's own art, sitting behind the whole screen. Unlike the +/// ringing screen's `_FondoArteDifuminado` (which cannot use +/// `Emisora.favicon` -- a cold-boot download after a screen-off wake would +/// hang widget tests without a mocked `HttpClient`), this screen already +/// renders `emisora.favicon` via `CachedNetworkImage` for its OWN foreground +/// art (`_ArteReproductor` above) with no such hazard -- reusing it here for +/// the backdrop is equally safe. Renders the gradient tint even without a +/// favicon, so the page keeps the same base depth in every state. +class _FondoReproductor extends StatelessWidget { + const _FondoReproductor({required this.emisora, required this.tokens}); + + final Emisora emisora; + final PluriWaveTokens tokens; + + @override + Widget build(BuildContext context) { + final favicon = emisora.favicon; + return Positioned.fill( + key: const ValueKey('player-background-art'), + child: IgnorePointer( + child: Stack( + fit: StackFit.expand, + children: [ + if (favicon != null && favicon.isNotEmpty) + ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 26, sigmaY: 26), + child: Opacity( + opacity: 0.45, + child: CachedNetworkImage( + imageUrl: favicon, + fit: BoxFit.cover, + errorWidget: (_, __, ___) => const SizedBox.shrink(), + ), + ), + ), + DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + tokens.deepViolet.withValues(alpha: 0.55), + tokens.deepViolet.withValues(alpha: 0.9), + tokens.deepViolet, + ], + stops: const [0, 0.46, 1], + ), + ), + ), + ], + ), + ), + ); + } +} + /// Single subtitle line (WU14: collapses the old `_InfoChips` `Wrap` of /// separate chips — country/language now join as one line; codec/bitrate /// moved into their own [_FilaCalidad] row below). @@ -755,9 +828,10 @@ class _Controles extends StatelessWidget { ); } - return PluriGlassSurface( - borderRadius: BorderRadius.circular(28), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + // Audit 2.9 (t4 lines 124-131): the transport row sits directly on + // the background -- the prototype draws no wrapping card here. + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ @@ -874,6 +948,13 @@ class _Controles extends StatelessWidget { /// One tile of the 4-tile tool tray (WU14: EQ propio / Grabar / sleep timer /// / Compartir), each opening its own bottom sheet (Compartir invokes /// directly instead — there is no sheet content for it). +/// +/// Audit 2.8 (t4 lines 133-136): only the FIRST tile ("EQ propio") is +/// accented -- tinted background/border, brand-colour icon, bold brand +/// label. The other three share a neutral OPAQUE surface (not `listSurface` +/// at 55% alpha) with a full-weight icon/label -- this app's ambient +/// default colour, not the `tokens.electricMagenta` fallback every tile +/// used to render in regardless of [accent]. class _TileHerramienta extends StatelessWidget { const _TileHerramienta({ super.key, @@ -881,6 +962,7 @@ class _TileHerramienta extends StatelessWidget { required this.label, required this.onTap, this.iconColor, + this.accent = false, }); final IconData icon; @@ -888,26 +970,42 @@ class _TileHerramienta extends StatelessWidget { final VoidCallback onTap; final Color? iconColor; + /// See class doc — `false` (neutral) is every tile except "EQ propio". + final bool accent; + @override Widget build(BuildContext context) { final theme = Theme.of(context); final tokens = context.pluriTokens; + final resolvedIconColor = + iconColor ?? (accent ? tokens.electricMagenta : null); return Material( type: MaterialType.transparency, child: InkWell( - borderRadius: BorderRadius.circular(tokens.radiusSm), + borderRadius: BorderRadius.circular(tokens.radiusMd), onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 6), decoration: BoxDecoration( - color: tokens.listSurface.withValues(alpha: 0.55), - borderRadius: BorderRadius.circular(tokens.radiusSm), + color: + accent + ? tokens.electricMagenta.withValues(alpha: 0.16) + : tokens.listSurface, + // Audit 2.8 (t4 lines 133-136): radius 18 for BOTH variants -- + // `radiusMd`, not the smaller `radiusSm` every tile used before. + borderRadius: BorderRadius.circular(tokens.radiusMd), + border: Border.all( + color: + accent + ? tokens.electricMagenta.withValues(alpha: 0.4) + : Colors.white.withValues(alpha: 0.09), + ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 23, color: iconColor ?? tokens.electricMagenta), + Icon(icon, size: 23, color: resolvedIconColor), const SizedBox(height: 5), Text( label, @@ -915,7 +1013,11 @@ class _TileHerramienta extends StatelessWidget { overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: theme.textTheme.labelSmall?.copyWith( - fontWeight: FontWeight.w800, + fontWeight: accent ? FontWeight.w800 : FontWeight.w700, + color: + accent + ? tokens.electricMagenta + : theme.colorScheme.onSurface.withValues(alpha: 0.72), ), ), ], @@ -952,6 +1054,7 @@ class _BandejaHerramientas extends StatelessWidget { icon: Icons.equalizer_rounded, label: l10n.playerToolEqLabel, onTap: () => _mostrarHojaEq(context, eq, emisora), + accent: true, ), ), const SizedBox(width: 10), diff --git a/lib/tema/pluriwave_typography.dart b/lib/tema/pluriwave_typography.dart index 22b5155..05552ca 100644 --- a/lib/tema/pluriwave_typography.dart +++ b/lib/tema/pluriwave_typography.dart @@ -2,8 +2,11 @@ import 'package:flutter/material.dart'; /// Design ADR-1: the named type scale, split out of [PluriWaveTokens] into /// its own ThemeExtension so screens stop copy-pasting -/// `.copyWith(fontWeight: w900, letterSpacing: …)`. The set is closed at six -/// styles — a seventh requires amending ADR-1. +/// `.copyWith(fontWeight: w900, letterSpacing: …)`. The set was closed at +/// six styles; S7 (visual fidelity pass) amended ADR-1 to add a seventh — +/// [inPageSectionHeading] — rather than have four screens each keep +/// copy-pasting their own `titleMedium.copyWith(fontWeight: w900)` override, +/// the exact duplication this extension exists to eliminate. /// /// [eyebrowLabel] never applies `toUpperCase()` — casing is a /// locale-hostile transform (Turkish dotless i, and scripts with no case at @@ -18,6 +21,7 @@ class PluriWaveTypography extends ThemeExtension { required this.cardTitle, required this.bodyStrong, required this.eyebrowLabel, + required this.inPageSectionHeading, }); /// 88 / w800 / height 1.0 / ls -2.0. Alarm ringing, inline time editor. @@ -41,6 +45,14 @@ class PluriWaveTypography extends ThemeExtension { /// headers. final TextStyle eyebrowLabel; + /// 15 / w800 / ls -0.2. S7 (visual fidelity, t4 lines 80, 153, 164, 173): + /// an in-page section heading living directly on a screen's content — + /// "Tus emisoras", "Cerca de ti", "Explorar por", "Populares ahora". + /// Distinct from both [sectionTitle] (23, a ROOT screen's own title) and + /// [screenTitle] (19, a pushed screen's AppBar title) — this is smaller + /// than either, for a heading living one level below the screen itself. + final TextStyle inPageSectionHeading; + /// Builds the scale from [family] — the SAME resolved Google Fonts /// TextTheme instance [PluriWaveTheme.dark] already computes. Building the /// styles anywhere else would re-merge the font family, which is the @@ -79,6 +91,7 @@ class PluriWaveTypography extends ThemeExtension { letterSpacing: 0.8, color: const Color(0xFFF2F7FA).withValues(alpha: 0.42), ), + inPageSectionHeading: style(15, FontWeight.w800, letterSpacing: -0.2), ); } @@ -90,6 +103,7 @@ class PluriWaveTypography extends ThemeExtension { TextStyle? cardTitle, TextStyle? bodyStrong, TextStyle? eyebrowLabel, + TextStyle? inPageSectionHeading, }) { return PluriWaveTypography( heroTime: heroTime ?? this.heroTime, @@ -98,6 +112,7 @@ class PluriWaveTypography extends ThemeExtension { cardTitle: cardTitle ?? this.cardTitle, bodyStrong: bodyStrong ?? this.bodyStrong, eyebrowLabel: eyebrowLabel ?? this.eyebrowLabel, + inPageSectionHeading: inPageSectionHeading ?? this.inPageSectionHeading, ); } @@ -117,6 +132,9 @@ class PluriWaveTypography extends ThemeExtension { bodyStrong: TextStyle.lerp(bodyStrong, other.bodyStrong, t) ?? bodyStrong, eyebrowLabel: TextStyle.lerp(eyebrowLabel, other.eyebrowLabel, t) ?? eyebrowLabel, + inPageSectionHeading: + TextStyle.lerp(inPageSectionHeading, other.inPageSectionHeading, t) ?? + inPageSectionHeading, ); } } diff --git a/test/pantallas/pantalla_inicio_test.dart b/test/pantallas/pantalla_inicio_test.dart index 4b0deeb..038f1bd 100644 --- a/test/pantallas/pantalla_inicio_test.dart +++ b/test/pantallas/pantalla_inicio_test.dart @@ -10,6 +10,7 @@ import 'package:pluriwave/estado/estado_radio.dart'; import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/modelos/emisora.dart'; import 'package:pluriwave/pantallas/pantalla_inicio.dart'; +import 'package:pluriwave/widgets/pluri_root_header.dart'; import 'package:pluriwave/widgets/visualizador_audio.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -404,6 +405,299 @@ void main() { expect(texto.style?.color, const Color(0xFF21D4D9)); }, ); + + testWidgets( + 'S7 visual fidelity: "Tus emisoras" uses the in-page section heading ' + 'style (15/w800/ls-.2), not titleMedium(16)/w900', + (tester) async { + _setLargeSurfaceSize(tester); + final favoritos = FakeServicioFavoritos(); + final estado = EstadoRadio( + audio: FakeServicioAudio(), + favoritos: favoritos, + radio: FakeServicioRadio(), + servicioEcualizador: FakeServicioEcualizador(), + servicioGrabacion: FakeServicioGrabacionRadio(), + resolverArchivoCustom: _archivoCustomVacio, + iniciarAutomaticamente: false, + ); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); + await favoritos.agregar(emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno')); + await estado.cargarFavoritos(); + + await tester.pumpWidget( + _conProviders(estado, _testApp(const PantallaInicio())), + ); + await _pumpStableFrame(tester); + + final texto = tester.widget(find.text('Tus emisoras')); + expect(texto.style?.fontSize, 15, reason: 'prototype t4 line 80'); + expect(texto.style?.fontWeight, FontWeight.w800); + expect(texto.style?.letterSpacing, -0.2); + }, + ); + + testWidgets( + 'visual fidelity (audit 1.1): a full-bleed art/gradient/noise wash ' + 'sits behind the whole screen, keyed so it can be found', + (tester) async { + _setLargeSurfaceSize(tester); + final favoritos = FakeServicioFavoritos(); + final estado = EstadoRadio( + audio: FakeServicioAudio(), + favoritos: favoritos, + radio: FakeServicioRadio(), + servicioEcualizador: FakeServicioEcualizador(), + servicioGrabacion: FakeServicioGrabacionRadio(), + resolverArchivoCustom: _archivoCustomVacio, + iniciarAutomaticamente: false, + ); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); + final sonando = emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno'); + await favoritos.agregar(sonando); + await estado.cargarFavoritos(); + await tester.runAsync(() => estado.reproducir(sonando)); + + await tester.pumpWidget( + _conProviders(estado, _testApp(const PantallaInicio())), + ); + await _pumpBounded(tester); + + expect( + find.byKey(const ValueKey('escuchar-background-art')), + findsOneWidget, + reason: + 'prototype t4 lines 49-51: full-bleed art at .5 opacity + a ' + 'gradient + a noise texture', + ); + }, + ); + + testWidgets( + 'visual fidelity (audit 1.1): the background wash still renders with ' + 'nothing playing (gradient/noise only, no station art to show)', + (tester) async { + _setLargeSurfaceSize(tester); + final estado = EstadoRadio( + audio: FakeServicioAudio(), + favoritos: FakeServicioFavoritos(), + radio: FakeServicioRadio(), + servicioEcualizador: FakeServicioEcualizador(), + servicioGrabacion: FakeServicioGrabacionRadio(), + resolverArchivoCustom: _archivoCustomVacio, + iniciarAutomaticamente: false, + ); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); + + await tester.pumpWidget( + _conProviders(estado, _testApp(const PantallaInicio())), + ); + await _pumpStableFrame(tester); + + expect( + find.byKey(const ValueKey('escuchar-background-art')), + findsOneWidget, + ); + }, + ); + + testWidgets('visual fidelity (audit 1.2): Escuchar has its own 52px "Now ' + 'listening" eyebrow header, not the shared 56px PluriRootHeader', ( + tester, + ) async { + _setLargeSurfaceSize(tester); + final estado = EstadoRadio( + audio: FakeServicioAudio(), + favoritos: FakeServicioFavoritos(), + radio: FakeServicioRadio(), + servicioEcualizador: FakeServicioEcualizador(), + servicioGrabacion: FakeServicioGrabacionRadio(), + resolverArchivoCustom: _archivoCustomVacio, + iniciarAutomaticamente: false, + ); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); + + await tester.pumpWidget( + _conProviders(estado, _testApp(const PantallaInicio())), + ); + await _pumpStableFrame(tester); + + expect(find.byType(PluriRootHeader), findsNothing); + expect( + find.text('Escuchando ahora'), + findsOneWidget, + reason: 'prototype t4 line 53: "ESCUCHANDO AHORA"', + ); + expect(find.byIcon(Icons.bedtime_outlined), findsOneWidget); + + final cabeceras = tester + .widgetList(find.byType(SizedBox)) + .where((s) => s.height == 52); + expect(cabeceras, isNotEmpty, reason: 'prototype t4 line 53: height:52px'); + }); + + testWidgets( + "visual fidelity (audit 1.2): the header's bedtime icon still opens " + 'the shared sleep-timer sheet', + (tester) async { + _setLargeSurfaceSize(tester); + final estado = EstadoRadio( + audio: FakeServicioAudio(), + favoritos: FakeServicioFavoritos(), + radio: FakeServicioRadio(), + servicioEcualizador: FakeServicioEcualizador(), + servicioGrabacion: FakeServicioGrabacionRadio(), + resolverArchivoCustom: _archivoCustomVacio, + iniciarAutomaticamente: false, + ); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); + + await tester.pumpWidget( + _conProviders(estado, _testApp(const PantallaInicio())), + ); + await _pumpStableFrame(tester); + + await tester.tap(find.byIcon(Icons.bedtime_outlined)); + await _pumpStableFrame(tester); + + expect(find.byType(ActionChip), findsWidgets); + }, + ); + + testWidgets('visual fidelity (audit 1.5): the station name is 24/w800/ls-.5/' + 'height 1.12, not titleLarge(22) with no letter-spacing/height', ( + tester, + ) async { + _setLargeSurfaceSize(tester); + final favoritos = FakeServicioFavoritos(); + final estado = EstadoRadio( + audio: FakeServicioAudio(), + favoritos: favoritos, + radio: FakeServicioRadio(), + servicioEcualizador: FakeServicioEcualizador(), + servicioGrabacion: FakeServicioGrabacionRadio(), + resolverArchivoCustom: _archivoCustomVacio, + iniciarAutomaticamente: false, + ); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); + // Deliberately NOT added to favoritos: the "Tus emisoras" grid also + // renders a station's bare name, and this assertion targets the + // hero's OWN station-name Text specifically -- a favorited station + // with the same name would render twice and make `find.text` throw. + final sonando = emisoraDemo(uuid: 'f1', nombre: 'Radio Horizonte'); + await tester.runAsync(() => estado.reproducir(sonando)); + + await tester.pumpWidget( + _conProviders(estado, _testApp(const PantallaInicio())), + ); + await _pumpBounded(tester); + + final texto = tester.widget(find.text('Radio Horizonte')); + expect(texto.style?.fontSize, 24, reason: 'prototype t4 line 60'); + expect(texto.style?.fontWeight, FontWeight.w800); + expect(texto.style?.letterSpacing, -0.5); + expect(texto.style?.height, 1.12); + }); + + testWidgets( + 'visual fidelity (audit 1.8): the hero play button is 72x72 with a ' + '38px icon, not 56x56 with a 28px icon', + (tester) async { + _setLargeSurfaceSize(tester); + final favoritos = FakeServicioFavoritos(); + final estado = EstadoRadio( + audio: FakeServicioAudio(), + favoritos: favoritos, + radio: FakeServicioRadio(), + servicioEcualizador: FakeServicioEcualizador(), + servicioGrabacion: FakeServicioGrabacionRadio(), + resolverArchivoCustom: _archivoCustomVacio, + iniciarAutomaticamente: false, + ); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); + final sonando = emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno'); + await favoritos.agregar(sonando); + await estado.cargarFavoritos(); + await tester.runAsync(() => estado.reproducir(sonando)); + + await tester.pumpWidget( + _conProviders(estado, _testApp(const PantallaInicio())), + ); + await _pumpBounded(tester); + + final botones = tester + .widgetList(find.byType(SizedBox)) + .where((s) => s.width == 72 && s.height == 72); + expect(botones, isNotEmpty, reason: 'prototype t4 line 73'); + + final iconos = tester + .widgetList(find.byType(Icon)) + .where( + (i) => + i.icon == Icons.play_arrow_rounded || + i.icon == Icons.pause_rounded, + ); + expect(iconos, isNotEmpty); + expect(iconos.first.size, 38); + }, + ); + + testWidgets( + 'visual fidelity (audit 1.9): the EQ/record/quality pill is centred, ' + 'pill-shaped and uses expand_less at 17px -- not a left-aligned ' + 'OutlinedButton with tune_rounded', + (tester) async { + _setLargeSurfaceSize(tester); + final favoritos = FakeServicioFavoritos(); + final estado = EstadoRadio( + audio: FakeServicioAudio(), + favoritos: favoritos, + radio: FakeServicioRadio(), + servicioEcualizador: FakeServicioEcualizador(), + servicioGrabacion: FakeServicioGrabacionRadio(), + resolverArchivoCustom: _archivoCustomVacio, + iniciarAutomaticamente: false, + ); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); + final sonando = emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno'); + await favoritos.agregar(sonando); + await estado.cargarFavoritos(); + await tester.runAsync(() => estado.reproducir(sonando)); + + await tester.pumpWidget( + _conProviders(estado, _testApp(const PantallaInicio())), + ); + await _pumpBounded(tester); + + expect(find.byType(OutlinedButton), findsNothing); + expect( + find.byIcon(Icons.expand_less_rounded), + findsOneWidget, + reason: 'prototype t4 line 78: expand_less at 17px', + ); + final icono = tester.widget(find.byIcon(Icons.expand_less_rounded)); + expect(icono.size, 17); + + final pildoras = tester + .widgetList(find.byType(DecoratedBox)) + .map((d) => d.decoration) + .whereType() + .where((d) => d.borderRadius == BorderRadius.circular(999)); + expect( + pildoras, + isNotEmpty, + reason: 'prototype t4 line 78: border-radius:999px pill', + ); + }, + ); } /// Mirrors the app.dart wiring: EstadoRadio owns the domain notifiers and diff --git a/test/pantallas/pantalla_reproductor_test.dart b/test/pantallas/pantalla_reproductor_test.dart index 77595dd..ab9d3fc 100644 --- a/test/pantallas/pantalla_reproductor_test.dart +++ b/test/pantallas/pantalla_reproductor_test.dart @@ -9,7 +9,10 @@ import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/modelos/emisora.dart'; import 'package:pluriwave/modelos/preset_ecualizador.dart'; import 'package:pluriwave/pantallas/pantalla_reproductor.dart'; +import 'package:pluriwave/tema/pluriwave_tokens.dart'; import 'package:pluriwave/widgets/ecualizador_widget.dart'; +import 'package:pluriwave/widgets/pluri_glass_surface.dart'; +import 'package:pluriwave/widgets/visualizador_audio.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -548,4 +551,157 @@ void main() { ); }, ); + + group('Visual fidelity (screens 1-3 closing pass)', () { + testWidgets('audit 2.1: the hero art is a fixed 246x246, not width*0.62', ( + tester, + ) async { + final estado = crearEstado(); + addTearDown(estado.dispose); + + await montarPantalla(tester, estado); + + final sizedBox = tester.widget( + find.byKey(const Key('player-hero-art')), + ); + expect(sizedBox.width, 246, reason: 'prototype t4 line 114'); + expect(sizedBox.height, 246); + }); + + testWidgets( + 'audit 2.2: a full-bleed backdrop sits behind the whole screen', + (tester) async { + const estacion = Emisora( + uuid: 'demo-uuid', + nombre: 'Radio Demo', + url: 'https://stream.demo/radio', + favicon: 'https://example.com/art.png', + ); + final estado = crearEstado(); + addTearDown(estado.dispose); + + await montarPantalla(tester, estado, estacion: estacion); + + expect( + find.byKey(const Key('player-background-art')), + findsOneWidget, + reason: 'prototype t4 lines 108-109: full-bleed blurred backdrop', + ); + }, + ); + + testWidgets('audit 2.2: the backdrop still renders (gradient only) with no ' + 'station art available', (tester) async { + final estado = crearEstado(); + addTearDown(estado.dispose); + + await montarPantalla(tester, estado); + + expect(find.byKey(const Key('player-background-art')), findsOneWidget); + }); + + testWidgets('audit 2.3: the title is 26/w800/ls-.5/height 1.15, not ' + 'headlineSmall(24)/w700', (tester) async { + final estado = crearEstado(); + addTearDown(estado.dispose); + + await montarPantalla(tester, estado); + + final texto = tester.widget(find.text(emisora.nombre)); + expect(texto.style?.fontSize, 26, reason: 'prototype t4 line 116'); + expect(texto.style?.fontWeight, FontWeight.w800); + expect(texto.style?.letterSpacing, -0.5); + expect(texto.style?.height, 1.15); + }); + + testWidgets( + 'audit 2.8: only the EQ propio tile is accented -- tinted brand ' + 'background/border, the other three share a neutral opaque surface', + (tester) async { + final estado = crearEstado(); + addTearDown(estado.dispose); + + await montarPantalla(tester, estado); + + final eqDecoration = + tester + .widget( + find + .descendant( + of: find.byKey(const Key('player-tool-eq')), + matching: find.byType(Container), + ) + .first, + ) + .decoration + as BoxDecoration; + expect( + eqDecoration.color, + PluriWaveTokens.brand.withValues(alpha: 0.16), + reason: 'prototype t4 line 133', + ); + + final recordDecoration = + tester + .widget( + find + .descendant( + of: find.byKey(const Key('player-tool-record')), + matching: find.byType(Container), + ) + .first, + ) + .decoration + as BoxDecoration; + expect( + recordDecoration.color, + isNot(PluriWaveTokens.brand.withValues(alpha: 0.16)), + reason: 'prototype t4 line 134: no explicit accent colour', + ); + }, + ); + + testWidgets( + 'audit 2.9: the transport row is not wrapped in a glass surface ' + 'card', + (tester) async { + final estado = crearEstado(); + addTearDown(estado.dispose); + + await montarPantalla(tester, estado); + + expect( + find.ancestor( + of: find.byIcon(Icons.favorite_outline_rounded), + matching: find.byType(PluriGlassSurface), + ), + findsNothing, + reason: 'prototype t4 lines 124-131: bare row, no glass card', + ); + }, + ); + + testWidgets( + 'audit 2.10: the visualizer is not wrapped in its own glass card', + (tester) async { + final estado = crearEstado(); + addTearDown(estado.dispose); + + await montarPantalla(tester, estado); + + final visualizador = find.byKey(const Key('player-visualizer')); + expect(tester.widget(visualizador), isA()); + expect( + find.ancestor( + of: visualizador, + matching: find.byType(PluriGlassSurface), + ), + findsNothing, + reason: + 'prototype t4 lines 120-122: bars sit on the bare ' + 'background', + ); + }, + ); + }); } diff --git a/test/pantallas/root_header_wiring_test.dart b/test/pantallas/root_header_wiring_test.dart index eba2982..6dc6ed1 100644 --- a/test/pantallas/root_header_wiring_test.dart +++ b/test/pantallas/root_header_wiring_test.dart @@ -102,8 +102,19 @@ void main() { ); testWidgets( - 'Escuchar draws its own PluriRootHeader with the tab title, no AppBar', + 'Escuchar is the documented header exception: a 52px "now listening" ' + 'eyebrow instead of a PluriRootHeader title row, and still no AppBar', (tester) async { + // The prototype gives Escuchar a STRUCTURALLY different header from + // every other root (t4 line 53 vs line 325): 52px rather than 56, an + // eyebrow at 15px/w800/55% opacity rather than a 23px title, and + // trailing actions rather than a CTA. This test originally asserted + // `PluriRootHeader` on all five roots — an over-broad premise, since + // Escuchar's header was never a title row in the design. + // + // What still holds unconditionally, and is what this guards: no + // AppBar anywhere (systemic item S1), and the sleep-timer action that + // used to live in the removed global chrome is still reachable here. _suppressListTileInkAssertion(); setLargeSurface(tester); final estado = crearEstadoRadio(); @@ -112,9 +123,18 @@ void main() { await tester.pumpWidget(testApp(estado, const PantallaInicio())); await pumpStable(tester); - expect(find.byType(PluriRootHeader), findsOneWidget); - expect(titleInHeader('Listen'), findsOneWidget); expect(find.byType(AppBar), findsNothing); + expect( + find.byType(PluriRootHeader), + findsNothing, + reason: 'Escuchar uses its own eyebrow header, not the shared one', + ); + expect(find.text('Now listening'), findsOneWidget); + expect( + find.byIcon(Icons.bedtime_outlined), + findsOneWidget, + reason: 'the sleep-timer action survived the global AppBar removal', + ); }, ); diff --git a/test/tema/pluriwave_typography_test.dart b/test/tema/pluriwave_typography_test.dart index aff2587..95b9ee8 100644 --- a/test/tema/pluriwave_typography_test.dart +++ b/test/tema/pluriwave_typography_test.dart @@ -65,6 +65,20 @@ void main() { expect(type.eyebrowLabel.letterSpacing, 0.8); }); + test( + 'S7 (visual fidelity): inPageSectionHeading is 15 / w800 / ' + 'letter-spacing -0.2 -- the ADR-1 amendment adding a seventh style', + () { + expect( + type.inPageSectionHeading.fontSize, + 15, + reason: 'prototype t4 lines 80, 153, 164, 173', + ); + expect(type.inPageSectionHeading.fontWeight, FontWeight.w800); + expect(type.inPageSectionHeading.letterSpacing, -0.2); + }, + ); + test('S9 (Tier 1 visual fidelity): eyebrowLabel bakes in the prototype ' "colour rgba(242,247,250,.42) — it used to carry no colour at all, " 'so it rendered at full onSurface opacity', () {