fix(views): segmented switches on the year and summary views
Replaces the two-loose-buttons pattern with Segmented on both, so spend/balance and by-post/by-month read as two states of one control rather than a filled primary action beside an unrelated outline button. Summary also had two row-height defects: "All time" was `sm` in an `items-end` row of default-height fields, so it sat 4px off their baseline; and the by-month branch's first cell was `py-2` where every other cell in the table is `py-2.5`, making those rows 4px shorter than the by-post ones. The rest of the diff is indentation — both Sections had their children at the parent's own indent level. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
25ad22a4e7
commit
7b7d36dc74
2 files changed
+83
-89
No files matched your search
@@ -7,6 +7,7 @@ import { useDataset } from "../hooks/useDataset.ts";
|
||||
import { datasetMonthSpan, summaryView } from "../../domain/views.ts";
|
||||
import { formatAmount, formatMoney } from "../format.ts";
|
||||
import { Section, Stat } from "../components/Section.tsx";
|
||||
import { Segmented } from "../components/Segmented.tsx";
|
||||
|
||||
export function SummaryRoute() {
|
||||
const dataset = useDataset();
|
||||
@@ -24,91 +25,87 @@ export function SummaryRoute() {
|
||||
<h1 className="text-2xl">Summary</h1>
|
||||
|
||||
<Section>
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="from">From</Label>
|
||||
<Input id="from" className="w-32" value={from} onChange={(e) => setFrom(e.target.value)} />
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="from">From</Label>
|
||||
<Input id="from" className="w-32" value={from} onChange={(e) => setFrom(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="to">To</Label>
|
||||
<Input id="to" className="w-32" value={to} onChange={(e) => setTo(e.target.value)} />
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setFrom(span.from);
|
||||
setTo(span.to);
|
||||
}}
|
||||
>
|
||||
All time
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="to">To</Label>
|
||||
<Input id="to" className="w-32" value={to} onChange={(e) => setTo(e.target.value)} />
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setFrom(span.from);
|
||||
setTo(span.to);
|
||||
}}
|
||||
>
|
||||
All time
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<dl className="mt-5 flex flex-wrap gap-x-10 gap-y-4 border-t border-budget-rule pt-5">
|
||||
<dl className="mt-5 flex flex-wrap gap-x-10 gap-y-4 border-t border-budget-rule pt-5">
|
||||
<Stat label="Income">{formatMoney(view.totalIncome, base)}</Stat>
|
||||
<Stat label="Spent">{formatMoney(view.totalCharges, base)}</Stat>
|
||||
<Stat label="Difference" tone={difference < 0 ? "overspend" : "default"}>
|
||||
{formatMoney(difference, base)}
|
||||
</Stat>
|
||||
</dl>
|
||||
<Stat label="Difference" tone={difference < 0 ? "overspend" : "default"}>
|
||||
{formatMoney(difference, base)}
|
||||
</Stat>
|
||||
</dl>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title={groupBy === "post" ? "By post" : "By month"}
|
||||
action={
|
||||
<div className="flex gap-1">
|
||||
{(["post", "month"] as const).map((option) => (
|
||||
<Button
|
||||
key={option}
|
||||
size="sm"
|
||||
variant={groupBy === option ? "default" : "outline"}
|
||||
onClick={() => setGroupBy(option)}
|
||||
>
|
||||
by {option}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Segmented
|
||||
label="Group rows by"
|
||||
value={groupBy}
|
||||
onChange={setGroupBy}
|
||||
options={[
|
||||
{ value: "post", label: "By post" },
|
||||
{ value: "month", label: "By month" },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-left">
|
||||
<tr className="border-b border-budget-rule text-[0.6875rem] uppercase tracking-wider text-budget-ink-muted">
|
||||
<th className="py-2 font-medium">{groupBy === "post" ? "Post" : "Month"}</th>
|
||||
<th className="py-2 pl-6 text-right font-medium">Spent</th>
|
||||
<th className="py-2 pl-6 text-right font-medium">Share</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groupBy === "post"
|
||||
? view.byPost.map((entry) => (
|
||||
<tr key={entry.post.id} className="border-b border-budget-rule transition-colors last:border-0 hover:bg-accent/60">
|
||||
<td className="py-2.5">{entry.post.name}</td>
|
||||
<td className="font-money py-2.5 pl-6 text-right">{formatAmount(entry.charges)}</td>
|
||||
<td className="font-money py-2.5 pl-6 text-right text-budget-ink-muted">
|
||||
{view.totalCharges === 0
|
||||
? "—"
|
||||
: `${((entry.charges / view.totalCharges) * 100).toFixed(1)}%`}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
: view.byMonth.map((entry) => (
|
||||
<tr key={entry.monthId} className="border-b border-budget-rule transition-colors last:border-0 hover:bg-accent/60">
|
||||
<td className="py-2">
|
||||
<Link to={`/month/${entry.monthId}`} className="hover:underline">
|
||||
{entry.monthId}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="font-money py-2.5 pl-6 text-right">{formatAmount(entry.charges)}</td>
|
||||
<td className="font-money py-2.5 pl-6 text-right text-budget-ink-muted">
|
||||
{view.totalCharges === 0
|
||||
? "—"
|
||||
: `${((entry.charges / view.totalCharges) * 100).toFixed(1)}%`}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-left">
|
||||
<tr className="border-b border-budget-rule text-[0.6875rem] uppercase tracking-wider text-budget-ink-muted">
|
||||
<th className="py-2 font-medium">{groupBy === "post" ? "Post" : "Month"}</th>
|
||||
<th className="py-2 pl-6 text-right font-medium">Spent</th>
|
||||
<th className="py-2 pl-6 text-right font-medium">Share</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groupBy === "post"
|
||||
? view.byPost.map((entry) => (
|
||||
<tr key={entry.post.id} className="border-b border-budget-rule transition-colors last:border-0 hover:bg-accent/60">
|
||||
<td className="py-2.5">{entry.post.name}</td>
|
||||
<td className="font-money py-2.5 pl-6 text-right">{formatAmount(entry.charges)}</td>
|
||||
<td className="font-money py-2.5 pl-6 text-right text-budget-ink-muted">
|
||||
{view.totalCharges === 0
|
||||
? "—"
|
||||
: `${((entry.charges / view.totalCharges) * 100).toFixed(1)}%`}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
: view.byMonth.map((entry) => (
|
||||
<tr key={entry.monthId} className="border-b border-budget-rule transition-colors last:border-0 hover:bg-accent/60">
|
||||
<td className="py-2.5">
|
||||
<Link to={`/month/${entry.monthId}`} className="hover:underline">
|
||||
{entry.monthId}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="font-money py-2.5 pl-6 text-right">{formatAmount(entry.charges)}</td>
|
||||
<td className="font-money py-2.5 pl-6 text-right text-budget-ink-muted">
|
||||
{view.totalCharges === 0
|
||||
? "—"
|
||||
: `${((entry.charges / view.totalCharges) * 100).toFixed(1)}%`}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
|
||||
+12
-15
@@ -1,10 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import { Link, useParams } from "react-router";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useDataset } from "../hooks/useDataset.ts";
|
||||
import { yearView } from "../../domain/views.ts";
|
||||
import { formatMoney } from "../format.ts";
|
||||
import { YearMatrix } from "../components/YearMatrix.tsx";
|
||||
import { Segmented } from "../components/Segmented.tsx";
|
||||
import { Section, Stat } from "../components/Section.tsx";
|
||||
|
||||
export function YearRoute() {
|
||||
@@ -20,14 +20,14 @@ export function YearRoute() {
|
||||
<header className="flex items-baseline gap-3">
|
||||
<Link
|
||||
to={`/year/${numericYear - 1}`}
|
||||
className="font-money rounded-md px-1.5 py-0.5 text-xs text-budget-ink-muted transition-colors hover:bg-accent hover:text-budget-ink"
|
||||
className="font-money shrink-0 rounded-md px-1.5 py-1 text-xs whitespace-nowrap text-budget-ink-muted transition-colors hover:bg-accent hover:text-budget-ink"
|
||||
>
|
||||
← {numericYear - 1}
|
||||
</Link>
|
||||
<h1 className="text-2xl">{year}</h1>
|
||||
<Link
|
||||
to={`/year/${numericYear + 1}`}
|
||||
className="font-money rounded-md px-1.5 py-0.5 text-xs text-budget-ink-muted transition-colors hover:bg-accent hover:text-budget-ink"
|
||||
className="font-money shrink-0 rounded-md px-1.5 py-1 text-xs whitespace-nowrap text-budget-ink-muted transition-colors hover:bg-accent hover:text-budget-ink"
|
||||
>
|
||||
{numericYear + 1} →
|
||||
</Link>
|
||||
@@ -43,18 +43,15 @@ export function YearRoute() {
|
||||
<Section
|
||||
title="Posts by month"
|
||||
action={
|
||||
<div className="flex gap-1">
|
||||
{(["spend", "balance"] as const).map((option) => (
|
||||
<Button
|
||||
key={option}
|
||||
size="sm"
|
||||
variant={mode === option ? "default" : "outline"}
|
||||
onClick={() => setMode(option)}
|
||||
>
|
||||
{option === "spend" ? "Spent" : "Closing balance"}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Segmented
|
||||
label="Figures to show"
|
||||
value={mode}
|
||||
onChange={setMode}
|
||||
options={[
|
||||
{ value: "spend", label: "Spent" },
|
||||
{ value: "balance", label: "Closing balance" },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<YearMatrix view={view} mode={mode} />
|
||||
|
||||
Reference in new issue
Block a user