diff --git a/neode-ui/src/components/AppLauncherOverlay.vue b/neode-ui/src/components/AppLauncherOverlay.vue index 8161de90..3d6d4f22 100644 --- a/neode-ui/src/components/AppLauncherOverlay.vue +++ b/neode-ui/src/components/AppLauncherOverlay.vue @@ -101,6 +101,13 @@ function injectScrollbarHideIfSameOrigin() { *::-webkit-scrollbar { display: none; } ` doc.head.appendChild(style) + // Escape from inside iframe → close overlay and return focus to launcher + doc.addEventListener('keydown', (e) => { + if ((e as KeyboardEvent).key === 'Escape') { + e.preventDefault() + window.parent.postMessage({ type: 'app-launcher-escape' }, '*') + } + }) } catch { /* Cross-origin: cannot access iframe document */ } @@ -120,6 +127,12 @@ function onKeyDown(e: KeyboardEvent) { } } +function onMessage(e: MessageEvent) { + if (e.data?.type === 'app-launcher-escape' && store.isOpen) { + store.close() + } +} + watch( () => store.isOpen, (open) => { @@ -133,10 +146,12 @@ watch( onMounted(() => { window.addEventListener('keydown', onKeyDown, true) + window.addEventListener('message', onMessage) }) onBeforeUnmount(() => { window.removeEventListener('keydown', onKeyDown, true) + window.removeEventListener('message', onMessage) }) diff --git a/neode-ui/src/composables/useControllerNav.ts b/neode-ui/src/composables/useControllerNav.ts index c8ea2743..556789d6 100644 --- a/neode-ui/src/composables/useControllerNav.ts +++ b/neode-ui/src/composables/useControllerNav.ts @@ -266,10 +266,25 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) { const mainEls = getElementsInZone('main') const hasZones = sidebarEls.length > 0 && mainEls.length > 0 - // Right: from sidebar → main (on App Store/My Apps, go straight to first app container) + // Right: from sidebar → main + // - On Apps/Marketplace: go to first app container + // - On Cloud: go to first folder (Pictures) + // - On Network (server): go to Services container + // - On Web5: go to Networking Profits container + // - On Settings: go to Change Password container + // - Otherwise: go to top right (App Switcher) const mainZone = document.querySelector('[data-controller-zone="main"]') + const isAppsOrMarketplace = /^\/dashboard\/(apps|marketplace)(\/|$)/.test(route.path) + const isCloud = /^\/dashboard\/cloud(\/|$)/.test(route.path) + const isNetwork = /^\/dashboard\/server(\/|$)/.test(route.path) + const isWeb5 = /^\/dashboard\/web5(\/|$)/.test(route.path) + const isSettings = /^\/dashboard\/settings(\/|$)/.test(route.path) const firstAppContainer = mainZone?.querySelector('[data-controller-container]') - const firstMain = firstAppContainer ?? mainEls[0] + const topRightEntry = mainZone?.querySelector('[data-controller-main-entry]') + const firstFocusableInTopRight = topRightEntry ? getFocusableElements(topRightEntry)[0] : null + const firstMain = ((isAppsOrMarketplace || isCloud || isNetwork || isWeb5 || isSettings) && firstAppContainer) + ? firstAppContainer + : (firstFocusableInTopRight ?? mainEls[0]) if (e.key === 'ArrowRight' && hasZones && isInZone(activeEl, 'sidebar') && firstMain) { playNavSound('move') firstMain.focus() @@ -323,12 +338,17 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) { } } - // Sidebar: linear up/down + // Sidebar: linear up/down with wrap (Home+Up→Logout, Logout+Down→Home) if (isInZone(activeEl, 'sidebar')) { const idx = sidebarEls.indexOf(activeEl) if (idx >= 0) { const isDown = e.key === 'ArrowDown' - const nextIdx = isDown ? Math.min(idx + 1, sidebarEls.length - 1) : Math.max(idx - 1, 0) + let nextIdx: number + if (isDown) { + nextIdx = idx >= sidebarEls.length - 1 ? 0 : idx + 1 + } else { + nextIdx = idx <= 0 ? sidebarEls.length - 1 : idx - 1 + } const next = sidebarEls[nextIdx] if (next && next !== activeEl) { playNavSound('move') diff --git a/neode-ui/src/stores/appLauncher.ts b/neode-ui/src/stores/appLauncher.ts index a7ee6e63..ba57d1c3 100644 --- a/neode-ui/src/stores/appLauncher.ts +++ b/neode-ui/src/stores/appLauncher.ts @@ -5,17 +5,26 @@ export const useAppLauncherStore = defineStore('appLauncher', () => { const isOpen = ref(false) const url = ref('') const title = ref('') + let previousActiveElement: HTMLElement | null = null function open(payload: { url: string; title: string }) { + previousActiveElement = (document.activeElement as HTMLElement) || null url.value = payload.url title.value = payload.title isOpen.value = true } function close() { + const toRestore = previousActiveElement + previousActiveElement = null isOpen.value = false url.value = '' title.value = '' + if (toRestore && typeof toRestore.focus === 'function') { + requestAnimationFrame(() => { + toRestore.focus() + }) + } } return { diff --git a/neode-ui/src/views/Dashboard.vue b/neode-ui/src/views/Dashboard.vue index a9b2fff7..646686e0 100644 --- a/neode-ui/src/views/Dashboard.vue +++ b/neode-ui/src/views/Dashboard.vue @@ -130,8 +130,8 @@ class="flex-1 overflow-hidden relative pb-20 md:pb-0 glass-piece z-10" :class="{ 'glass-throw-main': showZoomIn }" > - -
+ +
diff --git a/neode-ui/src/views/Settings.vue b/neode-ui/src/views/Settings.vue index ea164b09..9f064757 100644 --- a/neode-ui/src/views/Settings.vue +++ b/neode-ui/src/views/Settings.vue @@ -91,7 +91,7 @@
-
+