FilaAjuste's title Text had no maxLines/overflow, and neither did its trailing current-value Text. An unbounded value (e.g. a real station name in "Emisora preferida") let the trailing Row claim unbounded width, squeezing the title down until it wrapped across several lines that the row's fixed height then cut short. Constrain the title to a single ellipsized line and cap the trailing value's width the same way. FilaAjuste backs all 12 settings rows, so every row is protected, not just the one that happened to expose it.
161 lines
6.4 KiB
Dart
161 lines
6.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../../tema/pluriwave_theme.dart';
|
|
import '../../../widgets/pluri_glass_surface.dart';
|
|
import '../../../widgets/pluri_layout.dart';
|
|
|
|
/// Design ADR-3: the two nav-row primitives every Settings detail screen is
|
|
/// reached through. [GrupoAjustes] is a single [PluriGlassSurface] card
|
|
/// carrying a [PluriWaveTypography.eyebrowLabel] group heading and a list of
|
|
/// [FilaAjuste] rows, each pushing its detail screen via
|
|
/// `PluriPushScaffold.push`. Neither primitive owns any business logic or
|
|
/// provider read — they are pure navigation chrome, which is what keeps the
|
|
/// Settings root down to "grouped nav rows only".
|
|
class GrupoAjustes extends StatelessWidget {
|
|
const GrupoAjustes({super.key, required this.titulo, required this.filas});
|
|
|
|
/// Group heading, styled with [PluriWaveTypography.eyebrowLabel]. Authored
|
|
/// already in its display form — this style never applies `toUpperCase()`.
|
|
final String titulo;
|
|
|
|
/// S8 (Tier 1 visual fidelity): `Widget`, not `List<FilaAjuste>` — a few
|
|
/// rows source their current-value text asynchronously (e.g. recordings
|
|
/// count, app version) and wrap their own `FilaAjuste` in a
|
|
/// `FutureBuilder`. `GrupoAjustes` only iterates and inserts dividers; it
|
|
/// never reaches into `FilaAjuste`-specific state.
|
|
final List<Widget> filas;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final type = context.pluriType;
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Audit 10.2 (t4 line 511): the group eyebrow sits OUTSIDE the
|
|
// card entirely, at title-tier (20px) padding -- it used to live
|
|
// INSIDE the PluriGlassSurface, sharing the card's own 16px
|
|
// padding.
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(
|
|
PluriLayout.titleHorizontal,
|
|
0,
|
|
PluriLayout.titleHorizontal,
|
|
6,
|
|
),
|
|
child: Text(titulo, style: type.eyebrowLabel),
|
|
),
|
|
PluriGlassSurface(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
for (var i = 0; i < filas.length; i++) ...[
|
|
// S10 (Tier 4 visual fidelity): the prototype insets its
|
|
// row divider by 47px (t4 line 516), not full-bleed.
|
|
if (i > 0) const Divider(height: 1, indent: 47),
|
|
filas[i],
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A single Settings navigation row: icon, title, an optional trailing
|
|
/// current-value string, and a trailing chevron. Tapping it is the row's
|
|
/// only behaviour — it carries no switches, sliders or text fields, which
|
|
/// is what "zero inline controls" means at the root.
|
|
class FilaAjuste extends StatelessWidget {
|
|
const FilaAjuste({
|
|
super.key,
|
|
required this.icon,
|
|
required this.titulo,
|
|
required this.onTap,
|
|
this.valor,
|
|
this.iconColor,
|
|
});
|
|
|
|
final IconData icon;
|
|
final String titulo;
|
|
final VoidCallback onTap;
|
|
|
|
/// S8 (Tier 1 visual fidelity): the prototype puts a trailing current
|
|
/// value on nearly every row (t4 lines 512-539, 625 — e.g. "3 guardados",
|
|
/// "Alfabético", "Español"), 13px `rgba(242,247,250,.55)`. Null means
|
|
/// "no current value to show" — the row renders exactly as before.
|
|
final String? valor;
|
|
|
|
/// Audit 10.5 (t4 lines 514/516/522 — equalizer `#21D4D9`, hd
|
|
/// `#7EE4C2`, folder `#F4B860`): only the first row or two of a group
|
|
/// carries an accent colour in the prototype; every other row's icon
|
|
/// stays the ambient default. Null (the vast majority of rows) means
|
|
/// "no accent" — the icon renders exactly as before.
|
|
final Color? iconColor;
|
|
|
|
/// Issue 5 (feedback-pruebas): caps how much width the trailing current
|
|
/// value can claim. `ListTile` gives `trailing` as much width as it wants
|
|
/// before handing the title whatever is left — an unbounded value (e.g. a
|
|
/// real, arbitrarily long station name for "Emisora preferida") could
|
|
/// squeeze the title down to almost nothing, forcing it to wrap across
|
|
/// several lines that then get cut short by the row's fixed height.
|
|
static const _anchoMaximoValor = 108.0;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final type = context.pluriType;
|
|
final valorActual = valor;
|
|
return ListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
// 10.6 (Tier 4 visual fidelity): the prototype's row icon is 21px (t4
|
|
// line 514), not Material's 24px default.
|
|
leading: Icon(icon, size: 21, color: iconColor),
|
|
// 10.8 (Tier 4 visual fidelity): the prototype's row title is
|
|
// 14px/w700 (t4 line 514); cardTitle is 14.5/w700 — a one-off
|
|
// override, not a new PluriWaveTypography style (mirrors the
|
|
// precedent set for the ringing screen's station name, audit 9.7).
|
|
// Issue 5: constrained to one line, ellipsizing instead of wrapping —
|
|
// labels must wrap as little as possible and never render visibly
|
|
// truncated (a multi-line wrap inside this fixed-height row cuts the
|
|
// last line short, which reads as broken, not as intentional).
|
|
title: Text(
|
|
titulo,
|
|
style: type.cardTitle.copyWith(fontSize: 14),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
trailing: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (valorActual != null) ...[
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: _anchoMaximoValor),
|
|
child: Text(
|
|
valorActual,
|
|
// bodyStrong is already 13/w600, matching the prototype's row
|
|
// value spec exactly — only the colour needs overriding.
|
|
style: type.bodyStrong.copyWith(
|
|
color: const Color(0xFFF2F7FA).withValues(alpha: 0.55),
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
textAlign: TextAlign.end,
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
],
|
|
// 10.9 (Tier 4 visual fidelity): the prototype's chevron is 19px
|
|
// at 40% opacity (t4 lines 515-539), not Material's 24px
|
|
// full-opacity default.
|
|
Icon(
|
|
Icons.chevron_right_rounded,
|
|
size: 19,
|
|
color: const Color(0xFFF2F7FA).withValues(alpha: 0.4),
|
|
),
|
|
],
|
|
),
|
|
onTap: onTap,
|
|
);
|
|
}
|
|
}
|