fix(buscar): filter pills, Idioma chip, results eyebrow, states screen
Audit 6.2 (t4:292-293): active filter pills are brand-teal tinted with
a 15px close glyph and radius 10 -- was Material's own Chip theming.
Audit 6.3 (t4:294-295): an "Idioma" entry chip is now always reachable
once a search is active -- there was no standalone entry point for
language filtering before (only bundled inside "Filtros"). Opens the
same existing filter sheet rather than a new idioma-only picker.
Audit 6.4 (t4:299): the results-count line now uses eyebrowLabel
styling -- was labelLarge (14/w800).
Audit 13.3/13.4 (t4:649-651): the search-results loading skeleton
gains the missing "BUSCANDO EMISORAS..." eyebrow, and its row gap is
now 4px, not 10.
Audit 13.5/13.6 (t4:657-668): a new purpose-built _TarjetaSinResultados
replaces PluriEmptyState ONLY in the search-no-results branch (that
shared widget is left untouched for its other unrelated call sites --
favorites empty state, the discovery grid). The title now quotes the
typed query ("Sin resultados para <<jazzz>>"), and the clear-filters
pill sits INSIDE the card. New ARB key searchNoResultsForQueryTitle.
Item 6.1 (compact 60px active-search header replacing the persistent
glass pill) is NOT implemented: existing tests confirmed the quality
(bitrate) filter has NO entry point anywhere in the app besides the
header's "Filtros" pill (added first via _abrirYSeleccionarCalidad
while a country filter is ALREADY active, i.e. mid active-search).
Hiding PluriRootHeader during active search would make quality
filtering unreachable -- a real capability regression the task's own
"presentation changes, capability does not" principle forbids.
Re-plumbing where that filter lives is a bigger change than this
visual-fidelity pass should make unilaterally.
This commit is contained in:
@@ -253,10 +253,25 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
}),
|
||||
];
|
||||
|
||||
if (pills.isEmpty && estado.resultados.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
// Audit 6.3 (t4:294-295): "Idioma" is an always-reachable entry chip
|
||||
// once a search is active -- there was no standalone entry point for
|
||||
// language filtering before this (only bundled inside "Filtros").
|
||||
// Opens the SAME existing filter sheet (all 3 sections) rather than a
|
||||
// new idioma-only picker: "Ordenar" is a genuinely separate control
|
||||
// (not part of that sheet), and "Filtros" itself must stay reachable
|
||||
// from the header regardless of active-filter state -- the quality
|
||||
// (bitrate) filter has no OTHER entry point anywhere in the app, so
|
||||
// this chip is additive, not a replacement.
|
||||
final entryChips = <Widget>[
|
||||
ActionChip(
|
||||
label: Text(l10n.searchLanguageFilterLabel),
|
||||
onPressed: _abrirFiltros,
|
||||
),
|
||||
];
|
||||
|
||||
// Audit 6.3: entryChips is never empty (always carries "Idioma"), so
|
||||
// this row now always renders while a search is active -- the old
|
||||
// "nothing to show" early return no longer applies.
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
@@ -267,18 +282,16 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (pills.isNotEmpty)
|
||||
Wrap(spacing: 8, runSpacing: 8, children: pills),
|
||||
if (pills.isNotEmpty && estado.resultados.isNotEmpty)
|
||||
Wrap(spacing: 8, runSpacing: 8, children: [...pills, ...entryChips]),
|
||||
if (!estado.cargando && estado.resultados.isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
if (!estado.cargando && estado.resultados.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
// Audit 6.4 (t4:299): eyebrow styling (11/w800/ls.06em) --
|
||||
// was labelLarge (14/w800).
|
||||
Text(
|
||||
l10n.searchResultsCount(estado.resultados.length),
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
style: context.pluriType.eyebrowLabel,
|
||||
),
|
||||
const Spacer(),
|
||||
PopupMenuButton<OrdenEmisoras>(
|
||||
@@ -306,16 +319,28 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Audit 6.2 (t4:292-293): brand-teal tinted, radius 10, an inline 15px
|
||||
/// close glyph -- was Material's own `Chip` theming.
|
||||
Widget _pillFiltro(String label, VoidCallback onDeleted) {
|
||||
return Chip(
|
||||
label: Text(label),
|
||||
labelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Color(0xFFF2F7FA),
|
||||
),
|
||||
backgroundColor: PluriWaveTokens.brand.withValues(alpha: 0.2),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: BorderSide(color: PluriWaveTokens.brand.withValues(alpha: 0.45)),
|
||||
),
|
||||
onDeleted: onDeleted,
|
||||
deleteIcon: const Icon(Icons.close, size: 18),
|
||||
deleteIcon: const Icon(Icons.close, size: 15),
|
||||
visualDensity: VisualDensity.compact,
|
||||
);
|
||||
}
|
||||
@@ -521,10 +546,21 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(PluriLayout.horizontal),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Audit 13.3 (t4:649): the "BUSCANDO EMISORAS…" eyebrow --
|
||||
// never rendered before.
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Text(
|
||||
l10n.searchLoadingStationsLabel,
|
||||
style: context.pluriType.eyebrowLabel,
|
||||
),
|
||||
),
|
||||
for (var i = 0; i < 4; i++) ...[
|
||||
const TarjetaEmisoraShimmer(esCompacta: true),
|
||||
if (i < 3) const SizedBox(height: 10),
|
||||
// Audit 13.4 (t4:651): 4px between skeleton rows, not 10.
|
||||
if (i < 3) const SizedBox(height: 4),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -535,36 +571,40 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
|
||||
if (resultados.isEmpty) {
|
||||
final sinFiltros = _controller.text.isEmpty && _filtrosActivosCount == 0;
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 260,
|
||||
child: PluriEmptyState(
|
||||
glyph: PluriIconGlyph.search,
|
||||
title:
|
||||
sinFiltros
|
||||
? l10n.searchEmptyTitle
|
||||
: l10n.searchNoResultsTitle,
|
||||
subtitle:
|
||||
sinFiltros
|
||||
? l10n.searchEmptySubtitle
|
||||
: l10n.searchNoResultsSubtitle,
|
||||
),
|
||||
),
|
||||
// Audit 13.5/13.6 (t4:657-668): a purpose-built card for the
|
||||
// search-no-results state -- centred, `listSurface`, the title
|
||||
// QUOTES the typed query, and the clear-filters pill sits INSIDE
|
||||
// the card. A NEW widget, not a restyle of the shared
|
||||
// `PluriEmptyState` (used by several OTHER unrelated empty states
|
||||
// across the app -- favorites, the discovery grid -- which this
|
||||
// item does not touch).
|
||||
final query = _controller.text.trim();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: PluriLayout.horizontal),
|
||||
child: _TarjetaSinResultados(
|
||||
titulo:
|
||||
sinFiltros
|
||||
? l10n.searchEmptyTitle
|
||||
: query.isNotEmpty
|
||||
? l10n.searchNoResultsForQueryTitle(query)
|
||||
: l10n.searchNoResultsTitle,
|
||||
subtitulo:
|
||||
sinFiltros
|
||||
? l10n.searchEmptySubtitle
|
||||
: l10n.searchNoResultsSubtitle,
|
||||
// task 6.6 / spec "One-Tap Clear-All-Filters on Empty Results":
|
||||
// only offered once 1+ pill-filters are active AND the search
|
||||
// came back empty — not merely "no query typed yet".
|
||||
if (_filtrosActivosCount > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: OutlinedButton(
|
||||
onPressed: _quitarTodosLosFiltros,
|
||||
child: Text(
|
||||
l10n.searchClearFiltersAction(_filtrosActivosCount),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
accionQuitarFiltros:
|
||||
_filtrosActivosCount > 0
|
||||
? (
|
||||
etiqueta: l10n.searchClearFiltersAction(
|
||||
_filtrosActivosCount,
|
||||
),
|
||||
onTap: _quitarTodosLosFiltros,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1213,6 +1253,111 @@ class _ChipShimmer extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Audit 13.5/13.6 (t4:657-668): search-no-results card -- centred,
|
||||
/// `listSurface`, radius 22 (matches neither of the other 2 named token
|
||||
/// radii, so this is a local one-off like `_CeldaExplorarPor`'s own),
|
||||
/// with the clear-filters pill living INSIDE the card rather than below
|
||||
/// it.
|
||||
class _TarjetaSinResultados extends StatelessWidget {
|
||||
const _TarjetaSinResultados({
|
||||
required this.titulo,
|
||||
required this.subtitulo,
|
||||
this.accionQuitarFiltros,
|
||||
});
|
||||
|
||||
final String titulo;
|
||||
final String subtitulo;
|
||||
final ({String etiqueta, VoidCallback onTap})? accionQuitarFiltros;
|
||||
|
||||
static const _radio = 22.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accion = accionQuitarFiltros;
|
||||
return DecoratedBox(
|
||||
key: const ValueKey('search-no-results-card'),
|
||||
decoration: BoxDecoration(
|
||||
color: PluriWaveTokens.dark.listSurface,
|
||||
borderRadius: BorderRadius.circular(_radio),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.07)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 26),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search_off_rounded,
|
||||
size: 40,
|
||||
color: PluriWaveTokens.brand.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
titulo,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w800),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
subtitulo,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
height: 1.5,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
if (accion != null) ...[
|
||||
const SizedBox(height: 14),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: accion.onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 15,
|
||||
vertical: 9,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: PluriWaveTokens.brand.withValues(alpha: 0.18),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: PluriWaveTokens.brand.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.filter_alt_off_rounded,
|
||||
size: 17,
|
||||
color: PluriWaveTokens.brand,
|
||||
),
|
||||
const SizedBox(width: 7),
|
||||
Text(
|
||||
accion.etiqueta,
|
||||
style: const TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: PluriWaveTokens.brand,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
Reference in New Issue
Block a user