From 764843772a1f1a22c0eef901f28f3ffe6c35f50a Mon Sep 17 00:00:00 2001 From: Till JS Date: Tue, 24 Mar 2026 20:01:49 +0100 Subject: [PATCH] feat(manacore): add Seenplatte ecosystem observatory visualization Interactive SVG lake landscape that visualizes all ManaCore apps as plants around interconnected lakes. Plant type and health reflects ManaScore: - Oaks/Birches for mature/production apps (85+) - Young trees and reeds for beta apps - Sprouts for alpha/prototype apps - Animated water (waves, river flow particles) - Pan & zoom navigation - 6 lakes representing infrastructure (Auth, Redis, MinIO, 3x PostgreSQL) - 20 apps with real ManaScore data Accessible at /observatory in the ManaCore web dashboard. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../observatory/SeenplatteScene.svelte | 157 ++++++++ .../lib/components/observatory/data/colors.ts | 84 ++++ .../lib/components/observatory/data/layout.ts | 172 +++++++++ .../components/observatory/data/mockData.ts | 362 ++++++++++++++++++ .../lib/components/observatory/data/types.ts | 69 ++++ .../observatory/plants/MossCluster.svelte | 63 +++ .../observatory/plants/PlantFactory.svelte | 25 ++ .../observatory/plants/ReedPlant.svelte | 98 +++++ .../observatory/plants/Sprout.svelte | 81 ++++ .../observatory/plants/TreePlant.svelte | 117 ++++++ .../observatory/plants/WaterLily.svelte | 118 ++++++ .../observatory/terrain/Background.svelte | 70 ++++ .../observatory/terrain/Terrain.svelte | 59 +++ .../observatory/terrain/WaterBody.svelte | 70 ++++ .../observatory/water/RiverFlow.svelte | 69 ++++ .../apps/web/src/routes/(app)/+layout.svelte | 1 + .../src/routes/(app)/observatory/+page.svelte | 23 ++ 17 files changed, 1638 insertions(+) create mode 100644 apps/manacore/apps/web/src/lib/components/observatory/SeenplatteScene.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/observatory/data/colors.ts create mode 100644 apps/manacore/apps/web/src/lib/components/observatory/data/layout.ts create mode 100644 apps/manacore/apps/web/src/lib/components/observatory/data/mockData.ts create mode 100644 apps/manacore/apps/web/src/lib/components/observatory/data/types.ts create mode 100644 apps/manacore/apps/web/src/lib/components/observatory/plants/MossCluster.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/observatory/plants/PlantFactory.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/observatory/plants/ReedPlant.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/observatory/plants/Sprout.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/observatory/plants/TreePlant.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/observatory/plants/WaterLily.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/observatory/terrain/Background.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/observatory/terrain/Terrain.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/observatory/terrain/WaterBody.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/observatory/water/RiverFlow.svelte create mode 100644 apps/manacore/apps/web/src/routes/(app)/observatory/+page.svelte diff --git a/apps/manacore/apps/web/src/lib/components/observatory/SeenplatteScene.svelte b/apps/manacore/apps/web/src/lib/components/observatory/SeenplatteScene.svelte new file mode 100644 index 000000000..6b82157f3 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/observatory/SeenplatteScene.svelte @@ -0,0 +1,157 @@ + + +
+ + + + +
+

+ Mana Seenplatte +

+

Ecosystem Observatory

+
+ + +
+ + + Mature + + + + Production + + + + Beta + + + + Alpha + +
+ + + + + + + + + + {#each RIVERS as river} + + {/each} + + + {#each LAKES as lake} + + {/each} + + + {#each apps.toSorted((a, b) => a.position.y - b.position.y) as app (app.id)} + handleAppClick(app.id)} /> + {/each} + +
diff --git a/apps/manacore/apps/web/src/lib/components/observatory/data/colors.ts b/apps/manacore/apps/web/src/lib/components/observatory/data/colors.ts new file mode 100644 index 000000000..74856010b --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/observatory/data/colors.ts @@ -0,0 +1,84 @@ +// Natural color palette for the Seenplatte ecosystem + +export const sky = { + dayTop: '#87CEEB', + dayBottom: '#E0F0FF', + duskTop: '#2C1654', + duskBottom: '#E8956A', + nightTop: '#0B1026', + nightBottom: '#1A2444', +} as const; + +export const mountains = { + far: '#8BA4B8', + mid: '#6B8A9E', + near: '#4A7085', + snow: '#E8F0F4', +} as const; + +export const water = { + shallow: '#7EC8D9', + mid: '#4BA3B5', + deep: '#2A7A8C', + veryDeep: '#1A5666', + river: '#5BB5C5', + highlight: '#A8E4F0', + foam: '#E8F6FA', +} as const; + +export const terrain = { + meadow: '#7DB86A', + meadowLight: '#96CC86', + meadowDark: '#5E9A4D', + shore: '#C4B48A', + shoreDark: '#A89870', + path: '#D4C4A0', + rock: '#8C8C80', + rockLight: '#A8A89C', +} as const; + +export const vegetation = { + // Tree health colors based on ManaScore + healthyDark: '#2D6B30', + healthy: '#3E8B42', + healthyLight: '#5AAB5E', + moderate: '#7DB86A', + warning: '#C4B848', + stressed: '#D4944A', + critical: '#B85C4A', + + // Specific plant colors + trunk: '#6B4E3D', + trunkLight: '#8B6E5D', + reed: '#7DA868', + reedDark: '#5A8548', + lilyPad: '#4A8B50', + lilyFlower: '#E8B0D0', + lilyFlowerCenter: '#F0D060', + moss: '#5A8B4A', + mossLight: '#78A868', + sprout: '#90C880', + sproutStake: '#A89070', +} as const; + +/** + * Get tree crown color based on ManaScore (0-100) + */ +export function getHealthColor(score: number): string { + if (score >= 85) return vegetation.healthyDark; + if (score >= 70) return vegetation.healthy; + if (score >= 55) return vegetation.moderate; + if (score >= 40) return vegetation.warning; + if (score >= 25) return vegetation.stressed; + return vegetation.critical; +} + +/** + * Get water clarity based on error rate (0 = clear, 1 = murky) + */ +export function getWaterColor(clarity: number): string { + if (clarity > 0.8) return water.shallow; + if (clarity > 0.5) return water.mid; + if (clarity > 0.2) return water.deep; + return water.veryDeep; +} diff --git a/apps/manacore/apps/web/src/lib/components/observatory/data/layout.ts b/apps/manacore/apps/web/src/lib/components/observatory/data/layout.ts new file mode 100644 index 000000000..9775aa896 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/observatory/data/layout.ts @@ -0,0 +1,172 @@ +import type { LakeData, RiverData } from './types'; + +// Scene dimensions (SVG viewBox) +export const SCENE = { + width: 1600, + height: 900, + viewBox: '0 0 1600 900', +} as const; + +// Mountain layer paths (3 layers, back to front) +export const MOUNTAIN_PATHS = { + far: 'M0,280 Q200,180 400,240 Q500,210 600,250 Q750,160 900,220 Q1050,180 1200,230 Q1350,170 1500,210 L1600,240 L1600,350 L0,350 Z', + mid: 'M0,310 Q150,250 300,290 Q450,240 550,280 Q700,220 850,270 Q1000,230 1150,275 Q1300,240 1450,265 L1600,280 L1600,380 L0,380 Z', + near: 'M0,340 Q100,300 250,330 Q400,290 500,320 Q650,280 800,315 Q950,285 1100,320 Q1250,295 1400,310 L1600,320 L1600,400 L0,400 Z', +} as const; + +// Lake definitions with organic SVG paths +export const LAKES: LakeData[] = [ + { + id: 'auth', + name: 'auth', + label: 'Zentralsee', + path: 'M680,440 Q720,410 790,415 Q860,420 890,445 Q910,470 900,500 Q885,535 850,550 Q800,565 750,558 Q700,550 680,525 Q660,500 665,470 Q668,450 680,440 Z', + color: '#4BA3B5', + colorDeep: '#2A7A8C', + clarity: 1, + level: 0.8, + position: { x: 790, y: 485 }, + }, + { + id: 'redis', + name: 'redis', + label: 'Bergsee', + path: 'M280,390 Q310,370 350,375 Q390,380 405,400 Q415,420 405,440 Q390,455 355,460 Q320,462 295,450 Q275,435 270,415 Q268,398 280,390 Z', + color: '#7EC8D9', + colorDeep: '#4BA3B5', + clarity: 1, + level: 0.9, + position: { x: 340, y: 420 }, + }, + { + id: 'minio', + name: 'minio', + label: 'Stausee', + path: 'M1180,400 Q1220,375 1280,380 Q1340,385 1370,410 Q1390,435 1380,465 Q1360,490 1320,500 Q1270,510 1220,502 Q1185,492 1170,468 Q1158,445 1165,420 Q1170,405 1180,400 Z', + color: '#5BB5C5', + colorDeep: '#3A8A9A', + clarity: 0.9, + level: 0.7, + position: { x: 1275, y: 440 }, + }, + { + id: 'db-left', + name: 'postgres-left', + label: 'Waldsee West', + path: 'M180,580 Q220,555 280,560 Q340,565 365,590 Q380,615 370,640 Q355,665 310,675 Q260,682 215,670 Q180,658 165,635 Q155,612 165,595 Q170,582 180,580 Z', + color: '#3A8A9A', + colorDeep: '#1A5666', + clarity: 0.85, + level: 0.75, + position: { x: 270, y: 620 }, + }, + { + id: 'db-center', + name: 'postgres-center', + label: 'Waldsee Mitte', + path: 'M650,620 Q700,595 770,600 Q840,605 870,635 Q890,660 878,690 Q862,718 810,730 Q760,738 710,730 Q660,720 640,695 Q625,670 632,645 Q638,628 650,620 Z', + color: '#3A8A9A', + colorDeep: '#1A5666', + clarity: 0.9, + level: 0.8, + position: { x: 760, y: 665 }, + }, + { + id: 'db-right', + name: 'postgres-right', + label: 'Waldsee Ost', + path: 'M1120,590 Q1160,568 1220,572 Q1280,578 1305,600 Q1322,625 1315,652 Q1300,678 1255,690 Q1210,698 1165,688 Q1130,678 1115,655 Q1102,632 1108,610 Q1112,595 1120,590 Z', + color: '#3A8A9A', + colorDeep: '#1A5666', + clarity: 0.85, + level: 0.75, + position: { x: 1215, y: 630 }, + }, +]; + +// Rivers connecting the lakes +export const RIVERS: RiverData[] = [ + { + id: 'redis-to-auth', + from: 'redis', + to: 'auth', + path: 'M405,430 Q480,435 540,440 Q600,445 665,460', + flowSpeed: 0.8, + width: 8, + }, + { + id: 'minio-to-auth', + from: 'minio', + to: 'auth', + path: 'M1170,450 Q1100,455 1020,460 Q940,465 900,475', + flowSpeed: 0.6, + width: 8, + }, + { + id: 'auth-to-db-left', + from: 'auth', + to: 'db-left', + path: 'M710,555 Q620,570 520,580 Q420,590 365,595', + flowSpeed: 0.7, + width: 10, + }, + { + id: 'auth-to-db-center', + from: 'auth', + to: 'db-center', + path: 'M790,560 Q785,580 778,600 Q770,610 765,620', + flowSpeed: 0.7, + width: 10, + }, + { + id: 'auth-to-db-right', + from: 'auth', + to: 'db-right', + path: 'M870,548 Q960,565 1040,575 Q1080,582 1115,592', + flowSpeed: 0.7, + width: 10, + }, + { + id: 'inlet', + from: 'source', + to: 'auth', + path: 'M790,350 Q792,370 790,390 Q788,410 790,420', + flowSpeed: 0.9, + width: 6, + }, +]; + +// App positions around the lakes +export const APP_POSITIONS: Record = { + // Around Zentralsee (auth) - core/mature apps + manacore: { x: 730, y: 410, lakeId: 'auth' }, + chat: { x: 860, y: 420, lakeId: 'auth' }, + picture: { x: 910, y: 480, lakeId: 'auth' }, + presi: { x: 660, y: 490, lakeId: 'auth' }, + + // Around Waldsee West (db-left) + calendar: { x: 170, y: 560, lakeId: 'db-left' }, + todo: { x: 330, y: 555, lakeId: 'db-left' }, + contacts: { x: 370, y: 630, lakeId: 'db-left' }, + storage: { x: 160, y: 650, lakeId: 'db-left' }, + + // Around Waldsee Mitte (db-center) + zitare: { x: 640, y: 600, lakeId: 'db-center' }, + mukke: { x: 850, y: 610, lakeId: 'db-center' }, + clock: { x: 880, y: 680, lakeId: 'db-center' }, + nutriphi: { x: 650, y: 720, lakeId: 'db-center' }, + + // Around Waldsee Ost (db-right) + photos: { x: 1110, y: 575, lakeId: 'db-right' }, + skilltree: { x: 1310, y: 590, lakeId: 'db-right' }, + context: { x: 1320, y: 660, lakeId: 'db-right' }, + planta: { x: 1115, y: 675, lakeId: 'db-right' }, + + // Around Bergsee (redis) - lightweight/cache + matrix: { x: 260, y: 375, lakeId: 'redis' }, + traces: { x: 400, y: 385, lakeId: 'redis' }, + + // Around Stausee (minio) - storage-heavy + manadeck: { x: 1180, y: 385, lakeId: 'minio' }, + questions: { x: 1370, y: 400, lakeId: 'minio' }, +}; diff --git a/apps/manacore/apps/web/src/lib/components/observatory/data/mockData.ts b/apps/manacore/apps/web/src/lib/components/observatory/data/mockData.ts new file mode 100644 index 000000000..bdb160166 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/observatory/data/mockData.ts @@ -0,0 +1,362 @@ +import type { AppData, AppStatus, PlantType, CategoryScores } from './types'; +import { APP_POSITIONS } from './layout'; + +interface AppDefinition { + id: string; + displayName: string; + score: number; + status: AppStatus; + categories: CategoryScores; +} + +function getPlantType(status: AppStatus, score: number): PlantType { + if (score >= 85) return 'oak'; + if (score >= 70) return 'birch'; + if (status === 'beta' && score >= 55) return 'youngTree'; + if (status === 'beta') return 'reed'; + if (status === 'alpha') return 'sprout'; + if (status === 'prototype') return 'sprout'; + return 'youngTree'; +} + +const APP_DEFINITIONS: AppDefinition[] = [ + { + id: 'calendar', + displayName: 'Calendar', + score: 97, + status: 'mature', + categories: { + backend: 95, + frontend: 96, + database: 95, + testing: 98, + deployment: 100, + documentation: 98, + security: 95, + ux: 98, + }, + }, + { + id: 'todo', + displayName: 'Todo', + score: 96, + status: 'mature', + categories: { + backend: 94, + frontend: 95, + database: 95, + testing: 97, + deployment: 98, + documentation: 95, + security: 95, + ux: 96, + }, + }, + { + id: 'contacts', + displayName: 'Contacts', + score: 94, + status: 'production', + categories: { + backend: 92, + frontend: 90, + database: 95, + testing: 92, + deployment: 95, + documentation: 95, + security: 95, + ux: 94, + }, + }, + { + id: 'manacore', + displayName: 'ManaCore', + score: 88, + status: 'production', + categories: { + backend: 55, + frontend: 90, + database: 70, + testing: 72, + deployment: 90, + documentation: 88, + security: 80, + ux: 92, + }, + }, + { + id: 'presi', + displayName: 'Presi', + score: 86, + status: 'mature', + categories: { + backend: 90, + frontend: 82, + database: 85, + testing: 80, + deployment: 90, + documentation: 85, + security: 85, + ux: 88, + }, + }, + { + id: 'storage', + displayName: 'Storage', + score: 84, + status: 'production', + categories: { + backend: 88, + frontend: 84, + database: 82, + testing: 78, + deployment: 88, + documentation: 82, + security: 85, + ux: 82, + }, + }, + { + id: 'chat', + displayName: 'Chat', + score: 82, + status: 'production', + categories: { + backend: 90, + frontend: 82, + database: 80, + testing: 68, + deployment: 88, + documentation: 85, + security: 80, + ux: 78, + }, + }, + { + id: 'picture', + displayName: 'Picture', + score: 81, + status: 'production', + categories: { + backend: 90, + frontend: 80, + database: 75, + testing: 65, + deployment: 85, + documentation: 82, + security: 82, + ux: 80, + }, + }, + { + id: 'mukke', + displayName: 'Mukke', + score: 80, + status: 'beta', + categories: { + backend: 90, + frontend: 78, + database: 80, + testing: 60, + deployment: 85, + documentation: 78, + security: 80, + ux: 80, + }, + }, + { + id: 'matrix', + displayName: 'Matrix', + score: 68, + status: 'production', + categories: { + backend: 10, + frontend: 78, + database: 60, + testing: 50, + deployment: 85, + documentation: 70, + security: 75, + ux: 72, + }, + }, + { + id: 'nutriphi', + displayName: 'NutriPhi', + score: 63, + status: 'beta', + categories: { + backend: 78, + frontend: 62, + database: 65, + testing: 42, + deployment: 70, + documentation: 58, + security: 62, + ux: 65, + }, + }, + { + id: 'photos', + displayName: 'Photos', + score: 62, + status: 'beta', + categories: { + backend: 82, + frontend: 65, + database: 60, + testing: 38, + deployment: 68, + documentation: 55, + security: 60, + ux: 62, + }, + }, + { + id: 'zitare', + displayName: 'Zitare', + score: 62, + status: 'beta', + categories: { + backend: 72, + frontend: 78, + database: 60, + testing: 38, + deployment: 68, + documentation: 55, + security: 58, + ux: 65, + }, + }, + { + id: 'context', + displayName: 'Context', + score: 60, + status: 'beta', + categories: { + backend: 75, + frontend: 75, + database: 55, + testing: 32, + deployment: 65, + documentation: 52, + security: 58, + ux: 60, + }, + }, + { + id: 'clock', + displayName: 'Clock', + score: 58, + status: 'beta', + categories: { + backend: 75, + frontend: 70, + database: 55, + testing: 30, + deployment: 62, + documentation: 48, + security: 55, + ux: 58, + }, + }, + { + id: 'skilltree', + displayName: 'SkillTree', + score: 58, + status: 'beta', + categories: { + backend: 65, + frontend: 68, + database: 55, + testing: 35, + deployment: 60, + documentation: 50, + security: 55, + ux: 60, + }, + }, + { + id: 'planta', + displayName: 'Planta', + score: 50, + status: 'alpha', + categories: { + backend: 68, + frontend: 58, + database: 45, + testing: 25, + deployment: 55, + documentation: 42, + security: 48, + ux: 50, + }, + }, + { + id: 'manadeck', + displayName: 'ManaDeck', + score: 48, + status: 'alpha', + categories: { + backend: 50, + frontend: 65, + database: 42, + testing: 28, + deployment: 52, + documentation: 45, + security: 42, + ux: 55, + }, + }, + { + id: 'questions', + displayName: 'Questions', + score: 48, + status: 'alpha', + categories: { + backend: 88, + frontend: 62, + database: 40, + testing: 20, + deployment: 45, + documentation: 38, + security: 40, + ux: 42, + }, + }, + { + id: 'traces', + displayName: 'Traces', + score: 35, + status: 'alpha', + categories: { + backend: 72, + frontend: 10, + database: 35, + testing: 15, + deployment: 40, + documentation: 30, + security: 35, + ux: 25, + }, + }, +]; + +export function createMockEcosystem(): AppData[] { + return APP_DEFINITIONS.map((def) => { + const pos = APP_POSITIONS[def.id] || { x: 800, y: 500, lakeId: 'auth' }; + return { + id: def.id, + name: def.id, + displayName: def.displayName, + score: def.score, + status: def.status, + health: 'up' as const, + plantType: getPlantType(def.status, def.score), + categories: def.categories, + trend: 0, + lakeId: pos.lakeId, + position: { x: pos.x, y: pos.y }, + }; + }); +} diff --git a/apps/manacore/apps/web/src/lib/components/observatory/data/types.ts b/apps/manacore/apps/web/src/lib/components/observatory/data/types.ts new file mode 100644 index 000000000..51d495acc --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/observatory/data/types.ts @@ -0,0 +1,69 @@ +export type PlantType = + | 'oak' + | 'birch' + | 'youngTree' + | 'reed' + | 'waterLily' + | 'moss' + | 'shrub' + | 'sprout' + | 'stump' + | 'swampCluster'; + +export type AppStatus = 'prototype' | 'alpha' | 'beta' | 'production' | 'mature'; + +export type HealthStatus = 'up' | 'degraded' | 'down' | 'unknown'; + +export interface CategoryScores { + backend: number; + frontend: number; + database: number; + testing: number; + deployment: number; + documentation: number; + security: number; + ux: number; +} + +export interface AppData { + id: string; + name: string; + displayName: string; + score: number; + status: AppStatus; + health: HealthStatus; + plantType: PlantType; + categories: CategoryScores; + trend: number; + lakeId: string; + position: { x: number; y: number }; +} + +export interface LakeData { + id: string; + name: string; + label: string; + path: string; + color: string; + colorDeep: string; + clarity: number; // 0-1, 1 = crystal clear + level: number; // 0-1, normalized fill level + position: { x: number; y: number }; +} + +export interface RiverData { + id: string; + from: string; + to: string; + path: string; + flowSpeed: number; // 0-1 + width: number; +} + +export interface EcosystemState { + apps: AppData[]; + lakes: LakeData[]; + rivers: RiverData[]; + timeOfDay: number; // 0-24 + systemHealth: 'sunny' | 'cloudy' | 'stormy'; +} diff --git a/apps/manacore/apps/web/src/lib/components/observatory/plants/MossCluster.svelte b/apps/manacore/apps/web/src/lib/components/observatory/plants/MossCluster.svelte new file mode 100644 index 000000000..724fdae38 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/observatory/plants/MossCluster.svelte @@ -0,0 +1,63 @@ + + + e.key === 'Enter' && onclick?.()} +> + {#each patches as patch} + + {/each} + + + + {app.displayName} + + diff --git a/apps/manacore/apps/web/src/lib/components/observatory/plants/PlantFactory.svelte b/apps/manacore/apps/web/src/lib/components/observatory/plants/PlantFactory.svelte new file mode 100644 index 000000000..74fd31aef --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/observatory/plants/PlantFactory.svelte @@ -0,0 +1,25 @@ + + +{#if app.plantType === 'oak' || app.plantType === 'birch' || app.plantType === 'youngTree'} + +{:else if app.plantType === 'reed'} + +{:else if app.plantType === 'waterLily'} + +{:else if app.plantType === 'sprout'} + +{:else if app.plantType === 'moss'} + +{:else} + + +{/if} diff --git a/apps/manacore/apps/web/src/lib/components/observatory/plants/ReedPlant.svelte b/apps/manacore/apps/web/src/lib/components/observatory/plants/ReedPlant.svelte new file mode 100644 index 000000000..ccdfa9dfa --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/observatory/plants/ReedPlant.svelte @@ -0,0 +1,98 @@ + + + e.key === 'Enter' && onclick?.()} +> + + + + {#each stalkData as stalk} + + + + + {#if stalk.hasBulrush} + + {/if} + {/each} + + + + + + + + + {app.displayName} + + diff --git a/apps/manacore/apps/web/src/lib/components/observatory/plants/Sprout.svelte b/apps/manacore/apps/web/src/lib/components/observatory/plants/Sprout.svelte new file mode 100644 index 000000000..7d6872a75 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/observatory/plants/Sprout.svelte @@ -0,0 +1,81 @@ + + + e.key === 'Enter' && onclick?.()} +> + + + + + + + + + + + + + + + + + + + + {app.displayName} + + diff --git a/apps/manacore/apps/web/src/lib/components/observatory/plants/TreePlant.svelte b/apps/manacore/apps/web/src/lib/components/observatory/plants/TreePlant.svelte new file mode 100644 index 000000000..67f108a5f --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/observatory/plants/TreePlant.svelte @@ -0,0 +1,117 @@ + + + e.key === 'Enter' && onclick?.()} +> + + + + + + + + + + {#if isOak} + + + + + {/if} + + + + {#each blobs as blob, i} + {@const lightness = i % 3 === 0 ? 15 : i % 3 === 1 ? 0 : -10} + + {/each} + + + + + + + + + {app.displayName} + + + {app.score} + + diff --git a/apps/manacore/apps/web/src/lib/components/observatory/plants/WaterLily.svelte b/apps/manacore/apps/web/src/lib/components/observatory/plants/WaterLily.svelte new file mode 100644 index 000000000..6d74057ef --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/observatory/plants/WaterLily.svelte @@ -0,0 +1,118 @@ + + + e.key === 'Enter' && onclick?.()} +> + + + + + + + + + + + + + + + {#if app.score > 20} + + + {#each petalData as petal} + {@const px = Math.cos(petal.angle) * 5 * bloomAmount} + {@const py = Math.sin(petal.angle) * 3 * bloomAmount} + + {/each} + + + + + {/if} + + + + + {app.displayName} + + diff --git a/apps/manacore/apps/web/src/lib/components/observatory/terrain/Background.svelte b/apps/manacore/apps/web/src/lib/components/observatory/terrain/Background.svelte new file mode 100644 index 000000000..4744cb934 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/observatory/terrain/Background.svelte @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {#each Array(60) as _, i} + {@const x = i * 28 + 10} + {@const baseY = 335 + Math.sin(i * 0.8) * 15} + {@const h = 12 + Math.sin(i * 1.3) * 5} + + {/each} + diff --git a/apps/manacore/apps/web/src/lib/components/observatory/terrain/Terrain.svelte b/apps/manacore/apps/web/src/lib/components/observatory/terrain/Terrain.svelte new file mode 100644 index 000000000..9438136d0 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/observatory/terrain/Terrain.svelte @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {#each Array(40) as _, i} + {@const x = i * 42 + 15} + {@const y = 820 + Math.sin(i * 2.1) * 20} + + {/each} + diff --git a/apps/manacore/apps/web/src/lib/components/observatory/terrain/WaterBody.svelte b/apps/manacore/apps/web/src/lib/components/observatory/terrain/WaterBody.svelte new file mode 100644 index 000000000..ac7361670 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/observatory/terrain/WaterBody.svelte @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {lake.label} + + diff --git a/apps/manacore/apps/web/src/lib/components/observatory/water/RiverFlow.svelte b/apps/manacore/apps/web/src/lib/components/observatory/water/RiverFlow.svelte new file mode 100644 index 000000000..a1a3f3ec4 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/observatory/water/RiverFlow.svelte @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + diff --git a/apps/manacore/apps/web/src/routes/(app)/+layout.svelte b/apps/manacore/apps/web/src/routes/(app)/+layout.svelte index 473329979..7ae8c6663 100644 --- a/apps/manacore/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/+layout.svelte @@ -82,6 +82,7 @@ let baseNavItems: PillNavItem[] = [ { href: '/home', label: 'Home', icon: 'home' }, { href: '/dashboard', label: 'Dashboard', icon: 'grid' }, + { href: '/observatory', label: 'Observatory', icon: 'eye' }, { href: '/credits', label: 'Credits', icon: 'creditCard' }, { href: '/gifts', label: 'Geschenke', icon: 'gift' }, { href: '/api-keys', label: 'API Keys', icon: 'key' }, diff --git a/apps/manacore/apps/web/src/routes/(app)/observatory/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/observatory/+page.svelte new file mode 100644 index 000000000..054187875 --- /dev/null +++ b/apps/manacore/apps/web/src/routes/(app)/observatory/+page.svelte @@ -0,0 +1,23 @@ + + +
+
+ +
+ + {#if selectedApp} +
+

+ Selected: {selectedApp} — Detail panel coming in Phase 5 +

+
+ {/if} +