diff --git a/src/App.css b/src/App.css index 4faacb1..f7816f3 100644 --- a/src/App.css +++ b/src/App.css @@ -219,12 +219,46 @@ body { /* Graph Cards */ .cpu-graph, .memory-graph { + --mouse-x: 50%; + --mouse-y: 50%; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 12px; padding: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); transition: border-color 0.3s, box-shadow 0.3s; + position: relative; + overflow: hidden; +} + +.cpu-graph::before, +.memory-graph::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient( + circle 200px at var(--mouse-x) var(--mouse-y), + rgba(96, 165, 250, 0.12) 0%, + transparent 70% + ); + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; + z-index: 0; +} + +.cpu-graph > *, +.memory-graph > * { + position: relative; + z-index: 1; +} + +.cpu-graph:hover::before, +.memory-graph:hover::before { + opacity: 1; } .cpu-graph:hover, @@ -233,6 +267,89 @@ body { box-shadow: 0 4px 30px rgba(96, 165, 250, 0.15); } +/* Pulsating graph effect on hover */ +.cpu-graph.graph-hovered .graph-container, +.memory-graph.graph-hovered .graph-container { + animation: graphPulsate 2s ease-in-out infinite; +} + +.cpu-graph.graph-hovered .graph-value, +.memory-graph.graph-hovered .graph-value { + animation: valuePulsate 1.5s ease-in-out infinite; +} + +.cpu-graph.graph-hovered .core-indicator { + animation: corePulsate 2s ease-in-out infinite; +} + +.cpu-graph.graph-hovered .core-indicator:nth-child(odd) { + animation-delay: 0.15s; +} + +.cpu-graph.graph-hovered .core-indicator:nth-child(3n) { + animation-delay: 0.3s; +} + +.memory-graph.graph-hovered .memory-bar-fill { + animation: barPulsate 1.8s ease-in-out infinite; +} + +.memory-graph.graph-hovered .memory-stat .stat-value { + animation: valuePulsate 2s ease-in-out infinite; +} + +.memory-graph.graph-hovered .memory-stat:nth-child(2) .stat-value { + animation-delay: 0.2s; +} + +.memory-graph.graph-hovered .memory-stat:nth-child(3) .stat-value { + animation-delay: 0.4s; +} + +@keyframes graphPulsate { + 0%, 100% { + filter: brightness(1) saturate(1); + transform: scale(1); + } + 50% { + filter: brightness(1.3) saturate(1.4); + transform: scale(1.01); + } +} + +@keyframes valuePulsate { + 0%, 100% { + opacity: 1; + text-shadow: 0 0 20px currentColor; + } + 50% { + opacity: 0.8; + text-shadow: 0 0 40px currentColor, 0 0 60px currentColor; + } +} + +@keyframes corePulsate { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.85; + transform: scale(1.02); + } +} + +@keyframes barPulsate { + 0%, 100% { + filter: brightness(1); + box-shadow: 0 0 20px currentColor; + } + 50% { + filter: brightness(1.4); + box-shadow: 0 0 35px currentColor, 0 0 50px currentColor; + } +} + .graph-header { display: flex; justify-content: space-between; @@ -450,6 +567,113 @@ body { .col-time { width: 60px; color: var(--color-text-dim); } .col-command { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +/* Alert Bar */ +.alert-bar { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 16px; + border-radius: 12px; + border: 1px solid var(--border-color); + flex-shrink: 0; + transition: opacity 0.2s ease; +} + +.alert-bar.alert-flash-off { + opacity: 0.4; +} + +.alert-bar.alert-info { + background: linear-gradient(145deg, rgba(52, 211, 153, 0.1) 0%, rgba(13, 18, 25, 0.8) 100%); + border-color: rgba(52, 211, 153, 0.3); +} + +.alert-bar.alert-warn { + background: linear-gradient(145deg, rgba(251, 191, 36, 0.1) 0%, rgba(13, 18, 25, 0.8) 100%); + border-color: rgba(251, 191, 36, 0.3); +} + +.alert-bar.alert-critical { + background: linear-gradient(145deg, rgba(248, 113, 113, 0.15) 0%, rgba(13, 18, 25, 0.8) 100%); + border-color: rgba(248, 113, 113, 0.4); +} + +.alert-icon { + font-weight: 900; + font-size: 12px; + width: 22px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.alert-info .alert-icon { + color: var(--color-green); + background: rgba(52, 211, 153, 0.15); +} + +.alert-warn .alert-icon { + color: var(--color-yellow); + background: rgba(251, 191, 36, 0.15); +} + +.alert-critical .alert-icon { + color: var(--color-red); + background: rgba(248, 113, 113, 0.15); +} + +.alert-messages { + display: flex; + gap: 16px; + flex: 1; +} + +.alert-msg { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.5px; +} + +.alert-msg-info { color: var(--color-green); } +.alert-msg-warn { color: var(--color-yellow); } +.alert-msg-critical { color: var(--color-red); } + +.alert-timestamp { + color: var(--color-text-dim); + font-size: 10px; + margin-left: auto; +} + +/* Status Indicator */ +.status-indicator { + display: flex; + align-items: center; + gap: 6px; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--color-green); + opacity: 0.3; + transition: opacity 0.3s ease; +} + +.status-dot.active { + opacity: 1; + box-shadow: 0 0 8px var(--color-green), 0 0 16px rgba(52, 211, 153, 0.4); +} + +.status-text { + color: var(--color-green); + font-size: 10px; + font-weight: 700; + letter-spacing: 2px; +} + /* Status Bar */ .status-bar { display: flex; diff --git a/src/App.tsx b/src/App.tsx index 60c8dc0..7421865 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { CpuGraph } from './components/CpuGraph'; import { MemoryGraph } from './components/MemoryGraph'; import { ProcessTable } from './components/ProcessTable'; import { StatusBar } from './components/StatusBar'; +import { AlertBar } from './components/AlertBar'; import { EnvironmentPanel } from './components/EnvironmentPanel'; import { useSystemMetrics } from './hooks/useSystemMetrics'; import './App.css'; @@ -71,6 +72,8 @@ function App() { + + a + b, 0) / cpuUsage.length; + + if (avgCpu > 90) { + alerts.push({ message: 'CPU usage critical', level: 'critical' }); + } else if (avgCpu > 70) { + alerts.push({ message: 'CPU usage high', level: 'warn' }); + } + + if (memPercent > 90) { + alerts.push({ message: 'Memory usage critical', level: 'critical' }); + } else if (memPercent > 70) { + alerts.push({ message: 'Memory usage high', level: 'warn' }); + } + + if (alerts.length === 0) { + alerts.push({ message: 'All systems nominal', level: 'info' }); + } + + return alerts; +} + +export function AlertBar({ cpuUsage, memPercent }: AlertBarProps) { + const [visible, setVisible] = useState(true); + const alerts = getAlerts(cpuUsage, memPercent); + const hasWarnings = alerts.some((a) => a.level !== 'info'); + + useEffect(() => { + if (!hasWarnings) return; + const interval = setInterval(() => { + setVisible((prev) => !prev); + }, 800); + return () => clearInterval(interval); + }, [hasWarnings]); + + const highestLevel = alerts.reduce<'info' | 'warn' | 'critical'>((max, a) => { + const order = { info: 0, warn: 1, critical: 2 }; + return order[a.level] > order[max] ? a.level : max; + }, 'info'); + + return ( +
+ + {highestLevel === 'critical' ? '!!' : highestLevel === 'warn' ? '!' : '~'} + +
+ {alerts.map((alert, i) => ( + + {alert.message} + + ))} +
+ + {new Date().toLocaleTimeString()} + +
+ ); +} diff --git a/src/components/CpuGraph.tsx b/src/components/CpuGraph.tsx index a5a510a..fc03870 100644 --- a/src/components/CpuGraph.tsx +++ b/src/components/CpuGraph.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { AreaChart, Area, @@ -36,6 +36,16 @@ const COLORS = [ export function CpuGraph({ cpuUsage }: CpuGraphProps) { const [history, setHistory] = useState([]); + const [isHovered, setIsHovered] = useState(false); + const containerRef = useRef(null); + + const handleMouseMove = useCallback((e: React.MouseEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + const x = ((e.clientX - rect.left) / rect.width) * 100; + const y = ((e.clientY - rect.top) / rect.height) * 100; + e.currentTarget.style.setProperty('--mouse-x', `${x}%`); + e.currentTarget.style.setProperty('--mouse-y', `${y}%`); + }, []); useEffect(() => { const newPoint: HistoryPoint = { time: Date.now() }; @@ -57,7 +67,13 @@ export function CpuGraph({ cpuUsage }: CpuGraphProps) { : 0; return ( -
+
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onMouseMove={handleMouseMove} + >
CPU diff --git a/src/components/MemoryBar.tsx b/src/components/MemoryBar.tsx index 870bfa4..390ccfb 100644 --- a/src/components/MemoryBar.tsx +++ b/src/components/MemoryBar.tsx @@ -8,9 +8,9 @@ interface MemoryBarProps { function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; - const sizes = ['B', 'K', 'M', 'G', 'T']; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + sizes[i]; + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } export function MemoryBar({ label, used, total, percent }: MemoryBarProps) { diff --git a/src/components/MemoryGraph.tsx b/src/components/MemoryGraph.tsx index 995fe06..890a364 100644 --- a/src/components/MemoryGraph.tsx +++ b/src/components/MemoryGraph.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { AreaChart, Area, @@ -32,6 +32,16 @@ function formatBytes(bytes: number): string { export function MemoryGraph({ used, total, percent }: MemoryGraphProps) { const [history, setHistory] = useState([]); + const [isHovered, setIsHovered] = useState(false); + const containerRef = useRef(null); + + const handleMouseMove = useCallback((e: React.MouseEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + const x = ((e.clientX - rect.left) / rect.width) * 100; + const y = ((e.clientY - rect.top) / rect.height) * 100; + e.currentTarget.style.setProperty('--mouse-x', `${x}%`); + e.currentTarget.style.setProperty('--mouse-y', `${y}%`); + }, []); useEffect(() => { const newPoint: HistoryPoint = { @@ -59,11 +69,17 @@ export function MemoryGraph({ used, total, percent }: MemoryGraphProps) { const color = getMemoryColor(percent); return ( -
+
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onMouseMove={handleMouseMove} + >
Memory - {percent}% + {formatBytes(used)} / {formatBytes(total)} ({percent}%)
@@ -113,7 +129,7 @@ export function MemoryGraph({ used, total, percent }: MemoryGraphProps) {
Used - {formatBytes(used)} + {formatBytes(used)} ({percent}%) Total @@ -121,7 +137,7 @@ export function MemoryGraph({ used, total, percent }: MemoryGraphProps) { Free - {formatBytes(total - used)} + {formatBytes(total - used)} ({total > 0 ? Math.round(((total - used) / total) * 100) : 0}%)
diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx index 0180316..19abb56 100644 --- a/src/components/StatusBar.tsx +++ b/src/components/StatusBar.tsx @@ -1,3 +1,5 @@ +import { useState, useEffect } from 'react'; + interface StatusBarProps { filter: string; onFilterChange: (filter: string) => void; @@ -6,6 +8,15 @@ interface StatusBarProps { } export function StatusBar({ filter, onFilterChange, refreshRate, onRefreshRateChange }: StatusBarProps) { + const [statusActive, setStatusActive] = useState(true); + + useEffect(() => { + const interval = setInterval(() => { + setStatusActive((prev) => !prev); + }, 1000); + return () => clearInterval(interval); + }, []); + const shortcuts = [ { key: 'F1', label: 'Help' }, { key: 'F2', label: 'Setup' }, @@ -19,6 +30,10 @@ export function StatusBar({ filter, onFilterChange, refreshRate, onRefreshRateCh return (
+
+ + LIVE +
Filter: