fix: onboarding gamepad — autofocus, click sounds, focus styles

All screens:
- playNavSound('action') on every button click
- path-action-button orange focus glow (removed from suppression list)

Per-screen autofocus:
- Intro: CTA button (after animation)
- Path: Continue button
- Identity: name input
- Backup: passphrase input, Continue after download
- Verify: Sign Challenge, then Finish after verification
- Done: Set Password button

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-30 09:52:09 +01:00
parent 6fbdda772f
commit e104a214a4
7 changed files with 38 additions and 3 deletions

View File

@@ -93,6 +93,7 @@
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { rpcClient } from '@/api/rpc-client'
import { playNavSound } from '@/composables/useNavSounds'
const router = useRouter()
const passphraseInput = ref<HTMLInputElement | null>(null)
@@ -154,6 +155,7 @@ async function downloadBackup() {
}
function proceed() {
playNavSound('action')
router.push('/onboarding/verify').catch(() => {})
}

View File

@@ -45,6 +45,7 @@
<!-- Set Password Button -->
<p class="text-xs text-white/50 mb-3">You'll create your node password next</p>
<button
ref="setPasswordButton"
@click="goToLogin"
class="path-action-button path-action-button--continue mx-auto"
>
@@ -56,11 +57,21 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { playNavSound } from '@/composables/useNavSounds'
const router = useRouter()
const setPasswordButton = ref<HTMLButtonElement | null>(null)
onMounted(() => {
setTimeout(() => {
setPasswordButton.value?.focus({ preventScroll: true })
}, 500)
})
function goToLogin() {
playNavSound('action')
router.push('/login').catch(() => {})
}
</script>

View File

@@ -33,7 +33,7 @@
<button
v-for="p in purposes"
:key="p.value"
@click="selectedPurpose = p.value"
@click="playNavSound('action'); selectedPurpose = p.value"
class="px-4 py-3 rounded-lg border text-left transition-all"
:class="selectedPurpose === p.value
? 'bg-white/15 border-white/30 text-white'
@@ -79,6 +79,7 @@
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { rpcClient } from '@/api/rpc-client'
import { playNavSound } from '@/composables/useNavSounds'
const router = useRouter()
const nameInput = ref<HTMLInputElement | null>(null)
@@ -117,6 +118,7 @@ async function createIdentity() {
purpose: selectedPurpose.value
}
})
playNavSound('action')
router.push('/onboarding/backup').catch(() => {})
} catch (err) {
if (isServerStartingError(err)) {

View File

@@ -73,6 +73,7 @@ import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import AnimatedLogo from '@/components/AnimatedLogo.vue'
import { rpcClient } from '@/api/rpc-client'
import { playNavSound } from '@/composables/useNavSounds'
const router = useRouter()
const ctaButton = ref<HTMLButtonElement | null>(null)
@@ -85,6 +86,7 @@ onMounted(() => {
})
function goToOptions() {
playNavSound('action')
router.push('/onboarding/path').catch(() => {})
}

View File

@@ -82,6 +82,7 @@
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { completeOnboarding } from '@/composables/useOnboarding'
import { playNavSound } from '@/composables/useNavSounds'
const router = useRouter()
const selected = ref<string | null>(null)
@@ -100,6 +101,7 @@ async function proceed() {
} catch (e) {
if (import.meta.env.DEV) console.warn('completeOnboarding failed, localStorage fallback ensures onboarding is marked complete', e)
}
playNavSound('action')
router.push('/login').catch(() => {})
}
</script>

View File

@@ -96,18 +96,19 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { playNavSound } from '@/composables/useNavSounds'
const router = useRouter()
const continueButton = ref<HTMLButtonElement | null>(null)
onMounted(() => {
// Focus after slide transition completes (400ms + buffer)
setTimeout(() => {
continueButton.value?.focus({ preventScroll: true })
}, 500)
})
function proceed() {
playNavSound('action')
router.push('/onboarding/did').catch(() => {})
}
</script>

View File

@@ -18,6 +18,7 @@
<p v-else-if="errorMessage" class="text-red-400 text-sm">{{ errorMessage }}</p>
<!-- Sign Button (if not verified yet) -->
<button
ref="signButton"
v-if="!verified"
@click="signChallenge"
:disabled="isSigning"
@@ -65,6 +66,7 @@
<!-- Action Buttons -->
<div class="flex justify-center max-w-[600px] mx-auto flex-shrink-0 px-3 sm:px-4 pb-4 sm:pb-6">
<button
ref="finishButton"
v-if="verified"
@click="proceed"
class="path-action-button path-action-button--continue"
@@ -77,13 +79,22 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onMounted, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { completeOnboarding } from '@/composables/useOnboarding'
import { rpcClient } from '@/api/rpc-client'
import { playNavSound } from '@/composables/useNavSounds'
const router = useRouter()
const signButton = ref<HTMLButtonElement | null>(null)
const finishButton = ref<HTMLButtonElement | null>(null)
const verified = ref(false)
onMounted(() => {
setTimeout(() => {
signButton.value?.focus({ preventScroll: true })
}, 500)
})
const isSigning = ref(false)
const signature = ref('')
const currentChallenge = ref('')
@@ -119,6 +130,9 @@ async function signChallenge() {
} else {
verified.value = true
}
nextTick(() => {
setTimeout(() => finishButton.value?.focus({ preventScroll: true }), 100)
})
return
} catch (err) {
const msg = err instanceof Error ? err.message : ''
@@ -138,6 +152,7 @@ async function signChallenge() {
}
async function proceed() {
playNavSound('action')
try {
await completeOnboarding()
} catch {