Skip to content

Commit 22090c9

Browse files
committed
Add Cyberpunk, Neo-Brutalist, and Nordic themes; fix dark-theme readability issues
New themes follow the existing per-theme CSS variable pattern in variables.css, with a few Neo-Brutalist-only border/shadow additions for its signature hard-edged look. Also fixes several dark-background readability bugs surfaced while testing the new themes (affecting the existing Dark theme too): - Analytics charts had a hardcoded white card background and hardcoded grid/border/accent colors, so they never actually followed the theme - Sidebar tooltips and the transactions bulk-action bar paired hardcoded white text with --gray-800/900, which invert to near-white in dark themes - The transactions search input had no explicit background/text color and fell back to the browser's default white input styling - Date inputs' native calendar icon is browser-rendered and ignores page CSS; set the color-scheme property so it renders light/dark chrome to match the active theme
1 parent 40a755f commit 22090c9

10 files changed

Lines changed: 264 additions & 14 deletions

File tree

static/css/components.css

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,3 +473,42 @@
473473
border-color: var(--color-primary);
474474
box-shadow: 0 0 0 3px var(--focus-ring-color);
475475
}
476+
477+
/* Neo-Brutalist: thick ink-black borders + hard offset shadows, black text on bright fills */
478+
[data-theme="brutalist"] .card,
479+
[data-theme="brutalist"] .action-card {
480+
border: 3px solid var(--gray-900);
481+
}
482+
483+
[data-theme="brutalist"] .btn {
484+
border: 2px solid var(--gray-900);
485+
box-shadow: var(--shadow-base);
486+
transition: transform var(--transition-fast), box-shadow var(--transition-fast), background-color var(--transition-fast);
487+
}
488+
489+
[data-theme="brutalist"] .btn:hover:not(:disabled) {
490+
transform: translate(-2px, -2px);
491+
box-shadow: var(--shadow-md);
492+
}
493+
494+
[data-theme="brutalist"] .btn-primary,
495+
[data-theme="brutalist"] .btn-secondary,
496+
[data-theme="brutalist"] .btn-danger {
497+
color: var(--gray-900);
498+
}
499+
500+
[data-theme="brutalist"] .btn-danger {
501+
background-color: var(--color-danger);
502+
}
503+
504+
[data-theme="brutalist"] .form-input,
505+
[data-theme="brutalist"] .form-select,
506+
[data-theme="brutalist"] .form-textarea {
507+
border: 2px solid var(--gray-900);
508+
}
509+
510+
[data-theme="brutalist"] .form-input:focus,
511+
[data-theme="brutalist"] .form-select:focus,
512+
[data-theme="brutalist"] .form-textarea:focus {
513+
box-shadow: var(--shadow-base);
514+
}

static/css/global.css

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ p {
126126
}
127127

128128
.nav-icon-item.active {
129-
background-color: var(--color-primary);
129+
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary, var(--color-primary)));
130130
color: white;
131131
}
132132

@@ -137,7 +137,9 @@ p {
137137
left: calc(100% + 10px);
138138
top: 50%;
139139
transform: translateY(-50%);
140-
background-color: var(--gray-800);
140+
/* Fixed dark bubble regardless of theme — --gray-800 inverts to near-white
141+
in dark/cyberpunk, which would leave white text unreadable */
142+
background-color: rgba(17, 24, 39, 0.92);
141143
color: white;
142144
padding: 5px 10px;
143145
border-radius: var(--radius-base);
@@ -251,3 +253,8 @@ p {
251253
.mb-md { margin-bottom: var(--spacing-md); }
252254
.mb-lg { margin-bottom: var(--spacing-lg); }
253255
.mb-xl { margin-bottom: var(--spacing-xl); }
256+
257+
/* Neo-Brutalist: primary/secondary accents are bright, so use black text for contrast */
258+
[data-theme="brutalist"] .nav-icon-item.active {
259+
color: var(--gray-900);
260+
}

static/css/modals.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,8 @@
135135
padding: 0;
136136
}
137137
}
138+
139+
/* Neo-Brutalist: thick ink-black border around the modal */
140+
[data-theme="brutalist"] .modal {
141+
border: 3px solid var(--gray-900);
142+
}

static/css/pages/analytics.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
}
5656

5757
.analytics-card {
58-
background: #fff;
58+
background: var(--white);
5959
border: 1px solid var(--gray-200);
6060
border-radius: var(--radius-lg);
6161
padding: var(--spacing-lg);

static/css/pages/settle.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,8 @@
5858
.balance-amount {
5959
font-weight: var(--font-weight-semibold);
6060
}
61+
62+
/* Neo-Brutalist: thick ink-black border around each section */
63+
[data-theme="brutalist"] .settle-section {
64+
border: 3px solid var(--gray-900);
65+
}

static/css/pages/transactions.css

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
outline: none;
2828
font-size: var(--font-size-sm);
2929
padding: 0;
30+
background-color: transparent;
31+
color: var(--gray-900);
3032
}
3133

3234
.search-icon {
@@ -169,7 +171,9 @@
169171
align-items: center;
170172
gap: var(--spacing-md);
171173
padding: var(--spacing-sm) var(--spacing-lg);
172-
background: var(--gray-900);
174+
/* Fixed dark surface regardless of theme — --gray-900 inverts to near-white
175+
in dark/cyberpunk, which would leave the white text unreadable */
176+
background: rgba(17, 24, 39, 0.95);
173177
color: white;
174178
border-radius: var(--radius-lg);
175179
box-shadow: var(--shadow-lg);
@@ -184,7 +188,7 @@
184188
.bulk-action-count {
185189
font-size: var(--font-size-sm);
186190
font-weight: var(--font-weight-medium);
187-
color: var(--gray-300);
191+
color: rgba(255, 255, 255, 0.65);
188192
}
189193

190194
.bulk-action-buttons {
@@ -194,12 +198,12 @@
194198

195199
.btn-ghost {
196200
background: transparent;
197-
color: var(--gray-400);
198-
border: 1px solid var(--gray-600);
201+
color: rgba(255, 255, 255, 0.7);
202+
border: 1px solid rgba(255, 255, 255, 0.3);
199203
}
200204

201205
.btn-ghost:hover {
202-
background: var(--gray-800);
206+
background: rgba(255, 255, 255, 0.15);
203207
color: white;
204208
}
205209

static/css/table.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,8 @@
213213
font-size: var(--font-size-lg);
214214
color: var(--gray-500);
215215
}
216+
217+
/* Neo-Brutalist: thick ink-black border around the table */
218+
[data-theme="brutalist"] .table-container {
219+
border: 3px solid var(--gray-900);
220+
}

static/css/variables.css

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
:root {
2+
/* Tells the browser to render native widgets (date picker icon, scrollbars, etc.)
3+
with light chrome, regardless of the OS-level light/dark preference. Themes with
4+
a dark surface override this to "dark" below so those icons stay visible. */
5+
color-scheme: light;
6+
27
/* Surface (overridden per theme) */
38
--white: #ffffff;
49

@@ -159,6 +164,8 @@
159164

160165
/* ── Dark theme ────────────────────────────────────────────── */
161166
[data-theme="dark"] {
167+
color-scheme: dark;
168+
162169
--white: #1E293B;
163170

164171
--color-primary: #60A5FA;
@@ -200,3 +207,144 @@
200207
--focus-ring-color: rgba(96, 165, 250, 0.25);
201208
--primary-bg-hover: rgba(96, 165, 250, 0.1);
202209
}
210+
211+
/* ── Cyberpunk theme (High-Contrast Tech) ─────────────────────── */
212+
[data-theme="cyberpunk"] {
213+
color-scheme: dark;
214+
215+
--white: #1F2937;
216+
217+
--color-primary: #06B6D4;
218+
--color-primary-dark: #0891B2;
219+
--color-primary-light: #67E8F9;
220+
221+
/* Secondary/highlight accent (electric violet); components fall back to primary if unset */
222+
--color-secondary: #8B5CF6;
223+
224+
--color-success: #34D399;
225+
--color-success-light: rgba(52, 211, 153, 0.15);
226+
227+
--color-error: #F43F5E;
228+
--color-error-light: rgba(244, 63, 94, 0.15);
229+
230+
--color-warning: #FBBF24;
231+
--color-warning-light: rgba(251, 191, 36, 0.15);
232+
233+
--color-danger: #F43F5E;
234+
--color-income: #34D399;
235+
--color-expense: #F43F5E;
236+
237+
/* Inverted slate scale anchored to Deep Midnight Blue / Crisp Off-White */
238+
--gray-50: #0B0F19;
239+
--gray-100: #1F2937;
240+
--gray-200: #2D3748;
241+
--gray-300: #3F4B5E;
242+
--gray-400: #64748B;
243+
--gray-500: #94A3B8;
244+
--gray-600: #C4CDD5;
245+
--gray-700: #D8DEE4;
246+
--gray-800: #ECEFF2;
247+
--gray-900: #F9FAFB;
248+
249+
/* Neon-tinted shadows for a subtle glow on modals and hovered cards */
250+
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.5);
251+
--shadow-base: 0 1px 3px 0 rgba(0, 0, 0, 0.6), 0 1px 2px 0 rgba(0, 0, 0, 0.5);
252+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.6), 0 2px 4px -1px rgba(0, 0, 0, 0.5);
253+
--shadow-lg: 0 10px 15px -3px rgba(6, 182, 212, 0.15), 0 4px 6px -2px rgba(0, 0, 0, 0.5);
254+
--shadow-2xl: 0 25px 50px -12px rgba(6, 182, 212, 0.25), 0 0 40px -5px rgba(139, 92, 246, 0.15);
255+
256+
--focus-ring-color: rgba(6, 182, 212, 0.35);
257+
--primary-bg-hover: rgba(6, 182, 212, 0.1);
258+
}
259+
260+
/* ── Neo-Brutalist theme (Playful & Confident) ────────────────── */
261+
[data-theme="brutalist"] {
262+
--white: #FFFFFF;
263+
264+
--color-primary: #ADFF00;
265+
--color-primary-dark: #8FD400;
266+
--color-primary-light: #D4FF66;
267+
268+
/* Contrast accent (hot magenta); components fall back to primary if unset */
269+
--color-secondary: #FF007A;
270+
271+
--color-success: #16A34A;
272+
--color-success-light: #BBF7D0;
273+
274+
--color-error: #FF007A;
275+
--color-error-light: #FFD6EA;
276+
277+
--color-warning: #FFDE4D;
278+
--color-warning-light: #FFF6D1;
279+
280+
--color-danger: #FF007A;
281+
--color-income: #16A34A;
282+
--color-expense: #FF007A;
283+
284+
/* Warm ochre-to-black scale anchored to the Pale Ochre canvas and Ink Black text */
285+
--gray-50: #FDF8E2;
286+
--gray-100: #F3EEDA;
287+
--gray-200: #E8DFC0;
288+
--gray-300: #D4C89A;
289+
--gray-400: #A89B6E;
290+
--gray-500: #6B6347;
291+
--gray-600: #4A4430;
292+
--gray-700: #2E2A1D;
293+
--gray-800: #16140D;
294+
--gray-900: #000000;
295+
296+
/* Hard-edged, no-blur offset shadows and square corners for the brutalist look */
297+
--radius-sm: 0;
298+
--radius-base: 0;
299+
--radius-md: 0;
300+
--radius-lg: 0;
301+
--radius-xl: 0;
302+
303+
--shadow-sm: 2px 2px 0 0 #000000;
304+
--shadow-base: 3px 3px 0 0 #000000;
305+
--shadow-md: 4px 4px 0 0 #000000;
306+
--shadow-lg: 6px 6px 0 0 #000000;
307+
--shadow-2xl: 8px 8px 0 0 #000000;
308+
309+
--focus-ring-color: rgba(0, 0, 0, 0.5);
310+
--primary-bg-hover: rgba(173, 255, 0, 0.15);
311+
}
312+
313+
/* ── Nordic Minimalist theme (Clean & Professional) ───────────── */
314+
[data-theme="nordic"] {
315+
--white: #FFFFFF;
316+
317+
--color-primary: #4F46E5;
318+
--color-primary-dark: #4338CA;
319+
--color-primary-light: #818CF8;
320+
321+
/* Secondary brand nuance (deep forest green); components fall back to primary if unset */
322+
--color-secondary: #14532D;
323+
324+
--color-success: #14532D;
325+
--color-success-light: #DCFCE7;
326+
327+
--color-error: #DC2626;
328+
--color-error-light: #FEE2E2;
329+
330+
--color-warning: #FB923C;
331+
--color-warning-light: #FFEDD5;
332+
333+
--color-danger: #DC2626;
334+
--color-income: #14532D;
335+
--color-expense: #DC2626;
336+
337+
--gray-50: #F8F9FA;
338+
--gray-100: #F1F3F5;
339+
--gray-200: #E9ECEF;
340+
--gray-300: #DEE2E6;
341+
--gray-400: #ADB5BD;
342+
--gray-500: #868E96;
343+
--gray-600: #495057;
344+
--gray-700: #343A40;
345+
--gray-800: #212529;
346+
--gray-900: #1F2937;
347+
348+
--focus-ring-color: rgba(79, 70, 229, 0.2);
349+
--primary-bg-hover: rgba(79, 70, 229, 0.05);
350+
}

static/js/pages/analytics.js

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,29 @@ const CATEGORICAL_COLORS = [
2626
'#eb6834' // orange
2727
];
2828
const OTHER_COLOR = '#9CA3AF'; // fixed neutral gray — never a hue slot
29-
const ACCENT_COLOR = '#4A90E2'; // app primary, used for "selected" emphasis
30-
const DEEMPHASIS_COLOR = '#D1D5DB';
31-
const DEEMPHASIS_BORDER = '#9CA3AF';
29+
30+
// Chart chrome (grid lines, axis text, "selected month" emphasis, pie borders) follows
31+
// the active theme's CSS variables instead of being hardcoded for a light background.
32+
// Read once at load — theme.js sets data-theme before this module runs, and the theme
33+
// picker only lives on the Settings page, so it can't change without a full reload here.
34+
let ACCENT_COLOR = '#4A90E2';
35+
let DEEMPHASIS_COLOR = '#D1D5DB';
36+
let DEEMPHASIS_BORDER = '#9CA3AF';
37+
let CHART_GRID_COLOR = '#E5E7EB';
38+
let CHART_CARD_BG = '#ffffff';
39+
40+
function applyChartTheme() {
41+
const style = getComputedStyle(document.documentElement);
42+
const read = (name, fallback) => style.getPropertyValue(name).trim() || fallback;
43+
44+
ACCENT_COLOR = read('--color-primary', ACCENT_COLOR);
45+
DEEMPHASIS_COLOR = read('--gray-300', DEEMPHASIS_COLOR);
46+
DEEMPHASIS_BORDER = read('--gray-400', DEEMPHASIS_BORDER);
47+
CHART_GRID_COLOR = read('--gray-200', CHART_GRID_COLOR);
48+
CHART_CARD_BG = read('--white', CHART_CARD_BG);
49+
50+
Chart.defaults.color = read('--gray-600', '#6B7280');
51+
}
3252

3353
const CATEGORY_CAP = 5; // top N real slices; rest fold into "Other" (max 6 wedges total)
3454
const SOURCE_CAP = 5;
@@ -46,6 +66,8 @@ let sourceColorMap = {};
4666
let historicalMonths = [];
4767

4868
async function init() {
69+
applyChartTheme();
70+
4971
const dbId = AppState.getActiveDatabaseId();
5072

5173
try {
@@ -421,7 +443,7 @@ function renderSpendingOverTime(transactions) {
421443
type: 'linear',
422444
beginAtZero: true,
423445
ticks: { callback: v => formatCurrency(v) },
424-
grid: { color: '#E5E7EB' }
446+
grid: { color: CHART_GRID_COLOR }
425447
},
426448
x: { grid: { display: false } }
427449
}
@@ -483,7 +505,7 @@ function renderCategoryPie(transactions, categories) {
483505
datasets: [{
484506
data: visible.map(e => e.amount),
485507
backgroundColor: visible.map(e => e.color + 'cc'),
486-
borderColor: '#fff',
508+
borderColor: CHART_CARD_BG,
487509
borderWidth: 2
488510
}]
489511
},
@@ -607,7 +629,7 @@ function renderBySource(transactions) {
607629
y: {
608630
beginAtZero: true,
609631
ticks: { callback: v => formatCurrency(v) },
610-
grid: { color: '#E5E7EB' }
632+
grid: { color: CHART_GRID_COLOR }
611633
}
612634
}
613635
}

0 commit comments

Comments
 (0)