""" EU-Utility - Modern UI Design System ===================================== A beautiful, modern design system for EU-Utility featuring: - Dark gaming aesthetic with EU orange accents - Glassmorphism effects throughout - Smooth 60fps animations - Responsive layouts - Professional iconography Design Principles: 1. Visual Hierarchy - Clear distinction between elements 2. Consistency - Unified design language 3. Feedback - Clear interactive states 4. Performance - GPU-accelerated animations 5. Accessibility - WCAG compliant contrast ratios """ from PyQt6.QtCore import Qt, QPropertyAnimation, QEasingCurve, QParallelAnimationGroup, QTimer from PyQt6.QtCore import pyqtSignal, QSize, QPoint, QRectF from PyQt6.QtWidgets import ( QWidget, QFrame, QPushButton, QLabel, QVBoxLayout, QHBoxLayout, QGraphicsDropShadowEffect, QGraphicsOpacityEffect, QLineEdit, QScrollArea, QStackedWidget, QProgressBar, QTextEdit, QComboBox ) from PyQt6.QtGui import ( QColor, QPainter, QLinearGradient, QRadialGradient, QFont, QFontDatabase, QIcon, QPixmap, QCursor, QPainterPath ) from typing import Optional, Callable, List, Dict, Any import math # ============================================================================= # DESIGN TOKENS - Centralized Design System # ============================================================================= class DesignTokens: """Central design tokens for consistent UI across the application.""" # Brand Colors BRAND_ORANGE = "#FF6B35" BRAND_ORANGE_LIGHT = "#FF8C5A" BRAND_ORANGE_DARK = "#E55A2B" BRAND_ORANGE_GLOW = "rgba(255, 107, 53, 0.4)" # Extended Color Palette COLORS = { # Primary 'primary': '#FF6B35', 'primary_hover': '#FF8C5A', 'primary_pressed': '#E55A2B', 'primary_glow': 'rgba(255, 107, 53, 0.4)', # Backgrounds - Deep space aesthetic 'bg_darkest': '#0A0C10', 'bg_dark': '#111318', 'bg_card': '#161920', 'bg_elevated': '#1D2129', 'bg_hover': '#252A33', 'bg_pressed': '#2D333D', # Surfaces with glassmorphism 'surface': 'rgba(22, 25, 32, 0.85)', 'surface_hover': 'rgba(29, 33, 41, 0.9)', 'surface_active': 'rgba(37, 42, 51, 0.95)', # Accents 'accent_teal': '#00D4AA', 'accent_blue': '#4D9CFF', 'accent_purple': '#A855F7', 'accent_yellow': '#FBBF24', 'accent_green': '#22C55E', 'accent_red': '#EF4444', # Text 'text_primary': '#F0F4F8', 'text_secondary': '#9CA3AF', 'text_muted': '#6B7280', 'text_disabled': '#4B5563', # Borders 'border_subtle': 'rgba(255, 255, 255, 0.06)', 'border_default': 'rgba(255, 255, 255, 0.1)', 'border_hover': 'rgba(255, 255, 255, 0.15)', 'border_focus': 'rgba(255, 107, 53, 0.5)', 'border_active': 'rgba(255, 107, 53, 0.8)', # Status 'success': '#22C55E', 'warning': '#FBBF24', 'error': '#EF4444', 'info': '#4D9CFF', } # Typography TYPOGRAPHY = { 'font_family': '"Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif', 'font_mono': '"JetBrains Mono", "Fira Code", monospace', 'size_xs': 11, 'size_sm': 12, 'size_base': 13, 'size_md': 14, 'size_lg': 16, 'size_xl': 18, 'size_2xl': 20, 'size_3xl': 24, 'size_4xl': 30, 'size_5xl': 36, } # Spacing (4px grid system) SPACING = { '0': 0, '1': 4, '2': 8, '3': 12, '4': 16, '5': 20, '6': 24, '8': 32, '10': 40, '12': 48, '16': 64, } # Border Radius RADIUS = { 'none': 0, 'sm': 4, 'md': 8, 'lg': 12, 'xl': 16, '2xl': 20, '3xl': 24, 'full': 9999, } # Shadows SHADOWS = { 'sm': '0 1px 2px rgba(0, 0, 0, 0.3)', 'md': '0 4px 6px rgba(0, 0, 0, 0.4)', 'lg': '0 10px 15px rgba(0, 0, 0, 0.5)', 'xl': '0 20px 25px rgba(0, 0, 0, 0.6)', 'glow': '0 0 20px rgba(255, 107, 53, 0.3)', 'glow_strong': '0 0 30px rgba(255, 107, 53, 0.5)', } # Animation Durations (ms) DURATION = { 'fast': 150, 'normal': 250, 'slow': 350, 'slower': 500, } # Easing Curves EASING = { 'default': QEasingCurve.Type.OutCubic, 'bounce': QEasingCurve.Type.OutBounce, 'elastic': QEasingCurve.Type.OutElastic, 'smooth': QEasingCurve.Type.InOutCubic, } @classmethod def color(cls, name: str) -> str: """Get color by name.""" return cls.COLORS.get(name, '#FFFFFF') @classmethod def spacing(cls, name: str) -> int: """Get spacing value.""" return cls.SPACING.get(name, 0) @classmethod def radius(cls, name: str) -> int: """Get border radius value.""" return cls.RADIUS.get(name, 0) # ============================================================================= # ANIMATION UTILITIES # ============================================================================= class AnimationManager: """Manages smooth GPU-accelerated animations.""" _active_animations: List[QPropertyAnimation] = [] @classmethod def fade_in(cls, widget: QWidget, duration: int = 250) -> QPropertyAnimation: """Fade in a widget smoothly.""" effect = QGraphicsOpacityEffect(widget) widget.setGraphicsEffect(effect) anim = QPropertyAnimation(effect, b"opacity") anim.setDuration(duration) anim.setStartValue(0.0) anim.setEndValue(1.0) anim.setEasingCurve(QEasingCurve.Type.OutCubic) cls._active_animations.append(anim) anim.finished.connect(lambda: cls._cleanup_animation(anim)) return anim @classmethod def fade_out(cls, widget: QWidget, duration: int = 200, on_finish: Optional[Callable] = None) -> QPropertyAnimation: """Fade out a widget smoothly.""" effect = widget.graphicsEffect() if not isinstance(effect, QGraphicsOpacityEffect): effect = QGraphicsOpacityEffect(widget) widget.setGraphicsEffect(effect) anim = QPropertyAnimation(effect, b"opacity") anim.setDuration(duration) anim.setStartValue(1.0) anim.setEndValue(0.0) anim.setEasingCurve(QEasingCurve.Type.InCubic) if on_finish: anim.finished.connect(on_finish) cls._active_animations.append(anim) anim.finished.connect(lambda: cls._cleanup_animation(anim)) return anim @classmethod def slide_in(cls, widget: QWidget, direction: str = "bottom", duration: int = 300) -> QPropertyAnimation: """Slide widget in from specified direction.""" anim = QPropertyAnimation(widget, b"pos") anim.setDuration(duration) anim.setEasingCurve(QEasingCurve.Type.OutCubic) current_pos = widget.pos() if direction == "left": start_pos = current_pos - QPoint(widget.width() + 20, 0) elif direction == "right": start_pos = current_pos + QPoint(widget.width() + 20, 0) elif direction == "top": start_pos = current_pos - QPoint(0, widget.height() + 20) else: # bottom start_pos = current_pos + QPoint(0, widget.height() + 20) anim.setStartValue(start_pos) anim.setEndValue(current_pos) cls._active_animations.append(anim) anim.finished.connect(lambda: cls._cleanup_animation(anim)) return anim @classmethod def scale(cls, widget: QWidget, from_scale: float = 0.9, to_scale: float = 1.0, duration: int = 250) -> QPropertyAnimation: """Scale animation for widgets.""" anim = QPropertyAnimation(widget, b"minimumWidth") anim.setDuration(duration) anim.setEasingCurve(QEasingCurve.Type.OutBack) base_width = widget.width() anim.setStartValue(int(base_width * from_scale)) anim.setEndValue(int(base_width * to_scale)) cls._active_animations.append(anim) anim.finished.connect(lambda: cls._cleanup_animation(anim)) return anim @classmethod def pulse_glow(cls, widget: QWidget, duration: int = 2000) -> QPropertyAnimation: """Create a pulsing glow effect.""" effect = QGraphicsDropShadowEffect(widget) effect.setColor(QColor(255, 107, 53)) effect.setBlurRadius(20) effect.setOffset(0, 0) widget.setGraphicsEffect(effect) anim = QPropertyAnimation(effect, b"blurRadius") anim.setDuration(duration) anim.setStartValue(20) anim.setEndValue(40) anim.setEasingCurve(QEasingCurve.Type.InOutSine) anim.setLoopCount(-1) return anim @classmethod def _cleanup_animation(cls, anim: QPropertyAnimation): """Remove completed animation from tracking.""" if anim in cls._active_animations: cls._active_animations.remove(anim) # ============================================================================= # MODERN COMPONENTS # ============================================================================= class GlassCard(QFrame): """Glassmorphism card with frosted glass effect.""" clicked = pyqtSignal() def __init__(self, parent=None, elevation: int = 1, hover_lift: bool = True): super().__init__(parent) self.elevation = elevation self.hover_lift = hover_lift self._hovered = False self._setup_style() self._setup_shadow() if hover_lift: self.setMouseTracking(True) def _setup_style(self): """Apply glassmorphism styling.""" c = DesignTokens.COLORS opacity = 0.85 + (self.elevation * 0.03) self.setStyleSheet(f""" GlassCard {{ background: rgba(22, 25, 32, {min(opacity, 0.95)}); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: {DesignTokens.radius('xl')}px; }} """) def _setup_shadow(self): """Apply elevation shadow.""" self._shadow = QGraphicsDropShadowEffect(self) self._shadow.setBlurRadius(self.elevation * 15) self._shadow.setColor(QColor(0, 0, 0, int(80 + self.elevation * 20))) self._shadow.setOffset(0, self.elevation * 3) self.setGraphicsEffect(self._shadow) def enterEvent(self, event): """Hover effect.""" if self.hover_lift: self._hovered = True self._shadow.setBlurRadius((self.elevation + 1) * 15) self._shadow.setColor(QColor(0, 0, 0, int(100 + (self.elevation + 1) * 20))) self.setStyleSheet(f""" GlassCard {{ background: rgba(29, 33, 41, 0.9); border: 1px solid rgba(255, 107, 53, 0.2); border-radius: {DesignTokens.radius('xl')}px; }} """) super().enterEvent(event) def leaveEvent(self, event): """Reset hover effect.""" if self.hover_lift: self._hovered = False self._setup_shadow() self._setup_style() super().leaveEvent(event) def mousePressEvent(self, event): """Handle click.""" if event.button() == Qt.MouseButton.LeftButton: self.clicked.emit() super().mousePressEvent(event) class ModernButton(QPushButton): """Modern button with smooth animations and multiple variants.""" VARIANTS = ['primary', 'secondary', 'ghost', 'outline', 'danger', 'glass'] def __init__(self, text: str = "", variant: str = 'primary', icon: str = None, parent=None): super().__init__(text, parent) self.variant = variant self.icon_text = icon self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self._apply_style() self._setup_animations() def _apply_style(self): """Apply button styling based on variant.""" c = DesignTokens.COLORS r = DesignTokens.radius('full') styles = { 'primary': f""" ModernButton {{ background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 {c['primary']}, stop:1 {c['primary_hover']}); color: white; border: none; border-radius: {r}px; padding: 12px 24px; font-size: {DesignTokens.TYPOGRAPHY['size_md']}px; font-weight: 600; }} ModernButton:hover {{ background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 {c['primary_hover']}, stop:1 {c['primary']}); }} ModernButton:pressed {{ background: {c['primary_pressed']}; }} """, 'secondary': f""" ModernButton {{ background: {c['bg_elevated']}; color: {c['text_primary']}; border: 1px solid {c['border_default']}; border-radius: {r}px; padding: 12px 24px; font-size: {DesignTokens.TYPOGRAPHY['size_md']}px; font-weight: 600; }} ModernButton:hover {{ background: {c['bg_hover']}; border-color: {c['border_hover']}; }} """, 'ghost': f""" ModernButton {{ background: transparent; color: {c['text_secondary']}; border: none; border-radius: {r}px; padding: 12px 24px; font-size: {DesignTokens.TYPOGRAPHY['size_md']}px; font-weight: 500; }} ModernButton:hover {{ background: rgba(255, 255, 255, 0.05); color: {c['text_primary']}; }} """, 'outline': f""" ModernButton {{ background: transparent; color: {c['primary']}; border: 2px solid {c['primary']}; border-radius: {r}px; padding: 10px 22px; font-size: {DesignTokens.TYPOGRAPHY['size_md']}px; font-weight: 600; }} ModernButton:hover {{ background: rgba(255, 107, 53, 0.1); }} """, 'danger': f""" ModernButton {{ background: {c['error']}; color: white; border: none; border-radius: {r}px; padding: 12px 24px; font-size: {DesignTokens.TYPOGRAPHY['size_md']}px; font-weight: 600; }} ModernButton:hover {{ background: #DC2626; }} """, 'glass': f""" ModernButton {{ background: rgba(255, 255, 255, 0.08); color: white; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: {r}px; padding: 12px 24px; font-size: {DesignTokens.TYPOGRAPHY['size_md']}px; font-weight: 600; backdrop-filter: blur(10px); }} ModernButton:hover {{ background: rgba(255, 255, 255, 0.12); border-color: rgba(255, 107, 53, 0.3); }} """ } self.setStyleSheet(styles.get(self.variant, styles['primary'])) self.setFixedHeight(44) def _setup_animations(self): """Setup press animation.""" self._press_anim = QPropertyAnimation(self, b"minimumHeight") self._press_anim.setDuration(100) self._press_anim.setEasingCurve(QEasingCurve.Type.OutQuad) def enterEvent(self, event): """Hover animation.""" super().enterEvent(event) def mousePressEvent(self, event): """Press animation.""" if event.button() == Qt.MouseButton.LeftButton: self._press_anim.setStartValue(44) self._press_anim.setEndValue(42) self._press_anim.start() super().mousePressEvent(event) def mouseReleaseEvent(self, event): """Release animation.""" self._press_anim.setStartValue(42) self._press_anim.setEndValue(44) self._press_anim.start() super().mouseReleaseEvent(event) class ModernInput(QLineEdit): """Modern input field with floating label and animations.""" def __init__(self, placeholder: str = "", parent=None): super().__init__(parent) self.setPlaceholderText(placeholder) self._apply_style() def _apply_style(self): """Apply modern input styling.""" c = DesignTokens.COLORS r = DesignTokens.radius('lg') self.setStyleSheet(f""" ModernInput {{ background: {c['bg_elevated']}; color: {c['text_primary']}; border: 2px solid {c['border_default']}; border-radius: {r}px; padding: 12px 16px; font-size: {DesignTokens.TYPOGRAPHY['size_md']}px; selection-background-color: {c['primary']}; }} ModernInput:hover {{ border-color: {c['border_hover']}; }} ModernInput:focus {{ border-color: {c['primary']}; background: {c['bg_card']}; }} ModernInput::placeholder {{ color: {c['text_muted']}; }} """) self.setFixedHeight(48) class ModernComboBox(QComboBox): """Modern dropdown with custom styling.""" def __init__(self, parent=None): super().__init__(parent) self._apply_style() def _apply_style(self): """Apply modern combobox styling.""" c = DesignTokens.COLORS r = DesignTokens.radius('lg') self.setStyleSheet(f""" ModernComboBox {{ background: {c['bg_elevated']}; color: {c['text_primary']}; border: 2px solid {c['border_default']}; border-radius: {r}px; padding: 8px 12px; font-size: {DesignTokens.TYPOGRAPHY['size_md']}px; min-width: 150px; }} ModernComboBox:hover {{ border-color: {c['border_hover']}; }} ModernComboBox:focus {{ border-color: {c['primary']}; }} ModernComboBox::drop-down {{ border: none; width: 30px; }} ModernComboBox::down-arrow {{ image: none; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid {c['text_secondary']}; }} ModernComboBox QAbstractItemView {{ background: {c['bg_elevated']}; color: {c['text_primary']}; border: 1px solid {c['border_default']}; border-radius: {r}px; selection-background-color: {c['bg_hover']}; selection-color: {c['text_primary']}; padding: 4px; }} """) self.setFixedHeight(48) class Badge(QLabel): """Status badge with various styles.""" STYLES = ['default', 'success', 'warning', 'error', 'info', 'primary'] def __init__(self, text: str = "", style: str = 'default', parent=None): super().__init__(text, parent) self.badge_style = style self._apply_style() self.setAlignment(Qt.AlignmentFlag.AlignCenter) def _apply_style(self): """Apply badge styling.""" c = DesignTokens.COLORS colors = { 'default': ('rgba(255,255,255,0.1)', c['text_secondary']), 'success': ('rgba(34, 197, 94, 0.2)', c['accent_green']), 'warning': ('rgba(251, 191, 36, 0.2)', c['accent_yellow']), 'error': ('rgba(239, 68, 68, 0.2)', c['accent_red']), 'info': ('rgba(77, 156, 255, 0.2)', c['accent_blue']), 'primary': ('rgba(255, 107, 53, 0.2)', c['primary']), } bg, fg = colors.get(self.badge_style, colors['default']) self.setStyleSheet(f""" Badge {{ background: {bg}; color: {fg}; border-radius: {DesignTokens.radius('full')}px; padding: 4px 12px; font-size: {DesignTokens.TYPOGRAPHY['size_xs']}px; font-weight: 600; }} """) # Add subtle shadow shadow = QGraphicsDropShadowEffect(self) shadow.setBlurRadius(4) shadow.setColor(QColor(0, 0, 0, 40)) shadow.setOffset(0, 1) self.setGraphicsEffect(shadow) class ProgressIndicator(QProgressBar): """Modern progress indicator with gradient.""" def __init__(self, parent=None): super().__init__(parent) self._apply_style() self.setTextVisible(False) self.setRange(0, 100) self.setValue(0) def _apply_style(self): """Apply modern progress styling.""" c = DesignTokens.COLORS self.setStyleSheet(f""" ProgressIndicator {{ background: {c['bg_elevated']}; border: none; border-radius: {DesignTokens.radius('full')}px; height: 6px; }} ProgressIndicator::chunk {{ background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 {c['primary']}, stop:1 {c['primary_hover']}); border-radius: {DesignTokens.radius('full')}px; }} """) self.setFixedHeight(6) class IconButton(QPushButton): """Circular icon button with hover effects.""" def __init__(self, icon_text: str = "", size: int = 40, tooltip: str = "", parent=None): super().__init__(icon_text, parent) self.setFixedSize(size, size) self.setToolTip(tooltip) self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self._apply_style() def _apply_style(self): """Apply icon button styling.""" c = DesignTokens.COLORS size = self.width() self.setStyleSheet(f""" IconButton {{ background: transparent; color: {c['text_secondary']}; border: none; border-radius: {size // 2}px; font-size: 16px; }} IconButton:hover {{ background: rgba(255, 255, 255, 0.1); color: {c['text_primary']}; }} IconButton:pressed {{ background: rgba(255, 255, 255, 0.15); }} """) # ============================================================================= # LAYOUT HELPERS # ============================================================================= def create_spacer(horizontal: bool = False, size: int = None): """Create a spacer item.""" from PyQt6.QtWidgets import QSpacerItem, QSizePolicy if horizontal: policy = QSizePolicy.Policy.Expanding min_policy = QSizePolicy.Policy.Minimum return QSpacerItem(size or 0, 0, policy, min_policy) else: policy = QSizePolicy.Policy.Expanding min_policy = QSizePolicy.Policy.Minimum return QSpacerItem(0, size or 0, min_policy, policy) def create_separator(horizontal: bool = True): """Create a styled separator line.""" separator = QFrame() if horizontal: separator.setFrameShape(QFrame.Shape.HLine) separator.setFixedHeight(1) else: separator.setFrameShape(QFrame.Shape.VLine) separator.setFixedWidth(1) separator.setStyleSheet(f""" background: {DesignTokens.color('border_subtle')}; """) return separator # ============================================================================= # GLOBAL STYLESHEET # ============================================================================= def get_global_stylesheet() -> str: """Get complete global stylesheet for the application.""" c = DesignTokens.COLORS return f""" /* Base */ QWidget {{ font-family: {DesignTokens.TYPOGRAPHY['font_family']}; font-size: {DesignTokens.TYPOGRAPHY['size_base']}px; color: {c['text_primary']}; }} /* Main Window */ QMainWindow {{ background: {c['bg_darkest']}; }} /* Selection */ ::selection {{ background: {c['primary']}; color: white; }} /* Scrollbars */ QScrollBar:vertical {{ background: transparent; width: 8px; margin: 0; }} QScrollBar::handle:vertical {{ background: {c['border_default']}; border-radius: 4px; min-height: 40px; }} QScrollBar::handle:vertical:hover {{ background: {c['border_hover']}; }} QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0; }} QScrollBar:horizontal {{ background: transparent; height: 8px; margin: 0; }} QScrollBar::handle:horizontal {{ background: {c['border_default']}; border-radius: 4px; min-width: 40px; }} /* Tooltips */ QToolTip {{ background: {c['bg_elevated']}; color: {c['text_primary']}; border: 1px solid {c['border_default']}; border-radius: 8px; padding: 8px 12px; font-size: {DesignTokens.TYPOGRAPHY['size_sm']}px; }} /* Menu */ QMenu {{ background: {c['bg_elevated']}; color: {c['text_primary']}; border: 1px solid {c['border_default']}; border-radius: 12px; padding: 8px; }} QMenu::item {{ padding: 10px 20px; border-radius: 8px; }} QMenu::item:selected {{ background: {c['bg_hover']}; }} QMenu::separator {{ height: 1px; background: {c['border_subtle']}; margin: 8px 0; }} /* Check Box */ QCheckBox {{ spacing: 8px; }} QCheckBox::indicator {{ width: 20px; height: 20px; border: 2px solid {c['border_default']}; border-radius: 6px; background: {c['bg_elevated']}; }} QCheckBox::indicator:hover {{ border-color: {c['border_hover']}; }} QCheckBox::indicator:checked {{ background: {c['primary']}; border-color: {c['primary']}; }} /* Slider */ QSlider::groove:horizontal {{ height: 4px; background: {c['bg_elevated']}; border-radius: 2px; }} QSlider::handle:horizontal {{ width: 18px; height: 18px; background: {c['primary']}; border-radius: 9px; margin: -7px 0; }} QSlider::sub-page:horizontal {{ background: {c['primary']}; border-radius: 2px; }} """