Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,9 @@ class _ContentState extends State<_Content> {
left: NhamSpacing.sp3,
right: NhamSpacing.sp3,
top: NhamSpacing.sp3,
// pb-24 (96px) to clear the floating FAB.
bottom: bottomInset + 96,
// Clear the FAB's resting footprint (44 + 20 bottom) with a small
// gap — no more than that, so the scroll doesn't end in dead space.
bottom: bottomInset + 76,
),
children: [
// SECTION 1 — week strip + the paged day-viewer (greeting now lives
Expand Down Expand Up @@ -271,8 +272,10 @@ class _ContentState extends State<_Content> {
WeightChart(todayDate: widget.todayDate, args: widget.args),
],
),
// SECTION 3 — Consistency.
// SECTION 3 — Consistency. Last section: no trailing margin, so the
// scroll ends right under the heatmap instead of a dead gap.
_Section(
last: true,
children: [
SectionHeader(
title: tr('dashboard.consistency'),
Expand Down Expand Up @@ -441,7 +444,7 @@ class _DashboardSkeleton extends StatelessWidget {
left: NhamSpacing.sp3,
right: NhamSpacing.sp3,
top: NhamSpacing.sp3,
bottom: bottomInset + 96,
bottom: bottomInset + 76,
),
children: const [
// Week-strip row — four day pills.
Expand Down Expand Up @@ -478,15 +481,17 @@ class _DashboardSkeleton extends StatelessWidget {
}
}

/// A dashboard section: `mb-4 gap-1.5` (16px bottom margin, 6px inner gap).
/// A dashboard section: `mb-4 gap-1.5` (16px bottom margin, 6px inner gap). The
/// final section passes [last] to drop the bottom margin.
class _Section extends StatelessWidget {
const _Section({required this.children});
const _Section({required this.children, this.last = false});
final List<Widget> children;
final bool last;

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: NhamSpacing.sp4),
padding: EdgeInsets.only(bottom: last ? 0 : NhamSpacing.sp4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';

import '../../../data/api_client.dart';
import '../../../shared/widgets/widgets.dart';
Expand Down Expand Up @@ -113,6 +112,7 @@ class _CompactWeightLogState extends ConsumerState<CompactWeightLog> {
Future<void> _onSubmit() async {
final weightKg = parseDecimalInput(_controller.text);
if (weightKg.isNaN || weightKg < _weightMin || weightKg > _weightMax) {
HapticFeedback.heavyImpact(); // warn: rejected input
setState(() => _validationError = tr('dashboard.weightCard.invalid'));
_showFeedback(
_Feedback(_FeedbackKind.error, tr('dashboard.weightCard.invalidValue')),
Expand Down Expand Up @@ -172,84 +172,66 @@ class _CompactWeightLogState extends ConsumerState<CompactWeightLog> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Always "Today's weight" — this field only ever logs today. No leading
// icon: the label alone carries the meaning and reads cleaner.
Padding(
padding: const EdgeInsets.only(bottom: NhamSpacing.sp2),
child: Row(
children: [
const Icon(LucideIcons.scale, size: 16, color: kInkSecondary),
const SizedBox(width: NhamSpacing.sp2),
Text(
(_hasTodayWeight
? tr('dashboard.weightCard.todaysWeight')
: tr('dashboard.weightCard.logWeight'))
.toUpperCase(),
style: dashEyebrow(),
),
],
child: Text(
tr('dashboard.weightCard.todaysWeight').toUpperCase(),
style: dashEyebrow(),
),
),
// IntrinsicHeight + stretch so the field's filled area and the button
// are the exact same height. (A fixed height on the field alone doesn't
// work: isDense sizes the InputDecorator fill to its content and centers
// it, leaving transparent gaps — so the cream fill read shorter than the
// solid button.)
IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: TextField(
controller: _controller,
onChanged: _onChanged,
enabled: !_pending,
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9.,]')),
],
autocorrect: false,
cursorColor: NhamColors.accent,
// Substantial value type — a number entry reads as data, not
// body copy.
style: dashValue(color: kInk),
decoration: InputDecoration(
isDense: true,
filled: true,
// Soft warm fill instead of a hairline outline — the field
// reads as a tappable surface, the way native mobile inputs
// do, and the border only appears on focus / error.
fillColor: hasError
? NhamColors.danger.withValues(alpha: 0.06)
: kFieldFill,
// Vertical padding sets the field's intrinsic height (≈ the
// control height); the button stretches to match it.
contentPadding: const EdgeInsets.symmetric(
horizontal: NhamSpacing.sp4,
vertical: 15,
),
// Suffix in-flow (no Positioned overlay → no overlap).
suffixText: tr('dashboard.units.kg'),
suffixStyle: dashMeta(color: kInkSecondary),
border: _border(Colors.transparent),
enabledBorder: _border(
hasError ? NhamColors.danger : Colors.transparent),
focusedBorder: _border(
hasError ? NhamColors.danger : NhamColors.accent),
disabledBorder: _border(Colors.transparent),
),
),
),
const SizedBox(width: NhamSpacing.sp2),
_SubmitButton(
label: submitLabel,
pending: _pending,
pressed: _pressed,
onTapDown: () => setState(() => _pressed = true),
onTapUp: () => setState(() => _pressed = false),
onTapCancel: () => setState(() => _pressed = false),
onTap: _pending ? null : _onSubmit,
),
],
// Field on its own row, full width.
TextField(
controller: _controller,
onChanged: _onChanged,
enabled: !_pending,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9.,]')),
],
autocorrect: false,
cursorColor: NhamColors.accent,
// Substantial value type — a number entry reads as data, not body
// copy.
style: dashValue(color: kInk),
decoration: InputDecoration(
isDense: true,
filled: true,
// Soft warm fill instead of a hairline outline — the field reads as
// a tappable surface, the way native mobile inputs do, and the
// border only appears on focus / error.
fillColor: hasError
? NhamColors.danger.withValues(alpha: 0.06)
: kFieldFill,
contentPadding: const EdgeInsets.symmetric(
horizontal: NhamSpacing.sp4,
vertical: 15,
),
// Suffix in-flow (no Positioned overlay → no overlap).
suffixText: tr('dashboard.units.kg'),
suffixStyle: dashMeta(color: kInkSecondary),
border: _border(Colors.transparent),
enabledBorder:
_border(hasError ? NhamColors.danger : Colors.transparent),
focusedBorder:
_border(hasError ? NhamColors.danger : NhamColors.accent),
disabledBorder: _border(Colors.transparent),
),
),
const SizedBox(height: NhamSpacing.sp2),
// Submit button beneath the field, full width — a clearer, more
// thumb-friendly target than a cramped side-by-side button.
SizedBox(
width: double.infinity,
child: _SubmitButton(
label: submitLabel,
pending: _pending,
pressed: _pressed,
onTapDown: () => setState(() => _pressed = true),
onTapUp: () => setState(() => _pressed = false),
onTapCancel: () => setState(() => _pressed = false),
onTap: _pending ? null : _onSubmit,
),
),
if (_feedback != null)
Expand Down Expand Up @@ -322,8 +304,11 @@ class _SubmitButton extends StatelessWidget {
child: Opacity(
opacity: pending ? 0.55 : 1,
child: Container(
// No fixed height — the parent Row stretches it to the field's height.
padding: const EdgeInsets.symmetric(horizontal: NhamSpacing.sp5),
// Stands on its own row now — own comfortable vertical height.
padding: const EdgeInsets.symmetric(
horizontal: NhamSpacing.sp5,
vertical: NhamSpacing.sp3,
),
alignment: Alignment.center,
decoration: BoxDecoration(
color: pressed && !pending ? NhamColors.btnHover : NhamColors.btn,
Expand Down
Loading
Loading