Skip to content

v0.105.0 — New character sheet: foundations

Shipped 2026-09-21 — built on version/0.105.0, twelve commits; the merge hash and tag are in the changelog. The fifteenth of the releases the near-term roadmap sequences from the August 2026 heuristic evaluation; the roadmap page is foundations, part one of the new character sheet, whose page carries the design. Module API 1.39.0; no module release. The developer page is Character Sheet Renderer.

System-Platform Separation round 2. The Character Sheet API has the platform render a sheet from a description the module supplies — which is why every sheet looks like the platform rather than like its system. This release builds the seam that inverts it: a module may ship the sheet component itself, built from a kit the platform exports, writing only through the platform's one write path. Nothing user-visible changes except the dead sheet code going, the Sheet context existing (the quick view until v0.106.0), and the ability-score fix. Decisions NS-D1, D2, D5, D6, D7, D9 and D10 bind.

The seam

  • Module API 1.39.0 (NS.3) — CharacterSheetRendererExtension through registerCharacterSheetRenderer from the client entry (pages, supports.print), SystemModule.hasSheetRenderer on the server, SheetRendererProps, SheetApi, SheetEditIntent (the CS-8 four plus set-field, set-runtime, set-metadata under the module's namespace — NS-D7 — and patch), RecentChange. Additive; a module that registers none gets the platform's sheet.
  • The host (NS.3) — SheetHost: the frame (the gated Build toggle, History, the ⋯ menu's zoom and Export PDF, the sheet as its own scroller), the kit handed in, the module's CS-9 pickers mounted on api.openPicker, the fallback to the quick view. The Sheet context ('sheet'), the route /characters/:id/sheet, and three entries: the character page's ⋯ → Sheet view, the Character context's header, the token menu's View Character Sheet (a new tab since v0.33.0; now the panel beside the map).
  • SheetApi (NS.4, NS.5) — edit (the module's validateEdit and applyEdit first, the platform's fields after), roll (the chat seam), runAction (the impersonated-token executor), sendMessage, openPicker, registerField, campaign.configuration (v0.102.0), treasury.holdings / .carrying (v0.103.0), can, notify, damage.apply / .prompt (v0.78.0's helpers).
  • The recent-changes feed (NS.4, NS-D6) — every runtime write the platform makes, recorded with its source and handed to the renderer for a window: approved and auto-approved damage, healing, conditions, rests, resource pools, the API's own writes, and a diff of consecutive records for what arrived from elsewhere. The module decides how to show it.
  • The field registry (NS.4) — every field a stable id and a reading order; the host moves Tab by order and can focus a field on request; the sheetFocusStore is the door the v0.108.0 walkthrough will knock on.

The kit

  • @purple-cat/sheet-kit (NS.2) — the layout family (Sheet with tabs as a table of contents and the print rules, Page as a CSS grid keyed from a PageLayout and reflowing below its width, Region, Field, Checkbox, Lines, Portrait) and the editing family (EditableField with saving / saved / failed, FieldEditor — opens on focus, closes on blur or Escape, traps focus — NumericStepper, Rollable, ActionButton, ValidationMark, RestButton, ResourcePips); skin and zBand over the platform's tokens; the field-registry contract; the geometry from a layout.json (pageLayoutFromFields, regionRect) and its guard (checkGeometry, measureRegions). Under jsdom, with the export guard and a coverage ratchet.
  • Print (NS.6) — the same component in print mode through the browser's print path; the kit's rules snap pages and hide controls; the host's stylesheet hides the app around the sheet.
  • The fixture (NS.8) — @purple-cat/sheet-kit/fixture's two-page, ten-field FixtureSheet for the fixture-system, every feature once, run end to end against a recording API; the registry round-trip pinned in the web app; the sheet route at Playwright's auth boundary.

The house-keeping

  • NS.0 — the ability-score rule (NS-D10). The +/− already moved the declared step component; the typed total and the advanced rows still moved base. Now a typed total lands in the step component (floored at zero), base is read-only in the breakdown once a module names what + moves, and the row says Set at creation — Regenerate to change.
  • NS.1 — the inventory of system-specific code still in apps/web, each site with its destination, on the developer page.
  • NS.5 — the conventional keys. The platform reads startingLevel, abilityScoreMethods, rolledRerolls and pointBuyBudget from a campaign's configuration without naming a system: a new character begins at the campaign's starting level; the generation modal offers the methods the table allows.
  • NS.7 — CS-15.12 at last. PlayModeView and its 26 tests, the classic CharacterDetail, TokenSheet/, CombatStats and the siblings only they used — 7,824 lines nothing imported. Twenty-six readFileSync guards judged (not fifteen: two in server-core read the web source), each re-anchored to the live surface or deleted with a note; combatStats.ts reduced to propKey; the systemLiterals baseline 13 → 11 files.
  • NS.9 — guards. The seam's surfaces name no system; zod for the registration, RecentChange and SheetEditIntent, validated at the registry and the write path, with three new parity pairs.

Traps met on the way

  • shared is compiled without the DOM lib by the server: a HTMLElement in a shared type broke server-core's build. The seam types object.
  • A guard in packages/server-core read apps/web source (the v0.88.0 trap, twice more).
  • Mantine's Popover marks its dropdown inaccessible under jsdom until a transition that never runs: query with hidden: true.
  • A FieldEditor's stepper and a runtime NumericStepper both say Increase: scope the query to the editor.
  • The hidden Chrome tab renders neither Mantine popovers nor the desktop sidebar: the route was verified live, the panel by contract.