Skip to content

Commit b722e7a

Browse files
committed
feat(vscode): add chalkboard UI with dark/light theme and skill popups
Rebuild webview with chalkboard aesthetic — CSS custom properties for dark (black board) and light (white board) themes with toggle button. Skill tree rewritten as collapsible phase sections with centered modal popups. Add chalk-dust overlays, dashed borders, text glow effects, animated cards, particle fields, scroll reveals, and level-up cinematics. Replace all hardcoded SVG colors with theme-aware CSS variables.
1 parent bad65a6 commit b722e7a

18 files changed

Lines changed: 1727 additions & 538 deletions
Lines changed: 139 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,177 @@
1-
import React, { useState } from 'react';
1+
import React, { useCallback, useEffect, useState } from 'react';
2+
import { AnimatePresence, motion } from 'framer-motion';
23
import { useSkills } from './hooks/useSkills';
34
import { Dashboard } from './components/Dashboard';
45
import { SkillInventory } from './components/SkillInventory';
56
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';
610

711
type Tab = 'dashboard' | 'inventory' | 'skilltree';
12+
type Theme = 'dark' | 'light';
813

914
const TABS: { id: Tab; label: string; icon: string }[] = [
1015
{ id: 'dashboard', label: 'Dashboard', icon: '\u{1F3AE}' },
1116
{ id: 'inventory', label: 'Inventory', icon: '\u{1F0CF}' },
1217
{ id: 'skilltree', label: 'Skill Tree', icon: '\u{1F333}' },
1318
];
1419

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+
1545
export function App() {
1646
const root = document.getElementById('root');
1747
const initialTab = (root?.dataset.initialTab as Tab) ?? 'dashboard';
1848
const initialSkill = root?.dataset.initialSkill ?? undefined;
1949

2050
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]);
2278

2379
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}
34107
</div>
35-
</div>
36-
</div>
37-
)}
108+
</motion.div>
109+
)}
110+
</AnimatePresence>
38111

39112
{/* 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">
41114
{TABS.map(tab => (
42-
<button
115+
<motion.button
43116
key={tab.id}
44117
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'
47122
}`}
48123
>
49124
{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>
51134
))}
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>
54148
</div>
55149
</nav>
56150

57151
{/* 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>
73175
</div>
74176
);
75177
}

packages/vscode-extension/src/webview/components/AchievementBadge.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,24 @@ export function AchievementBadge({ name, description, icon, unlocked, category }
2121

2222
return (
2323
<div
24-
className={`flex flex-col items-center gap-1 p-2 ${unlocked ? 'badge-unlocked' : 'badge-locked'}`}
24+
className={`flex flex-col items-center gap-1 p-2 ${
25+
unlocked
26+
? 'badge-unlocked hover:bg-board-light transition-colors rounded-lg'
27+
: 'badge-locked'
28+
}`}
2529
title={`${name}: ${description}`}
2630
>
2731
<div
2832
className="w-16 h-16 rounded-full flex items-center justify-center text-2xl relative"
2933
style={{
30-
border: `3px solid ${unlocked ? ringColor : '#374151'}`,
31-
background: unlocked ? `${ringColor}15` : '#1a1a2e',
32-
boxShadow: unlocked ? `0 0 12px ${ringColor}40` : 'none',
34+
border: `2px dashed ${unlocked ? ringColor : 'var(--achievement-locked-border)'}`,
35+
background: unlocked ? `${ringColor}10` : 'var(--achievement-locked-bg)',
36+
boxShadow: unlocked ? `0 0 10px ${ringColor}25` : 'none',
3337
}}
3438
>
3539
{unlocked ? icon : '\u{1F512}'}
3640
</div>
37-
<span className="text-[10px] text-center max-w-[72px] truncate">
41+
<span className="text-[10px] text-center max-w-[80px] line-clamp-2">
3842
{unlocked ? name : '???'}
3943
</span>
4044
</div>

0 commit comments

Comments
 (0)