Dorian 901b9f660f 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

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 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

/* 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