"""Tema visual del Studio — aplica los design tokens del Sprint 0 a PySide6. Los tokens canónicos viven en `docs/design_system.md` y `docs/mockups/_tokens.css`. Este módulo replica esos valores en Python para construir el QSS (Qt Style Sheets) y los QPalette necesarios. """ from __future__ import annotations from PySide6.QtGui import QColor, QFont, QFontDatabase, QPalette from PySide6.QtWidgets import QApplication # ----- Paleta Deep Ocean (canónica) -------------------------------------- C_ABYSS = "#04111F" C_MIDNIGHT = "#0A1A2E" C_STEEL = "#1A2B42" C_IRON = "#2C3E5C" C_FOG = "#7C8B9F" C_SAND = "#E6EAF0" C_FOAM = "#F2F5F9" C_CYAN = "#00D9FF" C_CYAN_DEEP = "#1B7FB5" C_HORIZON = "#5BC0EB" C_OK = "#00E08A" C_INFO = "#5BC0EB" C_WARN = "#FFB020" C_HIGH = "#FF8030" C_EMERGENCY = "#FF3B47" # ----- Fuentes ----------------------------------------------------------- FAMILY_DISPLAY = "Space Grotesk" FAMILY_UI = "Inter" FAMILY_MONO = "JetBrains Mono" FALLBACK_DISPLAY = "Segoe UI" FALLBACK_UI = "Segoe UI" FALLBACK_MONO = "Cascadia Mono" def best_family(preferred: str, fallback: str) -> str: """Devuelve `preferred` si está instalado, si no `fallback`.""" families = set(QFontDatabase.families()) if preferred in families: return preferred return fallback def display_font( point_size: int = 16, weight: QFont.Weight | int = QFont.Weight.DemiBold ) -> QFont: f = QFont(best_family(FAMILY_DISPLAY, FALLBACK_DISPLAY)) f.setPointSize(point_size) if isinstance(weight, int): # PySide6 6.11 requiere QFont.Weight, no int. Aproximamos. if weight <= 300: weight = QFont.Weight.Light elif weight <= 400: weight = QFont.Weight.Normal elif weight <= 500: weight = QFont.Weight.Medium elif weight <= 600: weight = QFont.Weight.DemiBold elif weight <= 700: weight = QFont.Weight.Bold else: weight = QFont.Weight.Black f.setWeight(weight) return f def ui_font(point_size: int = 10) -> QFont: f = QFont(best_family(FAMILY_UI, FALLBACK_UI)) f.setPointSize(point_size) return f def mono_font(point_size: int = 10) -> QFont: f = QFont(best_family(FAMILY_MONO, FALLBACK_MONO)) f.setPointSize(point_size) return f # ----- Stylesheet global ------------------------------------------------- _QSS = f""" QMainWindow, QDialog, QWidget {{ background-color: {C_ABYSS}; color: {C_SAND}; }} QWidget#topbar {{ background-color: {C_MIDNIGHT}; border-bottom: 1px solid {C_STEEL}; }} QWidget#sidebar {{ background-color: {C_MIDNIGHT}; border-right: 1px solid {C_STEEL}; }} QWidget#statusbar {{ background-color: {C_MIDNIGHT}; border-top: 1px solid {C_STEEL}; color: {C_FOG}; }} QLabel {{ color: {C_SAND}; }} QLabel#title {{ color: {C_FOAM}; font-weight: 600; }} QLabel#h1 {{ color: {C_FOAM}; font-size: 22pt; font-weight: 600; }} QLabel#caption {{ color: {C_FOG}; font-size: 9pt; }} QLabel#overline {{ color: {C_FOG}; font-size: 9pt; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; }} QPushButton {{ background-color: transparent; color: {C_SAND}; border: 1px solid {C_IRON}; border-radius: 6px; padding: 8px 16px; min-width: 80px; }} QPushButton:hover {{ background-color: {C_STEEL}; }} QPushButton:pressed {{ background-color: {C_IRON}; }} QPushButton:disabled {{ background-color: {C_STEEL}; color: {C_FOG}; border-color: transparent; }} QPushButton#primary {{ background-color: {C_CYAN}; color: {C_ABYSS}; border: none; font-weight: 600; }} QPushButton#primary:hover {{ background-color: {C_HORIZON}; }} QPushButton#danger {{ background-color: {C_EMERGENCY}; color: {C_FOAM}; border: none; font-weight: 700; }} QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox, QPlainTextEdit, QTextEdit {{ background-color: {C_STEEL}; color: {C_FOAM}; border: 1px solid {C_IRON}; border-radius: 4px; padding: 6px 10px; selection-background-color: {C_CYAN}; selection-color: {C_ABYSS}; }} QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QComboBox:focus {{ border-color: {C_CYAN}; }} QComboBox::drop-down {{ border: none; width: 24px; }} QComboBox::down-arrow {{ image: none; width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 6px solid {C_FOG}; margin-right: 8px; }} QComboBox QAbstractItemView {{ background-color: {C_MIDNIGHT}; color: {C_SAND}; border: 1px solid {C_IRON}; selection-background-color: {C_CYAN_DEEP}; selection-color: {C_FOAM}; }} QCheckBox {{ color: {C_SAND}; spacing: 8px; }} QCheckBox::indicator {{ width: 18px; height: 18px; background-color: {C_STEEL}; border: 1px solid {C_IRON}; border-radius: 3px; }} QCheckBox::indicator:hover {{ border-color: {C_CYAN}; }} QCheckBox::indicator:checked {{ background-color: {C_CYAN}; border-color: {C_CYAN}; image: none; }} QListWidget, QTreeWidget, QTableWidget {{ background-color: {C_MIDNIGHT}; color: {C_SAND}; border: 1px solid {C_STEEL}; border-radius: 6px; outline: 0; }} QListWidget::item, QTreeWidget::item {{ padding: 8px 10px; border-radius: 4px; }} QListWidget::item:hover, QTreeWidget::item:hover {{ background-color: {C_STEEL}; }} QListWidget::item:selected, QTreeWidget::item:selected {{ background-color: rgba(0, 217, 255, 30); color: {C_FOAM}; border-left: 2px solid {C_CYAN}; }} QHeaderView::section {{ background-color: {C_MIDNIGHT}; color: {C_FOG}; border: none; border-right: 1px solid {C_STEEL}; padding: 8px; font-weight: 600; }} QMenuBar {{ background-color: {C_MIDNIGHT}; color: {C_SAND}; border-bottom: 1px solid {C_STEEL}; }} QMenuBar::item {{ padding: 6px 12px; background: transparent; }} QMenuBar::item:selected {{ background-color: {C_STEEL}; color: {C_CYAN}; }} QMenu {{ background-color: {C_MIDNIGHT}; color: {C_SAND}; border: 1px solid {C_IRON}; }} QMenu::item {{ padding: 6px 24px; }} QMenu::item:selected {{ background-color: {C_STEEL}; color: {C_CYAN}; }} QToolBar {{ background-color: {C_MIDNIGHT}; border: none; spacing: 4px; padding: 4px; }} QStatusBar {{ background-color: {C_MIDNIGHT}; color: {C_FOG}; border-top: 1px solid {C_STEEL}; }} QScrollBar:vertical {{ background: {C_ABYSS}; width: 12px; border: none; }} QScrollBar::handle:vertical {{ background: {C_IRON}; border-radius: 6px; min-height: 30px; }} QScrollBar::handle:vertical:hover {{ background: {C_FOG}; }} QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0; }} QScrollBar:horizontal {{ background: {C_ABYSS}; height: 12px; border: none; }} QScrollBar::handle:horizontal {{ background: {C_IRON}; border-radius: 6px; min-width: 30px; }} QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{ height: 0; }} QGroupBox {{ color: {C_FOAM}; border: 1px solid {C_STEEL}; border-radius: 8px; margin-top: 18px; padding-top: 12px; font-weight: 600; }} QGroupBox::title {{ subcontrol-origin: margin; left: 12px; padding: 0 6px; color: {C_FOG}; font-size: 9pt; letter-spacing: 1.5px; text-transform: uppercase; }} QWizard {{ background-color: {C_ABYSS}; }} QWizard QWidget {{ background-color: {C_ABYSS}; }} QToolTip {{ background-color: {C_MIDNIGHT}; color: {C_FOAM}; border: 1px solid {C_CYAN}; padding: 6px 10px; border-radius: 4px; }} """ def apply_theme(app: QApplication) -> None: """Aplica el tema completo Deep Ocean a la app. - Stylesheet QSS - QPalette (para widgets que no respetan QSS) - Fuentes default """ app.setStyle("Fusion") palette = QPalette() palette.setColor(QPalette.Window, QColor(C_ABYSS)) palette.setColor(QPalette.WindowText, QColor(C_SAND)) palette.setColor(QPalette.Base, QColor(C_MIDNIGHT)) palette.setColor(QPalette.AlternateBase, QColor(C_STEEL)) palette.setColor(QPalette.Text, QColor(C_FOAM)) palette.setColor(QPalette.Button, QColor(C_STEEL)) palette.setColor(QPalette.ButtonText, QColor(C_SAND)) palette.setColor(QPalette.BrightText, QColor(C_EMERGENCY)) palette.setColor(QPalette.Highlight, QColor(C_CYAN)) palette.setColor(QPalette.HighlightedText, QColor(C_ABYSS)) palette.setColor(QPalette.Link, QColor(C_CYAN)) palette.setColor(QPalette.ToolTipBase, QColor(C_MIDNIGHT)) palette.setColor(QPalette.ToolTipText, QColor(C_FOAM)) palette.setColor(QPalette.PlaceholderText, QColor(C_FOG)) app.setPalette(palette) app.setFont(ui_font(10)) app.setStyleSheet(_QSS)