# Two-Column TV Settings / Onboarding Layout Pattern

## When to use

Any Android TV screen that combines persistent navigation (sidebar/step list)
with dynamic content (settings panes, auth forms, configuration panels).
This pattern was proven on UNSPOOLED's Settings screen (May 2026) and
Onboarding wizard (May 2026).

## Architecture

```
┌──────────────┬──────────────────────────────────────┐
│   SIDEBAR    │           CONTENT AREA               │
│  (fixed W)   │          (fills rest)                │
│              │                                      │
│  ┌─ Header   │  Title                               │
│  │           │  Subtitle                            │
│  ├─ Nav 1 ●──┤                                      │
│  ├─ Nav 2    │  [Interactive content changes        │
│  ├─ Nav 3    │   based on selected sidebar item]     │
│  ├─ Nav 4    │                                      │
│  └─ Nav 5    │                                      │
│              │                                      │
└──────────────┴──────────────────────────────────────┘
```

## Compose Implementation

### Root Row

```kotlin
Row(modifier = Modifier.fillMaxSize().background(colors.Background)) {
    // Left sidebar — fixed width, fill height
    Column(
        modifier = Modifier
            .width(300.dp)
            .fillMaxHeight()
            .background(colors.BackgroundElevated)
            .padding(28.dp),
    ) { /* nav items */ }

    // Right content — fills remaining space
    Column(
        modifier = Modifier.fillMaxSize().padding(48.dp),
    ) { /* dynamic content */ }
}
```

### Sidebar nav item with colored accent bar

Each item gets a 3dp-wide colored accent bar on the left edge.
The bar is visible when selected. The background changes tint on focus.

```kotlin
Row(
    modifier = Modifier
        .fillMaxWidth()
        .padding(vertical = 4.dp)
        .clip(RoundedCornerShape(10.dp))
        .background(if (isSelected || isFocused) accentColor.copy(alpha = 0.15f) else Color.Transparent)
        .border(
            width = if (isSelected) 2.dp else 0.dp,
            color = if (isSelected) accentColor else Color.Transparent,
            shape = RoundedCornerShape(10.dp),
        )
        .onFocusChanged { navFocused = it.isFocused || it.hasFocus }
        .onPreviewKeyEvent { event ->
            if (event.type == KeyEventType.KeyUp && event.key == Key.DirectionCenter) {
                selectedIndex = index; true
            } else false
        }
        .focusable()
        .padding(horizontal = 14.dp, vertical = 12.dp),
    verticalAlignment = Alignment.CenterVertically,
    horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
    // Colored accent bar (3dp × 28dp)
    Box(
        modifier = Modifier
            .width(3.dp).height(28.dp)
            .clip(RoundedCornerShape(2.dp))
            .background(if (isSelected) accentColor else Color.Transparent)
    )
    Icon(imageVector = item.icon, tint = if (isSelected) accentColor else colors.TextSecondary)
    Text(item.label, color = if (isSelected) colors.TextPrimary else colors.TextSecondary)
}
```

### D-pad behavior

- Sidebar item gets focus naturally via D-pad UP/DOWN.
- On D-pad RIGHT: focus moves into the content area.
- On D-pad LEFT from first focusable in content: focus returns to sidebar.
- No FocusRequester needed — let Compose TV handle the Row layout naturally.

**Critical:** Do NOT wrap the sidebar in a `LazyColumn` — a single-item lazy list destroys
D-pad focus tracking across sidebar items. Use a plain `Column` for the fixed nav list.

### Progress checkmarks (Onboarding variant)

For onboarding wizards, each sidebar step shows a completion checkmark:

```kotlin
if (isCompleted) {
    Spacer(Modifier.weight(1f))
    Text("✅", fontSize = 16.sp)
}
```

### Content area switching

Use `AnimatedContent` for smooth transitions between content panels:

```kotlin
AnimatedContent(
    targetState = selectedIndex,
    transitionSpec = {
        (fadeIn(tween(300)) togetherWith fadeOut(tween(200)))
    },
) { index ->
    when (index) {
        0 -> PanelA()
        1 -> PanelB()
        // ...
    }
}
```

**Pitfall:** Must import `togetherWith` from `androidx.compose.animation.togetherWith`.
Using `fadeIn() + fadeOut()` produces a compile error (see skill Trap 38).

## Color conventions

| Role | Color |
|---|---|
| Dashboard | Red `#E53935` |
| Theme | Blue `#1E88E5` |
| Addons | Purple `#8E24AA` |
| History | Blue `#1E88E5` |
| Watchlist | Orange `#FB8C00` |
| Watched | Pink `#D81B60` |
| Profiles | Gray `#9E9E9E` |

Each nav item gets a unique accent color for its bar and focus tint — this helps
the user identify which section they're in at a glance, useful on large TV screens.

## TV-friendly sizing

- Sidebar width: 280–320dp (comfortable for D-pad navigation)
- Nav item padding: 12–14dp horizontal, 10–14dp vertical
- Font size: 16–18sp for nav labels, 22–24sp for headers
- Accent bar: 3dp × 28dp (visible from couch)
- Content padding: 48dp (generous TV margins)
- Focus border: 2dp Accent color

## Files using this pattern

- `app/.../settings/SettingsScreen.kt` — Dashboard sidebar + content panels
- `app/.../onboarding/OnboardingScreen.kt` — Progress sidebar + step content
