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>
This commit is contained in:
@@ -14,4 +14,6 @@ abstract class Routes {
|
||||
static const chat = '/chat/:id';
|
||||
static const quickCapture = '/quick-capture';
|
||||
static const settings = '/settings';
|
||||
static const briefing = '/briefing';
|
||||
static const library = '/library';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
// ── Colour constants ──────────────────────────────────────────────────────────
|
||||
|
||||
const _darkBackground = Color(0xFF111113);
|
||||
const _darkSurface = Color(0xFF18181C);
|
||||
const _darkSurfaceVar = Color(0xFF1E1E24);
|
||||
const _darkPrimary = Color(0xFF6366F1);
|
||||
const _darkOnSurface = Color(0xFFE8E8F0);
|
||||
const _darkOnSurfaceVar = Color(0xFF8888A8);
|
||||
const _darkOutline = Color(0xFF2E2E3A);
|
||||
|
||||
const _lightBackground = Color(0xFFF4F4F8);
|
||||
const _lightSurface = Color(0xFFFFFFFF);
|
||||
const _lightSurfaceVar = Color(0xFFF0F0F5);
|
||||
const _lightPrimary = Color(0xFF4F46E5);
|
||||
const _lightOnSurface = Color(0xFF18181C);
|
||||
const _lightOnSurfaceVar = Color(0xFF6B6B88);
|
||||
const _lightOutline = Color(0xFFD4D4E4);
|
||||
|
||||
// ── Typography ─────────────────────────────────────────────────────────────────
|
||||
|
||||
TextTheme _buildTextTheme(TextTheme base) {
|
||||
final fraunces = GoogleFonts.frauncesTextTheme(base);
|
||||
return base.copyWith(
|
||||
// Headings / titles use Fraunces
|
||||
headlineLarge: fraunces.headlineLarge,
|
||||
headlineMedium: fraunces.headlineMedium,
|
||||
headlineSmall: fraunces.headlineSmall,
|
||||
titleLarge: fraunces.titleLarge,
|
||||
titleMedium: fraunces.titleMedium,
|
||||
// Body / labels remain system default
|
||||
);
|
||||
}
|
||||
|
||||
// ── Themes ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
ThemeData fabledDarkTheme() {
|
||||
final cs = ColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
primary: _darkPrimary,
|
||||
onPrimary: Colors.white,
|
||||
primaryContainer: const Color(0xFF3730A3),
|
||||
onPrimaryContainer: _darkOnSurface,
|
||||
secondary: _darkPrimary,
|
||||
onSecondary: Colors.white,
|
||||
secondaryContainer: _darkSurfaceVar,
|
||||
onSecondaryContainer: _darkOnSurface,
|
||||
tertiary: _darkPrimary,
|
||||
onTertiary: Colors.white,
|
||||
tertiaryContainer: _darkSurfaceVar,
|
||||
onTertiaryContainer: _darkOnSurface,
|
||||
error: const Color(0xFFEF4444),
|
||||
onError: Colors.white,
|
||||
errorContainer: const Color(0xFF7F1D1D),
|
||||
onErrorContainer: const Color(0xFFFEE2E2),
|
||||
surface: _darkSurface,
|
||||
onSurface: _darkOnSurface,
|
||||
surfaceContainerHighest: _darkSurfaceVar,
|
||||
onSurfaceVariant: _darkOnSurfaceVar,
|
||||
outline: _darkOutline,
|
||||
outlineVariant: _darkOutline,
|
||||
shadow: Colors.black,
|
||||
scrim: Colors.black,
|
||||
inverseSurface: _darkOnSurface,
|
||||
onInverseSurface: _darkSurface,
|
||||
inversePrimary: _lightPrimary,
|
||||
);
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: cs,
|
||||
scaffoldBackgroundColor: _darkBackground,
|
||||
textTheme: _buildTextTheme(ThemeData.dark().textTheme),
|
||||
cardTheme: CardThemeData(
|
||||
color: _darkSurface,
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withValues(alpha: 0.4),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: _darkSurface,
|
||||
indicatorColor: _darkPrimary.withValues(alpha: 0.2),
|
||||
),
|
||||
navigationRailTheme: NavigationRailThemeData(
|
||||
backgroundColor: _darkSurface,
|
||||
indicatorColor: _darkPrimary.withValues(alpha: 0.2),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: _darkSurfaceVar,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderSide: BorderSide(color: _darkOutline),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderSide: BorderSide(color: _darkOutline),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderSide: BorderSide(color: _darkPrimary, width: 2),
|
||||
),
|
||||
),
|
||||
dividerTheme: DividerThemeData(color: _darkOutline, thickness: 1),
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: _darkSurfaceVar,
|
||||
labelStyle: TextStyle(color: _darkOnSurfaceVar, fontSize: 12),
|
||||
side: BorderSide(color: _darkOutline),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData fabledLightTheme() {
|
||||
final cs = ColorScheme(
|
||||
brightness: Brightness.light,
|
||||
primary: _lightPrimary,
|
||||
onPrimary: Colors.white,
|
||||
primaryContainer: const Color(0xFFE0E0FF),
|
||||
onPrimaryContainer: _lightOnSurface,
|
||||
secondary: _lightPrimary,
|
||||
onSecondary: Colors.white,
|
||||
secondaryContainer: _lightSurfaceVar,
|
||||
onSecondaryContainer: _lightOnSurface,
|
||||
tertiary: _lightPrimary,
|
||||
onTertiary: Colors.white,
|
||||
tertiaryContainer: _lightSurfaceVar,
|
||||
onTertiaryContainer: _lightOnSurface,
|
||||
error: const Color(0xFFDC2626),
|
||||
onError: Colors.white,
|
||||
errorContainer: const Color(0xFFFEE2E2),
|
||||
onErrorContainer: const Color(0xFF7F1D1D),
|
||||
surface: _lightSurface,
|
||||
onSurface: _lightOnSurface,
|
||||
surfaceContainerHighest: _lightSurfaceVar,
|
||||
onSurfaceVariant: _lightOnSurfaceVar,
|
||||
outline: _lightOutline,
|
||||
outlineVariant: _lightOutline,
|
||||
shadow: Colors.black,
|
||||
scrim: Colors.black,
|
||||
inverseSurface: _lightOnSurface,
|
||||
onInverseSurface: _lightSurface,
|
||||
inversePrimary: _darkPrimary,
|
||||
);
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: cs,
|
||||
scaffoldBackgroundColor: _lightBackground,
|
||||
textTheme: _buildTextTheme(ThemeData.light().textTheme),
|
||||
cardTheme: CardThemeData(
|
||||
color: _lightSurface,
|
||||
elevation: 1,
|
||||
shadowColor: Colors.black.withValues(alpha: 0.08),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: _lightSurface,
|
||||
indicatorColor: _lightPrimary.withValues(alpha: 0.12),
|
||||
),
|
||||
navigationRailTheme: NavigationRailThemeData(
|
||||
backgroundColor: _lightSurface,
|
||||
indicatorColor: _lightPrimary.withValues(alpha: 0.12),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: _lightSurfaceVar,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderSide: BorderSide(color: _lightOutline),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderSide: BorderSide(color: _lightOutline),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderSide: BorderSide(color: _lightPrimary, width: 2),
|
||||
),
|
||||
),
|
||||
dividerTheme: DividerThemeData(color: _lightOutline, thickness: 1),
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: _lightSurfaceVar,
|
||||
labelStyle: TextStyle(color: _lightOnSurfaceVar, fontSize: 12),
|
||||
side: BorderSide(color: _lightOutline),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── GradientButton ─────────────────────────────────────────────────────────────
|
||||
// Use wherever the web app uses the indigo gradient button (send, primary actions).
|
||||
|
||||
class GradientButton extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
const GradientButton({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.child,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final disabled = onPressed == null;
|
||||
return AnimatedOpacity(
|
||||
opacity: disabled ? 0.45 : 1.0,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: disabled
|
||||
? null
|
||||
: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF6366F1), Color(0xFF4F46E5)],
|
||||
),
|
||||
color: disabled ? const Color(0xFF6366F1) : null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: disabled
|
||||
? null
|
||||
: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF6366F1).withValues(alpha: 0.35),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(padding: padding, child: child),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user