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 --- docker-compose.yml | 13 ++- index.html | 69 +++++------- src/style.css | 275 ++++++++++++++++++++++++++++++--------------- 3 files changed, 220 insertions(+), 137 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b038711..61e92d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,11 +7,18 @@ services: container_name: cashflow ports: - "8080:80" + # Skip entrypoint template processing (fails on read-only root) + entrypoint: ["nginx", "-g", "daemon off;"] + # Mount local sources for live debugging without rebuild + volumes: + - ./index.html:/usr/share/nginx/html/index.html:ro + - ./src:/usr/share/nginx/html/src:ro read_only: true tmpfs: - - /tmp - - /var/cache/nginx - - /var/run + # nginx user (uid/gid 101) must own cache/run dirs + - /tmp:uid=101,gid=101,mode=1777 + - /var/cache/nginx:uid=101,gid=101,mode=1777 + - /var/run:uid=101,gid=101,mode=1777 security_opt: - no-new-privileges:true deploy: diff --git a/index.html b/index.html index 22dc97a..3b09c67 100644 --- a/index.html +++ b/index.html @@ -1049,6 +1049,32 @@

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 --- src/style.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/style.css b/src/style.css index d0f829d..f631447 100644 --- a/src/style.css +++ b/src/style.css @@ -120,9 +120,19 @@ html.dark .bg-gray-800 { html.dark .bg-blue-50 { background-color: var(--color-hover-bg) !important; } +html.dark .bg-blue-100 { + background-color: var(--color-hover-bg) !important; +} html.dark .bg-blue-900 { background-color: var(--color-hover-bg) !important; } +/* Tailwind hover:bg-blue-100 stays light unless overridden — washes out green/red values */ +html.dark .hover\:bg-blue-100:hover { + background-color: var(--color-hover-bg) !important; +} +html.dark .hover\:bg-gray-200:hover { + background-color: #374151 !important; +} html.dark .bg-red-50 { background-color: var(--color-error-bg) !important; } From 61551e3070edacf10cb7681c298c90fe5e54cc88 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 3/3] chore: bump version to 0.5.1 Co-Authored-By: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Cursor --- .release-please-manifest.json | 2 +- index.html | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6bbfeb6..1bae8f9 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{".":"0.5.0"} +{".":"0.5.1"} diff --git a/index.html b/index.html index 3b09c67..8a0a31b 100644 --- a/index.html +++ b/index.html @@ -43,7 +43,7 @@

Cashflow Simulator

- v0.5.0 + v0.5.1