Commit Graph
24 Commits
Author SHA1 Message Date
Lucas WintherandClaude Opus 5 5a11941d39 feat(ui): add purchase dialog with split editor
Validation lives in purchaseForm.ts as pure functions. Unbalanced splits
and over-budget purchases are deliberately not errors.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-01 20:48:51 +02:00
Lucas WintherandClaude Opus 5 b278dcaf12 fix(ui): namespace carry-meter accent token to avoid shadcn contrast regression
A bare --accent in src/index.css silently overrode shadcn's own
--accent/--accent-foreground pairing (near-white background paired with
near-black text) with the design contract's dark teal, without touching
the paired foreground. That dropped the computed contrast ratio on every
outline/ghost button, select item, badge link and dialog close button
that uses bg-accent/text-accent-foreground to 3.23:1 -- below WCAG AA's
4.5:1 minimum for text.

Renamed the token to --budget-accent everywhere it's declared and
consumed (src/index.css, src/ui/components/PostTable.tsx's carry meter),
leaving shadcn's own --accent/--accent-foreground pair untouched. The
other five contract tokens (--paper, --ink, --rule, --surplus,
--overspend) don't collide with any shadcn token name in
styles/globals.css, so they are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-01 20:39:52 +02:00
Lucas WintherandClaude Opus 5 dd612565cb feat(ui): add month view with income entry and post table
Negative remaining balances are shown in a warning treatment; nothing
blocks going over budget.

Applies the Tasks 18-23 design contract: --paper/--ink/--rule/--accent/
--surplus/--overspend tokens and the three type-role font variables in
src/index.css, --font-money on every money figure, and the carry meter
(a 3px hard-stopped gradient beneath each PostTable row) encoding
carried-in surplus, allocation track, spend fill and overrun.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-01 20:35:35 +02:00
Lucas WintherandClaude Opus 5 ccb97818d7 feat(store): add plan slicing and dataset actions
Every mutation is a pure function over a draft dataset, so the write
surface is unit-tested without React. Money amounts (income, purchase
totals, split values, schedule slices, fixed/percent rule amounts) are
rounded to their currency's minor unit before they land in the draft,
per CLAUDE.md's round-before-persisting rule -- actions.ts is the
single boundary every UI mutation passes through. FxRate.baseUnitsPerOne
is deliberately exempt: it is stored at six decimal places, not money.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-01 20:25:14 +02:00
Lucas Winther 7edcc7f31e feat(ui): add app shell, router, and base path handling
Base path comes from BUN_PUBLIC_BASE_PATH so the same bundle serves both
a root Docker deploy and a GitHub Pages subpath.

Deviation from the task-16 brief: does not install @happy-dom/global-registrator,
does not create test/setup.ts, and does not add a [test] preload to bunfig.toml.
A global DOM preload would make src/domain/smoke.test.ts's "importable without a
DOM" assertion vacuously true, silently retiring the check that guards the
domain-layer-stays-pure invariant. Nothing in this task needs a DOM: basePath.test.ts
is pure string logic, and the shell render test was intentionally omitted by the
brief itself. A later task will register happy-dom per-file where a DOM is
genuinely required.
2026-09-01 20:17:57 +02:00
Lucas Winther 69bf4393fb feat(export): add ODS workbook export
Generated in-browser with fflate, so export works offline. mimetype is
stored first and uncompressed as the ODS spec requires.

Also adds datasetMonthSpan to the domain views layer, which determines
the month range an export should cover: from the fold start through
the latest month touched by any month record or purchase slice,
including future finance-plan slices.

Widened the mimetype-header test's byte slice from 64 to 96 bytes: the
zip local-file header for "mimetype" is 38 bytes and ODS_MIMETYPE is
46 characters, so 64 bytes could never contain the full string
regardless of implementation.
2026-09-01 20:11:08 +02:00
Lucas WintherandClaude Opus 5 c627051e66 feat(export): build ODS XML documents
Numeric cells carry office:value so the export can be summed and pivoted
in a spreadsheet rather than arriving as text.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-01 20:05:40 +02:00
Lucas WintherandClaude Opus 5 932b88b189 feat(export): add JSON export and validated import
Import validates far enough that the fold cannot throw on imported data:
exactly one remainder-absorbing split per purchase, at least one split,
every split.postId resolving to a real post, every currency supported,
and every MonthId well-formed and in range (01-12). Over-budget and
percentages not summing to 100 remain permitted.

Tightens MONTH_ID to /^\d{4}-(0[1-9]|1[0-2])$/ (shape-only regex would
let "2026-13" through import and silently misfile inside the balance
fold, mirroring the defect already fixed in src/domain/months.ts).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-01 20:02:02 +02:00
Lucas WintherandClaude Opus 5 0625a5ffa5 feat(store): add in-memory snapshot with write-through
Persistence is injected, so the store is testable with no browser. Writes
land before the snapshot moves, so the UI never shows unsaved data.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-01 19:58:47 +02:00
Lucas WintherandClaude Opus 5 15ae45a804 feat(store): add IndexedDB persistence and schema migrations
The dataset is one atomic record, so a write can never leave posts and
purchases inconsistent.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-01 19:55:08 +02:00
Lucas WintherandClaude Opus 5 438d53d016 feat(domain): add seed dataset and domain barrel
Add SCHEMA_VERSION and createSeedDataset to initialize first-run state
with three budget envelopes (Video Games, Food, Events and Social),
newId() for generating unique identifiers, and complete the domain
export barrel with all pure domain modules.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-01 19:51:57 +02:00
Lucas Winther 564a5f4079 feat(domain): add month, year, and summary view models
All three aggregate over foldBalances rather than recomputing rollover.
2026-09-01 19:48:48 +02:00
Lucas Winther 4077036ca7 feat(domain): add the envelope rollover fold
Unspent carries forward, overspend carries as debt. This is the single
implementation of the rollover math.
2026-09-01 19:45:31 +02:00
Lucas Winther 71d69c20b3 feat(domain): distribute purchase slices across posts
Slices are weighted by the whole-purchase split parts, so financed split
purchases need no special case and parts always sum to the slice total.
2026-09-01 19:40:32 +02:00
Lucas WintherandClaude Opus 5 0df1971a03 feat(domain): resolve allocation rules
Month overrides win over standing rules; percentages above 100 are allowed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-01 19:37:09 +02:00
Lucas WintherandClaude Opus 5 9af581e69c fix(domain): add month range validation to parse and monthOf
Shape-valid but out-of-range months (e.g., month 13 or 00) previously
normalized silently through ordinal arithmetic, resulting in wrong months
downstream. Now throw on invalid month values (< 1 or > 12), with the
offending value in the error message.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-01 16:33:27 +02:00
Lucas WintherandClaude Opus 5 bab6fc5f3e feat(domain): add currency conversion
Missing rates throw MissingRateError rather than silently treating the
value as base currency.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-01 16:31:22 +02:00
Lucas WintherandClaude Opus 5 261c6fa9ad feat(domain): add MonthId arithmetic
Uses a month ordinal rather than Date, so month stepping is timezone-proof.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-01 16:28:18 +02:00
Lucas Winther b2fa0e1b58 feat(domain): add money arithmetic with exact distribution
Distribution computes the remainder part by subtraction so float splits
always sum exactly to the total. Omits money() and addAmounts() from the
brief since no downstream task consumes either and the brief's own tests
don't cover them.
2026-09-01 16:25:40 +02:00
Lucas Winther e1ac6d9fac feat(domain): add domain types 2026-09-01 16:22:40 +02:00
Lucas Winther 26752aa225 chore: scaffold Bun + React + shadcn project
bun init --react=shadcn, with the demo API routes removed since this app
has no backend. Adds project invariants to CLAUDE.md and a smoke test
proving the domain layer imports with no DOM.
2026-09-01 16:18:55 +02:00
Lucas WintherandClaude Opus 5 abd77c58ed docs: add budget app implementation plan
25 TDD tasks across five phases: foundation, pure domain core, IndexedDB
persistence, JSON/ODS export, UI, and shipping (PWA + Docker + CI).

Self-review caught five defects, fixed inline:
- distributeByWeight divided by zero on all-zero weights, which a
  zero-total purchase reaches and which would take the whole fold down
- summaryView's fold binding needed an explicit type or CI typecheck fails
- a duplicated module import in registerSw.ts
- a shell render test that would break once its routes read the store,
  since happy-dom has no IndexedDB
- an unreadable assertion in the ODS numeric-cell test

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-01 16:13:40 +02:00
Lucas WintherandClaude Opus 5 6833a58c91 docs: add budget app design spec
Design for a personal budgeting PWA replacing a spreadsheet workflow.

Key decisions captured:
- Static SPA, no backend in the data path, so it deploys to both GitHub
  Pages and a self-hosted Docker image from one build artifact
- Bun toolchain (bun init --react=shadcn); no Vite, no Electron
- Pure derivation core over an IndexedDB fact store; envelope rollover
  makes caching worthless, so every figure is folded from raw facts
- One Purchase entity covers simple, split, and financed purchases, which
  is what lets splits and finance plans compose without special-casing
- Money is a float, with mandatory rounding at boundaries and last-part-
  by-subtraction so distributed parts sum exactly to the whole

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-01 15:50:46 +02:00
Lucas Winther eba36fad9d Initial commit 2026-09-01 15:23:37 +02:00