From 562d4de5877adead29f689dadbbe4488c99edbe8 Mon Sep 17 00:00:00 2001
From: Thiago Macedo <1750447+macedot@users.noreply.github.com>
Date: Sun, 9 Aug 2026 01:03:19 +0200
Subject: [PATCH 1/3] fix: improve chart zero-line and dark table contrast
Draw y=0 in aviation orange via a real Chart.js plugin, darken dark-mode table/input backgrounds so green/red values stay readable, and harden local docker compose for debugging.
Co-Authored-By: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Cursor
const expenseData = chartData.map(r => (r.cashflow < 0 ? r.cashflow : null));
const balanceData = chartData.map(r => r.balance);
+ const AXIS_ORANGE = '#FF4F00';
+ // Must be attached via config.plugins (array); options.plugins only configures registered plugins.
+ // Attach after JSON clones — stringify strips functions.
+ const axisHighlightPlugin = {
+ id: 'axisHighlight',
+ afterDraw(chart) {
+ const yScale = chart.scales.y;
+ const ctx = chart.ctx;
+ const { left, right, top, bottom } = chart.chartArea;
+ // Only highlight y=0 — not the chart-area baseline (scale min)
+ const zeroY = yScale.getPixelForValue(0);
+ if (zeroY < top || zeroY > bottom) {
+ return;
+ }
+ ctx.save();
+ ctx.strokeStyle = AXIS_ORANGE;
+ ctx.lineWidth = 2.5;
+ ctx.setLineDash([]);
+ ctx.beginPath();
+ ctx.moveTo(left, zeroY);
+ ctx.lineTo(right, zeroY);
+ ctx.stroke();
+ ctx.restore();
+ },
+ };
+
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
@@ -1061,45 +1087,6 @@
display: true,
position: 'top',
},
- // Draw a stronger/brighter horizontal line at y=0
- zeroLine: {
- afterDraw: chart => {
- const yScale = chart.scales.y;
- const ctx = chart.ctx;
- const { top, bottom } = chart.chartArea;
- const xLeft = chart.chartArea.left;
- const xRight = chart.chartArea.right;
- const zeroY = yScale.getPixelForValue(0);
- if (zeroY >= top && zeroY <= bottom) {
- ctx.save();
- ctx.strokeStyle = darkMode.value ? '#e5e7eb' : '#374151';
- ctx.lineWidth = 2.5;
- ctx.setLineDash([]);
- ctx.beginPath();
- ctx.moveTo(xLeft, zeroY);
- ctx.lineTo(xRight, zeroY);
- ctx.stroke();
- ctx.restore();
- }
- },
- },
- // Draw a stronger x-axis baseline
- xAxisLine: {
- afterDraw: chart => {
- const ctx = chart.ctx;
- const { left, right, bottom } = chart.chartArea;
- const xBottomY = bottom;
- ctx.save();
- ctx.strokeStyle = darkMode.value ? '#e5e7eb' : '#374151';
- ctx.lineWidth = 2.5;
- ctx.setLineDash([]);
- ctx.beginPath();
- ctx.moveTo(left, xBottomY);
- ctx.lineTo(right, xBottomY);
- ctx.stroke();
- ctx.restore();
- },
- },
},
scales: {
x: {
@@ -1156,6 +1143,7 @@
inlineConfig.data.datasets[0].categoryPercentage = 0.5;
inlineConfig.data.datasets[1].barPercentage = 1.0;
inlineConfig.data.datasets[1].categoryPercentage = 0.5;
+ inlineConfig.plugins = [axisHighlightPlugin];
cashflowChartInstance = new Chart(cashflowChart.value, inlineConfig);
}
if (cashflowChartFullscreen.value) {
@@ -1180,8 +1168,6 @@
position: 'top',
labels: { color: textColor },
},
- zeroLine: chartOptions.plugins.zeroLine,
- xAxisLine: chartOptions.plugins.xAxisLine,
};
const fullscreenConfig = JSON.parse(JSON.stringify(chartConfig));
// Set bar spacing per dataset for fullscreen
@@ -1190,6 +1176,7 @@
fullscreenConfig.data.datasets[1].barPercentage = 1.0;
fullscreenConfig.data.datasets[1].categoryPercentage = 0.5;
fullscreenConfig.options = fullscreenOptions;
+ fullscreenConfig.plugins = [axisHighlightPlugin];
cashflowChartFullscreenInstance = new Chart(
cashflowChartFullscreen.value,
fullscreenConfig
diff --git a/src/style.css b/src/style.css
index ddd5280..d0f829d 100644
--- a/src/style.css
+++ b/src/style.css
@@ -1,96 +1,185 @@
- [v-cloak] { display: none; }
- .fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
- .fade-enter-from, .fade-leave-to { opacity: 0; }
- .chart-container { position: relative; height: 350px; width: 100%; }
- .error-msg { color: var(--color-error); font-size: 0.875rem; margin-top: 0.25rem; }
- .toggle-icon::before { color: inherit; }
- .toggle-icon.fold::after { content: '▶'; color: inherit; }
- .toggle-icon:not(.fold)::after { content: '▼'; color: inherit; }
+[v-cloak] {
+ display: none;
+}
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.3s ease;
+}
+.fade-enter-from,
+.fade-leave-to {
+ opacity: 0;
+}
+.chart-container {
+ position: relative;
+ height: 350px;
+ width: 100%;
+}
+.error-msg {
+ color: var(--color-error);
+ font-size: 0.875rem;
+ margin-top: 0.25rem;
+}
+.toggle-icon::before {
+ color: inherit;
+}
+.toggle-icon.fold::after {
+ content: '▶';
+ color: inherit;
+}
+.toggle-icon:not(.fold)::after {
+ content: '▼';
+ color: inherit;
+}
- /* CSS custom properties for theming — dark mode via .dark class */
- :root {
- --color-bg: #f3f4f6;
- --color-surface: #ffffff;
- --color-border: #d1d5db;
- --color-text: #1f2937;
- --color-text-muted: #6b7280;
- --color-header-bg: #2563eb;
- --color-error-bg: #fef2f2;
- --color-error-text: #dc2626;
- --color-input-bg: #ffffff;
- --color-input-text: #1f2937;
- --color-input-border: #d1d5db;
- --color-table-header-bg: #f3f4f6;
- --color-table-row-bg: #f9fafb;
- --color-hover-bg: #eff6ff;
- --color-edit-bg: #eff6ff;
- }
- .dark {
- --color-bg: #111827;
- --color-surface: #1f2937;
- --color-border: #4b5563;
- --color-text: #f3f4f6;
- --color-text-muted: #9ca3af;
- --color-blue: #3b82f6;
- --color-green: #22c55e;
- --color-red: #f87171;
- --color-error-bg: #7f1d1d;
- --color-error-text: #fca5a5;
- --color-input-bg: #374151;
- --color-input-text: #f3f4f6;
- --color-input-border: #4b5563;
- --color-table-header-bg: #374151;
- --color-table-row-bg: #1f2937;
- --color-hover-bg: #1e3a5f;
- --color-edit-bg: #1e3a5f;
- }
+/* CSS custom properties for theming — dark mode via .dark class */
+:root {
+ --color-bg: #f3f4f6;
+ --color-surface: #ffffff;
+ --color-border: #d1d5db;
+ --color-text: #1f2937;
+ --color-text-muted: #6b7280;
+ --color-header-bg: #2563eb;
+ --color-error-bg: #fef2f2;
+ --color-error-text: #dc2626;
+ --color-input-bg: #ffffff;
+ --color-input-text: #1f2937;
+ --color-input-border: #d1d5db;
+ --color-table-header-bg: #f3f4f6;
+ --color-table-row-bg: #f9fafb;
+ --color-hover-bg: #eff6ff;
+ --color-edit-bg: #eff6ff;
+}
+.dark {
+ --color-bg: #111827;
+ --color-surface: #1f2937;
+ --color-border: #4b5563;
+ --color-text: #f3f4f6;
+ --color-text-muted: #9ca3af;
+ --color-blue: #3b82f6;
+ --color-green: #22c55e;
+ --color-red: #f87171;
+ --color-error-bg: #7f1d1d;
+ --color-error-text: #fca5a5;
+ --color-input-bg: #111827;
+ --color-input-text: #f3f4f6;
+ --color-input-border: #4b5563;
+ --color-table-header-bg: #111827;
+ --color-table-row-bg: #111827;
+ --color-hover-bg: #1e3a5f;
+ --color-edit-bg: #1e3a5f;
+}
- /* Dark mode colors via CSS custom properties — use !important to beat Tailwind */
- .dark,
- html.dark {
- --color-bg: #111827;
- --color-surface: #1f2937;
- --color-border: #4b5563;
- --color-text: #f3f4f6;
- --color-text-muted: #9ca3af;
- --color-text-link: #93c5fd;
- --color-input-bg: #374151;
- --color-input-text: #f3f4f6;
- --color-input-border: #4b5563;
- --color-header-bg: #1f2937;
- --color-table-header-bg: #374151;
- --color-table-row-bg: #1f2937;
- --color-hover-bg: #374151;
- --color-error-bg: #7f1d1d;
- --color-error-text: #fca5a5;
- --color-link-blue: #60a5fa;
- }
+/* Dark mode colors via CSS custom properties — use !important to beat Tailwind */
+.dark,
+html.dark {
+ --color-bg: #111827;
+ --color-surface: #1f2937;
+ --color-border: #4b5563;
+ --color-text: #f3f4f6;
+ --color-text-muted: #9ca3af;
+ --color-text-link: #93c5fd;
+ --color-input-bg: #111827;
+ --color-input-text: #f3f4f6;
+ --color-input-border: #4b5563;
+ --color-header-bg: #1f2937;
+ --color-table-header-bg: #111827;
+ --color-table-row-bg: #111827;
+ --color-hover-bg: #1e3a5f;
+ --color-error-bg: #7f1d1d;
+ --color-error-text: #fca5a5;
+ --color-link-blue: #60a5fa;
+}
- /* Apply CSS variables to all relevant elements in dark mode */
- html.dark body { background-color: var(--color-bg) !important; color: var(--color-text) !important; }
- html.dark header { background-color: var(--color-header-bg) !important; color: #fff !important; }
- html.dark .bg-white { background-color: var(--color-surface) !important; }
- html.dark .bg-gray-50 { background-color: var(--color-table-row-bg) !important; }
- html.dark .bg-gray-100 { background-color: var(--color-table-header-bg) !important; }
- html.dark .bg-gray-200 { background-color: var(--color-border) !important; }
- html.dark .bg-blue-50 { background-color: var(--color-hover-bg) !important; }
- html.dark .bg-blue-900 { background-color: var(--color-hover-bg) !important; }
- html.dark .bg-red-50 { background-color: var(--color-error-bg) !important; }
- html.dark .text-gray-700 { color: var(--color-text) !important; }
- html.dark .text-gray-600 { color: var(--color-text-muted) !important; }
- html.dark .text-gray-500 { color: var(--color-text-muted) !important; }
- html.dark .text-gray-400 { color: var(--color-text-muted) !important; }
- html.dark .text-gray-300 { color: var(--color-text-muted) !important; }
- html.dark .text-gray-200 { color: var(--color-text-muted) !important; }
- html.dark .text-gray-100 { color: var(--color-text-muted) !important; }
- html.dark .text-black { color: var(--color-text) !important; }
- html.dark .text-blue-500 { color: var(--color-link-blue) !important; }
- html.dark .text-blue-200 { color: var(--color-text-muted) !important; }
- html.dark .text-red-500 { color: #f87171 !important; }
- html.dark .text-red-600 { color: #f87171 !important; }
- html.dark .text-green-600 { color: #4ade80 !important; }
- html.dark .border-gray-200 { border-color: var(--color-border) !important; }
- html.dark .border-gray-300 { border-color: var(--color-border) !important; }
- html.dark .border-gray-600 { border-color: var(--color-border) !important; }
- html.dark input, html.dark select { background-color: var(--color-input-bg) !important; color: var(--color-input-text) !important; border-color: var(--color-input-border) !important; }
- html.dark .error-msg { color: var(--color-error-text) !important; }
+/* Apply CSS variables to all relevant elements in dark mode */
+html.dark body {
+ background-color: var(--color-bg) !important;
+ color: var(--color-text) !important;
+}
+html.dark header {
+ background-color: var(--color-header-bg) !important;
+ color: #fff !important;
+}
+html.dark .bg-white {
+ background-color: var(--color-surface) !important;
+}
+html.dark .bg-gray-50 {
+ background-color: var(--color-table-row-bg) !important;
+}
+html.dark .bg-gray-100 {
+ background-color: var(--color-table-header-bg) !important;
+}
+html.dark .bg-gray-200 {
+ background-color: var(--color-border) !important;
+}
+html.dark .bg-gray-700 {
+ background-color: var(--color-table-row-bg) !important;
+}
+html.dark .bg-gray-800 {
+ background-color: var(--color-surface) !important;
+}
+html.dark .bg-blue-50 {
+ background-color: var(--color-hover-bg) !important;
+}
+html.dark .bg-blue-900 {
+ background-color: var(--color-hover-bg) !important;
+}
+html.dark .bg-red-50 {
+ background-color: var(--color-error-bg) !important;
+}
+html.dark .text-gray-700 {
+ color: var(--color-text) !important;
+}
+html.dark .text-gray-600 {
+ color: var(--color-text-muted) !important;
+}
+html.dark .text-gray-500 {
+ color: var(--color-text-muted) !important;
+}
+html.dark .text-gray-400 {
+ color: var(--color-text-muted) !important;
+}
+html.dark .text-gray-300 {
+ color: var(--color-text-muted) !important;
+}
+html.dark .text-gray-200 {
+ color: var(--color-text) !important;
+}
+html.dark .text-gray-100 {
+ color: var(--color-text) !important;
+}
+html.dark .text-black {
+ color: var(--color-text) !important;
+}
+html.dark .text-blue-500 {
+ color: var(--color-link-blue) !important;
+}
+html.dark .text-blue-200 {
+ color: var(--color-text-muted) !important;
+}
+html.dark .text-red-500 {
+ color: #f87171 !important;
+}
+html.dark .text-red-600 {
+ color: #f87171 !important;
+}
+html.dark .text-green-600 {
+ color: #4ade80 !important;
+}
+html.dark .border-gray-200 {
+ border-color: var(--color-border) !important;
+}
+html.dark .border-gray-300 {
+ border-color: var(--color-border) !important;
+}
+html.dark .border-gray-600 {
+ border-color: var(--color-border) !important;
+}
+html.dark input,
+html.dark select {
+ background-color: var(--color-input-bg) !important;
+ color: var(--color-input-text) !important;
+ border-color: var(--color-input-border) !important;
+}
+html.dark .error-msg {
+ color: var(--color-error-text) !important;
+}
From 56f18382cfceab84c0b123b5702ff7493d315b9d Mon Sep 17 00:00:00 2001
From: Thiago Macedo <1750447+macedot@users.noreply.github.com>
Date: Sun, 9 Aug 2026 01:05:54 +0200
Subject: [PATCH 2/3] fix: darken table cell hover background in dark mode
Override hover:bg-blue-100 so green/red values stay readable on hover.
Co-Authored-By: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Cursor
Cashflow Simulator