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

251 lines
12 KiB
Dart

// 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),
],
),
),
);
}
}