From b141c0a0582cd29c75d5ebd7f44f39850973c500 Mon Sep 17 00:00:00 2001 From: Andrew Zambazos <62979495+Bobbybear007@users.noreply.github.com> Date: Sun, 17 May 2026 19:40:44 +1200 Subject: [PATCH] Add gamepad profiles and controller UI improvements Introduce a new gamepadProfile module to detect controller type, map glyphs, and watch for active gamepad changes. Integrate controller profile and glyph refresh into app state (auto-refresh + event) and start the watcher during initialization. Update lock view to show controller badge, contextual copy, improved status/confirm handling, and use a new lock hints template. Move and scope guide backdrop into the main area and tweak related CSS; refine guide styling. Refactor keyboard overlay layout and behavior: rows instead of grid, variable key widths, caps/shift modifiers, improved key activation and navigation. Minor text change in user setup copy and assorted styling adjustments. --- src/core/gamepadProfile.js | 183 +++++++++++++++++++++++++ src/core/state.js | 64 ++++----- src/index.html | 15 +- src/main.js | 9 ++ src/styles/base.css | 1 + src/styles/guide.css | 10 +- src/views/lock/lock.css | 65 +++++++++ src/views/lock/lock.js | 98 +++++++++++-- src/views/onboarding/userSetup.js | 2 +- src/views/overlays/keyboard.css | 58 ++++++-- src/views/overlays/keyboard.js | 221 ++++++++++++++++++++++++------ 11 files changed, 614 insertions(+), 112 deletions(-) create mode 100644 src/core/gamepadProfile.js diff --git a/src/core/gamepadProfile.js b/src/core/gamepadProfile.js new file mode 100644 index 0000000..839c097 --- /dev/null +++ b/src/core/gamepadProfile.js @@ -0,0 +1,183 @@ +const PROFILE_GLYPH_MAP = { + xbox: { + accept: "A", + back: "B", + menu: "☰", + up: "↑", + down: "↓", + left: "←", + right: "→", + l1: "LB", + r1: "RB", + l2: "LT", + r2: "RT", + clear: "X", + y: "Y", + }, + playstation: { + accept: "✕", + back: "○", + menu: "☰", + up: "↑", + down: "↓", + left: "←", + right: "→", + l1: "L1", + r1: "R1", + l2: "L2", + r2: "R2", + clear: "□", + y: "△", + }, + switch: { + accept: "B", + back: "A", + menu: "+", + up: "↑", + down: "↓", + left: "←", + right: "→", + l1: "L", + r1: "R", + l2: "ZL", + r2: "ZR", + clear: "Y", + y: "X", + }, + generic: { + accept: "A", + back: "B", + menu: "☰", + up: "↑", + down: "↓", + left: "←", + right: "→", + l1: "L1", + r1: "R1", + l2: "L2", + r2: "R2", + clear: "X", + y: "Y", + }, +}; + +const GLYPH_ACTIONS = [ + "accept", + "back", + "menu", + "up", + "down", + "left", + "right", + "l1", + "r1", + "l2", + "r2", + "clear", + "y", +]; + +const CORE_GLYPH_IDS = { + accept: "confirm", + back: "back", + menu: "menu", + up: "dpad-up", + down: "dpad-down", + left: "dpad-left", + right: "dpad-right", + l1: "lb", + r1: "rb", + l2: "lt", + r2: "rt", + clear: "x", + y: "y", +}; + +export const CONTROLLER_PROFILES = ["xbox", "playstation", "switch", "generic"]; + +export const PROFILE_LABELS = { + xbox: "Xbox", + playstation: "PlayStation", + switch: "Nintendo Switch", + generic: "controller", +}; + +export const detectGamepadProfile = (gamepad) => { + const id = `${gamepad?.id ?? ""} ${gamepad?.mapping ?? ""}`.toLowerCase(); + + if (/xbox|x-input|microsoft|045e|8bitdo.*xbox|8bitdo.*x/i.test(id)) { + return "xbox"; + } + + if (/playstation|dualsense|dualshock|ps5|ps4|054c|sony|wireless controller/i.test(id)) { + return "playstation"; + } + + if (/nintendo|switch|pro controller|057e|joy-con|joycon/i.test(id)) { + return "switch"; + } + + return "generic"; +}; + +export const getActiveGamepad = () => { + const pads = navigator.getGamepads?.() ?? []; + return pads.find((pad) => pad?.connected) ?? null; +}; + +export const detectActiveProfile = () => { + const pad = getActiveGamepad(); + return pad ? detectGamepadProfile(pad) : "generic"; +}; + +export const getFallbackGlyphs = (profile = "generic") => ({ + ...(PROFILE_GLYPH_MAP[profile] ?? PROFILE_GLYPH_MAP.generic), +}); + +export const resolveGlyphsForProfile = (profile, glyphsModule) => { + const safeProfile = CONTROLLER_PROFILES.includes(profile) ? profile : "generic"; + const platform = safeProfile === "generic" ? "xbox" : safeProfile; + const fallback = getFallbackGlyphs(safeProfile); + + if (typeof glyphsModule?.getGlyph !== "function") { + return fallback; + } + + const resolved = {}; + GLYPH_ACTIONS.forEach((action) => { + const glyphId = CORE_GLYPH_IDS[action]; + resolved[action] = glyphsModule.getGlyph(platform, glyphId) ?? fallback[action]; + }); + return resolved; +}; + +export const watchGamepadProfile = (onChange) => { + let current = detectActiveProfile(); + + const emitIfChanged = () => { + const next = detectActiveProfile(); + if (next === current) { + return; + } + current = next; + onChange(next); + }; + + window.addEventListener("gamepadconnected", emitIfChanged); + window.addEventListener("gamepaddisconnected", emitIfChanged); + + let rafId = 0; + const poll = () => { + emitIfChanged(); + rafId = requestAnimationFrame(poll); + }; + rafId = requestAnimationFrame(poll); + + return () => { + window.removeEventListener("gamepadconnected", emitIfChanged); + window.removeEventListener("gamepaddisconnected", emitIfChanged); + if (rafId) { + cancelAnimationFrame(rafId); + } + }; +}; diff --git a/src/core/state.js b/src/core/state.js index 51ec883..48b8e53 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -1,3 +1,9 @@ +import { + detectActiveProfile, + getFallbackGlyphs, + resolveGlyphsForProfile, + watchGamepadProfile, +} from "./gamepadProfile.js"; import { createPasskeyController } from "./passkey.js"; import { loadExistingUser } from "./users.js"; @@ -33,22 +39,6 @@ const FALLBACK_THEME = { }, }; -const FALLBACK_GLYPHS = { - accept: "A", - back: "B", - menu: "≡", - up: "↑", - down: "↓", - left: "←", - right: "→", - l1: "LB", - r1: "RB", - l2: "LT", - r2: "RT", - clear: "X", - y: "Y", -}; - export const createAppState = () => { const passkey = createPasskeyController(); const state = { @@ -68,7 +58,8 @@ export const createAppState = () => { ui: null, }, theme: FALLBACK_THEME, - glyphs: { ...FALLBACK_GLYPHS }, + controllerProfile: "generic", + glyphs: getFallbackGlyphs("generic"), settingsCategory: "system", settingsValues: { network: true, @@ -80,6 +71,25 @@ export const createAppState = () => { passkeySetupRequired: !passkey.hasPasskey(), }; + state.refreshControllerGlyphs = (profile = detectActiveProfile()) => { + state.controllerProfile = profile; + state.glyphs = resolveGlyphsForProfile(profile, state.nebula.glyphs); + window.dispatchEvent( + new CustomEvent("nebula-controller-profile", { + detail: { profile }, + }), + ); + }; + + state.startGamepadProfileWatcher = () => { + if (state.stopGamepadProfileWatcher) { + state.stopGamepadProfileWatcher(); + } + state.stopGamepadProfileWatcher = watchGamepadProfile((profile) => { + state.refreshControllerGlyphs(profile); + }); + }; + const applyThemeToDocument = () => { const root = document.documentElement; const { colors, spacing, radius, typography } = state.theme; @@ -112,23 +122,7 @@ export const createAppState = () => { state.theme = typeof theme.createTheme === "function" ? theme.createTheme({}) : FALLBACK_THEME; - if (typeof glyphs.getGlyph === "function") { - state.glyphs = { - accept: glyphs.getGlyph("xbox", "confirm") ?? FALLBACK_GLYPHS.accept, - back: glyphs.getGlyph("xbox", "back") ?? FALLBACK_GLYPHS.back, - menu: glyphs.getGlyph("xbox", "menu") ?? FALLBACK_GLYPHS.menu, - up: glyphs.getGlyph("xbox", "dpad-up") ?? FALLBACK_GLYPHS.up, - down: glyphs.getGlyph("xbox", "dpad-down") ?? FALLBACK_GLYPHS.down, - left: glyphs.getGlyph("xbox", "dpad-left") ?? FALLBACK_GLYPHS.left, - right: glyphs.getGlyph("xbox", "dpad-right") ?? FALLBACK_GLYPHS.right, - l1: glyphs.getGlyph("xbox", "lb") ?? FALLBACK_GLYPHS.l1, - r1: glyphs.getGlyph("xbox", "rb") ?? FALLBACK_GLYPHS.r1, - l2: glyphs.getGlyph("xbox", "lt") ?? FALLBACK_GLYPHS.l2, - r2: glyphs.getGlyph("xbox", "rt") ?? FALLBACK_GLYPHS.r2, - clear: glyphs.getGlyph("xbox", "x") ?? FALLBACK_GLYPHS.clear, - y: glyphs.getGlyph("xbox", "y") ?? FALLBACK_GLYPHS.y, - }; - } + state.refreshControllerGlyphs(detectActiveProfile()); } catch (_error) { state.nebula = { ...state.nebula, @@ -136,7 +130,7 @@ export const createAppState = () => { source: "local-fallback", }; state.theme = FALLBACK_THEME; - state.glyphs = { ...FALLBACK_GLYPHS }; + state.refreshControllerGlyphs("generic"); } applyThemeToDocument(); diff --git a/src/index.html b/src/index.html index b479205..8c9c358 100644 --- a/src/index.html +++ b/src/index.html @@ -27,12 +27,11 @@
- -
+
@@ -71,10 +70,22 @@
+ +