fix(buscar): build the 2x2 "Explorar por" grid, add Novedades

Audit 3.2 (t4 lines 162-171): the prototype groups Países, Géneros,
Tendencias and the entirely-missing Novedades into one 2x2 entry-point
grid. The build had them as three unrelated always-visible widgets
(a Países ListTile, a Géneros FilterChip Wrap, a Tendencias ActionChip
strip) and no Novedades entry at all.

Replaces all three with a single grid section, capability-preserving:
Países still pushes PantallaPaises; Géneros and Tendencias now open
their exact existing content in a picker sheet instead of always-on-
screen (Géneros auto-closes on selection, matching this screen's other
single-choice filter sheets; Tendencias stays open, a browse list, not
a filter). Novedades re-triggers the existing discovery refresh, since
no distinct "new stations" feed exists anywhere in the domain.

New ARB key set (exploreByTitle/exploreTrendingTitle/
exploreTrendingSubtitle/exploreNewTitle/exploreNewSubtitle) translated
across all 13 locales with zero anti-copy allowlist entries needed.

Also fixes a pre-existing PluriEmptyState overflow this restructure
exposed (unrelated to the grid itself, confirmed via isolated repro):
wraps its Column in a SingleChildScrollView so a too-tall title/
subtitle scrolls instead of throwing a hard RenderFlex overflow.
This commit is contained in:
2026-07-30 00:43:27 +02:00
parent a49b2b6519
commit e5d461af53
31 changed files with 755 additions and 58 deletions
+5
View File
@@ -306,6 +306,11 @@
"detectAction": "اكتشاف",
"liveRadar": "رادار مباشر",
"genresTitle": "الأنواع",
"exploreByTitle": "استكشاف حسب",
"exploreTrendingTitle": "الرائجة",
"exploreTrendingSubtitle": "اليوم",
"exploreNewTitle": "جديد",
"exploreNewSubtitle": "هذا الأسبوع",
"offlineBannerTitle": "لا يوجد اتصال",
"retryAction": "إعادة المحاولة",
"noStationsAvailable": "لا توجد محطات متاحة",
+5
View File
@@ -306,6 +306,11 @@
"detectAction": "সনাক্ত করুন",
"liveRadar": "লাইভ রাডার",
"genresTitle": "ধরন",
"exploreByTitle": "যেভাবে অন্বেষণ করুন",
"exploreTrendingTitle": "ট্রেন্ডিং",
"exploreTrendingSubtitle": "আজ",
"exploreNewTitle": "নতুন",
"exploreNewSubtitle": "এই সপ্তাহে",
"offlineBannerTitle": "সংযোগ নেই",
"retryAction": "আবার চেষ্টা করুন",
"noStationsAvailable": "কোনো স্টেশন উপলব্ধ নেই",
+5
View File
@@ -306,6 +306,11 @@
"detectAction": "Erkennen",
"liveRadar": "Live-Radar",
"genresTitle": "Genres",
"exploreByTitle": "Entdecken nach",
"exploreTrendingTitle": "Trends",
"exploreTrendingSubtitle": "Heute",
"exploreNewTitle": "Neuheiten",
"exploreNewSubtitle": "Diese Woche",
"offlineBannerTitle": "Keine Verbindung",
"retryAction": "Erneut versuchen",
"noStationsAvailable": "Keine Sender verfügbar",
+5
View File
@@ -369,6 +369,11 @@
"detectAction": "Detect",
"liveRadar": "Live radar",
"genresTitle": "Genres",
"exploreByTitle": "Browse by",
"exploreTrendingTitle": "Trending",
"exploreTrendingSubtitle": "Today",
"exploreNewTitle": "New",
"exploreNewSubtitle": "This week",
"offlineBannerTitle": "No connection",
"retryAction": "Retry",
"noStationsAvailable": "No stations available",
+5
View File
@@ -369,6 +369,11 @@
"detectAction": "Detectar",
"liveRadar": "Radar en directo",
"genresTitle": "Géneros",
"exploreByTitle": "Explorar por",
"exploreTrendingTitle": "Tendencias",
"exploreTrendingSubtitle": "Hoy",
"exploreNewTitle": "Novedades",
"exploreNewSubtitle": "Esta semana",
"offlineBannerTitle": "Sin conexión",
"retryAction": "Reintentar",
"noStationsAvailable": "No hay emisoras disponibles",
+5
View File
@@ -306,6 +306,11 @@
"detectAction": "Détecter",
"liveRadar": "Radar en direct",
"genresTitle": "Genres",
"exploreByTitle": "Explorer par",
"exploreTrendingTitle": "Tendances",
"exploreTrendingSubtitle": "Aujourd'hui",
"exploreNewTitle": "Nouveautés",
"exploreNewSubtitle": "Cette semaine",
"offlineBannerTitle": "Aucune connexion",
"retryAction": "Réessayer",
"noStationsAvailable": "Aucune station disponible",
+5
View File
@@ -306,6 +306,11 @@
"detectAction": "पहचानें",
"liveRadar": "लाइव रडार",
"genresTitle": "शैलियाँ",
"exploreByTitle": "इस अनुसार खोजें",
"exploreTrendingTitle": "ट्रेंडिंग",
"exploreTrendingSubtitle": "आज",
"exploreNewTitle": "नया",
"exploreNewSubtitle": "इस सप्ताह",
"offlineBannerTitle": "कोई कनेक्शन नहीं",
"retryAction": "फिर कोशिश करें",
"noStationsAvailable": "कोई स्टेशन उपलब्ध नहीं",
+5
View File
@@ -306,6 +306,11 @@
"detectAction": "Deteksi",
"liveRadar": "Radar langsung",
"genresTitle": "Genre",
"exploreByTitle": "Jelajahi berdasarkan",
"exploreTrendingTitle": "Tren",
"exploreTrendingSubtitle": "Hari ini",
"exploreNewTitle": "Baru",
"exploreNewSubtitle": "Minggu ini",
"offlineBannerTitle": "Tidak ada koneksi",
"retryAction": "Coba lagi",
"noStationsAvailable": "Tidak ada stasiun tersedia",
+5
View File
@@ -306,6 +306,11 @@
"detectAction": "Rileva",
"liveRadar": "Radar live",
"genresTitle": "Generi",
"exploreByTitle": "Esplora per",
"exploreTrendingTitle": "Tendenze",
"exploreTrendingSubtitle": "Oggi",
"exploreNewTitle": "Novità",
"exploreNewSubtitle": "Questa settimana",
"offlineBannerTitle": "Nessuna connessione",
"retryAction": "Riprova",
"noStationsAvailable": "Nessuna emittente disponibile",
+5
View File
@@ -306,6 +306,11 @@
"detectAction": "検出",
"liveRadar": "ライブレーダー",
"genresTitle": "ジャンル",
"exploreByTitle": "カテゴリで探す",
"exploreTrendingTitle": "トレンド",
"exploreTrendingSubtitle": "今日",
"exploreNewTitle": "新着",
"exploreNewSubtitle": "今週",
"offlineBannerTitle": "接続なし",
"retryAction": "再試行",
"noStationsAvailable": "利用できる局がありません",
+5
View File
@@ -306,6 +306,11 @@
"detectAction": "Detectar",
"liveRadar": "Radar ao vivo",
"genresTitle": "Gêneros",
"exploreByTitle": "Navegar por",
"exploreTrendingTitle": "Tendências",
"exploreTrendingSubtitle": "Hoje",
"exploreNewTitle": "Novidades",
"exploreNewSubtitle": "Nesta semana",
"offlineBannerTitle": "Sem conexão",
"retryAction": "Tentar novamente",
"noStationsAvailable": "Não há estações disponíveis",
+5
View File
@@ -306,6 +306,11 @@
"detectAction": "Определить",
"liveRadar": "Радар в эфире",
"genresTitle": "Жанры",
"exploreByTitle": "Обзор по",
"exploreTrendingTitle": "В тренде",
"exploreTrendingSubtitle": "Сегодня",
"exploreNewTitle": "Новинки",
"exploreNewSubtitle": "На этой неделе",
"offlineBannerTitle": "Нет подключения",
"retryAction": "Повторить",
"noStationsAvailable": "Нет доступных станций",
+5
View File
@@ -306,6 +306,11 @@
"detectAction": "检测",
"liveRadar": "直播雷达",
"genresTitle": "类型",
"exploreByTitle": "浏览分类",
"exploreTrendingTitle": "热门",
"exploreTrendingSubtitle": "今天",
"exploreNewTitle": "最新",
"exploreNewSubtitle": "本周",
"offlineBannerTitle": "无连接",
"retryAction": "重试",
"noStationsAvailable": "没有可用电台",
+30
View File
@@ -1438,6 +1438,36 @@ abstract class AppLocalizations {
/// **'Géneros'**
String get genresTitle;
/// No description provided for @exploreByTitle.
///
/// In es, this message translates to:
/// **'Explorar por'**
String get exploreByTitle;
/// No description provided for @exploreTrendingTitle.
///
/// In es, this message translates to:
/// **'Tendencias'**
String get exploreTrendingTitle;
/// No description provided for @exploreTrendingSubtitle.
///
/// In es, this message translates to:
/// **'Hoy'**
String get exploreTrendingSubtitle;
/// No description provided for @exploreNewTitle.
///
/// In es, this message translates to:
/// **'Novedades'**
String get exploreNewTitle;
/// No description provided for @exploreNewSubtitle.
///
/// In es, this message translates to:
/// **'Esta semana'**
String get exploreNewSubtitle;
/// No description provided for @offlineBannerTitle.
///
/// In es, this message translates to:
+15
View File
@@ -766,6 +766,21 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get genresTitle => 'الأنواع';
@override
String get exploreByTitle => 'استكشاف حسب';
@override
String get exploreTrendingTitle => 'الرائجة';
@override
String get exploreTrendingSubtitle => 'اليوم';
@override
String get exploreNewTitle => 'جديد';
@override
String get exploreNewSubtitle => 'هذا الأسبوع';
@override
String get offlineBannerTitle => 'لا يوجد اتصال';
+15
View File
@@ -769,6 +769,21 @@ class AppLocalizationsBn extends AppLocalizations {
@override
String get genresTitle => 'ধরন';
@override
String get exploreByTitle => 'যেভাবে অন্বেষণ করুন';
@override
String get exploreTrendingTitle => 'ট্রেন্ডিং';
@override
String get exploreTrendingSubtitle => 'আজ';
@override
String get exploreNewTitle => 'নতুন';
@override
String get exploreNewSubtitle => 'এই সপ্তাহে';
@override
String get offlineBannerTitle => 'সংযোগ নেই';
+15
View File
@@ -772,6 +772,21 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get genresTitle => 'Genres';
@override
String get exploreByTitle => 'Entdecken nach';
@override
String get exploreTrendingTitle => 'Trends';
@override
String get exploreTrendingSubtitle => 'Heute';
@override
String get exploreNewTitle => 'Neuheiten';
@override
String get exploreNewSubtitle => 'Diese Woche';
@override
String get offlineBannerTitle => 'Keine Verbindung';
+15
View File
@@ -765,6 +765,21 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get genresTitle => 'Genres';
@override
String get exploreByTitle => 'Browse by';
@override
String get exploreTrendingTitle => 'Trending';
@override
String get exploreTrendingSubtitle => 'Today';
@override
String get exploreNewTitle => 'New';
@override
String get exploreNewSubtitle => 'This week';
@override
String get offlineBannerTitle => 'No connection';
+15
View File
@@ -770,6 +770,21 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get genresTitle => 'Géneros';
@override
String get exploreByTitle => 'Explorar por';
@override
String get exploreTrendingTitle => 'Tendencias';
@override
String get exploreTrendingSubtitle => 'Hoy';
@override
String get exploreNewTitle => 'Novedades';
@override
String get exploreNewSubtitle => 'Esta semana';
@override
String get offlineBannerTitle => 'Sin conexión';
+15
View File
@@ -774,6 +774,21 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get genresTitle => 'Genres';
@override
String get exploreByTitle => 'Explorer par';
@override
String get exploreTrendingTitle => 'Tendances';
@override
String get exploreTrendingSubtitle => 'Aujourd\'hui';
@override
String get exploreNewTitle => 'Nouveautés';
@override
String get exploreNewSubtitle => 'Cette semaine';
@override
String get offlineBannerTitle => 'Aucune connexion';
+15
View File
@@ -766,6 +766,21 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get genresTitle => 'शैलियाँ';
@override
String get exploreByTitle => 'इस अनुसार खोजें';
@override
String get exploreTrendingTitle => 'ट्रेंडिंग';
@override
String get exploreTrendingSubtitle => 'आज';
@override
String get exploreNewTitle => 'नया';
@override
String get exploreNewSubtitle => 'इस सप्ताह';
@override
String get offlineBannerTitle => 'कोई कनेक्शन नहीं';
+15
View File
@@ -766,6 +766,21 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get genresTitle => 'Genre';
@override
String get exploreByTitle => 'Jelajahi berdasarkan';
@override
String get exploreTrendingTitle => 'Tren';
@override
String get exploreTrendingSubtitle => 'Hari ini';
@override
String get exploreNewTitle => 'Baru';
@override
String get exploreNewSubtitle => 'Minggu ini';
@override
String get offlineBannerTitle => 'Tidak ada koneksi';
+15
View File
@@ -772,6 +772,21 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get genresTitle => 'Generi';
@override
String get exploreByTitle => 'Esplora per';
@override
String get exploreTrendingTitle => 'Tendenze';
@override
String get exploreTrendingSubtitle => 'Oggi';
@override
String get exploreNewTitle => 'Novità';
@override
String get exploreNewSubtitle => 'Questa settimana';
@override
String get offlineBannerTitle => 'Nessuna connessione';
+15
View File
@@ -740,6 +740,21 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get genresTitle => 'ジャンル';
@override
String get exploreByTitle => 'カテゴリで探す';
@override
String get exploreTrendingTitle => 'トレンド';
@override
String get exploreTrendingSubtitle => '今日';
@override
String get exploreNewTitle => '新着';
@override
String get exploreNewSubtitle => '今週';
@override
String get offlineBannerTitle => '接続なし';
+15
View File
@@ -769,6 +769,21 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get genresTitle => 'Gêneros';
@override
String get exploreByTitle => 'Navegar por';
@override
String get exploreTrendingTitle => 'Tendências';
@override
String get exploreTrendingSubtitle => 'Hoje';
@override
String get exploreNewTitle => 'Novidades';
@override
String get exploreNewSubtitle => 'Nesta semana';
@override
String get offlineBannerTitle => 'Sem conexão';
+15
View File
@@ -770,6 +770,21 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get genresTitle => 'Жанры';
@override
String get exploreByTitle => 'Обзор по';
@override
String get exploreTrendingTitle => 'В тренде';
@override
String get exploreTrendingSubtitle => 'Сегодня';
@override
String get exploreNewTitle => 'Новинки';
@override
String get exploreNewSubtitle => 'На этой неделе';
@override
String get offlineBannerTitle => 'Нет подключения';
+15
View File
@@ -737,6 +737,21 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get genresTitle => '类型';
@override
String get exploreByTitle => '浏览分类';
@override
String get exploreTrendingTitle => '热门';
@override
String get exploreTrendingSubtitle => '今天';
@override
String get exploreNewTitle => '最新';
@override
String get exploreNewSubtitle => '本周';
@override
String get offlineBannerTitle => '无连接';
+232 -34
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart' as shimmer;
@@ -8,6 +10,7 @@ import '../l10n/gen/app_localizations.dart';
import '../modelos/emisora.dart';
import '../tema/pluri_animate.dart';
import '../tema/pluriwave_theme.dart';
import '../tema/pluriwave_tokens.dart';
import '../widgets/pluri_glass_surface.dart';
import '../widgets/pluri_icon.dart';
import '../widgets/pluri_layout.dart';
@@ -199,9 +202,11 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
_resultados(estado, theme),
] else ...[
_seccionCercanas(context, theme, l10n),
_seccionTendencias(context, theme, l10n),
_chipGeneros(context, theme, l10n),
_seccionPaises(context, theme, l10n),
// Audit 3.2 (t4 lines 162-171): a single "Explorar por" 2x2 grid
// replaces the always-visible Tendencias chip strip, Géneros
// chip Wrap and Países ListTile — each cell keeps its EXACT
// existing capability, just behind a tap instead of always-on.
_seccionExplorarPor(context, theme, l10n),
if (context.select<EstadoRadio, String?>((e) => e.error) != null)
_errorBanner(
context,
@@ -798,6 +803,12 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
} else {
context.read<EstadoRadio>().cargarPopulares();
}
// Audit 3.2: only reachable from the "Explorar por"
// Géneros sheet now — tap-once-and-close, matching
// this screen's other single-choice filter sheets.
if (Navigator.canPop(context)) {
Navigator.of(context).pop();
}
},
);
}).toList(),
@@ -808,43 +819,157 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
);
}
/// WU7, `station-discovery-browse` spec: the "Países entry point" the
/// spec's landing-state scenario lists. Deferred from WU6 (this class's
/// own doc comment: `PantallaPaises` did not exist yet); added here now
/// that it does, alongside the screen it targets — an unreachable screen
/// would repeat the WU15/WU15b lesson (a fully-tested screen shipped with
/// no navigation path to it).
Widget _seccionPaises(
/// Audit 3.2 (t4 lines 162-171): "Explorar por" 2x2 grid. Replaces the
/// old always-visible Países `ListTile` (WU7), Géneros chip `Wrap` and
/// Tendencias chip strip — 4 cells, adding the entirely-missing
/// Novedades entry. Task constraint: presentation changes, capability
/// does not — Países still pushes `PantallaPaises` (WU7's own reasoning
/// for why that screen must stay reachable still applies); Géneros and
/// Tendencias now open their EXACT existing content in a picker sheet
/// instead of always-on-screen, so no selection logic is duplicated.
Widget _seccionExplorarPor(
BuildContext context,
ThemeData theme,
AppLocalizations l10n,
) {
return Padding(
padding: const EdgeInsets.fromLTRB(
PluriLayout.horizontal,
8,
PluriLayout.horizontal,
0,
),
child: PluriGlassSurface(
padding: EdgeInsets.zero,
// `PluriGlassSurface` paints via `DecoratedBox`, not `Material` — a
// tappable `ListTile` needs its own `Material` ancestor or its ink
// splash silently fails to paint (Flutter's own debug assertion).
child: Material(
type: MaterialType.transparency,
child: ListTile(
leading: const Icon(Icons.public_rounded),
title: Text(l10n.countriesScreenTitle),
trailing: const Icon(Icons.chevron_right_rounded),
onTap:
() => PluriPushScaffold.push(
context,
(_) => const PantallaPaises(),
),
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(
PluriLayout.titleHorizontal,
8,
PluriLayout.titleHorizontal,
0,
),
child: Text(
l10n.exploreByTitle,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w900,
),
),
),
),
const SizedBox(height: 10),
Padding(
key: const Key('explore-by-grid'),
padding: const EdgeInsets.symmetric(
horizontal: PluriLayout.horizontal,
),
// A manually-built 2-per-row layout, NOT `GridView` +
// `childAspectRatio`: a fixed aspect ratio sizes each cell's
// height as a function of the SCREEN width, which overflowed at
// a narrow/default test viewport even though the same ratio fit
// fine at the wider viewport this was first verified against.
// `Expanded` cells size their height from their OWN content,
// which cannot overflow this way at any width.
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _CeldaExplorarPor(
key: const Key('explore-cell-paises'),
icon: Icons.public_rounded,
color: PluriWaveTokens.brand,
title: l10n.countriesScreenTitle,
// No subtitle: a live count needs
// EstadoBusqueda.cargarPaises() triggered from this
// screen, which was tried and reverted — it forced
// an extra rebuild that (harmlessly in production,
// but fatally under the default 800-wide test
// viewport) surfaced a pre-existing, unrelated
// PluriEmptyState overflow in the empty discovery
// grid below. Not worth the coupling for a cosmetic
// badge.
subtitle: null,
onTap:
() => PluriPushScaffold.push(
context,
(_) => const PantallaPaises(),
),
),
),
const SizedBox(width: 10),
Expanded(
child: _CeldaExplorarPor(
key: const Key('explore-cell-generos'),
icon: Icons.library_music_rounded,
color: context.pluriTokens.liveGreen,
title: l10n.genresTitle,
subtitle: '${_generos.length}',
onTap: () => _abrirGenerosSheet(theme, l10n),
),
),
],
),
const SizedBox(height: 10),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _CeldaExplorarPor(
key: const Key('explore-cell-tendencias'),
icon: Icons.trending_up_rounded,
color: context.pluriTokens.warmCoral,
title: l10n.exploreTrendingTitle,
subtitle: l10n.exploreTrendingSubtitle,
onTap: () => _abrirTendenciasSheet(theme, l10n),
),
),
const SizedBox(width: 10),
Expanded(
child: _CeldaExplorarPor(
key: const Key('explore-cell-novedades'),
icon: Icons.fiber_new_rounded,
color: PluriWaveTokens.skyBlue,
title: l10n.exploreNewTitle,
subtitle: l10n.exploreNewSubtitle,
// No distinct "new stations" feed exists in the
// domain (ServicioRadio/EstadoRadio have no such
// concept, and Emisora carries no added/changed
// timestamp) — this re-runs the SAME discovery
// refresh the offline banner's retry action already
// calls, rather than inventing one.
onTap:
() => unawaited(
context.read<EstadoRadio>().cargarPopulares(),
),
),
),
],
),
],
),
),
],
);
}
/// Opens [_chipGeneros]'s EXACT existing content/selection logic behind
/// a tap instead of always-on-screen — same capability, same state
/// (`_generoSeleccionado`), same calls (`EstadoBusqueda.buscar(tag:)` /
/// `EstadoRadio.cargarPopulares()`). Auto-closes on selection, matching
/// this screen's country/language/quality filter sheets (single-choice
/// picker, not a browse list).
Future<void> _abrirGenerosSheet(ThemeData theme, AppLocalizations l10n) {
return showModalBottomSheet(
context: context,
showDragHandle: true,
builder: (ctx) => SafeArea(child: _chipGeneros(ctx, theme, l10n)),
);
}
/// Opens [_seccionTendencias]'s EXACT existing content/tap-to-play logic
/// behind a tap. Deliberately does NOT auto-close on selection — unlike
/// Géneros, this is a browse-and-preview list (tapping a station starts
/// playback in place, the same as tapping any station card elsewhere in
/// the app; it does not "choose" a single filter value).
Future<void> _abrirTendenciasSheet(ThemeData theme, AppLocalizations l10n) {
return showModalBottomSheet(
context: context,
showDragHandle: true,
builder: (ctx) => SafeArea(child: _seccionTendencias(ctx, theme, l10n)),
);
}
@@ -1037,3 +1162,76 @@ class _ChipShimmer extends StatelessWidget {
);
}
}
/// Audit 3.2 (t4 lines 167-170): one "Explorar por" grid cell — icon,
/// title (13.5/w800) and subtitle (11/55%). Radius 16 is a local one-off
/// (like `_errorBanner`'s), matching neither of the 3 named token radii.
class _CeldaExplorarPor extends StatelessWidget {
const _CeldaExplorarPor({
super.key,
required this.icon,
required this.color,
required this.title,
required this.subtitle,
required this.onTap,
});
final IconData icon;
final Color color;
final String title;
final String? subtitle;
final VoidCallback onTap;
static const _radio = 16.0;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return PluriGlassSurface(
borderRadius: BorderRadius.circular(_radio),
padding: const EdgeInsets.all(14),
child: Material(
type: MaterialType.transparency,
child: InkWell(
borderRadius: BorderRadius.circular(_radio),
onTap: onTap,
child: Row(
children: [
Icon(icon, size: 24, color: color),
const SizedBox(width: 11),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 13.5,
fontWeight: FontWeight.w800,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (subtitle != null)
Text(
subtitle!,
style: TextStyle(
fontSize: 11,
color: theme.colorScheme.onSurface.withValues(
alpha: 0.55,
),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
),
);
}
}
+34 -23
View File
@@ -74,31 +74,42 @@ class PluriEmptyState extends StatelessWidget {
child: PluriGlassSurface(
borderRadius: BorderRadius.circular(t.radiusLg + 10),
padding: const EdgeInsets.all(28),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
PluriIcon(
glyph: glyph,
variant: PluriIconVariant.activeGlow,
size: 58,
),
const SizedBox(height: 18),
Text(
title,
textAlign: TextAlign.center,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w900,
// Bugfix: every call site constrains this widget to a fixed
// height (`SizedBox(height: 260)`). At a narrow width with
// realistic two-line title/subtitle copy, the icon + gaps +
// wrapped text can exceed what's actually left after this
// surface's own padding — a hard RenderFlex overflow rather
// than a graceful clip. `SingleChildScrollView` is a no-op
// visually whenever the content already fits (the normal
// case, on any real device width this was designed for) and
// only engages scrolling in the rare case it does not.
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
PluriIcon(
glyph: glyph,
variant: PluriIconVariant.activeGlow,
size: 58,
),
),
const SizedBox(height: 8),
Text(
subtitle,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.72),
const SizedBox(height: 18),
Text(
title,
textAlign: TextAlign.center,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w900,
),
),
),
],
const SizedBox(height: 8),
Text(
subtitle,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.72),
),
),
],
),
),
),
),