fix: theme consistency — chart colours, axis readability, warning system

- Recharts axes: add fill to tick objects so labels are visible on dark themes
- Recharts axes: wrap stroke/gridcolor in hsl() so var() resolves to valid colour
- Chart primary lines/gradients: replace hardcoded #6366f1 with hsl(var(--primary))
  so charts adopt each theme's accent (gold on Vault, green on Terminal, etc.)
- Plotly charts: add cssVar() helper (reads getComputedStyle) to pass actual
  computed colour strings instead of unresolved var() references
- Budget radial gauge: use hsl(var(--destructive/success/warning)) for SVG colours
- Add --warning CSS variable to all 7 themes with per-theme appropriate values;
  wire into Tailwind config as themed colour
- Replace all text-yellow-500 / text-orange-500 / bg-yellow-500 with text-warning
  / bg-warning across Dashboard, Budget, Account, Predictions, Settings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-04-23 12:58:06 +00:00
parent da59fa9f23
commit 0b326cbd87
10 changed files with 118 additions and 83 deletions

View file

@ -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,

View file

@ -95,13 +95,13 @@ export default function AccountDetail() {
<div className="bg-card border border-border rounded-xl p-4">
<div className="flex justify-between text-sm mb-2">
<span className="font-medium">Credit Utilisation</span>
<span className={cn("font-semibold", utilPct > 80 ? "text-destructive" : utilPct > 50 ? "text-yellow-500" : "text-success")}>
<span className={cn("font-semibold", utilPct > 80 ? "text-destructive" : utilPct > 50 ? "text-warning" : "text-success")}>
{utilPct.toFixed(0)}%
</span>
</div>
<div className="h-2 bg-secondary rounded-full overflow-hidden">
<div
className={cn("h-full rounded-full transition-all", utilPct > 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}%` }}
/>
</div>
@ -298,7 +298,7 @@ function ImportModal({
{/* Detected format badge */}
<div className={cn(
"flex items-center gap-2 px-3 py-2 rounded-lg text-sm",
preview.detected_format ? "bg-success/10 text-success" : "bg-yellow-500/10 text-yellow-600"
preview.detected_format ? "bg-success/10 text-success" : "bg-warning/10 text-warning"
)}>
{preview.detected_format ? (
<><CheckCircle className="w-4 h-4 shrink-0" /> Detected: <strong>{preview.detected_format}</strong></>

View file

@ -315,7 +315,7 @@ function AccountGroup({
</div>
<div className="h-1.5 bg-secondary rounded-full overflow-hidden">
<div
className={cn("h-full rounded-full transition-all", utilPct > 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}%` }}
/>
</div>

View file

@ -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 (
<svg width={size} height={size} className="shrink-0">
@ -105,7 +109,7 @@ export default function BudgetPage() {
<span className="ml-2 text-destructive font-medium">· {overBudget} over budget</span>
)}
{alerted > 0 && (
<span className="ml-2 text-orange-500 font-medium">· {alerted} near limit</span>
<span className="ml-2 text-warning font-medium">· {alerted} near limit</span>
)}
</p>
</div>
@ -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 ? (
<AlertTriangle className="w-3.5 h-3.5 text-destructive shrink-0" />
) : item.alert_triggered ? (
<AlertTriangle className="w-3.5 h-3.5 text-orange-500 shrink-0" />
<AlertTriangle className="w-3.5 h-3.5 text-warning shrink-0" />
) : (
<CheckCircle className="w-3.5 h-3.5 text-success shrink-0" />
)}

View file

@ -53,13 +53,13 @@ export default function Dashboard() {
{/* 2FA nudge */}
{!totpEnabled && (
<div className="flex items-center gap-3 bg-yellow-500/10 border border-yellow-500/30 rounded-xl px-4 py-3">
<ShieldAlert className="w-5 h-5 text-yellow-500 shrink-0" />
<div className="flex items-center gap-3 bg-warning/10 border border-warning/30 rounded-xl px-4 py-3">
<ShieldAlert className="w-5 h-5 text-warning shrink-0" />
<p className="flex-1 text-sm">
<span className="font-medium text-yellow-500">Enable two-factor authentication</span>
<span className="font-medium text-warning">Enable two-factor authentication</span>
<span className="text-muted-foreground ml-1">to secure your account.</span>
</p>
<Link to="/security/totp" className="text-xs text-yellow-500 underline underline-offset-2 shrink-0">
<Link to="/security/totp" className="text-xs text-warning underline underline-offset-2 shrink-0">
Set up 2FA
</Link>
</div>
@ -111,14 +111,14 @@ export default function Dashboard() {
<AreaChart data={nwReport.points.map(p => ({ date: p.date, value: Number(p.net_worth) }))}>
<defs>
<linearGradient id="nwGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
<stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.3} />
<stop offset="95%" stopColor="hsl(var(--primary))" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis dataKey="date" tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" />
<YAxis tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" tickFormatter={v => `£${(v/1000).toFixed(0)}k`} width={45} />
<XAxis dataKey="date" tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" />
<YAxis tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={v => `£${(v/1000).toFixed(0)}k`} width={45} />
<Tooltip formatter={(v: number) => formatCurrency(v, nwReport.base_currency)} />
<Area type="monotone" dataKey="value" stroke="#6366f1" fill="url(#nwGrad)" strokeWidth={2} />
<Area type="monotone" dataKey="value" stroke="hsl(var(--primary))" fill="url(#nwGrad)" strokeWidth={2} />
</AreaChart>
</ResponsiveContainer>
) : (
@ -136,8 +136,8 @@ export default function Dashboard() {
{ieReport && ieReport.points.length > 0 ? (
<ResponsiveContainer width="100%" height={180}>
<BarChart data={ieReport.points.map(p => ({ month: p.month, income: Number(p.income), expenses: Number(p.expenses) }))}>
<XAxis dataKey="month" tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" />
<YAxis tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" tickFormatter={v => `£${(v/1000).toFixed(0)}k`} width={45} />
<XAxis dataKey="month" tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" />
<YAxis tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={v => `£${(v/1000).toFixed(0)}k`} width={45} />
<Tooltip formatter={(v: number) => formatCurrency(v, "GBP")} />
<Bar dataKey="income" fill="#22c55e" radius={[2,2,0,0]} name="Income" />
<Bar dataKey="expenses" fill="#ef4444" radius={[2,2,0,0]} name="Expenses" />

View file

@ -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,

View file

@ -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() {
</div>
<ResponsiveContainer width="100%" height={260}>
<BarChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
<XAxis dataKey="date" tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" />
<YAxis tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" tickFormatter={v => `£${v}`} width={55} />
<XAxis dataKey="date" tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" />
<YAxis tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={v => `£${v}`} width={55} />
<Tooltip formatter={(v: number) => formatCurrency(v, "GBP")} />
<Bar dataKey="actual" fill="#6366f1" name="Actual" radius={[2, 2, 0, 0]} />
<Bar dataKey="forecast" fill="#6366f180" name="Forecast" radius={[2, 2, 0, 0]} />
<Bar dataKey="actual" fill="hsl(var(--primary))" name="Actual" radius={[2, 2, 0, 0]} />
<Bar dataKey="forecast" fill="hsl(var(--primary) / 0.5)" name="Forecast" radius={[2, 2, 0, 0]} />
</BarChart>
</ResponsiveContainer>
@ -146,13 +147,13 @@ function BudgetAlerts() {
<div key={f.category_id}>
<div className="flex justify-between text-sm mb-1">
<span className="font-medium">{f.category_name}</span>
<span className={cn("text-xs font-medium", f.probability_overspend > 0.75 ? "text-destructive" : "text-yellow-500")}>
<span className={cn("text-xs font-medium", f.probability_overspend > 0.75 ? "text-destructive" : "text-warning")}>
{(f.probability_overspend * 100).toFixed(0)}% overspend risk
</span>
</div>
<div className="h-2 bg-secondary rounded-full overflow-hidden relative">
<div
className={cn("h-full rounded-full", f.probability_overspend > 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)}%` }}
/>
<div className="absolute top-0 h-full w-0.5 bg-foreground/40" style={{ left: "100%" }} />
@ -238,12 +239,12 @@ function NetWorthTab() {
<p className="text-sm font-semibold mb-4">Net Worth Projection</p>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
<XAxis dataKey="date" tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" interval="preserveStartEnd" />
<YAxis tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" tickFormatter={v => `£${(v / 1000).toFixed(0)}k`} width={55} />
<XAxis dataKey="date" tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" interval="preserveStartEnd" />
<YAxis tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={v => `£${(v / 1000).toFixed(0)}k`} width={55} />
<Tooltip formatter={(v: number) => formatCurrency(v, "GBP")} />
<Legend />
{lastHistory && <ReferenceLine x={lastHistory.date} stroke="var(--border)" strokeDasharray="4 2" label={{ value: "Today", fontSize: 10 }} />}
<Line type="monotone" dataKey="history" stroke="#6366f1" strokeWidth={2} dot={false} name="History" />
{lastHistory && <ReferenceLine x={lastHistory.date} stroke="hsl(var(--border))" strokeDasharray="4 2" label={{ value: "Today", fontSize: 10 }} />}
<Line type="monotone" dataKey="history" stroke="hsl(var(--primary))" strokeWidth={2} dot={false} name="History" />
<Line type="monotone" dataKey="conservative" stroke="#ef4444" strokeWidth={1.5} strokeDasharray="4 2" dot={false} name="Conservative" />
<Line type="monotone" dataKey="base" stroke="#22c55e" strokeWidth={2} strokeDasharray="4 2" dot={false} name="Base" />
<Line type="monotone" dataKey="optimistic" stroke="#f59e0b" strokeWidth={1.5} strokeDasharray="4 2" dot={false} name="Optimistic" />
@ -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() {
<AreaChart data={data.forecast} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
<defs>
<linearGradient id="balanceGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
<stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.3} />
<stop offset="95%" stopColor="hsl(var(--primary))" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis dataKey="date" tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" tickFormatter={v => v.slice(5)} />
<YAxis tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" tickFormatter={v => `£${(v / 1000).toFixed(1)}k`} width={55} />
<XAxis dataKey="date" tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={v => v.slice(5)} />
<YAxis tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={v => `£${(v / 1000).toFixed(1)}k`} width={55} />
<Tooltip formatter={(v: number) => formatCurrency(v, "GBP")} />
<ReferenceLine y={0} stroke="#ef4444" strokeDasharray="4 2" />
<Area type="monotone" dataKey="balance" stroke="#6366f1" fill="url(#balanceGrad)" strokeWidth={2} name="Balance" />
<ReferenceLine y={0} stroke="hsl(var(--destructive))" strokeDasharray="4 2" />
<Area type="monotone" dataKey="balance" stroke="hsl(var(--primary))" fill="url(#balanceGrad)" strokeWidth={2} name="Balance" />
</AreaChart>
</ResponsiveContainer>
</div>

View file

@ -249,15 +249,15 @@ function NetWorthTab() {
<AreaChart data={data.points.map(p => ({ ...p, net_worth: Number(p.net_worth), total_assets: Number(p.total_assets), total_liabilities: Number(p.total_liabilities) }))}>
<defs>
<linearGradient id="nwGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
<stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.3} />
<stop offset="95%" stopColor="hsl(var(--primary))" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis dataKey="date" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" />
<YAxis tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `£${(v/1000).toFixed(0)}k`} />
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis dataKey="date" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" />
<YAxis tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={(v) => `£${(v/1000).toFixed(0)}k`} />
<Tooltip formatter={(v: number) => formatCurrency(v, data.base_currency)} />
<Area type="monotone" dataKey="net_worth" stroke="#6366f1" fill="url(#nwGrad)" strokeWidth={2} name="Net Worth" />
<Area type="monotone" dataKey="net_worth" stroke="hsl(var(--primary))" fill="url(#nwGrad)" strokeWidth={2} name="Net Worth" />
<Area type="monotone" dataKey="total_assets" stroke="#22c55e" fill="none" strokeWidth={1.5} strokeDasharray="4 2" name="Assets" />
<Area type="monotone" dataKey="total_liabilities" stroke="#ef4444" fill="none" strokeWidth={1.5} strokeDasharray="4 2" name="Liabilities" />
</AreaChart>
@ -287,9 +287,9 @@ function IncomeExpenseTab() {
<p className="text-sm font-medium mb-4">Monthly Income vs Expenses</p>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis dataKey="month" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" />
<YAxis tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `£${(v/1000).toFixed(0)}k`} />
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis dataKey="month" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" />
<YAxis tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={(v) => `£${(v/1000).toFixed(0)}k`} />
<Tooltip formatter={(v: number) => formatCurrency(v, data.currency)} />
<Legend />
<Bar dataKey="income" fill="#22c55e" name="Income" radius={[2, 2, 0, 0]} />
@ -340,15 +340,15 @@ function CashFlowTab() {
<p className="text-sm font-medium mb-4">Daily Cash Flow Last 30 Days</p>
<ResponsiveContainer width="100%" height={320}>
<ComposedChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis dataKey="date" tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" />
<YAxis yAxisId="bars" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `£${(v/1000).toFixed(1)}k`} />
<YAxis yAxisId="line" orientation="right" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `£${(v/1000).toFixed(1)}k`} />
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis dataKey="date" tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" />
<YAxis yAxisId="bars" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={(v) => `£${(v/1000).toFixed(1)}k`} />
<YAxis yAxisId="line" orientation="right" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={(v) => `£${(v/1000).toFixed(1)}k`} />
<Tooltip formatter={(v: number) => formatCurrency(v, data.currency)} />
<Legend />
<Bar yAxisId="bars" dataKey="inflow" fill="#22c55e" name="Inflow" radius={[2, 2, 0, 0]} />
<Bar yAxisId="bars" dataKey="outflow" fill="#ef4444" name="Outflow" radius={[2, 2, 0, 0]} />
<Line yAxisId="line" type="monotone" dataKey="balance" stroke="#6366f1" strokeWidth={2} dot={false} name="Running Balance" />
<Line yAxisId="line" type="monotone" dataKey="balance" stroke="hsl(var(--primary))" strokeWidth={2} dot={false} name="Running Balance" />
</ComposedChart>
</ResponsiveContainer>
</div>
@ -404,15 +404,15 @@ function SavingsRateTab() {
<p className="text-sm font-medium mb-4">Savings Rate by Month</p>
<ResponsiveContainer width="100%" height={320}>
<ComposedChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis dataKey="month" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" />
<YAxis yAxisId="bars" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `£${(v/1000).toFixed(0)}k`} />
<YAxis yAxisId="rate" orientation="right" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `${v}%`} />
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis dataKey="month" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" />
<YAxis yAxisId="bars" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={(v) => `£${(v/1000).toFixed(0)}k`} />
<YAxis yAxisId="rate" orientation="right" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={(v) => `${v}%`} />
<Tooltip formatter={(v: number, name: string) => name === "Savings Rate %" ? `${v.toFixed(1)}%` : formatCurrency(v, data.currency)} />
<Legend />
<Bar yAxisId="bars" dataKey="income" fill="#22c55e" name="Income" radius={[2, 2, 0, 0]} />
<Bar yAxisId="bars" dataKey="expenses" fill="#ef4444" name="Expenses" radius={[2, 2, 0, 0]} />
<Line yAxisId="rate" type="monotone" dataKey="rate" stroke="#6366f1" strokeWidth={2.5} dot={{ r: 3 }} name="Savings Rate %" />
<Line yAxisId="rate" type="monotone" dataKey="rate" stroke="hsl(var(--primary))" strokeWidth={2.5} dot={{ r: 3 }} name="Savings Rate %" />
</ComposedChart>
</ResponsiveContainer>
</div>
@ -580,12 +580,12 @@ function BudgetVsActualTab() {
<p className="text-sm font-medium mb-4">Budget vs Actual Spending</p>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={chartData} layout="vertical">
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis type="number" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `£${v}`} />
<YAxis type="category" dataKey="name" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" width={120} />
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis type="number" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={(v) => `£${v}`} />
<YAxis type="category" dataKey="name" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" width={120} />
<Tooltip formatter={(v: number) => formatCurrency(v, data.currency)} />
<Legend />
<Bar dataKey="budgeted" fill="#6366f1" name="Budgeted" radius={[0, 2, 2, 0]} />
<Bar dataKey="budgeted" fill="hsl(var(--primary))" name="Budgeted" radius={[0, 2, 2, 0]} />
<Bar dataKey="actual" fill="#f97316" name="Actual" radius={[0, 2, 2, 0]} />
</BarChart>
</ResponsiveContainer>
@ -614,9 +614,9 @@ function SpendingTrendsTab() {
<p className="text-sm font-medium mb-4">Spending by Category (6 months)</p>
<ResponsiveContainer width="100%" height={320}>
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis dataKey="month" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" />
<YAxis tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `£${v}`} />
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis dataKey="month" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" />
<YAxis tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={(v) => `£${v}`} />
<Tooltip formatter={(v: number) => formatCurrency(v, data.currency)} />
<Legend />
{data.categories.slice(0, 8).map((cat, i) => (
@ -674,9 +674,9 @@ function InvestmentsTab() {
<p className="text-sm font-medium mb-4">Holdings Value</p>
<ResponsiveContainer width="100%" height={220}>
<BarChart data={holdingsData} layout="vertical">
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis type="number" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `£${(v/1000).toFixed(0)}k`} />
<YAxis type="category" dataKey="name" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" width={60} />
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis type="number" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={(v) => `£${(v/1000).toFixed(0)}k`} />
<YAxis type="category" dataKey="name" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" width={60} />
<Tooltip formatter={(v: number) => formatCurrency(v, perf.currency)} />
<Bar dataKey="value" name="Current Value" radius={[0, 3, 3, 0]}>
{holdingsData.map((entry, i) => (

View file

@ -234,7 +234,7 @@ function PasswordCard() {
<div className="mt-2 flex gap-1">
{[1,2,3,4].map(i => {
const score = Math.min(4, Math.floor(next.length / 3));
return <div key={i} className={cn("h-1 flex-1 rounded-full transition-colors", i <= score ? (score <= 1 ? "bg-destructive" : score <= 2 ? "bg-yellow-500" : score <= 3 ? "bg-primary" : "bg-success") : "bg-secondary")} />;
return <div key={i} className={cn("h-1 flex-1 rounded-full transition-colors", i <= score ? (score <= 1 ? "bg-destructive" : score <= 2 ? "bg-warning" : score <= 3 ? "bg-primary" : "bg-success") : "bg-secondary")} />;
})}
</div>
)}

View file

@ -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;
}