feat: cyberpunk "Matrix 2026" UI redesign
Replace Catppuccin theme with single dark cyberpunk palette featuring neon green accents, monospace fonts, and void-dark backgrounds. Remove light theme toggle entirely. Fix hardcoded inline colors across all views to use centralized palette constants. Fix table column widths in transactions, recurring, settings, and CSV mappings views. Add design system documentation for future development. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,52 +37,28 @@ from src.models.transaction import Transaction
|
||||
from src.services.analysis import AnalysisService
|
||||
from src.services.forecasting import ForecastingService
|
||||
from src.services.recurring import RecurringDetector
|
||||
from src.ui.themes import (
|
||||
VOID, SURFACE, TEXT, TEXT_BRIGHT, TEXT_DIM, BORDER, NEON_GREEN,
|
||||
CHART_COLORS, TAG_COLORS, chart_color,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dark-theme constants
|
||||
# Matplotlib theme helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
BG_COLOR = "#1e1e2e"
|
||||
TEXT_COLOR = "#cdd6f4"
|
||||
GRID_COLOR = "#45475a"
|
||||
|
||||
# Category color palette — consistent across all charts
|
||||
CATEGORY_COLORS = [
|
||||
"#f38ba8", # red
|
||||
"#fab387", # peach
|
||||
"#f9e2af", # yellow
|
||||
"#a6e3a1", # green
|
||||
"#94e2d5", # teal
|
||||
"#89b4fa", # blue
|
||||
"#b4befe", # lavender
|
||||
"#cba6f7", # mauve
|
||||
"#f5c2e7", # pink
|
||||
"#eba0ac", # maroon
|
||||
"#89dceb", # sky
|
||||
"#74c7ec", # sapphire
|
||||
"#f2cdcd", # flamingo
|
||||
"#f5e0dc", # rosewater
|
||||
]
|
||||
|
||||
TAG_COLORS = {
|
||||
"need": "#a6e3a1",
|
||||
"want": "#f38ba8",
|
||||
"savings": "#89b4fa",
|
||||
}
|
||||
|
||||
|
||||
def _color_for(index: int) -> str:
|
||||
return CATEGORY_COLORS[index % len(CATEGORY_COLORS)]
|
||||
BG_COLOR = VOID
|
||||
TEXT_COLOR = TEXT
|
||||
GRID_COLOR = BORDER
|
||||
|
||||
|
||||
def _style_ax(ax):
|
||||
"""Apply dark theme to a matplotlib Axes."""
|
||||
"""Apply cyberpunk theme to a matplotlib Axes."""
|
||||
ax.set_facecolor(BG_COLOR)
|
||||
ax.tick_params(colors=TEXT_COLOR, which="both")
|
||||
for spine in ax.spines.values():
|
||||
spine.set_color(GRID_COLOR)
|
||||
ax.xaxis.label.set_color(TEXT_COLOR)
|
||||
ax.yaxis.label.set_color(TEXT_COLOR)
|
||||
ax.title.set_color(TEXT_COLOR)
|
||||
ax.title.set_color(TEXT_BRIGHT)
|
||||
|
||||
|
||||
def _style_figure(fig: Figure):
|
||||
@@ -150,7 +126,7 @@ class _FilterBar(QWidget):
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# Tab 1 — Spending Over Time
|
||||
# Tab 1 -- Spending Over Time
|
||||
# ======================================================================
|
||||
class _SpendingOverTimeTab(QWidget):
|
||||
def __init__(self, session: Session, parent=None):
|
||||
@@ -205,9 +181,6 @@ class _SpendingOverTimeTab(QWidget):
|
||||
filters = self._filters.filters()
|
||||
period = self._current_period()
|
||||
|
||||
# We need per-category-per-period data for stacked bars.
|
||||
# Build it via raw query because AnalysisService.spending_by_period
|
||||
# returns only flat totals.
|
||||
start = filters.pop("start", None)
|
||||
end = filters.pop("end", None)
|
||||
|
||||
@@ -243,11 +216,10 @@ class _SpendingOverTimeTab(QWidget):
|
||||
if row.period not in seen_periods:
|
||||
periods_set.append(row.period)
|
||||
seen_periods.add(row.period)
|
||||
# Use absolute value for chart readability
|
||||
cat_data[row.category][row.period] = abs(float(row.total))
|
||||
|
||||
categories = sorted(cat_data.keys())
|
||||
cat_color_map = {cat: _color_for(i) for i, cat in enumerate(categories)}
|
||||
cat_color_map = {cat: chart_color(i) for i, cat in enumerate(categories)}
|
||||
|
||||
# Draw
|
||||
self._fig.clear()
|
||||
@@ -279,7 +251,7 @@ class _SpendingOverTimeTab(QWidget):
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# Tab 2 — Category Breakdown
|
||||
# Tab 2 -- Category Breakdown
|
||||
# ======================================================================
|
||||
class _CategoryBreakdownTab(QWidget):
|
||||
def __init__(self, session: Session, parent=None):
|
||||
@@ -293,7 +265,7 @@ class _CategoryBreakdownTab(QWidget):
|
||||
self._filters = _FilterBar(session)
|
||||
root.addWidget(self._filters)
|
||||
|
||||
# Canvas — two subplots side-by-side, plus a third row for tag bar
|
||||
# Canvas
|
||||
self._fig = Figure(tight_layout=True)
|
||||
_style_figure(self._fig)
|
||||
self._canvas = FigureCanvasQTAgg(self._fig)
|
||||
@@ -329,7 +301,7 @@ class _CategoryBreakdownTab(QWidget):
|
||||
if cat_data:
|
||||
labels = [d["category"] for d in cat_data]
|
||||
sizes = [d["total"] for d in cat_data]
|
||||
colors = [_color_for(i) for i in range(len(labels))]
|
||||
colors = [chart_color(i) for i in range(len(labels))]
|
||||
wedges, texts, autotexts = ax_donut.pie(
|
||||
sizes,
|
||||
labels=None,
|
||||
@@ -341,7 +313,6 @@ class _CategoryBreakdownTab(QWidget):
|
||||
for t in autotexts:
|
||||
t.set_color(TEXT_COLOR)
|
||||
t.set_fontsize(7)
|
||||
# White center -> donut
|
||||
centre_circle = ax_donut.add_artist(
|
||||
Circle((0, 0), 0.55, fc=BG_COLOR)
|
||||
)
|
||||
@@ -358,7 +329,7 @@ class _CategoryBreakdownTab(QWidget):
|
||||
sorted_cats = sorted(cat_data, key=lambda d: d["total"])
|
||||
labels_bar = [d["category"] for d in sorted_cats]
|
||||
values_bar = [d["total"] for d in sorted_cats]
|
||||
colors_bar = [_color_for(i) for i, _ in enumerate(sorted_cats)]
|
||||
colors_bar = [chart_color(i) for i, _ in enumerate(sorted_cats)]
|
||||
ax_bar.barh(labels_bar, values_bar, color=colors_bar)
|
||||
ax_bar.set_xlabel("Spending ($)")
|
||||
ax_bar.set_title("Categories Ranked", fontsize=10)
|
||||
@@ -404,7 +375,7 @@ class _CategoryBreakdownTab(QWidget):
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# Tab 3 — Forecasting
|
||||
# Tab 3 -- Forecasting
|
||||
# ======================================================================
|
||||
class _ForecastingTab(QWidget):
|
||||
def __init__(self, session: Session, parent=None):
|
||||
@@ -416,9 +387,9 @@ class _ForecastingTab(QWidget):
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
|
||||
# What-if controls — scrollable list of recurring-charge checkboxes
|
||||
# What-if controls
|
||||
what_if_label = QLabel("What-if: toggle recurring charges")
|
||||
what_if_label.setStyleSheet(f"color: {TEXT_COLOR}; font-weight: bold;")
|
||||
what_if_label.setStyleSheet(f"color: {TEXT_BRIGHT}; font-weight: bold;")
|
||||
root.addWidget(what_if_label)
|
||||
|
||||
scroll = QScrollArea()
|
||||
@@ -442,10 +413,10 @@ class _ForecastingTab(QWidget):
|
||||
|
||||
if not recurring:
|
||||
no_data = QLabel("No recurring charges detected.")
|
||||
no_data.setStyleSheet(f"color: {TEXT_COLOR};")
|
||||
no_data.setStyleSheet(f"color: {TEXT_DIM};")
|
||||
self._checkbox_layout.addWidget(no_data)
|
||||
|
||||
# Canvas — month-ahead (left) and year-ahead (right)
|
||||
# Canvas
|
||||
self._fig = Figure(tight_layout=True)
|
||||
_style_figure(self._fig)
|
||||
self._canvas = FigureCanvasQTAgg(self._fig)
|
||||
@@ -474,7 +445,6 @@ class _ForecastingTab(QWidget):
|
||||
actual_months: list[dict] = []
|
||||
for offset in [3, 2, 1]:
|
||||
m_start = (today.replace(day=1) - datetime.timedelta(days=offset * 30)).replace(day=1)
|
||||
# End of that month
|
||||
if m_start.month == 12:
|
||||
m_end = m_start.replace(year=m_start.year + 1, month=1, day=1) - datetime.timedelta(days=1)
|
||||
else:
|
||||
@@ -490,11 +460,11 @@ class _ForecastingTab(QWidget):
|
||||
for f in month_forecast:
|
||||
all_cats.add(f["category"])
|
||||
categories = sorted(all_cats)
|
||||
cat_color_map = {cat: _color_for(i) for i, cat in enumerate(categories)}
|
||||
cat_color_map = {cat: chart_color(i) for i, cat in enumerate(categories)}
|
||||
|
||||
self._fig.clear()
|
||||
|
||||
# ---- Month-ahead (left) — stacked bars: last 3 months + forecast
|
||||
# ---- Month-ahead (left)
|
||||
ax_month = self._fig.add_subplot(1, 2, 1)
|
||||
_style_ax(ax_month)
|
||||
|
||||
@@ -519,7 +489,7 @@ class _ForecastingTab(QWidget):
|
||||
ax_month.legend(fontsize=6, loc="upper left", framealpha=0.6,
|
||||
facecolor=BG_COLOR, edgecolor=GRID_COLOR, labelcolor=TEXT_COLOR)
|
||||
|
||||
# ---- Year-ahead (right) — simple bar chart per category
|
||||
# ---- Year-ahead (right)
|
||||
ax_year = self._fig.add_subplot(1, 2, 2)
|
||||
_style_ax(ax_year)
|
||||
|
||||
@@ -527,7 +497,7 @@ class _ForecastingTab(QWidget):
|
||||
sorted_yr = sorted(year_forecast, key=lambda f: abs(f["projected"]))
|
||||
yr_cats = [f["category"] for f in sorted_yr]
|
||||
yr_vals = [abs(f["projected"]) for f in sorted_yr]
|
||||
yr_colors = [cat_color_map.get(c, _color_for(i)) for i, c in enumerate(yr_cats)]
|
||||
yr_colors = [cat_color_map.get(c, chart_color(i)) for i, c in enumerate(yr_cats)]
|
||||
ax_year.barh(yr_cats, yr_vals, color=yr_colors)
|
||||
ax_year.set_xlabel("Projected Annual ($)")
|
||||
ax_year.set_title("Year-Ahead Projection", fontsize=10)
|
||||
|
||||
@@ -32,6 +32,9 @@ from src.models.csv_mapping import CsvMapping
|
||||
from src.models.household import HouseholdMember
|
||||
from src.services.csv_reader import detect_format
|
||||
from src.services.importer import ImportService
|
||||
from src.ui.themes import (
|
||||
SURFACE, BORDER, NEON_CYAN, TEXT, TEXT_DIM, TEXT_BRIGHT,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -50,15 +53,15 @@ class _DropZone(QFrame):
|
||||
self.setMinimumSize(400, 200)
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self.setStyleSheet(
|
||||
"#drop-zone {"
|
||||
" border: 2px dashed #aaa;"
|
||||
" border-radius: 12px;"
|
||||
" background: #f9f9f9;"
|
||||
"}"
|
||||
"#drop-zone[dragOver='true'] {"
|
||||
" border-color: #4a90d9;"
|
||||
" background: #eaf2fd;"
|
||||
"}"
|
||||
f"#drop-zone {{"
|
||||
f" border: 2px dashed {BORDER};"
|
||||
f" border-radius: 12px;"
|
||||
f" background: {SURFACE};"
|
||||
f"}}"
|
||||
f"#drop-zone[dragOver='true'] {{"
|
||||
f" border-color: {NEON_CYAN};"
|
||||
f" background: rgba(0,229,255,0.06);"
|
||||
f"}}"
|
||||
)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
@@ -66,12 +69,12 @@ class _DropZone(QFrame):
|
||||
|
||||
icon_label = QLabel("Drag & Drop CSV File Here")
|
||||
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
icon_label.setStyleSheet("font-size: 18px; color: #666;")
|
||||
icon_label.setStyleSheet(f"font-size: 18px; color: {TEXT_DIM};")
|
||||
layout.addWidget(icon_label)
|
||||
|
||||
or_label = QLabel("or")
|
||||
or_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
or_label.setStyleSheet("color: #999;")
|
||||
or_label.setStyleSheet(f"color: {TEXT_DIM};")
|
||||
layout.addWidget(or_label)
|
||||
|
||||
browse_btn = QPushButton("Browse...")
|
||||
@@ -81,7 +84,7 @@ class _DropZone(QFrame):
|
||||
|
||||
self._file_label = QLabel("")
|
||||
self._file_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self._file_label.setStyleSheet("color: #333; font-weight: bold; margin-top: 8px;")
|
||||
self._file_label.setStyleSheet(f"color: {TEXT_BRIGHT}; font-weight: bold; margin-top: 8px;")
|
||||
layout.addWidget(self._file_label)
|
||||
|
||||
# -- drag/drop support --------------------------------------------------
|
||||
@@ -142,7 +145,7 @@ class _FileSelectionPage(QWidget):
|
||||
|
||||
title = QLabel("Import Transactions")
|
||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
title.setStyleSheet("font-size: 24px; font-weight: bold; margin-bottom: 16px;")
|
||||
title.setStyleSheet(f"font-size: 24px; font-weight: bold; color: {TEXT_BRIGHT}; margin-bottom: 16px;")
|
||||
layout.addWidget(title)
|
||||
|
||||
self._drop_zone = _DropZone()
|
||||
@@ -224,7 +227,7 @@ class _ColumnMappingPage(QWidget):
|
||||
|
||||
# --- preview table ---
|
||||
preview_label = QLabel("CSV Preview (first 5 rows)")
|
||||
preview_label.setStyleSheet("font-weight: bold;")
|
||||
preview_label.setStyleSheet(f"font-weight: bold; color: {TEXT_BRIGHT};")
|
||||
outer.addWidget(preview_label)
|
||||
|
||||
self._table = QTableWidget()
|
||||
@@ -234,7 +237,7 @@ class _ColumnMappingPage(QWidget):
|
||||
|
||||
# --- column mapping combos row ---
|
||||
mapping_label = QLabel("Map each column to a target field:")
|
||||
mapping_label.setStyleSheet("font-weight: bold; margin-top: 8px;")
|
||||
mapping_label.setStyleSheet(f"font-weight: bold; color: {TEXT_BRIGHT}; margin-top: 8px;")
|
||||
outer.addWidget(mapping_label)
|
||||
|
||||
self._mapping_row = QHBoxLayout()
|
||||
@@ -351,7 +354,7 @@ class _ColumnMappingPage(QWidget):
|
||||
vbox = QVBoxLayout()
|
||||
lbl = QLabel(col_name)
|
||||
lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
lbl.setStyleSheet("font-size: 11px;")
|
||||
lbl.setStyleSheet(f"font-size: 11px; color: {TEXT_DIM};")
|
||||
vbox.addWidget(lbl)
|
||||
vbox.addWidget(combo)
|
||||
self._mapping_row.addLayout(vbox)
|
||||
@@ -561,7 +564,7 @@ class _ImportResultsPage(QWidget):
|
||||
title = QLabel("Importing...")
|
||||
title.setObjectName("results-title")
|
||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
title.setStyleSheet("font-size: 22px; font-weight: bold; margin-bottom: 12px;")
|
||||
title.setStyleSheet(f"font-size: 22px; font-weight: bold; color: {TEXT_BRIGHT}; margin-bottom: 12px;")
|
||||
layout.addWidget(title)
|
||||
self._title = title
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtWidgets import QMainWindow, QHBoxLayout, QWidget, QStackedWidget, QLabel
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QMainWindow, QHBoxLayout, QWidget, QStackedWidget
|
||||
from src.ui.sidebar import Sidebar
|
||||
from src.ui.import_view import ImportView
|
||||
from src.ui.transactions_view import TransactionsView
|
||||
from src.ui.analysis_view import AnalysisView
|
||||
from src.ui.recurring_view import RecurringView
|
||||
from src.ui.settings_view import SettingsView
|
||||
from src.ui.themes import get_stylesheet
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
@@ -29,7 +27,7 @@ class MainWindow(QMainWindow):
|
||||
self.stack = QStackedWidget()
|
||||
layout.addWidget(self.stack)
|
||||
|
||||
# Build views — real implementations where available, placeholders otherwise
|
||||
# Build views
|
||||
self._views = {}
|
||||
self._import_view = ImportView(session)
|
||||
self._transactions_view = TransactionsView(session)
|
||||
@@ -47,28 +45,17 @@ class MainWindow(QMainWindow):
|
||||
self._views[key] = self.stack.addWidget(self._recurring_view)
|
||||
elif key == "settings":
|
||||
self._views[key] = self.stack.addWidget(self._settings_view)
|
||||
else:
|
||||
placeholder = QLabel(f"{label} View")
|
||||
placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
placeholder.setStyleSheet("font-size: 24px; color: #888;")
|
||||
self._views[key] = self.stack.addWidget(placeholder)
|
||||
|
||||
# Wire up ImportView signals
|
||||
self._import_view.import_complete.connect(self._transactions_view.refresh)
|
||||
self._import_view.navigate_to.connect(self._switch_view)
|
||||
|
||||
self.sidebar.view_changed.connect(self._switch_view)
|
||||
self.sidebar.theme_toggled.connect(self._apply_theme)
|
||||
|
||||
# Apply dark theme by default
|
||||
self._apply_theme("dark")
|
||||
# Apply cyberpunk theme
|
||||
self.setStyleSheet(get_stylesheet())
|
||||
|
||||
def _switch_view(self, key: str):
|
||||
self.stack.setCurrentIndex(self._views[key])
|
||||
if key == "settings":
|
||||
self._settings_view.refresh()
|
||||
|
||||
def _apply_theme(self, theme: str):
|
||||
qss_path = Path(__file__).parent / "themes" / f"{theme}.qss"
|
||||
if qss_path.exists():
|
||||
self.setStyleSheet(qss_path.read_text(encoding="utf-8"))
|
||||
|
||||
@@ -6,6 +6,7 @@ from PySide6.QtCore import Qt
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.services.recurring import RecurringDetector
|
||||
from src.ui.themes import TEXT_DIM, TEXT_BRIGHT, NEON_GREEN
|
||||
|
||||
|
||||
class RecurringView(QWidget):
|
||||
@@ -24,7 +25,7 @@ class RecurringView(QWidget):
|
||||
self._build_ui()
|
||||
self._run_scan()
|
||||
|
||||
# ── UI construction ──────────────────────────────────────────────
|
||||
# -- UI construction ----------------------------------------------------
|
||||
|
||||
def _build_ui(self):
|
||||
root = QVBoxLayout(self)
|
||||
@@ -58,9 +59,19 @@ class RecurringView(QWidget):
|
||||
"", "",
|
||||
])
|
||||
header = self.table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||
header.setSectionResizeMode(6, QHeaderView.ResizeMode.Fixed)
|
||||
header.setSectionResizeMode(7, QHeaderView.ResizeMode.Fixed)
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) # Description
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) # Amount
|
||||
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed) # Frequency
|
||||
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) # Annual Cost
|
||||
header.setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed) # Last Date
|
||||
header.setSectionResizeMode(5, QHeaderView.ResizeMode.Fixed) # Status
|
||||
header.setSectionResizeMode(6, QHeaderView.ResizeMode.Fixed) # Confirm btn
|
||||
header.setSectionResizeMode(7, QHeaderView.ResizeMode.Fixed) # Dismiss btn
|
||||
self.table.setColumnWidth(1, 100)
|
||||
self.table.setColumnWidth(2, 100)
|
||||
self.table.setColumnWidth(3, 110)
|
||||
self.table.setColumnWidth(4, 100)
|
||||
self.table.setColumnWidth(5, 90)
|
||||
self.table.setColumnWidth(6, 100)
|
||||
self.table.setColumnWidth(7, 100)
|
||||
self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
@@ -77,17 +88,17 @@ class RecurringView(QWidget):
|
||||
layout.setContentsMargins(16, 12, 16, 12)
|
||||
|
||||
title_lbl = QLabel(title)
|
||||
title_lbl.setStyleSheet("font-size: 12px; color: #888;")
|
||||
title_lbl.setStyleSheet(f"font-size: 12px; color: {TEXT_DIM};")
|
||||
layout.addWidget(title_lbl)
|
||||
|
||||
value_lbl = QLabel(value)
|
||||
value_lbl.setObjectName("card_value")
|
||||
value_lbl.setStyleSheet("font-size: 22px; font-weight: bold;")
|
||||
value_lbl.setStyleSheet(f"font-size: 22px; font-weight: bold; color: {NEON_GREEN};")
|
||||
layout.addWidget(value_lbl)
|
||||
|
||||
return frame
|
||||
|
||||
# ── scanning / data ──────────────────────────────────────────────
|
||||
# -- scanning / data ----------------------------------------------------
|
||||
|
||||
def _run_scan(self):
|
||||
"""Run the recurring-charge detector and refresh the table."""
|
||||
|
||||
@@ -157,7 +157,13 @@ class SettingsView(QWidget):
|
||||
|
||||
self.rule_table = QTableWidget(0, 5)
|
||||
self.rule_table.setHorizontalHeaderLabels(["Pattern", "Category", "Tag Override", "Person", "Priority"])
|
||||
self.rule_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||
header = self.rule_table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) # Pattern
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # Category
|
||||
header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) # Tag Override
|
||||
header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) # Person
|
||||
header.setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed) # Priority
|
||||
self.rule_table.setColumnWidth(4, 70)
|
||||
self.rule_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
self.rule_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.rule_table.verticalHeader().setVisible(False)
|
||||
@@ -363,7 +369,15 @@ class SettingsView(QWidget):
|
||||
|
||||
self.acct_table = QTableWidget(0, 5)
|
||||
self.acct_table.setHorizontalHeaderLabels(["Name", "Institution", "Type", "Owner", "Shared"])
|
||||
self.acct_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||
header = self.acct_table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) # Name
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) # Institution
|
||||
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed) # Type
|
||||
header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) # Owner
|
||||
header.setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed) # Shared
|
||||
self.acct_table.setColumnWidth(1, 150)
|
||||
self.acct_table.setColumnWidth(2, 100)
|
||||
self.acct_table.setColumnWidth(4, 60)
|
||||
self.acct_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
self.acct_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.acct_table.verticalHeader().setVisible(False)
|
||||
@@ -449,7 +463,11 @@ class SettingsView(QWidget):
|
||||
|
||||
self.csv_table = QTableWidget(0, 3)
|
||||
self.csv_table.setHorizontalHeaderLabels(["Name", "Account", "Fingerprint"])
|
||||
self.csv_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||
header = self.csv_table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) # Name
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # Account
|
||||
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) # Fingerprint
|
||||
self.csv_table.setTextElideMode(Qt.TextElideMode.ElideMiddle)
|
||||
self.csv_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
self.csv_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.csv_table.verticalHeader().setVisible(False)
|
||||
|
||||
@@ -4,7 +4,6 @@ from PySide6.QtCore import Signal
|
||||
|
||||
class Sidebar(QFrame):
|
||||
view_changed = Signal(str)
|
||||
theme_toggled = Signal(str)
|
||||
|
||||
VIEWS = [
|
||||
("Import", "import"),
|
||||
@@ -32,24 +31,9 @@ class Sidebar(QFrame):
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
self._current_theme = "dark"
|
||||
self._theme_btn = QPushButton("Light Theme")
|
||||
self._theme_btn.setObjectName("theme-toggle")
|
||||
self._theme_btn.clicked.connect(self._on_theme_toggle)
|
||||
layout.addWidget(self._theme_btn)
|
||||
|
||||
self._buttons["import"].setChecked(True)
|
||||
|
||||
def _on_click(self, key: str):
|
||||
for k, btn in self._buttons.items():
|
||||
btn.setChecked(k == key)
|
||||
self.view_changed.emit(key)
|
||||
|
||||
def _on_theme_toggle(self):
|
||||
if self._current_theme == "dark":
|
||||
self._current_theme = "light"
|
||||
self._theme_btn.setText("Dark Theme")
|
||||
else:
|
||||
self._current_theme = "dark"
|
||||
self._theme_btn.setText("Light Theme")
|
||||
self.theme_toggled.emit(self._current_theme)
|
||||
|
||||
196
src/ui/themes/DESIGN_SYSTEM.md
Normal file
196
src/ui/themes/DESIGN_SYSTEM.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# Design System: "Matrix 2026" Cyberpunk Theme
|
||||
|
||||
Single dark cyberpunk theme for SpendingAnalysis. No light theme.
|
||||
|
||||
---
|
||||
|
||||
## 1. Color Palette
|
||||
|
||||
### Backgrounds
|
||||
| Name | Hex | Usage |
|
||||
|------------------|-------------|---------------------------------------------|
|
||||
| `VOID` | `#0a0a0f` | Deepest background -- main window, body |
|
||||
| `SURFACE` | `#0d1117` | Panels, cards, table body, sidebar |
|
||||
| `SURFACE_BRIGHT` | `#141b24` | Elevated: table headers, tab bar, hover |
|
||||
| `BORDER` | `#1a2332` | Subtle structural borders |
|
||||
| `BORDER_GLOW` | `#00ff4130` | Neon green border glow (19% opacity) |
|
||||
|
||||
### Text
|
||||
| Name | Hex | Usage |
|
||||
|---------------|-------------|---------------------------------------|
|
||||
| `TEXT` | `#b8c4d4` | Primary body text |
|
||||
| `TEXT_DIM` | `#4a5568` | Muted/secondary labels |
|
||||
| `TEXT_BRIGHT` | `#e8edf5` | Emphasized headings, active items |
|
||||
|
||||
### Accents
|
||||
| Name | Hex | Usage |
|
||||
|----------------|-------------|--------------------------------------|
|
||||
| `NEON_GREEN` | `#00ff41` | Primary accent -- Matrix phosphor |
|
||||
| `NEON_CYAN` | `#00e5ff` | Secondary accent -- links, info |
|
||||
| `NEON_MAGENTA` | `#ff2d6f` | Danger, expenses, alerts |
|
||||
| `NEON_AMBER` | `#ffb627` | Warnings, pending status |
|
||||
|
||||
### Semantic
|
||||
| Name | Hex | Usage |
|
||||
|-----------|-------------|-------------------|
|
||||
| `INCOME` | `#39ff14` | Positive amounts |
|
||||
| `EXPENSE` | `#ff2d6f` | Negative amounts |
|
||||
|
||||
### Glows (for inline styles)
|
||||
| Name | Value | Usage |
|
||||
|-----------------|-----------------------------|---------------|
|
||||
| `GLOW_GREEN` | `rgba(0,255,65,0.08)` | Active states |
|
||||
| `GLOW_CYAN` | `rgba(0,229,255,0.06)` | Hover states |
|
||||
| `GLOW_MAGENTA` | `rgba(255,45,111,0.08)` | Danger states |
|
||||
|
||||
---
|
||||
|
||||
## 2. Typography
|
||||
|
||||
### Font Stacks
|
||||
|
||||
- **Monospace (primary)**: `"JetBrains Mono", "Cascadia Code", "Consolas", monospace`
|
||||
Used for all body text, data tables, and UI elements. Monospace ensures financial data aligns cleanly.
|
||||
|
||||
- **Display**: `"Orbitron", "Share Tech", "JetBrains Mono", monospace`
|
||||
Reserved for future use in large headings or splash screens.
|
||||
|
||||
### Sizes
|
||||
| Element | Size |
|
||||
|-----------------|--------|
|
||||
| Base UI | 13px |
|
||||
| Table buttons | 12px |
|
||||
| Card titles | 12px |
|
||||
| Card values | 22px |
|
||||
| Page headings | 22-24px|
|
||||
| Column labels | 11px |
|
||||
|
||||
---
|
||||
|
||||
## 3. Component Patterns
|
||||
|
||||
### Tables
|
||||
- Background: `SURFACE`
|
||||
- Gridlines: `BORDER`
|
||||
- Header: `SURFACE_BRIGHT` background, `TEXT_BRIGHT` text, `NEON_GREEN` bottom border
|
||||
- Row hover: `GLOW_CYAN` tint
|
||||
- Selection: `GLOW_GREEN` with `TEXT_BRIGHT`
|
||||
- Alternate rows: `#0f1419`
|
||||
|
||||
Column sizing strategy:
|
||||
- **Fixed**: Date (~100px), Amount (~110px), Tag (~80px), Priority (~70px), Shared (~60px)
|
||||
- **Stretch**: Description, Pattern, Name
|
||||
- **ResizeToContents**: Account, Category, Person, Owner
|
||||
|
||||
### Buttons
|
||||
- Default: transparent bg, `NEON_GREEN` border + text
|
||||
- Hover: `GLOW_GREEN` fill, `TEXT_BRIGHT` text
|
||||
- Pressed: brighter glow
|
||||
- Table buttons: same style, smaller (3px 10px padding, 12px font)
|
||||
|
||||
### Cards
|
||||
- Frame: `SURFACE` bg, `BORDER` outline (via QFrame StyledPanel)
|
||||
- Title: `TEXT_DIM`, 12px
|
||||
- Value: `NEON_GREEN`, 22px bold
|
||||
|
||||
### Inputs (QLineEdit, QComboBox, QDateEdit)
|
||||
- Background: `SURFACE`
|
||||
- Border: `BORDER`, 4px radius
|
||||
- Focus: `NEON_GREEN` border
|
||||
|
||||
### Tabs
|
||||
- Inactive: `SURFACE_BRIGHT` bg, `TEXT` color
|
||||
- Active: `SURFACE` bg, `NEON_GREEN` text, green bottom border
|
||||
- Hover: `BORDER` bg, `TEXT_BRIGHT` text
|
||||
|
||||
### Sidebar
|
||||
- Background: `SURFACE`
|
||||
- Nav items: uppercase, letter-spacing 1px, bold
|
||||
- Active: left green border, `NEON_GREEN` text, `GLOW_GREEN` bg
|
||||
- Hover: `GLOW_CYAN` bg
|
||||
|
||||
---
|
||||
|
||||
## 4. Chart Styling (Matplotlib)
|
||||
|
||||
Import from `src.ui.themes`:
|
||||
|
||||
```python
|
||||
from src.ui.themes import (
|
||||
VOID, TEXT, TEXT_BRIGHT, BORDER,
|
||||
CHART_COLORS, TAG_COLORS, chart_color,
|
||||
)
|
||||
|
||||
# Use BG_COLOR = VOID for figure/axes facecolor
|
||||
# Use TEXT for tick labels, axis labels
|
||||
# Use TEXT_BRIGHT for chart titles
|
||||
# Use BORDER for spine color, grid color
|
||||
|
||||
# Category colors:
|
||||
color = chart_color(index) # cycles through CHART_COLORS
|
||||
|
||||
# Tag colors:
|
||||
TAG_COLORS["need"] # INCOME green
|
||||
TAG_COLORS["want"] # EXPENSE magenta
|
||||
TAG_COLORS["savings"] # NEON_CYAN
|
||||
```
|
||||
|
||||
Helper functions:
|
||||
- `_style_ax(ax)` -- sets facecolor, tick colors, spine colors, label colors
|
||||
- `_style_figure(fig)` -- sets figure facecolor
|
||||
|
||||
Legend styling:
|
||||
```python
|
||||
ax.legend(
|
||||
facecolor=BG_COLOR,
|
||||
edgecolor=GRID_COLOR,
|
||||
labelcolor=TEXT_COLOR,
|
||||
framealpha=0.6,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Adding a New View/Tab
|
||||
|
||||
Checklist:
|
||||
|
||||
1. **Import palette constants** at the top of your view file:
|
||||
```python
|
||||
from src.ui.themes import TEXT_DIM, TEXT_BRIGHT, NEON_GREEN, SURFACE, BORDER
|
||||
```
|
||||
|
||||
2. **Never hardcode hex colors** in `setStyleSheet()` calls. Always use f-strings with palette constants:
|
||||
```python
|
||||
label.setStyleSheet(f"color: {TEXT_DIM}; font-size: 12px;")
|
||||
```
|
||||
|
||||
3. **Set column widths explicitly** on any QTableWidget/QTableView:
|
||||
- Use `Stretch` for the primary content column (Description, Name, Pattern)
|
||||
- Use `Fixed` with explicit pixel widths for short data (Date, Amount, Tag, Status)
|
||||
- Use `ResizeToContents` for variable-width data (Account, Person, Category)
|
||||
- Set `setTextElideMode(Qt.TextElideMode.ElideMiddle)` for columns that may overflow
|
||||
|
||||
4. **For matplotlib charts**: use `_style_ax()` and `_style_figure()`, import `chart_color()` for category series.
|
||||
|
||||
5. **QSS handles most styling** (buttons, inputs, scrollbars, etc.) automatically. Only use inline `setStyleSheet()` when you need widget-specific overrides (e.g., font-size on a particular label).
|
||||
|
||||
6. **Cards pattern**: Use `QFrame` with `StyledPanel`, QSS handles the bg/border. Use `TEXT_DIM` for title labels, `NEON_GREEN` for value labels.
|
||||
|
||||
---
|
||||
|
||||
## 6. Do's and Don'ts
|
||||
|
||||
**Do:**
|
||||
- Import colors from `src.ui.themes`
|
||||
- Use f-strings for all inline style color references
|
||||
- Set explicit column widths on tables
|
||||
- Use `chart_color(i)` for matplotlib category colors
|
||||
- Use `TEXT_BRIGHT` for headings, `TEXT_DIM` for secondary labels
|
||||
|
||||
**Don't:**
|
||||
- Hardcode hex colors in Python files (e.g., `"#888"`, `"red"`, `"#f9f9f9"`)
|
||||
- Use `QColor("red")` or `QColor("green")` -- use `QColor(EXPENSE)` / `QColor(INCOME)`
|
||||
- Add a light theme or theme toggle
|
||||
- Use serif or sans-serif fonts -- everything is monospace
|
||||
- Set `stretchLastSection(True)` without explicit column modes for other columns
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Cyberpunk 'Matrix 2026' theme palette — single source of truth for all UI colors and fonts."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Color palette
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Backgrounds
|
||||
VOID = "#0a0a0f" # Deepest background — main window, body
|
||||
SURFACE = "#0d1117" # Panels, cards, table body, sidebar
|
||||
SURFACE_BRIGHT = "#141b24" # Elevated: table headers, tab bar, hover states
|
||||
BORDER = "#1a2332" # Subtle structural borders
|
||||
BORDER_GLOW = "#00ff4130" # Neon green border glow, 19% opacity
|
||||
|
||||
# Text
|
||||
TEXT = "#b8c4d4" # Primary body text
|
||||
TEXT_DIM = "#4a5568" # Muted/secondary labels
|
||||
TEXT_BRIGHT = "#e8edf5" # Emphasized headings, active items
|
||||
|
||||
# Accents
|
||||
NEON_GREEN = "#00ff41" # Primary accent — Matrix phosphor green
|
||||
NEON_CYAN = "#00e5ff" # Secondary accent — links, info
|
||||
NEON_MAGENTA = "#ff2d6f" # Danger, expenses, alerts
|
||||
NEON_AMBER = "#ffb627" # Warnings, pending status
|
||||
|
||||
# Semantic
|
||||
INCOME = "#39ff14" # Positive amounts
|
||||
EXPENSE = "#ff2d6f" # Negative amounts
|
||||
|
||||
# Glow backgrounds (for inline styles — QSS uses rgba() directly)
|
||||
GLOW_GREEN = "rgba(0,255,65,0.08)" # Active states
|
||||
GLOW_CYAN = "rgba(0,229,255,0.06)" # Hover states
|
||||
GLOW_MAGENTA = "rgba(255,45,111,0.08)" # Danger states
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Font stacks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
FONT_MONO = '"JetBrains Mono", "Cascadia Code", "Consolas", monospace'
|
||||
FONT_DISPLAY = '"Orbitron", "Share Tech", "JetBrains Mono", monospace'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Chart colors (for matplotlib category series)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
CHART_COLORS = [
|
||||
"#ff2d6f", # magenta
|
||||
"#ff6b3d", # orange
|
||||
"#ffb627", # amber
|
||||
"#39ff14", # green
|
||||
"#00e5ff", # cyan
|
||||
"#00ff41", # matrix green
|
||||
"#7b61ff", # purple
|
||||
"#c084fc", # lavender
|
||||
"#ff79c6", # pink
|
||||
"#f97583", # salmon
|
||||
"#56d4dd", # teal
|
||||
"#4a9eff", # blue
|
||||
"#f0883e", # peach
|
||||
"#e8edf5", # bright white
|
||||
]
|
||||
|
||||
TAG_COLORS = {
|
||||
"need": INCOME,
|
||||
"want": EXPENSE,
|
||||
"savings": NEON_CYAN,
|
||||
}
|
||||
|
||||
|
||||
def chart_color(index: int) -> str:
|
||||
"""Return a chart color by index, cycling through the palette."""
|
||||
return CHART_COLORS[index % len(CHART_COLORS)]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stylesheet loader
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_stylesheet() -> str:
|
||||
"""Read and return the single cyberpunk QSS stylesheet."""
|
||||
qss_path = Path(__file__).parent / "dark.qss"
|
||||
return qss_path.read_text(encoding="utf-8")
|
||||
|
||||
@@ -1,111 +1,113 @@
|
||||
/* ===== Dark Theme ===== */
|
||||
/* Background: #1e1e2e Surface: #2a2a3c Text: #cdd6f4
|
||||
Accent: #89b4fa Border: #45475a
|
||||
Success/Income: #a6e3a1 Error/Expense: #f38ba8 */
|
||||
/* ===== Cyberpunk "Matrix 2026" Theme ===== */
|
||||
/* Void: #0a0a0f Surface: #0d1117 SurfaceBright: #141b24
|
||||
Text: #b8c4d4 TextDim: #4a5568 TextBright: #e8edf5
|
||||
Border: #1a2332 NeonGreen: #00ff41 NeonCyan: #00e5ff
|
||||
NeonMagenta: #ff2d6f NeonAmber: #ffb627
|
||||
Income: #39ff14 Expense: #ff2d6f */
|
||||
|
||||
/* ---------- Base Widgets ---------- */
|
||||
QMainWindow, QWidget {
|
||||
background-color: #1e1e2e;
|
||||
color: #cdd6f4;
|
||||
font-family: "Segoe UI", "Roboto", sans-serif;
|
||||
font-size: 14px;
|
||||
background-color: #0a0a0f;
|
||||
color: #b8c4d4;
|
||||
font-family: "JetBrains Mono", "Cascadia Code", "Consolas", monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
QLabel {
|
||||
color: #cdd6f4;
|
||||
color: #b8c4d4;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ---------- Sidebar ---------- */
|
||||
#sidebar {
|
||||
background-color: #2a2a3c;
|
||||
border-right: 1px solid #45475a;
|
||||
background-color: #0d1117;
|
||||
border-right: 1px solid #1a2332;
|
||||
}
|
||||
|
||||
#sidebar QPushButton {
|
||||
background-color: transparent;
|
||||
color: #cdd6f4;
|
||||
color: #b8c4d4;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 0px;
|
||||
border-left: 3px solid transparent;
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
margin: 2px 8px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#sidebar QPushButton:hover {
|
||||
background-color: #45475a;
|
||||
background-color: rgba(0,229,255,0.06);
|
||||
color: #e8edf5;
|
||||
border-left: 3px solid #1a2332;
|
||||
}
|
||||
|
||||
#sidebar QPushButton:checked {
|
||||
background-color: #89b4fa;
|
||||
color: #1e1e2e;
|
||||
background-color: rgba(0,255,65,0.08);
|
||||
color: #00ff41;
|
||||
border-left: 3px solid #00ff41;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#sidebar QPushButton#theme-toggle {
|
||||
background-color: #45475a;
|
||||
color: #cdd6f4;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
margin: 8px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#sidebar QPushButton#theme-toggle:hover {
|
||||
background-color: #585b70;
|
||||
}
|
||||
|
||||
/* ---------- QPushButton (general) ---------- */
|
||||
QPushButton {
|
||||
background-color: #89b4fa;
|
||||
color: #1e1e2e;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background-color: transparent;
|
||||
color: #00ff41;
|
||||
border: 1px solid #00ff41;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #74a8fc;
|
||||
background-color: rgba(0,255,65,0.08);
|
||||
color: #e8edf5;
|
||||
}
|
||||
|
||||
QPushButton:pressed {
|
||||
background-color: #5b96f7;
|
||||
background-color: rgba(0,255,65,0.15);
|
||||
color: #e8edf5;
|
||||
}
|
||||
|
||||
QPushButton:disabled {
|
||||
background-color: #45475a;
|
||||
color: #6c7086;
|
||||
background-color: transparent;
|
||||
color: #4a5568;
|
||||
border-color: #1a2332;
|
||||
}
|
||||
|
||||
/* ---------- QPushButton inside tables ---------- */
|
||||
QTableView QPushButton, QTableWidget QPushButton {
|
||||
background-color: #89b4fa;
|
||||
color: #1e1e2e;
|
||||
padding: 2px 12px;
|
||||
color: #00ff41;
|
||||
border: 1px solid #00ff41;
|
||||
background-color: transparent;
|
||||
padding: 3px 10px;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
QTableView QPushButton:hover, QTableWidget QPushButton:hover {
|
||||
background-color: #74a8fc;
|
||||
background-color: rgba(0,255,65,0.08);
|
||||
color: #e8edf5;
|
||||
}
|
||||
|
||||
QTableView QPushButton:pressed, QTableWidget QPushButton:pressed {
|
||||
background-color: #5b96f7;
|
||||
background-color: rgba(0,255,65,0.15);
|
||||
}
|
||||
|
||||
/* ---------- QTableView / QTableWidget ---------- */
|
||||
QTableView, QTableWidget {
|
||||
background-color: #2a2a3c;
|
||||
color: #cdd6f4;
|
||||
gridline-color: #45475a;
|
||||
border: 1px solid #45475a;
|
||||
background-color: #0d1117;
|
||||
color: #b8c4d4;
|
||||
gridline-color: #1a2332;
|
||||
border: 1px solid #1a2332;
|
||||
border-radius: 4px;
|
||||
selection-background-color: #89b4fa;
|
||||
selection-color: #1e1e2e;
|
||||
alternate-background-color: #313244;
|
||||
selection-background-color: rgba(0,255,65,0.15);
|
||||
selection-color: #e8edf5;
|
||||
alternate-background-color: #0f1419;
|
||||
}
|
||||
|
||||
QTableView::item, QTableWidget::item {
|
||||
@@ -113,138 +115,173 @@ QTableView::item, QTableWidget::item {
|
||||
}
|
||||
|
||||
QTableView::item:hover, QTableWidget::item:hover {
|
||||
background-color: #45475a;
|
||||
background-color: rgba(0,229,255,0.06);
|
||||
}
|
||||
|
||||
/* ---------- QHeaderView ---------- */
|
||||
QHeaderView::section {
|
||||
background-color: #313244;
|
||||
color: #cdd6f4;
|
||||
background-color: #141b24;
|
||||
color: #e8edf5;
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
border-bottom: 2px solid #45475a;
|
||||
border-right: 1px solid #45475a;
|
||||
border-bottom: 2px solid #00ff41;
|
||||
border-right: 1px solid #1a2332;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
QHeaderView::section:hover {
|
||||
background-color: #45475a;
|
||||
background-color: #1a2332;
|
||||
}
|
||||
|
||||
/* ---------- QComboBox ---------- */
|
||||
QComboBox {
|
||||
background-color: #2a2a3c;
|
||||
color: #cdd6f4;
|
||||
border: 1px solid #45475a;
|
||||
border-radius: 6px;
|
||||
background-color: #0d1117;
|
||||
color: #b8c4d4;
|
||||
border: 1px solid #1a2332;
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
QComboBox:hover {
|
||||
border-color: #89b4fa;
|
||||
border-color: #00ff41;
|
||||
}
|
||||
|
||||
QComboBox:focus {
|
||||
border-color: #00ff41;
|
||||
}
|
||||
|
||||
QComboBox::drop-down {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: top right;
|
||||
width: 24px;
|
||||
border-left: 1px solid #45475a;
|
||||
border-top-right-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
border-left: 1px solid #1a2332;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #2a2a3c;
|
||||
color: #cdd6f4;
|
||||
border: 1px solid #45475a;
|
||||
selection-background-color: #89b4fa;
|
||||
selection-color: #1e1e2e;
|
||||
background-color: #0d1117;
|
||||
color: #b8c4d4;
|
||||
border: 1px solid #00ff41;
|
||||
selection-background-color: rgba(0,255,65,0.15);
|
||||
selection-color: #e8edf5;
|
||||
}
|
||||
|
||||
/* ---------- QLineEdit ---------- */
|
||||
QLineEdit {
|
||||
background-color: #2a2a3c;
|
||||
color: #cdd6f4;
|
||||
border: 1px solid #45475a;
|
||||
border-radius: 6px;
|
||||
background-color: #0d1117;
|
||||
color: #b8c4d4;
|
||||
border: 1px solid #1a2332;
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
QLineEdit:focus {
|
||||
border-color: #89b4fa;
|
||||
border-color: #00ff41;
|
||||
}
|
||||
|
||||
QLineEdit:disabled {
|
||||
background-color: #313244;
|
||||
color: #6c7086;
|
||||
background-color: #141b24;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
/* ---------- QDateEdit ---------- */
|
||||
QDateEdit {
|
||||
background-color: #2a2a3c;
|
||||
color: #cdd6f4;
|
||||
border: 1px solid #45475a;
|
||||
border-radius: 6px;
|
||||
background-color: #0d1117;
|
||||
color: #b8c4d4;
|
||||
border: 1px solid #1a2332;
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
QDateEdit:focus {
|
||||
border-color: #89b4fa;
|
||||
border-color: #00ff41;
|
||||
}
|
||||
|
||||
QDateEdit::drop-down {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: top right;
|
||||
width: 24px;
|
||||
border-left: 1px solid #45475a;
|
||||
border-left: 1px solid #1a2332;
|
||||
}
|
||||
|
||||
/* ---------- QCalendarWidget ---------- */
|
||||
QCalendarWidget {
|
||||
background-color: #0d1117;
|
||||
color: #b8c4d4;
|
||||
}
|
||||
|
||||
QCalendarWidget QWidget#qt_calendar_navigationbar {
|
||||
background-color: #141b24;
|
||||
}
|
||||
|
||||
QCalendarWidget QToolButton {
|
||||
color: #00ff41;
|
||||
background-color: transparent;
|
||||
border: 1px solid #1a2332;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
QCalendarWidget QToolButton:hover {
|
||||
background-color: rgba(0,255,65,0.08);
|
||||
}
|
||||
|
||||
QCalendarWidget QAbstractItemView {
|
||||
background-color: #0d1117;
|
||||
color: #b8c4d4;
|
||||
selection-background-color: rgba(0,255,65,0.15);
|
||||
selection-color: #e8edf5;
|
||||
}
|
||||
|
||||
/* ---------- QTabWidget / QTabBar ---------- */
|
||||
QTabWidget::pane {
|
||||
background-color: #2a2a3c;
|
||||
border: 1px solid #45475a;
|
||||
background-color: #0d1117;
|
||||
border: 1px solid #1a2332;
|
||||
border-radius: 4px;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
QTabBar::tab {
|
||||
background-color: #313244;
|
||||
color: #cdd6f4;
|
||||
border: 1px solid #45475a;
|
||||
border-bottom: none;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
background-color: #141b24;
|
||||
color: #b8c4d4;
|
||||
border: 1px solid #1a2332;
|
||||
border-bottom: 2px solid transparent;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
QTabBar::tab:hover {
|
||||
background-color: #45475a;
|
||||
background-color: #1a2332;
|
||||
color: #e8edf5;
|
||||
}
|
||||
|
||||
QTabBar::tab:selected {
|
||||
background-color: #89b4fa;
|
||||
color: #1e1e2e;
|
||||
background-color: #0d1117;
|
||||
color: #00ff41;
|
||||
border-bottom: 2px solid #00ff41;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* ---------- QScrollBar (vertical) ---------- */
|
||||
QScrollBar:vertical {
|
||||
background-color: #1e1e2e;
|
||||
width: 10px;
|
||||
background-color: #0a0a0f;
|
||||
width: 8px;
|
||||
margin: 0;
|
||||
border-radius: 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical {
|
||||
background-color: #45475a;
|
||||
background-color: #141b24;
|
||||
min-height: 30px;
|
||||
border-radius: 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background-color: #585b70;
|
||||
background-color: #00ff41;
|
||||
}
|
||||
|
||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
|
||||
@@ -257,20 +294,20 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
||||
|
||||
/* ---------- QScrollBar (horizontal) ---------- */
|
||||
QScrollBar:horizontal {
|
||||
background-color: #1e1e2e;
|
||||
height: 10px;
|
||||
background-color: #0a0a0f;
|
||||
height: 8px;
|
||||
margin: 0;
|
||||
border-radius: 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal {
|
||||
background-color: #45475a;
|
||||
background-color: #141b24;
|
||||
min-width: 30px;
|
||||
border-radius: 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background-color: #585b70;
|
||||
background-color: #00ff41;
|
||||
}
|
||||
|
||||
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
|
||||
@@ -283,12 +320,12 @@ QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
|
||||
|
||||
/* ---------- QMessageBox ---------- */
|
||||
QMessageBox {
|
||||
background-color: #2a2a3c;
|
||||
color: #cdd6f4;
|
||||
background-color: #0d1117;
|
||||
color: #b8c4d4;
|
||||
}
|
||||
|
||||
QMessageBox QLabel {
|
||||
color: #cdd6f4;
|
||||
color: #b8c4d4;
|
||||
}
|
||||
|
||||
QMessageBox QPushButton {
|
||||
@@ -297,18 +334,18 @@ QMessageBox QPushButton {
|
||||
|
||||
/* ---------- QToolTip ---------- */
|
||||
QToolTip {
|
||||
background-color: #2a2a3c;
|
||||
color: #cdd6f4;
|
||||
border: 1px solid #45475a;
|
||||
background-color: #0d1117;
|
||||
color: #b8c4d4;
|
||||
border: 1px solid #00ff41;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ---------- QGroupBox ---------- */
|
||||
QGroupBox {
|
||||
color: #cdd6f4;
|
||||
border: 1px solid #45475a;
|
||||
border-radius: 6px;
|
||||
color: #b8c4d4;
|
||||
border: 1px solid #1a2332;
|
||||
border-radius: 4px;
|
||||
margin-top: 8px;
|
||||
padding-top: 16px;
|
||||
font-weight: bold;
|
||||
@@ -318,29 +355,29 @@ QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
subcontrol-position: top left;
|
||||
padding: 0 6px;
|
||||
color: #89b4fa;
|
||||
color: #00ff41;
|
||||
}
|
||||
|
||||
/* ---------- QProgressBar ---------- */
|
||||
QProgressBar {
|
||||
background-color: #313244;
|
||||
border: 1px solid #45475a;
|
||||
border-radius: 6px;
|
||||
background-color: #0d1117;
|
||||
border: 1px solid #1a2332;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
color: #cdd6f4;
|
||||
color: #b8c4d4;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #89b4fa;
|
||||
border-radius: 5px;
|
||||
background-color: #00ff41;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* ---------- QMenu ---------- */
|
||||
QMenu {
|
||||
background-color: #2a2a3c;
|
||||
color: #cdd6f4;
|
||||
border: 1px solid #45475a;
|
||||
background-color: #0d1117;
|
||||
color: #b8c4d4;
|
||||
border: 1px solid #1a2332;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
@@ -351,34 +388,34 @@ QMenu::item {
|
||||
}
|
||||
|
||||
QMenu::item:selected {
|
||||
background-color: #89b4fa;
|
||||
color: #1e1e2e;
|
||||
background-color: rgba(0,255,65,0.15);
|
||||
color: #e8edf5;
|
||||
}
|
||||
|
||||
QMenu::separator {
|
||||
height: 1px;
|
||||
background-color: #45475a;
|
||||
background-color: #1a2332;
|
||||
margin: 4px 8px;
|
||||
}
|
||||
|
||||
/* ---------- QStatusBar ---------- */
|
||||
QStatusBar {
|
||||
background-color: #2a2a3c;
|
||||
color: #cdd6f4;
|
||||
border-top: 1px solid #45475a;
|
||||
background-color: #0d1117;
|
||||
color: #b8c4d4;
|
||||
border-top: 1px solid #1a2332;
|
||||
}
|
||||
|
||||
/* ---------- QCheckBox / QRadioButton ---------- */
|
||||
QCheckBox, QRadioButton {
|
||||
color: #cdd6f4;
|
||||
color: #b8c4d4;
|
||||
spacing: 8px;
|
||||
}
|
||||
|
||||
QCheckBox::indicator, QRadioButton::indicator {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #45475a;
|
||||
background-color: #2a2a3c;
|
||||
border: 2px solid #1a2332;
|
||||
background-color: #0d1117;
|
||||
}
|
||||
|
||||
QCheckBox::indicator {
|
||||
@@ -390,28 +427,45 @@ QRadioButton::indicator {
|
||||
}
|
||||
|
||||
QCheckBox::indicator:checked, QRadioButton::indicator:checked {
|
||||
background-color: #89b4fa;
|
||||
border-color: #89b4fa;
|
||||
background-color: #00ff41;
|
||||
border-color: #00ff41;
|
||||
}
|
||||
|
||||
/* ---------- QSpinBox / QDoubleSpinBox ---------- */
|
||||
QSpinBox, QDoubleSpinBox {
|
||||
background-color: #2a2a3c;
|
||||
color: #cdd6f4;
|
||||
border: 1px solid #45475a;
|
||||
border-radius: 6px;
|
||||
background-color: #0d1117;
|
||||
color: #b8c4d4;
|
||||
border: 1px solid #1a2332;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
QSpinBox:focus, QDoubleSpinBox:focus {
|
||||
border-color: #89b4fa;
|
||||
border-color: #00ff41;
|
||||
}
|
||||
|
||||
/* ---------- Semantic Colors (via properties or classes) ---------- */
|
||||
/* ---------- QDialog ---------- */
|
||||
QDialog {
|
||||
background-color: #0d1117;
|
||||
color: #b8c4d4;
|
||||
}
|
||||
|
||||
QDialogButtonBox QPushButton {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
/* ---------- QFrame (cards / panels) ---------- */
|
||||
QFrame[frameShape="6"] {
|
||||
background-color: #0d1117;
|
||||
border: 1px solid #1a2332;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ---------- Semantic Colors ---------- */
|
||||
QLabel[cssClass="income"] {
|
||||
color: #a6e3a1;
|
||||
color: #39ff14;
|
||||
}
|
||||
|
||||
QLabel[cssClass="expense"] {
|
||||
color: #f38ba8;
|
||||
color: #ff2d6f;
|
||||
}
|
||||
|
||||
@@ -1,417 +0,0 @@
|
||||
/* ===== Light Theme ===== */
|
||||
/* Background: #eff1f5 Surface: #ffffff Text: #4c4f69
|
||||
Accent: #1e66f5 Border: #ccd0da
|
||||
Success/Income: #40a02b Error/Expense: #d20f39 */
|
||||
|
||||
/* ---------- Base Widgets ---------- */
|
||||
QMainWindow, QWidget {
|
||||
background-color: #eff1f5;
|
||||
color: #4c4f69;
|
||||
font-family: "Segoe UI", "Roboto", sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
QLabel {
|
||||
color: #4c4f69;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ---------- Sidebar ---------- */
|
||||
#sidebar {
|
||||
background-color: #ffffff;
|
||||
border-right: 1px solid #ccd0da;
|
||||
}
|
||||
|
||||
#sidebar QPushButton {
|
||||
background-color: transparent;
|
||||
color: #4c4f69;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 10px 16px;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
margin: 2px 8px;
|
||||
}
|
||||
|
||||
#sidebar QPushButton:hover {
|
||||
background-color: #e6e9ef;
|
||||
}
|
||||
|
||||
#sidebar QPushButton:checked {
|
||||
background-color: #1e66f5;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#sidebar QPushButton#theme-toggle {
|
||||
background-color: #e6e9ef;
|
||||
color: #4c4f69;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
margin: 8px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#sidebar QPushButton#theme-toggle:hover {
|
||||
background-color: #ccd0da;
|
||||
}
|
||||
|
||||
/* ---------- QPushButton (general) ---------- */
|
||||
QPushButton {
|
||||
background-color: #1e66f5;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #1a5be0;
|
||||
}
|
||||
|
||||
QPushButton:pressed {
|
||||
background-color: #1650c8;
|
||||
}
|
||||
|
||||
QPushButton:disabled {
|
||||
background-color: #ccd0da;
|
||||
color: #9ca0b0;
|
||||
}
|
||||
|
||||
/* ---------- QPushButton inside tables ---------- */
|
||||
QTableView QPushButton, QTableWidget QPushButton {
|
||||
background-color: #1e66f5;
|
||||
color: #ffffff;
|
||||
padding: 2px 12px;
|
||||
}
|
||||
|
||||
QTableView QPushButton:hover, QTableWidget QPushButton:hover {
|
||||
background-color: #1a5be0;
|
||||
}
|
||||
|
||||
QTableView QPushButton:pressed, QTableWidget QPushButton:pressed {
|
||||
background-color: #1650c8;
|
||||
}
|
||||
|
||||
/* ---------- QTableView / QTableWidget ---------- */
|
||||
QTableView, QTableWidget {
|
||||
background-color: #ffffff;
|
||||
color: #4c4f69;
|
||||
gridline-color: #ccd0da;
|
||||
border: 1px solid #ccd0da;
|
||||
border-radius: 4px;
|
||||
selection-background-color: #1e66f5;
|
||||
selection-color: #ffffff;
|
||||
alternate-background-color: #eff1f5;
|
||||
}
|
||||
|
||||
QTableView::item, QTableWidget::item {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
QTableView::item:hover, QTableWidget::item:hover {
|
||||
background-color: #e6e9ef;
|
||||
}
|
||||
|
||||
/* ---------- QHeaderView ---------- */
|
||||
QHeaderView::section {
|
||||
background-color: #e6e9ef;
|
||||
color: #4c4f69;
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
border-bottom: 2px solid #ccd0da;
|
||||
border-right: 1px solid #ccd0da;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
QHeaderView::section:hover {
|
||||
background-color: #ccd0da;
|
||||
}
|
||||
|
||||
/* ---------- QComboBox ---------- */
|
||||
QComboBox {
|
||||
background-color: #ffffff;
|
||||
color: #4c4f69;
|
||||
border: 1px solid #ccd0da;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
QComboBox:hover {
|
||||
border-color: #1e66f5;
|
||||
}
|
||||
|
||||
QComboBox::drop-down {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: top right;
|
||||
width: 24px;
|
||||
border-left: 1px solid #ccd0da;
|
||||
border-top-right-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #ffffff;
|
||||
color: #4c4f69;
|
||||
border: 1px solid #ccd0da;
|
||||
selection-background-color: #1e66f5;
|
||||
selection-color: #ffffff;
|
||||
}
|
||||
|
||||
/* ---------- QLineEdit ---------- */
|
||||
QLineEdit {
|
||||
background-color: #ffffff;
|
||||
color: #4c4f69;
|
||||
border: 1px solid #ccd0da;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
QLineEdit:focus {
|
||||
border-color: #1e66f5;
|
||||
}
|
||||
|
||||
QLineEdit:disabled {
|
||||
background-color: #eff1f5;
|
||||
color: #9ca0b0;
|
||||
}
|
||||
|
||||
/* ---------- QDateEdit ---------- */
|
||||
QDateEdit {
|
||||
background-color: #ffffff;
|
||||
color: #4c4f69;
|
||||
border: 1px solid #ccd0da;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
QDateEdit:focus {
|
||||
border-color: #1e66f5;
|
||||
}
|
||||
|
||||
QDateEdit::drop-down {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: top right;
|
||||
width: 24px;
|
||||
border-left: 1px solid #ccd0da;
|
||||
}
|
||||
|
||||
/* ---------- QTabWidget / QTabBar ---------- */
|
||||
QTabWidget::pane {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #ccd0da;
|
||||
border-radius: 4px;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
QTabBar::tab {
|
||||
background-color: #e6e9ef;
|
||||
color: #4c4f69;
|
||||
border: 1px solid #ccd0da;
|
||||
border-bottom: none;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
QTabBar::tab:hover {
|
||||
background-color: #ccd0da;
|
||||
}
|
||||
|
||||
QTabBar::tab:selected {
|
||||
background-color: #1e66f5;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* ---------- QScrollBar (vertical) ---------- */
|
||||
QScrollBar:vertical {
|
||||
background-color: #eff1f5;
|
||||
width: 10px;
|
||||
margin: 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical {
|
||||
background-color: #ccd0da;
|
||||
min-height: 30px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background-color: #acb0be;
|
||||
}
|
||||
|
||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* ---------- QScrollBar (horizontal) ---------- */
|
||||
QScrollBar:horizontal {
|
||||
background-color: #eff1f5;
|
||||
height: 10px;
|
||||
margin: 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal {
|
||||
background-color: #ccd0da;
|
||||
min-width: 30px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background-color: #acb0be;
|
||||
}
|
||||
|
||||
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* ---------- QMessageBox ---------- */
|
||||
QMessageBox {
|
||||
background-color: #ffffff;
|
||||
color: #4c4f69;
|
||||
}
|
||||
|
||||
QMessageBox QLabel {
|
||||
color: #4c4f69;
|
||||
}
|
||||
|
||||
QMessageBox QPushButton {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
/* ---------- QToolTip ---------- */
|
||||
QToolTip {
|
||||
background-color: #ffffff;
|
||||
color: #4c4f69;
|
||||
border: 1px solid #ccd0da;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ---------- QGroupBox ---------- */
|
||||
QGroupBox {
|
||||
color: #4c4f69;
|
||||
border: 1px solid #ccd0da;
|
||||
border-radius: 6px;
|
||||
margin-top: 8px;
|
||||
padding-top: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
subcontrol-position: top left;
|
||||
padding: 0 6px;
|
||||
color: #1e66f5;
|
||||
}
|
||||
|
||||
/* ---------- QProgressBar ---------- */
|
||||
QProgressBar {
|
||||
background-color: #e6e9ef;
|
||||
border: 1px solid #ccd0da;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
color: #4c4f69;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #1e66f5;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* ---------- QMenu ---------- */
|
||||
QMenu {
|
||||
background-color: #ffffff;
|
||||
color: #4c4f69;
|
||||
border: 1px solid #ccd0da;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
QMenu::item {
|
||||
padding: 6px 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QMenu::item:selected {
|
||||
background-color: #1e66f5;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
QMenu::separator {
|
||||
height: 1px;
|
||||
background-color: #ccd0da;
|
||||
margin: 4px 8px;
|
||||
}
|
||||
|
||||
/* ---------- QStatusBar ---------- */
|
||||
QStatusBar {
|
||||
background-color: #ffffff;
|
||||
color: #4c4f69;
|
||||
border-top: 1px solid #ccd0da;
|
||||
}
|
||||
|
||||
/* ---------- QCheckBox / QRadioButton ---------- */
|
||||
QCheckBox, QRadioButton {
|
||||
color: #4c4f69;
|
||||
spacing: 8px;
|
||||
}
|
||||
|
||||
QCheckBox::indicator, QRadioButton::indicator {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #ccd0da;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
QCheckBox::indicator {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QRadioButton::indicator {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
QCheckBox::indicator:checked, QRadioButton::indicator:checked {
|
||||
background-color: #1e66f5;
|
||||
border-color: #1e66f5;
|
||||
}
|
||||
|
||||
/* ---------- QSpinBox / QDoubleSpinBox ---------- */
|
||||
QSpinBox, QDoubleSpinBox {
|
||||
background-color: #ffffff;
|
||||
color: #4c4f69;
|
||||
border: 1px solid #ccd0da;
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
QSpinBox:focus, QDoubleSpinBox:focus {
|
||||
border-color: #1e66f5;
|
||||
}
|
||||
|
||||
/* ---------- Semantic Colors (via properties or classes) ---------- */
|
||||
QLabel[cssClass="income"] {
|
||||
color: #40a02b;
|
||||
}
|
||||
|
||||
QLabel[cssClass="expense"] {
|
||||
color: #d20f39;
|
||||
}
|
||||
@@ -33,6 +33,7 @@ from src.models.category import Category
|
||||
from src.models.household import HouseholdMember
|
||||
from src.models.rule import CategorizationRule
|
||||
from src.models.transaction import Transaction
|
||||
from src.ui.themes import INCOME, EXPENSE
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -95,7 +96,7 @@ class TransactionTableModel(QAbstractTableModel):
|
||||
|
||||
if role == Qt.ItemDataRole.ForegroundRole and col == 2:
|
||||
amount = float(txn.amount) if txn.amount is not None else 0.0
|
||||
return QColor("red") if amount < 0 else QColor("green")
|
||||
return QColor(EXPENSE) if amount < 0 else QColor(INCOME)
|
||||
|
||||
# Store the category id for the delegate
|
||||
if role == Qt.ItemDataRole.UserRole and col == 4:
|
||||
@@ -320,10 +321,23 @@ class TransactionsView(QWidget):
|
||||
self._table.setSortingEnabled(True)
|
||||
self._table.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
||||
self._table.setAlternatingRowColors(True)
|
||||
self._table.horizontalHeader().setStretchLastSection(True)
|
||||
self._table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
||||
self._table.verticalHeader().setVisible(False)
|
||||
|
||||
# Column sizing: Date=Fixed, Description=Stretch, Amount=Fixed,
|
||||
# Account/Category=ResizeToContents, Tag=Fixed, Person=ResizeToContents
|
||||
header = self._table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed) # Date
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) # Description
|
||||
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed) # Amount
|
||||
header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) # Account
|
||||
header.setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents) # Category
|
||||
header.setSectionResizeMode(5, QHeaderView.ResizeMode.Fixed) # Tag
|
||||
header.setSectionResizeMode(6, QHeaderView.ResizeMode.ResizeToContents) # Person
|
||||
self._table.setColumnWidth(0, 100) # Date
|
||||
self._table.setColumnWidth(2, 110) # Amount
|
||||
self._table.setColumnWidth(5, 80) # Tag
|
||||
header.setMinimumSectionSize(80)
|
||||
|
||||
# Delegate for inline category editing
|
||||
self._cat_delegate = CategoryDelegate(self._table_model, session, self._table)
|
||||
self._table.setItemDelegateForColumn(4, self._cat_delegate)
|
||||
|
||||
Reference in New Issue
Block a user