From 5a536fb9e9606aba507f6c4c4fb927b6a1844bde Mon Sep 17 00:00:00 2001 From: NguyenND Date: Fri, 22 May 2026 09:54:02 -0400 Subject: [PATCH] feat: add 5 new themes via single QSS template engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the two hard-coded .qss files with a single resources/style_template.qss that uses @{token} placeholders. apply_qss() now does regex substitution at runtime, so adding a theme only requires a palette dict — no duplicate stylesheets. New themes (Preferences → Color theme): • One Dark Pro — VS Code classic dark • Nord — arctic blue-gray dark • Tokyo Night — deep purple-dark • Dracula — classic purple-dark • GitHub Light — clean minimal light Existing Catppuccin Frappé (dark) and Latte (light) are unchanged. Ctrl+Shift+T quick-toggle now recognises all dark themes via is_dark(). Co-Authored-By: Claude Sonnet 4.6 --- app/config/theme.py | 316 ++++++++++++------- app/ui/preferences_dialog.py | 15 +- resources/style_template.qss | 589 +++++++++++++++++++++++++++++++++++ 3 files changed, 812 insertions(+), 108 deletions(-) create mode 100644 resources/style_template.qss diff --git a/app/config/theme.py b/app/config/theme.py index a64ccb6..809afc7 100644 --- a/app/config/theme.py +++ b/app/config/theme.py @@ -1,121 +1,229 @@ """ -Theme palettes — Catppuccin Frappé (dark) and Catppuccin Latte (light). +Theme palettes and QSS application. -Usage: - from app.config.theme import get_palette, apply_qss, is_dark +Each theme is a flat dict of color tokens. apply_qss() loads +style_template.qss and substitutes @{key} tokens with the palette values. + +Supported themes +---------------- +dark Catppuccin Frappé (dark blue-gray) +light Catppuccin Latte (warm light) +one_dark One Dark Pro (VS Code classic dark) +nord Nord (arctic blue-gray dark) +tokyo_night Tokyo Night (purple-dark) +dracula Dracula (classic purple-dark) +github_light GitHub Light (clean minimal light) """ +from __future__ import annotations +import re from pathlib import Path - -# ── Catppuccin Frappé ───────────────────────────────────────────────────────── -DARK = { - "base": "#303446", - "mantle": "#292c3c", - "crust": "#232634", - "surface0": "#414559", - "surface1": "#51576d", - "surface2": "#626880", - "overlay": "#737994", - "subtext": "#a5adce", - "text": "#c6d0f5", - "blue": "#8caaee", - "lavender": "#babbf1", - "green": "#a6d189", - "teal": "#81c8be", - "sky": "#99d1db", - "red": "#e78284", - "peach": "#ef9f76", - "yellow": "#e5c890", - "mauve": "#ca9ee6", - # derived UI tokens - "gutter_bg": "#2a2d3e", - "dirty_bg": "#3a2f25", - "dirty_fg": "#ef9f76", - "delete_bg": "#3a2530", - "delete_fg": "#e78284", - # syntax - "syn_keyword": "#8caaee", - "syn_type": "#ef9f76", - "syn_function": "#a6d189", - "syn_string": "#a6d189", - "syn_number": "#ef9f76", - "syn_comment": "#737994", - "syn_operator": "#ca9ee6", - "syn_ident": "#99d1db", - # completer popup - "popup_bg": "#292c3c", - "popup_border": "#51576d", - "popup_selected": "#414559", - "popup_hover": "#363a4f", -} - -# ── Catppuccin Latte ────────────────────────────────────────────────────────── -LIGHT = { - "base": "#eff1f5", - "mantle": "#e6e9ef", - "crust": "#dce0e8", - "surface0": "#ccd0da", - "surface1": "#bcc0cc", - "surface2": "#acb0be", - "overlay": "#9ca0b0", - "subtext": "#6c6f85", - "text": "#4c4f69", - "blue": "#1e66f5", - "lavender": "#7287fd", - "green": "#40a02b", - "teal": "#179299", - "sky": "#04a5e5", - "red": "#d20f39", - "peach": "#fe640b", - "yellow": "#df8e1d", - "mauve": "#8839ef", - # derived UI tokens - "gutter_bg": "#dce0e8", - "dirty_bg": "#fff3e6", - "dirty_fg": "#fe640b", - "delete_bg": "#ffe8ed", - "delete_fg": "#d20f39", - # syntax - "syn_keyword": "#1e66f5", - "syn_type": "#fe640b", - "syn_function": "#40a02b", - "syn_string": "#40a02b", - "syn_number": "#fe640b", - "syn_comment": "#9ca0b0", - "syn_operator": "#8839ef", - "syn_ident": "#04a5e5", - # completer popup - "popup_bg": "#e6e9ef", - "popup_border": "#bcc0cc", - "popup_selected": "#ccd0da", - "popup_hover": "#d8dce8", -} - _QSS_DIR = Path(__file__).parent.parent.parent / "resources" +# ── Palettes ────────────────────────────────────────────────────────────────── + +# Catppuccin Frappé — dark blue-gray +_FRAPPÉ: dict = { + "base": "#303446", "mantle": "#292c3c", "crust": "#232634", + "surface0": "#414559", "surface1": "#51576d", "surface2": "#626880", + "overlay": "#737994", "subtext": "#a5adce", "text": "#c6d0f5", + "blue": "#8caaee", "lavender": "#babbf1", "green": "#a6d189", + "teal": "#81c8be", "sky": "#99d1db", "red": "#e78284", + "peach": "#ef9f76", "yellow": "#e5c890", "mauve": "#ca9ee6", + "red_hover": "#ea999c", + "tab_hover": "#353849", "tree_hover": "#363a4f", "btn_pressed": "#363849", + "gutter_bg": "#2a2d3e", + "dirty_bg": "#3a2f25", "dirty_fg": "#ef9f76", + "delete_bg": "#3a2530", "delete_fg": "#e78284", + "syn_keyword": "#8caaee", "syn_type": "#ef9f76", + "syn_function": "#a6d189", "syn_string": "#a6d189", + "syn_number": "#ef9f76", "syn_comment": "#737994", + "syn_operator": "#ca9ee6", "syn_ident": "#99d1db", + "popup_bg": "#292c3c", "popup_border": "#51576d", + "popup_selected": "#414559", "popup_hover": "#363a4f", +} + +# Catppuccin Latte — warm light +_LATTE: dict = { + "base": "#eff1f5", "mantle": "#e6e9ef", "crust": "#dce0e8", + "surface0": "#ccd0da", "surface1": "#bcc0cc", "surface2": "#acb0be", + "overlay": "#9ca0b0", "subtext": "#6c6f85", "text": "#4c4f69", + "blue": "#1e66f5", "lavender": "#7287fd", "green": "#40a02b", + "teal": "#179299", "sky": "#04a5e5", "red": "#d20f39", + "peach": "#fe640b", "yellow": "#df8e1d", "mauve": "#8839ef", + "red_hover": "#da2b50", + "tab_hover": "#d8dce8", "tree_hover": "#d8dce8", "btn_pressed": "#c8cdd9", + "gutter_bg": "#dce0e8", + "dirty_bg": "#fff3e6", "dirty_fg": "#fe640b", + "delete_bg": "#ffe8ed", "delete_fg": "#d20f39", + "syn_keyword": "#1e66f5", "syn_type": "#fe640b", + "syn_function": "#40a02b", "syn_string": "#40a02b", + "syn_number": "#fe640b", "syn_comment": "#9ca0b0", + "syn_operator": "#8839ef", "syn_ident": "#04a5e5", + "popup_bg": "#e6e9ef", "popup_border": "#bcc0cc", + "popup_selected": "#ccd0da", "popup_hover": "#d8dce8", +} + +# One Dark Pro — VS Code classic +_ONE_DARK: dict = { + "base": "#282c34", "mantle": "#21252b", "crust": "#1d2026", + "surface0": "#2c313c", "surface1": "#3e4452", "surface2": "#4b5363", + "overlay": "#5c6370", "subtext": "#828997", "text": "#abb2bf", + "blue": "#61afef", "lavender": "#c678dd", "green": "#98c379", + "teal": "#56b6c2", "sky": "#56b6c2", "red": "#e06c75", + "peach": "#d19a66", "yellow": "#e5c07b", "mauve": "#c678dd", + "red_hover": "#e8868f", + "tab_hover": "#2e3440", "tree_hover": "#2c313c", "btn_pressed": "#282c34", + "gutter_bg": "#1e2127", + "dirty_bg": "#3a2f1e", "dirty_fg": "#d19a66", + "delete_bg": "#3a1e20", "delete_fg": "#e06c75", + "syn_keyword": "#c678dd", "syn_type": "#e5c07b", + "syn_function": "#61afef", "syn_string": "#98c379", + "syn_number": "#d19a66", "syn_comment": "#5c6370", + "syn_operator": "#56b6c2", "syn_ident": "#abb2bf", + "popup_bg": "#21252b", "popup_border": "#3e4452", + "popup_selected": "#2c313c", "popup_hover": "#2e3440", +} + +# Nord — arctic blue-gray +_NORD: dict = { + "base": "#2e3440", "mantle": "#292e3b", "crust": "#242831", + "surface0": "#3b4252", "surface1": "#434c5e", "surface2": "#4c566a", + "overlay": "#616e88", "subtext": "#d8dee9", "text": "#eceff4", + "blue": "#88c0d0", "lavender": "#b48ead", "green": "#a3be8c", + "teal": "#8fbcbb", "sky": "#81a1c1", "red": "#bf616a", + "peach": "#d08770", "yellow": "#ebcb8b", "mauve": "#b48ead", + "red_hover": "#ca737b", + "tab_hover": "#333a47", "tree_hover": "#323844", "btn_pressed": "#2e3440", + "gutter_bg": "#272c38", + "dirty_bg": "#3a3320", "dirty_fg": "#d08770", + "delete_bg": "#3a2730", "delete_fg": "#bf616a", + "syn_keyword": "#81a1c1", "syn_type": "#ebcb8b", + "syn_function": "#88c0d0", "syn_string": "#a3be8c", + "syn_number": "#b48ead", "syn_comment": "#616e88", + "syn_operator": "#8fbcbb", "syn_ident": "#eceff4", + "popup_bg": "#292e3b", "popup_border": "#434c5e", + "popup_selected": "#3b4252", "popup_hover": "#323844", +} + +# Tokyo Night — deep purple-dark +_TOKYO_NIGHT: dict = { + "base": "#1a1b2e", "mantle": "#16161e", "crust": "#13131a", + "surface0": "#24283b", "surface1": "#292e42", "surface2": "#2f3549", + "overlay": "#565f89", "subtext": "#9aa5ce", "text": "#c0caf5", + "blue": "#7aa2f7", "lavender": "#bb9af7", "green": "#9ece6a", + "teal": "#73daca", "sky": "#7dcfff", "red": "#f7768e", + "peach": "#ff9e64", "yellow": "#e0af68", "mauve": "#bb9af7", + "red_hover": "#f98da0", + "tab_hover": "#1e2030", "tree_hover": "#1e2030", "btn_pressed": "#1a1b2e", + "gutter_bg": "#16161e", + "dirty_bg": "#2a2216", "dirty_fg": "#ff9e64", + "delete_bg": "#2a1620", "delete_fg": "#f7768e", + "syn_keyword": "#bb9af7", "syn_type": "#e0af68", + "syn_function": "#7aa2f7", "syn_string": "#9ece6a", + "syn_number": "#ff9e64", "syn_comment": "#565f89", + "syn_operator": "#73daca", "syn_ident": "#c0caf5", + "popup_bg": "#16161e", "popup_border": "#292e42", + "popup_selected": "#24283b", "popup_hover": "#1e2030", +} + +# Dracula — classic purple-dark +_DRACULA: dict = { + "base": "#282a36", "mantle": "#21222c", "crust": "#191a21", + "surface0": "#343746", "surface1": "#424450", "surface2": "#54555f", + "overlay": "#6272a4", "subtext": "#bfbfbf", "text": "#f8f8f2", + "blue": "#8be9fd", "lavender": "#bd93f9", "green": "#50fa7b", + "teal": "#8be9fd", "sky": "#8be9fd", "red": "#ff5555", + "peach": "#ffb86c", "yellow": "#f1fa8c", "mauve": "#bd93f9", + "red_hover": "#ff7373", + "tab_hover": "#2d2f3e", "tree_hover": "#2d2f3e", "btn_pressed": "#21222c", + "gutter_bg": "#20212b", + "dirty_bg": "#3a3020", "dirty_fg": "#ffb86c", + "delete_bg": "#3a1e1e", "delete_fg": "#ff5555", + "syn_keyword": "#ff79c6", "syn_type": "#ffb86c", + "syn_function": "#50fa7b", "syn_string": "#f1fa8c", + "syn_number": "#bd93f9", "syn_comment": "#6272a4", + "syn_operator": "#ff79c6", "syn_ident": "#8be9fd", + "popup_bg": "#21222c", "popup_border": "#424450", + "popup_selected": "#343746", "popup_hover": "#2d2f3e", +} + +# GitHub Light — clean minimal light +_GITHUB_LIGHT: dict = { + "base": "#ffffff", "mantle": "#f6f8fa", "crust": "#eaeef2", + "surface0": "#f0f3f6", "surface1": "#d0d7de", "surface2": "#afb8c1", + "overlay": "#8c959f", "subtext": "#57606a", "text": "#24292f", + "blue": "#0969da", "lavender": "#8250df", "green": "#1a7f37", + "teal": "#0a7bca", "sky": "#0550ae", "red": "#cf222e", + "peach": "#bc4c00", "yellow": "#9a6700", "mauve": "#8250df", + "red_hover": "#d93f47", + "tab_hover": "#eaeef2", "tree_hover": "#eaeef2", "btn_pressed": "#e0e6ec", + "gutter_bg": "#f0f3f6", + "dirty_bg": "#fff8e1", "dirty_fg": "#bc4c00", + "delete_bg": "#fde8e8", "delete_fg": "#cf222e", + "syn_keyword": "#cf222e", "syn_type": "#953800", + "syn_function": "#8250df", "syn_string": "#0a3069", + "syn_number": "#0550ae", "syn_comment": "#8c959f", + "syn_operator": "#cf222e", "syn_ident": "#0969da", + "popup_bg": "#f6f8fa", "popup_border": "#d0d7de", + "popup_selected": "#e8ecf0", "popup_hover": "#eaeef2", +} + +# Registry: theme key → palette dict +ALL_THEMES: dict[str, dict] = { + "dark": _FRAPPÉ, + "light": _LATTE, + "one_dark": _ONE_DARK, + "nord": _NORD, + "tokyo_night": _TOKYO_NIGHT, + "dracula": _DRACULA, + "github_light": _GITHUB_LIGHT, +} + +# Which theme keys are considered "dark" +_DARK_KEYS = {"dark", "one_dark", "nord", "tokyo_night", "dracula"} + +# Kept for backward compat (imported elsewhere as DARK / LIGHT) +DARK = _FRAPPÉ +LIGHT = _LATTE + + +# ── Public API ──────────────────────────────────────────────────────────────── def get_palette() -> dict: """Return the color dict for the currently active theme.""" from app.config.settings import get_settings name = get_settings().get("theme", "dark") - return DARK if name == "dark" else LIGHT + return ALL_THEMES.get(name, _FRAPPÉ) -def is_dark() -> bool: - from app.config.settings import get_settings - return get_settings().get("theme", "dark") == "dark" - - -def qss_path(theme: str) -> Path: - name = "style" if theme == "dark" else "style_light" - return _QSS_DIR / f"{name}.qss" - - -def apply_qss(app, theme: str = None) -> None: - """Load and apply the QSS for *theme* (defaults to current setting).""" +def is_dark(theme: str | None = None) -> bool: from app.config.settings import get_settings if theme is None: theme = get_settings().get("theme", "dark") - path = qss_path(theme) - if path.exists(): - app.setStyleSheet(path.read_text(encoding="utf-8")) + return theme in _DARK_KEYS + + +def apply_qss(app, theme: str | None = None) -> None: + """Load the QSS template, substitute palette tokens, and apply to *app*.""" + from app.config.settings import get_settings + if theme is None: + theme = get_settings().get("theme", "dark") + palette = ALL_THEMES.get(theme, _FRAPPÉ) + + template_path = _QSS_DIR / "style_template.qss" + if template_path.exists(): + template = template_path.read_text(encoding="utf-8") + qss = re.sub(r"@\{(\w+)\}", lambda m: palette.get(m.group(1), m.group(0)), template) + app.setStyleSheet(qss) + return + + # Fallback: legacy per-file approach + legacy = _QSS_DIR / ("style.qss" if is_dark(theme) else "style_light.qss") + if legacy.exists(): + app.setStyleSheet(legacy.read_text(encoding="utf-8")) + + +def qss_path(theme: str) -> Path: + """Legacy helper — kept so old call-sites don't break.""" + return _QSS_DIR / ("style.qss" if is_dark(theme) else "style_light.qss") diff --git a/app/ui/preferences_dialog.py b/app/ui/preferences_dialog.py index d8bfc6f..39f6288 100644 --- a/app/ui/preferences_dialog.py +++ b/app/ui/preferences_dialog.py @@ -10,8 +10,15 @@ from app.config.settings import get_settings _FONT_FAMILIES = ["Consolas", "JetBrains Mono", "Fira Code", "Cascadia Code", "Courier New"] _PAGE_SIZES = [50, 100, 500, 1000, 0] # 0 → All _PAGE_LABELS = ["50", "100", "500", "1 000", "All"] -_THEMES = [("dark", "🌙 Dark (Catppuccin Frappé)"), - ("light", "☀️ Light (Catppuccin Latte)")] +_THEMES = [ + ("dark", "🌙 Catppuccin Frappé (dark blue-gray)"), + ("one_dark", "⬛ One Dark Pro (VS Code classic)"), + ("nord", "❄️ Nord (arctic blue-gray)"), + ("tokyo_night", "🌃 Tokyo Night (deep purple-dark)"), + ("dracula", "🧛 Dracula (classic purple-dark)"), + ("light", "☀️ Catppuccin Latte (warm light)"), + ("github_light", "📄 GitHub Light (clean minimal)"), +] class PreferencesDialog(QDialog): @@ -20,8 +27,8 @@ class PreferencesDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Preferences") - self.setMinimumWidth(400) - self.resize(420, 380) + self.setMinimumWidth(440) + self.resize(460, 400) self._build_ui() self._load() diff --git a/resources/style_template.qss b/resources/style_template.qss new file mode 100644 index 0000000..2cd752f --- /dev/null +++ b/resources/style_template.qss @@ -0,0 +1,589 @@ +/* ═══════════════════════════════════════════════════════════════════════════ + DBClient — Theme template (tokens replaced at runtime by theme.py) + ═══════════════════════════════════════════════════════════════════════════ */ + +/* ── Global reset ───────────────────────────────────────────────────────── */ +* { + outline: none; +} + +QMainWindow, QDialog { + background-color: @{base}; + color: @{text}; +} + +QWidget { + background-color: @{base}; + color: @{text}; + font-family: "Segoe UI", "Inter", sans-serif; + font-size: 10pt; +} + +/* ── Menu bar ───────────────────────────────────────────────────────────── */ +QMenuBar { + background-color: @{crust}; + color: @{text}; + border-bottom: 1px solid @{surface0}; + padding: 2px 0; +} + +QMenuBar::item { + padding: 4px 12px; + border-radius: 4px; +} + +QMenuBar::item:selected { + background-color: @{surface0}; +} + +QMenu { + background-color: @{mantle}; + border: 1px solid @{surface1}; + border-radius: 6px; + padding: 4px; +} + +QMenu::item { + padding: 6px 24px 6px 12px; + border-radius: 4px; + color: @{text}; +} + +QMenu::item:selected { + background-color: @{surface0}; + color: @{text}; +} + +QMenu::separator { + height: 1px; + background: @{surface1}; + margin: 4px 8px; +} + +/* ── Status bar ─────────────────────────────────────────────────────────── */ +QStatusBar { + background-color: @{crust}; + border-top: 1px solid @{surface0}; + color: @{subtext}; + font-size: 9pt; + padding: 2px 8px; +} + +/* ── Scrollbars ─────────────────────────────────────────────────────────── */ +QScrollBar:vertical { + background: @{base}; + width: 10px; + margin: 0; +} + +QScrollBar::handle:vertical { + background: @{surface1}; + border-radius: 5px; + min-height: 24px; +} + +QScrollBar::handle:vertical:hover { + background: @{surface2}; +} + +QScrollBar:horizontal { + background: @{base}; + height: 10px; +} + +QScrollBar::handle:horizontal { + background: @{surface1}; + border-radius: 5px; + min-width: 24px; +} + +QScrollBar::handle:horizontal:hover { + background: @{surface2}; +} + +QScrollBar::add-line, QScrollBar::sub-line { height: 0; width: 0; } +QScrollBar::add-page, QScrollBar::sub-page { background: transparent; } + +/* ── Splitter ────────────────────────────────────────────────────────────── */ +QSplitter::handle { + background-color: @{surface0}; +} + +QSplitter::handle:horizontal { width: 2px; } +QSplitter::handle:vertical { height: 2px; } + +QSplitter::handle:hover { + background-color: @{blue}; +} + +/* ── Sidebar ─────────────────────────────────────────────────────────────── */ +#sidebarHeader { + background-color: @{mantle}; + border-bottom: 1px solid @{surface0}; +} + +#sidebarTitle { + color: @{blue}; + font-size: 11pt; + font-weight: 600; +} + +#newConnBtn { + background-color: @{surface0}; + color: @{blue}; + border: 1px solid @{surface1}; + border-radius: 6px; + font-size: 14pt; + padding: 0; +} + +#newConnBtn:hover { + background-color: @{surface1}; + color: @{lavender}; +} + +/* ── Tree widget ─────────────────────────────────────────────────────────── */ +QTreeWidget { + background-color: @{mantle}; + border: none; + color: @{text}; + font-size: 10pt; + show-decoration-selected: 1; +} + +QTreeWidget::item { + padding: 3px 4px; + border-radius: 4px; +} + +QTreeWidget::item:hover { + background-color: @{tree_hover}; +} + +QTreeWidget::item:selected { + background-color: @{surface0}; + color: @{text}; +} + +QTreeWidget::branch { + background: transparent; +} + +/* ── Tab widget ──────────────────────────────────────────────────────────── */ +QTabWidget::pane { + border: none; + border-top: 1px solid @{surface0}; + background-color: @{base}; +} + +QTabBar { + background-color: @{mantle}; +} + +QTabBar::tab { + background-color: @{mantle}; + color: @{subtext}; + padding: 7px 16px; + border: none; + border-right: 1px solid @{surface0}; + font-size: 10pt; +} + +QTabBar::tab:selected { + background-color: @{base}; + color: @{text}; + border-bottom: 2px solid @{blue}; +} + +QTabBar::tab:hover:!selected { + background-color: @{tab_hover}; + color: @{text}; +} + +QTabBar::close-button { + subcontrol-position: right; + padding: 2px; +} + +/* ── Table view ──────────────────────────────────────────────────────────── */ +QTableView, QTableWidget { + background-color: @{base}; + alternate-background-color: @{tree_hover}; + gridline-color: @{surface0}; + color: @{text}; + border: none; + selection-background-color: @{surface0}; + selection-color: @{text}; + font-size: 10pt; +} + +QTableView::item, QTableWidget::item { + padding: 2px 6px; +} + +QHeaderView { + background-color: @{mantle}; +} + +QHeaderView::section { + background-color: @{mantle}; + color: @{blue}; + border: none; + border-right: 1px solid @{surface0}; + border-bottom: 1px solid @{surface0}; + padding: 4px 8px; + font-weight: 600; +} + +QHeaderView::section:hover { + background-color: @{tab_hover}; +} + +/* ── Plain text edit (SQL editor body) ───────────────────────────────────── */ +QPlainTextEdit { + background-color: @{base}; + color: @{text}; + border: none; + selection-background-color: @{surface1}; + font-family: "Consolas", "JetBrains Mono", "Courier New", monospace; + font-size: 13pt; + line-height: 1.5; +} + +/* ── Line number gutter ──────────────────────────────────────────────────── */ +LineNumberArea { + background-color: @{gutter_bg}; +} + +/* ── Buttons ─────────────────────────────────────────────────────────────── */ +QPushButton { + background-color: @{surface0}; + color: @{text}; + border: 1px solid @{surface1}; + border-radius: 6px; + padding: 5px 14px; + font-size: 10pt; +} + +QPushButton:hover { + background-color: @{surface1}; + border-color: @{surface2}; +} + +QPushButton:pressed { + background-color: @{btn_pressed}; +} + +QPushButton:disabled { + color: @{surface2}; + background-color: @{btn_pressed}; + border-color: @{surface0}; +} + +#runBtn { + background-color: @{green}; + color: @{base}; + font-weight: 700; + border: none; +} + +#runBtn:hover { + background-color: @{teal}; +} + +#stopBtn { + background-color: @{red}; + color: @{base}; + font-weight: 700; + border: none; +} + +#stopBtn:hover { + background-color: @{red_hover}; +} + +#commitBtn { + background-color: @{green}; + color: @{base}; + font-weight: 700; + border: none; +} + +#rollbackBtn { + background-color: @{peach}; + color: @{base}; + font-weight: 700; + border: none; +} + +#crudAddBtn { + background-color: @{surface0}; + color: @{green}; + border: 1px solid @{green}; + font-weight: 600; +} + +#crudAddBtn:hover { + background-color: @{green}; + color: @{base}; +} + +#crudDeleteBtn { + background-color: @{surface0}; + color: @{red}; + border: 1px solid @{red}; + font-weight: 600; +} + +#crudDeleteBtn:hover { + background-color: @{red}; + color: @{base}; +} + +/* ── Line edit ───────────────────────────────────────────────────────────── */ +QLineEdit { + background-color: @{surface0}; + color: @{text}; + border: 1px solid @{surface1}; + border-radius: 6px; + padding: 5px 10px; + font-size: 10pt; + selection-background-color: @{blue}; + selection-color: @{base}; +} + +QLineEdit:focus { + border-color: @{blue}; +} + +QLineEdit::placeholder { + color: @{surface2}; +} + +/* ── Combo box ───────────────────────────────────────────────────────────── */ +QComboBox { + background-color: @{surface0}; + color: @{text}; + border: 1px solid @{surface1}; + border-radius: 6px; + padding: 5px 10px; + font-size: 10pt; +} + +QComboBox:focus { + border-color: @{blue}; +} + +QComboBox::drop-down { + border: none; + width: 24px; +} + +QComboBox QAbstractItemView { + background-color: @{mantle}; + color: @{text}; + border: 1px solid @{surface1}; + selection-background-color: @{surface0}; +} + +/* ── Spin box ────────────────────────────────────────────────────────────── */ +QSpinBox { + background-color: @{surface0}; + color: @{text}; + border: 1px solid @{surface1}; + border-radius: 6px; + padding: 4px 8px; +} + +QSpinBox:focus { border-color: @{blue}; } + +QSpinBox::up-button, QSpinBox::down-button { + background-color: @{surface1}; + border: none; + border-radius: 3px; + width: 16px; +} + +/* ── Check box ───────────────────────────────────────────────────────────── */ +QCheckBox { + color: @{text}; + spacing: 8px; +} + +QCheckBox::indicator { + width: 16px; + height: 16px; + border: 2px solid @{surface1}; + border-radius: 4px; + background: @{surface0}; +} + +QCheckBox::indicator:checked { + background-color: @{blue}; + border-color: @{blue}; +} + +/* ── Dialog ──────────────────────────────────────────────────────────────── */ +QDialog { + background-color: @{base}; +} + +QDialogButtonBox QPushButton { + min-width: 80px; +} + +/* ── Form layout labels ──────────────────────────────────────────────────── */ +QFormLayout QLabel { + color: @{subtext}; +} + +/* ── Tab widget in dialogs ───────────────────────────────────────────────── */ +QTabWidget#dialogTabs::pane { + border: 1px solid @{surface0}; + border-radius: 6px; + margin-top: -1px; +} + +/* ── Group box ───────────────────────────────────────────────────────────── */ +QGroupBox { + border: 1px solid @{surface1}; + border-radius: 6px; + margin-top: 1em; + color: @{subtext}; + font-weight: 600; +} + +QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 4px; +} + +/* ── Progress bar ────────────────────────────────────────────────────────── */ +QProgressBar { + background-color: @{surface0}; + border: 1px solid @{surface1}; + border-radius: 6px; + height: 10px; + text-align: center; +} + +QProgressBar::chunk { + background-color: @{blue}; + border-radius: 5px; +} + +/* ── Dock widget ─────────────────────────────────────────────────────────── */ +QDockWidget { + color: @{text}; + titlebar-close-icon: none; + font-weight: 600; +} + +QDockWidget::title { + background-color: @{mantle}; + padding: 6px; + border-bottom: 1px solid @{surface0}; +} + +/* ── Tool button ─────────────────────────────────────────────────────────── */ +QToolButton { + background-color: @{surface0}; + color: @{text}; + border: 1px solid @{surface1}; + border-radius: 5px; + padding: 4px 8px; + font-size: 13pt; +} + +QToolButton:hover { + background-color: @{surface1}; +} + +/* ── Message box ─────────────────────────────────────────────────────────── */ +QMessageBox { + background-color: @{base}; +} + +QMessageBox QLabel { + color: @{text}; + font-size: 10pt; +} + +/* ── Empty workspace label ───────────────────────────────────────────────── */ +#emptyLabel { + color: @{surface2}; + font-size: 14pt; + line-height: 2; +} + +/* ── Status label in results toolbar ────────────────────────────────────── */ +#statusLabel { + color: @{subtext}; + font-size: 9pt; + padding-left: 4px; +} + +/* ── DB label in SQL editor toolbar ─────────────────────────────────────── */ +#dbLabel { + color: @{overlay}; + font-size: 9pt; + padding-right: 4px; +} + +/* ── Workspace tab widget ────────────────────────────────────────────────── */ +#workspace > QTabBar::tab { + min-width: 120px; +} + +/* ── Horizontal separator line in dialog ────────────────────────────────── */ +QFrame[frameShape="4"] { /* HLine */ + color: @{surface0}; + margin: 4px 0; +} + +/* ── Structure view title ────────────────────────────────────────────────── */ +#structureTitle { + color: @{lavender}; +} + +/* ── Pagination bar ──────────────────────────────────────────────────────── */ +#paginationBar { + background-color: @{mantle}; + border-top: 1px solid @{surface1}; + min-height: 38px; +} + +#pgBtn { + background-color: @{btn_pressed}; + color: @{text}; + border: 1px solid @{surface1}; + border-radius: 5px; + padding: 4px 10px; + font-size: 9pt; + min-width: 52px; +} + +#pgBtn:hover { + background-color: @{surface0}; + border-color: @{surface2}; +} + +#pgBtn:disabled { + color: @{surface1}; + background-color: @{base}; + border-color: @{surface0}; +} + +#pageLbl { + color: @{blue}; + font-size: 9pt; + font-weight: 600; + padding: 0 6px; +} + +#rowCountLbl { + color: @{subtext}; + font-size: 9pt; + padding-left: 8px; +}