Source code for spacr.qt.theme

"""
Dark palette + QSS stylesheet for the spacr Qt GUI.

Single source of truth for every color, radius, and font size used by the
custom widgets and screens. Import `PALETTE` for programmatic access and
`stylesheet()` for the Qt StyleSheet string to hand to
`QApplication.setStyleSheet`.
"""
from __future__ import annotations

from PySide6.QtGui import QColor, QPalette
from PySide6.QtWidgets import QApplication


# ---------------------------------------------------------------------------
# Palette — kept aligned with the Tk gui_elements set_dark_style dict so
# switching between the two GUIs feels visually consistent.
# ---------------------------------------------------------------------------
[docs] PALETTE = { # Surfaces — pure black bg, subtle depth via layered near-blacks "bg": "#000000", # main window background "surface": "#0d0e10", # sidebar / panels — barely lifted from bg "surface_alt": "#161719", # cards / grouped sections — one more step "surface_hi": "#1f2124", # hovered surfaces "border": "#2a2d33", # visible dividers "border_soft": "#1c1e22", # hairline card borders # Text "fg": "#ffffff", "fg_muted": "#a1a6ad", # secondary text (was #9ba0a6) "fg_dim": "#6b6f76", # disabled / hints # Accent "accent": "#4A9EFF", # primary interactive "accent_hi": "#66B2FF", # hover "accent_lo": "#2F80D9", # pressed "accent_soft": "#1e3550", # accent-tinted surface (chips, highlights) # Status "success": "#3fb950", "warning": "#d29922", "error": "#f85149", "info": "#4A9EFF", }
# --------------------------------------------------------------------------- # Spacing / radius scale — 4/8-based, matches Tk gui_elements. # ---------------------------------------------------------------------------
[docs] SPACING = { "xs": 4, "sm": 8, "md": 12, "lg": 16, "xl": 24, "xxl": 32, }
[docs] RADIUS = { "sm": 4, "md": 8, "lg": 12, "pill": 999, }
[docs] FONT_SIZE = { "xs": 11, # inline metadata, table cell suffixes "small": 12, # captions, muted secondary text, form hints "body": 13, # default body text "label": 13, # form field labels "header": 15, # card / section titles "subtitle":17, # dialog headings, secondary display "title": 22, # screen-level headings "display": 30, # startup screen brand title "hero": 42, # empty-state hero numerals }
# Typography roles — pair size with weight + tracking + line-height
[docs] TYPOGRAPHY = { "display": {"size": FONT_SIZE["display"], "weight": 300, "tracking": "-0.4px", "line_height": "1.15"}, "title": {"size": FONT_SIZE["title"], "weight": 500, "tracking": "-0.2px", "line_height": "1.2"}, "subtitle": {"size": FONT_SIZE["subtitle"], "weight": 500, "tracking": "-0.1px", "line_height": "1.25"}, "header": {"size": FONT_SIZE["header"], "weight": 600, "tracking": "0px", "line_height": "1.3"}, "body": {"size": FONT_SIZE["body"], "weight": 400, "tracking": "0px", "line_height": "1.45"}, "small": {"size": FONT_SIZE["small"], "weight": 400, "tracking": "0px", "line_height": "1.4"}, "caption": {"size": FONT_SIZE["xs"], "weight": 500, "tracking": "0.6px", "line_height": "1.4"}, "hero": {"size": FONT_SIZE["hero"], "weight": 200, "tracking": "-0.5px", "line_height": "1.1"}, }
[docs] def apply_qpalette(app: QApplication) -> None: """Apply the palette to the QApplication so native controls (menu bars, tooltips, dialogs) match the QSS-styled widgets.""" p = app.palette() p.setColor(QPalette.Window, QColor(PALETTE["bg"])) p.setColor(QPalette.WindowText, QColor(PALETTE["fg"])) p.setColor(QPalette.Base, QColor(PALETTE["surface"])) p.setColor(QPalette.AlternateBase, QColor(PALETTE["surface_alt"])) p.setColor(QPalette.ToolTipBase, QColor(PALETTE["surface_alt"])) p.setColor(QPalette.ToolTipText, QColor(PALETTE["fg"])) p.setColor(QPalette.Text, QColor(PALETTE["fg"])) p.setColor(QPalette.Button, QColor(PALETTE["surface"])) p.setColor(QPalette.ButtonText, QColor(PALETTE["fg"])) p.setColor(QPalette.BrightText, QColor(PALETTE["error"])) p.setColor(QPalette.Highlight, QColor(PALETTE["accent"])) p.setColor(QPalette.HighlightedText, QColor(PALETTE["bg"])) p.setColor(QPalette.Link, QColor(PALETTE["accent"])) p.setColor(QPalette.LinkVisited, QColor(PALETTE["accent_lo"])) p.setColor(QPalette.PlaceholderText, QColor(PALETTE["fg_dim"])) p.setColor(QPalette.Mid, QColor(PALETTE["border"])) p.setColor(QPalette.Midlight, QColor(PALETTE["border_soft"])) p.setColor(QPalette.Dark, QColor(PALETTE["surface_alt"])) p.setColor(QPalette.Shadow, QColor("#000000")) app.setPalette(p)
[docs] def stylesheet() -> str: """Return the QSS string that styles every custom widget in the app.""" P = PALETTE S = SPACING R = RADIUS F = FONT_SIZE return f""" /* ----------------------------------------------------------------- * Base * ----------------------------------------------------------------- */ QWidget {{ background-color: {P["bg"]}; color: {P["fg"]}; font-family: "Open Sans", "Segoe UI", "Helvetica Neue", sans-serif; font-size: {F["body"]}px; outline: none; }} QMainWindow, QDialog {{ background-color: {P["bg"]}; }} /* Every QLabel is transparent by default so it inherits the bg of * whatever container it lives in (surface, surface_alt, hero card, * etc). Individual labels can override with their own object name. */ QLabel {{ background: transparent; }} /* ----------------------------------------------------------------- * Menu bar + menus * ----------------------------------------------------------------- */ QMenuBar {{ background-color: {P["bg"]}; color: {P["fg_muted"]}; padding: {S["xs"]}px {S["sm"]}px; border-bottom: 1px solid {P["border_soft"]}; font-size: {F["small"]}px; }} QMenuBar::item {{ background: transparent; padding: {S["xs"]}px {S["sm"]}px; border-radius: {R["sm"]}px; }} QMenuBar::item:selected {{ background: {P["surface"]}; color: {P["fg"]}; }} QMenu {{ background-color: {P["surface_alt"]}; color: {P["fg"]}; border: 1px solid {P["border"]}; border-radius: {R["md"]}px; padding: {S["xs"]}px; }} QMenu::item {{ padding: {S["xs"]}px {S["md"]}px; border-radius: {R["sm"]}px; background: transparent; }} QMenu::item:selected {{ background: {P["accent"]}; color: {P["bg"]}; }} QMenu::separator {{ height: 1px; background: {P["border"]}; margin: {S["xs"]}px {S["sm"]}px; }} /* ----------------------------------------------------------------- * Sidebar (main window navigation) * ----------------------------------------------------------------- */ #Sidebar {{ background-color: {P["surface"]}; border-right: 1px solid {P["border_soft"]}; }} #SidebarTitle {{ color: {P["accent"]}; font-size: {F["title"]}px; font-weight: 600; padding: {S["md"]}px {S["md"]}px {S["sm"]}px; background: {P["surface"]}; }} #SidebarSection {{ color: {P["fg_dim"]}; font-size: {F["xs"]}px; font-weight: 600; padding: {S["md"]}px {S["md"]}px {S["xs"]}px; text-transform: uppercase; letter-spacing: 1px; background: {P["surface"]}; }} QPushButton#SidebarItem {{ text-align: left; background: transparent; color: {P["fg_muted"]}; padding: {S["sm"]}px {S["md"]}px; border: none; border-left: 3px solid transparent; border-radius: 0px; font-size: {F["body"]}px; }} QPushButton#SidebarItem:hover {{ background: {P["surface_alt"]}; color: {P["fg"]}; }} QPushButton#SidebarItem:checked, QPushButton#SidebarItem[selected="true"] {{ background: {P["surface_alt"]}; color: {P["accent"]}; border-left: 3px solid {P["accent"]}; }} /* ----------------------------------------------------------------- * Cards / grouped sections * ----------------------------------------------------------------- */ QFrame#Card {{ background-color: {P["surface"]}; border: 1px solid {P["border_soft"]}; border-radius: {R["md"]}px; }} QFrame#Hero {{ background-color: qlineargradient( x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 {P["surface_alt"]}, stop: 1 {P["surface"]} ); border: 1px solid {P["border"]}; border-radius: {R["lg"]}px; }} QPushButton#Tile:hover {{ border: 1px solid {P["accent"]}; background-color: {P["surface_hi"]}; }} QLabel#CardTitle {{ color: {P["fg"]}; font-size: {F["header"]}px; font-weight: 600; padding: 0px; background: transparent; }} QLabel#CardSubtitle {{ color: {P["fg_muted"]}; font-size: {F["small"]}px; background: transparent; }} QFrame#Divider {{ background: {P["border"]}; max-height: 1px; min-height: 1px; border: none; }} /* ----------------------------------------------------------------- * Startup tiles * ----------------------------------------------------------------- */ QPushButton#Tile {{ background-color: {P["surface"]}; color: {P["fg"]}; border: 1px solid {P["border_soft"]}; border-radius: {R["lg"]}px; padding: {S["md"]}px; font-size: {F["body"]}px; text-align: center; min-width: 96px; min-height: 96px; }} QPushButton#Tile:hover {{ border: 1px solid {P["accent"]}; background-color: {P["surface_alt"]}; color: {P["accent"]}; }} QPushButton#Tile:pressed {{ background-color: {P["accent_lo"]}; color: {P["bg"]}; }} QLabel#TileCaption {{ color: {P["fg"]}; font-size: {F["body"]}px; font-weight: 500; background: transparent; padding-top: 4px; }} /* ----------------------------------------------------------------- * Typography helpers — pair each role with weight + tracking * ----------------------------------------------------------------- */ QLabel#Hero {{ color: {P["fg"]}; font-size: {F["hero"]}px; font-weight: 200; letter-spacing: -0.5px; background: transparent; }} QLabel#DisplayHeading {{ color: {P["fg"]}; font-size: {F["display"]}px; font-weight: 300; letter-spacing: -0.4px; background: transparent; }} QLabel#TitleHeading {{ color: {P["fg"]}; font-size: {F["title"]}px; font-weight: 500; letter-spacing: -0.2px; background: transparent; }} QLabel#Subtitle {{ color: {P["fg_muted"]}; font-size: {F["subtitle"]}px; font-weight: 400; background: transparent; }} QLabel#SubtitleSmall, QLabel#Muted {{ color: {P["fg_muted"]}; font-size: {F["small"]}px; background: transparent; }} QLabel#Caption {{ color: {P["fg_dim"]}; font-size: {F["xs"]}px; font-weight: 500; letter-spacing: 0.6px; text-transform: uppercase; background: transparent; }} QLabel#SectionHeading {{ color: {P["fg"]}; font-size: {F["header"]}px; font-weight: 600; background: transparent; }} /* ----------------------------------------------------------------- * Buttons * ----------------------------------------------------------------- */ QPushButton {{ background-color: {P["surface_alt"]}; color: {P["fg"]}; border: 1px solid {P["border_soft"]}; border-radius: {R["sm"]}px; padding: {S["sm"]}px {S["md"]}px; min-height: 22px; font-weight: 500; }} QPushButton:hover {{ background-color: {P["surface_hi"]}; border-color: {P["border"]}; color: {P["fg"]}; }} QPushButton:pressed {{ background-color: {P["accent_lo"]}; border-color: {P["accent_lo"]}; color: {P["bg"]}; }} QPushButton:checked {{ background-color: {P["accent_soft"]}; border-color: {P["accent"]}; color: {P["accent"]}; }} QPushButton:checked:hover {{ background-color: {P["accent_soft"]}; border-color: {P["accent_hi"]}; color: {P["accent_hi"]}; }} QPushButton:disabled {{ color: {P["fg_dim"]}; border-color: {P["border_soft"]}; background-color: {P["surface"]}; }} QPushButton#PrimaryButton {{ background-color: {P["accent"]}; color: {P["bg"]}; border: none; font-weight: 600; padding: {S["sm"]}px {S["lg"]}px; }} QPushButton#PrimaryButton:hover {{ background-color: {P["accent_hi"]}; }} QPushButton#PrimaryButton:pressed {{ background-color: {P["accent_lo"]}; }} QPushButton#DangerButton {{ background-color: transparent; color: {P["error"]}; border: 1px solid {P["error"]}; font-weight: 600; padding: {S["sm"]}px {S["lg"]}px; }} QPushButton#DangerButton:hover {{ background-color: {P["error"]}; color: {P["bg"]}; }} QPushButton#GhostButton {{ background-color: transparent; color: {P["fg_muted"]}; border: none; }} QPushButton#GhostButton:hover {{ color: {P["accent"]}; background: transparent; }} QPushButton#IconButton {{ background-color: transparent; border: none; padding: {S["xs"]}px; min-height: 0; color: {P["fg_muted"]}; }} QPushButton#IconButton:hover {{ color: {P["accent"]}; background: {P["surface_alt"]}; border-radius: {R["sm"]}px; }} /* ----------------------------------------------------------------- * Inputs (QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox) * ----------------------------------------------------------------- */ QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox, QPlainTextEdit, QTextEdit {{ background-color: {P["surface_alt"]}; color: {P["fg"]}; border: 1px solid {P["border"]}; border-radius: {R["sm"]}px; padding: {S["xs"]}px {S["sm"]}px; selection-background-color: {P["accent"]}; selection-color: {P["bg"]}; }} QPlainTextEdit#Console {{ background-color: #0a0b0d; color: #d4d7dc; border: 1px solid {P["border_soft"]}; font-family: "JetBrains Mono", "Menlo", "Consolas", monospace; font-size: {F["small"]}px; padding: {S["sm"]}px; selection-background-color: {P["accent_lo"]}; }} QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QComboBox:focus, QPlainTextEdit:focus, QTextEdit:focus {{ border: 1px solid {P["accent"]}; }} QLineEdit:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled, QComboBox:disabled {{ color: {P["fg_dim"]}; background-color: {P["surface"]}; border-color: {P["border_soft"]}; }} QLineEdit::placeholder {{ color: {P["fg_dim"]}; }} QSpinBox::up-button, QSpinBox::down-button, QDoubleSpinBox::up-button, QDoubleSpinBox::down-button {{ background: transparent; border: none; width: 16px; }} QSpinBox::up-arrow, QSpinBox::down-arrow, QDoubleSpinBox::up-arrow, QDoubleSpinBox::down-arrow {{ width: 8px; height: 8px; }} QComboBox::drop-down {{ subcontrol-origin: padding; subcontrol-position: center right; width: 24px; border: none; }} QComboBox::down-arrow {{ image: none; border: 4px solid transparent; border-top-color: {P["fg_muted"]}; margin-top: 4px; width: 0; height: 0; }} QComboBox QAbstractItemView {{ background-color: {P["surface_alt"]}; color: {P["fg"]}; border: 1px solid {P["border"]}; border-radius: {R["sm"]}px; padding: {S["xs"]}px; selection-background-color: {P["accent"]}; selection-color: {P["bg"]}; }} /* ----------------------------------------------------------------- * Checkboxes + toggles * ----------------------------------------------------------------- */ QCheckBox {{ color: {P["fg"]}; background: transparent; spacing: {S["sm"]}px; padding: 2px 0px; }} QCheckBox::indicator {{ width: 16px; height: 16px; border: 1px solid {P["border"]}; border-radius: {R["sm"]}px; background: {P["surface_alt"]}; }} QCheckBox::indicator:hover {{ border-color: {P["accent"]}; }} QCheckBox::indicator:checked {{ background: {P["accent"]}; border-color: {P["accent"]}; image: none; }} QCheckBox::indicator:disabled {{ background: {P["surface"]}; border-color: {P["border_soft"]}; }} QRadioButton {{ color: {P["fg"]}; background: transparent; spacing: {S["sm"]}px; }} QRadioButton::indicator {{ width: 16px; height: 16px; border: 1px solid {P["border"]}; border-radius: 8px; background: {P["surface_alt"]}; }} QRadioButton::indicator:checked {{ background: {P["accent"]}; border-color: {P["accent"]}; }} /* ----------------------------------------------------------------- * Scrollbars * ----------------------------------------------------------------- */ QScrollBar:vertical {{ background: {P["bg"]}; width: 10px; margin: 0px; border: none; }} QScrollBar::handle:vertical {{ background: {P["border"]}; border-radius: 5px; min-height: 30px; }} QScrollBar::handle:vertical:hover {{ background: {P["accent"]}; }} QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ background: transparent; height: 0px; }} QScrollBar:horizontal {{ background: {P["bg"]}; height: 10px; margin: 0px; border: none; }} QScrollBar::handle:horizontal {{ background: {P["border"]}; border-radius: 5px; min-width: 30px; }} QScrollBar::handle:horizontal:hover {{ background: {P["accent"]}; }} QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{ background: transparent; width: 0px; }} /* ----------------------------------------------------------------- * Progress bar * ----------------------------------------------------------------- */ QProgressBar {{ background: {P["surface"]}; border: none; border-radius: 4px; text-align: center; color: {P["fg_muted"]}; height: 8px; max-height: 8px; }} QProgressBar::chunk {{ background-color: {P["accent"]}; border-radius: 4px; }} QProgressBar#UsageBar {{ background: {P["surface_alt"]}; height: 6px; max-height: 6px; }} QProgressBar#UsageBar::chunk {{ background: {P["accent"]}; border-radius: 3px; }} QProgressBar#UsageBarWarn::chunk {{ background: {P["warning"]}; }} QProgressBar#UsageBarError::chunk {{ background: {P["error"]}; }} /* ----------------------------------------------------------------- * Splitter handle * ----------------------------------------------------------------- */ QSplitter::handle {{ background: {P["border_soft"]}; }} QSplitter::handle:horizontal {{ width: 1px; }} QSplitter::handle:vertical {{ height: 1px; }} QSplitter::handle:hover {{ background: {P["accent"]}; }} /* ----------------------------------------------------------------- * Tooltip * ----------------------------------------------------------------- */ QToolTip {{ background-color: {P["surface_alt"]}; color: {P["fg"]}; border: 1px solid {P["border"]}; border-radius: {R["sm"]}px; padding: {S["xs"]}px {S["sm"]}px; font-size: {F["small"]}px; }} /* ----------------------------------------------------------------- * AI Console chat bubbles (legacy standalone panel) * ----------------------------------------------------------------- */ QLabel#ChatBubbleUser {{ background-color: {P["accent_soft"]}; color: {P["fg"]}; border: 1px solid {P["accent_lo"]}; border-radius: {R["md"]}px; padding: {S["sm"]}px {S["md"]}px; font-size: {F["body"]}px; }} QLabel#ChatBubbleAssistant {{ background-color: {P["surface_alt"]}; color: {P["fg"]}; border: 1px solid {P["border_soft"]}; border-radius: {R["md"]}px; padding: {S["sm"]}px {S["md"]}px; font-size: {F["body"]}px; }} /* ----------------------------------------------------------------- * Merged Console (pipeline stdout + AI chat) * ----------------------------------------------------------------- */ QWidget#ConsolePanel {{ background-color: {P["surface_alt"]}; border-radius: {R["md"]}px; }} QWidget#ConsoleHolder {{ background-color: {P["surface_alt"]}; }} QScrollArea#ConsoleScroll {{ background-color: {P["surface_alt"]}; border: none; }} QFrame#ConsoleTopicBar {{ background-color: {P["surface_hi"]}; border-top: 1px solid {P["border_soft"]}; border-bottom: 1px solid {P["border_soft"]}; }} QLabel#ConsoleTopicLabel {{ color: {P["fg_muted"]}; font-size: {F["small"]}px; font-weight: 600; letter-spacing: 0.4px; background: transparent; }} QLabel#ConsoleStdoutBlock {{ color: {P["fg"]}; background-color: {P["surface_alt"]}; font-family: "JetBrains Mono", "Menlo", "Consolas", monospace; font-size: {F["small"]}px; padding: {S["sm"]}px {S["md"]}px; }} QLabel#ConsoleStdoutBlockError {{ color: {P["error"]}; background-color: {P["surface_alt"]}; font-family: "JetBrains Mono", "Menlo", "Consolas", monospace; font-size: {F["small"]}px; padding: {S["sm"]}px {S["md"]}px; }} QFrame#ConsoleBubbleUser {{ background-color: #163b28; /* dark green */ border: none; border-top: 1px solid #2a6a48; border-bottom: 1px solid #2a6a48; border-radius: 0px; }} QFrame#ConsoleBubbleAI {{ background-color: {P["accent_soft"]}; /* dark blue */ border: none; border-top: 1px solid {P["accent_lo"]}; border-bottom: 1px solid {P["accent_lo"]}; border-radius: 0px; }} QFrame#ConsoleInputBar {{ background-color: {P["surface"]}; border-top: 1px solid {P["border_soft"]}; }} /* ----------------------------------------------------------------- * Section — collapsible dropdown (custom widget) * ----------------------------------------------------------------- */ QFrame#SectionCard {{ background-color: {P["surface"]}; border: 1px solid {P["border_soft"]}; border-radius: {R["md"]}px; margin-bottom: {S["sm"]}px; }} QToolButton#SectionHeader {{ background: transparent; color: {P["fg_muted"]}; border: none; border-radius: {R["md"]}px; padding: {S["sm"]}px {S["md"]}px; text-align: left; font-size: {F["small"]}px; font-weight: 600; letter-spacing: 0.6px; }} QToolButton#SectionHeader:hover {{ color: {P["fg"]}; background: {P["surface_alt"]}; }} QToolButton#SectionHeader:checked {{ color: {P["fg"]}; background: {P["surface_alt"]}; border-bottom: 1px solid {P["border_soft"]}; border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; }} QWidget#SectionBody {{ background-color: transparent; border-bottom-left-radius: {R["md"]}px; border-bottom-right-radius: {R["md"]}px; }} /* ----------------------------------------------------------------- * Group box (used by settings sections) * ----------------------------------------------------------------- */ QGroupBox {{ background: transparent; border: 1px solid {P["border_soft"]}; border-radius: {R["md"]}px; margin-top: {S["md"]}px; padding: {S["md"]}px {S["sm"]}px {S["sm"]}px; color: {P["fg_muted"]}; font-weight: 600; font-size: {F["small"]}px; text-transform: uppercase; }} QGroupBox::title {{ subcontrol-origin: margin; subcontrol-position: top left; left: {S["md"]}px; top: -{S["xs"]}px; padding: 0px {S["xs"]}px; background: {P["bg"]}; }} /* ----------------------------------------------------------------- * Status bar * ----------------------------------------------------------------- */ QStatusBar {{ background: {P["surface"]}; color: {P["fg_muted"]}; border-top: 1px solid {P["border_soft"]}; font-size: {F["small"]}px; padding: 0px {S["sm"]}px; }} """