Fix theme text visibility, tooltip colours, and chart hover states

- Extract shared TOOLTIP_STYLE and ACTIVE_DOT to utils/chartTheme.ts so
  all four chart files use one source of truth
- itemStyle now uses card-foreground (not muted-foreground) — guarantees
  tooltip text is readable on all themes including Terminal, Vault, Synthwave
- cursor now uses primary at 12% opacity — always visible and thematic,
  replaces near-invisible muted-foreground at 8% opacity
- activeDot is now explicit on every Line/Area — prevents Recharts default
  white dot breaking dark themes
- Terminal: muted-foreground bumped 38%→55% lightness, border lightened,
  warning brightened, text-shadow scoped to headings/labels/table cells
  (was applying to every p and span, causing tooltip glow bleed)
- Synthwave: muted-foreground bumped 56%→68% lightness for legibility
- Vault: muted-foreground bumped 52%→60% lightness

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-04-23 23:14:36 +00:00
parent 54f6f66ff8
commit 1854026a76
6 changed files with 69 additions and 65 deletions

View file

@ -143,7 +143,7 @@
--secondary: 30 18% 14%; --secondary: 30 18% 14%;
--secondary-foreground: 38 20% 88%; --secondary-foreground: 38 20% 88%;
--muted: 30 18% 14%; --muted: 30 18% 14%;
--muted-foreground: 38 12% 52%; --muted-foreground: 38 18% 60%;
--accent: 38 92% 50%; --accent: 38 92% 50%;
--accent-foreground: 30 18% 6%; --accent-foreground: 30 18% 6%;
--destructive: 0 72% 55%; --destructive: 0 72% 55%;
@ -170,17 +170,17 @@
--secondary: 120 60% 9%; --secondary: 120 60% 9%;
--secondary-foreground: 120 100% 72%; --secondary-foreground: 120 100% 72%;
--muted: 120 60% 9%; --muted: 120 60% 9%;
--muted-foreground: 120 50% 38%; --muted-foreground: 120 60% 55%;
--accent: 120 100% 50%; --accent: 120 100% 50%;
--accent-foreground: 120 60% 3%; --accent-foreground: 120 60% 3%;
--destructive: 0 84% 55%; --destructive: 0 84% 55%;
--destructive-foreground: 0 0% 100%; --destructive-foreground: 0 0% 100%;
--border: 120 60% 14%; --border: 120 60% 16%;
--input: 120 60% 9%; --input: 120 60% 9%;
--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%; --warning: 60 100% 60%;
} }
.theme-terminal body, .theme-terminal body,
@ -213,11 +213,13 @@
z-index: 9999; z-index: 9999;
} }
/* Phosphor glow on text */ /* Phosphor glow — targeted to headings and explicit foreground text only */
.theme-terminal .text-foreground, .theme-terminal .text-foreground,
.theme-terminal p, .theme-terminal h1, .theme-terminal h2, .theme-terminal h3,
.theme-terminal span:not(.text-muted-foreground):not(.text-destructive) { .theme-terminal h4, .theme-terminal h5, .theme-terminal h6,
text-shadow: 0 0 8px hsl(120 100% 50% / 0.4); .theme-terminal th, .theme-terminal td,
.theme-terminal label {
text-shadow: 0 0 8px hsl(120 100% 50% / 0.35);
} }
/* Glow on cards */ /* Glow on cards */
@ -239,7 +241,7 @@
--secondary: 268 90% 14%; --secondary: 268 90% 14%;
--secondary-foreground: 280 30% 92%; --secondary-foreground: 280 30% 92%;
--muted: 268 90% 14%; --muted: 268 90% 14%;
--muted-foreground: 270 20% 56%; --muted-foreground: 270 25% 68%;
--accent: 190 100% 55%; --accent: 190 100% 55%;
--accent-foreground: 268 90% 6%; --accent-foreground: 268 90% 6%;
--destructive: 0 84% 60%; --destructive: 0 84% 60%;

View file

@ -15,23 +15,10 @@ import {
XAxis, YAxis, Tooltip, ResponsiveContainer, XAxis, YAxis, Tooltip, ResponsiveContainer,
} from "recharts"; } from "recharts";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { TOOLTIP_STYLE, ACTIVE_DOT } from "@/utils/chartTheme";
const COLORS = ["#6366f1","#22c55e","#f97316","#ec4899","#14b8a6","#f59e0b","#8b5cf6","#ef4444"]; const COLORS = ["#6366f1","#22c55e","#f97316","#ec4899","#14b8a6","#f59e0b","#8b5cf6","#ef4444"];
const TOOLTIP_STYLE = {
contentStyle: {
background: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
fontSize: "12px",
color: "hsl(var(--foreground))",
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
},
labelStyle: { color: "hsl(var(--foreground))", fontWeight: 500, marginBottom: "4px" },
itemStyle: { color: "hsl(var(--muted-foreground))" },
cursor: { fill: "hsl(var(--muted-foreground))", fillOpacity: 0.08 },
};
const TYPE_COLORS: Record<string, string> = { const TYPE_COLORS: Record<string, string> = {
income: "text-success", income: "text-success",
expense: "text-destructive", expense: "text-destructive",
@ -132,7 +119,7 @@ export default function Dashboard() {
<XAxis dataKey="date" tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" /> <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} /> <YAxis tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={v => `£${(v/1000).toFixed(0)}k`} width={45} />
<Tooltip {...TOOLTIP_STYLE} formatter={(v: number) => formatCurrency(v, nwReport.base_currency)} /> <Tooltip {...TOOLTIP_STYLE} formatter={(v: number) => formatCurrency(v, nwReport.base_currency)} />
<Area type="monotone" dataKey="value" stroke="hsl(var(--primary))" fill="url(#nwGrad)" strokeWidth={2} /> <Area type="monotone" dataKey="value" stroke="hsl(var(--primary))" fill="url(#nwGrad)" strokeWidth={2} activeDot={ACTIVE_DOT} />
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
) : ( ) : (

View file

@ -4,26 +4,13 @@ import {
} from "recharts"; } from "recharts";
import { formatCurrency } from "@/utils/currency"; import { formatCurrency } from "@/utils/currency";
import type { PortfolioSummary, HoldingResponse } from "@/api/investments"; import type { PortfolioSummary, HoldingResponse } from "@/api/investments";
import { TOOLTIP_STYLE } from "@/utils/chartTheme";
const COLORS = [ const COLORS = [
"#6366f1","#22c55e","#f97316","#ec4899","#14b8a6", "#6366f1","#22c55e","#f97316","#ec4899","#14b8a6",
"#f59e0b","#8b5cf6","#06b6d4","#84cc16","#ef4444", "#f59e0b","#8b5cf6","#06b6d4","#84cc16","#ef4444",
]; ];
const TOOLTIP_STYLE = {
contentStyle: {
background: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
fontSize: "12px",
color: "hsl(var(--foreground))",
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
},
labelStyle: { color: "hsl(var(--foreground))", fontWeight: 500, marginBottom: "4px" },
itemStyle: { color: "hsl(var(--muted-foreground))" },
cursor: { fill: "hsl(var(--muted-foreground))", fillOpacity: 0.08 },
};
const TYPE_COLORS: Record<string, string> = { const TYPE_COLORS: Record<string, string> = {
stock: "#6366f1", stock: "#6366f1",
etf: "#22c55e", etf: "#22c55e",

View file

@ -13,6 +13,7 @@ import {
} from "recharts"; } from "recharts";
import Plot from "react-plotly.js"; import Plot from "react-plotly.js";
import { cssVar } from "@/utils/cssVar"; import { cssVar } from "@/utils/cssVar";
import { TOOLTIP_STYLE, ACTIVE_DOT } from "@/utils/chartTheme";
const TABS = [ const TABS = [
{ id: "spending", label: "Spending", icon: BarChart3 }, { id: "spending", label: "Spending", icon: BarChart3 },
@ -108,7 +109,7 @@ function SpendingTab() {
<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, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" /> <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} /> <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 {...TOOLTIP_STYLE} formatter={(v: number) => formatCurrency(v, "GBP")} />
<Bar dataKey="actual" fill="hsl(var(--primary))" 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="hsl(var(--primary) / 0.5)" 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>
@ -241,13 +242,13 @@ function NetWorthTab() {
<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, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(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, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(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 {...TOOLTIP_STYLE} formatter={(v: number) => formatCurrency(v, "GBP")} />
<Legend /> <Legend />
{lastHistory && <ReferenceLine x={lastHistory.date} stroke="hsl(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="hsl(var(--primary))" strokeWidth={2} dot={false} name="History" /> <Line type="monotone" dataKey="history" stroke="hsl(var(--primary))" strokeWidth={2} dot={false} name="History" activeDot={ACTIVE_DOT} />
<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" activeDot={{ r: 3, fill: "#ef4444", stroke: "hsl(var(--card))", strokeWidth: 2 }} />
<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" activeDot={{ r: 3, fill: "#22c55e", stroke: "hsl(var(--card))", strokeWidth: 2 }} />
<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" activeDot={{ r: 3, fill: "#f59e0b", stroke: "hsl(var(--card))", strokeWidth: 2 }} />
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
@ -468,9 +469,9 @@ function CashFlowTab() {
</defs> </defs>
<XAxis dataKey="date" tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(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, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(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 {...TOOLTIP_STYLE} formatter={(v: number) => formatCurrency(v, "GBP")} />
<ReferenceLine y={0} stroke="hsl(var(--destructive))" strokeDasharray="4 2" /> <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" /> <Area type="monotone" dataKey="balance" stroke="hsl(var(--primary))" fill="url(#balanceGrad)" strokeWidth={2} name="Balance" activeDot={ACTIVE_DOT} />
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>

View file

@ -21,6 +21,7 @@ import {
Tooltip, ResponsiveContainer, Legend Tooltip, ResponsiveContainer, Legend
} from "recharts"; } from "recharts";
import { TrendingUp, TrendingDown, Minus, Landmark, CreditCard, PiggyBank } from "lucide-react"; import { TrendingUp, TrendingDown, Minus, Landmark, CreditCard, PiggyBank } from "lucide-react";
import { TOOLTIP_STYLE, ACTIVE_DOT } from "@/utils/chartTheme";
const TABS = [ const TABS = [
"Balance Sheet", "Net Worth", "Income vs Expense", "Balance Sheet", "Net Worth", "Income vs Expense",
@ -37,20 +38,6 @@ const COLORS = [
const ASSET_COLORS = ["#22c55e", "#14b8a6", "#6366f1", "#8b5cf6", "#06b6d4", "#84cc16"]; const ASSET_COLORS = ["#22c55e", "#14b8a6", "#6366f1", "#8b5cf6", "#06b6d4", "#84cc16"];
const LIABILITY_COLORS = ["#ef4444", "#f97316", "#ec4899"]; const LIABILITY_COLORS = ["#ef4444", "#f97316", "#ec4899"];
const TOOLTIP_STYLE = {
contentStyle: {
background: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
fontSize: "12px",
color: "hsl(var(--foreground))",
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
},
labelStyle: { color: "hsl(var(--foreground))", fontWeight: 500, marginBottom: "4px" },
itemStyle: { color: "hsl(var(--muted-foreground))" },
cursor: { fill: "hsl(var(--muted-foreground))", fillOpacity: 0.08 },
};
function StatCard({ label, value, change, currency }: { function StatCard({ label, value, change, currency }: {
label: string; value: number; change?: number; currency: string; label: string; value: number; change?: number; currency: string;
}) { }) {
@ -271,9 +258,9 @@ function NetWorthTab() {
<XAxis dataKey="date" tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" /> <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`} /> <YAxis tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} stroke="hsl(var(--muted-foreground))" tickFormatter={(v) => `£${(v/1000).toFixed(0)}k`} />
<Tooltip {...TOOLTIP_STYLE} formatter={(v: number) => formatCurrency(v, data.base_currency)} /> <Tooltip {...TOOLTIP_STYLE} formatter={(v: number) => formatCurrency(v, data.base_currency)} />
<Area type="monotone" dataKey="net_worth" stroke="hsl(var(--primary))" 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" activeDot={ACTIVE_DOT} />
<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" activeDot={{ r: 3, fill: "#22c55e", stroke: "hsl(var(--card))", strokeWidth: 2 }} />
<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" activeDot={{ r: 3, fill: "#ef4444", stroke: "hsl(var(--card))", strokeWidth: 2 }} />
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
@ -362,7 +349,7 @@ function CashFlowTab() {
<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="hsl(var(--primary))" 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" activeDot={ACTIVE_DOT} />
</ComposedChart> </ComposedChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
@ -426,7 +413,7 @@ function SavingsRateTab() {
<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="hsl(var(--primary))" 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, fill: "hsl(var(--primary))", stroke: "hsl(var(--card))", strokeWidth: 1.5 }} name="Savings Rate %" activeDot={ACTIVE_DOT} />
</ComposedChart> </ComposedChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>

View file

@ -0,0 +1,40 @@
/**
* Shared Recharts tooltip and chart style constants.
*
* All colour values use CSS custom properties so they adapt to every theme.
* itemStyle uses card-foreground (not muted-foreground) to guarantee
* readability inside the tooltip card on all themes including Terminal,
* Synthwave and Vault which have dim muted-foreground values.
*
* cursor uses --primary at low opacity always visible and thematic.
*/
export const TOOLTIP_STYLE = {
contentStyle: {
background: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
fontSize: "12px",
color: "hsl(var(--card-foreground))",
boxShadow: "0 4px 20px rgba(0,0,0,0.35)",
padding: "8px 12px",
},
labelStyle: {
color: "hsl(var(--card-foreground))",
fontWeight: 600,
marginBottom: "4px",
fontSize: "12px",
},
itemStyle: {
color: "hsl(var(--card-foreground))",
opacity: 0.8,
},
cursor: { fill: "hsl(var(--primary))", fillOpacity: 0.12 },
};
/** Standard activeDot for Line/Area charts — theme-adaptive. */
export const ACTIVE_DOT = {
r: 4,
stroke: "hsl(var(--card))",
strokeWidth: 2,
fill: "hsl(var(--primary))",
};