- 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>
4.7 KiB
4.7 KiB
name, description
| name | description |
|---|---|
| gamepad-nav | 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 zonesdata-controller-container— focusable card/group (Enter drills in, Escape exits)data-controller-focusable— marks element as focusabledata-controller-ignore— excludes from navigationdata-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 - alignmentwith 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
/* 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()inrequestAnimationFrameloop (cheap, returns snapshot) - Apply deadzone:
Math.abs(axis) > 0.2before 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
- Check
useControllerNav.tsfor thehandleKeyDownfunction - Check
data-controller-*attributes in the view's template - Verify focusable elements are in the right
data-controller-zone - Test with: arrow keys on keyboard (simulates D-pad)
- Check
style.cssforfocus-visiblerules