From b87008c7937f6a8dec461a0dc81aaf3d74a5f355 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 15 Aug 2026 21:56:06 -0400 Subject: [PATCH] chore(deps): bump @lizardbyte/gamepad-helper to 2026.816.4539 --- _data/licenses.yml | 2 +- _sass/styles.scss | 11 +- assets/js/gamepad-tester.js | 322 +++--------------------------------- gamepad-tester.html | 2 +- 4 files changed, 33 insertions(+), 304 deletions(-) diff --git a/_data/licenses.yml b/_data/licenses.yml index fabe25d3..3ab626bb 100644 --- a/_data/licenses.yml +++ b/_data/licenses.yml @@ -6,7 +6,7 @@ beautiful-jekyll: gamepad-helper: name: LizardByte/gamepad-helper # renovate: datasource=npm depName=@lizardbyte/gamepad-helper - version: '2026.815.220641' + version: '2026.816.4539' url: https://github.com/LizardByte/gamepad-helper/ license: MIT shared-web: diff --git a/_sass/styles.scss b/_sass/styles.scss index c095a4a2..6091c1a2 100644 --- a/_sass/styles.scss +++ b/_sass/styles.scss @@ -449,10 +449,15 @@ table tr:nth-child(2n) { position: relative; } -.gamepad-visual > .text-muted { +.gamepad-visual > .text-muted, +.gamepad-visual > .gamepad-visual-unavailable { grid-column: 1 / -1; } +.gamepad-visual-unavailable { + color: var(--bs-secondary-color); +} + .gamepad-visual-svg { display: block; height: auto; @@ -524,6 +529,10 @@ table tr:nth-child(2n) { width: 4rem; } +.gamepad-trigger-image-unavailable { + display: none; +} + .gamepad-trigger-name { display: none; font-weight: 700; diff --git a/assets/js/gamepad-tester.js b/assets/js/gamepad-tester.js index 8d2bacf3..239d277d 100644 --- a/assets/js/gamepad-tester.js +++ b/assets/js/gamepad-tester.js @@ -1,15 +1,5 @@ -function createDPadButtonVisuals(shapes) { - return shapes.map((shape, index) => ({ - index: index + 12, - ...shape - })); -} - -const FIREFOX_SWITCH_GAMEPAD_FIXED_VERSION = 155; - -function getFirefoxMajorVersion(userAgent) { - const match = /\bFirefox\/(\d+)/.exec(userAgent); - return match ? Number.parseInt(match[1], 10) : null; +function getGamepadColorScheme() { + return document.documentElement.dataset.bsTheme === 'dark' ? 'White' : 'Black'; } document.addEventListener('DOMContentLoaded', function() { @@ -25,107 +15,14 @@ document.addEventListener('DOMContentLoaded', function() { const gamepadStatus = document.getElementById('gamepad-status'); const gamepadStatusMessage = document.getElementById('gamepad-status-message'); const firefoxSwitchWarning = document.getElementById('firefox-switch-warning'); - const firefoxMajorVersion = getFirefoxMajorVersion(navigator.userAgent); - let gamepadVisualButtons = new Map(); - let gamepadVisualSticks = []; - let gamepadVisualTriggers = new Map(); - - const svgNamespace = 'http://www.w3.org/2000/svg'; const gamepadAssetBasePath = `https://cdn.jsdelivr.net/npm/@lizardbyte/gamepad-helper@${gamepadHelperVersion}/assets/img/gamepads/`; - const gamepadVisualConfigs = { - xbox: { - viewBox: '1050 450 2050 1350', - imagePaths: { - Black: 'xbox/Controller Images/Solid/Solid Black 4k.svg', - White: 'xbox/Controller Images/Solid/Solid White SVG.svg' - }, - buttons: [ - { index: 0, tag: 'circle', attributes: { cx: 2491, cy: 925, r: 70 } }, - { index: 1, tag: 'circle', attributes: { cx: 2606, cy: 811, r: 70 } }, - { index: 2, tag: 'circle', attributes: { cx: 2376, cy: 810, r: 70 } }, - { index: 3, tag: 'circle', attributes: { cx: 2491, cy: 695, r: 70 } }, - { index: 4, tag: 'path', attributes: { d: 'M1443,623.26s43.32-137.59,277-140.76c0,0,17.41,2.5,22.55,6.65,0,0,30.3,30.18,49.07,33.29C1791.65,522.44,1582.23,499.75,1443,623.26Z' } }, - { index: 5, tag: 'path', attributes: { d: 'M2653.21,623.26s-43.32-137.59-277-140.76c0,0-17.4,2.5-22.55,6.65,0,0-30.29,30.18-49.07,33.29C2304.6,522.44,2514,499.75,2653.21,623.26Z' } }, - { index: 8, tag: 'circle', attributes: { cx: 1922, cy: 808, r: 52 } }, - { index: 9, tag: 'circle', attributes: { cx: 2173, cy: 808, r: 52 } }, - { index: 10, tag: 'circle', attributes: { cx: 1602, cy: 816, r: 105 } }, - { index: 11, tag: 'circle', attributes: { cx: 2276, cy: 1073, r: 105 } }, - ...createDPadButtonVisuals([ - { tag: 'rect', attributes: { x: 1771, y: 964, width: 92, height: 82, rx: 13 } }, - { tag: 'rect', attributes: { x: 1771, y: 1139, width: 92, height: 82, rx: 13 } }, - { tag: 'rect', attributes: { x: 1692, y: 1046, width: 79, height: 93, rx: 13 } }, - { tag: 'rect', attributes: { x: 1863, y: 1046, width: 82, height: 93, rx: 13 } } - ]), - { index: 16, tag: 'circle', attributes: { cx: 2049, cy: 637, r: 65 } } - ], - sticks: [ - { buttonIndex: 10, axes: [0, 1], x: 1602, y: 816, range: 80 }, - { buttonIndex: 11, axes: [2, 3], x: 2276, y: 1073, range: 80 } - ] - }, - playstation: { - viewBox: '950 400 2200 1350', - imagePaths: { - Black: 'playstation/Controller Images/Solid/Solid Black SVG.svg', - White: 'playstation/Controller Images/Solid/Solid White SVG.svg' - }, - buttons: [ - { index: 0, tag: 'circle', attributes: { cx: 2668, cy: 966, r: 72 } }, - { index: 1, tag: 'circle', attributes: { cx: 2815, cy: 819, r: 72 } }, - { index: 2, tag: 'circle', attributes: { cx: 2521, cy: 819, r: 72 } }, - { index: 3, tag: 'circle', attributes: { cx: 2668, cy: 672, r: 72 } }, - { index: 4, tag: 'path', attributes: { d: 'M1306.92,530.22v-24s64.45-44.31,142-52.75,109.72,5,109.72,5a57.45,57.45,0,0,1,4.74,28.49S1402.82,497.78,1306.92,530.22Z' } }, - { index: 5, tag: 'path', attributes: { d: 'M2787.64,530.22v-24s-64.45-44.31-142-52.75-109.71,5-109.71,5a57.52,57.52,0,0,0-4.75,28.49S2691.74,497.78,2787.64,530.22Z' } }, - { index: 8, tag: 'rect', attributes: { x: 1545, y: 530, width: 80, height: 145, rx: 35, transform: 'rotate(-8 1585 602)' } }, - { index: 9, tag: 'rect', attributes: { x: 2470, y: 530, width: 80, height: 145, rx: 35, transform: 'rotate(8 2510 602)' } }, - { index: 10, tag: 'circle', attributes: { cx: 1724, cy: 1094, r: 115 } }, - { index: 11, tag: 'circle', attributes: { cx: 2370, cy: 1093, r: 115 } }, - ...createDPadButtonVisuals([ - { tag: 'path', attributes: { d: 'M1449.18,658.32s-11.48-1.24-24-1.24-24,1.24-24,1.24-26.51,1.17-28.49,23.66l1.59,48.59a24.8,24.8,0,0,0,5.44,14.73l35,38.58a6.74,6.74,0,0,0,4.16,2.45,40.6,40.6,0,0,0,6.35.53,41.32,41.32,0,0,0,7.07-.67,4.91,4.91,0,0,0,3-1.79l34.52-38a28.88,28.88,0,0,0,6.33-17.17l1.55-47.22C1475.15,659.49,1449.18,658.32,1449.18,658.32Z' } }, - { tag: 'path', attributes: { d: 'M1401.18,976.94s11.47,1.25,24,1.25,24-1.25,24-1.25,26.5-1.17,28.48-23.65l-1.59-48.6a24.79,24.79,0,0,0-5.43-14.73l-35-38.58a6.68,6.68,0,0,0-4.16-2.44,39.28,39.28,0,0,0-6.34-.53,41.49,41.49,0,0,0-7.08.66,5,5,0,0,0-3,1.79l-34.51,38a28.85,28.85,0,0,0-6.34,17.17l-1.55,47.23C1375.2,975.77,1401.18,976.94,1401.18,976.94Z' } }, - { tag: 'path', attributes: { d: 'M1265.87,793.63s-1.25,11.48-1.25,24,1.25,24,1.25,24,1.17,26.51,23.65,28.49l48.6-1.6a24.73,24.73,0,0,0,14.73-5.43l38.58-35a6.68,6.68,0,0,0,2.44-4.16,39.42,39.42,0,0,0,.53-6.35,41.46,41.46,0,0,0-.66-7.07,5.06,5.06,0,0,0-1.79-3l-38-34.51a28.87,28.87,0,0,0-17.18-6.33l-47.22-1.55C1267,767.65,1265.87,793.63,1265.87,793.63Z' } }, - { tag: 'path', attributes: { d: 'M1584.48,841.63s1.25-11.47,1.25-24-1.25-24-1.25-24-1.17-26.5-23.65-28.48l-48.6,1.59a24.79,24.79,0,0,0-14.72,5.43l-38.59,35a6.77,6.77,0,0,0-2.44,4.16,39.28,39.28,0,0,0-.53,6.34,41.21,41.21,0,0,0,.67,7.07,5,5,0,0,0,1.78,3l38,34.51a28.94,28.94,0,0,0,17.18,6.34l47.22,1.55C1583.31,867.61,1584.48,841.63,1584.48,841.63Z' } } - ]), - { index: 16, tag: 'circle', attributes: { cx: 2048, cy: 1094, r: 70 } }, - { index: 17, tag: 'path', attributes: { d: 'M2446.36,528.91l-.06-.3c.6-17-2.64-40.77-23.6-49.54-7.28-3.05-28.57-5.62-57.54-7.79-96.28-9.84-316.91-10.41-316.91-10.41h-.45s-220.63.57-316.91,10.41c-29,2.17-50.27,4.74-57.54,7.79-21,8.77-24.2,32.57-23.6,49.54,0,.1,0,.2-.07.3l.1.48a105.85,105.85,0,0,0,2.21,17.72l52.22,255.56q1.63,5.87,3.63,11.19A112,112,0,0,0,1718,834.93a106.93,106.93,0,0,0,26,29.1,110.63,110.63,0,0,0,34.7,18.12,119.88,119.88,0,0,0,15.93,4c2.53.46,5.11.87,7.78,1.21h491.27c2.66-.34,5.25-.75,7.78-1.21a119.88,119.88,0,0,0,15.93-4,110.63,110.63,0,0,0,34.7-18.12,106.74,106.74,0,0,0,26-29.1,111.54,111.54,0,0,0,10.17-21.07c1.32-3.55,2.54-7.28,3.63-11.19l52.22-255.56a105.85,105.85,0,0,0,2.21-17.72Z' } } - ], - sticks: [ - { buttonIndex: 10, axes: [0, 1], x: 1724, y: 1094, range: 85 }, - { buttonIndex: 11, axes: [2, 3], x: 2370, y: 1093, range: 85 } - ] - }, - switch: { - viewBox: '1050 450 2050 1350', - imagePaths: { - Black: 'switch/Controller Images/Pro Controller/Solid/Pro Controller Solid Black SVG.svg', - White: 'switch/Controller Images/Pro Controller/Solid/Pro Controller Solid White SVG.svg' - }, - buttons: [ - { index: 0, tag: 'circle', attributes: { cx: 2501, cy: 956, r: 70 } }, - { index: 1, tag: 'circle', attributes: { cx: 2637, cy: 841, r: 70 } }, - { index: 2, tag: 'circle', attributes: { cx: 2371, cy: 841, r: 70 } }, - { index: 3, tag: 'circle', attributes: { cx: 2501, cy: 725, r: 70 } }, - { index: 4, tag: 'path', attributes: { d: 'M1404.59,610.8c43.07-43.91,147.53-67.61,260.35-80.32l5.36-.59.62-.07c39.26-4.29,79.4-7.26,118.24-9.31C1759,511,1719,487.24,1719,487.24l-20-3.16c-210.46-1.59-285.89,86-285.89,86l-28.81,52.62A132.91,132.91,0,0,0,1404.59,610.8Z' } }, - { index: 5, tag: 'path', attributes: { d: 'M2424.51,529.82l.63.07,5.36.59c112.82,12.71,217.28,36.41,260.35,80.32a135.13,135.13,0,0,0,22.22,12.74l-29.3-53.49s-75.43-87.56-285.89-86l-20,3.16s-40.23,23.87-70.39,33.34C2345.93,522.63,2385.65,525.57,2424.51,529.82Z' } }, - { index: 8, tag: 'circle', attributes: { cx: 1837, cy: 711, r: 50 } }, - { index: 9, tag: 'circle', attributes: { cx: 2260, cy: 711, r: 50 } }, - { index: 10, tag: 'circle', attributes: { cx: 1578, cy: 841, r: 105 } }, - { index: 11, tag: 'circle', attributes: { cx: 2275, cy: 1072, r: 105 } }, - ...createDPadButtonVisuals([ - { tag: 'rect', attributes: { x: 1741, y: 950, width: 87, height: 83, rx: 17 } }, - { tag: 'rect', attributes: { x: 1741, y: 1120, width: 87, height: 83, rx: 17 } }, - { tag: 'rect', attributes: { x: 1658, y: 1033, width: 83, height: 87, rx: 17 } }, - { tag: 'rect', attributes: { x: 1828, y: 1033, width: 83, height: 87, rx: 17 } } - ]), - { index: 16, tag: 'circle', attributes: { cx: 2172, cy: 841, r: 55 } }, - { index: 17, tag: 'rect', attributes: { x: 1876, y: 791, width: 105, height: 105, rx: 18 } } - ], - sticks: [ - { buttonIndex: 10, axes: [0, 1], x: 1578, y: 841, range: 80 }, - { buttonIndex: 11, axes: [2, 3], x: 2275, y: 1072, range: 80 } - ] + const gamepadVisualizer = gamepadHelper.createVisualizer( + document.getElementById('controller-visual'), + { + assetBasePath: gamepadAssetBasePath, + colorScheme: getGamepadColorScheme() } - }; + ); // Check if the Gamepad API is supported if (!gamepadHelper.isSupported()) { @@ -140,7 +37,7 @@ document.addEventListener('DOMContentLoaded', function() { // Theme changed, reinitialize buttons with a new color scheme if (activeGamepadIndex !== null) { initGamepadButtons(); - initGamepadVisual(); + gamepadVisualizer.setColorScheme(getGamepadColorScheme()); } } }); @@ -214,15 +111,13 @@ document.addEventListener('DOMContentLoaded', function() { const gamepadIndices = Object.keys(gamepads); const hasGamepads = gamepadIndices.length > 0; - const hasNintendoSwitchController = Object.values(gamepads).some(gamepad => - gamepadHelper.detectControllerType(gamepad.id) === gamepadHelper.CONTROLLER_TYPES.SWITCH + const hasFirefoxSwitchMappingIssue = Object.values(gamepads).some(gamepad => + gamepadHelper.getCompatibilityIssues(gamepad).some(issue => + issue.code === 'firefox-switch-gamepad-mapping' + ) ); gamepadTester.classList.toggle('has-gamepad', hasGamepads); - firefoxSwitchWarning.hidden = !( - firefoxMajorVersion !== null - && firefoxMajorVersion < FIREFOX_SWITCH_GAMEPAD_FIXED_VERSION - && hasNintendoSwitchController - ); + firefoxSwitchWarning.hidden = !hasFirefoxSwitchMappingIssue; if (hasGamepads) { gamepadSelectorContainer.style.removeProperty('display'); @@ -265,154 +160,11 @@ document.addEventListener('DOMContentLoaded', function() { } } - function createSvgElement(tag, attributes = {}) { - const element = document.createElementNS(svgNamespace, tag); - - Object.entries(attributes).forEach(([name, value]) => { - element.setAttribute(name, value); - }); - - return element; - } - - function showVisualUnavailable(visualContainer) { - const message = document.createElement('span'); - message.className = 'text-muted'; - message.textContent = 'A visual is not available for this controller.'; - visualContainer.replaceChildren(message); - } - - function getGamepadVisualImagePath(config) { - const isDarkTheme = document.documentElement.dataset.bsTheme === 'dark'; - const colorScheme = isDarkTheme ? 'White' : 'Black'; - const encodedPath = config.imagePaths[colorScheme] - .split('/') - .map(pathPart => encodeURIComponent(pathPart)) - .join('/'); - - return gamepadAssetBasePath + encodedPath; - } - - function createTriggerVisual(controllerType, buttonIndex, colorScheme) { - const buttonName = gamepadHelper.getButtonName(controllerType, buttonIndex); - const trigger = document.createElement('div'); - trigger.className = 'gamepad-trigger-visual'; - trigger.setAttribute('role', 'progressbar'); - trigger.setAttribute('aria-label', `${buttonName} trigger pressure`); - trigger.setAttribute('aria-valuemin', '0'); - trigger.setAttribute('aria-valuemax', '1'); - trigger.setAttribute('aria-valuenow', '0'); - - const fill = document.createElement('span'); - fill.className = 'gamepad-trigger-fill'; - trigger.appendChild(fill); - - const buttonImagePath = gamepadHelper.getButtonImagePath( - controllerType, - buttonIndex, - gamepadAssetBasePath, - colorScheme - ); - const buttonImage = document.createElement('img'); - buttonImage.className = 'gamepad-trigger-image'; - buttonImage.src = buttonImagePath; - buttonImage.alt = buttonName; - trigger.appendChild(buttonImage); - - const fallback = document.createElement('span'); - fallback.className = 'gamepad-trigger-name'; - fallback.textContent = buttonName; - trigger.appendChild(fallback); - - buttonImage.addEventListener('error', function() { - buttonImage.classList.add('d-none'); - fallback.classList.add('visible'); - }, { once: true }); - - const value = document.createElement('span'); - value.className = 'gamepad-trigger-value'; - value.textContent = '0.00'; - trigger.appendChild(value); - - gamepadVisualTriggers.set(buttonIndex, { element: trigger, value }); - return trigger; - } - function initGamepadVisual() { - const visualContainer = document.getElementById('controller-visual'); - gamepadVisualButtons = new Map(); - gamepadVisualSticks = []; - gamepadVisualTriggers = new Map(); - visualContainer.replaceChildren(); - - if (activeGamepadIndex === null) { - showVisualUnavailable(visualContainer); - return; - } - - const gamepad = navigator.getGamepads()[activeGamepadIndex]; - if (!gamepad) { - showVisualUnavailable(visualContainer); - return; - } - - const gamepadInfo = gamepadHelper.getGamepadInfo(gamepad.id); - const config = gamepadVisualConfigs[gamepadInfo.type]; - if (!config) { - showVisualUnavailable(visualContainer); - return; - } - - const isDarkTheme = document.documentElement.dataset.bsTheme === 'dark'; - const colorScheme = isDarkTheme ? 'White' : 'Black'; - visualContainer.appendChild(createTriggerVisual(gamepadInfo.type, 6, colorScheme)); - - const visual = createSvgElement('svg', { - class: 'gamepad-visual-svg', - viewBox: config.viewBox, - role: 'img', - 'aria-label': `${gamepadInfo.name} input visual` - }); - const title = createSvgElement('title'); - title.textContent = `${gamepadInfo.name} input visual`; - visual.appendChild(title); - - const controllerImage = createSvgElement('image', { - href: getGamepadVisualImagePath(config), - width: 4096, - height: 2160, - preserveAspectRatio: 'xMidYMid meet' - }); - controllerImage.addEventListener('error', function() { - showVisualUnavailable(visualContainer); - }, { once: true }); - visual.appendChild(controllerImage); - - config.buttons.forEach(button => { - const control = createSvgElement(button.tag, button.attributes); - control.classList.add('gamepad-visual-control'); - control.dataset.buttonIndex = button.index; - visual.appendChild(control); - gamepadVisualButtons.set(button.index, control); - }); - - config.sticks.forEach(stick => { - const indicator = createSvgElement('circle', { - class: 'gamepad-stick-indicator', - cx: stick.x, - cy: stick.y, - r: 28 - }); - visual.appendChild(indicator); - gamepadVisualSticks.push({ - ...stick, - buttonElement: gamepadVisualButtons.get(stick.buttonIndex), - indicator - }); - }); - - visualContainer.appendChild(visual); - visualContainer.appendChild(createTriggerVisual(gamepadInfo.type, 7, colorScheme)); + const gamepad = activeGamepadIndex === null + ? null + : navigator.getGamepads()[activeGamepadIndex]; + gamepadVisualizer.mount(gamepad); } // Initialize gamepad button UI elements @@ -427,16 +179,14 @@ document.addEventListener('DOMContentLoaded', function() { const controllerType = gamepadHelper.detectControllerType(gamepad.id); - // Detect the current theme - use Black icons for the light theme, White icons for the dark theme - const isDarkTheme = document.documentElement.dataset.bsTheme === 'dark'; - const colorScheme = isDarkTheme ? 'White' : 'Black'; + const colorScheme = getGamepadColorScheme(); for (let i = 0; i < gamepad.buttons.length; i++) { const buttonName = gamepadHelper.getButtonName(controllerType, i); const buttonImagePath = gamepadHelper.getButtonImagePath( controllerType, i, - `https://cdn.jsdelivr.net/npm/@lizardbyte/gamepad-helper@${gamepadHelperVersion}/assets/img/gamepads/`, + gamepadAssetBasePath, colorScheme ); @@ -644,36 +394,6 @@ document.addEventListener('DOMContentLoaded', function() { } } - function updateGamepadVisual(gamepad) { - for (let i = 0; i < gamepad.buttons.length; i++) { - const control = gamepadVisualButtons.get(i); - if (!control) continue; - - const button = gamepad.buttons[i]; - const isActive = button.pressed || button.value > 0.1; - const opacity = Math.max(0.35, button.value * 0.9); - control.classList.toggle('active', isActive); - control.style.fillOpacity = isActive ? opacity.toFixed(2) : '0'; - } - - gamepadVisualTriggers.forEach((trigger, buttonIndex) => { - const button = gamepad.buttons[buttonIndex]; - const value = Math.max(0, Math.min(1, button?.value || 0)); - const isActive = Boolean(button?.pressed) || value > 0.1; - trigger.element.classList.toggle('active', isActive); - trigger.element.style.setProperty('--gamepad-trigger-value', `${(value * 100).toFixed(0)}%`); - trigger.element.setAttribute('aria-valuenow', value.toFixed(2)); - trigger.value.textContent = value.toFixed(2); - }); - - gamepadVisualSticks.forEach(stick => { - const horizontalValue = Math.max(-1, Math.min(1, gamepad.axes[stick.axes[0]] || 0)); - const verticalValue = Math.max(-1, Math.min(1, gamepad.axes[stick.axes[1]] || 0)); - const transform = `translate(${(horizontalValue * stick.range).toFixed(1)} ${(verticalValue * stick.range).toFixed(1)})`; - stick.buttonElement.setAttribute('transform', transform); - stick.indicator.setAttribute('transform', transform); - }); - } // Update axes UI states function updateAxes(gamepad) { @@ -840,7 +560,7 @@ document.addEventListener('DOMContentLoaded', function() { // Update all the UI elements updateGamepadInfo(gamepad); updateButtons(gamepad); - updateGamepadVisual(gamepad); + gamepadVisualizer.update(gamepad); updateAxes(gamepad); updateRawData(gamepad); } diff --git a/gamepad-tester.html b/gamepad-tester.html index e7e399a5..287dabec 100644 --- a/gamepad-tester.html +++ b/gamepad-tester.html @@ -8,7 +8,7 @@ ext-js: - href: "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.5.0/chart.umd.min.js" sri: "sha512-Y51n9mtKTVBh3Jbx5pZSJNDDMyY+yGe77DGtBPzRlgsf/YLCh13kSZ3JmfHGzYFCmOndraf0sQgfM654b7dJ3w==" - - href: "https://cdn.jsdelivr.net/npm/@lizardbyte/gamepad-helper@2026.815.220641/dist/gamepad-helper.js" + - href: "https://cdn.jsdelivr.net/npm/@lizardbyte/gamepad-helper@2026.816.4539/dist/gamepad-helper.js" js: - /assets/js/gamepad-tester.js ---