feat: replace linear calorie bar with goal-aware ring widget

The home screen CaloriesCard now uses a circular ring (CustomPainter)
instead of a LinearProgressIndicator. Ring colour is determined by the
user's goal type (lose / maintain / gain) with inverted semantics for
the gain goal — red means undereating, not overeating.

Overflow beyond 100% is shown as a thinner second-lap arc in the same
warning colour. Numbers (logged kcal, goal, remaining/overage) are
displayed both inside the ring and in a stat column to the right.

Adds docs/calorie_ring_color_spec.md with the full colour threshold
specification per goal type.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-03-17 12:34:30 +02:00
parent ddbc8e2bc0
commit 2a95bcd53c
2 changed files with 324 additions and 36 deletions

View File

@@ -0,0 +1,104 @@
# Calorie Ring — Colour Logic Specification
## Overview
The home screen displays daily calorie progress as a circular ring. The ring colour conveys
how the user is tracking relative to their calorie goal. Because the meaning of "over goal"
differs by goal type, each goal type has its own colour thresholds.
All colours are from the iOS system palette already defined in `AppColors`.
---
## Goal types
| Value | Russian | Semantic |
|---|---|---|
| `lose` | Похудение | Daily calories are a **ceiling** — eating less is success, eating over is a warning |
| `maintain` | Поддержание | Daily calories are a **target** — closeness in either direction is success |
| `gain` | Набор массы | Daily calories are a **floor** — eating enough (or more) is success, eating too little is a warning |
---
## Colour thresholds
Progress is defined as `loggedCalories / dailyGoal` (raw, unclamped — can exceed 1.0).
### `lose` — calorie ceiling
| Progress range | Colour | Hex | Message to user |
|---|---|---|---|
| 0% 74% | Blue | `#007AFF` | Well within budget |
| 75% 94% | Green | `#34C759` | Approaching limit — on track |
| 95% 109% | Yellow / Orange | `#FF9500` | Slightly over limit |
| ≥ 110% | Red | `#FF3B30` | Significantly over limit |
**Rationale:** For weight loss, the calorie limit is the primary constraint. Green signals
the user is tracking well towards their limit without excess. Red signals a meaningful
overshoot that threatens the calorie deficit required for weight loss.
---
### `maintain` — calorie target (bidirectional)
| Progress range | Colour | Hex | Message to user |
|---|---|---|---|
| < 70% | Blue | `#007AFF` | Significantly under target |
| 70% 89% | Yellow / Orange | `#FF9500` | Slightly under target |
| 90% 110% | Green | `#34C759` | On target ✓ |
| 111% 124% | Yellow / Orange | `#FF9500` | Slightly over target |
| ≥ 125% | Red | `#FF3B30` | Significantly over target |
**Rationale:** Maintenance is a two-sided target. Green covers a ±10% acceptance band.
Yellow is a soft nudge in either direction. Red only appears at ≥ 125% over (meaningful
calorie surplus that would cause weight gain) or the symmetric < 70% (meaningful deficit
that would cause weight loss).
---
### `gain` — calorie floor (semantics inverted)
| Progress range | Colour | Hex | Message to user |
|---|---|---|---|
| < 60% | Red | `#FF3B30` | Far below minimum — eat more |
| 60% 84% | Yellow / Orange | `#FF9500` | Under target |
| 85% 115% | Green | `#34C759` | At or above target ✓ |
| > 115% | Blue | `#007AFF` | Well above target — informational |
**Rationale:** For muscle gain, not eating enough is the primary failure mode. Red means
"not enough calories for muscle synthesis". Green means the calorie surplus target is met.
Blue (rather than red) is used above 115% because eating extra while bulking is neutral to
positive — it does not warrant an alarming colour.
---
### Unknown / unset goal
When `user.goal` is `null` or an unrecognised value, the ring always shows Blue `#007AFF`
(the app's primary colour) with no semantic judgement applied.
---
## Overflow arc
When `loggedCalories > dailyGoal` (progress > 100%), the ring is visually "full" and a
second overlapping arc is drawn from the start point (top/12 o'clock) clockwise to
`(progress 1.0) × 360°`. This overflow arc is rendered:
- Same colour as the main arc (which has already shifted to warning/error colour)
- `strokeWidth` reduced to 6 px (vs. 10 px for the main arc) for visual distinction
- Opacity 70% to distinguish it from the primary stroke
The text inside the ring switches from "осталось X" to "+X перебор" when in overflow state.
---
## Implementation reference
| Symbol | Location |
|---|---|
| `_ringColorFor(double rawProgress, String? goalType)` | `client/lib/features/home/home_screen.dart` |
| `AppColors.primary` (`#007AFF`) | `client/lib/core/theme/app_colors.dart` |
| `AppColors.success` (`#34C759`) | `client/lib/core/theme/app_colors.dart` |
| `AppColors.warning` (`#FF9500`) | `client/lib/core/theme/app_colors.dart` |
| `AppColors.error` (`#FF3B30`) | `client/lib/core/theme/app_colors.dart` |