Tasks now carry a tick box rather than being read-only rows. Ticking strikes the title through and leaves the item in place, so a day still reads as a record of what was asked rather than only what is left. New in designs/Checkbox.dart: - TickBox the square box itself, animating between empty and filled - TickRow box + struck-through title + caller-supplied detail and trailing - addTaskAffordance the centred "Add new task" control beneath a day's list Applied to the home plan, the overdue queue and the goal task list. Each screen holds a set of optimistically ticked ids so the tick lands immediately and rolls back if the call fails; the set is reconciled whenever fresh data arrives. A tick completes the item outright, whatever its proof requirement. The proof flow is still reachable from the Complete button in the overdue queue, so tasks that need an artefact have a route that asks for one — but the tick itself no longer enforces it. Late completes stay visually distinct: the row keeps a Late pill and the warning accent, and the user is told the window had already closed. flutter analyze lib/ reports no errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mfu2gQLSFN21YRBcU2NrTt
217 lines
6.5 KiB
Dart
217 lines
6.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../about/internal/application/TextType.dart';
|
|
import '../utils/Colors.dart';
|
|
import 'text/Text.dart';
|
|
|
|
/// The tick box. Square, hairline border when empty, filled with the accent
|
|
/// and a white check when set — and it animates, because the tick is the one
|
|
/// moment of satisfaction the app allows itself.
|
|
class TickBox extends StatelessWidget {
|
|
final bool checked;
|
|
|
|
final VoidCallback? onTap;
|
|
|
|
/// The accent used when checked. Defaults to the app's positive green;
|
|
/// callers pass the class colour so the row reads as its own weight.
|
|
final Color? accent;
|
|
|
|
final double size;
|
|
|
|
/// Renders muted and ignores taps — used for items that are settled.
|
|
final bool locked;
|
|
|
|
const TickBox({
|
|
super.key,
|
|
required this.checked,
|
|
this.onTap,
|
|
this.accent,
|
|
this.size = 24,
|
|
this.locked = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final Color tone = locked ? colorGrey : (accent ?? colorPositive);
|
|
|
|
return GestureDetector(
|
|
onTap: locked ? null : onTap,
|
|
// A bare box is a small target; the padding brings it up to a
|
|
// comfortable tap area without changing the drawn size.
|
|
behavior: HitTestBehavior.opaque,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 2),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 160),
|
|
curve: Curves.easeOut,
|
|
width: size,
|
|
height: size,
|
|
decoration: BoxDecoration(
|
|
color: checked ? tone : Colors.transparent,
|
|
borderRadius: BorderRadius.circular(7),
|
|
border: Border.all(
|
|
color: checked ? tone : colorGrey.withValues(alpha: 0.55),
|
|
width: 1.6,
|
|
),
|
|
),
|
|
child: AnimatedOpacity(
|
|
duration: const Duration(milliseconds: 140),
|
|
opacity: checked ? 1 : 0,
|
|
child: Icon(
|
|
Icons.check_rounded,
|
|
size: size * 0.68,
|
|
color: colorWhite,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A task line: tick box, then the title struck through once it is done.
|
|
/// Everything else about the row — window, class, cost — is the caller's,
|
|
/// passed in as [detail] so this stays one shape used everywhere.
|
|
class TickRow extends StatelessWidget {
|
|
final bool checked;
|
|
|
|
final String title;
|
|
|
|
final VoidCallback? onTick;
|
|
|
|
final VoidCallback? onTap;
|
|
|
|
final Color? accent;
|
|
|
|
/// Secondary line beneath the title.
|
|
final Widget? detail;
|
|
|
|
/// Trailing widget — a class pill, a chevron.
|
|
final Widget? trailing;
|
|
|
|
/// Struck through and muted, but not tickable.
|
|
final bool locked;
|
|
|
|
const TickRow({
|
|
super.key,
|
|
required this.checked,
|
|
required this.title,
|
|
this.onTick,
|
|
this.onTap,
|
|
this.accent,
|
|
this.detail,
|
|
this.trailing,
|
|
this.locked = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final bool struck = checked || locked;
|
|
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
behavior: HitTestBehavior.opaque,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
TickBox(
|
|
checked: checked,
|
|
onTap: onTick,
|
|
accent: accent,
|
|
locked: locked,
|
|
),
|
|
const SizedBox(width: 13),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(top: 9),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// The strikethrough is the whole point of the tick: the
|
|
// line stays in place so the day still reads as a record
|
|
// of what was asked, not just what is left.
|
|
AnimatedDefaultTextStyle(
|
|
duration: const Duration(milliseconds: 160),
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontFamily: struck ? "GroundedRegular" : "GroundedMedium",
|
|
color: struck ? colorGrey2 : colorPrimaryDark,
|
|
decoration: struck
|
|
? TextDecoration.lineThrough
|
|
: TextDecoration.none,
|
|
decorationColor: colorGrey2,
|
|
decorationThickness: 1.5,
|
|
height: 1.35,
|
|
),
|
|
child: Text(
|
|
title,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
if (detail != null) ...[
|
|
const SizedBox(height: 6),
|
|
DefaultTextStyle.merge(
|
|
style: TextStyle(
|
|
decoration: TextDecoration.none,
|
|
color: colorGrey2,
|
|
),
|
|
child: detail!,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (trailing != null) ...[
|
|
const SizedBox(width: 10),
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8),
|
|
child: trailing!,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// "Add new task" — the centred, low-key affordance beneath a day's list.
|
|
Widget addTaskAffordance(
|
|
VoidCallback onTap, {
|
|
String label = "Add new task",
|
|
bool enabled = true,
|
|
}) {
|
|
final Color tone = enabled ? colorPositive : colorGrey;
|
|
|
|
return GestureDetector(
|
|
onTap: enabled ? onTap : null,
|
|
behavior: HitTestBehavior.opaque,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 18),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 46,
|
|
height: 46,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: tone.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: tone.withValues(alpha: 0.25), width: 1),
|
|
),
|
|
child: Icon(Icons.add_rounded, size: 22, color: tone),
|
|
),
|
|
const SizedBox(height: 9),
|
|
text(label, 12, TextType.Regular, color: colorGrey2),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|