6232c7c99a
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>
247 lines
9.2 KiB
Dart
247 lines
9.2 KiB
Dart
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),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|