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:
parent
da59fa9f23
commit
0b326cbd87
10 changed files with 118 additions and 83 deletions
|
|
@ -23,6 +23,7 @@
|
||||||
--ring: 252 87% 67%;
|
--ring: 252 87% 67%;
|
||||||
--radius: 0.6rem;
|
--radius: 0.6rem;
|
||||||
--success: 142 71% 45%;
|
--success: 142 71% 45%;
|
||||||
|
--warning: 38 92% 58%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Base font ─────────────────────────────────────────────────────────────── */
|
/* ─── Base font ─────────────────────────────────────────────────────────────── */
|
||||||
|
|
@ -71,6 +72,7 @@
|
||||||
--ring: 252 87% 67%;
|
--ring: 252 87% 67%;
|
||||||
--radius: 0.6rem;
|
--radius: 0.6rem;
|
||||||
--success: 142 71% 45%;
|
--success: 142 71% 45%;
|
||||||
|
--warning: 38 92% 58%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ══════════════════════════════════════════════════════════════════════════════
|
/* ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -97,6 +99,7 @@
|
||||||
--ring: 252 87% 55%;
|
--ring: 252 87% 55%;
|
||||||
--radius: 0.6rem;
|
--radius: 0.6rem;
|
||||||
--success: 142 71% 40%;
|
--success: 142 71% 40%;
|
||||||
|
--warning: 38 90% 46%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ══════════════════════════════════════════════════════════════════════════════
|
/* ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -123,6 +126,7 @@
|
||||||
--ring: 252 87% 70%;
|
--ring: 252 87% 70%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
--success: 142 71% 45%;
|
--success: 142 71% 45%;
|
||||||
|
--warning: 38 92% 62%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ══════════════════════════════════════════════════════════════════════════════
|
/* ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -149,6 +153,7 @@
|
||||||
--ring: 38 92% 50%;
|
--ring: 38 92% 50%;
|
||||||
--radius: 0.4rem;
|
--radius: 0.4rem;
|
||||||
--success: 142 55% 42%;
|
--success: 142 55% 42%;
|
||||||
|
--warning: 38 92% 58%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ══════════════════════════════════════════════════════════════════════════════
|
/* ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -175,6 +180,7 @@
|
||||||
--ring: 120 100% 50%;
|
--ring: 120 100% 50%;
|
||||||
--radius: 0.2rem;
|
--radius: 0.2rem;
|
||||||
--success: 120 100% 50%;
|
--success: 120 100% 50%;
|
||||||
|
--warning: 60 100% 55%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-terminal body,
|
.theme-terminal body,
|
||||||
|
|
@ -243,6 +249,7 @@
|
||||||
--ring: 330 100% 62%;
|
--ring: 330 100% 62%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
--success: 165 100% 45%;
|
--success: 165 100% 45%;
|
||||||
|
--warning: 55 100% 65%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-synthwave h1,
|
.theme-synthwave h1,
|
||||||
|
|
@ -307,6 +314,7 @@
|
||||||
--ring: 0 65% 38%;
|
--ring: 0 65% 38%;
|
||||||
--radius: 0.2rem;
|
--radius: 0.2rem;
|
||||||
--success: 142 50% 32%;
|
--success: 142 50% 32%;
|
||||||
|
--warning: 38 90% 46%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-ledger body,
|
.theme-ledger body,
|
||||||
|
|
|
||||||
|
|
@ -95,13 +95,13 @@ export default function AccountDetail() {
|
||||||
<div className="bg-card border border-border rounded-xl p-4">
|
<div className="bg-card border border-border rounded-xl p-4">
|
||||||
<div className="flex justify-between text-sm mb-2">
|
<div className="flex justify-between text-sm mb-2">
|
||||||
<span className="font-medium">Credit Utilisation</span>
|
<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)}%
|
{utilPct.toFixed(0)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 bg-secondary rounded-full overflow-hidden">
|
<div className="h-2 bg-secondary rounded-full overflow-hidden">
|
||||||
<div
|
<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}%` }}
|
style={{ width: `${utilPct}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -298,7 +298,7 @@ function ImportModal({
|
||||||
{/* Detected format badge */}
|
{/* Detected format badge */}
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"flex items-center gap-2 px-3 py-2 rounded-lg text-sm",
|
"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 ? (
|
{preview.detected_format ? (
|
||||||
<><CheckCircle className="w-4 h-4 shrink-0" /> Detected: <strong>{preview.detected_format}</strong></>
|
<><CheckCircle className="w-4 h-4 shrink-0" /> Detected: <strong>{preview.detected_format}</strong></>
|
||||||
|
|
|
||||||
|
|
@ -315,7 +315,7 @@ function AccountGroup({
|
||||||
</div>
|
</div>
|
||||||
<div className="h-1.5 bg-secondary rounded-full overflow-hidden">
|
<div className="h-1.5 bg-secondary rounded-full overflow-hidden">
|
||||||
<div
|
<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}%` }}
|
style={{ width: `${utilPct}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,11 @@ function RadialGauge({ percent, size = 80 }: { percent: number; size?: number })
|
||||||
const circumference = 2 * Math.PI * r;
|
const circumference = 2 * Math.PI * r;
|
||||||
const clamped = Math.min(percent, 100);
|
const clamped = Math.min(percent, 100);
|
||||||
const offset = circumference - (clamped / 100) * circumference;
|
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 (
|
return (
|
||||||
<svg width={size} height={size} className="shrink-0">
|
<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>
|
<span className="ml-2 text-destructive font-medium">· {overBudget} over budget</span>
|
||||||
)}
|
)}
|
||||||
{alerted > 0 && (
|
{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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -139,7 +143,7 @@ export default function BudgetPage() {
|
||||||
item.is_over_budget
|
item.is_over_budget
|
||||||
? "border-destructive/50"
|
? "border-destructive/50"
|
||||||
: item.alert_triggered
|
: item.alert_triggered
|
||||||
? "border-orange-500/50"
|
? "border-warning/50"
|
||||||
: "border-border"
|
: "border-border"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -170,7 +174,7 @@ export default function BudgetPage() {
|
||||||
{item.is_over_budget ? (
|
{item.is_over_budget ? (
|
||||||
<AlertTriangle className="w-3.5 h-3.5 text-destructive shrink-0" />
|
<AlertTriangle className="w-3.5 h-3.5 text-destructive shrink-0" />
|
||||||
) : item.alert_triggered ? (
|
) : 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" />
|
<CheckCircle className="w-3.5 h-3.5 text-success shrink-0" />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -53,13 +53,13 @@ export default function Dashboard() {
|
||||||
|
|
||||||
{/* 2FA nudge */}
|
{/* 2FA nudge */}
|
||||||
{!totpEnabled && (
|
{!totpEnabled && (
|
||||||
<div className="flex items-center gap-3 bg-yellow-500/10 border border-yellow-500/30 rounded-xl px-4 py-3">
|
<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-yellow-500 shrink-0" />
|
<ShieldAlert className="w-5 h-5 text-warning shrink-0" />
|
||||||
<p className="flex-1 text-sm">
|
<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>
|
<span className="text-muted-foreground ml-1">to secure your account.</span>
|
||||||
</p>
|
</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
|
Set up 2FA
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -111,14 +111,14 @@ export default function Dashboard() {
|
||||||
<AreaChart data={nwReport.points.map(p => ({ date: p.date, value: Number(p.net_worth) }))}>
|
<AreaChart data={nwReport.points.map(p => ({ date: p.date, value: Number(p.net_worth) }))}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="nwGrad" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="nwGrad" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
|
<stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.3} />
|
||||||
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
|
<stop offset="95%" stopColor="hsl(var(--primary))" stopOpacity={0} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<XAxis dataKey="date" tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" />
|
<XAxis dataKey="date" tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" />
|
||||||
<YAxis tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" tickFormatter={v => `£${(v/1000).toFixed(0)}k`} width={45} />
|
<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)} />
|
<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>
|
</AreaChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -136,8 +136,8 @@ export default function Dashboard() {
|
||||||
{ieReport && ieReport.points.length > 0 ? (
|
{ieReport && ieReport.points.length > 0 ? (
|
||||||
<ResponsiveContainer width="100%" height={180}>
|
<ResponsiveContainer width="100%" height={180}>
|
||||||
<BarChart data={ieReport.points.map(p => ({ month: p.month, income: Number(p.income), expenses: Number(p.expenses) }))}>
|
<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)" />
|
<XAxis dataKey="month" tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" />
|
||||||
<YAxis tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" tickFormatter={v => `£${(v/1000).toFixed(0)}k`} width={45} />
|
<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")} />
|
<Tooltip formatter={(v: number) => formatCurrency(v, "GBP")} />
|
||||||
<Bar dataKey="income" fill="#22c55e" radius={[2,2,0,0]} name="Income" />
|
<Bar dataKey="income" fill="#22c55e" radius={[2,2,0,0]} name="Income" />
|
||||||
<Bar dataKey="expenses" fill="#ef4444" radius={[2,2,0,0]} name="Expenses" />
|
<Bar dataKey="expenses" fill="#ef4444" radius={[2,2,0,0]} name="Expenses" />
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { useUiStore } from "@/store/uiStore";
|
||||||
import { cn } from "@/utils/cn";
|
import { cn } from "@/utils/cn";
|
||||||
import { ArrowLeft, TrendingUp, TrendingDown } from "lucide-react";
|
import { ArrowLeft, TrendingUp, TrendingDown } from "lucide-react";
|
||||||
import Plot from "react-plotly.js";
|
import Plot from "react-plotly.js";
|
||||||
|
import { cssVar } from "@/utils/cssVar";
|
||||||
|
|
||||||
export default function AssetDetail() {
|
export default function AssetDetail() {
|
||||||
const { assetId } = useParams<{ assetId: string }>();
|
const { assetId } = useParams<{ assetId: string }>();
|
||||||
|
|
@ -152,8 +153,8 @@ export default function AssetDetail() {
|
||||||
high: highs as number[],
|
high: highs as number[],
|
||||||
low: lows as number[],
|
low: lows as number[],
|
||||||
close: closes as number[],
|
close: closes as number[],
|
||||||
increasing: { line: { color: "#22c55e" } },
|
increasing: { line: { color: cssVar("--success") } },
|
||||||
decreasing: { line: { color: "#ef4444" } },
|
decreasing: { line: { color: cssVar("--destructive") } },
|
||||||
name: holding?.symbol ?? "Price",
|
name: holding?.symbol ?? "Price",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -161,16 +162,16 @@ export default function AssetDetail() {
|
||||||
x: dates,
|
x: dates,
|
||||||
y: volumes as number[],
|
y: volumes as number[],
|
||||||
yaxis: "y2",
|
yaxis: "y2",
|
||||||
marker: { color: "rgba(99,102,241,0.3)" },
|
marker: { color: cssVar("--primary", 0.3) },
|
||||||
name: "Volume",
|
name: "Volume",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
layout={{
|
layout={{
|
||||||
paper_bgcolor: "transparent",
|
paper_bgcolor: "transparent",
|
||||||
plot_bgcolor: "transparent",
|
plot_bgcolor: "transparent",
|
||||||
font: { color: "var(--muted-foreground)", size: 11 },
|
font: { color: cssVar("--muted-foreground"), size: 11 },
|
||||||
xaxis: { rangeslider: { visible: false }, gridcolor: "var(--border)", showgrid: true },
|
xaxis: { rangeslider: { visible: false }, gridcolor: cssVar("--border"), showgrid: true },
|
||||||
yaxis: { gridcolor: "var(--border)", showgrid: true, domain: [0.25, 1] },
|
yaxis: { gridcolor: cssVar("--border"), showgrid: true, domain: [0.25, 1] },
|
||||||
yaxis2: { domain: [0, 0.2], showgrid: false },
|
yaxis2: { domain: [0, 0.2], showgrid: false },
|
||||||
margin: { t: 10, r: 10, b: 40, l: 60 },
|
margin: { t: 10, r: 10, b: 40, l: 60 },
|
||||||
showlegend: false,
|
showlegend: false,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, ReferenceLine,
|
XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, ReferenceLine,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import Plot from "react-plotly.js";
|
import Plot from "react-plotly.js";
|
||||||
|
import { cssVar } from "@/utils/cssVar";
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ id: "spending", label: "Spending", icon: BarChart3 },
|
{ id: "spending", label: "Spending", icon: BarChart3 },
|
||||||
|
|
@ -105,11 +106,11 @@ function SpendingTab() {
|
||||||
</div>
|
</div>
|
||||||
<ResponsiveContainer width="100%" height={260}>
|
<ResponsiveContainer width="100%" height={260}>
|
||||||
<BarChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
|
<BarChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
|
||||||
<XAxis dataKey="date" tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" />
|
<XAxis dataKey="date" tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" />
|
||||||
<YAxis tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" tickFormatter={v => `£${v}`} width={55} />
|
<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")} />
|
<Tooltip formatter={(v: number) => formatCurrency(v, "GBP")} />
|
||||||
<Bar dataKey="actual" fill="#6366f1" name="Actual" radius={[2, 2, 0, 0]} />
|
<Bar dataKey="actual" fill="hsl(var(--primary))" name="Actual" radius={[2, 2, 0, 0]} />
|
||||||
<Bar dataKey="forecast" fill="#6366f180" name="Forecast" radius={[2, 2, 0, 0]} />
|
<Bar dataKey="forecast" fill="hsl(var(--primary) / 0.5)" name="Forecast" radius={[2, 2, 0, 0]} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
|
||||||
|
|
@ -146,13 +147,13 @@ function BudgetAlerts() {
|
||||||
<div key={f.category_id}>
|
<div key={f.category_id}>
|
||||||
<div className="flex justify-between text-sm mb-1">
|
<div className="flex justify-between text-sm mb-1">
|
||||||
<span className="font-medium">{f.category_name}</span>
|
<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
|
{(f.probability_overspend * 100).toFixed(0)}% overspend risk
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 bg-secondary rounded-full overflow-hidden relative">
|
<div className="h-2 bg-secondary rounded-full overflow-hidden relative">
|
||||||
<div
|
<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)}%` }}
|
style={{ width: `${Math.min(100, forecastPct)}%` }}
|
||||||
/>
|
/>
|
||||||
<div className="absolute top-0 h-full w-0.5 bg-foreground/40" style={{ left: "100%" }} />
|
<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>
|
<p className="text-sm font-semibold mb-4">Net Worth Projection</p>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<LineChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
|
<LineChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
|
||||||
<XAxis dataKey="date" tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" interval="preserveStartEnd" />
|
<XAxis dataKey="date" tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" interval="preserveStartEnd" />
|
||||||
<YAxis tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" tickFormatter={v => `£${(v / 1000).toFixed(0)}k`} width={55} />
|
<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")} />
|
<Tooltip formatter={(v: number) => formatCurrency(v, "GBP")} />
|
||||||
<Legend />
|
<Legend />
|
||||||
{lastHistory && <ReferenceLine x={lastHistory.date} stroke="var(--border)" strokeDasharray="4 2" label={{ value: "Today", fontSize: 10 }} />}
|
{lastHistory && <ReferenceLine x={lastHistory.date} stroke="hsl(var(--border))" strokeDasharray="4 2" label={{ value: "Today", fontSize: 10 }} />}
|
||||||
<Line type="monotone" dataKey="history" stroke="#6366f1" strokeWidth={2} dot={false} name="History" />
|
<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="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="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" />
|
<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),
|
x: data.percentiles.p90.map(p => p.date),
|
||||||
y: data.percentiles.p90.map(p => p.value),
|
y: data.percentiles.p90.map(p => p.value),
|
||||||
fill: "tonexty",
|
fill: "tonexty",
|
||||||
fillcolor: "rgba(99,102,241,0.15)",
|
fillcolor: cssVar("--primary", 0.15),
|
||||||
line: { color: "#6366f1", width: 1 },
|
line: { color: cssVar("--primary"), width: 1 },
|
||||||
name: "P90",
|
name: "P90",
|
||||||
mode: "lines",
|
mode: "lines",
|
||||||
},
|
},
|
||||||
|
|
@ -352,8 +353,8 @@ function MonteCarloTab() {
|
||||||
x: data.percentiles.p75.map(p => p.date),
|
x: data.percentiles.p75.map(p => p.date),
|
||||||
y: data.percentiles.p75.map(p => p.value),
|
y: data.percentiles.p75.map(p => p.value),
|
||||||
fill: "tonexty",
|
fill: "tonexty",
|
||||||
fillcolor: "rgba(99,102,241,0.2)",
|
fillcolor: cssVar("--primary", 0.2),
|
||||||
line: { color: "#6366f1", width: 1 },
|
line: { color: cssVar("--primary"), width: 1 },
|
||||||
name: "P75",
|
name: "P75",
|
||||||
mode: "lines",
|
mode: "lines",
|
||||||
},
|
},
|
||||||
|
|
@ -361,7 +362,7 @@ function MonteCarloTab() {
|
||||||
type: "scatter" as const,
|
type: "scatter" as const,
|
||||||
x: data.percentiles.p50.map(p => p.date),
|
x: data.percentiles.p50.map(p => p.date),
|
||||||
y: data.percentiles.p50.map(p => p.value),
|
y: data.percentiles.p50.map(p => p.value),
|
||||||
line: { color: "#22c55e", width: 2.5 },
|
line: { color: cssVar("--success"), width: 2.5 },
|
||||||
name: "P50 (Median)",
|
name: "P50 (Median)",
|
||||||
mode: "lines",
|
mode: "lines",
|
||||||
},
|
},
|
||||||
|
|
@ -370,8 +371,8 @@ function MonteCarloTab() {
|
||||||
x: data.percentiles.p25.map(p => p.date),
|
x: data.percentiles.p25.map(p => p.date),
|
||||||
y: data.percentiles.p25.map(p => p.value),
|
y: data.percentiles.p25.map(p => p.value),
|
||||||
fill: "tonexty",
|
fill: "tonexty",
|
||||||
fillcolor: "rgba(239,68,68,0.1)",
|
fillcolor: cssVar("--destructive", 0.1),
|
||||||
line: { color: "#ef4444", width: 1 },
|
line: { color: cssVar("--destructive"), width: 1 },
|
||||||
name: "P25",
|
name: "P25",
|
||||||
mode: "lines",
|
mode: "lines",
|
||||||
},
|
},
|
||||||
|
|
@ -380,8 +381,8 @@ function MonteCarloTab() {
|
||||||
x: data.percentiles.p10.map(p => p.date),
|
x: data.percentiles.p10.map(p => p.date),
|
||||||
y: data.percentiles.p10.map(p => p.value),
|
y: data.percentiles.p10.map(p => p.value),
|
||||||
fill: "tonexty",
|
fill: "tonexty",
|
||||||
fillcolor: "rgba(239,68,68,0.15)",
|
fillcolor: cssVar("--destructive", 0.15),
|
||||||
line: { color: "#ef4444", width: 1 },
|
line: { color: cssVar("--destructive"), width: 1 },
|
||||||
name: "P10",
|
name: "P10",
|
||||||
mode: "lines",
|
mode: "lines",
|
||||||
},
|
},
|
||||||
|
|
@ -389,10 +390,10 @@ function MonteCarloTab() {
|
||||||
layout={{
|
layout={{
|
||||||
paper_bgcolor: "transparent",
|
paper_bgcolor: "transparent",
|
||||||
plot_bgcolor: "transparent",
|
plot_bgcolor: "transparent",
|
||||||
font: { color: "var(--muted-foreground)", size: 11 },
|
font: { color: cssVar("--muted-foreground"), size: 11 },
|
||||||
xaxis: { gridcolor: "var(--border)", showgrid: true },
|
xaxis: { gridcolor: cssVar("--border"), showgrid: true },
|
||||||
yaxis: {
|
yaxis: {
|
||||||
gridcolor: "var(--border)",
|
gridcolor: cssVar("--border"),
|
||||||
showgrid: true,
|
showgrid: true,
|
||||||
tickformat: "£,.0f",
|
tickformat: "£,.0f",
|
||||||
},
|
},
|
||||||
|
|
@ -461,15 +462,15 @@ function CashFlowTab() {
|
||||||
<AreaChart data={data.forecast} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
|
<AreaChart data={data.forecast} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="balanceGrad" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="balanceGrad" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
|
<stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.3} />
|
||||||
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
|
<stop offset="95%" stopColor="hsl(var(--primary))" stopOpacity={0} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<XAxis dataKey="date" tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" tickFormatter={v => v.slice(5)} />
|
<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 }} stroke="var(--muted-foreground)" tickFormatter={v => `£${(v / 1000).toFixed(1)}k`} width={55} />
|
<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")} />
|
<Tooltip formatter={(v: number) => formatCurrency(v, "GBP")} />
|
||||||
<ReferenceLine y={0} stroke="#ef4444" strokeDasharray="4 2" />
|
<ReferenceLine y={0} stroke="hsl(var(--destructive))" strokeDasharray="4 2" />
|
||||||
<Area type="monotone" dataKey="balance" stroke="#6366f1" fill="url(#balanceGrad)" strokeWidth={2} name="Balance" />
|
<Area type="monotone" dataKey="balance" stroke="hsl(var(--primary))" fill="url(#balanceGrad)" strokeWidth={2} name="Balance" />
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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) }))}>
|
<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>
|
<defs>
|
||||||
<linearGradient id="nwGrad" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="nwGrad" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
|
<stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.3} />
|
||||||
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
|
<stop offset="95%" stopColor="hsl(var(--primary))" stopOpacity={0} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
|
||||||
<XAxis dataKey="date" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" />
|
<XAxis dataKey="date" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" />
|
||||||
<YAxis tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `£${(v/1000).toFixed(0)}k`} />
|
<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)} />
|
<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_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" />
|
<Area type="monotone" dataKey="total_liabilities" stroke="#ef4444" fill="none" strokeWidth={1.5} strokeDasharray="4 2" name="Liabilities" />
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
|
|
@ -287,9 +287,9 @@ function IncomeExpenseTab() {
|
||||||
<p className="text-sm font-medium mb-4">Monthly Income vs Expenses</p>
|
<p className="text-sm font-medium mb-4">Monthly Income vs Expenses</p>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<BarChart data={chartData}>
|
<BarChart data={chartData}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
|
||||||
<XAxis dataKey="month" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" />
|
<XAxis dataKey="month" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" />
|
||||||
<YAxis tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `£${(v/1000).toFixed(0)}k`} />
|
<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)} />
|
<Tooltip formatter={(v: number) => formatCurrency(v, data.currency)} />
|
||||||
<Legend />
|
<Legend />
|
||||||
<Bar dataKey="income" fill="#22c55e" name="Income" radius={[2, 2, 0, 0]} />
|
<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>
|
<p className="text-sm font-medium mb-4">Daily Cash Flow — Last 30 Days</p>
|
||||||
<ResponsiveContainer width="100%" height={320}>
|
<ResponsiveContainer width="100%" height={320}>
|
||||||
<ComposedChart data={chartData}>
|
<ComposedChart data={chartData}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
|
||||||
<XAxis dataKey="date" tick={{ fontSize: 10 }} stroke="var(--muted-foreground)" />
|
<XAxis dataKey="date" tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" />
|
||||||
<YAxis yAxisId="bars" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `£${(v/1000).toFixed(1)}k`} />
|
<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 }} stroke="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)} />
|
<Tooltip formatter={(v: number) => formatCurrency(v, data.currency)} />
|
||||||
<Legend />
|
<Legend />
|
||||||
<Bar yAxisId="bars" dataKey="inflow" fill="#22c55e" name="Inflow" radius={[2, 2, 0, 0]} />
|
<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]} />
|
<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>
|
</ComposedChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -404,15 +404,15 @@ function SavingsRateTab() {
|
||||||
<p className="text-sm font-medium mb-4">Savings Rate by Month</p>
|
<p className="text-sm font-medium mb-4">Savings Rate by Month</p>
|
||||||
<ResponsiveContainer width="100%" height={320}>
|
<ResponsiveContainer width="100%" height={320}>
|
||||||
<ComposedChart data={chartData}>
|
<ComposedChart data={chartData}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
|
||||||
<XAxis dataKey="month" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" />
|
<XAxis dataKey="month" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" />
|
||||||
<YAxis yAxisId="bars" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `£${(v/1000).toFixed(0)}k`} />
|
<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 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `${v}%`} />
|
<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)} />
|
<Tooltip formatter={(v: number, name: string) => name === "Savings Rate %" ? `${v.toFixed(1)}%` : formatCurrency(v, data.currency)} />
|
||||||
<Legend />
|
<Legend />
|
||||||
<Bar yAxisId="bars" dataKey="income" fill="#22c55e" name="Income" radius={[2, 2, 0, 0]} />
|
<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]} />
|
<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>
|
</ComposedChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -580,12 +580,12 @@ function BudgetVsActualTab() {
|
||||||
<p className="text-sm font-medium mb-4">Budget vs Actual Spending</p>
|
<p className="text-sm font-medium mb-4">Budget vs Actual Spending</p>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<BarChart data={chartData} layout="vertical">
|
<BarChart data={chartData} layout="vertical">
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
|
||||||
<XAxis type="number" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `£${v}`} />
|
<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 }} stroke="var(--muted-foreground)" width={120} />
|
<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)} />
|
<Tooltip formatter={(v: number) => formatCurrency(v, data.currency)} />
|
||||||
<Legend />
|
<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]} />
|
<Bar dataKey="actual" fill="#f97316" name="Actual" radius={[0, 2, 2, 0]} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
|
@ -614,9 +614,9 @@ function SpendingTrendsTab() {
|
||||||
<p className="text-sm font-medium mb-4">Spending by Category (6 months)</p>
|
<p className="text-sm font-medium mb-4">Spending by Category (6 months)</p>
|
||||||
<ResponsiveContainer width="100%" height={320}>
|
<ResponsiveContainer width="100%" height={320}>
|
||||||
<BarChart data={chartData}>
|
<BarChart data={chartData}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
|
||||||
<XAxis dataKey="month" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" />
|
<XAxis dataKey="month" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" />
|
||||||
<YAxis tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `£${v}`} />
|
<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)} />
|
<Tooltip formatter={(v: number) => formatCurrency(v, data.currency)} />
|
||||||
<Legend />
|
<Legend />
|
||||||
{data.categories.slice(0, 8).map((cat, i) => (
|
{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>
|
<p className="text-sm font-medium mb-4">Holdings Value</p>
|
||||||
<ResponsiveContainer width="100%" height={220}>
|
<ResponsiveContainer width="100%" height={220}>
|
||||||
<BarChart data={holdingsData} layout="vertical">
|
<BarChart data={holdingsData} layout="vertical">
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
|
||||||
<XAxis type="number" tick={{ fontSize: 11 }} stroke="var(--muted-foreground)" tickFormatter={(v) => `£${(v/1000).toFixed(0)}k`} />
|
<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 }} stroke="var(--muted-foreground)" width={60} />
|
<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)} />
|
<Tooltip formatter={(v: number) => formatCurrency(v, perf.currency)} />
|
||||||
<Bar dataKey="value" name="Current Value" radius={[0, 3, 3, 0]}>
|
<Bar dataKey="value" name="Current Value" radius={[0, 3, 3, 0]}>
|
||||||
{holdingsData.map((entry, i) => (
|
{holdingsData.map((entry, i) => (
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@ function PasswordCard() {
|
||||||
<div className="mt-2 flex gap-1">
|
<div className="mt-2 flex gap-1">
|
||||||
{[1,2,3,4].map(i => {
|
{[1,2,3,4].map(i => {
|
||||||
const score = Math.min(4, Math.floor(next.length / 3));
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
21
frontend/src/utils/cssVar.ts
Normal file
21
frontend/src/utils/cssVar.ts
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue