Overlays
BookWish uses bottom sheet overlays for contextual content and actions, providing a non-intrusive way to display detailed information without full page navigation.
Overlay Pattern
All overlays use the same consistent pattern with DraggableScrollableSheet inside showModalBottomSheet.
Base Implementation
void showExampleOverlay(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.9,
minChildSize: 0.5,
maxChildSize: 0.95,
builder: (context, scrollController) => ExampleOverlay(
scrollController: scrollController,
),
),
);
}
class ExampleOverlay extends ConsumerWidget {
final ScrollController? scrollController;
const ExampleOverlay({super.key, this.scrollController});
Widget build(BuildContext context, WidgetRef ref) {
return Container(
decoration: BoxDecoration(
color: AppColors.parchment,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: ListView(
controller: scrollController,
padding: const EdgeInsets.all(16),
children: [
// Overlay content
],
),
);
}
}
Available Overlays
| Overlay | File | Purpose | Key Features |
|---|---|---|---|
| Book Info | book_info_overlay.dart | Display book details, add to cart/wishlist | Cover image, metadata, availability check, purchase/wishlist actions |
| Settings | settings_overlay.dart | App settings and preferences | Tabbed interface (Account/Preferences), profile editing, addresses, notifications |
| Profile | profile_overlay.dart | User profile information | User details, stats, follow button, activity feed |
| Scribble | scribble_overlay.dart | Note editing interface | Rich text editor, book association |
| Store Search | store_search_overlay.dart | Search and select bookstores | Location-based search, store selection |
Detailed Implementations
Book Info Overlay
Located in /lib/ui/overlays/book_info_overlay.dart
Features:
- Displays complete book information from Book model
- Shows cover image, title, subtitle, authors, description
- Metadata section (publisher, pages, ISBN, categories)
- Availability checking (home store vs Ingram)
- Add to cart with store context
- Add to wishlist with guest user handling
- Stock alerts for unavailable books
Show Method:
void showBookInfoOverlay(BuildContext context, Book book) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.9,
minChildSize: 0.5,
maxChildSize: 0.95,
builder: (context, scrollController) => BookInfoOverlay(
book: book,
scrollController: scrollController,
),
),
);
}
Usage:
// From book card or search result
showBookInfoOverlay(context, book);
Settings Overlay
Located in /lib/ui/overlays/settings_overlay.dart
Features:
- Two tabs: Account and Preferences
- Account tab:
- Profile editing (display name, username, email)
- Avatar upload with loading state
- Subscription tier display
- Home store management
- Shipping addresses CRUD
- Password change dialog
- Preferences tab:
- Push/email notification toggles
- Private profile setting
- Stock alerts management
- Blocked users management
- Browser helper toggle
- About/legal links
- App version display
Show Method:
void showSettingsOverlay(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.85,
minChildSize: 0.5,
maxChildSize: 0.95,
expand: false,
builder: (context, scrollController) => SettingsOverlay(
scrollController: scrollController,
),
),
);
}
Tabs Pattern:
enum SettingsView { account, preferences }
// Use BwFilterChips for tab selection
BwFilterChips<SettingsView>(
options: const [
FilterOption(value: SettingsView.account, label: 'Account'),
FilterOption(value: SettingsView.preferences, label: 'Preferences'),
],
selected: _selectedView,
onChanged: (view) => setState(() => _selectedView = view),
),
Profile Overlay
Located in /lib/ui/overlays/profile_overlay.dart
Features:
- User avatar, display name, username
- Follow/unfollow button
- User statistics (followers, following, books)
- Activity feed (reviews, lines, notes)
- Block/report actions
Show Method:
void showProfileOverlay(BuildContext context, String userId) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.9,
minChildSize: 0.5,
maxChildSize: 0.95,
builder: (context, scrollController) => ProfileOverlay(
userId: userId,
scrollController: scrollController,
),
),
);
}
Scribble Overlay
Located in /lib/ui/overlays/scribble_overlay.dart
Features:
- Rich text editing
- Book association
- Save/discard actions
- Character count
Usage:
showScribbleOverlay(
context,
bookId: bookId,
existingNote: note, // null for new note
);
Store Search Overlay
Located in /lib/ui/overlays/store_search_overlay.dart
Features:
- Search bookstores by name or location
- Display store cards with distance
- Return selected store to caller
Show Method:
Future<Store?> showStoreSearchOverlay(BuildContext context) async {
return await showModalBottomSheet<Store>(
context: context,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.9,
minChildSize: 0.5,
maxChildSize: 0.95,
builder: (context, scrollController) => StoreSearchOverlay(
scrollController: scrollController,
),
),
);
}
// Usage
final selectedStore = await showStoreSearchOverlay(context);
if (selectedStore != null) {
// Use selected store
}
Overlay Structure
Standard Header
All overlays should include:
- Handle bar - Visual drag indicator
- Title - Clear overlay purpose
- Close button - X icon in top right
Column(
children: [
const SizedBox(height: 8),
// Handle bar
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 16),
// Title and close button
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Overlay Title', style: AppTypography.headingLarge),
IconButton(
icon: Icon(
CupertinoIcons.xmark_circle_fill,
color: AppColors.inkBlue.withValues(alpha: 0.5),
),
onPressed: () => Navigator.pop(context),
),
],
),
),
// Content below
],
)
Scrollable Content
Use the provided scrollController for the main content area:
ListView(
controller: scrollController,
padding: const EdgeInsets.all(16),
children: [
// Content
],
)
Or for single-child scrolling:
SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Content
],
),
)
Design Guidelines
Sizing
- initialChildSize:
0.85-0.9(most overlays) - minChildSize:
0.5(minimum drag height) - maxChildSize:
0.95(leave status bar visible)
Colors
- Background:
AppColors.parchment(matches app background) - Surface:
Colors.whitefor cards/containers - Border:
AppColors.borderfor subtle borders
Border Radius
- Top corners:
16pxrounded - Bottom corners:
0px(full width)
decoration: BoxDecoration(
color: AppColors.parchment,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
Spacing
- Top padding:
8px(before handle) - Handle to title:
16px - Content padding:
16pxall sides - Section spacing:
24pxbetween major sections
Typography
- Title:
AppTypography.headingLarge - Section headers:
AppTypography.heading - Body text:
AppTypography.bodyorAppTypography.cardBodyfor cards - Captions:
AppTypography.caption
Loading States
Handle async data with proper loading indicators:
final dataAsync = ref.watch(dataProvider);
return dataAsync.when(
data: (data) => _buildContent(data),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(CupertinoIcons.exclamationmark_circle, size: 48),
SizedBox(height: 16),
Text('Error: $error'),
TextButton(
onPressed: () => ref.invalidate(dataProvider),
child: Text('Retry'),
),
],
),
),
);
State Management
ConsumerWidget for Read-Only
class ExampleOverlay extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final data = ref.watch(dataProvider);
return Container();
}
}
ConsumerStatefulWidget for Mutable State
class ExampleOverlay extends ConsumerStatefulWidget {
ConsumerState<ExampleOverlay> createState() => _ExampleOverlayState();
}
class _ExampleOverlayState extends ConsumerState<ExampleOverlay> {
bool _isEditing = false;
Widget build(BuildContext context) {
return Container();
}
}
Best Practices
- Always pass scrollController - Enables drag-to-dismiss
- Use consistent sizing - Stick to standard initialChildSize values
- Include handle bar - Visual affordance for dragging
- Provide close button - Don't rely solely on drag gesture
- Handle loading states - Show spinners for async operations
- Close before navigation - Use
Navigator.pop()beforecontext.go() - Return values - Use generic type for selection overlays
- Test keyboard behavior - Ensure inputs work with keyboard open
- Avoid nested scrolling - Use single scroll controller
- Match theme - Use AppColors and AppTypography consistently
Common Patterns
Overlay with Tabs
enum TabType { tab1, tab2 }
class TabbedOverlay extends ConsumerStatefulWidget {
ConsumerState<TabbedOverlay> createState() => _TabbedOverlayState();
}
class _TabbedOverlayState extends ConsumerState<TabbedOverlay> {
TabType _selectedTab = TabType.tab1;
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
// Header with handle and title
BwFilterChips<TabType>(
options: const [
FilterOption(value: TabType.tab1, label: 'Tab 1'),
FilterOption(value: TabType.tab2, label: 'Tab 2'),
],
selected: _selectedTab,
onChanged: (tab) => setState(() => _selectedTab = tab),
),
Expanded(
child: _selectedTab == TabType.tab1
? _Tab1Content()
: _Tab2Content(),
),
],
),
);
}
}
Overlay with Actions
// Bottom action buttons
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'),
),
),
SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: _handleSave,
child: Text('Save'),
),
),
],
),
)
Selection Overlay
Future<String?> showSelectionOverlay(BuildContext context) async {
return await showModalBottomSheet<String>(
context: context,
builder: (context) => DraggableScrollableSheet(
builder: (context, scrollController) => ListView(
controller: scrollController,
children: options.map((option) => ListTile(
title: Text(option),
onTap: () => Navigator.pop(context, option),
)).toList(),
),
),
);
}