style: SubscriptionsPage layout and responsiveness pass

- Page width consistent with rest of app
- Subscription/recommendation names no longer overflow
- Improved mobile/tablet wrapping for rows, amounts, action buttons
- Two-column layout delayed until very wide screens
- Added missing labels for food, education, shopping, security types
This commit is contained in:
null 2026-05-29 03:49:36 -05:00
parent 6b30ee4eb7
commit c3c0ab3542
2 changed files with 27 additions and 19 deletions

View File

@ -35,6 +35,10 @@ const TYPE_LABELS = {
gaming: 'Gaming',
utilities: 'Utilities',
insurance: 'Insurance',
food: 'Food',
education: 'Education',
shopping: 'Shopping',
security: 'Security',
other: 'Other',
};
@ -45,12 +49,12 @@ function cycleLabel(item) {
function StatCard({ icon: Icon, label, value, hint }) {
return (
<div className="rounded-lg border border-border/70 bg-card/80 p-4">
<div className="min-w-0 rounded-lg border border-border/70 bg-card/80 p-4">
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-foreground/70">
<Icon className="h-4 w-4 text-primary" />
{label}
<span className="truncate">{label}</span>
</div>
<p className="tracker-number mt-2 text-2xl font-bold tracking-tight text-foreground">{value}</p>
<p className="tracker-number mt-2 truncate text-2xl font-bold tracking-tight text-foreground">{value}</p>
{hint && <p className="mt-1 text-xs font-medium text-muted-foreground">{hint}</p>}
</div>
);
@ -59,7 +63,8 @@ function StatCard({ icon: Icon, label, value, hint }) {
function SubscriptionRow({ item, onEdit, onToggle }) {
return (
<div className={cn(
'flex flex-col gap-3 border-b border-border/40 px-4 py-4 last:border-b-0 sm:flex-row sm:items-center',
'grid min-w-0 gap-3 border-b border-border/40 px-4 py-4 last:border-b-0',
'md:grid-cols-[minmax(0,1fr)_14rem_auto] md:items-center',
!item.active && 'opacity-60',
)}>
<div className="min-w-0 flex-1">
@ -67,7 +72,7 @@ function SubscriptionRow({ item, onEdit, onToggle }) {
<button
type="button"
onClick={() => onEdit(item)}
className="truncate text-left text-[15px] font-semibold text-foreground hover:text-primary"
className="min-w-0 max-w-full truncate text-left text-[15px] font-semibold text-foreground hover:text-primary"
>
{item.name}
</button>
@ -88,7 +93,7 @@ function SubscriptionRow({ item, onEdit, onToggle }) {
</div>
</div>
<div className="grid grid-cols-2 gap-3 sm:w-[260px]">
<div className="grid grid-cols-2 gap-3 md:w-56">
<div>
<p className="text-xs text-muted-foreground">Per cycle</p>
<p className="tracker-number text-sm font-semibold text-foreground">{fmt(item.expected_amount)}</p>
@ -99,15 +104,18 @@ function SubscriptionRow({ item, onEdit, onToggle }) {
</div>
</div>
<div className="flex gap-2 sm:justify-end">
<Button type="button" variant="outline" size="sm" onClick={() => onEdit(item)}>
<div className="flex gap-2 md:justify-end">
<Button type="button" variant="outline" size="sm" className="flex-1 md:flex-none" onClick={() => onEdit(item)}>
Edit
</Button>
<Button
type="button"
variant="ghost"
size="sm"
className={item.active ? 'text-amber-300 hover:bg-amber-500/10 hover:text-amber-200' : 'text-emerald-300 hover:bg-emerald-500/10 hover:text-emerald-200'}
className={cn(
'flex-1 md:flex-none',
item.active ? 'text-amber-300 hover:bg-amber-500/10 hover:text-amber-200' : 'text-emerald-300 hover:bg-emerald-500/10 hover:text-emerald-200',
)}
onClick={() => onToggle(item)}
>
{item.active ? 'Pause' : 'Resume'}
@ -189,8 +197,8 @@ function RecommendationCard({ recommendation, categoryId, onAccept, onDecline, o
<div className="rounded-lg border border-primary/20 bg-primary/[0.05] p-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<p className="truncate text-sm font-semibold text-foreground">{recommendation.name}</p>
<div className="flex min-w-0 flex-wrap items-center gap-2">
<p className="min-w-0 max-w-full truncate text-sm font-semibold text-foreground">{recommendation.name}</p>
<Badge variant="outline" className="border-primary/25 bg-primary/10 text-primary">
{recommendation.confidence}% match
</Badge>
@ -211,7 +219,7 @@ function RecommendationCard({ recommendation, categoryId, onAccept, onDecline, o
<p className="tracker-number text-xs font-semibold text-emerald-600 dark:text-emerald-300">
{fmt(recommendation.monthly_equivalent)} / mo
</p>
<div className="mt-3 flex flex-wrap gap-2 sm:justify-end">
<div className="mt-3 grid grid-cols-1 gap-2 min-[380px]:grid-cols-3 sm:flex sm:flex-wrap sm:justify-end">
<Button
type="button"
size="sm"
@ -383,9 +391,9 @@ export default function SubscriptionsPage() {
const paused = subscriptions.filter(item => !item.active);
return (
<div className="space-y-6">
<div className="mx-auto w-full max-w-6xl space-y-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<div className="min-w-0">
<p className="mb-1 text-[11px] font-semibold uppercase tracking-[0.15em] text-muted-foreground">
Recurring Services
</p>
@ -394,7 +402,7 @@ export default function SubscriptionsPage() {
Track manual subscriptions and review recurring SimpleFIN charges.
</p>
</div>
<div className="flex flex-wrap gap-2">
<div className="grid grid-cols-2 gap-2 sm:flex sm:flex-wrap">
<Button type="button" variant="outline" className="gap-2" onClick={refreshAll} disabled={loading || recommendationsLoading}>
<RefreshCw className={cn('h-4 w-4', (loading || recommendationsLoading) && 'animate-spin')} />
Refresh
@ -413,9 +421,9 @@ export default function SubscriptionsPage() {
<StatCard icon={Cloud} label="Top Type" value={summary.top_type ? TYPE_LABELS[summary.top_type.type] || summary.top_type.type : '—'} hint={summary.top_type ? `${fmt(summary.top_type.monthly_total)} / mo` : 'No active subscriptions'} />
</div>
<div className="grid gap-5 xl:grid-cols-[minmax(0,1fr)_420px]">
<div className="grid min-w-0 gap-5 2xl:grid-cols-[minmax(0,1fr)_minmax(360px,420px)]">
<Card className="overflow-hidden">
<CardHeader className="pb-3">
<CardHeader className="px-4 pb-3 sm:px-6">
<CardTitle className="text-base">Tracked Subscriptions</CardTitle>
<CardDescription>Subscriptions are bills with recurring-service metadata.</CardDescription>
</CardHeader>
@ -446,7 +454,7 @@ export default function SubscriptionsPage() {
</Card>
<Card className="overflow-hidden">
<CardHeader className="pb-3">
<CardHeader className="px-4 pb-3 sm:px-6">
<div className="flex items-center gap-2">
<Sparkles className="h-4 w-4 text-primary" />
<CardTitle className="text-base">SimpleFIN Recommendations</CardTitle>

View File

@ -1,6 +1,6 @@
{
"name": "bill-tracker",
"version": "0.33.6",
"version": "0.33.7",
"description": "Monthly bill tracking system",
"main": "server.js",
"scripts": {