This repository has been archived on 2026-06-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
FabledApp/docs/superpowers/plans/2026-03-11-app-overhaul-p2-navigation.md
T
bvandeusen 6232c7c99a feat: app overhaul — Briefing-first navigation, Library, capture queue
Plans 1-3 implemented:

Plan 1 — Foundation
- Add google_fonts ^6.2.1
- lib/core/theme.dart: slate-indigo ColorScheme (dark/light), Fraunces
  headings, GradientButton widget
- lib/providers/capture_work_queue_provider.dart: in-memory sequential
  work queue; CaptureWorkQueueNotifier drains one item at a time;
  captureResultProvider feeds snackbars to UI
- lib/app.dart: wire fabledDarkTheme/fabledLightTheme; replace blocking
  _QuickCaptureBar with queue-based implementation (progress bar, badge)

Plan 2 — Navigation
- 3-tab shell: Briefing · Library · Chat (was Notes · Tasks · Projects · Chat)
- lib/screens/library/library_screen.dart: unified notes+tasks+projects list
  with filter pills, status sub-filter for tasks, live search, FAB
- lib/widgets/library_item_card.dart: NoteLibraryCard, TaskLibraryCard
  (status cycle), ProjectLibraryCard
- lib/screens/chat/conversations_tab_screen.dart: focused replacement for
  ConversationsListScreen
- Delete 5 dead screens: notes_list, tasks_list, project_list,
  conversations_list, quick_capture

Plan 3 — Briefing
- lib/data/models/briefing_conversation.dart
- lib/data/api/briefing_api.dart: getToday, getHistory, getMessages,
  triggerSlot
- lib/providers/briefing_provider.dart: BriefingNotifier with sendReply
  (optimistic + SSE + poll, same pattern as MessagesNotifier) and refresh
- lib/widgets/chat_message_bubble.dart: extracted + redesigned shared bubble
  (ghost user bubbles, left-accent assistant bubbles)
- lib/widgets/briefing_digest_card.dart: expandable first-message card
- lib/screens/briefing/briefing_screen.dart: digest card, conversation list,
  streaming reply bar, refresh button, history overflow menu
- lib/screens/briefing/briefing_history_screen.dart: read-only past dates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 23:17:38 -04:00

1126 lines
37 KiB
Markdown

# Fabled App Overhaul — Plan 2: Navigation (Shell + Library + Cleanup)
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Prerequisite:** Plan 1 (theme + capture queue) must be complete.
**Goal:** Replace the 4-tab shell (Notes · Tasks · Projects · Chat) with a 3-tab shell (Briefing · Library · Chat), build the unified LibraryScreen with filter pills and inline search, and delete the five screens made redundant by this change.
**Architecture:** `app.dart`'s `_Shell` is rewritten for 3 tabs. A new `LibraryScreen` replaces the three separate list screens with a unified `GET /api/notes` + `GET /api/tasks` feed filtered by pill selection. A `LibraryItemCard` widget renders note, task, and project rows uniformly. The Briefing tab shows a placeholder screen until Plan 3 ships. Dead screens are deleted.
**Tech Stack:** Flutter/Dart, Riverpod, GoRouter, Dio (existing), `GradientButton` from Plan 1 theme.
---
## File Map
**Create:**
- `lib/screens/briefing/briefing_screen.dart` — placeholder (full implementation in Plan 3)
- `lib/screens/library/library_screen.dart` — unified list with filter pills + search
- `lib/widgets/library_item_card.dart` — note / task / project row widget
**Modify:**
- `lib/app.dart` — rewrite `_Shell` for 3 tabs; add `/briefing` and `/library` routes; remove dead routes
- `lib/core/constants.dart` — add `briefing` and `library` route constants
- `lib/providers/api_client_provider.dart` — no changes needed (existing notes/tasks providers reused)
**Delete:**
- `lib/screens/notes/notes_list_screen.dart`
- `lib/screens/tasks/tasks_list_screen.dart`
- `lib/screens/projects/project_list_screen.dart`
- `lib/screens/chat/conversations_list_screen.dart`
- `lib/screens/quick_capture/quick_capture_screen.dart`
---
## Chunk 1: Constants + Placeholder Briefing Screen
### Task 1: Add route constants
**Files:**
- Modify: `lib/core/constants.dart`
- [ ] **Step 1: Read `lib/core/constants.dart`**
Check the existing `Routes` class. It will have constants like `notes`, `tasks`, etc.
- [ ] **Step 2: Add new constants**
Add to the `Routes` class:
```dart
static const briefing = '/briefing';
static const library = '/library';
```
- [ ] **Step 3: Commit**
```bash
git add lib/core/constants.dart
git commit -m "feat: add briefing and library route constants"
```
---
### Task 2: Briefing placeholder screen
**Files:**
- Create: `lib/screens/briefing/briefing_screen.dart`
This is a placeholder — Plan 3 will replace it entirely. It just renders a centred message so the tab is navigable.
- [ ] **Step 1: Create `lib/screens/briefing/briefing_screen.dart`**
```dart
import 'package:flutter/material.dart';
/// Placeholder — full implementation in Plan 3.
class BriefingScreen extends StatelessWidget {
const BriefingScreen({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text('Briefing', style: theme.textTheme.titleLarge),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.wb_sunny_outlined,
size: 48, color: theme.colorScheme.primary),
const SizedBox(height: 16),
Text('Briefing coming soon',
style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
Text('Your daily briefing will appear here.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant)),
],
),
),
);
}
}
```
- [ ] **Step 2: Analyze**
```bash
flutter analyze lib/screens/briefing/briefing_screen.dart
```
Expected: no errors.
- [ ] **Step 3: Commit**
```bash
git add lib/screens/briefing/briefing_screen.dart
git commit -m "feat: briefing placeholder screen"
```
---
## Chunk 2: Library Screen
### Task 3: LibraryItemCard widget
**Files:**
- Create: `lib/widgets/library_item_card.dart`
A unified card that renders a note row, a task row, or a project row depending on item type. Tapping navigates to the appropriate detail screen.
- [ ] **Step 1: Read the existing data models**
Check `lib/data/models/note.dart`, `lib/data/models/task.dart`, `lib/data/models/project.dart` to confirm field names before writing the widget.
- [ ] **Step 2: Create `lib/widgets/library_item_card.dart`**
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import '../core/constants.dart';
import '../data/models/note.dart';
import '../data/models/project.dart';
import '../data/models/task.dart';
import '../providers/tasks_provider.dart';
// ── Note card ────────────────────────────────────────────────────────────────
class NoteLibraryCard extends StatelessWidget {
final Note note;
const NoteLibraryCard({super.key, required this.note});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: InkWell(
onTap: () => context.push(
Routes.noteDetail.replaceFirst(':id', '${note.id}')),
borderRadius: BorderRadius.circular(14),
child: Padding(
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.article_outlined,
size: 15,
color: theme.colorScheme.onSurfaceVariant),
const SizedBox(width: 6),
Expanded(
child: Text(
note.title.isNotEmpty ? note.title : 'Untitled',
style: theme.textTheme.titleSmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Text(
_relativeTime(note.updatedAt),
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant),
),
],
),
if (note.body.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
note.body.replaceAll('\n', ' '),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
if (note.tags.isNotEmpty) ...[
const SizedBox(height: 6),
Wrap(
spacing: 4,
runSpacing: 2,
children: note.tags
.take(4)
.map((t) => Chip(
label: Text(t),
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
))
.toList(),
),
],
],
),
),
),
);
}
}
// ── Task card ────────────────────────────────────────────────────────────────
class TaskLibraryCard extends ConsumerWidget {
final Task task;
const TaskLibraryCard({super.key, required this.task});
Color _priorityColor(BuildContext context, String? priority) {
final cs = Theme.of(context).colorScheme;
return switch (priority) {
'high' => const Color(0xFFEF4444),
'medium' => const Color(0xFFF59E0B),
_ => cs.onSurfaceVariant,
};
}
IconData _statusIcon(String status) => switch (status) {
'done' => Icons.check_circle,
'in_progress' => Icons.timelapse,
_ => Icons.radio_button_unchecked,
};
Color _statusColor(BuildContext context, String status) {
final cs = Theme.of(context).colorScheme;
return switch (status) {
'done' => const Color(0xFF22C55E),
'in_progress' => cs.primary,
_ => cs.onSurfaceVariant,
};
}
String _nextStatus(String current) => switch (current) {
'todo' => 'in_progress',
'in_progress' => 'done',
_ => 'todo',
};
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: InkWell(
onTap: () => context.push(
Routes.taskEdit.replaceFirst(':id', '${task.id}')),
borderRadius: BorderRadius.circular(14),
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 10, 14, 10),
child: Row(
children: [
// Status cycle button
IconButton(
icon: Icon(
_statusIcon(task.status),
color: _statusColor(context, task.status),
),
onPressed: () => ref
.read(tasksProvider.notifier)
.updateStatus(task.id, _nextStatus(task.status)),
tooltip: 'Cycle status',
visualDensity: VisualDensity.compact,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
task.title.isNotEmpty ? task.title : 'Untitled',
style: theme.textTheme.titleSmall?.copyWith(
decoration: task.status == 'done'
? TextDecoration.lineThrough
: null,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (task.dueDate != null) ...[
const SizedBox(height: 2),
Text(
'Due ${DateFormat.MMMd().format(task.dueDate!)}',
style: theme.textTheme.labelSmall?.copyWith(
color: task.dueDate!.isBefore(DateTime.now()) &&
task.status != 'done'
? const Color(0xFFEF4444)
: theme.colorScheme.onSurfaceVariant,
),
),
],
],
),
),
if (task.priority != null && task.priority != 'none')
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: _priorityColor(context, task.priority),
shape: BoxShape.circle,
),
),
],
),
),
),
);
}
}
// ── Project card ─────────────────────────────────────────────────────────────
class ProjectLibraryCard extends StatelessWidget {
final Project project;
const ProjectLibraryCard({super.key, required this.project});
Color _parseColor(String? hex) {
if (hex == null || hex.isEmpty) return const Color(0xFF6366F1);
try {
return Color(int.parse(hex.replaceFirst('#', '0xFF')));
} catch (_) {
return const Color(0xFF6366F1);
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final color = _parseColor(project.color);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () {}, // Projects have no detail screen in this app
borderRadius: BorderRadius.circular(14),
child: Row(
children: [
// Colour strip
Container(width: 6, height: 64, color: color),
const SizedBox(width: 12),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
project.title,
style: theme.textTheme.titleSmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (project.description?.isNotEmpty == true) ...[
const SizedBox(height: 2),
Text(
project.description!,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
),
const SizedBox(width: 12),
],
),
),
);
}
}
// ── Helpers ───────────────────────────────────────────────────────────────────
String _relativeTime(DateTime? dt) {
if (dt == null) return '';
final diff = DateTime.now().difference(dt);
if (diff.inMinutes < 1) return 'just now';
if (diff.inHours < 1) return '${diff.inMinutes}m ago';
if (diff.inDays < 1) return '${diff.inHours}h ago';
if (diff.inDays < 7) return '${diff.inDays}d ago';
return DateFormat.MMMd().format(dt);
}
```
Note: `Routes.noteDetail` and `Routes.taskEdit` use path patterns like `/notes/:id` — check `lib/core/constants.dart` for exact values and adjust `replaceFirst` calls accordingly.
Also note: `tasksProvider.notifier.updateStatus()` — check `lib/providers/tasks_provider.dart` to verify this method exists. If not, add it (it calls `PATCH /api/tasks/:id` with `{status}`).
Also: `intl` package may need adding to `pubspec.yaml` — check if it's already a transitive dependency via `flutter pub deps`. If not, add `intl: ^0.19.0`.
- [ ] **Step 3: Analyze**
```bash
flutter analyze lib/widgets/library_item_card.dart
```
Fix any type errors (common: nullable field access, missing `fromJson` fields on models).
- [ ] **Step 4: Commit**
```bash
git add lib/widgets/library_item_card.dart
git commit -m "feat: LibraryItemCard — note, task, and project row widgets"
```
---
### Task 4: LibraryScreen
**Files:**
- Create: `lib/screens/library/library_screen.dart`
- [ ] **Step 1: Create `lib/screens/library/library_screen.dart`**
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/constants.dart';
import '../../data/models/note.dart';
import '../../data/models/task.dart';
import '../../providers/notes_provider.dart';
import '../../providers/projects_provider.dart';
import '../../providers/tasks_provider.dart';
import '../../widgets/library_item_card.dart';
enum _LibraryFilter { all, notes, tasks, projects }
enum _TaskStatusFilter { all, todo, inProgress, done }
class LibraryScreen extends ConsumerStatefulWidget {
const LibraryScreen({super.key});
@override
ConsumerState<LibraryScreen> createState() => _LibraryScreenState();
}
class _LibraryScreenState extends ConsumerState<LibraryScreen> {
_LibraryFilter _filter = _LibraryFilter.all;
_TaskStatusFilter _taskStatus = _TaskStatusFilter.all;
bool _searchActive = false;
String _searchQuery = '';
final _searchController = TextEditingController();
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
bool _matchesSearch(String text) {
if (_searchQuery.isEmpty) return true;
return text.toLowerCase().contains(_searchQuery.toLowerCase());
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final notesAsync = ref.watch(notesProvider);
final tasksAsync = ref.watch(tasksProvider);
final projectsAsync = ref.watch(projectsProvider);
return Scaffold(
appBar: AppBar(
title: _searchActive
? TextField(
controller: _searchController,
autofocus: true,
decoration: const InputDecoration(
hintText: 'Search…',
border: InputBorder.none,
isDense: true,
),
onChanged: (q) => setState(() => _searchQuery = q),
)
: Text('Library', style: theme.textTheme.titleLarge),
actions: [
IconButton(
icon: Icon(_searchActive ? Icons.close : Icons.search),
onPressed: () => setState(() {
_searchActive = !_searchActive;
if (!_searchActive) {
_searchQuery = '';
_searchController.clear();
}
}),
),
],
),
body: Column(
children: [
// ── Filter pills ──────────────────────────────────────────────────
SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
child: Row(
children: _LibraryFilter.values.map((f) {
final label = switch (f) {
_LibraryFilter.all => 'All',
_LibraryFilter.notes => 'Notes',
_LibraryFilter.tasks => 'Tasks',
_LibraryFilter.projects => 'Projects',
};
final selected = _filter == f;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(label),
selected: selected,
onSelected: (_) => setState(() {
_filter = f;
_taskStatus = _TaskStatusFilter.all;
}),
selectedColor:
theme.colorScheme.primary.withValues(alpha: 0.18),
checkmarkColor: theme.colorScheme.primary,
),
);
}).toList(),
),
),
// ── Task status sub-filter (Tasks pill only) ───────────────────
if (_filter == _LibraryFilter.tasks)
SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.fromLTRB(12, 0, 12, 4),
child: Row(
children: _TaskStatusFilter.values.map((s) {
final label = switch (s) {
_TaskStatusFilter.all => 'All',
_TaskStatusFilter.todo => 'To Do',
_TaskStatusFilter.inProgress => 'In Progress',
_TaskStatusFilter.done => 'Done',
};
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
label: Text(label),
selected: _taskStatus == s,
onSelected: (_) => setState(() => _taskStatus = s),
selectedColor:
theme.colorScheme.secondary.withValues(alpha: 0.15),
),
);
}).toList(),
),
),
const Divider(height: 1),
// ── Content ───────────────────────────────────────────────────────
Expanded(
child: switch (_filter) {
_LibraryFilter.notes => _buildNotesList(notesAsync),
_LibraryFilter.tasks => _buildTasksList(tasksAsync),
_LibraryFilter.projects => _buildProjectsList(projectsAsync),
_LibraryFilter.all => _buildAllList(notesAsync, tasksAsync),
},
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showCreateSheet(context),
child: const Icon(Icons.add),
),
);
}
Widget _buildNotesList(AsyncValue<List<Note>> notesAsync) {
return notesAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error: $e')),
data: (notes) {
final filtered = notes
.where((n) => _matchesSearch(n.title) || _matchesSearch(n.body))
.toList();
if (filtered.isEmpty) {
return const Center(child: Text('No notes found'));
}
return RefreshIndicator(
onRefresh: () async => ref.invalidate(notesProvider),
child: ListView.builder(
itemCount: filtered.length,
itemBuilder: (_, i) => NoteLibraryCard(note: filtered[i]),
),
);
},
);
}
Widget _buildTasksList(AsyncValue<List<Task>> tasksAsync) {
return tasksAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error: $e')),
data: (tasks) {
var filtered = tasks
.where((t) => _matchesSearch(t.title))
.toList();
if (_taskStatus != _TaskStatusFilter.all) {
final status = switch (_taskStatus) {
_TaskStatusFilter.todo => 'todo',
_TaskStatusFilter.inProgress => 'in_progress',
_TaskStatusFilter.done => 'done',
_ => '',
};
filtered = filtered.where((t) => t.status == status).toList();
}
if (filtered.isEmpty) {
return const Center(child: Text('No tasks found'));
}
return RefreshIndicator(
onRefresh: () async => ref.invalidate(tasksProvider),
child: ListView.builder(
itemCount: filtered.length,
itemBuilder: (_, i) => TaskLibraryCard(task: filtered[i]),
),
);
},
);
}
Widget _buildProjectsList(AsyncValue projects) {
return projects.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error: $e')),
data: (data) {
final list = data as List;
if (list.isEmpty) {
return const Center(child: Text('No projects'));
}
return RefreshIndicator(
onRefresh: () async => ref.invalidate(projectsProvider),
child: ListView.builder(
itemCount: list.length,
itemBuilder: (_, i) => ProjectLibraryCard(project: list[i]),
),
);
},
);
}
Widget _buildAllList(
AsyncValue<List<Note>> notesAsync,
AsyncValue<List<Task>> tasksAsync,
) {
final notes = notesAsync.valueOrNull ?? [];
final tasks = tasksAsync.valueOrNull ?? [];
// Merge and sort by updatedAt desc
final items = <(DateTime, Widget)>[];
for (final n in notes) {
if (_matchesSearch(n.title) || _matchesSearch(n.body)) {
items.add((n.updatedAt ?? DateTime(0), NoteLibraryCard(note: n)));
}
}
for (final t in tasks) {
if (_matchesSearch(t.title)) {
items.add((t.updatedAt ?? DateTime(0), TaskLibraryCard(task: t)));
}
}
items.sort((a, b) => b.$1.compareTo(a.$1));
if (notesAsync.isLoading || tasksAsync.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (items.isEmpty) {
return const Center(child: Text('Nothing here yet'));
}
return RefreshIndicator(
onRefresh: () async {
ref.invalidate(notesProvider);
ref.invalidate(tasksProvider);
},
child: ListView.builder(
itemCount: items.length,
itemBuilder: (_, i) => items[i].$2,
),
);
}
void _showCreateSheet(BuildContext context) {
showModalBottomSheet<void>(
context: context,
builder: (_) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.article_outlined),
title: const Text('New note'),
onTap: () {
Navigator.pop(context);
context.push(Routes.noteNew);
},
),
ListTile(
leading: const Icon(Icons.check_box_outlined),
title: const Text('New task'),
onTap: () {
Navigator.pop(context);
context.push(Routes.taskNew);
},
),
],
),
),
);
}
}
```
- [ ] **Step 2: Check model field names**
Read `lib/data/models/note.dart` and `lib/data/models/task.dart`. Verify:
- `Note` has: `id`, `title`, `body`, `tags` (List<String>), `updatedAt` (DateTime?)
- `Task` has: `id`, `title`, `status`, `priority`, `dueDate` (DateTime?), `updatedAt` (DateTime?)
- `Project` has: `id`, `title`, `description`, `color`
Adjust field names in `LibraryScreen` and `LibraryItemCard` if they differ.
- [ ] **Step 3: Verify `tasksProvider.notifier` has `updateStatus()`**
Check `lib/providers/tasks_provider.dart`. If `updateStatus(int id, String status)` doesn't exist, add it:
```dart
Future<void> updateStatus(int id, String status) async {
await ref.read(tasksRepositoryProvider).updateStatus(id, status);
ref.invalidateSelf();
}
```
And add to `lib/data/repositories/tasks_repository.dart`:
```dart
Future<void> updateStatus(int id, String status) async {
await _api.updateStatus(id, status);
}
```
And add to `lib/data/api/tasks_api.dart`:
```dart
Future<void> updateStatus(int id, String status) async {
try {
await _dio.patch('/api/tasks/$id/status', data: {'status': status});
} on DioException catch (e) {
throw dioToApp(e);
}
}
```
- [ ] **Step 4: Analyze**
```bash
flutter analyze lib/screens/library/ lib/widgets/library_item_card.dart
```
Fix any errors.
- [ ] **Step 5: Commit**
```bash
git add lib/screens/library/ lib/widgets/library_item_card.dart
git commit -m "feat: LibraryScreen — unified notes/tasks/projects with filter pills and search"
```
---
## Chunk 3: Shell Rewrite + Dead Code Cleanup
### Task 5: Rewrite the shell and routes
**Files:**
- Modify: `lib/app.dart``_Shell` class, `routerProvider` routes list
- [ ] **Step 1: Update imports in `lib/app.dart`**
Add:
```dart
import 'screens/briefing/briefing_screen.dart';
import 'screens/library/library_screen.dart';
```
Remove imports for deleted screens (they will be deleted next):
```dart
// Remove these:
import 'screens/notes/notes_list_screen.dart';
import 'screens/tasks/tasks_list_screen.dart';
import 'screens/projects/project_list_screen.dart';
import 'screens/chat/conversations_list_screen.dart';
```
- [ ] **Step 2: Update the shell tab list**
Find `static const _tabs = [Routes.notes, Routes.tasks, Routes.projects, Routes.conversations];`
Replace with:
```dart
static const _tabs = [Routes.briefing, Routes.library, Routes.conversations];
```
- [ ] **Step 3: Update NavigationBar destinations**
Replace the 4-destination `NavigationBar` with:
```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.library_books_outlined),
selectedIcon: Icon(Icons.library_books),
label: 'Library',
),
NavigationDestination(
icon: Icon(Icons.chat_bubble_outline),
selectedIcon: Icon(Icons.chat_bubble),
label: 'Chat',
),
],
),
```
- [ ] **Step 4: Update NavigationRail destinations (wide layout)**
Replace the 4-destination `NavigationRail` with:
```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.library_books_outlined),
selectedIcon: Icon(Icons.library_books),
label: Text('Library'),
),
NavigationRailDestination(
icon: Icon(Icons.chat_bubble_outline),
selectedIcon: Icon(Icons.chat_bubble),
label: Text('Chat'),
),
],
),
```
- [ ] **Step 5: Update the ShellRoute in routerProvider**
Find the `ShellRoute` with its child `GoRoute`s. Replace:
```dart
ShellRoute(
builder: (context, state, child) => _Shell(child: child),
routes: [
GoRoute(path: Routes.briefing, builder: (_, _) => const BriefingScreen()),
GoRoute(path: Routes.library, builder: (_, _) => const LibraryScreen()),
GoRoute(path: Routes.conversations, builder: (_, _) => const ConversationsInlineScreen()),
],
),
```
For the Chat tab: the existing `ConversationsListScreen` is being deleted. Replace it with an inline conversations list built directly in the ShellRoute, or create a minimal `lib/screens/chat/conversations_tab_screen.dart` that replaces it. The simplest approach: move the conversations list content inline.
Create `lib/screens/chat/conversations_tab_screen.dart`:
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/constants.dart';
import '../../providers/chat_provider.dart';
class ConversationsTabScreen extends ConsumerWidget {
const ConversationsTabScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final convsAsync = ref.watch(conversationsProvider);
return Scaffold(
appBar: AppBar(
title: Text('Chat', style: theme.textTheme.titleLarge),
actions: [
IconButton(
icon: const Icon(Icons.add),
tooltip: 'New conversation',
onPressed: () async {
final conv = await ref
.read(conversationsProvider.notifier)
.create('New conversation');
if (context.mounted) {
context.push(Routes.chat.replaceFirst(':id', '${conv.id}'));
}
},
),
],
),
body: convsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error: $e')),
data: (convs) {
if (convs.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.chat_bubble_outline,
size: 48,
color: theme.colorScheme.onSurfaceVariant),
const SizedBox(height: 16),
Text('No conversations yet',
style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
FilledButton.icon(
icon: const Icon(Icons.add),
label: const Text('Start a conversation'),
onPressed: () async {
final conv = await ref
.read(conversationsProvider.notifier)
.create('New conversation');
if (context.mounted) {
context.push(
Routes.chat.replaceFirst(':id', '${conv.id}'));
}
},
),
],
),
);
}
return RefreshIndicator(
onRefresh: () async => ref.invalidate(conversationsProvider),
child: ListView.builder(
itemCount: convs.length,
itemBuilder: (ctx, i) {
final c = convs[i];
return ListTile(
leading: const Icon(Icons.chat_bubble_outline),
title: Text(c.title, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: c.updatedAt != null
? Text(
_relativeTime(c.updatedAt!),
style: theme.textTheme.labelSmall,
)
: null,
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _confirmDelete(context, ref, c.id, c.title),
),
onTap: () => ctx.push(
Routes.chat.replaceFirst(':id', '${c.id}')),
);
},
),
);
},
),
);
}
Future<void> _confirmDelete(
BuildContext context, WidgetRef ref, int id, String title) async {
final ok = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: const Text('Delete conversation?'),
content: Text('"$title" will be permanently deleted.'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Delete')),
],
),
);
if (ok == true) {
await ref.read(conversationsProvider.notifier).delete(id);
}
}
}
String _relativeTime(DateTime dt) {
final diff = DateTime.now().difference(dt);
if (diff.inMinutes < 1) return 'just now';
if (diff.inHours < 1) return '${diff.inMinutes}m ago';
if (diff.inDays < 1) return '${diff.inHours}h ago';
if (diff.inDays < 7) return '${diff.inDays}d ago';
return '${dt.day}/${dt.month}/${dt.year}';
}
```
Then in the ShellRoute, use `ConversationsTabScreen`:
```dart
import 'screens/chat/conversations_tab_screen.dart';
// ...
GoRoute(path: Routes.conversations, builder: (_, _) => const ConversationsTabScreen()),
```
- [ ] **Step 6: Update initial location**
In `GoRouter(initialLocation: ...)`, change:
```dart
initialLocation: Routes.splash,
```
(This stays — the splash screen handles auth redirect. After auth, redirect to briefing. Update the redirect logic to send authenticated users to `Routes.briefing` instead of falling through to `Routes.notes`.)
In the `redirect` callback, ensure unauthenticated users still go to login, and authenticated users landing on `/` or `/notes`/`/tasks`/`/projects` are redirected to `/briefing`. The simplest: no change needed — GoRouter will use the shell's first tab (`Routes.briefing`) naturally as the initial shell route.
- [ ] **Step 7: Analyze**
```bash
flutter analyze lib/app.dart lib/screens/
```
Fix any errors.
- [ ] **Step 8: Commit**
```bash
git add lib/app.dart lib/screens/chat/conversations_tab_screen.dart lib/core/constants.dart
git commit -m "feat: 3-tab shell — Briefing, Library, Chat"
```
---
### Task 6: Delete dead screens
**Files:**
- Delete: 5 screen files
- [ ] **Step 1: Delete the dead files**
```bash
cd /home/bvandeusen/Nextcloud/Projects/fabled_app
git rm lib/screens/notes/notes_list_screen.dart
git rm lib/screens/tasks/tasks_list_screen.dart
git rm lib/screens/projects/project_list_screen.dart
git rm lib/screens/chat/conversations_list_screen.dart
git rm lib/screens/quick_capture/quick_capture_screen.dart
```
- [ ] **Step 2: Verify no remaining imports**
```bash
grep -r "notes_list_screen\|tasks_list_screen\|project_list_screen\|conversations_list_screen\|quick_capture_screen" lib/
```
Expected: no output.
- [ ] **Step 3: Analyze full project**
```bash
flutter analyze
```
Expected: zero errors.
- [ ] **Step 4: Run and verify**
```bash
flutter run --debug
```
Verify:
- App opens to Briefing tab (placeholder screen)
- Library tab shows unified list with filter pills
- Tasks filter → status sub-filter appears
- Tapping a note navigates to `NoteDetailScreen`
- Chat tab shows conversations list
- FAB on Library → bottom sheet with "New note" / "New task"
- [ ] **Step 5: Commit**
```bash
git commit -m "chore: delete dead list screens replaced by LibraryScreen"
```
---
## Verification Checklist
- [ ] `flutter analyze` — zero errors
- [ ] App opens to Briefing tab
- [ ] Library → All: notes and tasks interleaved, sorted by date
- [ ] Library → Notes: only notes
- [ ] Library → Tasks: tasks + status sub-filter works
- [ ] Library → Projects: project cards with colour strip
- [ ] Library search: typing filters results live
- [ ] Task status cycle: tapping checkbox icon on a task card cycles status
- [ ] FAB → bottom sheet → "New note" / "New task" navigates correctly
- [ ] Chat tab: conversations list, new conversation creates and navigates to ChatScreen
- [ ] No references to deleted files remain