Files
GroundedHelper/frontend/lib/Grounded/designs/Checkbox.dart
alvocool 9c858b8146 Square every corner, enlarge the main surfaces and add a to-do list
Rounded rectangles and round controls are now square across the app; only
status dots, glows, the sun, rings and the breathing circle stay round.
Cards, rows, thumbnails, featured cards, category tiles, the standing card
and the nav are larger. Home gets a quick To-do list kept on the device
(TodoItem in InternalMemory), seeded with sample items in mock mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
2026-11-02 14:50:31 +03:00

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