feat: onboarding polish, splash screen, controller nav, dev script
Onboarding flow: - Intro: improved layout and transitions - DID: better card styling and responsiveness - Path: added visual enhancements - Backup/Identity/Verify: streamlined markup - SplashScreen component added UI: - Controller navigation improvements (useControllerNav) - Style.css refinements Backend: - Runtime package fix Dev: - dev-start.sh improvements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ use anyhow::{Context, Result};
|
||||
/// Per-container graceful shutdown timeout in seconds.
|
||||
/// Bitcoin Core needs 600s to flush UTXO set, LND 330s for channel state,
|
||||
/// indexers 300s for index flush, databases 120s for WAL/transaction commit.
|
||||
fn stop_timeout_secs(container_name: &str) -> &'static str {
|
||||
pub fn stop_timeout_secs(container_name: &str) -> &'static str {
|
||||
let id = container_name.strip_prefix("archy-").unwrap_or(container_name);
|
||||
match id {
|
||||
"bitcoin-knots" | "bitcoin-core" | "bitcoin" => "600",
|
||||
|
||||
@@ -82,7 +82,7 @@ define(['./workbox-21a80088'], (function (workbox) { 'use strict';
|
||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.ld9oh2eb91o"
|
||||
"revision": "0.huo00jkc7v4"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
|
||||
@@ -277,6 +277,13 @@ if (!storedSeenIntro && isOnDashboard) {
|
||||
localStorage.setItem('neode_intro_seen', '1')
|
||||
}
|
||||
|
||||
function handleEnterKey(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' && showTapToStart.value && !tapStartTransitioning.value) {
|
||||
e.preventDefault()
|
||||
handleTapToStart()
|
||||
}
|
||||
}
|
||||
|
||||
function onIntroLogoHover() {
|
||||
introLogoHover.value = true
|
||||
if (!tapStartTransitioning.value) playKeyboardTypingSound()
|
||||
@@ -465,10 +472,13 @@ onMounted(() => {
|
||||
showSplash.value = false
|
||||
document.body.classList.add('splash-complete')
|
||||
emit('complete')
|
||||
} else {
|
||||
window.addEventListener('keydown', handleEnterKey)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', handleEnterKey)
|
||||
if (introTypingTimeout) {
|
||||
clearTimeout(introTypingTimeout)
|
||||
introTypingTimeout = null
|
||||
|
||||
@@ -115,7 +115,14 @@ function findNearestInDirection(
|
||||
|
||||
scored.sort((a, b) => {
|
||||
if (b.overlap !== a.overlap) return b.overlap - a.overlap
|
||||
return a.dist - b.dist
|
||||
if (a.dist !== b.dist) return a.dist - b.dist
|
||||
// Tiebreaker for up/down: prefer leftmost element in grid layouts
|
||||
if (direction === 'up' || direction === 'down') {
|
||||
const aLeft = a.el.getBoundingClientRect().left
|
||||
const bLeft = b.el.getBoundingClientRect().left
|
||||
return aLeft - bLeft
|
||||
}
|
||||
return 0
|
||||
})
|
||||
return scored[0]?.el ?? null
|
||||
}
|
||||
@@ -149,7 +156,7 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
|
||||
|
||||
const target = e.target as HTMLElement
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
|
||||
// Enter in text field: blur and move to next focusable element (e.g., submit button)
|
||||
// Enter in text field: find next focusable — if it's a button, click it directly (submit)
|
||||
if (e.key === 'Enter' && target.tagName === 'INPUT' && (target as HTMLInputElement).type !== 'submit') {
|
||||
e.preventDefault()
|
||||
const root = containerRef?.value ?? document
|
||||
@@ -157,12 +164,24 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
|
||||
const idx = all.indexOf(target as HTMLElement)
|
||||
const next = idx >= 0 ? all[idx + 1] : undefined
|
||||
if (next) {
|
||||
next.focus()
|
||||
next.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
||||
if (next.tagName === 'BUTTON' || next.getAttribute('role') === 'button') {
|
||||
next.focus()
|
||||
next.click()
|
||||
} else {
|
||||
next.focus()
|
||||
next.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if (e.key !== 'Escape') return
|
||||
// Up/Down arrows: exit field and navigate to element above/below
|
||||
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
;(target as HTMLElement).blur()
|
||||
// Fall through to arrow key handling below
|
||||
} else if (e.key !== 'Escape') {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const root = containerRef?.value ?? document
|
||||
|
||||
@@ -46,11 +46,12 @@
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
/* Controller / keyboard navigation - soft glow only (no box outline) */
|
||||
/* Controller / keyboard navigation - orange border (Archipelago brand) */
|
||||
*:focus-visible {
|
||||
outline: none;
|
||||
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;
|
||||
border-color: rgba(251, 146, 60, 0.8) !important;
|
||||
box-shadow: 0 0 0 2px rgba(251, 146, 60, 0.7), 0 0 16px rgba(251, 146, 60, 0.25);
|
||||
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Mobile touch targets — ensure tappable elements meet 44px minimum */
|
||||
@@ -98,12 +99,12 @@ input[type="radio"]:active + * {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
/* Containers get subtle grow + inner glow when focused (gamepad selection) */
|
||||
/* Containers get subtle grow + orange glow when focused (gamepad selection) */
|
||||
[data-controller-container]:focus-visible {
|
||||
transform: scale(1.02);
|
||||
box-shadow:
|
||||
0 0 24px rgba(120, 180, 255, 0.15),
|
||||
0 0 48px rgba(100, 160, 255, 0.08),
|
||||
0 0 0 2px rgba(251, 146, 60, 0.7),
|
||||
0 0 24px rgba(251, 146, 60, 0.2),
|
||||
inset 0 0 24px rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
@@ -978,8 +979,8 @@ input[type="radio"]:active + * {
|
||||
.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),
|
||||
0 0 0 2px rgba(251, 146, 60, 0.7),
|
||||
0 0 24px rgba(251, 146, 60, 0.2),
|
||||
inset 0 0 24px rgba(255, 255, 255, 0.03) !important;
|
||||
}
|
||||
}
|
||||
@@ -1302,7 +1303,7 @@ html:has(body.video-background-active)::before {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.cloud-file-item:focus-visible {
|
||||
box-shadow: 0 0 16px rgba(120, 180, 255, 0.2), 0 0 32px rgba(100, 160, 255, 0.1);
|
||||
box-shadow: 0 0 0 2px rgba(251, 146, 60, 0.7), 0 0 16px rgba(251, 146, 60, 0.25);
|
||||
}
|
||||
|
||||
.cloud-file-item-thumb {
|
||||
@@ -1480,7 +1481,7 @@ html:has(body.video-background-active)::before {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.cloud-grid-card:focus-visible {
|
||||
box-shadow: 0 0 16px rgba(120, 180, 255, 0.2), 0 0 32px rgba(100, 160, 255, 0.1);
|
||||
box-shadow: 0 0 0 2px rgba(251, 146, 60, 0.7), 0 0 16px rgba(251, 146, 60, 0.25);
|
||||
}
|
||||
|
||||
.cloud-grid-card-cover {
|
||||
|
||||
@@ -74,13 +74,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex gap-3 sm:gap-4 max-w-[600px] mx-auto flex-shrink-0 px-3 sm:px-4 pb-4 sm:pb-6">
|
||||
<button
|
||||
@click="skipForNow"
|
||||
class="path-action-button path-action-button--skip"
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
<div class="flex justify-center max-w-[600px] mx-auto flex-shrink-0 px-3 sm:px-4 pb-4 sm:pb-6">
|
||||
<button
|
||||
@click="proceed"
|
||||
:disabled="!downloaded"
|
||||
@@ -149,8 +143,5 @@ function proceed() {
|
||||
router.push('/onboarding/verify').catch(() => {})
|
||||
}
|
||||
|
||||
function skipForNow() {
|
||||
router.push('/onboarding/verify').catch(() => {})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -98,15 +98,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex gap-4 max-w-[600px] mx-auto flex-shrink-0">
|
||||
<button
|
||||
@click="skipForNow"
|
||||
class="path-action-button path-action-button--skip"
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
<div class="flex justify-center max-w-[600px] mx-auto flex-shrink-0">
|
||||
<button
|
||||
v-if="generatedDid"
|
||||
ref="continueButton"
|
||||
@click="proceed"
|
||||
class="path-action-button path-action-button--continue"
|
||||
>
|
||||
@@ -118,11 +113,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
const router = useRouter()
|
||||
const continueButton = ref<HTMLButtonElement | null>(null)
|
||||
const generatedDid = ref<string>('')
|
||||
const nostrNpub = ref<string>('')
|
||||
const isGenerating = ref(false)
|
||||
@@ -185,6 +181,16 @@ async function fetchDid() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(generatedDid, (did) => {
|
||||
if (did) {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
continueButton.value?.focus({ preventScroll: true })
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const cached = localStorage.getItem('neode_did')
|
||||
const cachedNpub = localStorage.getItem('neode_nostr_npub')
|
||||
@@ -205,11 +211,6 @@ function proceed() {
|
||||
router.push('/onboarding/identity').catch(() => {})
|
||||
}
|
||||
|
||||
function skipForNow() {
|
||||
stopTimers()
|
||||
router.push('/onboarding/identity').catch(() => {})
|
||||
}
|
||||
|
||||
function copyDid() {
|
||||
if (!generatedDid.value) return
|
||||
navigator.clipboard.writeText(generatedDid.value).catch(() => {})
|
||||
|
||||
@@ -60,13 +60,7 @@
|
||||
<p v-else-if="errorMessage" class="text-red-400 text-sm text-center mb-4">{{ errorMessage }}</p>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex gap-3 sm:gap-4 max-w-[600px] mx-auto flex-shrink-0 px-3 sm:px-4 pb-4 sm:pb-6">
|
||||
<button
|
||||
@click="skip"
|
||||
class="path-action-button path-action-button--skip"
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
<div class="flex justify-center max-w-[600px] mx-auto flex-shrink-0 px-3 sm:px-4 pb-4 sm:pb-6">
|
||||
<button
|
||||
@click="createIdentity"
|
||||
:disabled="isCreating"
|
||||
@@ -127,7 +121,4 @@ async function createIdentity() {
|
||||
}
|
||||
}
|
||||
|
||||
function skip() {
|
||||
router.push('/onboarding/backup').catch(() => {})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
</p>
|
||||
|
||||
<button
|
||||
ref="ctaButton"
|
||||
@click="goToOptions"
|
||||
class="glass-button px-6 py-3 sm:px-8 sm:py-4 rounded-lg text-base sm:text-lg font-medium transition-all hover:bg-black/70 hover:border-white/30 onb-cta"
|
||||
>
|
||||
@@ -65,12 +66,20 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import AnimatedLogo from '@/components/AnimatedLogo.vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
const router = useRouter()
|
||||
const ctaButton = ref<HTMLButtonElement | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
// Auto-focus after entry animation completes (1.4s animation delay + 0.6s duration)
|
||||
setTimeout(() => {
|
||||
ctaButton.value?.focus({ preventScroll: true })
|
||||
}, 2100)
|
||||
})
|
||||
|
||||
function goToOptions() {
|
||||
router.push('/onboarding/path').catch(() => {})
|
||||
|
||||
@@ -82,6 +82,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="continueButton"
|
||||
@click="proceed"
|
||||
class="path-action-button path-action-button--continue"
|
||||
>
|
||||
@@ -93,9 +94,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const continueButton = ref<HTMLButtonElement | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
continueButton.value?.focus({ preventScroll: true })
|
||||
}, 400)
|
||||
})
|
||||
|
||||
function proceed() {
|
||||
router.push('/onboarding/did').catch(() => {})
|
||||
|
||||
@@ -63,13 +63,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex gap-3 sm:gap-4 max-w-[600px] mx-auto flex-shrink-0 px-3 sm:px-4 pb-4 sm:pb-6">
|
||||
<button
|
||||
@click="skipForNow"
|
||||
class="path-action-button path-action-button--skip"
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
<div class="flex justify-center max-w-[600px] mx-auto flex-shrink-0 px-3 sm:px-4 pb-4 sm:pb-6">
|
||||
<button
|
||||
v-if="verified"
|
||||
@click="proceed"
|
||||
@@ -152,13 +146,5 @@ async function proceed() {
|
||||
router.push('/onboarding/done').catch(() => {})
|
||||
}
|
||||
|
||||
async function skipForNow() {
|
||||
try {
|
||||
await completeOnboarding()
|
||||
} catch {
|
||||
/* localStorage fallback ensures we can proceed */
|
||||
}
|
||||
router.push('/onboarding/done').catch(() => {})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -48,8 +48,9 @@ echo " 5) Existing user (login screen — mock)"
|
||||
echo " 6) Boot mode (simulated 25s startup — mock)"
|
||||
echo " 7) Testnet stack (signet Bitcoin + LND + ThunderHub via Podman)"
|
||||
echo " 8) Manual instructions"
|
||||
echo " 9) Container orchestration dev (live testing on .228)"
|
||||
echo ""
|
||||
read -p "Enter choice [0-8]: " choice
|
||||
read -p "Enter choice [0-9]: " choice
|
||||
|
||||
case $choice in
|
||||
0)
|
||||
@@ -278,6 +279,13 @@ case $choice in
|
||||
echo ""
|
||||
echo "Access: http://localhost:8100 (password: password123)"
|
||||
;;
|
||||
9)
|
||||
echo ""
|
||||
echo "Container Orchestration Dev (live testing on .228)"
|
||||
echo "Syncs code, builds on server, runs orchestration smoke tests."
|
||||
echo ""
|
||||
exec "$SCRIPT_DIR/dev-container-test.sh"
|
||||
;;
|
||||
*)
|
||||
echo "Invalid choice"
|
||||
exit 1
|
||||
|
||||
Reference in New Issue
Block a user