diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 62677f7..9c9be9c 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -788,6 +788,7 @@ "times": {} } }, + "vacationExplainerBanner": "خلال هذه الفترات، لن تُصدر المنبهات المُعلَّمة بـ «إيقاف أثناء الإجازة» صوتًا. أما المُعلَّمة بـ «تشغيل دائمًا» فلن تتأثر.", "vacationNoActiveRangeHint": "لا توجد فترة إجازة نشطة الآن.", "vacationPastSectionTitle": "الفترات السابقة", "vacationRangesCount": "{count} فترة", diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index f545b7f..b0dfc1d 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -788,6 +788,7 @@ "times": {} } }, + "vacationExplainerBanner": "এই সময়কালে \"ছুটিতে বিরতি\" হিসেবে চিহ্নিত অ্যালার্মগুলো বাজবে না। \"সবসময় বাজবে\" হিসেবে চিহ্নিত অ্যালার্মগুলো প্রভাবিত হবে না।", "vacationNoActiveRangeHint": "এই মুহূর্তে কোনো সক্রিয় ছুটির সময়সীমা নেই।", "vacationPastSectionTitle": "অতীত সময়সীমা", "vacationRangesCount": "{count}টি সময়সীমা", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 5683aa7..e7d9daa 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -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", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index cc04eb8..f9f3cd1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index b2e29d6..ba95b6e 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -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", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 8e2c1d5..f8390dd 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -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", diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index a7564a3..d42f3f3 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -788,6 +788,7 @@ "times": {} } }, + "vacationExplainerBanner": "इन अवधियों के दौरान \"छुट्टी में रोकें\" के रूप में चिह्नित अलार्म नहीं बजेंगे। \"हमेशा बजाएं\" के रूप में चिह्नित अलार्म पर कोई असर नहीं पड़ता।", "vacationNoActiveRangeHint": "अभी कोई सक्रिय छुट्टी अवधि नहीं है।", "vacationPastSectionTitle": "पिछली अवधियां", "vacationRangesCount": "{count} अवधियां", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 47c8ddc..ea625ca 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -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", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 4461135..a4a8174 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -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", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index fea1137..3a4256f 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -788,6 +788,7 @@ "times": {} } }, + "vacationExplainerBanner": "この期間中は「休暇中は一時停止」に設定したアラームは鳴りません。「常に鳴らす」に設定したアラームには影響しません。", "vacationNoActiveRangeHint": "現在アクティブな休暇期間はありません。", "vacationPastSectionTitle": "過去の期間", "vacationRangesCount": "{count} 件の期間", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index eba20e4..8ded454 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -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", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 9fa6b11..d14cc19 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -788,6 +788,7 @@ "times": {} } }, + "vacationExplainerBanner": "В эти периоды будильники с пометкой «пауза на время отпуска» не звонят. Будильники с пометкой «звонить всегда» это не затрагивает.", "vacationNoActiveRangeHint": "Сейчас нет активного периода отпуска.", "vacationPastSectionTitle": "Прошедшие периоды", "vacationRangesCount": "{count} периодов", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index d7c653a..37fc3ce 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -788,6 +788,7 @@ "times": {} } }, + "vacationExplainerBanner": "在这些时间段内,标记为“假期暂停”的闹钟不会响铃;标记为“始终响铃”的闹钟不受影响。", "vacationNoActiveRangeHint": "目前没有正在生效的假期时间段。", "vacationPastSectionTitle": "过往时间段", "vacationRangesCount": "{count} 个时间段", diff --git a/lib/l10n/gen/app_localizations.dart b/lib/l10n/gen/app_localizations.dart index 21928c1..4620764 100644 --- a/lib/l10n/gen/app_localizations.dart +++ b/lib/l10n/gen/app_localizations.dart @@ -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: diff --git a/lib/l10n/gen/app_localizations_ar.dart b/lib/l10n/gen/app_localizations_ar.dart index 92ad992..68d5a15 100644 --- a/lib/l10n/gen/app_localizations_ar.dart +++ b/lib/l10n/gen/app_localizations_ar.dart @@ -1120,6 +1120,10 @@ class AppLocalizationsAr extends AppLocalizations { @override String get addVacationRangeCta => 'إضافة فترة'; + @override + String get vacationExplainerBanner => + 'خلال هذه الفترات، لن تُصدر المنبهات المُعلَّمة بـ «إيقاف أثناء الإجازة» صوتًا. أما المُعلَّمة بـ «تشغيل دائمًا» فلن تتأثر.'; + @override String get vacationNoActiveRangeHint => 'لا توجد فترة إجازة نشطة الآن.'; diff --git a/lib/l10n/gen/app_localizations_bn.dart b/lib/l10n/gen/app_localizations_bn.dart index 7c2c4a0..0790578 100644 --- a/lib/l10n/gen/app_localizations_bn.dart +++ b/lib/l10n/gen/app_localizations_bn.dart @@ -1126,6 +1126,10 @@ class AppLocalizationsBn extends AppLocalizations { @override String get addVacationRangeCta => 'সময়সীমা যোগ করুন'; + @override + String get vacationExplainerBanner => + 'এই সময়কালে \"ছুটিতে বিরতি\" হিসেবে চিহ্নিত অ্যালার্মগুলো বাজবে না। \"সবসময় বাজবে\" হিসেবে চিহ্নিত অ্যালার্মগুলো প্রভাবিত হবে না।'; + @override String get vacationNoActiveRangeHint => 'এই মুহূর্তে কোনো সক্রিয় ছুটির সময়সীমা নেই।'; diff --git a/lib/l10n/gen/app_localizations_de.dart b/lib/l10n/gen/app_localizations_de.dart index f1a3a46..dd17ae6 100644 --- a/lib/l10n/gen/app_localizations_de.dart +++ b/lib/l10n/gen/app_localizations_de.dart @@ -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.'; diff --git a/lib/l10n/gen/app_localizations_en.dart b/lib/l10n/gen/app_localizations_en.dart index a48fc1f..df75acf 100644 --- a/lib/l10n/gen/app_localizations_en.dart +++ b/lib/l10n/gen/app_localizations_en.dart @@ -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.'; diff --git a/lib/l10n/gen/app_localizations_es.dart b/lib/l10n/gen/app_localizations_es.dart index fce8e35..4b436bf 100644 --- a/lib/l10n/gen/app_localizations_es.dart +++ b/lib/l10n/gen/app_localizations_es.dart @@ -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.'; diff --git a/lib/l10n/gen/app_localizations_fr.dart b/lib/l10n/gen/app_localizations_fr.dart index 49a6b5f..f546287 100644 --- a/lib/l10n/gen/app_localizations_fr.dart +++ b/lib/l10n/gen/app_localizations_fr.dart @@ -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.'; diff --git a/lib/l10n/gen/app_localizations_hi.dart b/lib/l10n/gen/app_localizations_hi.dart index a15dd55..eae2adb 100644 --- a/lib/l10n/gen/app_localizations_hi.dart +++ b/lib/l10n/gen/app_localizations_hi.dart @@ -1121,6 +1121,10 @@ class AppLocalizationsHi extends AppLocalizations { @override String get addVacationRangeCta => 'अवधि जोड़ें'; + @override + String get vacationExplainerBanner => + 'इन अवधियों के दौरान \"छुट्टी में रोकें\" के रूप में चिह्नित अलार्म नहीं बजेंगे। \"हमेशा बजाएं\" के रूप में चिह्नित अलार्म पर कोई असर नहीं पड़ता।'; + @override String get vacationNoActiveRangeHint => 'अभी कोई सक्रिय छुट्टी अवधि नहीं है।'; diff --git a/lib/l10n/gen/app_localizations_id.dart b/lib/l10n/gen/app_localizations_id.dart index a813041..430f546 100644 --- a/lib/l10n/gen/app_localizations_id.dart +++ b/lib/l10n/gen/app_localizations_id.dart @@ -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.'; diff --git a/lib/l10n/gen/app_localizations_it.dart b/lib/l10n/gen/app_localizations_it.dart index 07906b9..16ba528 100644 --- a/lib/l10n/gen/app_localizations_it.dart +++ b/lib/l10n/gen/app_localizations_it.dart @@ -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.'; diff --git a/lib/l10n/gen/app_localizations_ja.dart b/lib/l10n/gen/app_localizations_ja.dart index 76d1f95..7fb19ca 100644 --- a/lib/l10n/gen/app_localizations_ja.dart +++ b/lib/l10n/gen/app_localizations_ja.dart @@ -1088,6 +1088,10 @@ class AppLocalizationsJa extends AppLocalizations { @override String get addVacationRangeCta => '期間を追加'; + @override + String get vacationExplainerBanner => + 'この期間中は「休暇中は一時停止」に設定したアラームは鳴りません。「常に鳴らす」に設定したアラームには影響しません。'; + @override String get vacationNoActiveRangeHint => '現在アクティブな休暇期間はありません。'; diff --git a/lib/l10n/gen/app_localizations_pt.dart b/lib/l10n/gen/app_localizations_pt.dart index f6dc84e..b49f45c 100644 --- a/lib/l10n/gen/app_localizations_pt.dart +++ b/lib/l10n/gen/app_localizations_pt.dart @@ -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.'; diff --git a/lib/l10n/gen/app_localizations_ru.dart b/lib/l10n/gen/app_localizations_ru.dart index d006c88..919297f 100644 --- a/lib/l10n/gen/app_localizations_ru.dart +++ b/lib/l10n/gen/app_localizations_ru.dart @@ -1126,6 +1126,10 @@ class AppLocalizationsRu extends AppLocalizations { @override String get addVacationRangeCta => 'Добавить период'; + @override + String get vacationExplainerBanner => + 'В эти периоды будильники с пометкой «пауза на время отпуска» не звонят. Будильники с пометкой «звонить всегда» это не затрагивает.'; + @override String get vacationNoActiveRangeHint => 'Сейчас нет активного периода отпуска.'; diff --git a/lib/l10n/gen/app_localizations_zh.dart b/lib/l10n/gen/app_localizations_zh.dart index 8923f8e..55a8635 100644 --- a/lib/l10n/gen/app_localizations_zh.dart +++ b/lib/l10n/gen/app_localizations_zh.dart @@ -1084,6 +1084,10 @@ class AppLocalizationsZh extends AppLocalizations { @override String get addVacationRangeCta => '添加时间段'; + @override + String get vacationExplainerBanner => + '在这些时间段内,标记为“假期暂停”的闹钟不会响铃;标记为“始终响铃”的闹钟不受影响。'; + @override String get vacationNoActiveRangeHint => '目前没有正在生效的假期时间段。'; diff --git a/lib/pantallas/pantalla_vacaciones.dart b/lib/pantallas/pantalla_vacaciones.dart index ac1ba11..3acb755 100644 --- a/lib/pantallas/pantalla_vacaciones.dart +++ b/lib/pantallas/pantalla_vacaciones.dart @@ -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 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), + ], + ), + ), ], ), ); diff --git a/test/pantallas/pantalla_vacaciones_test.dart b/test/pantallas/pantalla_vacaciones_test.dart index 63ce19c..539bcf6 100644 --- a/test/pantallas/pantalla_vacaciones_test.dart +++ b/test/pantallas/pantalla_vacaciones_test.dart @@ -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(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); + }, + ); + }); }