/// Renders the Required Banner widget for the FocusFlow Flutter UI. library; import 'package:flutter/material.dart'; import '../models/today_screen_models.dart'; import '../theme/focus_flow_tokens.dart'; /// Banner that highlights the next required task, if one exists. class RequiredBanner extends StatelessWidget { /// Creates a required-task banner. const RequiredBanner({this.model, this.onShowUpcoming, super.key}); /// Optional required task model to render. final RequiredBannerModel? model; /// Callback invoked when the highlighted required task should be shown. final VoidCallback? onShowUpcoming; @override Widget build(BuildContext context) { final banner = model; if (banner == null) { return const SizedBox(height: _RequiredBannerMetrics.referenceHeight); } return LayoutBuilder( builder: (context, constraints) { final metrics = _RequiredBannerMetrics.fromWidth(constraints.maxWidth); return Container( height: _RequiredBannerMetrics.referenceHeight, padding: EdgeInsets.symmetric(horizontal: metrics.horizontalPadding), decoration: BoxDecoration( color: FocusFlowTokens.glassPanel, borderRadius: BorderRadius.circular(metrics.bannerRadius), border: Border.all( color: FocusFlowTokens.navBorder.withValues(alpha: 0.55), width: metrics.borderWidth, ), ), child: Row( children: [ Container( width: metrics.iconCircleSize, height: metrics.iconCircleSize, decoration: const BoxDecoration( shape: BoxShape.circle, color: FocusFlowTokens.glassPanelStrong, ), child: Icon( Icons.auto_awesome, color: FocusFlowTokens.accentMagenta, size: metrics.iconSize, ), ), SizedBox(width: metrics.iconGap), Expanded( child: Align( alignment: Alignment.centerLeft, child: FittedBox( alignment: Alignment.centerLeft, fit: BoxFit.scaleDown, child: RichText( text: TextSpan( style: TextStyle( color: FocusFlowTokens.textPrimary, fontSize: metrics.textSize, fontWeight: FontWeight.w700, ), children: [ const TextSpan(text: 'Next required task: '), TextSpan( text: banner.title, style: const TextStyle( color: FocusFlowTokens.accentMagenta, ), ), TextSpan( text: ' at ${banner.timeText}', style: TextStyle( fontWeight: FontWeight.w500, fontSize: metrics.textSize, ), ), ], ), ), ), ), ), SizedBox(width: metrics.buttonGap), _UpcomingButton(metrics: metrics, onPressed: onShowUpcoming), ], ), ); }, ); } } class _RequiredBannerMetrics { const _RequiredBannerMetrics(this.scale); static const referenceHeight = 72.0; static const _referenceWidth = 760.0; static const _minimumScale = 0.5; factory _RequiredBannerMetrics.fromWidth(double width) { final safeWidth = width.isFinite ? width : _referenceWidth; final scale = (safeWidth / _referenceWidth).clamp(_minimumScale, 1.0); return _RequiredBannerMetrics(scale.toDouble()); } final double scale; double get horizontalPadding => 18 * scale; double get bannerRadius => FocusFlowTokens.bannerRadius * scale; double get borderWidth => scale < 0.7 ? 0.8 : 1; double get iconCircleSize => 48 * scale; double get iconSize => 24 * scale; double get iconGap => 26 * scale; double get buttonGap => 20 * scale; double get textSize => 19 * scale; double get buttonWidth => 176 * scale; double get buttonHeight => 46 * scale; double get buttonRadius => FocusFlowTokens.smallButtonRadius * scale; double get buttonTextSize => 16 * scale; double get buttonIconSize => 20 * scale; double get buttonTextGap => 6 * scale; EdgeInsets get buttonPadding => EdgeInsets.symmetric(horizontal: 12 * scale); } class _UpcomingButton extends StatelessWidget { const _UpcomingButton({required this.metrics, required this.onPressed}); final _RequiredBannerMetrics metrics; final VoidCallback? onPressed; @override Widget build(BuildContext context) { return OutlinedButton( onPressed: onPressed, style: OutlinedButton.styleFrom( foregroundColor: FocusFlowTokens.textPrimary, side: BorderSide( color: FocusFlowTokens.navBorder.withValues(alpha: 0.55), width: metrics.borderWidth, ), fixedSize: Size(metrics.buttonWidth, metrics.buttonHeight), minimumSize: Size.zero, padding: metrics.buttonPadding, tapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(metrics.buttonRadius), ), ), child: FittedBox( fit: BoxFit.scaleDown, child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( 'Show upcoming', style: TextStyle( fontSize: metrics.buttonTextSize, fontWeight: FontWeight.w700, ), ), SizedBox(width: metrics.buttonTextGap), Icon(Icons.chevron_right, size: metrics.buttonIconSize), ], ), ), ); } }