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

171 lines
5.8 KiB
Dart

/// 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, super.key});
/// Optional required task model to render.
final RequiredBannerModel? model;
@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),
],
),
);
},
);
}
}
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});
final _RequiredBannerMetrics metrics;
@override
Widget build(BuildContext context) {
return OutlinedButton(
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),
],
),
),
);
}
}