Files
GroundedHelper/frontend/lib/Grounded/designs/Checkbox.dart
alvocool d3431ca5de Switch to Inter, round the corners softly and make the nav switch tabs
Inter replaces General Sans (Inter Display for titles and big numbers).
Corners come back from before the squaring commit, scaled to a soft set
(16 cards, 14 buttons, 12 rows, 8 chips) and applied to the screens built
since. The nav is now a cluster of round frosted buttons that switches
Today, Routines, Goals, Groups and Progress in place inside Home: no
pushed pages, no back arrows, and Android back returns to Today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
2026-11-09 07:08:46 +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.circular(6),
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(12),
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),
],
),
),
);
}