fix(alarmas): restyle the editor sheet as the prototype draws it
Audit 8.1-8.4: opaque bottom-anchored sheet with a grab handle, a framed time card and 7 circular day buttons. The date field, fallback-station picker and sound dropdown all stay reachable -- the prototype omits them, but presentation changes never remove capability.
This commit is contained in:
@@ -416,11 +416,18 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
||||
});
|
||||
}
|
||||
final favoritas = _favoritasConSeleccion(radio.listaFavoritos);
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(12, 12, 12, bottom + 12),
|
||||
child: PluriGlassSurface(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
padding: const EdgeInsets.all(18),
|
||||
// Audit 8.1 (t4 lines 372-374): opaque, bottom-anchored, full-bleed
|
||||
// sheet with rounded TOP corners only and a grab handle — was a
|
||||
// floating card (glass, 12px margin on all 4 sides, uniform radius).
|
||||
return DecoratedBox(
|
||||
key: const ValueKey('alarm-editor-sheet-surface'),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF0D1B24),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
|
||||
border: Border(top: BorderSide(color: Color(0x1CFFFFFF))),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(20, 14, 20, bottom + 24),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: SingleChildScrollView(
|
||||
@@ -428,6 +435,18 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
key: const ValueKey('alarm-editor-grab-handle'),
|
||||
width: 44,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.22),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
_AssetIcon(
|
||||
@@ -461,10 +480,28 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
||||
// WU10: the native showTimePicker dialog is replaced by a
|
||||
// giant inline HH:MM editor (drag/tap to adjust); see
|
||||
// `EditorHoraInline`, standalone-tested on its own.
|
||||
Center(
|
||||
child: EditorHoraInline(
|
||||
value: _hora,
|
||||
onChanged: (nuevo) => setState(() => _hora = nuevo),
|
||||
//
|
||||
// Audit 8.3 (t4 line 378): framed in its own card — radius
|
||||
// 22, `listSurface` (#102532), a thin border — instead of
|
||||
// sitting bare on the sheet background. `EditorHoraInline`
|
||||
// itself is unchanged (no other consumer to keep in sync).
|
||||
DecoratedBox(
|
||||
key: const ValueKey('alarm-editor-hour-card'),
|
||||
decoration: BoxDecoration(
|
||||
color: context.pluriTokens.listSurface,
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.08),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||
child: Center(
|
||||
child: EditorHoraInline(
|
||||
value: _hora,
|
||||
onChanged: (nuevo) => setState(() => _hora = nuevo),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -491,27 +528,39 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
||||
// 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
|
||||
// giant time. They stay disabled (onSelected: null, the
|
||||
// standard Material "greyed out" FilterChip state) outside
|
||||
// diasSemana mode rather than being wired to silently mutate
|
||||
// `_diasSemana` while a different `_tipo` is saved — no
|
||||
// scheduling-data-model change, presentation only.
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
// giant time. They stay disabled (habilitado: false, the
|
||||
// same "greyed out, non-interactive" state the previous
|
||||
// FilterChip used) outside diasSemana mode rather than
|
||||
// being wired to silently mutate `_diasSemana` while a
|
||||
// different `_tipo` is saved — no scheduling-data-model
|
||||
// change, presentation only.
|
||||
//
|
||||
// Audit 8.4 (t4 lines 383-390): 7 circles (flex:1 each,
|
||||
// aspect-ratio:1, gap 7) — was a `Wrap` of `FilterChip`s.
|
||||
Row(
|
||||
key: const ValueKey('alarm-weekday-circles'),
|
||||
children: [
|
||||
for (var i = DateTime.monday; i <= DateTime.sunday; i++)
|
||||
FilterChip(
|
||||
label: Text(_weekdayShort(l10n, i)),
|
||||
selected: _diasSemana.contains(i),
|
||||
onSelected:
|
||||
_tipo == TipoProgramacionAlarma.diasSemana
|
||||
? (selected) => setState(() {
|
||||
selected
|
||||
? _diasSemana.add(i)
|
||||
: _diasSemana.remove(i);
|
||||
})
|
||||
: null,
|
||||
for (
|
||||
var i = DateTime.monday;
|
||||
i <= DateTime.sunday;
|
||||
i++
|
||||
) ...[
|
||||
if (i > DateTime.monday) const SizedBox(width: 7),
|
||||
Expanded(
|
||||
child: _CirculoDiaSemana(
|
||||
label: _weekdayShort(l10n, i),
|
||||
seleccionado: _diasSemana.contains(i),
|
||||
habilitado:
|
||||
_tipo == TipoProgramacionAlarma.diasSemana,
|
||||
onTap:
|
||||
() => setState(() {
|
||||
_diasSemana.contains(i)
|
||||
? _diasSemana.remove(i)
|
||||
: _diasSemana.add(i);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -1112,6 +1161,71 @@ class _PanelVacaciones extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Audit 8.4 (t4 lines 383-390): one circular weekday button in the alarm
|
||||
/// editor's REPETIR row — was a `FilterChip`. `aspect-ratio:1` in the
|
||||
/// prototype is achieved here by the caller wrapping each instance in an
|
||||
/// `Expanded` inside an `AspectRatio`-free `Row` — this widget's own
|
||||
/// `AspectRatio(1)` does the squaring regardless of the column width the
|
||||
/// `Row` assigns it.
|
||||
class _CirculoDiaSemana extends StatelessWidget {
|
||||
const _CirculoDiaSemana({
|
||||
required this.label,
|
||||
required this.seleccionado,
|
||||
required this.habilitado,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final bool seleccionado;
|
||||
final bool habilitado;
|
||||
final VoidCallback onTap;
|
||||
|
||||
/// t4 lines 384-388: the selected circle's TEXT is this specific dark
|
||||
/// literal — this theme's `colorScheme.onPrimary` is `Colors.white`
|
||||
/// (`pluriwave_theme.dart:23`), which would fail contrast against the
|
||||
/// bright cyan fill. No existing token matches this exact value.
|
||||
static const _textoSobreBrand = Color(0xFF062126);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = context.pluriTokens;
|
||||
final colorFondo =
|
||||
seleccionado ? PluriWaveTokens.brand : tokens.listSurface;
|
||||
final colorTexto =
|
||||
seleccionado
|
||||
? _textoSobreBrand
|
||||
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6);
|
||||
|
||||
return AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Material(
|
||||
color: colorFondo,
|
||||
shape: CircleBorder(
|
||||
side:
|
||||
seleccionado
|
||||
? BorderSide.none
|
||||
: BorderSide(color: Colors.white.withValues(alpha: 0.1)),
|
||||
),
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: habilitado ? onTap : null,
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: seleccionado ? FontWeight.w800 : FontWeight.w700,
|
||||
color:
|
||||
habilitado ? colorTexto : colorTexto.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AssetIcon extends StatelessWidget {
|
||||
const _AssetIcon(this.asset, {this.size = 44, this.semanticLabel});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||
import 'package:pluriwave/modelos/alarma_musical.dart';
|
||||
import 'package:pluriwave/pantallas/pantalla_alarmas.dart';
|
||||
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
||||
import 'package:pluriwave/widgets/editor_hora_inline.dart';
|
||||
import 'package:pluriwave/widgets/pluri_push_scaffold.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -294,6 +295,113 @@ void main() {
|
||||
expect(volumen.min, 0.0);
|
||||
});
|
||||
|
||||
group('visual fidelity (audit 8.1/8.3/8.4, item 20)', () {
|
||||
testWidgets(
|
||||
'8.1: the sheet is an opaque, bottom-anchored surface (radius on the '
|
||||
'top corners only) with a grab handle',
|
||||
(tester) async {
|
||||
await _abrirEditor(tester);
|
||||
|
||||
final surface = tester.widget<DecoratedBox>(
|
||||
find.byKey(const ValueKey('alarm-editor-sheet-surface')),
|
||||
);
|
||||
final decoracion = surface.decoration as BoxDecoration;
|
||||
expect(
|
||||
decoracion.color,
|
||||
const Color(0xFF0D1B24),
|
||||
reason: 't4 line 373: background:#0D1B24 (opaque, not glass)',
|
||||
);
|
||||
expect(
|
||||
decoracion.borderRadius,
|
||||
const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
reason:
|
||||
't4 line 373: border-radius:30px 30px 0 0 — top corners '
|
||||
'only, unlike a floating dialog',
|
||||
);
|
||||
|
||||
final asa = tester.widget<Container>(
|
||||
find.byKey(const ValueKey('alarm-editor-grab-handle')),
|
||||
);
|
||||
expect(asa.constraints?.maxWidth, 44);
|
||||
expect(asa.constraints?.maxHeight, 4);
|
||||
final asaDecoracion = asa.decoration as BoxDecoration;
|
||||
expect(
|
||||
asaDecoracion.borderRadius,
|
||||
BorderRadius.circular(2),
|
||||
reason: 't4 line 374: width:44px;height:4px;border-radius:2px',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('8.3: the hour editor sits inside a framed card', (
|
||||
tester,
|
||||
) async {
|
||||
await _abrirEditor(tester);
|
||||
|
||||
final tarjeta = tester.widget<DecoratedBox>(
|
||||
find.ancestor(
|
||||
of: find.byType(EditorHoraInline),
|
||||
matching: find.byKey(const ValueKey('alarm-editor-hour-card')),
|
||||
),
|
||||
);
|
||||
final decoracion = tarjeta.decoration as BoxDecoration;
|
||||
expect(
|
||||
decoracion.borderRadius,
|
||||
BorderRadius.circular(22),
|
||||
reason: 't4 line 378: border-radius:22px',
|
||||
);
|
||||
expect(
|
||||
decoracion.color,
|
||||
const Color(0xFF102532),
|
||||
reason: 't4 line 378: background:#102532 (== listSurface token)',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'8.4: the weekday selector is exactly 7 circular buttons, not chips',
|
||||
(tester) async {
|
||||
await _abrirEditor(tester);
|
||||
|
||||
final fila = find.byKey(const ValueKey('alarm-weekday-circles'));
|
||||
expect(fila, findsOneWidget);
|
||||
expect(
|
||||
find.descendant(of: fila, matching: find.byType(FilterChip)),
|
||||
findsNothing,
|
||||
reason: 't4 lines 383-390: 7 circles, not FilterChips',
|
||||
);
|
||||
final circulos = find.descendant(
|
||||
of: fila,
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is Material && w.shape is CircleBorder,
|
||||
),
|
||||
);
|
||||
expect(circulos, findsNWidgets(7));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'capacity guard: the date field, the fallback-station picker and the '
|
||||
'sound dropdown all stay reachable behind Advanced',
|
||||
(tester) async {
|
||||
await _abrirEditor(tester);
|
||||
|
||||
await tester.ensureVisible(find.text(l10n.alarmAdvancedSectionTitle));
|
||||
await tester.tap(find.text(l10n.alarmAdvancedSectionTitle));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(l10n.dateField), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey('alarm-fallback-station-field')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byType(DropdownButtonFormField<SonidoInternoAlarma>),
|
||||
findsOneWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('WU8 — tarjeta de alarma simplificada', () {
|
||||
testWidgets(
|
||||
'tocar la tarjeta abre el editor precargado con los datos de esa '
|
||||
|
||||
Reference in New Issue
Block a user