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:
2026-02-10 16:10:29 -05:00
parent c80df54eb9
commit dfe62f442a
11 changed files with 587 additions and 683 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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"))

View File

@@ -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."""

View File

@@ -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)

View File

@@ -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)

View 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

View File

@@ -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")

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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)