feat: gamepad navigation rewrite, focus styling, container grid system
- Rewrite useControllerNav.ts with clean console-style navigation: Sidebar (up/down wrap, right→containers, left→nothing), Container tile grid (spatial nav, no wrap at edges), Nav bar support (up from containers, down to grid), Inner controls (enter drills in, escape exits, trapped arrows) - Add data-controller-container to Mesh, Fleet, Settings pages - Fix Home.vue fragment (modals outside root div) causing Vue warnings - Remove skip-to-content link (handled by controller nav) - Orange ambient glow focus styling matching glass aesthetic - Disable PWA service worker in dev mode (fixes HMR caching) - Add gamepad-nav skill and GAMEPAD-NAV-MAP.md spec document - 39 tests covering all navigation patterns Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1444bcb0c4
commit
aada19754d
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex relative dashboard-view" :class="{ 'glass-throw-active': showZoomIn }">
|
||||
<!-- Skip to main content link for keyboard users -->
|
||||
<a href="#main-content" class="skip-to-content">{{ t('common.skipToContent') }}</a>
|
||||
<!-- Skip-to-content handled by controller nav sidebar→main transition -->
|
||||
<!-- Background container with 3D perspective - full width to avoid letterboxing -->
|
||||
<div class="bg-perspective-container">
|
||||
<!-- Background - primary layer (visible for all routes, transitions out only for detail pages) -->
|
||||
@@ -126,7 +126,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { RouterView, useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { useAppLauncherStore } from '../stores/appLauncher'
|
||||
import AppSession from '@/views/AppSession.vue'
|
||||
@@ -140,8 +139,6 @@ import HealthNotifications from '@/views/dashboard/HealthNotifications.vue'
|
||||
import { useRouteTransitions, isDetailRoute, ROUTE_BACKGROUNDS } from '@/views/dashboard/useRouteTransitions'
|
||||
import '@/views/dashboard/dashboard-styles.css'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const store = useAppStore()
|
||||
|
||||
@@ -216,12 +216,12 @@
|
||||
<div v-if="uiMode.isChat" class="flex flex-col items-center justify-center min-h-[40vh]">
|
||||
<RouterLink to="/dashboard/chat" class="glass-button rounded-lg px-8 py-4 text-lg font-medium">{{ t('home.openAI') }}</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wallet Modals -->
|
||||
<SendBitcoinModal :show="showSendModal" @close="showSendModal = false" @sent="loadWeb5Status()" />
|
||||
<ReceiveBitcoinModal :show="showReceiveModal" @close="showReceiveModal = false" @received="loadWeb5Status()" />
|
||||
<TransactionsModal :show="showTransactionsModal" :transactions="walletTransactions" @close="showTransactionsModal = false" />
|
||||
<!-- Wallet Modals -->
|
||||
<SendBitcoinModal :show="showSendModal" @close="showSendModal = false" @sent="loadWeb5Status()" />
|
||||
<ReceiveBitcoinModal :show="showReceiveModal" @close="showReceiveModal = false" @received="loadWeb5Status()" />
|
||||
<TransactionsModal :show="showTransactionsModal" :transactions="walletTransactions" @close="showTransactionsModal = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -346,7 +346,7 @@ function truncatePubkey(hex: string | null): string {
|
||||
<!-- LEFT COLUMN: Status + Peers -->
|
||||
<div class="mesh-left" :class="{ 'mobile-hidden': mobileShowChat }">
|
||||
<!-- Device Status -->
|
||||
<div class="glass-card mesh-status-card">
|
||||
<div data-controller-container tabindex="0" class="glass-card mesh-status-card">
|
||||
<div class="mesh-status-header">
|
||||
<div class="mesh-status-indicator" :class="mesh.status?.device_connected ? 'connected' : 'disconnected'" />
|
||||
<h2 class="mesh-section-title">Device</h2>
|
||||
@@ -429,7 +429,7 @@ function truncatePubkey(hex: string | null): string {
|
||||
</div>
|
||||
|
||||
<!-- Peers list -->
|
||||
<div class="glass-card mesh-peers-card">
|
||||
<div data-controller-container tabindex="0" class="glass-card mesh-peers-card">
|
||||
<h2 class="mesh-section-title">Peers <span class="mesh-peer-count">{{ mesh.peers.length }}</span></h2>
|
||||
|
||||
<div v-if="mesh.peers.length === 0 && !mesh.status?.device_connected" class="mesh-empty">
|
||||
@@ -512,7 +512,7 @@ function truncatePubkey(hex: string | null): string {
|
||||
</div>
|
||||
|
||||
<!-- Chat Panel -->
|
||||
<div v-if="showChatPanel" class="glass-card mesh-chat-card">
|
||||
<div v-if="showChatPanel" data-controller-container tabindex="0" class="glass-card mesh-chat-card">
|
||||
<div v-if="!hasActiveChat" class="mesh-chat-empty">
|
||||
<div class="mesh-chat-empty-icon">📡</div>
|
||||
<p>Select a peer or channel to chat</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
|
||||
<div class="monitoring-stat-card">
|
||||
<div data-controller-container tabindex="0" class="monitoring-stat-card">
|
||||
<p class="text-xs text-white/50 uppercase tracking-wide">Total Nodes</p>
|
||||
<p class="text-2xl font-bold text-white">{{ nodeCount }}</p>
|
||||
<p class="text-xs text-white/40">
|
||||
@@ -8,22 +8,22 @@
|
||||
<span class="ml-1 fleet-dot-offline"></span> {{ offlineCount }} offline
|
||||
</p>
|
||||
</div>
|
||||
<div class="monitoring-stat-card">
|
||||
<div data-controller-container tabindex="0" class="monitoring-stat-card">
|
||||
<p class="text-xs text-white/50 uppercase tracking-wide">Fleet Health</p>
|
||||
<p class="text-2xl font-bold text-white">{{ fleetHealthPct }}%</p>
|
||||
<p class="text-xs text-white/40">{{ healthyCount }}/{{ nodeCount }} no alerts</p>
|
||||
</div>
|
||||
<div class="monitoring-stat-card">
|
||||
<div data-controller-container tabindex="0" class="monitoring-stat-card">
|
||||
<p class="text-xs text-white/50 uppercase tracking-wide">Avg CPU</p>
|
||||
<p class="text-2xl font-bold text-white" :class="healthTextClass(avgCpu)">{{ avgCpu.toFixed(1) }}%</p>
|
||||
<p class="text-xs text-white/40">across fleet</p>
|
||||
</div>
|
||||
<div class="monitoring-stat-card">
|
||||
<div data-controller-container tabindex="0" class="monitoring-stat-card">
|
||||
<p class="text-xs text-white/50 uppercase tracking-wide">Avg RAM</p>
|
||||
<p class="text-2xl font-bold text-white" :class="healthTextClass(avgMem)">{{ avgMem.toFixed(1) }}%</p>
|
||||
<p class="text-xs text-white/40">across fleet</p>
|
||||
</div>
|
||||
<div class="monitoring-stat-card">
|
||||
<div data-controller-container tabindex="0" class="monitoring-stat-card">
|
||||
<p class="text-xs text-white/50 uppercase tracking-wide">Avg Disk</p>
|
||||
<p class="text-2xl font-bold text-white" :class="healthTextClass(avgDisk)">{{ avgDisk.toFixed(1) }}%</p>
|
||||
<p class="text-xs text-white/40">across fleet</p>
|
||||
|
||||
@@ -114,7 +114,7 @@ init()
|
||||
</div>
|
||||
|
||||
<!-- Info Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div data-controller-container tabindex="0" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<!-- Server Name Card (editable) -->
|
||||
<div class="bg-black/20 rounded-xl px-5 py-4 border border-white/10">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
|
||||
@@ -48,7 +48,7 @@ async function performFactoryReset() {
|
||||
|
||||
<template>
|
||||
<!-- Network Diagnostics Link -->
|
||||
<div class="glass-card px-6 py-6 mb-6">
|
||||
<div data-controller-container tabindex="0" class="glass-card px-6 py-6 mb-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-white/96">{{ t('common.network') }}</h2>
|
||||
@@ -64,7 +64,7 @@ async function performFactoryReset() {
|
||||
</div>
|
||||
|
||||
<!-- Reboot Section -->
|
||||
<div class="path-option-card px-6 py-6 mt-6">
|
||||
<div data-controller-container tabindex="0" class="path-option-card px-6 py-6 mt-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-white/90 mb-1">Reboot</h2>
|
||||
@@ -109,7 +109,7 @@ async function performFactoryReset() {
|
||||
</Teleport>
|
||||
|
||||
<!-- Factory Reset Section -->
|
||||
<div class="path-option-card px-6 py-6 mt-6 border-red-500/30">
|
||||
<div data-controller-container tabindex="0" class="path-option-card px-6 py-6 mt-6 border-red-500/30">
|
||||
<h2 class="text-xl font-semibold text-red-400/90 mb-3">Factory Reset</h2>
|
||||
<p class="text-sm text-white/60 mb-4">
|
||||
Wipe all user data, identities, and credentials. Container images are preserved. The node will restart and show the onboarding screen.
|
||||
|
||||
@@ -128,7 +128,7 @@ loadTotpStatus()
|
||||
|
||||
<template>
|
||||
<!-- Two-Factor Authentication -->
|
||||
<div class="mb-6">
|
||||
<div data-controller-container tabindex="0" class="mb-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="w-5 h-5 text-white/70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
Reference in New Issue
Block a user