focus-flow/apps/focus_flow_flutter/lib/widgets/task_selection_modal.dart

298 lines
8.8 KiB
Dart

/// Renders the Task Selection Modal widget for the FocusFlow Flutter UI.
library;
import 'package:flutter/material.dart';
import '../models/today_screen_models.dart';
import '../theme/focus_flow_tokens.dart';
import 'timeline/difficulty_bars.dart';
import 'timeline/reward_icon.dart';
/// Modal shown when a selectable timeline task card is opened.
class TaskSelectionModal extends StatelessWidget {
/// Creates a task selection modal for [card].
const TaskSelectionModal({
required this.card,
required this.onClose,
this.onStatusPressed,
super.key,
});
/// Card model whose details and actions should be displayed.
final TimelineCardModel card;
/// Callback invoked when the modal should close.
final VoidCallback onClose;
/// Callback invoked when the status circle should toggle completion.
final VoidCallback? onStatusPressed;
@override
Widget build(BuildContext context) {
final accent = _accentFor(card.visualKind);
return Material(
color: Colors.transparent,
child: Container(
key: const ValueKey('task-selection-modal'),
width: 548,
padding: const EdgeInsets.fromLTRB(24, 22, 24, 20),
decoration: BoxDecoration(
color: FocusFlowTokens.elevatedPanel.withValues(alpha: 0.98),
borderRadius: BorderRadius.circular(FocusFlowTokens.modalRadius),
border: Border.all(color: accent.withValues(alpha: 0.86)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
_ModalStatusButton(
card: card,
color: accent,
onPressed: onStatusPressed,
),
const SizedBox(width: 28),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
card.title,
style: const TextStyle(
color: FocusFlowTokens.textPrimary,
fontSize: FocusFlowTokens.modalTitleSize,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 6),
_HeaderMetadata(card: card),
],
),
),
IconButton(
key: const ValueKey('close-task-modal'),
onPressed: onClose,
style: IconButton.styleFrom(
backgroundColor: FocusFlowTokens.glassPanelStrong,
foregroundColor: FocusFlowTokens.textPrimary,
),
icon: const Icon(Icons.close),
),
],
),
const SizedBox(height: 26),
Row(
children: [
Expanded(
child: _InfoTile(
label: card.rewardLabel,
child: RewardIcon(color: FocusFlowTokens.accentMagenta),
),
),
const SizedBox(width: 28),
Expanded(
child: _InfoTile(
label: card.effortLabel,
child: DifficultyBars(
difficulty: card.difficultyIconToken,
color: FocusFlowTokens.restPurple,
),
),
),
],
),
const SizedBox(height: 30),
GridView.count(
crossAxisCount: 2,
crossAxisSpacing: 26,
mainAxisSpacing: 16,
childAspectRatio: 4.1,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: const [
_ActionButton(icon: Icons.check, label: 'Done'),
_ActionButton(icon: Icons.arrow_forward, label: 'Push'),
_ActionButton(
icon: Icons.inventory_2_outlined,
label: 'Move to Backlog',
),
_ActionButton(icon: Icons.apps, label: 'Break up'),
],
),
const SizedBox(height: 14),
const Text(
'Options apply after confirmation.',
style: TextStyle(color: FocusFlowTokens.textMuted, fontSize: 14),
),
],
),
),
);
}
}
class _ModalStatusButton extends StatelessWidget {
const _ModalStatusButton({
required this.card,
required this.color,
required this.onPressed,
});
final TimelineCardModel card;
final Color color;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
final ring = Container(
key: const ValueKey('task-modal-status'),
width: 44,
height: 44,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: card.isCompleted ? color : Colors.transparent,
border: Border.all(color: color, width: 4),
),
child: card.isCompleted
? const Icon(
Icons.check,
color: FocusFlowTokens.appBackground,
size: 26,
)
: null,
);
if (onPressed == null) {
return ring;
}
return Tooltip(
message: card.isCompleted ? 'Mark not done' : 'Mark complete',
child: Semantics(
button: true,
label: card.isCompleted
? 'Mark ${card.title} not done'
: 'Mark ${card.title} complete',
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onPressed,
child: ring,
),
),
),
);
}
}
class _HeaderMetadata extends StatelessWidget {
const _HeaderMetadata({required this.card});
final TimelineCardModel card;
@override
Widget build(BuildContext context) {
const metadataStyle = TextStyle(
color: FocusFlowTokens.textMuted,
fontSize: 17,
);
if (!card.isCompleted) {
return Text('${card.typeLabel} - ${card.timeText}', style: metadataStyle);
}
final completedTime = card.completedTimeText;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
completedTime == null || completedTime.isEmpty
? 'Completed'
: 'Completed - $completedTime',
style: metadataStyle,
),
const SizedBox(height: 3),
Text(
'Planned: ${card.timeText}',
style: metadataStyle.copyWith(fontSize: 15),
),
],
);
}
}
class _InfoTile extends StatelessWidget {
const _InfoTile({required this.child, required this.label});
final Widget child;
final String label;
@override
Widget build(BuildContext context) {
return Container(
height: 58,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(FocusFlowTokens.smallButtonRadius),
border: Border.all(color: FocusFlowTokens.subtleBorder),
),
child: Row(
children: [
child,
const SizedBox(width: 18),
Flexible(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: FocusFlowTokens.textPrimary,
fontSize: 17,
fontWeight: FontWeight.w700,
),
),
),
],
),
);
}
}
class _ActionButton extends StatelessWidget {
const _ActionButton({required this.icon, required this.label});
final IconData icon;
final String label;
@override
Widget build(BuildContext context) {
return OutlinedButton.icon(
onPressed: () {},
style: OutlinedButton.styleFrom(
foregroundColor: FocusFlowTokens.textPrimary,
side: const BorderSide(color: FocusFlowTokens.subtleBorder),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
FocusFlowTokens.smallButtonRadius,
),
),
),
icon: Icon(icon),
label: Text(
label,
style: const TextStyle(
fontSize: FocusFlowTokens.modalButtonSize,
fontWeight: FontWeight.w700,
),
),
);
}
}
Color _accentFor(TaskVisualKind kind) {
return switch (kind) {
TaskVisualKind.flexible => FocusFlowTokens.flexibleGreen,
TaskVisualKind.required => FocusFlowTokens.requiredPink,
TaskVisualKind.appointment => FocusFlowTokens.appointmentBlue,
TaskVisualKind.freeSlot => FocusFlowTokens.restPurple,
TaskVisualKind.completedSurprise => FocusFlowTokens.completedGray,
};
}