Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0f2f425
feat(tokens): add warm-theme palette and DM fonts for Budget v3
raycashmore May 16, 2026
45c4470
fix(tokens): drop unsupported Serif Display 700 weight; alias warm-ne…
raycashmore May 16, 2026
cdac117
refactor(shell): restyle AppFrame and Header for warm-bordered v3 shell
raycashmore May 16, 2026
d99eae5
refactor(shell): rebuild Sidebar with v3 warm tokens and 96px footprint
raycashmore May 16, 2026
fde3303
feat(convex): add toCents/fromCents helpers and Vitest for @repo/convex
raycashmore May 16, 2026
c6e5f96
feat(convex): document cents convention and round rate-boundary deriv…
raycashmore May 16, 2026
7ac3f95
feat(budget): formatCurrency takes cents (matches schema convention)
raycashmore May 16, 2026
cdfd894
chore(convex): move seed script and data fixtures into @repo/convex/s…
raycashmore May 16, 2026
3fbff60
chore(convex): seed script reads .env.local and exposes pnpm seed
raycashmore May 16, 2026
a09df21
feat(convex): seed converts money to cents and clears tables first
raycashmore May 16, 2026
36fe349
fix(convex): use default import for xlsx to fix ESM readFile error
raycashmore May 16, 2026
6a19556
feat(convex): summarizeBudgetForPeriod helper with deltas
raycashmore May 16, 2026
fbf6c61
feat(convex): getBudgetPageSummary query
raycashmore May 16, 2026
c7a83d3
feat(convex): joinBudgetWithMortgage helper with carry-forward semantics
raycashmore May 16, 2026
b5dffed
feat(convex): getMonthlyBreakdown query
raycashmore May 16, 2026
0aaa44c
feat(convex): shapeMonthDetail helper for monthly overlay
raycashmore May 16, 2026
27aa125
feat(convex): getMonthlyDetail query (overlay data)
raycashmore May 16, 2026
d771d81
refactor(budget): TimePeriod uses monthly buckets to match v3 design
raycashmore May 16, 2026
981da27
feat(budget): KpiCard and BudgetKpiCards components
raycashmore May 16, 2026
1401e3b
style(budget): warm pill filter group for chart period
raycashmore May 16, 2026
e91dc5f
style(budget): restyle chart and tooltip to warm v3 palette
raycashmore May 16, 2026
bf91248
feat(budget): BudgetBreakdownTable with click-to-open rows
raycashmore May 16, 2026
bee2ab6
feat(budget): InsightsPanel placeholder reserving right-column space
raycashmore May 16, 2026
0febbc1
feat(budget): MonthlyDetailOverlay shell (portal, scrim, escape)
raycashmore May 16, 2026
666a1e0
feat(budget): MonthIncomeSection for overlay
raycashmore May 16, 2026
0564d89
feat(budget): MonthSpendSection with stubbed category breakdown
raycashmore May 16, 2026
1e93754
feat(budget): MonthMortgageSection for overlay
raycashmore May 16, 2026
e64505c
feat(budget): compose v3 page with KPIs, chart, breakdown, overlay
raycashmore May 16, 2026
e7012b8
fix(budget): wire period filter to breakdown table row limit
raycashmore May 16, 2026
c894a7c
fix(budget): align MonthlyDetailOverlay typography to QPjOt design
raycashmore May 16, 2026
cb5cb11
chore: prettier format pass
raycashmore May 16, 2026
065623b
feat(budget): add month-over-month trend row to overlay mini-cards
raycashmore May 16, 2026
57a5429
chore: prettier format pass
raycashmore May 16, 2026
45344fe
Fix BudgetBreakdownTable mobile view
raycashmore May 16, 2026
85f868f
Merge claude/xenodochial-curie-8a0898: overlay mini-card trend rows
raycashmore May 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ yarn-error.log*
# Local only
/data
/docs/superpowers
packages/convex/scripts/*.xlsx
.playwright-mcp
.claude
1 change: 1 addition & 0 deletions apps/budget/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ pnpm-lock.yaml
yarn.lock

convex/_generated
**/routeTree.gen.ts
93 changes: 93 additions & 0 deletions apps/budget/src/components/budget/BudgetBreakdownTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { formatCurrency } from '@/lib/budget';

export interface BreakdownRowData {
date: number;
income: number;
spend: number;
mortgage: number | null;
net: number;
}

interface Props {
rows: Array<BreakdownRowData> | undefined;
onRowClick: (date: number) => void;
}

function monthLabel(date: number): string {
return new Date(date).toLocaleString('en-AU', {
month: 'short',
year: 'numeric'
});
}

function SkeletonRows() {
return (
<div className="flex flex-col gap-2">
{Array.from({ length: 6 }).map((_, i) => (
<div
key={i}
className="h-10 rounded-xl bg-warm-bg-card-soft animate-pulse"
/>
))}
</div>
);
}

export default function BudgetBreakdownTable({ rows, onRowClick }: Props) {
return (
<div className="rounded-3xl bg-warm-bg-card-soft p-5">
<h2 className="text-base font-warm-display text-warm-text-primary mb-3">
Monthly breakdown
</h2>
{!rows ? (
<SkeletonRows />
) : rows.length === 0 ? (
<p className="text-sm text-warm-text-secondary">No budget rows.</p>
) : (
<div className="-mx-5 overflow-x-auto px-5">
<table className="w-full min-w-[460px] text-sm">
<thead>
<tr className="text-warm-text-tertiary text-[11px] uppercase tracking-wide">
<th className="text-left font-medium py-2 pr-3">Month</th>
<th className="text-right font-medium py-2 px-3">Income</th>
<th className="text-right font-medium py-2 px-3">Spend</th>
<th className="text-right font-medium py-2 px-3">Mortgage</th>
<th className="text-right font-medium py-2 pl-3">Net</th>
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr
key={r.date}
onClick={() => onRowClick(r.date)}
className="cursor-pointer border-t border-warm-border hover:bg-warm-bg-card transition-colors"
>
<td className="py-2 pr-3 text-warm-text-primary font-medium whitespace-nowrap">
{monthLabel(r.date)}
</td>
<td className="py-2 px-3 text-right text-warm-text-primary whitespace-nowrap">
{formatCurrency(r.income)}
</td>
<td className="py-2 px-3 text-right text-warm-text-primary whitespace-nowrap">
{formatCurrency(r.spend)}
</td>
<td className="py-2 px-3 text-right text-warm-text-secondary whitespace-nowrap">
{r.mortgage === null ? '—' : formatCurrency(r.mortgage)}
</td>
<td
className={`py-2 pl-3 text-right font-medium whitespace-nowrap ${
r.net >= 0 ? 'text-warm-positive' : 'text-warm-negative'
}`}
>
{r.net >= 0 ? '+' : ''}
{formatCurrency(r.net)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}
177 changes: 93 additions & 84 deletions apps/budget/src/components/budget/BudgetChart.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useState } from 'react';
import { Group } from '@visx/group';
import { scaleBand, scaleLinear } from '@visx/scale';
import { AxisBottom, AxisLeft } from '@visx/axis';
Expand All @@ -8,7 +7,6 @@ import { useTooltip } from '@visx/tooltip';
import BudgetChartBars from './BudgetChartBars';
import BudgetChartLines from './BudgetChartLines';
import BudgetChartTooltip from './BudgetChartTooltip';
import BudgetChartFilters from './BudgetChartFilters';
import type { BudgetDataPoint, TimePeriod } from '@/lib/budget';
import {
computeMovingAverage,
Expand All @@ -24,15 +22,15 @@ const CHART_HEIGHT = 520;

interface BudgetChartProps {
data: Array<BudgetDataPoint>;
period: TimePeriod;
}

function BudgetChartInner({
data,
period,
width,
height
}: BudgetChartProps & { width: number; height: number }) {
const [period, setPeriod] = useState<TimePeriod>('ALL');

const {
tooltipData,
tooltipLeft,
Expand All @@ -46,9 +44,9 @@ function BudgetChartInner({

if (filtered.length === 0) {
return (
<div className="flex min-h-[500px] flex-col items-center justify-center gap-2 text-center">
<p className="text-sm font-medium text-neutral-700">No budget data</p>
<p className="text-sm text-neutral-500">
<div className="flex min-h-[400px] flex-col items-center justify-center gap-2 text-center text-warm-text-secondary">
<p className="text-sm font-medium">No budget data</p>
<p className="text-sm text-warm-text-tertiary">
Seed the budget table to render the chart.
</p>
</div>
Expand Down Expand Up @@ -111,106 +109,117 @@ function BudgetChartInner({
if (innerWidth <= 0 || innerHeight <= 0) return null;

return (
<div className="relative">
<div className="rounded-3xl bg-warm-bg-card-soft p-5">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-6 text-sm text-gray-500">
<h2 className="text-base font-warm-display text-warm-text-primary">
Income vs Spending
</h2>
<div className="flex items-center gap-4 text-xs text-warm-text-secondary">
<div className="flex items-center gap-2">
<span
className="inline-block w-3 h-3 rounded-sm"
style={{ backgroundColor: 'rgba(250, 128, 114, 0.7)' }}
style={{ backgroundColor: '#D85A36' }}
/>
Spend
</div>
<div className="flex items-center gap-2">
<span
className="inline-block w-3 h-3 rounded-sm"
style={{ backgroundColor: 'rgba(100, 149, 237, 0.7)' }}
style={{ backgroundColor: '#5F9466' }}
/>
Sink or Swim
</div>
</div>
<BudgetChartFilters selected={period} onChange={setPeriod} />
</div>

<svg
viewBox={`0 0 ${width} ${height}`}
preserveAspectRatio="none"
className="h-[28rem] min-w-[720px] w-full sm:h-[32rem]"
>
<Group left={MARGIN.left} top={MARGIN.top}>
<GridRows
scale={yScale}
width={innerWidth}
stroke="#e5e7eb"
strokeOpacity={0.5}
/>
<div className="relative">
<svg
viewBox={`0 0 ${width} ${height}`}
preserveAspectRatio="none"
className="h-[28rem] min-w-[720px] w-full sm:h-[32rem]"
>
<Group left={MARGIN.left} top={MARGIN.top}>
<GridRows
scale={yScale}
width={innerWidth}
stroke="#EFE3D2"
strokeOpacity={0.6}
/>

<BudgetChartBars
data={filtered}
xScale={xScale}
yScale={yScale}
height={innerHeight}
onMouseMove={handleMouseMove}
onMouseLeave={hideTooltip}
/>
<BudgetChartBars
data={filtered}
xScale={xScale}
yScale={yScale}
height={innerHeight}
onMouseMove={handleMouseMove}
onMouseLeave={hideTooltip}
/>

<BudgetChartLines
spendTrend={spendTrend}
sinkOrSwimTrend={sinkOrSwimTrend}
xScale={xScale}
yScale={yScale}
/>
<BudgetChartLines
spendTrend={spendTrend}
sinkOrSwimTrend={sinkOrSwimTrend}
xScale={xScale}
yScale={yScale}
/>

<AxisBottom
top={innerHeight}
scale={xScale}
tickFormat={(date) => formatDateLabel(date)}
tickValues={filtered
.map((d) => d.date)
.filter((_, i) => i % labelInterval === 0)}
tickLabelProps={() => ({
fill: '#6b7280',
fontSize: 11,
textAnchor: 'end',
dy: '0.25em',
dx: '-0.5em',
angle: -45
})}
stroke="#d1d5db"
tickStroke="#d1d5db"
hideTicks={false}
/>
<AxisBottom
top={innerHeight}
scale={xScale}
tickFormat={(date) => formatDateLabel(date)}
tickValues={filtered
.map((d) => d.date)
.filter((_, i) => i % labelInterval === 0)}
tickLabelProps={() => ({
fill: '#7C6755',
fontSize: 11,
textAnchor: 'end',
dy: '0.25em',
dx: '-0.5em',
angle: -45
})}
stroke="#EFE3D2"
tickStroke="#EFE3D2"
hideTicks={false}
/>

<AxisLeft
scale={yScale}
tickFormat={(v) => formatCurrency(v as number)}
tickLabelProps={() => ({
fill: '#6b7280',
fontSize: 11,
textAnchor: 'end',
dx: '-0.5em',
dy: '0.33em'
})}
stroke="#d1d5db"
tickStroke="#d1d5db"
numTicks={6}
<AxisLeft
scale={yScale}
tickFormat={(v) => formatCurrency(v as number)}
tickLabelProps={() => ({
fill: '#7C6755',
fontSize: 11,
textAnchor: 'end',
dx: '-0.5em',
dy: '0.33em'
})}
stroke="#EFE3D2"
tickStroke="#EFE3D2"
numTicks={6}
/>
</Group>
</svg>

{tooltipOpen && tooltipData && (
<BudgetChartTooltip
date={tooltipData.date}
spend={tooltipData.spend}
sinkOrSwim={tooltipData.sinkOrSwim}
top={tooltipTop ?? 0}
left={tooltipLeft ?? 0}
/>
</Group>
</svg>

{tooltipOpen && tooltipData && (
<BudgetChartTooltip
date={tooltipData.date}
spend={tooltipData.spend}
sinkOrSwim={tooltipData.sinkOrSwim}
top={tooltipTop ?? 0}
left={tooltipLeft ?? 0}
/>
)}
)}
</div>
</div>
);
}

export default function BudgetChart({ data }: BudgetChartProps) {
return <BudgetChartInner data={data} width={CHART_WIDTH} height={CHART_HEIGHT} />;
export default function BudgetChart({ data, period }: BudgetChartProps) {
return (
<BudgetChartInner
data={data}
period={period}
width={CHART_WIDTH}
height={CHART_HEIGHT}
/>
);
}
4 changes: 2 additions & 2 deletions apps/budget/src/components/budget/BudgetChartBars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function BudgetChartBars({
y={y}
width={bandwidth}
height={Math.max(0, barHeight)}
fill="rgba(100, 149, 237, 0.5)"
fill="#D8E9D2"
onMouseMove={(e) =>
onMouseMove(e as React.MouseEvent<SVGRectElement>, d)
}
Expand All @@ -61,7 +61,7 @@ export default function BudgetChartBars({
y={y}
width={bandwidth}
height={Math.max(0, barHeight)}
fill="rgba(250, 128, 114, 0.5)"
fill="#FFDFC7"
onMouseMove={(e) =>
onMouseMove(e as React.MouseEvent<SVGRectElement>, d)
}
Expand Down
Loading
Loading