feedBack/static/v3
Matthew Harris Glover 23c509322b
Add gamepad/controller support (#1001)
* feat(input): add gamepad/controller support

Adds full gamepad/controller navigation and playback control, driven
by requests from players who use fee[dB]ack on a TV/console setup and
from wheelchair users for whom a controller is far more convenient
than a keyboard + mouse. Confirmed working end-to-end on a Steam Deck
across several rounds of on-device testing.

- static/v3/gamepad.js: polls navigator.getGamepads() and dispatches
  synthetic keydown events (Arrow/Enter/Space/Escape) on the focused
  element (falling back to document), reusing the app's existing
  keyboard pipeline (static/js/shortcuts.js's scope-aware dispatcher,
  player shortcuts, text-field/modal guards) instead of a parallel
  action-mapping table. Only acts on gamepads reporting the W3C
  "standard" mapping — which is what Steam Input presents for the
  Deck's built-in controls, both in Gaming Mode and in Desktop Mode
  via a non-Steam shortcut — so button order is guaranteed correct
  and a non-standard/raw device safely no-ops instead of misfiring.
  Handles Steam Input's virtual-pad duplicates (a real controller
  plus 1-2 mirrored XInput slots) without spamming connect toasts or
  losing input when the live pad isn't at index 0. Xbox-style face
  button mapping: bottom face = Space (play/pause, and activates the
  focused control), right face = Escape (back), top face reveals the
  player screen's tool rail (focuses it into visibility via the
  existing CSS :focus-within rule). D-pad/stick repeat while held,
  mirroring OS keyboard auto-repeat.

- static/v3/gamepad-nav.js: fills the one real gap in that reuse
  strategy — no screen but the song library grid had any arrow-key
  navigation, and Chromium doesn't run native Enter/Space button
  activation for untrusted synthetic events even when dispatched at
  the focused element. Gated entirely on `!e.isTrusted`, so it only
  ever reacts to gamepad-originated events and never touches real
  keyboard/mouse users: emulates Tab-order (the sidebar + active
  screen's real, already-focusable buttons/links) for Arrow keys,
  explicitly .click()s the focused element for Enter/Space, and gives
  Escape a consistent "go back" behavior — an existing in-screen back
  button if one's visible (reusing each screen's own drill-down logic
  for free), else the main menu. Every branch defers via
  `e.defaultPrevented` to any screen that already handles the key
  itself (the song grid, the player, settings), so nothing here
  overrides existing behavior.

- static/v3/songs.js: adds real 2D d-pad/arrow-key navigation to the
  song library's virtualized grid (only a slice of the library is
  ever in the DOM), including fetching/scrolling off-screen rows into
  view and correcting for the sticky filter toolbar's occlusion.

- static/v3/index.html: wires up the two new scripts.

* chore: regenerate stale tailwind.min.css

Rebuilt in a fresh clone (not the local working copy). Several plugin
directories (audio_engine, plugin_manager, community_charts, etc.) are
gitignored locally but present on disk from checking out plugin repos
for local dev/testing — Tailwind's content scan picks them up
regardless, so a rebuild against the contaminated local working copy
bakes in extra utility classes that don't belong in the real,
git-tracked build. A clean checkout reproduces CI's expected output
exactly.

* fix(gamepad): check all matching back buttons, not just the first

document.querySelector on the combined [data-ap-back], [data-albums-back],
#v3-pl-back selector only ever inspects the first match in DOM order —
since screens stay in the DOM (hidden, not removed) when you navigate
away, a hidden back button from an unrelated screen could sort before
the one that's actually visible, incorrectly falling through to
showScreen('v3-home') instead of clicking it. Uses querySelectorAll +
find(visible) instead.

* fix(gamepad): address CodeRabbit findings on connect/disconnect and grid nav

- gamepad.js: anyLiveConnectedPad -> anyLiveStandardPad, filtering by
  mapping === 'standard' like firstLiveStandardPad already does, and
  applied at the top of the gamepadconnected handler too. A still-
  connected non-standard raw mirror could otherwise mask the real
  pad's disconnect (toast never fires, polling never stops).

- songs.js _gpMove: an unset cursor now always seeds at index 0
  before the first press, instead of applying that press's delta
  immediately (ArrowDown/Right previously skipped straight past row
  0; Left/Up only looked right by accident of clamping). Matches the
  existing convention in shortcuts.js's legacy _handleLibArrowNav.

- songs.js _gpBlockedTarget: form-control/button blocking now
  requires the element to be visible (offsetParent !== null), not
  just present. Screens stay in the DOM hidden (not removed) when you
  navigate away, so a real button focused on some other now-hidden
  screen could leave document.activeElement pointing at it and block
  all grid navigation indefinitely. (An el.closest('#v3-songs') scope
  was tried first and reverted — it fixed that case but broke
  blocking for the topbar search input, which lives outside
  #v3-songs's DOM subtree even while v3-songs is active; visibility
  is the distinction that actually matters, not DOM nesting.)

Skipped two CodeRabbit suggestions, verified against current code:
gating songs.js's grid keydown listener to synthetic-only events
would regress the real keyboard accessibility this PR intentionally
added (v3-songs' grid had none before); renaming the _gp* helpers to
drop their underscore prefix would break from this codebase's own
established module-private naming convention.

Verified in-browser: first arrow press lands on index 0, stale hidden
focus no longer blocks grid nav, the topbar search input still
correctly blocks it, and normal nav resumes after blur.

* test(gamepad): unit-cover the controller + nav state machines

- gamepad.test.js (10): standard-mapping filter, Steam Input duplicate-slot
  dedup, disconnect masking, button edge-detection, d-pad/stick repeat timing,
  analog deadzone — driven via a fake navigator + manual rAF queue.
- gamepad_nav.test.js (10): !isTrusted/defaultPrevented gating, arrow focus
  traversal + clamping, hidden-element skipping, Enter/Space click activation
  (not into text fields/body), Escape visible-back-button vs home fallback.

songs.js grid nav is left to on-device coverage (async + windowed-DOM heavy).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Byron Gamatos <xasiklas@gmail.com>

---------

Signed-off-by: Byron Gamatos <xasiklas@gmail.com>
Co-authored-by: Byron Gamatos <xasiklas@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 11:52:51 +02:00
..
avatars Clean release snapshot 2026-06-16 18:47:13 +02:00
brand feat(v3): use PNG logo in sidebar nav instead of the text wordmark (#734) 2026-07-03 11:39:27 +02:00
pedals Clean release snapshot 2026-06-16 18:47:13 +02:00
audio-routing.js feat(a11y): app-wide "Interface size" setting (Accessibility) (#664) 2026-07-02 14:00:30 +02:00
badges.js Harden freqs_to_midis against NaN/Inf; badges read exact tuningMidis 2026-07-12 15:24:39 -05:00
brand.js Clean release snapshot 2026-06-16 18:47:13 +02:00
card-actions-core.js v3 library: song Details drawer + bulk edit — P2 (#703) 2026-07-02 13:29:05 +02:00
dashboard.js feat(career): profile passport wall, home career card, shareable PNG card (#955) 2026-07-14 01:25:39 +02:00
feedbarcade.js refactor(ui): defer every classic script, keep boot() on DOMContentLoaded (R3a) (#872) 2026-07-11 16:57:04 +02:00
gamepad-nav.js Add gamepad/controller support (#1001) 2026-07-19 11:52:51 +02:00
gamepad.js Add gamepad/controller support (#1001) 2026-07-19 11:52:51 +02:00
image-picker.js feat(v3 library): searchable Cover Art Archive picker in Change-cover (#783) 2026-07-05 23:36:49 +02:00
index.html Add gamepad/controller support (#1001) 2026-07-19 11:52:51 +02:00
interface-size-nudge.js refactor(ui): defer every classic script, keep boot() on DOMContentLoaded (R3a) (#872) 2026-07-11 16:57:04 +02:00
interface-size-ui.js refactor(ui): defer every classic script, keep boot() on DOMContentLoaded (R3a) (#872) 2026-07-11 16:57:04 +02:00
lessons.js fix(v3): floor accuracy percentages so 100% means all notes hit 2026-07-13 00:35:09 +02:00
live-guitar-tone-source.js refactor(ui): defer every classic script, keep boot() on DOMContentLoaded (R3a) (#872) 2026-07-11 16:57:04 +02:00
live-performance-hud.js fix(v3): floor accuracy percentages so 100% means all notes hit 2026-07-13 00:35:09 +02:00
manifest.json Clean release snapshot 2026-06-16 18:47:13 +02:00
match-review.js refactor(ui): defer every classic script, keep boot() on DOMContentLoaded (R3a) (#872) 2026-07-11 16:57:04 +02:00
notifications.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
onboarding-tour.js feat(v3): choose handedness in the instrument selector + onboarding (give lefties a break) (#793) 2026-07-05 23:50:30 +02:00
overlay-active.png Clean release snapshot 2026-06-16 18:47:13 +02:00
overlay-inactive.png Clean release snapshot 2026-06-16 18:47:13 +02:00
pedal-cables.js rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
pedal-default.svg Clean release snapshot 2026-06-16 18:47:13 +02:00
pedal-frame.svg Clean release snapshot 2026-06-16 18:47:13 +02:00
player-chrome.js refactor(highway): make the highway global explicit before the module flip (R3c) (#912) 2026-07-12 11:56:07 +02:00
playlists.js feat(playlists): flag songs that are not in your current tuning (#1009) 2026-07-19 00:10:53 -05:00
plugins-page.js refactor(ui): defer every classic script, keep boot() on DOMContentLoaded (R3a) (#872) 2026-07-11 16:57:04 +02:00
profile.js feat(career): profile passport wall, home career card, shareable PNG card (#955) 2026-07-14 01:25:39 +02:00
progress.js fix(v3): floor accuracy percentages so 100% means all notes hit 2026-07-13 00:35:09 +02:00
progression-core.js refactor(ui): defer every classic script, keep boot() on DOMContentLoaded (R3a) (#872) 2026-07-11 16:57:04 +02:00
scoreboard-pref.js fix(highway): user-selectable scoreboard to stop duplicate HUDs 2026-06-19 20:56:38 +02:00
settings.js refactor(ui): defer every classic script, keep boot() on DOMContentLoaded (R3a) (#872) 2026-07-11 16:57:04 +02:00
shell.js feat(career): career plugin — stars, venue tiers, pack downloads (career mode 2/3) (#907) 2026-07-12 22:39:19 +02:00
shop.js refactor(ui): defer every classic script, keep boot() on DOMContentLoaded (R3a) (#872) 2026-07-11 16:57:04 +02:00
songs.js Add gamepad/controller support (#1001) 2026-07-19 11:52:51 +02:00
stats-recorder.js fix(career): hours polish — recency stamp + non-2xx POST is a failure (#947) 2026-07-13 15:31:12 +02:00
theme-core.js feat(v3): host theme read surface — window.feedBack.theme + always-present --fbv-* tokens (#646) 2026-07-02 13:57:58 +02:00
tuner-core.js Clean release snapshot 2026-06-16 18:47:13 +02:00
v3.css feat(a11y): app-wide "Interface size" setting (Accessibility) (#664) 2026-07-02 14:00:30 +02:00
venue-crowd.js fix(venue): fly in once per set, not before every song (#978) 2026-07-15 12:39:25 +02:00
venue-instrument-pov.js Clean release snapshot 2026-06-16 18:47:13 +02:00
venue-mood-fx.js refactor(ui): defer every classic script, keep boot() on DOMContentLoaded (R3a) (#872) 2026-07-11 16:57:04 +02:00
venue-scene-3d.js fix(venue/highway): flyover replay on arrangement switch, venue on Virtuoso, and the paused throttle starving the venue (#968) 2026-07-14 22:11:52 +02:00
venue-viz.js Clean release snapshot 2026-06-16 18:47:13 +02:00