fix: hide task quick actions until hover
This commit is contained in:
parent
dacd189435
commit
5c6649945d
2 changed files with 137 additions and 67 deletions
|
|
@ -11,7 +11,7 @@ import 'difficulty_bars.dart';
|
|||
import 'reward_icon.dart';
|
||||
|
||||
/// Interactive card that renders a scheduled task on the compact timeline.
|
||||
class TaskTimelineCard extends StatelessWidget {
|
||||
class TaskTimelineCard extends StatefulWidget {
|
||||
/// Creates a task timeline card.
|
||||
const TaskTimelineCard({required this.card, required this.onTap, super.key});
|
||||
|
||||
|
|
@ -21,8 +21,16 @@ class TaskTimelineCard extends StatelessWidget {
|
|||
/// Callback invoked when the card is tapped.
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
State<TaskTimelineCard> createState() => _TaskTimelineCardState();
|
||||
}
|
||||
|
||||
class _TaskTimelineCardState extends State<TaskTimelineCard> {
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final card = widget.card;
|
||||
final colors = _CardColors.forKind(card.visualKind);
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
|
|
@ -30,84 +38,79 @@ class TaskTimelineCard extends StatelessWidget {
|
|||
constraints,
|
||||
kind: card.visualKind,
|
||||
);
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
key: ValueKey(card.id),
|
||||
borderRadius: BorderRadius.circular(metrics.cardRadius),
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: metrics.padding,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.background,
|
||||
borderRadius: BorderRadius.circular(metrics.cardRadius),
|
||||
border: Border.all(
|
||||
color: colors.accent,
|
||||
width: metrics.borderWidth,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colors.accent.withValues(alpha: 0.12),
|
||||
blurRadius: metrics.shadowBlur,
|
||||
spreadRadius: metrics.shadowSpread,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_StatusRing(
|
||||
card: card,
|
||||
return MouseRegion(
|
||||
onEnter: (_) {
|
||||
setState(() {
|
||||
_isHovered = true;
|
||||
});
|
||||
},
|
||||
onExit: (_) {
|
||||
setState(() {
|
||||
_isHovered = false;
|
||||
});
|
||||
},
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
key: ValueKey(card.id),
|
||||
borderRadius: BorderRadius.circular(metrics.cardRadius),
|
||||
onTap: widget.onTap,
|
||||
child: Container(
|
||||
padding: metrics.padding,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.background,
|
||||
borderRadius: BorderRadius.circular(metrics.cardRadius),
|
||||
border: Border.all(
|
||||
color: colors.accent,
|
||||
metrics: metrics,
|
||||
width: metrics.borderWidth,
|
||||
),
|
||||
SizedBox(width: metrics.statusGap),
|
||||
Expanded(
|
||||
child: _CardText(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colors.accent.withValues(alpha: 0.12),
|
||||
blurRadius: metrics.shadowBlur,
|
||||
spreadRadius: metrics.shadowSpread,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_StatusRing(
|
||||
card: card,
|
||||
color: colors.accent,
|
||||
metrics: metrics,
|
||||
),
|
||||
),
|
||||
if (card.showsQuickActions) ...[
|
||||
_NoOpIcon(
|
||||
icon: Icons.check,
|
||||
color: colors.accent,
|
||||
metrics: metrics,
|
||||
SizedBox(width: metrics.statusGap),
|
||||
Expanded(
|
||||
child: _CardText(
|
||||
card: card,
|
||||
color: colors.accent,
|
||||
metrics: metrics,
|
||||
),
|
||||
),
|
||||
_NoOpIcon(
|
||||
icon: Icons.arrow_forward,
|
||||
color: colors.accent,
|
||||
if (card.showsQuickActions)
|
||||
_QuickActions(
|
||||
color: colors.accent,
|
||||
metrics: metrics,
|
||||
visible: _isHovered,
|
||||
),
|
||||
_Badge(
|
||||
metrics: metrics,
|
||||
child: RewardIcon(
|
||||
color: colors.reward,
|
||||
size: metrics.rewardIconSize,
|
||||
),
|
||||
),
|
||||
_NoOpIcon(
|
||||
icon: Icons.calendar_today,
|
||||
color: colors.accent,
|
||||
SizedBox(width: metrics.badgeGap),
|
||||
_Badge(
|
||||
metrics: metrics,
|
||||
child: DifficultyBars(
|
||||
difficulty: card.difficultyIconToken,
|
||||
color: colors.reward,
|
||||
size: metrics.difficultySize,
|
||||
),
|
||||
),
|
||||
_NoOpIcon(
|
||||
icon: Icons.wb_sunny_outlined,
|
||||
color: colors.accent,
|
||||
metrics: metrics,
|
||||
),
|
||||
SizedBox(width: metrics.actionGap),
|
||||
],
|
||||
_Badge(
|
||||
metrics: metrics,
|
||||
child: RewardIcon(
|
||||
color: colors.reward,
|
||||
size: metrics.rewardIconSize,
|
||||
),
|
||||
),
|
||||
SizedBox(width: metrics.badgeGap),
|
||||
_Badge(
|
||||
metrics: metrics,
|
||||
child: DifficultyBars(
|
||||
difficulty: card.difficultyIconToken,
|
||||
color: colors.reward,
|
||||
size: metrics.difficultySize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -284,6 +287,55 @@ class _CardText extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _QuickActions extends StatelessWidget {
|
||||
const _QuickActions({
|
||||
required this.color,
|
||||
required this.metrics,
|
||||
required this.visible,
|
||||
});
|
||||
|
||||
final Color color;
|
||||
final _CardMetrics metrics;
|
||||
final bool visible;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExcludeSemantics(
|
||||
excluding: !visible,
|
||||
child: IgnorePointer(
|
||||
ignoring: !visible,
|
||||
child: AnimatedOpacity(
|
||||
opacity: visible ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 120),
|
||||
curve: Curves.easeOut,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_NoOpIcon(icon: Icons.check, color: color, metrics: metrics),
|
||||
_NoOpIcon(
|
||||
icon: Icons.arrow_forward,
|
||||
color: color,
|
||||
metrics: metrics,
|
||||
),
|
||||
_NoOpIcon(
|
||||
icon: Icons.calendar_today,
|
||||
color: color,
|
||||
metrics: metrics,
|
||||
),
|
||||
_NoOpIcon(
|
||||
icon: Icons.wb_sunny_outlined,
|
||||
color: color,
|
||||
metrics: metrics,
|
||||
),
|
||||
SizedBox(width: metrics.actionGap),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoOpIcon extends StatelessWidget {
|
||||
const _NoOpIcon({
|
||||
required this.icon,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
/// Tests Widget behavior in the FocusFlow Flutter app.
|
||||
library;
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:scheduler_core/scheduler_core.dart';
|
||||
|
|
@ -124,12 +125,29 @@ void main() {
|
|||
);
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(
|
||||
tester.widget<AnimatedOpacity>(find.byType(AnimatedOpacity)).opacity,
|
||||
0,
|
||||
);
|
||||
|
||||
final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer(
|
||||
location: tester.getCenter(find.byKey(const ValueKey('narrow-actions'))),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
tester.widget<AnimatedOpacity>(find.byType(AnimatedOpacity)).opacity,
|
||||
1,
|
||||
);
|
||||
|
||||
final button = tester.widget<IconButton>(
|
||||
find.widgetWithIcon(IconButton, Icons.arrow_forward),
|
||||
);
|
||||
expect(button.iconSize, lessThan(18));
|
||||
expect(button.constraints?.maxWidth, lessThan(32));
|
||||
|
||||
await gesture.removePointer();
|
||||
});
|
||||
|
||||
testWidgets('free slot text scales inside a short task bubble', (
|
||||
|
|
|
|||
Loading…
Reference in a new issue