refactor: visual tweaks
This commit is contained in:
@@ -38,6 +38,7 @@ import '../../../shared/widgets/sheet_handle.dart';
|
||||
import '../../../shared/widgets/measure_size.dart';
|
||||
import '../../../shared/widgets/conduit_components.dart';
|
||||
import '../../../shared/widgets/middle_ellipsis_text.dart';
|
||||
import '../../../shared/widgets/modal_safe_area.dart';
|
||||
import '../../../core/services/settings_service.dart';
|
||||
// Removed unused PlatformUtils import
|
||||
import '../../../core/services/platform_service.dart' as ps;
|
||||
@@ -1654,19 +1655,24 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
|
||||
}
|
||||
|
||||
void _filterModels(String query) {
|
||||
// Debounce for fast search
|
||||
setState(() => _searchQuery = query);
|
||||
|
||||
_searchDebounce?.cancel();
|
||||
_searchDebounce = Timer(const Duration(milliseconds: 160), () {
|
||||
setState(() {
|
||||
_searchQuery = query.toLowerCase();
|
||||
if (!mounted) return;
|
||||
|
||||
final normalized = query.trim().toLowerCase();
|
||||
Iterable<Model> list = widget.models;
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
|
||||
if (normalized.isNotEmpty) {
|
||||
list = list.where((model) {
|
||||
return model.name.toLowerCase().contains(_searchQuery) ||
|
||||
model.id.toLowerCase().contains(_searchQuery);
|
||||
final name = model.name.toLowerCase();
|
||||
final id = model.id.toLowerCase();
|
||||
return name.contains(normalized) || id.contains(normalized);
|
||||
});
|
||||
}
|
||||
// No capability filters
|
||||
|
||||
setState(() {
|
||||
_filteredModels = list.toList();
|
||||
});
|
||||
});
|
||||
@@ -1674,7 +1680,17 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DraggableScrollableSheet(
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => Navigator.of(context).maybePop(),
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.75,
|
||||
maxChildSize: 0.92,
|
||||
minChildSize: 0.45,
|
||||
@@ -1691,11 +1707,11 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
|
||||
),
|
||||
boxShadow: ConduitShadows.modal,
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(Spacing.bottomSheetPadding),
|
||||
child: ModalSheetSafeArea(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.modalPadding,
|
||||
vertical: Spacing.modalPadding,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Handle bar (standardized)
|
||||
@@ -1706,15 +1722,45 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
|
||||
padding: const EdgeInsets.only(bottom: Spacing.md),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
style: TextStyle(color: context.conduitTheme.textPrimary),
|
||||
style: AppTypography.standard.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
),
|
||||
onChanged: _filterModels,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
hintText: AppLocalizations.of(context)!.searchModels,
|
||||
hintStyle: TextStyle(
|
||||
hintStyle: AppTypography.standard.copyWith(
|
||||
color: context.conduitTheme.inputPlaceholder,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Platform.isIOS ? CupertinoIcons.search : Icons.search,
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.search
|
||||
: Icons.search,
|
||||
color: context.conduitTheme.iconSecondary,
|
||||
size: IconSize.input,
|
||||
),
|
||||
prefixIconConstraints: const BoxConstraints(
|
||||
minWidth: TouchTarget.minimum,
|
||||
minHeight: TouchTarget.minimum,
|
||||
),
|
||||
suffixIcon: _searchQuery.isNotEmpty
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
_filterModels('');
|
||||
},
|
||||
icon: Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.clear_circled_solid
|
||||
: Icons.clear,
|
||||
color: context.conduitTheme.iconSecondary,
|
||||
size: IconSize.input,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
suffixIconConstraints: const BoxConstraints(
|
||||
minWidth: TouchTarget.minimum,
|
||||
minHeight: TouchTarget.minimum,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: context.conduitTheme.inputBackground,
|
||||
@@ -1744,10 +1790,9 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.md,
|
||||
vertical: Spacing.md,
|
||||
vertical: Spacing.xs,
|
||||
),
|
||||
),
|
||||
onChanged: _filterModels,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1774,7 +1819,8 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
|
||||
Text(
|
||||
'No results',
|
||||
style: TextStyle(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
color:
|
||||
context.conduitTheme.textSecondary,
|
||||
fontSize: AppTypography.bodyLarge,
|
||||
),
|
||||
),
|
||||
@@ -1814,9 +1860,10 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
import 'dart:math' as math;
|
||||
import '../providers/chat_providers.dart';
|
||||
import '../../tools/providers/tools_providers.dart';
|
||||
import '../../../core/models/tool.dart';
|
||||
@@ -19,6 +20,7 @@ import '../../chat/services/voice_input_service.dart';
|
||||
|
||||
import '../../../shared/utils/platform_utils.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
import '../../../shared/widgets/modal_safe_area.dart';
|
||||
|
||||
class _SendMessageIntent extends Intent {
|
||||
const _SendMessageIntent();
|
||||
@@ -1044,6 +1046,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
builder: (modalContext) => Consumer(
|
||||
builder: (innerContext, modalRef, _) {
|
||||
final l10n = AppLocalizations.of(innerContext)!;
|
||||
@@ -1188,6 +1191,58 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
..add(_buildSectionLabel(l10n.tools))
|
||||
..add(toolsSection);
|
||||
|
||||
// Measure content height and cap the sheet's max size to avoid extra blank space
|
||||
final GlobalKey sheetContentKey = GlobalKey();
|
||||
double? measuredContentHeight;
|
||||
|
||||
return StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
// Schedule a post-frame measurement of the content height
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final ctx = sheetContentKey.currentContext;
|
||||
if (ctx != null) {
|
||||
final renderObject = ctx.findRenderObject();
|
||||
if (renderObject is RenderBox) {
|
||||
final double h = renderObject.size.height;
|
||||
if (h > 0 && h != measuredContentHeight) {
|
||||
measuredContentHeight = h;
|
||||
setModalState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
final media = MediaQuery.of(modalContext);
|
||||
final double availableHeight =
|
||||
media.size.height - media.padding.top;
|
||||
|
||||
double computedMax = 0.9;
|
||||
if (measuredContentHeight != null && availableHeight > 0) {
|
||||
computedMax = (measuredContentHeight! / availableHeight).clamp(
|
||||
0.1,
|
||||
0.9,
|
||||
);
|
||||
}
|
||||
final double computedMin = math.min(0.25, computedMax);
|
||||
final double computedInitial = math.min(0.4, computedMax);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => Navigator.of(modalContext).maybePop(),
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: computedInitial,
|
||||
minChildSize: computedMin,
|
||||
maxChildSize: computedMax,
|
||||
snap: true,
|
||||
snapSizes: [computedMax],
|
||||
builder: (sheetContext, scrollController) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.surfaceBackground,
|
||||
@@ -1200,17 +1255,12 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
),
|
||||
boxShadow: ConduitShadows.modal,
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: ModalSheetSafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
Spacing.modalPadding,
|
||||
Spacing.sm,
|
||||
Spacing.modalPadding,
|
||||
Spacing.modalPadding,
|
||||
),
|
||||
controller: scrollController,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
key: sheetContentKey,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: bodyChildren,
|
||||
),
|
||||
@@ -1219,6 +1269,12 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
).whenComplete(() {
|
||||
if (mounted) {
|
||||
_focusNode.canRequestFocus = prevCanRequest;
|
||||
@@ -1308,7 +1364,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
boxShadow: value ? ConduitShadows.low : const [],
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildToolGlyph(icon: icon, selected: value, theme: theme),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
@@ -1316,23 +1372,14 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
Text(
|
||||
title,
|
||||
style: AppTypography.bodyLargeStyle.copyWith(
|
||||
style: AppTypography.bodySmallStyle.copyWith(
|
||||
color: theme.textPrimary,
|
||||
fontWeight: value
|
||||
? FontWeight.w600
|
||||
: FontWeight.w500,
|
||||
fontWeight: value ? FontWeight.w600 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Spacing.xs),
|
||||
_buildTogglePill(isOn: value, theme: theme),
|
||||
],
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (description.isNotEmpty) ...[
|
||||
const SizedBox(height: Spacing.xs),
|
||||
@@ -1340,7 +1387,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
description,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppTypography.bodySmallStyle.copyWith(
|
||||
style: AppTypography.captionStyle.copyWith(
|
||||
color: theme.textSecondary.withValues(
|
||||
alpha: Alpha.strong,
|
||||
),
|
||||
@@ -1350,6 +1397,8 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
_buildTogglePill(isOn: value, theme: theme),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1402,7 +1451,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
boxShadow: selected ? ConduitShadows.low : const [],
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildToolGlyph(
|
||||
icon: _toolIconFor(tool),
|
||||
@@ -1414,23 +1463,16 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
Text(
|
||||
tool.name,
|
||||
style: AppTypography.bodyLargeStyle.copyWith(
|
||||
style: AppTypography.bodySmallStyle.copyWith(
|
||||
color: theme.textPrimary,
|
||||
fontWeight: selected
|
||||
? FontWeight.w600
|
||||
: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Spacing.xs),
|
||||
_buildTogglePill(isOn: selected, theme: theme),
|
||||
],
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (description.isNotEmpty) ...[
|
||||
const SizedBox(height: Spacing.xs),
|
||||
@@ -1438,7 +1480,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
description,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppTypography.bodySmallStyle.copyWith(
|
||||
style: AppTypography.captionStyle.copyWith(
|
||||
color: theme.textSecondary.withValues(
|
||||
alpha: Alpha.strong,
|
||||
),
|
||||
@@ -1448,6 +1490,8 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
_buildTogglePill(isOn: selected, theme: theme),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/providers/app_providers.dart';
|
||||
import '../../../shared/theme/theme_extensions.dart';
|
||||
import '../../../shared/widgets/modal_safe_area.dart';
|
||||
import '../../chat/providers/chat_providers.dart' as chat;
|
||||
// import '../../files/views/files_page.dart';
|
||||
import '../../profile/views/profile_page.dart';
|
||||
@@ -798,6 +799,9 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
String folderName,
|
||||
) {
|
||||
final theme = context.conduitTheme;
|
||||
// Ensure consistent modal padding/insets across the app
|
||||
// ignore: unnecessary_import
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: theme.surfaceBackground,
|
||||
@@ -807,7 +811,11 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
),
|
||||
),
|
||||
builder: (sheetContext) {
|
||||
return SafeArea(
|
||||
return ModalSheetSafeArea(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.modalPadding,
|
||||
vertical: Spacing.modalPadding,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -1329,7 +1337,11 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
),
|
||||
),
|
||||
builder: (sheetContext) {
|
||||
return SafeArea(
|
||||
return ModalSheetSafeArea(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.modalPadding,
|
||||
vertical: Spacing.modalPadding,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
@@ -22,6 +22,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import '../../chat/views/chat_page_helpers.dart';
|
||||
import 'app_customization_page.dart';
|
||||
import '../../../shared/widgets/modal_safe_area.dart';
|
||||
|
||||
/// Profile page (You tab) showing user info and main actions
|
||||
/// Enhanced with production-grade design tokens for better cohesion
|
||||
@@ -236,7 +237,9 @@ class ProfilePage extends ConsumerWidget {
|
||||
android: Icons.tune,
|
||||
),
|
||||
title: AppLocalizations.of(context)!.appCustomization,
|
||||
subtitle: AppLocalizations.of(context)!.appCustomizationSubtitle,
|
||||
subtitle: AppLocalizations.of(
|
||||
context,
|
||||
)!.appCustomizationSubtitle,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
@@ -334,7 +337,10 @@ class ProfilePage extends ConsumerWidget {
|
||||
(m) => m.id == settings.defaultModel,
|
||||
orElse: () => models.isNotEmpty
|
||||
? models.first
|
||||
: Model(id: 'none', name: AppLocalizations.of(context)!.noModelsAvailable),
|
||||
: Model(
|
||||
id: 'none',
|
||||
name: AppLocalizations.of(context)!.noModelsAvailable,
|
||||
),
|
||||
);
|
||||
|
||||
return ListTile(
|
||||
@@ -498,7 +504,9 @@ class ProfilePage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(ctx)!.versionLabel(info.version, info.buildNumber),
|
||||
AppLocalizations.of(
|
||||
ctx,
|
||||
)!.versionLabel(info.version, info.buildNumber),
|
||||
style: ctx.conduitTheme.bodyMedium?.copyWith(
|
||||
color: ctx.conduitTheme.textSecondary,
|
||||
),
|
||||
@@ -544,7 +552,10 @@ class ProfilePage extends ConsumerWidget {
|
||||
);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
UiUtils.showMessage(context, AppLocalizations.of(context)!.unableToLoadAppInfo);
|
||||
UiUtils.showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!.unableToLoadAppInfo,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,10 +658,7 @@ class _DefaultModelBottomSheetState
|
||||
// If no default model is set (null), default to auto-select
|
||||
_selectedModelId = widget.currentDefaultModelId ?? 'auto-select';
|
||||
// Add auto-select as first item
|
||||
_filteredModels = [
|
||||
const Model(id: 'auto-select', name: 'Auto-select'),
|
||||
...widget.models,
|
||||
];
|
||||
_filteredModels = _allModels();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -660,31 +668,49 @@ class _DefaultModelBottomSheetState
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _filterModels(String query) {
|
||||
_searchDebounce?.cancel();
|
||||
_searchDebounce = Timer(const Duration(milliseconds: 160), () {
|
||||
setState(() {
|
||||
_searchQuery = query.toLowerCase();
|
||||
List<Model> allModels = [
|
||||
List<Model> _allModels() {
|
||||
return [
|
||||
const Model(id: 'auto-select', name: 'Auto-select'),
|
||||
...widget.models,
|
||||
];
|
||||
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
_filteredModels = allModels.where((model) {
|
||||
return model.name.toLowerCase().contains(_searchQuery) ||
|
||||
model.id.toLowerCase().contains(_searchQuery);
|
||||
}).toList();
|
||||
} else {
|
||||
_filteredModels = allModels;
|
||||
}
|
||||
|
||||
void _filterModels(String query) {
|
||||
setState(() => _searchQuery = query);
|
||||
|
||||
_searchDebounce?.cancel();
|
||||
_searchDebounce = Timer(const Duration(milliseconds: 160), () {
|
||||
if (!mounted) return;
|
||||
|
||||
final normalized = query.trim().toLowerCase();
|
||||
final allModels = _allModels();
|
||||
final filtered = normalized.isEmpty
|
||||
? allModels
|
||||
: allModels.where((model) {
|
||||
final name = model.name.toLowerCase();
|
||||
final id = model.id.toLowerCase();
|
||||
return name.contains(normalized) || id.contains(normalized);
|
||||
}).toList();
|
||||
|
||||
setState(() {
|
||||
_filteredModels = filtered;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DraggableScrollableSheet(
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => Navigator.of(context).maybePop(),
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.75,
|
||||
maxChildSize: 0.92,
|
||||
minChildSize: 0.45,
|
||||
@@ -701,11 +727,11 @@ class _DefaultModelBottomSheetState
|
||||
),
|
||||
boxShadow: ConduitShadows.modal,
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(Spacing.bottomSheetPadding),
|
||||
child: ModalSheetSafeArea(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.modalPadding,
|
||||
vertical: Spacing.modalPadding,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Handle bar (standardized)
|
||||
@@ -719,15 +745,45 @@ class _DefaultModelBottomSheetState
|
||||
padding: const EdgeInsets.only(bottom: Spacing.md),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
style: TextStyle(color: context.conduitTheme.textPrimary),
|
||||
style: AppTypography.standard.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
),
|
||||
onChanged: _filterModels,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
hintText: AppLocalizations.of(context)!.searchModels,
|
||||
hintStyle: TextStyle(
|
||||
hintStyle: AppTypography.standard.copyWith(
|
||||
color: context.conduitTheme.inputPlaceholder,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Platform.isIOS ? CupertinoIcons.search : Icons.search,
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.search
|
||||
: Icons.search,
|
||||
color: context.conduitTheme.iconSecondary,
|
||||
size: IconSize.input,
|
||||
),
|
||||
prefixIconConstraints: const BoxConstraints(
|
||||
minWidth: TouchTarget.minimum,
|
||||
minHeight: TouchTarget.minimum,
|
||||
),
|
||||
suffixIcon: _searchQuery.isNotEmpty
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
_filterModels('');
|
||||
},
|
||||
icon: Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.clear_circled_solid
|
||||
: Icons.clear,
|
||||
color: context.conduitTheme.iconSecondary,
|
||||
size: IconSize.input,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
suffixIconConstraints: const BoxConstraints(
|
||||
minWidth: TouchTarget.minimum,
|
||||
minHeight: TouchTarget.minimum,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: context.conduitTheme.inputBackground,
|
||||
@@ -757,10 +813,9 @@ class _DefaultModelBottomSheetState
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.md,
|
||||
vertical: Spacing.md,
|
||||
vertical: Spacing.xs,
|
||||
),
|
||||
),
|
||||
onChanged: _filterModels,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -827,7 +882,8 @@ class _DefaultModelBottomSheetState
|
||||
Text(
|
||||
AppLocalizations.of(context)!.noResults,
|
||||
style: TextStyle(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
color:
|
||||
context.conduitTheme.textSecondary,
|
||||
fontSize: AppTypography.bodyLarge,
|
||||
),
|
||||
),
|
||||
@@ -840,7 +896,8 @@ class _DefaultModelBottomSheetState
|
||||
itemCount: _filteredModels.length,
|
||||
itemBuilder: (context, index) {
|
||||
final model = _filteredModels[index];
|
||||
final isAutoSelect = model.id == 'auto-select';
|
||||
final isAutoSelect =
|
||||
model.id == 'auto-select';
|
||||
final isSelected = isAutoSelect
|
||||
? _selectedModelId == null ||
|
||||
_selectedModelId == 'auto-select'
|
||||
@@ -855,7 +912,6 @@ class _DefaultModelBottomSheetState
|
||||
final selectedId = isAutoSelect
|
||||
? 'auto-select'
|
||||
: model.id;
|
||||
// Return selection immediately; caller handles persisting
|
||||
Navigator.pop(context, selectedId);
|
||||
},
|
||||
);
|
||||
@@ -866,9 +922,10 @@ class _DefaultModelBottomSheetState
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
37
lib/shared/widgets/modal_safe_area.dart
Normal file
37
lib/shared/widgets/modal_safe_area.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../theme/theme_extensions.dart';
|
||||
|
||||
/// Consistent safe area wrapper for modal sheets presented across the app.
|
||||
///
|
||||
/// All modal-bottom sheets should rely on this widget to guarantee that
|
||||
/// system insets (e.g. gesture areas or dynamic island) are respected while
|
||||
/// maintaining the same padding rhythm used by the attachments sheet.
|
||||
class ModalSheetSafeArea extends StatelessWidget {
|
||||
const ModalSheetSafeArea({super.key, required this.child, this.padding});
|
||||
|
||||
/// Content rendered inside the safe area.
|
||||
final Widget child;
|
||||
|
||||
/// Optional custom padding that wraps the [child]. When omitted the default
|
||||
/// modal spacing used by attachments/chat input is applied.
|
||||
final EdgeInsets? padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final resolvedPadding =
|
||||
padding ??
|
||||
const EdgeInsets.fromLTRB(
|
||||
Spacing.modalPadding,
|
||||
Spacing.sm,
|
||||
Spacing.modalPadding,
|
||||
Spacing.modalPadding,
|
||||
);
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: Padding(padding: resolvedPadding, child: child),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user