diff --git a/loop-start.mp3 b/loop-start.mp3 index 17ad6a34..2fdba326 100644 Binary files a/loop-start.mp3 and b/loop-start.mp3 differ diff --git a/neode-ui/src/components/SplashScreen.vue b/neode-ui/src/components/SplashScreen.vue index c966d6b7..79eac0b0 100644 --- a/neode-ui/src/components/SplashScreen.vue +++ b/neode-ui/src/components/SplashScreen.vue @@ -51,19 +51,19 @@ >
- > + > {{ displayLine1 }}
- > + > {{ displayLine2 }}
- > + > {{ displayLine3 }}
- > + > {{ displayLine4 }}
@@ -521,13 +521,13 @@ onBeforeUnmount(() => { min-width: 0; } -/* Intro typing cursor - block style, cyan blink (matches original typing-text caret) */ +/* Intro typing cursor - block style, yellow blink (Archipelago style) */ .intro-typing-caret { display: inline-block; width: 4px; min-width: 4px; height: 1.2em; - background: #00ffff; + background: #fbbf24; margin-left: 2px; vertical-align: text-bottom; animation: intro-caret-blink 0.5s step-end infinite; diff --git a/neode-ui/src/composables/useControllerNav.ts b/neode-ui/src/composables/useControllerNav.ts index 2c022b79..9689cb0f 100644 --- a/neode-ui/src/composables/useControllerNav.ts +++ b/neode-ui/src/composables/useControllerNav.ts @@ -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('[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('[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('.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) diff --git a/neode-ui/src/composables/useLoginSounds.ts b/neode-ui/src/composables/useLoginSounds.ts index 3e35f772..f5c7d2b4 100644 --- a/neode-ui/src/composables/useLoginSounds.ts +++ b/neode-ui/src/composables/useLoginSounds.ts @@ -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' diff --git a/neode-ui/src/style.css b/neode-ui/src/style.css index 3481032e..5e7a5ecd 100644 --- a/neode-ui/src/style.css +++ b/neode-ui/src/style.css @@ -17,20 +17,22 @@ font-style: normal; } -/* Controller / keyboard navigation - soft glow, hover-style (not yellow button) */ +/* Controller / keyboard navigation - soft glow only (no box outline) */ *:focus-visible { outline: none; - box-shadow: - 0 0 0 1px rgba(255, 255, 255, 0.25), - 0 0 16px rgba(120, 180, 255, 0.2), - 0 0 32px rgba(100, 160, 255, 0.1); + box-shadow: 0 0 16px rgba(120, 180, 255, 0.2), 0 0 32px rgba(100, 160, 255, 0.1); transition: box-shadow 0.2s ease; } -/* Containers get a subtle inner glow when focused */ +/* Containers: base scale for smooth grow animation */ +[data-controller-container] { + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +/* Containers get subtle grow + inner glow when focused (gamepad selection) */ [data-controller-container]:focus-visible { + transform: scale(1.02); box-shadow: - 0 0 0 1px rgba(255, 255, 255, 0.3), 0 0 24px rgba(120, 180, 255, 0.15), 0 0 48px rgba(100, 160, 255, 0.08), inset 0 0 24px rgba(255, 255, 255, 0.03); @@ -393,6 +395,18 @@ mask-composite: exclude; pointer-events: none; } + + /* Sidebar nav items: grow + glow on gamepad focus (same as containers) */ + .sidebar-nav-item { + transition: transform 0.2s ease, box-shadow 0.2s ease; + } + .sidebar-nav-item:focus-visible { + transform: scale(1.02) !important; + box-shadow: + 0 0 24px rgba(120, 180, 255, 0.15), + 0 0 48px rgba(100, 160, 255, 0.08), + inset 0 0 24px rgba(255, 255, 255, 0.03) !important; + } } /* Background image */ @@ -478,7 +492,7 @@ body { overflow: hidden; white-space: nowrap; max-width: 0; - border-right: 4px solid #00ffff; + border-right: 4px solid #fbbf24; animation: typing 2s steps(30, end) forwards, caretBlink 0.5s step-end 3 2.6s; @@ -496,7 +510,7 @@ body { @keyframes caretBlink { 0%, 100% { - border-right-color: #00ffff; + border-right-color: #fbbf24; } 50% { border-right-color: transparent; diff --git a/neode-ui/src/views/Apps.vue b/neode-ui/src/views/Apps.vue index 7e1ebdce..dde1edaf 100644 --- a/neode-ui/src/views/Apps.vue +++ b/neode-ui/src/views/Apps.vue @@ -27,6 +27,8 @@
diff --git a/neode-ui/src/views/Cloud.vue b/neode-ui/src/views/Cloud.vue index dd02717c..9502607f 100644 --- a/neode-ui/src/views/Cloud.vue +++ b/neode-ui/src/views/Cloud.vue @@ -23,6 +23,8 @@
diff --git a/neode-ui/src/views/ContainerApps.vue b/neode-ui/src/views/ContainerApps.vue index 11ea7413..b735e5fc 100644 --- a/neode-ui/src/views/ContainerApps.vue +++ b/neode-ui/src/views/ContainerApps.vue @@ -26,6 +26,8 @@
@@ -148,6 +150,8 @@
diff --git a/neode-ui/src/views/Home.vue b/neode-ui/src/views/Home.vue index f448d546..79a11449 100644 --- a/neode-ui/src/views/Home.vue +++ b/neode-ui/src/views/Home.vue @@ -355,7 +355,7 @@ const runningCount = computed(() => display: inline-block; width: 3px; height: 1.1em; - background: #00ffff; + background: #fbbf24; margin-left: 2px; vertical-align: text-bottom; animation: caret-blink 0.7s step-end infinite; diff --git a/neode-ui/src/views/Marketplace.vue b/neode-ui/src/views/Marketplace.vue index c04fc903..2dea92d3 100644 --- a/neode-ui/src/views/Marketplace.vue +++ b/neode-ui/src/views/Marketplace.vue @@ -134,6 +134,9 @@
@@ -171,6 +174,7 @@