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);
|
final favoritas = _favoritasConSeleccion(radio.listaFavoritos);
|
||||||
return Padding(
|
// Audit 8.1 (t4 lines 372-374): opaque, bottom-anchored, full-bleed
|
||||||
padding: EdgeInsets.fromLTRB(12, 12, 12, bottom + 12),
|
// sheet with rounded TOP corners only and a grab handle — was a
|
||||||
child: PluriGlassSurface(
|
// floating card (glass, 12px margin on all 4 sides, uniform radius).
|
||||||
borderRadius: BorderRadius.circular(28),
|
return DecoratedBox(
|
||||||
padding: const EdgeInsets.all(18),
|
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(
|
child: Material(
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
@@ -428,6 +435,18 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
_AssetIcon(
|
_AssetIcon(
|
||||||
@@ -461,10 +480,28 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
|||||||
// WU10: the native showTimePicker dialog is replaced by a
|
// WU10: the native showTimePicker dialog is replaced by a
|
||||||
// giant inline HH:MM editor (drag/tap to adjust); see
|
// giant inline HH:MM editor (drag/tap to adjust); see
|
||||||
// `EditorHoraInline`, standalone-tested on its own.
|
// `EditorHoraInline`, standalone-tested on its own.
|
||||||
Center(
|
//
|
||||||
child: EditorHoraInline(
|
// Audit 8.3 (t4 line 378): framed in its own card — radius
|
||||||
value: _hora,
|
// 22, `listSurface` (#102532), a thin border — instead of
|
||||||
onChanged: (nuevo) => setState(() => _hora = nuevo),
|
// 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),
|
const SizedBox(height: 16),
|
||||||
@@ -491,27 +528,39 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
|||||||
// WU10: weekday circles are now ALWAYS visible (previously
|
// WU10: weekday circles are now ALWAYS visible (previously
|
||||||
// only inserted into the tree in diasSemana mode) — matching
|
// only inserted into the tree in diasSemana mode) — matching
|
||||||
// the mockup, which shows them unconditionally under the
|
// the mockup, which shows them unconditionally under the
|
||||||
// giant time. They stay disabled (onSelected: null, the
|
// giant time. They stay disabled (habilitado: false, the
|
||||||
// standard Material "greyed out" FilterChip state) outside
|
// same "greyed out, non-interactive" state the previous
|
||||||
// diasSemana mode rather than being wired to silently mutate
|
// FilterChip used) outside diasSemana mode rather than
|
||||||
// `_diasSemana` while a different `_tipo` is saved — no
|
// being wired to silently mutate `_diasSemana` while a
|
||||||
// scheduling-data-model change, presentation only.
|
// different `_tipo` is saved — no scheduling-data-model
|
||||||
Wrap(
|
// change, presentation only.
|
||||||
spacing: 6,
|
//
|
||||||
|
// 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: [
|
children: [
|
||||||
for (var i = DateTime.monday; i <= DateTime.sunday; i++)
|
for (
|
||||||
FilterChip(
|
var i = DateTime.monday;
|
||||||
label: Text(_weekdayShort(l10n, i)),
|
i <= DateTime.sunday;
|
||||||
selected: _diasSemana.contains(i),
|
i++
|
||||||
onSelected:
|
) ...[
|
||||||
_tipo == TipoProgramacionAlarma.diasSemana
|
if (i > DateTime.monday) const SizedBox(width: 7),
|
||||||
? (selected) => setState(() {
|
Expanded(
|
||||||
selected
|
child: _CirculoDiaSemana(
|
||||||
? _diasSemana.add(i)
|
label: _weekdayShort(l10n, i),
|
||||||
: _diasSemana.remove(i);
|
seleccionado: _diasSemana.contains(i),
|
||||||
})
|
habilitado:
|
||||||
: null,
|
_tipo == TipoProgramacionAlarma.diasSemana,
|
||||||
|
onTap:
|
||||||
|
() => setState(() {
|
||||||
|
_diasSemana.contains(i)
|
||||||
|
? _diasSemana.remove(i)
|
||||||
|
: _diasSemana.add(i);
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
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 {
|
class _AssetIcon extends StatelessWidget {
|
||||||
const _AssetIcon(this.asset, {this.size = 44, this.semanticLabel});
|
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/modelos/alarma_musical.dart';
|
||||||
import 'package:pluriwave/pantallas/pantalla_alarmas.dart';
|
import 'package:pluriwave/pantallas/pantalla_alarmas.dart';
|
||||||
import 'package:pluriwave/servicios/servicio_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:pluriwave/widgets/pluri_push_scaffold.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@@ -294,6 +295,113 @@ void main() {
|
|||||||
expect(volumen.min, 0.0);
|
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', () {
|
group('WU8 — tarjeta de alarma simplificada', () {
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'tocar la tarjeta abre el editor precargado con los datos de esa '
|
'tocar la tarjeta abre el editor precargado con los datos de esa '
|
||||||
|
|||||||
Reference in New Issue
Block a user