diff --git a/web/package-lock.json b/web/package-lock.json index ec5cecb..c848807 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -15,6 +15,7 @@ "@angular/forms": "^22.2.0", "@angular/platform-browser": "^22.2.0", "@angular/router": "^22.2.0", + "@fontsource-variable/inter": "^5.3.0", "@ng-icons/core": "^36.1.0", "@ng-icons/lucide": "^36.1.0", "@ngx-translate/core": "^18.0.0", @@ -1469,6 +1470,15 @@ } } }, + "node_modules/@fontsource-variable/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@harperfast/extended-iterable": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", diff --git a/web/package.json b/web/package.json index 98835b1..cd32bd3 100644 --- a/web/package.json +++ b/web/package.json @@ -20,6 +20,7 @@ "@angular/forms": "^22.2.0", "@angular/platform-browser": "^22.2.0", "@angular/router": "^22.2.0", + "@fontsource-variable/inter": "^5.3.0", "@ng-icons/core": "^36.1.0", "@ng-icons/lucide": "^36.1.0", "@ngx-translate/core": "^18.0.0", diff --git a/web/src/app/features/annual/annual.ts b/web/src/app/features/annual/annual.ts index 71181aa..a64b392 100644 --- a/web/src/app/features/annual/annual.ts +++ b/web/src/app/features/annual/annual.ts @@ -253,7 +253,7 @@ export class AnnualComponent { ), bar( t('kpi.variableExpenses'), - '#fb7185', + SERIES_COLORS.expensesLight, m.map((r) => active(r, r.month.variableExpenses)), ), bar( diff --git a/web/src/app/features/dashboard/dashboard.ts b/web/src/app/features/dashboard/dashboard.ts index 5844355..dc3d76a 100644 --- a/web/src/app/features/dashboard/dashboard.ts +++ b/web/src/app/features/dashboard/dashboard.ts @@ -597,7 +597,7 @@ import { StatusBadgeComponent } from '../../shared/status-badge';
-
{{ o.averageMonthlySavingsRate | pct }}
@@ -983,9 +983,9 @@ export class DashboardComponent { type: 'line', connectNulls: false, symbol: 'circle', - symbolSize: 6, + symbolSize: 5, data: months.map((r) => active(r.month, r.month.netBalance)), - lineStyle: { width: 2, color: SERIES_COLORS.net }, + lineStyle: { width: 1.5, type: 'dashed', color: SERIES_COLORS.net }, itemStyle: { color: SERIES_COLORS.net }, z: 3, }, diff --git a/web/src/app/features/net-worth/net-worth.ts b/web/src/app/features/net-worth/net-worth.ts index eb9694a..dffadba 100644 --- a/web/src/app/features/net-worth/net-worth.ts +++ b/web/src/app/features/net-worth/net-worth.ts @@ -340,7 +340,7 @@ export class NetWorthComponent { showSymbol: series.length < 3, areaStyle: { opacity: 0.12 }, data: series.map((p) => [p.date, p.netWorth]), - itemStyle: { color: SERIES_COLORS.net }, + itemStyle: { color: SERIES_COLORS.wealth }, }, { name: this.i18n.instant('netWorth.liabilities'), diff --git a/web/src/app/features/portfolio/portfolio.ts b/web/src/app/features/portfolio/portfolio.ts index 739a8c6..6d0f91d 100644 --- a/web/src/app/features/portfolio/portfolio.ts +++ b/web/src/app/features/portfolio/portfolio.ts @@ -28,6 +28,7 @@ import { Toasts } from '../../core/toast'; import { ChartComponent } from '../../shared/chart'; import { SERIES_COLORS, + ASSET_CLASS_COLORS, baseChart, categoryAxis, moneyAxis, @@ -64,15 +65,7 @@ import { type SortKey = 'value' | 'gain' | 'today' | 'weight'; -const CLASS_COLORS: Record = { - Stock: '#8b5cf6', - Etf: '#10b981', - Bond: '#0891b2', - Fund: '#84cc16', - Crypto: '#f59e0b', - Cash: '#a1a1aa', - Other: '#a855f7', -}; +const CLASS_COLORS: Record = ASSET_CLASS_COLORS; /** * Read-only view of broker data: nothing on this page can change a broker account. Coins held elsewhere (an @@ -857,7 +850,7 @@ export class PortfolioComponent { showSymbol: false, areaStyle: { opacity: 0.1 }, data: series.map((p) => [p.date, p.value]), - itemStyle: { color: SERIES_COLORS.net }, + itemStyle: { color: SERIES_COLORS.value }, }, { name: this.i18n.instant('portfolio.contributions'), @@ -865,7 +858,7 @@ export class PortfolioComponent { showSymbol: false, step: 'end', data: series.map((p) => [p.date, p.netContributions]), - itemStyle: { color: '#a1a1aa' }, + itemStyle: { color: SERIES_COLORS.muted }, lineStyle: { type: 'dashed' }, }, ], diff --git a/web/src/app/shared/category-label.spec.ts b/web/src/app/shared/category-label.spec.ts index dae8893..14e84e3 100644 --- a/web/src/app/shared/category-label.spec.ts +++ b/web/src/app/shared/category-label.spec.ts @@ -12,10 +12,14 @@ describe('categoryLabel', () => { }); it("shows the owner's name for a renamed built-in category", () => { - expect(categoryLabel(i18n, { key: 'gym', name: 'Ginásio & Padel', renamed: true })).toBe('Ginásio & Padel'); + expect(categoryLabel(i18n, { key: 'gym', name: 'Ginásio & Padel', renamed: true })).toBe( + 'Ginásio & Padel', + ); }); it('shows custom categories by name', () => { - expect(categoryLabel(i18n, { key: 'custom-side-project', name: 'Side project' })).toBe('Side project'); + expect(categoryLabel(i18n, { key: 'custom-side-project', name: 'Side project' })).toBe( + 'Side project', + ); }); }); diff --git a/web/src/app/shared/chart-options.ts b/web/src/app/shared/chart-options.ts index 04b80b9..2f664dd 100644 --- a/web/src/app/shared/chart-options.ts +++ b/web/src/app/shared/chart-options.ts @@ -1,14 +1,39 @@ import type { EChartsOption } from 'echarts'; +/** + * One meaning, one colour, on every page: money in and wealth are the brand green, spending is rose, investing is + * violet, saving is teal. Balances and references are neutral so they never compete with the series they summarise. + */ export const SERIES_COLORS = { income: '#059669', expenses: '#e11d48', + /** Second expense shade (e.g. variable vs fixed). */ + expensesLight: '#fb7185', invested: '#7c3aed', saved: '#0891b2', budget: '#a1a1aa', - net: '#f59e0b', + /** Net balance line drawn over income/expense bars: neutral, it summarises them. */ + net: '#71717a', + /** Portfolio value: the investing colour. */ + value: '#7c3aed', + /** Net worth: the brand green. */ + wealth: '#059669', + muted: '#a1a1aa', }; +/** Asset classes, from the same family: shares violet, funds indigo, bonds teal, crypto amber, cash grey. */ +export const ASSET_CLASS_COLORS = { + Stock: '#7c3aed', + Etf: '#059669', + Bond: '#0891b2', + Fund: '#4f46e5', + Crypto: '#d97706', + Cash: '#a1a1aa', + Other: '#db2777', +} as const; + +export const CHART_FONT = "'Inter Variable', Inter, ui-sans-serif, system-ui, sans-serif"; + const axisLabel = { color: '#a1a1aa', fontSize: 11 }; export function moneyAxis(locale: string): EChartsOption['yAxis'] { diff --git a/web/src/app/shared/chart.ts b/web/src/app/shared/chart.ts index a58faf9..1591a0a 100644 --- a/web/src/app/shared/chart.ts +++ b/web/src/app/shared/chart.ts @@ -15,6 +15,7 @@ import { NgIcon } from '@ng-icons/core'; import { TranslatePipe } from '@ngx-translate/core'; import { Prefs } from '../core/prefs'; import { APP_ICONS } from './icons'; +import { CHART_FONT } from './chart-options'; /** True when no series has a single non-zero value: nothing worth drawing (e.g. a year without movements). */ export function chartIsEmpty(option: EChartsOption): boolean { @@ -104,7 +105,11 @@ export class ChartComponent implements OnDestroy { }); (this.chart as unknown as { __theme?: string }).__theme = themeKey; } - this.chart.setOption({ backgroundColor: 'transparent', ...this.option() }, true); + // The app's font (ECharts draws its own text and does not inherit CSS). + this.chart.setOption( + { backgroundColor: 'transparent', textStyle: { fontFamily: CHART_FONT }, ...this.option() }, + true, + ); } ngOnDestroy() { diff --git a/web/src/app/shared/progress.ts b/web/src/app/shared/progress.ts index b0f8557..cccfda7 100644 --- a/web/src/app/shared/progress.ts +++ b/web/src/app/shared/progress.ts @@ -21,10 +21,6 @@ export class ProgressComponent { readonly barColor = computed(() => { if (this.tone() === 'brand') return 'var(--primary)'; const v = this.value(); - return v > 1 - ? 'var(--color-rose-500)' - : v >= 0.9 - ? 'var(--color-amber-500)' - : 'var(--primary)'; + return v > 1 ? 'var(--color-rose-500)' : v >= 0.9 ? 'var(--color-amber-500)' : 'var(--primary)'; }); } diff --git a/web/src/app/shell/shell.ts b/web/src/app/shell/shell.ts index 7c9e719..10ae2a7 100644 --- a/web/src/app/shell/shell.ts +++ b/web/src/app/shell/shell.ts @@ -135,8 +135,16 @@ interface NavItem { variant="ghost" size="icon" (click)="toggleTheme()" - [attr.aria-label]="'settings.theme' | translate" - [hlmTooltip]="'settings.theme' | translate" + [attr.aria-label]=" + ('settings.theme' | translate) + + ': ' + + ('settings.themes.' + prefs.theme() | translate) + " + [hlmTooltip]=" + ('settings.theme' | translate) + + ': ' + + ('settings.themes.' + prefs.theme() | translate) + " >