Claude AI UI/UX Design Guide - SailorBook

## 🎨 Overview

Installs: 0
Used in: 1 repos
Updated: 2d ago
$npx ai-builder add agent WolfOfTheNorth/ui-ux

Installs to .claude/agents/ui-ux.md

# Claude AI UI/UX Design Guide - SailorBook

## 🎨 Overview

This guide defines the UI/UX standards, Material 3 implementation, and design patterns for Claude AI agents working on SailorBook's Flutter interface. All UI components must follow these guidelines for consistency and accessibility.

## 🎯 Design Philosophy

### Core Principles
1. **Privacy-First**: Clean, minimal design that feels trustworthy
2. **Reading-Focused**: Typography and layout optimized for long-form content
3. **Accessibility**: WCAG 2.1 AA compliance with keyboard and screen reader support
4. **Cross-Platform**: Consistent experience across mobile, tablet, and desktop
5. **Material 3**: Modern Google design language with dynamic color

## 🎨 Material 3 Implementation

### Color System

#### Theme Configuration
```dart
// themes/app_theme.dart - Already implemented
class AppTheme {
  static ThemeData get lightTheme {
    return ThemeData(
      useMaterial3: true,
      colorScheme: ColorScheme.fromSeed(
        seedColor: const Color(0xFF6366F1), // Indigo-500
        brightness: Brightness.light,
      ),
      typography: Typography.material2021(),
    );
  }
  
  static ThemeData get darkTheme {
    return ThemeData(
      useMaterial3: true,
      colorScheme: ColorScheme.fromSeed(
        seedColor: const Color(0xFF6366F1),
        brightness: Brightness.dark,
      ),
      typography: Typography.material2021(),
    );
  }
}
```

#### Color Usage Guidelines
```dart
// ✅ Use theme colors, not hardcoded colors
Container(
  color: Theme.of(context).colorScheme.surface,
  child: Text(
    'Book Title',
    style: TextStyle(
      color: Theme.of(context).colorScheme.onSurface,
    ),
  ),
)

// ❌ Avoid hardcoded colors
Container(
  color: Colors.white,  // Bad - won't work in dark theme
  child: Text(
    'Book Title',
    style: TextStyle(color: Colors.black),  // Bad
  ),
)
```

### Typography Scale

```dart
// ✅ Typography implementation
class BookTypography {
  static TextStyle get displayLarge => const TextStyle(
    fontSize: 32,
    fontWeight: FontWeight.w400,
    letterSpacing: -0.25,
    height: 1.2,
  );
  
  static TextStyle get headlineLarge => const TextStyle(
    fontSize: 28,
    fontWeight: FontWeight.w400,
    letterSpacing: 0,
    height: 1.3,
  );
  
  static TextStyle get titleLarge => const TextStyle(
    fontSize: 20,
    fontWeight: FontWeight.w500,
    letterSpacing: 0,
    height: 1.4,
  );
  
  static TextStyle get bodyLarge => const TextStyle(
    fontSize: 16,
    fontWeight: FontWeight.w400,
    letterSpacing: 0.15,
    height: 1.5,
  );
  
  // Reading-optimized styles
  static TextStyle get readingBody => const TextStyle(
    fontSize: 18,
    fontWeight: FontWeight.w400,
    letterSpacing: 0.1,
    height: 1.6,
    fontFamily: 'Georgia', // Better for reading
  );
}
```

## 📱 Component Design Patterns

### Navigation Structure

```dart
// ✅ Main navigation implementation
class MainNavigation extends StatefulWidget {
  @override
  State<MainNavigation> createState() => _MainNavigationState();
}

class _MainNavigationState extends State<MainNavigation> {
  int _currentIndex = 0;
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: [
          LibraryView(key: const Key('library-view')),
          SearchView(key: const Key('search-view')),
          SettingsView(key: const Key('settings-view')),
        ],
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (index) => setState(() {
          _currentIndex = index;
        }),
        destinations: const [
          NavigationDestination(
            key: Key('nav-library'),
            icon: Icon(Icons.library_books_outlined),
            selectedIcon: Icon(Icons.library_books),
            label: 'Library',
          ),
          NavigationDestination(
            key: Key('nav-search'),
            icon: Icon(Icons.search_outlined),
            selectedIcon: Icon(Icons.search),
            label: 'Search',
          ),
          NavigationDestination(
            key: Key('nav-settings'),
            icon: Icon(Icons.settings_outlined),
            selectedIcon: Icon(Icons.settings),
            label: 'Settings',
          ),
        ],
      ),
    );
  }
}
```

### Book Card Design

```dart
// ✅ Consistent book card component
class BookCard extends StatelessWidget {
  final Book book;
  final VoidCallback? onTap;
  final VoidCallback? onDownload;
  final bool isDownloaded;
  
  const BookCard({
    Key? key,
    required this.book,
    this.onTap,
    this.onDownload,
    this.isDownloaded = false,
  }) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Card(
      key: Key('book-card-${book.id}'),
      clipBehavior: Clip.antiAlias,
      child: InkWell(
        onTap: onTap,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // Book cover
              ClipRRect(
                borderRadius: BorderRadius.circular(8),
                child: SizedBox(
                  width: 60,
                  height: 90,
                  child: book.coverUrl != null
                      ? Image.network(
                          book.coverUrl!,
                          fit: BoxFit.cover,
                          errorBuilder: (_, __, ___) => _buildCoverPlaceholder(),
                        )
                      : _buildCoverPlaceholder(),
                ),
              ),
              
              const SizedBox(width: 16),
              
              // Book details
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      book.title,
                      style: Theme.of(context).textTheme.titleMedium,
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                    ),
                    
                    const SizedBox(height: 4),
                    
                    Text(
                      book.author,
                      style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                        color: Theme.of(context).colorScheme.onSurfaceVariant,
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    
                    const SizedBox(height: 8),
                    
                    // Action button
                    _buildActionButton(context),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
  
  Widget _buildCoverPlaceholder() {
    return Container(
      color: Theme.of(context).colorScheme.surfaceVariant,
      child: Icon(
        Icons.book,
        color: Theme.of(context).colorScheme.onSurfaceVariant,
        size: 32,
      ),
    );
  }
  
  Widget _buildActionButton(BuildContext context) {
    if (isDownloaded) {
      return FilledButton.tonal(
        key: Key('listen-btn-${book.id}'),
        onPressed: onTap,
        child: const Text('Listen'),
      );
    } else {
      return OutlinedButton(
        key: Key('download-btn-${book.id}'),
        onPressed: onDownload,
        child: const Text('Download'),
      );
    }
  }
}
```

### Search Interface

```dart
// ✅ Search interface with proper UX
class SearchView extends StatefulWidget {
  @override
  State<SearchView> createState() => _SearchViewState();
}

class _SearchViewState extends State<SearchView> {
  final TextEditingController _searchController = TextEditingController();
  final FocusNode _searchFocusNode = FocusNode();
  List<Book> _searchResults = [];
  bool _isLoading = false;
  String? _errorMessage;
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Search Books'),
        backgroundColor: Theme.of(context).colorScheme.surface,
      ),
      body: Column(
        children: [
          // Search input
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: SearchBar(
              key: const Key('search-input'),
              controller: _searchController,
              focusNode: _searchFocusNode,
              hintText: 'Search public domain books...',
              leading: const Icon(Icons.search),
              trailing: [
                if (_searchController.text.isNotEmpty)
                  IconButton(
                    key: const Key('clear-search'),
                    icon: const Icon(Icons.clear),
                    onPressed: _clearSearch,
                  ),
              ],
              onSubmitted: _performSearch,
              onChanged: (value) {
                if (value.isEmpty) {
                  _clearSearch();
                }
              },
            ),
          ),
          
          // Results area
          Expanded(
            child: _buildSearchResults(),
          ),
        ],
      ),
    );
  }
  
  Widget _buildSearchResults() {
    if (_isLoading) {
      return const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CircularProgressIndicator(),
            SizedBox(height: 16),
            Text('Searching...'),
          ],
        ),
      );
    }
    
    if (_errorMessage != null) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.error_outline,
              size: 64,
              color: Theme.of(context).colorScheme.error,
            ),
            const SizedBox(height: 16),
            Text(
              _errorMessage!,
              key: const Key('error-message'),
              style: TextStyle(
                color: Theme.of(context).colorScheme.error,
              ),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 16),
            FilledButton(
              key: const Key('retry-btn'),
              onPressed: () => _performSearch(_searchController.text),
              child: const Text('Retry'),
            ),
          ],
        ),
      );
    }
    
    if (_searchResults.isEmpty && _searchController.text.isNotEmpty) {
      return const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.search_off, size: 64),
            SizedBox(height: 16),
            Text('No books found'),
            SizedBox(height: 8),
            Text('Try different search terms'),
          ],
        ),
      );
    }
    
    return ListView.builder(
      key: const Key('search-results-list'),
      padding: const EdgeInsets.symmetric(horizontal: 16),
      itemCount: _searchResults.length,
      itemBuilder: (context, index) {
        return BookCard(
          key: Key('search-result-$index'),
          book: _searchResults[index],
          onTap: () => _openBookDetails(_searchResults[index]),
          onDownload: () => _downloadBook(_searchResults[index]),
        );
      },
    );
  }
}
```

## 📖 Reading Experience Design

### Player Interface

```dart
// ✅ Audio player with clean, focused design
class PlayerView extends StatefulWidget {
  final Book book;
  
  const PlayerView({
    Key? key,
    required this.book,
  }) : super(key: key);
  
  @override
  State<PlayerView> createState() => _PlayerViewState();
}

class _PlayerViewState extends State<PlayerView> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: const Key('player-view'),
      backgroundColor: Theme.of(context).colorScheme.surface,
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        elevation: 0,
        leading: IconButton(
          key: const Key('back-btn'),
          icon: const Icon(Icons.arrow_back),
          onPressed: () => Navigator.of(context).pop(),
        ),
        actions: [
          IconButton(
            key: const Key('voice-settings-btn'),
            icon: const Icon(Icons.settings_voice),
            onPressed: _openVoiceSettings,
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(24.0),
        child: Column(
          children: [
            // Book cover and info
            Expanded(
              flex: 3,
              child: Column(
                children: [
                  // Large book cover
                  Container(
                    width: 200,
                    height: 300,
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(16),
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black.withOpacity(0.2),
                          blurRadius: 20,
                          offset: const Offset(0, 10),
                        ),
                      ],
                    ),
                    child: ClipRRect(
                      borderRadius: BorderRadius.circular(16),
                      child: widget.book.coverUrl != null
                          ? Image.network(widget.book.coverUrl!, fit: BoxFit.cover)
                          : Container(
                              color: Theme.of(context).colorScheme.surfaceVariant,
                              child: Icon(
                                Icons.book,
                                size: 80,
                                color: Theme.of(context).colorScheme.onSurfaceVariant,
                              ),
                            ),
                    ),
                  ),
                  
                  const SizedBox(height: 24),
                  
                  // Book title and author
                  Text(
                    widget.book.title,
                    style: Theme.of(context).textTheme.headlineSmall,
                    textAlign: TextAlign.center,
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                  
                  const SizedBox(height: 8),
                  
                  Text(
                    widget.book.author,
                    style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                      color: Theme.of(context).colorScheme.onSurfaceVariant,
                    ),
                    textAlign: TextAlign.center,
                  ),
                ],
              ),
            ),
            
            // Current chapter info
            Container(
              key: const Key('chapter-info'),
              padding: const EdgeInsets.symmetric(vertical: 16),
              child: Column(
                children: [
                  Text(
                    'Chapter 1: Introduction',
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                  const SizedBox(height: 4),
                  Text(
                    'Paragraph 5 of 23',
                    style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                      color: Theme.of(context).colorScheme.onSurfaceVariant,
                    ),
                  ),
                ],
              ),
            ),
            
            // Playback controls
            Expanded(
              flex: 2,
              child: _buildPlaybackControls(),
            ),
          ],
        ),
      ),
    );
  }
  
  Widget _buildPlaybackControls() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        // Main controls
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            // Previous paragraph
            IconButton.filledTonal(
              key: const Key('prev-paragraph-btn'),
              onPressed: _previousParagraph,
              iconSize: 32,
              icon: const Icon(Icons.skip_previous),
            ),
            
            // Play/Pause
            IconButton.filled(
              key: const Key('play-pause-btn'),
              onPressed: _togglePlayback,
              iconSize: 48,
              icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow),
            ),
            
            // Next paragraph
            IconButton.filledTonal(
              key: const Key('next-paragraph-btn'),
              onPressed: _nextParagraph,
              iconSize: 32,
              icon: const Icon(Icons.skip_next),
            ),
          ],
        ),
        
        const SizedBox(height: 32),
        
        // Speed control
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Speed: ',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            SegmentedButton<double>(
              key: const Key('speed-selector'),
              segments: const [
                ButtonSegment(value: 0.8, label: Text('0.8×')),
                ButtonSegment(value: 1.0, label: Text('1.0×')),
                ButtonSegment(value: 1.25, label: Text('1.25×')),
                ButtonSegment(value: 1.5, label: Text('1.5×')),
                ButtonSegment(value: 2.0, label: Text('2.0×')),
              ],
              selected: {_playbackSpeed},
              onSelectionChanged: (speeds) {
                if (speeds.isNotEmpty) {
                  _setPlaybackSpeed(speeds.first);
                }
              },
            ),
          ],
        ),
      ],
    );
  }
}
```

## 🎯 Accessibility Guidelines

### Keyboard Navigation
```dart
// ✅ Keyboard navigation support
class AccessibleBookCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Focus(
      child: Builder(
        builder: (context) {
          final focused = Focus.of(context).hasFocus;
          return Container(
            decoration: BoxDecoration(
              border: focused
                  ? Border.all(
                      color: Theme.of(context).colorScheme.primary,
                      width: 2,
                    )
                  : null,
              borderRadius: BorderRadius.circular(8),
            ),
            child: InkWell(
              onTap: _onTap,
              child: Semantics(
                button: true,
                label: 'Book: ${book.title} by ${book.author}',
                onTap: _onTap,
                child: _buildCardContent(),
              ),
            ),
          );
        },
      ),
    );
  }
}
```

### Screen Reader Support
```dart
// ✅ Semantic labels for screen readers
Widget build(BuildContext context) {
  return Semantics(
    label: 'Search for books',
    hint: 'Enter book title or author name',
    textField: true,
    child: TextField(
      controller: _searchController,
      decoration: const InputDecoration(
        hintText: 'Search books...',
      ),
    ),
  );
}
```

## 📱 Responsive Design

### Layout Breakpoints
```dart
// ✅ Responsive layout implementation
class ResponsiveLayout extends StatelessWidget {
  final Widget mobile;
  final Widget tablet;
  final Widget desktop;
  
  const ResponsiveLayout({
    Key? key,
    required this.mobile,
    required this.tablet,
    required this.desktop,
  }) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        if (constraints.maxWidth < 600) {
          return mobile;
        } else if (constraints.maxWidth < 1200) {
          return tablet;
        } else {
          return desktop;
        }
      },
    );
  }
}

// Usage example
ResponsiveLayout(
  mobile: _buildMobileLayout(),
  tablet: _buildTabletLayout(),
  desktop: _buildDesktopLayout(),
)
```

### Adaptive Layouts
```dart
// ✅ Adaptive book grid
class BookGrid extends StatelessWidget {
  final List<Book> books;
  
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final crossAxisCount = _getCrossAxisCount(constraints.maxWidth);
        
        return GridView.builder(
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: crossAxisCount,
            crossAxisSpacing: 16,
            mainAxisSpacing: 16,
            childAspectRatio: 0.7,
          ),
          itemCount: books.length,
          itemBuilder: (context, index) => BookCard(book: books[index]),
        );
      },
    );
  }
  
  int _getCrossAxisCount(double width) {
    if (width < 600) return 2;      // Mobile
    if (width < 900) return 3;      // Tablet
    if (width < 1200) return 4;     // Small desktop
    return 5;                       // Large desktop
  }
}
```

## 🎨 Animation Guidelines

### Page Transitions
```dart
// ✅ Smooth page transitions
class BookDetailsRoute extends PageRouteBuilder {
  final Book book;
  
  BookDetailsRoute({required this.book})
      : super(
          pageBuilder: (context, animation, _) => BookDetailsView(book: book),
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            return SlideTransition(
              position: animation.drive(
                Tween(
                  begin: const Offset(1.0, 0.0),
                  end: Offset.zero,
                ).chain(CurveTween(curve: Curves.easeInOut)),
              ),
              child: child,
            );
          },
          transitionDuration: const Duration(milliseconds: 300),
        );
}
```

### Loading States
```dart
// ✅ Elegant loading animations
class LoadingBookCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Card(
      child: Shimmer.fromColors(
        baseColor: Theme.of(context).colorScheme.surfaceVariant,
        highlightColor: Theme.of(context).colorScheme.surface,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Row(
            children: [
              Container(
                width: 60,
                height: 90,
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(8),
                ),
              ),
              const SizedBox(width: 16),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Container(
                      width: double.infinity,
                      height: 16,
                      color: Colors.white,
                    ),
                    const SizedBox(height: 8),
                    Container(
                      width: 120,
                      height: 14,
                      color: Colors.white,
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
```

## 🚨 Common UI/UX Issues

### Issue: Inconsistent Spacing
```dart
// ❌ Inconsistent spacing
Column(
  children: [
    Text('Title'),
    SizedBox(height: 12),  // Random spacing
    Text('Subtitle'),
    SizedBox(height: 6),   // Different spacing
    Button(),
  ],
)

// ✅ Consistent spacing system
Column(
  children: [
    Text('Title'),
    const SizedBox(height: 16),  // Use 8px multiples
    Text('Subtitle'),
    const SizedBox(height: 8),   // Consistent system
    Button(),
  ],
)
```

### Issue: Missing Test Keys
```dart
// ❌ No test identification
ElevatedButton(
  onPressed: _download,
  child: Text('Download'),
)

// ✅ With test key
ElevatedButton(
  key: Key('download-btn-${book.id}'),
  onPressed: _download,
  child: const Text('Download'),
)
```

## 📚 UI Component Checklist

### Every Interactive Widget Must Have:
- [ ] Unique test key (`key: Key('element-id')`)
- [ ] Proper semantic labels for accessibility
- [ ] Loading and error states
- [ ] Keyboard navigation support
- [ ] Consistent Material 3 styling
- [ ] Responsive behavior
- [ ] Proper focus indicators

### Every Screen Must Have:
- [ ] AppBar with consistent styling
- [ ] Loading states for async operations
- [ ] Error handling with user-friendly messages
- [ ] Empty states with helpful guidance
- [ ] Keyboard navigation between elements
- [ ] Proper back button handling
- [ ] Test keys for major UI elements

---

**Last Updated**: 2025-08-28  
**Status**: Active  
**Design System**: Material 3

*Follow these guidelines for consistent, accessible, and beautiful UI across SailorBook.*

Quick Install

$npx ai-builder add agent WolfOfTheNorth/ui-ux

Details

Type
agent
Slug
WolfOfTheNorth/ui-ux
Created
6d ago