|
1 | | -import React, { useState } from 'react'; |
| 1 | +import React, { useCallback, useEffect, useState } from 'react'; |
| 2 | +import { AnimatePresence, motion } from 'framer-motion'; |
2 | 3 | import { useSkills } from './hooks/useSkills'; |
3 | 4 | import { Dashboard } from './components/Dashboard'; |
4 | 5 | import { SkillInventory } from './components/SkillInventory'; |
5 | 6 | import { SkillTree } from './components/SkillTree'; |
| 7 | +import { LevelUpCinematic } from './components/animations/LevelUpCinematic'; |
| 8 | +import { AchievementCeremony } from './components/animations/AchievementCeremony'; |
| 9 | +import { postMessage } from './vscode-api'; |
6 | 10 |
|
7 | 11 | type Tab = 'dashboard' | 'inventory' | 'skilltree'; |
| 12 | +type Theme = 'dark' | 'light'; |
8 | 13 |
|
9 | 14 | const TABS: { id: Tab; label: string; icon: string }[] = [ |
10 | 15 | { id: 'dashboard', label: 'Dashboard', icon: '\u{1F3AE}' }, |
11 | 16 | { id: 'inventory', label: 'Inventory', icon: '\u{1F0CF}' }, |
12 | 17 | { id: 'skilltree', label: 'Skill Tree', icon: '\u{1F333}' }, |
13 | 18 | ]; |
14 | 19 |
|
| 20 | +function applyTheme(theme: Theme) { |
| 21 | + document.documentElement.setAttribute('data-theme', theme); |
| 22 | + document.body.setAttribute('data-theme', theme); |
| 23 | +} |
| 24 | + |
| 25 | +function useTheme(): [Theme, () => void] { |
| 26 | + const root = document.getElementById('root'); |
| 27 | + const initial = (root?.dataset.theme as Theme) ?? 'dark'; |
| 28 | + const [theme, setTheme] = useState<Theme>(initial); |
| 29 | + |
| 30 | + useEffect(() => { |
| 31 | + applyTheme(theme); |
| 32 | + }, [theme]); |
| 33 | + |
| 34 | + const toggle = useCallback(() => { |
| 35 | + setTheme(prev => { |
| 36 | + const next = prev === 'dark' ? 'light' : 'dark'; |
| 37 | + postMessage({ type: 'theme:changed', payload: { theme: next } }); |
| 38 | + return next; |
| 39 | + }); |
| 40 | + }, []); |
| 41 | + |
| 42 | + return [theme, toggle]; |
| 43 | +} |
| 44 | + |
15 | 45 | export function App() { |
16 | 46 | const root = document.getElementById('root'); |
17 | 47 | const initialTab = (root?.dataset.initialTab as Tab) ?? 'dashboard'; |
18 | 48 | const initialSkill = root?.dataset.initialSkill ?? undefined; |
19 | 49 |
|
20 | 50 | const [activeTab, setActiveTab] = useState<Tab>(initialTab); |
21 | | - const { skills, progression, lastAchievement } = useSkills(); |
| 51 | + const [focusSkillId, setFocusSkillId] = useState<string | undefined>(initialSkill); |
| 52 | + const [theme, toggleTheme] = useTheme(); |
| 53 | + const { |
| 54 | + skills, |
| 55 | + progression, |
| 56 | + lastAchievement, |
| 57 | + dismissAchievement, |
| 58 | + levelUp, |
| 59 | + dismissLevelUp, |
| 60 | + lastAutoRecord, |
| 61 | + navigateTo, |
| 62 | + clearNavigate, |
| 63 | + } = useSkills(); |
| 64 | + |
| 65 | + // Handle navigate messages from extension host |
| 66 | + useEffect(() => { |
| 67 | + if (navigateTo) { |
| 68 | + const tab = navigateTo.tab as Tab; |
| 69 | + if (TABS.some(t => t.id === tab)) { |
| 70 | + setActiveTab(tab); |
| 71 | + } |
| 72 | + if (navigateTo.skillId) { |
| 73 | + setFocusSkillId(navigateTo.skillId); |
| 74 | + } |
| 75 | + clearNavigate(); |
| 76 | + } |
| 77 | + }, [navigateTo]); |
22 | 78 |
|
23 | 79 | return ( |
24 | | - <div className="min-h-screen bg-surface"> |
25 | | - {/* Achievement Toast */} |
26 | | - {lastAchievement && ( |
27 | | - <div className="achievement-toast"> |
28 | | - <div className="bg-surface-light border border-xp-bar/40 rounded-xl px-4 py-3 flex items-center gap-3 shadow-lg"> |
29 | | - <span className="text-2xl">{lastAchievement.icon}</span> |
30 | | - <div> |
31 | | - <div className="text-sm font-bold text-xp-bar">Achievement Unlocked!</div> |
32 | | - <div className="text-xs text-gray-300">{lastAchievement.name}</div> |
33 | | - <div className="text-[10px] text-xp-bar">+{lastAchievement.xpReward} XP</div> |
| 80 | + <div className="min-h-screen bg-board"> |
| 81 | + {/* Level Up Cinematic */} |
| 82 | + <LevelUpCinematic levelUp={levelUp} onDismiss={dismissLevelUp} /> |
| 83 | + |
| 84 | + {/* Achievement Ceremony — deferred if level-up is playing */} |
| 85 | + <AchievementCeremony |
| 86 | + achievement={lastAchievement} |
| 87 | + onDismiss={dismissAchievement} |
| 88 | + defer={!!levelUp} |
| 89 | + /> |
| 90 | + |
| 91 | + {/* Auto-record indicator */} |
| 92 | + <AnimatePresence> |
| 93 | + {lastAutoRecord && ( |
| 94 | + <motion.div |
| 95 | + className="fixed top-4 left-4 z-[80]" |
| 96 | + initial={{ opacity: 0, x: -40 }} |
| 97 | + animate={{ opacity: 1, x: 0 }} |
| 98 | + exit={{ opacity: 0, x: -40 }} |
| 99 | + > |
| 100 | + <div className="bg-board-light chalk-border rounded-lg px-3 py-2 text-xs text-chalk-green flex items-center gap-2"> |
| 101 | + <motion.div |
| 102 | + className="w-2 h-2 rounded-full bg-chalk-green" |
| 103 | + animate={{ scale: [1, 1.5, 1], opacity: [1, 0.5, 1] }} |
| 104 | + transition={{ duration: 1, repeat: Infinity }} |
| 105 | + /> |
| 106 | + Auto-recorded: {lastAutoRecord.skillId} |
34 | 107 | </div> |
35 | | - </div> |
36 | | - </div> |
37 | | - )} |
| 108 | + </motion.div> |
| 109 | + )} |
| 110 | + </AnimatePresence> |
38 | 111 |
|
39 | 112 | {/* Tab Navigation */} |
40 | | - <nav className="flex border-b border-white/10 px-4 sticky top-0 bg-surface z-40"> |
| 113 | + <nav className="flex chalk-line px-4 sticky top-0 bg-board z-40"> |
41 | 114 | {TABS.map(tab => ( |
42 | | - <button |
| 115 | + <motion.button |
43 | 116 | key={tab.id} |
44 | 117 | onClick={() => setActiveTab(tab.id)} |
45 | | - className={`px-4 py-3 text-sm font-medium transition-colors ${ |
46 | | - activeTab === tab.id ? 'tab-active' : 'tab-inactive' |
| 118 | + whileHover={{ y: -1 }} |
| 119 | + whileTap={{ scale: 0.97 }} |
| 120 | + className={`px-4 py-3 text-sm font-chalk font-medium transition-colors relative ${ |
| 121 | + activeTab === tab.id ? 'tab-active chalk-text' : 'tab-inactive' |
47 | 122 | }`} |
48 | 123 | > |
49 | 124 | {tab.icon} {tab.label} |
50 | | - </button> |
| 125 | + {activeTab === tab.id && ( |
| 126 | + <motion.div |
| 127 | + className="absolute bottom-0 left-2 right-2 h-[2px] rounded-full bg-chalk" |
| 128 | + style={{ boxShadow: '0 0 6px var(--text-glow)' }} |
| 129 | + layoutId="activeTab" |
| 130 | + transition={{ type: 'spring', stiffness: 400, damping: 30 }} |
| 131 | + /> |
| 132 | + )} |
| 133 | + </motion.button> |
51 | 134 | ))} |
52 | | - <div className="ml-auto flex items-center text-xs text-gray-500 px-3"> |
53 | | - {skills.length} skills loaded |
| 135 | + |
| 136 | + <div className="ml-auto flex items-center gap-2 px-3"> |
| 137 | + <span className="text-xs text-chalk-dim font-chalk"> |
| 138 | + {skills.length} skills |
| 139 | + </span> |
| 140 | + {/* Theme toggle */} |
| 141 | + <button |
| 142 | + onClick={toggleTheme} |
| 143 | + className="theme-toggle" |
| 144 | + title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`} |
| 145 | + > |
| 146 | + {theme === 'dark' ? '\u2600\uFE0F' : '\u{1F319}'} |
| 147 | + </button> |
54 | 148 | </div> |
55 | 149 | </nav> |
56 | 150 |
|
57 | 151 | {/* Content */} |
58 | | - <main> |
59 | | - {activeTab === 'dashboard' && ( |
60 | | - <Dashboard skills={skills} progression={progression} /> |
61 | | - )} |
62 | | - {activeTab === 'inventory' && ( |
63 | | - <SkillInventory |
64 | | - skills={skills} |
65 | | - progression={progression} |
66 | | - focusSkillId={initialSkill} |
67 | | - /> |
68 | | - )} |
69 | | - {activeTab === 'skilltree' && ( |
70 | | - <SkillTree skills={skills} progression={progression} /> |
71 | | - )} |
72 | | - </main> |
| 152 | + <AnimatePresence mode="wait"> |
| 153 | + <motion.main |
| 154 | + key={activeTab} |
| 155 | + initial={{ opacity: 0, y: 8 }} |
| 156 | + animate={{ opacity: 1, y: 0 }} |
| 157 | + exit={{ opacity: 0, y: -8 }} |
| 158 | + transition={{ duration: 0.2 }} |
| 159 | + > |
| 160 | + {activeTab === 'dashboard' && ( |
| 161 | + <Dashboard skills={skills} progression={progression} /> |
| 162 | + )} |
| 163 | + {activeTab === 'inventory' && ( |
| 164 | + <SkillInventory |
| 165 | + skills={skills} |
| 166 | + progression={progression} |
| 167 | + focusSkillId={focusSkillId} |
| 168 | + /> |
| 169 | + )} |
| 170 | + {activeTab === 'skilltree' && ( |
| 171 | + <SkillTree skills={skills} progression={progression} /> |
| 172 | + )} |
| 173 | + </motion.main> |
| 174 | + </AnimatePresence> |
73 | 175 | </div> |
74 | 176 | ); |
75 | 177 | } |
0 commit comments