Files
budget2.0/src/ui/components/Section.tsx
T
Lucas WintherandClaude Opus 5 cbab7358b0 fix(ui): cap the hint measure, and pad the last two bare controls
Section's hint ran the full width of the card — about 160 characters a line
at 12px on a 1140px card, more than twice a readable measure, and the
Currencies and Posts hints both wrapped to three lines that read as a wall
rather than as one aside. Capped at 75ch.

PostTable's "change from here" was a single line of 11px text with no
padding, so a 15px target. It gets padding cancelled by equal negative
margins, so the hit area grows and no baseline moves.

UpdatePrompt used `rounded` (4px) where the card system is 8px, which made
the one panel that appears over everything else the only one with a tighter
radius than its surroundings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 22:59:14 +02:00

76 lines
2.7 KiB
TypeScript

import type { ReactNode } from "react";
interface Props {
/** Omitted when the card's contents are self-explanatory (a toolbar). */
title?: string;
/** Sits opposite the title: a primary action, a mode switch. */
action?: ReactNode;
/** One line under the title. Long explanations belong in the body. */
hint?: ReactNode;
children: ReactNode;
className?: string;
}
/**
* The one card in the app. Every screen is a stack of these on the paper
* ground, which is what stops elevation, radius and padding drifting apart
* screen by screen — the previous design defined a card treatment in a
* contract document and then never applied it anywhere.
*
* The `.budget-card` class carries the visual treatment (see src/index.css) so
* light and dark elevation differ where they must: a drop shadow is invisible
* on a dark ground, where a lighter surface plus a hairline does the work.
*/
export function Section({ title, action, hint, children, className = "" }: Props) {
return (
<section className={`budget-card p-4 sm:p-5 ${className}`}>
{(title || action) && (
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<div className="min-w-0">
{title && <h2 className="text-base leading-tight">{title}</h2>}
{/* Capped measure. Uncapped, the Currencies and Posts hints ran
the full 1140px of the card at 12px — about 160 characters a
line, more than twice a readable measure, and both wrapped to
three lines that read as a wall rather than as one aside. */}
{hint && (
<p className="mt-1 max-w-[75ch] text-xs leading-relaxed text-budget-ink-muted">
{hint}
</p>
)}
</div>
{action && <div className="flex shrink-0 items-center gap-2">{action}</div>}
</div>
)}
{children}
</section>
);
}
/**
* A single figure with its label. Used for the month's totals, where the
* numbers matter more than the words: the label is small and quiet, the figure
* is large and mono so a column of them lines up.
*/
export function Stat({
label,
children,
tone = "default",
title,
}: {
label: string;
children: ReactNode;
tone?: "default" | "surplus" | "overspend";
title?: string;
}) {
const toneClass =
tone === "overspend" ? "text-overspend" : tone === "surplus" ? "text-surplus" : "";
return (
<div className="min-w-0" title={title}>
<dt className="text-[0.6875rem] font-medium uppercase tracking-wider text-budget-ink-muted">
{label}
</dt>
<dd className={`font-money mt-0.5 text-lg leading-tight ${toneClass}`}>{children}</dd>
</div>
);
}