feat: dark/light theme toggle (Catppuccin Frappé ↔ Latte)
- app/config/theme.py: palette definitions for both themes + apply_qss() helper used by both main.py (startup) and MainWindow (live toggle) - resources/style_light.qss: complete Catppuccin Latte QSS - View menu: ☀️/🌙 Switch Theme action (Ctrl+Shift+T) toggles live - Preferences → Appearance: Color theme dropdown persists choice - SQLHighlighter: _build_rules() reads get_palette(); update_theme() rebuilds rules and rehighlights all open editors on switch - SqlCompleter: popup stylesheet reads get_palette() via _apply_popup_style(); update_theme() called on switch - EditableTableModel: dirty/delete cell colors read get_palette() live - ResultTableModel: NULL_COLOR property reads get_palette() live - TableViewer: apply_settings() refreshes frozen-view border + repaints Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -144,6 +144,7 @@ All four drivers have a `_where(where: dict) → (clause_str, params)` static he
|
||||
- Connection ping indicator: 30-second `_PingWorker` (QThread) pings each active connection via `test_connection()`; status dot (● green / ● red) shown in status bar right side with per-connection tooltip
|
||||
- Frozen first column in `TableViewer`: 📌 toggle button in toolbar; dual-view overlay (`_frozen_view` child of `_table_view`) with synced vertical scroll and row heights; event-filter updates geometry on resize
|
||||
- Keyboard navigation in dialogs: OK set as default button (Enter submits) in `RowDialog` and `ConnectionDialog`; focus jumps to first input field on open; Enter in password field submits `ConnectionDialog`
|
||||
- Light / dark theme toggle: `app/config/theme.py` holds Catppuccin Frappé (dark) + Latte (light) palettes; `resources/style_light.qss` is the full Latte QSS; toggle via View → Switch Theme (Ctrl+Shift+T) or Preferences → Color theme; on switch, QSS is reloaded, syntax highlighter rebuilds rules via `update_theme()`, completer popup recolors, and model dirty/NULL colors update live
|
||||
|
||||
### Not Yet Implemented
|
||||
**High impact:**
|
||||
@@ -155,5 +156,4 @@ All four drivers have a `_where(where: dict) → (clause_str, params)` static he
|
||||
**Medium impact:**
|
||||
- Transaction panel: explicit BEGIN / COMMIT / ROLLBACK controls with in-flight indicator
|
||||
- Global schema search: search across all tables/columns/procedures in a database
|
||||
- Light theme: Catppuccin Latte or similar, toggle from Preferences
|
||||
- Schema diff: compare two database schemas and show structural differences
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
Theme palettes — Catppuccin Frappé (dark) and Catppuccin Latte (light).
|
||||
|
||||
Usage:
|
||||
from app.config.theme import get_palette, apply_qss, is_dark
|
||||
"""
|
||||
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"
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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)."""
|
||||
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"))
|
||||
+28
-1
@@ -17,6 +17,7 @@ from PyQt6.QtWidgets import (
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal
|
||||
from PyQt6.QtGui import QAction, QColor, QKeySequence
|
||||
from app.config.theme import apply_qss, is_dark
|
||||
|
||||
from app.ui.schema_browser import SchemaBrowser
|
||||
from app.ui.sql_editor import SQLEditorWidget
|
||||
@@ -208,6 +209,12 @@ class MainWindow(QMainWindow):
|
||||
view_menu.addAction(self._act("Toggle Query History", self._toggle_history, "Ctrl+Shift+H"))
|
||||
view_menu.addAction(self._act("New SQL Tab", self._new_sql_tab, "Ctrl+T"))
|
||||
view_menu.addAction(self._act("Close Tab", self._close_current_tab, "Ctrl+W"))
|
||||
view_menu.addSeparator()
|
||||
self._theme_action = QAction("", self)
|
||||
self._theme_action.setShortcut(QKeySequence("Ctrl+Shift+T"))
|
||||
self._theme_action.triggered.connect(self._toggle_theme)
|
||||
self._update_theme_action_label()
|
||||
view_menu.addAction(self._theme_action)
|
||||
|
||||
# ── Tools ─────────────────────────────────────────────────────────────
|
||||
tools_menu = mb.addMenu("&Tools")
|
||||
@@ -662,9 +669,29 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def _open_preferences(self):
|
||||
dlg = PreferencesDialog(parent=self)
|
||||
dlg.settings_applied.connect(self._apply_settings_to_open_tabs)
|
||||
dlg.settings_applied.connect(self._on_settings_applied)
|
||||
dlg.exec()
|
||||
|
||||
def _on_settings_applied(self):
|
||||
apply_qss(QApplication.instance())
|
||||
self._update_theme_action_label()
|
||||
self._apply_settings_to_open_tabs()
|
||||
|
||||
def _toggle_theme(self):
|
||||
from app.config.settings import get_settings
|
||||
new_theme = "light" if is_dark() else "dark"
|
||||
get_settings().set("theme", new_theme)
|
||||
get_settings().save()
|
||||
apply_qss(QApplication.instance(), new_theme)
|
||||
self._update_theme_action_label()
|
||||
self._apply_settings_to_open_tabs()
|
||||
|
||||
def _update_theme_action_label(self):
|
||||
if is_dark():
|
||||
self._theme_action.setText("☀️ Switch to Light Theme")
|
||||
else:
|
||||
self._theme_action.setText("🌙 Switch to Dark Theme")
|
||||
|
||||
def _apply_settings_to_open_tabs(self):
|
||||
"""Push updated settings to all currently open workspace tabs."""
|
||||
for i in range(self._workspace.count()):
|
||||
|
||||
@@ -7,11 +7,15 @@ from PyQt6.QtCore import (
|
||||
)
|
||||
from PyQt6.QtGui import QColor, QFont
|
||||
|
||||
from app.config.theme import get_palette
|
||||
|
||||
|
||||
class ResultTableModel(QAbstractTableModel):
|
||||
"""Immutable result-set model — replaces data via set_data()."""
|
||||
|
||||
NULL_COLOR = QColor("#737994") # muted grey for NULL
|
||||
@property
|
||||
def NULL_COLOR(self):
|
||||
return QColor(get_palette()["overlay"])
|
||||
NUM_ALIGN = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||
TEXT_ALIGN = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ 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)")]
|
||||
|
||||
|
||||
class PreferencesDialog(QDialog):
|
||||
@@ -64,6 +66,18 @@ class PreferencesDialog(QDialog):
|
||||
|
||||
root.addWidget(results_box)
|
||||
|
||||
# ── Appearance ────────────────────────────────────────────────────────
|
||||
appear_box = QGroupBox("Appearance")
|
||||
af = QFormLayout(appear_box)
|
||||
af.setSpacing(8)
|
||||
|
||||
self._theme = QComboBox()
|
||||
for _, label in _THEMES:
|
||||
self._theme.addItem(label)
|
||||
af.addRow("Color theme:", self._theme)
|
||||
|
||||
root.addWidget(appear_box)
|
||||
|
||||
# ── General ───────────────────────────────────────────────────────────
|
||||
general_box = QGroupBox("General")
|
||||
gf = QFormLayout(general_box)
|
||||
@@ -82,7 +96,7 @@ class PreferencesDialog(QDialog):
|
||||
root.addStretch()
|
||||
|
||||
# ── Buttons ───────────────────────────────────────────────────────────
|
||||
note = QLabel("Font changes take effect immediately in open editors.")
|
||||
note = QLabel("Theme and font changes take effect immediately.")
|
||||
note.setObjectName("statusLabel")
|
||||
root.addWidget(note)
|
||||
|
||||
@@ -116,6 +130,10 @@ class PreferencesDialog(QDialog):
|
||||
self._max_history.setValue(s.get("max_history", 500))
|
||||
self._auto_commit.setChecked(s.get("auto_commit", True))
|
||||
|
||||
theme_name = s.get("theme", "dark")
|
||||
ti = next((i for i, (k, _) in enumerate(_THEMES) if k == theme_name), 0)
|
||||
self._theme.setCurrentIndex(ti)
|
||||
|
||||
def _apply(self):
|
||||
s = get_settings()
|
||||
s.set("font_family", self._font_family.currentText())
|
||||
@@ -125,6 +143,7 @@ class PreferencesDialog(QDialog):
|
||||
s.set("query_timeout", self._timeout.value())
|
||||
s.set("max_history", self._max_history.value())
|
||||
s.set("auto_commit", self._auto_commit.isChecked())
|
||||
s.set("theme", _THEMES[self._theme.currentIndex()][0])
|
||||
s.save()
|
||||
self.settings_applied.emit()
|
||||
|
||||
|
||||
+23
-14
@@ -12,6 +12,7 @@ from PyQt6.QtWidgets import QApplication, QListWidget, QListWidgetItem
|
||||
|
||||
from app.ui.syntax_highlighter import _KEYWORDS, _TYPES, _FUNCTIONS
|
||||
from app.utils.worker import SchemaWorker
|
||||
from app.config.theme import get_palette
|
||||
|
||||
# Combined, deduplicated keyword list used as the static completion pool.
|
||||
SQL_KEYWORDS: list[str] = list(dict.fromkeys(_KEYWORDS + _TYPES + _FUNCTIONS))
|
||||
@@ -51,24 +52,28 @@ class _CompletionPopup(QListWidget):
|
||||
self.setFont(font)
|
||||
self._row_h = QFontMetrics(font).height() + 6
|
||||
|
||||
self.setStyleSheet("""
|
||||
QListWidget {
|
||||
background: #292c3c;
|
||||
color: #c6d0f5;
|
||||
border: 1px solid #51576d;
|
||||
self._apply_popup_style()
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
|
||||
def _apply_popup_style(self):
|
||||
p = get_palette()
|
||||
self.setStyleSheet(f"""
|
||||
QListWidget {{
|
||||
background: {p['popup_bg']};
|
||||
color: {p['text']};
|
||||
border: 1px solid {p['popup_border']};
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
outline: none;
|
||||
}
|
||||
QListWidget::item { padding: 2px 8px; }
|
||||
QListWidget::item:selected {
|
||||
background: #414559;
|
||||
color: #c6d0f5;
|
||||
}
|
||||
QListWidget::item:hover { background: #363a4f; }
|
||||
}}
|
||||
QListWidget::item {{ padding: 2px 8px; }}
|
||||
QListWidget::item:selected {{
|
||||
background: {p['popup_selected']};
|
||||
color: {p['text']};
|
||||
}}
|
||||
QListWidget::item:hover {{ background: {p['popup_hover']}; }}
|
||||
""")
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
|
||||
self.itemClicked.connect(
|
||||
lambda item: self._editor._completer._accept_completion(item.text())
|
||||
@@ -142,6 +147,10 @@ class SqlCompleter(QObject):
|
||||
|
||||
# ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
def update_theme(self) -> None:
|
||||
"""Reapply popup colors after a theme change."""
|
||||
self._popup._apply_popup_style()
|
||||
|
||||
def set_context(self, driver, database: str) -> None:
|
||||
"""Update the active driver/database and refresh schema cache."""
|
||||
self._driver = driver
|
||||
|
||||
@@ -471,7 +471,7 @@ class EditorTab(QWidget):
|
||||
self._splitter.setHandleWidth(3)
|
||||
|
||||
self._editor = CodeEditor()
|
||||
SQLHighlighter(self._editor.document())
|
||||
self._highlighter = SQLHighlighter(self._editor.document())
|
||||
self._completer = SqlCompleter(self._editor, parent=self)
|
||||
self._editor.set_completer(self._completer)
|
||||
if self._driver:
|
||||
@@ -603,6 +603,8 @@ class EditorTab(QWidget):
|
||||
|
||||
def apply_settings(self) -> None:
|
||||
self._editor.apply_settings()
|
||||
self._highlighter.update_theme()
|
||||
self._completer.update_theme()
|
||||
|
||||
|
||||
# ── Tabbed SQL editor container ───────────────────────────────────────────────
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
SQL syntax highlighter for QPlainTextEdit.
|
||||
Highlights keywords, types, functions, strings, comments, and numbers.
|
||||
Colors follow the active Catppuccin theme (dark = Frappé, light = Latte).
|
||||
"""
|
||||
import re
|
||||
from PyQt6.QtCore import QRegularExpression, Qt
|
||||
@@ -8,6 +9,8 @@ from PyQt6.QtGui import (
|
||||
QSyntaxHighlighter, QTextCharFormat, QColor, QFont
|
||||
)
|
||||
|
||||
from app.config.theme import get_palette
|
||||
|
||||
|
||||
def _fmt(color: str, bold: bool = False, italic: bool = False) -> QTextCharFormat:
|
||||
f = QTextCharFormat()
|
||||
@@ -17,17 +20,7 @@ def _fmt(color: str, bold: bool = False, italic: bool = False) -> QTextCharForma
|
||||
return f
|
||||
|
||||
|
||||
# ── Token categories (Catppuccin Frappé palette) ─────────────────────────────
|
||||
_KEYWORD_FMT = _fmt("#8caaee", bold=True) # blue — DDL/DML/control
|
||||
_TYPE_FMT = _fmt("#ef9f76") # peach — data types
|
||||
_FUNCTION_FMT = _fmt("#a6d189") # green — functions
|
||||
_STRING_FMT = _fmt("#a6d189") # green — string literals
|
||||
_NUMBER_FMT = _fmt("#ef9f76") # orange — numeric literals
|
||||
_COMMENT_FMT = _fmt("#737994", italic=True) # grey — comments
|
||||
_OPERATOR_FMT = _fmt("#ca9ee6") # mauve — operators/special
|
||||
_STAR_FMT = _fmt("#ca9ee6", bold=True) # mauve — * wildcard
|
||||
|
||||
# ── Keyword lists ─────────────────────────────────────────────────────────────
|
||||
# ── Keyword lists (shared with completer) ─────────────────────────────────────
|
||||
_KEYWORDS = [
|
||||
"SELECT", "FROM", "WHERE", "JOIN", "LEFT", "RIGHT", "INNER", "OUTER",
|
||||
"FULL", "CROSS", "ON", "AS", "AND", "OR", "NOT", "IN", "LIKE", "BETWEEN",
|
||||
@@ -79,56 +72,56 @@ class SQLHighlighter(QSyntaxHighlighter):
|
||||
def __init__(self, document):
|
||||
super().__init__(document)
|
||||
self._rules: list[tuple] = []
|
||||
self._build_rules()
|
||||
|
||||
def _build_rules(self):
|
||||
p = get_palette()
|
||||
|
||||
def kw_pattern(words: list[str]) -> str:
|
||||
return r"\b(?:" + "|".join(words) + r")\b"
|
||||
|
||||
ci = QRegularExpression.PatternOption.CaseInsensitiveOption
|
||||
self._rules = [
|
||||
# Keywords (case-insensitive handled via flag)
|
||||
(QRegularExpression(kw_pattern(_KEYWORDS),
|
||||
QRegularExpression.PatternOption.CaseInsensitiveOption),
|
||||
_KEYWORD_FMT),
|
||||
# Data types
|
||||
(QRegularExpression(kw_pattern(_TYPES),
|
||||
QRegularExpression.PatternOption.CaseInsensitiveOption),
|
||||
_TYPE_FMT),
|
||||
# Functions
|
||||
(QRegularExpression(kw_pattern(_FUNCTIONS),
|
||||
QRegularExpression.PatternOption.CaseInsensitiveOption),
|
||||
_FUNCTION_FMT),
|
||||
# Numbers
|
||||
(QRegularExpression(r"\b\d+(\.\d+)?\b"), _NUMBER_FMT),
|
||||
# Single-quoted strings
|
||||
(QRegularExpression(r"'[^'\\]*(?:\\.[^'\\]*)*'"), _STRING_FMT),
|
||||
# Double-quoted identifiers
|
||||
(QRegularExpression(r'"[^"]*"'), _fmt("#99d1db")),
|
||||
# Backtick identifiers (MySQL)
|
||||
(QRegularExpression(r"`[^`]*`"), _fmt("#99d1db")),
|
||||
# * wildcard
|
||||
(QRegularExpression(r"\bSELECT\s+\*|\*(?=\s*FROM)",
|
||||
QRegularExpression.PatternOption.CaseInsensitiveOption),
|
||||
_STAR_FMT),
|
||||
# Operators
|
||||
(QRegularExpression(r"[=<>!%&|^~+\-*/]"), _OPERATOR_FMT),
|
||||
# Single-line comment --
|
||||
(QRegularExpression(r"--[^\n]*"), _COMMENT_FMT),
|
||||
# Single-line comment #
|
||||
(QRegularExpression(r"#[^\n]*"), _COMMENT_FMT),
|
||||
(QRegularExpression(kw_pattern(_KEYWORDS), ci),
|
||||
_fmt(p["syn_keyword"], bold=True)),
|
||||
(QRegularExpression(kw_pattern(_TYPES), ci),
|
||||
_fmt(p["syn_type"])),
|
||||
(QRegularExpression(kw_pattern(_FUNCTIONS), ci),
|
||||
_fmt(p["syn_function"])),
|
||||
(QRegularExpression(r"\b\d+(\.\d+)?\b"),
|
||||
_fmt(p["syn_number"])),
|
||||
(QRegularExpression(r"'[^'\\]*(?:\\.[^'\\]*)*'"),
|
||||
_fmt(p["syn_string"])),
|
||||
(QRegularExpression(r'"[^"]*"'),
|
||||
_fmt(p["syn_ident"])),
|
||||
(QRegularExpression(r"`[^`]*`"),
|
||||
_fmt(p["syn_ident"])),
|
||||
(QRegularExpression(
|
||||
r"\bSELECT\s+\*|\*(?=\s*FROM)", ci),
|
||||
_fmt(p["syn_operator"], bold=True)),
|
||||
(QRegularExpression(r"[=<>!%&|^~+\-*/]"),
|
||||
_fmt(p["syn_operator"])),
|
||||
(QRegularExpression(r"--[^\n]*"),
|
||||
_fmt(p["syn_comment"], italic=True)),
|
||||
(QRegularExpression(r"#[^\n]*"),
|
||||
_fmt(p["syn_comment"], italic=True)),
|
||||
]
|
||||
|
||||
# Multi-line block comment /* ... */
|
||||
self._comment_fmt = _fmt(p["syn_comment"], italic=True)
|
||||
self._block_comment_start = QRegularExpression(r"/\*")
|
||||
self._block_comment_end = QRegularExpression(r"\*/")
|
||||
|
||||
def update_theme(self):
|
||||
"""Rebuild color rules for the current theme and re-highlight."""
|
||||
self._build_rules()
|
||||
self.rehighlight()
|
||||
|
||||
def highlightBlock(self, text: str) -> None:
|
||||
# Single-line rules
|
||||
for pattern, fmt in self._rules:
|
||||
it = pattern.globalMatch(text)
|
||||
while it.hasNext():
|
||||
m = it.next()
|
||||
self.setFormat(m.capturedStart(), m.capturedLength(), fmt)
|
||||
|
||||
# Multi-line block comments
|
||||
self.setCurrentBlockState(0)
|
||||
start_idx = 0
|
||||
if self.previousBlockState() != 1:
|
||||
@@ -139,10 +132,10 @@ class SQLHighlighter(QSyntaxHighlighter):
|
||||
end_m = self._block_comment_end.match(text, start_idx)
|
||||
if end_m.hasMatch():
|
||||
end_idx = end_m.capturedStart() + end_m.capturedLength()
|
||||
self.setFormat(start_idx, end_idx - start_idx, _COMMENT_FMT)
|
||||
self.setFormat(start_idx, end_idx - start_idx, self._comment_fmt)
|
||||
m = self._block_comment_start.match(text, end_idx)
|
||||
start_idx = m.capturedStart() if m.hasMatch() else -1
|
||||
else:
|
||||
self.setCurrentBlockState(1)
|
||||
self.setFormat(start_idx, len(text) - start_idx, _COMMENT_FMT)
|
||||
self.setFormat(start_idx, len(text) - start_idx, self._comment_fmt)
|
||||
break
|
||||
|
||||
+15
-6
@@ -22,6 +22,7 @@ from PyQt6.QtGui import QColor, QBrush, QFont, QKeySequence, QShortcut
|
||||
|
||||
from app.utils.worker import TableDataWorker
|
||||
from app.config.settings import get_settings
|
||||
from app.config.theme import get_palette
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -148,18 +149,20 @@ class EditableTableModel(QAbstractTableModel):
|
||||
return "" if val is None else str(val)
|
||||
|
||||
if role == Qt.ItemDataRole.ForegroundRole:
|
||||
p = get_palette()
|
||||
if self._is_deleted_row(r):
|
||||
return QBrush(QColor("#e78284")) # red — pending delete
|
||||
return QBrush(QColor(p["delete_fg"]))
|
||||
if (r, c) in self._dirty:
|
||||
return QBrush(QColor("#ef9f76")) # orange — edited
|
||||
return QBrush(QColor(p["dirty_fg"]))
|
||||
if val is None:
|
||||
return QBrush(QColor("#737994")) # grey — NULL
|
||||
return QBrush(QColor(p["overlay"]))
|
||||
|
||||
if role == Qt.ItemDataRole.BackgroundRole:
|
||||
p = get_palette()
|
||||
if self._is_deleted_row(r):
|
||||
return QBrush(QColor("#3a2530"))
|
||||
return QBrush(QColor(p["delete_bg"]))
|
||||
if (r, c) in self._dirty:
|
||||
return QBrush(QColor("#3a2f25"))
|
||||
return QBrush(QColor(p["dirty_bg"]))
|
||||
|
||||
return None
|
||||
|
||||
@@ -444,7 +447,7 @@ class TableViewer(QWidget):
|
||||
self._frozen_view.horizontalHeader().setSectionResizeMode(
|
||||
QHeaderView.ResizeMode.Fixed)
|
||||
self._frozen_view.setStyleSheet(
|
||||
"QTableView { border: none; border-right: 2px solid #8caaee; }")
|
||||
f"QTableView {{ border: none; border-right: 2px solid {get_palette()['blue']}; }}")
|
||||
self._frozen_view.hide()
|
||||
|
||||
# Sync vertical scroll between the two views
|
||||
@@ -887,6 +890,12 @@ class TableViewer(QWidget):
|
||||
)
|
||||
QApplication.clipboard().setText("\n".join(statements))
|
||||
|
||||
def apply_settings(self):
|
||||
"""Called by MainWindow when preferences (including theme) change."""
|
||||
self._frozen_view.setStyleSheet(
|
||||
f"QTableView {{ border: none; border-right: 2px solid {get_palette()['blue']}; }}")
|
||||
self._model.layoutChanged.emit() # repaint dirty/delete cells
|
||||
|
||||
def _filter_by_cell(self, index):
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
@@ -9,15 +9,13 @@ from PyQt6.QtGui import QFont
|
||||
|
||||
from app.utils.logger import setup_logging, install_qt_message_handler, get_logger
|
||||
from app.main_window import MainWindow
|
||||
from app.config.theme import apply_qss
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
def load_stylesheet(app: QApplication) -> None:
|
||||
style_path = os.path.join(os.path.dirname(__file__), "resources", "style.qss")
|
||||
if os.path.exists(style_path):
|
||||
with open(style_path, "r", encoding="utf-8") as f:
|
||||
app.setStyleSheet(f.read())
|
||||
apply_qss(app) # reads theme from settings, falls back to dark
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -0,0 +1,599 @@
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
DBClient — Light Theme (Catppuccin Latte palette)
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Variables via flat tokens ─────────────────────────────────────────────
|
||||
base #eff1f5 crust #dce0e8
|
||||
surface0 #ccd0da surface1 #bcc0cc surface2 #acb0be
|
||||
overlay #9ca0b0 subtext #6c6f85 text #4c4f69
|
||||
blue #1e66f5 lavender #7287fd sapphire #209fb5
|
||||
green #40a02b teal #179299 sky #04a5e5
|
||||
mauve #8839ef pink #ea76cb red #d20f39
|
||||
peach #fe640b yellow #df8e1d
|
||||
─────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* ── Global reset ───────────────────────────────────────────────────────── */
|
||||
* {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
QMainWindow, QDialog {
|
||||
background-color: #eff1f5;
|
||||
color: #4c4f69;
|
||||
}
|
||||
|
||||
QWidget {
|
||||
background-color: #eff1f5;
|
||||
color: #4c4f69;
|
||||
font-family: "Segoe UI", "Inter", sans-serif;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
/* ── Menu bar ───────────────────────────────────────────────────────────── */
|
||||
QMenuBar {
|
||||
background-color: #dce0e8;
|
||||
color: #4c4f69;
|
||||
border-bottom: 1px solid #ccd0da;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
QMenuBar::item {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QMenuBar::item:selected {
|
||||
background-color: #ccd0da;
|
||||
}
|
||||
|
||||
QMenu {
|
||||
background-color: #e6e9ef;
|
||||
border: 1px solid #bcc0cc;
|
||||
border-radius: 6px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
QMenu::item {
|
||||
padding: 6px 24px 6px 12px;
|
||||
border-radius: 4px;
|
||||
color: #4c4f69;
|
||||
}
|
||||
|
||||
QMenu::item:selected {
|
||||
background-color: #ccd0da;
|
||||
color: #4c4f69;
|
||||
}
|
||||
|
||||
QMenu::separator {
|
||||
height: 1px;
|
||||
background: #bcc0cc;
|
||||
margin: 4px 8px;
|
||||
}
|
||||
|
||||
/* ── Status bar ─────────────────────────────────────────────────────────── */
|
||||
QStatusBar {
|
||||
background-color: #dce0e8;
|
||||
border-top: 1px solid #ccd0da;
|
||||
color: #6c6f85;
|
||||
font-size: 9pt;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
/* ── Scrollbars ─────────────────────────────────────────────────────────── */
|
||||
QScrollBar:vertical {
|
||||
background: #eff1f5;
|
||||
width: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical {
|
||||
background: #bcc0cc;
|
||||
border-radius: 5px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background: #acb0be;
|
||||
}
|
||||
|
||||
QScrollBar:horizontal {
|
||||
background: #eff1f5;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal {
|
||||
background: #bcc0cc;
|
||||
border-radius: 5px;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background: #acb0be;
|
||||
}
|
||||
|
||||
QScrollBar::add-line, QScrollBar::sub-line { height: 0; width: 0; }
|
||||
QScrollBar::add-page, QScrollBar::sub-page { background: transparent; }
|
||||
|
||||
/* ── Splitter ────────────────────────────────────────────────────────────── */
|
||||
QSplitter::handle {
|
||||
background-color: #ccd0da;
|
||||
}
|
||||
|
||||
QSplitter::handle:horizontal { width: 2px; }
|
||||
QSplitter::handle:vertical { height: 2px; }
|
||||
|
||||
QSplitter::handle:hover {
|
||||
background-color: #1e66f5;
|
||||
}
|
||||
|
||||
/* ── Sidebar ─────────────────────────────────────────────────────────────── */
|
||||
#sidebarHeader {
|
||||
background-color: #e6e9ef;
|
||||
border-bottom: 1px solid #ccd0da;
|
||||
}
|
||||
|
||||
#sidebarTitle {
|
||||
color: #1e66f5;
|
||||
font-size: 11pt;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#newConnBtn {
|
||||
background-color: #ccd0da;
|
||||
color: #1e66f5;
|
||||
border: 1px solid #bcc0cc;
|
||||
border-radius: 6px;
|
||||
font-size: 14pt;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#newConnBtn:hover {
|
||||
background-color: #bcc0cc;
|
||||
color: #7287fd;
|
||||
}
|
||||
|
||||
/* ── Tree widget ─────────────────────────────────────────────────────────── */
|
||||
QTreeWidget {
|
||||
background-color: #e6e9ef;
|
||||
border: none;
|
||||
color: #4c4f69;
|
||||
font-size: 10pt;
|
||||
show-decoration-selected: 1;
|
||||
}
|
||||
|
||||
QTreeWidget::item {
|
||||
padding: 3px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QTreeWidget::item:hover {
|
||||
background-color: #d8dce8;
|
||||
}
|
||||
|
||||
QTreeWidget::item:selected {
|
||||
background-color: #ccd0da;
|
||||
color: #4c4f69;
|
||||
}
|
||||
|
||||
QTreeWidget::branch {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ── Tab widget ──────────────────────────────────────────────────────────── */
|
||||
QTabWidget::pane {
|
||||
border: none;
|
||||
border-top: 1px solid #ccd0da;
|
||||
background-color: #eff1f5;
|
||||
}
|
||||
|
||||
QTabBar {
|
||||
background-color: #e6e9ef;
|
||||
}
|
||||
|
||||
QTabBar::tab {
|
||||
background-color: #e6e9ef;
|
||||
color: #6c6f85;
|
||||
padding: 7px 16px;
|
||||
border: none;
|
||||
border-right: 1px solid #ccd0da;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
QTabBar::tab:selected {
|
||||
background-color: #eff1f5;
|
||||
color: #4c4f69;
|
||||
border-bottom: 2px solid #1e66f5;
|
||||
}
|
||||
|
||||
QTabBar::tab:hover:!selected {
|
||||
background-color: #d0d4e0;
|
||||
color: #4c4f69;
|
||||
}
|
||||
|
||||
QTabBar::close-button {
|
||||
subcontrol-position: right;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* ── Table view ──────────────────────────────────────────────────────────── */
|
||||
QTableView, QTableWidget {
|
||||
background-color: #eff1f5;
|
||||
alternate-background-color: #e6e9ef;
|
||||
gridline-color: #ccd0da;
|
||||
color: #4c4f69;
|
||||
border: none;
|
||||
selection-background-color: #ccd0da;
|
||||
selection-color: #4c4f69;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
QTableView::item, QTableWidget::item {
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
QHeaderView {
|
||||
background-color: #e6e9ef;
|
||||
}
|
||||
|
||||
QHeaderView::section {
|
||||
background-color: #e6e9ef;
|
||||
color: #1e66f5;
|
||||
border: none;
|
||||
border-right: 1px solid #ccd0da;
|
||||
border-bottom: 1px solid #ccd0da;
|
||||
padding: 4px 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
QHeaderView::section:hover {
|
||||
background-color: #d0d4e0;
|
||||
}
|
||||
|
||||
/* ── Plain text edit (SQL editor body) ───────────────────────────────────── */
|
||||
QPlainTextEdit {
|
||||
background-color: #eff1f5;
|
||||
color: #4c4f69;
|
||||
border: none;
|
||||
selection-background-color: #bcc0cc;
|
||||
font-family: "Consolas", "JetBrains Mono", "Courier New", monospace;
|
||||
font-size: 13pt;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Line number gutter ──────────────────────────────────────────────────── */
|
||||
LineNumberArea {
|
||||
background-color: #dce0e8;
|
||||
}
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────────────────── */
|
||||
QPushButton {
|
||||
background-color: #ccd0da;
|
||||
color: #4c4f69;
|
||||
border: 1px solid #bcc0cc;
|
||||
border-radius: 6px;
|
||||
padding: 5px 14px;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #bcc0cc;
|
||||
border-color: #acb0be;
|
||||
}
|
||||
|
||||
QPushButton:pressed {
|
||||
background-color: #d0d4de;
|
||||
}
|
||||
|
||||
QPushButton:disabled {
|
||||
color: #acb0be;
|
||||
background-color: #d0d4de;
|
||||
border-color: #ccd0da;
|
||||
}
|
||||
|
||||
#runBtn {
|
||||
background-color: #40a02b;
|
||||
color: #eff1f5;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#runBtn:hover {
|
||||
background-color: #179299;
|
||||
}
|
||||
|
||||
#stopBtn {
|
||||
background-color: #d20f39;
|
||||
color: #eff1f5;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#stopBtn:hover {
|
||||
background-color: #e5394b;
|
||||
}
|
||||
|
||||
#commitBtn {
|
||||
background-color: #40a02b;
|
||||
color: #eff1f5;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#rollbackBtn {
|
||||
background-color: #fe640b;
|
||||
color: #eff1f5;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#crudAddBtn {
|
||||
background-color: #ccd0da;
|
||||
color: #40a02b;
|
||||
border: 1px solid #40a02b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#crudAddBtn:hover {
|
||||
background-color: #40a02b;
|
||||
color: #eff1f5;
|
||||
}
|
||||
|
||||
#crudDeleteBtn {
|
||||
background-color: #ccd0da;
|
||||
color: #d20f39;
|
||||
border: 1px solid #d20f39;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#crudDeleteBtn:hover {
|
||||
background-color: #d20f39;
|
||||
color: #eff1f5;
|
||||
}
|
||||
|
||||
/* ── Line edit ───────────────────────────────────────────────────────────── */
|
||||
QLineEdit {
|
||||
background-color: #e6e9ef;
|
||||
color: #4c4f69;
|
||||
border: 1px solid #bcc0cc;
|
||||
border-radius: 6px;
|
||||
padding: 5px 10px;
|
||||
font-size: 10pt;
|
||||
selection-background-color: #1e66f5;
|
||||
selection-color: #eff1f5;
|
||||
}
|
||||
|
||||
QLineEdit:focus {
|
||||
border-color: #1e66f5;
|
||||
}
|
||||
|
||||
QLineEdit::placeholder {
|
||||
color: #acb0be;
|
||||
}
|
||||
|
||||
/* ── Combo box ───────────────────────────────────────────────────────────── */
|
||||
QComboBox {
|
||||
background-color: #e6e9ef;
|
||||
color: #4c4f69;
|
||||
border: 1px solid #bcc0cc;
|
||||
border-radius: 6px;
|
||||
padding: 5px 10px;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
QComboBox:focus {
|
||||
border-color: #1e66f5;
|
||||
}
|
||||
|
||||
QComboBox::drop-down {
|
||||
border: none;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #e6e9ef;
|
||||
color: #4c4f69;
|
||||
border: 1px solid #bcc0cc;
|
||||
selection-background-color: #ccd0da;
|
||||
}
|
||||
|
||||
/* ── Spin box ────────────────────────────────────────────────────────────── */
|
||||
QSpinBox {
|
||||
background-color: #e6e9ef;
|
||||
color: #4c4f69;
|
||||
border: 1px solid #bcc0cc;
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
QSpinBox:focus { border-color: #1e66f5; }
|
||||
|
||||
QSpinBox::up-button, QSpinBox::down-button {
|
||||
background-color: #bcc0cc;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
/* ── Check box ───────────────────────────────────────────────────────────── */
|
||||
QCheckBox {
|
||||
color: #4c4f69;
|
||||
spacing: 8px;
|
||||
}
|
||||
|
||||
QCheckBox::indicator {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #bcc0cc;
|
||||
border-radius: 4px;
|
||||
background: #e6e9ef;
|
||||
}
|
||||
|
||||
QCheckBox::indicator:checked {
|
||||
background-color: #1e66f5;
|
||||
border-color: #1e66f5;
|
||||
}
|
||||
|
||||
/* ── Dialog ──────────────────────────────────────────────────────────────── */
|
||||
QDialog {
|
||||
background-color: #eff1f5;
|
||||
}
|
||||
|
||||
QDialogButtonBox QPushButton {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
/* ── Form layout labels ──────────────────────────────────────────────────── */
|
||||
QFormLayout QLabel {
|
||||
color: #6c6f85;
|
||||
}
|
||||
|
||||
/* ── Tab widget in dialogs ───────────────────────────────────────────────── */
|
||||
QTabWidget#dialogTabs::pane {
|
||||
border: 1px solid #ccd0da;
|
||||
border-radius: 6px;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* ── Group box ───────────────────────────────────────────────────────────── */
|
||||
QGroupBox {
|
||||
border: 1px solid #bcc0cc;
|
||||
border-radius: 6px;
|
||||
margin-top: 1em;
|
||||
color: #6c6f85;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 10px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
/* ── Progress bar ────────────────────────────────────────────────────────── */
|
||||
QProgressBar {
|
||||
background-color: #ccd0da;
|
||||
border: 1px solid #bcc0cc;
|
||||
border-radius: 6px;
|
||||
height: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #1e66f5;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* ── Dock widget ─────────────────────────────────────────────────────────── */
|
||||
QDockWidget {
|
||||
color: #4c4f69;
|
||||
titlebar-close-icon: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
QDockWidget::title {
|
||||
background-color: #e6e9ef;
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid #ccd0da;
|
||||
}
|
||||
|
||||
/* ── Tool button ─────────────────────────────────────────────────────────── */
|
||||
QToolButton {
|
||||
background-color: #ccd0da;
|
||||
color: #4c4f69;
|
||||
border: 1px solid #bcc0cc;
|
||||
border-radius: 5px;
|
||||
padding: 4px 8px;
|
||||
font-size: 13pt;
|
||||
}
|
||||
|
||||
QToolButton:hover {
|
||||
background-color: #bcc0cc;
|
||||
}
|
||||
|
||||
/* ── Message box ─────────────────────────────────────────────────────────── */
|
||||
QMessageBox {
|
||||
background-color: #eff1f5;
|
||||
}
|
||||
|
||||
QMessageBox QLabel {
|
||||
color: #4c4f69;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
/* ── Empty workspace label ───────────────────────────────────────────────── */
|
||||
#emptyLabel {
|
||||
color: #acb0be;
|
||||
font-size: 14pt;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
/* ── Status label in results toolbar ────────────────────────────────────── */
|
||||
#statusLabel {
|
||||
color: #6c6f85;
|
||||
font-size: 9pt;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
/* ── DB label in SQL editor toolbar ─────────────────────────────────────── */
|
||||
#dbLabel {
|
||||
color: #9ca0b0;
|
||||
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: #ccd0da;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* ── Structure view title ────────────────────────────────────────────────── */
|
||||
#structureTitle {
|
||||
color: #7287fd;
|
||||
}
|
||||
|
||||
/* ── Pagination bar ──────────────────────────────────────────────────────── */
|
||||
#paginationBar {
|
||||
background-color: #e6e9ef;
|
||||
border-top: 1px solid #bcc0cc;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
#pgBtn {
|
||||
background-color: #d0d4de;
|
||||
color: #4c4f69;
|
||||
border: 1px solid #bcc0cc;
|
||||
border-radius: 5px;
|
||||
padding: 4px 10px;
|
||||
font-size: 9pt;
|
||||
min-width: 52px;
|
||||
}
|
||||
|
||||
#pgBtn:hover {
|
||||
background-color: #ccd0da;
|
||||
border-color: #acb0be;
|
||||
}
|
||||
|
||||
#pgBtn:disabled {
|
||||
color: #bcc0cc;
|
||||
background-color: #eff1f5;
|
||||
border-color: #ccd0da;
|
||||
}
|
||||
|
||||
#pageLbl {
|
||||
color: #1e66f5;
|
||||
font-size: 9pt;
|
||||
font-weight: 600;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
#rowCountLbl {
|
||||
color: #6c6f85;
|
||||
font-size: 9pt;
|
||||
padding-left: 8px;
|
||||
}
|
||||
Reference in New Issue
Block a user