171 lines
5.8 KiB
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),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|