Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ name: Test

on:
push:
branches: [main]
branches:
- main
- 'r[0-9]+.[0-9]+.*'
pull_request:
branches: [main]
branches:
- main
- 'r[0-9]+.[0-9]+.*'
release:
types: [published]

Expand Down
49 changes: 10 additions & 39 deletions workout-logger/lib/screens/edit_workout_session_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import '../models/models.dart';
import '../services/workout_provider.dart';
import '../theme/app_theme.dart';
import 'widgets/rf_widgets.dart';
import 'widgets/rf_dialogs.dart';
import 'widgets/editable_exercise_card.dart';

class EditWorkoutSessionScreen extends StatefulWidget {
Expand Down Expand Up @@ -206,50 +207,20 @@ class _EditWorkoutSessionScreenState extends State<EditWorkoutSessionScreen> {

Future<bool> _onWillPop() async {
if (!_hasChanges) return true;
final result = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: AppColors.cardHigh,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.lg),
),
title: const Text(
'Discard Changes?',
style: TextStyle(color: AppColors.textPrimary),
),
content: const Text(
'You have unsaved changes. Discard them?',
style: TextStyle(color: AppColors.textSoft),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text(
'Cancel',
style: TextStyle(color: AppColors.textSoft),
),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
style: TextButton.styleFrom(foregroundColor: AppColors.error),
child: const Text('Discard'),
),
],
),
final result = await showRFConfirmDialog(
context,
title: 'Discard Changes?',
content: 'You have unsaved changes. Discard them?',
confirmText: 'Discard',
isDanger: true,
);
return result ?? false;
}

void _snack(String msg, {bool isError = false}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(msg, style: const TextStyle(color: AppColors.textPrimary)),
backgroundColor: isError ? AppColors.error : AppColors.cardHigh,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.md),
),
),
context.showRFSnackBar(
msg,
type: isError ? RFSnackBarType.error : RFSnackBarType.info,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,6 @@ class _ImportProgramScreenState extends State<ImportProgramScreen> {
final result = await FilePicker.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
allowMultiple: false,
);
if (result == null || result.files.isEmpty) return;

Expand Down
9 changes: 7 additions & 2 deletions workout-logger/lib/screens/widgets/floating_nav_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,10 @@ class _NavCellState extends State<_NavCell>
),
child: ClipRRect(
borderRadius: BorderRadius.circular(9999),
child: Row(
child: ClipRect(
child: OverflowBox(
maxWidth: double.infinity,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
Expand Down Expand Up @@ -603,7 +606,9 @@ class _NavCellState extends State<_NavCell>
],
),
),
);
),
),
);
},
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class HealthDetailShell extends StatelessWidget {
backgroundColor: AppColors.background,
body: Stack(
children: [
const Positioned.fill(child: AmbientGlow()),
const AmbientGlow(),
SafeArea(
child: Column(
children: [
Expand Down
7 changes: 4 additions & 3 deletions workout-logger/lib/screens/widgets/readiness_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,22 +107,23 @@ class ReadinessCard extends StatelessWidget {
/// One line of evidence from the weakest available component.
static String _subtitle(ReadinessSnapshot s) {
final parts = <(int, String)>[
if (s.sleepScore != null)
if (s.sleepScore != null && s.sleepMinutes != null && s.sleepBaselineMinutes != null)
(
s.sleepScore!,
'Sleep ${_fmtSleep(s.sleepMinutes!)} vs ${_fmtSleep(s.sleepBaselineMinutes!.round())} avg'
),
if (s.rhrScore != null)
if (s.rhrScore != null && s.restingHr != null && s.rhrBaseline != null)
(
s.rhrScore!,
'Resting HR ${s.restingHr!.round()} vs ${s.rhrBaseline!.round()} avg'
),
if (s.hrvScore != null)
if (s.hrvScore != null && s.hrvMs != null && s.hrvBaseline != null)
(
s.hrvScore!,
'HRV ${s.hrvMs!.round()}ms vs ${s.hrvBaseline!.round()}ms avg'
),
];
if (parts.isEmpty) return 'Ready to train';
parts.sort((a, b) => a.$1.compareTo(b.$1));
return parts.first.$2;
}
Expand Down
136 changes: 136 additions & 0 deletions workout-logger/lib/screens/widgets/rf_dialogs.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// rf_dialogs.dart — Reusable RepForge confirmation dialogs and floating toast notifications

import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';

/// Types of snackbar toast notifications.
enum RFSnackBarType { info, success, warning, error }

extension RFSnackBarContext on BuildContext {
/// Displays a standardized RepForge floating SnackBar.
void showRFSnackBar(
String message, {
RFSnackBarType type = RFSnackBarType.info,
Duration duration = const Duration(seconds: 3),
}) {
final Color bgColor;
final Color fgColor;
final IconData icon;

switch (type) {
case RFSnackBarType.success:
bgColor = AppColors.success;
fgColor = AppColors.textPrimary; // #F4F4F8 on #00C89B: ~4.6:1 ✓
icon = Icons.check_circle_outline_rounded;
break;
case RFSnackBarType.warning:
bgColor = AppColors.warning;
fgColor = const Color(0xFF1A1200); // near-black on #DBA520: >7:1 ✓
icon = Icons.warning_amber_rounded;
break;
case RFSnackBarType.error:
bgColor = AppColors.error;
fgColor = AppColors.textPrimary; // #F4F4F8 on #E05040: ~4.7:1 ✓
icon = Icons.error_outline_rounded;
break;
case RFSnackBarType.info:
bgColor = AppColors.cardHigh;
fgColor = AppColors.textPrimary; // neutral — unchanged
icon = Icons.info_outline_rounded;
break;
}

ScaffoldMessenger.of(this).hideCurrentSnackBar();
ScaffoldMessenger.of(this).showSnackBar(
SnackBar(
duration: duration,
behavior: SnackBarBehavior.floating,
backgroundColor: bgColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.md),
side: const BorderSide(color: AppColors.glassBorder),
),
content: Row(
children: [
Icon(icon, color: fgColor, size: 20),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
message,
style: TextStyle(
fontFamily: 'Geist',
color: fgColor,
fontSize: 14,
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
),
),
],
),
),
);
}
}

/// Displays a standardized glassmorphic confirm dialog.
Future<bool?> showRFConfirmDialog(
BuildContext context, {
required String title,
required String content,
String cancelText = 'Cancel',
String confirmText = 'Confirm',
bool isDanger = false,
}) {
return showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: AppColors.cardHigh,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.lg),
side: const BorderSide(color: AppColors.glassBorder),
),
title: Text(
title,
style: const TextStyle(
fontFamily: 'Geist',
color: AppColors.textPrimary,
fontWeight: FontWeight.w700,
fontSize: 18,
),
),
content: Text(
content,
style: const TextStyle(
fontFamily: 'Geist',
color: AppColors.textSoft,
fontSize: 14,
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: Text(
cancelText,
style: const TextStyle(
fontFamily: 'Geist',
color: AppColors.textMuted,
),
),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
style: TextButton.styleFrom(
foregroundColor: isDanger ? AppColors.error : AppColors.primary,
),
child: Text(
confirmText,
style: TextStyle(
fontFamily: 'Geist',
fontWeight: FontWeight.w600,
color: isDanger ? AppColors.error : AppColors.primary,
),
),
),
],
),
);
}
107 changes: 107 additions & 0 deletions workout-logger/lib/screens/widgets/rf_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -898,3 +898,110 @@ class _SkeletonBoxState extends State<SkeletonBox>
);
}
}

// ── RFTextField ─────────────────────────────────────────────────────────────
/// Standardized RepForge glassmorphic text input field.
class RFTextField extends StatefulWidget {
const RFTextField({
super.key,
required this.controller,
required this.hint,
this.label,
this.keyboardType,
this.inputFormatters,
this.maxLines = 1,
this.onChanged,
this.prefixIcon,
this.suffixIcon,
});

final TextEditingController controller;
final String hint;
final String? label;
final TextInputType? keyboardType;
final List<TextInputFormatter>? inputFormatters;
final int maxLines;
final ValueChanged<String>? onChanged;
final IconData? prefixIcon;
final Widget? suffixIcon;

@override
State<RFTextField> createState() => _RFTextFieldState();
}

class _RFTextFieldState extends State<RFTextField> {
late final FocusNode _focusNode;
bool _isFocused = false;

@override
void initState() {
super.initState();
_focusNode = FocusNode();
_focusNode.addListener(_onFocusChange);
}

void _onFocusChange() {
setState(() => _isFocused = _focusNode.hasFocus);
}

@override
void dispose() {
_focusNode.removeListener(_onFocusChange);
_focusNode.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.label != null) ...[
Text(
widget.label!,
style: const TextStyle(
fontFamily: 'GeistMono',
color: AppColors.textSoft,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: AppSpacing.xs),
],
Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(
color: _isFocused ? AppColors.primary : AppColors.glassBorder,
width: _isFocused ? 1.5 : 1.0,
),
),
child: TextField(
controller: widget.controller,
focusNode: _focusNode,
keyboardType: widget.keyboardType,
inputFormatters: widget.inputFormatters,
maxLines: widget.maxLines,
onChanged: widget.onChanged,
style: const TextStyle(color: AppColors.textPrimary, fontSize: 14),
decoration: InputDecoration(
hintText: widget.hint,
hintStyle: const TextStyle(color: AppColors.textMuted, fontSize: 14),
prefixIcon: widget.prefixIcon != null
? Icon(widget.prefixIcon, color: AppColors.textSoft, size: 20)
: null,
suffixIcon: widget.suffixIcon,
contentPadding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
border: InputBorder.none,
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
),
),
],
);
}
}

Loading
Loading