feat(ui): design token discipline, accessibility and i18n pass
- Replace all hardcoded Color literals outside lib/tema with theme tokens (new static brand palette in PluriWaveTokens); media notification uses the brand color instead of the Material default purple - Favorite button on station cards grows to a 48dp target and becomes an independent semantics node for screen readers (Semantics container fix) - All flutter_animate call sites route through the PluriAnimate reduced-motion gate (zero direct .animate() left) - Locale-aware short dates via intl DateFormat (new lib/l10n/formato_fechas.dart) replacing the hardcoded DD/MM/YYYY; proper plural messages for the favorites counter; example stream URL as a localized key - all 13 locales - Rounded shimmer placeholders matching card radii; shimmer loading state in search instead of a bare spinner; rounded icon variants unified in settings; bottom-sheet conventions on the custom station form - Fix latent debug crash: vacation editor read AppLocalizations in initState - 11 new tests (121 total green), flutter analyze clean
This commit is contained in:
@@ -8,6 +8,7 @@ import '../l10n/display_names.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
import '../tema/pluriwave_theme.dart';
|
||||
import '../tema/pluriwave_tokens.dart';
|
||||
import 'pluri_glass_surface.dart';
|
||||
import 'pluri_icon.dart';
|
||||
|
||||
@@ -188,9 +189,19 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: SweepGradient(
|
||||
colors: [t.electricMagenta, const Color(0xFF20E6FF), t.warmCoral, t.electricMagenta],
|
||||
colors: [
|
||||
t.electricMagenta,
|
||||
PluriWaveTokens.brightCyan,
|
||||
t.warmCoral,
|
||||
t.electricMagenta,
|
||||
],
|
||||
),
|
||||
boxShadow: [BoxShadow(color: t.glowColor.withValues(alpha: 0.24), blurRadius: 22)],
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: t.glowColor.withValues(alpha: 0.24),
|
||||
blurRadius: 22,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ClipRRect(
|
||||
@@ -268,7 +279,11 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
|
||||
: l10n.favoritesAddTooltip,
|
||||
);
|
||||
|
||||
// S5-R2: container forces an OWN semantics node — without it the card's
|
||||
// InkWell absorbs the favorite into one merged node and screen readers
|
||||
// cannot reach the action independently.
|
||||
return Semantics(
|
||||
container: true,
|
||||
button: true,
|
||||
toggled: esFavorito,
|
||||
label:
|
||||
@@ -279,11 +294,8 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: _toggling ? null : _toggle,
|
||||
child: SizedBox(
|
||||
width: mini ? 36 : 44,
|
||||
height: mini ? 36 : 44,
|
||||
child: Center(child: icono),
|
||||
),
|
||||
// S5-R2: 48dp minimum touch target in both variants.
|
||||
child: SizedBox(width: 48, height: 48, child: Center(child: icono)),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -318,18 +330,21 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
|
||||
Image.asset(
|
||||
art,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
context.pluriTokens.deepViolet,
|
||||
context.pluriTokens.electricMagenta.withValues(alpha: 0.8),
|
||||
],
|
||||
errorBuilder:
|
||||
(_, __, ___) => DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
context.pluriTokens.deepViolet,
|
||||
context.pluriTokens.electricMagenta.withValues(
|
||||
alpha: 0.8,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: PluriIcon(
|
||||
@@ -365,7 +380,10 @@ class _LiveBadge extends StatelessWidget {
|
||||
final color = Theme.of(context).colorScheme.secondary;
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: mini ? 8 : 6, vertical: mini ? 5 : 4),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: mini ? 8 : 6,
|
||||
vertical: mini ? 5 : 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
color: Colors.black.withValues(alpha: 0.35),
|
||||
@@ -374,10 +392,19 @@ class _LiveBadge extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.fiber_manual_record_rounded, size: mini ? 10 : 8, color: color),
|
||||
Icon(
|
||||
Icons.fiber_manual_record_rounded,
|
||||
size: mini ? 10 : 8,
|
||||
color: color,
|
||||
),
|
||||
if (mini) ...[
|
||||
const SizedBox(width: 5),
|
||||
Text(l10n.liveNow, style: Theme.of(context).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w900)),
|
||||
Text(
|
||||
l10n.liveNow,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w900),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -386,35 +413,74 @@ class _LiveBadge extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// Placeholder shimmer para listas en carga.
|
||||
///
|
||||
/// S5-R6: corners match the real card radii; [esCompacta] mirrors the
|
||||
/// compact row layout used in search results.
|
||||
class TarjetaEmisoraShimmer extends StatelessWidget {
|
||||
const TarjetaEmisoraShimmer({super.key});
|
||||
const TarjetaEmisoraShimmer({super.key, this.esCompacta = false});
|
||||
|
||||
final bool esCompacta;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final t = context.pluriTokens;
|
||||
final color = theme.colorScheme.surfaceContainerHighest;
|
||||
|
||||
Widget bloque({
|
||||
double? width,
|
||||
double? height,
|
||||
double? radius,
|
||||
BoxShape shape = BoxShape.rectangle,
|
||||
}) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: shape,
|
||||
borderRadius:
|
||||
shape == BoxShape.circle || radius == null
|
||||
? null
|
||||
: BorderRadius.circular(radius),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final contenido =
|
||||
esCompacta
|
||||
? Row(
|
||||
children: [
|
||||
bloque(width: 58, height: 58, shape: BoxShape.circle),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
bloque(height: 14, radius: 6),
|
||||
const SizedBox(height: 6),
|
||||
bloque(height: 12, width: 90, radius: 6),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(aspectRatio: 1, child: bloque(radius: t.radiusLg)),
|
||||
const SizedBox(height: 8),
|
||||
bloque(height: 14, radius: 6),
|
||||
const SizedBox(height: 4),
|
||||
bloque(height: 12, width: 60, radius: 6),
|
||||
],
|
||||
);
|
||||
|
||||
return Shimmer.fromColors(
|
||||
baseColor: theme.colorScheme.surfaceContainerHighest,
|
||||
baseColor: color,
|
||||
highlightColor: theme.colorScheme.surface,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Container(color: theme.colorScheme.surfaceContainerHighest),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
height: 14,
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
height: 12,
|
||||
width: 60,
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: contenido,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user