Dorian aada19754d feat: gamepad navigation rewrite, focus styling, container grid system
- Rewrite useControllerNav.ts with clean console-style navigation:
  Sidebar (up/down wrap, right→containers, left→nothing),
  Container tile grid (spatial nav, no wrap at edges),
  Nav bar support (up from containers, down to grid),
  Inner controls (enter drills in, escape exits, trapped arrows)
- Add data-controller-container to Mesh, Fleet, Settings pages
- Fix Home.vue fragment (modals outside root div) causing Vue warnings
- Remove skip-to-content link (handled by controller nav)
- Orange ambient glow focus styling matching glass aesthetic
- Disable PWA service worker in dev mode (fixes HMR caching)
- Add gamepad-nav skill and GAMEPAD-NAV-MAP.md spec document
- 39 tests covering all navigation patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:01:17 +00:00

115 lines
4.7 KiB
Markdown

---
name: gamepad-nav
description: Expert-level gamepad/controller navigation for Archipelago's console-style UI. Use when working on D-pad navigation, focus management, spatial navigation, controller support, or 10-foot UI design.
---
# Gamepad Navigation Expert
When working on gamepad/controller navigation in Archipelago, apply these console-derived patterns.
## Architecture
**File**: `neode-ui/src/composables/useControllerNav.ts`
**Styles**: `neode-ui/src/style.css` (focus-visible rules)
The system uses `data-` attributes for navigation zones:
- `data-controller-zone="sidebar"` / `"main"` — navigation zones
- `data-controller-container` — focusable card/group (Enter drills in, Escape exits)
- `data-controller-focusable` — marks element as focusable
- `data-controller-ignore` — excludes from navigation
- `data-controller-install` / `data-controller-launch` — app-specific actions
## Core Navigation Rules (Xbox/PS5/Switch consensus)
### D-pad Movement
- **4 directions only** — Up/Down/Left/Right, one element per press
- **Spatial navigation** — find nearest focusable in direction using bounding rect geometry
- **Distance formula**: `euclidean + displacement - alignment` with overlap scoring
- **Tiebreaker for up/down**: prefer leftmost element (visual consistency in grids)
### Wrapping
- **Linear lists (1D)**: WRAP (last to first, first to last) — sidebar menu, tab bars
- **Grids (2D)**: NO WRAP — stops at edges, prevents disorientation
### Zone Transitions
- **Right from sidebar** -> first focusable in main content (topmost)
- **Left from main's leftmost** -> sidebar's active tab (`.nav-tab-active`)
- **Focus memory**: remember last-focused element per zone, restore on re-entry
### Container Navigation
- **Enter/A**: drill into container (focus first inner element)
- **Escape/B**: exit container (focus the container itself)
- **D-pad inside container**: navigate among inner elements spatially
- **D-pad at container edge**: exit and navigate to adjacent container
### Text Input Handling
- **Up/Down arrows**: EXIT input, navigate to nearest element above/below
- **Left/Right arrows**: stay in input (cursor movement)
- **Enter**: if next focusable is a button, click it directly (submit)
- **Escape**: blur input, navigate out
### Button Mapping
| Action | Xbox | PlayStation | Switch | Keyboard |
|--------|------|------------|--------|----------|
| Confirm | A | Cross | A | Enter |
| Back | B | Circle | B | Escape |
| Navigate | D-pad | D-pad | D-pad | Arrow keys |
## Focus Visual Design
### Console standard (10-foot viewing distance)
- **Minimum 2px** border/outline (1px flickers on interlaced TVs)
- **3:1 contrast ratio** against adjacent colors (WCAG 2.4.7)
- **Smooth transitions**: 150-200ms ease-out
- **GPU compositing**: use `translateZ(0)` on animated elements
- **Never pure white** (#f1f1f1 prevents TV halo effects)
### Archipelago Focus Patterns
```css
/* Global — subtle outline that follows border-radius */
*:focus-visible {
outline: 2px solid rgba(251, 146, 60, 0.6);
outline-offset: 2px;
}
/* Containers — soft glow + slight scale */
[data-controller-container]:focus-visible {
outline: none;
transform: scale(1.01);
box-shadow: 0 0 0 1.5px rgba(251, 146, 60, 0.5),
0 0 20px rgba(251, 146, 60, 0.15);
}
/* Sidebar items — background tint + thin ring */
.sidebar-nav-item:focus-visible {
outline: none;
background: rgba(251, 146, 60, 0.12);
box-shadow: 0 0 0 1.5px rgba(251, 146, 60, 0.45);
}
```
## Gamepad API Integration
### Polling
- Poll `navigator.getGamepads()` in `requestAnimationFrame` loop (cheap, returns snapshot)
- Apply deadzone: `Math.abs(axis) > 0.2` before registering input
- D-pad repeat: 400ms initial delay, 150ms interval (gamepads don't auto-repeat)
### Button indices (W3C Standard Mapping)
- 0=A, 1=B, 2=X, 3=Y, 4=LB, 5=RB, 12=DUp, 13=DDown, 14=DLeft, 15=DRight
## When Investigating Issues
1. Check `useControllerNav.ts` for the `handleKeyDown` function
2. Check `data-controller-*` attributes in the view's template
3. Verify focusable elements are in the right `data-controller-zone`
4. Test with: arrow keys on keyboard (simulates D-pad)
5. Check `style.css` for `focus-visible` rules
## Key Sources
- [Xbox Accessibility Guideline 112](https://learn.microsoft.com/en-us/gaming/accessibility/xbox-accessibility-guidelines/112)
- [Microsoft: Gamepad and remote interactions](https://learn.microsoft.com/en-us/windows/apps/design/input/gamepad-and-remote-interactions)
- [W3C CSS Spatial Navigation](https://www.w3.org/TR/css-nav-1/)
- [W3C Gamepad Spec](https://w3c.github.io/gamepad/)
- [Norigin Spatial Navigation (React reference)](https://github.com/NoriginMedia/Norigin-Spatial-Navigation)