Rebuild Home around a forest header with streak, score and water

Home now opens on a deep green header: greeting, a Today / Weekly toggle,
a seven-day streak with a leaf on every kept day, minutes held with a +
ring, plan thumbnails, the report-card letter as the score, and one bar per
commitment class (weekly view swaps in per-day bars and class rates). The
+ opens an "Add to today" sheet over the blurred day. A water tracker sits
below the header, kept on the device. The nav marks the selected tab as a
filled block and drops the side action button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
This commit is contained in:
alvocool
2026-11-02 20:58:41 +03:00
parent 9c858b8146
commit 4548e1f164
11 changed files with 1076 additions and 290 deletions

View File

@@ -15,7 +15,7 @@ spark on night-green for the primary control.
| `GlassSurface` / `glassCircle` | Frosted panels and square controls; `onDark` over a scene or tinted stage. |
| `SceneArt` | Painted sky, sun and ridges with an optional figure glyph. Palettes come from `scenePalette(kind)`, `goalScene(accent)`, `classScene(class)`; seed with `sceneSeed(title)` so an item always wears the same picture. |
| `DotDigits` | The dotted clock in the routine player. |
| `FloatingNav` | Home's tab bar and action button. |
| `FloatingNav` | Home's tab bar; the selected tab is a filled night block. The side action button is optional. |
| `GroundedMark` | The logo, drawn — a lime sun on a ground line under two arcs. The launcher PNGs in `assets/icons/` were rendered from the same geometry. |
Cards are image-led where an item has a kind (routines, goals, tasks): scene on
@@ -78,3 +78,15 @@ square top edge → `sectionHeader` (label + light display title) → content.
- Chips carry state, never actions.
- One primary button per screen; everything else is outlined or text.
- Empty states are plain. An empty overdue queue is good news, not a party.
## Home
Home opens on a deep forest header block (`colorHeaderTop` → `colorHeaderBottom`):
greeting and avatar, a Today / Weekly toggle, a seven-day streak panel ending
today (a leaf on every kept day, kept = 80%+), then the numbers. Today shows
minutes held against planned, a ring with + that opens the "Add to today"
sheet, the plan as thumbnails, the report-card letter as the Score, and one
bar per commitment class. Weekly swaps in the week's average, a bar per day
and the class completion rates. Below the block, on the light ambient
surface: the water tracker (drops of 250 ml, kept on the device), then the
rest of the day. Bars on the dark block use the `colorBar*` tokens.

View File

@@ -7,22 +7,23 @@ import 'text/Text.dart';
typedef NavEntry = ({IconData icon, String label, VoidCallback onTap});
/// The floating glass tab bar with the dark round action button beside it.
/// The floating tab bar: the selected tab is a filled night block, the rest
/// sit quietly on white. An optional dark action button can sit beside it.
class FloatingNav extends StatelessWidget {
final List<NavEntry> entries;
final int selected;
final IconData actionIcon;
final IconData? actionIcon;
final VoidCallback onAction;
final VoidCallback? onAction;
const FloatingNav({
super.key,
required this.entries,
required this.selected,
required this.actionIcon,
required this.onAction,
this.actionIcon,
this.onAction,
});
@override
@@ -58,6 +59,7 @@ class FloatingNav extends StatelessWidget {
),
),
),
if (onAction != null) ...[
const SizedBox(width: 10),
GestureDetector(
onTap: onAction,
@@ -81,6 +83,7 @@ class FloatingNav extends StatelessWidget {
child: Icon(actionIcon, size: 24, color: colorLime),
),
),
],
],
),
),
@@ -94,9 +97,9 @@ class FloatingNav extends StatelessWidget {
child: AnimatedContainer(
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
padding: const EdgeInsets.symmetric(vertical: 9),
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: active ? colorWhite : colorWhite.withValues(alpha: 0.0),
color: active ? colorHeaderBottom : colorWhite.withValues(alpha: 0.0),
borderRadius: BorderRadius.zero,
boxShadow: active
? const [
@@ -112,10 +115,10 @@ class FloatingNav extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Icon(entry.icon,
size: 23, color: active ? colorInk : colorInkSoft),
size: 23, color: active ? colorWhite : colorInkSoft),
const SizedBox(height: 4),
text(entry.label, 10, active ? TextType.Medium : TextType.Regular,
color: active ? colorInk : colorInkSoft, maxLines: 1),
color: active ? colorWhite : colorInkSoft, maxLines: 1),
],
),
),

View File

@@ -85,10 +85,12 @@ Widget inputField(
Widget quickAddField(
TextEditingController controller,
String hint,
ValueChanged<String> onSubmitted,
) {
ValueChanged<String> onSubmitted, {
FocusNode? focusNode,
}) {
return TextField(
controller: controller,
focusNode: focusNode,
onSubmitted: onSubmitted,
textInputAction: TextInputAction.done,
style: TextStyle(

View File

@@ -120,6 +120,12 @@ class AppDataManager implements DataManager {
@override
Future setAmnestySpent(int spent) => memory.setAmnestySpent(spent);
@override
Future<int> getWaterToday() => memory.getWaterToday();
@override
Future setWaterToday(int ml) => memory.setWaterToday(ml);
@override
Future<List<TodoItem>> getTodos() => memory.getTodos();

View File

@@ -60,6 +60,11 @@ abstract class ConnectInternalMemory {
Future setAmnestySpent(int spent);
/// Water drunk today in ml; a new day reads as 0.
Future<int> getWaterToday();
Future setWaterToday(int ml);
/// The quick to-do list on Home.
Future<List<TodoItem>> getTodos();

View File

@@ -41,6 +41,8 @@ class InternalMemory implements ConnectInternalMemory {
static const String AMNESTY_SPENT = "AMNESTY_SPENT_Jr4xVh9KmT3gLc6B";
static const String WATER = "WATER_Qe7tNd4ZsM2kBv9C";
static const String TODOS = "TODOS_Wm3kRb8TqZ5nHc2F";
static const String SHOW_ONBOARDING = "SHOW_ONBOARDING_Nz6qGf2SdX7wPb4M";
@@ -255,6 +257,31 @@ class InternalMemory implements ConnectInternalMemory {
await groundedStorage.write(key: AMNESTY_SPENT, value: spent.toString());
}
String _today() {
final DateTime now = DateTime.now();
return "${now.year}-${now.month}-${now.day}";
}
@override
Future<int> getWaterToday() async {
String? value = await groundedStorage.read(key: WATER);
if (value != null) {
final Map<String, dynamic> entry = jsonDecode(value);
if (entry['day'] == _today()) {
return entry['ml'] ?? 0;
}
}
return 0;
}
@override
Future setWaterToday(int ml) async {
await groundedStorage.write(
key: WATER, value: jsonEncode({'day': _today(), 'ml': ml}));
}
@override
Future<List<TodoItem>> getTodos() async {
String? value = await groundedStorage.read(key: TODOS);

View File

@@ -19,6 +19,7 @@ class MockMemory extends InternalMemory {
setMyDescription(
MeDescription(id: "mock-user", name: "Alex", token: "mock-token"));
setUserDetails(UserDetails(pic: "", name: "Alex"));
setWaterToday(1250);
setTodos(<TodoItem>[
TodoItem(id: "t1", title: "Refill water bottle"),
TodoItem(id: "t2", title: "Book physio for Thursday"),

View File

@@ -1,5 +1,6 @@
import '../../about/external/data/Commitment.dart';
import '../../about/external/data/ExcuseCluster.dart';
import '../../about/external/data/ReportCard.dart';
import '../../about/external/data/RoutineChain.dart';
import '../../about/internal/application/Standing.dart';
import '../../about/internal/application/TodoItem.dart';
@@ -15,6 +16,13 @@ abstract class ConnectHome {
/// The routine library, for the up-next rail.
void onRoutinesLoaded(List<RoutineChain> routines);
/// This week's report card, for the streak row, the score and the weekly
/// view. Null when it could not be loaded.
void onWeekLoaded(ReportCard? week);
/// Water drunk today, in ml.
void onWaterLoaded(int ml);
/// The quick to-do list, read from the device.
void onTodosLoaded(List<TodoItem> todos);

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import '../../about/external/data/Commitment.dart';
import '../../about/external/data/ExcuseCluster.dart';
import '../../about/external/data/ReportCard.dart';
import '../../about/external/data/RoutineChain.dart';
import '../../about/external/data/pages/request/CommitmentsRequest.dart';
import '../../about/external/data/pages/request/HistoryRequest.dart';
@@ -34,9 +35,40 @@ class ViewHome extends ParentViewModel {
loadTodos();
loadWater();
loadWeek();
loadPlan();
}
/// The last seven days. Quiet on failure: the header falls back to today.
void loadWeek() async {
try {
final DateTime now = DateTime.now();
final DateTime start = DateTime(now.year, now.month, now.day)
.subtract(const Duration(days: 6));
final response =
await getDataManager().getWeeklyReportCard(ReportCardRequest(
periodStart: start.toIso8601String(),
periodEnd: now.toIso8601String(),
));
connection.onWeekLoaded(ReportCard.fromJson(response.data));
} catch (e) {
connection.onWeekLoaded(null);
}
}
void loadWater() async {
connection.onWaterLoaded(await getDataManager().getWaterToday());
}
void saveWater(int ml) async {
await getDataManager().setWaterToday(ml);
}
void loadTodos() async {
connection.onTodosLoaded(await getDataManager().getTodos());
}

View File

@@ -127,6 +127,21 @@ const Color colorOverdueWarm = Color(0xFFF9E1D8);
const Color colorOverdueWarmEnd = Color(0xFFFFF6EF);
const Color colorOverdueGlow = Color(0xFFD7654B);
// ── Home header ───────────────────────────────────────────────────────────────
// The deep forest block the day sits in, with brighter bars that read on it.
const Color colorHeaderTop = Color(0xFF203D30);
const Color colorHeaderBottom = Color(0xFF15281F);
const Color colorHeaderPanel = Color(0x14FFFFFF);
const Color colorHeaderLine = Color(0x26FFFFFF);
const Color colorHeaderMuted = Color(0x99FFFFFF);
const Color colorHeaderSelected = Color(0xFF3D6B53);
const Color colorBarNonNegotiable = Color(0xFFE8735A);
const Color colorBarStandard = Color(0xFFF2C14E);
const Color colorBarElective = Color(0xFF6FD39B);
// Water: filled drops and the empty ones.
const Color colorWater = Color(0xFF6E8EF0);
const Color colorWaterEmpty = Color(0xFFDDE1DE);
// ── Scene art ─────────────────────────────────────────────────────────────────
// The painted landscapes that stand in for photography on cards. Each kind has
// a sky (top → horizon), a sun, three ridges (far → near) and an ember accent.