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';
-
-
+
{{ 'kpi.avgSavingsRate' | translate }}
- {{ 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)
+ "
>