refactor: ui/ux refinements

This commit is contained in:
cogwheel0
2025-08-22 01:24:04 +05:30
parent 9f80b1e727
commit 2cdbbbc1d3
11 changed files with 459 additions and 537 deletions

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../features/chat/views/chat_page.dart'; import '../../features/chat/views/chat_page.dart';
import '../../features/files/views/files_page.dart'; import '../../features/files/views/workspace_page.dart';
import '../../features/profile/views/profile_page.dart'; import '../../features/profile/views/profile_page.dart';
/// Service for handling deep links and navigation routing /// Service for handling deep links and navigation routing
@@ -16,11 +16,11 @@ class DeepLinkService {
); );
} }
/// In single-screen mode, files/profile deep links route via navigator /// In single-screen mode, workspace/profile deep links route via navigator
static void navigateToFiles(BuildContext context) { static void navigateToWorkspace(BuildContext context) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => const FilesPage()), MaterialPageRoute(builder: (context) => const WorkspacePage()),
); );
} }
@@ -37,9 +37,12 @@ class DeepLinkService {
case '/chat': case '/chat':
case '/main/chat': case '/main/chat':
return '/chat'; return '/chat';
case '/files': // Support both new and legacy paths for workspace
case '/main/files': case '/workspace':
return '/files'; case '/main/workspace':
case '/files': // legacy
case '/main/files': // legacy
return '/workspace';
case '/profile': case '/profile':
case '/main/profile': case '/main/profile':
return '/profile'; return '/profile';
@@ -52,8 +55,8 @@ class DeepLinkService {
static Widget handleDeepLink(String route) { static Widget handleDeepLink(String route) {
final path = parsePath(route); final path = parsePath(route);
switch (path) { switch (path) {
case '/files': case '/workspace':
return const FilesPage(); return const WorkspacePage();
case '/profile': case '/profile':
return const ProfilePage(); return const ProfilePage();
case '/chat': case '/chat':

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
// ThemedDialogs handles theming; no direct use of extensions here // ThemedDialogs handles theming; no direct use of extensions here
import '../../features/auth/views/connect_signin_page.dart'; import '../../features/auth/views/connect_signin_page.dart';
import '../../features/chat/views/chat_page.dart'; import '../../features/chat/views/chat_page.dart';
import '../../features/files/views/files_page.dart'; import '../../features/files/views/workspace_page.dart';
import '../../features/profile/views/profile_page.dart'; import '../../features/profile/views/profile_page.dart';
import '../../shared/widgets/themed_dialogs.dart'; import '../../shared/widgets/themed_dialogs.dart';
@@ -130,8 +130,8 @@ class NavigationService {
page = const ConnectAndSignInPage(); page = const ConnectAndSignInPage();
break; break;
case Routes.files: case Routes.workspace:
page = const FilesPage(); page = const WorkspacePage();
break; break;
// chats list route removed (replaced by drawer) // chats list route removed (replaced by drawer)
@@ -154,5 +154,5 @@ class Routes {
static const String login = '/login'; static const String login = '/login';
static const String profile = '/profile'; static const String profile = '/profile';
static const String serverConnection = '/server-connection'; static const String serverConnection = '/server-connection';
static const String files = '/files'; static const String workspace = '/workspace';
} }

View File

@@ -18,8 +18,6 @@ import '../widgets/assistant_message_widget.dart' as assistant;
import '../widgets/file_attachment_widget.dart'; import '../widgets/file_attachment_widget.dart';
import '../services/voice_input_service.dart'; import '../services/voice_input_service.dart';
import '../services/file_attachment_service.dart'; import '../services/file_attachment_service.dart';
import '../../files/views/files_page.dart';
import '../../profile/views/profile_page.dart';
import '../../tools/providers/tools_providers.dart'; import '../../tools/providers/tools_providers.dart';
import '../../navigation/widgets/chats_drawer.dart'; import '../../navigation/widgets/chats_drawer.dart';
import '../../../shared/widgets/offline_indicator.dart'; import '../../../shared/widgets/offline_indicator.dart';
@@ -30,6 +28,7 @@ import '../../../shared/widgets/loading_states.dart';
import 'chat_page_helpers.dart'; import 'chat_page_helpers.dart';
import '../../../shared/widgets/themed_dialogs.dart'; import '../../../shared/widgets/themed_dialogs.dart';
import '../../onboarding/views/onboarding_sheet.dart'; import '../../onboarding/views/onboarding_sheet.dart';
import '../../../shared/widgets/sheet_handle.dart';
class ChatPage extends ConsumerStatefulWidget { class ChatPage extends ConsumerStatefulWidget {
const ChatPage({super.key}); const ChatPage({super.key});
@@ -464,133 +463,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
// Replaced bottom-sheet chat list with left drawer (see ChatsDrawer) // Replaced bottom-sheet chat list with left drawer (see ChatsDrawer)
void _showQuickAccessMenu() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => Container(
decoration: BoxDecoration(
color: context.conduitTheme.surfaceBackground,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.modal),
),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle bar
Container(
width: 40,
height: 4,
margin: const EdgeInsets.symmetric(vertical: Spacing.sm),
decoration: BoxDecoration(
color: context.conduitTheme.dividerColor,
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
),
),
// Hint text
Padding(
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
child: Text(
'Quick Actions',
style: AppTypography.bodySmallStyle.copyWith(
color: context.conduitTheme.textSecondary,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: Spacing.xs),
// Menu items
ListTile(
leading: Icon(
Platform.isIOS ? CupertinoIcons.plus : Icons.add_rounded,
color: context.conduitTheme.iconPrimary,
),
title: Text(
'New Chat',
style: AppTypography.bodyLargeStyle.copyWith(
color: context.conduitTheme.textPrimary,
),
),
subtitle: Text(
'Start a new conversation',
style: AppTypography.bodySmallStyle.copyWith(
color: context.conduitTheme.textSecondary,
),
),
onTap: () {
Navigator.pop(context);
_handleNewChat();
},
),
ListTile(
leading: Icon(
Platform.isIOS
? CupertinoIcons.doc
: Icons.description_outlined,
color: context.conduitTheme.iconPrimary,
),
title: Text(
'Files',
style: AppTypography.bodyLargeStyle.copyWith(
color: context.conduitTheme.textPrimary,
),
),
subtitle: Text(
'Manage your files and documents',
style: AppTypography.bodySmallStyle.copyWith(
color: context.conduitTheme.textSecondary,
),
),
onTap: () {
Navigator.pop(context);
_navigateToFiles();
},
),
ListTile(
leading: Icon(
Platform.isIOS ? CupertinoIcons.person : Icons.person_outline,
color: context.conduitTheme.iconPrimary,
),
title: Text(
'Profile',
style: AppTypography.bodyLargeStyle.copyWith(
color: context.conduitTheme.textPrimary,
),
),
subtitle: Text(
'View and manage your profile',
style: AppTypography.bodySmallStyle.copyWith(
color: context.conduitTheme.textSecondary,
),
),
onTap: () {
Navigator.pop(context);
_navigateToProfile();
},
),
const SizedBox(height: Spacing.sm),
],
),
),
),
);
}
void _navigateToFiles() {
Navigator.of(
context,
).push(MaterialPageRoute(builder: (context) => const FilesPage()));
}
void _navigateToProfile() {
Navigator.of(
context,
).push(MaterialPageRoute(builder: (context) => const ProfilePage()));
}
void _onScroll() { void _onScroll() {
if (!_scrollController.hasClients) return; if (!_scrollController.hasClients) return;
@@ -1126,10 +998,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
// Open left drawer instead of bottom sheet // Open left drawer instead of bottom sheet
Scaffold.of(ctx).openDrawer(); Scaffold.of(ctx).openDrawer();
}, },
onLongPress: () {
HapticFeedback.mediumImpact();
_showQuickAccessMenu();
},
child: Padding( child: Padding(
padding: const EdgeInsets.all(4.0), padding: const EdgeInsets.all(4.0),
child: Icon( child: Icon(
@@ -1635,19 +1503,8 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
padding: const EdgeInsets.all(Spacing.bottomSheetPadding), padding: const EdgeInsets.all(Spacing.bottomSheetPadding),
child: Column( child: Column(
children: [ children: [
// Handle bar // Handle bar (standardized)
Container( const SheetHandle(),
margin: const EdgeInsets.only(
top: Spacing.sm,
bottom: Spacing.md,
),
width: Spacing.xxl,
height: Spacing.xs,
decoration: BoxDecoration(
color: context.conduitTheme.dividerColor,
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
),
),
// Search field // Search field
Padding( Padding(

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../shared/theme/app_theme.dart'; import '../../../shared/theme/app_theme.dart';
import '../../../shared/widgets/sheet_handle.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -207,16 +208,8 @@ class CopyOptionsSheet extends ConsumerWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Handle bar // Handle bar (standardized)
Container( const SheetHandle(),
margin: const EdgeInsets.only(top: Spacing.sm),
width: 40,
height: 4,
decoration: BoxDecoration(
color: context.conduitTheme.dividerColor,
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
),
),
const SizedBox(height: Spacing.lg - Spacing.xs), const SizedBox(height: Spacing.lg - Spacing.xs),
@@ -340,16 +333,8 @@ class ExportOptionsSheet extends ConsumerWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Handle bar // Handle bar (standardized)
Container( const SheetHandle(),
margin: const EdgeInsets.only(top: Spacing.sm),
width: 40,
height: 4,
decoration: BoxDecoration(
color: context.conduitTheme.dividerColor,
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
),
),
const SizedBox(height: Spacing.lg - Spacing.xs), const SizedBox(height: Spacing.lg - Spacing.xs),
@@ -465,16 +450,8 @@ class MoreOptionsSheet extends ConsumerWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Handle bar // Handle bar (standardized)
Container( const SheetHandle(),
margin: const EdgeInsets.only(top: Spacing.sm),
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppTheme.neutral50.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
),
),
const SizedBox(height: Spacing.lg - Spacing.xs), const SizedBox(height: Spacing.lg - Spacing.xs),

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../../../shared/theme/theme_extensions.dart'; import '../../../shared/theme/theme_extensions.dart';
import '../../../shared/widgets/sheet_handle.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -615,17 +616,8 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Handle bar // Handle bar (standardized)
Container( const SheetHandle(),
width: 40,
height: 4,
decoration: BoxDecoration(
color: context.conduitTheme.textPrimary.withValues(
alpha: Alpha.medium,
),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: Spacing.lg), const SizedBox(height: Spacing.lg),
// Options grid // Options grid

View File

@@ -8,16 +8,17 @@ import '../../../core/services/navigation_service.dart';
import '../../../shared/widgets/improved_loading_states.dart'; import '../../../shared/widgets/improved_loading_states.dart';
import '../../../shared/utils/ui_utils.dart'; import '../../../shared/utils/ui_utils.dart';
import '../../../shared/widgets/sheet_handle.dart';
/// Files page for managing documents and uploads /// Files page for managing documents and uploads
class FilesPage extends ConsumerStatefulWidget { class WorkspacePage extends ConsumerStatefulWidget {
const FilesPage({super.key}); const WorkspacePage({super.key});
@override @override
ConsumerState<FilesPage> createState() => _FilesPageState(); ConsumerState<WorkspacePage> createState() => _WorkspacePageState();
} }
class _FilesPageState extends ConsumerState<FilesPage> class _WorkspacePageState extends ConsumerState<WorkspacePage>
with TickerProviderStateMixin { with TickerProviderStateMixin {
int _selectedTab = 0; int _selectedTab = 0;
late AnimationController _tabAnimationController; late AnimationController _tabAnimationController;
@@ -109,17 +110,12 @@ class _FilesPageState extends ConsumerState<FilesPage>
onPressed: () => NavigationService.goBack(), onPressed: () => NavigationService.goBack(),
tooltip: 'Back', tooltip: 'Back',
), ),
title: Row( title: Text(
mainAxisSize: MainAxisSize.min, 'Workspace',
children: [ style: AppTypography.headlineSmallStyle.copyWith(
Text( color: context.conduitTheme.textPrimary,
'Files', fontWeight: FontWeight.w600,
style: context.conduitTheme.headingSmall?.copyWith( ),
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
),
],
), ),
centerTitle: true, centerTitle: true,
actions: [ actions: [
@@ -290,16 +286,8 @@ class _FilesPageState extends ConsumerState<FilesPage>
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Enhanced handle bar // Handle bar (standardized)
Container( const SheetHandle(),
margin: const EdgeInsets.symmetric(vertical: Spacing.sm),
width: 40,
height: 4,
decoration: BoxDecoration(
color: context.conduitTheme.dividerColor,
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
),
),
// Header with enhanced typography // Header with enhanced typography
Padding( Padding(

View File

@@ -7,12 +7,12 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/providers/app_providers.dart'; import '../../../core/providers/app_providers.dart';
import '../../../core/auth/auth_state_manager.dart';
import '../../../shared/theme/theme_extensions.dart'; import '../../../shared/theme/theme_extensions.dart';
import '../../chat/providers/chat_providers.dart' as chat; import '../../chat/providers/chat_providers.dart' as chat;
import '../../files/views/files_page.dart'; // import '../../files/views/files_page.dart';
import '../../profile/views/profile_page.dart'; import '../../profile/views/profile_page.dart';
import '../../../shared/utils/ui_utils.dart'; import '../../../shared/utils/ui_utils.dart';
import '../../../core/auth/auth_state_manager.dart';
class ChatsDrawer extends ConsumerStatefulWidget { class ChatsDrawer extends ConsumerStatefulWidget {
const ChatsDrawer({super.key}); const ChatsDrawer({super.key});
@@ -27,6 +27,9 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
Timer? _debounce; Timer? _debounce;
String _query = ''; String _query = '';
bool _isLoadingConversation = false; bool _isLoadingConversation = false;
String? _dragHoverFolderId;
bool _isDragging = false;
bool _draggingHasFolder = false;
// UI state providers for sections // UI state providers for sections
static final _showArchivedProvider = StateProvider<bool>((ref) => false); static final _showArchivedProvider = StateProvider<bool>((ref) => false);
@@ -49,8 +52,15 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
}); });
} }
// Payload for drag-and-drop of conversations
// Kept local to this widget
// ignore: unused_element
static _DragConversationData _dragData(String id, String title) =>
_DragConversationData(id: id, title: title);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Bottom section now only shows navigation actions
final theme = context.conduitTheme; final theme = context.conduitTheme;
return SafeArea( return SafeArea(
@@ -80,35 +90,33 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
final theme = context.conduitTheme; final theme = context.conduitTheme;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(Spacing.md, Spacing.md, Spacing.md, 0), padding: const EdgeInsets.fromLTRB(Spacing.md, Spacing.md, Spacing.md, 0),
child: Row( child: Stack(
alignment: Alignment.center,
children: [ children: [
Icon( // Centered title (no leading icon)
Platform.isIOS Text(
? CupertinoIcons.chat_bubble_2 'Chats',
: Icons.chat_bubble_outline_rounded, style: AppTypography.headlineSmallStyle.copyWith(
color: theme.iconPrimary, color: theme.textPrimary,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
), ),
const SizedBox(width: Spacing.sm), // Right-aligned new chat action
Expanded( Positioned(
child: Text( right: 0,
'Chats', child: IconButton(
style: AppTypography.headlineSmallStyle.copyWith( icon: Icon(
color: theme.textPrimary, Platform.isIOS ? CupertinoIcons.plus : Icons.add_rounded,
fontWeight: FontWeight.w600, color: theme.iconPrimary,
), ),
onPressed: () {
chat.startNewChat(ref);
if (mounted) Navigator.of(context).maybePop();
},
tooltip: 'New Chat',
), ),
), ),
IconButton(
icon: Icon(
Platform.isIOS ? CupertinoIcons.plus : Icons.add_rounded,
color: theme.iconPrimary,
),
onPressed: () {
chat.startNewChat(ref);
if (mounted) Navigator.of(context).maybePop();
},
tooltip: 'New Chat',
),
], ],
), ),
); );
@@ -234,8 +242,14 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
const SizedBox(height: Spacing.md), const SizedBox(height: Spacing.md),
], ],
if (foldered.isNotEmpty) ...[ // Folders section (shown even if empty)
...ref.watch(foldersProvider).when( _buildFoldersSectionHeader(),
const SizedBox(height: Spacing.xs),
if (_isDragging && _draggingHasFolder) ...[
_buildUnfileDropTarget(),
const SizedBox(height: Spacing.sm),
],
...ref.watch(foldersProvider).when(
data: (folders) { data: (folders) {
final grouped = <String, List<dynamic>>{}; final grouped = <String, List<dynamic>>{};
for (final c in foldered) { for (final c in foldered) {
@@ -243,18 +257,16 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
grouped.putIfAbsent(id, () => []).add(c); grouped.putIfAbsent(id, () => []).add(c);
} }
// Only show folders that have items // Show all folders (including empty)
final sections = folders final sections = folders.map((folder) {
.where((f) => grouped.containsKey(f.id))
.map((folder) {
final expandedMap = ref.watch(_expandedFoldersProvider); final expandedMap = ref.watch(_expandedFoldersProvider);
final isExpanded = expandedMap[folder.id] ?? false; final isExpanded = expandedMap[folder.id] ?? false;
final convs = grouped[folder.id]!; final convs = grouped[folder.id] ?? const <dynamic>[];
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_buildFolderHeader(folder.id, folder.name, convs.length), _buildFolderHeader(folder.id, folder.name, convs.length),
if (isExpanded) ...[ if (isExpanded && convs.isNotEmpty) ...[
const SizedBox(height: Spacing.xs), const SizedBox(height: Spacing.xs),
...convs.map((c) => _buildTileFor(c, inFolder: true)), ...convs.map((c) => _buildTileFor(c, inFolder: true)),
const SizedBox(height: Spacing.sm), const SizedBox(height: Spacing.sm),
@@ -262,13 +274,12 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
], ],
); );
}).toList(); }).toList();
return sections; return sections.isEmpty ? [const SizedBox.shrink()] : sections;
}, },
loading: () => [const SizedBox.shrink()], loading: () => [const SizedBox.shrink()],
error: (e, st) => [const SizedBox.shrink()], error: (e, st) => [const SizedBox.shrink()],
), ),
const SizedBox(height: Spacing.md), const SizedBox(height: Spacing.md),
],
if (regular.isNotEmpty) ...[ if (regular.isNotEmpty) ...[
_buildSectionHeader('Recent', regular.length), _buildSectionHeader('Recent', regular.length),
@@ -348,26 +359,30 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
...pinned.map((conv) => _buildTileFor(conv)), ...pinned.map((conv) => _buildTileFor(conv)),
const SizedBox(height: Spacing.md), const SizedBox(height: Spacing.md),
], ],
if (foldered.isNotEmpty) ...[ // Folders section (shown even if empty)
...ref.watch(foldersProvider).when( _buildFoldersSectionHeader(),
data: (folders) { const SizedBox(height: Spacing.xs),
final grouped = <String, List<dynamic>>{}; if (_isDragging && _draggingHasFolder) ...[
for (final c in foldered) { _buildUnfileDropTarget(),
final id = c.folderId!; const SizedBox(height: Spacing.sm),
grouped.putIfAbsent(id, () => []).add(c); ],
} ...ref.watch(foldersProvider).when(
data: (folders) {
final grouped = <String, List<dynamic>>{};
for (final c in foldered) {
final id = c.folderId!;
grouped.putIfAbsent(id, () => []).add(c);
}
final sections = folders final sections = folders.map((folder) {
.where((f) => grouped.containsKey(f.id))
.map((folder) {
final expandedMap = ref.watch(_expandedFoldersProvider); final expandedMap = ref.watch(_expandedFoldersProvider);
final isExpanded = expandedMap[folder.id] ?? false; final isExpanded = expandedMap[folder.id] ?? false;
final convs = grouped[folder.id]!; final convs = grouped[folder.id] ?? const <dynamic>[];
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_buildFolderHeader(folder.id, folder.name, convs.length), _buildFolderHeader(folder.id, folder.name, convs.length),
if (isExpanded) ...[ if (isExpanded && convs.isNotEmpty) ...[
const SizedBox(height: Spacing.xs), const SizedBox(height: Spacing.xs),
...convs.map((c) => _buildTileFor(c, inFolder: true)), ...convs.map((c) => _buildTileFor(c, inFolder: true)),
const SizedBox(height: Spacing.sm), const SizedBox(height: Spacing.sm),
@@ -375,13 +390,12 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
], ],
); );
}).toList(); }).toList();
return sections; return sections.isEmpty ? [const SizedBox.shrink()] : sections;
}, },
loading: () => [const SizedBox.shrink()], loading: () => [const SizedBox.shrink()],
error: (e, st) => [const SizedBox.shrink()], error: (e, st) => [const SizedBox.shrink()],
), ),
const SizedBox(height: Spacing.md), const SizedBox(height: Spacing.md),
],
if (regular.isNotEmpty) ...[ if (regular.isNotEmpty) ...[
_buildSectionHeader('Recent', regular.length), _buildSectionHeader('Recent', regular.length),
const SizedBox(height: Spacing.xs), const SizedBox(height: Spacing.xs),
@@ -440,24 +454,225 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
); );
} }
/// Header for the Folders section with a create button on the right
Widget _buildFoldersSectionHeader() {
final theme = context.conduitTheme;
return Row(
children: [
Text(
'Folders',
style: AppTypography.bodySmallStyle.copyWith(
fontWeight: FontWeight.w600,
color: theme.textSecondary,
letterSpacing: 0.2,
),
),
const Spacer(),
IconButton(
visualDensity: VisualDensity.compact,
tooltip: 'New Folder',
icon: Icon(
Platform.isIOS ? CupertinoIcons.folder_badge_plus : Icons.create_new_folder_outlined,
color: theme.iconPrimary,
),
onPressed: _promptCreateFolder,
),
],
);
}
Future<void> _promptCreateFolder() async {
final theme = context.conduitTheme;
final controller = TextEditingController();
final name = await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: theme.surfaceBackground,
title: Text('New Folder', style: TextStyle(color: theme.textPrimary)),
content: TextField(
controller: controller,
autofocus: true,
style: TextStyle(color: theme.inputText),
decoration: InputDecoration(
hintText: 'Folder name',
hintStyle: TextStyle(color: theme.inputPlaceholder),
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: theme.inputBorder)),
focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: theme.buttonPrimary)),
),
onSubmitted: (v) => Navigator.pop(ctx, controller.text.trim()),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, controller.text.trim()),
child: const Text('Create'),
),
],
),
);
if (name == null) return;
if (name.isEmpty) return;
try {
final api = ref.read(apiServiceProvider);
if (api == null) throw Exception('No API service');
await api.createFolder(name: name);
HapticFeedback.lightImpact();
ref.invalidate(foldersProvider);
if (!mounted) return;
UiUtils.showMessage(context, 'Folder created');
} catch (e) {
if (!mounted) return;
UiUtils.showMessage(context, 'Failed to create folder', isError: true);
}
}
Widget _buildFolderHeader(String folderId, String name, int count) { Widget _buildFolderHeader(String folderId, String name, int count) {
final theme = context.conduitTheme; final theme = context.conduitTheme;
final expandedMap = ref.watch(_expandedFoldersProvider); final expandedMap = ref.watch(_expandedFoldersProvider);
final isExpanded = expandedMap[folderId] ?? false; final isExpanded = expandedMap[folderId] ?? false;
return Material( final isHover = _dragHoverFolderId == folderId;
color: theme.surfaceBackground.withValues(alpha: 0.05), return DragTarget<_DragConversationData>(
shape: RoundedRectangleBorder( onWillAcceptWithDetails: (details) {
borderRadius: BorderRadius.circular(AppBorderRadius.md), setState(() => _dragHoverFolderId = folderId);
side: BorderSide(color: theme.dividerColor, width: BorderWidth.regular), return true;
), },
child: InkWell( onLeave: (_) => setState(() => _dragHoverFolderId = null),
borderRadius: BorderRadius.circular(AppBorderRadius.md), onAcceptWithDetails: (details) async {
onTap: () { setState(() {
final current = {...ref.read(_expandedFoldersProvider)}; _dragHoverFolderId = null;
current[folderId] = !isExpanded; _isDragging = false;
ref.read(_expandedFoldersProvider.notifier).state = current; });
}, try {
child: Padding( final api = ref.read(apiServiceProvider);
if (api == null) throw Exception('No API service');
await api.moveConversationToFolder(details.data.id, folderId);
HapticFeedback.selectionClick();
ref.invalidate(conversationsProvider);
ref.invalidate(foldersProvider);
if (mounted) {
UiUtils.showMessage(context, 'Moved "${details.data.title}" to "$name"');
}
} catch (_) {
if (mounted) {
UiUtils.showMessage(context, 'Failed to move chat', isError: true);
}
}
},
builder: (context, candidateData, rejectedData) {
return Material(
color: isHover
? theme.buttonPrimary.withValues(alpha: 0.08)
: theme.surfaceBackground.withValues(alpha: 0.05),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md),
side: BorderSide(
color: isHover
? theme.buttonPrimary.withValues(alpha: 0.6)
: theme.dividerColor,
width: BorderWidth.regular,
),
),
child: InkWell(
borderRadius: BorderRadius.circular(AppBorderRadius.md),
onTap: () {
final current = {...ref.read(_expandedFoldersProvider)};
current[folderId] = !isExpanded;
ref.read(_expandedFoldersProvider.notifier).state = current;
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.sm,
),
child: Row(
children: [
Icon(
isExpanded
? (Platform.isIOS ? CupertinoIcons.folder_open : Icons.folder_open)
: (Platform.isIOS ? CupertinoIcons.folder : Icons.folder),
color: theme.iconPrimary,
),
const SizedBox(width: Spacing.sm),
Expanded(
child: Text(
name,
style: AppTypography.bodyLargeStyle.copyWith(
color: theme.textPrimary,
fontWeight: FontWeight.w600,
),
),
),
Text(
'$count',
style: AppTypography.bodySmallStyle.copyWith(
color: theme.textSecondary,
),
),
const SizedBox(width: Spacing.xs),
Icon(
isExpanded
? (Platform.isIOS ? CupertinoIcons.chevron_up : Icons.expand_less)
: (Platform.isIOS ? CupertinoIcons.chevron_down : Icons.expand_more),
color: theme.iconSecondary,
)
],
),
),
),
);
},
);
}
Widget _buildUnfileDropTarget() {
final theme = context.conduitTheme;
final isHover = _dragHoverFolderId == '__UNFILE__';
return DragTarget<_DragConversationData>(
onWillAcceptWithDetails: (details) {
setState(() => _dragHoverFolderId = '__UNFILE__');
return true;
},
onLeave: (_) => setState(() => _dragHoverFolderId = null),
onAcceptWithDetails: (details) async {
setState(() {
_dragHoverFolderId = null;
_isDragging = false;
});
try {
final api = ref.read(apiServiceProvider);
if (api == null) throw Exception('No API service');
await api.moveConversationToFolder(details.data.id, null);
HapticFeedback.selectionClick();
ref.invalidate(conversationsProvider);
ref.invalidate(foldersProvider);
if (mounted) {
UiUtils.showMessage(context, 'Removed "${details.data.title}" from folder');
}
} catch (_) {
if (mounted) {
UiUtils.showMessage(context, 'Failed to move chat', isError: true);
}
}
},
builder: (context, candidate, rejected) {
return AnimatedContainer(
duration: const Duration(milliseconds: 120),
decoration: BoxDecoration(
color: isHover
? theme.buttonPrimary.withValues(alpha: 0.08)
: theme.surfaceBackground.withValues(alpha: 0.03),
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: isHover
? theme.buttonPrimary.withValues(alpha: 0.6)
: theme.dividerColor,
width: BorderWidth.regular,
),
),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: Spacing.md, horizontal: Spacing.md,
vertical: Spacing.sm, vertical: Spacing.sm,
@@ -465,58 +680,102 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
child: Row( child: Row(
children: [ children: [
Icon( Icon(
isExpanded Platform.isIOS
? (Platform.isIOS ? CupertinoIcons.folder_open : Icons.folder_open) ? CupertinoIcons.folder_badge_minus
: (Platform.isIOS ? CupertinoIcons.folder : Icons.folder), : Icons.folder_off_outlined,
color: theme.iconPrimary, color: theme.iconPrimary,
), ),
const SizedBox(width: Spacing.sm), const SizedBox(width: Spacing.sm),
Expanded( Expanded(
child: Text( child: Text(
name, 'Drop here to remove from folder',
style: AppTypography.bodyLargeStyle.copyWith( style: AppTypography.bodyMediumStyle.copyWith(
color: theme.textPrimary, color: theme.textPrimary,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
), ),
Text(
'$count',
style: AppTypography.bodySmallStyle.copyWith(
color: theme.textSecondary,
),
),
const SizedBox(width: Spacing.xs),
Icon(
isExpanded
? (Platform.isIOS ? CupertinoIcons.chevron_up : Icons.expand_less)
: (Platform.isIOS ? CupertinoIcons.chevron_down : Icons.expand_more),
color: theme.iconSecondary,
)
], ],
), ),
), );
), },
); );
} }
Widget _buildTileFor(dynamic conv, {bool inFolder = false}) { Widget _buildTileFor(dynamic conv, {bool inFolder = false}) {
final isActive = ref.watch(activeConversationProvider)?.id == conv.id; final isActive = ref.watch(activeConversationProvider)?.id == conv.id;
final title = conv.title?.isEmpty == true ? 'Chat' : (conv.title ?? 'Chat');
final tile = _ConversationTile(
title: title,
pinned: conv.pinned == true,
selected: isActive,
onTap: _isLoadingConversation ? null : () => _selectConversation(context, conv.id),
// Remove long-press context menu to avoid conflict with drag gesture
onLongPress: null,
onMorePressed: () {
HapticFeedback.selectionClick();
_showConversationContextMenu(context, conv);
},
);
return Padding( return Padding(
padding: EdgeInsets.only(bottom: Spacing.xs, left: inFolder ? Spacing.md : 0), padding: EdgeInsets.only(bottom: Spacing.xs, left: inFolder ? Spacing.md : 0),
child: _ConversationTile( child: LongPressDraggable<_DragConversationData>(
title: conv.title?.isEmpty == true ? 'Chat' : (conv.title ?? 'Chat'), data: _DragConversationData(id: conv.id, title: title),
pinned: conv.pinned == true, dragAnchorStrategy: pointerDragAnchorStrategy,
selected: isActive, feedback: Material(
onTap: _isLoadingConversation ? null : () => _selectConversation(context, conv.id), color: Colors.transparent,
onLongPress: () { elevation: 6,
HapticFeedback.selectionClick(); borderRadius: BorderRadius.circular(AppBorderRadius.md),
_showConversationContextMenu(context, conv); child: Opacity(
}, opacity: 0.9,
onMorePressed: () { child: Container(
HapticFeedback.selectionClick(); padding: const EdgeInsets.symmetric(
_showConversationContextMenu(context, conv); horizontal: Spacing.md,
vertical: Spacing.sm,
),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: Theme.of(context).dividerColor,
width: BorderWidth.regular,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Platform.isIOS
? CupertinoIcons.chat_bubble_2
: Icons.chat_bubble_outline,
size: IconSize.md,
),
const SizedBox(width: Spacing.xs),
Text(title, maxLines: 1, overflow: TextOverflow.ellipsis),
],
),
),
),
),
childWhenDragging: Opacity(
opacity: 0.5,
child: IgnorePointer(child: tile),
),
onDragStarted: () {
HapticFeedback.lightImpact();
final hasFolder = (conv.folderId != null && (conv.folderId as String).isNotEmpty);
setState(() {
_isDragging = true;
_draggingHasFolder = hasFolder;
});
}, },
onDragEnd: (_) => setState(() {
_dragHoverFolderId = null;
_isDragging = false;
_draggingHasFolder = false;
}),
child: tile,
), ),
); );
} }
@@ -632,6 +891,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (user != null) ...[ if (user != null) ...[
const SizedBox(height: Spacing.sm),
Container( Container(
padding: const EdgeInsets.all(Spacing.sm), padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -693,37 +953,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
], ],
), ),
), ),
const SizedBox(height: Spacing.sm),
], ],
Row(
children: [
Expanded(
child: _BottomAction(
icon: Platform.isIOS ? CupertinoIcons.doc : Icons.description_outlined,
label: 'Files',
onTap: () {
Navigator.of(context).maybePop();
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const FilesPage()),
);
},
),
),
const SizedBox(width: Spacing.sm),
Expanded(
child: _BottomAction(
icon: Platform.isIOS ? CupertinoIcons.person : Icons.person_outline,
label: 'Profile',
onTap: () {
Navigator.of(context).maybePop();
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ProfilePage()),
);
},
),
),
],
),
], ],
), ),
), ),
@@ -918,6 +1148,12 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
} }
} }
class _DragConversationData {
final String id;
final String title;
const _DragConversationData({required this.id, required this.title});
}
class _ConversationTile extends StatelessWidget { class _ConversationTile extends StatelessWidget {
final String title; final String title;
final bool pinned; final bool pinned;
@@ -960,14 +1196,6 @@ class _ConversationTile extends StatelessWidget {
), ),
child: Row( child: Row(
children: [ children: [
Icon(
pinned
? (Platform.isIOS ? CupertinoIcons.pin_fill : Icons.push_pin)
: (Platform.isIOS ? CupertinoIcons.chat_bubble : Icons.chat_bubble_outline_rounded),
color: selected ? theme.buttonPrimary : theme.iconSecondary,
size: IconSize.md,
),
const SizedBox(width: Spacing.sm),
Expanded( Expanded(
child: Text( child: Text(
title, title,
@@ -1001,46 +1229,4 @@ class _ConversationTile extends StatelessWidget {
} }
} }
class _BottomAction extends StatelessWidget { // Bottom quick actions widget removed as design now shows only profile card
final IconData icon;
final String label;
final VoidCallback onTap;
const _BottomAction({
required this.icon,
required this.label,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = context.conduitTheme;
return Material(
color: theme.surfaceBackground.withValues(alpha: 0.04),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md),
side: BorderSide(color: theme.dividerColor, width: BorderWidth.regular),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppBorderRadius.md),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: Spacing.sm),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: theme.iconPrimary, size: IconSize.lg),
const SizedBox(height: 6),
Text(
label,
style: AppTypography.bodySmallStyle.copyWith(
color: theme.textPrimary,
),
),
],
),
),
),
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import '../../../shared/theme/theme_extensions.dart'; import '../../../shared/theme/theme_extensions.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import '../../../shared/widgets/sheet_handle.dart';
class OnboardingSheet extends StatefulWidget { class OnboardingSheet extends StatefulWidget {
const OnboardingSheet({super.key}); const OnboardingSheet({super.key});
@@ -43,10 +44,10 @@ class _OnboardingSheetState extends State<OnboardingSheet> {
_OnboardingPage( _OnboardingPage(
title: 'Quick actions', title: 'Quick actions',
subtitle: subtitle:
'Longpress the topleft menu to open shortcuts like New Chat, Files, and Profile.', 'Use the topleft menu to open the chats list and navigation.',
icon: CupertinoIcons.line_horizontal_3, icon: CupertinoIcons.line_horizontal_3,
bullets: [ bullets: [
'Tap to open chats list; longpress for Quick Actions', 'Tap the menu to open the chats list and navigation',
'Jump instantly to New Chat, Files, or Profile', 'Jump instantly to New Chat, Files, or Profile',
], ],
), ),
@@ -81,16 +82,8 @@ class _OnboardingSheetState extends State<OnboardingSheet> {
padding: const EdgeInsets.all(Spacing.lg), padding: const EdgeInsets.all(Spacing.lg),
child: Column( child: Column(
children: [ children: [
// Handle bar // Handle bar (standardized)
Container( const SheetHandle(),
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: Spacing.md),
decoration: BoxDecoration(
color: context.conduitTheme.dividerColor,
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
),
),
Expanded( Expanded(
child: PageView.builder( child: PageView.builder(

View File

@@ -10,6 +10,7 @@ import '../../../core/widgets/error_boundary.dart';
import '../../../shared/widgets/improved_loading_states.dart'; import '../../../shared/widgets/improved_loading_states.dart';
import '../../../shared/utils/ui_utils.dart'; import '../../../shared/utils/ui_utils.dart';
import '../../../shared/widgets/sheet_handle.dart';
import '../../../shared/widgets/conduit_components.dart'; import '../../../shared/widgets/conduit_components.dart';
import '../../../core/providers/app_providers.dart'; import '../../../core/providers/app_providers.dart';
import '../../auth/providers/unified_auth_providers.dart'; import '../../auth/providers/unified_auth_providers.dart';
@@ -49,17 +50,12 @@ class ProfilePage extends ConsumerWidget {
), ),
toolbarHeight: kToolbarHeight, toolbarHeight: kToolbarHeight,
titleSpacing: 0.0, titleSpacing: 0.0,
title: Row( title: Text(
mainAxisSize: MainAxisSize.min, 'You',
children: [ style: AppTypography.headlineSmallStyle.copyWith(
Text( color: context.conduitTheme.textPrimary,
'You', fontWeight: FontWeight.w600,
style: context.conduitTheme.headingSmall?.copyWith( ),
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
),
],
), ),
centerTitle: true, centerTitle: true,
), ),
@@ -114,7 +110,7 @@ class ProfilePage extends ConsumerWidget {
), ),
title: Text( title: Text(
'You', 'You',
style: context.conduitTheme.headingSmall?.copyWith( style: AppTypography.headlineSmallStyle.copyWith(
color: context.conduitTheme.textPrimary, color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
@@ -144,7 +140,7 @@ class ProfilePage extends ConsumerWidget {
), ),
title: Text( title: Text(
'You', 'You',
style: context.conduitTheme.headingSmall?.copyWith( style: AppTypography.headlineSmallStyle.copyWith(
color: context.conduitTheme.textPrimary, color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
@@ -195,56 +191,14 @@ class ProfilePage extends ConsumerWidget {
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(height: Spacing.xs), const SizedBox(height: Spacing.sm),
Text( Text(
user?.email ?? 'No email', user?.email ?? 'No email',
style: context.conduitTheme.bodyMedium?.copyWith( style: context.conduitTheme.bodyMedium?.copyWith(
color: context.conduitTheme.textSecondary, color: context.conduitTheme.textSecondary,
), ),
), ),
const SizedBox(height: Spacing.sm), // Status badge removed per design update
// Enhanced status badge with better styling
Container(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.sm,
vertical: Spacing.xs,
),
decoration: BoxDecoration(
color: context.conduitTheme.success.withValues(
alpha: Alpha.badgeBackground,
),
borderRadius: BorderRadius.circular(
AppBorderRadius.badge,
),
border: Border.all(
color: context.conduitTheme.success.withValues(
alpha: Alpha.avatarBorder,
),
width: BorderWidth.thin,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: context.conduitTheme.success,
shape: BoxShape.circle,
),
),
const SizedBox(width: Spacing.xs),
Text(
'Active',
style: context.conduitTheme.label?.copyWith(
color: context.conduitTheme.success,
fontWeight: FontWeight.w600,
),
),
],
),
),
], ],
), ),
), ),
@@ -789,55 +743,11 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe
padding: const EdgeInsets.all(Spacing.bottomSheetPadding), padding: const EdgeInsets.all(Spacing.bottomSheetPadding),
child: Column( child: Column(
children: [ children: [
// Handle bar // Handle bar (standardized)
Container( const SheetHandle(),
margin: const EdgeInsets.only(
top: Spacing.sm,
bottom: Spacing.md,
),
width: Spacing.xxl,
height: Spacing.xs,
decoration: BoxDecoration(
color: context.conduitTheme.textPrimary.withValues(alpha: Alpha.medium),
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
),
),
// Header // Header removed (no icon/title or save button)
Padding( const SizedBox(height: Spacing.md),
padding: const EdgeInsets.only(bottom: Spacing.md),
child: Row(
children: [
Icon(
Platform.isIOS ? CupertinoIcons.cube : Icons.psychology,
color: context.conduitTheme.iconPrimary,
),
const SizedBox(width: Spacing.sm),
Expanded(
child: Text(
'Default Model',
style: context.conduitTheme.headingMedium?.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
),
),
TextButton(
onPressed: () {
HapticFeedback.lightImpact();
Navigator.pop(context, _selectedModelId);
},
child: Text(
'Save',
style: context.conduitTheme.bodyMedium?.copyWith(
color: context.conduitTheme.buttonPrimary,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
// Search field // Search field
Padding( Padding(
@@ -963,10 +873,11 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe
isSelected: isSelected, isSelected: isSelected,
isAutoSelect: isAutoSelect, isAutoSelect: isAutoSelect,
onTap: () { onTap: () {
HapticFeedback.selectionClick(); HapticFeedback.lightImpact();
setState(() { final selectedId =
_selectedModelId = isAutoSelect ? 'auto-select' : model.id; isAutoSelect ? 'auto-select' : model.id;
}); // Return selection immediately; caller handles persisting
Navigator.pop(context, selectedId);
}, },
); );
}, },

View File

@@ -8,6 +8,7 @@ import '../../../shared/theme/theme_extensions.dart';
import '../../chat/providers/chat_providers.dart'; import '../../chat/providers/chat_providers.dart';
import '../../../core/providers/app_providers.dart'; import '../../../core/providers/app_providers.dart';
import '../providers/tools_providers.dart'; import '../providers/tools_providers.dart';
import '../../../shared/widgets/sheet_handle.dart';
class UnifiedToolsModal extends ConsumerStatefulWidget { class UnifiedToolsModal extends ConsumerStatefulWidget {
const UnifiedToolsModal({super.key}); const UnifiedToolsModal({super.key});
@@ -47,17 +48,8 @@ class _UnifiedToolsModalState extends ConsumerState<UnifiedToolsModal> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Handle bar // Handle bar (standardized)
Center( const SheetHandle(),
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: theme.textPrimary.withValues(alpha: Alpha.medium),
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: Spacing.md), const SizedBox(height: Spacing.md),
// Removed header for minimal, focused layout // Removed header for minimal, focused layout

View File

@@ -0,0 +1,23 @@
import 'package:flutter/widgets.dart';
import '../theme/theme_extensions.dart';
class SheetHandle extends StatelessWidget {
final EdgeInsetsGeometry? margin;
const SheetHandle({super.key, this.margin});
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: margin ?? const EdgeInsets.only(top: Spacing.sm, bottom: Spacing.md),
width: 40,
height: 4,
decoration: BoxDecoration(
color: context.conduitTheme.textPrimary.withValues(alpha: Alpha.medium),
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
),
),
);
}
}