From a23af0658ab322413a98c5e6327eb57eaea2f2c4 Mon Sep 17 00:00:00 2001 From: bvandeusen Date: Mon, 6 Apr 2026 06:40:06 -0400 Subject: [PATCH] docs: add Android nav restructure implementation plan --- .../2026-04-06-android-nav-restructure.md | 521 ++++++++++++++++++ 1 file changed, 521 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-06-android-nav-restructure.md diff --git a/docs/superpowers/plans/2026-04-06-android-nav-restructure.md b/docs/superpowers/plans/2026-04-06-android-nav-restructure.md new file mode 100644 index 0000000..4b207e9 --- /dev/null +++ b/docs/superpowers/plans/2026-04-06-android-nav-restructure.md @@ -0,0 +1,521 @@ +# Android Nav Restructure & Projects Staleness Fix Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the Projects bottom-nav tab with a "More" bottom sheet giving access to Projects, News, and Calendar; fix stale task data when returning from task edit. + +**Architecture:** The shell's `_tabs` list shrinks from 4 to 3 entries; the 4th nav destination ("More") is intercepted in `onDestinationSelected` to show a `showModalBottomSheet` instead of navigating. Projects moves from a ShellRoute to a top-level push route. News and Calendar are added as stub push-routes. `_tabIndex` maps the three overflow paths to index 3 so "More" highlights correctly. The staleness fix moves the task-tap callback out of the stateless `_TaskRow` widget into the parent `ConsumerState` where `ref` is available, using `.then()` to invalidate after pop. + +**Tech Stack:** Flutter, GoRouter, Riverpod, Material 3 + +--- + +## Files + +| File | Action | What changes | +|------|--------|-------------| +| `lib/core/constants.dart` | Modify | Add `news` and `calendar` route constants | +| `lib/app.dart` | Modify | Remove Projects from ShellRoute, add 3 push routes, shrink `_tabs`, add `_showMoreSheet`, update `_tabIndex`, update both nav widgets | +| `lib/screens/library/project_tasks_screen.dart` | Modify | Add `onTap: VoidCallback` to `_TaskRow`; add `_openTask` method to state; pass callback at both call sites | + +--- + +### Task 1: Add route constants + +**Files:** +- Modify: `lib/core/constants.dart` + +- [ ] **Step 1: Add `news` and `calendar` to `Routes`** + +Open `lib/core/constants.dart`. The file currently ends with: +```dart +abstract class Routes { + static const splash = '/'; + static const setup = '/setup'; + static const login = '/login'; + static const notes = '/notes'; + static const noteDetail = '/notes/:id'; + static const noteEdit = '/notes/:id/edit'; + static const noteNew = '/notes/new'; + static const tasks = '/tasks'; + static const taskNew = '/tasks/new'; + static const taskEdit = '/tasks/:id/edit'; + static const knowledge = '/knowledge'; + static const projects = '/projects'; + static const projectEdit = '/projects/:id/edit'; + static const conversations = '/chat'; + static const chat = '/chat/:id'; + static const quickCapture = '/quick-capture'; + static const settings = '/settings'; + static const briefing = '/briefing'; + static const projectTasks = '/projects/:id/tasks'; +} +``` + +Add two constants after `briefing`: +```dart +abstract class Routes { + static const splash = '/'; + static const setup = '/setup'; + static const login = '/login'; + static const notes = '/notes'; + static const noteDetail = '/notes/:id'; + static const noteEdit = '/notes/:id/edit'; + static const noteNew = '/notes/new'; + static const tasks = '/tasks'; + static const taskNew = '/tasks/new'; + static const taskEdit = '/tasks/:id/edit'; + static const knowledge = '/knowledge'; + static const projects = '/projects'; + static const projectEdit = '/projects/:id/edit'; + static const conversations = '/chat'; + static const chat = '/chat/:id'; + static const quickCapture = '/quick-capture'; + static const settings = '/settings'; + static const briefing = '/briefing'; + static const news = '/news'; + static const calendar = '/calendar'; + static const projectTasks = '/projects/:id/tasks'; +} +``` + +- [ ] **Step 2: Verify no analysis errors** + +```bash +cd /home/bvandeusen/Nextcloud/Projects/fabled_app +flutter analyze lib/core/constants.dart +``` +Expected: `No issues found!` + +--- + +### Task 2: Restructure shell and add routes in `app.dart` + +**Files:** +- Modify: `lib/app.dart` + +This task has several sub-steps. Make them all before running analyze. + +- [ ] **Step 1: Move Projects out of ShellRoute, add three push routes** + +Find the `ShellRoute` block (currently lines ~142–162): +```dart + ShellRoute( + builder: (context, state, child) => _Shell(child: child), + routes: [ + GoRoute( + path: Routes.briefing, + builder: (_, _) => const BriefingScreen(), + ), + GoRoute( + path: Routes.knowledge, + builder: (_, _) => const KnowledgeScreen(), + ), + GoRoute( + path: Routes.conversations, + builder: (_, _) => const ConversationsTabScreen(), + ), + GoRoute( + path: Routes.projects, + builder: (_, _) => const ProjectsScreen(), + ), + ], + ), +``` + +Replace with (Projects removed from shell; three new top-level routes added after the closing `],` of the ShellRoute): +```dart + ShellRoute( + builder: (context, state, child) => _Shell(child: child), + routes: [ + GoRoute( + path: Routes.briefing, + builder: (_, _) => const BriefingScreen(), + ), + GoRoute( + path: Routes.knowledge, + builder: (_, _) => const KnowledgeScreen(), + ), + GoRoute( + path: Routes.conversations, + builder: (_, _) => const ConversationsTabScreen(), + ), + ], + ), + GoRoute( + path: Routes.projects, + builder: (_, _) => const ProjectsScreen(), + ), + GoRoute( + path: Routes.news, + builder: (_, _) => Scaffold( + appBar: AppBar(title: const Text('News')), + body: const Center(child: Text('News — coming soon')), + ), + ), + GoRoute( + path: Routes.calendar, + builder: (_, _) => Scaffold( + appBar: AppBar(title: const Text('Calendar')), + body: const Center(child: Text('Calendar — coming soon')), + ), + ), +``` + +- [ ] **Step 2: Shrink `_tabs` from 4 to 3 entries** + +Find in `_ShellState`: +```dart + static const _tabs = [ + Routes.briefing, + Routes.knowledge, + Routes.conversations, + Routes.projects, + ]; +``` + +Replace with: +```dart + static const _tabs = [ + Routes.briefing, + Routes.knowledge, + Routes.conversations, + ]; +``` + +- [ ] **Step 3: Update `_tabIndex` to map overflow routes to index 3** + +Find: +```dart + int _tabIndex(String location) { + for (var i = 0; i < _tabs.length; i++) { + if (location.startsWith(_tabs[i])) return i; + } + return 0; + } +``` + +Replace with: +```dart + int _tabIndex(String location) { + for (var i = 0; i < _tabs.length; i++) { + if (location.startsWith(_tabs[i])) return i; + } + if (location.startsWith(Routes.projects) || + location.startsWith(Routes.news) || + location.startsWith(Routes.calendar)) return 3; + return 0; + } +``` + +- [ ] **Step 4: Add `_showMoreSheet` method to `_ShellState`** + +Add this method anywhere in `_ShellState`, e.g. just before `build`: +```dart + void _showMoreSheet(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (_) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.folder_outlined), + title: const Text('Projects'), + onTap: () { + Navigator.pop(context); + context.push(Routes.projects); + }, + ), + ListTile( + leading: const Icon(Icons.newspaper_outlined), + title: const Text('News'), + onTap: () { + Navigator.pop(context); + context.push(Routes.news); + }, + ), + ListTile( + leading: const Icon(Icons.calendar_month_outlined), + title: const Text('Calendar'), + onTap: () { + Navigator.pop(context); + context.push(Routes.calendar); + }, + ), + ], + ), + ), + ); + } +``` + +- [ ] **Step 5: Update `NavigationRail` — intercept index 3 and swap destination** + +Find in the wide-layout branch: +```dart + NavigationRail( + selectedIndex: index, + onDestinationSelected: (i) => context.go(_tabs[i]), + labelType: NavigationRailLabelType.all, + destinations: const [ + NavigationRailDestination( + icon: Icon(Icons.wb_sunny_outlined), + selectedIcon: Icon(Icons.wb_sunny), + label: Text('Briefing'), + ), + NavigationRailDestination( + icon: Icon(Icons.menu_book_outlined), + selectedIcon: Icon(Icons.menu_book), + label: Text('Knowledge'), + ), + NavigationRailDestination( + icon: Icon(Icons.chat_bubble_outline), + selectedIcon: Icon(Icons.chat_bubble), + label: Text('Chat'), + ), + NavigationRailDestination( + icon: Icon(Icons.folder_outlined), + selectedIcon: Icon(Icons.folder), + label: Text('Projects'), + ), + ], + ), +``` + +Replace with: +```dart + NavigationRail( + selectedIndex: index, + onDestinationSelected: (i) { + if (i == 3) { + _showMoreSheet(context); + } else { + context.go(_tabs[i]); + } + }, + labelType: NavigationRailLabelType.all, + destinations: const [ + NavigationRailDestination( + icon: Icon(Icons.wb_sunny_outlined), + selectedIcon: Icon(Icons.wb_sunny), + label: Text('Briefing'), + ), + NavigationRailDestination( + icon: Icon(Icons.menu_book_outlined), + selectedIcon: Icon(Icons.menu_book), + label: Text('Knowledge'), + ), + NavigationRailDestination( + icon: Icon(Icons.chat_bubble_outline), + selectedIcon: Icon(Icons.chat_bubble), + label: Text('Chat'), + ), + NavigationRailDestination( + icon: Icon(Icons.more_horiz_outlined), + selectedIcon: Icon(Icons.more_horiz), + label: Text('More'), + ), + ], + ), +``` + +- [ ] **Step 6: Update `NavigationBar` — intercept index 3 and swap destination** + +Find in the narrow-layout branch: +```dart + bottomNavigationBar: NavigationBar( + selectedIndex: index, + onDestinationSelected: (i) => context.go(_tabs[i]), + destinations: const [ + NavigationDestination( + icon: Icon(Icons.wb_sunny_outlined), + selectedIcon: Icon(Icons.wb_sunny), + label: 'Briefing', + ), + NavigationDestination( + icon: Icon(Icons.menu_book_outlined), + selectedIcon: Icon(Icons.menu_book), + label: 'Knowledge', + ), + NavigationDestination( + icon: Icon(Icons.chat_bubble_outline), + selectedIcon: Icon(Icons.chat_bubble), + label: 'Chat', + ), + NavigationDestination( + icon: Icon(Icons.folder_outlined), + selectedIcon: Icon(Icons.folder), + label: 'Projects', + ), + ], + ), +``` + +Replace with: +```dart + bottomNavigationBar: NavigationBar( + selectedIndex: index, + onDestinationSelected: (i) { + if (i == 3) { + _showMoreSheet(context); + } else { + context.go(_tabs[i]); + } + }, + destinations: const [ + NavigationDestination( + icon: Icon(Icons.wb_sunny_outlined), + selectedIcon: Icon(Icons.wb_sunny), + label: 'Briefing', + ), + NavigationDestination( + icon: Icon(Icons.menu_book_outlined), + selectedIcon: Icon(Icons.menu_book), + label: 'Knowledge', + ), + NavigationDestination( + icon: Icon(Icons.chat_bubble_outline), + selectedIcon: Icon(Icons.chat_bubble), + label: 'Chat', + ), + NavigationDestination( + icon: Icon(Icons.more_horiz_outlined), + selectedIcon: Icon(Icons.more_horiz), + label: 'More', + ), + ], + ), +``` + +- [ ] **Step 7: Verify no analysis errors** + +```bash +cd /home/bvandeusen/Nextcloud/Projects/fabled_app +flutter analyze lib/app.dart lib/core/constants.dart +``` +Expected: `No issues found!` + +- [ ] **Step 8: Commit** + +```bash +git add lib/core/constants.dart lib/app.dart +git commit -m "feat: replace Projects tab with More bottom sheet (news/calendar stubs)" +``` + +--- + +### Task 3: Fix stale tasks after returning from task edit + +**Files:** +- Modify: `lib/screens/library/project_tasks_screen.dart` + +- [ ] **Step 1: Add `_openTask` method to `_ProjectTasksScreenState`** + +`_ProjectTasksScreenState` is a `ConsumerState` — it has `ref` and `context`. Find the class body (look for `_cycleStatus` method as a landmark) and add `_openTask` as a sibling method: + +```dart + void _openTask(int taskId) { + context + .push(Routes.taskEdit.replaceFirst(':id', '$taskId')) + .then((_) => ref.invalidate(projectTasksProvider(widget.projectId))); + } +``` + +- [ ] **Step 2: Add `onTap` parameter to `_TaskRow`** + +Find the `_TaskRow` class definition: +```dart +class _TaskRow extends StatelessWidget { + final Task task; + final TaskStatus effectiveStatus; + final VoidCallback onStatusTap; + + const _TaskRow({ + required this.task, + required this.effectiveStatus, + required this.onStatusTap, + }); +``` + +Replace with: +```dart +class _TaskRow extends StatelessWidget { + final Task task; + final TaskStatus effectiveStatus; + final VoidCallback onStatusTap; + final VoidCallback onTap; + + const _TaskRow({ + required this.task, + required this.effectiveStatus, + required this.onStatusTap, + required this.onTap, + }); +``` + +- [ ] **Step 3: Use the `onTap` callback in `InkWell`** + +Find inside `_TaskRow.build`: +```dart + child: InkWell( + onTap: () => context + .push(Routes.taskEdit.replaceFirst(':id', '${task.id}')), +``` + +Replace with: +```dart + child: InkWell( + onTap: onTap, +``` + +- [ ] **Step 4: Pass `onTap` at both `_TaskRow` call sites** + +There are two places in `_buildBody` where `_TaskRow` is instantiated. Update both: + +**First call site (milestone tasks, around line 170):** +```dart + return _TaskRow( + task: task, + effectiveStatus: _effectiveStatus(task), + onStatusTap: () => _cycleStatus(task), + onTap: () => _openTask(task.id), + ); +``` + +**Second call site (unassigned tasks, around line 197):** +```dart + return _TaskRow( + task: task, + effectiveStatus: _effectiveStatus(task), + onStatusTap: () => _cycleStatus(task), + onTap: () => _openTask(task.id), + ); +``` + +- [ ] **Step 5: Verify no analysis errors** + +```bash +cd /home/bvandeusen/Nextcloud/Projects/fabled_app +flutter analyze lib/screens/library/project_tasks_screen.dart +``` +Expected: `No issues found!` + +- [ ] **Step 6: Commit** + +```bash +git add lib/screens/library/project_tasks_screen.dart +git commit -m "fix: invalidate project tasks on return from task edit screen" +``` + +--- + +### Task 4: Final check + +- [ ] **Step 1: Full analyze** + +```bash +cd /home/bvandeusen/Nextcloud/Projects/fabled_app +flutter analyze +``` +Expected: `No issues found!`