- 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>
115 lines
4.7 KiB
Markdown
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)
|