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:
@@ -788,6 +788,7 @@
|
||||
"times": {}
|
||||
}
|
||||
},
|
||||
"vacationExplainerBanner": "خلال هذه الفترات، لن تُصدر المنبهات المُعلَّمة بـ «إيقاف أثناء الإجازة» صوتًا. أما المُعلَّمة بـ «تشغيل دائمًا» فلن تتأثر.",
|
||||
"vacationNoActiveRangeHint": "لا توجد فترة إجازة نشطة الآن.",
|
||||
"vacationPastSectionTitle": "الفترات السابقة",
|
||||
"vacationRangesCount": "{count} فترة",
|
||||
|
||||
@@ -788,6 +788,7 @@
|
||||
"times": {}
|
||||
}
|
||||
},
|
||||
"vacationExplainerBanner": "এই সময়কালে \"ছুটিতে বিরতি\" হিসেবে চিহ্নিত অ্যালার্মগুলো বাজবে না। \"সবসময় বাজবে\" হিসেবে চিহ্নিত অ্যালার্মগুলো প্রভাবিত হবে না।",
|
||||
"vacationNoActiveRangeHint": "এই মুহূর্তে কোনো সক্রিয় ছুটির সময়সীমা নেই।",
|
||||
"vacationPastSectionTitle": "অতীত সময়সীমা",
|
||||
"vacationRangesCount": "{count}টি সময়সীমা",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -788,6 +788,7 @@
|
||||
"times": {}
|
||||
}
|
||||
},
|
||||
"vacationExplainerBanner": "इन अवधियों के दौरान \"छुट्टी में रोकें\" के रूप में चिह्नित अलार्म नहीं बजेंगे। \"हमेशा बजाएं\" के रूप में चिह्नित अलार्म पर कोई असर नहीं पड़ता।",
|
||||
"vacationNoActiveRangeHint": "अभी कोई सक्रिय छुट्टी अवधि नहीं है।",
|
||||
"vacationPastSectionTitle": "पिछली अवधियां",
|
||||
"vacationRangesCount": "{count} अवधियां",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -788,6 +788,7 @@
|
||||
"times": {}
|
||||
}
|
||||
},
|
||||
"vacationExplainerBanner": "この期間中は「休暇中は一時停止」に設定したアラームは鳴りません。「常に鳴らす」に設定したアラームには影響しません。",
|
||||
"vacationNoActiveRangeHint": "現在アクティブな休暇期間はありません。",
|
||||
"vacationPastSectionTitle": "過去の期間",
|
||||
"vacationRangesCount": "{count} 件の期間",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -788,6 +788,7 @@
|
||||
"times": {}
|
||||
}
|
||||
},
|
||||
"vacationExplainerBanner": "В эти периоды будильники с пометкой «пауза на время отпуска» не звонят. Будильники с пометкой «звонить всегда» это не затрагивает.",
|
||||
"vacationNoActiveRangeHint": "Сейчас нет активного периода отпуска.",
|
||||
"vacationPastSectionTitle": "Прошедшие периоды",
|
||||
"vacationRangesCount": "{count} периодов",
|
||||
|
||||
@@ -788,6 +788,7 @@
|
||||
"times": {}
|
||||
}
|
||||
},
|
||||
"vacationExplainerBanner": "在这些时间段内,标记为“假期暂停”的闹钟不会响铃;标记为“始终响铃”的闹钟不受影响。",
|
||||
"vacationNoActiveRangeHint": "目前没有正在生效的假期时间段。",
|
||||
"vacationPastSectionTitle": "过往时间段",
|
||||
"vacationRangesCount": "{count} 个时间段",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1120,6 +1120,10 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
@override
|
||||
String get addVacationRangeCta => 'إضافة فترة';
|
||||
|
||||
@override
|
||||
String get vacationExplainerBanner =>
|
||||
'خلال هذه الفترات، لن تُصدر المنبهات المُعلَّمة بـ «إيقاف أثناء الإجازة» صوتًا. أما المُعلَّمة بـ «تشغيل دائمًا» فلن تتأثر.';
|
||||
|
||||
@override
|
||||
String get vacationNoActiveRangeHint => 'لا توجد فترة إجازة نشطة الآن.';
|
||||
|
||||
|
||||
@@ -1126,6 +1126,10 @@ class AppLocalizationsBn extends AppLocalizations {
|
||||
@override
|
||||
String get addVacationRangeCta => 'সময়সীমা যোগ করুন';
|
||||
|
||||
@override
|
||||
String get vacationExplainerBanner =>
|
||||
'এই সময়কালে \"ছুটিতে বিরতি\" হিসেবে চিহ্নিত অ্যালার্মগুলো বাজবে না। \"সবসময় বাজবে\" হিসেবে চিহ্নিত অ্যালার্মগুলো প্রভাবিত হবে না।';
|
||||
|
||||
@override
|
||||
String get vacationNoActiveRangeHint =>
|
||||
'এই মুহূর্তে কোনো সক্রিয় ছুটির সময়সীমা নেই।';
|
||||
|
||||
@@ -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.';
|
||||
|
||||
@@ -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.';
|
||||
|
||||
|
||||
@@ -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.';
|
||||
|
||||
@@ -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.';
|
||||
|
||||
@@ -1121,6 +1121,10 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
@override
|
||||
String get addVacationRangeCta => 'अवधि जोड़ें';
|
||||
|
||||
@override
|
||||
String get vacationExplainerBanner =>
|
||||
'इन अवधियों के दौरान \"छुट्टी में रोकें\" के रूप में चिह्नित अलार्म नहीं बजेंगे। \"हमेशा बजाएं\" के रूप में चिह्नित अलार्म पर कोई असर नहीं पड़ता।';
|
||||
|
||||
@override
|
||||
String get vacationNoActiveRangeHint => 'अभी कोई सक्रिय छुट्टी अवधि नहीं है।';
|
||||
|
||||
|
||||
@@ -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.';
|
||||
|
||||
@@ -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.';
|
||||
|
||||
@@ -1088,6 +1088,10 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get addVacationRangeCta => '期間を追加';
|
||||
|
||||
@override
|
||||
String get vacationExplainerBanner =>
|
||||
'この期間中は「休暇中は一時停止」に設定したアラームは鳴りません。「常に鳴らす」に設定したアラームには影響しません。';
|
||||
|
||||
@override
|
||||
String get vacationNoActiveRangeHint => '現在アクティブな休暇期間はありません。';
|
||||
|
||||
|
||||
@@ -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.';
|
||||
|
||||
@@ -1126,6 +1126,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get addVacationRangeCta => 'Добавить период';
|
||||
|
||||
@override
|
||||
String get vacationExplainerBanner =>
|
||||
'В эти периоды будильники с пометкой «пауза на время отпуска» не звонят. Будильники с пометкой «звонить всегда» это не затрагивает.';
|
||||
|
||||
@override
|
||||
String get vacationNoActiveRangeHint =>
|
||||
'Сейчас нет активного периода отпуска.';
|
||||
|
||||
@@ -1084,6 +1084,10 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get addVacationRangeCta => '添加时间段';
|
||||
|
||||
@override
|
||||
String get vacationExplainerBanner =>
|
||||
'在这些时间段内,标记为“假期暂停”的闹钟不会响铃;标记为“始终响铃”的闹钟不受影响。';
|
||||
|
||||
@override
|
||||
String get vacationNoActiveRangeHint => '目前没有正在生效的假期时间段。';
|
||||
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -180,7 +180,8 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'los rangos pasados aparecen bajo el encabezado "Rangos pasados"',
|
||||
'los rangos pasados aparecen bajo el encabezado "Rangos pasados" tras '
|
||||
'expandir la fila (audit 9b.7: colapsada por defecto, t4:489)',
|
||||
(tester) async {
|
||||
final estado = await _crearEstado(
|
||||
vacaciones: [
|
||||
@@ -201,6 +202,10 @@ void main() {
|
||||
tester.element(find.byType(PantallaVacaciones)),
|
||||
);
|
||||
expect(find.text(l10n.vacationPastSectionTitle), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text(l10n.vacationPastSectionTitle));
|
||||
await _pumpEstable(tester);
|
||||
|
||||
expect(find.text('Rango viejo'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
@@ -343,4 +348,101 @@ void main() {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group('visual fidelity (audit 9b.1/9b.2/9b.6/9b.7)', () {
|
||||
testWidgets(
|
||||
'9b.1: a header "Add" action is reachable and opens the same form '
|
||||
'as the CTA (t4:446)',
|
||||
(tester) async {
|
||||
final estado = await _crearEstado();
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(_buildScreen(estado));
|
||||
await _pumpEstable(tester);
|
||||
|
||||
final l10n = AppLocalizations.of(
|
||||
tester.element(find.byType(PantallaVacaciones)),
|
||||
);
|
||||
final appBar = tester.widget<AppBar>(find.byType(AppBar));
|
||||
expect(appBar.actions, isNotNull);
|
||||
expect(appBar.actions, isNotEmpty);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('vacation-add-header')));
|
||||
await _pumpEstable(tester);
|
||||
|
||||
expect(find.text(l10n.newVacationRangeTitle), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'9b.2: the explanatory info banner is always visible (t4:448)',
|
||||
(tester) async {
|
||||
final estado = await _crearEstado();
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(_buildScreen(estado));
|
||||
await _pumpEstable(tester);
|
||||
|
||||
final l10n = AppLocalizations.of(
|
||||
tester.element(find.byType(PantallaVacaciones)),
|
||||
);
|
||||
expect(find.text(l10n.vacationExplainerBanner), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'9b.6: the bottom CTA has a dashed border and a date_range icon '
|
||||
'(t4:487)',
|
||||
(tester) async {
|
||||
final estado = await _crearEstado();
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(_buildScreen(estado));
|
||||
await _pumpEstable(tester);
|
||||
|
||||
expect(find.byIcon(Icons.date_range_rounded), findsOneWidget);
|
||||
// Exactly ONE add_rounded on the whole screen -- the header's OWN
|
||||
// "Add" action (audit 9b.1). The bottom CTA no longer uses it.
|
||||
expect(find.byIcon(Icons.add_rounded), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'9b.7: "Rangos pasados" is collapsed by default, showing a count, '
|
||||
'and expands on tap (t4:489)',
|
||||
(tester) async {
|
||||
final estado = await _crearEstado(
|
||||
vacaciones: [
|
||||
RangoVacaciones(
|
||||
id: 'pa1',
|
||||
nombre: 'Rango viejo',
|
||||
inicio: _hoyDia.subtract(const Duration(days: 30)),
|
||||
fin: _hoyDia.subtract(const Duration(days: 20)),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(_buildScreen(estado));
|
||||
await _pumpEstable(tester);
|
||||
|
||||
final l10n = AppLocalizations.of(
|
||||
tester.element(find.byType(PantallaVacaciones)),
|
||||
);
|
||||
expect(find.text(l10n.vacationPastSectionTitle), findsOneWidget);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Rango viejo'),
|
||||
findsNothing,
|
||||
reason:
|
||||
'collapsed by default -- matches the prototype count-only row',
|
||||
);
|
||||
|
||||
await tester.tap(find.text(l10n.vacationPastSectionTitle));
|
||||
await _pumpEstable(tester);
|
||||
|
||||
expect(find.text('Rango viejo'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user