From 100fe116b45004e911214e50d8309bb2655438a1 Mon Sep 17 00:00:00 2001
From: Claude
Date: Sun, 4 Oct 2026 01:36:52 +0000
Subject: [PATCH 1/2] Plan reads like every other tab, and its pages open on
one statement
- Plan's masthead sits where every tab's does, with the calendar and the
page's actions beside it and a full-width Recurring | Buckets switch under
it (PlanTabs becomes PlanHeader).
- Buckets and Income open on the same hero Recurring does: a shared
HeroCard and Ribbon, with on-hand by bucket and monthly income by person.
- The recurring chip rail no longer snaps "All" flush to the card edge.
- Bucket activity no longer pops into the ledger after arrival: an absent
`movements` param now defers to the saved preference on the server, so the
first paint has the rows and the post-mount second navigation is gone.
- Inline edit forms are inset inside their cards; list tiles, action order
and small meta text match across Recurring, Buckets and Income.
- New purchase: the currency symbol hugs the amount. Statement: a zero
figure carries no "+". Categories: the "Bought" caption no longer clips.
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_019iNojndGyPD9Kt2Db6gCCf
---
src/lib/components/HeroCard.svelte | 69 ++++++++++
src/lib/components/Money.svelte | 3 +-
src/lib/components/PlanHeader.svelte | 64 +++++++++
src/lib/components/PlanTabs.svelte | 49 -------
src/lib/components/RecurringBreakdown.svelte | 106 ++++----------
src/lib/components/Ribbon.svelte | 106 ++++++++++++++
src/lib/ledger-query.test.ts | 21 +++
src/lib/ledger-query.ts | 17 ++-
src/routes/w/[workspace]/buckets/+page.svelte | 130 ++++++++++--------
src/routes/w/[workspace]/buckets/handlers.ts | 5 +
src/routes/w/[workspace]/income/+page.svelte | 84 ++++++++++-
src/routes/w/[workspace]/income/handlers.ts | 13 ++
.../w/[workspace]/purchases/+page.svelte | 53 ++++---
.../w/[workspace]/purchases/data/handlers.ts | 6 +-
.../w/[workspace]/purchases/handlers.ts | 10 +-
.../w/[workspace]/purchases/new/+page.svelte | 12 +-
.../w/[workspace]/recurring/+page.svelte | 33 +++--
.../settings/categories/+page.svelte | 8 +-
18 files changed, 546 insertions(+), 243 deletions(-)
create mode 100644 src/lib/components/HeroCard.svelte
create mode 100644 src/lib/components/PlanHeader.svelte
delete mode 100644 src/lib/components/PlanTabs.svelte
create mode 100644 src/lib/components/Ribbon.svelte
create mode 100644 src/lib/ledger-query.test.ts
diff --git a/src/lib/components/HeroCard.svelte b/src/lib/components/HeroCard.svelte
new file mode 100644
index 0000000..8ef51dc
--- /dev/null
+++ b/src/lib/components/HeroCard.svelte
@@ -0,0 +1,69 @@
+
+
+
+
+ {#if icon}{icon}{/if}
+ {overline}
+
+
+ {#if line}
+
+ {@render line()}
+
+ {/if}
+ {@render children?.()}
+
diff --git a/src/lib/components/Money.svelte b/src/lib/components/Money.svelte
index a16a4b6..d843ff6 100644
--- a/src/lib/components/Money.svelte
+++ b/src/lib/components/Money.svelte
@@ -32,7 +32,8 @@
let text = $derived.by(() => {
const body = formatMinor(minor < 0n ? -minor : minor, currency);
- const prefix = minor < 0n ? '−' : sign ? '+' : '';
+ // Zero carries no sign: "+$0.00" under Spending read as money coming in.
+ const prefix = minor < 0n ? '−' : sign && minor > 0n ? '+' : '';
// The sign goes with the digits when masked: a leading − would announce
// "you're under", which is exactly what the mask is meant to withhold.
return masked ? maskAmount(body) : prefix + body;
diff --git a/src/lib/components/PlanHeader.svelte b/src/lib/components/PlanHeader.svelte
new file mode 100644
index 0000000..d2ca2d5
--- /dev/null
+++ b/src/lib/components/PlanHeader.svelte
@@ -0,0 +1,64 @@
+
+
+
diff --git a/src/lib/components/RecurringBreakdown.svelte b/src/lib/components/RecurringBreakdown.svelte
index f3683fa..55d2483 100644
--- a/src/lib/components/RecurringBreakdown.svelte
+++ b/src/lib/components/RecurringBreakdown.svelte
@@ -44,7 +44,8 @@
*/
import { ArrowRight } from '@lucide/svelte';
import { prefersReducedMotion } from 'svelte/motion';
- import Money from '$lib/components/Money.svelte';
+ import HeroCard from '$lib/components/HeroCard.svelte';
+ import Ribbon from '$lib/components/Ribbon.svelte';
import { formatMinor } from '$lib/money-format';
import { ledgerLink, NO_CATEGORY } from '$lib/ledger-filters';
@@ -87,14 +88,6 @@
return `${Math.round(f * 100)}%`;
}
- // The ribbon grows in from nothing once, on mount, so the proportions arrive
- // as a gesture rather than a flash. Two frames so there is a from-state.
- let drawn = $state(false);
- $effect(() => {
- const id = requestAnimationFrame(() => requestAnimationFrame(() => (drawn = true)));
- return () => cancelAnimationFrame(id);
- });
-
// The chosen chip is brought into view — it may be the smallest category,
// last on the rail, and arriving by link or by tapping the ribbon would
// otherwise leave the one highlighted control off the edge of the screen.
@@ -156,55 +149,31 @@
}
-
-
-
- {#if focus?.icon}{focus.icon}{/if}
- {headline.overline} · per month
-
-
-
+ {#snippet line()}
{formatMinor(headline.yearly, currency)} a year
· {headline.line}
-
+ {/snippet}
{#if costs.length > 0}
-
-
`${c.name} ${pct(share(c.monthlyMinor))}`).join(', ')}
- >
- {#each costs as c (c.key)}
- {@const on = !focus || focus.key === c.key}
-
- {/each}
-