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>
475 lines
13 KiB
Python
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;
|
|
}}
|
|
"""
|