Update UI styles and enhance controller navigation functionality

- Replaced cyan color with yellow in various UI components for a cohesive visual theme.
- Improved focus styles for controller navigation, adding subtle grow and glow effects to sidebar items and containers.
- Enhanced controller navigation logic to support direct focus on app containers in the Marketplace and My Apps sections.
- Introduced wheel scrolling support for better navigation experience within scrollable areas.
- Removed unused audio tone function from useLoginSounds.ts to streamline code.
This commit is contained in:
Dorian
2026-02-17 20:40:26 +00:00
parent c72b97e940
commit 5a04875dcc
12 changed files with 109 additions and 60 deletions
+52 -7
View File
@@ -201,6 +201,17 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
const el = focusable[currentIndex] as HTMLElement
if (el.hasAttribute('data-controller-container')) {
// Marketplace: Enter = install (click install button)
if (el.hasAttribute('data-controller-install')) {
const installBtn = el.querySelector<HTMLButtonElement>('[data-controller-install-btn]:not([disabled])')
if (installBtn) {
playNavSound('action')
installBtn.click()
e.preventDefault()
return
}
}
// My Apps, etc: Enter = focus first inner control
const inner = getInnerFocusables(el)
const firstInner = inner[0]
if (firstInner) {
@@ -231,8 +242,10 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
const mainEls = getElementsInZone('main')
const hasZones = sidebarEls.length > 0 && mainEls.length > 0
// Right: from sidebar → main
const firstMain = mainEls[0]
// Right: from sidebar → main (on App Store/My Apps, go straight to first app container)
const mainZone = document.querySelector('[data-controller-zone="main"]')
const firstAppContainer = mainZone?.querySelector<HTMLElement>('[data-controller-container]')
const firstMain = firstAppContainer ?? mainEls[0]
if (e.key === 'ArrowRight' && hasZones && isInZone(activeEl, 'sidebar') && firstMain) {
playNavSound('move')
firstMain.focus()
@@ -254,13 +267,15 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
return
}
// No element in that direction: Left from leftmost → sidebar
// No element in that direction: Left from leftmost → sidebar (focus active tab, not logout)
if (e.key === 'ArrowLeft' && dir === 'left') {
const lastSidebar = sidebarEls[sidebarEls.length - 1]
if (lastSidebar) {
const sidebarZone = document.querySelector('[data-controller-zone="sidebar"]')
const activeNavTab = sidebarZone?.querySelector<HTMLElement>('.nav-tab-active')
const target = activeNavTab ?? sidebarEls[0]
if (target) {
playNavSound('move')
lastSidebar.focus()
lastSidebar.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
target.focus()
target.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
e.preventDefault()
return
}
@@ -350,9 +365,38 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
isControllerActive.value = gamepadCount.value > 0
}
/** Find nearest scrollable ancestor (overflow-y auto/scroll) */
function getScrollableAncestor(el: HTMLElement | null): HTMLElement | null {
let p = el?.parentElement
while (p) {
const style = getComputedStyle(p)
const oy = style.overflowY
if ((oy === 'auto' || oy === 'scroll') && p.scrollHeight > p.clientHeight) return p
p = p.parentElement
}
return null
}
/** Ensure wheel scrolls the scrollable area containing the focused element */
function handleWheel(e: WheelEvent) {
const active = document.activeElement as HTMLElement | null
if (!active) return
const scrollable = getScrollableAncestor(active)
if (!scrollable) return
if (e.deltaY !== 0) {
scrollable.scrollTop += e.deltaY
e.preventDefault()
}
if (e.deltaX !== 0 && scrollable.scrollWidth > scrollable.clientWidth) {
scrollable.scrollLeft += e.deltaX
e.preventDefault()
}
}
onMounted(() => {
checkGamepads()
window.addEventListener('keydown', handleKeyDown, true)
window.addEventListener('wheel', handleWheel, { passive: false })
window.addEventListener('gamepadconnected', handleGamepadConnected)
window.addEventListener('gamepaddisconnected', handleGamepadDisconnected)
pollIntervalId = setInterval(handleGamepadInput, 500)
@@ -360,6 +404,7 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
onBeforeUnmount(() => {
window.removeEventListener('keydown', handleKeyDown, true)
window.removeEventListener('wheel', handleWheel)
window.removeEventListener('gamepadconnected', handleGamepadConnected)
window.removeEventListener('gamepaddisconnected', handleGamepadDisconnected)
if (pollIntervalId) clearInterval(pollIntervalId)
@@ -16,28 +16,6 @@ function getContext(): AudioContext | null {
}
}
function playTone(
ctx: AudioContext,
freq: number,
duration: number,
gain: number,
type: OscillatorType = 'sine',
startOffset = 0,
dest: AudioNode = ctx.destination
) {
const osc = ctx.createOscillator()
const g = ctx.createGain()
osc.connect(g)
g.connect(dest)
g.gain.setValueAtTime(0, ctx.currentTime)
g.gain.linearRampToValueAtTime(gain, ctx.currentTime + 0.01)
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration)
osc.frequency.value = freq
osc.type = type
osc.start(ctx.currentTime + startOffset)
osc.stop(ctx.currentTime + startOffset + duration)
}
const INTRO_AUDIO_URL = '/assets/audio/cosmic-updrift.mp3'
const LOOP_START_URL = '/assets/audio/loop-start.mp3'