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:
2026-07-30 16:53:04 +02:00
parent 36d7d5f692
commit f61b0b9163
29 changed files with 414 additions and 24 deletions
+1
View File
@@ -788,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "خلال هذه الفترات، لن تُصدر المنبهات المُعلَّمة بـ «إيقاف أثناء الإجازة» صوتًا. أما المُعلَّمة بـ «تشغيل دائمًا» فلن تتأثر.",
"vacationNoActiveRangeHint": "لا توجد فترة إجازة نشطة الآن.",
"vacationPastSectionTitle": "الفترات السابقة",
"vacationRangesCount": "{count} فترة",
+1
View File
@@ -788,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "এই সময়কালে \"ছুটিতে বিরতি\" হিসেবে চিহ্নিত অ্যালার্মগুলো বাজবে না। \"সবসময় বাজবে\" হিসেবে চিহ্নিত অ্যালার্মগুলো প্রভাবিত হবে না।",
"vacationNoActiveRangeHint": "এই মুহূর্তে কোনো সক্রিয় ছুটির সময়সীমা নেই।",
"vacationPastSectionTitle": "অতীত সময়সীমা",
"vacationRangesCount": "{count}টি সময়সীমা",
+1
View File
@@ -788,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "Während dieser Zeiträume klingeln Alarme mit der Markierung \"im Urlaub pausieren\" nicht. Als \"immer klingeln\" markierte Alarme sind davon nicht betroffen.",
"vacationNoActiveRangeHint": "Momentan ist kein aktiver Urlaubszeitraum vorhanden.",
"vacationPastSectionTitle": "Vergangene Zeiträume",
"vacationRangesCount": "{count} Zeiträume",
+1
View File
@@ -576,6 +576,7 @@
"vacationUpcomingSectionTitle": "SCHEDULED",
"vacationPastSectionTitle": "Past ranges",
"addVacationRangeCta": "Add range",
"vacationExplainerBanner": "During these ranges, alarms marked \"pause during vacations\" won't ring. Alarms marked \"always ring\" are not affected.",
"vacationNoActiveRangeHint": "No active vacation range right now.",
"vacationsDefaultName": "Vacation",
"newVacationRangeTitle": "New vacation range",
+1
View File
@@ -576,6 +576,7 @@
"vacationUpcomingSectionTitle": "PROGRAMADOS",
"vacationPastSectionTitle": "Rangos pasados",
"addVacationRangeCta": "Añadir rango",
"vacationExplainerBanner": "Durante estos rangos no suenan las alarmas marcadas como \"pausar en vacaciones\". Las marcadas como \"sonar siempre\" no se ven afectadas.",
"vacationNoActiveRangeHint": "No hay un rango de vacaciones activo ahora mismo.",
"vacationsDefaultName": "Vacaciones",
"newVacationRangeTitle": "Nuevo rango de vacaciones",
+1
View File
@@ -788,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "Pendant ces periodes, les alarmes marquees \"pause pendant les vacances\" ne sonnent pas. Celles marquees \"toujours sonner\" ne sont pas concernees.",
"vacationNoActiveRangeHint": "Aucune période de vacances active pour le moment.",
"vacationPastSectionTitle": "Périodes passées",
"vacationRangesCount": "{count} périodes",
+1
View File
@@ -788,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "इन अवधियों के दौरान \"छुट्टी में रोकें\" के रूप में चिह्नित अलार्म नहीं बजेंगे। \"हमेशा बजाएं\" के रूप में चिह्नित अलार्म पर कोई असर नहीं पड़ता।",
"vacationNoActiveRangeHint": "अभी कोई सक्रिय छुट्टी अवधि नहीं है।",
"vacationPastSectionTitle": "पिछली अवधियां",
"vacationRangesCount": "{count} अवधियां",
+1
View File
@@ -788,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "Selama periode ini, alarm yang ditandai \"jeda saat liburan\" tidak akan berbunyi. Alarm yang ditandai \"selalu berbunyi\" tidak terpengaruh.",
"vacationNoActiveRangeHint": "Tidak ada periode liburan aktif saat ini.",
"vacationPastSectionTitle": "Periode lampau",
"vacationRangesCount": "{count} periode",
+1
View File
@@ -788,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "Durante questi periodi le sveglie contrassegnate come \"pausa in vacanza\" non suonano. Quelle contrassegnate come \"suona sempre\" non sono interessate.",
"vacationNoActiveRangeHint": "Nessun periodo di vacanza attivo al momento.",
"vacationPastSectionTitle": "Periodi passati",
"vacationRangesCount": "{count} periodi",
+1
View File
@@ -788,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "この期間中は「休暇中は一時停止」に設定したアラームは鳴りません。「常に鳴らす」に設定したアラームには影響しません。",
"vacationNoActiveRangeHint": "現在アクティブな休暇期間はありません。",
"vacationPastSectionTitle": "過去の期間",
"vacationRangesCount": "{count} 件の期間",
+1
View File
@@ -788,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "Durante esses periodos, os alarmes marcados como \"pausar nas ferias\" nao tocam. Os marcados como \"tocar sempre\" nao sao afetados.",
"vacationNoActiveRangeHint": "Não há período de férias ativo no momento.",
"vacationPastSectionTitle": "Períodos passados",
"vacationRangesCount": "{count} períodos",
+1
View File
@@ -788,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "В эти периоды будильники с пометкой «пауза на время отпуска» не звонят. Будильники с пометкой «звонить всегда» это не затрагивает.",
"vacationNoActiveRangeHint": "Сейчас нет активного периода отпуска.",
"vacationPastSectionTitle": "Прошедшие периоды",
"vacationRangesCount": "{count} периодов",
+1
View File
@@ -788,6 +788,7 @@
"times": {}
}
},
"vacationExplainerBanner": "在这些时间段内,标记为“假期暂停”的闹钟不会响铃;标记为“始终响铃”的闹钟不受影响。",
"vacationNoActiveRangeHint": "目前没有正在生效的假期时间段。",
"vacationPastSectionTitle": "过往时间段",
"vacationRangesCount": "{count} 个时间段",
+6
View File
@@ -2054,6 +2054,12 @@ abstract class AppLocalizations {
/// **'Añadir rango'**
String get addVacationRangeCta;
/// No description provided for @vacationExplainerBanner.
///
/// In es, this message translates to:
/// **'Durante estos rangos no suenan las alarmas marcadas como \"pausar en vacaciones\". Las marcadas como \"sonar siempre\" no se ven afectadas.'**
String get vacationExplainerBanner;
/// No description provided for @vacationNoActiveRangeHint.
///
/// In es, this message translates to:
+4
View File
@@ -1120,6 +1120,10 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get addVacationRangeCta => 'إضافة فترة';
@override
String get vacationExplainerBanner =>
'خلال هذه الفترات، لن تُصدر المنبهات المُعلَّمة بـ «إيقاف أثناء الإجازة» صوتًا. أما المُعلَّمة بـ «تشغيل دائمًا» فلن تتأثر.';
@override
String get vacationNoActiveRangeHint => 'لا توجد فترة إجازة نشطة الآن.';
+4
View File
@@ -1126,6 +1126,10 @@ class AppLocalizationsBn extends AppLocalizations {
@override
String get addVacationRangeCta => 'সময়সীমা যোগ করুন';
@override
String get vacationExplainerBanner =>
'এই সময়কালে \"ছুটিতে বিরতি\" হিসেবে চিহ্নিত অ্যালার্মগুলো বাজবে না। \"সবসময় বাজবে\" হিসেবে চিহ্নিত অ্যালার্মগুলো প্রভাবিত হবে না।';
@override
String get vacationNoActiveRangeHint =>
'এই মুহূর্তে কোনো সক্রিয় ছুটির সময়সীমা নেই।';
+4
View File
@@ -1128,6 +1128,10 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get addVacationRangeCta => 'Zeitraum hinzufügen';
@override
String get vacationExplainerBanner =>
'Während dieser Zeiträume klingeln Alarme mit der Markierung \"im Urlaub pausieren\" nicht. Als \"immer klingeln\" markierte Alarme sind davon nicht betroffen.';
@override
String get vacationNoActiveRangeHint =>
'Momentan ist kein aktiver Urlaubszeitraum vorhanden.';
+4
View File
@@ -1120,6 +1120,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get addVacationRangeCta => 'Add range';
@override
String get vacationExplainerBanner =>
'During these ranges, alarms marked \"pause during vacations\" won\'t ring. Alarms marked \"always ring\" are not affected.';
@override
String get vacationNoActiveRangeHint => 'No active vacation range right now.';
+4
View File
@@ -1125,6 +1125,10 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get addVacationRangeCta => 'Añadir rango';
@override
String get vacationExplainerBanner =>
'Durante estos rangos no suenan las alarmas marcadas como \"pausar en vacaciones\". Las marcadas como \"sonar siempre\" no se ven afectadas.';
@override
String get vacationNoActiveRangeHint =>
'No hay un rango de vacaciones activo ahora mismo.';
+4
View File
@@ -1131,6 +1131,10 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get addVacationRangeCta => 'Ajouter une période';
@override
String get vacationExplainerBanner =>
'Pendant ces periodes, les alarmes marquees \"pause pendant les vacances\" ne sonnent pas. Celles marquees \"toujours sonner\" ne sont pas concernees.';
@override
String get vacationNoActiveRangeHint =>
'Aucune période de vacances active pour le moment.';
+4
View File
@@ -1121,6 +1121,10 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get addVacationRangeCta => 'अवधि जोड़ें';
@override
String get vacationExplainerBanner =>
'इन अवधियों के दौरान \"छुट्टी में रोकें\" के रूप में चिह्नित अलार्म नहीं बजेंगे। \"हमेशा बजाएं\" के रूप में चिह्नित अलार्म पर कोई असर नहीं पड़ता।';
@override
String get vacationNoActiveRangeHint => 'अभी कोई सक्रिय छुट्टी अवधि नहीं है।';
+4
View File
@@ -1125,6 +1125,10 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get addVacationRangeCta => 'Tambahkan periode';
@override
String get vacationExplainerBanner =>
'Selama periode ini, alarm yang ditandai \"jeda saat liburan\" tidak akan berbunyi. Alarm yang ditandai \"selalu berbunyi\" tidak terpengaruh.';
@override
String get vacationNoActiveRangeHint =>
'Tidak ada periode liburan aktif saat ini.';
+4
View File
@@ -1130,6 +1130,10 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get addVacationRangeCta => 'Aggiungi periodo';
@override
String get vacationExplainerBanner =>
'Durante questi periodi le sveglie contrassegnate come \"pausa in vacanza\" non suonano. Quelle contrassegnate come \"suona sempre\" non sono interessate.';
@override
String get vacationNoActiveRangeHint =>
'Nessun periodo di vacanza attivo al momento.';
+4
View File
@@ -1088,6 +1088,10 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get addVacationRangeCta => '期間を追加';
@override
String get vacationExplainerBanner =>
'この期間中は「休暇中は一時停止」に設定したアラームは鳴りません。「常に鳴らす」に設定したアラームには影響しません。';
@override
String get vacationNoActiveRangeHint => '現在アクティブな休暇期間はありません。';
+4
View File
@@ -1124,6 +1124,10 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get addVacationRangeCta => 'Adicionar período';
@override
String get vacationExplainerBanner =>
'Durante esses periodos, os alarmes marcados como \"pausar nas ferias\" nao tocam. Os marcados como \"tocar sempre\" nao sao afetados.';
@override
String get vacationNoActiveRangeHint =>
'Não há período de férias ativo no momento.';
+4
View File
@@ -1126,6 +1126,10 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get addVacationRangeCta => 'Добавить период';
@override
String get vacationExplainerBanner =>
'В эти периоды будильники с пометкой «пауза на время отпуска» не звонят. Будильники с пометкой «звонить всегда» это не затрагивает.';
@override
String get vacationNoActiveRangeHint =>
'Сейчас нет активного периода отпуска.';
+4
View File
@@ -1084,6 +1084,10 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get addVacationRangeCta => '添加时间段';
@override
String get vacationExplainerBanner =>
'在这些时间段内,标记为“假期暂停”的闹钟不会响铃;标记为“始终响铃”的闹钟不受影响。';
@override
String get vacationNoActiveRangeHint => '目前没有正在生效的假期时间段。';
+240 -23
View File
@@ -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),
],
),
),
],
),
);