The Brief's Value at stake card was a read-only figure. Vega proposed a number, counsel could only look at it. If the framer read ₹4.2 crore out of a quantum statement that actually said ₹42 crore, the Brief carried a wrong figure — the single most consequential number on the page — with no way to correct it in place.
This Story promotes the card: the amount edits in place under numeric validation, and the note beside it (what the figure covers — "principal only", "excluding interest") edits as a plain line.
The frozen vocabulary (docs/design/editable-surface-vocabulary.md §3, founder-ruled
on Spike-2316) settles this before anyone can argue it: "Validation is a prop axis, not a
kind or preset." The HTML precedent is exact — <input type="number"> is
still an <input>. So an amount is not a fifth kind and
not a new preset. It is the same EditableField with two extra
props:
// src/components/shared/editable-surface.tsx validate?: (next: string) => boolean; // false → reject the commit onInvalid?: (rejected: string) => void; // the caller toasts inputMode?: "text" | "decimal" | "numeric";
The whole seam lives in one place — the blur commit:
if (next !== current.value) {
if (current.validate && !current.validate(next)) {
instance.commands.setContent(...current.value...); // revert to the last good value
current.onInvalid?.(next); // caller toasts
} else {
current.onSave(next);
}
}
0; and (2) the silent unsaved edit — counsel types, the write is
dropped, the field looks committed, and the Brief quietly still says the old number. A rejected
commit here is visible: the field snaps back to the last good figure and a toast
says why.
Every editable-surface Story starts the same way: will the write path actually accept this? For Value at stake the answer is unusual — the amount and the note are written through two different endpoints, and getting that wrong would have shipped a field that looks saved and isn't.
| Value | Where the truth lives | Write path |
|---|---|---|
| Amount + currency | Canonical — matters.value_at_stake_minor / _currency |
PATCH /app/matters/{id} → useAppUpdateMatter (proxy already exists at /api/internal/matters/[matterId]) |
| Note | Frame — value_note on FrameOutput |
PATCH …/frame → the Brief's existing save("value_note") |
The trap is the amount. It is rendered canonical-first — valueAtStake() in
presentation.ts reads brief.canonical.valueAtStake and only falls back
to the frame's proposal when the canonical column is empty. So writing the edited amount to the
frame would have been swallowed whole: the PATCH succeeds, the frame changes, and the
Brief keeps rendering the old canonical figure. The edit vanishes with a green network tab.
pramaan-functions says the same thing in its own words:
src/domains/matter_frame/frame_contract.py # `case_ref`, `value_*`, and `relief_sought` are also Category-1 overlaps but their # accept is lock-only for now (canonical columns stay primary via their own editors)
value_note, which is in EDITABLE_FRAME_FIELDS
(frozenset(FrameOutput.model_fields) - meta). Both endpoints exist, both proxies
exist, nothing was stubbed.
The field displays the formatted figure — ₹4,20,00,000 — so counsel edits
what they read. The validator therefore has to accept the rendering it produced, plus the ways a
person actually types an amount:
// src/components/matter-detail/brief/presentation.ts
const AMOUNT_NOISE = /[₹,\s]/g; // the symbol we rendered, INR groups, spaces
const AMOUNT = /^\d+(\.\d{1,2})?$/; // what must be left over
isAmountValid("₹4,20,00,000") → true amountToMinor(...) → 4_200_000_000
isAmountValid("1,25,000.50") → true amountToMinor(...) → 12_500_050
isAmountValid("") → true (clearing is a legitimate edit → null)
isAmountValid("about 42 crore")→ false REJECTED — reverts + toasts
isAmountValid("42cr") → false
isAmountValid("1.234") → false (more precision than paise can hold)
Storage is minor units (paise), matching the column: Math.round(major × 100).
Clearing the amount clears the currency with it — the pair is one fact, and a currency with no
amount is a value at stake nobody can read.
The write is optimistic per AGENTS.md: onMutate patches
canonical.valueAtStake in the Brief's TanStack cache so the new figure renders the
moment counsel clicks away; the PATCH runs behind it; onError restores the snapshot
and toasts; onSettled invalidates to reconcile with server truth. The note rides the
frame-edit mutation that already had this exact shape.
The card is now always rendered, empty included — the placeholder is the empty state, the same add-first law as Relief sought and Key instruments. Previously a matter with no value at stake showed no card at all, which is precisely the matter where counsel most needs to type the number in. A non-monetary matter still reads rather than edits: that flag is set where the matter is configured, not on the Brief.
| File | Change |
|---|---|
src/components/shared/editable-surface.tsx | The validation axis: validate, onInvalid, inputMode. Reject → revert + notify, never persist. |
src/components/shared/editable-surface.stories.tsx | NumericValidated story — happy path AND the invalid-input rejection/revert, under Shared/EditableSurface. |
src/components/matter-detail/brief/presentation.ts | isAmountValid, amountToMinor, valueCurrency. |
src/components/matter-detail/brief/matter-brief-sections.tsx | The value card: ValueAmountField (validated) + the note as a plain EditableSurface. Always rendered. |
src/components/matter-detail/brief/matter-brief-page.tsx | The canonical amount write — useAppUpdateMatter, optimistic on the Brief cache, rollback + toast. |
messages/en.json, messages/hi.json | Placeholder, note label/placeholder, and the rejection toast — both locales. |
pnpm lint — clean (only pre-existing warnings).pnpm test — 1242 passing, including the new amount-validation suite in presentation.test.ts (accepts the rendered figure / INR groups / decimals / empty; rejects prose, suffixes, negatives, sub-paise precision; parses to minor units).pnpm build — clean.Shared/EditableSurface → NumericValidated — the reject-and-revert interaction, replayable.8 file(s), +271 / -21 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +4 | -0 |
| messages/hi.json | Config | +4 | -0 |
| src/components/matter-detail/brief/matter-brief-page.tsx | Component | +49 | -0 |
| src/components/matter-detail/brief/matter-brief-sections.tsx | Component | +65 | -12 |
| src/components/matter-detail/brief/presentation.test.ts | Component | +40 | -1 |
| src/components/matter-detail/brief/presentation.ts | Component | +32 | -0 |
| src/components/shared/editable-surface.stories.tsx | Component | +43 | -0 |
| src/components/shared/editable-surface.tsx | Component | +34 | -8 |