From dcb415b3f817fd585dfb79d3597f22c441087af2 Mon Sep 17 00:00:00 2001 From: freetlab Date: Thu, 30 Jul 2026 16:42:04 +0200 Subject: [PATCH] fix(alarmas-editor): REPETIR eyebrow, grouped card, volume row Audit 8.5 (t4:381): the "REPETIR" eyebrow above the weekday circles -- never rendered before. New ARB key alarmRepeatSectionLabel. Audit 8.6 (t4:392-401): the station picker, volume, fade-in and vacation toggle now share ONE bordered card with sangred divider lines -- were four separately-spaced widgets. Restyled _CampoSelectorEmisora as a flat nav row (icon/bold label/muted value/chevron, matching FilaAjuste's own convention) so it fits inside the card instead of drawing its own outlined InputDecoration chrome -- used by BOTH the primary and fallback (Advanced) station fields. Snooze duration, "use current station", and the Advanced section stay OUTSIDE the card, exactly where they were (audit 8.8's own documented deliberate extras). A transparent Material sits inside the card's coloured box so the fade-in ListTile and the vacation SwitchListTile still paint their ink/background correctly (the same class of constraint already documented for PluriGlassSurface elsewhere in this codebase). Audit 8.7 (t4:396): the volume row is now a compact 112px track with a live percentage label -- was a bare full-width Slider with no visible value. New ArtB key alarmVolumeLabel. Item 8.8 (extra name/type-selector/snooze-selector/"use current station"/Advanced fields) stays exactly as documented: deliberate, required by the hard constraint that the editor keep its date field, fallback-station picker and sound dropdown. --- lib/l10n/app_ar.arb | 1 + lib/l10n/app_bn.arb | 1 + lib/l10n/app_de.arb | 1 + lib/l10n/app_en.arb | 1 + lib/l10n/app_es.arb | 1 + lib/l10n/app_fr.arb | 1 + lib/l10n/app_hi.arb | 1 + lib/l10n/app_id.arb | 1 + lib/l10n/app_it.arb | 1 + lib/l10n/app_ja.arb | 1 + lib/l10n/app_pt.arb | 1 + lib/l10n/app_ru.arb | 1 + lib/l10n/app_zh.arb | 1 + lib/l10n/gen/app_localizations.dart | 6 + lib/l10n/gen/app_localizations_ar.dart | 3 + lib/l10n/gen/app_localizations_bn.dart | 3 + lib/l10n/gen/app_localizations_de.dart | 3 + lib/l10n/gen/app_localizations_en.dart | 3 + lib/l10n/gen/app_localizations_es.dart | 3 + lib/l10n/gen/app_localizations_fr.dart | 3 + lib/l10n/gen/app_localizations_hi.dart | 3 + lib/l10n/gen/app_localizations_id.dart | 3 + lib/l10n/gen/app_localizations_it.dart | 3 + lib/l10n/gen/app_localizations_ja.dart | 3 + lib/l10n/gen/app_localizations_pt.dart | 3 + lib/l10n/gen/app_localizations_ru.dart | 3 + lib/l10n/gen/app_localizations_zh.dart | 3 + lib/pantallas/pantalla_alarmas.dart | 359 ++++++++++++------ .../pantalla_alarmas_editor_test.dart | 60 +++ 29 files changed, 364 insertions(+), 113 deletions(-) diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 2c53529..62677f7 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -594,6 +594,7 @@ } }, "alarmRepeatSectionLabel": "تكرار", + "alarmVolumeLabel": "مستوى الصوت", "androidReliabilityTitle": "مراجعة موثوقية Android", "closeAction": "إغلاق", "customOption": "مخصص", diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index 68545c2..f545b7f 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -594,6 +594,7 @@ } }, "alarmRepeatSectionLabel": "পুনরাবৃত্তি", + "alarmVolumeLabel": "ভলিউম", "androidReliabilityTitle": "Android নির্ভরযোগ্যতা দেখুন", "closeAction": "বন্ধ করুন", "customOption": "কাস্টম", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 96a177f..5683aa7 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -594,6 +594,7 @@ } }, "alarmRepeatSectionLabel": "WIEDERHOLEN", + "alarmVolumeLabel": "Lautstärke", "androidReliabilityTitle": "Android-Zuverlässigkeit prüfen", "closeAction": "Schließen", "customOption": "Benutzerdefiniert", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3f11097..cc04eb8 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -705,6 +705,7 @@ } }, "alarmRepeatSectionLabel": "REPEAT", + "alarmVolumeLabel": "Volume", "androidReliabilityTitle": "Review Android reliability", "closeAction": "Close", "customOption": "Custom", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 89f9c18..b2e29d6 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -661,6 +661,7 @@ "alarmScheduleWeekdays": "Días: {days}", "@alarmScheduleWeekdays": {"placeholders": {"days": {}}}, "alarmRepeatSectionLabel": "REPETIR", + "alarmVolumeLabel": "Volumen", "androidReliabilityTitle": "Revisar fiabilidad Android", "closeAction": "Cerrar", "customOption": "Personalizada", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index a4c69cc..8e2c1d5 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -594,6 +594,7 @@ } }, "alarmRepeatSectionLabel": "RÉPÉTER", + "alarmVolumeLabel": "Volume", "androidReliabilityTitle": "Vérifier la fiabilité Android", "closeAction": "Fermer", "customOption": "Personnalisée", diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index bfbde01..a7564a3 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -594,6 +594,7 @@ } }, "alarmRepeatSectionLabel": "दोहराएं", + "alarmVolumeLabel": "आवाज़", "androidReliabilityTitle": "Android विश्वसनीयता जाँचें", "closeAction": "बंद करें", "customOption": "कस्टम", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 1597da7..47c8ddc 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -594,6 +594,7 @@ } }, "alarmRepeatSectionLabel": "ULANGI", + "alarmVolumeLabel": "Volume", "androidReliabilityTitle": "Tinjau keandalan Android", "closeAction": "Tutup", "customOption": "Kustom", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 4ab2750..4461135 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -594,6 +594,7 @@ } }, "alarmRepeatSectionLabel": "RIPETI", + "alarmVolumeLabel": "Volume", "androidReliabilityTitle": "Controlla affidabilità Android", "closeAction": "Chiudi", "customOption": "Personalizzata", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 995c545..fea1137 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -594,6 +594,7 @@ } }, "alarmRepeatSectionLabel": "繰り返し", + "alarmVolumeLabel": "音量", "androidReliabilityTitle": "Androidの信頼性を確認", "closeAction": "閉じる", "customOption": "カスタム", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index b50e602..eba20e4 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -594,6 +594,7 @@ } }, "alarmRepeatSectionLabel": "REPETIÇÃO", + "alarmVolumeLabel": "Volume", "androidReliabilityTitle": "Revisar confiabilidade Android", "closeAction": "Fechar", "customOption": "Personalizada", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 3f793f5..9fa6b11 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -594,6 +594,7 @@ } }, "alarmRepeatSectionLabel": "ПОВТОР", + "alarmVolumeLabel": "Громкость", "androidReliabilityTitle": "Проверить надёжность Android", "closeAction": "Закрыть", "customOption": "Своя", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 4d4c8d5..d7c653a 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -594,6 +594,7 @@ } }, "alarmRepeatSectionLabel": "重复", + "alarmVolumeLabel": "音量", "androidReliabilityTitle": "检查 Android 可靠性", "closeAction": "关闭", "customOption": "自定义", diff --git a/lib/l10n/gen/app_localizations.dart b/lib/l10n/gen/app_localizations.dart index bcf307b..21928c1 100644 --- a/lib/l10n/gen/app_localizations.dart +++ b/lib/l10n/gen/app_localizations.dart @@ -2498,6 +2498,12 @@ abstract class AppLocalizations { /// **'REPETIR'** String get alarmRepeatSectionLabel; + /// No description provided for @alarmVolumeLabel. + /// + /// In es, this message translates to: + /// **'Volumen'** + String get alarmVolumeLabel; + /// No description provided for @androidReliabilityTitle. /// /// 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 f98798f..92ad992 100644 --- a/lib/l10n/gen/app_localizations_ar.dart +++ b/lib/l10n/gen/app_localizations_ar.dart @@ -1372,6 +1372,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get alarmRepeatSectionLabel => 'تكرار'; + @override + String get alarmVolumeLabel => 'مستوى الصوت'; + @override String get androidReliabilityTitle => 'مراجعة موثوقية Android'; diff --git a/lib/l10n/gen/app_localizations_bn.dart b/lib/l10n/gen/app_localizations_bn.dart index abb11ff..7c2c4a0 100644 --- a/lib/l10n/gen/app_localizations_bn.dart +++ b/lib/l10n/gen/app_localizations_bn.dart @@ -1381,6 +1381,9 @@ class AppLocalizationsBn extends AppLocalizations { @override String get alarmRepeatSectionLabel => 'পুনরাবৃত্তি'; + @override + String get alarmVolumeLabel => 'ভলিউম'; + @override String get androidReliabilityTitle => 'Android নির্ভরযোগ্যতা দেখুন'; diff --git a/lib/l10n/gen/app_localizations_de.dart b/lib/l10n/gen/app_localizations_de.dart index 3a0e69f..f1a3a46 100644 --- a/lib/l10n/gen/app_localizations_de.dart +++ b/lib/l10n/gen/app_localizations_de.dart @@ -1389,6 +1389,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get alarmRepeatSectionLabel => 'WIEDERHOLEN'; + @override + String get alarmVolumeLabel => 'Lautstärke'; + @override String get androidReliabilityTitle => 'Android-Zuverlässigkeit prüfen'; diff --git a/lib/l10n/gen/app_localizations_en.dart b/lib/l10n/gen/app_localizations_en.dart index 3fa21d4..a48fc1f 100644 --- a/lib/l10n/gen/app_localizations_en.dart +++ b/lib/l10n/gen/app_localizations_en.dart @@ -1373,6 +1373,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get alarmRepeatSectionLabel => 'REPEAT'; + @override + String get alarmVolumeLabel => 'Volume'; + @override String get androidReliabilityTitle => 'Review Android reliability'; diff --git a/lib/l10n/gen/app_localizations_es.dart b/lib/l10n/gen/app_localizations_es.dart index 1f78cb9..fce8e35 100644 --- a/lib/l10n/gen/app_localizations_es.dart +++ b/lib/l10n/gen/app_localizations_es.dart @@ -1383,6 +1383,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get alarmRepeatSectionLabel => 'REPETIR'; + @override + String get alarmVolumeLabel => 'Volumen'; + @override String get androidReliabilityTitle => 'Revisar fiabilidad Android'; diff --git a/lib/l10n/gen/app_localizations_fr.dart b/lib/l10n/gen/app_localizations_fr.dart index e624d4d..49a6b5f 100644 --- a/lib/l10n/gen/app_localizations_fr.dart +++ b/lib/l10n/gen/app_localizations_fr.dart @@ -1393,6 +1393,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get alarmRepeatSectionLabel => 'RÉPÉTER'; + @override + String get alarmVolumeLabel => 'Volume'; + @override String get androidReliabilityTitle => 'Vérifier la fiabilité Android'; diff --git a/lib/l10n/gen/app_localizations_hi.dart b/lib/l10n/gen/app_localizations_hi.dart index 8518e2d..a15dd55 100644 --- a/lib/l10n/gen/app_localizations_hi.dart +++ b/lib/l10n/gen/app_localizations_hi.dart @@ -1376,6 +1376,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get alarmRepeatSectionLabel => 'दोहराएं'; + @override + String get alarmVolumeLabel => 'आवाज़'; + @override String get androidReliabilityTitle => 'Android विश्वसनीयता जाँचें'; diff --git a/lib/l10n/gen/app_localizations_id.dart b/lib/l10n/gen/app_localizations_id.dart index 9100009..a813041 100644 --- a/lib/l10n/gen/app_localizations_id.dart +++ b/lib/l10n/gen/app_localizations_id.dart @@ -1384,6 +1384,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get alarmRepeatSectionLabel => 'ULANGI'; + @override + String get alarmVolumeLabel => 'Volume'; + @override String get androidReliabilityTitle => 'Tinjau keandalan Android'; diff --git a/lib/l10n/gen/app_localizations_it.dart b/lib/l10n/gen/app_localizations_it.dart index f6f8156..07906b9 100644 --- a/lib/l10n/gen/app_localizations_it.dart +++ b/lib/l10n/gen/app_localizations_it.dart @@ -1392,6 +1392,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get alarmRepeatSectionLabel => 'RIPETI'; + @override + String get alarmVolumeLabel => 'Volume'; + @override String get androidReliabilityTitle => 'Controlla affidabilità Android'; diff --git a/lib/l10n/gen/app_localizations_ja.dart b/lib/l10n/gen/app_localizations_ja.dart index 52966c1..76d1f95 100644 --- a/lib/l10n/gen/app_localizations_ja.dart +++ b/lib/l10n/gen/app_localizations_ja.dart @@ -1333,6 +1333,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get alarmRepeatSectionLabel => '繰り返し'; + @override + String get alarmVolumeLabel => '音量'; + @override String get androidReliabilityTitle => 'Androidの信頼性を確認'; diff --git a/lib/l10n/gen/app_localizations_pt.dart b/lib/l10n/gen/app_localizations_pt.dart index 36b1217..f6dc84e 100644 --- a/lib/l10n/gen/app_localizations_pt.dart +++ b/lib/l10n/gen/app_localizations_pt.dart @@ -1381,6 +1381,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get alarmRepeatSectionLabel => 'REPETIÇÃO'; + @override + String get alarmVolumeLabel => 'Volume'; + @override String get androidReliabilityTitle => 'Revisar confiabilidade Android'; diff --git a/lib/l10n/gen/app_localizations_ru.dart b/lib/l10n/gen/app_localizations_ru.dart index 56f48b5..d006c88 100644 --- a/lib/l10n/gen/app_localizations_ru.dart +++ b/lib/l10n/gen/app_localizations_ru.dart @@ -1385,6 +1385,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get alarmRepeatSectionLabel => 'ПОВТОР'; + @override + String get alarmVolumeLabel => 'Громкость'; + @override String get androidReliabilityTitle => 'Проверить надёжность Android'; diff --git a/lib/l10n/gen/app_localizations_zh.dart b/lib/l10n/gen/app_localizations_zh.dart index 9c6baba..8923f8e 100644 --- a/lib/l10n/gen/app_localizations_zh.dart +++ b/lib/l10n/gen/app_localizations_zh.dart @@ -1328,6 +1328,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get alarmRepeatSectionLabel => '重复'; + @override + String get alarmVolumeLabel => '音量'; + @override String get androidReliabilityTitle => '检查 Android 可靠性'; diff --git a/lib/pantallas/pantalla_alarmas.dart b/lib/pantallas/pantalla_alarmas.dart index 40b70b6..593b518 100644 --- a/lib/pantallas/pantalla_alarmas.dart +++ b/lib/pantallas/pantalla_alarmas.dart @@ -647,6 +647,15 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> { (value) => setState(() => _tipo = value.first), ), const SizedBox(height: 10), + // Audit 8.5 (t4 line 381): the "REPETIR" eyebrow above the + // weekday circles -- never rendered anywhere before. + Padding( + padding: const EdgeInsets.fromLTRB(4, 0, 4, 8), + child: Text( + l10n.alarmRepeatSectionLabel, + style: context.pluriType.eyebrowLabel, + ), + ), // WU10: weekday circles are now ALWAYS visible (previously // only inserted into the tree in diasSemana mode) — matching // the mockup, which shows them unconditionally under the @@ -688,80 +697,117 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> { const SizedBox(height: 12), _vistaProximaEjecucion(l10n), const SizedBox(height: 14), - _SectionLabel( - icon: 'assets/icons/alarmas/fallback_sound.png', - text: l10n.soundAndVolumeSection, - ), - Slider( - value: _volumen, - // S2-R11: floor lowered from 0.25 to 0.0. - min: 0, - max: 1, - divisions: 20, - label: '${(_volumen * 100).round()}%', - onChanged: (value) => setState(() => _volumen = value), - ), - const SizedBox(height: 8), - ListTile( - contentPadding: EdgeInsets.zero, - title: Text( - l10n.alarmFadeInTitle, - style: context.pluriType.cardTitle, + // Audit 8.6 (t4 lines 392-401): the station picker, volume, + // fade-in and vacation toggle now share ONE bordered card + // with sangred divider lines between rows -- matching the + // prototype's own single grouped block -- instead of + // sitting bare on the sheet, each with its own gaps. The + // extra fields the prototype does NOT show (name, type + // selector, snooze selector, "use current station", + // Advanced) stay OUTSIDE this card, exactly where they + // were (documented as deliberate in audit 8.8 / WU10). + DecoratedBox( + key: const ValueKey('alarm-editor-grouped-card'), + decoration: BoxDecoration( + color: context.pluriTokens.listSurface, + borderRadius: BorderRadius.circular(18), + border: Border.all( + color: Colors.white.withValues(alpha: 0.08), + ), ), - subtitle: Text( - _fadeInSegundos == 0 - ? l10n.alarmFadeInOff - : l10n.alarmFadeInSummary(_fadeInSegundos), + // A transparent Material sits directly inside the coloured + // box, closer to the fade-in ListTile and the vacation + // SwitchListTile than the DecoratedBox's own opaque fill + // -- both paint their ink/background on the NEAREST + // Material ancestor, and without this the box's colour + // would hide those effects (Flutter's own debug-mode + // check for exactly this). + child: Material( + type: MaterialType.transparency, + child: Column( + children: [ + // S2-R9: searchable bottom-sheet picker instead of a + // dropdown, for the primary station. The backup + // (fallback) picker moves into the Advanced section + // below (WU10) — the primary choice stays a + // top-level field, only its secondary/backup sibling + // is now one tap further away. + _CampoSelectorEmisora( + key: const ValueKey('alarm-station-field'), + label: l10n.favoriteStationLabel, + icon: Icons.radio_rounded, + value: + _emisora == null + ? l10n.noStationUseInternalSound + : localizedStationName( + l10n, + _emisora!.nombre, + ), + onTap: + () => _elegirEmisora( + favoritas, + seleccionar: + (emisora) => + setState(() => _emisora = emisora), + ), + ), + const Divider(height: 1, indent: 66), + // Audit 8.7 (t4 line 396): a compact 112px track with + // a trailing percentage label -- was a bare + // full-width Slider with no visible value. + _FilaVolumen( + volumen: _volumen, + onChanged: + (value) => setState(() => _volumen = value), + ), + const Divider(height: 1, indent: 46), + Padding( + padding: const EdgeInsets.fromLTRB(14, 4, 14, 4), + child: ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + l10n.alarmFadeInTitle, + style: context.pluriType.cardTitle, + ), + subtitle: Text( + _fadeInSegundos == 0 + ? l10n.alarmFadeInOff + : l10n.alarmFadeInSummary(_fadeInSegundos), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Slider( + value: _fadeInSegundos.toDouble(), + min: 0, + max: 60, + divisions: 60, + label: '${_fadeInSegundos}s', + onChanged: + (value) => setState( + () => _fadeInSegundos = value.round(), + ), + ), + ), + const Divider(height: 1, indent: 46), + SwitchListTile.adaptive( + value: _sonarEnVacaciones, + onChanged: + (value) => + setState(() => _sonarEnVacaciones = value), + secondary: _AssetIcon( + 'assets/icons/alarmas/vacation_wave.png', + size: 42, + semanticLabel: l10n.vacationIconLabel, + ), + title: Text(l10n.playDuringVacations), + subtitle: Text(l10n.playDuringVacationsHint), + ), + ], + ), ), ), - Slider( - value: _fadeInSegundos.toDouble(), - min: 0, - max: 60, - divisions: 60, - label: '${_fadeInSegundos}s', - onChanged: - (value) => - setState(() => _fadeInSegundos = value.round()), - ), - ListTile( - contentPadding: EdgeInsets.zero, - title: Text(l10n.alarmSnoozeDurationTitle), - subtitle: Text(l10n.alarmSnoozeOptionLabel(_snoozeMinutos)), - ), - SegmentedButton( - segments: [ - for (final minutos in _opcionesSnooze()) - ButtonSegment( - value: minutos, - label: Text(l10n.alarmSnoozeOptionLabel(minutos)), - ), - ], - selected: {_snoozeMinutos}, - onSelectionChanged: - (value) => setState(() => _snoozeMinutos = value.first), - ), - const SizedBox(height: 8), - // S2-R9: searchable bottom-sheet picker instead of a dropdown, - // for the primary station. The backup (fallback) picker moves - // into the Advanced section below (WU10) — the primary choice - // stays a top-level field, only its secondary/backup sibling - // is now one tap further away. - _CampoSelectorEmisora( - key: const ValueKey('alarm-station-field'), - label: l10n.favoriteStationLabel, - icon: Icons.radio_rounded, - value: - _emisora == null - ? l10n.noStationUseInternalSound - : localizedStationName(l10n, _emisora!.nombre), - onTap: - () => _elegirEmisora( - favoritas, - seleccionar: - (emisora) => setState(() => _emisora = emisora), - ), - ), if (favoritas.isEmpty) ...[ const SizedBox(height: 6), Text(l10n.saveFavoritesAlarmHint), @@ -778,19 +824,23 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> { ), ), ], - const SizedBox(height: 8), - SwitchListTile.adaptive( + const SizedBox(height: 12), + ListTile( contentPadding: EdgeInsets.zero, - value: _sonarEnVacaciones, - onChanged: - (value) => setState(() => _sonarEnVacaciones = value), - secondary: _AssetIcon( - 'assets/icons/alarmas/vacation_wave.png', - size: 42, - semanticLabel: l10n.vacationIconLabel, - ), - title: Text(l10n.playDuringVacations), - subtitle: Text(l10n.playDuringVacationsHint), + title: Text(l10n.alarmSnoozeDurationTitle), + subtitle: Text(l10n.alarmSnoozeOptionLabel(_snoozeMinutos)), + ), + SegmentedButton( + segments: [ + for (final minutos in _opcionesSnooze()) + ButtonSegment( + value: minutos, + label: Text(l10n.alarmSnoozeOptionLabel(minutos)), + ), + ], + selected: {_snoozeMinutos}, + onSelectionChanged: + (value) => setState(() => _snoozeMinutos = value.first), ), const SizedBox(height: 8), // WU10 (native-alarms delta — Alarm Editor Preserves Date, @@ -1033,18 +1083,122 @@ class _CampoSelectorEmisora extends StatelessWidget { final String value; final VoidCallback onTap; + /// Audit 8.6 (t4:394): a flat nav row -- icon, bold label, muted value, + /// chevron -- matching the same convention `FilaAjuste` uses everywhere + /// else in Settings, so this fits cleanly inside the grouped card below + /// instead of drawing its own outlined `InputDecoration` chrome. @override Widget build(BuildContext context) { - return InkWell( - borderRadius: BorderRadius.circular(12), - onTap: onTap, - child: InputDecorator( - decoration: InputDecoration( - labelText: label, - prefixIcon: Icon(icon), - suffixIcon: const Icon(Icons.arrow_drop_down_rounded), + final type = context.pluriType; + return Material( + type: MaterialType.transparency, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11), + child: Row( + children: [ + Icon(icon, size: 20), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: type.cardTitle), + Text( + value, + overflow: TextOverflow.ellipsis, + style: type.bodyStrong.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.55), + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right_rounded, + size: 19, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.4), + ), + ], + ), ), - child: Text(value, overflow: TextOverflow.ellipsis), + ), + ); + } +} + +/// Audit 8.7 (t4 line 396): a compact volume row -- icon, label, a 112px +/// track, and a trailing "80%" -- was a bare full-width `Slider` with no +/// visible current value. Constrains a real, still-draggable `Slider` +/// (not a static bar) to the prototype's 112px track width via a +/// `SliderTheme` + fixed-width `SizedBox`, preserving drag interactivity. +class _FilaVolumen extends StatelessWidget { + const _FilaVolumen({required this.volumen, required this.onChanged}); + + final double volumen; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final onSurface = Theme.of(context).colorScheme.onSurface; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4), + child: Row( + children: [ + Icon( + Icons.volume_up_rounded, + size: 20, + color: onSurface.withValues(alpha: 0.7), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + l10n.alarmVolumeLabel, + style: context.pluriType.cardTitle, + ), + ), + SizedBox( + width: 112, + height: 24, + child: SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: 4, + activeTrackColor: PluriWaveTokens.brand, + inactiveTrackColor: Colors.white.withValues(alpha: 0.14), + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 7), + overlayShape: const RoundSliderOverlayShape(overlayRadius: 14), + ), + child: Slider( + value: volumen, + // S2-R11: floor lowered from 0.25 to 0.0. + min: 0, + max: 1, + divisions: 20, + label: '${(volumen * 100).round()}%', + onChanged: onChanged, + ), + ), + ), + SizedBox( + width: 34, + child: Text( + '${(volumen * 100).round()}%', + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w800, + color: onSurface.withValues(alpha: 0.7), + ), + ), + ), + ], ), ); } @@ -1446,27 +1600,6 @@ class _PickerButton extends StatelessWidget { } } -class _SectionLabel extends StatelessWidget { - const _SectionLabel({required this.icon, required this.text}); - - final String icon; - final String text; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - _AssetIcon(icon, size: 34), - const SizedBox(width: 8), - // WU10: swapped the raw TextTheme lookup for the named type-scale - // token (cosmetic only — same weight class, now shared with every - // other card/section title in the redesign). - Text(text, style: context.pluriType.cardTitle), - ], - ); - } -} - class _NoticeLine extends StatelessWidget { const _NoticeLine({super.key, required this.icon, required this.text}); diff --git a/test/pantallas/pantalla_alarmas_editor_test.dart b/test/pantallas/pantalla_alarmas_editor_test.dart index 01c6ef7..fbfefd7 100644 --- a/test/pantallas/pantalla_alarmas_editor_test.dart +++ b/test/pantallas/pantalla_alarmas_editor_test.dart @@ -400,6 +400,66 @@ void main() { ); }, ); + + testWidgets('8.5: the "REPETIR" eyebrow sits above the weekday circles', ( + tester, + ) async { + await _abrirEditor(tester); + + expect(find.text(l10n.alarmRepeatSectionLabel), findsOneWidget); + final eyebrowY = + tester.getBottomLeft(find.text(l10n.alarmRepeatSectionLabel)).dy; + final circlesY = + tester + .getTopLeft(find.byKey(const ValueKey('alarm-weekday-circles'))) + .dy; + expect(eyebrowY <= circlesY, isTrue); + }); + + testWidgets( + '8.6: the station field, volume, fade-in and vacation toggle share ' + 'ONE grouped card', + (tester) async { + await _abrirEditor(tester); + + final tarjeta = find.byKey(const ValueKey('alarm-editor-grouped-card')); + expect(tarjeta, findsOneWidget); + expect( + find.descendant( + of: tarjeta, + matching: find.byKey(const ValueKey('alarm-station-field')), + ), + findsOneWidget, + ); + expect( + find.descendant(of: tarjeta, matching: find.byType(Slider)), + findsNWidgets(2), + reason: 'volume + fade-in sliders both live inside the card', + ); + expect( + find.descendant(of: tarjeta, matching: find.byType(SwitchListTile)), + findsOneWidget, + reason: 'the vacation toggle lives inside the card', + ); + // Snooze duration is a deliberate EXTRA (audit 8.8), kept OUTSIDE + // the prototype-mandated 4-row card. + expect( + find.descendant( + of: tarjeta, + matching: find.text(l10n.alarmSnoozeDurationTitle), + ), + findsNothing, + ); + }, + ); + + testWidgets('8.7: the volume row shows a live percentage label next to a ' + 'compact track', (tester) async { + await _abrirEditor(tester); + + expect(find.text(l10n.alarmVolumeLabel), findsOneWidget); + expect(find.text('85%'), findsOneWidget); + }); }); group('WU8 — tarjeta de alarma simplificada', () {