onOpenChange(false)}
+ />
+ )}
+
+ {/* Modal container - exact same structure as LogViewer */}
+
+ {/* Header - same style as LogViewer */}
+
+
+
Mission Control Dashboard
+
+
+
+
+ {/* Content container - full height with scrolling */}
+
+
+ >
+ );
+};
+
+export default MissionControlModal;
diff --git a/plugins/analytics/ui/components/dashboard/tabs/MissionControlTab.tsx b/plugins/analytics/ui/components/dashboard/tabs/MissionControlTab.tsx
new file mode 100644
index 00000000..033ee55a
--- /dev/null
+++ b/plugins/analytics/ui/components/dashboard/tabs/MissionControlTab.tsx
@@ -0,0 +1,1417 @@
+import React, { useState, useMemo, useEffect } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
+import { useMissionControl, type Activity } from '@plugins/analytics/ui/hooks/useMissionControl';
+import { useRealTimeMissionControl, useProviderHistory } from '@plugins/analytics/ui/hooks/useMissionControlData';
+import { useConfig } from '@/components/ConfigProvider';
+import { formatResponseTime, formatPercentage, formatTokens, getResponseTimeColor, formatSuccessRate, getErrorRateColor } from '@/lib/formatters';
+import type { MissionControlData, ModelStat, HealthHistoryData } from '@plugins/analytics/ui/types/missionControl';
+import { api } from '@/lib/api';
+import { analyticsAPI } from '@plugins/analytics/ui/lib/analytics';
+import type { RealtimeStats, ModelStats } from '@plugins/analytics/ui/lib/analytics';
+import '@plugins/analytics/styles/mission-control.css';
+
+interface RouteCardData {
+ route: string;
+ displayName: string;
+ config: {
+ enabled: boolean;
+ provider: string;
+ model: string;
+ description: string;
+ };
+ stats: {
+ totalRequests: number;
+ successfulRequests: number;
+ failedRequests: number;
+ avgResponseTime: number;
+ successRate: number;
+ errorRate: number;
+ totalTokens: number;
+ totalCost: number;
+ lastUsed: number;
+ } | null;
+ recentActivity: Activity[];
+ score: number;
+ status: 'healthy' | 'warning' | 'error' | 'inactive' | 'low_success' | 'slow_response';
+}
+
+interface RequestData {
+ time: string;
+ requests: number;
+ errors: number;
+ latency: number;
+}
+
+interface RequestLog {
+ id: string;
+ timestamp: string;
+ method: string;
+ model: string;
+ status: 'success' | 'error' | 'pending';
+ responseTime: number;
+ tokens: number;
+}
+
+interface ServiceStatus {
+ name: string;
+ status: 'online' | 'offline' | 'loading';
+ port?: string;
+ icon: string;
+ details: string;
+}
+
+export function MissionControlTab() {
+ const [activeTab, setActiveTab] = useState<'routes' | 'overview' | 'providers' | 'analytics' | 'activity'>('routes');
+
+ // Hook-uri pentru date în timp real
+ const { routerConfig, routeStats, liveActivity, loading: basicLoading, error: basicError } = useMissionControl();
+ const { data: missionControlData, loading: mcLoading, error: mcError } = useRealTimeMissionControl(); // Too aggressive - 2s polling
+ const { data: providerHistory, loading: historyLoading } = useProviderHistory();
+
+ // Theme context
+ // Independent theme configuration for analytics (when themes plugin is disabled)
+ const isAdvanced = false; // Use standard UI styling for analytics dashboard
+
+ // Analytics state
+ const { config } = useConfig();
+ const [timeRange, setTimeRange] = useState('1h');
+ const [chartData, setChartData] = useState
([]);
+ const [requestLogs, setRequestLogs] = useState([]);
+ const [isLiveMode, setIsLiveMode] = useState(true);
+
+ // Activity logs state
+ const [activityTimeRange, setActivityTimeRange] = useState<'10m' | '1h' | '6h' | '24h' | 'all'>('1h');
+ const [activityLimit, setActivityLimit] = useState(50);
+
+ // Overview state - integrated from OverviewTab
+ const [realtimeStats, setRealtimeStats] = useState(null);
+ const [modelStats, setModelStats] = useState([]);
+ const [overviewLoading, setOverviewLoading] = useState(true);
+ const [services, setServices] = useState([
+ {
+ name: 'Proxy Service',
+ status: 'loading',
+ port: '8001',
+ icon: 'exchange-alt',
+ details: 'AI Request Proxy'
+ },
+ {
+ name: 'Web Interface',
+ status: 'online',
+ port: '3456',
+ icon: 'globe',
+ details: 'Management UI'
+ },
+ {
+ name: 'Socket Connections',
+ status: 'loading',
+ icon: 'plug',
+ details: 'WebSocket'
+ },
+ {
+ name: 'API Endpoints',
+ status: 'loading',
+ icon: 'code',
+ details: 'REST API'
+ }
+ ]);
+
+ // Real stats from config
+ const providers = Array.isArray(config?.Providers) ? config.Providers : [];
+ const totalProviders = providers.length;
+ const configuredProviders = providers.filter(p => p.api_key).length;
+ const totalModels = providers.reduce((acc, provider) => acc + (provider.models?.length || 0), 0);
+ const activeModel = config?.Router?.default || 'None selected';
+ const hasActiveModel = activeModel !== 'None selected' && activeModel !== '';
+
+ // Procesează datele pentru cardurile de rute
+ const routeCards: RouteCardData[] = useMemo(() => {
+ if (!routerConfig) return [];
+
+ const routes = [
+ { key: 'default', name: 'Default Route', icon: 'fa-home', color: 'cyan' },
+ { key: 'background', name: 'Background Tasks', icon: 'fa-tasks', color: isAdvanced ? 'blue' : 'green' },
+ { key: 'think', name: 'Thinking & Planning', icon: 'fa-brain', color: 'purple' },
+ { key: 'longContext', name: 'Long Context', icon: 'fa-scroll', color: 'blue' },
+ { key: 'webSearch', name: 'Web Search', icon: 'fa-search', color: 'orange' }
+ ];
+
+ return routes.map(route => {
+ const config = routerConfig[route.key as keyof typeof routerConfig];
+
+ // Găsește statisticile pentru această rută din routeStats
+ const routeStat = Array.isArray(routeStats)
+ ? routeStats.find(stat => stat.route === route.key)
+ : routeStats?.[route.key];
+
+ // Filtrează activitatea recentă pentru această rută
+ const safeActivity = Array.isArray(liveActivity) ? liveActivity : [];
+ const routeActivity = safeActivity.filter(activity => {
+ // Verifică dacă activitatea aparține acestei rute prin diverse metode
+ if (activity.route === route.key) return true;
+
+ // Fallback: încearcă să determine ruta din mesaj sau model
+ const message = activity.message.toLowerCase();
+ const model = (activity.actualModel || activity.model).toLowerCase();
+
+ switch (route.key) {
+ case 'background':
+ return message.includes('background') || message.includes('task');
+ case 'think':
+ return message.includes('think') || message.includes('reasoning') || message.includes('planning');
+ case 'longContext':
+ return message.includes('long') || message.includes('context') || message.includes('large');
+ case 'webSearch':
+ return message.includes('search') || message.includes('web') || message.includes('browse');
+ case 'default':
+ // Default primește tot ce nu se potrivește altundeva
+ return !['background', 'think', 'long', 'context', 'search', 'web'].some(keyword =>
+ message.includes(keyword) || model.includes(keyword)
+ );
+ default:
+ return false;
+ }
+ }).slice(0, 10); // Limitează la ultimele 10 activități
+
+ // Calculează scorul pentru această rută
+ const calculateScore = (stats: any) => {
+ if (!stats || stats.totalRequests === 0) return 0;
+
+ const successWeight = 0.7; // Creștem ponderea succesului
+ const performanceWeight = 0.3; // Reducem ponderea performanței
+
+ const successScore = (stats.successfulRequests / stats.totalRequests) * 100;
+ // Ajustăm formula pentru performance - permițând timpi mai lungi (până la 30 secunde)
+ const performanceScore = Math.max(0, 100 - Math.min(100, (stats.avgResponseTime / 300))); // 0-100 scale based on response time (30s max)
+
+ return Math.round(successScore * successWeight + performanceScore * performanceWeight);
+ };
+
+ const score = calculateScore(routeStat);
+
+ // Determină statusul
+ const getStatus = (config: any, stats: any, score: number) => {
+ if (!config.enabled) return 'inactive';
+ if (!stats || stats.totalRequests === 0) return 'inactive';
+ if (score >= 80) return 'healthy';
+ if (score >= 60) return 'warning';
+ // Pentru scoruri sub 60, determinăm cauza specifică
+ if (stats && stats.totalRequests > 0) {
+ const successRate = (stats.successfulRequests / stats.totalRequests) * 100;
+ if (successRate < 70) return 'low_success';
+ if (stats.avgResponseTime > 15000) return 'slow_response';
+ }
+ return 'warning';
+ };
+
+ return {
+ route: route.key,
+ displayName: route.name,
+ config,
+ stats: routeStat || null,
+ recentActivity: routeActivity,
+ score: Math.round(score),
+ status: getStatus(config, routeStat, score)
+ };
+ });
+ }, [routerConfig, routeStats, liveActivity]);
+
+ // Calculăm statistici agregate pentru celelalte taburi
+ const aggregatedStats = useMemo(() => {
+ if (!missionControlData?.aggregated) return null;
+
+ const { modelStats, totalRequests, successRate, avgResponseTime } = missionControlData.aggregated;
+
+ return {
+ totalRequests,
+ successRate,
+ avgResponseTime,
+ modelStats
+ };
+ }, [missionControlData]);
+
+ // Status sistem
+ const systemStatus = useMemo(() => {
+ if (!missionControlData?.live) return null;
+
+ const { queue, rateLimiting, deduplication } = missionControlData.live;
+
+ return {
+ queueStatus: queue.currentSize > 0 ? 'active' : 'idle',
+ circuitBreaker: rateLimiting.circuitBreakerState,
+ cacheHealth: deduplication.cacheHitRate > 50 ? 'good' : deduplication.cacheHitRate > 20 ? 'warning' : 'poor',
+ isProcessing: queue.processing
+ };
+ }, [missionControlData]);
+
+ // Filter activities based on time range and limit
+ const filteredActivities = useMemo(() => {
+ if (!liveActivity || !Array.isArray(liveActivity)) return [];
+
+ let filtered = [...liveActivity];
+
+ // Filter by time range
+ if (activityTimeRange !== 'all') {
+ const now = Date.now();
+ let timeRangeMs: number;
+
+ switch (activityTimeRange) {
+ case '10m': timeRangeMs = 10 * 60 * 1000; break;
+ case '1h': timeRangeMs = 60 * 60 * 1000; break;
+ case '6h': timeRangeMs = 6 * 60 * 60 * 1000; break;
+ case '24h': timeRangeMs = 24 * 60 * 60 * 1000; break;
+ default: timeRangeMs = 0;
+ }
+
+ filtered = filtered.filter(activity => {
+ // Convert timestamp to number if it's a string
+ const activityTimestamp = typeof activity.timestamp === 'string'
+ ? new Date(activity.timestamp).getTime()
+ : activity.timestamp;
+ return now - activityTimestamp <= timeRangeMs;
+ });
+ }
+
+ // Apply limit
+ if (activityLimit > 0) {
+ filtered = filtered.slice(0, activityLimit);
+ }
+
+ return filtered;
+ }, [liveActivity, activityTimeRange, activityLimit]);
+
+ // Provider status
+ const providerStatus = useMemo(() => {
+ if (!missionControlData?.live?.providers) return [];
+
+ return Object.entries(missionControlData.live.providers).map(([provider, stats]) => ({
+ provider,
+ status: stats.failureCount > 5 ? 'degraded' : stats.inRecovery ? 'recovery' : 'healthy',
+ healthScore: Math.max(0, 100 - (stats.failureCount * 10)),
+ ...stats
+ }));
+ }, [missionControlData]);
+
+ // Funcții helper pentru culori și iconițe
+ const getRouteColor = (route: string) => {
+ const colors = {
+ default: 'cyan',
+ background: isAdvanced ? 'blue' : 'green',
+ think: 'purple',
+ longContext: 'blue',
+ webSearch: 'orange'
+ };
+ return colors[route as keyof typeof colors] || 'gray';
+ };
+
+ // Analytics data loading effect
+ useEffect(() => {
+ const loadRealData = async () => {
+ try {
+ // Load realtime stats
+ const realtimeResponse = await fetch('/api/analytics/realtime');
+ const realtimeData = await realtimeResponse.json();
+
+ // Load recent requests
+ const requestsResponse = await fetch('/api/analytics/requests?limit=50');
+ const requestsData = await requestsResponse.json();
+
+ // Generate chart data from real analytics time-series data
+ const generateChartDataFromReal = async () => {
+ try {
+ // Get time-series data based on selected time range
+ let hours = 1;
+ switch (timeRange) {
+ case '1h': hours = 1; break;
+ case '6h': hours = 6; break;
+ case '24h': hours = 24; break;
+ case '7d': hours = 24 * 7; break;
+ }
+
+ const timeSeriesResponse = await fetch(`/api/analytics/timeseries?hours=${hours}`);
+ const timeSeriesData = await timeSeriesResponse.json();
+
+ if (timeSeriesData.success && timeSeriesData.data && Array.isArray(timeSeriesData.data)) {
+ // Convert backend time-series data to chart format
+ const chartData: RequestData[] = timeSeriesData.data.map((point: any) => ({
+ time: point.time,
+ requests: point.requests || 0,
+ errors: point.errors || 0,
+ latency: point.avgResponseTime || 0
+ }));
+ setChartData(chartData);
+ } else {
+ // Fallback to empty data if no time-series available
+ const data: RequestData[] = [];
+ const now = new Date();
+ for (let i = 23; i >= 0; i--) {
+ const time = new Date(now.getTime() - i * 5 * 60 * 1000);
+ data.push({
+ time: time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
+ requests: 0,
+ errors: 0,
+ latency: 0
+ });
+ }
+ setChartData(data);
+ }
+ } catch (error) {
+ console.error('Error loading time-series data:', error);
+ // Fallback to empty data
+ setChartData([]);
+ }
+ };
+
+ // Convert real requests to request logs
+ if (requestsData.success && requestsData.data && Array.isArray(requestsData.data)) {
+ const realLogs: RequestLog[] = requestsData.data.map((req: any, i: number) => ({
+ id: req.id || `req-${i}`,
+ timestamp: new Date(req.timestamp).toLocaleTimeString(),
+ method: `${req.method} ${req.endpoint}`,
+ model: req.actualModel || req.model,
+ status: req.statusCode < 400 ? 'success' : 'error',
+ responseTime: req.responseTime,
+ tokens: req.tokenCount || 0
+ }));
+ setRequestLogs(realLogs);
+ } else {
+ // No real data, show empty
+ setRequestLogs([]);
+ }
+
+ await generateChartDataFromReal();
+ } catch (error) {
+ console.error('Error loading real analytics data:', error);
+ // Fallback to empty data instead of fake data
+ setChartData([]);
+ setRequestLogs([]);
+ }
+ };
+
+ // Only load analytics data when analytics tab is active
+ if (activeTab === 'analytics') {
+ loadRealData();
+
+ // Update data every 5 seconds if live mode is on
+ const interval = isLiveMode ? setInterval(() => {
+ loadRealData();
+ }, 5000) : null;
+
+ return () => {
+ if (interval) clearInterval(interval);
+ };
+ }
+ }, [isLiveMode, timeRange, config, activeTab]);
+
+ // Overview data loading effect - integrated from OverviewTab
+ useEffect(() => {
+ let cleanup: (() => void) | null = null;
+
+ const loadOverviewData = async () => {
+ if (activeTab !== 'overview') return;
+
+ try {
+ setOverviewLoading(true);
+
+ // Load initial data
+ const [realtimeData, modelData] = await Promise.all([
+ analyticsAPI.getRealtimeStats(),
+ analyticsAPI.getModelStats()
+ ]);
+
+ setRealtimeStats(realtimeData);
+ setModelStats(modelData);
+
+ // Check service statuses
+ await checkServices();
+
+ // Set up real-time subscription
+ cleanup = analyticsAPI.subscribeToRealtimeStats((stats) => {
+ setRealtimeStats(stats);
+ }, 30000); // Update every 30 seconds
+
+ } catch (error) {
+ console.error('Error loading overview analytics data:', error);
+ // Fallback to config-only data if analytics fail
+ } finally {
+ setOverviewLoading(false);
+ }
+ };
+
+ loadOverviewData();
+
+ return () => {
+ if (cleanup) cleanup();
+ };
+ }, [activeTab]);
+
+ const checkServices = async () => {
+ try {
+ // Test if we can reach the API (since we're using it)
+ await api.getConfig();
+
+ setServices(prev => prev.map(service => {
+ if (service.name === 'API Endpoints') {
+ return { ...service, status: 'online' };
+ }
+ if (service.name === 'Proxy Service') {
+ return { ...service, status: 'online' }; // Assume online if we can reach API
+ }
+ // For other services, mark as online if we can reach the API, otherwise offline
+ return { ...service, status: 'online' };
+ }));
+ } catch (error) {
+ console.error('Error checking services:', error);
+ setServices(prev => prev.map(service => ({
+ ...service,
+ status: service.name === 'Web Interface' ? 'online' : 'offline'
+ })));
+ }
+ };
+
+ // Analytics calculations
+ const totalRequests = chartData.reduce((sum, data) => sum + data.requests, 0);
+ const totalErrors = chartData.reduce((sum, data) => sum + data.errors, 0);
+ const avgLatency = Math.round(chartData.reduce((sum, data) => sum + data.latency, 0) / chartData.length) || 0;
+ const errorRate = totalRequests > 0 ? (totalErrors / totalRequests) * 100 : 0;
+
+ const getRouteIcon = (route: string) => {
+ const icons = {
+ default: 'fa-home',
+ background: 'fa-tasks',
+ think: 'fa-brain',
+ longContext: 'fa-scroll',
+ webSearch: 'fa-search'
+ };
+ return icons[route as keyof typeof icons] || 'fa-circle';
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'healthy':
+ return isAdvanced ? 'text-blue-400 border-blue-400/30' : 'text-green-400 border-green-400/30';
+ case 'warning':
+ return 'text-yellow-400 border-yellow-400/30';
+ case 'error':
+ return 'text-red-400 border-red-400/30';
+ case 'low_success':
+ return 'text-orange-400 border-orange-400/30';
+ case 'slow_response':
+ return 'text-blue-400 border-blue-400/30';
+ case 'inactive':
+ return 'text-gray-400 border-gray-400/30';
+ default:
+ return 'text-gray-400 border-gray-400/30';
+ }
+ };
+
+ const getStatusBg = (status: string) => {
+ switch (status) {
+ case 'healthy':
+ return isAdvanced ? 'bg-blue-400/10 hover:bg-blue-400/20' : 'bg-green-400/10 hover:bg-green-400/20';
+ case 'warning':
+ return 'bg-yellow-400/10 hover:bg-yellow-400/20';
+ case 'error':
+ return 'bg-red-400/10 hover:bg-red-400/20';
+ case 'low_success':
+ return 'bg-orange-400/10 hover:bg-orange-400/20';
+ case 'slow_response':
+ return 'bg-blue-400/10 hover:bg-blue-400/20';
+ case 'inactive':
+ return 'bg-gray-400/10 hover:bg-gray-400/20';
+ default:
+ return 'bg-gray-400/10 hover:bg-gray-400/20';
+ }
+ };
+
+ const getScoreColor = (score: number) => {
+ if (score >= 80) return isAdvanced ? 'text-blue-400' : 'text-green-400';
+ if (score >= 60) return 'text-yellow-400';
+ return 'text-red-400';
+ };
+
+ const getActivityStatusColor = (type: string) => {
+ switch (type) {
+ case 'success':
+ return isAdvanced ? 'bg-blue-400' : 'bg-green-400';
+ case 'error':
+ return 'bg-red-400';
+ case 'warning':
+ return 'bg-yellow-400';
+ default:
+ return 'bg-blue-400';
+ }
+ };
+
+ // Loading state combinat
+ const isLoading = basicLoading || mcLoading || historyLoading;
+ const error = basicError || mcError;
+
+ return (
+
+ {/* Header */}
+
+ Mission Control
+ Monitorizare completă a sistemului în timp real.
+
+
+ {/* Navigation */}
+
+
+
+
+
+
+
+
+ {/* Content */}
+
+
+ {isLoading && Se încarcă datele...
}
+ {error && Eroare: {error}
}
+
+ {/* Enhanced Overview Tab - Integrated from OverviewTab */}
+ {activeTab === 'overview' && (
+
+ {/* System Overview - Real Stats from Config and Analytics */}
+
+
+
+
System Overview
+ {overviewLoading && (
+
+
+ Loading real-time data...
+
+ )}
+
+
+ {/* Real Stats from Config and Analytics */}
+
+
+ {realtimeStats?.last24h.totalRequests || 0}
+ Total Requests (24h)
+
+
+
+ {totalProviders}
+ Total Providers
+
+
+
+ {formatResponseTime(realtimeStats?.last24h.avgResponseTime || 0)}
+ Avg Response Time
+
+
+
+ {formatSuccessRate(realtimeStats?.last24h.errorRate || 0)}
+ Success Rate
+
+
+
+ {/* Live Performance Metrics */}
+ {realtimeStats && (
+
+
+
+
Live Performance
+ ● Live
+
+
+
+
+
Current Activity
+
+
+ Active Requests:
+ {realtimeStats.current.activeRequests}
+
+
+ Avg Response:
+
+ {formatResponseTime(realtimeStats.current.avgResponseTime)}
+
+
+
+ Error Rate:
+
+ {formatPercentage(realtimeStats?.current?.errorRate || 0)}
+
+
+
+
+
+
+
Last Hour
+
+
+ Total Requests:
+ {realtimeStats.last1h.totalRequests}
+
+
+ Avg Response:
+
+ {formatResponseTime(realtimeStats.last1h.avgResponseTime)}
+
+
+
+ Top Model:
+
+ {realtimeStats.last1h.topModels[0]?.model.substring(0, 15) || 'None'}
+
+
+
+
+
+
+
Top Models (24h)
+
+ {realtimeStats?.last24h?.topModels && Array.isArray(realtimeStats.last24h.topModels) ?
+ realtimeStats.last24h.topModels.slice(0, 3).map((model, index) => (
+
+ {model.model.substring(0, 12)}:
+ {model.count}
+
+ )) : (
+
No data available
+ )
+ }
+
+
+
+
+ )}
+
+ {/* Current Configuration Summary */}
+
+
+
+
Current Configuration
+
+
+
+ {/* Active Model */}
+
+
Active Router
+
+
+ Default Model:
+
+ {activeModel}
+
+
+
+ Background Model:
+ {config?.Router?.background || 'Not set'}
+
+
+ Long Context:
+ {config?.Router?.longContext || 'Not set'}
+
+
+
+
+ {/* Provider Summary */}
+
+
Provider Status
+
+ {providers.slice(0, 3).map((provider, index) => (
+
+
{provider.name || `Provider ${index + 1}`}:
+
+
+ {provider.models?.length || 0} models
+
+
+ ))}
+ {providers.length > 3 && (
+
+ +{providers.length - 3} more providers
+
+ )}
+
+
+
+
+
+
+ {/* Services Status */}
+
+
+
+
+
Services Status
+
+
+
+
+
+ {services.map((service, index) => (
+
+
+
+
+
+
{service.name}
+
{service.details}
+ {service.port &&
Port {service.port}
}
+
+
+ {service.status === 'loading' ? 'Checking...' : service.status}
+
+
+ ))}
+
+
+
+ )}
+
+ {/* Routes Tab - 5 Beautiful Route Cards */}
+ {activeTab === 'routes' && (
+
+
+
Live Route Monitoring
+
+ Actualizat în timp real • {routeCards.length} rute active
+
+
+
+ {!routerConfig && !isLoading && (
+
+
+
+ Nu s-au putut încărca configurațiile rutelor. Verificați conexiunea API.
+
+
+ )}
+
+ {routeCards.length > 0 && (
+
+ {routeCards.map((card, index) => {
+ const color = getRouteColor(card.route);
+ const icon = getRouteIcon(card.route);
+ const statusColors = getStatusColor(card.status);
+ const scoreColor = getScoreColor(card.score);
+
+ return (
+
+ {/* Header - more compact */}
+
+
+
+
+
+
+
+
{card.displayName}
+
{card.config.description}
+
+
+
+
+ {card.score}
+
+
Score
+
+
+
+ {/* Status Badge - more compact */}
+
+
+ {card.status === 'healthy' ? 'Sănătos' :
+ card.status === 'warning' ? 'Atenție' :
+ card.status === 'error' ? 'Eroare' :
+ card.status === 'low_success' ? 'Succes scăzut' :
+ card.status === 'slow_response' ? 'Răspuns lent' : 'Inactiv'}
+
+
+
+ {/* Model Configuration - more compact */}
+
+
+
+ Model:
+
+ {card.config.model || 'N/A'}
+
+
+
+ Provider:
+
+ {card.config.provider || 'N/A'}
+
+
+
+
+
+ {/* Statistics - more compact */}
+ {card.stats && (
+
+
+
+
+ {card.stats.totalRequests.toLocaleString()}
+
+
Cereri
+
+
+
+ {card.stats.successRate.toFixed(1)}%
+
+
Succes
+
+
+
+ {formatResponseTime(card.stats.avgResponseTime)}
+
+
Timp
+
+
+
+ )}
+
+
+ {/* Recent Activity - more compact with internal scroll */}
+
+
+
Activitate
+
+ {card.recentActivity.length}
+
+
+
+ {card.recentActivity.length > 0 ? (
+ card.recentActivity.map((activity, idx) => (
+
+ {/* Single compact row: Status + Time */}
+
+
+
+
+ {new Date(activity.timestamp).toLocaleTimeString('ro-RO', {
+ hour: '2-digit',
+ minute: '2-digit'
+ })}
+
+
+
+ {formatResponseTime(activity.responseTime)}
+
+
+ {/* Message row */}
+
+ {activity.message.replace('Request successful', '✓').replace('Request failed', '✗')}
+
+
+ ))
+ ) : (
+
+
+
Nicio activitate recentă
+
+ )}
+
+
+
+ {/* Decorative Elements */}
+
+
+
+ );
+ })}
+
+ )}
+
+ {routeCards.length === 0 && !isLoading && (
+
+
+
Nu sunt configurate rute
+
Configurați rutele pentru a vedea monitorizarea în timp real
+
+ )}
+
+ )}
+
+ {/* Providers Tab */}
+ {activeTab === 'providers' && (
+
+
Status Providers
+
+ {!providerStatus.length && !isLoading && (
+
+
+
+ Nu s-au putut încărca informațiile despre provideri. Verificați conexiunea API.
+
+
+ )}
+
+ {providerStatus.length > 0 && (
+
+ {providerStatus.map((provider) => (
+
+
+
{provider.provider}
+
+
+
+ {provider.healthScore}%
+
+
+
+
+
+
+ Failures:
+ {provider.failureCount}
+
+
+ Recovering:
+ {provider.inRecovery ? 'Da' : 'Nu'}
+
+
+
+ ))}
+
+ )}
+
+ {!providerStatus.length && !isLoading && (
+
+
+
Nu sunt disponibile informații despre provideri
+
Verificați conexiunea la API
+
+ )}
+
+ )}
+
+ {/* Analytics Tab */}
+ {activeTab === 'analytics' && (
+
+ {/* Controls */}
+
+
+
+
+
Request Tracking
+
+
+
+
+ Live Mode
+
+
+
+
+
+
+
+ {/* Tracking Stats */}
+
+
+
{totalRequests > 999 ? formatTokens(totalRequests) : totalRequests}
+
Total Requests
+
+
+
{totalErrors}
+
Errors
+
+
+
{formatResponseTime(avgLatency)}
+
Avg Latency
+
+
+
{formatPercentage(errorRate)}
+
Error Rate
+
+
+
+
+ {/* Charts */}
+
+ {/* Request Volume Chart */}
+
+
+
+ Request Volume
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Latency Chart */}
+
+
+
+ Response Latency
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Activity Tab */}
+ {activeTab === 'activity' && (
+
+
+ {/* Controls */}
+
+
+
Detailed Activity Logs
+
Filtrează și navighează prin logs
+
+
+
+
+ Interval:
+
+
+
+
+ Limită:
+
+
+
+
+
+
+
+
+
Afișare: {activityLimit === 0 ? 'Toate' : activityLimit} logs
+
{filteredActivities.length} evenimente găsite
+
+
+
+
+
+
+
+ | Time |
+ Type |
+ Provider |
+ Model |
+ Route |
+ Message |
+ Response Time |
+ Tokens |
+
+
+
+ {filteredActivities.map((activity: Activity) => (
+
+
+ {new Date(activity.timestamp).toLocaleString('ro-RO', {
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit'
+ })}
+ |
+
+
+
+ {activity.type}
+
+ |
+
+ {activity.provider || 'N/A'}
+ |
+
+ {activity.actualModel}
+ |
+
+ {activity.route || 'N/A'}
+ |
+
+ {activity.message}
+ |
+
+ {activity.responseTime ? formatResponseTime(activity.responseTime) : 'N/A'}
+ |
+
+ {activity.tokens !== undefined && activity.tokens !== null ? formatTokens(activity.tokens) : 'N/A'}
+ |
+
+ ))}
+
+
+
+
+ {filteredActivities.length === 0 && (
+
+
+
No Activity Logs
+
Waiting for system activity...
+
+ )}
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/plugins/analytics/ui/contexts/MissionControlContext.tsx b/plugins/analytics/ui/contexts/MissionControlContext.tsx
new file mode 100644
index 00000000..a5b11c80
--- /dev/null
+++ b/plugins/analytics/ui/contexts/MissionControlContext.tsx
@@ -0,0 +1,128 @@
+import React, { createContext, useContext } from 'react';
+import type { ReactNode } from 'react';
+import type {
+ MissionControlData,
+ ExecutionStats,
+ AggregatedData,
+ HistoricalDataPoint,
+ SystemPreset,
+ HealthHistoryData
+} from '@plugins/analytics/ui/types/missionControl';
+import { useMissionControlData, type UseMissionControlDataReturn } from '@plugins/analytics/ui/hooks/useMissionControlData';
+
+// Types for state
+interface MissionControlState {
+ data: MissionControlData | null;
+ loading: boolean;
+ error: string | null;
+ lastUpdated: number | null;
+}
+
+type MissionControlAction =
+ | { type: 'SET_LOADING'; payload: boolean }
+ | { type: 'SET_DATA'; payload: MissionControlData }
+ | { type: 'SET_ERROR'; payload: string | null }
+ | { type: 'UPDATE_TIMESTAMP' }
+ | { type: 'RESET' };
+
+// Initial state
+const initialState: MissionControlState = {
+ data: null,
+ loading: false,
+ error: null,
+ lastUpdated: null,
+};
+
+// Reducer
+function missionControlReducer(
+ state: MissionControlState,
+ action: MissionControlAction
+): MissionControlState {
+ switch (action.type) {
+ case 'SET_LOADING':
+ return { ...state, loading: action.payload };
+
+ case 'SET_DATA':
+ return {
+ ...state,
+ data: action.payload,
+ error: null,
+ lastUpdated: Date.now()
+ };
+
+ case 'SET_ERROR':
+ return { ...state, error: action.payload, loading: false };
+
+ case 'UPDATE_TIMESTAMP':
+ return { ...state, lastUpdated: Date.now() };
+
+ case 'RESET':
+ return initialState;
+
+ default:
+ return state;
+ }
+}
+
+// Context
+const MissionControlContext = createContext(null);
+
+// Provider
+interface MissionControlProviderProps {
+ children: ReactNode;
+}
+
+export function MissionControlProvider({ children }: MissionControlProviderProps) {
+ const missionControlData = useMissionControlData();
+
+ return (
+
+ {children}
+
+ );
+}
+
+// Hook
+export function useMissionControl() {
+ const context = useContext(MissionControlContext);
+
+ if (!context) {
+ throw new Error('useMissionControl must be used within MissionControlProvider');
+ }
+
+ return context;
+}
+
+// Selector hooks for performance optimization
+export function useExecutionStats(): ExecutionStats | null {
+ const context = useMissionControl();
+ return context.data?.live || null;
+}
+
+export function useAggregatedData(): AggregatedData | null {
+ const context = useMissionControl();
+ return context.data?.aggregated || null;
+}
+
+export function useHistoricalData(): HistoricalDataPoint[] {
+ const context = useMissionControl();
+ return context.data?.historical || [];
+}
+
+export function useMissionControlState() {
+ const context = useMissionControl();
+ return {
+ data: context.data,
+ isLoading: context.loading,
+ error: context.error,
+ live: context.data?.live,
+ historical: context.data?.historical || [],
+ historicalProviders: context.data?.historicalProviders || [],
+ aggregated: context.data?.aggregated
+ };
+}
+
+export function useConfig() {
+ const context = useMissionControl();
+ return context.data?.config || null;
+}
\ No newline at end of file
diff --git a/plugins/analytics/ui/contexts/ProviderManagerContext.tsx b/plugins/analytics/ui/contexts/ProviderManagerContext.tsx
new file mode 100644
index 00000000..e2259538
--- /dev/null
+++ b/plugins/analytics/ui/contexts/ProviderManagerContext.tsx
@@ -0,0 +1,56 @@
+import React, { createContext, useContext, useMemo } from 'react';
+import { useApiPolling } from '../hooks/useApiPolling';
+import type { Provider } from '../types/dashboard';
+
+// Define the context shape
+interface ProviderManagerContextType {
+ providers: Provider[] | null;
+ loading: boolean;
+ error: string | null;
+ refetch: () => void;
+ lastUpdated: number | null;
+}
+
+// Create the context with default values
+const ProviderManagerContext = createContext({
+ providers: null,
+ loading: false,
+ error: null,
+ refetch: () => {},
+ lastUpdated: null,
+});
+
+// Custom hook to use the provider manager context
+export const useProviderManager = () => {
+ const context = useContext(ProviderManagerContext);
+ if (!context) {
+ throw new Error('useProviderManager must be used within a ProviderManagerProvider');
+ }
+ return context;
+};
+
+// Provider component that fetches and provides provider data
+export const ProviderManagerProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ // Use the generic polling hook to fetch provider data
+ const { data: providers, loading, error, refetch, lastUpdated } = useApiPolling({
+ endpoint: '/api/v1/mission-control/provider-health',
+ interval: 10000, // 10 seconds
+ initialLoad: true,
+ retryCount: 3,
+ });
+
+ // Memoize the context value to prevent unnecessary re-renders
+ const contextValue = useMemo(() => ({
+ providers,
+ loading,
+ error,
+ refetch,
+ lastUpdated,
+ }), [providers, loading, error, refetch, lastUpdated]);
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/plugins/analytics/ui/hooks/useApiPolling.ts b/plugins/analytics/ui/hooks/useApiPolling.ts
new file mode 100644
index 00000000..8feebb73
--- /dev/null
+++ b/plugins/analytics/ui/hooks/useApiPolling.ts
@@ -0,0 +1,131 @@
+import { useState, useEffect, useCallback, useRef } from 'react';
+
+interface UseApiPollingOptions {
+ interval?: number; // milliseconds for polling
+ initialLoad?: boolean;
+ retryCount?: number;
+ endpoint: string;
+}
+
+interface UseApiPollingReturn {
+ data: T | null;
+ loading: boolean;
+ error: string | null;
+ refetch: () => Promise;
+ lastUpdated: number | null;
+}
+
+const DEFAULT_OPTIONS = {
+ interval: 5000, // 5 seconds for polling
+ initialLoad: true,
+ retryCount: 3,
+};
+
+export function useApiPolling(
+ options: UseApiPollingOptions
+): UseApiPollingReturn {
+ const { interval, initialLoad, retryCount, endpoint } = { ...DEFAULT_OPTIONS, ...options };
+
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [lastUpdated, setLastUpdated] = useState(null);
+ const retryCountRef = useRef(0);
+ const isMountedRef = useRef(true);
+ const abortControllerRef = useRef(null);
+
+ const fetchData = useCallback(async (isRetry = false, isInitial = false) => {
+ // Cancel any ongoing request
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+
+ // Create new abort controller for this request
+ abortControllerRef.current = new AbortController();
+ const signal = abortControllerRef.current.signal;
+
+ if (!isMountedRef.current) return;
+
+ // Only show loading for initial load or retries, not for periodic updates
+ if (isInitial || isRetry) {
+ setLoading(true);
+ }
+ if (!isRetry) {
+ setError(null);
+ }
+
+ try {
+ const response = await fetch(endpoint, { signal });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (!isMountedRef.current) return;
+
+ setData(result);
+ setLastUpdated(Date.now());
+ retryCountRef.current = 0; // Reset retry count on success
+
+ } catch (err: any) {
+ if (!isMountedRef.current) return;
+
+ // Don't set error state if the fetch was aborted
+ if (err.name === 'AbortError') {
+ console.log('Fetch aborted');
+ return;
+ }
+
+ console.error(`Error fetching data from ${endpoint}:`, err);
+
+ if (retryCountRef.current < (retryCount || DEFAULT_OPTIONS.retryCount)) {
+ retryCountRef.current++;
+ console.log(`Retrying... (${retryCountRef.current}/${retryCount || DEFAULT_OPTIONS.retryCount})`);
+ setTimeout(() => fetchData(true, false), 1000 * retryCountRef.current);
+ } else {
+ setError(err.message || `Failed to fetch data from ${endpoint}`);
+ setData(null);
+ }
+ } finally {
+ if (isMountedRef.current && (isInitial || isRetry)) {
+ setLoading(false);
+ }
+ }
+ }, [endpoint, retryCount]);
+
+ const refetch = useCallback(() => {
+ retryCountRef.current = 0;
+ return fetchData(false, true); // Manual refetch should show loading
+ }, [fetchData]);
+
+ useEffect(() => {
+ isMountedRef.current = true;
+ if (initialLoad) {
+ fetchData(false, true); // Initial load should show loading
+ }
+
+ const intervalId = setInterval(() => {
+ fetchData(false, false); // Periodic updates should NOT show loading
+ }, interval);
+
+ return () => {
+ isMountedRef.current = false;
+ clearInterval(intervalId);
+
+ // Abort any ongoing request when component unmounts
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+ };
+ }, [initialLoad, interval, fetchData]);
+
+ return {
+ data,
+ loading,
+ error,
+ refetch,
+ lastUpdated,
+ };
+}
\ No newline at end of file
diff --git a/plugins/analytics/ui/hooks/useMissionControl.ts b/plugins/analytics/ui/hooks/useMissionControl.ts
new file mode 100644
index 00000000..2fb15ab9
--- /dev/null
+++ b/plugins/analytics/ui/hooks/useMissionControl.ts
@@ -0,0 +1,154 @@
+import { useState, useEffect } from 'react';
+
+// Define types for the data we expect
+export interface RouteConfig {
+ enabled: boolean;
+ provider: string;
+ model: string;
+ description: string;
+}
+
+export interface RouterModels {
+ default: RouteConfig;
+ background: RouteConfig;
+ think: RouteConfig;
+ longContext: RouteConfig;
+ webSearch: RouteConfig;
+}
+
+interface RouteStat {
+ totalRequests: number;
+ successfulRequests: number;
+ avgResponseTime: number;
+}
+
+interface RouteStats {
+ [routeName: string]: RouteStat;
+}
+
+export interface Activity {
+ id: string;
+ type: 'success' | 'error' | 'warning';
+ message: string;
+ timestamp: string;
+ model: string;
+ provider: string;
+ responseTime: number;
+ tokens: number;
+ route?: string;
+ originalModel?: string;
+ actualModel?: string;
+}
+
+async function fetchData(url: string): Promise {
+ console.log(`🔄 Fetching data from: ${url}`);
+ const response = await fetch(url);
+ console.log(`📊 Response status for ${url}:`, response.status);
+
+ if (!response.ok) {
+ console.error(`❌ Fetch failed for ${url}:`, response.status, response.statusText);
+ throw new Error(`Failed to fetch ${url}`);
+ }
+
+ const result = await response.json();
+ console.log(`📦 Raw response for ${url}:`, result);
+
+ // API /api/config returnează direct datele, nu cu wrapper success/data
+ if (url === '/api/config') {
+ console.log(`✅ Using direct response for ${url}`);
+ return result;
+ }
+
+ // API /api/v1/mission-control/live-activity returnează {success: true, data: Array}
+ if (url === '/api/v1/mission-control/live-activity') {
+ console.log(`✅ Extracting data array from response for ${url}`);
+ return result.data || [];
+ }
+
+ if (!result.success) {
+ console.error(`❌ API error for ${url}:`, result.error);
+ throw new Error(result.error || `API error for ${url}`);
+ }
+
+ console.log(`✅ Success response for ${url}:`, result.data);
+ return result.data;
+}
+
+export function useMissionControl(refreshTrigger?: any) {
+ const [routerConfig, setRouterConfig] = useState(null);
+ const [routeStats, setRouteStats] = useState(null);
+ const [liveActivity, setLiveActivity] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ async function loadAllData() {
+ try {
+ console.log('🚀 Starting to load Mission Control data...');
+ setLoading(true);
+ // Folosim endpoint-uri valide
+ const [config, stats, activity] = await Promise.all([
+ fetchData('/api/config'),
+ fetchData('/api/v1/mission-control/route-stats'),
+ fetchData('/api/v1/mission-control/live-activity')
+ ]);
+
+ console.log('📋 Config loaded:', config);
+ console.log('📊 Stats loaded:', stats);
+ console.log('🔄 Activity loaded:', activity);
+
+ // Transformăm config-ul pentru a se potrivi cu tipul RouterModels
+ // Configurația router-ului este în config.Router, format: "provider,model"
+ const routerConfig: RouterModels = {
+ default: {
+ enabled: true, // Sunt activate by default
+ provider: config.Router.default?.split(',')[0] || '',
+ model: config.Router.default?.split(',')[1] || '',
+ description: 'Model principal pentru sarcini generale'
+ },
+ background: {
+ enabled: true, // Sunt activate by default
+ provider: config.Router.background?.split(',')[0] || '',
+ model: config.Router.background?.split(',')[1] || '',
+ description: 'Pentru sarcini în fundal'
+ },
+ think: {
+ enabled: true, // Sunt activate by default
+ provider: config.Router.think?.split(',')[0] || '',
+ model: config.Router.think?.split(',')[1] || '',
+ description: 'Pentru raționament și planificare'
+ },
+ longContext: {
+ enabled: true, // Sunt activate by default
+ provider: config.Router.longContext?.split(',')[0] || '',
+ model: config.Router.longContext?.split(',')[1] || '',
+ description: 'Pentru contexte lungi'
+ },
+ webSearch: {
+ enabled: true, // Sunt activate by default
+ provider: config.Router.webSearch?.split(',')[0] || '',
+ model: config.Router.webSearch?.split(',')[1] || '',
+ description: 'Pentru căutări web'
+ }
+ };
+
+ console.log('🎯 Final routerConfig:', routerConfig);
+ setRouterConfig(routerConfig);
+ setRouteStats(stats);
+ setLiveActivity(activity);
+ console.log('✅ Mission Control data loaded successfully!');
+ } catch (err: any) {
+ console.error('💥 Error loading Mission Control data:', err);
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ loadAllData();
+ const intervalId = setInterval(loadAllData, 30000); // Refresh every 30s
+ return () => clearInterval(intervalId);
+ }, [refreshTrigger]);
+
+ return { routerConfig, routeStats, liveActivity, loading, error };
+}
diff --git a/plugins/analytics/ui/hooks/useMissionControlData.ts b/plugins/analytics/ui/hooks/useMissionControlData.ts
new file mode 100644
index 00000000..ca2810db
--- /dev/null
+++ b/plugins/analytics/ui/hooks/useMissionControlData.ts
@@ -0,0 +1,316 @@
+import { useState, useEffect, useCallback, useRef } from 'react';
+import type { MissionControlData, RawMissionControlData } from '@plugins/analytics/ui/types/missionControl';
+import { missionControlAPI } from '@plugins/analytics/ui/lib/missionControlAPI';
+
+interface UseMissionControlDataOptions {
+ interval?: number; // milliseconds for polling
+ initialLoad?: boolean;
+ retryCount?: number;
+}
+
+export interface UseMissionControlDataReturn {
+ data: MissionControlData | null;
+ loading: boolean;
+ error: string | null;
+ refetch: () => Promise;
+ lastUpdated: number | null;
+}
+
+// Hook pentru istoricul providerilor
+export interface ProviderHistoryData {
+ provider: string;
+ timestamp: string;
+ successRate: number;
+ avgResponseTime: number;
+ errorRate: number;
+ totalRequests: number;
+}
+
+export interface UseProviderHistoryReturn {
+ data: ProviderHistoryData[] | null;
+ loading: boolean;
+ error: string | null;
+ refetch: () => Promise;
+ lastUpdated: number | null;
+}
+
+const DEFAULT_OPTIONS: Required = {
+ interval: 5000, // 5 seconds for polling
+ initialLoad: true,
+ retryCount: 3,
+};
+
+export function useMissionControlData(
+ options: UseMissionControlDataOptions = DEFAULT_OPTIONS
+): UseMissionControlDataReturn {
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [lastUpdated, setLastUpdated] = useState(null);
+ const retryCountRef = useRef(0);
+ const retryTimeoutRef = useRef(null); // Adăugați un ref pentru timer
+ const isMountedRef = useRef(true);
+
+ const { interval, initialLoad, retryCount } = { ...DEFAULT_OPTIONS, ...options };
+
+ const fetchData = useCallback(async (isRetry = false, isInitial = false) => {
+ if (!isMountedRef.current) return;
+
+ // Only show loading for initial load or retries, not for periodic updates
+ if (isInitial || isRetry) {
+ setLoading(true);
+ }
+ if (!isRetry) {
+ setError(null);
+ }
+
+ try {
+ const result = await missionControlAPI.getMissionControlStats();
+
+ if (!isMountedRef.current) return;
+
+ const normalizedData = normalizeMissionControlData(result);
+ setData(normalizedData);
+ setLastUpdated(Date.now());
+ retryCountRef.current = 0; // Reset retry count on success
+
+ } catch (err: any) {
+ if (!isMountedRef.current) return;
+
+ console.error('Error fetching mission control data:', err);
+
+ if (retryCountRef.current < retryCount) {
+ retryCountRef.current++;
+ console.log(`Retrying... (${retryCountRef.current}/${retryCount})`);
+ // Stocați ID-ul timer-ului
+ retryTimeoutRef.current = setTimeout(() => fetchData(true, false), 1000 * retryCountRef.current);
+ } else {
+ setError(err.message || 'Failed to fetch mission control data');
+ setData(null);
+ }
+ } finally {
+ if (isMountedRef.current && (isInitial || isRetry)) {
+ setLoading(false);
+ }
+ }
+ }, [retryCount]);
+
+ const refetch = useCallback(() => {
+ retryCountRef.current = 0;
+ return fetchData(false, true); // Manual refetch should show loading
+ }, [fetchData]);
+
+ useEffect(() => {
+ isMountedRef.current = true;
+ if (initialLoad) {
+ fetchData(false, true); // Initial load should show loading
+ }
+
+ const intervalId = setInterval(() => {
+ fetchData(false, false); // Periodic updates should NOT show loading
+ }, interval);
+
+ return () => {
+ isMountedRef.current = false;
+ clearInterval(intervalId);
+ // Anulați timer-ul la unmount
+ if (retryTimeoutRef.current) {
+ clearTimeout(retryTimeoutRef.current);
+ }
+ };
+ }, [initialLoad, interval, fetchData]);
+
+ return {
+ data,
+ loading,
+ error,
+ refetch,
+ lastUpdated,
+ };
+}
+
+// Normalize mission control data to ensure all properties exist
+function normalizeMissionControlData(data: RawMissionControlData): MissionControlData {
+ return {
+ live: {
+ deduplication: {
+ totalCachedRequests: data.live?.deduplication?.totalCachedRequests || 0,
+ totalDuplicateRequestsBlocked: data.live?.deduplication?.totalDuplicateRequestsBlocked || 0,
+ cacheHitRate: data.live?.deduplication?.cacheHitRate || 0,
+ memoryUsage: data.live?.deduplication?.memoryUsage || 0,
+ },
+ rateLimiting: {
+ circuitBreakerState: data.live?.rateLimiting?.circuitBreakerState || 'CLOSED',
+ totalRequestsTracked: data.live?.rateLimiting?.totalRequestsTracked || 0,
+ rulesUsage: data.live?.rateLimiting?.rulesUsage || {},
+ },
+ queue: {
+ currentSize: data.live?.queue?.currentSize || 0,
+ totalProcessed: data.live?.queue?.totalProcessed || 0,
+ averageWaitTime: data.live?.queue?.averageWaitTime || 0,
+ processing: data.live?.queue?.processing || false,
+ },
+ retry: {
+ totalRetries: data.live?.retry?.totalRetries || 0,
+ successAfterRetry: data.live?.retry?.successAfterRetry || 0,
+ finalFailures: data.live?.retry?.finalFailures || 0,
+ },
+ providers: data.live?.providers || {},
+ },
+ aggregated: {
+ modelStats: data.aggregated?.modelStats || [],
+ totalRequests: data.aggregated?.totalRequests || 0,
+ successRate: data.aggregated?.successRate || 0,
+ avgResponseTime: data.aggregated?.avgResponseTime || 0,
+ },
+ historical: data.historical || [],
+ config: {
+ routes: data.config?.routes || [],
+ executionGuard: {
+ enabled: data.config?.executionGuard?.enabled || false,
+ presets: {
+ economy: {
+ minDelayMs: data.config?.executionGuard?.presets?.economy?.minDelayMs || 1500,
+ initialBackoffMs: data.config?.executionGuard?.presets?.economy?.initialBackoffMs || 3000,
+ maxQueueSize: data.config?.executionGuard?.presets?.economy?.maxQueueSize || 100,
+ maxRetries: data.config?.executionGuard?.presets?.economy?.maxRetries || 3,
+ },
+ balanced: {
+ minDelayMs: data.config?.executionGuard?.presets?.balanced?.minDelayMs || 500,
+ initialBackoffMs: data.config?.executionGuard?.presets?.balanced?.initialBackoffMs || 1000,
+ maxQueueSize: data.config?.executionGuard?.presets?.balanced?.maxQueueSize || 200,
+ maxRetries: data.config?.executionGuard?.presets?.balanced?.maxRetries || 5,
+ },
+ highThroughput: {
+ minDelayMs: data.config?.executionGuard?.presets?.highThroughput?.minDelayMs || 200,
+ initialBackoffMs: data.config?.executionGuard?.presets?.highThroughput?.initialBackoffMs || 500,
+ maxQueueSize: data.config?.executionGuard?.presets?.highThroughput?.maxQueueSize || 500,
+ maxRetries: data.config?.executionGuard?.presets?.highThroughput?.maxRetries || 2,
+ },
+ },
+ current: {
+ minDelayMs: data.config?.executionGuard?.current?.minDelayMs || 500,
+ initialBackoffMs: data.config?.executionGuard?.current?.initialBackoffMs || 1000,
+ maxQueueSize: data.config?.executionGuard?.current?.maxQueueSize || 200,
+ maxRetries: data.config?.executionGuard?.current?.maxRetries || 5,
+ active: data.config?.executionGuard?.current?.active || false,
+ },
+ },
+ },
+ timestamp: data.timestamp || new Date().toISOString(),
+ historicalProviders: data.historicalProviders || [],
+ };
+}
+
+// Hook for real-time data only (more aggressive polling)
+export function useRealTimeMissionControl() {
+ return useMissionControlData({
+ interval: 2000, // 2 seconds for real-time
+ initialLoad: true,
+ retryCount: 2,
+ });
+}
+
+// Hook for aggregated data (less frequent polling)
+export function useAggregatedMissionControl() {
+ return useMissionControlData({
+ interval: 30000, // 30 seconds for aggregated data
+ initialLoad: true,
+ retryCount: 1,
+ });
+}
+
+// Hook pentru istoricul providerilor
+export function useProviderHistory(
+ options: UseMissionControlDataOptions = DEFAULT_OPTIONS
+): UseProviderHistoryReturn {
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [lastUpdated, setLastUpdated] = useState(null);
+ const retryCountRef = useRef(0);
+ const retryTimeoutRef = useRef(null);
+ const isMountedRef = useRef(true);
+
+ const { interval, initialLoad, retryCount } = { ...DEFAULT_OPTIONS, ...options };
+
+ const fetchData = useCallback(async (isRetry = false, isInitial = false) => {
+ if (!isMountedRef.current) return;
+
+ // Only show loading for initial load or retries, not for periodic updates
+ if (isInitial || isRetry) {
+ setLoading(true);
+ }
+ if (!isRetry) {
+ setError(null);
+ }
+
+ try {
+ // Fetch real data from the API
+ const response = await fetch('/api/v1/mission-control/provider-health-history');
+ const result = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.error || 'Failed to fetch provider history data');
+ }
+
+ if (!isMountedRef.current) return;
+
+ // Ensure data is always an array to prevent .map errors
+ const providerHistory = Array.isArray(result.data) ? result.data : [];
+ setData(providerHistory);
+ setLastUpdated(Date.now());
+ retryCountRef.current = 0; // Reset retry count on success
+
+ } catch (err: any) {
+ if (!isMountedRef.current) return;
+
+ console.error('Error fetching provider history data:', err);
+
+ if (retryCountRef.current < retryCount) {
+ retryCountRef.current++;
+ console.log(`Retrying... (${retryCountRef.current}/${retryCount})`);
+ retryTimeoutRef.current = setTimeout(() => fetchData(true, false), 1000 * retryCountRef.current);
+ } else {
+ setError(err.message || 'Failed to fetch provider history data');
+ setData(null);
+ }
+ } finally {
+ if (isMountedRef.current && (isInitial || isRetry)) {
+ setLoading(false);
+ }
+ }
+ }, [retryCount]);
+
+ const refetch = useCallback(() => {
+ retryCountRef.current = 0;
+ return fetchData(false, true); // Manual refetch should show loading
+ }, [fetchData]);
+
+ useEffect(() => {
+ isMountedRef.current = true;
+ if (initialLoad) {
+ fetchData(false, true); // Initial load should show loading
+ }
+
+ const intervalId = setInterval(() => {
+ fetchData(false, false); // Periodic updates should NOT show loading
+ }, interval);
+
+ return () => {
+ isMountedRef.current = false;
+ clearInterval(intervalId);
+ if (retryTimeoutRef.current) {
+ clearTimeout(retryTimeoutRef.current);
+ }
+ };
+ }, [initialLoad, interval, fetchData]);
+
+ return {
+ data,
+ loading,
+ error,
+ refetch,
+ lastUpdated,
+ };
+}
\ No newline at end of file
diff --git a/plugins/analytics/ui/hooks/useProviderManager.ts b/plugins/analytics/ui/hooks/useProviderManager.ts
new file mode 100644
index 00000000..ec7d3d8c
--- /dev/null
+++ b/plugins/analytics/ui/hooks/useProviderManager.ts
@@ -0,0 +1,119 @@
+import { useState, useEffect, useCallback } from 'react';
+import type { Provider } from '@plugins/analytics/ui/types/dashboard';
+import { missionControlAPI } from '@plugins/analytics/ui/lib/missionControlAPI';
+
+interface UseProviderManagerOptions {
+ interval?: number; // milliseconds for polling
+ initialLoad?: boolean;
+ retryCount?: number;
+}
+
+interface UseProviderManagerReturn {
+ providers: Provider[] | null;
+ loading: boolean;
+ error: string | null;
+ refetch: () => Promise;
+ lastUpdated: number | null;
+}
+
+const DEFAULT_OPTIONS: Required = {
+ interval: 10000, // 10 seconds for polling
+ initialLoad: true,
+ retryCount: 3,
+};
+
+export function useProviderManager(
+ options: UseProviderManagerOptions = DEFAULT_OPTIONS
+): UseProviderManagerReturn {
+ const [providers, setProviders] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [lastUpdated, setLastUpdated] = useState(null);
+ const retryCountRef = useState(0)[0];
+ const isMountedRef = useState(true)[0];
+
+ const { interval, initialLoad, retryCount } = { ...DEFAULT_OPTIONS, ...options };
+
+ const fetchData = useCallback(async (isRetry = false, isInitial = false) => {
+ if (!isMountedRef) return;
+
+ // Only show loading for initial load or retries, not for periodic updates
+ if (isInitial || isRetry) {
+ setLoading(true);
+ }
+ if (!isRetry) {
+ setError(null);
+ }
+
+ try {
+ const response = await missionControlAPI.getProviderHealth();
+
+ if (!isMountedRef) return;
+
+ if (response.success) {
+ // Ensure response.data is an array before mapping
+ const safeResponseData = Array.isArray(response.data) ? response.data : [];
+ // Transform the API response to match our Provider type
+ const transformedProviders: Provider[] = safeResponseData.map((provider: any) => ({
+ id: provider.id,
+ name: provider.name,
+ status: provider.status,
+ uptime: provider.healthScore || 99.9,
+ responseTime: provider.avgResponseTime || 0,
+ lastCheck: new Date(provider.lastCheck),
+ outages: provider.errorRate ? Math.round(provider.errorRate * 100) : 0,
+ modelOverrides: [] // In a real implementation, this would come from the API
+ }));
+
+ setProviders(transformedProviders);
+ setLastUpdated(Date.now());
+ } else {
+ throw new Error('Failed to fetch provider health data');
+ }
+
+ } catch (err: any) {
+ if (!isMountedRef) return;
+
+ console.error('Error fetching provider data:', err);
+
+ if (retryCountRef < retryCount) {
+ // We would increment retryCountRef here in a real implementation
+ console.log(`Retrying... (${retryCountRef + 1}/${retryCount})`);
+ setTimeout(() => fetchData(true, false), 1000 * (retryCountRef + 1));
+ } else {
+ setError(err.message || 'Failed to fetch provider data');
+ setProviders([]);
+ }
+ } finally {
+ if (isMountedRef && (isInitial || isRetry)) {
+ setLoading(false);
+ }
+ }
+ }, [retryCount]);
+
+ const refetch = useCallback(() => {
+ return fetchData(false, true); // Manual refetch should show loading
+ }, [fetchData]);
+
+ useEffect(() => {
+ if (initialLoad) {
+ fetchData(false, true); // Initial load should show loading
+ }
+
+ const intervalId = setInterval(() => {
+ fetchData(false, false); // Periodic updates should NOT show loading
+ }, interval);
+
+ return () => {
+ clearInterval(intervalId);
+ };
+ }, [initialLoad, interval, fetchData]);
+
+ return {
+ providers,
+ loading,
+ error,
+ refetch,
+ lastUpdated,
+ };
+}
\ No newline at end of file
diff --git a/plugins/analytics/ui/lib/analytics.ts b/plugins/analytics/ui/lib/analytics.ts
new file mode 100644
index 00000000..67622488
--- /dev/null
+++ b/plugins/analytics/ui/lib/analytics.ts
@@ -0,0 +1,432 @@
+import api from '@/lib/api';
+
+export interface RealtimeStats {
+ current: {
+ activeRequests: number;
+ avgResponseTime: number;
+ errorRate: number;
+ };
+ last1h: {
+ totalRequests: number;
+ avgResponseTime: number;
+ errorRate: number;
+ topModels: Array<{ model: string; count: number }>;
+ };
+ last24h: {
+ totalRequests: number;
+ avgResponseTime: number;
+ errorRate: number;
+ topModels: Array<{ model: string; count: number }>;
+ };
+}
+
+export interface ModelStats {
+ model: string;
+ provider: string;
+ totalRequests: number;
+ successfulRequests: number;
+ failedRequests: number;
+ avgResponseTime: number;
+ totalTokens: number;
+ totalCost: number;
+ lastUsed: number;
+ errorRate: number;
+ popularityScore: number;
+}
+
+export interface RequestLog {
+ id: string;
+ timestamp: number;
+ model: string;
+ provider: string;
+ endpoint: string;
+ method: string;
+ statusCode: number;
+ responseTime: number;
+ tokenCount?: number;
+ inputTokens?: number;
+ outputTokens?: number;
+ cost?: number;
+ userAgent?: string;
+ ipAddress?: string;
+ error?: string;
+ timeAgo: string;
+}
+
+export interface TimeSeriesData {
+ timestamp: number;
+ time: string;
+ requests: number;
+ successRate: number;
+ avgResponseTime: number;
+ errors: number;
+}
+
+export interface ModelComparison {
+ name: string;
+ provider: string;
+ requests: number;
+ successRate: string;
+ avgResponseTime: number;
+ totalTokens: number;
+ totalCost: string;
+ lastUsed: number;
+ popularity: number;
+ errorRate: string;
+}
+
+export interface CostAnalytics {
+ breakdown: Array<{
+ model: string;
+ provider: string;
+ totalCost: number;
+ costPerRequest: number;
+ totalTokens: number;
+ costPerToken: number;
+ }>;
+ summary: {
+ totalCost: string;
+ totalTokens: number;
+ totalRequests: number;
+ avgCostPerRequest: string;
+ avgCostPerToken: string;
+ };
+}
+
+class AnalyticsAPI {
+ // Get real-time statistics
+ async getRealtimeStats(): Promise {
+ const response = await fetch('/api/analytics/realtime');
+ const result = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.error || 'Failed to fetch realtime stats');
+ }
+
+ return result.data;
+ }
+
+ // Get model statistics
+ async getModelStats(): Promise {
+ const response = await fetch('/api/analytics/models');
+ const result = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.error || 'Failed to fetch model stats');
+ }
+
+ return result.data;
+ }
+
+ // Get recent requests
+ async getRecentRequests(limit: number = 50): Promise {
+ const response = await fetch(`/api/analytics/requests?limit=${limit}`);
+ const result = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.error || 'Failed to fetch recent requests');
+ }
+
+ return result.data;
+ }
+
+ // Get time-series data for charts
+ async getTimeSeriesData(hours: number = 24): Promise {
+ const response = await fetch(`/api/analytics/timeseries?hours=${hours}`);
+ const result = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.error || 'Failed to fetch time series data');
+ }
+
+ return result.data;
+ }
+
+ // Get model comparison data
+ async getModelComparison(): Promise {
+ const response = await fetch('/api/analytics/comparison');
+ const result = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.error || 'Failed to fetch comparison data');
+ }
+
+ return result.data;
+ }
+
+ // Get top performing models
+ async getTopModels(metric: string = 'requests', limit: number = 10): Promise> {
+ const response = await fetch(`/api/analytics/top-models?metric=${metric}&limit=${limit}`);
+ const result = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.error || 'Failed to fetch top models');
+ }
+
+ return result.data;
+ }
+
+ // Get cost analytics
+ async getCostAnalytics(): Promise {
+ const response = await fetch('/api/analytics/costs');
+ const result = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.error || 'Failed to fetch cost analytics');
+ }
+
+ return result.data;
+ }
+
+ // Get usage analytics by period
+ async getUsageAnalytics(period: 'hour' | 'day' | 'week' | 'month'): Promise {
+ const response = await fetch(`/api/analytics/usage/${period}`);
+ const result = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.error || 'Failed to fetch usage analytics');
+ }
+
+ return result.data;
+ }
+
+ // Helper to format currency
+ formatCurrency(amount: number): string {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ minimumFractionDigits: 4
+ }).format(amount);
+ }
+
+ // Helper to format numbers
+ formatNumber(num: number): string {
+ if (num >= 1e6) {
+ return (num / 1e6).toFixed(1) + 'M';
+ } else if (num >= 1e3) {
+ return (num / 1e3).toFixed(1) + 'K';
+ }
+ return num.toString();
+ }
+
+ // Helper to format response time
+ formatResponseTime(ms: number): string {
+ if (ms >= 1000) {
+ return (ms / 1000).toFixed(1) + 's';
+ }
+ return ms.toFixed(0) + 'ms';
+ }
+
+ // Helper to format time ago
+ formatTimeAgo(timestamp: number): string {
+ const diff = Date.now() - timestamp;
+ const minutes = Math.floor(diff / (60 * 1000));
+ const hours = Math.floor(diff / (60 * 60 * 1000));
+ const days = Math.floor(diff / (24 * 60 * 60 * 1000));
+
+ if (days > 0) return `${days}d ago`;
+ if (hours > 0) return `${hours}h ago`;
+ if (minutes > 0) return `${minutes}m ago`;
+ return 'Just now';
+ }
+
+ // Real-time data subscription (using polling for now)
+ subscribeToRealtimeStats(callback: (stats: RealtimeStats) => void, interval: number = 5000): () => void {
+ const intervalId = setInterval(async () => {
+ try {
+ const stats = await this.getRealtimeStats();
+ callback(stats);
+ } catch (error) {
+ console.error('Failed to fetch realtime stats:', error);
+ }
+ }, interval);
+
+ // Return cleanup function
+ return () => clearInterval(intervalId);
+ }
+}
+
+// Export singleton instance
+export const analyticsAPI = new AnalyticsAPI();
+
+// =================================================================
+// PLUGIN UI INTEGRATION - Independent Analytics Button System
+// =================================================================
+
+// Only run in browser environment
+if (typeof window !== 'undefined' && typeof document !== 'undefined') {
+ console.log('📊 Analytics plugin: UI integration script loaded');
+
+ /**
+ * Add analytics button to topbar
+ */
+ function addAnalyticsButtonToTopbar(): void {
+ console.log('🔍 Analytics plugin: Attempting to add button to topbar');
+
+ // Check if button already exists
+ const existingButton = document.querySelector('[data-analytics-plugin-button="true"]');
+ console.log('🔍 Analytics plugin: Existing button found:', existingButton);
+ if (existingButton) {
+ console.log('🔍 Analytics plugin: Button already exists in topbar');
+ return;
+ }
+
+ // Find the topbar header
+ const header = document.querySelector('header');
+ console.log('🔍 Analytics plugin: Header element found:', header);
+ if (!header) {
+ console.warn('Could not find header element for analytics button');
+ return;
+ }
+
+ // Find header actions container
+ const headerActions = header.querySelector('.flex.items-center.gap-2');
+ console.log('🔍 Analytics plugin: Header actions container found:', headerActions);
+ if (!headerActions) {
+ console.warn('Could not find header actions container');
+ return;
+ }
+
+ console.log('🔍 Analytics plugin: Creating analytics button');
+
+ // Create analytics button
+ const analyticsButton = document.createElement('button');
+ analyticsButton.className = 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-9 w-9 px-0 relative group ml-2';
+ analyticsButton.setAttribute('data-analytics-plugin-button', 'true');
+ analyticsButton.setAttribute('title', 'Analytics Dashboard');
+ analyticsButton.innerHTML = `
+
+
+ `;
+
+ // Add click handler to navigate to analytics
+ analyticsButton.addEventListener('click', () => {
+ // Try to find and activate Mission Control tab
+ const missionControlTab = document.querySelector('[data-tab="mission-control"]');
+ if (missionControlTab) {
+ (missionControlTab as HTMLElement).click();
+ } else {
+ // Fallback: dispatch custom event for navigation
+ const event = new CustomEvent('navigate-to-analytics', {
+ detail: { tab: 'analytics' }
+ });
+ document.dispatchEvent(event);
+ }
+
+ console.log('📊 Analytics button clicked - navigating to Mission Control');
+ });
+
+ console.log('🔍 Analytics plugin: Inserting button into header');
+
+ // Insert button in header actions (before settings button)
+ if (headerActions) {
+ headerActions.insertBefore(analyticsButton, headerActions.firstChild);
+ console.log('✅ Analytics plugin: Button added to topbar');
+ } else {
+ console.warn('Could not find header actions container for analytics button');
+ }
+ }
+
+ /**
+ * Remove analytics button from topbar
+ */
+ function removeAnalyticsButtonFromTopbar(): void {
+ const analyticsButton = document.querySelector('[data-analytics-plugin-button="true"]');
+ if (analyticsButton) {
+ analyticsButton.remove();
+ console.log('🗑️ Analytics plugin: Button removed from topbar');
+ }
+ }
+
+ /**
+ * Check if analytics should be active based on config
+ */
+ function isAnalyticsActive(): boolean {
+ console.log('🔍 Analytics plugin: Checking if analytics is active');
+
+ // Check localStorage (set by SettingsDialog)
+ const localStorageValue = localStorage.getItem('analytics-enabled');
+ console.log('🔍 Analytics plugin: localStorage analytics-enabled:', localStorageValue);
+ if (localStorageValue === 'true') {
+ console.log('🔍 Analytics plugin: Analytics enabled via localStorage');
+ return true;
+ }
+
+ // Check config from server
+ try {
+ const configStr = localStorage.getItem('app-config') || '{}';
+ console.log('🔍 Analytics plugin: app-config from localStorage:', configStr);
+ const config = JSON.parse(configStr);
+ const isEnabled = config.plugins?.analytics?.enabled === true;
+ console.log('🔍 Analytics plugin: Analytics enabled via app-config:', isEnabled);
+ return isEnabled;
+ } catch (e) {
+ console.log('🔍 Analytics plugin: Error parsing app-config:', e);
+ return false;
+ }
+ }
+
+ /**
+ * Initialize analytics button based on current state
+ */
+ function initializeAnalyticsButton(): void {
+ const isActive = isAnalyticsActive();
+ console.log('🔍 Analytics plugin: Initializing button, analytics active:', isActive);
+
+ if (isActive) {
+ console.log('🔍 Analytics plugin: Adding button to topbar');
+ addAnalyticsButtonToTopbar();
+ } else {
+ console.log('🔍 Analytics plugin: Removing button from topbar');
+ removeAnalyticsButtonFromTopbar();
+ }
+ }
+
+ // Initialize on DOM ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initializeAnalyticsButton);
+ } else {
+ initializeAnalyticsButton();
+ }
+
+ // Listen for config changes (SettingsDialog saves to localStorage)
+ window.addEventListener('storage', (event) => {
+ console.log('🔄 Analytics plugin: Received storage event:', event.key, event.newValue);
+ if (event.key === 'analytics-enabled' || event.key === 'app-config') {
+ console.log('🔄 Analytics plugin: Config changed via storage event, updating button visibility');
+ setTimeout(initializeAnalyticsButton, 100); // Small delay to ensure localStorage is updated
+ }
+ });
+
+ // Also listen for custom events from SettingsDialog
+ window.addEventListener('analytics-toggle-changed', () => {
+ console.log('🔄 Analytics plugin: Received toggle change event, updating button visibility');
+ setTimeout(initializeAnalyticsButton, 100);
+ });
+
+ // Also listen for custom events from SettingsDialog
+ window.addEventListener('analytics-config-changed', () => {
+ console.log('🔄 Analytics plugin: Received config change event, updating button visibility');
+ setTimeout(initializeAnalyticsButton, 100);
+ });
+
+ console.log('📊 Analytics plugin UI integration initialized');
+
+ // Debug: Check initial state
+ console.log('🔍 Analytics plugin debug - Initial state:', {
+ localStorageAnalytics: localStorage.getItem('analytics-enabled'),
+ localStorageConfig: localStorage.getItem('app-config'),
+ isAnalyticsActive: isAnalyticsActive()
+ });
+}
diff --git a/plugins/analytics/ui/lib/missionControlAPI.ts b/plugins/analytics/ui/lib/missionControlAPI.ts
new file mode 100644
index 00000000..2f022b61
--- /dev/null
+++ b/plugins/analytics/ui/lib/missionControlAPI.ts
@@ -0,0 +1,229 @@
+import { api } from '@/lib/api';
+
+export interface CircuitBreakerResetResponse {
+ success: boolean;
+ message: string;
+ timestamp: string;
+}
+
+export interface ExecutionGuardUpdateResponse {
+ success: boolean;
+ message: string;
+ timestamp: string;
+ newConfig: any;
+}
+
+export interface RouteConfigUpdateResponse {
+ success: boolean;
+ message: string;
+ timestamp: string;
+ oldRoute: string;
+ newModel: string;
+}
+
+export interface ProviderTestResponse {
+ success: boolean;
+ message: string;
+ status: 'online' | 'offline' | 'unknown';
+ responseTime?: number;
+ timestamp: string;
+}
+
+export interface ProviderHealthResponse {
+ success: boolean;
+ data: ProviderHealthStatus[];
+ timestamp: string;
+}
+
+export interface HealthHistoryResponse {
+ success: boolean;
+ data: HealthHistoryData[];
+ timestamp: string;
+}
+
+export interface ProviderHealthStatus {
+ id: string;
+ name: string;
+ provider: string;
+ status: 'online' | 'offline' | 'degraded';
+ healthScore: number;
+ avgResponseTime: number;
+ errorRate: number;
+ totalRequests: number;
+ lastCheck: string;
+ features: string[];
+}
+
+export interface HealthHistoryData {
+ provider: string;
+ timestamp: string;
+ successRate: number;
+ avgResponseTime: number;
+ errorCount: number;
+ errorRate?: number;
+ totalRequests?: number;
+ hourlyStats?: any[];
+}
+
+/**
+ * Mission Control API Client
+ * Provides specialized API methods for the Mission Control dashboard
+ */
+class MissionControlAPI {
+ private baseUrl: string = '/api';
+
+ constructor(baseUrl?: string) {
+ if (baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+ }
+
+ /**
+ * Get provider health status
+ */
+ async getProviderHealth(): Promise {
+ return api.get('/v1/mission-control/provider-health');
+ }
+
+ /**
+ * Get provider health history
+ */
+ async getProviderHealthHistory(): Promise {
+ return api.get('/v1/mission-control/provider-health-history');
+ }
+
+ /**
+ * Reset the circuit breaker manually
+ */
+ async resetCircuitBreaker(): Promise {
+ return api.post('/v1/mission-control/reset-circuit-breaker', {});
+ }
+
+ /**
+ * Update ExecutionGuard configuration with preset
+ */
+ async updateExecutionGuardPreset(
+ preset: 'economy' | 'balanced' | 'high-throughput'
+ ): Promise {
+ return api.post('/v1/mission-control/update-execution-guard', {
+ action: 'update-preset',
+ preset,
+ });
+ }
+
+ /**
+ * Update ExecutionGuard with custom configuration
+ */
+ async updateExecutionGuardCustom(
+ config: Partial<{
+ minDelayMs: number;
+ initialBackoffMs: number;
+ maxQueueSize: number;
+ maxRetries: number;
+ }>
+ ): Promise {
+ return api.post('/v1/mission-control/update-execution-guard', {
+ action: 'update-custom',
+ config,
+ });
+ }
+
+ /**
+ * Update route configuration (model assignment)
+ */
+ async updateRouteConfig(
+ routeName: string,
+ newModel: string
+ ): Promise {
+ return api.post('/v1/mission-control/update-route', {
+ routeName,
+ newModel,
+ });
+ }
+
+ /**
+ * Test a provider connectivity
+ */
+ async testProvider(provider: string): Promise {
+ return api.post('/v1/mission-control/test-provider', {
+ provider,
+ testAction: 'ping',
+ });
+ }
+
+ /**
+ * Get aggregated analytics data
+ */
+ async getAggregatedData(): Promise {
+ return api.get('/v1/mission-control/aggregated-data');
+ }
+
+ /**
+ * Get live activity feed
+ */
+ async getLiveActivity(): Promise {
+ return api.get('/v1/mission-control/live-activity');
+ }
+
+ /**
+ * Get system health status
+ */
+ async getSystemHealth(): Promise {
+ return api.get('/v1/mission-control/system-health');
+ }
+
+ /**
+ * Get threat matrix data
+ */
+ async getThreatMatrix(): Promise {
+ return api.get('/v1/mission-control/threat-matrix');
+ }
+
+ /**
+ * Get route efficiency data
+ */
+ async getRouteEfficiency(): Promise {
+ return api.get('/v1/mission-control/route-efficiency');
+ }
+
+ /**
+ * Get model performance leaderboard
+ */
+ async getModelPerformance(): Promise {
+ return api.get('/v1/mission-control/model-performance');
+ }
+
+ /**
+ * Get historical performance data
+ */
+ async getHistoricalPerformance(): Promise {
+ return api.get('/v1/mission-control/historical-performance');
+ }
+
+ /**
+ * Get ALL mission control data in a single unified endpoint
+ */
+ async getMissionControlStats(): Promise {
+ return api.get('/v1/mission-control/stats');
+ }
+
+ /**
+ * Emergency stop all operations
+ */
+ async emergencyStop(): Promise<{ success: boolean; message: string }> {
+ return api.post<{ success: boolean; message: string }>('/v1/mission-control/emergency-stop', {});
+ }
+
+ /**
+ * Resume operations after emergency stop
+ */
+ async resumeOperations(): Promise<{ success: boolean; message: string }> {
+ return api.post<{ success: boolean; message: string }>('/v1/mission-control/resume', {});
+ }
+}
+
+// Export singleton instance
+export const missionControlAPI = new MissionControlAPI();
+
+// Export class for custom instances if needed
+export { MissionControlAPI };
\ No newline at end of file
diff --git a/plugins/analytics/ui/missionControlNavigator.ts b/plugins/analytics/ui/missionControlNavigator.ts
new file mode 100644
index 00000000..9b21f33d
--- /dev/null
+++ b/plugins/analytics/ui/missionControlNavigator.ts
@@ -0,0 +1,88 @@
+interface MissionControlNavigatorConfig {
+ targetElementId?: string;
+ renderInModal?: boolean;
+ replaceMainContent?: boolean;
+}
+
+class MissionControlNavigator {
+ private initialized = false;
+ private config: MissionControlNavigatorConfig;
+
+ constructor(config: MissionControlNavigatorConfig = {}) {
+ this.config = {
+ targetElementId: 'root',
+ renderInModal: false,
+ replaceMainContent: true,
+ ...config
+ };
+ }
+
+ async initialize(): Promise {
+ if (this.initialized) return;
+
+ try {
+ console.log('[MissionControlNavigator] Initializing simple event-based navigation...');
+
+ // Listen for analytics events
+ document.addEventListener('open-mission-control', this.handleOpenMissionControl.bind(this));
+ document.addEventListener('close-mission-control', this.handleCloseMissionControl.bind(this));
+
+ this.initialized = true;
+ console.log('[MissionControlNavigator] Initialized successfully');
+ } catch (error) {
+ console.error('[MissionControlNavigator] Failed to initialize:', error);
+ }
+ }
+
+ private async handleOpenMissionControl(): Promise {
+ console.log('[MissionControlNavigator] Opening Mission Control - App.tsx will handle tab switching');
+ // App.tsx will handle the tab switching via event listener
+ }
+
+ private async handleCloseMissionControl(): Promise {
+ console.log('[MissionControlNavigator] Closing Mission Control - App.tsx will handle tab switching');
+ // App.tsx will handle the tab switching via event listener
+ }
+
+ public destroy(): void {
+ document.removeEventListener('open-mission-control', this.handleOpenMissionControl.bind(this));
+ document.removeEventListener('close-mission-control', this.handleCloseMissionControl.bind(this));
+ this.initialized = false;
+ }
+}
+
+// Initialize the navigator when the module is loaded
+let missionControlNavigator: MissionControlNavigator | null = null;
+
+// Export initialization function
+export const initializeMissionControlNavigator = (config?: MissionControlNavigatorConfig) => {
+ if (!missionControlNavigator) {
+ missionControlNavigator = new MissionControlNavigator(config);
+ }
+ return missionControlNavigator.initialize();
+};
+
+// Export destroy function
+export const destroyMissionControlNavigator = () => {
+ if (missionControlNavigator) {
+ missionControlNavigator.destroy();
+ missionControlNavigator = null;
+ }
+};
+
+// Auto-initialize if running in browser
+if (typeof window !== 'undefined') {
+ document.addEventListener('DOMContentLoaded', () => {
+ console.log('[MissionControlNavigator] Auto-initializing...');
+ initializeMissionControlNavigator();
+ });
+
+ // Also initialize immediately if DOM is already loaded
+ if (document.readyState === 'loading') {
+ // DOM is still loading
+ } else {
+ // DOM is already loaded
+ console.log('[MissionControlNavigator] DOM already loaded, initializing immediately...');
+ initializeMissionControlNavigator();
+ }
+}
diff --git a/plugins/analytics/ui/types/dashboard.ts b/plugins/analytics/ui/types/dashboard.ts
new file mode 100644
index 00000000..d16be4b7
--- /dev/null
+++ b/plugins/analytics/ui/types/dashboard.ts
@@ -0,0 +1,78 @@
+// Shared types for dashboard widgets and hooks
+export interface Provider {
+ id: string;
+ name: string;
+ status: 'online' | 'offline' | 'degraded';
+ uptime: number; // percentage
+ responseTime: number; // ms
+ lastCheck: Date;
+ outages: number;
+ modelOverrides: ModelOverride[];
+}
+
+export interface ModelOverride {
+ route: string;
+ configuredModel: string;
+ actualModel: string;
+ timestamp: Date;
+}
+
+export interface CostOptimization {
+ totalSavings: number;
+ recommendations: OptimizationRecommendation[];
+ currentMonthlyCost: number;
+ projectedMonthlyCost: number;
+}
+
+export interface OptimizationRecommendation {
+ id: string;
+ title: string;
+ description: string;
+ savings: number;
+ action: 'auto-apply' | 'manual' | 'settings-change';
+ status: 'pending' | 'applied' | 'dismissed';
+}
+
+export interface PerformanceAlert {
+ id: string;
+ severity: 'critical' | 'warning' | 'info';
+ title: string;
+ description: string;
+ action: string;
+ impact: string;
+ timestamp: Date;
+ resolved: boolean;
+}
+
+export interface RouteUsage {
+ route: string; // 'default', 'background', 'think', 'webSearch', 'longContext'
+ requests: number;
+ configuredModel: string;
+ actualModel: string;
+ cost: number;
+ avgResponseTime: number;
+ successRate: number;
+ recentLogs: ActivityLog[];
+ status: 'active' | 'warning' | 'error';
+}
+
+export interface ActivityLog {
+ id: string;
+ timestamp: Date;
+ model: string;
+ provider: string;
+ route: string;
+ status: 'success' | 'error' | 'retrying' | 'cached';
+ latency: number;
+ statusCode?: number;
+}
+
+export interface SessionStats {
+ totalRequests: number;
+ totalCost: number;
+ avgResponseTime: number;
+ mostUsedRoute: string;
+ modelOverrides: number;
+ fallbacks: number;
+ sessionStart: Date;
+}
\ No newline at end of file
diff --git a/plugins/analytics/ui/types/missionControl.ts b/plugins/analytics/ui/types/missionControl.ts
new file mode 100644
index 00000000..f0fb9d6e
--- /dev/null
+++ b/plugins/analytics/ui/types/missionControl.ts
@@ -0,0 +1,127 @@
+// Types for Mission Control v2
+export interface ProviderStats {
+ failureCount: number;
+ inRecovery: boolean;
+ [key: string]: any; // Permite alte proprietăți care ar putea exista
+}
+
+export interface ExecutionStats {
+ deduplication: {
+ totalCachedRequests: number;
+ totalDuplicateRequestsBlocked: number;
+ cacheHitRate: number;
+ memoryUsage: number;
+ };
+ rateLimiting: {
+ circuitBreakerState: 'CLOSED' | 'HALF_OPEN' | 'OPEN';
+ totalRequestsTracked: number;
+ rulesUsage: Record;
+ };
+ queue: {
+ currentSize: number;
+ totalProcessed: number;
+ averageWaitTime: number;
+ processing: boolean;
+ };
+ retry: {
+ totalRetries: number;
+ successAfterRetry: number;
+ finalFailures: number;
+ };
+ providers: Record;
+}
+
+export interface ModelStat {
+ model: string;
+ provider: string;
+ totalRequests: number;
+ successfulRequests: number;
+ failedRequests: number;
+ avgResponseTime: number;
+ totalTokens: number;
+ totalCost: number;
+ lastUsed: number;
+ errorRate: number;
+ popularityScore: number;
+}
+
+export interface AggregatedData {
+ modelStats: ModelStat[];
+ totalRequests: number;
+ successRate: number;
+ avgResponseTime: number;
+}
+
+export interface HistoricalDataPoint {
+ timestamp: number;
+ time: string;
+ requests: number;
+ successRate: number;
+ avgResponseTime: number;
+ errors: number;
+}
+
+export interface HealthHistoryData {
+ provider: string;
+ timestamp: string;
+ successRate: number;
+ avgResponseTime: number;
+ errorCount: number;
+ errorRate?: number;
+ totalRequests?: number;
+ hourlyStats?: any[];
+}
+
+export interface MissionControlConfig {
+ routes: Record | string[];
+ executionGuard: {
+ enabled: boolean;
+ presets: {
+ economy: {
+ minDelayMs: number;
+ initialBackoffMs: number;
+ maxQueueSize: number;
+ maxRetries: number;
+ };
+ balanced: {
+ minDelayMs: number;
+ initialBackoffMs: number;
+ maxQueueSize: number;
+ maxRetries: number;
+ };
+ highThroughput: {
+ minDelayMs: number;
+ initialBackoffMs: number;
+ maxQueueSize: number;
+ maxRetries: number;
+ };
+ };
+ current: {
+ minDelayMs: number;
+ initialBackoffMs: number;
+ maxQueueSize: number;
+ maxRetries: number;
+ active: boolean;
+ };
+ };
+}
+
+export interface RawMissionControlData {
+ live?: Partial;
+ aggregated?: Partial;
+ historical?: HistoricalDataPoint[];
+ config?: Partial;
+ timestamp?: string;
+ historicalProviders?: HealthHistoryData[];
+}
+
+export interface MissionControlData {
+ live: ExecutionStats;
+ aggregated: AggregatedData;
+ historical: HistoricalDataPoint[];
+ config: MissionControlConfig;
+ timestamp: string;
+ historicalProviders?: HealthHistoryData[]; // Added historicalProviders property
+}
+
+export type SystemPreset = 'economy' | 'balanced' | 'high-throughput';
\ No newline at end of file
diff --git a/plugins/core/PluginManager.tsx b/plugins/core/PluginManager.tsx
new file mode 100644
index 00000000..3f65645a
--- /dev/null
+++ b/plugins/core/PluginManager.tsx
@@ -0,0 +1,216 @@
+// Plugin Manager - STANDALONE în plugins/ folder
+// Zero dependențe externe, auto-contained
+import React, { useState, useEffect } from 'react';
+import { themesPlugin } from '../themes/index';
+
+interface Plugin {
+ id: string;
+ name: string;
+ description: string;
+ enabled: boolean;
+ version: string;
+ component?: React.ComponentType;
+}
+
+interface PluginManagerProps {
+ className?: string;
+}
+
+// TOATE funcționalitățile concentrate aici - zero dependențe externe
+export const PluginManager: React.FC = ({ className = "" }) => {
+ const [plugins, setPlugins] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ // Auto-discovery plugins din folderul plugins/
+ useEffect(() => {
+ const initializePlugins = async () => {
+ setLoading(true);
+ try {
+ // 1. Fetch the source of truth for state from the server
+ const response = await fetch('/api/plugins/getState');
+ if (!response.ok) {
+ throw new Error(`Failed to fetch plugin state: ${response.statusText}`);
+ }
+ const serverState = await response.json();
+
+ // 2. Discover available plugins by trying to import them
+ const discoveredPlugins: Plugin[] = [];
+
+ // Analytics Plugin
+ try {
+ const { AnalyticsSettings } = await import('../analytics/ui/AnalyticsSettings');
+ discoveredPlugins.push({
+ id: 'analytics',
+ name: 'Analytics & Mission Control',
+ description: 'Real-time analytics and Mission Control dashboard',
+ // 3. Combine discovered plugin with server state
+ enabled: serverState.analytics?.enabled ?? false,
+ version: '1.0.0',
+ component: AnalyticsSettings
+ });
+ } catch (error) {
+ // Plugin not found, do nothing
+ }
+
+ // Themes Plugin
+ try {
+ const { ThemeSettings } = await import('../themes/ui/ThemeSettings');
+ discoveredPlugins.push({
+ id: 'themes',
+ name: 'Advanced Themes',
+ description: 'Glassmorphism effects and premium theming',
+ // 3. Combine discovered plugin with server state
+ enabled: serverState.themes?.enabled ?? false,
+ version: '1.0.0',
+ component: ThemeSettings
+ });
+ } catch (error) {
+ // Plugin not found, do nothing
+ }
+
+ setPlugins(discoveredPlugins);
+ } catch (error) {
+ console.error('Failed to initialize plugins:', error);
+ setPlugins([]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ initializePlugins();
+ }, []);
+
+ useEffect(() => {
+ const analyticsPlugin = plugins.find(p => p.id === 'analytics');
+ if (analyticsPlugin?.enabled) {
+ document.documentElement.setAttribute('data-analytics', 'enabled');
+ } else {
+ document.documentElement.removeAttribute('data-analytics');
+ }
+ }, [plugins]);
+
+ const togglePlugin = async (pluginId: string) => {
+ const plugin = plugins.find(p => p.id === pluginId);
+ if (!plugin) return;
+
+ const newEnabled = !plugin.enabled;
+
+ if (newEnabled && pluginId === 'analytics') {
+ document.documentElement.setAttribute('data-analytics', 'enabled');
+ } else if (!newEnabled) {
+ if (pluginId === 'themes') {
+ // FIXED: Properly disable themes plugin by updating its config
+ themesPlugin.setConfig({ enabled: false });
+ themesPlugin.cleanup();
+ } else if (pluginId === 'analytics') {
+ document.documentElement.removeAttribute('data-analytics');
+ }
+ }
+
+ // Update server config
+ try {
+ await fetch('/api/plugins/setState', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ pluginId, enabled: newEnabled })
+ });
+
+ // Dispatch event pentru cross-tab sync
+ window.dispatchEvent(new CustomEvent(`plugin-${pluginId}-toggled`, {
+ detail: { enabled: newEnabled }
+ }));
+
+ // FIXED: Dispatch backwards-compatible events for analytics plugin
+ if (pluginId === 'analytics') {
+ window.dispatchEvent(new CustomEvent('analytics-toggle-changed', {
+ detail: { enabled: newEnabled }
+ }));
+ window.dispatchEvent(new CustomEvent('analytics-config-changed', {
+ detail: { enabled: newEnabled }
+ }));
+
+ // Sync to localStorage for compatibility
+ if (newEnabled) {
+ localStorage.setItem('analytics-enabled', 'true');
+ } else {
+ localStorage.removeItem('analytics-enabled');
+ }
+ }
+
+ // Update local state
+ setPlugins(prev => prev.map(p =>
+ p.id === pluginId ? { ...p, enabled: newEnabled } : p
+ ));
+
+ } catch (error) {
+ console.error('Failed to update plugin state:', error);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+
Discovering plugins...
+
+
+ );
+ }
+
+ if (plugins.length === 0) {
+ return (
+
+
🔌
+
No plugins available
+
Place plugins in the plugins/ directory
+
+ );
+ }
+
+ return (
+
+
+
🔌 Plugin Management
+ {plugins.length} plugins
+
+
+ {plugins.map((plugin) => (
+
+ {/* Plugin Card */}
+
+
+
+
{plugin.name}
+
v{plugin.version}
+
+
+
{plugin.description}
+
+
+
+
+ {/* Plugin Settings - doar dacă e enabled */}
+ {plugin.enabled && plugin.component && (
+
+ )}
+
+ ))}
+
+ );
+};
+
+export default PluginManager;
\ No newline at end of file
diff --git a/plugins/core/stateManager.ts b/plugins/core/stateManager.ts
new file mode 100644
index 00000000..d79f00dc
--- /dev/null
+++ b/plugins/core/stateManager.ts
@@ -0,0 +1,65 @@
+import * as fs from 'fs/promises';
+import * as path from 'path';
+import { homedir } from 'os';
+
+// In-memory state for plugins
+let pluginState: { [key: string]: any } = {};
+
+const stateFilePath = path.join(homedir(), '.claude-code-router', 'runtime-plugin-state.json');
+
+export const savePluginState = async () => {
+ try {
+ await fs.mkdir(path.dirname(stateFilePath), { recursive: true });
+ await fs.writeFile(stateFilePath, JSON.stringify(pluginState, null, 2));
+ console.log('Plugin state saved successfully.');
+ } catch (error) {
+ console.error('Failed to save plugin state:', error);
+ }
+};
+
+export const loadPluginState = async () => {
+ try {
+ const data = await fs.readFile(stateFilePath, 'utf8');
+ const persistedState = JSON.parse(data);
+ console.log('LOADED PERSISTED STATE:', JSON.stringify(persistedState, null, 2));
+ pluginState = { ...pluginState, ...persistedState };
+ console.log('MERGED STATE AFTER LOAD:', JSON.stringify(pluginState, null, 2));
+ console.log('Plugin state loaded successfully.');
+ } catch (error: any) {
+ if (error.code === 'ENOENT') {
+ console.log('Plugin state file not found, state remains from initial config.');
+ } else {
+ console.error('Failed to load plugin state:', error);
+ }
+ }
+};
+
+export const initStateManager = async (app: any, initialPluginConfig: any) => {
+ console.log('Initializing Plugin State Manager...');
+ // 1. Start with the base config from config.json
+ pluginState = initialPluginConfig || {};
+ console.log('INITIAL CONFIG STATE:', JSON.stringify(pluginState, null, 2));
+
+ // 2. Load persisted state from runtime file, which will override the base config
+ await loadPluginState();
+
+ // 3. Decorate the app with the final, merged state
+ console.log('FINAL STATE BEFORE DECORATING:', JSON.stringify(pluginState, null, 2));
+ app.decorate('pluginState', pluginState);
+
+ app.get('/api/plugins/getState', async (request: any, reply: any) => {
+ console.log('GET /api/plugins/getState called. Returning:', JSON.stringify(pluginState, null, 2));
+ reply.send(pluginState);
+ });
+
+ app.post('/api/plugins/setState', async (request: any, reply: any) => {
+ const { pluginId, enabled } = request.body;
+ if (typeof pluginId !== 'string' || typeof enabled !== 'boolean') {
+ return reply.code(400).send({ error: 'Invalid payload. pluginId (string) and enabled (boolean) are required.' });
+ }
+
+ pluginState[pluginId] = { enabled };
+ await savePluginState();
+ reply.send({ message: `Plugin ${pluginId} state updated to enabled: ${enabled}` });
+ });
+};
diff --git a/plugins/themes/README.md b/plugins/themes/README.md
new file mode 100644
index 00000000..6dc7b5ce
--- /dev/null
+++ b/plugins/themes/README.md
@@ -0,0 +1,471 @@
+# 🎨 Themes Plugin v2.0.0 - COMPLETE EDITION
+
+A **COMPREHENSIVE** theme system for claude-code-router with **FULL** advanced theme experience, including ALL glassmorphism components from advanced-theme-redesign branch.
+
+## ✨ Features - NOW COMPLETE!
+
+- **3 Unified Themes**: Light (professional), Dark (eye-friendly), Advanced (full glassmorphism)
+- **🚀 ALL ADVANCED COMPONENTS**: glass-card, nav-tab, stat-card, dashboard-bg, and 40+ more!
+- **Complete Component Coverage**: Every UI element styled consistently
+- **Plugin Architecture**: Easy to enable/disable, modular design
+- **Auto-persistence**: Theme preferences saved automatically
+- **System Theme Detection**: Optional auto-sync with OS theme
+- **CSS Variables**: Modern, performant styling system
+- **Accessibility**: Full support for high contrast and reduced motion
+- **Mobile Optimized**: Responsive design with touch-friendly interactions
+
+## 🎭 Available Themes - COMPLETE SET
+
+### 🌞 Light Theme
+- **Perfect for**: Daytime use, professional environments
+- **Colors**: Clean whites, subtle grays, blue accents
+- **Features**: High contrast, sharp borders, professional look
+
+### 🌙 Dark Theme
+- **Perfect for**: Night use, eye strain reduction
+- **Colors**: Deep backgrounds, light text, blue accents
+- **Features**: Easy on eyes, modern feel, accessibility focused
+
+### ⭐ Advanced Theme - **NOW WITH ALL COMPONENTS!**
+- **Perfect for**: Modern, premium experience
+- **Colors**: Space gradients, glassmorphism, neon accents
+- **Features**: **ALL 40+ glassmorphism components**, animations, blur effects
+- **Components**:
+ - `glass-card` - Main cards with blur effect
+ - `nav-tabs` & `nav-tab` - Navigation with glass styling
+ - `stat-card` - Statistics cards with hover effects
+ - `dashboard-bg` - Space gradient background
+ - `glass-input` - Form inputs with transparency
+ - `glass-button-primary/secondary` - Gradient buttons
+ - `glass-badge` - Status badges with blur
+ - `glass-dialog` - Modal dialogs with glassmorphism
+ - `service-status-card` - Service status indicators
+ - `quick-actions` - Action buttons grid
+ - `component-card` - Component containers
+ - `glass-list-item` - List items with hover effects
+ - `notification` - System notifications
+ - **And 25+ more components!**
+
+## 📦 Installation Complete
+
+### 1. Files Added:
+```
+plugins/themes/
+├── index.ts # Plugin API
+├── components/ThemeSelector.tsx # React selector
+├── contexts/ThemeContext.tsx # React context
+├── styles/
+│ ├── themes.css # Main CSS with imports
+│ ├── variables.css # CSS variables for all themes
+│ ├── components.css # Base component styles
+│ ├── advanced-complete.css # ALL advanced components
+│ └── advanced-reference.css # Reference from advanced-theme-redesign
+└── types/index.ts # TypeScript definitions
+```
+
+### 2. Enable in Configuration:
+
+```json
+{
+ "plugins": {
+ "themes": {
+ "enabled": true,
+ "activeTheme": "advanced",
+ "availableThemes": ["light", "dark", "advanced"],
+ "persistUserChoice": true,
+ "autoApplySystemTheme": false
+ }
+ }
+}
+```
+
+## 🎯 Complete Component List
+
+### Core Glass Components:
+- `glass-card` - Main container with blur
+- `glass-input` - Transparent form inputs
+- `glass-button-primary` - Gradient action buttons
+- `glass-button-secondary` - Secondary glass buttons
+- `glass-badge` - Status indicators
+- `glass-dialog` - Modal containers
+- `glass-select` - Dropdown selectors
+- `glass-list-item` - List items with effects
+
+### Navigation Components:
+- `nav-tabs` - Tab container with glass
+- `nav-tab` - Individual tab styling
+- `nav-tab.active` - Active tab highlighting
+
+### Dashboard Components:
+- `dashboard-bg` - Space gradient background
+- `stat-card` - Statistics display cards
+- `stats-grid` - Grid layout for stats
+- `service-status-card` - Service indicators
+- `quick-actions` - Action grid
+- `quick-action` - Individual actions
+
+### Advanced Features:
+- `component-card` - Generic containers
+- `component-header` - Card headers
+- `component-title` - Gradient titles
+- `notification` - Alert system
+- `loading-spinner` - Animated loaders
+- Custom scrollbars
+- Pulse animations
+- Fade transitions
+
+## ⚡ Usage Examples
+
+```jsx
+// Basic glass card
+
+
Dashboard
+
+ Content here...
+
+
+
+// Navigation tabs
+
+
+
+
+
+
+// Statistics grid
+
+
+
1,234
+
Total Requests
+
+
+
+// Form elements
+
+
+```
+
+## 🔧 Configuration Options
+
+```typescript
+interface ThemePluginConfig {
+ enabled: boolean; // Enable/disable plugin
+ activeTheme: 'light' | 'dark' | 'advanced';
+ availableThemes: ThemeType[]; // Which themes to show
+ persistUserChoice: boolean; // Save to localStorage
+ autoApplySystemTheme: boolean; // Follow OS theme
+}
+```
+
+## 📊 **Comparison with Advanced-Theme-Redesign:**
+
+| Feature | Plugin Version | Advanced-Theme-Redesign |
+|---------|---------------|-------------------------|
+| Light Theme | ✅ Complete | ✅ Complete |
+| Dark Theme | ✅ Complete | ✅ Complete |
+| Advanced Glassmorphism | ✅ **ALL 40+ Components** | ✅ Complete |
+| Glass Cards | ✅ Full Implementation | ✅ Original |
+| Navigation Tabs | ✅ Complete Styling | ✅ Original |
+| Statistics Cards | ✅ Full Effects | ✅ Original |
+| Dashboard Background | ✅ Space Gradients | ✅ Original |
+| Form Elements | ✅ All Glass Components | ✅ Original |
+| Animations | ✅ All Keyframes | ✅ Original |
+| CSS Variables | ✅ Complete Set | ✅ Original |
+
+## 🎉 **Result: 100% Feature Parity!**
+
+This plugin now provides **IDENTICAL** experience to the advanced-theme-redesign branch through a clean, modular plugin architecture. All glassmorphism effects, animations, and components are included!
+
+### 🌙 Dark Theme
+- **Perfect for**: Low-light environments, extended coding sessions
+- **Colors**: Deep grays, soft whites, gentle blue accents
+- **Features**: Easy on eyes, reduced strain, elegant appearance
+- **Best for**: Night work, focus sessions, creative tasks
+
+### ✨ Advanced Theme
+- **Perfect for**: Modern, stunning visual experience
+- **Colors**: Gradient backgrounds, glassmorphism effects
+- **Features**: Backdrop blur, animated elements, premium feel
+- **Best for**: Demos, showcases, impressive presentations
+
+## 📝 Configuration Options
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `enabled` | `boolean` | `true` | Enable/disable the themes plugin |
+| `activeTheme` | `'light' \| 'dark' \| 'advanced'` | `'light'` | Currently active theme |
+| `availableThemes` | `Array` | `['light', 'dark', 'advanced']` | Themes available for selection |
+| `persistUserChoice` | `boolean` | `true` | Save theme choice to localStorage |
+| `autoApplySystemTheme` | `boolean` | `false` | Auto-switch based on OS theme |
+
+### Example Configurations
+
+**Minimal Setup:**
+```json
+{
+ "plugins": {
+ "themes": {
+ "enabled": true
+ }
+ }
+}
+```
+
+**Dark Mode Only:**
+```json
+{
+ "plugins": {
+ "themes": {
+ "enabled": true,
+ "activeTheme": "dark",
+ "availableThemes": ["dark"]
+ }
+ }
+}
+```
+
+**Auto System Theme:**
+```json
+{
+ "plugins": {
+ "themes": {
+ "enabled": true,
+ "autoApplySystemTheme": true,
+ "availableThemes": ["light", "dark"]
+ }
+ }
+}
+```
+
+## 🛠️ Technical Details
+
+### Architecture
+
+```
+plugins/themes/
+├── index.ts # Main plugin entry & API
+├── types/index.ts # TypeScript definitions
+├── contexts/ThemeContext.tsx # React context & state
+├── components/ThemeSelector.tsx # UI selector component
+└── styles/
+ ├── variables.css # CSS custom properties
+ ├── components.css # Component-specific styles
+ └── themes.css # Main theme definitions
+```
+
+### CSS Variables System
+
+Each theme defines consistent variables:
+
+```css
+.theme-light {
+ --background: oklch(0.98 0 0);
+ --foreground: oklch(0.2 0 0);
+ --primary: oklch(0.5 0.15 220);
+ /* ... 30+ variables */
+}
+```
+
+### Component Coverage
+
+All UI elements are themed consistently:
+
+- ✅ **Buttons** (primary, secondary, ghost, destructive)
+- ✅ **Inputs** (text, select, textarea, focus states)
+- ✅ **Cards** (backgrounds, borders, shadows)
+- ✅ **Modals** (dialogs, popovers, overlays)
+- ✅ **Tables** (headers, rows, hover states)
+- ✅ **Badges** (success, error, warning, info, neutral)
+- ✅ **Loading** (spinners, skeleton loaders)
+- ✅ **Switches** (toggles, checkboxes)
+- ✅ **Notifications** (toasts, alerts)
+- ✅ **Scrollbars** (WebKit and Firefox)
+
+## 🔧 API Reference
+
+### Plugin API
+
+```typescript
+import { themesPlugin } from './plugins/themes';
+
+// Get current theme
+const currentTheme = themesPlugin.getCurrentTheme(); // 'light' | 'dark' | 'advanced'
+
+// Set theme
+themesPlugin.setTheme('dark');
+
+// Get configuration
+const config = themesPlugin.getConfig();
+
+// Update configuration
+themesPlugin.setConfig({
+ activeTheme: 'advanced',
+ persistUserChoice: true
+});
+```
+
+### React Hooks
+
+```typescript
+import { useTheme } from './plugins/themes/contexts/ThemeContext';
+import { useThemesPlugin } from './plugins/themes';
+
+function MyComponent() {
+ // Full context access
+ const { currentTheme, setTheme, themes, pluginConfig } = useTheme();
+
+ // Simple plugin access
+ const { setTheme, getCurrentTheme, isEnabled } = useThemesPlugin();
+
+ return (
+
+
Current theme: {currentTheme}
+
+
+ );
+}
+```
+
+## 🎨 Customization
+
+### Adding Custom Themes
+
+Extend the theme definitions in `contexts/ThemeContext.tsx`:
+
+```typescript
+const CUSTOM_THEME: ThemeDefinition = {
+ id: 'custom',
+ name: 'My Custom Theme',
+ description: 'A personalized theme',
+ colors: {
+ background: 'your-color',
+ foreground: 'your-color',
+ // ... define all required colors
+ },
+ features: {
+ glassmorphism: false,
+ animations: true,
+ gradients: false
+ }
+};
+```
+
+### CSS Overrides
+
+Create theme-specific overrides:
+
+```css
+.theme-advanced .my-component {
+ background: var(--gradient-primary);
+ backdrop-filter: var(--glass-blur);
+ border: var(--glass-border);
+}
+```
+
+## 🧪 Testing
+
+### Manual Testing Checklist
+
+- [ ] **Theme Switching**: All 3 themes switch correctly
+- [ ] **Persistence**: Theme choice survives page refresh
+- [ ] **Components**: All UI components display properly in each theme
+- [ ] **Animations**: Smooth transitions between themes
+- [ ] **Accessibility**: Focus states visible in all themes
+- [ ] **Mobile**: Responsive design on mobile devices
+- [ ] **Settings UI**: Plugin toggle works in SettingsDialog
+
+### Browser Compatibility
+
+- ✅ **Chrome** 90+ (full support)
+- ✅ **Firefox** 88+ (full support)
+- ✅ **Safari** 14+ (full support)
+- ✅ **Edge** 90+ (full support)
+
+## 🔄 Migration & Updates
+
+### From Existing Themes
+
+If you have custom theme modifications:
+
+1. **Backup** your existing theme files
+2. **Enable** the themes plugin
+3. **Configure** available themes in config.json
+4. **Test** each theme with your content
+5. **Customize** using CSS overrides if needed
+
+### Future Updates
+
+The plugin is designed for easy updates:
+
+- **CSS Variables**: Changes automatically apply to all components
+- **Modular Architecture**: Update individual components without affecting others
+- **Backward Compatibility**: Existing configurations continue to work
+
+### Update Steps
+
+1. **Backup** current plugin directory
+2. **Replace** plugin files with new version
+3. **Check** config.json for new options
+4. **Test** theme switching functionality
+5. **Apply** any custom overrides if needed
+
+## 🐛 Troubleshooting
+
+### Theme Not Applying
+
+**Problem**: Theme colors not showing
+**Solutions**:
+- Check if plugin is enabled in config.json
+- Verify CSS files are loading (check browser dev tools)
+- Ensure ThemeProvider wraps your components
+
+### Settings Not Saving
+
+**Problem**: Theme resets after refresh
+**Solutions**:
+- Verify `persistUserChoice: true` in config
+- Check browser localStorage permissions
+- Clear localStorage and try again
+
+### Performance Issues
+
+**Problem**: Slow theme switching
+**Solutions**:
+- Disable animations: `prefers-reduced-motion: reduce`
+- Use Light/Dark themes instead of Advanced
+- Check for conflicting CSS
+
+### Styling Conflicts
+
+**Problem**: Components not styled correctly
+**Solutions**:
+- Check CSS specificity conflicts
+- Verify CSS custom properties are defined
+- Use browser dev tools to inspect applied styles
+
+## 📞 Support
+
+- **Issues**: Report bugs and feature requests
+- **Documentation**: This README and inline code comments
+- **Examples**: Check `components/ThemeSelector.tsx` for usage patterns
+
+## 🎯 Roadmap
+
+### v1.1.0 (Planned)
+- [ ] Theme preview in settings
+- [ ] Custom theme creator UI
+- [ ] Import/export theme configurations
+- [ ] More animation options
+
+### v1.2.0 (Future)
+- [ ] Seasonal themes
+- [ ] Time-based theme switching
+- [ ] Component-specific theme overrides
+- [ ] Theme sharing system
+
+---
+
+**Built with ❤️ for claude-code-router**
+*Unified, beautiful, performant theme system*
\ No newline at end of file
diff --git a/plugins/themes/components/PluginThemeWrapper.tsx b/plugins/themes/components/PluginThemeWrapper.tsx
new file mode 100644
index 00000000..a3ed475a
--- /dev/null
+++ b/plugins/themes/components/PluginThemeWrapper.tsx
@@ -0,0 +1,65 @@
+import { useEffect, useState } from 'react';
+import type { ReactNode } from 'react';
+import { ThemeProvider } from '../contexts/ThemeContext';
+
+interface PluginThemeWrapperProps {
+ children: ReactNode;
+}
+
+/**
+ * Self-managing Theme Plugin Wrapper
+ * Only loads when theme plugin is enabled
+ */
+export function PluginThemeWrapper({ children }: PluginThemeWrapperProps) {
+ const [isEnabled, setIsEnabled] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const checkPluginStatus = async () => {
+ try {
+ // Check runtime plugin state first (authoritative during session)
+ const response = await fetch('/api/plugins/getState');
+ if (response.ok) {
+ const state = await response.json();
+ const enabled = state.themes?.enabled === true;
+ setIsEnabled(enabled);
+ } else {
+ // Fallback to localStorage
+ const enabled = localStorage.getItem('themes-enabled') === 'true';
+ setIsEnabled(enabled);
+ }
+ } catch (error) {
+ // Default disabled
+ setIsEnabled(false);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ checkPluginStatus();
+
+ // Listen for plugin state changes
+ const handlePluginChange = (event: CustomEvent) => {
+ const { enabled } = event.detail;
+ setIsEnabled(enabled);
+ };
+
+ // Listen for the correct event from PluginManager
+ window.addEventListener('plugin-themes-toggled', handlePluginChange as EventListener);
+ return () => window.removeEventListener('plugin-themes-toggled', handlePluginChange as EventListener);
+ }, []);
+
+ if (isLoading) {
+ return <>{children}>;
+ }
+
+ if (isEnabled) {
+ return {children};
+ }
+
+ // Plugin disabled - return children without wrapper
+ return <>{children}>;
+}
+
+// Export for external use when theme plugin is needed
+export { ThemeProvider } from '../contexts/ThemeContext';
diff --git a/plugins/themes/components/ThemeSelector.tsx b/plugins/themes/components/ThemeSelector.tsx
new file mode 100644
index 00000000..b62d410b
--- /dev/null
+++ b/plugins/themes/components/ThemeSelector.tsx
@@ -0,0 +1,171 @@
+import React from 'react';
+import { useTheme } from '../contexts/ThemeContext';
+import type { ThemeType } from '../types';
+
+interface ThemeOption {
+ id: ThemeType;
+ label: string;
+ description: string;
+ icon: string;
+ previewColors: {
+ primary: string;
+ secondary: string;
+ accent: string;
+ background: string;
+ border: string;
+ };
+}
+
+const THEME_OPTIONS: ThemeOption[] = [
+ {
+ id: 'light',
+ label: 'Light Theme',
+ description: 'Clean and professional light theme',
+ icon: '☀️',
+ previewColors: {
+ primary: '#e5e8eeff',
+ secondary: '#050c13ff',
+ accent: '#3b82f6',
+ background: '#ffffff',
+ border: '#e2e8f0'
+ }
+ },
+ {
+ id: 'dark',
+ label: 'Dark Theme',
+ description: 'Elegant and sophisticated dark experience',
+ icon: '🌙',
+ previewColors: {
+ primary: '#0b0c0cff',
+ secondary: '#eaeff5ff',
+ accent: '#60a5fa',
+ background: '#0f172a',
+ border: '#334155'
+ }
+ },
+ {
+ id: 'advanced',
+ label: 'Advanced Theme',
+ description: 'Spectacular glassmorphism space experience',
+ icon: '🚀',
+ previewColors: {
+ primary: 'rgba(255, 255, 255, 0.95)',
+ secondary: 'rgba(255, 255, 255, 0.25)',
+ accent: '#e94560',
+ background: 'rgba(45,45,75,0.95)',
+ border: 'rgba(255, 255, 255, 0.2)'
+ }
+ }
+];
+
+const ThemeSelector: React.FC = () => {
+ const { currentTheme, setTheme, isPluginEnabled } = useTheme();
+
+ const handleThemeChange = (themeId: ThemeType) => {
+ if (isPluginEnabled()) {
+ setTheme(themeId);
+ }
+ };
+
+ if (!isPluginEnabled()) {
+ return (
+
+ Enable Advanced Themes plugin to access theme options
+
+ );
+ }
+
+ return (
+
+
+
+
+ {THEME_OPTIONS.map((option) => (
+
+ ))}
+
+
+
+
+
+ Current: {currentTheme}
+
+
+ Glassmorphism effects available in Advanced themes.
+
+
+
+ );
+};
+
+export default ThemeSelector;
\ No newline at end of file
diff --git a/plugins/themes/components/ThemeSelectorSimple.tsx b/plugins/themes/components/ThemeSelectorSimple.tsx
new file mode 100644
index 00000000..41a09a36
--- /dev/null
+++ b/plugins/themes/components/ThemeSelectorSimple.tsx
@@ -0,0 +1,165 @@
+import React from 'react';
+import { useTheme } from '../contexts/ThemeContext';
+
+interface ThemeOption {
+ id: 'light' | 'dark' | 'advanced';
+ label: string;
+ description: string;
+ previewColors: {
+ primary: string;
+ secondary: string;
+ accent: string;
+ background: string;
+ border: string;
+ };
+}
+
+const themeOptions: ThemeOption[] = [
+ {
+ id: 'light',
+ label: 'Classic Light',
+ description: 'Clean and professional',
+ previewColors: {
+ primary: '#ffffff',
+ secondary: '#f3f4f6',
+ accent: '#3b82f6',
+ background: '#ffffff',
+ border: '#e5e7eb'
+ }
+ },
+ {
+ id: 'dark',
+ label: 'Classic Dark',
+ description: 'Dark and elegant',
+ previewColors: {
+ primary: '#1f2937',
+ secondary: '#374151',
+ accent: '#60a5fa',
+ background: '#111827',
+ border: '#4b5563'
+ }
+ },
+ {
+ id: 'advanced',
+ label: '🚀 Advanced Space',
+ description: 'Glassmorphism effects',
+ previewColors: {
+ primary: 'rgba(42, 42, 62, 0.9)',
+ secondary: 'rgba(38, 49, 78, 0.8)',
+ accent: 'rgba(31, 68, 112, 0.9)',
+ background: 'linear-gradient(135deg, #2a2a3e, #26314e, #1f4470)',
+ border: 'rgba(255, 255, 255, 0.2)'
+ }
+ }
+];
+
+const ThemeSelectorSimple: React.FC = () => {
+ const { currentTheme, setTheme, isPluginEnabled } = useTheme();
+
+ const handleThemeChange = (themeId: 'light' | 'dark' | 'advanced') => {
+ if (isPluginEnabled()) {
+ setTheme(themeId);
+ }
+ };
+
+ if (!isPluginEnabled()) {
+ return (
+
+ Enable Advanced Themes plugin to access theme options
+
+ );
+ }
+
+ return (
+
+
+
+
+ {themeOptions.map((option) => (
+
+ ))}
+
+
+
+
+
+ Current: {currentTheme}
+
+
+ Glassmorphism effects available in Advanced themes.
+
+
+
+ );
+};
+
+export default ThemeSelectorSimple;
diff --git a/plugins/themes/contexts/ThemeContext.tsx b/plugins/themes/contexts/ThemeContext.tsx
new file mode 100644
index 00000000..467925f0
--- /dev/null
+++ b/plugins/themes/contexts/ThemeContext.tsx
@@ -0,0 +1,528 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import type { ReactNode } from 'react';
+import type {
+ ThemeType,
+ ThemeDefinition,
+ ThemePluginConfig,
+ ThemeContextType,
+ ThemeColors
+} from '../types';
+
+// Predefined theme definitions
+const THEME_DEFINITIONS: Record = {
+ light: {
+ id: 'light',
+ name: 'Light Theme',
+ description: 'Clean and professional light theme',
+ colors: {
+ background: 'oklch(0.98 0 0)',
+ foreground: 'oklch(0.2 0 0)',
+ card: 'oklch(1 0 0)',
+ cardForeground: 'oklch(0.15 0 0)',
+ popover: 'oklch(1 0 0)',
+ popoverForeground: 'oklch(0.15 0 0)',
+ primary: 'oklch(0.5 0.15 220)',
+ primaryForeground: 'oklch(0.98 0 0)',
+ secondary: 'oklch(0.94 0 0)',
+ secondaryForeground: 'oklch(0.2 0 0)',
+ muted: 'oklch(0.94 0 0)',
+ mutedForeground: 'oklch(0.5 0 0)',
+ accent: 'oklch(0.92 0 0)',
+ accentForeground: 'oklch(0.2 0 0)',
+ destructive: 'oklch(0.65 0.2 25)',
+ destructiveForeground: 'oklch(0.98 0 0)',
+ border: 'oklch(0.9 0 0)',
+ input: 'oklch(0.92 0 0)',
+ ring: 'oklch(0.5 0.15 220)'
+ },
+ features: {
+ glassmorphism: false,
+ animations: true,
+ gradients: false
+ }
+ },
+
+ dark: {
+ id: 'dark',
+ name: 'Dark Theme',
+ description: 'Elegant and eye-friendly dark theme',
+ colors: {
+ background: 'oklch(0.12 0 0)',
+ foreground: 'oklch(0.9 0 0)',
+ card: 'oklch(0.18 0 0)',
+ cardForeground: 'oklch(0.9 0 0)',
+ popover: 'oklch(0.18 0 0)',
+ popoverForeground: 'oklch(0.9 0 0)',
+ primary: 'oklch(0.65 0.15 200)',
+ primaryForeground: 'oklch(0.12 0 0)',
+ secondary: 'oklch(0.25 0 0)',
+ secondaryForeground: 'oklch(0.9 0 0)',
+ muted: 'oklch(0.25 0 0)',
+ mutedForeground: 'oklch(0.65 0 0)',
+ accent: 'oklch(0.28 0 0)',
+ accentForeground: 'oklch(0.9 0 0)',
+ destructive: 'oklch(0.7 0.19 22)',
+ destructiveForeground: 'oklch(0.9 0 0)',
+ border: 'oklch(0.25 0 0)',
+ input: 'oklch(0.22 0 0)',
+ ring: 'oklch(0.65 0.15 200)'
+ },
+ features: {
+ glassmorphism: false,
+ animations: true,
+ gradients: false
+ }
+ },
+
+ advanced: {
+ id: 'advanced',
+ name: 'Advanced Theme',
+ description: 'Modern glassmorphism with gradients and effects',
+ colors: {
+ background: 'oklch(0.15 0.02 240)',
+ foreground: 'oklch(0.95 0 0)',
+ card: 'rgba(255, 255, 255, 0.1)',
+ cardForeground: 'oklch(0.95 0 0)',
+ popover: 'rgba(255, 255, 255, 0.15)',
+ popoverForeground: 'oklch(0.95 0 0)',
+ primary: 'oklch(0.75 0.2 340)',
+ primaryForeground: 'oklch(0.95 0 0)',
+ secondary: 'rgba(255, 255, 255, 0.2)',
+ secondaryForeground: 'oklch(0.95 0 0)',
+ muted: 'rgba(255, 255, 255, 0.1)',
+ mutedForeground: 'oklch(0.7 0 0)',
+ accent: 'rgba(255, 255, 255, 0.15)',
+ accentForeground: 'oklch(0.95 0 0)',
+ destructive: 'oklch(0.65 0.25 15)',
+ destructiveForeground: 'oklch(0.95 0 0)',
+ border: 'rgba(255, 255, 255, 0.2)',
+ input: 'rgba(255, 255, 255, 0.15)',
+ ring: 'oklch(0.75 0.2 340)',
+ gradient: 'linear-gradient(135deg, rgba(30,30,50,0.95), rgba(20,40,70,0.95))',
+ glassBg: 'rgba(255, 255, 255, 0.1)',
+ glassBlur: 'blur(20px)'
+ },
+ features: {
+ glassmorphism: true,
+ animations: true,
+ gradients: true
+ }
+ }
+};
+
+const DEFAULT_CONFIG: ThemePluginConfig = {
+ enabled: true,
+ activeTheme: 'light',
+ availableThemes: ['light', 'dark', 'advanced'],
+ persistUserChoice: true,
+ autoApplySystemTheme: false
+};
+
+const ThemeContext = createContext(undefined);
+
+export const ThemeProvider: React.FC<{
+ children: ReactNode;
+ initialConfig?: Partial;
+}> = ({ children, initialConfig }) => {
+ const [pluginConfig, setPluginConfig] = useState({
+ ...DEFAULT_CONFIG,
+ ...initialConfig
+ });
+
+ const [currentTheme, setCurrentTheme] = useState(pluginConfig.activeTheme);
+
+ // Load theme from config first, then localStorage
+ useEffect(() => {
+ if (initialConfig) {
+ setPluginConfig(prev => ({ ...prev, ...initialConfig }));
+ if (initialConfig.activeTheme) {
+ setCurrentTheme(initialConfig.activeTheme);
+ }
+ } else {
+ // Load configuration from runtime plugin state (not static config)
+ const loadServerConfig = async () => {
+ try {
+ const response = await fetch('/api/plugins/getState');
+ if (response.ok) {
+ const pluginState = await response.json();
+ if (pluginState.themes) {
+ setPluginConfig(prev => ({ ...prev, ...pluginState.themes }));
+ if (pluginState.themes.activeTheme) {
+ setCurrentTheme(pluginState.themes.activeTheme);
+ }
+ }
+ }
+ } catch (error) {
+ console.warn('Failed to load plugin state for themes:', error);
+ }
+ };
+ loadServerConfig();
+
+ // Set up polling to detect server state changes
+ const pollInterval = setInterval(async () => {
+ try {
+ const response = await fetch('/api/plugins/getState');
+ if (response.ok) {
+ const pluginState = await response.json();
+ const serverEnabled = pluginState.themes?.enabled ?? true;
+
+ if (serverEnabled !== pluginConfig.enabled) {
+ console.log(`🔄 Server state changed: themes enabled = ${serverEnabled}`);
+ setPluginConfig(prev => ({ ...prev, enabled: serverEnabled }));
+ }
+ }
+ } catch (error) {
+ console.warn('Failed to poll plugin state:', error);
+ }
+ }, 3000); // Poll every 3 seconds (increased from 2s to give more time for sync)
+
+ return () => clearInterval(pollInterval);
+ }
+ }, [initialConfig, pluginConfig.enabled]);
+
+ // Load theme from localStorage on mount (only if not overridden by config)
+ useEffect(() => {
+ if (pluginConfig.enabled && pluginConfig.persistUserChoice && !initialConfig?.activeTheme) {
+ try {
+ const savedTheme = localStorage.getItem('claude-router-theme-plugin');
+ if (savedTheme) {
+ const parsedData = JSON.parse(savedTheme);
+ if (parsedData.activeTheme && pluginConfig.availableThemes.includes(parsedData.activeTheme)) {
+ setCurrentTheme(parsedData.activeTheme);
+ }
+ if (parsedData.config) {
+ setPluginConfig(prev => ({ ...prev, ...parsedData.config }));
+ }
+ }
+ } catch (error) {
+ console.warn('Failed to load theme plugin data from localStorage:', error);
+ }
+ }
+ }, [initialConfig?.activeTheme]); // FIXED: Remove pluginConfig.persistUserChoice to prevent infinite loop
+
+ // Apply theme to document element
+ useEffect(() => {
+ const documentElement = document.documentElement;
+
+ console.log(`🔌 ThemeProvider useEffect triggered - pluginEnabled: ${pluginConfig.enabled}, currentTheme: ${currentTheme}`);
+
+ if (!pluginConfig.enabled) {
+ // COMPREHENSIVE CLEANUP when plugin is disabled
+ documentElement.classList.remove('theme-light', 'theme-dark', 'theme-advanced');
+ documentElement.classList.remove('themes-plugin-active');
+ documentElement.classList.remove('dark');
+
+ // Remove ALL theme CSS custom properties
+ const themeProperties = [
+ '--background', '--foreground', '--card', '--card-foreground',
+ '--popover', '--popover-foreground', '--primary', '--primary-foreground',
+ '--secondary', '--secondary-foreground', '--muted', '--muted-foreground',
+ '--accent', '--accent-foreground', '--destructive', '--destructive-foreground',
+ '--border', '--input', '--ring', '--gradient', '--glass-bg', '--glass-blur'
+ ];
+
+ themeProperties.forEach(property => {
+ documentElement.style.removeProperty(property);
+ });
+
+ // Remove plugin CSS files completely
+ const pluginStyles = document.querySelectorAll('link[data-themes-plugin="true"]');
+ pluginStyles.forEach(style => {
+ console.log(`🧹 Removing plugin CSS: ${style.getAttribute('href')}`);
+ style.remove();
+ });
+
+ // Reset to system default classes
+ const isSystemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+ if (isSystemDark) {
+ documentElement.classList.add('dark');
+ }
+
+ console.log('🔄 Themes plugin disabled - reverted to system defaults');
+ return;
+ }
+
+ // Load plugin CSS when enabled
+ const loadPluginCSS = () => {
+ const cssFiles = [
+ '/plugins/themes/styles/themes.css',
+ '/plugins/themes/styles/variables.css',
+ '/plugins/themes/styles/modern-effects.css',
+ '/plugins/themes/styles/components.css'
+ ];
+
+ cssFiles.forEach(cssFile => {
+ if (!document.querySelector(`link[href="${cssFile}"]`)) {
+ const link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = cssFile;
+ link.setAttribute('data-themes-plugin', 'true');
+ document.head.appendChild(link);
+ console.log(`✅ Loaded plugin CSS: ${cssFile}`);
+ }
+ });
+ };
+
+ // Load CSS first, then apply theme
+ loadPluginCSS();
+
+ const theme = THEME_DEFINITIONS[currentTheme];
+
+ // Remove all theme classes
+ documentElement.classList.remove('theme-light', 'theme-dark', 'theme-advanced');
+
+ // Add new theme class
+ documentElement.classList.add(`theme-${currentTheme}`);
+
+ // Add plugin active indicator for CSS layout override
+ documentElement.classList.add('themes-plugin-active');
+
+
+ // Apply layout classes for navigation
+ applyLayoutClasses();
+
+ // Unify with TailwindCSS dark mode
+ if (currentTheme === 'dark' || currentTheme === 'advanced') {
+ documentElement.classList.add('dark');
+ } else {
+ documentElement.classList.remove('dark');
+ }
+
+ // COMPREHENSIVE CLEANUP: Remove ALL possible theme properties first
+ const allThemeProperties = [
+ '--background', '--foreground', '--card', '--card-foreground',
+ '--popover', '--popover-foreground', '--primary', '--primary-foreground',
+ '--secondary', '--secondary-foreground', '--muted', '--muted-foreground',
+ '--accent', '--accent-foreground', '--destructive', '--destructive-foreground',
+ '--border', '--input', '--ring', '--gradient', '--glass-bg', '--glass-blur'
+ ];
+
+ allThemeProperties.forEach(property => {
+ documentElement.style.removeProperty(property);
+ });
+
+ console.log(`🧹 Cleared all theme properties before applying ${currentTheme} theme`);
+
+ // Apply CSS custom properties for current theme
+ Object.entries(theme.colors).forEach(([property, value]) => {
+ if (value && typeof value === 'string') {
+ const cssProperty = `--${property.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
+ documentElement.style.setProperty(cssProperty, value);
+ console.log(`✅ Applied ${cssProperty}: ${value}`);
+ }
+ });
+
+ // Save to localStorage
+ if (pluginConfig.persistUserChoice) {
+ try {
+ localStorage.setItem('claude-router-theme-plugin', JSON.stringify({
+ activeTheme: currentTheme,
+ config: pluginConfig
+ }));
+ } catch (error) {
+ console.warn('Failed to save theme plugin data to localStorage:', error);
+ }
+ }
+ }, [currentTheme, pluginConfig.enabled]); // FIXED: Remove full pluginConfig to prevent infinite loop
+
+ // Auto-apply system theme
+ useEffect(() => {
+ if (pluginConfig.autoApplySystemTheme && pluginConfig.enabled) {
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ const handleChange = (e: MediaQueryListEvent) => {
+ setCurrentTheme(e.matches ? 'dark' : 'light');
+ };
+
+ // Set initial theme
+ setCurrentTheme(mediaQuery.matches ? 'dark' : 'light');
+
+ // Listen for changes
+ mediaQuery.addEventListener('change', handleChange);
+ return () => mediaQuery.removeEventListener('change', handleChange);
+ }
+ }, [pluginConfig.autoApplySystemTheme, pluginConfig.enabled]);
+
+ // Listen for plugin state changes from PluginContext
+ useEffect(() => {
+ const handlePluginStateChange = (event: CustomEvent) => {
+ const { id, enabled } = event.detail;
+ if (id === 'themes') {
+ console.log('🔌 Received plugin state change from PluginContext:', enabled);
+ if (pluginConfig.enabled !== enabled) {
+ togglePlugin(enabled);
+ }
+ }
+ };
+
+ window.addEventListener('plugin-state-changed', handlePluginStateChange as EventListener);
+ return () => window.removeEventListener('plugin-state-changed', handlePluginStateChange as EventListener);
+ }, [pluginConfig.enabled]);
+
+ const setTheme = (theme: ThemeType) => {
+ if (pluginConfig.availableThemes.includes(theme)) {
+ setCurrentTheme(theme);
+ const newConfig = { ...pluginConfig, activeTheme: theme };
+ setPluginConfig(newConfig);
+
+ // Sync to server - fire and forget
+ syncConfigToServer(newConfig);
+ }
+ };
+
+ const isPluginEnabled = () => pluginConfig.enabled;
+
+ const togglePlugin = async (enabled: boolean) => {
+ const newConfig = { ...pluginConfig, enabled };
+ setPluginConfig(newConfig);
+
+ // Sync to server IMMEDIATELY to prevent race condition with polling
+ await syncConfigToServer(newConfig);
+
+ if (!enabled) {
+ // COMPREHENSIVE CLEANUP when disabled
+ const documentElement = document.documentElement;
+
+ // Remove all theme classes
+ documentElement.classList.remove('theme-light', 'theme-dark', 'theme-advanced');
+ documentElement.classList.remove('themes-plugin-active');
+ documentElement.classList.remove('dark');
+
+ // Remove ALL theme CSS custom properties
+ const themeProperties = [
+ '--background', '--foreground', '--card', '--card-foreground',
+ '--popover', '--popover-foreground', '--primary', '--primary-foreground',
+ '--secondary', '--secondary-foreground', '--muted', '--muted-foreground',
+ '--accent', '--accent-foreground', '--destructive', '--destructive-foreground',
+ '--border', '--input', '--ring', '--gradient', '--glass-bg', '--glass-blur'
+ ];
+
+ themeProperties.forEach(property => {
+ documentElement.style.removeProperty(property);
+ });
+
+ // REMOVE PLUGIN CSS FILES COMPLETELY
+ const pluginStyles = document.querySelectorAll('link[data-themes-plugin="true"]');
+ pluginStyles.forEach(style => {
+ console.log(`🧹 Completely removing plugin CSS: ${style.getAttribute('href')}`);
+ style.remove();
+ });
+
+ // Also remove any injected style elements
+ const pluginStyleTags = document.querySelectorAll('style[data-themes-plugin="true"]');
+ pluginStyleTags.forEach(style => style.remove());
+
+ // Reset to system default
+ const isSystemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+ if (isSystemDark) {
+ documentElement.classList.add('dark');
+ }
+
+ console.log('🧹 Themes plugin completely disabled - CSS removed, reverted to system defaults');
+ } else {
+ // Re-apply current theme when enabled
+ console.log('🎨 Themes plugin re-enabled, CSS will be loaded and theme applied:', currentTheme);
+ }
+ };
+
+ const syncConfigToServer = async (config: ThemePluginConfig) => {
+ try {
+ // Update runtime plugin state (not static config)
+ await fetch('/api/plugins/setState', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ pluginId: 'themes', enabled: config.enabled })
+ });
+ } catch (error) {
+ console.warn('Failed to sync theme config to server:', error);
+ }
+ };
+
+
+ const applyLayoutClasses = () => {
+ if (typeof window === 'undefined' || typeof document === 'undefined') return;
+
+ // Apply nav-tabs class to tab containers
+ const tabContainers = document.querySelectorAll('[role="tablist"], .tabs-container, [data-tabs]');
+ tabContainers.forEach(container => {
+ container.classList.add('nav-tabs');
+ if (currentTheme !== 'advanced') {
+ container.classList.add('modern-tab-container');
+ }
+ });
+
+ // Apply nav-tab class to individual tabs
+ const tabs = document.querySelectorAll('[role="tab"], .tab, [data-tab]');
+ tabs.forEach(tab => {
+ tab.classList.add('nav-tab');
+ if (currentTheme !== 'advanced') {
+ tab.classList.add('modern-tab');
+ }
+ });
+
+ };
+
+ // Watch for analytics attribute and dynamically add/remove Mission Control tab
+ useEffect(() => {
+ const missionControlManager = (analyticsEnabled: boolean) => {
+ const tabContainer = document.querySelector('[role="tablist"], .nav-tabs');
+ if (!tabContainer) return;
+
+ const existingTab = document.querySelector('.mission-control-tab, [data-tab="mission-control"]');
+
+ if (analyticsEnabled && !existingTab) {
+ const missionControlTab = document.createElement('button');
+ missionControlTab.className = 'nav-tab mission-control-tab';
+ missionControlTab.setAttribute('data-tab', 'mission-control');
+ missionControlTab.textContent = '🎯 Mission Control';
+ missionControlTab.addEventListener('click', () => {
+ const event = new CustomEvent('navigate-mission-control');
+ document.dispatchEvent(event);
+ });
+ tabContainer.appendChild(missionControlTab);
+ } else if (!analyticsEnabled && existingTab) {
+ existingTab.remove();
+ }
+ };
+
+ const observer = new MutationObserver((mutations) => {
+ mutations.forEach(mutation => {
+ if (mutation.type === 'attributes' && mutation.attributeName === 'data-analytics') {
+ const analyticsEnabled = document.documentElement.hasAttribute('data-analytics');
+ missionControlManager(analyticsEnabled);
+ }
+ });
+ });
+
+ observer.observe(document.documentElement, { attributes: true });
+
+ // Initial check
+ missionControlManager(document.documentElement.hasAttribute('data-analytics'));
+
+ return () => observer.disconnect();
+ }, []);
+
+ const value: ThemeContextType = {
+ currentTheme,
+ themes: THEME_DEFINITIONS,
+ pluginConfig,
+ setTheme,
+ isPluginEnabled,
+ togglePlugin
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useTheme = (): ThemeContextType => {
+ const context = useContext(ThemeContext);
+ if (!context) {
+ throw new Error('useTheme must be used within a ThemeProvider from the themes plugin');
+ }
+ return context;
+};
+
+export default ThemeContext;
\ No newline at end of file
diff --git a/plugins/themes/index.ts b/plugins/themes/index.ts
new file mode 100644
index 00000000..050c01fb
--- /dev/null
+++ b/plugins/themes/index.ts
@@ -0,0 +1,519 @@
+/**
+ * Themes Plugin - Main Entry Point
+ * Unified theme system with 3 themes: Light, Dark, Advanced
+ */
+
+import { ThemeProvider, useTheme } from './contexts/ThemeContext';
+import ThemeSelector from './components/ThemeSelector';
+import type {
+ ThemeType,
+ ThemePluginConfig,
+ ThemePluginAPI,
+ ThemeDefinition
+} from './types';// Export components for external use
+export { ThemeProvider, useTheme, ThemeSelector };
+export type { ThemeType, ThemePluginConfig, ThemeDefinition };
+
+// Default configuration
+export const DEFAULT_THEME_CONFIG: ThemePluginConfig = {
+ enabled: true,
+ activeTheme: 'light',
+ availableThemes: ['light', 'dark', 'advanced'],
+ persistUserChoice: true,
+ autoApplySystemTheme: false,
+ // Enhanced UI Experience settings
+ enableEnhancedUIExperience: false,
+ autoEnableAnalytics: true,
+ autoEnableDashboard: true
+};
+
+/**
+ * Plugin API Implementation
+ */
+class ThemesPluginAPI implements ThemePluginAPI {
+ readonly name = 'themes' as const;
+ readonly version = '1.0.0' as const;
+
+ private isRegistered = false;
+ private currentConfig: ThemePluginConfig = { ...DEFAULT_THEME_CONFIG };
+
+ /**
+ * Register the plugin - loads CSS and initializes
+ */
+ register(): void {
+ if (this.isRegistered) {
+ console.warn('Themes plugin is already registered');
+ return;
+ }
+
+ try {
+ // Load plugin CSS
+ this.loadPluginStyles();
+
+ // Apply initial theme
+ this.applyThemeToDocument();
+
+ // Mark as registered
+ this.isRegistered = true;
+
+ // Expose global API (browser only)
+ if (typeof window !== 'undefined') {
+ window.__THEMES_PLUGIN__ = this;
+ }
+
+ console.log(`✅ Themes Plugin v${this.version} registered successfully`);
+ } catch (error) {
+ console.error('Failed to register themes plugin:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Unregister the plugin - cleanup
+ */
+ unregister(): void {
+ if (!this.isRegistered) return;
+
+ try {
+ // Remove theme classes
+ this.removeThemeClasses();
+
+ // Remove plugin styles
+ this.removePluginStyles();
+
+ // Clean up global API (browser only)
+ if (typeof window !== 'undefined') {
+ delete window.__THEMES_PLUGIN__;
+ }
+
+ this.isRegistered = false;
+
+ console.log('🔄 Themes Plugin unregistered');
+ } catch (error) {
+ console.error('Failed to unregister themes plugin:', error);
+ }
+ }
+
+ /**
+ * Get current plugin configuration
+ */
+ getConfig(): ThemePluginConfig {
+ return { ...this.currentConfig };
+ }
+
+ /**
+ * Update plugin configuration
+ */
+ setConfig(config: Partial): void {
+ this.currentConfig = {
+ ...this.currentConfig,
+ ...config
+ };
+
+ // Apply configuration changes
+ if (this.isRegistered) {
+ this.applyThemeToDocument();
+ }
+
+ // Save to localStorage if persistence is enabled (browser only)
+ if (this.currentConfig.persistUserChoice && typeof window !== 'undefined' && typeof localStorage !== 'undefined') {
+ try {
+ localStorage.setItem('claude-router-theme-plugin', JSON.stringify({
+ activeTheme: this.currentConfig.activeTheme,
+ config: this.currentConfig
+ }));
+ } catch (error) {
+ console.warn('Failed to save theme config to localStorage:', error);
+ }
+ }
+ }
+
+ /**
+ * Set active theme
+ */
+ setTheme(theme: ThemeType): void {
+ if (!this.currentConfig.availableThemes.includes(theme)) {
+ console.warn(`Theme "${theme}" is not available in current configuration`);
+ return;
+ }
+
+ const previousTheme = this.currentConfig.activeTheme;
+ this.setConfig({ activeTheme: theme });
+
+ // Theme change is handled by ThemeContext through layout enhancer
+ if (previousTheme !== theme) {
+ console.log(`🎨 Theme changed from ${previousTheme} to ${theme}`);
+ }
+ }
+
+ /**
+ * Get current active theme
+ */
+ getCurrentTheme(): ThemeType {
+ return this.currentConfig.activeTheme;
+ }
+
+ /**
+ * Get list of available themes
+ */
+ getAvailableThemes(): ThemeType[] {
+ return [...this.currentConfig.availableThemes];
+ }
+
+ /**
+ * Apply theme classes to a specific element (browser only)
+ */
+ applyToElement(element: HTMLElement): void {
+ if (!this.isRegistered || !this.currentConfig.enabled) return;
+ if (typeof window === 'undefined') return; // Skip in Node.js
+
+ // Remove existing theme classes
+ element.classList.remove('theme-light', 'theme-dark', 'theme-advanced');
+
+ // Add current theme class
+ element.classList.add(`theme-${this.currentConfig.activeTheme}`);
+ }
+
+ /**
+ * Clean up theme classes and styles
+ */
+ cleanup(): void {
+ this.removeThemeClasses();
+ this.removePluginStyles();
+
+ // ENHANCED: Remove dynamic CSS variables and force style recalculation
+ if (typeof document !== 'undefined') {
+ const rootStyles = document.documentElement.style;
+ const propertiesToRemove: string[] = [];
+ for (let i = 0; i < rootStyles.length; i++) {
+ const property = rootStyles[i];
+ if (property && (property.startsWith('--theme-') || property.startsWith('--themes-plugin-'))) {
+ propertiesToRemove.push(property);
+ }
+ }
+ propertiesToRemove.forEach(prop => rootStyles.removeProperty(prop));
+
+ // Force style recalculation
+ document.documentElement.offsetHeight;
+ }
+ }
+
+ /**
+ * Private: Load plugin CSS files (browser only)
+ */
+ private loadPluginStyles(): void {
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
+ console.log('Themes plugin: CSS loading skipped (Node.js environment)');
+ return;
+ }
+
+ const cssFiles = [
+ '/plugins/themes/styles/themes.css',
+ '/plugins/themes/styles/variables.css',
+ '/plugins/themes/styles/modern-effects.css',
+ '/plugins/themes/styles/components.css',
+ '/plugins/themes/styles/notifications.css',
+ '/plugins/themes/styles/centered-layout.css'
+ ];
+
+ cssFiles.forEach((cssFile) => {
+ // Check if already loaded
+ if (document.querySelector(`link[href="${cssFile}"]`)) {
+ return;
+ }
+
+ const link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = cssFile;
+ link.setAttribute('data-themes-plugin', 'true');
+ document.head.appendChild(link);
+
+ console.log(`✅ Themes plugin CSS loaded: ${cssFile}`);
+ });
+ }
+
+
+ /**
+ * Private: Remove plugin CSS files
+ */
+ private removePluginStyles(): void {
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
+ console.log('Themes plugin: CSS cleanup skipped (Node.js environment)');
+ return;
+ }
+
+ const pluginStyles = document.querySelectorAll('link[data-themes-plugin="true"]');
+ pluginStyles.forEach(style => {
+ console.log(`🧹 Removing themes plugin CSS: ${style.getAttribute('href')}`);
+ style.remove();
+ });
+
+ // Also remove any injected style elements
+ const pluginStyleTags = document.querySelectorAll('style[data-themes-plugin="true"]');
+ pluginStyleTags.forEach(style => style.remove());
+
+ console.log('✅ All themes plugin CSS removed');
+ }
+
+ /**
+ * Private: Apply theme to document (browser only)
+ */
+ private applyThemeToDocument(): void {
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
+ console.log('Themes plugin: Document theming skipped (Node.js environment)');
+ return;
+ }
+
+ const documentElement = document.documentElement;
+
+ // FIXED: Always remove theme classes first, regardless of enabled state
+ this.removeThemeClasses();
+
+ // Only apply theme if enabled
+ if (this.currentConfig.enabled) {
+ // Add current theme class
+ documentElement.classList.add(`theme-${this.currentConfig.activeTheme}`);
+
+ // Add plugin active indicator
+ documentElement.classList.add('themes-plugin-active');
+
+ // Check and set analytics status
+ this.checkAnalyticsStatus();
+ }
+ }
+
+ /**
+ * Check if analytics plugin is enabled and update HTML attributes
+ */
+ private checkAnalyticsStatus(): void {
+ if (typeof window === 'undefined' || typeof document === 'undefined') return;
+
+ // Check for analytics plugin indicators
+ const analyticsEnabled =
+ localStorage.getItem('analytics-enabled') === 'true' ||
+ document.querySelector('[data-analytics="true"]') ||
+ window.location.search.includes('analytics=true') ||
+ // Check if Mission Control tab exists
+ document.querySelector('[class*="mission-control"]') ||
+ // Check for analytics plugin global
+ (window as any).__ANALYTICS_PLUGIN__;
+
+ if (analyticsEnabled) {
+ document.documentElement.setAttribute('data-analytics', 'enabled');
+ console.log('📊 Analytics detected - notifying analytics plugin');
+ // Notify analytics plugin to show its button
+ window.dispatchEvent(new CustomEvent('themes-plugin-analytics-enabled'));
+ } else {
+ document.documentElement.removeAttribute('data-analytics');
+ // Notify analytics plugin to hide its button
+ window.dispatchEvent(new CustomEvent('themes-plugin-analytics-disabled'));
+ }
+ }
+
+ /**
+ * Private: Remove all theme classes (browser only)
+ */
+ private removeThemeClasses(): void {
+ if (typeof window === 'undefined' || typeof document === 'undefined') return;
+
+ const documentElement = document.documentElement;
+ documentElement.classList.remove(
+ 'theme-light',
+ 'theme-dark',
+ 'theme-advanced',
+ 'themes-plugin-active'
+ );
+ }
+
+ /**
+ * Initialize from localStorage (browser only)
+ */
+ private initializeFromStorage(): void {
+ // Skip localStorage operations in Node.js environment
+ if (typeof window === 'undefined' || typeof localStorage === 'undefined') {
+ console.log('Themes plugin: localStorage not available (Node.js environment)');
+ return;
+ }
+
+ try {
+ const saved = localStorage.getItem('claude-router-theme-plugin');
+ if (saved) {
+ const data = JSON.parse(saved);
+ if (data.config) {
+ this.currentConfig = { ...this.currentConfig, ...data.config };
+ }
+ if (data.activeTheme && this.currentConfig.availableThemes.includes(data.activeTheme)) {
+ this.currentConfig.activeTheme = data.activeTheme;
+ }
+ }
+ } catch (error) {
+ console.warn('Failed to load theme config from localStorage:', error);
+ }
+ }
+
+ /**
+ * Enhanced UI Experience - Activează automat themes + analytics
+ */
+ enableEnhancedUIExperience(): void {
+ console.log('🚀 Enabling Enhanced UI Experience...');
+
+ // Activează themes plugin
+ this.setConfig({
+ enabled: true,
+ enableEnhancedUIExperience: true
+ });
+
+ // Activează analytics dacă este configurat
+ if (this.currentConfig.autoEnableAnalytics) {
+ this.enableAnalyticsPlugin();
+ }
+
+ // Activează dashboard dacă este configurat
+ if (this.currentConfig.autoEnableDashboard) {
+ this.enableDashboardFeatures();
+ }
+
+ console.log('✅ Enhanced UI Experience enabled successfully');
+ }
+
+ /**
+ * Dezactivează Enhanced UI Experience
+ */
+ disableEnhancedUIExperience(): void {
+ console.log('🔄 Disabling Enhanced UI Experience...');
+
+ this.setConfig({
+ enabled: false,
+ enableEnhancedUIExperience: false
+ });
+
+ console.log('✅ Enhanced UI Experience disabled');
+ }
+
+ /**
+ * Activează analytics plugin prin configurație
+ */
+ private enableAnalyticsPlugin(): void {
+ try {
+ // Trimite cerere către server pentru a activa pluginul analytics
+ fetch('/api/plugins/analytics/enable', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ source: 'themes-plugin' })
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ console.log('📊 Analytics plugin activation requested successfully');
+
+ // Emit event pentru a semnala că analytics a fost activat în UI
+ window.dispatchEvent(new CustomEvent('analytics-plugin-enabled', {
+ detail: { source: 'themes-plugin' }
+ }));
+ } else {
+ console.warn('Failed to enable analytics plugin:', data.error);
+ }
+ })
+ .catch(error => {
+ console.warn('Failed to enable analytics plugin:', error);
+ });
+ } catch (error) {
+ console.warn('Failed to enable analytics plugin:', error);
+ }
+ }
+
+ /**
+ * Activează dashboard features
+ */
+ private enableDashboardFeatures(): void {
+ try {
+ // Emit event pentru a semnala că dashboard trebuie activat
+ window.dispatchEvent(new CustomEvent('enable-dashboard-features', {
+ detail: {
+ source: 'themes-plugin',
+ features: ['missionControl', 'analytics', 'realTimeMetrics']
+ }
+ }));
+
+ console.log('📈 Dashboard features activation requested');
+ } catch (error) {
+ console.warn('Failed to enable dashboard features:', error);
+ }
+ }
+
+ /**
+ * Check dacă Enhanced UI Experience este activ
+ */
+ isEnhancedUIExperienceEnabled(): boolean {
+ return this.currentConfig.enabled && (this.currentConfig.enableEnhancedUIExperience || false);
+ }
+
+ /**
+ * Enable analytics integration manually
+ */
+ enableAnalyticsIntegration(): void {
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('analytics-enabled', 'true');
+ this.checkAnalyticsStatus();
+ console.log('📊 Analytics integration enabled manually');
+ }
+ }
+
+ /**
+ * Disable analytics integration
+ */
+ disableAnalyticsIntegration(): void {
+ if (typeof window !== 'undefined') {
+ localStorage.removeItem('analytics-enabled');
+ this.checkAnalyticsStatus();
+ console.log('📊 Analytics integration disabled');
+ }
+ }
+
+ constructor() {
+ this.initializeFromStorage();
+ }
+}
+
+// Create and export plugin instance
+export const themesPlugin = new ThemesPluginAPI();
+
+/**
+ * Auto-register helper function
+ */
+export function initializeThemesPlugin(config?: Partial): ThemesPluginAPI {
+ if (config) {
+ themesPlugin.setConfig(config);
+ }
+
+ if (typeof window !== 'undefined' && !window.__THEMES_PLUGIN__) {
+ themesPlugin.register();
+ }
+
+ return themesPlugin;
+}
+
+/**
+ * React Hook for using the themes plugin
+ */
+export function useThemesPlugin() {
+ const plugin = (typeof window !== 'undefined' && window.__THEMES_PLUGIN__) || themesPlugin;
+
+ return {
+ setTheme: plugin.setTheme.bind(plugin),
+ getCurrentTheme: plugin.getCurrentTheme.bind(plugin),
+ getAvailableThemes: plugin.getAvailableThemes.bind(plugin),
+ getConfig: plugin.getConfig.bind(plugin),
+ setConfig: plugin.setConfig.bind(plugin),
+ isEnabled: () => plugin.getConfig().enabled,
+ // Enhanced UI Experience methods
+ enableEnhancedUIExperience: plugin.enableEnhancedUIExperience.bind(plugin),
+ disableEnhancedUIExperience: plugin.disableEnhancedUIExperience.bind(plugin),
+ isEnhancedUIExperienceEnabled: plugin.isEnhancedUIExperienceEnabled.bind(plugin)
+ };
+}
+
+// Default export for convenience
+export default themesPlugin;
\ No newline at end of file
diff --git a/plugins/themes/plugin.manifest.json b/plugins/themes/plugin.manifest.json
new file mode 100644
index 00000000..66d62cea
--- /dev/null
+++ b/plugins/themes/plugin.manifest.json
@@ -0,0 +1,92 @@
+{
+ "id": "themes",
+ "name": "Advanced Themes",
+ "description": "Premium theming system with glassmorphism effects, advanced animations, customizable color schemes, and modern UI transformations",
+ "version": "2.2.0",
+ "author": "UI/UX Team",
+ "category": "ui",
+ "type": "component",
+ "requirements": {
+ "minAppVersion": "1.0.0",
+ "permissions": [
+ "storage",
+ "settings"
+ ]
+ },
+ "settings": {
+ "defaults": {
+ "theme": "light",
+ "enableAnimations": true,
+ "enableGlassmorphism": false,
+ "animationSpeed": "normal",
+ "customColors": {},
+ "borderRadius": "medium",
+ "shadowIntensity": "medium",
+ "blurIntensity": "medium",
+ "enableHoverEffects": true,
+ "enableTransitions": true
+ },
+ "schema": {
+ "theme": {
+ "type": "string",
+ "enum": ["light", "dark", "system"],
+ "description": "Base theme mode"
+ },
+ "enableAnimations": {
+ "type": "boolean",
+ "description": "Enable UI animations and transitions"
+ },
+ "enableGlassmorphism": {
+ "type": "boolean",
+ "description": "Enable glassmorphism visual effects"
+ },
+ "animationSpeed": {
+ "type": "string",
+ "enum": ["slow", "normal", "fast"],
+ "description": "Animation speed setting"
+ },
+ "borderRadius": {
+ "type": "string",
+ "enum": ["none", "small", "medium", "large", "full"],
+ "description": "UI border radius setting"
+ },
+ "shadowIntensity": {
+ "type": "string",
+ "enum": ["none", "low", "medium", "high"],
+ "description": "Shadow intensity level"
+ },
+ "blurIntensity": {
+ "type": "string",
+ "enum": ["none", "low", "medium", "high"],
+ "description": "Blur effect intensity for glassmorphism"
+ }
+ },
+ "userConfigurable": true
+ },
+ "lifecycle": {
+ "onActivate": "applyTheme",
+ "onDeactivate": "resetTheme",
+ "onStart": "initializeThemeSystem",
+ "onStop": "cleanupThemeSystem"
+ },
+ "metadata": {
+ "homepage": "/plugins/themes",
+ "repository": "https://github.com/claude-code-router/plugins/themes",
+ "license": "MIT",
+ "keywords": [
+ "themes",
+ "ui",
+ "glassmorphism",
+ "design",
+ "animations",
+ "customization",
+ "modern",
+ "effects"
+ ],
+ "screenshots": [
+ "/plugins/themes/screenshots/light-theme.png",
+ "/plugins/themes/screenshots/dark-theme.png",
+ "/plugins/themes/screenshots/glassmorphism.png"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/plugins/themes/styles/centered-layout.css b/plugins/themes/styles/centered-layout.css
new file mode 100644
index 00000000..c09c1a79
--- /dev/null
+++ b/plugins/themes/styles/centered-layout.css
@@ -0,0 +1,469 @@
+/**
+ * 🎯 SISTEM LAYOUT CENTRAT PENTRU TOATE TEMELE
+ * ===========================================
+ *
+ * Transformă layout-ul din full-width în centrat, modern și aerisit
+ * pentru toate cele 3 teme (Light, Dark, Advanced)
+ *
+ * ✨ FEATURES:
+ * - Container centrat cu max-width responsiv
+ * - Topbar centrat în loc de full-width
+ * - Spacing modern și aerisit
+ * - Respectă identitatea vizuală a fiecărei teme
+ * - Responsive design pentru toate ecranele
+ */
+
+/* ===========================================
+ 🎯 MASTER CONTAINER CENTRAT
+ =========================================== */
+
+/* Containerul principal centrat */
+.themes-plugin-active .app-container,
+.themes-plugin-active body > div:first-child,
+.themes-plugin-active #root,
+.themes-plugin-active [data-testid="app-container"],
+.themes-plugin-active div[class*="h-screen bg-gray-50"] {
+ max-width: 1200px !important;
+ margin: 0 auto !important;
+ padding: 0 24px !important;
+ min-height: 100vh !important;
+ display: flex !important;
+ flex-direction: column !important;
+
+ /* Reset background pentru theme control */
+ background: none !important;
+}
+
+/* Layout wrapper centrat */
+.themes-plugin-active .layout-wrapper {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 24px;
+ width: 100%;
+}
+
+/* ===========================================
+ 🏗️ TOPBAR CENTRAT (nu mai e full-width)
+ =========================================== */
+
+/* Topbar centrat pentru toate temele */
+.themes-plugin-active header,
+.themes-plugin-active .header,
+.themes-plugin-active nav,
+.themes-plugin-active .topbar,
+.themes-plugin-active .navigation,
+.themes-plugin-active banner {
+ width: 100% !important;
+ max-width: none !important; /* Remove max-width restriction */
+ margin: 24px 0 !important; /* Remove auto centering, use container centering */
+ border-radius: var(--radius-xl) !important;
+ padding: 16px 24px !important;
+ box-shadow: var(--shadow-md) !important;
+ backdrop-filter: var(--glass-blur) !important;
+ position: relative !important;
+
+ /* Transforma din fixed full-width în centrat */
+ position: static !important;
+ left: auto !important;
+ right: auto !important;
+ top: auto !important;
+
+ /* Remove original styling */
+ background: none !important;
+ border: none !important;
+}
+
+/* Spacing pentru topbar centrat */
+.themes-plugin-active .topbar-content,
+.themes-plugin-active .header-content {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20px;
+ max-width: none;
+}
+
+/* Logo și titlu în topbar centrat */
+.themes-plugin-active .topbar h1,
+.themes-plugin-active .header h1,
+.themes-plugin-active banner h1 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: var(--foreground);
+}
+
+/* Butoanele din topbar */
+.themes-plugin-active .topbar-actions,
+.themes-plugin-active .header-actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+/* ===========================================
+ 📄 MAIN CONTENT CENTRAT ȘI AERISIT
+ =========================================== */
+
+/* Main content cu spacing modern */
+.themes-plugin-active main,
+.themes-plugin-active .main-content,
+.themes-plugin-active .content-wrapper,
+.themes-plugin-active main[class*="flex flex-1 gap-6 p-6"] {
+ max-width: 1200px !important;
+ margin: 0 auto !important;
+ padding: 32px 24px !important;
+ flex: 1 !important;
+
+ /* Remove original background */
+ background: none !important;
+}
+
+/* Grid layout pentru secțiuni */
+.themes-plugin-active .content-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 32px;
+ margin-bottom: 32px;
+}
+
+/* Secțiuni individuale cu spacing */
+.themes-plugin-active .section,
+.themes-plugin-active .panel,
+.themes-plugin-active .content-section {
+ background: var(--gradient-card);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-xl);
+ padding: 24px;
+ box-shadow: var(--shadow-sm);
+ backdrop-filter: var(--glass-blur);
+ margin-bottom: 24px;
+}
+
+/* Headers secțiuni */
+.themes-plugin-active .section-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 20px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid var(--glass-border);
+}
+
+.themes-plugin-active .section-title {
+ font-size: 1.125rem;
+ font-weight: 600;
+ color: var(--foreground);
+ margin: 0;
+}
+
+/* ===========================================
+ 🎨 STILIZĂRI SPECIFICE PENTRU FIECARE TEMĂ
+ =========================================== */
+
+/* LIGHT THEME - Layout centrat profesional */
+html.theme-light.themes-plugin-active {
+ background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
+}
+
+html.theme-light.themes-plugin-active .topbar,
+html.theme-light.themes-plugin-active header,
+html.theme-light.themes-plugin-active banner {
+ background: linear-gradient(135deg,
+ rgba(255, 255, 255, 0.9) 0%,
+ rgba(248, 250, 252, 0.95) 100%
+ );
+ border: 1px solid rgba(226, 232, 240, 0.8);
+ box-shadow:
+ 0 4px 20px rgba(0, 0, 0, 0.08),
+ 0 1px 3px rgba(0, 0, 0, 0.05);
+}
+
+/* DARK THEME - Layout centrat elegant */
+html.theme-dark.themes-plugin-active {
+ background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
+}
+
+html.theme-dark.themes-plugin-active .topbar,
+html.theme-dark.themes-plugin-active header,
+html.theme-dark.themes-plugin-active banner {
+ background: linear-gradient(135deg,
+ rgba(30, 41, 59, 0.9) 0%,
+ rgba(15, 23, 42, 0.95) 100%
+ );
+ border: 1px solid rgba(51, 65, 85, 0.6);
+ box-shadow:
+ 0 4px 20px rgba(0, 0, 0, 0.4),
+ 0 1px 3px rgba(0, 0, 0, 0.3),
+ 0 0 20px rgba(59, 130, 246, 0.1);
+}
+
+/* ADVANCED THEME - Layout centrat cosmic spectacular */
+html.theme-advanced.themes-plugin-active {
+ background: var(--space-bg);
+ background-attachment: fixed;
+}
+
+html.theme-advanced.themes-plugin-active::before {
+ content: '';
+ position: fixed;
+ inset: 0;
+ background: var(--nebula-glow);
+ pointer-events: none;
+ z-index: -1;
+}
+
+html.theme-advanced.themes-plugin-active .topbar,
+html.theme-advanced.themes-plugin-active header,
+html.theme-advanced.themes-plugin-active banner {
+ background: linear-gradient(135deg,
+ rgba(30, 41, 59, 0.4) 0%,
+ rgba(15, 23, 42, 0.6) 50%,
+ rgba(7, 12, 22, 0.8) 100%
+ );
+ border: 1px solid rgba(139, 92, 246, 0.3);
+ box-shadow:
+ 0 8px 32px rgba(0, 0, 0, 0.6),
+ 0 0 60px rgba(139, 92, 246, 0.2),
+ 0 0 100px rgba(59, 130, 246, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(24px) saturate(200%);
+}
+
+/* Efect cosmic pentru topbar advanced */
+html.theme-advanced.themes-plugin-active .topbar::before,
+html.theme-advanced.themes-plugin-active header::before,
+html.theme-advanced.themes-plugin-active banner::before {
+ content: '';
+ position: absolute;
+ inset: -1px;
+ background: linear-gradient(
+ 45deg,
+ rgba(139, 92, 246, 0.5) 0%,
+ rgba(59, 130, 246, 0.3) 25%,
+ rgba(139, 92, 246, 0.5) 50%,
+ rgba(59, 130, 246, 0.3) 75%,
+ rgba(139, 92, 246, 0.5) 100%
+ );
+ border-radius: calc(var(--radius-xl) + 1px);
+ z-index: -1;
+ opacity: 0.6;
+ animation: cosmicGlow 8s ease-in-out infinite;
+}
+
+@keyframes cosmicGlow {
+ 0%, 100% { opacity: 0.6; }
+ 50% { opacity: 0.8; }
+}
+
+/* ===========================================
+ 📦 CARDURI ȘI COMPONENTE
+ =========================================== */
+
+/* Carduri cu spacing îmbunătățit */
+.themes-plugin-active .card,
+.themes-plugin-active .provider-card,
+.themes-plugin-active .config-card {
+ background: var(--gradient-card);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-lg);
+ padding: 20px;
+ margin-bottom: 20px;
+ box-shadow: var(--shadow-sm);
+ backdrop-filter: var(--glass-blur);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.themes-plugin-active .card:hover,
+.themes-plugin-active .provider-card:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-lg);
+}
+
+/* Input și form elements cu spacing */
+.themes-plugin-active .form-group {
+ margin-bottom: 20px;
+}
+
+.themes-plugin-active .form-label {
+ display: block;
+ margin-bottom: 8px;
+ font-weight: 500;
+ color: var(--foreground);
+}
+
+.themes-plugin-active input,
+.themes-plugin-active select,
+.themes-plugin-active textarea {
+ width: 100%;
+ padding: 12px 16px;
+ background: var(--glass-bg);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-md);
+ color: var(--foreground);
+ font-size: 14px;
+ transition: all 0.3s ease;
+}
+
+.themes-plugin-active input:focus,
+.themes-plugin-active select:focus,
+.themes-plugin-active textarea:focus {
+ outline: none;
+ border-color: var(--focus-ring);
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus-ring) 12%, transparent);
+ background: var(--card);
+}
+
+/* ===========================================
+ 📱 RESPONSIVE DESIGN
+ =========================================== */
+
+/* Tablet și ecrane mici */
+@media (max-width: 1024px) {
+ .themes-plugin-active .app-container,
+ .themes-plugin-active body > div:first-child,
+ .themes-plugin-active #root {
+ max-width: 900px;
+ padding: 0 20px;
+ }
+
+ .themes-plugin-active header,
+ .themes-plugin-active .topbar,
+ .themes-plugin-active banner {
+ margin: 20px 0; /* Remove auto centering */
+ padding: 14px 20px;
+ }
+
+ .themes-plugin-active main,
+ .themes-plugin-active .main-content {
+ padding: 24px 20px;
+ }
+
+ .themes-plugin-active .content-grid {
+ gap: 24px;
+ }
+}
+
+/* Mobile */
+@media (max-width: 768px) {
+ .themes-plugin-active .app-container,
+ .themes-plugin-active body > div:first-child,
+ .themes-plugin-active #root {
+ max-width: none;
+ padding: 0 16px;
+ }
+
+ .themes-plugin-active header,
+ .themes-plugin-active .topbar,
+ .themes-plugin-active banner {
+ margin: 16px 0; /* Remove auto centering */
+ padding: 12px 16px;
+ border-radius: var(--radius-lg);
+ }
+
+ .themes-plugin-active main,
+ .themes-plugin-active .main-content {
+ padding: 20px 16px;
+ }
+
+ .themes-plugin-active .content-grid {
+ grid-template-columns: 1fr;
+ gap: 20px;
+ }
+
+ .themes-plugin-active .section,
+ .themes-plugin-active .panel {
+ padding: 16px;
+ margin-bottom: 16px;
+ }
+
+ .themes-plugin-active .topbar-content,
+ .themes-plugin-active .header-content {
+ flex-wrap: wrap;
+ gap: 12px;
+ }
+
+ .themes-plugin-active .topbar h1,
+ .themes-plugin-active banner h1 {
+ font-size: 1.25rem;
+ }
+}
+
+/* Small mobile */
+@media (max-width: 480px) {
+ .themes-plugin-active .app-container,
+ .themes-plugin-active body > div:first-child,
+ .themes-plugin-active #root {
+ padding: 0 12px;
+ }
+
+ .themes-plugin-active header,
+ .themes-plugin-active .topbar,
+ .themes-plugin-active banner {
+ margin: 12px 0; /* Remove auto centering */
+ padding: 10px 12px;
+ }
+
+ .themes-plugin-active main,
+ .themes-plugin-active .main-content {
+ padding: 16px 12px;
+ }
+
+ .themes-plugin-active .section,
+ .themes-plugin-active .panel {
+ padding: 12px;
+ }
+}
+
+/* ===========================================
+ ⚡ PERFORMANCE OPTIMIZATIONS
+ =========================================== */
+
+/* Reduce motion pentru users cu preferințe de accesibilitate */
+@media (prefers-reduced-motion: reduce) {
+ .themes-plugin-active * {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+
+ html.theme-advanced.themes-plugin-active .topbar::before,
+ html.theme-advanced.themes-plugin-active header::before,
+ html.theme-advanced.themes-plugin-active banner::before {
+ animation: none;
+ }
+}
+
+/* High contrast mode support */
+@media (prefers-contrast: high) {
+ .themes-plugin-active .topbar,
+ .themes-plugin-active header,
+ .themes-plugin-active banner {
+ border-width: 2px !important;
+ backdrop-filter: none !important;
+ }
+
+ .themes-plugin-active .section,
+ .themes-plugin-active .panel,
+ .themes-plugin-active .card {
+ border-width: 2px !important;
+ }
+}
+
+/* ===========================================
+ 🎯 LAYOUT DEBUGGING (Development only)
+ =========================================== */
+
+/* Uncomment pentru debugging layout-ul */
+/*
+.themes-plugin-active .app-container,
+.themes-plugin-active .topbar,
+.themes-plugin-active main {
+ outline: 1px dashed rgba(255, 0, 0, 0.3);
+}
+
+.themes-plugin-active .section,
+.themes-plugin-active .card {
+ outline: 1px dashed rgba(0, 255, 0, 0.3);
+}
+*/
\ No newline at end of file
diff --git a/plugins/themes/styles/components.css b/plugins/themes/styles/components.css
new file mode 100644
index 00000000..1b5223d7
--- /dev/null
+++ b/plugins/themes/styles/components.css
@@ -0,0 +1,801 @@
+/**
+ * Unified Components System v2.0
+ * Consistent component styling across all 3 themes
+ * - Same modern design patterns for Light, Dark, and Advanced
+ * - No theme-specific logic, only unified classes using CSS variables
+ * - Professional, elegant, and spectacular - all at the same time
+ */
+
+/* ===========================================
+ BASE COMPONENT RESET & SETUP
+ =========================================== */
+
+/* Apply modern styling to all basic elements */
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: system-ui, -apple-system, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
+ line-height: 1.6;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* ===========================================
+ UNIFIED LAYOUT COMPONENTS
+ =========================================== */
+
+/* Modern Container */
+.modern-container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: var(--space-md);
+}
+
+/* Modern Grid System */
+.modern-grid {
+ display: grid;
+ gap: var(--space-lg);
+}
+
+.modern-grid-2 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
+.modern-grid-3 { grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
+.modern-grid-4 { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
+
+/* Modern Flex Layouts */
+.modern-flex {
+ display: flex;
+ gap: var(--space-md);
+}
+
+.modern-flex-center {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-md);
+}
+
+.modern-flex-between {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-md);
+}
+
+/* ===========================================
+ UNIFIED CARD COMPONENTS
+ =========================================== */
+
+/* Base Card - Includes modern-card styles */
+.card {
+ background: var(--gradient-card);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-sm);
+ backdrop-filter: var(--glass-blur);
+ transition: all var(--transition-smooth);
+ position: relative;
+ overflow: hidden;
+}
+
+/* Card variants */
+.card-elevated {
+ box-shadow: var(--shadow-lg);
+ transform: translateY(-2px);
+}
+
+.card-interactive {
+ cursor: pointer;
+ transition: all var(--transition-smooth);
+}
+
+.card-interactive:hover {
+ transform: translateY(-6px);
+ box-shadow: var(--shadow-2xl);
+}
+
+/* Card content */
+.card-header {
+ padding: var(--space-lg);
+ border-bottom: 1px solid var(--glass-border);
+ background: var(--gradient-subtle);
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
+}
+
+.card-title {
+ margin: 0;
+ font-size: var(--font-xl);
+ font-weight: var(--font-bold);
+ color: var(--foreground);
+ line-height: 1.2;
+}
+
+.card-description {
+ margin: var(--space-xs) 0 0;
+ color: var(--muted-foreground);
+ font-size: var(--font-sm);
+}
+
+.card-content {
+ padding: var(--space-lg);
+ color: var(--card-foreground);
+}
+
+.card-footer {
+ padding: var(--space-lg);
+ border-top: 1px solid var(--glass-border);
+ background: var(--gradient-subtle);
+ border-radius: 0 0 var(--radius-lg) var(--radius-lg);
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: var(--space-sm);
+}
+
+/* ===========================================
+ UNIFIED BUTTON COMPONENTS
+ =========================================== */
+
+/* Base Button - Includes modern-button styles */
+.btn {
+ background: var(--gradient-primary);
+ color: var(--primary-foreground);
+ border: none;
+ border-radius: var(--radius-md);
+ padding: var(--space-sm) var(--space-lg);
+ font-weight: var(--font-semibold);
+ font-size: var(--font-sm);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ box-shadow: var(--shadow-sm);
+ position: relative;
+ overflow: hidden;
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-xs);
+}
+
+/* Button sizes */
+.btn-sm {
+ padding: var(--space-xs) var(--space-sm);
+ font-size: var(--font-xs);
+}
+
+.btn-lg {
+ padding: var(--space-md) var(--space-xl);
+ font-size: var(--font-lg);
+}
+
+/* Button variants */
+.btn-primary {
+ background: var(--gradient-primary);
+ color: var(--primary-foreground);
+}
+
+.btn-secondary {
+ background: var(--gradient-subtle);
+ color: var(--foreground);
+ border: 1px solid var(--glass-border);
+ backdrop-filter: var(--glass-blur);
+}
+
+.btn-outline {
+ background: transparent;
+ color: var(--foreground);
+ border: 1px solid var(--glass-border);
+ backdrop-filter: var(--glass-blur);
+}
+
+.btn-outline:hover {
+ background: var(--gradient-subtle);
+ border-color: var(--glass-border-strong);
+}
+
+.btn-ghost {
+ background: transparent;
+ color: var(--foreground);
+ border: none;
+ box-shadow: none;
+}
+
+.btn-ghost:hover {
+ background: var(--hover-overlay);
+}
+
+/* Button states */
+.btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none;
+}
+
+.btn:disabled:hover {
+ transform: none;
+ box-shadow: var(--shadow-sm);
+}
+
+/* ===========================================
+ UNIFIED INPUT COMPONENTS
+ =========================================== */
+
+/* Form Groups */
+.form-group {
+ margin-bottom: var(--space-lg);
+}
+
+.form-label {
+ display: block;
+ margin-bottom: var(--space-xs);
+ font-weight: var(--font-medium);
+ color: var(--foreground);
+ font-size: var(--font-sm);
+}
+
+.form-help {
+ margin-top: var(--space-xs);
+ font-size: var(--font-xs);
+ color: var(--muted-foreground);
+}
+
+/* Base Input - Includes modern-input styles */
+.input {
+ background: var(--glass-bg);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-md);
+ padding: var(--space-sm) var(--space-md);
+ font-size: var(--font-md);
+ color: var(--foreground);
+ transition: all var(--transition-fast);
+ backdrop-filter: var(--glass-blur);
+ box-shadow: var(--shadow-xs);
+ width: 100%;
+}
+
+/* Input variants */
+.input-sm {
+ padding: var(--space-xs) var(--space-sm);
+ font-size: var(--font-sm);
+}
+
+.input-lg {
+ padding: var(--space-md) var(--space-lg);
+ font-size: var(--font-lg);
+}
+
+/* Textarea */
+.textarea {
+ background: var(--glass-bg);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-md);
+ padding: var(--space-sm) var(--space-md);
+ font-size: var(--font-md);
+ color: var(--foreground);
+ transition: all var(--transition-fast);
+ backdrop-filter: var(--glass-blur);
+ box-shadow: var(--shadow-xs);
+ width: 100%;
+ resize: vertical;
+ min-height: 100px;
+}
+
+/* Select */
+.select {
+ background: var(--glass-bg);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-md);
+ padding: var(--space-sm) var(--space-md);
+ font-size: var(--font-md);
+ color: var(--foreground);
+ transition: all var(--transition-fast);
+ backdrop-filter: var(--glass-blur);
+ box-shadow: var(--shadow-xs);
+ width: 100%;
+ appearance: none;
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
+ background-position: right var(--space-sm) center;
+ background-repeat: no-repeat;
+ background-size: 16px 12px;
+ padding-right: var(--space-xl);
+}
+
+/* Input States */
+.input-error,
+.textarea-error,
+.select-error {
+ border-color: var(--status-error-border);
+ box-shadow: var(--shadow-sm), 0 0 0 3px var(--status-error-bg);
+}
+
+.input-success,
+.textarea-success,
+.select-success {
+ border-color: var(--status-success-border);
+ box-shadow: var(--shadow-sm), 0 0 0 3px var(--status-success-bg);
+}
+
+/* ===========================================
+ UNIFIED NAVIGATION COMPONENTS
+ =========================================== */
+
+/* Tab Navigation - Includes modern-tabs styles */
+.tabs {
+ display: flex;
+ background: var(--gradient-card);
+ border-radius: var(--radius-lg);
+ padding: var(--space-xs);
+ gap: var(--space-xs);
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--glass-border);
+ backdrop-filter: var(--glass-blur);
+}
+
+.tab {
+ flex: 1;
+ padding: var(--space-sm) var(--space-lg);
+ border: none;
+ background: transparent;
+ color: var(--muted-foreground);
+ cursor: pointer;
+ transition: all var(--transition-smooth);
+ border-radius: var(--radius-md);
+ font-weight: var(--font-medium);
+ font-size: var(--font-sm);
+ position: relative;
+ overflow: hidden;
+}
+
+/* Navigation Menu */
+.nav-menu {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+ padding: var(--space-md);
+ background: var(--gradient-card);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--glass-border);
+ backdrop-filter: var(--glass-blur);
+}
+
+.nav-item {
+ padding: var(--space-sm) var(--space-md);
+ border-radius: var(--radius-md);
+ color: var(--muted-foreground);
+ text-decoration: none;
+ font-weight: var(--font-medium);
+ transition: all var(--transition-fast);
+ position: relative;
+}
+
+.nav-item:hover,
+.nav-item.active {
+ color: var(--foreground);
+ background: var(--hover-overlay);
+}
+
+.nav-item.active::after {
+ content: '';
+ position: absolute;
+ bottom: -2px;
+ left: 50%;
+ width: 20px;
+ height: 2px;
+ background: var(--primary);
+ transform: translateX(-50%);
+ border-radius: var(--radius-full);
+}
+
+/* ===========================================
+ UNIFIED FEEDBACK COMPONENTS
+ =========================================== */
+
+/* Notifications - Includes modern-notification styles */
+.notification {
+ background: var(--gradient-card);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-lg);
+ padding: var(--space-md);
+ box-shadow: var(--shadow-lg);
+ backdrop-filter: var(--glass-blur);
+ animation: slideInUp 0.4s var(--ease-out);
+ position: relative;
+ overflow: hidden;
+}
+
+/* Status Indicators - Includes status-indicator styles */
+.status {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-xs);
+ padding: var(--space-xs) var(--space-sm);
+ border-radius: var(--radius-full);
+ font-size: var(--font-xs);
+ font-weight: var(--font-medium);
+ backdrop-filter: var(--glass-blur);
+ transition: all var(--transition-fast);
+}
+
+/* Progress Bar */
+.progress {
+ width: 100%;
+ height: 8px;
+ background: var(--muted);
+ border-radius: var(--radius-full);
+ overflow: hidden;
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
+}
+
+.progress-bar {
+ height: 100%;
+ background: var(--gradient-primary);
+ border-radius: var(--radius-full);
+ transition: width var(--transition-smooth);
+ position: relative;
+ overflow: hidden;
+}
+
+.progress-bar::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background: linear-gradient(45deg, transparent 35%, rgba(255, 255, 255, 0.2) 35%, rgba(255, 255, 255, 0.2) 65%, transparent 65%);
+ background-size: 20px 20px;
+ animation: shimmer 2s linear infinite;
+}
+
+/* Loading Spinner */
+.loading-spinner {
+ width: 32px;
+ height: 32px;
+ border: 3px solid var(--muted);
+ border-top: 3px solid var(--primary);
+ border-radius: 50%;
+ animation: rotate 1s linear infinite;
+ margin: 0 auto;
+}
+
+/* ===========================================
+ UNIFIED MODAL & OVERLAY COMPONENTS
+ =========================================== */
+
+/* Modal Overlay */
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(8px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: var(--z-modal-backdrop);
+ animation: fadeIn 0.2s ease-out;
+}
+
+/* Enhanced Modal Overlay for Advanced Theme */
+html.theme-advanced .modal-overlay {
+ background: rgba(0, 0, 0, 0.85) !important;
+ backdrop-filter: blur(20px) !important;
+}
+
+/* Modal Content */
+.modal {
+ background: var(--gradient-card);
+ border: 1px solid var(--glass-border-strong);
+ border-radius: var(--radius-xl);
+ box-shadow: var(--shadow-2xl);
+ backdrop-filter: var(--glass-blur);
+ max-width: 500px;
+ width: 90vw;
+ max-height: 90vh;
+ overflow: hidden;
+ animation: slideInUp 0.3s ease-out;
+}
+
+/* Enhanced Modal Content for Advanced Theme */
+html.theme-advanced .modal {
+ background: var(--modal-bg) !important;
+ border: 1px solid var(--modal-border) !important;
+ backdrop-filter: blur(18px) !important;
+ box-shadow: var(--modal-shadow) !important;
+}
+
+/* Enhanced Glassmorphism Components for Advanced Theme */
+html.theme-advanced .input,
+html.theme-advanced .textarea,
+html.theme-advanced .select {
+ background: rgba(255, 255, 255, 0.35) !important;
+ border: 1px solid rgba(255, 255, 255, 0.5) !important;
+ backdrop-filter: blur(14px) !important;
+}
+
+/* Enhanced Cards and Containers for Advanced Theme - exclude modals */
+html.theme-advanced .card:not(.fixed),
+html.theme-advanced .container:not(.fixed),
+html.theme-advanced .panel:not(.fixed) {
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.35), rgba(255, 255, 255, 0.28)) !important;
+ border: 1px solid rgba(255, 255, 255, 0.4) !important;
+ backdrop-filter: blur(14px) !important;
+}
+
+.modal-header {
+ padding: var(--space-lg);
+ border-bottom: 1px solid var(--glass-border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.modal-title {
+ margin: 0;
+ font-size: var(--font-xl);
+ font-weight: var(--font-bold);
+ color: var(--foreground);
+}
+
+.modal-close {
+ background: none;
+ border: none;
+ color: var(--muted-foreground);
+ cursor: pointer;
+ padding: var(--space-xs);
+ border-radius: var(--radius-sm);
+ transition: all var(--transition-fast);
+}
+
+.modal-close:hover {
+ color: var(--foreground);
+ background: var(--hover-overlay);
+}
+
+.modal-body {
+ padding: var(--space-lg);
+ color: var(--foreground);
+}
+
+.modal-footer {
+ padding: var(--space-lg);
+ border-top: 1px solid var(--glass-border);
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--space-sm);
+}
+
+/* ===========================================
+ UNIFIED TABLE COMPONENTS
+ =========================================== */
+
+.table-container {
+ background: var(--gradient-card);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ box-shadow: var(--shadow-sm);
+ backdrop-filter: var(--glass-blur);
+}
+
+.table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.table th,
+.table td {
+ padding: var(--space-md);
+ text-align: left;
+ border-bottom: 1px solid var(--glass-border);
+}
+
+.table th {
+ background: var(--gradient-subtle);
+ font-weight: var(--font-semibold);
+ color: var(--foreground);
+ font-size: var(--font-sm);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.table td {
+ color: var(--card-foreground);
+}
+
+.table tbody tr {
+ transition: all var(--transition-fast);
+}
+
+.table tbody tr:hover {
+ background: var(--hover-overlay);
+ transform: scale(1.01);
+}
+
+.table tbody tr:last-child td {
+ border-bottom: none;
+}
+
+/* ===========================================
+ UNIFIED UTILITY CLASSES
+ =========================================== */
+
+/* Spacing utilities */
+.p-0 { padding: 0; }
+.p-1 { padding: var(--space-xs); }
+.p-2 { padding: var(--space-sm); }
+.p-3 { padding: var(--space-md); }
+.p-4 { padding: var(--space-lg); }
+.p-5 { padding: var(--space-xl); }
+
+.m-0 { margin: 0; }
+.m-1 { margin: var(--space-xs); }
+.m-2 { margin: var(--space-sm); }
+.m-3 { margin: var(--space-md); }
+.m-4 { margin: var(--space-lg); }
+.m-5 { margin: var(--space-xl); }
+
+/* Text utilities */
+.text-xs { font-size: var(--font-xs); }
+.text-sm { font-size: var(--font-sm); }
+.text-md { font-size: var(--font-md); }
+.text-lg { font-size: var(--font-lg); }
+.text-xl { font-size: var(--font-xl); }
+.text-2xl { font-size: var(--font-2xl); }
+
+.font-normal { font-weight: var(--font-normal); }
+.font-medium { font-weight: var(--font-medium); }
+.font-semibold { font-weight: var(--font-semibold); }
+.font-bold { font-weight: var(--font-bold); }
+
+.text-center { text-align: center; }
+.text-left { text-align: left; }
+.text-right { text-align: right; }
+
+/* Color utilities using semantic colors */
+.text-primary { color: var(--primary); }
+.text-secondary { color: var(--secondary); }
+.text-muted { color: var(--muted-foreground); }
+.text-success { color: var(--status-success); }
+.text-warning { color: var(--status-warning); }
+.text-error { color: var(--status-error); }
+.text-info { color: var(--status-info); }
+
+/* Background utilities */
+.bg-card { background: var(--gradient-card); }
+.bg-primary { background: var(--gradient-primary); }
+.bg-success { background: var(--status-success-bg); }
+.bg-warning { background: var(--status-warning-bg); }
+.bg-error { background: var(--status-error-bg); }
+.bg-info { background: var(--status-info-bg); }
+
+/* Border utilities */
+.rounded-sm { border-radius: var(--radius-sm); }
+.rounded-md { border-radius: var(--radius-md); }
+.rounded-lg { border-radius: var(--radius-lg); }
+.rounded-xl { border-radius: var(--radius-xl); }
+.rounded-full { border-radius: var(--radius-full); }
+
+/* Shadow utilities */
+.shadow-sm { box-shadow: var(--shadow-sm); }
+.shadow-md { box-shadow: var(--shadow-md); }
+.shadow-lg { box-shadow: var(--shadow-lg); }
+.shadow-xl { box-shadow: var(--shadow-xl); }
+.shadow-2xl { box-shadow: var(--shadow-2xl); }
+
+/* ===========================================
+ RESPONSIVE DESIGN
+ =========================================== */
+
+@media (max-width: 768px) {
+ .modern-grid-2,
+ .modern-grid-3,
+ .modern-grid-4 {
+ grid-template-columns: 1fr;
+ }
+
+ .modern-flex {
+ flex-direction: column;
+ }
+
+ .modern-flex-between {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .card-footer {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .modal {
+ margin: var(--space-md);
+ }
+
+ .table-container {
+ overflow-x: auto;
+ }
+}
+
+/* ===========================================
+ ADVANCED THEME MODAL FIXES - Cosmic but Readable
+ =========================================== */
+
+/* Target ALL fullscreen modals - LogViewer, JsonEditor, MissionControl */
+html.theme-advanced .fixed.bottom-0.bg-white,
+html.theme-advanced .fixed.z-50.bg-white,
+html.theme-advanced [class*="fixed"][class*="bottom-0"].bg-white,
+html.theme-advanced [class*="fixed"][class*="z-50"].bg-white,
+html.theme-advanced .bg-white.fixed.bottom-0,
+html.theme-advanced .bg-white.fixed.z-50 {
+ background: var(--modal-bg) !important;
+ backdrop-filter: blur(20px) saturate(180%) !important;
+ border-top: 1px solid var(--modal-border) !important;
+ box-shadow: var(--modal-shadow) !important;
+}
+
+/* Enhanced targeting for specific modal patterns */
+html.theme-advanced .fixed.bottom-0.left-0.right-0.bg-white,
+html.theme-advanced .fixed.inset-0.bg-white {
+ background: var(--modal-bg) !important;
+ backdrop-filter: blur(20px) saturate(180%) !important;
+}
+
+/* Target overlay backgrounds with multiple patterns */
+html.theme-advanced [class*="fixed"][class*="inset-0"][class*="bg-black"],
+html.theme-advanced .fixed.inset-0[class*="bg-black"] {
+ background: var(--modal-overlay) !important;
+ backdrop-filter: blur(12px) !important;
+}
+
+/* Ensure modal headers have cosmic glow */
+html.theme-advanced [class*="fixed"][class*="bg-white"] .border-b,
+html.theme-advanced .fixed.bg-white .border-b {
+ border-color: rgba(123, 97, 255, 0.3) !important;
+ box-shadow: 0 1px 20px rgba(123, 97, 255, 0.2) !important;
+}
+
+/* Keep text readable in modal contexts */
+html.theme-advanced [class*="fixed"][class*="bg-white"] *,
+html.theme-advanced .fixed.bg-white * {
+ color: inherit !important;
+}
+
+/* Additional fix for any remaining bg-white modals */
+html.theme-advanced .bg-white[class*="fixed"] {
+ background: var(--modal-bg) !important;
+ backdrop-filter: blur(20px) saturate(180%) !important;
+}
+
+/* Force all modal overlays to be less transparent */
+html.theme-advanced div[class*="fixed"][class*="inset-0"] {
+ background: var(--modal-overlay) !important;
+ backdrop-filter: blur(12px) !important;
+}
+
+/* Comprehensive modal background fix - catches all patterns */
+html.theme-advanced [class~="fixed"][class~="bg-white"],
+html.theme-advanced [class*="fixed bg-white"],
+html.theme-advanced .fixed[class*="bg-white"] {
+ background: var(--modal-bg) !important;
+ backdrop-filter: blur(20px) saturate(180%) !important;
+ border: 1px solid var(--modal-border) !important;
+}
+
+/* Nuclear option - force all bg-white with fixed positioning in Advanced theme */
+html.theme-advanced .bg-white.fixed,
+html.theme-advanced .fixed.bg-white,
+html.theme-advanced div[class*="bg-white"][class*="fixed"] {
+ background: var(--modal-bg) !important;
+ backdrop-filter: blur(20px) saturate(180%) !important;
+}
+
+/* Fix for regular modals that are too transparent */
+html.theme-advanced .modal-window,
+html.theme-advanced .dialog,
+html.theme-advanced [role="dialog"],
+html.theme-advanced [class*="modal"] {
+ background: var(--modal-bg) !important;
+ backdrop-filter: blur(18px) saturate(160%) !important;
+ border: 1px solid var(--modal-border) !important;
+ box-shadow: var(--modal-shadow) !important;
+}
\ No newline at end of file
diff --git a/plugins/themes/styles/modern-effects.css b/plugins/themes/styles/modern-effects.css
new file mode 100644
index 00000000..801ca9b9
--- /dev/null
+++ b/plugins/themes/styles/modern-effects.css
@@ -0,0 +1,533 @@
+/**
+ * Modern Effects System v2.0
+ * Unified visual effects for all 3 themes with consistent styling
+ * - Same level of sophistication and visual appeal across Light, Dark, and Advanced
+ * - Hover effects, animations, and transitions that work beautifully in all themes
+ */
+
+/* ===========================================
+ UNIVERSAL KEYFRAME ANIMATIONS
+ =========================================== */
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes slideInUp {
+ from {
+ opacity: 0;
+ transform: translateY(30px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes pulseGlow {
+ 0%, 100% {
+ box-shadow: 0 0 5px currentColor;
+ }
+ 50% {
+ box-shadow: 0 0 20px currentColor, 0 0 30px currentColor;
+ }
+}
+
+@keyframes shimmer {
+ 0% {
+ background-position: -200px 0;
+ }
+ 100% {
+ background-position: 200px 0;
+ }
+}
+
+@keyframes float {
+ 0%, 100% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-5px);
+ }
+}
+
+@keyframes scale {
+ 0%, 100% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.05);
+ }
+}
+
+@keyframes rotate {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+/* ===========================================
+ UNIFIED CARD EFFECTS (All Themes)
+ =========================================== */
+
+.modern-card {
+ background: var(--gradient-card);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-sm);
+ backdrop-filter: var(--glass-blur);
+ transition: all var(--transition-smooth);
+ position: relative;
+ overflow: hidden;
+}
+
+.modern-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, var(--hover-overlay), transparent);
+ transition: left var(--transition-slow);
+ z-index: 1;
+}
+
+.modern-card:hover {
+ transform: translateY(-4px);
+ box-shadow: var(--shadow-xl);
+ border-color: var(--glass-border-strong);
+}
+
+.modern-card:hover::before {
+ left: 100%;
+}
+
+/* Theme-specific card enhancements */
+.theme-light .modern-card:hover {
+ box-shadow: var(--shadow-lg), 0 0 0 1px var(--primary);
+}
+
+.theme-dark .modern-card:hover {
+ box-shadow: var(--shadow-xl), 0 0 20px rgba(255, 255, 255, 0.1);
+}
+
+.theme-advanced .modern-card:hover {
+ box-shadow: var(--shadow-2xl), var(--hover-glow);
+ background: var(--gradient-card);
+}
+
+/* ===========================================
+ UNIFIED BUTTON EFFECTS (All Themes)
+ =========================================== */
+
+.modern-button {
+ background: var(--gradient-primary);
+ color: var(--primary-foreground);
+ border: none;
+ border-radius: var(--radius-md);
+ padding: var(--space-sm) var(--space-lg);
+ font-weight: var(--font-semibold);
+ font-size: var(--font-sm);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ box-shadow: var(--shadow-sm);
+ position: relative;
+ overflow: hidden;
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-xs);
+}
+
+.modern-button::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
+ transition: left var(--transition-smooth);
+}
+
+.modern-button:hover {
+ transform: translateY(-2px) scale(1.02);
+ box-shadow: var(--shadow-lg);
+}
+
+.modern-button:hover::before {
+ left: 100%;
+}
+
+.modern-button:active {
+ transform: translateY(0) scale(var(--active-scale));
+ transition: all 0.1s ease;
+}
+
+/* Secondary button variant */
+.modern-button-secondary {
+ background: var(--gradient-subtle);
+ color: var(--foreground);
+ border: 1px solid var(--glass-border);
+ backdrop-filter: var(--glass-blur);
+}
+
+.modern-button-secondary:hover {
+ background: var(--gradient-accent);
+ border-color: var(--glass-border-strong);
+}
+
+/* ===========================================
+ UNIFIED INPUT EFFECTS (All Themes)
+ =========================================== */
+
+.modern-input {
+ background: var(--glass-bg);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-md);
+ padding: var(--space-sm) var(--space-md);
+ font-size: var(--font-md);
+ color: var(--foreground);
+ transition: all var(--transition-fast);
+ backdrop-filter: var(--glass-blur);
+ box-shadow: var(--shadow-xs);
+ width: 100%;
+}
+
+.modern-input:focus {
+ outline: none;
+ border-color: var(--focus-ring);
+ box-shadow: var(--shadow-md), 0 0 0 3px color-mix(in srgb, var(--focus-ring) 12%, transparent);
+ transform: scale(1.01);
+}
+
+.modern-input::placeholder {
+ color: var(--muted-foreground);
+ opacity: 0.8;
+}
+
+/* ===========================================
+ UNIFIED TAB NAVIGATION (All Themes)
+ =========================================== */
+
+.modern-tabs {
+ display: flex;
+ background: var(--gradient-card);
+ border-radius: var(--radius-lg);
+ padding: var(--space-xs);
+ gap: var(--space-xs);
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--glass-border);
+ backdrop-filter: var(--glass-blur);
+}
+
+.modern-tab {
+ flex: 1;
+ padding: var(--space-sm) var(--space-lg);
+ border: none;
+ background: transparent;
+ color: var(--muted-foreground);
+ cursor: pointer;
+ transition: all var(--transition-smooth);
+ border-radius: var(--radius-md);
+ font-weight: var(--font-medium);
+ font-size: var(--font-sm);
+ position: relative;
+ overflow: hidden;
+}
+
+.modern-tab::before {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ width: 0;
+ height: 2px;
+ background: var(--primary);
+ transition: all var(--transition-smooth);
+ transform: translateX(-50%);
+}
+
+.modern-tab:hover {
+ color: var(--foreground);
+ background: var(--hover-overlay);
+ transform: translateY(-1px);
+}
+
+.modern-tab.active {
+ color: var(--primary-foreground);
+ background: var(--gradient-primary);
+ box-shadow: var(--shadow-md);
+ transform: translateY(-2px);
+}
+
+.modern-tab.active::before {
+ width: 80%;
+}
+
+/* ===========================================
+ UNIFIED NOTIFICATION EFFECTS
+ =========================================== */
+
+.modern-notification {
+ background: var(--gradient-card);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-lg);
+ padding: var(--space-md);
+ box-shadow: var(--shadow-lg);
+ backdrop-filter: var(--glass-blur);
+ animation: slideInUp 0.4s var(--ease-out);
+ position: relative;
+ overflow: hidden;
+}
+
+.modern-notification::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 4px;
+ height: 100%;
+ background: var(--primary);
+}
+
+.modern-notification.success::before {
+ background: var(--status-success);
+}
+
+.modern-notification.warning::before {
+ background: var(--status-warning);
+}
+
+.modern-notification.error::before {
+ background: var(--status-error);
+}
+
+/* ===========================================
+ UNIFIED STATUS INDICATORS
+ =========================================== */
+
+.status-indicator {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-xs);
+ padding: var(--space-xs) var(--space-sm);
+ border-radius: var(--radius-full);
+ font-size: var(--font-xs);
+ font-weight: var(--font-medium);
+ backdrop-filter: var(--glass-blur);
+ transition: all var(--transition-fast);
+}
+
+.status-indicator::before {
+ content: '';
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ animation: pulseGlow 2s infinite;
+}
+
+.status-indicator.success {
+ background: var(--status-success-bg);
+ color: var(--status-success);
+ border: 1px solid var(--status-success-border);
+}
+
+.status-indicator.success::before {
+ background: var(--status-success);
+}
+
+.status-indicator.warning {
+ background: var(--status-warning-bg);
+ color: var(--status-warning);
+ border: 1px solid var(--status-warning-border);
+}
+
+.status-indicator.warning::before {
+ background: var(--status-warning);
+}
+
+.status-indicator.error {
+ background: var(--status-error-bg);
+ color: var(--status-error);
+ border: 1px solid var(--status-error-border);
+}
+
+.status-indicator.error::before {
+ background: var(--status-error);
+}
+
+.status-indicator.info {
+ background: var(--status-info-bg);
+ color: var(--status-info);
+ border: 1px solid var(--status-info-border);
+}
+
+.status-indicator.info::before {
+ background: var(--status-info);
+}
+
+/* ===========================================
+ UNIFIED HOVER EFFECTS
+ =========================================== */
+
+.hover-lift {
+ transition: all var(--transition-smooth);
+}
+
+.hover-lift:hover {
+ transform: translateY(-4px);
+ box-shadow: var(--shadow-xl);
+}
+
+.hover-glow {
+ transition: all var(--transition-smooth);
+}
+
+.hover-glow:hover {
+ box-shadow: var(--shadow-lg), var(--shadow-glow);
+}
+
+.hover-scale {
+ transition: all var(--transition-fast);
+}
+
+.hover-scale:hover {
+ transform: scale(1.05);
+}
+
+.hover-rotate {
+ transition: all var(--transition-smooth);
+}
+
+.hover-rotate:hover {
+ transform: rotate(5deg);
+}
+
+/* ===========================================
+ UNIFIED LOADING STATES
+ =========================================== */
+
+.loading-shimmer {
+ background: linear-gradient(90deg, var(--muted) 25%, var(--accent) 50%, var(--muted) 75%);
+ background-size: 200px 100%;
+ animation: shimmer 1.5s infinite;
+}
+
+.loading-pulse {
+ animation: scale 2s infinite ease-in-out;
+}
+
+.loading-spin {
+ animation: rotate 1s linear infinite;
+}
+
+.loading-float {
+ animation: float 3s ease-in-out infinite;
+}
+
+/* ===========================================
+ THEME-SPECIFIC ENHANCEMENTS
+ =========================================== */
+
+/* Light Theme - Professional touches */
+.theme-light .modern-card {
+ box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.1);
+}
+
+.theme-light .modern-button {
+ box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.2);
+}
+
+/* Dark Theme - Elegant enhancements */
+.theme-dark .modern-card {
+ box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255, 255, 255, 0.05);
+}
+
+.theme-dark .modern-button {
+ box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255, 255, 255, 0.1);
+}
+
+/* Advanced Theme - Spectacular effects */
+.theme-advanced .modern-card {
+ box-shadow: var(--shadow-lg), var(--shadow-glow);
+ background: var(--gradient-card), var(--nebula-glow);
+}
+
+.theme-advanced .modern-button {
+ box-shadow: var(--shadow-lg), 0 0 20px rgba(123, 97, 255, 0.3);
+ background: var(--gradient-primary), var(--gradient-cosmic);
+}
+
+.theme-advanced .modern-card:hover,
+.theme-advanced .modern-button:hover,
+.theme-advanced .modern-tabs:hover {
+ box-shadow: var(--shadow-2xl), var(--hover-glow);
+}
+
+/* ===========================================
+ RESPONSIVE DESIGN
+ =========================================== */
+
+@media (max-width: 768px) {
+ .modern-tabs {
+ flex-direction: column;
+ gap: var(--space-xs);
+ }
+
+ .modern-tab {
+ text-align: center;
+ }
+
+ .modern-card {
+ margin: var(--space-sm);
+ }
+
+ .hover-lift:hover,
+ .hover-scale:hover,
+ .hover-rotate:hover {
+ transform: none; /* Disable hover effects on mobile */
+ }
+}
+
+/* ===========================================
+ ACCESSIBILITY & REDUCED MOTION
+ =========================================== */
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+
+ .loading-shimmer,
+ .loading-pulse,
+ .loading-spin,
+ .loading-float {
+ animation: none;
+ }
+}
+
+/* High contrast mode support */
+@media (prefers-contrast: high) {
+ .modern-card,
+ .modern-button,
+ .modern-input,
+ .modern-tabs {
+ border-width: 2px;
+ box-shadow: none;
+ }
+}
\ No newline at end of file
diff --git a/plugins/themes/styles/notifications.css b/plugins/themes/styles/notifications.css
new file mode 100644
index 00000000..75dba818
--- /dev/null
+++ b/plugins/themes/styles/notifications.css
@@ -0,0 +1,670 @@
+/**
+ * 🔔 SISTEM COMPLET NOTIFICĂRI PENTRU TOATE TEMELE
+ * ================================================
+ *
+ * Implementare de 3 variante (Subtle, Standard, Bold) pentru fiecare temă:
+ * - 🌞 LIGHT THEME: Professional cu depth progresiv
+ * - 🌙 DARK THEME: Elegant cu glow effects
+ * - 🌌 ADVANCED THEME: Glassmorphism cosmic spectacular
+ *
+ * Fiecare temă are:
+ * - Variant 1: Subtle (minimalist, discret)
+ * - Variant 2: Standard (echilibrat, profesional)
+ * - Variant 3: Bold (dramatic, high-impact)
+ */
+
+/* ===========================================
+ 🔔 BASE NOTIFICATION SYSTEM - OVERRIDE TOAST
+ =========================================== */
+
+/* Override original Toast component with our themed notifications */
+.notification,
+div[class*="fixed top-4 right-4"] {
+ position: fixed !important;
+ top: 20px !important;
+ right: 20px !important;
+ min-width: 320px !important;
+ max-width: 450px !important;
+ padding: 16px 20px !important;
+ border-radius: var(--radius-lg) !important;
+ font-family: var(--font-sans) !important;
+ font-size: 14px !important;
+ font-weight: 500 !important;
+ line-height: 1.5 !important;
+ z-index: 9999 !important;
+ pointer-events: auto !important;
+ cursor: pointer !important;
+
+ /* Reset Tailwind styles */
+ background: none !important;
+ border: none !important;
+ color: inherit !important;
+ box-shadow: none !important;
+
+ /* Base animation */
+ animation: notificationSlideIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) !important;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
+}
+
+.notification:hover {
+ transform: translateX(-4px) scale(1.02);
+}
+
+/* Stacking multiple notifications */
+.notification:nth-child(2) { top: 90px; }
+.notification:nth-child(3) { top: 160px; }
+.notification:nth-child(4) { top: 230px; }
+
+/* Base animation keyframes */
+@keyframes notificationSlideIn {
+ from {
+ opacity: 0;
+ transform: translateX(100%) scale(0.8);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0) scale(1);
+ }
+}
+
+/* Dismiss animation */
+.notification.dismissing {
+ animation: notificationSlideOut 0.3s cubic-bezier(0.4, 0, 1, 1) forwards;
+}
+
+@keyframes notificationSlideOut {
+ to {
+ opacity: 0;
+ transform: translateX(100%) scale(0.8);
+ }
+}
+
+/* ===========================================
+ 🌞 LIGHT THEME NOTIFICATIONS
+ =========================================== */
+
+/* LIGHT THEME - VARIANT 1: SUBTLE */
+html.theme-light .notification.subtle.success {
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
+ border: 1px solid #0ea5e9;
+ color: #0c4a6e;
+ box-shadow: 0 4px 12px rgba(14, 165, 233, 0.1);
+}
+
+html.theme-light .notification.subtle.error {
+ background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
+ border: 1px solid #f87171;
+ color: #991b1b;
+ box-shadow: 0 4px 12px rgba(239, 68, 68, 0.1);
+}
+
+html.theme-light .notification.subtle.warning {
+ background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
+ border: 1px solid #f59e0b;
+ color: #92400e;
+ box-shadow: 0 4px 12px rgba(245, 158, 11, 0.1);
+}
+
+html.theme-light .notification.subtle.info {
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+ border: 1px solid #64748b;
+ color: #334155;
+ box-shadow: 0 4px 12px rgba(100, 116, 139, 0.1);
+}
+
+/* LIGHT THEME - VARIANT 2: STANDARD */
+html.theme-light .notification.success,
+html.theme-light div[class*="fixed top-4 right-4"]:has(.text-green-500) {
+ background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%) !important;
+ border: 1px solid #16a34a !important;
+ color: #14532d !important;
+ box-shadow: 0 8px 25px rgba(22, 163, 74, 0.2) !important;
+}
+
+html.theme-light .notification.error,
+html.theme-light div[class*="fixed top-4 right-4"]:has(.text-red-500) {
+ background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%) !important;
+ border: 1px solid #dc2626 !important;
+ color: #7f1d1d !important;
+ box-shadow: 0 8px 25px rgba(220, 38, 38, 0.2) !important;
+}
+
+html.theme-light .notification.warning,
+html.theme-light div[class*="fixed top-4 right-4"]:has(.text-yellow-500) {
+ background: linear-gradient(135deg, #fef3c7 0%, #fed7aa 100%) !important;
+ border: 1px solid #d97706 !important;
+ color: #78350f !important;
+ box-shadow: 0 8px 25px rgba(217, 119, 6, 0.2) !important;
+}
+
+html.theme-light .notification.info,
+html.theme-light div[class*="fixed top-4 right-4"]:not(:has(.text-green-500)):not(:has(.text-red-500)):not(:has(.text-yellow-500)) {
+ background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%) !important;
+ border: 1px solid #2563eb !important;
+ color: #1e3a8a !important;
+ box-shadow: 0 8px 25px rgba(37, 99, 235, 0.2) !important;
+}
+
+/* LIGHT THEME - VARIANT 3: BOLD */
+html.theme-light .notification.bold.success {
+ background: linear-gradient(135deg, #16a34a 0%, #15803d 100%);
+ border: 2px solid #22c55e;
+ color: white;
+ box-shadow: 0 12px 35px rgba(22, 163, 74, 0.4);
+}
+
+html.theme-light .notification.bold.error {
+ background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
+ border: 2px solid #ef4444;
+ color: white;
+ box-shadow: 0 12px 35px rgba(220, 38, 38, 0.4);
+}
+
+html.theme-light .notification.bold.warning {
+ background: linear-gradient(135deg, #d97706 0%, #c2410c 100%);
+ border: 2px solid #f59e0b;
+ color: white;
+ box-shadow: 0 12px 35px rgba(217, 119, 6, 0.4);
+}
+
+html.theme-light .notification.bold.info {
+ background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
+ border: 2px solid #3b82f6;
+ color: white;
+ box-shadow: 0 12px 35px rgba(37, 99, 235, 0.4);
+}
+
+/* ===========================================
+ 🌙 DARK THEME NOTIFICATIONS
+ =========================================== */
+
+/* DARK THEME - VARIANT 1: SUBTLE */
+html.theme-dark .notification.subtle.success {
+ background: linear-gradient(135deg, rgba(6, 78, 59, 0.4) 0%, rgba(5, 46, 22, 0.6) 100%);
+ border: 1px solid rgba(34, 197, 94, 0.3);
+ color: #86efac;
+ box-shadow: 0 8px 32px rgba(34, 197, 94, 0.1);
+ backdrop-filter: blur(16px);
+}
+
+html.theme-dark .notification.subtle.error {
+ background: linear-gradient(135deg, rgba(127, 29, 29, 0.4) 0%, rgba(69, 10, 10, 0.6) 100%);
+ border: 1px solid rgba(239, 68, 68, 0.3);
+ color: #fca5a5;
+ box-shadow: 0 8px 32px rgba(239, 68, 68, 0.1);
+ backdrop-filter: blur(16px);
+}
+
+html.theme-dark .notification.subtle.warning {
+ background: linear-gradient(135deg, rgba(120, 53, 15, 0.4) 0%, rgba(69, 26, 3, 0.6) 100%);
+ border: 1px solid rgba(245, 158, 11, 0.3);
+ color: #fbbf24;
+ box-shadow: 0 8px 32px rgba(245, 158, 11, 0.1);
+ backdrop-filter: blur(16px);
+}
+
+html.theme-dark .notification.subtle.info {
+ background: linear-gradient(135deg, rgba(30, 58, 138, 0.4) 0%, rgba(15, 23, 42, 0.6) 100%);
+ border: 1px solid rgba(59, 130, 246, 0.3);
+ color: #93c5fd;
+ box-shadow: 0 8px 32px rgba(59, 130, 246, 0.1);
+ backdrop-filter: blur(16px);
+}
+
+/* DARK THEME - VARIANT 2: STANDARD */
+html.theme-dark .notification.success,
+html.theme-dark div[class*="fixed top-4 right-4"]:has(.text-green-500) {
+ background: linear-gradient(135deg, rgba(6, 78, 59, 0.8) 0%, rgba(5, 46, 22, 0.9) 100%) !important;
+ border: 1px solid #22c55e !important;
+ color: #bbf7d0 !important;
+ box-shadow: 0 8px 32px rgba(34, 197, 94, 0.25), 0 0 20px rgba(34, 197, 94, 0.1) !important;
+ backdrop-filter: blur(20px) !important;
+}
+
+html.theme-dark .notification.error,
+html.theme-dark div[class*="fixed top-4 right-4"]:has(.text-red-500) {
+ background: linear-gradient(135deg, rgba(127, 29, 29, 0.8) 0%, rgba(69, 10, 10, 0.9) 100%) !important;
+ border: 1px solid #ef4444 !important;
+ color: #fecaca !important;
+ box-shadow: 0 8px 32px rgba(239, 68, 68, 0.25), 0 0 20px rgba(239, 68, 68, 0.1) !important;
+ backdrop-filter: blur(20px) !important;
+}
+
+html.theme-dark .notification.warning,
+html.theme-dark div[class*="fixed top-4 right-4"]:has(.text-yellow-500) {
+ background: linear-gradient(135deg, rgba(120, 53, 15, 0.8) 0%, rgba(69, 26, 3, 0.9) 100%) !important;
+ border: 1px solid #f59e0b !important;
+ color: #fde68a !important;
+ box-shadow: 0 8px 32px rgba(245, 158, 11, 0.25), 0 0 20px rgba(245, 158, 11, 0.1) !important;
+ backdrop-filter: blur(20px) !important;
+}
+
+html.theme-dark .notification.info,
+html.theme-dark div[class*="fixed top-4 right-4"]:not(:has(.text-green-500)):not(:has(.text-red-500)):not(:has(.text-yellow-500)) {
+ background: linear-gradient(135deg, rgba(30, 58, 138, 0.8) 0%, rgba(15, 23, 42, 0.9) 100%) !important;
+ border: 1px solid #3b82f6 !important;
+ color: #dbeafe !important;
+ box-shadow: 0 8px 32px rgba(59, 130, 246, 0.25), 0 0 20px rgba(59, 130, 246, 0.1) !important;
+ backdrop-filter: blur(20px) !important;
+}
+
+/* DARK THEME - VARIANT 3: BOLD */
+html.theme-dark .notification.bold.success {
+ background: linear-gradient(135deg, #16a34a 0%, #15803d 100%);
+ border: 2px solid #22c55e;
+ color: white;
+ box-shadow:
+ 0 8px 32px rgba(34, 197, 94, 0.4),
+ 0 0 40px rgba(34, 197, 94, 0.2),
+ inset 0 1px 0 rgba(255, 255, 255, 0.2);
+ backdrop-filter: blur(24px);
+}
+
+html.theme-dark .notification.bold.error {
+ background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
+ border: 2px solid #ef4444;
+ color: white;
+ box-shadow:
+ 0 8px 32px rgba(239, 68, 68, 0.4),
+ 0 0 40px rgba(239, 68, 68, 0.2),
+ inset 0 1px 0 rgba(255, 255, 255, 0.2);
+ backdrop-filter: blur(24px);
+}
+
+html.theme-dark .notification.bold.warning {
+ background: linear-gradient(135deg, #d97706 0%, #c2410c 100%);
+ border: 2px solid #f59e0b;
+ color: white;
+ box-shadow:
+ 0 8px 32px rgba(245, 158, 11, 0.4),
+ 0 0 40px rgba(245, 158, 11, 0.2),
+ inset 0 1px 0 rgba(255, 255, 255, 0.2);
+ backdrop-filter: blur(24px);
+}
+
+html.theme-dark .notification.bold.info {
+ background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
+ border: 2px solid #3b82f6;
+ color: white;
+ box-shadow:
+ 0 8px 32px rgba(59, 130, 246, 0.4),
+ 0 0 40px rgba(59, 130, 246, 0.2),
+ inset 0 1px 0 rgba(255, 255, 255, 0.2);
+ backdrop-filter: blur(24px);
+}
+
+/* ===========================================
+ 🌌 ADVANCED THEME NOTIFICATIONS (GLASSMORPHISM COSMIC)
+ =========================================== */
+
+/* ADVANCED THEME - VARIANT 1: SUBTLE */
+html.theme-advanced .notification.subtle.success {
+ background: linear-gradient(135deg,
+ rgba(6, 78, 59, 0.15) 0%,
+ rgba(5, 46, 22, 0.25) 50%,
+ rgba(21, 128, 61, 0.2) 100%
+ );
+ border: 1px solid rgba(34, 197, 94, 0.2);
+ color: #86efac;
+ box-shadow:
+ 0 8px 32px rgba(34, 197, 94, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(20px) saturate(180%);
+}
+
+html.theme-advanced .notification.subtle.error {
+ background: linear-gradient(135deg,
+ rgba(127, 29, 29, 0.15) 0%,
+ rgba(69, 10, 10, 0.25) 50%,
+ rgba(185, 28, 28, 0.2) 100%
+ );
+ border: 1px solid rgba(239, 68, 68, 0.2);
+ color: #fca5a5;
+ box-shadow:
+ 0 8px 32px rgba(239, 68, 68, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(20px) saturate(180%);
+}
+
+html.theme-advanced .notification.subtle.warning {
+ background: linear-gradient(135deg,
+ rgba(120, 53, 15, 0.15) 0%,
+ rgba(69, 26, 3, 0.25) 50%,
+ rgba(194, 65, 12, 0.2) 100%
+ );
+ border: 1px solid rgba(245, 158, 11, 0.2);
+ color: #fbbf24;
+ box-shadow:
+ 0 8px 32px rgba(245, 158, 11, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(20px) saturate(180%);
+}
+
+html.theme-advanced .notification.subtle.info {
+ background: linear-gradient(135deg,
+ rgba(30, 58, 138, 0.15) 0%,
+ rgba(15, 23, 42, 0.25) 50%,
+ rgba(29, 78, 216, 0.2) 100%
+ );
+ border: 1px solid rgba(59, 130, 246, 0.2);
+ color: #93c5fd;
+ box-shadow:
+ 0 8px 32px rgba(59, 130, 246, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(20px) saturate(180%);
+}
+
+/* ADVANCED THEME - VARIANT 2: STANDARD */
+html.theme-advanced .notification.success,
+html.theme-advanced div[class*="fixed top-4 right-4"]:has(.text-green-500) {
+ background: linear-gradient(135deg,
+ rgba(6, 78, 59, 0.3) 0%,
+ rgba(5, 46, 22, 0.5) 50%,
+ rgba(21, 128, 61, 0.4) 100%
+ ) !important;
+ border: 1px solid rgba(34, 197, 94, 0.4) !important;
+ color: #bbf7d0 !important;
+ box-shadow:
+ 0 8px 32px rgba(34, 197, 94, 0.2),
+ 0 0 60px rgba(34, 197, 94, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.2) !important;
+ backdrop-filter: blur(24px) saturate(200%) !important;
+}
+
+html.theme-advanced .notification.success::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(135deg,
+ rgba(34, 197, 94, 0.1) 0%,
+ transparent 50%,
+ rgba(34, 197, 94, 0.05) 100%
+ );
+ border-radius: inherit;
+ pointer-events: none;
+}
+
+html.theme-advanced .notification.error,
+html.theme-advanced div[class*="fixed top-4 right-4"]:has(.text-red-500) {
+ background: linear-gradient(135deg,
+ rgba(127, 29, 29, 0.3) 0%,
+ rgba(69, 10, 10, 0.5) 50%,
+ rgba(185, 28, 28, 0.4) 100%
+ ) !important;
+ border: 1px solid rgba(239, 68, 68, 0.4) !important;
+ color: #fecaca !important;
+ box-shadow:
+ 0 8px 32px rgba(239, 68, 68, 0.2),
+ 0 0 60px rgba(239, 68, 68, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.2) !important;
+ backdrop-filter: blur(24px) saturate(200%) !important;
+}
+
+html.theme-advanced .notification.error::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(135deg,
+ rgba(239, 68, 68, 0.1) 0%,
+ transparent 50%,
+ rgba(239, 68, 68, 0.05) 100%
+ );
+ border-radius: inherit;
+ pointer-events: none;
+}
+
+html.theme-advanced .notification.warning,
+html.theme-advanced div[class*="fixed top-4 right-4"]:has(.text-yellow-500) {
+ background: linear-gradient(135deg,
+ rgba(120, 53, 15, 0.3) 0%,
+ rgba(69, 26, 3, 0.5) 50%,
+ rgba(194, 65, 12, 0.4) 100%
+ ) !important;
+ border: 1px solid rgba(245, 158, 11, 0.4) !important;
+ color: #fde68a !important;
+ box-shadow:
+ 0 8px 32px rgba(245, 158, 11, 0.2),
+ 0 0 60px rgba(245, 158, 11, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.2) !important;
+ backdrop-filter: blur(24px) saturate(200%) !important;
+}
+
+html.theme-advanced .notification.warning::before,
+html.theme-advanced div[class*="fixed top-4 right-4"]:has(.text-yellow-500)::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(135deg,
+ rgba(245, 158, 11, 0.1) 0%,
+ transparent 50%,
+ rgba(245, 158, 11, 0.05) 100%
+ );
+ border-radius: inherit;
+ pointer-events: none;
+}
+
+html.theme-advanced .notification.info,
+html.theme-advanced div[class*="fixed top-4 right-4"]:not(:has(.text-green-500)):not(:has(.text-red-500)):not(:has(.text-yellow-500)) {
+ background: linear-gradient(135deg,
+ rgba(30, 58, 138, 0.3) 0%,
+ rgba(15, 23, 42, 0.5) 50%,
+ rgba(29, 78, 216, 0.4) 100%
+ ) !important;
+ border: 1px solid rgba(59, 130, 246, 0.4) !important;
+ color: #dbeafe !important;
+ box-shadow:
+ 0 8px 32px rgba(59, 130, 246, 0.2),
+ 0 0 60px rgba(59, 130, 246, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.2) !important;
+ backdrop-filter: blur(24px) saturate(200%) !important;
+}
+
+html.theme-advanced .notification.info::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(135deg,
+ rgba(59, 130, 246, 0.1) 0%,
+ transparent 50%,
+ rgba(59, 130, 246, 0.05) 100%
+ );
+ border-radius: inherit;
+ pointer-events: none;
+}
+
+/* ADVANCED THEME - VARIANT 3: BOLD (SPECTACULAR COSMIC) */
+html.theme-advanced .notification.bold.success {
+ background: linear-gradient(135deg,
+ rgba(34, 197, 94, 0.8) 0%,
+ rgba(21, 128, 61, 0.9) 50%,
+ rgba(5, 46, 22, 0.95) 100%
+ );
+ border: 2px solid rgba(34, 197, 94, 0.8);
+ color: white;
+ box-shadow:
+ 0 8px 32px rgba(34, 197, 94, 0.4),
+ 0 0 80px rgba(34, 197, 94, 0.3),
+ 0 0 120px rgba(34, 197, 94, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.3),
+ inset 0 -1px 0 rgba(0, 0, 0, 0.2);
+ backdrop-filter: blur(32px) saturate(250%);
+}
+
+html.theme-advanced .notification.bold.success::before {
+ content: '';
+ position: absolute;
+ inset: -2px;
+ background: conic-gradient(
+ from 0deg,
+ rgba(34, 197, 94, 0.5) 0%,
+ rgba(22, 163, 74, 0.3) 25%,
+ rgba(34, 197, 94, 0.5) 50%,
+ rgba(22, 163, 74, 0.3) 75%,
+ rgba(34, 197, 94, 0.5) 100%
+ );
+ border-radius: calc(var(--radius-lg) + 2px);
+ z-index: -1;
+ animation: cosmicRotate 8s linear infinite;
+}
+
+html.theme-advanced .notification.bold.error {
+ background: linear-gradient(135deg,
+ rgba(239, 68, 68, 0.8) 0%,
+ rgba(185, 28, 28, 0.9) 50%,
+ rgba(69, 10, 10, 0.95) 100%
+ );
+ border: 2px solid rgba(239, 68, 68, 0.8);
+ color: white;
+ box-shadow:
+ 0 8px 32px rgba(239, 68, 68, 0.4),
+ 0 0 80px rgba(239, 68, 68, 0.3),
+ 0 0 120px rgba(239, 68, 68, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.3),
+ inset 0 -1px 0 rgba(0, 0, 0, 0.2);
+ backdrop-filter: blur(32px) saturate(250%);
+}
+
+html.theme-advanced .notification.bold.error::before {
+ content: '';
+ position: absolute;
+ inset: -2px;
+ background: conic-gradient(
+ from 0deg,
+ rgba(239, 68, 68, 0.5) 0%,
+ rgba(220, 38, 38, 0.3) 25%,
+ rgba(239, 68, 68, 0.5) 50%,
+ rgba(220, 38, 38, 0.3) 75%,
+ rgba(239, 68, 68, 0.5) 100%
+ );
+ border-radius: calc(var(--radius-lg) + 2px);
+ z-index: -1;
+ animation: cosmicRotate 8s linear infinite;
+}
+
+html.theme-advanced .notification.bold.warning {
+ background: linear-gradient(135deg,
+ rgba(245, 158, 11, 0.8) 0%,
+ rgba(194, 65, 12, 0.9) 50%,
+ rgba(69, 26, 3, 0.95) 100%
+ );
+ border: 2px solid rgba(245, 158, 11, 0.8);
+ color: white;
+ box-shadow:
+ 0 8px 32px rgba(245, 158, 11, 0.4),
+ 0 0 80px rgba(245, 158, 11, 0.3),
+ 0 0 120px rgba(245, 158, 11, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.3),
+ inset 0 -1px 0 rgba(0, 0, 0, 0.2);
+ backdrop-filter: blur(32px) saturate(250%);
+}
+
+html.theme-advanced .notification.bold.warning::before {
+ content: '';
+ position: absolute;
+ inset: -2px;
+ background: conic-gradient(
+ from 0deg,
+ rgba(245, 158, 11, 0.5) 0%,
+ rgba(217, 119, 6, 0.3) 25%,
+ rgba(245, 158, 11, 0.5) 50%,
+ rgba(217, 119, 6, 0.3) 75%,
+ rgba(245, 158, 11, 0.5) 100%
+ );
+ border-radius: calc(var(--radius-lg) + 2px);
+ z-index: -1;
+ animation: cosmicRotate 8s linear infinite;
+}
+
+html.theme-advanced .notification.bold.info {
+ background: linear-gradient(135deg,
+ rgba(59, 130, 246, 0.8) 0%,
+ rgba(29, 78, 216, 0.9) 50%,
+ rgba(15, 23, 42, 0.95) 100%
+ );
+ border: 2px solid rgba(59, 130, 246, 0.8);
+ color: white;
+ box-shadow:
+ 0 8px 32px rgba(59, 130, 246, 0.4),
+ 0 0 80px rgba(59, 130, 246, 0.3),
+ 0 0 120px rgba(59, 130, 246, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.3),
+ inset 0 -1px 0 rgba(0, 0, 0, 0.2);
+ backdrop-filter: blur(32px) saturate(250%);
+}
+
+html.theme-advanced .notification.bold.info::before {
+ content: '';
+ position: absolute;
+ inset: -2px;
+ background: conic-gradient(
+ from 0deg,
+ rgba(59, 130, 246, 0.5) 0%,
+ rgba(37, 99, 235, 0.3) 25%,
+ rgba(59, 130, 246, 0.5) 50%,
+ rgba(37, 99, 235, 0.3) 75%,
+ rgba(59, 130, 246, 0.5) 100%
+ );
+ border-radius: calc(var(--radius-lg) + 2px);
+ z-index: -1;
+ animation: cosmicRotate 8s linear infinite;
+}
+
+/* Cosmic rotation animation for bold advanced notifications */
+@keyframes cosmicRotate {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+/* ===========================================
+ 📱 RESPONSIVE DESIGN
+ =========================================== */
+
+@media (max-width: 768px) {
+ .notification {
+ left: 10px;
+ right: 10px;
+ top: 10px;
+ min-width: auto;
+ max-width: none;
+ font-size: 13px;
+ }
+
+ .notification:nth-child(2) { top: 70px; }
+ .notification:nth-child(3) { top: 130px; }
+ .notification:nth-child(4) { top: 190px; }
+}
+
+/* ===========================================
+ ♿ ACCESSIBILITY ENHANCEMENTS
+ =========================================== */
+
+@media (prefers-reduced-motion: reduce) {
+ .notification {
+ animation: none;
+ transition: opacity 0.2s ease;
+ }
+
+ html.theme-advanced .notification.bold::before {
+ animation: none;
+ }
+}
+
+@media (prefers-contrast: high) {
+ .notification {
+ border-width: 2px !important;
+ }
+
+ html.theme-light .notification.subtle,
+ html.theme-dark .notification.subtle,
+ html.theme-advanced .notification.subtle {
+ backdrop-filter: none;
+ background: var(--background) !important;
+ border-color: currentColor !important;
+ }
+}
+
+/* Focus states for keyboard navigation */
+.notification:focus {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: 2px;
+}
\ No newline at end of file
diff --git a/plugins/themes/styles/themes.css b/plugins/themes/styles/themes.css
new file mode 100644
index 00000000..5f2d210c
--- /dev/null
+++ b/plugins/themes/styles/themes.css
@@ -0,0 +1,270 @@
+/**
+ * Unified Theme System v2.0 - Main Entry Point
+ * Revolutionary redesign for consistent modern experience across all 3 themes:
+ *
+ * 🎨 LIGHT THEME: Professional & clean with sophisticated gradients and shadows
+ * 🌙 DARK THEME: Elegant & sophisticated with enhanced blur effects and depth
+ * 🌌 ADVANCED THEME: Spectacular glassmorphism space with cosmic visual effects
+ *
+ * ✨ KEY IMPROVEMENTS:
+ * - Same level of visual sophistication for all themes
+ * - Unified component styling using CSS variables
+ * - Consistent hover effects, animations, and interactions
+ * - No theme-specific logic in components
+ * - Modern design patterns that work beautifully in all themes
+ */
+
+/* Import unified system files */
+@import './variables.css'; /* Theme-specific color variables and design tokens */
+@import './modern-effects.css'; /* Unified visual effects and animations */
+@import './components.css'; /* Consistent component styling */
+@import './notifications.css'; /* Advanced notification system for all themes */
+@import './centered-layout.css'; /* Centered layout system for modern design */
+
+/* ===========================================
+ THEME SYSTEM ACTIVATION
+ =========================================== */
+
+/* Plugin activation marker */
+.themes-plugin-active {
+ --themes-plugin-enabled: 1;
+}
+
+/* Universal base styling that works across all themes */
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ background: var(--background);
+ color: var(--foreground);
+ transition: background-color var(--transition-smooth), color var(--transition-smooth);
+ font-family: system-ui, -apple-system, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
+ line-height: 1.6;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* ===========================================
+ THEME-SPECIFIC BODY BACKGROUNDS
+ =========================================== */
+
+/* Light Theme Body */
+html.theme-light body {
+ background: var(--background);
+ min-height: 100vh;
+}
+
+/* Dark Theme Body */
+html.theme-dark body {
+ background: var(--background);
+ min-height: 100vh;
+}
+
+/* Advanced Theme Body - Spectacular Space Background */
+html.theme-advanced body {
+ background: var(--space-bg);
+ background-attachment: fixed;
+ min-height: 100vh;
+ position: relative;
+}
+
+/* Advanced theme cosmic overlay effects */
+html.theme-advanced body::before {
+ content: '';
+ position: fixed;
+ inset: 0;
+ background: var(--nebula-glow);
+ pointer-events: none;
+ z-index: -1;
+}
+
+/* ===========================================
+ THEME-AWARE COMPONENT OVERRIDES
+ =========================================== */
+
+/* Ensure all major layout components use theme variables */
+main,
+.main-content,
+.app-container {
+ background: var(--background);
+ color: var(--foreground);
+ transition: all var(--transition-smooth);
+}
+
+/* Headers and navigation */
+header,
+nav,
+.header,
+.navigation {
+ background: var(--gradient-card);
+ border-bottom: 1px solid var(--glass-border);
+ backdrop-filter: var(--glass-blur);
+ box-shadow: var(--shadow-sm);
+}
+
+/* Cards and containers */
+.card,
+.container,
+.panel {
+ background: var(--gradient-card);
+ border: 1px solid var(--glass-border);
+ box-shadow: var(--shadow-sm);
+ backdrop-filter: var(--glass-blur);
+}
+
+/* Buttons inherit theme variables */
+button:not(.custom-button) {
+ background: var(--gradient-primary);
+ color: var(--primary-foreground);
+ border: none;
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-sm);
+ transition: all var(--transition-fast);
+}
+
+button:not(.custom-button):hover {
+ box-shadow: var(--shadow-lg);
+ transform: translateY(-1px);
+}
+
+/* Input elements */
+input,
+textarea,
+select {
+ background: var(--glass-bg);
+ border: 1px solid var(--glass-border);
+ color: var(--foreground);
+ border-radius: var(--radius-md);
+ backdrop-filter: var(--glass-blur);
+ transition: all var(--transition-fast);
+}
+
+input:focus,
+textarea:focus,
+select:focus {
+ border-color: var(--focus-ring);
+ box-shadow: var(--shadow-md), 0 0 0 3px color-mix(in srgb, var(--focus-ring) 12%, transparent);
+ outline: none;
+}
+
+/* ===========================================
+ LEGACY COMPATIBILITY OVERRIDES
+ =========================================== */
+
+/* Override common Tailwind/Bootstrap classes to use theme variables */
+.bg-white,
+.bg-gray-50,
+.bg-gray-100 {
+ background: var(--gradient-card) !important;
+ color: var(--card-foreground) !important;
+}
+
+.text-gray-900,
+.text-gray-800,
+.text-gray-700 {
+ color: var(--foreground) !important;
+}
+
+.text-gray-600,
+.text-gray-500,
+.text-gray-400 {
+ color: var(--muted-foreground) !important;
+}
+
+.border-gray-300,
+.border-gray-200 {
+ border-color: var(--glass-border) !important;
+}
+
+/* Rounded corners */
+.rounded,
+.rounded-md,
+.rounded-lg {
+ border-radius: var(--radius-lg) !important;
+}
+
+.rounded-xl {
+ border-radius: var(--radius-xl) !important;
+}
+
+/* ===========================================
+ RESPONSIVE DESIGN ENHANCEMENTS
+ =========================================== */
+
+@media (max-width: 768px) {
+ /* Reduce blur effects on mobile for better performance */
+ html.theme-light {
+ --glass-blur: blur(4px);
+ }
+
+ html.theme-dark {
+ --glass-blur: blur(8px);
+ }
+
+ html.theme-advanced {
+ --glass-blur: blur(12px);
+ }
+
+ /* Simplify advanced theme background on mobile */
+ html.theme-advanced body::before {
+ opacity: 0.5;
+ }
+}
+
+/* ===========================================
+ ACCESSIBILITY ENHANCEMENTS
+ =========================================== */
+
+/* High contrast mode support */
+@media (prefers-contrast: high) {
+ :root {
+ --glass-blur: none;
+ }
+
+ .card,
+ button,
+ input,
+ textarea,
+ select {
+ border-width: 2px !important;
+ }
+}
+
+/* Reduced motion support */
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+
+ html {
+ scroll-behavior: auto;
+ }
+}
+
+/* ===========================================
+ PRINT STYLES
+ =========================================== */
+
+@media print {
+ html.theme-light,
+ html.theme-dark,
+ html.theme-advanced {
+ --background: white;
+ --foreground: black;
+ --card: white;
+ --glass-border: #ccc;
+ --glass-blur: none;
+ }
+
+ .card,
+ button,
+ input {
+ box-shadow: none !important;
+ backdrop-filter: none !important;
+ }
+}
\ No newline at end of file
diff --git a/plugins/themes/styles/variables.css b/plugins/themes/styles/variables.css
new file mode 100644
index 00000000..d324e687
--- /dev/null
+++ b/plugins/themes/styles/variables.css
@@ -0,0 +1,303 @@
+/**
+ * Unified Theme Variables System v2.0
+ * Complete restructure for consistent modern experience across all 3 themes
+ * - Light: Professional & clean with subtle shadows and gradients
+ * - Dark: Elegant & sophisticated with enhanced blur effects
+ * - Advanced: Spectacular glassmorphism space theme with cosmic elements
+ */
+
+/* ===========================================
+ BASE SYSTEM VARIABLES (All Themes)
+ =========================================== */
+
+:root {
+ /* Modern Border Radius System */
+ --radius-xs: 0.25rem; /* 4px - small badges */
+ --radius-sm: 0.375rem; /* 6px - small buttons */
+ --radius-md: 0.5rem; /* 8px - inputs, regular buttons */
+ --radius-lg: 0.75rem; /* 12px - cards, containers */
+ --radius-xl: 1rem; /* 16px - modals, large cards */
+ --radius-2xl: 1.5rem; /* 24px - special components */
+ --radius-full: 9999px; /* full circles */
+
+ /* Consistent Spacing System */
+ --space-xs: 0.25rem; /* 4px */
+ --space-sm: 0.5rem; /* 8px */
+ --space-md: 1rem; /* 16px */
+ --space-lg: 1.5rem; /* 24px */
+ --space-xl: 2rem; /* 32px */
+ --space-2xl: 3rem; /* 48px */
+ --space-3xl: 4rem; /* 64px */
+
+ /* Typography Scale */
+ --font-xs: 0.75rem; /* 12px */
+ --font-sm: 0.875rem; /* 14px */
+ --font-md: 1rem; /* 16px */
+ --font-lg: 1.125rem; /* 18px */
+ --font-xl: 1.25rem; /* 20px */
+ --font-2xl: 1.5rem; /* 24px */
+ --font-3xl: 1.875rem; /* 30px */
+ --font-4xl: 2.25rem; /* 36px */
+
+ /* Font Weights */
+ --font-normal: 400;
+ --font-medium: 500;
+ --font-semibold: 600;
+ --font-bold: 700;
+ --font-black: 900;
+
+ /* Universal Transitions */
+ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-smooth: 300ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-slow: 500ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-bounce: 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55);
+
+ /* Universal Easing Functions */
+ --ease-in: cubic-bezier(0.4, 0, 1, 1);
+ --ease-out: cubic-bezier(0, 0, 0.2, 1);
+ --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
+ --ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
+
+ /* Z-index Scale */
+ --z-dropdown: 1000;
+ --z-sticky: 1020;
+ --z-fixed: 1030;
+ --z-modal-backdrop: 1040;
+ --z-modal: 1050;
+ --z-popover: 1060;
+ --z-tooltip: 1070;
+ --z-toast: 1080;
+}
+
+/* ===========================================
+ LIGHT THEME - Professional & Modern
+ =========================================== */
+
+html.theme-light {
+ /* Core Semantic Colors */
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --destructive-foreground: oklch(0.985 0 0);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+
+ /* Professional Shadow System */
+ --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
+ --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.06);
+ --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.05);
+ --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1), 0 8px 10px rgba(0, 0, 0, 0.04);
+ --shadow-2xl: 0 25px 50px rgba(0, 0, 0, 0.15);
+ --shadow-glow: 0 0 20px rgba(0, 0, 0, 0.1);
+
+ /* Sophisticated Gradients */
+ --gradient-primary: linear-gradient(135deg, oklch(0.205 0 0), oklch(0.305 0 0));
+ --gradient-card: linear-gradient(135deg, oklch(1 0 0), oklch(0.98 0 0));
+ --gradient-subtle: linear-gradient(135deg, oklch(0.97 0 0), oklch(0.95 0 0));
+ --gradient-accent: linear-gradient(135deg, oklch(0.97 0 0) 0%, oklch(0.94 0 0) 100%);
+ --gradient-hero: linear-gradient(135deg, oklch(0.98 0 0) 0%, oklch(0.96 0 0) 100%);
+
+ /* Modern Blur & Glass Effects */
+ --glass-blur: blur(8px);
+ --glass-bg: rgba(255, 255, 255, 0.8);
+ --glass-bg-strong: rgba(255, 255, 255, 0.9);
+ --glass-border: rgba(0, 0, 0, 0.1);
+ --glass-border-strong: rgba(0, 0, 0, 0.15);
+
+ /* Interactive States */
+ --hover-overlay: rgba(0, 0, 0, 0.02);
+ --focus-ring: oklch(0.708 0 0);
+ --active-scale: 0.98;
+
+ /* Status Colors - Professional */
+ --status-success: oklch(0.65 0.2 145);
+ --status-success-bg: oklch(0.65 0.2 145 / 0.1);
+ --status-success-border: oklch(0.65 0.2 145 / 0.3);
+ --status-warning: oklch(0.75 0.15 85);
+ --status-warning-bg: oklch(0.75 0.15 85 / 0.1);
+ --status-warning-border: oklch(0.75 0.15 85 / 0.3);
+ --status-error: oklch(0.65 0.2 25);
+ --status-error-bg: oklch(0.65 0.2 25 / 0.1);
+ --status-error-border: oklch(0.65 0.2 25 / 0.3);
+ --status-info: oklch(0.6 0.2 220);
+ --status-info-bg: oklch(0.6 0.2 220 / 0.1);
+ --status-info-border: oklch(0.6 0.2 220 / 0.3);
+}
+
+/* ===========================================
+ DARK THEME - Elegant & Sophisticated
+ =========================================== */
+
+html.theme-dark {
+ /* Core Semantic Colors */
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --destructive-foreground: oklch(0.985 0 0);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+
+ /* Enhanced Shadow System */
+ --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.25);
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
+ --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4), 0 2px 4px rgba(0, 0, 0, 0.25);
+ --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5), 0 4px 6px rgba(0, 0, 0, 0.3);
+ --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.6), 0 8px 10px rgba(0, 0, 0, 0.4);
+ --shadow-2xl: 0 25px 50px rgba(0, 0, 0, 0.7);
+ --shadow-glow: 0 0 20px rgba(255, 255, 255, 0.1);
+
+ /* Elegant Gradients */
+ --gradient-primary: linear-gradient(135deg, oklch(0.922 0 0), oklch(0.822 0 0));
+ --gradient-card: linear-gradient(135deg, oklch(0.205 0 0), oklch(0.225 0 0));
+ --gradient-subtle: linear-gradient(135deg, oklch(0.269 0 0), oklch(0.289 0 0));
+ --gradient-accent: linear-gradient(135deg, oklch(0.269 0 0) 0%, oklch(0.32 0 0) 100%);
+ --gradient-hero: linear-gradient(135deg, oklch(0.16 0 0) 0%, oklch(0.2 0 0) 100%);
+
+ /* Modern Blur & Glass Effects */
+ --glass-blur: blur(12px);
+ --glass-bg: rgba(255, 255, 255, 0.1);
+ --glass-bg-strong: rgba(255, 255, 255, 0.15);
+ --glass-border: rgba(255, 255, 255, 0.2);
+ --glass-border-strong: rgba(255, 255, 255, 0.25);
+
+ /* Interactive States */
+ --hover-overlay: rgba(255, 255, 255, 0.05);
+ --focus-ring: oklch(0.556 0 0);
+ --active-scale: 0.98;
+
+ /* Status Colors - Elegant */
+ --status-success: oklch(0.7 0.25 150);
+ --status-success-bg: oklch(0.7 0.25 150 / 0.15);
+ --status-success-border: oklch(0.7 0.25 150 / 0.4);
+ --status-warning: oklch(0.8 0.2 90);
+ --status-warning-bg: oklch(0.8 0.2 90 / 0.15);
+ --status-warning-border: oklch(0.8 0.2 90 / 0.4);
+ --status-error: oklch(0.75 0.25 30);
+ --status-error-bg: oklch(0.75 0.25 30 / 0.15);
+ --status-error-border: oklch(0.75 0.25 30 / 0.4);
+ --status-info: oklch(0.65 0.25 230);
+ --status-info-bg: oklch(0.65 0.25 230 / 0.15);
+ --status-info-border: oklch(0.65 0.25 230 / 0.4);
+}
+
+/* ===========================================
+ ADVANCED THEME - Spectacular Glassmorphism Space
+ =========================================== */
+
+html.theme-advanced {
+ /* Core Semantic Colors - Cosmic Space - Further Enhanced Readability */
+ --background: oklch(0.08 0.05 280);
+ --foreground: oklch(0.95 0.02 270);
+ --card: rgba(255, 255, 255, 0.25);
+ --card-foreground: oklch(0.92 0.02 270);
+ --popover: rgba(255, 255, 255, 0.35);
+ --popover-foreground: oklch(0.95 0.02 270);
+ --primary: oklch(0.7 0.25 280);
+ --primary-foreground: oklch(0.08 0.05 280);
+ --secondary: rgba(255, 255, 255, 0.35);
+ --secondary-foreground: oklch(0.85 0.03 270);
+ --muted: rgba(255, 255, 255, 0.23);
+ --muted-foreground: oklch(0.65 0.05 280);
+ --accent: oklch(0.75 0.2 320);
+ --accent-foreground: oklch(0.08 0.05 280);
+ --destructive: oklch(0.7 0.3 20);
+ --destructive-foreground: oklch(0.95 0.02 270);
+ --border: rgba(255, 255, 255, 0.47);
+ --input: rgba(255, 255, 255, 0.3);
+ --ring: oklch(0.7 0.25 280);
+
+ /* Special Modal Variables - Cosmic but Readable */
+ --modal-bg: linear-gradient(135deg, rgba(20, 15, 45, 0.92) 0%, rgba(25, 20, 55, 0.88) 50%, rgba(20, 15, 45, 0.92) 100%);
+ --modal-overlay: linear-gradient(135deg, rgba(0, 0, 0, 0.85) 0%, rgba(20, 20, 40, 0.9) 50%, rgba(0, 0, 0, 0.85) 100%);
+ --modal-border: rgba(123, 97, 255, 0.4);
+ --modal-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.6), 0 0 64px rgba(123, 97, 255, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1);
+
+ /* Spectacular Space Background */
+ --space-bg: linear-gradient(135deg, oklch(0.08 0.05 280) 0%, oklch(0.12 0.08 260) 35%, oklch(0.1 0.06 300) 70%, oklch(0.09 0.07 320) 100%);
+ --nebula-glow: radial-gradient(circle at 25% 25%, rgba(123, 97, 255, 0.3) 0%, transparent 50%), radial-gradient(circle at 75% 75%, rgba(255, 64, 129, 0.2) 0%, transparent 50%);
+
+ /* Cosmic Shadow System with Glow */
+ --shadow-xs: 0 1px 3px rgba(0, 0, 0, 0.4), 0 0 8px rgba(123, 97, 255, 0.1);
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.5), 0 0 16px rgba(123, 97, 255, 0.15);
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.6), 0 0 24px rgba(123, 97, 255, 0.2);
+ --shadow-lg: 0 8px 20px rgba(0, 0, 0, 0.7), 0 0 32px rgba(123, 97, 255, 0.25);
+ --shadow-xl: 0 16px 32px rgba(0, 0, 0, 0.8), 0 0 48px rgba(123, 97, 255, 0.3);
+ --shadow-2xl: 0 32px 64px rgba(0, 0, 0, 0.9), 0 0 64px rgba(123, 97, 255, 0.4);
+ --shadow-glow: 0 0 40px rgba(123, 97, 255, 0.5), 0 0 80px rgba(123, 97, 255, 0.3);
+
+ /* Spectacular Cosmic Gradients - Enhanced for Better Readability */
+ --gradient-primary: linear-gradient(135deg, oklch(0.7 0.25 280), oklch(0.8 0.3 260), oklch(0.75 0.28 300));
+ --gradient-card: linear-gradient(135deg, rgba(255, 255, 255, 0.35), rgba(255, 255, 255, 0.45), rgba(255, 255, 255, 0.38));
+ --gradient-subtle: linear-gradient(135deg, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.35), rgba(255, 255, 255, 0.28));
+ --gradient-accent: linear-gradient(135deg, oklch(0.75 0.2 320) 0%, oklch(0.85 0.25 300) 50%, oklch(0.8 0.23 280) 100%);
+ --gradient-hero: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.3) 50%, rgba(255, 255, 255, 0.25) 100%);
+ --gradient-cosmic: linear-gradient(45deg, #667eea 0%, #764ba2 25%, #f093fb 50%, #f5576c 75%, #4facfe 100%);
+
+ /* Advanced Glassmorphism Effects - Further Enhanced for Better Modal Readability */
+ --glass-blur: blur(14px);
+ --glass-bg: rgba(255, 255, 255, 0.28);
+ --glass-bg-strong: rgba(255, 255, 255, 0.38);
+ --glass-bg-ultra: rgba(255, 255, 255, 0.48);
+ --glass-border: rgba(255, 255, 255, 0.4);
+ --glass-border-strong: rgba(255, 255, 255, 0.5);
+ --glass-border-ultra: rgba(255, 255, 255, 0.7);
+
+ /* Interactive States - Space Magic */
+ --hover-overlay: rgba(255, 255, 255, 0.08);
+ --focus-ring: oklch(0.7 0.25 280);
+ --active-scale: 0.95;
+ --hover-glow: 0 0 20px rgba(123, 97, 255, 0.4);
+
+ /* Status Colors - Neon Space */
+ --status-success: oklch(0.75 0.3 155);
+ --status-success-bg: oklch(0.75 0.3 155 / 0.25);
+ --status-success-border: oklch(0.75 0.3 155 / 0.5);
+ --status-warning: oklch(0.85 0.25 95);
+ --status-warning-bg: oklch(0.85 0.25 95 / 0.25);
+ --status-warning-border: oklch(0.85 0.25 95 / 0.5);
+ --status-error: oklch(0.8 0.3 35);
+ --status-error-bg: oklch(0.8 0.3 35 / 0.25);
+ --status-error-border: oklch(0.8 0.3 35 / 0.5);
+ --status-info: oklch(0.7 0.3 240);
+ --status-info-bg: oklch(0.7 0.3 240 / 0.25);
+ --status-info-border: oklch(0.7 0.3 240 / 0.5);
+}
+
+/* ===========================================
+ RESPONSIVE BREAKPOINTS
+ =========================================== */
+
+:root {
+ --breakpoint-sm: 640px;
+ --breakpoint-md: 768px;
+ --breakpoint-lg: 1024px;
+ --breakpoint-xl: 1280px;
+ --breakpoint-2xl: 1536px;
+}
\ No newline at end of file
diff --git a/plugins/themes/types/index.ts b/plugins/themes/types/index.ts
new file mode 100644
index 00000000..48064112
--- /dev/null
+++ b/plugins/themes/types/index.ts
@@ -0,0 +1,106 @@
+/**
+ * Types for Themes Plugin
+ * Unified theme system with 3 themes: Light, Dark, Advanced
+ */
+
+export type ThemeType = 'light' | 'dark' | 'advanced';
+
+export interface ThemeColors {
+ background: string;
+ foreground: string;
+ card: string;
+ cardForeground: string;
+ popover: string;
+ popoverForeground: string;
+ primary: string;
+ primaryForeground: string;
+ secondary: string;
+ secondaryForeground: string;
+ muted: string;
+ mutedForeground: string;
+ accent: string;
+ accentForeground: string;
+ destructive: string;
+ destructiveForeground: string;
+ border: string;
+ input: string;
+ ring: string;
+ // Additional colors for advanced theme
+ gradient?: string;
+ glassBg?: string;
+ glassBlur?: string;
+}
+
+export interface ThemeDefinition {
+ id: ThemeType;
+ name: string;
+ description: string;
+ colors: ThemeColors;
+ features: {
+ glassmorphism: boolean;
+ animations: boolean;
+ gradients: boolean;
+ };
+}
+
+export interface ThemePluginConfig {
+ enabled: boolean;
+ activeTheme: ThemeType;
+ availableThemes: ThemeType[];
+ persistUserChoice: boolean;
+ autoApplySystemTheme: boolean;
+ customThemes?: Record;
+ // Enhanced UI Experience - activează automat analytics și dashboard
+ enableEnhancedUIExperience?: boolean;
+ autoEnableAnalytics?: boolean;
+ autoEnableDashboard?: boolean;
+}
+
+export interface ThemeContextType {
+ currentTheme: ThemeType;
+ themes: Record;
+ pluginConfig: ThemePluginConfig;
+ setTheme: (theme: ThemeType) => void;
+ isPluginEnabled: () => boolean;
+ togglePlugin: (enabled: boolean) => void;
+}
+
+export interface ThemePluginAPI {
+ name: 'themes';
+ version: '1.0.0';
+ register: () => void;
+ unregister: () => void;
+ getConfig: () => ThemePluginConfig;
+ setConfig: (config: Partial) => void;
+ setTheme: (theme: ThemeType) => void;
+ getCurrentTheme: () => ThemeType;
+ getAvailableThemes: () => ThemeType[];
+ applyToElement: (element: HTMLElement) => void;
+ cleanup: () => void;
+ // Enhanced UI Experience methods
+ enableEnhancedUIExperience: () => void;
+ disableEnhancedUIExperience: () => void;
+ isEnhancedUIExperienceEnabled: () => boolean;
+}
+
+// Extend the main Config interface to include plugins
+declare global {
+ interface Window {
+ __THEMES_PLUGIN__?: ThemePluginAPI;
+ LayoutTransformer?: any;
+ }
+}
+
+export interface PluginsConfig {
+ themes?: ThemePluginConfig;
+ analytics?: {
+ enabled: boolean;
+ dashboardEnabled?: boolean;
+ missionControlEnabled?: boolean;
+ };
+}
+
+// This will be merged with the main Config interface
+export interface ConfigWithPlugins {
+ plugins?: PluginsConfig;
+}
\ No newline at end of file
diff --git a/plugins/themes/ui/ThemeSettings.tsx b/plugins/themes/ui/ThemeSettings.tsx
new file mode 100644
index 00000000..2c820492
--- /dev/null
+++ b/plugins/themes/ui/ThemeSettings.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { useTheme } from '../contexts/ThemeContext';
+import type { ThemeType } from '../types';
+
+interface ThemeSettingsProps {
+ isEnabled: boolean;
+}
+
+// This component will now only be responsible for rendering the theme selection UI.
+// The enabled/disabled state is managed by the parent PluginManager.
+function ThemeSelector() {
+ const { currentTheme, setTheme } = useTheme();
+
+ const handleThemeChange = (theme: ThemeType) => {
+ setTheme(theme);
+ };
+
+ const themes = [
+ { id: 'light', label: 'Light', icon: '☀️' },
+ { id: 'dark', label: 'Dark', icon: '🌙' },
+ { id: 'advanced', label: 'Advanced', icon: '✨' }
+ ];
+
+ return (
+
+
Select Theme:
+
+ {themes.map((theme) => (
+
+ ))}
+
+
+ );
+}
+
+export function ThemeSettings({ isEnabled }: ThemeSettingsProps) {
+ return (
+
+
+
+ 🎨
+
Advanced Themes
+
+
+
+ Glassmorphism effects and premium theming
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/run-tests.js b/run-tests.js
new file mode 100644
index 00000000..e410b86a
--- /dev/null
+++ b/run-tests.js
@@ -0,0 +1,9 @@
+const { execSync } = require('child_process');
+
+try {
+ console.log('Running Playwright tests...');
+ execSync('npx playwright test', { stdio: 'inherit' });
+} catch (error) {
+ console.error('Playwright tests failed:', error);
+ process.exit(1);
+}
diff --git a/scripts/install-local.sh b/scripts/install-local.sh
new file mode 100755
index 00000000..1ba719b3
--- /dev/null
+++ b/scripts/install-local.sh
@@ -0,0 +1,76 @@
+#!/bin/bash
+
+# Script pentru instalarea globală a versiunii locale modificate
+# Autor: Claude Code Dashboard Extension
+# Data: $(date)
+
+set -e
+
+# Culori pentru output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+echo -e "${BLUE}🚀 Claude Code Router - Local to Global Installation${NC}"
+echo -e "${BLUE}===================================================${NC}"
+
+# Verific dacă sunt în directorul corect
+if [ ! -f "package.json" ] || [ ! -d "dist" ]; then
+ echo -e "${RED}❌ Eroare: Trebuie să fii în directorul root al proiectului${NC}"
+ echo -e "${YELLOW}💡 Rulează: cd /opt/lampp/htdocs/claude-code-router${NC}"
+ exit 1
+fi
+
+# Verific dacă dist/cli.js există
+if [ ! -f "dist/cli.js" ]; then
+ echo -e "${YELLOW}⚠️ dist/cli.js nu există. Rulând build...${NC}"
+ npm run build
+fi
+
+# Actualizez versiunea în package.json pentru a evita conflictele
+CURRENT_VERSION=$(node -p "require('./package.json').version")
+TIMESTAMP=$(date +%Y%m%d%H%M%S)
+NEW_VERSION="${CURRENT_VERSION}-local.${TIMESTAMP}"
+
+echo -e "${BLUE}📦 Actualizez versiunea la: ${NEW_VERSION}${NC}"
+npm version $NEW_VERSION --no-git-tag-version
+
+# Creez backup-ul versiunii precedente dacă există
+if command -v ccr &> /dev/null; then
+ CCR_VERSION=$(ccr --version 2>/dev/null || echo "unknown")
+ echo -e "${YELLOW}💾 Backup versiune existentă: ${CCR_VERSION}${NC}"
+fi
+
+# Dezinstalez versiunea globală existentă (dacă există)
+echo -e "${BLUE}🗑️ Dezinstalez versiunea globală existentă...${NC}"
+npm uninstall -g @musistudio/claude-code-router 2>/dev/null || true
+
+# Instalez versiunea locală global
+echo -e "${GREEN}🔧 Instalez versiunea locală global...${NC}"
+npm install -g .
+
+# Verific instalarea
+if command -v ccr &> /dev/null; then
+ NEW_CCR_VERSION=$(ccr --version 2>/dev/null || echo "unknown")
+ echo -e "${GREEN}✅ Instalare completă!${NC}"
+ echo -e "${GREEN}📋 Versiunea instalată: ${NEW_CCR_VERSION}${NC}"
+ echo -e "${BLUE}🧪 Testez comenzile...${NC}"
+
+ # Test comenzi
+ echo -e "${YELLOW} ccr status...${NC}"
+ ccr status || echo -e "${YELLOW} (serverul nu rulează - normal)${NC}"
+
+ echo -e "${GREEN}🎉 Gata! Poți folosi comenzile ccr ca înainte:${NC}"
+ echo -e " • ${BLUE}ccr start${NC} - Pornește serverul"
+ echo -e " • ${BLUE}ccr stop${NC} - Oprește serverul"
+ echo -e " • ${BLUE}ccr status${NC} - Status server"
+ echo -e " • ${BLUE}ccr code${NC} - Claude Code"
+ echo -e " • ${BLUE}ccr ui${NC} - Deschide UI-ul (cu noile funcționalități!)"
+ echo
+ echo -e "${GREEN}🌟 Dashboard-ul tău cu funcționalitățile Advanced este acum activ!${NC}"
+else
+ echo -e "${RED}❌ Eroare la instalare. ccr nu este disponibil global.${NC}"
+ exit 1
+fi
\ No newline at end of file
diff --git a/scripts/start-with-ollama.sh b/scripts/start-with-ollama.sh
new file mode 100755
index 00000000..fa821f3c
--- /dev/null
+++ b/scripts/start-with-ollama.sh
@@ -0,0 +1,187 @@
+#!/bin/bash
+
+# Claude Code Router - Start with Local Models
+# Ensures Ollama models are running before starting CCR
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
+LOG_FILE="$HOME/.claude-code-router/startup.log"
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+log() {
+ echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE"
+}
+
+error() {
+ echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE"
+}
+
+success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1" | tee -a "$LOG_FILE"
+}
+
+warn() {
+ echo -e "${YELLOW}[WARN]${NC} $1" | tee -a "$LOG_FILE"
+}
+
+# Create log directory if not exists
+mkdir -p "$(dirname "$LOG_FILE")"
+
+log "🚀 Starting Claude Code Router with local models..."
+
+# Check if Ollama is installed
+if ! command -v ollama &> /dev/null; then
+ error "Ollama is not installed. Please install it first:"
+ error "curl -fsSL https://ollama.ai/install.sh | sh"
+ exit 1
+fi
+
+# Function to check if Ollama service is running
+check_ollama_service() {
+ if pgrep -x "ollama" > /dev/null; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# Function to check if a model is loaded and ready
+check_model_ready() {
+ local model=$1
+ local max_attempts=30
+ local attempt=1
+
+ log "🔍 Checking if model $model is ready..."
+
+ while [ $attempt -le $max_attempts ]; do
+ if curl -sf "http://localhost:11434/api/generate" \
+ -H "Content-Type: application/json" \
+ -d "{\"model\":\"$model\",\"prompt\":\"test\",\"stream\":false}" > /dev/null 2>&1; then
+ success "✅ Model $model is ready!"
+ return 0
+ fi
+
+ warn "⏳ Attempt $attempt/$max_attempts: Model $model not ready yet, waiting..."
+ sleep 2
+ attempt=$((attempt + 1))
+ done
+
+ error "❌ Model $model failed to load after $max_attempts attempts"
+ return 1
+}
+
+# Start Ollama service if not running
+if ! check_ollama_service; then
+ log "🔄 Starting Ollama service..."
+ ollama serve > "$HOME/.claude-code-router/ollama.log" 2>&1 &
+ sleep 3
+
+ if check_ollama_service; then
+ success "✅ Ollama service started"
+ else
+ error "❌ Failed to start Ollama service"
+ exit 1
+ fi
+else
+ success "✅ Ollama service is already running"
+fi
+
+# Pre-load models to ensure they're ready
+log "🤖 Pre-loading local models..."
+
+declare -a models=("gemma3:4b" "qwen2.5-coder:7b" "qwen2.5-coder:14b")
+declare -a failed_models=()
+
+for model in "${models[@]}"; do
+ log "📥 Loading model: $model"
+
+ # Start loading model in background
+ curl -sf "http://localhost:11434/api/pull" \
+ -H "Content-Type: application/json" \
+ -d "{\"name\":\"$model\"}" > /dev/null 2>&1 &
+
+ # Wait a bit then check if ready
+ sleep 2
+
+ if check_model_ready "$model"; then
+ success "✅ Model $model loaded successfully"
+ else
+ error "❌ Failed to load model $model"
+ failed_models+=("$model")
+ fi
+done
+
+# Report model loading results
+if [ ${#failed_models[@]} -eq 0 ]; then
+ success "🎉 All models loaded successfully!"
+else
+ warn "⚠️ Some models failed to load: ${failed_models[*]}"
+ warn "CCR will fallback to cloud providers for those models"
+fi
+
+# Check if CCR is already running
+if pgrep -f "claude-code-router" > /dev/null; then
+ log "🔄 Stopping existing CCR instance..."
+ pkill -f "claude-code-router" || true
+ sleep 2
+fi
+
+# Start Claude Code Router
+log "🚀 Starting Claude Code Router..."
+cd "$PROJECT_DIR"
+
+# Set environment variables for optimal performance
+export NODE_ENV=production
+export CCR_OLLAMA_READY=1
+export CCR_LOCAL_MODELS_AVAILABLE=1
+
+# Start CCR in background (detached)
+if nohup node "$PROJECT_DIR/dist/cli.js" start > "$HOME/.claude-code-router/ccr.log" 2>&1 &; then
+ CCR_PID=$!
+ sleep 3
+
+ # Check if process is still running
+ if kill -0 $CCR_PID 2>/dev/null; then
+ success "🎉 Claude Code Router started successfully! (PID: $CCR_PID)"
+ success "📊 Local models: ${#models[@]} loaded"
+ success "🔗 Web UI: http://localhost:3456"
+ success "📝 Logs:"
+ success " • Startup: $LOG_FILE"
+ success " • Runtime: $HOME/.claude-code-router/ccr.log"
+
+ # Display configuration summary
+ log ""
+ log "📋 Configuration Summary (conform documentației CCR):"
+ log " • Default (task-uri generale): ollama-local,gemma3:4b"
+ log " • Background (task-uri ușoare): ollama-local,qwen2.5-coder:7b"
+ log " • Think (reasoning/planning): ollama-local,qwen2.5-coder:14b"
+ log " • Long Context (peste 32K): mircea-gabriel,gemini-2.5-pro"
+ log " • Web Search: mircea-gabriel,gemini-2.5-flash"
+ log ""
+ success "🎯 Ready to use! Try: ccr code 'Hello from local model!'"
+ success "🔄 Service is now running in background. Use 'ccr status' to check."
+
+ # Write PID to file for later management
+ echo $CCR_PID > "$HOME/.claude-code-router/ccr.pid"
+
+ else
+ error "❌ CCR failed to start properly"
+ exit 1
+ fi
+else
+ error "❌ Failed to start CCR"
+ exit 1
+fi
+
+log ""
+log "✅ Startup script completed. Claude Code Router is running in background."
+log " Use 'ccr status' to check service status"
+log " Use 'ccr stop' to stop the service"
\ No newline at end of file
diff --git a/scripts/update-local.sh b/scripts/update-local.sh
new file mode 100755
index 00000000..62b5dc7e
--- /dev/null
+++ b/scripts/update-local.sh
@@ -0,0 +1,58 @@
+#!/bin/bash
+
+# Script pentru actualizarea rapidă a versiunii globale cu modificările locale
+# Autor: Claude Code Dashboard Extension
+
+set -e
+
+# Culori pentru output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+echo -e "${BLUE}🔄 Claude Code Router - Local Update${NC}"
+echo -e "${BLUE}=================================${NC}"
+
+# Verific dacă sunt în directorul corect
+if [ ! -f "package.json" ] || [ ! -d "dist" ]; then
+ echo -e "${RED}❌ Eroare: Trebuie să fii în directorul root al proiectului${NC}"
+ exit 1
+fi
+
+# Opresc serverul dacă rulează
+echo -e "${YELLOW}⏸️ Opresc serverul dacă rulează...${NC}"
+ccr stop 2>/dev/null || true
+
+# Build nou
+echo -e "${BLUE}🔨 Building versiunea actualizată...${NC}"
+npm run build
+
+# Actualizez versiunea
+CURRENT_VERSION=$(node -p "require('./package.json').version" | sed 's/-local\.[0-9]*$//')
+TIMESTAMP=$(date +%Y%m%d%H%M%S)
+NEW_VERSION="${CURRENT_VERSION}-local.${TIMESTAMP}"
+
+echo -e "${BLUE}📦 Versiune nouă: ${NEW_VERSION}${NC}"
+npm version $NEW_VERSION --no-git-tag-version
+
+# Reinstallez global
+echo -e "${GREEN}🚀 Actualizez instalarea globală...${NC}"
+npm uninstall -g @musistudio/claude-code-router 2>/dev/null || true
+npm install -g .
+
+# Verific actualizarea
+if command -v ccr &> /dev/null; then
+ NEW_CCR_VERSION=$(ccr --version 2>/dev/null || echo "unknown")
+ echo -e "${GREEN}✅ Actualizare completă!${NC}"
+ echo -e "${GREEN}📋 Versiunea actualizată: ${NEW_CCR_VERSION}${NC}"
+ echo -e "${GREEN}🎉 Modificările tale sunt acum active global!${NC}"
+ echo
+ echo -e "${BLUE}💡 Pentru a testa:${NC}"
+ echo -e " • ${BLUE}ccr start${NC} - pornește serverul"
+ echo -e " • ${BLUE}ccr ui${NC} - vezi dashboard-ul actualizat"
+else
+ echo -e "${RED}❌ Eroare la actualizare.${NC}"
+ exit 1
+fi
\ No newline at end of file
diff --git a/server.log b/server.log
new file mode 100644
index 00000000..a857826f
--- /dev/null
+++ b/server.log
@@ -0,0 +1,10 @@
+
+> @musistudio/claude-code-router@1.0.43-local.20250821095332 start
+> node dist/cli.js start
+
+Loaded 1000 analytics metrics
+Loaded stats for 39 models
+Loaded daily stats for 3 days
+⚠️ API key is not set. HOST is forced to 127.0.0.1.
+127.0.0.1
+Loaded JSON config from: /home/mircea/.claude-code-router/config.json
diff --git a/src/@types/musistudio__llms/index.d.ts b/src/@types/musistudio__llms/index.d.ts
new file mode 100644
index 00000000..a23fd6f7
--- /dev/null
+++ b/src/@types/musistudio__llms/index.d.ts
@@ -0,0 +1,10 @@
+declare module '@musistudio/llms' {
+ import { FastifyInstance } from 'fastify';
+
+ class Server {
+ app: FastifyInstance;
+ constructor(config: any);
+ }
+
+ export default Server;
+}
\ No newline at end of file
diff --git a/src/constants.ts b/src/constants.ts
index 5b7f3bf0..a36e9ed8 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,7 +1,9 @@
-import path from "node:path";
-import os from "node:os";
+import { join } from "path";
+import { homedir, tmpdir } from "os";
+import * as path from "path";
+import * as os from "os";
-export const HOME_DIR = path.join(os.homedir(), ".claude-code-router");
+export const HOME_DIR = join(homedir(), ".claude-code-router");
export const CONFIG_FILE = path.join(HOME_DIR, "config.json");
@@ -9,7 +11,7 @@ export const PLUGINS_DIR = path.join(HOME_DIR, "plugins");
export const PID_FILE = path.join(HOME_DIR, '.claude-code-router.pid');
-export const REFERENCE_COUNT_FILE = path.join(os.tmpdir(), "claude-code-reference-count.txt");
+export const REFERENCE_COUNT_FILE = path.join(tmpdir(), "claude-code-reference-count.txt");
export const DEFAULT_CONFIG = {
diff --git a/src/index.ts b/src/index.ts
index 207b9e9c..a3ab807b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -19,6 +19,7 @@ import {SSEParserTransform} from "./utils/SSEParser.transform";
import {SSESerializerTransform} from "./utils/SSESerializer.transform";
import {rewriteStream} from "./utils/rewriteStream";
import JSON5 from "json5";
+import { initStateManager } from '../plugins/core/stateManager';
import { IAgent } from "./agents/type";
import agentsManager from "./agents";
import { EventEmitter } from "node:events";
@@ -137,6 +138,22 @@ async function run(options: RunOptions = {}) {
},
logger: loggerConfig,
});
+
+ // Initialize state manager FIRST
+ await initStateManager(server.app, config.plugins);
+
+ // Plugin loading system - Now uses runtime state instead of config.json
+ const pluginsConfig = (server.app as any).pluginState || {};
+ if (pluginsConfig.analytics?.enabled) {
+ const AnalyticsPlugin = require('../plugins/analytics').default;
+ new AnalyticsPlugin().install(server.app, config);
+ }
+
+ if (pluginsConfig.themes?.enabled) {
+ const ThemesPlugin = require('../plugins/themes').default;
+ ThemesPlugin.register();
+ console.log('🎨 Themes Plugin loaded successfully');
+ }
// Add global error handlers to prevent the service from crashing
process.on("uncaughtException", (err) => {
@@ -146,6 +163,7 @@ async function run(options: RunOptions = {}) {
process.on("unhandledRejection", (reason, promise) => {
server.log.error("Unhandled rejection at:", promise, "reason:", reason);
});
+
// Add async preHandler hook for authentication
server.addHook("preHandler", async (req, reply) => {
return new Promise((resolve, reject) => {
@@ -376,7 +394,6 @@ async function run(options: RunOptions = {}) {
return payload;
})
-
server.start();
}
diff --git a/src/server.ts b/src/server.ts
index 74839131..1a77bfb5 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -61,6 +61,14 @@ export const createServer = (config: any): Server => {
maxAge: "1h",
});
+ // Register plugin static file serving
+ server.app.register(fastifyStatic, {
+ root: join(__dirname, "..", "plugins"),
+ prefix: "/plugins/",
+ maxAge: "1h",
+ decorateReply: false
+ });
+
// Redirect /ui to /ui/ for proper static file serving
server.app.get("/ui", async (_, reply) => {
return reply.redirect("/ui/");
diff --git a/src/utils/close.ts b/src/utils/close.ts
index 27de6ab8..cfd2fd20 100644
--- a/src/utils/close.ts
+++ b/src/utils/close.ts
@@ -6,23 +6,69 @@ import { join } from 'path';
export async function closeService() {
const PID_FILE = join(HOME_DIR, '.claude-code-router.pid');
const isRunning = await isServiceRunning()
-
+
if (!isRunning) {
- console.log("No service is currently running.");
+ console.log("ℹ️ No service is currently running.");
+ // Still try to cleanup any remaining processes
+ await forceCleanupAllProcesses();
return;
}
- if (getReferenceCount() > 0) {
+ const refCount = getReferenceCount();
+ if (refCount > 0 && !force) {
+ console.log(`⚠️ Other processes are still using the service (count: ${refCount}). Keeping it running.`);
+ console.log("💡 Use force cleanup if needed: run './cleanup.sh' or restart with 'ccr restart'");
return;
}
try {
const pid = parseInt(readFileSync(PID_FILE, 'utf-8'));
- process.kill(pid);
+ console.log(`🔄 Stopping main service process (PID: ${pid})...`);
+ process.kill(pid, 'SIGTERM');
+
+ // Wait a bit for graceful shutdown
+ setTimeout(() => {
+ try {
+ process.kill(pid, 0); // Check if still running
+ console.log("⚡ Force killing main process...");
+ process.kill(pid, 'SIGKILL');
+ } catch (e) {
+ // Process already stopped
+ }
+ }, 2000);
+
cleanupPidFile();
console.log("claude code router service has been successfully stopped.");
} catch (e) {
- console.log("Failed to stop the service. It may have already been stopped.");
+ console.log("⚠️ Failed to stop the main service. Attempting force cleanup...");
cleanupPidFile();
+ await forceCleanupAllProcesses();
}
}
+
+async function forceCleanupAllProcesses(): Promise {
+ return new Promise((resolve) => {
+ console.log("🧹 Force cleaning up all related processes...");
+
+ // Reset reference count
+ const REFERENCE_COUNT_FILE = join(HOME_DIR, '../..', 'tmp', 'claude-code-reference-count.txt');
+ try {
+ require('fs').unlinkSync(REFERENCE_COUNT_FILE);
+ console.log("🗑️ Reset reference count file");
+ } catch (e) {
+ // File doesn't exist or couldn't delete
+ }
+
+ // Kill all ccr processes
+ exec('pkill -f "ccr" 2>/dev/null || true', (error) => {
+ // Kill all claude-code-router processes
+ exec('pkill -f "claude-code-router" 2>/dev/null || true', (error) => {
+ // Kill processes on port 3456
+ exec('lsof -ti:3456 | xargs kill -9 2>/dev/null || true', (error) => {
+ console.log("✅ Force cleanup completed.");
+ resolve();
+ });
+ });
+ });
+ });
+}
diff --git a/src/utils/dynamic-provider-detector.ts b/src/utils/dynamic-provider-detector.ts
new file mode 100644
index 00000000..243f3e15
--- /dev/null
+++ b/src/utils/dynamic-provider-detector.ts
@@ -0,0 +1,370 @@
+/**
+ * Dynamic Provider Detection and Switching System
+ * Detectează automat provider-ii disponibili și poate schimba între ei
+ */
+
+// Simple logging functions since log utility was removed
+const info = (...args: any[]) => console.log('[INFO]', ...args);
+const error = (...args: any[]) => console.error('[ERROR]', ...args);
+const warn = (...args: any[]) => console.warn('[WARN]', ...args);
+
+export interface DetectedProvider {
+ name: string;
+ models: string[];
+ apiBaseUrl: string;
+ apiKey: string;
+ transformer?: any;
+ priority?: number;
+ enabled: boolean;
+ capabilities: {
+ hasTools: boolean;
+ maxContextLength: number;
+ supportsStreaming: boolean;
+ };
+ status: {
+ isOnline: boolean;
+ lastChecked: number;
+ responseTime?: number;
+ errorCount: number;
+ };
+}
+
+export interface ModelCapabilities {
+ contextLength: number;
+ supportsTools: boolean;
+ supportsVision: boolean;
+ supportsAudio: boolean;
+ costTier: 'free' | 'paid' | 'premium';
+ speed: 'slow' | 'medium' | 'fast' | 'ultra-fast';
+}
+
+export class DynamicProviderDetector {
+ private detectedProviders: Map = new Map();
+ private modelCapabilities: Map = new Map();
+ private lastScan = 0;
+ private scanInterval = 300000; // 5 minutes
+
+ constructor() {
+ this.initializeModelCapabilities();
+ }
+
+ /**
+ * Scan și detectează toți provider-ii disponibili din config
+ */
+ async scanProvidersFromConfig(config: any): Promise {
+ const now = Date.now();
+
+ if (now - this.lastScan < this.scanInterval && this.detectedProviders.size > 0) {
+ info(`[DynamicDetector] Using cached provider scan from ${Math.floor((now - this.lastScan) / 1000)}s ago`);
+ return Array.from(this.detectedProviders.values());
+ }
+
+ info(`[DynamicDetector] Scanning providers from config...`);
+ this.detectedProviders.clear();
+
+ const providers = config.Providers || [];
+
+ for (const provider of providers) {
+ try {
+ const detected = await this.analyzeProvider(provider);
+ this.detectedProviders.set(provider.name, detected);
+ info(`[DynamicDetector] ✅ Detected provider: ${provider.name} with ${detected.models.length} models`);
+ } catch (error: any) {
+ error(`[DynamicDetector] ❌ Failed to analyze provider ${provider.name}: ${error.message}`);
+ }
+ }
+
+ this.lastScan = now;
+ return Array.from(this.detectedProviders.values());
+ }
+
+ /**
+ * Analizează un provider și detectează capabilities
+ */
+ private async analyzeProvider(providerConfig: any): Promise {
+ const capabilities = {
+ hasTools: this.detectToolsSupport(providerConfig),
+ maxContextLength: this.detectMaxContext(providerConfig),
+ supportsStreaming: this.detectStreamingSupport(providerConfig)
+ };
+
+ const detected: DetectedProvider = {
+ name: providerConfig.name,
+ models: providerConfig.models || [],
+ apiBaseUrl: providerConfig.api_base_url,
+ apiKey: providerConfig.api_key,
+ transformer: providerConfig.transformer,
+ priority: providerConfig.priority || 1,
+ enabled: true,
+ capabilities,
+ status: {
+ isOnline: true, // Will be tested asynchronously
+ lastChecked: Date.now(),
+ errorCount: 0
+ }
+ };
+
+ // Test connectivity asynchronously (non-blocking)
+ this.testProviderConnectivity(detected).catch(error => {
+ error(`[DynamicDetector] Connectivity test failed for ${detected.name}: ${error.message}`);
+ });
+
+ return detected;
+ }
+
+ /**
+ * Găsește cel mai potrivit provider pentru un request specific
+ */
+ findBestProviderForRequest(
+ requestModel: string,
+ hasTools: boolean,
+ contextSize: number,
+ excludeProviders: string[] = []
+ ): DetectedProvider | null {
+ const availableProviders = Array.from(this.detectedProviders.values())
+ .filter(p =>
+ p.enabled &&
+ p.status.isOnline &&
+ !excludeProviders.includes(p.name)
+ )
+ .sort((a, b) => {
+ // Sort by priority first, then by capabilities match
+ // Ensure 'priority' is defined before comparison
+ if (a.priority !== undefined && b.priority !== undefined) {
+ if (a.priority !== b.priority) return a.priority - b.priority;
+ }
+
+ // Prefer providers that support tools if needed
+ if (hasTools && a.capabilities.hasTools !== b.capabilities.hasTools) {
+ return a.capabilities.hasTools ? -1 : 1;
+ }
+
+ // Prefer providers with sufficient context length
+ const aCanHandle = a.capabilities.maxContextLength >= contextSize;
+ const bCanHandle = b.capabilities.maxContextLength >= contextSize;
+ if (aCanHandle !== bCanHandle) return aCanHandle ? -1 : 1;
+
+ return 0;
+ });
+
+ // Try to find exact model match first
+ for (const provider of availableProviders) {
+ if (provider.models.includes(requestModel)) {
+ info(`[DynamicDetector] Found exact model match: ${provider.name} -> ${requestModel}`);
+ return provider;
+ }
+ }
+
+ // Find compatible model
+ for (const provider of availableProviders) {
+ const compatibleModel = this.findCompatibleModel(provider, requestModel, hasTools);
+ if (compatibleModel) {
+ info(`[DynamicDetector] Found compatible provider: ${provider.name} -> ${compatibleModel}`);
+ return {
+ ...provider,
+ models: [compatibleModel] // Override with compatible model
+ };
+ }
+ }
+
+ info(`[DynamicDetector] No suitable provider found for ${requestModel} (tools:${hasTools}, context:${contextSize})`);
+ return null;
+ }
+
+ /**
+ * Găsește un model compatibil într-un provider
+ */
+ private findCompatibleModel(provider: DetectedProvider, requestedModel: string, needsTools: boolean): string | null {
+ // Model compatibility mapping
+ const compatibilityMap: { [key: string]: string[] } = {
+ 'gemini-2.5-pro': ['gemini-2.5-flash', 'gemini-2.0-flash', 'qwen/qwen3-coder:free'],
+ 'gemini-2.5-flash': ['gemini-2.0-flash', 'gemini-2.5-flash-lite', 'qwen/qwen3-coder:free'],
+ 'gemini-2.0-flash': ['gemini-2.5-flash', 'qwen/qwen3-coder:free'],
+ 'claude-3-sonnet': ['gemini-2.5-pro', 'qwen/qwen3-coder:free'],
+ 'gpt-4': ['gemini-2.5-pro', 'qwen/qwen3-coder:free']
+ };
+
+ const compatibleModels = compatibilityMap[requestedModel] || [];
+
+ for (const model of provider.models) {
+ // Direct compatibility check
+ if (compatibleModels.includes(model)) {
+ const modelCaps = this.modelCapabilities.get(model);
+ if (!needsTools || !modelCaps || modelCaps.supportsTools) {
+ return model;
+ }
+ }
+
+ // Fuzzy matching for similar models
+ if (this.areModelsCompatible(requestedModel, model)) {
+ const modelCaps = this.modelCapabilities.get(model);
+ if (!needsTools || !modelCaps || modelCaps.supportsTools) {
+ return model;
+ }
+ }
+ }
+
+ // Fallback: return first model if provider supports tools when needed
+ if (provider.models.length > 0) {
+ const firstModel = provider.models[0];
+ const modelCaps = this.modelCapabilities.get(firstModel);
+ if (!needsTools || !modelCaps || modelCaps.supportsTools) {
+ return firstModel;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Verifică dacă două modele sunt compatibile
+ */
+ private areModelsCompatible(model1: string, model2: string): boolean {
+ // Normalize model names for comparison
+ const normalize = (name: string) => name.toLowerCase()
+ .replace(/[-_]/g, '')
+ .replace(/\d+(\.\d+)?[a-z]?/g, '') // Remove version numbers
+ .replace(/(free|paid|pro|lite|flash)$/g, ''); // Remove suffixes
+
+ const norm1 = normalize(model1);
+ const norm2 = normalize(model2);
+
+ // Check for similar families
+ const families = [
+ ['gemini', 'gemma'],
+ ['qwen', 'qwen3'],
+ ['claude', 'anthropic'],
+ ['gpt', 'openai']
+ ];
+
+ for (const family of families) {
+ if (family.some(f => norm1.includes(f)) && family.some(f => norm2.includes(f))) {
+ return true;
+ }
+ }
+
+ return norm1 === norm2;
+ }
+
+ /**
+ * Schimbă dinamic la un alt provider când unul eșuează
+ */
+ async switchToFallbackProvider(
+ failedProvider: string,
+ originalRequest: any,
+ config: any
+ ): Promise<{ provider: DetectedProvider; model: string } | null> {
+ warn(`[DynamicDetector] Switching away from failed provider: ${failedProvider}`);
+
+ // Mark provider as problematic
+ const provider = this.detectedProviders.get(failedProvider);
+ if (provider) {
+ provider.status.errorCount++;
+ if (provider.status.errorCount >= 3) {
+ provider.status.isOnline = false;
+ warn(`[DynamicDetector] Marked provider ${failedProvider} as offline due to errors`);
+ }
+ }
+
+ // Find alternative
+ const requestModel = originalRequest.model || 'gemini-2.5-flash';
+ const hasTools = Array.isArray(originalRequest.tools) && originalRequest.tools.length > 0;
+ const contextSize = JSON.stringify(originalRequest.messages || []).length;
+
+ const alternative = this.findBestProviderForRequest(
+ requestModel,
+ hasTools,
+ contextSize,
+ [failedProvider] // Exclude failed provider
+ );
+
+ if (alternative) {
+ const compatibleModel = this.findCompatibleModel(alternative, requestModel, hasTools) || alternative.models[0];
+ info(`[DynamicDetector] Switched to: ${alternative.name} -> ${compatibleModel}`);
+ return { provider: alternative, model: compatibleModel };
+ }
+
+ return null;
+ }
+
+ /**
+ * Test connectivity to a provider
+ */
+ private async testProviderConnectivity(provider: DetectedProvider): Promise {
+ // Simplified connectivity test - in real implementation, make a lightweight API call
+ const startTime = Date.now();
+
+ try {
+ // Mock test - replace with actual API ping
+ await new Promise(resolve => setTimeout(resolve, 100 + Math.random() * 200));
+
+ provider.status.responseTime = Date.now() - startTime;
+ provider.status.isOnline = true;
+ provider.status.lastChecked = Date.now();
+
+ } catch (error) {
+ provider.status.isOnline = false;
+ provider.status.errorCount++;
+ throw error;
+ }
+ }
+
+ // Helper methods for capability detection
+ private detectToolsSupport(provider: any): boolean {
+ const name = provider.name.toLowerCase();
+ // Gemini, OpenAI-compatible providers usually support tools
+ return name.includes('gemini') || name.includes('openai') || name.includes('anthropic');
+ }
+
+ private detectMaxContext(provider: any): number {
+ const name = provider.name.toLowerCase();
+ if (name.includes('gemini')) return 2000000; // 2M tokens
+ if (name.includes('claude')) return 200000; // 200k tokens
+ if (name.includes('gpt')) return 128000; // 128k tokens
+ return 32000; // Conservative default
+ }
+
+ private detectStreamingSupport(provider: any): boolean {
+ // Most modern providers support streaming
+ return true;
+ }
+
+ private initializeModelCapabilities(): void {
+ // Initialize known model capabilities
+ const capabilities: Array<[string, ModelCapabilities]> = [
+ ['gemini-2.5-pro', { contextLength: 2000000, supportsTools: true, supportsVision: true, supportsAudio: true, costTier: 'paid', speed: 'medium' }],
+ ['gemini-2.5-flash', { contextLength: 1000000, supportsTools: true, supportsVision: true, supportsAudio: false, costTier: 'free', speed: 'fast' }],
+ ['gemini-2.0-flash', { contextLength: 1000000, supportsTools: true, supportsVision: true, supportsAudio: true, costTier: 'free', speed: 'ultra-fast' }],
+ ['qwen/qwen3-coder:free', { contextLength: 32000, supportsTools: false, supportsVision: false, supportsAudio: false, costTier: 'free', speed: 'fast' }],
+ ['PetrosStav/gemma3-tools:4b', { contextLength: 8000, supportsTools: true, supportsVision: false, supportsAudio: false, costTier: 'free', speed: 'ultra-fast' }]
+ ];
+
+ for (const [model, caps] of capabilities) {
+ this.modelCapabilities.set(model, caps);
+ }
+ }
+
+ /**
+ * Get current provider status for monitoring
+ */
+ getProviderStatus(): { [providerName: string]: any } {
+ const status: any = {};
+
+ for (const [name, provider] of this.detectedProviders.entries()) {
+ status[name] = {
+ enabled: provider.enabled,
+ isOnline: provider.status.isOnline,
+ models: provider.models,
+ capabilities: provider.capabilities,
+ lastChecked: provider.status.lastChecked,
+ errorCount: provider.status.errorCount,
+ responseTime: provider.status.responseTime
+ };
+ }
+
+ return status;
+ }
+}
+
+// Singleton instance
+export const dynamicProviderDetector = new DynamicProviderDetector();
\ No newline at end of file
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 5e47e842..85582f4f 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,5 +1,5 @@
-import fs from "node:fs/promises";
-import readline from "node:readline";
+import fs, { access, mkdir } from "node:fs/promises";
+import readline, { createInterface } from "node:readline";
import JSON5 from "json5";
import path from "node:path";
import {
@@ -32,9 +32,9 @@ const interpolateEnvVars = (obj: any): any => {
const ensureDir = async (dir_path: string) => {
try {
- await fs.access(dir_path);
+ await access(dir_path);
} catch {
- await fs.mkdir(dir_path, { recursive: true });
+ await mkdir(dir_path, { recursive: true });
}
};
@@ -45,7 +45,7 @@ export const initDir = async () => {
};
const createReadline = () => {
- return readline.createInterface({
+ return createInterface({
input: process.stdin,
output: process.stdout,
});
diff --git a/src/utils/processCheck.ts b/src/utils/processCheck.ts
index 1c9eed31..835c5028 100644
--- a/src/utils/processCheck.ts
+++ b/src/utils/processCheck.ts
@@ -56,6 +56,10 @@ export function savePid(pid: number) {
writeFileSync(PID_FILE, pid.toString());
}
+export function resetReferenceCount() {
+ writeFileSync(REFERENCE_COUNT_FILE, '0');
+}
+
export function cleanupPidFile() {
if (existsSync(PID_FILE)) {
try {
diff --git a/src/utils/provider-fallback.ts b/src/utils/provider-fallback.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/test-layout-enhancer.html b/test-layout-enhancer.html
new file mode 100644
index 00000000..f9bccac6
--- /dev/null
+++ b/test-layout-enhancer.html
@@ -0,0 +1,217 @@
+
+
+
+
+
+ Layout Enhancer Test
+
+
+
+
+
🎨 Layout Enhancer Test
+
+
+ Checking layout enhancer status...
+
+
+
+
Theme Controls
+
+
+
+
+
+
+
Layout Enhancer Controls
+
+
+
+
+
+
+
Mock App Structure
+
+
+
+ Providers Section
+
(should get glass effect in advanced theme)
+
+
+ Router Section
+
(should get glass effect in advanced theme)
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test-results/.last-run.json b/test-results/.last-run.json
new file mode 100644
index 00000000..0284ec05
--- /dev/null
+++ b/test-results/.last-run.json
@@ -0,0 +1,6 @@
+{
+ "status": "failed",
+ "failedTests": [
+ "28e279948d60428e1abc-2ea4fbc0b05cc5bc9066"
+ ]
+}
\ No newline at end of file
diff --git a/test-results/plugin-state-verifies-real-time-plugin-state-management-flow-chromium/error-context.md b/test-results/plugin-state-verifies-real-time-plugin-state-management-flow-chromium/error-context.md
new file mode 100644
index 00000000..967320a1
--- /dev/null
+++ b/test-results/plugin-state-verifies-real-time-plugin-state-management-flow-chromium/error-context.md
@@ -0,0 +1,188 @@
+# Page snapshot
+
+```yaml
+- generic [ref=e3]:
+ - banner [ref=e4]:
+ - heading "Claude Code Router" [level=1] [ref=e6]
+ - generic [ref=e7]:
+ - button "Analytics Dashboard" [ref=e8]:
+ - img [ref=e9]
+ - button [ref=e12]:
+ - img
+ - button [ref=e13]:
+ - img
+ - button [ref=e14]:
+ - img
+ - button [ref=e15]:
+ - generic [ref=e16]:
+ - img
+ - button "Save" [ref=e17]:
+ - img
+ - text: Save
+ - button "Save and Restart" [ref=e18]:
+ - img
+ - text: Save and Restart
+ - main [ref=e19]:
+ - generic [ref=e21]:
+ - generic [ref=e22]:
+ - generic [ref=e23]:
+ - generic [ref=e24]:
+ - text: Providers
+ - generic [ref=e25]: (6/6)
+ - button "Add Provider" [ref=e26]
+ - generic [ref=e28]:
+ - img [ref=e29]
+ - textbox "Search providers..." [ref=e32]
+ - generic [ref=e34]:
+ - generic [ref=e35]:
+ - generic [ref=e36]:
+ - paragraph [ref=e37]: openrouter
+ - paragraph [ref=e38]: https://openrouter.ai/api/v1/chat/completions
+ - generic [ref=e39]:
+ - generic [ref=e40]: z-ai/glm-4.5-air:free
+ - generic [ref=e41]: z-ai/glm-4.5-air
+ - generic [ref=e42]: google/gemini-2.0-flash-exp:free
+ - generic [ref=e43]:
+ - button [ref=e44]:
+ - img
+ - button [ref=e45]:
+ - img
+ - generic [ref=e46]:
+ - generic [ref=e47]:
+ - paragraph [ref=e48]: GLM Provider
+ - paragraph [ref=e49]: https://api.z.ai/api/paas/v4/chat/completions
+ - generic [ref=e50]:
+ - generic [ref=e51]: glm-4.5
+ - generic [ref=e52]: glm-4.5-flash
+ - generic [ref=e53]: glm-4.5-air
+ - generic [ref=e54]: glm-4.5-airx
+ - generic [ref=e55]: glm-4.5-x
+ - generic [ref=e56]:
+ - button [ref=e57]:
+ - img
+ - button [ref=e58]:
+ - img
+ - generic [ref=e59]:
+ - generic [ref=e60]:
+ - paragraph [ref=e61]: Big Models
+ - paragraph [ref=e62]: https://open.bigmodel.cn/api/paas/v4/chat/completions
+ - generic [ref=e63]:
+ - generic [ref=e64]: glm-4.5
+ - generic [ref=e65]: glm-4.5V
+ - generic [ref=e66]: glm-4.5-flash
+ - generic [ref=e67]: glm-4.5-air
+ - generic [ref=e68]:
+ - button [ref=e69]:
+ - img
+ - button [ref=e70]:
+ - img
+ - generic [ref=e71]:
+ - generic [ref=e72]:
+ - paragraph [ref=e73]: qwen
+ - paragraph [ref=e74]: https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions
+ - generic [ref=e75]:
+ - generic [ref=e76]: qwen-turbo-latest
+ - generic [ref=e77]: qwen-plus-latest
+ - generic [ref=e78]: qwen-flash
+ - generic [ref=e79]: qwen-max
+ - generic [ref=e80]: qwen3-coder-plus
+ - generic [ref=e81]: qwen3-coder-flash
+ - generic [ref=e82]:
+ - button [ref=e83]:
+ - img
+ - button [ref=e84]:
+ - img
+ - generic [ref=e85]:
+ - generic [ref=e86]:
+ - paragraph [ref=e87]: gemini-via-cli
+ - paragraph [ref=e88]: https://cloudcode-pa.googleapis.com/v1internal
+ - generic [ref=e89]:
+ - generic [ref=e90]: gemini-2.5-flash
+ - generic [ref=e91]: gemini-2.5-pro
+ - generic [ref=e92]:
+ - button [ref=e93]:
+ - img
+ - button [ref=e94]:
+ - img
+ - generic [ref=e95]:
+ - generic [ref=e96]:
+ - paragraph [ref=e97]: qwen-via-cli
+ - paragraph [ref=e98]: https://portal.qwen.ai/v1/chat/completions
+ - generic [ref=e100]: qwen3-coder-plus
+ - generic [ref=e101]:
+ - button [ref=e102]:
+ - img
+ - button [ref=e103]:
+ - img
+ - generic [ref=e104]:
+ - generic [ref=e106]:
+ - generic [ref=e108]: Router
+ - generic [ref=e109]:
+ - generic [ref=e110]:
+ - generic [ref=e111]: Default
+ - combobox [ref=e112]:
+ - text: gemini-via-cli, gemini-2.5-flash
+ - img
+ - generic [ref=e113]:
+ - generic [ref=e114]: Background
+ - combobox [ref=e115]:
+ - text: gemini-via-cli, gemini-2.5-flash
+ - img
+ - generic [ref=e116]:
+ - generic [ref=e117]: Think
+ - combobox [ref=e118]:
+ - text: qwen-via-cli, qwen3-coder-plus
+ - img
+ - generic [ref=e120]:
+ - generic [ref=e121]:
+ - generic [ref=e122]: Long Context
+ - combobox [ref=e123]:
+ - text: gemini-via-cli, gemini-2.5-pro
+ - img
+ - generic [ref=e124]:
+ - generic [ref=e125]: Context Threshold
+ - textbox "60000" [ref=e126]
+ - generic [ref=e127]:
+ - generic [ref=e128]: Web Search
+ - combobox [ref=e129]:
+ - text: gemini-via-cli, gemini-2.5-flash
+ - img
+ - generic [ref=e131]:
+ - generic [ref=e132]:
+ - generic [ref=e133]: Image (beta)
+ - combobox [ref=e134]:
+ - text: gemini-via-cli, gemini-2.5-flash
+ - img
+ - generic [ref=e135]:
+ - generic [ref=e136]: Force Use Image Agent
+ - combobox "Force Use Image Agent" [ref=e137]:
+ - option "No" [selected]
+ - option "Yes"
+ - generic [ref=e139]:
+ - generic [ref=e140]:
+ - generic [ref=e141]:
+ - text: Custom Transformers
+ - generic [ref=e142]: (2)
+ - button "Add Custom Transformer" [ref=e143]
+ - generic [ref=e145]:
+ - generic [ref=e146]:
+ - generic [ref=e147]:
+ - paragraph [ref=e148]: /home/mircea/.claude-code-router/plugins/gemini-cli.js
+ - generic [ref=e150]:
+ - generic [ref=e151]: "project:"
+ - generic [ref=e152]: mircea-gabriel
+ - generic [ref=e153]:
+ - button [ref=e154]:
+ - img
+ - button [ref=e155]:
+ - img
+ - generic [ref=e156]:
+ - generic [ref=e157]:
+ - paragraph [ref=e158]: /home/mircea/.claude-code-router/plugins/qwen-cli.js
+ - paragraph [ref=e159]: No parameters configured
+ - generic [ref=e160]:
+ - button [ref=e161]:
+ - img
+ - button [ref=e162]:
+ - img
+```
\ No newline at end of file
diff --git a/ui/package-lock.json b/ui/package-lock.json
index e558a47d..b348a7f2 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -12,13 +12,16 @@
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14",
+ "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/vite": "^4.1.11",
+ "@types/recharts": "^1.8.29",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
+ "framer-motion": "^12.23.12",
"i18next": "^25.3.2",
"i18next-browser-languagedetector": "^8.2.0",
"lucide-react": "^0.525.0",
@@ -29,6 +32,7 @@
"react-dom": "^19.1.0",
"react-i18next": "^15.6.1",
"react-router-dom": "^7.7.0",
+ "recharts": "^3.1.2",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7"
},
@@ -1152,6 +1156,12 @@
"node": ">= 8"
}
},
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
@@ -1181,6 +1191,32 @@
}
}
},
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@@ -1247,6 +1283,21 @@
}
}
},
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
@@ -1495,6 +1546,129 @@
}
}
},
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
+ "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
+ "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
@@ -2115,6 +2289,18 @@
"win32"
]
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
+ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
"node_modules/@tailwindcss/node": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
@@ -2436,6 +2622,69 @@
"@babel/types": "^7.20.7"
}
},
+ "node_modules/@types/d3-array": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
+ "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -2463,7 +2712,6 @@
"version": "19.1.9",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
@@ -2479,6 +2727,37 @@
"@types/react": "^19.0.0"
}
},
+ "node_modules/@types/recharts": {
+ "version": "1.8.29",
+ "resolved": "https://registry.npmjs.org/@types/recharts/-/recharts-1.8.29.tgz",
+ "integrity": "sha512-ulKklaVsnFIIhTQsQw226TnOibrddW1qUQNFVhoQEyY1Z7FRQrNecFCGt7msRuJseudzE9czVawZb17dK/aPXw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-shape": "^1",
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/recharts/node_modules/@types/d3-path": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz",
+ "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/recharts/node_modules/@types/d3-shape": {
+ "version": "1.3.12",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz",
+ "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "^1"
+ }
+ },
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
@@ -3104,9 +3383,129 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "devOptional": true,
"license": "MIT"
},
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -3125,6 +3524,12 @@
}
}
},
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -3178,6 +3583,16 @@
"node": ">=10.13.0"
}
},
+ "node_modules/es-toolkit": {
+ "version": "1.39.10",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz",
+ "integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
"node_modules/esbuild": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
@@ -3420,6 +3835,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -3558,6 +3979,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
+ "node_modules/framer-motion": {
+ "version": "12.23.12",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz",
+ "integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.23.12",
+ "motion-utils": "^12.23.6",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -3708,6 +4156,16 @@
"node": ">= 4"
}
},
+ "node_modules/immer": {
+ "version": "10.1.3",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
+ "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -3735,6 +4193,15 @@
"node": ">=0.8.19"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -4234,6 +4701,21 @@
"license": "MIT",
"peer": true
},
+ "node_modules/motion-dom": {
+ "version": "12.23.12",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz",
+ "integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.23.6"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.23.6",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
+ "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -4680,6 +5162,97 @@
}
}
},
+ "node_modules/recharts": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.1.2.tgz",
+ "integrity": "sha512-vhNbYwaxNbk/IATK0Ki29k3qvTkGqwvCgyQAQ9MavvvBwjvKnMTswdbklJpcOAoMPN/qxF3Lyqob0zO+ZXkZ4g==",
+ "license": "MIT",
+ "dependencies": {
+ "@reduxjs/toolkit": "1.x.x || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^10.1.1",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.1.1",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/recharts/node_modules/@reduxjs/toolkit": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
+ "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^10.0.3",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/recharts/node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/recharts/node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
+ "node_modules/recharts/node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/redux": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
@@ -4689,6 +5262,12 @@
"@babel/runtime": "^7.9.2"
}
},
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -4919,6 +5498,12 @@
"node": ">=18"
}
},
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
@@ -5145,6 +5730,37 @@
}
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
+ "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
"node_modules/vite": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz",
diff --git a/ui/package.json b/ui/package.json
index 04e28a74..c8ab9d7e 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -14,13 +14,16 @@
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14",
+ "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/vite": "^4.1.11",
+ "@types/recharts": "^1.8.29",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
+ "framer-motion": "^12.23.12",
"i18next": "^25.3.2",
"i18next-browser-languagedetector": "^8.2.0",
"lucide-react": "^0.525.0",
@@ -31,6 +34,7 @@
"react-dom": "^19.1.0",
"react-i18next": "^15.6.1",
"react-router-dom": "^7.7.0",
+ "recharts": "^3.1.2",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7"
},
diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico
new file mode 100644
index 00000000..e7b8dfb1
--- /dev/null
+++ b/ui/public/favicon.ico
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index b6416807..d53bc1e4 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback, useRef } from "react";
+import React, { useState, useEffect, useCallback, useRef, Suspense } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { SettingsDialog } from "@/components/SettingsDialog";
@@ -9,8 +9,30 @@ import { JsonEditor } from "@/components/JsonEditor";
import { LogViewer } from "@/components/LogViewer";
import { Button } from "@/components/ui/button";
import { useConfig } from "@/components/ConfigProvider";
+import { PluginProvider } from "@/contexts/PluginContext";
import { api } from "@/lib/api";
import { Settings, Languages, Save, RefreshCw, FileJson, CircleArrowUp, FileText } from "lucide-react";
+
+// Lazy load MissionControlTab from plugin - RE-ENABLED
+const MissionControlTab = React.lazy(() =>
+ import('@plugins/analytics/ui/components/dashboard/tabs/MissionControlTab')
+ .then(module => ({ default: module.MissionControlTab }))
+ .catch(() => ({ default: () => Analytics plugin not available
}))
+);
+
+// Lazy load AnalyticsButton from plugin
+const AnalyticsButton = React.lazy(() =>
+ import('@plugins/analytics/ui/AnalyticsButton')
+ .then(module => ({ default: module.AnalyticsButton }))
+ .catch(() => ({ default: () => null }))
+);
+
+// Lazy load MissionControlModal from plugin (similar to LogViewer approach)
+const MissionControlModal = React.lazy(() =>
+ import('@plugins/analytics/ui/components/MissionControlModal')
+ .then(module => ({ default: module.MissionControlModal }))
+ .catch(() => ({ default: ({ open }: { open: boolean }) => open ? Analytics plugin not available
: null }))
+);
import {
Popover,
PopoverContent,
@@ -34,6 +56,9 @@ function App() {
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false);
const [isLogViewerOpen, setIsLogViewerOpen] = useState(false);
+ const [isMissionControlModalOpen, setIsMissionControlModalOpen] = useState(false);
+ const [currentView, setCurrentView] = useState<'dashboard' | 'analytics'>('dashboard');
+ const [activeTab, setActiveTab] = useState<'dashboard' | 'analytics'>('dashboard');
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);
// 版本检查状态
@@ -44,6 +69,27 @@ function App() {
const [hasCheckedUpdate, setHasCheckedUpdate] = useState(false);
const hasAutoCheckedUpdate = useRef(false);
+ // Listen for analytics plugin events
+ useEffect(() => {
+ const handleOpenMissionControl = () => {
+ // Instead of switching tab, open modal (like LogViewer)
+ setIsMissionControlModalOpen(true);
+ };
+
+ const handleCloseMissionControl = () => {
+ // Close modal
+ setIsMissionControlModalOpen(false);
+ };
+
+ document.addEventListener('open-mission-control', handleOpenMissionControl);
+ document.addEventListener('close-mission-control', handleCloseMissionControl);
+
+ return () => {
+ document.removeEventListener('open-mission-control', handleOpenMissionControl);
+ document.removeEventListener('close-mission-control', handleCloseMissionControl);
+ };
+ }, []);
+
const saveConfig = async () => {
// Handle case where config might be null or undefined
if (!config) {
@@ -161,7 +207,7 @@ function App() {
} finally {
setIsCheckingUpdate(false);
}
- }, [hasCheckedUpdate, isNewVersionAvailable, t]);
+ }, [isNewVersionAvailable, t]);
useEffect(() => {
const checkAuth = async () => {
@@ -169,7 +215,7 @@ function App() {
if (config) {
setIsCheckingAuth(false);
// 自动检查更新,但不显示对话框
- if (!hasCheckedUpdate && !hasAutoCheckedUpdate.current) {
+ if (!hasAutoCheckedUpdate.current) {
hasAutoCheckedUpdate.current = true;
checkForUpdates(false);
}
@@ -199,7 +245,7 @@ function App() {
} finally {
setIsCheckingAuth(false);
// 在获取配置完成后检查更新,但不显示对话框
- if (!hasCheckedUpdate && !hasAutoCheckedUpdate.current) {
+ if (!hasAutoCheckedUpdate.current) {
hasAutoCheckedUpdate.current = true;
checkForUpdates(false);
}
@@ -218,7 +264,7 @@ function App() {
return () => {
window.removeEventListener('unauthorized', handleUnauthorized);
};
- }, [config, navigate, hasCheckedUpdate, checkForUpdates]);
+ }, [config, navigate]);
// 执行更新函数
const performUpdate = async () => {
@@ -268,9 +314,32 @@ function App() {
}
return (
-
+
+ {/* Analytics Button - self-contained plugin component */}
+
+
+
+
+
- {t('app.title')}
+
+
{t('app.title')}
+
+ {/* Tab indicator (only shows when on Analytics) */}
+ {activeTab === 'analytics' && (
+
+
+ Analytics Mode
+
+
+ )}
+
+
+
-
-
-
-
-
-
-
+ {activeTab === 'analytics' ? (
+
+
+ 📊
+
Analytics
+
+
+ Loading Analytics...
}>
+
+
+
-
+ ) : (
+ <>
+
+
+ >
+ )}
setToast({ message, type })}
/>
+
+ setToast({ message, type })}
+ />
+
{/* 版本更新对话框 */}
+
+
);
}
diff --git a/ui/src/components/JsonEditor.tsx b/ui/src/components/JsonEditor.tsx
index 918434c6..06f70407 100644
--- a/ui/src/components/JsonEditor.tsx
+++ b/ui/src/components/JsonEditor.tsx
@@ -141,7 +141,7 @@ export function JsonEditor({ open, onOpenChange, showToast }: JsonEditorProps) {
return (
<>
{(isVisible || open) && (
- import('../../../plugins/core/PluginManager'));
import {
Dialog,
DialogContent,
@@ -13,7 +16,6 @@ import { Switch } from "@/components/ui/switch";
import { Combobox } from "@/components/ui/combobox";
import { useConfig } from "./ConfigProvider";
import { StatusLineConfigDialog } from "./StatusLineConfigDialog";
-import { useState } from "react";
import type { StatusLineConfig } from "@/types";
interface SettingsDialogProps {
@@ -21,6 +23,7 @@ interface SettingsDialogProps {
onOpenChange: (isOpen: boolean) => void;
}
+
export function SettingsDialog({ isOpen, onOpenChange }: SettingsDialogProps) {
const { t } = useTranslation();
const { config, setConfig } = useConfig();
@@ -59,7 +62,10 @@ export function SettingsDialog({ isOpen, onOpenChange }: SettingsDialogProps) {
return (
+
+ {/* Plugin Manager - Single component from plugins folder */}
+