feat(ui): ink & copper ledger retheme + analytics redesign
ci / lint-test (push) Successful in 43s

App-wide retheme done at the token layer: Tailwind's zinc scale is remapped
to warm ink/paper neutrals and indigo to copper in globals.css, so every
page inherits the palette. Fraunces (serif display) added for page titles
and hero figures; all figures now render in mono with tabular numerals.

Analytics page redesigned around a month spine — twelve clickable columns
scaled to each month's spend that act as hero, context, and period
navigation. Adds a 'What changed' top-movers panel vs the previous month,
replaces the 8-line category trend chart with per-category sparkline small
multiples, heat-tints the six-month ledger table, and restyles the Pareto,
pace chart, and cashflow strip. Kept: Pareto, cumulative-vs-typical pace,
drill-downs, regular/occasional split.

Insights and Merchants restyled to the same kit; chart tokens centralised
in category-colors.ts (CHART). Cleared the pre-existing lint errors in
insights (typed tooltip, removed any-casts).
This commit is contained in:
2026-07-19 20:37:49 +10:00
parent 99af10f9ea
commit 831d30669b
14 changed files with 426 additions and 293 deletions
+15 -15
View File
@@ -129,7 +129,7 @@ function MerchantProfile({
}));
}, [merchant.monthly_trend]);
const color = CATEGORY_COLORS[merchant.category] || "#6366f1";
const color = CATEGORY_COLORS[merchant.category] || "#bc6f30";
return (
<div className="fixed inset-0 z-50 flex justify-end">
@@ -190,23 +190,23 @@ function MerchantProfile({
<p className="text-sm font-medium text-zinc-300 mb-3">Monthly Spend</p>
<ResponsiveContainer width="100%" height={120}>
<LineChart data={trendData}>
<CartesianGrid stroke="#27272a" strokeDasharray="3 3" />
<CartesianGrid stroke="#242019" strokeDasharray="3 3" />
<XAxis
dataKey="label"
tick={{ fill: "#71717a", fontSize: 10 }}
tick={{ fill: "#94896f", fontSize: 10 }}
axisLine={false}
tickLine={false}
/>
<YAxis
tick={{ fill: "#71717a", fontSize: 10 }}
tick={{ fill: "#94896f", fontSize: 10 }}
axisLine={false}
tickLine={false}
tickFormatter={(v) => `$${Math.round(v)}`}
width={45}
/>
<Tooltip
contentStyle={{ background: "#18181b", border: "1px solid #3f3f46", borderRadius: "8px" }}
labelStyle={{ color: "#a1a1aa" }}
contentStyle={{ background: "#171410", border: "1px solid #332d23", borderRadius: "8px" }}
labelStyle={{ color: "#b3a88e" }}
/>
<Line
type="monotone"
@@ -295,7 +295,7 @@ export default function MerchantsPage() {
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-semibold text-white">Merchants</h1>
<h1 className="text-2xl font-display text-zinc-50">Merchants</h1>
<p className="text-zinc-400 text-sm mt-0.5">{merchants.length} merchants · last {months} months</p>
</div>
<select
@@ -320,28 +320,28 @@ export default function MerchantsPage() {
<QuadrantLabels medianX={medianX} medianY={medianY} />
<ResponsiveContainer width="100%" height={360}>
<ScatterChart margin={{ top: 10, right: 20, bottom: 20, left: 10 }}>
<CartesianGrid stroke="#27272a" strokeDasharray="3 3" />
<CartesianGrid stroke="#242019" strokeDasharray="3 3" />
<XAxis
dataKey="debit_count"
name="Transactions"
type="number"
tick={{ fill: "#71717a", fontSize: 11 }}
tick={{ fill: "#94896f", fontSize: 11 }}
axisLine={false}
tickLine={false}
label={{ value: "Transaction Count", position: "insideBottom", offset: -10, fill: "#52525b", fontSize: 11 }}
label={{ value: "Transaction Count", position: "insideBottom", offset: -10, fill: "#6e644f", fontSize: 11 }}
/>
<YAxis
dataKey="net_spend"
name="Net Spend"
type="number"
tick={{ fill: "#71717a", fontSize: 11 }}
tick={{ fill: "#94896f", fontSize: 11 }}
axisLine={false}
tickLine={false}
tickFormatter={(v) => `$${Math.round(v / 1000)}k`}
width={48}
label={{ value: "Total Spend", angle: -90, position: "insideLeft", offset: 10, fill: "#52525b", fontSize: 11 }}
label={{ value: "Total Spend", angle: -90, position: "insideLeft", offset: 10, fill: "#6e644f", fontSize: 11 }}
/>
<Tooltip content={<ScatterTooltip />} cursor={{ strokeDasharray: "3 3", stroke: "#52525b" }} />
<Tooltip content={<ScatterTooltip />} cursor={{ strokeDasharray: "3 3", stroke: "#6e644f" }} />
<Scatter
data={scatterData}
onClick={(d) => setSelected(d as unknown as MerchantRow)}
@@ -350,7 +350,7 @@ export default function MerchantsPage() {
{scatterData.map((entry, idx) => (
<Cell
key={idx}
fill={CATEGORY_COLORS[entry.category] || "#6366f1"}
fill={CATEGORY_COLORS[entry.category] || "#bc6f30"}
fillOpacity={0.75}
stroke={selected?.merchant === entry.merchant ? "#fff" : "transparent"}
strokeWidth={2}
@@ -400,7 +400,7 @@ export default function MerchantsPage() {
</thead>
<tbody>
{filtered.map((m) => {
const color = CATEGORY_COLORS[m.category] || "#6366f1";
const color = CATEGORY_COLORS[m.category] || "#bc6f30";
return (
<tr
key={m.merchant}