diff --git a/frontend/src/index.css b/frontend/src/index.css index 7ee97c9..cb4d240 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -23,6 +23,7 @@ --ring: 252 87% 67%; --radius: 0.6rem; --success: 142 71% 45%; + --warning: 38 92% 58%; } /* ─── Base font ─────────────────────────────────────────────────────────────── */ @@ -71,6 +72,7 @@ --ring: 252 87% 67%; --radius: 0.6rem; --success: 142 71% 45%; + --warning: 38 92% 58%; } /* ══════════════════════════════════════════════════════════════════════════════ @@ -97,6 +99,7 @@ --ring: 252 87% 55%; --radius: 0.6rem; --success: 142 71% 40%; + --warning: 38 90% 46%; } /* ══════════════════════════════════════════════════════════════════════════════ @@ -123,6 +126,7 @@ --ring: 252 87% 70%; --radius: 0.5rem; --success: 142 71% 45%; + --warning: 38 92% 62%; } /* ══════════════════════════════════════════════════════════════════════════════ @@ -149,6 +153,7 @@ --ring: 38 92% 50%; --radius: 0.4rem; --success: 142 55% 42%; + --warning: 38 92% 58%; } /* ══════════════════════════════════════════════════════════════════════════════ @@ -175,6 +180,7 @@ --ring: 120 100% 50%; --radius: 0.2rem; --success: 120 100% 50%; + --warning: 60 100% 55%; } .theme-terminal body, @@ -243,6 +249,7 @@ --ring: 330 100% 62%; --radius: 0.5rem; --success: 165 100% 45%; + --warning: 55 100% 65%; } .theme-synthwave h1, @@ -307,6 +314,7 @@ --ring: 0 65% 38%; --radius: 0.2rem; --success: 142 50% 32%; + --warning: 38 90% 46%; } .theme-ledger body, diff --git a/frontend/src/pages/accounts/AccountDetail.tsx b/frontend/src/pages/accounts/AccountDetail.tsx index 2ecc6d2..771cbec 100644 --- a/frontend/src/pages/accounts/AccountDetail.tsx +++ b/frontend/src/pages/accounts/AccountDetail.tsx @@ -95,13 +95,13 @@ export default function AccountDetail() {
Credit Utilisation - 80 ? "text-destructive" : utilPct > 50 ? "text-yellow-500" : "text-success")}> + 80 ? "text-destructive" : utilPct > 50 ? "text-warning" : "text-success")}> {utilPct.toFixed(0)}%
80 ? "bg-destructive" : utilPct > 50 ? "bg-yellow-500" : "bg-success")} + className={cn("h-full rounded-full transition-all", utilPct > 80 ? "bg-destructive" : utilPct > 50 ? "bg-warning" : "bg-success")} style={{ width: `${utilPct}%` }} />
@@ -298,7 +298,7 @@ function ImportModal({ {/* Detected format badge */}
{preview.detected_format ? ( <> Detected: {preview.detected_format} diff --git a/frontend/src/pages/accounts/AccountList.tsx b/frontend/src/pages/accounts/AccountList.tsx index b5a18e9..45e0a00 100644 --- a/frontend/src/pages/accounts/AccountList.tsx +++ b/frontend/src/pages/accounts/AccountList.tsx @@ -315,7 +315,7 @@ function AccountGroup({
80 ? "bg-destructive" : utilPct > 50 ? "bg-yellow-500" : "bg-success")} + className={cn("h-full rounded-full transition-all", utilPct > 80 ? "bg-destructive" : utilPct > 50 ? "bg-warning" : "bg-success")} style={{ width: `${utilPct}%` }} />
diff --git a/frontend/src/pages/budgets/BudgetPage.tsx b/frontend/src/pages/budgets/BudgetPage.tsx index 3bf6d33..170cbca 100644 --- a/frontend/src/pages/budgets/BudgetPage.tsx +++ b/frontend/src/pages/budgets/BudgetPage.tsx @@ -12,7 +12,11 @@ function RadialGauge({ percent, size = 80 }: { percent: number; size?: number }) const circumference = 2 * Math.PI * r; const clamped = Math.min(percent, 100); const offset = circumference - (clamped / 100) * circumference; - const color = percent >= 100 ? "#ef4444" : percent >= 80 ? "#f97316" : "#22c55e"; + const color = percent >= 100 + ? "hsl(var(--destructive))" + : percent >= 80 + ? "hsl(var(--warning))" + : "hsl(var(--success))"; return ( @@ -105,7 +109,7 @@ export default function BudgetPage() { · {overBudget} over budget )} {alerted > 0 && ( - · {alerted} near limit + · {alerted} near limit )}

@@ -139,7 +143,7 @@ export default function BudgetPage() { item.is_over_budget ? "border-destructive/50" : item.alert_triggered - ? "border-orange-500/50" + ? "border-warning/50" : "border-border" )} > @@ -170,7 +174,7 @@ export default function BudgetPage() { {item.is_over_budget ? ( ) : item.alert_triggered ? ( - + ) : ( )} diff --git a/frontend/src/pages/dashboard/Dashboard.tsx b/frontend/src/pages/dashboard/Dashboard.tsx index a530470..ddaba24 100644 --- a/frontend/src/pages/dashboard/Dashboard.tsx +++ b/frontend/src/pages/dashboard/Dashboard.tsx @@ -53,13 +53,13 @@ export default function Dashboard() { {/* 2FA nudge */} {!totpEnabled && ( -
- +
+

- Enable two-factor authentication + Enable two-factor authentication to secure your account.

- + Set up 2FA
@@ -111,14 +111,14 @@ export default function Dashboard() { ({ date: p.date, value: Number(p.net_worth) }))}> - - + + - - `£${(v/1000).toFixed(0)}k`} width={45} /> + + `£${(v/1000).toFixed(0)}k`} width={45} /> formatCurrency(v, nwReport.base_currency)} /> - + ) : ( @@ -136,8 +136,8 @@ export default function Dashboard() { {ieReport && ieReport.points.length > 0 ? ( ({ month: p.month, income: Number(p.income), expenses: Number(p.expenses) }))}> - - `£${(v/1000).toFixed(0)}k`} width={45} /> + + `£${(v/1000).toFixed(0)}k`} width={45} /> formatCurrency(v, "GBP")} /> diff --git a/frontend/src/pages/investments/AssetDetail.tsx b/frontend/src/pages/investments/AssetDetail.tsx index 9898267..d1827b5 100644 --- a/frontend/src/pages/investments/AssetDetail.tsx +++ b/frontend/src/pages/investments/AssetDetail.tsx @@ -6,6 +6,7 @@ import { useUiStore } from "@/store/uiStore"; import { cn } from "@/utils/cn"; import { ArrowLeft, TrendingUp, TrendingDown } from "lucide-react"; import Plot from "react-plotly.js"; +import { cssVar } from "@/utils/cssVar"; export default function AssetDetail() { const { assetId } = useParams<{ assetId: string }>(); @@ -152,8 +153,8 @@ export default function AssetDetail() { high: highs as number[], low: lows as number[], close: closes as number[], - increasing: { line: { color: "#22c55e" } }, - decreasing: { line: { color: "#ef4444" } }, + increasing: { line: { color: cssVar("--success") } }, + decreasing: { line: { color: cssVar("--destructive") } }, name: holding?.symbol ?? "Price", }, { @@ -161,16 +162,16 @@ export default function AssetDetail() { x: dates, y: volumes as number[], yaxis: "y2", - marker: { color: "rgba(99,102,241,0.3)" }, + marker: { color: cssVar("--primary", 0.3) }, name: "Volume", }, ]} layout={{ paper_bgcolor: "transparent", plot_bgcolor: "transparent", - font: { color: "var(--muted-foreground)", size: 11 }, - xaxis: { rangeslider: { visible: false }, gridcolor: "var(--border)", showgrid: true }, - yaxis: { gridcolor: "var(--border)", showgrid: true, domain: [0.25, 1] }, + font: { color: cssVar("--muted-foreground"), size: 11 }, + xaxis: { rangeslider: { visible: false }, gridcolor: cssVar("--border"), showgrid: true }, + yaxis: { gridcolor: cssVar("--border"), showgrid: true, domain: [0.25, 1] }, yaxis2: { domain: [0, 0.2], showgrid: false }, margin: { t: 10, r: 10, b: 40, l: 60 }, showlegend: false, diff --git a/frontend/src/pages/predictions/PredictionsPage.tsx b/frontend/src/pages/predictions/PredictionsPage.tsx index 42cbad3..9317ffa 100644 --- a/frontend/src/pages/predictions/PredictionsPage.tsx +++ b/frontend/src/pages/predictions/PredictionsPage.tsx @@ -12,6 +12,7 @@ import { XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, ReferenceLine, } from "recharts"; import Plot from "react-plotly.js"; +import { cssVar } from "@/utils/cssVar"; const TABS = [ { id: "spending", label: "Spending", icon: BarChart3 }, @@ -105,11 +106,11 @@ function SpendingTab() {
- - `£${v}`} width={55} /> + + `£${v}`} width={55} /> formatCurrency(v, "GBP")} /> - - + + @@ -146,13 +147,13 @@ function BudgetAlerts() {
{f.category_name} - 0.75 ? "text-destructive" : "text-yellow-500")}> + 0.75 ? "text-destructive" : "text-warning")}> {(f.probability_overspend * 100).toFixed(0)}% overspend risk
0.75 ? "bg-destructive" : "bg-yellow-500")} + className={cn("h-full rounded-full", f.probability_overspend > 0.75 ? "bg-destructive" : "bg-warning")} style={{ width: `${Math.min(100, forecastPct)}%` }} />
@@ -238,12 +239,12 @@ function NetWorthTab() {

Net Worth Projection

- - `£${(v / 1000).toFixed(0)}k`} width={55} /> + + `£${(v / 1000).toFixed(0)}k`} width={55} /> formatCurrency(v, "GBP")} /> - {lastHistory && } - + {lastHistory && } + @@ -342,8 +343,8 @@ function MonteCarloTab() { x: data.percentiles.p90.map(p => p.date), y: data.percentiles.p90.map(p => p.value), fill: "tonexty", - fillcolor: "rgba(99,102,241,0.15)", - line: { color: "#6366f1", width: 1 }, + fillcolor: cssVar("--primary", 0.15), + line: { color: cssVar("--primary"), width: 1 }, name: "P90", mode: "lines", }, @@ -352,8 +353,8 @@ function MonteCarloTab() { x: data.percentiles.p75.map(p => p.date), y: data.percentiles.p75.map(p => p.value), fill: "tonexty", - fillcolor: "rgba(99,102,241,0.2)", - line: { color: "#6366f1", width: 1 }, + fillcolor: cssVar("--primary", 0.2), + line: { color: cssVar("--primary"), width: 1 }, name: "P75", mode: "lines", }, @@ -361,7 +362,7 @@ function MonteCarloTab() { type: "scatter" as const, x: data.percentiles.p50.map(p => p.date), y: data.percentiles.p50.map(p => p.value), - line: { color: "#22c55e", width: 2.5 }, + line: { color: cssVar("--success"), width: 2.5 }, name: "P50 (Median)", mode: "lines", }, @@ -370,8 +371,8 @@ function MonteCarloTab() { x: data.percentiles.p25.map(p => p.date), y: data.percentiles.p25.map(p => p.value), fill: "tonexty", - fillcolor: "rgba(239,68,68,0.1)", - line: { color: "#ef4444", width: 1 }, + fillcolor: cssVar("--destructive", 0.1), + line: { color: cssVar("--destructive"), width: 1 }, name: "P25", mode: "lines", }, @@ -380,8 +381,8 @@ function MonteCarloTab() { x: data.percentiles.p10.map(p => p.date), y: data.percentiles.p10.map(p => p.value), fill: "tonexty", - fillcolor: "rgba(239,68,68,0.15)", - line: { color: "#ef4444", width: 1 }, + fillcolor: cssVar("--destructive", 0.15), + line: { color: cssVar("--destructive"), width: 1 }, name: "P10", mode: "lines", }, @@ -389,10 +390,10 @@ function MonteCarloTab() { layout={{ paper_bgcolor: "transparent", plot_bgcolor: "transparent", - font: { color: "var(--muted-foreground)", size: 11 }, - xaxis: { gridcolor: "var(--border)", showgrid: true }, + font: { color: cssVar("--muted-foreground"), size: 11 }, + xaxis: { gridcolor: cssVar("--border"), showgrid: true }, yaxis: { - gridcolor: "var(--border)", + gridcolor: cssVar("--border"), showgrid: true, tickformat: "£,.0f", }, @@ -461,15 +462,15 @@ function CashFlowTab() { - - + + - v.slice(5)} /> - `£${(v / 1000).toFixed(1)}k`} width={55} /> + v.slice(5)} /> + `£${(v / 1000).toFixed(1)}k`} width={55} /> formatCurrency(v, "GBP")} /> - - + +
diff --git a/frontend/src/pages/reports/ReportsPage.tsx b/frontend/src/pages/reports/ReportsPage.tsx index fa2abf3..6524bdc 100644 --- a/frontend/src/pages/reports/ReportsPage.tsx +++ b/frontend/src/pages/reports/ReportsPage.tsx @@ -249,15 +249,15 @@ function NetWorthTab() { ({ ...p, net_worth: Number(p.net_worth), total_assets: Number(p.total_assets), total_liabilities: Number(p.total_liabilities) }))}> - - + + - - - `£${(v/1000).toFixed(0)}k`} /> + + + `£${(v/1000).toFixed(0)}k`} /> formatCurrency(v, data.base_currency)} /> - + @@ -287,9 +287,9 @@ function IncomeExpenseTab() {

Monthly Income vs Expenses

- - - `£${(v/1000).toFixed(0)}k`} /> + + + `£${(v/1000).toFixed(0)}k`} /> formatCurrency(v, data.currency)} /> @@ -340,15 +340,15 @@ function CashFlowTab() {

Daily Cash Flow — Last 30 Days

- - - `£${(v/1000).toFixed(1)}k`} /> - `£${(v/1000).toFixed(1)}k`} /> + + + `£${(v/1000).toFixed(1)}k`} /> + `£${(v/1000).toFixed(1)}k`} /> formatCurrency(v, data.currency)} /> - +
@@ -404,15 +404,15 @@ function SavingsRateTab() {

Savings Rate by Month

- - - `£${(v/1000).toFixed(0)}k`} /> - `${v}%`} /> + + + `£${(v/1000).toFixed(0)}k`} /> + `${v}%`} /> name === "Savings Rate %" ? `${v.toFixed(1)}%` : formatCurrency(v, data.currency)} /> - +
@@ -580,12 +580,12 @@ function BudgetVsActualTab() {

Budget vs Actual Spending

- - `£${v}`} /> - + + `£${v}`} /> + formatCurrency(v, data.currency)} /> - + @@ -614,9 +614,9 @@ function SpendingTrendsTab() {

Spending by Category (6 months)

- - - `£${v}`} /> + + + `£${v}`} /> formatCurrency(v, data.currency)} /> {data.categories.slice(0, 8).map((cat, i) => ( @@ -674,9 +674,9 @@ function InvestmentsTab() {

Holdings Value

- - `£${(v/1000).toFixed(0)}k`} /> - + + `£${(v/1000).toFixed(0)}k`} /> + formatCurrency(v, perf.currency)} /> {holdingsData.map((entry, i) => ( diff --git a/frontend/src/pages/settings/SettingsPage.tsx b/frontend/src/pages/settings/SettingsPage.tsx index f8941c7..9e4dd9d 100644 --- a/frontend/src/pages/settings/SettingsPage.tsx +++ b/frontend/src/pages/settings/SettingsPage.tsx @@ -234,7 +234,7 @@ function PasswordCard() {
{[1,2,3,4].map(i => { const score = Math.min(4, Math.floor(next.length / 3)); - return
; + return
; })}
)} diff --git a/frontend/src/utils/cssVar.ts b/frontend/src/utils/cssVar.ts new file mode 100644 index 0000000..f3bcf8b --- /dev/null +++ b/frontend/src/utils/cssVar.ts @@ -0,0 +1,21 @@ +/** + * Reads a CSS custom property from the root element and returns a valid colour + * string suitable for Plotly (which does not resolve CSS var() references). + * + * The project stores HSL components without hsl() — e.g. "--primary: 252 87% 67%". + * This converts them to "hsl(252, 87%, 67%)" or "hsla(..., alpha)" for Plotly. + */ +export function cssVar(name: string, alpha?: number): string { + const raw = getComputedStyle(document.documentElement) + .getPropertyValue(name) + .trim(); + + const parts = raw.split(/\s+/); + if (parts.length === 3) { + return alpha !== undefined + ? `hsla(${parts[0]}, ${parts[1]}, ${parts[2]}, ${alpha})` + : `hsl(${parts[0]}, ${parts[1]}, ${parts[2]})`; + } + + return raw; +}