diff --git a/apps/skilltree/CLAUDE.md b/apps/skilltree/CLAUDE.md
new file mode 100644
index 000000000..a7d31ce8a
--- /dev/null
+++ b/apps/skilltree/CLAUDE.md
@@ -0,0 +1,104 @@
+# SkillTree
+
+Gamified personal skill tracking app - like an RPG skill tree for real life.
+
+## Overview
+
+Track your skills, earn XP through activities, and level up your abilities across different life domains.
+
+## Tech Stack
+
+- **Web**: SvelteKit + Svelte 5 + Tailwind CSS
+- **Storage**: IndexedDB (offline-first, no backend needed)
+- **State**: Svelte 5 runes (`$state`, `$derived`)
+
+## Development
+
+```bash
+# Start development server (port 5195)
+pnpm dev:web
+
+# Or from monorepo root
+pnpm --filter @skilltree/web dev
+```
+
+## Project Structure
+
+```
+apps/skilltree/
+├── apps/
+│ └── web/ # SvelteKit web app
+│ ├── src/
+│ │ ├── lib/
+│ │ │ ├── components/ # UI components
+│ │ │ ├── services/ # IndexedDB storage
+│ │ │ ├── stores/ # Svelte 5 reactive stores
+│ │ │ └── types/ # TypeScript types
+│ │ └── routes/ # SvelteKit routes
+│ └── static/ # Static assets
+└── package.json
+```
+
+## Features
+
+### MVP (Current)
+
+- [x] Skill creation with name, description, and branch
+- [x] Six skill branches: Intellect, Body, Creativity, Social, Practical, Mindset
+- [x] XP system with 6 levels (0-5)
+- [x] Activity logging with XP rewards
+- [x] Stats overview (total XP, skills, highest level, streak)
+- [x] Offline-first with IndexedDB
+- [x] Branch filtering
+- [x] Recent activities feed
+
+### Planned
+
+- [ ] Skill editing
+- [ ] Skill tree visualization (graph view)
+- [ ] Skill dependencies/prerequisites
+- [ ] Achievements/badges
+- [ ] Data export/import
+- [ ] Cloud sync (optional)
+
+## Data Model
+
+### Skill
+```typescript
+interface Skill {
+ id: string;
+ name: string;
+ description: string;
+ branch: SkillBranch;
+ parentId: string | null;
+ icon: string;
+ color: string | null;
+ currentXp: number;
+ totalXp: number;
+ level: number;
+ createdAt: string;
+ updatedAt: string;
+}
+```
+
+### Levels
+
+| Level | Name | XP Required |
+|-------|---------------|-------------|
+| 0 | Unbekannt | 0 |
+| 1 | Anfänger | 100 |
+| 2 | Fortgeschritten | 500 |
+| 3 | Kompetent | 1,500 |
+| 4 | Experte | 4,000 |
+| 5 | Meister | 10,000 |
+
+## Branches
+
+| Branch | Icon | Color | Description |
+|------------|-----------|---------|--------------------------------|
+| Intellect | brain | blue | Knowledge, languages, science |
+| Body | dumbbell | red | Fitness, sports, health |
+| Creativity | palette | pink | Art, music, writing |
+| Social | users | purple | Communication, leadership |
+| Practical | wrench | orange | Crafts, cooking, tech |
+| Mindset | heart | emerald | Meditation, focus, resilience |
diff --git a/apps/skilltree/apps/web/package.json b/apps/skilltree/apps/web/package.json
new file mode 100644
index 000000000..66774ff58
--- /dev/null
+++ b/apps/skilltree/apps/web/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "@skilltree/web",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "preview": "vite preview",
+ "prepare": "svelte-kit sync || echo ''",
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
+ "type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
+ },
+ "devDependencies": {
+ "@sveltejs/adapter-node": "^5.0.0",
+ "@sveltejs/kit": "^2.0.0",
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
+ "@tailwindcss/vite": "^4.1.7",
+ "@types/node": "^20.0.0",
+ "svelte": "^5.0.0",
+ "svelte-check": "^4.0.0",
+ "tailwindcss": "^4.1.7",
+ "tslib": "^2.4.1",
+ "typescript": "^5.0.0",
+ "vite": "^6.0.0"
+ },
+ "dependencies": {
+ "@manacore/shared-tailwind": "workspace:*",
+ "@manacore/shared-theme": "workspace:*",
+ "@manacore/shared-utils": "workspace:*",
+ "idb": "^8.0.0",
+ "lucide-svelte": "^0.556.0",
+ "uuid": "^11.0.0"
+ },
+ "type": "module"
+}
diff --git a/apps/skilltree/apps/web/src/app.css b/apps/skilltree/apps/web/src/app.css
new file mode 100644
index 000000000..48822d4bf
--- /dev/null
+++ b/apps/skilltree/apps/web/src/app.css
@@ -0,0 +1,141 @@
+@import 'tailwindcss';
+@import '@manacore/shared-tailwind/themes.css';
+
+:root {
+ /* SkillTree - Emerald/Green Theme (Growth & Progress) */
+ --color-primary: #10b981;
+ --color-primary-hover: #059669;
+ --color-primary-light: #34d399;
+ --color-primary-dark: #047857;
+
+ --color-secondary: #ecfdf5;
+ --color-secondary-hover: #d1fae5;
+
+ --color-accent: #6ee7b7;
+ --color-accent-hover: #34d399;
+
+ /* XP & Level Colors */
+ --color-xp: #fbbf24;
+ --color-xp-glow: rgba(251, 191, 36, 0.4);
+ --color-level-up: #f59e0b;
+
+ /* Skill Levels */
+ --color-level-0: #6b7280;
+ --color-level-1: #3b82f6;
+ --color-level-2: #8b5cf6;
+ --color-level-3: #ec4899;
+ --color-level-4: #f97316;
+ --color-level-5: #fbbf24;
+
+ /* Branch Colors */
+ --color-branch-intellect: #3b82f6;
+ --color-branch-body: #ef4444;
+ --color-branch-creativity: #ec4899;
+ --color-branch-social: #8b5cf6;
+ --color-branch-practical: #f97316;
+ --color-branch-mindset: #10b981;
+}
+
+/* Dark mode overrides */
+:root.dark {
+ --color-secondary: #064e3b;
+ --color-secondary-hover: #065f46;
+}
+
+/* Skill card */
+.skill-card {
+ transition:
+ transform 0.2s ease,
+ box-shadow 0.2s ease,
+ border-color 0.2s ease;
+}
+
+.skill-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px -5px rgba(16, 185, 129, 0.2);
+}
+
+/* XP Progress Bar */
+.xp-bar {
+ background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-xp) 100%);
+ transition: width 0.5s ease;
+}
+
+.xp-bar-container {
+ background: rgba(107, 114, 128, 0.2);
+ overflow: hidden;
+}
+
+/* Level badge glow animation */
+@keyframes level-glow {
+ 0%,
+ 100% {
+ box-shadow: 0 0 5px var(--color-xp-glow);
+ }
+ 50% {
+ box-shadow: 0 0 20px var(--color-xp-glow);
+ }
+}
+
+.level-badge-glow {
+ animation: level-glow 2s ease-in-out infinite;
+}
+
+/* Level up animation */
+@keyframes level-up {
+ 0% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.2);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
+
+.level-up-animation {
+ animation: level-up 0.5s ease-in-out;
+}
+
+/* Branch indicator */
+.branch-indicator {
+ width: 4px;
+ border-radius: 2px;
+}
+
+/* Skill tree node */
+.tree-node {
+ transition:
+ transform 0.2s ease,
+ opacity 0.2s ease;
+}
+
+.tree-node:hover {
+ transform: scale(1.05);
+}
+
+.tree-node.locked {
+ opacity: 0.5;
+ filter: grayscale(0.8);
+}
+
+/* Progress ring */
+.progress-ring {
+ transition: stroke-dashoffset 0.5s ease;
+}
+
+/* Add XP button pulse */
+@keyframes pulse-xp {
+ 0%,
+ 100% {
+ box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4);
+ }
+ 50% {
+ box-shadow: 0 0 0 10px rgba(16, 185, 129, 0);
+ }
+}
+
+.pulse-xp:hover {
+ animation: pulse-xp 1.5s infinite;
+}
diff --git a/apps/skilltree/apps/web/src/app.d.ts b/apps/skilltree/apps/web/src/app.d.ts
new file mode 100644
index 000000000..da08e6da5
--- /dev/null
+++ b/apps/skilltree/apps/web/src/app.d.ts
@@ -0,0 +1,13 @@
+// See https://svelte.dev/docs/kit/types#app.d.ts
+// for information about these interfaces
+declare global {
+ namespace App {
+ // interface Error {}
+ // interface Locals {}
+ // interface PageData {}
+ // interface PageState {}
+ // interface Platform {}
+ }
+}
+
+export {};
diff --git a/apps/skilltree/apps/web/src/app.html b/apps/skilltree/apps/web/src/app.html
new file mode 100644
index 000000000..d8bfadbb4
--- /dev/null
+++ b/apps/skilltree/apps/web/src/app.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
diff --git a/apps/skilltree/apps/web/src/lib/components/AddSkillModal.svelte b/apps/skilltree/apps/web/src/lib/components/AddSkillModal.svelte
new file mode 100644
index 000000000..d03d15fe5
--- /dev/null
+++ b/apps/skilltree/apps/web/src/lib/components/AddSkillModal.svelte
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
Neuer Skill
+
+
+
+
+
+
diff --git a/apps/skilltree/apps/web/src/lib/components/AddXpModal.svelte b/apps/skilltree/apps/web/src/lib/components/AddXpModal.svelte
new file mode 100644
index 000000000..36ce4d803
--- /dev/null
+++ b/apps/skilltree/apps/web/src/lib/components/AddXpModal.svelte
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
+
XP hinzufügen
+
{skill.name} (Lvl {skill.level})
+
+
+
+
+
+
+
diff --git a/apps/skilltree/apps/web/src/lib/components/SkillCard.svelte b/apps/skilltree/apps/web/src/lib/components/SkillCard.svelte
new file mode 100644
index 000000000..e0840e1e6
--- /dev/null
+++ b/apps/skilltree/apps/web/src/lib/components/SkillCard.svelte
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
{skill.name}
+
{branchInfo.name}
+
+
+ {#each Array(skill.level) as _, i}
+
+ {/each}
+ {#each Array(5 - skill.level) as _, i}
+
+ {/each}
+
+
+
+
+
+
+ Lvl {skill.level} - {levelName}
+
+
+
+
+
+
+ XP
+
+ {skill.totalXp.toLocaleString()}
+ {#if !isMaxLevel}
+ / {nextLevelXp.toLocaleString()}
+ {/if}
+
+
+
+
+
+
+ {#if skill.description}
+
{skill.description}
+ {/if}
+
+
+
+
+
+
+
diff --git a/apps/skilltree/apps/web/src/lib/components/StatsOverview.svelte b/apps/skilltree/apps/web/src/lib/components/StatsOverview.svelte
new file mode 100644
index 000000000..52ac64441
--- /dev/null
+++ b/apps/skilltree/apps/web/src/lib/components/StatsOverview.svelte
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
Gesamt-XP
+
+ {skillStore.userStats.totalXp.toLocaleString()}
+
+
+
+
+
+
+
+
+
+
+
+
+
Skills
+
+ {skillStore.userStats.totalSkills}
+
+
+
+
+
+
+
+
+
+
+
+
+
Höchstes Level
+
+ {skillStore.userStats.highestLevel}
+
+
+
+
+
+
+
+
+
+
+
+
+
Streak
+
+ {skillStore.userStats.streakDays} Tage
+
+
+
+
+
diff --git a/apps/skilltree/apps/web/src/lib/services/storage.ts b/apps/skilltree/apps/web/src/lib/services/storage.ts
new file mode 100644
index 000000000..115a0e337
--- /dev/null
+++ b/apps/skilltree/apps/web/src/lib/services/storage.ts
@@ -0,0 +1,232 @@
+import { openDB, type DBSchema, type IDBPDatabase } from 'idb';
+import type { Skill, Activity, UserStats } from '$lib/types';
+
+interface SkillTreeDB extends DBSchema {
+ skills: {
+ key: string;
+ value: Skill;
+ indexes: {
+ 'by-branch': string;
+ 'by-parent': string | null;
+ 'by-level': number;
+ };
+ };
+ activities: {
+ key: string;
+ value: Activity;
+ indexes: {
+ 'by-skill': string;
+ 'by-timestamp': string;
+ };
+ };
+ stats: {
+ key: 'user-stats';
+ value: UserStats;
+ };
+}
+
+const DB_NAME = 'skilltree-db';
+const DB_VERSION = 1;
+
+let dbPromise: Promise> | null = null;
+
+function getDB(): Promise> {
+ if (!dbPromise) {
+ dbPromise = openDB(DB_NAME, DB_VERSION, {
+ upgrade(db) {
+ // Skills store
+ if (!db.objectStoreNames.contains('skills')) {
+ const skillStore = db.createObjectStore('skills', { keyPath: 'id' });
+ skillStore.createIndex('by-branch', 'branch');
+ skillStore.createIndex('by-parent', 'parentId');
+ skillStore.createIndex('by-level', 'level');
+ }
+
+ // Activities store
+ if (!db.objectStoreNames.contains('activities')) {
+ const activityStore = db.createObjectStore('activities', { keyPath: 'id' });
+ activityStore.createIndex('by-skill', 'skillId');
+ activityStore.createIndex('by-timestamp', 'timestamp');
+ }
+
+ // Stats store
+ if (!db.objectStoreNames.contains('stats')) {
+ db.createObjectStore('stats');
+ }
+ },
+ });
+ }
+ return dbPromise;
+}
+
+// Skills CRUD
+export async function getAllSkills(): Promise {
+ const db = await getDB();
+ return db.getAll('skills');
+}
+
+export async function getSkillById(id: string): Promise {
+ const db = await getDB();
+ return db.get('skills', id);
+}
+
+export async function getSkillsByBranch(branch: string): Promise {
+ const db = await getDB();
+ return db.getAllFromIndex('skills', 'by-branch', branch);
+}
+
+export async function getChildSkills(parentId: string): Promise {
+ const db = await getDB();
+ return db.getAllFromIndex('skills', 'by-parent', parentId);
+}
+
+export async function saveSkill(skill: Skill): Promise {
+ const db = await getDB();
+ skill.updatedAt = new Date().toISOString();
+ await db.put('skills', skill);
+}
+
+export async function deleteSkill(id: string): Promise {
+ const db = await getDB();
+ // Delete skill and all its activities
+ const activities = await db.getAllFromIndex('activities', 'by-skill', id);
+ const tx = db.transaction(['skills', 'activities'], 'readwrite');
+ await Promise.all([
+ tx.objectStore('skills').delete(id),
+ ...activities.map((a) => tx.objectStore('activities').delete(a.id)),
+ ]);
+ await tx.done;
+}
+
+// Activities CRUD
+export async function getAllActivities(): Promise {
+ const db = await getDB();
+ return db.getAll('activities');
+}
+
+export async function getActivitiesBySkill(skillId: string): Promise {
+ const db = await getDB();
+ return db.getAllFromIndex('activities', 'by-skill', skillId);
+}
+
+export async function getRecentActivities(limit = 10): Promise {
+ const db = await getDB();
+ const all = await db.getAllFromIndex('activities', 'by-timestamp');
+ return all.reverse().slice(0, limit);
+}
+
+export async function saveActivity(activity: Activity): Promise {
+ const db = await getDB();
+ await db.put('activities', activity);
+}
+
+export async function deleteActivity(id: string): Promise {
+ const db = await getDB();
+ await db.delete('activities', id);
+}
+
+// User Stats
+export async function getUserStats(): Promise {
+ const db = await getDB();
+ const stats = await db.get('stats', 'user-stats');
+ return (
+ stats ?? {
+ totalXp: 0,
+ totalSkills: 0,
+ highestLevel: 0,
+ streakDays: 0,
+ lastActivityDate: null,
+ }
+ );
+}
+
+export async function saveUserStats(stats: UserStats): Promise {
+ const db = await getDB();
+ await db.put('stats', stats, 'user-stats');
+}
+
+// Utility: Recalculate stats from all skills
+export async function recalculateStats(): Promise {
+ const skills = await getAllSkills();
+ const activities = await getAllActivities();
+
+ const stats: UserStats = {
+ totalXp: skills.reduce((sum, s) => sum + s.totalXp, 0),
+ totalSkills: skills.length,
+ highestLevel: skills.reduce((max, s) => Math.max(max, s.level), 0),
+ streakDays: calculateStreak(activities),
+ lastActivityDate: activities.length > 0 ? activities[activities.length - 1].timestamp : null,
+ };
+
+ await saveUserStats(stats);
+ return stats;
+}
+
+function calculateStreak(activities: Activity[]): number {
+ if (activities.length === 0) return 0;
+
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ const sortedDates = activities
+ .map((a) => {
+ const d = new Date(a.timestamp);
+ d.setHours(0, 0, 0, 0);
+ return d.getTime();
+ })
+ .filter((v, i, a) => a.indexOf(v) === i) // unique dates
+ .sort((a, b) => b - a); // newest first
+
+ let streak = 0;
+ let expectedDate = today.getTime();
+
+ for (const date of sortedDates) {
+ if (date === expectedDate || date === expectedDate - 86400000) {
+ streak++;
+ expectedDate = date - 86400000;
+ } else if (date < expectedDate - 86400000) {
+ break;
+ }
+ }
+
+ return streak;
+}
+
+// Export all data (for backup)
+export async function exportData(): Promise<{
+ skills: Skill[];
+ activities: Activity[];
+ stats: UserStats;
+}> {
+ const [skills, activities, stats] = await Promise.all([
+ getAllSkills(),
+ getAllActivities(),
+ getUserStats(),
+ ]);
+ return { skills, activities, stats };
+}
+
+// Import data (restore backup)
+export async function importData(data: {
+ skills: Skill[];
+ activities: Activity[];
+ stats: UserStats;
+}): Promise {
+ const db = await getDB();
+ const tx = db.transaction(['skills', 'activities', 'stats'], 'readwrite');
+
+ // Clear existing data
+ await tx.objectStore('skills').clear();
+ await tx.objectStore('activities').clear();
+
+ // Import new data
+ for (const skill of data.skills) {
+ await tx.objectStore('skills').put(skill);
+ }
+ for (const activity of data.activities) {
+ await tx.objectStore('activities').put(activity);
+ }
+ await tx.objectStore('stats').put(data.stats, 'user-stats');
+
+ await tx.done;
+}
diff --git a/apps/skilltree/apps/web/src/lib/stores/skills.svelte.ts b/apps/skilltree/apps/web/src/lib/stores/skills.svelte.ts
new file mode 100644
index 000000000..c5a5c0e96
--- /dev/null
+++ b/apps/skilltree/apps/web/src/lib/stores/skills.svelte.ts
@@ -0,0 +1,176 @@
+import type { Skill, Activity, UserStats, SkillBranch } from '$lib/types';
+import {
+ calculateLevel,
+ createDefaultSkill,
+ createActivity,
+ BRANCH_INFO,
+} from '$lib/types';
+import * as storage from '$lib/services/storage';
+
+// Reactive state using Svelte 5 runes
+let skills = $state([]);
+let activities = $state([]);
+let userStats = $state({
+ totalXp: 0,
+ totalSkills: 0,
+ highestLevel: 0,
+ streakDays: 0,
+ lastActivityDate: null,
+});
+let isLoading = $state(true);
+let initialized = $state(false);
+
+// Derived values
+const skillsByBranch = $derived(() => {
+ const grouped: Record = {
+ intellect: [],
+ body: [],
+ creativity: [],
+ social: [],
+ practical: [],
+ mindset: [],
+ custom: [],
+ };
+ for (const skill of skills) {
+ grouped[skill.branch].push(skill);
+ }
+ return grouped;
+});
+
+const topSkills = $derived(() => {
+ return [...skills].sort((a, b) => b.totalXp - a.totalXp).slice(0, 5);
+});
+
+const recentActivities = $derived(() => {
+ return [...activities].sort((a, b) =>
+ new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
+ ).slice(0, 10);
+});
+
+const branchStats = $derived(() => {
+ const stats: Record = {} as any;
+ for (const branch of Object.keys(BRANCH_INFO) as SkillBranch[]) {
+ const branchSkills = skills.filter((s) => s.branch === branch);
+ stats[branch] = {
+ count: branchSkills.length,
+ totalXp: branchSkills.reduce((sum, s) => sum + s.totalXp, 0),
+ avgLevel:
+ branchSkills.length > 0
+ ? branchSkills.reduce((sum, s) => sum + s.level, 0) / branchSkills.length
+ : 0,
+ };
+ }
+ return stats;
+});
+
+// Actions
+async function initialize() {
+ if (initialized) return;
+
+ isLoading = true;
+ try {
+ const [loadedSkills, loadedActivities, loadedStats] = await Promise.all([
+ storage.getAllSkills(),
+ storage.getAllActivities(),
+ storage.getUserStats(),
+ ]);
+ skills = loadedSkills;
+ activities = loadedActivities;
+ userStats = loadedStats;
+ initialized = true;
+ } catch (error) {
+ console.error('Failed to initialize skills store:', error);
+ } finally {
+ isLoading = false;
+ }
+}
+
+async function addSkill(data: Partial): Promise {
+ const skill = createDefaultSkill(data);
+ await storage.saveSkill(skill);
+ skills = [...skills, skill];
+ await updateStats();
+ return skill;
+}
+
+async function updateSkill(id: string, updates: Partial): Promise {
+ const index = skills.findIndex((s) => s.id === id);
+ if (index === -1) return;
+
+ const updatedSkill = { ...skills[index], ...updates, updatedAt: new Date().toISOString() };
+ await storage.saveSkill(updatedSkill);
+ skills = [...skills.slice(0, index), updatedSkill, ...skills.slice(index + 1)];
+ await updateStats();
+}
+
+async function deleteSkill(id: string): Promise {
+ await storage.deleteSkill(id);
+ skills = skills.filter((s) => s.id !== id);
+ activities = activities.filter((a) => a.skillId !== id);
+ await updateStats();
+}
+
+async function addXp(skillId: string, xp: number, description: string, duration?: number): Promise<{ leveledUp: boolean; newLevel: number }> {
+ const index = skills.findIndex((s) => s.id === skillId);
+ if (index === -1) return { leveledUp: false, newLevel: 0 };
+
+ const skill = skills[index];
+ const newTotalXp = skill.totalXp + xp;
+ const newCurrentXp = skill.currentXp + xp;
+ const newLevel = calculateLevel(newTotalXp);
+ const leveledUp = newLevel > skill.level;
+
+ const updatedSkill: Skill = {
+ ...skill,
+ totalXp: newTotalXp,
+ currentXp: newCurrentXp,
+ level: newLevel,
+ updatedAt: new Date().toISOString(),
+ };
+
+ const activity = createActivity(skillId, xp, description, duration);
+
+ await Promise.all([
+ storage.saveSkill(updatedSkill),
+ storage.saveActivity(activity),
+ ]);
+
+ skills = [...skills.slice(0, index), updatedSkill, ...skills.slice(index + 1)];
+ activities = [...activities, activity];
+ await updateStats();
+
+ return { leveledUp, newLevel };
+}
+
+async function updateStats(): Promise {
+ userStats = await storage.recalculateStats();
+}
+
+function getSkill(id: string): Skill | undefined {
+ return skills.find((s) => s.id === id);
+}
+
+function getSkillActivities(skillId: string): Activity[] {
+ return activities.filter((a) => a.skillId === skillId);
+}
+
+// Export store as object with getters for reactive access
+export const skillStore = {
+ get skills() { return skills; },
+ get activities() { return activities; },
+ get userStats() { return userStats; },
+ get isLoading() { return isLoading; },
+ get initialized() { return initialized; },
+ get skillsByBranch() { return skillsByBranch; },
+ get topSkills() { return topSkills; },
+ get recentActivities() { return recentActivities; },
+ get branchStats() { return branchStats; },
+
+ initialize,
+ addSkill,
+ updateSkill,
+ deleteSkill,
+ addXp,
+ getSkill,
+ getSkillActivities,
+};
diff --git a/apps/skilltree/apps/web/src/lib/types/index.ts b/apps/skilltree/apps/web/src/lib/types/index.ts
new file mode 100644
index 000000000..43079c9ab
--- /dev/null
+++ b/apps/skilltree/apps/web/src/lib/types/index.ts
@@ -0,0 +1,164 @@
+// Skill Tree Types
+
+export type SkillBranch =
+ | 'intellect'
+ | 'body'
+ | 'creativity'
+ | 'social'
+ | 'practical'
+ | 'mindset'
+ | 'custom';
+
+export interface Skill {
+ id: string;
+ name: string;
+ description: string;
+ branch: SkillBranch;
+ parentId: string | null;
+ icon: string;
+ color: string | null;
+ currentXp: number;
+ totalXp: number;
+ level: number;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface Activity {
+ id: string;
+ skillId: string;
+ xpEarned: number;
+ description: string;
+ duration: number | null; // minutes
+ timestamp: string;
+}
+
+export interface UserStats {
+ totalXp: number;
+ totalSkills: number;
+ highestLevel: number;
+ streakDays: number;
+ lastActivityDate: string | null;
+}
+
+// Level thresholds (XP needed for each level)
+export const LEVEL_THRESHOLDS = [0, 100, 500, 1500, 4000, 10000] as const;
+
+export const LEVEL_NAMES = [
+ 'Unbekannt',
+ 'Anfänger',
+ 'Fortgeschritten',
+ 'Kompetent',
+ 'Experte',
+ 'Meister',
+] as const;
+
+export const BRANCH_INFO: Record<
+ SkillBranch,
+ { name: string; icon: string; color: string; description: string }
+> = {
+ intellect: {
+ name: 'Intellekt',
+ icon: 'brain',
+ color: 'var(--color-branch-intellect)',
+ description: 'Wissen, Sprachen, Wissenschaft',
+ },
+ body: {
+ name: 'Körper',
+ icon: 'dumbbell',
+ color: 'var(--color-branch-body)',
+ description: 'Fitness, Sport, Gesundheit',
+ },
+ creativity: {
+ name: 'Kreativität',
+ icon: 'palette',
+ color: 'var(--color-branch-creativity)',
+ description: 'Kunst, Musik, Schreiben',
+ },
+ social: {
+ name: 'Sozial',
+ icon: 'users',
+ color: 'var(--color-branch-social)',
+ description: 'Kommunikation, Leadership, Empathie',
+ },
+ practical: {
+ name: 'Praktisch',
+ icon: 'wrench',
+ color: 'var(--color-branch-practical)',
+ description: 'Handwerk, Kochen, Technologie',
+ },
+ mindset: {
+ name: 'Mindset',
+ icon: 'heart',
+ color: 'var(--color-branch-mindset)',
+ description: 'Meditation, Fokus, Resilienz',
+ },
+ custom: {
+ name: 'Eigene',
+ icon: 'star',
+ color: 'var(--color-primary)',
+ description: 'Eigene Kategorien',
+ },
+};
+
+// Helper functions
+export function calculateLevel(xp: number): number {
+ for (let i = LEVEL_THRESHOLDS.length - 1; i >= 0; i--) {
+ if (xp >= LEVEL_THRESHOLDS[i]) {
+ return i;
+ }
+ }
+ return 0;
+}
+
+export function xpForNextLevel(currentLevel: number): number {
+ if (currentLevel >= LEVEL_THRESHOLDS.length - 1) {
+ return Infinity;
+ }
+ return LEVEL_THRESHOLDS[currentLevel + 1];
+}
+
+export function xpProgress(xp: number, level: number): number {
+ if (level >= LEVEL_THRESHOLDS.length - 1) {
+ return 100;
+ }
+ const currentThreshold = LEVEL_THRESHOLDS[level];
+ const nextThreshold = LEVEL_THRESHOLDS[level + 1];
+ const progress = ((xp - currentThreshold) / (nextThreshold - currentThreshold)) * 100;
+ return Math.min(100, Math.max(0, progress));
+}
+
+export function createDefaultSkill(partial: Partial = {}): Skill {
+ const now = new Date().toISOString();
+ return {
+ id: crypto.randomUUID(),
+ name: '',
+ description: '',
+ branch: 'custom',
+ parentId: null,
+ icon: 'star',
+ color: null,
+ currentXp: 0,
+ totalXp: 0,
+ level: 0,
+ createdAt: now,
+ updatedAt: now,
+ ...partial,
+ };
+}
+
+export function createActivity(
+ skillId: string,
+ xpEarned: number,
+ description: string,
+ duration?: number
+): Activity {
+ return {
+ id: crypto.randomUUID(),
+ skillId,
+ xpEarned,
+ description,
+ duration: duration ?? null,
+ timestamp: new Date().toISOString(),
+ };
+}
diff --git a/apps/skilltree/apps/web/src/routes/+layout.svelte b/apps/skilltree/apps/web/src/routes/+layout.svelte
new file mode 100644
index 000000000..a4e23339c
--- /dev/null
+++ b/apps/skilltree/apps/web/src/routes/+layout.svelte
@@ -0,0 +1,32 @@
+
+
+
+ SkillTree - Level Up Your Life
+
+
+
+{#if loading}
+
+
+
🌳
+
Loading SkillTree...
+
+
+{:else}
+
+ {@render children()}
+
+{/if}
diff --git a/apps/skilltree/apps/web/src/routes/+page.svelte b/apps/skilltree/apps/web/src/routes/+page.svelte
new file mode 100644
index 000000000..5c8cdeadb
--- /dev/null
+++ b/apps/skilltree/apps/web/src/routes/+page.svelte
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+
+
+
SkillTree
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#each Object.entries(BRANCH_INFO) as [branch, info]}
+ {@const count = skillStore.skills.filter((s) => s.branch === branch).length}
+ {#if count > 0 || branch !== 'custom'}
+
+ {/if}
+ {/each}
+
+
+
+
+ {#if filteredSkills().length === 0}
+
+
+
+
+
Noch keine Skills
+
+ Füge deinen ersten Skill hinzu und beginne dein Abenteuer!
+
+
+
+ {:else}
+
+ {#each filteredSkills() as skill (skill.id)}
+ openAddXpModal(skill)}
+ onEdit={() => {}}
+ onDelete={() => skillStore.deleteSkill(skill.id)}
+ />
+ {/each}
+
+ {/if}
+
+
+ {#if skillStore.recentActivities().length > 0}
+
+
+
+ Letzte Aktivitäten
+
+
+ {#each skillStore.recentActivities().slice(0, 5) as activity}
+ {@const skill = skillStore.getSkill(activity.skillId)}
+ {#if skill}
+
+
+
+ +{activity.xpEarned}
+
+
+ {skill.name}
+ - {activity.description}
+
+
+
+ {new Date(activity.timestamp).toLocaleDateString('de-DE')}
+
+
+ {/if}
+ {/each}
+
+
+ {/if}
+
+
+
+
+{#if showAddSkillModal}
+ (showAddSkillModal = false)}
+ onSave={async (skill) => {
+ await skillStore.addSkill(skill);
+ showAddSkillModal = false;
+ }}
+ />
+{/if}
+
+{#if showAddXpModal && selectedSkillForXp}
+ {
+ if (selectedSkillForXp) {
+ const result = await skillStore.addXp(selectedSkillForXp.id, xp, description, duration);
+ if (result.leveledUp) {
+ // Could show a level-up celebration here
+ }
+ }
+ closeAddXpModal();
+ }}
+ />
+{/if}
diff --git a/apps/skilltree/apps/web/static/manifest.json b/apps/skilltree/apps/web/static/manifest.json
new file mode 100644
index 000000000..d7d1624a9
--- /dev/null
+++ b/apps/skilltree/apps/web/static/manifest.json
@@ -0,0 +1,21 @@
+{
+ "name": "SkillTree",
+ "short_name": "SkillTree",
+ "description": "Track your skills like a game. Level up in real life.",
+ "start_url": "/",
+ "display": "standalone",
+ "background_color": "#111827",
+ "theme_color": "#10b981",
+ "icons": [
+ {
+ "src": "/icons/icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/icons/icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ]
+}
diff --git a/apps/skilltree/apps/web/svelte.config.js b/apps/skilltree/apps/web/svelte.config.js
new file mode 100644
index 000000000..a7a917e4c
--- /dev/null
+++ b/apps/skilltree/apps/web/svelte.config.js
@@ -0,0 +1,14 @@
+import adapter from '@sveltejs/adapter-node';
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ preprocess: vitePreprocess(),
+ kit: {
+ adapter: adapter({
+ out: 'build',
+ }),
+ },
+};
+
+export default config;
diff --git a/apps/skilltree/apps/web/tsconfig.json b/apps/skilltree/apps/web/tsconfig.json
new file mode 100644
index 000000000..a8f10c8e3
--- /dev/null
+++ b/apps/skilltree/apps/web/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "moduleResolution": "bundler"
+ }
+}
diff --git a/apps/skilltree/apps/web/vite.config.ts b/apps/skilltree/apps/web/vite.config.ts
new file mode 100644
index 000000000..8b51fcdc6
--- /dev/null
+++ b/apps/skilltree/apps/web/vite.config.ts
@@ -0,0 +1,17 @@
+import { sveltekit } from '@sveltejs/kit/vite';
+import tailwindcss from '@tailwindcss/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [tailwindcss(), sveltekit()],
+ server: {
+ port: 5195,
+ strictPort: true,
+ },
+ ssr: {
+ noExternal: ['@manacore/shared-tailwind', '@manacore/shared-theme'],
+ },
+ optimizeDeps: {
+ exclude: ['@manacore/shared-tailwind', '@manacore/shared-theme'],
+ },
+});
diff --git a/apps/skilltree/package.json b/apps/skilltree/package.json
new file mode 100644
index 000000000..30cf3c910
--- /dev/null
+++ b/apps/skilltree/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "skilltree",
+ "version": "1.0.0",
+ "private": true,
+ "description": "SkillTree - Gamified Personal Skill Tracking",
+ "scripts": {
+ "dev": "pnpm run --filter=@skilltree/* --parallel dev",
+ "dev:web": "pnpm --filter @skilltree/web dev"
+ },
+ "devDependencies": {
+ "typescript": "^5.9.3"
+ },
+ "packageManager": "pnpm@9.15.0"
+}