AGENTS.md: record the rules the charts and the site cards now depend on

Three things in here cost real time to work out, so they are written down
rather than left to be re-derived:

- The Recharts constraints behind the profit line (a Line is one <Curve>,
  so <Cell> cannot recolour a segment; a Line with its own `data` corrupts
  the shared categorical axis; profit Lines must be direct children or they
  vanish silently; the tooltip needs custom `content`).
- globals.css hard-codes the axis tick font size, which beats Recharts'
  tick.fontSize prop — so the 12px mobile bump has to happen in that CSS
  rule or nowhere.
- /sites now defaults to Cards and must be kept in step with IncomeView.

Plus the two deliberate palette exceptions (--color-chart-loss,
--color-chart-profit) and the client rulings behind them.
This commit is contained in:
David 2026-10-03 09:20:29 +02:00
parent 649e495f49
commit 123484a8c4

View file

@ -94,6 +94,12 @@ Other rules worth not re-learning:
Months from other years are dropped, not folded in, and the panel says so.
- `SiteSummary.pinned` exists because the table is a client component and cannot look
the site up in the `sites` collection itself.
- **`/sites` defaults to the Cards view, deliberately mirroring `IncomeView`** — same
toolbar, same card shape, same 12-month strip on expand, same three-up footer. The
client likes the vehicles overview and asked for the sites to read the same way, so
keep the two in step when either changes. Grid is the old table, kept as a toggle.
The site detail link lives **inside the expanded panel only** — a card is a
`<button>`, so a nested `<a>` would be invalid HTML.
## Money on invoices — VAT is NOT revenue
@ -139,7 +145,59 @@ Coolors:
Chart marks (deeper steps of the same hues — see HANDOFF-PROMPT-3 §A.4):
- `--color-chart-income` (light `#0090A6`, dark `#00A6BE`)
- `--color-chart-fixed` (`#BE851F` both)
- `--color-chart-running` (light `#C13B4D`, dark `#D94F60`)
- `--color-chart-running` (`#8B5CF6` both) — the project's existing
`--color-brand-purple`, so Running introduces no new hue. **Client ruling, 2026-10-02:**
Running was moved off red because red is now reserved for a month that made a loss, and a
red Running line sitting next to a red loss segment was unreadable. Yellow was considered
and rejected — `--color-chart-fixed` is already a gold.
- `--color-chart-loss` (light `#C13B4D`, dark `#D94F60`) — a month that did not make
profit. **It has its own token on purpose:** it used to read
`--color-chart-running`, so recolouring Running silently dragged the loss red with it.
- `--color-chart-profit` (`#7A9E06` both) — the profit line's green. **Client ruling,
2026-10-02: this is a second deliberate exception to the "never change the palette" rule.**
It is a bright yellow-green, chosen so the profit line reads as its own series instead of
blending into the teal income line.
`globals.css` hard-codes `.recharts-cartesian-axis-tick text { font-size: 11px }`, which
**beats Recharts' `tick.fontSize` prop**. Any "bump chart text to 12px on mobile" work has to
happen in that CSS rule or it silently does nothing.
## The profit line on `/charts` — Recharts constraints, do not re-derive
`MachineLineChart` draws profit as teal where the month made money and red where it did not,
fading across each sign crossing. Three things make this fragile:
- **A `Line` is one `<Curve>` path.** `<Cell>` does **not** recolour a segment — Cells only
work for `Bar`. Per-segment colour therefore requires **one `<Line>` per same-sign run**,
plus one extra `<Line>` per crossing for the gradient.
- **Never give a `Line` its own `data` prop inside a shared chart.** Recharts builds a
categorical axis by concatenating the data of *every* series, so a short sliced `data`
re-adds the month labels: the axis renders "Jan … Aug, Jan … Aug" while that series plots
at the wrong months. Put each variant on the shared dataset as its own key
(`profitRun0`, `profitCross0`) and null it outside its run; `connectNulls={false}` then
draws just that run in the right place.
- **Profit `Line`s must be direct children of the chart.** Recharts parses only known child
types, so wrapping them in a `<g>` makes the entire profit line disappear without an error.
- **The tooltip needs a custom `content` component** (`ChartTooltip` in `MachineLineChart`).
Recharts feeds the tooltip one entry per series, so the per-run keys would surface as
`profitRun0` / `profitCross0` and "profit" would appear three or four times. The component
drops every profit-backed series and adds one computed row, coloured by the month's sign.
The base series carry an explicit `name`, because Recharts otherwise labels them by key
(`running` reads as "Running cost", which is what the key is called).
Two deliberate honesty rules: profit stays **null** in a month with no income figure (never
treated as 0, which would draw a green dot for a month that was never billed), and a month
with no `monthly_running_costs` row gets a **hollow dot** because its profit is optimistic.
`/charts` alone reads profit including running cost — `lib/profit.ts` keeps its workbook
default for every other surface, and the card badge, the profit line and the Profit bars
view all share the one flag in `ChartsGrid`.
The toolbar's **Series** button toggles which lines a card draws (`mp:chartsSeries` in
localStorage, so the choice survives a reload). Two rules keep it honest: a hidden series
leaves the legend **and its Y axis** with it, so there is never an axis for a line that is
not drawn; and the last visible series cannot be switched off, because an empty chart reads
as broken. The Profit toggle also owns the loss colour, since red is a sign of the profit
line rather than a series of its own.
## Cost model