Files
amethyst/.claude/skills/desktop-expert/references/desktop-navigation.md
T
2025-12-30 15:18:48 +02:00

14 KiB

Desktop Navigation Patterns

Comparison of mobile vs desktop navigation patterns in AmethystMultiplatform.

Core Difference

Platform Pattern Location Rationale
Android Bottom Navigation Bar Horizontal, bottom Thumb reach on mobile
Desktop Navigation Rail Vertical, left sidebar Horizontal screen space

Desktop: NavigationRail

Current Implementation

File: desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt:191-264

@Composable
fun MainContent(
    currentScreen: AppScreen,
    onScreenChange: (AppScreen) -> Unit,
    // ...
) {
    Row(Modifier.fillMaxSize()) {
        // LEFT: Vertical Sidebar (NavigationRail)
        NavigationRail(
            modifier = Modifier.width(80.dp).fillMaxHeight(),
            containerColor = MaterialTheme.colorScheme.surfaceVariant
        ) {
            Spacer(Modifier.height(16.dp))

            // Top navigation items
            NavigationRailItem(
                icon = { Icon(Icons.Default.Home, "Feed") },
                label = { Text("Feed") },
                selected = currentScreen == AppScreen.Feed,
                onClick = { onScreenChange(AppScreen.Feed) }
            )

            NavigationRailItem(
                icon = { Icon(Icons.Default.Search, "Search") },
                label = { Text("Search") },
                selected = currentScreen == AppScreen.Search,
                onClick = { onScreenChange(AppScreen.Search) }
            )

            NavigationRailItem(
                icon = { Icon(Icons.Default.Email, "Messages") },
                label = { Text("DMs") },
                selected = currentScreen == AppScreen.Messages,
                onClick = { onScreenChange(AppScreen.Messages) }
            )

            NavigationRailItem(
                icon = { Icon(Icons.Default.Notifications, "Notifications") },
                label = { Text("Alerts") },
                selected = currentScreen == AppScreen.Notifications,
                onClick = { onScreenChange(AppScreen.Notifications) }
            )

            NavigationRailItem(
                icon = { Icon(Icons.Default.Person, "Profile") },
                label = { Text("Profile") },
                selected = currentScreen == AppScreen.Profile,
                onClick = { onScreenChange(AppScreen.Profile) }
            )

            // Push Settings to bottom
            Spacer(Modifier.weight(1f))

            HorizontalDivider(Modifier.padding(horizontal = 16.dp))

            NavigationRailItem(
                icon = { Icon(Icons.Default.Settings, "Settings") },
                label = { Text("Settings") },
                selected = currentScreen == AppScreen.Settings,
                onClick = { onScreenChange(AppScreen.Settings) }
            )

            Spacer(Modifier.height(16.dp))
        }

        VerticalDivider()

        // RIGHT: Main Content Area
        Box(modifier = Modifier.weight(1f).fillMaxHeight().padding(24.dp)) {
            when (currentScreen) {
                AppScreen.Feed -> FeedScreen(relayManager)
                AppScreen.Search -> SearchPlaceholder()
                AppScreen.Messages -> MessagesPlaceholder()
                AppScreen.Notifications -> NotificationsPlaceholder()
                AppScreen.Profile -> ProfileScreen(account, accountManager)
                AppScreen.Settings -> RelaySettingsScreen(relayManager)
            }
        }
    }
}

Layout Structure

┌────────────────────────────────────────┐
│  [Menu Bar: File, Edit, View, Help]   │  ← MenuBar (OS-native)
├──────┬─────────────────────────────────┤
│      │                                 │
│ [🏠] │                                 │
│ Feed │                                 │
│      │                                 │
│ [🔍] │      Main Content Area          │
│Search│      (Feed, Messages, etc.)     │
│      │                                 │
│ [✉️] │                                 │
│ DMs  │                                 │
│      │                                 │
│ [🔔] │                                 │
│Alerts│                                 │
│      │                                 │
│ [👤] │                                 │
│Profile                                 │
│      │                                 │
│  ─   │                                 │
│ [⚙️] │                                 │
│Settings                                │
│      │                                 │
└──────┴─────────────────────────────────┘
  80dp          Remaining width (weight=1f)

Key Features

  1. Always visible: All nav items visible at once
  2. Icon + Label: Both shown (not just icons)
  3. Vertical list: Natural reading order
  4. Settings at bottom: Separated by divider + Spacer.weight(1f)
  5. 80dp width: Standard NavigationRail width

Android: BottomNavigationBar (Future)

Expected Implementation

Location: amethyst/src/androidMain/kotlin/... (not yet implemented)

@Composable
fun MainScreen(
    currentScreen: AppScreen,
    onScreenChange: (AppScreen) -> Unit
) {
    Scaffold(
        bottomBar = {
            NavigationBar {
                NavigationBarItem(
                    icon = { Icon(Icons.Default.Home, "Feed") },
                    label = { Text("Feed") },
                    selected = currentScreen == AppScreen.Feed,
                    onClick = { onScreenChange(AppScreen.Feed) }
                )
                NavigationBarItem(
                    icon = { Icon(Icons.Default.Search, "Search") },
                    label = { Text("Search") },
                    selected = currentScreen == AppScreen.Search,
                    onClick = { onScreenChange(AppScreen.Search) }
                )
                NavigationBarItem(
                    icon = { Icon(Icons.Default.Email, "Messages") },
                    label = { Text("Messages") },
                    selected = currentScreen == AppScreen.Messages,
                    onClick = { onScreenChange(AppScreen.Messages) }
                )
                NavigationBarItem(
                    icon = { Icon(Icons.Default.Person, "Profile") },
                    label = { Text("Profile") },
                    selected = currentScreen == AppScreen.Profile,
                    onClick = { onScreenChange(AppScreen.Profile) }
                )
            }
        }
    ) { paddingValues ->
        Box(Modifier.padding(paddingValues)) {
            when (currentScreen) {
                AppScreen.Feed -> FeedScreen()
                AppScreen.Search -> SearchScreen()
                AppScreen.Messages -> MessagesScreen()
                AppScreen.Profile -> ProfileScreen()
                // Settings accessed via Profile or overflow menu
            }
        }
    }
}

Layout Structure

┌─────────────────────────────────────┐
│                                     │
│                                     │
│        Main Content Area            │
│        (Feed, Messages, etc.)       │
│                                     │
│                                     │
│                                     │
├─────────────────────────────────────┤
│ [🏠]   [🔍]   [✉️]   [👤]          │  ← NavigationBar
│ Feed  Search  DMs  Profile          │
└─────────────────────────────────────┘

Key Differences from Desktop

  1. Bottom placement: Thumb reach
  2. Horizontal layout: Limited vertical space
  3. Fewer items: 3-5 primary destinations
  4. Label optional: Can hide on small screens
  5. Settings hidden: In profile or overflow

Shared Navigation State

Both platforms use the same AppScreen enum from commons.

File: commons/src/commonMain/kotlin/.../navigation/AppScreen.kt (expected)

// Shared navigation destinations
enum class AppScreen {
    Feed,
    Search,
    Messages,
    Notifications,
    Profile,
    Settings
}

State management (shared):

// commons/src/jvmAndroid/kotlin/.../navigation/NavigationViewModel.kt
class NavigationViewModel : ViewModel() {
    private val _currentScreen = MutableStateFlow(AppScreen.Feed)
    val currentScreen: StateFlow<AppScreen> = _currentScreen.asStateFlow()

    fun navigateTo(screen: AppScreen) {
        _currentScreen.value = screen
    }
}

Multi-Pane Desktop Layout (Advanced)

Desktop can utilize horizontal space for multi-pane layouts.

Two-Pane Layout

Row(Modifier.fillMaxSize()) {
    // Left: NavigationRail (fixed 80dp)
    NavigationRail { /* ... */ }

    VerticalDivider()

    // Center: Main content (60% width)
    Box(Modifier.weight(0.6f)) {
        FeedScreen()
    }

    // Right: Detail pane (40% width, conditional)
    if (selectedNote != null) {
        VerticalDivider()
        Box(Modifier.weight(0.4f)) {
            NoteDetailPane(selectedNote)
        }
    }
}

Layout:

┌──────┬───────────────────┬─────────────┐
│      │                   │             │
│ Nav  │   Feed List       │   Detail    │
│ Rail │   (60%)           │   Pane      │
│      │                   │   (40%)     │
│      │                   │             │
└──────┴───────────────────┴─────────────┘
 80dp      weight(0.6f)      weight(0.4f)

Use cases:

  • Email: List + message detail
  • Notes: List + editor
  • Settings: Categories + options

Keyboard Navigation

Desktop should support keyboard navigation.

Tab Navigation

NavigationRail(
    modifier = Modifier.focusable()
) {
    NavigationRailItem(
        icon = { Icon(Icons.Default.Home, "Feed") },
        label = { Text("Feed") },
        selected = currentScreen == AppScreen.Feed,
        onClick = { onScreenChange(AppScreen.Feed) },
        modifier = Modifier.focusable()
    )
    // More items...
}

Keyboard Shortcuts

Window(
    onPreviewKeyEvent = { event ->
        when {
            event.key == Key.One && event.isCtrlPressed ->
                onScreenChange(AppScreen.Feed).also { true }
            event.key == Key.Two && event.isCtrlPressed ->
                onScreenChange(AppScreen.Search).also { true }
            event.key == Key.Three && event.isCtrlPressed ->
                onScreenChange(AppScreen.Messages).also { true }
            else -> false
        }
    }
) {
    // Content
}

Standard:

  • Ctrl+1: First nav item (Feed)
  • Ctrl+2: Second nav item (Search)
  • Ctrl+3: Third nav item (Messages)
  • Ctrl+Comma: Settings

Navigation Transitions

Desktop (Instant)

No fancy animations. Instant switch.

Box {
    when (currentScreen) {
        AppScreen.Feed -> FeedScreen()
        AppScreen.Search -> SearchScreen()
    }
}

Android (Animated, Future)

Can use Navigation Compose for transitions.

NavHost(navController, startDestination = "feed") {
    composable("feed") { FeedScreen() }
    composable("search") { SearchScreen() }
}

Best Practices

Desktop NavigationRail

DO:

  • Keep width 72-80dp
  • Show both icon and label
  • Use Spacer.weight(1f) for bottom items
  • Separate sections with HorizontalDivider
  • Limit to 5-7 primary items

DON'T:

  • Use bottom navigation on desktop
  • Hide labels (plenty of space)
  • Make it collapsible (not standard)
  • Use hamburger menu (not desktop pattern)

Android NavigationBar

DO:

  • Limit to 3-5 items
  • Use bottom placement
  • Consider label visibility on small screens
  • Use standard icons

DON'T:

  • Put more than 5 items
  • Use top placement (deprecated)
  • Put critical actions only in nav bar

Migration Strategy

When adding Android support:

  1. Extract shared state: Move AppScreen to commons/commonMain
  2. Platform layouts: Keep NavigationRail in desktopApp/jvmMain, NavigationBar in amethyst/androidMain
  3. Shared screens: Composables in commons/commonMain (FeedScreen content)
  4. Platform chrome: Navigation containers in platform modules

Example:

// commons/commonMain - Shared screen content
@Composable
fun FeedContent(notes: List<Note>) {
    LazyColumn {
        items(notes) { note ->
            NoteCard(note)
        }
    }
}

// desktopApp/jvmMain - Desktop wrapper
@Composable
fun FeedScreen() {
    Column {
        FeedHeader()  // Desktop-specific header
        FeedContent(notes)  // Shared content
    }
}

// amethyst/androidMain - Android wrapper
@Composable
fun FeedScreen() {
    Scaffold(
        topBar = { TopAppBar { Text("Feed") } }
    ) {
        FeedContent(notes)  // Same shared content
    }
}

References