feat: gamepad navigation for Mesh tab — zone-based panel nav

- Peer rows: tabindex + role=button + Enter handler for D-pad selection
- Zone attributes: mesh-left, mesh-chat, mesh-tools for cross-panel nav
- Actions row: data-controller-container for Up from peers
- Right from peers → chat input, Right from chat → tools tabs (wide)
- Down from tabs → panel fields/buttons in grid fashion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-30 10:24:48 +01:00
parent 9ea8877d20
commit 377195f7e0

View File

@@ -344,7 +344,7 @@ function truncatePubkey(hex: string | null): string {
<!-- Responsive column layout -->
<div class="mesh-columns" :class="{ 'mesh-columns-wide': isWideDesktop }">
<!-- LEFT COLUMN: Status + Peers -->
<div class="mesh-left" :class="{ 'mobile-hidden': mobileShowChat }">
<div class="mesh-left" data-controller-zone="mesh-left" :class="{ 'mobile-hidden': mobileShowChat }">
<!-- Device Status -->
<div data-controller-container tabindex="0" class="glass-card mesh-status-card">
<div class="mesh-status-header">
@@ -410,7 +410,7 @@ function truncatePubkey(hex: string | null): string {
</div>
<!-- Actions row -->
<div class="mesh-actions">
<div class="mesh-actions" data-controller-container tabindex="0">
<button class="glass-button mesh-action-btn" :disabled="configuring" @click="handleToggleEnabled">
{{ mesh.status?.enabled ? 'Disable' : 'Enable' }}
</button>
@@ -441,7 +441,10 @@ function truncatePubkey(hex: string | null): string {
<div
class="mesh-peer-row is-channel"
:class="{ active: archChannelActive }"
tabindex="0"
role="button"
@click="openArchChannel"
@keydown.enter="openArchChannel"
>
<div class="mesh-peer-avatar channel" style="background: rgba(251,146,60,0.2); color: #fb923c;">A</div>
<div class="mesh-peer-info">
@@ -454,7 +457,10 @@ function truncatePubkey(hex: string | null): string {
<div
class="mesh-peer-row is-channel"
:class="{ active: activeChatChannel?.index === 0 }"
tabindex="0"
role="button"
@click="openChannelChat(publicChannel)"
@keydown.enter="openChannelChat(publicChannel)"
>
<div class="mesh-peer-avatar channel">#</div>
<div class="mesh-peer-info">
@@ -466,7 +472,10 @@ function truncatePubkey(hex: string | null): string {
v-for="peer in sortedPeers" :key="peer.contact_id"
class="mesh-peer-row"
:class="{ active: activeChatPeer?.contact_id === peer.contact_id, 'is-archy': isArchyNode(peer) }"
tabindex="0"
role="button"
@click="openChat(peer)"
@keydown.enter="openChat(peer)"
>
<div class="mesh-peer-avatar" :class="{ archy: isArchyNode(peer) }">
<AnimatedLogo v-if="isArchyNode(peer)" size="sm" />
@@ -493,7 +502,7 @@ function truncatePubkey(hex: string | null): string {
</div>
<!-- RIGHT COLUMN: Tabbed panels -->
<div class="mesh-right" :class="{ 'mobile-hidden': !mobileShowChat }">
<div class="mesh-right" data-controller-zone="mesh-chat" :class="{ 'mobile-hidden': !mobileShowChat }">
<!-- Tab bar (medium desktop only) -->
<div v-if="showTabBar" class="mesh-tab-bar">
<button class="mesh-tab" :class="{ active: activeTab === 'chat' }" @click="activeTab = 'chat'">Chat</button>
@@ -614,8 +623,8 @@ function truncatePubkey(hex: string | null): string {
</template>
</div>
<!-- Tools panels -->
<div class="mesh-tools-wrapper">
<!-- Tools panels (3rd column on wide screens) -->
<div class="mesh-tools-wrapper" data-controller-zone="mesh-tools">
<!-- Tools tab bar (wide desktop only) -->
<div v-if="isWideDesktop" class="mesh-tools-tab-bar">
<button class="mesh-tab" :class="{ active: toolsTab === 'bitcoin' }" @click="toolsTab = 'bitcoin'">