Files
cimtechniques-service-suite/cim_suite/core/ui/theme/stylesheet.py
Andy 63169a7644 feat: add versioning and changelog system
Conventional-Commit-driven version bumps (scripts/bump.py), a commit-msg validation hook, and a /release-notes workflow that produces a human-reviewed CHANGELOG.md. Adds an in-app version badge + 'What's new' dialog on the launcher. The version is single-sourced from pyproject.toml (cim_suite.__version__), and the deterministic bump backbone lives in scripts/release/ with tests in tests/release/. Marks the 1.0.0 baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 11:31:22 -04:00

475 lines
13 KiB
Python

"""Builds the application-wide Qt Style Sheet (QSS) from design tokens.
One function, ``build_qss``, returns the full stylesheet string. It is applied once
to the ``QApplication`` in ``theme.apply_theme`` and cascades to every widget.
Keep all visual rules here (not scattered in widgets) so the look stays coherent and
a future theme swap is a one-file change.
Conventions used by the rules below — set these where you build widgets:
* objectName ``"BrandTitle"`` → the product wordmark in the toolbar
* objectName ``"PrimaryAction"`` → the highlighted toolbar button (Connect)
* objectName ``"HelpText"`` → quiet helper/hint text under inputs
* property ``variant="primary"`` on a QPushButton → filled accent button
* property ``state`` on the status connection label → "on" | "off"
"""
from __future__ import annotations
from .tokens import Color as C
from .tokens import Radius as R
from .tokens import Space as S
from .tokens import Type as T
def build_qss(family: str) -> str:
"""Return the full application stylesheet using ``family`` as the base font."""
f = family
return f"""
/* ===================================================================== *
* DA-12 Service Tool — application theme (light, SmartScan azure) *
* ===================================================================== */
QWidget {{
background-color: {C.BG_WINDOW};
color: {C.TEXT};
font-family: "{f}", "{T.FALLBACK}", sans-serif;
font-size: {T.BASE_PT}pt;
}}
QMainWindow, QDialog {{
background-color: {C.BG_WINDOW};
}}
QToolTip {{
background-color: {C.TEXT};
color: {C.TEXT_ON_ACCENT};
border: none;
padding: {S.XS}px {S.SM}px;
border-radius: {R.SM}px;
font-size: {T.SMALL_PT}pt;
}}
/* ---- Top toolbar / app header --------------------------------------- */
QToolBar {{
background-color: {C.BG_SURFACE};
border: none;
border-bottom: 1px solid {C.BORDER};
padding: {S.XS}px {S.SM}px;
spacing: {S.XS}px;
}}
QToolBar::separator {{
background: {C.DIVIDER};
width: 1px;
margin: {S.XS}px {S.SM}px;
}}
QToolButton {{
background: transparent;
color: {C.TEXT};
border: 1px solid transparent;
border-radius: {R.MD}px;
padding: {S.XS}px {S.MD}px;
margin: 0px 1px;
}}
QToolButton:hover {{
background-color: {C.ACCENT_SOFT};
border-color: {C.ACCENT_SOFT_LINE};
}}
QToolButton:pressed {{
background-color: {C.BG_RAISED};
}}
QToolButton:checked {{
background-color: {C.ACCENT_SOFT};
border-color: {C.ACCENT_SOFT_LINE};
color: {C.ACCENT_PRESSED};
}}
QToolButton#PrimaryAction {{
background-color: {C.ACCENT};
color: {C.TEXT_ON_ACCENT};
font-weight: 700;
}}
QToolButton#PrimaryAction:hover {{ background-color: {C.ACCENT_HOVER}; }}
QToolButton#PrimaryAction:pressed {{ background-color: {C.ACCENT_PRESSED}; }}
QLabel#BrandTitle {{
color: {C.TEXT};
font-size: {T.TITLE_PT}pt;
font-weight: 900;
padding: 0px {S.SM}px 0px {S.XS}px;
}}
QLabel#BrandTitleAccent {{
color: {C.ACCENT};
font-size: {T.TITLE_PT}pt;
font-weight: 900;
}}
/* ---- Tabs: clean underline bar, the signature element --------------- */
QTabWidget::pane {{
background-color: {C.BG_SURFACE};
border: 1px solid {C.BORDER};
border-radius: {R.LG}px;
top: -1px;
}}
QTabBar {{
qproperty-drawBase: 0;
background: transparent;
}}
QTabBar::tab {{
background: transparent;
color: {C.TEXT_MUTED};
border: none;
border-bottom: 2px solid transparent;
padding: {S.SM}px {S.LG}px;
margin-right: {S.XS}px;
font-weight: 600;
}}
QTabBar::tab:hover {{
color: {C.TEXT};
border-bottom-color: {C.ACCENT_SOFT_LINE};
}}
QTabBar::tab:selected {{
color: {C.ACCENT_PRESSED};
border-bottom-color: {C.ACCENT};
}}
/* ---- Tables: the workhorse views ----------------------------------- */
QTableWidget, QTableView {{
background-color: {C.BG_SURFACE};
alternate-background-color: {C.ROW_ALT};
color: {C.TEXT};
gridline-color: {C.DIVIDER};
border: 1px solid {C.BORDER};
border-radius: {R.MD}px;
selection-background-color: {C.ACCENT};
selection-color: {C.TEXT_ON_ACCENT};
outline: none;
}}
QTableView::item {{
padding: {S.XS}px {S.SM}px;
border: none;
}}
QTableView::item:hover {{
background-color: {C.ROW_HOVER};
}}
QTableView::item:selected {{
background-color: {C.ACCENT};
color: {C.TEXT_ON_ACCENT};
}}
QHeaderView {{ background-color: {C.BG_RAISED}; }}
QHeaderView::section {{
background-color: {C.BG_RAISED};
color: {C.TEXT_MUTED};
padding: {S.SM}px {S.SM}px;
border: none;
border-right: 1px solid {C.DIVIDER};
border-bottom: 1px solid {C.BORDER};
font-weight: 700;
text-transform: uppercase;
}}
QHeaderView::section:first {{ border-top-left-radius: {R.MD}px; }}
QHeaderView::section:last {{ border-right: none; border-top-right-radius: {R.MD}px; }}
QTableCornerButton::section {{
background-color: {C.BG_RAISED};
border: none;
border-bottom: 1px solid {C.BORDER};
}}
/* ---- Buttons ------------------------------------------------------- */
QPushButton {{
background-color: {C.BG_SURFACE};
color: {C.TEXT};
border: 1px solid {C.BORDER_STRONG};
border-radius: {R.MD}px;
padding: {S.SM}px {S.LG}px;
font-weight: 600;
min-height: 16px;
}}
QPushButton:hover {{
background-color: {C.ACCENT_SOFT};
border-color: {C.ACCENT};
color: {C.ACCENT_PRESSED};
}}
QPushButton:pressed {{ background-color: {C.ACCENT_SOFT_LINE}; }}
QPushButton:disabled {{
background-color: {C.BG_RAISED};
color: {C.TEXT_FAINT};
border-color: {C.BORDER};
}}
QPushButton[variant="primary"] {{
background-color: {C.ACCENT};
color: {C.TEXT_ON_ACCENT};
border: 1px solid {C.ACCENT};
}}
QPushButton[variant="primary"]:hover {{
background-color: {C.ACCENT_HOVER};
border-color: {C.ACCENT_HOVER};
color: {C.TEXT_ON_ACCENT};
}}
QPushButton[variant="primary"]:pressed {{ background-color: {C.ACCENT_PRESSED}; }}
/* ---- Text & selection inputs --------------------------------------- */
QLineEdit, QComboBox, QSpinBox, QDoubleSpinBox, QPlainTextEdit, QTextEdit {{
background-color: {C.BG_SURFACE};
color: {C.TEXT};
border: 1px solid {C.BORDER_STRONG};
border-radius: {R.SM}px;
padding: {S.XS}px {S.SM}px;
selection-background-color: {C.ACCENT};
selection-color: {C.TEXT_ON_ACCENT};
}}
QLineEdit:focus, QComboBox:focus, QSpinBox:focus, QDoubleSpinBox:focus,
QPlainTextEdit:focus, QTextEdit:focus, QAbstractItemView:focus {{
border: 1px solid {C.ACCENT};
}}
QLineEdit:disabled, QComboBox:disabled {{
background-color: {C.BG_RAISED};
color: {C.TEXT_FAINT};
}}
QComboBox::drop-down {{
border: none;
width: 22px;
}}
QComboBox QAbstractItemView {{
background-color: {C.BG_SURFACE};
border: 1px solid {C.BORDER};
selection-background-color: {C.ACCENT};
selection-color: {C.TEXT_ON_ACCENT};
outline: none;
}}
/* ---- Group boxes & dialogs ----------------------------------------- */
QGroupBox {{
background-color: {C.BG_SURFACE};
border: 1px solid {C.BORDER};
border-radius: {R.LG}px;
margin-top: {S.MD}px;
padding: {S.MD}px {S.SM}px {S.SM}px {S.SM}px;
font-weight: 700;
}}
QGroupBox::title {{
subcontrol-origin: margin;
subcontrol-position: top left;
left: {S.MD}px;
padding: 0px {S.XS}px;
color: {C.TEXT_MUTED};
}}
/* ---- Checkboxes ---------------------------------------------------- */
QCheckBox {{ spacing: {S.SM}px; background: transparent; }}
QCheckBox::indicator {{
width: 16px; height: 16px;
border: 1px solid {C.BORDER_STRONG};
border-radius: {R.SM}px;
background-color: {C.BG_SURFACE};
}}
QCheckBox::indicator:hover {{ border-color: {C.ACCENT}; }}
QCheckBox::indicator:checked {{
background-color: {C.ACCENT};
border-color: {C.ACCENT};
}}
/* ---- Helper / hint text -------------------------------------------- */
QLabel#HelpText {{
color: {C.TEXT_MUTED};
font-size: {T.SMALL_PT}pt;
}}
/* ---- Status bar ---------------------------------------------------- */
QStatusBar {{
background-color: {C.BG_SURFACE};
border-top: 1px solid {C.BORDER};
color: {C.TEXT_MUTED};
font-size: {T.SMALL_PT}pt;
}}
QStatusBar::item {{ border: none; }}
QStatusBar QLabel {{ color: {C.TEXT_MUTED}; padding: 0px {S.XS}px; }}
/* Connection pill in the status bar — color driven by the `state` property. */
QLabel#ConnPill {{
border-radius: {R.SM}px;
padding: 2px {S.SM}px;
font-weight: 700;
}}
QLabel#ConnPill[state="on"] {{
background-color: {C.OK_FILL};
color: {C.OK};
}}
QLabel#ConnPill[state="off"] {{
background-color: {C.BG_RAISED};
color: {C.TEXT_MUTED};
}}
QLabel#ConnPill[state="bad"] {{
background-color: {C.DANGER_FILL};
color: {C.DANGER};
}}
/* Live activity indicator in the status bar — color driven by `state`. */
QLabel#ActivityDot {{
font-weight: 700;
padding: 2px {S.SM}px;
}}
QLabel#ActivityDot[state="active"] {{ color: {C.OK}; }}
QLabel#ActivityDot[state="idle"] {{ color: {C.TEXT_MUTED}; }}
/* Buffered-records-waiting stat — amber when the upload backlog is growing. */
QLabel#BufferedStat[backlog="growing"] {{ color: {C.CAUTION}; font-weight: 700; }}
QLabel#BufferedStat[backlog="steady"] {{ color: {C.TEXT_MUTED}; }}
/* "Reboot recommended" banner — shown on the Station tab when a LAN setting that
only takes effect after a reboot has been changed. Warning amber, not an alarm. */
QFrame#RebootBanner {{
background: {C.WARN_FILL};
border: 1px solid {C.WARN};
border-radius: {R.SM}px;
}}
QFrame#RebootBanner QLabel {{ color: {C.WARN}; font-weight: 600; background: transparent; }}
/* ---- Scrollbars: slim, unobtrusive --------------------------------- */
QScrollBar:vertical {{
background: transparent; width: 12px; margin: 0px;
}}
QScrollBar::handle:vertical {{
background: {C.BORDER_STRONG};
border-radius: 6px;
min-height: 28px;
}}
QScrollBar::handle:vertical:hover {{ background: {C.TEXT_FAINT}; }}
QScrollBar:horizontal {{
background: transparent; height: 12px; margin: 0px;
}}
QScrollBar::handle:horizontal {{
background: {C.BORDER_STRONG};
border-radius: 6px;
min-width: 28px;
}}
QScrollBar::handle:horizontal:hover {{ background: {C.TEXT_FAINT}; }}
QScrollBar::add-line, QScrollBar::sub-line {{ width: 0px; height: 0px; }}
QScrollBar::add-page, QScrollBar::sub-page {{ background: transparent; }}
/* ---- Menus & message boxes ----------------------------------------- */
QMenu {{
background-color: {C.BG_SURFACE};
border: 1px solid {C.BORDER};
border-radius: {R.MD}px;
padding: {S.XS}px;
}}
QMenu::item {{
padding: {S.XS}px {S.LG}px;
border-radius: {R.SM}px;
}}
QMenu::item:selected {{
background-color: {C.ACCENT};
color: {C.TEXT_ON_ACCENT};
}}
/* ---- Launcher landing page & module cards -------------------------- */
QWidget#LauncherPage {{
background-color: {C.BG_WINDOW};
}}
QLabel#LauncherHeading {{
color: {C.TEXT};
font-size: {T.TITLE_PT}pt;
font-weight: 900;
padding: {S.LG}px {S.XS}px {S.SM}px {S.XS}px;
}}
QLabel#LauncherVersion {{
color: {C.TEXT_MUTED};
font-size: {T.SMALL_PT}pt;
font-weight: 700;
padding-right: {S.SM}px;
}}
QPushButton#WhatsNewButton {{
background-color: transparent;
color: {C.ACCENT};
border: 1px solid {C.ACCENT_SOFT_LINE};
border-radius: {R.MD}px;
padding: {S.XS}px {S.MD}px;
font-weight: 700;
}}
QPushButton#WhatsNewButton:hover {{
background-color: {C.ACCENT_SOFT};
border-color: {C.ACCENT};
}}
QFrame#ModuleCard {{
background-color: {C.BG_SURFACE};
border: 1px solid {C.BORDER};
border-radius: {R.LG}px;
}}
QFrame#ModuleCard[available="false"] {{
background-color: {C.BG_RAISED};
border: 1px dashed {C.BORDER_STRONG};
}}
QLabel#ModuleCardTitle {{
color: {C.TEXT};
font-size: {T.SECTION_PT}pt;
font-weight: 800;
}}
QLabel#ModuleCardSummary {{
color: {C.TEXT_MUTED};
font-size: {T.SMALL_PT}pt;
}}
QLabel#ModuleCardBadge {{
color: {C.TEXT_MUTED};
background-color: {C.BG_RAISED};
border: 1px solid {C.BORDER};
border-radius: {R.SM}px;
padding: 2px {S.SM}px;
font-size: {T.SMALL_PT}pt;
font-weight: 700;
}}
/* ---- Dashboard cable panel ----------------------------------------- */
QFrame#CablePanel {{
background-color: {C.BG_SURFACE};
border: 1px solid {C.BORDER};
border-radius: {R.LG}px;
}}
QFrame#CablePanel[state="error"] {{
background-color: {C.DANGER_FILL};
border: 1px solid {C.DANGER};
}}
QLabel#CablePanelTitle {{
color: {C.TEXT};
font-size: {T.SECTION_PT}pt;
font-weight: 800;
}}
QLabel#CablePanelHint {{
color: {C.TEXT_MUTED};
font-size: {T.SMALL_PT}pt;
}}
/* ---- Subnet edit dialog preview ------------------------------------ */
QLabel#SubnetPreview {{
color: {C.TEXT_MUTED};
font-size: {T.SMALL_PT}pt;
}}
QLabel#SubnetPreview[state="ok"] {{
color: {C.TEXT};
font-weight: 700;
}}
QLabel#SubnetPreview[state="error"] {{
color: {C.DANGER};
font-weight: 700;
}}
/* ---- Loading overlay (blocking refresh indicator) ------------------ */
QWidget#LoadingOverlay {{
background-color: {C.SCRIM};
}}
QFrame#LoadingCard {{
background-color: {C.BG_SURFACE};
border: 1px solid {C.BORDER};
border-radius: {R.LG}px;
}}
QLabel#LoadingTitle {{
background: transparent;
color: {C.TEXT};
font-size: {T.TITLE_PT}pt;
font-weight: 800;
}}
QLabel#LoadingSubtitle {{
background: transparent;
color: {C.TEXT_MUTED};
font-size: {T.SMALL_PT}pt;
}}
"""