# Android TV Cinematic UI Patterns — Clean-Room from Nuxt/Movies

**Date:** 2026-05-21  
**Reference source:** https://github.com/nuxt/movies (MIT)  
**Adapted for:** Jetpack Compose for TV (tv-material3 1.0.0-rc01), NexStream Android TV

## What Was Adapted (Clean-Room — Visual Pattern Only)

### 1. Hero Banner Layout

**Source pattern (Nuxt/Movies):**
- Full-width backdrop image with dark gradient overlay
- Metadata stack left-aligned: title → rating + year + runtime → synopsis → action buttons
- CSS: `from-black via-black to-transparent` gradient on left 2/3 of the hero
- `aspect-ratio-3/2 lg:aspect-ratio-25/9` for the hero area

**Compose adaptation:**
```kotlin
// Multi-stop gradient overlay — 3-stop vertical + 3-stop horizontal
Brush.verticalGradient(        // bottom fade
    colors = listOf(Color.Transparent, Background.copy(0.4f), Background.copy(0.85f), Background),
)
Brush.horizontalGradient(      // left spotlight
    colors = listOf(Background.copy(0.55f), Background.copy(0.2f), Color.Transparent),
)
```

**Key insight:** The left-side gradient should be stronger than the original — the original was `alpha=0.3` but `0.55` creates a more cinematic "spotlight" effect on TV.

### 2. Carousel / Row Hierarchy

**Source pattern:** Three-tier component hierarchy
- `Base.vue` — scroll container with arrow buttons, title slot, "more" slot
- `Items.vue` — maps item array to cards inside Base
- `AutoQuery.vue` — fetches data from API, passes to Items, shows title + "Explore more" link

**Compose adaptation:**
- `CinematicMediaRow` — the AutoQuery equivalent: title header + LazyRow of cards
- `MediaRow` — the Items equivalent: simpler version for static lists
- `MediaCard` / `FocusScalingCard` — individual poster cards

**Key insight:** Nuxt uses `w-40 md:w-60` (10rem = 160px → 15rem = 240px). For TV at 10-foot distance, 150dp is the sweet spot (larger than mobile 140dp but smaller than the full 240px which would show too few items).

### 3. TMDB Proxy Pattern

**Source pattern:** Vercel proxy at `https://movies-proxy.vercel.app/tmdb/...` — TMDB API key never exposed to client.

**NexStream adaptation:** Cloudflare Worker at `https://nexstream-catalog.tw24fr.workers.dev/v1/...` — serves the same role. All TMDB/Trakt/MDBList keys are Worker secrets.

### 4. Layout Grid

**Source pattern:** CSS Grid — sidebar nav + scrollable content area:
```css
grid="~ lt-lg:rows-[1fr_max-content] lg:cols-[max-content_1fr]"
```

**Compose adaptation:** `Row { NavRail + Column { TopBar + Content } }` with animated rail width (56dp collapsed / 220dp expanded).

### 5. LRU Response Cache

**Source pattern:** `lru-cache` with 2-hour TTL for TMDB responses, `ohash` for cache key computation.

**NexStream adaptation:** Cloudflare edge cache (free, built-in) + potential Room-based local cache for offline.

## Complete UI Overhaul Execution Pattern

When executing a multi-phase UI overhaul on a Jetpack Compose for TV project:

### Phase Execution Rules

1. **Phase 1: Design system FIRST** — Expand color tokens, create icon constants, update shared components (NavRail, focus indicator). Everything else depends on this.
2. **Phase 2: Most visible screen** — Home screen, hero banner, card components. Sets the visual tone.
3. **Phases 3-5: Remaining screens in order of user flow** — Onboarding → Profiles → Search → Watchlist → History → Addons → Settings → Details → Player.
4. **Build after EVERY phase** — `./gradlew assembleDebug testDebugUnitTest`. Catch errors immediately.
5. **Direct execution > subagents for tightly-coupled UI** — When 20+ files share the same Compose/TV APIs, subagent API drift is a real cost. Execute directly with targeted `patch()` calls.

### API_SNIPPETS.md Pattern

For any project where subagents might be used later, create an `API_SNIPPETS.md` at the project root with exact API signatures:

```markdown
## Border (tv-material3)
Border(border = BorderStroke(2.dp, color), shape = RoundedCornerShape(8.dp))
// WRONG: ImmutableBorder(width=, color=, shape=) — does NOT exist in rc01

## ClickableSurfaceDefaults.border()
focusedBorder = Border(...)  // parameter is focusedBorder, NOT focused
```

This prevents the #1 cost driver of subagent-generated Compose code: wrong API signatures from outdated training data.

### Patch Tool with Literal Emoji

When replacing inline emoji strings in Kotlin source, the `patch()` tool matches literal characters. Unicode escapes (`\\uD83D\\uDD12`) won't match literal emoji (`🔒`) in the source file. Always use the actual literal character for matching, or use `replace_all=true` if the emoji appears in multiple contexts.

## Stream-Box (Electron) CSS-to-Compose Patterns

**Reference:** https://github.com/jonbarrow/stream-box — Electron desktop streaming app with HTML/CSS UI. No native Android code, but the CSS is useful for visual patterns.

### Focus Ring Sizing

**Source:** `box-shadow: 0 0 0 2px white` on `.kb-navigation-selected`
**Adaptation:** `BorderStroke(3.dp, NexColors.Accent)` — 3dp (slightly thicker than the default 2dp) gives a more visible TV focus ring. The white-to-accent color swap follows the app's design tokens.

**Source scale:** `transform: scale(1.2)` on `.media:hover` / `.media.selected`
**TV adaptation:** 1.08x (NOT 1.2x). 1.2x is too aggressive for TV — it clips adjacent cards and looks jumpy at 10-foot distance. 1.08x provides visible feedback without breaking the grid.

### Row Title + Decorative Divider

**Source:** CSS `::after` pseudo-element on `.section .title` — a 3px horizontal line that fills remaining space:
```css
.title::after {
    content: "";
    margin-left: 20px;
    background-color: #26282A;
    height: 3px;
    flex: 1;
}
```

**Compose adaptation with Row + weight:**
```kotlin
Row(verticalAlignment = Alignment.CenterVertically) {
    Text(title, modifier = Modifier.weight(1f, fill = false))
    Box(Modifier.weight(1f).height(2.dp).background(Divider))  // fills remaining space
    if (showSeeAll) { Text("See All", color = Accent) }
}
```

The `weight(1f, fill = false)` on the title keeps it at intrinsic width, while `weight(1f)` on the divider box fills the remaining row space. This replaces the previous `Box` overlay pattern (which used absolute positioning via `align()`) and is more predictable on TV layouts.

### Nav Slider Indicator

**Source:** An `<hr>` element under nav items that slides left/right based on `.selected` class:
```css
[data-navigation="home-page"].selected ~ hr { margin-left: 0; }
[data-navigation="search-page"].selected ~ hr { margin-left: 33%; }
```

**Compose adaptation:** Red left-accent bar (3dp wide) drawn on the active NavRail item. Already implemented — the stream-box pattern validates this approach vs. a bottom slider.

### Dark Background

**Source:** `background-color: #15161A` (very dark blue-gray)
**NexStream:** `0xFF050505` (OLED black). Stream-box targets desktop/Electron where true OLED isn't a factor. For Android TV on OLED displays, the deeper black is preferred for power efficiency and visual contrast.

### Backdrop Dimming

**Source:** `filter: brightness(40%)` on `.backdrop` images
**Compose adaptation:** Multi-stop gradient overlays (vertical + horizontal) rather than a uniform brightness filter. Gradients create a more cinematic \"spotlight\" effect and allow the image to remain visible on the right side while text is readable on the left.
