archy/neode-ui/src/views/Home.vue
Dorian 7b044d22ef feat: implement three-mode UI system (Easy / Pro / Chat)
Add switchable UI modes with conditional rendering:
- Easy mode: goal-based interface with 7 guided workflows
- Pro mode: current technical interface preserved with Quick Start Goals
- Chat mode: AIUI placeholder for future integration

New components: ModeSwitcher, EasyHome, GoalDetail wizard, Chat placeholder
New stores: uiMode (mode persistence), goals (progress tracking)
New data: goal definitions catalog, helpTree goals section
Modified: Dashboard (reactive nav), Home (mode dispatcher), Settings (mode picker),
Router (goal/chat routes), Spotlight (goal search integration)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 07:09:31 +00:00

474 lines
18 KiB
Vue

<template>
<div>
<div class="mb-8">
<div class="min-h-[4.5rem]">
<h1 class="text-3xl font-bold text-white mb-2 drop-shadow-[0_2px_8px_rgba(0,0,0,0.6)]">
{{ line1Text }}<span v-if="showCaretLine1" class="typing-caret"></span>
</h1>
<p class="text-white/80">
{{ line2Text }}<span v-if="showCaretLine2" class="typing-caret"></span>
</p>
</div>
</div>
<!-- Easy Mode: Goal-based cards -->
<EasyHome
v-if="uiMode.isEasy"
:show="!showWelcomeBlock || animateCards"
:animate="animateCards"
/>
<!-- Pro Mode: Section overview cards -->
<div
v-else-if="uiMode.isGamer"
class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8 transition-opacity duration-300"
:class="{ 'opacity-0 pointer-events-none': showWelcomeBlock && !animateCards }"
>
<!-- My Apps Overview -->
<div
data-controller-container
tabindex="0"
class="home-card controller-focusable"
:class="{ 'home-card-animate': animateCards }"
style="--card-stagger: 0"
>
<div class="home-card-shell">
<div class="home-card-inner p-6 flex flex-col h-full min-h-0">
<div class="home-card-header flex items-start justify-between mb-4 shrink-0">
<div class="home-card-text">
<h2 class="text-xl font-semibold text-white mb-1">My Apps</h2>
<p class="text-sm text-white/70">Manage your installed applications</p>
</div>
<RouterLink to="/dashboard/apps" class="text-white/60 hover:text-white transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</RouterLink>
</div>
<div class="home-card-stats grid grid-cols-2 gap-4 mb-4 flex-1 min-h-0">
<div class="p-4 bg-white/5 rounded-lg">
<p class="text-xs text-white/60 mb-1">Installed</p>
<p class="text-2xl font-bold text-white">{{ appCount }}</p>
</div>
<div class="p-4 bg-white/5 rounded-lg">
<p class="text-xs text-white/60 mb-1">Running</p>
<p class="text-2xl font-bold text-white">{{ runningCount }}</p>
</div>
</div>
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
<RouterLink to="/dashboard/marketplace" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
Browse Store
</RouterLink>
<RouterLink to="/dashboard/apps" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
Manage Apps
</RouterLink>
</div>
</div>
</div>
</div>
<!-- Cloud Overview -->
<div
data-controller-container
tabindex="0"
class="home-card controller-focusable"
:class="{ 'home-card-animate': animateCards }"
style="--card-stagger: 1"
>
<div class="home-card-shell">
<div class="home-card-inner p-6 flex flex-col h-full min-h-0">
<div class="home-card-header flex items-start justify-between mb-4 shrink-0">
<div class="home-card-text">
<h2 class="text-xl font-semibold text-white mb-1">Cloud</h2>
<p class="text-sm text-white/70">Cloud services and storage</p>
</div>
<RouterLink to="/dashboard/cloud" class="text-white/60 hover:text-white transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</RouterLink>
</div>
<div class="home-card-stats grid grid-cols-2 gap-4 mb-4 flex-1 min-h-0">
<div class="p-4 bg-white/5 rounded-lg">
<p class="text-xs text-white/60 mb-1">Storage Used</p>
<p class="text-2xl font-bold text-white">2.4 GB</p>
</div>
<div class="p-4 bg-white/5 rounded-lg">
<p class="text-xs text-white/60 mb-1">Folders</p>
<p class="text-2xl font-bold text-white">5</p>
</div>
</div>
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
<RouterLink to="/dashboard/cloud" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
View Folders
</RouterLink>
<button class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors" @click="() => {}">
Upload Files
</button>
</div>
</div>
</div>
</div>
<!-- Network Overview -->
<div
data-controller-container
tabindex="0"
class="home-card controller-focusable"
:class="{ 'home-card-animate': animateCards }"
style="--card-stagger: 2"
>
<div class="home-card-shell">
<div class="home-card-inner p-6 flex flex-col h-full min-h-0">
<div class="home-card-header flex items-start justify-between mb-4 shrink-0">
<div class="home-card-text">
<h2 class="text-xl font-semibold text-white mb-1">Network</h2>
<p class="text-sm text-white/70">Network infrastructure and Web3 services</p>
</div>
<RouterLink to="/dashboard/server" class="text-white/60 hover:text-white transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</RouterLink>
</div>
<div class="home-card-stats space-y-3 mb-4 flex-1 min-h-0">
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<div class="w-2 h-2 rounded-full bg-green-400"></div>
<span class="text-sm text-white/80">Services Status</span>
</div>
<span class="text-sm text-green-400 font-medium">All Running</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<div class="w-2 h-2 rounded-full bg-green-400"></div>
<span class="text-sm text-white/80">Connectivity</span>
</div>
<span class="text-sm text-green-400 font-medium">Connected</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<div class="w-2 h-2 rounded-full bg-blue-400"></div>
<span class="text-sm text-white/80">Connected Nodes</span>
</div>
<span class="text-sm text-white/80 font-medium">12</span>
</div>
</div>
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
<RouterLink to="/dashboard/server" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
Manage Network
</RouterLink>
<button class="home-card-btn px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors" @click="() => {}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- Web5 Overview -->
<div
data-controller-container
tabindex="0"
class="home-card controller-focusable"
:class="{ 'home-card-animate': animateCards }"
style="--card-stagger: 3"
>
<div class="home-card-shell">
<div class="home-card-inner p-6 flex flex-col h-full min-h-0">
<div class="home-card-header flex items-start justify-between mb-4 shrink-0">
<div class="home-card-text">
<h2 class="text-xl font-semibold text-white mb-1">Web5</h2>
<p class="text-sm text-white/70">Decentralized identity and data protocols</p>
</div>
<RouterLink to="/dashboard/web5" class="text-white/60 hover:text-white transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</RouterLink>
</div>
<div class="home-card-stats space-y-3 mb-4 flex-1 min-h-0">
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<div class="w-2 h-2 rounded-full bg-green-400"></div>
<span class="text-sm text-white/80">DID Status</span>
</div>
<span class="text-sm text-green-400 font-medium">Active</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<div class="w-2 h-2 rounded-full bg-green-400"></div>
<span class="text-sm text-white/80">DWN Sync</span>
</div>
<span class="text-sm text-green-400 font-medium">Synced</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<span class="text-lg text-orange-500 font-bold"></span>
<span class="text-sm text-white/80">Networking Profits</span>
</div>
<span class="text-sm text-orange-500 font-medium">0.024</span>
</div>
</div>
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
<RouterLink to="/dashboard/web5" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
Manage Web5
</RouterLink>
<button class="home-card-btn px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors" @click="() => {}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Quick Start Goals - shown in Pro mode below the overview cards -->
<div v-if="uiMode.isGamer" class="path-option-card cursor-default px-6 py-6">
<h2 class="text-xl font-semibold text-white/96 mb-2">Quick Start Goals</h2>
<p class="text-sm text-white/60 mb-4">Not sure where to start? Try a guided setup.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<RouterLink
v-for="goal in topGoals"
:key="goal.id"
:to="`/dashboard/goals/${goal.id}`"
class="path-action-button path-action-button--continue flex items-center justify-center gap-3"
>
<span>{{ goal.title }}</span>
</RouterLink>
</div>
</div>
<!-- Chat Mode: redirect to Chat view -->
<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">
Open AI Assistant
</RouterLink>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch, onBeforeUnmount } from 'vue'
import { RouterLink } from 'vue-router'
import { useAppStore } from '../stores/app'
import { useLoginTransitionStore } from '../stores/loginTransition'
import { useUIModeStore } from '@/stores/uiMode'
import { PackageState } from '../types/api'
import { playTypingSound } from '@/composables/useLoginSounds'
import { GOALS } from '@/data/goals'
import EasyHome from '@/components/EasyHome.vue'
const uiMode = useUIModeStore()
const topGoals = GOALS.slice(0, 3)
const store = useAppStore()
const loginTransition = useLoginTransitionStore()
const LINE1 = "Welcome Noderunner"
const LINE2 = "Here's an overview of your sovereign life"
const MS_PER_CHAR = 55
const displayLine1 = ref('')
const displayLine2 = ref('')
const showCaretLine1 = ref(false)
const showCaretLine2 = ref(false)
const showWelcomeBlock = ref(false)
const hasTypedWelcome = ref(false)
const animateCards = ref(false)
let typingInterval: ReturnType<typeof setInterval> | null = null
const line1Text = computed(() =>
showWelcomeBlock.value ? displayLine1.value : LINE1
)
const line2Text = computed(() =>
showWelcomeBlock.value ? displayLine2.value : LINE2
)
onBeforeUnmount(() => {
if (typingInterval) clearInterval(typingInterval)
})
watch(() => loginTransition.pendingWelcomeTyping, (pending) => {
if (pending) showWelcomeBlock.value = true
})
watch(() => loginTransition.startWelcomeTyping, (shouldStart) => {
if (!shouldStart || hasTypedWelcome.value) return
hasTypedWelcome.value = true
showWelcomeBlock.value = true
displayLine1.value = ''
displayLine2.value = ''
showCaretLine1.value = true
showCaretLine2.value = false
playTypingSound()
animateCards.value = true
let i = 0
typingInterval = setInterval(() => {
if (i < LINE1.length) {
displayLine1.value = LINE1.slice(0, i + 1)
i++
} else if (i < LINE1.length + LINE2.length) {
showCaretLine1.value = false
showCaretLine2.value = true
displayLine2.value = LINE2.slice(0, i - LINE1.length + 1)
i++
} else {
if (typingInterval) clearInterval(typingInterval)
typingInterval = null
showCaretLine2.value = false
loginTransition.setStartWelcomeTyping(false)
}
}, MS_PER_CHAR)
}, { immediate: true })
// @ts-ignore - Computed kept for future use
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const version = computed(() => store.serverInfo?.version || '0.0.0')
const packages = computed(() => store.packages)
const appCount = computed(() => Object.keys(packages.value).length)
const runningCount = computed(() =>
Object.values(packages.value).filter(pkg => pkg.state === PackageState.Running).length
)
</script>
<style scoped>
.typing-caret::after {
content: '';
display: inline-block;
width: 3px;
height: 1.1em;
background: #fbbf24;
margin-left: 2px;
vertical-align: text-bottom;
animation: caret-blink 0.7s step-end infinite;
}
@keyframes caret-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* 2advanced-style card animation sequence */
.home-card {
min-height: 280px;
}
.home-card-shell {
background-color: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
border-radius: 1rem;
overflow: hidden;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
border: 1px solid transparent;
height: 100%;
}
.home-card-animate .home-card-shell {
animation: card-fly-in 1.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-delay: calc(var(--card-stagger) * 0.18s);
opacity: 0;
transform: translateY(50px) scale(0.92);
}
@keyframes card-fly-in {
0% {
opacity: 0;
transform: translateY(50px) scale(0.92);
border-color: transparent;
}
75% {
opacity: 1;
transform: translateY(0) scale(1);
border-color: transparent;
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
border-color: rgba(255, 255, 255, 0.18);
}
}
.home-card-inner {
overflow: hidden;
opacity: 0;
}
.home-card-animate .home-card-inner {
animation: inner-draw 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-delay: calc(var(--card-stagger) * 0.18s + 0.9s);
}
@keyframes inner-draw {
0% {
opacity: 0;
clip-path: inset(0 100% 0 0);
}
15% {
opacity: 1;
}
100% {
opacity: 1;
clip-path: inset(0 0 0 0);
}
}
.home-card-text {
overflow: hidden;
}
.home-card-stats {
overflow: hidden;
}
.home-card-btn {
opacity: 0;
transform: scale(0.5);
border-color: transparent;
}
.home-card-animate .home-card-btn {
animation: btn-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
animation-delay: calc(var(--card-stagger) * 0.18s + 1.5s);
}
@keyframes btn-pop {
0% {
opacity: 0;
transform: scale(0.5);
border-color: transparent;
}
85% {
opacity: 1;
transform: scale(1);
border-color: transparent;
}
100% {
opacity: 1;
transform: scale(1);
border-color: rgba(255, 255, 255, 0.18);
}
}
/* When not animating, show everything */
.home-card:not(.home-card-animate) .home-card-inner,
.home-card:not(.home-card-animate) .home-card-btn {
opacity: 1;
animation: none;
clip-path: none;
transform: none;
border-color: rgba(255, 255, 255, 0.18);
}
.home-card:not(.home-card-animate) .home-card-shell {
border-color: rgba(255, 255, 255, 0.18);
}
</style>