fix(vacaciones): header Add action, info banner, dashed CTA, collapse
Audit 9b.1 (t4:446): a solid brand-teal "Add" header action -- the
prototype's own mid-page CTA (audit 9b.6, still present, now dashed)
is a SECOND, additional entry point, not a replacement.
Audit 9b.2 (t4:448): the teal explanatory banner ("alarms marked
pause-during-vacations won't ring...") is now ALWAYS visible -- never
rendered before. New ARB key vacationExplainerBanner, all 13 locales.
Audit 9b.6 (t4:487): the bottom CTA is now dashed-border with a
date_range glyph -- was a solid OutlinedButton with an add glyph.
Reuses the dashed-painter shape already established in
pantalla_favoritos.dart's custom-station CTA (audit 4.5), duplicated
rather than shared.
Audit 9b.7 (t4:489): "Rangos pasados" is now a collapsible row --
icon, title, count, chevron -- COLLAPSED by default, expanding on tap.
Was always fully expanded inline. Updated the pre-existing widget test
to tap-then-assert instead of asserting immediate visibility.
Item 9b.3 (the eyebrow literally reading "EN CURSO") is NOT
implemented: the eyebrow STYLING is already correct (audit 9b.4), and
the residual copy gap is a shared ARB string
(vacationSummaryActiveCountdown) also used compactly in
pantalla_alarmas.dart's vacation summary row -- diverging its wording
just for this screen's eyebrow, or forcing a shoutier tone into that
other compact usage, is not worth it for a trivial-rated copy nuance.
This commit is contained in:
@@ -31,9 +31,40 @@ class PantallaVacaciones extends StatelessWidget {
|
||||
|
||||
return PluriPushScaffold(
|
||||
title: l10n.vacationRangesTitle,
|
||||
// Audit 9b.1 (t4:446): a solid brand-teal "Add" header action --
|
||||
// the prototype's OWN mid-page CTA (audit 9b.6, still present below,
|
||||
// now dashed) is a SECOND, additional entry point in the prototype,
|
||||
// not a replacement for this one.
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: FilledButton.icon(
|
||||
key: const ValueKey('vacation-add-header'),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: PluriWaveTokens.brand,
|
||||
foregroundColor: const Color(0xFF062126),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
onPressed: () => _abrirAlta(context),
|
||||
icon: const Icon(Icons.add_rounded, size: 17),
|
||||
label: Text(l10n.addAction),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: ListView(
|
||||
padding: PluriLayout.pageContentPadding,
|
||||
children: [
|
||||
// Audit 9b.2 (t4:448): the explanatory banner is ALWAYS visible
|
||||
// -- never rendered anywhere before.
|
||||
_BannerExplicativo(texto: l10n.vacationExplainerBanner),
|
||||
const SizedBox(height: 18),
|
||||
if (activo != null)
|
||||
_HeroRangoActivo(estado: estado, rango: activo)
|
||||
else
|
||||
@@ -41,15 +72,10 @@ class PantallaVacaciones extends StatelessWidget {
|
||||
const SizedBox(height: 16),
|
||||
_SeccionProgramados(proximas: proximas),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _abrirAlta(context),
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: Text(l10n.addVacationRangeCta),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Audit 9b.6 (t4:487): a dashed border + `date_range` icon --
|
||||
// was a solid `OutlinedButton` with an `add` glyph.
|
||||
_CtaAnadirRango(onTap: () => _abrirAlta(context)),
|
||||
const SizedBox(height: 14),
|
||||
_SeccionRangosPasados(pasadas: pasadas),
|
||||
],
|
||||
),
|
||||
@@ -67,6 +93,136 @@ class PantallaVacaciones extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Audit 9b.2 (t4:448): teal-tinted explainer banner, always visible above
|
||||
/// the active-range hero.
|
||||
class _BannerExplicativo extends StatelessWidget {
|
||||
const _BannerExplicativo({required this.texto});
|
||||
|
||||
final String texto;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = context.pluriTokens;
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.liveGreen.withValues(alpha: 0.09),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: tokens.liveGreen.withValues(alpha: 0.26)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 13),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.info_outline_rounded, size: 20, color: tokens.liveGreen),
|
||||
const SizedBox(width: 11),
|
||||
Expanded(
|
||||
child: Text(
|
||||
texto,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1.5,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.72),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Audit 9b.6 (t4:487): dashed-border CTA with a `date_range` glyph --
|
||||
/// reuses the same dashed-painter shape already established in
|
||||
/// `pantalla_favoritos.dart`'s custom-station CTA (audit 4.5), duplicated
|
||||
/// rather than shared (small, self-contained, matching this codebase's own
|
||||
/// precedent for tiny per-screen painters).
|
||||
class _CtaAnadirRango extends StatelessWidget {
|
||||
const _CtaAnadirRango({required this.onTap});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final colorTexto = Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6);
|
||||
return CustomPaint(
|
||||
painter: _DashedBorderPainter(
|
||||
color: Colors.white.withValues(alpha: 0.16),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.date_range_rounded, size: 20, color: colorTexto),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.addVacationRangeCta,
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: colorTexto,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DashedBorderPainter extends CustomPainter {
|
||||
const _DashedBorderPainter({required this.color});
|
||||
|
||||
final Color color;
|
||||
|
||||
static const _radius = 16.0;
|
||||
static const _dashWidth = 6.0;
|
||||
static const _gapWidth = 4.0;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final rrect = RRect.fromRectAndRadius(
|
||||
Offset.zero & size,
|
||||
const Radius.circular(_radius),
|
||||
);
|
||||
final path = Path()..addRRect(rrect);
|
||||
final paint =
|
||||
Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.5;
|
||||
for (final metric in path.computeMetrics()) {
|
||||
var distance = 0.0;
|
||||
while (distance < metric.length) {
|
||||
final next = distance + _dashWidth;
|
||||
canvas.drawPath(
|
||||
metric.extractPath(distance, next.clamp(0.0, metric.length)),
|
||||
paint,
|
||||
);
|
||||
distance = next + _gapWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _DashedBorderPainter oldDelegate) =>
|
||||
oldDelegate.color != color;
|
||||
}
|
||||
|
||||
/// Active-range hero: name, days-remaining countdown (reusing WU8's own
|
||||
/// `vacationSummaryActiveCountdown` string — same concept, bigger stage),
|
||||
/// a start/end date pair (item 21 / audit 9b.4, replacing the former
|
||||
@@ -184,31 +340,92 @@ class _SeccionProgramados extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SeccionRangosPasados extends StatelessWidget {
|
||||
/// Audit 9b.7 (t4:489): a collapsible row -- icon, title, count, chevron
|
||||
/// -- COLLAPSED by default; tapping reveals the full list below it. Was
|
||||
/// always fully expanded inline.
|
||||
class _SeccionRangosPasados extends StatefulWidget {
|
||||
const _SeccionRangosPasados({required this.pasadas});
|
||||
|
||||
final List<RangoVacaciones> pasadas;
|
||||
|
||||
@override
|
||||
State<_SeccionRangosPasados> createState() => _SeccionRangosPasadosState();
|
||||
}
|
||||
|
||||
class _SeccionRangosPasadosState extends State<_SeccionRangosPasados> {
|
||||
/// Ephemeral UI state only (design's "State is for ephemeral UI only"
|
||||
/// ruling) -- collapsed by default, matching the prototype's own count
|
||||
/// only row.
|
||||
bool _expandido = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (pasadas.isEmpty) return const SizedBox.shrink();
|
||||
if (widget.pasadas.isEmpty) return const SizedBox.shrink();
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final type = context.pluriType;
|
||||
return PluriGlassSurface(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.vacationPastSectionTitle, style: type.eyebrowLabel),
|
||||
const SizedBox(height: 8),
|
||||
// No prototype-specified header text exists for an already-ended
|
||||
// range, so `encabezado` is omitted rather than invented.
|
||||
PluriPanelColumn(
|
||||
gap: 10,
|
||||
children: [
|
||||
for (final rango in pasadas)
|
||||
_TarjetaRangoVacaciones(rango: rango),
|
||||
],
|
||||
Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: () => setState(() => _expandido = !_expandido),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.history_rounded,
|
||||
size: 20,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.vacationPastSectionTitle,
|
||||
style: context.pluriType.cardTitle,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${widget.pasadas.length}',
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
_expandido
|
||||
? Icons.expand_less_rounded
|
||||
: Icons.chevron_right_rounded,
|
||||
size: 19,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_expandido)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: PluriPanelColumn(
|
||||
gap: 10,
|
||||
children: [
|
||||
for (final rango in widget.pasadas)
|
||||
_TarjetaRangoVacaciones(rango: rango),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user