Files
focusflow/lib/core/widgets/time_visualizer.dart
Oracle Public Cloud User 50931d839d Initial scaffold: FocusFlow ADHD Task Manager Flutter app
BLoC/Cubit state management, ADHD-friendly theme (calming teal, no red),
GetIt DI, GoRouter navigation. Screens: task dashboard, focus mode,
task create/detail, streaks, time perception, settings, onboarding, auth.
Custom widgets: TaskCard, RewardPopup, StreakRing, GentleNudgeCard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:53:58 +00:00

126 lines
4.0 KiB
Dart

import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
/// Visual time block widget showing estimated vs actual duration.
///
/// - Horizontal bar: green (under estimate) -> yellow (near) -> soft orange (over).
/// - NO red — going over is shown gently, not punitively.
class TimeVisualizer extends StatelessWidget {
/// Estimated duration in minutes.
final int estimatedMinutes;
/// Actual duration in minutes (null if not yet completed).
final int? actualMinutes;
/// Optional height for the bars.
final double barHeight;
const TimeVisualizer({
super.key,
required this.estimatedMinutes,
this.actualMinutes,
this.barHeight = 12,
});
/// Returns a color based on actual / estimated ratio.
/// <= 0.8 -> green (under)
/// 0.8-1.1 -> teal (on time)
/// 1.1-1.5 -> yellow (slightly over)
/// > 1.5 -> soft orange (over, but gentle)
Color _barColor(double ratio) {
if (ratio <= 0.8) return AppColors.completed;
if (ratio <= 1.1) return AppColors.primary;
if (ratio <= 1.5) return AppColors.energyMedium;
return AppColors.energyHigh; // soft orange, NOT red
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final actual = actualMinutes;
final hasActual = actual != null && actual > 0;
// If no actual yet, show just the estimate bar.
final ratio = hasActual ? actual / estimatedMinutes : 0.0;
final estimateWidth = 1.0; // always full width = estimate
final actualWidth = hasActual ? ratio.clamp(0.0, 2.0) / 2.0 : 0.0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Labels
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Estimated: $estimatedMinutes min',
style: theme.textTheme.bodySmall,
),
if (hasActual)
Text(
'Actual: $actual min',
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w600,
color: _barColor(ratio),
),
),
],
),
const SizedBox(height: 6),
// ── Estimate bar (background / reference) ──────────────────
ClipRRect(
borderRadius: BorderRadius.circular(barHeight / 2),
child: Stack(
children: [
// Full estimate background
Container(
height: barHeight,
width: double.infinity,
decoration: BoxDecoration(
color: AppColors.primary.withAlpha(30),
borderRadius: BorderRadius.circular(barHeight / 2),
),
),
// Actual fill
if (hasActual)
FractionallySizedBox(
widthFactor: actualWidth.clamp(0.0, estimateWidth),
child: Container(
height: barHeight,
decoration: BoxDecoration(
color: _barColor(ratio),
borderRadius: BorderRadius.circular(barHeight / 2),
),
),
),
],
),
),
// ── Gentle insight ─────────────────────────────────────────
if (hasActual) ...[
const SizedBox(height: 4),
Text(
_insightText(ratio),
style: theme.textTheme.bodySmall?.copyWith(
color: _barColor(ratio),
fontWeight: FontWeight.w500,
),
),
],
],
);
}
String _insightText(double ratio) {
if (ratio <= 0.8) return 'Finished early — nice!';
if (ratio <= 1.1) return 'Right on target.';
if (ratio <= 1.5) return 'A little over — totally fine.';
return 'Took longer than expected. That happens!';
}
}