diff --git a/src/ui/analysis_view.py b/src/ui/analysis_view.py index 18d57cf..979bba9 100644 --- a/src/ui/analysis_view.py +++ b/src/ui/analysis_view.py @@ -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) diff --git a/src/ui/import_view.py b/src/ui/import_view.py index d8ee9f7..96abc12 100644 --- a/src/ui/import_view.py +++ b/src/ui/import_view.py @@ -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 diff --git a/src/ui/main_window.py b/src/ui/main_window.py index a778367..d7dd41e 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -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")) diff --git a/src/ui/recurring_view.py b/src/ui/recurring_view.py index 48bd212..bbcb361 100644 --- a/src/ui/recurring_view.py +++ b/src/ui/recurring_view.py @@ -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.""" diff --git a/src/ui/settings_view.py b/src/ui/settings_view.py index bba613c..679421b 100644 --- a/src/ui/settings_view.py +++ b/src/ui/settings_view.py @@ -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) diff --git a/src/ui/sidebar.py b/src/ui/sidebar.py index 842087b..9d2057e 100644 --- a/src/ui/sidebar.py +++ b/src/ui/sidebar.py @@ -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) diff --git a/src/ui/themes/DESIGN_SYSTEM.md b/src/ui/themes/DESIGN_SYSTEM.md new file mode 100644 index 0000000..a790403 --- /dev/null +++ b/src/ui/themes/DESIGN_SYSTEM.md @@ -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 diff --git a/src/ui/themes/__init__.py b/src/ui/themes/__init__.py index e69de29..382c0e7 100644 --- a/src/ui/themes/__init__.py +++ b/src/ui/themes/__init__.py @@ -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") diff --git a/src/ui/themes/dark.qss b/src/ui/themes/dark.qss index 7f7d852..20fd65e 100644 --- a/src/ui/themes/dark.qss +++ b/src/ui/themes/dark.qss @@ -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; } diff --git a/src/ui/themes/light.qss b/src/ui/themes/light.qss deleted file mode 100644 index 9bc2127..0000000 --- a/src/ui/themes/light.qss +++ /dev/null @@ -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; -} diff --git a/src/ui/transactions_view.py b/src/ui/transactions_view.py index c24e49e..6f210d3 100644 --- a/src/ui/transactions_view.py +++ b/src/ui/transactions_view.py @@ -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)