// SPDX-FileCopyrightText: 2026 FocusFlow contributors // SPDX-License-Identifier: AGPL-3.0-only /// 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; /// Builds the widget subtree for this component. /// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data. @override Widget build(BuildContext context) { final banner = model; if (banner == null) { return const SizedBox.shrink(); } 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), ], ), ); }, ); } } /// Private implementation type for `_RequiredBannerMetrics` in this library. /// It keeps supporting state and behavior scoped to the owning file so the public API stays focused on the scheduler concepts that callers use directly. class _RequiredBannerMetrics { /// Creates a `_RequiredBannerMetrics` instance with the values required by its invariants. /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code. const _RequiredBannerMetrics(this.scale); /// Shared constant value for `referenceHeight`. /// The constant gives this repeated scheduler or UI value one documented source of truth instead of scattering literals across call sites. static const referenceHeight = 72.0; /// Shared constant value for `_referenceWidth`. /// The constant gives this repeated scheduler or UI value one documented source of truth instead of scattering literals across call sites. static const _referenceWidth = 760.0; /// Shared constant value for `_minimumScale`. /// The constant gives this repeated scheduler or UI value one documented source of truth instead of scattering literals across call sites. static const _minimumScale = 0.5; /// Creates a `_RequiredBannerMetrics.fromWidth` instance with the values required by its invariants. /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code. factory _RequiredBannerMetrics.fromWidth(double width) { final safeWidth = width.isFinite ? width : _referenceWidth; final scale = (safeWidth / _referenceWidth).clamp(_minimumScale, 1.0); return _RequiredBannerMetrics(scale.toDouble()); } /// Stores the `scale` value for this object. /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior. final double scale; /// Returns the derived `horizontalPadding` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. double get horizontalPadding => 18 * scale; /// Returns the derived `bannerRadius` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. double get bannerRadius => FocusFlowTokens.bannerRadius * scale; /// Returns the derived `borderWidth` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. double get borderWidth => scale < 0.7 ? 0.8 : 1; /// Returns the derived `iconCircleSize` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. double get iconCircleSize => 48 * scale; /// Returns the derived `iconSize` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. double get iconSize => 24 * scale; /// Returns the derived `iconGap` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. double get iconGap => 26 * scale; /// Returns the derived `buttonGap` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. double get buttonGap => 20 * scale; /// Returns the derived `textSize` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. double get textSize => (19 * scale).clamp(13.0, 14.0).toDouble(); /// Returns the derived `buttonWidth` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. double get buttonWidth => 176 * scale; /// Returns the derived `buttonHeight` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. double get buttonHeight => 46 * scale; /// Returns the derived `buttonRadius` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. double get buttonRadius => FocusFlowTokens.smallButtonRadius * scale; /// Returns the derived `buttonTextSize` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. double get buttonTextSize => (16 * scale).clamp(11.0, 12.5).toDouble(); /// Returns the derived `buttonIconSize` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. double get buttonIconSize => 20 * scale; /// Returns the derived `buttonTextGap` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. double get buttonTextGap => 6 * scale; /// Returns the derived `buttonPadding` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. EdgeInsets get buttonPadding => EdgeInsets.symmetric(horizontal: 12 * scale); } /// Private implementation type for `_UpcomingButton` in this library. /// It keeps supporting state and behavior scoped to the owning file so the public API stays focused on the scheduler concepts that callers use directly. class _UpcomingButton extends StatelessWidget { /// Creates a `_UpcomingButton` instance with the values required by its invariants. /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code. const _UpcomingButton({required this.metrics, required this.onPressed}); /// Stores the `metrics` value for this object. /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior. final _RequiredBannerMetrics metrics; /// Stores the `onPressed` value for this object. /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior. final VoidCallback? onPressed; /// Builds the widget subtree for this component. /// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data. @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), ], ), ), ); } }