From b9e68e3bc8deb6e4f7777bba004480e54c275637 Mon Sep 17 00:00:00 2001 From: bvandeusen Date: Tue, 28 Apr 2026 19:14:28 -0400 Subject: [PATCH] =?UTF-8?q?feat(design):=20surface=20phase=20=E2=80=94=20L?= =?UTF-8?q?ucide=20icons,=20input=20radius,=20Illuminated=20Transcript,=20?= =?UTF-8?q?ActionColors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-screen application of the design system to the Flutter app. Mirrors the web's surface phase landed in FabledScribe v26.04.28.1. Foundation port shipped in 0f05f47; this is the surface work. Lucide icon migration - Added lucide_icons ^0.257.0 dependency - 107 Material Icons references → LucideIcons across 21 files. Drop-in IconData swap (Icon(LucideIcons.X) instead of Icon(Icons.x)). - Lucide import added to each touched file. Input border radius - theme.dart inputDecorationTheme borderRadius 24 → 8 in both light and dark themes. Doc says radius-md (8px) for inputs; previous pill shape was Material default that the doc deviates from. Illuminated Transcript pattern (ChatMessageBubble) - User bubble: accent-tinted border → neutral Pewter (scheme.outline). Asymmetric corner already correct (bottomRight 4px). - Assistant bubble: topLeft corner 4 → 16; only bottomLeft stays 4 (the "tail" effect, mirroring web's `border-bottom-left-radius: 4px`). Background switched from surfaceContainerHighest (Slate) to surface (Iron) per the doc spec "card surface". - Assistant bubble glow shadow added — accent-tinted blur (28px alpha 0.14) + depth shadow (8px alpha 0.4 black). Mirrors web's --color-bubble-asst-shadow. ActionColors wiring (Hybrid rule) - 5 'Delete' confirm buttons across notes / tasks / chat conversations / calendar event sheet → Oxblood action-destructive via the ActionColors ThemeExtension defined in the foundation port. Foreground for ghost/text variants, backgroundColor for filled. - Calendar event Save button → Moss action-primary. The first call site to wire ActionColors.primary; serves as the pattern for future Save reclassifications. - Other Save buttons (note edit, task edit, project edit, etc.) still flow through colorScheme.primary (dusty violet) and read as brand-moment. Reclassifying those is deferred — the wiring pattern is established and can be applied incrementally as files are touched. Indigo cleanup - 4 hardcoded #7C3AED / #5B21B6 literals → dusty-violet equivalents (#5B4A8A / #3F3560). Spots: project_tasks_screen color fallback (×2), journal_screen gradient. Verification - flutter analyze: No issues found What's deferred - Per-screen Save / Cancel reclassification beyond the calendar event Save button. Wiring pattern established; rollout opportunistic. - Long-form 1.7 line-height on assistant Markdown content (would require MarkdownStyleSheet work; minor). - Surface walk on Knowledge / Projects / Settings screens for any hardcoded styling that needs touch. Co-Authored-By: Claude Opus 4.7 --- lib/app.dart | 49 ++++++++++--------- lib/core/theme.dart | 12 ++--- lib/screens/auth/login_screen.dart | 7 +-- lib/screens/calendar/calendar_screen.dart | 3 +- lib/screens/calendar/event_form_sheet.dart | 32 ++++++------ lib/screens/chat/chat_screen.dart | 5 +- .../chat/conversations_tab_screen.dart | 17 ++++--- .../journal/journal_history_screen.dart | 5 +- lib/screens/journal/journal_screen.dart | 7 +-- lib/screens/knowledge/knowledge_screen.dart | 13 ++--- lib/screens/library/project_tasks_screen.dart | 15 +++--- lib/screens/notes/note_detail_screen.dart | 11 ++++- lib/screens/notes/note_edit_screen.dart | 13 +++-- lib/screens/projects/project_edit_screen.dart | 3 +- lib/screens/projects/projects_screen.dart | 3 +- lib/screens/settings/settings_screen.dart | 21 ++++---- lib/screens/tasks/task_edit_screen.dart | 15 ++++-- lib/widgets/chat_message_bubble.dart | 27 +++++++--- lib/widgets/journal_prep_card.dart | 3 +- lib/widgets/knowledge_item_card.dart | 11 +++-- lib/widgets/offline_banner.dart | 3 +- lib/widgets/tool_call_chip.dart | 27 +++++----- lib/widgets/voice_mic_button.dart | 5 +- pubspec.lock | 8 +++ pubspec.yaml | 1 + 25 files changed, 191 insertions(+), 125 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 5d0db2f..05c727a 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -296,7 +297,7 @@ class _ShellState extends ConsumerState<_Shell> with WidgetsBindingObserver { mainAxisSize: MainAxisSize.min, children: [ ListTile( - leading: const Icon(Icons.folder_outlined), + leading: const Icon(LucideIcons.folder), title: const Text('Projects'), onTap: () { Navigator.pop(context); @@ -304,7 +305,7 @@ class _ShellState extends ConsumerState<_Shell> with WidgetsBindingObserver { }, ), ListTile( - leading: const Icon(Icons.calendar_month_outlined), + leading: const Icon(LucideIcons.calendar), title: const Text('Calendar'), onTap: () { Navigator.pop(context); @@ -370,28 +371,28 @@ class _ShellState extends ConsumerState<_Shell> with WidgetsBindingObserver { labelType: NavigationRailLabelType.all, destinations: const [ NavigationRailDestination( - icon: Icon(Icons.menu_book_outlined), - selectedIcon: Icon(Icons.menu_book), + icon: Icon(LucideIcons.bookOpen), + selectedIcon: Icon(LucideIcons.bookOpen), label: Text('Journal'), ), NavigationRailDestination( - icon: Icon(Icons.lightbulb_outline), - selectedIcon: Icon(Icons.lightbulb), + icon: Icon(LucideIcons.lightbulb), + selectedIcon: Icon(LucideIcons.lightbulb), label: Text('Knowledge'), ), NavigationRailDestination( - icon: Icon(Icons.chat_bubble_outline), - selectedIcon: Icon(Icons.chat_bubble), + icon: Icon(LucideIcons.messageCircle), + selectedIcon: Icon(LucideIcons.messageCircle), label: Text('Chat'), ), NavigationRailDestination( - icon: Icon(Icons.folder_outlined), - selectedIcon: Icon(Icons.folder), + icon: Icon(LucideIcons.folder), + selectedIcon: Icon(LucideIcons.folder), label: Text('Projects'), ), NavigationRailDestination( - icon: Icon(Icons.calendar_month_outlined), - selectedIcon: Icon(Icons.calendar_month), + icon: Icon(LucideIcons.calendar), + selectedIcon: Icon(LucideIcons.calendar), label: Text('Calendar'), ), ], @@ -432,23 +433,23 @@ class _ShellState extends ConsumerState<_Shell> with WidgetsBindingObserver { }, destinations: const [ NavigationDestination( - icon: Icon(Icons.menu_book_outlined), - selectedIcon: Icon(Icons.menu_book), + icon: Icon(LucideIcons.bookOpen), + selectedIcon: Icon(LucideIcons.bookOpen), label: 'Journal', ), NavigationDestination( - icon: Icon(Icons.lightbulb_outline), - selectedIcon: Icon(Icons.lightbulb), + icon: Icon(LucideIcons.lightbulb), + selectedIcon: Icon(LucideIcons.lightbulb), label: 'Knowledge', ), NavigationDestination( - icon: Icon(Icons.chat_bubble_outline), - selectedIcon: Icon(Icons.chat_bubble), + icon: Icon(LucideIcons.messageCircle), + selectedIcon: Icon(LucideIcons.messageCircle), label: 'Chat', ), NavigationDestination( - icon: Icon(Icons.more_horiz_outlined), - selectedIcon: Icon(Icons.more_horiz), + icon: Icon(LucideIcons.moreHorizontal), + selectedIcon: Icon(LucideIcons.moreHorizontal), label: 'More', ), ], @@ -579,7 +580,7 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> { prefixIcon: totalPending > 0 ? Badge( label: Text('$totalPending'), - child: const Icon(Icons.cloud_upload_outlined), + child: const Icon(LucideIcons.uploadCloud), ) : isWorking ? const Padding( @@ -591,10 +592,10 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> { strokeWidth: 2), ), ) - : const Icon(Icons.auto_awesome_outlined), + : const Icon(LucideIcons.sparkles), suffixIcon: _controller.text.trim().isNotEmpty ? IconButton( - icon: const Icon(Icons.send), + icon: const Icon(LucideIcons.send), onPressed: _submit, tooltip: 'Capture', ) @@ -609,7 +610,7 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> { onTap: _toggleCaptureMic, ), IconButton( - icon: const Icon(Icons.settings_outlined), + icon: const Icon(LucideIcons.settings), tooltip: 'Settings', onPressed: () => context.push(Routes.settings), ), diff --git a/lib/core/theme.dart b/lib/core/theme.dart index 04a7c21..52c264c 100644 --- a/lib/core/theme.dart +++ b/lib/core/theme.dart @@ -199,15 +199,15 @@ ThemeData fabledDarkTheme() { filled: true, fillColor: _darkSurfaceVar, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: _darkOutline), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: _darkOutline), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: _darkPrimary, width: 2), ), ), @@ -279,15 +279,15 @@ ThemeData fabledLightTheme() { filled: true, fillColor: _lightSurfaceVar, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: _lightOutline), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: _lightOutline), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: _lightPrimary, width: 2), ), ), diff --git a/lib/screens/auth/login_screen.dart b/lib/screens/auth/login_screen.dart index 832c7f6..2d46d24 100644 --- a/lib/screens/auth/login_screen.dart +++ b/lib/screens/auth/login_screen.dart @@ -1,6 +1,7 @@ import 'dart:io' as io; import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -119,7 +120,7 @@ class _LoginScreenState extends ConsumerState { const SizedBox(height: 32), if (_oauthEnabled) ...[ FilledButton.icon( - icon: const Icon(Icons.login), + icon: const Icon(LucideIcons.logIn), label: const Text('Sign in with SSO'), onPressed: _openOAuth, ), @@ -155,8 +156,8 @@ class _LoginScreenState extends ConsumerState { border: const OutlineInputBorder(), suffixIcon: IconButton( icon: Icon(_obscure - ? Icons.visibility - : Icons.visibility_off), + ? LucideIcons.eye + : LucideIcons.eyeOff), onPressed: () => setState(() => _obscure = !_obscure), ), diff --git a/lib/screens/calendar/calendar_screen.dart b/lib/screens/calendar/calendar_screen.dart index 9d9e628..d077f81 100644 --- a/lib/screens/calendar/calendar_screen.dart +++ b/lib/screens/calendar/calendar_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:table_calendar/table_calendar.dart'; @@ -91,7 +92,7 @@ class CalendarScreen extends ConsumerWidget { notifier: notifier, ), ), - child: const Icon(Icons.add), + child: const Icon(LucideIcons.plus), ) : null, ); diff --git a/lib/screens/calendar/event_form_sheet.dart b/lib/screens/calendar/event_form_sheet.dart index 012bf13..2bb871e 100644 --- a/lib/screens/calendar/event_form_sheet.dart +++ b/lib/screens/calendar/event_form_sheet.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../core/theme.dart'; import '../../data/models/calendar_event.dart'; import '../../providers/api_client_provider.dart'; import '../../providers/calendar_provider.dart'; @@ -152,11 +154,10 @@ class _EventFormSheetState extends ConsumerState { ), TextButton( onPressed: () => Navigator.pop(dialogContext, true), - child: Text( - 'Delete', - style: TextStyle( - color: Theme.of(dialogContext).colorScheme.error), + style: TextButton.styleFrom( + foregroundColor: Theme.of(dialogContext).extension()!.destructive, ), + child: const Text('Delete'), ), ], ), @@ -273,7 +274,7 @@ class _EventFormSheetState extends ConsumerState { const Spacer(), if (!_isCreate) IconButton( - icon: const Icon(Icons.delete_outline), + icon: const Icon(LucideIcons.trash2), color: Theme.of(context).colorScheme.error, onPressed: _saving ? null : _delete, ), @@ -303,7 +304,7 @@ class _EventFormSheetState extends ConsumerState { // Start date ListTile( contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.calendar_today_outlined), + leading: const Icon(LucideIcons.calendarDays), title: Text(_fmtDate(_startDt)), subtitle: const Text('Start date'), onTap: _pickStartDate, @@ -313,7 +314,7 @@ class _EventFormSheetState extends ConsumerState { if (!_allDay) ListTile( contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.access_time_outlined), + leading: const Icon(LucideIcons.clock), title: Text(_fmtTime(_startDt)), subtitle: const Text('Start time'), onTap: _pickStartTime, @@ -322,14 +323,14 @@ class _EventFormSheetState extends ConsumerState { // End date ListTile( contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.event_outlined), + leading: const Icon(LucideIcons.calendarCheck), title: Text( _endDt != null ? _fmtDate(_endDt!) : 'No end date'), subtitle: const Text('End date'), onTap: _pickEndDate, trailing: _endDt != null ? IconButton( - icon: const Icon(Icons.clear), + icon: const Icon(LucideIcons.x), onPressed: () => setState(() => _endDt = null), ) : null, @@ -339,7 +340,7 @@ class _EventFormSheetState extends ConsumerState { if (!_allDay && _endDt != null) ListTile( contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.access_time_outlined), + leading: const Icon(LucideIcons.clock), title: Text(_fmtTime(_endDt!)), subtitle: const Text('End time'), onTap: _pickEndTime, @@ -351,14 +352,14 @@ class _EventFormSheetState extends ConsumerState { if (_isCustomRrule) ListTile( contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.repeat_outlined), + leading: const Icon(LucideIcons.repeat), title: const Text('Custom (read-only)'), subtitle: Text(widget.event!.recurrence ?? ''), ) else Row( children: [ - const Icon(Icons.repeat_outlined), + const Icon(LucideIcons.repeat), const SizedBox(width: 16), DropdownButton( value: _recurrence, @@ -419,7 +420,7 @@ class _EventFormSheetState extends ConsumerState { width: _color.isEmpty ? 2 : 1, ), ), - child: const Icon(Icons.block, size: 16), + child: const Icon(LucideIcons.ban, size: 16), ), ), ..._presetColors.map((hex) { @@ -451,10 +452,13 @@ class _EventFormSheetState extends ConsumerState { const SizedBox(height: 24), - // Save button + // Save button — Moss action-primary per Hybrid rule SizedBox( width: double.infinity, child: FilledButton( + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).extension()!.primary, + ), onPressed: _saving ? null : _save, child: _saving ? const SizedBox( diff --git a/lib/screens/chat/chat_screen.dart b/lib/screens/chat/chat_screen.dart index af341f5..bbb7e97 100644 --- a/lib/screens/chat/chat_screen.dart +++ b/lib/screens/chat/chat_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/exceptions.dart'; @@ -173,7 +174,7 @@ class _ChatScreenState extends ConsumerState height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) - : const Icon(Icons.refresh), + : const Icon(LucideIcons.refreshCw), ), ], ), @@ -280,7 +281,7 @@ class _ChatScreenState extends ConsumerState child: CircularProgressIndicator(strokeWidth: 2), ) - : const Icon(Icons.send), + : const Icon(LucideIcons.send), ), ], ), diff --git a/lib/screens/chat/conversations_tab_screen.dart b/lib/screens/chat/conversations_tab_screen.dart index da7e1b9..773785b 100644 --- a/lib/screens/chat/conversations_tab_screen.dart +++ b/lib/screens/chat/conversations_tab_screen.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/constants.dart'; +import '../../core/theme.dart'; import '../../providers/chat_provider.dart'; import 'chat_screen.dart'; @@ -51,6 +53,9 @@ class _ConversationsTabScreenState child: const Text('Cancel')), FilledButton( onPressed: () => Navigator.pop(dialogContext, true), + style: FilledButton.styleFrom( + backgroundColor: Theme.of(dialogContext).extension()!.destructive, + ), child: const Text('Delete')), ], ), @@ -74,7 +79,7 @@ class _ConversationsTabScreenState title: Text('Chat', style: theme.textTheme.titleLarge), actions: [ IconButton( - icon: const Icon(Icons.add), + icon: const Icon(LucideIcons.plus), tooltip: 'New conversation', onPressed: _createConversation, ), @@ -89,14 +94,14 @@ class _ConversationsTabScreenState child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.chat_bubble_outline, + Icon(LucideIcons.messageCircle, 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), + icon: const Icon(LucideIcons.plus), label: const Text('Start a conversation'), onPressed: _createConversation, ), @@ -113,7 +118,7 @@ class _ConversationsTabScreenState final c = convs[i]; final selected = isWide && c.id == _selectedConvId; return ListTile( - leading: const Icon(Icons.chat_bubble_outline), + leading: const Icon(LucideIcons.messageCircle), title: Text( c.title.isEmpty ? 'New conversation' : c.title, maxLines: 1, @@ -124,7 +129,7 @@ class _ConversationsTabScreenState ), selected: selected, trailing: IconButton( - icon: const Icon(Icons.delete_outline), + icon: const Icon(LucideIcons.trash2), onPressed: () => _confirmDelete(c.id, c.title), ), onTap: () => _openConversation(c.id), @@ -155,7 +160,7 @@ class _ConversationsTabScreenState child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.chat_bubble_outline, + Icon(LucideIcons.messageCircle, size: 48, color: theme.colorScheme.onSurfaceVariant), const SizedBox(height: 16), diff --git a/lib/screens/journal/journal_history_screen.dart b/lib/screens/journal/journal_history_screen.dart index d88724b..983f8e8 100644 --- a/lib/screens/journal/journal_history_screen.dart +++ b/lib/screens/journal/journal_history_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../data/models/journal_day.dart'; @@ -27,9 +28,9 @@ class JournalHistoryScreen extends ConsumerWidget { itemBuilder: (context, i) { final isoDate = days[i]; return ListTile( - leading: const Icon(Icons.menu_book_outlined), + leading: const Icon(LucideIcons.bookOpen), title: Text(isoDate), - trailing: const Icon(Icons.chevron_right), + trailing: const Icon(LucideIcons.chevronRight), onTap: () => Navigator.of(context).push( MaterialPageRoute( builder: (_) => _JournalDayDetailScreen(isoDate: isoDate), diff --git a/lib/screens/journal/journal_screen.dart b/lib/screens/journal/journal_screen.dart index df46534..c1dab35 100644 --- a/lib/screens/journal/journal_screen.dart +++ b/lib/screens/journal/journal_screen.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/exceptions.dart'; @@ -188,7 +189,7 @@ class _JournalScreenState extends ConsumerState ) else IconButton( - icon: const Icon(Icons.refresh_outlined), + icon: const Icon(LucideIcons.refreshCw), tooltip: 'Regenerate prep', onPressed: _refresh, ), @@ -426,7 +427,7 @@ class _GradientSendButton extends StatelessWidget { : const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFF7C3AED), Color(0xFF5B21B6)], + colors: [Color(0xFF5B4A8A), Color(0xFF3F3560)], ), color: disabled ? scheme.onSurface.withValues(alpha: 0.12) : null, borderRadius: BorderRadius.circular(10), @@ -442,7 +443,7 @@ class _GradientSendButton extends StatelessWidget { ), ) : Icon( - Icons.send, + LucideIcons.send, color: disabled ? scheme.onSurface.withValues(alpha: 0.38) : Colors.white, diff --git a/lib/screens/knowledge/knowledge_screen.dart b/lib/screens/knowledge/knowledge_screen.dart index 4a9d72b..2222beb 100644 --- a/lib/screens/knowledge/knowledge_screen.dart +++ b/lib/screens/knowledge/knowledge_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -95,7 +96,7 @@ class _KnowledgeScreenState extends ConsumerState : const Text('Knowledge'), actions: [ IconButton( - icon: Icon(_searchActive ? Icons.close : Icons.search), + icon: Icon(_searchActive ? LucideIcons.x : LucideIcons.search), onPressed: () { setState(() => _searchActive = !_searchActive); if (!_searchActive) { @@ -149,7 +150,7 @@ class _KnowledgeScreenState extends ConsumerState floatingActionButton: FloatingActionButton( onPressed: () => _onFabTapped(context), tooltip: 'New', - child: const Icon(Icons.edit_outlined), + child: const Icon(LucideIcons.pencil), ), ); } @@ -259,7 +260,7 @@ class _KnowledgeScreenState extends ConsumerState mainAxisSize: MainAxisSize.min, children: [ _TypePickerRow( - icon: Icons.description_outlined, + icon: LucideIcons.fileText, label: 'Note', description: 'General note or document', onTap: () { @@ -268,7 +269,7 @@ class _KnowledgeScreenState extends ConsumerState }, ), _TypePickerRow( - icon: Icons.person_outlined, + icon: LucideIcons.user, label: 'Person', description: 'Contact, colleague, or reference person', onTap: () { @@ -277,7 +278,7 @@ class _KnowledgeScreenState extends ConsumerState }, ), _TypePickerRow( - icon: Icons.place_outlined, + icon: LucideIcons.mapPin, label: 'Place', description: 'Location, venue, or place of interest', onTap: () { @@ -286,7 +287,7 @@ class _KnowledgeScreenState extends ConsumerState }, ), _TypePickerRow( - icon: Icons.checklist_outlined, + icon: LucideIcons.listChecks, label: 'List', description: 'Checklist or structured list', onTap: () { diff --git a/lib/screens/library/project_tasks_screen.dart b/lib/screens/library/project_tasks_screen.dart index 775715d..9dd73f3 100644 --- a/lib/screens/library/project_tasks_screen.dart +++ b/lib/screens/library/project_tasks_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -55,11 +56,11 @@ class _ProjectTasksScreenState extends ConsumerState { } Color _parseColor(String? hex) { - if (hex == null || hex.isEmpty) return const Color(0xFF7C3AED); + if (hex == null || hex.isEmpty) return const Color(0xFF5B4A8A); try { return Color(int.parse(hex.replaceFirst('#', '0xFF'))); } catch (_) { - return const Color(0xFF7C3AED); + return const Color(0xFF5B4A8A); } } @@ -96,7 +97,7 @@ class _ProjectTasksScreenState extends ConsumerState { ), actions: [ IconButton( - icon: const Icon(Icons.edit_outlined), + icon: const Icon(LucideIcons.pencil), tooltip: 'Edit project', onPressed: () => context.push('/projects/${widget.projectId}/edit'), @@ -133,7 +134,7 @@ class _ProjectTasksScreenState extends ConsumerState { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.check_box_outlined, + Icon(LucideIcons.checkSquare, size: 48, color: Theme.of(context).colorScheme.onSurfaceVariant), const SizedBox(height: 12), @@ -302,9 +303,9 @@ class _TaskRow extends StatelessWidget { }); IconData get _statusIcon => switch (effectiveStatus) { - TaskStatus.done => Icons.check_circle, - TaskStatus.inProgress => Icons.timelapse, - TaskStatus.todo => Icons.radio_button_unchecked, + TaskStatus.done => LucideIcons.checkCircle2, + TaskStatus.inProgress => LucideIcons.loader, + TaskStatus.todo => LucideIcons.circle, }; Color _statusColor(BuildContext context) { diff --git a/lib/screens/notes/note_detail_screen.dart b/lib/screens/notes/note_detail_screen.dart index 19dea9a..9fa659d 100644 --- a/lib/screens/notes/note_detail_screen.dart +++ b/lib/screens/notes/note_detail_screen.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/constants.dart'; +import '../../core/theme.dart'; import '../../core/wikilink_syntax.dart'; import '../../providers/notes_provider.dart'; @@ -44,13 +46,13 @@ class NoteDetailScreen extends ConsumerWidget { data: (note) => Row( children: [ IconButton( - icon: const Icon(Icons.edit), + icon: const Icon(LucideIcons.pencil), onPressed: () => context.push( Routes.noteEdit.replaceFirst(':id', '$noteId'), ), ), IconButton( - icon: const Icon(Icons.delete), + icon: const Icon(LucideIcons.trash2), onPressed: () async { final confirm = await showDialog( context: context, @@ -65,6 +67,11 @@ class NoteDetailScreen extends ConsumerWidget { TextButton( onPressed: () => Navigator.pop(dialogContext, true), + style: TextButton.styleFrom( + foregroundColor: Theme.of(dialogContext) + .extension()! + .destructive, + ), child: const Text('Delete'), ), ], diff --git a/lib/screens/notes/note_edit_screen.dart b/lib/screens/notes/note_edit_screen.dart index e02477d..bb3851f 100644 --- a/lib/screens/notes/note_edit_screen.dart +++ b/lib/screens/notes/note_edit_screen.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/exceptions.dart'; +import '../../core/theme.dart'; import '../../core/wikilink_syntax.dart'; import '../../providers/api_client_provider.dart'; import '../../providers/notes_provider.dart'; @@ -85,6 +87,9 @@ class _NoteEditScreenState extends ConsumerState { ), TextButton( onPressed: () => Navigator.pop(dialogContext, true), + style: TextButton.styleFrom( + foregroundColor: Theme.of(dialogContext).extension()!.destructive, + ), child: const Text('Delete'), ), ], @@ -163,12 +168,12 @@ class _NoteEditScreenState extends ConsumerState { actions: [ if (widget.noteId != null) IconButton( - icon: const Icon(Icons.delete_outline), + icon: const Icon(LucideIcons.trash2), tooltip: 'Delete', onPressed: _delete, ), IconButton( - icon: Icon(_preview ? Icons.edit : Icons.preview), + icon: Icon(_preview ? LucideIcons.pencil : LucideIcons.eye), tooltip: _preview ? 'Edit' : 'Preview', onPressed: () => setState(() => _preview = !_preview), ), @@ -179,7 +184,7 @@ class _NoteEditScreenState extends ConsumerState { height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) - : const Icon(Icons.check), + : const Icon(LucideIcons.check), onPressed: _saving ? null : _save, ), ], @@ -272,7 +277,7 @@ class _TagInput extends StatelessWidget { label: Text('#$tag', style: const TextStyle(fontSize: 12)), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.symmetric(horizontal: 4), - deleteIcon: const Icon(Icons.close, size: 14), + deleteIcon: const Icon(LucideIcons.x, size: 14), onDeleted: () => onRemove(tag), ), ), diff --git a/lib/screens/projects/project_edit_screen.dart b/lib/screens/projects/project_edit_screen.dart index ca5f3e3..4e085ba 100644 --- a/lib/screens/projects/project_edit_screen.dart +++ b/lib/screens/projects/project_edit_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -105,7 +106,7 @@ class _ProjectEditScreenState extends ConsumerState { height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) - : const Icon(Icons.check), + : const Icon(LucideIcons.check), onPressed: _saving ? null : _save, ), ], diff --git a/lib/screens/projects/projects_screen.dart b/lib/screens/projects/projects_screen.dart index 2436fb9..0a9ecf0 100644 --- a/lib/screens/projects/projects_screen.dart +++ b/lib/screens/projects/projects_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -30,7 +31,7 @@ class ProjectsScreen extends ConsumerWidget { floatingActionButton: FloatingActionButton( onPressed: () => context.push('/projects/new'), tooltip: 'New project', - child: const Icon(Icons.add), + child: const Icon(LucideIcons.plus), ), ); } diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index 0dcf546..1a115e0 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -24,28 +25,28 @@ class SettingsScreen extends ConsumerWidget { ListTile( title: const Text('Server URL'), subtitle: Text(serverUrl ?? 'Not configured'), - leading: const Icon(Icons.dns), + leading: const Icon(LucideIcons.server), onTap: () => context.go(Routes.setup), ), const Divider(), ListTile( title: const Text('Appearance'), - leading: const Icon(Icons.brightness_6), + leading: const Icon(LucideIcons.sunMoon), trailing: SegmentedButton( segments: const [ ButtonSegment( value: ThemeMode.system, - icon: Icon(Icons.brightness_auto), + icon: Icon(LucideIcons.contrast), tooltip: 'System', ), ButtonSegment( value: ThemeMode.light, - icon: Icon(Icons.light_mode), + icon: Icon(LucideIcons.sun), tooltip: 'Light', ), ButtonSegment( value: ThemeMode.dark, - icon: Icon(Icons.dark_mode), + icon: Icon(LucideIcons.moon), tooltip: 'Dark', ), ], @@ -58,7 +59,7 @@ class SettingsScreen extends ConsumerWidget { // ── Updates ────────────────────────────────────────────────────── ListTile( - leading: const Icon(Icons.update), + leading: const Icon(LucideIcons.refreshCw), title: const Text('Update repository'), subtitle: Text(repoUrl?.isNotEmpty == true ? repoUrl! @@ -66,7 +67,7 @@ class SettingsScreen extends ConsumerWidget { onTap: () => _editRepoUrl(context, ref, repoUrl), ), ListTile( - leading: const Icon(Icons.info_outline), + leading: const Icon(LucideIcons.info), title: const Text('App version'), subtitle: _versionSubtitle(update), trailing: update.status == UpdateStatus.checking @@ -98,7 +99,7 @@ class SettingsScreen extends ConsumerWidget { if (update.status == UpdateStatus.error) ListTile( leading: - const Icon(Icons.error_outline, color: Colors.red), + const Icon(LucideIcons.alertCircle, color: Colors.red), title: const Text('Update check failed'), subtitle: Text( update.errorMessage ?? 'Unknown error', @@ -110,7 +111,7 @@ class SettingsScreen extends ConsumerWidget { const Divider(), ListTile( title: const Text('Sign Out'), - leading: const Icon(Icons.logout), + leading: const Icon(LucideIcons.logOut), onTap: () async { await ref.read(authProvider.notifier).logout(); if (context.mounted) context.go(Routes.login); @@ -184,7 +185,7 @@ class _UpdateTile extends ConsumerWidget { final isDownloading = update.status == UpdateStatus.downloading; return ListTile( - leading: const Icon(Icons.system_update, color: Colors.green), + leading: const Icon(LucideIcons.downloadCloud, color: Colors.green), title: Text('v${update.latestVersion} available'), subtitle: isDownloading ? Column( diff --git a/lib/screens/tasks/task_edit_screen.dart b/lib/screens/tasks/task_edit_screen.dart index cdab04e..0b1ed17 100644 --- a/lib/screens/tasks/task_edit_screen.dart +++ b/lib/screens/tasks/task_edit_screen.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/constants.dart'; import '../../core/exceptions.dart'; +import '../../core/theme.dart'; import '../../data/models/task.dart'; import '../../providers/api_client_provider.dart'; import '../../providers/tasks_provider.dart'; @@ -111,6 +113,9 @@ class _TaskEditScreenState extends ConsumerState { child: const Text('Cancel')), TextButton( onPressed: () => Navigator.pop(dialogContext, true), + style: TextButton.styleFrom( + foregroundColor: Theme.of(dialogContext).extension()!.destructive, + ), child: const Text('Delete')), ], ), @@ -190,7 +195,7 @@ class _TaskEditScreenState extends ConsumerState { actions: [ if (widget.taskId != null) IconButton( - icon: const Icon(Icons.delete), + icon: const Icon(LucideIcons.trash2), onPressed: _delete, ), IconButton( @@ -200,7 +205,7 @@ class _TaskEditScreenState extends ConsumerState { height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) - : const Icon(Icons.check), + : const Icon(LucideIcons.check), onPressed: _saving ? null : _save, ), ], @@ -272,10 +277,10 @@ class _TaskEditScreenState extends ConsumerState { title: Text(_dueDate == null ? 'No due date' : 'Due: ${_dueDate!.toLocal().toString().substring(0, 10)}'), - leading: const Icon(Icons.calendar_today), + leading: const Icon(LucideIcons.calendarDays), trailing: _dueDate != null ? IconButton( - icon: const Icon(Icons.clear), + icon: const Icon(LucideIcons.x), onPressed: () => setState(() => _dueDate = null), ) @@ -331,7 +336,7 @@ class _SubTasksSection extends ConsumerWidget { const Spacer(), TextButton.icon( onPressed: onAdd, - icon: const Icon(Icons.add, size: 16), + icon: const Icon(LucideIcons.plus, size: 16), label: const Text('Add'), style: TextButton.styleFrom( foregroundColor: colorScheme.primary, diff --git a/lib/widgets/chat_message_bubble.dart b/lib/widgets/chat_message_bubble.dart index bf000b0..488b1b7 100644 --- a/lib/widgets/chat_message_bubble.dart +++ b/lib/widgets/chat_message_bubble.dart @@ -41,13 +41,16 @@ class ChatMessageBubble extends ConsumerWidget { maxWidth: min(MediaQuery.of(context).size.width * 0.82, 480), ), margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 4), + // Illuminated Transcript pattern (mirrors web's ChatMessage.vue): + // - User bubble: transparent bg, neutral Pewter border, only the + // bottom-right corner clipped (the "from-me" tail). + // - Assistant bubble: card surface, 2px accent left edge (the + // "illuminated capital"), accent-tinted glow shadow + depth + // shadow, only the bottom-left corner clipped. decoration: isUser ? BoxDecoration( color: Colors.transparent, - border: Border.all( - color: scheme.primary.withValues(alpha: 0.35), - width: 1, - ), + border: Border.all(color: scheme.outline, width: 1), borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), @@ -56,16 +59,28 @@ class ChatMessageBubble extends ConsumerWidget { ), ) : BoxDecoration( - color: scheme.surfaceContainerHighest, + color: scheme.surface, border: Border( left: BorderSide(color: scheme.primary, width: 2), ), borderRadius: const BorderRadius.only( - topLeft: Radius.circular(4), + topLeft: Radius.circular(16), topRight: Radius.circular(16), bottomLeft: Radius.circular(4), bottomRight: Radius.circular(16), ), + boxShadow: [ + BoxShadow( + color: scheme.primary.withValues(alpha: 0.14), + blurRadius: 28, + offset: const Offset(0, 4), + ), + BoxShadow( + color: Colors.black.withValues(alpha: 0.4), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), diff --git a/lib/widgets/journal_prep_card.dart b/lib/widgets/journal_prep_card.dart index 0e44630..d8802a7 100644 --- a/lib/widgets/journal_prep_card.dart +++ b/lib/widgets/journal_prep_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import '../data/models/message.dart'; @@ -46,7 +47,7 @@ class _JournalPrepCardState extends State { children: [ Row( children: [ - Icon(Icons.menu_book_outlined, size: 18, color: scheme.primary), + Icon(LucideIcons.bookOpen, size: 18, color: scheme.primary), const SizedBox(width: 8), Text( _todayLabel(), diff --git a/lib/widgets/knowledge_item_card.dart b/lib/widgets/knowledge_item_card.dart index 9af1688..f1f8dda 100644 --- a/lib/widgets/knowledge_item_card.dart +++ b/lib/widgets/knowledge_item_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:go_router/go_router.dart'; import '../data/models/knowledge_item.dart'; @@ -8,11 +9,11 @@ class KnowledgeItemCard extends StatelessWidget { const KnowledgeItemCard({super.key, required this.item}); IconData get _icon => switch (item.noteType) { - 'person' => Icons.person_outlined, - 'place' => Icons.place_outlined, - 'list' => Icons.checklist_outlined, - 'task' => Icons.task_alt_outlined, - _ => Icons.description_outlined, + 'person' => LucideIcons.user, + 'place' => LucideIcons.mapPin, + 'list' => LucideIcons.listChecks, + 'task' => LucideIcons.checkCircle2, + _ => LucideIcons.fileText, }; Color _statusColor(BuildContext context) { diff --git a/lib/widgets/offline_banner.dart b/lib/widgets/offline_banner.dart index 2d9ee72..94137e1 100644 --- a/lib/widgets/offline_banner.dart +++ b/lib/widgets/offline_banner.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/auth_provider.dart'; @@ -40,7 +41,7 @@ class _OfflineBannerState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( children: [ - Icon(Icons.cloud_off_outlined, + Icon(LucideIcons.cloudOff, size: 18, color: scheme.onErrorContainer), const SizedBox(width: 8), Expanded( diff --git a/lib/widgets/tool_call_chip.dart b/lib/widgets/tool_call_chip.dart index ba61774..a801979 100644 --- a/lib/widgets/tool_call_chip.dart +++ b/lib/widgets/tool_call_chip.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import 'package:go_router/go_router.dart'; import '../core/constants.dart'; @@ -45,24 +46,24 @@ const Map _toolLabels = { }; IconData _iconFor(String fn) { - if (fn.contains('note')) return Icons.sticky_note_2_outlined; - if (fn.contains('task')) return Icons.check_circle_outline; + if (fn.contains('note')) return LucideIcons.stickyNote; + if (fn.contains('task')) return LucideIcons.checkCircle2; if (fn.contains('event') || fn.contains('calendar')) { - return Icons.event_outlined; + return LucideIcons.calendarCheck; } - if (fn.contains('project')) return Icons.folder_outlined; - if (fn.contains('milestone')) return Icons.flag_outlined; + if (fn.contains('project')) return LucideIcons.folder; + if (fn.contains('milestone')) return LucideIcons.flag; if (fn.contains('web') || fn.contains('research') || fn.contains('article')) { - return Icons.public; + return LucideIcons.globe; } - if (fn.contains('image')) return Icons.image_outlined; + if (fn.contains('image')) return LucideIcons.image; if (fn.contains('person') || fn.contains('profile')) { - return Icons.person_outline; + return LucideIcons.user; } - if (fn.contains('place')) return Icons.place_outlined; - if (fn.contains('rag') || fn.contains('scope')) return Icons.tune; - if (fn.contains('calculate')) return Icons.calculate_outlined; - return Icons.auto_awesome; + if (fn.contains('place')) return LucideIcons.mapPin; + if (fn.contains('rag') || fn.contains('scope')) return LucideIcons.sliders; + if (fn.contains('calculate')) return LucideIcons.calculator; + return LucideIcons.sparkles; } /// Pull the destination route for this tool call from its `result` payload, @@ -161,7 +162,7 @@ class ToolCallChip extends StatelessWidget { ), if (route != null) ...[ const SizedBox(width: 4), - Icon(Icons.arrow_forward, size: 11, color: fg), + Icon(LucideIcons.arrowRight, size: 11, color: fg), ], ], ), diff --git a/lib/widgets/voice_mic_button.dart b/lib/widgets/voice_mic_button.dart index d59c550..bbbb509 100644 --- a/lib/widgets/voice_mic_button.dart +++ b/lib/widgets/voice_mic_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; import '../providers/voice_provider.dart'; @@ -48,8 +49,8 @@ class VoiceMicButton extends StatelessWidget { color: iconColor, ), ), - VoiceMode.playing => Icon(Icons.volume_up, color: iconColor, size: 20), - _ => Icon(Icons.mic, color: iconColor, size: 20), + VoiceMode.playing => Icon(LucideIcons.volume2, color: iconColor, size: 20), + _ => Icon(LucideIcons.mic, color: iconColor, size: 20), }; } diff --git a/pubspec.lock b/pubspec.lock index 5c8f215..e7d0b33 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -496,6 +496,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + lucide_icons: + dependency: "direct main" + description: + name: lucide_icons + sha256: ad24d0fd65707e48add30bebada7d90bff2a1bba0a72d6e9b19d44246b0e83c4 + url: "https://pub.dev" + source: hosted + version: "0.257.0" markdown: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index a618b73..5a4fc9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: sdk: flutter cupertino_icons: ^1.0.8 + lucide_icons: ^0.257.0 flutter_riverpod: ^3.3.1 go_router: ^17.1.0 dio: ^5.6.0