v0.46.0 — Character Sheet Overhaul: Edit Mode for System 8
v0.45.0 shipped the overhauled sheet read-only. This release gives it a write side: Build mode stops being a label and starts making fields editable, the interim Token Context edit mode retires, and attack authoring gets a module-owned seam.
Main article: Character Sheet Overhaul · task list: todo-v2.md · sheet spec: phase-1-character-sheet.md
Module-side work lives in the sibling
vtt-modulesrepo. v0.46.1 is the manifest bump that carries System 80.6.0and dnd5e0.2.0into a deployment.
Phase A — Attack authoring
Injected at the user's request after N9 found Attacks had no Add. Attacks are authored, not chosen
from a catalogue, and their shape is system vocabulary: a 3e attack line reads 1d20+7, 20/×2, range 10/20, which means nothing in System 8. So the editor and the play-mode summary both belong to the
module.
- [x] A1 — the seams, as two additive Module API members. Two of the three options considered
converge: CS-7 has carried a React component from a module since Module API
1.5.0(typedanyinsharedto keep that package React-free, cast at the client registry), so no new API is warranted. CS-9attackEditoris CS-7's trick for a smaller slot; CS-1.7formatAttackis CS-1.6formatAttribute's exact sibling.MODULE_API_VERSION→ 1.8.0, additive — every 1.7.0 module keeps working. - [x] A2 / A3 / A5 — render module-authored attacks: summary, editor, write path. Done together
because all three meet in
<AttacksSection>.findAttackForActionmatches a projected action back to its authored attack by name — that collection's own identity rule, and the onesameTraitrelies on. Both module seams fail soft and identically: no module, no formatter, no matching attack, a formatter returning null/empty, or one that throws all collapse to the attack's bare name. "Edit attack" is a separate menu item from "Edit action", because editing the derivative would be silently undone by the next projection. - [x] A4 — System 8 implements both (vtt-modules
27612e0), so the platform seams have a real consumer and Attacks gains its Add button.AttacksListwas deliberately not reused — itsis5ebranch is the leak CS-9 removes.
N9 — Build-mode affordances for the sections that have none
Found during the 0.46.0 acceptance pass: Feats and Powers rendered identically in Play and Build.
Only <MetadataSection> and <AttributeGroupSection> responded to editMode. That was a hole in the
plan rather than a missed task — 1b.7–1b.11 each shipped "read mode" on purpose, and R5–R11 built the
mechanism but wired only two sections to it.
- [x] Traits — slots and remove. CS-1.3's
slotSchedulehad been declared and contract-tested since the Character Sheet API landed, with no consumer — so 1b.9's promise needed a reader, not a new module surface. Over-capacity traits keep slots of their own. The storage seam already existed:registerNameKeyedCollectionRoutesis a platform convention over a platform type, so no Module API change and no CS-8applyEdit. - [x] Traits — the remove bug the live test caught. Every trash click silently did nothing. CS-2
requires globally-unique trait slugs, so System 8's projection emits
alert-featwhile the stored element's slug isalert; comparing them matched nothing and the guardreturned without a word. Fixed by matching on name through a sharedsameTrait— these are name-keyed collections, so the name is the identity — and the no-match path now warns and notifies instead of failing invisibly. - [x] Traits — add, using the old sheet's
AddFeatModal/AddPowerModalper the user's call: they already do catalogue search, filters, rank/level validation and slot-aware "add and continue". Two entry points — an empty slot's+fills that scheduled slot; a section-level Add creates a new unleveled slot. Over-capacity is deliberate (GM fiat, or correcting a character when the system's own slot data is wrong); Phase 2 validation flags the excess rather than preventing it. - [x] Traits — rearrange, as up/down controls rather than drag: keyboard- and touch-reachable, and
it composes with a positional slot model without pulling
@dnd-kitinto a row that already carries expand, move, remove and add. Reorder cannot go throughsyncCharacterCollection— a permutation holds the same slugs with the same contents, so that helper issues zero requests and the change silently does not happen. Reordering PATCHes the whole array instead, which the patch guard allows because a lone array key is a legitimate whole-collection write. A modelling limit, flagged rather than faked: the collection is dense, so order is the slot assignment. A trait cannot occupy the level-4 slot while level-2 stands empty. - [x] Inventory — Build-mode add, reusing
AddEquipmentModal. Equipment is not a character collection: it has its own table and its own REST resource, so this does not go throughsyncCharacterCollection. The add lives in<InventorySection>rather than<CharacterSheet>, because the section already holds thecharacterIdthe equipment API is keyed by. CC-1's guard caught the obvious wiring — the picker branches onsystemId, which is banned fromCharacterSheet.tsx, so it comes from the campaign instead, which is also the truer source. Not covered: removing or reordering equipment. - [x] Lift the Add pickers above the sidebar that opens them. All three opened behind the
Character Context: Mantine's default modal
zIndexis 200 and the platform'sSIDEBARlayer is 600. The three modals gained azIndexprop (ui-componentscannot import the platform'sZ_INDEX) and both call sites passZ_INDEX.MODAL.AddEquipmentModalrenders two<Modal>s, so a test counts them against forwarded props to stop the pair drifting. Exactly the caseCLAUDE.md's standing z-index rule names. - [x] Notes / Options — plain field edits. Build mode subsumed the old sheet's raw/preview toggle rather than porting it: Play is the preview, Build is the source. The trap in both was the empty state — each short-circuits to "No notes." / "No options set.", which is right for reading and exactly wrong for building, so both editors sit above the empty guard and a test pins the ordering. Notes commit on blur; the colour commits on change-END, since dragging through the picker's gradient would otherwise be a request per pixel.
- [x] The action
⋯menu, back on every action row. Editing and deleting an action had been unreachable from the overhauled sheet in both contexts — the Character Context lost it at N4, the Impersonated Token Context at 1e.1 — but nothing was ever broken except the control.<ActionButtonRow>'s documentedtrailingslot now carries a shared<ActionMenu>, and a test asserts none of the three action-rendering sections declares its own<Menu>. One rule from the old inline menu is kept and now has a test: asource: 'campaign'action gets Info but no Edit or Delete, because deleting it from one sheet would take it from every other character using it.
Not built, deliberately: an Add for Attacks was left out of N9 because attacks reach the section through the projection, not from a list the sheet owns — Phase A is what answered it.
Server-side power derivation
- [x] Derive power actions on the server, for every writer. Powers only got their real actions
when a client happened to PATCH a character with
updates.powers— the one trigger, inuseCharacters. Every other writer produced powers with none: the sub-entity collection routes (which N9's Add uses), imports, generators. The failure is invisible, which is why it survived — CS-6 emits a generic action per power and the sheet falls back to it, so an underived power expands to a convincing placeholder rather than to nothing. Characters looked right and rolled wrong. What kept this client-side was ref resolution, not the derivation: the route's own comment said the server "has no access to the cached system-data proxy result". That comment was stale —systemDataProxygained a 24h server cache ofpowers.json, soloadPowerRefscan resolve them server-side. Derivation now fires from the powers collection routes themselves, after both add and update, so it covers every writer including ones not written yet. It runs after the response is sent (deriving is one HTTP request per action document against an external wiki), it never fails the write that followed it, and a wiki outage returning zero actions leaves existing data alone rather than clearing it. The derive loop moved intoderivePowersForCharacterso the route and the endpoint share one implementation — two copies would drift on the stat maths, and the same power would then roll differently depending on which path derived it. - [x] Let a backfill be
{ force: true }and nothing else.systemBaseUrland the fullpowers[]array are now optional, since the server can resolve refs itself. Supplying them still works and still takes precedence, for a caller deriving a specific subset or pointing at a different wiki.
Phase 1e — Sunset (partial)
- [x] 1e.1 — Retire the interim Token Context edit mode. v0.36.0's pencil toggle in
TokenSheetgoes;AdvancedTokenSettingsand the tokenPermissionsPanelare already token sections in the new sheet's derivation. Six test files repointed. The obituary-as-corpse trap struck a third time — two tests searched the context's source for<TokenSheet>/AdvancedTokenSettingsand found the comments explaining their removal; comment-stripped, as in R7 and R11.<TokenSheet>now has no consumer at all.
1e.2 (point all routes at the new sheet), 1e.3 (delete the old sheet) and 1e.4 (dead styles) carried into v0.47.0.